8 标准库
STL标准模板库在不同产品使用程度不同,这里列出一些基本规则和建议,供各团队参考。
规则8.1 避免使用auto_ptr
说明:在stl库中的std::auto_ptr具有一个隐式的所有权转移行为,如下代码:
auto_ptr<T> p1(new T);
auto_ptr<T> p2 = p1;
当执行完第2行语句后,p1已经不再指向第1行中分配的对象,而是变为NULL。正因为如此,auto_ptr
不能被置于各种标准容器中。
转移所有权的行为通常不是期望的结果。对于必须转移所有权的场景,也不应该使用隐式转移的方式。
这往往需要程序员对使用auto_ptr的代码保持额外的谨慎,否则出现对空指针的访问。
使用auto_ptr常见的有两种场景,一是作为智能指针传递到产生auto_ptr的函数外部,二是使用
auto_ptr作为RAII管理类,在超出auto_ptr的生命周期时自动释放资源。
对于第1种场景,可以使用boost::shared_ptr或者是std::tr1::shared_ptr(在C++11标准中是
std::shared_ptr)来代替。
对于第2种场景,可以使用boost::scoped_ptr或者C++11标准中的std::unique_ptr来代替。其中
std::unique_ptr是std::auto_ptr的代替品,支持显式的所有权转移。
例外:
在C++11标准得到普遍使用之前,在一定需要对所有权进行转移的场景下,可以使用std::auto_ptr,
但是建议对std::auto_ptr进行封装,并禁用封装类的拷贝构造函数和赋值运算符,以使该封装类无法
用于标准容器。
规则8.2 仅将scoped_ptr、shared_ptr和unique_ptr用于管理单个对象
说明:boost::scoped_ptr、boost::shared_ptr、std::tr1::shared_ptr(在C++11标准中是
std::shared_ptr)和std::unique_ptr(C++11标准)都是用于管理单一对象的智能指针。当这些智能指
针在销毁所指向的对象时使用的都是delete而不是delete[],而使用delete删除数组是undefined行
为,因此不可使用上述智能指针管理数组。
当需要一个具有RAII特性的数组时,可以使用boost::scoped_array、boost::shared_array、
std::vector<std::tr1::shared_ptr>或std::vector<std::unique_ptr>(C++11标准)代替。
shared_ptr是基于引用计数的智能指针,可以安全用于大部分场景中,更新引用计数时需要略微消耗
一些性能,一般来说对性能不会有显著的影响。使用shared_ptr的另外一个需要注意的问题是不要产
生循环引用,基于引用计数的智能指针当出现循环引用时会造成内存泄漏。当需要循环引用时,其中
一个智能指针请使用weak_ptr。
boost::shared_ptr、std::tr1::shared_ptr(std::shared_ptr)的线程安全与stl中常见类型的线程一
致,即:
1. 多个线程可以同时读同一个shared_ptr对象;
2. 多个线程可以同时写不同的shared_ptr对象。
注:当多个不同的shared_ptr对象指向同一个底层对象时,同时写这些shared_ptr对象本身是线程安
全的,但是需要额外操作保证底层对象的线程安全。
规则8.3 如果涉及循环引用,使用weak_ptr解开循环
说明:当使用各种基于引用计数的shared_ptr时,会遇到循环引用的问题,例如:
#include <memory>
class TChild;
class TParent
{
public:
void SetChild(std::shared_ptr<TChild> const& Child)
{
Child_ = Child;
}
private:
std::shared_ptr<TChild> Child_;
};
class TChild
{
public:
void SetParent(std::shared_ptr<TParent> const& Parent)
{
Parent_ = Parent;
}
private:
std::shared_ptr<TParent> Parent_;
};
int main()
{
std::shared_ptr<TParent> Parent = std::make_shared<TParent>();
std::shared_ptr<TChild> Child = std::make_shared<TChild>();
Parent->SetChild(Child);
Child->SetParent(Parent);
//到这里Parent和Child产生了循环引用,当Parent、Child超出作用域后将产生内存泄漏。
}
为了解决循环引用导致的内存泄漏,需要引入weak_ptr。将代码修改成下面这样:
class TChild
{
public:
void SetParent(std::shared_ptr<TParent> const& Parent)
{
Parent_ = Parent;
}
void UseParent()
{
//使用weak_ptr指向的对象之前通过lock()获得shared_ptr。
std::shared_ptr<TParent> Parent = Parent_.lock();
}
private:
std::weak_ptr<TParent> Parent_;
};
注意红色部分以及新增的UseParent()函数,演示了如何使用weak_ptr。另外需要注意的是SetParent()
函数中形参依然是shared_ptr,这样将使用了weak_ptr的细节隐藏了起来,从外部看全部是
shared_ptr。举例使用的是C++11标准中的std::shared_ptr,但是对于boost::shared_ptr和
std::tr1::shared_ptr同样适用。
规则8.4 使用make_shared代替new生成shared_ptr
说明:在代码中,我们可以使用形如std::shared_ptr<T> A(new T)的方式初始化shared_ptr。但是在
涉及到shared_ptr的地方使用new涉及到3个潜在的风险。
一是容易出现下面的代码,访问悬空指针:
T* A = new T;
std::shared_ptr<T> B(A);
A->xxxxx; //当B超出作用域后:A指向的内存被释放,访问出错
二是容易出现下面的代码,引起重复delete:
T* A = new T;
std::shared_ptr<T> B(A);
//在许多代码之后再次出现:
std::shared_ptr<T> C(A);
当使用一个原生指针初始化一个shared_ptr时,引用计数会被置为1,于是出现了2组独立的引用计数 ,
当这2组引用计数到达0时都会引发销毁对象的操作,于是就会出现重复delete的问题。
三是可能出现内存泄漏的风险,考虑如下代码:
int func1();
void func2(std::shared_ptr<T> const& P1, int P2);
int main()
{
func2(std::shared_ptr<T>(new T), func1());
}
在以上调用func2的代码中,根据编译器不同,可能会以以下顺序执行代码:
1. new T
2. func1()
3. std::shared_ptr<T>构造
在这种情况下如果在func1()中抛出了异常,将会造成new T泄漏。所以建议使用new初始化的
shared_ptr要放入单独的语句中,即将调用func2的代码修改为:
std::shared_ptr<T> temp(new T);
func2(temp, func1());
使用以上方法相对略微烦琐,多引入了一个变量。跟shared_ptr配套存在的make_shared可以解决使用
new初始化的问题。上述两种情况下的代码可以修改为如下:
std::shared_ptr<T> B = std::make_shared<T>();
func2(std::make_shared<T>(), func1());
make_shared模板会构造一个指定类型的对象和一个shared_ptr并用该对象初始化shared_ptr,以上操
作是一步完成的,不存在中间抛出异常的风险。对于构造时需要参数的类型,将参数加在make_shared
模板后面的括号中即可。
延伸阅读材料: 《Effective C++中文版 第三版》 [美]Scott Meyers著 侯捷译 2006 电子工业出版
社 75页 条款17: 以独立语句将newed对象置入智能指针
规则8.5 对于同一个对象一旦使用shared_ptr,后续就要处处使用shared_ptr
说明:规则8.4描述了混用原生指针和shared_ptr容易导致问题:使用悬空指针和重复释放。所以,同
一对象的指针要统一用法,要么使用原生指针,要么使用shared_ptr,不要混用。
规则8.6 对于返回自身的shared_ptr指针的对象,要从enable_shared_from_this类派生
说明:对于需要使用shared_ptr管理的对象,当需要this指针时也需要使用对应的shared_ptr,但是
从3.4中可以看出,如果直接使用shared_ptr<T>(this)构造一个shared_ptr将会导致严重错误。为此,
boost和stl都提供了对应的enable_shared_from_this类,该类提供了一个shared_from_this()函数返
回this指针对应的shared_ptr。
示例:
class TClass : public std::enable_shared_from_this<TClass>
{
public:
std::shared_ptr<TClass> GetSelf()
{
return shared_from_this();
}
std::shared_ptr<TClass const> GetSelf() const
{
return shared_from_this();
}
};
规则8.7 不要将使用不同版本stl、boost等模板库编译的模块连接在一起
说明:模板库大量使用了内联函数,不同版本的模板库编译的模块对同一种数据类型的操作都已固化
在该模块中。如果不同版本的模板库中同一种数据类型的结构或者内存布局不同,在一个模块中定义
的对象被另外一个模块操作时可能会产生严重的错误。因为静态连接的模块常常不会划分出明确的接
口,常常会相互访问其它模块中定义的对象。
例外:
有些现存的模块已经无法获得源码,不可能重新使用同一版本的模板库重新编译。在这种情况下,如
果模块定义了清晰的接口,且接口中没有传递存在风险的数据类型,可以谨慎地混用,但是一定要进
行充足的验证。
规则8.8 不要保存string::c_str()指针
说明:在C++标准中并未规定string::c_str()指针持久有效,因此特定stl实现完全可以在调用
string::c_str()时返回一个临时存储区并很快释放。所以为了保证程序的移植性,一定不要保存
string::c_str()的结果,而是在每次需要时直接调用。
示例://不好的例子:
std::string DemoStr = "demo";
const char* buf = DemoStr.c_str();
//在这里buf指向的位置有可能已经失效。
strncpy (info_buf,buf, INFOBUF_SIZE - 1);
建议8.1 不要将stl、boost等模板库中的数据类型传递到动态链接库或者其它进程中
说明:跨动态链接库或者其它进程还存在另外一个更复杂的问题,一般来说,内存分配与释放要在同
一个模块中,如果将容器或者其它数据类型传递到其它模块中由其它模块进行了内存分配释放操作,
将会造成不可预知的问题,通常是程序崩溃或者数据消失等但是也有可能在某些情况下程序完全正常
运行,因此定位错误会比较困难。
建议8.2 使用容器时要评估大量插入删除是否会生成大量内存碎片
说明:不同的操作系统和运行时库分配内存的策略各不相同,由于容器内存多是动态分配而来,对于
反复大量插入删除的操作,有可能会造成大量的内存碎片或者内存无法收回。
对于长期运行的服务程序,建议在使用容器时要对容器是否会造成内存碎片进行评估和测试,如果存
在风险的,可以使用内存池(如boost::pool)来避免这个问题。
建议8.3 使用string代替char*
说明:使用string代替char*有很多优势,比如:
1. 不用考虑结尾的’\0’;
2. 可以直接使用+, =, ==等运算符以及其它字符串操作函数;
3. 不需要考虑内存分配操作,避免了显式的new/delete,以及由此导致的错误;
需要注意的是某些stl实现中string是基于写时复制策略的,这会带来2个问题,一是某些版本的写时
复制策略没有实现线程安全,在多线程环境下会引起程序崩溃;二是当与动态链接库相互传递基于写
时复制策略的string时,由于引用计数在动态链接库被卸载时无法减少可能导致悬挂指针。因此,慎
重选择一个可靠的stl实现对于保证程序稳定是很重要的。
例外:
当调用系统或者其它第三方库的API时,针对已经定义好的接口,只能使用char*。但是在调用接口之
前都可以使用string,在调用接口时使用string::c_str()获得字符指针。
当在栈上分配字符数组当作缓冲区使用时,可以直接定义字符数组,不要使用string,也没有必要使
用类似vector<char>等容器。
建议8.4 使用stl、boost等知名模板库提供的容器,而不要自己实现容器
说明:stl、boost等知名模板库已经提供较完善的功能,与其自行设计并维护一个不成熟且不稳定的
库,不如掌握和使用标准库,标准库的使用经验在业界已有成熟的经验和使用技巧。
建议8.5 使用新的标准库头文件
说明:使用stl的时候, 头文件采用<vector>、<cstring>等,而不是<vector.h>、<string.h>。