《C++ primer plus》第13章:类继承(3)

《C++ primer plus》第13章:类继承(3)静态联编和动态联编 程序调用函数时 将使用哪个可执行代码块呢 编译器负责回答这个问题 将源代码中的函数调用解释为执行特定的函数代码块被称为函数名 u 对应一个不同的函数 在 C 中 由于函数重载的缘故 这项任务更复杂 编译器必须查看函数参数以及函数名才能确定使用哪个函数 然而 C C

大家好,我是讯享网,很高兴认识大家。

静态联编和动态联编

程序调用函数时,将使用哪个可执行代码块呢?编译器负责回答这个问题。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名u对应一个不同的函数。在 C++ 中,由于函数重载的缘故,这项任务更复杂。编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而,C/C++ 编译器可以在编译过程完成这种联编。在编译过程中进行联编被称为静态联编(static binding),又称为早期联编(early binding)。然而,虚函数使这项工作变得更困难。正如在上面程序所示的那样,使用哪一个函数是不能在编译时确定的,因为编译器不知道用户将选择哪种类型的对象。所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)。

知道虚方法的行为后,下面深入地探讨这一过程,首先介绍C++如何处理指针和引用类型的兼容性。

指针和引用类型的兼容性

在 C++ 中,动态联编与通过指针和引用调用方法相关,从某种程度上说,这是由继承控制的。公有继承建立 is-a 关系的一种方法是如何处理指向对象的指针和引用。通常,C++ 不允许将一种类型的地址赋给另一种类型的指针,也不允许将一种类型的引用指向另一种类型:

double x = 2.5; int * pi = &x; // invalid assignment, mismatched pointer type long & r1 = x; // invalic assignment, mismatched reference type 

讯享网

然而,指向基类的引用或指针可以引用派生类对象,而不必进行显示类型转换。例如,下面的初始化是允许的:

讯享网BrassPlus dilly ("Annie Dill", , 2000); Brass * pb = &dilly; // ok Brass & rb = dilly; // ok 

将派生类引用或指针转换为基类引用或指针被称为向上强制转换( upcasting),这使公有继承不需要进行显示转换。该规则是 is-a 关系的一部分。BrassPlus 对象都是 Brass 对象,因为它继承了 Brass 对象所有的数据成员和成员函数。

虚成员函数和动态联编

来回顾一下使用引用或指针调用方法的过程。请看下面的代码:

BrassPlus ophelia; // derived-class object Brass * bp; // base-class pointer bp = &ophelia; // Brass pointer to BrassPlus object bp -> ViewAcct(); // which version? 

正如前面介绍的,如果在基类中没有将 ViewAcct() 声明为虚的,则 bp->ViewAcct() 将根据指针类型(Brass *)调用 Brass::ViewAcct()。指针类型在编译时已知,因此编译器在编译时,可以将 ViewAcct() 关联到 Brass::ViewAcct()。总之,编译器对非虚方法使用静态联编

然而,如果在基类中将 ViewAcct() 声明为虚的,则 bp->ViewAcct() 根据对象类型(BrassPlus)调用 BrassPlus::ViewAcct()。在这个例子中,对象类型为 BrassPlus,但通常(如上面的程序所示)只有在运行程序时才能确定对象的类型。所以编译器生成的代码将在程序执行时,根据对象类型将 ViewAcct() 关联到 Brass::ViewAcct() 或 BrassPlus::ViewAcct()。总之,编译器对虚方法使用动态联编。

在大多数情况下,动态联编很好,因为它让程序能够选择为特定类型设计的方法。因此,您可能会问:

  • 为什么会有两种类型的联编?
  • 既然动态联编如此之好,为什么不将它设置成默认的?
  • 动态联编是如何工作的?

下面来看看这些问题的答案。

  1. 为什么有两种类型的联编以及为什么默认为静态联编

原因有两个——效率和概念模型

首先来看效率,为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销。例如,如果类不会用作基类,则不需要动态联编。同样,如果派生类不重新定义基类的任何方法,也不需要使用动态联编。在这些情况下,使用静态联编更合理,效率也更高。由于静态联编的效率更高,因此被设置为 C ++ 的默认选择。Strousstrup 说,C++ 的指导原则之一是,不要为不使用的特性付出代价(内存或者处理时间)。仅当程序设计确实需要虚函数时,才使用它们。

接下来看概念模型。在设计类时,可能包含一些不在派生类重新定义的成员函数。例如Brass::Balance() 函数返回账户结余,不应该重新定义。不将该函数设置为虚函数,有两方面的好处:首先效率更高;其次,指出不要重新定义该函数。这表明,仅将哪些预期将被重新定义的方法声明为虚的。

提示:如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则,设置为非虚方法。

  1. 虚函数的工作原理

C++ 规定了虚函数的行为,但将实现方法留给了编译器作者。不需要知道实现方法就可以使用虚函数,但了解虚函数的工作原理有助于更好地理解概念,因此,这里对其进行介绍。

通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table,vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该 vtbl 将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到 vtbl 中。注意,无论类中包含的虚函数是1个还是10个,都只需要在对象中添加 1 个地址成员,只是表的大小不同而已。

讯享网class Scientist { ... char name[40]; public: virtual void show_name(); virtual coid show_all(); ... }; class Physicist : public Scientist { ... char field[40]; public: void show_all(); // redefined virtual void show_field(); // new ... }; 


讯享网

  • 每个对象都将增大,增大量为存储地址的空间;
  • 对于每个类,编译器都创建一个虚函数地址表(数组);
  • 对于每个函数调用都需要执行一项额外的操作,即到表中查找地址。

虽然非虚函数的效率比虚函数稍高,但不具备动态联编功能。

有关虚函数注意事项

我们已经讨论了虚函数的一些要点。

  • 在基类方法的声明中使用关键字 virtual 可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的。
  • 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这种行为非常重要,因为这样基类指针或引用可以指向派生类对象。
  • 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。

对于虚方法,还需要了解其它一些知识,其中有的已经介绍过。下面来看看这些内容。

  1. 构造函数
    构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将类构造函数声明为虚的没什么意义。
  2. 析构函数
    析构函数应当是虚函数,除非类不用做基类。例如,假设Employee是基类,Singer 是派生类,并添加一个 char * 成员,该成员指向由 new 分配的内存。当 Singer 对象过期时,必须调用 ~Singer() 析构函数来释放内存。

    请看下面的代码

    Employee * pe = new Singer; // legal because Employee is base for Singer ... delete pe; // ~Employee() or ~Singer() ? 

    如果使用默认的静态联编, delete 语句将调用 ~Employee() 析构函数。这将释放由 Singer 对象中的 Employee 部分指向的内存,但不会释放新的类成员指向的内存。但如果析构函数是虚的,则上述代码将先调用 ~Singer() 析构函数释放由 Singer 组件指向的内存,然后,调用 ~Employee() 析构函数来释放由 Employee 组件指向的内存。

    这意味着,即使基类不需要显式析构函数提供服务,也不应依赖于默认析构函数,而应提供虚析构函数,即使它不执行任何操作:

    讯享网virtual ~BaseClass() { } 
  3. 友元
    友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。如果由于这个原因引起了设计问题,可以通过让友元函数使用虚成员函数来解决。
  4. 没有重新定义
    如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。
  5. 重新定义将隐藏方法
    假设创建了如下所示的代码:
    class Dwelling{ public: virtual void showperks(int a) const; ... }; class Hovel : public Dwelling{ public: virtual void showperks() const; ... } 

    这将导致问题,可能会出现类似于下面这样的编译器警告:

    讯享网Warning: Hovel::showperks(void) hides Dwelling::showperks(int) 

    也可能不会出现警告。但不管结果怎样,代码将具有如下含义:

    Hovel trup; trump.showperks(); // valid trump.showperks(5); // invalid 
    讯享网class Dwelling{ public: // a base method virtual Dwelling & build(int n); ... }; class Hovel : public Dwelling{ public: // a derived method with a covariant return type virtual Hovel & build(int n); // sam function signature }; 
    class Dwelling{ public: // three overloaded showperks() virtual void showperks(int a) const; virtual void showperks(double x) const; virtual void showperks() const; ... }; class Hovel : public Dwelling{ // three redefined showperks() virtual void showperks(int a) const; virtual void showperks(double x ) const; virtual void showperks() const; ... }; 

    如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。注意,如果不需要修改,则新定义可只调用基类版本:

    讯享网void Hovel::showperks() const { Dwelling::showperks(); } 
小讯
上一篇 2025-03-22 15:25
下一篇 2025-02-28 20:34

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/48171.html