零基础学C语言——作用域

码哥比特课程 2024-04-03 00:02:50
这是一个C语言系列文章,如果是初学者的话,建议先行阅读之前的文章。笔者也会按照章节顺序发布。 在数学中,变量x一般都有其值域,也就是x都可以有哪些值,或者说在什么数值范围内x是有效的。同理,C语言中,每个变量或者函数都有其作用域,也就是在什么范围内这个变量或函数有效(可被编译器找到)。 对于变量,根据作用域的不同被分为两大类——局部变量和全局变量。 局部变量所谓的局部变量是指在函数体中定义的变量,这些变量的作用域就是函数内部,即函数返回后,这些变量将被销毁。这类变量我们一般称作自动变量。 自动变量的定义形式我们在变量一文中介绍过: 数据类型 变量名;或数据类型 变量名 = 初始值;还有一种自动变量——函数参数,函数参数也是仅在函数内有效的。 此外,C语言还提供了一种作用域在函数内,但函数返回后不会销毁的变量——静态变量。 静态变量是指在函数体中,用如下形式定义的变量: static 数据类型 变量名;或static 数据类型 变量名 = 初始值;静态变量与自动变量的区别是:每次函数调用时,函数内的自动变量所占用的内存都会被重新分配,其值也会按照语句重新赋值。而静态变量不同,如果使用定义同时初始化形式定义静态变量,则静态变量仅会被初始化一次。并且每次函数调用时静态变量的值都是沿用上一次调用改变后的值,而不会被重置。 举个例子: #include void foo(void){ static int a = 1; ++a; printf("%d\n", a);}int main(void){ foo(); foo(); return 0;}这个例子的输出是: 23原因是,第一次进入foo时,静态变量a被初始化为1,然后自加变为2,所以printf打印的结果是2(第一行)。随后函数返回main,之后再次调用foo函数。这次foo中a不再被重新赋值为1,而是依旧保持上次被修改后的结果,即2。然后再自加变为3,最后打印其值3(第二行)。 下面看一个初学者常犯的错误: int *return_array(void){ int array[2] = {1, 2}; return array;}int main(void){ int *ret = return_array(); return 0;}这是一个典型的错误用法。我们说过,函数内的自动变量的作用域仅限于函数内,当函数返回时,自动变量会被销毁。因此,main中ret指向的数组,其内容将是不可预知的内容,访问其内容可能会导致程序崩溃。 想要正常返回一个数组,利用静态变量是一种解决方案。除此之外,还有一种动态分配内存的方案,将在后续内存管理相关的文章中说明。 全局变量全局变量是指变量定义于任何函数体之外,且作用域是整个程序范围内的变量。这类变量又分为两类——普通全局变量和静态全局变量。 普通全局变量定义形式如下: 数据类型 变量名;或数据类型 变量名 = 初始值;而静态全局变量的定义形式为: static 数据类型 变量名;或static 数据类型 变量名 = 初始值;与局部变量中自动变量和静态变量的定义一样,但是含义完全不同的。 静态全局变量与普通全局变量的不同在于作用域范围。普通全局变量是作用于整个程序范围内的,而静态全局变量的作用域则是当前的源文件。 举例: /*a.c*/int a = 10;static int b = 100;int main(void){ foo();}/*b.c*/#include void foo(void){ printf("a:%d\n", a); printf("b:%d\n", b);//这句是无法通过编译的}如果按照上面代码创建两个源文件并编译,是无法生成可执行程序的,且会报错。 原因有二: 1.正如我注释所写,b是a.c中的静态全局变量,作用域仅在a.c,因此b.c无法访问。 2.全局变量a虽然不是静态全局变量,但在b.c中缺少声明,因此无法使用。 下面我们重写b.c,修正这两个问题: /* b.c */#include extern int a;void foo(void){ printf("a:%d\n", a);}这里,去掉了b的打印,同时增加了全局变量a的声明。 注意,这个全局变量的声明使用了extern关键字。extern关键字用于告知编译器,用其声明的变量或者函数是全局作用域的,需要从可执行程序涉及到的全部源文件中寻找。 同名覆盖不知是否有读者想过,如果全局变量和局部变量同名,那么函数内的变量的值会是什么呢? 看一个例子: #include int a = 1;int main(void){ int a = 2; printf("%d\n", a); return 0;}这段代码的执行结果是:2。 这里存在同名覆盖原则:同名的局部变量会覆盖同名的全局变量。 函数作用域函数的作用域与全局变量的作用域相同,毕竟在C语言中函数内部无法再定义函数。 提供给外部其他源文件使用的函数的声明形式如下: extern 返回值类型 函数名(参数列表...);给本文件内使用的函数的声明形式如下: static 返回值类型 函数名(参数列表...);并且,函数对编译器的可见性也取决于函数声明的位置,例如: int main(void){ foo(); return 0;}static void foo(void);void foo(void){}如此声明foo函数,编译器依旧会报错,因为foo函数的定义对main不可见。如果将foo函数的static声明提前到main函数前(即本例中放在第一行),则可正常编译。 块作用域前面关于语句的文章中并未提及一种特殊的语句——块语句。 这种语句是以大括号({})扩起的,其大括号内部可以是单条语句,也可以是多条语句。 { ...//一条或多条语句}这并非是说C语言中看到大括号就是块语句。函数的大括号并不属于块语句,其余则皆为块语句,包括if-else、for、while等结构中涉及大括号的部分。 我们先来看一个例子: #include int main(void){ int a = 1; { int a = 2; printf("In block a:%d\n", a); } printf("Out of block a:%d\n", a); return 0;}运行结果为: In block a:2Out of block a:1这个例子告诉我们两个事实: 块内同名变量将覆盖外层同名变量块内定义的变量在块外无法访问,即块结构内的自动变量会随块结构完结而销毁。头文件与源文件之前的文章中,所涉及到的例子都是放在.c文件中的。然而C语言中并不只有.c文件。 在C语言中有两种文件——头文件和源文件。 源文件就是我们所说的文件名后缀以.c结尾的文件,其中的代码一般都是各类函数的定义。 头文件是文件名以.h结尾的文件。这类文件中一般记录一些结构定义、函数声明、变量声明、类型定义等。关于结构体和类型定义我们后续文章会有专门说明。 什么情况下需要头文件呢?我们来看个例子: /*b.c*/extern void foo(void);void bar(void){ foo();}/*c.c*/void foo(void){}可以看到,a.c和b.c都用到了c.c中的foo函数,因此它们都需要声明foo函数。如果这时我对foo函数的返回类型做了修改,那么我需要到声明foo的其他源文件中修改其声明。如果我有20个源文件中都用到了foo呢?那么此时的修改会不会引起混乱呢?因此,头文件就派上了用场。 我们看下修改后的代码: /*a.c*/#include "c.h"extern void bar(void);int main(void){ foo(); bar(); return 0;}/*b.c*/#include "c.h"void bar(void){ foo();}/*c.c*/void foo(void){}/*c.h*/extern void foo(void);如此,我们将foo的extern声明仅写一份放在c.h头文件中。 然后利用预编译的include指令,将c.h的内容引入到需要使用foo函数的a.c和b.c文件中。关于include的更详细介绍,将在预编译宏文章中给出。目前只需要知道,在编译时,include会将其后紧跟的文件名所指定的文件中的内容原封不动展开(可看作复制)进使用该include指令的源文件中,且展开点就是include指令所在位置。 喜欢的小伙伴可以关注码哥,也可以给码哥留言评论,如有建议或者意见也欢迎私信码哥,我会第一时间回复。 感谢阅读!
0 阅读:0

码哥比特课程

简介:感谢大家的关注