97.隐式转换,如何消除隐式转换?

发布时间 2023-07-11 15:42:54作者: CodeMagicianT

97.隐式转换,如何消除隐式转换?

1.C++的基本类型中并非完全的对立,部分数据类型之间是可以进行隐式转换的。所谓隐式转换,是指不需要用户干预,编译器私下进行的类型转换行为。很多时候用户可能都不知道进行了哪些转换

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

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

4.C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换。

5.如果构造函数只接受一个参数,则它实际上定义了转换为此类类型的隐式转换机制。可以通过将构造函数声明为explicit加以制止隐式类型转换,关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit。

1.C语言中的类型转换

C语言和C++都是强类型语言,如果赋值运算符左右两侧变量的类型不同,或形参与实参的类型不匹配,或返回值类型与接收返回值的变量类型不一致,那么就需要进行类型转换。

C语言中有两种形式的类型转换,分别是隐式类型转换和显式类型转换:

●隐式类型转换:编译器在编译阶段自动进行,能转就转,不能转就编译失败。
●显式类型转换:需要用户自己处理,以(指定类型)变量的方式进行类型转换。
需要注意的是,只有相近类型之间才能发生隐式类型转换,比如int和double表示的都是数值,只不过它们表示的范围和精度不同。而指针类型表示的是地址编号,因此整型和指针类型之间不会进行隐式类型转换,如果需要转换则只能进行显式类型转换。比如:

为什么C++需要四种类型转换
int main()
{
	//隐式类型转换
	int i = 1;
	double d = i;
	cout << i << endl;
	cout << d << endl;

	//显式类型转换
	int* p = &i;
	int address = (int)p;
	cout << p << endl;
	cout << address << endl;
	return 0;
}

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

C风格的转换格式虽然很简单,但也有很多缺点:

●隐式类型转换在某些情况下可能会出问题,比如数据精度丢失。
●显式类型转换将所有情况混合在一起,转换的可视性比较差。
  因此C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符,分别是static_cast、reinterpret_cast、const_cast和dynamic_cast。

————————————————
原文链接:https://blog.csdn.net/chenlong_cxy/article/details/127144522

3.隐式转换

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

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

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

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

●NULL指针可以转换为任意类型指针
●任意类型的指针可以转换为void指针
●指针向上提升:一个派生类指针可以被转换为一个可访问的无歧义的基类指针,不会改变它的const或volatile属性
原文链接:https://blog.csdn.net/luolaihua2018/article/details/111996610

3.1为什么要进行隐式转换

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

3.2C++隐式转换的原则

基本数据类型 基本数据类型的转换以取值的范围作为转换基础(保证精度不丢失)。隐式转换发生在从小到大的转换中。比如从char转换为int。从int到long。

●自定义对象子类对象可以隐式的转换为父类对象

3.3C++隐式转换发生条件

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

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

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

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

●函数参数传值。例如:

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

●函数返回值。例如:

double add( int a, int b)
{
     return a + b;

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

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

数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针:

int ia[10];//含有10个整数的数组
int* ip = ia;//ia转换成指向数组首元素的指针 

●数组转换成指针

  当数组被用作decltype关键字的参数,或者作为取地址符(&)、sizeof及typeid(第19.2.2节, 732页将介绍)等运算符的运算对象时,上述转换不会发生。同样的,如果用一个引用来初始化数组(参见3.5.1节,第102页), 上述转换也不会发生。我们将在6.7节(第221页)看到, 当在表达式中使用函数类型时会发生类似的指针转换。

●指针的转换

   C++还规定了几种其他的指针转换方式, 包括常量整数值0或者字面值 nullptr能转换成任总指针类型:指向任意非常阰的指针能转换成void*:指向任意对象的指针能转换成canst void*。 15.2.2节(第530页)将要介绍,在有继承关系的类型间还有另外一种指针转换的方式。
●转换成布尔类型

  存在一种从算术类型或指针类型向布尔类型自动转换的机制。 如果指针或算术类型的值为0,转换结果是false;否则转换结果是true:

char *cp = get_string (); 
if(cp) / *... * / //如果指针cp不是0,条件为真
while (*cp) /*... */ //如果*cp不是空字符,条件为真

●转换成常量

  允许将指向非常量类型的指针转换成指向相应的常品类型的指针,对于引用也是这样。也就是说,如果T是一种类型,我们就能将指向T的指针或引用分别转换成指向const T的指针或引用(参见2.4.1节, 第54页和2.4.2节,第56页):

int i; 
const int &j = i;//非常量转换成const int的引用
const int *p = &i;//非常量的地址转换成const的地址
int &r = j, *q = p;//错误:不允许const转换成非常量

  相反的转换并不存在, 因为它试图删除掉底层const。

●类类型定义的转换

  类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。 在7.5.4节(第263页)中我们将看到一个例子,如果同时提出多个转换请求,这些请求将被拒绝。

  之前的程序已经使用过类类型转换:一处是在需要标准库string类型的地方使用C风格字符串(参见3.5.5节, 第111页);另一处是在条件部分读入istream:

String s, t = "a value";//字符串字面值转换成string类型
while (cin >> s)//while的条件部分把cin转换成布尔值

  条件(cin>>s)读入cin的内容并将cin作为其求值结果。条件部分本来需要一个布尔类型的值,但是这里实际检查的是istream类型的值。幸好,IO库定义了从istream向布尔值转换的规则, 根据这一规则,cin自动地转换成布尔值。所得的布尔值到底是什么由输入流的状态决定,如果最后一次读入成功,转换得到的布尔值是true; 相反,如果最后一次读入不成功,0转换得到的布尔值是false。

3.4隐式转换的风险

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

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

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

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

#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 s = 1;//也正确
	//下面两种写法比较正常:
	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对象。这个在程序中是绝对不能允许的。

3.5禁止隐式转换

隐式转换存在风险多,那如何能够禁止隐式转换的发生呢。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; //编译不通过  不存在从 "int" 转换到 "Str" 的适当构造函数	
	Str s5 = 'a'; //编译不通过
	
	return 0;
}

4.显式转换

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)

4.4static_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在有类型指针之间进行类型转换。
————————————————
原文链接:https://blog.csdn.net/luolaihua2018/article/details/111996610

  任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。例如, 通过将一个运算对象强制转换成 double 类型就能使表达式执行浮点数除法:

//进行强制类型转换以便执行浮点数除法
double slope = static_cast<double>(j) / i;

  当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。此时,强制类型转换告诉程序的读者和编译器:我们知道并且不在乎潜在的精度损失。一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警告信息;但是当我们执行了显式的类型转换后,警告信息就会被关闭了。
  static_cast对于编译器无法自动执行的类型转换也非常有用。 例如,我们可以使用static_cast找回存在于void*指针(参见2.3.2节, 第50页)中的值

void *p = &d;//正确:任何非常量对象的地址都能存入void*
//正确:将void*转换回初始的指针类型
double *dp = static_cast<double*>(p);

  当我们把指针存放在void*中,并且使用static_cast将其强制转换回原来的类型时,应该确保指针的值保持不变。也就是说,强制转换的结果将与原始的地址值相等,因此我们必须确保转换后所得的类型就是指针所指的类型。类型一旦不符,将产生未定义的后果。

4.2const_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才可以对常量进行操作,一般都是用它来去除常量性,去除常量性是危险操作,还是要谨慎操作。

const_cast只能改变运算对象的底层const(参见C++ Primer 2.4.3节, 第57页)

const char *pc; 
char *p = const_cast<char*>(pc);//正确:但是通过p写值是未定义的行为

  对于将常量对象转换成非常量对象的行为,我们一般称其为“去掉const性质(cast away the const) ”。 一旦我们去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为。然而如果对象是一个常记,再使用const_cast 执行写操作就会产生未定义的后果。

  只有const_cast能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误。同样的,也不能用const_cast 改变表达式的类型:

const char *cp; 
//错误:static_cast不能转换掉const性质 
char *q = static_cast<char*>(cp); 
static_cast<string>(cp);//正确:字符串字面值转换成string类型
const_cast<string>(cp);//错误:const_cast只改变常量属性

  const_cast 常常用于有函数重载的上下文中,关丁函数重载将在C++ Primer 6.4节(第208页) 进行详细介绍。

4.3reinterpret_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),保证能够将其转换回有效的指针。
————————————————
原文链接:https://blog.csdn.net/luolaihua2018/article/details/111996610

reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。举个例子,假设有如下的转换

int *ip; 
char *pc = reinterpret_cast<char*>(ip); 

我们必须牢记pc所指的真实对象是一个int而非字符,如果把pc当成普通的字符指针使用就可能在运行时发生错误。例如:

string str(pc); 

可能导致异常的运行时行为。

  使用reinterpret_cast是非常危险的,用pc初始化str的例子很好地证明了这 一点。其中的关键问题是类型改变了,但编译器没有给出任何警告或者错误的提示信息。当我们用一个int的地址初始化pc时,由于显式地声称这种转换合法,所以编译器不会 发出任何警告或错误信息。接下来再使用pc时就会认定它的值是char*类型,编译器没法知道它实际存放的是指向int的指针。最终的结果就是,在上面的例子中虽然用pc初始化str没什么实际意义,甚至还可能引发更糟糕的后果,但仅从语法上而言这种操作无可指摘。查找这类问题的原因非常困难,如果将ip强制转换成pc的语句和用pc初始化string对象的语句分属不同文件就更是如此。

建议:避免强制类型转换

  强制类型转换干扰了正常的类型检查(参见C++ Primer 2.2.2节, 第42页),因此我们强烈建议程序员避免使用强制类型转换。这个建议对于reinterpret_cast尤其适用,因为此类类型转换总是充满了风险。在有重载函数的上下文中使用const_cast无可厚非,关于这一点将在C++ Primer 6.4节(第208页)中详细介绍;但是在其他情况下使用const_cast也就意味着程序存在某种设计缺陷。其他强制类型转换,比如static_cast和dynamic_cast,都不应该频繁使用。每次书写了一条强制类型转换语句,都应该反复斟酌能否以其他方式实现相同的目标。就算实在无法避免,也应该尽量限制类型转换值的作用域,并且记录对相关类型的所有假定,这样可以减少错误发生的机会。

4.4旧式的强制类型转换

在早期版本的C++语言中,显式地进行强制类型转换包含两种形式:

type (expr);//函数形式的强制类型转换
(type) expr;//c语言风格的强制类型转换 

根据所涉及的类型不同,旧式的强制类型转换分别具有与const_cast、static_cast或reinterpret_cast相似的行为。当我们在某处执行旧式的强制类型转换时,如果换成const_cast和static_cast也合法,则其行为与对应的命名转换一致。如果替换后不合法,则旧式强制类型转换执行与reinterpret_cast类似的功能:

char *pc = (char*) ip; // ip是指向整数的指针

的效果与使用reinterpret_cast一样。