[C++Prime笔记]-12、动态内存

内存分区

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填充

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值