前言

hello,大家好,今天我们来更新关于C++类和对象接下来的部分,历经前两篇博客,我们已经来到了第三重境界“众里寻他千百度,蓦然回首,那人却在灯火阑珊处”闲言少叙,让我们开始吧。

1.运算符重载

1.1 why?

你或许已经听说过运算符重载这个名词,或许也已经掌握了一些关于运算符重载的知识,但你有没有想过,我们为什么要进行运算符重载?
我们想象这样一个场景,假设我们去市场买水果,假设那个小摊只卖苹果,你对小摊主说,给我称二斤,老板肯定知道,你是要买二斤苹果。但是如果老板既卖苹果,又卖梨,还卖桃,你对老板说,给我称二斤,老板就懵了,他不知道你要买什么,于是他就会问你,来二斤啥啊。
回到这个问题,我们之所以需要运算符重载,就是要在编译器不知道怎么计算的时候(老板不知道你要什么二斤水果),告诉编译器怎么运算。那你会问,编译器会不知道怎么运算吗?
我们来看这样一个例子:


#include<iostream>
using namespace std;
class Time
{
   
	int hour;
	int minute;
	int second;
public:
	Time(int a, int b, int c)
	{
   
		hour = a;
		minute = b;
		second = c;
	}
};
int main()
{
   
	Time T1(8,12,24);
	Time T2(8, 12, 46);
	Time T3(9, 23, 12);
	cout << T1 - T2 << endl;
}



看看报错报这一大串子,我们来分析一下为什么会报错,我们分析一下


于是,我们需要告诉编译器,关于时间的加减规则,也就是说,我们需要重新定义一下减法,于是,我们需要运算符重载。

1.2运算符重载的实现

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)

返回值类型  operator 运算符(形参表)
{
   
	……
}

1.2.1 加减乘除运算符重载示例

#include<iostream>
using namespace std;
class Complex
{
   
	int _a;
	int _b;
public:
	Complex(int a, int b)
	{
   
		_a = a;
		_b = b;
	}
	void show()
	{
   
		cout << "(" << _a << "," << _b << ")" << endl;
	}
	friend Complex operator +(Complex &C1, Complex &C2);
};
Complex operator +(Complex &C1, Complex &C2)
{
   
	Complex C(C1._a + C2._a, C1._b + C2._b);
	return C;
}
int main()
{
   
	Complex C1(1, 2);
	Complex C2(2, 3);
	Complex C3 = C1 + C2;
	C3.show();
	return 0;
}

这是对+运算符重载的一个示例,对于-、*、/的重载,改变符号就可以啦
我们来分析一下这个重载的实现过程:

我们在之前的文章中已经详细介绍过引用,在这里,我们将参数设为引用时为了提高效率。我们设置返回类型为Complex类型,return后接一个对象,二者是匹配的。其实我们不要忘记,这里面还是隐藏着this指针的。

1.2.2赋值运算符的重载

#include<iostream>
using namespace std;
class Complex
{
   
	int _a;
	int _b;
public:
	Complex(int a, int b)
	{
   
		_a = a;
		_b = b;
	}
	void show()
	{
   
		cout << "(" << _a << "," << _b << ")" << endl;
	}
	Complex &operator =(const Complex &C)
	{
   
		if (this != &C)
		{
   
			_a = C._a;
			_b = C._b;
		}
	}
};
int main()
{
   
	Complex C1(1,2);
	Complex C2 = C1;
	C2.show();
	return 0;
}

我们来分析一下这个重载:

首先,this指针与&C比较是为了防止自己给自己赋值的情况出现,返回引用时为了提高效率,const是常量,这个我们在之后的文章会介绍,用const可以使&C不能被修改,提高代码的安全性。

1.2.3自增自减运算符重载

注:此部分内容为博主搬运的光光老师教材上的内容。

自增运算符有前自增和后自增两种,重载增量运算操作符时,虽然运算符(函数名)相同,但功能不同,应该有前自增和后自增的差别。这种差别应该体现在正确匹配函数的参数和反映操作本质的返回值两方面。
前自增操作的结果应该与对象一致,而且前自增操作的结果可以作为左值,操作可以连贯。后自增操作的结果是自增操作之前的对象,是一个临时对象,当表达式计算完成后,该对象就消失了,所以后自增返回值与后自增之后的对象是不一致的

前自增:

后自增:

/******************************************************************** File name : f0402.h Description : 复数类头文件 ********************************************************************/
#ifndef _COMPLEX_H
#define _COMPLEX_H
/*class define ---------------------------------------------------*/
class CComplex
{
   
private:
double m_im, m_re;
public:
CComplex(double = 0, double = 0); //构造函数
void set_re(double); //设置实部的函数
void set_im(double); //设置虚部的函数
void display()const; //常成员函数
friend CComplex& operator++(CComplex&); //前自增运算符重载函数
friend CComplex operator++(CComplex&, int); //后自增运算符重载函数
};
#endif
//=============================================================
/******************************************************************** File name : f0402.cpp Description : 复数类源文件 ********************************************************************/
#include<iostream>
#include"f0402.h"
using namespace std;
/*member function ------------------------------------------------*/
CComplex::CComplex(double re, double im)
{
   
m_re = re;
m_im = im;
}
inline void CComplex::set_re(double re)
{
   
m_re = re;
}
inline void CComplex::set_im(double im)
{
   
m_im = im;
}
void CComplex::display()const
{
   
cout << m_re <<"+"<< m_im <<"i";
}
/*friend function--------------------------------------------------*/
CComplex & operator++(CComplex& cobj)
{
   
++cobj.m_re;
return cobj;
}
CComplex operator++(CComplex& cobj, int)
{
   
CComplex temp(cobj); //先保存原来对象到 temp 中
++cobj.m_re;
return temp;
}
//=============================================================
/******************************************************************** File name : f0402_driver.cpp Description : 复数类测试程序 ********************************************************************/
#include<iostream>
#include"f0402.h"
using namespace std;
int main()
{
   
CComplex complex1(1, 2), complex2;
cout <<"complex1: ";
complex1.display(); //显示 complex1
cout <<"\n 前自增 complex1: ";
complex2 = ++complex1;
complex1.display();
cout <<"\n complex2: ";
complex2.display();
cout <<"\n 后自增 complex1:";
complex2 = complex1++;
complex1.display();
cout <<"\n complex2: ";
complex2.display();
cout <<"\n";
return 0;
}
//=============================================================

运行结果为:

1.2.4 下标运算符的重载

注:此部分内容为博主搬运的光光老师教材上的内容。

下标运算符必须重载为非静态成员函数。下标运算符的功能是提取类对象的某个成员数据,提取的数据既可以作为右值使用,也可以作为左值使用,当作为左值使用表示该数据可以被修改

int i_arry[100], ia;
i_arry[0] = 10; //下标运算符重载函数返回结果作为左值
ia = i_arry[0]; //下标运算符重载函数返回结果作为右值

如果下标运算符重载函数作为左值,则函数的返回值就必须是某个可以修改的对象。一般来说,函数的返回值不可以作为左值使用,因为它是一个值。除非一种例外的情况,就是函数返回的是某个对象的地址或引用。如果返回某个对象的地址,则作为左值使用时,需要取值运算才能表示该对象。

int i_arry[100];
*i_arry[0] = 10; //当下标运算符返回对象地址时的情况,显然不符合常规

因为下标运算符重载函数返回指针,所以必须进行取值运算才能获取其指向的对象。这种使用显然不合理,因为数组元素表示一个个整型变量,在整型变量上进行取值运算符显然不合理。因此,下标运算符重载函数只能返回对象的引用。

/******************************************************************** File name : f0403.h Description : 字符串类头文件 ********************************************************************/
#ifndef _MYSTRING_H
#define _MYSTRING_H
/*class define----------------------------------------------------*/
class CMyString
{
   
private:
unsigned int m_size; //字符串的最大长度-1 
char *mp_data; //指向存放数据的内存空间
public:
CMyString(unsigned int = 0); //构造函数
CMyString(char* pstr); //构造函数
~CMyString(); //析构函数
char & operator[](int); //下标运算符重载函数
int length()const; //返回当前数组中元素个数
int capacity()const; //返回数组的最大元素个数
void display()const; //显示字符串函数
};
#endif
//==================================================================
/******************************************************************** File name : f0403.cpp Description : 字符串类源文件 ********************************************************************/
#include <iostream>
#include "f0403.h"
#include <cassert>
using namespace std;
/*member function define-----------------------------------------*/
CMyString::CMyString(unsigned int size)
{
   
m_size = size;
if (size > 0)
{
   
mp_data = new char[size];
assert(mp_data);
}
else
{
   
mp_data = 0; //将指针赋值为空
}
}
CMyString::CMyString(char* pstr)
{
   
m_size = strlen(pstr) + 20; //为字符串类对象预留 20 个字符空间
mp_data = new char[m_size]; //申请内存空间
assert(mp_data);
strcpy(mp_data, pstr); //复制字符串内容到 mp_data 
}
CMyString::~CMyString()
{
   
if (mp_data != 0)
delete[] mp_data;
}
inline int CMyString::length()const
{
   
return strlen(mp_data);
}
inline int CMyString::capacity()const
{
   
return m_size;
}
char& CMyString::operator[](int index)
{
   
return mp_data[index];
}
void CMyString::display()const
{
   
cout << mp_data << endl;
}
//================================================================
/******************************************************************** File name : f0403_driver.cpp Description : 字符串类测试程序 ********************************************************************/
#include<iostream>
#include"f0403.h"
using namespace std;
int main()
{
   
CMyString str("ABCDEFG"); //定义一个字符串对象
cout <<"Orignal string:"<< endl;
str.display();
str[1] = '5'; //下标运算符作为左值
cout <<"Modified string:"<< endl;
str.display();
cout <<"No.1 char: "<< str[1] << endl; //下标运算符作为右值
return 0;
}
//=============================================================

1.2.5 流运算符重载

注:此部分内容为博主搬运的光光老师教材上的内容。

因为流插入运算符和流提取运算符的第一个操作数必须是流对象,所以这两个运算符只能重载为友元函数。

istream& operator>>(istream& in, classtype& obj)
{
   
 //输入操作
 return in;
}

ostream& operator<<(ostream& out, const classtype& obj)
{
   
 //输出操作
return out;
}

流提取运算符重载函数的第 1 个参数为输入流对象,它既可以是文件输入流,又可以是标准输入设备流。第 2 参数表示保存提取数据的对象,必须是引用作为参数。流插入运算符重载函数的第 1 个参数为输出流对象,既可以是文件输出流,也可以是标准输出设备流。第 2个参数表示要输出的对象,可以是对象的引用,也可以是对象的常引用,但建议使用对象的常引
用作为函数参数。因为引用参数只需传递一个指针值,而传值形式需要传递整个对象,所以引用作为参数可以提高函数参数的传递效率。常引用可以保护引用参数不被重载函数修改。

/******************************************************************** File name : f0406.h Description : 复数类头文件 ********************************************************************/
#ifndef _COMPLEX_H
#define _COMPLEX_H
/*class define -----------------------------------------------*/
class CComplex
{
   
private:
double m_im, m_re;
public:
CComplex(double = 0, double = 0); //构造函数
void set_re(double); //设置实部的函数
void set_im(double); //设置虚部的函数
void display()const; //常成员函数
friend CComplex&operator++(CComplex&); //前自增运算符重载函数
friend CComplex operator++(CComplex&, int); //后自增运算符重载函数
friend CComplex operator+(const CComplex&, const CComplex&);
//加法运算符
friend std::istream& operator>>( std::istream&, CComplex&); 
//流提取运算符
friend std::ostream& operator<<( std::ostream&, CComplex&); 
//流插入运算符
};
#endif
//=============================================================

/******************************************************************** File name : f0406.cpp Description : 复数类源文件 ********************************************************************/
#include<iostream>
#include"f0406.h"
using namespace std;
/*member function ------------------------------------------------*/
CComplex::CComplex(double re, double im)
{
   
m_re = re;
m_im = im;
}
inline void CComplex::set_re(double re)
{
   
m_re = re;
}
inline void CComplex::set_im(double im)
{
   
m_im = im;
}
void CComplex::display()const
{
   
cout << m_re <<"+"<< m_im <<"i";
}
/*friend function--------------------------------------------------*/
CComplex& operator++(CComplex& cobj)
{
   
++cobj.m_re;
return cobj;
}
CComplex operator++(CComplex& cobj, int)
{
   
CComplex temp(cobj); //先保存原来对象到 temp 中
++cobj.m_re;
return temp;
}
CComplex operator+(const CComplex& c1, const CComplex& c2)
{
   
CComplex temp(c1); //用 c1 复制构造临时对象 temp
temp.m_re += c2.m_re;
temp.m_im += c2.m_im;
return temp;
}
std::istream& operator>>(std::istream& in, CComplex& obj)
{
   
in >> obj.m_re >> obj.m_im;
return in;
}
std::ostream& operator<<(std::ostream& out, CComplex& obj)
{
   
out << obj.m_re <<"+"<< obj.m_im <<"i";
return out;
}
//============================================================

/******************************************************************** File name : f0406_driver.cpp Description : 复数类测试文件 ********************************************************************/
#include<iostream>
#include"f0406.h"
using namespace std;
int main()
{
   
CComplex complex1, complex2, complex3;
cout <<"请输入一个复数: "<< endl;
cin >> complex1;
cout <<"complex1: "<< complex1 << endl; //显示 complex1 
cout <<"前自增 complex1:";
complex2 = ++complex1;
cout << complex1 << endl;
cout <<"前自增 complex1 的结果:";
cout << complex2 << endl;
cout <<"后自增 complex1:";
complex2 = complex1++;
cout << complex1 << endl;
cout <<"后自增 complex1 的结果: ";
cout << complex2 << endl;
complex3 = complex1 + complex2;
cout <<"complex1 + complex2 : "<< complex3 << endl;
complex3 = complex1 + 2.1;
cout <<"complex1+2.1: "<< complex3 << endl;
complex3 = 2.5 + complex1;
cout <<"2.5+complex1: "<< complex3 << endl;
return 0;
}
//=============================================================

1.3注意

  1. 不能通过连接其他符号来创建新的操作符:比如operator@
    2.重载操作符必须有一个类类型或者枚举类型的操作数
    3.用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
    4.作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
    5…* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载

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.1初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

class Date
{
   
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{
   }
private:
int _year;
int _month;
int _day;
};

那么我们可能会有这样的疑问,既然已经有在函数体内初始化的方式了,我们为什么还需要初始化列表初始化呢?其实二者在一般情况下都可以,甚至还可以搭配来使用,也就是一部分用初始化列表,一部分在函数体内初始化,但是对于一些情况,我们是必须采用初始化列表来初始化的:

引用成员变量
const成员变量
自定义类型成员(该类没有默认构造函数

class A
{
   
public:
A(int a)
:_a(a)
{
   }
private:
int _a;
};
class B
{
   
public:
B(int a, int ref)
:_aobj(a)
,_ref(ref)
,_n(10)
{
   }
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};

我们建议尽量采用初始化列表初始化,这是一种正规的大气的写法。还有我们要注意的一点是:成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。

#include<iostream>
using namespace std;

class A
{
   
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{
   }
	void Print() {
   
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main() {
   
	A aa(1);
	aa.Print();
}

我们来看看输出结果

为什么会出现这样的结果呢?我们来分析一下

总结

好的,我们类和对象系列的第三篇博文就到此结束啦,感谢大家的支持,也欢迎大家一起交流学习,博主会继续更新C++方面的文章,欢迎大家订阅我的专栏哦。最后我们来分享辛弃疾的《青玉案-元夕》

东风夜放花千树。更吹落、星如雨。宝马雕车香满路。
凤箫声动,玉壶光转,一夜鱼龙舞。

蛾儿雪柳黄金缕。笑语盈盈暗香去。众里寻他千百度。
蓦然回首,那人却在,灯火阑珊处。