第七章 内存管理
主线是分配、使用、释放。分配的话,分配多少合适,有多少可用,分配失败怎么处理。
7、1内存的分配方式
三种分配内存的方式:
1) 从静态存储区域分配。内存在编译时已分配好,这块内存在程序整个运行期间都存在。如全局变量,static变量。
2) 在栈上创建。栈内存分配运算内置于处理器的指令集中,效率很高,但分配的内存容量有限。
3) 从堆上分配,亦称动态分配。运行时用new或malloc申请任意多的内存,程序员自己负责在何时free或delete内存。
7、2常见的内存错误及其对策
常见的内存错误及其对策:
1) 内存分配未成功,却是用了它。常用解决方式是,在使用前检查指针是否为NULL。若指针p是函数的参数,则在函数入口处用assert(p!=NULL)进行检查。若用malloc或new来申请内存,应该用if (p == NULL)或if (p != NULL)进行放错处理。
2) 内存分配虽成功,但尚未初始化就使用它。因内存的缺省初始值究竟是什么没有统一的标识,所以无论何种方式创建数组,都别忘了赋初值,即使是赋零值也不可省略。
3) 内存分配成功且已初始化,但操作越过了内存的边界。
4) 忘记释放内存,造成内存泄露。动态内存的申请与释放必须配对,否则肯定有错。
5) 释放了内存却继续使用它。有三种情况:
Ø 程序中对象调用关系过于复杂,搞不清某个对象究竟是否已释放了内存,此时该重新设计数据结构,从根本上解决对象管理的混乱局面。
Ø 函数的return写错了,主要不要返回指向“栈内存”的指针或引用。如
char * Func(void)
{
char str[] = “hello world”; // str的内存位于栈上
…
return str; // 该内存在函数结束时被自动销毁,所以将导致错误
}
Ø 使用free或delete释放内存后,没有将指针设置为NULL。导致产生“野指针”。
7、3指针与数组的对比
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命周期内保持不变,只有数组的内容可变。你可能好奇,难道不能在堆上创建数组吗?int *a=new int[3];a难道不是在堆上吗?可是a是数组吗?a是一个指针。也许还有的说指针数组,int *p [10];可它再堆上吗?复习下指针数组和数组指针的区别:加“的”,加括号的为指针。
下面比较下指针与数组的特性:
1) 修改内容。字符数组的内容可以改变。若指针p指向常量字符串”world”,常量字符串的内容是不可以被改变的。p[0]=’x’错误。
2) 内容复制与比较。不能对数组名进行直接复制与比较。若想把数组a的内容复制给数组b,不能用b = a,该用标准库函数strcpy进行复制;比较时用strcmp。
p = a只是把a的地址赋给了p,若想复制内容,可先用malloc为p申请一块容量为strlen(a) +1个字符的内存,再用strcpy进行字符串复制。
3) 计算内存的容量。sizeof可计算出数组的容量。C/C++没法知道指针所指向的内存容量,除非在申请内存时记住它。注意当数组作为函数参数传递时,该数组自动退化为同类型的指针。
char *a = “hello world”;
cout<<sizeof(a)<<endl; //12字节
void Func(char a[100])
{
cout<<sizeof(a)<<endl; //4字节而不是100字节
}
7、4指针参数是如何传递内存的
1)若函数的参数是一个指针,不要指望用该指针去申请动态内存。
void GetMemory(char *p, int num) { p = (char *)malloc(sizeof(char) * num); } |
void Test(void) { char *str = NULL; GetMemory(str, 100); // str 仍然为 NULL strcpy(str, "hello"); // 运行错误 } |
str并没有获得期望的内存,str依旧是NULL。毛病出在GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是_p,编译器使_p = p。若函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可作为输出参数的原因。本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但p丝毫未变。所以函数GetMemory不能输出任何东西。
2)若非要用指针参数去申请内存,该用“指向指针的指针”。
void GetMemory2(char **p, int num) { *p = (char *)malloc(sizeof(char) * num); } |
void Test2(void) { char *str = NULL; GetMemory2(&str, 100); // 注意参数是 &str,而不是str strcpy(str, "hello"); cout<< str << endl; free(str); } |
3)因“指向指针的指针”不易理解,可用函数返回值来传递动态内存。
char *GetMemory3(int num) { char *p = (char *)malloc(sizeof(char) * num); return p; } |
void Test3(void) { char *str = NULL; str = GetMemory3(100); strcpy(str, "hello"); cout<< str << endl; free(str); } |
4) 上述方法虽好用,但常被用错。这里强调不要return返回指向“栈内存”的指针。
char *GetString(void) { char p[] = "hello world"; return p; // 编译器将提出警告 } |
void Test4(void) { char *str = NULL; str = GetString(); // str 的内容是垃圾 cout<< str << endl; } |
5)
char *GetString2(void) { char *p = "hello world"; return p; } |
void Test5(void) { char *str = NULL; str = GetString2(); cout<< str << endl; } |
虽然运行正确,但GetString2的设计概念是错误的。因GetString2内的"hello world"是常量字符串,位于静态存储区,在生命周期内恒定不变。无论何时调用GetString2,返回的都是一个“只读”的内存块。
7、5free和delete把指针怎么啦
它们只是把指针所指向的内存释放掉,并没有把指针本身干掉。指针p被free后其地址仍然不变(非NULL),只是该地址对应的内存是垃圾,p成了“野指针”。所以在释放掉内存后,记得将p设置为NULL,下次使用时可通过if (p! = NULL)来进行放错处理。
7、6动态内存会自动释放吗
函数体内的局部变量在函数结束时自动消亡。不要误以为局部指针变量p所指向的动态内存也会随函数结束而释放。
两个特征:
指针消亡了,并不表示所指的内存会被自动释放。
内存释放了,并不表示指针会消亡或成了NULL指针。
7、7杜绝“野指针”
“野指针”不是NULL指针,是指向垃圾内存的指针。“野指针”的成因主要有:
Ø 指针变量没有初始化。
Ø 指针p被free或delete后,没有置为NULL,让人误以为p是个合法的指针。
Ø 指针操作超越了变量的作用范围。这种情况让人防不胜防。如
class A
{
public:
void Func(void){ cout << “Func of class A”<< endl; }
};
void Test(void)
{
A *p;
{
A a;
p = &a; // 注意 a 的生命期
}
p->Func(); // p是“野指针”
}执行语句p->Func()时,对象a已经消失,而p是指向a的,所以p就成了“野指针”。有点编译器可能会将这个优化,不报错。
7、8有了malloc/free为什么还要new/delete
malloc/free是C/C++的标准库函数,new/delete是C++的运算符。先说下标准库函数和运算符的区别:对非内部数据类型的对象而言,对象在创建的同时要自动执行构造函数,对象在消亡之前要执行析构函数。malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能把执行构造函数和析构函数的任务强加于malloc/free。C++运算符new/delete可以完成动态内存分配和初始化以及完成清理与释放内存工作。对内部数据类型而言,这两个函数无差别。
7、9内存耗尽怎么办
内存耗尽时,最好调用exit(1)直接退出程序,用return的话,若多处申请动态内存,就需要释放每个内存,太麻烦。
7、10malloc/free的使用要点
应当把注意力集中在两个要素上:“类型转换”和“sizeof”
Ø malloc返回值的类型是void*,所以在使用malloc时要显示进行类型转换,将void*转换成所需的指针类型。
Ø malloc只关心内存的总字节数,所以申请时注意使用sizeof。
例子:用malloc申请一块长度为length的整数类型的内存:
int *p = (int*)malloc(sizeof(int)*length);
free:若p是NULL指针,则free对p操作多少次都不会有问题;若p不是NULL指针,free对p连续操作两次就会导致程序运行错误。
7、11new/delete的使用要点
new内置了sizeof、类型转换和类型安全检查功能。若用new创建对象数组,只能使用对象的无惨构造函数。Obj *objects = new Obj[100];在使用delete释放对象数组时,应这样:delete []objects;若写为delete objects;相当于delete objects[0],漏掉了另外99个对象。