C++中的虚函数、重写与多态

在C++中顺利使用虚函数需掌握的技术细节

  • 如函数在派生类中的定义有别于基类中的定义,而且你希望它成为虚函数,就要为基类的函数声明添加保留字virtual。在派生类的函数声明中,则可以不添加virtual。函数在基类中virtual,在派生类中自动virtual(但为了澄清,最好派生类中也将函数声明标记为virtual,尽管这并非必须)。
  • 保留字virtual在函数声明中添加,不要再函数定义中添加。
  • 除非使用保留字virtual,否则不能获得虚函数,也不能获得虚函数的任何好处。
  • 既然虚函数如此好用,为何不将所有成员函数都设为virtual?这似乎只有一个理由——效率。编译器和“运行时”环境要为虚函数做多得多的工作。所以,无谓地将成员函数为virtual会影响程序执行效率。

重写

虚函数定义在派生类中发生改变时我们说函数定义被重写。一些C++书籍区分了重定义(redefine)和重写(override)。两者都是在派生类更改函数定义。函数是虚函数,就称为重写。如果不是,就称为重定义。对于程序员,这种区分似乎有点无聊。因为程序员在两种情况下做的事情是一样的。不过,编译器对于这两种情况确定是区别对待的。

多态

多态性是指借助晚期绑定技术,为一个函数名关联多种含义的能力。因此,多态性、晚期绑定和虚函数其实是同一个主题。

虚函数和扩展类型兼容性、切割问题

#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;

class Pet
{
public:
    virtual void print();
    string name;
};

class Dog : public Pet
{
public:
    virtual void print();
    string breed; // 品种
};

void Pet::print()
{
    cout << "Pet name: " << name << endl;
}

void Dog::print()
{
    cout << "Dog name: " << name << ", breed: " << breed << endl;
}

int main()
{
    Pet vPet;
    Dog vDog;
    vDog.name = "Tiny";
    vDog.breed = "Great Dane";
    vPet = vDog;
    // cout << vPet.breed;
    return 0;
}

上述代码第37行的赋值是允许的,但赋给变量vPet的值会丢失其breed字段。这称为切割问题(slicing problem)。例如,第38行的语句会报错。

切割问题:在将派生类对象赋给基类变量时,派生类对象有、基类没有的数据成员会在赋值过程中丢失,基类没有的成员函数也会丢失。在最终的基类对象中,将无法使用这些丢失的成员。

切割测试:

#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::string;

class Demo
{
public:
    Demo(const string& s): str(s)
    {
        cout << "Demo constructor called (" + str + ").\n";
    }
    ~Demo()
    {
        cout << "Demo deconstructor called (" + str + ").\n";
    }
    Demo(const Demo& other)
    {
        str = other.str;
        cout << "Demo copy constructor called (" + str + ").\n";
    }
    Demo& operator=(const Demo& other)
    {
        str = other.str;
        cout << "Demo operator= called (" + str + ").\n";
        return *this;
    }
private:
    string str;
};

class Base
{
public:
    Demo member1 = Demo("member1");
};

class Derived : public Base
{
public:
    Demo member2 = Demo("member2");
};

int main()
{
    Derived derived;
    Base base;
    base = derived;
}
/* Output
Demo constructor called (member1).
Demo constructor called (member2).
Demo constructor called (member1).
Demo operator= called (member1).
Demo deconstructor called (member1).
Demo deconstructor called (member2).
Demo deconstructor called (member1).
*/

幸好,C++提供了一种方式,允许在将一个Dog视为Pet的同时不丢失品种名称:

Pet *pPet;
Dog *pDog;
pDog = new Dog;
pDog->name = "Tiny";
pDog->breed = "Great Dane";
pPet = pDog;
pPet->print(); // prints "Dog name: Tiny, breed: Great Dane"

基类Petprint()声明为virtual。所以一旦编译器看到pPet->print();就会检查PetDogvirtual表,判断pPet指向的是Dog类型的对象。因此,它会使用Dog::print(),而不是Pet::print()

配合动态变量进行OOP是一种全然不同的编程方式。刚开始许多人都会不知所措,但只要记住以下两条简单的规则,理解起来就容易得多。

  1. 如果指针pAncestor的域类型是指针pDescendant的域类型的基类,则以下指针赋值操作允许:pAncestor = pDescendant;。此外,pDescendant指向的动态变量的任何数据成员或成员函数都不会丢失。
  2. 虽然动态变量所有附加字段(成员)都没有丢,但要用virtual成员函数访问。

视图对虚成员函数定义不齐全的类进行编译

编译前,如果还有任何尚未实现的virtual成员函数,编译就会失败,并产生形如undefined reference to Class_Name virtual table的错误信息。即使没有派生类,只有一个virtual成员,并且没有调用该虚函数,只要函数没有定义,就会产生这种形式的消息。此外,可能还会产生进一步的错误消息,声称程序对默认构造函数进行了未定义的引用,即使确实已定义了这些构造函数。

始终/尽量使析构函数成为虚函数

析构函数最好都是虚函数。但在解释它为什么好之前,首先解释一下析构函数和指针如何交互,以及虚析构函数的具体含义。如以下代码,其中SomeClass是含有非虚析构函数的类:

SomeClass *p = new SomeClass;
// ...
delete p;

p调用delete,会自动调用SomeClass类的析构函数,现在看看将析构函数标记为virtual之后会发生什么。为了描述析构函数与虚函数机制的交互,最简单的方式是将所有析构函数都视为同名(即使它们并非真的同名)。如假定Derived类是Base类的派生类,并假定Base类的析构函数标记为virtual,现在分析以下代码:

Base *pBase = new Derived;
// ...
delete pBase;

pBase调用delete时,会调用一个析构函数。由于Base类中的析构函数标记为virtual,且指向的对象是Derived类型,故会调用Derived的析构函数(它进而调用Base类的析构函数)。若Base类的析构函数没有标记为virtual,则只调用Base类的析构函数。

还要注意一点,将析构函数标记为virtual后,派生类的所有析构函数都自动成为virtual的(不管是否用virtual标记)。同样,这种行为就好比所有析构函数具有相同的名称(即使事实上不同名)

现在,已准备好解释为什么所有析构函数都应该是虚函数。假定Base类有一个指针类型的成员变量pBBase类的构造函数会创建由pB指向的一个动态变量,而Base类的析构函数会删除之;另外,假定Base类的析构函数没有标记为virtual,并假定Derived类(从Base派生)有一个指针类型的成员变量pDDerived类的构造函数会创建由pD指向的一个动态变量,而Derived类的析构函数会删除之。则以下代码

Base *pBase = new Derived;
// ...
delete pBase;

由于基类析构函数未标记为virtual,所以只会调用Base类的析构函数。这会将pB指向的动态变量的内存返还给自由存储;但pD指向的动态变量占用的内存永远不会返还给自由存储直到程序终止。

另一方面,将基类Base析构函数标记为virtualdelete pBase;时会调用Derived类的析构函数(因为指向的对象是Derived类型)。Derived类的析构函数会删除pD指向的动态变量,再自动调用基类Base的析构函数删除pB指向的动态变量。

测试代码:

#include <iostream>

class Base
{
public:
    Base()
    {
        baseData = new int;
        std::cout << "baseData allocated.\n";
    }
    ~Base()
    {
        delete baseData;
        std::cout << "baseData deleted.\n";
    }
private:
    int *baseData;
};

class Derived : public Base
{
public:
    Derived()
    {
        derivedData = new int;
        std::cout << "derivedData allocated.\n";
    }
    ~Derived()
    {
        delete derivedData;
        std::cout << "derivedData deleted.\n";
    }
private:
    int *derivedData;

};

int main()
{
    Base *base = new Derived;
    delete base;
}
/* Output
baseData allocated.
derivedData allocated.
baseData deleted.
*/

将第11行的~Base()改为virtual ~Base(),程序输出为

/* Output
baseData allocated.
derivedData allocated.
derivedData deleted.
baseData deleted.
*/

Reference

Walter Savitch《Problem Solving with C++, Tenth Edition》

原文地址:https://www.cnblogs.com/sandychn/p/12421961.html

时间: 2024-10-08 03:22:29

C++中的虚函数、重写与多态的相关文章

【继承与多态】C++:继承中的赋值兼容规则,子类的成员函数,虚函数(重写),多态

实现基类(父类)以及派生类(子类),验证继承与转换--赋值兼容规则: 子类对象可以赋值给父类对象(切割/切片) 父类对象不能赋值给子类对象 父类的指针/引用可以指向子类对象 子类的指针/引用不能指向父类对象(可以通过强制类型转换完成) #include<iostream> using namespace std; class People    //父类或者基类 { public:     void Display()     {         cout << "_na

在构造函数和析构函数中调用虚函数------新标准c++程序设计

在构造函数和析构函数中调用虚函数不是多态,因为编译时即可确定调用的是哪个函数.如果本类有该函数,调用的就是本类的函数:如果本类没有,调用的就是直接基类的函数:如果基类没有,调用的就是间接基类的函数,以此类推.例如: #include<iostream> using namespace std; class A { public: virtual void hello(){cout<<"A::hello()"<<endl;} virtual void

C++中的虚函数解析[The explanation for virtual function of CPlusPlus]

1.什么是虚函数?                                                                                                                                               答:在C++的类中,使用virtual修饰的函数. 例如: virtual void speak() const { std::cout << "Mammal speak!\n&quo

c++虚函数&amp;重写

虚函数是C++中实现多态的一种方法,父类A的一个函数声明为虚函数,在子类B中覆盖定义之后,当在调用的时候使用A*a=new B(),此时调用对应的那个虚函数的名字,则会执行B中的函数.当父类中没有定义虚函数的实体时候,virtual void foo()=0:这个函数就是一个纯虚函数,对应的父类就是抽象类,则这个抽象类不能被实例化,只能由子类派生实例化. 每个含有虚函数的对象都有一个虚指针,这个虚指针和这个对象的基地址是一样的,即一个对象的第一块内存单元存储的一定这个类对象的虚指针. 普通继承中

继承中的虚函数、纯虚函数、普通函数

一.虚函数 被virtual关键字修饰的类成员函数就是虚函数.虚函数的作用就是实现运行时的多态性,将接口与实现分离.简单理解就是相同函数有着不同的实现,但因个体差异而采用不同的策略. 基类中提供虚函数的实现,为派生类提供默认的函数实现.派生类可以重写基类的虚函数以实现派生类的特殊化.如下: class Base{ public: virtual void foo() { cout<<"Base::foo() is called"<<endl; } }; clas

C++中的 虚函数 纯虚函数 虚基类(virtual)

前言:需要了解三者的区别,必须要掌握多态的三个必要条件: 继承 重载 父类指针指向子类对象. 虚函数 纯虚函数 虚基类三者区别 1.虚函数是用于多态中virtual修饰父类函数,确保父类指针调用子类对象时,运行子类函数的. 2.纯虚函数是用来定义接口的,也就是基类中定义一个纯虚函数,基类不用实现,让子类来实现. 3.虚基类是用来在多继承中,比如菱形继承中,如果两个父类继承自同一个类,就只实例化一个父类 ①虚函数第一个是没有使用多态(只用继承)的一般实现方式: class A { public:

第八章:不要在构造和析构函数中使用虚函数

前言 本文将讲解一个新手C++程序员经常会犯的错误 - 在构造/析构函数中使用虚函数,并分析错误原因所在以及规避方法. 错误起因 首先,假设我们以一个实现交易的类为父类,然后一个实现买的类,一个实现卖的类为其子类. 这三个类的对象初始化过程中,都需要完成注册的这么一件事情 (函数).然而,各自注册的具体行为是不同的. 有些人会写出以下这样的代码: 1 class Transaction { 2 public: 3 Transaction(); // 父类构造函数 4 //...... 5 pri

c++中的虚函数

多态是指使用相同的函数名来访问函数不同的实现方法,即“一种接口,多种方法”,用相同的形式访问一组通用的运算,每个运算可能对应的行为不同. C++支持编译时多态和运行时多态,运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态. 1.运行时多态: class A { public: virtual  void play() { cout<< "A:play"<<endl; } }: class B : public class A { public:

[C++]在构造函数及析构函数中调用虚函数

(ISO/IEC 14882:2011 section 12.7.4): Member functions, including virtual functions (10.3), can be called during construction or destruction (12.6.2).When a virtual function is called directly or indirectly from a constructor or from a destructor, inc

【C++】C++中的虚函数与纯虚函数

C++中的虚函数 先来看一下实际的场景,就很容易明白为什么要引入虚函数的概念.假设我们有一个基类Base,Base中有一个方法eat:有一个派生类Derived从基类继承来,并且覆盖(Override)了基类的eat:继承表明ISA(“是一个”)的关系,现在我们有一个基类的指针(引用)绑定到派生类对象(因为派生类对象是基类的一个特例,我们当然可以用基类指针指向派生类对象),当我们调用pBase->eat()的时候,我们希望调用的是Derived类的eat,而实际上调用的是Base类的eat,测试