C++_03_动态内存与智能指针
动态内存
- 静态内存:用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。
- 栈内存:用来保存定义在函数内的非static对象。
- 自由空间(free store)或堆(heap):程序用堆存储动态分配的对象——程序运行时分配的对象。
分配在静态内存和栈内存中的对象由编译器自动创建和销毁。动态对象的生存期由程序来控制(代码显示的创建和销毁对象)。
使用动态内存的原因:
- 程序不知道自己需要使用多少对象;
- 程序不知道所需对象的准确类型;
- 程序需要在多个对象间共享数据;
动态内存的使用很容易死出现问题:
- 动态内存使用时,忘记释放内存,就会产生内存泄漏。
- 有指针引用内存的情况下,我们就释放了它,导致引用非法内存的指针。
智能指针
C++11新标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要区别是它负责自动释放所指向的对象。
- stared_ptr: 允许多个指针指向同一个对象。
- unique_ptr: 独占所指向的对象。
- weak_ptr: 弱引用,指向stared_ptr所管理的对象。
auto_ptr在C++11中被废弃,不再讨论。
stared_ptr指针
stared_ptr和unique_ptr都支持的操作
stared_ptr |
空智能指针,可以指向类型为T的对象 |
---|---|
p | 将p用作一个条件判断,若p指向一个对象,则为true |
*p | 解引用p,获得它指向的对象 |
p→mem | 等价于(*p).mem |
p.get() | 返回p中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针锁指向的对象也就消失了。 |
swap(p, q) | |
p.swap(q) | 交换p和q中的指针 |
shared_ptr中独有的操作
make_shared |
返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象 |
---|---|
shared_ptr |
p是shared_ptr q的拷贝;此操作会递增q中的计数器,q中的指针必须能够转换为T* |
p = q | p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p中的引用数量,递增q的引用计数;若p的引用计数为0,则将其管理的原内存释放。 |
p.unique() | 若p.use_count()为1,返回true,否则返回false |
p.use_count() | 返回与p共享对象的智能指针数量;可能很慢,主要用于调试。 |
一旦一个shared_ptr的计数器为0,它就会自动释放自己所管理的对象。
由于在最后一个shared_ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留就非常重要。
注意:如果将shared_ptr存放于一个容器中,而后不在需要全部的元素,而只是使用其中的一部分,要记得用erase删除不在需要的那些元素。
shared_ptr与new结合使用
不能将内置指针隐式转换为一个智能指针,必须使用直接初始化形式,如下:
shared_ptr<int> p1 = new int(1024); // 错误
shared_ptr<int> p2(new int(1024)); // 正确
p2 = new int(1024); // 错误
p2.reset(new int(1024)); // 正确
注意:
- 不要混用普通指针与智能指针!
- 不要使用get初始化另一个智能指针或为智能指针赋值!
容易出现同一块内存被delete两次,而出现core!
如果使用智能指针,即使程序块过早结束,智能指针也能确保在内存不再需要时将其释放。而如果是普通指针,在new和delete之间发生了异常,且异常未捕获,则内存就永远不会被释放了。
自定义删除器
例子:
void end_connection(connection* p) { disconnect(*p); }
void f(distination& d) {
connection c = connection(&d);
shared_ptr<connection> p(&c, end_connection);
}
智能指针陷阱
智能指针使用规范:
- 不使用相同的内置指针值初始化(或reset)多个智能指针。
- 不delete get()返回的指针。
- 不使用get()初始化或reset另一个智能指针。
- 如果使用get()返回指针,记住当最后一个对应的智能指针销毁后,普通指针就变为无效了。
- 如果使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。
unique_ptr指针
与shared_ptr不同的是,unique_ptr某个时刻只能有一个unique_ptr指向一个给定对象。
unique_ptr操作
unique_ptr |
空unique_ptr,可以指向类型为T的对象,u1会使用delete来释放它的指针;u2会使用一个类型为D的可调用对象来释放它的指针。 |
---|---|
unique_ptr<T, D> u(d) | 空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete |
u = nullptr | 释放u指向的对象,将u置为空 |
u.release() | u放弃对指针的控制权,返回指针,并将u置为空 |
u.reset() | 释放u指向的对象 |
u.reset(q) | |
u.reset(nullptr) | 如果提供了内置指针q,令u指向这个对象;否则将u值为空 |
unique_ptr不支持普通的拷贝或赋值操作:
unique_ptr<string> p1(new string("hello"));
unique_ptr<string> p2(p1); // 错误,unique_ptr不支持拷贝
unique_ptr<string> p3;
p3 = p2; // 错误,unique_ptr不支持赋值
虽然不能通过拷贝或赋值unique_ptr,单可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique:
// 将所有权从p1转移给p2
unique_ptr<string> p2(p1.release());
unique_ptr<string> p3(new string("hello"));
// 将所有权从p3转移给p2
p2.reset(p3.release()); // reset释放了p2原来指向的内存
weak_ptr
weak_ptr是一种不可控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。
将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。
weak_ptr的引入是为了解决shared_ptr存在的一个问题——循环引用 。
weak_ptr操作
weak_ptr |
空weak_ptr可以执行类型为T的对象 |
---|---|
weak_ptr |
与shared_ptr sp指向相同对象的weak_ptr,T必须能够转换为sp指向的类型 |
w = p | p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象 |
w.reset() | 将w值为空 |
w.use_count() | 与w共享对象的shared_ptr的数量 |
w.expired() | 若w.use_count()为0,返回true,否则返回false |
w.lock() | 如果expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr |
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。此函数检查weak_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的shared_ptr。
经验之谈
不要混用
指针之间的混用,有时候会造成不可预知的错误,所以建议尽量不要混用。包括裸指针和智能指针以及智能指针之间的混用****
裸指针和智能指针混用
void fun() {
auto ptr = new Type;
std::shared_ptr<Type> t(ptr);
delete ptr;
}
在上述代码中,将ptr所有权归shared_ptr所拥有,所以在出fun()函数作用域的时候,会自动释放ptr指针,而在函数末尾又主动调用delete来释放,这就会造成double delete,会造成segment fault
。
智能指针混用
void fun() {
std::unique_ptr<Type> t(new Type);
std::shared_ptr<Type> t1(t.get());
}
在上述代码中,将t关联的对象又给了t1,也就是说同一个对象被两个智能指针所拥有,所以在出fun()函数作用域的时候,二者都会释放其关联的对象,这就会造成double delete,会造成segment fault
。
需要注意的是,下面代码在STL中是支持的:
void fun() {
std::unique_ptr<Type> t(new Type);
std::shared_ptr<Type> t1(std::move(t));
}
不要管理同一个裸指针
void fun() {
auto ptr = new Type;
std::unique_ptr<Type> t(ptr);
std::shared_ptr<Type> t1(ptr);
}
在上述代码中,ptr所有权同时给了t和t1,也就是说同一个对象被两个智能指针所拥有,所以在出fun()函数作用域的时候,二者都会释放其关联的对象,这就会造成double delete,会造成segment fault
。
避免使用get()获取原生指针
void fun(){
auto ptr = std::make_shared<Type>();
auto a= ptr.get();
std::shared_ptr<Type> t(a);
delete a;
}
一般情况下,生成的指针都要显式调用delete来进行释放,而上述这种,很容易稍不注意就调用delete;非必要不要使用get()获取原生指针
。****
不要管理this指针
class Type {
private:
void fun() {
std::shared_ptr<Type> t(this);
}
};
在上述代码中,如果Type在栈上,则会导致segment fault
,堆上视实际情况(如果在对象在堆上生成,那么使用合理的话,是允许的)。****
只管理堆上的对象
void fun() {
Type t;
std::shared_ptr<Type> ptr(&t);
};
在上述代码中,t在栈上进行分配,在出作用域的时候,会自动释放。而ptr在出作用域的时候,也会调用delete释放t,而t本身在栈上,delete一个栈上的地址,会造成segment fault
。
优先使用unique_ptr
根据业务场景,如果需要资源独占,那么建议使用unique_ptr而不是shared_ptr,原因如下:
- 性能优于shared_ptr
- 因为shared_ptr在拷贝或者释放时候,都需要操作引用计数
- 内存占用上小于shared_ptr
- shared_ptr需要维护它指向的对象的线程安全引用计数和一个控制块,这使得它比unique_ptr更重量级
使用make_shared初始化
我们看下常用的初始化shared_ptr两种方式,代码如下:
std::shared_ptr<Type> p1(new Type);
std::shared_ptr<Type> p2 = std::make_shared<Type>();
那么,上述两种方法孰优孰劣呢?我们且从源码的角度进行分析。
第一种初始化方法,有两次内存分配:
- new Type分配对象
- 为p1分配控制块(control block),控制块用于存放引用计数等信息
第一种初始化方式(new方式)共有两次内存分配操作,而第二种初始化方式(make_shared)只有一次内存申请,所以建议使用make_shared
方式进行初始化。
Reference:
- 《C++ Primer》
- 智能指针-使用、避坑和实现