第五章 类

文章介绍了C++中用户定义的类型转换,包括转换构造函数和类型转换运算符。转换构造函数通过单参数构造函数实现隐式转换,而类型转换运算符(如`operatorint()`)允许将类对象转换为其他类型。显式类型转换有助于避免意外的隐式转换,特别是向bool的转换。文章强调了避免二义性转换和如何通过`explicit`关键字抑制隐式转换的重要性。
摘要由CSDN通过智能技术生成

5.8 类的类型转换

简介

转换构造函数和类型转换运算符共同定义了类的类型转换class-type conversions,这样的转换有时也被称为用户定义的类型转换user-defined conversion

类型转换运算符

1. 简介

类型转换运算符conversion operator是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下所示:

operator type() const;

其中type表示某种类型,类型转换运算符可以面向任意类型(除了void之外)进行定义,只要该类型能作为函数的返回类型。因此,我们不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针以及函数指针)或者引用类型。类型转换运算符既没有显式的返回类型,也没有形参,而且必须定义成类的成员函数。类型转换运算符通常不应该改变待转换对象的内容,所以一般被定义为const成员。

2. 例子

我们定义一个表示0~255之间一个整数的一个类:

// 类型转换运算符支持将SmallInt对象转化成int
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_;
};

// 首先将4隐式地转换成SmallInt,然后调用SmallInt::operator=
si = 4;
// 首先将si隐式地转换成int,然后执行整数的加法
si + 3; 
3. 显式的类型转换运算符

Tips:实践中类很少提供类型转换运算符,所以类型转换运算符常被explicit修饰以阻止隐式转换。

在实践中类很少提供类型转换运算符,在大多数情况下,如果类型转换自动发生,用户可能会感觉比较意外,而不是感觉受到了帮助。然而这条经验法则存在一种例外情况:对于类来说,定义向bool的类型转换还是比较普遍的现象。但是这种类型转换可能引发意想不到的结果,特别是当istream含有向bool的类型转换时,下面的代码仍然编译通过:

// 如果向bool的类型转换不是显式的,则该代码在编译器看来将是合法的
// 因为istream本身并没有定义<<运算符,所以本来这段代码应该产生错误
int i = 42;
cin << i;

// 执行过程如下:
// 1) istream的bool类型转换符将cin转换为bool
// 2) bool被提升为int并作为左移运算符的左侧运算对象
// 3) 提升后的bool值(1或0)会被左移42个位置

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

class SmallInt {
 public:
    // 转换构造函数: 编译支持int到SmallInt的隐式转换
    SmallInt(int i = 0) : val_(i) {
        if (i < 0 || i > 255)
            throw std::out_of_range("Bad SmallInt value");
    }
    // 类类型转换运算符: 编译器不支持SmallInt到int的隐式转换
    explicit operator int() const { return val_; }
    // ...其他成员

 private:
    int val_;
};

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

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

两种情况下可能存在多重转换路径:

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

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

struct B;
struct A {
    A() = default;
    A(const B&);
};

struct B {
    operator A() const;
};

A f(const A&);
B b;
// 二义性错误: 含义可能是f(B::operator A())或f(A::A(const B&))
A a = f(b);
// 正确: 显式地使用B的类型转换运算符
A a1 = f(b.operator A());
// 正确: 显式地调用A的转换构造函数
A a2 = f(A(b));

转换构造函数:隐式的类类型转换

1. 简介

Tips:能通过一个实参调用的构造函数定义了从构造函数的参数类型向类类型隐式转换的规则。

C++在不同的内置类型之间定义了几种自动转换规则,同样地我们也可以为类定义隐式转换规则。如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,我们也将这种构造函数称为转换构造函数。

举个简单的例子:如果一个类包含接受一个string类型的构造函数,那么它也就定义了从string类型到该类隐式转换的规则。

class Foo {
 public:
    // 定义从std::string到Foo类型的隐式转换规则
    Foo(std::string s) : str(s) { }
 private:
    std::string str;
}
2. 只允许一步类类型转换

编译器只会自动地执行一步类型转换,例如下面代码隐式地使用了两种转换规则,因此它是错误的:

// 错误: 需要用户定义的两种转换:
// 1) 把"tomocat"转换成string
// 2) 再把这个临时的string转换成Foo类型
Foo = "tomocat";
3. 抑制构造函数定义的隐式转换

我们可以将构造函数声明为explicit来阻止构造函数定义的隐式类类型转换,需要注意如下几点:

  • 关键字explicit只对一个实参的构造函数有效:需要多个实参的构造函数不能用于执行隐式转换,因此无需将这些构造函数指定为explicit
  • 只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应该重复
  • explicit声明的构造函数只能以直接初始化的形式使用
class Foo {
 public:
    // 阻止隐式类类型转换
    explicit Foo(std::string s) : str(s) { }
 private:
    std::string str;
}

std::string str = "tomocat";
// 错误: 不支持string到Foo类型的隐式初始化
Foo = str;
// 正确: 显式初始化
Foo(str);
static_cast<Foo>(str);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值