【C++21天养成计划】面向对象入门——继承(Day20)

大家好!我是【AI 菌】,一枚爱弹吉他的程序员。我热爱AI、热爱分享、热爱开源! 这博客是我对学习的一点总结与思考。如果您也对 深度学习、机器视觉、算法、Python、C++ 感兴趣,可以关注我的动态,我们一起学习,一起进步~
我的博客地址为:【AI 菌】的博客
我的Github项目地址是:【AI 菌】的Github


今天的学习要点有:

  • 继承的基本思想
  • C++继承语法
  • 公有继承、私有继承和保护继承
  • 多继承
  • 继承中需要注意的构造顺序和析构顺序

一、什么是继承

大家都知道大头儿子和小头爸爸的故事。小头儿子从爸爸那儿继承了一口标准的普通话和拔萝卜的本领。但是大头儿子并没有继承爸爸小小的脑袋。

现在,如果要你用C++以最简单的方式来模拟大头儿子和小头爸爸,你该怎么做呢?

前面学过类的思想,我们自然可以想到声明两个类。其中Father类里实现小头爸爸的所有属性,Son类中实现大头儿子的所有属性。

仔细想想,这种方法虽然有效,但是有些属性会在上每个类中重复实现,比如会说普通话、拔萝卜等。因此,提出了一种更有效的方案,即我们今天学习的继承

继承:在基类中实现所有通用的功能,比如说标准的普通话、拔萝卜等。从同一个基类可以派生出相似的类,并且在每个派生类中单独实现其特有的属性,从而让每个类都独一无二。

在这里插入图片描述

二、基类与派生类

2.1 概念理解

有些同学,可能还是不能理解基类与派生类的关系,为此我画了个简易的示意图:
在这里插入图片描述
从图中可以看出,这两个派生类均继承于基类。其中,基类也可以称作超类;派生类也可以叫作子类

想象一下,如果我们将作为基类。那么它的派生类就有很多了,比如:草鱼、鲈鱼、武昌鱼、鲤鱼等都可以看作是它的派生。

如果将作为基类,那么麻雀、鹦鹉、燕子、乌鸦等都可看作是它的派生类。

由此就很好总结出基类和派生类的关系了:基类可看做是派生类的总称,它具有派生类最基本的属性。派生类是在基类上的进一步升华,具有自定义的属性和功能。

2.2 派生语法

前面说了很多,下面我们用C++语法来实现一个简单的继承结构吧!

假设Animal是基类,Cat和Fish是继承于它的子类。

#include <iostream>
using namespace std;

class Animal
{
public:
	bool LandAnimal; //陆地动物

	void Life()
	{
		if(LandAnimal)
			cout<<"Life in Land"<<endl;
		else
			cout<<"Life in water"<<endl;
	}
};

class Cat: public Animal
{
public:
	Cat()
	{
		LandAnimal = true;
	}	
};

class Fish: public Animal
{
public:
	Fish()
	{
		LandAnimal = false;
	}
};

int main()
{
	Cat cat1;
	Fish fish1;
	
	cout<<"Cat: ";
	cat1.Life();
	cout<<"Fish: ";
	fish1.Life();

	return 0;
}

运行输出:
在这里插入图片描述

2.3 访问限定符protected

protected 作为访问限定符,能够让基类的某些属性仅在派生类中访问,但不能在继承层次结构外部访问。

在上面一个例子中,存在一个严重的隐患:如果你愿意,可以在main()中修改LandAnimal,因为它被声明为公有的。

比如,你可以在main()中进行如下修改:

cat1.LandAnimal = False;

很显然,这样定义违背了Cat类的属性,是不被允许的。因此在基类中采用LandAnimal用限定符protected进行限定,是一种简单而有效的方法。即:

class Animal
{
protected:
	bool LandAnimal; //仅允许在派生类中访问,但不能在继承层次结构外部访问。
	
public:
	void Life()
	{
		if(LandAnimal)
			cout<<"Life in Land"<<endl;
		else
			cout<<"Life in water"<<endl;
	}
};

三、构造顺序与析构顺序

我们在前面的文章中学习了构造顺序与析构顺序,知道了在实例化对象时,会自动调用类的构造函数;销毁对象时,会自动调用析构函数。

了解构造顺序与析构顺序,可以戳戳:【C++养成计划】类与对象 || 构造函数 || 析构函数(Day10)

那么问题来了,如果Cat类是从Animal类派生而来的,创建Cat对象时,先调用Cat的构造函数还是Animal的构造函数?

另外,实例化对象时,成员属性是在调用构造函数之前还是之后实例化?

实际上,基类Animal在派生类Cat对象之前被实例化,因此,首先构造Cat对象的Animal部分,这样实例化Cat部分时,成员属性就准备就绪,可以使用了。

总的来说,在实例化派生类Cat对象时,会先实例化基类Animal的成员属性,再调用基类Animal的构造函数;再实例化派生类Cat的成员属性,最后调用派生类Cat的构造函数。

因此,在实例化派生类对象时,构造顺序是:先实例化成员属性,再调用构造函数,自顶(基类)向下(派生类)完成。

相反,析构顺序是:自底(派生类)向顶(基类),调用析构函数

四、私有继承

前面介绍的都是公有继承,私有继承不同于公有继承,在指定派生类的基类时使用关键字private,其基本语法如下:

class Base
{
 // ... 基类的成员和方法
};
class Derived: private Base
{
 // ... 派生类的成员和方法
}

私有继承意味着在派生类的实例中,基类的所有公有成员和方法都是私有的——不能从外部访问。即:即使是Base类的公有成员方法,也只能被派生类使用,而不能通过实例化派生类对象来使用它们。

这与公有继承截然不同,从继承层次结构外部看,私有继承并非is-a的关系。私有继承使得只有派生类才能使用基类的属性和方法,因此也被称作has-a关系。

在现实世界中,存在一些私有继承的例子:

基类派生类
MotorCar(汽车,汽车有发动机)
MoonSky(太空,太空中有月亮)
AnimalZoo(动物园,动物园中有动物)

下面来看看汽车与发动机之间的私有继承关系:

#include <iostream>
using namespace std;

class Motor
{
public:
	void SwitchIgnition()
	{
		cout << "Ignition ON" << endl;
	}
	void PumpFuel()
	{
		cout << "Fuel in cylinders" << endl;
	}
	void FireCylinders()
	{
		cout << "Vroooom" << endl;
	}
};

class Car:private Motor // 私有继承
{
public:
	void Move()
	{
		SwitchIgnition();
		PumpFuel();
		FireCylinders();
	}
};

int main()
{
	Car myDreamCar;
	myDreamCar.Move();
	return 0;
}

运行输出:
在这里插入图片描述
分析:Car 类使用关键字 private 继承了 Motor 类,因此Motor 类的成员方法只能被Car 类使用,不能通过Car 类实例化对象使用。

下面这种方式就是典型的错误示范:

Car myDreamCar;
myDreamCar.SwitchIgnition();

运行,编译器将报错,如下所示:
在这里插入图片描述

五、保护继承

保护继承不同于公有继承,在声明派生类继承基类时使用关键字protected。其基本语法如下:

class Base
{
// ... 基类的方法和成员
};
class Derived: protected Base // 保护继承
{
// ... 派生类的方法和成员
};

保护继承与私有继承的类似之处如下:

  • 它也表示 has-a 关系;
  • 它也让派生类能够访问基类的所有公有和保护成员;
  • 在继承层次结构外面,也不能通过派生类实例访问基类的公有成员。

但是,随着继承层次结构的加深,保护继承将与私有继承有些不同:

class Derived2: protected Derived
{
// 派生类Derived2能够访问基类的所有公有和保护成员
};

使用保护继承,派生类Derived2能够访问基类的所有公有和保护成员。这在私有继承中是不被允许的。

下面举一个简单的例子,RaceCar 类以保护方式继承了 Car 类,而 Car 类以保护方式继承了 Motor 类。

#include <iostream>
using namespace std;

class Motor
{
public:
	void SwitchIgnition()
	{
		cout << "Ignition ON" << endl;
	}
	void PumpFuel()
	{
		cout << "Fuel in cylinders" << endl;
	}
	void FireCylinders()
	{
		cout << "Vroooom" << endl;
	}
};

class Car:protected Motor
{
public:
	void Move()
	{
		SwitchIgnition();
		PumpFuel();
		FireCylinders();
	}
};

class RaceCar:protected Car
{
public:
	void Move()
	{
		SwitchIgnition(); // RaceCar has access to members of
		PumpFuel(); // base Motor due to "protected" inheritance
		FireCylinders(); // between RaceCar & Car, Car & Motor
	}
};

int main()
{
	RaceCar myDreamCar;
	myDreamCar.Move()
	
	return 0;
}

运行结果:
在这里插入图片描述
分析:
Car 类以保护方式继承了 Motor 类,而 RaceCar 类以保护方式继承了 Car 类,RaceCar::Move( )的实现使用了基类 Motor 中定义的公有方法。能否经由中间基类 Car 访问终极基类 Motor 的公有成员呢?这取决于 Car 和 Motor 之间的继承关系。如果继承关系是私有的,而不是保护的,RaceCar 将不能访问 Motor 类的公有成员,因为编译器根据最严格的访问限定符来确定访问权。

六、多继承

所谓多继承,从字面意义就能理解,即:一个子类可以有多个基类,它继承了多个基类的特性。

C++ 类可以从多个类继承成员和方法,语法如下:

class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,{
	<派生类类体>
};

其中,访问修饰符继承方式是 public、protected 或 private 其中的一个,用来修饰每个基类,各个基类之间用逗号分隔,如上所示。

现在让我们一起看看下面的实例:

#include <iostream>
 
using namespace std;
 
// 基类 Shape
class Shape 
{
   public:
      void setWidth(int w)
      {
         width = w;
      }
      void setHeight(int h)
      {
         height = h;
      }
   protected:
      int width;
      int height;
};
 
// 基类 PaintCost
class PaintCost 
{
   public:
      int getCost(int area)
      {
         return area * 70;
      }
};
 
// 派生类
class Rectangle: public Shape, public PaintCost
{
   public:
      int getArea()
      { 
         return (width * height); 
      }
};
 
int main(void)
{
   Rectangle Rect; //实例化对象 
   int area;
 
   Rect.setWidth(5);
   Rect.setHeight(7);
 
   area = Rect.getArea();
   
   // 输出对象的面积
   cout << "Total area: " << Rect.getArea() << endl;
 
   // 输出总花费
   cout << "Total paint cost: $" << Rect.getCost(area) << endl;
 
   return 0;
}

运行结果:
在这里插入图片描述


由于水平有限,博客中难免会有一些错误,有纰漏之处恳请各位大佬不吝赐教!

在这里插入图片描述
上一篇:【C++21天养成计划】STL的三大构成——容器 || 迭代器 || 算法(Day19)

养成习惯,先赞后看!你的支持是我创作的最大动力!

AI 菌 CSDN认证博客专家 博客专家 CSDN合作伙伴 算法实习僧
研究僧一枚,CSDN博客专家,公众号【AI 修炼之路】作者。专注于无人驾驶(环境感知方向),热衷于分享AI、CV、DL、ML、OpenCV、Python、C++等相关技术文章。
已标记关键词 清除标记