继承是面向对象编程最重要的概念之一。

继承允许我们根据另一个类来定义一个类。 这有助于更轻松地创建和维护应用程序。

当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为(base)基类,新建的类称为(derived)派生类。

派生类继承了基类的所有特性,并且可以拥有自己的附加特性。

为了演示继承关系,我们通过创建一个Father类和Daughter类来进行演示。

class Father
{
 public:
  Father() {};
  void sayHellow() {
    cout << "hellow,W3Cschool";
  } 
};

class Daughter 
{
 public: 
  Daughter() {};
};

Father类中有一个sayHellow()的公共方法.
实例中通过Father类派生出Daughter类。

class Daughter : public Father
{
 public: 
  Daughter() {};
};

通过:(冒号)加上public(访问说明符)可以指定基类,public代表基类中的所有公共成员在派生类中同样也是公共的。

我们可以理解为,Father类中的所有公共成员都成为了Daughter类的公共成员。

由于Father类中的所有公共成员都被Daughter类继承了。我们可以创建一个Daughter类型的对象,并通过该对象调用Father类中的sayHellow()函数。

#include <iostream>
using namespace std;

class Father
{
 public:
  Father() {};
  void sayHellow() {
   cout << "hellow,w3cschool";
  }
};

class Daughter: public Father
{
 public:
  Daughter() {};
};

int main() {
  Daughter d;
  d.sayHellow();
}
//结果将会输出 "hellow,w3cschool"

派生类继承了所有的基类方法,但有以下几个例外:

基类的构造函数、析构函数和拷贝构造函数。
基类的重载运算符。
基类的友元函数。
通过逗号进行分隔可以让派生类指定多个基类。例如 狗:public 哺乳动物,public 犬科动物

访问说明符

Public成员可以从类外任何地方访问,而private成员的访问仅限于类和友元函数。

正如我们以前所见,使用公共方法访问私有类变量是一个好习惯。
除此之外还有一个访问说明符 - protected。

protected的成员变量或函数与私有成员非常相似,唯一的区别就是 - 可以在派生类中访问它。

class Father {
 public:
  void sayHellow() {
   cout << var;
  }

 private:
  int var=0;

 protected:
  int someVar;
};

someVar可以被Father类的所有派生类访问,而var则不行。
访问说明符也被用来指定继承的类型。

注意,我们使用public来继承Father类:

class Daughter: public Father

继承的类型也支持private和protected

公共继承(Public Inheritance):基类的public成员成为派生类的public成员,基类的protected成员成为派生类的protected成员。 基类的private成员永远不能直接从派生类访问,但是可以通过调用基类的public和protected成员来访问。
受保护继承(Protected Inheritance):基类的public和protected成员成为派生类的受保护成员。
私有继承(Private Inheritance):基类的public和protected成员成为派生类的私有成员。
Public是最常用的继承类型,继承类型默认为Private

当继承类时,基类的构造函数和析构函数不会被继承。

但是,当派生类的对象被创建或删除时,它们将被调用。

为了进一步解释这种行为,我们来创建一个包含构造函数和析构函数的示例类:

class Father {
 public:
 Father() 
 {
  cout <<"Father构造函数"<<endl;
 }
 ~Father()
 {
  cout <<"Father析构函数"<<endl;
 }
};

在main中创建一个对象会产生一下的输出:

int main() {
  Father f;
}
/* 输出
Father构造函数
Father析构函数
*/

接下来,让我们创建一个Daughter类,使用它自己的构造函数和析构函数,并使其成为Father的派生类:

class Daughter: public Father {
public:
 Daughter()
 {
  cout <<"Daughter构造函数"<<endl;
 }
 ~Daughter()
 {
  cout <<"Daughter析构函数"<<endl;
 }
};

当我们创建一个Daughter对象时会发生什么?(Daughter继承了Father,并且他们都声明了自己的构造函数与析构函数)

int main() {
  Daughter f;
}

/*输出
Father 构造函数
Daughter 构造函数
Daughter 析构函数
Father 析构函数
*/

注意,首先调用基类的构造函数,然后调用派生类的构造函数。

当对象被销毁时,派生类的析构函数被调用,然后调用基类的析构函数。

可以理解为,派生类需要它的基类才能工作,这就是为什么基类是首先设置的原因。

多态

多态按字面的意思就是多种形态。

当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

简单地说,多态意味着单个函数可以有多个不同的实现。

接下来我们将用例子来更清晰的了解多态。

假设我们现在要制作一个简单的游戏,游戏需要先创建一个角色,角色可以选择很多种不同的职业:法师,战士,射手等。但是这些职业有一个共同的功能就是攻击。不过由于职业性质的不同攻击的方式也会不一样,在这种情况下,多态允许在不同的对象上调用相同的攻击函数,但会导致不同的行为。

第一步是创建角色

class Role {
 protected: 
  int attackPower;
 public:
  void setAttackPower(int a){
   attackPower = a;
  }
};

我们的Role类有一个名为setAttackPower的公共方法,它设置受保护的成员变量attackPower。

接下来我们为两个不同的职业,战士和法师创建类。这两个类都将继承Role类,所以他们都有attackPower(攻击力),但是他们又都有自己特定的攻击方式。

class Warrior: public Role{
 public:
  void attack() {
   cout << "剑刃风暴! - "<<attackPower<<endl;
  }
};

class Magician: public Role {
 public:
  void attack() {
   cout << "冰暴! - "<<attackPower<<endl;
  }
};

如上所示,他们的攻击方式各不相同。接下来我们准备创建我们的战士和法师的对象。

int main() {   
 Warrior w;
 Magician m;  
}

战士和法师都继承了Role类,所以战士和法师都是角色。所以我们可以实现以下的功能。

Role *r1 = &w;
Role *r2 = &m;

我们现在已经创建了两个Role类型的指针,指向战士和法师的对象。
现在,我们可以调用相应的功能:

int main() {
  Warrior w;
  Magician m;
  Role *r1 = &w;
  Role *r2 = &m;

 r1->setAttackPower(50);
 r2->setAttackPower(80);

 w.attack();
 m.attack();
}

/* 输出:
剑刃风暴! - 50
冰暴! - 80
*/

通过直接在对象上调用函数,我们可以达到相同的效果。 但是,使用指针效率更高

抽象函数

前几节的例子演示了派生类与基类指针的使用方法。接下来我们接着之前游戏的例子,我们的每一个角色都有一个attack()函数。

为了能够让Role指针为每一个派生类提供调用attack()函数,我们需要在基类将函数声明成抽象函数。

在基类中声明一个抽象函数,在派生类中使用相应的函数,多态允许使用Role指针来调用派生类的函数。

每个派生类将覆盖attack()函数并有一个单独的实现:

class Role{
 public:
  virtual void attack() {
  }
};

class Warrior: public Role {
 public:
  void attack() {
   cout << "剑刃风暴!"<<endl;
  }
};

class Magician: public Role {
 public:
  void attack() {
   cout << "冰暴!"<<endl;
 }
};

通过关键字virtual可以将基类的函数声明成抽象函数。

现在,我们可以使用Role指针来调用attack()函数。

int main() {
  Warrior w;
  Magician m;
  Role *r1 = &w;
  Role *r2 = &m;

  r1->attack();
  r2->attack();
}

/* 输出:
剑刃风暴!
冰暴!
*/

由于attack()函数被声明为抽象的,它就像一个模板,告诉派生类自己有一个attack()函数。

如果基类中的函数是抽象的,则派生类中的函数实现将根据所引用对象的实际类型进行调用,而不管原先声明的是那种类型。

声明或者继承了一个抽象函数的类被称为一个多态类。
抽象函数也可以在基类中实现。

class Role {
 public:
  virtual void attack() {
   cout << "角色!"<<endl;
  }
};

class Warrior: public Role {
 public:
  void attack() {
   cout << "战士!"<<endl;
  }
};

class Magician: public Role {
 public:
  void attack() {
   cout << "法师!"<<endl;
  }
};

现在,当你创建一个Role指针,并调用attack()函数时,编译器会调用该指针指向的对应于该对象类型的函数:

int main() {
 Warrior w;
 Magician m;
 Role r;

 Role *r1 = &w;
 Role *r2 = &m;
 Role *r3 = &r;

 r1->attack();
 // 输出 "战士!"

 r2->attack();
 // 输出 "法师!"

 r3->attack();
 // 输出 "角色!"
}

当通过基类指针调用抽象函数时,它调用基类指针指向的那些类的函数

纯虚函数

在某些情况下,你希望在一个基类中包含一个抽象函数,以便它可以在派生类中被重新定义以适应该类的对象,但是没有有意义的定义可以给基类中的函数类。

没有定义的抽象成员函数被称为纯虚函数。他们指定派生类自己定义该函数。

语法是用= 0(一个等号和一个零)替换它们的定义:

class Role {
 public:
  virtual void attack() = 0;
}; 

一个纯虚函数基本上定义了派生类将自己定义的那个函数。

从具有纯虚拟函数的类继承的每个派生类必须重写该函数。

如果纯虚函数没有在派生类中重写,那么当您尝试实例化派生类的对象时,代码将无法编译并导致错误。

Role类中的纯虚函数必须在其派生类中重写。

class Role {
 public:
  virtual void attack() = 0;
};

class Warrior: public Role {
 public:
  void attack() {
   cout << "战士!"<<endl;
  }
};

class Magician: public Role {
 public:
  void attack() {
   cout << "法师!"<<endl;
  }
};

抽象类

你不能对一个有纯虚函数的基类创建对象。

下列例子将会报错。

Role r; // Role拥有一个纯虚函数,这样创建对象将会报错

这些类被称为抽象类。他们只能被当作基类使用,因此被允许具有纯虚函数。

你可能会认为抽象基类是无用的,但事实并非如此。 它可以用来创建指针并利用其多态性质。

例如:

Warrior w;
Magician m;
Role *r1 = &w;
Role *r2 = &m;

r1->attack();
r2->attack();
最后修改:2022 年 12 月 05 日
如果觉得我的文章对你有用,请随意赞赏