一、C语言支持的类型转换
C语言的类型转换分为两大类:隐式类型转换(自动转换) 和 显式类型转换(强制类型转换) 。二者的核心区别是:是否需要程序员手动编写代码触发。
1.隐式类型转换
定义:
简单理解就是编译器会根据预设规则,自动将「小类型 / 不匹配类型」转换为「目标类型」。
隐式转换优先级规则:
编译器会按照范围 / 精度从低到高 自动转换,优先级顺序:long double > double > float > 无符号长整型 > 长整型 > 无符号整型 > 整型
触发场景:
1).赋值运算转换
赋值号右侧的表达式类型,自动转换为左侧变量的类型。
例如:
cpp
int a = 3.14; // double 自动转 int,a = 3(丢失小数部分)
char b = 97; // int 自动转 char,b = 'a'
2).算数运算中类型的提示(特别小心,容易忘)
不同类型参与数学运算时,编译器会先统一类型,再计算,遵循向范围更大、精度更高的类型靠拢原则。
例如:
cpp
char x = 'a'; // ASCII 97
int res = x + 1; // x 自动从 char 转 int,res = 98
double pi = 10 / 3.0; // int → double,结果为 double 类型
注意:我们char,short类型参与运算,会自动提示至int类型,我们平常写代码小类型转大类型没关系,但是大类型强转小类型,要注意会被二进制截断。
3).函数传参
4).函数返回值
2.显式(强制)类型转换
定义:显式转换是程序员手动编写代码触发的类型转换,用于编译器无法自动转换、或需要刻意修改类型的场景。
语法:(目标类型) 表达式
核心用处:主要用于我们指针与整形、指针与指针之间的转换
例如:
cpp
int i = 1; // 1. 定义整型变量 i,值为 1
int *p = &i; // 2. 定义指针 p,存储变量 i 的**内存地址**
int address = (int)p; // 3. 把指针(地址值)强制转换成 int 整数
printf("%p,%d\n",p,address); // 4. 打印指针地址(十六进制) + 地址的十进制值
二、C++支持的类型转换
1.隐式类型转换
2.显式类型转换
由于与C语言一致,不多介绍,主要是C++是面向对象编程,所以C++有类,所以我们具体说一下C++中比C语言多支持的内置类型和自定义类型之间的转换,以及自定义类型与自定义类型之间的转换
1)内置类型与自定义之间的转换
内置类型---->自定义类型
cpp
#include <iostream>
#include <string>
class A {
public:
A(int a):_a1(a),a2(a)
{}
A(int a1,int a2):_a1(a1),_a2(a2)
{}
private:
int _a1;
int _a2;
};
int main() {
//1.隐式转换
A aa1 = 1;
A aa2 = {1,1};
return 0;
}
在上面代码中,例如A aa1 = 1发生了隐式转换,1会被隐式转换成一个临时的A对象,然后再拷贝构造给aa1(但是现在编译器基本都是直接构造了),如果我们不想隐式转换,我们可以在第一个构造函数中加上explicit
隐式转换(自定义类型--->内置类型)
cpp
#include<iostream>
#include<string>
class A {
public:
explicit A(int a):_a1(a),_a2(a)
{}
A(int a1,int a2):_a1(a1),_a2(a2)
{}
operator int()
{
return _a1+_a2;
}
//这里也可以用模板优化
//template<typename Type>
//operator Type() const {
//std::cout << "模板转换:→ " << typeid(Type).name() << '\n';
//return static_cast<Type>(_a1+_a2);
//}
private:
int _a1;
int _a2;
};
int main() {
//1.隐式转换
A aa2 = {1,1};
int i = aa2;
//2.显示转换
A aa1 = (A)1;
i = (int)aa1;
return 0;
}
operator Type()语法:
在上面的代码中我们int i = aa2;是一种隐式类型转换,我们C++语言在类中提供通过重载 operator Type()运算符 Type 是目标类型,可为内置 / 自定义类型),定义当前类对象转为其他类型的逻辑。
我们一般都用上述语法来重载,而不是Type operator()这种方式来重载,虽然我们用模板来优化,但是这个()这个运算符,即可用于函数调用,又可用于强转,所以不推荐直接重载运算符()来实现转换
而且我们一般内置类型转自定义类型,最好还是强转,虽然可以隐式转换,但是我们写代码更方便维护。
关于自定义类型与内置类型之间相互转换的显示转换代码就不展示了,与隐式一致,只不过前面加了()来显式转换,而且我们推荐使用显式转换,因为更方便维护。
注意:我们这个operator Type()如果Type是bool值的话那么会有坑的
例如:
cpp
class MyPtr {
public:
MyPtr(void* p) : ptr_(p) {}
operator bool() const { return ptr_ != nullptr; }
private:
void* ptr_;
};
int main() {
MyPtr p(nullptr);
if (p) { /* 正常:仅在bool场景使用 */ }
int x = p; // 意外:p隐式转为bool,再转成int(0或1)
}
所以这里我们最好养成一个习惯:如果Type()是bool(),则前面加上explicit
内置类型------>内置类型
同理我们的内置类型----->内置类型也需要我们的被转的内置类型,需要有一个构造函数来支持
例如:
cpp
#include<iostream>
#include<string>
class A {
public:
explicit A(int a):_a1(a),_a2(a)
{}
A(int a1,int a2):_a1(a1),_a2(a2)
{}
explicit operator int()
{
return _a1+_a2;
}
//这里也可以用模板优化
//template<typename Type>
//operator Type() const {
//std::cout << "模板转换:→ " << typeid(Type).name() << '\n';
//return static_cast<Type>(_a1+_a2);
//}
private:
int _a1;
int _a2;
};
class B
{
public:
B(int b):_b1(b)
{}
B(A &aa):_b1((int)aa)
{}
private:
int _b1;
};
int main() {
A aa1 = (A)1;
B bb = aa1;
return 0;
}
三、C++显示类型强制转换之类型安全
1.类型安全
类型安全是指编程语言在编译和运行时提供一种保护机制,避免非法的类型转化和操作,导致出现一个内存访问错误等,从而减少程序运行时的错误
C语言就不是一个类型安全的语言,C语言运行隐式类型转换,一些特殊情况下就会导致越界访问的内存错误,其次不合理的使用强制类型转换也会导致问题,例如一个int*的指针强转成double*访问就会出现越界
C++兼容C语言,支持隐式类型转换和强制类型转换,C++也不是类型安全语言,C++提出4个显示的命名转换static_cast/reinterpret_cast/const_cast/dynamic_cast就是为了让类型转换相对而言更安全(其实提出的4个显示的命名转换就是把强制类型转换分了四个类别,更规范而已)
经典的一个C/C++中非类型安全导致的问题:
cpp
#include<iostream>
using namespace std;
int main()
{
const int y=0;
int *p2 = (int*)&y;
(*p2) = 1;
//这里打印结果是1和0,也是因为我们类型转换去掉了const属性
//但是编译器认为y是const的,不会被改变,所以会优化编译时放到
//寄存器或者直接替换y为0导致的
cout<<*p2<<endl;
cout<<y<<endl;
}
如果我们这里在const int y = 0;前面加上volatile关键字,就会输出1和1,因为volatile表示每次都从内存中访问。
1.static_cast:相近类型的安全转换
核心定位 :用于语义相近、有明确定义的类型转换,编译期完成检查,无运行时开销。
适用场景:
- 基础数值类型转换(如
double↔int) - 无多态的基类与派生类指针 / 引用转换(上行,即派生类->基类)
- 有明确定义的自定义类型转换(需配合转换构造 / 类型转换运算符)
例如:
cpp
#include <iostream>
using namespace std;
class Base {};
class Derived : public Base {};
int main() {
// 场景1:基础数值类型转换
double pi = 3.14159;
int int_pi = static_cast<int>(pi);
cout << "static_cast: double(" << pi << ") → int(" << int_pi << ")\n";
// 场景2:基类指针 → 派生类指针(下行转换,编译期通过)
Base* base_ptr = new Derived();
Derived* derived_ptr = static_cast<Derived*>(base_ptr);
cout << "static_cast: Base* → Derived* 转换成功\n";
delete base_ptr;
return 0;
}
2.reinterpret_cast:内存的 "重新解释"
核心定位 :用于语义完全无关的类型转换,直接重新解释内存的二进制位模式,不修改数据本身,仅改变访问方式。
适用场景:
- 指针与整数的互转(如存储 / 打印地址)
- 不同类型指针的 "暴力转换"(如
int*↔char*) - 底层系统编程中对内存的直接操作
例如:
cpp
#include <iostream>
#include <cstdint> // 跨平台地址类型
using namespace std;
int main() {
int num = 0x12345678;
// 场景1:int* → char* 重新解释内存字节
char* byte_ptr = reinterpret_cast<char*>(&num);
cout << "reinterpret_cast: 按字节读取num的内存:";
for (int i = 0; i < sizeof(num); ++i) {
printf("%02X ", (unsigned char)byte_ptr[i]);
}
cout << "\n";
// 场景2:指针 → 整数(64位系统用uintptr_t更安全)
uintptr_t addr = reinterpret_cast<uintptr_t>(&num);
cout << "变量num的地址:0x" << hex << addr << "\n";
// 场景3:不同类型指针互转(底层内存解释改变)
float* float_ptr = reinterpret_cast<float*>(&num);
cout << "按float解释的num值:" << *float_ptr << "\n";
return 0;
}
3.const_cast:移除 const/volatile 属性
核心定位 :专门用于修改指针 / 引用的 const 或 volatile 限定符,唯一能移除 const 属性的 C++ 语法。
适用场景:
- 底层对象本身非 const,但持有 const 指针 / 引用,需要修改其值
- 兼容 C 语言的 const 参数 API
cpp
#include <iostream>
using namespace std;
int main() {
// 场景1:修改非const对象的const指针
int value = 10;
const int* const_ptr = &value;
// ❌ 直接修改会编译报错
// *const_ptr = 20;
// ✅ 用const_cast移除const属性
int* mutable_ptr = const_cast<int*>(const_ptr);
*mutable_ptr = 20;
cout << "修改后value的值:" << value << "\n";
// ❌ 危险场景:修改真正的const对象(未定义行为)
const int const_value = 100;
const int* cv_ptr = &const_value;
int* cv_mutable_ptr = const_cast<int*>(cv_ptr);
// *cv_mutable_ptr = 200; // 绝对禁止!
return 0;
}
注意:只有当底层对象本身不是 const 时,移除 const 并修改才是安全的,我们把//*cv_mutable_ptr=200这个注释去掉,去编译器运行其实可以运行的,并不会报错(可能不同编译器结果不一样,我用的vscode),所以其实我们的这个提供的类型安全,本质还需要我们程序员自己解决,这只是方便辨认。
4.dynamic_cast:多态类的安全转换
核心定位 :用于带虚函数的基类与派生类指针 / 引用的转换,运行时安全检查,失败时返回nullptr(指针)或抛出std::bad_cast异常(引用)。而且我们的dynamic_cast使用的话,我们基类一定得有多态性,因为dynamic_cast要靠虚表中的Type_info这个字段判断是基类还是派生类,从而进行转换安全的检查
适用场景:
- 基类指针 / 引用指向派生类对象时,安全转换为派生类类型
- 运行时判断对象的真实类型(RTTI)
例如:
cpp
#include <iostream>
#include <typeinfo>
using namespace std;
class Base {
public:
virtual void show() { cout << "Base::show()\n"; }
virtual ~Base() {} // 必须有虚析构函数,保证多态
};
class Derived : public Base {
public:
void show() override { cout << "Derived::show()\n"; }
void derived_func() { cout << "Derived::derived_func()\n"; }
};
int main() {
// 场景1:基类指针指向派生类对象,转换成功
Base* base_to_derived = new Derived();
Derived* derived_ptr = dynamic_cast<Derived*>(base_to_derived);
if (derived_ptr) {
cout << "场景1:转换成功,调用派生类方法:";
derived_ptr->derived_func();
}
// 场景2:基类指针指向基类对象,转换失败
Base* pure_base = new Base();
Derived* fail_ptr = dynamic_cast<Derived*>(pure_base);
if (!fail_ptr) {
cout << "场景2:转换失败,返回nullptr\n";
}
// 场景3:引用转换失败,抛出异常
try {
Base& base_ref = *pure_base;
Derived& derived_ref = dynamic_cast<Derived&>(base_ref);
} catch (const bad_cast& e) {
cout << "场景3:引用转换失败,异常:" << e.what() << "\n";
}
delete base_to_derived;
delete pure_base;
return 0;
}
5.RTTI
RTTI :Run-Time Type Information,运行时类型信息。
一句话人话:
C++ 程序运行起来之后 ,还能知道「这个指针 / 引用真实是什么类型」的机制,就叫 RTTI。
就比如我们的dynamic_cast要运行时才会检查
C++ 里依赖 RTTI 的两个核心功能:
- dynamic_cast 安全向下转型(基类指针 → 派生类指针)
- typeid 获取运行时类型信息、比较类型是否相同
dynamic_cast为什么要依赖RTTI?
原因:
- 只有带虚函数的类 ,编译器才会给它生成 虚表 + type_info 类型信息
- 程序运行时,dynamic_cast靠虚表里的 RTTI 类型信息 去判断:
- 基类指针真的指向派生类对象 → 转换成功
- 只是基类对象 → 转换失败返回 nullptr / 抛异常
没有虚函数 → 没有 RTTI 信息 → dynamic_cast 编译直接报错。
typeid介绍
typeid是 C++ RTTI(运行时类型信息) 提供的关键字
作用:在编译期或运行时获取一个类型 / 对象的类型信息 ,返回 std::type_info 类型对象。
基本语法:
cpp
// 1. 直接写类型
typeid(类型名)
// 2. 写对象、表达式
typeid(变量/对象/表达式)
例如:
cpp
#include <iostream>
#include <typeinfo>
using namespace std;
class Person {
public:
virtual void show() {} // 有虚函数 → 多态类 → 开启RTTI
virtual ~Person() {}
};
class Student : public Person { };
class Teacher : public Person { };
int main() {
int a = 10;
double b = 3.14;
// 获取类型名字符串
cout << typeid(int).name() << endl;
cout << typeid(a).name() << endl;
cout << typeid(double).name() << endl;
cout << typeid(b).name() << endl;
// 静态类型编译时判断类型是否相等
if (typeid(a) == typeid(int)) {
cout << "a 的类型就是 int" << endl;
}
Person* p1 = new Student;
Person* p2 = new Teacher;
// 动态类型运行时识别真实对象类型
if (typeid(*p1) == typeid(Student)) {
cout << "p1 实际指向 Student 对象" << endl;
}
if (typeid(*p2) == typeid(Teacher)) {
cout << "p2 实际指向 Teacher 对象" << endl;
}
delete p1;
delete p2;
return 0;
}
typeid(e)时,当运算对象不属于类类型或者是一个不包含任何虚函数的类时,typeid返回的是运算对象的静态类型,当运算对象是定义了至少一个虚函数的类的左值时,typeid的返回结果是一个动态类型,直到运行时才求得