C++作死代码黑榜:避坑实战手册_c++代码讲解

C++作死代码黑榜:避坑实战手册_c++代码讲解

编码文章call10242025-09-12 16:26:585A+A-

面向一线开发者的“反面教材”与修复策略合集。每个坑都配有作死示例与正确做法,并标注应用场景与模块归类,便于团队培训与代码评审使用。


内存与资源管理

new/delete 与 RAII 缺失

应用场景: 业务模块快速原型、旧项目迁移。 作死示例:

 // 模块:订单处理/缓存
 void foo() { 
     int* p = new int[10];
     // ... 中途return或异常时泄漏
     if (some_condition()) return;
     delete p; // 错误:应为 delete[]
 }

问题解析:

  • 提前返回未释放;deletenew[] 不匹配导致未定义行为(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::stringstd::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 用于开发期,生产路径避免副作用。

代码评审检查清单

  1. 是否存在手写 new/delete?能否以 RAII 替代。
  2. 是否返回/存储了对临时或局部对象的引用/指针。
  3. 是否可能触发迭代器失效。
  4. 多线程路径是否有明确的同步和内存序。
  5. 是否误用 noexcept 或异常逃逸跨线程。
  6. 是否存在 ABI/ODR 风险(头文件定义、编译选项不一致)。
  7. string_view 的生命周期是否被正确管理。
  8. 是否有未初始化、越界、对齐问题。
  9. 是否定义了合理的拷贝/移动语义(=default/=delete)。
  10. 是否有潜在环引用。

附:示例综合演练(坏 -> 好)

场景:日志系统异步落盘

作死版(内存泄漏、竞态、异常逃逸):

 #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_lockstd::lock 统一多锁获取顺序,避免死锁。
  • [[nodiscard]] 标记关键返回值,防止被忽略。
  • 使用 fmtstd::format(C++20)统一格式化输出,避免 printf 类型不匹配。



点击这里复制本文地址 以上内容由文彬编程网整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!
qrcode

文彬编程网 © All Rights Reserved.  蜀ICP备2024111239号-4