运算重载符
概念: 运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数原型:
返回值 operator操作符(参数列表)
注意:
- 运算符重载,就是对已有的运算符重新进行定义,赋予其另外一种功能,以适应不同的数据类型
- 不能通过重载操作符来创建新的操作符,如operator@
- 必须含有一个类类型或枚举类型的操作数
- 不能改变内置类型的含义
- 重载函数如果写成成员函数,左侧第一个参数隐含了一个Date* const this的形参,限定为第一个形参
- 重载函数如果写成全局,那么左边是你输入的第一个参数,右边是第二个参数
- .*,::,?:,sizeof,.这五个运算符不能重载
注意以下几点:
- 除了赋值号(=)之外,基类中被重载的操作符都将被派生类继承
- =,[],(),->操作符只能通过成员函数进行重载
- << 和 >> 操作符最好通过友元进行重载
- 不要重载&&和||运算符,因为无法实现短路规则
选择作为成员或非成员
当我们定义重载的运算符时,必须首先决定是将其声明为类的成员函数还是声明为一个普通的非成员函数。在某些时候我们别无选择,因为有的运算符必须作为成员;另一些情况下,运算符作为普通函数比作为成员更好。
下面的准则有利于我们在运算符定义为成员函数还是普通的非成员函数做出抉择:
- 赋值(=)、下标([ ])、调用(())和成员访问箭头)(->),运算符必须是成员
- 复合运算符一般来说是成员,但是非必须
- 改变成员状态的运算符或者给定类型密切相关的运算符,比如递增、递减和解引用运算符,通常应该是成员
- 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此他们通常应该是普通的非成员对象
赋值运算符重载
我们可以重载赋值运算符,无论形参的类型是什么,赋值运算符都必须定义为成员函数
赋值运算符,赋值之后,左侧运算对象和右侧运算对象的值相等,并且运算应该返回它左侧运算对象的一个引用
特性:
- 返回是*this
- 如果没有显示定义,编译器也会生成一个,按字节序拷贝
Date& operator=(const Date& d) { // 检测是否自己给自己赋值 if (this == &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; }
实例:
#define _CRT_SECURE_NO_WARNINGS #include<iostream> //引入头文件 #include<string>//C++中的字符串 using namespace std; //标准命名空间 class Maker { public: Maker() { id = 0; age = 0; } Maker(int id, int age) { this->id = id; this->age = age; } public: int id; int age; }; void test01() { Maker m1(10, 20); Maker m2; m2 = m1; //默认的赋值运算符重载函数进行了简单的赋值操作 //就类似于字节序的浅拷贝 cout << m2.age << m2.id << endl; } class Student { public: Student(const char * name) { pName = new char[strlen(name)+1]; strcpy(pName, name); } //防止浅拷贝 Student(const Student& stu) { pName = new char[strlen(stu.pName) + 1]; strcpy(pName, stu.pName); } //重写赋值运算符重载函数 //为什么要返回引用 Student& operator =(const Student& stu)//第一个参数默认是this指针 { //1.不能确定this指向的空间是否能装下stu中的数据,所以先释放this指向的空间 if (this->pName!= NULL) { delete[] this->pName; this->pName = NULL; } //2.释放了之后再来申请堆区空间,大小由stu决定 this->pName = new char[(strlen(stu.pName) + 1)]; //3.拷贝函数 strcpy(this->pName, stu.pName); //返回对象本身 return *this;//this中存放的是对象的地址,对地址取*表示对象本身 } void printfStudent() { cout << this->pName << endl; } ~Student() { delete[] pName; pName = NULL; } public: char* pName ; }; void test02() { Student s1("悟空"); Student s2("唐僧"); Student s3("八戒"); s1 = s2 = s3; s1.printfStudent(); s2.printfStudent(); s3.printfStudent(); cout << &(s2 = s3) << endl; cout << &s2 << endl; } int main() { test01(); cout << "-------------------------------" << endl; test02(); system("pause"); return EXIT_SUCCESS; }
运行结果如下:
在代码中留下了一个问题,为什么重载的赋值操作运算符要返回左侧对象的引用呢?
从s1 = s2 = s3开始说,赋值运算符本来的寓意是s3赋值s2,s2赋值s1,也就是说s2=s3这个表达式要返回原来s2的对象,如果不是引用那么s2将会生成一个新的对象,所以要返回引用
本质:Student &operator =(const Student& stu)这个函数返回的是return *this,如果加上引用Student & = *this;实际上就是this空间重新取了个名字
但是如果不是引用,那么就是Student = *this,会重新生成一个对象。就比如 int &a = b和int a = b的区别,第一种返回的还是原来的b,第二个返回的就是新的a
算术运算符(加号)重载
一般来说,我们把算数和关系运算符定义成非成员函数以允许对左侧或者右侧的运算对象进行转换。因为这些运算符一般不需要改变运算对象的状态,所以形参都是常量的引用。
下面的代码从成员函数和非成员函数都介绍算术运算符的重载
#define _CRT_SECURE_NO_WARNINGS #include<iostream> //引入头文件 #include<string>//C++中的字符串 using namespace std; //标准命名空间 class Maker { public: Maker(int id, int age) { this->age = age; this->id = id; } //1.成员函数,就需要一个参数,这个参数就是加号的右边 Maker operator+(const Maker& m2) { //加号左边变成this Maker temp(this->id + m2.id, this->age + m2.age); return temp;//返回的时候调用拷贝构造 } public: int id; int age; }; //2.全局的方式 //编译器会调用这个函数,编译器会检查参数是否对应 Maker operator+(Maker& m1, Maker &m2) { Maker temp(m1.id + m2.id, m1.age + m2.age);//调用有参构造 return temp;//返回的时候会调用拷贝构造 } void test() { Maker m1(1, 20); Maker m2(2, 22); //m1+m2显然是不可以的,要重载运算符 Maker m3 = m1 + m2;//编译器看到两个对象相加,编译器会去找有没有operator+函数 cout << m3.id << endl; cout << m3.age << endl; } int main() { test(); system("pause"); return EXIT_SUCCESS; }
运行结果如下:
关系运算符重载
bool operator == (Maker &m) { if(this->id == m.id && this->age == m.age){ return true; } return false; }
输出和输入运算符重载
输出 << 运算符 的重载
输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符
输出运算符的第一个形参是一个非常量ostream对象的引用,之所以ostream是个非常量是因为向流写入内容会改变其状态;而该形参是引用是因为我们无法直接复制一个ostream对象
第二个形参一般来说是一个常量的引用,该常量就是我们想要打印的类类型。第二个形参是引用是因为我们希望避免复制实参;而之所以该形参可以是常量是因为通常情况下打印对象不会改变对象的内容
#define _CRT_SECURE_NO_WARNINGS #include<iostream> //引入头文件 #include<string>//C++中的字符串 using namespace std; //标准命名空间 class Maker { //友元函数 friend ostream& operator <<(ostream& out, Maker& m); public: Maker(int id, string name) { this->id = id; this->name = name; } private: int id; string name; }; //1.形参和实参是一个对象,因为用的是引用,只是对同一块空间取别名 //2.对 << 运算符进行重载,左边是ostream对象,右边是Maker对象 //3.一定要用引用,因为ostream把拷贝构造函数私有化了 //4.如果要和endl一起用,那么就必须返回ostream ostream& operator << (ostream& out, Maker& m) { cout << m.id << " " << m.name << endl; return out; } void test01() { Maker m(10, "小花"); cout << m; cout << endl; /* endl是一个函数 operator << (函数指针) operator(endl)把endl函数传进来 */ cout << 10;//在内部重载了基础数据类型 } int main() { test01(); system("pause"); return EXIT_SUCCESS; }
输入 >> 运算符的重载
输入运算符必须处理输入可能失败的情况,而输出运算符不需要
输入运算符的第一个形参是运算符想要读取的流的引用,第二个形参是将要读入到的(非常量)对象的引用。该运算符通常会返回某个给定流的引用。第二个形参之所以必须是个非常量是因为输入运算符本身的目的就是将数据读入到这个对象中
#define _CRT_SECURE_NO_WARNINGS #include<iostream> //引入头文件 #include<string>//C++中的字符串 using namespace std; //标准命名空间 void test01() { int a; cin >> a;//从键盘中获取数据 cout << a << endl; } class Maker { //访问私有成员要设置为友元函数 friend istream& operator >>(istream& in, Maker& m); public: Maker(string name, int age) { this->name = name; this->age = age; } int getAge() { return age; } private: string name; int age; }; //重载>>右移运算符 //同一个对象取别名 istream& operator >>(istream& in, Maker& m) { in >> m.age; in >> m.name; return in; } void test02() { Maker m("悟空", 15); Maker m2("悟空2", 25); cin >> m >> m2; cout << m.getAge() << endl; cout << m2.getAge() << endl; } int main() { test02(); system("pause"); return EXIT_SUCCESS; }
前置++和后置++运算符重载
前置++和后置++最大的区别就是返回值不同,前置是返回变化之后的值,后置是返回变化之前的值,两个在重载是,都是operator++,我们如何区分呢?
一般operator++默认是前置++,为了区分后置++,我们通常会在参数列表加一个占位参数,且这个参数必须是int类型的,从而构造成函数重载。
- 前置和后置运算符应该返回递增或者递减后对象的引用,因为是在同一个对象上进行的操作
- 后置运算应该返回对象的原值,返回的形式是一个值而并非是引用
#define _CRT_SECURE_NO_WARNINGS #include<iostream> //引入头文件 #include<string>//C++中的字符串 using namespace std; //标准命名空间 class Person { public: Person(string name, int age) { this->_name = name; this->_age = age; } // 前置++ Person& operator++() { //传入一个this指针 this->_age++; return *this;// 返回变化之后的值,传引用 } // 后置++,占位参数(必须是int) Person operator++(int) { //后置++,先返回,后++ Person ret = *this;//调用拷贝构造,拷贝this的一个临时备份,用于返回 this->_age++;//this也就是传入的对象中的_age值会改变 return ret;// 返回变化之前的值,传值,返回的时候也调用的是拷贝构造 } void Print() { cout << _name << "-" << _age << endl; } private: string _name; int _age; }; int main() { Person p("wxj", 19); p.Print(); cout << "前置++" << endl; // 前置++ Person ret = ++p; ret.Print(); p.Print(); cout << "后置++" << endl; // 后置++ ret = p++; ret.Print(); p.Print(); return 0; }
运行结果如下:
值得注意的是成员函数的操作运算符左边第一个参数是Date* const this一个this指针,所以只要对this指针进行操作,并且返回的是引用,那么依旧是同一个对象的值发生改变并且里面的值也会发生改变。
数组下标重载
表示容器的类通常可以通过元素在容器中的位置访问元素,这些类一般会定义下标运算符operator []
- 下标运算符必须是成员函数
如果一个类包含下标运算符,那么它通常会定义两个版本:一个返回普通引用,另一个是类的常量成员并且返回常量引用
返回值类型 & operator[ ] (参数); const 返回值类型 & operator[ ] (参数) const;
使用第一种声明方式,[ ]
不仅可以访问元素,还可以修改元素。使用第二种声明方式,[ ]
只能访问而不能修改元素。在实际开发中,我们应该同时提供以上两种形式,这样做是为了适应 const 对象,因为通过 const 对象只能调用 const 成员函数,如果不提供第二种形式,那么将无法访问 const 对象的任何元素。
#include <iostream> using namespace std; class Array{ public: Array(int length = 0); ~Array(); public: int & operator[](int i); const int & operator[](int i) const; public: int length() const { return m_length; } void display() const; private: int m_length; //数组长度 int *m_p; //指向数组内存的指针 }; Array::Array(int length): m_length(length){ if(length == 0){ m_p = NULL; }else{ m_p = new int[length]; } } Array::~Array(){ delete[] m_p; } int& Array::operator[](int i){ return m_p[i]; } const int & Array::operator[](int i) const{ return m_p[i]; } void Array::display() const{ for(int i = 0; i < m_length; i++){ if(i == m_length - 1){ cout<<m_p[i]<<endl; }else{ cout<<m_p[i]<<", "; } } } int main(){ int n; cin>>n; Array A(n); for(int i = 0, len = A.length(); i < len; i++){ A[i] = i * 5; } A.display(); const Array B(n); cout<<B[n-1]<<endl; //访问最后一个元素 return 0; }
需要说明的是,B 是 const 对象,如果 Array 类没有提供 const 版本的operator[ ]
,那么编译器就会报错。虽然只是读取对象的数据,并没有试图修改对象,但是它调用了非 const 版本的operator[ ]
,编译器不管实际上有没有修改对象,只要是调用了非 const 的成员函数,编译器就认为会修改对象(至少有这种风险)。
例子1:数组类
在.h文件中将数组类的功能和成员进行声明
#pragma once #define _CRT_SECURE_NO_WARNINGS #include<iostream> //引入头文件 #include<string>//C++中的字符串 using namespace std; //标准命名空间 class MyArray { public: MyArray(); //拷贝函数,防止浅拷贝,因为有pArray需要开辟堆区空间 MyArray(const MyArray& arr); MyArray(int capacity, int val = 0);//有参构造 //重写赋值运算符重载函数 MyArray& operator = (const MyArray& m); //要能当左右值 int& operator[](int dex); ~MyArray(); //头插法 void PushFront(int val); //尾插 void PushBack(int val); //头删 void PopFront(); //尾删 void PopBack(); //获取数组元素个数 int Size(); //获取数组容量 int Capacity(); //指定位置插入元素 void Insert(int pos, int val); //获取指定位置的值 int& Get(int pos); //在指定位置修改 void Set(int pos, int val); private: int* pArray;//指向堆区空间,存储数据 int mSize;//元素个数 int mCapacity;//容量 };
在.c文件中对声明的函数进行封装,并进行测试
#include "MyArray.h" //构造函数 MyArray::MyArray() { this->mCapacity = 20; this->mSize = 0; this->pArray = new int[this->mCapacity]; for (int i = 0; i < this->mCapacity; i++) { this->pArray[i] = 0; } } //析构函数 MyArray::~MyArray() { if (this->pArray != NULL) { delete[] this->pArray; this->pArray = NULL; } } //拷贝构造函数 MyArray::MyArray(const MyArray& arr) { this->mCapacity = arr.mCapacity; this->mSize = arr.mSize; //申请空间 this->pArray = new int[arr.mCapacity]; //拷贝数据 for (int i = 0; i < this->mSize; i++) { this->pArray[i] = arr.pArray[i]; } } MyArray::MyArray(int capacity, int val) { this->mCapacity = capacity; this->mSize = capacity; this->pArray = new int[capacity]; for (int i = 0; i < this->mSize; i++) { this->pArray[i] = val; } } //头插法 void MyArray::PushFront(int val) { //判断容量是否已经满了 if (this->mSize == this->mCapacity) { return; } //头插法,数组元素全部后移 for (int j = this->mSize - 1; j >= 0; j--) { this->pArray[j + 1] = this->pArray[j]; } //空出0的位置 this->pArray[0] = val; //维护元素个数 this->mSize++; } //尾插 void MyArray::PushBack(int val) { //判断容量是否已经满了 if (this->mSize == this->mCapacity) { return; } //尾插法 this->pArray[this->mSize] = val; this->mSize++; } //头删 void MyArray::PopFront() { //删除第一个元素的数据,实际上就是整体前移,进行覆盖 if (this->mSize == 0) { return; } for (int i = 0; i < mSize - 1; i++) { this->pArray[i] = this->pArray[i + 1]; } this->mSize--; } //尾删 void MyArray::PopBack() { if (this->mSize == 0) { return; } this->mSize--; } //获取数据元素个数 int MyArray::Size() { return this->mSize; } //获取数组容量 int MyArray::Capacity() { return this->mCapacity; } //指定位置插入 void MyArray::Insert(int pos, int val) { //满了就无法添加 if (this->mSize == this->mCapacity) { return; } //如果位置不对,就插入到尾部 if (pos<0 || pos>this->mCapacity) { pos = this->mSize; } for (int i = this->mSize - 1; i >= pos; i++) { this->pArray[i + 1] = this->pArray[i]; } this->pArray[pos] = val; //将pos的位置空出来 this->mSize++; } //获取指定位置的值 int& MyArray::Get(int pos) { return this->pArray[pos]; } //在指定位置修改 void MyArray::Set(int pos, int val) { if (pos<0 || pos>mCapacity - 1) { return; } this->pArray[pos] = val; } //重写赋值运算符重载函数 MyArray& MyArray::operator=(const MyArray& m) { //1.释放原来的空间 if (this->pArray != NULL) { delete[] this->pArray; this->pArray = NULL; } this->mCapacity = m.mCapacity; this->mSize = m.mSize; //2.申请空间,大小m决定 this->pArray = new int[m.mCapacity]; //3.拷贝数据 for (int i = 0; i < this->mCapacity; i++) { this->pArray[i] = m.pArray[i]; } return *this; } //重写下标[]//要能当左值 int& MyArray::operator[](int i) { //赋值的时候++ /* for (int i = 0; i < 20; i++) { arr[i] = i + 10; } for (int i = 0; i < 20; i++) { cout << arr[i] << endl; } */ //this->mSize++; if (this->mSize <= i) { this->mSize++; } return this->pArray[i]; } void printMyArray(MyArray& arr) { for (int i = 0; i < arr.Size(); i++) { cout << arr.Get(i) << " "; cout << endl; } } void test01() { MyArray arr(20, 1);//调用有参构造 printMyArray(arr); //修改数组中的值 for (int i = 0; i < arr.Size(); i++) { arr.Get(i) = i + 100; } printMyArray(arr); //指定位置修改值 arr.Set(2, 0); printMyArray(arr); //测试是否发生浅拷贝 MyArray arr2 = arr; printMyArray(arr2); } void test02() { MyArray arr; //添加元素 for (int i = 0; i < 10; i++) { //尾插 arr.PushBack(i + 10); } //头插 for (int i = 0; i < 9; i++) { arr.PushFront(i + 20); } //指定位置插入 arr.Insert(10, 100); //打印 printMyArray(arr); arr.PopBack(); arr.PopFront(); printMyArray(arr); } int main() { test01(); test02(); system("pause"); return EXIT_SUCCESS; }
例子2:字符串类
在.h文件中将字符串类的功能和成员进行声明
#pragma once #define _CRT_SECURE_NO_WARNINGS #include<iostream> //引入头文件 #include<string>//C++中的字符串 using namespace std; //标准命名空间 //要重载的内容 /* MyString s1; MyString s1; MyString s3=s1+s2;重载加号,拷贝构造 MyString s3=s1+“hello”;重载加号 s4+=s3 重载+= s4+="hello" 重载+= cout<<s4<<endl;重载<< cin>>s4;重载>> */ class MyString { friend ostream& operator<<(ostream& out, MyString& str); friend istream& operator>>(istream& in, MyString& str); public: MyString(); //用户可以设定初始化字符串,n个c组成的字符串 MyString(int n, char c); MyString(const MyString& str); MyString& operator=(const MyString& str); MyString operator+(const MyString& str); MyString operator+(const char* s); MyString& operator+=(const MyString& str); MyString& operator+=(const char* s); int Size(); char& operator[](int index); ~MyString(); private: char* pM;//指向堆区空间 int mSize; };
在.c文件中对声明的函数进行封装,并进行测试
#include "Mystring.h" //用户可以设定初始化字符串,n个c组成的字符串 MyString::MyString() { this->pM = new char[1]; this->pM[0] = ' '; this->mSize = 0; } //有参构造 MyString::MyString(int n, char c) { //多留出一个位置最后加上' ' this->pM = new char[n+1]; for (int i = 0; i < n; i++) { this->pM[i] = c; } this->pM[n] = ' '; this->mSize = n; } //拷贝构造 MyString::MyString(const MyString& str) { this->pM = new char[strlen(str.pM) + 1]; strcpy(this->pM, str.pM); this->mSize = str.mSize; } //赋值操作,返回原来的对象 MyString& MyString::operator=(const MyString& str) { //1.释放原来的空间 delete[] this->pM; this->pM = NULL; //2.申请空间 this->pM = new char[strlen(str.pM) + 1]; //3.拷贝数据 strcpy(this->pM, str.pM); this->mSize = str.mSize; return *this; } MyString MyString::operator+(const MyString& str) { //s3=s1+s2,this是s1,str是s2 //1.获取s2要开辟的空间大小 int newlen = this->mSize + str.mSize + 1; //2.定义一个临时变量 MyString tmp; if (tmp.pM != NULL) { //2.释放原来的空间 delete[] tmp.pM; tmp.pM = NULL; } //3.申请新的空间 tmp.pM = new char[newlen]; memset(tmp.pM, 0, newlen); tmp.mSize = this->mSize+str.mSize; //4.追加字符到空间当中 strcat(tmp.pM, this->pM); strcat(tmp.pM, str.pM); return tmp;//返回的时候会调用拷贝构造 } MyString MyString::operator+(const char *s) { int newlen = this->mSize + strlen(s); //开辟空间 char* newspace = new char[newlen + 1]; memset(newspace, 0, newlen + 1); //追加数据到空间 strcat(newspace, this->pM); strcat(newspace, s); MyString temp; if (temp.pM != NULL) { delete[] temp.pM; temp.pM = NULL; } temp.pM = newspace; temp.mSize = newlen; return temp; } MyString& MyString::operator+=(const MyString& str) { /* s4+=s3; */ //1.获取两个字符串的总字符个数 int newlen = this->mSize + str.mSize; //2.申请新空间 char* newspace = new char[newlen + 1]; memset(newspace, 0, newlen + 1); //3.追加数据 strcat(newspace, this->pM); strcat(newspace, str.pM); //4.释放本身的空间 if (this->pM!= NULL) { this->pM = NULL; } this->pM = newspace; this->mSize = newlen; return *this; } MyString& MyString::operator+=(const char* s) { //1.获取两个字符串的总字符个数 int newlen = this->mSize + strlen(s); //2.申请新空间 char* newspace = new char[newlen + 1]; memset(newspace, 0, newlen + 1); //3.追加数据 strcat(newspace, this->pM); strcat(newspace, s); //4.释放本身的空间 if (this->pM != NULL) { this->pM = NULL; } this->pM = newspace; this->mSize = newlen; return *this; } int MyString::Size() { return this->mSize; } char& MyString::operator[](int index) { return this->pM[index]; } MyString::~MyString() { if (this->pM != NULL) { delete[] this->pM; pM = NULL; } } //并不是类的成员函数 ostream& operator<<(ostream& out, MyString& str) { cout << str.pM; return out; } istream& operator>>(istream& in, MyString& str) { //cin>>s4 //用户输入的字符串要储存到s4.pM指向的堆区空间 //1.定义临时空间 char tmp[1024] = { 0 }; //2.获取用户输入的信息 in >> tmp; //3.释放s4的空间 if (str.pM != NULL) { delete[] str.pM; str.pM = NULL; } //4.申请新的空间 str.pM = new char[strlen(tmp) + 1]; memset(str.pM, 0, strlen(tmp) + 1); //5.拷贝用户输入的信息到堆区空间 strcpy(str.pM, tmp); str.mSize = strlen(tmp); return in; } //测试 void test() { MyString s1(10, 'a'); cout << s1 << endl; MyString s2(3, 'b'); cout << s2 << endl; MyString s3 = s1 + s2; cout << s3 << endl; MyString s4 = s3 + "hello"; cout << s4 << endl; for (int i = 0; i < s4.Size(); i++) { cout << s4[i] << endl; } } int main() { test(); system("pause"); return EXIT_SUCCESS; }