C++类型转换


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

原文链接(英文):Type conversions - C++ Tutorials

隐式类型转换

(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语言类型转换 

类型转换有c 风格的,当然还有 c++风格的。c 风格的转换的格式很简单(TYPE) EXPRESSION,但是 c 风格的类型转换有不少的缺点,有的时候用 c 风格的转换是不合 适的,因为它可以在任意类型之间转换,比如你可以把一个指向 const 对象的指针转换 成指向非 const 对象的指针,把一个指向基类对象的指针转换成指向一个派生类对象的 指针,这两种转换之间的差别是巨大的,但是传统的 c 语言风格的类型转换没有区分这 些。还有一个缺点就是,c 风格的转换不容易查找,他由一个括号加上一个标识符组成, 而这样的东西在 c++程序里一大堆。所以 c++为了克服这些缺点,引进了 4 新的类型转换操作符。

//c语言类型转换
void testCast()
{
	//隐式类型转换
 
	//内置类型
	int i = 10;
	char c = i;
 
	
	//不支持转换,类型差别过大,但可以强转
	int *pi = &i;
	int i2 = pi;
 
	//强制转换,但像下面的这样没有意义
	int *pi = &i;
	int i2 = (int)pi;
}

缺陷: 转换的可视性比较差,所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换

  • c++提供了四种类型转换,分别适用于其他场景

为什么C++需要四种类型转换

  • 因为从上面可以看出C风格的转换格式很简单,但是有不少缺点的:为什么C++需要四种类型转换
  • 隐式类型转化有些情况下可能会出问题
  • 显式类型转换将所有情况混合在一起,代码不够清晰

C++类型转换


标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:static_cast、reinterpret_cast、const_cast、dynamic_cast。

1. static_cast 静态类型转换

  • static_cast<目标类型>(标识符)
  • 所谓的静态,即在编译期内即可决定其类型的转换,用的也是最多的一种。

      static_cast用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用static_cast,但它不能用于两个不相关的类型进行转换。 

1、static_cast 作用和C语言风格强制转换的效果基本一样,由于没有运行时类型检查来保证转换的安全性,所以这类型的强制转换和C语言风格的强制转换都有安全隐患。 
2、用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。注意:进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
3、用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性需要开发者来维护。 

4、static_cast不能转换掉原有类型的const、volatile、或者 __unaligned属性。(前两种可以使用const_cast 来去除)                     

5、在c++ primer 中说道:c++ 的任何的隐式转换都是使用 static_cast 来实现。


#include <iostream>
using namespace std;  

void testCast2()
{
	int i = 10 ;
	//静态转换:static_cast
	//支持任何隐式类型转换  但它不支持两个不相关的类型进行强制转换
	char c = static_cast<char>(i); 
    int *pi = &i;
	int i3 = static_cast<int> (pi);//报错  不支持两个不相关的类型进行强制转换
}


int main(void)
{
	double dPi = 3.1415926;
	int num1 = (int)dPi;    //c语言的旧式类型转换
	int num2 = dPi;         //隐式类型转换
	// 静态的类型转换:   
	// 在编译的时 进⾏行基本类型的转换 能替代c⻛风格的类型转换 可以进⾏行⼀一部分检查     
	int num3 = static_cast<int> (dPi); //c++的新式的类型转换运算符   
	cout << "num1:" << num1 << " num2:" << num2 << " num3:" << num3 << endl;
	return 0;
}

 2. dynamic_cast 子类与父类之间的多态类型准换

  • dynamic_cast<目标类型>(标识符)
  • 用于多态中父子类之间的多态转换 【 将一个父类对象的指针转换为子类对象的指针或引用(动态转换) 】

dynamic_cast强制转换,应该是这四种中最特殊的一个,因为他涉及到面向对象的多态性和程序运行时的状态,也与编译器的属性设置有关.所以不能完全使用C语言的强制转换替代,它也是最常有用的,最不可缺少的一种强制转换.

  1. 向上转型:子类对象指针->父类指针/引用(不需要转换,赋值兼容规则)
  2. 向下转型:父类对象指针->子类指针/引用(用dynamic_cast转型是安全的)

注意: 1. dynamic_cast只能用于含有虚函数的类 2. dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回0。

例 1:

#include<iostream>

class Animal {
public:
	virtual void cry() = 0;
};

class Dog : public Animal
{
public:
	virtual void cry()
	{
		std::cout << "旺旺" << std::endl;
	}
	void dohome()
	{
		std::cout << "看家" << std::endl;
	}
};

class Cat : public Animal
{
public:
	virtual void cry()
	{
		std::cout << "喵喵" << std::endl;
	}
	void dohome()
	{
		std::cout << "抓老鼠" << std::endl;
	}
};

int main()
{
	Animal* base = NULL;
	base = new Cat();
	base->cry();

	//⽤用于将⽗父类指针转换成⼦子类,
	Dog	*pDog = dynamic_cast<Dog*>(base); //此时转换时失败的,因为父类指针现在指向的对象是猫,所以转换狗是失败的。
	                                      //转换失败返回空(NULL)
	if (pDog != NULL)
	{
		pDog->cry();
		pDog->dohome();
	}

	Cat* pCat = dynamic_cast<Cat*>(base); //此时转换成功,成功将父类指针转换成子类指针
	if (pCat != NULL)
	{
		pCat->cry();
		pCat->dohome();
	}

	system("pause");
	return 0;
}

例2:

class A
{
public:
	virtual void fun()
	{
		cout << "A: fun()" << endl;
	}
};
 
class B:public A
{
public:
	virtual void fun()
	{
		cout << "B: fun()" << endl;
	}
};
void testCast4()
{
	B b;
	//切片  子类---》父类
	A a = b;
	A& rb = b;
	A* pb = &b;
	
	A aobj;
	B* pa = (B*)&aobj;//父类-》子类 需要强转
	B* pa2 = reinterpret_cast<B*> (&aobj);//可以强转但是不安全,容易访问越界。
 
	B* pa3 = dynamic_cast<B*> (&aobj);
	//static_cast没有类型安全检查,解引用可能会有问题。
	B* pa4 = static_cast<B*> (&aobj);//可以转换,没有语法错误,但是没有类型安全检查
 
	cout << pa << endl; //0032FA30 pa和pa2地址一样
	cout << pa2 << endl;//0032FA30
	cout << pa3 << endl;//不安全 00000000
 
	B* pb1 = dynamic_cast<B*> (pb);//pb: 指向子类对象的父类指针 因此可以转换是安全的
}

 例3:

对于从子类到基类的指针转换 ,dynamic_cast 成功转换,没有什么运行异常,且达到预期结果,

从基类到子类的转换 , dynamic_cast 在转换时也没有报错,但是输出给 base2sub 是一个 nullptr ,说明dynami_cast 在程序运行时对类型转换对“运行期类型识别”(Runtime type identification,RTTI)进行了检查. 

这个检查主要来自虚函数(virtual function) ,在C++的面对对象思想中,虚函数起到了很关键的作用,当一个类中拥有至少一个虚函数,那么编译器就会构建出一个虚函数表(virtual method table)来指示这些函数的地址,假如继承该类的子类定义并实现了一个同名并具有同样函数签名(function siguature)的方法重写了基类中的方法,那么虚函数表会将该函数指向新的地址。此时多态性就体现出来了:当我们将基类的指针或引用指向子类的对象的时候,调用方法时,就会顺着虚函数表找到对应子类的方法而非基类的方法。因此注意下代码中 Base 和 Sub 都有声明定义的一个虚函数 ” i_am_virtual_foo” ,我这份代码的 Base 和 Sub 使用 dynami_cast 转换时检查的运行期类型信息,可以说就是这个虚函数。

 dynamic_cast 测试非多态 

//非多态不可用
class C
{};
 
class D :public C
{};
 
void testCast5()
{
	//非多态不可使用 D 和 C没构成多态场景
	C c;
	D* pc = dynamic_cast<D*> (&c);
}

3. const_cast 去掉const属性转换

  • const_cast<目标类型>(标识符):目标类型只能是指针或者引用
#include<iostream>

class A {
public:
	int data;
};

int main()
{
	const A a = { 200 };
	A a1 = const_cast<A>(a);    //错误,const_cast 目标类型只能是引用或者指针
	a1.data = 100;

	A& a2 = const_cast<A&>(a);
	a2.data = 100;
	std::cout << a.data << ' ' << a2.data << std::endl;

	A* a3 = const_cast<A*>(&a);
	a3->data = 100;
	std::cout << a.data << ' ' << a3->data << std::endl;

	const int x = 3;

	int& x1 = const_cast<int&>(x);
	x1 = 200;
	std::cout << x << ' ' << x1 << std::endl;

	int* x2 = const_cast<int*>(&x);
	*x2 = 300;
	std::cout << x << ' ' << *x2 << std::endl;

	system("pause");
	return 0;
}

4. reinterpret_cast 重新解释类型转换

  • reinterpret_cast<目标类型>(表达式)
  • 数据的二进制重新解释,但是不改变其值(通过重新解释底层位模式在类型间转换)。 

解释:

        与 static_cast 不同,但与 const_cast 类似,reinterpret_cast 表达式不会编译成任何 CPU 指令(除非在整数和指针间转换,或在指针表示依赖其类型的不明架构上)。它纯粹是一个编译时指令,指示编译器将 表达式 视为如同具有 目标类型 类型一样处理。

唯有下列转换能用 reinterpret_cast 进行,但若转换会转型走常量性易变性则亦不允许。

1) 整型、枚举、指针或成员指针类型的表达式可转换到其自身的类型。产生的值与 表达式 的相同。(C++11 起)

2) 指针能转换成大小足以保有其类型所有值的任何整型类型(例如转换成 std::uintptr_t

3) 任何整型或枚举类型的值可转换到指针类型。指针转换到有足够大小的整数再转换回同一指针类型后,保证拥有其原值,否则结果指针无法安全地解引用(不保证相反方向的往返转换;相同指针可拥有多种整数表示)。不保证空指针常量 NULL 或整数零生成目标类型的空指针值;为此目的应该用  static_cast 或隐式转换

4) 任何 std::nullptr_t 类型的值,包含 nullptr,可转换成任何整型类型,如同它是 (void*)0 一样,但没有值能转换成 std::nullptr_t ,甚至 nullptr 也不行:为该目的应该用 static_cast。(C++11 起)

5) 任何对象指针类型 T1* 可转换成指向对象指针类型 cv T2 。这严格等价于 static_cast<cv T2*>(static_cast<cv void*>(表达式))(这意味着,若 T2 的对齐要求不比 T1 的更严格,则指针值不改变,且将结果指针转换回原类型将生成其原值)。任何情况下,只有类型别名化(type aliasing)规则允许(见下文)时,结果指针才可以安全地解引用

6) T1 类型的左值表达式可转换成到另一个类型 T2 的引用。结果是与原左值指代同一对象,但有不同类型的左值或亡值。不创建临时量,不进行复制,不调用构造函数或转换函数。只有类型别名化(type aliasing)规则允许(见下文)时,结果指针才可以安全地解引用

7) 任何函数指针可转换成指向不同函数类型的指针。通过指向不同函数类型的指针调用函数是未定义的,但将这种指针转换回指向原函数类型的指针将生成指向原函数的指针值。

8) 一些实现上(特别是在任何 POSIX 兼容的系统上,即基于 dlsym 的要求),函数指针可以转换成 void* 或任何其他对象指针,反之亦然。若实现支持双向的转换,则转换回原类型将生成原值,否则结果指针不能安全地解引用或调用。

9) 任何指针类型的空指针值可转换成任何其他指针类型,产生该类型的空指针值。注意不能用 reinterpret_cast 将空指针常量 nullptr 或任何其他 std::nullptr_t 类型的值转换为指针:为此目的应该使用隐式转换或 static_cast。

10) 成员函数指针可转换成指向不同类型的不同成员函数的指针。转换回原类型将生成原值,否则结果指针不能安全使用。

11) 指向某类 T1 的成员对象的指针可转换成指向另一个类 T2 的另一个成员对象的指针。若 T2 的对齐不比 T1 更严格,则转换回原类型 T1 将生成原值,否则不能安全地使用结果指针。

同所有转型表达式,结果是:

  • 左值,若 new_type 是左值引用或到函数类型的右值引用;
  • 亡值,若 new_type 是到对象类型的右值引用;
  • 否则为纯右值。

类型别名化  

凡在试图通过 AliasedType 类型的泛左值读或修改类型为 DynamicType 的对象的值时,行为未定义,除非下列之一为真:

AliasedType 与 DynamicType 相似。
AliasedType 是 DynamicType 的(可有 cv 限定的)有符号或无符号变体。
AliasedType 为 std::byte、 (C++17 起)char 或 unsigned char:这容许将任何对象的对象表示作为一个字节数组加以检验。
非正式地说,忽略顶层 cv 限定性,若两个类型符合下列条件,则它们相似:

  • 它们是同一类型;或
  • 它们都是指针,且被指向的类型相似;或
  • 它们都是指向相同类的成员指针,且被指向的成员类型相似;或
  • 它们都是大小相同的数组或都是未知边界数组,且数组元素类型相似。(c++20起)
  • 它们都是数组,大小相同或至少一个是未知边界数组,且数组元素类型相似。(c++20起) 

例如:

  • const int * volatile * 与 int * * const 相似;
  • const int (* volatile S::* const)[20] 与 int (* const S::* volatile)[20] 相似;
  • int (* const *)(int *) 与 int (* volatile *)(int *) 相似;
  • int (S::*)() const 与 int (S::*)() 不相似;
  • int (*)(int *) 与 int (*)(const int *) 不相似;
  • const int (*)(int *) 与 int (*)(int *) 不相似;
  • int (*)(int * const) 与 int (*)(int *) 相似(它们是同一类型);
  • std::pair<int, int> 与std::pair<const int, int> 不相似。

此规则允许进行基于类型的别名分析,即编译器假设通过一个类型的泛左值读取的值,不会被通过不同类型的泛左值的写入所修改(依据上述例外情况)。

注意,许多 C++ 编译器作为非标准语言扩展放松此规则,以允许通过  union  的不活跃成员的进行类型错误的访问(这种访问在 C 中并不是未定义的)。

注解  

 标准中定义严格别名化规则的段落含有两条额外条例,部分地是从 C 继承而来:

  • AliasedType 为 聚合类型 或 union  类型,它保有前述各类型之一作为其元素或非静态成员(递归地包含子聚合体的元素和被包含的联合体的非静态数据成员)。
  • AliasedType 为 DynamicType 的 (可有 cv 限定的)基类

这些条例所描述的情况不可能出现于 C++,从而从上面的讨论中省略。在 C 中,聚合复制和赋值将聚合体对象作为整体访问。但 C++ 中始终通过成员函数调用进行这种行动,这会访问单独的子对象而非整个对象(或在联合体的情况下,复制对象表示,即经由 unsigned char)。见核心问题 2051

假设符合对齐要求,则 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<int*>(&s1); // p1 的值为“指向 s1.a 的指针”
                                       // 因为 s1.a 与 s1 为指针可互转换
 
int* p2 = reinterpret_cast<int*>(&s2); // reinterpret_cast 不更改 p2 的值为“指向 s2 的指针”。 
 
int* p3 = reinterpret_cast<int*>(&u);  // p3 的值为“指向 u.a 的指针”:u.a 与 u 指针可互转换
 
double* p4 = reinterpret_cast<double*>(p3); // p4 的指针为“指向 u.b 的指针”:u.a 与 u.b
                                            // 指针可互转换,因为都与 u 指针可互转换
 
int* p5 = reinterpret_cast<int*>(&arr); // reinterpret_cast 不更改 p5 的值为“指向 arr 的指针”

在不实际代表适当类型的对象的泛左值(例如通过 reinterpret_cast 所获得)上,进行代表非静态数据成员或非静态成员函数的成员访问,将导致未定义行为: 

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

许多编译器在这种情况下发布“严格别名化”警告,即使在技术上这种构造所违背的并非称为“严格别名化规则”段落的规则。

严格别名化及其相关规则的目的,是启用基于类型的别名分析,若程序能合法地创建一种情形,使得两个指向无关类型的指针(例如一个 int* 和一个 float*)能同时存在并可一同用于加载或存储同一内存(见此 SG12 reflector 上的邮件),则别名分析会普遍无效。故任何看起来能够创建这种情形的技巧都必然导致未定义行为。

当需要将对象的字节解释为不同类型的值时,可以使用std::memcpy 或 std::bit_cast (C++20 起):

double d = 0.1;
std::int64_t n;
static_assert(sizeof n == sizeof d);
// n = *reinterpret_cast<std::int64_t*>(&d); // 未定义行为
std::memcpy(&n, &d, sizeof d); // OK
n = std::bit_cast<std::int64_t>(d); // 亦 OK

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

DR应用于出版时的行为正确行为
CWG 195C++98不允许函数指针和对象指针间的转换使之为条件性支持

 示例:

演示 reinterpret_cast 的一些用法 :

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

 可能的输出:

The value of &i is 0x7fff352c3580
42
This system is little-endian
42

 

例1: 

#include<iostream>

class Animal {
public:
	void cry()
	{
		std::cout << "Animal cry" << std::endl;
	}
};

class Book {
public:
	void look()
	{
		std::cout << "Book look " << std::endl;
	}
};

int main()
{
	Animal* a = new Animal();
	a->cry();
	Book* b = reinterpret_cast<Book*>(a);  //强制类型的转换
	b->look();
	system("pause");
	return 0;
}

例2:

上述代码将指针ptr的地址的值转换成了 unsigned int 类型的ptr_addr 的整数值. 
提供下IBM C++ 对 reinterpret_cast 推荐使用的地方 

  • A pointer to any integral type large enough to hold it (指针转向足够大的整数类型) 
  • A value of integral or enumeration type to a pointer (从整形或者enum枚举类型转换为指针) 
  • A pointer to a function to a pointer to a function of a different type (从指向函数的指针转向另一个不同类型的指向函数的指针) 
  • A pointer to an object to a pointer to an object of a different type (从一个指向对象的指针转向另一个不同类型的指向对象的指针) 
  • A pointer to a member to a pointer to a member of a different class or type, if the types of the members are both function types or object types (从一个指向成员的指针转向另一个指向类成员的指针!或者是类型,如果类型的成员和函数都是函数类型或者对象类型)

下面这个例子来自 MSDN 的一个哈希函数辅助

                
// expre_reinterpret_cast_Operator.cpp
// compile with: /EHsc
#include <iostream>
// Returns a hash code based on an address
unsigned short Hash( void *p ) {
	unsigned int val = reinterpret_cast<unsigned int>( p );
	return ( unsigned short )( val ^ (val >> 16));
}
 
using namespace std;
int main() {
	int a[20];
	for ( int i = 0; i < 20; i++ )
		cout << Hash( a + i ) << endl;
}
 
//如果跟我一样是64位的系统,可能需要将unsigned int改成 unsigned long才能运行。

            

这段代码适合体现哈希的思想,暂时不做深究,但至少看Hash函数里面的操作,也能体会到,对整数的操作显然要对地址操作更方便。在集合中存放整形数值,也要比存放地址更具有扩展性(当然如果存void *扩展性也是一样很高的),唯一损失的可能就是存取的时候整形和地址的转换(这完全可以忽略不计)。

不过可读性可能就不高,所以在这种情况下使用的时候,就可以用typedef来定义个指针类型:
typedef unsigned int PointerType;

这样不是更棒,当我们在64位机器上运行的时候,只要改成:
typedef unsigned long PointerType;

当reinterpret_cast面对const

IBM的C++指南指出:reinterpret_cast不能像const_cast那样去除const修饰符。 这是什么意思呢?代码还是最直观的表述:

int main() 
{
	typedef void (*FunctionPointer)(int);
	int value = 21;
	const int* pointer = &value;
	
	//int * pointer_r = reinterpret_cast<int*> (pointer); 
	// Error: reinterpret_cast from type 'const int*' to type 'int*' casts away constness
	
	FunctionPointer funcP = reinterpret_cast<FunctionPointer> (pointer);
}

 

例子里,我们像前面const_cast一篇举到的例子那样,希望将指向const的指针用运算符转换成非指向const的指针。但是当实用reinterpret_cast的时候,编译器直接报错组织了该过程。这就体现出了const_cast的独特之处。

但是,例子中还有一个转换是将指向const int的指针付给指向函数的指针,编译顺利通过编译,当然结果也会跟前面的例子一样是无意义的。

如果我们换一种角度来看,这似乎也是合理的。因为
const int* p = &value;
int * const q = &value;

这两个语句的含义是不同的,前者是"所指内容不可变",后者则是"指向的地址不可变"(具体参考此处)。因此指向函数的指针默认应该就带有"所指内容不可变"的特性。

毕竟函数在编译之后,其操作过程就固定在那里了,我们唯一能做的就是传递一些参数给指针,而无法改变已编译函数的过程。所以从这个角度来想,上边例子使用reinterpret_cast从const int * 到FunctionPointer转换就变得合理了,因为它并没有去除const限定。

explicit

explicit关键字阻止经过转换构造函数进行的隐式转换的发生

//单参构造的隐式类型转换举例
class E
{
public:
	E(int a)
	:_a(a)
	{
		cout << "E(int a) " << endl;
	}
private:
	int _a;
};
 
int main()
{
	//单参构造的隐式类型转换
	E e = 10;//把一个整数类型赋给自定义类型,会支持   
	//实际上是它会用10调用构造函数创建一个匿名的E类型的对象,再把这个匿名对象赋给e 
	return 0;
}

实际上面的代码在执行时:它会用10调用构造函数创建一个匿名的E类型的对象,再把这个匿名对象赋给e。类型一致,赋值当然没错。

//多参构造的隐式类型转换举例
class E
{
public:
	E(int a, int b)
		:_a(a)
		{}
private:
	int _a;
};
 
int main()
{
	E e2 = 10, 20;
	return 0;
}

//explicit
class E
{
public:
	//explicit会阻止单参构造的隐式类型转换
	explicit E(int a)
		:_a(a)
	{
		cout << "E(int a) " << endl;
	}
private:
	int _a;
};
 
int main()
{
	//explicit关键字会阻止单参构造函数进行的隐式转换
	E e = 10;
	return 0;
}

 C++中的RTTI机制

RTTI:Run-time Type identification的简称,即:运行时类型识别。

RTTI提供了以下两个非常有用的操作符:

  1. typeid操作符,返回指针和引用所指的实际类型。
  2. dynamic_cast操作符,将基类类型的指针或引用安全地转换为派生类型的指针或引用。

使用强制转换方法的选择 如下

  • 如果实在需要用到类型转换,用c++风格转换。不要再使用c语言风格的转换。
  • 去const属性用const_cast, 资料说:使用const_cast意味着涉及缺陷。
  • 基本类型转换用static_cast
  • 多态类之间的类型转换用dynamic_cast
  • 不同类型的指针类型转换用reinterpret_cast,资料说:reinterpret_cast很危险,reinterpret_cast 合乎规则的用,其实很好用 
  • 一般static_cast 和reinterpret_cast取代c语言风格的转换

强制类型转换实际上 关闭或挂起了正常的类型检查,每次使用强制类型转换前,程序员应该仔细考虑是否还有其他不同的方法达到同一目的,如果非强制类型转换不可,则应限制强制转换值的作用域,以减少发生错误的机会。

强烈建议:避免使用强制类型转换



  参考:

C++类型转换_xusiwei1236的技术博客-CSDN博客

https://blog.csdn.net/qq_41300114/article/details/105686871 

c++ 四种强制类型转换_u010540025的专栏-CSDN博客_c++强制类型转换

关于 reinterpert_cast :

reinterpert_cast_huan_xia的专栏-CSDN博客

C++标准转换运算符之 reinterpret_cast:https://blog.csdn.net/p942005405/article/details/105783090 

C++标准转换运算符reinterpret_cast - Ider - 博客园

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值