内存管理

内存管理

C++使用new和delete两个运算符进行内存管理。

使用new进行动态分配和初始化对象

在自由空间内分配的内存是无名的,因此new无法为其分配的对象进行命名,而是返回一个指向该对象的指针:

int *pi=new int;

上述表达式在自由空间(栈空间?堆空间?)内构造一个 int对象,并且返回一个指向这个对象的指针。

默认情况下,动态分配的对象是默认初始化的,这就意味着内置类型(int)或者是组合类型的对象的值是未定义的,而类类型的对象将会使用默认构造函数进行初始化:

string *ps=new string;
int *pi=new int;

旧标准下,可以使用直接初始化的方式进行初始化一个动态分配对象。也可以使用传统的构造方式(使用圆括号)

int *pi=new int(1024);  //使用直接初始化的方式初始化一个动态分配对象
string *ps=new string(10,'9');  //使用传统的构造方式进行初始化的过程
vector<int> *pv=new vector<int>{0,1,2,3,4,5,6}; //使用列表初始化的形式

也可以对动态分配的对象进行值初始化,只需要在类型名之后添加括号即可

string *ps1=new string; //默认初始化为空 string
string *ps=new string(); //值初始化为空string
int *pi1  = new int; //默认初始化,*pi1的值未定义
int *pi2 = new int(); //值初始化为0. *pi2 为 0 这种方式只能用于初始化指针,不能用于初始化int对象

值初始化内置类型对象具有良好定义的值,默认初始化对象的值则是未定义的。对于类中那些依赖编译器射程的默认构造函数的内置类型成员,没有进行类内初始化的时候,他们的值也是未定义的。

如果具有括号包围的初始化器,可以使用auto来推断我们想要分配的对象的类型。(需要保证使用单一初始化器)

auto p1=new auto(obj); //p指向一个和obj类型相同的对象,该对象使用obj进行初始化。
auto p2=new auto{a,b,c};//括号中只能有单个初始化器

动态分配的const对象

使用new 分配const对象是合法的

//分配并初始化一个const int
const int *pci=new const int(1024);
//分配并初始化一个const的空string
const string *pcs = new const string();

类似其他的任何const对象,一个动态分配的const的对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const动态对象可以阴式初始化。由于分配的对象是const的,new返回的指针是指向一个const的指针。

内存耗尽

当一个程序用光了所有的可用内存之后,new表达式将会失败,默认情况下,当new不能分配所要求的内存空间的时候,将会抛出一个类型为bad_alloc的异常,可以通过改变new的使用方式阻止其抛出异常:

int *pi=new int();//如果分配失败,new将会抛出std::bad_alloc
int *p2=new (nothrow) int();//如果分配失败,new返回一个空指针。

我们将这种形式的new称为定位new。定位new 表达式允许我们想new传递额外的参数。在上述例子中,我们传递给它一个标准库定义的名为nothrow的对象。

释放动态内存

为了防止内存耗尽,在动态内存使用完毕之后,必须要将其归还给系统,我们通过使用delete表达式,将动态内存归还给系统。delete表达式接收一个指针,指向我们想要释放的对象。

delete p;//p需要指向一个动态分配对象或者是一个空指针

delete表达式执行两个操作:销毁给定指针指向的对象;释放对应的内存。

指针值和delete

传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者是将相同的指针释放多次,其行为是未定义的:

int i,*pi1 = &i,*pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i;//错误:i并非一个指针
delete pi1;//未定义:pi1指向一个局部变量
delete pd;//正确
delete pd2;//未定义: pd2指向的内存已经被释放了
delete pi2;//正确: 释放一个空指针总是没有问题的

对于delete pi1和pd2所产生的错误是具有潜在危害的:通常情况下,编译器不能分辨一个指针指向静态还是动态分配的对象。
虽然一个const对象的值不能被改变,但是是可以被销毁的。

动态对象的生存期直到被释放为止

调用者使用返回指向动态内存的指针的函数的时候,需要进行手动的内存释放。

//factory 返回一个指针,指向一个动态分配的对象
Foo* factory(T arg){
  return new Foo(arg);//调用者负责释放这个内存
}

调用者需要避免使用了但是未释放的情况,如下所示:

void use_factory(T arg){
  Foo *p=factory(arg);//使用了p 但是没有进行delete
}//p离开了它的作用域,但是它所指向的内存并没有被释放。

内置类型的对象被销毁的时候什么也不会发生,特别是,当一个指针离开其作用域的时候,它所指向的对象什么也不会发生。如果这个指针指向的是动态内存,内存将不会被自动释放。

使用new和delete进行动态内存管理时常见的错误

  • 没有使用delete进行内存释放,导致发生内存泄漏
  • 使用已经释放掉的对象,通过释放内存之后将指针置为空
  • 同一块内存释放两次。当有两个指针指向相同的动态分配对象时候,有可能会对其中一个指针进行了delete操作,随即又对第二个指针进行了delete操作,导致自由空间被破坏。
    坚持只使用只能指针,可以避免这些所有的问题,对于一个内存,只有在没有任何智能指针指向他的时候,才会自动进行释放。

delete之后重置指针值

当我们delete一个指针之后,指针值就变成了无效。虽然指针已经无效,但是指针仍然保存(已经释放了的)动态内存的地址。意味着这时候指针变成了空悬指针(指向一块曾经保存数据对象但是已经无效的内存的指针)。
未初始化指针的所有缺点,空悬指针也有。为了解决这个问题,可以在delete之后,将nullptr赋予指针,清楚的指向指针不指向任何对象。
动态指针的问题在于可能有多个指针指向了同一个内存。delete内存之后重置指针的方式只对当前指针有效,对于仍然指向(已经释放掉)内存的指针是没有任何作用的。
同时在实际系统中,查找指向相同内存的所有指针是异常困难的。

智能指针

c++标准库提供了两种智能指针来管理内存对象。智能指针的功能类似于常规指针,但是只能指针可以自动释放所指向的对象。

shared_ptr允许多个指针指向同一个对象;unique_ptr则是独占所指向的对象。

shared_ptr

只能指针是一种模板,当创建了一个智能指针的时候,必须提供额外的信息-指针所指向的类型。

shared_ptr<string> p1; //shared_ptr 可以指向string
shared_ptr<list<int>> p2; //shared_ptr 可以指向int的list

默认初始化的智能指针中保存一个空指针。智能指针的使用方式和普通指针是类似的,通过解引用一个智能指针返回它所指向的对象。也可以用作判断条件表明当前智能指针是否为空。

//如果p1不为空,检查它是否指向一个空string
if(p1 && p1->empty())
    *p1 = "hi"; //如果p1指向一个空string ,解引用p1,将一个新值赋予string

shared_ptr和uniqu_ptr都具有的操作如下:

shared_ptr<T> sp 空智能指针,指向类型为T的对象
unqiue_ptr<T> up 空智能指针,指向类型为T的对象
p 将p作为一个条件判断,如果p指向一个对象,则为true
*p 解引用p,获取其指向的对象
p->mem 等价于(*p).mem
p.get() 返回p中保存的指针
swap(p,q) 交换p,q保存的指针
p.swap(q) 交换p,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共享对象的智能指针的数量;主要用于调试

make_shared函数

最安全的分配和使用动态内存的方法是调用一个名为make_shard的标准库函数。这个函数在动态内存中分配一个对象并进行初始化。返回指向这个对象的shared_ptr。
使用make_shared函数的时候,必须制定想要创建的对象的类型。定义的方式和模板类相同。

//指向一个值为42的int的shared_ptr
shared_ptr<int> p3= make_shared<int>(42)
//p4指向一个值为“999999999”的string
shared_ptr<string> p4 = make_shared<string>(10,'9');
//p5指向一个值初始化的int,即值为0
shared_ptr<int> p5 =make_shared<int>();

shared_ptr的拷贝和赋值

当进行拷贝和赋值操作的时候,每个shared_ptr都会记录有多少个其他shared_ptr和其指向的是相同的对象:
我们可以认为每个shared_ptr都是具有一个关联计数器的,称其为引用计数。无论何时我们拷贝一个shared_ptr,引用计数的值都会递增。例如:

  • 使用一个shared_ptr初始化另一个shared_ptr;
  • 将shared_ptr作为一个参数传递给一个函数;
  • 作为函数的返回值;
    当我们给一个shared_ptr赋予一个新值或者是shared_ptr被销毁 (一个局部的shared_ptr)离开其作用域的时候,计数器就会进行递减。
    一旦一个shared_ptr的计数器变为0,就会自动释放所管理的对象。

shared_ptr自动销毁所管理的对象

当指向一个对象的最后一个shared_ptr被销毁的时候,shared_ptr将会自动销毁这个对象。这是通过特殊的成员函数析构函数完成销毁的动作。

shared_ptr自动释放相关联的内存

当动态对象不再被使用的时候,shared_ptr类将会自动释放动态对象,这一特性使得动态内存的使用变得较为简单。

//factory返回一个shared_ptr,指向一个动态分配的对象
shared_ptr<Foo> factory(T arg){
  return make_shared<Foo>(arg);
}

由于factory返回一个shared_ptr,所以我们确保它分配的对象在恰当的时候被释放。
由于在最后一个shared_ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留是非常重要的。
shared_ptr在无用之后的一保留的一种可能的情况是,将shared_ptr存放在了一个容器之中,随后排了容器,从而不再需要某些元素,在这种情况下应该确保使用erase删除不再需要的shared_ptr元素

不要使用get初始化另一个智能指针或者是为智能指针赋值

智能指针类型定义了一个名为get的函数,返回一个内置指针,指向智能指针管理的对象。这种情况是为了这样的一种情况设计:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回指针的代码不能delete这个指针。

使用了动态生存期资源的类

程序使用动态内存主要出于以下的原因:

  1. 程序不知道自己需要使用多少对象
  2. 程序不知道所需对象的准确类型
  3. 程序需要在多个对象之间共享数据。

智能指针和异常

如果使用智能指针,即使程序块过早结束,智能指针类也能确保内存在不再需要的时候将其释放:

void f(){
  shared_ptr<int> sp(new int(42));//分配一个新对象
  //这段代码抛出一个异常,且在f中没有被捕获
}//在函数结束的时候,shared_ptr将会自动释放内存

函数对象的退出有两种可能,正常处理结束或者是发生了异常,无论哪种情况,局部对像都会被销毁,而在局部对象销毁的过程中将会检查引用计数,就会保证了智能指针在异常情况下,可以释放掉内存。

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容

  • 内存管理 简述OC中内存管理机制。与retain配对使用的方法是dealloc还是release,为什么?需要与a...
    丶逐渐阅读 1,960评论 1 16
  • 11.看下面的程序,第一个NSLog会输出什么?这时str的retainCount是多少?第二个和第三个呢? 为什...
    AlanGe阅读 724评论 1 4
  • 内存是计算机非常关键的部件之一,是暂时存储程序以及数据的空间,CPU只有有限的寄存器可以用于 存储计算数据,而大部...
    dreamer_lk阅读 1,195评论 2 10
  • 12.1 智能指针 智能指针行为类似普通指针,但它负责自动释放所知的对象。 #include <memory> s...
    龙遁流阅读 361评论 0 1
  • 如果现在流的泪 是当初脑子进的水,那么,此刻泪流干了,是不是也该清醒了。
    緗怡阅读 161评论 0 0