C++中的四种类型转换

1 引子

这篇笔记是根据StackOverflow上面的一个问题整理而成,主要内容是对C/C++当中四种类型转换操作进行举例说明。在之前其实对它们都是有所了解的,而随着自己在进行总结,并敲了一些测试示例代码进行验证之后,对它们的理解又深刻了一些。

总所周知,在C++ 当中引入了四种新的类型转换操作符:static_cast, dynamic_cast, reinterpret_cast,还有const_cast。就自己见过的一些C++代码当中,它们的使用其实并不普遍。不少程序员依然乐于去使用C-like的类型转换,因为它强大且编写起来又简单。据说C-Like类型转换操作符的作用实际上已经包括了static_cast, const_cast和reinterpret_cast三种操作符,你相信吗?一起来着看。

注:上面提到的C-Like类型转换操作有如下的两种形式,这一点大家一定都不会陌生。

(new-type) expression
new-type (expression)

2 static_cast vs dynamic_cast

之所以把static_cast与dynamic_cast两兄弟放在一起是因为它们两者对比起来更容易记得住。首先,从名称上面它们就有语义相对的关系,一“静”一“动”。另外,在功能上面也在一定程度上体现了这一对比的特性,如dynamic_cast的Run-time Checkingt,static_cast在编译时增加的类型检测。简单而言:

static_cast: 1)完成基础数据类型,2)同一个继承体系中类型的转换
dynamic_cast:使用多态的场景,增加了一层对真实调用对象类型的检查

2.1 从C-Like到static_cast

static_cast对于基础类型如int, float, char以及基础类型对应指针的处理大多情况下恰如C-Like的转换一样,不过static_cast会来得更加安全。

char c = 10;      // 1 个字节
int *p = (int *)&c;  // 4 个字节(32bit platform)

*p = 5;        // 内存踩脏
int *q = static_cast<int *>(&c); // 使用static_cast可在编译阶段将该错误检查出来。

对于自定义类型的处理,相比C-Like而言,它也多了一层保护,也就是它不支持在不属于同一继承体系的类型之间进行转换。但是C-Like就可以办到,看下面这个例子:

#include <iostream>

class A
{
public:
 A(){}
 ~A(){}

private:
 int i, j;
};

class C
{
public:
 C(){}
 ~C(){}

 void printC()
 {
  std::cout <<"call printC() in class C" <<std::endl;
 }
private:
 char c1, c2;
};

int main()
{
 A *ptrA = new A;
 //C *ptrC = static_cast<C *>(ptrA);
 // 编译无法通过,提示:
 // In function ‘int main()':
 // error: invalid static_cast from type ‘A*' to type ‘C*'

 C *ptrC = (C *)(ptrA);
 ptrC->printC();
 // 编译正常通过。
 // 尽管这个时候能够正常调用printC,但实际上这种做法的结果是“undefined”
 // 尝试过,如果添加一些数据成员的运算,这个时候将会使得运算结果无法预测
 // 所以,在运行时候该逻辑相关的行为是不清晰的。

 return 0;
}

2.2 static_cast对于自定义类型的转换

上面这个小例子简单对比了static_cast与C-Like在针对不同继承体系的类之间表现的差异性,现在先把范围缩小到同一继承体系当中的类型转换。(注:这里所说的类型一般是针对类的指针或者类的引用)

static_cast针对同一继承体系的类之间的转换,它既可以进行upcast也可以进行downcast。一般来说,在进行upcast时是没有问题的,毕竟子类当中一定包含有父类的相关操作集合,所以通过转换之后的指针或者引用来操作对应的对象,其行为上是可以保证没问题。这和使用static_cast与使用C-Like或者直接隐式转换效果一样(当然,其结果是否符合程序员本身的预期与当时的设计有关系)。

需要注意的是,使用static_cast进行downcast应该避免,因为它可以顺利逃过编译器的法眼,但在运行时却会爆发未定义的问题:

#include <iostream>

class A
{
public:
 A():i(1), j(1){}
 ~A(){}

 void printA()
 {
  std::cout <<"call printA() in class A" <<std::endl;
 }

 void printSum()
 {
  std::cout <<"sum = " <<i+j <<std::endl;
 }

private:
 int i, j;
};

class B : public A
{
public:
 B():a(2), b(2) {}
 ~B(){}

 void printB()
 {
  std::cout <<"call printB() in class B" <<std::endl;
 }

 void printSum()
 {
  std::cout <<"sum = " <<a+b <<std::endl;
 }

 void Add()
 {
  a++;
  b++;
 }

private:
 double a, b;
};

int main()
{
 B *ptrB = new B;
 ptrB->printSum();
 //打印结果:sum = 4
 A *ptrA = static_cast<B *>(ptrB);
 ptrA->printA();
 ptrA->printSum();
 //打印结果:sum = 2
 //在进行upcast的时候,指针指向的对象的行为与指针的类型相关。

 ptrA = new A;
 ptrA->printSum();
 //打印结果:sum = 2
 ptrB = static_cast<B *>(ptrA);
 ptrB->printB();
 ptrB->printSum();
 //打印结果:sum = 0
 //在进行downcast的时候,其行为是“undefined”。

 //B b;
 //B &rB = b;
 //rB.printSum();
 //打印结果:sum = 4
 //A &rA = static_cast<A &>(rB);
 //rA.printA();
 //rA.printSum();
 //打印结果:sum = 2
 //在进行upcast的时候,引用指向的对象的行为与引用的类型相关。

 //A a;
 //A &rA = a;
 //rA.printSum();
 //打印结果:sum = 4
 //B &rB = static_cast<B &>(rA);
 //rB.printB();
 //rB.printSum();
 //打印结果:sum = 5.18629e-317
 //在进行downcast的时候,其行为是“undefined”。

 return 0;
}

如上,static_cast在对同一继承体系的类之间进行downcast时的表现,与C-Like针对分属不同继承体系的类之间进行转换时的表现一样,将是未定义的。所以,应该尽可能使用static_cast执行downcast转换,更准确的说,应该尽可能避免对集成体系的类对应的指针或者引用进行downcast转换。

既然这样,那是不是在软件开发过程当中就不会存在downcast的这种情况了呢?实际上不是的。一般来说,进行downcast的时候一般是在虚继承的场景当中,这个时候dynamic_cast就上场了。

2.3 dynamic_cast

dynamic_cast的使用主要在downcast的场景,它的使用需要满足两个条件:

downcast时转换的类之间存在着“虚继承”的关系
转换之后的类型与其指向的实际类型要相符合
dynamic_cast对于upcast与static_cast的效果是一样的,然而因为dynamic_cast依赖于RTTI,所以在性能上面相比static_cast略低。

#include <iostream>
#include <exception>

class A
{
public:
 virtual void print()
 {
  std::cout <<"Welcome to WorldA!" <<std::endl;
 }
};

class B : public A
{
public:
 B():a(0), b(0) {}
 ~B(){}
 virtual void print()
 {
  std::cout <<"Welcome to WorldB!" <<std::endl;
 }
private:
 double a, b;
};

int main()
{
 B *ptrB = new B;
 A *ptrA = dynamic_cast<A *>(ptrB);
 ptrA->print();
 //在虚继承当中,针对指针执行upcast时dynamic_cast转换的效果与static_cast一样
 //对是否存在virtual没有要求,会实际调用所指向对象的成员。

 //A *ptrA = new A;
 //B *ptrB = dynamic_cast<B *>(ptrA);
 //ptrB->print();
 //Segmentation fault,针对指针执行downcast时转换不成功,返回NULL。

 //A a;
 //A &ra = a;
 //B &b = dynamic_cast<B &>(ra);
 //b.print();
 //抛出St8bad_cast异常,针对引用执行downcast时转换不成功,抛出异常。

 //ptrA = new A;
 //ptrB = static_cast<B *>(ptrA);
 //ptrB->print();
 //使用static_cast进行downcast的时候,与dynamic_cast返回NULL不同,
 //这里会调用ptrB实际指向的对象的虚函数。

 //ptrA = new A;
 //ptrB = dynamic_cast<B *>(ptrA);
 //ptrB->print();
 //在进行downcast时,如果没有virtual成员,那么在编译时会提示:
 // In function ‘int main()':
 // cannot dynamic_cast ‘ptrA' (of type ‘class A*') to type ‘class B*' (source type is not polymorphic)

 return 0;
}

从这个例子可以看出,在虚继承场景下,能够使用dynamic_cast的地方一定可以使用static_cast,然而dynamic_cast却有着更严格的要求,以便帮助程序员编写出更加严谨的代码。只不过,它在性能上面多了一部分开销。

3 reinterpret_cast

reinterpret_cast是最危险的一种cast,之所以说它最危险,是因为它的表现和C-Like一般强大,稍微不注意就会出现错误。它一般在一些low-level的转换或者位操作当中运用。

#include <iostream>

class A
{
public:
 A(){}
 ~A(){}
 void print()
 {
  std::cout <<"Hello World!" <<std::endl;
 }
};

class B
{
public:
 B():a(0), b(0) {}
 ~B(){}

 void call()
 {
  std::cout <<"Happy for your call!" <<std::endl;
 }

private:
 double a, b;
};

int main()
{
 //A *ptrA = new A;
 //B *ptrB = reinterpret_cast<B *>(ptrA);
 //ptrB->call();
 //正常编译
 //A *ptrA = new A;
 //B *ptrB = (B *)(ptrA);
 //ptrB->call();
 //正常编译
 //A *ptrA = new A;
 //B *ptrB = static_cast<B *>(ptrA);
 //ptrB->call();
 //编译不通过,提示:
 //In function ‘int main()':
 //error: invalid static_cast from type ‘A*' to type ‘B*'

 //char c;
 //char *pC = &c;
 //int *pInt = static_cast<int *>(pC);
 //编译提示错误:error: invalid static_cast from type ‘char*' to type ‘int*'
 //int *pInt = reinterpret_cast<int *>(pC);
 //正常编译。
 //int *pInt = (int *)(pC);
 //正常编译。

 return 0;
}

分析了static_cast,dynamic_cast与reinterpret_cast之后就可以画出如下的图示对它们之间的区别进行简单比较了。这里没有将const_cast纳入进来是因为它比较特殊,另外分节对它进行介绍。

     ----------------
     /  dynamic_cast \ -->同一继承体系(virtual)的类指针或引用[更安全的downcast]
    ~~~~~~~~~~~~~~~~~~~~
    /   static_cast  \ -->基础类型[更安全],同一继承体系的类指针或引用
   ~~~~~~~~~~~~~~~~~~~~~~~~
   /  reinterpret_cast  \ -->与C-Like的作用一致,没有任何静态或者动态的checking机制
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  /     C-Like      \ -->基础类型,同一继承体系的类指针或引用,不同继承体系类的指针或引用
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

4 const_cast

const_cast能够使用来移出或者增加一个变量的const属性,最初的时候我觉得这个const_cast比较怪异,C里面一直都没有类似的东西来消除const属性,这里是否会多余呢?其实,我这种想法本身就没根没据。后来想想,在C++当中一直提倡将常量声明为const,这样一旦常量变得多了起来,在与其他软件组件或者第三方库进行衔接的时候就难免会碰到需要cast const属性的问题。比如:

const int myConst = 15;
int *nonConst = const_cast<int *>(&myConst);

void print(int *p)
{
  std::cout << *p;
}

print(&myConst); // 编译错误:error: invalid conversion from ‘const int*' to ‘int*'
print(nonConst); // 正常

不过,在使用const_cast的时候应该要注意,如果没有必要尽量不要去修改它的值:

const int myConst = 15;
int *nonConst = const_cast<int *>(&myConst);

*nonConst = 10;
// 如果该变量存放在read-only内存区当中,在运行时可能会出现错误。

5 小结

在C++当中对于大部分数据类型而言,使用C-Like的类型转换已经完全够用了。然而,不少人一直在倡导进行显式数据类型转换的时候尽可能地使用C++规定的类型转换操作。我想这里面大概有两方面的原因:

第一种,C++是一门“新”的编程语言,应该学会用它本身的思想来解决编程方面的问题;
第二种,尽管C-Like转换操作能力强大,但如果将其任意使用,会产生不少在编译期间隐藏,却在运行时候神出鬼没。这些问题使得软件的行为极不清晰。
如此,C++当中引出了其他四种类型转换方式,用来更加安全的完成一些场合的类型转换操作。比如使用reinterpret_cast的时候会表示你确定无疑的想使用C-Like的类型转换;在使用static_cast的时候想要确保转换的对象基本兼容,比如无法将char *转换为int *,无法在不同继承体系类的指针或引用之间进行转换;而使用dynamic_cast的时候是要对虚继承下的类执行downcast转换,并且已经明了当前性能已经不是主要的影响因素......

回答一下前文提到的问题。可以这么说,对于const_cast, static_cast, reinterpret_cast和dynamic_cast所能够完成的所有转换,C-Like也可以完成。但是,C-Like转换却没有static_cast, dynamic_cast分别提供的编译时类型检测和运行时类型检测。

C++之父Bjarne Stroustrup博士在这里也谈到了他的观点,主要有两点:其一,C-Like的cast极具破坏性并且在代码文本上也难得花不少力气搜索到它;其二,新式的cast使得程序员更有目的使用它们并且让编译器能够发现更多的错误;其三,新的cast符合模板声明规范,可以让程序员编写它们自己的cast。

(0)

相关推荐

  • C++编程中的数据类型和常量学习教程

    C++数据类型 计算机处理的对象是数据,而数据是以某种特定的形式存在的(例如整数.浮点数.字符等形式).不同的数据之间往往还存在某些联系(例如由若干个整数组成一个整数数组).数据结构指的是数据的组织形式.例如,数组就是一种数据结构.不同的计算机语言所允许使用的数据结构是不同的.处理同一类问题,如果数据结构不同,算法也会不同.例如,对10个整数排序和对包含10个元素的整型数组排序的算法是不同的. C++的数据包括常量与变量,常量与变量都具有类型.由以上这些数据类型还可以构成更复杂的数据结构.例如利

  • 浅谈C++的语句语法与强制数据类型转换

    一个程序包含一个或多个程序单位(每个程序单位构成一个程序文件).每一个程序单位由以下几个部分组成: 预处理命令.如#include命令和#define命令. 声明部分.例如对数据类型和函数的声明,以及对变量的定义. 函数.包括函数首部和函数体,在函数体中可以包含若干声明语句和执行语句. 如下面是一个完整的C++程序: #include <iostream>//预处理命令 using namespace std; //在函数之外的声明部分 int a=3; //在函数之外的声明部分 int ma

  • 深入解析C++中的动态类型转换与静态类型转换运算符

    dynamic_cast 运算符 将操作数 expression 转换成类型为type-id 的对象. 语法 dynamic_cast < type-id > ( expression ) 备注 type-id 必须是一个指针或引用到以前已定义的类类型的引用或"指向 void 的指针".如果 type-id 是指针,则expression 的类型必须是指针,如果 type-id 是引用,则为左值. 有关静态和动态强制转换之间区别的描述,以及各在什么情况下适合使用,请参见 s

  • C++运行时获取类型信息的type_info类与bad_typeid异常

    type_info 类 type_info 类描述编译器在程序中生成的类型信息.此类的对象可以有效存储指向类型的名称的指针. type_info 类还可存储适合比较两个类型是否相等或比较其排列顺序的编码值.类型的编码规则和排列顺序是未指定的,并且可能因程序而异. 必须包含 <typeinfo> 标头文件才能使用 type_info 类. type_info 类的接口是: class type_info { public: virtual ~type_info(); size_t hash_co

  • 一起聊聊C++中的四种类型转换符

    目录 一:背景 二:理解四大运算符 1. const_cast 2. reinterpret_cast 3. dynamic_cast 3. static_cast 一:背景 在玩 C 的时候,经常会用 void* 来指向一段内存地址开端,然后再将其强转成尺度更小的 char* 或 int* 来丈量一段内存,参考如下代码: int main() { void* ptr = malloc(sizeof(int) * 10); int* int_ptr = (int*)ptr; char* char

  • C++中的四种类型转换

    1 引子 这篇笔记是根据StackOverflow上面的一个问题整理而成,主要内容是对C/C++当中四种类型转换操作进行举例说明.在之前其实对它们都是有所了解的,而随着自己在进行总结,并敲了一些测试示例代码进行验证之后,对它们的理解又深刻了一些. 总所周知,在C++ 当中引入了四种新的类型转换操作符:static_cast, dynamic_cast, reinterpret_cast,还有const_cast.就自己见过的一些C++代码当中,它们的使用其实并不普遍.不少程序员依然乐于去使用C-

  • 一文搞懂C++中的四种强制类型转换

    在了解c++的强制类形转换的时候,先看看在c语言中是怎么进行强制类形转换的. C语言中的强制类形转换分为两种 隐式类型转换 显示类型转换 int main() { int a = 97; char ch = a; // 隐式类型转换 int b = (int)ch; // 显示类型转换 cout << "a = " << a << endl; cout << "ch = " << ch << e

  • 详解C++中常用的四种类型转换方式

    目录 1.静态类型转换:static_cast(exp) 2.动态类型转换:dynamic_cast(exp) 3.常类型转换:const_case(exp) 4. 解释类型转换: reinterpret_cast(exp) 1.静态类型转换:static_cast(exp) 1.1静态类型转换主要用于两种转换环境 1.1.1 C++内置类型的转换:与C风格强转类似. 与c相同的地方: #include <iostream> using namespace std; int main() {

  • C++实例讲解四种类型转换的使用

    目录 C++类型转换 C语言风格的转换 C++风格的类型转换 static_cast reinterpret_cast const_cast dynamic_cast 小结 C++类型转换 C语言风格的转换 C语言提供了自己的一套转换规则,有好处也有坏处. C语言的风格:(type_name)expression; C语言提供了隐式类型转换和显式类型转换.显式类型转换一般也叫做强转,隐式类型转换编译器完成,如果转换不了就报错. 而C语言类型转换的风格好处就是简单,缺陷比如转换的可视性差,显式类型

  • 浅谈Java中的四种引用方式的区别

    强引用.软引用.弱引用.虚引用的概念 强引用(StrongReference) 强引用就是指在程序代码之中普遍存在的,比如下面这段代码中的object和str都是强引用: Object object = new Object(); String str = "hello"; 只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象. 比如下面这段代码: public class Main { publi

  • 深入理解在JS中通过四种设置事件处理程序的方法

    所有的JavaScript事件处理程序的作用域是在其定义时的作用域而非调用时的作用域中执行,并且它们能存取那个作用域中的任何一个本地变量.但是HTML标签属性注册处理程序就是一个例外.看下面四种方式: 第一种方式(HTML标签属性): <input type="button" id="btn1" value="测试" onclick="alert(this.id);" /> 上面的代码是通过设置HTML标签属性为给

  • Javascript技术栈中的四种依赖注入详解

    作为面向对象编程中实现控制反转(Inversion of Control,下文称IoC)最常见的技术手段之一,依赖注入(Dependency Injection,下文称DI)可谓在OOP编程中大行其道经久不衰.比如在J2EE中,就有大名鼎鼎的执牛耳者Spring.Javascript社区中自然也不乏一些积极的尝试,广为人知的AngularJS很大程度上就是基于DI实现的.遗憾的是,作为一款缺少反射机制.不支持Annotation语法的动态语言,Javascript长期以来都没有属于自己的Spri

  • 详解java中的四种代码块

    在java中用{}括起来的称为代码块,代码块可分为以下四种: 一.简介 1.普通代码块: 类中方法的方法体 2.构造代码块: 构造块会在创建对象时被调用,每次创建时都会被调用,优先于类构造函数执行. 3.静态代码块: 用static{}包裹起来的代码片段,只会执行一次.静态代码块优先于构造块执行. 4.同步代码块: 使用synchronized(){}包裹起来的代码块,在多线程环境下,对共享数据的读写操作是需要互斥进行的,否则会导致数据的不一致性.同步代码块需要写在方法中. 二.静态代码块和构造

  • 详解Python中的四种队列

    队列是一种只允许在一端进行插入操作,而在另一端进行删除操作的线性表. 在Python文档中搜索队列(queue)会发现,Python标准库中包含了四种队列,分别是queue.Queue / asyncio.Queue / multiprocessing.Queue / collections.deque. collections.deque deque是双端队列(double-ended queue)的缩写,由于两端都能编辑,deque既可以用来实现栈(stack)也可以用来实现队列(queue

随机推荐