[c++] 繼承和多型整理二

王燕龍(大衛)發表於2024-03-02

1 虛擬函式和純虛擬函式

虛擬函式,之所以說是虛的,說的是在派生類中,可以覆蓋基類中的虛擬函式;相對於虛擬函式來說,沒有 virtual 修飾的函式可以叫做實函式,實函式就不能被覆蓋。虛擬函式是實現多型的核心。虛擬函式和純虛擬函式比較的話,虛擬函式可以在派生類中被覆蓋,但是虛擬函式也是有自己的實現的,可以被直接呼叫;純虛擬函式沒有自己的實現,在派生類中可以被覆蓋,並且必須實現。包含純虛擬函式的類是抽象類,不能建立物件,如果抽象類的派生類中沒有實現純虛擬函式 ,那麼派生類也是抽象類,不能建立物件。

虛擬函式和純虛擬函式並沒有嚴格的優劣之分。

從實際使用中,純虛擬函式有一個優點,假如一個基類中的函式,派生類中必須實現自己的邏輯,而不能使用基類中的邏輯,那麼就可以使用純虛擬函式,這樣在派生類中如果忘記實現了,那麼編譯器就會提示錯誤,起到了一個約束的作用;如果用虛擬函式實現,那麼派生類中忘記實現的話,編譯器也不會報錯,起不到約束提醒的作用。

純虛擬函式有以下兩點:

(1)純虛擬函式的宣告方式

函式宣告之後加 = 0,而不是花括號

(2)抽象類不能建立物件,抽象類的派生類如果沒有實現所有的純虛擬函式,派生類也是抽象類

(3)抽象類可以定義指標,並且可以使用派生類的指標給它賦值

如下是使用抽象類的一個例子:

#include <iostream>
#include <string>

class Phone {
public:
  Phone() {
    std::cout << "Phone()" << std::endl;
  }

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

  virtual void Call() = 0;
  virtual void SendMessage(std::string msg) = 0;
};

class Apple : public Phone {
public:
  Apple() {
    std::cout << "Apple()" << std::endl;
  }

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

  virtual void Call() {
    std::cout << "Apple Call()" << std::endl;
  }

  virtual void SendMessage(std::string msg) {
    std::cout << "apple send msg: " << msg << std::endl;
  }
};

class Oppo : public Phone {
public:
  Oppo() {
    std::cout << "Oppo()" << std::endl;
  }

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

  virtual void Call() {
    std::cout << "Oppo Call()" << std::endl;
  }
};

class Vivo : public Phone {
public:
  Vivo() {
    std::cout << "Vivo()" << std::endl;
  }

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

  virtual void Call() {
    std::cout << "Vivo Call()" << std::endl;
  }

  virtual void SendMessage(std::string msg) {
    std::cout << "vivo send msg: " << msg << std::endl;
  }
};

int main() {
  // 不能建立 Phone 物件,因為 Phone 是抽象類
  // Phone phone;
  // 不能建立 Oppo 物件,因為 Oppo 沒有實現 Phone 中的 SendMessage 函式
  // 所以 Oppo 也是抽象類
  // Oppo oppo;

  std::cout << "sizeof(Phone) = " << sizeof(Phone) << std::endl;
  std::cout << "sizeof(Apple) = " << sizeof(Apple) << std::endl;
  std::cout << "sizeof(Oppo) = " << sizeof(Oppo) << std::endl;
  std::cout << "sizeof(Vivo) = " << sizeof(Vivo) << std::endl;

  Phone *phone;
  Apple apple;
  Vivo vivo;

  phone = &apple;
  phone->Call();
  phone->SendMessage("this is apple");

  phone = &vivo;
  phone->Call();
  phone->SendMessage("this is vivo");
  return 0;
}

執行結果如下:

抽象類中也有虛表,從上邊的列印來看,sizeof(Phone) 計算出來的結果是 8。

2 建構函式呼叫虛擬函式

2.1 基類建構函式呼叫虛擬函式,呼叫的是基類的虛擬函式

在建構函式中呼叫虛擬函式,基類建構函式呼叫虛擬函式呼叫的是基類的虛擬函式還是呼叫的子類的虛擬函式;派生類的建構函式中呼叫虛擬函式,呼叫的是基類的虛擬函式還是呼叫的自己的虛擬函式。

使用下邊的程式碼來做一下實驗,基類是 Base,有一個虛擬函式 VDo(),派生類是 Derived1,覆蓋了基類的虛擬函式 VDo()。在 Base 的建構函式中呼叫了虛擬函式 VDo(),在 Derived1 的建構函式中也呼叫了虛擬函式 VDo()。在 main 函式中建立一個 Derived1 物件。

#include <iostream>
#include <string>

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

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

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

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

class Derived1 : public Base {
public:
  Derived1() {
    std::cout << "Derived1()" << std::endl;
    VDo();
  }

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

  virtual void VDo() {
    std::cout << "Derived1() VDo()" << std::endl;
  }
};

int main() {
  Derived1 d1;
  return 0;
}

如下是列印的日誌,從日誌可以看出來,在構造 Derived1 的時候首先要構造 Base。在 Base 建構函式中呼叫的 VDo() 是 Base 中的,在 Derived1 的建構函式中呼叫的 VDo 是 Derived1 中的。

當派生類構造的時候,首先構造基類,然後再構造派生類。在呼叫基類建構函式的時候,派生類還沒構造,還沒有初始化,所以在基類建構函式中呼叫的虛擬函式是基類中的虛擬函式。

派生類建構函式中呼叫的虛擬函式是派生類的虛擬函式。

2.1 基類和派生類之間的引用傳遞,指標傳遞,值傳遞

引用傳遞和指標傳遞都能體現多型,值傳遞無法體現多型。

#include <iostream>
#include <string>

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

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

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

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

class Derived1 : public Base {
public:
  Derived1() {
    std::cout << "Derived1()" << std::endl;
    VDo();
  }

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

  virtual void VDo() {
    std::cout << "Derived1() VDo()" << std::endl;
  }
};

void CallDo(Base &b) {
  std::cout << "CallDo(Base &b)" << std::endl;
  b.VDo();
}

void CallDo(Base *b) {
  std::cout << "CallDo(Base *b)" << std::endl;
  b->VDo();
}

void CallDoValue(Base b) {
  std::cout << "CallDoValue(Base b)" << std::endl;
  b.VDo();
}

int main() {
  Derived1 d1;
  std::cout << std::endl;

  CallDo(d1);
  std::cout << std::endl;

  CallDo(&d1);
  std::cout << std::endl;

  CallDoValue(d1);
  std::cout << std::endl;

  return 0;
}

程式執行結果,使用值傳遞的時候,呼叫的函式還是 Base 中的 VDo()。

2.2 虛擬函式過載

如下程式碼 Base 是基類,Derived 是派生類。Base 中有兩個虛擬函式 func1() 和 func2(),Derived 中也有兩個虛擬函式 func1() 和 func2()。Derived 中的 func1() 和 Base 中的 func1() 的形參列表是不一樣的,所以 Derived 中的 func1 不會覆蓋 Base 中的 func1。

#include <iostream>
#include <string>

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

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

  virtual void func1(int a) {
    std::cout << "Base()::func1(int a), a = " << a << std::endl;
  }

  virtual void func2(int a = 100) {
    std::cout << "Base()::func2(int a = 100), a = " << a << std::endl;
  }
};

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

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

  virtual void func1(double a) {
    std::cout << "Derived()::func1(double a), a = " << a << std::endl;
  }

  virtual void func2(int a = 200) {
    std::cout << "Derived()::func2(int a = 200), a = " << a << std::endl;
  }
};

typedef void (*PF)(int);
typedef void (*PF1)(double);
int main() {
  Base *b = new Derived;
  Derived *d = new Derived;

  std::cout << "----------------" << std::endl;
  b->func1(10);
  b->func1(1.123);
  b->func2();

  std::cout << "----------------" << std::endl;
  d->func1(10);
  d->func1(1.123);
  d->func2();

  return 0;
}

執行結果如下:



(1)派生類中的虛擬函式表如下

第一個表項是 Base 中的 func1。

第二個表項在基類中是 Base 中的 func2,在派生類中,Derived 中的 func2 對 Base 中的 func2 進行了覆蓋。

第三個表象是派生類中的 func1,因為派生類中的 func1 和 Base 中的 func1 形參不一樣,所以不會對 Base 中的 func1 進行覆蓋。

(2)執行結果分析

① 使用 Base 型別的指標或者 Base 型別的引用,當指標指向 Derived 物件的時候,呼叫 func1(),不管傳參是 int 型別還是 double 型別,都是呼叫的 Base 中的 func1()。當傳參是 double 型別的時候也不是呼叫的 Derived 中的 func1,也就是派生類和基類形不成過載。

② 使用 Base 型別的指標或者引用,當指標指向 Derived 物件的時候,呼叫 func2,呼叫的函式是 Derived 中的 func2,但是預設引數還是 Base 中初始化的。這個現象讓人看起來有點奇怪,函式和預設引數不是配套的。

③ 使用 Derived 指標或者引用,呼叫的 func1() 都是 Derived 中的函式,不管入參是 int 還是 double。

④ 使用 Derived 指標或者引用,呼叫 func2() 呼叫的是 Derived 中的 func2(),並且形參預設是 200。

相關文章