【C++ 入门】类和对象(中)

大家好!上一篇我们学会了对象的 "出生"(构造函数)和 "死亡"(析构函数),今天咱们聚焦对象的 "日常互动"------ 怎么用已有对象 "克隆" 新对象?怎么让自定义类型像int一样用===做运算?这篇就带大家吃透拷贝构造函数运算符重载,全程用代码 + 图解拆解,新手也能轻松上手~

一、拷贝构造函数:用 "旧对象" 造 "新对象"

先想个场景:你已经有了一个日期对象d1(2024, 6, 1),现在想创建一个和d1完全一样的d2,总不能再写一遍2024,6,1吧?C++ 里这种 "用已存在对象初始化新对象" 的操作,就需要拷贝构造函数来帮我们自动完成。

1.1 先搞懂:什么时候会触发拷贝构造?

拷贝构造只负责 "新对象的初始化",常见场景有 3 种,记牢这 3 个例子就够了:

  • 直接用旧对象初始化新对象:Date d2 = d1;Date d2(d1);(最直观的场景)
  • 函数参数是类类型(传值调用):比如void Func(Date d) {},调用Func(d1)时,会拷贝d1给参数d
  • 函数返回值是类类型(传值返回):比如Date Func() { Date d; return d; },返回时会拷贝d给临时对象

1.2 拷贝构造的 "致命坑":参数必须是引用!

这是初学者最容易踩的雷!拷贝构造的参数必须是 "类类型的引用" (比如const Date& d),如果用传值(Date d),编译器会直接报错,因为会触发无穷递归

咱们用代码拆解这个坑:

错误示例:参数用传值,触发死循环

cpp

复制代码
class Date {
public:
    Date(int year=2000, int month=1, int day=1) { // 普通构造
        _year = year; _month = month; _day = day;
    }
    // 错误:参数d是传值!
    Date(Date d) { 
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
private:
    int _year, _month, _day;
};

int main() {
    Date d1(2024,6,1);
    Date d2(d1); // 试图用d1初始化d2,触发拷贝构造
    return 0;
}

为什么会递归? 调用Date d2(d1)时,需要把d1传给拷贝构造的参数d------ 而 "传值参数" 本身需要先拷贝d1生成d,这又要调用拷贝构造函数...... 相当于 "要调用函数,得先完成参数拷贝;要完成拷贝,又得调用函数",陷入死循环!

正确示例:参数用const 引用

cpp

复制代码
class Date {
public:
    Date(int year=2000, int month=1, int day=1) { // 普通构造
        _year = year; _month = month; _day = day;
        cout << "普通构造调用" << endl;
    }
    // 正确:参数是const Date&,避免额外拷贝
    Date(const Date& d) { 
        _year = d._year;   // 直接访问d的成员(引用无拷贝)
        _month = d._month;
        _day = d._day;
        cout << "拷贝构造调用" << endl;
    }
private:
    int _year, _month, _day;
};

int main() {
    Date d1(2024,6,1); // 输出:普通构造调用
    Date d2(d1);       // 输出:拷贝构造调用(用d1初始化d2)
    return 0;
}

const的原因 :防止在拷贝构造里修改原对象(比如误写d._year = 2025),符合 "拷贝不修改原对象" 的逻辑。

1.3 浅拷贝的 "隐形炸弹" 与深拷贝的救赎

如果我们没写拷贝构造,编译器会自动生成一个 "默认拷贝构造",但它有个大问题:只做浅拷贝(字节级拷贝)

什么是浅拷贝?

默认拷贝构造对成员的处理规则:

  • 内置类型(int、指针等):直接拷贝值(比如把d1._year复制给d2._year,指针也只复制地址);
  • 自定义类型(比如其他类对象):调用该类型的拷贝构造。

Date这种无资源的类,浅拷贝够用;但对Stack这种需要申请堆内存的类,浅拷贝会直接导致程序崩溃!

实战:浅拷贝导致的 "双重析构"

cpp

复制代码
class Stack {
public:
    // 构造函数:申请堆内存存数据
    Stack(int capacity=4) {
        _arr = new int[capacity]; // 堆内存地址存在_arr里
        _top = 0;
        _capacity = capacity;
        cout << "Stack构造:" << _arr << endl;
    }

    // 析构函数:释放堆内存(自己写的,因为有资源申请)
    ~Stack() {
        delete[] _arr; // 释放_arr指向的堆内存
        _arr = nullptr;
        cout << "Stack析构:" << _arr << endl;
    }

private:
    int* _arr;     // 指向堆内存的指针(关键!)
    int _top;      // 栈顶
    int _capacity; // 容量
};

int main() {
    Stack st1;     // 构造st1,_arr指向堆地址A
    Stack st2(st1); // 用默认拷贝构造,st2._arr也指向A
    return 0;      // 析构时:先析st2(释放A),再析st1(再释放A→崩溃!)
}

运行结果 :程序崩溃(提示 "双重释放内存")。内存图解 (建议配图):![图 1:浅拷贝的内存问题](示意图内容:st1 和 st2 的_arr指针都指向同一块堆内存(地址 A),析构时两次释放地址 A,触发内存错误)

解决:自己写深拷贝构造

深拷贝的核心:不仅拷贝成员的值,还要给指针成员重新申请堆内存,拷贝原内存的数据

修改Stack的拷贝构造:

cpp

复制代码
class Stack {
public:
    Stack(int capacity=4) { // 普通构造
        _arr = new int[capacity];
        _top = 0;
        _capacity = capacity;
        cout << "Stack构造:" << _arr << endl;
    }

    // 自己写的深拷贝构造
    Stack(const Stack& st) {
        // 1. 给st2的_arr重新申请堆内存(和st1容量一样)
        _arr = new int[st._capacity];
        // 2. 把st1._arr里的数据拷贝到新内存
        memcpy(_arr, st._arr, sizeof(int) * st._top);
        // 3. 拷贝其他成员
        _top = st._top;
        _capacity = st._capacity;
        cout << "Stack拷贝构造:" << _arr << endl;
    }

    ~Stack() { // 析构
        delete[] _arr;
        _arr = nullptr;
        cout << "Stack析构:" << _arr << endl;
    }

private:
    int* _arr;
    int _top;
    int _capacity;
};

int main() {
    Stack st1;     // 输出:Stack构造:0x00123456(假设地址)
    Stack st2(st1); // 输出:Stack拷贝构造:0x00789abc(新地址)
    return 0;      // 析构:先释st2的0x00789abc,再释st1的0x00123456→正常!
}

内存图解 (建议配图):![图 2:深拷贝的正确内存分布](示意图内容:st1 的_arr指向地址 A,st2 的_arr指向新申请的地址 B,且 B 里拷贝了 A 的数据,析构时各自释放自己的地址,无冲突)

1.4 小技巧:拷贝构造的 "伴生关系"

文档里提到一个超实用的规律:如果一个类需要自己写析构函数(因为有资源申请),那它一定需要自己写拷贝构造函数。反之,如果析构用默认的,拷贝构造也大概率能用默认的。

二、运算符重载:让自定义类型 "支持运算"

C 语言里,+==这些运算符只能用在intdouble等内置类型上,想比较两个Date对象(比如d1 == d2),只能写个IsEqual(d1,d2)函数,很麻烦。

C++ 的运算符重载解决了这个问题:让自定义类型也能像内置类型一样用运算符!

2.1 运算符重载的 "黄金规则"

先记住这些核心规则,避免踩坑:

  1. 函数名固定operator + 运算符(比如operator==operator+);
  2. 参数个数:和运算对象数量一致(一元运算符 1 个参数,二元运算符 2 个参数);
  3. 成员函数特殊处理 :如果运算符重载是类的成员函数,第一个参数会被默认作为this指针(指向左侧运算对象),所以参数个数会少 1 个;
  4. 不能重载的运算符 :5 个特殊运算符绝对不能重载(记牢!选择题常考):.*(成员指针访问)、::(作用域解析)、sizeof(大小计算)、?:(三目运算符)、.(成员访问);
  5. 必须有类类型参数 :不能重载内置类型的运算(比如int operator+(int a,int b)是不允许的)。

2.2 实战:给 Date 类重载==运算符

需求:判断两个Date对象的年、月、日是否完全相同。

实现:作为成员函数

cpp

复制代码
class Date {
public:
    Date(int year=2000, int month=1, int day=1) {
        _year = year; _month = month; _day = day;
    }

    // 重载==运算符(成员函数)
    bool operator==(const Date& d) {
        // this指向左侧对象(比如d1==d2中,this就是&d1)
        return _year == d._year 
            && _month == d._month 
            && _day == d._day;
    }

private:
    int _year, _month, _day;
};

int main() {
    Date d1(2024,6,1);
    Date d2(2024,6,1);
    Date d3(2024,6,2);

    // 直接用==,编译器会转成d1.operator==(d2)
    cout << (d1 == d2) << endl; // 输出1(真)
    cout << (d1 == d3) << endl; // 输出0(假)
    return 0;
}

调用逻辑图解 (建议配图):![图 3:== 运算符重载的调用逻辑](示意图内容:d1 == d2被编译器解析为d1.operator==(d2)this指针指向d1,参数dd2的引用,对比两者的成员变量)

三、赋值运算符重载:"旧对象" 给 "旧对象" 赋值

很多人会把 "拷贝构造" 和 "赋值重载" 搞混,其实核心区别就一个:

  • 拷贝构造:新对象初始化 (用旧对象造新对象,比如Date d2 = d1);
  • 赋值重载:两个已存在对象赋值 (比如d1d2都已创建,执行d1 = d2)。

3.1 赋值重载的 "必记特点"

赋值运算符重载是默认成员函数(没写的话编译器会自动生成),但它有 3 个关键特点:

  1. 必须是成员函数:这是 C++ 规定的,不能写成全局函数;
  2. 参数建议用const 引用:避免传值导致的拷贝(和拷贝构造一样);
  3. 返回值建议用类类型引用 :支持 "连续赋值"(比如d3 = d2 = d1)。

3.2 默认赋值重载的 "浅拷贝坑"

和默认拷贝构造一样,默认赋值重载也做浅拷贝。对Stack这种有资源的类,同样会导致双重析构。

错误示例:默认赋值重载

cpp

复制代码
int main() {
    Stack st1(4); // 构造st1,_arr指向地址A
    Stack st2(8); // 构造st2,_arr指向地址B
    st2 = st1;    // 用默认赋值重载:st2._arr = st1._arr(指向A)
    return 0;     // 析构:st2先释A,st1再释A→崩溃!且地址B的内存泄漏了!
}

额外问题 :st2 原来的_arr指向地址 B,赋值后被覆盖,再也无法释放 B→内存泄漏!

解决:自己写深拷贝赋值重载

实现要点:

  1. 先判断 "是否给自己赋值"(比如st1 = st1),避免不必要的操作;
  2. 释放当前对象的旧资源(避免内存泄漏);
  3. 深拷贝新资源;
  4. 返回*this(支持连续赋值)。

cpp

复制代码
class Stack {
public:
    // 省略构造、拷贝构造、析构...

    // 自己写的深拷贝赋值重载
    Stack& operator=(const Stack& st) {
        // 1. 防止给自己赋值(比如st1 = st1)
        if (this != &st) {
            // 2. 释放当前对象的旧资源(避免泄漏)
            delete[] _arr;
            // 3. 深拷贝:申请新资源+拷贝数据
            _arr = new int[st._capacity];
            memcpy(_arr, st._arr, sizeof(int) * st._top);
            _top = st._top;
            _capacity = st._capacity;
        }
        // 4. 返回*this,支持连续赋值(比如st3 = st2 = st1)
        return *this;
    }

private:
    int* _arr;
    int _top;
    int _capacity;
};

int main() {
    Stack st1(4);
    Stack st2(8);
    Stack st3(10);
    st3 = st2 = st1; // 连续赋值:先st2=st1,再st3=st2(返回的st2)
    return 0;        // 析构正常,无泄漏!
}

3.3 又一个小技巧

和拷贝构造类似:自己写析构的类,一定要自己写赋值运算符重载!三者(析构、拷贝构造、赋值重载)往往是 "捆绑出现" 的。

总结:核心知识点速记

  1. 拷贝构造 :用旧对象初始化新对象,参数必须是const 引用,有资源要深拷贝;
  2. 运算符重载 :函数名operator+运算符,成员函数少一个参数(this),5 个运算符不能重载;
  3. 赋值重载 :处理两个已存在对象的赋值,要先释旧资源、再深拷贝,返回*this支持连续赋值;
  4. 黄金规律:自己写析构 → 必须自己写拷贝构造 + 赋值重载。

下一篇我们会讲 "初始化列表" 和 "static 成员",帮大家进一步完善类的知识体系~ 现在不妨动手敲一敲今天的Stack代码,感受一下浅拷贝和深拷贝的区别,有问题欢迎在评论区交流!

相关推荐
雪夜行人2 小时前
cobra命令行工具
开发语言·golang
王家视频教程图书馆2 小时前
C#使用 tcp socket控制台程序和unity客户端通信
开发语言·tcp/ip·c#
小兜全糖(xdqt)2 小时前
python cobnfluent kafka transaction事务
开发语言·python·kafka
新鲜势力呀2 小时前
低成本实现轻量级 Claude 风格对话交互 ——PHP 极简版开发详解
开发语言·php·交互
MyBFuture2 小时前
C#抽象类与重载重写实战
开发语言·c#·visual studio
墨有6662 小时前
【C++ 入门】类和对象下
c++
悟能不能悟2 小时前
如何部署wiki.js
开发语言·javascript·ecmascript
sinat_384241092 小时前
从零开始打造一个 Android 音乐播放器(Kotlin + Jetpack Compose)
android·开发语言·kotlin
zore_c2 小时前
【数据结构】二叉树初阶——超详解!!!(包含二叉树的实现)
c语言·开发语言·数据结构·经验分享·笔记·算法·链表