本例只是对含有本类对象指针的类的构造函数、析构函数、拷贝构造函数、复制运算符使用方法的一个简单示例,以加深对构造函数和拷贝控制成员的理解。
读C++ primer 5th 第13章后加上自己的理解,完整的写了下课后习题的代码。
第一版:
#include <string>
#include <iostream>
using namespace std;
class TreeNode{
private:
string value;
TreeNode *left;
TreeNode *right;
public:
TreeNode() : value(""), left(nullptr), right(nullptr){}
~TreeNode(){
cout << "~TreeNode()" << endl;
if (left != nullptr){
delete left; //递归析构左子树
}
if (right != nullptr){
delete right;//递归析构右子树
}
}
TreeNode(const TreeNode &tn) : value(tn.value), left(nullptr), right(nullptr){
if (tn.left != nullptr){
left = new TreeNode(*tn.left);//递归复制拷贝左子树 (其实又一次调用了以(*tn.left)作为参数的复制构造函数)
}
if (tn.right != nullptr){
right = new TreeNode(*tn.right);//递归复制拷贝右子树 (其实又一次调用了以(*tn.left)作为参数的复制构造函数)
}
}
TreeNode & operator=(const TreeNode& tn){
value = tn.value;
TreeNode * pl, *pr;
//左侧的对象因为要被覆盖,所以记得如果左侧对象中的指针已经保持有对象,要记得释放资源,否则就会内存泄露了
if (left != nullptr){
delete left;
left = nullptr;
}
if (right != nullptr){
delete right;
right = nullptr;
}
if (tn.left != nullptr){
left = new TreeNode(*tn.left);//递归赋值左子树 (去调用复制构造函数,这样就会去构造新new出来的这个对象的中保存的对象指针left和right,构造完后此new出来的地址赋予left)下同
}
if (tn.right != nullptr){
right = new TreeNode(*tn.right);//递归复制右子树
}
return (*this);
}
TreeNode *getLeft()const{
return left;
}
TreeNode *getRight()const{
return right;
}
void setLeft(TreeNode * const le){
left = le;
}
void setRight(TreeNode * const ri){
right = ri;
}
};
以上这一版已经能完成正常的复制构造,赋值操作以及正常的析构,并且不会造成内存泄露,但是,这里有一个问题就是不能支持自身给自身赋值,因为一旦给自己赋值,就会出现,现将自己的左右子树析构了,然后再用左右子树做参数,这就会出现未定义的行为,虽然多次运行可能都能得到正确结果,但是确实是非常危险的行为。下面是改进版,此版就支持自身赋值。
ps(借用一下C++ primer中的提示:当你编写一个赋值运算符时,一个好的模式是先将右侧运算对象拷贝到一个局部临时对象中,这样销除左侧运算对象的现有成员就是安全的了,一旦左侧运算对象的资源被销毁,就只剩下将数据从临时对象拷贝到左侧运算对象的成员中了。) 说的确实经典。。。。
改进版:
#include <string>
#include <iostream>
using namespace std;
class TreeNode{
private:
string value;
TreeNode *left;
TreeNode *right;
public:
TreeNode() : value(""), left(nullptr), right(nullptr){}
~TreeNode(){
cout << "~TreeNode()" << endl;
if (left != nullptr){
delete left; //递归析构左子树
}
if (right != nullptr){
delete right;//递归析构右子树
}
}
TreeNode(const TreeNode &tn) : value(tn.value), left(nullptr), right(nullptr){
if (tn.left != nullptr){
left = new TreeNode(*tn.left);//递归复制拷贝左子树 (其实又一次调用了以(*tn.left)作为参数的复制构造函数)
}
if (tn.right != nullptr){
right = new TreeNode(*tn.right);//递归复制拷贝右子树 (其实又一次调用了以(*tn.left)作为参数的复制构造函数)
}
}
TreeNode & operator=(const TreeNode& tn){
value = tn.value;
TreeNode * pl, *pr;
pl = pr = nullptr;
if (tn.left != nullptr){
pl = new TreeNode(*tn.left);//递归赋值左子树 (去调用复制构造函数,这样就会去构造新new出来的这个对象的左指针和右指针,构造完后此new出来的地址赋予left)下同
}
if (tn.right != nullptr){
pr = new TreeNode(*tn.right);//递归复制右子树
}
//左侧的对象因为要被覆盖,所以记得如果左侧对象的指针已经保持有对象要记得释放资源,否则就会内存泄露了
if (left != nullptr){
delete left;
left = nullptr;
}
if (right != nullptr){
delete right;
right = nullptr;
}
//一下将临时对象赋值过来就ok了,这样的操作也很好的支持了自身赋值
left = pl;
right = pr;
return (*this);
}
TreeNode *getLeft()const{
return left;
}
TreeNode *getRight()const{
return right;
}
void setLeft(TreeNode * const le){
left = le;
}
void setRight(TreeNode * const ri){
right = ri;
}
};
以上的代码因为对指针类型的成员进行的是深度拷贝,所以效率比较低,也很容易看出来,每次赋值和赋值构造都要递归的不断分配新的内存保证赋值对象间能够互不影响。
一般情况下对于这种含有指针类型对象的类我们的很多操作可以直接更改指针来进行实现,但是这样的话就必须保证最终所有的内存都能正确的释放而且不能重复释放资源,很好的一种方式是使用智能指针,在这里先模拟一下智能指针即增加一个引用计数来实现正确拷贝及赋值等操作。
代码如下:
#include <string>
#include <iostream>
class TreeNode{
private:
string value;
TreeNode *left;
TreeNode *right;
int *use;
public:
TreeNode() : value(""), left(nullptr), right(nullptr), use(new int(1)){}
TreeNode(string str) : value(str), left(nullptr), right(nullptr), use(new int(1)){}
~TreeNode(){
//析构的时候,先把该对象的引用计数指针减一,如果引用计数不为0,则说明还有其他对象指向该对象的资源,这时候就不能析构掉
//本对象的资源,防止其他引用对象产生未定义行为。
if (--(*use) != 0){
return;
}
if (left != nullptr){
delete left; //递归析构左子树
}
if (right != nullptr){
delete right;//递归析构右子树
}
delete use;
}
//拷贝构造函数就比较简单了,只是单纯的公用同一内存即可,但是别忘了将引用计数器加1操作
TreeNode(const TreeNode &tn) : value(tn.value), left(tn.left), right(tn.right), use(tn.use){
++(*use);
}
TreeNode & operator=(TreeNode& tn){
//因为要复制右侧对象,所以先把右侧对象的计数增一,这样能解决自赋值问题
//这样在自赋值时,不会因为要清理左侧对象而导致未定义行为
++(*tn.use);
//左侧对象的要被覆盖掉,所以左侧对象的引用计数要先减一,如果减一后计数值位0,则说明左侧对象现在可以安全的清理掉
if (--*use == 0){
if (left != nullptr){
delete left;
}
if (right != nullptr){
delete right;
}
delete use;//记得把左侧对象的引用计数指针也清理掉
}
//因为左侧对象的资源已经正确处理(如果有的话,没有保持资源的话及上面的left和right为nullptr无需释放资源)
//则可以直接设置指针覆盖实现赋值运算的目的
left = tn.left;
right = tn.right;
use = tn.use;
return (*this);
}
bool operator< (const TreeNode &tn){
return (this->value < tn.value);
}
TreeNode *getLeft()const{
return left;
}
TreeNode *getRight()const{
return right;
}
void setLeft(TreeNode * const le){
left = le;
}
void setRight(TreeNode * const ri){
right = ri;
}
};
不断回顾,以加深对构造函数和拷贝控制成员的理解。