龙空技术网

c语言解剖课:作用域和生命周期的那些事儿

段誉和语言 101

前言:

今天看官们对“c语言中的源程序是什么”可能比较着重,咱们都需要知道一些“c语言中的源程序是什么”的相关文章。那么小编也在网摘上搜集了一些对于“c语言中的源程序是什么””的相关内容,希望大家能喜欢,我们快快来了解一下吧!

c语言中有两个非常重要的概念,他们是构成C语言体系的基座之一,是非常重要的两块压舱石。

这两个特性决定了程序中每一份数据、每一个指令的生与死,是短命还是长寿。

也决定了每一份数据、每一个指令的影响力有多大,影响力是遍布整个程序,还是只能影响到自己的一亩三分地。

这两个特性,即相似,又不相同,你中有我,我中有你,互相交织。经常把c语言携手,甚至很多老手,给搞得焦头烂额。

代码5分钟,BUG5小时,其中可能大部分时间,都在被他们折腾。

这两个掌握了整个程序里所有成员生杀大权的哼哈二将,就是“作用域”和“生命周期”。

作用域,显得比较深奥,如果翻译成 scope(范围),就很直观。

程序中,由指令或数据组成的一行行代码,其中某行代码中的数据(这里的数据指变量,暂不涉及函数)在程序中可以被哪些行代码所“看见,我们就把这行代码和能看见它的所有代码称为在同一个作用域内,在同一个作用域的所有代码,都是彼此可以”看见”的。

实际上,通过下面的例子,就可以快速明白了:

#include <stdio.h>int main() {    int x = 2;    int y = 3;    printf("%d\n",x + y);    {        printf("%d\n",x + y);        //printf("%d\n",x1 + y1);//error        int x1 = 20,y1 = 30;        printf("%d\n",x1+y1);        int x = 200;        int y = 300;        printf("%d\n",x + y);        {            printf("%d\n",x + y);        }    }    printf("%d\n",x + y);    return 0;}

变量x和y,它们最开始是在main函数里诞生的。它诞生后,它后面的printf函数就可以看见它了,然后ptrint就对x和y进行了使用,求了个和。

紧接着出现了一个“括号对”,括号对里的第一个printf也使用了外层的x和y,这是因为作用域遵循外层可以穿透所有内层的原则,所以内层的代码也能看见外层的代码。

被注释的printf证明了数据如果没有诞生,是不能被使用的。

如果内层创建了和外层同名的数据对象,作用域将遵循内层遮蔽外层的原则,优先以最近的外层的同名数据为准。比如最内层的printf看见的是离它最近的同名数据。

用花括号包裹起来的代码,我们一般称为代码块。一个代买快就是一个作用域空间。当然,不止花括号,有很多种方式可以构建作用域空间。比如:

#include "stdio.h"int main(){    int i = 4;    for(int i=1;i < 5;i++){        printf("%d ",i);    }    for(;i > 0;i--){        printf("%d ",i);    }    return  0;}

这两个for循环中,第一个for循环的小括号里,创建了一个和外层同名的变量i,那么准寻内层遮蔽外层的有原则,所以是没问题的。

第二个for循环直接使用了外层的i,遵循了外层穿透所有内层的选择,也没问题。

实际上,每个变量都与离它最近的且相关的花括号,一起构成了它的作用域。

刚才讲的是数据被其他数据能够看见的范围,其实还有一个隐含的信息就是,它到底能活多久?所谓活着,就是它能被其他变量或函数看见和使用,它被程序销毁了,就是死亡了。

它能活多久,和它的可见范围(作用域),是两个问题,根据不同的情况,有的情况,是不相关的,有的情况,是相关的。

我们先以上面这两个for循环内的变量i为例分析。

第一个for循环的变量i只能可见于这个for循环,但存活于它所在的函数的整个生命周期。

c语言编写的程序,本质上就是由一个个源文件构成的。每一个源文件是由一个个函数构成的。函数是构成c程序的基本单位。因此,有人经常会说,c语言就是函数式的语言。

注意,这里的函数式语言,和另一种“函数式编程”(或者说“函数式语言”),是不一样的。

我们一般说函数式语言,或函数式编程,只相对于面向对象编程、基于对象编程、泛型编程、过程式编程等等不同的编程方法,进行对比而言。

此处的“函数式语言”,只是为了强调函数是c程序的基本构成单位,没有其他含义,所以不要混淆。

一个数据它的生命周期,一般只存活于它所在的函数,当这个函数被调用时,然后开始顺序执行函数体内的代码行,当它被创建时,它就诞生了,当它被使用完毕后,它还活着,直到它所在的函数结束执行,这个函数会被销毁,它和函数内所有的数据都会被一起销毁。

下一次这个函数如果再一次被调用,这个数据会随着函数的创建,有一次被创建,然后再次随着函数的销毁而销毁,如此循环往复。

但是,特别的,如果函数内某个对象被static修饰了,那么它第一次随着函数的被调用,跟着一起被创建,然后函数内所有代码执行完毕后,函数虽然被销毁,但是具有static属性的变量,却会一直存活,直到程序执行结束。

那么函数第二次和多次被调用时,它因为存在着,就不会被创建,可以一直使用到程序结束。

既然它所在的函数都执行结束了,那么它为什么还要存活呢?

这样做当然很不好,破坏了函数的封装性,让程序变得复杂,随着程序规模的增加,这种变量越来越多的话,会对变量的变化情况越来越难以掌握,使程序难以维护。

但有时候,也是有点好处的。

比如,你想统计这个函数在程序执行时,被执行了多少次,通过static的函数局部变量,就可以轻松实现,这有时候很有用。比如下面这个例子:

#include "stdio.h"int counter();int main(){    int *p = (int[]){1,3,1,2,1};    int i = 0;    do{      if(p[i++] == 1)          counter();    }while(i < 5);    printf("%d\n",counter());    return 0;}int counter(){    static int couter = -1;    return ++couter;}

通过static的静态变量特性,当第一次初始化之后,该初始化语句就不再被执行,统计了counter函数被调用了三次。

我们再稍微升级下复杂度,看一看新的使用例子。比如:

#include "stdio.h"int foo(int);int main(){    static int t = 1;    for(;t < 4;t++){       foo(t);    }    printf("%d\n",t);    return 0;}//int foo(static int i){int foo(int i){    return ++i;}

foo函数的本意是打算将static变量t作为参数传入到函数内部, 并期望保持static的特性。但是,因为foo的函数参数无法声明为static的属性,给出了错误的提示信息,所以传进去的只能是被强制转换成了普通变量的t的副本。

当然也就实现不了通过static的特性,将t通过foo函数累加一次后,再在for循环的结尾部分再累加一次,达到两次两次累加的目的。

出现这个问题的核心是,foo函数和main函数处在同一个平行的层面,foo内部是看不到main函数内部的static变量的,除非将stati变量再提高一个层面,这样main函数和foo函数的内部,都可以通过外层穿透至内层的作用域原则,看到static变量, 并使用它实现在两个函数之间,既传递了数据,该数据又持续生效。(当然为了实现累加两次的效果,有很多办法,这里只是为了通过static来实现,来介绍其特性)

下面的代码就是将static变量提高一个层次,代码如下:

#include "stdio.h"static int t = 1;int bar();int main(){    for(;t < 4;t++){        bar();    }    printf("%d\n",t);    return 0;}int bar(){    return ++t;}

此时的t是静态变量,但却独立于所有函数之外,像这种情况,它的作用范围就是从它创建的位置一直作用到当前文件的尾部。

实际上,如果t仅仅只是取消static属性,其他代码不变,效果和上面的代码一样的。

程序输出5,和上面代码一样的结果。像这种在文件中,不属于任何函数的变量,我们称为全局变量。而如果再用static属性修饰该全局变量,那么这个变量我们称为静态全局变量。

刚才的例子里看起来两者的功能是一样的,实际上不是。我们再看下面这个例子:

//// main05.c//#include "stdio.h"extern int w;int main(){    w = 520;    printf("%d\n",w);    return 0;}

上面是源文件main05.c,下面是另一个源文件main06.c:

//// main06.c//ver01int w = 250;int foo(){    w = 520;    return w;}

这个编译单元(项目)里有2个源文件,分别是main05.c 和main06.c。在main06.c里定义了一个全局变量w,初始化为250。在main05.c里,通过extern修饰符,就可以访问另一个文件中的全局变量w,并将其值修改为520。

这非常有用,可以很方便的实现两个文件中变量的数据传递。但有时候,我们并不希望别的源文件(不是我负责的源文件),访问我的某些变量,而这些变量又需要定义成全局变量,供整个文件使用,那么最好的办法就是用static修饰。这样,在文件内部是全局变量,对文件外部又隐藏起来。比如下面的代码的示例。

这是第一个源文件main07.c:

//// main07.c//#include "stdio.h"extern int w ;extern int foo();extern int hiden_w;//无效的int main(){   // hiden_w = 520;   // printf("%d\n",hiden_w);    w = 520;    printf("%d\n",w);    printf("%d\n",foo());    return 0;}

用static修饰后的全局变量,只能在本文件中可见,外部文件无法使用。在这个例子中,main07.c里试图访问main06.c里的hiden_w变量,却无法使用,因为用static修饰了(被注释的部分)。但是可以通过访问main06.c里的函数foo,也算间接获取了hiden_w的值。

这个例子中,很好的体现了函数本身就是全局对象。如果你不希望foo函数被外部文件访问,也可以声明为static属性。同全局属性一样,函数也可以用static进行修饰。

如果你希望某个全局变量可以给外部提供数据,但不希望被修改,那么你可以这样做:

这是其中一个源文件main08.c:

//// main08.c//#include "stdio.h"extern int ro_w;int main(){    ro_w = 1413;    printf("%d\n",ro_w);}

这是另一个源文件main06.c:

//// main06.c//int w = 250;static int hiden_w = 502;const int ro_w = 1314;int foo(){    w = 520;    return w;}

main06中,定义了全局的const变量,mainn08中可以访问,但修改时,编译通过,执行时却出现异常(程序异常退出)。

通常,我们都会用 extern const int ro_w;声明,当试图修改时,编译器就会直接给出警告,比如代码修改i如下:

#include "stdio.h"extern const int ro_w;int main(){    ro_w = 1413;    printf("%d\n",ro_w);}

这样当声明外部变量,并显式的声明外部变量是const类型,这样误操作时,编译器会给出警告提醒:

全局变量的生命周期是直到程序结束才消亡,所以不用担心该变量死亡了,外部文件访问时会产生错误。

总结

一个c语言编写的源程序,由一个或多个源文件按组成,每个源文件由一个或多个函数组成。

一个变量的作用范围与它所在的位置有关。

离变量最近的且与它相关的花括号就是它的作用范围,从它被创建,直到花括号结尾,就是它全部的作用范围,也从它创建,直到花括号结尾中间的嵌套扩括号。

一般我们把花括号抱起来的语句称为代码块。

而它的生命周期,当它未用static修饰 时,它生命周期就是从它创建开始,直到它所在的函数结束调用,连同函数一起被销毁。

作用域遵循两个原则,一个是外层穿透所有内层,都可见。一个是内层同名数据可以遮蔽外层同名变量。

一个数据要么在一个函数的内部(包括main函数),要么不在任何一个函数内部。

如果不在任何一个函数内部时,若没用static修饰,就是全局变量。

不但当前源文件内所有数据都可以看见,而且外部文件的数据都可以使用。

如果一个变量用static访问,它如果在函数内部,它的作用域不变,但生命周期从第一次创建开始,一直存活到程序结束。

如果这个变量不在任何函数内部,被static修饰,那么它的作用域就受到变化,只对当前文件所有数据可见。

如果这个全局变量不用static修饰,用const修饰,那么它的作用域未改变,但外部文件内的成员只能读取,不能修改。

特别的,外部文件声明时,最好用extern const 一起修饰,更加安全。

如果是函数内部的变量,我们称为局部变量,用const修饰,不改变它的作用域和生命周期。

文件的函数都属于全局函数,它的作用域和全局变量一样,可以被外部文件的数据使用。如果用static修饰,这个函数将只对本文件的成员可见。

最后,本文没有展开剖析两个文件的同名函数、同名变量如何解决命名冲突的问题,将会后面的文章中专门讲述。

段誉,2024年2月4日,写于合肥。

标签: #c语言中的源程序是什么 #c语言的源程序由什么组成 #c语言的源程序由什么构成