C++的运算符重载

前言


c++文章连载:
1.C++基础
  1.C++基础
  2.C++新增和有变化的关键字
  3.C++的内存管理
2.面向对象
  1.C++的封装和访问权限
  2.C++继承和多态特性
  3.C++的运算符重载
  4.C++静态类和静态成员
  5.C++的友元函数和友元类
3.模板编程和STL
  1.C++模板编程入门
  2.STL的容器类和迭代器
  3.STL的泛型算法
  4.模板特化与类型萃取
  5.STL的其他容器讲解
  6.智能指针与STL查漏补缺
4.杂项
  1.c++各种流操作
  2.依赖,关联,聚合,组合,继承
  3.一些技巧性的代码设计
  
  


1.运算符重载引入

1.1、从函数重载说起
(1)函数重载是在一定作用域内,多个相同名称但不同参数列表的函数重载
(2)编译时由编译器根据实际调用时给的实参情况来判定本次实际用哪个函数,这个过程叫重载决策
(3)重载函数本质上就是多个独立函数,重载机制在编译时发生,运行时不参与
(4)函数重载的意义就是避免我们胡乱起名,方便编写类库覆盖所有可能操作,是一种语法糖
1.2、什么是运算符重载
(1)什么是运算符?譬如+ - * / %等算术运算符和> < == !=等关系运算符就是典型的可重载运算符
但不是所有的运算符都可以重载,譬如sizeof
(2)运算符诞生于C语言中,用来对变量进行某种“预定义”的运算操作,这种预定义是编译器预制好的,编译时会翻译对应到CPU机器码
(3)面向对象时代带来新的问题:两个对象如何运算?譬如:Person a, b, c; c = a + b; 此处的+让编译器如何解读?见示例

2.运算符重载示例

#include <iostream>
using namespace std;
class coordinate
{
public:
	int x;					// x轴坐标
	int y;					// y轴坐标
	
	coordinate();
	coordinate(int x0, int y0);	
	void print(void);
	
	//coordinate operator+(const coordinate& other);
	coordinate& operator+(const coordinate& other);
};

coordinate::coordinate()
{
	x = 0;
	y = 0;
};
coordinate::coordinate(int x0, int y0)
{
	x = x0;
	y = y0;
};

coordinate& coordinate::operator+(const coordinate& other)
{
	// 在该函数内,去实现+的真正应该做的操作
	coordinate tmp;
	tmp.x = this->x + other.x;
	tmp.y = this->y + other.y;
	coordinate &c=tmp;
	return c;
}

int main(void)
{
	coordinate a;	//会调用没有参数的构造函数
	coordinate b(2, 6);
	coordinate c;
	
//	c = a + b;			// 编译时被编译器翻译成: c = a.operator+(b);		c = b.operator+(a);
	c = a.operator+(b);
	return 0;
}

(1)class coordinate, 2个属性x和y,直接对2个对象执行+,编译不通过
(2)重载+运算符后,再编译并执行,查看结果,分析结果
写法:class operator+(const class other);
c = a + b和 c = a.operator+(b);是一样的。

3.深度理解运算符重载

3.1、运算符重载的本质
(1)表面上,运算符重载是
对C++源生运算符的意义,在某个class中做重定义
(2)本质上,运算符被映射到执行相应的成员函数,所以运算符重载其实是重定义对象的运算符所对应的函数
3.2、运算符重载的意义
(1)运算符重载是一种语法糖,可以让代码“看起来更简洁,更优雅”,将复杂实现隐藏在类的内部
(2)运算符重载机制加大了类库作者的工作量,减少了调用类库写功能的人的书写量
(3)C++支持运算符重载机制有其理念和历史原因,是对写法简洁和效率优秀的综合考量

(4)赋值运算符=重载和引用结合,可有效提升代码效率
(5)赋值运算符=重载时要注意有指针成员时会涉及浅拷贝和深拷贝
(6)运算符重载一定程度上体现了C++的多态性,因为同样的运算符在不同的class中表现是不同的

4.理解运算符重载的关键点

#include <iostream>
using namespace std;

class coordinate
{
public:
	int x;					// x轴坐标
	int y;					// y轴坐标
	
	coordinate();
	coordinate(int x0, int y0);
	
	void print(void);
	
	coordinate operator=(const coordinate& other);
	void operator+=(const coordinate& other);
//	void operator=(const coordinate& other);
	
};

coordinate::coordinate()
{
	x = 0;
	y = 0;
};

coordinate::coordinate(int x0, int y0)
{
	x = x0;
	y = y0;
};

void coordinate::print(void)
{
	cout << "(" << this->x << ", " << this->y << ")" << endl;
}

coordinate coordinate::operator=(const coordinate& other)
{
	// c = a;	c是this,a是other,c=a整个表达式的值是返回值
	this->x = other.x;
	this->y = other.y;	
	return *this;
}

void coordinate::operator+=(const coordinate& other)
{
	// 重载函数实际操作按照 a = a + b的效果来的
	// a是this,b是other,操作完成后a的值被改变,但是b的值不变
	this->x = this->x + other.x;
	this->y = this->y + other.y;
	return;
}

/*
void coordinate::operator=(const coordinate& other)
{
	// c = a;	c是this,a是other,c=a整个表达式的值是返回值
//	coordinate tmp;
	this->x = other.x;
	this->y = other.y;
	return ;
}
*/

int main(void)
{
	coordinate a(1, 3);
	coordinate b(2, 6);
	coordinate c;
	coordinate d;

//	d = (c = a);
	
	a += b;			// 效果上等价于a = a + b;
//	c = (a += b);		因为我们一般不会这样写代码,所以 a+=b整个表达式的值不会再拿来给别人赋值用,所以重载函数返回值其实没用
	a.print();
	return 0;
}

4.1、运算符=的默认提供问题
(1)=运算符有点特殊,编译器会提供一个默认的=运算符的重载,所以不提供时也能用
(2)如果自己显式写了=运算符的重载函数,则会覆盖编译器自动提供的那一个
4.2、+=运算符
4.3、总结
(3)a+b有可能不等于b+a,这个也是运算符重载衍生出来的不好把控的风险问题
(4)运算符重载给了类库作者非常大自由度,所以容易失控

5.运算符重载函数中的细节

5.1、赋值运算符重载与拷贝构造函数
下面代码等价:

coordinate b = a;			// 通过类的拷贝构造函数来生成的b
coordinate b(a);

(1) 区分初始化时的赋值(一般就叫初始化),和非初始化时的赋值(一般就叫赋值
赋值->赋值运算符重载函数
初始化->拷贝构造函数

(2)实验验证初始化和赋值时各自对应
在执行以下代码时
coordinate b;
b = a;
会执行拷贝构造函数,为什么赋值运算会执行拷贝构造函数呢,因为执行了以下=运算符重载函数

coordinate coordinate::operator=(const coordinate& other)
{
	// c = a;	c是this,a是other,c=a整个表达式的值是返回值
	cout << "---operator= function---" << endl;
	if (this != &other)
	{
		this->x = other.x;
		this->y = other.y;
	}
	return *this;
}

最后返回*this造成的,返回值是作为一个值传递过去的,所谓值传递就是复制传递,也就是说会把*this这个值整个复制一遍传递过去,这个复制的大小就是变量的大小,所以这样的代码是效率不高的,因为他要把整个变量大小的值进行复制操作,如果是个普通变量,那就直接复制,如果是个对象,那么调用它的拷贝构造函数,所以这里就执行了一遍拷贝构造函数,如果不希望执行一遍复制操作,那么修改代码如下:

void coordinate::operator=(const coordinate& other)
{
	cout << "---void operator= function---" << endl;
	this->x = other.x;
	this->y = other.y;
	return ;
}

5.2、赋值运算符重载函数参数中的const
(1)如果运算符对其右侧的操作数并不改变,则应该在参数列表中使用const引用
(2)C++的const要求非常严格,在c = a + b;中如果=运算符的重载函数参数列表中没有用const会编译报错。右值不能是非const的。
5.3、避免赋值运算符中的自赋值
(1)自赋值就是Person a; a = a;
(2)自赋值如果不处理,轻则浪费效率,重则导致内存丢失(该深拷贝时做了浅拷贝,在8中详解)
(3)避免自赋值很简单,只需要在赋值运算符重载函数所有操作前加上一个判断 if (this != &other)即可

6.赋值运算符重载函数返回引用

6.1、返回引用好处1:提升程序效率
(1)赋值运算符重载函数返回值可以返回对象类型,也可以返回对象引用类型,都能工作。代码验证
(2)区别在于:返回引用可以避免一次返回值值传递的对象复制,这需要消耗资源的。代码验证
(3)总结:虽然C++语法并未强制要求,但是好的写法是应该返回引用
6.2、返回引用好处2:允许连续赋值式
(1)返回对象而不是引用时,在连续赋值(c = a = b;)时会编译可以,运行也可以,但是效率低同1中所讲。
(2)原因是先执行a=b操作,返回值再作为第2次赋值运算的右值(也就是函数参数),对象和引用是类型兼容的
(3)总结:连等在返回对象和引用时都可以,但是在返回void时就不可以了
6.3、思考:传参为什么要传引用
(1)如果传对象,则调用时是值传递,调用时需要复制一次,增加额外开销
(2)如果传指针,则重载后,使用时为了符合函数参数格式必须写成 a = &b;这种,不符合C语言写法习惯,也有歧义
(3)实际测试发现真的传指针时,写 a=b; a=&b;都能编译通过,且运行正确。但是确实很别扭。
(4)总结:有资料表明,其实C++早期发明引用概念时,就是为了解决此处运算符重载传参的尴尬。

7.String类的赋值运算符重载

7.1、标准库中String类的运算符重载
(1)https://zh.cppreference.com/w/%E9%A6%96%E9%A1%B5
(2)String类提供了=和+等多个运算符重载函数的成员
(3)标准库只给了API,没给实现,咱们可以自己实现个=运算符重载函数
7.2、String的=运算符重载演练
(1)自己编写一个MyString类,成员变量char *pBuf指向实际存储字符串的buf,new动态内存来使用。
(2)实践:先完成构造函数,成员函数print,len等基础设计,写代码测试ok
(3)编写=运算符重载函数,引出浅拷贝深拷贝问题
MyString.hpp:

#ifndef __MYSTRING_H__
#define __MYSTRING_H__
#include <iostream>
using namespace std;
class MyString
{
private:
	char *pBuf;			// 指针指向实际存储字符串内容的buffer空间,采用动态分配
	int cnt;				// 用来记录MyString对象中存储的有效字符的个数	
public:
	// 构造函数
	MyString();
	MyString(const char *pSrc);	
	// 拷贝构造函数	
	// 普通成员函数
	void print(void);			// 用来打印字符串的长度和内容给我们看	
	// 运算符重载函数
	MyString& operator=(const MyString& other);	
	// 析构函数
	~MyString();
};
#endif

MyString.cpp:

#include "MyString.hpp"
#include <iostream>
using namespace std;

// 构造和析构
MyString::MyString()
{
	// 不写,空着,这样做的坏处就是咱们后续main中不要去用这个
	// 所以咱们自己写代码时注意别用就行了
	this->pBuf = new char[1];
}

MyString::MyString(const char *pSrc)
{
	// 先计算pSrc字符串的大小,也就是有效字符的个数,并且记录到this->cnt中
	const char *p = pSrc;
	while (*p++ != '\0')
		cnt++;
	
	// 先给pBuf分配动态内存,方便之后去复制字符串
	this->pBuf = new char[cnt + 1];
	
	// 把pSrc指向的源字符串的内容,复制给当前MyString对象来存储即可生成
	p = pSrc;		// 要复制的源字符串
	char *p2 = this->pBuf;
	while (*p != '\0')
		*p2++ = *p++;
	*p2 = '\0';
}

MyString::~MyString()
{
	delete[] this->pBuf;
}

// 普通成员函数
void MyString::print(void)
{
	cout << "cnt of MyString = " << this->cnt << endl;
	char *p = this->pBuf;
	while (*p != '\0')
		cout << *p++;

	cout << endl;
}
	
// 运算符重载函数
MyString& MyString::operator=(const MyString& other)
{
	// this是返回值,左值是this,右值是other
	// 赋值操作,其实就是把other里的字符串内容,拷贝给this里的字符串即可
	
	// 第1步:先释放掉原来的buf
	delete[] this->pBuf;
	
	// 第2步:重新给this->pBuf分配足够大空间
	// 如果直接这样做确实可以解决buf不够大的问题,但是会造成原有的buf动态内存丢失
	// 解决办法就是先释放掉原来的动态内存
	this->pBuf = new char[other.cnt + 1];
	
	// 第3步:完成复制
	// 如果直接这样,是可以完成复制,但是有可能会造成this-pBuf的数组越界,因为p2可能比p1大
	// 解决的办法就是重新给this-pBuf分配一个足够大的空间
	char *p1 = this->pBuf;
	char *p2 = other.pBuf;
	while (*p2 != '\0')
		*p1++ = *p2++;
}

int main(void)
{
	MyString s1("linuxlinuxlinuxlinuxlinux");
	MyString s2("abcdefg");
	
	s1.print();
	s2.print();
	s1 = s2;			// 赋值运算符重载函数

	cout << "----------------" << endl;
	s1.print();
	s2.print();	
	return 0;
}

7.3、总结
(1)运算符重载会牵出其他技术点(譬如浅拷贝深拷贝)共同完成某个工作,这就有难点,讲究功底了
(2)C++编程中涉及到动态内存的地方一定要慎重

8.++和–运算符的前置后置如何实现

8.1、可前置可后置的运算符
(1)int a = 5; a++; ++a;结果不同
(2)重载++和–有无意义
8.2、如何重载++和–
(1)按最底层规则分析
a++; a是this,other是空的,a是返回值,附带操作是a+1,对应 Type& operator++(void);
++a; this是空的,a是other,a+1是返回值,附带操作是a+1,对应 Type& operator++(const Tpye& other);
(2)代码实战和验证
实践验证编译不通过,报错:coordinate& coordinate::operator++(const coordinate&)’ must take ‘int’ as its argumen
(3)再次分析,因为编译器强制要求++a的重载函数必须携带int类型的参数
Type& operator++(int x);
编译器的理解是:++a时,a是this,other是空的,int x用来做区分,a+1是返回值,附带操作是a+1
实践验证:对应结论刚好是反的。
(4)再次分析
C++编译器认为,a++; 对应Type& operator++(int x); 而++a对应Type& operator++(void);
咱们其他分析包括代码编写都是ok的
8.3、总结
在函数中定义的临时变量不能用引用返回,只能当作值返回。如tmp?
(但是我试了一下可以的)

#include <iostream>
using namespace std;

class coordinate
{
public:
	int x;					// x轴坐标
	int y;					// y轴坐标
	
	coordinate();
	coordinate(int x0, int y0);

	coordinate(const coordinate& rhs);
	
	void print(void);
	
	// ++的2个运算符重载函数
	coordinate operator++(void);
//	coordinate& operator++(const coordinate& other);

	coordinate& operator++(int x);

};

coordinate::coordinate()
{
	x = 0;
	y = 0;
};

coordinate::coordinate(int x0, int y0)
{
	x = x0;
	y = y0;
};

// 拷贝构造函数
coordinate::coordinate(const coordinate& rhs)
{
	cout << "---copy construtor---" << endl;
	this->x = rhs.x;
	this->y = rhs.y;
}

void coordinate::print(void)
{
	cout << "(" << this->x << ", " << this->y << ")" << endl;
}

coordinate coordinate::operator++(void)
{
	// this要+1,然后要返回+1前的this
	coordinate tmp;
	
	tmp.x = this->x;
	tmp.y = this->y;
	
	this->x = this->x + 1;
	this->y = this->y + 1;
	
	return tmp;
}
/*
coordinate& coordinate::operator++(const coordinate& other)
{
	other.x = other.x + 1;
	other.y = other.y + 1;
	
	return other;
}
*/

coordinate& coordinate::operator++(int x)
{
	this->x = this->x + 1;
	this->y = this->y + 1;
	
	return *this;
}
int main(void)
{
	coordinate a(1, 3);
	coordinate c;
	
	//c = a++;
	c = ++a;
	
	a.print();
	c.print();
	return 0;
}

9.两种运算符重载方法

9.1、并非所有运算符都支持重载
(1)下面是不可重载的运算符列表

.			成员访问运算符
.*, ->*		成员指针访问运算符
::			域运算符
sizeof		长度运算符
? :			条件运算符
#			预处理符号

9.2、并非只有一种方式实现重载
(1)有两种方法可以使运算符重载
第一种:使重载运算符成为该类的成员函数。这允许运算符函数访问类的私有成员。它也允许函数使用隐式的this指针形参来访问调用对象。
第二种:使重载的成员函数成为独立分开的函数。当以这种方式重载时,运算符函数必须声明为类的友元才能访问类的私有成员。
9.3、如何选择两种运算符重载
(1)大多数运算符(如+ =等)既可以作为成员函数也可以作为独立函数重载。
(2)更好的做法是将二元运算符重载为釆用相同类型形参的独立函数。这是因为,与独立运算符的重载不同,成员函数的重载通过使左侧形参变成隐式的,造成了两个形参之间的人为区别,这将允许转换构造函数应用右侧形参,但不应用左侧形参,从而产生了更改形参顺序的情况,导致正确的程序如果换个方式却出现了编译器错误。示例如下:
Length a(4, 2), c(0);
c = a + 2; //编译,当于 c = a.operator+(2)
c = 2 + a; //不能编译:相当于 c = 2.operator+(a);

#include <iostream>
using namespace std;

class coordinate
{
public:
	int x;					// x轴坐标
	int y;					// y轴坐标
	
	coordinate();
	coordinate(int x0, int y0);
	
	// 拷贝构造函数
	coordinate(const coordinate& rhs);
	
	void print(void);
	
	friend coordinate operator+(const coordinate& a, const coordinate& b);
	
};

coordinate::coordinate()
{
	x = 0;
	y = 0;
};

coordinate::coordinate(int x0, int y0)
{
	x = x0;
	y = y0;
};

// 拷贝构造函数
coordinate::coordinate(const coordinate& rhs)
{
	cout << "---copy construtor---" << endl;
	this->x = rhs.x;
	this->y = rhs.y;
}

void coordinate::print(void)
{
	cout << "(" << this->x << ", " << this->y << ")" << endl;
}

// 用独立函数来实现+的运算符重载
coordinate operator+(const coordinate& a, const coordinate& b)
{
	coordinate tmp;
	tmp.x = a.x + b.x;
	tmp.y = a.y + b.y;
	return tmp;
}

int main(void)
{
	coordinate a(1, 3);
	coordinate b(2, 4);
	coordinate c;
	
	c = a + b;			//  a+b 就相当于是 c = operator+(a, b);
//	c = b + a;			// 相当于 c = operator+(b, a);
	
	a.print();
	b.print();
	c.print();

	return 0;
}

分析String类的+=方法,和其他operator重载

令运算符重载函数作为类的成员函数重载前置运算符和重载后置运算符分别如何实现?
注:这里的int类型参数只是用来区别后置++与前置++,此外没有任何其他作用。
https://blog.csdn.net/jacket_/article/details/89714947

令运算符重载函数作为类的成员函数重载插入运算符和重载提取运算符分别如何实现,写个案例重载<<和>>这2个运算符

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值