对于程序员,一般来说,我们可以简单将内存分为三个部分:静态区,栈,堆。

静态区:保存自动全局变量和static 变量(包括static 全局和局部变量)。静态区的内容在整个程序的生命周期内都存在,由编译器在编译的时候分配。

栈:保存局部变量。栈上的内容只在函数的范围内存在,当函数运行结束,这些内容也会自动被销毁。其特点是效率高,但空间大小有限。

堆:由malloc 系列函数或new 操作符分配的内存。其生命周期由free 或delete决定。在没有释放之前一直存在,直到程序结束。其特点是使用灵活,空间比较大,但容易出错。

 以上,紧密相关的一个词就是“生命周期”,以及变量的分类。所以,下面我们简单总结一下变量类型的内容。

一、变量类型

1.生命周期

变量的生命周期,也称生存期,是指变量值保留的期限。按照生命周期,可将变量分为两类:静态变量和动态变量。

静态变量:变量存储在内存中的静态存储区,在编译时就分配了存储空间,在整个程序运行期间,该变量占有固定的存储单元,变量的值都始终存在,程序结束后,这部分空间才释放。这类变量的生存期为整个程序。

动态变量:变量存储在内存中的动态存储区,在程序运行过程中,只有当变量所在函数被调用时,编译系统才临时为该变量分配一段内存单元,该变量才有值,函数调用结束,变量值立即消失,这部分空间释放。我们说这类变量的生存期仅在函数调用期间

 C语言中具有静态存储性质的变量:外部变量,静态局部变量和静态全局变量

C语言中具有动态存储性质的变量:自动变量(auto,默认可以不写),寄存器变量(register)

与上面的物理内存相对,我们很容易看出,C语言中,静态变量存储在静态区,动态变量存储在栈,由程序员自己动态分配的变量就存储在堆。

 2.作用域

变量的作用域也称为可见性,指变量的有效范围,可分为局部和全局两种。

局部变量:在一个函数或复合语句内定义的变量是局部变量,局部变量仅在定义它的函数或复合语句内有效。

全局变量:定义在所有函数之外的变量是全局变量,作用范围是从定义开始,到本文件或程序结束。

C语言中自动变量、寄存器变量和内部静态变量都属于局部变量;外部变量是程序级的全局变量,外部静态变量是源文件级的全局变量。

 3.C语言变量

从上面我们看到,C语言中的变量有:自动变量、寄存器变量、外部变量、内部静态变量和外部静态变量。下面分别就这几个变量进行简单说明。

3.1自动变量

auto:编译器在默认的缺省情况下,所有变量都是auto 的。

auto int a    等价于   int a

3.2寄存器变量

register:这个关键字请求编译器尽可能的将变量存在CPU 内部寄存器中,而不是内存中。因为如果一个变量在程序中频繁使用,如循环控制变量,大量访问内存就会影响程序的执行效率。注意是尽可能,不是绝对。一个CPU 的寄存器也就那么几个或几十个,如果在一个函数中定义的register 变量多于CPU中的寄存器数量,C编译程序会自动将寄存器变量转为自动变量。

使用register 修饰符的注意点

1)由于受硬件寄存器长度的限制,素以寄存器变量只能是char,int或指针型,只能用于说明函数中变量或函数中的形参。

2)由于register变量使用的是CPU中的寄存器,寄存器变量无地址,所以不能用取址运算符“&”来获取register变量的地址

3.3外部变量

外部变量的说明一般形式是:

extern  类型说明符   变量名

所谓“外部”是相对于函数内部而言的,C语言中的外部变量就是定义在所有函数之外的全局变量。

如果外部变量的定义和使用是在同一个文件中,则在该源文件中的函数在使用外部变量时,不需要再进行其他的说明,可直接使用。当外部变量的定义和使用在两个不同的源文件,若要使用其他源文件中定义的外部变量,就必须在使用该外部变量之前,就必须使用extern存储类型说明符进行变量的“外部”说明。

下面举个简单地小栗子:



文件1
//定义一个全局变量,并在testExtern中调用,
//测试是否在该变量定义的源文件下不需要用extern关键字进行变量声明
//在其他源文件下必须使用extern关键字声明才能使用
//同时注意变量声明的两个必备:extern关键字; 不显式赋值
int externVal = 1; 
//extern void printfExternVal();
void printfExternVal();
void printExternVal()
{
    printf("%-5d\n", externVal);  //%-5d  右空5格;%6d   左空6格
}
int _tmain(int argc, _TCHAR* argv[])
{
    printExternVal();
    printfExternVal();
    system("pause");
    return 0;
}



文件2
#include "stdafx.h"
#include <stdio.h>

//当将下面这行声明注释掉后
//会显示错误:未定义标识符“externVal”
extern int externVal;
void printfExternVal()
{
    printf("%6d\n", externVal);
}



这时候我们想在文件1中的main函数里调用文件2的函数,怎么办呢?通常我们看到在大的工程项目中,都是创建一个头文件,将文件2中函数的声明放在头文件中,然后文件1 #include<>这个头文件就可以用了。我们这里只是一个测试小程序,所以用不着牛刀杀鸡。

C语言中不仅有外部变量,而且有外部函数。当需要调用的函数在另一个源文件时,必须使用“extern”说明符说明被调用函数是外部函数。加粗部分是我摘抄C语言书上的原话,那么问题来了,我们发现,即使我们不加extern关键字,只在文件1中加简单地函数声明也是可以编译运行的,为什么呢?

以下Q&A摘自:

提问:【C语言】调用另一个源文件中的函数需要用extern关键字申明吗?

回答:函数声明主要是给链接器一个明确的hint,从而在匹配函数名字以后还能检查一下类型是否正确。至于extern关键字,对于函数声明本身是无所谓的,反正末尾一个分号编译器就懂了,能识别出来这是个声明而不是定义;只是对于变量的声明,没它就不行。C标准里是怎么要求的我不确定,不过建议是,对于本文件的函数不加extern,外部文件的加上,这样可以给读源码的人一个hint。p.s. stdio.h里的函数声明都是有extern的。

3.4静态变量

静态变量有两种:外部静态变量和内部静态变量

外部静态变量是全局变量,但作用域仅仅在定义它的那个源文件中,出了该源文件不管是否用extern说明都是不可见的。简单而言,外部静态变量仅仅作用于定义它的一个源文件,而外部变量作用于整个程序。

内部静态变量与自动变量有相似之处。内部静态变量也是局限于一个特定的函数内部,出了定义它的函数,即使对于同一个文件中的其他函数也是不可见的。但它不像自动变量那样,仅当定义自动变量的函数被调用时才存在,退出函数调用就消失。内部静态变量是始终存在的,当函数被调用退出后,内部静态变量会保存数值,再次调用该函数时,以前调用时的数值仍然保留着。

 二、内存管理

我们已经清楚C语言中各类变量的存储属性,以及对应存储在计算机中的什么区域,那么回答下面几个问题也轻而易举了。

1.什么是静态区越界,什么是栈越界,什么是堆越界?

2.为什么在静态或动态检测中我们常听到的是数组越界,缓冲区溢出,内存泄露,而不是我们问题1中的这些名词?

无论是静态区,栈还是堆,它们的越界都是指存储在这些位置(区域)上的变量出现了越界,那么问题来了,既然都是检测变量越界,按照不同变量进行分类不就好了,比如字符串越界,数组越界…为什么还会对在堆中存储的变量单独处理呢?尤其是对malloc()系列函数,double free,use after free,null dereference等等。因为静态区和栈的变量存储空间都是系统编译器分配和释放的,而堆中的存储空间是程序员分配和释放的,为编写程序增加灵活的同时,也增添了风险,所以针对堆,就有了不同于栈和静态区的其他可能的缺陷,也就被提出来另当别论了。

下面我们再来解释一下缓冲区溢出到底包含多少内容。

计算机程序一般都会使用到一些内存,这些内存或是程序内部使用,或是存放用户的输入数据,这样的内存一般称作缓冲区。溢出是指盛放的东西超出容器容量而溢出来了,在计算机程序中,就是数据使用到了被分配内存空间之外的内存空间。而缓冲区溢出,简单的说就是计算机对接收的输入数据没有进行有效的检测(理想的情况是程序检查数据长度并不允许输入超过缓冲区长度的字符),向缓冲区内填充数据时超过了缓冲区本身的容量,而导致数据溢出到被分配空间之外的内存空间,使得溢出的数据覆盖了其他内存空间的数据。

所以从百度百科摘下来的这段话表明,缓冲区溢出可以换一种说法,叫做存储数据的某一部分内存溢出,而内存的范围就是我们上面说到的静态区、栈和堆,也就是说缓冲区溢出包含了溢出问题这一大类,即我们上面所说的静态区越界,栈越界,堆越界。

明白了以上的理论基础知识,我们就可以对我们要解决的问题做很好的分类,下面我们简单看一些常见的缓冲区溢出错误,以及堆内存泄露方面的系列缺陷

 1.导致缓冲区溢出的常见 C 和 C++ 错误(摘录)

从根本上讲,在程序将数据读入或复制到缓冲区中的任何时候,它需要在复制之前检查是否有足够的空间。能够容易看出来的异常就不可能会发生

遗憾的是,C 和 C++ 附带的大量危险函数(或普遍使用的库)甚至连这点(指检查空间)也无法做到。程序对这些函数的任何使用都是一个警告信号,因为除非慎重地使用它们,否则它们就会成为程序缺陷。您不需要记住这些函数的列表;我的真正目的是说明这个问题是多么普遍。这些函数包括 strcpy(3)、strcat(3)、sprintf(3) (及其同类 vsprintf(3) )和 gets(3) 。 scanf() 函数集( scanf(3)、fscanf(3)、sscanf(3)、vscanf(3)、vsscanf(3) 和 vfscanf(3) )可能会导致问题,因为使用一个没有定义最大长度的格式是很容易的(当读取不受信任的输入时,使用格式“%s”总是一个错误)。

其他危险的函数包括 realpath(3)、getopt(3)、getpass(3)、streadd(3)、strecpy(3) 和 strtrns(3) 。 从理论上讲, snprintf() 应该是相对安全的 ―― 在现代 GNU/Linux 系统中的确是这样。但是非常老的 UNIX 和 Linux 系统没有实现 snprintf() 所应该实现的保护机制。

Microsoft 的库中还有在相应平台上导致同类问题的其他函数(这些函数包括 wcscpy()、_tcscpy()、_mbscpy()、wcscat()、_tcscat()、_mbscat() 和 CopyMemory() )。注意,如果使用 Microsoft 的 MultiByteToWideChar() 函数,还存在一个常见的危险错误 ―― 该函数需要一个最大尺寸作为字符数目,但是程序员经常将该尺寸以字节计(更普遍的需要),结果导致缓冲区溢出缺陷。

另一个问题是 C 和 C++ 对整数具有非常弱的类型检查,一般不会检测操作这些整数的问题。由于它们要求程序员手工做所有的问题检测工作,因此以某种可被利用的方式不正确地操作那些整数是很容易的。特别是,当您需要跟踪缓冲区长度或读取某个内容的长度时,通常就是这种情况。但是如果使用一个有符号的值来存储这个长度值会发生什么情况呢 ―― 攻击者会使它“成为负值”,然后把该数据解释为一个实际上很大的正值吗?当数字值在不同的尺寸之间转换时,攻击者会利用这个操作吗?数值溢出可被利用吗? 有时处理整数的方式会导致程序缺陷。

所以,我们发现缓冲区溢出大部分时候是由未知长度的字符串造成的,之后的博客中我们会继续温习字符串的知识,并总结这些由字符串导致的缓冲区溢出问题。

 2.常见其他内存错误及对策

2.1指针没有指向一块合法的内存

定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存。

1)结构体成员指针未初始化



struct student
{
    char *name;
    int score;
}stu, *pstu;

//结构体中的指针成员namw未初始化
//定义结构体变量stu时,为指针变量name分配了4字节的内存,存放一个指向字符的地址
//但并没有给name初始化。因此name中存放的是乱码,而这个乱码在后面会被理解为一个地址
//并在对应的该地址下存储字符串“Jimy”
int structPMemberTest_UP()
{
    strcpy(stu.name, "Jimy");
    stu.score = 99;
    return 0;
}



很多初学者犯了这个错误还不知道是怎么回事。这里定义了结构体变量stu,但是他没想到这个结构体内部char *name 这成员在定义结构体变量stu 时,只是给name 这个指针变量本身分配了4 个字节。name 指针并没有指向一个合法的地址,这时候其内部存的只是一些乱码。所以在调用strcpy 函数时,会将字符串"Jimy"往乱码所指的内存上拷贝,而这块内存name 指针根本就无权访问,导致出错。解决的办法是为name 指针malloc 一块空间。同样,也有人犯如下错误:



//同上面的错误是一样的
//虽然定义结构体时,malloc了内存空间,可那是存储结构体的
//name内部值仍然是乱码
int structPMemberTest_P()
{
    pstu = (struct student*)malloc(sizeof(struct student));
    strcpy(pstu->name, "Jimy");
    pstu->score = 99;
    free(pstu);
    return 0;
}



为指针变量pstu 分配了内存,但是同样没有给name 指针分配内存。错误与上面第一种情况一样,解决的办法也一样。这里用了一个malloc 给人一种错觉,以为也给name 指针分配了内存。

2)没有为结构体指针分配足够的内存

 



//没有为结构体指针分配足够的内存
//struct student* 表示定义了一个结构体student 的指针变量,只有4个字节
//当然name指针同样没有被分配内存
int enoughSizeForStruct()
{
    pstu = (struct student*)malloc(sizeof(struct student*));
    strcpy(pstu->name, "Jimy");
    pstu->score = 99;
    free(pstu);
    return 0;
}



 

为pstu 分配内存的时候,分配的内存大小不合适。这里把sizeof(struct student)误写为sizeof(struct student*)。当然name 指针同样没有被分配内存。解决办法同上。

3)函数的入口校验

不管什么时候,我们使用指针之前一定要确保指针是有效的。

一般在函数入口处使用assert(NULL != p)对参数进行校验。在非参数的地方使用if(NULL != p)来校验。但这都有一个要求,即p 在定义的同时被初始化为NULL 了。比如上面的例子,即使用if(NULL != p)校验也起不了作用,因为name 指针并没有被初始化为NULL,其内部是一个非NULL 的乱码。

assert 是一个宏,而不是函数,包含在assert.h 头文件中。原型定义:



#include <assert.h>
void assert( int expression );



如果其后面括号里expression的值为假(即为0),则程序终止运行,并提示出错;如果后面括号里的值为真,则继续运行后面的代码。这个宏只在Debug 版本上起作用,而在Release 版本被编译器完全优化掉,这样就不会影响代码的性能。举个例子:



//函数入口出的参数校验,宏assert
//#include <assert.h>     void assert( int expression );  
//assert翻译成中文,有断言的意思,就是我保证
//所以使用assert,一般是在十分确定就是这样的情况下
//参数定义为const的只读类型 readonly
char* clone_string(const char *source)
{
    char *result = NULL;
    assert(source != NULL);  //如果括号内表达式为假(0),程序停止运行
    result = (char   *)malloc(strlen(source) + 1);
    if (result != NULL)
    {
        strcpy(result, source);
        assert(strcmp(result, source) == 0);
    }
    return   result;
}



2.2为指针分配的内存太小

为指针分配了内存,但是内存大小不够,导致出现越界错误。

2.3内存分配成功,但并未初始化

犯这个错误往往是由于没有初始化的概念或者是以为内存分配好之后其值自然为0。未初始化指针变量也许看起来不那么严重,但是它确确实实是个非常严重的问题,而且往往出现这种错误很难找到原因。所以在定义一个变量时,第一件事就是初始化。你可以把它初始化为一个有效的值,比如:



int i = 10;
char *p = (char *)malloc(sizeof(char));



但是往往这个时候我们还不确定这个变量的初值,这样的话可以初始化为0 或NULL。



int i = 0;
char *p = NULL;



如果定义的是数组的话,可以这样初始化:



int a[10] = {0};



或者用memset 函数来初始化为0:


memset(a,0,sizeof(a));



memset 函数有三个参数,第一个是要被设置的内存起始地址;第二个参数是要被设置的值;第三个参数是要被设置的内存大小,单位为byte。指针变量如果未被初始化,会导致if 语句或assert 宏校验失败。

2.4内存越界

内存分配成功,且已经初始化,但是操作越过了内存的边界。这种错误经常是由于操作数组或指针时出现“多1”或“少1”。

2.5内存泄漏

会产生泄漏的内存就是堆上的内存(这里不讨论资源或句柄等泄漏情况),也就是说由malloc 系列函数或new 操作符分配的内存。如果用完之后没有及时free 或delete,这块内存就无法释放,直到整个程序终止。

1)用malloc 函数申请0 字节内存

有一个问题:用malloc 函数申请0 字节内存会返回NULL 指针吗?

可以测试一下,也可以去查找关于malloc 函数的说明文档。申请0 字节内存,函数并不返回NULL,而是返回一个正常的内存地址。但是你却无法使用这块大小为0 的内存。这就像尺子上的某个刻度,刻度本身并没有长度,只有某两个刻度一起才能量出长度。对于这一点一定要小心,因为这时候if(NULL != p)语句校验将不起作用。

2)double free 和 no free
3)Use after free

既然使用free 函数之后指针变量p 本身保存的地址并没有改变,那我们就需要重新把p的值变为NULL,否则,在free(p)之后,你用if(NULL != p)这样的校验语句也毫无作用。例如:



char *p = (char *)malloc(100);
strcpy(p, “hello”);
free(p); /* p 所指的内存被释放,但是p 所指的地址仍然不变*/
if (NULL != p)
{
    /* 没有起到防错作用*/
    strcpy(p, “world”); /* 出错*/
}



释放完一块内存之后,没有把指针置NULL,这个指针就成为了“野指针”,也有书叫“悬挂指针”。这是很危险的,而且也是经常出错的地方。所以一定要记住一条:free 完之后,一定要给指针置NULL。

三、动态内存分配相关函数及操作符

1.Malloc


2.Free

free() 函数用来释放动态分配的内存空间,其原型为:



#include <stdlib.h>
void free (void* ptr);



【参数说明】ptr 为将要释放的内存空间的地址。

free() 可以释放由 malloc()、calloc()、realloc() 分配的内存空间,以便其他程序再次使用。free() 只能释放动态分配的内存空间,并不能释放任意的内存。下面的写法是错误的:



int a[10];
free(a);



如果 ptr 所指向的内存空间不是由上面的三个函数所分配的,或者已被释放,那么调用 free() 会有无法预知的情况发生。如果 ptr 为 NULL,那么 free() 不会有任何作用。

注意:free() 不会改变 ptr 变量本身的值,调用 free() 后它仍然会指向相同的内存空间,但是此时该内存已无效,不能被使用。所以建议将 ptr 的值设置为 NULL,

3. C++ 中的New 和 delete


 

参考文章及书籍

《C语言程序设计教程》  李凤霞   北京理工大学出版社

《C语言深度剖析》     陈正冲

https://www.ibm.com/developerworks/cn/linux/l-sp/part4/

http://c.biancheng.net/cpp/html/135.html