对象的生命周期:
(1) shared_ptr类:
shared_ptr和unique_ptr 都支持的操作:
- 全局对象:在程序启动时分配,在程序结束时销毁。
- 局部对象:在进入程序块时创建,在离开程序块时销毁。
- 局部static对象:在第一次使用前分配,在程序结束时销毁。
- 动态分配对象:只能显式地被释放。
对象的内存位置:
- 静态内存:用来保存局部static对象、类static对象、定义在任何函数之外的变量。
- 栈内存:用来保存定义在函数内的非static对象。
- 堆内存:又称自由空间,用来存储动态分配的对象。
12.1 动态内存与智能指针
动态内存管理:- new:在动态内存中为对象分配空间 并返回一个指向该对象的指针。
- delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
- 管理动态对象。
- 行为类似常规指针。
- 负责自动释放 所指向的对象。
- 智能指针也是模板。
shared_ptr和unique_ptr 都支持的操作:
shared_ptr<T> sp | 空智能指针,可以指向类型为T的对象。 |
unique_ptr<T> up | |
p | 将p用作一个条件判断,若p指向一个对象,则为true。 |
*p | 解引用p,获得它指向的对象。 |
p->mem | 等价于(*p).mem。 |
p.get() | 返回p中保存的指针,要小心使用,若智能指针释放了对象,返回的指针 所指向的对象也就消失了。 |
swap(p, q) | 交换p和q中的指针。 |
p.swap(q) |
shared_ptr独有的操作:
make_shared<T>(args) | 返回一个shared_ptr,指向一个动态分配的 类型为T的对象。使用args初始化此对象。 |
shared_ptr<T>p(q) | 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共享对象的智能指针数量;可能很慢,主要用于调试。 |
int main() { shared_ptr<string> p1; // p1为一个shared_ptr<string>类型的 智能指针对象 (可以指向一个string对象) shared_ptr<list<string>> p2; // p1为一个shared_ptr<list<string>>类型的 智能指针对象 (可以指向一个list<string>对象) // 定义和初始化 智能指针shared_ptr: p1 = std::make_shared<string>("aaa"); // make_shared在动态内存中 创建一个string对象 并初始化为"aaa",返回指向该对象的shared_ptr p2 = std::make_shared<list<string>>(3, "aaa"); // make_shared在动态内存中 创建一个list<string>对象 并初始化为{"aaa","aaa","aaa"},返回指向该对象的shared_ptr shared_ptr<char> p3 = std::make_shared<char>('a'); // p3是一个指向 char类型值为'a' 的shared_ptr auto p4 = std::make_shared<string>(3, 'a'); // p4是一个指向 string类型值为"aaa" 的shared_ptr // 输出 智能指针shared_ptr指向的对象: cout << *p1 << endl; // 输出: aaa std::copy(p2->begin(), p2->end(), std::ostream_iterator<string>(cout)); // 输出: aaaaaaaaa cout<<endl; cout << *p3 << endl; // 输出: 1 cout << *p4 << endl; // 输出: aaa // shared_ptr的拷贝和复制: auto p5 = std::make_shared<string>("aaa"); auto p6 = p5; // 递增p5指向的对象 的引用记数 auto p7 = p6; // 递增p6指向的对象 的引用记数 cout << p5.use_count() << endl; // 输出: 3 (p5,p6,p7共享一个对象) p7 = nullptr; // 递减p7原来指向的对象 的引用记数 cout << p5.use_count() << endl; // 输出: 2 (p7指向nullptr后,就只有p5,p6共享这个对象) }注:
- 我们可以认为每个shared_ptr都有一个 关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。当我们给shared_ptr赋予一个新值 或是shared_ ptr被销毁时,计数器就会递减。
- shared_ptr的析构函数 会递减它所指向的对象 的引用计数。如果引用计数变为0,shared_ ptr的析构函数就会销毁对象,并释放它占用的内存。
- 如果忘记销毁 程序不再需要的shared_ptr,程序仍会正确执行,但会浪费内存。
使用动态内存的三种原因:
- 程序不知道 自己需要使用多少对象。(比如容器类)
- 程序不知道 所需要对象的准确类型。
- 程序需要 在多个对象间共享数据。
(2) 直接管理内存:
- 使用new 动态分配和初始化对象。
- new无法为分配的对象命名 (因为自由空间分配的内存是无名的),因此返回一个 指向该对象的指针。
- 一旦一个程序用光了它所有可用的内存,new表达式就会失败。默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。(可以通过改变 使用new的方式 来阻止它抛出异常)
- 使用delete 将动态内存归还给系统。(为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统)
int main() { // 使用new动态分配时的 4种初始化对象方式: int* p1 = new int; // 默认初始化 int* p2 = new int(123); // 直接初始化 string* p3 = new string(3, 'a'); // 构造初始化 vector<string>* p4 = new vector<string>{ "aaa","bbb","ccc" }; // 列表初始化 // 对动态分配的对象 进行值初始化 通常是个好主意: (通过在类型名后面 跟一对空括号) string* p5 = new string; // 默认初始化: *p5="" string* p6 = new string(); // 值初始化: *p6="" int* p7 = new int; // 默认初始化: *p7的值未定义! int* p8 = new int(); // 值初始化: *p8=0 // 如果我们提供一个 括号包围的初始化器,就可以使用auto从初始化器来推断 分配的对象的类型。但是,由于编译器要用初始化器的类型 来推断要分配的类型,只有当括号中 仅有单一初始化器时 才可以使用auto auto p9 = new auto (123); // 正确: p9为int*类型,*p9=123 auto p10 = new auto(string("aaa")); // 正确: p10为string*类型,*p10="aaa" auto p11 = new auto{'a', 'a', 'a'}; // 错误: 括号中只能有单个初始化器 // 动态分配的const对象 auto p12 = new const int(123); // 正确: p12为const int*类型 (底层const) const int* p13 = new const int(123); // 正确: p13为const int*类型 (底层const) auto p14 = new const int; // 错误: const对象必须初始化 auto p15 = new const int(); // 正确: 类型名后面加了括号后,会对对象进行值初始化 auto p16 = new const string; // 正确: string类型定义了自己的 默认构造函数,所以其const对象可以进行 隐式初始化 // 动态分配内存时 内存耗尽: int* p17 = new int(); // 如果分配失败,new抛出std::bad_alloc int* p18 = new(std::nothrow)int(); // 如果分配失败,new返回一个空指针 (定位new表达式 允许我们向new传递额外的参数) // 释放动态内存: int a = 123, * p_1 = &a, * p_2 = nullptr; int* p19 = new int(123), * p20 = p19; delete p_2; // 正确: 释放一个空指针 总是没有错误的 delete p19; // 正确: 释放一个动态分配的int对象 占用的动态内存 delete p20; // 错误: p20指向的动态内存 已经被释放掉的 delete a; // 错误: a不是一个指针 delete p_1; // 错误: p_1不是一个指向动态内存的指针 (p_1指向一个局部变量,而不是一个动态分配的对象) // 动态对象的生存期 直到被释放时为止: { int* p21 = new int(123); // 在该代码块中,如果p21指向的动态对象 没有被显式释放掉,那么之后就没办法释放这块内存 (程序运行完该代码块后,局部变量p21就会被销毁) } }注:
- 被delete的指针 必须指向一个动态分配的对象 或是一个空指针。
- delete后的指针 称为空悬指针。(最好在delete之后 将nullptr赋予指针)
- 由内置指针(而不是智能指针)管理的动态内存 在被显式释放之前 会一直存在。
使用new和delete管理动态内存 存在三个常见问题:
- 忘记delete内存。
- 使用已经释放掉的对象。
- 同一块内存释放两次。
(3) shared_ptr和new结合使用:
定义和改变shared_ptr的其他方法:
定义和改变shared_ptr的其他方法:
shared_ptr<T> p(q) | p管理 内置指针q所指向的对象。q必须指向new分配的内存,且能够转换为T*类型。 |
shared_ptr<T> p(u) | p从unique_ptr u那里 接管对象的所有权,并将u置为空。 |
shared_ptr<T> p(q, d) | p接管 内置指针q所指向的对象的所有权。q必须能转换为T*类型。p使用 可调用对象d 来代替delete。 |
shared_ptr<T> p(p2, d) | p是shared_ptr p2的拷贝,唯一的区别是 p使用可调用对象d来代替delete。 |
p.reset() | 若p是唯一指向其对象的shared_ptr,reset会释放此对象。 若传递了可选参数 内置指针q,则会令p指向q,否则会将p置空。 若还传递了参数d,则会调用d而不是delete 来释放q。 |
p.reset(q) | |
p.reset(q, d) |
注:
(4) 智能指针和异常:
nuique_ptr操作:
注:
(6) weak_ptr:
- 接受指针参数的智能指针构造函数 是explicit的。因此,我们不能将一个内置指针 隐式转换为一个智能指针,必须使用直接初始化形式 来初始化一个智能指针。
- 默认情况下,一个用来初始化智能指针的普通指针 必须指向动态内存,因为智能指针默认使用delete 释放它所关联的对象。我们可以将智能指针绑定到一个 指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代delete。
- 不要混合使用 普通指针和智能指针。使用一个内置指针来访问 一个智能指针所负责的对象 是很危险的,因为我们无法知道 对象何时会被销毁。
- get用来将指针的访问权限 传递给代码,只有在确定代码 不会delete指针的情况下,才能使用get。
- 不要使用get初始化另一个智能指针 或为智能指针赋值。
- reset经常与unique一起使用,来控制多个 shared_ptr共享的对象。
void process(shared_ptr<int> ptr) { // 调用该函数时 ptr被创建并初始化(实参被拷贝到ptr中),因此ptr指向对象的 引用计数+1 ++(*ptr); } // ptr离开作用域 而被销毁,因此ptr指向对象的 引用计数-1 int main() { // 接受指针参数的智能指针构造函数 是explicit的: shared_ptr<int> p1 = new int(123); // 错误: 不支持隐式转换 shared_ptr<int> p2(new int(123)); // 正确: 使用了直接初始化形式 // 默认情况下,一个用来初始化智能指针的普通指针 必须指向动态内存: int a = 123, * p_a = &a; shared_ptr<int> p3(p_a); // 错误: p_a不是一个 指向动态内存的指针 shared_ptr<int> p4(new int(123)); // 正确 // 不要混合使用 普通指针和智能指针: shared_ptr<int> p5(new int(123)); // p5指向的对象 引用计数为1 (*p5=123) process(p5); // p5指向对象值+1 (形参拷贝实参p5时 引用计数+1,因此在process中p5指向的对象 引用计数为2。process结束,形参被销毁后 引用计数为-1,因此p5指向的对象 引用计数为1) int i = *p5; // p5指向的对象 引用计数为1 (i=124) int* x = new int(123); // 危险: x是一个普通指针 process(x); // 错误: 不能将int* 隐式转换为一个shared_ptr<int> process(shared_ptr<int>(x)); // 合法,但是x指向的内存 会被释放! (形参拷贝实参x时 引用计数+1,因此在process中x指向的对象 引用计数为1。process结束,形参被销毁后 引用计数为-1,因此x指向的对象 引用计数为0。所以x指向的内存 被自动释放) int j = *x; // 未定义: x此时是一个空悬指针 (x指向的内存已经被释放,不能再使用x的值) // 不要使用get初始化另一个智能指针 或为智能指针赋值: shared_ptr<int> p6(new int(123)); // p指向的对象 引用计数为1 int* q = p6.get(); // 正确: 但使用q时要注意,不要让它管理的指针被释放 (危险: 使用get返回的指针的代码 不能delete此指针) { shared_ptr<int> p7(q); // 未定义: 两个独立的shared_ptr指向 相同的内存! (p6和p7指向相同的内存,因为它们是相互独立创建的,因此它们的引用计数都是1) } // 程序块结束,p7被销毁后 其指向对象引用计数-1(即0),因此p7指向的内存被释放 int k = *p6; // 未定义: p7指向的内存已经被释放,也就是p6指向的内存被释放 // 使用reset()函数,可以取消 shared_ptr对象与相关指针的关联: shared_ptr<int> p8(new int(123)); // p8指向的对象 引用计数为1 shared_ptr<int> p9(p8); // p8指向的对象 引用计数为2 shared_ptr<int> p10(p9); // p8指向的对象 引用计数为3 p10.reset(); // p8指向的对象 引用计数为2 (解除p10与其指向的对象的关联,因此其指向对象的 引用计数-1=2) p9.reset(new int(456)); // 正确: p9指向一个新的对象,p9指向对象的 引用计数为1 (p8指向对象的 引用计数-1=1) p9 = new int(456); // 错误: 不能将一个指针赋予 shared_ptr // reset经常与unique一起使用,来控制多个 shared_ptr共享的对象: if (!p8.unique()) { p8.reset(new int(*p8)); // 如果我们不是唯一用户,则分配新的拷贝(即解除p8与其原指向对象的关联,重新关联到另一个对象,并且先后对象的值是相同的) } // 因为我们不是唯一的用户,为了不能影响到其他用户,得把自己从原对象先分离出来,再绑定到一个 新的、自己独有的、且值和原对象值相同的 新对象,而不能直接改变 多个用户共享的原对象。 *p8 += 1; // 现在我们知道自己是"唯一的用户",因此可以改变对象的值 (*p8=124) }
(4) 智能指针和异常:
- 如果使用智能指针管理内存,即使程序块由于异常过早结束,智能指针类也能确保在内存不需要的时候将其释放。
- 如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放。
void fun1() { shared_ptr<int> p(new int(123)); // 假如这里抛出异常,且在fun1中未捕获 (函数异常退出,局部对象p也会被销毁,因此其指向的动态内存 也会被释放) } void fun2() { int* p = new int(123); // 假如这里抛出异常,且在fun2中未捕获 (函数异常退出,delete得不到执行,则动态内存得不到释放。若在函数fun2之外 没有指针指向这块内存,则永远无法释放这块内存) delete p; }
智能指针陷阱 (基本规范):
(5) unique_ptr:- 不使用相同的内置指针 初始化(或reset)多个智能指针。
- 不delete get()返回的指针。
- 如果你使用get()返回的指针,记得当最后一个对应的智能指针销毁后,你的指针就无效了。
- 如果你使用智能指针管理的资源 不是new分配的内存,记住传递给它一个删除器。(默认情况下,智能指针管理的资源是 动态内存,且默认使用delete释放 其指向的动态内存)
nuique_ptr操作:
unique_ptr<T> u1 | 空unique_ptr,可以指向类型是T的对象。u1会使用delete 来是释放它的指针。 |
unique_ptr<T, D> u2 | 同上,但u2会使用一个 类型为D的可调用对象 来释放它的指针。 |
unique_ptr<T, D> u(d) | 同上,用类型为D的对象d 代替delete。 |
u = nullptr | 释放u指向的对象,将u置为空。 |
u.release() | u放弃对指针的控制权,返回指针,并将u置空。 |
u.reset() | 释放u指向的对象 |
u.reset(q) | 令u指向 内置指针q指向的对象 |
u.reset(nullptr) | 将u置空 |
- 某个时刻只能有一个unique_ptr 指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。
- 由于一个unique_ptr 拥有它指向的对象,因此unique_ pt不支持 普通的拷贝或赋值操作。(但可通过调用release或reset 将指针的所有权从一个(非const)unique_ptr 转移给另一个unique_ptr)
- 如果我们不用另一个智能指针 来保存release返回的指针,我们的程序就必须 负责资源的释放。
- 向后兼容auto_ptr(较早版本):具有unique_ptr的部分特性。特别是,不能在容器中保存auto_ptr,也不能从函数返回auto_ptr。
int main() { // 初始化unique_ptr必须采用直接初始化形式: unique_ptr<int> p1 = new int(123); // 错误: 不支持隐式转换 unique_ptr<int> p2(new int(123)); // 正确: 直接初始化 // 由于一个unique_ptr 拥有它指向的对象,因此unique_ pt不支持 普通的拷贝或赋值操: unique_ptr<int> p3(new int(123)); unique_ptr<int> p4(p3); // 错误: unique_ptr不支持拷贝 unique_ptr<int> p5 = p3; // 错误: unique_ptr不支持赋值 // 虽然不能拷贝或赋值unique_ptr,但可通过调用release或reset 将指针的所有权从一个(非const)unique_ptr 转移给另一个unique_ptr: unique_ptr<int> p6(new int(123)); unique_ptr<int> p7(new int(456)); unique_ptr<int> p8(p6.release()); // p6放弃指针的控制权,release返回指针,且将p6置为空。再利用返回的指针构造p8 (相当于p6将指针所有权 转移给p8) cout << *p8; // 输出: 123 p8.reset(p7.release()); // p8先调用reset 释放原来指向的内存,再接受p7放弃的指针所有权 cout << *p8; // 输出: 456 // 如果我们不用另一个智能指针 来保存release返回的指针,我们的程序就必须 负责资源的释放: unique_ptr<int> p9(new int(123)); p9.release(); // 错误: p9不会释放内存,而且丢失了指针,因此再也无法释放这块内存 int *p = p9.release(); // 正确: 但是必须记得delete(p) }
(6) weak_ptr:
shared_ptr可以说近乎完美,但是通过引用计数实现的它,虽然解决了指针独占的问题,但也引来了引用成环的问题,这种问题靠它自己是没办法解决的,所以在C++11的时候将shared_ptr和weak_ptr一起引入了标准库,用来解决循环引用的问题。
weak_ptr是一种 不控制所指向对象生存期 的智能指针,指向一个由shared_ptr管理的对象。
将一个weak_ptr绑定到一个shared_ptr 不会改变shared_ptr的引用计数。
一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,不管有没有weak_ptr指向该对象。
一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,不管有没有weak_ptr指向该对象。
weak_ptr接受 shared_ptr类型的变量赋值,但是反过来是行不通的,需要使用lock函数。
由于对象可能不存在,我们不能使用weak_ptr 直接访问对象,而必须调用lock。
weak_ptr操作:
操作 解释
weak_ptr<T> w 空weak_ptr可以指向类型为T的对象
weak_ptr<T> w(sp) 与shared_ptr指向相同对象的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操作:
操作 解释
weak_ptr<T> w 空weak_ptr可以指向类型为T的对象
weak_ptr<T> w(sp) 与shared_ptr指向相同对象的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。