C语言变量类型与内存管理

对于程序员,通常来讲,咱们能够简单将内存分为三个部分:静态区,栈,堆。html

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

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

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

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

1、变量类型

1.生命周期

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

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

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

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

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摘自:https://segmentfault.com/q/1010000000249480

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

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

3.4静态变量

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

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

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

 2、内存管理

咱们已经清楚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)参数进行校验。在非参数的地方使用ifNULL != p)来校验。但这都有一个要求,即p 在定义的同时被初始化为NULL 了。好比上面的例子,即便用ifNULL != 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 = 10char *p = (char *)malloc(sizeof(char));

可是每每这个时候咱们还不肯定这个变量的初值,这样的话能够初始化为0 NULL

int i = 0char *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 的内存。这就像尺子上的某个刻度,刻度自己并无长度,只有某两个刻度一块儿才能量出长度。对于这一点必定要当心,由于这时候ifNULL = p)语句校验将不起做用。

2)double free no free

3)Use after free

既然使用free 函数以后指针变量p 自己保存的地址并无改变,那咱们就须要从新把p的值变为NULL,不然,在freep)以后,你用ifNULL = p)这样的校验语句也毫无做用。例如:

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

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

3、动态内存分配相关函数及操做符

1.Malloc

http://www.cnblogs.com/wangyuxia/p/6115262.html

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

http://www.cnblogs.com/hazir/p/new_and_delete.html

 

参考文章及书籍

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

C语言深度剖析》     陈正冲

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

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

http://www.cnblogs.com/hazir/p/new_and_delete.html

相关文章
相关标签/搜索