编译、调试和版本管理
g++ 的常用参数、用 CMake 管理多文件项目、用 gdb 和 AddressSanitizer 找 bug、用 git 留下干净的提交记录。
g++:几个必须认识的参数
g++ -std=c++17 -O2 -g -Wall -Wextra -o server main.cpp net.cpp -lpthread
| 参数 | 作用 |
|---|---|
-std=c++17 |
使用 C++17 标准 |
-O0 / -O2 |
优化级别。调试时用 -O0,压测时用 -O2 |
-g |
生成调试信息,gdb 需要它才能显示源码行号 |
-Wall -Wextra |
打开大部分警告。警告往往就是 bug,别忽略 |
-lpthread |
链接线程库(阶段 1 起需要) |
-fsanitize=address |
打开 AddressSanitizer,下面会讲 |
CMake:文件一多就离不开它
项目超过三个文件,手写 g++ 命令就会很痛苦。CMake 根据 CMakeLists.txt 生成构建脚本:
cmake_minimum_required(VERSION 3.16)
project(mini_redis CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(net src/net/EventLoop.cpp src/net/Channel.cpp src/net/EpollPoller.cpp)
target_include_directories(net PUBLIC src)
add_executable(echo_server examples/echo_server.cpp)
target_link_libraries(echo_server PRIVATE net pthread)
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug # 生成构建目录
cmake --build build -j # 编译
./build/echo_server
把构建产物放在单独的 build/ 目录里,并把 build/ 写进 .gitignore。
gdb:程序崩了先看调用栈
gdb ./build/echo_server
(gdb) break EventLoop::loop # 在函数入口打断点
(gdb) run
(gdb) next # 执行下一行(不进入函数)
(gdb) step # 进入函数
(gdb) print fd # 看变量
(gdb) bt # 崩溃后看调用栈,最有用的命令
(gdb) info threads # 多线程时看所有线程
段错误(Segmentation fault)时,先运行 bt,看崩在哪一行、是谁调用的。九成问题到这一步就能找到方向。
AddressSanitizer:内存错误的照妖镜
加上 -fsanitize=address -g 重新编译后运行程序,越界访问、释放后使用(use-after-free)、内存泄漏都会被当场抓住,并打印出出错行和内存分配的位置。网络库里最常见的是 use-after-free:连接对象已经析构了,回调还在用它。
g++ -std=c++17 -g -fsanitize=address -o server main.cpp
./server
# ==12345==ERROR: AddressSanitizer: heap-use-after-free on address ...
git:提交记录也是简历的一部分
git init
git add .
git commit -m "Add epoll echo server"
git remote add origin git@github.com:你的用户名/mini-redis.git
git push -u origin main
面试官打开你的仓库时会看提交记录。几十个提交、每个都有清楚的说明(“Add Buffer with readv”),比一个 “update” 塞进所有代码可信得多。养成每完成一小步就提交一次的习惯。
自测
为什么调试时用 -O0,压测时用 -O2?
-O2 会重排、内联甚至删除代码,gdb 里变量会显示 “optimized out”,单步执行也会乱跳。压测则必须开优化,否则测出来的性能没有参考价值。
程序段错误了,第一步做什么?
用带 -g 的版本在 gdb 里运行,崩溃后执行 bt 看调用栈。如果怀疑是内存问题,再用 -fsanitize=address 编译运行一次。
面试常问
从源码到可执行文件经历了哪几步?
参考回答: 四步。预处理:展开 #include 和宏,去掉注释(g++ -E);编译:把 C++ 翻译成汇编(-S);汇编:把汇编变成目标文件 .o(-c);链接:把多个目标文件和库合在一起,解析符号引用,生成可执行文件。常见的 undefined reference 就是链接阶段找不到符号的定义。
静态库和动态库有什么区别?
参考回答: 静态库(.a)在链接时整个拷进可执行文件,运行时不依赖外部文件,但体积大,库更新了要重新编译。动态库(.so)在运行时才加载,多个进程可以共享同一份,体积小、升级方便,但部署时要保证目标机器上有对应版本,否则报找不到 .so。
程序段错误了,你怎么定位?
参考回答: 先 ulimit -c unlimited 打开 core dump,用 -g 重新编译,复现后用 gdb ./server core 打开,执行 bt 看崩在哪一行、调用链是什么,再用 p 看相关变量。如果是越界、use-after-free 这类问题,崩溃点往往不是出错点,这时用 -fsanitize=address 编译再跑一次,ASan 会直接报出第一次非法访问的位置和那块内存在哪里分配、在哪里释放。
可能的追问: 段错误常见原因?空指针解引用、数组越界、访问已经释放的内存、栈溢出(递归太深)。
怎么检查内存泄漏?
参考回答: 开发时用 ASan 自带的 LeakSanitizer,程序退出时会列出没释放的内存和分配位置;也可以用 valgrind --leak-check=full,不用重新编译但会慢几十倍。长期运行的服务可以观察 RSS 是否一直涨。从根上说,用 RAII 和智能指针管理资源,泄漏就很少出现。
git merge 和 git rebase 有什么区别?
参考回答: 两者都是把一个分支的改动合到另一个分支。merge 会生成一个合并提交,保留完整的分叉历史;rebase 把当前分支的提交“挪”到目标分支最新提交的后面重新应用一遍,历史是一条直线,更干净。rebase 会改写提交记录,所以不要对已经推送、别人也在用的分支做 rebase。
原文与延伸
- CMake 官方教程
- AddressSanitizer 说明
- Pro Git 中文版(CC BY-NC-SA 3.0,免费在线阅读)