文章目录
- 概念
- 行为像值的类
- 行为像指针的类
- 概念
- 引用计数
- 动态内存实现计数器
- 类的swap
- 概念
- swap实现自赋值
概念
行为像值的类和行为像指针的类这两种说法其实蛮拗口的,这也算是 《C++Primer》 翻译的缺点之一吧。。。
其实两者的意思分别是:
- 行为像值的类: 每个类的对象都有自己的实现
- 行为像指针的类: 所有类的对象共享类的资源(类似于
shared_ptr
智能指针,每有一个对象持有该资源则引用计数+1,每有一个对象释放该资源则引用计数-1,引用计数为0时释放内存)
本篇博客的内容跟 类 和 智能指针 两篇博客有关。不了解的同学可以先看看这两篇博客。
行为像值的类
对于类管理的资源,每个对象都应该有一份自己的拷贝(实现)。如下面的 string类型的指针
,使用拷贝构造函数 or 赋值运算符时,每个对象拷贝的都是 指针成员ps 指向的 string
而非 ps本身
。换言之,每个对象 都有一个ps
而不是 给ps加引用计数
。
class A
{int i = 0;string* ps;
public:A(const string &s = string()): ps(new string(s)), i(0) {}A(const A &a): ps(new string(*a.ps)), i(a.i) {}A& operator=(const A&);~A() { delete ps; }
};A& A::operator=(const A& a)
{string* newps = new string(*a.ps); // 将a.ps指向的值拷贝到局部临时对象newps中delete ps; // 销毁ps指向的内存,避免旧内存泄漏ps = newps; i = a.i;return *this; // 返回此对象的引用
}
为什么不能像下面这样实现赋值运算符呢?
A& A::operator=(const A& a)
{delete ps; // 销毁ps指向的内存,避免内存泄漏ps = new string(*(a.ps)); i = a.i;return *this; // 返回此对象的引用
}
这是因为如果 a
和 *this
是 同一个对象,delete ps
会释放 *this
和 a
指向的 string
。接下来,当我们在 new表达式
中试图拷贝*(a.ps)
时,就会访问一个指向无效内存的指针(即空悬指针),其行为和结果是未定义的。
因此,第一种实现方法可以确保销毁 *this
的现有成员操作是绝对安全的,不会产生空悬指针。
行为像指针的类
概念
对于行为类似指针的类,使用拷贝构造函数 or 赋值运算符时,每个对象拷贝的都是 ps本身
而非 指针成员ps 指向的 string
。换言之,每有一个对象都是 给指向string的ps加引用计数
。
因此,析构函数不能粗暴地释放 ps
指向的 string
,只有当最后一个指向 string
的 A类对象
销毁时,才可以释放 string
。我们会发现这个特性很符合 shared_ptr
的功能,因此我们可以使用 shared_ptr 来管理 像指针的类 中的资源。
但是,有时我们需要程序员直接管理资源,因此就要用到 引用计数(reference count) 了。
引用计数
工作方式:
- 每个构造函数(拷贝构造函数除外)都要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态。当我们创建一个对象时,只有一个对象共享状态,因此将计数器初始化为1。
- 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新用户所共享。
- 析构函数递减计数器,指出共享状态的用户少了一个。如果计数器变为0,则析构函数释放状态。
- 拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器。如果左侧运算对象的计数器变为0,意味着它的共享状态没有用户了,拷贝赋值运算符就必须销毁状态。
唯一的难题是确定在哪里存放引用计数。计数器不能直接作为 A对象
的成员。举个例子:
A a1("cmy");
A a2(a1); // a2和a1指向相同的string
A a3(a2); // a1、a2、a3都指向相同的string
如果计数器保存在每个对象中,创建 a2
时可以递增 a1
的计数器并拷贝到 a2
中。可创建 a3
时,诚然可以更新 a1
的计数器,但怎么找到 a2
并将它的计数器更新呢?
那么怎么处理计数器呢?
动态内存实现计数器
class A
{int i = 0;string *ps;size_t *use; // 记录有多少个对象共享*ps的成员
public:A(const string &s = string()): ps(new string(s)), i(0), use(new size_t(1)) {}A(const A &a): ps(new string(*a.ps)), i(a.i), use(a.use) { ++*use; }A& operator=(const A&);~A() {}
};
A::~A(){if(--*use == 0){ // 引用计数变为0delete ps; // 释放string内存delete use; // 释放计数器内存}
}
A& A::operator=(const A& a)
{++*(a.use); // 之所以将计数器自增操作放这么前// 是为了防止自赋值时计数器自减导致ps、use直接被释放if(--(*use) == 0){delete ps;delete use;}ps = a.ps;i = a.i;use = a.use;return *this; // 返回此对象的引用
}
类的swap
概念
我们在设计类的 swap
时,虽然逻辑上是这样:
A tmp = a1;
a1 = a2;
a2 = tmp;
但如果真的这样实现的话,还需要创建一个新的对象 tmp
,效率是很低的,造成了内存空间的浪费。因此我们实际上希望的是这样的逻辑实现:
string *tmp = a1.ps;
a1.ps = a2.ps;
a2.ps = tmp;
创建一个 string类型
总比创建一个 A类对象
要省内存。具体实现:
class A
{friend void swap(A&, A&);
};
inline void swap(A& a1, A& a2){using std::swap;swap(a1.ps, a2.ps);swap(a1.i, a2.i);
}
swap实现自赋值
使用拷贝和交换的赋值运算符:
A& A::operator=(A a){ // 传值,使用拷贝构造函数通过实参(右侧运算对象)拷贝生成临时量aswap(*this, a); // a现在指向*this曾使用的内存return *this; // a的作用域结束,被销毁,delete了a中的ps
}
上面重载的赋值运算符参数并不是一个引用,也就是说 a
是右侧运算对象的一个副本。
在函数体中,swap
交换了 a
和 *this
中的数据成员。*this
的 ps
指向右侧运算对象中 string
的一个副本;*this
原来的 ps
存入 a
中。但函数体执行完,a
作为局部变量被销毁,delete
了 a
中的 ps
,即 释放掉了左侧运算对象(*this
)中原来的内存。
这个技术的有趣之处是它自动处理了自赋值情况且天然就是异常安全的。