前面提到,C++中尽量不要使用指针。在代码量变得很高的情况下,程序猿很容易错误使用指针。为此,我们可以利用C++的类和智能指针来管理资源。这两者是相辅相成的。他们称为RAII技术。
一、如何设计资源管理类?
《Effective C++》这本书的类那一章,告诉我们很多设计类的方法。对于资源管理类,我们读到的准则是,将资源管理类设计得犹如int整型一样。
int整形数据有这些特征:
- 用的时候定义即可使用。比如
int a; a=10; int b(10); int c = 10;
同样我们应该设计资源管理类ResourceWrapper a; ResourceWrapper b = ...; a=b; ResourceWrapper c(b);
在ResourceWrapper类掌管的资源非常大的情况下,如何构造、复制、销毁值得商榷。比如复制的开销可能很大,我们应该将拷贝构造函数设置为private成员函数,以避免程序猿滥用拷贝;可以专门设置一个显式的Copy函数并在注释里给出警告。 - 退出作用域就销毁。
{int a = 10;} {//a is invalide}
这仅仅需要编写一个优秀的析构函数,必须注意释放所有资源。有时候资源管理类太大,销毁的代价很高,我们会希望能够重复使用。然而,我们不能因为有这个需求就改变“资源管理类退出作用域就销毁”这个原则。替代方法有重写内存池,使用智能指针包裹资源管理类。 - 如果支持与不同类型对象的二元运算,那么资源管理类应该像int那样随意使用。譬如
ResourceWrapper a; int b = 10; ResourceWrapper c = a+b; c= b+a;
《Effective C++》上面提到,如果ResourceWrapper的二元运算符函数用的是友元函数,那么b+a可能就用不了,因为此时+运算实际上调用的是int整型b的函数int.operator(int, int)函数,如果ResourceWrapper不支持隐式转换为int类型就会出错。《Effective C++》给出的做法是定义一个普通操作符函数和一个成员函数const ResourceWrapper operator+(const ResourceWrapper&, const ResourceWrapper&); ResourceWrapper::ResourceWrapper(int);
。这一点也是《Effective C++》另外一个话题的依据,即不一定要把所有的操作函数放到类中,类不能太臃肿了;对于已经写好很久,正常使用了很久的类,要扩展功能的话,建议是写相关的普通操作函数来扩展类的功能。这一做法很像C语言的做法。
二、什么时候使用智能指针?
不仅在某个作用域发挥作用,而是要在程序中很多地方发挥作用的资源,需要用智能指针来包装。如果只是临时使用,用资源管理类完全OK,直接用指针都可以。
智能指针一般用来包裹资源管理类。资源管理类可以有效遏制资源泄露、滥用资源的问题,其超出作用域即销毁。如果不想让资源管理类销毁,想把它用到其他地方,就用智能指针包裹。智能指针的好处是,如果没有地方引用智能指针,智能指针就连带资源管理类销毁。例如:
{
MyClass a;
{
std::shared_ptr<ResourceWrapper> ptr = std::make_shared<ResourceWrapper>(构造参数);//创建资源
a.use_shared_ptr(ptr);//引用ptr
}//ptr被a引用了,ptr被销毁,但是指向的资源不会销毁
std::shared_ptr<ResourceWrapper> ptr = a.get_shared_ptr();
}//最后的引用a被销毁。资源的引用数为0,被销毁。
三、智能指针可能有什么问题?
智能指针并不是指针,而是行为表现像指针的类。其内部有一个指向真正资源的成员变量和一个记录被引用次数的计数器。
指针是指向数据存放地址的数据。C++编译器通过灵活解释指针,来实现C++类的多态特性。譬如有父类SuperClass和子类DerivedClass,父类指针可以指向子类指针,在处理指向子类的父类指针的行为时,编译器认为这是一个SuperClass指针,只会搜索SuperClass的定义范围,从而行为上只能表现出SuperClass才有的行为;如果用dynamic_cast将父类指针转换为子类指针,在处理指向子类的父类指针的行为时,编译器认为这是一个DerivedClass指针,会搜索DerivedClass的定义范围,然而指针还是那个指针。
我们按照引用计数的方法自己制作的智能指针是不具有多态性的。因为它只是一个类。
在新版本C++中,智能指针使用复杂的模板技术实现了“多态性”,跟普通指针行为惊人地相似:
#include <iostream>
#include <memory>
struct SuperClass
{
virtual void print(){printf("super\n");}
};
struct DerivedClass:public SuperClass
{
virtual void print(){printf("derived\n");}
};
void foo1(SuperClass *p){p->print();}
void foo2(std::shared_ptr<SuperClass> p){p->print();}
int main(void)
{
SuperClass *a = new DerivedClass();
DerivedClass *b = new DerivedClass();
std::shared_ptr<SuperClass> c = std::make_shared<DerivedClass>();
std::shared_ptr<DerivedClass> d = std::make_shared<DerivedClass>();
foo1(a);
foo1(b);
foo2(c);
foo2(d);
return 0;
}
输出为四个derived。标准库的智能指针实现了多态性,我们应该使用标准库的智能指针。自己造轮子吃力不讨好。
还可以通过dynamic_pointer_cast来显式获取指向多态类型的智能指针,例如:
std::shared_ptr<DerivedClass> d = ...;//定义
std::shared_ptr<SuperClass> s = std::dynamic_pointer_cast<SuperClass>(d);
智能指针建议不要包裹除了资源管理类以外的对象。比如std::shared_ptr<int> a = std::shared_ptr<int>(new int[5]);
是错误用法。替代方法是使用std::shared_array
,但是这个是固定长度的数组,还不如用std::shared_ptr<std::vector<...> >
。vector本身可以看做对动态长度数组的包装。
std::shared_ptr在循环引用时会遇到问题,参见【C++】C++避坑经验谈:数组、vector。