目錄

廣告 AD

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

Header 到底要怎麼 include? 為甚麼都一直重複定義?

Static 和 Extern 到底是甚麼?

Forward declaration 聽起來好難…

廣告 AD

我們把下方結構稱作 Header Guard,當你的專案漸漸大起來了,Code 之間互動多了起來,c.c include a.hc.c 也 include b.hb.h 再 include a.h

a.h:

C

#include <stdio.h>

void func_a(){
    printf("FUNC A\n");
}

b.h:

C

#include <stdio.h>
#include "a.h"

void func_ab(){
    func_a();
    printf("FUNC B\n");
}

c.c

C

#include "a.h"
#include "b.h"

int main(){
    func_a();
    func_ab();
    return 0;
}

這時候你會發現:這樣 c.c 有兩份 a.h 的定義,直接編譯的話會出現:

bash

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。

text

#ifndef __TEST_H__
#define __TEST_H__

...

#endif  /* __TEST_H__ */

Reference


在專案中,把不同功能區分開來撰寫會比較好維護,這樣的話就會遇到要撰寫多個 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

C

#include <stdio.h>

void print(int num){
    printf(">> %d\n", num);
}

void add(int a, int b){
    print(a + b);
}

b.c

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

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,如下:

bash

/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,就可以順利編譯完成了。

C

static void print(int num){
    printf(">> %d\n", num);
}

// or

static void print(long long num){
    printf(">> %lld\n", num);
}

// output
>> 66666
>> 670592745
C++
那在 C++ 中,print 會被當作 overloading,編譯器會自動選擇正確的定義,或是我們可以選擇使用 namespace,在不同的 namespace 中各自定義不同的 print,這樣使用時加上 namespace 即可。

Reference


又是一個新的問題:你想要在不同的 c file 裡面都要用到某一個全域變數,但我要如何使用到別的 c file 上的變數呢?答案就是用 extern,在要使用其他 c file 的變數的地方宣告一個相同的變數,名稱也要相同,但在前面加上 extern,當編譯器遇到了 extern,編譯器知道說這個變數是別人家的,所以不會為這個變數分配空間,等到處理該變數的 c file 的時候,才會為他分配空間,最後再透過 linker 做連結。

下面我們看個例子,a.c 裡面想要用到 b.c 裡面的 count

a.c

C

#include <stdio.h>

void addCount();

int main(){
    printf("%d\n", count);
    addCount();
    printf("%d\n", count);
}

b.c

C

int count = 0;

void addCount(){
    count++;
}

但是直接使用是不行的,會顯示沒有宣告

bash

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

C

#include <stdio.h>

int count;
void addCount();

int main(){
    printf("%d\n", count);
    addCount();
    printf("%d\n", count);
}

bash

/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

C

#include <stdio.h>

extern int count;
void addCount();

int main(){
    printf("%d\n", count);
    addCount();
    printf("%d\n", count);
}

bash

0
1

最後來提一下建議使用 extern 的方式,最好是把要使用 extern 的宣告放到 header 中,然後讓其他要使用該變數的 c file 引入 header 做使用,這樣比較好維護,也一目瞭然這個變數會在其他使用,可能會被修改,此外也建議定義該變數的 c file 也要引入 header,編譯器會幫你檢查是否有不一致的問題,然後把 function 的宣告和 extern 的宣告分開使用。

a.c

C

#include <stdio.h>
#include "b.h"
#include "b_var.h"

int main(){
    printf("%d\n", count);
    addCount();
    printf("%d\n", count);
}

b.c

C

#include "b.h"

int count = 0;

void addCount(){
    count++;
}

b.h

C

extern int count;

b_var.h

C

void addCount();

Reference


這是一個撰寫的技巧,如果今天有兩個 struct/class 的 member 都要使用到對方,A 裡面有 B 的 pointer,B 裡面有 A 的 pointer,但由於 B 還沒有定義,因此會顯示錯誤,因此我們就要用到 Forward declaration。

如果我們用原本的做法:

main.c

C

typedef struct{
    B* b_ptr;
} A;

typedef struct B{
    A* a_ptr;
} B;

int main(){
    A a;
    B b;
    return 0;
}

會導致 B 沒有定義:

bash

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

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 的大小,會出現錯誤。

C

struct A{
    struct B b_var;
};

struct B{
    struct A a_var;
};

int main(){
    struct A a;
    struct B b;
    return 0;
}

編譯器顯示 struct B 定義不完全:

bash

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++ 使用場景

C++ 的話如果 某個 function 是兩個 class 的 friendly function,也會需要用到 forward declaration,如下:

main.cpp

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

廣告 AD