跳到主要内容
C++ 类与对象进阶:默认成员函数详解 | 极客日志
C++ 算法
C++ 类与对象进阶:默认成员函数详解 综述由AI生成 C++ 类与对象进阶主要涵盖构造函数、拷贝构造函数、析构函数及操作符重载四大核心。重点解析了编译器默认生成机制,特别是内置类型与自定义类型的初始化差异。深入探讨了浅拷贝与深拷贝的区别,强调涉及资源管理时需手动实现深拷贝以避免内存泄漏。同时介绍了运算符重载的语法规则及全局与成员函数的选择策略,帮助开发者编写更安全、高效的 C++ 代码。
蓝绿部署 发布于 2026/3/15 更新于 2026/4/25 1 浏览前言
在基础概念学习中,我们已经掌握了类与实例化对象的核心定义。本文将深入探讨类与对象的默认成员函数,重点聚焦于构造函数、拷贝构造函数、析构函数以及操作符重载这四个方面。
一、类的默认成员函数
定义
默认成员函数是指用户未显式实现时,编译器自动生成的成员函数。
在一个类中,若未显式编写,编译器通常会默认生成以下 4 个重要的默认成员函数:
掌握这部分内容需要从两个维度入手:
了解编译器自动生成的默认函数行为及其适用场景。
当默认函数无法满足需求时,掌握自定义实现的方法。
二、构造函数
什么是构造函数?
你可以把构造函数想象成产品的'出厂设置'。当你根据图纸(类)生产出一个新零件(对象)时,构造函数就是那个自动运行、负责初始化状态(如赋值、分配内存)的程序。
注意 :构造函数不是给对象开辟空间的函数,而是完成初始化的函数。对象的空间开辟是栈帧的任务。
2.1 构造函数的核心语法
#include <iostream>
using namespace std;
class Student {
public :
int age;
Student () {
age = 18 ;
cout << "调用了默认构造函数" << endl;
}
};
int main () {
Student s1;
cout << s1. age << endl;
return 0 ;
}
语法要点 :
类名 (形式参数)
无返回值(连 void 都不写)
名称必须与类名相同
形式参数可有可无
2.2 构造函数的常见类型
2.2.1 无参构造函数 class Date {
public :
Date () {
_year = 1 ;
_month = 1 ;
_day = 1 ;
}
private :
int _year;
int _month;
int _day;
};
2.2.2 带参数的构造函数 class Date {
public :
Date (int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
private :
int _year;
int _month;
int _day;
};
2.2.3 全缺省构造函数 class Date {
public :
Date (int year = 1 , int month = 1 , int day = 1 ) {
_year = year;
_month = month;
_day = day;
}
private :
int _year;
int _month;
int _day;
};
注意 :全缺省构造函数和无参构造函数不能同时出现。因为创建不带参数的对象时,编译器会产生歧义。
2.3 默认构造函数
无参构造函数
全缺省构造函数
编译器自动生成的默认构造函数(当用户未定义任何构造函数时)
无参和全缺省虽然构成重载,但调用时存在歧义。
很多人误以为只有编译器生成的才叫默认构造,实际上无参和全缺省也是默认构造。
总结 :不传实参就可以调用的构造函数都叫默认构造函数。
2.4 编译器默认生成的构造函数
2.4.1 生成的条件 编译器生成这个构造函数有且仅有一个条件:你的类中没有定义任何默认构造函数。
如果你没写构造函数,编译器会生成一个默认无参的。
一旦你自己写了任意一个构造函数(哪怕是带参数的),编译器就会停止赠送默认构造函数。
class A {
};
class B {
public :
B (int x) { cout << x << endl; }
};
int main () {
B b;
B b (1 ) ;
return 0 ;
}
2.4.2 执行的逻辑 前置知识:C++ 把类型分成内置类型 (基本类型,如 int/char/double/指针等)和自定义类型 (class/struct 定义的类型)。
这是最容易踩坑的地方,编译器对待不同类型的态度截然不同:
A. 对待'自定义类型' —— 负责
如果你的类里包含其他类对象(比如 string, vector 或另一个 class),编译器生成的构造函数会自动调用这些成员的默认构造函数。
B. 对待'内置类型' —— 摆烂
对于基础数据类型,编译器默认生成的构造函数什么都不做。这意味着这些变量的内存里原来是什么垃圾数据,现在还是什么。
代码示例 A:通过双栈实现队列
在队列类中无需实现构造函数,编译器默认生成的构造函数会主动调用 Stack 类的构造函数,完成队列类成员的初始化。
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;
};
class MyQueue {
public :
private :
Stack pushst;
Stack popst;
};
int main () {
MyQueue mq;
return 0 ;
}
风险提示 :如果你在 Stack 类中删除了显式定义的构造函数,将会发生连带反应:
编译阶段 :代码依然可以编译通过。原因:当你没有定义任何构造函数时,编译器会按照 C++ 标准自动为你生成一个隐式的默认构造函数。
运行阶段 :极度危险(未定义行为)。编译器自动生成的默认构造函数对内置类型成员变量(如 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 或指针等。
2.5 构造函数小结 构造函数是一种特殊的成员函数,主要用于在创建对象时初始化对象。
名称与类名相同。
没有返回值。
自动调用,不需要手动调用。
可以重载,只要参数列表不同即可。
如果类中没有显式定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但这三个函数有且只有一个存在,不能同时存在。
三、拷贝构造函数 什么是拷贝构造函数?
简单来说,它的作用是'克隆':用一个已存在的对象来初始化一个新对象。就像你有一份文件(对象 A),放入复印机按一下,得到了一份一模一样的新文件(对象 B)。
3.1 拷贝构造的核心语法 class ClassName {
public :
ClassName (const ClassName& other) {
}
};
引用 & 是必须的:如果不传引用而是传值,传参过程本身又要拷贝,就会无限递归调用拷贝构造函数,导致栈溢出。
const 通常要加:保证在拷贝过程中,不会意外修改那个'原件'。
1. 前置知识:按值传递的代价
在 C++ 中,当通过传值传参的方式调用函数时,编译器需要将实参对象复制给形参对象,此时复制的过程就会调用拷贝构造函数。
2. 灾难推演:如果去掉了 &
假设我们将拷贝构造函数写成了这样(去掉了 &),通过调用拷贝构造函数让对象 d1 初始化新对象 d2。
class Date {
public :
Date (int year = 1 , int month = 1 , int day = 1 ) {
_year = year; _month = month; _day = day;
}
Date (Date d) {
_year = d._year; _month = d._month; _day = d._day;
}
void Print () { cout << _year << "-" << _month << "-" << _day << endl; }
private :
int _year;
int _month;
int _day;
};
int main () {
Date d1 (2030 , 1 , 1 ) ;
Date d2 (d1) ;
d2. Print ();
return 0 ;
}
详情解释:程序执行 Date d2(d1),准备调用拷贝构造函数。系统发现参数是 Date d(按值传递),为了调用这个函数,系统必须先把实参 d1 复制给形参 d。如何复制?系统必须调用 Date 的拷贝构造函数。回到第 2 步... 结果形成无限递归调用。
3. 为什么加了引用 & 就没事了?
当你加上 &,变成 Date(const Date& d) 时:
引用传递的含义是:形参 d 只是实参 d1 的一个别名。
传递参数时,不需要创建新的副本,也不需要分配新的内存,只是把 d1 的地址传进去了。
既然不需要创建新对象,就不会触发'拷贝'动作,也就不会再次调用拷贝构造函数。死循环被打破了。
3.2 拷贝构造的特性 C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
场景一:传值传参
当你把一个对象作为参数传给函数时,本质上是用实参初始化形参。
void func (Student s)
{
}
int main () {
Student s1;
func (s1);
return 0 ;
}
开辟空间:函数 func 被调用,栈内存中为形参 s 开辟空间。
触发拷贝:既然 s 是新诞生的对象,且它的初值来源于 s1,编译器必须调用拷贝构造函数 Student(const Student&)。
结果:s 成为 s1 的一份独立拷贝。当函数结束后,s 被销毁,s1 不受影响。
场景二:传值返回
这是比较隐蔽的拷贝场景,当函数返回一个对象时,局部变量在函数结束时就会销毁,所以通过'拷贝'把值传出去。
Student createStudent () {
Student temp;
return temp;
}
int main () {
Student s2 = createStudent ();
return 0 ;
}
创建临时对象:createStudent 结束前,编译器会在调用处的栈帧上创建一个'临时匿名对象'。
第一次拷贝:调用拷贝构造函数,把 temp 拷贝给这个'临时匿名对象'。(然后 temp 销毁)。
第二次拷贝:在 main 函数中,使用'临时匿名对象'去初始化 s2,再次调用拷贝构造函数。(然后临时对象销毁)。
3.3 编译器默认生成的拷贝构造
3.3.1 生成的条件 若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。
如果你太懒了,没写拷贝构造函数,编译器会给你生成一个默认的。
一旦你自己写了任意一个拷贝构造函数,编译器就会认为'你有自己的想法',它立刻停止赠送默认的构造构造函数。
3.3.2 执行的逻辑 A. 对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝)。
B. 对自定义类型成员变量会调用他的拷贝构造。
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 ;
}
Wallet 的拷贝构造被打印 -> 证明自定义类型调用了它的拷贝构造。
p2.age 等于 18 -> 证明内置类型进行了值拷贝。
p2.scorePtr 的地址 等于 p1.scorePtr -> 证明指针只拷贝了地址(浅拷贝)。
3.4 拷贝构造的易错点
3.4.1 浅拷贝操作 浅拷贝是 C++ 编译器默认的复制行为,当你把一个对象赋值给另一个对象时,编译器会进行'按位拷贝',即把原对象中所有变量的值直接复制给新对象。
对于普通成员变量(如 int, double, char):浅拷贝没有问题,值被直接复制。
对于指针成员变量:浅拷贝只复制了指针本身(内存地址),而没有复制指针指向的数据。
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 ;
}
对于栈类而言,浅拷贝出现严重的缺陷:
这就好比你把你家的钥匙(指针)复制了一把给你的朋友。现在,你们两个手里都有钥匙,但指向的是同一个房间(堆内存)。
数据共享冲突 :如果你的朋友修改了房间里的东西,你回到家也会看到东西变了。
重复释放 :这是最严重的问题。当你们两个的生命周期结束时(对象销毁),析构函数都会尝试'销毁房间'。当对象 A 释放了内存,对象 B 尝试释放同一块已经被释放的内存,造成程序崩溃。
3.4.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 = (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 ;
}
避免了数据共享冲突:对象 st1 数据进行改变,对象 st2 的数据不会受到影响。
避免了重复释放:当 st1 和 st2 进行释放空间时,两者不会出现重复释放而是独立地释放各自的空间。
3.5 拷贝构造小结
拷贝构造函数是构造函数的一种重载形式。
拷贝构造函数的第一个参数必须是类类型对象的引用。若采用传值方式,编译器会直接报错,因为这会引发无限递归调用。拷贝构造函数可以包含多个参数,但第一个参数必须是类类型对象的引用,后续参数必须具有默认值。
C++ 规定自定义类型对象进行拷贝时必须调用拷贝构造函数。因此,在传值参数和传值返回自定义类型对象时,都会调用拷贝构造函数完成操作。
如果未显式定义拷贝构造函数,编译器会自动生成一个。自动生成的拷贝构造函数会对内置类型成员变量进行值拷贝(浅拷贝,即逐字节复制),对自定义类型成员变量则会调用其拷贝构造函数。
对于像 Date 这样仅包含内置类型成员且不涉及资源管理的类,编译器自动生成的拷贝构造函数即可满足需求,无需手动实现。
但对于像 Stack 这样虽然使用内置类型但涉及资源管理(如指针_a 指向资源)的类,自动生成的浅拷贝无法满足需求,需要手动实现深拷贝。
若类中包含自定义类型成员,编译器会自动调用自定义成员类的拷贝构造函数,因此也不需要手动实现拷贝构造函数。
传值返回会通过拷贝构造函数生成临时对象,而传引用返回则直接返回对象的引用(别名),不会产生拷贝。
特别注意:若返回的是函数局部对象(函数结束后即销毁),则不能使用引用返回,否则会导致野引用(类似于野指针),只有当返回对象在函数结束后仍然有效时,才适合使用引用返回来减少拷贝开销。
四、析构函数 什么是析构函数?
析构函数是面向对象编程中的一个核心概念,简单来说,它是构造函数的'反义词'。如果说构造函数是'对象出生时的初始化(比如分配资源)',那么析构函数就是'对象临终前的遗言(比如清理资源)'。
4.1 析构函数的核心语法
名称与类名相同,但在前面加一个波浪号 ~。
没有返回值,也没有类型。
不接受任何参数(因此析构函数不能被重载,一个类只能有一个析构函数)。
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;
};
注意 :在构造函数中动态分配内存资源时,必须显式实现析构函数来释放这些资源,以防止内存泄漏。
4.2 析构函数的调用条件
4.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 ;
}
4.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 ;
}
4.3 编译器默认生成的析构函数
4.3.1 生成的条件 编译器生成这个析构函数有且仅有一个条件:你的类中没有定义任何析构函数。
如果你太懒了,没写析构函数,编译器会给你生成一个析构函数。
一旦你自己写了析构函数,编译器就会认为'你有自己的想法',它立刻停止赠送默认的析构函数。
4.3.2 行为逻辑 A. 对待'自定义类型' (Class/Struct) —— 负责
如果你的类里包含其他的类对象,编译器生成的析构函数会自动调用这些成员的析构函数。
B. 对待'内置类型' (int, double, 指针等) —— 摆烂
对于基础数据类型,编译器默认生成的构造函数什么都不做。
代码示例 A:通过双栈实现队列
在队列类中无需实现析构函数,编译器默认生成的析构函数会主动调用队列类中自定义成员栈类的析构函数。
typedef int STDataType;
class Stack {
public :
Stack (int n = 4 ) { }
~Stack () { free (_a); _a = nullptr ; }
private :
STDataType* _a;
size_t _capacity;
size_t _top;
};
class MyQueue {
public :
private :
Stack pushst;
Stack popst;
};
注意:即使你在队列类中实现了析构函数,编译器不信任用户实现的析构函数,仍然会调用 Stack 类中的析构函数完成自定义成员的资源释放,但如果栈类中也没有实现析构函数则会出现资源未释放,导致内存泄漏。
代码示例 B:在栈类中未显示实现析构函数
使用编译器生成的析构函数什么都不做,不会主动释放开辟的堆内存空间。
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;
};
特别注意:编译器生成的默认析构函数回收了栈区的 _a(那把钥匙),堆区的那块内存(那个房间)依然被标记为'占用',但因为钥匙没了,你再也无法访问它,也无法释放它,这就导致了内存泄漏。
4.4 析构函数调用顺序
4.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;
return 0 ;
}
温馨提示:析构顺序:d2 完成析构 -> d1 完成析构。
4.4.2 数组对象 对于数组对象而言:数组元素的析构顺序与构造顺序严格相反。
构造顺序:按照数组下标从小到大 (0 -> N)。
析构顺序:按照数组下标从大到小 (N -> 0)。
#include <iostream>
using namespace std;
class Item {
private :
int id;
static int count;
public :
Item () { id = count++; cout << "构造:Item [" << id << "] (地址:" << this << ")" << endl; }
~Item () { cout << "析构:Item [" << id << "] (地址:" << this << ")" << endl; }
};
int Item::count = 0 ;
int main () {
cout << "--- 数组作用域开始 ---" << endl;
Item myArray[3 ];
cout << "--- 数组作用域即将结束 ---" << endl;
return 0 ;
}
4.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 () {
cout << "--- main 开始 ---" << endl;
func ();
cout << "--- main 结束 ---" << endl;
return 0 ;
}
4.5 析构函数的小结
析构函数名由类名前加波浪号 ~ 构成。
析构函数无参数且无返回值。
每个类只能有一个析构函数。若未显式定义,编译器会自动生成默认析构函数。
当对象生命周期结束时,系统会自动调用其析构函数。
与构造函数类似,编译器生成的默认析构函数对内置类型成员不做处理,但会调用自定义类型成员的析构函数。
显式定义的析构函数同样会调用自定义类型成员的析构函数。
当类未申请资源时,可以不写析构函数而使用编译器生成的默认版本;若默认析构函数已满足需求,也无需显式定义。
但涉及资源申请时,必须自定义析构函数以避免资源泄漏。
在局部作用域中,多个对象的析构顺序遵循 C++ 规定:后定义的对象先析构。
五、操作符重载
操作符重载让你能够为自定义类型(类或结构体)重新定义运算符(如 +、-、*、<< 等)的行为。
这项功能的主要价值在于提升代码可读性,让自定义类型的使用体验与内置类型一样自然直观。
举例说明:在 C++ 中,1 + 2 很容易理解。但是,如果你定义了一个 Person 类,person1 + person2 是什么意思?编译器默认不知道。操作符重载就是你告诉编译器:'当你在两个 Person 对象之间看到 + 号时,请执行这段代码。'本质上,操作符重载只是函数调用的一种语法糖。
5.1 运算符重载 核心概念 :C++ 支持通过运算符重载为自定义类型赋予新的运算语义,当对类对象使用运算符时,编译器会自动将其转换为对应的重载函数调用。
若未定义相关运算符重载,则会触发编译错误。
当且仅当参数中至少包含一个自定义类型,才会触发运算符重载函数。
5.1.1 运算符重载的语法
返回类型 + 关键字 operator + 操作符 Op + 参数
返回类型:可以是 void、int、bool、返回'引用'、返回'对象'等。
关键字和操作符:必须是 operator。操作符 Op 必须是 C++ 现有的符号。
参数:重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。
一元运算符:仅需一个参数。
二元运算符:需要两个参数,左侧的运算对象传给第一个参数,右侧运算对象传给第二个参数。
禁止重载的符号 :. (点号)、:: (双冒号)、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 ;
}
代码详解:当你写下 p1 + p2 时,C++ 编译器做了以下动作:
扫描:编译器看到两个 Person 对象中间有一个 + 号。
查找:它去寻找是否存在一个函数签名匹配 operator+(Person, Person)。
调用:它找到了你写的全局函数,于是将 p1 + p2 翻译成了函数调用:operator+(p1, p2)。
执行:函数内部提取了 p1 的 _age (18) 和 p2 的 _age (20),相加得到整数 38。
返回:将 38 返回给 cout 进行打印。
5.1.2 全局函数和成员函数的运算符重载 1. 全局函数的运算符重载
当我们将运算符重载为全局函数时,该函数不属于任何类,它只是一个普通的函数,名字叫 operatorX。
核心机制:所有操作数都必须通过参数显式传递。
参数数量:等于运算符原本需要的操作数。
双目运算符(如 +):需要 2 个参数。
单目运算符:需要 1 个参数。
代码示例:通过全局函数的运算符重载,实现二元操作符+
class Point {
public :
Point (int x, int y) : x (x), y (y) {}
int x, y;
};
Point operator +(const Point& a, const Point& b) {
return Point (a.x + b.x, a.y + b.y);
}
缺陷 :全局函数的运算符重载会面临对象访问私有成员变量的问题,在类外没有足够权限访问类内的私有成员。
解决办法 :
将私有成员放为公有成员 (不推荐,破坏了封装性)。
自定义类提供 getxxx 函数,从而访问私有成员。
通过声明为友元函数。
重载为成员函数。
2. 成员函数的运算符重载
当我们将运算符重载为类的成员函数时,该函数是类的一部分。
核心机制:运算符左侧的操作数会自动成为调用该函数的对象(即 this 指针指向的对象)。
参数数量:比运算符原本需要的操作数少一个(因为左侧操作数隐含为 this)。
双目运算符(如 +):只需要 1 个参数。
单目运算符(如 ++):不需要参数(或者是占位参数用于区分前/后置)。
代码示例:通过在类内实现运算符重载函数,完成二元操作符+
class Point {
public :
int x, y;
Point (int x, int y) : x (x), y (y) {}
Point operator +(const Point& other) {
return Point (x + other.x, y + other.y);
}
};
5.1.3 全局函数和成员函数的选择
必须用成员函数的情况 :=, [], (), ->。这些运算符与对象的状态紧密相关,C++ 语法强制要求它们必须是成员。
必须用全局函数的情况 :<<, >>。原因:cout << p 的左操作数是 ostream 系统类,我们无法修改 ostream 的源码来添加成员函数,只能通过全局函数 operator<<(ostream& out, Point p) 来实现。
建议用全局函数的情况 :所有的二元算术运算符 (+, -, *, /),比较运算符 (==, !=, <)。这样可以保持操作数的对称性,允许左侧操作数进行隐式类型转换。
建议用成员函数的情况 :复合赋值运算符 (+=, -=, *=)。因为 a += b 通常会改变 a 自身的状态(修改 this),且返回值通常是 *this 的引用。
5.1.4 特殊的运算符重载
① 前置 ++ 和 后置 ++ 的运算符重载 在重载 ++ 运算符时,存在前置 ++ 和后置 ++ 两种形式,它们都使用 operator++ 作为函数名,这会导致难以区分。
为了解决这个问题,C++ 规定:重载后置 ++ 运算符时,需要额外添加一个 int 类型的形参,这样就能通过函数重载机制来明确区分前置和后置 ++ 运算符。
1. 前置 ++ 进行函数重载
前置递增的逻辑是:'先加,后用'。
实现逻辑:直接修改对象的数据,然后返回修改后的对象本身。
返回值:通常是对象的引用 (Type&)。这样做既提高了效率(避免拷贝),又支持链式调用。
函数原型:Type& operator++()
MyInteger& operator ++() {
this ->value += 1 ;
return *this ;
}
2. 后置 ++ 进行函数重载
后置递增的逻辑是:'先用,后加'。
实现逻辑:必须先保存对象原本的值,然后修改对象的数据,最后返回保存的'旧值'。
区分标志:为了和前置 ++ 区分,C++ 规定后置 ++ 重载函数必须带有一个 int 类型的占位参数。这个参数在调用时不需要传递,仅用于编译器区分签名。
返回值:必须是值 (Type),也就是一个副本,因为返回的是局部临时变量(旧值),不能返回引用。
函数原型:Type operator++(int)
MyInteger operator ++(int ) {
MyInteger temp = *this ;
this ->value += 1 ;
return temp;
}
#include <iostream>
using namespace std;
class MyInt {
private :
int value;
public :
MyInt (int v = 0 ) : value (v) {}
MyInt& operator ++() {
value++;
return *this ;
}
MyInt operator ++(int ) {
MyInt temp = *this ;
value++;
return temp;
}
void print () const {
cout << "Value: " << value << endl;
}
};
int main () {
MyInt a (10 ) ;
MyInt b = ++a;
a.print ();
b.print ();
MyInt c = a++;
a.print ();
c.print ();
return 0 ;
}
类似于前置 ++ 和后置 ++ 的重载方式,我们同样可以实现前置 -- 和后置 -- 的运算符重载。
② 流插入 << 和 流提取 >> 的运算符重载 前置知识:在 C++ 中,cout 和 cin 是我们最常打交道的两个'老朋友',它们是 C++ 标准库预定义好的'全局对象'。
cout (标准输出流):它是一个 ostream 类的全局对象。
cin (标准输入流):它是一个 istream 类的全局对象。
核心逻辑:在 C++ 中,重载流插入运算符 (<<) 和流提取运算符 (>>) ,目的是让自定义类支持 cout 和 cin 的标准方式。
核心规则:必须实现为全局函数(非成员函数),并通常声明为类的 friend(友元)。
原因:如果写成成员函数,调用方式会变成 obj << cout,这非常反直觉。为了保持 cout << obj 的习惯,运算符的左操作数必须是 ostream,右操作数才是你的对象。
1. 流插入运算符 <<
核心作用:用于输出对象的状态。
函数原型:ostream& operator<<(ostream& out, const Type& obj)
参数 1 (out):ostream 的引用(即 cout),必须引用,因为流对象禁止拷贝。
参数 2 (obj):要输出的对象,必须加 const,因为输出不应修改对象。
返回值:返回 out 的引用(即 cout),以支持链式调用 (如 cout << a << b)。
friend ostream& operator <<(ostream& out, const MyClass& obj);
ostream& operator <<(ostream& out, const MyClass& obj) {
out << "Data: " << obj.data;
return out;
}
2. 流提取运算符 >>
核心作用:用于从输入流读取数据并填充到对象中。
函数原型:istream& operator>>(istream& in, Type& obj)
参数 1 (in):istream 的引用(即 cin),必须引用,因为流对象禁止拷贝。
参数 2 (obj):接收数据的对象,不能加 const,因为我们要修改它。
返回值:返回 in(即 cin)的引用。
friend istream& operator >>(istream& in, MyClass& obj);
istream& operator >>(istream& in, MyClass& obj) {
in >> obj.data;
return in;
}
#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) {
cout << "请输入实部和虚部 (空格分隔): " ;
in >> c.real >> c.imag;
return in;
}
int main () {
Complex c1;
cin >> c1;
cout << "你输入的复数是:" << c1 << endl;
return 0 ;
}
5.2 赋值运算符重载 什么是赋值运算符重载?
赋值运算符重载是一个默认成员函数,用于实现两个已存在对象之间的拷贝赋值。
需要注意的是,它与拷贝构造函数有所区别:拷贝构造函数用于在创建新对象时,用另一个对象来初始化它。
5.2.1 赋值重载的核心语法 核心语法 :ClassName& operator=(const ClassName& 参数名)
返回类型:ClassName&。核心作用:1. 通过返回类型,支持链式赋值 a = b = c;2. 引用 (&):避免拷贝返回,提高效率。
函数名:operator=
参数:const ClassName&。核心作用:1. 引用 (&):避免拷贝实参,提高效率;2. const:保证右值(源对象)在赋值过程中不被修改。
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;
};
5.2.2 编译器默认生成的赋值重载 1. 生成的条件
如果你没有自己编写赋值运算符重载(operator=),编译器会自动为你生成一个。
遵循'免费午餐原则':
如果你太懒了,没写赋值运算符重载,编译器会给你生成一个默认的。
一旦你自己写了任意一个赋值运算符重载,编译器就会认为'你有自己的想法',它立刻停止赠送默认的赋值运算符重载。
2. 行为逻辑
内置类型成员变量会进行值拷贝(逐字节复制),而自定义类型成员变量则会调用其赋值运算符重载函数。
A. 对待'自定义类型' (Class/Struct) —— 负责
当类中包含自定义类对象(如 std::string、std::vector 或其他类)时,编译器默认生成的赋值运算符重载函数,会调用这些成员对象自身的赋值运算符重载函数(operator=)。
B. 对待'内置类型' (int, double, 指针等) —— 摆烂
对于基础类型 (int, double, char, bool),直接复制数值,对于指针类型 (int*, char*) 直接复制地址值,这是最危险的地方,因为它不复制指针指向的内容。
class User {
public :
User& operator =(const User& other) {
this ->id = other.id;
this ->name = other.name;
this ->score = other.score;
return *this ;
}
private :
int id;
std::string name;
double * score;
};
总结:三法则 (Rule of Three) C++ 有一个著名的原则:如果你需要显式定义以下其中一个,你通常需要定义全部三个:
析构函数 (Destructor)
拷贝构造函数 (Copy Constructor)
拷贝赋值运算符 (Copy Assignment Operator)
相关免费在线工具 加密/解密文本 使用加密算法(如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