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

C++ 类与对象进阶:构造函数、拷贝构造与操作符重载

C++ 类与对象进阶涉及默认成员函数的核心机制,包括构造函数初始化、拷贝构造的深浅拷贝区别、析构函数资源释放及操作符重载语法。文章详细解析了编译器生成规则、内置与自定义类型的处理差异、内存泄漏风险及三法则原则。通过代码示例演示了对象生命周期管理、赋值运算符重载及流插入提取的实现方法,旨在帮助开发者理解 C++ 对象模型并编写安全高效的代码。

ArchDesign发布于 2026/3/26更新于 2026/7/2443 浏览
C++ 类与对象进阶:构造函数、拷贝构造与操作符重载

类的默认成员函数

定义: 默认成员函数是用户未显式实现,由编译器自动生成的成员函数。

在一个类中,如果不显式编写,编译器会默认生成以下 4 个重要的默认成员函数:

文章配图

默认成员函数是 C++ 中重要且复杂的知识点,需要从两个维度进行掌握:

  1. 了解编译器自动生成的默认函数行为及其适用性
  2. 当编译器自动生成的默认函数无法满足需求时,掌握自定义实现的方法

一、构造函数

什么是构造函数?

构造函数用于在创建对象时初始化对象的状态。对象开辟空间是栈帧的任务,构造函数负责完成初始化。

1.1 构造函数的核心语法

#include <iostream>
using namespace std;

class Student {
public:
    int age;
    // 构造函数的语法:类名 (形式参数)
    // 1. 无需返回值
    // 2. 名称与类名相同
    Student() {
        age = 18; // 设置默认值
        cout << "调用了默认构造函数" << endl;
    }
};

int main() {
    Student s1; // 3. 自动调用 Student(),age 变为 18
    // s1.Student(); // error: 不需要手动调用,在声明对象的时候自动调用
    cout << s1.age << endl;
    return 0;
}

构造函数的语法要点:

  • 类名 (形式参数)
  • 无需返回值
  • 名称与类名必须相同
  • 形式参数可有可无,根据需求而定

1.2 构造函数的常见类型

1.2.1 无参构造函数

不带任何参数的构造函数。

class  {
:
    
    () {
        _year = ;
        _month = ;
        _day = ;
    }
:
     _year;
     _month;
     _day;
};
Date
public
// 1. 无参构造函数
Date
1
1
1
private
int
int
int
1.2.2 带参数的构造函数

允许在创建对象时传入特定的值。

class Date {
public:
    // 2. 带参构造函数
    Date(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};
1.2.3 全缺省构造函数

在未传参时设置默认的初始值,且允许在创建对象时传入特定的值。

class Date {
public:
    // 3. 全缺省构造函数
    Date(int year = 1, int month = 1, int day = 1) {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};

注意: 全缺省构造函数和无参构造函数不能同时出现,因为当创建一个不带参数的对象时,编译器会产生歧义。

1.3 默认构造函数

默认构造函数包括以下三种类型:

  1. 无参构造函数
  2. 全缺省构造函数
  3. 编译器自动生成的默认构造函数(当用户未定义任何构造函数时)

常见误区:

  1. 无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。
  2. 很多读者会认为默认构造函数是编译器默认生成的才叫默认,实际上无参构造函数、全缺省构造函数也是默认构造。
  3. 总结来说,不传实参就可以调用的构造函数就叫默认构造函数。

1.4 编译器默认生成的构造函数

1.4.1 生成的条件

编译器生成这个构造函数有且仅有一个条件:你的类中没有定义任何默认构造函数。

遵循'编译器默认行为原则':

  • 如果未写构造函数,编译器会生成一个默认无参的构造函数。
  • 一旦自己写了任意一个默认构造函数(哪怕是带参数的),编译器就会停止生成默认构造函数。

代码示例:

class A { 
    // 什么都没写 -> 编译器生成 A()
}; 

class B {
public:
    B(int x) { cout << x << endl; } 
    // 你写了一个带参构造 -> 编译器不再生成 B()
}; 

int main() {
    B b; // 错误写法,此时类中只含有一个带实参的构造函数,必须手动添上实参
    B b(1); // 正确写法
    return 0;
}
1.4.2 执行的逻辑

C++ 把类型分成内置类型(基本类型)和自定义类型。

  • 内置类型: C++ 提供的原生数据类型,如 int, char, double, 指针等。
  • 自定义类型: 使用 class/struct 等关键字自己定义的类型。

编译器生成的默认构造函数对待不同类型的成员变量态度截然不同:

A. 对待'自定义类型' —— 负责 如果类里包含其他的类对象(比如 string, vector 或另一个 class),编译器生成的构造函数会自动调用这些成员的默认构造函数。

B. 对待'内置类型' —— 不处理 对于基础数据类型,编译器默认生成的构造函数什么都不做,这意味着这些变量的内存里原来是什么数据,现在还是什么(可能是垃圾数据)。

代码示例 A:通过双栈实现队列

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;
    }
    // ...
private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

// 两个 Stack 实现队列
class MyQueue {
public:
    // 编译器默认生成 MyQueue 的构造函数调用了 Stack 的构造,完成了两个成员的初始化
private:
    Stack pushst;
    Stack popst;
};

int main() {
    MyQueue mq; // 编译器生成的构造函数,调用了 Stack 的默认构造函数。
    return 0;
}

如果在 Stack 类中删除了显式定义的构造函数,将会发生连带反应:

  1. 编译阶段:代码依然可以编译通过。原因:当你没有定义任何构造函数时,编译器会按照 C++ 标准自动为你生成一个隐式的默认构造函数。
  2. 运行阶段:极度危险(未定义行为)。编译器自动生成的默认构造函数对内置类型成员变量(如 int、指针、size_t)不做任何初始化处理。此时 _a (指针) 变成了野指针,存储的是内存中的随机值;_capacity 和 _top 也是随机值。

代码示例 B:在日期类中未显示实现构造函数

class Date {
public:
    void Print() {
        cout << _year << "/" << _month << "/" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main() {
    Date d1; // 通过编译器生成的构造函数进行初始化,初始化了垃圾值
    d1.Print(); // 输出垃圾值
    return 0;
}

结论: 永远不要信任编译器默认生成的构造函数来处理内置类型如 int、bool 或指针等。

1.5 构造函数小结

  1. 名称与类名相同。
  2. 没有返回值:连 void 都不用写。
  3. 自动调用:不需要手动调用,它会在对象被创建的那一刻自动执行。
  4. 可以重载:一个类可以有多个构造函数,只要参数列表不同即可。
  5. 如果类中没有显式定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
  6. 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有一个存在,不能同时存在。

二、拷贝构造函数

什么是拷贝构造函数?

简单来说,它的作用是'克隆':用一个已存在的对象来初始化一个新对象。

2.1 拷贝构造的核心语法

它的标准写法非常固定:

class ClassName {
public:
    // 语法:类名 (const 类名& 别名)
    ClassName(const ClassName& other) {
        // 在这里做拷贝工作
    }
};

关键点:

  1. 引用 & 是必须的:如果不传引用而是传值,传参过程本身又要拷贝,就会无限递归调用拷贝构造函数,导致栈溢出。
  2. const 通常要加:保证在拷贝过程中,不会意外修改那个'原件'。

为什么拷贝构造函数的第一个参数必须要加引用?

  1. 前置知识:按值传递的代价 在 C++ 中,当通过传值传参的方式调用函数时,编译器需要将实参对象复制给形参对象,此时复制的过程就会调用拷贝构造函数。

  2. 灾难推演:如果去掉了 & 假设我们将拷贝构造函数写成了这样(去掉了 &),通过调用拷贝构造函数让对象 d1 初始化新对象 d2。

    class Date {
    public:
        Date(Date d) { // 参数为值传递
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        // ...
    };
    

    程序执行 Date d2(d1),准备调用拷贝构造函数。系统发现拷贝构造函数 Date(Date d) 的参数是 Date d(按值传递)。为了调用这个函数,系统必须先把实参 d1 复制给形参 d。如何复制?系统必须调用 Date 的拷贝构造函数。系统再次准备调用拷贝构造函数……回到第 2 步。结果:这就形成了一个无限递归调用。

  3. 为什么加了引用 & 就没事了? 引用传递的含义是:形参 d 只是实参 d1 的一个别名。传递参数时,不需要创建新的副本,也不需要分配新的内存,只是把 d1 的地址/身份传进去了。既然不需要创建新对象,就不会触发'拷贝'动作,也就不会再次调用拷贝构造函数。死循环被打破了,函数顺利进入内部执行赋值操作。

2.2 拷贝构造的特性

C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。

场景一:传值传参 当你把一个对象作为参数传给函数时,本质上是用实参初始化形参。

void func(Student s) // 这里的 s 是形参,它是 s1 的副本
{
    // ...
}
int main() {
    Student s1;
    func(s1); // 调用 func,传入 s1
    return 0;
}

调用逻辑:

  1. 开辟空间:函数 func 被调用,栈内存中为形参 s 开辟空间。
  2. 触发拷贝:既然 s 是新诞生的对象,且它的初值来源于 s1,编译器必须调用拷贝构造函数 Student(const Student&)。
  3. 结果:s 成为 s1 的一份独立拷贝。当函数结束后,s 被销毁,s1 不受影响。

场景二:传值返回 这是比较隐蔽的拷贝场景,当函数返回一个对象时,局部变量在函数结束时就会销毁,所以通过'拷贝'把值传出去。

Student createStudent() {
    Student temp;
    return temp; // 返回 temp
}
int main() {
    Student s2 = createStudent();
    return 0;
}

返回过程的详情解释:

  1. 创建临时对象:createStudent 结束前,编译器会在调用处的栈帧上创建一个'临时匿名对象'。
  2. 第一次拷贝:调用拷贝构造函数,把 temp 拷贝给这个'临时匿名对象'。(然后 temp 销毁)。
  3. 第二次拷贝:在 main 函数中,使用'临时匿名对象'去初始化 s2,再次调用拷贝构造函数。(然后临时对象销毁)。

2.3 编译器默认生成的拷贝构造

2.3.1 生成的条件

若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。

2.3.2 执行的逻辑
  1. 对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝)。
  2. 对自定义类型成员变量会调用他的拷贝构造。

代码示例 A:对内置类型的成员进行值拷贝

class Date {
public:
    Date() { _year = 1; _month = 1; _day = 1; }
    Date(const Date& d) {
        _day = d._day;
        _month = d._month;
        _year = d._year;
    }
    void Print() { cout << _year << "/" << _month << "/" << _day << endl; }
private:
    int _year;
    int _month;
    int _day;
};
int main() {
    Date d1;
    Date d2 = d1;
    return 0;
}

代码示例 B:对自定义类型的成员,调用其类中的拷贝构造函数

#include <iostream>
#include <string>
using namespace std;

class Wallet {
public:
    Wallet() {}
    Wallet(const Wallet& w) {
        cout << "【自定义类型】Wallet 的拷贝构造被调用了!" << endl;
    }
};

class Person {
public:
    int age;
    int* scorePtr;
    Wallet myWallet;
    // 这里我们【不写】拷贝构造函数,编译器会自动生成一个
};

int main() {
    Person p1;
    p1.age = 18;
    int score = 100;
    p1.scorePtr = &score;
    cout << "--- 开始拷贝 ---" << endl;
    Person p2 = p1; // 触发编译器生成的默认拷贝构造
    cout << "p1 的年龄:" << p1.age << " " << "p2 的年龄:" << p2.age << endl;
    cout << "p1 成绩的地址:" << p1.scorePtr << " " << "p2 成绩的地址:" << p2.scorePtr << endl;
    cout << "--- 拷贝结束 ---" << endl;
    return 0;
}

2.4 拷贝构造的易错点

2.4.1 浅拷贝操作

浅拷贝是 C++ 编译器默认的复制行为,当你把一个对象赋值给另一个对象时,编译器会进行'按位拷贝',即把原对象中所有变量的值直接复制给新对象。

  1. 对于普通成员变量(如 int, double, char):浅拷贝没有问题,值被直接复制。
  2. 对于指针成员变量:浅拷贝只复制了指针本身(内存地址),而没有复制指针指向的数据。

代码示例:错误展示栈类的浅拷贝

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(const Stack& st) {
        _a = st._a;
        _capacity = st._capacity;
        _top = st._top;
    }
    ~Stack() { free(_a); _a = nullptr; }
private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};
int main() {
    Stack st1;
    Stack st2 = st1;
    return 0;
}

对于栈类而言,浅拷贝出现严重的缺陷:

  1. 数据共享冲突:如果朋友修改了房间里的东西,你回到家也会看到东西变了。
  2. 重复释放:这是最严重的问题。当你们两个的生命周期结束时(对象销毁),析构函数都会尝试'销毁房间'。当对象 A 释放了内存,对象 B 尝试释放同一块已经被释放的内存,造成程序崩溃。
2.4.2 深拷贝操作

为了克服浅拷贝的局限性,我们需要采用深拷贝机制。 在深拷贝过程中,当遇到指针成员时,会执行以下操作:

  1. 为新对象分配独立的内存空间
  2. 将原对象指针指向的数据完整复制到新内存中
  3. 使新对象的指针指向新分配的内存区域 这种处理方式确保了对象间的完全独立性。

代码示例:正确实现栈类的拷贝构造

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(const Stack& st) {
        // 需要对_a 指向资源创建同样大的资源再拷贝值
        _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
        if (nullptr == _a) {
            perror("malloc 申请空间失败!!!");
            return;
        }
        memcpy(_a, st._a, sizeof(STDataType) * st._top);
        _capacity = st._capacity;
        _top = st._top;
    }
    ~Stack() { free(_a); _a = nullptr; }
private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};
int main() {
    Stack st1;
    Stack st2 = st1;
    return 0;
}
  1. 避免了数据共享冲突:对象 st1 数据进行改变,对象 st2 的数据不会受到影响。
  2. 避免了重复释放:当 st1 和 st2 进行释放空间时,两者不会出现重复释放而是独立地释放各自的空间。

2.5 拷贝构造小结

  1. 拷贝构造函数是构造函数的一种重载形式。
  2. 拷贝构造函数的第一个参数必须是类类型对象的引用。若采用传值方式,编译器会直接报错,因为这会引发无限递归调用。拷贝构造函数可以包含多个参数,但第一个参数必须是类类型对象的引用,后续参数必须具有默认值。
  3. C++ 规定自定义类型对象进行拷贝时必须调用拷贝构造函数。因此,在传值参数和传值返回自定义类型对象时,都会调用拷贝构造函数完成操作。
  4. 如果未显式定义拷贝构造函数,编译器会自动生成一个。自动生成的拷贝构造函数会对内置类型成员变量进行值拷贝(浅拷贝,即逐字节复制),对自定义类型成员变量则会调用其拷贝构造函数。
  5. 对于像 Date 这样仅包含内置类型成员且不涉及资源管理的类,编译器自动生成的拷贝构造函数即可满足需求,无需手动实现。
  6. 但对于像 Stack 这样虽然使用内置类型但涉及资源管理(如指针 _a 指向资源)的类,自动生成的浅拷贝无法满足需求,需要手动实现深拷贝。
  7. 若类中包含自定义类型成员,编译器会自动调用自定义成员类的拷贝构造函数,因此也不需要手动实现拷贝构造函数。
  8. 传值返回会通过拷贝构造函数生成临时对象,而传引用返回则直接返回对象的引用(别名),不会产生拷贝。

特别注意:若返回的是函数局部对象(函数结束后即销毁),则不能使用引用返回,否则会导致野引用(类似于野指针),只有当返回对象在函数结束后仍然有效时,才适合使用引用返回来减少拷贝开销。

三、析构函数

什么是析构函数?

析构函数是面向对象编程(尤其是 C++)中的一个核心概念,简单来说,它是构造函数的'反义词'。如果说构造函数是'对象出生时的初始化(比如分配资源)',那么析构函数就是'对象临终前的清理(比如释放资源)'。

3.1 析构函数的核心语法

核心语法特征:

  1. 名称与类名相同,但在前面加一个波浪号 ~。
  2. 没有返回值,也没有类型。
  3. 不接受任何参数(因此析构函数不能被重载,一个类只能有一个析构函数)。

代码示例:显示实现析构函数

class MyClass {
public:
    MyClass() { /* 初始化代码 */ }
    ~MyClass() { /* 清理代码 */ }
};

代码示例:清理动态数组资源

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() {
        free(_a);
        _a = nullptr;
    }
private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

注意: 在构造函数中动态分配内存资源时,必须显式实现析构函数来释放这些资源,以防止内存泄漏。

3.2 析构函数的调用条件

3.2.1 离开作用域

离开作用域(栈对象):在函数内部定义的局部变量,当函数执行完毕或遇到右大括号 } 时。

代码示例:局部对象在函数执行完毕时自动调用析构函数

#include <iostream>
using namespace std;

class Date {
public:
    Date() { _year = 1; _month = 1; _day = 1; }
    ~Date() { cout << "析构函数被调用" << endl; }
private:
    int _year;
    int _month;
    int _day;
};

void testScope() {
    cout << "---testScope 函数开始 ---" << endl;
    Date obj;
    cout << "--- testScope 函数即将结束 ---" << endl;
}

int main() {
    cout << "---执行 main 函数,准备调用 testScope 函数---" << endl;
    testScope();
    cout << "---回到 main 函数,继续执行---" << endl;
    return 0;
}
3.2.2 delete 操作

delete 操作:当你拥有一个指向堆对象的指针,并显式调用 delete pointer 时进行析构函数的调用。

代码示例:

class Date {
public:
    Date() { _year = 1; _month = 1; _day = 1; }
    ~Date() { cout << "析构函数被调用" << endl; }
private:
    int _year;
    int _month;
    int _day;
};

void testHeap() {
    cout << "--- testHeap 函数开始 ---" << endl;
    Date* ptr = new Date;
    cout << "--- 做一些操作 ---" << endl;
    delete ptr; // 只有这行代码执行时,析构函数才触发
    cout << "--- testHeap 函数结束 ---" << endl;
}

int main() {
    testHeap();
    return 0;
}

3.3 编译器默认生成的析构函数

3.3.1 生成的条件

编译器生成这个析构函数有且仅有一个条件:你的类中没有定义任何析构函数。

3.3.2 行为逻辑
  1. 对待'自定义类型':如果你的类里包含其他的类对象,编译器生成的析构函数会自动调用这些成员的析构函数。
  2. 对待'内置类型':对于基础数据类型,编译器默认生成的构造函数什么都不做。

代码示例 A:通过双栈实现队列

typedef int STDataType;
class Stack {
public:
    Stack(int n = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * n); _capacity = n; _top = 0; }
    ~Stack() { free(_a); _a = nullptr; }
private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

class MyQueue {
public:
    // 编译器默认生成 MyQueue 的析构函数调用了 Stack 的析构,释放的 Stack 内部的资源
private:
    Stack pushst;
    Stack popst;
};

代码示例 B:在栈类中未显示实现析构函数

typedef int STDataType;
class Stack {
public:
    Stack(int n = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * n); _capacity = n; _top = 0; }
private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

特别注意:编译器生成的默认析构函数回收了栈区的 _a(那把钥匙),堆区的那块内存(那个房间)依然被标记为'占用',但因为钥匙没了,你再也无法访问它,也无法释放它,这就导致了内存泄漏。

3.4 析构函数调用顺序

3.4.1 同一作用域的局部对象

如果在同一个函数里定义了多个对象,它们的析构顺序遵循栈 (Stack) 的特性:后进先出 (LIFO)。

代码示例:

class Date {
public:
    Date() { _year = 1; _month = 1; _day = 1; }
    ~Date() { cout << "析构函数被调用" << endl; }
private:
    int _year;
    int _month;
    int _day;
};

int main() {
    Date d1;
    Date d2;
}

析构顺序:d2 完成析构 -> d1 完成析构。

3.4.2 数组对象

对于数组对象而言:数组元素的析构顺序与构造顺序严格相反。

  • 构造顺序:按照数组下标从小到大 (0 -> N)。
  • 析构顺序:按照数组下标从大到小 (N -> 0)。
3.4.3 局部静态对象和全局对象

这些对象像是'元老',程序的生命不结束,它们就不退休。

  • 全局对象:在 main 开始前出生,main 结束后销毁。
  • 静态对象:第一次运行到定义处出生,main 结束后销毁。
#include <iostream>
#include <string>
using namespace std;

class Date {
public:
    Date(string name) : _name(name) { cout << "构造:" << _name << endl; }
    ~Date() { cout << "析构:" << _name << endl; }
private:
    string _name;
};

Date globalObj("Global 对象");

void func() {
    static Date staticObj("Static 对象");
}

int main() {
    func();
    return 0;
}

3.5 析构函数的小结

  1. 析构函数名由类名前加波浪号 ~ 构成。
  2. 析构函数无参数且无返回值。
  3. 每个类只能有一个析构函数。若未显式定义,编译器会自动生成默认析构函数。
  4. 当对象生命周期结束时,系统会自动调用其析构函数。
  5. 与构造函数类似,编译器生成的默认析构函数对内置类型成员不做处理,但会调用自定义类型成员的析构函数。
  6. 显式定义的析构函数同样会调用自定义类型成员的析构函数。
  7. 当类未申请资源时,可以不写析构函数而使用编译器生成的默认版本;若默认析构函数已满足需求,也无需显式定义。
  8. 但涉及资源申请时,必须自定义析构函数以避免资源泄漏。
  9. 在局部作用域中,多个对象的析构顺序遵循 C++ 规定:后定义的对象先析构。

四、操作符重载

什么是操作符重载?

  1. 操作符重载让你能够为自定义类型(类或结构体)重新定义运算符(如 +, -, *, << 等)的行为。
  2. 这项功能的主要价值在于提升代码可读性,让自定义类型的使用体验与内置类型一样自然直观。

本质上,操作符重载只是函数调用的一种语法糖。

4.1 运算符重载

核心概念:C++ 支持通过运算符重载为自定义类型赋予新的运算语义,当对类对象使用运算符时,编译器会自动将其转换为对应的重载函数调用。

温馨提示:

  1. 若未定义相关运算符重载,则会触发编译错误。
  2. 当且仅当参数中至少包含一个自定义类型,才会触发运算符重载函数。
4.1.1 运算符重载的语法

语法规则:返回类型 + 关键字 operatorOp + 参数。

  1. 返回类型:可以是 void, int, bool, 返回'引用', 返回'对象'等。
  2. 关键字和操作符:关键字必须是 operator。操作符 Op 必须是 C++ 现有的符号。
  3. 参数:重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。
    • 一元运算符:仅需一个参数。
    • 二元运算符:需要两个参数,对于二元运算符而言,其左侧的运算对象传给第一个参数,其右侧运算对象传给第二个参数。

有 5 个特殊的符号,绝对禁止重载:. (点号), :: (双冒号), sizeof, ?:, .*。

代码示例:对 Person 这个自定义类型实现'+'运算符重载

#include <iostream>
using namespace std;

class Person {
public:
    Person(int age) { _age = age; }
private:
    int _age;
};

int operator+(Person a, Person b) {
    return a._age + b._age;
}

int main() {
    Person p1(18);
    Person p2(20);
    cout << "p1 和 p2 的总年龄为:" << p1 + p2 << endl;
    return 0;
}
4.1.2 全局函数和成员函数的运算符重载

1. 全局函数的运算符重载 当我们将运算符重载为全局函数时,该函数不属于任何类。

  • 核心机制:所有操作数都必须通过参数显式传递。
  • 参数数量:等于运算符原本需要的操作数。

2. 成员函数的运算符重载 当我们将运算符重载为类的成员函数时,该函数是类的一部分。

  • 核心机制:运算符左侧的操作数会自动成为调用该函数的对象(即 this 指针指向的对象)。
  • 参数数量:比运算符原本需要的操作数少一个。
4.1.3 全局函数和成员函数的选择

通用建议:

  1. 必须用成员函数的情况: =, [], (), ->。
  2. 必须用全局函数的情况: <<, >>。
  3. 建议用全局函数的情况: 所有的二元算术运算符 (+, -, *, /),比较运算符 (==, !=, <)。
  4. 建议用成员函数的情况: 复合赋值运算符 (+=, -=, *=)。
4.1.4 特殊的运算符重载

① 前置 ++ 和后置 ++ 的运算符重载 在重载 ++ 运算符时,存在前置 ++ 和后置 ++ 两种形式,它们都使用 operator++ 作为函数名。

  • 前置 ++: 先加,后用。返回引用。原型:Type& operator++()。
  • 后置 ++: 先用,后加。必须带有一个 int 类型的占位参数。返回旧值的副本。原型:Type operator++(int)。

代码示例:综合使用前置 ++ 和后置 ++

#include <iostream>
using namespace std;

class MyInt {
private:
    int value;
public:
    MyInt(int v = 0) : value(v) {}
    // 1. 前置 ++
    MyInt& operator++() {
        value++;
        return *this;
    }
    // 2. 后置 ++
    MyInt operator++(int) {
        MyInt temp = *this;
        value++;
        return temp;
    }
    void print() const { cout << "Value: " << value << endl; }
};

int main() {
    MyInt a(10);
    MyInt b = ++a; // 前置
    MyInt c = a++; // 后置
    return 0;
}

② 流插入 << 和 流提取 >> 的运算符重载 核心规则:必须实现为全局函数(非成员函数),并通常声明为类的 friend(友元)。

1. 流插入运算符 << 函数原型:ostream& operator<<(ostream& out, const Type& obj)。

2. 流提取运算符 >> 函数原型:istream& operator>>(istream& in, Type& obj)。

代码示例:综合使用流插入 << 和 流提取 >>

#include <iostream>
using namespace std;

class Complex {
private:
    int real;
    int imag;
    friend ostream& operator<<(ostream& out, const Complex& c);
    friend istream& operator>>(istream& in, Complex& c);
public:
    Complex(int r = 0, int i = 0) : real(r), imag(i) {}
};

ostream& operator<<(ostream& out, const Complex& c) {
    out << c.real << "+" << c.imag << "i";
    return out;
}

istream& operator>>(istream& in, Complex& c) {
    in >> c.real >> c.imag;
    return in;
}

int main() {
    Complex c1;
    cin >> c1;
    cout << "你输入的复数是:" << c1 << endl;
    return 0;
}

4.2 赋值运算符重载

什么是赋值运算符重载? 赋值运算符重载是一个默认成员函数,用于实现两个已存在对象之间的拷贝赋值。

4.2.1 赋值重载的核心语法

核心语法:ClassName& operator=(const ClassName& 参数名)。

  1. 返回类型:ClassName&,支持链式赋值。
  2. 函数名:operator=。
  3. 参数:const ClassName&,避免拷贝实参,提高效率。

代码示例:

class Date {
public:
    Date& operator=(const Date& d) {
        if (this == &d) {
            return *this;
        }
        _year = d._year;
        _month = d._month;
        _day = d._day;
        return *this;
    }
private:
    int _year;
    int _month;
    int _day;
};
4.2.2 编译器默认生成的赋值重载

如果你没有自己编写赋值运算符重载,编译器会自动为你生成一个。

  1. 内置类型成员变量会进行值拷贝(逐字节复制)。
  2. 自定义类型成员变量则会调用其赋值运算符重载函数。

对于基础类型,直接复制数值;对于指针类型,直接复制地址值,这是最危险的地方,因为它不复制指针指向的内容。

五、总结:三法则 (Rule of Three)

C++ 有一个著名的原则:如果你需要显式定义以下其中一个,你通常需要定义全部三个:

  1. 析构函数 (Destructor)
  2. 拷贝构造函数 (Copy Constructor)
  3. 拷贝赋值运算符 (Copy Assignment Operator)

目录

  1. 类的默认成员函数
  2. 一、构造函数
  3. 1.1 构造函数的核心语法
  4. 1.2 构造函数的常见类型
  5. 1.2.1 无参构造函数
  6. 1.2.2 带参数的构造函数
  7. 1.2.3 全缺省构造函数
  8. 1.3 默认构造函数
  9. 1.4 编译器默认生成的构造函数
  10. 1.4.1 生成的条件
  11. 1.4.2 执行的逻辑
  12. 1.5 构造函数小结
  13. 二、拷贝构造函数
  14. 2.1 拷贝构造的核心语法
  15. 2.2 拷贝构造的特性
  16. 2.3 编译器默认生成的拷贝构造
  17. 2.3.1 生成的条件
  18. 2.3.2 执行的逻辑
  19. 2.4 拷贝构造的易错点
  20. 2.4.1 浅拷贝操作
  21. 2.4.2 深拷贝操作
  22. 2.5 拷贝构造小结
  23. 三、析构函数
  24. 3.1 析构函数的核心语法
  25. 3.2 析构函数的调用条件
  26. 3.2.1 离开作用域
  27. 3.2.2 delete 操作
  28. 3.3 编译器默认生成的析构函数
  29. 3.3.1 生成的条件
  30. 3.3.2 行为逻辑
  31. 3.4 析构函数调用顺序
  32. 3.4.1 同一作用域的局部对象
  33. 3.4.2 数组对象
  34. 3.4.3 局部静态对象和全局对象
  35. 3.5 析构函数的小结
  36. 四、操作符重载
  37. 4.1 运算符重载
  38. 4.1.1 运算符重载的语法
  39. 4.1.2 全局函数和成员函数的运算符重载
  40. 4.1.3 全局函数和成员函数的选择
  41. 4.1.4 特殊的运算符重载
  42. 4.2 赋值运算符重载
  43. 4.2.1 赋值重载的核心语法
  44. 4.2.2 编译器默认生成的赋值重载
  45. 五、总结:三法则 (Rule of Three)
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • OpenClaw + Kimi K2.5 本地私有化部署与办公自动化实战
  • 快速排序深度解析与实战优化
  • Openclaw 2026.3.7 Docker 离线部署指南
  • Git 在 Linux 及麒麟国防版 V10 上的安装配置
  • 为什么 VsCode 远程 Copilot 用不了 Claude Agent?系统权限不足的解决策略
  • Kimi 高级用法教程:从入门到 AI 应用实战
  • 深度学习 YOLOv11 空域安全无人机检测识别系统
  • C++ GESP 三级认证手册:计算机基础与算法
  • 数据结构:带头双向循环链表详解与实现
  • FastGPT 结合 MCP 协议构建工具增强型智能体
  • 使用Cython提升Python运行性能详解
  • AgentScope Java 实战:构建 AI 奶茶店应用
  • Python 常用库实用清单:从数值计算到深度学习
  • Python pip 常用命令详解与使用技巧
  • 学生如何申请并使用 GitHub Copilot 进行编程实践
  • 视觉 - 骨架双模态框架用于帕金森病步态的泛化评估
  • Windows 环境下编译 Torchvision C++ 版本指南
  • WebAssembly 技术全景解析:重塑 Web 与原生边界
  • 动态规划 01 背包问题详解与空间优化实战
  • TinyLlama 与 LiteLlama:轻量级模型实现高性能推理与应用

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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