C++-类型转换

0.目录

  1. 隐式转换
    1.1 数值提升
    1.2 数值转换
    1.3 限定性转换
  2. 显示转换
    2.1 C风格转换
    2.2 static_cast
    2.3 dynamic_cast
    2.4 const_cast
    2.5 reinterpret_cast
  3. 用户定义转换
  4. 引用
1.隐式转换 如果表达式包含不同内置类型的操作数,并且不存在显式强制转换,则编译器将使用内置的标准转换来转换其中一个操作数,使这些类型匹配。编译器将尝试按一个明确定义的顺序进行转换,直到有一个转换成功。如果所选转换是提升转换,则编译器不会发出警告。如果转换是收缩转换,则编译器会发出有关数据可能丢失的警告。如果涉及到用户定义的类型,则编译器将尝试使用类定义中指定的转换。 如果编译器找不到可接受的转换,则会发出错误且不会编译程序。
1.1 数值提升
在数值提升中,较小的变量中的值将赋给较大的变量,同时不会丢失数据。由于扩大转换始终是安全的,编译器将在不提示的情况下执行它们且不会发出警告。
整型提升 小整型类型(如char)的纯右值可转换为较大整型类型(如int)的纯右值。具体而言,算术运算符不接受小于int的类型为其实参,而在左值到右值转换后,若适用则自动实施整型提升。转换始终保持原值。以下隐式转换被归类为整型提升:
  • signed char, signed short 可转换为int
  • bool可转换为intfalse0?true1
  • int能保有其整个值范围,则unsigned charchar8_t(C++20起)或unsigned short可转换为int,否则可转换为unsigned int
  • char可转换为intunsigned int,取决于其底层类型为signed char还是unsigned char(见上文)
  • wchar_tchar16_tchar32_t (C++11 起) 可转换为以下列表中能保有其整个值范围的首个类型:intunsigned intlongunsigned longlong longunsigned long long(C++11 起)
  • 底层类型不固定的无作用域枚举类型可转换为以下列表中能保有其整个值范围的首个类型:intunsigned intlongunsigned longlong longunsigned long long、扩展整数类型(以大小顺序,有符号优先于无符号)(C++11 起)。若值范围更大,则不应用整型提升
  • 底层类型固定的无作用域枚举类型可转换为其底层类型,而当底层类型亦适用整型提升时,则亦可转换为提升后的底层类型。到未提升的底层类型的转换,对于重载决议而言为更优
  • int能表示位域的整个值范围,则位域类型可转换为int,否则若unsigned int能表示位域的整个值范围,则可转换为unsigned int,否则不实施整型提升
除这些外,所有其他转换都不是提升;例如重载决议选择char -> int(提升)优先于char -> short(转换)。
浮点提升 【C++-类型转换】float类型纯右值可转换为double类型的纯右值。值不更改。
1.2 数值转换
不同于提升,数值转换可以更改值,而且有潜在的精度损失。
收缩转换(强制) 编译器隐式执行收缩转换,但会发出有关数据丢失可能的警告。
  • 如果确定数据不会丢失(较大变量中的值始终适合较小变量),则添加显式强制转换,去除编译器警告
  • 如果不确定转换是否安全,应为代码添加运行时检查以处理可能出现的数据丢失,确保转换不会程序错误
  • 任何从浮点类型到整型的转换都是收缩转换,因为浮点值的小数部分将会丢弃和丢失
以MSVC编译器为例:
int i = INT_MAX + 1; //warning C4307:'+':integral constant overflow wchar_t wch = 'A'; //OK char c = wch; // warning C4244:'initializing':conversion from 'wchar_t' // to 'char', possible loss of data unsigned char c2 = 0xfffe; //warning C4305:'initializing':truncation from // 'int' to 'unsigned char' int j = 1.9f; // warning C4244:'initializing':conversion from 'float' to // 'int', possible loss of data int k = 7.7; // warning C4244:'initializing':conversion from 'double' to // 'int', possible loss of data

有符号到无符号的转换 有符号整型及其对应的无符号整型的大小总是相同,但它们的区别在于为值转换解释位模式的方式。
unsigned short num = numeric_limits::max(); // #include short num2 = num; cout << "unsigned val = " << num << " signed val = " << num2 << endl; // Prints: unsigned val = 65535 signed val = -1// Go the other way. num2 = -1; num = num2; cout << "unsigned val = " << num << " signed val = " << num2 << endl; // Prints: unsigned val = 65535 signed val = -1// 如果程序生成了奇怪的结果,值的符号似乎与预期相反,有可能发生了有符号和无符号间的隐式转换 unsigned int u3 = 0 - 1; cout << u3 << endl; // prints 4294967295

指针转换 在很多表达式中,C 样式数组将隐式转换为指向该数组中的第一个元素的指针,并且可能在没有提示的情况下进行常量转换。尽管此方法很方便,但它也可能容易出错。如下:
// “Help”字符串常量将转换为指向数组的第一个元素的 `char*` // 该指针随后递增 3 个元素,现在指向最后一个元素“p” char* s = "Help" + 3;

布尔转换
  • 整数、浮点、无作用域枚举、指针和成员指针类型的纯右值,可转换成bool类型的纯右值
  • 零值(对于整数、浮点和无作用域枚举)、空指针值和空成员指针值变为false,所有其他值变为true
  • 直接初始化的语境中,可以std::nullptr_t类型纯右值(包括nullptr)初始化bool对象,结果为false,不认为它是隐式转换
1.3 限定性转换
  • 指向有const, volatile关键字限定(以下简称cv)的类型T的指针类型的纯右值,可转换为指向有更多cv限定的同一类型T的指针纯右值(换言之,能添加常性和易变性)。
  • 指向类X中有cv限定的类型T的成员指针的纯右值,可转换成指向类X中有更多cv限定的类型T的成员指针纯右值。
“更多” cv 限定表明:
  • 指向无限定类型的指针能转换成指向 const 的指针;
  • 指向无限定类型的指针能转换成指向 volatile 的指针;
  • 指向无限定类型的指针能转换成指向 const volatile 的指针;
  • 指向 const 类型的指针能转换成指向 const volatile 的指针;
  • 指向 volatile 类型的指针能转换成指向 const volatile 的指针。
对于多级指针,应用下列限制:身为限定指针,指向…… 限定指针,指向限定T的多级指针P1,可转换成身为限定指针,指向…… 限定指针,指向限定T的多级指针P2,仅当:
  • 两个指针的级数n相同
  • 若在P1的某级(除了零级)的中有cv,则在P2的同级中有cv
  • 若在P1的某级(除了零级)有未知边界数组类型,则在 P2 的同级有未知边界数组类型(C++20 起)
  • 在涉及数组类型的每一级,至少一个数组类型拥有未知边界,或两个数组类型均拥有相同大小(C++20 起)
  • 若在某级k上,P2P1有更多cv限定或P1中有已知边界数组类型而P2中有未知边界数组类型(C++20 起),则P2k为止的每一级(除了零级)...上都必须有const
  • 同样的规则用于指向成员的多级指针及指向对象和指向成员的多级混合指针
  • 同样的规则适用于包含任何级为指向已知边界或未知边界数组(认为有cv限定元素的数组自身有等同的cv限定)的多级指针(C++14 起)
  • 零级由非多级限定性转换的规则处理。
char** p = 0; const char** p1 = p; // 错误:2 级有更多 cv 限定但 1 级非 const const char* const * p2 = p; // OK:2 级有更多 cv 限定并在 1 级添加 const volatile char * const * p3 = p; // OK:2 级更有 cv 限定并在 1 级添加 const volatile const char* const* p4 = p2; // OK:2 级更有 cv 限定而 const 已在 1 级 double *a[2][3]; double const * const (*ap)[3] = a; // C++14 起 OK double * const (*ap1)[] = a; // C++20 起 OK// 注意 C 编程语言中,只能添加 const/volatile 到第一级: char** p = 0; char * const* p1 = p; // C 与 C++ 中 OK const char* const * p2 = p; // C 中错误,C++ 中 OK



2.显示转换 利用强制转换运算,可以指示编译器将一种类型的值转换为另一种类型。在某些情况下,如果两个类型完全无关,编译器将引发错误,但在其他情况下,即使运算不是类型安全的,编译器也不会引发错误。
2.1 C风格转换
语法:
  • (new_type)expression ?????(1)
  • new_type(expression) ?????(2)
  • new_type(expression list) ???(3)
  • new_type( ) ?????????(4)
  • new_type{ expression list(可选) } (5)(C++11 起)
  • 模板名( expression list(可选) ) ?(6)(C++17 起)
  • 模板名{ expression list(可选) } ?(7)(C++17 起)
返回new_type类型的值。
解释:
  1. 遇到 C 风格转型表达式时,编译器尝试将它解释成下列转型表达式,以此顺序:
    a) const_cast(expression)
    b) static_cast(expression),带扩展:额外允许将到派生类的指针或引用转型成到无歧义基类的指针或引用(反之亦然),纵使基类不可访问也是如此(即此转型忽略private继承说明符)。同样适用于将成员指针转型为指向无歧义非虚基类的成员的指针
    c) static_cast(带扩展)后随const_cast
    d) reinterpret_cast<new_type>(expression)
    e) reinterpret_cast后随const_cast
    选择首个满足相应转型运算符要求的方式,即便它无法编译。若转型能解释成多于一种 static_cast 后随 const_cast 的方式,则它无法编译。另外,C 风格转型写法允许在不完整类型的指针之间进行双向转型。若expressionnew_type是指向不完整类型的指针,则选用static_cast还是reinterpret_cast是未指明的。
  2. 函数式转型表达式由一个简单类型说明符或一个typedef说明符构成(换言之,它是单个单词的类型名:unsigned int(expression)int*(expression)非法),后随带括号的单个表达式。此转型表达式准确等价于对应的 C 风格转型表达式。
  3. 若括号中有多于一个表达式,则new_type必须是带有适当声明的构造函数的类。此表达式是new_type类型的纯右值,其指代的临时量 (C++17 前)其结果对象 (C++17 起)以 表达式列表直接初始化。
  4. new_type指名一个非数组完整对象类型,则此表达式是new_type类型的纯右值,指代该类型临时量 (C++17 前)其结果对象为该类型(可能添加cv限定符) (C++17 起)。若new_type是对象类型,则对象被值初始化。若new_type是(可cv限定的void,则表达式是void纯右值而无结果对象 (C++17 起)。
  5. 单个单词的类型名后随花括号初始化器列表,是指定类型的纯右值,其指代的临时量 (C++17 前)其结果对象 (C++17 起)以指定的花括号初始化器列表直接列表初始化。若new_type是(可cv限定的void,则表达式是void纯右值而无结果对象(C++17 起)。这是仅有的能创建数组纯右值的表达式。
  6. 同 (2-5),但首先进行类模板实参推导
  7. 同 (2-5),但首先进行类模板实参推导
double f = 3.14; unsigned int n1 = (unsigned int)f; // C 风格转型 unsigned int n2 = unsigned(f); // 函数式转型 class C1; class C2; C2* foo(C1* p) { return (C2*)p; // 转型不完整类型到不完整类型 } // 此示例中,C 风格转型被转译成 static_cast // 尽管它也能作为 reinterpret_cast 工作 struct A {}; struct I1 : A {}; struct I2 : A {}; struct D : I1, I2 {}; int main() { D* d = nullptr; //A* a = (A*)d; // 编译时错误 A* a = reinterpret_cast(d); // 这能编译 }

2.2 static_cast
static_cast(expression), 返回new_type类型的值。编译时检查。
如果编译器检测到在完全不兼容的类型之间强制转换,static_cast将返回错误。 可以使用它在指向基对象的指针和指向派生对象的指针之间强制转换,但编译器无法总是判断出此类转换在运行时是否安全。
  • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。
    • 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
    • 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
  • 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum
  • 把空指针转换成目标类型的空指针。
  • 把任何类型的表达式转换成void类型。
注意:static_cast不能转换掉expressionconstvolatile、或者__unaligned属性
char a = 'a'; int b = static_cast(a); // ok,char -> intdouble *c = new double; void *d = static_cast(c); // ok,double pointer -> void pointerint e = 10; const int f = static_cast(e); // ok,int -> const intconst int g = 20; int *h = static_cast(&g); // 编译错误,static_cast不能转换掉g的const属性double d = 1.58947; int i = d; // warning C4244 possible loss of data int j = static_cast(d); // No warning. string s = static_cast(d); // Error C2440:cannot convert from // double to std:string// ok Derived* d2 = new Derived(); Base* b= static_cast(d2)// No error but not necessarily safe. Base* b = new Base(); Derived* d2 = static_cast(b);

2.3 dynamic_cast
dynamic_cast(expression), expression必须是一个有效的指针
dynamic_cast(expression), expression必须是一个左值
dynamic_cast(expression), expression必须是一个右值
expression的类型必须符合以下三个条件中的任何一个:
  1. expression的类型是目标类型new_type的公有派生类
  2. expression的类型是目标new_type的共有基类
  3. expression的类型就是目标new_type的类型。
运行时检查,有一定开销,会影响效率。若成功,返回相应类型的值。若失败且目标类为是指针类型,则返回该类型的空指针。若失败且目标类型是引用类型,则它抛出与类型std::bad_cast的处理块匹配的异常。expression也可以是一个空指针,结果是所需类型的空指针。
dynamic_cast是基于RTTI进行运行时检查,检查时会逐个对比虚表,所以类中必须有虚函数。
struct V { virtual void f() {}; // 必须为多态以使用运行时检查的 dynamic_cast }; struct A : virtual V {}; struct B : virtual V { B(V* v, A* a) // 构造中转型(见后述 D 的构造函数中的调用) { dynamic_cast(v); // 良好定义:v 有类型 V*,B 的 V 基类,产生 B* dynamic_cast(a); // 未定义行为:a 有类型 A*,A 非 B 的基类 } }; struct D : A, B { D() : B((A*)this, this) { } }; struct Base { virtual ~Base() {} }; struct Derived: Base { virtual void name() {} }; int main() { D d; // 最终派生对象 A& a = d; // 向上转型,可以用 dynamic_cast,但不必须 D& new_d = dynamic_cast(a); // 向下转型 B& new_b = dynamic_cast(a); // 侧向转型Base* b1 = new Base; if(Derived* d = dynamic_cast(b1)) { [std::cout](http://zh.cppreference.com/w/cpp/io/cout) << "downcast from b1 to d successful\n"; d->name(); // 可以安全调用 }Base* b2 = new Derived; if(Derived* d = dynamic_cast(b2)) { [std::cout](http://zh.cppreference.com/w/cpp/io/cout) << "downcast from b2 to d successful\n"; d->name(); // 可以安全调用 }delete b1; delete b2; return 0; }

对于一些复杂的继承关系来说,使用dynamic_cast进行转换是存在一些陷阱的;比如,有如下的一个结构:

C++-类型转换
文章图片

D类型可以安全的转换成B和C类型,但是D类型要是直接转换成A类型呢?
class A { virtual void Func() = 0; }; class B : public A { void Func(){}; }; class C : public A { void Func(){}; }; class D : public B, public C { void Func(){} }; int main() { D *pD = new D; A *pA = dynamic_cast(pD); // You will get a pA which is NULL } // 如果进行上面的直接转,会得到一个NULL的pA指针 // 这是因为,B和C都继承了A,并且都实现了虚函数Func,导致在进行转换时,无法进行抉择应该向哪个A进行转换 // 正确的做法是: int main() { D *pD = new D; B *pB = dynamic_cast(pD); A *pA = dynamic_cast(pB); }

2.4 const_cast
const_cast(expression)
在有不同cv限定的类型间转换。
唯有下列转换能用const_cast进行。特别是,唯有const_cast可用于转型掉(移除)constvolatile
  1. 两个指向同一类型的可能多级的指针可以互相转换,无关乎每个层级的cv限定符
  2. 任何 T 类型的左值可转换为到同一类型 T 的左值或右值引用,cv限定可更多或更少。同样地,右值可转换成具有更多或更少cv限定的右值引用。引用 const_cast 的结果指代原对象,若expression是泛左值,否则指代实质化的临时量(C++17 起)
  3. 同样的规则适用于可能多层的到数据成员的指针,及可能多层的到已知和未知边界数组(cv 限定元素的数组被认为是自身亦有 cv 限定) (C++17 起)
  4. 空指针值可转换成new_type的空指针值
同所有转型表达式,结果是:
  • 左值,若new_type是左值引用或到函数类型的右值引用;
  • 亡值,若new_type是到对象类型的右值引用;
  • 否则为纯右值。
注意,函数指针和成员函数指针不可用于const_cast
const_cast 使得能够组成实际指代const对象的到非const类型的引用或指针,或组成实际指代volatile的到非volatile类型的引用或指针。通过非const访问路径修改const对象和通过非volatile泛左值涉指 volatile对象是未定义行为。
struct type { int i; type(): i(3) {} void f(int v) const { // this->i = v; // 编译错误:this 是指向 const 的指针 const_cast(this)->i = v; // 只要该对象不是 const 就 OK } }; int main() { int i = 3; // 不声明 i 为 const const int& rci = i; const_cast(rci) = 4; // OK:修改 i std::cout << "i = " << i << '\n'; type t; // 假如这是 const type t,则 t.f(4) 会是未定义行为 t.f(4); std::cout << "type::i = " << t.i << '\n'; const int j = 3; // 声明 j 为 const int* pj = const_cast(&j); // *pj = 4; // 未定义行为void (type::* pmf)(int) const = &type::f; // 指向成员函数的指针 // const_cast(pmf); // 编译错误:const_cast 不在成员函数指针上工作 }// 输出 // i = 4 // type::i = 4

2.5 reinterpret_cast
reinterpret_cast(expression)
通过重新解释底层位模式在类型间转换。reinterpret_cast表达式不会编译成任何CPU指令(除非在整数和指针间转换,或在指针表示依赖其类型的不明架构上)。它纯粹是一个编译时指令,指示编译器将expression视为如同具有new_type类型一样处理。它会产生一个新的值,这个值会有与原始参数expression有完全相同的比特位。
reinterpret_cast用在任意指针(或引用)类型之间的转换;以及指针与足够大的整数类型之间的转换;从整数类型(包括枚举类型)到指针类型,无视大小。
struct S1 { int a; } s1; struct S2 { int a; private: int b; } s2; // 非标准布局 union U { int a; double b; } u = {0}; int arr[2]; int* p1 = reinterpret_cast(&s1); // p1 的值为“指向 s1.a 的指针” // 因为 s1.a 与 s1 为指针可互转换 int* p2 = reinterpret_cast(&s2); // reinterpret_cast 不更改 p2 的值为“指向 s2 的指针”。 int* p3 = reinterpret_cast(&u); // p3 的值为“指向 u.a 的指针”:u.a 与 u 指针可互转换 double* p4 = reinterpret_cast(p3); // p4 的指针为“指向 u.b 的指针”:u.a 与 u.b // 指针可互转换,因为都与 u 指针可互转换 int* p5 = reinterpret_cast(&arr); // reinterpret_cast 不更改 p5 的值为“指向 arr 的指针”

struct S { int x; }; struct T { int x; int f(); }; struct S1 : S {}; // 标准布局 struct ST : S, T {}; // 非标准布局 S s = {}; auto p = reinterpret_cast(&s); // p 的值为“指向 s 的指针” auto i = p->x; // 类成员访问表达式为未定义行为:s 不是 T 对象 p->x = 1; // 未定义行为 p->f(); // 未定义行为 S1 s1 = {}; auto p1 = reinterpret_cast(&s1); // p1 的值为“指向 S 的 s1 子对象的指针” auto i = p1->x; // OK p1->x = 1; // OK ST st = {}; auto p2 = reinterpret_cast(&st); // p2 的值为“指向 st 的指针” auto i = p2->x; // 未定义行为 p2->x = 1; // 未定义行为

int f() { return 42; } int main() { int i = 7; // 指针到整数并转回 std::uintptr_t v1 = reinterpret_cast(&i); // static_cast 为错误 std::cout << "The value of &i is 0x" << std::hex << v1 << '\n'; int* p1 = reinterpret_cast(v1); assert(p1 == &i); // 到另一函数指针并转回 void(*fp1)() = reinterpret_cast(f); // fp1(); 未定义行为 int(*fp2)() = reinterpret_cast(fp1); std::cout << std::dec << fp2() << '\n'; // 安全// 通过指针的类型别名化 char* p2 = reinterpret_cast(&i); if(p2[0] == '\x7') std::cout << "This system is little-endian\n"; else std::cout << "This system is big-endian\n"; // 通过引用的类型别名化 reinterpret_cast(i) = 42; std::cout << i << '\n'; [[maybe_unused]] const int &const_iref = i; // int &iref = reinterpret_cast(const_iref); // 编译错误——不能去除 const // 必须用 const_cast 代替:int &iref = const_cast(const_iref); }// 输出 // The value of &i is 0x7fff352c3580 // 42 // This system is little-endian // 42



3.用户定义转换 允许从类类型到其他类型的隐式转换或显式转换。
转换函数的声明类似于非静态成员函数或函数模板,但没有参数或显式返回类型,并拥有下列形式的名字:
  • operator 转换类型标识 ????(1)
  • explicit operato 转换类型标识 ?(2)(C++11 起)
  • explicit (expression) perator 转换类型标识 ?(3)(C++20 起)
(1) 声明用户定义的转换函数,它参与所有隐式和显式转换。
(2) 声明用户定义的转换函数,它仅参与直接初始化和显式转换。
(3) 声明用户定义的转换函数,它为条件性explicit
转换类型标识是一个类型标识,但其声明器中不允许出现函数与数组运算符 []()(因此,转换到诸如数组的指针的类型要求使用类型别名、typedef或标识模板:见下文)。无论是否使用typedef,转换类型标识都不能代表数组或函数类型。
尽管用户定义转换函数的声明中不允许出现返回类型,声明的文法中的 声明说明符序列可以存在并可包含除了 类型说明符 或关键词 static 之外的任何说明符。尤其是,其不仅允许 explicit,亦允许说明符 inlinevirtualconstexpr (C++11 起)、consteval(C++20 起) 及 friend(注意 friend 要求有限定名:friend A::operator B(); )。
如果在类 X 中声明这种成员函数,则它进行从 X 到 转换类型标识 的转换:
struct X { // 隐式转换 operator int() const { return 7; } // 显式转换 explicit operator int*() const { return nullptr; } //错误:转换类型标识中不允许出现数组运算符 //operator int(*)[3]() const { return nullptr; } using arr_t = int[3]; operator arr_t*() const { return nullptr; } // 若通过 typedef 进行则 OK //operator arr_t () const; // 错误:不允许任何情况下转换到数组 }; int main() { X x; int n = static_cast(x); // OK:设 n 为 7 int m = x; // OK:设 m 为 7 int* p = static_cast(x); // OK:设 p 为 null //int* q = x; // 错误:无隐式转换 int (*pa)[3] = x; // OK }



4.引用 C++/C++语言/表达式/类型转换
类型转换和类型安全
Type conversions

    推荐阅读