近期在学C++的过程中,看到了复制拷贝函数,第一遍看过去有些许迷糊,然后网上找了很多资料来学习,自己也敲了一些代码来验证,现将学习成果总结一下,和各位一起交流学习。
一、什么是复制构造函数
一种特殊的重载构造函数,他的形式很固定:[类名]::[构造函数名](const [类名] &[参数]); (参数是本类型的一个引用变量)
实际代码:MyString::MyString(const MyString &CopySource);
二、什么情况下会用到复制构造函数
复制构造函数英文copy constructior,顾名思义在类的对象间发生拷贝时会调用,下面我们分情况讨论: 先贴出代码
#include<iostream>
using namespace std;
class Example
{
private:
int a;
public:
Example(int b) //构造函数
{
cout << "constructor is called" << endl;
a = b;
}
Example(const Example & c) //复制构造函数 (注意规定格式)
{
cout << "copy constructor is called" << endl;
a = c.a;
}
~Example() //析构函数
{
cout << "destructor is called" << endl;
}
void Show()
{
cout << a << endl;
}
};
void g_fun(Example c)
{
cout << "g_func" << endl;
}
Example r_fun()
{
Example temp(0);
return temp;
}
int main()
{
Example A(100);
Example B = A;
B.Show();
g_fun(A);
r_fun();
system("pause");
return 0;
}
分析代码我们可以发现,复制构造函数被调用三次,涉及类的对象拷贝出现了三次,分别是;
(1)一个对象通过拷贝另一个以及初始化好了的对象来初始化自己
Example A(100);
Example B = A;
(2)将实参A拷贝给形参c,然后调用g_fun()
void g_fun(Example c)
{
cout << "g_func" << endl;
}
g_fun(A);
(3)函数的返回值类型是类的对象,实际调用过程是,函数内先创建一个temp对象,再把这个对象拷贝给返回的对象
Example r_fun()
{
Example temp(0);
return temp;
}
r_fun();
到这里,我们基本清楚了,复制构造函数什么时候会被用到,但是我们在实际码代码的过程或者看代码的时候,出现类的地方并不是都有复制构造函数,是因为用户未定义复制构造函数,编译系统则会调用默认的复制构造函数。一般情况问题也不大,但是在下面这种情况下问题就大了,先贴出代码:
#include <iostream>
using namespace std;
class Student
{
private:
int num;
char *name;
public:
Student();
~Student();
};
Student::Student()
{
name = new char(20);
cout << "Student" << endl;
}
Student::~Student()
{
cout << "~Student " << (int)name << endl;
delete name;
name = NULL;
}
int main()
{
{// 花括号让s1和s2变成局部对象,方便测试
Student s1;
Student s2(s1);// 复制对象
}
system("pause");
return 0;
}
运行结果如下
由结果,我们可以分析:调用一次构造函数,调用两次析构函数,两个对象的指针成员所指内存相同,这会导致严重的问题,name指针被分配一次内存,但是程序结束时该内存却被释放了两次,会导致程序崩溃!下面进一步分析这个问题。
三、复制构造函数中的浅复制和深复制
接着讨论上面程序运行结果,这是由于编译系统在我们没有自己定义复制构造函数时,会在复制对象时调用默认复制构造函数,进行的是浅复制!即对指针name复制后后会出现两个指针指向同一个内存空间。
官方概念->浅复制:复制一个类的对象时,将复制他的指针成员,但不复制指针指向的缓冲区,结果是,两个对象指向同一块动态分配的内存。这就叫浅复制,会威胁程序的稳定性。
所以,在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。故添加构造函数如下:
#include <iostream>
using namespace std;
class Student
{
private:
int num;
char *name;
public:
Student();
~Student();
Student(const Student &s);//拷贝构造函数,const防止对象被改变
};
Student::Student()
{
name = new char(20);
cout << "Student" << endl;
}
Student::~Student()
{
cout << "~Student " << (int)name << endl;
delete name;
name = NULL;
}
Student::Student(const Student &s)
{
name = new char(20);
memcpy(name, s.name, strlen(s.name));
cout << "copy Student" << endl;
}
int main()
{
{// 花括号让s1和s2变成局部对象,方便测试
Student s1;
Student s2(s1);// 复制对象
}
system("pause");
return 0;
}
运行结果如下:
可分析到:调用一次构造函数,一次自定义拷贝构造函数,两次析构函数。两个对象的指针成员所指内存不同。这里我们叫做深复制,而并非浅复制(复制指针的值),即将指向的内容复制给当前对象新分配的缓冲区中。
四、在实际码代码中我们要注意什么?
1、类包含原始指针成员(如char*)时,一定要编写复制构造函数和复制赋值运算符;
2、写复制构造函数时,要将接受源对象的参数声明为const引用;
3、将成员变量声明为std::string和智能指针。
五、面试可能碰到的问题:(参考:https://www.cnblogs.com/wuchanming/p/4050969.html)
Q1:构造函数能否重载,析构函数能否重载,为什么?
A1:构造函数可以,析构函数不可以。
Q2:析构函数为什么一般情况下要声明为虚函数?
A2:虚函数是实现多态的基础,当我们通过基类的指针是析构子类对象时候,如果不定义成虚函数,那只调用基类的析构函 数,子类的析构函数将不会被调用。如 果定义为虚函数,则子类父类的析构函数都会被调用。
Q3:什么情况下必须定义拷贝构造函数?
A3:当类的对象用于函数值传递时(值参数,返回类对象),拷贝构造函数会被调用。如果对象复制并非简单的值拷贝,那 就必须定义拷贝构造函数。例如大的堆 栈数据拷贝。如果定义了拷贝构造函数,那也必须重载赋值操作符。
参考博客: