C++ 面向对象编程(OOP)之继承
什么是面向对象程序设计?
面向对象程序设计的核心思想是数据抽象、继承和动态绑定。通过数据抽象,我们可以将类的接口与实现分离;使用继承,我们可以定义相似的类型并对其相似关系进行建模;使用动态绑定,可以一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
继承
说到继承,离不开两个名词“基类”和“派生类”。继承使得联系在一起的类构成一种层次关系,派生类则直接或间接的由基类继承而来。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
如何继承?下面我们以一个 Animal类作为演示。Animal类的结构如下:
// 基类 Animal
class Animal
{
protected:
std::string name;
int age;
public:
Animal(const std::string &name, int age) : name(name), age(age) {}
void makeSound() const
{
std::cout << name << " makes a sound." << std::endl;
}
void describe() const
{
std::cout << "I am an animal named " << name << ", " << age << " years old." << std::endl;
}
~Animal() {}
};
还记得访问控制一文吗?这里我们把 name和 age声明为 protected,就是想要在派生类中访问这两个成员。接下来我们需要定义一个 Bird类,其继承于 Animal类:
// 派生类 Bird
class Bird : public Animal // 继承的写法
{
private:
std::string color;
public:
Bird(const std::string &name, int age, const std::string &color)
: Animal(name, age), color(color) {} // 在初始化成员列表中要初始化基类
void fly()
{
std::cout << name << " is flying!" << std::endl;
}
};
派生类继承基类必须通过类派生列表明确指出它是从哪个(哪些)基类继承而来的。类派生列表的形式是,首先是一个冒号,后面紧跟以逗号分隔的基类列表,每个积累前面可以有访问说明符。
虚函数
在基类和派生类中,有些成员函数不希望区分对待。而有些成员函数希望在不同的派生类中自定义适合自身的版本。此时基类需要将这些函数声明称虚函数。下面我们将定义适合 Bird类的 makeSound()和 describe()函数。
// 基类 Animal
class Animal
{
protected:
std::string name;
int age;
public:
Animal(const std::string &name, int age) : name(name), age(age) {}
virtual void makeSound() const
{
std::cout << name << " makes a sound." << std::endl;
}
virtual void describe() const
{
std::cout << "I am an animal named " << name << ", " << age << " years old." << std::endl;
}
~Animal() {}
};
// 派生类 Bird
class Bird : public Animal // 继承的写法
{
private:
std::string color;
public:
Bird(const std::string &name, int age, const std::string &color)
: Animal(name, age), color(color) {} // 在初始化成员列表中要初始化基类
void fly() const
{
std::cout << name << " is flying!" << std::endl;
}
void makeSound() const override
{
std::cout << name << " sings." << std::endl;
}
void describe() const override
{
std::cout << "I am a bird named " << name << ", " << age << " years old." << std::endl;
}
};
在C++中,基类必须将它的两种成员函数区分开来;一种是基类希望其派生类进行覆盖的函数,一种是基类希望派生类直接继承而不要改变的函数。对于前者,基类通常将其定义为虚函数,通过说明符 virtual来指定。有关虚函数的更多知识,我们会在下文中讲述。
在派生类中,覆盖基类的成员函数的方法是再重新声明定义一遍。派生类可以在它所覆盖的函数前使用 virtual关键字,但不是必须。C++11新标准允许派生类显式地注明它使用某个成员函数,具体做法是在形参列表后面、或是在const成员函数的const关键字后面、或是在引用成员函数的引用限定符后面添加一个关键字 override。
派生类对象向基类的类型转换
当我们定义一个派生类对象时,该对象既有基类的成员,又有派生类新增的成员。因为如此,我们能够把派生类的对象当作基类对象来使用,我们能将基类的指针或引用绑定在派生类对象中的基类部分上。这种转换称为派生类到基类的类型转换。
Bird a("Foo", 3, "red");
Animal* p = &a; // Animal类型的指针可以绑定在Bird类的对象上
p->describe(); // I am a bird named Foo, 3 years old.
Animal& q = a; // Animal类型的引用可以是Bird类的对象
q.makeSound(); // Foo sings.
return 0;
这种转换是隐式的,假设我们有一个函数需要传入 Animal类参数的引用(或指针),我们能够将 Bird类传入函数。
不存在基类向派生类的隐式类型转换,对象之间不存在类型转换。
注意基类和派生类的定义顺序
如果我们想讲某个类用作基类,则该类必须已经被定义。这一规定的原因显而易见,派生类包含基类继承而来的成员,为了使用撰写成员,派生类需要知道它们是什么。
一个类不能派生自身。为什么?
我们将基类和派生类定义在不同的文件中。
基类:
Animal.h
派生类:
Bird.h
将其放在与主函数文件的同目录下,然后尝试分别添加这两个头文件,来试试什么情况下会报错,什么情况下能够正常编译通过?
#include "Animal.h"
#include "Bird.h"
先引用基类头文件,如此下来,基类就被定义好了,再引用派生类头文件,能够正常编译。否则会报错“基类未定义”。
看下面一个继承关系:
Animal--派生->Bird--派生->Parrot
此时称Animal是Bird的直接基类,是Parrot的间接基类。
阻止继承
C++11提供一种组织该类被继承的方法,就是在类名后面跟一个final关键字。
class Animal final{/../};
评论区