拷贝构造函数(Copy Constructor)
前言
拷贝构造函数也称为复制构造函数。
0x00 拷贝构造函数
以 VectorInt2D 类为例子:
class VectorInt2D
{
public:
VectorInt2D()
{
Init();
}
VectorInt2D(int x, int y)
{
Init(x, y);
}
void Init(int x = 0, int y = 0)
{
uint32_t nLen = strlen("VectorInt2D") + 1;
m_pszClassName = new char[nLen];
strcpy_s(m_pszClassName, nLen, "VectorInt2D");
this->x = x;
this->y = y;
}
~VectorInt2D()
{
if (m_pszClassName != nullptr)
{
delete m_pszClassName;
m_pszClassName = nullptr;
}
}
char* GetClassName()
{
return m_pszClassName;
}
private:
char* m_pszClassName;
int x;
int y;
};
用一个已经存在的对象初始化一个新的对象,会调用拷贝构造函数。
所以拷贝构造函数的参数是类对象。但在传参的时候会要拷贝该已存在对象,为提高效率,因此拷贝构造函数的参数应该是类的引用。但不能修改该存在的对象,所以拷贝构造函数的参数应该是const类的引用。
VectorInt2D::VectorInt2D(const VectorInt2D& obj)
{
this->x = obj.x;
this->y = obj.y;
// 重新申请新的内存空间
uint32_t nLen = strlen(obj.m_pszClassName) + 1;
m_pszClassName = new char[nLen];
strcpy_s(m_pszClassName, nLen, obj.m_pszClassName);
}
0x01 默认的拷贝构造函数
如果开发者没有提供拷贝构造函数,编译器将会提供一个默认的拷贝构造函数。默认的拷贝构造函数本质上是memcpy。
class VectorInt2D
{
public:
VectorInt2D()
{
Init();
}
VectorInt2D(int x, int y)
{
Init(x, y);
}
void Init(int x = 0, int y = 0)
{
uint32_t nLen = strlen("VectorInt2D") + 1;
m_pszClassName = new char[nLen];
strcpy_s(m_pszClassName, nLen, "VectorInt2D");
this->x = x;
this->y = y;
}
~VectorInt2D()
{
if (m_pszClassName != nullptr)
{
delete m_pszClassName;
m_pszClassName = nullptr;
}
}
char* GetClassName()
{
return m_pszClassName;
}
private:
char* m_pszClassName;
int x;
int y;
};
int main(int argc, char* argv[])
{
VectorInt2D pos1(2, 1);
VectorInt2D pos2 = pos1; // 调用默认的拷贝构造函数,也就是memcpy,导致两个对象的 m_pszClassName 指向同一块堆内存,而对象析构时却被析构两次
return 0;
}
运行结果:
C语言中可以用一个结构体变量直接赋值给另一个新定义的结构体变量,本质上是memcpy。类要兼容C的结构体,所以类对象之间的赋值也是memcpy。这也就是调用默认的拷贝构造函数时,本质上是memcpy。这将会导致两个对象中的指针成员将会指向同一块内存。进而会导致析构函数调用时,重复释放资源的问题。
所以如果类中有开辟内存空间的时候,需要进行深拷贝,把数据也拷贝到新对象中。
0x02 拷贝构造函数调用的时机
2.0 用一个已存在的对象初始化一个新的对象
以下两种形式均会调用拷贝构造函数:
int main(int argc, char* argv[])
{
VectorInt2D pos1(2, 1);
VectorInt2D pos2 = pos1; // 形式1
VectorInt2D pos3(10, 20);
VectorInt2D pos4(pos3); // 形式2
return 0;
}
形式 2 还比较好理解,因为拷贝构造函数的参数就是类对象。
主要是形式1,要区别于两个对象的赋值:
如果开发者没有提供赋值运算符重载函数,编译器默认的赋值运算符本质上是memcpy。
int main(int argc, char* argv[])
{
VectorInt2D pos1(2, 1);
VectorInt2D pos2(20, 10);
pos2 = pos1; // 这里会调用赋值运算符函数
return 0;
}
这里也会发生上面的情况,两个对象析构同一块堆内存,并且 pos2 原来的堆内存会泄露。
2.1 类对象作为函数的参数
void PrintVectorInt2D(VectorInt2D vec)
{
}
int main(int argc, char* argv[])
{
VectorInt2D pos1(2, 1);
PrintVectorInt2D(pos1); // 拷贝构造 将实参pos1拷贝到形参vec
return 0;
}
实际上类对象作为函数的参数时,准确的说,会自动匹配构造函数,而不是只匹配拷贝构造函数。
参考:C++中的explicit
2.2 类对象作为函数的返回值
VectorInt2D RetrunVectorInt2D()
{
VectorInt2D pos1(2, 1); // 调用构造函数
return pos1; // 调用拷贝构造 然后pos1调用析构
}
int main(int argc, char* argv[])
{
VectorInt2D pos2 = RetrunVectorInt2D(); // 直接将pos1拷贝构造给pos2
return 0;
}