一. 内存分配方式
- 从静态存储区域分配。内存在程序编译的时候就已经分配好了(即已经编址),这些内存在程序的整个允许期间都存在。例如全局变量,static变量等。
- 在堆栈上分配。在函数执行期间,函数内局部变量(包括形参)的存储单元都创建在堆栈上,函数结束时这些存储单元自动释放(堆栈清退)。堆栈内存分配运算内置于处理器的指令集中,效率很高,并且一般不存在失败的危险,但是分配的内存容量有限,可能出现堆栈溢出。
- 从堆(heap)或自由存储空间上分配,亦称动态内存分配。程序在运行期间用malloc() 或 new 申请任意数量的内存,程序员自己掌握释放内存的恰当时机(使用free() 或 delete).
二. 常见的内存错误及其对策
- 内存分配未成功,却使用了它。解决办法:在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用 assert(p!=NULL) 进行检查以避免输入非法参数。如果是用malloc() 或 new 来申请内存,应该用 if(p==NULL),if(p!=NULL) 或者捕获异常来进行错误处理。
- 内存分配虽然成功,但是尚未初始化就使用它。创建指针对象或者数组的时候都要赋初值,零值也可,但不可省略。
- 内存越界。多会出现在循环中。
- 忘记了释放内存或者只释放了部分内存,因此造成了内存泄漏。
- 动态内存的申请与释放必须配对,程序中malloc() 与 free() 的使用次数一定要相同,否则肯定有错误(new/delete同理).
- 动态创建数组与删除数组空间的正确方法是
char* p = new char[1025]; delete []p; int* q = new int[1024]; delete []q; //多维数组的动态创建 //一个多维数组在语义上并不等价于一个指向其元素类型的指针,相反它等价于一个“指向数组的指针” char *p1 = new char[5][3]; //ERROR! 语义不等价 int *p2 = new int[4][6]; //ERROR! 语义不等价 char (*p3)[4] = new char[5][4]; //OK,退化第一维,语义等价 int (*p4)[5] = new char[3][5]; //OK,退化第一维,语义等价 char (*p5)[5][7] = new char[20][5][7]; //OK,退化第一维,语义等价 //错误的删除多维数组的方法 delete [][]p3; delete [][]p4; delete [][][]p5; //正确的方法 delete []p3; delete []p4; delete []p5;
- 用free 或 delete 释放了内存后,立即将指针设置为NULL,防止产生“野指针”.
三、指针参数如何传递内存
通过下面几个示例深入了解:
void GetMemory(char* p, int num) { //编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是_p,编译器使_p = p. //如果函数体内的程序修改了_p指向的内容,p也会跟着改变,因为它们指向同一块内存,这就是指针可用作输出参数的原因 p = (char*)malloc(sizeof(char) * num); //_p申请了新的内存,只是把_p本身的值改变了,即指向了新的内存空间,但是p本身丝毫未变,所以该函数不能输出任何东西,且每执行一次都会泄漏一块内存,因为没有free()释放内存 } void Test1(void) { char * str = NULL; GetMemory(str, 100); //str 仍未 NULL strcpy(str, "hello"); //运行时错误 }
针对上面的例子,可以改用“指针的指针” 或者 “指针的引用”。
void GetMemory(char** p, int num) //或者是char* &rp { *p = (char*)malloc(sizeof(char) * num); // rp = (char*)malloc(sizeof(char) * num); } void Test2(void) { char * str = NULL; GetMemory(&str, 100); //因为参数是指针的指针,所以这里要对str再取地址:&str strcpy(str, "hello"); //OK cout << str << endl; free(str); //OK }
如果“指针的指针” “指针的引用” 不好理解,可以采取参数返回值传递动态内存的方式:
char* GetMemory(int num) { char *p = (char*)malloc(sizeof(char) * num); return p; } void Test3(void) { char * str = NULL; str = GetMemory(100); strcpy(str, "hello"); cout << str << endl; free(str); }
使用return的注意不要返回指向“栈内存”的指针或引用,因为该内存在函数结束的时候将自动释放,见下例:
char* GetString() { char p[] = "hello world"; //用字符串常量来初始化数组的内存空间 return p; } void Test4(void) { char * str = NULL; str = GetString();//str不再是NULL,但它的内容是垃圾 cout << str << endl; }
上面GetString函数进行如下修改后,又将是一种新的问题:
char* GetString2() { char *p = "hello world"; //此处变成了常量字符串,位于静态存储区,它在程序的生命期内有效,无论什么时候调用GetString2, //返回的始终是一个只读的内存块,不可改写,如确实需要如此,请将返回值改为 const char* return p; } void Test5(void) { char * str = NULL; str = GetString2(); cout << str << endl; }
四、free,delete与指针,内存的恩怨情仇
free() 和 delete只是把指针所指的内存给释放掉,并没有把指针本身删除掉。
free() 和 delete 后,一定要将指针置为NULL,避免成为“野指针”!
请牢记:
- 指针消亡了,并不表示它所指向的内存会自动释放。
- 内存被释放了,并不表示指针会消亡或者成为了NULL。
class A { public: void Func(void) { cout << "A::func()" << endl; } }; void Test(void) {//外出程序块 A *p = NULL; { A a; p = &a;//a的生命周期在此终止 } p->Func(); //p在此已经是野指针了 }
上面代码段运行时不会报错,具体原因是:
a 虽然退栈了,但仅仅是调用了一下析构函数而已,而我们知道析构函数没干什么重要的事情,它并没有清除a的内存(a的内存空间仍然在函数堆栈上),所以a仍然好好地在那里放着,只是你无法在程序块外直接访问而已。
五、malloc/free 与 new/delete 难兄难弟
malloc/free是C++/C语言的标准库函数。
new/delete 是C++的运算符。
它们都可用于申请和释放动态内存.由于内部数据类型的“对象”没有构造和析构的过程,它们是等价的.
但是对于非内部数据类型(ADT:Abstract Data Type / UDT: User Define Type)的对象而言,光用malloc/free() 无法满足动态对象的要求:对象在创建的同时要自动调用构造函数,对象在销毁的时候自动调用析构函数。所以C++需要一个能够完成动态内存分配和初始化工作的运算符new,以及一个能够完成清理与释放内存工作的运算符delete。
new/delete并非库函数,而是语言实现直接支持的运算符,就像sizeof()和typeid()及C++新增的4个类型转换运算符也不是库函数一样。
为何要让二者同时存在?
- 为了兼容C,C程序只能用malloc()和free()来管理动态内存。
- new/delete 更安全。因为new可以自动计算它要构造的对象的字节数量(包括成员边界调整而增加的填补字节和隐含成员),而malloc则不能;new直接返回目标类型的指针,不需要显示转换类型,而malloc()则返回void*,必须首先显示地转换为目标类型后才能使用。
- 我们可以为自定义类重载new/delete,实现富有“个性”的内存分配和释放策略。而malloc()/free() 不能被任何类重载。
- 在某些情况下,malloc()/free()可以提供比new/delete更高的效率,因此某些STL实现版本的内存分配器会采用malloc()/free()来进行存储管理。
malloc/free使用要点
malloc()原型如下:
void* malloc(size_t size);
例如:申请一块长度为length的整形数组的内存:
int *p = (int*)malloc(sizeof(int) * length);
要点:
- malloc()函数返回值是void*,所以在调用malloc()时要显示地进行类型转换。
- malloc()函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。最好每次都使用sizeof运算符。
new的三种使用方式
- plain new:就是我们常用的普通的new,在失败后抛出标准异常std::bad_alloc而不是返回NULL,所以通过检查其返回值是否为NULL来判断分配成功与否,毫无意义。应该使用try.catch.
- nothrow new:就是上面的new不抛出异常,而是返回NULL。
- placement new:这种形式的主要用途就是反复使用一块较大的动态分配成功的内存来构造不同类型的对象或它们的数组。此种方式不用担心内存分配失败,因为它根本不会分配内存,它做的唯一的事情就是调用对象的构造函数。
以上三种的用法总结如下表:
六、用对象模拟指针
此部分内容难以理解,暂留!!!
七、智能指针
泛型指针auto_ptr
泛型技术(模板技术)可以达到适应任何类型的通用性。
简化版代码如下:
template<typename T> class auto_ptr { public: explicit auto_ptr(T* p = 0) :m_ptr(p) {} auto_ptr(const auto_ptr<T>& copy) :m_ptr(copy.release()) {} auto_ptr<T> & operator=(const auto_ptr<T>& assign) { if (this != &assign) { delete m_ptr; m_ptr = assign.release(); //释放并移交拥有权 } return *this; }; ~auto_ptr() {delete m_ptr}; //负责释放存储 T& operator*() { return *m_p; } //重载“*” T* operator->() { return m_p; } //重载“->” T* release() const { T* temp = m_ptr; (const_cast<auto_ptr<T>*>(this))->m_ptr = 0; return temp; } private: T* m_ptr; };
使用auto_ptr<>这样的灵巧指针有一个好处:当函数即将退出或有异常抛出时,不需要我们显示地用delete来删除每一个动态创建起来的对象,C++保证会在堆栈清退的过程中自动调用每一个局部对象的析构函数,而析构函数会调用delete来完成这个任务。
见下例:
void func() { auto_ptr<BigClass> pb(new BigClass); //............ if(x==0) throw "x equal to 0"; //............ } //这里会自动调用~auto_ptr<>
带有引用计数的智能指针
带有引用计数功能的智能指针兼有普通指针共享实值对象和auto_ptr自动释放实值对象的双重功能,并自动管理实值对象的生命周期和有效引用的计数,不会造成丢失引用、内存泄漏及多次释放等问题。
关于智能指针:MSDN智能指针