引言:当C++不再“仅仅是一门语言”
你已经编写了十几个控制台程序,学会了使用std::vector,甚至掌握了模板。但C++是一门从不原谅肤浅态度的语言。当开发者遇到未定义行为(UB)并开始意识到编译器并非友善的朋友,而是严格的对手时,才算真正进入了新的阶段。
在本文中,我们将讨论的不是诸如忘记;或混淆=与==这类低级错误。我们将探讨那些已经熟悉语法但尚未掌握语言精髓的人所面临的陷阱。这些错误是工业代码中90%难以捕捉的bug的根源。
1. 忽视五法则与零法则
问题:隐式生成的特殊成员函数
许多从Java或Python转来的开发者认为,编译器会自动“完美处理”对象的拷贝和移动。这是一个危险的误解。
考虑一个包含动态资源的类:
class Buffer {public: Buffer(size_t size) : data(new char[size]), size(size) {} ~Buffer() { delete[] data; } // 一切正常?不!缺少拷贝构造函数和赋值运算符!private: char* data; size_t size;};
void process() { Buffer a(100); Buffer b = a; // 浅拷贝!两个指针指向同一块内存。} // 双重释放——UB!五法则规定:如果你定义了至少一个特殊成员函数(析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符),你就必须定义它们全部。
解决方案:使用std::unique_ptr或std::vector管理资源。这就是零法则:如果你的类不直接管理资源,编译器会自动生成正确的特殊成员函数。
class Buffer {public: Buffer(size_t size) : data(std::make_unique<char[]>(size)), size(size) {} // 零法则——析构、拷贝和移动都能正确工作!private: std::unique_ptr<char[]> data; size_t size;};2. 错误使用移动语义
问题:移动不会自动使对象变为“空”
最阴险的错误之一就是相信在std::move之后,源对象会变得“安全”可用。实际上,std::move仅仅是将对象转换为右值引用。被移动对象的状态取决于具体实现。
std::string a = "Hello, World!";std::string b = std::move(a);std::cout << a; // 技术上——UB?不,但a的内容未定义!// 可能是空字符串,也可能是 "Hello, World!"标准仅保证被移动对象处于有效但未定义的状态。你可以调用a.clear()或a = "new value",但不应依赖其内容。
建议:移动后,要么将对象重置为已知状态,要么停止使用它。对于自定义类型,始终记录移动构造函数的后置条件。
3. 通过原始指针和异常导致的内存泄漏
问题:异常“打断”资源释放链
即使是有经验的开发者有时也会编写手动管理内存的代码:
void process() { int* ptr1 = new int(42); int* ptr2 = new int(100); // 如果这里抛出异常——ptr1泄漏! dangerous_function(); delete ptr2; delete ptr1;}C++中没有finally