1. C中的类型转换
在C语言中,类型转换是一个非常重要的概念,它发生在多种情况下:当赋值运算符左右两侧的变量类型不同时(如int赋值给float);当函数调用时实参类型与形参类型不匹配时;或者函数返回值类型与接收返回值的变量类型不一致时。C语言提供了两种类型转换机制:
1.1 隐式类型转换(自动类型转换)
- 由编译器在编译阶段自动完成
- 遵循特定的转换规则,按照数据类型优先级(如double > float > int > char)自动提升
- 转换失败会导致编译错误
- 示例:
int i = 10; float f = i; // 隐式将int转换为float
1.2 显式强制类型转换
- 需要程序员明确指定转换类型
- 使用强制类型转换运算符:(type)expression
- 可以用于指针类型转换等复杂场景
- 示例:
float f = 3.14; int i = (int)f; // 显式将float强制转换为int
1.3 类型转换的限制条件
转换必须具有实际意义:
- 数值类型之间可以相互转换(如int与float)
- 指针类型转换需要符合内存访问规则
- 不能随意转换毫无关联的类型(如int与结构体)
常见支持转换的类型组合:
- 整数类型之间(char, short, int, long等)
- 浮点类型之间(float, double)
- 整数与浮点类型之间
- 指针与整数类型之间(需谨慎使用)
转换可能导致的数据问题:
- 精度损失(如float转int会丢失小数部分)
- 数据溢出(如long转short可能超出范围)
- 指针类型转换可能导致非法内存访问
在实际编程中,应该谨慎使用类型转换,特别是强制类型转换,因为它可能掩盖潜在的类型不匹配问题,导致运行时错误。
int main()
{
int i = 1;
// 隐式类型转换
// 隐式类型转换主要发生在整形和整形之间,整形和浮点数之间,浮点数和浮点数之间
double d = i;
printf("%d, %.2f\n", i, d);
int* p = &i;
// 显式的强制类型转换
// 强制类型转换主要发生在指针和整形之间,指针和指针之间
int address = (int)p;
printf("%p, %d\n", p, address);
// malloc返回值是void*,被强转成int*
int* ptr = (int*)malloc(8);
// 编译报错:类型强制转换: 无法从“int *”转换为“double”
// 指针是地址的编号,也是一种整数,所以可以和整形互相转换
// 但是指针和浮点数毫无关联,强转也是不支持的
// d = (double)p;
return 0;
}
运行结果:
2. C++中的类型转换
• C++兼容C语言,因此支持C语言的所有隐式类型转换和显式强制类型转换方式。
• C++还支持内置类型与自定义类型之间的相互转换:
- 内置类型转换为自定义类型:需要通过构造函数实现
- 自定义类型转换为内置类型:需要定义operator 类型()成员函数
如:
// 内置类型和自定义类型之间
// 1、自定义类型 = 内置类型 ->构造函数支持
// 2、内置类型 = 自定义类型 ->operator 内置类型 支持
class A
{
public:
// 构造函数加上explicit就不支持隐式类型转换了
//explicit A(int a)
A(int a)
:_a1(a)
, _a2(a)
{}
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
// 加上explicit就不支持隐式类型转换了
// explicit operator int()
operator int() const
{
return _a1 + _a2;
}
private:
int _a1 = 1;
int _a2 = 1;
};
class B
{
public:
B(int b)
:_b1(b)
{}
private:
int _b1 = 1;
};
int main()
{
// 单参数的转换
string s1 = "1111111";
A aa1 = 1;
A aa2 = (A)1;
// 多参数的转换
A aa3 = { 2,2 };
const A& aa4 = { 2,2 };
// 自定义类型转内置类型
int z = aa1.operator int();
int x = aa1;
int y = (int)aa2;
cout << x << endl;
cout << y << endl;
cout << z << endl;
std::shared_ptr<int> foo;
std::shared_ptr<int> bar(new int(34));
//if (foo.operator bool())
if (foo)
std::cout << "foo points to " << *foo << '\n';
else
std::cout << "foo is null\n";
if (bar)
std::cout << "bar points to " << *bar << '\n';
else
std::cout << "bar is null\n";
return 0;
}
运行结果:
注意:单参数和多参数转换的本质,是通过隐式类型转换调用构造函数,构造一个临时对象,然后再拷贝给原对象,不过编译器优化成直接构造。
如果我们加上explicit就不支持隐式类型转换,我们这么写就会报错,因为单参数和多参数转换此时并不能通过隐式类型转换来构造,如下图所示:
• 对于自定义类型之间的转换,只需在目标类型中定义接收源类型参数的构造函数即可实现。例如,要将A类型对象转换为B类型,只需在B类中定义接收A类型参数的构造函数。
如:
class B
{
public:
B(int b)
:_b1(b)
{}
// 支持A类型对象转换为B类型对象
B(const A & aa)
: _b1(aa) // 调用aa.operator int()
{}
private:
int _b1 = 1;
};
int main()
{
// A类型对象隐式转换为B类型
B bb1 = aa1;
B bb2(2);
bb2 = aa1;
const B& ref1 = aa1; // 必须加const,引用临时对象,临时对象具有常性
return 0;
}
通过调试窗口查看结果:
3. C++显式强制类型转换
3.1 类型安全
• 类型安全是指编程语言在编译和运行时提供保护机制,避免非法的类型转换和操作,导致出现内存访问错误等,从而减少程序运行时的错误。类型安全的语言通常具有以下特征:
- 严格的类型检查机制
- 禁止或限制隐式类型转换
- 提供明确的类型转换操作符
- 运行时类型识别(RTTI)支持
• C语言不是类型安全的语言,主要表现在:
- 允许广泛的隐式类型转换,如int和指针之间的转换
- 强制类型转换(cast)操作过于自由,缺乏安全性检查
- 典型问题示例:
int a = 10; double* p = (double*)&a; // 潜在的内存访问问题 *p = 3.14; // 可能导致未定义行为
• C++虽然兼容C语言,支持隐式类型转换和强制类型转换,但为了改进类型安全性,引入了四种显式的命名强制类型转换:
- static_cast:用于基本类型转换和具有继承关系的类之间的转换
double d = 3.14; int i = static_cast<int>(d);
- reinterpret_cast:用于低级别的类型重新解释
int* p = new int(10); long addr = reinterpret_cast<long>(p);
- const_cast:用于添加或移除const/volatile限定符
const int a = 10; int* p = const_cast<int*>(&a);
- dynamic_cast:用于安全地进行多态类型转换
Base* b = new Derived(); Derived* d = dynamic_cast<Derived*>(b);
这些显式转换操作符的目的是:
- 使代码中的类型转换意图更加明确
- 限制潜在危险的转换操作
- 提供编译时和运行时的类型检查
- 提高代码的可读性和可维护性
void insert(size_t pos, char ch)
{
// 这里当pos==0时,就会引发由于隐式类型转换
// end跟pos比较时,提升为size_t导致判断结束逻辑出现问题
// 在数组中访问挪动数据就会出现越界,经典的类型安全问题
int end = 10;
while (end >= pos)
{
// ...
cout << end << endl;
--end;
}
}
int main()
{
insert(5, 'x');
//insert(0, 'x');
// 这里会本质已经出现了越界访问,只是越界不一定能被检查出来
int x = 100;
double* p1 = (double*)&x;
cout << *p1 << endl;
const int y = 0;
int* p2 = (int*)&y;
(*p2) = 1;
// 这里打印的结果是1和0,也是因为我们类型转换去掉了const属性
// 但是编译器认为y是const的,不会被改变,所以会优化编译时放到
// 寄存器或者直接替换y为0导致的
cout << *p2 << endl;
cout << y << endl;
return 0;
}
insert在0下标处插入就会出现越界,也会造成死循环
这里我们来看看y的示例运行结果:
这里打印的结果是1和0,也是因为我们类型转换去掉了const属性,但是编译器认为y是const的,不会被改变,所以会优化编译时放到寄存器或者直接替换y为0导致的
编译器默认会优化代码(如缓存变量到寄存器、省略"冗余"访问)。如果你不想编译器这么做,可以使用volatile
关键字,volatile
强制每次访问都直接从内存读取/写入,确保数据是最新值。
volatile const int y = 0;
int* p2 = (int*)&y;
(*p2) = 1;
cout << *p2 << endl;
cout << y << endl;
3.2 C++中4个显式强制类型转换运算符
1. static_cast
- 静态类型转换
用途:最常用的类型转换,用于编译时确定的类型转换
特点:
- 基本数据类型之间的转换,如将int转换为double
- 将void*指针转换为其他指针类型
- 在类层次结构中进行向上转换(派生类指针/引用转换为基类)
- 将非const类型转换为const类型
示例:
// 基本类型转换
double d = 3.14;
int i = static_cast<int>(d); // 3
// 类层次结构中的向上转换(安全)
class Base {};
class Derived : public Base {};
Derived* d = new Derived;
Base* b = static_cast<Base*>(d); // 向上转换
// 类层次结构中的向下转换(不安全!)
Base* base = new Base;
Derived* derived = static_cast<Derived*>(base); // 可能引发未定义行为
// void* 转换
int x = 10;
void* v = static_cast<void*>(&x);
int* p = static_cast<int*>(v);
注意事项:
- 不能用于去除const属性
- 不能用于无关类型指针之间的转换
- 不会进行运行时类型检查
2. reinterpret_cast
- 重新解释转换
用途:低级别的位模式重新解释
特点:
- 指针类型之间的任意转换
- 指针和整数之间的转换
- 不同类型引用之间的转换
示例:
// 指针与整数互转
int* p = new int(42);
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
// 不同类型指针互转
float f = 3.14f;
unsigned int bits = reinterpret_cast<unsigned int&>(f);
// 函数指针转换
using FuncPtr = void(*)();
auto func = reinterpret_cast<FuncPtr>(&someFunction);
// 不相关类指针转换(危险!)
class A {};
class B {};
A* a = new A;
B* b = reinterpret_cast<B*>(a); // 高风险
风险提示:
- 可能导致内存访问越界
- 最危险的转换(可能破坏类型安全)
- 平台依赖性强
- 通常用于底层编程或特殊硬件操作
3. const_cast
- 常量性移除
用途:添加或移除 const
/volatile
属性
特点:
- 去除const/volatile属性
- 添加const/volatile属性
示例:
// 移除 const
const int ci = 10;
int* modifiable = const_cast<int*>(&ci);
*modifiable = 20; // 危险!原始常量对象被修改
// 合法使用:调用非 const 重载
const std::string str = "hello";
auto& nonConstStr = const_cast<std::string&>(str);
nonConstStr.clear(); // 合法前提是 str 原本非常量
// 添加 const
int x = 5;
const int* cx = const_cast<const int*>(&x);
注意事项:
- 修改原const变量可能导致未定义行为
- 主要用于调用非const参数的旧式API
- 不能改变基础类型
4. dynamic_cast
- 动态类型转换
用途:在继承层次结构中进行安全的向下转换
特点:
- 向下转换(基类到派生类)
- 横向转换(同一层次不同派生类之间)
- 运行时类型检查
示例:
class A
{
public:
virtual void f() {}
int _a = 1;
};
class B : public A
{
public:
int _b = 2;
};
void fun1(A* pa)
{
// 指向父类转换时有风险的,后续访问存在越界访问的风险
// 指向子类转换时安全
B* pb1 = (B*)pa;
cout << "pb1:" << pb1 << endl;
cout << pb1->_a << endl;
cout << pb1->_b << endl;
pb1->_a++;
pb1->_b++;
cout << pb1->_a << endl;
cout << pb1->_b << endl;
}
void fun2(A* pa)
{
// dynamic_cast会先检查是否能转换成功(指向子类对象),能成功则转换,
// (指向父类对象)转换失败则返回nullptr
B* pb1 = dynamic_cast<B*>(pa);
if (pb1)
{
cout << "pb1:" << pb1 << endl;
cout << pb1->_a << endl;
cout << pb1->_b << endl;
pb1->_a++;
pb1->_b++;
cout << pb1->_a << endl;
cout << pb1->_b << endl;
}
else
{
cout << "转换失败" << endl;
}
}
void fun3(A& pa)
{
// 转换失败,则抛出bad_cast异常
try {
B& pb1 = dynamic_cast<B&>(pa);
cout << "转换成功" << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
}
int main()
{
A a;
B b;
//fun1(&a);
//fun1(&b);
fun2(&a);
fun2(&b);
fun3(a);
fun3(b);
return 0;
}
注意:
向下转换分为两种情况:
- 如果父类的指针(或引用)指向的是一个父类对象,那么将其转换为子类的指针(或引用)是不安全的,因为转换后可能会访问到子类的资源,而这个资源是父类对象所没有的。
- 如果父类的指针(或引用)指向的是一个子类对象,那么将其转换为子类的指针(或引用)则是安全的。
实现原理:
- 通过虚表(vtable)获取运行时类型信息(RTTI)
- 检查类型兼容性
- 调整指针位置(多重继承情况下)
- 返回转换结果或抛出异常
特殊要求:
- 基类必须包含至少一个虚函数
- 需要启用RTTI支持
- 相比static_cast有额外性能开销
最佳实践
优先使用
static_cast
:满足大部分常规转换需求慎用
reinterpret_cast
:除非绝对必要且理解所有风险避免 C 风格转换:如
(int)3.14
,它可能意外执行reinterpret_cast
类型转换前思考:是否需要转换?是否有更好的设计?
4. RTTI(Runtime Type Identification)
• RTTI的英文全称是"Runtime Type Identification",中文称为"运行时类型识别",它指的是程序在运行时期确定对象类型信息的机制。与静态类型识别不同,RTTI允许程序在运行时(而不是编译时)动态获取对象的类型信息,这在多态场景下特别有用。例如,在处理基类指针指向派生类对象时,RTTI可以帮助确定实际的对象类型。
• RTTI主要由两个运算符实现:
- typeid:用于获取对象的类型信息,返回一个type_info对象的引用
- dynamic_cast:用于在继承层次中进行安全的向下转型(downcasting),将基类的指针或引用转换为派生类的指针或引用,如果转换失败会返回nullptr(对于指针)或抛出bad_cast异常(对于引用)
• typeid运算符的详细说明:
- 语法:typeid(e),其中e可以是任意表达式或类型的名字
- 返回值:返回type_info或type_info派生类对象的常量引用
- 功能特性:
- 支持相等(==)和不等(!=)比较操作
- 提供name()成员函数,返回表示类型名称的C风格字符串
- 注意事项:
- 不同编译器对type_info的实现可能不同,导致typeid(e).name()的返回值有差异
- 例如,在GCC中可能返回带有修饰的名称,而在MSVC中可能返回更简洁的名称
- 文档参考:typeinfo官方文档
• typeid运算符的行为分析:
- 当运算对象是以下情况时,返回静态类型(编译时确定):
- 非类类型(如基本数据类型int、float等)
- 不包含任何虚函数的类类型
- 当运算对象满足以下条件时,返回动态类型(运行时确定):
- 是定义了至少一个虚函数的类的左值
- 例如:
class Base { virtual void foo() {} }; class Derived : public Base {}; Base* b = new Derived; // 这里typeid(*b)将在运行时返回Derived的类型信息
• RTTI的应用场景:
- 调试和日志记录:在调试时输出对象的实际类型信息
- 序列化/反序列化:根据运行时类型信息进行正确的对象序列化
- 对象验证:在向下转型前验证对象的实际类型
- 插件系统:动态加载的模块中识别对象类型
注意:过度使用RTTI可能表明设计存在问题,良好的面向对象设计应该尽量通过虚函数实现多态行为。
int main()
{
int a[10];
int* ptr = nullptr;
cout << typeid(10).name() << endl;
cout << typeid(a).name() << endl;
cout << typeid(ptr).name() << endl;
cout << typeid(string).name() << endl;
cout << typeid(string::iterator).name() << endl;
cout << typeid(vector<int>).name() << endl;
cout << typeid(vector<int>::iterator).name() << endl;
return 0;
}
运行结果:
// vs2019下的运行结果
int
int[10]
int*
class std::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >
class std::_String_iterator<class std::_String_val<struct std::_Simple_types<char> > >
class std::vector<int, class std::allocator<int> >
class std::_Vector_iterator<class std::_Vector_val<struct std::_Simple_types<int> > >
// gcc 9.4下运行结果
i
A10_i
Pi
NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
N9__gnu_cxx17__normal_iteratorIPcNSt7__cxx1112basic_stringIcSt11char_traitsIcES
aIcEEEEE
St6vectorIiSaIiEE
N9__gnu_cxx17__normal_iteratorIPiSt6vectorIiSaIiEEEE