前言
该系列是《C++Primer第五版》的笔记,包含本人认为值得记录和整理的主要的知识点,并不是全部内容,也不是具体的内容。
该系列文章的作用应该是作为复习或预习的参考,有哪些知识点忘记或想学,可以大致浏览下该文章,然后再去书中寻找详细解答。(本系列文章基本是按书本顺序罗列的知识点,便于大家去书中寻找)
所以看该文章前,需要有一定的C++基础,否则阅读起来可能有困难。
本文大致整理了第十二章的知识点,涉及到C++关于动态内存和智能指针的知识,重要!
链接目录
- 第二章:变量与基本类型
- 第三章:字符串、向量和数组
- 第四章:表达式
- 第五章:语句
- 第六章:函数
- 第七章:类
- 第八章:IO库
- 第九章:顺序容器
- 第十章:泛型算法
- 第十一章:关联容器
- 第十二章:动态内存
- 第十三章:拷贝控制
- 第十四章:重载运算与类型转换
- 第十五章:面向对象程序设计
- 第十六章:模板与泛型编程
- 第十七章:标准库特殊设施
- 第十八章:用于大型程序的工具
- 第十九章:特殊工具与技术
动态内存与智能指针
基本概念:
- 静态内存:保存局部
static
对象、类static
数据成员以及任何函数之外的变量。 - 栈内存:保存定义在函数内的非
static
对象。 - 堆内存:保存动态分配的对象。
动态内存的管理:
new
:在动态内存中为对象分配空间并返回一个指向该对象的指针。delete
:接受一个动态对象的指针,销毁对象并释放内存。
智能指针:定义在头文件memory
中。
shared_ptr
:允许多个指针指向同一个对象。unique_ptr
:独占所指的对象。weak_ptr
:弱引用,指向shared_ptr
所管理的对象。
shared_ptr类
智能指针也是模板,需要提供对象类型。
shared_ptr<string> p1;//默认初始化是一个空指针
//在使用指针前,应当检测指针是否为空
if(p1){
...
}
shared_ptr
和unique_ptr
都支持的操作:
shared_ptr
独有的操作:
make_shared
函数:类似顺序容器中的emplace
成员,跳过拷贝直接构造。
shared_ptr
的拷贝和赋值:当进行拷贝和赋值操作时,shared_ptr
会记录有多少个其他shared_ptr
指向相同的对象。这个计数器叫做引用计数。
引用计数递增的情况:
- 用一个
shared_ptr
初始化另一个shared_ptr
。 - 一个
shared_ptr
作为参数传递给一个函数。 shared_ptr
作为函数的返回值。
引用计数递减的情况:
- 给一个
shared_ptr
赋予新值。 - 一个
shared_ptr
被销毁(离开作用域)。
当一个shared_ptr
的计数器为0时,会自动释放所管理的对象。
使用动态内存的原因:
- 程序不知道自己需要使用多少对象。
- 程序不知道所需对象的准确类型。
- 程序需要在多个对象间共享数据。
直接管理内存
使用new
动态分配和初始化对象:
int *pi = new int;//默认初始化,值未定义
int *pi2 = new int(42);//显式初始化
int *pi3 = new int();//值初始化为0
//对于定义了构造函数的类型,值初始化和默认初始化都是调用构造函数
int *p1 = new string;
int *p2 = new string();//结果都是初始化为空string
动态分配的const
对象:
const int *pci = new const int(1024);//const对象必须初始化
内存耗尽:如果堆内存没有空间,new
表达式会失败,抛出bad_alloc
异常。
int *p1 = new int;//如果分配失败,抛出bad_alloc异常
int *p2 = new(nothrow)int;//阻止抛出异常,如果分配失败返回一个空指针
释放动态内存:销毁指向的对象,释放对应的内存。
delete p;//p必须指向一个动态分配的对象或是一个空指针
指针值和delete
:编译器能分辨p
是否是指针,但通常不能分辨指针是指向静态还是动态分配的对象,也不能分辨对象是否以及被释放。
动态对象的生存期直到被释放时为止:如果没有显式释放,它会一直存在直到程序结束。
动态内存的管理非常容易出错!
使用new
和delete
管理动态内存存在三个常见问题:
- 忘记
delete
内存,会导致内存泄漏问题。 - 使用已经释放掉的对象。
- 同一块内存释放两次。
delete
之后重置指针值:在delete
之后,指针就变成了空悬指针,指向一块无效的内存。可以在释放指针之后置为nullptr
,避免后续使用空悬指针。
shared_ptr和new结合使用
可以用new
返回的指针来初始化智能指针,由于智能指针的构造函数是explicit
的,所以不能将内置指针隐式转换为只能指针,必须使用直接初始化。
shared_ptr<int> p(new int(42));
shared_ptr<int> p2 = new int(1024);//错误,必须使用直接初始化形式
//在函数返回值也类似
shared_ptr<int> f(int p){
return new int(p);//错误,不能隐式转换
return shared_ptr<int>(new int(p));//正确
}
定义和改变shared_ptr
的其他方法:
不要混合使用普通指针和智能指针:
void process(shared_ptr<int> ptr){
...
}//ptr离开作用域,被销毁
int *x = new int(1024);
precess(x);//错误,不能将int*转换为一个shared_ptr<int>
pricess(shared_ptr<int>(x));//合法,但process执行完成后,x指向的内存会被释放
int j = *x;//x是一个空悬指针,由于智能指针判定计数为0,x指向的内存被释放
也不要使用get
初始化另一个智能指针或为智能指针赋值:get
函数返回一个内置指针,通常用于向不能使用只能指针的代码传递一个内置指针。不能delete
用get
返回的指针,否则会导致智能指针无法正常工作(计数未置为0,但内存已经被释放)。
其他shared_ptr
操作:
if(!p.unique())
p.reset(new string(*p));//不是唯一的用户,使用拷贝而不改变原对象
*p += newVal;//是唯一的用户,可以根据自己的需要操作
智能指针和异常
void f(){
shared_ptr<int> sp(new int(42));
//代码中抛出一个异常,且在f中未被捕获
}//在函数结束时会释放内存
void f(){
int *ip = new int(42);
//代码中抛出一个异常,且在f中未被捕获
delete ip;//由于异常未被执行
}//ip指向的内存无法释放,导致内存泄漏
智能指针和哑类:对于没有良好定义的析构函数(为C和C++两种语言设计的类,通常要求用户显式释放资源),即使显式释放,也可能在释放前发生异常导致无法释放。这时可以使用shared_ptr
避免内存泄露,即使代码段没被执行,局部智能指针离开作用域也会被销毁使内存正常释放。
使用自定义的释放操作:
void our_delete(Sales_data *p){
//手动执行析构函数的内容
}
void f(...){
Sales_data c(...);
shared_ptr<Sales_data> p(&c, our_delete);
//当f退出时,即使是因为异常退出,也能正确释放内存
}
//此时p被销毁时,不执行delete,而是调用our_delete。
智能指针陷阱:为了正确使用,必须坚持一些规范
- 不使用相同的内置指针值初始化(或
reset
)多个智能指针。 - 不
delete
用get()
返回的指针。 - 不适用
get()
初始化或reset
另一个智能指针。 - 如果使用
get()
返回的指针,当最后一个智能指针销毁后,这个指针就无效了。 - 如果使用智能指针管理的资源不是
new
分配的内存(一般不能delete
,比如malloc
出来的),记住传递给它一个删除器。
unique_ptr
unique_ptr
并不支持拷贝或赋值操作:
unique_ptr<string> p1(new string());
unique_ptr<string> p2(p1);//错误,不能拷贝
unique_ptr<string> p3;
p3 = p1;//错误,不能赋值
//但可以转移所有权
unique_ptr<string> p2(p1.release());//转移所有权,p1为空
unique_ptr<string> p3(new string());
p2.reset(p3.release());//reset释放p2的内存,接着指向p3指向的内容
p2.release();//错误,p2指向空,不会释放内存,而且丢失了指向原本内存的指针
auto p = p2.release();//正确,但该类型不是智能指针,需要手动delete(p)
传递unique_ptr
参数和返回unique_ptr
:不能拷贝unique_ptr
的例外,此时要返回的unique_ptr
对象即将销毁,会执行一种特殊的拷贝。
unique_ptr<int> clone(int p){
return unique_ptr<int>(new int(p));
}
unique_ptr<int> clone(int p){
unique_ptr<int> ret(new int(p));
return ret;
}
向unique_ptr
传递删除器:
//decltype(end_connection)*是函数类型,end_connection是函数指针
void f(destination &d){
connection c = connect(&d);
unique_ptr<connection, decltype(end_connection)*>
p(&c, end_connection);
}
weak_ptr
weak_ptr
指向由shared_ptr
管理的对象,将一个weak_ptr
绑定到shared_ptr
不会改变引用次数。
由于对象可能不存在,不能直接使用weak_ptr
直接访问对象,必须调用lock
。
动态数组
通常STL中内置的类型可以很好的代替动态数组(通常更安全且更高效),如非必要,不使用动态数组。
new
和数组:
int *pia = new int[get_size()];//pia指向第一个int
分配一个数组得到的一个元素类型的指针,并不是数组类型,所以不能对动态数组调用begin或end,也不能使用范围for语句。
初始化动态数组:
int *pia = new int[10];//未初始化的int
int *pia2 = new int[10]();//值初始化为0的int,但不能在括号中给出初始化器
int *pia3 = new int[10]{1, 2, 3};//剩下的执行值初始化
动态分配一个空数组是合法的:
int *p = new int[0];//合法,但p不能解引用
释放动态数组:如果错用了释放方式,行为结果是未定义的。
delete p;//p指向一个动态分配的对象
delete[] p;//p指向一个动态分配的数组
智能指针和动态数组:可以使用unique_ptr
来管理动态数组,此时不支持点和箭头运算符。
unique_ptr<int[]> up(new int[10]);
up.release();//自动调用delete[]销毁
要使用shared_ptr
必须提供自定义的删除器:默认情况下shared_ptr
使用delete
销毁,如果对象是一个动态数组,则结果是未定义的,所以需要提供自定义的删除器。而且shared_ptr
未定义下标运算符,智能指针类型也不支持指针算术运算,为了访问数组元素,必须用get
获得内置指针,然后再用它来访问数组元素。
allocator类
将内存分配和对象构造分离开,避免不必要的浪费。
allocator<string> alloc;
auto const p = alloc.allocate(n);//分配n个未初始化的string,p指向不可变
auto q = p;//q指向可变
alloc.construct(q++);//构造一个空string
alloc.construct(q++, 10, 'c');//构造一个string(10, 'c')
cout << *p << endl;
cout << *q << endl;//错误,q指向的内存未构造
while(q != p)
alloc.destroy(--q);//释放构造过的string
alloc.deallocate(p, n);//释放内存
拷贝和填充未初始化内存的算法: