【转载】C++ 拷贝构造函数和赋值运算符

2022年 7月 13日 45点热度 0人点赞

file

原文地址: C++ 拷贝构造函数和赋值运算符

本文主要介绍了拷贝构造函数和赋值运算符的区别, 以及在什么时候调用拷贝构造函数, 什么情况下调用赋值运算符. 最后, 简单的分析了下深拷贝和浅拷贝的问题.

拷贝构造函数和赋值运算符

在默认情况下 (用户没有定义, 但是也没有显式的删除), 编译器会自动的隐式生成一个拷贝构造函数和赋值运算符. 但用户可以使用 delete 来指定不生成拷贝构造函数和赋值运算符, 这样的对象就不能通过值传递, 也不能进行赋值运算.

class Person
{
public:

    Person(const Person& p) = delete;

    Person& operator=(const Person& p) = delete;

private:
    int age;
    string name;
};

上面的定义的类 Person 显式的删除了拷贝构造函数和赋值运算符, 在需要调用拷贝构造函数或者赋值运算符的地方, 会提示【无法调用该函数, 它是已删除的函数】.

还有一点需要注意的是, 拷贝构造函数必须以引用的方式传递参数. 这是因为, 在值传递的方式传递给一个函数的时候, 会调用拷贝构造函数生成函数的实参. 如果拷贝构造函数的参数仍然是以值的方式, 就会无限循环的调用下去, 直到函数的栈溢出.

何时调用

拷贝构造函数和赋值运算符的行为比较相似, 都是将一个对象的值复制给另一个对象; 但是其结果却有些不同, 拷贝构造函数使用传入对象的值生成一个新的对象的实例, 而赋值运算符是将对象的值复制给一个已经存在的实例. 这种区别从两者的名字也可以很轻易的分辨出来, 拷贝构造函数也是一种构造函数, 那么它的功能就是创建一个新的对象实例; 赋值运算符是执行某种运算, 将一个对象的值复制给另一个对象 (已经存在的). 调用的是拷贝构造函数还是赋值运算符, 主要是看是否有新的对象实例产生. 如果产生了新的对象实例, 那调用的就是拷贝构造函数;如果没有, 那就是对已有的对象赋值, 调用的是赋值运算符.

调用拷贝构造函数主要有以下场景:

  • 对象作为函数的参数, 以值传递的方式传给函数.
  • 对象作为函数的返回值, 以值的方式从函数返回
  • 使用一个对象给另一个对象初始化

代码如下:

class Person
{
public:
    Person(){}
    Person(const Person& p)
    {
        cout << "Copy Constructor" << endl;
    }

    Person& operator=(const Person& p)
    {
        cout << "Assign" << endl;
        return *this;
    }

private:
    int age;
    string name;
};

void f(Person p)
{
    return;
}

Person f1()
{
    Person p;
    return p;
}

int main()
{
    Person p;
    Person p1 = p;    // 1
    Person p2;
    p2 = p;           // 2
    f(p2);            // 3

    p2 = f1();        // 4

    Person p3 = f1(); // 5

    getchar();
    return 0;
}

上面代码中定义了一个类 Person, 显式的定义了拷贝构造函数和赋值运算符. 然后定义了两个函数: f, 以值的方式参传入 Person 对象; f1, 以值的方式返回 Person 对象. 在 main 中模拟了 5 中场景, 测试调用的是拷贝构造函数还是赋值运算符. 执行结果如下:

分析如下:

  1. 这是虽然使用了 =, 但是实际上使用对象 p 来创建一个新的对象 p1. 也就是产生了新的对象, 所以调用的是拷贝构造函数.
  2. 首先声明一个对象 p2, 然后使用赋值运算符 =, 将 p 的值复制给 p2, 显然是调用赋值运算符, 为一个已经存在的对象赋值 .
  3. 以值传递的方式将对象 p2 传入函数 f 内, 调用拷贝构造函数构建一个函数 f 可用的实参.
  4. 这条语句拷贝构造函数和赋值运算符都调用了. 函数 f1 以值的方式返回一个 Person 对象, 在返回时会调用拷贝构造函数创建一个临时对象 tmp 作为返回值; 返回后调用赋值运算符将临时对象 tmp 赋值给 p2.
  5. 按照 4 的解释, 应该是首先调用拷贝构造函数创建临时对象; 然后再调用拷贝构造函数使用刚才创建的临时对象创建新的对象 p3, 也就是会调用两次拷贝构造函数. 不过, 编译器也没有那么傻, 应该是直接调用拷贝构造函数使用返回值创建了对象 p3.

深拷贝和浅拷贝

说到拷贝构造函数, 就不得不提深拷贝和浅拷贝. 通常, 默认生成的拷贝构造函数和赋值运算符, 只是简单的进行值的复制. 例如: 上面的 Person 类, 字段只有 int 和 string 两种类型, 这在拷贝或者赋值时进行值复制创建的出来的对象和源对象也是没有任何关联, 对源对象的任何操作都不会影响到拷贝出来的对象. 反之, 假如 Person 有一个对象为 int *, 这时在拷贝时还只是进行值复制, 那么创建出来的 Person 对象的 int * 的值就和源对象的 int * 指向的是同一个位置. 任何一个对象对该值的修改都会影响到另一个对象, 这种情况就是浅拷贝.

深拷贝和浅拷贝主要是针对类中的指针和动态分配的空间来说的, 因为对于指针只是简单的值复制并不能分割开两个对象的关联, 任何一个对象对该指针的操作都会影响到另一个对象. 这时候就需要提供自定义的深拷贝的拷贝构造函数, 消除这种影响. 通常的原则是:

  • 含有指针类型的成员或者有动态分配内存的成员都应该提供自定义的拷贝构造函数
  • 在提供拷贝构造函数的同时, 还应该考虑实现自定义的赋值运算符

对于拷贝构造函数的实现要确保以下几点:

  • 对于值类型的成员进行值复制
  • 对于指针和动态分配的空间, 在拷贝中应重新分配分配空间
  • 对于基类, 要调用基类合适的拷贝方法, 完成基类的拷贝

总结

拷贝构造函数和赋值运算符的行为比较相似, 却产生不同的结果; 拷贝构造函数使用已有的对象创建一个新的对象, 赋值运算符是将一个对象的值复制给另一个已存在的对象. 区分是调用拷贝构造函数还是赋值运算符, 主要是否有新的对象产生.

关于深拷贝和浅拷贝. 当类有指针成员或有动态分配空间, 都应实现自定义的拷贝构造函数. 提供了拷贝构造函数, 最后也实现赋值运算符.

rainbow

这个人很懒,什么都没留下

文章评论