Cgo中使用varjs声明变量不用varC结构的变量是否需要释放内存?

1、基本数据类型变量名和内存的关系:
  scanf_s("%d", &i);
,在这一句就为i分配了内存(但尚未对这块内存进行初始化),所以可以通过&i直接使用这块内存。赋值就更不用说啦,i = 3;。
变量名i,是为方便编程人员使用,是这块内存的别名,指代到块内存,对编程人员i代表这块内存中存储的值(实际上是i指到这个内存,然后取值)。通常我们都是通过变量名来使用已知的内存的。
i代表取(这块内存中存储的)值,而&i代表取(这块内存的)址。程序本身是不为i这个变量名分配空间的。在最终的机器代码中,是不会出现变量名的,这一点在分析反汇编语言时可以看出(命令:dumpbin /disasm xx.obj &xx_disasm.asm可以查看反汇编语言)。那么编译器是如何处理变量名的呢,变量名会存储在符号表中,并将符号表中的索引对应到实际物理空间(地址)上去,当调用这个变量时,查找符号表就可以找到对应的地址并取值了。
2、不同类型变量的变量名和内存间的关系:
上面分析的是基本数据类型(如int、char等)的变量名。C中除了变量名之外,还有函数名、常量名、指针名、数组名、结构名等。和变量名不同,这些标识符都是直接对应着地址的。基本数据类型的变量,和地址的对应关系需要取址符&才能得到地址,而其余的这些,名字本身就对应着地址。
例如char *pc = “se”;,就是将字符串常量”se”的首地址(位于常量存储区)赋值给了字符指针pc。这也就解释了为什么不需要为pc分配地址就可以为其赋值,而不会遇到类似下面代码所带来的野指针问题:
  int *pi;
  *pi = 1;
int *pi句,是为pi分配空间,而不是开辟pi所指向的空间。
2.1 C语言中的常量:
C对常量是怎么处理的呢?比如上面的i = 3;中的常量3,存储常量3的地址并不是随机分配的,是在程序中的数据段中(.data?这个我也还不是很确定,希望知道的前辈们给个指导),也就是程序本身并不为3分配内存,而是直接写入指令。3是数字常量,对于字符常量和字符串常量,又分别是怎么处理的呢?
字符常量和数字常量是一样的处理方式,都是类似汇编中的立即数,直接写入指令;
而字符串常量,则是存储在常量存储区,可以使用&(“string”)取得具体地址。也就是字符串常量名字本身指代着地址,只是不能直接操作(和int i中的i相同)。
2.2 C语言的变量名
c中的数据类型除常量之外大致有5种:
基本数据类型:short、int、float、double、char:
对各基本数据类型的变量名及其声明时的处理方式都是一样的,声明时即分配内存,并使用变量名直接操作这段内存;使用取地址符号&取得地址的数字表示,至于声明时要不要做初始化,要看是不是全局变量或者 static变量了。
这类变量名指向一个地址空间,但不能直接当做地址使用,而是通过取址符&操作其地址。
2.3 构造数据类型:数组、结构、联合:
数组在声明时,即分配了空间:
  int a[5];
一旦声明a[5],相当于有a、a[0]、a[1]、a[2]、a[3]、a[4]这6个变量名。a[i]的指代方式和普通的变量名int i相同,指到一个对应的内存空间;关键是变量名a,本身就可以做地址用。我们知道a是数组名,但a并不代表整个数组的地址,而是指向数组首元素的地址(虽然在数值上是相同的,下面会有详细解释),所以可以有 int *p =。那么&a又怎么解释呢?对于int i而言,i代表一个空间,&i表示i所代表的空间地址;那么&a应该也是表示a所代表的地址了,也就是整个数组的地址。
a、&a和&a[0]同代表地址,且由于数组是顺序存储,所以a、&a和&a[0]所表示的地址在数据上是相同的,但是实际的指代意义却是不同的:
a是个int*类型的数据,相当于&(*a),是指向数组首元素的地址;
&a指代整个数组,是个int(*)[]类型的数据,是指针数组的地址;
&a[0]则是仅指代一个存储int的空间,是int*类型的数据。
也就是数组名,本身可以作为地址使用,指代该结构的首元素的地址。
结构在声明的时候,就分配了空间。结构体和数组不同,结构体类型的变量名并不能直接当作地址使用,这一点和基本数据类型相同。需要对结构体名使用取址符&才能进行地址操作,并且取址所得到地址代表的是指向结构体的指针,只是在数据上和结构体中的首元素地址相同。
对于结构体中的各个元素,其名称的指代关系和其数据类型相同,并不因为是结构体的元素而受到影响。具体见下面代码:
struct stu{
int score[5];
int main()
struct stu st1;
printf("%d\n", &st1);
printf("%d\n", &st1.age);
printf("%d\n", &st1.sex);
printf("%d\n", &st1.name);
printf("%d\n", st1.score);
联合是特殊的结构体,为节省空间,在其各元素依次存储,各元素的首地址均相对变量的基地址偏移为0,具体各变量名的分析和结构体同。
2.4 指针类型
声明一个指针类型 int *p;,则是为存储指针p分配空间,而并不会为p所指向的内存做任何动作,这就是野指针的原因。如下代码,p就是一个未指向任何已知内存的指针,为*p赋值,自然会出现错误:
  int *p;
  *p = 1;
指针中,char 是个很特殊的指针。一般的指针,仅指向一个所定义类型的内存,而char 则可以指向一个字符串,之所以可以实现这个功能是字符串结尾符’\0’的存在标识了字符串的结束。如下的代码,就是将pc指向了“string”所指代的常量存储区地址。
  char *pc = “string”;
这也是char *pc = “string”合法,而int *p =1不合法的原因:”string”本身即代表了它的存储地址,而整型常量1仅仅是个操作数,并不是地址,如果希望使用数据为指针(指向的地址)赋值,可以使用一个强制转换 int*p = (int*)1,只是这样如果不加以检查的话,写出来的代码会存在安全隐患。因此,不管指针变量是全局的还是局部的、静态的还是非静态的,都应该在声明它的同时进行初始化,要么赋予一个有效的地址,要么赋予NULL。
另外,声明一个指针,只是在栈区为指针本身的存储分配了地址,而不限制指针所指向的内存到底是在栈区、还是在堆区、还是在常量存储区。这也就造成了 函数调用返回值 会因实现不同而有不同意义,是函数调用结束后返回值有效性不同的原因。
2.5 空类型
C中有void关键字,但其实C中是并没有空类型的。比如我们不能做如下定义:
因为C、C++是静态类型的语言,定义变量就会分配内存。然而,不同类型的变量所占内存不同,如果定义一个任意类型的变量,就无法为其分配内存。所以,C、C++中没有任意类型的变量。
但是定义void p;是合法的,void 所定义的p表示以指针,所指向的类型未定。因为void *p;声明是为指针p分配了空间,无论指针指向什么类型,存储指针所需的空间的固定的,所以不存在因为所需空间大小而无法为p分配空间的问题。
但void p的使用也是很受限制的,由于不知道其指向的数据类型,所以是不能对p进行自增操作的;void的主要作用有两点,一个是限制函数的返回值,一个是限制函数的参数类型;void 则常用于指针的类型转换。如下代码:
  int *pi;
  float *pf;
如果想将pi指向pf所指向的内存空间,就必须进行类型转换:pi = (int *)。
而如果是将pi换成void *p,就不需要转换,可以直接为指针赋值。这样的直接赋值,只能是将一个已知类型的指针赋值给void *p,而不能是将void *p未加强制转换地赋值给一个已知类型的指针,如:
float *pf;
// pf =就是非法的,不能将 "void *" 类型的值分配到 "float *" 类型的实体
但需要注意的是,即使进行了转换,p仍然是个void*类型的指针,不能对其进行sizeof(p)等涉及所指类型的操作,同样地p也不能直接用于具体数据类型的操作。如下面的代码中*p = 1.73; 和printf(“%f”, *p)都是非法的:
float *pf;
*p = 1.73;
= 1.73;合法
printf("%f", *p);
//printf("%f", *pf); 合法
这样说来,void *的意义何在呢?可以使用强制类型转换使用void *p作为中介,见下面的代码:
  float *
  void *p;
  float f=1.6;
  p = (void*)&f;
  pf = (float*)p;
这样,float pf就指向了float f所在的地址,但注意p依然不能直接使用。这个例子,只是为我们展示了void 有这样的功能,但平常代码中很少这样无意义地转换,更多地是将void *作为函数参数,这样就可以接受任意类型的指针了,典型的如内存操作函数memcpy和memset的函数,其原型分别为:
  void * memcpy(void *dest, const void *src, size_t len);
  void * memset ( void * buffer, int c, size_t num );
也可以编写自己的将void 作为函数参数的函数,由于char是C中最小长度的变量,其它任何变量的长度都是它的整数倍。可以使用char作为中转,详见下面的函数实现:
void swap(void *pvData1, void *pvData2, int iDataSize)
unsigned char
*pcData1 = NULL;
unsigned char
*pcData2 = NULL;
unsigned char
pcData1 = (unsigned char *)pvData1;
pcData2 = (unsigned char *)pvData2;
= *pcData1;
= *pcData2;
pcData1++;
} while (--iDataSize & 0);
int main()
float fa = 1.23, fb = 2.32;
float *f1=&fa, *f2=&
int iDataSize = sizeof(float)/sizeof(char);
swap(f1, f2, iDataSize);
C中对NULL的预定义有两个:
  #define NULL
  #define NULL
((void *)0)
并且标准C规定,在初始化、赋值或比较时,如果一边是变量或指针类型的表达式,则编译器可以确定另一边的常数0为空指针,并生成正确的空指针值。即在指针上下文中“值为0的整型常量表达式”在编译时转换为空指针。那么也就是上面的两个的定义在指针上下文中是一致的了。
我们经常在声明一个指针时,为避免野指针的情况常用的int pi = NULL;中的NULL,是会被自动转换为(void )0的。所以下面的代码也是合法的:
int *pi = 0;
if(pi == 0){
2.6 函数类型 和 函数指针
尽管函数并不是变量,但它在内存中仍有其物理地址。每个函数都有一个入口地址,由函数名指向这个入口地址,函数名相当于一个指向其函数入口的指针常量。
可以将函数名赋值给一个指针,使该指针指向这个函数的入口,即是函数指针。
这里注意和指针函数区分开来:
指针函数是一个返回指针的函数,指针函数具体定义方式:
char *Convert(char *pName , int length);
函数指针的定义要和具体所指向的函数的形式一致,如对函数int Max(int a, int b)定义一个函数指针:
  int (*pMax)(int a, int b);
  pMax = M
int (pMax)(int a, int b)句中,函数指针pMax外的括号一定要带上,因为“()”的优先级高于“”,如果无括号,就变成了int *pMax(int a, int b)的形式,变成了一个函数(指针函数)的声明了。pMax=Max句将代表函数int Max(int a, int b)入口地址的其函数名Max,赋值给了指向同类型函数的指针pMax。这样pMax就和Max有相同的指代作用,并且pMax还可以指向与int Max(int a, int b)同参同返回值的函数。
通过括号强行将pMax首先与“*”结合,也就意味着,pMax是一个指针;接着与后面的“()”结合,说明该指针指向的是一个函数,然后再与前面的int结合,也就是说,该函数的返回值是int。由此可见,pfun是一个指向返回值为int的函数的指针。
  int Max(int a, int b);
  int Min(int a, int b);
  int (*p)(int a, int b);
   int max, min;
   p = M
   max = (*p)(3, 5);
   p = M
最后需要注意的是,由于函数在内存中的分布方式并不是齐整的,所以函数指针并没有++自增运算和—自减运算。
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:45533次
积分:2416
积分:2416
排名:第17297名
原创:206篇
评论:12条
(6)(3)(2)(7)(26)(30)(19)(58)(43)(12)
(window.slotbydup = window.slotbydup || []).push({
id: '4740887',
container: s,
size: '250,250',
display: 'inlay-fix'golang 调用C语言 Cgo
- Go语言中文网 - Golang中文社区
<meta name="author" content="polaris ">
golang 调用C语言 Cgo
· 2229 次点击 ·
开始浏览 & &
Cgo 使得Go程序能够调用C代码. cgo读入一个用特别的格式写的Go语言源文件, 输出Go和C程序, 使得C程序能打包到Go语言的程序包中.
举例说明一下. 下面是一个Go语言包, 包含了两个函数 -- Random 和 Seed -- 是C语言库中random和srandom函数的马甲.
package rand
#include &stdlib.h&
import &#34;C&#34;
func Random() int {
return int(C.random())
func Seed(i int) {
C.srandom(C.uint(i))
我们来看一下这里都有什么内容. 开始是一个包的导入语句.
rand包导入了&#34;C&#34;包, 但你会发现在Go的标准库里没有这个包. 那是因为C是一个&#34;伪包&#34;, 一个为cgo引入的特殊的包名, 它是C命名空间的一个引用.
rand 包包含4个到C包的引用: 调用 C.random和C.srandom, 类型转换 C.uint(i)还有引用语句.
Random函数调用libc中的random函数, 然后回返结果. 在C中, random返回一个C类型的长整形值, cgo把它轮换为C.long. 这个值必需转换成Go的类型, 才能在Go程序中使用. 使用一个常见的Go类型转换:
func Random() int {
return int(C.random())
这是一个等价的函数, 使用了一个临时变量来进行类型转换:
func Random() int {
var r C.long = C.random()
return int(r)
Seed函数则相反. 它接受一个Go语言的int类型, 转换成C语言的unsigned int类型, 然后传递给C的srandom函数.
func Seed(i int) {
C.srandom(C.uint(i))
需要注意的是, cgo中的unsigned int类型写为C. cgo的文档中有完整的类型列表.
这个例子中还有一个细节我们没有说到, 那就是导入语句上面的注释.
#include &stdlib.h&
import &#34;C&#34;
Cgo可以识别这个注释, 并在编译C语言程序的时候将它当作一个头文件来处理. 在这个例子中, 它只是一个include语句, 然而其实它可以是使用有效的C语言代码. 这个注释必需紧靠在import &#34;C&#34;这个语句的上面, 不能有空行, 就像是文档注释一样.
Strings and things
与Go语言不同, C语言中没有显式的字符串类型. 字符串在C语言中是一个以0结尾的字符数组.
Go和C语言中的字符串转换是通过C.CString, C.GoString,和C.GoStringN这些函数进行的. 这些转换将得到字符串类型的一个副本.
下一个例子是实现一个Print函数, 它使用C标准库中的fputs函数把一个字符串写到标准输出上:
package print
// #include &stdio.h&
// #include &stdlib.h&
import &#34;C&#34;
import &#34;unsafe&#34;
func Print(s string) {
cs := C.CString(s)
C.fputs(cs, (*C.FILE)(C.stdout))
C.free(unsafe.Pointer(cs))
在C程序中进行的内存分配是不能被Go语言的内存管理器感知的. 当你使用C.CString创建一个C字符串时(或者其它类型的C语言内存分配), 你必需记得在使用完后用C.free来释放它.
调用C.CString将返回一个指向字符数组开始处的指错, 所以在函数退出前我们把它转换成一个unsafe.Pointer(Go中与C的void 等价的东西), 使用C.free来释放分配的内存. 一个惯用法是在分配内存后紧跟一个defer(特别是当这段代码比较复杂的时候), 这样我们就有了下面这个Print函数:
func Print(s string) {
cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
C.fputs(cs, (*C.FILE)(C.stdout))
构建 cgo 包
如果你使用goinstall, 构建cgo包就比较容易了, 只要调用像平常一样使用goinstall命令, 它就能自动识别这个特殊的import &#34;C&#34;, 然后自动使用cgo来编译这些文件.
如果你想使用Go的Makefiles来构建, 那在CGOFILES变量中列出那些要用cgo处理的文件, 就像GOFILES变量包含一般的Go源文件一样.
rand包的Makefile可以写成下面这样:
include $(GOROOT)/src/Make.inc
TARG=goblog/rand
CGOFILES=\
include $(GOROOT)/src/Make.pkg
然后输入gomake开始构建.
更多 cgo 的资源
cgo的文档中包含了关于C伪包的更多详细的说明, 以及构建过程. Go代码树中的cgo的例子给出了更多更高级的用法.
一个简单而又符合Go惯用法的基于cgo的包是Russ Cox写的gosqlite. 而Go语言的网站上也列出了更多的的cgo包.
最后, 如果你对于cgo的内部是怎么运作这个事情感到好奇的话, 去看看运行时包的cgocall.c文件的注释吧.
2229 次点击 &
请尽量让自己的回复能够对别人有帮助
支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
支持 @ 本站用户;支持表情(输入 : 提示),见
图片支持拖拽、截图粘贴等方式上传
记住登录状态
Cgo 使得Go程序能够调用C代码. cgo读入一个用特别的格式写的Go语言源文件, 输出Go和C程序, 使得C程序能打包到Go语言的程序包中.
举例说明一下. 下面是一个Go语言包, 包含了两个函数 -- Random 和 Seed -- 是C语言库中random和srandom函数的马甲.
package rand
#include &stdlib.h&
import &#34;C&#34;
func Random() int {
return int(C.random())
func Seed(i int) {
C.srandom(C.uint(i))
我们来看一下这里都有什么内容. 开始是一个包的导入语句.
rand包导入了&#34;C&#34;包, 但你会发现在Go的标准库里没有这个包. 那是因为C是一个&#34;伪包&#34;, 一个为cgo引入的特殊的包名, 它是C命名空间的一个引用.
rand 包包含4个到C包的引用: 调用 C.random和C.srandom, 类型转换 C.uint(i)还有引用语句.
Random函数调用libc中的random函数, 然后回返结果. 在C中, random返回一个C类型的长整形值, cgo把它轮换为C.long. 这个值必需转换成Go的类型, 才能在Go程序中使用. 使用一个常见的Go类型转换:
func Random() int {
return int(C.random())
这是一个等价的函数, 使用了一个临时变量来进行类型转换:
func Random() int {
var r C.long = C.random()
return int(r)
Seed函数则相反. 它接受一个Go语言的int类型, 转换成C语言的unsigned int类型, 然后传递给C的srandom函数.
func Seed(i int) {
C.srandom(C.uint(i))
需要注意的是, cgo中的unsigned int类型写为C. cgo的文档中有完整的类型列表.
这个例子中还有一个细节我们没有说到, 那就是导入语句上面的注释.
#include &stdlib.h&
import &#34;C&#34;
Cgo可以识别这个注释, 并在编译C语言程序的时候将它当作一个头文件来处理. 在这个例子中, 它只是一个include语句, 然而其实它可以是使用有效的C语言代码. 这个注释必需紧靠在import &#34;C&#34;这个语句的上面, 不能有空行, 就像是文档注释一样.
Strings and things
与Go语言不同, C语言中没有显式的字符串类型. 字符串在C语言中是一个以0结尾的字符数组.
Go和C语言中的字符串转换是通过C.CString, C.GoString,和C.GoStringN这些函数进行的. 这些转换将得到字符串类型的一个副本.
下一个例子是实现一个Print函数, 它使用C标准库中的fputs函数把一个字符串写到标准输出上:
package print
// #include &stdio.h&
// #include &stdlib.h&
import &#34;C&#34;
import &#34;unsafe&#34;
func Print(s string) {
cs := C.CString(s)
C.fputs(cs, (*C.FILE)(C.stdout))
C.free(unsafe.Pointer(cs))
在C程序中进行的内存分配是不能被Go语言的内存管理器感知的. 当你使用C.CString创建一个C字符串时(或者其它类型的C语言内存分配), 你必需记得在使用完后用C.free来释放它.
调用C.CString将返回一个指向字符数组开始处的指错, 所以在函数退出前我们把它转换成一个unsafe.Pointer(Go中与C的void 等价的东西), 使用C.free来释放分配的内存. 一个惯用法是在分配内存后紧跟一个defer(特别是当这段代码比较复杂的时候), 这样我们就有了下面这个Print函数:
func Print(s string) {
cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
C.fputs(cs, (*C.FILE)(C.stdout))
构建 cgo 包
如果你使用goinstall, 构建cgo包就比较容易了, 只要调用像平常一样使用goinstall命令, 它就能自动识别这个特殊的import &#34;C&#34;, 然后自动使用cgo来编译这些文件.
如果你想使用Go的Makefiles来构建, 那在CGOFILES变量中列出那些要用cgo处理的文件, 就像GOFILES变量包含一般的Go源文件一样.
rand包的Makefile可以写成下面这样:
include $(GOROOT)/src/Make.inc
TARG=goblog/rand
CGOFILES=\
include $(GOROOT)/src/Make.pkg
然后输入gomake开始构建.
更多 cgo 的资源
cgo的文档中包含了关于C伪包的更多详细的说明, 以及构建过程. Go代码树中的cgo的例子给出了更多更高级的用法.
一个简单而又符合Go惯用法的基于cgo的包是Russ Cox写的gosqlite. 而Go语言的网站上也列出了更多的的cgo包.
最后, 如果你对于cgo的内部是怎么运作这个事情感到好奇的话, 去看看运行时包的cgocall.c文件的注释吧.
420 人在线
&最高记录 2325
&2013- Go语言中文网,中国 Golang 社区,致力于构建完善的 Golang 中文社区,Go语言爱好者的学习家园。
Powered by
&o&服务器由
赞助 &·&CDN 由
VERSION: V3.0.0&·&56.828825ms&·&为了更好的体验,本站推荐使用 Chrome 或 Firefox 浏览器
登录和大家一起探讨吧
记住登录状态
还不是会员& & 本文所讨论的&内存&主要指(静态)数据区、堆区和栈区空间(详细的布局和描述参考《》一文)。数据区内存在程序编译时分配,该内存的生存期为程序的整个运行期间,如全局变量和static关键字所声明的静态变量。函数执行时在栈上开辟局部自动变量的储存空间,执行结束时自动释放栈区内存。堆区内存亦称动态内存,由程序在运行时调用malloc/calloc/realloc等库函数申请,并由使用者显式地调用free库函数释放。堆内存比栈内存分配容量更大,生存期由使用者决定,故非常灵活。然而,堆内存使用时很容易出现内存泄露、内存越界和重复释放等严重问题。
& & 本文将详细讨论三种内存使用时常见的问题及其对策,并对各种内存问题给出简单的示例代码。示例代码的运行环境如下:
二& 内存问题
2.1 数据区内存
2.1.1 内存越界
& & &内存越界访问分为读越界和写越界。读越界表示读取不属于自己的数据,如读取的字节数多于分配给目标变量的字节数。若所读的内存地址无效,则程序立即崩溃;若所读的内存地址有效,则可读到随机的数据,导致不可预料的后果。写越界亦称&缓冲区溢出&,所写入的数据对目标地址而言也是随机的,因此同样导致不可预料的后果。
& & &内存越界访问会严重影响程序的稳定性,其危险在于后果和症状的随机性。这种随机性使得故障现象和本源看似无关,给排障带来极大的困难。
& & &数据区内存越界主要指读写某一数据区内存(如全局或静态变量、数组或结构体等)时,超出该内存区域的合法范围。
& & &写越界的主要原因有两种:1) memset/memcpy/memmove等内存覆写调用;2) 数组下标超出范围。
1 #define NAME_SIZE
2 #define NAME_LEN
NAME_SIZE-1/*Terminator*/
3 char gszName[NAME_SIZE] = "Mike";
4 char *pszName = "Jason";
5 int main(void)
memset(gszName, 0, NAME_SIZE+1); //越界1
gszName[NAME_SIZE] = 0;
if(strlen(pszName) &= NAME_SIZE)
//越界3(注意'='号)
strcpy(gszName, pszName);
int dwSrcLen = strlen(pszName);
if(dwSrcLen & NAME_SIZE)
memcpy(gszName, pszName, dwSrcLen); //未拷贝结束符('\0')
& & &使用数组时,经常发生下标&多1&或&少1&的操作,特别是当下标用于for循环条件表达式时。此外,当数组下标由函数参数传入或经过复杂运算时,更易发生越界。
1 void ModifyNameChar(unsigned char ucCharIdx, char cModChar)
gszName[ucCharIdx] = cModC
5 int main(void)
ModifyNameChar(NAME_SIZE, 'L');
unsigned char ucIdx = 0;
for(; ucIdx &= NAME_SIZE; ucIdx++)
//'='号导致读越界
printf("NameChar = %c\n", gszName[ucIdx]);
& & &对于重要的全局数据,可将其植入结构体内并添加CHK_HEAD和CHK_TAIL进行越界保护和检查:
1 #define CODE_SIZE
//越界保护码的字节数
2 #if (1 == CODE_SIZE)
#define CODE_TYPE
#define CHK_CODE
//除0外的特殊值
5 #elif (2 == CODE_SIZE)
#define CODE_TYPE
#define CHK_CODE
//除0外的特殊值
#define CODE_TYPE
#define CHK_CODE
0xABCDDCBA //除0外的特殊值
12 #define CHK_HEAD
CODE_TYPE ChkH
13 #define CHK_TAIL
CODE_TYPE ChkT
14 #define INIT_CHECK(ptChkMem) do{ \
(ptChkMem)-&ChkHead = CHK_CODE; \
(ptChkMem)-&ChkTail = CHK_CODE; \
17 }while(0)
18 #define CHK_OVERRUN(ptChkMem) do{ \
if((ptChkMem)-&ChkHead != CHK_CODE || (ptChkMem)-&ChkTail != CHK_CODE) { \
printf("[%s(%d)&%s&]Memory Overrun(ChkHead:0x%X,ChkTail:0x%X)!\n", __FILE__, __LINE__, FUNC_NAME, \
(ptChkMem)-&ChkHead, (ptChkMem)-&ChkTail); \
23 }while(0)
24 typedef struct{
char szName[NAME_SIZE];
28 }T_CHK_MEM;
29 T_CHK_MEM gtChkM
30 int main(void)
memset(&gtChkMem, 0, sizeof(T_CHK_MEM));
INIT_CHECK(&gtChkMem);
memset(&gtChkMem, 11, 6);
CHK_OVERRUN(&gtChkMem);
strcpy(gtChkMem.szName, "Elizabeth");
CHK_OVERRUN(&gtChkMem);
& & &执行结果如下,可见被检查的szName数组其头尾地址均发生越界:
1 [test.c(177)&main&]Memory Overrun(dwChkHead:0xB0B0B0B,dwChkTail:0xABCDDCBA)!
2 [test.c(179)&main&]Memory Overrun(dwChkHead:0xB0B0B0B,dwChkTail:0xABCD0068)!
& & &若模块提供有全局数据的访问函数,则可将越界检查置于访问函数内:
1 #ifdef CHK_GLOBAL_OVERRUN
#define CODE_SIZE
//越界保护码的字节数
#if (1 == CODE_SIZE)
#define CODE_TYPE
#define CHK_CODE
(CODE_TYPE)0xCC
//除0外的特殊值
#elif (2 == CODE_SIZE)
#define CODE_TYPE
#define CHK_CODE
(CODE_TYPE)0xCDDC
//除0外的特殊值
#define CODE_TYPE
#define CHK_CODE
(CODE_TYPE)0xABCDDCBA //除0外的特殊值
#define CHK_HEAD
CODE_TYPE ChkHead
#define CHK_TAIL
CODE_TYPE ChkTail
#define HEAD_VAL(pvGlblAddr)
(*(CODE_TYPE*)(pvGlblAddr))
#define TAIL_VAL(pvGlblAddr, dwGlbSize)
(*(CODE_TYPE*)((char*)pvGlblAddr+dwGlbSize-sizeof(CODE_TYPE)))
#define INIT_CHECK(pvGlblAddr, dwGlbSize) do{\
HEAD_VAL(pvGlblAddr) = TAIL_VAL(pvGlblAddr, dwGlbSize) = CHK_CODE;}while(0)
#define CHK_OVERRUN(pvGlblAddr, dwGlbSize, pFileName, dwCodeLine) do{\
if((HEAD_VAL(pvGlblAddr) != CHK_CODE) || (TAIL_VAL(pvGlblAddr, dwGlbSize) != CHK_CODE)) {\
printf("[%s(%d)]Memory Overrun(ChkHead:0x%X,ChkTail:0x%X)!\n", pFileName, dwCodeLine, \
HEAD_VAL(pvGlblAddr), TAIL_VAL(pvGlblAddr, dwGlbSize)); \
}}while(0)
#define INIT_GLOBAL(pvGlblAddr, dwInitVal, dwGlbSize) \
InitGlobal(pvGlblAddr, dwInitVal, dwGlbSize, __FILE__, __LINE__)
#define SET_GLOBAL(pvGlblAddr, pvGlblVal, dwGlbSize) \
SetGlobal(pvGlblAddr, pvGlblVal, dwGlbSize, __FILE__, __LINE__)
#define GET_GLOBAL(pvGlblAddr, pvGlblVal, dwGlbSize) \
GetGlobal(pvGlblAddr, pvGlblVal, dwGlbSize, __FILE__, __LINE__)
#define CHK_CODE
#define CHK_HEAD
#define CHK_TAIL
#define HEAD_VAL(pvGlblAddr)
#define TAIL_VAL(pvGlblAddr, dwGlbSize)
#define INIT_CHECK(pvGlblAddr, dwGlbSize)
#define CHK_OVERRUN(pvGlblAddr, dwGlbSize, pFileName, dwCodeLine)
#define INIT_GLOBAL(pvGlblAddr, dwInitVal, dwGlbSize) do{\
memset(pvGlblAddr, dwInitVal, dwGlbSize);}while(0)
#define SET_GLOBAL(pvGlblAddr, pvGlblVal, dwGlbSize) do{\
memcpy(pvGlblAddr, pvGlblVal, dwGlbSize);}while(0)
#define GET_GLOBAL(pvGlblAddr, pvGlblVal, dwGlbSize) do{\
memcpy(pvGlblVal, pvGlblAddr, dwGlbSize);}while(0)
49 void InitGlobal(void* pvGlblAddr, int dwInitVal, unsigned int dwGlbSize,
const char* pFileName, INT32U dwCodeLine)
if(NULL == pvGlblAddr) //理论上pFileName必不为空
printf("[%s(%d)]Arg1 Null!\n", pFileName, dwCodeLine);
memset(pvGlblAddr, dwInitVal, dwGlbSize);
INIT_CHECK(pvGlblAddr, dwGlbSize);
61 void SetGlobal(void* pvGlblAddr, void* pvGlblVal, unsigned int dwGlbSize,
const char* pFileName, INT32U dwCodeLine)
if((NULL == pvGlblAddr) || (NULL == pvGlblVal))
printf("[%s(%d)]Arg1(%p) or Arg2(%p) Null!\n", pFileName, dwCodeLine, pvGlblAddr, pvGlblVal);
memcpy(pvGlblAddr, pvGlblVal, dwGlbSize);
CHK_OVERRUN(pvGlblAddr, dwGlbSize, pFileName, dwCodeLine);
73 void GetGlobal(void* pvGlblAddr, void* pvGlblVal, unsigned int dwGlbSize,
const char* pFileName, INT32U dwCodeLine)
if((NULL == pvGlblAddr) || (NULL == pvGlblVal))
printf("[%s(%d)]Arg1(%p) or Arg2(%p) Null!\n", pFileName, dwCodeLine, pvGlblAddr, pvGlblVal);
memcpy(pvGlblVal, pvGlblAddr, dwGlbSize);
CHK_OVERRUN(pvGlblAddr, dwGlbSize, pFileName, dwCodeLine);
86 int main(void)
INIT_GLOBAL(&gtChkMem, 0, sizeof(T_CHK_MEM));
printf("[%d]ChkHead:0x%X,ChkTail:0x%X!\n", __LINE__, HEAD_VAL(&gtChkMem), TAIL_VAL(&gtChkMem, sizeof(T_CHK_MEM)));
T_CHK_MEM tChkM
GET_GLOBAL(&gtChkMem, &tChkMem, sizeof(T_CHK_MEM));
strcpy(tChkMem.szName, "Elizabeth");
SET_GLOBAL(&gtChkMem, &tChkMem, sizeof(T_CHK_MEM));
& & &其中,TAIL_VAL宏假定系统为1字节对齐(否则请置CODE_SIZE为4字节)。因0xCC默认为四字节(对应于0xFFFFFFCC),故需用(CODE_TYPE)0xCC做类型转换,否则CHK_OVERRUN宏内if判断恒为真。
& & &该检查机制的缺点是仅用于检测写越界,且拷贝和解引用次数增多,访问效率有所降低。读越界后果通常并不严重,除非试图读取不可访问的区域,否则难以也不必检测。
& & &数据区内存越界通常会导致相邻的全局变量被意外改写。因此若已确定被越界改写的全局变量,则可通过工具查看符号表,根据地址顺序找到前面(通常向高地址越界)相邻的全局数据,然后在代码中排查访问该数据的地方,看看有哪些位置可能存在越界操作。
& & &有时,全局数据被意外改写并非内存越界导致,而是某指针(通常为野指针)意外地指向该数据地址,导致其内容被改写。野指针导致的内存改写往往后果严重且难以定位。此时,可编码检测全局数据发生变化的时机。若能结合堆栈回溯(Call Backtrace),则通常能很快地定位问题所在。
& & &修改只读数据区内容会引发段错误(Segmentation Fault),但这种低级失误并不常见。一种比较隐秘的缺陷是函数内试图修改由指针参数传入的只读字符串,详见《》一文。
& & &因其作用域限制,静态局部变量的内存越界相比全局变量越界更易发现和排查。
& & 【对策】某些工具可帮助检查内存越界的问题,但并非万能。内存越界通常依赖于测试环境和测试数据,甚至在极端情况下才会出现,除非精心设计测试数据,否则工具也无能为力。此外,工具本身也有限制,甚至在某些大型项目中,工具变得完全不可用。
& & &与使用工具类似的是自行添加越界检测代码,如本节上文所示。但为求安全性而封装检测机制的做法在某种意义上得不偿失,既不及Java等高级语言的优雅,又损失了C语言的简洁和高效。因此,根本的解决之道还是在于设计和编码时的审慎周密。相比事后检测,更应注重事前预防。
& & &编程时应重点走查代码中所有操作全局数据的地方,杜绝可能导致越界的操作,尤其注意内存覆写和拷贝函数memset/memcpy/memmove和数组下标访问。
& & &在内存拷贝时,必须确保目的空间大于或等于源空间。也可封装库函数使之具备安全校验功能,如:
1 /******************************************************************************
2 * 函数名称:
3 * 功能说明:
带长度安全拷贝字符串
4 * 输入参数:
dwSrcLen : 目的字符串缓冲区长度
: 源字符串
dwSrcLen : 源字符串长度(含终止符'\0')
7 * 输出参数:
: 目的字符串缓冲区
8 * 返回值
成功: ptD 失败: "Nil"
9 * 用法示例:
char *pSrcStr = "HelloWorld"; char szDstStr[20] = {0};
StrCopy(szDstStr, sizeof(szDstStr), pSrcStr, strlen(pSrcStr))+1);
11 * 注意事项:
拷贝长度为min(dwDstLen, dwSrcLen) - 1{Terminator}
12 ******************************************************************************/
13 char *StrCopy(char *pDstStr, int dwDstLen, char *pSrcStr, int dwSrcLen)
if(((NULL == pDstStr) || (NULL == pSrcStr)) ||
((0 == dwDstLen) || (0 == dwSrcLen)))
return (char *)"Nil";
int dwActLen = (dwDstLen &= dwSrcLen) ? dwDstLen : dwSrcL
pDstStr[dwActLen - 1] = '\0';
return strncpy(pDstStr, pSrcStr, dwActLen - 1);
& & &在使用memcpy和strcpy拷贝字符串时应注意是否包括结束符(memcpy不自动拷贝&\0&)。
& & &按照下标访问数组元素前,可进行下标合法性校验:
1 /* 数组下标合法性校验宏 */
2 #define CHECK_ARRAY_INDEX(index, maxIndex) do{\
if(index & maxIndex) { \
printf("Too large "#index": %d(Max: %d)!!!\n\r", index, maxIndex); \
index = maxI \
7 }while(0)
2.1.2 多重定义
& & &函数和定义时已初始化的全局变量是强符号;未初始化的全局变量是弱符号。多重定义的符号只允许最多一个强符号。Unix链接器使用以下规则来处理多重定义的符号:
& & &规则一:不允许有多个强符号。在被多个源文件包含的头文件内定义的全局变量会被定义多次(预处理阶段会将头文件内容展开在源文件中),若在定义时显式地赋值(初始化),则会违反此规则。
& & &规则二:若存在一个强符号和多个弱符号,则选择强符号。
& & &规则三:若存在多个弱符号,则从这些弱符号中任选一个。
& & &当不同文件内定义同名(即便类型和含义不同)的全局变量时,该变量共享同一块内存(地址相同)。若变量定义时均初始化,则会产生重定义(multiple definition)的链接错误;若某处变量定义时未初始化,则无链接错误,仅在因类型不同而大小不同时可能产生符号大小变化(size of symbol `XXX' changed)的编译警告。在最坏情况下,编译链接正常,但不同文件对同名全局变量读写时相互影响,引发非常诡异的问题。这种风险在使用无法接触源码的第三方库时尤为突出。
& & &下面的例子编译链接时没有任何警告和错误,但结果并非所愿:
1 //test.c
2 int gdwCount = 0;
3 int GetCount(void)
return gdwC
9 //main.c
10 extern int GetCount(void);
11 int gdwC
12 int main(void)
gdwCount = 10;
printf("GetCount=%d\n", GetCount());
16 return 0;
& & &编码者期望函数GetCount的返回值打印出来是0,但其实是10。若将main.c中的int gdwCount语句改为int gdwCount = 0,编译链接时就会报告multiple definition of 'gdwCount'的错误。因此尽量不要依赖和假设这种符号规则。
& & &关于全局符号多重定义的讨论,详见《》一文。
& & 【对策】尽量避免使用全局变量。若确有必要,应采用静态全局变量(无强弱之分,且不会和其他全局符号产生冲突),并封装访问函数供外部文件调用。
2.1.3 volatile修饰
& & &关键字volatile用于修饰易变的变量,告诉编译器该变量值可能会在任意时刻被意外地改变,因此不要试图对其进行任何优化。每次访问(读写)volatile所修饰的变量时,都必须从该变量的内存区域中重新读取,而不要使用寄存器(CPU)中保存的值。这样可保证数据的一致性,防止由于变量优化而出错。
& & &以下几种情况通常需要volatile关键字:
外围并行设备的硬件寄存器(如状态寄存器);
中断服务程序(ISR)中所访问的非自动变量(Non-automatic Variable),即全局变量;
多线程并发环境中被多个线程所共享的全局变量。
& & &变量可同时由const和volatile修饰(如只读的状态寄存器),表明它可能被意想不到地改变,但程序不应试图修改它。指针可由volatile修饰(尽管并不常见),如中断服务子程序修改一个指向某buffer的指针时。又如:
1 //只读端口(I/O与内存共享地址空间,非IA架构)
2 const volatile char *port = (const volatile char *)0x01F7
& & &误用volatile关键字可能带来意想不到的错误,例如:
1 int CalcSquare(volatile int *pVal)
return (*pVal) * (*pVal);
4 } //deficient
& & &函数CalcSquare返回指针pVal所指向值的平方,但由于该值被volatile修饰,编译器将产生类似下面的代码:
1 int CalcSquare(volatile int *pVal)
int dwTemp1, dwTemp2;
dwTemp1 = *pV
dwTemp2 = *pV
return dwTemp1 * dwTemp2;
7 }//deficient
& & &多线程环境下,指针pVal所指向值在函数CalcSquare执行两次赋值操作时可能被意想不到地该变,因此dwTemp1和dwTemp2的取值可能不同,最终未必返回期望的平方值。
& & &正确的代码如下(使用全局变量的拷贝也是提高线程安全性的一种方法):
1 long CalcSquare(volatile int *pVal)
dwTemp = *pV
return dwTemp * dwT
6 }//deficient
& & &再举一例:
1 #define READ(val, addr)
(val = *(unsigned long *)addr)
& & &编译器优化这段代码时,若addr地址的数据读取太频繁,优化器会将该地址上的值存入寄存器中,后续对该地址的访问就转变为直接从寄存器中读取数据,如此将大大加快数据读取速度。但在并发操作时,一个进程读取数据,另一进程修改数据,这种优化就会造成数据不一致。此时,必须使用volatile修饰符。
& & 【对策】合理使用volatile修饰符。
2.2 栈区内存
2.2.1 内存未初始化
& & &未初始化的栈区变量其内容为随机值。直接使用这些变量会导致不可预料的后果,且难以排查。
& & &指针未初始化(野指针)或未有效初始化(如空指针)时非常危险,尤以野指针为甚。
& & 【对策】在定义变量时就对其进行初始化。某些编译器会对未初始化发出警告信息,便于定位和修改。
2.2.2 堆栈溢出
& & &每个线程堆栈空间有限,稍不注意就会引起堆栈溢出错误。注意,此处&堆栈&实指栈区。
1 #define MAX_SIZE
//系统不同该值不同(ulimit &s: 10240kbytes)
2 int main(void){
int aStackCrasher[MAX_SIZE] = {0};
//可能导致Segmentation fault
aStackCrasher[0] = 1;
& & &堆栈溢出主要有两大原因:1) 过大的自动变量;2) 递归或嵌套调用层数过深。
& & &有时,函数自身并未定义过大的自动变量,但其调用的系统库函数或第三方接口内使用了较大的堆栈空间(如printf调用就要使用2k字节的栈空间)。此时也会导致堆栈溢出,并且不易排查。
& & &此外,直接使用接口模块定义的数据结构或表征数据长度的宏时也存在堆栈溢出的风险,如:
1 typedef struct{
unsigned short wV
unsigned char aMacAddr[6];
unsigned char ucMacT
5 }T_MAC_ADDR_ENTRY;
6 typedef struct{
unsigned int dwTotalAddrN
T_MAC_ADDR_ENTRY tMacAddrEntry[MAX_MACTABLE_SIZE];
9 }T_MAC_ADDR_TABLE;
& & &上层模块在自行定义的T_MAC_ADDR_TABLE结构中,使用底层接口定义的MAX_MACTABLE_SIZE宏指定MAC地址表最大条目数。接口内可能会将该宏定义为较大的值(如8000个条目),上层若直接在栈区使用TABLE结构则可能引发堆栈溢出。
& & &在多线程环境下,所有线程栈共享同一虚拟地址空间。若应用程序创建过多线程,可能导致线程栈的累计大小超过可用的虚拟地址空间。在用pthread_create反复创建一个线程(每次正常退出)时,可能最终因内存不足而创建失败。此时,可在主线程创建新线程时指定其属性为PTHREAD_CREATE_DETACHED,或创建后调用pthread_join,或在新线程内调用pthread_detach,以便新线程函数返回退出或pthread_exit时释放线程所占用的堆栈资源和线程描述符。
& & 【对策】应该清楚所用平台的资源限制,充分考虑函数自身及其调用所占用的栈空间。对于过大的自动变量,可用全局变量、静态变量或堆内存代替。此外,嵌套调用最好不要超过三层。
2.2.3 内存越界
& & &因其作用域和生存期限制,发生在栈区的内存越界相比数据区更易发现和排查。
& & &下面的例子存在内存越界,并可能导致段错误:
1 int bIsUniCommBlv = 1;
2 int main(void)
char szWanName[] = "OAM_WAN_VOIP";
if(bIsUniCommBlv)
strcpy(szWanName, "OAM_WAN_MNGIP");
& & &但该例的另一写法则更为糟糕:
1 int bIsUniCommBlv = 1;
2 int main(void)
char szWanName[] = ""; //字符数组szWanName仅能容纳1个元素('\0')!
if(bIsUniCommBlv)
strcpy(szWanName, "OAM_WAN_MNGIP");
strcpy(szWanName, " OAM_WAN_VOIP");
& & &函数传递指针参数时也可能发生内存越界:
1 typedef struct{
int dwErrNo;
int aErrInfo[6];
4 }T_ERR_INFO;
5 int PortDftDot1p(int dwPort, int dwDot1p, void *pvOut)
int dwRet = 0;
T_ERR_INFO *ptErrInfo = (T_ERR_INFO *)pvO
//dwRet = DoSomething();
ptErrInfo-&dwErrNo
ptErrInfo-&aErrInfo[0] = dwP
return dwR
15 int main(void)
int dwOut = 0;
PortDftDot1p(0, 5, &dwOut);
& & &上例中,接口函数PortDftDot1p使用T_ERR_INFO结构向调用者传递出错信息,但该结构并非调用者必知和必需。出于隐藏细节或其他原因,接口将出参指针声明为void*类型,而非T_ERR_INFO*类型。这样,当调用者传递的相关参数为其他类型时,编译器也无法发现类型不匹配的错误。此外,接口内未对pvOut指针判空就进行类型转换,非常危险(即使判空依旧危险)。从安全和实用角度考虑,该接口应该允许pvOut指针为空,此时不向调用者传递出错信息(调用方也许并不想要这些信息);同时要求传入pvOut指针所指缓冲区的字节数,以便在指针非空时安全地传递出错信息。
& & &错误的指针偏移运算也常导致内存越界。例如,指针p+n等于(char*)p + n * sizeof(*p),而非(char*)p + n。若后者才是本意,则p+n的写法很可能导致内存越界。
& & &栈区内存越界还可能导致函数返回地址被改写,详见《》一文。
& & &两种情况可能改写函数返回地址:1) 对自动变量的写操作超出其范围(上溢);2) 主调函数和被调函数的参数不匹配或调用约定不一致。
& & &函数返回地址被改写为有效地址时,通过堆栈回溯可看到函数调用关系不符合预期。当返回地址被改写为非法地址(如0)时,会发生段错误,并且堆栈无法回溯:
1 Program received signal SIGSEGV, Segmentation fault.
2 0x in ?? ()
& & &这种故障从代码上看特征非常明显,即发生在被调函数即将返回的位置。
& & 【对策】与数据区内存越界对策相似,但更注重代码走查而非越界检测。
2.2.4 返回栈内存地址
& & &(被调)函数内的局部变量在函数返回时被释放,不应被外部引用。虽然并非真正的释放,通过内存地址仍可能访问该栈区变量,但其安全性不被保证。详见《》一文。
1 const static char *paMsgNameMap[] = {
"GetCurData",
"SetTable"
16 const static unsigned char ucMsgNameNum = sizeof(paMsgNameMap) / sizeof(paMsgNameMap[0]);
18 char *ParseOmciMsgType(unsigned char ucMsgType)
if(ucMsgType & ucMsgNameNum)
return paMsgNameMap[ucMsgType];
char szStrMsgType[sizeof("255")] = {0};
/* Max:"255" */
sprintf(szStrMsgType, "%u", ucMsgType);
return szStrMsgT
//编译警告:
& & &编译上述代码,函数ParseOmciMsgType在返回szStrMsgType处产生function returns address of local variable的警告。可将szStrMsgType定义为静态变量:
1 char *ParseOmciMsgType(unsigned char ucMsgType)
if(ucMsgType & ucMsgNameNum)
return paMsgNameMap[ucMsgType];
static char szStrMsgType[sizeof("255")] = {0}; /* Max:"255" */
sprintf(szStrMsgType, "%u", ucMsgType);
return szStrMsgT
& & &若将结果通过函数参数而非返回值传递,则代码会更为安全:
1 void ParseOmciMsgType(unsigned char ucMsgType, char *pszMsgType)
if(ucMsgType & ucMsgNameNum)
strcpy(pszMsgType, paMsgNameMap[ucMsgType]);
sprintf(pszMsgType, "%u", ucMsgType);
& & &注意,不可采用下面的写法:
1 void ParseOmciMsgType(unsigned char ucMsgType, char *pszMsgType)
if(ucMsgType & ucMsgNameNum)
pszMsgType = paMsgNameMap[ucMsgType];
sprintf(pszMsgType, "%u", ucMsgType);
& & &因为指针作为函数参数时,函数内部只能改变指针所指向地址的内容,并不能改变指针的指向。
& & &若线程在自身栈上分配一个数据结构并将指向该结构的指针传递给pthread_exit,则调用pthread_join的线程试图使用该结构时,原先的栈区内存可能已被释放或另作他用。
& & 【对策】不要用return语句返回指向栈内变量的指针,可改为返回指向静态变量或动态内存的指针。但两者都存在重入性问题,而且后者还存在内存泄露的危险。
2.3 堆区内存
2.3.1 内存未初始化
& & &通过malloc库函数分配的动态内存,其初值未定义。若访问未初始化或未赋初值的内存,则会获得垃圾值。当基于这些垃圾值控制程序逻辑时,会产生不可预测的行为。
& & 【对策】在malloc之后调用&memset&将内存初值清零,或使用&calloc代替malloc。
1 char *pMem = malloc (10);
2 memset(pMem, 0, 10); // memset前应对申请的动态内存做有效性检查
4 char *pMem = calloc (10, 1);
& & &注意,&memset函数按字节操作,且第三个参数以字节为单位。因此,若将内存初值全部置为1,则应使用memset(pMem, 0xFF, 10),而不是memset(pMem, 1, 10)。
2.3.2 内存分配失败
& & &动态内存成功分配的前提是系统具有足够大且连续可用的内存。内存分配失败的主要原因有:
& & &1) 剩余内存空间不足;
& & &2) 剩余内存空间充足,但内存碎片太多,导致申请大块内存时失败;
& & &3) 内存越界,导致malloc等分配函数所维护的管理信息被破坏。
& & &剩余内存空间不足的情况相对少见,通常发生在申请超大块内存时。例如:
1 #include &stdlib.h&
2 #include &errno.h&
3 #define ALLOC_BYTES
4 int main(void){
unsigned int dwRound = 0;
char *pMem = malloc(ALLOC_BYTES);
if(NULL == pMem){
printf("Alloc failed(%s)!\n", strerror(errno));
return -1;
printf("%d -& 0x%p\n", dwRound, pMem);
dwRound++;
& & &执行后产生内存分配失败的错误:
1 0 -& 0x77f6b008
2 1 -& 0x37f6a008
3 Alloc failed(Cannot allocate memory)!
& & &内存越界导致内存分配失败的情况更为常见。此时,可从分配失败的地方开始回溯最近那个分配成功的malloc,看附近是否存在内存拷贝和数组越界的操作。
& & &注意,调用malloc(0)时,某些库实现将其等同malloc(1)处理,某些则返回空指针。
& & 【对策】若申请的内存单位为吉字节(GigaByte),可考虑选用64位寻址空间的机器,或将数据暂存于硬盘文件中。此外,申请动态内存后,必须判断指向该内存的指针是否为NULL,并进行防错处理,比如使用return语句终止本函数或调用exit(1)终止整个程序的运行。
2.3.3 内存释放失败
& & &内存释放失败的主要原因有:
& & &1) 释放未指向动态内存的指针;
& & &2) 指向动态内存的指针在释放前被修改;
& & &3) 内存越界,导致malloc等分配函数所维护的管理信息被破坏;
& & &4) 内存重复释放(Double Free)。
& & &情况1属于低级错误,即指针并未执行malloc分配,却调用free释放该指针指向的内存。
1 int main(void)
int dwMem = 0; //具有迷惑性的变量名
int *pBuf = &dwM
free(pBuf);
9 //执行后报错:*** glibc detected *** ./test: free(): invalid pointer: 0xbf84b35c ***
& & &情况2多发生在从申请内存到最后释放跨越多个模块历经大量处理逻辑时,指针初始值被修改掉。简单示例如下:
1 int main(void)
char *pMem = malloc(10);
if(NULL == pMem)
return -1;
*pMem++; &//即*(pMem++),等效于*pM pMem++;
free(pMem);
12 //执行后报错:*** glibc detected *** ./test: free(): invalid pointer: 0x082b5009 ***
& & &内存越界也可能导致内存释放失败:
1 int main(void)
char *pMem = malloc(2);
if(NULL == pMem)
return -1;
memset(pMem, 0, sizeof(int)*10);
free(pMem);
11 //执行后报错:*** glibc detected *** ./test: free(): invalid next size (fast): 0x09efa008 ***
& & &内存重复释放最简单但最不可能出现的示例如下:
1 int main(void)
char *pMem = malloc(10);
if(NULL == pMem)
return -1;
free(pMem);
free(pMem);
12 //执行后报错:*** glibc detected *** ./test: double free or corruption (fasttop): 0x ***
& & &通常,编码者会封装接口以更好地管理内存的申请和释放。若释放接口内部在释放前未判断指向动态内存的指针是否为空,或释放后未将指向该内存的指针设置为空。当程序中调用关系或处理逻辑过于复杂(尤其是对于全局性的动态内存),难以搞清内存何时或是否释放,加之接口未作必要的防护,极易出现内存重复释放。
& & &此外,当程序中存在多份动态内存指针的副本时,很容易经由原内存指针及其副本释放同一块内存。
1 int main(void)
char *pMem = malloc(sizeof(char)*10);
if(NULL == pMem)
return -1;
char *pMemTemp = pM
//Do Something...
free(pMem);
free(pMemTemp);
& & &上例中仅需释放pMem或pMemTemp其一即可。
& & 【对策】幸运的是,内存释放失败会导致程序崩溃,故障明显。并且,可借助静态或动态的内存检测技术进行排查。
& & &对于重复释放,可仿照《》一文中介绍的SAFE_FREE宏,尽可能地&规避&其危害(但当内存指针存在多个副本时无能为力)。
1 #define SAFE_FREE(pointer)
SafeFree(&(pointer))
//与SAFE_ALLOC入参指针形式一致
2 void SafeFree(void **pointer)
if(pointer != NULL)
free(*pointer);
*pointer = NULL;
& & &此外,应在设计阶段保证数据结构和流程尽量地简洁合理,从根本上解决对象管理的混乱。
2.3.4 内存分配与释放不配对
& & &编码者一般能保证malloc和free配对使用,但可能调用不同的实现。例如,同样是free接口,其调试版与发布版、单线程库与多线程库的实现均有所不同。一旦链接错误的库,则可能出现某个内存管理器中分配的内存,在另一个内存管理器中释放的问题。此外,模块封装的内存管理接口(如GetBuffer和FreeBuffer)在使用时也可能出现GetBuffer配free,或malloc配FreeBuffer的情况,尤其是跨函数的动态内存使用。
& & 【对策】动态内存的申请与释放接口调用方式和次数必须配对,防止内存泄漏。分配和释放最好由同一方管理,并提供专门的内存管理接口。若不能坚持谁申请谁释放,则应进行协商或加代码注释说明。
2.3.5 内存越界
& & &除明显的读写越界外,关于动态内存还存在一种sizeof计算错误导致的越界:
1 int main(void)
T_CHK_MEM *pMem = malloc(sizeof(pMem));
if(NULL == pMem)
return -1;
memset(pMem, 0, sizeof(T_CHK_MEM));
free(pMem);
11 //执行后报错:*** glibc detected *** ./test: free(): invalid next size (fast): 0x ***
& & &这种越界也是内存释放失败的一个原因。正确的内存申请写法应该是:
1 T_CHK_MEM *pMem = malloc(sizeof(*pMem));
3 T_CHK_MEM *pMem = malloc(sizeof(T_CHK_MEM));
& & 【对策】当模块提供动态内存管理的封装接口时,可采用&红区&技术检测内存越界。例如,接口内每次申请比调用者所需更大的内存,将其首尾若干字节设置为特殊值,仅将中间部分的内存返回给调用者使用。这样,通过检查特殊字节是否被改写,即可获知是否发生内存越界。其结构示意图如下:
2.3.6 内存泄露
& & 内存泄漏指由于疏忽或错误造成程序未能释放已不再使用的内存。这时,内存并未在物理上消失,但程序因设计错误导致在释放该块内存之前就失去对它的控制权,从而造成内存浪费。只发生一次的少量内存泄漏可能并不明显,但内存大量或不断泄漏时可能会表现出各种征兆:如性能逐渐降低、全部或部分设备停止正常工作、程序崩溃以及系统提示内存耗尽。当发生泄漏的程序消耗过多内存以致其他程序失败时,查找问题的真正根源将会非常棘手。此外,即使无害的内存泄漏也可能是其他问题的征兆。
& & &短暂运行的程序发生内存泄漏时通常不会导致严重后果,但以下各种内存泄漏将导致较严重的后果:
?&& 程序运行后置之不理,并随着时间流逝不断消耗内存(如服务器后台任务,可能默默运行若干年);
?&& 频繁分配新的内存,如显示电脑游戏或动画视频画面时;
?&& 程序能够请求未被释放的内存(如共享内存),甚至在程序终止时;
?&& 泄漏发生在操作系统内部或关键驱动中;
?&& 内存受限,如嵌入式系统或便携设备;
?&& 某些操作系统在程序运行终止时并不自动释放内存,且一旦内存丢失只能通过重启来恢复。
& & &通常所说的内存泄漏指堆内存的泄漏。广义的内存泄漏还包括系统资源的泄漏(Resource Leak),而且比堆内存的泄漏更为严重。
& & &内存泄漏按照发生频率可分为四类:
& & &1) 常发性内存泄漏。即发生内存泄漏的代码被多次执行,每次执行都会泄漏一块内存。
& & &2) 偶发性内存泄漏。即发生内存泄漏的代码只发生在特定环境或操作下。特定的环境或操作下,偶发性泄漏也会成为常发性泄漏。
& & &3) 一次性内存泄漏。即发生内存泄漏的代码只执行一次,导致有且仅有一块内存发生泄漏。例如:
1 char* gpszFileName = NULL;
2 void SetFileName(const char* pszFileName)
if(gpszFileName != NULL)
free(gpszFileName);
gpszFileName = strdup(pszFileName);
9 int main(void)
SetFileName("test.c");
SetFileName("test.h");
& & &若程序结束时未释放gpszFileName指向的字符串,则即使多次调用SetFileName函数,也总有且仅有一块内存发生泄漏。
& & &4) 隐式内存泄漏。即程序在运行过程中不停地分配内存,但直到结束时才释放内存。例如,一个线程不断分配内存,并将指向内存的指针保存在一个数据存储(如链表)中。但在运行过程中,一直没有任何线程进行内存释放。或者,N个线程分配内存,并将指向内存的指针传递给一个数据存储,M个线程访问数据存储进行数据处理和内存释放。若N远大于M,或M个线程数据处理的时间过长,则分配内存的速度远大于释放内存的速度。严格地说这两种场景下均未发生内存泄漏,因为最终程序会释放所有已申请的内存。但对于长期运行(如服务器)或内存受限(如嵌入式)的系统,若不及时释放内存可能会耗尽系统的所有内存。
& & &内存泄漏的真正危害在于其累积性,这将最终耗尽系统所有的内存。因此,一次性内存泄漏并无大碍,因为它不会累积;而隐式内存泄漏危害巨大,因其相比常发性和偶发性内存泄漏更难检测。
& & &内存泄漏的主要原因有:
& & &1) 指向已申请内存的指针被挪作他用并被改写;
& & &2) 因函数内分支语句提前退出,导致释放内存的操作未被执行;
& & &3) 数据结构或处理流程复杂,导致某些应该释放内存的地方被遗忘;
& & &4) 试图通过函数指针参数申请并传递动态内存;
& & &5) 线程A分配内存,线程B操作并释放内存,但分配速度远大于释放速度。
& & &情况1属于低级错误,通常发生在同时管理多块动态内存时。
1 int main(void)
char *pPrevMem = malloc(sizeof(char)*10);
if(NULL == pPrevMem)
return -1;
char *pNextMem = malloc(sizeof(char)*10);
if(NULL == pNextMem)
return -1;
pNextMem = pPrevM
free(pNextMem);
& & &上例将指针pPrevMem赋值给指针pNextMem,从而导致pNextMem以前所指向的动态内存无法释放,因为已经丢失指向该位置的引用。
& & &情况2是最为常见的内存泄漏案例,尤其是在分支语句为异常和错误处理时。
1 int IsSthElseValid(void) {return 0; /*dummy*/}
2 int main(void)
char *pMem = malloc(sizeof(char)*10);
if(NULL == pMem)
return -1;
if(!IsSthElseValid())
return -2;
free(pMem);
& & &上例当函数IsSthElseValid()返回值不为真时,指针pMem指向的内存将就不被释放。通常程序在入口处分配内存,在出口处释放内存。但C函数可在任何地方退出,一旦某个出口处未释放应该释放的内存,就会发生内存泄漏。
& & &与之相似的是,为完成某功能需要连续申请一系列动态内存。但当某次分配失败退出时,未释放系列中其他已成功分配的内存。
& & &情况3多发生在内存挂接(分配的动态内存中某些元素又指向其他动态内存)时,容易出现仅释放父内存或先释放父内存后释放子内存的错误。
1 struct book{
szTitle[50];
szAuthor[40];
dwMask[4];
6 }gtRef = { .fPrice = 11.62,
.szAuthor = "F. Scott Fitzgerald",
.szTitle = "The Great Gatsby",
.dwMask[0 ... 3] = 1};
11 typedef struct{
int dwDataL
14 }T_DATA_BUF;
16 int main(void)
T_DATA_BUF *ptBuf = (T_DATA_BUF *)calloc(sizeof(T_DATA_BUF), 1);
ptBuf-&dwDataLen = sizeof(struct book);
ptBuf-&pData = (char *)calloc(ptBuf-&dwDataLen, 1);
memcpy(ptBuf-&pData, &gtRef, ptBuf-&dwDataLen);
struct book *ptBook = (struct book *)ptBuf-&pD
printf("Reference: '%s' by %s, $%.2f, [%d-%d-%d-%d].\n", ptBook-&szTitle,
ptBook-&szAuthor, ptBook-&fPrice, ptBook-&dwMask[0],
ptBook-&dwMask[1], ptBook-&dwMask[2], ptBook-&dwMask[3]);
free(ptBuf-&pData);
free(ptBuf);
& & &若只执行free(ptBuf)语句,则pData指向的子内存泄露;若先执行free(ptBuf)后执行free(ptBuf-&pData),则释放ptBuf所指内存后,该内存无效且ptBuf成为迷途指针,无法保证能通过pData释放子内存。当分配的挂接内存提供给外部使用时,很难保证调用者进行两次释放操作,并且顺序正确。
& & &在消息驱动通信中,同一消息的处理往往跨越多个模块。处于消息接收末端的模块,需要释放消息内的消息体。一旦忘记释放,在消息转发频繁时将不断泄露内存。这种错误从代码层面很难发现,需要设计时对流程有很强的理解。
& & &情况4根源在于对C语言函数参数传递方式(传值调用)的误解。
1 void GetMemory(char *pMem, int dwMemBytes)
pMem = (char *)malloc(sizeof(char) * dwMemBytes);
5 int main(void)
char *pStr = NULL;
GetMemory(pStr, 100);
//pStr仍为NULL
//strcpy(pStr, "hello");
//Segmentation fault
free(pStr);
& & &编译器为函数GetMemory的每个参数制作临时副本。假设指针参数pMem的副本是_ pMem,则编译器使_ pMem初值等于pMem。在函数体内修改_pMem的值(即所指的内存地址),并不会影响到pMem的取值。因此pMem仍为空指针,自然无法借此释放函数GetMemory内所申请的动态内存。若函数main中循环调用GetMemory&,则内存将不断泄露。
& & &若非要用指针参数申请内存,可改用指向指针的指针,或用函数返回值来传递动态内存。
1 void GetMemory(char **ppMem, int dwMemBytes){
*ppMem = (char *)malloc(sizeof(char) * dwMemBytes);
3 } //调用方式:GetMemory(&pStr, 100);
5 char *GetMemory(int dwMemBytes){
pMem = (char *)malloc(sizeof(char) * dwMemBytes);
8 } //调用方式:pStr = GetMemory(pStr, 100);
& & 【对策】设计时应规范各动态内存的用途及申请释放的流程,避免指针多用和忘记释放。
& & &函数内部若存在退出分支,则每个返回之前都应确保释放已成功分配的内存。
& & &对于挂接内存,应按照分配顺序反向遍历释放各子内存,最后释放父内存(最好能为其提供专门的分配和释放接口)。也可借助柔性数组特性来简化释放操作,尤其是当挂接内存提供给外部调用者使用时:
1 typedef struct{
int dwDataL
aData[0]; //Or 'char aData[]'
4 }T_DATA_BUF2;
5 int main(void)
T_DATA_BUF2 *ptBuf = (T_DATA_BUF2 *)calloc(sizeof(T_DATA_BUF)+sizeof(struct book), 1);
ptBuf-&dwDataLen = sizeof(struct book);
memcpy(ptBuf-&aData, &gtRef, ptBuf-&dwDataLen);
struct book *ptBook = (struct book *)ptBuf-&aD
printf("Reference: '%s' by %s, $%.2f, [%d-%d-%d-%d].\n", ptBook-&szTitle,
ptBook-&szAuthor, ptBook-&fPrice, ptBook-&dwMask[0],
ptBook-&dwMask[1], ptBook-&dwMask[2], ptBook-&dwMask[3]);
free(ptBuf);
& & &这种写法分配的内存连续,而且只需一次free即可释放,易用性更好。
& & &消息通信过程中,消息处理结束时必须释放消息内的消息体,异步通信时可由接收末端的模块释放,同步通信时可由发送前端的模块释放。这需要设计时规范消息的转发和处理流程。
& & &不要试图通过函数指针参数申请并传递动态内存,可改由二级指针或函数返回值传递。
& & &当程序代码庞杂且逻辑复杂时,可考虑增加内存泄漏检测机制。其基本原理是截获对内存分配和释放函数的调用,从而跟踪每块内存的生命周期。例如,每次成功分配一块内存后,将内存分配信息(如指向它的指针、文件名、函数名、行号和申请字节数等)加入一个全局链表中;每当释放一块内存时,再从链表中删除相应的内存信息。这样,当程序结束时,链表中剩余的内存信息结点就对应那些未被释放的内存。详细算法见《基于链表的C语言堆内存检测》一文。
& & &对于隐式内存泄露,可在程序运行过程中监控当前内存的总使用量和分配释放情况。以分配内存时的文件名和行号为索引,遍历链表结点即可计算出各处已分配但未释放的内存总量。若在连续多个时间间隔内,某文件中某行所分配的内存总量不断增长,则基本可确定属于隐式内存泄露(尤其是多线程引起的)。
& & &最后,频繁地调用库函数申请和释放内存效率较低,且易产生内存碎片。可采用内存池技术,以高效地管理和检测内存。设计和编码时应仔细分析需求,以减少不必要的动态内存使用。例如,解析定长的短消息内容时,就无需分配动态内存,定义固定长度的数组即可。
2.2.7 使用已释放堆内存
& & &动态内存被释放后,其中的数据可能被应用程序或堆分配管理器修改。不要再试图访问这块已被释放的内存,否则可能导致不可预料的后果。
1 int main(void)
char *pStr = (char *)malloc(100);
strcpy(pStr, "hello");
free(pStr);
if(pStr != NULL)
//出现迷途指针
strcpy(pStr, "world");
printf("%s\n", pStr);
& & &上例通常不会导致程序异常。但若使用迷途指针时,已释放的动态内存恰好被重新分配给其他数据,则strcpy语句可能造成意想不到的错误。除非法访问外,迷途指针还可能导致重复释放内存等故障。
& & &在多线程环境下,线程A通过异步消息通知线程B操作某块全局动态内存,通知后稍等片刻(以便线程B完成操作)再释放该内存。若延时不足无法保证其先操作后释放的顺序,则可能因访问已释放的动态内存而导致进程崩溃。
& & 【对策】务必保证已分配的内存块被且仅被释放一次,禁止访问指向已释放内存的指针。若该指针还存在多个副本,则必须保证当它所指向的动态内存被释放后,不再使用所有其他副本。
& & &避免上述错误发生的常用方法是释放内存后立即将对应的指针设置为空(NULL)。
2.3.8 误用realloc函数
& & &realloc函数极易误用,因此专门作为一节以强调其常见性。
& & &realloc函数原型为:
#include &stdlib.h&
void *realloc(void *ptr, size_t newsize);
& & &该函数将参数ptr指向的内存块更改为newsize字节。其行为因参数取值而变:
& & &1) 若参数ptr为空,则等同malloc(newsize),即分配一块newsize字节的内存。若成功返回该内存地址,否则返回NULL。
& & &2) 若参数newsize为0,则等同free(ptr),即释放ptr指向的内存块,并返回NULL。
& & &3) 若ptr指向的内存后面有足够的空闲且连续空间,则在原内存区位置上向高地址方向扩充,并返回原ptr指针值;若原内存区后面空间不足,则重新分配一块newsize字节的未初始化内存空间,将原内存区数据拷贝到新分配的内存区,然后自动释放原内存区,返回新分配的内存区首地址。若分配失败则返回NULL,且原内存块保持不变(不会释放或移动)。注意,若newsize值小于ptr所指向的原内存区长度,则原内存区尾部多出的oldsize-newsize字节内存可能会被释放(导致数据丢失)。
& & 【对策】使用realloc函数时需注意以下几点:
& & &1) 参数ptr指向的内存块必须位于堆区,即ptr必须是malloc/calloc/realloc的返回值,或者为NULL。否则运行时会报告"realloc(): invalid pointer"的错误。
& & &2) realloc函数分配内存时,返回的指针一定是适当对齐的,使其可用于任何数据对象。
& & &3) 若参数newsize为0,则realloc函数释放ptr所指的原内存块并返回NULL。此时ptr成为迷途指针,若不检查realloc返回值而直接使用ptr,会导致程序崩溃。
& & &4) 若newsize大于原内存区长度,则realloc函数可能释放ptr指向的原内存块并重新分配内存。此时ptr成为迷途指针,再次访问时会导致程序崩溃。正因为内存区域可能移动位置,所以不应使任何指针指向该区。
1 int main(void)
char *pMem1 = (char *)malloc(10);
if(NULL == pMem1)
return -1;
char *pMem2 = pMem1;
pMem1 = (char *)realloc(pMem2, 100);
if(NULL == pMem1)
return -1;
& & &上例中若realloc函数重新分配内存,则pMem2所记录的原pMem2内存区被自动释放,从而pMem2成为迷途指针。
& & &此外,若重新分配内存时失败(返回NULL),则调用者需调用free函数显式地释放ptr指向的原内存块。
& & &5) 不要将realloc函数返回结果再赋值给ptr,即避免使用ptr=realloc(ptr, newsize)。如:
1 int main(void)
char *pMem = (char *)malloc(10);
if(NULL == pMem)
return -1;
pMem = (char *)realloc(pMem, 100);
if(NULL == pMem)
return -1;
& & &若realloc函数分配内存失败,则pMem会变为空指针,从而丢失其原先指向的10字节内存空间(造成内存泄露)。为避免内存泄露,可将realloc函数返回值赋给pNewMem指针,成功分配内存后再将pNewMem指针值赋给pMem指针。
& & &循环调用realloc函数时,可先定义指针pMem并初始化为NULL。然后在循环体内将pMem作为入参调用realloc(pMem为空时等同malloc),并将返回值赋给指针pReMem,成功分配内存后再将pReMem指针值赋给pMem指针。这样,初次分配和再次分配都调用realloc,程序比较清晰健壮。该方式的实例可参考《》一文中的ReadLine函数。
& & &本文已详细讨论了三种内存使用时常见的问题及其对策。除设计和编码时加以注意外,还可借助内存检测工具(如Valgrind等)静态或动态地检查代码中的内存缺陷。但对于用户终端或大型工程,外部工具往往不可用,此时内置的内存检测代码就可派上用场。
& & &除本文所述内容外,设备或模块间通信还涉及内存对齐和字节顺序等问题。某一方(尤其是DLL库)增删接口结构体内成员或调整成员顺序时,若另一方忘记同步更新,则必然导致解析错误。
阅读(...) 评论()}

我要回帖

更多关于 var声明变量 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信