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' 时,
检查如下:

  1. 是否编译了所需要的全部 .c 或.o 文件

当出现如
my_graph.o:(.data+0x18): multiple definition of `MUSIC'
state.o:(.data+0x18): first defined here 时,
检查如下:

  1. 头文件里是否有定义 (应该只有声明,不应该有定义)

数组与指针

在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都可以设置输出预编译日志),千万不能让变量类型在需要使用它的函数声明之后出现

如图声明
image
image

如图初始化
image

posted @ 2020-12-06 22:22  蓝天上的云℡  阅读(2091)  评论(0编辑  收藏  举报