程序员C语言快速上手——进阶篇(八)

  • 进阶篇
    • 程序结构与作用域
      • 局部变量
      • 全局变量
      • static关键字
      • extern关键字
    • 模块化开发的补充
      • 头文件的嵌套包含
      • 头文件的保护

进阶篇

程序结构与作用域

过程式、模块化的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.hh2.hh3.h三个头文件h1.h内容

#include "h3.h"
……

h2.h内容

#include "h3.h"
……

可以看到,h1.hh2.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.hh2.hh3.h三个头文件都应当使用头文件保护机制。

原文发布于微信公众号 - 编程之路从0到1(artofprogram)

原文发表时间:2019-07-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券