第 8 节

函数与头文件

函数

概述

**作用:**将一段经常使用的代码封装起来,减少重复代码

一个较大的程序,一般分为若干个程序块,每个模块实现特定的功能。

函数的定义

函数的定义一般主要有5个步骤:

1、返回值类型

2、函数名

3、参数表列

4、函数体语句

5、return 表达式

语法:

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

       函数体语句

       return表达式

}

运行/观察结果: 这段是语法或接口示例,重点观察写法;放入完整程序后再运行验证。

  • 返回值类型 :一个函数可以返回一个值。在函数定义中
  • 函数名:给函数起个名称
  • 参数列表:使用该函数时,传入的数据
  • 函数体语句:花括号内的代码,函数内需要执行的语句
  • return表达式: 和返回值类型挂钩,函数执行完后,返回相应的数据

**示例:**定义一个加法函数,实现两个数相加

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

运行/观察结果: 这段是语法或接口示例,重点观察写法;放入完整程序后再运行验证。

函数的调用

**功能:**使用定义好的函数

语法: 函数名(参数)

示例:

//函数定义
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;
}

运行/观察结果: 运行后会按输出语句打印对应内容,变量值可结合初始化、赋值和函数调用顺序推导。

总结:函数定义里小括号内称为形参,函数调用时传入的参数称为实参

值传递

  • 所谓值传递,就是函数调用时实参将数值传入给形参
  • 值传递时,==如果形参发生,并不会影响实参==

示例:

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;
}

运行/观察结果: 运行后会按输出语句打印对应内容,变量值可结合初始化、赋值和函数调用顺序推导。

总结: 值传递时,形参是修饰不了实参的

函数的常见样式

常见的函数样式有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;
}

运行/观察结果: 运行后会打印示例中的变量值或地址;地址值与运行环境有关,以同类对象的相对位置和指针变化为观察重点。

函数的声明

作用: 告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。

  • 函数的声明可以多次,但是函数的定义只能有一次

示例:

//声明可以多次,定义只能一次
//声明
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;
}

运行/观察结果: 运行后会按输出语句打印对应内容,变量值可结合初始化、赋值和函数调用顺序推导。

函数的分文件编写

**作用:**让代码结构更加清晰

前面几章我们主要在 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

整理 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

修改 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。不过这里先专心理解"头文件声明、源文件定义、主函数调用"这条线,因此暂时使用这份简单的教学代码。

修改 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也没有被刷新,先不用管代码提示的报错.

修改 main.cpp

打开:

src/main.cpp

写入:

#include "swap/swap.h"

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

    swap(a, b);

    return 0;
}

alt text

main.cpp 只负责调用函数,不再把 swap 函数的具体实现写在自己里面。

修改 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 决定"即可。

修改 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 函数的定义。

重新构建并运行

编译

alt text

此时你再看clangd的代码提示就不报错了.

alt text

然后运行:

alt text

输出类似:

a = 200
b = 100

可以看到,swap.cpp 里的函数确实被主程序调用到了。

分文件编写的核心理解

在这个模板中,函数分文件编写不是只把文件拆开,还要让 CMake 知道这些文件属于哪个库。

对应关系如下:

文件作用
src/swap/inc/swap/swap.h写函数声明,让其他 .cpp 文件知道怎么调用
src/swap/src/swap.cpp写函数定义,提供真正的实现
src/swap/CMakeLists.txtswap.cpp 编译成 swap_src_lib
src/CMakeLists.txtswap_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 函数的实现。

头文件的组织方式

刚才我们把 swap 函数的声明放进了 swap.h。其他源文件只要包含这个头文件,就能知道这个库提供了哪些函数。这些提供给外部代码使用的函数、类型和变量,通常统称为库的 API

头文件一般负责放置:

  1. 函数声明。
  2. 类型、结构体和类的声明或定义。
  3. 必要的常量与变量声明。
  4. 使用这些声明所必需的其他头文件。

头文件本身不能单独运行。修改头文件后,需要重新编译包含它的 .c.cpp 文件来验证。

纯 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++ 标准规定的命名方式,不代表项目中的头文件也应该省略扩展名。

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 工程基础上修改。可以只选择一种练习;如果连续练习,需要按照第二个例子的说明先恢复文件名。

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;
}

alt text

编译 swap.c 时,C 编译器不会定义 __cplusplus,因此会跳过 extern "C"。编译 main.cpp 时,C++ 编译器会看到 extern "C",从而按照 C 语言的链接规则寻找 swap.c 中的 swap 函数。

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++ 专有功能。

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.hmain.ccpp_interface.cpp需要,因为它是 C/C++ 边界
swap/swap.hswap.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

#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" 兼容部分。