第7章 内存管理
接待进入内存这片雷区。巨大的Bill Gates 曾经失口:
640K ought to be enough for everybody
— Bill Gates 1981
序次递次员们但凡编写内存管理序次递次,每每胆战心惊。假定不想触雷,独一的处置办法就是发明扫数窜伏的地雷并且断根它们,躲是躲不了的。本章的内容比普通教科书的要深切得多,读者需细心阅读,做到真正地知晓内存管理。
7.1内存分派编制
内存分派编制有三种:
(1) 从静态存储地区分派。内存在序次递次编译的时分就曾经分派好,这块内存在序次递次的整个运转期间都存在。譬喻全局变量,static变量。
(2) 在栈上成立。在测验考试函数时,函数外部门变量的存储单位都可以在栈上成立,函数测验考试终了时这些存储单位自动被释放。栈内存分派运算内置于处置赏罚器的指令集合,效率很高,但是分派的内存容量无限。
(3) 从堆上分派,亦称静态内存分派。序次递次在运转的时分用malloc或new请求随意几何的内存,序次递次员自己负责在何时用free或delete释放内存。静态内存的生计期由我们决意,运用极度灵动,但成果也最多。
7.2罕见的内存错误及其对策
发生内存错误是件极度费事的事情。编译器不能自动发明这些错误,但凡是在序次递次运转时才干捕捉到。而这些错误大多没有清晰的症状,时隐时现,添加了改错的难度。偶尔用户肝火冲发地把你找来,序次递次却没有发生任何成果,你一走,错误又发生负气了。
罕见的内存错误及其对策如下:
u 内存分派未乐成,却运用了它。
编程熟手外行常犯这种错误,由于他们没有看法到内存分派会不乐成。常用途置办法是,在运用内存之前反省指针能否为NULL。假定指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)中断反省。假定是用malloc或new来请求内存,应该用if(p==NULL) 或if(p!=NULL)中断防错处置赏罚。
u 内存分派虽然乐成,但是尚未初始化就引用它。
犯这种错误首要有两个缘故起因:一是没有初始化的观念;二是误认为内存的缺省初值全为零,招致引用初值错误(譬喻数组)。
内存的缺省初值理想后果是什么并没有同等的尺度,虽然有些时分为零值,我们宁可托其无不可托其有。所以无论用何种编制成立数组,都别忘了赋初值,即就是赋零值也不可省略,不要嫌费事。
u 内存分派乐成并且曾经初始化,但应用越过了内存的边界。
譬喻在运用数组时但凡发生下标“多1”也许“少1”的应用。特别是在for轮回语句中,轮回次数很随便搞错,招致数组应用越界。
u 遗忘了释放内存,构成内存透露。
含有这种错误的函数每被挪用一次就丧失一块内存。刚入手下手时琐屑的内存充沛,你看不到错误。终有一次序次递次突然物化失,琐屑出现提示:内存耗尽。
静态内存的请求与释放必需配对,序次递次中malloc与free的运用次数一定要相反,否则一定有错误(new/delete同理)。
u 释放了内存却持续运用它。
有三种情况:
(1)序次递次中的东西挪用关连过于严重,其实难以搞体会理睬某个东西理想后果能否曾经释放了内存,此时应该重新谋划数据机关,从基础内幕上处置东西管理的紊乱场合排场。
(2)函数的return语句写错了,细心不要前往指向“栈内存”的“指针”也许“引用”,由于该内存在函数体终了时被自动销毁。
(3)运用free或delete释放了内存后,没有将指针设置为NULL。招致发生“野指针”。
l 【纪律7-2-1】用malloc或new请求内存之后,应该立地反省指针值能否为NULL。抗御运用指针值为NULL的内存。
l 【纪律7-2-2】不要遗忘为数组和静态内存赋初值。抗御将未被初始化的内存作为右值运用。
l 【纪律7-2-3】抗御数组或指针的下标越界,特别要留神发生“多1”也许“少1”应用。
l 【纪律7-2-4】静态内存的请求与释放必需配对,抗御内存透露。
l 【纪律7-2-5】用free或delete释放了内存之后,立地将指针设置为NULL,抗御发生“野指针”。
7.3指针与数组的相比
C /C序次递次中,指针和数组在不少中心可以互相交换着用,让人发生一种错觉,认为两者是等价的。
数组要么在静态存储区被成立(如全局数组),要么在栈上被成立。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只罕见组的内容可以转变。
指针可以随时指向随意规范的内存块,它的特征是“可变”,所以我们常用指针来应用静态内存。指针远比数组灵动,但也更损伤。
下面以字符串为例相比指针与数组的特征。
7.3.1 编削内容
示例7-3-1中,字符数组a的容量是6个字符,其内容为hello\0。a的内容可以转变,如a[0]= ‘X’。指针p指向常量字符串“world”(位于静态存储区,内容为world\0),常量字符串的内容是不可以被编削的。从语法上看,编译器并不认为语句p[0]= ‘X’有什么不妥,但是该语句妄想编削常量字符串的内容而招致运转错误。
char a[] = “hello”;
a[0] = ‘X’;
cout << a << endl;
char *p = “world”; // 细心p指向常量字符串
p[0] = ‘X’; // 编译器不能发明该错误
cout << p << endl;
示例7-3-1 编削数组和指针的内容
7.3.2 内容复制与相比
不能对数组名中断间接复制与相比。示例7-3-2中,若想把数组a的内容复制给数组b,不能用语句 b = a ,否则将发生编译错误。应该用尺度库函数strcpy中断复制。同理,相比b和a的内容能否相反,不能用if(b==a) 来剖断,应该用尺度库函数strcmp中断相比。
语句p = a 并不能把a的内容复制指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数malloc为p请求一块容量为strlen(a) 1个字符的内存,再用strcpy中断字符串复制。同理,语句if(p==a) 相比的不是内容而是地址,应该用库函数strcmp来相比。
// 数组…
char a[] = "hello";
char b[10];
strcpy(b, a); // 不能用 b = a;
if(strcmp(b, a) == 0) // 不能用 if (b == a)
…
// 指针…
int len = strlen(a);
char *p = (char *)malloc(sizeof(char)*(len 1));
strcpy(p,a); // 不要用 p = a;
if(strcmp(p, a) == 0) // 不要用 if (p == a)
…
示例7-3-2 数组和指针的内容复制与相比
7.3.3 角力盘算争论内存容量
用运算符sizeof可以角力盘算争论出数组的容量(字节数)。示例7-3-3(a)中,sizeof(a)的值是12(细心别忘了’\0’)。指针p指向a,但是sizeof(p)的值倒是4。这是由于sizeof(p)取得的是一个指针变量的字节数,相等于sizeof(char*),而不是p所指的内存容量。C /C语言没有办法晓得指针所指的内存容量,除非在请求内存时记取它。
细心当数组作为函数的参数中断传递时,该数组自动退步为同规范的指针。示例7-3-3(b)中,不论数组a的容量是几何,sizeof(a)不休就是sizeof(char *)。
char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12字节
cout<< sizeof(p) << endl; // 4字节
示例7-3-3(a) 角力盘算争论数组和指针的内存容量
void Func(char a[100])
{
cout<< sizeof(a) << endl; // 4字节而不是100字节
}
示例7-3-3(b) 数组退步为指针
7.4指针参数是如何传递内存的?
假定函数的参数是一个指针,不要指望用该指针去请求静态内存。示例7-4-1中,Test函数的语句GetMemory(str, 200)并没有使str取得希冀的内存,str模仿仍是是NULL,为什么?
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"); // 运转错误
}
示例7-4-1 试图用指针参数请求静态内存
方向出在函数GetMemory中。编译器老是要为函数的每个参数制造且自正本,指针参数p的正本是 _p,编译器使 _p = p。假定函数体内的序次递次编削了_p的内容,就招致参数p的内容作相应的编削。这就是指针可以用作输入参数的缘故起因。在本例中,_p请求了新的内存,只是把_p所指的内存地址转变了,但是p丝毫未变。所以函数GetMemory并不能输入任何器械。理想上,每测验考试一次GetMemory就会透露一块内存,由于没有用free释放内存。
假定非得要用指针参数去请求内存,那么应该改用“指向指针的指针”,见教例7-4-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);
}
示例7-4-2用指向指针的指针请求静态内存
由于“指向指针的指针”这个欠好看念不随便体会,我们可以用函数前往值来传递静态内存。这种办法越发复杂,见教例7-4-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);
}
示例7-4-3 用函数前往值来传递静态内存
用函数前往值来传递静态内存这种办法虽然好用,但是但凡有人把return语句用错了。这里夸大不要用return语句前往指向“栈内存”的指针,由于该内存在函数终了时自动消亡,见教例7-4-4。
char *GetString(void)
{
char p[] = "hello world";
return p; // 编译器将提出劝诫
}
void Test4(void)
{
char *str = NULL;
str = GetString(); // str 的内容是渣滓
cout<< str << endl;
}
示例7-4-4 return语句前往指向“栈内存”的指针
用调试器慢慢跟踪Test4,发理论验str = GetString语句后str不再是NULL指针,但是str的内容不是“hello world”而是渣滓。
假定把示例7-4-4改写成示例7-4-5,会如何样?
char *GetString2(void)
{
char *p = "hello world";
return p;
}
void Test5(void)
{
char *str = NULL;
str = GetString2();
cout<< str << endl;
}
示例7-4-5 return语句前往常量字符串
函数Test5运转虽然不会出错,但是函数GetString2的谋划欠好看念倒是错误的。由于GetString2内的“hello world”是常量字符串,位于静态存储区,它在序次递次生命期内恒定不变。无论什么时分挪用GetString2,它前往的不休是统一个“只读”的内存块。
7.5 free和delete把指针如何啦?
别看free和delete的名字恶狠狠的(尤其是delete),它们只是把指针所指的内存给释放失,但并没有把指针自己干失。
用调试器跟踪示例7-5,发明指针p被free以后其地址仍旧不变(非NULL),只是该地址对应的内存是渣滓,p成了“野指针”。假定此时不把p设置为NULL,会让人误认为p是个正当的指针。
假定序次递次相比长,我们偶尔记不住p所指的内存能否曾经被释放,在持续运用p之前,但凡会用语句if (p != NULL)中断防错处置赏罚。很遗憾,此时if语句起不到防错陶染,由于即便p不是NULL指针,它也不指向正当的内存块。
char *p = (char *) malloc(100);
strcpy(p, “hello”);
free(p); // p 所指的内存被释放,但是p所指的地址仍旧不变
…
if(p != NULL) // 没有起到防错陶染
{
strcpy(p, “world”); // 出错
}
示例7-5 p成为野指针
7.6 静态内存会被自动释放吗?
函数体内的部门变量在函数终了时自动消亡。许多人误认为示例7-6是切确的。起因是p是部门的指针变量,它消亡的时分会让它所指的静态内存一起倒台。这是错觉!
void Func(void)
{
char *p = (char *) malloc(100); // 静态内存会自动释放吗?
}
示例7-6 试图让静态内存自动释放
我们发明指针有一些“貌同实异”的特征:
(1)指针消亡了,并不暗示它所指的内存会被自动释放。
(2)内存被释放了,并不暗示指针会消亡也许成了NULL指针。
这表达释放内存并不是一件可以轻率对待的事。也许有人不佩服,一定要找出可以轻率行事的起因:
假定序次递次停止了运转,齐全指针都会消亡,静态内存会被应用琐屑采取。既然云云,在序次递次临终前,就可以不必释放内存、不必将指针设置为NULL了。终于可以偷懒而不会发生错误了吧?
想得美。假定别人把那段序次递次取出来用到别的中心如何办?
7.7 根绝“野指针”
“野指针”不是NULL指针,是指向“渣滓”内存的指针。人们普通不会错用NULL指针,由于用if语句很随便剖断。但是“野指针”是很损伤的,if语句对它不起陶染。
“野指针”的成因首要有两种:
(1)指针变量没有被初始化。任何指针变量刚被创顿时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在成立的同时应当被初始化,要么将指针设置为NULL,要么让它指向正当的内存。譬喻
char *p = NULL;
char *str = (char *) malloc(100);
(2)指针p被free也许delete之后,没有置为NULL,让人误认为p是个正当的指针。参见7.5节。
(3)指针应用跨越了变量的陶染领域。这种情况让人防不胜防,示例序次递次如下:
>
{
public:
void Func(void){ cout << “Func of >” << endl; }
};
void Test(void)
{
A *p;
{
A a;
p = &a; // 细心 a 的生命期
}
p->Func(); // p是“野指针”
}
函数Test在测验考试语句p->Func()时,东西a曾经消失,而p是指向a的,所以p就成了“野指针”。但奇怪的是我运转这个序次递次时居然没有出错,这可以也许与编译器有关。
7.8 有了malloc/free为什么还要new/delete ?
malloc与free是C /C语言的尺度库函数,new/delete是C 的运算符。它们都可用于请求静态内存和释放内存。
对于非外部数据规范的东西而言,光用maloc/free无法惬意静态东西的要求。东西在成立的同时要自动测验考试机关函数,东西在消亡之前要自动测验考试析构函数。由于malloc/free是库函数而不是运算符,不在编译器节制权限之内,不可以把测验考试机关函数和析构函数的义务强加于malloc/free。
是以C 语言需求一个能完成静态内存分派和初始化任务的运算符new,以及一个能完成清理与释放内存任务的运算符delete。细心new/delete不是库函数。
我们先看一看malloc/free和new/delete如何完成东西的静态内存管理,见教例7-8。
>
{
public :
Obj(void){ cout << “Initialization” << endl; }
~Obj(void){ cout << “Destroy” << endl; }
void Initialize(void){ cout << “Initialization” << endl; }
void Destroy(void){ cout << “Destroy” << endl; }
};
void UseMallocFree(void)
{
Obj *a = (obj *)malloc(sizeof(obj)); // 请求静态内存
a->Initialize(); // 初始化
//…
a->Destroy(); // 断根任务
free(a); // 释放内存
}
void UseNewDelete(void)
{
Obj *a = new Obj; // 请求静态内存并且初始化
//…
delete a; // 断根并且释放内存
}
示例7-8 用malloc/free和new/delete如何完成东西的静态内存管理
类Obj的函数Initialize模拟告终构函数的成果,函数Destroy模拟了析构函数的成果。函数UseMallocFree中,由于malloc/free不能测验考试机关函数与析构函数,必需挪用成员函数Initialize和Destroy来完成初始化与断根任务。函数UseNewDelete则复杂得多。
所以我们不要妄想用malloc/free来完成静态东西的内存管理,应该用new/delete。由于外部数据规范的“东西”没有机关与析构的历程,对它们而言malloc/free和new/delete是等价的。
既然new/delete的成果完全掩饰笼罩了malloc/free,为什么C 不把malloc/free添加出局呢?这是由于C 序次递次但凡要挪用C函数,而C序次递次只能用malloc/free管理静态内存。
假定用free释放“new成立的静态东西”,那么该东西因无法测验考试析构函数而可以也许招致序次递次出错。假定用delete释放“malloc请求的静态内存”,理论上讲序次递次不会出错,但是该序次递次的可读性很差。所以new/delete必需配对运用,malloc/free也一样。
7.9 内存耗尽如何办?
假定在请求静态内存时找不到充沛大的内存块,malloc和new将前往NULL指针,宣告内存请求失败。但凡有三种编制处置赏罚“内存耗尽”成果。
(1)剖断指针能否为NULL,假定是则顿时用return语句停止本函数。譬喻:
void Func(void)
{
A *a = new A;
if(a == NULL)
{
return;
}
…
}
(2)剖断指针能否为NULL,假定是则顿时用exit(1)停止整个序次递次的运转。譬喻:
void Func(void)
{
A *a = new A;
if(a == NULL)
{
cout << “Memory Exhausted” << endl;
exit(1);
}
…
}
(3)为new和malloc设置十分处置赏罚函数。譬喻Visual C 可以用_set_new_hander函数为new设置用户自己定义的十分处置赏罚函数,也可以让malloc享用与new相反的十分处置赏罚函数。细致内容请参考C 运用手册。
上述(1)(2)编制运用最广泛。假定一个函数内有多处需求请求静态内存,那么编制(1)就显得力所能及(释放内存很费事),应该用编制(2)来处置赏罚。
许多人不忍心用exit(1),问:“不编写出错处置赏罚序次递次,让应用琐屑自己处置行不可?”
不可。假定发生“内存耗尽”多么的事情,普通说来应用序次递次曾经无药可救。假定不必exit(1) 把坏序次递次杀物化,它可以也许会害物化应用琐屑。事理如同:假定不把坏人击毙,坏人在老物化之前会犯下更多的罪。
有一个很首要的征象要通知大师。对于32位以上的应用序次递次而言,无论如何运用malloc与new,几乎不能够招致“内存耗尽”。我在Windows 98下用Visual C 编写了测试序次递次,见教例7-9。这个序次递次会无胁制地运转下去,基础内幕不会停止。由于32位应用琐屑支撑“虚存”,内存用完了,自动用硬盘空间顶替。我只听到硬盘嘎吱嘎吱地响,Window 98曾经累得对键盘、鼠标毫无回响反映。
我可以得出这么一个结论:对于32位以上的应用序次递次,“内存耗尽”错误处置赏罚序次递次毫无用途。这下可把Unix和Windows序次递次员们乐坏了:反正错误处置赏罚序次递次不起陶染,我就不写了,省了许多费事。
我不想误导读者,必需夸大:不加错误处置赏罚将招致序次递次的质量很差,万万不可因小失大。
void main(void)
{
float *p = NULL;
while(TRUE)
{
p = new float[1000000];
cout << “eat memory” << endl;
if(p==NULL)
exit(1);
}
}
示例7-9试图耗尽应用琐屑的内存
7.10 malloc/free 的运用要点
函数malloc的原型如下:
void * malloc(size_t size);
用malloc请求一块长度为length的整数规范的内存,序次递次如下:
int *p = (int *) malloc(sizeof(int) * length);
我们应当把详积极集合在两个要素上:“规范转换”和“sizeof”。
u malloc前往值的规范是void *,所以在挪用malloc时要显式地中断规范转换,将void * 转换成所需求的指针规范。
u malloc函数自己并不识别要请求的内存是什么规范,它只谅解内存的总字节数。我们但凡记不住int, float等数据规范的变量的稳当字节数。譬喻int变量在16位琐屑下是2个字节,在32位下是4个字节;而float变量在16位琐屑下是4个字节,在32位下也是4个字节。最好用以下序次递次作一次测试:
cout << sizeof(char) << endl;
cout << sizeof(int) << endl;
cout << sizeof(unsigned int) << endl;
cout << sizeof(long) << endl;
cout << sizeof(unsigned long) << endl;
cout << sizeof(float) << endl;
cout << sizeof(double) << endl;
cout << sizeof(void *) << endl;
在malloc的“()”中运用sizeof运算符是杰出的气焰气宇,但要留神偶尔我们会昏了头,写出 p = malloc(sizeof(p))多么的序次递次来。
u 函数free的原型如下:
void free( void * memblock );
为什么free函数不象malloc函数那样严重呢?这是由于指针p的规范以及它所指的内存的容量事前都是晓得的,语句free(p)能切确地释放内存。假定p是NULL指针,那么free对p无论应用几何次都不会出成果。假定p不是NULL指针,那么free对p持续应用两次就会招致序次递次运转错误。
7.11 new/delete 的运用要点
运算符new运用起来要比函数malloc复杂得多,譬喻:
int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];
这是由于new内置了sizeof、规范转换和规范平静反省成果。对于非外部数据规范的东西而言,new在成立静态东西的同时完成了初始化任务。假定东西有多个机关函数,那么new的语句也可以有多种方式。譬喻
>
{
public :
Obj(void); // 无参数的机关函数
Obj(int x); // 带一个参数的机关函数
…
}
void Test(void)
{
Obj *a = new Obj;
Obj *b = new Obj(1); // 初值为1
…
delete a;
delete b;
}
假定用new成立东西数组,那么只能运用东西的无参数机关函数。譬喻
Obj *objects = new Obj[100]; // 成立100个静态东西
不能写成
Obj *objects = new Obj[100](1);// 成立100个静态东西的同时赋初值1
在用delete释放东西数组时,留神不要丢了标志‘[]’。譬喻
delete []objects; // 切确的用法
delete objects; // 错误的用法
后者相等于delete objects[0],漏失了别的99个东西。
7.12 一些心得体会
我熟习不少技术不错的C /C序次递次员,很少有人能拍拍胸脯说知晓指针与内存管理(网罗我自己)。我最后学习C语言时特别怕指针,招致我斥地第一个应用软件(约1万行C代码)时没有运用一个指针,全用数组来顶替指针,其实拙笨得过度。逃避指针不是办法,厥后我改写了这个软件,代码量减少到原先的一半。
我的经历经历是:
(1)越是怕指针,就越要运用指针。不会切确运用指针,一定算不上是合格的序次递次员。
(2)必需养成“运用调试器慢慢跟踪序次递次”的习气,只需多么才干发明成果的本色。
版权声明:
原创作品,批准转载,转载时请务必以超链接方式标明文章 原始出处 、作者信息和本声明。否则将清查执法责任。