%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # 类型转换 C++中的类型转换(type conversion)是将一种数据类型的值转换为另一种数据类型的值的过程。 ## 类型转换的触发形式——显式与隐式类型转换 从**触发形式**来看,类型转换根据 **"是否有在代码中显式声明请求转换**",可划分为两种: - **==显式类型转换==** - **由特定的显式类型转换语法**进行声明,**==在代码中明确指定进行==** 的类型转换 - C 风格的强制类型转换语法: `(target-type) expression`; - C++的显式类型转换语法:`target-type(expression)`、`target-type{expression}`; - 通过 `static_cast`、`const_cast`、`dynamic_cast`、`reinterpret_cast` 四种强制类型转换操作符(type cast)声明; - **==隐式类型转换==** - 隐式类型转换**是==未在代码中显式声明==、由==编译器自动进行==的类型转换(根据两种类型间存在的隐式转换路径执行) - 当"**一个具有 `T1` 类型的表达式**" 被用于 "**==不接受该类型==,但接受==另一个 `T2` 类型表达式**==" 的上下文时 **==自动触发==**。<br>(如函数调用时传参、函数返回值、拷贝初始化、赋值、用于 switch 语句、用于 if 语句或循环语句中的条件判断时) > [!info] > "**==直接初始化==**"在涉及类型转换时, **等价于声明"显式类型转换"**————**显式请求了从源类型到目标类型的类型转换**。 <br> ### 显式/隐式类型转换的特点 - "**显式类型转换**" 会考虑 **所有构造函数与转换函数**; - "**隐式类型转换**" 只考虑 **`non-explicit` 的转换构造函数与转换函数**。 从"用户定义转换"(即构造函数与转换函数)的角度来看: - **`explicit` 的构造函数、转换函数** 仅允许用于 **==显式类型转换==**。 - **`non-explicit` 的转换构造函数、转换函数** 可被用于所有场景——"**==显式/隐式类型转换==**"。 > [!important] > > ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-类型转换.assets/IMG-cpp-类型转换-125F0E10981D33D90C60FB46AB0DE724.png|895]] > <br> ### 显式/隐式类型转换的区分 对于一个从 **==源类型==** 到 **==目标类型==** 的**类型转换**,该转换是"显式"还是"隐式"取决于 "**==是否有被显式地声明请求转换==**",但"显式/隐式"仅表示"触发形式",具体转换过程是由编译器根据两类型间的转换路径决定。 如果源类型到目标类型之间存在**直接转换路径**,则: - 在**直接初始化 & 声明显式类型转换** 的上下文中,该转换属于 "**显式类型转换**"; - 在**拷贝初始化**以及**其他隐式触发类型转换**的上下文中,该转换属于 "**隐式类型转换**"。 如果源类型到目标类型之间**不存在直接转换路径**,则: 编译器将考虑是否存在合法的==**隐式转换路径**==(通过一个或多个 **==中间类型==**,其中最后一步中间类型到目标类型的转换只能是由调用目标类型的构造函数,或者为标准转换): - 如果目标类型为**类类型**,而**源类型**可以转换为**目标类型**的**某一个==构造函数的参数类型==(中间类型)**,则: - 在"**==直接初始化/声明显式类型转换==**"的上下文中: - 从 **源类型** 到 **中间类型** 的转换为 "**==隐式类型转换==**",允许一个 **==隐式转换序列==**(标准转换&用户定义的转换) - 得到中间类型后,**目标类型**的**构造函数将被直接调用**,调用构造函数这一步不属于隐式转换过程。 - 在"**==拷贝初始化==**"的上下文中: - 从 "**源类型**" 到 "**目标类型**" 这整个转换过程均为 "**==隐式类型转换==**",构成一个 **==隐式转换序列==**: - "**源类型**" 到 "**中间类型**" 只允许为 **一个==标准转换序列==**。 - "**中间类型**" 到 "**目标类型**" 为 **==用户定义的转换==(隐式调用目标类型的构造函数)** - 如果目标类型为**基本数据类型**,而**源类型**能够转换另一种**基本数据类型**(**中间类型**),则 - 从 "**源类型**" 到 "**目标类型**" 这整个转换过程为 "**==隐式类型转换==**",构成一个 **==隐式转换序列==**。 否则,编辑器将报错:不存在可行的转换路径。 <br> ## 类型转换与初始化的关系 "**初始化**" 与 "**类型转换**" 是两个不同的概念,但存在关联。 初始化对象时,**如果 "==用于初始化的源类型==" 与 "==被初始化的目标类型==" 不同,则涉及类型转换**。 **直接初始化**与**拷贝初始化**本身的语义在类型转换上存在差异: - "**直接初始化**"的语义是"**显式**"的——**==直接请求进行类型转换==**(等同于声明 **==显式类型转换==**), - 因此 **`explicit`** 的用户定义转换(**构造函数与转换函数**)均可以被使用。 - "**拷贝初始化**"的语义是"**隐式**"的——其执行逻辑是: - 1. 编译器尝试将"**==源表达式==类型**"转换为"**==目标类型==**"(这一过程为 **==隐式类型转换==**), - 2. 再调用**目标类型**的"**拷贝构造函数**"或"**移动构造函数**"完成初始化 (这一步骤通常被编译器优化而省略)。 > [!NOTE] > - **直接初始化**等价于声明显式类型转换,但对其"**构造函数参数**" 可能存在**隐式类型转换**—— "**==传入实参类型==**" 到 "**==形参类型==**" 的**隐式转换**。 > - **拷贝初始化**中**只**可能涉及 "**==隐式==类型转换**" <br> ## 类型转换的类别 根据**类型转换的具体转换过程以及==所依赖的转换规则==** ,类型转换可以划分为两大类:「**标准转换**」 与 「 **用户定义的转换」 - **==标准转换==**(Standard Conversions):由 **C++语言标准定义**的、**==基本数据类型==** 之间的转换规则; - **==用户定义的转换==**(User-Defined Conversions):由**用户自定义**的、**==类类型==与其他类型**之间的转换,通过在类中定义的"构造函数"或"转换函数"来实现; - **构造函数**:将 **其他类型的值** 转为 **该类类型的对象** - **转换函数**:将 **该类的实例对象** 转为 **其他类型** 这两种类型的转换都可以被**显式、隐式**地执行,显式类型转换即**需要通过代码明确指出**,而隐式类型转换则**由编译器自动进行**。 其中,当"用户定义转换"(**构造函数**、**转换函数**)被 **`explicit` 关键字** 修饰时则表示 "**禁止用于隐式类型转换**"。 <br> ## 类型转换路径的确定 在进行一个特定的类型转换时,**如果存在多个可行的转换路径/序列**,编译器将根据一定规则来选择 **最匹配的转换路径/序列**: - **标准转换**优先于**用户定义的转换**。 - 对于**用户定义的转换**,则根据 **==重载解析规则==(overload resolution)** 对**构造函数与转换函数**一同进行**排序**,选取最匹配的用户定义转换: - **非模板函数优先于模板函数**; - **少的用户定义转换步骤优先于多的步骤**。 - **`non-const` 的参数类型优先于 `const` 参数类型**,例如 `A(B&)` > `A(const B&)` 如果无法确定最佳匹配,编译器将报告**二义性错误**。 <br><br><br> # 标准转换 标准转换是由 **C++语言标准**定义的、**基本数据类型**之间的转换规则[^1](P159)。 标准转换主要包括以下几种: - **==左值到右值的转换==**(lvalue-to-rvalue conversion)<br>当一个左值表达式需要用作右值时,编译器自动将左值转换为对应的右值,从而允许从内存位置读取值 - **==数值到指针的转换==**(array-to-pointer conversion)<br>**数组名** (数组类型)用在大多数表达式或上下文中,自动转换为**指向该数组第一个元素的指针类型**;<br>(例如:当数组被传递给一个期望指针参数的函数时) - **==函数到指针的转换==**(function-to-pointer conversion)<br>**函数名**(函数类型)用在大多数表达式或上下文中,自动转换为**指向该函数的指针类型**;<br>(例如:当函数名作为参数传递给另一个接受函数指针的参数时) - **==数值转换==**(numeric conversions) - **==不同算术类型之间的转换==** - **整型与浮点型之间**的转换:例如从 `int` 到 `double` 或从 `double` 到 `int` - **==数值提升==**(numeric promotions):**当算术运算符的操作数具有不同的类型时,较小的类型通常会转换为较大的类型** - **==整型提升==**(integral promotions): - **较小整数类型**(`char`、`short`)到**较大整数类型**(`int` 、`long long` 等)的转换; - **有符号整型**(`int`,`short`)到**无符号整型**(`unsigned int`、`unsigned short`)的转换; - **==浮点提升==**(floating-point promotions):`float` 到 `double` 类型的自动转换; - **==函数指针转换==**(function pointer conversion) since C++17 <br>函数指针类型之间转换的规则,特别是在涉及到不同调用约定时; - **==限定修饰转换==**(qualification conversions)<br> `const`、`volatile` 等限定符的添加或移除。最常见的是**将非 `const` 对象的引用或指针转换为 `const` 引用或指针**,以允许函数以只读方式访问对象。 - **==布尔转换==**(boolean conversion) <br>非布尔类型到布尔类型的转换,其中 **"`0` 或 `nullptr`"** 转为 `false`,**"非 0 值"** 转换为 `true`。 - **==指针转换==** - **常量整数值`0`、`nullptr` 到任意指针类型的转换**; - 指向任意非常量的指针**转换为 `void*` 指针**; - 指向任意对象的指针**转换为 `const void*` 指针**; - **向上转型(Upcasting)**:"**派生类指针或引用**" 到 "**基类指针或引用**" 的转换 - **向下转型(Downcasting)**:"**基类指针或引用**" 到 "**派生类指针或引用**" 的转换 ## 指针转换 > [!caution] C++ 中不支持 "`void*`" **==隐式转换==** 为 "**其他指针类型**",但反过来可以。 > > `void*` 转到其他任意指针类型必须 "**==显式==**" 通过 `static_cast<>` 或 C 风格的强制类型转换进行。 ^fmg7gb > [!example] 示例一:**常量整数值`0`、`nullptr`** 到**任意指针类型**的隐式转换 > > ```cpp > void func(int* ptr) {} > > int main() { > // 下列初始化是等价的, p1与p2均被初始化为一个空指针 > int* p1 = 0; // 整型常量0到`int*`指针类型的隐式类型转换, 得到`int*`类型的空指针 > int *p3 = NULL; // `NULL`实际上是值为0的常量宏. > int* p2 = nullptr; // `std:nullptr_t`类型的特殊字面量`nullptr`到`int*`指针类型的隐式类型转换, 得到`int*`类型的空指针. > assert(p1 == nullptr); > assert(p2 == nullptr); > assert(p3 == nullptr); > func(0); > func(NULL); > func(nullptr); > } > ``` > > [!example] 示例二:**任意指针类型到 `void*` 的隐式转换** > > ```cpp > int main() { > void* ptr_void = nullptr; > int* ptr_i = nullptr; > char* ptr_c = nullptr; > > const void* ptr_cvoid = nullptr; > const char* ptr_cc = nullptr; > > // (1) 支持指向"非常量"的任意指针类型到void*的隐式转换 > // 支持任意指针类型到const void*的隐式转换 > ptr_void = ptr_c; > ptr_cvoid = ptr_cc; > ptr_cvoid = ptr_i; > > // (2) 不支持 `void*` 到其他指针类型的隐式转换 > // ptr_i = ptr_void; // error, 不支持`void*`到其他任何指针类型的隐式转换 > ptr_i = (int*) ptr_void; > ptr_i = static_cast<int*>(ptr_void); > } > ``` > ^jj8rpd <br><br> ## 数值转换 > [!example] 数值转换示例 > > ```cpp > // 算术类型转换: 当算术运算符的操作数具有不同的类型时,较小的类型通常会转换为较大的类型。 > int var_i = 42; > double d = var_i + 3.14; // int 转为 double > > // 整型提升: 较小的整型(如 char 和 short)在参与表达式运算时通常会被提升为 int。 > char c = 'a'; > int sum = c + 5; // 'c'被提升为 int > > // 整型提升:有符号整型(`int`,`short`)到无符号整型(`unsigned int`、`unsigned short`)的转换; > int signed_i = -2; // 位模式为 0xFFFFFFFE > unsigned int unsigned_i = 1; // 位模式为 0x00000001 > // 发生隐式类型转换(整型提升), int 被提升为 unsigned int, > // 变成两个无符号数相加, 表达式类型为无符号整型. > // 两个位模式相加结果为 0xFFFFFFFF, 在无符号数中表示 4294967295 > cout << (signed_i + unsigned_i) << endl; > > // 浮点数提升: float 通常会被提升为 double > float f = 2.5f; > double d2 = f + 3.0; // 'f'被提升为 double > ``` > <br><br> ### 整型提升 #### 小整型到大整型的提升 > [!NOTE] C++中,若位运算符的操作数为 "**小整型**",则会被自动提升为 `int` 或 `unsigned int` 类型。 > > 参见[^1](P136),**小整型**包括 bool, char, signed char, unsigned char, short, unsigned short。 > > 例如 `unsigned char bits = 0b10010111;`,而 `~bits` 则将得到 `int` 类型的值 `-152`。 > **即 `bits` 首先执行零扩展,提升为了 `int` 类型,然后再取反**。 > #### 有符号数到无符号数的整型提升 C/C++中,当执行一个运算时,如果两个操作数**一个是有符号的**,**另一个是无符号的**,则将按照**标准转换中的 "==整型提升=="** 规则进行处理,**==有符号整数==被隐式转换(提升)为==无符号整数**==,进而**对两个非负的无符号整数执行运算**,**表达式类型也为==无符号整型==**。 - 尽量**避免在同一表达式中混用有符号数和无符号数**。 - 如果必须混用,明确知道转换发生时的后果,并确保这种转换不会影响程序逻辑。 > [!caution] 如果 "带符号类型" 大于 "无符号类型",则无符号类型会转换成带符号类型,例如 `unsigned int => long long`。 [^1] (P142) > [!example] 整型提升——==**有符号整型**==转换为**==无符号整型==** (示例一) > > ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-类型转换.assets/IMG-cpp-类型转换-918A56255A0AB61D4FDBA465A7EADB47.png|450]] > > 涉及知识点:  > > 1. **补码表示**:"-2" 的补码为 `0xFFFFFFFE`; > 2. **隐式类型转换**:表达式中,两个操作数的类型不同, 因此触发了"**隐式类型转换**"; > 3. **"标准转换"-"整型提升"规则**:类型转换依据 C++中"**标准转换**"里的"**整型提升**"的规则,将**有符号数提升为无符号数**, > > 因此表达式变为两个"无符号整型"相加,**表达式类型为无符号整型**, > 二进制位相加的结果 `0xFFFFFFFF` **在无符号整型下表示的真值为 4294967295**! > [!example] 整型提升——==**有符号整型**==转换为**==无符号整型==** (示例二) > > **有符号数和无符号数==比较==**时, **有符号数会被提升为无符号数**,然后**比较两个无符号数**。 > > > ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-类型转换.assets/IMG-cpp-类型转换-4C9B64D939803119C1DF4A4614590167.png|700]] <br><br><br> ## 向上转型与向下转型 向上转型(Upcasting)和向下转型(Downcasting)是涉及**类的继承**体系时的类型转换,参见 [^6]。 - ==**向上转型**==:**将派生类的指针(或引用)转换为基类的指针(或引用**)。 - 安全的类型转换,可直接 **==隐式进行==**,**无需使用任何显式类型转换语法**; <br>(原因是每个 "**派生类对象**" 都包含一个**基类部分**) - **==向下转型==**:将**基类的指针(或引用)转换为派生类的指针(或引用)**。 - 潜在的不安全转换,**==不能隐式进行==**,因为基类指针或引用指向的不一定是派生类对象。 - 向下转型需要**显式地使用类型转换操作符**,如 `static_cast<>` 或 `dynamic_cast<>`。 - 向下转型 **只应当在明确知道==基类指针或引用实际指向一个派生类对象==时进行**。 <br> ### 向上转型:派生类向基类的类型转换 派生类对象中包含有基类的组成部分,故**可将派生类对象当成基类对象使用**(将发生 "**==对象切割==**",**派生类特有部分将丢失**) C++中支持 "**派生类到基类**" 的 "==**隐式类型转换**==",即 1. 可以**将 "==基类的指针或引用==" 绑定到 "==派生类对象=="**; (进一步,结合对 "虚函数" 的**动态绑定**机制,可实现运行时多态) 2. 可以将 "**==对派生类的引用或指针==**" 用于需要 "**==对基类的引用或指针==**"的地方。 - 例如,在**派生类的拷贝构造函数**的成员初始化列表中,用 "**派生类引用**" 调用 "**基类的拷贝构造函数**",完成基类部分的拷贝。 说明示例: ```cpp struct Base { Base() {} Base(const Base& rhs) {} }; struct Derived: public Base { Derived() : Base() {} // 向上转型: 将派生类引用, 传递给基类的拷贝构造函数, 从而完成基类部分的拷贝构造. Derived(const Derived& rhs) : Base(rhs) {} }; void func(Base&) { cout << "func(Base&)" << endl; } void func2(Base*) { cout << "func(Base*)" << endl; } int main() { Derived derived_obj; func(derived_obj); func2(&derived_obj); return 0; } ``` > [!NOTE] 「智能指针类」也支持派生类向基类的 "**向上转型**",因此可以将一个**派生类对象的指针存储在一个 "基类的智能指针"**内。 > [!caution] 派生类向基类的自动类型转换只对 "指针或引用类型" 有效,不存在 "派生类类型" 与 "基类类型" 之间的转换。 > > 一些**看似表现为 "派生类类型" 转为 "基类类型" 的场景**,其实际过程仍然是 "**基类的引用绑定到派生类对象**"。 > > 例如,为一个 "**形参为基类类型**" 的函数传入 "**派生类类型**" 的实参, > 实际上是触发了 "**基类的拷贝构造函数**",该拷贝构造函数中的 "**基类引用**" 绑定到了 "派生类类型实参" 上。 > > ```cpp > struct Base { > Base() {} > Base(const Base& rhs) { cout << "Base(const Base& rhs)" << endl; } > }; > > struct Derived: public Base {}; > > void func(Base) { > cout << "func(Base)" << endl; > } > > int main() { > Derived derived_obj; > // 实际上并不是 Derived 类型直接转为 Base 类型. > // 而是 Derived 实参调用了 Base 的拷贝构造函数 > func(derived_obj); > return 0; > } > ``` > <br> #### 向上转型的可访问性 🚨 **派生类 `D` 向基类 `B`的类型转换**是否可使用,取决于**转换代码的位置**以及**派生类`D`的派生访问说明符**: - 仅当 **==派生类 `public` 地继承基类==** 时,"**用户代码**" 才能使用 "**==派生类到基类的转换==**。<br>("用户代码"即指类的使用者,类外代码) - **派生类的==成员函数和友元==中,永远能够使用==派生类到其直接基类==的转换**,无论派生类以何种方式继承。 - 如果**派生类`D` 以 `public` 或 `protected` 的方式继承自基类 `B`**,则在 "**==派生类`D`的派生类==的成员函数与友元**" 中可以执行 `D->B` 的向上转型;若是私有继承,则不行。 ```cpp struct Base { Base() {} Base(const Base&) {} }; struct Derived: protected Base { Derived() : Base() {} // 无论以何种方式继承, // 在派生类的成员函数或友元内, 均可执行派生类->基类的向上转型. Derived(const Derived& rhs) : Base(rhs) {} }; int main() { Derived derived_obj; // error: Derived以`protected`的方式继承自Base // 故外部代码不能执行 Derived => Base的向上转型. // Base* ptr_base = &derived_obj; // error; return 0; } ``` <br> ### 向下转型:"基类指针或引用" 向 "派生类指针或引用" 的转换 向下转型**不支持 "隐式转换"**,必须**通过 `static_cast<>` 或 `dynamic_cast` "==显式进行转换=="**。 - `static_cast<Derived*>()` 执行 "**编译时转换**",编译器将**假设类型转换合法**,即 `Base*` 实际指向 `Derived` 类型的对象。 - 若 `Base*` 实际指向的不是 `Derived*` 类型,将导致 "**==未定义行为==**"; - `dynamic_cast<Derived*>()` 执行 "**==运行时转换==**",其将**在运行时根据指针所指对象的 "动态类型" 决定转换结果**。 - 若 `Base*` 实际指向确为 `Derived*` 类型,则转换成功; - 否则,**返回空指针 `nullptr`**。 ![[02-开发笔记/01-cpp/类型相关/cpp-类型转换#^n4n08c]] 示例可见 [^7] ```cpp Base *ptr_base = new Derived; // 基类指针, 实际上指向派生类对象. // error: 不能直接执行 "向下转型", 将实际指向派生类的基类指针转为派生类指针 // 因为不存在"基类到派生类的隐式类型转换", 编译器在编译时执行类型检查, 将报告错误. // Derived *ptr_derived = ptr_base; // error! // 可通过`dynamic_cast<>`进行转换, 其在"运行时"进行类型检查. if (Derived *ptr_derived = dynamic_cast<Derived*>(ptr_base)) { ... // 若ptr_base所指对象的动态类型确为Derived时, 转换成功; } else { ... // 若ptr_base所指对象的动态类型非Derived, 转换失败, 返回空指针赋给ptr_derived. } ``` ### 使用示例 > [!example] 向上向下转型示例 > > ```cpp title: upcasting_downcasting. cpp > struct Base { > virtual ~Base () {} // 基类至少要拥有一个虚函数, 从而使其存在"虚函数表". > }; > > struct Derived : public Base { > ~Derived () override {} // 确保派生类也有正确的虚函数定义 > }; > > // `dynamic_cast` 在运行时使用类型信息来检查转换的有效性,其所用的类型信息通过虚函数表来维护, > // 因此要求使用 `dynamic_cast` 进行向下转型的基类必须至少有一个基函数。 > > // 向上转型: 可隐式进行 > void upcasting () { > Derived d; > Base* ptr_base = &d; // 向上转型, `&d` 表达式的类型是"派生类指针", 将其直接赋给"基类指针", 隐式实现类型转换 > // 这里不是向上转型. 而是用派生类对象的基类部分拷贝构造基类对象 > Base base_obj = d; // 使用派生类对象来初始化基类对象 > } > > // 向下转型: 只应当在"明确知道基类指针或引用实际指向一个派生类对象"时进行. > // 向下转型需要使用显式类型转换语法, 如类型转换操作符. > void downcasting () { > Derived d; > Base* ptr_base = &d; > > Derived* ptr_derive = static_cast<Derived*>(ptr_base); // 向下转型, 基类指针转换为一个派生类指针 > > // 更安全的向下转型, 使用 dynamic_cast<>, 在运行时检查类型合法性. > Derived* ptr_derive2 = dynamic_cast<Derived*>(ptr_base); > if (ptr_derive2) { > // 转换成功, 表明 ptr_base 确实指向一个 Derived 对象. > } else { // 转换失败时, ptr_derive2 为空指针. > // 转换失败, 表明 ptr_base 不指向一个 Derived 对象. > } > } > ``` > > <br><br><br> # 用户定义的转换 **用户定义的转换**(User-Defined Conversion) 包括两种类型[^2]: - **接收其他类型参数的==构造函数**==: 提供了**其他类型**到该**类类型**的转换路径 - **==转换函数==**:提供该 **类类型** 到 **其他类型** 的转换路径 当类中提供了一个**接收其他类型参数的构造函数**或者一个**转换函数**时,即是**提供**了一个 "**类型转换路径**": - 若构造函数、转换函数**没有被 `explicit` 修饰**,则该转换过程允许显式或隐式地被执行。 - 当构造函数、转换函数**被 `explicit` 修饰**时,表明禁止用于 **==隐式类型转换==** ,仅能以"显式"的方式调用。 ```cpp title:user_defined_conversion.cpp struct A { // `non-explicit`的转换构造函数与转换函数所提供的"转换路径" 在显式、隐式类型转换中均会被考虑 A(int); // 提供了`int->A` 的转换路径. operator string(); // 提供了`A->string`的转换路径. }; struct AA { // explicit构造函数与转换函数所提供的"显式转换路径"仅能用于"显式类型转换", 禁止编译器将该路径用于隐式类型转换. explicit AA(int); // 提供了`int->A` 的"显式"转换路径 explicit operator string(); // 提供了`A->string`的"显式"转换路径 }; void FuncAcceptA(A); void FuncAcceptAA(AA); void FuncAcceptString(string); template<typename T> string FuncReturnString(T& obj) { cout << "Invoke FuncReturnString(T& obj)" << '\n'; return obj; } int main() { A a(10); AA aa(10); // 拷贝初始化 // 对类A, 隐式调用其构造函数`A(int)`, 属于隐式类型转换 // 对类AA, 由于其构造函数`explicit AA(int)`禁止用于隐式类型转换, 因此编译报错. A a1 = 10; // AA aa1 = 10; // error AA aa1 = static_cast<AA>(10); // 显式类型转换 // 函数调用传参 // 对FuncAcceptA(), 需拷贝初始化A类对象, 隐式调用`A(int)`进行隐式类型转换. // 对FuncAcceptAA(), 需拷贝初始化AA类对象, 但由于构造函数`explicit AA(int)`禁止用于隐式类型转换, 因此编译报错. FuncAcceptA(10); // FuncAcceptAA(10); // error // 函数调用传参: 对FuncAcceptString(), 需拷贝初始化string类对象. // 当传入参数为A类对象时, 调用A的`operator string()`进行A->string的隐式类型转换. // 当传入参数为AA类对象时, 由于AA的`explicit operator string()`禁止用于隐式类型转换, 因此编译报错. FuncAcceptString(a); // FuncAcceptString(aa); // error } ``` ### 转换构造函数 参见 [[#[转换构造函数](https //en. cppreference. com/w/cpp/language/converting_constructor) (converting constructor)|转换构造函数]] ### 转换函数 User-defined conversion function <br><br> # 窄化转换/缩窄转换 **窄化转换/缩窄转换**(narrowing conversion)特指 "**==可能会导致数据丢失或数值改变==**" 的**类型转换**——**原始数据类型下的值无法在 "目标类型" 中被精确表示,值会改变。 **窄化转换** 是 **标准转换中需要特别注意的一种情况**,通常发生在 "**将一种较大范围或精度的类型**" 转换为 "**较小范围或精度的类型**" 时。 发生**窄化转换后的结果**可能为: - **==精度丢失==** - 例如将精度更细的 **浮点型** 转为 **整型** 时,**小数部分将被丢弃**。 - **==位截断==**:**较大字长/位宽的类型**转为**较小字长/位宽的类型** - 例如将 64 位的 `long long` 转为 32 位的 `int` 时,高 32 位将被截断丢弃; - **==溢出==**:**位宽/字长相同**,但**两类型对同一种位模式的解释不同**,**转换到目标类型时发生溢出(被解释为另一个值)** - 例如将 64 位的 `long long` 类型的值 `2147483648` 转为 32 位的 `int` 类型时,低 32 位的位模式不变(`0x80000000`),但在 `int` 类型下解释为 `-2147483648`,发生了溢出。 - 例如将 32 位的 `unsigned int` 类型的值 `2147483648` 转为 32 位的 `int` 类型时,位模式不变,但在 `int` 类型下解释为 `-2147483648`,发生了溢出; > [!example] 发生窄化转换的常见场景 > > - 将精度更细的 **浮点型** 转为 **整型** 时,**小数部分将被丢弃**。 > - 例如:`double` 转为 `int`。 > - 将 **"较大的整数类型" 转为 "较小的整数类型"** 时,如果**值超出了目标类型的范围**,结果是未定义的: > - 如果位宽不同,则会发生**位截断**; > - 即使位宽相同,位模式不变,但**对位模式的解释也会不同**,可能**发生溢出**(被解释为另一个值); > > ```cpp title:narrowing_exam.cpp > // 窄化转换: 精度丢失 > // 将精度更细的浮点型转为整型时,小数部分将被丢弃. > double d = 3.14; > int i = d; // 缩窄转换, 丢失了小数部分, i 为 3 > > // 窄化转换: 超出范围, 未定义行为 > // 将"较大的整数类型"转为"较小的整数类型"时,如果值超出了目标类型的范围,则结果是未定义的 > // 可能发生位截断, 也可能发生溢出(被解释为另一个值) > > // (1)窄化转换-超出范围: 位截断 > int aa = 2147483647; // 0x7FFFFFFF > short ss = aa; // 0xFFFF, 高 16 位被截断丢弃, 只保留低 16 位 > assert(ss == -1); > > // (2)窄化转换-超出范围: 溢出 > // long long 类型或 unsigned int 类型的值 2147483648 转换为 int 类型. > // 2147483648超出了int的表示范围(`INT_MAX==214748364`), 因此发生溢出. > // 2147483648 的二进制补码表示是 0x80000000 > // 位模式 0x80000000 不变, 但在 int 类型下被解释为-2147483648. > long long lli {2147483648}; > unsigned ui {2147483648}; > assert((int) lli == -2147483648); > assert((int) ui == -2147483648); > int v = 2147483648; // long long 类型的字面量 > assert(v == -2147483648); > > // 128 超出了 signed char 类型的表示范围, 其位模式的值是 0x80, 在 signed char 类型下被解释为-128 > // -128 这个值不对应任何实际字符, 因此无意义. > int a = 128; > signed char c = a; > assert(c == -128); > ``` > > > [!caution] 花括号列表初始化中禁止缩窄转换 > > **==花括号列表初始化==**( `{}`)中**禁止缩窄转换**,如果尝试进行缩窄转换,则 **编译器将报错**。 > > ```cpp > // 花括号初始化列表禁止窄化转换 > void bracket_initialization_list_forbid_narrowing_conversion() { > // 对于字面量, 只有当字面量确实会导致窄化转换时, 花括号初始化列表才会禁止使用而产生编译器报错 > // int i {3.00}; // 缩窄转换, 丢失了小数部分 > // int j {2147483648}; // 超出了 int 的表示范围, 未定义行为 > // char c {128}; // 超出了 char 的表示范围, 未定义行为 > // unsigned char uc {-1}; // 超出了 unsigned char 的表示范围, 未定义行为 > > // 以下初始化是合法的 > int i {3}; > int j {2147483647}; // 在 int 的表示范围内 > char c {127}; // 在 char 的表示范围内 > unsigned char uc {255}; // 在 unsigned char 的表示范围内 > > > // 对于变量, 只要变量类型间"有可能"存在发生窄化转换, 编译器就会报错 > // Non-constant-expression cannot be narrowed from type '...' to '...' in initializer list > long long lli {2}; > unsigned ui {2}; > // int a1 {lli}; // error > // int a2 {ui}; // error > // char c2 {i}; // error > } > ``` > <br><br><br> # 显式类型转换 > [!summary] > 显式类型转换"只是 **"==显式"地声明请求进行类型转换==**,而**实际转换过程由编译器根据两种类型之间存在的转换路径** 确定。 > > 因此,一个显式声明的类型转换(包括直接初始化),其转换过程中仍可能包含有"**隐式类型转换**": > 如果不存在直接转换路径,但可通过一个或多个中间类型进行间接转换,则存在一条隐式转换路径。此时从"**源类型**"到"**中间类型**"的转换为**隐式类型转换**,而从中间类型到目标类型的转换则取决于上下文。 ## 显式类型转换的触发语法 触发语法: ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-类型转换.assets/IMG-cpp-类型转换-75C555C3E1F9B8BB4980608E29D32EB5.png|450]] - (1) **C 风格**的类型转换表达式; - (2) **函数风格**的类型转换表达式(`target-type` **只允许为"==单个==类型说明符或 `typedef` 说明符"**,即形如 `unsigned int(exp)` 与 `int*(exp)` 是非法的) - 如果圆括号内只有**一个表达式**,等价于 C 风格的类型转换; - 如果圆括号内有**多个表达式**,或者内嵌**花括号初始化列表**,则 `target-type` 必须为**具有对应构造函数的==类类型==**。 - 如果圆括号内没有表达式: - 若 `target-type` 是非数组的完整对象类型,则该表达式是**该类型的纯右值**(rvalue); - 若 `target-type` 是对象类型,则该对象被==**值初始化**==。 - 若 `target-type` 是 `void`, 则该表达式是一个**无结果对象的空纯右值**(prvalue)。 - (3) **花括号初始化列表**形式的类型转换表达式 - 单个类型说明符后跟随一个花括号初始化列表,则该表达式是一个目标类型的纯右值,结果对象将由指定的初始化列表进行 "直接列表初始化"。 - 若 `target-type` 是 `void`,则则该表达式是一个**无结果对象的空纯右值**(prvalue)。 - 这是唯一能创建一个 "**数组纯右值**" 的表达式(unti C++20) - (4)(5) 与 (2)(3)相同,除了首先执行类模版参数推导。 - (6)(7) 略 在**显式类型转换**场景下,根据 `target-type` 的类型,返回的转换结果可以是: - 若 `target-type` 是一个 "**==左值引用==**" 类型或 "**==函数类型的右值引用==**",则返回结果为一个 **左值**。 - 若 `target-type` 是一个 "**==object 类型的右值引用==**",则返回结果为一个==**亡值(xvalue)**== - 其他情况下,返回结果为一个 "**纯右值**"。 <br><br> ## 显式类型转换操作符 C++中提供了四种**显式类型转换操作符** [^1](P145),用于明确地将值**从一种类型转换为另一种类型**。 语法格式:`op<typename>(obj)`: - `static_cast` :**静态类型转换**,在 **==编译时执行严格的类型检查==**,只能用于 "**==具有明确定义的类型转换==**",包括标准转换、用户定义转换、左值到右值引用等 - `dynamic_cast`:**动态类型转换**,是指在 **==运行时==执行类型转换**,用于处理 **==多态类型的安全转换==**,主要用于 **==向下转换==**(从基类到派生类)。 - `const_cast`: 主要用于 **==移除指针或引用的底层 `const` 属性==**。 - `reinterpret_cast`:可用于**任意的类型转换**,**==不提供类型安全性==**,**同 ==C 风格的强制类型转换==**。 - 例如,如**将指针类型转换为足够大的整数类型,或将指针从一种类型转换为完全不相关的另一种类型**。 > [!NOTE] 相比 C 风格的强制类型转换,如 `(int)x`,C++的四种**显式转换操作符**意图更显著,且更有助于实现类型安全的转换。 > [!info] 类型安全 > > "**类型安全**" 是指**程序在运行时或编译时**,**变量只能以其定义的类型进行操作**,不会因出现 "**类型不匹配**" 而导致错误。 > > - **类型一致性**:一个变量被赋予的值或用于运算的方式,必须与其定义的类型相符。 > - **==明确的类型转换规则==**:只允许进行 "**具有明确定义的类型转换**"。 > - **禁止未定义行为**:语言和编译器应该防止类型不匹配的操作导致未定义行为。 <br> ### (1) `static_cast` 操作符 `static_cast<>` 执行 "**==静态==类型转换**",其在 "**==编译时==**" 检查**类型转换**的合法性。 **任何 "==具有明确定义的类型转换=="**,**在不涉及底层 `const`** 的情况下,都可应用 `static_cast<>` 完成: 1. **基本数据类型之间**的转换: 2. 类继承体系中,**基类与派生类之间**的转换: - 将 "**派生类引用或指针**" 转换为 "**基类指针或引用**"(向上转型,必然是类型安全的) - 将 "**基类指针或引用**" 转换为 "**派生类指针或引用**"(向下转型) - `static_cast<>` **不进行运行时类型检查**。**如果转换不安全**(即 **==实际对象不是目标类型的实例==**),则会导致**未定义行为**。 3. **任意类型的指针与 `void*` 之间**的转换; 4. **==用户定义的类型转换==**:调用转换构造函数 or 转换函数完成; 5. 从 "**==左值到右值引用==**" 的特殊转换 6. **==添加 `const` 属性==**: - 添加**指针或引用的 "底层 const"** 属性; - 添加**指针或值的 "顶层 const"** 属性; 对于 "**无明确定义的转换**",属于不安全的转换,将会报告编译错误,例如: - **不同类型的指针之间的转换**(除了 `void*` 与任意指针之间的转换) - **整数与指针之间的转换**(例如 `0` 或 `NULL` 到指针是不行的) > [!important] `static_cast` 支持 **==从左值到右值引用==的特殊转换**。 > > C++中不允许**从左值到右值引用**的**隐式类型转换**,这一转换也不属于标准转换,**只是显式类型转换中的一个特例**。 > > `std::move()` 的实现内部即是调用了 `static_cast<remove_reference<T>::type&&>(t)` > ```cpp title:static_cast.cpp class Base {}; class Derived : public Base {}; struct MyClass { MyClass(int) {} operator double() { return 0.0; } }; int main() { // 基本数据类型间转换 double d = 10.5; int i = static_cast<int>(d); // 将double转换为int double db = static_cast<int>(i); // 将int转为double // 类层次结构中的转换 Base obj_b; Derived obj_d; Base* ptr_base = static_cast<Base*>(&obj_d); // 向上转型(保证类型安全) Derived* ptr_d1 = static_cast<Derived*>(ptr_base); // 向下转型(不进行安全检查, 但这里是安全的, 因为ptr_base实际指向的是Derived对象 Derived* ptr_d2 = static_cast<Derived*>(&obj_b); // 未定义行为, 因为obj_b不是Derived对象 // void指针转换 int a = 10; void* vp = static_cast<void*>(&a); // 将int*转换为void* int* ip = static_cast<int*>(vp); // 将void*转换回int* // 转换枚举类型 enum class Color { red, green, blue }; Color c = Color::red; int color = static_cast<int>(c); // 将枚举类型转换为int // 调用单参数构造函数或转换函数进行转换 MyClass obj = static_cast<MyClass>(10); // 调用MyClass(int)构造函数 double d2 = static_cast<double>(obj); // 调用MyClass::operator double()转换函数 // static_cast支持从"左值到右值引用"的特殊转换 int n = 5; int& lref = n; int&& rref = static_cast<int&&>(n); int&& rref2 = static_cast<int&&>(n); // error: 不能将左值转换为右值引用 } ``` <br><br> ### (2) `dynamic_cast` 操作符 `dynamic_cast` 专用于处理 **==多态类型的安全转换==**(如下列两种情况), - **==向下转型==**:安全地将**基类指针或引用**转换为**派生类指针或引用**; - **==交叉转型==**:**在同一继承体系中的不同派生类之间**进行转换; 其将**在==运行时==执行类型检查**: - 如果**指针或引用实际指向的对象类型(动态类型)** 恰好即是**目标类型**,则转换成功; - 否则,转换失败: - 对 **==指针==类型的转换**,将**返回 0** (赋给指针时,于是得到空指针) - 对 **==引用==类型的转换**,则抛出 `std::bad_cast` 异常(定义于 `<typeinfo>` 头文件中) > [!NOTE] `dynamic_cast<>` 的运行时转换依赖于 **"虚函数" 表**,因此用其进行基类到派生类的"**向下转型**"时,要求**基类必须至少具有一个==虚函数==**。 > > `dynamic_cast<>` 在**运行时**使用**类型信息**来检查转换的有效性,其所用的类型信息通过**虚函数表**来维护。 > ^n4n08c #### 使用说明 ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-类型转换.assets/IMG-cpp-类型转换-0CA554B3049013A2B5AB3CCC8CBE6663.png|690]] ```cpp title:dynamic_cast_usage.cpp struct Base { virtual ~Base() = default; // 虚函数. 使得Base类以及Derived类成为多态类型 int base_v = 6; }; struct Derived : public Base { int derived_v = 15; }; int main() { // `ptr_base`的静态类型是"指向基类的指针", 但实际指向一个派生类对象. Base *ptr_base = new Derived; // 向上转型: // 1) `dynamic_cast`将基类指针转为派生类指针(向下转型) // 在条件语句中声明向下转型, 则指针ptr_derived作用域为条件代码块内部 if (Derived *ptr_derived = dynamic_cast<Derived*>(ptr_base)) { // 转换成功, 指针所指的实际对象确实是Derived // 通过ptr_derived使用其指向的Derived对象 cout << ptr_derived->derived_v << '\n'; } else { // 转换失败, 指针所指的实际对象非Derived // 通过ptr_base使用其指向的Base对象 cout << ptr_base->base_v << '\n'; } // 2) `dynamic_cast` 将基类引用转换为派生类引用 (向下转型) Base b; const Base& b_ref = b; try { const Derived& d = dynamic_cast<const Derived&>(b_ref); } catch (std::bad_cast) { // 处理类型转换失败的情况. } } ``` <br> ### (2+)`dynamic_pointer_cast` 操作符 专用于对 **==`std::shared_ptr` 类型智能指针==** 实现 "**向下转换**": 用于**将一个 `std::shared_ptr` 转换为另一种类型的 `std::shared_ptr`**。如果转换失败,返回一个空的 `std::shared_ptr`。 ```cpp class Base { public: virtual ~Base() = default; }; class Derived : public Base { public: void display() { std::cout << "Derived class display function" << std::endl; } }; int main() { std::shared_ptr<Base> basePtr = std::make_shared<Derived>(); // 使用`dynamic_pointer_cast` 将 std::shared_ptr<Base> 转换为 std::shared_ptr<Derived> std::shared_ptr<Derived> derivedPtr = std::dynamic_pointer_cast<Derived>(basePtr); if (derivedPtr) { derivedPtr->display(); } else { std::cout << "Conversion failed" << std::endl; } } ``` <br><br> ### (3) `const_cast` 操作符 `const_cast<>()` 用于**去除/添加==指针或引用==的 ==const 或 `volatile`属性==**,通常用在 **存在 "重载函数" 的场景**来明确调用版本。 > [!danger] **若被 `const T*` 或 `const T&`指向的==对象本身是常量==**,则**通过移除 `const` 属性后的指针或引用==修改对象值==,将是未定义行为**。 > [!caution] `const_cast<>` 只能用于到 "==**指针或引用**==" 类型的转换,不能用于到 **==值类型==** 的转换(编译错误)! > [!NOTE] `const_cast<>` 主要用于 "**==去除==**" **指针或引用的底层 const 属性** > > - `static_cast<>` 即可 **"添加" const 属性**(顶 or 底层),因此 `const_cast<>` 主要用于 "**去除**"。 > - `const_cast<>` 可以 **增添/移除 "==指针==" 的 "顶层 const"**,可编译通过,但是什么场景会需要这样做呢? > 使用说明: ```cpp title:const_cast_usage.cpp // `const_cast`主要用于去除"指针或引用"的底层const属性. void remove_const() { int i = 5; const int ci = 10; int* const cp = &i; const int* p_ci = &ci; const int* const cp_ci = &ci; const int& c_lref = i; // 去除指针的底层const int* p = const_cast<int*>(p_ci); // 去除`const int*`的底层const属性 int* const p2 = const_cast<int* const>(cp_ci); // 去除`const int* const`底层const属性 // 去除引用的底层const int& lref = const_cast<int&>(c_lref); // 去除`const int&`的底层const属性 // 未定义行为: 尝试去除指针的顶层const. 能编译通过, 但是是ub. // int* p3 = const_cast<int*>(cp); // error: 未定义行为 // int* p4 = const_cast<int*>(cp_ci); // error: 未定义行为 // error: 不能去除"值对象"(非指针非引用)的顶层`const`属性 // int i2 = const_cast<int>(ci); // 不能将`const int`转换为int } // 通过`const_cast`可以为"指针或引用"添加顶层/底层const属性, 通常用于"函数重载"的场景, 明确触发具有const的重载版本. // 但是不能为"值对象"(非指针非引用)添加`const`属性 void add_const() { int i = 5; int* p = &i; int& lref = i; // 为指针添加顶层或底层const int* const cp = const_cast<int*>(p); // 添加`int* const`的顶层const属性 int* const cp2 = p; const int* p_ci = const_cast<const int*>(p); // 添加`const int*`的底层const属性 const int* p_ci2 = p; const int* const cp_ci = const_cast<const int* const>(p); // 添加`const int* const`的顶层、底层const属性 const int* const cp_ci2 = p; // 为引用添加底层const const int& c_lref = const_cast<const int&>(lref); const int& c_lref2 = lref; // error: 不能为值对象(非指针非引用)添加`const`属性 // const int ci = const_cast<const int>(i); // 不能将int转换为`const int` } ``` > [!caution] 尝试通过一个==**经 `const_cast` 去除了 const 属性的指针或引用**==来修改 ==const 对象==是未定义行为。 > > ```cpp > const int ci = 10; > int* p = const_cast<int*>(&ci); // 移除变量`ci`地址`const int*`的const属性, 转为`int*` > *p = 20; // 未定义行为, 因为p指向的实际上是一个`const`对象. > ``` #### 应用场景 `const_cast<>` 用于对 **指针和引用** 修改其 const 属性,常用于实现 "**==重载函数==**"[^3] [^4]: 当需要为一个函数**重载接收 `non-const` 与 `const` 形参的两个版本**时,可**在 `non-const` 版本中借助 `const_cast<>` 来调用 `const`形参版本**(或者反之,在 const 版本中通过 const_cast 去掉 const 修饰后调用 non-const 版本),从而**减少重复代码**。 > [!example] boost 中 any_cast 的两个重载版本 > ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-类型转换.assets/IMG-cpp-类型转换-1D43319E931728EDB0BA04A1F39070B4.png|670]] 示例: 1. 一个以 **`const` 引用**为形参、**`const` 引用**为返回类型 2. 一个以**普通引用**为形参,**普通引用**为返回类型。 - 该版本涉及到两次转型: - (1)通过 `const_cast` 为形参加上 `const`,**调用上述重载**; - (2)通过 `const_cast` 对返回值去掉 `const`。 ```cpp const_cast_usage.cpp // 示例一: 用于重载普通函数 const string& shorterString(const string &s1, const string &s2) { return s1.size() <= s2.size() ? s1 : s2; } string& shorterString(string &s1, string &s2) { // 1) 通过`const_cast`来添加`const`限定符, 调用另一个重载版本. auto &r = shorterString( const_cast<const string&>(s1), const_cast<const string&>(s2)); // 2) 通过`const_cast`来去除`const`限定符, 返回一个`string&`类型的引用. return const_cast<string&>(r); } // 示例二: 用于重载 "成员函数" class MyClass { public: const char& operator[](std::size_t pos) const { ... return text[pos]; } char& operator[](std::size_t pos) { ... // 将(*this)转为`cosnt MyClass&`, 调用const重载版本, 再通过const_cast将其返回值的const属性去掉. return const_cast<char&>(static_cast<const MyClass&>(*this)[pos]); } private: char* text; }; ``` <br><br> ### (4) `reinterpret_cast` 操作符 `reinterpret_cast<>()` 为运算对象的 "**==位模式==**" 提供**低级别的重新解释**,即用于在**不同类型之间**进行**低级别的强制类型转换**, 通常用于: - **==不同指针类型==之间进行转换**(几乎可在任意指针类型间转换) - **==指针与足够大的整数类型==之间的转换**:例如,将指针转换为`uintptr_t`类型(一个无符号整型,足以存储指针值),或将`uintptr_t`转换回指针类型。 `reinterpret_cast` **不提供类型安全性**,**==不执行任何类型检查或转换运算==**,只是**简单地重新解释给定值的位模式**。 > [!caution] `reinterpret_cast` 可以去除指针的顶层 const 属性,**但==不能去除指针或引用的底层 const 属性==**。 ```cpp title:reinterpret_cast_exam.cpp // 任意指针类型之间的转换 int* ip = nullptr; void *vp = reinterpret_cast<void*>(ip); int* ip2 = reinterpret_cast<int*>(vp); char* char_p = reinterpret_cast<char*>(ip2); const char* cstr = "example"; unsigned char* ustr = reinterpret_cast<unsigned char*>(const_cast<char*>(cstr)); // `reinterpret_cast`可以去除指针的顶层const属性, 但不能去除指针的底层const属性 const int* p_ci = nullptr; int* const cp = nullptr; int* p = reinterpret_cast<int*>(cp); // 可以移除顶层const // int *p2 = reinterpret_cast<int*>(p_ci); // error: 不能移除底层const // `reinterpret_cast`不能去除引用的底层const属性 int i = 5; const int& c_lref = i; // int& lref = reinterpret_cast<int&>(c_lref); // error: 不能移除底层const } ``` <br> ## 关于 C 风格的类型转换 C 风格的类型转换源自 C 语言,C++进一步沿用,是最早期、最基本的转换方式。 C 风格的类型转换不考虑类型安全问题,而**直接进行强制转换**,**允许几乎任意类型之间的转换,而不进行严格的类型检查**,**不进行编译时的类型安全检查**,因此可能导致难以发现的错误。 语法如下: ```cpp // C风格的显式类型转换通用格式 (typename) value; // 来自C语言 typename (value); // C++特有格式 ``` 示例: ```cpp double d = 10.5; // 将double转换为int int i = (int)d; // 将整数转换为指针 void* p = (void*)i; ``` 编译器会将"**C 风格的类型转换**" 尝试解释为下列**转换表达式**(按顺序进行): - `const_cast<target-type>(expression)` ; - `static_cast<target-type>(expression)`,包括扩展的特殊转换: - **指向派生类的指针或引用**,允许被转换为**指向明确基类的指针或引用**(即使基类不可访问;即 `static_cast` 会忽略 `private` 继承的限制),反之亦然; - **指向成员的指针**,允许被转换为 "**指向明确的非虚基类的指针**" 。 - `static_cast`(包括扩展)后跟随 `const_cast` ; - `reinterpret_cast<target-type>(expression)` - `reinterpret_cast` 后跟随 `const_cast` 编译器将根据上述顺序进行 check,将选取并应用**首个==满足转换操作符要求==的转换**(即使不能被编译,则报编译错误) 如果转换可以**以多种方式被解释为 `static_cast` 后跟随 `const_cast`**,则不能被编译。 此外,C 风格的转换语法允许**从==指针==到==不完整类类型==之间的转换、以及==指向不完整类类型的指针之间==的转换**。如果 `expression` 和目标类型 `target-type` 都是指向不完整类类型的指针,则选择 ` static_cast ` 还是 ` reinterpret_cast ` 是未指定的。 %% > > 关于 "**C 风格的类型转换**" 与 "**C++风格的类型转换**" > > ![image-20231028152654173](file:///E:/%E6%88%91%E7%9A%84%E6%96%87%E6%A1%A3/%E6%88%91%E7%9A%84%E7%AC%94%E8%AE%B0/markdown_notes/Work%20Vault/01-%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/01-cpp/assets/%E7%BC%96%E7%A8%8B.assets/image-20231028152654173.png?lastModify=1707630191) > > ![image-20240114161447904](file:///E:/%E6%88%91%E7%9A%84%E6%96%87%E6%A1%A3/%E6%88%91%E7%9A%84%E7%AC%94%E8%AE%B0/markdown_notes/Work%20Vault/01-%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/01-cpp/assets/%E7%BC%96%E7%A8%8B.assets/image-20240114161447904.png?lastModify=1707630191) %% <br><br><br> # 隐式类型转换 **隐式转换**(implicit conversion):由编译器自动执行的类型转换,无需在代码中显式指定转换。 > [!important] **隐式类型转换的界定** > > > 对一个 `T1` 类型的表达式 `e` ,**当且仅当类型 `T2` 可由 `e` "==拷贝初始化=="(即 `T2 t = e;`)时,判定存在从 `T1` 到 `T2` 类型的==隐式转换路径==**。 > > 注意,仅满足 "**==直接初始化=="( `T2 t(e)` )不能确定存在可行的"隐式类型转换"**,因为直接初始化会将 **`explict` 的 "构造函数与转换函数" 都会纳入考虑使用**。 <br> ## 隐式类型转换的触发场景 隐式类型转换发生在 "**一个具有 `T1` 类型的表达式**" 被用于 "**==不接受该类型== 但 ==接受另一个 T2 类型表达式**==" 的上下文中,例如: - **函数调用时**: 以 **`T1` 类型**表达式作为**实参**,调用一个**形参为 `T2` 类型**的函数时 - **用作操作数时**:对一个**期望 `T2` 类型操作数**的运算符,使用 `T1` 类型表达式作为其操作数时 - **==赋值==** 时,将一种算术类型(字符、整型、布尔型、浮点型)值赋给另一种算术类型的变量时 - 表达式中包含**不同数值类型**并进行**算术运算**时,编译器将对其值的类型进行隐式转换 - **初始化一个 `T2` 类型的新对象时**: - 在一个返回类型为 `T2` 函数中,`return` 语句提供了一个 `T1` 类型的表达式时; - Ps:如果 `T2` 类型具有一个以 `T1` 类型为参数的构造函数且显式调用该构造函数,则这种情况不属于隐式类型转换。 - **用于 `switch` 语句时**:`T1` 类型的表达式隐式转换为"**整型**"。 - **用于 `if` 或循环语句中的"条件判断"时**:`T1` 类型的表达式隐式转换为 **"bool 类型**" **隐式类型转换的过程**允许一个 "**==隐式转换序列==**",仅当**存在一个从 `T1` 到 `T2` 的明确的合法的==隐式转换序列**== 时,**程序才可编译**,否则将会报错,无法进行隐式转换。 <br><br> ## 隐式转换序列 **==隐式转换序列==**(Implicit sequence consists)确定了一个**隐式转换过程**。 隐式转换序列允许由下列项**按序**构成: - **零个或一个==标准转换序列==**(Standard Convertion Sequence) - "**一个标准转换序列**" 由下列项**按顺序**组成: - 零个或一个下列转换: - **"==左值到右值=="的转换** - **"==数组到指针=="的转换** - **"==函数到指针=="的转换**:当数组和函数名用在大多数表达式中时,它们会自动转换为对应的指针类型。 - 零个或一个 **==数值提升==** 或 **==数值转换==** - 零个或一个 **==函数指针==**(function pointer)转换(since C++17) - 零个或一个 "**==限定修饰==**(qualification)" 转换 - **零个或一个 ==用户定义的转换==**(user-defined conversion) - **零个或一个 ==标准转换序列==**(仅当使用了用户定义转换时,其后可再跟随一个标准转换序列) 此外,对于**非类类型之间的转换**(一个 "non-class"类型转换到另一个"non-class"类型 ),**转换过程中==只允许发生一次标准转换序列==**,**==不能涉及用户定义的转换==**,**只能使用 C++语言标准提供的转换规则,例如数值提升**。 > [!summary] > 简单来说,隐式转换序列最多可包含**三个步骤** [^5]: > > - **首先**,可能有一个**标准转换序列**(可选的); > - **其次**,可能有一个**用户定义转换**(可选的); > - **最后**,**如果**前一步存在用户定义转换,则其后可以**跟随另一个标准转换序列**(可选的)。 > > 即在隐式转换过程中,**==最多只能有一个用户定义的转换==,而在用户定义的转换之前或者之后==可以有一个标准转换序列==**。 > (暂未想到完整包含三个步骤的例子,实际上应该只能是要么在用户转换之前、要么在用户转换之后再带有一个标准转换序列) > > 这一规则**防止了用户定义的转换被连锁使用**,避免导致复杂和难以预测的转换结果。 --- ```cpp void FuncAcceptTypeA(A) {     cout << "Invoke function `FuncAcceptTypeA(A)`" << '\n'; } void FuncAcceptTypeAConf(A&) {     cout << "Invoke function `FuncAcceptTypeA(A&)`" << '\n'; } FuncAcceptTypeA(10); // 禁止, 形参参数是一个左值引用, 只能传入左值. 而10是一个右值. // FuncAcceptTypeAConf(10); // No matching function for call to 'FuncAcceptTypeA' ``` %% 此外,一个常犯的错误:对于一个接收 `string` 类对象的传入 C-string 字符串,似乎不存在从 C-string 到 string 的转换(存在!应该是被 `explicit` 禁止了!) %% <br><br> <br> # ♾️参考资料 # Footnotes [^1]: 《C++ Primer》 [^2]: [Implicit conversions - cppreference.com](https://en.cppreference.com/w/cpp/language/implicit_conversion) [^3]: 《C++ Primer》P209 [^4]: 《Effective C++》Item3 [^5]: 《C++ Primer》P515 [^6]: 《C++ Primer》P530,534 [^7]: 《深度探索 C++对象模型》P31