C++:( ͡• ͜ʖ ͡• )详解类型转换运算
目录
C风格类型转换
dynamic_cast动态类型转换
const_cast
static_cast静态转型
reinterpret_cast转型
主页🚀:R6bandito_
相关专栏推荐📰:《C++新特性》
C风格类型转换
C++完全支持C语言中的类型转换风格,因此对于类型的转换使用c风格完全没有任何问题。
意味着你可以有以下操作:
double d = 3.154;float *ptr_f = (float *) &d; std::cout<<*ptr_f<<std::endl;int i = 654;std::cout<<*(char *)&i<<std::endl;
上述代码中,我们将变量d的地址强转为float类型的指针,最后通过解一次引用来访问其内存种所指向的值并且打印数据。
同样的,我们将整型变量i的地址强转为char类型的指针,并解引用来访问其内存中的值并打印数据。
很明显,这是段错误的代码。double
类型的变量的内存布局与float
类型的变量不同,因此访问指针所指向的内存中的值是未定义的。另外,float
类型的指针ptr_f
指向的内存区域可能没有被正确对齐,这也可能导致未定义的行为。
同样的道理,int
类型的变量的内存布局与char
类型的变量不同,因此将双方指针进行强转并且访问其中的数据是一种暴力的做法。但是编译器却为此不会发出任何的抱怨。
void myfunc() {std::cout<<"myfunc"<<std::endl;
}void (*ptr_v)() = myfunc;
int (*ptr_i)() = (int (*)())ptr_v;
(*ptr_i)();
以上的操作也是被编译器所默许的。上述例子中我们声明了一个返回类型为void型,无参数传递的函数指针,并将其指向了一个具体的函数。随后进行强制类型转换将其转换为了返回为int类型的函数指针变量。
此类函数指针类型的转换,返回值的类型并不匹配,这是明显的不安全行为,却也能通过编译。
很显然,C风格的显式类型转换尽管方便,但却十分松散,安全性也大打折扣。为此,C++便引入了自身的类型转换符,更加严格地限制了类型之间地转换。
dynamic_cast动态类型转换
作为一门面向对象的编程语言,自然避免不了类对象之间的类型转换。由此衍生出了dynamic_cast
转型。
dynamic_cast< Type_Name > Expression
dynamic_cast
依赖运行时类型识别(RTTI),它可以在运行时检查转换的合法,确保转换是安全的。其用于带有继承关系的多态类型之间的类型转换。也就是说,若要使此转型成功需满足以下条件:
-
用于类对象之间,且转型的对象之间必须存在继承关系(无论是间接或是直接继承)。
-
类间必须实现多态。(基类至少含有一个虚函数)。
dynamic_cast
不能用于基本数据类型之间的转型,其是专门被设计用来处理对象指针或引用之间的类型转换的。
由于是运行时检查合法性,因此对于可能存在转型失败的情况并不会由编译器给出错误警告,而是在运行过程中,若转型失败,则返回nullptr
(若是引用,则会抛出bad_cast
异常)。
class Cat {public: virtual ~Cat() {}
};
class Bird {public: virtual ~Bird() {}
};
auto &&func2 = []() {Cat *cat = new Cat;Bird *bird = dynamic_cast<Bird *>(cat); //能通过编译.if (bird == nullptr) {std::cout<<"cast failed"<<std::endl;delete cat;}else {std::cout<<"Successful"<<std::endl;delete cat;}
};
如上代码,Cat与Bird之间没有任何关系,为两个互相独立的类。也就是说,该转型并不完全满足上述条件。但是编译器并不会给出错误。运行上述代码,执行结果如下:
cast failed
毫无疑问,转型失败。但是注意:若注释掉两个virtual函数,并且让Cat继承Bird(控制变量法,仅仅作为一个示例,虽然哈基米来继承鸟类确实很扯淡。。。😶)
将代码修改如下:
class Bird {public: //virtual ~Bird() {}
};class Cat : public Bird {public: //virtual ~Cat() {}
};
auto &&func2 = []() {Bird *bird = new Bird;Cat *cat = dynamic_cast<Cat *>(bird); //报错:运行时 dynamic_cast 的操作数必须包含多态类型.if (cat == nullptr) {std::cout<<"cast failed"<<std::endl;delete bird;}else {std::cout<<"Successful"<<std::endl;delete bird;}
};
上述代码我们以Bird作为基类,并且进行下行转换,可以看到代码又无法通过编译了。保持类间关系不动,我们将下行转换更改为上行转换:
auto &&func2 = []() {Cat *cat = new Cat;Bird *bird = dynamic_cast<Bird *>(cat); //通过编译//由派生类向基类进行转型if (bird == nullptr) {std::cout<<"cast failed"<<std::endl;delete cat;}else {std::cout<<"Successful"<<std::endl;delete cat;}
};
--Output:
Successful
又可以通过编译了,甚至转型成功。
有点迷糊了?别急,我们来总结一下:
-
dynamic_cast
主要用于安全地将基类指针或引用转换为派生类指针或引用。如果没有继承关系,dynamic_cast
将无法进行有效的转换,但编译器仍然会允许你写出这样的代码。这意味着就算没有继承关系,dynamic_cast在某些条件下是依然适用的,编译器允许你这么写,但是写上去转型是失败的,这毫无意义。 -
dynamic_cast
不仅仅依赖于虚函数表,它还可以通过其他方式来确定类型的兼容性。因此上述代码就算我们注释掉了virtual
函数,但是Cat
类是Bird
类的派生类,Cat
类的对象可以被视为Bird
类的对象。因此,即使Bird
类没有虚函数,dynamic_cast
仍然可以成功将Cat
类指针转换为Bird
类指针。 -
由于RTTI机制,
dynamic_cast
的性能开销相对于static_cast
较大。
再来看一个例子:
class Animal {public:void eat() {std::cout<<"Animal Eating"<<std::endl;}virtual ~Animal() {}
};class Dog : public Animal {public:void eat() {std::cout<<"Dog eating"<<std::endl;}virtual ~Dog() = default;virtual void bark() {}
};class Bulldog : public Dog {public:virtual ~Bulldog() = default;void eat() {std::cout<<"Bulldog eating"<<std::endl;}virtual void bark() {}
};auto &&func = []() {Animal *anml = new Animal;Bulldog *buld = dynamic_cast<Bulldog *>(anml);if (buld == nullptr) {std::cout<<"failed"<<std::endl;delete anml;}else {std::cout<<"Successful"<<std::endl;buld->bark();delete anml;}
}
上述为一个多重继承的示例。在功能函数中,我们new了一个基类Animal
对象,并将其指针通过动态转型向下转型成其派生类Bulldog
对象。执行程序,结果如下:
failed
可以看到转型失败了。上述程序完全符合动态转型的规范,为何还是转型失败返回了nulllptr
呢?失败的关键在于动态转型需要检查对象的实际类型是否与目标类型匹配,而此处没有正确的对象类型。
关于动态转型,并不是满足转型条件就一定会成功!此处的anml
是一个指向基类对象的指针,我们知道:具有继承关系的类中,基类是派生类的泛化,派生类则是基类的特殊实现。派生类中包含了基类以及自身的独特部分,每一个派生类都可以是基类的实例化,但是反之则不然。
上例Animal
类与Bulldog
类可能内存布局都不同,持有这样一个基类对象并将其向下转型为派生类对象很明显是个不安全行为,因此以安全著称的dynamic_cast
自然不会允许你这么做,尽管在语法上这没有问题。
因此我们得保证被转换的对象必须是目标类型或其派生类型的实例。
在上述代码的基础上,我们设计一个接口用于返回一个指向派生类对象的基类指针:
Animal *createBulldog() {return new Bulldog;
}
auto &&func = []() {Animal *anml = createBulldog();Bulldog *Pinv = dynamic_cast<Bulldog *>(anml);if (Pinv == nullptr) {std::cout<<"failed"<<std::endl;delete anml;}else {std::cout<<"Successful"<<std::endl;Pinv->eat();delete anml;}
}
--Output:
Successful
Bulldog eating
修改过后,接口处返回的是一个基类的指针,但是这个指针实例化的对象是其子类的子类 Bulldog类对象。因此可以安全地进行转型。
-
1.总的说来,
dynamic_cast
是c++中所提供的动态类型转换,运用RTTI机制在程序运行时对类型进行转换推导。2.其只能用于类对象之间地转型,而不能用于基本数据类型之间地转换。
3.若不能够安全转换则返回
nullptr
。4.通常用于下行转换,转换时请确保对象的类型是正确的,且只有在类间含有虚函数情况下才会触发RTTI机制。
5.由于RTTI机制的存在,运行开销较静态转型
static_cast
高。
const_cast
顾名思义,该转型用于临时去除CV限定符的保护,以实现某些特定功能(如函数调用)。
const_cast< Type_Name > Expression
多的不说,直接以代码来切入:
auto &&func = []() {const int cVal = 45;int *pInv = const_cast<int *>(&cVal);*pInv += 100;std::cout<<*pInv<<std::endl;
};
上述示例,我们定义了一个局部常变量cVal
,理论上,常量的值是不允许修改的,但是此处我们使用const_cast转型去除掉了其const的保护,因此才使得 *pInv += 100;
该语句合法合规的出现而不引起编译器的抱怨。执行结果如下:
145
看起来是成功修改了?紧接着再来看下面这段代码
void change(const int* ptr,int value);
auto &&func2 = []() {const int cVal = 60;change(&cVal,50);std::cout<<"The cVal is: "<<cVal<<std::endl;
};
void change(const int* ptr,int value) {auto pInv = const_cast<int *>(ptr);*pInv -= value;
}
int main() {func2();return 0;
}
有了前车之鉴,输出10已是必然,你怎么认为呢?别急着下定论,先来看看结果:
The cVal is: 60
咦?尽管两段代码着实很相似,但是运行结果却大相径庭,什么问题?在此处对于这个问题,其实我也没有确切的答案。但可以确定的是,我们都知道在C++中,常量是不能被修改的,尝试对常量进行修改会导致未定义的行为。
第一段代码之所以能够输出正确结果,其实可能只是其中一种可能的表象。这可能是来自于编译器的“杰作”,也有可能是在内存模型中,某些特殊情况下cVal
的内存并未被保护,也有可能是运行环境的问题。
总而言之,输出正确结果并不意味着就是稳定安全的,也有可能是未定义行为的歪打正着。至于第二个示例的问题,目前我唯一能想到的就是由于const的保护性,尽管在change中修改了*pInv
的值,但是被选择性的进行了忽略,cVal
仍然保持初始值。(这两个示例是一个至今仍困惑着我的问题,在这里便一起分享给各位,也希望高人能够为其解惑,感激不尽)。
但是可以明确,修改const常量是未定义的行为,因此不要使用const_cast
转型来试图对已经初始化了的常量进行修改,这绝对不是一个好事。
const_cast
转型允许移除CV限定符,但是不允许在转型的同时更改原有的类型,也就是说,在上述语法表达式中:
-
Type_Name
与Expression
类型必须相同,否则转型会失败。
class Base {};
class Derived : public Base {};
auto &&func3 = []() {Base base;const Base *pbase = &base;Base *ptr_b = const_cast<Base *>(pbase); //valid.
};
上述示例中,将指向Base的常量指针移除const限定符。这是合法的。
class Base {};
class Derived : public Base {};
auto &&func3 = []() {Base base;const Base *pbase = &base;Derived *ptr_b = const_cast<Derived *>(pbase); //invalid. const_cast不支持改变原类型
};
上例中,不仅剔除CV限定符,还更改了原类型,不能通过编译。
const_cast
转型使用C风格转型也能实现,甚至C风格能够在剔除CV限定符的同时转变其原类型。但是正由于此特性,使得用户可能无意间同时改变类型和常量特征。因此为了更加安全,最好使用c++的const_cast来进行相关转型。以下示例为上述代码的C风格实现:
auto &&func3 = []() {Base base;const Base *pbase = &base;Base *ptr_b = (Base *) pbase; //valid.old type(C风格转换).Derived *ptr_b = (Derived *) pbase; //also valid.old type,不建议.
};
最后给大家提供一种运用的场景,仅供参考。
类中常成员函数与普通成员函数的相互调用,以提高代码复用。
class Regualar_Point {public:Regualar_Point(int x, int y) : x(x), y(y) {}const int getX() const {/*执行一些条件约束检查*/return this->x;}int getX() {/*同样执行一些条件约束检查*/return this->x;}private:int x;int y;
};
上述示例中,为常对象与普通对象各准备了一份getX
实现,但是可以发现两段代码除了返回值的特征其余基本是一致的。如果我们分别为两段代码各写一份代码实现,那就十分臃肿且没有任何必要,因此直接使用转型进行修改,令普通函数直接调用其常量成员函数:
class Regualar_Point {public:Regualar_Point(int x, int y) : x(x), y(y) {}const int& getX() const {std::cout<<"const getX called"<<std::endl;/*执行一些条件约束检查*/return this->x;}int& getX() {return const_cast<int &>(static_cast<const Regualar_Point &>(*this).getX());}private:int x;int y;
};
int main() {Regualar_Point poin1(1,2);const Regualar_Point poin2(3,4);poin1.getX();poin2.getX();return 0;
}
--Output:
const getX called
const getX called
上述代码中,我们让非常量成员函数去调用常成员函数之中的实现,进而实现一个代码复用(好吧,我承认这种写法确实有点难读且抽象@_@)。
我们来分解一下上面的转型过程:
首先,我们将*this
通过静态转型将其加上了const
限定符,从而能够调用const
中的实现。而后又通过 const_cast<int&>
,移除了 const
限定符,从而返回一个非 const
的 int&
。
仅仅当做参考吧,为了代码更加易读不采用这种复杂的转换复用也可以。
小结:
const_cast
只能用于移除指针或引用的const
或volatile
修饰符,而不能用于移除对象本身的const
修饰符。
const_cast
常用于一些函数接口的实现,而不是给你提供一种途径通过const_cast
移除CV限定符来修改常量值,这是未定义行为。
const_cast
转型只能剔除CV限定符,不能连带修改其原类型。倘若真想这么做请使用C风格类型转型。
static_cast静态转型
static_cast
不同于dynamic_cast
,静态转型是一种编译期时的类型转换(动态转型则是在运行期进行类型的转换检查并且转换),因此静态转型的开销相对于动态较少。
static_cast
主要用于已知安全的隐式或显式的类型转换,例如:
-
基本数据类型之间的转换(
int,double,float
)。 -
或者是在类的层次结构中进行类型转换(类的上下行转换)。
-
以及可以删除对类的层次结构(将某类指针转换为通用指针
void *
,当然也可以将通用指针指向某个类)。
同样,以下列具体代码来进行说明:
class Base {};
class Derived : public Base {};
class Unrelated {};
auto &&func = []() {Base base;Derived *ptr_der = static_cast<Derived *>(&base); //下行转型Base *ptr_bas = static_cast<Base *>(ptr_der); //上行转换//Unrelated *ptr_unr = static_cast<Unrelated *>(ptr_bas); //既不属于上行转换,也不属于下行,转换无效
};
-
上述代码我们给出了三个类,其中
Base
与Derived
之间具有公有继承关系。使用静态转型可以在此类具有继承关系的类中进行转型。无继承关系的两类之间是无法互相转换的(这不难理解,内存布局不同的两个类相互转换会引发一系列问题,也没有任何意义)。
看到这,可能会有这样的疑惑:既然static_cast
可以达到dynamic_cast
的效果,那为什么不直接使用静态转型呢?开销还小一点,这绝对是笔划算的买卖。
问题正是出在static
与dynamic
这两个词上,static
是在编译期确定其类型,因此如果转换不安全,没有任何机制来保证类型正确性。而dynamic
使用RTTI机制,虽然牺牲了部分性能,但是在运行期的检查能够大大提升转型的安全性。
如上述代码所示,我们创建了一个Base
类的栈对象,将其进行下行转换为其派生类Derived
。这是一个十分危险的操作,尽管编译器将你成功放过去。
将代码修改如下你就明白为什么危险了:
class Base {public:int iVal;Base(int i = 0) : iVal(i) {}
};
class Derived : public Base {public:using Base::Base;float fVal;Derived(int i = 0,float f = 5.0) : Base(i), fVal(f) {}
};
auto &&func = []() {Base base;Derived *ptr_der = static_cast<Derived *>(&base); //下行转型std::cout<<ptr_der->iVal<<","<<ptr_der->fVal<<std::endl;
};
--Output:
0,-2.18189e-20
通过执行结果可以看到,我们通过转型后的派生类对象指针访问其数据成员fVal
发生了错误。这是很显然的嘛,我们手里持有的是指向基类对象的指针,而却将其化为指向内存布局与其可能完全不一样的派生类对象,这肯定会出错。这种下行转换的问题在上面我们已经讨论过了,这便是static_cast
的一个弊端。
-
除了可以对类指针和引用进行处理,
static_cast
也常用于基本类型间的转换。
auto &&func = []() {int a;size_t k;float f = static_cast<float>(a); //validuint8_t num = static_cast<uint8_t>(k); //validint *ptr_i = static_cast<int *>(&a); //valid
};
以上转型都没有任何问题。但static_cast
不允许出现下列谜之操作:
int ii_num = static_cast<int>(ptr_i); //报错!invalid
试图将一个整型指针转换为int
数据类型?很明显的未定义行为,怎么可能放你过去?别笑,这种操作放在c风格转型里面是完全可以通过编译的:
int ii_num2 = (int) ptr_i; //valid
这也是为什么c++风格的新式转型要比c风格转型安全得多得多的原因所在。
-
同样不同于C风格转型,
static_cast
不允许在函数指针上执行类型间转换操作,哪怕看起来是合理的。
/*函数指针类型的转换static_cast不适用于函数指针类型之间的转换*/void *(*ptr_func)();int *(*ptr_func2)() = static_cast<int *(*)()>(ptr_func); //invalidint *(*ptr_func3)();void *(*ptr_func2)() = static_cast<void *(*)()>(ptr_func3); //invalid
};
上例中,两处转换均为错误转换,无法通过编译器! 但是很遗憾,C风格也允许你这么做。。。
void *(*ptr_func)();
int* (*ptr_func_ret)() = (int* (*)()) ptr_func; //valid
至于运行后会发生什么,我也不知道。
-
可以向通用型指针进行转型。
auto &&func = []() {Base base;Derived *ptr_der = static_cast<Derived *>(&base); //下行转型void *ptr_com = static_cast<void *>(ptr_der); //允许转换为通用型指针Base *ptr_2bas = static_cast<Base *>(ptr_com); //也允许从通用型指针转换为具体的类型指针
};
小结:
static_cast
不同于dynamic_cast
,其是一种编译时期进行类型转换的转型方式。
static_cast
用于已确定安全的隐式或显式类型转换,常用于基本类型之间的转换。
static_cast
可用于具有继承关系的类间对象的引用或指针类型转换,但是并不保证安全性。
static_cast
可以在通用型指针(void *
)之间进行转型。
static_cast
不可以用于函数指针之间的类型转换。
reinterpret_cast转型
reinterpret_cast
是一种低级的、直接的位转换。它允许将指针或引用解释为不同类型的指针或引用,主要用于不同类型间的位级别转换。
它不会进行任何数据的实际修改或检查,仅仅是告诉编译器把一个数据类型看作另一个数据类型。因此,它是一种非常底层且危险的转换。
struct dat {short a;short b;
};
auto &&func2 = []() {unsigned long long ull = 0xA224A224A224A224;dat *ptr_d = reinterpret_cast<dat *>(&ull);//dat *ptr_d = static_cast<dat *>(&ull); //编译不通过!类型转换无效std::cout<<"a:"<<ptr_d->a<<",b:"<<ptr_d->b<<std::endl;
};
如上述示例代码。ull为无符号长长整数类型,而ptr_d
是一个指向dat
的指针。这在static
看来根本毫无逻辑,不可能发生的转型竟然可以通过reinterpret_cast
联系在一起,其运行结果也可想而知:
a:-24028,b:-24028
再如下列示例:
auto &&func2 = []() {unsigned long long ull = 0xA224A224A224;/*以下转型均为错误的:地址和值相互转换是没有意义的.即使在一些平台上,地址可以用整数来表示,但是这种转换仍然是未定义的,因为它依赖于具体的平台和编译器。*/int ptr_i = reinterpret_cast<int>(&ull);int ptr_i = (int) &ull; //C风格强制转型,依然是未定义的。char *ch;char ch1 = reinterpret_cast<char>(ch);
};
将地址转型成值,这种毫无意义且不安全的转型仍然可以使用reinterpret_cast
发生。
小结:
最不安全的转换类型。
可以将任意指针类型转换为其他指针类型,比如将
int*
转换为char*
,也可以将指针转换为整数类型(比如uintptr_t
)以便进行指针运算。
reinterpret_cast
在大部分情况下和C的强制类型转换效果一样,你可以完全可以使用C风格进行代替。但在跨平台或跨编译器环境中,reinterpret_cast
可能具有更好的兼容性和可移植性。同时这种方式在代码中相比起C风格的转型,能更清晰表明用户所要“重新解释”内存内容的意图。
🌹🌹🌹(*^_^*)