- 内容提要
- 前言
- 第 1 章 预备知识
- 第 2 章 开始学习 C++
- 第 3 章 处理数据
- 第 4 章 复合类型
- 第 5 章 循环和关系表达式
- 第 6 章 分支语句和逻辑运算符
- 第 7 章 函数——C++的编程模块
- 第 8 章 函数探幽
- 第 9 章 内存模型和名称空间
- 第 10 章 对象和类
- 第 11 章 使用类
- 第 12 章 类和动态内存分配
- 第 13 章 类继承
- 第 14 章 C++中的代码重用
- 第 15 章 友元、异常和其他
- 第 16 章 string 类和标准模板库
- 第 17 章 输入、输出和文件
- 第 18 章 探讨 C++新标准
- 附录 A 计数系统
- 附录 B C++保留字
- 附录 C ASCII 字符集
- 附录 D 运算符优先级
- 附录 E 其他运算符
- 附录 F 模板类 string
- 附录 G 标准模板库方法和函数
- 附录 H 精选读物和网上资源
- 附录 I 转换为 ISO 标准 C++
- 附录 J 复习题答案
11.6 类的自动转换和强制类型转换
下面介绍类的另一个主题——类型转换。本节讨论 C++如何处理用户定义类型的转换。在讨论这个问题之前,我们先来复习一下 C++是如何处理内置类型转换的。将一个标准类型变量的值赋给另一种标准类型的变量时,如果这两种类型兼容,则 C++自动将这个值转换为接收变量的类型。例如,下面的语句都将导致数值类型转换:
上述赋值语句都是可行的,因为在 C++看来,各种数值类型都表示相同的东西——一个数字,同时 C++包含用于进行转换的内置规则。然而,第 3 章介绍过,这些转换将降低精度。例如,将 3.33 赋给 int 变量时,转换后的值为 3,丢失了 0.33。
C++语言不自动转换不兼容的类型。例如,下面的语句是非法的,因为左边是指针类型,而右边是数字:
虽然计算机内部可能使用整数来表示地址,但从概念上说,整数和指针完全不同。例如,不能计算指针的平方。然而,在无法自动转换时,可以使用强制类型转换:
上述语句将 10 强制转换为 int 指针类型(即 int *类型),将指针设置为地址 10。这种赋值是否有意义是另一回事。
可以将类定义成与基本类型或另一个类相关,使得从一种类型转换为另一种类型是有意义的。在这种情况下,程序员可以指示 C++如何自动进行转换,或通过强制类型转换来完成。为了说明这是如何进行的,我们将第 3 章中的磅转换为英石的程序改写成类的形式。首先,设计一种合适的类型。我们基本上是以两种方式(磅和英石)来表示重量的。对于在一个实体中包含一个概念的两种表示来说,类提供了一种非常好的方式。因此可以将重量的两种表示放在同一个类中,然后提供以这两种方式表达重量的类方法。程序清单 11.16 提供了这个类的头文件。
程序清单 11.16 stonewt.h
正如第 10 章指出的,对于定义类特定的常量来说,如果它们是整数,enum 提供了一种方便的途径。也可以采用下面这种方法:
Stonewt 类有 3 个构造函数,让您能够将 Stonewt 对象初始化为一个浮点数(单位为磅)或两个浮点数(分别代表英石和磅)。也可以创建 Stonewt 对象,而不进行初始化:
这个类并非真的需要声明构造函数,因为自动生成的默认构造函数就很好。另一方面,提供显式的声明可为以后做好准备,以防必须定义构造函数
另外,Stonewt 类还提供了两个显示函数。一个以磅为单位来显示重量,另一个以英石和磅为单位来显示重量。程序清单 11.17 列出了类方法的实现。每个构造函数都给这三个私有成员全部赋了值。因此创建 Stonewt 对象时,将自动设置这两种重量表示。
程序清单 11.17 stonewt.cpp
因为 Stonewt 对象表示一个重量,所以可以提供一些将整数或浮点值转换为 Stonewt 对象的方法。我们已经这样做了!在 C++中,接受一个参数的构造函数为将类型与该参数相同的值转换为类提供了蓝图。因此,下面的构造函数用于将 double 类型的值转换为 Stonewt 类型:
也就是说,可以编写这样的代码:
程序将使用构造函数 Stonewt(double) 来创建一个临时的 Stonewt 对象,并将 19.6 作为初始化值。随后,采用逐成员赋值方式将该临时对象的内容复制到 myCat 中。这一过程称为隐式转换,因为它是自动进行的,而不需要显式强制类型转换。
只有接受一个参数的构造函数才能作为转换函数。下面的构造函数有两个参数,因此不能用来转换类型:
然而,如果给第二个参数提供默认值,它便可用于转换 int:
将构造函数用作自动类型转换函数似乎是一项不错的特性。然而,当程序员拥有更丰富的 C++经验时,将发现这种自动特性并非总是合乎需要的,因为这会导致意外的类型转换。因此,C++新增了关键字 explicit,用于关闭这种自动特性。也就是说,可以这样声明构造函数:
这将关闭上述示例中介绍的隐式转换,但仍然允许显式转换,即显式强制类型转换:
注意:
只接受一个参数的构造函数定义了从参数类型到类类型的转换。如果使用关键字 explicit 限定了这种构造函数,则它只能用于显示转换,否则也可以用于隐式转换。
编译器在什么时候将使用 Stonewt(double) 函数呢?如果在声明中使用了关键字 explicit,则 Stonewt(double) 将只用于显式强制类型转换,否则还可以用于下面的隐式转换。
- 将 Stonewt 对象初始化为 double 值时。
- 将 double 值赋给 Stonewt 对象时。
- 将 double 值传递给接受 Stonewt 参数的函数时。
- 返回值被声明为 Stonewt 的函数试图返回 double 值时。
- 在上述任意一种情况下,使用可转换为 double 类型的内置类型时。
下面详细介绍最后一点。函数原型化提供的参数匹配过程,允许使用 Stonewt(double)构造函数来转换其他数值类型。也就是说,下面两条语句都首先将 int 转换为 double,然后使用 Stonewt(double)构造函数。
然而,当且仅当转换不存在二义性时,才会进行这种二步转换。也就是说,如果这个类还定义了构造函数 Stonewt(long),则编译器将拒绝这些语句,可能指出:int 可被转换为 long 或 double,因此调用存在二义性。
程序清单 11.18 使用类的构造函数初始化了一些 Stonewt 对象,并处理类型转换。请务必将程序清单 11.18 和程序清单 11.17 一起编译。
程序清单 11.18 stone.cpp
下面是程序清单 11.18 所示程序的输出:
程序说明
当构造函数只接受一个参数时,可以使用下面的格式来初始化类对象:
这等价于前面介绍过过的另外两种格式:
然而,后两种格式可用于接受多个参数的构造函数。
接下来,请注意程序清单 11.18 的下面两条赋值语句:
第一条赋值语句使用接受 double 参数的构造函数,将 276.8 转换为一个 Stonewt 值,这将把 incognito 的 pound 成员设置为 276.8。因为该语句使用了构造函数,所以还将设置 stone 和 pds_left 成员。同样,第二条赋值语句将一个 int 值转换为 double 类型,然后使用 Stonewt(double) 来设置全部 3 个成员。
最后,请注意下面的函数调用:
display( ) 的原型表明,第一个参数应是 Stonewt 对象(Stonewt 和 Stonewt &形参都与 Stonewt 实参匹配)。遇到 int 参数时,编译器查找构造函数 Stonewt(int),以便将该 int 转换为 Stonewt 类型。由于没有找到这样的构造函数,因此编译器寻找接受其他内置类型(int 可以转换为这种类型)的构造函数。Stone(double) 构造函数满足这种要求,因此编译器将 int 转换为 double,然后使用 Stonewt(double) 将其转换为一个 Stonewt 对象。
11.6.1 转换函数
程序清单 11.18 将数字转换为 Stonewt 对象。可以做相反的转换吗?也就是说,是否可以将 Stonewt 对象转换为 double 值,就像如下所示的那样?
可以这样做,但不是使用构造函数。构造函数只用于从某种类型到类类型的转换。要进行相反的转换,必须使用特殊的 C++运算符函数——转换函数。
转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。例如,如果定义了从 Stonewt 到 double 的转换函数,就可以使用下面的转换:
也可以让编译器来决定如何做:
编译器发现,右侧是 Stonewt 类型,而左侧是 double 类型,因此它将查看程序员是否定义了与此匹配的转换函数。(如果没有找到这样的定义,编译器将生成错误消息,指出无法将 Stonewt 赋给 double。)
那么,如何创建转换函数呢?要转换为 typeName 类型,需要使用这种形式的转换函数:
operator typeName();
请注意以下几点:
- 转换函数必须是类方法;
- 转换函数不能指定返回类型;
- 转换函数不能有参数。
例如,转换为 double 类型的函数的原型如下:
typeName(这里为 double)指出了要转换成的类型,因此不需要指定返回类型。转换函数是类方法意味着:它需要通过类对象来调用,从而告知函数要转换的值。因此,函数不需要参数。
要添加将 stone_wt 对象转换为 int 类型和 double 类型的函数,需要将下面的原型添加到类声明中:
程序清单 11.19 列出了修改后的类声明。
程序清单 11.19 stonewt1.h
程序清单 11.20 是在程序清单 11.18 的基础上修改而成的,包括了这两个转换函数的定义。注意,虽然没有声明返回类型,这两个函数也将返回所需的值。另外,int 转换将待转换的值四舍五入为最接近的整数,而不是去掉小数部分。例如,如果 pounds 为 114.4,则 pounds +0.5 等于 114.9,int(114.9)等于 114。但是如果 pounds 为 114.6,则 pounds + 0.5 是 115.1,而 int(115.1)为 115。
程序清单 11.20 stonewt1.cpp
程序清单 11.21 对新的转换函数进行测试。该程序中的赋值语句使用隐式转换,而最后的 cout 语句使用显式强制类型转换。请务必将程序清单 11.20 与程序清单 11.21 一起编译。
程序清单 11.21 stone1.cpp
下面是程序清单 11.19~程序清单 11.21 组成的程序的输出;它显示了将 Stonewt 对象转换为 double 类型和 int 类型的结果:
自动应用类型转换
程序清单 11.21 将 int(poppins) 和 cout 一起使用。假设省略了显式强制类型转换:
程序会像在下面的语句中那样使用隐式转换吗?
答案是否定的。在 p_wt 示例中,上下文表明,poppins 应被转换为 double 类型。但在 cout 示例中,并没有指出应转换为 int 类型还是 double 类型。在缺少信息时,编译器将指出,程序中使用了二义性转换。该语句没有指出要使用什么类型。
有趣的是,如果类只定义了 double 转换函数,则编译器将接受该语句。这是因为只有一种转换可能,因此不存在二义性。
赋值的情况与此类似。对于当前的类声明来说,编译器将认为下面的语句有二义性而拒绝它。
在 C++中,int 和 double 值都可以被赋给 long 变量,所以编译器使用任意一个转换函数都是合法的。编译器不想承担选择转换函数的责任。然而,如果删除了这两个转换函数之一,编译器将接受这条语句。例如,假设省略了 double 定义,则编译器将使用 int 转换,将 poppins 转换为一个 int 类型的值。然后在将它赋给 gone 时,将 int 类型值转换为 long 类型。
当类定义了两种或更多的转换时,仍可以用显式强制类型转换来指出要使用哪个转换函数。可以使用下面任何一种强制类型转换表示法:
第一条语句将 poppins 转换为一个 double 值,然后赋值操作将该 double 值转换为 long 类型。同样,第二条语句将 poppins 首先转换为 int 类型,随后转换为 long。
和转换构造函数一样,转换函数也有其优缺点。提供执行自动、隐式转换的函数所存在的问题是:在用户不希望进行转换时,转换函数也可能进行转换。例如,假设您在睡眠不足时编写了下面的代码:
通常,您以为编译器能够捕获诸如使用了对象而不是整数作为数组索引等错误,但 Stonewt 类定义了一个 operator int( ),因此 Stonewt 对象 temp 将被转换为 int 200,并用作数组索引。原则上说,最好使用显式转换,而避免隐式转换。在 C++98 中,关键字 explicit 不能用于转换函数,但 C++11 消除了这种限制。因此,在 C++11 中,可将转换运算符声明为显式的:
有了这些声明后,需要强制转换时将调用这些运算符。
另一种方法是,用一个功能相同的非转换函数替换该转换函数即可,但仅在被显式地调用时,该函数才会执行。也就是说,可以将:
替换为:
这样,下面的语句将是非法的:
但如果确实需要这种转换,可以这样做:
警告:
应谨慎地使用隐式转换函数。通常,最好选择仅在被显式地调用时才会执行的函数。
总之,C++为类提供了下面的类型转换。
- 只有一个参数的类构造函数用于将类型与该参数相同的值转换为类类型。例如,将 int 值赋给 Stonewt 对象时,接受 int 参数的 Stonewt 类构造函数将自动被调用。然而,在构造函数声明中使用 explicit 可防止隐式转换,而只允许显式转换。
- 被称为转换函数的特殊类成员运算符函数,用于将类对象转换为其他类型。转换函数是类成员,没有返回类型、没有参数、名为 operator typeName( ),其中,typeName 是对象将被转换成的类型。将类对象赋给 typeName 变量或将其强制转换为 typeName 类型时,该转换函数将自动被调用。
11.6.2 转换函数和友元函数
下面为 Stonewt 类重载加法运算符。在讨论 Time 类时指出过,可以使用成员函数或友元函数来重载加法。(出于简化的目的,我们假设没有定义 operator double( ) 转换函数。)可以使用下面的成员函数实现加法:
也可以将加法作为友元函数来实现,如下所示:
别忘了,可以提供方法定义或友元函数定义,但不能都提供。上面任何一种格式都允许这样做:
另外,如果提供了 Stonewt(double)构造函数,则也可以这样做:
但只有友元函数才允许这样做:
为了解其中的原因,将每一种加法都转换为相应的函数调用。首先:
被转换为:
或:
上述两种转换中,实参的类型都和形参匹配。另外,成员函数是通过 Stonewt 对象调用的。
其次:
被转换为:
或:
同样,成员函数也是通过 Stonewt 对象调用的。这一次,每个调用中都有一个参数(kennyD)是 double 类型的,因此将调用 Stonewt(double)构造函数,将该参数转换为 Stonewt 对象。
另外,在这种情况下,如果定义了 operator double( ) 成员函数,将造成混乱,因为该函数将提供另一种解释方式。编译器不是将 kennyD 转换为 double 并执行 Stonewt 加法,而是将 jennySt 转换为 double 并执行 double 加法。过多的转换函数将导致二义性。
最后:
被转换为:
其中,两个参数都是 double 类型,因此将调用构造函数 Stonewt(double),将它们转换为 Stonewt 对象。
然而,不能调用成员函数将 jennySt 和 peenyD 相加。将加法语法转换为函数调用将类似于下面这样:
这没有意义,因为只有类对象才可以调用成员函数。C++不会试图将 pennyD 转换为 Stonewt 对象。将对成员函数参数进行转换,而不是调用成员函数的对象。
这里的经验是,将加法定义为友元可以让程序更容易适应自动类型转换。原因在于,两个操作数都成为函数参数,因此与函数原型匹配。
实现加法时的选择
要将 double 量和 Stonewt 量相加,有两种选择。第一种方法是(刚介绍过)将下面的函数定义为友元函数,让 Stonewt(double) 构造函数将 double 类型的参数转换为 Stonewt 类型的参数:
第二种方法是,将加法运算符重载为一个显式使用 double 类型参数的函数:
这样,下面的语句将与成员函数 operator + (double x) 完全匹配:
而下面的语句将与友元函数 operator + (double x, Stonewt &s) 完全匹配:
前面对 Vector 乘法做了类似的处理。
每一种方法都有其优点。第一种方法(依赖于隐式转换)使程序更简短,因为定义的函数较少。这也意味程序员需要完成的工作较少,出错的机会较小。这种方法的缺点是,每次需要转换时,都将调用转换构造函数,这增加时间和内存开销。第二种方法(增加一个显式地匹配类型的函数)则正好相反。它使程序较长,程序员需要完成的工作更多,但运行速度较快。
如果程序经常需要将 double 值与 Stonewt 对象相加,则重载加法更合适;如果程序只是偶尔使用这种加法,则依赖于自动转换更简单,但为了更保险,可以使用显式转换。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论