前言
前面我们完成了高并发 WebServer 网络项目,代码全部堆在少量头文件与源文件中。 真实企业 C++ 后端项目不会把全部代码写在少数几个文件,需要:模块化拆分、库封装、构建管理、单元测试、调试定位 bug、性能分析。
聚焦C++ 工程化能力,这一块很多刷题同学薄弱,面试经常被提问 CMake、库链接、编译报错、gdb 调试。 本篇内容:CMake 语法实战、静态库动态库区别、大型项目目录组织、单元测试 gtest、gdb 常用调试、perf 性能分析、常见编译链接踩坑。
前置知识:Linux 基础命令、C++ 编译链接基础。
1.C++ 编译链接基础回顾
C++ 完整流程:预处理 → 编译 → 汇编 → 链接
- 预处理:
#include头文件展开、宏替换、条件编译 - 编译:
.cpp生成汇编代码 - 汇编:汇编生成
.o目标文件 - 链接:把多个
.o、库文件合并,生成可执行程序
两大库类型:
- 静态库
.a(Windows.lib):编译链接阶段,把库代码拷贝进可执行文件。 - 动态库
.so(Windows.dll):运行阶段加载,程序不拷贝库代码。
| 特性 | 静态库 .a | 动态库 .so |
|---|---|---|
| 链接时机 | 编译期链接 | 程序运行时加载 |
| 程序体积 | 可执行文件变大 | 程序体积小 |
| 依赖 | 运行不需要原库文件 | 运行环境必须存在 so 文件 |
| 更新 | 库更新需要重新编译项目 | 替换 so 即可,不用重编程序 |
| 符号冲突 | 容易出现重复定义 | 相对不易 |
开发库选型:基础底层组件常用静态库;插件、版本频繁更新模块适合动态库。
2. 大型 C++ 项目标准目录结构
以我们 WebServer 项目改造为大型工程目录,工业界通用结构:
WebServerPro/
├── CMakeLists.txt # 根cmake
├── build/ # 编译输出目录,git忽略
├── src/ # 业务源码
│ ├── net/ # 网络模块 reactor epoll tcp
│ │ ├── reactor.cpp
│ │ ├── reactor.h
│ │ ├── tcp_connection.cpp
│ │ └── CMakeLists.txt
│ ├── thread/ # 线程池模块
│ │ ├── threadpool.cpp
│ │ ├── threadpool.h
│ │ └── CMakeLists.txt
│ ├── timer/ # 时间轮定时器
│ │ ├── timewheel.cpp
│ │ ├── timewheel.h
│ │ └── CMakeLists.txt
│ ├── utils/ # 工具、对象池、基础工具
│ │ ├── objectpool.cpp
│ │ └── CMakeLists.txt
│ ├── http/ # http解析模块
│ │ ├── http_parser.cpp
│ │ └── CMakeLists.txt
│ └── main.cpp # 程序入口
├── thirdparty/ # 第三方库 gtest等
├── tests/ # 单元测试代码
│ ├── test_threadpool.cpp
│ ├── test_timewheel.cpp
│ └── CMakeLists.txt
├── assets/ # 静态资源 html配置文件
└── doc/ # 文档
核心思想:按模块拆分子 CMakeLists.txt,根 cmake 统一管理,模块编译成静态库,main 链接各个库。
3.CMake 语法实战,模块化拆分 WebServer
3.1 根目录 CMakeLists.txt
cpp
cmake_minimum_required(VERSION 3.16)
project(WebServerPro LANGUAGES C CXX)
# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 编译选项:警告、调试符号
add_compile_options(-Wall -Wextra -g)
# 设置输出目录
set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
# 子模块,递归进入子目录执行CMakeLists
add_subdirectory(src/net)
add_subdirectory(src/thread)
add_subdirectory(src/timer)
add_subdirectory(src/utils)
add_subdirectory(src/http)
# 单元测试模块
enable_testing()
add_subdirectory(tests)
# 生成主程序
add_executable(web_server src/main.cpp)
# 链接各个模块静态库 + pthread系统库
target_link_libraries(web_server
net_lib
thread_lib
timer_lib
utils_lib
http_lib
pthread
)
3.2 src/net/CMakeLists.txt 示例(编译静态库)
cpp
# 将本目录源码编译为静态库 net_lib
add_library(net_lib STATIC
reactor.cpp
tcp_connection.cpp
)
# 当前头文件对外暴露,其他模块可以#include
target_include_directories(net_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
STATIC生成静态库;SHARED生成动态库。PUBLIC:库本身和链接该库的程序都可以看到头文件;PRIVATE仅库内部可见。
3.3 src/thread/CMakeLists.txt
cpp
add_library(thread_lib STATIC
threadpool.cpp
)
target_include_directories(thread_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
其他 timer、utils、http 模块写法完全同理。
3.4 编译构建命令
cpp
# 生成构建文件
mkdir build && cd build
cmake ..
# 编译全部模块、库、可执行程序
make -j$(nproc)
# 运行程序
./bin/web_server
3.5 动态库简单示例
把静态库改为动态库,只需要修改关键字:
cpp
add_library(net_lib SHARED reactor.cpp tcp_connection.cpp)
运行动态库程序需要指定 so 搜索路径:
cpp
export LD_LIBRARY_PATH=./lib:$LD_LIBRARY_PATH
./bin/web_server
4. 单元测试集成 GoogleTest (gtest)
企业 C++ 项目必备:每个模块写单元测试,修改代码后快速验证逻辑正确性,防止改代码引入 bug。
4.1 tests/CMakeLists.txt
cpp
# 引入gtest,thirdparty放置gtest源码
add_subdirectory(../thirdparty/googletest build_gtest)
# 线程池单元测试可执行文件
add_executable(test_threadpool test_threadpool.cpp)
target_link_libraries(test_threadpool thread_lib gtest gtest_main pthread)
# 时间轮单元测试
add_executable(test_timewheel test_timewheel.cpp)
target_link_libraries(test_timewheel timer_lib gtest gtest_main pthread)
# 注册测试用例,cmake ctest命令执行
add_test(NAME ThreadPoolTest COMMAND test_threadpool)
add_test(NAME TimeWheelTest COMMAND test_timewheel)
4.2 简单单元测试示例 test_threadpool.cpp
cpp
#include <gtest/gtest.h>
#include "threadpool.h"
#include <atomic>
TEST(ThreadPoolTest, SubmitTask)
{
ThreadPool pool(4);
std::atomic<int> cnt{0};
for(int i=0;i<100;i++)
{
pool.submit([&cnt](){
cnt++;
});
}
//等待任务全部执行
std::this_thread::sleep_for(std::chrono::seconds(1));
ASSERT_EQ(cnt,100);
}
int main(int argc,char** argv)
{
testing::InitGoogleTest(&argc,argv);
return RUN_ALL_TESTS();
}
4.3 执行单元测试
cpp
cd build
make
#方式1:直接运行测试程序
./bin/test_threadpool
#方式2:ctest批量跑全部用例
ctest
5.gdb 调试 C++ 程序(面试高频)
gdb 是 Linux 下 C++ 核心调试工具,线上 core 崩溃排查必备。
5.1 开启调试符号
CMake 编译选项添加 -g,不要开启-O3优化调试(优化会变量被销毁,阅读困难)。
cpp
add_compile_options(-g)
5.2 生成 core 转储文件,程序崩溃自动保存现场
cpp
#临时设置core大小不限制
ulimit -c unlimited
#设置core文件输出路径
echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern
程序崩溃后,会在 /tmp 生成 core 文件,使用 gdb 分析:
cpp
gdb ./bin/web_server /tmp/core.web_server.12345
(gdb) bt #打印调用栈,直接定位崩溃行
(gdb) f 0 #切换栈帧
(gdb) p var #打印变量
(gdb) l #查看源码
5.3gdb 常用基础命令汇总
| 命令 | 作用 |
|---|---|
gdb ./app |
调试可执行程序 |
run [args] |
启动程序 |
b main.cpp:20 |
在指定文件行打断点 |
b ClassName::func |
函数断点 |
info b |
查看全部断点 |
delete 1 |
删除 1 号断点 |
c |
continue 继续运行 |
n |
next 单步,不进入函数 |
s |
step 单步,进入函数内部 |
finish |
执行完当前函数返回 |
p x |
打印变量 x |
ptype x |
查看变量类型 |
bt |
backtrace 打印调用栈 |
print *obj |
打印对象内容 |
info threads |
查看全部线程 |
thread 2 |
切换到 2 号线程 |
多线程调试:
set scheduler‑locking on,调试一个线程时其他线程暂停,防止乱跳。
6.Linux 性能工具简单上手
6.1 perf 性能采样,定位 CPU 热点函数
cpp
#记录程序运行采样
perf record -g ./bin/web_server
#分析报告,看哪个函数占用CPU最高
perf report
可以快速定位:哪一个函数消耗大量 CPU,排查高 CPU 占用。
6.2 valgrind 内存泄漏检测
检测内存泄漏、野指针、越界访问。
cpp
valgrind --leak-check=full ./bin/web_server
valgrind 速度很慢,不适合压测场景,适合小流量单元测试。
7. 编译链接高频踩坑(面试常问)
坑 1:头文件找到了,但是报 undefined reference
原因:头文件只做声明,没有链接对应的.o/ 库文件。 解决:target_link_libraries 把对应静态库加上。
坑 2:多重头文件包含,重复定义
解决方案:头文件保护
cpp
#pragma once
//或者老式
#ifndef HEADER_H
#define HEADER_H
//内容
#endif
坑 3:静态库链接顺序问题
静态库链接顺序:被依赖库放后面 。A 依赖 B,链接顺序A B。 动态库无顺序问题。
坑 4:动态库运行提示 cannot open shared object file
编译成功,运行找不到 so。 解决:设置LD_LIBRARY_PATH,或者把 so 放到系统库目录/usr/lib。
坑 5:多重继承、虚函数导致 vtable 未定义
虚函数没有实现;类虚函数至少一个虚函数必须有定义。
坑 6:Debug 和 Release 混编
Debug 库和 Release 程序混用,ABI 不匹配,出现各种诡异崩溃。工程中统一编译模式。
8.Debug 与 Release 编译模式
CMake 两种构建模式:
- Debug:
-g调试符号,无优化,适合开发调试。 - Release:
-O3高性能优化,去除调试符号,上线发布。
cpp
#Debug构建
cmake -DCMAKE_BUILD_TYPE=Debug ..
#Release构建
cmake -DCMAKE_BUILD_TYPE=Release ..
注意:Release 模式下很多局部变量会被编译器优化,gdb 无法打印变量。
9. 面试高频工程化真题
Q1:静态库和动态库区别?分别什么场景用?
静态库编译期拷贝代码进程序,运行不需要库文件,程序体积大;动态库运行加载,程序体积小,替换库不需要重编译。工具组件、小工具多用静态库;插件、版本频繁更新模块使用动态库。
Q2:undefined reference 一般是什么原因?
- 忘记链接对应库;2. 静态库链接顺序错误;3. 函数声明存在,但是没有实现;4.c/c++ 混编 extern "C" 符号问题。
Q3:core dump 是什么?怎么使用 core 排查崩溃?
程序发生段错误、内存越界崩溃,操作系统保存进程内存镜像为 core 文件。gdb 加载可执行程序 + core 文件,bt 打印调用栈直接定位崩溃代码行。
Q4:CMake 中 PUBLIC PRIVATE INTERFACE 区别?
- PRIVATE:本库内部使用,链接本库的外部程序看不到头文件。
- PUBLIC:库内部使用,外部链接方也可以看见头文件。
- INTERFACE:外部链接方可见,库本身不使用。
Q5:为什么要写单元测试?
模块迭代修改代码,回归验证功能正确性,提前发现 bug;大型项目不可能全部靠手动复现测试。
Q6:valgrind 与 perf 的用途区别?
valgrind 检测内存泄漏、内存越界;perf 做性能采样,定位 CPU 热点函数。
小结
完成 C++ 工程体系学习:
- 掌握静态库动态库原理、优缺点、适用场景;
- 大型项目目录组织,CMake 模块化拆分项目,子 CMakeLists 编写;
- gtest 单元测试集成,保证模块迭代稳定性;
- gdb 调试、core 文件分析崩溃,perf 性能分析;
- 梳理编译链接高频报错与解决方案,面试工程题全部覆盖。