第十四章 重载运算与类型转换

重载运算

基本概念

  • 重载运算符是具有特殊名字的函数:由关键字operator和其后要定义的运算符号共同组成。
  • 当一个重载的运算符是成员函数时,this绑定到左侧运算对象。动态运算符符函数的参数数量比运算对象的数量少一个
  • 只能重载大多数的运算符,而不能发明新的运算符号。
  • 重载运算符的优先级和结合律跟对应的内置运算符保持一致。
  • 调用方式:
    • data1 + data2;
    • operator+(data1, data2);
  • 是否是成员函数:
    • 赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员。
    • 复合赋值运算符一般来说是成员。
    • 改变对象状态的运算符或者和给定类型密切相关的运算符通常是成员,如递增、解引用。
    • 具有对称性的运算符如算术、相等性、关系和位运算符等,通常是非成员函数。

运算符:

可以被重载 不可以被重载
+, -, *, /, %, ^ ::, .*, ., ? :,
&, 竖线, ~, !, ,, =
<, >, <=, >=, ++, --
<<, >>, ==, !=, &&, `
+=, -=, /=, %=, ^=, &=
|=, *=, <<=, >>=, [], ()
->, ->*, new, new[], delete, delete[]

输入和输出运算符

重载输出运算符<<

  • 第一个形参通常是一个非常量的ostream对象的引用。非常量是因为向流中写入会改变其状态;而引用是因为我们无法复制一个ostream对象。
  • 输入输出运算符必须是非成员函数。
点击查看代码
左移运算符重载:
1.不要乱用运算符重载
2.内置数据类型的运算符 不可重载
3.cout << 直接对Person自定义数据类型, 进行输出
4.写道全局函数中 ostream& operator<<(ostream& cout, Person& p1);
5.如果重载时想访问 p1 的私有属性,那么全局函数要做Person的友元函数

#include<iostream>
using namespace std;


class Person
{

	friend ostream& operator<<(ostream& cout, Person& p1);

public:
	Person() {}
	Person(int a, int b)
	{
		this->m_A = a;
		this->m_B = b;
	}

	/*
	* //重载左移运算符不可以写到成员函数中
	* 原因:
	*如果让成员函数重载,调用的方法必须是形如:p1 << ,而不能用cout <<
	void operator<<()
	{

	}
	*/
private:
	int m_A;
	int m_B;

};
//cout 属于 ostream 类型
//要保证全局只有一个cout,不要自己创建一个新的cout,所以用引用的方式传参
//cout << 直接对Person数据类型进行输出
ostream& operator<<(ostream &cout,Person & p1)//第一个参数:cout,第二个参数:p1
{
	cout << "m_A = " << p1.m_A << " m_B = " << p1.m_B << endl;
	return cout;
}


void test01()
{
	Person p1(10, 10);

	//需求:cout << p1 << endl; 这行代码能直接而输出 p1.m_A 和 p1.m_B 的值
	cout << p1;//重写后的 << 返回值是void,就变成了void << endl;
	//因此,返回值是void不太合适,需要return 一个 ostream的引用:ostream &
	//此时,把int m_A和int m_B都写成私有的
 
	//那样的话,这个函数就用不了了,
	// ostream& operator<<(ostream & cout, Person & p1)//第一个参数:cout,第二个参数:p1
	//{
	//	cout << "m_A = " << p1.m_A << " m_B = " << p1.m_B << endl;
	//	return cout;
	//}

	cout << p1 << "哈哈" << endl;

	//有没有一种方法,可以在全局函数内访问到这个函数的私有属性? 当然有,那就是——友元!!!
}

int main()
{
	test01();

	system("pause");
	return 0;
}

重载输入运算符>>

  • 第一个形参通常是运算符将要读取的流的引用,第二个形参是将要读取到的(非常量)对象的引用。
  • 输入运算符必须处理输入可能失败的情况,而输出运算符不需要。

算数和关系运算符(+、-、*、/)

  • 如果类同时定义了算数运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算数运算符。
点击查看代码
加号运算符重载:
1.使用成员函数的方式	
2.使用全局函数的方式
 p1.operator+(p2)(调用成员方法) operator+(p1,p2)(调用全局方法)

#include<iostream>
using namespace std;

int a = 10;
int b = 10;
int c = a + b;

class Person
{
public:
	Person() {}
	Person(int a, int b) :m_A(a), m_B(b) {}

	//使用 + 运算符重载:
	//1.成员函数的方式
	//Person operator+(Person &p)//二元,成员函数没有参数是 一元
	//{
	//	Person tmp;//要调用默认构造,而默认构造已经被有参构造取代了,因此要重新提供一下
	//	tmp.m_A = this->m_A + p.m_A;
	//	tmp.m_B = this->m_B + p.m_B;
	//	return tmp;
	//}

	int m_A;
	int m_B;
};

//利用全局函数进行 + 运算符的重载
Person operator+(Person& p1, Person& p2)//二元:针对形参列表的个数
{
	Person tmp;
	tmp.m_A = p1.m_A + p2.m_A;
	tmp.m_B = p1.m_B + p2.m_B;

	return tmp;
}

//利用全局函数进行 + 运算符的重载
Person operator+(Person& p1, int a)//二元:针对形参列表的个数
{
	Person tmp;
	tmp.m_A = p1.m_A + a;
	tmp.m_B = p1.m_B + a;

	return tmp;
}

void test01()
{
	Person p1(1, 1);
	Person p2(1, 1);

	//Person p3 = p1 + p2;//编译器是不知道如何让两个Person类型进行运算的
	
	Person p3 = p1 + p2;//p1 + p2从什么表达式转变的?p1.operator+(p2)(调用成员方法) operator+(p1,p2)(调用全局方法)
	Person p3 = p1 + 10;//重载的版本
	cout << "p3 的 m_A:" << p3.m_A << endl;
	cout << "p3 的 m_B:" << p3.m_B << endl;

}

int main()
{
	test01();

	system("pause");
	return 0;
}

相等运算符==

  • 如果定义了operator==,则这个类也应该定义operator!=
  • 相等运算符和不等运算符的一个应该把工作委托给另一个。
  • 相等运算符应该具有传递性。
  • 如果某个类在逻辑上有相等性的含义,则该类应该定义operator==,这样做可以使用户更容易使用标准库算法来处理这个类。

关系运算符

  • 如果存在唯一一种逻辑可靠的<定义,则应该考虑为这个类定义<运算符。如果同时还包含==,则当且晋档<的定义和++产生的结果一直时才定义<运算符。

赋值运算符=

  • 我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。
  • 赋值运算符必须定义成类的成员,复合赋值运算符通常情况下也应该这么做。这两类运算符都应该返回左侧运算对象的引用。

下标运算符[]

  • 下标运算符必须是成员函数。
  • 一般会定义两个版本:
    • 1.返回普通引用。
    • 2.类的常量成员,并返回常量引用。

递增和递减运算符(++、--)

  • 定义递增和递减运算符的类应该同时定义前置版本和后置版本。
  • 通常应该被定义成类的成员。
  • 为了和内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。
  • 同样为了和内置版本保持一致,后置运算符应该返回递增或递减前对象的值,而不是引用。
  • 后置版本接受一个额外的,不被使用的int类型的形参。因为不会用到,所以无需命名。
点击查看代码

#include<iostream>
using namespace std;



class MyInteger
{

	friend ostream& operator<<(ostream& cout, MyInteger& p1);
public:
	MyInteger()
	{
		m_Num = 0;
	}
	//前置++重载
	MyInteger & operator++()
	{
		this->m_Num++;
		return *this;
	}

	//后置++重载
	MyInteger operator++(int)//这就是占位参数的唯一用处
	{
		//先使用临时数据,保存一下目前的值
		MyInteger tmp = *this;
		m_Num++;
		return tmp;
	}

	int m_Num;
};


ostream& operator<<(ostream & cout,MyInteger & myInt)
{
	cout << myInt.m_Num << endl;
	return cout;
}


void test01()
{
	MyInteger myInt;
	//++myInt;//前置++ //error只要用的是编译器不认的运算符,都会报:没有与这些操作符匹配的 "某某" 运算符
	//++myInt;
	//myInt++;//后置++

	cout << ++myInt << endl;//error只要用的是编译器不认的运算符,都会报:没有与这些操作符匹配的 "某某" 运算符
	cout << myInt++ << endl;//仅仅这样的话,这里会报错
//报错的原因是:
//MyInteger operator++(int)函数返回的是一个临时对象,临时对象‘一般'不能被修改
//但是C++11引入右值引用 && 之后,临时对象也是可以被修改的
//因此,左值引用(即非常量对象引用)不能绑定临时变量,而常量函数引用可以绑定临时对象。
//即,要把
	/*
改  ostream& operator<<(ostream & cout,MyInteger & myInt)
为  ostream& operator<<(ostream & cout,const MyInteger & myInt)
*/
}

int main()
{
	test01();

	system("pause");
	return 0;
}





1.前置++为什么要返回引用?
当把
	//前置++重载
	MyInteger operator++()
	{
		this->m_Num++;
		return *this;
	}

返回值改为MyInteger 的时候,

	cout << ++(++myInt) << endl;//2
	cout << myInt << endl;//1
原因:
因为,此时返回的已经不是引用了,(++myInt)运算后是一个临时数据,
临时数据再进行++操作,++的就不是myInt本体,而是另一个复制的临时数据(++myInt),
而此时再输出myInt,就只能是 1 而不是2 了,即后一个 ++ 没有作用到myInt本体身上。
如果想要像 原始的++ 操作一样,就需要返回引用。
返回引用就能保证:
第一次运算完的结果还是它本身,之后另一个++操作也是作用到它自身的。

2.后置++为什么要返回值

//后置++【return tmp】返回的是临时数据,临时数据在函数结束之后就被销毁了,临时数据返回引用的话会出现:
//第一次编译器会照顾一下给返回正确的值,第二次就直接出现问题了。
#include<iostream>
using namespace std;



class MyInteger
{

	friend ostream& operator<<(ostream& cout, const MyInteger& p1);
public:
	MyInteger() 
	{
		m_Num = 0;
	}
	//前置++重载
	MyInteger operator++()
	{
		this->m_Num++;
		return *this;
	}

	//后置++重载
	MyInteger operator++(int)//这就是占位参数的唯一用处
	{
		//先使用临时数据,保存一下目前的值
		MyInteger tmp = *this;
		m_Num++;
		return tmp;
	}

	int m_Num;
};


ostream& operator<<(ostream& cout, const MyInteger& myInt)
{
	cout << myInt.m_Num;
	return cout;
}


void test01()
{
	MyInteger myInt;
	//++myInt;//前置++ //error只要用的是编译器不认的运算符,都会报:没有与这些操作符匹配的 "某某" 运算符
	//++myInt;
	//myInt++;//后置++

	cout << ++(++myInt) << endl;//error只要用的是编译器不认的运算符,都会报:没有与这些操作符匹配的 "某某" 运算符
	cout << myInt << endl;
	cout << myInt++ << endl;//仅仅这样的话,这里会报错
}

int main()
{
	test01();
	int a = 10;

	++(++a);
	cout << a << endl;

	system("pause");
	return 0;
}

成员访问运算符(*、->)

  • 箭头运算符必须是类的成员。解引用运算符通常也是类的成员,尽管并非必须如此。
  • 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
  • 解引用和乘法的区别是一个是一元运算符,一个是二元运算符。

函数调用运算符

  • 可以像使用函数一样,调用该类的对象。因为这样对待类同时也能存储状态,所以与普通函数相比更加灵活。
  • 函数调用运算符必须是成员函数。
  • 一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
  • 如果累定义了调用运算符,则该类的对象称作函数对象

lambda是函数对象

  • lambda捕获变量:lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数。

标准库定义的函数对象

标准库函数对象:

算术 关系 逻辑
plus<Type> equal_to<Type> logical_and<Type>
minus<Type> not_equal_to<Type> logical_or<Type>
multiplies<Type> greater<Type> logical_not<Type>
divides<Type> greater_equal<Type>
modulus<Type> less<Type>
negate<Type> less_equal<Type>
  • 可以在算法中使用标准库函数对象。

可调用对象与function

标准库function类型

操作 解释
function<T> f; f是一个用来存储可调用对象的空function,这些可调用对象的调用形式应该与类型T相同。
function<T> f(nullptr); 显式地构造一个空function
function<T> f(obj) f中存储可调用对象obj的副本
f f作为条件:当f含有一个可调用对象时为真;否则为假。
定义为function<T>的成员的类型
result_type function类型的可调用对象返回的类型
argument_type T有一个或两个实参时定义的类型。如果T只有一个实参,则argument_type
first_argument_type 第一个实参的类型
second_argument_type 第二个实参的类型
  • 例如:声明一个function类型,它可以表示接受两个int,返回一个int的可调用对象。function<int(int, int)>

重载、类型转换、运算符

类型转换运算符

  • 类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下:operator type() const;
  • 一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const
  • 避免过度使用类型转换函数。
  • C++11引入了显式的类型转换运算符。
  • bool的类型转换通常用在条件部分,因此operator bool一般定义成explicit的。

避免有二义性的类型转换

  • 通常,不要为类第几个亿相同的类型转换,也不要在类中定义两个及以上转换源或转换目标是算术类型的转换。
  • 在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用。如果所需的用户定义的类型转换不止一个,则该调用具有二义性。

函数匹配与重载运算符

  • 如果a是一种类型,则表达式a sym b可能是:
    • a.operatorsym(b);
    • operatorsym(a,b);
  • 如果我们队同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。

C++类型转换

定义:

  • 类型转换(cast)是将一种数据类型转换成另一种数据类型。例如,如果将一个整型值赋给一个浮点类型的变量,编译器会暗地里将其转换成浮点类型。

  • 转换是非常有用的,但是它也会带来一些问题,比如在转换指针时,我们很可能将其转换成一个比它更大的类型,但这可能会破坏其他的数据。

  • 应该小心类型转换,因为转换也就相当于对编译器说:忘记类型检查,把它看做其他的类型。

  • 一般情况下,尽量少的去使用类型转换,除非用来解决非常特殊的问题。

  • 无论什么原因,任何一个程序如果使用很多类型转换都值得怀疑.

  • 标准c++提供了一个显示的转换的语法,来替代旧的C风格的类型转换。

  • 使用C风格的强制转换可以把想要的任何东西转换成我们需要的类型。那为什么还需要一个新的C++类型的强制转换呢?

  • 新类型的强制转换可以提供更好的控制强制转换过程,允许控制各种不同种类的强制转换。C++风格的强制转换其他的好处是,它们能更清晰的表明它们要干什么。程序员只要扫一眼这样的代码,就能立即知道一个强制转换的目的。

静态转换(static_cast)

  • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。
    • 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
    • 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
  • 用于基本数据类型之间的转换,如把int转换成char,把char转换成int。这种转换的安全性也要开发人员来保证。
class Animal{};
class Dog : public Animal{};
class Other{};

//基础数据类型转换
void test01(){
	char a = 'a';
	double b = static_cast<double>(a);
}

//继承关系指针互相转换
void test02(){
	//继承关系指针转换
	Animal* animal01 = NULL;
	Dog* dog01 = NULL;
	//子类指针转成父类指针,安全
	Animal* animal02 = static_cast<Animal*>(dog01);
	//父类指针转成子类指针,不安全
	Dog* dog02 = static_cast<Dog*>(animal01);
}

//继承关系引用相互转换
void test03(){

	Animal ani_ref;
	Dog dog_ref;
	//继承关系指针转换
	Animal& animal01 = ani_ref;
	Dog& dog01 = dog_ref;
	//子类指针转成父类指针,安全
	Animal& animal02 = static_cast<Animal&>(dog01);
	//父类指针转成子类指针,不安全
	Dog& dog02 = static_cast<Dog&>(animal01);
}

//无继承关系指针转换
void test04(){
	
	Animal* animal01 = NULL;
	Other* other01 = NULL;

	//转换失败
	//Animal* animal02 = static_cast<Animal*>(other01);
}

动态转换(dynamic_cast)

  • dynamic_cast主要用于类层次间的上行转换和下行转换;
  • 在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;
  • 在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全;
class Animal {
public:
	virtual void ShowName() = 0;
};
class Dog : public Animal{
	virtual void ShowName(){
		cout << "I am a dog!" << endl;
	}
};
class Other {
public:
	void PrintSomething(){
		cout << "我是其他类!" << endl;
	}
};

//普通类型转换
void test01(){

	//不支持基础数据类型
	int a = 10;
	//double a = dynamic_cast<double>(a);
}

//继承关系指针
void test02(){

	Animal* animal01 = NULL;
	Dog* dog01 = new Dog;

	//子类指针转换成父类指针 可以
	Animal* animal02 = dynamic_cast<Animal*>(dog01);
	animal02->ShowName();
	//父类指针转换成子类指针 不可以
	//Dog* dog02 = dynamic_cast<Dog*>(animal01);
}

//继承关系引用
void test03(){

	Dog dog_ref;
	Dog& dog01 = dog_ref;

	//子类引用转换成父类引用 可以
	Animal& animal02 = dynamic_cast<Animal&>(dog01);
	animal02.ShowName();
}

//无继承关系指针转换
void test04(){
	
	Animal* animal01 = NULL;
	Other* other = NULL;

	//不可以
	//Animal* animal02 = dynamic_cast<Animal*>(other);
}

常量转换(const_cast)

  • 该运算符用来修改类型的const属性。。
  • 常量指针被转化成非常量指针,并且仍然指向原来的对象;
  • 常量引用被转换成非常量引用,并且仍然指向原来的对象;
    注意:不能直接对非指针和非引用的变量使用const_cast操作符去直接移除它的const.
//常量指针转换成非常量指针
void test01(){
	
	const int* p = NULL;
	int* np = const_cast<int*>(p);

	int* pp = NULL;
	const int* npp = const_cast<const int*>(pp);

	const int a = 10;  //不能对非指针或非引用进行转换
	//int b = const_cast<int>(a); }

//常量引用转换成非常量引用
void test02(){

int num = 10;
	int & refNum = num;

	const int& refNum2 = const_cast<const int&>(refNum);
	
}

重新解释转换(reinterpret_cast)。。。。。

这是最不安全的一种转换机制,最有可能出问题。
主要用于将一种数据类型从一种类型转换为另一种类型。它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针.
posted @ 2023-02-10 21:44  nullptrException  阅读(16)  评论(0编辑  收藏  举报