【C++】C++的拷贝控制

目录结构:ios

contents structure [-]


定义一个类,会显式或隐式指定此类型的对象拷贝、移动、赋值和销毁时作什么。类经过定义五种特殊的成员函数来控制这些操做,包括:拷贝构造函数(copy constructor)、拷贝赋值运算符(copy-assignment operator)、移动构造函数(move constructor)、移动赋值运算符(move-assignment operator)和析构函数(destructor)。

其中拷贝构造和移动构造器定义了当用同类型的另外一个对象初始化本对象时的行为。而拷贝赋值和移动赋值定义了将一个对象赋予同类型的另外一个对象时的行为。析构函数定义了此类型对象销毁时作什么。这些统称为拷贝控制操做。

对于没有显式定义这些成员的,编译器会自动定义默认的版本。一些类必需要本身定义拷贝控制成员,另一些则不须要。因此,什么时候须要本身去定义就考察程序员的功底了。git

 

1 拷贝、赋值与销毁

移动语义是C++11新引入的,事后再谈。程序员

1.1 拷贝构造函数

仅有一个参数为自身类类型引用的构造函数就是拷贝构造函数,形如:github

class Foo{
public:
    Foo();                //默认构造函数
    Foo(const Foo&);    //拷贝构造函数
}

该参数必须是引用类型,通常是const引用。因为拷贝构造函数会在几种状况下隐式地调用,因此通常不是explicit。

若是本身不定义,编译器就会合成一个默认的(合成拷贝构造函数)。合成的拷贝构造函数会把参数成员逐个拷贝到正在建立的对象中(非static成员)。

成员的类型决定了拷贝的方式:类类型的成员会用它本身的拷贝构造函数来拷贝;内置类型则直接值拷贝。数组会逐个复制,若是数组成员是类类型,会逐个调用成员自己的拷贝构造函数。算法

class Sales_data{
public:
    Sales_data(const Sales_data&);
private:
    std::string bookNo;
    int units_sold = 0;
    double revenue = 0.0;
};

//定义Sales_data的拷贝构造函数,与Sales_data的合成拷贝构造函数等价
Sales_data::Sales_data(const Sales_data &orig) :
    bookNo(orig.bookNo), //使用string的拷贝构造函数
    units_sold(orig.units_sold),    //拷贝orig.units_sold
    revenue(orig.revenue)    //拷贝orig.revenue
    {}    //空函数体

 

1.1.1 拷贝初始化


拷贝初始化和直接初始化的差别:

数组

string dots(10,',');    //直接初始化
string s(dots);            //直接初始化
string s2 = dots;        //拷贝初始化
string null_book = "9-999-99999-9";    //拷贝初始化
string nines = string(100, '9');    //拷贝初始化

拷贝初始化通常由拷贝构造函数完成,之因此说通常是由于移动语义的引入,致使若是类由移动构造函数时,拷贝初始化有时会使用移动构造函数而非拷贝构造函数。

拷贝初始化不只在用=定义变量时发生,在下列情形也会发生:
将一个对象做为实参传递给一个非引用类型的形参
从一个返回类型为非引用类型的函数返回一个对象
用花括号列表初始化一个数组中的元素或一个聚合类中的成员
某些类类型还会对它们所分配的对象使用拷贝初始化。如初始化标准库容器或调用其insert或push成员时,容器会对其元素进行拷贝初始化。而emplace建立的元素都是直接初始化。安全

1.1.2 参数和返回值

拷贝构造函数被用来初始化非引用类类型参数,因此拷贝构造函数自身的参数必须是引用类型。否则的话,就两者矛盾而无限循环了。

为了调用拷贝构造函数,咱们必须拷贝它的实参,但为了拷贝实参,咱们又须要调用拷贝构造函数。函数

 

1.2 拷贝赋值运算符

Sales_data trans, accum;
trans = accum;    //使用Sales_data的拷贝赋值运算符

若是类未定义,编译器会合成一个(合成拷贝赋值运算符)。

这个函数的定义涉及了重载运算符的概念,这里重载的是赋值运算符。

重载运算符本质上是函数,名字由operator关键字接要定义的运算符符号组成。因此,赋值运算符就对应operator=的函数。

重载运算符的参数表示运算符的运算对象,某些运算符包括赋值必须定义为成员函数。若是一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数。对一个二元运算符,例如赋值运算符,右侧运算对象做为显式参数传递。

拷贝赋值运算符接受一个与其所在类相同类型的参数:性能

class Foo{
public:
    Foo &operator=(const Foo&);    //赋值运算符
    // ...
}

为了与内置类型的赋值保持一直,赋值运算符一般返回一个指向其左侧运算对象的引用。另外,标准库一般要求保存在容器中的类型具备赋值运算符,且返回值是左侧运算符对象的引用。

编译器合成的拷贝赋值运算符相似拷贝构造,也是逐一进行成员拷贝(非static),类类型经过它自身的拷贝赋值运算符来完成,数组成员为类类型的,也会逐一调用自身的拷贝赋值运算符。最后,返回一个指向左侧运算对象的引用。优化

//等价于合成拷贝赋值运算符
Sales_data& Sales_data::operator=(const Sales_data &rhs)
{
    bookNo = rhs.bookNo;    //调用string::operator=
    units_sold = rhs.units_sold;    //使用内置的int赋值
    revenue = rhs.revenue;    //使用内置的double赋值
    return *this;    //返回左侧对象的引用
}

 

1.3 析构函数

与构造执行的操做相反。

析构函数名字比构造函数多了一个~。没有返回值,也没有参数。

class Foo{
public:
    ~Foo();    //析构函数
    ...
};

析构函数不能被重载,是唯一的。

调用析构的时机:
变量在离开做用域时被销毁
当一个对象被销毁时,其成员被销毁
容器被销毁时(标准库容器或数组),其元素被销毁
动态分配的对象,当对指向它的指针应用delete时被销毁
临时对象,当建立它的完整表达式结束时被销毁

{//新做用域
    //p和p2指向动态分配对象
    Sales_data *p = new Sales_data;//p是一个内置指针
    auto p2 = make_shared<Sales_data>();    //p2是一个shared_ptr
    Sales_data item(*p);    //拷贝构造函数将*p拷贝到item中
    vector<Sales_data> vec;    //局部对象
    vec.push_back(*p2);        //拷贝p2指向的对象
    delete p;                //对p指向的对象执行析构函数
}//退出局部做用域;对item、p2和vec调用析构函数
//销毁p2会递减其引用计数;若是引用计数变为0,则对象释放
//销毁vec会销毁它的元素

若是类未定义析构,则编译器会自动合成(合成析构函数)。

class Sales_data{
public:
    //成员会被自动销毁,除此以外不须要作其余事情
    ~Sales_data(){}
    //其余成员的定义
    ...
};

析构函数体(空)执行完毕后,成员会被自动销毁。本例中string的析构函数会被调用,释放bookNo的内存。析构函数体自己不直接销毁成员,它们是在函数体以后隐含的析构阶段中被销毁的。析构函数体只是析构过程的一部分。

 

2 三五法则

这里解释一下三五法则(分别是Three Rule,Five Rule)。Three Rule指的是定义的类有拷贝构造函数,拷贝赋值运算符,和析构函数。而Five Rule就是除了前面的三种,还有移动赋值运算符,移动构造函数。

这里是一个Five Rule的案例:

class rule_of_five
{
    char* cstring; // raw pointer used as a handle to a dynamically-allocated memory block
 public:
    rule_of_five(const char* s = "")
    : cstring(nullptr)
    { 
        if (s) {
            std::size_t n = std::strlen(s) + 1;
            cstring = new char[n];      // allocate
            std::memcpy(cstring, s, n); // populate 
        } 
    }
 
    ~rule_of_five()
    {
        delete[] cstring;  // deallocate
    }
 
    rule_of_five(const rule_of_five& other) // copy constructor
    : rule_of_five(other.cstring)
    {}
 
    rule_of_five(rule_of_five&& other) noexcept // move constructor
    : cstring(std::exchange(other.cstring, nullptr))
    {}
 
    rule_of_five& operator=(const rule_of_five& other) // copy assignment
    {
         return *this = rule_of_five(other);
    }
 
    rule_of_five& operator=(rule_of_five&& other) noexcept // move assignment
    {
        std::swap(cstring, other.cstring);
        return *this;
    }
 
// alternatively, replace both assignment operators with 
//  rule_of_five& operator=(rule_of_five other) noexcept
//  {
//      std::swap(cstring, other.cstring);
//      return *this;
//  }
};

更详细的能够查看:https://en.cppreference.com/w/cpp/language/rule_of_three

 

须要析构函数的类也须要拷贝和赋值操做。
由于析构函数须要去手工delete成员指针。这种状况下,编译器合成的拷贝构造和赋值运算符就会有问题,由于仅仅只是完成了浅拷贝,拷贝了成员指针的地址值,这可能引发问题。因此这种状况咱们要本身写深拷贝代码。

须要拷贝操做的类也须要赋值操做,反之亦然
由于语义上拷贝构造和赋值操做是一致的,只是调用时机不一样。提供了一个就说明须要特化某些操做,那么对应的另外一个也要一致。但须要两者却不必定须要一个析构。

=default
=default能够显式地要求编译器生成合成的版本。

class Sales_data{
public:
    Sales_data() = default;
    Sales_data(const Sales_data&) = default;
    Sales_data &operator=(const Sales_data &);
    ~Sales_data() = default;
    //其余成员
    ...
};
Sales_data &Sales_data::operator=(const Sales_data&) = default;

类内使用=default声明,合成的函数会隐式地声明为inline。

=delete
有些状况咱们但愿阻止类的拷贝或赋值。好比iostream就阻止了拷贝,避免多个对象写入或读取相同的IO缓冲。

struct NoCopy{
    NoCopy() = default;    //合成的默认构造函数
    NoCopy(const NoCopy&) = delete;    //阻止拷贝
    NoCopy& operator=(const NoCopy&) = delete;    //阻止赋值
    ~NoCopy() = default;
};

=delete通知编译器,不但愿定义这些成员。

注意,析构函数不能删除,其余任何函数均可以指定=delete。虽然语法上容许析构函数指定=delete,但这样一来涉及到该类的对象都不能用,由于它没法销毁。

因此,记着析构函数不能加=delete这条软规则便可。

若是一个类有数据成员不能默认构造、拷贝、复制或销毁,那么对应的成员函数将被定义为删除的。这就意味着,composite模式的数据成员自身残疾将影响整个团队残疾。

具备引用成员或没法默认构造的const成员的类,编译器不会合成默认构造函数。若是类有const成员,则它不能使用合成的拷贝赋值运算符(新值是不能给const对象的)。

在没有=delete以前,C++是经过private权限限制拷贝构造函数和拷贝赋值运算符来阻止拷贝的。这种方法有一个疏漏,就是友元函数和成员函数是能够进行拷贝的。

 

3 拷贝控制和资源管理

类一旦管理了类外资源,每每就须要自定义析构,根据三五法则也就意味着要自定义拷贝构造和拷贝赋值运算符。

而定义拷贝控制成员时,首先要肯定类的拷贝语义,咱们是让类的行为看起来像值仍是像指针。

若是是像值,好比string、标准库容器类等,它们的拷贝会使得副本对象和原对象彻底独立,改变副本不会影响原对象。
若是是像指针,好比shared_ptr,那么拷贝的就是指针,指向的是同一个对象。
固然,也能够设置为不容许拷贝或赋值,此时既不像值也不像指针。

行为像值的类

class HasPtr{
public:
      HasPtr(const std::string &s = std::string()):ps(new std::string(s)), i(0){}
      //对ps指向的string,每一个HasPtr对象都有本身的拷贝
      HasPtr(const HasPtr &p):ps(new std::string(*p.ps)), i(p.i){}
      HasPtr& operator=(const HasPtr &);
      ~HasPtr(){delete ps;}
private:
      std::string *ps;
      int i;
};

HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
      //这里必定要先new再delete,由于赋值操做赋值给本身是合法的
      //若是赋值给本身,先delete意味着rhs.ps就丢了
    auto newp = new string(*rhs.ps);    //拷贝底层string
      delete ps;    //释放旧内存
      ps = newp;    //从右侧运算对象拷贝数据到本对象
      i = rhs.i;
      return *this;    //返回本对象
}

赋值运算符要谨记一个好习惯,在销毁左侧运算对象资源以前先拷贝右侧运算对象资源。

行为像指针的类

class HasPtr{
public:
      //构造函数分配新的string和新的计数器,将计数器置为1
      HasPtr(const std::string &s = std::string()):ps(new std::string(s)), i(0), use(new std::size_t(1)){}
      //拷贝构造函数拷贝全部3个数据成员,并递增计数器
      HasPtr(const HasPtr &p):ps(p.ps), i(p.i), use(p.use){++*use;}
      HasPtr& operator=(const HasPtr&);
      ~HasPtr();
private:
      std::string *ps;
      int i;
      std::size_t *use;    //用来记录有多少个对象共享*ps的成员
};

HasPtr::~HasPtr()
{
    if(--*use == 0){    //若是引用计数变为0
        delete ps;        //释放string内存
          delete use;        //释放计数器内存
    }
}

HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
    ++*rhs.use;    //递增右侧运算对象的引用计数
      if(--*use == 0){    //而后递减本对象的引用计数
        delete ps;        //若是没有其余用户
          delete use;        //释放本对象分配的成员
    }
      ps = rhs.ps;    //将数据从rhs拷贝到本对象
      i = rhs.i;
      use = rhs.use;
      return *this;    //返回本对象
}

赋值运算符要考虑自赋值的状况,因此在左侧递减引用计数以前先递增右侧引用计数。

 

4 交换操做

除了拷贝控制成员外,管理资源的类通常还定义一个swap函数。对与重排元素顺序的算法一块儿使用的类来讲,swap很是重要,由于这些算法交换两个元素时会调用swap。

若是类本身定义了swap,算法就使用自定义版本,不然使用标准库定义的swap。

class HasPtr{
    friend void swap(HasPtr&, HasPtr&));
      //其余成员定义
      ...
};

inline void swap(HasPtr &lhs, HasPtr &rhs)
{
    using std::swap;
      swap(lhs.ps, rhs.ps);    // 交换指针,而不是string数据
      swap(lhs.i, rhs.i);        // 交换int成员
}

swap不是必要的,但对分配了资源的类来讲,定义swap是一种很重要的优化手段。

swap定义的一个坑:

//Foo有类型为HasPtr的成员h
void swap(Foo &lhs, Foo &rhs)
{
    //错误:这个函数使用了标准库版本的swap,而不是HasPtr版本
      std::swap(lhs.h, rhs.h);
      // 交换类型Foo的其余成员
}

//正确的写法:
void swap(Foo &lhs, Foo &rhs)
{
    using std::swap;
      swap(lhs.h, rhs.h);    //使用HasPtr版本的swap
      //交换类型Foo的其余成员
}

这种未加限定的写法之因此可行,本质上是由于类型特定的swap版本匹配程度优于声明的std::swap版本。而对std::swap的声明可使得在找不到类型特定版本时能够正确的找到std中的版本。

swap经常使用于赋值运算符,它能够一步到位完成拷贝并交换的技术。

//注意rhs是按值传递的,意味着HasPtr的拷贝构造函数将右侧运算对象中的string拷贝到rhs
HasPtr& HasPtr::operator=(HasPtr rhs)
{
    //交换左侧运算对象和局部变量rhs的内容
      swap(*this, rhs);    //rhs如今指向本对象曾经使用的内存
      return *this;    //rhs被销毁,从而delete了rhs中的指针
}

这里的参数不是引用,右侧运算对象是值传递,因此rhs是右侧运算对象的副本。所以直接swap就一步到位了,自动销毁rhs时就自动销毁了原对象(执行析构)。

使用拷贝和交换的赋值运算符天生异常安全,且能正确处理自赋值。

 

5 对象移动

C++11引入了一个特性:能够移动而非拷贝对象。移动而非拷贝对象会大幅度提高性能。

旧版本即便在没必要拷贝对象的状况下,也不得不拷贝,对象若是巨大,那么拷贝的代价是昂贵的。在旧版本的标准库中,容器所能保存的类型必须是可拷贝的。但在新标准中,能够用容器保存不可拷贝,但可移动的类型。

标准库容器、string和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类能够移动但不能拷贝。

 

5.1 右值引用

为了支持移动操做,C++11引入了一个新的引用类型——右值引用(rvalue reference)。所谓右值引用就是必须绑定到右值的引用。经过&&来得到右值引用(左值引用是经过&)。右值引用只能绑定到一个将要销毁的对象。所以,才得以自由地将一个右值引用的资源转移给另外一个对象。

int i = 42;    
int &r = i;        //正确:r引用i,r是左值引用
int &&rr = i;    //错误:右值引用不能绑定到左值上
int &r2 = i*42;    //错误:i*42是右值
const int &r3 = i*42;    //正确:能够将一个const引用绑定到一个右值上
int &&rr2 = i*42;    //正确:将rr2绑定到乘法结果上

最特别的就是const左值引用是能够绑定到右值的。

变量表达式都是左值,因此不能将一个右值引用直接绑定到一个变量上,即便这个变量的类型是右值引用也不行。

int &&rr1 = 42;        //正确:字面常量是右值
int &&rr2 = rr1;    //错误:表达式rr1是左值

左值是持久的,右值是短暂的。

5.2 标准库move函数

虽然右值引用不能绑定到左值,但能够显式地将左值转换为对应的右值引用类型。调用move函数能够得到绑定在左值上的右值引用,此函数定义在头文件utility中。

int &&rr3 = std::move(rr1);    //正确

move告诉编译器:咱们有一个左值,但咱们但愿像一个右值同样处理它。但使用move就意味着承诺:除了对rr1赋值或销毁它外,咱们将再也不使用它。

能够销毁一个移后源对象,也能够赋予它新值,但不能使用移后源对象的值。

调用move函数的代码应该使用std::move而非move,这样作能够避免潜在的名字冲突。

 

5.3 移动构造函数

移动构造函数相似拷贝构造,第一个参数是该类类型的引用。不一样于拷贝构造函数,这个引用参数在移动构造函数中是一个右值引用。其余任何额外参数都必须有默认值(与拷贝构造一致)。

除了完成资源移动,移动构造函数还要保证移后源对象处于一个状态:销毁它是无害的。移动以后,源对象必须再也不指向被移动的资源,这些资源归新对象全部。

StrVec::StrVec(StrVec &&s) noexcept    //移动构造不该该抛任何异常
  //成员初始化器接管s中资源
  : elements(s.elements), first_free(s.first_free), cap(s.cap)
{
    //令s进入这样一个状态————对其运行析构函数是安全的
    s.elements = s.first_free = s.cap = nullptr;
}

移动构造函数不会分配任何新内存,它接管给定的StrVec的内存。接管以后,源对象的指针置nullptr。

移动操做一般不分配任何资源,所以移动操做一般不抛出任何异常。而经过noexcept能够通知标准库构造函数不会抛出异常,若是不通知,那么标准库会认为移动构造函数可能会抛出异常,为此会作一些额外的工做。

为何要指出移动操做不抛出异常呢?由于标准库能对异常发生时其自身的行为提供保证,好比vector保证push_back时发生异常不会改变vector自己。

之所不异常时不改变vector,是由于拷贝构造函数中发生异常时,旧元素的内存空间是没有变化的,至于新内存空间尽管发生了异常,vector能够直接释放新分配的内存(还没有成功构造)并返回,这不会影响vector原有的元素。但移动语义就不一样,若是移动了部分元素时发生了异常,那么这时源元素就已经被改变了,这就没法知足自身保持不变的要求了。

因此除非vector知道元素类型的移动构造函数不会抛异常,不然在从新分配内存时,它必须使用拷贝构造而不是移动构造。基于此,若是但愿vector从新分配内存时可使用自定义类型对象的移动操做而不是拷贝操做,那就要显式的声明咱们的移动构造函数是noexcept的。

 

5.4 移动赋值运算符

相似移动构造,若是不抛出任何异常,也要标记为noexcept。

 

StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
    //直接检测自赋值
      if(this != &rhs){
        free();    //释放已有元素
          elements = rhs.elements;    /从rhs接管资源
        first_free = rhs.first_free;
          cap = rhs.cap;
          //将rhs置于可析构状态
          rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }
      return *this;
}

 

5.5 合成的移动操做

若是本身不定义,编译器也会自动合成移动操做,但这和拷贝操做不一样,它须要一些条件。

若是一个类定义了本身的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会合成移动操做。

只有当一个类没有定义任何本身版本的拷贝控制成员,且类的每一个非static数据成员均可以移动时,编译器才会为它合成移动构造和移动赋值运算符。

//编译器为X和hasX合成移动操做
struct X{
    int i;    //内置类型能够移动
      std::string s;    //string定义了本身的移动操做
};
struct hasX{
    X mem;    //X有合成的移动操做
};
X x, x2 = std::move(x);    //使用合成的移动构造函数
hasX hx, hx2 = std::move(hx);    //使用合成的移动构造函数

与拷贝操做不一样,移动操做永远不会被隐式定义为删除的函数。但若是显式地要求编译器生成=default的移动操做,且编译器不能移动所有成员,则移动操做会被定义为删除的函数。

定义了移动构造或移动赋值的类也必须定义本身的拷贝操做,不然拷贝操做默认被定义为删除的。

若是类既有移动构造,也有拷贝构造,那么编译器使用普通的函数匹配规则来肯定使用哪一个构造函数。赋值也相似。

StrVec v1, v2;
v1 = v2;    //v2是左值,使用拷贝赋值
StrVec getVec(istream &s);    //getVec返回一个右值
v2 = getVec(cin);    //getVec(cin)是一个右值,使用移动赋值

若是类有拷贝构造,但没有移动构造,函数匹配规则会保证该类型的对象会被拷贝:

class Foo{
public:
      Foo() = default;
      Foo(const Foo&);
      ...
};
Foo x;
Foo y(x);    //拷贝构造函数,x是左值
Foo z(std::move(x));    //拷贝构造函数,由于未定义移动构造函数

在未定义移动构造的情境下,Foo z(std::move(x)之因此可行,是由于咱们能够把Foo&&转换为一个const Foo&。

五个拷贝控制成员应该当成一个总体来对待。若是一个类须要任何一个拷贝操做,它就应该定义全部五个操做。

C++11标准库定义了移动迭代器(move iterator)适配器。一个移动迭代器经过改变给定迭代器的解引用运算符的行为来适配此迭代器。移动迭代器的解引用运算符返回一个右值引用。调用make_move_iterator函数能将一个普通迭代器转换成移动迭代器。原迭代器的全部其余操做在移动迭代器中都照常工做。

最好不要在移动构造函数和移动赋值运算符这些类实现代码以外的地方随意使用move操做。std::move是危险的。

 

5.6 右值引用和成员函数

在非static成员函数的形参列表后面添加引用限定符(reference qualifier)能够指定this的左值/右值属性。引用限定符能够是&或者&&,分别表示this能够指向一个左值或右值对象。引用限定符必须同时出如今函数的声明和定义中。

class Foo
{
public:
    Foo &operator=(const Foo&) &; // 只能向可修改的左值赋值
    // 其余成员
};

Foo &Foo::operator=(const Foo &rhs) &
{
    // 执行将rhs赋予本对象所需的工做
    return *this;
}

一个非static成员函数能够同时使用const和引用限定符,此时引用限定符跟在const限定符以后。

class Foo
{
public:
    Foo someMem() & const;      // error
    Foo anotherMem() const &;   // ok
};

引用限定符也能够区分红员函数的重载版本。

class Foo
{
public:
    Foo sorted() &&;        // 可用于可改变的右值
    Foo sorted() const &;   // 可用于任何类型的Foo
      //Foo其余成员
private:
      vector<int> data;
};

//本对象为右值,所以能够原址排序
Foo Foo::sorted() &&
{
    sort(data.begin(), data.end());
      return *this;
}
//本对象是const或是一个左值,哪一种状况咱们都不能对其进行原址排序
Foo Foo::sorted() const &{
    Foo ret(*this);
      sort(ret.data.begin(), ret.data.end());
      return ret;
}

retVal().sorted();    //retVal()是右值,调用Foo::sorted() &&
retFoo().sorted();    //retFoo()是左值,调用Foo::sorted() const &

若是定了两个或两个以上具备相同名字和相同参数列表的成员函数,要么都加引用限定符,要么都不加,这一点不受const this的影响。

class Foo
{
public:
    Foo sorted() &&;
    Foo sorted() const;    // 错误:必须加上引用限定符
    // Comp是函数类型的类型别名
    // 此函数类型能够用来比较int值
    using Comp = bool(const int&, const int&);
    Foo sorted(Comp*);          // 正确:不一样的参数列表
    Foo sorted(Comp*) const;    //正确:两个版本都没有引用限定符
};

 

原文连接:

https://r00tk1ts.github.io/2018/11/29/C++%20Primer%20-%20%E6%8B%B7%E8%B4%9D%E6%8E%A7%E5%88%B6/

相关文章
相关标签/搜索