🔷 第十一章 继承与派生
📄 一句话概括

转换运算符让"你的类型"能自动变成内置类型,写法是 operator 类型名() { return ...; }。它有两个怪特征:没有返回类型(double 那个类型名本身就是返回类型)、没有参数。它是把"隐式转换"这条通道从"内置类型 → 我的类型"(靠构造函数)打通到相反方向("我的类型 → 内置类型")。便宜,但要小心——一旦有了它,所有涉及这个类型的表达式都可能悄悄改道。

📝 本节和前面几节的关系

11.2.1 讲"运算符让自定义类型像内置类型",本节算这个思路的极限:不只是"能用运算符",而是"能被当成内置类型本身用"。它和转换构造函数是一对镜像——那个管"别的类型 → 我",这个管"我 → 别的类型"。10.2 的构造函数在这里再次出场(本节 Demo 里 Fraction(int n) 就是转换构造函数)。

一、语法与两个怪特征

class 类名 {
public:
    operator 目标类型() {
        return 一个目标类型的值;
    }
};
class Fraction {
    int num, den;
public:
    operator double() const {          // ★ 就是这一行
        return (double)num / den;
    }
};

两个怪特征,必须记住:

特征 说明
没有返回类型 operator double 里的 double 就是返回类型——再写一次 double operator double() 反而是错的
没有参数 因为是"把自己变成别人",不需要额外信息
不能指定返回值类型(同第一条) 也不能写成 operator void 之类的无意义目标
可以是 const 成员函数 强烈建议写 const——转换只读自己,不改自己(10.4.1)
必须是成员函数 不能写成全局函数(11.2.2 的"必须成员"表里就有它)

二、两个方向,一对镜像

"类型转换"在 C++ 里有两条通道:

别的类型 ──────────→ 我的类型     :靠"转换构造函数"(单参构造函数)
我的类型 ──────────→ 别的类型     :靠"转换运算符"  ← 本节

看一个分数类 Fraction 的双向转换:

class Fraction {
    int num, den;
public:
    // ① 转换构造函数:int → Fraction("3" 可以当 "3/1" 用)
    Fraction(int n) : num(n), den(1) { }

    // ② 转换运算符:Fraction → double
    operator double() const { return (double)num / den; }
};

有了这两条,"分数的四则运算"可以偷懒:

Fraction f(3, 4);
double   d = f + 0.5;      // f 先转成 double(0.75),再算 → 1.25

注意"偷懒"的代价:这次的加法是"浮点加法",结果不再是精确分数。这往往是"能用但结果不对"的开始(见第五节误区)。

三、什么时候该用

场景 该不该用转换运算符
本质就是"同一个量、换个单位"(角度 → 弧度、分数 → 小数、string → const char*) ✅ 合适
需要和标准库/算法配合(比如放进需要 double 的地方) ✅ 合适(但要权衡精度)
两个类型语义完全不同(Person → int) ❌ 不要。会让人完全读不懂
"我懒得写 getX()" ❌ 不该。写个明确的函数名更好
只是为了让 f + 1 编过 ⚠️ 慎重。最后往往是"能编过但算错"

工程上的倾向:现代 C++ 更推荐"显式函数"(f.toDouble()),只在语义天然贴合时才用隐式转换。

四、动手 Demo:分数类 → 小数

#include <iostream>
using namespace std;

class Fraction {
private:
    int num;                    // 分子
    int den;                    // 分母

public:
    Fraction(int n, int d) : num(n), den(d ? d : 1) { }   // 防呆:分母不为 0

    // ★ 转换运算符:Fraction → double
    operator double() const {
        return (double)num / den;
    }

    // 换个方向:int → Fraction(转换构造函数,注释着,见 30 秒实验)
    // Fraction(int n) : num(n), den(1) { }
};

int main() {
    Fraction f(3, 4);

    double d = f;                       // ← 隐式调用 operator double()
    cout << "d = " << d << endl;

    cout << "f * 2.0 = " << f * 2.0 << endl;    // f 先转成 double,再做乘法

    cout << "C 风格转换:" << (double)f << endl;
    return 0;
}

看到什么算成功:

d = 0.75
f * 2.0 = 1.5
C 风格转换:0.75

读结果:

行 发生了什么
double d = f; f 被隐式转换成 0.75——你没写任何"转换"字样
f * 2.0 编译器先找"Fraction 乘 double"的重载(没有),于是把 f 转成 double 再乘
(double)f 显式指名,同样调这个函数

30 秒实验(这一步最能体会"便宜与风险"): 1. 在 main 里加一句:if (f == 0.75) cout << "相等" << endl; 2. 它能编过,而且输出"相等"——因为在比较之前,f 悄悄变成了 double。 3. 这就是转换运算符的"隐形权力":它不只在你想转换的地方生效,它在"任何需要 double 的地方"都生效。 4. 再把注释里那行 Fraction(int n) : num(n), den(1) { } 打开,然后加一句 Fraction g = f + 1;——它会去调浮点加法,1 变成 Fraction(1/1) 还是 1.0,取决于哪条转换通道先被选中。两条通道同时存在时,"该走哪条"会变成编译器和你的猜谜游戏。

五、常见误区

  1. "转换运算符要写返回类型"——不用,且不能写。operator double 里的 double 就是返回类型。写成 double operator double() 会报错。
  2. "有了 operator double(),f + 1 就是"分数加整数"——往往不是。它很可能变成浮点加法(f 换成 double),中间丢了精度。如果你要"分数加整数",应该专门重载 operator+(int)。
  3. "转换运算符可以随便多写几个"——写多了会二义。比如同时有 operator int() 和 operator double(),那么 f + 1 到底转成 int 还是 double?编译器可能报 ambiguous。
  4. "转换运算符越方便越好"——方便的反面是"你不知道哪儿被转了"。读代码时最怕的不是复杂,而是"看不见的跳转"。能写显式函数(f.toDouble())就别指望隐式转换。
  5. "显式转换 (double)f 和隐式转换 double d = f; 走的是两套代码"——是同一个函数。区别只在"谁决定要转":显式是你决定了,隐式是编译器替你决定了。
  6. "现代 C++ 推荐用 explicit 关掉隐式转换"——对,而且这是主流做法:写成 explicit operator double() const,就只允许显式转换((double)f 可用,double d = f; 报错)。语法不复杂,但先知道"有这么个开关"就够了。
自测三问(点开对答案) 1. 转换运算符的签名有什么特别?(提示:返回类型、参数)——**没有**返回类型(**目标类型名自己就是返回类型**)、**没有**参数、**必须是成员函数**;建议加 `const`。 2. "转换构造函数"和"转换运算符"分别是哪个方向?——**转换构造函数**:`别的类型 → 我的类型`(如 `Fraction(int)` 让 `int` 变 `Fraction`);**转换运算符**:`我的类型 → 别的类型`(如 `operator double()` 让 `Fraction` 变 `double`)。 3. 转换运算符最大的风险是什么?——**它会在"任何需要目标类型的地方"悄悄生效**(`f == 0.75`、`f * 2.0`……),于是**你以为在做分数运算,实际在做浮点运算**(丢精度);**写多了还会造成二义性**。所以它更适合"语义天然贴合"的场合,或者干脆用 `explicit` 限制成显式转换。

相关笔记