C++ 标准库STL是 C++ 编程语言的重要组成部分,为开发者提供了丰富的功能和工具,极大地提高了开发效率和代码的可移植性。
其主要包括:标准容器库,输入 / 输出流库,算法库,迭代器库,字符串库,数值计算库,异常处理库。
C++标准库STL在使用中,也有一些基本的使用规则和建议,列举如下:
1. 避免使用auto_ptr
在stl库中的std::auto_ptr具有一个隐式的所有权转移行为,转移所有权的行为通常不是我们期望的结果。对于必须转移所有权的场景,也不应该使用隐式转移的方式。所以,使用auto_ptr的代码需要额外谨慎,否则出现对空指针的访问。如下例子:
auto_ptr<T> p1(new T);
auto_ptr<T> p2 = p1;
//当执行完第2行语句后,p1已经不再指向第1行中分配的对象,而是变为NULL。
//正因为如此,auto_ptr不能被置于各种标准容器中。
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::vectorstd::tr1::shared_ptr或std::vectorstd::unique_ptr(C++11标准)代替。
shared_ptr是基于引用计数的智能指针,可以安全用于大部分场景中,更新引用计数时需要略微消耗一些性能,一般来说对性能不会有显著的影响。使用shared_ptr的另外一个需要注意的问题是不要产生循环引用,基于引用计数的智能指针当出现循环引用时会造成内存泄漏。当需要循环引用时,请使用weak_ptr智能指针。
3. 如果涉及循环引用,使用weak_ptr解开循环
当使用各种基于引用计数的shared_ptr时,会遇到循环引用的问题。为了解决循环引用导致的内存泄漏,需要引入weak_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;
}
void UseParent()
{
//使用weak_ptr指向的对象之前通过lock()获得shared_ptr。
std::shared_ptr<TParent> Parent = Parent_.lock();
}
private:
std::weak_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);
//到这里如果TChild中没有引入weak_ptr在处理,那么Parent和Child产生了循环引用,当Parent、Child超出作用域后将产生内存泄漏。
}
4. 使用make_shared代替new生成shared_ptr
在代码中,我们可以使用形如std::shared_ptr A(new T)的方式初始化shared_ptr。但是在涉及到shared_ptr的地方使用new涉及到3个潜在的风险。
- 一是容易出现访问悬空指针;
T* A = new T;
std::shared_ptr<T> B(A);
//当B超出作用域后:A指向的内存被释放,访问出错
A->xxxxx;
- 二是容易引起重复delete;
T* A = new T;
std::shared_ptr<T> B(A);
//在许多代码之后再次出现:
std::shared_ptr<T> C(A);
当使用一个原生指针初始化一个shared_ptr时,引用计数会被置为1,于是出现了两组独立的引用计数 ,当这两组引用计数到达0时都会引发销毁对象的操作,于是就会出现重复delete的问题;
- 三是可能出现内存泄漏的风险;
int func1();
void func2(std::shared_ptr<T> const& P1, int P2);
int main()
{
func2(std::shared_ptr<T>(new T), func1());
}
如果在func1()中抛出了异常,将可能会造成new T泄漏;
5. 对于同一个对象一旦使用shared_ptr,后续就要处处使用shared_ptr
像前一个描述的,混用原生指针和shared_ptr容易导致悬空指针和重复释放的问题,所以同一对象的指针要统一用法,要么使用原生指针,要么使用shared_ptr,不要混用。
6. 对于返回自身的shared_ptr指针的对象,要从enable_shared_from_this类派生
对于需要使用shared_ptr管理的对象,当需要this指针时也需要使用对应的shared_ptr,但是如果直接使用shared_ptr(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();
}
};
7. 不要使用不同版本stl、boost等模板库编译的模块
模板库大量使用了内联函数,不同版本的模板库编译的模块对同一种数据类型的操作都已固化在该模块中。如果不同版本的模板库中同一种数据类型的结构或者内存布局不同,在一个模块中定义的对象被另外一个模块操作时可能会产生严重的错误。因为静态连接的模块常常不会划分出明确的接口,常常会相互访问其它模块中定义的对象。
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);
9. 尽量使用stl、boost等知名模板库提供的容器,而不要自己实现容器
stl、boost等知名模板库已经提供较完善的功能,与其自行设计并维护一个不成熟且不稳定的库,不如掌握和使用标准库,标准库的使用经验在业界已有成熟的经验和使用技巧。
10. 尽量使用string代替char*
使用string代替char*有很多优势:
-
不用考虑结尾的’\0’;
-
可以直接使用+, =, ==等运算符以及其它字符串操作函数;
-
不需要考虑内存分配操作,避免了显式的new/delete,以及由此导致的错误;
当然也有一些例外:
- 当调用系统或者其它第三方库的API时,人家已经定义好的接口,让你只能使用char*。那么在调用接口之前都可以使用string,在调用接口时使用string::c_str()获得字符指针。
- 当在栈上分配字符数组当作缓冲区使用时,可以直接定义字符数组,不要使用string,也没有必要使用类似vector等容器。