C的变量类型、作用域与生命周期的总结

最近在看“C Programing Language" (Kernighan, Ritchie)关于外部变量的讨论,之前在学C的时候对这些extern, auto, static, register等不是太理解,这本书讲的很详细,现在总结一下。

首先, C的变量分成局部变量 local variable 和全局变量 global variable。

【注】

  1. C 中局部(local)变量(也有翻译成本地变量),也可以叫做内部(internal)变量

  2. C 中全局(global)变量,又可叫外部(external)变量。

这些称呼都可以互换,不同的称呼可能强调的是不同的特性,以下尽量用对应使用的关键字来称呼,比如局部变量将用自动变量(auto)来称呼,全局变量将用外部变量(extern)来称呼。

C语言程序可以看成是由许多外部对象构成的,这些外部对象可以是变量或函数。外部(external)和内部(internal)是相对的,internal是用来描述在函数内部的函数参数或变量,external描述的是定义在函数外部的变量。由于C语言不允许在函数内部定义函数,因此函数都可以看成是外部的(extern)。

栗子:

#include <stdio.h>
#define MAX 100
char s[MAX];
void printString(void)
{
printf("%s", s);
} int main(void)
{
scanf("%s", s);
printString(s);
return 0;
}

这个简单的printString.c源文件即可看成是由三个外部对象构成:外部变量s, 外部函数printString()main()组成,还有一个include的标准库文件stdio.h(这个下次再谈)。

默认情况下,外部变量与函数具有以下性质:通过同一个名字引用的所有外部变量(即使这种引用来自单独编译的不同函数)实际上都是引用内存中同一个对象。

因为外部变量可以在全局范围内访问,这就为函数之间的数据交换提供一种新的方式,可以代替函数参数与返回值,如上一个栗子。任何函数都可以通过名字访问一个外部变量,当然这个名字需要通过某种方式来声明。

外部变量与自动变量的作用域与生命周期

  • 变量或者符号的作用域是指程序中可以使用这个变量或名字的范围。这个可以看成是静态的代码范围
  • 变量生命周期则是指该变量存在的时间范围。这个可以理解为程序运行时的变量存在的时间周期。

自动变量只能在函数内部使用,作用域从声明处开始直至函数结束,生命周期是从其所在的函数被调用时,变量开始存在,在函数退出时变量将消失。对于在函数开头声明的自动变量,其作用域即为声明该变量名的函数内部,函数的参数也是如此,实际上可以将它看作是这个函数的局部变量。当然,自动变量也可以定义在函数内的语句块中,比如在下面的for循环中定义的临时变量temp:

int func(void)
{
int i;
for(i = 0; i < 10; i++)
{
int temp;
...
}
}

这种变量当然作用域是限定在语句块内部,生命周期也是在该语句块内部,当程序执行完该语句块,变量也就消失了。

外部变量作用域为从其定义处开始直至所在的文件的结尾结束,生命周期是永久存在的,即程序执行期间一直存在,它们的值在一次函数调用结束到下一次函数调用开始之前保持不变。另一方面,可以通过添加声明的方式来使用外部变量,按照上面所说,外部变量是唯一的,因此添加extern声明可以扩展外部变量的作用域到其他文件中。

如果要在外部变量的定义之前使用该变量,或者外部变量的定义与变量的使用不在同一个文件中,则必须在相应的变量声明中强制使用关键字extern

将外部变量的定义与声明区别开是很有必要的,外部变量的声明用于说明变量的属性(主要是类型),而外部变量的定义除此之外还会引起内存的分配(在定义后编译程序将为它分配内存单元)。

而自动变量则不然,自动变量在C中没有定义这一说法,只要先声明再使用即可,这是因为自动变量(即局部变量)是在运行时由栈来管理的,而外部变量(即全局变量)是在编译过程中由编译器、汇编器分配存储地址,一直到链接时确定内存位置(这些内容将在之后会专门总结有关编译、汇编、链接的内容),在这里都可以理解为在编译时即分配了内存单元。

栗子:如果将下面这两条语句放在所有函数的外部:

int a;
double b[MAX];

则这两条语句将定义外部变量a与b,并为之分配内存,同时这两条语句还可以作为该源文件中其余部分的声明。而下面的两行语句:

extern int a;
extern double b[];

为所在文件该语句之后的部分声明了一个int类型的外部变量a以及一个double类型的外部变量b(该数组的长度在其他地方确定),但这两个声明并没有建立变量或为他们分配内存。

在程序的源文件中,一个外部变量只能在某个文件中定义一次,而其他文件可以通过extern声明来访问它(定义外部变量的源文件中也可以包含对该外部变量的extern声明)。外部变量的定义必须指定数组的长度,但extern声明则不一定要指定数组的长度。外部变量的初始化只能出现在其定义中(注:若外部变量未初始化,编译器将它初始化为0)。

【注】外部变量的声明也可以通过上下文隐式声明(即如上所说,定义即可作为之后语句的声明)如下面程序版本2中的外部变量lenbuf在main函数中就无需在main中再声明。

版本1:

#include <stdio.h>
#define MAXLENGTH 1000 // buffer最大长度 char buf[MAXLENGTH]; int getline(void); /*一个简单的copy-paste程序
*/
int main(void)
{
while (getline()!=EOF)
{
printf("%s\n", buf);
}
return 0;
} int getline(void)
{
int c, i;
extern char buf[MAXLENGTH]; i= 0;
while ( i < MAXLENGTH && (c = getchar()) != EOF && c != '\n')
buf[i++] = c;
if (c = '\n')
buf[i++] = c;
buf[i] = '\0';
return i;
}

版本2:

#include <stdio.h>
#define MAXLENGTH 1000 // buffer最大长度 char buf[MAXLENGTH]; int getline(void); /*一个简单的copy-paste程序
*/
int main(void)
{
while (getline()!=EOF)
{
printf("%s\n", buf);
}
return 0;
} int getline(void)
{
int c, i;
// 通过上下文隐式声明 buf[] i= 0;
while ( i < MAXLENGTH && (c = getchar()) != EOF && c != '\n')
buf[i++] = c;
if (c = '\n')
buf[i++] = c;
buf[i] = '\0';
return i;
}

静态变量与寄存器变量

静态变量

之前已经提到了,外部变量与自动变量,其中外部变量是可以被全局使用的,这个全局指的是整个源程序的所有源文件都可以通过添加extern声明来使用。但是,如果我们希望限定这个外部变量仅限于该定义的源文件使用,而不希望被其他源文件使用。那我们可以使用static声明限定外部变量和函数,可以将其声明的对象的作用域限定为该源文件的剩余部分。通过static限定外部对象,可以达到隐藏外部对象的目的。

static char buf[BUFSIZE];
static int bufp = 0;

变量声明为static之后,该变量即为静态存储,其他文件中的函数就不可以访问变量buf, bufp,因此这两个名字就不会和同一程序中的其他文件中相同名字的变量相冲突。

【注】:多个函数中的自动变量同名,也不会造成冲突,因为在编译过程中,编译器会将自动变量改成不同名字,比如加上函数名,具体做法依赖于编译器版本。

外部的static声明多用于变量,当然,也可以用于声明函数。通常情况下,函数名是全局可访问的,对整个程序的各个部分都是可见的。但是,如果把函数声明为static类型,则该函数除了对该函数声明所在的文件可见外,其他文件都无法访问。

static也可用于声明自动变量,static类型的自动变量同一般的自动变量一样,是某个特定函数内的局部变量,只能在该函数中使用。但它与一般的自动变量不同的是,不管其所在函数是否被调用,它一直存在。换句话说,static类型的内部变量作用域不变,生命周期和外部变量一样为整个程序运行期间。

寄存器变量

register声明告诉编译器,它所声明的变量在程序中使用频率较高。其思想史,将register变量放在机器的寄存器中,这样可以使程序更小,执行速度更快。

register声明的形式如下:

register int x;
register char c;

register声明只适用于自动变量以及函数的形式参数,看下面的例子:

void f(register unsigned a, register long n)
{
register int i;
...
}

实际使用的时候,底层硬件环境会对寄存器变量的使用有一些限制。每个函数中只有很少的变量可以保存在寄存器中,且只允许某些类型的变量。但是,过量的寄存器变量并没有什么害处,因为编译器可以忽略过量的或不支持的寄存器变量声明。另外,无论寄存器变量实际上是不是存放在寄存器中,它的地址都是不能访问的。

总结

变量类型 定义 作用域 生命周期 说明
自动变量 定义在函数内部或者是函数参数 自声明其至函数结尾或者是所在语句块结尾 作用域生效则生效,作用域失效则失效,多次调用,重新创建该变量 在运行时,由栈管理
外部变量 定义在函数外部或者是函数 自定义起至所在文件结尾,可通过extern声明,扩展至全局 整个程序运行期间 在编译时,一旦定义即创建变量、分配内存
静态变量 声明时使用,通过添加static来声明 声明外部变量时,该外部变量作用域仅为声明所在文件,声明自动变量时,不改变 整个程序运行期间 可以限定全局变量,函数或者是自动变量
寄存器类型 通过添加register声明 - - 只能限定自动变量或函数参数,可能被存放在寄存器中,也可能被忽略,但是被声明为寄存器类型的变量地址不可访问,即取地址运算符&不可进行运算

因此,可以将变量分为:被初始化的全局范围的外部变量,被初始化的静态类型外部变量,未被初始化的两类外部变量,自动变量,静态类型自动变量,寄存器类型自动变量。

至于为什么这么分,下次讨论编译的时候用的上。

C的变量类型、作用域与生命周期的总结的更多相关文章

  1. C/C++——C++变量的作用域与生命周期,C语言中变量的作用域和生命周期

    全局变量 作用域:全局作用域(全局变量只需在一个源文件中定义,就可以作用于所有的源文件.) 生命周期:程序运行期一直存在 引用方法:其他文件中要使用必须用extern 关键字声明要引用的全局变量. 内 ...

  2. 《征服 C 指针》摘录2:C变量的 作用域 和 生命周期(存储期)

    在开发一些小程序的时候,也许我们并不在意作用域的必要性.可是,当你书写几万行,甚至几十万行的代码的时候,没有作用域肯定是不能忍受的. C 语言有如下 3 种作用域. 1.全局变量 在函数之外声明的变量 ...

  3. C语言 变量的作用域和生命周期(转)

    转自 https://blog.csdn.net/u011616739/article/details/62052179 a.普通局部变量 属于某个{},在{}外部不能使用此变量,在{}内部是可以使用 ...

  4. C语言中变量的作用域和生命周期

    变量的类型: 1. 局部变量和全局变量 局部变量也称为内部变量. 局部变量是在函数内作定义说明的.其作用域仅限于函数内, 离开该函数后再 使用这种变量是非法的. 全局变量也称为外部变量,它是在函数外部 ...

  5. C语言基础 (10) 变量作用域,生命周期 内存结构

    01 课程回顾 1.指针数组 注意: 对于数组来说,在使用sizeof的时候a和&a[0]是不一样的, 虽然以%x打印出来他们都是地址 2.值传递 int a; fun(a); int *** ...

  6. spring作用、spring注解、管理对象的作用域与生命周期、自动装配、Spring的框架包有哪些作用是什么

    Spring 1. 作用 创建和管理对象,使得开发过程中,可以不必使用new关键字创建对象,而是直接获取对象!并且,还可以通过一些配置,使得某些获取到的对象,其中某些属性已经是被赋值的! 2. Spr ...

  7. Mybatis学习-配置、作用域和生命周期

    核心配置文件:Mybatis-config.xml Mybatis的配置文件包含了会深深影响Mybatis行为的设置和属性信息 配置(configuration) 在mybatis-config.xm ...

  8. MyBatis 作用域和生命周期

    理解到目前为止所讨论的类的作用域和生命周期是非常重要的.如果使用不当可导致严重的并发性问题. SqlSessionFactoryBuilder  这个类可以在任何时候被实例化.使用和销毁.一旦您创造了 ...

  9. Bean 注解(Annotation)配置(2)- Bean作用域与生命周期回调方法配置

    Spring 系列教程 Spring 框架介绍 Spring 框架模块 Spring开发环境搭建(Eclipse) 创建一个简单的Spring应用 Spring 控制反转容器(Inversion of ...

随机推荐

  1. IT从业者疫情之下出路何在

    作为一个IT行业十年经历的从业人员,在北京大公司工作过,但因衡量着北京大都市的繁华下高消费和高房价,选择到二线城市发展和组建家庭,由此逃离北上广,结束了数年的北漂生涯.很荣幸到了二线城市顺利遇见属于自 ...

  2. HTTP协议 有这篇文章足够了

    HTTP 协议详解 HTTP(HyperText Transfer Protocol)超文本传输协议.其最初的设计目的是为了提供一种发布和接收HTML页面的方法. HTTP是一个客户端(用户)和服务端 ...

  3. shell 之 case。。。esac多分支选择

    case分支属于匹配执行的方式,它针对指定的变量预先设置一个可能的取值,判断该变量的实际取值是否与预设的某一个值相匹配,如果匹配上了,就执行相应的一组操作,如果没有任何值能够匹配,就执行预先设置的默认 ...

  4. bootstrap简介与入门

    bootstrap前端框架 1.概念:一个前端开发的框架,Bootstrap,来自 Twitter,是目前很受欢迎的前端框架.Bootstrap 是基于 HTML.CSS.JavaScript 的,它 ...

  5. iMX287A多种方法实现流水灯效果

    目录 1.流水灯在电子电路中的地位 2.硬件电路分析 3.先点个灯吧 4.shell脚本实现流水灯 5.ANSI C文件操作实现流水灯 6.Linux 系统调用实现流水灯 @ 1.流水灯在电子电路中的 ...

  6. 7-49 求前n项的阶乘之和 (15 分)

    从键盘输入一个整数n,求前n项的阶乘之和,1+2!+3!+...+n!的和 输入格式: 输入一个大于1的整数.例如:输入20. 输出格式: 输出一个整数.例如:2561327494111820313. ...

  7. JUC常用同步工具类——CountDownLatch,CyclicBarrier,Semaphore

    在 JUC 下包含了一些常用的同步工具类,今天就来详细介绍一下,CountDownLatch,CyclicBarrier,Semaphore 的使用方法以及它们之间的区别. 一.CountDownLa ...

  8. Yoshino: 一个基于React的可定制化的PC组件库

    Github: https://github.com/Yoshino-UI... Docs: https://yoshino-ui.github.io/#/ Cli-Tool: https://git ...

  9. Vue双向绑定的实现原理系列(三):监听器Observer和订阅者Watcher

    监听器Observer和订阅者Watcher 实现简单版Vue的过程,主要实现{{}}.v-model和事件指令的功能 主要分为三个部分 github源码 1.数据监听器Observer,能够对数据对 ...

  10. JZOJ 3505. 【NOIP2013模拟11.4A组】积木(brick)

    3505. [NOIP2013模拟11.4A组]积木(brick) (File IO): input:brick.in output:brick.out Time Limits: 1000 ms Me ...