浅谈C++类的拷贝控制
Posted Coder学习路
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈C++类的拷贝控制相关的知识,希望对你有一定的参考价值。
点击蓝字
在C++语言的学习过程中,类的拷贝控制是一个较为繁杂的知识点。虽然它的难度不是很大,但是细节很多,需要记忆。本文简略地介绍一下C++类的拷贝控制基本内容,即拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数这 5 个函数的写法。
为了便于介绍,我们自己实现一个简单的 string 类,命名为 String 。它只包含一个私有的数据成员:char *data ,函数成员包括:两个构造函数和五大拷贝控制函数。
类的定义如下:
class String
{
private:
char *data;
public:
String() : data(nullptr) {}
String(const char *s);
// 拷贝构造函数
String(const String &s);
// 拷贝赋值运算符
String &operator=(const String &s);
// 移动构造函数
String(String &&s);
// 移动赋值运算符
String &operator=(String &&s);
// 析构函数释放内存
~String();
};
为了介绍五大拷贝控制函数,还需要先看类的两个构造函数。
第一个为默认构造函数,仅仅把 data 赋值为空指针。
第二个构造函数接收一个 const char * 类型指针,动态申请内存并从参数中拷贝构造字符串。代码实现如下:
String(const char *s)
{
int length = strlen(s);
data = new char[length + 1];
if (data)
{ // 拷贝
strcpy(data, s);
data[length] = '\0'; // 尾 0
}
else
{
cout << "Error!" << endl;
exit(-1);
}
}
接下来正式进入拷贝控制的内容。
先从最简单的析构函数开始:
1、析构函数
析构函数的声明形式为:
~ClassName() { /* 函数体 */ }
析构函数一般进行类的资源释放,对于本String 类,需要释放 data 指针对应的内存,代码如下:
// 析构函数释放内存
~String()
{
if (data)
delete[] data;
}
何时调用析构函数:
-
变量离开作用域时 -
当一个对象被销毁时,其成员被销毁 -
容器(标准库容器以及数组)被销毁时,其元素被销毁 -
动态分配对象被 delete 时 -
对于临时对象,当创建它的完整表达式结束时被销毁
2、拷贝构造函数
拷贝构造函数声明形式为:
ClassName(const ClassName &s) { /* 函数体 */ }
注意点:
-
拷贝构造函数是构造函数, 无返回类型 -
拷贝构造函数的参数类型为 const 引用类型 -
拷贝构造函数一般需要动态申请内存,进行深拷贝工作,对于本 String 类,需要申请内存,并从参数字符串中拷贝数据
String 类的拷贝构造函数实现如下:
// 拷贝构造函数
String(const String &s)
{
cout << "拷贝构造函数"
<< "\t" << __LINE__ << ":\t" << __FUNCTION__ << endl;
if (s.data)
{
int length = strlen(s.data);
data = new char[length + 1]; // 分配内存空间
if (data)
{
strcpy(data, s.data);
data[length] = '\0';
}
else
{
cout << "Error!" << endl;
exit(-1);
}
}
else
{
data = nullptr;
}
}
何时调用拷贝构造函数:
-
用 = 定义变量时 -
将一个对象作为实参传递给一个非引用类型的形参(函数调用以值传递) -
从一个返回类型为非引用类型的函数返回一个对象(函数返回) -
用花括号列表初始化一个数组中的元素或者一个聚合类中的成员
3、拷贝赋值运算符
拷贝赋值运算符声明形式为:
ClassName & operator= (const ClassName &s) { /* 函数体 */ }
注意点:
-
拷贝赋值运算符是一种运算符重载,可以看作名字为:operator= 的函数,其 参数类型为 const 引用类型, 返回类型为引用类型 -
拷贝赋值运算符一般需要执行析构函数和构造函数的功能 -
需要考虑自赋值情况,即:a = a ,确保自赋值情况拷贝赋值运算符正确执行。为了保证自赋值正确执行,一般采取:先申明临时变量,然后将参数拷贝到临时变量,最后将临时变量拷贝到本对象 的步骤
String 类的拷贝赋值运算符实现如下:
// 拷贝赋值运算符
String &operator=(const String &s)
{
cout << "拷贝赋值运算符"
<< "\t" << __LINE__ << ":\t" << __FUNCTION__ << endl;
int length = strlen(s.data);
char *temp = new char[length + 1]; // 分配内存空间
if (temp)
{
strcpy(temp, s.data);
temp[length] = '\0';
delete[] data; // 释放内存空间
data = temp;
}
else
{
exit(-1);
}
return *this;
}
何时调用拷贝赋值运算符:
-
赋值符号右侧为一个左值时
4、移动构造函数
移动构造函数声明形式为:
ClassName(ClassName &&s) { /* 函数体 */ }
注意点:
-
移动构造函数属于构造函数,无返回类型。其 参数类型为右值引用, 不取const 类型 -
移动构造函数的特性在于“移动”,它一般执行“浅拷贝”,即不需要动态申请内存,而是将 s 的动态内存指针赋值给本对象的指针,然后将 s 的指针赋值为空
String类的移动构造函数实现:
// 移动构造函数
String(String &&s)
{
cout << "移动构造函数"
<< "\t" << __LINE__ << ":\t" << __FUNCTION__ << endl;
if (this != &s) // 排除自赋值情况
{
if (data)
delete[] data; // 释放本对象内存
data = s.data; // 直接赋值 不申请内存
s.data = nullptr; // 将赋值运算符左侧对象指针置为空
}
}
何时调用移动构造函数:
-
显式地从一个右值进行构造时,例如:利用标准库 move 函数返回一个右值进行构造 -
利用移动迭代器构造时,具体内容请自行查阅相关资料
5、移动赋值运算符
移动赋值运算符声明形式为:
ClassName &operator= ( ClassName &&s) { /* 函数体 */ }
注意点:
-
移动赋值运算符同样是一种运算符重载,其返回类型为类的引用类型,参数类型为类的右值引用, 不是 const 类型 -
移动赋值运算符特性同样在“移动”二字,它不执行拷贝,执行的动作一般拷贝析构本对象原有的动态内存,然后将参数对象动态内存指针赋值给本对象内指针,最后将参数对象指针赋值为空。正因为该函数需要改变参数,因此不申明为 const 类型
String 类移动赋值运算符实现:
// 移动赋值运算符
String &operator=(String &&s)
{
cout << "移动赋值运算符"
<< "\t" << __LINE__ << ":\t" << __FUNCTION__ << endl;
if (this != &s) // 排除自赋值情况
{
if (data)
delete[] data; // 释放本对象内存
data = s.data; // 直接赋值 不申请内存
s.data = nullptr; // 将赋值运算符左侧对象指针置为空
}
return *this;
}
何时调用移动赋值运算符:
-
赋值符号右侧为一个右值时
测试函数:
int main()
{
String s1; // 默认构造函数
String s2("I'm String.\n"); // 由字符串构造
String s3 = s2; // 拷贝构造函数
s1 = s2; // 拷贝赋值运算符
String s4(move(s1)); // 移动构造函数
s2 = String("hhh"); // 移动赋值运算符
return 0;
}
运行结果:
备注:
本文主要参考书籍:《C++ Primer 第五版》 第 13 章拷贝控制。
拷贝控制是学习 C++ 过程中的一个大坑,尤其是移动构造与移动赋值运算符,这二者涉及标准库的实现,若代码编写正确,能很大程度减少拷贝,提高程序运行效率;若代码编写有bug,可能导致runtime error等错误。此外编译器优化、编译器版本支持等也可能使程序运行结果与预期不同。上文仅是最简略的介绍,更多内容请阅读书籍。
以上是关于浅谈C++类的拷贝控制的主要内容,如果未能解决你的问题,请参考以下文章