含默认拷贝构造函数的代码理应触发段错误,为何能正常运行?
问题分析:浅拷贝引发的野指针与未定义行为
咱们直接给结论:你看到的现象符合默认语法逻辑,但这种内存访问属于未定义行为,绝对不是“安全”的预期结果——简单说就是你只是暂时没触发崩溃,但这本质上是代码的严重漏洞。
核心陷阱:默认拷贝构造函数的浅拷贝
你的代码没有自定义拷贝构造函数,C++会自动生成一个默认拷贝构造函数,它的逻辑只是把源对象的所有成员变量逐字节复制给新对象。这就带来了致命问题:
main里的myClass m构造时,dangerData指向一块malloc分配的堆内存(比如输出里的02f71458)。- 调用
myFunction(m)时,默认拷贝构造函数直接把m的dangerData指针复制给参数param,也就是说两个对象共享同一块堆内存。
析构后的野指针
当myFunction执行完毕退出作用域时,参数param会被销毁,触发自定义析构函数:
- 析构函数调用
free(dangerData),把共享内存归还给操作系统。 - 但
main里的m对象的dangerData指针依然指向这块已经被释放的内存——这就是典型的野指针。
为什么没触发段错误?
这是很多开发者的困惑点,原因其实很简单:
操作系统在你调用free后,并不会立刻把这块内存从进程地址空间中移除,也不会清空内存内容。它只是把这块内存标记为“可重新分配”。所以在内存被重新分配给其他变量/程序之前,你依然能读取甚至修改这块内存的内容——但这完全是运气问题,属于未定义行为。
比如你输出里m.msg()显示dangerData[12] = 97,正好是析构函数里赋值的'a'(ASCII码97),这说明内存内容还没被覆盖,但如果此时有其他代码申请内存,这块内存被分配走,你再访问就会触发段错误,或者出现莫名其妙的程序崩溃、数据错乱。
修复方案:遵循三法则实现深拷贝
要解决这个问题,你需要遵循C++的三法则(Rule of Three):当自定义了析构函数时,必须同时自定义拷贝构造函数和拷贝赋值运算符,实现深拷贝,让每个对象拥有独立的堆内存:
#include <cstdio> #include <cstdlib> #include <cstring> // 需要memcpy实现内存复制 class myClass { static int nextid; int myID; char *dangerData; public: myClass() { myID = nextid++; printf("Constructing myClass number %d\n", myID); dangerData = (char *)malloc(1024); dangerData[12] = 0; } // 自定义拷贝构造函数:深拷贝 myClass(const myClass& other) { myID = nextid++; printf("Copy constructing myClass number %d from %d\n", myID, other.myID); dangerData = (char *)malloc(1024); memcpy(dangerData, other.dangerData, 1024); // 复制内存内容而非指针 } // 自定义拷贝赋值运算符 myClass& operator=(const myClass& other) { if (this != &other) { // 防止自赋值导致的内存泄漏 free(dangerData); // 先释放当前对象的内存 dangerData = (char *)malloc(1024); memcpy(dangerData, other.dangerData, 1024); myID = nextid++; // 可根据业务需求调整ID逻辑 } return *this; } ~myClass() { printf("Destructing myClass number %d. dangerData = %p\n", myID, (void *) dangerData); free(dangerData); // 每个对象释放自己的独立内存 } void msg() { printf("Message from myClass number %d. dangerData[12] = %d\n", myID, dangerData[12]); } }; int myClass::nextid = 1; void myFunction(myClass param) { param.msg(); } int main() { myClass m; myFunction(m); m.msg(); return 0; }
修改后,每个myClass对象都会拥有独立的dangerData内存,析构时不会互相影响,彻底解决野指针问题。
你的代码输出解读
再回头看你的输出:
Constructing myClass number 1 Message from myClass number 1. dangerData[12] = 0 Destructing myClass number 1. dangerData = 02f71458. dangerData[12] = 0 Message from myClass number 1. dangerData[12] = 97 Destructing myClass number 1. dangerData = 02f71458. dangerData[12] = 97
- 第一个析构是
param对象的析构,它修改了共享内存的dangerData[12]为'a'然后free了内存。 - 之后
main里的m访问的是已释放的内存,读取到了之前修改的97,这是未定义行为的表现。 - 最后
main里的m析构时,又对已经被free的内存调用了一次free——这同样是未定义行为,可能破坏堆结构或导致程序崩溃。
内容的提问来源于stack exchange,提问作者Marco Merlini
相关产品推荐
相关产品推荐

