讲师中心 微信公众号
AI工具推荐 视频效率加速

C++对象拷贝构造与内存分配关系

酷枫君_7711

酷枫君_7711

发布时间:2025-09-21 15:27:01

|

228人浏览过

|

来源于php中文网

原创

对象拷贝时若含指针,默认浅拷贝会导致多对象共享同一内存,引发双重释放或数据污染;深拷贝通过自定义拷贝构造函数与赋值运算符,为新对象分配独立内存并复制内容,避免资源冲突;C++11移动语义进一步优化,以右值引用实现资源“窃取”,转移而非复制内存,提升性能。

c++对象拷贝构造与内存分配关系

C++中,对象拷贝构造与内存分配的关系,说白了,就是当你复制一个对象时,它内部的数据,尤其是那些在堆上动态分配的资源,究竟是跟着新对象一起“复制”一份全新的内存,还是仅仅让新旧对象共享同一块内存地址。这听起来有点抽象,但它直接决定了你的程序会不会出现内存泄露、双重释放,甚至莫名其其妙的崩溃。在我看来,理解这一点是掌握C++资源管理的关键一步,它远比你想象的要复杂,也更有趣。

当一个C++对象被拷贝时,无论是通过拷贝构造函数还是拷贝赋值运算符,其核心就在于如何处理这个对象所拥有的资源。默认情况下,C++编译器会为我们生成一个“成员逐一拷贝”的版本。对于那些基本类型(如int、double)或者不含指针的复合类型,这通常没什么问题,因为每个成员都会被直接复制一份,新旧对象各自拥有自己的数据。

然而,一旦你的类中包含了指向动态分配内存的指针(比如

char*
指向一个堆上的字符串,或者
int*
指向一个整数数组),问题就来了。默认的成员逐一拷贝,只会把指针本身的值(也就是内存地址)复制给新对象。这意味着,新旧两个对象内部的指针,都指向了堆上的同一块内存。这就是所谓的“浅拷贝”。

浅拷贝的后果是灾难性的。当其中一个对象被销毁时,它的析构函数会释放这块共享的内存。而当另一个对象也被销毁时,它的析构函数会试图去释放一块已经被释放过的内存,这就会导致“双重释放”(double-free)错误,程序轻则崩溃,重则产生难以追踪的内存腐败。更别提,一个对象对共享内存的修改,会悄无声息地影响到另一个对象,这显然不是我们期望的“拷贝”。

立即学习“C++免费学习笔记(深入)”;

为了解决这个问题,我们需要实现“深拷贝”。深拷贝的核心在于,当复制一个对象时,如果它拥有动态分配的资源,我们不仅要复制对象本身,还要为这些动态资源在堆上重新分配一块全新的内存,并将旧内存中的内容复制到新内存中。这样,新旧对象就各自拥有了独立的资源,互不影响。这通常涉及到自定义拷贝构造函数和拷贝赋值运算符,在其中显式地进行内存分配(

new
)和内容复制。

为什么默认拷贝构造函数有时会引发内存问题?

这几乎是所有C++初学者都会遇到的一个坑。简单来说,默认拷贝构造函数执行的是“位拷贝”或者说“浅拷贝”。对于非指针成员,它会按位复制,这没毛病。但对于指针成员,它只会复制指针变量本身存储的地址值,而不会去复制指针所指向的那块内存区域的内容。

想象一下,你有一个

MyString
类,内部有个
char* data
成员,它指向了堆上的一段字符串。当你写下
MyString s2 = s1;
时,如果使用的是默认拷贝构造函数,那么
s1.data
和
s2.data
会指向同一块堆内存。

现在,

s1
和
s2
都认为自己“拥有”这块内存。当
s1
的生命周期结束,它的析构函数会调用
delete[] data;
来释放这块内存。一切看起来很正常。然而,当
s2
的生命周期也结束时,它的析构函数同样会调用
delete[] data;
。此时,它试图释放的内存地址,已经被
s1
释放过了!这就是典型的“双重释放”错误。程序很可能会因此崩溃,或者出现不可预测的行为。

此外,如果

s1
或
s2
中的任何一个修改了
data
指向的内容,另一个对象也会受到影响,因为它们操作的是同一块内存。这显然违背了“拷贝”的语义——我们期望拷贝后的对象是独立的。这种情况下,默认拷贝构造函数的设计哲学(效率优先,假设用户会处理复杂资源)就与实际需求产生了冲突。

C++14
C++14

C++14 对 C++11 的修正与增强版本,适合旧系统维护和较老工具链兼容。

下载

如何正确实现深拷贝以避免资源泄露和悬空指针?

正确实现深拷贝,关键在于“重新分配”和“复制内容”。它要求我们手动编写拷贝构造函数和拷贝赋值运算符,来处理类中动态分配的资源。

我们以一个简单的

MyString
类为例,它内部管理一个
char*
类型的动态字符串:

#include <iostream>
#include <cstring> // For strlen, strcpy

class MyString {
public:
    char* data;
    size_t length;

    // 构造函数
    MyString(const char* str = "") : length(strlen(str)) {
        data = new char[length + 1]; // +1 for null terminator
        strcpy(data, str);
        std::cout << "Constructor: " << data << std::endl;
    }

    // 析构函数:释放动态分配的内存
    ~MyString() {
        if (data) {
            std::cout << "Destructor: " << data << std::endl;
            delete[] data;
            data = nullptr; // Good practice to nullify
        }
    }

    // 深拷贝构造函数
    MyString(const MyString& other) : length(other.length) {
        data = new char[length + 1]; // 1. 为新对象分配新的内存
        strcpy(data, other.data);    // 2. 复制内容
        std::cout << "Deep Copy Constructor: " << data << std::endl;
    }

    // 深拷贝赋值运算符
    MyString& operator=(const MyString& other) {
        if (this == &other) { // 处理自我赋值,避免删除自己的资源
            return *this;
        }

        // 1. 释放旧资源
        delete[] data;

        // 2. 为新对象分配新的内存
        length = other.length;
        data = new char[length + 1];

        // 3. 复制内容
        strcpy(data, other.data);

        std::cout << "Deep Copy Assignment: " << data << std::endl;
        return *this;
    }

    // 打印字符串
    void print() const {
        std::cout << "String: " << data << std::endl;
    }
};

int main() {
    MyString s1("Hello, C++");
    MyString s2 = s1; // 调用深拷贝构造函数
    MyString s3;
    s3 = s1;          // 调用深拷贝赋值运算符

    s1.print();
    s2.print();
    s3.print();

    // 尝试修改s1,看是否影响s2和s3
    // 这里为了简化,不提供修改接口,但如果提供,它们将是独立的。
    // 例如,如果s1内部修改了data指向的内存,s2和s3不会受影响。

    return 0;
}

在这个例子中:

  1. 拷贝构造函数
    MyString(const MyString& other)
    :它接收一个常量引用作为参数。在函数体内部,我们首先为
    data
    成员重新分配一块足够大的内存,然后使用
    strcpy
    将
    other.data
    指向的内容复制到新分配的内存中。这样,
    s2
    就拥有了与
    s1
    完全独立的一份“Hello, C++”副本。
  2. 拷贝赋值运算符
    operator=(const MyString& other)
    :这个稍微复杂一些。它需要先检查是否是自我赋值(
    this == &other
    ),以防止在释放旧资源时把源对象的资源也删掉。接着,它会释放当前对象(
    this
    )旧的
    data
    内存,然后重新分配内存,并复制
    other
    的内容。最后返回
    *this
    以便链式赋值。

通过这种方式,我们确保了每个

MyString
对象在拷贝后都拥有自己独立的内存资源,从而避免了双重释放和悬空指针的问题。这就是所谓的“Rule of Three”(如果你定义了析构函数、拷贝构造函数、拷贝赋值运算符中的任何一个,就应该定义全部三个),在C++11后扩展为“Rule of Five”(增加了移动构造函数和移动赋值运算符)。

C++11后的移动语义如何影响对象拷贝和内存管理?

C++11引入的移动语义(Move Semantics)是对传统拷贝行为的一次重大优化,尤其是在处理大型对象或含有动态资源的对象时。它并没有取代拷贝,而是提供了一种更高效的资源转移方式,而不是复制。

我们知道,深拷贝涉及到内存的重新分配和内容的复制,这对于大型对象来说开销是很大的。想象一下,一个函数返回一个

MyString
对象,或者将一个
MyString
对象传递给另一个函数,如果每次都进行深拷贝,性能会很差。在这些场景下,源对象往往是一个即将被销毁的临时对象,它的资源我们不再需要,与其深拷贝,不如直接“偷”过来。

移动语义就是为了解决这个问题。它通过移动构造函数(

MyString(MyString&& other)
)和移动赋值运算符(
MyString& operator=(MyString&& other)
)来实现。这里的
&&
表示右值引用,它通常绑定到临时对象或即将被销毁的对象。

在移动构造函数中,我们不再为新对象分配内存,也不再复制内容。相反,我们直接将源对象(

other
)的资源(比如
data
指针)“偷”过来,赋给新对象,然后将源对象的指针置为
nullptr
,这样源对象在销毁时就不会错误地释放被“偷走”的资源了。

// 移动构造函数
MyString(MyString&& other) noexcept : data(nullptr), length(0) { // 初始化为安全状态
    data = other.data;      // 1. 窃取资源
    length = other.length;

    other.data = nullptr;   // 2. 将源对象的资源指针置空
    other.length = 0;       // 避免源对象析构时释放资源
    std::cout << "Move Constructor: " << data << std::endl;
}

// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
    if (this == &other) {
        return *this;
    }

    delete[] data; // 释放当前对象的旧资源

    data = other.data;      // 1. 窃取资源
    length = other.length;

    other.data = nullptr;   // 2. 将源对象的资源指针置空
    other.length = 0;       // 避免源对象析构时释放资源
    std::cout << "Move Assignment: " << data << std::endl;
    return *this;
}

通过移动语义,内存管理变得更加高效:

  • 拷贝构造函数:仍然用于需要独立副本的场景,它会进行新的内存分配和内容复制。
  • 移动构造函数:用于资源所有权转移的场景,它不会分配新内存,只是简单地将指针从一个对象转移到另一个对象,并将源对象的指针清空。这大大减少了不必要的内存分配和数据复制,提升了性能。

在现代C++编程中,我们倾向于遵循“Rule of Zero”或“Rule of Five”。“Rule of Zero”意味着如果可能,尽量使用

std::unique_ptr
、
std::shared_ptr
等智能指针或标准库容器来管理资源,让它们自动处理拷贝和移动,从而避免手动编写这些特殊成员函数。如果必须手动管理资源,那么就应该完整实现Rule of Five,确保拷贝、移动和析构都得到妥善处理。

热门AI工具

更多
火山引擎

火山引擎是一款面向企业的云计算与AI服务平台。

讯飞智作

讯飞智作是一款AI视频创作工具,AI文本配音工具,数字人课程、营销视频制作。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

切问学术

切问学术是一款AI论文写作工具,复旦大学NLP团队推出的AI学术智能体。

Atoms
Atoms Hot

Atoms是一款AI智能体工具,第一支自动构建真实业务的 AI 团队。

AionClaw
AionClaw Hot

AionClaw是一款面向办公、创作和编程任务的AI桌面智能体。

音述AI
音述AI Hot

一款AI音频处理工具,主要用于音述AI是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区,适合需要提升相关任务效率的用户。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

相关专题

更多
java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

6044

2023.10.24

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

6044

2023.10.24

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2884

2024.02.23

php三元运算符用法
php三元运算符用法

本专题整合了php三元运算符相关教程,阅读专题下面的文章了解更多详细内容。

1752

2025.10.17

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

2178

2023.09.20

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

1718

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

2644

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

6044

2023.10.24

OpenJDK JVM参数怎么调优
OpenJDK JVM参数怎么调优

Java应用运行缓慢,如何调整JVM参数?本专题介绍OpenJDK JVM性能调优的基本流程,涵盖性能指标确认、运行数据采集、内存配置和垃圾回收分析,帮助你先定位瓶颈,再通过对照测试验证效果,避免直接套用参数而影响应用稳定性。

0

2026.10.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn