C++将运算符重载扩展到自定义的数据类型,它可以让对象操作更美观。
例如字符串string用加号(+)拼接、cout用两个左尖括号(<<)输出。
运算符重载函数的语法:返回值operator运算符(参数列表);
运算符重载函数的返回值类型要与运算符本身的含义一致。
非成员函数版本的重载运算符函数:形参个数与运算符的操作数个数相同;
成员函数版本的重载运算符函数:形参个数比运算符的操作数个数少一个,其中的一个操作数隐式传递了调用对象。
如果同时重载了非成员函数和成员函数版本,会出现二义性。
注意:
1)返回自定义数据类型的引用可以让多个运算符表达式串联起来。(不要返回局部变量的引用)
2)重载函数参数列表中的顺序决定了操作数的位置。
3)重载函数的参数列表中至少有一个是用户自定义的类型,防止程序员为内置数据类型重载运算符。
4)如果运算符重载既可以是成员函数也可以是全局函数,应该优先考虑成员函数,这样更符合运算符重载的初衷。
5)重载函数不能违背运算符原来的含义和优先级。
6)不能创建新的运算符。
类内重载操作符
重载操作符:本质上是一个函数,是对原有操作运算符的扩展,告诉编译器当遇到这个操作符并且满足使用场景,调用这个重载的函数函数名 : operator 后接 要重载的操作符,函数参数 取决于 该运算的 使用规则,顺序与类型,要保持一致返回值 :一般是要有的,为了和后续的操作符继续操作operator:C++中的关键字,重载操作符的关键字operator+:函数名
#include<iostream>
using namespace std;
class CTest {
public:
int m_a;
CTest():m_a(4){}
int operator+(/*CTest * const this*/ int a) {
return this->m_a + a;
}
int operator=(/*CTest * const this*/ int a) {
this->m_a = a;
return this->m_a;
}
int operator+=(/*CTest * const this*/ int a) {
this->m_a += a;
return this->m_a;
}
};
int main() {
CTest tst;
int aa=tst + 1;
tst = 10;
tst += 3;//15
cout << tst.m_a << endl;
tst.operator+=(3);//18 也可以显式的调用函数
return 0;
}
++左和右++的重载
//单目运算符
//不带参数,匹配的是++左
int operator++() {
return ++this->m_a;
}
//参数 写一个int,此时是右++
int operator++(int) {
return this->m_a++;
}
类外重载操作符
没有隐藏的this指针,需要开发者手动定义自定义的参数类型,顺序将不再有约束注意:是否与类内重载操作符产生歧义。参数不能有默认值,否则就改变了其使用规则。一些操作符只能在类内重载:=,[],(),->是对原有操作符的扩展,而不是重新定义,不允许创建新的操作符重新定义操作符,而不是扩展,在类外重载的时候,必须有至少一个自定义类型
#include<iostream>
using namespace std;
class CTest {
public:
int m_a;
CTest():m_a(4){}
int operator+(/*CTest * const this*/ int a) {
return this->m_a + a;
}
int operator=(/*CTest * const this*/ int a) {
this->m_a = a;
return this->m_a;
}
int operator+=(/*CTest * const this*/ int a) {
this->m_a += a;
return this->m_a;
}
};
//类外重载操作符,至少有一个自定义的操作符,而且要是引用(值传递,可能会无法修改实参;指针传递,报错)
int operator+(int a, CTest& tst) {
return tst.m_a + a;
}
//int operator+(CTest& tst,int a) {
// return tst.m_a + a;
//}
int operator++(CTest& tst) {
return ++tst.m_a;
}
int operator++(CTest& tst,int) { //同样的,为了与 ++左 区分开来,我们也要传入一个整型参数
return tst.m_a++;
}
//* ,双目运算符 ,乘法
int operator*(CTest& tst, int a) {
return tst.m_a * a;
}
//作为单目运算符 ,间接引用
int operator*(CTest& tst) { // * tst
return tst.m_a;
}
//若是直接输出对象,会没有操作符与之匹配,可以对<<进行重载 cout << tst << endl; //>>同理 cin >> tst; cout << tst << endl;
ostream& operator<<(ostream& os, CTest& tst) {
os << tst.m_a;
return os;
}
istream& operator>>(istream& is, CTest& tst) {
is>>tst.m_a;
return is;
}
若已经在类内重载了操作符,在类外再次重载相同操作符函数,则会引发歧义,通常只保留一个
可以这样进行区分类内重载操作符,与类外重载操作符,但是没有意义
tst.operator++(0);
::operator++(tst,0);
不能重载的操作符
sizeof sizeof运算符
. 成员运算符
.* 成员指针运算符
:: 作用域解析运算符
?: 条件运算符
typeid 一个RTTI运算符
const_cast 强制类型转换运算符
dynamic_cast 强制类型转换运算符
reinterpret_cast 强制类型转换运算符
static_cast 强制类型转换运算符
只能在类内重载的操作符
以下运算符只能通过成员函数进行重载:
= 赋值运算符
() 函数调用运算符
[] 下标运算符
-> 通过指针访问类成员的运算符
重载关系运算符
重载关系运算符(==、!=、>、>=、<、<=)用于比较两个自定义数据类型的大小。
可以使用非成员函数和成员函数两种版本,建议采用成员函数版本。
重载左移运算符
重载左移运算符(<<)用于输出自定义对象的成员变量,在实际开发中很有价值(调试和日志)。
只能使用非成员函数版本。
如果要输出对象的私有成员,可以配合友元一起使用。
#include<iostream>
using namespace std;
class CTest {
public:
int m_a;
CTest() :m_a(4) {}
};
//类外重载操作符
ostream& operator<<(ostream &os, CTest& tst) {
os << tst.m_a;
return os;
}
istream& operator>>(istream &is,CTest& tst) {
is >> tst.m_a;
return is;
}
int main() {
CTest tst;
cin >> tst;
cout << tst << endl;
return 0;
}
重载下标运算符
如果一个对象包含数组,可以通过重载下标运算符 [] 来操作对象中的数组,使其像操作普通数组一样方便。下标运算符必须以成员函数的形式进行重载。
下标运算符重载函数的语法如下:
- 返回值类型 &operator[](int index);
- const 返回值类型 &operator[](int index) const;
第一种声明方式允许 [] 既访问数组元素,也修改数组元素。第二种声明方式则只允许访问数组元素,而不能修改。这种重载形式使我们能够适应 const 对象,因为通过 const 对象只能调用 const 成员函数。如果不提供第二种形式,将无法访问 const 对象的任何数组元素。
在重载函数中,可以对下标进行合法性检查,防止数组越界。
#include<iostream>
using namespace std;
class CTest {
public:
int m_arr[5];
CTest() : m_arr{0,1,2} {}
int& operator[](int index) {
return m_arr[index];
}
const int& operator[](int index) const{
return m_arr[index];
}
};
int main() {
CTest tst;
tst[2] = 20;//修改元素
cout<<"tst[2]="<<tst[2]<<endl;
return 0;
}
重载赋值运算符
在 C++ 中,编译器可能会自动为类生成四个默认函数:
- 默认构造函数:一个空实现的构造函数。
- 默认析构函数:一个空实现的析构函数。
- 默认拷贝构造函数:对成员变量进行浅拷贝的构造函数。
- 默认赋值运算符:对成员变量进行浅拷贝的赋值运算符。
对象的赋值运算是指将一个已经存在的对象的值赋给另一个已经存在的对象。如果类的定义中没有重载赋值运算符,编译器会提供一个默认赋值运算符。如果类中重载了赋值运算符,编译器将不再提供默认的赋值运算符。
重载赋值运算符的语法如下:
类名& operator=(const 类名& 源对象);
需要注意的是,编译器提供的默认赋值运算符执行的是浅拷贝。如果类的对象不涉及堆内存,默认的浅拷贝赋值运算符通常是足够的。然而,如果对象包含指向堆内存的指针,那么就需要实现深拷贝。
赋值运算和拷贝构造是不同的概念:
- 拷贝构造:用于创建一个新对象,并用一个已存在的对象进行初始化。
- 赋值运算:用于将一个已存在对象的值赋给另一个已存在的对象。
#include<iostream>
using namespace std;
class CTest {
public:
int m_a;
int m_arr[5];
int* m_ptr;
CTest() :m_a(4), m_arr{0,1,2},m_ptr(nullptr) {}
int operator+(/*CTest * const this*/ int a) {
return this->m_a + a;
}
CTest& operator=(const CTest& g) {
if (this == &g) return *this;
if (g.m_ptr == nullptr) {
if (m_ptr != nullptr) {
delete m_ptr;
m_ptr = nullptr;
}
}
else {
if (m_ptr == nullptr) m_ptr = new int;
memcpy(m_ptr, g.m_ptr, sizeof(int));
}
m_a = g.m_a;
cout << "调用了重载赋值符" << endl;
return *this;
}
};
int main() {
CTest tst1;
CTest tst2;
tst2.m_ptr=new int(15);
tst1=tst2;
cout<<*tst1.m_ptr<<endl;
return 0;
}
重载new&delete运算符
重载 new 和 delete 运算符的目的是为了自定义内存分配的细节,例如使用内存池来实现快速分配和释放,减少内存碎片。建议在此之前,先学习 C 语言的内存管理函数 malloc() 和 free()。
在 C++ 中,使用 new 运算符时,编译器会进行以下操作:
- 调用标准库函数 operator new() 分配内存;
- 调用构造函数对内存进行初始化。
使用 delete 运算符时,编译器会进行以下操作:
- 调用析构函数;
- 调用标准库函数 operator delete() 释放内存。
构造函数和析构函数由编译器自动调用,我们无法控制。但是,我们可以重载内存分配函数 operator new() 和内存释放函数 operator delete()。
重载内存分配函数
重载内存分配函数的语法如下:
void* operator new(size_t size);
参数必须是 size_t 类型,返回值必须是 void*。
重载内存释放函数
重载内存释放函数的语法如下:
void operator delete(void* ptr);
参数必须是 void*(指向由 operator new() 分配的内存),返回值必须是 void。
重载的 new 和 delete 可以是全局函数,也可以是类的成员函数。当为一个类重载 new 和 delete 时,尽管不必显式地将其声明为 static,但实际上它们仍然是静态成员函数。编译器在看到使用 new 创建自定义类的对象时,会选择类成员版本的 operator new() 而不是全局版本的 new()。
此外,new[] 和 delete[] 也可以重载,语法类似于单个对象的 new 和 delete。
#include<iostream>
using namespace std;
class CTest {
public:
int m_a;
int m_b;
CTest(int a,int b){
m_a=a;
m_b=b;
cout<<"调用了构造函数"<<endl;
}
//重载new运算符
void* operator new(size_t size) {
cout << "调用了重载new运算符 Size: " << size << endl;
void* ptr = malloc(size);
cout << "申请到的内存的地址是:" << ptr << endl;
return ptr;
}
// 重载delete运算符
void operator delete(void* ptr) {
cout << "调用了类的重载的delete。"<<endl;
if (ptr == 0) return; // 对空指针delete是安全的。
free(ptr); // 释放内存。
}
};
int main() {
int* p1 = new int(3);
cout << "p1=" << (void *)p1 <<",*p1=" <<*p1<< endl;
delete p1;
CTest* p2 = new CTest(3, 8);
cout << "p2的地址是:" << p2 << "m_a:" << p2->m_a<< ",m_b:" << p2->m_b<< endl;
delete p2;
return 0;
}
内存池
#include <iostream>
#include <cstring>
using namespace std;
class CTest
{
public:
int m_a;
int m_b;
static char* m_pool; // 内存池的起始地址。
static bool initpool() // 初始化内存池的函数。
{
m_pool = (char*)malloc(18); // 向系统申请18字节的内存。
if (m_pool == 0) return false; // 如果申请内存失败,返回false。
memset(m_pool, 0, 18); // 把内存池中的内容初始化为0。
cout << "内存池的起始地址是:" << (void*)m_pool << endl;
return true;
}
static void freepool() // 释放内存池。
{
if (m_pool == 0) return; // 如果内存池为空,不需要释放,直接返回。
free(m_pool); // 把内存池归还给系统。
cout << "内存池已释放。\n";
}
CTest(int a, int b) : m_a(a), m_b(b) { cout << "调用了构造函数CTest()\n"; }
~CTest() { cout << "调用了析构函数~CTest()\n"; }
void* operator new(size_t size) // 参数必须是size_t,返回值必须是void*。
{
if (m_pool[0] == 0) // 判断第一个位置是否空闲。
{
cout << "分配了第一块内存:" << (void*)(m_pool + 1) << endl;
m_pool[0] = 1; // 把第一个位置标记为已分配。
return m_pool + 1; // 返回第一个用于存放对象的地址。
}
if (m_pool[9] == 0) // 判断第二个位置是否空闲。
{
cout << "分配了第二块内存:" << (void*)(m_pool + 9) << endl;
m_pool[9] = 1; // 把第二个位置标记为已分配。
return m_pool + 9; // 返回第二个用于存放对象的地址。
}
// 如果以上两个位置都不可用,那就直接系统申请内存。
void* ptr = malloc(size); // 申请内存。
cout << "申请到的内存的地址是:" << ptr << endl;
return ptr;
}
void operator delete(void* ptr) // 参数必须是void*,返回值必须是void。
{
if (ptr == 0) return; // 如果传进来的地址为空,直接返回。
if (ptr == m_pool + 1) // 如果传进来的地址是内存池的第一个位置。
{
cout << "释放了第一块内存。\n";
m_pool[0] = 0; // 把第一个位置标记为空闲。
return;
}
if (ptr == m_pool + 9) // 如果传进来的地址是内存池的第二个位置。
{
cout << "释放了第二块内存。\n";
m_pool[9] = 0; // 把第二个位置标记为空闲。
return;
}
// 如果传进来的地址不属于内存池,把它归还给系统。
free(ptr); // 释放内存。
}
};
char* CTest::m_pool = 0; // 初始化内存池的指针。
int main()
{
// 初始化内存池。
if (CTest::initpool() == false) { cout << "初始化内存池失败。\n"; return -1; }
CTest* p1 = new CTest(3, 8); // 将使用内存池的第一个位置。
cout << "p1的地址是:" << p1 << ",m_a:" << p1->m_a << ",m_b:" << p1->m_b << endl;
CTest* p2 = new CTest(4, 7); // 将使用内存池的第二个位置。
cout << "p2的地址是:" << p2 << ",m_a:" << p2->m_a << ",m_b:" << p2->m_b << endl;
CTest* p3 = new CTest(6, 9); // 将使用系统的内存。
cout << "p3的地址是:" << p3 << ",m_a:" << p3->m_a << ",m_b:" << p3->m_b << endl;
delete p1; // 将释放内存池的第一个位置。
CTest* p4 = new CTest(5, 3); // 将使用内存池的第一个位置。
cout << "p4的地址是:" << p4 << ",m_a:" << p4->m_a << ",m_b:" << p4->m_b << endl;
delete p2; // 将释放内存池的第二个位置。
delete p3; // 将释放系统的内存。
delete p4; // 将释放内存池的第一个位置。
CTest::freepool(); // 释放内存池。
}
重载括号运算符
括号运算符 () 也可以被重载,这样对象名就可以像函数一样使用(即函数对象或仿函数)。重载括号运算符的语法如下:
返回值类型 operator()(参数列表);
注意事项
- 括号运算符必须以成员函数的形式进行重载。
- 括号运算符重载函数具备普通函数的全部特征。
- 如果函数对象与全局函数同名,按作用域规则选择调用的函数。
函数对象的用途
- 模拟函数:在一些场景中,可以使用函数对象代替普通函数,在标准模板库(STL)中广泛应用。
- 存储信息:函数对象本质是类,可以用成员变量存放更多的信息。
- 数据类型:函数对象有自己的数据类型。
- 继承:可以通过继承来扩展函数对象的功能。
#include<iostream>
using namespace std;
class CTest {
public:
void operator()(const string &str){
cout << "调用了括号重载函数" << str << endl;
}
};
int main() {
CTest tst;
tst("你好");
return 0;
}
重载一元运算符
可重载的一元运算符
- ++ 自增
- -- 自减
- ! 逻辑非
- & 取地址
- ~ 二进制反码
- * 解引用
- + 一元加
- - 一元求反
一元运算符通常出现在它们所操作的对象的左边。但是,自增运算符和自减运算符 -- 有前置和后置之分。C++ 规定,重载 ++ 或 -- 时,如果重载函数有一个 int 形参,编译器处理后置表达式时将调用这个重载函数。
重载运算符的语法
成员函数版
- 前置:CTest& operator++();
- 后置:CTest operator++(int);
非成员函数版
- 前置:CTest& operator++(CTest&);
- 后置:CTest operator++(CTest&, int);