程序员C语言快速上手——进阶篇(八) 水深无声 2021-12-17 00:51 264阅读 0赞 ### 文章目录 ### * 进阶篇 * * 程序结构与作用域 * * 局部变量 * 全局变量 * static关键字 * extern关键字 * 模块化开发的补充 * * 头文件的嵌套包含 * 头文件的保护 * 欢迎关注我的公众号:编程之路从0到1 # 进阶篇 # ## 程序结构与作用域 ## 过程式、模块化的C语言程序是由多个源文件(.c文件)构成的,在每一个源文件中,都形成一个文件作用域。所谓**作用域**,实际上就是指有效范围。一旦离开这个源文件的范围,就相当于离开了该源文件的文件作用域。在源文件中定义函数,那么在函数之外的地方,就属于全局作用域,即使是多个源文件,只要在函数之外,那它们就都属于全局作用域,全局作用域,全局都可访问。而在函数之内的空间声明变量,那它属于局部作用域。 ### 局部变量 ### 局部变量是指在某个函数内部声明的变量。它有两个含义 1. 在某个函数内声明的局部变量,不能被其他的函数使用,意即只在声明它的函数内有效。 2. 每次调用函数时,生成的局部变量的储存空间可能都是不同的,意即局部变量在函数调用结束后,就会释放,下次调用函数,生成的局部变量又是一个新的。 还要注意一点,在函数的形式参数中声明的变量,也都是局部变量。 ### 全局变量 ### 与局部变量相对的概念是全局变量,它声明在所有的函数体之外。全局变量在**文件作用域**内可见,即从变量被声明的下一行,一直到当前文件的末尾,它都可以被直接使用,因此全局变量可以被它之后定义的所有函数访问。 需要注意一点,编译器会自动将全局变量进行零值初始化。因此在使用时,只需要声明即可。如果需要手动指定其值进行初始化,则它只能被常量表达式初始化,使用其他的变量表达式初始化是不合法的。 //全局变量(正确) int minute = 360 -10; //错误!!! 全局变量必须使用常量表达式初始化 int hour = minute/60; // 访问全局变量 minute int f(int h){ //h 是局部变量 return h*minute; } int main(){ // 局部变量 int day=0; return 0; } ### static关键字 ### 除了局部变量和全局变量,C语言中还有静态局部变量和静态全局变量,声明时使用`static`关键字修饰即代表静态的意思。 #include <stdio.h> // 静态全局变量 static int s_global; int get_count(){ // 静态局部变量 static int count; count++; return count; } int main(){ printf("%d\n",get_count()); printf("%d\n",get_count()); printf("%d\n",get_count()); printf("%d\n",get_count()); return 0; } 静态全局变量和普通全局变量的区别不是很大,主要体现在访问权限的区别上。在C语言中,全局变量是在整个程序的生命期中都有效的,换句话说,也就是一旦声明了一个全局变量,则整个程序中都可以访问,而静态全局变量,则只在声明它的那个源文件中可以访问。静态全局变量虽然也是在整个程序的生命期中都有效,但它在其他文件中不可见,无法被访问。关于这一点的细则,在下面的`extern`关键字的使用中做详细说明。 **静态局部变量和普通局部变量的区别就比较大了,主要有三个区别** 1. 存储位置不同。静态局部变量被编译器放在全局存储区,虽是局部变量,但是在程序的整个生命期中都存在。而普通局部变量在函数调用结束后就会被释放。从这一点上看,静态局部变量和全局变量被放在了相同的储存位置。 2. 静态局部变量会被编译器自动初始化为零值。我们都知道普通局部变量的原则是先初始化后使用,而静态局部变量则和全局变量一样,会被自动初始化,使用时只需声明,无需手动初始化。 3. 静态局部变量只能被声明它的函数访问。静态局部变量与普通局部变量的访问权限相同,都只能被声明它的函数使用。如上例,静态局部变量`count`只能被`get_count`函数使用,即使`count`变量在整个程序的生命期中都有效,其他函数也无法使用它。 说完了静态局部变量后,大家肯定疑惑,既然它只在声明它的函数中使用,那它还有什么意义呢?直接使用普通局部变量不就行了,干嘛要用它?我们知道,普通局部变量在函数每次被调用的时候都会生成一个新的,调用结束后又将它释放,如果一个函数被频繁调用,这样性能岂不是很低?因为需要不停的生成新的局部变量,然后又释放掉,然后又生成新的……但是给局部变量加上了`static`修饰后,函数无论被调用多少次,都不会再生成新的局部变量,始终都是复用的同一个变量,这就大大减少了对内存的操作,提升了性能。 举个生活中的例子,如果你在公司楼下有一个固定的私人停车位,那么你每天上班只需要把车停在固定的地方就好,如果你没有私人停车位,那你每天到公司楼下,都需要四处去找一个空位子停车,岂不是很麻烦,效率又低,弄不好因为找停车位导致打卡迟到。 既然静态局部变量这么好,那是不是可以滥用呢?还是回到上面的例子,如果你是公司特聘人员,一个月只需要上两天班,那么你有必要在公司楼下买一个固定的私人停车位吗?显然是没有必要的,因此当函数不会被频繁调用时,不应当考虑使用静态局部变量。 最后需要特别注意,静态局部变量会一直保存上次的值,因为它一直都存在。基于这个特性,我们通常可以使用静态局部变量做计数器,如上例,每次调用`get_count`函数时,对静态局部变量`count`自增1,打印结果如下: 1 2 3 4 **静态函数** `static`关键字除了可以修饰变量,还可以用来修饰函数。在C++、Java等面向对象的编程语言中,都存在类似于`private`的权限访问控制,而C语言中的`static`关键字,就类似这种`private`,被它修饰的函数只能在当前源文件中使用,在其他源文件中无法被访问。通常来说,C语言编写的大型的模块化工程中,不需要共享的函数都应该使用`static`关键字来修饰。 需要特别注意,由于C语言没有命名空间的概念,它只有一个全局作用域,当你的C程序十分庞大时,存在几百上千个函数时,很难保证函数不会同名。当然,通过严格的代码规范,命名规范,可以人为的保证函数不会同名,但我们可以保证自己写的函数不会同名,却无法保证引入的外部库的函数不会和我们的函数同名。一旦函数同名了,就会形成命名冲突,这就是为什么我们看一些C语言编写的开源库时,变量名、函数命名非常的复杂,名字很长,多个单词大写或以下划线分隔,这样怪异的命名很大程度上就是为了避免命名冲突。基于此,我们编写非公开、非共享的函数时,都应当使用`static`修饰,以此来避免一部分命名冲突问题。`static`修饰的函数,只在当前源文件中可见,在另一个源文件中声明一个同名的函数,就不会产生命名冲突。 示例 编写`f1.c`源文件 int get_count(){ static int count; count++; return count; } 编写`main.c`源文件 #include <stdio.h> int get_count(); int main(){ printf("%d\n",get_count()); printf("%d\n",get_count()); return 0; } 编译:`gcc f1.c main.c -o main` 编译、运行正常 修改`f1.c`,添加`static`修饰 static int get_count(){ static int count; count++; return count; } 编译报错,在`main.c`源文件中无法使用静态函数`get_count` ### extern关键字 ### 在说明`extern`关键字前,先来看一个示例 编写`t1.c` // 全局变量 int s_global=12; 编写`main.c` #include <stdio.h> int main(){ printf("s_global=%d\n",s_global); return 0; } 编译:`gcc t1.c main.c -o main` 这样会直接报错: `error: 's_global' undeclared (first use in this function)` 这好像和我们前面说的有些不符,全局变量是在整个程序的生命期都有效的,在全局可访问的,但是现在却报错了。大家要注意前面的措辞,全局变量在**文件作用域**内可见,即从变量被声明的下一行,一直到当前文件的末尾,它都可以被直接使用。这里的关键就是**直接使用**,在`t1.c`源文件中是可以直接使用的,但是`main.c`中就无法直接使用了。 当全局变量离开了它的文件作用域后,无法直接使用,这时候我们需要另一个关键字`extern`来帮助我们使用它。 修改`main.c` #include <stdio.h> // 写在函数外部,表示在当前文件中的任意地方都可以使用s_global // extern int s_global; int main(){ // 写在函数内部,仅在函数中使用 extern int s_global; printf("s_global=%d\n",s_global); return 0; } 再次编译成功,运行结果 s_global=12 在这里,`extern int s_global;`并不是重新声明变量的意思,它表示的是引用全局变量`s_global`,一定要注意,如果不存在一个全局变量`s_global`,是无法编译的,也就是说,使用`extern`来引用全局变量时,全局变量一定要存在。 `extern`主要是用来修饰变量的,当然也可以用来修饰函数,通常C语言中的函数都使用头文件包含的方式引入声明,但我们也可以使用`extern`修饰。实际上C语言中的函数声明默认都是包含`extern`的,无需手动指定。 //以下两种是等价的,无需手动指定extern关键字 int get_count(); extern int get_count(); **小拓展** 有时候我们可能会看到`extern “C”`这样的声明,请注意,这不是C语言的语法,也不属于C语言。有些C++程序员,经常把`C`语言和`C++`语言搞混,实际上这是两种不同的语言,C++也并不是很多人说的那样,完全是C语言的超集,更准确的说法应该是,C++是一种独立的语言,它兼容C语言的绝大多数语法,但并不是百分百完全兼容。C++除了兼容的C语言的语法,另一部分就是它独立的内容。如果不能完全清楚这两种语言的边界,就会发生语法弄混的情况。 在C++中,当需要调用纯C语言编写的函数时,通常会使用`extern “C”`声明,表明这是纯C语言的内容。 ## 模块化开发的补充 ## ### 头文件的嵌套包含 ### 所谓嵌套包含,就是指在一个头文件中,还可以使用`#include`预编译指令,包含其他的头文件。 例如,我们编写一个头文件`bool.h` #define Bool int #define False 0 #define True 1 在以上头文件中,我们使用宏定义了新类型`Bool`,接着编写`func.h`头文件 #include "bool.h" // 声明一个函数,返回值为Bool类型,值可以是False 或者True Bool check(); ### 头文件的保护 ### **如果一个源文件将同一个头文件包含两次,那么就可能会产生编译错误**。因此,在C语言的模块化开发中,一定要避免将同一个头文件包含两次。但是,有时候这种包含不是明显的,而是一种隐式的包含,不易察觉,不知不觉就犯下了错误。 如下例,分别创建`h1.h`、`h2.h`、`h3.h`三个头文件 `h1.h`内容 #include "h3.h" …… `h2.h`内容 #include "h3.h" …… 可以看到,`h1.h`、`h2.h`两个头文件分别都包含了一个相同的`h3.h`头文件,那么如果在`main.c`中分别包含这两个头文件 // main.c #include "h1.h" #include "h2.h" …… 这样一来,实际上就等同于在`main.c`中将`h3.h`头文件`include`了两次,显然违背了我们上面说的,不能在一个源文件中将同一个头文件包含两次的原则。因为所谓头文件包含,实际上就是将头文件中的声明复制到当前源文件中,那么上例中`h3.h`一定会被复制两次。 问题出来了,该如何解决呢?在复杂的大型工程中,头文件被重复包含的问题一定是避免不了的,这个时候就需要我们上一章讲的条件编译知识出来救场了。 修改`h3.h`文件 内容如下 // 如果没有定义过_H_H3_ 宏,则定义一个_H_H3_ 宏 #ifndef _H_H3_ #define _H_H3_ // 声明的内容 …… #endif 改造头文件之后,再去源文件使用,就不会存在重复包含的问题了。 注意,这里使用`#ifndef`和`#endif`将整个头文件中的全部内容包裹起来,然后在`#ifndef`之后通过`#define`定义一个宏,这样一来,`#ifndef`和`#endif`之间的内容就只会被预编译一次,而不会重复包含。这种机制,被戏称为头文件卫士,或者称为头文件保护。如果对于这种写法不太理解,可以使用上一章介绍的`gcc -E`命令,生成预编译代码查看,即可明了。 最后,需特别注意的地方是宏的名字,这里是`_H_H3_`,使用头文件包含这种机制时,宏定义的名字一定要独特,避免重复,以免导致各种不可预知的问题。通常宏的名字要全部大写,并用下划线来分隔单词或缩写,在这个宏的名称中,最好包含当前头文件的文件名,例如`H3`。 在C语言中,我们以后自己编写头文件,建议在所有编写的头文件中都使用这种头文件保护机制,因为你不知道什么时候,你的这个头文件可能就会被重复包含,如上例,`h1.h`、`h2.h`、`h3.h`三个头文件都应当使用头文件保护机制。 # 欢迎关注我的公众号:编程之路从0到1 # ![编程之路从0到1][0_1] [0_1]: /images/20211214/70512311be7c47bf8fd337aef8bd4aed.png
相关 程序员C语言快速上手——基础篇(三) 文章目录 小拓展:C语言中int的正确使用姿势 语法基础 表达式 算术运算符 关系运算符 待我称王封你为后i/ 2022年01月23日 00:19/ 0 赞/ 282 阅读
相关 程序员C语言快速上手——高级篇(九) 文章目录 高级篇 结构体 背景 结构体的声明与使用 结构体变量的初始化 太过爱你忘了你带给我的痛/ 2021年12月09日 20:55/ 0 赞/ 352 阅读
还没有评论,来说两句吧...