目录

隐式转换

为什么要进行隐式转换

C++隐式转换的原则

C++隐式转换发生条件

隐式转换的风险

禁止隐式转换

显式转换

dynamic_cast

static_cast

const_cast

reinterpret_cast


隐式转换

当一个值拷贝给另一个兼容类型的值时,隐式转换会自动进行。所谓隐式转换,是指不需要用户干预,编译器私下进行的类型转换行为。

例如:

short a=2000;
int b;
b=a;

在这里,a在没有任何显示操作符的干预下,由short类型转换为int类型。这就是标准转换,标准转换将影响基本数据类型,并允许数字类型之间的转换(short到int, int到float, double到int…),到bool或从bool,以及一些指针转换。

对于非基本类型,数组和函数隐式地转换为指针,并且指针允许如下转换:

  • NULL指针可以转换为任意类型指针
  • 任意类型的指针可以转换为void指针
  • 指针向上提升:一个派生类指针可以被转换为一个可访问的无歧义的基类指针,不会改变它的const或volatile属性

为什么要进行隐式转换

C++面向对象的多态特性,就是通过父类的类型实现对子类的封装。通过隐式转换,你可以直接将一个子类的对象使用父类的类型进行返回。再比如,数值和布尔类型的转换,整数和浮点数的转换等。某些方面来说,隐式转换给C++程序开发者带来了不小的便捷。C++是一门强类型语言,类型的检查是非常严格的。如果没有类型的隐式转换,这将给程序开发者带来很多的不便。

C++隐式转换的原则

  • 基本数据类型 基本数据类型的转换以取值范围的作为转换基础(保证精度不丢失)。隐式转换发生在从小->大的转换中。比如从char转换为int。从int->long。
  •  自定义对象子类对象可以隐式的转换为父类对象。

C++隐式转换发生条件

  • 混合类型的算术运算表达式中。例如:

    1

    2

    3

    int a = 3;

    double b = 4.5;

    a + b; // a将会被自动转换为double类型,转换的结果和b进行加法操作

  •  不同类型的赋值操作。例如:

    1

    2

    int a = true ; ( bool 类型被转换为 int 类型)

    int * ptr = null;(null被转换为 int *类型)

  •  函数参数传值。例如:

    1

    2

    void func( double a);

    func(1); // 1被隐式的转换为double类型1.0

  •  函数返回值。例如:

    1

    2

    3

    4

    double add( int a, int b)

    {

         return a + b;

    //运算的结果会被隐式的转换为double类型返回

#以上四种情况下的隐式转换,都满足了一个基本原则:低精度 –> 高精度转换。不满足该原则,隐式转换是不能发生的。

当然这个时候就可以使用与之相对于的显式类型转换(又称强制类型转换),使用方法如下:

       double a = 2.0;
       int b = (int)a;

     使用强制类型转换会导致精度的损失,因此使用时务必确保你已经拥有足够的把握。

隐式转换的风险

类的隐式转换:在类中,隐式转换可以被三个成员函数控制:

  • 单参数构造函数:允许隐式转换特定类型来初始化对象。
  • 赋值操作符:允许从特定类型的赋值进行隐式转换。
  • 类型转换操作符:允许隐式转换到特定类型

参考文章

隐式转换的风险一般存在于自定义的类构造函数中。

按照默认规定,只有一个参数的构造函数也定义了一个隐式转换,将该构造函数对应数据类型的数据转换为该类对象。

#include <iostream>
#include<cstdlib>
#include<ctime>
 
using namespace std;

class Str
{
public:
	// 用C风格的字符串p作为初始化值
	Str(const char*p) {
		cout << p << endl;
	}
	//本意是预先分配n个字节给字符串
	Str(int n) {
		cout << n << endl;
	}

};

int main(void) {

	Str s = "Hello";//隐式转换,等价于Str s = Str("Hello");

	//下面两种写法比较正常:
	Str s2(10);   //OK 分配10个字节的空字符串
	Str s3 = Str(10); //OK 分配10个字节的空字符串

	//下面两种写法就比较疑惑了:
	Str s4 = 10; //编译通过,也是分配10个字节的空字符串
	Str s5 = 'a'; //编译通过,分配int(‘a’)个字节的空字符串,使用的是Str(int n)构造函数
	//s4 和s5 分别把一个int型和char型,隐式转换成了分配若干字节的空字符串,容易令人误解。
	return 0;
}
/*
 *
Hello
10
10
10
97
*/

参考文章

例二
如下例:

#include <iostream>
#include<cstdlib>
#include<ctime>
 
using namespace std;
class Test {
public:
	Test(int a):m_val(a) {}
	bool isSame(Test other)
	{
		return m_val == other.m_val;
	}
private:
		int m_val;
};

int main(void) {

	Test a(10);
	if (a.isSame(10)) //该语句将返回true
	{
		cout << "隐式转换" << endl;
	}
	return 0;
}

本来用于两个Test对象的比较,竟然和int类型相等了。这里就是由于发生了隐式转换,实际比较的是一个临时的Test对象。这个在程序中是绝对不能允许的。

禁止隐式转换

既然隐式转换存在这么多的风险,那如何能够禁止隐式转换的发生呢。C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换。使用方法如下:

class Test
{
explicit Test( int a);
……
 
}
#include <iostream>
#include<cstdlib>
#include<ctime>
 
using namespace std;
class Str
{
public:
	// 用C风格的字符串p作为初始化值
	explicit Str(const char*p) {
		cout << p << endl;
	}
	//本意是预先分配n个字节给字符串
	explicit  Str(int n) {
		cout << n << endl;
	}

};
class Test {
public:
	explicit Test(int a):m_val(a) {}
	bool isSame(Test other)
	{
		return m_val == other.m_val;
	}
private:
		int m_val;
};

int main(void) {

	Test a(10);
	if (a.isSame(10)) 编译不通过
	{
		cout << "隐式转换" << endl;
	}

	Str s = "Hello";//编译不通过

//下面两种写法比较正常:
	Str s2(10);   //OK 分配10个字节的空字符串
	Str s3 = Str(10); //OK 分配10个字节的空字符串

	//下面两种写法就比较疑惑了:
	Str s4 = 10; //编译不通过
	Str s5 = 'a'; //编译不通过
	
	return 0;
}

参考文章

显式转换

C++是一门强类型的语言,许多转换,特别是那些暗示值的不同解释的转换,需要显式转换,在c++中称为类型转换。泛型类型转换有两种主要语法:函数型和类c型:

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()<<endl;//Dummy 类中没有result,但是编译器不报错

	//情况二:将指向const对象的指针转成指向非const
	int a = 666;
	const int *p1 = &a;
	//*p1 = 999;//这里会报错,p指向的值为常量,不能赋值更改
	int *p2 = (int *)p1;
	*p2 = 999;//经过强制类型转换后,失去了const属性,此时不报错
	cout <<"a = "<< a << endl;//a 的值已被更改了
	return 0;
}

程序声明了一个指向Addition的指针,但随后使用显式类型转换将另一个不相关类型对象的引用赋给该指针:

padd = (Addition*) &d;

不受限制的显式类型转换允许将任何指针转换为任何其他指针类型,而不依赖于指针所指向的类型。后面成员函数result的调用将产生运行时错误或其他一些意外结果。

其他情况:

  • 将指向const对象的指针转换成非const对象的指针
  • 可能将基类对象指针转成了派生类对象的指针

 总结:编译时报错优于运行时报错,所以C++引入的四种类型转换,不同场景下不同需求使用不同的类型转换方式,同时有利于代码审查。

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

dynamic_cast

dynamic_cast只能用于指向类的指针和引用(或void*)。它的目的是确保类型转换的结果指向目标指针类型的有效完整对象。

将dynamic_cast用于引用时,其用法稍有不同:没有与空指针对应的引用值,也就是说没有空引用,引用必须要初始化,因此无法使用特殊的引用值来指示失败,当请求不正确时,dynamic_cast将引发bad_cast异常。

向上转换:将指向派生类的指针转为指向基类的指针,其方式与隐式转换相同。

向下转换:将指向基类的指针转为指向派生类的指针,此时的类需要是多态类(具有虚成员的类)

#include <iostream>

using namespace std;

struct Base {
    virtual void Func() { cout << "Base Func \n"; }
};

struct Derive : public Base {
    void Func() override { cout << "Derive Func \n"; }
};

int main() {
    Derive d;
    d.Func();
    Base *b = dynamic_cast<Base *>(&d);
    b->Func();
    Derive *dd = dynamic_cast<Derive *>(b);
    dd->Func();
    return 0;
}

// dynamic_cast
#include <iostream>
#include <exception>
using namespace std;

class Base {
public:
	virtual void show() {
	cout << "我是基类" << endl;
} };
class Derived : public Base { 
	int a; 
public:
	void show() {
		cout << "我是派生类" << endl;
	}
};

int main() {
	try {
		Base * pba = new Derived;//这里做了隐式转换,将指向派生类的指针转为基类指针
		Base * pbb = new Base;
		Derived * pd;
		Base *pb1, *pb2;

		//向下转换,基类指针转为派生类指针,不过有条件,这个基类指针指向的对象原本就是派生类对象
		pd = dynamic_cast<Derived*>(pba); //如果基类不是多态类型(无虚函数),则不能用dynamic_cast()向下转换
		if (pd == 0) cout << "Null pointer on first type-cast.\n";
		pd->show();

		//如果基类指针指向的是基类对象,则不能转换为派生类指针,返回空指针
		pd = dynamic_cast<Derived*>(pbb);
		if (pd == 0) cout << "Null pointer on second type-cast.\n";

		Derived *pd2 = new Derived;
		//向上转换,将派生类指针转为基类指针
		pb1 = pd2; //可以隐式转换
		pb1->show();//如果show是虚函数,将会打印我是派生类,声明了虚函数之后,它是看内存中的对象,而不是指针类型
		pb2 = dynamic_cast<Derived*>(pd2);//也可以显示转换
		pb2->show();
		

	}
	catch (exception& e) { cout << "Exception: " << e.what(); }
	return 0;
}
/*
 *
我是派生类
Null pointer on second type-cast.
我是派生类
我是派生类*/

 使用场景:用于将父类的指针或引用转换为子类的指针或引用,此场景下父类必须要有虚函数,因为dynamic_cast是运行时检查,检查需要运行时信息RTTI,而RTTI存储在虚函数表中.

static_cast

static_cast可以在类相关的指针中完成转换,不仅是向上转换,还有向下转换。在运行时期间不执行任何检查,以确保正在转换的对象实际上是目标类型的完整对象。因此它依靠编译器确保转换是否安全,另一方面,它没有dynamic_cast运行时检查的开销。

// dynamic_cast
#include <iostream>
#include <exception>
using namespace std;

class Base {
public:
	virtual void show() {
	cout << "我是基类" << endl;
} };
class Derived : public Base { 
	int a; 
public:
	void show() {
		cout << "我是派生类" << endl;
	}
};

int main() {
	try {
		Base * a = new Base;
		Derived * b = static_cast<Derived*>(a);
		b->show(); //如果show为虚函数,则显示我是基类,不是虚函数则显示我是派生类

	}
	catch (exception& e) { cout << "Exception: " << e.what(); }
	return 0;
}

 上面的代码可以编译通过,但很明显b指向的是一个不完整的对象,很可能在运行时发生错误。

使用场景:基本数据类型之间的转换使用,例如float转int,int转char等,在有类型指针和void*之间转换使用,子类对象指针转换成父类对象指针也可以使用static_cast。

非多态类型转换一般都使用static_cast,而且最好把所有的隐式类型转换都是用static_cast进行显示替换,不能使用static_cast在有类型指针之间进行类型转换。

const_cast

这种类型的类型转换操作指针所指向的对象的常量,可以是要设置的,也可以是要删除的。例如,为了将const指针传递给需要非const实参的函数:

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

 上面的例子保证可以工作,因为函数print不会写指向的对象。但是请注意,移除指向对象的常量以实际写入它会导致未定义的行为。

int main() {
    int data = 10;
    const int *cpi = &data;

    int *pi = const_cast<int *>(cpi);

    const int *cpii = const_cast<const int *>(pi);
    return 0;
}

 使用场景:用于常量指针或引用与非常量指针或引用之间的转换,只有const_cast才可以对常量进行操作,一般都是用它来去除常量性,去除常量性是危险操作,还是要谨慎操作。

reinterpret_cast

 reinterpret_cast可以将指针类型任意转换,甚至是不相关的类之间,

int main() {
    int data = 10;
    int *pi = &data;

    float *fpi = reinterpret_cast<float *>(pi);

    return 0;
}

使用场景:没啥场景,类似C语言中的强制类型转换,什么都可以转,万不得已不要使用,一般前三种转换方式不能解决问题了使用这种强制类型转换方式。操作结果是从一个指针到另一个指针的值的简单二进制拷贝

允许所有的指针转换:既不检查指针所指向的内容,也不检查指针类型本身。

可以由reinterpret_cast执行但不能由static_cast执行的转换是基于重新解释类型的二进制表示的低级操作,在大多数情况下,这将导致特定于系统的代码,因此不可移植。

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

 这段代码可以编译,尽管它没有多大意义,因为现在b指向一个完全不相关且可能不兼容的类的对象。解引用b是不安全的。

它还可以强制转换指向或来自整数类型的指针。这个整数值表示指针的格式与平台有关。唯一的保证是,将指针转换为足够大的整数类型以完全包含它(如intptr_t),保证能够将其转换回有效的指针。

Type conversions - C++ Tutorials

C++为什么非要引入那几种类型转换?

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐