1. 内存管理从入门到翻车现场刚入行那会儿我最怕面试官问内存管理。直到自己带团队后才发现90%的C崩溃问题都源于内存使用不当。先说个真实案例我们项目曾因一个vector.clear()操作导致线上服务崩溃最后发现是迭代器失效引发野指针。这种问题用Valgrind都难查最好的办法就是提前预防。1.1 new/delete的十八种死法新手最常犯的错误就是以为delete完就万事大吉。看这段代码class Texture { public: Texture(int w, int h) : data(new float[w*h]) {} ~Texture() { delete[] data; } private: float* data; }; void loadAsset() { Texture* tex new Texture(1024, 1024); // ...使用纹理... delete tex; }表面看没问题实际暗藏三个坑没检查new是否成功虽然现代C很少失败缺少拷贝构造函数和赋值运算符会导致重复delete更安全的做法是用unique_ptr封装我现在的编码规范是只要看到裸new/delete就要求重构。用make_shared/make_unique能避免90%的内存泄漏auto tex std::make_uniqueTexture(1024, 1024);1.2 容器类的内存陷阱STL容器用起来顺手但有些行为反直觉。比如这段代码std::vectorint createData() { std::vectorint data {1,2,3,4,5}; return data; } void process() { auto ref createData(); // 悬垂引用 ref.push_back(6); // 瞬间爆炸 }问题出在返回临时对象的引用。更隐蔽的是迭代器失效问题std::vectorint v {1,2,3,4,5}; auto it v.begin(); v.push_back(6); // 可能导致迭代器失效 *it 10; // 可能崩溃建议修改容器后假设所有迭代器失效必要时用索引替代迭代器。2. 多线程并发编程的黑暗森林去年我们团队有个bug找了整整两周在多核CPU上概率性出现的数值错误。最后发现是缓存一致性导致的可见性问题。多线程就像黑暗森林每个线程都可能是猎人也可能是猎物。2.1 volatile的经典误解很多面试者说volatile能保证线程安全这是大错特错。看这个例子volatile int counter 0; void increment() { for(int i0; i1000000; i) { counter; // 仍然存在竞态条件 } }volatile只保证每次从内存读取但操作包含读-改-写三个步骤。正确做法是std::atomicint counter{0}; void safeIncrement() { for(int i0; i1000000; i) { counter.fetch_add(1, std::memory_order_relaxed); } }2.2 锁的使用艺术锁用不好比不用更危险。常见死锁场景// 错误示例 std::mutex m1, m2; void threadA() { m1.lock(); m2.lock(); // 如果threadB先锁m2就死锁 // ... m2.unlock(); m1.unlock(); } void threadB() { m2.lock(); m1.lock(); // 与threadA锁顺序相反 // ... m1.unlock(); m2.unlock(); }解决方案总是按固定顺序上锁用std::lock同时锁多个互斥量void safeLock() { std::lock(m1, m2); std::lock_guardstd::mutex lk1(m1, std::adopt_lock); std::lock_guardstd::mutex lk2(m2, std::adopt_lock); // ... }3. 面向对象看似简单实则坑多C的OOP特性比其他语言复杂得多。有次代码评审我发现有人用多态时没把析构函数声明为virtual导致内存泄漏。这类问题编译期不会报错运行时才会暴露。3.1 多态的隐藏成本虚函数看似简单实则影响深远class Base { public: virtual ~Base() default; virtual void foo() { std::cout Base; } }; class Derived : public Base { public: void foo() override { std::cout Derived; } }; void test() { Base* b new Derived(); b-foo(); // 输出Derived delete b; // 正确调用Derived析构函数 }如果忘记写virtual析构函数delete时就不会调用派生类的析构函数。更隐蔽的是性能影响每个含虚函数的类都有虚表指针虚函数调用比普通函数多一次间接寻址影响编译器内联优化3.2 移动语义的陷阱C11的移动语义是性能利器但用错会很惨class Buffer { public: Buffer(size_t size) : data(new int[size]), size(size) {} // 移动构造函数 Buffer(Buffer other) noexcept : data(other.data), size(other.size) { other.data nullptr; // 必须置空 } ~Buffer() { delete[] data; } private: int* data; size_t size; }; void process() { Buffer buf1(1024); Buffer buf2 std::move(buf1); // 此时buf1.data为nullptr }常见错误移动后没置空原指针没加noexcept导致标准库不敢用移动在移动操作中抛出异常4. 模板元编程编译期的双刃剑模板是C最强大的特性之一也是编译错误的重灾区。我曾见过一个模板导致的编译错误信息有2000多行IDE直接卡死。4.1 SFINAE的巧妙用法SFINAE(Substitution Failure Is Not An Error)是模板元编程的核心技术templatetypename T auto print(const T val) - decltype(std::cout val, void()) { std::cout val; } templatetypename T void print(...) { std::cout [unprintable]; } void test() { print(42); // 调用第一个版本 print(std::vectorint{}); // 调用第二个版本 }这种技术可以用来根据类型特性选择不同实现编译期类型检查实现概念(concept)检查4.2 模板的性能代价模板虽强大但过度使用会导致编译时间爆炸模板实例化是编译期行为代码膨胀每个类型参数组合都会生成新代码调试困难错误信息晦涩难懂建议用extern template显式实例化常用类型将模板实现分离到.cpp文件需特殊技巧优先使用运行时多态替代模板元编程5. 现代C的最佳实践经过多年踩坑我总结了几条黄金法则能用智能指针就不用裸指针能用std::array/vector就不用C风格数组多线程代码默认加锁再考虑优化所有继承体系的基类析构函数必须为virtual移动操作要加noexcept模板代码要写注释因为编译器错误信息不会读最后分享一个真实案例我们曾用std::string存储二进制数据结果因为遇到\0字符导致截断。后来改用std::vectoruint8_t才解决问题。C的坑永远踩不完关键是要建立防御性编程的习惯。
【C++八股文】实战避坑篇
1. 内存管理从入门到翻车现场刚入行那会儿我最怕面试官问内存管理。直到自己带团队后才发现90%的C崩溃问题都源于内存使用不当。先说个真实案例我们项目曾因一个vector.clear()操作导致线上服务崩溃最后发现是迭代器失效引发野指针。这种问题用Valgrind都难查最好的办法就是提前预防。1.1 new/delete的十八种死法新手最常犯的错误就是以为delete完就万事大吉。看这段代码class Texture { public: Texture(int w, int h) : data(new float[w*h]) {} ~Texture() { delete[] data; } private: float* data; }; void loadAsset() { Texture* tex new Texture(1024, 1024); // ...使用纹理... delete tex; }表面看没问题实际暗藏三个坑没检查new是否成功虽然现代C很少失败缺少拷贝构造函数和赋值运算符会导致重复delete更安全的做法是用unique_ptr封装我现在的编码规范是只要看到裸new/delete就要求重构。用make_shared/make_unique能避免90%的内存泄漏auto tex std::make_uniqueTexture(1024, 1024);1.2 容器类的内存陷阱STL容器用起来顺手但有些行为反直觉。比如这段代码std::vectorint createData() { std::vectorint data {1,2,3,4,5}; return data; } void process() { auto ref createData(); // 悬垂引用 ref.push_back(6); // 瞬间爆炸 }问题出在返回临时对象的引用。更隐蔽的是迭代器失效问题std::vectorint v {1,2,3,4,5}; auto it v.begin(); v.push_back(6); // 可能导致迭代器失效 *it 10; // 可能崩溃建议修改容器后假设所有迭代器失效必要时用索引替代迭代器。2. 多线程并发编程的黑暗森林去年我们团队有个bug找了整整两周在多核CPU上概率性出现的数值错误。最后发现是缓存一致性导致的可见性问题。多线程就像黑暗森林每个线程都可能是猎人也可能是猎物。2.1 volatile的经典误解很多面试者说volatile能保证线程安全这是大错特错。看这个例子volatile int counter 0; void increment() { for(int i0; i1000000; i) { counter; // 仍然存在竞态条件 } }volatile只保证每次从内存读取但操作包含读-改-写三个步骤。正确做法是std::atomicint counter{0}; void safeIncrement() { for(int i0; i1000000; i) { counter.fetch_add(1, std::memory_order_relaxed); } }2.2 锁的使用艺术锁用不好比不用更危险。常见死锁场景// 错误示例 std::mutex m1, m2; void threadA() { m1.lock(); m2.lock(); // 如果threadB先锁m2就死锁 // ... m2.unlock(); m1.unlock(); } void threadB() { m2.lock(); m1.lock(); // 与threadA锁顺序相反 // ... m1.unlock(); m2.unlock(); }解决方案总是按固定顺序上锁用std::lock同时锁多个互斥量void safeLock() { std::lock(m1, m2); std::lock_guardstd::mutex lk1(m1, std::adopt_lock); std::lock_guardstd::mutex lk2(m2, std::adopt_lock); // ... }3. 面向对象看似简单实则坑多C的OOP特性比其他语言复杂得多。有次代码评审我发现有人用多态时没把析构函数声明为virtual导致内存泄漏。这类问题编译期不会报错运行时才会暴露。3.1 多态的隐藏成本虚函数看似简单实则影响深远class Base { public: virtual ~Base() default; virtual void foo() { std::cout Base; } }; class Derived : public Base { public: void foo() override { std::cout Derived; } }; void test() { Base* b new Derived(); b-foo(); // 输出Derived delete b; // 正确调用Derived析构函数 }如果忘记写virtual析构函数delete时就不会调用派生类的析构函数。更隐蔽的是性能影响每个含虚函数的类都有虚表指针虚函数调用比普通函数多一次间接寻址影响编译器内联优化3.2 移动语义的陷阱C11的移动语义是性能利器但用错会很惨class Buffer { public: Buffer(size_t size) : data(new int[size]), size(size) {} // 移动构造函数 Buffer(Buffer other) noexcept : data(other.data), size(other.size) { other.data nullptr; // 必须置空 } ~Buffer() { delete[] data; } private: int* data; size_t size; }; void process() { Buffer buf1(1024); Buffer buf2 std::move(buf1); // 此时buf1.data为nullptr }常见错误移动后没置空原指针没加noexcept导致标准库不敢用移动在移动操作中抛出异常4. 模板元编程编译期的双刃剑模板是C最强大的特性之一也是编译错误的重灾区。我曾见过一个模板导致的编译错误信息有2000多行IDE直接卡死。4.1 SFINAE的巧妙用法SFINAE(Substitution Failure Is Not An Error)是模板元编程的核心技术templatetypename T auto print(const T val) - decltype(std::cout val, void()) { std::cout val; } templatetypename T void print(...) { std::cout [unprintable]; } void test() { print(42); // 调用第一个版本 print(std::vectorint{}); // 调用第二个版本 }这种技术可以用来根据类型特性选择不同实现编译期类型检查实现概念(concept)检查4.2 模板的性能代价模板虽强大但过度使用会导致编译时间爆炸模板实例化是编译期行为代码膨胀每个类型参数组合都会生成新代码调试困难错误信息晦涩难懂建议用extern template显式实例化常用类型将模板实现分离到.cpp文件需特殊技巧优先使用运行时多态替代模板元编程5. 现代C的最佳实践经过多年踩坑我总结了几条黄金法则能用智能指针就不用裸指针能用std::array/vector就不用C风格数组多线程代码默认加锁再考虑优化所有继承体系的基类析构函数必须为virtual移动操作要加noexcept模板代码要写注释因为编译器错误信息不会读最后分享一个真实案例我们曾用std::string存储二进制数据结果因为遇到\0字符导致截断。后来改用std::vectoruint8_t才解决问题。C的坑永远踩不完关键是要建立防御性编程的习惯。