C++动态内存
C++编译器维护三种内存空间用于保存不同种类的数据。
- 静态内存
- 保存局部
static对象、类static数据成员和定义在任何函数之外的变量 - 静态内存对象由编译器自动创建和销毁。静态内存对象在使用之前创建,在程序结束时销毁
- 保存局部
- 栈内存
- 保存定义在函数内的非
static对象 - 栈对象由编译器自动创建和销毁。栈对象仅在其定义的程序块运行时才存在
- 保存定义在函数内的非
- 堆/自由空间
- 程序用堆来存储动态分配的对象,即程序运行时分配的对象
- 动态对象的生命周期由程序控制,需要显式的创建和销毁
动态内存可以直接使用new和delete运算符进行管理,但是直接管理容易出现问题,所以标准库提供了智能指针
智能指针用于管理动态对象,行为类似常规指针,主要负责自动释放所指向的对象。
标准库在memory头文件中提供了两种智能指针和一个伴随类
shared_ptr指针,允许多个指针指向同一个对象unique_ptr指针,独占所指向的对象weak_ptr指针,是伴随类,指向share_ptr所管理的对象
1. 直接管理
使用new运算符分配内存,使用delete运算符释放new分配的内存
1.1 new运算符
new动态分配和初始化
普通对象
new运算符使用下方所示格式,分配内存并返回一个指向该动态内存的指针
new表达式执行两个动作:创建并初始化对象、分配内存
1 | int *p1 = new int; |
使用new创建的对象的初始化有以下几种方式
-
默认初始化
默认情况下
new T语句会对T类型执行默认初始化如果
T为内置类型和复合类型,则为未定义;如果T为类类型,则调用默认构造函数1
2
3
4
5double*p = new double; //默认初始化
int main(){
cout << *p << endl; //输出不为0,取决于编译器
return 0;
} -
值初始化
当使用
new T()类型的语句时,会对T执行值初始化如果
T为内置类型和复合类型,则初始化为零;如果T为类类型,则调用默认构造函数1
2
3
4
5double *p = new double(); //值初始化
int main(){
cout << *p << endl; //输出为0
return 0;
} -
直接初始化
不使用等号,而是直接在类型右侧紧跟
()或者{}来进行初始化。注意要和构造函数相匹配1
2
3int *a = new int(1);
string *b = new string(7,'b');
vector<int>*c = new vector<int>{1,2,3,4,5,6};
初始化过程中的,当()或{}中只有单一初始化器时(一个元素),可以使用auto自动推断要分配的对象的类型
1 | auto p1 = new auto(1); //p1为 int*类型 |
const对象
new运算符还可用于动态分配const对象。语法格式类似普通对象,且new操作返回的是指向const的指针
1 | //语法格式 |
类似其他的const对象,动态分配的const对象在分配内存时必须进行初始化
动态分配的const对象的初始化有如下情况
- 如果是一个有默认构造函数的类类型,可以使用上述三种方式的任意一种
- 其他类型的对象必须使用值初始化或者直接初始化
定位new
当内存耗尽时,new表达式会返回一个空指针并抛出**bad_alloc异常**
1.2 delete运算符
在动态内存使用完毕之后,需要使用delete表达式来将动态内存归还给系统
delete表达式接收一个指向将要释放的对象的指针
1 | int *p = new int(); |
delete表达式执行两个动作:销毁对象、释放内存
delete运算符必须接收指针,并且最好是使用new运算符动态分配的内存的指针。如果将非new分配的指针交给delete运算符,或者是一个指针释放多次,其行为都是未定义的,可能产生破坏效果
动态分配的const对象的内存释放格式相同,没有特别之处
1 | const int *a = new const int(); |
对于基本数据类型,delete运算符可以直接释放内存,对于自定义数据类型,delete运算符会调用其析构函数
1.3 相关问题
忘记释放内存
1 | int *test(int a){ |
使用已经被释放的对象
1 | int *a = new int(); |
对同一内存释放多次
1 | int *a = new int(); |
2. 智能指针
2.1 共同特性
定义和初始化
智能指针也是模板,创建智能指针时,必须给出指针所指向的类型
1 | //智能指针的声明和定义 |
智能指针在没有显式初始化时采用默认初始化,此时智能指针被初始化为一个空指针(nullptr)
智能指针可以使用new的返回指针来初始化,但是由于接受指针参数的智能指针构造函数是explicit修饰的,因此此种初始化方式只能使用直接初始化,而不能使用拷贝初始化
1 | shared_ptr<int>p1(new int(1)); //正确,使用直接初始化 |
智能指针默认使用delete释放它所关联的对象,可以通过修改delete操作来修改智能指针的释放操作
除了修改delete操作,还可以重载智能指针的默认删除器,但是不同的智能指针格式不同
相关操作
shared_ptr和unique_ptr都支持的操作
| 操作 | 说明 |
|---|---|
shared_ptr<T> spunique_ptr<T> up |
空智能指针,可以指向类型为T的对象 |
p |
将智能指针p作为条件判断,如果p指向一个对象,则为true |
*p |
解引用智能指针,获取指向的对象 |
p->mem |
访问对象元素mem,等价于(*p).mem |
p.get() |
返回指向智能指针p所指对象的普通指针,类型为T* |
swap(p,q)p.swap(q) |
交换p和q中的指针 |
2.2 shared_ptr类
定义和初始化
除了shared_ptr和unique_ptr的相同的定义和初始化操作,shared_ptr还有如下方式可以进行初始化
| 操作 | 说明 |
|---|---|
shared_ptr<T> p(q); |
使用p管理指针q指向的对象q必须指向使用new分配的内存且能够转换为T*类型 |
shared_ptr<T>p(q);shared_ptr<T>p = q; |
p是shared_ptr q的拷贝,此操作会递增q中的计数q中的指针必须能够转换为T* |
shared_ptr<T> p(q,d); |
使用p管理指针q指向的对象q可以是普通指针或者是shared_ptr指针p使用对象d代替delete |
相关操作
操作概述
| 操作 | 说明 |
|---|---|
make_shared<T>(args); |
创建一个类型为T的动态分配的对象,使用args初始化返回指向此对象的 shared_ptr指针 |
p = q; |
二者都是shared_ptr,所保存的指针必须可以相互转换此操作会递减 p的引用计数,递增q的引用计数 |
p.unique(); |
p.use_count()为1,返回true,否则返回false |
p.use_count(); |
返回与p共享对象的智能指针数量,速度可能比较慢 |
p.reset();p.reset(q);p.reset(q,d); |
如果p的引用计数为1,则释放此对象如果提供了指针 q,则p指向q,否则置空如果提供了参数 d,则调用d而不是delete释放q |
make_shared函数
定义在头文件memory中。此函数在内存中分配一个对象并初始化它,然后返回指向此对象的shared_ptr指针
1 | shared_ptr<int> p1 = make_shared<int>(1); |
make_shared实际上调用指定类型的构造函数来创建对象,因此传递的参数args必须与T的某个构造函数相匹配。不传递参数时,则进行值初始化
通常用auto定义变量来保存make_shared的结果
1 | auto p = make_shared<int>(1); |
重载删除器
shared_ptr默认使用delete运算符释放对象,对于自定义类型,delete运算符默认会调用对象的析构函数
当对象没有析构函数时,容易忘记显式释放内存;或者默认的析构函数无法满足要求时,可以通过shared_ptr的重载删除器的操作,修改释放对象内存时的操作,并且实现对象的自动管理
1 | /* |
1 | class A{ |
管理动态内存
引用计数
每个shared_ptr内部都保存一个计数器,称为引用计数,用于保存指向指针当前所指对象的shared_ptr的个数
当拷贝一个shared_ptr时,引用计数会增加。比如,用一个shared_ptr初始化另一个shared_ptr、将shared_ptr作为函数参数、将shared_ptr作为函数返回值。
当给shared_ptr赋一个新值,或者销毁shared_ptr(如离开作用域)时,引用计数会减少。
1 | shared_ptr<int> p1 = make_shared<int>(1); |
自动销毁
当一个对象的引用计数为0时,shared_ptr类会自动销毁此对象
每次销毁shared_ptr时,调用shared_ptr的析构函数。此析构函数会将引用计数减1,如果引用计数为0,则调用对象的析构函数,销毁对象
1 | shared_ptr<int> factory(int a){ |
不要将shared_ptr和普通指针混用,因为shared_ptr随时可能释放内存,造成普通指针悬置
1 | void test(shared_ptr<int>p){ |
不要使用get方法的返回指针初始化或者赋值给另一个智能指针,容易造成引用计数混乱,从而导致内存被意外释放
可以使用get方法返回的指针初始化或赋值给一个普通指针,但是注意此指针及其衍生出的指针不能使用delete或者让其引用计数归零,否则会导致原有智能指针悬置
1 | shared_ptr<int>p(new int(1)); |
使用示例
1 | //通过拷贝构造的对象和原对象共享相同的容器,且使用智能指针管理 |
2.3 unique_ptr类
特点
unique_ptr独占它所指向的对象,不能将两个unique_ptr指向同一个对象
1 | int *p = new int(1); |
当unique_ptr被销毁时,它所指向的对象也被销毁
由于unique_ptr独占它所指向的对象,因此不允许进行拷贝和赋值操作
1 | unique_ptr<int>u1(new int(1)); |
不能对unique_ptr进行拷贝和赋值有一个例外,即可以对将要销毁的unique_ptr进行拷贝和赋值
常用于函数返回
1 | unique_ptr<int> test(unique_ptr<int> p){ //出错,参数传递执行了一次拷贝,但原指针不会销毁 |
相关操作
| 操作 | 说明 |
|---|---|
unqiue_ptr<T> u1; |
空unqiue_ptr,指向类型为T的对象 |
u = nullptr; |
使用u指向的对象并将u置空 |
u.release(); |
u放弃指针的控制权,返回指向对象的普通指针,并将u置空 |
u.reset();u.reset(q);u.reset(nullptr); |
释放u所指向的对象如果提供了指针 q,则令u重新指向q所指对象没有提供指针 q,则将u置空 |
unique_ptr<T,D*> u(p,d); |
重载删除器的unique_ptr指针的声明、定义和初始化操作 |
1 | int *p = new int(1); |
重载删除器
类似shared_ptr对删除器的重载,unique_ptr也可以对删除器进行重载,只是格式略有不同,必须显式给出删除器的类型,且删除器的类型是unique_ptr类型的一部分
1 | /* |
1 | class A{ |
1 | //注意此时类型的匹配 |
2.4 weak_ptr类
weak_ptr是一种不控制所指向对象的生命周期的智能指针,指向一个由shared_ptr管理的对象,但是不会影响shared_ptr的引用计数,更不会影响到对象的销毁。weak_ptr类似python中的弱引用
定义和初始化
weak_ptr可以使用默认初始化,同shared_ptr和unique_ptr的默认初始化一样,初始化为nullptr
1 | weak_ptr<T> w; |
不同于shared_ptr和unique_ptr可以使用new进行直接初始化,weak_ptr指向的是shared_ptr管理的对象,可以使用shared_ptr或者weak_ptr进行直接初始化和拷贝初始化
1 | shared_ptr<int>p(new int(1)); |
操作总结
| 操作 | 说明 |
|---|---|
weak_ptr<T> w;weak_ptr<T> w(p);weak_ptr<T> w = p; |
weak_ptr指针的定义和初始化包括默认初始化,直接初始化和拷贝初始化 |
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(); |
w.expired()=true,返回空的shared_ptrw.expired()=false,返回指向w所指对象的shared_ptr用于检查 weak_ptr指向的对象是否还存在 |
1 | if(shared_ptr<int>p = w.lock()){ |
3. 动态数组
某些应用场景下,需要一次性为很多对象分配连续内存,此时传统的new操作无法满足要求
标准库提供了两种一次性分配一个对象数组的方法
- 新的
new表达式语法 - 标准库
allocator类
使用上述方式分配内存构造的对象数组称为动态数组,大部情况下最好使用标准库容器以获取更好的体验和性能,而不是手动分配动态数组
3.1 new和智能指针
定义、初始化及释放
定义和释放
使用新的new和delete语法来进行动态数组的定义和释放
1 | T *p = new T[size]; //T为数组内对象的类型 |
定义动态数组时,其中的size为整型,但不一定是常量,表示动态数组中的对象的个数。p为指向动态数组中第一个T类型对象的指针。也可以使用第二种累哦行别名的方式定义动态数组,实际执行过程和第一种完全相同
对于new表达式的返回结果p,p是一个T*类型的指针,指向数组中的第一个对象。p不是指向数组类型的指针更不是数组类型的对象,不能将其用于数组的begin和end操作,也不能使用范围for语句直接处理动态数组。动态数组不是数组类型。
1 | delete [] p; |
释放动态数组时,不论使用何种方式定义,delete后必须加上[],否则执行的结果不确定,可能引发错误
初始化
默认情况下使用默认初始化
1 | int *p = new int[10]; |
可以在[]后紧跟一个()来进行值初始化
1 | int *p = new int[10](); |
还可以使用列表初始化{}的方式显式的初始化动态数组,类似普通数组的列表初始化,但是之只能进行直接初始化
初始化器的数目少于元素的数目,剩下的元素进行值初始化;初始化器的数目大于元素的数目,报错,不会分配内存
1 | int *p = new int[4]{0,1,2,3}; |
智能指针管理动态数组
使用unique_ptr管理通过new运算符创建的动态数组时,需要在对象类型后跟[]
1 | unique_ptr<T[]>p(new T[size]); |
由于此时unique_ptr指向的是动态数组,而不是单个对象,因此智能指针p不能直接使用*和->运算符。此时销毁智能指针所指对象时自动调用delete[]而不是delete,并且可以直接使用下标运算符来访问数组中的元素
1 | p[0]; p[1]; |
除了上述特殊之外,unique_ptr的其他操作都可用于管理动态数组
shared_ptr不支持直接管理动态数组,需要在初始化时显式提供适用于动态数组的自定义删除器。只要提供了自定义的适用于动态数组的删除器,就可以使用shared_ptr管理动态数组
1 | shared_ptr<T>p(new T[size],删除器); |
指向动态数组的shared_ptr未定义下标运算符,也不支持指针算数运算。但不同于unique_ptr,此处的shared_ptr支持*和->运算符,也支持其他的shared_ptr操作。此时对shared_ptr执行*或者->运算时,shared_ptr相当于指向动态数组的第一个对象的指针,只能直接访问第一个对象
1 | shared_ptr<Test>p(new Test[3]{Test(2),Test(3),Test(4)},[](Test*p){delete []p;}); |
3.2 allocator类
new操作将内存分配和对象构造组合在一起,delete操作将对象析构和内存释放组合在一起。当分配一大块内存时,通常需要按需构造对象,此时如果使用new和delete则会造成大量不必要的对象构造和赋值操作,从而导致资源的浪费
当分配一大块内存时,通常需要将内存分配和对象构造相分离,待需要的时候按需构造对象
标准库memory提供了allocator类实现内存分配和对象构造的分离
定义和内存分配
allocator类是一个模板类,定义时必须指定要分配内存的对象类型
1 | allocator<T>alloc; //定义allocator对象 |
allocator类根据对象类型和数量来确定内存大小和对齐位置,并且使用allocator类分配的内存是未构造的
allocator类使用construct成员函数在给定的内存位置构造特定的对象。
construct函数接收一个指针(类型为T*,指向将要构造的对象的位置)和零个或多个额外参数
- 没有额外参数,进行默认初始化
- 有额外参数,将额外参数传入相匹配的构造函数来构造对象
1 | allocator<string>alloc; |
销毁和内存释放
构造出来的对象可以使用destory成员函数来销毁。destory函数接收一个指向要销毁对象的指针,并调用此对象的析构函数来销毁对象。不能将destory用于未构造的内存
1 | allocator<int>alloc; |
对象或元素被销毁后,释放出来的内存空间可以重复利用,再次进行构造,当然也可以使用deallocate释放
deallocate函数接收两个参数p和n
p为指向要释放的内存的起始位置的指针n为要释放的内存块的个数,要和分配内存时的一致,否则会出现异常
注意可以直接使用deallocate回收内存,而不用提前销毁对象,但是这样可能会导致对象销毁异常
并且使用deallocate时,n的值必须和内存分配时给出的值一致,即整块内存必须同时分配和释放
伴随算法
标准库为allocator类定义了两种伴随算法(不是成员函数),用于快速拷贝和填充未初始化的内存
| 算法 | 说明 |
|---|---|
uninitialized_copy(b,e,b2); |
从迭代器b和e指定的范围内拷贝元素到迭代器b2指定的内存中b2指定的内存必须足够容纳所有要拷贝的元素,否则可能出错算法返回指向最后一个构造元素之后位置的指针 |
uninitialized_copy_n(b,n,b2); |
从迭代器b开始拷贝n个元素到迭代器b2指定的内存中b2指定的内存必须足够大算法返回指向最后一个构造元素之后位置的指针 |
uninitialized_fill(b,e,t); |
在迭代器b和e指定的范围内构造对象对象的值均为 t的拷贝 |
uninitialized_fill_n(b,n,t); |
从迭代器b指向的内存地址开始构造n个对象对象的值均为 t的拷贝,且内存必须足够大 |
上述伴随算法构造对象的类型根据迭代器的类型确定。此处的迭代器可以是容器的迭代器类型,也可以是指针
1 | allocator<int>alloc; |