C++类基础10——重载,类型转换,运算符

本文详细讨论了C++中的类型转换运算符,包括其定义、用法以及如何避免二义性问题。着重讲解了转换构造函数、重载运算符与用户定义类型转换之间的关系,以及在处理混合模式运算时的潜在问题。
摘要由CSDN通过智能技术生成

类型转换运算符

类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。

类型转换函数的一般形式如下所示:

operator type() const;

其中type表示某种类型。

类型转换运算符可以面向任意类型(除了void之外)进行定义,只要该类型能作为函数的返回类型。

因此,我们不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针及函数指针)或者引用类型。

类型转换运算符既没有显式的返回类型,也没有形参,而且必须定义成类的成员函数。

类型转换运算符通常不应该改变待转换对象的内容,因此,类型转换运算符一般被定义成const成员。

一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const。

定义含有类型转换运算符的类

举个例子,我们定义一个比较简单的类,令其表示0到255之间的一个整数:

class SmallInt 
{
public:

SmallInt(int i =0):val(i)
{
if (i <0||i >255)
throw std::out of range("Bad SmallInt value");
}

operator int() const { return val;}

private:
std::size t val;
};

我们的SmallInt 类既定义了向类类型的转换,也定义了从类类型向其他类型的转换,其中,构造函数将算术类型的值转换成SmallInt对象,而类型转换运算符将SmallInt对象转换成int:

SmallInt si;
si = 4;// 首先将 4 隐式地转换成 SmallInt,然后调用 SmallInt::operatoc=
si+3; // 首先将si隐式地转换成int,然后执行整数的加法

尽管编译器一次只能执行一个用户定义的类型转换,但是隐式的用户定义类型转换可以置于一个标准(内置)类型转换之前或之后,并与其一起使用。

因此,我们可以将任何算术类型传递给SmallInt的构造函数。

类似的,我们也能使用类型转换运算符将一个SmallInt对象转换成int,然后再将所得的int转换成任何其他算术类型:

// 内置类型转换将 double 实参转换成 int
SmallInt si =3.14; //调用SmallInt(int)构造函数

//SmallInt 的类型转换运算符将 si 转换成 int
si + 3.14; //内置类型转换将所得的int 继续转换成double

因为类型转换运算符是隐式执行的,所以无法给这些函数传递实参,当然也就不能在类型转换运算符的定义中使用任何形参。

同时,尽管类型转换函数不负责指定返回类型,但实际上每个类型转换函数都会返回一个对应类型的值:

class SmallInt;
operator int(SmallInt&); //错误:不是成员函数
class SmallInt
{
public:

int operator int() const; //错误:指定了返回类型
operator int(int =0) const;// 错误:参数列表不为空
operator int*()const { return 42;} // 错误:42不是一个指针
}

提示:避免过度使用类型转换函数

和使用重载运算符的经验一样,明智地使用类型转换运算符也能极大地简化类设计者的工作,同时使得使用类更加容易。然而,如果在类类型和转换类型之间不存在明显的映射关系,则这样的类型转换可能具有误导性。

例如,假设某个类表示Date,我们也许会为它添加一个从Date到int的转换。然而,类型转换函数的返回值应该是什么?一种可能的解释是,函数返回一个十进制数,依次表示年、月、日,例如,July 30,1989可能转换为int值19890730。同时还存在另外一种合理的解释,即类型转换运算符返回的int 表示的是从某个时间节点(比如January 1, 1970)开始经过的天数。显然这两种理解都合情合理,毕竟从形式上看它们产生的效果都是越靠后的日期对应的整数值越大,而且两种转换都有实际的用处。

问题在于Date类型的对象和int类型的值之间不存在明确的一对一映射关系。因此在此例中,不定义该类型转换运算符也许会更好。作为替代的手段,类可以定义一个或多个普通的成员函数以从各种不同形式中提取所需的信息。

类型转换运算符可能产生意外结果

在实践中,类很少提供类型转换运算符。

在大多数情况下,如果类型转换自动发生,用户可能会感觉比较意外,而不是感觉受到了帮助。

然而这条经验法则存在一种例外情况:于类来说,定义向bool的类型转换还是比较普遍的现象。

在C++标准的早期版本中,如果类想定义一个向bool的类型转换,则它常常遇到一个问题:

因为bool是一种算术类型,所以类类型的对象转换成bool后就能被用在任何需要算术类型的上下文中,特别是当istream向含有向bool的类型转换时,下面的代码仍将编译通过:

int i =42;
cin<<i;// 如果向bool的类型转换不是显式的,则该代码在编译器看来将是合法的!

这段程序试图将输出运算符作用于输入流。

因为istream本身并没有定义<<,所以本来代码应该产生错误。然而,该代码能使用istream的bool类型转换运算符将cin转换成bool,而这个bool值接着会被提升成int并用作内置的左移运算符的左侧运算对象。这样一来,提升后的bool值(1或0)最终会被左移42个位置。

这一结果显然与我们的预期大相径庭。

显式的类型转换运算符

为了防止上面这样的异常情况发生,C++11新标准引入了显式的类型转换运算符

class SmallInt 
{
public:
//编译器不会自动执行这一类型转换
explicit operator int() const { return val;}
//其他成员与之前的版本一致
};

和显式的构造函数一样,编译器(通常)也不会将一个显式的类型转换运算符用于隐式类型转换:

SmallInt si=3; //正确:SmallInt 的构造函数不是显式的
si + 3;//错误:此处需要隐式的类型转换,但类的运算符是显式的
static_cast<int>(si) +3; // 正确:显式地请求类型转换

当类型转换运算符是显式的时,我们也能执行类型转换,不过必须通过显式的强制类型转换才可以。

该规定存在一个例外,即如果表达式被用作条件,则编译器会将显式的类型转换自动立用于它。

换句话说,当表达式出现在下列位置时,显式的类型转换将被隐式地执行:

  • if、while及do语句的条件部分
  • for语句头的条件表达式
  • 逻辑非运算符(!)、逻辑或运算符(||)、逻辑与运算符(&&)的运算对象
  • 条件运算符(?:)的条件表达式。

转换为bool

在标准库的早期版本中,IO类型定义了向void*的转换规则,以求避免上面提到的问题。

在C++11新标准下,IO标准库通过定义一个向bool的显式类型转换实现同样的目的。

无论我们什么时候在条件中使用流对象,都会使用为IO 类型定义的operator bool。

例如:

while (std::cin >> value)

while语句的条件执行输入运算符,它负责将数据读入到value并返回cin。

为了对条件求值,cin被istream operator bool类型转换函数隐式地执行了转换。

如果cin的条件状态是good,则该函数返回为真;否则该函数返回为假。

避免有二义性的类型转换

如果类中包含一个或多个类型转换,则必须确保在类类型和目标类型之间只存在唯一种转换方式。否则的话,我们编写的代码将很可能会具有二义性。

在两种情况下可能产生多重转换路径。

  1. 第一种情况是两个类提供相同的类型转换:例如,当A类定义了一个接受B类对象的转换构造函数,同时B类定义了一个转换目标是A类的类型转换运算符时,我们就说它们提供了相同的类型转换。
  2. 第二种情况是类定义了多个转换规则,而这些转换涉及的类型本身可以通过其他类型转换联系在一起。最典型的例子是算术运算符,对某个给定的类来说,最好只定义最多一个与算术类型有关的转换规则。

通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个及两个以上转换源或转换目标是算术类型的转换。

实参匹配和相同的类型转换

在下面的例子中,我们定义了两种将B转换成A的方法:

  • 一种使用B的类型转换运算符
  • 另一种使用A的以B为参数的构造函数:
// 最好不要在两个类之间构建相同的类型转换
struct B;

struct A {
A() = default;
A(const B&); // 把一个B转换成 A
//其他数据成员
};

struct B {
operator A()const; // 也是把一个 B转换成 A
//其他数据成员
};

A f(const A&){//...}

B b;
A a=f(b);  //二义性错误:舍义是f(B::operator A())
//还是f(A::A(const B&))?

因为同时存在两种由B获得A的方法,所以造成编译器无法判断应该运行哪个类型转换,也就是说,对f的调用存在二义性。

该调用可以使用以B为参数的A的构造函数,也可以使用B当中把B转换成A的类型转换运算符。因为这两个函数效果相当,所以该调用将产生错误。

如果我们确实想执行上述的调用,就不得不显式地调用类型转换运算符或者转换构造函数:

A al =f(b.operator A()); // 正确:使用 B的类型转换运算符
A a2 = f(A(b));// 正确:使用A的构造函数

值得注意的是,我们无法使用强制类型转换来解决二义性问题,因为强制类型转换本身也面临二义性。

二义性与转换目标为内置类型的多重类型转换

另外如果类定义了一组类型转换,它们的转换源(或者转换目标)类型本身可以通过其他类型转换联系在一起,则同样会产生二义性的问题。

最简单也是最困扰我们的例子就是类当中定义了多个参数都是算术类型的构造函数,或者转换目标都是算术类型的类型转换运算符。

例如,在下面的类中包含两个转换构造函数,它们的参数是两种不同的算术类型;同时还包含两个类型转换运算符,它们的转换目标也恰好是两种不同的算术类型

struct A 
{
A(int =0); // 最好不要创建两个转换源都是算术类型的类型转换
A(double);
operator int() const;// 最好不要创建两个转换对象都是算术类型的类型转换operator double()const;
void f2(long double);
// 其他成员
};

A a;
f2(a); // 二义性错误:含义是f(A::operator int())
//还是f(A::operator double())?

long 1g;
A a2(1g); //二义性错误:含义是A::A(int)还是A::A(double)?

在对f2的调用中,哪个类型转换都无法精确匹配long double。

然而这两个类型转换都可以使用,只要后面再执行一次生成long double的标准类型转换即可。因此,在上面的两个类型转换中哪个都不比另一个更好,调用将产生二义性。

当我们试图用long初始化a2时也遇到了同样问题,哪个构造函数都无法精确匹配long类型。它们在使用构造函数前都要求先将实参进行类型转换:

  • 先执行 long  double的标准类型转换,再执行A(double)
  • 先执行 long到int的标准类型转换,再执行A(int)

编译器没办法区分这两种转换序列的好坏,因此该调用将产生二义性。

调用 f2 及初始化a2的过程之所以会产生二义性,根本原因是它们所需的标准类型转换级别一致。

当我们使用用户定义的类型转换时,如果转换过程包含标准类型转换,则标准类型转换的级别将决定编译器选择最佳匹配的过程;

short s = 42;
//把short提升成int优于把short转换成double
A a3(s); //使用A::A(int)

在此例中,把short提升成int的操作要优于把short转换成double的操作,因此编译器将使用A::A(int)构造函数构造a3,其中实参是s(提升后)的值。

当我们使用两个用户定义的类型转换时,如果转换函数之前或之后存在标准类型转换,则标准类型转换将决定最佳匹配到底是哪个。

提示:类型转换与运算符

要想正确地设计类的重载运算符、转换构造函数及类型转换函数,必须加倍小心。

尤其是当类同时定义了类型转换运算符及重载运算符时特别容易产生二义性。以下的经验规则可能对你有所帮助:
1.不要令两个类执行相同的类型转换:如果Foo类有一个接受Bar类对象的构造函数,则不要在Bar类中再定义转换目标是Foo类的类型转换运算符。

2.避免转换目标是内置算术类型的类型转换特别是当你已经定义了一个转换成算
术类型的类型转换时,接下来
     ——不要再定义接受算术类型的重载运算符。如果用户需要使用这样的运算符,则类型转换操作将转换你的类型的对象,然后使用内置的运算符。
     ——不要定义转换到多种算术类型的类型转换。让标准类型转换完成向其他算术类型转换的工作。

言以蔽之:除了显式地向bool类型的转换之外,我们应该尽量避免定义类型转换函做并尽可能地限制那些“显然正确”的非显式构造函数。

重载函数与转换构造函数

当我们调用重载的函数时,从多个类型转换中进行选择将变得更加复杂。

如果两个或多个类型转换都提供了同一种可行匹配,则这些类型转换一样好。

举个例子,当几个重载函数的参数分属不同的类类型时,如果这些类恰好定义了同样的转换构造函数,则二义性问题将进一步提升:

struct C{
C(int);
//其他成员
};

struct D
{
D(int);
//其他成员
};

void manip(const C&);
void manip(const D&);

manip(10);//二义性错误:含义是manip (C(10))还是manip(D(10))

其中C和D都包含接受int的构造函数,两个构造函数各自匹配manip的一个版本。此调用将具有三义性;它的含义可能是把int转换成,然后调用manip的第一个版木也可能是把int转换成D,然后调用manip的第二个版本。

调用者可以显式地构造正确的类型从而消除二义性:

manip(C(10)); // 正确:调用manip(const C&)

如果在调用重载函数时我们需要使用构造函数或者强制类型转换来改变实参的类型,则这通常意味着程序的设计存在不足。

重载函数与用户定义的类型转换

当调用重载函数时,如果两个(或多个)用户定义的类型转换都提供了可行匹配,则 我们认为这些类型转换一样好。

在这个过程中,我们不会考虑任何可能出现的标准类型转换的级别。只有当重载函数能通过同一个类型转换函数得到匹配时,我们才会考虑其中出现的标准类型转换。

例如当我们调用manip时,即使其中一个类定义了需要对实参进行标准类型转换的构造函数,这次调用仍然会具有二义性:

struct E{
E(double);
// 其他成员
};

struct C{
C(int);
//其他成员
};

void manip2(const C&);
void manip2(const E&);

//二义性错误:两个不同的用户定义的类型转换都能用在此处
manip2(10); // 含义是manip2 (C(10))还是manip2(E(double(10)))

在此例中,C有一个转换源为int的类型转换,E有一个转换源为double的类型转换。

对于manip2(10)来说,两个manip2函数都是可行的:

  1. manip2(constC&)是可行的,因为C有一个接受int的转换构造函数,该构造函数与实参精确匹配。
  2. manip2(const E&)是可行的,因为E有一个接受double的转换构造函数,而且为了使用该函数我们可以利用标准类型转换把int转换成所需的类型。

因为调用重载函数所请求的用户定义的类型转换不止一个且彼此不同,所以该调用具有二义性。即使其中一个调用需要额外的标准类型转换而另一个调用能精确匹配,编译器也会将该调用标示为错误。

在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用。如果所需的用户定义的类型转换不止一个,则该调用具有二义性。

函数匹配与重载运算符

重载的运算符也是重载的函数。

因此,通用的函数匹配规则同样适用于判断在给定的表达式中到底应该使用内置运算符还是重载的运算符。

不过当运算符函数出现在表达式中时,候选函数集的规模要比我们使用调用运算符调用函数时更大。

如果a是一种类类型,则表达式a sym b可能是

a.operatorsym(b);//a有一个operatorsym成员函数
operatorsym(a, b);//operatorsym是一个普通函数

和普通函数调用不同,我们不能通过调用的形式来区分当前调用的是成员函数还是非成员函数。

当我们使用重载运算符作用于类类型的运算对象时,候选函数中包含该运算符的普通非成员版本和内置版本。除此之外,如果左侧运算对象是类类型,则定义在该类中的运算符的重载版本也包含在候选函数内。

当我们调用一个命名的函数时,具有该名字的成员函数和非成员函数不会彼此重载,这是因为我们用来调用命名函数的语法形式对于成员函数和非成员函数来说是不相同的。

当我们通过类类型的对象(或者该对象的指针及引用)进行函数调用时,只考虑该类的成员函数。

而当我们在表达式中使用重载的运算符时,无法判断正在使用的是成员函数还是非成员函数,因此二者都应该在考虑的范围内。

表达式中运算符的候选函数集既应该包括成员函数,也应该包括非成员函数。

举个例子,我们为SmallInt类定义一个加法运算符:

class SmallInt {
friend SmallInt operator+(const SmallInt&, const SmallInt&);
public:
SmallInt(int=0); // 转换源为 int的类型转换
operator int()const ( return val;)// 转换目标为int的类型转换private:
std::size_t val;
};

可以使用这个类将两个SmallInt对象相加,但如果我们试图执行混合模式的算术运算,就将遇到二义性的问题:

SmallInt sl,s2;
SmallInt s3 =s1+s2; // 使用重载的 operator+
int i=s3 +0; // 二义性错误

第一条加法语句接受两个SmallInt值并执行+运算符的重载版本。第二条加法语句具有二义性:因为我们可以把0转换成SmallInt,然后使用SmallInt的+;或者把s3转换成int,然后对于两个int执行内置的加法运算。

如果我们对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值