C/C++ 的小技巧:Header Guard、Static、Extern 和 Forward declaration

Header 到底要怎麼 include? 為甚麼都一直重複定義?
Static 和 Extern 到底是甚麼?
Forward declaration 聽起來好難…
Header Guard
我們把下方結構稱作 Header Guard,當你的專案漸漸大起來了,Code 之間互動多了起來,c.c
include a.h
,c.c
也 include b.h
,b.h
再 include a.h
,

a.h:
#include <stdio.h>
void func_a(){
printf("FUNC A\n");
}
b.h:
#include <stdio.h>
#include "a.h"
void func_ab(){
func_a();
printf("FUNC B\n");
}
c.c
#include "a.h"
#include "b.h"
int main(){
func_a();
func_ab();
return 0;
}
這時候你會發現:這樣 c.c
有兩份 a.h
的定義,直接編譯的話會出現:
In file included from b.h:2,
from c.c:2:
a.h:3:6: error: redefinition of 'func_a'
3 | void func_a(){
| ^~~~~~
In file included from c.c:1:
a.h:3:6: note: previous definition of 'func_a' with type 'void()'
3 | void func_a(){
| ^~~~~~
為了解決這個問題,我們要撰寫 Header Guard,確保我們只會有一份定義,至於定義的名稱可以自己定義,習慣上都用大寫和檔名來當作 key。
流程:如果我們先前沒有定義過 __TEST_H__
,則定義 __TEST_H__
,並且處理接下來的 Code 直到 endif
,因此我們要確保 __TEST_H__
只會出現在這個檔案中,其他檔案不能使用這個 define。
#ifndef __TEST_H__
#define __TEST_H__
...
#endif /* __TEST_H__ */
Reference
Static
在專案中,把不同功能區分開來撰寫會比較好維護,這樣的話就會遇到要撰寫多個 c file 和 header,多個 c file 之間會引用不同的 header,可能也會互相使用到對方的函式,如果今天我想要在不同的 c file 裡面都有一個相同名稱的函式,但實作內容不同,要如何實作?,甚至要如何管理不同 c file 中的可見度呢?答案就是使用 static
。
今天我們說到的 static
只侷限在 global 的 static,共有兩種:
- global static variable
- global static function
這兩個的差別不大,只是一個是變數,一個是函式而已,只要是 global static,就代表說這個東西只會存在該 translation unit 裡面而已,對於其他 translation unit 來說,他們根本就看不到這個東西,只有該 translation unit 獨享而已,運用這個特性,我們就可以做到控制能見度,達成在不同 c file 中宣告同樣名稱的函式了。
那什麼是 translation unit 呢?其實 translation unit 就是單一個 source file,像是 .c
或是 .cpp
的檔案,編譯器會將你 include 的 header 全部一層一層地展開,像洋蔥?然後去掉你用 #if
和 #endif
去掉的 code,就會是一個 translation unit,因此可以簡單當作每個 c file 就會是一個 translation unit。
下面我們來看看一個例子,今天我們要兩個功能,一個是加法,一個是乘法,然後把結果印出來,但由於運算結果的型態不同,我們各自寫了一個 print
。
a.c
#include <stdio.h>
void print(int num){
printf(">> %d\n", num);
}
void add(int a, int b){
print(a + b);
}
b.c
#include <stdio.h>
void print(long long num){
printf(">> %lld\n", num);
}
void mul(int a, int b){
print((long long)a * b);
}
c.c
void add(int a, int b);
void mul(int a, int b);
int main(){
int a = 12345;
int b = 54321;
add(a, b);
mul(a, b);
}
如果我們不加 static
,會造成在 link 的時候找到兩個同樣名稱的 print
,如下:
/usr/bin/ld: /tmp/ccxtCt0f.o: in function `print':
b.c:(.text+0x0): multiple definition of `print'; /tmp/ccz8Zhzf.o:a.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
如果加上 static
,就可以順利編譯完成了。
static void print(int num){
printf(">> %d\n", num);
}
// or
static void print(long long num){
printf(">> %lld\n", num);
}
// output
>> 66666
>> 670592745
print
會被當作 overloading,編譯器會自動選擇正確的定義,或是我們可以選擇使用 namespace,在不同的 namespace 中各自定義不同的 print
,這樣使用時加上 namespace 即可。Reference
Extern
又是一個新的問題:你想要在不同的 c file 裡面都要用到某一個全域變數,但我要如何使用到別的 c file 上的變數呢?答案就是用 extern,在要使用其他 c file 的變數的地方宣告一個相同的變數,名稱也要相同,但在前面加上 extern,當編譯器遇到了 extern,編譯器知道說這個變數是別人家的,所以不會為這個變數分配空間,等到處理該變數的 c file 的時候,才會為他分配空間,最後再透過 linker 做連結。
下面我們看個例子,a.c
裡面想要用到 b.c
裡面的 count
:
a.c
#include <stdio.h>
void addCount();
int main(){
printf("%d\n", count);
addCount();
printf("%d\n", count);
}
b.c
int count = 0;
void addCount(){
count++;
}
但是直接使用是不行的,會顯示沒有宣告
a.c: In function 'int main()':
a.c:6:20: error: 'count' was not declared in this scope
6 | printf("%d\n", count);
| ^~~~~
如果一樣宣告同樣的,會顯示重複定義:
a.c
#include <stdio.h>
int count;
void addCount();
int main(){
printf("%d\n", count);
addCount();
printf("%d\n", count);
}
/usr/bin/ld: /tmp/ccS083Zs.o:(.bss+0x0): multiple definition of `count'; /tmp/cc92FNKv.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status
那我們再加上 extern
就成功了~
a.c
#include <stdio.h>
extern int count;
void addCount();
int main(){
printf("%d\n", count);
addCount();
printf("%d\n", count);
}
0
1
最後來提一下建議使用 extern 的方式,最好是把要使用 extern 的宣告放到 header 中,然後讓其他要使用該變數的 c file 引入 header 做使用,這樣比較好維護,也一目瞭然這個變數會在其他使用,可能會被修改,此外也建議定義該變數的 c file 也要引入 header,編譯器會幫你檢查是否有不一致的問題,然後把 function 的宣告和 extern 的宣告分開使用。
a.c
#include <stdio.h>
#include "b.h"
#include "b_var.h"
int main(){
printf("%d\n", count);
addCount();
printf("%d\n", count);
}
b.c
#include "b.h"
int count = 0;
void addCount(){
count++;
}
b.h
extern int count;
b_var.h
void addCount();
Reference
Forward declaration
這是一個撰寫的技巧,如果今天有兩個 struct/class 的 member 都要使用到對方,A
裡面有 B
的 pointer,B
裡面有 A
的 pointer,但由於 B
還沒有定義,因此會顯示錯誤,因此我們就要用到 Forward declaration。
如果我們用原本的做法:
main.c
typedef struct{
B* b_ptr;
} A;
typedef struct B{
A* a_ptr;
} B;
int main(){
A a;
B b;
return 0;
}
會導致 B
沒有定義:
c.c:2:5: error: 'B' does not name a type
2 | B* b_ptr;
| ^
於是我們可以使用 Forward declaration,Forward declaration 叫做向前宣告,那方法也很簡單,就是在你要使用之前先宣告,但不定義,因為 C/C++ 要求要有宣告才能使用,因為可以事先做錯誤檢查,像是拼錯或是呼叫參數的錯誤。
上述例子,我們只需要在最前面使用 Forward declaration,也就是先宣告 B
的存在 typedef struct B B
:
main.c
typedef struct B B; // forward declaration
typedef struct{
B* b_ptr;
} A;
typedef struct B{
A* a_ptr;
} B;
int main(){
A a;
B b;
return 0;
}
前面的例子用的是 pointer,但不能使用變數,因為會互相定義對方,這樣一個 struct 的大小會無限大,且由於 struct B
的定義不完全,編譯器無法正確知道 struct 的大小,會出現錯誤。
struct A{
struct B b_var;
};
struct B{
struct A a_var;
};
int main(){
struct A a;
struct B b;
return 0;
}
編譯器顯示 struct B 定義不完全:
c.c:2:14: error: field 'b_var' has incomplete type 'B'
2 | struct B b_var;
| ^~~~~
c.c:2:12: note: forward declaration of 'struct B'
2 | struct B b_var;
| ^
C++ 的話如果 某個 function 是兩個 class 的 friendly function,也會需要用到 forward declaration,如下:
main.cpp
#include <iostream>
class B; // forward declaration
class A{
int val;
public:
A(int _v): val(_v) {}
friend void show(A &a, B &b);
};
class B{
int val;
public:
B(int _v): val(_v) {}
friend void show(A &a, B &b);
};
void show(A &a, B &b){
std::cout << a.val + b.val << std::endl;
}
int main(){
A a(10);
B b(5);
show(a, b);
}
參考:C++ TUTORIAL - FRIEND FUNCTIONS AND FRIEND CLASSES - 2020
Reference
後記
這篇東西有點多,總之就是紀錄遇過的狀況和解決的方法
順便做點研究,減少之後踩雷的機會XD
如果你覺得這篇文章有用 可以考慮贊助飲料給大貓咪