找回密码
 用户注册

QQ登录

只需一步,快速开始

查看: 5594|回复: 6

求解,内存分配在什么情况下会失败

[复制链接]
发表于 2008-9-20 16:01:23 | 显示全部楼层 |阅读模式
这是和sim在交流的时候冒出来的问题。其实很早以前也想过这个事情,只是的确没碰见过,查了一些材料,没有找到合适的答案,求教各位有研究的朋友,用C++的new操作符,分配内存,什么时候会失败?期望能详细解答一下,提供材料也成。
 楼主| 发表于 2008-9-20 16:03:37 | 显示全部楼层
Effective C++ 中文第二版 50个有效改善程序设计效率的办法
<-- postdatatop --><-- google_ad_section_end --><-- google_ad_section_start -->
条款7:预先准备好内存不够的情况

operator new在无法完成内存分配请求时会抛出异常(以前的做法一般是返回0,一些旧一点的编译器还这么做。你愿意的话也可以把你的编译器设置成这样。关于这个话题我将推迟到本条款的结尾处讨论)。大家都知道,处理内存不够所产生的异常真可以算得上是个道德上的行为,但实际做起来又会象刀架在脖子上那样痛苦。所以,你有时会不去管它,也许一直没去管它。但你心里一定还是深深地隐藏着一种罪恶感:万一new真的产生了异常怎么办?

你会很自然地想到处理这种情况的一种方法,即回到以前的老路上去,使用预处理。例如,c的一种常用的做法是,定义一个类型无关的宏来分配内存并检查分配是否成功。对于c++来说,这个宏看起来可能象这样:

#define new(ptr, type) \
try { (ptr) = new type; } \
catch (std::bad_alloc&) { assert(0); }

(“慢!std::bad_alloc是做什么的?”你会问。bad_alloc是operator new不能满足内存分配请求时抛出的异常类型,std是bad_alloc所在的名字空间(见条款28)的名称。“好!”你会继续问,“assert又有什么用?”如果你看看标准c头文件(或与它相等价的用到了名字空间的版本,见条款49),就会发现assert是个宏。这个宏检查传给它的表达式是否非零,如果不是非零值,就会发出一条出错信息并调用abort。assert只是在没定义标准宏ndebug的时候,即在调试状态下才这么做。在产品发布状态下,即定义了ndebug的时候,assert什么也不做,相当于一条空语句。所以你只能在调试时才能检查断言(assertion))。

new宏不但有着上面所说的通病,即用assert去检查可能发生在已发布程序里的状态(然而任何时候都可能发生内存不够的情况),同时,它还在c++里有另外一个缺陷:它没有考虑到new有各种各样的使用方式。例如,想创建类型t对象,一般有三种常见的语法形式,你必须对每种形式可能产生的异常都要进行处理:

new t;
new t(constructor arguments);
new t[size];

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

那么,怎么办?如果想用一个很简单的出错处理方法,可以这么做:当内存分配请求不能满足时,调用你预先指定的一个出错处理函数。这个方法基于一个常规,即当operator new不能满足请求时,会在抛出异常之前调用客户指定的一个出错处理函数——一般称为new-handler函数。(operator new实际工作起来要复杂一些,详见条款8)

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

typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();

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

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

可以象下面这样使用set_new_handler:

// function to call if operator new can’t allocate enough memory
void nomorememory()
{
cerr << "unable to satisfy request for memory\n";
abort();
}

int main()
{
set_new_handler(nomorememory);
int *pbigdataarray = new int[100000000];

...

}

假如operator new不能为100,000,000个整数分配空间,nomorememory将会被调用,程序发出一条出错信息后终止。这就比简单地让系统内核产生错误信息来结束程序要好。(顺便考虑一下,假如cerr在写错误信息的过程中要动态分配内存,那将会发生什么...)

operator new不能满足内存分配请求时,new-handler函数不只调用一次,而是不断重复,直至找到足够的内存。实现重复调用的代码在条款8里可以看到,这里我用描述性的的语言来说明:一个设计得好的new-handler函数必须实现下面功能中的一种。
·产生更多的可用内存。这将使operator new下一次分配内存的尝试有可能获得成功。实施这一策略的一个方法是:在程序启动时分配一个大的内存块,然后在第一次调用new-handler时释放。释放时伴随着一些对用户的警告信息,如内存数量太少,下次请求可能会失败,除非又有更多的可用空间。
·安装另一个不同的new-handler函数。如果当前的new-handler函数不能产生更多的可用内存,可能它会知道另一个new-handler函数可以提供更多的资源。这样的话,当前的new-handler可以安装另一个new-handler来取代它(通过调用set_new_handler)。下一次operator new调用new-handler时,会使用最近安装的那个。(这一策略的另一个变通办法是让new-handler可以改变它自己的运行行为,那么下次调用时,它将做不同的事。方法是使new-handler可以修改那些影响它自身行为的静态或全局数据。)
·卸除new-handler。也就是传递空指针给set_new_handler。没有安装new-handler,operator new分配内存不成功时就会抛出一个标准的std::bad_alloc类型的异常。
·抛出std::bad_alloc或从std::bad_alloc继承的其他类型的异常。这样的异常不会被operator new捕捉,所以它们会被送到最初进行内存请求的地方。(抛出别的不同类型的异常会违反operator new异常规范。规范中的缺省行为是调用abort,所以new-handler要抛出一个异常时,一定要确信它是从std::bad_alloc继承来的。想更多地了解异常规范,参见条款m14。)
·没有返回。典型做法是调用abort或exit。abort/exit可以在标准c库中找到(还有标准c++库,参见条款49)。

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

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

class x {
public:
static void
outofmemory();

...

};

class y {
public:
static void outofmemory();

...

};

x* p1 = new x; // 若分配成功,调用x::outofmemory
y* p2 = new y; // 若分配不成功,调用y::outofmemory

c++不支持专门针对于类的new-handler函数,而且也不需要。你可以自己来实现它,只要在每个类中提供自己版本的set_new_handler和operator new。类的set_new_handler可以为类指定new-handler(就象标准的set_new_handler指定全局new-handler一样)。类的operator new则保证为类的对象分配内存时用类的new-handler取代全局new-handler。

假设处理类x内存分配失败的情况。因为operator new对类型x的对象分配内存失败时,每次都必须调用出错处理函数,所以要在类里声明一个new_handler类型的静态成员。那么类x看起来会象这样:

class x {
public:
static new_handler set_new_handler(new_handler p);
static void * operator new(size_t size);

private:
static new_handler currenthandler;
};

类的静态成员必须在类外定义。因为想借用静态对象的缺省初始化值0,所以定义x::currenthandler时没有去初始化。

new_handler x::currenthandler; //缺省设置currenthandler为0(即null)
类x中的set_new_handler函数会保存传给它的任何指针,并返回在调用它之前所保存的任何指针。这正是标准版本的set_new_handler所做的:

new_handler x::set_new_handler(new_handler p)
{
new_handler oldhandler = currenthandler;
currenthandler = p;
return oldhandler;
}

最后看看x的operator new所做的:
1. 调用标准set_new_handler函数,输入参数为x的出错处理函数。这使得x的new-handler函数成为全局new-handler函数。注意下面的代码中,用了"::"符号显式地引用std空间(标准set_new_handler函数就存在于std空间)。

2. 调用全局operator new分配内存。如果第一次分配失败,全局operator new会调用x的new-handler,因为它刚刚(见1.)被安装成为全局new-handler。如果全局operator new最终未能分配到内存,它抛出std::bad_alloc异常,x的operator new会捕捉到它。x的operator new然后恢复最初被取代的全局new-handler函数,最后以抛出异常返回。

3. 假设全局operator new为类型x的对象分配内存成功,, x的operator new会再次调用标准set_new_handler来恢复最初的全局出错处理函数。最后返回分配成功的内存的指针。
c++是这么做的:

void * x::operator new(size_t size)
{
new_handler globalhandler = // 安装x的new_handler
std::set_new_handler(currenthandler);

void *memory;

try { // 尝试分配内存
memory = ::operator new(size);
}

catch (std::bad_alloc&) { // 恢复旧的new_handler
std::set_new_handler(globalhandler);
throw; // 抛出异常
}
std::set_new_handler(globalhandler); // 恢复旧的new_handler
return memory;
}

如果你对上面重复调用std::set_new_handler看不顺眼,可以参见条款m9来除去它们。

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

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

x::set_new_handler(nomorememory);
// 把nomorememory设置为x的
// new-handling函数
x *px1 = new x;
// 如内存分配失败,
// 调用nomorememory
string *ps = new string;
// 如内存分配失败,调用全局new-handling函数

x::set_new_handler(0);
// 设x的new-handling函数为空

x *px2 = new x;
// 如内存分配失败,立即抛出异常
// (类x没有new-handling函数)

你会注意到,处理以上类似情况,如果不考虑类的话,实现代码是一样的,这就很自然地想到在别的地方也能重用它们。正如条款41所说明的,继承和模板可以用来设计可重用代码。在这里,我们把两种方法结合起来使用,从而满足了你的要求。

你只要创建一个“混合风格”(mixin-style)的基类,这种基类允许子类继承它某一特定的功能——这里指的是建立一个类的new-handler的功能。之所以设计一个基类,是为了让所有的子类可以继承set_new_handler和operator new功能,而设计模板是为了使每个子类有不同的currenthandler数据成员。这听起来很复杂,不过你会看到代码其实很熟悉。区别只不过是它现在可以被任何类重用了。

template // 提供类set_new_handler支持的
class newhandlersupport { // 混合风格”的基类
public:
static new_handler set_new_handler(new_handler p);
static void * operator new(size_t size);

private:
static new_handler currenthandler;
};

template
new_handler newhandlersupport::set_new_handler(new_handler p)
{
new_handler oldhandler = currenthandler;
currenthandler = p;
return oldhandler;
}

template
void * newhandlersupport::operator new(size_t size)
{
new_handler globalhandler =
std::set_new_handler(currenthandler);
void *memory;
try {
memory = ::operator new(size);
}
catch (std::bad_alloc&) {
std::set_new_handler(globalhandler);
throw;
}

std::set_new_handler(globalhandler);
return memory;
}
// this sets each currenthandler to 0

template
new_handler newhandlersupport::currenthandler;
有了这个模板类,对类x加上set_new_handler功能就很简单了:只要让x从newhandlersupport继承:
// note inheritance from mixin base class template. (see
// my article on counting objects for information on why
// private inheritance might be preferable here.)
class x: public newhandlersupport {

… // as before, but no declarations for
}; // set_new_handler or operator new

使用x的时候依然不用理会它幕后在做些什么;老代码依然工作。这很好!那些你常不去理会的东西往往是最可信赖的。
 楼主| 发表于 2008-9-20 16:04:22 | 显示全部楼层
使用set_new_handler是处理内存不够情况下一种方便,简单的方法。这比把每个new都包装在try模块里当然好多了。而且,newhandlersupport这样的模板使得向任何类增加一个特定的new-handler变得更简单。“混合风格”的继承不可避免地将话题引入到多继承上去,在转到这个话题前,你一定要先阅读条款43。

1993年前,c++一直要求在内存分配失败时operator new要返回0,现在则是要求operator new抛出std::bad_alloc异常。很多c++程序是在编译器开始支持新规范前写的。c++标准委员会不想放弃那些已有的遵循返回0规范的代码,所以他们提供了另外形式的operator new(以及operator new[]——见条款8)以继续提供返回0功能。这些形式被称为“无抛出”,因为他们没用过一个throw,而是在使用new的入口点采用了nothrow对象:

class widget { … };

widget *pw1 = new widget;// 分配失败抛出std::bad_alloc if

if (pw1 == 0) … // 这个检查一定失败
widget *pw2 = new (nothrow) widget; // 若分配失败返回0

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

不管是用“正规”(即抛出异常)形式的new还是“无抛出”形式的new,重要的是你必须为内存分配失败做好准备。最简单的方法是使用set_new_handler,因为它对两种形式都有用。
条款8: 写operator new和operator delete时要遵循常规

自己重写operator new时(条款10解释了为什么有时要重写它),很重要的一点是函数提供的行为要和系统缺省的operator new一致。实际做起来也就是:要有正确的返回值;可用内存不够时要调用出错处理函数(见条款7);处理好0字节内存请求的情况。此外,还要避免不小心隐藏了标准形式的new,不过这是条款9的话题。

有关返回值的部分很简单。如果内存分配请求成功,就返回指向内存的指针;如果失败,则遵循条款7的规定抛出一个std::bad_alloc类型的异常。

但事情也不是那么简单。因为operator new实际上会不只一次地尝试着去分配内存,它要在每次失败后调用出错处理函数,还期望出错处理函数能想办法释放别处的内存。只有在指向出错处理函数的指针为空的情况下,operator new才抛出异常。

另外,c++标准要求,即使在请求分配0字节内存时,operator new也要返回一个合法指针。(实际上,这个听起来怪怪的要求确实给c++语言其它地方带来了简便)

这样,非类成员形式的operator new的伪代码看起来会象下面这样:
void * operator new(size_t size) // operator new还可能有其它参数
{

if (size == 0) { // 处理0字节请求时,
size = 1; // 把它当作1个字节请求来处理
}
while (1) {
分配size字节内存;

if (分配成功)
return (指向内存的指针);

// 分配不成功,找出当前出错处理函数
new_handler globalhandler = set_new_handler(0);
set_new_handler(globalhandler);

if (globalhandler) (*globalhandler)();
else throw std::bad_alloc();
}
}

处理零字节请求的技巧在于把它作为请求一个字节来处理。这看起来也很怪,但简单,合法,有效。而且,你又会多久遇到一次零字节请求的情况呢?

你又会奇怪上面的伪代码中为什么把出错处理函数置为0后又立即恢复。这是因为没有办法可以直接得到出错处理函数的指针,所以必须通过调用set_new_handler来找到。办法很笨但也有效。

条款7提到operator new内部包含一个无限循环,上面的代码清楚地说明了这一点——while (1)将导致无限循环。跳出循环的唯一办法是内存分配成功或出错处理函数完成了条款7所描述的事件中的一种:得到了更多的可用内存;安装了一个新的new-handler(出错处理函数);卸除了new-handler;抛出了一个std::bad_alloc或其派生类型的异常;或者返回失败。现在明白了为什么new-handler必须做这些工作中的一件。如果不做,operator new里面的循环就不会结束。

很多人没有认识到的一点是operator new经常会被子类继承。这会导致某些复杂性。上面的伪代码中,函数会去分配size字节的内存(除非size为0)。size很重要,因为它是传递给函数的参数。但是大多数针对类所写的operator new(包括条款10中的那种)都是只为特定的类设计的,不是为所有的类,也不是为它所有的子类设计的。这意味着,对于一个类x的operator new来说,函数内部的行为在涉及到对象的大小时,都是精确的sizeof(x):不会大也不会小。但由于存在继承,基类中的operator new可能会被调用去为一个子类对象分配内存:
class base {
public:
static void * operator new(size_t size);

};

class derived: public base // derived类没有声明operator new
{ … }; //

derived *p = new derived; // 调用base::operator new

如果base类的operator new不想费功夫专门去处理这种情况——这种情况出现的可能性不大——那最简单的办法是把这个“错误”数量的内存分配请求转给标准operator new来处理,象下面这样:
void * base::operator new(size_t size)
{
if (size != sizeof(base)) // 如果数量“错误”,让标准operator new
return ::operator new(size); // 去处理这个请求
//

… // 否则处理这个请求
}

“停!”我听见你在叫,“你忘了检查一种虽然不合理但是有可能出现的一种情况——size有可能为零!”是的,我没检查,但拜托下次再叫出声的时候不要这么文绉绉的。:)但实际上检查还是做了,只不过融合到size != sizeof(base)语句中了。c++标准很怪异,其中之一就是规定所以独立的(freestanding)类的大小都是非零值。所以sizeof(base)永远不可能是零(即使base类没有成员),如果size为零,请求会转到::operator new,由它来以一种合理的方式对请求进行处理。(有趣的是,如果base不是独立的类,sizeof(base)有可能是零,详细说明参见”my article on counting objects”)。

如果想控制基于类的数组的内存分配,必须实现operator new的数组形式——operator new[](这个函数常被称为“数组new”,因为想不出”operator new[]”)该怎么发音)。写operator new[]时,要记住你面对的是“原始”内存,不能对数组里还不存在的对象进行任何操作。实际上,你甚至还不知道数组里有多少个对象,因为不知道每个对象有多大。基类的operator new[]会通过继承的方式被用来为子类对象的数组分配内存,而子类对象往往比基类要大。所以,不能想当然认为base::operator new[]里的每个对象的大小都是sizeof(base),也就是说,数组里对象的数量不一定就是(请求字节数)/sizeof(base)。关于operator new[]的详细介绍参见条款m8。

重写operator new(和operator new[])时所有要遵循的常规就这些。对于operator delete(以及它的伙伴operator delete[]),情况更简单。所要记住的只是,c++保证删除空指针永远是安全的,所以你要充分地应用这一保证。下面是非类成员形式的operator delete的伪代码:
void operator delete(void *rawmemory)
{
if (rawmemory == 0) return; file://如/果指针为空,返回
//

释放rawmemory指向的内存;

return;
}

这个函数的类成员版本也简单,只是还必须检查被删除的对象的大小。假设类的operator new将“错误”大小的分配请求转给::operator new,那么也必须将“错误”大小的删除请求转给::operator delete:

class base { // 和前面一样,只是这里声明了
public: // operator delete
static void * operator new(size_t size);
static void operator delete(void *rawmemory, size_t size);

};

void base::operator delete(void *rawmemory, size_t size)
{
if (rawmemory == 0) return; // 检查空指针

if (size != sizeof(base)) { // 如果size”错误”,
::operator delete(rawmemory); // 让标准operator来处理请求
return;
}

释放指向rawmemory的内存;

return;
}

可见,有关operator new和operator delete(以及他们的数组形式)的规定不是那么麻烦,重要的是必须遵守它。只要内存分配程序支持new-handler函数并正确地处理了零内存请求,就差不多了;如果内存释放程序又处理了空指针,那就没其他什么要做的了。至于在类成员版本的函数里增加继承支持,那将很快就可以完成。

<-- content --><-- google_ad_section_end --><-- google_ad_section_start(weight=ignore) -->
 楼主| 发表于 2008-9-20 16:04:38 | 显示全部楼层
翻阅了一下资料:

new失败的有两种处理行为:

1.非MFC时,返回NULL(vc6及以前)。MFC时,抛出CMemoryException异常

2.抛出异常(vc7,7.1...):非mfc时,抛出std::bad_alloc异常,mfc时,抛出CMemoryException异常

C++标准行为是出异常

如果想不抛出异常,使用 char *p = new(std::nothrow) char[xxx];这样的方法。

如上所述,再来分析一下ACE_NEW_RETURN这个宏,这个宏使用了new(ACE_nothrow),确定失败时返回的是NULL
 楼主| 发表于 2008-9-20 16:11:07 | 显示全部楼层
分配内存当然是在内存不够时就会失败啊。说到C++的new操作符的内存分配,大家应该比较清楚,是从一个叫作“堆(heap)”的东西里面给程序分配内存的。这个“堆”是一种内存资源,通常是由编译环境来负责管理的。操作系统当然是内存的最底层管理者(除非你是在写操作系统),但是为了性能上的原因,编译器通常会自带“堆管理器”,而不是简单地把应用程序的内存请求照单转发给操作系统(那样会导致程序反复陷入系统调用中,降低了程序的性能)。简单地说,编译器会一次过向操作系统申请一大块内存,准备好给应用程序用;当应用程序使用new操作符时,编译器的new操作符缺省版本就会从大块内存中给应用程序分配一点内存;如果编译器的“堆”中剩余的内存不够用了,“堆管理器”就会再尝试向操作系统申请更多的内存。

所以说,分配内存失败最大的可能性就是连操作系统都无法提供足够的内存了,另一个比较小的可能性是“堆管理器”由于某种原因不给分配内存,还有就是别忘了,new操作符是可以重载的,重载后的new操作符可以被有意设计为分配内存失败。

至于操作系统什么情况下没有足够内存可分配,这有几个可能。首先,再牛的机器(虚拟)内存也是有限的,你如果申请个1TB内存,可以想象有什么后果。其次,就算是申请一个不算太大的内存也有可能失败,现代的多用户操作系统允许管理员为不同用户、不同进程指定最大的可用内存数,如果你的用户级别不够,或者管理员有意为难你,你的程序就很可能会申请不到内存。最后,想想嵌入式系统吧,那里通常只有几十K到几十M的内存可用,而且很可能没有虚拟内存这一说,new操作失败就不奇怪了。
 楼主| 发表于 2008-9-20 16:11:28 | 显示全部楼层
如果你用的是能发生异常的NEW,直接在程序入口的地方try一下。
或者用set_new_handle来ExitProcess也可以。
 楼主| 发表于 2008-9-20 16:11:35 | 显示全部楼层
c++的语言特性对处理内存分配失败的支持不是很大,不可能把大量的new都包裹在try...catch里面,

这样会造成很大的性能损害。

这个时候应该尽可能的利用系统特性。

比如在windows系统下,当有大量的相同对象要产生时就应该重载operator new

在重载的new函数里面加入像这样的代码:

static HANDLE s_hHeap = HeapCreate(...);

return (HeapAlloc(s_hHeap));

对于更大规模的内存分配就应该考虑使用VirtualAlloc一类的函数。

使用这些方法,一般来说只需要对HeapCreate等的返回结果进行检查以及出错处理。

更多的细节可以参考Advanced Windows和Undocumented Windows NT.

说的不对,还请各位达人批评指正。  ^_^
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|手机版|小黑屋|ACE Developer ( 京ICP备06055248号 )

GMT+8, 2024-5-3 07:28 , Processed in 1.624050 second(s), 6 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表