跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C++ 类和对象:拷贝构造与赋值运算符重载详解

综述由AI生成本文深入解析 C++ 中对象复制的核心机制,涵盖拷贝构造函数与赋值运算符重载。重点讲解了拷贝构造的参数要求(引用传参避免递归)、浅拷贝与深拷贝的区别及风险(如资源重复释放),以及赋值运算符中的自赋值检查。通过栈(Stack)和日期(Date)类的实例代码,演示了如何正确实现深拷贝以确保资源安全,并对比了传值返回与传引用返回的差异。掌握这些内容有助于编写更安全、高效的 C++ 代码,深入理解对象生命周期管理。

灵魂伴侣发布于 2026/3/28更新于 2026/6/820 浏览
C++ 类和对象:拷贝构造与赋值运算符重载详解

引言

在 C++ 面向对象编程中,对象的复制操作无处不在。无论是函数传参、返回值传递,还是对象间的赋值,都需要精确控制数据的复制行为。C++ 通过拷贝构造函数和赋值运算符重载两套机制,为开发者提供了对象复制的完整解决方案。本文将从基础概念出发,深入解析这两种复制机制的实现细节与应用技巧。

一、拷贝构造函数

如果一个构造函数的第一个参数是自身类型的引用,且其他所有参数都有默认值(如果有),就叫做拷贝构造,它是特殊的构造函数。

基本形式

#include <iostream>
using namespace std;

class Example {
public:
    Example(const Example& d) {
        // ...
    }
};

1.1 解析:拷贝构造特点

这里有一些关键规则需要掌握:

  1. 拷贝构造函数是构造函数的一个重载。
  2. 拷贝构造函数的第一个参数必须是自身类类型的引用:类名& 或 const 类名&(建议加 const)。如果使用传值的方式,逻辑上会引发无穷递归调用。
  3. 拷贝构造函数可以有多个参数,第一个为引用,其他必须有缺省值。
  4. C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
  5. 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。默认生成的拷贝构造对内置类型成员变量会完成值拷贝(浅拷贝),对自定义类型成员变量会调用它的拷贝构造。
  6. 类似 Date 类,成员变量全是内置类型且不指向资源,编译器默认生成的拷贝构造就够了。类似 Stack 类,虽然也都是内置类型,但是指针指向资源,那么编译器默认生成的浅拷贝就不太够,需要显式定义深拷贝。再对于 MyQueue 类,自定义类型 Stack 变量成员就直接调用它的拷贝构造。

【技巧】:如果一个类显式实现了析构并释放资源,那么他就需要显式定义深拷贝,否则就不需要。

  1. 传值返回会产生一个临时对象来调用拷贝构造;而传引用返回,返回的是对象的别名,不会产生拷贝,但是返回的对象为一个当前函数局部域的局部对象,函数结束就会销毁,这时传引用返回是有问题的,类似于野指针。(传引用返回会减少拷贝,但是要确保返回对象在函数结束时不会被销毁)

解释特点第 2 条:

当拷贝构造函数传值传参时,函数的形参是实参拷贝出来的新对象,要调用拷贝构造,但是拷贝构造函数也是传值传参就又要调用拷贝构造,这样无限循环下去……

其次,在引用传参最好加上 const,因为将对象传过来,也不会将对象进行改变操作,那么 const 就方便了传参(权限缩小)。当然,这时候传 const 对象也是可以的(权限平移)。

特点第 2 条拓展:既然要引用传参,那么指针可以吗?

先说,传指针是可以的,但是函数就变成普通的构造函数,不是拷贝构造函数。

#include <iostream>
using namespace std;

  {
:
    
    ( day = ,  month = ,  year = ) {
        _day = day;
        _month = month;
        _year = year;
    }
    
    (Date* d) {
        _day = d->_day;
        _month = d->_month;
        _year = d->_year;
    }
    {
        cout << _year <<  << _month <<  << _day << endl;
    }
:
     _day;
     _month;
     _year;
};

{
    
    Date d1;
    d();
    
    ;
    d();
     ;
}
class
Date
public
// 构造函数:全缺省
Date
int
8
int
1
int
2026
// 指针传参
Date
void Print()
'/'
'/'
private
int
int
int
int main()
// 调用构造函数初始化 d1
1.
Print
// 传地址
Date d2(&d1)
2.
Print
return
0

解释特点第 5 条:

拷贝构造函数就和构造、析构有点不同。它会对内置类型的成员变量进行处理。类似 Date 类这样全是内置类型的变量,编译器默认生成的就够用;对于复杂结构的类 Stack,就要自定义深拷贝;对于 MyQueue 这样的类,不显式定义拷贝构造,编译器就会调用成员变量对应类的拷贝构造。

解释特点第 6 条: 通过实现栈来观察

  • 有指向的资源,浅拷贝的后果:
    • 一个对象改变会影响另一个对象;
    • 析构时,同一块空间会释放两次空间;
#include <cstdlib>
#include <cstring>

typedef int STDataType;

class Stack {
public:
    Stack(int n = 4) {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a) {
            perror("malloc 申请空间失败");
            return;
        }
        _capacity = n;
        _top = 0;
    }
    // Stack st2(st1);
    Stack(const Stack& s) {
        _a = s._a;
        _capacity = s._capacity;
        _top = s._top;
    }
    void Push(STDataType x) {
        if (_top == _capacity) {
            int newcapacity = _capacity * 2;
            STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
            if (tmp == NULL) {
                perror("realloc fail");
                return;
            }
            _a = tmp;
            _capacity = newcapacity;
        }
        _a[_top++] = x;
    }
    ~Stack() {
        cout << "~Stack()" << endl;
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }
private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

int main() {
    Stack s1;
    s1.Push(1);
    s1.Push(2);
    s1.Push(3);
    s1.Push(4);
    Stack s2(s1);
    return 0;
}

(会产生错误,通常是内存相关的问题。)

这样浅拷贝会使两个对象的指针变量都指向同一块空间,最后的两次析构就导致第二次析构对已经释放完的空间再次释放,发生错误。

  • 有指向的资源,自定义深拷贝:(先简单了解)
    • 不仅仅对成员拷贝,还对指向的资源空间数据进行处理。(开空间)
Stack(const Stack& s) {
    _a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);
    if (_a == NULL) {
        perror("realloc fail");
        return;
    }
    memcpy(_a, s._a, s._top * sizeof(STDataType));
    _capacity = s._capacity;
    _top = s._top;
}

调试程序发现:不指向同一空间。

解释特点第 7 条:

在上面栈的基础上

int& func1() {
    int ret = 1;
    return ret; // 返回的是 ret 的别名
}

Stack& func2() {
    Stack st;
    return st;
}

int main() {
    int ret1 = func1(); // ret 在函数结束时就销毁了,所以这里存在错误
    cout << ret1 << '\n'; // 可能是 1 或者随机值
    Stack ret2 = func2(); // 调拷贝构造,但是 st 不存在
    return 0;
}

根据特点 7,函数传值返回是会调用拷贝构造的,但是传引用返回不会。对于 st 这里,函数就是进行析构(成员函数),那么在通过返回的别名来访问 st 肯定是错的。

【所以,在传引用返回是一定要注意返回对象是否还存在!】

1.2 关键:拷贝构造的调用

  • 用一个对象初始化另一个同类的对象(在创建的同时初始化)

基本形式:

Example a;
Example b(a); // 调用拷贝构造函数
Example c = a; // 调用拷贝构造函数
#include <iostream>
using namespace std;

class Date {
public:
    // 构造函数:全缺省
    Date(int day = 8, int month = 1, int year = 2026) {
        _day = day;
        _month = month;
        _year = year;
    }
    // 拷贝构造函数
    Date(const Date& d) {
        _day = d._day;
        _month = d._month;
        _year = d._year;
    }
    void Print() {
        cout << _year << '/' << _month << '/' << _day << endl;
    }
private:
    int _day;
    int _month;
    int _year;
};

int main() {
    // 调用构造函数初始化 d1
    Date d1;
    // 不要写成 Date d1();
    d1.Print();
    // 创建对象的同时,调用拷贝构造进行初始化
    Date d2(d1);
    d2.Print();
    return 0;
}
  • 函数参数按值传递该类的对象(传值传参)

基本形式:

void func(Example obj) {
    // ...
}
Example a;
func(a); // 调用拷贝构造函数

注意: 调用函数,形参是用实参拷贝构造出来的新对象,将实参传递就符合调用拷贝构造的规则。(函数形参也是一个需要被创建的对象。)

#include <iostream>
using namespace std;

class Date {
public:
    // 构造函数:全缺省
    Date(int day = 8, int month = 1, int year = 2026) {
        _day = day;
        _month = month;
        _year = year;
    }
    // 拷贝构造函数
    Date(const Date& d) {
        _day = d._day;
        _month = d._month;
        _year = d._year;
    }
    void Print() {
        cout << _year << '/' << _month << '/' << _day << endl;
    }
private:
    int _day;
    int _month;
    int _year;
};

void func(Date d) {
    d.Print();
}

int main() {
    // 调用构造函数初始化 d1
    Date d1;
    d1.Print();
    func(d1); // 调用拷贝函数
    return 0;
}

二、赋值运算符重载

2.1 铺垫:运算符重载特点

  1. 当运算符被用于类类型的对象时,C++ 允许通过运算符重载的形式指定新的含义。C++ 规定,类类型对象使用运算符时,需要转换成调用相应的运算符重载,没有会报错;
  2. 运算符重载式具有特殊名字的函数,名称由 operator 和后面的运算符构成,与普通函数一样,具有返回值、返回类型、参数、函数体等;
  3. 重载运算符函数的参数个数和运算符的操作对象数量相同:一元运算符一个参数,二元运算符两个参数。对于二元:左侧运算对象传给第一个参数,右侧传给第二个参数;
  4. 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传隐式的 this 指针,因此运算符重载作为成员函数时,参数比运算对象少一个;
  5. 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致;
  6. 不能通过连接语法中没有的符号来创建新的操作符:比如 operator@;
  7. .*、::、sizeof、? :、. 以上五个运算符不能重载;
  8. 重载运算符至少有一个类类型的参数,不能通过运算符重载改变内置类型对象的含义,如:operator+(int x, int y);
  9. 一个类需要哪些运算符重载。是看那些有实际意义,比如:Date 类的 operator- 有意义,operator* 没有意义;
  10. 重载 ++ 运算符时,有前置 ++ 和后置 ++,运算符重载函数名都是 operator++,无法很好的区分。C++ 规定,后置 ++ 重载时,增加一个 int 形参,跟前置 ++ 构成函数重载,方便区分;
  11. 重载 << 和 >> 时,需要重载为全局函数,因为重载为成员函数,this 指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了对象 <<cout,不符合使用习惯和可读性。重载为全局函数把 ostream/istream 放到第⼀个形参位置就可以了,第二个形参位置当类类型对象。
2.1.1 核心:理解运算符重载

【搭配举例】:

#include <iostream>
using namespace std;

class Date {
public:
    // 构造函数
    Date(int day = 8, int month = 1, int year = 2026) {
        _day = day;
        _month = month;
        _year = year;
    }
    // 如果运算符重载载在类外实现,解决变量私有化一个方法:
    // int Getyear(){return _year;}
    // 或者在类内定义
    // 比较 Date 类对象是否相等
    bool operator==(const Date& d) {
        // this 占第一个,显式第一个参数为左侧运算对象
        // 第 4 条:默认第一个参数为 this 指针
        return _day == d._day && _month == d._month && _year == d._year;
    }
private:
    int _day;
    int _month;
    int _year;
};

// 类外定义
// bool operator==(const Date& x1, const Date& x2) // 第 2 条
// {
//     return x1.day == x2._day && x1._month == x2._month &&
//            x1.year == x2._year;
// }

int main() {
    Date d1(1, 1, 1);
    Date d2;
    cout << (d1 == d2) << endl; // 第 3 条
}

【额外注意】:

  • 当在类外定义运算符重载时,因为成员变量私有无法访问,可采取:
    • 将成员变量访问改为公有,但是太极端危险;
    • 在类内定义 int Getyear(); 之类函数,获取成员变量;
    • 直接在类内定义运算符重载成为成员函数(推荐),但是要注意参数的改变(第 4 条)。
    • 友元函数(后面会有)。
  • 在最后输出结构,注意优先级。<< / >> 优先级较高,所以 ... == ... 要加括号。

【介绍 .* 运算符】:C++ 不常用,了解

#include <iostream>
using namespace std;

void func1() {
    cout << "void func()" << endl;
}

class A {
public:
    void func2() {
        cout << "A::func()" << endl;
    }
};

int main() {
    // 普通函数指针
    void (*pf1)() = func1;
    (*pf1)();
    // A 类型成员函数的指针
    void (A::* pf2)() = &A::func2;
    A aa;
    (aa.*pf2)(); // 这里就是使用的 .*
    // (aa.*pf2)(&aa); 错误,this 指针不能显式出现参数。
    return 0;
}

2.2 进阶:赋值运算符重载特点

赋值运算符重载是一个默认成员函数,用于完成两个已存在的对象直接的拷贝复制,要和拷贝构造区分开。

  1. 赋值运算复是一个运算符重载,C++ 规定必须为成员函数。参数建议写成 const 当前类类型引用传参,当然传值传参会调用拷贝构造;
  2. 有返回值,建议写成当前类类型引用,传引用返回可以提高效率,有返回值就可以连续赋值;
  3. 当没有显式实现,编译器会默认生成,其行为和默认生成的拷贝构造类似,对内置类型成员变量会先完成值拷贝,对自定义类型会调用相应的赋值重载函数;
  4. 类似 Date 类,为内置类型成员且不指向任何资源,编译器默认生成的浅拷贝就够了。但是类似 Stack 类,有指向的资源,就需要自定义深拷贝。

(这里和拷贝构造类似)

【技巧】:如果一个类显式实现了析构并释放资源,那么他就需要显式定义深拷贝,否则就不需要。

2.2 核心:理解赋值运算符重载
#include <iostream>
using namespace std;

class Date {
public:
    // 构造函数
    Date(int day = 8, int month = 1, int year = 2026) {
        _day = day;
        _month = month;
        _year = year;
    }
    // 拷贝构造
    Date(const Date& d) {
        _day = d._day;
        _month = d._month;
        _year = d._year;
    }
    // 赋值重载
    // d1 = d2
    Date& operator=(const Date& d) {
        if (this != &d) {
            _day = d._day;
            _month = d._month;
            _year = d._year;
        }
        return *this; // 返回 d1 别名,不拷贝
    }
    void Print() {
        cout << _year << '/' << _month << '/' << _day << endl;
    }
private:
    int _day;
    int _month;
    int _year;
};

int main() {
    Date d1(1, 1, 1);
    Date d2(d1); // 拷贝构造
    Date d3 = d1; // 拷贝构造
    d2.Print();
    d3.Print();
    Date d4;
    Date d5;
    d5 = d4 = d1; // 赋值重载
    d4.Print();
    return 0;
}

【说明】:Date& operator=(const Date& d); 为什么可以传引用返回?

在拷贝构造部分,有过说明'传值返回会发生拷贝',但是 this 不是这个函数的局部对象,不会销毁,额外的拷贝就很麻烦,没必要。

总结

拷贝构造函数与赋值运算符重载构成了 C++ 对象复制机制的核心支柱。它们分别负责对象初始化和对象赋值两种不同场景的复制需求。掌握这些复制控制机制,不仅能写出更安全的代码,更能深入理解 C++ 对象生命周期的管理哲学。这是从 C++ 使用者迈向 C++ 设计者的重要一步。

目录

  1. 引言
  2. 一、拷贝构造函数
  3. 基本形式
  4. 1.1 解析:拷贝构造特点
  5. 1.2 关键:拷贝构造的调用
  6. 二、赋值运算符重载
  7. 2.1 铺垫:运算符重载特点
  8. 2.1.1 核心:理解运算符重载
  9. 2.2 进阶:赋值运算符重载特点
  10. 2.2 核心:理解赋值运算符重载
  11. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 大模型产品经理转行指南:核心素质与学习路径
  • Openclaw 开源仿生机械爪:原理、应用与生态解析
  • Python 词云库 wordcloud 中文显示问题详解
  • 使用 Go 语言构建命令行 AI 对话客户端:从环境部署到核心实现
  • OpenClaw 配置飞书机器人教程
  • 利用 AI 预测模型优化数据中心能耗的实施方案
  • AI 时代内存需求激增:能源、隐私与绿色技术深度解析
  • ONNX Runtime for Java 实战:模型部署与性能优化指南
  • 从零开始训练大模型的技术实践与讨论
  • Stable Diffusion v4.10 与 ComfyUI 整合包安装指南
  • ThinkPHP 5 在 Windows IIS 上的部署与 PHP 版本兼容性配置
  • 企业级 AI 四层架构:RAG、AI Agents、MCP 与 A2A 协同体系
  • Spring Boot Redis 存储对象报 ClassCastException 问题排查与解决
  • 通义千问插件在 IDEA 中的 Java 开发实战应用
  • Flutter 组件 tavily_dart 适配鸿蒙系统:AI 聚合搜索与语义降噪方案
  • 企业级招聘数据采集实战:基于 Bright Data AI Studio 的自动化方案
  • OpenClaw WebUI 空白页故障排查与修复
  • 文心一言开源模型部署与性能测评指南
  • 电力电网巡检计算机视觉数据集:3729 张图像与深度学习实战指南
  • AI Agent 技术栈解析:Skills、MCP、RAG 与 Memory 核心架构

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online