c++从入门到入土第四篇:运算符重载(代码片段)

不难~ 不难~     2022-12-26     468

关键词:

系列文章目录

【C++从入门到入土】第一篇:从C到C++.
【C++从入门到入土】第二篇:类和对象基础.
【C++从入门到入土】第三篇:类和对象提高.



前言

  • 在数学上,两个复数可以直接进行+、-等运算。但 在C++中,直接将+或-用于复数对象是不允许的。
  • 有时会希望,让对象也能通过运算符进行运算。这样代码更简洁,容易理解。
  • 例如: complex_a和complex_b是两个复数对象; 求两个复数的和, 希望能直接写:

complex_a + complex_b

所以让我们对运算符进行重载,满足自身的需求。
本文较长,请耐心阅读。


运算符重载

1、定义和相关规则

  • 运算符重载,就是对已有的运算符(C++中预定义的运算符)赋予多重的含义,使同一运算符作用于不同类型的数据时导致不同类型的行为。
  • 运算符重载的目的是:扩展C++中提供的运算符的适用范围,使之能作用于对象。
  • 同一个运算符,对不同类型的操作数,所发生的行为不同。
  • 运算符重载的实质是函数重载
    以下是运算符重载的规则:
  • 不可以引人新的运算符。除了 . 、.* 、:: 、? :四个运算符,其他的运算符皆可被重载。
  • 运算符的操作数(operand)个数不可改变。每个二元运算符都需要两个操作数,每个一元运算符都需要恰好一个操作数。因此,我们无法定义出一个 equality运算符,并令它接受两个以上或两个以下的操作数。
  • 运算符的优先级(precedence)不可改变。例如,除法的运算优先级永远高于加法。
  • 运算符函数的参数列表中,必须至少有一个参数为 class类型。也就是说,我们无法为诸如指针之类的non-class类型,重新定义其原已存在的运算符,当然更无法为它引进新运算符。

运算符重载的形式

  • 运算符重载的实质是函数重载
  • 可以重载为普通函数,也可以重载为成员函数
  • 把含运算符的表达式转换成对运算符函数的调用。
  • 把运算符的操作数转换成运算符函数的参数。
  • 运算符被多次重载时,根据实参的类型决定调用哪个运算符函数。

定义形式:

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

……

重载实例:

class Complex

public:
	double real,imag;
	Complex( double r = 0.0, double i= 0.0 ):real(r),imag(i)  
	Complex operator-(const Complex & c);
;
Complex operator+( const Complex & a, const Complex & b)

	return Complex( a.real+b.real,a.imag+b.imag); // 返回一个临时对象

Complex Complex::operator-(const Complex & c)

	return Complex(real - c.real, imag - c.imag); // 返回一个临时对象

int main()

	Complex a(4,4),b(1,1),c;
	c = a + b; // 等价于c=operator+(a,b);
	cout << c.real << "," << c.imag << endl;
	cout << (a-b).real << "," << (a-b).imag << endl;
	//a-b 等价于a.operator-(b)
	return 0;

特点

重载为成员函数时 , 参数个数为运算符目数减一 。
重载为普通函数时 , 参数个数为运算符目数 。

2、赋值运算符的重载

赋值运算符 ‘=’重载

有时候希望赋值运算符两边的类型可以不匹配,比如,把一个int类型变量赋值给一个Complex对象,或把一个 char * 类型的字符串赋值给一个字符串对象,此时就需要重载赋值运算符“=”。

  • 赋值运算符“=”只能重载为成员函数赋值。

代码如下 :

class String 
private:
	char * str;
public:
	String ():str(new char[1])
	 
	 	str[0] = 0;
	
	const char * c_str() 
	 
		return str; 
	;
	String & operator = (const char * s);
	~String( )  delete [] str; 
;
String & String::operator = (const char * s)
 	
	// 重载“=”得 以使得 obj = “hello” 能够成立
	delete [] str;
	str = new char[strlen(s)+1];
	strcpy( str, s);
	return * this;

int main()

	String s;
	s = "Good Luck," ; //等价于 s.operator=("Good Luck,");
	cout << s.c_str() << endl;
	// String s2 = "hello!"; //这条语句要是不注释掉就会出错
	//因为这里不是使用赋值语句而是初始化,应调用构造函数
	s = "Shenzhou 8!"; //等价于 s.operator=("Shenzhou 8!");
	cout << s.c_str() << endl;
	return 0;

输出:
Good Luck,
Shenzhou 8!

浅拷贝和深拷贝

引用上面的类说明此问题
有如下代码:

String S1, S2;
S1 = "this";
S2 = "that";
s1 = s2;

执行完这些语句,真的能达到我们想要效果吗?
当然没有,甚至很多时候会发生内存泄漏等重大错误。

  1. 如不定义自己的赋值运算符,那么S1=S2实际上导致 S1.str和 S2.str 指向同一地方。
  2. 如果S1对象消亡,析构函数将释放 S1.str指向的空间,则S2消亡时还 要释放一次,不妥。
  3. 另外,如果执行 S1 = “other”;会导致S2.str指向的地方被delete。

因此要在 class String里添加成员函数,定义一个属于String类的赋值运算

String & operator = (const String & s) 

	delete [] str;
	str = new char[strlen( s.str)+1];
	strcpy( str,s.str);
	return * this;

这么做就够了吗?还有什么需要改进的地方?
考虑下面语句:

String s;
s = "Hello";
s = s;

是否会有问题?
如果这样会开辟两片相同的空间,存放相同的数据,但是却都由s.str指向,明显错误,无法执行。
解决办法:
加一个判断两个变量是否相同

String & operator = (const String & s)

	if( this == & s)
		return * this;
	delete [] str;
	str = new char[strlen(s.str)+1];
	strcpy( str,s.str);
	return * this;

对 operator = 返回值类型的讨论

void 好不好?
String 好不好?
为什么是 String &

  • 对运算符进行重载的时候,好的风格是应该尽量保留运算符原本的特性

考虑: a = b = c;
和 (a=b)=c; //会修改a的值
分别等价于:
a.operator=(b.operator=( c ));
(a.operator=(b)).operator=( c );

上面的String类是否就没有问题了?
为 String类编写拷贝构造函数的时候,会面临和 = 同样的问题(浅深拷贝),用同样的方法处理。

String( String & s)

	str = new char[strlen(s.str)+1];
	strcpy(str,s.str);

3、运算符重载为友元函数

  • 一般情况下,将运算符重载为类的成员函数,是较好的选择。
  • 但有时,重载为成员函数不能满足使用要求,重载为普通函数,又不能访问类的私有成员,所以需要将运算符重载为友元。

例如:

class Complex

	double real,imag;
public:
	Complex( double r, double i):real(r),imag(i) ;
	Complex operator+( double r );
;
Complex Complex::operator+( double r )
	//能解释 c+5
	return Complex(real + r,imag);

经过上述重载后:
Complex c ;
c = c + 5; // 有定义,相当于 c = c.operator +(5);
但是:
c = 5 + c; //编译出错
所以,为了使得上述的表达式能成立,需要将 + 重载为普通函数。

Complex operator+ (double r,const Complex & c)
 	//能解释 5+c
	return Complex( c.real + r, c.imag);

但是普通函数又不能访问私有成员,所以,需要将运算符 + 重载为友元。

class Complex

	double real,imag;
public:
	Complex( double r, double i):real(r),imag(i) ;
	Complex operator+( double r );
	friend Complex operator + (double r,const Complex & c);
;

4、运算符重载实例:可变长整型数组(类似vector)

首先确定我们要实现的功能

  1. 可变长 -> 动态开辟内存
  2. 能用数组初始化 -> 重写拷贝构造函数
  3. 有时需要用到赋值运算 -> 重载赋值运算符
  4. 需要输出类似a[i]的值 -> 重载下标运算符 ’ [ ] ’

代码如下:

int main() 
   //要编写可变长整型数组类,使之能如下使用:
	CArray a; //开始里的数组是空的
	for( int i = 0;i < 5;++i)
		a.push_back(i);
	CArray a2,a3;
	a2 = a;     //重载“=”
	for( int i = 0; i < a.length(); ++i )
		cout << a2[i] << " " ;
	a2 = a3; //a2是空的
	for( int i = 0; i < a2.length(); ++i ) //a2.length()返回0
		cout << a2[i] << " ";
	cout << endl;
	a[3] = 100;
	CArray a4(a);//要自己写拷贝构造函数
	for( int i = 0; i < a4.length(); ++i )
		cout << a4[i] << " ";
	return 0;

接下来开始设置类

class CArray 
int size; //数组元素的个数
int *ptr; //指向动态分配的数组
public:
	CArray(int s = 0); //s代表数组元素的个数
	CArray(CArray & a);
	~CArray();
	void push_back(int v); //用于在数组尾部添加一个元素v
	CArray & operator=( const CArray & a);//用于数组对象间的赋值
	int length()  return size;  //返回数组元素个数
	_____ CArray::operator[](int i) //返回值是什么类型?
		//用以支持根据下标访问数组元素,
		// 如n = a[i] 和a[i] = 4; 这样的语句
		return ptr[i];
	
;

上述重载下标运算符 [] 需要什么返回值呢?
因为我们需要支持如n = a[i] 和a[i] = 4; 这样的语句
所以需要引用,它能当左值。

具体函数实现:

CArray::CArray(int s):size(s)

	if( s == 0)
		ptr = NULL;
	else
		ptr = new int[s];

CArray::CArray(CArray & a) 

	if( !a.ptr) 
	
		ptr = NULL;
		size = 0;
		return;
	
	ptr = new int[a.size];
	memcpy( ptr, a.ptr, sizeof(int ) * a.size);
	size = a.size;

CArray::~CArray()

	if( ptr) delete [] ptr;

CArray & CArray::operator=( const CArray & a)
 // 赋值号的作用是使“=” 左边对象里存放的数组,大小和内容都和右边的对象一样
	if( ptr == a.ptr) // 防止a=a 这样的赋值导致出错
		return * this;
	if( a.ptr == NULL) 
	 // 如果a 里面的数组是空的
		//判断this->ptr是否为空
		if( ptr ) 
			delete [] ptr;
		ptr = NULL;
		size = 0;
		return * this;
	
	if( size < a.size) 
	 // 如果原有空间够大,就不用分配新的空间
		if(ptr)
			delete [] ptr;
		ptr = new int[a.size];
	
	memcpy( ptr,a.ptr,sizeof(int)*a.size);
	size = a.size;
	return * this;
 
void CArray::push_back(int v)
 // 在数组尾部添加一个元素
	if( ptr) 
	
		int * tmpPtr = new int[size+1]; // 重新分配空间
		memcpy(tmpPtr,ptr,sizeof(int)*size); // 拷贝原数组内容
		delete [] ptr;
		ptr = tmpPtr;
	
	else // 数组本来是空的
		ptr = new int[1];
	ptr[size++] = v; // 加入新的数组元素

5、流插入运算符和流提取运算符的重载

  • cout 是在 iostream 中定义的,ostream 类 的对象。
  • “<<” 能用在cout 上是因为,在iostream里对 “<<” 进行了重载。 考虑,怎么重载才能使得cout << 5; 和cout << “this”都能成立 ?

因为语句cout<<5<<“this”;能成立,即<<能持续作用,既可作为右值也可作为左值,所以<<的返回值应该为ostream类的引用。
猜测代码如下:

ostream & ostream::operator<<(int n)

	…… // 输出n 的代码
	return * this;

ostream & ostream::operator<<(const char * s )

	…… // 输出s 的代码
	return * this;

cout << 5 << “this”;
本质上的函数调用的形式是什么?
cout.operator<<(5).operator<<(“this”);

当我们对一个类输出时,可对" << "重载:

ostream & operator<<( ostream & o,const String & s)

	o << s.ptr ;
	return o;

对" >> "也同样如此重载,需要访问类的私有成员时可重载为友元函数。

6、类型转换运算符的重载

需要类和内置类型互相转换时可用

#include <iostream>
using namespace std;
class Complex

	double real,imag;
public:
	Complex(double r=0,double i=0):real(r),imag(i)  ;
	operator double ()  return real; 
	//重载强制类型转换运算符 double
;
int main()

	Complex c(1.2,3.4);
	cout << (double)c << endl; //输出 1.2
	double n = 2 + c; //等价于 double n=2+c.operator double()
	cout << n; //输出 3.2

7、自增、自减运算符的重载

自增运算符++、自减运算符–有前置/后置之分,为了区分所重载的是前
置运算符还是后置运算符,C++规定:

前置运算符作为一元运算符重载
重载为成员函数:
T & operator++();
T & operator–();
重载为全局函数:
T1 & operator++(T2);
T1 & operator—(T2);

后置运算符作为二元运算符重载,多写一个没用的参数:
重载为成员函数:
T operator++(int);
T operator–(int);
重载为全局函数:
T1 operator++(T2,int );
T1 operator—( T2,int);

接下来为函数实现:

class CDemo 

	private :
		int n;
	public:
		CDemo(int i=0):n(i)  
		CDemo & operator++(); // 用于前置形式
		CDemo operator++( int ); // 用于后置形式
		operator int ( )  return n; 
		friend CDemo & operator--(CDemo & );
		friend CDemo operator--(CDemo & ,int);

CDemo & CDemo::operator++()
 //前置 ++
	n ++;
	return * this;
 // ++s即为: s.operator++();

CDemo CDemo::operator++( int k )
 	//后置 ++
	CDemo tmp(*this); // 记录修改前的对象
	n ++;
	return tmp; // 返回修改前的对象
 
CDemo & operator--(CDemo & d)
	// 前置--
	d.n--;
	return d;
 //--s即为: operator--(s);
CDemo operator--(CDemo & d,int)
	// 后置--
	CDemo tmp(d);
	d.n --;
	return tmp;
 //s--即为: operator--(s, 0);

类型转换函数重载
operator int ( ) return n;
这里int 作为一个类型强制转换运算符被重载, 此后
Demo s;
(int) s ; //等效于 s.int();

  • 类型强制转换运算符被重载时不能写返回值类型,实际上其返回值类型就是该类型强制转换运算符代表的类型

8、函数调用运算符重载

  • 函数调用运算符 () 也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活
class MyPrint

public:
	void operator()(string text)
	
		cout << text << endl;
	
;
void test01()

	//重载的()操作符 也称为仿函数
	MyPrint myFunc;
	myFunc("hello world");

class MyAdd

public:
	c++从入门到入土第十四篇:list的介绍与使用

list的介绍与使用文章目录list的介绍与使用一、list的介绍二、list的使用1.构造函数2.正向迭代器3.反向迭代器4.范围for5.获取首尾元素6.插入元素的相关操作7.删除导致迭代器失效8.resize9.链表的拼接10.remove删除元素11.元素去重12.链... 查看详情

c++从入门到入土第八章:string类的使用

...ing类创建对象的方式(constructor)析构赋值string对象的访问[]运算符重载at接口backfront迭代器正向迭代器begin/end反向迭代器rbegin/rend范围for与迭代器容量相关接口size/lengthresize(修改字符个数)capacity/reserve/cl 查看详情

c++从入门到入土第二篇:类和对象基础(代码片段)

系列文章目录【C++从入门到入土】第一篇:从C到C++.文章目录系列文章目录前言一、类和对象的基本概念结构化程序设计面向对象的程序设计使用类的成员变量和成员函数二、类和对象基础类成员的可访问范围构... 查看详情

c++师傅领进门,修行靠个人第四篇:你所不知道的那些默认成员函数(代码片段)

...认成员函数1.1构造函数1.2析构函数1.3拷贝构造函数1.4赋值运算符重载1.5取地址及const取地址操作符重载1类里还有这6个默认成员函数1.1构造函数classDatapublic: voidSetDate(intyear,intmonth,intday)//模拟实现构造函数 _year=year; _month=mon... 查看详情

c++从入门到入土第一篇:初识c++

C++入门文章目录C++入门C++关键字(C++98)命名空间1.命名空间的定义2.命名空间的使用C++输入&输出缺省参数1.缺省参数概念2.缺省参数分类函数重载C++关键字(C++98)C+... 查看详情

极简嵌入式c语言教程——从入门到入土(代码片段)

...言标识符与关键字(1)标识符:(2)关键字3.数据类型与运算符(1)变量与常量<1>变量<2>变量的定义<3>常量(2)运算符<1>算术运算符<2>自增、自减运算符<3>赋值与赋值组合运算符<4>关系运算符<5>逻... 查看详情

极简嵌入式c语言教程——从入门到入土(代码片段)

...言标识符与关键字(1)标识符:(2)关键字3.数据类型与运算符(1)变量与常量<1>变量<2>变量的定义<3>常量(2)运算符<1>算术运算符<2>自增、自减运算符<3>赋值与赋值组合运算符<4>关系运算符<5>逻... 查看详情

c++从入门到入土第三篇:类与对象(上篇)(代码片段)

类与对象(上篇)文章目录类与对象(上篇)面向过程和面向对象的初步认识类的引入类的访问限定符访问限定符说明类的实例化类对象模型类对象大小的计算this指针面向过程和面向对象的初步认识面向过程(Proce... 查看详情

《侯老师c++面向对象开发》从入门到“入土”(代码片段)

...小和布局,该范围内的值都可以存储在内存中,运算符可应用于变量上。        变量的名称可以由字母、数字和下划线字符组成。它必须以字母或下划线开头。大写字母和小写字母是不同的,因为C+ 查看详情

c++从入门到入土第十篇:string模拟实现

...g模拟实现传统写法1.构造函数2.析构函数3.拷贝构造4.赋值运算符现代写法1.构造函数2.析构函数3.拷贝构造4.赋值运算符类的成员传统写法1.构造函数正确写法:这才是一个空的string类对象,字符串的内容为空,创建的... 查看详情

c++从入门到入土第十九篇:二叉搜索树(代码片段)

二叉搜索树文章目录二叉搜索树二叉搜索树概念原理性质复杂度二叉搜索树操作1.结构2.查找3.插入4.中序遍历5.拷贝6.销毁7.删除二叉搜索树概念二叉查找树(BinarySearchTree),(又:二叉搜索树,二叉排序树... 查看详情

c++从入门到入土第十八篇:多态

多态文章目录多态什么是多态虚函数重写的两个例外协变(基类与派生类虚函数返回值类型不同)析构函数的重写(基类与派生类析构函数的名字不同)重载、覆盖(重写)、隐藏(重定义)的对比抽象类多态的原理虚... 查看详情

c++从入门到入土第十八篇:多态

多态文章目录多态什么是多态虚函数重写的两个例外协变(基类与派生类虚函数返回值类型不同)析构函数的重写(基类与派生类析构函数的名字不同)重载、覆盖(重写)、隐藏(重定义)的对比抽象类多态的原理虚... 查看详情

c++入门运算符重载详解(代码片段)

1、什么是运算符重载不可重载运算符运算符含义.成员访问运算符->成员指针访问运算符::域运算符sizeof长度运算符?:条件运算符#预处理符号(1)运算符:运算符分为算术运算符(+、-、*、/)、关系运算符(=、!=、==... 查看详情

c++从入门到入土第七篇:模板初阶

模板初阶文章目录模板初阶一、函数模板二、类模板一、函数模板以前我们要写交换函数的话是这样来写的:虽然函数重载可以实现,但每次新增加一个数据类型,就需要重新再写一个交换函数,比较麻烦;... 查看详情

c++从入门到入土第二十二篇:数据结构之红黑树(代码片段)

红黑树文章目录红黑树一、红黑树简介性质节点红黑树的实现一、红黑树红黑树是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。... 查看详情

c++从入门到入土第二十二篇:数据结构之红黑树(代码片段)

红黑树文章目录红黑树一、红黑树简介性质节点红黑树的实现一、红黑树红黑树是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。... 查看详情

c++从青铜到王者第二十四篇:c++的类型转换(代码片段)

...结前言一、C语言中的类型转换在C语言中,如果赋值运算符左右两侧类型不同&# 查看详情