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)。

栗子:

  1. #include <stdio.h>
  2. #define MAX 100
  3. char s[MAX];
  4. void printString(void)
  5. {
  6. printf("%s", s);
  7. }
  8. int main(void)
  9. {
  10. scanf("%s", s);
  11. printString(s);
  12. return 0;
  13. }

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

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

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

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

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

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

  1. int func(void)
  2. {
  3. int i;
  4. for(i = 0; i < 10; i++)
  5. {
  6. int temp;
  7. ...
  8. }
  9. }

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

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

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

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

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

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

  1. int a;
  2. double b[MAX];

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

  1. extern int a;
  2. extern double b[];

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

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

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

版本1:

  1. #include <stdio.h>
  2. #define MAXLENGTH 1000 // buffer最大长度
  3. char buf[MAXLENGTH];
  4. int getline(void);
  5. /*一个简单的copy-paste程序
  6. */
  7. int main(void)
  8. {
  9. while (getline()!=EOF)
  10. {
  11. printf("%s\n", buf);
  12. }
  13. return 0;
  14. }
  15. int getline(void)
  16. {
  17. int c, i;
  18. extern char buf[MAXLENGTH];
  19. i= 0;
  20. while ( i < MAXLENGTH && (c = getchar()) != EOF && c != '\n')
  21. buf[i++] = c;
  22. if (c = '\n')
  23. buf[i++] = c;
  24. buf[i] = '\0';
  25. return i;
  26. }

版本2:

  1. #include <stdio.h>
  2. #define MAXLENGTH 1000 // buffer最大长度
  3. char buf[MAXLENGTH];
  4. int getline(void);
  5. /*一个简单的copy-paste程序
  6. */
  7. int main(void)
  8. {
  9. while (getline()!=EOF)
  10. {
  11. printf("%s\n", buf);
  12. }
  13. return 0;
  14. }
  15. int getline(void)
  16. {
  17. int c, i;
  18. // 通过上下文隐式声明 buf[]
  19. i= 0;
  20. while ( i < MAXLENGTH && (c = getchar()) != EOF && c != '\n')
  21. buf[i++] = c;
  22. if (c = '\n')
  23. buf[i++] = c;
  24. buf[i] = '\0';
  25. return i;
  26. }

静态变量与寄存器变量

静态变量

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

  1. static char buf[BUFSIZE];
  2. static int bufp = 0;

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

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

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

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

寄存器变量

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

register声明的形式如下:

  1. register int x;
  2. register char c;

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

  1. void f(register unsigned a, register long n)
  2. {
  3. register int i;
  4. ...
  5. }

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

总结

变量类型 定义 作用域 生命周期 说明
自动变量 定义在函数内部或者是函数参数 自声明其至函数结尾或者是所在语句块结尾 作用域生效则生效,作用域失效则失效,多次调用,重新创建该变量 在运行时,由栈管理
外部变量 定义在函数外部或者是函数 自定义起至所在文件结尾,可通过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. ip修改成域名

    将ip修改成域名,这样的话可以使程序变得更加健壮,别人不能直接看见你的ip地址. 后来总结下分享给大家.首先找到hosts文件的位置,这个文件是系统dns默认查找的文件. windows 系统:C:\ ...

  2. 手写node可读流之流动模式

    node的可读流基于事件 可读流之流动模式,这种流动模式会有一个"开关",每次当"开关"开启的时候,流动模式起作用,如果将这个"开关"设置成 ...

  3. Dubbo 入门-细说分布式与集群

    什么是Dubbo Dubbo是一款高性能.轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现. 什么是RPC RPC全称(Rem ...

  4. nginx如何连接多个服务?

    记录一下: 刚开始用nginx部署,在项目文件内touch了一个nginx.conf配置文件,然后将这个conf文件软链接到nginx的工作目录中 sudo ln -s /home/ubuntu/xx ...

  5. java线程间的协作

    本次内容主要讲等待/通知机制以及用等待/通知机制手写一个数据库连接池. 1.为什么线程之间需要协作 线程之间相互配合,完成某项工作,比如:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行 ...

  6. 如何将zTree选中节点传递给后台

    获取zTree选中节点 <body> <script type="text/javascript"> var setting = { view: { dbl ...

  7. 分布式图数据库 Nebula Graph 的 Index 实践

    导读 索引是数据库系统中不可或缺的一个功能,数据库索引好比是书的目录,能加快数据库的查询速度,其实质是数据库管理系统中一个排序的数据结构.不同的数据库系统有不同的排序结构,目前常见的索引实现类型如 B ...

  8. 网址封锁的几种方法 公司把 pan.baidu.com 封了 研究实现原理

    HTTP 和 HTTPS 协议HTTP 协议在 头部会发送 host 就是要访问的域名,可以用来被检测. HTTPS 协议虽然会加密全部通讯,但是在握手之前还是明文传输.有证书特证可被检测. 1, D ...

  9. 如何在国内离线安装Chrome扩展并科学查资料

    国内离线安装Chrome扩展 这些链接是从知乎国内离线安装 Chrome 扩展程序的方法总结 - 知乎看到的, 怕这个链接失效, 在这里自己备一份: Crx4Chrome - Download CRX ...

  10. 《自拍教程46》Python_adb自动拍照100张

    Android手机测试, 涉及照相机(Camera)应用程序的稳定性测试的用例, 需要涉及100张照片的拍照自动化测试. 准备阶段 先清理老照片,照片一般存放在/scard/DCIM目录下 adb s ...