C++中的多态性

136

AFAIK:

C++提供了三种不同类型的多态性。

  • 虚函数
  • 函数名重载
  • 运算符重载

除了上述三种多态性外,还存在其他种类的多态性:

  • 运行时多态性
  • 编译时多态性
  • 临时多态性
  • 参数多态性

我知道运行时多态性可以通过虚函数来实现,而静态多态性可以通过模板函数来实现。

但对于另外两个

  • 临时多态性
  • 参数多态性 网站所说的是,

临时多态性:

如果可以使用的实际类型范围有限,并且组合必须在使用前单独指定,则称为临时多态性。

参数多态性:

如果所有代码都没有提及任何特定类型,因此可以透明地与任意数量的新类型一起使用,则称为参数多态性。

我几乎无法理解它们 :(

如果可能,请有人给出一个例子来解释这两个问题吗? 我希望这些问题的答案对许多新的毕业生有帮助。


31
实际上,C++ 有四种多态性:参数化多态性(通过 C++ 模板实现的泛型)、包含多态性(通过 C++ 中的虚方法实现的子类型化)、重载和强制转换(隐式转换)。在概念上,函数重载和运算符重载之间几乎没有区别。 - fredoverflow
@zombie:那个网站涉及了很多好的概念,但在术语的使用上不够精确和一致(例如,一旦开始谈论虚拟调度/运行时多态性,它就会对多态性做出许多一般上是错误的但对于虚拟调度来说是正确的陈述)。如果你已经理解了这个主题,你可以理解其中的内容并在脑海中插入必要的警告,但通过阅读该网站很难达到这个目标... - Tony Delroy
@sbi,人们在面试中经常会遇到这个问题:“模板函数和函数模板有什么区别?”如果没有这样的概念,为什么还会被问到呢?http://www.comeaucomputing.com/techtalk/templates/#terms - Vijay
@zombie:我之前不知道Greg用过它,这也是我第一次听到它被这样使用,所以我认为这种情况不存在。 - sbi
@sbi - 如果我的话说得不好,请原谅。如果我只是评论“模板函数”被错误使用的频率有多高,那么我的意思就不清楚了。我知道你在开玩笑,而且大部分时间我并不打算否定你所说的话 - 我只是在为我的问题和建议做铺垫。关于“但人类语言是由常用法定义的”这一方面,那只是我的一个执念式口头禅 - 一个反复出现的笑话(虽然背后有一点意义),你不应该把它当作个人攻击。 - user180247
显示剩余11条评论
7个回答

225

理解多态性/多态性的要求

要理解计算机科学中所使用的多态性,从一个简单的测试和定义开始会有所帮助。请考虑以下内容:

    Type1 x;
    Type2 y;

    f(x);
    f(y);

在这里,f()是执行一些操作,并将xy作为输入值给出。

为了展示多态性,f()必须能够操作至少两个不同类型的值(例如intdouble),找到并执行不同的类型适当的代码。


C ++的多态机制

显式程序员指定的多态

您可以编写f(),使其以以下任何一种方式操作多个类型:

  • 预处理:

#define f(X) ((X) += 2)
// (note: in real code, use a longer uppercase name for a macro!)
  • 重载:

  • void f(int& x)    { x += 2; }
    
    void f(double& x) { x += 2; }
    
  • 模板:

    template <typename T>
    void f(T& x) { x += 2; }
    
    虚拟分发:
    struct Base { virtual Base& operator+=(int) = 0; };
    
    struct X : Base
    {
        X(int n) : n_(n) { }
        X& operator+=(int n) { n_ += n; return *this; }
        int n_;
    };
    
    struct Y : Base
    {
        Y(double n) : n_(n) { }
        Y& operator+=(int n) { n_ += n; return *this; }
        double n_;
    };
    
    void f(Base& x) { x += 2; } // run-time polymorphic dispatch
    
  • 其他相关机制

    针对内置类型的编译器提供的多态性、标准转换以及强制类型转换将在后面进行讨论,以完整性为原则,因为:

    • 它们通常是直观理解的(值得一个“哦,是这样啊”反应)。
    • 它们影响需要、使用上述机制的无缝程度的门槛。
    • 解释会分散注意力,从而使更重要的概念变得混乱不清。

    术语

    进一步分类

    鉴于上述多态机制,我们可以以各种方式对它们进行分类:

    • 何时选择多态类型特定代码?

      • 运行时意味着编译器必须为程序可能处理的所有类型生成代码,并在运行时选择正确的代码(虚拟派遣)。
      • 编译时意味着在编译期间选择特定于类型的代码。对此的一个结果是:假设程序只使用int参数调用f函数 - 根据所使用的多态机制和内联选择,编译器可能避免生成任何f(double)的代码,或者在某些编译或链接阶段抛弃生成的代码(除虚拟派遣外的所有机制)。

    • 支持哪些类型?

      • 特定于应用程序意味着您提供了明确的代码来支持每种类型(例如重载、模板专业化)。您显式地添加对“这个”(根据“特定于...”的含义)类型的支持,以及其他一些“这个”,可能还有“那个”。
      • 参数化意味着您可以尝试使用各种参数类型的函数,而无需专门执行任何操作以启用其对它们的支持(例如模板、宏)。对象的函数/运算符的行为就像模板/宏所期望的那样1,模板/宏需要完成其工作,实际的类型无关紧要。C++20引入的“概念”表达并强制执行这些期望 - 请参见此处的cppreference页面

        • 参数化多态性提供了鸭子类型 - 这是一种由詹姆斯·惠特科姆·莱利(James Whitcomb Riley)所归属的概念,他显然说过:“当我看到一只走路像鸭子、游泳像鸭子、嘎嘎叫像鸭子的鸟时,我称那只鸟为鸭子。”

          template <typename Duck>
          void do_ducky_stuff(const Duck& x) { x.walk().swim().quack(); }
          
          do_ducky_stuff(Vilified_Cygnet());
          
      • 子类型(也称为包容)多态性允许您在不更新算法/函数的情况下使用新类型,但它们必须派生自同一基类(虚分派)

    1 - 模板非常灵活。 SFINAE(参见 http://en.wikipedia.org/wiki/Substitution_failure_is_not_an_errorstd::enable_if)有效地允许对于参数化多态性存在几组期望值。例如,您可以编码,当您处理的数据类型具有 .size() 成员时,您将使用一个函数,否则使用不需要 .size() 的另一个函数(但可能会以某种方式变慢,例如使用较慢的 strlen() 或在日志中不打印出有用的消息)。您还可以指定模板在实例化具有特定参数时的临时行为,可以使某些参数是基于参数的(部分模板特化),也可以不是(完全特化)。

    "多态性"

    Alf Steinbach 指出,在 C++ 标准中,多态性 只指运行时通过虚分派实现的多态性。通用计算机科学含义更加广泛,如 C++ 创始人 Bjarne Stroustrup 的术语表所述 (http://www.stroustrup.com/glossary.html):

    多态性 - 为不同类型的实体提供单一接口。虚函数通过基类提供的接口,提供动态(运行时)多态性。重载函数和模板提供静态(编译时)多态性。 TC++PL 12.2.6、13.6.1、D&E 2.9。

    本答案 - 就像问题一样 - 将 C++ 特性与计算机科学术语相关联。

    讨论

    由于 C++ 标准使用比计算机科学社区更狭窄的 "多态性" 定义,为了确保相互理解,考虑...

    • 使用清晰明确的术语(例如“我们是否可以使此代码对其他类型重用?”或“我们是否可以使用虚分派?”而不是“我们是否可以使此代码多态?”)和/或
    • 清晰地定义您的术语。

    仍然,成为优秀的 C++ 程序员需要理解多态性对您的工作实际上有哪些帮助...

        让你编写“算法”代码一次,然后将其应用于多种类型的数据

    ......然后非常注意不同的多态机制如何匹配您的实际需求。

    运行时多态适合于:

    • 通过工厂方法处理的输入,并作为异构对象集合由Base*处理,
    • 基于配置文件、命令行开关、UI设置等在运行时选择的实现,
    • 在运行时变化的实现,例如状态机模式。

    当没有明确的运行时多态驱动程序时,通常更喜欢编译时选项。考虑:

    • 模板类的编译哪些被调用的方面比失败于运行时的fat接口更可取
    • SFINAE
    • CRTP
    • 优化(包括内联和死代码消除,循环展开,静态堆栈数组 vs 堆)
    • __FILE__, __LINE__, 字符串文本连接以及宏的其他独特功能(宏仍然很邪恶 ;-))
    • 模板和宏测试语义用法受支持,但不人为限制该支持是如何提供的(与虚拟分派倾向于要求完全匹配的成员函数覆盖相反)

    支持多态的其他机制

    如承诺的,出于完整考虑了一些外围主题:

    • 编译器提供的重载
    • 类型转换
    • 强制转换/强制类型转换

    本答案最后讨论了上述内容如何结合起来增强和简化多态代码 - 特别是参数化多态性(模板和宏)。

    映射到特定类型操作的机制

    > 隐式编译器提供的重载

    概念上,编译器对内置类型进行了许多运算符重载。这与用户指定的重载在概念上没有区别,但因为容易被忽视而列出。例如,您可以使用相同的符号x += 2将其添加到intdouble中,并且编译器会生成:

    • 特定于类型的CPU指令
    • 相同类型的结果。

    然后重载无缝扩展到用户定义的类型:

    std::string x;
    int y = 0;
    
    x += 'c';
    y += 'c';
    

    在高级(第三代及以上)计算机语言中,编译器通常会提供基本类型的重载函数,而显式讨论多态性通常意味着更多的内容。(汇编语言-第二代语言-通常要求程序员显式地使用不同的助记符来表示不同的类型。)

    > 标准转换

    C++标准的第四部分介绍了标准转换。

    第一点从旧版本中很好地总结了(希望仍然基本正确):

    -1- 标准转换是为内置类型定义的隐式转换。子句conv枚举了此类转换的完整集合。标准转换序列是以下顺序的标准转换序列:

    • 来自以下集合的零个或一个转换:lvalue-to-rvalue转换、数组到指针转换和函数到指针转换。

    • 来自以下集合的零个或一个转换:integral promotions、floating point promotion、integral conversions、floating point conversions、floating-integral conversions、pointer conversions、pointer to member conversions和boolean conversions。

    • 零个或一个限定符转换。

    [注意:标准转换序列可以为空,即它可以不包含转换。]如果需要将表达式转换为所需的目标类型,则将应用标准转换序列。

    这些转换允许编写如下代码:

    double a(double x) { return x + 2; }
    
    a(3.14);
    a(42);
    

    应用之前的测试:

    为了是多态的,[a()] 必须能够使用至少两种不同类型的值进行操作(例如 intdouble),并且找到并执行适当类型的代码

    a() 本身只针对 double 运行代码,因此不是 多态的。

    但是,在第二次调用 a() 时,编译器知道要生成 "浮点提升" 的类型适当代码(标准 §4)将 42 转换为 42.0。那个额外的代码在 调用 函数中。我们将在结论中讨论其重要性。

    > 强制转换、类型转换和隐式构造函数

    这些机制允许用户定义的类指定类似于内置类型的标准转换的行为。让我们来看一下:

    int a, b;
    
    if (std::cin >> a >> b)
        f(a, b);
    

    这里,对象 std::cin 在布尔上下文中进行评估,借助于一个转换运算符。这可以从上面主题中的“标准转换”中概念上归类为“整数提升”等。

    隐式构造函数实际上也是做同样的事情,但由强制转换类型控制:

    f(const std::string& x);
    f("hello");  // invokes `std::string::string(const char*)`
    

    编译器提供的重载、转换和强制转换的影响

    考虑以下内容:

    void f()
    {
        typedef int Amount;
        Amount x = 13;
        x /= 2;
        std::cout << x * 1.1;
    }
    

    如果我们想要在除法时将数量 x 视为实数(即变为6.5而不是向下舍入为6),我们只需要将 typedef double Amount 进行更改。

    这很好,但明确“类型正确”的代码也不会太难:

    void f()                               void f()
    {                                      {
        typedef int Amount;                    typedef double Amount;
        Amount x = 13;                         Amount x = 13.0;
        x /= 2;                                x /= 2.0;
        std::cout << double(x) * 1.1;          std::cout << x * 1.1;
    }                                      }
    

    但是,请注意我们可以将第一个版本转换为模板

    template <typename Amount>
    void f()
    {
        Amount x = 13;
        x /= 2;
        std::cout << x * 1.1;
    }
    
    由于这些小的“便利功能”,它可以轻松地为intdouble实例化并正常工作。没有这些功能,我们需要显式转换、类型特征和/或策略类,一些冗长而容易出错的混乱代码。
    template <typename Amount, typename Policy>
    void f()
    {
        Amount x = Policy::thirteen;
        x /= static_cast<Amount>(2);
        std::cout << traits<Amount>::to_double(x) * 1.1;
    }
    

    因此,编译器提供的内建类型运算符重载、标准转换、强制类型转换/隐式构造函数——它们都为多态性提供了微妙的支持。按照本答案顶部的定义,它们通过映射解决“查找和执行适用于类型的代码”:

    • 从参数类型中“移开”

      • 多态算法代码处理的许多数据类型

      • 编写为少量(相同或其他)类型的代码。

    • "向"参数化类型从常量类型的值

    它们本身并不建立多态上下文,但确实有助于使这些上下文中的代码变得更加简单/强大。

    你可能会感到失望……似乎不是很重要。其重要性在于,在参数化多态上下文中(即在模板或宏内部),我们试图支持任意范围的类型,但通常希望以其他函数、文字和操作的形式表达对它们的操作,而这些操作是为小型类型集设计的。当操作/值在逻辑上相同时,它减少了按每个类型创建近乎相同的函数或数据的需要。这些特性合作,添加了“尽力而为”的态度,通过使用有限的可用函数和数据来做出直观预期,只有在有真正的歧义时才停止。

    这有助于限制需要支持多态代码的多态代码的需求,在使用局部化的多态不会强制使用广泛使用的情况下,将网更紧地织在多态性的使用周围,并使多态性的好处根据需要提供,而不会强加在编译时暴露实现、在目标代码中支持多个副本的逻辑函数以支持使用的类型,并进行虚拟分派而不是内联或至少编译时解析调用的成本。正如C++中典型的那样,程序员被赋予了许多控制多态性使用范围的自由。


    1
    很棒的答案,除了术语讨论。C++标准在§1.8/1中定义了“多态”一词,在此引用了有关虚函数的第10.3节。因此,在标准C++的上下文中,没有任何余地、讨论或个人意见的空间:该术语已经被定义了一劳永逸。它确实在实践中发挥着作用。例如,§5.2.7/6关于dynamic_cast要求“指向多态类型的指针或lvalue”。祝好! - Cheers and hth. - Alf
    1
    @Alf:其次,我相信多态性在任何一本像样的计算机科学通用书籍中都有正式定义,这种定义方式与我的使用(和Stroustrup的使用)兼容。维基百科文章链接了一些学术出版物,以此来定义它:“多态函数是指其操作数(实际参数)可以具有多种类型的函数。多态类型是指其操作适用于多种类型的值。”(来自http://lucacardelli.name/Papers/OnUnderstanding.A4.pdf)。所以问题是“谁代表计算机科学”...? - Tony Delroy
    鸭子类型是(隐式)约束多态性——函数仅适用于支持“嘎嘎叫”的类型。C++中的SFINAE规则意味着模板会被隐式地约束在这种方式下。“参数化”和“非约束”通常在多态性方面被视为同义词。严格来说,即使在像Haskell这样的语言中,有约束的参数多态性也与特定多态性(特定多态性在类实例中)是分开的,但倾向于将“非约束”和“参数化”视为同义词,并且由于条件不同,在C++中应用这些术语会让事情变得混乱。 - user180247
    @Steve314:抱歉,我有点跟不上。您是说鸭子类型与参数多态性的区别在于它要求支持约束(例如C++0x的Concepts承诺)或基于类型属性的重载解析,比如SFINAE吗?关键区别是一些测试“支持呱呱叫的类型”来指导选择或自定义实现,在模板实例化或宏展开之前,避免致命的编译器错误吗?我的谷歌搜索没有找到这个鸭子类型方面的先例 - 您知道任何关于此的定义/文章吗? - Tony Delroy
    @Tony D - 对不起 - "通过 SFINAE 错误" 是个口误,我想说的是 "通过 SFINAE 规则"。我不确定我们是否有分歧 - 为了澄清我的意思,当你在模板中调用 T::quack() 时,这会隐式地将该模板约束为只能实例化拥有 quack() 的类型 T。即使你为某些没有 quack() 方法的类型 T 实例化,也不会出现错误,但如果没有其他模板覆盖该情况,则会出现错误。因此,它不是一个普通的模板,在某些情况下出现错误,而是带有隐式约束的模板。 - user180247
    显示剩余17条评论

    15
    在C++中,重要的区别是运行时绑定和编译时绑定。临时绑定与参数化并不能真正帮助理解,我稍后会解释。
    |----------------------+--------------|
    | Form                 | Resolved at  |
    |----------------------+--------------|
    | function overloading | compile-time |
    | operator overloading | compile-time |
    | templates            | compile-time |
    | virtual methods      | run-time     |
    |----------------------+--------------|
    

    注意 - 运行时多态性仍然可以在编译时解决,但那只是优化。需要高效地支持运行时分辨率,并权衡其他问题,这是导致虚函数成为它们所代表的一部分的原因。而这对于C++中所有形式的多态性都非常关键 - 每种多态性都源于在不同上下文中做出的不同权衡。
    函数重载和运算符重载在所有重要方面都是相同的。名称和使用它们的语法不会影响多态性。
    模板允许您一次指定许多函数重载。
    还有另一组名称表示相同的解析时间概念...
    |---------------+--------------|
    | early binding | compile-time |
    | late binding  | run-time     |
    |---------------+--------------|
    

    这些名称更与OOP相关,因此说模板或其他非成员函数使用早期绑定有点奇怪。

    为了更好地理解虚函数和函数重载之间的关系,也有必要了解“单分派”和“多分派”的区别。这个想法可以理解为一个进展...

    • 首先,有单态函数。函数的实现由函数名唯一标识。没有任何参数是特殊的。
    • 然后,有单分派。其中一个参数被认为是特殊的,并且用于(连同名称一起)标识要使用哪个实现。在OOP中,我们倾向于将此参数视为“对象”,在函数名称之前列出等。
    • 然后,有多分派。任何/所有参数都有助于确定要使用哪个实现。因此,再次,没有任何参数需要特殊处理。

    显然,OOP不仅仅是提名一个参数作为特殊参数的借口,但这是其中的一部分。并且回到我所说的权衡 - 单分派很容易高效地执行(通常的实现称为“虚表”)。多分派更加麻烦,不仅在效率方面,而且在分离编译方面也是如此。如果您感兴趣,可以查找“表达式问题”。

    就像把“早期绑定”这个术语用于非成员函数一样有些奇怪,用“单分派”和“多分派”这两个术语来描述在编译时解决多态性也有些奇怪。通常认为,C++ 不具备多分派,因为它被认为是一种特定的运行时解析方式。但是,可以将函数重载视为在编译时完成的多重分派。

    回到参数化 vs. 特殊 polymorphism 的讨论,这些术语在函数式编程中更流行,在 C++ 中并不完全适用。即使如此...

    参数化多态意味着您将类型作为参数,并且无论您对这些参数使用何种类型,都使用完全相同的代码。

    特殊多态是指根据特定类型提供不同代码的特殊方法。

    重载和虚函数都是特殊 polymorphism 的示例。

    再次说明一下,这里面还有一些同义词...

    |------------+---------------|
    | parametric | unconstrained |
    | ad-hoc     | constrained   |
    |------------+---------------|
    

    然而,这些并不是完全的同义词,尽管它们通常被视为同义词,这就是C++中可能会出现混淆的地方。

    将它们视为同义词的原因在于,通过将多态性约束到特定类别的类型上,可以使用特定于这些类型的操作。这里的“类别”一词可以按照面向对象编程(OOP)的意义进行解释,但实际上只是指共享某些操作的(通常命名的)类型集合。

    因此,参数多态性通常被认为(至少默认情况下)意味着无约束多态性。由于相同的代码用于所有类型参数,因此唯一支持的操作是适用于所有类型的操作。通过保留类型集合的无约束状态,严重限制了可以应用于这些类型的操作集。

    例如,在Haskell中,您可以有...

    myfunc1 :: Bool -> a -> a -> a
    myfunc1 c x y = if c then x else y
    

    a 在这里是一个无约束的多态类型。它可以是任何东西,因此我们对该类型的值没有太多可做的事情。

    myfunc2 :: Num a => a -> a
    myfunc2 x = x + 3
    

    这里,a 受到限制必须是 Num 类的成员 - 像数字一样行事的类型。这种限制允许您使用这些值进行类似数字的操作,例如将它们相加。即使是 3 也具有多态性 - 类型推断确定你指的是 a 类型的 3

    我认为这是受限泛型多态性。只有一个实现方式,但只能在受限情况下应用。特定的 +3 的选择是临时决定的方面。每个 Num 的“实例”都拥有自己独特的这些实现方式。因此,即使在 Haskell 中,“参数化”和“无约束”的概念并不真正是同义词 - 别怪我,这不是我的错!

    在 C++ 中,重载和虚函数都是临时多态。临时多态的定义不关心实现是否在运行时或编译时被选中。

    如果每个模板参数都有 typename 类型,那么 C++ 的模板就非常接近参数多态性。它有类型参数,并且不管使用哪种类型,都有单一的实现方法。然而,“替换失败不是错误”的规则意味着在模板中使用操作会导致隐式约束。另外,还包括为提供替代模板进行模板特化 - 不同的(特定的)实现。

    因此,在某种程度上,C++ 具有参数多态性,但它是隐式受限的,并且可能会被特定应用程序的替代方案覆盖 - 也就是说,这种分类对 C++ 实际上并不起作用。


    很多有趣的观点和见解。我只花了几个小时阅读 Haskell 相关内容,所以 "a 在这里是一个无约束的多态类型[...],因此我们对该类型的值没有太多可以做的事情" 很有意思 - 在没有概念的 C++ 中,您不仅限于尝试特定一组操作在指定为模板参数的类型的参数上...像 Boost 概念这样的库是另一种方式 - 确保类型支持您指定的操作,而不是防止意外使用其他操作。 - Tony Delroy
    @Tony - 概念是一种明确约束模板多态性的方式。显式约束显然不会因为兼容性而消失,但显式约束肯定会显著改善事情。我相当确定过去一些概念的计划与 Haskell 类型类有些关联,尽管我没有深入研究它们,当我最后一次浅显地看到它们时,我对 Haskell 的了解不多。 - user180247
    从记忆中来看,C++0x概念确实(承诺)防止了“隐式约束”,因为兼容性显然不会消失-您只能按照概念承诺的方式使用类型。 - Tony Delroy

    2
    这可能对你没有帮助,但我制作了这个程序来通过定义函数(如STARTEND),并在主函数中使用它们,以便让我的朋友更容易地学习编程(他们只需使用main.cpp文件)。它包含多态类和结构、模板、向量、数组、预处理器指令、友元、运算符和指针(在尝试多态性之前,你应该掌握所有这些知识)。
    注意:这还没有完成,但你可以了解一下。
    #include "main.h"
    #define ON_ERROR_CLEAR_SCREEN false
    START
        Library MyLibrary;
        Book MyBook("My Book", "Me");
        MyBook.Summarize();
        MyBook += "Hello World";
        MyBook += "HI";
        MyBook.EditAuthor("Joe");
        MyBook.EditName("Hello Book");
        MyBook.Summarize();
        FixedBookCollection<FairyTale> FBooks("Fairytale Books");
        FairyTale MyTale("Tale", "Joe");
        FBooks += MyTale;
        BookCollection E("E");
        MyLibrary += E;
        MyLibrary += FBooks;
        MyLibrary.Summarize();
        MyLibrary -= FBooks;
        MyLibrary.Summarize();
        FixedSizeBookCollection<5> Collection("My Fixed Size Collection");
        /* Extension Work */ Book* Duplicate = MyLibrary.DuplicateBook(&MyBook);
        /* Extension Work */ Duplicate->Summarize();
    END
    

    main.h

    #include <iostream>
    #include <sstream>
    #include <vector>
    #include <string>
    #include <type_traits>
    #include <array>
    #ifndef __cplusplus
    #error Not C++
    #endif
    #define START int main(void)try{
    #define END GET_ENTER_EXIT return(0);}catch(const std::exception& e){if(ON_ERROR_CLEAR_SCREEN){system("cls");}std::cerr << "Error: " << e.what() << std::endl; GET_ENTER_EXIT return (1);}
    #define GET_ENTER_EXIT std::cout << "Press enter to exit" << std::endl; getchar();
    class Book;
    class Library;
    typedef std::vector<const Book*> Books;
    bool sContains(const std::string s, const char c){
        return (s.find(c) != std::string::npos);
    }
    bool approve(std::string s){
        return (!sContains(s, '#') && !sContains(s, '%') && !sContains(s, '~'));
    }
    template <class C> bool isBook(){
        return (typeid(C) == typeid(Book) || std::is_base_of<Book, C>());
    }
    template<class ClassToDuplicate> class DuplicatableClass{ 
    public:
        ClassToDuplicate* Duplicate(ClassToDuplicate ToDuplicate){
            return new ClassToDuplicate(ToDuplicate);
        }
    };
    class Book : private DuplicatableClass<Book>{
    friend class Library;
    friend struct BookCollection;
    public:
        Book(const char* Name, const char* Author) : name_(Name), author_(Author){}
        void operator+=(const char* Page){
            pages_.push_back(Page);
        }
        void EditAuthor(const char* AuthorName){
            if(approve(AuthorName)){
                author_ = AuthorName;
            }
            else{
                std::ostringstream errorMessage;
                errorMessage << "The author of the book " << name_ << " could not be changed as it was not approved";
                throw std::exception(errorMessage.str().c_str());
            }
        }
        void EditName(const char* Name){
            if(approve(Name)){
                name_ = Name;
            }
            else{
                std::ostringstream errorMessage;
                errorMessage << "The name of the book " << name_ << " could not be changed as it was not approved";
                throw std::exception(errorMessage.str().c_str());
            }
        }
        virtual void Summarize(){
            std::cout << "Book called " << name_ << "; written by " << author_ << ". Contains "
                << pages_.size() << ((pages_.size() == 1) ? " page:" : ((pages_.size() > 0) ? " pages:" : " pages")) << std::endl;
            if(pages_.size() > 0){
                ListPages(std::cout);
            }
        }
    private:
        std::vector<const char*> pages_;
        const char* name_;
        const char* author_;
        void ListPages(std::ostream& output){
            for(int i = 0; i < pages_.size(); ++i){
                output << pages_[i] << std::endl;
            }
        }
    };
    class FairyTale : public Book{
    public:
        FairyTale(const char* Name, const char* Author) : Book(Name, Author){}
    };
    struct BookCollection{
    friend class Library;
        BookCollection(const char* Name) : name_(Name){}
        virtual void operator+=(const Book& Book)try{
            Collection.push_back(&Book); 
        }catch(const std::exception& e){
            std::ostringstream errorMessage;
            errorMessage << e.what() << " - on line (approx.) " << (__LINE__ -3);
            throw std::exception(errorMessage.str().c_str());
        }
        virtual void operator-=(const Book& Book){
            for(int i = 0; i < Collection.size(); ++i){
                if(Collection[i] == &Book){
                    Collection.erase(Collection.begin() + i);
                    return;
                }
            }
            std::ostringstream errorMessage;
            errorMessage << "The Book " << Book.name_ << " was not found, and therefore cannot be erased";
            throw std::exception(errorMessage.str().c_str());
        }
    private:
        const char* name_;
        Books Collection;
    };
    template<class FixedType> struct FixedBookCollection : public BookCollection{
        FixedBookCollection(const char* Name) : BookCollection(Name){
            if(!isBook<FixedType>()){
                std::ostringstream errorMessage;
                errorMessage << "The type " << typeid(FixedType).name() << " cannot be initialized as a FixedBookCollection";
                throw std::exception(errorMessage.str().c_str());
                delete this;
            }
        }
        void operator+=(const FixedType& Book)try{
            Collection.push_back(&Book); 
        }catch(const std::exception& e){
            std::ostringstream errorMessage;
            errorMessage << e.what() << " - on line (approx.) " << (__LINE__ -3);
            throw std::exception(errorMessage.str().c_str());
        }
        void operator-=(const FixedType& Book){
            for(int i = 0; i < Collection.size(); ++i){
                if(Collection[i] == &Book){
                    Collection.erase(Collection.begin() + i);
                    return;
                }
            }
            std::ostringstream errorMessage;
            errorMessage << "The Book " << Book.name_ << " was not found, and therefore cannot be erased";
            throw std::exception(errorMessage.str().c_str());
        }
    private:
        std::vector<const FixedType*> Collection;
    };
    template<size_t Size> struct FixedSizeBookCollection : private std::array<const Book*, Size>{
        FixedSizeBookCollection(const char* Name) : name_(Name){ if(Size < 1){ throw std::exception("A fixed size book collection cannot be smaller than 1"); currentPos = 0; } }
        void operator+=(const Book& Book)try{
            if(currentPos + 1 > Size){
                std::ostringstream errorMessage;
                errorMessage << "The FixedSizeBookCollection " << name_ << "'s size capacity has been overfilled";
                throw std::exception(errorMessage.str().c_str());
            }
            this->at(currentPos++) = &Book;
        }catch(const std::exception& e){
            std::ostringstream errorMessage;
            errorMessage << e.what() << " - on line (approx.) " << (__LINE__ -3);
            throw std::exception(errorMessage.str().c_str());
        }
    private:
        const char* name_;
        int currentPos;
    };
    class Library : private std::vector<const BookCollection*>{
    public:
        void operator+=(const BookCollection& Collection){
            for(int i = 0; i < size(); ++i){
                if((*this)[i] == &Collection){
                    std::ostringstream errorMessage;
                    errorMessage << "The BookCollection " << Collection.name_ << " was already in the library, and therefore cannot be added";
                    throw std::exception(errorMessage.str().c_str());
                }
            }
            push_back(&Collection);
        }
        void operator-=(const BookCollection& Collection){
            for(int i = 0; i < size(); ++i){
                if((*this)[i] == &Collection){
                    erase(begin() + i);
                    return;
                }
            }
            std::ostringstream errorMessage;
            errorMessage << "The BookCollection " << Collection.name_ << " was not found, and therefore cannot be erased";
            throw std::exception(errorMessage.str().c_str());
        }
        Book* DuplicateBook(Book* Book)const{
            return (Book->Duplicate(*Book));
        }
        void Summarize(){
            std::cout << "Library, containing " << size() << ((size() == 1) ? " book collection:" : ((size() > 0) ? " book collections:" : " book collections")) << std::endl;
            if(size() > 0){
                for(int i = 0; i < size(); ++i){
                    std::cout << (*this)[i]->name_ << std::endl;
                }
            }
        }
    };
    

    2

    1
    不幸的是,尽管正确,但这是具有误导性的。由于SFINAE规则,模板函数可以获得隐式约束 - 在模板中使用操作会隐含地约束多态性 - 并且模板特化可以提供特定的替代模板,覆盖更通用的模板。因此,默认情况下,模板提供无约束的参数化多态性,但没有强制执行 - 至少有两种方式可以使其变得受限或特定。 - user180247
    实际上,您的示例 - 排序 - 暗示了一个约束条件。排序仅适用于有序的类型(即提供 < 和类似运算符的类型)。在 Haskell 中,您可以使用类 Ord 显式地表达该要求。由于您根据 Ord 的实例提供的特定类型而获得不同的 <,这被认为是特定场景下的多态性。 - user180247

    1
    这是一个使用多态类的基本示例。
    #include <iostream>
    
    class Animal{
    public:
       Animal(const char* Name) : name_(Name){/* Add any method you would like to perform here*/
        virtual void Speak(){
            std::cout << "I am an animal called " << name_ << std::endl;
        }
        const char* name_;
    };
    
    class Dog : public Animal{
    public:
        Dog(const char* Name) : Animal(Name) {/*...*/}
        void Speak(){
            std::cout << "I am a dog called " << name_ << std::endl;
        }
    };
    
    int main(void){
        Animal Bob("Bob");
        Dog Steve("Steve");
        Bob.Speak();
        Steve.Speak();
        //return (0);
    }
    

    0

    多态性意味着许多形式,因此它用于使运算符在不同实例下表现不同。多态性用于实现继承。例如,我们为类形状定义了一个绘制(draw)函数,那么可以为绘制圆形、盒子、三角形和其他形状(这些都是类形状的对象)实现绘制函数。


    -3

    如果有人对这些人说CUT

    The Surgeon
    The Hair Stylist
    The Actor
    

    会发生什么?

    The Surgeon would begin to make an incision.
    The Hair Stylist would begin to cut someone's hair.
    The Actor would abruptly stop acting out of the current scene, awaiting directorial guidance.
    

    所以上面的表示法展示了面向对象编程中多态(相同名称,不同行为)是什么。

    如果你去面试,面试官要求你在我们坐的同一个房间里给出一个多态的实时例子,那么你可以说-

    答案-门/窗户

    想知道为什么吗?

    通过门/窗户-人可以进来,空气可以进来,光线可以进来,雨水可以进来等等。

    即一种形式,不同的行为(多态性)。

    为了更好地理解和简化,我使用了上面的例子。如果您需要代码参考,请参考上面的答案。


    正如我之前提到的,为了更好地理解C++中的多态性,我使用了上面的例子。这可能有助于新手实际理解和关联代码在面试中的执行背后的含义或发生的事情。谢谢! - Sanchit
    OP问了“C++中的多态性”,你的回答太过抽象。 - StahlRat

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接