类的继承与多态

129 阅读6分钟

继承

类的继承是在一个基类的基础上,派生出一个新的类,派生类会拥有基类中所有的成员,此外,派生类还可以定义自己独有的成员变量和函数,派生类还可以继续派生出新的类。

语法

class 派生类名称: public 基类名称
{
    /*...*/
};

其中,访问控制符public表示:派生类从基类中继承的成员的最大访问权限是public;例如,当访问控制符是protected时,表示基类中的public成员,在派生类中的访问权限会变为protected,而基类中的protected成员,在派生类中仍然是protected

在类继承时,如果省略访问控制符,则默认为public

关键点

  • 在派生类中,是不可以访问基类中的private成员的:

image.png

  • 以下类成员是不可以被继承的:

    • 构造函数和析构函数
    • 运算符= () 成员
    • 类的友元
  • 派生类对象在创建时,会自动调用基类的默认构造函数,并且在派生类对象销毁时,会自动调用基类的析构函数:

#include <iostream>

class Base {
public:
    Base() {
        std::cout << "Base class default constructor called." << std::endl;
    }
    ~Base() {
        std::cout << "Base class destructor called." << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "\tDerived class constructor called." << std::endl;
    }
    ~Derived() {
        std::cout << "\tDerived class destructor called." << std::endl;
    }
};

int main() {
    Derived obj;
    return 0;
}

/*
Base class default constructor called.
        Derived class constructor called.
        Derived class destructor called.
Base class destructor called.
*/
  • 创建派生类对象时,基类的某个构造函数必须被调用。比如下面的例子,当我们为基类定义了一个单参数的构造函数时,基类便不会再自动生成一个默认的无参构造函数,这时需要在派生类的构造函数中,显式地调用基类的构造函数:
class Base {
public:
    // 不会自动生成默认的无参构造函数了
    Base(int x) {
        std::cout << "Base class default constructor called." << std::endl;
    }
};

class Derived : public Base {
public:
    Derived(): Base(0) {
        std::cout << "Derived class constructor called." << std::endl;
    }
};

多态

C++ 中的多态是指通过基类的指针或引用调用函数时,程序在运行时根据实际对象的类型来决定调用哪个函数,从而实现同一接口的不同实现。 这使得不同的对象可以以相同的方式被操作,但表现出不同的行为。

类的多态需要满足以下几个条件:

  • 基类的成员函数为虚函数
  • 派生类是公有继承的
  • 派生类对基类的虚函数进行了重新
  • 使用基类的指针或引用指向派生类对象

成员函数的重写

如下所示,当派生类中存在与基类完全一致的函数时,我们称派生类重写了基类的函数。当我们创建一个派生类对象,然后通过基类指针或基类引用指向派生类对象,然后通过基类指针或者基类引用调用对象的Greet()时,实际调用的是基类的Greet()函数:

#include <iostream>

class Base {
public:
    void Greet() {
        std::cout << "Base::Greet()" << std::endl;
    }
};

class Derived : public Base {
public:
    void Greet() {
        std::cout << "Derived::Greet()" << std::endl;
    }
};

int main() {
    Derived obj;
    Base *base = &obj;
    Base &baseRef = obj;
    obj.Greet();
    base->Greet();
    baseRef.Greet();
    return 0;
}


/*
Derived::Greet()
Base::Greet()
Base::Greet()
*/

而当基类中被重写的函数声明为virtual时,情况发生了改变。如下例所示,虽然使用基类指针或基类引用指向派生类对象,但实际调用的还是派生类中实现的函数。也就是说,实际调用的是对象的真实类型中的函数,这便是类的多态,而virtual是实现多态的关键:

#include <iostream>

class Base {
public:
    virtual void Greet() {
        std::cout << "Base::Greet()" << std::endl;
    }
};

class Derived : public Base {
public:
    void Greet() {
        std::cout << "Derived::Greet()" << std::endl;
    }
};

int main() {
    Derived obj;
    Base *base = &obj;
    Base &baseRef = obj;
    obj.Greet();
    base->Greet();
    baseRef.Greet();
    return 0;
}

/*
Derived::Greet()
Derived::Greet()
Derived::Greet()
*/

如下所示,我们通过多态,可以对多种类型的对象,使用同一套存储和调用逻辑进行处理:

  Polygon *p1 = new Polygon();
  Polygon *p2 = new Triangle();
  Polygon *p3 = new Rectangle();
  vector<Polygon *> list = {p1, p2, p3};
  for (auto p : list) {
      p->draw();
  }

基类的析构函数声明为虚函数

当我们使用基类指针指向派生类对象时,当该对象释放时,默认不会调用基类的析构函数,如:

#include <iostream>
#include <vector>

class Base {
public:
    Base() {
        std::cout << "Base::Base()" << std::endl;
    }
    virtual void Greet() {
        std::cout << "Base::Greet()" << std::endl;
    }

    ~Base() {
        std::cout << "Base::~Base()" << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived::Derived()" << std::endl;
    }
    void Greet() {
        std::cout << "Derived::Greet()" << std::endl;
    }
    ~Derived() {
        std::cout << "Derived::~Derived()" << std::endl;
    }
};

int main() {
    Base *base = new Derived();
    delete base;
    return 0;
}


/*
Base::Base()
Derived::Derived()
Base::~Base()
*/

而当基类的析构函数声明为虚函数时,则会顺序调用派生类和基类的析构函数:

#include <iostream>
#include <vector>

class Base {
public:
    Base() {
        std::cout << "Base::Base()" << std::endl;
    }
    virtual void Greet() {
        std::cout << "Base::Greet()" << std::endl;
    }

    virtual ~Base() {
        std::cout << "Base::~Base()" << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived::Derived()" << std::endl;
    }
    void Greet() {
        std::cout << "Derived::Greet()" << std::endl;
    }
    ~Derived() {
        std::cout << "Derived::~Derived()" << std::endl;
    }
};

int main() {
    Base *base = new Derived();
    delete base;
    return 0;
}


/*
Base::Base()
Derived::Derived()
Derived::~Derived()
Base::~Base()
*/

所以,除非基类的析构函数什么都不做,否则,需要将基类的析构函数声明为virtual !

纯虚函数和抽象类

纯虚函数的声明方式:

class Shape
{
public:
    virtual void draw() = 0;
};

具有纯虚函数的类被称作抽象类或接口类;

我们不能直接实例化抽象类,而是必须实现一个继承了抽象类的派生类,并在派生类中实现了基类中的纯虚函数,才可以实例化这个派生类。如果派生类没有实现基类的纯虚函数,那么这个派生类仍然是一个抽象类。

例如:

#include <iostream>
#include <vector>

class Shape {
public:
    virtual void draw() = 0;
};

class Rectangle : public Shape {
public:
    // virtual表示该函数还能继续被子类重写并实现多态
    virtual void draw() {
        std::cout << "Rectangle draw" << std::endl;
    }
};

int main() {
    Shape *shape = new Rectangle();
    shape->draw();
    return 0;
}

override

当我们在派生类中重写基类的虚函数时,使用override关键字,可以让编译器帮我们检查派生类中的函数是否和基类中声明的一致,防止出现意外的错误

如下所示,我们的预期结果是调用派生类的方法,但因为派生类中重写的方法与基类不一致,导致错误的调用到了基类的方法:

#include <iostream>

class Base {
public:
    virtual void Greet(int a) {
        std::cout << "Base::Greet() " << a << std::endl;
    }
};

class Derived : public Base {
public:
    void Greet(int &a) {
        std::cout << "Derived::Greet() " << a << std::endl;
    }
};

int main() {
    Base *base = new Derived();
    int num {10};
    base->Greet(num);
    delete base;
    return 0;
}

// 输出 Base::Greet() 10

这时,我们可以在派生类重新的方法后面加override关键字来检查这种错误:

  void Greet(int &a) override {
      std::cout << "Derived::Greet() " << a << std::endl;
  }

此时如果不小心将参数写成了int &,在编译时会报错:

error: 'void Derived::Greet(int&)' marked 'override', but does not override

final

final限定符的作用,是让基类的虚函数不能再子类中被重写,或标识该类不能被继承,例如:

class Derived : public Base
{
    void Greet() override final;
};
class Derived final : public Base
{
    /*...*/
};