通常,管理类外资源的类必须定义拷贝控制成员。为了定义这些成员,我们首先必须确定此类型对象的拷贝语义。一般有两种选择:使类的行为看起来像一个值或者像一个指针。
行为像值的类
类的行为像一个值,意味着它应该有自己的状态。当我们拷贝一个像值的对象时,副本和源对象是完全独立的。改变副本不会对原对象有任何影响。如标准库类型中string
1、定义一个类值行为的类
#include<iostream>
#include<string>
using namespace std;
class hasptr
{
public:
hasptr(const string &s = string()) :
ps(new string(s)) ,i(0) {}
hasptr(const hasptr &p) : //拷贝构造函数
ps(new string(*p.ps)), i(p.i) {}
hasptr& operator=(const hasptr&);
hasptr& operator=(const string&); //拷贝赋值运算符
string& operator*(); // 重载解引用
~hasptr();
private:
string *ps;
int i;
};
hasptr::~hasptr()
{
delete ps;
}
inline hasptr& hasptr::operator=(const hasptr&rhs)
{
auto newps = new string(*rhs.ps);
delete ps;
ps = newps;
i = rhs.i;
return *this;
}
hasptr& hasptr::operator=(const string& rhs)
{
*ps = rhs;
return *this;
}
string& hasptr::operator*()
{
return *ps;
}
int main()
{
hasptr h("hi mom!");
hasptr h2(h);
hasptr h3 = h; //行为类值,指向不同部分,互不影响
h2 = "hi,dad!";
h3 = "hi,son!";
cout << "h: " << *h << endl;
cout << "h2: " << *h2 << endl;
cout << "h3: " << *h3 << endl;
getchar();
}
对于上面例子的说明:
- 定义拷贝构造函数时,是对string的拷贝,而不是对指针的拷贝。
- 类值行为的拷贝赋值运算符,通常综合了析构函数和构造函数的操作。赋值操作会销毁左侧运算对象的资源,并且类似于拷贝构造函数,赋值操作会从右侧运算对象拷贝数据。最重要的是:这些操作要以正确的顺序执行,即将一个对象赋予它自身,赋值运算符可以正常工作,自赋正确。如以下就是错误的:
//此时不能将一个对象赋予它自身
inline hasptr& hasptr::operator=(const hasptr&rhs)
{
delete ps;
auto newps = new string(*rhs.ps);
ps = newps;
i = rhs.i;
return *this;
}
行为像指针的类
行为像指针的类共享状态,当我们拷贝一个这种类对象时,副本和原对象使用相同的底层数据。改变副本也会改变原对象。例如在动态内存中讲到的shared_ptr类就是。
1、定义一个行为像指针的类
#include<string>
#include<iostream>
using namespace std;
class hasptr {
public:
hasptr(const string &s = string()) :
ps(new string(s)), i(0), use(new size_t(1)) {}
hasptr(const hasptr&p) :
ps(p.ps), i(p.i), use(p.use) {
++*use;
}
hasptr& operator=(hasptr&);
hasptr& operator=(const string&);
string& operator*();
~hasptr();
private:
string *ps;
int i;
size_t *use; //引用计数类似shared_ptr,用来记录有多少成员共享*ps的成员
};
hasptr::~hasptr()
{
if (--*use == 0) //如果引用计数变为0,就释放对应的内存
{
delete ps;
delete use;
}
}
hasptr& hasptr::operator=(hasptr& rhs)
{
++*rhs.use;//应该放这里,处理自赋值
if (--*use == 0)
{
delete ps;
delete use;
}
ps = rhs.ps;
i = rhs.i;
use = rhs.use;
return *this;
}
hasptr& hasptr::operator=(const string&rhs)
{
*ps = rhs;
return *this;
}
string& hasptr::operator*()
{
return *ps;
}
int main()
{
hasptr h("hi,mom!");
hasptr h2 = h; //未分配新string,h2和h指向相同的string
h = "hi,dad!";
cout << "h:" << *h << endl;
cout << "h2:" << *h2 << endl;
getchar();
}
由上面结果可以看到h和h2指向相同的string。
对于上数例子的说明:
- 我们定义其拷贝构造函数和拷贝赋值运算符时,拷贝的是指针成员本身,而不是他指向的内容。
我们的析构函数中不能单方面的释放所关联的对象。只有当最后一个指向对象的销毁时才可以释放string,在这里我们学习shared_ptr中,使用一个引用计数来记录有多少个用户指向共享的对象,当引用计数变为0时,才会释放所关联的内存。
引用计数
1、在我们创建对象时,构造函数除了初始化每个对象,还要创建一个引用计数,记录多少个对象来共享内容
2、拷贝构造函数不会分配新的计数器,而是拷贝给定的数据成员,包括计数器,并且会递增计数器的值。
3、析构函数递减计数器,当计数器的值变为0时,析构函数会释放对应的关联内存。
4、拷贝赋值运算符,递减左侧运算对象计数器,递增右侧运算对象计数器,如果左侧运算对象的计数器值变为0,就必须释放对应的内存。
然而引用计数应该存放在哪里?
对于这个问题,假如计数器直接作为hasptr对象的成员:
hasptr p1("hai");
hasptr p2(p1);
hasptr p3(p1);
/*当我们创建p3时我们可以递增p1中的计数器的值,并且将其拷贝到p3中,这时我们没办法更新p2中的计数器*/
我们将计数器的值保存在动态内存中,当创建一个对象时,我们也分配一个新的计数器。当拷贝和赋值对象时,我们拷贝指向计数器的指针,使原对象和副本都会指向相同的计数器。
交换操作
除了定义拷贝控制成员,管理资源的类通常还定义一个名为swap的函数。与拷贝控制成员不同,swap并不是必要的。但是,对于分配了资源的类,定义swap可能是一种重要的优化手段。
例如为上述的类值版本的hasptr 定义一个swap函数:
class hasptr
{
friend void swap(hasptr&, hasptr&);
//和上述类似
};
inline void swap(hasptr &lhs, hasptr&rhs)
{
cout << "交换 " << *lhs.ps << "和" << *rhs.ps << endl;
using std::swap;
swap(lhs.ps, rhs.ps);
swap(lhs.i, rhs.i);
}
int main()
{
hasptr h("hi mom");
hasptr h2(h);
hasptr h3 = h;
h2 = "hi dad!";
h3 = "hi son!";
swap(h2, h3);
cout << "h: " << *h << endl;
cout << "h2: " << *h2 << endl;
cout << "h3: " << *h3 << endl;
getchar();
}
上述例子的说明:
上述的swap函数中又调用了swap函数不会导致递归循环,因为这两个成员是内置类型,因此在函数中的调用会被解析成std::swap,而不是hasptr的特定版本的swap。