给学习者和他们的 agent:C++ 里"看着对、其实错"的高频坑。 每条 = 误解 → 真相 → 最小示例。这是个持续补充的活文档,种子条目如下。
误解:auto x = something; 保留 something 的 const 和引用。
真相:auto 走模板参数推导规则,剥掉顶层 const 和引用,得到的是副本。
int v = 1;
const int& r = v;
auto a = r; // a 是 int(副本),const 和 & 都没了
const auto& b = r; // b 才是 const int&
for (auto x : vec) { /* 每个元素都拷贝! */ }
for (const auto& x : vec) { /* 不拷贝 */ }要保留,用 auto& / const auto& / decltype(auto)。
参见:卷二·现代特性(vol2-modern-features)。
误解:这行构造了一个 Widget。
真相:这是最恼人解析(Most Vexing Parse) —— 编译器把它读成"一个叫 w、接受(返回 Bar 的函数)、返回 Widget 的函数声明"。
Widget w(Bar()); // 声明了一个函数,不是对象!
Widget w{Bar()}; // 这才是对象(花括号初始化)
Widget w((Bar())); // 加括号也是对象参见:卷一·基础(vol1-fundamentals)。
误解:花括号初始化和圆括号一样,会隐式收窄。
真相:花括号初始化禁止窄化。
int x(3.14); // OK,隐式截断为 3
int y{3.14}; // 编译错误误解:std::move(x) 把 x 的内容搬走了。
真相:std::move 只是个转成右值引用的 cast,本身什么都不搬。真正搬不搬,取决于类型有没有移动构造/赋值;没有就回退到拷贝。
void sink(std::string&& s) {
std::string local = s; // 拷贝!s 在这里是左值
std::string moved = std::move(s); // 这才真移动
}两个连带坑:
- 命名的右值引用是左值:参数
T&& s,用s时是左值,想再搬得再std::move(s)。 - 对
const对象std::move→ 拷贝:std::move得到const T&&,移动构造要非 const,于是匹配拷贝,"移动"悄悄变拷贝。
参见:卷二·现代特性(vol2-modern-features)。
误解:只要不手动 delete,基类析构写不写 virtual 无所谓。
真相:通过基类指针/引用 delete 派生对象,若基类析构非 virtual,是未定义行为(典型后果:派生部分不析构 → 资源泄漏)。有虚函数的类,析构就该 virtual。
struct Base { virtual void f(); /* 缺 virtual ~Base() */ };
struct Derived : Base { std::ofstream file; /* 析构关文件 */ };
Base* p = new Derived;
delete p; // UB:Derived::~Derived 没被调用,file 没关参见:卷一·基础(vol1-fundamentals)。
误解:我拿到的 vector 元素引用或迭代器,后面继续 push_back 也没事。
真相:push_back 触发扩容(容量变化)时,所有引用和迭代器全部失效。要跨修改持有,用下标或先 reserve。
std::vector<int> v = {1, 2, 3};
int& first = v[0];
v.push_back(4); // 可能扩容 → first 失效,再用是 UB各操作的失效规则不同(如 erase 只失效被删及之后的),改动容器前先查规则。
参见:卷三·标准库(vol3-standard-library)。
误解:程序输出了预期结果,代码就是对的。
真相:未定义行为(UB) —— 有符号整数溢出、越界访问、未初始化读取、违反 ODR、数据竞争等 —— 在 -O0 下可能"碰巧"正常,在 -O2 下可能整个崩、或被编译器直接优化掉。不能靠"能跑"证明正确,要用工具(ASan / UBSan / TSan)+ 按标准理解行为。
int a = INT_MAX;
int b = a + 1; // 有符号溢出 = UB,不是"绕回"参见:卷六·性能(vol6-performance)、卷七·工程(vol7-engineering)、卷五·并发的数据竞争部分(vol5-concurrency)。