C++作死代码黑榜:避坑实战手册_c++代码讲解
面向一线开发者的“反面教材”与修复策略合集。每个坑都配有作死示例与正确做法,并标注应用场景与模块归类,便于团队培训与代码评审使用。
内存与资源管理
new/delete 与 RAII 缺失
应用场景: 业务模块快速原型、旧项目迁移。 作死示例:
// 模块:订单处理/缓存
void foo() {
int* p = new int[10];
// ... 中途return或异常时泄漏
if (some_condition()) return;
delete p; // 错误:应为 delete[]
}
问题解析:
- 提前返回未释放;delete 与 new[] 不匹配导致未定义行为(UB)。
正确做法:
#include <vector>
void foo_ok() {
std::vector<int> v(10); // RAII,异常安全,无需手动释放
}
要点: 优先使用标准容器/智能指针;手写 new/delete 仅在确有必要时。
悬垂引用与返回局部变量地址
应用场景: 工具库返回引用/指针提高性能。 作死示例:
// 模块:配置管理
const std::string& getConf() {
std::string s = "tmp";
return s; // 返回局部变量引用 -> 悬垂
}
正确做法:
// 1) 返回值(NRVO/移动)
std::string getConf_ok() {
std::string s = "tmp";
return s; // 高效且安全
}
// 2) 若需缓存,延长生命周期(静态或成员)
const std::string& getConf_cached() {
static std::string s = "cached";
return s; // C++11起局部静态初始化线程安全
}
双重释放与混用分配器
应用场景: 插件系统/跨库传递内存。 作死示例:
// 模块:图像解码插件
extern "C" void* decode(); // 在A库中用malloc分配
extern "C" void release(void*); // 在B库中用delete释放
void client() {
void* p = decode();
delete static_cast<int*>(p); // 与malloc不匹配 -> UB/崩溃
}
正确做法:
- 谁分配谁释放;或统一使用接口回调:
// 约定:由提供方提供deleter
using Deleter = void(*)(void*);
struct Buf { void* data; Deleter del; };
void use(Buf b) {
// ...
b.del(b.data); // 统一释放
}
未初始化与越界访问
应用场景: 高性能路径“少做检查”。 作死示例:
// 模块:网络包解析
struct Header { int len; char name[8]; };
void parse(const char* buf) {
Header* h = (Header*)buf; // 未对齐/未校验长度
char id = h->name[8]; // 越界访问
int x; if (h->len > 0) x += 1; // 读取未初始化变量x -> UB
}
正确做法:
#include <cstdint>
#include <cstring>
struct HeaderPOD { std::uint32_t len; char name[8]; };
bool parse_ok(const unsigned char* buf, size_t n) {
if (n < sizeof(HeaderPOD)) return false;
HeaderPOD h{};
std::memcpy(&h, buf, sizeof(h)); // 避免未对齐读取
if (h.len > n) return false;
// 安全使用 h.name[0..7]
return true;
}
智能指针环引用
应用场景: 观察者/树形结构。 作死示例:
// 模块:UI组件树
struct Node {
std::shared_ptr<Node> parent;
std::vector<std::shared_ptr<Node>> children;
};
// parent 与 child 都是 shared_ptr -> 环引用造成内存泄漏
正确做法:
#include <memory>
struct Node {
std::weak_ptr<Node> parent; // 断环
std::vector<std::shared_ptr<Node>> children;
};
指针别名与严格别名规则
应用场景: “优化”转型访问不同类型视图。 作死示例:
// 模块:数值计算
float f = 1.0f;
int* pi = reinterpret_cast<int*>(&f); // 违反严格别名,UB(除char/std::byte)
正确做法:
#include <cstring>
float f = 1.0f; int i = 0;
std::memcpy(&i, &f, sizeof i); // 合法的按位重解释
对象生存期与值类别
移动后对象的使用
应用场景: 以移动优化性能。 作死示例:
// 模块:日志缓冲
std::string a = "hello";
std::string b = std::move(a);
use(a); // 使用已移动对象的“内容”假定未变 -> 危险
正确做法:
- 只对已移动对象做析构/赋值/重新构造等安全操作;或在使用前重置:
a.clear(); // 明确状态
if (!a.empty()) { /* ... */ }
临时对象延长与绑定引用
应用场景: 返回 string_view/引用的轻量接口。 作死示例:
// 模块:配置切片
const std::string& ref = std::string("abc"); // 绑定到临时,生命周期结束即悬空
正确做法:
const std::string val = "abc"; // 拥有对象
const std::string& ref_ok = val; // 安全引用
// 或:使用 string_view 但确保被观测对象存活
异常与错误处理
构造函数抛异常导致资源泄漏
应用场景: 复杂对象聚合外部资源。 作死示例:
// 模块:数据库连接器
struct Conn {
FILE* f;
Conn(const char* path) {
f = std::fopen(path, "r");
if (!f) throw std::runtime_error("open fail");
// 下面又抛异常,f泄漏
throw std::runtime_error("later fail");
}
};
正确做法:
#include <cstdio>
#include <memory>
struct FileCloser { void operator()(FILE* f) const noexcept { if (f) std::fclose(f); } };
using File = std::unique_ptr<FILE, FileCloser>;
struct ConnOk {
File f;
ConnOk(const char* path) : f(std::fopen(path, "r")) {
if (!f) throw std::runtime_error("open fail");
// 之后即使抛异常,f自动关闭
}
};
noexcept/exception 规格误用
应用场景: 追求更快的移动操作或异常边界。 作死示例:
struct X {
X(X&&) noexcept { throw 42; } // 标记noexcept却抛异常 -> std::terminate
};
正确做法:
- 仅在强保证不抛时标记 noexcept;或内部捕获:
struct X2 {
X2(X2&&) noexcept { try { risky(); } catch (...) { /*回滚并吞异常*/ } }
void risky();
};
跨线程传播异常的坑
应用场景: 任务调度/线程池。 作死示例:
#include <thread>
void worker() { throw std::runtime_error("boom"); } // 未捕获 -> 调用 std::terminate
int main() {
std::thread t(worker);
t.join();
}
正确做法:
#include <exception>
#include <functional>
#include <thread>
#include <future>
template<class F>
auto wrap_async(F&& f) {
return std::async(std::launch::async, [g=std::forward<F>(f)]{
try { return g(); }
catch(...) { throw; }
});
}
并发与内存模型
数据竞争与未同步共享
应用场景: 多线程统计、缓存。 作死示例:
// 模块:统计计数
int g = 0;
void add() { for (int i=0;i<100000;i++) g++; } // 数据竞争
正确做法:
#include <atomic>
std::atomic<int> g{0};
void add_ok() { for (int i=0;i<100000;i++) g.fetch_add(1, std::memory_order_relaxed); }
非原子读写与释放-获取缺失
应用场景: 单例/发布-订阅。 作死示例(经典错误内存序)
#include <atomic>
struct Obj { int x; };
std::atomic<Obj*> p{nullptr};
void publish() {
Obj* q = new Obj{42};
p.store(q, std::memory_order_relaxed); // 未发布构造完成的happens-before
}
void consume() {
Obj* q = p.load(std::memory_order_relaxed); // 可能看到部分构造
if (q) (void)q->x;
}
正确做法:
void publish_ok() {
Obj* q = new Obj{42};
p.store(q, std::memory_order_release);
}
void consume_ok() {
if (Obj* q = p.load(std::memory_order_acquire)) {
(void)q->x; // 构造对读可见
}
}
死锁:锁顺序与双重检查锁定
应用场景: 多资源合并、单例。 作死示例:
std::mutex a, b;
void f() { std::lock_guard<std::mutex> l1(a); std::lock_guard<std::mutex> l2(b); /*...*/ }
void g() { std::lock_guard<std::mutex> l1(b); std::lock_guard<std::mutex> l2(a); /*...*/ } // 可能死锁
正确做法:
void h() {
std::scoped_lock lk(a, b); // 一次性获取多个锁,避免死锁
}
STL/库使用误区
容器迭代器失效
应用场景: 动态增长的 vector/map。 作死示例:
std::vector<int> v{1,2,3};
auto it = v.begin();
v.push_back(4); // 可能触发扩容
int x = *it; // 迭代器可能失效 -> UB
正确做法:
- 扩容可能导致 vector 的指针/迭代器全部失效;操作后应重新获取迭代器,或用 std::list / std::deque 视需求。
std::string_view 悬空
应用场景: 零拷贝切片。 作死示例:
std::string_view sv;
{
std::string s = "hello";
sv = s; // s销毁后,sv悬空
}
use(sv); // UB
正确做法:
- string_view 只观测不拥有;确保底层对象生命周期覆盖使用期,或改为 std::string。
算法与所有权的错位
应用场景: 自定义删除条件后继续使用元素。 作死示例:
std::vector<std::unique_ptr<int>> v;
v.emplace_back(new int(1)); v.emplace_back(new int(2));
v.erase(std::remove_if(v.begin(), v.end(), [](auto& p){ return *p==1; }), v.end());
int y = *v[0]; // 可能已被移动/删除
正确做法:
- erase/remove 惯用法后,不要假定索引稳定;必要时先稳定排序或收集保留项再重建容器。
语言细节与未定义行为
未序列化的自增表达式
应用场景: 想“一行写完”。 作死示例:
int i = 0;
i = i++; // 未定义行为(旧标准),语义混乱
正确做法:
int i = 0;
int old = i;
i += 1;
位域/移位越界与未定义行为
应用场景: 协议打包。 作死示例:
unsigned x = 1u << 32; // 在32位unsigned上移位越界 -> UB
正确做法:
#include <cstdint>
std::uint64_t x = 1ull << 32; // 使用足够宽度类型
对齐/严格别名与 memcpy 陷阱
应用场景: 从网络缓冲区读取结构体。 作死示例:
struct P { int a; double b; };
const unsigned char* buf = /*...*/ nullptr;
P* p = (P*)buf; // 可能未对齐,直接解引用UB
正确做法:
P p{};
std::memcpy(&p, buf, sizeof p); // 按位复制,避免未对齐解引用
接口设计与模块化
暴露裸指针与悬空 API
应用场景: C 接口或性能诉求。 作死示例:
// 返回内部缓冲区指针
const char* get_buf() { static std::string s = "abc"; return s.c_str(); }
// 调用方把指针缓存后,若 s 变更导致重分配,指针悬空
正确做法:
- 返回 std::string 或 std::string_view(并明确生命周期约束);或使用 Pimpl 隐藏实现并提供安全复制。
值语义/引用语义混乱
应用场景: DTO/领域对象传递。 作死示例:
struct User { std::string name; };
void setName(User& u, std::string_view v) { u.name = v; } // v 来自临时 view 被保存更糟
正确做法:
- 对输入使用值或 string_view(仅观测);对存储使用拥有类型(std::string)。为避免二义性,接口命名清晰:setNameView, setNameOwned。
构建/ABI/ODR
ODR 违反与重复定义
应用场景: 头文件中放定义。 作死示例:
// a.h
int x = 42; // 放在头文件被多次包含 -> 多重定义
正确做法:
// a.h
extern int x;
// a.cpp
int x = 42;
不同编译选项导致 ABI 不兼容
应用场景: 多模块联编。 作死示例:
- 一个静态库用 /MDd,另一个用 /MT;或不同的结构体对齐设置,导致跨模块传递对象崩溃。
正确做法:
- 统一标准库实现/运行时/对齐/异常模型;导出纯 C ABI 或 Pimpl 隐藏实现细节。
诊断与调试策略
工具链与编译选项建议
- 开启警告并视为错误:-Wall -Wextra -Werror(GCC/Clang),MSVC:/W4 /WX。
- 地址/未定义行为/线程检测:ASan/UBSan/TSan。
- 静态分析:clang-tidy(如 modernize-*、performance-*、bugprone-*)。
- 编译期契约:static_assert,[[nodiscard]]。
- 断言:assert 用于开发期,生产路径避免副作用。
代码评审检查清单
- 是否存在手写 new/delete?能否以 RAII 替代。
- 是否返回/存储了对临时或局部对象的引用/指针。
- 是否可能触发迭代器失效。
- 多线程路径是否有明确的同步和内存序。
- 是否误用 noexcept 或异常逃逸跨线程。
- 是否存在 ABI/ODR 风险(头文件定义、编译选项不一致)。
- string_view 的生命周期是否被正确管理。
- 是否有未初始化、越界、对齐问题。
- 是否定义了合理的拷贝/移动语义(=default/=delete)。
- 是否有潜在环引用。
附:示例综合演练(坏 -> 好)
场景:日志系统异步落盘
作死版(内存泄漏、竞态、异常逃逸):
#include <thread>
#include <vector>
#include <string>
#include <cstdio>
struct Logger {
FILE* f;
std::thread th;
std::vector<const char*> q; // 裸指针指向临时字符串
bool stop = false;
Logger(const char* path) { f = std::fopen(path, "w"); th = std::thread([this]{ run(); }); }
~Logger() { th.join(); std::fclose(f); } // 若线程抛异常或未stop,行为未定义
void log(const std::string& s) { q.push_back(s.c_str()); } // 悬空
void run() {
while(!stop) {
if (!q.empty()) { std::fprintf(f, "%s\n", q.back()); q.pop_back(); }
}
}
};
修正版(RAII + 线程安全 + 无悬空):
#include <mutex>
#include <condition_variable>
#include <queue>
#include <memory>
#include <atomic>
struct FileCloser { void operator()(FILE* f) const noexcept { if (f) std::fclose(f); } };
using File = std::unique_ptr<FILE, FileCloser>;
class LoggerOK {
File f_;
std::thread th_;
std::mutex m_;
std::condition_variable cv_;
std::queue<std::string> q_;
std::atomic<bool> stop_{false};
public:
explicit LoggerOK(const char* path) : f_(std::fopen(path, "w")) {
if (!f_) throw std::runtime_error("open fail");
th_ = std::thread([this]{ run(); });
}
~LoggerOK() {
stop_.store(true, std::memory_order_release);
cv_.notify_all();
if (th_.joinable()) th_.join();
}
void log(std::string s) {
{ std::lock_guard<std::mutex> lk(m_); q_.push(std::move(s)); }
cv_.notify_one();
}
private:
void run() {
std::unique_lock<std::mutex> lk(m_);
while (!stop_.load(std::memory_order_acquire)) {
cv_.wait(lk, [this]{ return stop_.load(std::memory_order_relaxed) || !q_.empty(); });
while (!q_.empty()) {
auto s = std::move(q_.front()); q_.pop();
lk.unlock();
std::fprintf(f_.get(), "%s\n", s.c_str());
lk.lock();
}
}
}
};
总结
- C++的“作死”多数源于生命周期不清、所有权边界不清与并发内存模型误解。
- 以 RAII、值语义优先、明确同步、工具链加持 为核心策略,结合代码审查清单,即可系统性降低风险。
额外提示
- 使用 =delete 禁用不期望的拷贝:X(const X&) = delete; X& operator=(const X&) = delete;
- 给移动操作标明 noexcept(确实不抛时),便于容器优化。
- 尽量使用 std::span/gsl::span 表达边界受控的视图。
- 在接口层返回 expected<T,E> 或 tl::expected 表达无异常错误;日志留证。
- 跨模块避免暴露 STL 类型到 ABI 边界;采用 Pimpl 或纯 C 结构。
- 用 std::scoped_lock、std::lock 统一多锁获取顺序,避免死锁。
- 用 [[nodiscard]] 标记关键返回值,防止被忽略。
- 使用 fmt 或 std::format(C++20)统一格式化输出,避免 printf 类型不匹配。