C++ 深拷贝、浅拷贝及拷贝构造函数

1. 深拷贝和浅拷贝

浅拷贝(shallowCopy):

对于基本数据类型和简单对象,他们之间的拷贝非常简单,就是按位复制内存,这种默认的拷贝行为就是浅拷贝,这和memcpy()函数的调用效果类似。

深拷贝(deepCopy):

深拷贝会将原有对象的所有成员变量拷贝给新对象,对于指针等数据还会为新对象重新在堆上分配一块内存,并将原有对象所持有的堆上的数据也拷贝过来,这样能保证原有对象和新对象所持有的动态内存都是相互独立的,更改一个对象的数据不会影响另一个对象,同时也不会造成double free的错误。

一个通俗的理解

浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

#include <iostream>

class shallowCopy {
public:
    shallowCopy(int len) : m_len(len) {
        m_ptr = new int(0); // m_ptr指向一个值为0的int
    }
    shallowCopy() {}

    ~shallowCopy() {
        delete m_ptr;
    }

public: // 定义为public,方便输出
    int* m_ptr;
    int m_len;
};

class deepCopy {
public:
    deepCopy(int len) : m_len(len) {
        std::cout << "call deepCopy(int len) " << std::endl;
        m_ptr = new int(1);
    }
    deepCopy(const deepCopy& deepcopy) {
        std::cout << "call deepCopy(const deepCopy& deepcopy) " << std::endl;
        m_len = deepcopy.m_len;
        m_ptr = new int(*(deepcopy.m_ptr)); // 重新分配内存,并且赋值
    } // 拷贝构造函数
    ~deepCopy() {
        delete m_ptr;
    }

public:
    int* m_ptr;
    int m_len;
};

int main()
{
    shallowCopy sc(1);
    auto sc1 = sc; // 浅拷贝
    std::cout << "shallowCopy: " << std::endl;
    std::cout << "sc.m_ptr = " << sc.m_ptr << std::endl;
    std::cout << "sc1.m_ptr = " << sc1.m_ptr << std::endl;

    std::cout << "deepCopy: " << std::endl;
    deepCopy dc(1);
    deepCopy dc1(dc); // 深拷贝
    std::cout << "dc.m_ptr = " << dc.m_ptr << std::endl;
    std::cout << "dc1.m_ptr = " << dc1.m_ptr << std::endl;    
}
root@MY:~/cppLearn# g++ Copy.cpp -o copy
root@MY:~/cppLearn# ./copy 
shallowCopy: 
sc.m_ptr = 0x560c930aeeb0
sc1.m_ptr = 0x560c930aeeb0
deepCopy: 
call deepCopy(int len) 
call deepCopy(const deepCopy& deepcopy) 
dc.m_ptr = 0x560c930af2e0
dc1.m_ptr = 0x560c930af300
free(): double free detected in tcache 2
Aborted

2. 拷贝构造函数

拷贝构造函数

拷贝构造函数只有一个参数,而且必须是当前类的引用,可以是const引用,也可以是非const引用,一般都是用const引用,含义更加明确,并且添加const限制后,可以将const或非const对象传递给形参,因为非const类型可以转换为const类型,但是const类型不能转换为非const类型。

默认拷贝构造函数

如果类不持有数据指针、动态分配内存、打开文件、网络连接等资源,默认拷贝构造函数就够用了,没有必要再显示定义一个。

拷贝构造函数的形参必须是引用类型

在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,……这个过程会一直持续下去,陷入死循环。

什么时候会调用拷贝构造函数

  • 用类的一个对象去初始化另一个对象时;
  • 当函数的形参是类的对象时(也就是值传递),引用传递不会调用拷贝构造函数;
  • 当函数返回值是类的对象时。
/* 到底什么时候会调用拷贝构造函数? */

/* 明白 初始化 和 赋值 的区别:
    初始化:定义(第一次出现)的同时赋值(也可以不赋值 默认初始化)  只有一次
    赋值:定义完成以后再赋值(不管在定义的时候有没有赋值)就叫做赋值 有多次*/

#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
    Student(string name = "", int age = 0, float score = 0.0f); //普通构造函数
    Student(const Student &stu);                                //拷贝构造函数
public:
    Student &operator=(const Student &stu); //重载=运算符
    void display();

private:
    string m_name;
    int m_age;
    float m_score;
};
Student::Student(string name, int age, float score) : m_name(name), m_age(age), m_score(score) {
    cout << "Normal constructor was called." << endl;
}
//拷贝构造函数
Student::Student(const Student &stu)
{
    this->m_name = stu.m_name;
    this->m_age = stu.m_age;
    this->m_score = stu.m_score;
    cout << "Copy constructor was called." << endl;
}
//重载=运算符
Student &Student::operator=(const Student &stu)
{
    this->m_name = stu.m_name;
    this->m_age = stu.m_age;
    this->m_score = stu.m_score;
    cout << "operator=() was called." << endl;

    return *this;
}
void Student::display()
{
    cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << endl;
}
//3.函数的形参为类类型
void fun(Student stu)
{
}
//4.函数返回值为类类型
Student fun()
{
    Student stu("八戒", 26, 45.6);
    return stu;
}
int main()
{
    //stu1、stu2 都会调用普通构造函数Student(string name, int age, float score)
    Student stu1("赵云", 16, 90.5);
    Student stu2("鲁班七号", 17, 89.0);

    //拷贝函数调用时机:
    //1.将其它对象作为参数
    Student stu3(stu2); //调用拷贝构造函数Student(const Student &stu)

    //2.创建对象的同时进行初始化
    Student stu4 = stu1; //调用拷贝构造函数Student(const Student &stu) C语言 struct 风格!
    stu4 = stu2;         //调用operator=()
    stu4 = stu3;         //调用operator=()

    //3.函数的形参为类类型,值传递
    Student stu5("安琪拉", 18, 56.3);
    fun(stu5); //相当于 Student stu = stu5 复制构造产生的局部对象(函数调用完成后,对象被销毁!)

    //4.函数返回值为类类型
    Student stu6 = fun(); //两次调用拷贝构造函数,一次是返回 stu 对象时(不希望局部对象被销毁,复制构造产生中间对象(匿名对象)接收),另外一次是创建 stu6 对象时
    return 0;
}
root@MY:~/cppLearn# ./call 
Normal constructor was called. // Student stu1("赵云", 16, 90.5);
Normal constructor was called. // Student stu2("鲁班七号", 17, 89.0);
Copy constructor was called.   // Student stu3(stu2);
Copy constructor was called.   // Student stu4 = stu1;
operator=() was called.        // stu4 = stu2; 
operator=() was called.        // stu4 = stu3; 
Normal constructor was called. // Student stu5("安琪拉", 18, 56.3);
Copy constructor was called.   // fun(stu5);
Normal constructor was called. // Student fun() {Student stu("八戒", 26, 45.6);}

上面代码中Student stu6 = fun(); 并没有打印出两次拷贝构造函数,应该是编译器优化;return 之后一定会调用拷贝构造函数,这个返回的局部变量估计是编译器直接优化给了stu6了。

返回值优化
返回值优化(Return value optimization,缩写为RVO)是C++的一项编译优化技术。即删除保持函数返回值的临时对象。这可能会省略两次复制构造函数,即使复制构造函数有副作用。
典型地,当一个函数返回一个对象实例,一个临时对象将被创建并通过复制构造函数把目标对象复制给这个临时对象。C++标准允许省略这些复制构造函数,即使这导致程序的不同行为,即使编译器把两个对象视作同一个具有副作用。

#include <iostream>

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C();
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f();
}
root@MY:~/cppLearn# ./returnopt 
Hello World! // 没有打印 "A copy was made",可见返回值优化

还可以看一个题目:
image

本文引用多篇文章,主打一个融合,主要便于笔者自己可以更好的回顾与总结,如果能帮助你,十分有幸,欢迎评判指正!

参考:
https://www.jianshu.com/p/289f8baa7d5c
https://blog.csdn.net/weixin_43445441/article/details/122089265
https://www.nowcoder.com/questionTerminal/a0f69e79a60d45f2b441c7e92f8f6ad3
https://zh.wikipedia.org/zh-cn/返回值优化

posted @ 2023-05-06 15:10  人生逆旅,我亦行人  阅读(1014)  评论(0编辑  收藏  举报