cmake项目#
gcc g++#
GCC 是编译器。 gcc是c语言,g++是其中的c++.
编译流程
1 2 3 4 |
|
常用选项
- -Wall 开启常见警告
- -g 生成调试信息
- -O2 或 -O3 优化
- -I \<dir> 添加头文件搜索路径
- -L \<dir> 和 -l \<name> 添加库搜索路径与链接库
单个文件#
g++ -c main.cpp -o main.o
g++ main.cpp
这会直接编译汇编链接成可执行文件,不利于理解
常用命令
- 只编译生成目标文件(不链接):
g++ -c main.cpp -o main.o
这会生成 main.o(可重定位目标文件),适合把多个源文件分别编译再链接。
- 链接生成可执行文件(把 .o 链接成可执行文件):
g++ main.o -o main
多个文件无库#
多个文件,
可以不用头文件,
g++ -c mian.cc b.cc
// main.cpp
#include <stdio.h>
extern const char *msg;
int main() {
printf("%s\n", msg);
return 0;
}
//b.cc
const char *msg = "您好";
注意
- .o 文件是目标文件,不可直接运行,需要链接生成可执行文件。
- 当有多个源文件时,推荐分别 -c 生成 .o,再统一链接,方便增量构建与并行编译。
- 对 C++ 项目用 g++ 来链接,避免缺少标准库链接的问题。
按需编译与未定义符号示例
main.cpp 中引用了一个外部变量 extern const char *msg;
。当编译器编译 main.cpp 时,它并不知道 msg
在哪儿定义,所以在生成的目标文件 main.o 中会留下对 msg
的未定义符号引用。之后编译 b.cpp 时,如果在 b.cpp 中定义了 const char *msg = ...;
,编译器会把该定义编译到 b.o。最终链接器把 main.o 与 b.o 链接在一起,从而解析并完成对 msg
的解析。
这个流程说明按需编译的要点:先把每个源文件分别编译成目标文件(.o),再由链接器把它们合并解析未定义符号。这样当某个源文件修改时,只需重新编译被修改的源文件生成新的 .o,然后重新链接,避免不必要的全量编译,提升构建效率。
“无头文件”的写法是一种为了教学、为了展示extern关键字和链接器底层工作原理的极限简化示例。在任何真实的、稍具规模的项目中,这都是绝对不推荐的做法。
我们来对比一下:
无头文件的做法 (不推荐)
工作原理: 在 main.cpp 中使用 extern const char *msg; 告诉编译器:“别担心,msg 这个变量确实存在,只不过在别的文件里。你先编译,链接器到时候会找到它的。”
为什么这是坏习惯:
没有“契约”: main.cpp 的作者只能靠口头约定或者记忆来知道 b.cpp 里有 msg,并且知道它的类型是 const char *。如果 b.cpp 的作者把 msg 的类型改成了 std::string,main.cpp 在编译时完全不会报错,直到链接阶段才会因为找不到匹配的符号而失败,这种错误非常难以排查。
可读性极差: 任何人想使用 b.cpp 里的功能,都必须去阅读 b.cpp 的源代码,而不是查阅一个清晰的 .h 头文件。
无法提供函数声明: 如果 b.cpp 提供的是一个函数 int get_value(),main.cpp 同样需要 extern int get_value(); 这样的“外部声明”。随着函数增多,在每个使用它的 .cpp 文件里都手写一遍声明,是一场维护灾难。
有头文件的做法
#include <iostream>
#include "b.h" // 包含接口说明书,编译器就知道 msg 是什么了
int main() {
std::cout << msg << std::endl;
return 0;
}
#pragma once // 防止头文件被重复包含
// 在头文件中“声明”接口,告诉所有人:
// “我这里对外提供一个名为 msg 的变量”
extern const char* msg;
#include "b.h" // 包含自己的接口声明,确保实现与声明一致
// 在源文件中“定义”实现
const char* msg = "Hello from b.cpp with header!";
include 相当于在预处理把 b.h 的内容直接插入到 main.cpp 和 b.cpp 里。这样 main.cpp 和 b.cpp 都能看到对 msg 的声明,编译器就知道 msg 是什么了。
动态库 静态库#
无头文件的不规范写法#
打包成动态库
g++ -fPIC -c b.cpp -o b.o # 生成位置无关代码
g++ -shared -o libb.so b.o # 打包成动态库
g++ main.cpp -L. -lb -o main # 使用动态库
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH && ./main
打包成静态库
g++ -c b.cpp -o b.o # 编译目标文件
ar rcs libb.a b.o # 打包成静态库
g++ main.cpp -L. -lb -o main # 使用静态库
有头文件的规范写法#
要点
- 头文件 (*.h / *.hpp) 只放公有接口:函数声明、外部变量声明、类型定义、宏、inline/模板实现等;实现放在 .cpp/.cc。
- 使用 include guard 或 #pragma once
防止重复包含。
- 模板、inline 函数、类定义需在头文件中可见,不能仅靠二进制库提供。
- 若需以 C 风格导出以避免 C++ name mangling,可在头文件使用 extern "C"
(仅在需要与 C 互操作时)。
- 在 Windows 上常用宏控制导出/导入(如 __declspec(dllexport/__declspec(dllimport))。
最小示例
// b.h
#pragma once
extern const char *msg;
// b.cc
#include "b.h"
const char *msg = "您好";
// main.cpp
#include <stdio.h>
#include "b.h"
int main() {
printf("%s\n", msg);
return 0;
}
构建与链接(静态库)
g++ -c b.cc -o b.o
ar rcs libb.a b.o
g++ main.cpp libb.a -o main
./main
构建与链接(动态库)
g++ -fPIC -c b.cc -o b.o
g++ -shared -o libb.so b.o
g++ main.cpp -L. -lb -o main
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./main
简短注意事项
- 头文件保证声明与定义一致,减少调用方错误;不使用头文件易出现类型/签名不匹配难以排查的问题。
- 模板与 inline 必须放头文件;若需要跨语言或控制可见性,需在头文件中添加相应的修饰(extern "C"、导出宏等)。
- 保持头文件最小且稳定,避免在头文件中包含大量实现细节以减少不必要的重编译。
简短注意事项
- 头文件保证声明与定义一致,减少调用方错误;不使用头文件易出现类型/签名不匹配难以排查的问题。
- 模板与 inline 必须放头文件;若需要跨语言或控制可见性,需在头文件中添加相应的修饰(extern "C"、导出宏等)。
make#
一个较好的文档 https://seisman.github.io/how-to-write-makefile/index.html
make原理,主要通过文件的时间戳来判断哪些文件需要重新编译。每个目标文件都有一个依赖列表,make 会检查这些依赖文件的修改时间,如果发现某个依赖文件比目标文件新,就会重新编译该目标文件。
make命令用于检查更新,执行命令。(当make命令寻找makefile时,它按照以下顺序尝试查找文件名: GNUmakefile 、 makefile 和 Makefile。)
GNU Make是一个自动化构建工具,主要用于管理项目中的文件依赖关系和自动化编译过程。它通过读取Makefile文件中的规则和指令,来决定哪些文件需要重新编译,并执行相应的命令。在linux中认为make=gnu make。
make install 把可执行文件(target目标)复制到系统目录(如 /usr/local/bin),以便全局使用。
# Makefile
CXX := g++ # CXX是makefile的内置变量,表示C++编译器
TARGET := hello
# 内置变量有很多,常用的有:
# CXX : C++ 编译器
# CC : C 编译器
# CFLAGS : C 编译器选项
# CXXFLAGS : C++ 编译器选项
# LDFLAGS : 链接器选项
# LDLIBS : 链接库选项
# TARGET : 目标文件名
# SRCS : 源文件 一般写很多个,就不用再编译链接都手动加了
# OBJS : 目标文件
$(TARGET): main.o b.o
$(CXX) -o $(TARGET) main.o b.o
main.o: main.cpp # main.o 目标文件, 检查main.cpp更新时间,若更新则执行下面的命令
$(CXX) -c main.cpp
b.o: b.cpp
$(CXX) -c b.cpp
clean:
rm -f *.o $(TARGET)
.PHONY: clean #声明伪目标,并不生成一个叫clean的文件
上诉makefile压根没写.h文件,makefile如何找到源文件所对应的头文件的:
实际上make 本身完全不关心 C++ 语法,它不认识 #include。make 的任务是执行写下的命令。所以,“找头文件”这个动作是由 make 调用的 g++ 命令来完成的。
- 通过依赖关系:在 makefile 中明确指定源文件对头文件的依赖,例如
main.o: main.cpp b.h
。这样当 b.h 修改时,make 会重新编译 main.o。(简单但不推荐) - 使用自动依赖生成工具:如 gcc 的
-MMD -MP
选项,可以在编译时生成依赖文件(.d),然后在 makefile 中包含这些依赖文件,自动处理头文件的变化。
在上个 Makefile 中,因为 b.h 和 main.cpp 都在同一个目录下,所以 g++ 默认就能找到它,一切看似正常。
g++ 查找头文件遵循以下顺序:
对于 #include "b.h":首先在源文件所在的当前目录查找。
对于 #include
在 -I 选项指定的路径中查找:我们可以通过 g++ 的 -I 选项来手动添加头文件的搜索目录。
如何改进 Makefile 来明确指定路径?
一个好的 Makefile 应该使用 CXXFLAGS 变量来统一管理编译选项,包括头文件路径。
# Makefile - 版本 2 (明确指定头文件路径)
CXX := g++
# 使用 CXXFLAGS 统一管理编译选项
# -I. 表示将当前目录“.”也加入头文件搜索路径
CXXFLAGS := -Wall -I.
TARGET := hello
$(TARGET): main.o b.o
$(CXX) -o $(TARGET) main.o b.o
main.o: main.cpp
$(CXX) $(CXXFLAGS) -c main.cpp # 使用 $(CXXFLAGS)
b.o: b.cpp
$(CXX) $(CXXFLAGS) -c b.cpp # 使用 $(CXXFLAGS)
clean:
rm -f *.o $(TARGET)
.PHONY: clean
# Makefile - 最终版 (自动化依赖管理)
CXX := g++
CXXFLAGS := -Wall -I. -g # 添加了-g调试选项
TARGET := hello
SOURCES := main.cpp b.cpp
OBJECTS := $(SOURCES:.cpp=.o) # 通过源文件列表自动生成目标文件列表
DEPS := $(SOURCES:.cpp=.d) # 通过源文件列表自动生成依赖文件列表
# 默认目标
all: $(TARGET)
$(TARGET): $(OBJECTS)
$(CXX) $(OBJECTS) -o $(TARGET)
# 通用编译规则
# %.o: %.cpp 表示任何 .o 文件都依赖于同名的 .cpp 文件
%.o: %.cpp
# -MMD -MP 会在编译时自动生成 .d 依赖文件
$(CXX) $(CXXFLAGS) -c $< -o $@ -MMD -MP
clean:
rm -f $(OBJECTS) $(TARGET) $(DEPS)
# 包含所有自动生成的 .d 依赖文件
# -include 会在文件不存在时不报错,这在第一次编译时是必需的
-include $(DEPS)
.PHONY: clean all
CMAKE#
用于生成makefile。
简单的 add_executable(hello main.cpp b.cpp) 写法在项目变大后难以维护。现代CMake的核心思想是**“万物皆目标 (Target)”**,我们将属性(如头文件路径、链接库、编译选项)精确地附加到具体的目标上。
一个较为优秀的例子,由上交的ipads提供。 提供了在项目中cmake的各种功能。
【IPADS新人培训第二讲:CMake】 https://www.bilibili.com/video/BV14h41187FZ/?share_source=copy_web&vd_source=727ffe1727e24c229169af42b43aa2e0
https://github.com/richardchien/modern-cmake-by-example
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(HelloProject)
add_executable(hello main.cpp b.cpp)
构建流程:
mkdir build && cd build
cmake ..
make
./hello
cmake 怎么怎么查找第三方包?
cmake 查找包主要通过 find_package
命令实现。它会在系统预定义的路径以及用户指定的路径中查找包的配置文件(通常是 <PackageName>Config.cmake
或 Find<PackageName>.cmake
)。这些配置文件定义了包的包含目录、库文件等信息,供 cmake 使用。
cmake 怎么加入自己写的动态库或者静态库?
可以使用 add_library
命令来添加自己写的动态库或静态库,然后使用 target_link_libraries
将其链接到可执行文件。
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(HelloProject)
# 添加静态库
add_library(mylib STATIC mylib.cpp)
# 添加可执行文件
add_executable(hello main.cpp)
# 链接库到可执行文件
target_link_libraries(hello mylib)
一个能测试的最小demo#
源文件,库文件,main.cpp, lib.cpp, lib.h
#include <iostream>
#include "lib.h"
int main() {
std::cout << "Hello, World!" << std::endl;
mylib_function();
return 0;
}
#include <iostream>
void mylib_function() {
std::cout << "Hello from mylib!" << std::endl;
}
# 1. 设置CMake最低版本和项目信息
cmake_minimum_required(VERSION 3.15)
project(RealWorldProject LANGUAGES CXX VERSION 1.0)
# 2. 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 3. [核心] 将模块化代码构建成一个库(Target)
add_library(mylib STATIC src/b.cpp)
# 4. [核心] 为库(Target)附加属性
# target_include_directories: 为目标指定头文件搜索路径
# PUBLIC: 编译 mylib 自己需要此路径,任何链接 mylib 的目标也自动获得此路径。
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 5. 添加可执行文件(Target)
add_executable(hello src/main.cpp)
# 6. [核心] 将可执行文件(Target)链接到库(Target)
# PRIVATE: 只有 hello 这个目标需要链接 mylib,这个链接关系不会传递给其他目标。
target_link_libraries(hello PRIVATE mylib)
# 7. [进阶] 查找与链接外部库 (以ROOT为例)
# 在粒子物理领域,这非常常见
find_package(ROOT COMPONENTS RIO Hist REQUIRED)
if(ROOT_FOUND)
# 如果找到了ROOT,将其库链接到我们的可执行文件
target_link_libraries(hello PRIVATE ROOT::RIO ROOT::Hist)
endif()
# 8. [进阶] 精细控制编译选项
# 为所有目标添加基础警告选项
add_compile_options(-Wall -Wextra -Wpedantic)
# 使用“生成器表达式”为不同构建类型设置不同选项
# $<CONFIG:Debug> 表示仅在Debug模式下生效
target_compile_options(hello PRIVATE
$<$<CONFIG:Debug>:-g>
$<$<CONFIG:Release>:-O3>
)
target_compile_definitions(hello PRIVATE
$<$<CONFIG:Debug>:MY_DEBUG_MACRO>
)
# 9. [进阶] 添加测试 (使用CTest)
enable_testing() # 开启测试功能
# 添加一个名为 "BasicTest" 的测试,它执行命令 ./hello
add_test(NAME BasicTest COMMAND hello)
# 运行测试: cd build && ctest
debug 和 测试#
创建日期: 2025-10-08