C++类型转换

一篇来自cplusplus.com的文章,这是我所看过的关于C++类型转换的最全面、最细致、最深入的一篇文章。本文介绍了C++的各种类型转换,具体包括:基本类型的隐式类型转换,C风格的类型转换,类的隐式转换(implicit conversion),explicit关键字,static_cast, reintperet_cast, const_cast, dynamic_cast。 以及和RTTI相关的typeid关键字。

原文链接(英文):http://www.cplusplus.com/doc/tutorial/typecasting/


隐式类型转换

(ps:首先是内置类型之间的类型转换)

当一个值被拷贝到一个可与之兼容的类型时,隐式转化自动发生。例如:

short a = 2000;
int b;
b = a;
这里,a的值从short提升到了int不需要任何显式的运算符,这被称作标准转换(standard conversion)。标准转换作用于基本数据类型,允许数值类型之间的转换(short 到int, int 到float,double到int...), 转或从bool,以及一些指针转换。

从较小的整型类型转到int,或者从float转到double被称作提升(promotion),在目标类型中产生绝对相同的值能够得到保证。其他算术类型之间的转型不一定能够表示绝对相同的值:

  • 如果从一个负整数(PS:有符号)转换到无符号类型,得到的值对应于它的二进制补数表示
  • 从(到)bool值转型,false将等价于0(对于数值类型),或者空指针(null pointer)(对于指针类型);所有其他值等价于true, 且会被转换为1
  • 如果是从浮点型到整型的转换,值将被截断(小数部分将会丢失)。如果结果超出了(目标)类型的表示范围,将会导致未定义的行为(undefined behavior)
  • 另外,如果是相同类型的数值类型之间的转换(整型到整型,或浮点到浮点),转型是合法的,但值是实现定义的(implementation-specific,PS:编译器实现的),且可能会导致不可移植.(ps: 没人会这么干)

以上这些转型有可能导致精度丢失,这些情况编译器能够产生警告。这些警告可以通过显式转换避免。

对于非基本类型,数值和函数能隐式转换为指针,而指针通常允许如下转换:

  • NULL可以转到任意类型的指针
  • 任意类型的指针可以转换void*
  • 指针向上转型:派生类指针可以转换到一个能够访问的(accessible)且非不明确的(unambiguous)基类指针,不会丢失它的const或volatile限定。

类间隐式转换

在类的世界里,隐式转换可以通过以下三种成员函数控制:

  • 单参数构造函数(Single-argument constructors): 允许从一个已知类型的隐式转换,用来初始化一个对象
  • 赋值运算符(Assignment operator): 允许从一个已知类型的隐式转换,用来赋值
  • 类型转换运算符(Type-cast opertor, PS:也叫operator type): 允许转到一个已知类型

例如:

// 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;
}

类型转换运算符用一个已知的语句:用一个opertor关键字跟目标类型和一个空的圆括号。记住:返回值类型是目标类型,且它并没有在operator关键字的前面指出。(PS:一般的运算符重载成员函数,返回值类型是写在operator前面的)


explicit关键字

在一个函数调用上,C++允许每个参数的隐式转换。这对于类可能会有点问题,因为它并不总是期望的。例如,我们将下面的函数加到刚才的例子中:

void fn (B arg) {}

这个函数有个类型为B的参数,但它也能以一个A类型的参数进行调用:

fn(foo);

这可能是不期望的。但是,任何情况下,能够通过在构造函数上使用explicit关键字得到保护:

/ 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;
}

另外,标上explicit关键字的构造函数不能使用赋值式的语句进行调用;这种情况下的例子是,bar不能想下面这样构造:

B bar = foo;

类型转换成员函数(前面章节以及描述)也可以指定为explicit. 这同样能够像保护explicit修饰的ctor那样保护到目标类型的转换。


类型转换

C++是一种强类型的语言。多数转型,特别是那些用在值的表示形式不同的(类型之间的转换),需要显式转换,在C++中称作类型转换(type-casting)。有两种通用的用于类型转换的语句:函数式的(functional)和C风格的(c-like):

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的指针,但随后被用显式转换赋值为一个不相关的类型:

padd = (Addition*) &d;
没有限制的显示类型转换允许任意指针转为其他类型的指针, 独立于她们实际指向的类型。接下来的成员函数调用将会导致一个运行时错误或者一些不希望的结果。


新式转型运算符

为了控制这种类之间的转型,我们有四种特定的类型转换运算符:dynamic_cast, reinterpret_cast, static_cast, 和 const_cast. 它们的格式是新类型被尖括号(<>)括起来,跟在后面的是括号括起来的要转型的表达式。

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

传统的类型转换表达式的是:

(new_type) expression
new_type (expression)

但是它们每个都有特点:

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不但能够进行隐式的转型(PS: 如upcast, 整型转换),而且能够进行相反的转型(PS: 如downcast,但是最好别这么干!)。

static_cast能够进行所有的隐式转型允许的转型(不止是例子中的指向类的指针之间),而且能够进行和它相反的转型(PS: 这句重复了!)。它可以:

  • 从void*转换到任意类型的指针。这种情况下,它会保证如果void*值能够被目标指针值容纳,结果指针值将会是相同的
  • 转换整型,浮点值,以及枚举类型到枚举类型

另外,static_cast能够进行下面的:

  • 明确地调用单参数的构造函数(single-argument constructor)或一个转型运算符(conversion operator)
  • 转换到右值引用(rvalue reference, PS: C++11)
  • 转换枚举值到整型或浮点值
  • 转换任何类型到void,计算并忽略这个值

(PS: static_cast可以替换绝大多数的C风格的基本类型转换)


reinterpret_cast

reinterpret_cast转换任意指针类型到其他任意指针类型,即使是不相关的类。这种操作的结果是只简单的二进制的将值从一个指针拷贝到另一个。所有的指针转型都是允许的:它本身的指针类型和所指内容都不会被检查。

它同样可以转换指针类型到整型类型,或从整型转换到指针。这个整型值所表示的指针的格式是平台相关的(platform-specific, PS:可能和大小段有关)。仅有的保证是一个指针转换到一个整型是足够完全容纳它的(比如 intptr_t), 保证能够转换回一个合法的指针。

能够使用reinterpret_cast却不能用static_cast的转型是基于重解释(reinterperting)类型的二进制表示的低级操作,这多数情况下,代码的结果是系统相关(system-sepcific),而且不可移植(non-portable)。例如:

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

这段代码编译起来并没有多少感觉,但是从此以后,b指向了一个完全不相关的且不是完整的B类的对象a,解引用b是不安全的。

(PS: reinterpret_cast可以替换多数C风格的指针转换)


const_cast

这种类型的转换用来操作指针所指对象的const属性,包括设置和去除。例如,传递一个const指针到一个期望non-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;
}
程序输出:sampel text

这个例子保证能够运行是因为函数只是打印,并没有修改它所指的对象。同样记住:去除所指对象的const属性,再修改它会导致未定义的行为(undefined behavior, PS: 标准没有定义的行为)。

dynamic_cast

dynamic_cast只能用于类的指针和引用(或者是void*). 它的目的是保证转型的结果指向一个目标类型的可用的完整对象。

这自然包括指针的向上转型(pointer upcast, 从派生类指针转向基类指针), 同样的方法也同样允许隐式转换。

但dynamic_cast还可以向下转型(downcast, 从基类指针转向派生类指针)多态的类(具有virtual成员的),当且仅当,所指对象是一个合法的完整的目标类型对象(转型才会成功)。例如:

/ 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;
}
程序输出:Null pointer on second type-cast.

通用性提示:这种类型的dynamic_cast需要运行时类型识别(Run-Time Type Information)以保持对动态类型的跟踪。一些编译器支持这个特性,但默认却是关闭的。这时,需要开启运行时类型检查才能使dynamic_cast运行良好。

接下来的代码,试图进行两次从Base*类型的指针到Derived*类型的指针的转型,但只有第一次是成功的。看清它们各自的初始化:

Base * pba = new Derived;
Base * pbb = new Base;

尽管两个指针都是Base*类型的指针,pba实际指向的对象的类型是Derived,而pbb指向一个Base类型的对象。因此,当它们分别用dynamic_cast进行类型转换时,pba指向的是一个完整的Derived类的对象,而pbb指向的是一个Base类型的对象,不是一个完整的Derived类型。

当dynamic_cast(表达式内)指针不是一个完整的所需对象类型时(上例中的第二次转型),它将得到一个空指针(null pointer)以表示转型失败(PS: 这是dynamic_cast的独特之处)。如果dynamic_cast用来转换一个引用类型,且转换是不可能的,相应的将会抛出bad_cast异常。

dynamic_cast还允许进行指针的其他隐式转换:指针类型之间转换空指针(即使是不相关的类),转换任意指针类型到void*指针。(PS:不太常用)

(PS: dynamic_cast的RTTI能力是C-like做不到的,它的downcast作用相当于java的isinstanceof)


typeid

typeid允许用来检查一个表达式的类型:

typeid (expression)

这个操作符将返回一个定义在标准头文件<typeinfo>中的type_info类型的常量对象(constant object)的引用。一个typeid返回的值可以用==运算符和!=运算符与另一个typeid返回的值进行比较,或者可以通过用它的name()成员得到一个字符串(null-terminated character sequence)表示他的数据类型或类名。

// 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;
}
程序输出:

a and b are of different types:
a is: int *
b is: int  

当typeid被用在类上,typeid使用RTTI来跟踪动态类型对象(PS: 有virtual function的)。当typeid被用于一个多态class类型的表达式,结果将是完整的派生对象:

// 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;
}
程序输出:

a is: class Base *
b is: class Base *
*a is: class Base
*b is: class Derived

记住:type_info的name成员所返回的字符串是依赖于你的编译器和库实现的。并不一定是典型的类名,例如编译器将会生成它特有的输出(PS: gcc 就不会生成上面这么好看的类型名)。

记住typeid用于指针考虑的类型是指针的类型(a, b的类型都是Base*)。然而,当typeid被用在对象上(如*a 和*b),typeid产生它们的动态类型(例如,它们最终派生的完整对象)。

如果typeid评估的类型是一个指针解引用计算得到的值,且这个指针指向一个空值,typeid将会抛出一个bad_typeid异常。(PS: 由此可见,使用dynamic_cast和typeid的代码,需要考虑异常安全的问题)


后记

一些C++概念性的名词记得不是很清楚了,都给了原文,互相对照,读者自行斟酌。另外,我调整了dynamic_cast在文中的排序,原因有二。一是static_cast和interpret_cast和C-like转型的作用类似应与前文靠近。二是dynamic_cast和typeid都和RTTI有关,应当靠近。

若觉得我翻译的有问题可以留言或邮件交流。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。