问题
假设我们有一些需要一起释放的对象,这些对象的类型并不完全一致.我们需要一种方式跟踪这些对象并且在合适的时候一起释放他们.
设计方案
按照C++的传统艺能:用类来表示概念,我们需要定义一个类,表示一些要一起释放的对象.假设叫做Cluster
class Cluster {};
我们知道这个类应该包含一些对象,并且在适当的时候释放掉他们.适当的时候应该怎么定义呢?一般而言,在析构Cluster
对象的时候应该一并释放掉包含在Cluster
对象中的其他对象.因为错过这个时机,我们将不知道什么时候释放这些对象了.
对于这些要释放的对象,我们有什么了解呢? 因为Cluster
对象包含这些对象,所以我们应该有一种方式将这些对象创建在Cluster
对象里面.也就是说:我们需要在簇c中分配一个类型为T的对象.
T* p = new(c)T;
这个方案需要类型T
支持void* operator new(size_t, Cluster&)
.但是并不是所有的类都支持这个运算符.考虑我们前面提到的代理类和句柄类的方案,我们可以通过继承和指针创建一个包含任意类对象的类.假设我们定义基类为ClusterItem
:
class ClusterItem {
public:
void* operator new(size_t, Cluster& c);
};
一般C++编译器要求我们如果重载了operator new
那么最少应该包含一个void* operator new(size_t);
的重载.因为我们不想在Cluster以外的地方分配ClusterItem
的对象,所以我们将它设置为delete
.同理:我们禁用掉拷贝构造和赋值运算符.
因为我们定义的是一个基类,所以应该包含一个虚析构函数.我们还需要提供默认构造函数,以便可以定义这个类型的数组对象.
现在我们得到了ClusterItem
的声明
class ClusterItem {
friend class Cluster; // 为了访问析构函数
public:
ClusterItem();
void* operator new(size_t, Cluster& c); // 使用placement_new的方式
ClusterItem(const ClusterItem&) = delete;
ClusterItem& operator=(const ClusterItem& ) = delete;
void* operator new(size_t) = delete; // 我们不实现这个,也不允许使用
protected:
virtual ~ClusterItem() {}
private:
// ...
};
因为我们希望只有Cluster
可以销毁ClusterItem
对象,所以我们不将析构函数设置为public
,同时设置友元类Cluster
.
现在我们来看看Cluster
类.对于这个类我们知道应该有public
的构造函数,析构函数,构造函数应该有默认构造函数.同时,Cluster
应该持有ClusterItem
的一系列对象,用于在适当的时候进行销毁.我们采用最简单的链表形式持有ClusterItem
对象.由此得到Cluster
的定义:
class ClusterItem {
friend class Cluster; // 为了访问析构函数
public:
ClusterItem();
void* operator new(size_t, Cluster& c); // 使用placement_new的方式
ClusterItem(const ClusterItem&) = delete;
ClusterItem& operator=(const ClusterItem& ) = delete;
void* operator new(size_t) = delete; // 我们不实现这个,也不允许使用
protected:
virtual ~ClusterItem() {}
private:
ClusterItem* next; // 新增
};
class Cluster {
public:
Cluster():head(nullptr) {}
~Cluster() {
while (head != nullptr) {
ClusterItem* next = head->next;
delete head;
head = next;
}
}
Cluster(const Cluster&) = delete;
Cluster& operator=(const Cluster&) = delete;
private:
ClusterItem* head;
};
现在我们来看看ClusterItem
的实现.由于我们使用了placement_new
的语义重载了operator new
,但是operator new
仅仅是申请void *
类型的内存,并不能直接操作对象,所以将ClusterItem
加入Cluster
持有的链表的动作应该交给ClusterItem
的构造函数才是正确的.但是只有operator new
的重载才有可能获取到一个有效的Cluster
对象,构造函数如何实现呢?
如果给ClusterItem
添加一个构造函数ClusterItem(Cluster& );
是不是就可以了?这样我们要考虑默认构造函数应该怎么实现.默认构造函数无法直接将自己添加到Cluster
持有的链表中,需要Cluster
提供一个添加的方法才行.
这里我们采用书中使用的比较巧妙的方式实现.
我们在实现的文件中添加一个static
类型的变量:static Cluster* cp;
因为使用了static
修饰,所以他的作用域不会超过这个文件.然后我们实现这个类:
void* ClusterItem::operator new(size_t n, Cluster& c) {
cp = &c;
return ::operator new(n); // 使用全局命名空间的operator new 申请空间
}
// 构造函数中将自身加入链表中.
ClusterItem::ClusterItem(){
next = cp->head;
cp->head = this;
}
加入继承
我们定义的ClusterItem
是一个需要被继承的基类,我们来看看怎么使用它.
class MyClass : public ClusterItem {};
int main() {
Cluster c;
MyClass* p = new(c) MyClass;
// ...
}
因为我们限制了ClusterItem
的析构函数是protected
访问权限,这导致我们不能定义MyClass my_class;
的局部变量.通过引入多继承可以解决这个问题.
class MyClass {};
class MyClusteredClass : public MyClass, public ClusterItem {};
int main() {
MyClass my_class; // 这里是合法的
Cluster c;
MyClass *mp = new(c) MyClusteredClass;
// ...
}
这里通过多继承将mp
添加到c
的管理中,当c
析构的时候,通过ClusterItem
的虚析构函数会正确的释放掉MyClusterdClass
进而正确释放MyClass
.