C语言的变量作用域及头文件
关于C语言的变量作用域和头文件的问题都是比较基础的问题,但是这些问题在实际使用过程中的概念不清和混乱会对一个多文件的项目的组织结构及文件结构造成很大的影响,使得项目本身的脉络也变的很模糊。在项目中,多人相互协作完成的项目,这个问题就更加突出。所以也就有了我写(总结)这个文档。
一.C语言的变量作用域及相关
1.作用域:
作用域描述了程序中可以访问一个标识符的一个或多个区域。即变量的可见性。一个C变量的作用域可以是代码块作用域、函数原型作用域,和文件作用域。 函数作用域(Function Scope),标识符在整个函数中都有效。只有语句标号属于函数作用域。标号在函数中不需要先声明后使用,在前面用一个goto语句也可以跳转到后面的某个标号,但仅限于同一个函数之中。 文件作用域(File Scope),标识符从它声明的位置开始直到这个程序文件的末尾都有效。例如下例中main函数外面的sum,add,还有main也算,printf其实是在stdio.h中声明的,被包含到这个程序文件中了,所以也算文件作用域的。 块作用域(Block Scope),标识符位于一对{}括号中(函数体或语句块),从它声明的位置开始到右}括号之间有效。例如上例中main函数里的num。此外,函数定义中的形参也算块作用域的,从声明的位置开始到函数末尾之间有效。 函数原型作用域(Function Prototype Scope),标识符出现在函数原型中,这个函数原型只是一个声明而不是定义(没有函数体),那么标识符从声明的位置开始到在这个原型末尾之间有效。例如void add(int num);中的num。
下面再介绍另一种分类形式:它分为代码块作用域和文件作用域。代码块作用域和文件作用域也有另一种分类方法,局部作用域和全局作用域。
代码块作用域:代码块是指一对花括号之间的代码,函数的形参虽然是在花括号前定义但也属于代码作用域。在C99中把代码块的概念扩大到包括由for循环、while循环、do while循环、if语句所控制的代码。在代码块作用域中,从该变量被定义到代码块末尾该变量都可见。 文件作用域:一个在所有函数之外定义的变量具有文件作用域。具有文件作用域的变量从它的定义处到包含该定义的文件结尾都是可见的。
2.链接
一个C语言变量具有下列链接之一:外部链接(external linkage),内部链接(internal linkage)或空链接(no linkage)。 空链接:具有代码块作用域或者函数原型作用域的变量就具有空链接,这意味着他们是由其定义所在的代码块或函数原型所私有。 内部链接:具有文件作用域的变量可能有内部或外部链接,一个具有文件作用域的变量前使用了static标识符标识时,即为具有内部链接的变量。一个具有内部链接的变量可以在一个文件的任何地方使用。 外部链接:一个具有文件作用域的变量默认是具有外部链接的。但当起前面用static标识后即转变为内部链接。一个具有外部链接的链接的变量可以在一个多文件程序的任何地方使用。
例: static int a;(在所有函数外定义)内部链接变量 int b; (在所有函数外定义) 外部链接变量 main() {
int b;//空链接,仅为main函数私有。
..
} 3.存储时期
一个C语言变量有以下两种存储时期之一:(未包括动态内存分配malloc和free等) 静态存储时期(static storage duration)和自动存储时期(automatic storage duration)和动态存储时期。 静态存储时期:如果一个变量具有静态存储时期,他在程序执行期间将一直存在。具有文件作用域的变量具有静态存储时期。这里注意一点:对于具有文件作用域的变量,关键词static表明链接类型,而不是存储时期。一个使用了static声明了的文件作用域的变量具有内部链接,而所有的文件作用域变量,无论他具有内部链接,是具有外部链接,都具有静态存储时期。
自动存储时期:具有代码块作用域的变量一般情况下具有自动存储时期。在程序进入定义这些变量的代码块时,将为这些变量分配内存,当退出这个代码块时,分配的内存将被释放。
举例如下:
//example_1.c
#include <stdio.h>
#include <stdlib.h>
void add(int num);//文件作用域,外部链接,
void chang_sum();//文件作用域,外部链接
int sum=1; //文件作用域 外部链接,静态存储时期
int main(int argc, char *argv[])
{
int num = 5;//函数作用域,空链接
add(num);
printf("main num=%d\n",num); /*输出5*/
//内层定义覆盖原则,当内层代码块定义了一个与外层代码块相同时名字的变量时,
//运行到内层代码块时,使用内层定义的变量,离开内层代码块时,外层变量恢复
//此时sum为for中定义的sum,而不是全局的sum
for(int sum=0, num=0;num<5;num++)//代码块作用域,空链接,自动存储时期
{
sum+=num;
printf("====================\n");
printf("for num=%d\n",num);//输出0-5
printf("for sum=%d\n",sum);//输出0-5的叠加和
}
printf("====================\n");
{
int i;//代码作用域。仅在该大括号内可见。空链接,自动存储时期
for(i=0;i<10;i++);
printf("i=%d\n",i);
}
// printf("i=%d\n",i);//编译通不过
printf("main sum=%d\n",sum);//输出0。
printf("main num=%d\n",num);// 输出5
chang_sum();
printf("file sum=%d\n",sum);//输出1。全局的sum。内层定义覆盖原则。
system("PAUSE");
return 0;
}
void add(int num)//代码作用域
{
num++;
printf("add num= %d\n",num); /*输出6*/
}
void chang_sum()
{
sum++;
printf("chang_sum = %d\n",sum); /*输出1*/
}以上示例须在在C99标准下编译。(gcc支持c99的方法,编译时加入参数 –std=C99)。从上例中可以比较清楚明白代码作用域和文件作用域的概念。另外注意文件作用域不仅限于变量也包括函数。在文件作用域中函数也是以其声明开始到文件结尾结束。而且当拥有文件作用域与拥有代码作用域变量同名时,不会发生冲突,而是以最小作用域的变量可见。
4.存储类修饰符(Storage Class Specifier)有以下几种关键字,可以修饰变量或函数声明:
static,用它修饰的变量的存储空间是静态分配的,用它修饰的文件作用域的变量或函数具有Internal Linkage(内部链接)。 auto,用它修饰的变量在函数调用时自动在栈上分配存储空间,函数返回时自动释放,例如上例中main函数里的num其实就是用auto修饰的,只不过auto可以省略不写(此处与编译器有关,参照编译器不同而有所变动),auto不能修饰文件作用域的变量。
register,编译器对于用register修饰的变量会尽可能分配一个专门的寄存器来存储,但如果实在分配不开寄存器,编译器就把它当auto变量处理了,register不能修饰文件作用域的变量。现在一般编译器的优化都做得很好了,它自己会想办法有效地利用CPU的寄存器,所以现在register关键字也用得比较少了。 extern,上面讲过,链接属性是根据一个标识符多次声明时是不是代表同一个变量或函数来分类的,extern关键字就用于多次声明同一个标识符。
c语言使用作用域,链接和存储时期来定义了5种存储类:自动,寄存器,具有代码块的作用域的静态、具有外部链接的静态,以及具有内部链接的静态。
五种存储类
存储类
时期
作用域
链接
声明方式
自动
自动
代码块
空
代码块内
寄存器
自动
代码块
空
代码块内,使用register
具有外部链接的静态
静态
文件之间
外部
所有函数之外
具有内部链接的静态
静态
文件之内
内部
所有函数之外使用关键字static
空链接的静态
静态
代码块
空
代码块内,使用关键字static
二.头文件的处理和书写 很多人对C语言中的 “文件包含”都不陌生了,文件包含处理在程序开发中会给我们的模块化程序设计带来很大的好处,通过文件包含的方法把程序中的各个功能模块联系起来是模块化程序设计中的一种非常有利的手段。 头文件的功能:
(1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。
(2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。
文件包含处理是指在一个源文件中,通过文件包含命令将另一个源文件的内容全部包含在此文件中。在源文件编译时,连同被包含进来的文件一同编译,生成目标目标文件。怎么写文件件? 怎么包含才能避免重定义? 其实这个只要了解了文件包含的基本处理方法就可以对文件包含有一个很好的理解与应用了: 文件包含的处理方法:
(1) 处理时间:文件包含也是以"#"开头来写的(#include ), 那么它就是写给预处理器来看了, 也就是说文件包含是会在编译预处理阶段进行处理的。
(2) 处理方法:在预处理阶段,系统自动对#include命令进行处理,具体做法是:降包含文件的内容复制到包含语句(#include )处,得到新的文件,然后再对这个新的文件进行编译。
抓住这两点,那么这个就没有什么难的了。。。
首先,先对#include指令的作用和实际验证一下。 #include指令是预处理指令,它仅仅是将#incluce "A.h"中的A.h的内容替换它自己所在位置,和C语言中的宏的使用类似。而且A.h并不是说必须是.h的文件,只要是文本文件都可以的。下面我给出两个例子证明一下。
例1:有以下两个文件,main.c和main.n
//file1 main.c
#include<stdio.h>
#include<stdlib.h>
#include "main.n"//包含了main.n的文本文件。
int main()
{
n = 2;
printf("n=%d\n",n);
return 1;
}
//file2 main.n
int n;这时我们对main.c进行编译 gcc main.c -o main.exe(我在windows系统下),你会发现能编译通过并打印出n的值。如果你使用预编译参数-E,会在预编译后的文件中发现其原因所在。使用 gcc -E main.c -o main.cpp。打开main.cpp后在文件最后会有如下内容。
# 3 "main.c" 2
# 1 "main.n" 1
int n;
# 5 "main.c" 2
int main()
{
printf("n=%d\n",n);
system("pause");
return 1;
}以上的示例应该能比较明显解释#include的作用,和使用方法了。但是在实际开发中,这种使用方式是严重的不规范行为,强烈建议不要使用。同样下边的例子也是一样的建议。
例2:
(1)包含.c文件:
//file1: main.c
#include <stdio.h>
#include <stdlib.h>
#include "test.c"
int main(int argc, char *argv[])
{
m=5;
for(int i=0;i<5;i++)
{
add();
m++;
test();
}
system("PAUSE");
return 0;
}
12: //end of file1
//file2:test.c
static int n;
int m;
int add();
void test()
{
int t_sum;
printf("m = %d\n",m);
printf("n = %d\n",n++);
t_sum = add();
printf("add = %d\n",t_sum);
}
int add()
{
static int sum;
sum++;
return sum;
}
//end of file2这个例子是采用 包含.c文件 的方法实现的。 在编译时,直接去编译main.c文件,预处理器会先把test.c文件中的内容复制到main.c中来,然后再对新的main.c进行编译。
编译命令:
gcc main.c -o main
可以看到,这里并没有对test.c进行编译,但还是生成了最终的main可执行程序。 也可以通过命令来观察一下预处理的结果: 编译命令:
gcc -E main.c -o main.cpp(仅预处理)
在main.cpp文件末尾可以看来下面一段代码:
# 3 "main.c" 2
# 1 "test.c" 1
static int n;//此处是test.c的内容
int m;
int add();
void test()
{
int t_sum;
printf("m = %d\n",m);
printf("n = %d\n",n++);
t_sum = add();
printf("add = %d\n",t_sum);
}
int add()
{
static int sum;
sum++;
return sum;
}
# 4 "main.c" 2//此处是main.c的内容
int main(int argc, char *argv[])
{
m=5;
for(int i=0;i<5;i++)
{
add();
m++;
test();
}可见,其实就是将test.c文件中的内容添加到了main函数之前,然后对新的文件进行编译,生成最终的可执行程序。
这次如果还是按照上面的方法只编译main.c的话就会出错,因为变量m和函数add并没有在main.c中定义,所以编译时需要将test.c一起编译:
编译命令:
gcc -c main.c -o main.o #编译main.c
gcc -c fun.c -o fun.o #编译fun.c
gcc main.o fun.o -o main #用main.o fun.o生成main
到这里大家应该已经理解包含#include文件和多文件程序的本质区别了。
包含文件仅仅是在c预编译时进行再次整合,最终的还是合并成一个文件编译,生成执行文件。
而多文件的编译,是多个文件分别编译,(也可能是在编译时添加必须的标识),然后通过链接器将各个文件链接后加载形成可执行文件。
这种方式会使得我们的定义和声明分开,不容易产生重定义。而且也利于模块化,仅通过头文件来给出接口,而隐藏具体的实现。
预处理时会把头文件中的内容复制到包含它的文件中去,而复制的这些内容只是声名,不是定义,所以它被复制再多份也不会出现"重定义"的错误。。。
前面说了头文件的方法也是模块化程序设计中的一种非常有利的手段。把同一类功能写到一个.c文件中,这样可以把他们划为一个模块,另外再对应的写上一
个.h文件做它的声明。这样以后再使用这个模块时只需要把这两个文件添加进工程,同时在要使用模块内函数或变量的文件中包含.h文件就可以了。
举个很实际的例子,在单片机、ARM或其他嵌入式开发中,每一个平台可能本身都有多种不同的硬件模块,使用时需要去写相应的驱动程序,
这样就可以把各个硬 件模块的驱动程序作为一个模块(比如lcd驱动对对应lcd.c和lcd.h,IIC驱动对应I2C.c和I2C.h等),当具体使用到某个模块时,
只需 要在将对应的.c和.h文件添加进工程,并在文件中包含对就的.h文件即可。
根据以上的原理理解和实际中使用的一些问题及模块化的原则,对头文件写法给出以下几点个人建议作为基础:
(1) 按相同功能或相关性组织.c和.h文件,同一文件内的聚合度要高,不同文件中的耦合度要低。接口通过.h文件给出。
(2) 对应的.c文件中写变量、函数的定义,并指定链接范围。对于变量和函数的定义时,仅本文件使用的变量和函数,要用static限定为内部链接防止外部调用。
(3) 对应的.h文件中写变量、函数的声明。仅声明外部需要的函数,和必须给出变量。有时可以通过使用设定和修改变量函数声明,来减少变量外部声明。
(4) 如果有数据类型的声明 和 宏定义 ,请写的头文件(.h)中,这时也要注意模块化问题,如果数据类型仅本文件使用则不必在写头文件中,而写在源文件(.c)中,会提高聚合度。减少不必要的格式外漏。
(5) 头文件中一定加上#ifndef...#define....#endif之类的防止重包含的语句
(6) 头文件中不要包含其他的头文件,头文件的互相包含使的程序组织结构和文件组织变得混乱,同时给会造成潜在的错误,同时给错误查找造成麻烦。如果出现,头文件中类型定义需要其他头文件时,将其提出来,单独形成全局的一个源文件和头文件。
(7)模块的.c文件中别忘包含自己的.h文件以上几点仅是个人观点,供大家讨论,如果有意见或是认为不合理或是有更合理的方式请讨论指出。
补充1:
按照c语言的规则,变量和函数必须是先声明再使用。可以多次声明,但不可以多次定义。
补充2:变量的定义和声明。
“声明”仅仅是告诉编译器某个标识符是:变量(什么类型)还是函数(参数和返回值是什么)。要是在后面的代码中出现该标识符,编译器就知道如何处理。记住最重要的一点:声明变量不会导致编译器为这个变量分配存储空间。 C语言专门有一个关键字(keyword)用于声明变量或函数:extern。带有extern的语句出现时,编译器将只是认为你要告诉它某个标识符是什么,除此之外什么也不会做(直接变量初始化除外)。
编译器在什么情况下将语句认为是定义,什么情况下认为是声明。这里给出若干原则: #1 带有初始化的语句是定义 例如:
int a = 1; //定义
#2 带有extern的语句是声明(除非对变量进行初始化) 例如:
extern int a; //声明 extern int b = 2; //定义
#3既没有初始化又没有extern的语句是“暂时定义”(tentative definition) C语言中,外部变量只能被(正式)定义一次:
int a = 0; int a = 0; //错误!重复定义
又或者:
int a = 0; double a = 0.1; //错误!标识符a已经被使用
暂时定义有点特殊,因为它是暂时的,我们不妨这样看: 暂时定义可以出现无数次,如果在链接时系统全局空间没有相同名字的变量定义,则暂时定义“自动升级”为(正式的)定义,这时系统会为暂时定义的变量分配存储空间,此后,这些相同的暂时定义(加起来)仍然只算作是一个(正式)定义。 例如: /*Example C code*/ int a; //暂时定义 int a; //暂时定义 int main(void) {
a = 1; return 0;
} int a; //暂时定义
相关推荐
在编程语言中,全局变量是一种特殊类型的变量,它的作用域和生存期与函数的存储分类息息相关。本课件将详细介绍全局变量的定义、作用域、生存期和存储分类,并通过实例解释全局变量的使用和注意事项。 一、全局变量...
在AngularJS中,作用域(Scope)是框架的核心部分,它作为连接应用程序模型与视图的桥梁。在Angular中,每个HTML元素通过特定指令(如ng-app、ng-controller等)都可以拥有自己的作用域,这些作用域是作用域树的一...
5. 链式结构:链式结构引入了指针数据类型,逻辑顺序不依赖于物理顺序,通过指针域连接数据元素来表达逻辑关系。链式存储结构在动态操作中表现出的优势使其成为广泛使用的存储类型。这种结构不需要连续的物理地址,...
### C语言数据知识整理 #### 数据类型与基本概念...综上所述,C语言中的数据类型、作用域、连接属性和存储类型是理解和编写高效、可靠的C程序的基础。掌握这些概念可以帮助开发者更好地管理内存和提高程序的可维护性。
文件作用域下声明的inline 函数默认为static 存储类型,文件作用域下声明的const 常量默认为static 存储类型。 作用域包括局部作用域、函数作用域、文件作用域等,标识符的可见范围不超过作用域,标识符的生命期与...
不过,如果这个文件是域测试过程中的一个部分,它可能包含了特定的测试策略或步骤,需要结合具体工具和上下文来理解其作用。 总结,理解和有效利用"域测试小工具"对于确保企业网络的稳定性和安全性至关重要。通过...
在AngularJS中,作用域(Scope)是连接视图和控制器的关键组件,它是一个特殊的JavaScript对象,负责存储和管理应用程序的数据模型。本文将深入探讨AngularJS的作用域以及相关示例。 ### 1. 作用域的基本概念 作用...
【金融实验1】作业主要涉及了C++编程语言中的几个核心概念,包括作用域、可见性、静态数据成员、静态函数成员、友元类、编译与连接过程、运算符“*”和“&”、指针、字符串、引用以及指针常量。 1. **作用域**:...
JavaScript通过作用域链解决自由变量的查找问题,作用域链连接了当前作用域与上层作用域,允许在不同作用域中访问变量。 (15)——闭包 闭包是JavaScript中一种强大的特性,它允许函数访问并操作其词法作用域内的...
在PHP编程语言中,闭包(Closure)是一种特殊类型的匿名函数,它能够捕获其外部作用域中的变量,即使在闭包被定义后这些变量的生命周期已经结束。`use`关键字在PHP闭包中用于引入外部作用域的变量,并将其复制到闭包...
2. 成员变量(字段):在类中,方法或构造器外部声明的变量,具有类作用域。 3. 类静态变量(静态字段):用static关键字声明的成员变量,属于类本身,而非类的实例。 4. 匿名内部类作用域:在匿名内部类中声明的...
在Java中,变量的作用域主要有四种:局部作用域、方法作用域、类作用域和包作用域。 局部作用域通常是在方法、构造器或者块中声明的变量,只在该方法、构造器或块内部可见。 方法作用域的变量是在类的方法内声明的...
- 明确存储过程和函数中变量的作用域,以避免潜在的作用域冲突问题。 - 在存储过程中合理使用参数和返回值,以保证程序逻辑的清晰和正确性。 总之,存储过程和存储函数是数据库编程中极为有用的功能,能够实现复杂...
- **作用域链**:用于连接当前执行环境与外部执行环境的链条,从而能够访问外部环境中的变量。 ##### 2.2 作用域链与[[Scope]] 当函数被创建时,它会记录下创建时的作用域链。这个作用域链通常被称为函数的 `[...
这对于服务器识别客户端类型、调整响应内容具有重要作用。 #### Referer头域 `Referer`头域包含请求的前一个页面的URL,这有助于服务器了解流量来源,并可能用于统计分析或安全验证。 #### Range头域 `Range`头...
本篇文章将深入探讨C++程序的结构,主要包括外部存储类型、作用域、可见性、生命期、头文件的使用,以及多文件结构和编译预处理的概念。 首先,我们关注的是外部存储类型,这在多文件程序中扮演着关键角色。当一个...
#### 变量作用域和存储类别 在C语言中,变量的作用域决定了变量的可见性和生命周期。理解变量的作用域和存储类别对于编写无错误且高效的代码至关重要。 - **作用域**:变量的作用域指的是变量可以在程序的哪个部分...
在编程语言中,作用域和可见性是...以上就是从给定的描述中提炼出的相关知识点,包括作用域、可见性、静态成员、友元关系、编译连接过程、指针和引用以及const指针的使用。了解这些概念对于理解和编写C++程序至关重要。
变量有三个关键要素:变量名、变量类型和变量作用域。变量名是用来标识变量的字符串,它必须符合Java的标识符规则,即以字母或下划线开头,后跟任意数量的字母、下划线或数字。值得注意的是,变量名不应与Java的...