拷贝构造函数的调用以及浅拷贝与深拷贝的理解

       今天一直在研究拷贝构造函数相关的东西,我这个大四老狗感觉又回到了大一学C++的时候。瞎捣鼓了一天,略微还是有些收获的,趁着脑子中的概念正热,把自己的心得赶紧整理出来分享给大家。

​       首先简单介绍下拷贝构造函数的概念:拷贝构造函数是形参是本类对象的引用的构造函数,它的一般声明形式诸如这样:

Location(const Location & obj)

其实我们经常在类中并未显示地定义类的拷贝构造函数,即当缺省拷贝构造函数时,这个时候系统会自动生成一个默认的拷贝构造函数,把成员值一一复制。这种缺省拷贝构造函数的情况很常见,一般也不会给程序带来问题,但是需要注意的是,如果一个类需要析构函数来释放资源,那么它需要定义一个显式拷贝构造函数来实现深拷贝,关于这一点,本文后面会详细说明缘由。

一、拷贝构造函数的调用

       接下来说一下调用拷贝构造函数的三种情况:

  • 当用类的一个对象去初始化该类的另一个对象时。

  • 如果函数的形参是类的对象,调用函数时用实参的值初始化形参时

  • 如果函数的返回值是类的对象函数执行完成返回调用者时。

    附上代码,在示例代码中,覆盖了拷贝构造函数的三种情况。

#include <iostream>
#include <string.h>

class student { 
    public:
    	// 普通构造函数
	    student(int i, char* c, int a, float s) {
        	// std::cout << "Constructing..." << std::endl;
            id = i;
            age = a;
            score = s;
            name = new char[strlen(c) + 1]; //申请堆空间
            if (name != 0) strcpy(name, c);
        }

        // 显示声明拷贝函数,自定义实现深拷贝
	    student(const student& s) {	
            std::cout << "Copy Constructing..." << std::endl;
            id = s.id; //一般成员简单复制
            age = s.age;
            score = s.score;
            name = new char[strlen(s.name) + 1]; //先申请堆空间
            if (name != 0) strcpy(name, s.name); //复制字符串
        };

        ~student() {
            // std::cout <<"Destructing..."<< std::endl;
            delete []name;
        }

        int getid() {
            return id;
        }
 
    private:
	    int id;
	    char* name;
	    int age;
	    float score;
};

// 函数返回值是类对象,函数执行完成返回调用时,系统会自动调用拷贝构造函数
student g() {
    student s1(10, "Lina", 18, 86);
    return s1;
}

// 如果函数的形参是类的对象,调用函数时,用实参的值初始化形参时,系统会自动调用拷贝构造函数
void print_1(student s) {
    std::cout << "学号:" << s.getid() << std::endl;
}

int main() {	
    student s1(10, "Wang", 18, 86); //创建和初始化对象
    std::cout << std::endl;
    // 第一种情况,用s1初始化s2。第一次调用拷贝构造函数
	student s2 = s1;
    std::cout << std::endl;
    // 第二种情况,对象s2作为print_1的实参。第二次调用拷贝构造函数
    print_1(s2);
    std::cout << std::endl;
    // 第三种情况,函数的返回值是类对象,函数返回时调用拷贝构造函数
	s2 = g();
    std::cout << std::endl;

    return 0;
} 

需要提及的是,对于第三种情况,在IDE上运行时,并没有打印出 "Copy Constructing…" 这个结果:

Copy Constructing...

Copy Constructing...
学号:10

看到打印出这个结果时,我也是很蒙圈的,搜索了网上的问答了解到这是编译器做了优化,例如我这边是gcc编译器,它做了优化,使得返回值为对象时,不会再产生临时对象,因而不会再调用拷贝构造函数。那么如果想要看到结果需要怎么做呢,因为我是linux系统,此处就简单说下Linux的处理方式,在终端输入:

g++ -w  -fno-elide-constructors  test.cpp -o test

这时再在终端运行,打印出理想的结果:

Copy Constructing...

Copy Constructing...
学号:10

Copy Constructing...

二、浅拷贝和深拷贝

​       完成简单的一一对应的复制的拷贝构造函数称为浅拷贝。在浅拷贝过程中,如果对象中含有指针变量,将使得所复制的对象中的指针成员与原来对象的指针成员指向相同的内存空间。在退出程序时,析构函数会释放指针成员指向的内存空间,譬如用对象a初始化对象b,调用拷贝构造函数实现浅拷贝后,程序退出运行时,析构对象a时,析构函数释放其指针成员指向的内存空间,而析构复制对象b时系统会调用相同的析构函数,也要释放其指针成员指向的内存空间。则出现同一内存空间,申请一次,释放两次的情况, new 与 delete不成对,系统会报错。附上简单的示例图:

拷贝构造函数的调用以及浅拷贝与深拷贝的理解
给出浅拷贝的代码:
#include <iostream>
#include <string.h>

class student { 
    public:
	    student(int i, char* c, int a, float s) {
        	// std::cout << "Constructing..." << std::endl;
            id = i;
            age = a;
            score = s;
            name = new char[strlen(c) + 1]; //申请堆空间
            if (name != 0) strcpy(name, c);
        }
    
        ~student() {
            // std::cout <<"Destructing..."<< std::endl;
            delete []name;
        }
 
    private:
	    int id;
	    char* name;
	    int age;
	    float score;
};

int main() {	
    student s1(10, "Wang", 18, 86); //创建和初始化对象
	student s2 = s1;
    return 0;
} 

此处用s1初始化s2时,调用了拷贝构造函数,因为缺省,默认用了浅拷贝,那么退出时,调用析构函数,因为同一内存空间释放了两次,系统会报错。正确的方法是定义一个显式的拷贝构造函数实现深拷贝,可以见示例代码:

#include <iostream>
#include <string.h>

class student { 
    public:
	    student(int i, char* c, int a, float s) {
        	// std::cout << "Constructing..." << std::endl;
            id = i;
            age = a;
            score = s;
            name = new char[strlen(c) + 1]; //申请堆空间
            if (name != 0) strcpy(name, c);
        }
       
        // 显示声明拷贝函数,自定义实现深拷贝
	    student(const student& s) {	
            std::cout << "Copy Constructing..." << std::endl;
            id = s.id; //一般成员简单复制
            age = s.age;
            score = s.score;
            name = new char[strlen(s.name) + 1]; //先申请堆空间
            if (name != 0) strcpy(name, s.name); //复制字符串
        };
        
        ~student() {
            // std::cout <<"Destructing..."<< std::endl;
            delete []name;
        }
 
    private:
	    int id;
	    char* name;
	    int age;
	    float score;
};

int main() {	
    student s1(10, "Wang", 18, 86); //创建和初始化对象
	student s2 = s1;
    return 0;
} 
拷贝构造函数的调用以及浅拷贝与深拷贝的理解
这样的话便成功解决了内存空间多次释放报错的问题。

三、普通构造函数、拷贝构造函数、析构函数的调用分析

​ ​ ​ ​ ​ ​ ​ 前面两部分把拷贝构造函数已经分析清楚了,接下来就依据上面的程序简要分析下普通构造函数、拷贝构造函数、析构函数的调用,普通构造函数简单地说就是非拷贝构造函数范畴的构造函数,析构函数则是用于当对象生存期结束时调用它释放对象所占的内存。普通构造函数以及析构函数如果程序中没有显示定义的话,系统也会提供一个默认的析构函数,需要注意的是默认的析构函数只能用来释放对象的数据成员所占用的空间,但不包括堆内存空间。在本例中,为了方便观测,普通构造函数、拷贝构造函数、析构函数在程序中均显示地定义,下面看下示例代码:

#include <iostream>
#include <string.h>

class student { 
    public:
        // 普通构造函数
	    student(int i, char* c, int a, float s) {
        	std::cout << "Constructing..." << std::endl;
            id = i;
            age = a;
            score = s;
            name = new char[strlen(c) + 1]; //申请堆空间
            if (name != 0) strcpy(name, c);
        }
    
        // 显示声明拷贝函数,自定义实现深拷贝
	    student(const student& s) {	
            std::cout << "Copy Constructing..." << std::endl;
            id = s.id; //一般成员简单复制
            age = s.age;
            score = s.score;
            name = new char[strlen(s.name) + 1]; //先申请堆空间
            if (name != 0) strcpy(name, s.name); //复制字符串
        };
        
        ~student() {
            std::cout << "Destructing..." << std::endl;
            delete []name;
        }

        int getid() {
            return id;
        }
 
    private:
	    int id;
	    char* name;
	    int age;
	    float score;
};

// 函数返回值是类对象,函数执行完成返回调用时,系统会自动调用拷贝构造函数
student g() {
    student s1(10, "Lina", 18, 86);
    return s1;
}

// 如果函数的形参是类的对象,调用函数时,用实参的值初始化形参时,系统会自动调用拷贝构造函数
void print_1(student s) {
    std::cout << "学号:" << s.getid() << std::endl;
}

int main() {	
    student s1(10, "Wang", 18, 86); //创建和初始化对象
    std::cout << std::endl;
    // 第一种情况,用s1初始化s2。第一次调用拷贝构造函数
	student s2 = s1;
    std::cout << std::endl;
    // 第二种情况,对象s2作为print_1的实参。第二次调用拷贝构造函数
    print_1(s2);
    std::cout << std::endl;
    // 第三种情况,函数的返回值是类对象,函数返回时调用拷贝构造函数
	s2 = g();
    std::cout << std::endl;

    return 0;
} 
Constructing...

Copy Constructing...

Copy Constructing...
学号:10
Destructing...

Constructing...
Copy Constructing...
Destructing...
Destructing...

Destructing...
Destructing...

看一下打印的结果,发现对于拷贝构造函数调用的第二种及第三种情况,既打印了"Copy Constructing…“又打印了"Destructing…”,情况较为复杂,下面仔细地分析下:

  • 第二种情况:函数的形参为类对象,调用函数时,用实参初始化形参,系统自动调用拷贝构造函数
// 如果函数的形参是类的对象,调用函数时,用实参的值初始化形参时,系统会自动调用拷贝构造函数
void print_1(student s) {
    std::cout << "学号:" << s.getid() << std::endl;
}

// 第二种情况,对象s2作为print_1的实参。第二次调用拷贝构造函数
print_1(s2);
std::cout << std::endl;
Copy Constructing...
学号:10
Destructing...

因为print_1()函数的形参为student对象,所以调用它时,用s2初始化形参时,会调用拷贝构造函数生成一个函数内的临时对象,这个临时对象那个随着函数的调用结束也就被析构了。

  • 第三种情况:函数的返回值是类对象,函数返回时调用拷贝构造函数
// 函数返回值是类对象,函数执行完成返回调用时,系统会自动调用拷贝构造函数
student g() {
    student s1(10, "Lina", 18, 86);
    return s1;
}

// 第三种情况,函数的返回值是类对象,函数返回时调用拷贝构造函数
s2 = g();
std::cout << std::endl;
Constructing...
Copy Constructing...
Destructing...
Destructing...

在g()函数中,初始化s1对象时调用普通构造函数,打印"Constructing…";因为函数返回值是类对象,所以会调用拷贝构造函数返回一个新的匿名对象,这时候会执行拷贝构造函数,打印"Copy Constructing…";g()运行结束后,s1被析构,打印"Destructing…";接下来用g()返回的匿名对象赋值给s2,接下来匿名对象任务完成,就会被析构,打印"Destructing…"。


参考博客:
(copy)赋值构造函数的4种调用时机or方法
C++返回值为对象时复制构造函数不执行怎么破