拷贝构造和拷贝赋值

1. 拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。例如我们有如下类框架:

class HasPtr{
public:
    HasPtr(const std::string &s = std::string()):
        ps(new std::string(s)), i(0) { }
    
    //拷贝构造函数,其中动态分配了一个新的string,并将对象拷贝到ps指向的位置
    //这里涉及到string的new构造方法,即动态创建一个堆内存,该内存保存hp.ps保存的值,并将ps设置为指向当前新开辟内存,这样就不会出现当前对象的ps和传入的参数同时指向一个内存地址的情况
    HasPtr(const HasPtr& hp):
        ps(new std::string(*hp.ps)), i(hp.i) {}
private:
    std::string *ps;
    int i;
}

拷贝构造函数在很多情况下都会被隐式使用,所以其不应该是explicit的。同时,其第一个参数必须是引用类型,否则其调用永远不会成功。假设该参数不是引用,而是传值参数,

HasPtr(const HasPtr hp):   

那么拷贝构造时

HasPtr hp1;
HasPtr hp2(hp1); //拷贝构造

此时hp1是实参,将会传值给形参hp,而此时相当于调用HasPtr hp(hp1),此时编译器又会调用类的拷贝构造函数,导致这一过程无限继续下去,使得调用无法成功。

拷贝构造会在以下情况发生:

  • 拷贝初始化(用=初始化变量)
  • 将一个对象作为实参传递给一个非引用类型的形参
  • 从一个返回类型为非引用类型的函数返回一个对象
  • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
  • 某些类类型还会对它们所分配的对象使用拷贝初始化,例如初始化标准库容器或者调用其insertpush成员时。(与之相对的是,用emplace创建的元素都进行直接初始化)。

2. 拷贝赋值运算符

与类控制其对象如何初始化一样,类也可以控制其对象如何赋值。

HasPtr hp1, hp2;
hp1 = hp2; //使用拷贝赋值运算符

为了实现自定义的拷贝赋值运算符,我们需要重载赋值运算符,其本质上就是一个函数,接受一个与其所在类相同类型的参数,如下形式:

class HasPtr {
public:
    HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) { }
    HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) { }

    //重载赋值运算符实现拷贝赋值运算符,返回引用
    HasPtr& operator=(const HasPtr &rhs_hp) {
        if(this != &rhs_hp){
            //在堆上新开辟内存,保存hp.ps的内容,并返回一个指向当前位置的指针
            std::string *temp_ps = new std::string(*rhs_hp.ps);
            delete ps; //这里销毁了ps指向的内存,而ps指针还存在
            ps = temp_ps; //将当前对象的ps设置为temp_ps
            i = rhs_hp.i;
        }
        return *this; //返回自身类类型的引用
    }
private:
    std::string *ps;
    int i;
};

为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用。

3. 合成拷贝构造函数

如果我们没有为一个类定义拷贝构造函数,编译器就会为我们定义一个,称为合成拷贝构造函数。一般情况下,它的作用是将其参数给定的对象中每个非static成员逐个拷贝到正在创建的对象中。那么我们就可以理解直接初始化和拷贝初始化的差异了。(拷贝初始化是指使用=进行的初始化)。

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

使用直接初始化时,实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数。当使用拷贝初始化时,要求编译器将右侧运算对象拷贝到正在创建的对象中,必要的化还要进行类型转换。拷贝初始化一般情况下使用拷贝构造函数来完成,在之后的章节会介绍移动构造函数,会更方便的进行拷贝构造。

4. 合成拷贝赋值运算符

如果我们没有为一个类定义拷贝赋值运算符,编译器就会为我们定义一个,称为合成拷贝赋值运算符。它会将右侧对象的每个非static成员赋予左侧运算对象的对应成员,对于数组成员,逐个赋值数组元素。最终返回一个指向左侧运算对象的引用。

5. 注意事项

  • 某些情况下,合成拷贝构造函数和合成拷贝赋值运算符的作用是禁止该类型对象的构造和赋值。这时候是因为我们定义的类并不需要拷贝、赋值或者销毁其成员。这里参考13.1.6章节的阻止拷贝章节。
  • 对于使用shared_ptr控制的对象类型,拷贝构造和拷贝赋值会增加count次数,而weak_ptr则不会。