C++类型转换

C++类型转换

1. const_cast

const_cast可以将const转换成非const,也可以将非const转换成const。需要注意的是 const_cast只能用于改变指针或者引用的底层const。

底层const和顶层const

首先要弄清楚 const修饰的到底是谁,用顶层表示指针本身是个常量(指针常量),底层表示指针所指向的对象是个常量(常量指针)。

int* const p1 = &i1;
int const* p2 = &i2;
const int i3 = 3;
int const i4 = 4;
const int* p3 = &i5;
const int* const p4 = &i3;
const int const* p5 = &i6;
const int& r = i7;

const默认是修饰它左边的符号的,如果左边没有,那么就修饰它右边的符号,比如例子中

  • 第一个 const左边是 int*,表示指针不能修改,所以是顶层const
  • 第二个修饰的 int,表示指针指向的值不能修改,所以是底层const
  • 第三个 const左边没有,修饰右边符号 int,这个非指针非引用,没有底层顶层的概念
  • 第四个修饰左边 int和第三个一样
  • 第五个左边没有,修饰右边 int,注意不是修饰 int*
  • 第六个第一个 const左边没有,修饰右边 int是底层;第二个 const修饰左边 int*是顶层
  • 第七个两个 const都是修饰 int,都是底层,这个指针是可以修改指向对象的
  • 第八个第一个 const修饰 int,是底层const;引用默认了顶层 const,因为引用一旦初始化就不能再改为其他对象的引用。

关于顶层const和底层const可以看看这篇文章C++干货系列——顶层const和底层const

const_cast只可以对指针和引用使用

const_cast针对指针和引用是语法上的要求,把一个常量变量变成非常量变量不应该是我们的目的。我们不应该为了修改一个const变量的值而去使用,例如我们有一个常量 const_a,但是我们想去修改它的值于是决定用一个常量指针指向它,然后用 const_cast让常量指针变成普通指针,修改这个变量

const int const_a = 66;
const int* const_p = &const_a;
int* p = const_cast<int*>(const_p);
*p = 88;
std::cout << *p << std::endl;//输出结果88
std::cout << const_a << std::endl;//输出结果66

理论上这个变量的内存的内容确实改变了,我们输出 *p的结果也证实了这一点。但是当我们输出 const_a的值会发现结果依然是66。难道是他们的内存地址不一样?

std::cout << &const_a << std::endl;//000000730AAFF5C4
std::cout << p << std::endl;	   //000000730AAFF5C4

事实上他们确实是同一块内存地址。真实的原因是编译器将const变量直接用66替换了。加const只是告诉编译器不能修改而不是真正地不可修改,如果程序员不注意而去修改了它会报错,现在我们利用const_cast去除了常量性,然后通过指针和引用对其进行了修改,所以通过指针打印或者引用传参的时候就能看出其内存确实变化了,但为了保护这个变量本来的const特性,所以每次我们使用const变量时,系统都将其替换成初始值66,确保了const变量还是“不可变”的。可以参考文章const_cast的小问题

这种情况是一种未定义行为,所谓未定义,是说这个语句在标准C++中没有明确的规定,由编译器来决定如何处理。很可能换一个编译器,处理结果就完全不同,我们要避免未定义行为的发生。

什么时候使用const_cast

const_cast的目的,在于某些变量原本不是const的,但由于某种特殊原因,无意间被变成了const的。例如使用了一个const引用指向了一个本来不是const的对象。结果写了一些代码之后发现它实际上需要被修改。这在平时的工作中不会遇到因为你可以直接把const引用修改成非const的,但C++中可能的情况太多,尤其考虑到很多复用的时候,有时还是会出现本不该是const的对象被const引用了这种情况。

int k=10;

const int& ref=k;

int& x=const_cast<int&>(ref);

x=11;

还要一些情况,例如我们需要在类里面重载一个const函数和非const函数。这么做的目的很明确,因为不能在一个常量对象中调用非常成员函数,这是因为函数需要传入隐式的 this指针,在默认情况下,this的类型是指向类的非常量版本的常量指针(意思是 this的值不能改变,永远指向那个对象,即“指针常量”,但是被 this指向的对象本身是可以改变的,因为是非常量版本,这里 this相当于是顶层const),而 this尽管是隐式的,它仍然需要遵循初始化规则,普通成员函数的隐式参数之一是一个底层非const指针,在默认情况下我们无法把一个底层const的 this指针转化为非const的 this指针,因此我们不能在常量对象上调用普通的成员函数。可以参考文章常量对象只能调用常成员函数 原因分析

所以为了让常量对象能调用同样的函数,我们需要重载一个常量版本。

class A
{
public:
	void fun()
	{
		std::cout << "非常量版本" << std::endl;
	}
};

int main()
{
	A a;
	const A const_a;

	a.fun();
	const_a.fun();//错误,对象含有与成员函数不兼容的类型限定符

}

这样就会报错:对象含有与成员函数不兼容的类型限定符

我们需要加一个常量成员函数

class A
{
public:
	void fun()
	{
		std::cout << "非常量版本" << std::endl;
	}

	void fun() const
	{
		std::cout << "非常量版本" << std::endl;
	}
};

int main()
{
	A a;
	const A const_a;

	a.fun();
	const_a.fun();

}

如果这两个版本做的事情完全一样,那么我们并不需要写两份一样的代码,可以利用 const_cast这样做

class A
{
public:
	void fun()
	{
		std::cout << "hello" << std::endl;
	}

	void fun() const
	{
		const_cast<A*>(this)->fun();
	}
};

int main()
{
	A a;
	const A const_a;

	a.fun();
	const_a.fun();

}

参考文章C++中const_cast的作用和缘由【C++】const_cast基本用法(详细讲解)

2. dynamic_cast

dynamic_cast:动态类型转换,用于将基类的指针引用安全地转换成派生类的指针或引用(也可以向上转换),若指针转换失败返回NULL,若引用返回失败抛出bad_cast异常。

什么时候使用dynamic_cast

如果我们有一个 Eneity类,PlayerEnemy继承了这个类。我们用基类指针e1和e2分别指向Player和Enemy。现在我有了基类指针,但我想把基类的指针分别转型到它们自己的类别指针上面。但可能我并不清楚e1和e2实际指向的到底是Player类还是Enemy类。如果我做了错误的转型,我把实际指向Enemy类的e2转型成了Player类,并且调用了函数 func,最后输出的是“I am Player”。这不是我们想要的,如果我们尝试做一些Enemy没有,而Player所独有的事情,比如访问特定的成员变量,只有player有,而enemy没有,因为潜在的类型实际上是Enemy,我们的程序很可能崩溃,或者做出些我们不想做的奇怪的事情。

class Entity
{

};

class Player : public Entity
{
public:
	void func()
	{
		std::cout << "I am Player";
	}
};

class Enemy : public Entity
{
public:
	void func()
	{
		std::cout << "I am Enemy";
	}
};


int main()
{
	Entity* e1 = new Player();
	Entity* e2 = new Enemy();

	Player* p = (Player*)e2;
	p->func(); //输出“I am Player”
}

为了避免一些安全问题,我们可以使用 dynamic_castdynamic_cast是在运行时进行安全性检查;使用 dynamic_cast父类一定要有虚函数,否则编译不通过;

我们给基类加一个虚析构函数就行了。最终 dynamic_cast返回一个空指针,转型失败

class Entity
{
public:
	virtual ~Entity() {};
};

class Player : public Entity
{
public:
	void func()
	{
		std::cout << "I am Player";
	}

};

class Enemy : public Entity
{
public:
	void func()
	{
		std::cout << "I am Enemy";
	}
};


int main()
{
	Entity* e1 = new Player();
	Entity* e2 = new Enemy();

	Player* p = dynamic_cast<Player*>(e2);
	if (p)
		p->func();
	else
		std::cout << "转型失败" << std::endl;
}

dynamic_cast的原理

dynamic_cast是怎么做到能知道Player是Player,Enemy是Enemy的呢?这是因为他用到了运行时类型信息RTTI(Runtime Type Information)。RTTI的实现原理是通过在虚表中放一个额外的指针,每个新类只产生一个typeinfo实例,额外指针指向typeinfo, typeid返回对它的一个引用 。类的内存布局可以看这篇文章C++类内存布局与虚继承

我们来看上面示例的布局

1697791106360

第一个指针就是指向当前类对应的 type_info 对象。当程序在运行阶段获取类型信息时,可以通过对象指针 p 找到虚函数表指针 vfptr,再通过 vfptr 找到 type_info 对象的指针,进而取得类型信息。只要对比type_info就能知道是否能进行转型。虽然这么做会消耗资源,但也是不得已而为之。

RTTI不仅仅有 dynamic_cast,要更详细了解可以看视频明解 C++ Primer:19.2运行时类型识别RTTI,讲的是C++Primer730页的内容。

3. static_cast

static_cast: 隐式类型转换,可以实现C++中内置基本数据类型之间的相互转换,enum、struct、 int、char、float等,能进行类层次间的向上类型转换和向下类型转换(向下不安全,因为没有进行动态类型检查)。它不能进行无关类型(如非基类和子类)指针之间的转换,也不能作用包含底层const的对象;

4. reinterpret_cast

reinterpret_cast:reinterpret是重新解释的意思,此标识符的意思即为将数据的二进制形式重新解释,但是不改变其值,有着和C风格的强制转换同样的能力。它可以转化任何内置的数据类型为其他任何的数据类型,也可以转化任何指针类型为其他的类型。它甚至可以转化内置的数据类型为指针,无须考虑类型安全或者常量的情形。不到万不得已绝对不用(比较不安全)

posted @ 2023-10-20 17:02  DogWealth~  阅读(89)  评论(0编辑  收藏  举报