啃书《C++ Primer Plus》 面向对象部分 类型转换——转换构造函数 与 转换函数

啃书系列持续更新ing,关注博主一起xiao习鸭~

系列文章:

语言基础部分:

面向对象部分:


这一篇,我们来专门介绍对象的类型转换问题。这个问题涉及到两类函数,一类函数就是我们熟知的构造函数,经过这类函数可以将其他类型转化为该对象类型;另一类函数就是转换函数了,这类函数需要用到 operator关键字,是一类专门用于将对象转化为其他类型的函数。

本文的思维导图如下:
在这里插入图片描述


转换构造函数

转换构造函数是构造函数的一种,具体一些,是哪些可以只接受一个参数进行构造的构造函数。

在出现 自定义类型对象 = 其他类型对象(变量) 这种情况时,我们的本能反应是利用等号后面给定的数值重新构造前面的对象,就像是重新创建一个新的对象一般,只不过这里不创建新的对象,而是重新构造等式左边的对象。

需要特别注意的是

  • 在这个过程中,没有创建新的对象,只是根据等式右边的值重新构造
  • 这个过程重新构造了这个对象,所有成员都会被初始化
  • 类型转换仅是看上去的效果,实质上是对象根据其他类型的重新构造。

对于上面的说明,我们来看个粟子:

/*在不同的构造函数中给成员变量以不同的赋值。通过赋值调用转换构造函数*/
class A{
public:
    A(int k):c(10){this->k = k;p = 10;}		//利用一个整型进行构造,可看做转换构造函数
    A():c(20){k = 9;p = 9;}					//无参构造函数
    int k;
    int p;
    int c;
};

int main()
{
						/*打印对象a的地址以及成员进行查看*/
	A a;
    cout << "members: " << a.k << " " << a.p << " " << a.c << " address:" << &a << endl;
    a = 30;
    cout << "members: " << a.k << " " << a.p << " " << a.c << " address:" << &a << endl;
}

运行结果:
在这里插入图片描述
地址没有改变,对象还是那个对象,成员被转换构造函数初始化成了新的值。

当然,不仅是内置类型,自定义类型也可以参与转换:

#include<iostream>
using namespace std;
class B{
public:
    int k = 10086;
};
class A{
public:
    A():k(10){}
    A(B& b){this->k = b.k;}
    int k;
};
int main()
{
    A a;
    B b;
    a = b;
}

另外,在上一篇说明构造函数的博文中提到了实例化对象的两种方式:

显式调用: 类名 对象名 = 类名(参数列表);
隐式调用: 类名 对象名(参数列表);

现在,由于转换构造函数机制,可以产生一种新的实例化对象的方式:

转换构造函数 类名 对象名 = 可转换对象/变量;

这种方式本质上是指导编译器调用相应的构造函数。使用上面的例子,我们可以看到实例化A类对象的若干种方式:

B b;
A a;		//隐式调用无参构造函数,省略括号
A a(10);	//隐式调用参数为整型的构造函数
A a = A(10);//显式调用参数为整型的构造函数
A a = 10;	//使用类型转换调用参数为整型的构造函数
A a = b;	//使用类型转换调用参数为B类型的构造函数	
...

转换函数

说完了转换构造函数,下面介绍转换函数。在刚刚的转换构造函数中,对象接受转换的一方,现在当类对象出现在了等式的右边成为右值时,仅有构造函数就不能满足这样的赋值了。此时,就需要使用一种特殊的函数——转换函数,使对象可以主动地转换为其他类型。

这个特别的成员函数,它长这个亚子:

 operator 目标类型() [const]{}

特别的函数有特别的规则:

  • 转换函数必须是类方法
  • 转换函数不能指定返回类型
  • 转换函数不能有任何参数

对于第二点,该函数必须有返回值但是不能指定返回值的类型,返回值类型必须是目标类型。相信这点不难理解,毕竟我们是希望将这个类转化为目标类型。

使用它的时候,对象可以被显式的强制转换也可以被隐式转换:

显式转换: 目标类型对象/变量 = 目标类型(被转换对象);
隐式转换: 目标类型对象/变量 = 被转换对象;

来借助一个粟子理解转换函数的使用:

class B{};
class A
{
public:
	A(int k){this->k = k;}
	operator int()	//类型转换为int
	{
		return k;
	}
	operator B()	//类型转换为B
	{
		return B();
	}
private:
	int k;
};
int main()
{
	A a = A(9);
	int k = a;		//隐式的将a转换成int类型
	int kk = int(a);//显式的将a转换成int类型
	B b;
	B c;
	b = a;			//隐式的将a转换为B类型
	c = B(a);		//显式的将a转换为B类型
}

在这个例子中,我们在类A中添加了两个转换函数,分别将A类对象转换为 int 类型和 B 类型。在主函数中将A类对象作为右值分别赋值给一个整型变量和一个 B 类型对象。


说完了两种类型转换,下面呢来谈谈使用类型转换时会遇到的一些问题:

出现类型转换的场景

首先一个,就是在何时,会使用到这些类型转换。
在上面介绍这些函数时,在赋值和初始化时使用到了类型转换。这些类型转换很明显也很常见。还有一些类型转换也是常见的,但却不那么明显,那就是在进行函数操作时发生的参数按值传递以及返回值

看个粟子:

#include<iostream>
using namespace std;
class A
{
public:
	A(int k){this->k = k;}
	operator int(){return k * k;}
private:
	int k;
};
int f(A a){return a;}
int main()
{
	cout <<f(4);
}

结果,如下:
在这里插入图片描述
在这个例子中,将整型作为参数按值传递给一个参数为A类对象的函数,在这个位置产生了第一次类型转换,它调用了A类的转换构造函数,用整型实参构造了形参。

返回结果时,返回的是A类对象而非整型,在此时产生了第二次类型转换,将A类对象转换成为了整型并将其返回,这次调用到的是

显式转换与隐式转换——explicit关键字

前面说了这么多有关类型的转换,不难发现的是,不论是转换构造函数还是转换函数,显式的转换和隐式的转换都是被允许的。
这样一来就会导致一些问题,程序很可能在意想不到的地方进行某次类型转换,导致程序出bug。
解决这个问题,需要对类型转化进行一些限制,让他们不能再默认的发生。也就是限制隐式转换。

explicit关键字就起到了这个作用。被explicit修饰的转换函数和转换构造函数不能够在进行隐式的调用而必须被显示的调用。
我们还是用上面函数调用的粟子,并对它进行修改:

#include<iostream>
using namespace std;
class A
{
public:
	explicit A(int k){this->k = k;}			//不能隐式调用
	explicit operator int(){return k * k;}	//不能隐式调用
private:
	int k;
};
int f(A a){return a;}	//隐式的调用转换函数是错误的
int main()
{
	f(4);	//隐式的调用转换构造函数是错误的
}

上面程序在加上了explicit关键字修饰后,两个转换函数的神通被限定了起来,原有两处隐式转换的地方此时都会报错。
倘若想让程序正常的运行,就必须将隐式的调用改成显式的调用,向编译器充分的表达编程者的想法:

#include<iostream>
using namespace std;
class A
{
public:
	explicit A(int k){this->k = k;}			//不能隐式调用
	explicit operator int(){return k * k;}	//不能隐式调用
private:
	int k;
};
int f(A a){return int(a);}	//显式的将a对象转换为int类型
int main()
{
	f(A(4));	//显式的将整型转换为A类型对象
}

这时的程序,就可以正常的执行了。

将转换函数使用explicit关键字限制起来,在类型转换时尽可能多的使用显式的强制类型转换,可以有效地减少程序出bug的概率,是个人人应该遵守的好习惯。

两种转换的冲突

编译器一定报错的情况,就是一条语句存在歧义。当两种转换同时存在且都可以使一条语句合法时,编译器就会迷茫,报出二义性错误。

例如,当等式左边的对象由转换构造函数而右边的对象有转换函数,且二者都未被explicit关键字修饰,就会产生一个典型,两种转换同时存在的二义性错误。
代码:

#include"B.h"
class A{
public:
	A(B b);
}
#include"A.h"
class B(){
public:
	operator A();
}
int main()
{
	A a;
	B b;
	a = b;	//出现二义性错误
}

这个二义性错误,就是编译器不知道到底应该将对象a根据对象b进行构造,还是将对象b转换成A类对象赋值给A。
不要看二者意义好似相同,具体的函数实现可能是天差地别的。

解决这个问题的关键,就是方才所说的,将两个转换函数使用explicit关键字修饰。在编写赋值语句时,程序员必须清晰明了的体现它的调用意图,指出具体的转换方案:

#include"B.h"
class A{
public:
	explicit A(B b);
}
#include"A.h"
class B(){
public:
	explicit operator A();
}
int main()
{
	A a;
	B b;
	a = A(b);	//对编译器不存在二义性错误
}

这里的调用不存在二义性错误,不过它看上去仍然很具有迷惑性,从阅读代码者的角度来看,还是不能分清具体调用的那个函数。但是对于编译器却很清晰,因为编译器会在这种情况下优先调用构造函数(实际上从形式上来看,这个结构也更像是在调用构造函数。)

猜你喜欢

转载自blog.csdn.net/wayne_lee_lwc/article/details/105783965
今日推荐