类型转换
定义与作用
C++ 的类型转换分两层次:
- 隐式转换:编译器自动执行的类型变化(整型提升、派生类指针转基类指针等)
- 显式转换:程序员明确指定的类型变化
C++ 提供了四种命名的显式转换运算符来替代 C 风格的 (T)expr:
| 转换 | 用途 | 安全性 |
|---|---|---|
static_cast | 相关类型间的编译期转换 | 较安全,编译期检查 |
dynamic_cast | 多态类型的运行期安全向下转换 | 安全,运行期检查 |
const_cast | 移除或添加 const/volatile | 危险,仅用于特定场景 |
reinterpret_cast | 无关类型间的位模式重解释 | 最危险,极少使用 |
核心原理
转换选择决策树
隐式转换层级
C 风格转换 vs C++ 转换
// C 风格:(T)expr —— 什么都做,不安全
double d = 3.14;
int x = (int)d; // 可能是 static_cast
const int* p = &x;
int* q = (int*)p; // 可能是 const_cast(危险!)
// C++ 风格:明确意图
int x2 = static_cast<int>(d); // 明确:数值转换
int* q2 = const_cast<int*>(p); // 明确:去除 const(我知道这是危险的)
完整示例
示例一:static_cast 的典型场景
场景说明:展示 static_cast 在数值转换、指针转换(非多态)和显式触发隐式转换中的运用。
#include <iostream>
#include <iomanip>
class Base {
public:
void base_func() { std::cout << "Base::base_func()" << std::endl; }
};
class Derived : public Base {
public:
void derived_func() { std::cout << "Derived::derived_func()" << std::endl; }
};
void process_data(double* data, size_t size) {
std::cout << "处理 " << size << " 个数据" << std::endl;
}
int main() {
std::cout << "===== 数值转换 =====" << std::endl;
// 明确精度损失意图
double pi = 3.1415926;
int int_pi = static_cast<int>(pi);
std::cout << "pi = " << pi << ", static_cast<int>(pi) = " << int_pi << std::endl;
// 强制浮点除法(而非整型除法)
int a = 5, b = 2;
double ratio = static_cast<double>(a) / b;
std::cout << "5/2 = " << ratio << " (不会截断)" << std::endl;
// 枚举与整数互转
enum class Color : int { Red = 1, Green = 2, Blue = 3 };
Color c = Color::Blue;
int color_val = static_cast<int>(c);
std::cout << "Color::Blue = " << color_val << std::endl;
std::cout << "\n===== 指针转换(非多态) =====" << std::endl;
// 派生类指针 → 基类指针(向上转换,安全)
Derived d;
Base* bp = static_cast<Base*>(&d);
bp->base_func();
// void* ↔ T*(从 C API 取回类型)
void* vp = static_cast<void*>(&d);
Derived* dp_back = static_cast<Derived*>(vp);
dp_back->derived_func();
// 阻止危险的向下转换(非多态)
Base b;
// Derived* bad = static_cast<Derived*>(&b); // 编译通过,但运行时是 UB
// bad->derived_func(); // 访问不存在的成员 → 未定义行为
std::cout << "\n===== 与 C 风格转换对比 =====" << std::endl;
// C 风格:可能做多种转换,难以审查
// double* → int* 的 C 风格转换在编译期无法发现问题
// static_cast 则会拒绝:double* 和 int* 不是相关类型
// int* ip = static_cast<int*>(&pi); // 编译错误
return 0;
}
预期输出:
===== 数值转换 =====
pi = 3.14159, static_cast<int>(pi) = 3
5/2 = 2.5 (不会截断)
Color::Blue = 3
===== 指针转换(非多态) =====
Base::base_func()
Derived::derived_func()
逐段分析:
static_cast<int>(pi)明确表达了"我知道会截断小数"的意图static_cast<double>(a) / b将整数转为浮点以触发浮点除法,而非整数除法- 向上转换(Derived → Base)即使不写 cast 也安全,显式写出增加可读性
void*与T*之间用static_cast是合法的static_cast不允许无关类型间的转换(如double*转int*),C 风格转换则不会阻止
示例二:dynamic_cast 的安全向下转换
场景说明:形状类层次中的安全向下转换,演示 dynamic_cast 的运行期类型检查。
#include <iostream>
#include <vector>
#include <memory>
#include <cmath>
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
virtual void describe() const {
std::cout << "形状" << std::endl;
}
};
class Circle : public Shape {
public:
explicit Circle(double r) : radius_(r) {}
double area() const override {
return M_PI * radius_ * radius_;
}
void describe() const override {
std::cout << "圆形 (半径=" << radius_
<< ", 面积=" << area() << ")" << std::endl;
}
double radius() const { return radius_; }
private:
double radius_;
};
class Rectangle : public Shape {
public:
Rectangle(double w, double h) : width_(w), height_(h) {}
double area() const override {
return width_ * height_;
}
void describe() const override {
std::cout << "矩形 (宽=" << width_
<< ", 高=" << height_
<< ", 面积=" << area() << ")" << std::endl;
}
bool is_square() const {
return std::abs(width_ - height_) < 1e-9;
}
private:
double width_, height_;
};
// 仅对圆形进行特殊处理(如放大半径)
void enlarge_if_circle(Shape* shape, double factor) {
// dynamic_cast 在运行期检查实际类型
Circle* circle = dynamic_cast<Circle*>(shape);
if (circle) {
std::cout << " 放大圆形(原半径=" << circle->radius() << ")"
<< std::endl;
} else {
std::cout << " 不是圆形,跳过" << std::endl;
}
}
// 统计正方形数量
size_t count_squares(const std::vector<std::unique_ptr<Shape>>& shapes) {
size_t count = 0;
for (const auto& s : shapes) {
// 先转换为 Rectangle*,再判断是否为正方形
auto* rect = dynamic_cast<Rectangle*>(s.get());
if (rect && rect->is_square()) {
++count;
}
}
return count;
}
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(3.0));
shapes.push_back(std::make_unique<Rectangle>(4.0, 4.0)); // 正方形
shapes.push_back(std::make_unique<Circle>(2.0));
shapes.push_back(std::make_unique<Rectangle>(5.0, 3.0));
std::cout << "===== 所有形状 =====" << std::endl;
for (const auto& s : shapes) {
s->describe();
}
std::cout << "\n===== 尝试放大圆形 =====" << std::endl;
for (auto& s : shapes) {
enlarge_if_circle(s.get(), 2.0);
}
std::cout << "\n正方形数量: " << count_squares(shapes) << std::endl;
// dynamic_cast 对引用:失败抛出 std::bad_cast
Shape& ref = *shapes[0];
try {
auto& circle_ref = dynamic_cast<Circle&>(ref);
std::cout << "\n第一项是圆形,半径=" << circle_ref.radius() << std::endl;
} catch (const std::bad_cast& e) {
std::cout << "转换失败: " << e.what() << std::endl;
}
// 错误演示:对非多态类型使用 dynamic_cast
// class NonPoly {}; NonPoly np;
// dynamic_cast<int*>(&np); // 编译错误:非多态类型
return 0;
}
预期输出:
===== 所有形状 =====
圆形 (半径=3, 面积=28.2743)
矩形 (宽=4, 高=4, 面积=16)
圆形 (半径=2, 面积=12.5664)
矩形 (宽=5, 高=3, 面积=15)
===== 尝试放大圆形 =====
放大圆形(原半径=3)
不是圆形,跳过
放大圆形(原半径=2)
不是圆形,跳过
正方形数量: 1
第一项是圆形,半径=3
逐段分析:
dynamic_cast<Circle*>(shape)在运行期通过虚函数表(vtable)查询对象的实际类型- 指针版本失败返回
nullptr,引用版本失败抛出std::bad_cast dynamic_cast只能用于多态类型(至少有一个虚函数的类),这是因为它依赖 vtable 中的 RTTI 信息count_squares先用dynamic_cast<Rectangle*>过滤出矩形,再调用is_square()——这是典型的"先转换后使用"模式
示例三:const_cast 与 reinterpret_cast 的正确用法
#include <iostream>
#include <cstring>
// const_cast 的正当用途:兼容遗留 C API
void legacy_c_function(char* str) {
// 遗留代码不保证不修改,但实际上这里只读
std::cout << "C 函数收到: " << str << std::endl;
}
// 安全的包装函数
void safe_wrapper(const char* str) {
// 我们知道 legacy_c_function 实际上不修改 str
legacy_c_function(const_cast<char*>(str));
}
int main() {
// ---- const_cast ----
const char* msg = "Hello, const world";
safe_wrapper(msg);
// 危险示例:试图修改真正的 const 对象
const int ci = 42;
// int* p = const_cast<int*>(&ci);
// *p = 100; // 未定义行为!ci 存储在只读区域
// ---- reinterpret_cast ----
// 正当用途:网络编程中解析字节流
uint32_t ip_addr = 0xC0A80001; // 192.168.0.1
uint8_t* bytes = reinterpret_cast<uint8_t*>(&ip_addr);
std::cout << "\nIP 地址字节: ";
for (int i = 0; i < 4; ++i) {
std::cout << static_cast<int>(bytes[i]);
if (i < 3) std::cout << ".";
}
std::cout << std::endl;
// 注意:reinterpret_cast 不改变位模式,只改变解释方式
float f = 3.14f;
uint32_t bits = reinterpret_cast<uint32_t&>(f);
std::cout << "3.14f 的 IEEE 754 位模式: 0x"
<< std::hex << bits << std::dec << std::endl;
return 0;
}
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
对非多态类型用 dynamic_cast | 编译错误 | 改用 static_cast 或添加虚函数 |
static_cast 做不安全的向下转换 | 未定义行为 | 多态层次用 dynamic_cast |
const_cast 修改真正的 const 对象 | 未定义行为 | 仅用于兼容不修改参数的遗留 API |
reinterpret_cast 跨类型访问 | 严格别名(strict aliasing)违规 | 用 memcpy 或 std::bit_cast(C++20) |
C 风格 (T)expr 在代码审查中被忽略 | 多种转换混杂,难以识别风险 | 始终使用 C++ 命名转换 |
常见面试问题
C++ 四种命名的强制转换及其使用场景?——
static_cast(相关类型编译期转换)、dynamic_cast(多态运行期安全向下转换)、const_cast(去除/添加 cv 限定符)、reinterpret_cast(位模式重解释,极少使用)。dynamic_cast的实现原理?——依赖虚函数表中的 RTTI(Run-Time Type Information)信息,在运行期比较类型信息以确定转换是否安全。这也是为什么它只能用于多态类型。为什么推荐 C++ 命名转换而非 C 风格转换?——C 风格转换含义模糊(可能是 static/const/reinterpret 的任意组合),编译器不检查意图;命名转换明确表达目的,方便代码审查和工具搜索。
reinterpret_cast的严格别名(strict aliasing)问题?——C++ 标准规定,通过不兼容类型的指针访问同一块内存是未定义行为。reinterpret_cast不改变位模式,但后续通过该指针访问可能违反严格别名规则。安全替代方案是memcpy或 C++20 的std::bit_cast。explicit关键字在类型转换中的作用?——阻止构造函数或转换运算符被用于隐式类型转换,要求调用者显式写出转换意图。C++11 扩展了explicit到转换运算符。
小结
- 抛弃 C 风格
(T)expr,始终使用 C++ 四种命名转换 static_cast是最常用的转换:数值、非多态指针、显式触发隐式转换dynamic_cast用于多态层次的安全向下转换:指针版返回 nullptr,引用版抛异常const_cast应极少使用,仅用于兼容不修改参数的遗留 APIreinterpret_cast几乎不应出现在应用层代码中