类型转换(C++)
文章目录
1. 为什么需要类型转换
类型转换是将一个类型的对象转换为其他对象。这样做的目的是为了在不同类型之间传递信息,或者在运算过程中统一数据类型,避免错误。
C语言和C++都是强类型语言,如果=
两边的类型不同或形参接收的实参类型不匹配,以及接收返回值的类型和实际返回值类型不一样,就需要通过类型转换统一数据类型。
2. C语言的类型转换
C语言中有两种类型转换:隐式类型转换和显式类型转换。
2.1 隐式类型转换
隐式类型转换,也称自动类型转换,是指不需要用户干预,编译器默认进行的类型转换行为,使得两个变量的数据类型一致,才能进行相关的计算。它通常发生在赋值转换和运算转换两种情况。
- 赋值转换:将一种类型的数据赋值给另外一种类型的变量时,发生隐式类型转换。例如:
int x = 1.23; // 1.23是double类型,先隐式转换为int
- 运算转换:C语言中不同类型的数据需要转换成同一类型,才可以进行计算。字符型、整型、浮点型之间的变量通过隐式类型转换,可以进行混合运算(不是所有数据类型之间都可以隐式转换)。
总的来说,C语言的隐式类型转换发生在意义相近的类型之间,例如整型和浮点型家族,它们都是表示数字的规模。将一个数字转化为一个对象这样的操作,显然不是编译器应该自动做的。
2.2 显式类型转换
显式类型转换则是我们人为地进行数据类型的转换,这里可以理解为是一种强制的类型的转换。这种转换将不再遵守上面的转换规则,而是按照我们人为地标明的类型进行转换。就是在我们需要指定类型的变量前加上数据类型,并用圆括号包裹。例如:(int)a
,(float)b
,(long)c
等。
总的来说,显式类型转换有各自的意义,例如整型转换成指针类型。
2.3 特点
-
优点:在于它能够帮助我们解决一些特殊情况下的问题。例如,当我们需要将一个浮点数赋值给一个整型变量时,隐式类型转换会自动将浮点数转换为整型,使得程序能够正常运行。显式类型转换也可以用来解决一些特殊情况下的问题。
-
缺点:隐式类型转换可能会导致数据失真(精度降低),因此不一定是安全的。显式类型转换也可能会导致数据失真或其他问题。因此,在使用类型转换时应谨慎。
常见由隐式类型转换造成的错误:
// 顺序表插入函数
void Insert(size_t pos, int a)
{
size_t _size = 5; // 假设长度是5
// ...
int end = _size - 1; // 发生隐式类型转换
while(end >= pos)
{
// ... 数据挪动等操作
--end;
}
}
通常,我们用无符号整数类型
size_t
接收长度、位置等非负参数。
因为担心end如果是无符号整数,就会导致后面--end
语句在end=0时再减1后变成一个很大的整数,导致死循环,所以特地将end的类型设置为int
类型。但是int end = _size - 1
中,end会被隐式类型转换为size_t
,依然导致死循环。
虽然可以再用强制类型转换解决,int end = (int)_size - 1
,但这样补救的方式并不符合直觉,且常常容易因为类似的问题而出错。
复习:在此之前,所有的类型转换都会产生临时变量。例如:
int main()
{
int ia = 0;
double da = ia;
// ↑__□__↓
const double & rd1 = ia;
// ↑__□__↓
const double& rd2 = static_cast<double>(ia);
// ↑______□_________________↓
return 0;
}
3. C++的类型转换
C++兼容C语言,所以C语言的类型转换在C++中依然可用。
C++类型转换分为显式类型转换和隐式类型转换。隐式类型转换由编译器自动完成,这里只讨论显式类型转换。C++ 引入了四种功能不同的强制类型转换运算符以进行强制类型转换。
- const_cast: 运算符用于修改类型的 const / volatile 属性。除了 const 或 volatile 属性之外,目标类型必须与源类型相同。这种类型的转换主要是用来操作所传对象的 const 属性,可以加上 const 属性,也可以去掉 const 属性
- dynamic_cast: 在运行时执行转换,验证转换的有效性。如果转换未执行,则转换失败,表达式则被判定为 null。
3.1 static_cast
进行普通数据类型转换(如 int m=static_cast(3.14)
),用于两个相关类型之间的转换。
int main()
{
double d = 1.23;
int a = static_cast<int>(d);
cout << a << endl;
int* p = &a;
//int address = static_cast<int>(p); // error
return 0;
}
输出:
1
3.2 reinterpret_cat
可以将一个指针类型转换为另一个指针类型,即使转换前后的数据类型不同。它不检查指针类型和指针所指向的数据是否相同。它的字面意思是“重新诠释”,也就是说,它只是改变了编译器对指针的理解方式,而不改变其二进制表示。很像C的一般类型转换操作,用于两个不相关类型之间的转换。
int main()
{
int a = 1;
int* p = &a;
int adress = reinterpret_cast<int>(p);
return 0;
}
注意:在检查严格的编译器(Clion、gcc)中这段代码无法编译通过,在VS2019中是合法的。
但是这段代码有一些问题,比如:
int* p = &a;
这里p是一个指向int类型的指针,它存储了变量a的地址。int adress = reinterpret_cast<int>(p);
这里adress是一个int类型的变量,它用reinterpret_cast强制转换了p的值为int类型。这样做可能会导致数据丢失或未定义行为。
reinterpret_cast有点不讲道理,例如下面这段代码中,reinterpret_cast将有参数有返回值的函数指针转换成了无参无返回值的函数指针,并且还可以用转换后函数指针调用这个函数。
typedef void(*FUNC)();
int func(int i)
{
cout << "func: " << i << endl;
return 0;
}
int main()
{
FUNC F = reinterpret_cast<FUNC>(func);
F();
return 0;
}
输出:
func: 13504547
输出的结果是不确定的,只要删除生成的二进制文件重新编译,结果就会改变。VS编译器可以这么做:
造成结果不确定的原因是:
因为它触发了C++中的一种叫做undefined behavior的情况。undefined behavior是指C++标准没有定义或者允许编译器自由定义的程序行为。当程序遇到undefined behavior时,它可能会产生任何结果,包括正确的结果、错误的结果、异常、崩溃等。这些结果可能会随着平台、编译器、优化选项等因素而变化。在这段代码中,用reinterpret_cast将func转换为FUNC类型并调用F()就是一种undefined behavior,因为func和F的类型不匹配,而C++标准没有规定这种情况下应该发生什么。所以,它的结果是不确定的,并且可能会导致程序出错或者崩溃。
reinterpret_cast可以将任何指针类型转换为任何其他指针类型,但是这种转换是不安全的,因为它不检查转换前后的指针是否兼容。如果转换后的指针被用于调用函数,那么它必须与函数的实际类型匹配,否则可能会导致未定义行为。
在这段代码中,func函数的实际类型是int(int),也就是接受一个int参数并返回一个int值的函数。FUNC类型是void(),也就是没有参数也没有返回值的函数指针。用reinterpret_cast将func转换为FUNC类型并赋值给F变量,相当于告诉编译器把F当作一个void()类型的函数指针来处理。但是这并不改变func函数的实际类型和地址。
当调用F()时,相当于调用func(0),因为没有传递任何参数。这个调用可能会成功地打印出"func: 0"并返回0,但是这只是巧合,并不保证在所有平台和编译器上都能正常工作。因为F和func的类型不匹配,所以调用F()可能会破坏栈平衡或者引发异常。
总之,reinterpret_cast可以将有参数有返回值的函数指针转换成无参无返回值的函数指针,并且还可以用转换后函数指针调用这个函数,但是这种做法是非常危险和不推荐的。
3.3 const_cast
去除复合类型中const和volatile属性(没有真正去除)。变量本身的const属性是不能去除的,要想修改变量的值,一般是去除指针(或引用)的const属性,再进行间接修改。
用法:const_cast<type>(expression)
假设你有一个const int类型的变量a,你想通过一个int类型的指针p来修改它的值,你可以这样写:
int main()
{
const int a = 10; // 声明一个const int类型的变量a
int* p = const_cast<int*>(&a); // 使用const_cast去除a的地址的const属性,赋值给指针p
*p = 20; // 通过指针p修改a的值
cout << a << endl;
cout << *p << endl;
return 0;
}
输出:
10
20
这样就可以实现对const变量的间接修改。但是要注意,如果a是一个真正的常量(例如用#define定义),那么这样做可能会导致未定义行为。所以使用const_cast要谨慎,并且尽量避免修改原本不应该被修改的数据。
造成未定义行为的原因:
这是因为C++标准没有明确规定对一个真正的常量进行修改的行为,所以编译器可以自由地处理这种情况。有可能编译器会把常量放在只读内存中,如果你试图修改它,就会触发内存保护错误。或者编译器会对常量进行优化,直接用它的值替换所有的引用,那么你修改的只是一个临时变量,而不是原来的常量。总之,这种操作是不安全和不可预测的,所以应该避免。
为什么输出的结果不同?
这是因为使用const_cast去除了a的const属性,然后修改了它的值,这是一种未定义行为。编译器可能会对a进行优化,直接用它的值10替换所有的引用,而不是从内存中读取。
也就是说,编译器认为const变量是不会被修改的,因此会将const变量存放到寄存器中(这是一种优化),但是我们通过指针p修改的是内存中的值。所以当输出a时,是从寄存器中读取的,也就是未修改之前的10;而当输出*p时,读取的是内存中被修改后的20。这种操作是不安全和不可预测的,所以应该避免。
如果想避免编译器的优化:
使用volatile
关键字声明const变量,告诉编译器不要优化。如果将上面的代码改成:
volatile const int a = 10;
输出:
20
20
volatile关键字是一种类型修饰符,用它声明的变量表示可以被多线程或外部因素修改。编译器在遇到volatile变量时,不会对它进行优化,而是每次都从内存中读取它的值。这样可以保证变量的可见性和一致性。
volatile关键字可以用于以下类型的变量:
- 引用类型
- 指针类型(在不安全的上下文中)
- 简单类型,如sbyte, byte, short, ushort, int, uint, char, float等
- 枚举类型
- 带有一个简单类型字段的结构体
3.4 dynamic_cast
dynamic_cast作用是将一个父类对象指针(或引用)转换为子类指针(或引用)。它的格式是:
dynamic_cast<type_id> (expression)
其中:
- type_id:类的指针、类的引用或void*;
- expression:对应的指针或引用。
它会在运行时识别根据父类指针所指向的对象的真实类型来判断是否可以转换:
- 转换成功:返回目标类型的值;
- 转换失败且目标类型是指针类型:返回空指针;
- 转换失败且目标类型是引用类型:抛出一个std::bad_cast异常。
转型
dynamic_cast它可以在类层次结构中进行向上和向下转型:
- 向上转型(Up Cast)是将子类指针或引用转换为父类指针或引用,这是一种隐式转换,是语法天然支持的,也就是切割/切片(slice)不需要使用dynamic_cast。
- 向下转型(Down Cast)是将父类指针或引用转换为子类指针或引用,这是一种显式转换,需要使用dynamic_cast。dynamic_cast会检查运行时类型信息(RTTI):
- 父类指针或引用指向子类对象:转换成功;
- 父类指针或引用指向父类对象:转换失败,会返回空指针或抛出异常。
也就是说,==向下转型的对象不是真正的子类对象,就会转型失败。==是因为向下转型时,需要保证对象的实际类型和目标类型一致或者有继承关系。如果对象的实际类型和目标类型不匹配,那么就会发生类型转换异常。
例如,如果你有一个父类Fruits和两个子类Apple和Banana,那么你可以把一个Apple对象向下转型为Fruits或者Apple,但是不能把它向下转型为Banana。因为Apple和Banana没有继承关系,所以不能相互转换。
按理来说,父类转换成子类是不被允许的,原因是子类中可能有父类没有的成员,这就是内存非法访问。但实际上是一个父类指针指向子类是有可能出现的错误情况,所以要通过某种手段减少这种错误。
向下转型的安全问题
C语言风格的强制类型转换进行向下转型是不安全的,因为它可能导致运行时异常或数据丢失,也就相当于越界访问(尤其是子类有自定义变量时)。但是,C语言的强制类型转换父子类型的对象互相转换都是合法的,编译器不会帮我们检查错误。
一个C++的例子是:
class Pet
{
public:
virtual void speak() {
cout << "Pet" << endl;
}
};
class Cat : public Pet
{
public:
void speak() override {
cout << "Meow" << endl;
}
};
int main()
{
Pet* pet = new Pet(); // 创建一个Pet对象
Cat* cat = (Cat*)pet; // 强制向下转型为Cat对象,但这么做是错误的
cat->speak(); // 调用Cat的speak方法
return 0;
}
输出:
Pet
果然,这段错误的代码会编译通过,但运行时会出现未定义行为。因为Pet指针指向的对象并不是Cat类型的,所以不能把它强制转换为Cat指针。
改进方法是:先向上转型,再向下转型。例如:
Pet* pet = new Cat(); // 创建一个Cat对象,并向上转型为Pet指针
Cat* cat = (Cat*)pet; // 强制向下转型为Cat指针
cat->speak(); // 调用Cat的speak方法
这段代码就可以正常运行,因为Pet指针实际上指向的是一个Cat类型的对象,所以可以把它强制转换回来。
dynamic_cast运算符可以在运行时检查对象的实际类型是否和目标类型匹配,并返回一个合法的指针或引用。如果不匹配,它会返回空指针或者抛出异常。以此实现安全地进行向下转型。
除此之外,实现父类对象向子类对象转型,还必须要求父类具有虚函数。那是因为dynamic_cast是运行时检查类型是否是真正的子类对象,那编译器如何知道我这个子类对象是真正的子类对象呢——通过虚函数表中存储的类型信息找到真正的子类对象。
虚函数表是一种用于实现C++多态的机制,它是一个存储了虚函数地址的数组。每个包含虚函数的类或者虚继承的子类,都有一个虚函数表。每个类对象也都有一个隐藏成员,叫做虚表指针(vptr),它指向该类的虚函数表。
当编译器编译一个类时,它会为该类生成一个虚函数表,并将该类的所有虚函数地址按照声明顺序存放在其中。如果该类有基类,那么它会先复制基类的虚函数表,然后再根据自己的情况修改其中的某些项。例如,如果子类重写了某个基类的虚函数,那么子类的虚函数表中对应位置就会替换为子类自己的版本。
当创建一个对象时,编译器会为该对象分配内存,并将其vptr指向对应类型的虚函数表。当通过基类指针或引用调用一个虚函数时,编译器会根据vptr找到正确类型的虚函数表,并从中取出相应位置的地址来执行。
例子:
// 假设有一个基类Base和一个派生类Derived
class Base
{
public:
virtual void foo()
{}
};
class Derived : public Base
{
public:
void bar()
{
cout << "void bar()" << endl;
}
};
int main()
{
// 假设有一个基类指针p,指向一个派生类对象
Base* p = new Derived();
// 使用dynamic_cast将p转换为派生类指针q
Derived* q = dynamic_cast<Derived*>(p);
// 如果转换成功,q不为空,可以调用派生类的成员函数bar()
if (q)
{
q->bar();
}
return 0;
}
输出:
void bar()
这段代码演示了如何将基类指针转换为派生类指针。func函数的参数是一个基类指针pa,它试图用dynamic_cast将pa转换为派生类指针pb。如果转换成功,pb不为空,可以访问派生类的成员变量_a和_b,并对它们进行自增操作;如果转换失败,pb为空,不能访问任何成员变量。main函数中分别传入了基类对象a和派生类对象b的地址给func函数。当传入a时,转换失败,输出“转换失败”;当传入b时,转换成功,输出“转换成功”和自增后的_a和_b的值。
class A
{
public:
virtual void f()
{}
int _a;
};
class B : public A
{
public:
int _b;
};
void func(A* pa) // 参数是父类指针类型
{
B* pb = dynamic_cast<B*>(pa); // 向下转型
if(pb)
{
cout << "转换成功" << endl;
pb->_a++; // 自增继承自父类的_a
pb->_b++; // 自增子类自己的的_b
cout << pb->_a << ":" << pb->_b << endl;
}
else
{
cout << "转换失败" << endl;
// 失败pb=nullptr
//pb->_a++; // error
//cout << pb->_a << endl; // error
}
}
int main()
{
A a; // 创建基类对象
B b; // 创建子类对象
func(&a); // 传入基类对象地址
func(&b); // 传入子类对象地址
return 0;
}
输出:
转换失败
转换成功
1873164113:2
pb->_a是随机值,是因为当转换失败时,pb为空指针,它没有指向任何有效的内存地址。所以,试图访问pb->_a会导致未定义行为,可能会输出一个随机值,也可能会引发段错误。这是要检查dynamic_cast的返回值是否为空的原因。
补充:用下面的代码验证,当向下转型的对象是真正的子类时,指针的偏移是如何被矫正的:
class A1
{
public:
virtual void f() {}
};
class A2
{
public:
virtual void f() {}
};
class B : public A1, public A2
{};
int main()
{
B b;
A1* ptr1 = &b;
A2* ptr2 = &b;
cout << ptr1 << endl;
cout << ptr2 << endl << endl;
B* pb1 = (B*)ptr1;
B* pb2 = (B*)ptr2;
cout << pb1 << endl;
cout << pb2 << endl << endl;
B* pb3 = dynamic_cast<B*>(ptr1);
B* pb4 = dynamic_cast<B*>(ptr2);
cout << pb3 << endl;
cout << pb4 << endl;
return 0;
}
B类继承自A1和A2两个类。在main函数中,创建了一个B类的对象b,并将其地址分别赋给了指向A1和A2类型的指针ptr1和ptr2。然后,使用强制类型转换将ptr1和ptr2转换为指向B类型的指针pb1和pb2,并打印它们的值。接着,使用dynamic_cast进行类型转换并将结果赋给pb3和pb4,最后输出它们的值。
这段代码主要演示了多重继承以及不同类型之间的强制类型转换和dynamic_cast的使用。
输出:
0x16dccb738
0x16dccb740
0x16dccb738
0x16dccb738
0x16dccb738
0x16dccb738
这些输出的地址表明,指向A1类型的指针ptr1和指向A2类型的指针ptr2分别指向了B类对象b中不同的内存地址。这是因为B类继承自A1和A2两个类,所以它包含了两个子对象,分别对应于A1和A2。即发生了切片。
而通过强制类型转换或dynamic_cast将ptr1和ptr2转换为指向B类型的指针后,它们都指向了B类对象b的起始地址。这说明了强制类型转换和dynamic_cast可以用来在继承关系中进行类型转换。即类型转换(C语言和C++风格)能使偏移的指针被矫正。
另外:
class A1
{};
class A2
{};
class B : public A1, public A2
{};
int main()
{
B b;
A1* ptr1 = &b;
A2* ptr2 = &b;
cout << ptr1 << endl;
cout << ptr2 << endl << endl;
B* pb1 = (B*)ptr1;
B* pb2 = (B*)ptr2;
cout << pb1 << endl;
cout << pb2 << endl << endl;
return 0;
}
这段代码和上一段代码的主要区别在于,A1和A2类中没有定义虚函数。这意味着B类不再包含虚函数表指针。
由于A1和A2类中没有定义虚函数,所以B类对象b不再包含虚函数表指针。因此,当我们将B类对象b的地址分别赋给指向A1和A2类型的指针ptr1和ptr2时,它们都将指向B类对象b的起始地址。
输出:
0x16f0df74b
0x16f0df74b
0x16f0df74b
0x16f0df74b
这些输出的地址表明,指向A1类型的指针ptr1和指向A2类型的指针ptr2都指向了B类对象b的起始地址。这是因为在这段代码中,A1和A2类中没有定义虚函数,所以B类对象b不再包含虚函数表指针。
而通过强制类型转换将ptr1和ptr2转换为指向B类型的指针后,它们仍然都指向了B类对象b的起始地址。这说明了强制类型转换可以用来在继承关系中进行类型转换。
从这两个例子可以体会:dynamic_cast的功能主要是区分指针的指向。
3.5 explicit
在C++中,explicit
关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。
下面是一个简单的例子,它演示了explicit
关键字的作用:
#include <iostream>
using namespace std;
class MyClass
{
public:
int _x;
// 构造函数
explicit MyClass(int a)
: _x(a)
{}
};
int main()
{
MyClass obj1(10); // 正确:直接初始化
cout << obj1._x << endl;
// MyClass obj2 = 20; // 错误:拷贝初始化(隐式转换),被explicit阻止
// cout << obj2._x << endl;
MyClass obj3 = (MyClass)30; // 正确:强制类型转换(显示转换)
cout << obj3._x << endl;
return 0;
}
在这个例子中,我们定义了一个名为MyClass
的类,它有一个带有explicit
关键字的构造函数。这意味着我们不能使用拷贝初始化(隐式转换)来创建该类的对象。因此,下面这行代码是错误的:
MyClass obj2 = 20; // 错误:拷贝初始化(隐式转换),被explicit阻止
但是,我们仍然可以使用直接初始化或强制类型转换(显示转换)来创建该类的对象。
为什么要用explicit
关键字修饰构造函数?
对于编译器而言,语句MyClass obj2 = 20;
等价于MyClass obj2 = MyClass(20);
。这种初始化方式被称为拷贝初始化(隐式转换),它会调用类的构造函数来创建一个临时对象,然后使用拷贝构造函数将临时对象的值复制给obj2
。
对于是单参数的构造函数的对象,用括号传参的方式更加直观,像MyClass obj2 = 20;
这样的语句可读性不是很好,所以用explicit
修饰单参的构造函数,限制单参构造函数对象的构造方式。
在C++11之前,
explicit
关键字只能用于修饰只有一个参数的构造函数。但是,在C++11及以后的版本中,explicit
关键字也可以用于修饰多参数构造函数,以及转换运算符。例如,在C++11及以后的版本中,我们可以这样定义一个类:
class MyClass { public: int x; int y; // 多参数构造函数 explicit MyClass(int a, int b) : x(a), y(b) {} // 转换运算符 explicit operator bool() const { return x != 0 || y != 0; } };
在这个例子中,我们定义了一个带有两个参数的构造函数,并且使用了
explicit
关键字。此外,我们还定义了一个转换运算符,并且使用了explicit
关键字。总之,在C++11及以后的版本中,
explicit
关键字可以用于修饰多参数构造函数和转换运算符。已收到消息. 在C++11之前,
explicit
关键字只能用于修饰只有一个参数的构造函数。但是,在C++11及以后的版本中,explicit
关键字也可以用于修饰多参数构造函数,以及转换运算符。 例如,在C++11及以后的版本中,我们可以这样定义一个类:class MyClass { public: int x; int y; // 多参数构造函数 explicit MyClass(int a, int b) : x(a), y(b) {} // 转换运算符 explicit operator bool() const { return x != 0 || y != 0; } };
在这个类中,我们定义了一个带有两个参数的构造函数,并且使用了
explicit
关键字。此外,我们还定义了一个转换运算符,并且使用了explicit
关键字。 总之,在C++11及以后的版本中,explicit
关键字可以用于修饰多参数构造函数和转换运算符。在
main
函数中,我们可以使用直接初始化或列表初始化来构造MyClass
类的对象。例如:int main() { MyClass obj1(10, 20); // 直接初始化 MyClass obj2{30, 40}; // 列表初始化(C++11及以后) return 0; }
在这个例子中,我们使用直接初始化和列表初始化两种方式来构造
MyClass
类的对象。注意,由于构造函数被声明为explicit
,所以我们不能使用拷贝初始化(隐式转换)来构造该类的对象。
4. RTTI
RTTI是运行时类型识别(Runtime Type Identification)的简称,它是C++中的一种特性,用于在运行时确定对象的实际类型。RTTI主要包括三个运算符:typeid、dynamic_cast和decltype。
-
typeid:返回一个指向std::type_info类的常量对象的引用,该对象包含了类型的信息,如类型名和哈希码。typeid可以用于任何表达式,不管该表达式是否涉及多态。
-
dynamic_cast:在运行时识别出一个父类的指针(或引用)指向的是父类对象还是子类对象。
-
decltype:在运行时推演出一个表达式或函数返回值的类型。
5. 常见题目
C++中的4种类型转换分别是:____ 、____ 、____ 、____。
- static_cast、reinterpret_cast、const_cast和dynamic_cast
4种类型转换的应用场景?
- static_cast用于相近类型的类型之间的转换,编译器隐式执行的任何类型转换都可用static_cast。
- reinterpret_cast用于两个不相关类型之间的转换。
- const_cast用于删除变量的const属性,方便赋值。
- dynamic_cast用于安全的将父类的指针(或引用)转换成子类的指针(或引用)。