C++类型转换

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

原文链接(英文):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;
}

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


explicitkeyword

在一个函数调用上,C++同意每一个參数的隐式转换。这对于类可能会有点问题,由于它并不总是期望的。比如,我们将以下的函数加到刚才的样例中:

void fn (B arg) {}

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

fn(foo);

这可能是不期望的。可是,不论什么情况下,可以通过在构造函数上使用explicitkeyword得到保护:

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

另外,标上explicitkeyword的构造函数不能使用赋值式的语句进行调用;这样的情况下的样例是,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有关,应与后文靠近。

若认为我翻译的有问题能够留言或邮件交流。

你可能感兴趣的:(类型转换)