编译链接的应用
Compiler(编译器)
简单陈列下 C/C++ 编译器
GNU GCC
Clang
MSVC
Configure/Build Tools(构建工具)
Makefile
根据 Makefile 生成 compile_commands.json
工具:compiledb
使用示例
简单多文件:main.c hello.c fun.c function.h 为例
项目目录结构如下所示
.
|----Makefile
|----main.c
|----hello.c
|----fun.c
|----function.h
Version 1.0
Hello: main.c hello.c fun.c
gcc -o Hello main.c hello.c fun.c lm
Version 2.0
CC = gcc
TARGET = Hello
OBJ = main.o hello.o fun.o
$(TARGET): $(OBJ)
$(CC) -o $(TARGET) $(OBJ)
main.o: main.c function.h
$(CC) -c main.c
hello.o: hello.c function.h
$(CC) -c hello.c
fun.o: fun.c function.c
$(CC) -c fun.c
CMake
使用示例
以一个简单的多文件项目为例
演示环境使用 vscode,为了 cmake 的支持,需要安装两个插件:
项目目录结构如下所示
.
├── CMakeLists.txt
├── include
│ └── A.hpp
├── lib
│ └── A.cpp
└── src
└── main.cpp
// A.hpp
#ifndef A_H
#define A_H
class A {
private:
int a_;
int b_;
public:
A(int a, int b);
int Add();
};
#endif_ /* A_H */_
// A.cpp
#include "A.hpp"
A::A(int a, int b) : a_(a), b_(b) {
}
int A::Add() {
return a_ + b_;
}
// main.cpp
#include <iostream>
#include "A.hpp"
int main() {
int a = 1, b = 2;
A c{a, b};
std::cout << "a + b: " << c.Add() << std::endl;
return 0;
}
我们的目标很简单,将 A.cpp 编译成库文件,在 main.cpp 中链接该库文件,最终生成一个可执行文件。
CMakeLists.txt 如下所示
cmake_minimum_required(VERSION 3.10.0)
project(example VERSION 0.1.0 LANGUAGES C CXX)
add_library(libA lib/A.cpp)
target_include_directories(libA PUBLIC include)
add_executable(example src/main.cpp)
target_link_libraries(example PRIVATE libA)
使用 cmake 进行目标构建时相当直观,add_executable 用于生成可执行文件,add_library 用于生成库文件。
仅以 add_library 做一说明,add_executable 的解读方式是类似,不再赘述。
add_library(<name> [<type>] [EXCLUDE_FROM_ALL] <sources>...)
<name>指待生成的 TARGET 的名称<type>指定库的类型,静态库/共享库(默认)<sources>指定源文件
构建命令如下所示
mkdir build && cd build
cmake ..
make
./main
不出意外的话,此时你已经能看到 a + b: 3 的输出信息了。
配合 clangd 提供 Intelligent
clangd 是 C++ 语言的 lsp
clangd 需要 compile_commands.json 来提供 intelligence support。
compile_commands.json是一种用于描述如何编译单个文件的 JSON 格式文件,广泛应用于 C/C++ 开发环境中。它通常被编辑器、构建工具和静态分析工具用来理解项目的编译选项,以便提供代码补全、语法检查、重构支持等功能。此文件一般由构建系统生成。
使用 CMake 生成 compile_commands.json 的方法:
# 第一种
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# 第二种
# 命令行参数指定 cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1
据其行为表现观测(先叠甲,不一定对),clangd 检测语言所需支持版本的方式仅仅是从里面查找 -std 参数的值。
现有如下两种方式可以影响 compile_commands.json 中的版本:
# 第一种
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 17)
# 第二种
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17")
可见性
Examples of when PUBLIC/PRIVATE/INTERFACE should be used in cmake
PRIVATE PUBLIC INTERFACE
大体来说,权限关键词只有当构建目标为库文件时才有实际意义,因为它们的作用是控制库的依赖和头文件的暴露情况。将可执行文件作为依赖的情况几乎是没有的,因此当构建可执行文件时,将权限设置为 PRIVATE 即可。
target_include_directories(<target> [PRIVATE | PUBLIC | INTERFACE] <item>)
控制构建目标的所依赖库的头文件的可见性。
target_include_libraries(<target> [PRIVATE | PUBLIC | INTERFACE] <item>)
控制构建目标的所依赖库的链接时的可见性。
- 若一个库 libA 的内部实现依赖 libB,并且对于 libA 使用者只需要调用 libA本身提供的 API,那么就可以将权限设置为
PRIVATE,libA的使用者将接触不到 libB的 API。 - 若一个库 libA 的内部实现依赖 libB,并且对于 libA 使用者除调用 libA本身提供的 API 之外还需要调用 libB提供的 API,那么就可以将权限设置为
PUBLIC,libA的使用者也能获取到 libB的 API。(常见的一种情况是 libB是一个比较大的库,比如 Qt)
对曾经疑问的回答:
Q: 静态库如果已经把所有目标文件都打包好了,权限是不是没有意义了。
A: 静态库和共享库的区别,要从链接生成可执行文件的角度看。权限控制的仅仅是库的依赖对库的使用者的可见性,无关静态库或共享库。
导入依赖
find_package
find_package(<PackageName> [<version>] [REQUIRED] [COMPONENTS <components>...])
find_package 有两种搜索模式:
Module mode
这是 CMake 默认的方式,它会查找 <PackageName>Config.cmake 或者 Find<PackageName>.cmake 文件。
Config mode
一些库提供自己的 CMake 配置文件(通常是 <lowercaseName>Config.cmake 或者 <PackageName>Targets.cmake),这些文件通常与库一起分发,并且当你安装这个库的时候也会被安装到特定位置。在这种情况下,你可以直接使用 find_package(<PackageName>),其中 <PackageName> 是库的名称。
FAQ
在导入 bupt_can 队库的过程中遇到 find_package 找不到对应库文件的问题,尽管已经加上了 REQUIRED 参数 cmake 也没有报错。这个帖子遇到了和我类似的问题,所以这并非 cmake 的问题,而是我使用的问题。
现代的 CMake 以 Target 这一概念为核心进行可执行文件以及库文件的构建,队库中 CMakeListst.txt 对库文件的安装部分,仅仅导出了 target,所以当使用老的方式进行导入时会出错。
一个典型的老式 CMakeListst.txt 案例:
cmake_minimum_required(VERSION 3.8)
project(motion_controller)
set(EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99")
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
find_package(ament_cmake REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(rclcpp REQUIRED)
add_executable(turtle_painter src/turtle_painter.cpp)
target_include_directories(turtle_painter PRIVATE ${rclcpp_INCLUDE_DIRS}
${geometry_msgs_INCLUDE_DIRS})
target_link_libraries(turtle_painter ${rclcpp_LIBRARIES}
${geometry_msgs_LIBRARIES})
Macros
CMAKE_SOURCE_DIRCMAKE_BINARY_DIRCMAKE_CXX_EXTENSIONS
该变量告诉 CMake 采用更加通用的编译参数,比如这个开关打开,传递给 GCC 的参数就会是 -std=c++14 而不是 -std=gnu++14。
CMakePresets
ROS 构建工具
由于 ROS 中使用的 catkin_make 和 ament_cmake 本质上都是 cmake 套壳,由是记录于此。
ROS1
catkin make 是 ROS1 中对 CMake 构建做的一层 wrapper catkin_tools 是更为推荐使用的工具。
正确姿势
以下是目录结构示例:
./your_workspace
├── build
├── devel
├── logs
└── src
./src
├── insert_cc.sh
├── rm_control
│ ├── Doxyfile
│ ├── LICENSE
│ ├── README.md
│ ├── rm_common
│ ├── rm_control
│ ├── rm_dbus
│ ├── rm_gazebo
│ ├── rm_hw
│ ├── rm_msgs
│ ├── rm_referee
│ └── rm_vt
└── rm_controllers
├── Doxyfile
├── LICENSE
├── README.md
├── gpio_controller
├── mimic_joint_controller
├── rm_calibration_controllers
├── rm_chassis_controllers
├── rm_controllers
├── rm_gimbal_controllers
├── rm_orientation_controller
├── rm_shooter_controllers
├── robot_state_controller
└── tof_radar_controller
在 workspace 目录下先安装依赖:
rosdep install --from-paths . --ignore-src
进行编译构建:
catkin build
接下来请在 src 目录下创建一个脚本,它的作用是为 build 目录下各个 package 的 compile_commands.json 创建软链接置于 src 目录下各个 package 中。如此一来,vscode + clangd lsp 就能为项目正确地提供智能提示了。
GPT 秘制小脚本内容如下:
#!/bin/bash
# 设置工作空间路径为当前目录的上一级
WORKSPACE_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BUILD_DIR="$WORKSPACE_ROOT/build"
SRC_DIR="$WORKSPACE_ROOT/src"
echo "Searching for compile_commands.json files in: $BUILD_DIR"
# 遍历 build 目录下所有 compile_commands.json
find "$BUILD_DIR" -mindepth 2 -maxdepth 2 -name "compile_commands.json" | while read -r json_path; do
package_name=$(basename "$(dirname "$json_path")")
echo "Looking for package: $package_name"
# 在 src 目录下递归查找同名 package 文件夹
match_path=$(find "$SRC_DIR" -type d -name "$package_name" | head -n 1)
if [ -n "$match_path" ]; then
# 目标链接路径
link_path="$match_path/compile_commands.json"
# 如果已有链接或文件,删除
if [ -L "$link_path" ] || [ -f "$link_path" ]; then
rm -f "$link_path"
fi
# 创建软链接
ln -s "$json_path" "$link_path"
echo "Linked: $json_path → $link_path"
else
echo "Could not find $package_name in src/, skipping."
fi
done
FAQ
使用 catkin_tools 自动生成 compile_commands.json 时的问题。
ROS2
ament_cmake 是对原生 cmake 在 ROS2 构建中做的拓展 参考链接
库的安装
以 rclcpp 的 install 为例
install(
TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
# Export old-style CMake variables
ament_export_include_directories("include/${PROJECT_NAME}")
ament_export_libraries(${PROJECT_NAME})
# Export modern CMake targets
ament_export_targets(${PROJECT_NAME})
ament_export_dependencies(
builtin_interfaces
libstatistics_collector
rcl
rcl_interfaces
rcl_yaml_param_parser
rcpputils
rcutils
rmw
rosgraph_msgs
rosidl_dynamic_typesupport
rosidl_runtime_c
rosidl_runtime_cpp
rosidl_typesupport_cpp
statistics_msgs
tracetools
)
msg, srv
msg 和 srv 是两种分别描述了 ROS2 topic 和 service 信息格式的描述文件,ros2 提供了根据描述文件生成对应 C++ 代码的功能,这使得我们能够便利地在 ROS2 中自定义消息格式。
在过去,由于在编译时遇到了“生成的 srv 不能够同时被导入”的问题,我选择沿袭祖传代码的做法,专门新建一个功能包仅用于提供 msg/srv。每次新添加 msg/srv 后,都需要先单独使用 colcon build --packages-select bupt_interfaces 先构建它,再使用 colcon build 编译其他包。
现在我忍无可忍
不妨参照 ROS2 官方的实例进行模仿,我选取的参照对象是 turtlesim。
只需要在该 package 中依赖该 msg/srv 的 target 后加上下面这句,就能正确配置。
rosidl_target_interfaces(turtlesim_node ${PROJECT_NAME} "rosidl_typesupport_cpp")
这指示我们,为了更熟悉 ROS2 的构建,需要挖掘 ament_cmake 在背后做的事,也正好帮助学习 CMake。
ROS 项目中包含非 ROS 包依赖
cmake_minimum_required(VERSION 3.15)
project(mi_motor)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# find dependencies_
find_package(ament_cmake REQUIRED)
# uncomment the following section in order to fill in further dependencies_
# manually. find_package(<dependency> REQUIRED)_
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(bupt_can REQUIRED)
add_executable(motor_node src/motor_node.cpp)
target_include_directories(motor_node PRIVATE include)
target_link_libraries(motor_node PUBLIC bupt_can)
ament_target_dependencies(motor_node PUBLIC rclcpp std_msgs)
ament_package()
分别用原生的 target_link_libraries 和 ament_target_dependencies 即可,但是要保证它们有一致的关键字签名,因为 ament_target_dependencies() 不打算支持 PRIAVTE,因此我们使用 PUBLIC。
PkgConfig
这里以导入 glib 库为例
Pkgconfig 实际上也是将写有库配置信息的 pc 文件安装到相应目录,才能在查找时进行导入。
cmake_minimum_required(VERSION 3.22.1)
project(DNS-Relay VERSION 0.1.0 LANGUAGES C)
find_package(PkgConfig REQUIRED)
pkg_check_modules(GLIB REQUIRED glib-2.0)
add_executable(DNS-Relay src/main.c)
target_include_directories(DNS-Relay PRIVATE ${GLIB_INCLUDE_DIRS})
target_link_libraries(DNS-Relay ${GLIB_LIBRARIES})
xmake
配置智能提示
官方教程 仅以 vscode + clangd 为例
首先安装必要的插件


创建控制台项目
xmake create -l c -t console ./test
cd ./test
code .
配置 clangd 对 compile_commands.json 的搜索目录
这一步的必要性在于,clangd 插件对编译数据库的默认搜索目录为 build 和 workspace 根目录,因此在使用 CMake 时可以无感提供智能提示。然而 xmake 插件会在 xmake.lua 配置文件保存时自动生成 compile_commands.json 到 .vscode 目录下,这导致了配置的差异。
# 创建 .clangd 配置文件
touch .clangd
# 在 .clangd 加入以下内容
CompileFlags:
CompilationDatabase: .vscode/
https://github.com/clangd/coc-clangd/issues/182#issuecomment-771543697
build, run, debug 支持
xmake 插件为构建、运行、调试提供了全面的 GUI 按键支持

使用示例
vcpkg
头文件支持
由于头文件并不直接参与目标构建,似乎没有办法为头文件添加依赖使其得到 clangd 智能提示。幸运的是,clangd 的解析具有传染性,尽管没有”正规“的方法为其添加依赖,但是可以通过”跳转“的方式使头文件被缓存。具体来说,可以先在某一个源代码文件中 include 该头文件,然后 ctrl+click 跳转到该头文件中,clangd 就会自动支持该头文件的智能提示。
Compilation(编译)
Concept
Translation Unit(翻译单元)
翻译单元和链接 (C++) https://en.wikipedia.org/wiki/Translation_unit_(programming)
外部链接和内部链接描述的是对象(不是 OOP 中的对象)在翻译单元间的可见性。
Linking
链接的详细规范请参照 System V ABI
静态库与共享库的一大区别是静态库仅仅将目标文件进行打包,加上一些索引,并未进行实际的链接过程。
Skills
C/C++ 混合编译
在 C++ 中,为了支持函数重载(Function Overloading),编译器会对函数名进行修饰(Name Mangling),以便在链接阶段区分同名但参数不同的函数。
extern "C" 的作用是告诉 C++ 编译器,将指定的代码按照 C 语言的规则进行编译,避免名称修饰,从而实现与 C 语言代码的兼容。
需要在 C 语言的声明(函数)处进行修饰
#ifdef __cplusplus
extern "C" {
#endif
void crcInit(void);
crc crcSlow(unsigned char const message[], int nBytes);
crc crcFast(unsigned char const message[], int nBytes);
#ifdef __cplusplus
}
#endif