C++内存管理详解.docx

上传人:b****8 文档编号:9958462 上传时间:2023-05-22 格式:DOCX 页数:29 大小:33.15KB
下载 相关 举报
C++内存管理详解.docx_第1页
第1页 / 共29页
C++内存管理详解.docx_第2页
第2页 / 共29页
C++内存管理详解.docx_第3页
第3页 / 共29页
C++内存管理详解.docx_第4页
第4页 / 共29页
C++内存管理详解.docx_第5页
第5页 / 共29页
C++内存管理详解.docx_第6页
第6页 / 共29页
C++内存管理详解.docx_第7页
第7页 / 共29页
C++内存管理详解.docx_第8页
第8页 / 共29页
C++内存管理详解.docx_第9页
第9页 / 共29页
C++内存管理详解.docx_第10页
第10页 / 共29页
C++内存管理详解.docx_第11页
第11页 / 共29页
C++内存管理详解.docx_第12页
第12页 / 共29页
C++内存管理详解.docx_第13页
第13页 / 共29页
C++内存管理详解.docx_第14页
第14页 / 共29页
C++内存管理详解.docx_第15页
第15页 / 共29页
C++内存管理详解.docx_第16页
第16页 / 共29页
C++内存管理详解.docx_第17页
第17页 / 共29页
C++内存管理详解.docx_第18页
第18页 / 共29页
C++内存管理详解.docx_第19页
第19页 / 共29页
C++内存管理详解.docx_第20页
第20页 / 共29页
亲,该文档总共29页,到这儿已超出免费预览范围,如果喜欢就下载吧!
下载资源
资源描述

C++内存管理详解.docx

《C++内存管理详解.docx》由会员分享,可在线阅读,更多相关《C++内存管理详解.docx(29页珍藏版)》请在冰点文库上搜索。

C++内存管理详解.docx

C++内存管理详解

1.对应的new和delete要采用相同的形式   下面的语句有什么错?

string*stringarray=newstring[100];

...

deletestringarray;

一切好象都井然有序——一个new对应着一个delete——然而却隐藏着很大的错误:

程序的运行情况将是不可猜测的。

至少,stringarray指向的100个string对象中的99个不会被正确地摧毁,因为他们的析构函数永远不会被调用。

用new的时候会发生两件事。

首先,内存被分配(通过operatornew函数,详见条款7-10和条款m8),然后,为被分配的内存调用一个或多个构造函数。

用delete的时候,也有两件事发生:

首先,为将被释放的内存调用一个或多个析构函数,然后,释放内存(通过operatordelete函数,详见条款8和m8)。

对于delete来说会有这样一个重要的问题:

内存中有多少个对象要被删除?

答案决定了将有多少个析构函数会被调用。

这个问题简单来说就是:

要被删除的指针指向的是单个对象呢,还是对象数组?

这只有你来告诉delete。

假如你在用delete时没用括号,delete就会认为指向的是单个对象,否则,它就会认为指向的是一个数组:

string*stringptr1=newstring;

string*stringptr2=newstring[100];

...

deletestringptr1;//删除一个对象

delete[]stringptr2;//删除对象数组

假如你在stringptr1前加了"[]"会怎样呢?

答案是:

那将是不可猜测的;假如你没在stringptr2前没加上"[]"又会怎样呢?

答案也是:

不可猜测。

而且对于象int这样的固定类型来说,结果也是不可猜测的,即使这样的类型没有析构函数。

所以,解决这类问题的规则很简单:

假如你调用new时用了[],调用delete时也要用[]。

假如调用new时没有用[],那调用delete时也不要用[]。

在写一个包含指针数据成员,并且提供多个构造函数的类时,牢记这一规则尤其重要。

因为这样的话,你就必须在所有初始化指针成员的构造函数里采用相同的new的形式。

否则,析构函数里将采用什么形式的delete呢?

关于这一话题的进一步阐述,参见条款11。

这个规则对喜欢用typedef的人来说也很重要,因为写typedef的程序员必须告诉别人,用new创建了一个typedef定义的类型的对象后,该用什么形式的delete来删除。

举例如下:

typedefstringaddresslines[4];//一个人的地址,共4行,每行一个string

//因为addresslines是个数组,使用new:

string*pal=newaddresslines;//注重"newaddresslines"返回string*,和

//"newstring[4]"返回的一样

delete时必须以数组形式与之对应:

deletepal;//错误!

delete[]pal;//正确

为了避免混乱,最好杜绝对数组类型用typedefs。

这其实很轻易,因为标准c++库(见条款49)包含有stirng和vector模板,使用他们将会使对数组的需求减少到几乎零。

举例来说,addresslines可以定义为一个字符串(string)的向量(vector),即addresslines可定义为vector类型。

2.析构函数里对指针成员调用delete

大多数情况下,执行动态内存分配的的类都在构造函数里用new分配内存,然后在析构函数里用delete释放内存。

最初写这个类的时候当然不难做,你会记得最后对在所有构造函数里分配了内存的所有成员使用delete。

然而,这个类经过维护、升级后,情况就会变得困难了,因为对类的代码进行修改的程序员不一定就是最早写这个类的人。

而增加一个指针成员意味着几乎都要进行下面的工作:

·在每个构造函数里对指针进行初始化。

对于一些构造函数,假如没有内存要分配给指针的话,指针要被初始化为0(即空指针)。

·删除现有的内存,通过赋值操作符分配给指针新的内存。

·在析构函数里删除指针。

假如在构造函数里忘了初始化某个指针,或者在赋值操作的过程中忘了处理它,问题会出现得很快,很明显,所以在实践中这两个问题不会那么折磨你。

但是,假如在析构函数里没有删除指针,它不会表现出很明显的外部症状。

相反,它可能只是表现为一点微小的内存泄露,并且不断增长,最后吞噬了你的地址空间,导致程序夭折。

因为这种情况经常不那么引人注重,所以每增加一个指针成员到类里时一定要记清楚。

另外,删除空指针是安全的(因为它什么也没做)。

所以,在写构造函数,赋值操作符,或其他成员函数时,类的每个指针成员要么指向有效的内存,要么就指向空,那在你的析构函数里你就可以只用简单地delete掉他们,而不用担心他们是不是被new过。

当然对本条款的使用也不要绝对。

例如,你当然不会用delete去删除一个没有用new来初始化的指针,而且,就象用智能指针对象时不用劳你去删除一样,你也永远不会去删除一个传递给你的指针。

换句话说,除非类成员最初用了new,否则是不用在析构函数里用delete的。

说到智能指针,这里介绍一种避免必须删除指针成员的方法,即把这些成员用智能指针对象来代替,比如c++标准库里的auto_ptr。

想知道它是如何工作的,看看条款m9和m10。

3.预先预备好内存不够的情况         

  operatornew在无法完成内存分配请求时会抛出异常(以前的做法一般是返回0,一些旧一点的编译器还这么做。

你愿意的话也可以把你的编译器设置成这样。

关于这个话题我将推迟到本条款的结尾处讨论)。

大家都知道,处理内存不够所产生的异常真可以算得上是个道德上的行为,但实际做起来又会象刀架在脖子上那样痛苦。

所以,你有时会不去管它,也许一直没去管它。

但你心里一定还是深深地隐藏着一种罪恶感:

万一new真的产生了异常怎么办?

你会很自然地想到处理这种情况的一种方法,即回到以前的老路上去,使用预处理。

例如,c的一种常用的做法是,定义一个类型无关的宏来分配内存并检查分配是否成功。

对于c++来说,这个宏看起来可能象这样:

#definenew(ptr,type)

try{(ptr)=newtype;}

catch(std:

:

bad_alloc&){assert(0);}

(“慢!

std:

:

bad_alloc是做什么的?

”你会问。

bad_alloc是operatornew不能满足内存分配请求时抛出的异常类型,std是bad_alloc所在的名字空间(见条款28)的名称。

“好!

”你会继续问,“assert又有什么用?

”假如你看看标准c头文件(或与它相等价的用到了名字空间的版本,见条款49),就会发现assert是个宏。

这个宏检查传给它的表达式是否非零,假如不是非零值,就会发出一条出错信息并调用abort。

assert只是在没定义标准宏ndebug的时候,即在调试状态下才这么做。

在产品发布状态下,即定义了ndebug的时候,assert什么也不做,相当于一条空语句。

所以你只能在调试时才能检查断言(assertion))。

new宏不但有着上面所说的通病,即用assert去检查可能发生在已发布程序里的状态(然而任何时候都可能发生内存不够的情况),同时,它还在c++里有另外一个缺陷:

它没有考虑到new有各种各样的使用方式。

例如,想创建类型t对象,一般有三种常见的语法形式,你必须对每种形式可能产生的异常都要进行处理:

newt;

newt(constrUCtorarguments);

newt[size];

这里对问题大大进行了简化,因为有人还会自定义(重载)operatornew,所以程序里会包含任意个使用new的语法形式。

那么,怎么办?

假如想用一个很简单的出错处理方法,可以这么做:

当内存分配请求不能满足时,调用你预先指定的一个出错处理函数。

这个方法基于一个常规,即当operatornew不能满足请求时,会在抛出异常之前调用客户指定的一个出错处理函数——一般称为new-handler函数。

(operatornew实际工作起来要复杂一些,详见条款8)

指定出错处理函数时要用到set_new_handler函数,它在头文件里大致是象下面这样定义的:

typedefvoid(*new_handler)();

new_handlerset_new_handler(new_handlerp)throw();

可以看到,new_handler是一个自定义的函数指针类型,它指向一个没有输入参数也没有返回值的函数。

set_new_handler则是一个输入并返回new_handler类型的函数。

set_new_handler的输入参数是operatornew分配内存失败时要调用的出错处理函数的指针,返回值是set_new_handler没调用之前就已经在起作用的旧的出错处理函数的指针。

可以象下面这样使用set_new_handler:

//functiontocallifoperatornewcan'tallocateenoughmemory

voidnomorememory()

{

cerr<<"unabletosatisfyrequestformemory

";

abort();

}

intmain()

{

set_new_handler(nomorememory);

int*pbigdataarray=newint[100000000];

...

}

假如operatornew不能为100,000,000个整数分配空间,nomorememory将会被调用,程序发出一条出错信息后终止。

这就比简单地让系统内核产生错误信息来结束程序要好。

(顺便考虑一下,假如cerr在写错误信息的过程中要动态分配内存,那将会发生什么...)

operatornew不能满足内存分配请求时,new-handler函数不只调用一次,而是不断重复,直至找到足够的内存。

实现重复调用的代码在条款8里可以看到,这里我用描述性的的语言来说明:

一个设计得好的new-handler函数必须实现下面功能中的一种。

·产生更多的可用内存。

这将使operatornew下一次分配内存的尝试有可能获得成功。

实施这一策略的一个方法是:

在程序启动时分配一个大的内存块,然后在第一次调用new-handler时释放。

释放时伴随着一些对用户的警告信息,如内存数量太少,下次请求可能会失败,除非又有更多的可用空间。

·安装另一个不同的new-handler函数。

假如当前的new-handler函数不能产生更多的可用内存,可能它会知道另一个new-handler函数可以提供更多的资源。

这样的话,当前的new-handler可以安装另一个new-handler来取代它(通过调用set_new_handler)。

下一次operatornew调用new-handler时,会使用最近安装的那个。

(这一策略的另一个变通办法是让new-handler可以改变它自己的运行行为,那么下次调用时,它将做不同的事。

方法是使new-handler可以修改那些影响它自身行为的静态或全局数据。

·卸除new-handler。

也就是传递空指针给set_new_handler。

没有安装new-handler,operatornew分配内存不成功时就会抛出一个标准的std:

:

bad_alloc类型的异常。

·抛出std:

:

bad_alloc或从std:

:

bad_alloc继续的其他类型的异常。

这样的异常不会被operatornew捕捉,所以它们会被送到最初进行内存请求的地方。

(抛出别的不同类型的异常会违反operatornew异常规范。

规范中的缺省行为是调用abort,所以new-handler要抛出一个异常时,一定要确信它是从std:

:

bad_alloc继续来的。

想更多地了解异常规范,参见条款m14。

·没有返回。

典型做法是调用abort或exit。

abort/exit可以在标准c库中找到(还有标准c++库,参见条款49)。

上面的选择给了你实现new-handler函数极大的灵活性。

处理内存分配失败的情况时采取什么方法,取决于要分配的对象的类:

classx{

public:

staticvoid

outofmemory();

...

};

classy{

public:

staticvoidoutofmemory();

...

};

x*p1=newx;//若分配成功,调用x:

:

outofmemory

y*p2=newy;//若分配不成功,调用y:

:

outofmemory

c++不支持专门针对于类的new-handler函数,而且也不需要。

你可以自己来实现它,只要在每个类中提供自己版本的set_new_handler和operatornew。

类的set_new_handler可以为类指定new-handler(就象标准的set_new_handler指定全局new-handler一样)。

类的operatornew则保证为类的对象分配内存时用类的new-handler取代全局new-handler。

假设处理类x内存分配失败的情况。

因为operatornew对类型x的对象分配内存失败时,每次都必须调用出错处理函数,所以要在类里声明一个new_handler类型的静态成员。

那么类x看起来会象这样:

classx{

public:

staticnew_handlerset_new_handler(new_handlerp);

staticvoid*operatornew(size_tsize);

private:

staticnew_handlercurrenthandler;

};

类的静态成员必须在类外定义。

因为想借用静态对象的缺省初始化值0,所以定义x:

:

currenthandler时没有去初始化。

new_handlerx:

:

currenthandler;//缺省设置currenthandler为0(即null)

类x中的set_new_handler函数会保存传给它的任何指针,并返回在调用它之前所保存的任何指针。

这正是标准版本的set_new_handler所做的:

new_handlerx:

:

set_new_handler(new_handlerp)

{

new_handleroldhandler=currenthandler;

currenthandler=p;

returnoldhandler;

}

最后看看x的operatornew所做的:

1.调用标准set_new_handler函数,输入参数为x的出错处理函数。

这使得x的new-handler函数成为全局new-handler函数。

注重下面的代码中,用了":

:

"符号显式地引用std空间(标准set_new_handler函数就存在于std空间)。

2.调用全局operatornew分配内存。

假如第一次分配失败,全局operatornew会调用x的new-handler,因为它刚刚(见1.)被安装成为全局new-handler。

假如全局operatornew最终未能分配到内存,它抛出std:

:

bad_alloc异常,x的operatornew会捕捉到它。

x的operatornew然后恢复最初被取代的全局new-handler函数,最后以抛出异常返回。

3.假设全局operatornew为类型x的对象分配内存成功,,x的operatornew会再次调用标准set_new_handler来恢复最初的全局出错处理函数。

最后返回分配成功的内存的指针。

c++是这么做的:

void*x:

:

operatornew(size_tsize)

{

new_handlerglobalhandler=//安装x的new_handler

std:

:

set_new_handler(currenthandler);

void*memory;

try{//尝试分配内存

memory=:

:

operatornew(size);

}

catch(std:

:

bad_alloc&){//恢复旧的new_handler

std:

:

set_new_handler(globalhandler);

throw;//抛出异常

}

std:

:

set_new_handler(globalhandler);//恢复旧的new_handler

returnmemory;

}

假如你对上面重复调用std:

:

set_new_handler看不顺眼,可以参见条款m9来除去它们。

使用类x的内存分配处理功能时大致如下:

voidnomorememory();//x的对象分配内存失败时调用的new_handler函数的声明

x:

:

set_new_handler(nomorememory);

//把nomorememory设置为x的

//new-handling函数

x*px1=newx;

//如内存分配失败,

//调用nomorememory

string*ps=newstring;

//如内存分配失败,调用全局new-handling函数

x:

:

set_new_handler(0);

//设x的new-handling函数为空

x*px2=newx;

//如内存分配失败,立即抛出异常

//(类x没有new-handling函数)

你会注重到,处理以上类似情况,假如不考虑类的话,实现代码是一样的,这就很自然地想到在别的地方也能重用它们。

正如条款41所说明的,继续和模板可以用来设计可重用代码。

在这里,我们把两种方法结合起来使用,从而满足了你的要求。

你只要创建一个“混合风格”(mixin-style)的基类,这种基类答应子类继续它某一特定的功能——这里指的是建立一个类的new-handler的功能。

之所以设计一个基类,是为了让所有的子类可以继续set_new_handler和operatornew功能,而设计模板是为了使每个子类有不同的currenthandler数据成员。

这听起来很复杂,不过你会看到代码其实很熟悉。

区别只不过是它现在可以被任何类重用了。

template//提供类set_new_handler支持的

classnewhandlersupport{//混合风格”的基类

public:

staticnew_handlerset_new_handler(new_handlerp);

staticvoid*operatornew(size_tsize);

private:

staticnew_handlercurrenthandler;

};

template

new_handlernewhandlersupport:

:

set_new_handler(new_handlerp)

{

new_handleroldhandler=currenthandler;

currenthandler=p;

returnoldhandler;

}

template

void*newhandlersupport:

:

operatornew(size_tsize)

{

new_handlerglobalhandler=

std:

:

set_new_handler(currenthandler);

void*memory;

try{

memory=:

:

operatornew(size);

}

catch(std:

:

bad_alloc&){

std:

:

set_new_handler(globalhandler);

throw;

}

std:

:

set_new_handler(globalhandler);

returnmemory;

}

//thissetseachcurrenthandlerto0

template

new_handlernewhandlersupport:

:

currenthandler;

有了这个模板类,对类x加上set_new_handler功能就很简单了:

只要让x从newhandlersupport继续:

//noteinheritancefrommixinbaseclasstemplate.(see

//myarticleoncountingobjectsforinformationonwhy

//privateinheritancemightbepreferablehere.)

classx:

publicnewhandlersupport{

...//asbefore,butnodeclarationsfor

};//set_new_handleroroperatornew

使用x的时候依然不用理会它幕后在做些什么;老代码依然工作。

这很好!

那些你常不去理会的东西往往是最可信赖的。

使用set_new_handler是处理内存不够情况下一种方便,简单的方法。

这比把每个new都包装在try模块里当然好多了。

而且,newhandlersupport这样的模板使得向任何类增加一个特定的new-handler变得更简单。

“混合风格”的继续不可避免地将话题引入到多继续上去,在转到这个话题前,你一定要先阅读条款43。

1993年前,c++一直要求在内存分配失败时operatornew要返回0,现在则是要求operatornew抛出std:

:

bad_alloc异常。

很多c++程序是在编译器开始支持新规范前写的。

c++标准委员会不想放弃那些已有的遵循返回0规范的代码,所以他们提供了另外形式的operatornew(以及operatornew[]——见条款8)以继续提供返回0功能。

这些形式被称为“无抛出”,因为他们没用过一个throw,而是在使用new的入口点采用了nothrow对象:

classwidget{...};

widget*pw1=newwidget;//分配失败抛出std:

:

bad_allocif

if(pw1==0)...//这个检查一定失败

widget*pw2=new(nothrow)widget;//若分配失败返回0

if(pw2==0)...//这个检查可能会成功

不管是用“正规”(即抛出异常)形式的new还是“无抛出”形式的new,重要的是你必须为内存分配失败做好预备。

最简单的方法是使用set_new_handler,因为它对两种形式都有用。

4.写operatornew和operatordelete时要遵循常规

自己重写operatornew时(条款10解释了为什么有时要重写它),很重要的一点是函数提供的行为要和系统缺省的operatornew一致。

实际做起来也就是:

要有正确的返回值;可用内存不够时要调用

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > IT计算机 > 电脑基础知识

copyright@ 2008-2023 冰点文库 网站版权所有

经营许可证编号:鄂ICP备19020893号-2