跳至主要內容

拷贝构造函数

AkashiNeko原创C++构造函数

拷贝构造函数

拷贝构造是构造函数的一个重载,用于从其他对象拷贝初始化。

class A {
public:
    // 构造函数
    A() :i_(0) {}
    // 拷贝构造
    A(const A &a) i_(a.i_) {}

private:
    int i_;
};

拷贝构造的调用

拷贝初始化

当对象通过拷贝其他对象来初始化时,会调用拷贝构造函数。

下面对象 a2 通过拷贝 a1 来完成自身的初始化。

A a1;
A a2(a1);

同样的,可以通过下面的代码看出拷贝构造是如何调用的。

class A {
public:
    // 构造函数
    A() :i_(0) {}

    // 拷贝构造
    A(const A &a) :i_(a.i_) {
        cout << "A(const A&)" << endl;
    }
private:
    int i_;
};

int main() {
    A a1;
    A a2(a1);
    return 0;
}

函数的传参

只要对象发生了拷贝初始化,就必须调用拷贝构造,包括函数传参的过程。

void fun(A a) {} // 对象a1传参给a时,会调用一次拷贝构造

int main() {
    A a1;
    fun(a1); // 函数调用,传参
    return 0;
}

由于拷贝构造函数也是函数,所以在传参时,对象必须以引用的形式传参。

如果以非引用的形式传参,会发生拷贝构造的调用,在拷贝构造的参数列表中,发生拷贝构造函数的调用,即对拷贝构造自身的调用,会造成无限递归。

非引用传参的危害

假设拷贝函数的参数 a 不使用引用传参

A(A other) :i_(other.i_) {}

对象 a2 调用拷贝构造函数,试图对对象 a1 进行拷贝

int main() {
    A a1;
    A a2(a1);
    return 0;
}

在调用 A a2(a1) 时,对象 a1 被作为参数传递给拷贝构造函数 A(A other) 中的对象 other,由于 other 不是一个引用,所以此时会发生新的拷贝:从 a1other 的拷贝。而调用拷贝函数时,又会发生拷贝式的传参,造成无限递归。

因此,在定义拷贝构造时必须使用引用传参

深拷贝和浅拷贝

为什么需要自定义拷贝构造

在上面的示例中,拷贝构造对成员变量进行了拷贝,即直接对对象的内存空间进行了复制,这种拷贝称为浅拷贝。这种情况下,直接使用默认生成的拷贝构造函数也达成目的。

如果对象中管理了其他的资源,而拷贝时需要对这些额外的资源进行拷贝,就会涉及到对象的深拷贝。比如成员变量中含有其他资源的指针,就不得不自己定义拷贝构造。

编译器默认生成的拷贝构造都是浅拷贝。

拷贝构造的实现

以之前Stack类举例。

int main() {
    Stack s1;
    Stack s2(s1);
    return 0;
}

如果使用编译器默认生成的拷贝构造,在对Stack对象进行拷贝时,_data 指针会被拷贝到新对象,而其指向的数据没有被拷贝。这样会造成诸多问题,比如:

  • 由于两个对象的 _data 指针指向同一块空间,在原对象中对 _data 指向的空间做修改时,会影响新对象中 _data 指向的空间。
  • 在调用析构函数时,_data 指向的空间会被释放两次,而导致程序异常。

此时就需要自己定义深拷贝的逻辑了,不能只拷贝 _data 指针,而是要拷贝其指向的空间。

Stack(const Stack& s) :_capacity(s._capacity), _top(s._top), _data(nullptr) {
    // 申请新的空间
    _data = (int*)malloc(_capacity * sizeof(int));
    assert(_data != nullptr);

    // 对空间进行拷贝
    memcpy(_data, s._data, _capacity * sizeof(int));
}

这样就能正确地完成Stack类的深拷贝了。