【一天一个C++小知识】005. C++中的句柄类(智能指针)

  句柄类作用主要有两个,一是支持面向对象编程,实现多态性质;二是减少头文件的编译依赖关系,让文件间的编译更加独立。

  句柄类存储和管理基类指针,指针既可以指向基类类型对象又可以指向派生类型对象。用户通过句柄类访问继承层次的操作,用户可以获得动态行为,同时能够确保自动正确的销毁动态分配的对象,防止内存泄露。C++不能通过对象支持多态,而必须使用指针或引用。若保存基类的对象:派生类对象只有基类部分保存下来,而派生类部分被切掉;如果保存派生类的对象,基类对象无法有效转换为派生类对象。

  在Effective C++类的实现中条款22说明了为了实现接口和实现的分离,将对象的实现隐藏在指针身后,这样就能减少头文件的编译依赖关系。实现技术:使用引用数(referencecount)。句柄类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。计数器变化的情况:

  • 创建类的新对象时,初始化指针并将引用计数置为1;
  • 对象作为另一对象的副本,拷贝构造函数拷贝指针并增加与之相应的引用计数;
  • 对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;
  • 调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除对象)。

  句柄类(智能指针smart point)是存储指向动态分配(堆)对象指针的类。除了能够在适当的时间自动删除指向的对象外,他们的工作机制很像C++的内置指针。智能指针在面对异常的时候格外有用,因为他们能够确保正确的销毁动态分配的对象。他们也可以用于跟踪被多用户共享的动态分配对象。
  在C++中一个通用的技术是定义包装(cover)类或句柄(handle)类,也称智能指针。句柄类存储和管理基类指针。指针所指向对象的类型可以变化,它既可以指向基类类型对象又可以指向派生类型对象。用户通过句柄类访问继承层次的操作(指针的对应的类型的操作)。因为句柄类使用指针执行操作,虚成员的行为将在运行时根据句柄实际绑定的对象类型而变化,即实现c++运行时动态绑定。故句柄用户可以获得动态行为但无需操心指针的管理。

  使用的相关技术:

引入使用计数

  定义句柄类或智能指针的通用技术是采用一个使用计数(use count)。句柄类将一个计数器与类指向的对象相关联。使用计数跟踪该类有多少个指针共享同一对象。当使用计数为0时,就删除该类对象,否则再删除类对象时,只要引用计数不为0,就不删除实际的类对象,而是是引用计数减1,实现虚删除。
使用计数类

  为了便于理解,我们定义一个实际类(Point),一个引用计数器类(UPoint),一个句柄类(Handle)。

  实现使用计数有两种经典策略:一种是定义一个单独的具体的类用以封装使用计数和指向实际类的指针;另一种是定义一个单独的具体的类用以封装引用计数和类的对象成员。我们称这种类为计数器类(UPoint)。在计数器类中,所有成员均设置为private,避免外部访问,但是将句柄类Handle类声明为自己的友元,从而使句柄类能操纵引用计数器。

写时复制

  写时复制(copy on write)技术是解决如何保证要改动的那个引用计数器类UPoint对象不能同时被任何其他的句柄类(Handle类)所引用。通俗的来说,就是当实际对象Point被多个Handle类的指针共享时,如果需要通过指针改变实际对象Point,而其他的指针又需要保持原来的值时,这就有矛盾了。打个不恰当的比方来说,两个以上的人共有5W块钱,如果其中一个人想用这5W块钱去消费,那就必须通知其他人。否则在这个人消费了5块钱后,其他人还以为他们仍然有5W块钱,如果这儿时候,他们去买5W的东西,就会发现钱变少了或是没有了,此时他们就陷入债务的泥团。在C++中通过指针访问已经删除或是不存在的对象,将是非常危险的。有可能系统提示该行为未定义,也有可以内存非法访问,还有可能使系统崩溃。

  具体地看代码:

class A
{
public:
	A() {}
	~A() {}
	virtual void func() { std::cout << "A"; }
};
class B : public A
{
public:
	B() {}
	~B() {}
	void func() { std::cout << "B"; }
}

  假设现在有容器vector vec;,可以这样使用:

A a;
B b;
vec.push_back(a);
vec.push_back(b);

  但这样实际上把b转化为了它的基类A,假如:

vector<A>::iterator iter;
for (iter = vec.begin(); iter != vec.end(); iter++)
{
	(*iter).func();
}

  实际都会输出A,解决这个问题就是用指针代替对象(指针进行多态):vector vec;,这样可以达到多态的目的,但程序员必须接管内存的管理,例如:

B* b = new B;
vec.push_back(b);

  这时如果vec的生命周期结束了,它并不会主动释放b所占用的内存,不手动delete b;的话,就会产生内存泄漏。

  这个时候就有了句柄类的用武之地:

class A
{
public:
	A() {}
	~A() {}
	virtual void func() const
	{
		std::cout << "A";
	}
	virtual A* clone() const { return new A(*this); }
};

class B : public A
{
public:
	B() {}
	~B() {}
	void func() const
	{
		std::cout << "B";
	}
	virtual B* clone() const { return new B(*this); }
};
class sample
{
public:
	sample() :p(0), use(1) {}
	sample(const A& a) :p(a.clone()), use(1) {}
	sample(const sample& i) :p(i.p), use(i.use) { use++; }
	~sample() { decr_use(); }

	sample& operator= (const sample& i)
	{
		use++;
		decr_use();
		p = i.p;
		use = i.use;
		return *this;
	}
	const A* operator->() const { if (p) return p; }
	const A& operator*() const { if (p) return *p; }
private:
	A* p;
	std::size_t use;
	void decr_use() { if (--use == 0) delete p; }
};

int _tmain(int argc, _TCHAR* argv[])
{
	vector<sample> vec;
	A a;
	B b;
	sample sample1(a);
	sample sample2(b);
	vec.push_back(sample1);
	vec.push_back(sample2);
	vector<sample>::iterator iter;
	for (iter = vec.begin(); iter != vec.end(); iter++)
	{
		(*iter)->func();

	}
	return 0;
}

  这样不但得到了正确的输出而且vec声明周期结束时,会自动调用析构函数释放内存。

  这里看到class A中使用了virtual A* clone() const { return new A(*this);}而不是virtual A* clone() const { return this;}是因为,后者存在问题,比如:

B b;
sample sam(b);
vec.push_back(sam);

  当b的生命周期结束后,vec生命周期尚未结束,调用(*iter)->func();会出错,这和直接用指针进行多台没有太大区别了。

参考:
1.C++句柄类(智能指针)小结
2.浅谈C++中句柄的使用


欢迎扫描二维码关注微信公众号 深度学习与数学   [每天获取免费的大数据、AI等相关的学习资源、经典和最新的深度学习相关的论文研读,算法和其他互联网技能的学习,概率论、线性代数等高等数学知识的回顾]
在这里插入图片描述

你可能感兴趣的:(C++)