【C++要笑着学】多态 | 重写(覆盖) | 协变构多态 | 父虚子非虚也构多态 | final与override关键字(C++11) | 抽象类 | 纯虚函数 | 抽象类指针

简介: 本章我们继续讲解面向对象三大特性,上一章我们讲解了继承,本章我们讲解多态。从多态的概念一步步讲解,介绍构成多态的必要条件和两个例外。还会顺带讲解一下 C++11 更新的两个和多态有关系的关键字 final 和 override。上一章我们讲解了虚函数,本章会先补充纯虚函数的概念,然后再引入抽象类的概念。本章我们只学习C++多态的基础知识,至于多态的原理(VTBL、决议等)的难度相对较大,我们单独放到下一章去做专门讲解。

💭 写在前面



"大家好久不见,我们最近比较忙,专栏的更新速度明显缓慢了许多,我们会尽力更新的。"


本章我们继续讲解面向对象三大特性,上一章我们讲解了继承,本章我们讲解多态。从多态的概念一步步讲解,介绍构成多态的必要条件和两个例外。还会顺带讲解一下 C++11 更新的两个和多态有关系的关键字 final 和 override。上一章我们讲解了虚函数,本章会先补充纯虚函数的概念,然后再引入抽象类的概念。本章我们只学习C++多态的基础知识,至于多态的原理(VTBL、决议等)的难度相对较大,我们单独放到下一章去做专门讲解。


Ⅰ. 多态(polymorphism)


0x00 多态的概念

006cb72223297eb027087332d79ca830_c6d8d91fa4004eabb688dbe5a5a4378d.png

多态,就是 "多种形态" 的意思。


说具体点就是:去完成某个行为,不同的对象去做会产生不同的结果(状态)。

b3459285b9c3fdb218f46935d5fe0a93_5ebbad659b5e46188edb8bc257dc4143.png

比如说地铁站买票这个行为,普通人、学生、军人买票是不同的。


普通人必须买全价票,学生就可以买半价票,而军人可以优先购买到预留票:

f386ccc5b5ec93aa7800c47aaf2b1c6b_c1235a7e9ef34389a6034b607a1dd57f.png

比如有一个 BuyTicket 买票的成员函数,创建普通人、学生和军人三个对象,他们调用该函数形态结果我们就要设计成不一样的。


再举个例子,某款APP推出会员,分为以下四种用户类型:


这些不同类型的用户去调用商品的 Pay 付款接口,形态结果是不一样的。


比如会员会给你来一个:


"尊敬的xxx会员您好,您本次消费xxx元~ "


可能还会给你安排联系专属客服的按钮,然后在结算时给你显示出打折后的价格,就是这么高贵!


所以由此可见,我们需要一种特性来做到这种 "分类" 的操作,这时我们就可以将其实现成多态。


0x01 重写(覆盖)

我们先用代码实现一下我们刚才的购票场景。


这里我们当然是用继承了,将 Student 和 Soldier 继承自 Person:

class Person {};
class Student : public Person {};
class Soldier : public Person {};

并且既然是要设计多态,我们就要为这三个身份设计不同的 BuyTicket 接口,


void BuyTicket() {...}

这里用 virtual 虚函数,并且做到函数名、参数和返回值相同,就能够达到 "重写" 的效果:

class Person {
public:
//  [virtual] + [返回值] + [函数名] + [参数] 相同 = 构成多态
        👇         👇         👇       👇
      virtual     void     BuyTicket   ()  {
          cout << "Person: 买票-全价 100¥" << endl;
      }
};
class Student : public Person {
public:
    // 这里也都相同
    virtual void BuyTicket() {
        cout << "Student: 买票-半价 50¥" << endl;
    }
};
class Soldier : public Person {
public:
    // 这里也都相同
    virtual void BuyTicket() {
        cout << "Soldier: 优先买预留票-全价 100¥" << endl;
    }
};


📚 概念:重写也称为覆盖,重写即重新改写。


重写是为了将一个已有的事物进行某些改变以适应新的要求。


重写是子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变。 即:


"外壳不变,核心重写。"


最后我们再设计一个 Pay 函数去接收不同的身份,以调用对应的 BuyTicket 函数。


这里我们可以用指针和引用,这里我们先用指针:

void Pay(Person* ptr) {
    ptr->BuyTicket();
}

(构成多态的条件我们下面会细讲,这里先看代码实现)


💬 代码演示:BuyTicket

#include <iostream>
using namespace std;
class Person {
public:
  // 虚函数
  virtual void BuyTicket() {
  cout << "Person: 买票-全价 100¥" << endl;
  }
};
class Student : public Person {
public:
  // 虚函数 + 函数名/参数/返回 -> 重写(覆盖)
  virtual void BuyTicket() {
  cout << "Student: 买票-半价 50¥" << endl;
  }
};
class Soldier : public Person {
public:
  // 虚函数 + 函数名/参数/返回 -> 重写(覆盖)
  virtual void BuyTicket() {
  cout << "Student: 优先买预留票-全价 100¥" << endl;
  }
};
/* 接收身份 */
void Pay(Person* ptr) {
  ptr->BuyTicket();  // 到底是谁在买票,取决于传来的是谁
}
int main(void)
{
  int option = 0;
  Person p;
  Student st;
  Soldier so;
  do {
  cout << "1.普通人  2.学生  3.军人" << endl;
  cout << "请选择身份:";
  cin >> option;
  switch (option)
  {
  case 1:
    Pay(&p);
    break;
  case 2:
    Pay(&st);
    break;
  case 3:
    Pay(&so);
    break;
  default:
    cout << "输入错误!请重新输入!" << endl;
    break;
  }
  } while (option != -1);
  return 0;
}


🚩 运行结果:

c86ec9b073d47527f7fdb99309d4fb27_ba1d4d60133c4b3cbe41c4bfa0fc51ee.png

我们现在还想在用户选完名字后可以输入姓名:

cout << "1.普通人  2.学生  3.军人" << endl;
cout << "请选择身份:";
cin >> option;
cout << "请输入你的姓名:";
string name;
cin >> name;

因此我们增加一个 _name 成员变量,并且写好初始化构造:


class Person {
public:
  Person(const char* name)
  : _name(name) {}
  // 虚函数
  virtual void BuyTicket() {
  cout << _name << ": " << "Person: 买票 - 全价 100¥" << endl;
  }
protected:
  string _name;
};
class Student : public Person {
public:
  Student(const char* name)
  : Person(name) {}
  // 虚函数 + 函数名/参数/返回 -> 重写(覆盖)
  virtual void BuyTicket() {
  cout << _name << ": " << "Student: 买票-半价 50¥" << endl;
  }
};
class Soldier : public Person {
public:
  Soldier(const char* name)
  : Person(name) {}
  // 虚函数 + 函数名/参数/返回 -> 重写(覆盖)
  virtual void BuyTicket() {
  cout << _name << ": " << "Soldier: 优先买预留票-全价 100¥" << endl;
  }
};

由于多了一个参数,我们创建对象的地方也需要修改,我们可以直接 new:


/* 接收身份 */
void Pay(Person* ptr) {
  ptr->BuyTicket();  // 到底是谁在买票,取决于传来的是谁
  delete ptr;
}
int main(void)
{
  int option = 0;
  //Person p;
  //Student st;
  //Soldier so;
  do {
  cout << "1.普通人  2.学生  3.军人" << endl;
  cout << "请选择身份:";
  cin >> option;
  cout << "请输入你的姓名:";
  string name;
  cin >> name;
  switch (option) {
    case 1:
    Pay(new Person(name.c_str()));
    break;
    case 2:
    Pay(new Student(name.c_str()));
    break;
    case 3:
    Pay(new Soldier(name.c_str()));
    break;
    default:
    cout << "输入错误!请重新输入!" << endl;
    break;
  }
  cout << endl;
  } while (option != -1);
  return 0;
}


🚩 运行结果:

8359454cb09dac61b07da7551eacbae5_2d9b721119044316bc0e6f40abda9175.png

我们现在就做到了不同类型的对象做不同的事情,这就是用多态实现出来的效果。


0x02 多态构成的条件

多态是 在不同继承关系的类对象中去调用同一个函数,产生了不同的行为。


比如我们刚才的 Student 继承了 Person,Person 买票是全价,但 Student 买票却是半价:

42832d638fbf8e737b894d911602d3a7_9dc7840e1123408aa6b4201074b8452a.png

📌 注意:继承中想要构成多态,必须满足以下两个条件:


① 必须是子类的虚函数重写成父类函数(重写:三同 + 虚函数)

② 必须是父类的指针或者引用去调用虚函数。

* 三同指的是:同函数名、同参数、同返回值。


* 虚函数:即被 virtual 修饰的类成员函数。


💬 代码演示:既然还可以用引用去调用虚函数,我们来把它改成引用的方式接收身份试试:

/* 接收身份 */
void Pay(Person& ref) {
  ref.BuyTicket();  // 到底是谁在买票,取决于传来的是谁
}

我们不用 new 了,我们试试能不能在 case 里面创建 Person、Student、Solider 然后传给 Pay:

switch (option) {    ❌
    case 1:
    Person p(name.c_str());
    Pay(p);
    break;
    case 2:
    Student st(name.c_str());
    Pay(st);
    break;
    case 3:
    Soldier so(name.c_str());
    break;
    default:
    cout << "输入错误!请重新输入!" << endl;
    break;
  }

1b300bf4d288d22754ec10d5cbed3877_1fbb7cb006fe4881927c9750d728df55.png

啊这,这是为什么,难道 switch 里面不能定义对象吗?遇到问题,百度一下看看:

102200669120b454c3c62286c8f11efc_4bd4d28aabef4f47ad81d416d39fc142.png

放个花括号?我们加上试试看:


int main(void)
{
  int option = 0;
  do {
  cout << "1.普通人  2.学生  3.军人" << endl;
  cout << "请选择身份:";
  cin >> option;
  cout << "请输入你的姓名:";
  string name;
  cin >> name;
  switch (option) {
    case 1: {
    Person p(name.c_str());
    Pay(p);
    break;
    }
    case 2: {
    Student st(name.c_str());
    Pay(st);
    break;
    }
    case 3: {
    Soldier so(name.c_str());
    break;
    }
    default:
    cout << "输入错误!请重新输入!" << endl;
    break;
  }
  cout << endl;
  } while (option != -1);
  return 0;
}

🚩 运行结果:

0975b6504fe1bc32b4cfe8e5529d4b3a_ca1324e560724ac38a5feebd6d26bd6e.png

(加了花括号就可以了)


由此可见,我们也可以用父类的引用去调用虚函数。


❓ 思考:用对象行不行呢?


void Pay(Person* ptr) {    // 指针
  ptr->BuyTicket();  
  delete ptr;
}
void Pay(Person& ref) {    // 引用
  ref.BuyTicket();
}
void Pay(Person obj) {     // 用对象可以吗?
  obj.BuyTicket();
}

不可以,我们说过,必须是父类的指针或者引用才能调用虚函数。


对象虽然可以传,但是符合不了多态的条件,自然就无法构成多态:

05a8dd76018ba3bd2a4c32f44064ae6b_cff6d4eff4d7465c9854dce3b7162194.png

这时调用的都是父类的那一个,具体的原因我们放到下一章讲解多态原理的时候去说明。


至此,我们验证了 "必须是父类的指针或者引用去调用虚函数" 这段话。


0x03 协变构成多态

虚函数重写有两个例外,我们先讲解第一个例外。


💬 观察下面的代码,并没有达到 "三同" 的标准,它的返回值是不同的,但依旧构成多态:

class A {};
class B : public A {};
class Person {
public:
  virtual A* f() {
  cout << "virtual A* Person::f()" << endl;
  return nullptr;
  }
};
class Student : public Person {
public:
  virtual B* f() {
  cout << "virtual B* Student:::f()" << endl;
  return nullptr;
  };
};
int main(void)
{
  Person p;
  Student s;
  Person* ptr = &p;
  ptr->f();
  ptr = &s;
  ptr->f();
  return 0;
}

🚩 运行结果:

d6ae1d8bc696805eb9a3f4bbbfea6d7c_b51383a1252b48aaa8bbe8c5df611c0a.png


💡 因为虚函数的重写要求有一个例外 —— 协变(Covariant)。


但是协变也是有条件的,协变的类型必须是父子关系。


💬 代码演示:当你不是父子关系时,就不能协变:

class A {};
class B {};   // 我们取消A与B的父子关系
class Person {
public:
  virtual A* f() {
  cout << "virtual A* Person::f()" << endl;
  return nullptr;
  }
};
class Student : public Person {
public:
  virtual B* f() {
  cout << "virtual B* Student:::f()" << endl;
  return nullptr;
  };
};

🚩 运行结果:(报错)

error C2555: “Student::f”: 重写虚函数返回类型有差异,且不是来自“Person::f”的协 message : 参见“Person::f”的声明


当然了,刚才用的是指针,引用也是可以的:

class A {};
class B : A {};
class Person {
public:
  virtual A& f() {
  cout << "virtual A* Person::f()" << endl;
  return nullptr;
  }
};
class Student : public Person {
public:
  virtual B& f() {
  cout << "virtual B* Student:::f()" << endl;
  return nullptr;
  };
};


0x04 父虚子非虚构成多态

我们现在来讲第二个例外。


💬 父类的虚函数没了无法构成多态:

class Person {
public:
  A* f() {
  cout << "virtual A* Person::f()" << endl;
  return nullptr;
  }
};
class Student : public Person {
public:
  virtual B* f() {
  cout << "virtual B* Student:::f()" << endl;
  return nullptr;
  };
};
int main(void)
{
  Person p;
  Student s;
  Person* ptr = &p;
  ptr->f();
  ptr = &s;
  ptr->f();
  return 0;
}

🚩 运行结果:

ada5c4733dabff3313d26b82bcb8dc30_a263225e9f524f4cb3acb3b530ac6ac3.png


💬 但是,子类的虚函数没了却能构成多态:

class A {};
class B : public A {};
class Person {
public:
  virtual A* f() {
  cout << "virtual A* Person::f()" << endl;
  return nullptr;
  }
};
class Student : public Person {
public:
  B* f() {
  cout << "virtual B* Student:::f()" << endl;
  return nullptr;
  };
};
int main(void)
{
  Person p;
  Student s;
  Person* ptr = &p;
  ptr->f();
  ptr = &s;
  ptr->f();
  return 0;
}

🚩 运行结果:

cd11927998b5c94b9430d06d8fe38b5c_d05950bfba5745bdbc32640e86e54bd7.png

啊这,这都不是虚函数了怎么也能构成多态呢?怎会如此!


💡 解答:子类虚函数没有写 virtual,但 f 依旧是虚函数,是因为先继承了父类的函数接口声明。


子类继承父类的虚函数是一种接口继承,所以即使子类的 virtual 没写,它也是虚函数,符合多态条件。


这是重写父类虚函数的实现,也就是说父类有 virtual 的属性,子类也就有了。


最后,虽然子类虚函数可以不加 virtual,但是我们自己写的时候 子类虚函数建议加上 virtual。


🔺 总结:父类为虚函数,子类继承其父的情况下,即使不声明 virtual 也能构成多态。


0x05 析构函数的重写

💬 观察下列代码:

class Person {
public:
  ~Person() {
  cout << "~Person()" << endl;
  }
};
class Student : public Person {
public:
  ~Student() {
  cout << "~Student()" << endl;
  }
};
int main(void)
{
  Person p;
  Student s;
  return 0;
}

🚩 运行结果:

bada3ba122666f840edab95ae01df43e_7092f03b8ac7471cbbce71f381580633.png

❓ 思考:这三行分别是谁的?


💡 解读:第一行和第二行是 Student s 的,第三行是 Person p 的。我们来看看析构顺序,Student s 是后定义的,析构顺序是后定义先析构。根据子类对象析构先子后父,调用子类的析构函数结束后自动调用父类的析构函数,所以第一行的 ~Student() 和第二行的 ~Person() 都是 Student 的,随后第三行的 ~Person() 是 Person p 自己调的。


现在这两个析构函数默认是隐藏关系,


因为它们的函数名会被同一处理修改成 destructor:

696423e07210999f6483b086f07fe512_799d89fd8c884c1d82161b25f8b15a6b.png

但是如果我用 virtual 修饰 ~Person,我们知道,如果这加了不管 ~Student 加不加 virtual,


子类都会跟着父类变身成 virtual,那么现在这两个析构函数还是隐藏关系吗?


如果 Person 的析构函数加了 virtual,它们的关系就变了:

8175db5187aceea2a5b1523594f03b83_6575bff23e93414590dcca4c7c7fe123.png

然并卵,对普通对象来说,这里加 virtual 并不会带来什么改变,对于普通对象是没有影响的:

13740b2838085ec99e98788dbe4c84f4_c25350470f8c43008a5e90432711da9f.png

class Person {
public:
  virtual ~Person() {
  cout << "~Person()" << endl;
  }
};
class Student : public Person {
public:
  // 隐藏(重定义)关系 -> 重写(覆盖) 关系
  ~Student() {
  cout << "~Student()" << endl;
  }
};
int main(void)
{
  // delete 调用 Person 的析构,对这个也没有影响
  Person* ptr1 = new Person;
  delete ptr1;
  // 但是对这样的场景会产生影响
  Person* ptr2 = new Student;
  delete ptr2;
  return 0;
}

🚩 运行结果:

bd341fd570c7b1c4e5ecdf8aac1f58e8_82e7b4f8e1304e409dae5df16c0672cc.png


干脆直接用一个 ptr 去演示好了:


Person* ptr = new Person;
  delete ptr;   // ptr->destructor() + operator delete(ptr)
  ptr = new Student;  
  delete ptr;   // ptr->destructor() + operator delete(ptr)

刚才我们看到了,如果这里不加 virtual,~Student 是没有调用析构的。


你可能会想这有啥,那是因为这里没场景,这其实是非常致命的,是不经意间会发生的内存泄露。


💬 比如下面这个场景,我们是希望 delete 谁调用的就是谁的析构:

class Person {
public:
  ~Person() {
  cout << "~Person()" << endl;
  }
};
class Student : public Person {
public:
  // 隐藏(重定义)关系 -> 重写(覆盖) 关系
  ~Student() {
  cout << "~Student()" << endl;
  delete[] _name;
  cout << "delete: " << (void*)_name << endl;
  }
private:
  char* _name = new char[10]{ 'h', 'e', 'l', 'l', 'o' };
};
int main(void)
{
  // 我们期望 delete ptr 调用析构函数是一个多态调用
  Person* ptr = new Person;
  delete ptr;   // ptr->destructor() + operator delete(ptr)
  ptr = new Student;  
  delete ptr;   // ptr->destructor() + operator delete(ptr)
  return 0;
}

🚩 运行结果:

5cb536049aa774a9afc8f89ed645d071_c81e4a8033334e3482666c2e6937eb87.png

但是结果让我们很失望,Student 没析构!


💬 我们加上 virtual 再试试:

class Person {
public:
  virtual ~Person() {
  cout << "~Person()" << endl;
  }
};
...

🚩 运行结果:

23ec1961d22b41f2743fb3999e191ebe_16cf320677d04c489d6b53c4f08b129f.png

 完成了正确的调用。


🔺 结论:如果设计的类可能会作为父类,析构函数最好设计成虚函数,即加上 virtual。

43ff318269cdef167054573223e10afc_2c59d11521a54c58bf8c57816d35ae2d.png

像刚才这种场景不加上 virtual 就会发生内存泄露,可怕的是还是悄无声息的!


报错不可怕,怕的是这种悄无声息的,像这种内存泄露找起来可是相当的恶心。


0x06 final 关键字(C++11)

如果我有个虚函数,但我不想让它被人重写:

class Car {
public:
  // 我是虚函数,但我不想被人重写,怎么办?
  virtual void Drive() {}
};
class Benz : public Car {
public:
  // Drive 被人重写了
  virtual void Drive() { 
  cout << "Benz-舒适" << endl; 
  }
};

这种情况,就可以将 C++11 的 final 关键字置于函数尾部:

class Car {
public:
  virtual void Drive() final {}
};
class Benz : public Car {
public:
  virtual void Drive() {  ❌
  cout << "Benz-舒适" << endl; 
  }
};

🚩 运行结果:(报错)


E1850   无法重写“final”函数 "Car::Drive"  

a66973fdebf0b176e2b0588c271d1ca3_32fee5f5114241858e4c66a8fbc095b1.png

final 不仅能让虚函数不能被重写,还能让直接把类的继承功能一刀砍了。


我们上一章探讨过实现一个 "不能被继承的类" ,是通过将构造函数私有化的方式去实现的,


构造函数私有化后子类就创建不出对象了,自然也就不能用了。


但是这种 "将构造函数私有化的方式" 其实并不是一种特别好的方式:


class Car {
public:
private: // 将构造函数私有化
  Car() {}
};
class Benz : public Car {
public:
  virtual void Drive() {
  cout << "Benz-舒适" << endl; 
  }
};
int main(void) {
  return 0;
}

这样运行后不会报错,因为它并不阻止继承行为,它还是会给子类继承的,


这种方式是以间接的方式去实现的,当你创建子类对象的时候才会报错:

int main(void) {
  Benz car; ❌
  return 0;
}

间接让子类创建不了对象,因为子类的构造函数必须调用父类的构造函数进行初始化,


而父类的构造函数被私有化了,子类不可见。通过这样联合的方式间接实现了 "不能被继承的类"。


💬 上面的方式是 C++98 的间接方式,到了C++11 final 登场后就有了更直观的实现方式:

// C++11
class Car final {
public:
  Car() {}
};
// 此时就不能被继承了
class Benz : public Car ❌ {
public:
  virtual void Drive() {
  cout << "Benz-舒适" << endl; 
  }
};

将 final 放在类名后面,该类就不能被继承了,因此你不用创建对象他就可以报错给你检查出来。


final 的意思是 "最终的",可以理解为是最终的类了,不能再继承了。


以后如果你想让某个类不能被继承,就可以在类名后面加上 final 关键字。

0714be909ed3944a3c184f4a7eb830b0_bf96f620c0a04977b9819c30898cd78b.png

" 用屠龙宝刀 final 一刀给它阉了,直接断子绝孙,既然就无法继承了。"


🔺 总结:final 的两个作用


让虚函数不能被重写

让类不能被继承

0x07 override 关键字(C++11)

相信大家也体会到了 C++ 对函数重写的要求是非常严格的,


但是人难免会犯错,有些时候可能会导致函数名次序写反而无法构成重载,


而这种错误在编译期间是不会报的,因此往往只有在程序运行时你发现没有得到预期结果,


去 debug 找个半天才能将问题找出,这会让人感到非常的不爽:


"妈的,原来是函数名次序写反了"


C++11 为了增加容错率,推出了 final 和 override,find 是禁止重写,override 是必须重写。


find 我们刚才说过了,我们现在介绍一下 override


💬 代码演示:override 关键字可以帮助你检查重写:

// C++11
class Car {
public:
  virtual void Drive() {}
};
// override 写在子类中:要求严格检查是否完成重写,如果没有重写就报错
class Benz : public Car {
public:
  virtual void Drive() override {
  cout << "Benz-舒适" << endl; 
  }
};
 如果没有完成重写,就会报错:
class Car {
public:
  void Drive() {}  ❌
};
class Benz : public Car {
public:
  virtual void Drive() override {
  cout << "Benz-舒适" << endl; 
  }
};

367d7a668f14470a5ced9f2e8303e909_37794aa653294aefac62a203e92693a6.png

有了 override 修饰,像如果没有加 virtual 或参数不同就会报错。


当然,子类是可以省略 virtual 的,父类有 virtual 子类继其虚,override 不会犯病报错放心使用。


override 在某些场景是非常有用的,拿我们刚才举的 "函数名次序写反了” 的例子说:

d9f7d96436bfa1ad34e29b56e2acc83b_92bc5d9fdcfc4d5c8aca24e5b9ce2b9d.png

在想要重写的地方加上 override,如果你大意了不小心没构成重写,


它能直接报错,能让你免去痛苦的调试找 BUG 环节!请说:谢谢 override!


" 谢谢 override "

0be97f8bf3a038d6690ebcfbe80eea69_3769eee2aa6a408eb2c47616d72e350d.png

🔺 总结:override 写在子类中,会严格检查是否完成重写,如果没有就会报错提醒。


0x08 重载、覆盖、隐藏的对比


8bbd0e55d0520c07e82e7ac098fe7213_35246f607ce14fd59fbe255e920b7604.png

Ⅱ. 抽象类(Abstract Class)


0x00 纯虚函数和抽象类

在虚函数的后面写上 =0,则我们称这个函数为 "纯虚函数"。

36148764d7370c3b60100f7c85e8ec82_d61c5f169c244353bceb3af8771539a4.png

"一眼顶针鉴定为 —— 纯纯的虚函数"


包含纯虚函数的类,就是 抽象类(abstract class),也叫接口类。

/* 抽象类 */
class Car {
public:
  virtual void Drive() = 0;  // 纯虚函数
};

抽象类不能实例化出对象,子类即使在继承后也不能实例化出对象:

c693e3c4356aa9401ff0d6a92348b5ed_dc7a89b0ea87490eaeea3c163005c4b5.png

只有重写纯虚函数,子类才能实例化出对象:

/* 抽象类 */
class Car {
public:
  virtual void Drive() = 0;
};
// 如果父类是抽象类,子类必须重写才能实例化
class BMW : public Car {
public:
  virtual void Drive() {   // 重写
  cout << "BMW-操控性" << endl;
  }
};
int main(void)
{
  BMW b;
  b.Drive();
  return 0;
}

🚩 运行结果:

e4010eaca14d12550994bbd014262df6_18dbeb1e78344508aed6a48ae406807c.png

(间接要求你重写)


如果 override 是直接要求你重写,那设计成抽象类就是间接要求你重写。


override 是放在子类虚函数,检查重写,它们的功能其实是有一些重叠和相似的。


纯虚函数规范了子类必须重写,另外虚函数更体现出了接口继承。


🔺 总结:抽象类不能实例化出对象,子类即使在继承后也不能实例化出对象,除非子类重写。


0x01 抽象类为什么叫抽象类?

❓ 为什么要叫抽象类?和纯虚函数又有什么关系呢,难道是因为抽象带蓝子太受欢迎了嘛?

ef48d1d142ca9a45f79d6ca0735d840b_8b6c0f7f6a654808a9261432b21f086a.png

(吃饱喝足去敲代码)


当然不是。叫抽象类是很合理的。艺术中也有抽象的概念,我们来欣赏一下抽象派的代表作:

20e842785eb3320408f320677a16fe96_184d8ed41c114d748239214f5dd5f37d.png

(毕加索经典之作)


抽象在现实一般没有具体对应的实体,而不能实例化对象也就是没有实体,所以叫抽象类。


"抽象即不可名状,对应的是具象,具象即现实,抽象即虚拟。"


0x02 抽象类指针

虽然父类是抽象类不能定义对象,但是可以定义指针。


定义指针时如果 new 父类对象因为是纯虚函数,自然是 new 不出来的,但是可以 new 子类对象:

/* 抽象类 */
class Car {
public:
  virtual void Drive() = 0;
};
class Benz : public Car {
public:
  virtual void Drive() {
  cout << "Benz-舒适" << endl;
  }
};
int main(void)
{
  Car* pBenz1 = new Benz;
  pBenz1->Drive();
  Benz* pBenz2 = new Benz;
  pBenz2->Drive();
  return 0;
}


🚩 运行结果:

485205815766fb05ca22fbed297a259e_82d351aab2704cd9bef34278be12ee67.png


0x03 纯虚函数的实现问题

纯虚函数也是可以实现的:

/* 抽象类 */
class Car {
public:
    // 实现没有价值,因为压根没有对象会调用它
  virtual void Drive() = 0 {      // 纯虚函数
  cout << "Drive()" << endl;   
  }
};

但是,纯虚函数的实现没有什么太大意义,因为根本就没人能用它。


你实现一个东西是为了让人能调用你,纯虚函数谁能调用?根本没有人能调用它。


所以纯虚函数一般给个声明就可以了,它本身就是一个接口继承。


0x04 关于接口继承和实现继承(补充说明)

普通函数的继承是一种实现继承,子类继承了父类函数,可以使用函数,继承的是函数的实现。


虚函数的继承是一种接口继承,子类继承的是父类虚函数的接口,目的是为了重写,


达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。


出现虚函数就是为了提醒你重写的,以实现多态。如果虚函数不重写,那写成虚函数就没价值了。


相关文章
|
14天前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
1月前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
30 1
|
1月前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
23 2
|
1月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
1月前
|
存储 C++ 索引
C++函数指针详解
【10月更文挑战第3天】本文介绍了C++中的函数指针概念、定义与应用。函数指针是一种指向函数的特殊指针,其类型取决于函数的返回值与参数类型。定义函数指针需指定返回类型和参数列表,如 `int (*funcPtr)(int, int);`。通过赋值函数名给指针,即可调用该函数,支持两种调用格式:`(*funcPtr)(参数)` 和 `funcPtr(参数)`。函数指针还可作为参数传递给其他函数,增强程序灵活性。此外,也可创建函数指针数组,存储多个函数指针。
|
2月前
|
编译器 C++
【C++核心】指针和引用案例详解
这篇文章详细讲解了C++中指针和引用的概念、使用场景和操作技巧,包括指针的定义、指针与数组、指针与函数的关系,以及引用的基本使用、注意事项和作为函数参数和返回值的用法。
35 3
|
1月前
|
存储 编译器 程序员
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
2月前
|
C++
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`-&gt;`运算符,可以方便地访问和操作智能指针所指向的对象。
|
2月前
|
C++
C++(九)this指针
`this`指针是系统在创建对象时默认生成的,用于指向当前对象,便于使用。其特性包括:指向当前对象,适用于所有成员函数但不适用于初始化列表;作为隐含参数传递,不影响对象大小;类型为`ClassName* const`,指向不可变。`this`的作用在于避免参数与成员变量重名,并支持多重串联调用。例如,在`Stu`类中,通过`this-&gt;name`和`this-&gt;age`明确区分局部变量与成员变量,同时支持链式调用如`s.growUp().growUp()`。
|
3月前
|
存储 安全 C++
C++:指针引用普通变量适用场景
指针和引用都是C++提供的强大工具,它们在不同的场景下发挥着不可或缺的作用。了解两者的特点及适用场景,可以帮助开发者编写出更加高效、可读性更强的代码。在实际开发中,合理选择使用指针或引用是提高编程技巧的关键。
32 1