前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >程序员C语言快速上手——进阶篇(八)

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

作者头像
arcticfox
发布2019-07-10 10:55:51
8940
发布2019-07-10 10:55:51
举报
  • 进阶篇
    • 程序结构与作用域
      • 局部变量
      • 全局变量
      • 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三个头文件都应当使用头文件保护机制。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程之路从0到1 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 进阶篇
    • 程序结构与作用域
      • 局部变量
      • 全局变量
      • static关键字
      • extern关键字
    • 模块化开发的补充
      • 头文件的嵌套包含
      • 头文件的保护
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档