You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

含默认拷贝构造函数的代码理应触发段错误,为何能正常运行?

问题分析:浅拷贝引发的野指针与未定义行为

咱们直接给结论:你看到的现象符合默认语法逻辑,但这种内存访问属于未定义行为,绝对不是“安全”的预期结果——简单说就是你只是暂时没触发崩溃,但这本质上是代码的严重漏洞。

核心陷阱:默认拷贝构造函数的浅拷贝

你的代码没有自定义拷贝构造函数,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 09:44:55