【C/C++】C++ 中的类型转换
C++ 中的类型转换
C++ 提供了四种命名转换(named cast)和从 C 继承的旧式转换。它们的能力和安全性各不相同。这篇文章逐一讲清楚每种转换的用途、原理和陷阱,每段都配有可以直接编译运行的代码。
一、static_cast —— 最常用的安全转换
static_cast 执行的是编译期就能确定合理性的类型转换。它覆盖了日常开发中九成以上的场景:数值类型之间的转换、枚举与整数的互转、继承体系中的上行/下行转换、以及 void* 到具体指针的还原。
它的核心特点是:编译器会做合理性检查,但不会生成运行时检查代码。如果你对一个基类指针做 static_cast 向下转换到派生类,编译器信任你——如果实际类型不对,就是未定义行为(undefined behavior),程序不会报错,只会默默出错。
// static_cast_demo.cpp
// compile: g++ -std=c++17 -o static_cast_demo static_cast_demo.cpp
#include <iostream>
#include <cstdint>
enum class Color { Red, Green, Blue };
class Animal {
public:
virtual ~Animal() = default;
virtual void speak() { std::cout << "..." << std::endl; }
};
class Dog : public Animal {
public:
void speak() override { std::cout << "Woof!" << std::endl; }
void fetch() { std::cout << "Fetching stick!" << std::endl; }
};
int main() {
// 1) 数值转换:double -> int,截断小数部分
double pi = 3.14159;
int n = static_cast<int>(pi); // n = 3
std::cout << "pi truncated: " << n << std::endl;
// 2) 枚举 <-> 整数
// scoped enum (enum class) 不允许隐式转换,必须显式 cast
Color c = Color::Blue;
int ci = static_cast<int>(c); // ci = 2
Color c2 = static_cast<Color>(1); // c2 = Color::Green
std::cout << "Blue = " << ci << std::endl;
// 3) void* -> typed pointer
// malloc 返回 void*,C++ 中需要 cast(C 中不需要)
void* raw = new int(42);
int* ip = static_cast<int*>(raw);
std::cout << "value: " << *ip << std::endl;
delete ip;
// 4) 继承体系中的下行转换(downcast)
// 编译器不生成任何运行时检查 —— 你必须自己保证类型正确
// NOTE: if base doesn't actually point to Dog, this is UB (undefined behavior)
Animal* animal = new Dog();
Dog* dog = static_cast<Dog*>(animal); // no runtime check
dog->fetch();
delete animal;
return 0;
}
使用原则很简单:凡是"逻辑上说得通"的转换,首选 static_cast。它比 C 风格转换安全,因为它不会悄悄退化成 reinterpret_cast,也不会绕过访问控制。
二、dynamic_cast —— 带运行时检查的安全下行转换
在实际项目中,你经常拿到一个基类指针或引用,但不确定它背后的真实类型。static_cast 在这种情况下是赌博——猜错了就是 UB。dynamic_cast 是安全的替代方案:它在运行时通过 RTTI(Run-Time Type Information,运行时类型信息)查询对象的真实类型,只有匹配才返回有效指针。
前提条件:基类必须是多态的(polymorphic),即至少有一个 virtual 函数。 没有虚函数的类没有 vtable,也就没有 RTTI 信息可查。编译器会直接报错。
对指针做 dynamic_cast,失败时返回 nullptr;对引用做 dynamic_cast,失败时抛出 std::bad_cast 异常——因为不存在"空引用"这回事。
// dynamic_cast_demo.cpp
// compile: g++ -std=c++17 -o dynamic_cast_demo dynamic_cast_demo.cpp
#include <iostream>
#include <typeinfo> // for std::bad_cast
class Shape {
public:
// 至少一个 virtual 函数,使类成为 polymorphic
// without this, dynamic_cast won't compile
virtual ~Shape() = default;
virtual double area() const = 0;
};
class Circle : public Shape {
double r_;
public:
Circle(double r) : r_(r) {}
double area() const override { return 3.14159 * r_ * r_; }
double radius() const { return r_; }
};
class Rectangle : public Shape {
double w_, h_;
public:
Rectangle(double w, double h) : w_(w), h_(h) {}
double area() const override { return w_ * h_; }
double width() const { return w_; }
double height() const { return h_; }
};
void describe(Shape* s) {
// 尝试转换为 Circle*
// runtime check: examines the vtable / type_info of *s
if (Circle* c = dynamic_cast<Circle*>(s)) {
// 成功:s 确实指向 Circle
std::cout << "Circle, radius=" << c->radius()
<< ", area=" << c->area() << std::endl;
}
// 尝试转换为 Rectangle*
else if (Rectangle* r = dynamic_cast<Rectangle*>(s)) {
std::cout << "Rectangle, " << r->width() << "x" << r->height()
<< ", area=" << r->area() << std::endl;
}
else {
std::cout << "Unknown shape, area=" << s->area() << std::endl;
}
}
void describe_ref(Shape& s) {
// 引用版本:失败时抛出 std::bad_cast,不返回 nullptr
try {
Circle& c = dynamic_cast<Circle&>(s);
std::cout << "It's a circle (ref), radius=" << c.radius() << std::endl;
} catch (const std::bad_cast& e) {
std::cout << "Not a circle: " << e.what() << std::endl;
}
}
int main() {
Circle circle(5.0);
Rectangle rect(3.0, 4.0);
describe(&circle); // Circle, radius=5, area=78.5398
describe(&rect); // Rectangle, 3x4, area=12
describe_ref(circle); // It's a circle (ref), radius=5
describe_ref(rect); // Not a circle: std::bad_cast
return 0;
}
dynamic_cast 有运行时成本(通常很小,但在极端热路径上可能可测量),所以不要在高频循环里滥用。更深层的问题是,如果你发现自己到处 dynamic_cast,往往意味着虚函数接口设计得不够好——理想情况下,多态应该通过虚函数调用而非手动类型判断来实现。
三、const_cast —— 增加或移除 const / volatile
const_cast 的功能非常纯粹:它只改变类型的 const 或 volatile 修饰,不改变底层类型。你不能用它把 int* 变成 double*,只能把 const int* 变成 int*(或反过来)。
最正当的使用场景是对接那些本应标记 const 但没有的旧 API。在你确定函数不会修改数据的前提下,可以安全地 cast away const。
关键规则(critical rule): 如果对象本身就是用 const 声明的,通过 const_cast 获得的非 const 指针/引用去修改它是未定义行为。编译器可能把 const 对象放在只读内存段,也可能在编译期直接内联它的值,任何修改都是一颗定时炸弹。
// const_cast_demo.cpp
// compile: g++ -std=c++17 -o const_cast_demo const_cast_demo.cpp
#include <iostream>
#include <cstring>
// 模拟一个旧的 C 库函数:参数没有标 const,但实际上不修改 s
// simulating a legacy C API that forgot to add const
void legacy_print(char* s) {
// 只读取,不修改
while (*s) {
std::putchar(*s);
++s;
}
std::putchar('\n');
}
// 一个合理的 const_cast 使用场景:
// 你的字符串是 const 的,但 legacy API 需要 char*
void safe_usage() {
const char* msg = "Hello from const string";
// const_cast 移除 const,传给旧 API
// safe because legacy_print doesn't actually modify the data
legacy_print(const_cast<char*>(msg));
}
// 一个危险的例子(undefined behavior)
void dangerous_usage() {
const int x = 42;
// x 本身是 const —— 编译器可能把它优化成立即数
// the compiler may have replaced all reads of x with the literal 42
int* p = const_cast<int*>(&x);
// *p = 100; // UB! 取消注释会导致未定义行为
// // 可能"成功"修改,可能崩溃,可能毫无效果
// // the value of x might still read as 42 due to compiler optimization
std::cout << "x = " << x << std::endl; // 几乎肯定输出 42
std::cout << "address of x: " << &x << std::endl;
std::cout << "p points to: " << p << std::endl;
}
// 另一个合法场景:在 const 成员函数中获取 non-const this
// 用于实现 const 和 non-const 版本共享逻辑
class TextBuffer {
char* data_;
size_t len_;
public:
TextBuffer(const char* s) : len_(std::strlen(s)) {
data_ = new char[len_ + 1];
std::strcpy(data_, s);
}
~TextBuffer() { delete[] data_; }
// const 版本
const char& operator[](size_t i) const {
// 假设这里有边界检查等复杂逻辑
return data_[i];
}
// non-const 版本:复用 const 版本的实现,避免代码重复
// Scott Meyers 的经典技巧(Effective C++ Item 3)
char& operator[](size_t i) {
// add const to *this, call const version, then remove const from result
return const_cast<char&>(
static_cast<const TextBuffer&>(*this)[i]
);
}
};
int main() {
safe_usage();
dangerous_usage();
TextBuffer buf("Hello");
buf[0] = 'h'; // 调用 non-const operator[]
std::cout << buf[0] << buf[1] << buf[2] << buf[3] << buf[4] << std::endl;
return 0;
}
const_cast 在代码中出现得越少越好。如果你发现自己频繁需要它,通常意味着接口设计的 const 正确性有问题,应该从源头修复。
四、reinterpret_cast —— 最危险的底层位重解释
reinterpret_cast 做的事情非常简单粗暴:把一段比特原封不动,当成另一种类型来看。它不做任何值的转换或检查,只是重新解释(reinterpret)同一块内存。
常见用途包括:指针和整数之间的互转(用于打印地址、与 C 库交互等)、不相关指针类型之间的转换(如把结构体指针转成 char* 以便按字节操作),以及硬件编程中访问特定内存地址。
几乎所有 reinterpret_cast 的使用都是平台相关的、不可移植的,或者在严格的 C++ 标准下属于未定义行为。在应用层代码中看到它通常是一个设计问题的信号。
// reinterpret_cast_demo.cpp
// compile: g++ -std=c++17 -o reinterpret_cast_demo reinterpret_cast_demo.cpp
#include <iostream>
#include <cstdint>
#include <cstring>
// 一个网络协议的包头(packed struct)
// simulating a binary protocol header
#pragma pack(push, 1) // disable padding
struct PacketHeader {
uint8_t version;
uint8_t type;
uint16_t length;
uint32_t sequence;
};
#pragma pack(pop)
void demo_pointer_to_integer() {
int x = 42;
int* p = &x;
// 指针 -> 整数:获取内存地址的数值
// pointer -> integer: get the raw address as a number
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
std::cout << "pointer value: " << p << std::endl;
std::cout << "as integer: 0x" << std::hex << addr << std::dec << std::endl;
// 整数 -> 指针:还原回来
int* p2 = reinterpret_cast<int*>(addr);
std::cout << "restored value: " << *p2 << std::endl;
}
void demo_binary_protocol() {
// 模拟从网络收到一段原始字节
// simulating raw bytes received from network
uint8_t raw_data[] = {
0x01, // version = 1
0x03, // type = 3
0x00, 0x20, // length = 32 (big-endian, but we ignore byte order here)
0x00, 0x00, 0x00, 0x0A // sequence = 10
};
// reinterpret raw bytes as a PacketHeader struct
// 把原始字节重解释为结构体
// WARNING: this assumes matching endianness and alignment
PacketHeader* header = reinterpret_cast<PacketHeader*>(raw_data);
std::cout << "version: " << (int)header->version << std::endl;
std::cout << "type: " << (int)header->type << std::endl;
std::cout << "length: " << header->length << std::endl;
std::cout << "sequence: " << header->sequence << std::endl;
}
void demo_byte_inspection() {
float f = 3.14f;
// 想看 float 的内存表示(IEEE 754 bit pattern)
// 方法 1:reinterpret_cast(技术上违反 strict aliasing rule,UB)
// Method 1: reinterpret_cast (technically violates strict aliasing, UB)
uint32_t bits_unsafe = *reinterpret_cast<uint32_t*>(&f);
// 方法 2:memcpy(安全,defined behavior,编译器会优化成和上面一样的代码)
// Method 2: memcpy (safe, defined behavior, compiler optimizes to same code)
uint32_t bits_safe;
std::memcpy(&bits_safe, &f, sizeof(float));
std::cout << "float 3.14f bit pattern:" << std::endl;
std::cout << " reinterpret_cast: 0x" << std::hex << bits_unsafe << std::endl;
std::cout << " memcpy (safe): 0x" << bits_safe << std::dec << std::endl;
// 两者结果相同:0x4048f5c3
}
int main() {
std::cout << "=== Pointer <-> Integer ===" << std::endl;
demo_pointer_to_integer();
std::cout << "\n=== Binary Protocol ===" << std::endl;
demo_binary_protocol();
std::cout << "\n=== Float Bit Inspection ===" << std::endl;
demo_byte_inspection();
return 0;
}
上面的 demo_byte_inspection 展示了一个重要的实践问题:通过 reinterpret_cast<uint32_t*>(&f) 读取 float 的比特,从 C++ 标准的角度看违反了 strict aliasing rule(严格别名规则,即同一块内存不能同时通过不兼容的类型访问)。实际上几乎所有编译器都能正确处理,但严格来说是 UB。正确的做法是 memcpy,编译器会把它优化成完全相同的机器码。C++20 之后,std::bit_cast 是更优雅的选择。
五、C 风格转换 (T)expr 和函数风格转换 T(expr)
这两种写法本质上等价,都是从 C 继承来的旧语法。它们看起来简短方便,但暗藏杀机:编译器会按顺序尝试多种转换策略,选第一个能成功的:
const_caststatic_caststatic_cast+const_castreinterpret_castreinterpret_cast+const_cast
此外,C 风格转换还能访问 private/protected 基类,这是所有命名转换都做不到的。
// c_style_cast_demo.cpp
// compile: g++ -std=c++17 -o c_style_cast_demo c_style_cast_demo.cpp
#include <iostream>
#include <cstdint>
class Base {
public:
virtual ~Base() = default;
};
class Derived : public Base {
public:
int value = 99;
};
class Unrelated {
public:
double x = 1.23;
};
int main() {
// 1) 数值转换 —— 等价于 static_cast
double pi = 3.14;
int n = (int)pi; // C 风格
int m = int(pi); // 函数风格, 完全等价
std::cout << n << " " << m << std::endl;
// 2) 继承体系下行转换 —— 等价于 static_cast(无运行时检查)
Base* b = new Derived();
Derived* d = (Derived*)b; // looks innocent, but no runtime check
std::cout << d->value << std::endl;
delete b;
// 3) 不相关类型的指针转换 —— 悄悄退化成 reinterpret_cast
// THIS IS THE DANGER: same syntax, vastly different safety level
Derived obj;
Unrelated* u = (Unrelated*)&obj; // compiles without warning!
// 这行代码把 Derived 的内存当 Unrelated 读,完全是垃圾数据
// reads Derived's memory as if it were Unrelated — total nonsense
std::cout << "garbage: " << u->x << std::endl;
// 4) 移除 const —— 悄悄退化成 const_cast
const int cx = 10;
int* px = (int*)&cx; // strips const silently
// *px = 20; // compiles, but UB if uncommented
// 问题在于:你无法从语法上区分上面四种情况
// The syntax (Type)expr gives NO indication of which cast is being performed.
// If you later change a type, a safe static_cast can silently become
// a dangerous reinterpret_cast, and the code still compiles cleanly.
std::cout << "\nPrefer named casts in C++ code!" << std::endl;
return 0;
}
结论:在 C++ 代码中不要使用 C 风格转换。 命名转换不仅让意图明确,还让代码可搜索——你可以 grep reinterpret_cast 来找出所有危险的转换点。C 风格转换则把所有危险都藏在一对括号里。
六、std::bit_cast(C++20)—— 安全的位重解释
C++20 引入的 std::bit_cast 填补了一个长期存在的空白:如何安全地、以 defined behavior 的方式,将一种类型的比特模式重新解释为另一种类型。
它要求源类型和目标类型大小相同、都是 trivially copyable 的。不满足这些条件就是编译错误,而不是运行时灾难。它还可以在编译期(constexpr)使用,这是 memcpy 和 reinterpret_cast 都做不到的。
// bit_cast_demo.cpp
// compile: g++ -std=c++20 -o bit_cast_demo bit_cast_demo.cpp
#include <iostream>
#include <bit> // std::bit_cast
#include <cstdint>
#include <cstring>
#include <array>
int main() {
// 1) 查看 float 的 IEEE 754 位模式
// the safe, modern, defined-behavior way to type-pun
float f = 3.14f;
uint32_t bits = std::bit_cast<uint32_t>(f);
std::cout << "3.14f = 0x" << std::hex << bits << std::dec << std::endl;
// 输出:0x4048f5c3
// 反过来:从位模式还原 float
float f2 = std::bit_cast<float>(uint32_t(0x40490fdb));
std::cout << "0x40490fdb = " << f2 << std::endl;
// 输出:3.14159 (approximately pi)
// 2) double 的位模式
double d = 1.0;
uint64_t dbits = std::bit_cast<uint64_t>(d);
std::cout << "1.0 = 0x" << std::hex << dbits << std::dec << std::endl;
// 输出:0x3ff0000000000000
// 3) 对比旧方法(memcpy)—— 功能相同,但 bit_cast 更简洁且 constexpr
double d2 = 2.0;
uint64_t dbits2;
std::memcpy(&dbits2, &d2, sizeof(d2)); // old way, also safe
std::cout << "2.0 = 0x" << std::hex << dbits2 << std::dec << std::endl;
// 4) 编译期使用(constexpr)
// this is impossible with memcpy or reinterpret_cast
constexpr float pi_f = 3.14159265f;
constexpr uint32_t pi_bits = std::bit_cast<uint32_t>(pi_f);
static_assert(pi_bits == 0x40490fdb, "unexpected bit pattern");
std::cout << "constexpr bit_cast works!" << std::endl;
// 5) 大小不匹配 -> 编译错误(这是安全性的体现)
// uint16_t small = std::bit_cast<uint16_t>(f); // ERROR: sizeof mismatch
// 编译器会告诉你 sizeof(uint16_t) != sizeof(float)
return 0;
}
简单来说,std::bit_cast 就是"正确版本的 reinterpret_cast 加取值操作"。在 C++20 及以后的代码中,凡是需要查看或重解释值的比特表示的场景,都应该用 std::bit_cast 而不是 reinterpret_cast 或 memcpy。
总结:怎么选?
日常写代码时的决策路径很简单。数值转换、枚举转换、已知类型的指针上下行转换,用 static_cast。不确定实际类型的多态下行转换,用 dynamic_cast。对接遗留 API 需要去掉 const,用 const_cast。需要在相同大小的类型之间重新解释比特模式(如看 float 的二进制表示),C++20 用 std::bit_cast,C++17 及更早用 memcpy。只有在底层系统编程中实在没有替代方案时(指针转整数、处理硬件寄存器地址等),才使用 reinterpret_cast。永远不要在 C++ 代码中使用 C 风格转换。
每种 cast 的存在都对应一种特定的需求。用对了,类型转换是安全的工具;用错了,就是埋在代码里的地雷。选择最窄、最明确的 cast,让编译器为你把关。
更多推荐

所有评论(0)