65.C++的四种强制转换
65.C++的四种强制转换
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;
}
输出:
1
1
000000B7F4AFF864
-189794204
2.为什么C++需要四种类型转换
C风格的转换格式虽然很简单,但也有很多缺点:
- 隐式转换缺点:
- 可能会导致数据精度丢失:当将一个数据类型转换为另一个数据类型时,可能会发生精度丢失,例如将一个浮点数转换为整数时,小数部分会被截断。
- 可能导致未定义行为:隐式转换可能会导致未定义行为,例如将一个指针类型转换为整数类型可能会导致未定义行为。
- 代码不够清晰:隐式转换可能会使代码不够清晰,因为它不需要在代码中明确地指定转换的类型。
- 显式转换缺点:
- 可能会导致数据失真:当使用显式转换将一个数据类型转换为另一个数据类型时,可能会导致数据失真,例如将一个整数转换为浮点数时,小数部分可能会出现不准确的情况。
- 可能会导致代码不够清晰:显式转换需要程序员在代码中明确地指定转换的类型,这可能会使代码不够清晰,同时也增加了代码的复杂性。
C++中的类型转换更加灵活和强大,可以实现更复杂的类型转换操作。C++中的类型转换包括自动类型转换、强制类型转换、类型解析转换和类型推断转换,这四种类型转换可以帮助开发者更好地管理代码中的数据类型,提高代码的可读性和可维护性。
因此C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符,分别是static_cast、reinterpret_cast、const_cast和dynamic_cast。
————————————————
原文链接:[(7条消息) C++的类型转换_2021dragon的博客-CSDN博客]
3.隐式转换
当一个值拷贝给另一个兼容类型的值时,隐式转换会自动进行。所谓隐式转换,是指不需要用户干预,编译器私下进行的类型转换行为。
short a=2000;
int b;
b=a;
在这里,a在没有任何显示操作符的干预下,由short类型转换为int类型。这就是标准转换,标准转换将影响基本数据类型,并允许数字类型之间的转换(short到int,int到float,double到int…),和bool与其他数字类型转换,以及一些指针转换。
对于非基本类型,数组和函数隐式地转换为指针,并且指针允许如下转换:
●NULL指针可以转换为任意类型指针
●任意类型的指针可以转换为void指针
●指针向上提升:一个派生类指针可以被转换为一个可访问的无歧义的基类指针,不会改变它的const或volatile属性
原文链接:[(7条消息) C++类型转换:隐式转换和显式转换_c++隐式类型转换_SOC罗三炮的博客-CSDN博客]
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对象。这个在程序中是绝对不能允许的。
例三
C++ primer 中有这么一句话:可以用单个实参来调用的构造函数定义了从形参类型到该类型的一个隐式转换。
这么解释:
比如有个类A的对象a的成员函数的参数应该是类A的对象,但是把一个别的类型B的对象b传进去了,而且这个对象b的类型恰好是A的单参数构造函数参数类型,这时系统就用这个b自作聪明的创建了一个类A的临时对象c,虽然c和a都是A类型,但是是不同的对象。
这种隐式转换有很大风险,可以用explicit加在单参数构造函数前来避免这种类类隐形转换。
我个人认为这就是一种人为使用错误,而编译器又恰恰没有指出。
本来应该A.a(A(b)),凭什么用成A.a(b)?哈哈……
#include <string>
#include <iostream>
using namespace std;
class Fruit //定义一个类,名字叫Fruit
{
string name;//定义一个name成员
string colour;//定义一个colour成员
public:
bool isSame(const Fruit& otherFruit)//期待的形参是另一个Fruit类对象,测试是否同名
{
return name == otherFruit.name;
}
void print()//定义一个输出名字的成员print()
{
cout << colour << " " << name << endl;
}
Fruit(const string& nst, const string& cst = "green") :name(nst), colour(cst) {}
Fruit() {}
};
int main()
{
Fruit apple("apple");
Fruit orange("orange");
cout << "apple = orange? : " << apple.isSame(orange) << endl;
//隐式转换
cout << "apple = \"apple \"? :" << apple.isSame(string("apple")) << endl;
return 0;
}
你会发现最后的使用上,我们用一个string类型作一个期待Fruit类形参的函数的参数,结果竟然得出了是true(1),不要感到奇怪,这就是我现在要讲的东西,隐式类类型转换:“可以用单个实参来调用的构造函数定义了从形参类型到该类型的一个隐式转换。”(C++ Primer)
首先要单个实参,你可以把构造函数colour的默认实参去掉,也就是定义一个对象必须要两个参数的时候,文件编译不能通过。然后满足这个条件后,系统就知道怎么转换了,不过这里比较严格
以前我们构造对象的时候Fruit apple("apple")其实也已经有了一个转换,从const char *的C字符串格式,转为string,在这里,你再apple.isSame("apple")的话,蠢系统不懂得帮你转换两次,所以你必须要用 string()来先强制转换,然后系统才知道帮你从string隐式转换为Fruit,当然其实你自己也可以帮他完成。 cout<<"apple = /"apple/" ?:"<<apple.isSame(Fruit("apple"));这样。Fruit apple = Fruit("apple"); //定义一个Fruit类对象apple。也就是这样转换的。不过这就叫显式转换了,我们不标出来,系统帮我们完成的,叫隐式的呗。
这里要说的是,假如你显示转换就可以不管有多少参数了,比如在前面提到的必须需要两个参数的构造函数时的例子。
int main()
{
Fruit apple("apple");
Fruit orange("orange");
cout << "apple = orange? : " << apple.isSame(orange) << endl;
//隐式转换
cout << "apple = \"apple \"? :" << apple.isSame(string("apple")) << endl;
return 0;
}
在你不想隐式转换,以防用户误操作怎么办?
C++提供了一种抑制构造函数隐式转换的办法,就是在构造函数前面加explicit关键字,你试试就知道,那时你再希望隐式转换就会导致编译失败,但是,要说明的是,显式转换还是可以进行,出于不提供错误源代码例子的原则,错误的情况就不提供了,自己试试吧
参考:[(7条消息) 隐式类类型转换_隐式转换_dingyuanpu的博客-CSDN博客]
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.1static_cast(编译时类型检查)
static_cast关键字(编译时类型检查)
用法:
static_cast < type-id > ( expression )
将 expression
转换为 type-id
类型。static_cast
是静态类型转换,发生在编译期。这种转换不会进行运行时的动态检查(RTTI),因而这种转换可能是不安全的。
它主要有如下几种用法:
(1)用于基本数据类型之间的转换,如把int转换为char,把int转换成enum,但这种转换的安全性需要开发者自己保证(这可以理解为保证数据的精度,即程序员能不能保证自己想要的程序安全),如在把int转换为char时,如果char没有足够的比特位来存放int的值(int>127或int<-127时),那么static_cast所做的只是简单的截断,及简单地把int的低8位复制到char的8位中,并直接抛弃高位。
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。例如,通过将一个运算对象强制转换成 double 类型就能使表达式执行浮点数除法:
//进行强制类型转换以便执行浮点数除法
double slope = static_cast<double>(j) / i;
当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。此时,强制类型转换告诉程序的读者和编译器:我们知道并且不在乎潜在的精度损失。一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警告信息;但是当我们执行了显式的类型转换后,警告信息就会被关闭了。
(2)把void*转换成目标类型的指针
可以使用static_cast找回存在于void*指针中的值
#include <iostream>
using namespace std;
int main(int argc, char const *argv[])
{
int i = 10;
void* v = static_cast<void*>(&i);
int* ip = static_cast<int*>(v);
return 0;
}
当我们把指针存放在void*中,并且使用static_cast将其强制转换回原来的类型时,应该确保指针的值保持不变。也就是说,强制转换的结果将与原始的地址值相等,因此我们必须确保转换后所得的类型就是指针所指的类型。类型一旦不符,将产生未定义的后果。
(3)把任何类型的表达式类型转换成void类型。显然这种转换也是不安全的,需要程序员自行保证安全转换。
int* pi = new int[10];
void* vp = static_cast<void*>(pi);
vp = pi;
(4)用于类层次结构中父类和子类之间指针和引用的转换。
①上行转换(子类到父类),上行转换时安全的。
因为static_cast的转换时粗暴的,它仅根据类型转换语句中提供的信息(尖括号中的类型)来进行转换,这种转换方式对于上行转换,由于子类总是包含父类的所有数据成员和函数成员,因此从子类转换到父类的指针对象可以没有任何顾虑的访问其(指父类)的成员。编译器认为指针的寻址范围缩小了
②下行转换(父类到子类),而下行转换时不安全的。
是因为static_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;
}
输出:
我是基类
例子2:
// dynamic_cast
#include <iostream>
#include <exception>
using namespace std;
class A
{
public:
virtual void func()
{
std::cout << "A::func()" << std::endl;
}
};
class B : public A
{
public:
virtual void func()
{
std::cout << "B::func()" << std::endl;
}
void print()
{
std::cout << "B::print()" << std::endl;
}
};
int main()
{
//上行转换,肯定是安全的
B* pb = new B();
A* pa = static_cast<A*>(pa);
pa->func();
//下行转换:
A* pa = new B();
B* pb = static_cast<B*>(pa);
pb->print();
return 0;
}
此外,对于两个不存在继承关系的两个类之间转换,总是失败的,编译器报错:
#include <iostream>
class A
{
virtual void func() {}
};
class B
{
virtual void func() {}
};
int main()
{
A* pa = new A();
B* pb = static_cast<B*>(pa);
return 0;
}
5)有转换构造函数或者类型转换函数的类与其它类型之间的转换。例如:
#include <iostream>
class Point
{
private:
double m_x;
double m_y;
public:
Point(double x, double y) : m_x(x), m_y(y) { }
Point(double& x) : m_x(x), m_y(1.1) { }
public:
operator double() const { return m_x; } //类型转换函数
void print()
{
std::cout << "m_x: " << m_x << " m_y: " << m_y << std::endl;
}
};
int main()
{
Point p1(12.5, 23.8);
double x = static_cast<double>(p1);
std::cout << x << std::endl;
Point p2 = static_cast<Point>(x);
p2.print();
return 0;
}
6)另外,与const_cast相比,static_cast不能换掉变量的const属性,也包括volitale或者__unaligned属性。
4.2const_cast
const_cast只能改变运算对象的底层const,用来移除变量的const或volatile限定符。
注意:const_cast是不能用来执行任何类型的转换的,比如只能讲const char* p 转换成char* p,而不能转成int* p。
这种类型的类型转换操作指针所指向的对象的常量,可以是要设置的,也可以是要删除的。例如,为了将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才可以对常量进行操作,一般都是用它来去除常量性,去除常量性是危险操作,还是要谨慎操作。
#include <iostream>
int main()
{
const int n = 111;
int* p = const_cast<int*>(&n);
*p = 222;
std::cout << "n = " << n << std::endl;
std::cout << "*p = " << *p << std::endl;
return 0;
}
这里需要注意:按照正常理解,n 的打印值应该是 222。但是,由于编译器的常量传播优化,std::cout<< "n = " << n << std::endl;
会被编译器替换成类似 std::cout<< "n = " << 111 << std::endl;
的语义。
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 和重载
const_cast 常常用于有函数重载的上下文中。举个例子,先看下shorterString函数:
//比较两个string对象的长度, 返回较短的那个引用
const string& shorterString(const string& s1, const string& s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
这个函数的参数和返回类型都是 const string的引用。我们可以对两个非常量的 string实参调用这个函数,但返回的结果仍然是const string的引用。因此我们需一种新的shorterString函数,当它的实参不是常量时, 得到的结果是一个普通的引用,使用const_cast可以做到这一点:
string& shorterString(string& s1, string& s2)
{
auto& r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
return const_cast<string&>(r);
}
在这个 版本的函数中,首先将它的实参强制转换成对 const的引用,然后调用了 shorterString函数的const版本。const版本返回对const string的引用,这个引用事实上绑定在了某个初始的非常量实参上。因此,我们可以再将其转换回一个普通的string&,这显然是安全的。
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),保证能够将其转换回有效的指针。
————————————————
原文链接:[(7条消息) C++类型转换:隐式转换和显式转换_c++隐式类型转换_SOC罗三炮的博客-CSDN博客]
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对象的语句分属不同文件就更是如此。
建议:避免强制类型转换
强制类型转换干扰了正常的类型检查,因此我们强烈建议程序员避免使用强制类型转换。这个建议对于reinterpret_cast尤其适用,因为此类类型转换总是充满了风险。在有重载函数的上下文中使用const_cast无可厚非;但是在其他情况下使用const_cast也就意味着程序存在某种设计缺陷。其他强制类型转换,比如static_cast和dynamic_cast,都不应该频繁使用。每次书写了一条强制类型转换语句,都应该反复斟酌能否以其他方式实现相同的目标。就算实在无法避免,也应该尽量限制类型转换值的作用域,并且记录对相关类型的所有假定,这样可以减少错误发生的机会。
4.4dynamic_cast
dynamic_cast只能用于指向类的指针和引用(或void*)。它的目的是确保类型转换的结果指向目标指针类型的有效完整对象。dynamic_cast主要用于类层次结构中父类和子类之间指针和引用的转换,由于具有运行时类型检查,因此可以保证下行转换的安全性,何为安全性?即转换成功就返回转换后的正确类型指针,如果转换失败,则返回NULL,之所以说static_cast在下行转换时不安全,是因为即使转换失败,它也不返回NULL。
将dynamic_cast用于引用时,其用法稍有不同:没有与空指针对应的引用值,也就是说没有空引用,引用必须要初始化,因此无法使用特殊的引用值来指示失败,当请求不正确时,dynamic_cast将引发bad_cast异常。
向上转换:将指向派生类的指针转为指向基类的指针,其方式与隐式转换相同。
向下转换:将指向基类的指针转为指向派生类的指针,此时的类需要是多态类(具有虚成员的类)
对于下行转换,有一点需要了解的是在C++中,一般是可以用父类指针指向一个子类对象,如parent* P1 = new Children(); 但这个指针只能访问父类定义的数据成员和函数,这是C++中的静态联编,但一般不定义指向父类对象的子类类型指针,如Children* P1 = new parent;这种定义方法不符合生活习惯,在程序设计上也很麻烦。这就解释了也说明了,在上行转换中,static_cast和dynamic_cast效果是一样的,而且都比较安全,因为向上转换的对象一般是指向子类对象的子类类型指针;而在下行转换中,由于可以定义就不同了指向子类对象的父类类型指针,同时static_cast只在编译时进行类型检查,而dynamic_cast是运行时类型检查,则需要视情况而定。下面通过代码进行说明
class Base
{
virtual void fun() {}
};
class Derived :public Base
{
};
由于需要进行向下转换,因此需要定义一个 父类类型的指针Base *P ,但是由于子类继承于父类,父类指针可以指向父类对象,也可以指向子类对象,这就是重点所在。如果 P 指向的确实是子类对象,则dynamic_cast和static_cast都可以转换成功,如下所示:
Base* P = new Derived();
Derived* pd1 = static_cast<Derived*>(P);
Derived* pd2 = dynamic_cast<Derived*>(P);
以上转换都能成功。
但是,如果 P 指向的不是子类对象,而是父类对象,如下所示:
Base *P = new Base;
Derived *pd3 = static_cast<Derived *>(P);
Derived *pd4 = dynamic_cast<Derived *>(P);
在以上转换中,static_cast转换在编译时不会报错,也可以返回一个子类对象指针(假想),但是这样是不安全的,在运行时可能会有问题,因为子类中包含父类中没有的数据和函数成员,这里需要理解转换的字面意思,转换是什么?转换就是把对象从一种类型转换到另一种类型,如果这时用 pd3 去访问子类中有但父类中没有的成员,就会出现访问越界的错误,导致程序崩溃。而dynamic_cast由于具有运行时类型检查功能,它能检查P的类型,由于上述转换是不合理的,所以它返回NULL。
#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存储在虚函数表中。
总结:
C++中层次类型转换中无非两种:上行转换和下行转换
对于上行转换,static_cast和dynamic_cast效果一样,都安全;
对于下行转换:你必须确定要转换的数据确实是目标类型的数据,即需要注意要转换的父类类型指针是否真的指向子类对象,如果是,static_cast和dynamic_cast都能成功;如果不是static_cast能返回,但是不安全,可能会出现访问越界错误,而dynamic_cast在运行时类型检查过程中,判定该过程不能转换,返回NULL。
参考:[(8条消息) C++中static_cast和dynamic_cast强制类型转换_lovemysea的博客-CSDN博客]
4.5旧式的强制类型转换
在早期版本的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一样