1. 项目概述为什么多态是C的“灵魂”干了这么多年C我越来越觉得多态Polymorphism这东西就像武侠小说里的内功心法。你光会写几个类、继承几下那叫花拳绣腿真正能把多态玩明白、用到位你的代码才算是有了“灵魂”才能写出既灵活又健壮的系统。我见过太多项目初期为了赶进度一堆if-else或者switch-case硬编码类型判断后期需求一变代码就跟打满补丁的旧衣服一样牵一发而动全身维护成本指数级上升。所谓多态简单说就是“一个接口多种实现”。在C里这主要靠虚函数Virtual Function和继承来实现。它解决的痛点非常明确降低模块间的耦合度提升代码的可扩展性和可维护性。想象一下你写了一个图形渲染引擎需要处理圆形、方形、三角形。没有多态你可能得在draw()函数里写if (shape.type CIRCLE) drawCircle()...。每加一种新图形你都得去修改这个核心的draw()函数。而用了多态你只需要定义一个抽象的Shape基类里面有个虚函数virtual void draw() const 0;然后让CircleSquareTriangle都去继承并实现自己的draw()。主循环里只需要持有Shape*的容器统一调用shape-draw()具体画什么由对象自己的类型决定。新增图形类型你只需要添加一个新类原有的代码一行都不用动。这不仅仅是代码看起来更“优雅”的问题它直接关系到软件工程的核心质量。无论是开发大型游戏引擎、高频交易系统还是嵌入式设备驱动多态都是构建复杂、可演化系统的基石技术。接下来我会结合我踩过的无数个坑从设计思路、核心实现到排查技巧把C多态的最佳实践和那些教科书上不会写的“暗坑”给你彻底讲透。2. 多态的核心机制与设计抉择在动手写代码之前我们必须先理解C实现多态的底层机制以及由此带来的各种设计考量。知其然更要知其所以然这样才能在复杂场景下做出正确选择。2.1 虚函数表vtable与动态绑定的代价C的多态是通过虚函数表vtable机制实现的。这是理解一切多态行为的基础。当一个类声明了至少一个虚函数包括纯虚函数编译器就会为这个类生成一张虚函数表。这张表本质上是一个函数指针数组里面按顺序存放了这个类所有虚函数的地址。同时编译器会在这个类的每个对象实例中隐式地添加一个指针成员通常称为vptr指向该类的虚函数表。当你通过基类指针或引用调用一个虚函数时比如basePtr-virtualFunc()实际发生的步骤如下通过basePtr找到对象内部的vptr。通过vptr找到该对象实际类型可能是派生类的虚函数表。在虚函数表中根据虚函数的声明顺序找到virtualFunc对应的槽位slot。调用该槽位中存储的函数地址。这个过程就是“动态绑定”或“晚期绑定”它发生在程序运行时。与之相对的是“静态绑定”即对普通成员函数的调用在编译期就确定了具体调用哪个函数。这里就引出了第一个关键实践理解性能开销。动态绑定带来的开销主要来自两方面一是每次调用需要额外的指针间接寻址查vtable这通常只是一两条指令在现代CPU上开销微乎其微二是它阻碍了编译器的内联优化。对于性能极其敏感的代码段比如在循环最内层每秒调用上亿次的函数虚函数调用可能成为瓶颈。实操心得不要过早优化。99%的场景下虚函数调用带来的性能损失可以忽略不计而它带来的设计收益是巨大的。只有当性能剖析器Profiler明确告诉你这个虚函数调用是热点时才需要考虑其他方案如CRTP静态多态、策略模式等。为了那1%的可能牺牲代码99%的清晰度和扩展性是典型的“捡了芝麻丢了西瓜”。2.2 继承体系的设计何时使用公有继承“is-a”关系是公有继承的唯一合理理由。也就是说当你能够肯定地说“派生类对象是一个基类对象”时才使用公有继承。Circleis aShapeSavingsAccountis anAccount这没问题。但“有一个”、“实现为”都不是使用公有继承的好理由。常见陷阱滥用继承实现代码复用。比如你有一个Window类带边框、标题栏。现在你要写一个Clock类也需要显示边框和标题。如果你让Clock公有继承Window那就意味着“Clock是一个Window”这显然不合理。正确的做法应该是组合Composition让Clock类包含一个Window成员对象或者私有继承但组合通常更优。// 错误滥用公有继承 class Window { /* 绘制边框、标题等 */ }; class Clock : public Window { /* ... */ }; // Clock “是一个” Window 不合理 // 正确使用组合 class Clock { public: void draw() { window_.drawBorder(); // 使用窗口的功能 // ... 绘制钟表盘 } private: Window window_; // Clock “有一个” Window };设计原则优先使用组合而非继承。组合提供了更大的灵活性降低了类之间的耦合度。只有在确切的“is-a”关系并且需要利用多态行为时才使用公有继承。2.3 接口类与实现类的分离这是大型项目中提升模块性的关键技巧。我们将接口定义为只包含纯虚函数和虚析构函数的抽象类不包含任何数据成员和具体实现。// 接口类只有纯虚函数定义契约。 class IDataSerializer { public: virtual ~IDataSerializer() default; // 接口类必须有虚析构函数 virtual std::vectorchar serialize(const Data data) const 0; virtual Data deserialize(const std::vectorchar bytes) const 0; }; // 实现类实现接口可以继承其他类获得实现但对外只暴露接口。 class JsonSerializer : public IDataSerializer { public: std::vectorchar serialize(const Data data) const override { // 具体的JSON序列化实现 } Data deserialize(const std::vectorchar bytes) const override { // 具体的JSON反序列化实现 } }; class ProtobufSerializer : public IDataSerializer { /* ... */ };这样做的好处是依赖倒置高层模块如业务逻辑只依赖IDataSerializer这个稳定接口而不依赖具体的JsonSerializer或ProtobufSerializer。具体实现可以独立变化和替换。易于测试你可以很容易地为接口创建Mock对象进行单元测试。减少编译依赖只需要包含接口类的头文件实现类的头文件可以通过前置声明等方式隔离大大加快编译速度。3. 多态实现的关键语法与陷阱规避理解了设计思想我们来看看在C语法层面有哪些必须严格遵守的规则和容易踩进去的坑。3.1 虚析构函数内存泄漏的“元凶”这是C多态中最著名、也最容易犯的错误。规则非常简单但必须刻在脑子里如果一个类有可能被继承并且会通过基类指针来删除派生类对象那么它的析构函数必须是虚函数。class Base { public: ~Base() { std::cout Base dtor\n; } // 非虚析构函数危险 }; class Derived : public Base { public: ~Derived() { std::cout Derived dtor\n; } }; int main() { Base* ptr new Derived(); delete ptr; // 未定义行为通常只会调用 ~Base()而不会调用 ~Derived()。 // 输出只有 “Base dtor”Derived部分的资源泄漏了 return 0; }当delete一个指向派生类对象的基类指针时如果基类析构函数不是虚函数那么只会调用基类的析构函数派生类自己的析构函数不会被调用。这会导致派生类中分配的资源如内存、文件句柄、锁等无法被正确释放造成资源泄漏。最佳实践如果一个类设计为基类即使当前没有派生类将其析构函数声明为virtual。对于接口类纯虚类同样需要提供虚析构函数如上例中的 default。反之如果一个类不是设计用来被多态使用的例如std::stringstd::vector就不要声明虚析构函数避免不必要的vptr开销。3.2 override与final关键字让编译器成为你的保镖C11引入的override和final关键字是提升代码安全性和表达力的利器。override明确告知编译器这个函数意图重写基类的虚函数。如果拼写错误或者函数签名参数类型、常量性等不匹配编译器会直接报错而不是静默地创建一个新的虚函数隐藏了基类函数。这能避免许多难以调试的问题。class Base { public: virtual void foo(int) const; virtual void bar(double); }; class Derived : public Base { public: virtual void foo(int) const override; // 正确 virtual void foo(int) override; // 错误常量性不匹配编译报错 virtual void bar(int) override; // 错误参数类型不匹配编译报错 void bar(double) override; // 正确virtual关键字可省略override已隐含 };final可以用于类或虚函数。用于类class Derived final : public Base {};表示Derived不能再被继承。这有助于防止继承体系被过度扩展也允许编译器在某些情况下进行优化如去虚拟化。用于虚函数virtual void func() final;表示这个虚函数在派生类中不能再被重写。这用于锁定某个关键算法的实现。强制习惯在派生类中重写虚函数时总是加上override关键字。这几乎没有任何成本却能带来巨大的安全性提升。3.3 默认参数与虚函数一个违反直觉的坑这是一个经典的陷阱虚函数是动态绑定的但默认参数是静态绑定的。class Base { public: virtual void print(int x 10) const { std::cout Base: x \n; } }; class Derived : public Base { public: void print(int x 20) const override { std::cout Derived: x \n; } }; int main() { Derived d; Base b d; b.print(); // 输出什么 }输出是Derived: 10。 虽然调用的是Derived::print动态绑定但使用的默认参数10来自于Base::print的声明静态绑定在编译期根据引用b的静态类型Base确定。这非常容易导致迷惑和错误。最佳实践是避免在虚函数中使用默认参数。如果确实需要类似功能可以考虑使用重载的非虚函数作为包装或者使用“命名参数”等设计模式。3.4 对象切片Object Slicing多态的“杀手”当派生类对象被以值传递的方式赋值给基类对象时会发生对象切片。派生类特有的部分会被“切掉”只保留基类的子对象。class Base { int x; }; class Derived : public Base { int y; }; void func(Base b) { /* ... */ } Derived d; func(d); // 切片发生func内部收到的b只是一个Base对象没有y成员。 Base b d; // 同样发生切片切片后对象的多态性完全丧失。通过切片得到的基类对象其vptr指向的是基类的虚函数表调用虚函数时只会执行基类的版本。如何避免使用指针或引用在需要多态的上下文中始终使用基类的指针Base*或引用Base来操作对象。这是最根本的解决方法。警惕容器std::vectorBase存储派生类对象会发生切片。应该使用std::vectorstd::unique_ptrBase或std::vectorBase*需手动管理内存来存储多态对象。禁止值语义如果不想让一个类被切片可以考虑将其拷贝构造函数和拷贝赋值运算符声明为private或delete并提供一个克隆虚函数virtual Base* clone() const 0;。4. 多态在复杂场景下的高级实践掌握了基础我们来看看在更复杂的工程实践中如何优雅地运用多态。4.1 工厂模式与多态对象的创建我们通常不直接new具体的派生类而是通过工厂方法来创建对象这样可以将对象的创建逻辑与使用逻辑解耦。class Product { public: virtual ~Product() default; virtual void operate() 0; }; class ConcreteProductA : public Product { /* ... */ }; class ConcreteProductB : public Product { /* ... */ }; class ProductFactory { public: enum class Type { A, B }; // 工厂方法根据传入的类型标识创建对应的产品对象。 static std::unique_ptrProduct createProduct(Type type) { switch (type) { case Type::A: return std::make_uniqueConcreteProductA(); case Type::B: return std::make_uniqueConcreteProductB(); default: return nullptr; } } // 更高级的注册式工厂避免在工厂类中硬编码所有产品类型。 using Creator std::functionstd::unique_ptrProduct(); static std::unordered_mapstd::string, Creator getRegistry() { static std::unordered_mapstd::string, Creator registry; return registry; } static bool registerProduct(const std::string name, Creator creator) { getRegistry()[name] std::move(creator); return true; } static std::unique_ptrProduct createProduct(const std::string name) { auto it getRegistry().find(name); if (it ! getRegistry().end()) { return it-second(); // 调用注册的创建函数 } return nullptr; } }; // 在每个具体产品类的源文件中进行静态注册 namespace { bool registeredA ProductFactory::registerProduct(ProductA, [](){ return std::make_uniqueConcreteProductA(); }); }注册式工厂的优点是新增产品类型时只需要在新类的文件中进行静态注册而无需修改工厂类的源代码符合“开闭原则”。4.2 使用std::unique_ptr和std::shared_ptr管理多态对象手动管理多态对象的生命周期new/delete极易出错。现代C强烈推荐使用智能指针。std::unique_ptrBase表示独占所有权。当工厂方法返回一个产品时这是最自然的选择。它保证了资源在任何时候都只有一个所有者避免了内存泄漏和重复释放。std::unique_ptrProduct product factory.createProduct(type); product-operate(); // 使用 // 离开作用域时自动删除std::shared_ptrBase表示共享所有权。当多个模块需要共同持有并访问同一个多态对象时使用。注意直接从this指针创建shared_ptr是危险的通常需要让类继承自std::enable_shared_from_thisBase。class Observable : public std::enable_shared_from_thisObservable { // ... }; auto obj std::make_sharedObservable();关键技巧自定义删除器。如果多态对象的析构方式不是简单的delete比如来自DLL、使用自定义分配器可以通过智能指针的删除器来管理。struct CustomDeleter { void operator()(Product* p) const { // 自定义销毁逻辑例如调用一个特定的销毁函数 p-destroy(); // 不要自己调用 delete p; } }; std::unique_ptrProduct, CustomDeleter customProduct(ptrFromDLL);4.3 多态与STL算法、类型擦除的结合STL算法如std::sortstd::find_if通常要求容器元素类型支持值语义和比较操作。多态对象由于切片问题不适合直接放入std::vectorBase。这时类型擦除Type Erasure技术就派上用场了。std::function就是一个经典的类型擦除例子。我们可以利用多态和智能指针实现自己的“可调用对象”容器或者“任何类型”的容器。// 一个简单的、支持多态比较的类型擦除包装器 class Comparable { struct Concept { virtual ~Concept() default; virtual bool lessThan(const Concept* other) const 0; virtual std::unique_ptrConcept clone() const 0; }; templatetypename T struct Model : Concept { T data; Model(T d) : data(std::move(d)) {} bool lessThan(const Concept* other) const override { // 关键将other转换回ModelT进行比较 // 这里假设T支持 操作符。更安全的实现需要dynamic_cast和类型检查。 auto* p dynamic_castconst ModelT*(other); return p data p-data; } std::unique_ptrConcept clone() const override { return std::make_uniqueModelT(data); } }; std::unique_ptrConcept pimpl; public: templatetypename T Comparable(T value) : pimpl(std::make_uniqueModelT(std::move(value))) {} // 支持拷贝需要clone Comparable(const Comparable other) : pimpl(other.pimpl ? other.pimpl-clone() : nullptr) {} Comparable operator(Comparable other) { swap(pimpl, other.pimpl); return *this; } friend bool operator(const Comparable a, const Comparable b) { return a.pimpl-lessThan(b.pimpl.get()); } }; // 现在我们可以把任何支持的类型放进同一个容器排序了 std::vectorComparable vec; vec.push_back(42); // int vec.push_back(std::string(hello)); // std::string vec.push_back(3.14); // double std::sort(vec.begin(), vec.end()); // 可以正常排序这种模式非常强大它结合了多态的运行时灵活性和模板的编译时类型安全是构建通用库组件如std::anystd::function的核心思想。5. 多态相关的典型问题与调试技巧即使你小心翼翼地遵循了所有最佳实践在实际开发中依然会遇到各种稀奇古怪的问题。下面是我总结的一些常见“病症”和“药方”。5.1 运行时类型识别RTTI与dynamic_cast的使用与限制dynamic_cast和typeid是C的RTTI机制允许在运行时查询和转换类型。dynamic_castDerived*(basePtr)尝试将基类指针安全地向下转换为派生类指针。如果basePtr实际指向的对象是Derived类型或其派生类则转换成功返回有效指针否则返回nullptr。对于引用类型失败时会抛出std::bad_cast异常。Base* ptr /* ... */; if (auto* dPtr dynamic_castDerived*(ptr)) { // 转换成功可以安全使用dPtr访问Derived的特定成员 dPtr-derivedMethod(); } else { // 转换失败ptr并不指向Derived对象 }typeid运算符返回一个std::type_info对象的引用描述表达式的类型。常用于日志、调试或实现简单的类型区分。if (typeid(*ptr) typeid(Derived)) { // *ptr 的动态类型是 Derived }然而过度使用RTTI通常是糟糕设计的信号。它破坏了多态的封装性迫使高层代码了解具体的派生类型。这往往意味着你的基类接口设计得不够完备无法通过虚函数提供统一的操作。最佳实践优先使用虚函数如果可以通过在基类中添加一个虚函数来解决问题那就绝对不要用dynamic_cast。例如与其判断类型后调用特定函数不如在基类声明一个虚函数让派生类各自实现。考虑访问者模式Visitor Pattern当你需要对一个复杂继承结构中的多种类型进行不同操作时访问者模式是比到处dynamic_cast更优雅、更可扩展的解决方案。理解性能与开销dynamic_cast需要遍历继承链可能比虚函数调用更慢。并且RTTI通常会增加可执行文件的大小。在一些嵌入式或高性能场景下编译器甚至会提供关闭RTTI的选项如-fno-rtti。确保基类至少有一个虚函数dynamic_cast只能用于多态类型即有虚函数的类。5.2 构造函数与析构函数中的虚函数调用这是一个非常隐蔽的陷阱在构造函数和析构函数中调用虚函数不会发生多态行为。class Base { public: Base() { init(); } // 在构造函数中调用虚函数 virtual void init() { std::cout Base::init\n; } virtual ~Base() { cleanup(); } // 在析构函数中调用虚函数 virtual void cleanup() { std::cout Base::cleanup\n; } }; class Derived : public Base { public: Derived() { /* ... */ } void init() override { std::cout Derived::init\n; } void cleanup() override { std::cout Derived::cleanup\n; } }; int main() { Derived d; // 输出是什么 // 构造时Base::init (不是 Derived::init!) // 析构时Base::cleanup (不是 Derived::cleanup!) return 0; }原因在构造Derived对象时会先调用Base的构造函数。此时Derived对象尚未完全构造其vptr指向的是Base的虚函数表。因此在Base构造函数中调用的init()是Base::init。析构过程则相反先调用Derived的析构函数再调用Base的析构函数。在Base析构函数执行时Derived对象的部分已经被销毁vptr可能已被修改或指向Base的虚函数表因此调用的是Base::cleanup。解决方案避免在构造/析构函数中直接调用虚函数来完成关键初始化或清理工作。可以采用以下模式传递参数给构造函数将初始化所需的数据作为参数传递给基类构造函数。使用两阶段初始化构造函数只做最简单的成员初始化然后提供一个独立的initialize()虚函数或非虚函数调用虚函数供用户显式调用。但这种方法需要注意异常安全和调用顺序。将清理工作移到具体的派生类析构函数中基类析构函数只负责清理基类自己的资源派生类析构函数负责清理派生类的资源。这是最推荐的做法符合RAII思想。5.3 菱形继承与虚继承的迷雾多重继承本身已经足够复杂当出现“菱形继承”时问题会加倍。class A { public: int data; }; class B : public A {}; class C : public A {}; class D : public B, public C {}; D d; // d.data 10; // 错误歧义不知道是B::data还是C::data d.B::data 10; // 需要明确指定路径在菱形继承中D对象内部会有两份A的子对象分别来自B和C。这通常不是我们想要的我们可能希望D只包含一份A。虚继承Virtual Inheritance就是用来解决这个问题的。它保证在继承体系中无论虚基类被派生多少次在最终的子类对象中都只存在一个实例。class A { public: int data; }; class B : virtual public A {}; // 虚继承 class C : virtual public A {}; // 虚继承 class D : public B, public C {}; D d; d.data 10; // 正确现在只有一份A子对象没有歧义。然而虚继承带来了新的复杂性和开销对象布局复杂虚继承的对象通常包含指向虚基类子对象的指针vbase pointer增加了内存开销和访问间接性。初始化责任转移在非虚继承中每个构造函数负责初始化其直接基类。在虚继承中最终派生类Most Derived Class的构造函数负责直接初始化虚基类。中间类的构造函数对虚基类的初始化会被忽略。析构顺序析构顺序与构造顺序严格相反同样需要特别注意。最佳实践尽量避免使用多重继承尤其是菱形继承。绝大多数情况下通过单继承和组合包含对象成员可以更好地表达类之间的关系。如果必须使用多重继承优先考虑使用接口纯虚类的多重继承这通常不会导致菱形问题因为接口没有数据成员。如果确实需要共享带有状态的基类并形成菱形结构再谨慎考虑使用虚继承并充分理解其带来的复杂性。5.4 多态与异常安全在多态环境中处理异常需要格外小心特别是在构造函数和析构函数中。构造函数中的异常如果派生类构造函数中抛出异常其基类子对象和所有已完成构造的成员子对象会被自动析构按构造相反顺序。但是如果基类构造函数中已经分配了资源如new了内存并且在其构造函数中抛出异常那么该基类的析构函数不会被调用因为对象尚未完全构造。因此基类构造函数中获取的资源必须用智能指针或类似的RAII对象来管理以确保异常安全。析构函数中的异常析构函数绝对不应该抛出异常。如果析构函数在栈展开stack unwinding过程中因为异常退出即抛出异常程序通常会直接调用std::terminate()终止。因为C无法同时处理两个异常。对于可能失败的操作应在析构函数内部用try-catch块捕获并处理例如记录日志但不要将异常传播到析构函数之外。多态销毁与异常当你delete一个多态对象指针时如果派生类的析构函数抛出异常而这个异常没有被派生类析构函数自身捕获那么它会在传播到基类析构函数之前导致程序终止因为此时正处于异常处理过程中。这进一步强调了在析构函数中吞掉或处理所有异常的必要性。6. 性能考量与替代方案虽然虚函数是C多态的基石但在极端追求性能的场景下我们需要了解其开销并知道替代方案。6.1 虚函数调用的真实开销分析虚函数调用开销主要来自间接调用通过vptr和vtable的二次指针解引用。在现代CPU上这通常意味着一次缓存未命中如果vtable不在缓存中和分支预测失败的风险。但在大多数情况下这个开销很小几个时钟周期。无法内联编译器通常无法在编译期确定调用哪个虚函数因此无法进行内联优化。内联可以消除函数调用开销并开启大量的优化机会如常量传播、循环展开。这才是虚函数在性能关键路径上的主要成本。测量而非猜测在优化之前一定要使用性能剖析工具如perfVTuneCallgrind来定位真正的热点。盲目地将所有函数改为非虚会严重损害代码设计却可能收效甚微。6.2 静态多态CRTP简介当多态行为在编译期就能确定时可以使用“奇异递归模板模式”Curiously Recurring Template Pattern CRTP来实现静态多态从而消除运行时开销。// 基类模板 template typename Derived class Base { public: void interface() { // 将调用派发到派生类的实现 static_castDerived*(this)-implementation(); } void implementation() { // 可选的默认实现 std::cout Default implementation in Base\n; } }; // 派生类 class Derived1 : public BaseDerived1 { public: void implementation() { std::cout Custom implementation in Derived1\n; } }; class Derived2 : public BaseDerived2 { // 没有重写implementation将使用Base中的默认实现 }; template typename T void doSomething(BaseT obj) { obj.interface(); // 编译期绑定可能被内联 } int main() { Derived1 d1; Derived2 d2; doSomething(d1); // 输出: Custom implementation in Derived1 doSomething(d2); // 输出: Default implementation in Base }CRTP的优点零开销所有函数调用在编译期确定可以内联。编译期多态错误在编译期暴露。CRTP的缺点运行时灵活性丧失无法在运行时动态改变对象类型。容器中不能存放不同类型的CRTP基类指针。代码膨胀模板实例化会为每个派生类生成一份基类代码。可读性降低语法相对怪异。适用场景适用于性能极其关键、类型在编译期已知的场合例如数学库中的向量/矩阵运算、特定设计模式如策略模式的编译期实现。6.3 基于std::variant和std::visit的替代方案C17对于已知的、有限的类型集合C17的std::variant和std::visit提供了一种类型安全、且通常比基于堆分配的多态更高效的替代方案。这被称为“闭集多态”。#include variant #include iostream class Circle { public: void draw() const { std::cout Drawing a circle\n; } }; class Square { public: void draw() const { std::cout Drawing a square\n; } }; using Shape std::variantCircle, Square; // 形状只能是Circle或Square // 访问者是一个可调用对象能处理variant中的所有类型 struct DrawVisitor { void operator()(const Circle c) const { c.draw(); } void operator()(const Square s) const { s.draw(); } }; int main() { std::vectorShape shapes; shapes.emplace_back(Circle{}); shapes.emplace_back(Square{}); for (const auto shape : shapes) { std::visit(DrawVisitor{}, shape); // 编译期生成分发代码效率高 } // 或者使用泛型lambda (C20更简洁) // std::visit([](const auto s){ s.draw(); }, shape); }优点值语义对象通常存储在栈或容器内内存局部性好避免堆分配开销。高效std::visit的实现通常使用编译期生成的跳转表比虚函数调用更快或相当。类型安全所有可能类型在编译期已知。缺点闭集类型集合必须预先确定无法在运行时动态扩展。可能的内存浪费variant的大小是其所有可能类型中最大的那个可能造成内存浪费。选择哪种方案取决于你的具体需求需要运行时无限扩展的灵活性还是对已知类型的极致性能。