C语言变量可见性以及工程项目源码布局

死线 发布于

写过C语言项目或者看过C开源项目可能都对C语言源文件的分布比较熟悉。前段时间需要自己去「白手」开发一个C项目,而我以前都是 Review 别人的项目,从来没有自己完整的做过。这就自然不可避免的要给自己的项目「分割功能」并放到适当的文件中去,尤其是像C语言这种结构化的语言分割起来尤其的困难,下面我将我遇到的一些问题罗列出来:

  • 声明与定义
  • extern 与 static 关键字
  • 头文件的条件编译

最常见的C项目(数据库)布局往往如下所示:

db.h
1
2
3
4
5
6
7
8
9
10
11
12
13
 
//这个头文件没有对应的.c文件,因为这个头文件定义最基础的数据类型
//并且别的.c项目文件均要引用

#ifdef _DB_H_
#define _DB_H_

typedef struct db {
int a;
int b;
} db;

#endif
A.h
1
2
3
4
5
6
7
8
//功能模块A的头文件。
#ifdef _A_H_
#define _A_H_

extern int aa;
extern int aa();

#endif
B.h
1
2
3
4
5
6
7
8
//功能模块B的头文件。
#ifdef _B_H_
#define _B_H_

extern int bb; //当外部的文件引用这个源文件时会自动加入这条语句,就省去了单独再去声明的麻烦。
extern int bb();

#endif
A.c
1
2
3
4
5
6
7
8
9
10
11
//功能模块A源文件,需要使用功能模块B中的某些接口和数据。
#include "db.h"
#include "A.h"
#include "B.h"

int aa; //全局变量,全工程都是可见的,然而别的文件使用前必须先要声明。
int aa(); //全局的函数,特性同上。

static void aa2(void); //静态函数,对其余源文件不可见。
static aa2; //静态变量,特性与上面相同。
#endif

声明与定义

很多人没搞清楚声明与定义的区别,当然他们肯定是知道声明只是『言明』了一下变量,而定义才是真正的产生该变量的『空间』,但是具体又很难说出他们的区别,简单总结一下,其实它们的区别如下

  • 声明没有实际开辟空间,而且是可以重复声明的,不会出现编译错误;定义是实际预分配存储空间的,而且不能有重复定义的情况。
  • 声明的目的主要是为了产生一个『约定』,使在可见性范围以外的代码区照样能够知道它的存在并使用它。

extern 与 static 关键字

那么有了这么清晰的区别,为什么有很多人会对那个概念产生混乱呢?我感觉语言设计本身需要承担一部分责任,比如下面的代码在C编译能通过而在C++中却不能通过:

test.c
1
2
int a;
int a;

原因在于C语言默认把未初始化的声明当成一个真正的『声明』,但C++却不是这样,即便未初始化,它仍将这条语句当成一个定义,除非你强制使用 extern 关键字,否则他就会将这条语句当成一个『定义』并且返回重复定义的错误。

我们可以看出在头文件声明全局变量的一个好处,因为头文件只是简单的复制信息到源文件中,所以相应的『extern』变量都不需要在重复声明,直接使用即可。至于『static』变量是与 extern 变量相对应的,使用此关键字的变量只在文件内部可见,如果你在头文件内定义了一个 static 变量,那别的不同文件内的 static 变量互相是不同的。

头文件的条件编译

很多人也许不知道条件的真正作用,只是简单的知道它们是为了重复包含头文件。其实在编译阶段每个模块的源文件时分开编译生成 .o 文件的,所以当我们需要某个某款的功能时简单的引用这个模块的头文件即可,但是有的时候模块直接回涉及到交叉的调度,这时候为了防止重复包含头文件就只能用条件编译语句来避免重复包含语句了。

这么就有人问了,那为什么我在头文件中定义了一个全局变量,当我编译整个工程的时候还是会出现重复定义的错误呢?不是用条件编译避免了重复引用吗?那是因为虽然『单个模块』避免了重复使用同一个头文件,但是『整个工程』内还是重复的。所以比如你在每个文件内都有一个『全局变量a』,那么对于整个工程都可见的『a』,那可能是重复定义了。这样一来 extern 关键字的作用就更体现出来了,他只在对应的功能模块的 .c 内定义了一次,而别的文件只是声明并使用而已。

总结

综上所述,我们在设计工程的时候通常会遵循以下的『规则』:

  • 头文件只用来定义一些全局的宏, 以及 typedef 自己的类型设计。
  • 外部功能模块需要使用的『接口』以及『变量』均在头文件中用『extern』声明。
  • 如果只在功能模块内使用的『接口』以及『变量』使用『static』关键字声明即可。

题图

椎名真白