内存分区
C++中的内存分区分为以下五类
栈内存:存储定义在函数内的非static对象,有局部变量、函数参数、返回地址等,栈内存中的对象是由编译器自动创建和销毁的。
堆内存:需要动态申请的内存空间,也称为自由空间,它是使用malloc分配的内存区。由程序员来控制内存的分配和释放。使用不当会造成内存泄漏。
全局区/静态存储区:存储全局变量(定义在函数之外的变量),局部static变量以及类static数据成员。此类对象在程序开始时创建,结束时销毁。
常量存储区:存放常量,不可修改,程序结束时释放。
代码区:存放代码,不允许修改,但可以执行,存放编译后的二进制文件。
内存相关内容:https://leetcode.cn/leetbook/read/cpp-interview-highlights/e4vkxv/
智能指针
C++中,常使用new和delete运算来管理动态内存:new用于在动态内存中为对象分配空间并返回一个指向该对象的指针。delete接收一个动态对象的指针,销毁该对象,并释放与之关联的内存。
在使用new和delete动态管理内存时,很难保证能够在正确的时间来释放内存。忘记释放动态内存会导致内存泄漏;在内存仍在使用时释放,会导致引用非法内存指针。
C++标准库提供了一些智能指针来管理动态对象,不同于常规指针的是 :智能指针可以安全的自动释放所指向的对象。
智能指针包含在头文件<memory>中;
程序使用动态内存的三种情况:
①程序不知道自己需要使用多少个对象时使用容器类
②程序不知道所需对象的准确类型
③程序需要多个对象之间共享底层数据。
shared_ptr类
shared_ptr共享指针使用一个引用计数,来确保多个指针指向同一个底层数据。
智能指针也是一个模板类,当创建一个智能指针时,需要指定该指针指向的类型。它的使用方式和普通指针类似,可以解引用返回指针指向的对象。使用智能指针时也要确保指针合法。
// 默认初始化的智能指针保存着一个空指针
shared_ptr<string> p1;// 指向一个字符串的指针
shared_ptr<vector<int>> p2;//指向一个int型容器的指针
make_shared(...)
最安全的分配和使用动态内存的方法是,调用一个标准库函数make_shared(...);此函数在动态内存中分配一个对象并初始化它,该函数必须要指定想要创建对象的类型,并返回指向此对象的shared_ptr。
// p3指向一个值为10的智能指针
shared_ptr<int> p3 = make_shared<int>(10);
// p4指向你给一个置位"9999"的字符串
shared_ptr<string> p4 = make_shared<string>(4,'9');
// p5指向一个值初始化的int型变量,值为0
shared_ptr<int> p5 = make_shared<int>();
make_shared<type>()函数使用参数来构造给定类型type的对象,但这些参数必须与给定类型type的某个构造函数相匹配。如果不传入参数,则使用值初始化。
// 使用auto来定义一个对象来保存make_shared创建的指针
auto p6 = make_shared<int>(10);
shared_ptr的赋值和拷贝
当使用智能指针进行拷贝和赋值时,会使得多个智能指针指向同一个底层数据,并且每一个智能指针都会记录有多少个其他的shared_ptr指向该底层数据。
shared_ptr中有一个关联计数器 ,也叫作引用计数。当拷贝一个shared_ptr时,该计数器都会递增。比如:shared_ptr作为函数参数或函数返回值、shared_ptr用于初始化另一个shared_ptr。当给一个shared_ptr赋值或者局部shared_ptr离开作用域时,计数器会递减。
shared_ptr的计数器变为0时,它会自动释放自己所管理的内存。
// p7指向值为111的的指针
shared_ptr<int> p7 = make_shared<int>(111);
// p7的计数器会递减为0,释放111所占用的空间,p6的计数器会增加
p7 = p6;
自动销毁对象
shared_ptr的使用析构函数来销毁它的对象。shared_ptr的析构函数会递减它所指向对象的计数器,当引用计数变为0时,shared_ptr的析构函数会销毁指向的对象,并释放占用的内存。即,当动态对象不在被使用时,shared_ptr会自动释放动态对象。
new、delete直接管理内存
使用new和delete直接管理内存的类是不能依赖类对象的任何默认的拷贝、赋值和销毁操作。这会导致非法使用内存。
默认情况下,new动态分配的对象是默认初始化,即内置类型或组合类型的对象的值将是未定义的。类类型对象江永默认构造函数进行初始化
// 默认初始化,p1指向的值是未定义的
int * p1 = new int;
// 使用默认构造函数初始化一个空的string
string * sp = new string;
也可以使用直接初始化的方式来初始化一个动态分配的对象。即使用()运算符或列表初始化。
// 直接初始化,使用指定的值直接初始化指针
int *p2 = new int(10);
string *sp2 = new string(10,'i');
使用值初始化,直接在类型名后跟一对括号()即可
int *p3 = new int();// 值初始化为0
也可以使用auto从初始化器中推断要分配的对象的类型,但只有当括号中仅有单一初始化器时才可以使用。
auto p1 = new auto(10);// 正确,p1是个int型指针
auto p2 = new auto{11,12,13};//是非法的
使用new动态分配一个const对象,但必须进行初始化。分配const对象时,new返回一个指向const的指针。对于定义了默认构造函数的类类型,const动态对象可以隐式初始化,其他类型必须显式初始化。
const int *pci = new const int;// 不合法的,值是未定义的
const int *pc2 = new const int(10);// 显式初始化一个const int
const string *sp = new const string;//隐式初始化一个空的const字符串
内存耗尽
当程序使用完它可用的内存后,new表达式会失败,一般它会抛出一个bad alloc的异常。但也可以改变使用new的方式来阻止抛出异常。
使用定位new,向new传递额外参数。
int *p1 = new int();// 如果分配失败,返回空指针,并抛出bad_alloc异常
int *p2 = new (nothrow)int;// 只返回空指针,不抛出异常
释放内存
使用delete表达式,将不再使用的内存归还给系统。delete销毁给定指针的对象,并释放相应的内存,传递给delete的指针必须指向动态分配的内存或者一个空指针。释放一块 飞new分配的动态内存,或者将相同的指针释放多次的行为都是未定义的。
int i = 0;
delete i;// 不能释放一个非指针变量
int *p1 = &i;
delete p1;//未定义行为,
int * p2 = new int(10);
int * p3 =p2;
delete p2;//正确
delete p3;// 指向的内存已经释放,不能释放第二次
int *p4 = nullptr;
delete p4;// 允许释放一个空指针
当使用内置指针(非智能指针)管理某块动态内存时,在显式释放前,该内存都是被占用的。
空悬指针
当delete一个指针后,指针值就变得无效了,虽然指针已经无效,但在很多机器上仍然保存着已经释放的动态内存的地址,即delete后,指针就变成了空悬指针:指向一块曾经保存数据对象但现在已经无效的内存地址。
可以再delete指针后,将nullptr赋予指针,来释放掉指针和内存的所有关联。但是若有多个指针指向相同的内存,重置指针方法只对这个指针有效。对其他仍指向已释放内存的指针是无效的。
shared_ptr与new
可以使用new返回的指针来初始化智能指针,但是接收指针参数的智能指针的构造函数是explicit的,所以不能直接将一个内置指针隐式的转换成一个智能指针。必须使用直接初始化的形式来初始化一个指针。
shared_ptr<int> p1 = new int(10);// 错误
shared_ptr<int> p2(new int(10));
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放内存。但我们也可以将智能指针绑定到一个指向其他类型的资源指针上,但是必须提供自己的操作来代替delete。
shared_ptr <T> sp | 一个空的制作你能指针,可以指向类型为T的对象 |
sp、*sp、sp->mem | 单个智能指针sp可以作为条件判断,*sp解引用、sp->mem等价于(*sp).mem; |
sp.get() | 返回sp中保存对象的内置指针,当智能指针释放对象后,此指针指向的对象便消失了 |
swap(sp1,sp2),p.swap(q) | 交换指针 |
make_shared<T>(args) | 返回一个shared_ptr,指向一个动态分配的T对象,使用args来初始化对象 |
shared_ptr<T>p(q); | p是q的拷贝,此操作递增q中的计数器,q中的指针必须能转换成T* |
shared_ptr<T>p(unique); | p从unique_ptr那里接管对象的所有权 ,将u置空 |
shared_ptr<T>p(q,d); | 当q是内置指针,p接管q所指对象的所有权,并且p将使用可调用对象d来代替 delete。 当q是shared_ptr时,p是q的拷贝,唯一的区别是p使用可调用对象d来代替delete |
p = q; | shared_ptr的赋值操作,递减p的因引用计数,递增q的引用计数 |
p.unique() | 若p.use_count()为1,则返回true |
p.use_count() | 返回p共享对象的智能指针数量。 |
p.reset() p.reset(q) p.reset(q,d) | 若p是唯一指向其对象的智能指针,reset会释放此对象,若可选参数内置指针q,会使得p指向q,若传递了参数d,p将调用d来释放q |
混合使用内置指针和智能指针
不要使用多个 智能指针独立的指向同一个内置指针,也不要使用一个新的临时智能指针去指向一个内置指针。这样会导致系统去调用一个已经被释放的内存。
void process(shared_ptr<int> ptr){
//
}
int *x(new int(10));
// 使用一个临时智能指针指向一个内置指针
process(shared_ptr<int>(x));
// 使用完之后,引用计数变为0,x指向的内存被释放
int j = *x;// 未定义行为,x已变成一个空悬指针
智能指针有一个get函数可以获得它所指向对象的内置指针,该指针不能随意使用delete,也不能将此内置指针在绑定到其他智能指针上。
shared_ptr<int> p(new int(10));
int *q = p.get();
// 将q再次绑定到一个新的智能指针上
process(shared_ptr<int>(q));
// 临时变量离开它的生存区后自动销毁,q指向的对象被释放
int foo = *p;// 未定义行为,p指向的内存已被释放
所以,当一个内置指针被绑定到一个智能指针上后,这块内存的管理权就交给了智能指针。就不要再使用内置指针来访问它了。
动态内存与异常
在函数中,如果中间产生了一场,函数会抛出异常后退出,出现异常后的代码不再执行。使用智能指针能够确保在异常发生后正确的释放指针。
void f1(){
shared_ptr<int> p(new int(10));
// 在此处产生一个异常
// other....
}// 在函数结束后,智能指针可以自动释放内存
void f2(){
int *p(new int(10));
// 在此处产生一个异常
// other....
delete p;
}// 在函数结束后,因为产生异常,p指向的内存未能被释放
unique_ptr
某个时刻 ,只能有一个unique_ptr拥有它指向的对象。当独享指针被销毁时,它所指向的对象也被销毁。
由于一个 unique_ptr拥有它指向的对象,因此它不支持普通的拷贝和赋值。
unique_ptr<int>p1(new int(10));
unique_ptr<int>p2(p1);// 错误 不支持拷贝
unique_ptr<int> p3;
p3 = p1;// 错误,不支持赋值
虽然不能使用拷贝或赋值unique_ptr,但可以使用release或者reset函数,将指针的所有权从一个非const的unique_ptr转移给另一个unique_ptr;
unique_ptr<int> p1(new int(10));
// release将p1置空,并用p1的指针初始化p2
unique_ptr<int> p2(p1.release());
unique_ptr<int> p3(new int(1111));
p2.reset(p3.release());// 释放p2原来指向的内存,并将p3的所有权转给p2
unique_ptr的操作
unique_ptr<T> u1 unique_ptr<T, D> u2 unique_ptr<T, D> u3(d) | 一个空的unique_ptr指针,可以指向类型为T的对象,u1使用delete来释放内存,u2使用类型为D的可调用对象来释放指针,u3使用类型为D的对象d代替delete |
u = nullptr | 释放u指向的内存,并将u置位空 |
u.release() | u放弃对指针的控制权,返回一个内置指针,并将u置位空。如果不用另一个智能指针来接管智能指针,则需要我们负责资源的释放 |
u.reset(), u.reset(q), u.reset(numllptr) | 释放u指向的对象,如果提供了q,则令u指向这个对象,否则将u置位空 |
传递和返回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的删除器
unique_ptr管理删除器的方式与shared_ptr不同,重载一个unique_ptr的删除器会影响到该类型以及如何构造该类型的对象。我们需要在尖括号指向的类型之后提供删除器类型。在创建或者reset一个这种unique_ptr类型的对象时,必须要提供一个指定类型的可调用对象。
void f(int i){
int * i = new int(10);
unqiue_ptr<int,decltype(mydeleter)*> p(i,mydeleter);
}
weak_ptr
弱指针是一种不控制所指对象生存周期的智能指针,它指向有一个shared_ptr管理的对象。将weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。当最后一个指向对象的shared_ptr被销毁后,即使有weak_ptr指向此对象,对象也会被释放。
weak_ptr<T> w | 空weak_ptr可以指向类型为T的对象 |
weak_ptr<T> w(sp) | 与shared_ptr指针sp指向相同对象 |
w = p | p可以是一个shared_ptr或一个weak_ptr |
w.reset() | 将w置为空 |
w.use_count() | 与w共享对象的shared_ptr的数量 |
w.expired() | 若w.use_count()为0,则返回true,即指针过期 |
w.lock() | 若w.expired()返回true,则返回一个空的shared_ptr,否则返回一个指向w对象的shared_ptr |
weak_ptr是弱共享指针,不会增加共享指针的引用计数,但它指向ed对象可能被释放掉。所以不要直接使用弱共享指针来访问对象。在使用前要检查指针指向的对象是否存在。
if(shared_ptr<int> np = wp.lock()){
// 如果np不为空,则可以使用弱共享指针wp
}
动态数组
new和delete也可以管理对象数组。
new分配动态数组
当使用new来分配一个动态数组时,需要在类型名后加上[]运算符。new [ ]分配一个数组是,得到的并不是一个数组类型的对象,而是返回第一个元素的指针。因此不能使用begin()、end()迭代器,也不能是范围for语句处理动态数组中的元素。
// size可以不是常量,但必须是整型变量
int * p1 = new int[size];
// 使用类型别名来分配动态数组
typedef int arrT[size];
int *p2 = new arrT;// 分配一个大小为size的动态数组
在分配动态数组时,new默认使用默认初始化,但也可以对数组中元素值初始化,方法是在[]运算符后使用()运算符。
新标准中,可以提供一个元素初始化器的花括号列表来初始化。若初始化器中的元素小于size,则剩余元素使用值初始化。但初始化器中的数目不能大于[size]中的size,否则new会跑出一个bad_arry_new_length的异常。
int *p2 = new int[10];// 默认初始化,数组中的值是未初始化的元素
int *p3 = new int[10]();// 值初始化,数组中元素值都为0
string *s1 = new string[10]();// 长度为10的空字符串数组
使用初始化器的花括号列表
int *p4 = new int[3]{1,2,3};
动态分配一个空数组是合法的。当new一个大小为0的数组时,new返回一个合法的非空指针。此指针类似于尾后迭代器一样,可以用于比较,但不能用于解引用。
int n= getsize();
int *p = new int[n];
// 当n = 0时,此循环依旧是合法的
for(int *q =p;q!=p+n;q++){
// process
}
delete动态数组
使用delete释放动态数组的内存时,需要在指针前面加上一个[]运算符。然后动态数组中的元素会按照逆序依次销毁。[]运算符指示编译器此指针指向的是一个对象数组的首元素,当我们delete一个指向数组的指针时忽略了[]的行为是未定义的。
int * p = new int[10];
delete [] p;
typedef int arr[10];
int *p1 = new arr;
delete [] p1;// 不可忽略[]
智能指针管理动态数组
标准库提供了一个可以管理new分配的数组的unique_ptr版本,需要在unique_ptr对象类型后面跟一对[]。但当一个unique_ptr指向一个数组时,它就不能使用 点运算符和剪头成员运算符,而是使用下标运算符来访问数组。
unique_ptr<int[]> up(new int[10]);
// 访问动态数组中的元素
for(size_t i = 0;i!=10;++i){
up[i] = i;
}
up.release();// 自动使用delete[]来释放数组
共享指针shared_ptr并不能直接管理动态数组,如果使用它来管理动态数组时,必须要提供自己定义的删除器,否则的话代码的行为将是未定义的,因为shared_ptr默认使用delete来销毁对象。
shared_ptr<int> sp(new int[10],[](int *p){delete []p;});
// 使用内置指针来访问元素
for(int i =0;i!=10;++i){
*(sp.get()+i) = i;
}
sp.reset();// sp使用自定义的lambda函数来释放动态数组内存
allocator类
new将内存的分配和对象的构造组合在一起。delete将对象析构和内存释放组合在一起。通常计划在一大块内存上按需来构造需要的对象,也即我们希望将内存分配和对象构造相分离,这也就意味着我们可以分配大块内存,但是只在真正需要的时候才执行对象创建操作,这样可以减少一些不必要的操作(免去创建许多用不到的对象)。
标准库allocator类将内存分配和对象的构造分离开,它提供了一种类型感知的内存分配方法。它分配的内存时原始的未构造的。它在分配内存时,为根据给定的类型来确定内存的大小和对齐位置。
C++的类型感知的内存分配方法是指在编译时,根据变量的类型,自动分配内存空间的方法。这种方法可以有效地减少内存空间的浪费,提高程序的运行效率。
allocator<T> all | 定义了一个名为 all的allocator对象,可以为类型为T的对象分配内存。 |
all.allocate(n) | 分配一段原始的未构造的内存,保存n个T类型对象 |
a.deallocate(p,n) | 释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象。p必须是一个先前由allocate返回的指针,且n必须是p创建时所要求的的大小。在调用deallocate之前,用户必须对每个在这块内存创建的对象使用destory函数。 |
a.construct(p,args) | p必须是一个类型为T*的指针,指向一块原始内存,arg被传递给T的构造函数来构造对象 |
a.destory(p) | p为T*类型的指针,此算法对p指向的对象执行析构函数 |
// 创建一个可以分配string的allocator对象
allocator<string> alloc;
// 创建n个未初始化的string
// 返回分配空间的首地址,指针类型是void*
auto const p = alloc.allocate(n);
auto q = p;
alloc.construct(q++);// 在q的位置构建一个空字符串,然后q的向前移动一个位置
alloc.construct(q,10,'c');// 在q的位置构建一个长度为10内容全为'c'的字符串
q++;// 上一个构造结束后,q的位置没有变更,q当前的值为'cccccccccc'所以要移动q的位置
alloc.construct(q,"hi");
cout<<*p;// 允许访问已构建的字符串
cout<<*q;// 不能访问未构造的区间
// 销毁构造的内存
while(q!=p){
alloc.destory(--q);
}
// 释放内存
alloc.deallocate(p,n);// n的大小和参数必须与调用allocate分配是提供的大小相同
allocator类有两个伴随算法,可以再未初始化的内存中创建对象。这些函数在给定的位置创建元素,而非系统给他们分配内存
uninitialized_copy(b,e,b2); | 从迭代器b和e支出的范围拷贝元素到迭代器b2指定的未构造的原始内存中,b2指向的内存必须足够容纳输入序列中元素的拷贝 |
uninitialized_copy_n(b,n,b2) | 从迭代器b开始拷贝n个元素到b2开始的内存中 |
uninitialized_fill(b,e,t) | 在迭代器b和e指定的原始内存范围中创建对象,对象的值均为t的拷贝 |
uninitialized_fill_n(b,n,t) | 从迭代器b指向的内存地址开始创建n个对象,b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。 |
vector<int> vi{1,2,3,4,5,6,7};
auto p = alloc.allocate(vi.size()*2);
// q是一个指针,指向最后一个构造的元素之后的位置
auto q = uninitialized_copy(vi.begin(),vi.end(),p);
uninitialized_fill(q,vi.size(),0);// 剩余的位置用0填充