指针是C语言的灵魂,因此随意挑选一个C语言程序项目,一般都能够看到指针的大量应用。指针允许程序员访问内存,但是初学者往往会误以为“指针就是内存”,写出错误的C语言代码,为此,本节将以问答的方式讨论C语言中指针与内存的关系。
下面这段C语言代码为什么不能正常工作?
小明写了一段C语言程序,允许用户输入一段字符串,并将其打印到控制台,核心代码段如下:
char *answer;
printf("Type something:\n");
gets(answer);
printf("You typed \"%s\"\n", answer);
小明定义了一个 char 型指针,用于接收用户输入的字符串,但是编译执行这段C语言代码时,它发现程序并不能按照他的预期进行,这是为什么呢?
这是因为指针 answer 尚未指向系统分配给该程序使用的内存。事实上,上述C语言代码中的 answer 没有被初始化,它就有可能指向任意地址,而它指向的内存并不是提供给这段程序使用的。所以,小明的C语言程序表现就未知了,可能根本无法编译通过,可能表现正常,也有可能运行时崩溃。
归根结底,就是 answer 指针没有指向一块属于自己的内存。若希望小明的C语言程序正常,可以将 answer 改为数组,例如:
#include <stdio.h>
#include <string.h>
char answer[100], *p;
printf("Type something:\n");
fgets(answer, sizeof(answer), stdin);
if((p = strchr(answer, '\n')) != NULL)
*p = '\0';
printf("You typed \"%s\"\n", answer)
修改后的C语言代码还将 gets() 改为 fgets(),以确保用户的输入不会溢出数组 answer。
既如此,下面这段C语言代码为什么又可以正常工作了呢?
可能有读者曾经写过下面这样的C语言代码,按照上面的分析,代码并没有让 p 指向设定的内存,为什么还是能够得到预期的结果呢?
char *p;
strcpy(p, "abc");
其实,这只能说明读者“幸运”。按照前面的分析,未初始化的指针 p 是可能指向任意地址内存的,那么如果它指向的内存允许被上面这两行C语言代码使用,并且这段内存暂时还没有被别的代码使用,程序自然是有可能正常运行的。
总结一下,程序员定义了指针 char *p
,也仅仅只是申请了一块用于存放指针 p 自己的内存,如果希望使用 p 管理某段内存,程序员应该显式的申请一块内存,并让 p 指向它。
小明从文件读数据到数组,为什么不正常呢?
相关的C语言代码是下面这样的,小明发现这段代码执行结束后,所有的指针里的内容都是文件最后一行的内容,为什么呢?
char linebuf[80];
char *lines[100];
int i;
for(i = 0; i < 100; i++) {
char *p = fgets(linebuf, 80, fp);
if(p == NULL) break;
lines[i] = p;
}
如果读者弄清楚了前面两个问题,再考虑本题就不难了。其实原因很简单,小明只为文件的一行内存申请了内存:linebuf。上面这段C语言代码每次调用 fgets(),上一次读取存放在 linebuf 里的内容都会被覆盖,这种现象一直持续到读到最后一行。
指针数组 lines 的每一个元素指向的内存实际上是同一块内存,都是 linebuf,而最终 linebuf 里的内容最终是文件最后一行的内容,所以 lines 没有将文件所有内容都记录就不足为奇了。
free() 函数怎么知道要释放多少内存?
在C语言程序开发中,malloc() 函数可以申请内存供指针使用。但是小明发现 free() 函数释放 malloc() 返回的指针时,并不需要指定指针指向的内存大小,那么 free() 函数如何确定自己要释放多少内存呢?
其实 malloc/free 库会记录每次 malloc() 申请的内存块,所以调用 free() 函数时,就不必再指定要释放的内存大小了。事实上,大多数 malloc/free 库并不会把“释放(free)”掉的内存返回给操作系统,free(p) 函数仅仅只是告诉操作系统,调用者不打算再占用 p 指向的内存了,因此下次 malloc() 时,可以把 p 指向的内存块分配给其他代码使用。
有时候,读者可能会发现自己的C语言程序明明 free() 了一大块内存,但是通过操作系统资源管理器查看可用内存却并未增加,这就是原因了。
malloc 申请的内存被 free 后,就无法使用了,对不对?
C语言很少限制程序员“自由”,前面一个问题讨论到,很多 malloc/free 库并不会把“释放(free)”掉的内存返回给操作系统,free(p) 仅仅是告诉操作系统,调用者不打算再占用 p 指向的内存了。
但是这段内存仍然还存在于这个世上,通过指针 p,自然也是可以继续访问该内存段的。只不过,这么做是不安全的,因为操作系统随时有可能将这段内存分配给其他代码使用。
虽说很少有C语言程序员会故意使用已经释放的内存内容,但是却很容易“意外的”使用,例如下面这段C语言代码:
struct list *listp, *nextp;
for(listp = base; listp != NULL; listp = nextp) {
nextp = listp->next;
free(listp);
}
如果不使用临时指针变量 nextp,而是使用更明显的迭代表达式 listp = listp->next,将会发生什么呢?留给读者自己考虑了。
在函数内部使用局部变量接收malloc申请的内存,是否还需要free()呢?
自然是需要的,读者应该明白,C语言中的指针和它指向的内容是两个独立的概念,例如下面这段C语言代码:
void fun()
{
void *p = malloc(100);
}
虽然局部变量 p 随着函数 fun() 执行结束被释放,但是 p 指向的 100 字节内存却不会被释放,操作系统仍然会认为有人在使用它,不会再将这段内存分配给其他代码使用。如果没有 free(p),这段内存在C语言程序的整个生命周期内,永远都不会再被使用了。
这就是所谓的“内存泄漏”。
如果读者希望申请一段不需要 free() 的内存,可以调用 alloca() 函数。不过,很多有经验的C语言程序员都会规避 alloca() 的使用,因为 alloc() 申请的内存存在于函数栈帧上,嵌入式C语言程序中,函数的栈帧一般都不会太长。
另外,alloca() 函数的实现有些特殊,在一些没有传统栈帧的机器上很难实现。所以 alloca() 函数不是标准的,调用了该函数的C语言程序将丢失可移植性。
小结
虽然C语言的指针可以操作内存,但是这并不意味着指针就是内存。所谓“指针操作内存”,其实只是一种手段,要操作的内存必须已经准备好才行,本节通过几个例子说明了这一点。在文章最后,还讨论了关于动态内存分配可能存在的问题,应该明白,C语言一般不会限制程序员,所以在编写C语言程序时,我们应该清楚每一块内存的使用情况,究竟哪一块已经被释放,哪一块可以安全使用,这些都是需要了然于胸的。