c程序可以看作是一系列外部对象构成,这些外部对象可能是变量或函数

外部对象和内部对象

external和internal是相对应的,internal用于描述定义在函数内部的函数参数及变量,外部变量定义在函数之外,可以在许多函数中使用。默认情况下,外部变量与函数(默认是external)具有下列性质:

  • 通过同一个名字引用的所有外部变量(即使这种引用来自单独编译的不同文件)实际上都是引用同一个对象(标准中将这一性质称为外部链接)
  • 在这个意义上,外部变量类似于Fortran语言的COMMON块或Pascal语言中在最外层程序块中声明的变量

每个源文件中包含一个编译预处理命令和若干函数和变量定义,各个文件可以单独编译,并可以与库中已经编译过的函数一起加载。源文件里的内容一般都是相对独立的,在编译时不需要与其他文件互通,只需要在编译成目标文件后再与其他的目标文件做一次链接就行了。在不同系统中,保存在多个源文件中的c程序的编译和加载工作是不同的,如在UNIX系统中可使用cc命令:

cc main.c geline.c strindex.c 编译文件后,将生成的目标代码分别放在文件main.o,geline.o与strindex.o中

然后再将三个文件一起加载到a.out可执行文件中,若源程序存在错误(比如main.c存在错误),则可以通过命令对main.c文件重新编:

cc main.c geline.o strindex.o

并将编译的结果与以前已编译过的目标文件getline.o与strindex.o一起加载到可执行文件中,cc命令使用.c与.o两种扩展名来区分源文件与目标文件

变量作用域(scope)

变量作用域指在源程序中定义的变量位置及其能被读写访问的范围。作用域可以分为:全局作用域,局部作用域,其中声明的变量分别对应于局部变量和全局变量。对于在函数开头声明的自动变量,其作用域是声明该变量名的函数,不同函数中声明的具有相同名字的各局部变量之间则没有任何关系,函数的参数也是一样,实际可以将其看作是局部变量。当然全局和局部的说法主要是在单个文件中划分的,如果要涉及多个文件的链接的话,不同作用域内的变量更适合从内部和外部的角度进行讨论。

内部变量(Interanl Variable)

语句块内定义的变量,包括形参,分动态和静态,缺省定义的是动态内部变量(自动变量),特点:

  • 生存期是该语句块的生存期,进入语句块时获得内存,仅能由语句块内语句访问,退出语句块时释放内存并且不再有效
  • 定义时不会自动初始化,除非设定初值
  • 并列语句块各自定义的同名变量互不干扰
  • 形参和实参可以同名

c语言并不是Pascal等语言意义上的程序块结构式语言,它不允许在函数中定义函数,但在函数中可以以程序块结构的形式定义变量。变量的声明(包括初始化)除了可以紧跟在函数开始的花括号之后,还可以紧跟在任何其他标识复合语句开始的左花括号之后,这种方式声明的变量可以隐藏程序块外与之同名的变量,并在与左花括号匹配的右花括号出现之前一直存在,如:

if(n>0){int i;for(i=0;i<n;i++);}中变量i的作用域是if语句的‘真’分支,与程序块外声明的i无关

每次进入程序块时,程序块内声明以及初始化的自动变量都将被初始化,静态变量只在第一次进行时初始化一次。

自动变量也可以隐藏同名的外部变量和函数,如:

int x,y;f(double x){double y;}

一个好的程序设计风格中,应当避免出现变量名隐藏外部作用域中相同名字的情况,否则很容易引起混乱和错误,外部变量或函数作用域从声明它的地方开始,到其所在的(待编译的)文件末尾结束。

外部变量(External Variable)

在所有函数之外定义的变量,特点:

  • 生存期是整个程序,从程序运行起占据内存,程序运行过程中可随时访问,程序退出时释放内存
  • 有效范围是从定义变量的位置开始到本程序结束
    • 注意作用范围不是从变量定义处到该文件结束,在其他文件中也有效

构成c程序的函数与外部变量可以分开进行编译,一个程序可以放在几个文件中,原先已经编译过的函数可以从库中进行加载,如:

main(){…}int sp=0;double val[MAXVAL];void push(double);double pop(void);

push和pop不需任何声明就可以访问sp与val,但sp,val,push和pop都不能用在main函数中。另一方面,如果在外部变量定义之前使用该变量,或者外部变量的定义与变量的使用不在同一个源文件,则必须在相应的变量声明中强制使用extern,定义格式:

extern 数据类型 变量名

extern最基本的用法是声明外部变量(注意得是非静态的),可以扩大该变量的作用域,extern用于声明函数的话仅仅是暗示这个函数可能在别的源文件里定义,无实际作用

  • 这主要是因为函数声明和定义区别很明显,从是否有定义体就能看出来,因此用不用extern都能区分
  • 外部数据变量则不同,因为是在全局作用域所以默认会自动初始化,如果不加extern的话就必须为这个变量分配空间,所以加上extern就是让编译器知道这是个声明不用为其分配空间

原本外部变量的作用域是从定义点处开始直到文件结束,使用extern提前声明之后就变成从声明处开始,直到文件结束,例:

int main()
{
    extern int x;         //声明外部变量,如果不提前声明编译器到这里找不到x
    printf("%d",x);
}  
int x=1;                  //外部变量定义点在main()函数以后

外部变量(和函数)的声明与定义必须要严格区分,变量声明用于说明变量的属性(主要是类型),而变量定义除此外还将引起存储器的分配(主要是外部变量和函数要么在其他地方有定义,要么在声明的同时初始化),如:

int sp;double val[MAXVAL]; 写在所有函数之外则相当于直接定义了该文件中的外部变量,并为之分配存储单元(自动初始化),以及作为该文件中其余部分的声明
extern int sp;extern double val[];只作为该文件中其余部分的声明(数组长度在其他部分确定),声明没有建立变量或分配存储单元

变量定义中必须指定数组的长度,但extern声明不一定需要,因为变量的初始化(指的是真正的为内存中的变量赋值的过程)只能出现其定义中,这进一步体现了声明不需要实际分配存储空间的特点。

一个外部变量是不可能在另一个文件中同时定义的,否则必然是重定义错误(就像在同一个作用域内不能定义同名变量)。在一个程序的所有源文件中,一个外部变量只能在某个文件中定义一次,其他文件可以通过extern声明来访问。假定函数push和pop定义在同一个文件中,而变量val和sp在另一个文件中定义并被初始化(通常不大可能这样组织程序),如:

file1:
int sp=0;double val[MAXVAL];
file2:
extern int sp;extern double val[];void push(double f){}double pop(void){}

通过这样的声明和定义可以将函数和变量“绑定”在一起,file2中的extern声明不仅放在函数定义外面,还要在它们的前面,适用于该文件所有函数,如果要在同一文件中先使用后定义变量,也需要像这样的方式来组织文件。

相比于内部变量,外部变量具有更大的作用域和更长的生存期,外部变量为函数的数据交换提供了一种可以代替函数参数与返回值的方式,任何函数都可以通过名字访问一个外部变量,如:

int a=-1;void Function(){int a=5;}int main(){int a=10;{int a=5;}Function();printf(“a=%d”,a);}
输出结果:a=10

上例中外部变量a的生存期和程序生存期相同,Function()被调用,里面的内部变量a=5才分配空间,Function()执行完成生存期结束,作用域范围是从定义位置开始到函数体末尾,主函数main()中定义的a在程序执行时分配空间,main()执行完成生存期结束,作用域范围也是从定义位置开始到函数体末尾。main()中复合语句的内部变量只在程序块中执行,生存期和作用域范围也都是在程序块内。如果函数之间需要共享大量的变量,使用外部变量要比使用一个很长的参数表更方便有效,比如两个函数互不调用对方,但必须共享某些数据,这种情况下最方便的方式是将这些共享定义为外部变量,而不是分别作为函数参数传递,不过这样做也可能对程序结构产生不良的影响,导致程序中各个函数之间具有太多的数据依赖。

实际上需要理解外部变量这一概念的关键在于理解编译器的工作原理:
简单说编译器是并不关心程序的具体实现的(当然优秀的编译器会进行语义检查和优化,不过这不属于它必须要做的事),它只关心程序在“逻辑”上能不能运行起来。编译器的要求是只要程序在适当位置有声明,能够将变量函数之类的用符号表存储起来并为其进行适当的逻辑上的存储分配,满足程序的过程调用关系就可以了,它检查完这些就进行“文本翻译”,将源程序编译为其他程序(汇编程序、可重定位目标程序)。
那么,真正需要完成地址绑定的是链接器(当然一般编译器软件都是包含配套链接器的)。链接器链通过编译器提供的全局符号表找到相对应的符号,将每个引用符号的地方进行地址修改,如果是外部符号就由链接器在其输入的可重定义目标文件中查找该符号,此时就要求符号声明要在对应文件中有无二义性的定义实现,如果找不到就会报链接错误。另一方面也就是说,假如在多个文件中同时定义外部变量,这时候它们会各自存在于所在的源文件编译出来的可重定位目标文件中,链接器根本不知道应该要链接哪个,也会产生错误。

变量存储类型(storage class)

存储类型指数据在内存中存储的方式,即编译器为数据分配内存的方式,决定变量的生存期。在定义变量时可以指定变量存储类型,基本格式为:

存储类型 数据类型 变量名;

c程序变量的读写类型分为:auto型-自动变量,static型-静态变量,extern型-外部变量,register型-寄存器变量

在内存(RAM)中,代码在代码区,常量在常量区(也有部分在代码区),变量存储区域分静态存储区和动态存储区,分别是数据区和栈/堆。静态存储区主要包括静态局部变量和全局变量,动态存储段主要存放自动变量(动态局部变量),寄存器变量存放在cpu的寄存器中(普通变量是运行时cpu从内存读取需要的数据到寄存器中进行运算,而寄存器变量直接在寄存器读取即可)。当然这只是语言定义的一种理想化模型,实际上如何分配属于编译器和操作系统的行为。
静态存储区中变量与整个程序“共存亡”,动态存储区中变量和寄存器变量与所在函数“共存亡”。(生存期)

动态局部变量

动态局部变量是缺省的存储类型,如果变量定义在局部(函数内)时,称为自动变量(动态局部变量),定义格式:

auto 数据类型 变量名 (或者直接省略auto)

动态局部变量在执行流进入语句块时自动申请内存,退出时自动释放内存。

静态变量

静态变量(静态局部变量或全局变量) 定义格式:

static 数据类型 变量名

使用static修饰变量,对内部变量而言是改变生存期,对外部变量是改变作用域。在函数内部定义的话静态变量作用同动态局部变量,生存期为整个程序运行期间。不管其所在函数是否被调用,static类型的内部变量是一种只能在某个特定函数中使用但一直占据存储空间的变量。
如果用static声明限定外部变量和函数,则可以将其后声明的对象作用域限制在编译源文件的剩余部分,通过static限定外部对象,可以达到隐藏外部对象的目的(类似于private的效果)。外部的static声明通常多用于变量,当然也可声明用于函数,通常情况下函数名是全局可访问的,对程序的各个部分而言都可见,但如果把函数声明为static类型则除了对该函数所在文件仍然可见外,其他文件都无法访问。在这种情况下不同源文件中同名的外部变量甚至是不冲突的,可以认为非静态外部变量的作用域是整个源程序,静态外部变量的作用域是单个源文件。

(内部)静态变量示例程序:使用静态变量输出1-10的阶乘

#include<stdio.h>
int Factorial(int n)
{
    static int p=1;              //静态变量只初始化一次,函数调用结束后不释放内存,值可以保存到下次进入函数,是函数具有了记忆功能
    p*=n;                        //p从1开始,每调用一次就乘一次n,即为阶乘
    return p;
}
int main()
{
    int i,n;
    scanf("%d",&n);
    for(i=1;i<n;i++)
    {
        printf("%d!=%d\n",i,Factorial(i));
    }
}

寄存器变量

寄存器变量定义格式:

register 数据类型 变量名

cpu内部容量有限但运算速度极快,register声明就是告诉编译器,其所声明的变量在程序中使用频率较高,其思想是将register变量放在cpu寄存器中,将使用频率比较高的变量声明为寄存器变量,可使程序更小,执行速度更快。不过编译器本身也是可以忽略这一选项的,一些现代编译器有能力自动将普通变量优化为寄存器变量,并且可以忽略用户的指定,所以现在一般已经无需特别声明变量为register类型。register声明只适用于自动变量以及函数的形式参数,如:

f(register unsigned m,register long n){register int i;}

这主要是因为extern和static类型变量本身在存储分配、生存期、作用域方面有其特殊性,其实现并不容易让寄存器参与进去。另外实际使用时,底层硬件环境实际情况会对寄存器变量的使用有一些限制,不同机器中对寄存器变量的数目和类型的具体限制也是不同的,每个函数中只有很少变量可以保存在寄存器中,且只允许某些类型的变量。不过过量声明也没太多坏处就是了(但不意味着可以瞎写等着编译器优化),因为编译器可以自动过滤,无论寄存器变量是不是真正存放在寄存器中,它的地址都是不能访问的(因为地址格式和内存不一定相同故而无法进行取地址运算)。