资讯详情

资讯详情

C++的dynamic示例代码详解

前言先纠正题面dynamic不是 C 的关键字标准里没有这个关键字也没有叫dynamic的语言特性。这个标题所指的内容通常是下面三者之一dynamic_cast运行时类型转换、动态绑定dynamic binding即虚函数分派、动态内存分配dynamic memory allocation。本文以dynamic_cast为主线因为它最常被简称为「dynamic」同时把动态绑定和动态分配的区别一并讲清避免概念混淆。dynamic_cast解决的是「在运行期安全地把基类指针转成派生类指针」的问题。它依赖运行时类型信息run-time type information, RTTI要求源类型是多态类型至少有一个虚函数。转换失败时指针形式返回空指针引用形式抛出std::bad_cast。常见的误解有三个。第一以为任何类都能dynamic_cast——没有虚函数的类不行编译期就会报错。第二以为dynamic_cast和static_cast只是「快慢不同」——前者会做运行期检查后者不会语义完全不同。第三以为dynamic_cast可以替代虚函数——大量使用dynamic_cast做类型分支往往说明接口设计需要调整。一、dynamic_cast 的前提多态类型dynamic_castT*(expr)中expr的静态类型必须是多态类型也就是类中至少声明了一个虚函数通常是虚析构函数。编译器需要虚函数表里的类型信息来判断对象的真实类型。// 适用于 C17struct Base {virtual ~Base() default; // 有了虚函数Base 才是多态类型};struct Derived : Base {int value 0;};struct Plain { // 没有虚函数int value 0;};对Plain*使用dynamic_castDerived*会直接编译失败而不是运行期返回空。这是编译期约束和转换是否「安全」无关。虚析构函数本身也是必须的如果打算通过基类指针delete派生类对象基类析构函数不是虚函数就是未定义行为undefined behavior, UB。把析构函数声明为virtual一举两得既满足多态要求又保证正确释放。二、指针形式与引用形式指针形式在失败时返回nullptr适合「探测」// 适用于 C17#include iostreamvoid describe(Base* p) {if (p nullptr) {std::cout null\n;return;}if (auto* d dynamic_castDerived*(p)) {std::cout Derived, value d-value \n;} else {std::cout not a Derived\n;}}引用形式在失败时抛出std::bad_cast定义在typeinfo适合「调用方确信类型正确、否则就是逻辑错误」的场合// 适用于 C17#include typeinfoDerived asDerived(Base b) {return dynamic_castDerived(b); // 失败抛 std::bad_cast}引用形式没有「空引用」这种表示所以只能抛异常。调用方要么让异常继续向上传播要么用try/catch (const std::bad_cast)捕获。注意 C 没有finally清理工作应当交给 RAII 与析构函数。三、运行时开销与 RTTIdynamic_cast的实现方式由编译器决定通常要查虚表、比对类型信息可能沿继承层次逐层检查。它的开销与继承深度、实现策略有关不能简单说成「常数时间」。可以确定的是它比static_cast慢因为后者在运行期什么也不做。两个实际影响频繁在热路径上做dynamic_cast会带来可观测的额外开销。若确实需要按类型分派虚函数通常是更好的设计。部分项目为了减小二进制体积或避免 RTTI会用-fno-rttiGCC/Clang或/GR-MSVC关闭 RTTI。关闭后dynamic_cast与typeid都无法使用代码直接编译失败。跨平台库要留意这个编译选项。typeid也能用于类型判断但它返回的是静态类型信息对多态类型是动态类型只能做相等比较不能做「向下转换」。二者用途不同手段作用失败表现需要 RTTIdynamic_castT*安全向下/横向转换返回nullptr是dynamic_castT同上引用形式抛std::bad_cast是typeid获取类型信息并比较不适用是static_castT*编译期转换不做检查不检查可能 UB否四、横向转换多重继承下的能力dynamic_cast还能做「横向转换」在同一個完整对象的不同基类子对象之间转换。这是static_cast做不到的因为编译期不知道两个基类之间的相对偏移。// 适用于 C17struct A { virtual ~A() default; int a 1; };struct B { virtual ~B() default; int b 2; };struct C : A, B { int c 3; };void crossCast(B* pb) {// B* 指向的实际对象可能是 C此时可以转到 A*if (A* pa dynamic_castA*(pb)) {// 成功pa 指向同一对象的 A 子对象}}如果pb指向的对象不是C或其它同时继承 A、B 的类型转换返回空。static_cast在这种场景下无法从B*直接转到A*因为两者没有继承关系。五、动态绑定与 dynamic_cast 的分工「动态绑定」指的是通过基类指针或引用调用虚函数时实际执行的是对象的动态类型对应的版本。它和dynamic_cast是两件事// 适用于 C17#include iostreamstruct Shape {virtual ~Shape() default;virtual double area() const 0;virtual const char* name() const 0;};struct Circle : Shape {explicit Circle(double r) : r_(r) {}double area() const override { return 3.141592653589793 * r_ * r_; }const char* name() const override { return circle; }private:double r_;};struct Square : Shape {explicit Square(double s) : s_(s) {}double area() const override { return s_ * s_; }const char* name() const override { return square; }private:double s_;};shape-area()会自动分派到正确的版本不需要任何转换。只有当派生类有基类接口没有暴露的操作时才需要dynamic_cast拿到派生类指针。判断标准很简单如果这个操作对所有派生类都说得通就放进基类接口用虚函数如果只对某一个派生类有意义才考虑dynamic_cast。前者是开闭原则后者是必要的逃生口。六、完整示例// 适用于 C17#include iostream#include memory#include typeinfo#include vectorstruct Shape {virtual ~Shape() default;virtual double area() const 0;};struct Circle : Shape {explicit Circle(double r) : r_(r) {}double area() const override { return 3.141592653589793 * r_ * r_; }double radius() const { return r_; }private:double r_;};struct Rect : Shape {Rect(double w, double h) : w_(w), h_(h) {}double area() const override { return w_ * h_; }bool isSquare() const { return w_ h_; }private:double w_, h_;};int main() {std::vectorstd::unique_ptrShape shapes;shapes.push_back(std::make_uniqueCircle(1.0));shapes.push_back(std::make_uniqueRect(2.0, 3.0));shapes.push_back(std::make_uniqueRect(4.0, 4.0));for (const auto sp : shapes) {std::cout area sp-area() \n; // 动态绑定if (auto* c dynamic_castCircle*(sp.get())) {std::cout radius c-radius() \n;} else if (auto* r dynamic_castRect*(sp.get())) {std::cout square? std::boolalpha r-isSquare() \n;}std::cout type typeid(*sp).name() \n;}return 0;}这里sp是std::unique_ptrShapesp.get()拿到Shape*再转成具体类型。typeid(*sp).name()返回的字符串由实现决定不同编译器的输出格式不同只适合调试不要用它做逻辑判断——需要判断类型时用typeid(*a) typeid(*b)或直接dynamic_cast。常见坑点1. 对非多态类型使用 dynamic_cast❌struct Plain { int x; }; dynamic_castDerived*(plainPtr);✅ 基类至少声明一个虚函数通常是virtual ~Base() default;。2. 用 static_cast 做向下转换却不检查❌auto* d static_castDerived*(basePtr);当对象实际不是Derived时访问成员是 UB。 ✅ 用dynamic_cast并在使用前判空。3. 引用形式不处理异常❌Derived d dynamic_castDerived(base);失败时异常逃逸导致程序终止。 ✅ 用try/catch (const std::bad_cast)或改用指针形式判空。4. 基类析构函数不是虚函数❌struct Base { ~Base() default; };通过Base*删除派生对象。 ✅virtual ~Base() default;同时满足多态与正确析构。5. 在构造函数/析构函数里做向下转换❌ 在Base的构造函数里dynamic_castDerived*(this)总是失败。 ✅ 构造期间对象的动态类型是当前正在构造的类构造完成后再转换。6. 用 dynamic_cast 代替虚函数❌ 一个process(Shape*)里堆十几层dynamic_cast分支。 ✅ 把操作声明为基类虚函数确实只影响单一类型时才用转换做补充。7. 以为转换失败一定返回空❌ 对引用形式也判空。 ✅ 引用形式失败抛std::bad_cast没有空引用。8. 在关闭 RTTI 的项目里使用❌ 编译选项带-fno-rtti或/GR-仍然写dynamic_cast。 ✅ 确认项目开启 RTTI对体积敏感的场景重新评估是否真的需要运行期类型识别。总结概念含义典型语法运行期检查dynamic_cast安全的多态类型转换dynamic_castT*(p)有动态绑定虚函数的运行期分派p-virtualFunc()不适用动态内存分配堆上创建对象new/make_unique无static_cast编译期转换static_castT*(p)无typeid获取类型信息typeid(*p)有dynamic_cast的价值在于「转换前先确认」代价是运行期开销和对 RTTI 的依赖。把它用在真正需要区分具体类型的少数位置把公共行为交给虚函数是兼顾安全与设计的做法。至于标题里的dynamic请记住它不是关键字写代码时也不存在dynamic这个标识符。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →