1995旧版与现代C++构造/析构函数及拷贝消除机制疑问
返回值场景下的对象构造行为演变
你观察到的全程仅触发一次构造、一次析构的现象,来自返回值优化(RVO/NRVO),这项特性的出现远早于C11的移动语义,在C标准发展的不同阶段行为有明确差异:
C98/03(C11落地前)的实际逻辑
- 从标准规则层面,C++98/03就已经允许编译器执行拷贝省略优化:哪怕拷贝构造函数存在可观测的副作用(比如你代码里打印日志的逻辑),编译器也可以直接跳过拷贝构造的调用,将函数内要返回的对象直接构造在最终接收变量的内存位置上。
- 这个阶段优化不是强制要求,实际运行结果完全依赖编译器实现和编译选项:
- 无优化的debug模式下,早期编译器(如VC6、GCC 2.x时代的版本)会严格按照抽象语义执行:首先在create函数栈帧构造局部temp对象,return时拷贝构造出临时返回值对象,temp析构;再用临时返回值拷贝构造main函数中的f对象,临时对象析构;最后main退出时f析构。整个流程会触发2次拷贝构造,共3个对象的生命周期,输出顺序为
ctor -> copy -> dstor -> copy -> dstor -> dstor。 - 开启基础优化后,几乎所有合规编译器都会实现命名返回值优化(NRVO,即针对有名字的局部返回变量的优化),直接将temp对象构造在main函数中f的内存位置,全程仅1次构造、1次析构,和你现在看到的运行结果完全一致。
- 无优化的debug模式下,早期编译器(如VC6、GCC 2.x时代的版本)会严格按照抽象语义执行:首先在create函数栈帧构造局部temp对象,return时拷贝构造出临时返回值对象,temp析构;再用临时返回值拷贝构造main函数中的f对象,临时对象析构;最后main退出时f析构。整个流程会触发2次拷贝构造,共3个对象的生命周期,输出顺序为
C11到C17阶段的规则变化
- C++11新增的移动语义并没有替代拷贝省略,只是给未触发优化的场景提供了效率更高的兜底方案:如果编译器没有选择做拷贝省略,return局部对象时会优先调用移动构造而非拷贝构造,但拷贝省略本身仍然是可选的优化项。
- 你手动添加移动构造却没有触发调用,本质是编译器直接执行了拷贝省略,连移动构造的步骤都被跳过了。
C++17及之后的规则
- C++17开始,纯右值场景的拷贝省略成为标准强制要求,不再是可选优化:比如
Foo f = Foo::create()这种写法,create返回的纯右值必须直接构造在f的内存位置,不允许调用拷贝或移动构造——哪怕你手动删除了拷贝和移动构造函数,代码也可以正常编译。 - 针对命名局部变量返回的NRVO(包括你写的多层函数嵌套返回的场景),截至C++23标准仍然是可选优化,但当前主流编译器在O1及以上优化级别下都可以稳定实现跨多层函数栈帧的优化,直接将最内层创建的对象构造在最外层接收变量的位置,全程不触发拷贝或移动构造。
指向自身成员的指针不悬空的原因
你代码中p = &v的写法在优化生效时没有出现悬空指针,原因非常直接:当NRVO生效时,create函数里的temp、f2函数里的t、f1函数里的t、main里的f是同一个对象,占用完全相同的内存地址,p存储的就是当前对象内成员v的真实地址,自然不会失效。
注意:这种写法是强依赖拷贝省略的未定义安全行为。如果遇到编译器无法做NRVO的场景——比如函数内存在多个return分支返回不同的局部对象、编译时关闭了所有优化,默认生成/你手写的拷贝构造如果没有同步更新p指针的指向,p就会指向已经析构的局部对象的内存,成为悬空指针,调用
show()就会触发未定义行为。
你可以通过修改代码让编译器无法执行NRVO来复现这个问题,比如:
Foo Foo::create(int v) { Foo t1(v); Foo t2(v + 1); // 多分支返回不同命名对象,编译器无法稳定做NRVO if (v > 0) { return t1; } return t2; }
这种场景下编译器会调用拷贝/移动构造,你写的拷贝构造没有更新p的值,就会出现悬空指针问题。
内容的提问来源于stack exchange,提问作者samanthaj
相关产品推荐
相关产品推荐

