内存管理

以下内容摘自高质量C++/C编程指南(作者 林锐 ),之所以放在这里,只是为了以后查找方便。

内存分配方式有三种:

(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量, static 变量。

(2)
在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己负责在何时用 free 或
delete 释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

常见的内存错误及其对策

发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。

常见的内存错误及其对策如下:

@ 内存分配未成功,却使用了它。

编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为 NULL 。如果指针 p
是函数的参数,那么在函数的入口处用 assert(p!=NULL) 进行检查。如果是用 malloc 或 new 来申请内存,应该用
if(p==NULL) 或 if(p!=NULL) 进行防错处理。

@ 内存分配虽然成功,但是尚未初始化就引用它。

犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。

内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

@ 内存分配成功并且已经初始化,但操作越过了内存的边界。

例如在使用数组时经常发生下标“多 1 ”或者“少 1 ”的操作。特别是在 for 循环语句中,循环次数很容易搞错,导致数组操作越界。

@ 忘记了释放内存,造成内存泄露。

含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。

动态内存的申请与释放必须配对,程序中 malloc 与 free 的使用次数一定要相同,否则肯定有错误( new/delete 同理)。

@ 释放了内存却继续使用它。

有三种情况:

( 1 )程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。

( 2 )函数的 return 语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。

( 3 )使用 free 或 delete 释放了内存后,没有将指针设置为 NULL 。导致产生“野指针”。

我们发现指针有一些“似是而非”的特征:

( 1 )指针消亡了,并不表示它所指的内存会被自动释放。



2 )内存被释放了, 并不表示指针会消亡或者成了 NULL 指针。



这表明释放内存并不是一件可以草率对待的事。也许有人不服气,一定要找出可以草率行事的理由:

如果程序终止了运行,一切指针都会消亡,动态内存会被操作系统回收。既然如此,在程序临终前,就可以不必释放内存、不必将指针设置为 NULL
了。终于可以偷懒而不会发生错误了吧?

想得美。如果别人把那段程序取出来用到其它地方怎么办?

指针与数组的对比

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 ) 数组退化为指针

杜绝“野指针”

“野指针”不是 NULL 指针,是指向“垃圾”内存的指针。人们一般不会错用 NULL 指针,因为用 if
语句很容易判断。但是“野指针”是很危险的, if 语句对它不起作用。

“野指针”的成因主要有两种:

( 1 )指针变量没有被初始化。任何指针变量刚被创建时不会自动成为 NULL
指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为 NULL ,要么让它指向合法的内存。例如

char *p = NULL;

char str = (char ) malloc(100);

( 2 )指针 p 被 free 或者 delete 之后,没有置为 NULL ,让人误以为 p 是个合法的指针。参见 7.5
节。

( 3 )指针操作超越了变量的作用范围。这种情况让人防不胜防,示例程序如下:

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 是“野指针”

}

函数 Test 在执行语句 p->Func() 时 ,对象 a 已经消失,而 p 是指向 a 的,所以 p 就成了
“野指针”。但奇怪的是我运行这个程序时居然没有出错,这可能与编译器有关。

引用与指针的比较

引用是 C++ 中的概念,初学者容易把引用和指针混淆一起。一下程序中, n 是 m 的一个引用( reference ), m
是被引用物( referent )。

int m;

int &n = m;

n 相当于 m 的别名(绰号),对 n 的任何操作就是对 m
的操作。例如有人名叫王小毛,他的绰号是“三毛”。说“三毛”怎么怎么的,其实就是对王小毛说三道四。所以 n 既不是 m 的拷贝,也不是指向 m
的指针,其实 n 就是 m 它自己。

引用的一些规则如下:

( 1 )引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。

( 2 )不能有 NULL 引用,引用必须与合法的存储单元关联(指针则可以是 NULL )。

( 3 )一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。

以下示例程序中, k 被初始化为 i 的引用。语句 k = j 并不能将 k 修改成为 j 的引用,只是把 k 的值改变成为 6
。由于 k 是 i 的引用,所以 i 的值也变成了 6 。

int i = 5;

int j = 6;

int &k = i;

k = j; // k 和 i 的值都变成了 6;

上面的程序看起来象在玩文字游戏,没有体现出引用的价值。引用的主要功能是传递函数的参数和返回值。 C++
语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递。

以下是“值传递”的示例程序。由于 Func1 函数体内的 x 是外部变量 n 的一份拷贝,改变 x 的值不会影响 n, 所以 n
的值仍然是 0 。

void Func1(int x)

{

x = x + 10;

}

int n = 0;

Func1(n);

cout << “n = ” << n << endl; // n = 0

以下是“指针传递”的示例程序。由于 Func2 函数体内的 x 是指向外部变量 n 的指针,改变该指针的内容将导致 n 的值改变,所以 n
的值成为 10 。

void Func2(int *x)

{

( x) = ( x) + 10;

}

int n = 0;

Func2(&n);

cout << “n = ” << n << endl; // n = 10

以下是“引用传递”的示例程序。由于 Func3 函数体内的 x 是外部变量 n 的引用, x 和 n 是同一个东西,改变 x
等于改变 n ,所以 n 的值成为 10 。

void Func3(int &x)

{

x = x + 10;

}

int n = 0;

Func3(n);

cout << “n = ” << n << endl; // n = 10

对比上述三个示例程序,会发现“引用传递”的性质象“指针传递”,而书写方式象“值传递”。实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用”这东西?

答案是“用适当的工具做恰如其分的工作”。

指针能够毫无约束地操作内存中的如何东西,尽管指针功能强大,但是非常危险。就象一把刀,它可以用来砍树、裁纸、修指甲、理发等等,谁敢这样用?

如果的确只需要借用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”,以免发生意外。比如说,某人需要一份证明,本来在文件上盖上公章的印子就行了,如果把取公章的钥匙交给他,那么他就获得了不该有的权利。