摘自http://www.quanxue.cn/JC_CLanguage/CLang/Clang13.html
C语言程序员要严防内存泄漏,这个“内存泄漏”就是由动态内存分配引起的。指针是C语言和其它语言的最大区别,也是很多人不能跨入C语言的一道门槛。既然指针是这么一个“危险”的坏东西,干吗不取消它呢?
其实指针本身并没有好坏,它只是一种操作地址的方法,学会了便可以发挥其它语言难以匹敌的功能,没学会的话,只能做其它语言的程序员,也同样发挥你的光和热。小雅本人也在C语言门外徘徊多年,至今仍属于初学者。
“int
*x”中的x究竟是不是数组?光看这一句小雅无法告诉你,因为它既可表示单个变量内容,也可表示数组。下面是小雅专门为你准备的例子,理解之后,对动态分配时长度计算有好处。
#include
<stdio.h>int
main(void
) {int
*num = NULL;int
*x, y[] = {12, 22,32}, z = 100; //下面演示,指针既可充当变量、也可充当数组 x=&z; //整型变量的地址赋给x printf("*x=%d, x[0]=%d\n", *x, x[0]); x = y; //数组的地址赋给x printf("*x=%d, x[ 0]=%d, x[ 1]=%d, x[2]=%d\n", *x, x[0], x[1], x[2]); x = y + 1; //数组的第二位地址赋给x printf("*x=%d, x[-1]=%d, x[ 0]=%d, x[1]=%d\n", *x, x[-1], x[0], x[1]); x = y + 2; //数组的第三位地址赋给x printf("*x=%d, x[-2]=%d, x[-1]=%d, x[0]=%d\n", *x, x[-2], x[-1], x[0]);return
0; }
前面讲到的指针,基本上将已经定义好的变量的地址赋给指针变量,现在要学的是向操作系统申请一块新的内存。申请到的内存,必须在某个地方手动释放,因此下面2个函数必须配对使用。malloc()和free(),都是标准函数,在stdlib.h中定义。
根据不同的电脑使用状况,申请内存有可能失败,失败时返回NULL,因此,动态申请内存时,一定要判断结果是否为空。malloc()的返回值类型是“void *”,因此,不要忘记类型转换。(许多人都省略了。)
#include
<stdio.h>#include
<stdlib.h>#include
<string.h>int
main(void
) {char
*p ; p = (char
*)malloc(40 *sizeof
(char
)) ;if
(p == NULL) { //这个判断是必须的 printf("内存分配出错!"); exit(1); } strcpy(p,"这是劝学网C语言教程。\n"); //不要忘记给新内存赋值 printf("%s", p); free(p); //过河一定要拆桥 p = NULL ; //释放后的指针置空,这是非常好的习惯,防止野指针。return
0; }
内存泄漏主要有以下几种情况:
- 内存分配未成功,却使用了它。
- 内存分配虽然成功,但是尚未初始化就引用它。
- 内存分配成功并且已经初始化,但操作越过了内存的边界。
- 忘记了释放内存,造成内存泄露。
- 释放了内存却继续使用它。
下面的程序造成内存泄漏,想想错在何处?如何修改?
#include
<stdio.h>#include
<stdlib.h>int
main(void
) {int
*p, i; p = (int
*)malloc(6 * sizeof(int
)) ;if
(p == NULL) { //判断是否为空 printf("内存分配出错!"); exit(1); }for
(i=0; i<6; i++) { p++; *p = i; printf("%2d", *p);
}
printf("\n");
free(p); //这句运行时出错
return
0;
}
有人对某一只在函数内使用的指针动态分配了内存,用完后不释放。其理由是:函数运行结束后,函数内的所有变量全部消亡。这是错误的。动态分配的内存是在“堆”里定义,并不随函数结束而消亡。
有人对某动态分配了内存的指针,用完后直接设置为NULL。其理由是:已经为NULL了,这就释放了。这也是错误的。指针可以任意赋值,而内存并没有释放;相反,内存释放后,指针也并不为NULL。
C/C++内存存储问题是笔试中必须掌握的。先看下面的程序:
#include <stdio.h> #include "string.h" #include "malloc.h" void Swap(int a,int b) { int temp; temp=a; a=b; b=temp; } int Get_Int(int a) { int i=1+a; return i; } char* Get_Memory0() { char* p=(char*)malloc(sizeof(char)* 20); strcpy(p,"hello world"); return p; } char* Get_Memory1() { char* p="hello world"; return p; } char* Get_Memory2() { char p[]="hello world"; return p; } void main() { int x=4,y=3; Swap(x,y); int z=x-y; printf("z=%d\n",z); z=Get_Int(z); printf("z=%d\n",z); char* c0=Get_Memory0(); printf("c0=%s\n",c0); const char* c1=Get_Memory1(); printf("c1=%s\n",c1); char* const c2=Get_Memory2(); printf("c2=%s\n",c2); }
基础知识
内存存储:
在C/C++中,通常可以把内存理解成4个分区:栈、堆、全局/静态存储区和常量存储区。
(1)栈:通常是用那些在编译期间就能确定其存储大小的变量的存储区,用于在函数作用域内创建、在离开作用域后自动销毁的变量的存储区。通常是局部变量、函数参数等的存储区。它的存储空间是连续的,两个紧挨着的定义的局部变量,它们的存储空间是紧挨着的。栈的大小是有限的,通常Visual C++编译器默认栈的大小是1M,所以不要定义int a[1000000]这样的超大数组。
(2)堆:通常是用于那些在编译期间不能确定存储大小的变量存储区,它的存储空间是不连续的,一般同malloc(或new)函数来分配内存块,并且需要要free(或delete)释放内存。如果程序员没有释放掉,那么就会出现常说的内存泄漏问题。需要注意的是,两个紧挨定义的指针变量,所指向的malloc出来的内存并不一定是紧挨着的。另外需要注意的一点是,堆的大小几乎是不受限制的,理论上每个程序最大可达4GB。
(3)全局/静态存储区:和“栈”一样,通常是用于那些在编译期间就能确定存储大小的变量的存储区,但它用于的是在整个程序运行期间都可见的全局变量和静态变量。
(4)常量存储区:和“全局/静态存储区”一样,通常是用于那些在编译期间就能确定存储大小的常量存储区,并且在程序运行期间,存储区内的常量也是全局可见的。这是一块比较特殊的存储区,它们里面放的是常量,不允许被修改。
程序解答:
(1)Swap函数:我们知道,所有函数都会在运行时从程序“栈”上得到分配给它的一块存储区(这里的“栈”就是前两面讲解内存存储时提到的“栈”,第个运行程序都拥有自己的运行时“栈”)。这块“栈”上的函数存储区随函数的开始面开始,随着函数的结束而结束。函数结束后,这块存储区就会自动释放,以供程序的其他用途使用。系统在函数运行时会为函数的每一个参数都提供存储区,参数在存储区中的存储长度由自身的类型决定。参数传递,就是系统用函数实参初始化函数参数存储区的过程。
函数的参数有传值(传指针)和传址(传引用)两种。所谓参会传值,就是实参的值复制到函数运行时分配给函数的参数存储区中。参数在传值时,函数不会访问当前调用的实参,函数处理的是实参在本地的拷贝,这些拷贝被存储在函数的“栈”中,所以这些拷贝值的改变不会影响实参的值。
答案:
z=1
(2)问题:返回值i是一个局部变量,函数的返回参数怎么能是局部变量呢?局部变量在离开作用域后就自动被销毁了,还怎么能返回值给调用都呢?
解答:函数的返回值有传值和传址两种。int Get_Int(int a)属于返回值是传值的函数,这就意味着函数int Get_Int(int a)会在函数返回处产生一个临时对象,用于存放局部变量i的值的一份拷贝(变量i的右值的拷贝),临时对象是没有名称的,这份没有名称的对象的值(右值)会存储在调用者的“栈”中。所以当i作为局部变量离开作用域后,虽然被销毁了,但它的拷贝仍然存在,并在函数返回时作为“右值”赋给“左值z”。
答案:
z=2
(3)此处考查对“堆”的理解:函数返回的是指向“堆”内存的指针。程序中malloc()是用于分配“堆”内存的库函数,而对于“堆”内存,只要程序中没有调用free()库函数去释放掉该“堆”内存,那么在程序运行期间,malloc()库函数分配的“堆”内存将一直存在。
char* p=(char*)malloc(sizeof(char)*20)表示分配一块“堆”内存并使得变量p指向这块“堆”内存。
strcpy(p,"hello world")表示往p指向的“堆”内存中复制字符串hello world。
return p,变量p的左值是一个局部变量指针,存储于函数栈上,右值 是“堆”的地址(“堆”的值是hello world)。
函数char* Get_Memory0()属于返回值是传指针的函数,这就意味着函数char* Get_Memory0()会在函数返回处产生一个对返回变量p的“左值”的拷贝,也就是在“左值”的拷贝中存储了指向“堆”的地址。作为局部变量的 p,在离开函数作用域的时候虽然被销毁了,但函数返回值珠“左值”的拷贝是存在的,该拷贝存储了指向“堆“的地址,而而该“堆”的值是hello world。
答案:
c1=hello world
(4)这里考查对函数返回值和指针的理解:变量分为左值和右值,在char* p="hello world"中,左值是局部变量指针p,存储于函数栈上,右值是字符串常量hello world,存储于常量存储区。
char* Get_Memory1()属于返回值是传指针的函数,这就意味着函数char* Get_Memory1()会在函数返回处产生一个对返回对象的“左值”的拷贝,也就是在“左值”拷贝中存储了指向字符串常量hello world的地址。作为局部变量的p,在离开函数作用域的时候虽然被销毁了,但函数 返回的“左值”拷贝仍然存储了指向常量存储区的字符串常量hello world的地址。
答案:
c1=hello world
(5)和问题(4)有些相似,考查的也是对函数返回值和指针的理解:返回值是传指针,会在函数返回时产生“左值”拷贝。可以看到,p[]不是指针,是一个数组变量。编译器根据右值“hello world”的长度(12个字符)在编译期间在函数的栈上为数组分配大小为12个字符的内存存储区,其值是“hello world”。
通常,变量的意义在于,它给一块内存存储区提供名字,方便程序对这块内存进行读写。变量包含两个值:左值和右值。左值是内存存储区的名字,右值是存放存储区中的值。从程序中可以看到,函数返回的“左值”拷贝 指向的是局部变量数组p[12]的首地址。当局部数组p[12]作用域后会被自动销毁。这时,函数返回的“左值”拷贝指向的是一个被销毁的局部变量地址。
答案:
warning C4172: returning address of local variable or temporary
c2=未知