跳到正文

Wiki

函式與標頭檔案

約 18 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

1.函式

1.1.概述

**作用:**將一段經常使用的程式碼封裝起來,減少重複程式碼

一個較大的程式,一般分為若干個程式塊,每個模組實現特定的功能。

1.2.函式的定義

函式的定義一般主要有5個步驟:

1、返回值型別

2、函式名

3、參數列列

4、函式體語句

5、return 表示式

語法:

返回值类型 函数名 (参数列表)
{

       函数体语句

       return表达式

}

執行結果: 此程式碼塊不含 main 函式,是語法、介面或分檔案示例,不能獨立執行,因此沒有終端輸出。

  • 返回值型別 :一個函式可以返回一個值。在函式定義中
  • 函式名:給函式起個名稱
  • 引數列表:使用該函式時,傳入的資料
  • 函式體語句:花括號內的程式碼,函式內需要執行的語句
  • return表示式: 和返回值型別掛鉤,函式執行完後,返回相應的資料

**示例:**定義一個加法函式,實現兩個數相加

//函数定义
int add(int num1, int num2)
{
	int sum = num1 + num2;
	return sum;
}

執行結果: 此程式碼塊不含 main 函式,是語法、介面或分檔案示例,不能獨立執行,因此沒有終端輸出。

1.3.函式的呼叫

**功能:**使用定義好的函式

語法: 函数名(参数)

示例:

//函数定义
int add(int num1, int num2) //定义中的num1,num2称为形式参数,简称形参
{
	int sum = num1 + num2;
	return sum;
}

int main() {

	int a = 10;
	int b = 10;
	//调用add函数
	int sum = add(a, b);//调用时的a,b称为实际参数,简称实参
	cout << "sum = " << sum << endl;

	a = 100;
	b = 100;

	sum = add(a, b);
	cout << "sum = " << sum << endl;


	return 0;
}

執行結果:

sum = 20
sum = 200

總結:函式定義裡小括號內稱為形參,函式呼叫時傳入的引數稱為實參

1.4.值傳遞

  • 所謂值傳遞,就是函式呼叫時實參將數值傳入給形參
  • 值傳遞時,==如果形參發生,並不會影響實參==

示例:

void swap(int num1, int num2)
{
	cout << "交换前:" << endl;
	cout << "num1 = " << num1 << endl;
	cout << "num2 = " << num2 << endl;

	int temp = num1;
	num1 = num2;
	num2 = temp;

	cout << "交换后:" << endl;
	cout << "num1 = " << num1 << endl;
	cout << "num2 = " << num2 << endl;

	//return ; 当函数声明时候,不需要返回值,可以不写return
}

int main() {

	int a = 10;
	int b = 20;

	swap(a, b);

	cout << "mian中的 a = " << a << endl;
	cout << "mian中的 b = " << b << endl;


	return 0;
}

執行結果:

交换前:
num1 = 10
num2 = 20
交换后:
num1 = 20
num2 = 10
mian中的 a = 10
mian中的 b = 20

總結: 值傳遞時,形參是修飾不了實參的

1.5.函式的常見樣式

常見的函式樣式有4種

  1. 無參無返
  2. 有參無返
  3. 無參有返
  4. 有參有返

示例:

//函数常见样式
//1、 无参无返
void test01()
{
	//void a = 10; //无类型不可以创建变量,原因无法分配内存
	cout << "this is test01" << endl;
	//test01(); 函数调用
}

//2、 有参无返
void test02(int a)
{
	cout << "this is test02" << endl;
	cout << "a = " << a << endl;
}

//3、无参有返
int test03()
{
	cout << "this is test03 " << endl;
	return 10;
}

//4、有参有返
int test04(int a, int b)
{
	cout << "this is test04 " << endl;
	int sum = a + b;
	return sum;
}

執行結果: 此程式碼塊不含 main 函式,是語法、介面或分檔案示例,不能獨立執行,因此沒有終端輸出。

1.6.函式的宣告

作用: 告訴編譯器函式名稱及如何呼叫函式。函式的實際主體可以單獨定義。

  • 函式的宣告可以多次,但是函式的定義只能有一次

示例:

//声明可以多次,定义只能一次
//声明
int max(int a, int b);
int max(int a, int b);
//定义
int max(int a, int b)
{
	return a > b ? a : b;
}

int main() {

	int a = 100;
	int b = 200;

	cout << max(a, b) << endl;


	return 0;
}

執行結果:

200

1.7.函式的分檔案編寫

**作用:**讓程式碼結構更加清晰

前面幾章我們主要在 src/main.cpp 裡寫程式碼。這樣適合學習語法,但是程式稍微大一點,就會變得很亂。

函式的分檔案編寫,就是把程式碼拆成:

  1. 標頭檔案 .h:寫函式宣告。
  2. 原始檔 .cpp:寫函式定義。
  3. main.cpp:呼叫函式。

C++開發環境與第一個工程 開始,本教程一直使用同一個 CMake 模板。寫到這裡時,你的工程大致還是這個結構:

src/
├── CMakeLists.txt
├── main.cpp
├── lib1/
│   ├── CMakeLists.txt
│   ├── inc/lib1/eigen3_test.hpp
│   └── src/eigen3_test.cpp
└── lib2/
    ├── CMakeLists.txt
    ├── inc/lib2/eigen3_test.hpp
    └── src/eigen3_test.cpp

lib1lib2 是模板自帶的兩個示例庫。本節第一次整理它們:刪掉暫時用不到的 lib2,把 lib1 改成我們自己的 swap 小庫,再把模板自帶的示例標頭檔案和原始檔分別重新命名為 swap.hswap.cpp,用它們完成本節的函式分檔案編寫示例。

改完之後,目錄結構變成:

src/
├── CMakeLists.txt
├── main.cpp
└── swap/
    ├── CMakeLists.txt
    ├── inc/swap/swap.h
    └── src/swap.cpp

也就是說:

  1. 刪除本例暫時用不到的 lib2
  2. lib1 目錄重新命名為 swap
  3. inc/lib1 目錄重新命名為 inc/swap,再把其中的 eigen3_test.hpp 重新命名為 swap.h
  4. src/eigen3_test.cpp 重新命名為 src/swap.cpp

1.7.1.整理 lib1 和 lib2

首先刪除掉lib2,這裡不需要.

alt text

  1. lib1 目錄重新命名為 swap:
  2. inc/lib1 目錄重新命名為 inc/swap,再把其中的 eigen3_test.hpp 重新命名為 swap.h:
  3. src/eigen3_test.cpp 重新命名為 src/swap.cpp:

alt text

1.7.2.修改 swap.h

開啟:

src/swap/inc/swap/swap.h

寫入:

#include<iostream>
using namespace std;

void swap(int a, int b);

alt text

這是我們第一次把函式宣告單獨放進標頭檔案。swap.h 只負責告訴其他原始檔:工程中存在一個名為 swap、接收兩個 int 引數的函式。

補充說明:真實工程裡通常不建議在標頭檔案中寫 using namespace std;,函式名也儘量避免直接叫 swap,因為標準庫裡有 std::swap。不過這裡先專心理解"標頭檔案宣告、原始檔定義、主函式呼叫"這條線,因此暫時使用這份簡單的教學程式碼。

1.7.3.修改 swap.cpp

開啟:

src/swap/src/swap.cpp

寫入:

#include "swap/swap.h"

void swap(int a, int b)
{
	int temp = a;
	a = b;
	b = temp;

	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
}

alt text

這裡的 #include "swap/swap.h" 表示:swap.cpp 要先看到函式宣告,再提供函式定義。

這時候有VScode的報錯無所謂,是因為CMakeLists沒修改,其次clangd也沒有被重新整理,先不用管程式碼提示的報錯.

1.7.4.修改 main.cpp

開啟:

src/main.cpp

寫入:

#include "swap/swap.h"

int main()
{
	int a = 100;
	int b = 200;

	swap(a, b);

	return 0;
}

執行結果(與上面的 swap.cpp 一起編譯並連結):

a = 200
b = 100

alt text

main.cpp 只負責呼叫函式,不再把 swap 函式的具體實現寫在自己裡面。

1.7.5.修改 src/swap/CMakeLists.txt

開啟:

src/swap/CMakeLists.txt

這個檔案原來來自 lib1,所以裡面還有 lib1 和 Eigen 示例的痕跡。

先把第一行從:

set(PREFIX "lib1")

改成:

set(PREFIX "swap")

這樣 CMake 會建立一個名為:

swap_src_lib

的庫。

swap 這個例子不需要 Eigen,所以刪除 Third-party dependencies 區塊裡的 Eigen 依賴:

find_package(Eigen3 REQUIRED)

target_link_libraries(${PREFIX}_src_lib
  PUBLIC
    Eigen3::Eigen
)

只用修改以上兩項,其他的均不用修改,下面介紹一下比較重點的地方:

整理後,src/swap/CMakeLists.txt 可以寫成:

set(PREFIX "swap")

file(GLOB_RECURSE ${PREFIX}_SRC_LIST CONFIGURE_DEPENDS
  "${CMAKE_CURRENT_LIST_DIR}/src/*.c"
  "${CMAKE_CURRENT_LIST_DIR}/src/*.cpp"
)

add_library(${PREFIX}_src_lib SHARED
  ${${PREFIX}_SRC_LIST}
)

target_include_directories(${PREFIX}_src_lib
  PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/inc>
    $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

target_link_libraries(${PREFIX}_src_lib
  PUBLIC
    project_options
  PRIVATE
    project_warnings
)

# ========================
# Third-party dependencies
# ========================

# =======
# Install
# =======
install(TARGETS ${PREFIX}_src_lib
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/inc/"
  DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

這裡最重要的幾行是:

file(GLOB_RECURSE ${PREFIX}_SRC_LIST CONFIGURE_DEPENDS
  "${CMAKE_CURRENT_LIST_DIR}/src/*.c"
  "${CMAKE_CURRENT_LIST_DIR}/src/*.cpp"
)

它會把 src/swap/src/ 裡面的 .cpp 檔案收集進來,所以 swap.cpp 會參與編譯。

還有:

target_include_directories(${PREFIX}_src_lib
  PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/inc>
    $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

它把 src/swap/inc/ 加入標頭檔案搜尋路徑,所以程式碼裡才能寫:

#include "swap/swap.h"

注意:不要寫成 #include "inc/swap/swap.h"。CMake 已經把 src/swap/inc/ 告訴編譯器了,所以 include 時從 inc/ 裡面繼續往後寫,也就是 swap/swap.h

完整 CMake 寫法會在 CMake工程模板 中系統講解。這裡先知道"標頭檔案放進 inc/,原始檔放進 src/,庫名由 PREFIX 決定"即可。

1.7.6.修改 src/CMakeLists.txt

開啟:

src/CMakeLists.txt

原來模板裡有:

add_subdirectory(lib1)
add_subdirectory(lib2)

改成:

add_subdirectory(swap)

原來的模板裡允許lib1呼叫lib2,現在lib2沒了,所以刪掉這部分:

target_link_libraries(lib1_src_lib
  PRIVATE
    lib2_src_lib
)

上面的這個直接刪除.

原來模板裡還會連結:

target_link_libraries(${PROJECT_NAME}
  PRIVATE
    lib1_src_lib
    lib2_src_lib
)

改成:

target_link_libraries(${PROJECT_NAME}
  PRIVATE
    swap_src_lib
)

只用修改以上三項,其他的均不用修改,下面介紹一下比較重點的地方:

整理後,src/CMakeLists.txt 可以寫成:

add_executable(${PROJECT_NAME}
  ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
)

target_link_libraries(${PROJECT_NAME}
  PRIVATE
    project_options
    project_warnings
)

add_subdirectory(swap)

target_link_libraries(${PROJECT_NAME}
  PRIVATE
    swap_src_lib
)

set_target_properties(${PROJECT_NAME} PROPERTIES
  INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}"
)

install(TARGETS ${PROJECT_NAME}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

這兩處最關鍵:

add_subdirectory(swap)

意思是:讓 CMake 進入 src/swap/,讀取 src/swap/CMakeLists.txt

target_link_libraries(${PROJECT_NAME}
  PRIVATE
    swap_src_lib
)

意思是:主程式呼叫了 swap 函式,所以主程式要連結 swap_src_lib,否則連結器找不到 swap 函式的定義。

1.7.7.重新構建並執行

編譯

alt text

此時你再看clangd的程式碼提示就不報錯了.

alt text

然後執行:

alt text

輸出類似:

a = 200
b = 100

可以看到,swap.cpp 裡的函式確實被主程式呼叫到了。

1.7.8.分檔案編寫的核心理解

在這個模板中,函式分檔案編寫不是隻把檔案拆開,還要讓 CMake 知道這些檔案屬於哪個庫。

對應關係如下:

檔案 作用
src/swap/inc/swap/swap.h 寫函式宣告,讓其他 .cpp 檔案知道怎麼呼叫
src/swap/src/swap.cpp 寫函式定義,提供真正的實現
src/swap/CMakeLists.txt swap.cpp 編譯成 swap_src_lib
src/CMakeLists.txt swap_src_lib 連結到主程式
src/main.cpp #include "swap/swap.h" 後呼叫 swap(a, b)

如果漏了 #include "swap/swap.h",編譯器不知道 swap 函式的宣告。

如果漏了 add_subdirectory(swap),CMake 不會進入 src/swap

如果漏了:

target_link_libraries(${PROJECT_NAME}
  PRIVATE
    swap_src_lib
)

連結器找不到 swap 函式的實現。

2.標頭檔案的組織方式

剛才我們把 swap 函式的宣告放進了 swap.h。其他原始檔只要包含這個標頭檔案,就能知道這個庫提供了哪些函式。這些提供給外部程式碼使用的函式、型別和變數,通常統稱為庫的 API

標頭檔案一般負責放置:

  1. 函式宣告。
  2. 型別、結構體和類的宣告或定義。
  3. 必要的常量與變數宣告。
  4. 使用這些宣告所必需的其他標頭檔案。

標頭檔案本身不能單獨執行。修改標頭檔案後,需要重新編譯包含它的 .c.cpp 檔案來驗證。

2.1.純 C++ 工程的標頭檔案

本章的 swap 庫由 swap.cpp 實現,並由 main.cpp 呼叫,所有原始檔都按照 C++ 程式碼編譯。因此,我們先來看普通 C++ 標頭檔案的完整寫法。

純 C++ 工程推薦使用下面的標頭檔案模板:

#ifndef __FILE_NAME_H_  // 防止头文件被重复包含,使用时要修改宏名
#define __FILE_NAME_H_

/* 头文件内容开始 */

// 在这里编写必要的预处理指令、类型定义、函数声明和变量声明

/* 头文件内容结束 */

#endif  // __FILE_NAME_H_

使用模板時,要根據實際檔名修改保護宏。例如:

  • swap.h 可以使用 __SWAP_H_
  • camera.hpp 可以使用 __CAMERA_HPP_

本章的 swap.h 可以整理成:

#ifndef __SWAP_H_
#define __SWAP_H_

#include <iostream>
using namespace std;

void swap(int a, int b);

#endif  // __SWAP_H_

這才是C++標頭檔案最規範的形態.

編譯執行後:

alt text

這裡開頭的 #ifndef#define,以及末尾的 #endif,組成了 標頭檔案保護宏。同一個標頭檔案即使被間接包含多次,其中的內容也只會參與一次編譯,避免出現重複定義等問題。

純 C++ 專案中也經常使用 .hpp 副檔名,用來強調這是 C++ 標頭檔案;.h 同樣可以使用。專案自己的標頭檔案通常應該保留副檔名,例如:

#include "swap/swap.h"

標準庫中的 <iostream><string> 等名字沒有副檔名,是 C++ 標準規定的命名方式,不代表專案中的標頭檔案也應該省略副檔名。

2.2.C 介面以及 C/C++ 混合工程的標頭檔案

一個工程中可以同時存在 .c.cpp 檔案。例如,既可以讓 C++ 原始檔xxx.cpp呼叫 .c 檔案中的函式,也可以讓 main.c 呼叫 .cpp 檔案中的函式。

但是,C 與 C++ 編譯器處理函式名稱的方式並不完全相同。只要呼叫跨越了 C 和 C++ 的邊界,就應提供一個相容的介面標頭檔案。

標頭檔案中的 extern "C" 只會在它被 C++ 編譯器處理時生效,作用是告訴 C++ 編譯器:這些函式按照 C 語言的連結規則處理。

對於需要在 C 和 C++ 之間使用的介面,推薦使用下面的標頭檔案模板,且建議副檔名為.h

#ifndef __FILE_NAME_H_  // 防止头文件被重复包含,使用时要修改宏名
#define __FILE_NAME_H_

#ifdef __cplusplus      // 当前文件被 C++ 编译器处理时才会成立
extern "C" {
#endif

/* 头文件内容开始 */

// 在这里编写必要的预处理指令、类型定义、函数声明和变量声明

/* 头文件内容结束 */

#ifdef __cplusplus
}
#endif

#endif  // __FILE_NAME_H_

下面的兩個例子是兩種獨立的工程形式,都在前面完成的 swap 工程基礎上修改。可以只選擇一種練習;如果連續練習,需要按照第二個例子的說明先恢復檔名。

2.2.1.main.cpp 呼叫 swap.c

這個例子保留 C++ 編寫的 main.cpp,把 swap 函式改為使用 C 語言實現。

首先將 swap.cpp 重新命名為 swap.c

改完後的主要目錄結構為:

src/
├── CMakeLists.txt
├── main.cpp
└── swap/
    ├── CMakeLists.txt
    ├── inc/swap/swap.h
    └── src/swap.c

alt text

因為 src/swap/CMakeLists.txt 已經會同時收集 .c.cpp 檔案,所以這裡只需重新命名檔案,不用修改這個 CMake 檔案。

src/swap/inc/swap/swap.h 改成可以同時被 C 和 C++ 包含的形式:

#ifndef __SWAP_H_
#define __SWAP_H_

#ifdef __cplusplus
extern "C" {
#endif

void swap(int a, int b);

#ifdef __cplusplus
}
#endif

#endif  // __SWAP_H_

注意,這個標頭檔案可能被 C 編譯器處理,所以不能再在其中包含 <iostream> 或編寫 using namespace std;

src/swap/src/swap.c 中使用 C 語言實現 swap

#include "swap/swap.h"
#include <stdio.h>

void swap(int a, int b)
{
    int temp = a;
    a = b;
    b = temp;

    printf("a = %d\n", a);
    printf("b = %d\n", b);
}

src/main.cpp 仍然是 C++ 原始檔,且不用做任何更改:

#include "swap/swap.h"

int main()
{
    int a = 100;
    int b = 200;

    swap(a, b);
    return 0;
}

執行結果(與上面的 swap.c 一起編譯並連結):

a = 200
b = 100

alt text

編譯 swap.c 時,C 編譯器不會定義 __cplusplus,因此會跳過 extern "C"。編譯 main.cpp 時,C++ 編譯器會看到 extern "C",從而按照 C 語言的連結規則尋找 swap.c 中的 swap 函式。

2.2.2.main.c 呼叫 swap.cpp

難點,在STM32HAL庫中常用

這個例子反過來:入口檔案使用 C 語言,而 swap 庫繼續使用 C++ 實現。

如果剛做完上一個例子,先把 swap.c 改回 swap.cpp

接著把入口檔案 main.cpp 重新命名為 main.c

alt text

改完後的主要目錄結構為:

src/
├── CMakeLists.txt
├── main.c
└── swap/
    ├── CMakeLists.txt
    ├── inc/swap/swap.h
    └── src/swap.cpp

檔名發生變化後,還要開啟 src/CMakeLists.txt,把:

add_executable(${PROJECT_NAME}
  ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
)

改成:

add_executable(${PROJECT_NAME}
  ${CMAKE_CURRENT_SOURCE_DIR}/main.c
)

src/swap/inc/swap/swap.h 繼續使用前面的相容寫法:

#ifndef __SWAP_H_
#define __SWAP_H_

#ifdef __cplusplus
extern "C" {
#endif

void swap(int a, int b);

#ifdef __cplusplus
}
#endif

#endif  // __SWAP_H_

src/swap/src/swap.cpp 中使用 C++ 實現 swap

#include "swap/swap.h"
#include <iostream>

void swap(int a, int b)
{
    int temp = a;
    a = b;
    b = temp;

    std::cout << "a = " << a << std::endl;
    std::cout << "b = " << b << std::endl;
}

swap.cpp 必須包含 swap.h。C++ 編譯器讀取標頭檔案後,才會知道這個 swap 函式需要按照 C 語言的連結規則對外提供。

最後在 src/main.c 中呼叫它,main.c仍然沿用main.cpp不需要做任何修改:

#include "swap/swap.h"

int main(void)
{
    int a = 100;
    int b = 200;

    swap(a, b);
    return 0;
}

main.c 只通過標頭檔案呼叫 swap,不需要理解 std::cout 等 C++ 寫法。C++ 程式碼全部留在 swap.cpp 中。

只有需要跨越 C/C++ 邊界的函式才需要這種相容寫法,其他 .cpp 檔案之間仍然按照普通 C++ 方式呼叫。

模板的頂層 CMakeLists.txt 已經通過 LANGUAGES C CXX 啟用了 C 和 C++。CMake 會用 C 編譯器編譯 main.c,用 C++ 編譯器編譯 swap.cpp,並把 swap.cpp 構建成模板中的 swap_src_lib 共享庫,最後再將它連結給主程式。

extern "C" 只改變連結規則,並不會把 C++ 程式碼自動轉換成 C。放在其中的介面應使用 C 能理解的型別和寫法,不要使用函式過載、類、模板或 C++ 引用等 C++ 專有功能。

2.2.3.main.c 呼叫 swap.cpp(進階)

難點,在STM32HAL庫中常用

上一個例子直接把 swap 函式做成了 C 介面。這樣可以跑通,但也帶來一個限制:swap.h 必須讓 C 編譯器看得懂,所以裡面不能寫名稱空間、類、模板、函式過載、C++ 引用等 C++ 專有寫法。

更常見的做法是再加一層 C 介面檔案

  1. main.c 只調用一個 C 能理解的函式,例如 cpp_main()
  2. cpp_interface.cpp 負責進入 C++ 世界。
  3. swap.cpp 保持普通 C++ 寫法,可以使用名稱空間。

改完後的主要目錄結構為:

src/
├── CMakeLists.txt
├── cpp_interface.cpp
├── cpp_interface.h
├── main.c
└── swap/
    ├── CMakeLists.txt
    ├── inc/swap/swap.h
    └── src/swap.cpp

先在 src/ 下建立 cpp_interface.h

#ifndef __CPP_INTERFACE_H_
#define __CPP_INTERFACE_H_

#ifdef __cplusplus
extern "C" {
#endif

int cpp_main(void);

#ifdef __cplusplus
}
#endif

#endif  // __CPP_INTERFACE_H_

這個標頭檔案才是真正給 main.c 使用的 C 介面。它裡面只宣告 cpp_main(void),引數和返回值都是 C 能理解的型別。

然後修改 src/main.c

#include "cpp_interface.h"

int main(void)
{
    return cpp_main();
}

此時 main.c 不再包含 swap/swap.h,也不再直接呼叫 swap。它只負責啟動 C++ 側的 cpp_main()

接著把 src/swap/inc/swap/swap.h 改回普通 C++ 標頭檔案,並給 swap 加上名稱空間:

#ifndef __SWAP_H_
#define __SWAP_H_

namespace swap_demo
{
void swap(int a, int b);
}

#endif  // __SWAP_H_

注意,這個 swap.h 現在已經不是 C 介面標頭檔案了。它裡面有 namespace,所以不能再讓 main.c 直接包含。

這裡也不需要給 swap.h 加:

#ifdef __cplusplus
extern "C" {
#endif

原因是:swap_demo::swap 本來就是一個普通 C++ 函式,我們希望它保留 C++ 的名稱空間寫法。extern "C" 應該只放在 C 程式碼真正要呼叫的那層介面上,也就是前面的 cpp_interface.h 裡的 cpp_main(void)

換句話說,進階版中有兩個標頭檔案:

標頭檔案 誰來包含 是否需要 extern "C"
cpp_interface.h main.ccpp_interface.cpp 需要,因為它是 C/C++ 邊界
swap/swap.h swap.cppcpp_interface.cpp 不需要,因為它是純 C++ 標頭檔案

繼續修改 src/swap/src/swap.cpp

#include "swap/swap.h"
#include <iostream>

namespace swap_demo
{
void swap(int a, int b)
{
    int temp = a;
    a = b;
    b = temp;

    std::cout << "a = " << a << std::endl;
    std::cout << "b = " << b << std::endl;
}
}

最後建立 src/cpp_interface.cpp

#include "cpp_interface.h"
#include "swap/swap.h"

int cpp_main(void)
{
    int a = 100;
    int b = 200;

    swap_demo::swap(a, b);

    return 0;
}

cpp_interface.cpp 是 C 和 C++ 的分界線。它包含了 cpp_interface.h,所以 cpp_main() 會按照 C 語言的連結規則對外提供;它本身又是 .cpp 檔案,所以內部可以正常呼叫 swap_demo::swap(a, b) 這種 C++ 寫法。

檔案增加後,還要開啟 src/CMakeLists.txt,把 cpp_interface.cpp 加入主程式:

add_executable(${PROJECT_NAME}
  ${CMAKE_CURRENT_SOURCE_DIR}/main.c
  ${CMAKE_CURRENT_SOURCE_DIR}/cpp_interface.cpp
)

這裡不用把 cpp_interface.h 寫進 add_executable()。CMake 主要負責把 .c.cpp 這類原始檔交給編譯器編譯,標頭檔案本身不會單獨編譯成一個目標檔案。

cpp_interface.h 是通過程式碼裡的 #include 被找到的:

#include "cpp_interface.h"

因為 main.ccpp_interface.cppcpp_interface.h 都在 src/ 同一層目錄下,所以這裡使用雙引號包含時,編譯器可以從當前原始檔所在目錄找到它。

如果以後把 cpp_interface.h 移到單獨的 inc/ 目錄,就需要再用 target_include_directories() 把那個目錄加入標頭檔案搜尋路徑。當前這個例子放在同一層目錄,所以不需要額外修改 CMake。

src/swap/CMakeLists.txt 不需要修改,因為它本來就會收集 src/swap/src/ 目錄下的 .cpp 檔案,swap.cpp 仍然會被編譯進 swap_src_lib

編譯執行後,輸出仍然類似:

a = 200
b = 100

這個進階寫法的重點是:extern "C" 只放在最外層、最簡單的介面上,也就是 cpp_main(void)。真正的 C++ 程式碼放在 cpp_interface.cpp 後面,swap 可以使用名稱空間,以後也可以繼續使用類、模板、函式過載等 C++ 功能,只要這些 C++ 專有內容不要直接暴露給 main.c

2.3.#include <...>#include "..."

兩種寫法都用於包含標頭檔案,但常見用途不同:

#include <iostream>
#include <opencv2/opencv.hpp>

尖括號通常用於標準庫或已經安裝的第三方庫標頭檔案。

#include "swap/swap.h"

雙引號通常用於當前工程自己的標頭檔案。這裡能夠寫成 "swap/swap.h",是因為 CMake 已經把 src/swap/inc/ 新增到了編譯器的標頭檔案搜尋路徑中。

無論使用 C 還是 C++,都應為標頭檔案新增保護宏;只有標頭檔案需要作為 C 與 C++ 之間的介面時,才在保護宏內部增加 extern "C" 相容部分。