【cpluplus教程翻译】类型转换(Type conversions)

发布时间 2023-06-04 19:01:10作者: xiaoweing

隐式类型转换(implicit conversion)

如果一个值被拷贝到另一个兼容类型中,隐式类型转换会自动执行(注意对象 指针 引用的区别)。比如

short a=2000;
int b;
b=a;

a的值从short被提升到int,这个过程不需要显式的转换,这被称为标准转换(standard conversion),标准转换针对的是基础数据类型,数值类型之间可以进行转换(short to int,int to float, double to int),bool及指针类型也可以。
从更小的整型转到int或者从float转到double被称为提升(promotion),这个过程保证目的值一样。大转小则不一定能保持相同的值。
如果负数被转成无符号类型,结果值是补码表示,比如(-1表示最大值)
对于bool类型,0认为是false,其他值都认为是true
如果浮点转成整数,值会被截断,在表示范围外的值,行为是未定义的
如果是浮点间或者整数间,值是和实现有关
转换可能会损失一些精度,编译器会对此提出warning,显式类型转换可以避免这些warning(static_cast)
对于非基础类型,数组和函数会隐式转换成指针,指针可以进行如下转换:
1 null指针可以转成其他任意类型的null指针
2 任意类型的指针可以转换成void指针
3 指针上提:派生类的指针可以转成基类的指针,这个过程不能修改const和volatile属性

类的隐式类型转换

在类的世界,隐式类型转换可以被三个成员函数控制
1 单个参数的构造函数:运行用另一个类型来初始对象
2 赋值操作符:赋值过程中的隐式转换
3 类型转换操作符

// implicit conversion of classes:
#include <iostream>
using namespace std;

class A {};

class B {
public:
  // conversion from A (constructor):
  B (const A& x) {}
  // conversion from A (assignment):
  B& operator= (const A& x) {return *this;}
  // conversion to A (type-cast operator)
  operator A() {return A();}
};

int main ()
{
  A foo;
  B bar = foo;    // calls constructor
  bar = foo;      // calls assignment
  foo = bar;      // calls type-cast operator
  return 0;
}

注意类型转换操作符的语法:返回值类型A在括号的前面,operator前不用再加返回值类型

关键字explicit

在函数调用中,C++允许每个参数发生一次隐式类型转换,这个对类来说有些时候可能会产生问题,因为这是类的设计者不想产生的行为(类的构造函数很重要,不想没事就调用),如果有如下代码:void fn (B arg) {},这个函数的参数是类型B,但是如果实参类型是A,比如fn (foo);,这个可能是代码不小心写错了,编译器可以对这种情况进行检查

// explicit:
#include <iostream>
using namespace std;

class A {};

class B {
public:
  explicit B (const A& x) {}
  B& operator= (const A& x) {return *this;}
  operator A() {return A();}
};

void fn (B x) {}

int main ()
{
  A foo;
  B bar (foo);
  bar = foo;
  foo = bar;
  
//  fn (foo);  // not allowed for explicit ctor.
  fn (bar);  

  return 0;
}

注意某些赋值运算符其实也是构造函数,那些情况也不能用B bar = foo;
类型转换沟站函数也可以用explicit修饰,也可以避免无意识的转换

类型转换(Type casting)

C++是强类型语言,转换需要显式进行,特别是那些对值进行不同的理解,这个被称为类型转换,有两种语法,函数式和C风格

double x = 10.3;
int y;
y = int (x);    // functional notation
y = (int) x;    // c-like cast notation

这些类型转换的通用形式的功能足以满足基本数据类型的大多数需求。然而,这些运算符可以不加区分地应用于类和指向类的指针,这可能导致代码在语法正确的情况下会导致运行时错误。例如,以下代码编译时没有出现错误:

// class type-casting
#include <iostream>
using namespace std;

class Dummy {
    double i,j;
};

class Addition {
    int x,y;
  public:
    Addition (int a, int b) { x=a; y=b; }
    int result() { return x+y;}
};

int main () {
  Dummy d;
  Addition * padd;
  padd = (Addition*) &d;
  cout << padd->result();
  return 0;
}

程序声明了一个Addition指针,但是通过类型转换,把一个不相关对象的地址赋值过去
这种没有限制的显式类型转换,可以把任意类型的指针转成其他任意指针类型,不管指针指向什么类型,所以后续调用成员函数可能会导致运行时错误或其他不可预料的结果
为了控制类之间的类型转换,提供了四个类型转换操作符:dynamic_cast, reinterpret_cast, static_cast and const_cast。语法格式如下

dynamic_cast <new_type> (expression)
reinterpret_cast <new_type> (expression)
static_cast <new_type> (expression)
const_cast <new_type> (expression)

这个和类型转换表达式一样,不过特点差不多

dynamic_cast

dynamic_cast只能用来转换指针和引用(void*的规则不变),只能用于有虚函数类,可以运行时动态检查对象是否完整
这自然包括指针上转换(从指针到派生到指针到基的转换),与隐式转换相同。
但是dynamic_cast也可以向下转换(从指向基的指针转换为指向派生的指针)多态类(那些具有虚拟成员的类),如果并且仅当指向的对象是目标类型的有效完整对象。例如:

// dynamic_cast
#include <iostream>
#include <exception>
using namespace std;

class Base { virtual void dummy() {} };
class Derived: public Base { int a; };

int main () {
  try {
    Base * pba = new Derived;
    Base * pbb = new Base;
    Derived * pd;

    pd = dynamic_cast<Derived*>(pba);
    if (pd==0) cout << "Null pointer on first type-cast.\n";

    pd = dynamic_cast<Derived*>(pbb);
    if (pd==0) cout << "Null pointer on second type-cast.\n";

  } catch (exception& e) {cout << "Exception: " << e.what();}
  return 0;
}

这项特性依赖RTTI,可能需要编译的时候打开对应的编译选项
两个指针的初始化方式不同,这也导致两次转换的结果不同
虽然指针类型相同,但是一个指针指向派生类对象,另一个是基类对象。
如果是转换指针不成功的话,会返回null指针,如果是转引用,会抛出异常
dynamic_cast还可以对指针执行其他允许的隐式强制转换:在指针类型之间(甚至在不相关的类之间)强制转换null指针,以及将任何类型的任何指针强制转换为void*指针。

static_cast

static_cast可以在指向相关类的指针之间执行转换,不仅可以进行上转换(从指针到派生到指针到基),还可以进行下转换(从指向基到指针到派生)。运行时不会执行任何检查,以确保转换的对象实际上是目标类型的完整对象。因此,由程序员来确保转换是安全的。另一方面,它不会引起dynamic_cast的类型安全检查的开销。

class Base {};
class Derived: public Base {};
Base * a = new Base;
Derived * b = static_cast<Derived*>(a);

上面的代码是有效的,尽管解引用b可能导致运行错误
因此static_cast不仅可以用在隐式转换允许的情况下,隐式转换规则的相反情况也可以进行
static_cast还能够执行隐式允许的所有转换(不仅仅是那些具有指向类的指针的转换),并且还能够执行与这些相反的转换。它可以:
把void*转成其他任意类型,这种情况下,只能保证如果指向的对象确实是用对应的对象,那么是可以用的
把整数浮点转成enum
除此之外static_cast还可以
1 显式调用构造函数或转换操作符
2 转成右值引用
3 把enum转成整数或浮点数
4 把任何类型转成void,不管值的类型

reinterpret_cast

reinterpret_cast类似最开始提到的强转,只能用于指针类型,即使是完全不相关的类,操作的结果只是简单的二进制拷贝,所有的转换都允许:指针指向的内容和指针的类型都不进行检查
也可以进行整数和指针类型间的转换,这个和平台有关,32位平台和64位平台的指针大小是不一样的,所以需要足够大,比如intptr_t
reinterpret_cast能做但是static_cast不能做的操作是比较底层的转换,基于值的二进制表示,大多数这种代码都是系统相关因此可移植性较差

class A { /* ... */ };
class B { /* ... */ };
A * a = new A;
B * b = reinterpret_cast<B*>(a);

上面的代码可以编过,但没有太大意义,解引用会有问题

const_cast

const_cast用于设置或移除指针指向对象的常属性

// const_cast
#include <iostream>
using namespace std;

void print (char * str)
{
  cout << str << '\n';
}

int main () {
  const char * c = "sample text";
  print ( const_cast<char *> (c) );
  return 0;
}

因为print函数不会修改对象,所以上面的代码没有问题,但是如果真的去修改常对象,会导致未定义行为

typeid

typeid可以检查一个表达式的类型(auto的底层原理?
typeid会返回常对象,类定义在头文件,typeid返回值之间可以判等或者不等,name可以返回类型名,具体怎么命名和平台有关

// typeid
#include <iostream>
#include <typeinfo>
using namespace std;

int main () {
  int * a,b;
  a=0; b=0;
  if (typeid(a) != typeid(b))
  {
    cout << "a and b are of different types:\n";
    cout << "a is: " << typeid(a).name() << '\n';
    cout << "b is: " << typeid(b).name() << '\n';
  }
  return 0;
}

typeid也可以用于类,机制和RTTI一致,如果typeid作用的是多态类,结果是派生最完整的类

// typeid, polymorphic class
#include <iostream>
#include <typeinfo>
#include <exception>
using namespace std;

class Base { virtual void f(){} };
class Derived : public Base {};

int main () {
  try {
    Base* a = new Base;
    Base* b = new Derived;
    cout << "a is: " << typeid(a).name() << '\n';
    cout << "b is: " << typeid(b).name() << '\n';
    cout << "*a is: " << typeid(*a).name() << '\n';
    cout << "*b is: " << typeid(*b).name() << '\n';
  } catch (exception& e) { cout << "Exception: " << e.what() << '\n'; }
  return 0;
}

注意指针类型是指针本身,如果null的话,是bad_typeid异常

总结

注意每个cast能支持的类型,dynamic_cast只能作用于指针或者引用,static_cast既可以用指针也可以用于对象,reinterpret_cast只能用于指针,const_cast也只能用于指针。
场景也不同:dynamic_cast用于多态类,运行时检查对象是否完整,static_cast可以执行隐式规则相反的操作,reinterpret_cast则是无脑强转,const_cast用于常量
核心是隐式类型转换的规则:
基础类型:int float unsigned int bool都可以互相转,注意范围和正负号
非基础类型:null指针可以转换为其他类型的null指针
任意类型都可以转void
派生类指针可以转基类