C++学习:5、运算符重载、模板、类型转换

397 阅读10分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

运算符重载

1、运算符重载(operator overload)

在 C++ 当中非常重要!!!!

运算符重载(操作符重载) :可以给运算符增加一些新的特性

举个例:我们想直接加俩个point,利用加号。

image-20210529172405747

operator

image-20210529172634076

再次理解:

image-20210529173318621

汇编语言:

image-20210529173350476


2、完善

运算符重载参数: const Point &p1

  • 输入型参数
  • 减少中间变量的产生
  • 既可以接收 const 类型的参数,可以接收 非const 类型的参数

image-20210529173710410


拷贝构造函数的参数如果是 对象类型

  • 会造成无线调用构造函数,所以编译器报错。
  • Point p1 = p2 , 无限调用。

image-20210530113401695

拷贝构造函数的参数如果是 const Point &p1

  • 输入型参数
  • 减少中间变量的产生
  • 既可以接收 const 类型的参数,可以接收 非const 类型的参数

image-20210530113541822


完善运算符的特性:

image-20210530163104384

改进一:将重载运算符的返回值,变为 const 类型

  • 因为 p1 + p2 ,返回一个 const 类型的对象,只能调用,不能进行修改赋值

image-20210530163241048

缺点:

  • 不能进行 连加 运算:

image-20210530163948225

原因: const 类型的对象,不能调用 非const 类型的成员函数。(在 const 成员函数当中有分析)

image-20210530164320220

改进二:将其改为 const 成员函数

image-20210530164708426


3、更多运算符

之前我们的运算符重载,我们是写在了类外面,我们现在将他写到类里面

优点:

  • 可以少写一个参数,通过 this 指针来代替一个。
  • 既然是成员函数,那么就可以访问类内的所有成员,不需要变为友元函数

image-20210530161457631


实现 的重载:

image-20210530162021824


实现 += 运算符:

image-20210530175353051

更改返回值类型: 更换为 引用

  • 如果返回的是 对象类型,那么会产生中间对象, p3 就会赋值给中间对象

image-20210530175455918


实现 == 运算符,判断一下两个点是否相等。

image-20210530181249810


4、单目运算符

单目运算是指运算符包括 算术运算符逻辑运算符位逻辑运算符位移运算符关系运算符自增自减运算符

实现 负号

需求:我们只是想要 p1 的负数,并不想让 p1 本身发生改变

  • 返回一个临时对象并不是返回引用

image-20210530183627641

缺点:赋值的时候不报错

改进:将返回值更改为 const 类型, 则不能进行赋值。

image-20210530183821787

image-20210530183847699

缺点:不能连续使用负号,

原因:返回的 const 临时对象不能调用 非const 函数

改进:将函数更改为 const 类型

image-20210530184035025

image-20210530184055061


实现 ++自增) 运算符

image-20210530185910984


5、cout 的重载

实现 << 运算符:(cout << p1 << endl;)

  • 成员函数:只能控制右边是什么,左边已经固定为调用成员函数的对象

  • 全局函数:可以通过参数,来控制运算符的左边是什么,右边是什么。

image-20210530191036126

改进:变为 Point 的友元函数,全局函数可以访问 Point 的成员函数。

image-20210530191431861

实现连续打印

image-20210530191831157

  • 缺点:换行符 已经写死到 << 里面

image-20210530192028419

改进换行符:将换行符 << 的控制权更换到 << 函数外面

image-20210530192323383


6、cin 的重载

同理:

实现 >> 运算符:( cin>>a )

  • 成员函数:只能控制右边是什么,左边已经固定为调用成员函数的对象

  • 全局函数:可以通过参数,来控制运算符的左边是什么右边是什么


我们的需求:从键盘输入一个坐标,赋值给 Point 对象

  • cin 的类型为 input stream

  • 右边的对象应该是 非const 类型。(输出型参数),通过键盘来进行更改

image-20210530194743769


7、调用父类的运算符重载函数

  • 继承之后,父类的私有成员,不能直接调用,但是可以通过调用父类的运算符重载即可。

  • 返回值为引用:连续赋值

image-20210530204925489


8、仿函数(函数对象)

仿函数:将一个对象当作一个函数一样来使用

  • ( ) 进行重载

image-20210530205508604

  • 对比普通函数,它作为对象可以保存状态

9、注意点

为什么有些运算符只能写在类里面呢?

  • 这些运算符都是这个类特有的
  • 如果写成全局函数,那么就成了全局的重载。

image-20210530210059765

#include <iostream>
using namespace std;

class Point {
	//friend Point operator+ (const Point &p1, const Point &p2);
	int m_x;
	int m_y;
public:
	int getX() { return m_x; }
	int getY() { return m_y; }
	Point(int x = 0, int y = 0) : m_x(x), m_y(y) {}

	Point(const Point &p1) {
	
	}
	
	void display() {
		cout << m_x << "  " << m_y << endl;
	}

	const Point operator+ (const Point &p2) const {
		return Point(m_x + p2.m_x, m_y + p2.m_y);
	}

	const Point operator- (const Point &p2) const {
		return Point(m_x - p2.m_x, m_y - p2.m_y);
	}
	 
	Point & operator+= (const Point &p2) {    // 这里要将其修改为 非const 类型,因为里面要修改成员变量
		m_x = m_x + p2.m_x;
		m_y = m_y + p2.m_y;
		return *this;
	}

	bool operator== (const Point &p2) {
		if (m_x == p2.m_x && m_y == p2.m_y) {
			return true;
		}
		else {
			return false;
		}
	}

	const Point operator-()const {
		return Point(-m_x, -m_y);
	}

	Point &operator++() {
		m_x++;
		m_y++;
		return *this;
	}
	
	const Point operator++(int) {  // 语法糖,参数添加 int 就代表后置 ++
		Point old(m_x, m_y);
		m_x++;
		m_y++;
		return old;
	}

	Point &operator--() {
		m_x--;
		m_y--;
		return *this;
	}

	const Point operator--(int) {  // 语法糖,参数添加 int 就代表后置 ++
		Point old(m_x, m_y);
		m_x--;
		m_y--;
		return old;
	}

	friend ostream & operator<<(ostream &cout, const Point &point);
	friend istream &operator>>(istream &cin, Point &point);
}; 

ostream &operator<<(ostream &cout, const Point &point) {
	cout << "(" << point.m_x << "," << point.m_y << ")" ;
	return cout;
}

//  input stream ==== istream
istream &operator>>(istream &cin, Point &point) {
	cin >> point.m_x;
	cin >> point.m_y;  // 以回车结尾
	return cin;
}

int main()
{


	getchar();
	return 0;
}

模板

先来了解什么是泛型?

  • 类型参数化
  • 达到代码复用的目的。
  • C++ 当中使用 模板 来实现泛型。

先来看一个需求:实现一个加法运算

image-20210530215025391

分析:

都是实现一个加法,但是需要写 3 个函数,很麻烦。

解决:

泛型:是一种将类型参数化以达到代码复用的技术, C++中使用模板来实现泛型。


1、函数模板

使用模板的目的:进行泛型编程

格式:

image-20210530215533357

image-20210530220140086

语法糖:

理解什么是类型参数化类型 也可以当作是一个参数传过去

  • T = int , T = double ,T = Point。

image-20210530220833441

可以通过反汇编看出,add 本质调用的并不是同一个函数,而是 3 个函数

本质:我们写 1 份函数,编译器帮我们生成 n 份函数

image-20210530224313586

注意:

1、模板没有被使用时,函数实体是不会被实例化出来的

2、当传入参数是不同类型的时候:

image-20210530221602365

3、模板的声明实现如果分离到.h和.cpp中,会导致链接错误,一般将模板的声明和实现统一放到一个 .hpp文件中。

之前的函数实现:

image-20210530225028423

使用模板的函数实现:

image-20210530225217911

报错:无法解析外部符号

image-20210530225238108

通过下面的编译细节,我们来分析为什么会报错


2、编译细节

正常函数的编译和链接

分析:模板的声明实现如果分离到.h和.cpp中,会导致链接错误,为什么会报错?

1、分析哪些文件会被编译。

  • .h 头文件不会参与编译,在预处理的时候被替换。
  • .cpp 源文件会参与编译

2、怎么进行编译呢?

  • 第一步:每个 .cpp 源文件 单独进行编译。生成一个 obj 文件。
  • 第二部:链接各个 obj 文件,从而生成一个 exe 文件。

image-20210530225859384

查看生成的文件:

image-20210530230042354


再次深入的分析流程:

  • add 函数的真正代码实体,在 math.obj 里面。

  • 函数的调用,本质就是 call 函数地址 。(函数地址在 math.obj 里面,main.obj 里面并没有函数体)


问题:main 文件,怎么找到 call 函数地址 中的函数地址呢?

  • main.cpp 编译成 main.obj 的过程当中,call 当中的函数地址是瞎写的。(只是为了骗过编译器,真正地址在链接时候给
  • main.obj 和 math.obj 链接过程当中,

image-20210530233029913

  • 编译阶段: call 当中的函数地址是假的,因为 main.obj 文件并不知道 math.obj 文件存在
  • 链接阶段:修正 call 当中的函数地址,保证可以调用正确。

image-20210530234457786


模板函数的编译和链接

image-20210531000458030

  • main.cpp 编译可以通过,不会报错,因为 call 当中的函数地址也是假的
  • math.cpp 编译可以通过不会报错,但是 math.obj 当中并不会有函数实体生成
  • 因为 math.cpp 并不知道 main.cpp 的存在,不知道传入了什么样的参数类型,所以也不会进行实例化。

image-20210531001424141

所以一般将:一般将模板的声明和实现统一放到一个**.hpp文件**中 (hpp 文件 == h文件 + cpp文件)

image-20210531001711772

  • hpp 文件并不参与编译
  • 本质还是只有 main.cpp 进行编译,函数实体也生成在 main.obj 当中

image-20210531001833490


3、类模板

引出一个案例:在 C 语言当中的数组,数组在定义之后大小是固定的不能进行更改

需求:我们想要一个动态数组,大小可以动态进行改变


分析:

1、我们的数组应该放在栈空间?还是堆空间

答:堆空间,因为堆空间的申请和释放是动态的,可以根据程序员需求进行更改。

栈空间却是确定死的,由系统决定。

2、怎么实现动态扩容?

  • 申请一个更大的堆空间
  • 将前面的数据拷贝过去

分析为什么?

image-20210531004729565

新 new 一个int 可以吗?

  • 发现前面的堆空间丢了,造成了内存泄漏,并且前面的数据,我们也不能访问了。

image-20210531005051360

再使用一个新的指针?

  • 根本就不是一个数组了,数据也不连续,数组名也不同
  • 并且非常浪费栈空间,我们每添加一个数据,就需要一个新的指针。

image-20210531005222514


解决办法:

  • 申请一个更大的堆空间
  • 将前面的数据拷贝过去
  • 然后将旧的堆空间,进行 delete 。

image-20210531005701495

再来分析我们动态数组类当中,需要那些成员?

  • 指向堆内存的指针
  • 目前数组里面有多少元素
  • 初始化最多可以放多少数组。

image-20210531200153688


缺点:这个数组的数据比较单调,现在只能放 int 。

  • 我们希望可以存放,char、Point 、double 、等等其他的数据。

  • 使用类模板

怎么使用类模板 :

image-20210531202950212


image-20210531201311953


image-20210531201822430

同理:类模板也可放在 hpp 文件当中。


4、完善类模板

image-20210601214523310


类型转换

铺垫:

1、C 语言风格的类型转换符

  • (type)expression
  • type(expression)
int a = (int) 10.5;

2、C++中有4个类型转换符

  • static_cast
  • dynamic_cast
  • reinterpret_cast
  • const_cast

使用格式: xx_cast<type>(expression)

int a = 10;
double b = static_cast <double>(a);
double b = dynamic_cast <double>(a);
double b = reinterpret_cast <double>(a);
double b = const_cast   <double>(a);

注意:这些强制转换比较复杂


1、const_cast

一般用于去除const属性,将const转换成 非const

理解:本质其实就是为了骗过编译器,使代码可以编译通过。

  • p1 的本质还是 const Person 类型, p2 的本质就是 Person 类型。
  • 因为类型不同,不能直接进行赋值操作,所以需要进行类型转换。
  • 类型转换之后,p1 和 p2 的本质还是不变的

image-20210601012847976


对比一下,c语言的转化 和 c++ 转换:

  • 可以发现,根本没有区别
  • 本质的汇编都是一样的,都是用来骗过编译器

image-20210601013403175


2、dynamic_cast

  • 一般用于多态类型的转换,有运行时安全检测

多态本质:实现父类与各个子类之间的多态。(所以必定要涉及继承)

来看一下用法:

image-20210601014458684

修改:将 Person 类Student 类 变为多态类

image-20210601014643900


区别一下 C 语言的转换:可以明显看出是不一样的

  • C++ 的 dynamic_cast 多了一个安全检测

image-20210601014935741


怎么理解安全检测呢

  • 可以看出,当我们使用 子类指针指向父类指针 的时候, dynamic_cast 转换的指针直接将他们变为 0000000.

image-20210601015529760

第二种安全检测:

  • 两个毫不相关的类不是赋值。

image-20210601020201252


3、static_cast

  • 对比dynamic_cast,缺乏运行时安全检测
  • 不能交叉转换(不是同一继承体系的,无法转换,这点和 dynamic_cast 类似)
  • 常用于基本数据类型的转换、非const转成const
  • 使用范围较广

不能交叉转换(不是同一继承体系的,无法转换,这点和 dynamic_cast 类似)

image-20210601020637112


对比dynamic_cast,缺乏运行时安全检测

image-20210601020728624


4、reinterpret_cast

我们常用一般只有两个:

  • dynamic_cast : 因为它有安全检测。
  • reinterpret_cast :属于比较底层的强制转换

  • 属于比较底层的强制转换,没有任何类型检查和格式转换,仅仅是简单的二进制数据拷贝
  • 可以交叉转换
  • 可以将指针和整数互相转换

分析存储方式:

x86 是小端模式:

  • int a = 10;
  • 低地址 ————> 高地址: 0a 00 00 00
  • 读取变量 a 的时候:先读取高地址的值。

image-20210601215458743

继续分析 :double = 10 的内存存储:

  • 可以看出浮点数的存储方式和我们的认知不一样

  • double 是 8 字节,我们可能本能以为是: 0x 0a 00 00 00 00 00 00 00

image-20210601215806262


思考:int 强制转化为 double 类的时候:

  • 并不是简简单单的二进制拷贝

image-20210601220120149

image-20210601220855162

如果我们想要进行二进制拷贝,怎么办呢?

解决: 使用 reinterpret_cast 转换即可。(不会进行任何类型转换,直接进行二进制拷贝)

image-20210601220722527