在知乎上关注了一些C++相关的话题。看到一个答主说:
也就是说,如果一个class有析构函数,并且析构函数有释放资源的操作,那么作者应该对拷贝构造和拷贝赋值函数有所处理,要么提供正确实现,要么delete。否则,这个class不是一个完整安全的设计。
这个回答所在的问题是“C++后台开发有哪些练基础的开源项目?”。提问者应该是想通过一些开源项目来学习和实践C++。很多回答都给出了不少Github上的优秀项目的链接。
然而这个答主却只给出了这么简短的回答。
这个答主是:陈硕。
好比每一个学习C++的人都知道侯捷大师一样,每个C++开发者想必也都听说过陈硕。(如果没有,那么看到这里你也听说过了)。比起Github上纷繁复杂的开源项目,陈硕大佬说,项目太多,你要能够找到真正好的项目来学习。如何判断一个项目的优劣?答案就是上面那段话。
简单解释下为什么析构函数里有释放资源的操作后,就得处理拷贝构造和拷贝赋值。
比如下面一个class A:
class A
{
public:
A(char ia = ' ', short ib = 0, int ic = 0) : a(ia), b(ib), c(ic) {}
private:
char a;
short b;
int c;
};
对于这个class,不需要析构函数,因为class中没有涉及申请内存资源的操作,对于基础的数据类型,对象析构时也不涉及释放资源的操作。
但如果类中涉及到申请资源的操作,如果没有自己定义拷贝构造函数和拷贝赋值函数,编译器默认生成的版本都是浅拷贝。那么当源对象析构过程释放掉资源时(delete ptr),浅拷贝生成的对象里的ptr指向的资源实际已经被回收了,那么拷贝的对象中的ptr实际指向的资源已经不存在,当再次析构时将引发异常。比如下面的class B:
class B
{
public:
B() {}
B(int ilen, const char *iname) : len(ilen)
{
name = new char[len];
memcpy(name, iname, len);
}
~B()
{
delete[] name;
}
void print()
{
cout << "len = " << len << " name = " << name << endl;
}
private:
char *name;
int len;
};
int main(int argc, char *argv[])
{
B b1(4, "abc");
b1.print();
B b2 = b1;
b2.print();
return 0;
}
该程序退出时会引发异常,因为退出时,两个栈对象b1和b2将依次调用析构函数。B的析构函数中会释放掉name指针指向的内存空间。但由于没有提供B的拷贝构造函数的定义,因此下面的代码将引发浅拷贝:
B b2 = b1;
因此b2和b1中的name实际指向同一块资源。当b1析构时,b1和b2指向的资源已经被释放。当b2析构时,再去delete一个已经不存在的资源,引发异常。
(当然,如果B中没有定义析构函数,或者析构函数中没有去delete,那么上述代码也不会出现问题。但是new的内存没有释放,最后将交给OS释放,这也不是一种好的编码,更不是一种好的编程习惯!)
解决办法是,要么我完全禁止拷贝构造和拷贝赋值的行为:
class B
{
public:
B() {}
B(int ilen, const char *iname) : len(ilen)
{
name = new char[len];
memcpy(name, iname, len);
}
B(const B& other) = delete;
B& operator=(const B& other) = delete;
~B()
{
delete[] name;
}
private:
char *name;
int len;
};
要么就提供拷贝构造和拷贝赋值的实现:
class B
{
public:
B() {}
B(int ilen, const char *iname) : len(ilen)
{
name = new char[len];
memcpy(name, iname, len);
}
B(const B& other)
{
len = other.len;
name = new char[len];
memcpy(name, other.name, len);
}
B& operator=(const B& other)
{
if(this != &other){
delete []name;
len = other.len;
name = new char[len];
memcpy(name, other.name, len);
}
return *this;
}
~B()
{
delete[] name;
}
private:
char *name;
int len;
};
所以万丈高楼平地起,纷繁复杂的项目令人眼花缭乱,但稳固的代码基础才是基石。