对象的生命周期:
  • 全局对象:在程序启动时分配,在程序结束时销毁。
  • 局部对象:在进入程序块时创建,在离开程序块时销毁。
  • 局部static对象:在第一次使用前分配,在程序结束时销毁。
  • 动态分配对象:只能显式地被释放。
对象的内存位置:
  • 静态内存:用来保存局部static对象、类static对象、定义在任何函数之外的变量。
  • 栈内存:用来保存定义在函数内的非static对象。
  • 堆内存:又称自由空间,用来存储动态分配的对象。

12.1 动态内存与智能指针

动态内存管理:
  • new:在动态内存中为对象分配空间 并返回一个指向该对象的指针。
  • delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
智能指针介绍:
  • 管理动态对象。
  • 行为类似常规指针。
  • 负责自动释放 所指向的对象。
  • 智能指针也是模板。

(1) shared_ptr类:
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,程序仍会正确执行,但会浪费内存。

使用动态内存的三种原因:
  1. 程序不知道 自己需要使用多少对象。(比如容器类)
  2. 程序不知道 所需要对象的准确类型。
  3. 程序需要 在多个对象间共享数据。

(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管理动态内存 存在三个常见问题:
  1. 忘记delete内存。
  2. 使用已经释放掉的对象。
  3. 同一块内存释放两次。
经验:坚持只使用智能指针 可以避免上述所有问题。

(3) shared_ptr和new结合使用:
定义和改变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)
注:
  • 接受指针参数的智能指针构造函数 是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;
}   
智能指针陷阱 (基本规范):
  • 不使用相同的内置指针 初始化(或reset)多个智能指针。
  • 不delete get()返回的指针。
  • 如果你使用get()返回的指针,记得当最后一个对应的智能指针销毁后,你的指针就无效了。
  • 如果你使用智能指针管理的资源 不是new分配的内存,记住传递给它一个删除器。(默认情况下,智能指针管理的资源是 动态内存,且默认使用delete释放 其指向的动态内存

(5) unique_ptr:
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指向该对象。
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。