C++学习笔记————类型转换Casting Operator
C++可用Casting Operator手动指定变量类型的转换,使得编译和运行时类型转换更加安全,对变量类型的转换可读性更高。
转换分为四种:
- const_cast< type >( expression )
- reinterpret_cast< type >( expression )
- static_cast< type >( expression )
- dynamic_cast< type >( expression )
const_cast:
const_cast仅用于去除const关键字,而且const_cast只接收指针和引用类型,不能将const基础类型的变量转换为基础类型。
总之,const_cast有两个主要功能:
- 常量指针被转化成非常量的指针,并且仍然指向原来的对象。
- 常量引用被转换成非常量的引用,并且仍然指向原来的对象。
因为无法直接将常量类型赋值给非const型的变量,所以需要先转换后再赋值,这也是const_cast的主要使用点。
其主要目的并非真的改变原变量的const属性,而是相当于提供了一个接口(指针或引用)让你有机会去修改常量类型的值。
const_cast同样也可以给非常量类型加上const修饰字。
总的来说,建议尽量不要使用const_cast。
const基本类型引用:
using namespace std
int main(){
const int i = 111;
int &ii = i; //编译错误,无法直接将const常量赋值给非const变量
const int &a = i;
int &b = const_cast<int &>(i);
int &c = const_cast<int &>(a);
a++; //编译错误,a是const int&型,不可改变其指向的值
b++;
cout << b << endl; //结果为112
c++;
cout << c << endl; //结果为113
//说明b,c引用的是同一个变量
return 0;
}
类的const对象转换为非const对象:
class A {
public:
A(const char *str) :value(a) {};
const char *value;
};
int main() {
const A a("hello");
a.value = "world"; //编译错误,a为const A型,不可改变内部成员的值
A *b = const_cast<A *>(&a);
//使用const_cast提供了非const的A类指针,开放了可以更改常量A类对象的接口
b->value = "world"; //运行成功
cout << b->value << endl; //输出“world”
cout << a.value << endl; //输出“world”,a内成员变量值被成功修改
return 0;
}
const_cast引出的常量重叠:
const int i = 111;
cout << (int)&i << endl;
const int *pi = &i;
int *upi = const_cast<int *>(pi);
*upi = 222;
cout << i << endl << *pi << endl << *upi << endl;
cout << (int)&i << endl << (int)pi << endl << (int)upi << endl;
运行结果:
很奇怪,整型常量i明明有内存地址且与pi和upi两个指针所指向的地址一样,为什么在输出时还是保留了原本的值?
经过思考和查询,这涉及到了C++相比于C的优化机制——常量折叠,日后继续研究。
reinterpret_cast:
reinterpret_cast是四种强制转换中功能最为强大的,它可以暴力完成两个完全无关类型的指针之间或指针和数之间的互转,比如用char类型指针指向double值。它对原始对象的位模式提供较低层次上的重新解释(即reinterpret),完全复制二进制比特位到目标对象,转换后的值与原始对象无关但比特位一致,前后无精度损失。
简单来说就是reinterpret_cast可以强制将一个指针类型与另一个不同类型的指针或整型数或枚举进行转换,转换后内存中的bit位完全不变。
reinterpret_cast适用范围:
- 从指针类型到一个足够大的整数类型
- 从整数类型或者枚举类型到指针类型
- 从一个指向函数的指针到另一个不同类型的指向函数的指针
- 从一个指向对象的指针到另一个不同类型的指向对象的指针
- 从一个指向类函数成员的指针到另一个指向不同类型的函数成员的指针
- 从一个指向类数据成员的指针到另一个指向不同类型的数据成员的指针
int i = 111;
int *pi = &i;
char *pc = reinterpret_cast<char *>(pi);
cout << (int)pi << endl << (int)pc << endl;
//指向地址相同,但指针类型不同
cout << *pi << endl << *pc << endl;;
//pi会输出int型,pc会输出char型,内存中是同一段数据,111的整型数在ASCII中为字符o
int ii = reinterpret_cast<int>(pi);
cout << ii << endl;
运行结果:
由此可以看出reinterpret_cast是一个功能强大的无视类型强转指令,但是无脑使用也会出现错误:
typedef int (*FunctionPointer)(int);
int value = 21;
FunctionPointer funcP;
funcP = reinterpret_cast<FunctionPointer> (&value);
funcP(value);
以上代码可以编译通过没有错误,但是运行时就会发生错误,因为虽然funcP被转换为了一个函数指针,但是funP指向的地址根本不是函数入口,而是value内存中存储的二进制数据。
由此可知,reinterpret_cast虽然看似强大,作用却没有那么广。IBM的C++指南、C++之父Bjarne Stroustrup的FAQ网页和MSDN的Visual C++也都指出:错误的使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式。
reinterpret_cast不可像const_cast一样消除const符。
static_cast:
static_cast也类似C语言中的强制转换,主要应用为:
- 基本类型间的互相转换(非指针),如 int 型转 char 型或枚举类型等。
- 任意基本类型与 void* 指针的相互转换。
- 基类对象与派生类对象的指针与引用间的相互转换。(up_cast安全,down_cast不安全)
- 任意表达式到 void 类型的转换。
static_cast 转换仅在编译时检查转换语法是否正确,不会在运行时进行类型检查,所以不是类型安全的转换。
举例,在代码中,可以使用 static_cast 对一个基类 Father 和其派生类 Son 的对象指针进行相互转换而不报错。在运行时,如果是将 Son 指针强制转换为 Father 指针使用,调用 Father 类内声明的成员变量和方法没有任何问题。但是反过来,将 Father 对象的指针强制转换为 Son 类型,代码语法和逻辑上也没有错误,所以编译可以通过,但是这时如果要调用 Son 类内声明的不同于 Father 类内的派生类内成员,则会导致错误。
class F {
public:
F() {};
};
class S :F {
public:
int a;
};
int main() {
F f;
F *pf = &f;
S *s = static_cast<S *>(&f);
cout << s->a << endl;
return 0;
}
这里编译和运行都不会出错,但是输出 s->a 时,会出现一个随机值,即使我们没有去初始化 a,这在工程中就可能导致不可控的后果。
所以,如果要使用static_cast进行有关联的类对象的指针或引用转换时,应该只进行上行转换。
static_cast在转换时不能去除被转换类型的 const,volatile 等属性。
同时,因为static_cast是粗暴的强制转换,非类型安全,所以不止是在下行转换中可能出错,在其他类型的转换中也有可能出错:
int i = 1;
void *p = &i;
char *pc = static_cast<char *>(p);
*pc = 2;
std::cout << i << endl;
输出 i 的结果毫无疑问是2,没有问题,但是还有别的情况:
int i = 256;
void *p = &i;
char *pc = static_cast<char *>(p);
*pc = 2;
std::cout << i << endl;
此时输出 i 的结果不会是2,而是258,不是我们预期的结果。
造成这种错误的原因在于不同数据类型的长度不同,int 型有4字节32位,char 型则是1字节8位,当 int 型的值为256时,从低到高的第九位是1,低八位都是0,而static_cast强制转换为 char *型后,指针指向的是一个 char 型空间,即原 int 型的低八位,此时通过 char *指针改变变量的值,只会改变低八位,造成最终结果 i 的值变为 256 + 2 = 258 。
int型初始值256: 00000000 00000000 00000001 00000000
char *型指向后八位: 00000000
char *型改变值为2: 00000010
最终输出int型值258: 00000000 00000000 00000001 00000010
这种错误感觉如果在实际应用中发生,也是很难排查。
感觉static_cast本质跟强制转换没什么不同,可能多了一个编译期检查,其实还是暴力转换。
dynamic_cast:
dynamic_cast主要用于有关联的类之间的指针和引用转换,包括上行,下行以及交叉转换。
dynamic_cast由于自带动态类型检查,对照static_cast来看是类型安全的,所以使用的时候更加安全清晰,不易出错。
dynamic_cast在转换时,被转换类型和转换类型必须要有关联(公有派生类或公有基类或就是本身的类型),只能转换指针和引用,且转换中的基类必须为多态类型(至少有一个虚函数)。
class Father {
public:
virtual void func(){};
};
class Son :public Father {
public:
int a;
void func() override {};
};
int main(){
Father *f = new Father();
Son *s = dynamic_cast<Son *>(f);
}
上述代码会报错,因为dynamic_cast 是类型安全的,而直接从基类指针转换为派生类指针是很有可能出错的(理由在上面说过了),所以dynamic_cast 在转换时,如果是下行转换,会首先判断基类指针指向的对象,如果转换前基类指针指向的是派生类对象,则说明这次的下行转换是完全安全的,等于是将转换后的派生类指针指向了一个派生类对象,没有任何问题,否则就会编译不通过。
class Father {
public:
void func(){};
};
class Son :public Father {
public:
int a;
};
int main(){
Father *f = new Son();
Son *s = dynamic_cast<Son *>(f);
}
这里虽然基类指针指向的是一个派生类对象,但是还是会有编译错误,因为基类中并未包含至少一个虚函数,所以不能用dynamic_cast 进行转换,会报错基类不是多态类型。
正确使用方法为:
class Base{
public:
virtual void fun(){}
};
class Drived : public base{
public:
int i;
};
Base *Bptr = new Drived();
Derived *Dptr2 = dynamic_cast<Derived *>(Bptr);
dynamic_cast还有交叉转换的情况:
class Base1{
virtual void f1(){}
};
class Base2{
virtual void f2(){}
};
class Derived: public Base1, public Base2{
void f1(){}
void f2(){}
};
Base1 *pD = new Derived;
Derived *pD1 = dynamic_cast<Derived*>(pD);
Derived *pD2 = static_cast<Derived*>(pD);
Base2 *pB1 = dynamic_cast<Base2*>(pD);
Base2 *pB2 = static_cast<Base2*>(pD); //编译错误
当从 pD 转换到 pB1时,虽然Base1 和Base2 这两个类没有关联,但 pD 指针实际指向的是一个派生类对象,派生类对象同时继承 Base1 和 Base2 类,所以可以使用 dynamic_cast 将 Base1 *型指针 pD 转换为 Base2 *型指针 pB1 。
而下一行的 static_cast 则会出现编译错误,因为 static_cast 不存在运行时的动态类型检查,编译过程中仅判定为 Base1 * 到 Base2 * 的转换,Base1 和 Base2 类没有关联,所以无法转换,编译出错。