C++ 操作重载与类型转换 《C++Primer》第14章 读书笔记

40 篇文章 7 订阅

本章内容一览:

在这里插入图片描述

1、基本概念 和 限制条件

  • 只有重载的函数调用运算符operator()能有默认实参,其他重载运算符不能有默认实参。
  • 一个重载的运算符,至少含有一个类类型的参数。
  • 可被重载的运算符:在这里插入图片描述
  • 一般不重载&& || , &这几个运算符
  • 返回值类型一般与内置版本兼容:
    • 逻辑和关系运算符返回bool
    • 算术运算符返回类类型的值
    • 赋值运算符 和 复合运算符 返回左侧运算对象的引用
  • 若一个运算符是成员函数,则他的左侧运算对象绑定到隐式的this指针上。这就是为什么后面重载<< >>运算符不能是成员函数的原因。

2、定义为 成员 还是 非成员?

在这里插入图片描述
什么是对称性呢?举个例子:
计算一个intdouble的和,他们中的任意一个都可以是左侧运算对象或右侧运算对象,所以加法就是对称的。如果想计算 含有类对象混合的表达式,则运算符必须定义成非成员函数

当把运算符定义为成员函数时,他的左侧运算对象必须是运算符所属类的对象。
在这里插入图片描述
练习题:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
说明g:==具有对称性,所以定义为非成员

3、重载输入运算符

3.1 注意处理输入失败

在这里插入图片描述

X& operator>>(istream& is, X& x)
{
	is >> 输入巴拉巴拉.....
	if (!is)		这个检查是一次性检查,没有逐个检查每个读取操作
		x = X();	输入失败,赋予对象默认状态
	else		
		某些成员需要由刚刚出入的数据计算出来,则在确保输入正常的情况下进行
	return is;
}

发生输入错误可能是如下原因:

  • 输入的数据类型与代码要求的不符
  • 读取到达文件末尾,或遇到其他错误

3.2 标示错误

在这里插入图片描述

4、算术运算符 和 复合赋值运算符

在这里插入图片描述
因为使用复合赋值运算符来实现算数运算符是很方便的。如下所示:

Sale_data operator+(const Sale_data& s1, const Sale_data& s2)
{
	Sale_data sum = s1;
	sum += s2;		直接使用复合赋值运算符就行了,而不用逐成员相加
	return sum;
}

5、相等运算符

  • 定义了==也应该定义!=
  • != ==其中一个定义好之后,直接用他去实现另一个
bool operator==(const Sale_data& s1, const Sale_data& s2)
{
	return s1.isbn() == s2.isbn() &&
		   s1.units_sold == s2.units_sold &&
		   s1.reevnue == s2.revenue;
}

bool operator!=(const Sale_data& s1, const Sale_data& s2)
{
	return !(s1 == s2);		就像这样,不要傻不拉几再逐个比一遍了
}

6、关系运算符

在这里插入图片描述
也就是说要有严密的逻辑自洽。当两个对象经!=运算返回true,那么必有一个是<另一个的。

7、赋值运算符

之前学的拷贝赋值运算符只是赋值运算符重载中的一种。我们还可以用很多其他类型给目标赋值。
如下面的例子:

class StrVec
{
public:
	StrVec& operator=(initializer_list<string> l)
	{
		auto data = alloc_n_copy(l.begin(), l.end());
		free();				不管什么样的赋值运算符都要先销毁掉原来的内存空间
		elements = data.first;
		first_free = cap = data.second;
		return *this;
	}
};

int main()
{
	StrVec s;
	s = { "hello", "world" };
	return 0;
}

上例实现了自定义类型StrVec的列表赋值。

  • 无论什么赋值运算符,都要先销毁原来的内存。在创建新空间。
  • 类的所有赋值运算符都必须定义为成员函数。
  • 这种非拷贝赋值运算符就不用检查自赋值了。

在这里插入图片描述

8、下标运算符

  • 下标运算符必须是成员函数
  • 类的下标运算符通常定义一对,一个返回普通引用,一个返回常量引用

如下例所示:

class StrVec
{
public:
	string& operator[](size_t n) { return elements[n]; }
	const string& operator[](size_t n) const { return elements[n]; }	const的对象使用下标
																		运算符,就用这个版本
};

9、递增和递减运算符

  • 通常定义为成员函数。
  • 要同时定义前置后置 版本

9.1 前置版本

class X
{
public:
	X& operator++() { x++; return *this; }
	X& operator--() { x--; return *this; }
private:
	int x = 0;
};

注意:返回的是对象的引用。内置版本也是这样的。

9.2 后置版本

虽说是后置版本,但是名字完全一样,所以为了重载他们勉强加一个int类型的参数进去。这个int的作用只有区分重载版本:

class X
{
public:
	X operator++(int);		这里只是声明
	X operator--(int);		函数的定义和前置有所区别
private:
	int x = 0;
};

注意:返回的是对象的原值(递增减之前的值)。内置版本也是这样的。
在递增减之前要首先记录对象的状态

X operator++(int)	不需要用到int形参,无需为其命名
{
	X x = *this;	记录原始状态,便于待会返回
	++*this;	递增,直接用已实现的前置递增更方便,当然,是对于更复杂的类来说的,这个类太简单了
	return x;		返回对象原始状态的副本(拷贝)
}

X operator--(int)	与后置递增同理
{
	X x = *this;
	--*this;
	return x;
}

10、成员访问运算符*->

箭头运算符必须是类的成员,解引用运算符通常也是类的成员,但也可以不是。

重载 ->

  • 箭头运算符的作用只能是获取成员
  • 对于形如point->member的表达式,point只能是指向类对象的指针或者是一个重载了operator->的类的对象,绝无其它。
    在这里插入图片描述

11、函数调用运算符

必须是成员函数。

11.1 函数对象

函数对象其实是类,这个类定义了函数调用运算符,当使用这个类的对象调用重载的函数调用运算符时,其形式和调用函数一毛一样,所以叫做函数对象。下面我们举个例子:

class X
{
public:
	int operator()(int value)
	{
		return value > 0 ? value : -value;
	}
};

int main()
{
	X x;
	int a = x(-42);			a 的值是42
}

你看,a = x(-42),这多像函数调用,可以称X为函数对象。

函数对象通常作为泛型算法的实参。

就类似lambda表达式那个作用,还是举个例子,让我们把上面的类改一改。

class X
{
public:
	X(ostream& o = cout, char c = ' ') : os(o), sep(c) { }		构造函数
	void operator()(const int& x)
	{
		os << (x > 0 ? x : -x) << sep;
	}
private:
	ostream& os;
	char sep;
};

int mina()
{
	X x;
	vector<int> v{ -1, 3, 5, -9, -54, 9, -1, -3, -2 };
	for_each( v.begin(), v.end(), X(cout, '\n') );
}

修改后的类X有两个成员;分别表示 输出流 和 间隔符。
他的函数调用运算符,能够向给定的输出流以给定间隔符 输出传入的int的绝对值。

main函数中我们使用for_each对每个v的元素调用X的函数调用运算符,就实现了像标准输出流输出v中所有元素的绝对值,并以换行符为间隔。

这就是上面说的:函数对象通常作为泛型算法的实参



11.2 深入剖析lambda表达式

当我们编写一个lambda表达式,编译器将该表达式翻译为一个未命名类未命名对象,这个对象是一个函数调用运算符
哇,原来如此,惊为天人,原来是这样。我们举个例子:

stable_sort(words.begin(), words.end(), 
					  [](const string& a, const string& s2) { return a.size() < b.size(); }

上面实现了根据长度将string排序。其lambda表达式的行为类似于下面的函数对象

class shorter
{
	bool operator()(const string& s1, const string& s2)
	{
		return s1.size() < s2.size();
	}
};

调用这个函数对象:

stable_sort( words.begin(), words.end(), shorter() );

通过这个例子可一目了然地看到,上面的 lambda表达式和下面的函数对象是等价的。

那么 捕获 又是怎么一回事呢?

两种情况

  • lambda通过引用捕获变量时,由程序确保引用的对象确实存在,故,编译器直接使用该引用, 不用在lambda产生的类中将其存储为数据成员
  • lambda通过值捕获方式捕获变量时,变量是被拷贝到lambda的,故,lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数构造函数使用捕获到的值初始化数据成员

焯!连起来的,知识连起来了!太妙了!!!还是举个例子:

auto first_iter = find_if(words.begin(), words.end(), 
									[size](const string& a) { return a.size() >= size; }

上面能够获得指向序列中第一个长度>= size的迭代器。
lambda产生的类形如:

class size_cpomare
{
public:
	size_compare(size_t n) : size(n) { }	这里得构造函数使用捕获到的值初始化 size 成员
	bool operator(const string& s)
	{
		return s.size() >= size;
	}
private:
	size_t size;	这里有一个名为 size 的成员,准们保存捕获到的 size 的值
};

调用这个函数对象:

auto first_iter = find_if( words.begin(), words.end(), size_compare(size) );
lambda表达式产生的类不含默认构造函数、赋值运算符 及 默认析构函数;是否含有默认的拷贝 / 移动构造函数 则通常要视捕获的数据成员类型而定。
这里有个好问题:

在这里插入图片描述

11.3 标准库定义的函数对象

下面是标准库定义的表示算术关系逻辑 运算符的类,每个类都定义了调用运算符,可以执行相应的运算。
在这里插入图片描述
标准库定义的这些类都非常好,可以进行我们做不到的操作。例如通过比较指针的地址排序指针序列
这些指针可以是毫无关系的指针,我们直接比较两个指针会产生未定义的行为,使用标准库的less则不会。

vector<string*> v;
sort(v.begin(), v.end(), [](string* a, string* b) { return a < b; } 
					错误,v里面的指针彼此之间没有关系,<会产生未定义行为。
sort( v.begin(), v.end(), less<string*>() );		正确,标准库定义的 less 是良好的

注意
关联容器默认使用less<key_type>对元素排序,因此可以定义一个指针的set或在map中使用指针作为关键字而无须声明less

精彩练习:
在这里插入图片描述

答案:这个办法就很妙!隐式的取模的结果转换成bool类型正好可以用于判断是否能整除

bool divided_by_all(vector<int>& vec, int dividend)
{
	return count_if( vec.begin(), vec.end(), bind2nd(modulus<int>(), dividend) ) == 0;
}

11.4 可调用对象 和 function类型

目前为止见过的可调用对象有:

  • 函数
  • 函数指针
  • lambda表达式
  • bind创建的对象
  • 重载了函数调用运算符的类
  • 标准库定义的函数对象(其实这个也算上一条里的)

和其它对象一样,可调用对象也有类型。例如,每个lambda表达式都有自己唯一的(未命名)类类型。函数和函数指针也有类型,他们的类型由其返回值类型和实参类型决定

但是,不同类型的可调用对象,可能共享同一种调用形式

调用形式:是可调用对象的 返回值 参数列表 组合

举个例子,现有如下三种可调用对象:

auto add1 = [](int a, int b) { return a + b; }	   这是lambda表达式,是 未命名 的类,
											用编译器打印	typeid(add1).name()	,是下面这串东西
											class <lambda_1df7cfe0482636e736c1805ed2e94511>

int add2(int a, int b) { return a + b; }		这是函数,是 int (int, int) 类,
											它的类型其实就是调用形式

class add3		这是函数对象,是 add 类
{
public:
	int operator()(int a, int b) { return a + b; }
};

嗯,他们都是可调用表达式,类都不相同。但是他们都有 共同的调用方式

那就是:
int(int, int)		返回值 和 参数列表的组合,

根据这一特性,C++推出一个 function,允许我们 用一个对象 存储 具有相同调用方式的 可调用对象

function

在这里插入图片描述
function对象允许我们用一个对象,存储一类具有相同调用方式的可调用对象。
就用上面的三个可调用对象举个例子:

				f1, f2, f3, 都具有相同的类型,都是 function<int(int, int)> 类
function<int(int, int)> f1 = add1;			f1 存储了可调用对象 add1
function<int(int, int)> f2 = add2;			f2 存储了可调用对象 add2
function<int(int, int)> f3 = add3();		f3 存储了可调用对象 add3

cout << f1(1, 2);		打印3
cout << f2(1, 2);		打印3
cout << f3(1, 3);		打印3

下面使用function做一个简易计算器,阅读代码,体会一下function的用处:

class mul		函数对象,实现乘法
{
public:
	int operator()(int a, int b) { return a * b; }
};

int divi(int a, int b)		函数,实现除法
{
	return a / b;
}

int main()
{
	map<string, function<int(int, int)>> cal;	 一个 map,运算符号为键,其实现作为值
	
	auto mod = [](int a, int b) { return a % b; };		lambda表达式,实现取模
	插入元素:
	cal.insert({ "+", plus<int>() });						  标准库的加法函数对象
	cal.insert({ "-", [](int a, int b) { return a - b; } });  未命名的 lambda 对象,实现减法
	cal.insert({ "*", mul() });								  函数指针
	cal.insert({ "/", divi });								  用户定义的函数对象
	cal.insert({ "%", mod });								  命名了的 lambda 表达式

	int a, b;
	string s;
	while (cin >> a >> s >> b)
	{
		cout << a << s << b << " = " << cal[s](a, b) << endl;
	}
}

上面代码中名为calmap是一种叫函数表的东西:
在这里插入图片描述

重载函数与function

我们不能直接重载函数的名字存入function类型的对象中。如下所示:

int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }

function(int(int, int)> cal = add;		错误,出现二义性,到底要存哪个 add

即便在声明cal前面加上了调用方式仍然不行
这时应该使用函数指针,而非函数的名字。

int (*fun_ptr)(int, int) = add;		指针'fun_ptr'所指的'add'是接受两个 int 的版本
cal = fun_ptr;			这回对了

12、重载、类型转换 与 运算符

我们已经见过很多内置类型之间的类型转换了,下面将学习自定义类的类型转换

这其中包含两个方向的转换,从其他类型转换到本类型,通过转换构造函数来完成。
以及从本类型转换成其他类型,通过类型转换运算符完成。

转换构造函数其实和普通的构造函数没啥区别,只不过他只有一个其它类型的参数,用这个参数来构造一个本类型的对象。

12.1 类型转换运算符

类型转换函数的一般形式:
operator type() const;		type 是要转换成的类型

type的类型要求是能作为函数的返回类型void除外)。因此不允许传换成数组函数类型,但可以是数组指针、函数指针或者引用类型。

注意:

  • 类型转换运算符必须是成员函数
  • 不能声明返回类型
  • 形参列表必须为空
  • 函数应该用const修饰

下面举一个简单的例子:

class X
{
public:
	X(int i = 0) : val(i) { }			转换构造函数, int 类型转 X 类型
	operator int() { return val; }		类型转换运算符,X 类型转 int 类型
private:
	size_t val;					
};

X x;
x = 4;		4 先隐式转换成 X 类型,然后调用合成的赋值运算符
x + 3;		x 隐式的转换成 int 类型,然后执行整数加法

x = 3.14;	3.14 转换成 intint 再转换成 X
x + 3.14;	x 先转换成 intint 再转换成 double

哎停停停停停!最后两行转换代码是不是有什么问题?怎么会自动执行两步类型转换呢???
之前学的分明是:编译器只会自动执行一步类型转换。

嗯,确实只会执行一步。不过这里出现了新的特性

类型转换运算符的特性:

用户定义的隐式的类型转换可以置于一个内置类型转换之前或之后,并与之一起使用只有这种情况下,允许执行两步类型转换。

上面的最后两条语句都是有自定义的类型转换参与,所以可以进行。

因此,可以把任何算术类型传递给X的构造函数,同理,也能使用类型转换运算符把一个 X对象转换成int,然后把得到的int转换成任何其他算术类型

不能滥用类型转换运算符:

直接上例子,在早期C++版本中,下面的代码是能编译通过的:

int i = 10;
cin << i;		这里 cin 后接左移运算符,显然是错的,但却能通过编译

istream本身确实没定义<<,能通过编译是因为,在早期C++版本的这种情况下,istream能够隐式的转换成bool类型,由于bool是算术类型,所以能执行<<,就是将bool的值左移10位。这显然不是我们想要的结果,他应该报错才对。

现实的类型转换运算符:

为了避免上述情况,C++11引入显式类型转换运算符
在转换运算符前用explicit修饰,就不会自动执行这一类型转换。同时,(一般情况下)也不能用于隐式类型转换。只能显示的使用它。如下所示:

class X
{
public:
	operator int() { return val; }
	int val;
};

X x;
x + 3;						错误,隐式转化
static_cast<int>(x) + 3;	正确,显示强转转换
上面的规定存在 例外

如果表达式被用作条件,则编译器会将explicit修饰的类型转换运算符自动应用于它。在下列位置,显式类型转换将被隐式的执行:

  • ifwhiledo语句的条件部分
  • for语句的条件表达式
  • !||&&这些逻辑运算符
  • ? : 条件运算符的条件表达式

这已经是明示你了,C++就是想让你在自定义的类型转换运算符的时候,使用explicit修饰,同时,要把类类型转换成bool类型,专门用于这些判断是否为真的情况。不过这是我个人的理解,有时候肯定也是有其他应用情况的。
在这里插入图片描述
在这里插入图片描述

12.2 避免有二义性的类型转换

有四种由类型转换导致的二义性错误。

12.2.1 两个类定义了转换到同一个类型的成员

图示:
在这里插入图片描述

class A
{
public:
	A(const B&);	转换构造函数,B 类型转 A 类型
	A fun(const A&);
};

class B
{
public:
	operator A() const;	转换运算符,B 类型转 A 类型
};

B b;
A a = fun(b);		fun()里需要一个A类型对象,b需要转换,但是会产生二义性
						是 fun(B::operator()) 呢? 还是 fun(A::A(const B&)) 呢?

你现在有两种由B转换成A的方法,那到底用哪个???这就产生了二义性。
想要区分两种方法,就必须显示的调用相关成员:

A a1 = f(b.operator A());	用B的类型转换运算符
A a2 = f( A(b) );			用A的构造函数
解决方法:

不要定义出两种转换成同一类型的方法噻。有一种方法不就够了吗

12.2.2 涉及到内置类型的多重类型转换

图示:
在这里插入图片描述
直接上例子:

class A
{
public:
	A(int = 0);		int 转换成 A
	A(double);		double 转换成 A
	operator int() const;		A 转换成 int
	operator double() const;	A 转换成 double
};

void fun(long double);		函数,需要 long double 做成员

A a;		a 到底是先转 int 再转 long double 呢?
fun(a);		还是先转 double 再转 long double 呢?
			
lone lg;	lg 到底是先转 int 再转 A 呢?
A a2(lg);	还是先转 double 再转 A 呢?
解决方法:

少定义点类类型 和 算术类型之间的转换,很多算术类型间本来就能转换,你还定义这么多,不是添乱??

在这里插入图片描述

12.2.3 重载 和 转换构造函数

直接上例子:

class A
{
public:
	A(int);		int 转 A
};

class B
{
public:
	B(int);		int 转 B
};

void f1(const A&);		用 A 重载
void f1(const B&);		用 B 重载
f1(10);		显然,又二义性了

f1( A(10) );	显式的用A,避免二义性

虽说可以通过显式的构造,但是存在这种操作,说明程序设计存在缺陷。

12.2.3 重载 和 转换构造函数(比上一个稍微复杂点)

这个和上一个很想,但要复杂一点:

class A
{
public:
	A(int);		int 转 A
};

class B
{
public:
	B(double);		double 转 B
};

void f1(const A&);		用 A 重载
void f1(const B&);		用 B 重载
f1(10);		哎!哎哎哎!!10int 欸!是不是不会二义性了?
			不不不!很可惜,还是会二义性。

分析一波:

  • f1(const A&)成立,int可以转A,而且是精确匹配
  • f1(const B&)成立,int可先转double,然后double再转B

啊?这不是有一个可精确匹配吗?虽然但是,不可以!
编译器会报错,这是二义性。
在这里插入图片描述
就是因为你有两个类,如果只有一个类的话,里面有intdouble的构造函数就不会有二义性了。

这小节很绕,看看这个练习题:
在这里插入图片描述
14.50:在这里插入图片描述
看好了LongDouble并不是内置的long double,不存在精确匹配一说。
14.51:在这里插入图片描述
这一题告诉我们,内置类型转换优先于自定义的转换构造函数。

12.3 函数匹配 与 重载运算符

注意:本节的讨论的核心是重载运算符

一言以蔽之,就是下面这句话:
在这里插入图片描述
说详细点就是:
当我们使用重载运算符作用于类类型的运算对象时,候选函数中包含该运算符的普通非成员版本内置版本。除此之外,如果左侧运算对象是类类型,则定义在该类中的运算符的重载版本也包含在候选函数内。

也就是说,若a是一种类类型,则表达式a sym b的等价可能是:

a.operator(b);		a 的成员函数
operator(a, b);		一个普通的非成员函数

产生二义性的例子:

class X
{
	friend X operator+(const X&, const X&);
public:
	X(int = 0);
	operator int() const { return val; }
	int val;
};

X x1, x2;
X x3 = x1 + x2;		使用重载的operator+成员
int i = s3 + 0;		产生二义性

例题:
这个例题算是您能遇见的最复杂的情况了,把他搞明白,就没问题了。
在这里插入图片描述
这是 上图缺少的 LongDoubleSmallInt的定义:
在这里插入图片描述在这里插入图片描述
对于第一个式子ld = si + ld;在这里插入图片描述
对于第二个式子ld = ld +si:仍然使用同样的策略
在这里插入图片描述

总结:

当两个类类型相加时,优先考虑一方进行类型之间的转换,再相加,如果不行,再考虑双方都转换成内置类型相加。

再来一个,这个题主要想让你知道,出现二义性,要怎么改
SmallInt的定义和上题一样在这里插入图片描述
很明显会产生二义性
在这里插入图片描述

所以我们只需 显式的 让他走其中一条路即可。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值