C/C++动态内存创建与内存管理

1 内存空间逻辑组织

A 静态数据区:内存在程序启动的时候才被分配,而且可能直到程序开始执行的时候才被初始化,如函数中的静态变量就是在程序第一次执行到定义该变量的代码时才被初始化。所分配的内存在程序的整个运行期间都存在,如全局变量,static变量等。

注意:初始化的全局变量和静态变量在一块区域,未初始化的全局变量与静态变量在相邻的另一块区域,同时未被初始化的对象存储区可以通过void*来访问和操纵,程序结束后由系统自行释放。

B 代码区:存放函数体的二进制代码;

C 栈区:存放自动变量。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元由编译器自动释放,超出其作用域外的操作没有定义。栈内存分配运算内置于处理器的指令集中,效率很高,但分配的内存容量有限。栈存放函数的参数值,局部变量的值等。

D 堆区(自由存储区):在运行的时候调用程序(如C中的malloc或C++中的new)分配内存,可以在任何时候决定分配内存及分配的大小,用户自己负责在何时释放内存(如用free或delete)。堆中的所有东西都是匿名的,这样不能按名字访问,而只能通过指针访问。

堆需要一种策略来保存其内存是否已分配的信息。一种策略是建立一个可用块(自由存储区)的链表,每块由malloc分配的内存块都在自己的前面标明自己的大小,一般而言都经过边界对齐(alignment)处理,堆的大小受限于计算机系统中有效的虚拟内存。

堆的末端由一个称为break 的指针来标识,当堆管理器需要更多内存时,它可以通过系统调用brk和sbrk来移动break指针,一般情况下不必显式地调用brk,如果分配的内存容量很大,brk会被自动调用。用于管理内存的调用有:

Malloc和free--从堆中获得内存以及把内存返回给堆;

brk与sbrk――调整数据段的大小至一个绝对值(通过某个增量)。

注意:程序可能无法同时调用malloc()与brk(),因为如果使用了malloc,malloc希望当你调用brk与sbrk时,它具有唯一的控制权。由于sbrk向进程提供了唯一的方法将数据段内存返回给系统内核,所以如果使用了malloc,就有效地防止了程序的数据段缩小的可能性。

此处的堆与数据结构中的堆是两回事,它的分配方式类似于链表。

由于堆中的空间由用户负责分配及释放,因此需要注意内存泄漏的问题。

另外实际上堆区与自由存储区并不是一回事,详细信息见exceptional c++条款35。

E 文字常量区(常量数据区):存放常量字符串等在编译期间就能确定的值,在程序结束后由系统自动释放。类对象不能存在于这个区域中。在本区域中所有的数据都是只读的,任何企图修改本区域数据的行为都会造成无法预料的后果。

演示内存分布的示例如下:(示例需要更改完善,同时解决其中的bug)

int a = 0;  //全局初始化区

char *p1; //全局未初始化区

main()

{

int b; //栈

char s[] = "abc";  //栈

char *p2; //栈

char *p3 = "123456";  //123456在文字常量区,p3在栈上。(如何得到文字常量地址?)

static int c =0;  //全局(静态)数据区

p1 = (char *)malloc(10);

p2 = (char *)malloc(20);   //分配得来的10和20字节的区域就在堆区。(如何得到?)

strcpy(p1, "123456");   //123456放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。

}

 

2 C中内存分配

A malloc:原型为void*malloc(size_t size);在内存中的动态存储区中分配一个长度为size的空间,返回指向大小为size的内存区域首地址的void指针;用户必须决定对象的长度,即申请空间的大小。同时,malloc只是分配了一块返回值为void*的内存而不是生成一个对象。由于malloc返回值的类型是void*,所以在调用malloc时要显式地进行类型转换,将void* 转换成所需要的指针类型,malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数,因此使用sizeof是很好的方式,例如:int* p = (int*) malloc(sizeof(int) * length);使用malloc所分配的是一块连续的内存,同时由于编译器的实现问题(比如边界对齐等),其所分配的内存可能比所请求的多一点。

如果内存池为空,或者可用内存不能满足所请求的内存,则编译器向操作系统请求,要求得到更多的内存,并在所得到的内存上执行分配任务。若操作系统无法向malloc提供更多的内存,那么malloc返回一个NULL指针,因此有必要对malloc的返回指针进行检查。

B realloc:原型为voidrealloc(void* ptr, size_t sz);用于修改一个原先已经分配内存块的大小,可以扩大也可以缩小。若扩大时,则保留原先的内存,将新添加的内存放于原先内存块的后面,新内存并未以任何方式进行初始化;若缩小时,该内存块尾部的部分内存释放,其余部分保留。

注意:若realloc的第一个参数为NULL,那么其行为与malloc一样。另外如果原先的内存块无法改变大小,realloc将分配另一块正确大小的内存,并把原先那块内存的内容复制到新块上,因此在使用realloc之后就不能再使用指向原内存的指针,而应该改用realloc所返回的新指针。

C calloc:原型为void*calloc(size_t elem_num, size_t elem_sz);与malloc的主要区别在于calloc在返回指向内存的指针之前把它初始化为0,另一个区别是他们请求内存数量的方式不通,calloc的参数包括所需元素的数量和每个元素的字节数,由此它能够计算出总共需要分配的内存。

D free:原型为void free( void*p ); 由于指针p的类型以及它所指的内存的容量事先都是知道的,因此语句free(p)能正确地释放内存。如果p是NULL指针,那么free对p无论操作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致未定义的结果。

演示示例:(需要写出具有代表性的示例)

 3C++中动态内存创建

3.1 new operator与deleteoperator

C++中动态内存创建new operator分为两个步骤:在堆里为对象分配内存(C++中的operator new具有内置的长度计算,类型转换与安全检查);如果内存分配成功,则为该内存调用合适的构造函数进行初始化。

new operator实际上总以标准C中的malloc()完成,同时deleteoperator也是以标准C中的free()完成。[参见Inside the C++ Object Model]

delete operator也相应地分为两步:调用相应类的析构函数;释放内存。

注意:delete一个void指针,唯一发生的是释放了内存,因为没有类型信息也没有办法让编译器知道要调用的是哪个析构函数,而delete一个NULL指针,则是安全的,因为它什么都没有作。

Delete后指针并不会自动清除为0,指针所指对象的生命期因delete而结束,尽管delete指针后该地址上的对象不再合法,但地址本身却仍代表一个合法的程序空间,对它进行操作将没有定义,因此建议delete指针后将指针赋值为0,这样避免对它删除多次或误用。

演示示例:(由伪码可以清晰地看出两个步骤)

Point3d *origin = new Point3d;

delete origin;

其伪码可能为:

Point3d *origin;

if ( origin = __new( sizeof( Point3d)))   //分配空间

    origin = Point3d::Point3d( origin ); //调用构造函数初始化

if ( origin != 0 )

{

   Point3d::~Point3d( origin );  //调用析构函数

    __delete( origin );         //释放空间

}

3.2 operator new与operatordelete

3.2.1 new operator与operator new的区别

String* ps = new string(“string”);其中的new表示newoperator,由语言内建,不能改变其意义,总是作相同的事情:分配足够的内存,用于存放某类型的对象;调用构造函数,为刚分配的内存中的对象设定初值。返回指向特定类型的指针,完成了对象的创建。程序员不能改变其行为,只能改变用来容纳对象的那块内存的分配行为。New operator调用某个函数,执行必要的内存分配动作,可以重写或者重载那个函数,改变其行为,该函数就是operator new;

operator new的通常声明为void* operatornew(size_t size);它返回一个void指针,指向一块生鲜的,没有初值的内存。Operator new唯一的任务就是分配内存,取得operator new返回的内存并将之转换为一个对象,是new operator的责任,示例:

new operator:string *ps = newstring("Memory Management");

转化为operator new:

void *memory = operatornew(sizeof(string)); //得到未经处理的内存为String对象

  call string::string("Memory Management") on *memory; //初始化内存中的对象

  string *ps = static_cast<string*>(memory);   //是ps指针指向新的对象

3.2.2标准中支持的三种new形式

A plain new(简单new):即一般所用的new,他不接受任何的额外参数;plainnew抛出一个异常的类型std::bad_alloc。这个是标准适应性态。在早期C++的舞台上,这个性态和现在的非常不同――new将返回0来指出一个失败,和malloc()非常相似。

B nothrow new:可以接受额外参数;在一定的环境下,返回一个NULL指针来表示一个失败依然是一个不错的选择。C++标准委员会意识到这个问题,所以他们决定定义一个特别的new操作符版本nothrow new,返回0表示失败。一个nothrow new语句和普通的new语句相似,除了它的变量将涉及到std::nothrow_t。

C placement new:在内存的指定位置上构造一个对象,此举不会分配任何新空间,使用placement new相当于一次显式构造函数调用。注意在一个已存在的对象上调用构造函数是没有意义的,因为构造函数用来初始化对象,而一个对象仅仅能在给它初值时被初始化一次。Placement new用于在一些已被分配但是尚未处理的的raw内存中构造一个对象。

注意:使用了placement new时,需要显式调用析构函数,如t->T::~T();

三种new的原型声明如下:

Plain new: void* ::operator new(std::size_tsz) throw (std::bad_alloc);

Nothrow new: void* ::operatornew(std::size_t sz, const std::nothrow_t &nt) throw();

Placement new: void* ::operatornew(std::size_t sz, void* ptr) throw();

其用法的列表比较如下:

Operator new 额外的形参 是否进行内存分配 是否可能失败 抛出异常 是否可替换

Plain 无 是 是(抛出) Std::bad_alloc 是

Nothrow Std::nothrow 是 是(返回) 无 是

Placement Void* 否 否 无 否

 

Operator new一般不支持多态。

演示示例需要

3.3 重载operator new与operatordelete

重载operator new的问题:对于new operator,能够改变的只有operator new部分,即只能改变原有的内存分配方式,而不能改变为初始化该内存而调用构造函数部分。

重载operator new的方面:分配内存的方式;分配失败时所需作的事情:返回与抛出异常等。

3.3.1重载全局new/delete

重载全局new 后,使默认版本完全不能被访问,甚至在这个重新定义里也不能调用他们,因此在使用时需要慎重考虑。

重载的operator new必须含有一个size_t参数,表示要分配内存的对象的长度,同时必须返回一个指向等于这个长度的对象的指针。若没有找到符合要求的存储单元,构造函数将不被调用,另外需要一个表示失败的返回值以外还应该抛出异常(可以自定义)。

Operator delete参数为一个指向由operatornew分配内存的void*(已调用析构函数后得到的指针),其返回类型为void。

演示示例

3.3.2重载类中的new/delete

重载类中的new实际上创建的是一个static成员函数,该new只为创建该类对象起作用,不会影响默认的全局版本new,但是需要注意名字隐藏的问题。

注意:任何类中只要提供了自己的operator new或operator new[],那么就得同时提供对应的类相关版本的plain new,placement new以及nothrow new,否则根据名字隐藏的规则(将全局new遮掩掉了),将会发生没有可用匹配的错误。

一旦为类重载了operator new与operator delete,那么无论何时创建这个类的对象,都将调用这些重载的运算符,但若创建该类的一个对象数组时,全局operator new将立即被调用,用来为这个数组分配足够的内存,因此要避免出现这种情况,需要重载operator new的数组版本:operator new[]与operator delete[]。

演示示例:(item 36 in exceptional c++)

1.             //问题1:为什么B的delete有第二个参数而D没有?2.             class B 3.             {4.             public:5.               virtual ~B();6.               void operator delete ( void*,size_t ) throw();7.               voidoperator delete[]( void*, size_t ) throw();8.               void f( void*, size_t )throw();9.             };10.          class D : public B11.          {12.         public:13.            void operator delete ( void* )throw();14.            void operatordelete[]( void* ) throw();15.          };16.          void f()17.          {18.            //问题2:下面各个语句中,调用的是哪一个delete以及调用时的参数,为什么?19.       D* pd1 = new D; 20.       delete pd1;21.       B* pb1 = new D;22.       delete pb1;23.       D* pd2 = new D[10];24.       delete[] pd2;25.       B* pb2 = new D[10];26.       delete[] pb2;27.     28.      //问题3:下面两个赋值语句合法吗?29.       typedef void(B::*PMF)(void*, size_t); 30.       PMFp1 = &B::f;31.       PMF p2 =&B::operator delete;32.     }3.4 内存分配失败问题

A 内存分配失败的报告方式:大多数new通过抛出bad_alloc异常来报告分配失败;nothrownew则通过C中malloc方式报告失败,即仅返回空指针,永远不会抛出异常。

B 内存分配失败的处理过程:通过set_new_handler调用错误处理函数new_handler,检查指向函数的指针,若指针非0,则指向的函数被调用。

new_handler与set_new_handler的原型如下:

typedef void (*new_handler)();

new_handler set_new_handler(new_handler p)throw();

new_handler是一个typedef,表现出一个函数指针,该函数没有参数也没有传回值,而set_new_handler是一个函数,需要一个new_handler参数并传回一个new_handler。

Set_new_handler的参数指针指向的函数正是当operatornew无法配置足够内存时,应该去调用的函数,其传回值是一个函数指针,指向先前登陆过的new_handler。用法示例如下:

void nomorememory()

{

      cerr << "unable to satisfy request for memory\n";

      abort();

}

int main()

{

      set_new_handler(nomorememory);

      int *pbigdataarray = new int[100000000];

      ...

}

 

阅读更多
个人分类: C++
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭