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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM