一 温故而知新
????????在第六章,为了避免复制,我们设计了Handle类。对于某个特定的类(我们是以Point类为例),它工作的很好,既能体现指针语义,也能提供写时复制。回想类的设计过程,我们为Point类设计了一个中间类UsePoint,用来控制引用计数,并在类内放了一个Point 成员:
class UsePoint{
//...
Point p;
};
????????同时在Handle类内放了一个私有数据成员:
class Point_Handle{
//...
private:
UsePoint* up;
};
????????你应该发现了一些不方便的地方:每当我们想为一个特定类设计Handle,就要为其写一个新的中间类。
????????再回到第五章,我们用代理类来解决在容器中存储处于继承体系中的类。
????????当我们想为继承体系设计Handle时,记住我们想这样使用:
Handle arr[ 10 ] = { /*......*/ };
????????Handle现在没法工作了,因为我们的Handle只为特定类服务,没有动态绑定的功能,现在就只能在容器中存储某个特定类。
二 问题出在哪里
????????为了触发C++的动态绑定,我们需要使用基类的引用或指针,很显然目前的Handle类并没有:因为使用了中间类UsePoint,在Handle类内存放的是UsePoint*。为了实现我们的目标,我们应该在Handle类内存放基类的指针:
class Point_Handle{
//...
private:
Point* p;
};
????????这样,我们可以为Handle传递任意派生类的对象。
三 分离引用计数
? ? ? ? 接下来,由于丢弃了中间类,我们需要再次考虑引用计数。我们当然不能将引用计数放在Point中,因为不想修改Point,而且Point现在是一个继承体系。不过我们可以在将引用计数放在Handle类中,并以指针的形式。
? ? ? ? 还记得第六章我们的讨论吗?Handle类具有指针语义,也就是多个Handle可能指向同一个Point。那么引用计数能否自动同步?我们看看修改后Handle类的拷贝构造函数:
Point_Handle( const Point_Handle& ph ):up( ph.up ),uc(ph.uc){
++*uc;
}
? ? ? ? 复制的都是一份指针的拷贝,析构时,也可以根据引用计数的值,删除指针,完全满足我们的要求。
????????简而言之,我们需要丢弃中间类,将引用计数分离,并修改Handle类:
class Point_Handle{
public:
//...
private:
Point* p; //基类指针
int* uc; //引用计数
};
? ? ? ? 我们看看修改后Handle类的完整定义:
class Point_Handle{
public:
Point_Handle( ?):up( new Use_Point( ) ),uc(new int(1)){ ?}
Point_Handle( int x,int y ):up( new Use_Point( x,y ),uc(new int(1)) ){ ?}
~Point_Handle( ?)
{
--*uc;
if(*uc == 0)
{
delete up;
delete uc;
}
}
Point_Handle( const Point_Handle& ph ):up( ph.up ),uc(ph.uc){
++*uc;
}
Point_Handle( const Point& p ):up( new Use_Point(p) ),uc(new int(1)){ ?}
Point_Handle& operator=( const Point_Handle& ph ){
++*ph.uc;; //右侧对象引用计数+1
if( --*uc == 0 ) //左侧对象将指向新对象
{ //原引用计数-1
delete up; //如果为0,就析构原来指向的内存
delete uc;
}
up = ph.up;
uc = ph.uc; //现在指向新的副本了
return *this;
}
private:
Point* p;
int* uc;
};
四 封装引用计数
?????????到目前为止,我们可以用Handle保存处于继承体系的对象了。但是引用计数毫无封装性可言,更别谈之后想要扩充引用计数的功能。
? ? ? ? 我们打算封装引用计数为一个类,那么,它的功能至少应该是这样:
class UseCount{
public:
UseCount():p(new int(1)){}
UseCount( const UseCount& uc):p(uc.p){
++*p;
}
UseCount& operator=(const UseCount& uc){
//...
}
~UseCount(){
if(--*p == 0)
{
delete p;
}
}
private:
int* p;
};
????????现在重写Handle类。
? ? ? ? 由于引用计数可以依靠默认构造函数,因此Handle类的构造函数变的异常简单:
class Point_Handle{
public:
//默认构造函数
Point_Handle():p(new Point()){}
//构造函数
Point_Handle(int x,int y):p(new Point(x,y)){}
Point_Handle(const Point& p0):p(new Point(p0)){}
//拷贝构造函数
Point_Handle(const Point_Handle& ph):p(ph.p),uc(ph.uc){}
//析构函数
~Point_Handle(){
//?
}
//拷贝赋值运算符
Point_Handle& operator=(const Point_Handle& ph){
//?
}
private:
Point* p; //基类指针
UseCount uc; //引用计数
}
? ? ? ? 析构函数和拷贝赋值运算符稍微复杂点。
? ? ? ? 为了析构 Point* p ,我们需要知道UseCount是否是1。因此我们需要为UseCount添加一个成员函数,用来判断UseCount是否为1:
class UseCount{
public:
//...
bool only(){
return *p == 1;
}
};
? ? ? ? 为了执行Handle类的赋值操作,我们换一种思路来考虑:Handle类的赋值操作包括了两部分:Point* 和 UseCount。Point*依赖于UseCount。
????????赋值操作符左边的UseCount始终被改写。
????????如果左边的类引用计数目前为1,那么被改写后,引用计数(int* uc;)应该被析构,我们就把右边的引用计数赋值给左边;
????????如果左边的引用计数大于1,那么被改写后,仅仅自减,引用计数不需要析构,我们再把右边的引用计数赋值给左边。
????????如果左边的引用计数被析构,意味着左边的Handle类是唯一的控制类,此时我们也应该析构Point*;否则,就不需要析构左边的Point*,做简单的赋值操作就可以。
? ? ? ? 我们可以在UseCount内新增一个成员函数,用来判断当前的引用计数是否被析构:
? ? ? ? 与此同时,UseCount的拷贝赋值运算符应该私有化,因为我们不希望为外界提供该接口,以防止外界随意赋值,修改计数器的值:
class UseCount{
public:
//...
bool destruct(const UseCount& u){
++*u.p; //右边先自加
if(--*p == 0) //左边自减,如果此时为0,应该析构
{
delete p; //析构计数器
p = u.p; //把右边赋值给左边
return true; //告诉外界,析构了
}
p = u.p; //没有析构,直接赋值
return false; //未析构
}
private:
UseCount& operator=(UseCount& u){} //私有化,不允许赋值操作
};
? ? ? ? 有了这些,我们来重写Handle类的析构函数和赋值操作符:
//析构函数
~Point_Handle(){
if(uc.only()){ //如果计数器为1
delete p; //就析构当前对象
}
}
//拷贝赋值运算符
Point_Handle& operator=(const Point_Handle& ph){
if(uc.destruct(ph.uc)){ //当前的计数器对象是否将被析构;
delete p; //如果是的话,也应该析构Point* p;
}
p = ph.p; //不是的话,就直接赋值即可,因为左边的Handle类对象仍然存在
return *this;
}
五 存取和写入
? ? ? ? 对于写操作,我们仍然使用写时复制实现Handle类的值语义。先看看最开始的实现方法:
? ? ? ? 当我们执行了这样的操作,并进行写值的时候:
Point_Handle h(3,4);
Point_Handle h1;
//复制
h1 = h;
//写对象。为了体现值语义,我们在这里新建一个Point,并让h1控制这个新的Point
//所以h.x() == 3 h1.x() == 5
h1.x(5);
? ? ? ? 所以,我们是这样做的:
//...其他同...
Point_Handle& xw(int x0) {
if (up->use_count != 1) //如果引用计数不为1
{
--up->use_count; //递减引用计数
up = new Use_Point(up->p); //创建新Point
}
up->p.xw(x0); //写新Point
return *this;
}
Point_Handle& yw(int y0) {
if (up->use_count != 1)
{
--up->use_count;
up = new Use_Point(up->p);
}
up->p.yw(y0);
return *this;
}
? ? ? ? 我们需要判断当前的引用计数是否为1。如果是,那么就直接修改值;如果不是,就自减引用计数,并新建Point,让Handle类控制这个新Point。
? ? ? ? 应用到现在的UseCount类,我们需要一个成员函数,来判断是不是需要创建新的Point:
class UseCount{
public:
//...
bool createNewPoint()
{
if(*p == 1) //判断引用计数是否为1
{
return false; //是的话就不用创建
}
--*p; //不是先自减
p = new int(1); //创建新的引用计数,并置1
return true; //需要创建新的Point
}
};
? ? ? ? 那么写操作就应该是这样:
Point_Handle& xw(int x0) {
if (uc.createNewPoint()) //如果需要创建新Point
{
p = new Point(*p); //用当前Point的值创建新Point
}
p->xw(x0); //写新Point对象
return *this;
}
Point_Handle& yw(int y0) {
if (uc.createNewPoint())
{
p = new Point(*p);
}
p->yw(y0);
return *this;
}
? ? ? ? 对于取操作,就很简单了:
class Point_Handle()
{
public:
//...
int xr() const {
return p->x();
}
int yr() const {
return p->y();
}
};
六 使用Handle
? ? ? ? 有这样一个继承体系:
#ifndef TEST1_H
#define TEST1_H
#include <iostream>
//base class
class A {
public:
A() {}
A(int x0, int y0) :x(x0), y(y0) {}
virtual void print() {
std::cout << "x = " << x << "y = " << y << std::endl;
}
//虚析构函数
virtual ~A() {
}
private:
int x, y;
};
class B : public A {
public:
//默认构造
B() {}
B(int x0,int y0,std::string na):A(x0,y0),name(na){
}
//拷贝构造
B(const B& b):name(b.name) {
}
void print() {
std::cout << name << std::endl;
}
//析构函数
virtual ~B() {
}
private:
std::string name;
};
class C : public A {
public:
//默认构造
C() {
}
C(int x0, int y0, int age) :A(x0, y0), age(age) {
}
//拷贝构造
C(const C& c) :age(c.age) {
}
void print() {
std::cout << age << std::endl;
}
//析构函数
virtual ~C() {
}
private:
int age;
};
class D : public A {
public:
//默认构造
D() {
}
D(int x0, int y0, std::string ss) :A(x0, y0), s(ss) {
}
//拷贝构造
D(const D& d) :s(d.s) {
}
void print() {
std::cout << s << std::endl;
}
//析构函数
virtual ~D() {
}
private:
std::string s;
};
#endif //TEST1_H
? ? ? ? UseCount类:
#ifndef USECOUNT_H
#define USECOUNT_H
#include <iostream>
class UseCount {
public:
UseCount() :p(new int(1)) {}
UseCount(const UseCount& uc) :p(uc.p) {}
~UseCount() {
if (-- * p == 0) {
delete p;
}
}
bool only() {
return *p == 1;
}
bool destructor(const UseCount& uc) {
++* uc.p;
if (-- * p == 0) {
delete p;
p = uc.p;
return true;
}
p = uc.p;
return false;
}
bool createNew() {
if (*p == 1)
{
return false;
}
--* p;
p = new int(1);
return true;
}
private:
int* p;
UseCount& operator=(UseCount& uc) {}
};
#endif //USECOUNT_H
? ? ? ? 我们希望在容器中存储这个继承的所有派生类,为此提供这个类专属的Handle类:
#ifndef HANDLE_H
#define HANDLE_H
#include <iostream>
#include "UseCount.h"
#include "test1.h"
class Handle {
public:
Handle():ap(0) {}
Handle(A* a):ap(a) {
}
Handle(const Handle& h):ap(h.ap), u(h.u) {}
~Handle() {
if (u.only())
{
//delete ap;
}
}
Handle& operator=(const Handle& h) {
if (u.destructor(h.u)) {
delete ap;
}
ap = h.ap;
return *this;
}
void sprint() {
ap->print();
}
private:
A* ap;
UseCount u;
};
#endif //HANDLE_H
? ? ? ? ?注意,这几处有所修改:
~Handle() {
if (u.only())
{
//delete ap; //由于存在虚基类,我们为其传入对象指针,因此不需要delete,将由对象负责
}
}
Handle(A& a):ap(a.copy()) {
}
? ? ? ? ?现在,我们可以这样使用了:
#include <iostream>
#include "Handle.h"
void htest() {
A a(5,7);
B b(1,2,"yxl");
C c(3,4,20);
D d(5,6,"hw");
Handle arr[4]{&a,&b,&c,&d};
arr[0].sprint();
arr[1].sprint();
arr[2].sprint();
arr[3].sprint();
}
int main()
{
htest();
return 0;
}
? ? ? ? 输出:
x = 5y = 7
yxl
20
hw
F:\yxl\windows_program\Project2\Debug\Project2.exe (进程 19932)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .
?七 总结
? ? ? ? 改进的Handle类相比于第六章,完全可以应付处于继承体系下的对象了;并且,不需要再为特定的类创建引用计数类,也不需要考虑内存管理方面的问题。所有的修改操作现在都集中在Handle类来完成。
|