【C++基本语法】系列用来记录C++常用基本语法,惯用法。本文主要回忆我们最常写的构造函数(尤其是拷贝构造和移动构造),赋值运算和析构函数。由于本人长期cv,以至于最近在裸写这些函数的时候发现竟然不是一气呵成。
本文的示例也是初、中级程序员的面试题。另外文中会参考《Effective C++》的表述。
1. 构造函数和operator =
假设这样一个类,你会如何写它的构造函数和赋值运算符。你能一次写对吗?本文的源码和测试代码放在文章末尾,大家可以拷下来测试验证。
class A{
public: //这里把成员变量定义成public是偷懒,为了不写get, set
int* data_ = nullptr;
int size_;
};
C++11之后,一般我们会写这些: 构造函数、拷贝构造函数、拷贝赋值函数、移动构造函数、移动赋值函数、析构函数。
成员中是否有指针对这些函数的影响较大,这里我们以有指针为例探讨。
1.1 默认构造
考虑如何安全地进行默认构造;
A::A(){
cout<<"调用默认构造函数, ";
size_ = 10;
data_ = new int[size_];
for(int i = 0; i < size_; ++i){
data_[i] = i;
}
printData();
}
1.2 拷贝构造函数
安全的拷贝构造应该考虑什么问题?
A::A(const A& a){
cout<<"调用拷贝构造函数, ";
size_ = a.size_;
if (size_ <= 0){
size_ = 0;
data_ = nullptr;
std::cerr<<"size "<<size_<<"is illegal\n"; //构造函数中调用打印合适吗,安全吗
}else{
data_ = new int[size_];
for(int i = 0; i < size_; ++i){
data_[i] = a.data_[i];
}
printData();
}
}
这是我现写的拷贝构造函数,对异常的处理并不漂亮。关于构造函数有几点注意:
- 函数声明中入参为 const A&类型,拷贝构造一般不改变入参
- 拷贝构造的拷贝应是深拷贝,尤指指针对象
- 注意下面这两种写法,A a2 = a1;并非是拷贝赋值,二是拷贝构造。
A a1;
A a2 = a1; //这是拷贝构造,不是赋值!
A a3;
a3 = a2; //这里是拷贝赋值
1.3 拷贝赋值运算符
下面是我的代码,并不优雅。但有两条应牢记
A& A::operator=(const A& a){
cout<<"调用拷贝赋值运算符, ";
if (this == &a){
cout<<" 赋值给自己了!\n";
return *this;
}
size_ = a.size_;
if (size_ > 0){
delete [] data_; // 这里为什么要delete
data_ = new int[size_];
for(int i = 0; i < size_; ++i){
data_[i] = a.data_[i];
}
printData();
}
return *this;
}
1.3.1 《Effective C++》条款10*请记住:令operator = 返回一个reference to this
为了实现连锁赋值(x = y = z = 15 或者 x = (y = (z = 15))),赋值运算符必须返回一个reference指向操作符左侧的实参,所以我们应当遵循这样的协议:
Widget operator=(const Widget& rhs){
...
return *this
}
当然,该协议也适用于所有的赋值运算,比如 operator+=。该协议非强制性,但所有人都遵守,我们最好也这么做。
1.3.2 《Effective C++》条款11在operator = 处理自我赋值
“自我赋值”发生在对象被赋值给自己时`
// 情形1
Widget w;
w = w; // 自我赋值
// 情形2
a[i] = a[j]; // i == j 时,潜在的自我赋值
// 情形3
*px = *py; //px 和py指向同一对象时,潜在的自我赋值
// 情形4
class Base{};
clase Derived : public Base{...};
void doSomething(const Base& rb, Derived * pd); // rb 和 *pd有可能指向同一对象
下面是这个实现看起来合理,但自我赋值时会有什么安全问题?
A& A::operator=(const A& rhs){
delete data_; //销毁当前对象的data_
data_ = new int[rhs.size_]; // 假设size_合法,使用rhs的副本
//copy rhs->data to this->data_
return *this;
}
这里自我赋值的问题是,如果*this 和rhs是同一个对象,那么delete操作就销毁了当前对象的data_。如何解决呢,可以通过“识别自我的方式”:
A& A::operator=(const A& rhs){
if (this == &rhs) return *this; //证同测试(identity test)
...
data_ = new int[rhs.size_]; // 假设size_合法,使用rhs的副本
//copy rhs->data to this->data_
return *this;
}
这个版本仍然在异常情况存在麻烦,书上推荐把注意力放在针对异常安全性(exception safety)(条款29),于是下面这种写法被提出(这种方式并不高效,其重点是在赋值rhs的对象内容之前不删除当前对象管理的内容):
A& A::operator=(const A& rhs){
if (this == &rhs) return *this; //证同测试(identity test)
...
int * tmp = data_; //记住原先的data
data_ = new int[rhs.size_]; // 假设size_合法,使用rhs的副本
//copy rhs->data to this->data_
delete tmp; //删除原先的data
return *this;
}
书上最后介绍了 copy and swap技术,这里不展开了。
看到这里之后,我把我的代码改成了如下。但在这个例子中,感觉意义不大,如果new失败了,data_是否指向原内容也就不重要了。
A& A::operator=(const A& rhs){
cout<<"调用拷贝赋值运算符, ";
if (this == &rhs){
cout<<" 赋值给自己了!\n";
return *this;
}
int * tmp_data = data_; //先保存当前内容
if (rhs.size_ > 0){
size_ = rhs.size_;
data_ = new int[size_];
for(int i = 0; i < size_; ++i){
data_[i] = rhs.data_[i];
}
delete [] tmp_data; // 拷贝完之后再删除
printData();
}
return *this;
}
1.4 移动构造函数
A::A(A&& rhs){
cout<<"调用移动构造函数, ";
if(this != &rhs){
delete[] data_;
data_ = rhs.data_;
size_ = rhs.size_;
rhs.data_ = nullptr; //这句不可缺少
rhs.size_ = 0;
}
printData();
}
- 移动构造的形参非const类型
- 移动构造一般是浅拷贝过程
- 注意:移动构造后,rhs的指针必须置空,否则rhs析构时,this->data_管理的内容也将被释放,从而引起coredump。
1.5 移动赋值运算符
A& A::operator=(A&& rhs){
cout<<"调用移动赋值运算符, ";
if (this != &rhs){
delete[] data_;
data_ = rhs.data_;
size_ = rhs.size_;
rhs.data_ = nullptr;
rhs.size_ = 0;
}
printData();
return *this;
}
- 移动赋值运算符,同样要注意rhs.data_=nullptr不可少
- 同样要注意自我赋值
1.6 析构函数
~A(){
// cout<<"调用析构函数\n";
delete[] data_; // 无需判断data_是否为nullptr,delete已做相关处理
data_ = nullptr;
}
2 源码加测试代码
#include <iostream>
using namespace std;
class A{
public:
int* data_ = nullptr;
int size_;
public:
A(){
cout<<"调用默认构造函数, ";
size_ = 10;
data_ = new int[size_];
for(int i = 0; i < size_; ++i){
data_[i] = i;
}
printData();
}
A(int size): size_(size){
if (size_ <= 0){
size_ = 10;
std::cout<<"size "<<size<<"is illegal\n";
}
data_ = new int[size_];
for(int i = 0; i < size_; ++i){
data_[i] = i + size;
}
cout<<"调用带参构造函数: ";
printData();
}
A(const A& rhs){
cout<<"调用拷贝构造函数, ";
size_ = rhs.size_;
if (size_ <= 0){
size_ = 0;
data_ = nullptr;
std::cerr<<"size "<<size_<<" is illegal\n";
}else{
data_ = new int[size_];
for(int i = 0; i < size_; ++i){
data_[i] = rhs.data_[i];
}
printData();
}
}
A& operator=(const A& rhs){
cout<<"调用拷贝赋值运算符, ";
if (this == &rhs){
cout<<" 赋值给自己了!\n";
return *this;
}
int * tmp_data = data_;
if (rhs.size_ > 0){
size_ = rhs.size_;
data_ = new int[size_];
for(int i = 0; i < size_; ++i){
data_[i] = rhs.data_[i];
}
delete [] tmp_data;
printData();
}
return *this;
}
A(A&& rhs){
cout<<"调用移动构造函数, ";
if(this != &rhs){
delete[] data_;
data_ = rhs.data_;
size_ = rhs.size_;
rhs.data_ = nullptr;
rhs.size_ = 0;
}
printData();
}
A& operator=(A&& rhs){
cout<<"调用移动赋值运算符, ";
if (this != &rhs){
delete[] data_;
data_ = rhs.data_;
size_ = rhs.size_;
rhs.data_ = nullptr;
rhs.size_ = 0;
}
printData();
return *this;
}
void printData(){
std::cout<<"data is : ";
for (int i = 0; i < size_; ++i){
std::cout<<data_[i]<<" ";
}
std::cout<<std::endl;
}
~A(){
// cout<<"调用析构函数\n";
delete[] data_; // 无需判断data_是否为nullptr,delete已做相关处理
data_ = nullptr;
}
};
void test()
{
// 默认构造测试;
cout<<"case 1 默认构造测试: \n";
A a;
// 带参构造函数测试
cout<<"\ncase 2 带参构造函数测试: \n";
A a1(10);
// 拷贝构造函数测试
cout<<"\ncase 3 拷贝构造函数测试: \n";
A a2(a1);
A a3 = a1; //这是拷贝构造,不是赋值!
// 移动赋值运算符
cout<<"\ncase 4 拷贝赋值运算符测试: \n";
A a4;
a4 = a3;
a4 = a4;
// 移动构造函数
cout<<"\ncase 5 移动构造函数测试: \n";
A tmp;
A a6(std::move(tmp));
// 移动赋值运算符
cout<<"\ncase 5 移动赋值运算符测试: \n";
A a7(30), a8;
a8 = std::move(a7);
}
void test_illegal()
{
A a1(-1);
A a2(a1);
a2 = a1;
}
int main()
{
test();
test_illegal();
return 0;
}
执行结果
case 1 默认构造测试:
调用默认构造函数, data is : 0 1 2 3 4 5 6 7 8 9
case 2 带参构造函数测试:
调用带参构造函数: data is : 10 11 12 13 14 15 16 17 18 19
case 3 拷贝构造函数测试:
调用拷贝构造函数, data is : 10 11 12 13 14 15 16 17 18 19
调用拷贝构造函数, data is : 10 11 12 13 14 15 16 17 18 19
case 4 拷贝赋值运算符测试:
调用默认构造函数, data is : 0 1 2 3 4 5 6 7 8 9
调用拷贝赋值运算符, data is : 10 11 12 13 14 15 16 17 18 19
调用拷贝赋值运算符, 赋值给自己了!
case 5 移动构造函数测试:
调用默认构造函数, data is : 0 1 2 3 4 5 6 7 8 9
调用移动构造函数, data is : 0 1 2 3 4 5 6 7 8 9
case 5 移动赋值运算符测试:
调用带参构造函数: data is : 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
调用默认构造函数, data is : 0 1 2 3 4 5 6 7 8 9
调用移动赋值运算符, data is : 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
size -1 is illegal
调用带参构造函数: data is : -1 0 1 2 3 4 5 6 7 8
调用拷贝构造函数, data is : -1 0 1 2 3 4 5 6 7 8
调用拷贝赋值运算符, data is : -1 0 1 2 3 4 5 6 7 8
关于这个主题,我也没有进行深入研究,上述代码一定有不合理,不优雅的地方,请有缘人指正。
参考链接/文献
《Effective C++》