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
>> 670592745print 會被當作 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
如果你覺得這篇文章有用 可以考慮贊助飲料給大貓咪




