Makefile 的用处与头文件包含顺序引发的问题,解决已包含头文件但还是 undefined reference to
PS. 条件编译宏并不是万能的,相反,它只能解决最基本的重复包含问题,而头文件问题并不止于此
A.c (main函数)
#include "B.h"
int main(void) {
//内容
return 1;
}
B.c
#include "B.h"
void func_b() {
//内容
}
B.h
#ifndef B_H_
#define B_H_
//头文件内容
void func_b();
#endif
注:B_H_ 是规范的写法,_B_H 不是规范写法,因为c库内置的定义都是下划线开头的,用户定义的头文件不应该以下划线开头
好了,我们编译一下(Linux 下的可执行文件可以没有后缀名,而 Windows 下的可执行文件需要 exe 后缀,即 A.exe)
gcc A.c -o A
这时会提示 undefined reference to func_b
为什么呢,我们看一下编译工具预处理后但还没汇编的源码文件
gcc -E A.c -o A.i
这时候你可以看到 A.i 里面的内容,只有 A.c 和 B.h 的内容以及其他链接信息,但没有包含 B.c 的内容。
因为我们的编译命令错了
正确的编译命令
gcc A.c B.c -o A
单文件的编译流程:
hello.c(预处理) -> hello.i(编译) -> hello.s(汇编) -> hello.o(链接) -> hello
gcc -E hello.c -o hello.i //预处理
gcc -S hello.i -o hello.S //编译成汇编码
gcc -c hello.s -o hello.o //汇编成二进制
gcc hello.o -o hello //链接
多文件就要生成多个.o文件,然后用以下命令链接起来
gcc 1.o 2.o 3.o -o hello
当然如果目录里只放需要的文件,也可以使用
gcc *.c -o hello
但是文件多了之后,只放需要的文件就几乎不可能了,何况还有子目录呢
由于文件过多,所以后来人们使用专用的脚本来处理
其中 Makefile Cmake 等是比较受欢迎的
下面放个例子
目录结构
$ tree
.
├── bit_bmp.h
├── bmp
│ ├── 000.bmp
│ ├── 1.bmp
│ ├── 2.bmp
│ ├── 3.bmp
│ ├── 4.bmp
│ ├── 5.bmp
│ └── background.bmp
├── Makefile
├── my_graph (生成的可执行文件)
├── my_graph.c
├── my_graph.o (用于连接的Object)
├── mylib
│ ├── graph.c
│ ├── graph.h
│ └── graph.o (用于连接的Object)
├── show_bmp2.c
├── state
└── state.c
2 directories, 18 files
效果如 make 后输出的日志一样,当然你复制make后的日志执行也是一样的,但是只打 make 命令就可以完成多爽啊,脚本的魅力就在于此
语法详解:
前面的 := 都是变量定义
解释一下第10行到第17行
产物名: 原料1名 原料2名
命令(如果原料更新了则执行此处)
如 第13行
graph.o: mylib/graph.c mylib/graph.h
$(CC) -c mylib/graph.c -o mylib/graph.o
产物graph.o: 原料mylib/graph.c mylib/graph.h
如果原料更新了就执行命令 $(CC) -c mylib/graph.c -o mylib/graph.o
注意:其中原料名必须和其依赖的产物名一致。
思考:命令和冒号前都要标注产物文件名,那岂不是很多余?
那我们用上通配符符号:%
$@ 表示目标文件
$< 表示第1个依赖文件
$^ 表示所有依赖文件
好,改写一下我的 Makefile
Ps. Makefile 会根据文件是否更新而决定是否编译某个文件
重点:头文件不要有定义
不要在头文件里定义函数或变量,头文件里应只有声明
因为c语言里,可以重复声明,但不能重复定义。
当头文件多次包含时,就会导致重复定义,这个问题是 #ifndef
#endif
条件宏也无法解决的(因为c语言处理头文件包含就是把整个头文件合并到一个文件里,而条件宏只对单个Object文件有效,多个Object文件连接后里就会导致很多重复的地方)
所以头文件里只应该有声明,而不应该有定义。
正确做法:
在 c 文件里,定义全局变量如 int your_value = 0;
在 h 头文件里,声明外部全局变量 extern int your_value;
另外,头文件里应该只放对外部有用的东西,或者方便修改的宏。仅对头文件同名的 c 文件有效的声明或宏,应该只放在 c 文件里。
总结
当出现如 undefined reference to `album' 时,
检查如下:
- 是否编译了所需要的全部 .c 或.o 文件
当出现如
my_graph.o:(.data+0x18): multiple definition of `MUSIC'
state.o:(.data+0x18): first defined here 时,
检查如下:
- 头文件里是否有定义 (应该只有声明,不应该有定义)
数组与指针
在C语言中,数组是不可拷贝的,因此当数组作为参数传递时,会退化成指针,所以sizeof宏得到的是一个指针的大小 [知乎]
函数内,参与运算的形参是实参的拷贝。而拷贝过程只发生了值传递,既传递了数组的地址,而把大小丢了。实参既数组本身你可以理解为一个地址加一个类型加一个大小。而形参就剩个地址了。这就是数组名作函数入参会退化为指针。[知乎]
关于 #231-D 警告和 #167 报错
关于使用自定义结构体后 #231-D: declaration is not visible outside of function
的#231-D警告,并引发 #167: argument of type "struct conf_data *" is incompatible with parameter of type "struct conf_data *"
的#167报错
检查头文件包含顺序,排错可以从main.c的第一个头文件开始推测,或者看看预编译结果(GCC和KEIL都可以设置输出预编译日志),千万不能让变量类型在需要使用它的函数声明之后出现
如图声明
如图初始化