C++面试中,经常有面试官问“请你说一下C++拷贝构造函数是什么?“。而我们在阅读STL等很多C++的源码时,我们也经常看到作者对拷贝构造函数的处理。可以说,在学习C++的过程中,拷贝构造函数的理解是衡量一个人对C++了解深度的一个必考点。答得好,恭喜你入门了,否则可以说你还没有入门。那么,在此我将总结下这块的相关知识给大家参考理解。下面,我将从3个方面来说明这个知识点。
1. 拷贝构造函数是什么?
先直接上一段拷贝构造函数的代码,如下:
class MString
{
MString(const MString& other)
{
m_pChar = new char[strlen(other.m_pChar)+1];
assert( nullptr != m_pChar);
strcpy(m_pChar, other.m_pChar);
}
private:
char* m_pChar;
};
拷贝构造函数的形式是: Class(const Class& other)。首先,它本质上就是一个构造函数;其次,它实现的方式是从一个其他同类对象复制过来的。所以,和普通构造函数不一样的是,它是从另一个类对象中”克隆“过来的。既然是”克隆“,那么就是涉及到2个问题:1)什么情况下需要这种方式的”克隆“?2)怎么”克隆“? 对此,下面继续回答这两个问题。
2. 拷贝构造函数什么时候会被用到?
拷贝构造函数的使用场景有3类。代码形式分别如下:
class X { ...... };
X x;
X y;
X yy = x; // 第1类:用存在的类X的对象x,来初始化一个新声明的类对象yy。这句话等同于X yy(x);
y = x; // 这里,就不是了,因为y是已经存在了,所以这里调用的是类的operator=()函数。
void call(X x);
void bar()
{
X xx;
call(xx); //第2类:类对象xx作为函数的传值参数。进函数call时,会发生值copy。函数内部就会先调用类的拷贝构造函数创造一个值copy,即一个新的类对象副本在函数内使用。
...
}
X funcall()
{
X zz;
//....
return zz; //第3类:用类对象作为函数的返回值时,会先调用类的拷贝构造函数生成一个类副本对象,用作后面的返回值对象。然后再执行zz对象的析构。
}
对此,我们可以看到,类对象的初始化、函数传值和函数对象返回时,都是调用类的拷贝构造函数来完成的。这么看,怎么保证这个类对象被正确的”克隆“呢?下面回答这个问题。
3.拷贝构造函数怎么执行“克隆”的?
我们知道,拷贝构造函数我们一般可以不写的。如下类:
class 3DPoint
{
float m_x;
float m_y;
float m_z;
};
void test()
{
3DPoint p1;
3DPoint p2 = p1;//这里,编译器会自动生成一个默认的拷贝构造函数来复制对象。
.......
}
test()函数执行正常。那么如果将这个类改成:
class 3DPoint
{
public:
~3DPoint()
{
if(m_x)
delete m_x;
if(m_y)
delete m_y;
if(m_z)
delete m_z;
}
float *m_x;
float *m_y;
float *m_z;
};
void test()
{
3DPoint p1;
3DPoint p2 = p1;
.......
}
函数test()执行正常吗?对此,我们必须了解下, 如果没有实现,那么编译器给我们生成的拷贝构造函数采用什么方式来copy对象内容呢? 这里实际上执行的是“Default Memberwise Initialization" 方式。什么意思?就是对象成员的默认初始化方式。如果类成员里有类对象,就执行这个类对象的默认初始化方式。举个例子:
class Cicle
{
float m_radius;
3DPoint m_center;
};
这个类,编译器自动生成的拷贝构造函数就是大概这么实现的:
Cicle(const Circle& other)
{
m_radius = other.m_radius;
m_center = 3DPoint(other.m_radius);//调用成员类的copy构造函数。依次递归,直到最底层基本类型数据。
}
那么我们发现,最终调用的是基本数据类型的复制方式。那么基本类型的复制,就是一块内存里面的数据内容复制,如果没有说明指定方式,那么复制方式就是直接一个位一个位内容的copy。由此,我们现在来看改造类后的第二个test()函数的执行过程。在函数返回时,会调用对象p1、p2的析构,2个析构函数都会进行内存释放操作。如果上面改成指针后,却没有拷贝构造函数时,那么默认编译器会自动生成一个,但是默认生成的拷贝构造函数执行对象“克隆”时,采用的是3DPoint 的默认拷贝构造函数。那么,3DPoint p2 = p1; 这句执行后, 对象p2的成员m_x, m_y,m_z 指针就是将p1 中的成员 m_x, m_y,m_z 指针内容直接copy过来, 这样,就造成了两个类对象的指针指向的是同样的内存地址。因此p1,p2两个对象的成员m_x,m_y,m_z指针指向的对象是完全一样的。在析构时,就会造成同一块内存被2次释放!对此,解决方案,就是重写copy构造函数。对涉及到的成员指针对象进行“深copy”,即自己进行内存copy。
总结: 拷贝构造函数的深入理解是C++入门的一个关键问题。对于一个类,尽可能的去自己实现其copy构造函数和operator=() 函数,而不要采用默认的生成函数。这样避免类内存方面的错误。
网友评论