类与对象
- [1. 类的默认成员函数](#1. 类的默认成员函数)
- [2. 构造函数](#2. 构造函数)
-
- [2.1 无参构造函数(默认构造之一)](#2.1 无参构造函数(默认构造之一))
- [2.2 带参构造函数(注,不算默认构造函数)](#2.2 带参构造函数(注,不算默认构造函数))
- [2.3 全缺省构造函数(默认构造之一)](#2.3 全缺省构造函数(默认构造之一))
- [2.4 编译器自己生成(默认构造之一 · 难点❗️)](#2.4 编译器自己生成(默认构造之一 · 难点❗️))
-
- [2.4.1 成员变量为内置类型](#2.4.1 成员变量为内置类型)
- [2.4.2 成员变量为自定义成员变量](#2.4.2 成员变量为自定义成员变量)
- [2.5 总结](#2.5 总结)
- [3. 析构函数](#3. 析构函数)
-
- [3.1 析构函数的特点](#3.1 析构函数的特点)
- [3.2 自己写析构](#3.2 自己写析构)
- [3.3 编译器自动生成析构](#3.3 编译器自动生成析构)
- [3.4 总结](#3.4 总结)
- 便利之处
- [4. 拷贝构造函数](#4. 拷贝构造函数)
-
- [4.1 拷贝构造函数什么时候需要显式写,什么时候不需要?](#4.1 拷贝构造函数什么时候需要显式写,什么时候不需要?)
-
- [4.1.1 为什么要进行深拷贝?](#4.1.1 为什么要进行深拷贝?)
- [4.1.2 如何进行深拷贝?](#4.1.2 如何进行深拷贝?)
- [4.2 什么时候会调用拷贝构造?](#4.2 什么时候会调用拷贝构造?)
-
- [4.2.1 用 旧自定义类型对象 初始化 新自定义类型对象](#4.2.1 用 旧自定义类型对象 初始化 新自定义类型对象)
- [4.2.2 自定义类型对象 作为 函数参数,进行值传递](#4.2.2 自定义类型对象 作为 函数参数,进行值传递)
- [4.2.3 函数返回一个 局部自定义类型对象(值返回)](#4.2.3 函数返回一个 局部自定义类型对象(值返回))
- [4.3 传值返回和传引用返回 在拷贝构造的区别](#4.3 传值返回和传引用返回 在拷贝构造的区别)
- [4.4 总结](#4.4 总结)
- [5. 赋值运算符重载](#5. 赋值运算符重载)
-
- [5.1 运算符重载](#5.1 运算符重载)
- [5.2 赋值运算符重载](#5.2 赋值运算符重载)
- [5.2.1 赋值运算符重载 什么时候需要显式写,什么时候不需要?](#5.2.1 赋值运算符重载 什么时候需要显式写,什么时候不需要?)
- [6. 取地址运算符重载](#6. 取地址运算符重载)
-
- [6.1 const 成员函数](#6.1 const 成员函数)
- [6.2 取地址重载](#6.2 取地址重载)
1. 类的默认成员函数
- 默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数就叫默认成员函数。
一个类,我们不写的情况下,编译器会默认生成以下6个默认成员函数。需要注意的是,这6个中,最重要的是前4个,最后两个取地址重载不重要,我们稍微了解一下即可。 - 其次,就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个以后讲解。

-
默认成员函数很重要,也比较复杂,我们要从两个方面去学习:
- 第一:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
- 第二:编译器默认生成的函数不满足我们的需求,我们需要自己实现时,如何自己实现?
2. 构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。
构造函数本质是要替代我们以前
Stack类中写的Init函数的功能,构造函数自动调用的特点完美替代了Init()。
构造函数的特点:
-
基本特性:
- 函数名与类名相同。
- 无返回值。(返回值什么都不用给,也不需要写void)
- 对象实例化时,系统就会自动调用对应的构造函数。
- 构造函数可以重载。
-
进阶特性:
-
如果类中没有显式定义的构造函数,C++编译器就会自动生成一个无参的默认构造函数,一旦用户显示定义,编译器将不再生成。
-
无参构造函数、全缺省构造函数、我们不写构造时,编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数中,有且仅有一个存在,不能同时存在。无参构造函数和全缺省构造函数,虽然构成函数重载,但是调用时会存在歧义。
-
注意:很多人会认为默认构造函数,是编译器默认生成的函数叫默认构造,实际上,无参构造函数、全缺省构造函数也是默认构造。
总结一下,就是不传实参就可以调用的构造函数,就叫默认构造函数。
- (难点❗️)我们不写,而编译器默认生成的构造:
- 对内置类型成员变量的初始化没没有要求,也就是说,是否初始化是不确定的,看编译器。
- 对于自定义类型成员变量 ,要求调用这个成员变量的 默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决,初始化列表,下个章节再细细讲解。
注:C++把类型分为内置类型(基本类型)和自定义类型。
内置类型:语言提供的原生数据类型,如:int/char/double/指针等。
自定义类型:我们使用class/struct等关键字自己定义的类型。
2.1 无参构造函数(默认构造之一)
❗️注意,无参自动调用函数时,不需要加括号。
若给无参加括号,无法区分是在定义对象 还是 函数声明,所以不能给无参加括号
cpp
#include<iostream>
using namespace std;
class Date
{
public:
// 1. 无参构建函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//自动调用,无参不用加括号
d1.Print();
return 0;
}
2.2 带参构造函数(注,不算默认构造函数)
cpp
#include<iostream>
using namespace std;
class Date
{
public:
// 2. 定义带参函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d2(2026, 4, 6);//带参把括号加到后面
d2.Print();
return 0;
}
2.3 全缺省构造函数(默认构造之一)
会和无参函数产生调用歧义,功能上也可以取代带参函数
所以用了全缺省就不用无参和带参了
cpp
#include<iostream>
using namespace std;
class Date
{
public:
// 3. 全缺省构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();// 结果:1/1/1
Date d2(2026, 4, 6);
d2.Print();// 结果:2026/4/6
Date d3(2026);
d3.Print();// 结果:2026/1/1
return 0;
}
2.4 编译器自己生成(默认构造之一 · 难点❗️)
2.4.1 成员变量为内置类型
cpp
#include<iostream>
using namespace std;
class Date
{
public:
//4. 什么都不写,编译器自己生成
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();//随机值
return 0;
}
2.4.2 成员变量为自定义成员变量
编译器自己生成的构造,只做相对浅显的初始化操作,因此大部分构造函数的初始化操作,都需要我们自己手搓;
但是!当类的成员变量,只包含自定义成员变量时,编译器自己生成的构造,就足够了。
例:用两个栈(Stack)来实现队列(MyQueue)
cpp
#include<iostream>
using namespace std;
//自己实现栈,需要自己写
typedef int STDataType;
class Stack
{
public:
//构造函数
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
private:
STDataType* _a;
size_t _top;
size_t _capacity;
};
//两个栈实现队列 - 不需要自己写
class MyQueue
{
public:
//编译器默认生成 MyQueue 的构造函数,调用了 Stack 的构造
//完成了两个成员的初始化
private:
Stack pushst;//类类型 - 调用 Stack 的默认构造初始化✅️
Stack popst;//类类型 - 调用 Stack 的默认构造初始化✅️
//下面这个int x;只是为了说明内置类型是浅初始化
//实际用两个栈实现队列时不需要写下面这句代码
// int x; //内置类型 - 浅初始化(可能是随机值,也可能为0)
};
⭕️原理 :
编译器自己生成的构造 会调用每个自定义类型成员变量的【默认构造函数初始化】。
注: 对内置类型成员变量 的初始化没有要求,内置类型成员变量是随机值还是0,其实是不确定的,看编译器决定。
补充概念- 成员对象:成员变量里,自定义类型的对象。
2.5 总结
总结:大多数情况,构造函数都需要我们自己去实现 。
当类的成员变量,只包含自定义成员变量时,且自定义成员变量有默认构造时,编译器自动生成函数函数,就能用。(类似 MyQueue 且 Stack 有默认构造时,MyQueue 靠编译器自动生成构造函数就够了)。
3. 析构函数
析构函数 与 构造函数 的功能相反。
析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束,栈帧销毁,它就释放了,不需要我们管。
C++规定,对象在销毁时会自动调用析构函数,完成对象中资源清理释放工作。
析构函数的功能类比我们之前 Stack 实现的 Destroy 功能,而像 Date 没有 Destroy ,其实就是没有资源需要释放,所以严格来说 Date 是不需要析构函数的。
❗️注意:
如果类中没有申请资源,析构函数可以不写,直接使用编译器生成的默认析构函数即可,如Date;
如果默认生成的析构就能用,也不需要显式写析构,如MyQueue;
但是,有资源申请时,一定要自己写析构,否则会造成资源泄露,如Stack。
3.1 析构函数的特点
- 析构函数名是类名前加上字符~(记忆方式:"~"在C语言中正好是"按位取反"的意思,而析构函数和构造函数的功能相反)
- 无参数,无返回值(这里分构造类似,也不需要加void)
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,系统会自动调用析构函数。
- 跟构造函数类似,我们不写,编译器自动生成的析构函数,对于内置联系成员不做处理,而对于自定义类型成员 则会调用他的析构函数。
- 还需要注意的是,我们显式写析构函数时,对于自定义类型成员也会调用它的析构,也就是说,自定义类型成员,无论什么情况,都会自动调用析构函数。
- 如果类中没有申请资源,析构函数可以不写,直接使用编译器生成的默认析构函数即可,如Date;如果默认生成的析构就能用,也不需要显式写析构,如MyQueue;但是,有资源申请时,一定要自己写析构,否则会造成资源泄露,如Stack。
- 一个局部域的多个对象,C++规定,后定义的先析构。
3.2 自己写析构
❗️注意:
- 析构函数无参数,无返回值,并且一个类只能有一个析构函数。
- 后定义的先析构
cpp
typedef int STDataType;
class Stack
{
public:
//构造函数
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
//析构函数
~Stack()
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
STDataType* _a;
size_t _top;
size_t _capacity;
};
int main()
{
Stack s1;
Stack s2;//s2先被析构
return 0;
}
3.3 编译器自动生成析构
💡原理:
遇到自定义类型成员,无论什么情况,编译器都会自动调用析构函数,所以,当类中包含自定义类型成员,且该成员已写好析构函数时,我们可以不管该成员。
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
//自己实现栈,需要自己写
typedef int STDataType;
class Stack
{
public:
//构造函数
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
//析构函数
~Stack()
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
STDataType* _a;
size_t _top;
size_t _capacity;
};
//两个栈实现队列 - 不需要自己写
class MyQueue
{
public:
// 编译器默认生成 MyQueue 的构造函数,调用了Stack的构造
// 编译器默认生成 MyQueue 的析构函数,调用了Stack的析构
// 显式写析构,也会自动调用 Stack 的析构
~MyQueue()
{
cout << "~MyQueue()" << endl;
}
private:
Stack pushst;//类类型 - 调用 Stack 的默认构造初始化✅️
Stack popst;//类类型 - 调用 Stack 的默认构造初始化✅️
// int x; //内置类型 - 浅初始化(随机值)
};
int main()
{
MyQueue mq;
return 0;
}
3.4 总结
后定义的函数先被析构。
如果类中没有申请资源,析构函数可以不写,直接使用编译器生成的默认析构函数即可,如Date;
如果默认生成的析构就能用,也不需要显式写析构,如MyQueue;
但是,有资源申请时,一定要自己写析构,否则会造成资源泄露,如Stack。
便利之处
有些人可能会想,构造函数和析构函数用起来十分奇怪,接下来,这边利用一道经典oj题:有效的括号,来讲解构造函数和析构函数的便利之处
代码实现:栈
cpp
#include<iostream>
using namespace std;
//自己实现栈,需要自己写
typedef char STDataType;
class Stack
{
public:
// 构造函数
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
// 析构函数
~Stack()
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
void Push(STDataType x)
{
if (_top == _capacity)
{
int newcapacity = 2 * _capacity;
STDataType* tmp = (STDataType*)realloc(_a, sizeof(STDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_capacity = newcapacity;
_a = tmp;
}
_a[_top++] = x;
}
void Pop()
{
_top--;
}
bool Empty()
{
return _top == 0;
}
STDataType Top()
{
return _a[_top - 1];
}
private:
STDataType* _a;
size_t _top;
size_t _capacity;
};
代码实现:判断括号是否有效
cpp
//判断左右括号是否有效
bool isValid(const char* s)
{
Stack st;
// 自动调用默认构造函数初始化 ✅️
while (*s)
{
//左括号入栈
if (*s == '(' || *s == '[' || *s == '{')
{
st.Push(*s);
}
else
{
//右比左多
if (st.Empty())
return false;
char top = st.Top();
if ((top == '{' && *s != '}') ||
(top == '(' && *s != ')') ||
(top == '[' && *s != ']'))
{
return false;
}
st.Pop();
}
s++;
}
// 若不为空,则说明左比右多,不匹配
// 若为空,则数量一致,匹配
return st.Empty();
// 在C语言中,我们需要储存返回值,再释放栈内数组
// 而我们常常忘记释放内存,内存泄露是个很严重的问题,还不会报错
// 而在C++,有了析构函数后,我们就可以直接返回了✅️
//这就是C++的便利之处。
}
也就是说,在C++里有了构造函数和析构函数后,我们在代码中使用类时,就不需要额外的调用初始化函数;在返回时,也可以直接返回,不用额外去释放内存了,不用担心内存泄露。
4. 拷贝构造函数
如果一个构造函数的第一个参数,是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫拷贝构造函数,也就是说,拷贝构造是一个特殊的构造函数。
拷贝构造的特点:
- 拷贝构造函数,是构造函数的一个重载。
- 拷贝构造函数的第一个参数,必须是类类型对象的引用,且任何额外的参数都有默认值,使用传值方式传参,编译器会直接报错,因为语法逻辑上会引发无穷递归调用。
- C++规定,自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
- 若未显式定义拷贝构造 ,编译器会自动生成拷贝构造函数 。
自动生成的拷贝构造,对内置类型成员和自定义类型成员的操作如下------
对内置类型成员变量 :会完成值拷贝/浅拷贝(一个字节一个字节地拷贝)
对自定义类型成员:会调用它的拷贝构造。
这里用Date类来具体讲解拷贝构造函数的注意点:
❌️错误案例:直接传值传参
cpp
// error: "Date" 的复制构造函数不能带有 "Date" 类型的参数
Date(Date d1)//直接传值传参❌️
{
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
int main()
{
Date d1(2026, 4, 11);
Date d2(d1);
d2.Print();
return 0;
}
✅️正确示范:传递自定义类型的引用
cpp
Date(const Date& d1)//传引用✅️
{
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
int main()
{
Date d1(2026, 4, 11);
Date d2(d1);
d2.Print();
return 0;
}
❓️ask:为什么要传递引用,而不是直接传值传参呢?
⭕️答:首先要明白,传值传参的过程中,会触发拷贝构造函数。
若对拷贝构造的参数进行传值 (而不是引用),则在进入拷贝构造函数体之前,实参d1需要形成自己的临时拷贝 ------形参d,而这就是一个传值传参 的过程,此时就会再次触发拷贝构造 的机制,形成一种新的拷贝构造,又开始传值传参...最后形成无穷递归。
简单来讲就是,传值传参->拷贝构造->传值传参->拷贝构造...无穷套娃。
若使用了引用 ,则相当于只是给拷贝的值,也就是实参d1取了个别名 d,没有开辟新的内存空间(意味着没有创建新对象 ),也就是说,这并没有进行传值传参,自然也不会再次触发拷贝构造操作,保证了拷贝构造函数正常执行。
4.1 拷贝构造函数什么时候需要显式写,什么时候不需要?
Date类:这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显式实现拷贝构造。Stack类:虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝,并不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。MyQueue类:内部主要是是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显式实现MyQueue的拷贝构造。
💡小技巧:如果一个类显式实现了析构,并释放资源,那么它就需要显式写拷贝构造,否则不需要。
4.1.1 为什么要进行深拷贝?
接下来以 Stack 类来举例:
⭕️答:
Stack 不显式实现拷贝构造,用自动生成的拷贝构造完成浅拷贝,会导致 st1 和 st2 里的 _a 指针指向同一块资源 ,析构时会析构两次,程序崩溃。
所以需要我们进行深拷贝。
❌️错误案例:无显式实现拷贝构造,指向资源的指针导致多次析构,程序崩溃。
cpp
#include<iostream>
using namespace std;
//自己实现栈,需要自己写
typedef char STDataType;
class Stack
{
public:
// 构造函数
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
// 析构函数
~Stack()
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
void Push(STDataType x)
{
if (_top == _capacity)
{
int newcapacity = 2 * _capacity;
STDataType* tmp = (STDataType*)realloc(_a, sizeof(STDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_capacity = newcapacity;
_a = tmp;
}
_a[_top++] = x;
}
private:
STDataType* _a;
size_t _top;
size_t _capacity;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
// Stack 不显式实现拷贝构造,用自动生成的拷贝构造完成浅拷贝
// 会导致 st1 和 st2 里的 _a 指针指向同一块资源
// 析构时会析构两次,程序崩溃
Stack st2(st1);
return 0;
}
由此可知,就需要我们进行深拷贝。
4.1.2 如何进行深拷贝?
✅️正确示范 :显式实现拷贝构造。
代码实现:
- 开辟同样大的内存空间
- 拷贝数据
cpp
#include<iostream>
using namespace std;
//拷贝构造函数
// st2(st1) - 需要深拷贝
Stack(const Stack& st)
{
// 需要对_a 指向资源创建同样大的资源再拷贝值
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
// 拷贝数据
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}
4.2 什么时候会调用拷贝构造?
总结:只要出现 用一个同类型的类对象,去初始化另一个新对象,就会调用拷贝构造。
4.2.1 用 旧自定义类型对象 初始化 新自定义类型对象
代码演示:
cpp
A a1;
A a2 = a1;// 拷贝构造
A a3(a1);// 拷贝构造
4.2.2 自定义类型对象 作为 函数参数,进行值传递
代码演示:
cpp
void func(A a)// 参数是值传递
{
//...
}
int main()
{
A a1;
func(a1);//将a1拷贝一份给a -> 调用拷贝构造
return 0;
}
注:若传的是对象的引用 ,则不会调用拷贝构造
代码演示:
cpp
void func(const A& a)// 传对象的引用
{
//...
}
int main()
{
A a1;
func(a1);
// a是a1的别名,没有创建新对象,不会调用拷贝构造
return 0;
}
4.2.3 函数返回一个 局部自定义类型对象(值返回)
代码演示:
cpp
A func()
{
A a;
return a;// 利用a拷贝出临时对象返回->调用拷贝构造
}
4.3 传值返回和传引用返回 在拷贝构造的区别
传值返回:会产生一个临时对象拷贝构造。
传值引用返回:返回的是返回对象的别名(引用),没有产生拷贝。
但如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针。
传引用返回可以减少拷贝,但一定要确保返回对象,在当前函数结束后还在,才能使用引用返回。
- 传值返回
cpp
A func1()
{
A a1;
return a1;//利用a1拷贝出一个临时对象后返回
}
int main()
{
A ret = func1();
return 0;
}
可以返回已创建的类对象的引用的值
cpp
A func2(A& a)
{
return a;
}
int main()
{
A a;
A ret = func2(a);
return 0;
}
- 传引用返回
❌️错误做法:返回局部变量的引用
cpp
A& func2()
{
A a2;
return a2;// 返回a2的别名,a2出函数后就销毁,相当于野引用
}
int main()
{
A ret = func2();
return 0;
}
改进方法:
✅️创建局部变量时,加上static 延长生命周期
cpp
A& func2()
{
static A a2;//static延长了a2的生命周期,使其出函数后不销毁
return a2;
}
int main()
{
A ret = func2();
return 0;
4.4 总结
当函数的参数为自定义类型 时,最好不要使用传值传参,而是要传它的引用!
5. 赋值运算符重载
5.1 运算符重载
-
当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式,指定新的含义。
- C++规定类类型对象 使用运算符时,必须转换成调用对应运算符重载。
- 若没有对应的运算符重载,则会编译报错。
-
运算符重载,是具有特殊名字的函数 ,它的名字是由
operator和后面要定义的运算符共同构成。和其他函数一样,它也有其返回类型 和参数列表 ,以及函数体。 -
重载运算符函数的参数个数 和 该运算作用的运算对象数量一样多。
- 一元运算符有一个参数,二元运算符有两个参数。
- 二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
-
如果一个重载运算符函数,是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
-
运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
-
❌️注意:不能连接语法中没有的符号,来创建新的运算符,比如:
operator@ -
重点 :
.*::sizeof?:.注意,以上5个运算符,不可以构成重载(选择题常考❗️) -
重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:
int operator(int x, int y) -
一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载
operator-就有意义,但是重载operator+就没有意义。
重载为全局时,面临对象访问私有成员变量的问题,有以下方法可以解决:
- 成员放公有(不推荐)
- Date提供getxxx函数
- 友元函数(在类和对象下会讲)
- 重载为成员函数(最推荐)
以下先用成员放公有的方式来展示运算符重载,实际不建议用!
cpp
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//private:
int _year;
int _month;
int _day;
};
// 重载为全局时,面临对象访问私有成员变量的问题
// 有几种方法可以解决:
// 1、成员放公有 - 不推荐
// 2、Date提供getxxx函数
// 3、友元函数
// 4、重载为成员函数
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
// 运算符重载函数可以显式调用
operator==(d1, d2);
// 编译器会转换成 operator==(d1, d2);
d1 == d2;
return 0;
}
⭐最推荐的方法:重载为成员函数
❗️注意:如果一个重载运算符函数,是成员函数 ,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个 。
(注:如果全局中的重载和成员函数重载是一样的,则编译器会优先调用成员函数中的重载)
代码如下:
cpp
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//重载为成员函数
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
d1.operator==(d2);
// 编译器会转换成 d1.operator==(d2);
d1 == d2;
return 0;
}
5.2 赋值运算符重载
注意区分 赋值重载拷贝和拷贝构造:
赋值运算符重载 :是一个默认成员函数,用于完成两个已经存在的对象 直接拷贝赋值。
拷贝构造 :一个对象拷贝初始化给另一个要创建的对象
代码举例:
cpp
// 赋值重载
//d1 = d2
void operator=(const Date& d)//写成 Date d也可以
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int main()
{
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
// 赋值重载拷贝:两个已存在的对象直接拷贝赋值
d1 = d2;
// 拷贝构造:⼀个对象拷贝初始化给另⼀个要创建的对象。
Date d3(d2);
Date d4 = d2;
return 0;
}
赋值运算符重载的特点:
- 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算符重载的参数建议 写成
const 当前类类型引用,否则传值传参会有拷贝。(注:可以进行传值传参,在这里并不会引起无穷递归,但为了减少拷贝,还是建议去传 引用)
赋值重载可以传值(类类型对象)的原因:
若传值
d2,则类类型对象会调用拷贝构造,形成形参d,然后返回,进入到赋值重载中,不会进行无穷递归。而拷贝构造,是类类型对象会调用拷贝构造,形成形参
d,返回后仍在进行拷贝构造,会造成无穷递归。
- 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值的目的是为了支持连续赋值场景。
⭐补充 - 有返回值的情形
形如 int i = j = k = 1; 这种连续赋值的情况,所以,当需要连续赋值 时,赋值运算符重载也是有返回值 的。
代码演示:
cpp
// d3 = d1;
Date& operator=(Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
❓️ask1:为什么要返回*this?*this代表什么?
⭕️答:为了能够进行连续赋值 ,我们需要返回
d3,this实际上是d3的指针,为了返回d3我们需要对this指针解引用 ,所以*this就是指的是d3。注:虽然参数中不可以显式写
this指针,但是在成员函数体内部是可以显式写this指针的。
❓️ask2:为什么要返回Date的引用(当前类类型的引用)?
⭕️答:因为
d3出当前函数后并不会销毁,可以传引用 ,直接返回d3本身,就不需要再进行拷贝操作了,提升效率。若传值返回,则还需要多余的拷贝一遍临时对象再返回,拉低效率。
- 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节地拷贝),对自定义类型成员变量会调用它的赋值重载函数。
5.2.1 赋值运算符重载 什么时候需要显式写,什么时候不需要?
(其实跟4.1的道理一样)
Date类:这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显式实现运算符重载。Stack类:虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载 完成的值拷贝/浅拷贝,并不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。MyQueue类:内部主要是是自定义类型Stack成员,编译器自动生成的赋值运算符重载 会调用Stack的拷贝构造,也不需要我们显式实现MyQueue的赋值运算符重载。
💡小技巧 :如果一个类显式实现了析构,并释放资源,那么它就需要显式写赋值运算符重载,否则不需要。
6. 取地址运算符重载
6.1 const 成员函数
-
将
const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。- 非
const成员函数:void Print(); const成员函数:void Print() const;
- 非
-
const实际修饰该成员隐含的this指针,表明在该成员函数中,不能对类的任何成员进行修改。- 例如,
const修饰Date类的Print成员函数,Print隐含的this指针由Date* const this,变为const Date* const this
- 例如,
✅️非const对象 调用 const成员函数(权限缩小)
对象为非const ,本身可以改变,但是在const成员函数中,就不可以改变了,体现了权限缩小,因此是合法的。
❌️const对象 调用 非const成员函数 (权限扩大)
对象为const,本身不可改变,在非const成员函数中,有可能发生改变!权限扩大,因此是非法的。
- 所以,如果成员函数中的成员不需要改变,我们就可以给成员函数加上
const,保证其安全性。
6.2 取地址重载
取地址运算符重载,分为普通取地址运算符重载 和 const取地址运算符重载,一般这两个函数编译器自动生成的就够用啦,不需要显式实现。
除非一些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址。
📃代码展示:
cpp
class Date
{
public:
Date* operator&() // 函数1
{
return this;
// return nullptr;
}
const Date* operator&()const // 函数2
{
return this;
// return nullptr;
}
private:
int _year;
int _month;
int _day;
};
注:函数1和函数2构成函数重载,可以同时存在。
虽然语法上只写2并不会报错(无论是传入const对象,还是非const对象,都不会报错)但是,请注意,函数2返回的是const类型的地址 !
若传入的是非const对象,返回的 const类型的地址,那么,逻辑上岂不是乱套了吗?所以还需要函数1。
而且编译器调用时,也会去调用最匹配的函数。