c高级篇(一)——难发现的命名重复

前言:
   这是我开始编写的第一个高级篇博文,分类的大体的原则是,基础篇写必须掌握的基础知识,进阶篇写需要一定理解深度才能真正掌握的知识,而高级篇写一些偏经验性的,书本上可能学不到的知识。

1 宏和枚举名重复

  为什么需要命名空间?在c中并不支持命名命名空间,在大型项目中会遇到烦人的命名重复的问题,比如宏名、枚举名、全局变量名、函数名。大型工程中,不同的模块会分配给不同的人或团队开发,各自模块测试通过,最终合到一起时可能会发现命名有较多重复,没发现的就将成为Bug。比如下面这c段代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

typedef enum
{
NAME = 0
} tenuTest;

#define NAME 5

int main(void)
{
printf("NAME is %d\n", NAME);

return 0;
}

  这段代码枚举和宏都定义了 NAME 但值不同,编译器并不会报错甚至没有警告。运行输出的结果为。

1
NAME is 5

  如果将上面枚举和宏定义的顺序交换一下,编译器则会报错,从而帮助开发这发现重复命名的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

#define NAME 5

typedef enum
{
NAME = 0
} tenuTest;

int main(void)
{
printf("NAME is %d\n", NAME);

return 0;
}

  报错信息为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
╰─ make
Consolidate compiler generated dependencies of target app
[ 50%] Building CXX object CMakeFiles/app.dir/main.cpp.o
/home/calm/calm/tmp/test/cpp/main.cpp:3:14: error: expected identifier before numeric constant
3 | #define NAME 5
| ^
/home/calm/calm/tmp/test/cpp/main.cpp:7:5: note: in expansion of macro ‘NAME’
7 | NAME = 0
| ^~~~
/home/calm/calm/tmp/test/cpp/main.cpp:3:14: error: expected ‘}’ before numeric constant
3 | #define NAME 5
| ^
/home/calm/calm/tmp/test/cpp/main.cpp:7:5: note: in expansion of macro ‘NAME’
7 | NAME = 0
| ^~~~
/home/calm/calm/tmp/test/cpp/main.cpp:6:1: note: to match this ‘{’
6 | {
| ^
/home/calm/calm/tmp/test/cpp/main.cpp:3:14: error: expected unqualified-id before numeric constant
3 | #define NAME 5
| ^
/home/calm/calm/tmp/test/cpp/main.cpp:7:5: note: in expansion of macro ‘NAME’
7 | NAME = 0
| ^~~~
/home/calm/calm/tmp/test/cpp/main.cpp:8:1: error: expected declaration before ‘}’ token
8 | } tenuTest;
| ^
/home/calm/calm/tmp/test/cpp/main.cpp:8:3: error: ‘tenuTest’ does not name a type
8 | } tenuTest;
| ^~~~~~~~
make[2]: *** [CMakeFiles/app.dir/build.make:76: CMakeFiles/app.dir/main.cpp.o] Error 1
make[1]: *** [CMakeFiles/Makefile2:83: CMakeFiles/app.dir/all] Error 2
make: *** [Makefile:91: all] Error 2

  要怎么解释这个现象?宏是在预编译阶段处理并完成替换的,而枚举是在编译阶段处理,在编译阶段将枚举转换成相应的值。从上往下如果见定义的是枚举,预编译阶段并不会处理枚举,接着往下到 NAME 的宏定义开始,之后遇到的 NAME 字符串都被替换为整数 5 ,这是程序运行输出值为5的原因,再到编译阶段开始处理枚举定义,但此时预编译阶段已经结束,NAME 已经被宏替换为 5 ,编译器会发现枚举 NAME 并为被使用,但这不构成错误,编译器不会报出任何错误和警告。第二段程序交换了枚举和宏的定义顺序为什么又能报错?先定义了 NAME 宏,会将枚举定义中 NAME = 0NAME 替换为5,到编译阶段发现枚举中 5 = 5 当然就报错了。

2 危害

  这个错误可能导致使用的值并不是开发者所预期的,比如开发者与其输出的是枚举 NAME 的值,即0,但程序实际值是5,从而可能引发各种非预期的程序Bug。特别在大型复杂的项目中,枚举和宏可能定义在不同的模块中,编译器也不会发现错误,将会是一个很隐蔽的Bug,比如以下代码。

  • main.c
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include "b.h"
#include "a.h"

int main(void)
{
printf("NAME is %d\n", NAME);

return 0;
}
  • a.h
1
#define NAME 5
  • b.h
1
2
3
4
typedef enum
{
NAME = 0
} tenuTest;

  开发者很难发现 a.hb.h 中的枚举和宏重名。修改这两个头文件的调用顺序会导致编译报错,也是让人疑惑的现象。

2 思考

  既然编译器不能发现这种命名重复的错误,有什么解决办法吗?

  • 模块内部,应先写宏定义,后写枚举定义。

  应规定良好的编码习惯,在一个模块内,从上往下应先写宏定义,后写枚举定义,这样即使重名编译器也能发现并报错。

  • 模块之间,应规定好的命名习惯,从名称上区分宏和枚举。

  一些公司对模块、变量、函数有严格的命名要求,但枚举和宏命名没有任何区分,都是大写加下划线。要防止模块之间枚举和宏重名,可以在这两中命名前加上模块名前缀,即 MODULE1_NAME

  • 使用静态扫描测试。

  虽然编译器编译无法发现这种错误,但可以借助一些专业的代码静态测试工具,前提是团队要重视静态测试的结果,认真去分析每一条case。