繼承和多態C++

這里寫目錄標題

    • 繼承
      • public、protected、private 修飾類的成員
      • public、protected、private 指定繼承方式
        • 改變訪問權限
      • C++繼承時的名字遮蔽問題
      • 基類成員函數和派生類成員函數不構成重載
      • C++基類和派生類的構造函數
        • 構造函數的調用順序
        • 基類構造函數調用規則
      • C++基類和派生類的析構函數
      • C++多繼承(多重繼承)
      • 多繼承下的構造函數
      • 多繼承造成的命名沖突
      • C++將派生類賦值給基類(向上轉型)?
        • 將派生類對象賦值給基類對象
        • 將派生類指針賦值給基類指針
        • 將派生類引用賦值給基類引用
    • 多態與虛函數
      • 多態的用途
      • C++虛函數注意事項
      • 純虛函數

搬運自C語言中文網

繼承

繼承(Inheritance)可以理解為一個類從另一個類獲取成員變量和成員函數的過程。例如類 B 繼承于類 A,那么 B 就擁有 A 的成員變量和成員函數。
在C++中,派生(Derive)和繼承是一個概念,只是站的角度不同。繼承是兒子接收父親的產業,派生是父親把產業傳承給兒子。

被繼承的類稱為父類或基類,繼承的類稱為子類或派生類。“**子類”和“父類”**通常放在一起稱呼,“基類”和“派生類”通常放在一起稱呼。

派生類除了擁有基類的成員,還可以定義自己的新成員,以增強類的功能。

以下是兩種典型的使用繼承的場景:

  1. 當你創建的新類與現有的類相似,只是多出若干成員變量或成員函數時,可以使用繼承,這樣不但會減少代碼量,而且新類會擁有基類的所有功能。

  2. 當你需要創建多個類,它們擁有很多相似的成員變量或成員函數時,也可以使用繼承。可以將這些類的共同成員提取出來,定義為基類,然后從基類繼承,既可以節省代碼,也方便后續修改成員。

#include<iostream>
using namespace std;//基類 Pelple
class People{
public:void setname(char *name);void setage(int age);char *getname();int getage();
private:char *m_name;int m_age;
};
void People::setname(char *name){ m_name = name; }
void People::setage(int age){ m_age = age; }
char* People::getname(){ return m_name; }
int People::getage(){ return m_age;}//派生類 Student
class Student: public People{
public:void setscore(float score);float getscore();
private:float m_score;
};
void Student::setscore(float score){ m_score = score; }
float Student::getscore(){ return m_score; }int main(){Student stu;stu.setname("小明");stu.setage(16);stu.setscore(95.5f);cout<<stu.getname()<<"的年齡是 "<<stu.getage()<<",成績是 "<<stu.getscore()<<endl;return 0;
}
//派生類 Student
class Student: public People{
……
}

由此總結出繼承的一般語法為:
class 派生類名:[繼承方式] 基類名{
派生類新增加的成員
};

繼承方式包括 public(公有的)、private(私有的)和 protected(受保護的),此項是可選的,如果不寫,那么默認為 private

public、protected、private 三個關鍵字除了可以修飾類的成員,還可以指定繼承方式。

public、protected、private 修飾類的成員

類的內部(定義類的代碼內部),無論成員被聲明為 public、protected 還是 private,都是可以互相訪問的,沒有訪問權限的限制。

在類的外部(定義類的代碼之外),只能通過對象訪問成員,并且通過對象只能訪問 public 屬性的成員在類外也不能通過對象訪問 private、protected 屬性的成員。

聲明為 protected 的成員在類外也不能通過對象訪問,但是在它的派生類內部可以訪問
protected 成員和 private 成員類似,也不能通過對象訪問。但是當存在繼承關系時,protected 和 private 就不一樣了:基類中protected 成員可以在派生類中使用,而基類中的 private 成員不能在派生類中使用

public、protected、private 指定繼承方式

不同的繼承方式會影響基類成員在派生類中的訪問權限。
在這里插入圖片描述
通過上面的分析可以發現:

  1. 基類成員在派生類中的訪問權限不得高于繼承方式中指定的權限。例如,當繼承方式為 protected 時,那么基類成員在派生類中的訪問權限最高也為 protected,高于 protected 的會降級為 protected,但低于 protected 不會升級。再如,當繼承方式為 public 時,那么基類成員在派生類中的訪問權限將保持不變。

也就是說,繼承方式中的 public、protected、private 是用來指明基類成員在派生類中的最高訪問權限的。

  1. 不管繼承方式如何,基類中的 private 成員在派生類中始終不能使用(不能在派生類的成員函數中訪問或調用)。

  2. 如果希望基類的成員能夠被派生類繼承并且毫無障礙地使用,那么這些成員只能聲明為 public 或 protected;只有那些不希望在派生類中使用的成員才聲明為 private。

  3. 如果希望基類的成員既不向外暴露(不能通過對象訪問),還能在派生類中使用,那么只能聲明為 protected。

注意,我們這里說的是基類的 private 成員不能在派生類中使用,并沒有說基類的 private 成員不能被繼承。實際上,基類的 private 成員是能夠被繼承的,并且(成員變量)會占用派生類對象的內存,它只是在派生類中不可見,導致無法使用罷了。private 成員的這種特性,能夠很好的對派生類隱藏基類的實現,以體現面向對象的封裝性。
在這里插入圖片描述
由于 private 和 protected 繼承方式會改變基類成員在派生類中的訪問權限,導致繼承關系復雜,所以實際開發中我們一般使用 public。

在派生類中訪問基類 private 成員的唯一方法就是借助基類的非 private 成員函數,如果基類沒有非 private 成員函數,那么該成員在派生類中將無法訪問。(因為 m_hobby 是 private 屬性的,在派生類中不可見,所以只能借助基類的 public 成員函數 sethobby()、gethobby() 來訪問。)

改變訪問權限

使用 using 關鍵字可以改變基類成員在派生類中的訪問權限,例如將 public 改為 private、將 protected 改為 public。

注意:using 只能改變基類中 public 和 protected 成員的訪問權限,不能改變 private 成員的訪問權限,因為基類中 private 成員在派生類中是不可見的,根本不能使用,所以基類中的 private 成員在派生類中無論如何都不能訪問。

//派生類Student
class Student : public People {
public:void learning();
public:using People::m_name;  //將protected改為publicusing People::m_age;  //將protected改為publicfloat m_score;
private:using People::show;  //將public改為private
};
void Student::learning() {cout << "我是" << m_name << ",今年" << m_age << "歲,這次考了" << m_score << "分!" << endl;
}

C++繼承時的名字遮蔽問題

如果派生類中的成員(包括成員變量和成員函數)和基類中的成員重名,那么就會遮蔽從基類繼承過來的成員。所謂遮蔽,就是在派生類中使用該成員(包括在定義派生類時使用,也包括通過派生類對象訪問該成員)時,實際上使用的是派生類新增的成員,而不是從基類繼承來的。

#include<iostream>
using namespace std;//基類People
class People{
public:void show();
protected:char *m_name;int m_age;
};
void People::show(){cout<<"嗨,大家好,我叫"<<m_name<<",今年"<<m_age<<"歲"<<endl;
}//派生類Student
class Student: public People{
public:Student(char *name, int age, float score);
public:void show();  //遮蔽基類的show()
private:float m_score;
};
Student::Student(char *name, int age, float score){m_name = name;m_age = age;m_score = score;
}
void Student::show(){cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<endl;
}int main(){Student stu("小明", 16, 90.5);//使用的是派生類新增的成員函數,而不是從基類繼承的stu.show();//使用的是從基類繼承來的成員函數stu.People::show();return 0;
}

運行結果:
小明的年齡是16,成績是90.5
嗨,大家好,我叫小明,今年16歲

本例中,基類 People 和派生類 Student 都定義了成員函數 show(),它們的名字一樣,會造成遮蔽。第 37 行代碼中,stu 是 Student 類的對象,默認使用 Student 類的 show() 函數。

但是,基類 People 中的 show() 函數仍然可以訪問,不過要加上類名和域解析符.People:: stu.People::show();

基類成員函數和派生類成員函數不構成重載

基類成員和派生類成員的名字一樣時會造成遮蔽,這句話對于成員變量很好理解,對于成員函數要引起注意,不管函數的參數如何,只要名字一樣就會造成遮蔽。換句話說,基類成員函數和派生類成員函數不會構成重載,如果派生類有同名函數,那么就會遮蔽基類中的所有同名函數,不管它們的參數是否一樣。

#include<iostream>
using namespace std;//基類Base
class Base{
public:void func();void func(int);
};
void Base::func(){ cout<<"Base::func()"<<endl; }
void Base::func(int a){ cout<<"Base::func(int)"<<endl; }//派生類Derived
class Derived: public Base{
public:void func(char *);void func(bool);
};
void Derived::func(char *str){ cout<<"Derived::func(char *)"<<endl; }
void Derived::func(bool is){ cout<<"Derived::func(bool)"<<endl; }int main(){Derived d;d.func("c.biancheng.net");d.func(true);d.func();  //compile errord.func(10);  //compile errord.Base::func();d.Base::func(100);return 0;
}

如果說有重載關系,那么也是 Base 類的兩個 func 構成重載,而 Derive 類的兩個 func 構成另外的重載。

C++基類和派生類的構造函數

前面我們說基類的成員函數可以被繼承,可以通過派生類的對象訪問,但這僅僅指的是普通的成員函數,類的構造函數不能被繼承。構造函數不能被繼承是有道理的,因為即使繼承了,它的名字和派生類的名字也不一樣,不能成為派生類的構造函數,當然更不能成為普通的成員函數。
這種矛盾在C++繼承中是普遍存在的,解決這個問題的思路是:在派生類的構造函數中調用基類的構造函數

//People(name, age)就是調用基類的構造函數
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }

People(name, age)就是調用基類的構造函數,并將 name 和 age 作為實參傳遞給它,m_score(score)是派生類的參數初始化表,它們之間以逗號,隔開。

也可以將基類構造函數的調用放在參數初始化表后面:

Student::Student(char *name, int age, float score): m_score(score), People(name, age){ }

函數頭部是對基類構造函數的調用 People("小明", 16),而不是聲明,所以括號里的參數是實參,它們不但可以是派生類構造函數參數列表中的參數,還可以是局部變量、常量等,例如:

Student::Student(char *name, int age, float score): People("小明", 16), m_score(score){ }

構造函數的調用順序

在這里插入圖片描述
還有一點要注意,派生類構造函數中只能調用直接基類的構造函數,不能調用間接基類的。以上面的 A、B、C 類為例,C 是最終的派生類,B 就是 C 的直接基類,A 就是 C 的間接基類。

C++ 這樣規定是有道理的,因為我們在 C 中調用了 B 的構造函數,B 又調用了 A 的構造函數,相當于 C 間接地(或者說隱式地)調用了 A 的構造函數,如果再在 C 中顯式地調用 A 的構造函數,那么 A 的構造函數就被調用了兩次,相應地,初始化工作也做了兩次,這不僅是多余的,還會浪費CPU時間以及內存,毫無益處,所以 C++ 禁止在 C 中顯式地調用 A 的構造函數。

基類構造函數調用規則

通過派生類創建對象時必須調用基類的構造函數,這是語法規定。換句話說,定義派生類構造函數時最好指明基類構造函數;如果不指明,就調用基類的默認構造函數(不帶參數的構造函數);如果沒有默認構造函數,那么編譯失敗。請看下面的例子:

People::People(): m_name("xxx"), m_age(0){ }
People::People(char *name, int age): m_name(name), m_age(age){}
Student::Student(): m_score(0.0){ }  //派生類默認構造函數
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }
int main(){Student stu1;stu1.display();Student stu2("小明", 16, 90.5);stu2.display();return 0;
}

創建對象 stu1 時,執行派生類的構造函數Student::Student(),它并沒有指明要調用基類的哪一個構造函數,從運行結果可以很明顯地看出來,系統默認調用了不帶參數的構造函數,也就是People::People()

創建對象 stu2 時,執行派生類的構造函數Student::Student(char *name, int age, float score),它指明了基類的構造函數。

在第 27 行代碼中,如果將People(name, age)去掉,也會調用默認構造函數,第 37 行的輸出結果將變為:
xxx的年齡是0,成績是90.5。

如果將基類 People 中不帶參數的構造函數刪除,那么會發生編譯錯誤,因為創建對象 stu1 時需要調用 People 類的默認構造函數, 而 People 類中已經顯式定義了構造函數,編譯器不會再生成默認的構造函數。(顯式定義了構造函數 就不會生成默認的構造函數,所以要自己寫一個構造函數)

C++基類和派生類的析構函數

析構函數的執行順序和構造函數的執行順序也剛好相反:
創建派生類對象時,構造函數的執行順序和繼承順序相同,即先執行基類構造函數,再執行派生類構造函數。
而銷毀派生類對象時,析構函數的執行順序和繼承順序相反,即先執行派生類析構函數,再執行基類析構函數。

#include <iostream>
using namespace std;class A{
public:A(){cout<<"A constructor"<<endl;}~A(){cout<<"A destructor"<<endl;}
};class B: public A{
public:B(){cout<<"B constructor"<<endl;}~B(){cout<<"B destructor"<<endl;}
};class C: public B{
public:C(){cout<<"C constructor"<<endl;}~C(){cout<<"C destructor"<<endl;}
};int main(){C test;//就是創建了一個C類的對象return 0;
}

運行結果:
A constructor
B constructor
C constructor
C destructor
B destructor
A destructor

C++多繼承(多重繼承)

在前面的例子中,派生類都只有一個基類,稱為單繼承(Single Inheritance)。除此之外,C++也支持多繼承(MultipleInheritance),即一個派生類可以有兩個或多個基類。
多繼承的語法也很簡單,將多個基類用逗號隔開即可。例如已聲明了類A、類B和類C,那么可以這樣來聲明派生類D:

class D: public A, private B, protected C{//類D新增加的成員
}

D 是多繼承形式的派生類,它以公有的方式繼承 A 類,以私有的方式繼承 B 類,以保護的方式繼承 C 類。D 根據不同的繼承方式獲取 A、B、C 中的成員,確定它們在派生類中的訪問權限。

多繼承下的構造函數

多繼承形式下的構造函數和單繼承形式基本相同,只是要在派生類的構造函數中調用多個基類的構造函數。以上面的 A、B、C、D 類為例,D 類構造函數的寫法為:

D(形參列表): A(實參列表), B(實參列表), C(實參列表){//其他操作
}

基類構造函數的調用順序和和它們在派生類構造函數中出現的順序無關,而是和聲明派生類時基類出現的順序相同。仍然以上面的 A、B、C、D 類為例,即使將 D 類構造函數寫作下面的形式:

D(形參列表): B(實參列表), C(實參列表), A(實參列表){//其他操作
}

那么也是先調用 A 類的構造函數,再調用 B 類構造函數,最后調用 C 類構造函數。 因為是這樣聲明的

class D: public A, private B, protected C{//類D新增加的成員
}
#include <iostream>
using namespace std;//基類
class BaseA{
public:BaseA(int a, int b);~BaseA();
protected:int m_a;int m_b;
};
BaseA::BaseA(int a, int b): m_a(a), m_b(b){cout<<"BaseA constructor"<<endl;
}
BaseA::~BaseA(){cout<<"BaseA destructor"<<endl;
}//基類
class BaseB{
public:BaseB(int c, int d);~BaseB();
protected:int m_c;int m_d;
};
BaseB::BaseB(int c, int d): m_c(c), m_d(d){cout<<"BaseB constructor"<<endl;
}
BaseB::~BaseB(){cout<<"BaseB destructor"<<endl;
}//派生類
class Derived: public BaseA, public BaseB{
public:Derived(int a, int b, int c, int d, int e);~Derived();
public:void show();
private:int m_e;
};
Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e){cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){cout<<"Derived destructor"<<endl;
}
void Derived::show(){cout<<m_a<<", "<<m_b<<", "<<m_c<<", "<<m_d<<", "<<m_e<<endl;
}int main(){Derived obj(1, 2, 3, 4, 5);obj.show();return 0;
}

多繼承造成的命名沖突

當兩個或多個基類中有同名的成員時,如果直接訪問該成員,就會產生命名沖突,編譯器不知道使用哪個基類的成員。這個時候需要在成員名字前面加上類名和域解析符::,以顯式地指明到底使用哪個類的成員,消除二義性。

void BaseA::show(){cout<<"m_a = "<<m_a<<endl;cout<<"m_b = "<<m_b<<endl;
}
void BaseB::show(){cout<<"m_c = "<<m_c<<endl;cout<<"m_d = "<<m_d<<endl;`#include <iostream>
using namespace std;//基類
class BaseA{
public:BaseA(int a, int b);~BaseA();
public:void show();
protected:int m_a;int m_b;
};
BaseA::BaseA(int a, int b): m_a(a), m_b(b){cout<<"BaseA constructor"<<endl;
}
BaseA::~BaseA(){cout<<"BaseA destructor"<<endl;
}
void BaseA::show(){cout<<"m_a = "<<m_a<<endl;cout<<"m_b = "<<m_b<<endl;
}//基類
class BaseB{
public:BaseB(int c, int d);~BaseB();void show();
protected:int m_c;int m_d;
};
BaseB::BaseB(int c, int d): m_c(c), m_d(d){cout<<"BaseB constructor"<<endl;
}
BaseB::~BaseB(){cout<<"BaseB destructor"<<endl;
}
void BaseB::show(){cout<<"m_c = "<<m_c<<endl;cout<<"m_d = "<<m_d<<endl;
}//派生類
class Derived: public BaseA, public BaseB{
public:Derived(int a, int b, int c, int d, int e);~Derived();
public:void display();
private:int m_e;
};
Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e){cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){cout<<"Derived destructor"<<endl;
}
void Derived::display(){BaseA::show();  //調用BaseA類的show()函數BaseB::show();  //調用BaseB類的show()函數cout<<"m_e = "<<m_e<<endl;
}int main(){Derived obj(1, 2, 3, 4, 5);obj.display();return 0;
}`
}

我們顯式的指明了要調用哪個基類的 show() 函數。

void Derived::display(){BaseA::show();  //調用BaseA類的show()函數BaseB::show();  //調用BaseB類的show()函數cout<<"m_e = "<<m_e<<endl;
}

完整代碼

#include <iostream>
using namespace std;//基類
class BaseA{
public:BaseA(int a, int b);~BaseA();
public:void show();
protected:int m_a;int m_b;
};
BaseA::BaseA(int a, int b): m_a(a), m_b(b){cout<<"BaseA constructor"<<endl;
}
BaseA::~BaseA(){cout<<"BaseA destructor"<<endl;
}
void BaseA::show(){cout<<"m_a = "<<m_a<<endl;cout<<"m_b = "<<m_b<<endl;
}//基類
class BaseB{
public:BaseB(int c, int d);~BaseB();void show();
protected:int m_c;int m_d;
};
BaseB::BaseB(int c, int d): m_c(c), m_d(d){cout<<"BaseB constructor"<<endl;
}
BaseB::~BaseB(){cout<<"BaseB destructor"<<endl;
}
void BaseB::show(){cout<<"m_c = "<<m_c<<endl;cout<<"m_d = "<<m_d<<endl;
}//派生類
class Derived: public BaseA, public BaseB{
public:Derived(int a, int b, int c, int d, int e);~Derived();
public:void display();
private:int m_e;
};
Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e){cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){cout<<"Derived destructor"<<endl;
}
void Derived::display(){BaseA::show();  //調用BaseA類的show()函數BaseB::show();  //調用BaseB類的show()函數cout<<"m_e = "<<m_e<<endl;
}int main(){Derived obj(1, 2, 3, 4, 5);obj.display();return 0;
}

C++將派生類賦值給基類(向上轉型)?

將派生類對象賦值給基類對象

#include <iostream>
using namespace std;//基類
class A{
public:A(int a);
public:void display();
public:int m_a;
};
A::A(int a): m_a(a){ }
void A::display(){cout<<"Class A: m_a="<<m_a<<endl;
}//派生類
class B: public A{
public:B(int a, int b);
public:void display();
public:int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;
}int main(){A a(10);B b(66, 99);//賦值前a.display();b.display();cout<<"--------------"<<endl;//賦值后a = b;a.display();b.display();return 0;
}

本例中 A 是基類, B 是派生類,a、b 分別是它們的對象,由于派生類 B 包含了從基類 A 繼承來的成員,因此可以將派生類對象 b 賦值給基類對象 a: a = b;。通過運行結果也可以發現,賦值后 a 所包含的成員變量的值已經發生了變化。

將派生類指針賦值給基類指針

#include <iostream>
using namespace std;//基類A
class A{
public:A(int a);
public:void display();
protected:int m_a;
};
A::A(int a): m_a(a){ }
void A::display(){cout<<"Class A: m_a="<<m_a<<endl;
}//中間派生類B
class B: public A{
public:B(int a, int b);
public:void display();
protected:int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;
}//基類C
class C{
public:C(int c);
public:void display();
protected:int m_c;
};
C::C(int c): m_c(c){ }
void C::display(){cout<<"Class C: m_c="<<m_c<<endl;
}//最終派生類D
class D: public B, public C{
public:D(int a, int b, int c, int d);
public:void display();
private:int m_d;
};
D::D(int a, int b, int c, int d): B(a, b), C(c), m_d(d){ }
void D::display(){cout<<"Class D: m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
}int main(){A *pa = new A(1);B *pb = new B(2, 20);C *pc = new C(3);D *pd = new D(4, 40, 400, 4000);pa = pd;pa -> display();pb = pd;pb -> display();pc = pd;pc -> display();cout<<"-----------------------"<<endl;cout<<"pa="<<pa<<endl;cout<<"pb="<<pb<<endl;cout<<"pc="<<pc<<endl;cout<<"pd="<<pd<<endl;return 0;
}

本例中定義了多個對象指針,并嘗試將派生類指針賦值給基類指針。與對象變量之間的賦值不同的是,對象指針之間的賦值并沒有拷貝對象的成員,也沒有修改對象本身的數據,僅僅是改變了指針的指向
將派生類指針賦值給基類指針時,通過基類指針只能使用派生類的成員變量,但不能使用派生類的成員函數(有待細講)

將派生類引用賦值給基類引用

int main(){D d(4, 40, 400, 4000);A &ra = d;B &rb = d;C &rc = d;ra.display();rb.display();rc.display();return 0;
}

向上轉型后通過基類的對象、指針、引用 只能訪問從基類繼承過去的成員(包括成員變量和成員函數),不能訪問派生類新增的成員

多態與虛函數

在上一節講到,基類的指針也可以指向派生類對象
將派生類指針賦值給基類指針時,通過基類指針只能使用派生類的成員變量,但不能使用派生類的成員函數(有待細講)

#include <iostream>
using namespace std;//基類People
class People{
public:People(char *name, int age);void display();
protected:char *m_name;int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){cout<<m_name<<"今年"<<m_age<<"歲了,是個無業游民。"<<endl;
}//派生類Teacher
class Teacher: public People{
public:Teacher(char *name, int age, int salary);void display();
private:int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){cout<<m_name<<"今年"<<m_age<<"歲了,是一名教師,每月有"<<m_salary<<"元的收入。"<<endl;
}int main(){People *p = new People("王志剛", 23);p -> display();p = new Teacher("趙宏佳", 45, 8200);p -> display();return 0;
}

運行結果:
王志剛今年23歲了,是個無業游民。
趙宏佳今年45歲了,是個無業游民。

當基類指針 p 指向派生類 Teacher 的對象時,雖然使用了 Teacher 的成員變量,但是卻沒有使用它的成員函數,導致輸出結果不倫不類(趙宏佳本來是一名老師,輸出結果卻顯示人家是個無業游民),不符合我們的預期。

換句話說,通過基類指針只能訪問派生類的成員變量,但是不能訪問派生類的成員函數。

為了消除這種尷尬,讓基類指針能夠訪問派生類的成員函數,C++ 增加了虛函數(Virtual Function)。使用虛函數非常簡單,只需要在函數聲明前面增加 virtual 關鍵字。

更改上面的代碼,將 display() 聲明為虛函數:

#include <iostream>
using namespace std;//基類People
class People{
public:People(char *name, int age);virtual void display();  //聲明為虛函數
protected:char *m_name;int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){cout<<m_name<<"今年"<<m_age<<"歲了,是個無業游民。"<<endl;
}//派生類Teacher
class Teacher: public People{
public:Teacher(char *name, int age, int salary);virtual void display();  //聲明為虛函數
private:int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){cout<<m_name<<"今年"<<m_age<<"歲了,是一名教師,每月有"<<m_salary<<"元的收入。"<<endl;
}int main(){People *p = new People("王志剛", 23);p -> display();p = new Teacher("趙宏佳", 45, 8200);p -> display();return 0;
}

運行結果:
王志剛今年23歲了,是個無業游民。
趙宏佳今年45歲了,是一名教師,每月有8200元的收入。

和前面的例子相比,本例僅僅是在 display() 函數聲明前加了一個virtual關鍵字,將成員函數聲明為了虛函數(Virtual Function),這樣就可以通過 p 指針調用 Teacher 類的成員函數了,運行結果也證明了這一點(趙宏佳已經是一名老師了,不再是無業游民了)。

有了虛函數,基類指針指向基類對象時就使用基類的成員(包括成員函數和成員變量),指向派生類對象時就使用派生類的成員(在使用虛函數之前,指向派生類對象只能使用派生類成員變量而不能使用成員函數)

換句話說,基類指針可以按照基類的方式來做事,也可以按照派生類的方式來做事,它有多種形態,或者說有多種表現方式,我們將這種現象稱為多態(Polymorphism)

多態是面向對象編程的主要特征之一,C++中虛函數的唯一用處就是構成多態。

C++提供多態的目的是:可以通過基類指針對所有派生類(包括直接派生和間接派生)的成員變量和成員函數進行“全方位”的訪問,尤其是成員函數。如果沒有多態,我們只能訪問成員變量。

引用不像指針靈活,指針可以隨時改變指向,而引用只能指代固定的對象,在多態性方面缺乏表現力,所以以后我們再談及多態時一般是說指針。本例的主要目的是讓讀者知道,除了指針,引用也可以實現多態。

多態的用途

多態的用途
通過上面的例子讀者可能還未發現多態的用途,不過確實也是,多態在小項目中鮮有有用武之地。

接下來的例子中,我們假設你正在玩一款軍事游戲,敵人突然發動了地面戰爭,于是你命令陸軍、空軍及其所有現役裝備進入作戰狀態。具體的代碼如下所示:

#include <iostream>
using namespace std;//軍隊
class Troops{
public:virtual void fight(){ cout<<"Strike back!"<<endl; }
};//陸軍
class Army: public Troops{
public:void fight(){ cout<<"--Army is fighting!"<<endl; }
};
//99A主戰坦克
class _99A: public Army{
public:void fight(){ cout<<"----99A(Tank) is fighting!"<<endl; }
};
//武直10武裝直升機
class WZ_10: public Army{
public:void fight(){ cout<<"----WZ-10(Helicopter) is fighting!"<<endl; }
};
//長劍10巡航導彈
class CJ_10: public Army{
public:void fight(){ cout<<"----CJ-10(Missile) is fighting!"<<endl; }
};//空軍
class AirForce: public Troops{
public:void fight(){ cout<<"--AirForce is fighting!"<<endl; }
};
//J-20隱形殲擊機
class J_20: public AirForce{
public:void fight(){ cout<<"----J-20(Fighter Plane) is fighting!"<<endl; }
};
//CH5無人機
class CH_5: public AirForce{
public:void fight(){ cout<<"----CH-5(UAV) is fighting!"<<endl; }
};
//轟6K轟炸機
class H_6K: public AirForce{
public:void fight(){ cout<<"----H-6K(Bomber) is fighting!"<<endl; }
};int main(){Troops *p = new Troops;p ->fight();//陸軍p = new Army;p ->fight();p = new _99A;p -> fight();p = new WZ_10;p -> fight();p = new CJ_10;p -> fight();//空軍p = new AirForce;p -> fight();p = new J_20;p -> fight();p = new CH_5;p -> fight();p = new H_6K;p -> fight();return 0;
}

這個例子中的派生類比較多,如果不使用多態,那么就需要定義多個指針變量,很容易造成混亂;而有了多態,只需要一個指針變量 p 就可以調用所有派生類的虛函數。

從這個例子中也可以發現,對于具有復雜繼承關系的大中型程序,多態可以增加其靈活性,讓代碼更具有表現力。

C++虛函數注意事項

    1. 只需要在虛函數的聲明處加上 virtual 關鍵字,函數定義處可以加也可以不加。
  1. 為了方便,你可以只將基類中的函數聲明為虛函數,這樣所有派生類中具有遮蔽關系的同名函數都將自動成為虛函數。關于名字遮蔽已在《C++繼承時的名字遮蔽》一節中進行了講解。
    所謂遮蔽,就是在派生類中使用該成員(包括在定義派生類時使用,也包括通過派生類對象訪問該成員)時,實際上使用的是派生類新增的成員,而不是從基類繼承來的。
    不理解 ,按照 遮蔽 的說法,即使基類中的函數不申明為虛函數,也會被派生類中的同名函數 遮蔽啊

  2. 當在基類中定義了虛函數時,如果派生類沒有定義新的函數來遮蔽此函數,那么將使用基類的虛函數

  3. 只有派生類的虛函數覆蓋基類的虛函數(函數原型相同)才能構成多態(通過基類指針訪問派生類函數)。例如基類虛函數的原型為virtual void func();,派生類虛函數的原型為virtual void func(int);,那么當基類指針 p 指向派生類對象時,語句p -> func(100);將會出錯,而語句p -> func();將調用基類的函數。

  4. 構造函數不能是虛函數。對于基類的構造函數,它僅僅是在派生類構造函數中被調用,這種機制不同于繼承。也就是說,派生類不繼承基類的構造函數,將構造函數聲明為虛函數沒有什么意義。

純虛函數

在C++中,可以將虛函數聲明為純虛函數,語法格式為:

virtual 返回值類型 函數名 (函數參數) = 0;

純虛函數沒有函數體,只有函數聲明,在虛函數聲明的結尾加上=0,表明此函數為純虛函數。
最后的=0并不表示函數返回值為0,它只起形式上的作用,告訴編譯系統“這是純虛函數”。
包含純虛函數的類稱為抽象類(Abstract Class)。之所以說它抽象,是因為它無法實例化,也就是無法創建對象。原因很明顯,純虛函數沒有函數體,不是完整的函數,無法調用,也無法為其分配內存空間。

抽象類通常是作為基類,讓派生類去實現純虛函數。派生類必須實現純虛函數才能被實例化。

純虛函數使用舉例:

#include <iostream>
using namespace std;
//線
class Line{
public:Line(float len);virtual float area() = 0;virtual float volume() = 0;
protected:float m_len;
};
Line::Line(float len): m_len(len){ }
//矩形
class Rec: public Line{
public:Rec(float len, float width);float area();
protected:float m_width;
};
Rec::Rec(float len, float width): Line(len), m_width(width){ }
float Rec::area(){ return m_len * m_width; }
//長方體
class Cuboid: public Rec{
public:Cuboid(float len, float width, float height);float area();float volume();
protected:float m_height;
};
Cuboid::Cuboid(float len, float width, float height): Rec(len, width), m_height(height){ }
float Cuboid::area(){ return 2 * ( m_len*m_width + m_len*m_height + m_width*m_height); }
float Cuboid::volume(){ return m_len * m_width * m_height; }
//正方體
class Cube: public Cuboid{
public:Cube(float len);float area();float volume();
};
Cube::Cube(float len): Cuboid(len, len, len){ }
float Cube::area(){ return 6 * m_len * m_len; }
float Cube::volume(){ return m_len * m_len * m_len; }
int main(){Line *p = new Cuboid(10, 20, 30);cout<<"The area of Cuboid is "<<p->area()<<endl;cout<<"The volume of Cuboid is "<<p->volume()<<endl;p = new Cube(15);cout<<"The area of Cube is "<<p->area()<<endl;cout<<"The volume of Cube is "<<p->volume()<<endl;return 0;
}

運行結果:
The area of Cuboid is 2200
The volume of Cuboid is 6000
The area of Cube is 1350
The volume of Cube is 3375

本例中定義了四個類,它們的繼承關系為:Line --> Rec --> Cuboid --> Cube。

Line 是一個抽象類,也是最頂層的基類,在 Line 類中定義了兩個純虛函數 area() 和 volume()。

在 Rec 類中,實現了 area() 函數;所謂實現,就是定義了純虛函數的函數體。但這時 Rec 仍不能被實例化,因為它沒有實現繼承來的 volume() 函數,volume() 仍然是純虛函數,所以 Rec 也仍然是抽象類。

直到 Cuboid 類,才實現了 volume() 函數,才是一個完整的類,才可以被實例化。

可以發現,Line 類表示“線”,沒有面積和體積,但它仍然定義了 area() 和 volume() 兩個純虛函數。這樣的用意很明顯:Line 類不需要被實例化,但是它為派生類提供了“約束條件”,派生類必須要實現這兩個函數,完成計算面積和體積的功能,否則就不能實例化。

在實際開發中,你可以定義一個抽象基類,只完成部分功能,未完成的功能交給派生類去實現(誰派生誰實現)。這部分未完成的功能,往往是基類不需要的,或者在基類中無法實現的。雖然抽象基類沒有完成,但是卻強制要求派生類完成,這就是抽象基類的“霸王條款”。

抽象基類除了約束派生類的功能,還可以實現多態。請注意第 51 行代碼 Line *p = new Cuboid(10, 20, 30);指針 p 的類型是 Line,但是它卻可以訪問派生類中的 area() 和 volume() 函數,正是由于在 Line 類中將這兩個函數定義為純虛函數;如果不這樣做,51 行后面的代碼都是錯誤的???(回憶一下多態概念的引入,通過基類指針只能訪問派生類的成員變量,但是不能訪問派生類的成員函數,至于基類被派生類同名函數遮蔽,是在通過派生類對象訪問成員函數的情況下發生的)。我想,這或許才是C++提供純虛函數的主要目的。
關于純虛函數的幾點說明

  1. 一個純虛函數就可以使類成為抽象基類,但是抽象基類中除了包含純虛函數外,還可以包含其它的成員函數(虛函數或普通函數)和成員變量。(可以有純虛函數已經被實現,只要還剩一個沒有

  2. 只有類中的虛函數才能被聲明為純虛函數,普通成員函數和頂層函數均不能聲明為純虛函數。如下例所示:

//頂層函數不能被聲明為純虛函數
void fun() = 0;   //compile error
class base{
public ://普通成員函數不能被聲明為純虛函數void display() = 0;  //compile error
};

摘自C語言中文網

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/39162.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/39162.shtml
英文地址,請注明出處:http://en.pswp.cn/news/39162.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

MTK Android隱藏NavigationBar

安卓MTK屏蔽NavigationBar, 在SDK中通過搜索關鍵字修改&#xff0c;可適用大部分MTK及安卓版本&#xff0e; 方法介紹 搜索device/mediatek與device/mediateksample下的.xml把config_showNavigationBar值置為false 如下為搜索指令 find device/mediatek -name “*.xml” | xa…

系統架構師---開發方法---敏捷開發

目錄 前言 極限編程 四大價值觀 溝通 簡單 反饋 勇氣 尊重&#xff1a; 十二個最佳實踐 計劃游戲 小型發布 隱喻 簡單設計 測試先行 重構 結對編程 集體代碼所所有制 持續集成 每周工作40小時 現場客戶 編碼標準 前言 2001年2月&#xff0c;在美國的猶他州…

Grafana展示k8s中pod的jvm監控面板/actuator/prometheus

場景 為保障java服務正常運行&#xff0c;對服務的jvm進行監控&#xff0c;通過使用actuator組件監控jvm情況&#xff0c;使用prometheus對數據進行采集&#xff0c;并在Grafana展現。 基于k8s場景 prometheus數據收集 配置service的lable&#xff0c;便于prometheus使用labl…

LVS負載均衡集群

目錄 集群 什么是集群 (含義) 集群的分類 LVS 負載均衡器的集群架構 負載均衡器的群集工作模式 LVS負載均衡器的調度算法 LVS組成作用 組成 作用 LVS群集創建與管理 創建步驟 ipvsadm工具 LVS-NAT部署實戰 1、部署共享存儲 2、配置節點服務器&#xff08;后端服…

JetPack Compose 學習筆記(持續整理中...)

1.為什么要學&#xff1f; 1.命令式和聲明式 UI大戰,個人認為命令式UI自定義程度較高,能更深入到性能,內存優化方面,而申明式UI 是現在主流的設計,比如React,React Native,Flutter,Swift UI等等,現在性能也逐漸在變得更好 2.還有一個原因compose 是KMM 是完整跨平臺的UI基礎 3.…

kafka使用心得(一)

kafka入門 一種分布式的、基于發布/訂閱的消息系統&#xff0c;scala編寫&#xff0c;具備快速、可擴展、可持久化的特點。 基本概念 topic 主題 partition 分區&#xff0c;一個topic下可以有多個partition&#xff0c;消息是分散到多個partition里存儲的&#xff0c;part…

劍指Offer48.最長不含重復字符的子字符串 C++

1、題目描述 請從字符串中找出一個最長的不包含重復字符的子字符串&#xff0c;計算該最長子字符串的長度。 示例 1: 輸入: “abcabcbb” 輸出: 3 解釋: 因為無重復字符的最長子串是 “abc”&#xff0c;所以其長度為 3。 示例 2: 輸入: “bbbbb” 輸出: 1 解釋: 因為無重復字…

圖像處理技巧形態學濾波之膨脹操作

1. 引言 歡迎回來&#xff0c;我的圖像處理愛好者們&#xff01;今天&#xff0c;讓我們繼續研究圖像處理領域中的形態學計算。在本篇中&#xff0c;我們將重點介紹腐蝕操作的反向效果膨脹操作。 閑話少說&#xff0c;我們直接開始吧&#xff01; 2. 膨脹操作原理 膨脹操作…

macOS CLion 使用 bits/stdc++.h

macOS 下 CLion 使用 bits/stdc.h 頭文件 terminal運行 brew install gccCLion里配置 -D CMAKE_CXX_COMPILER/usr/local/bin/g-11

Visual Studio 2022 中解決使用scanf報錯的方法(一勞永逸)

目錄 【前言】 一、scanf報錯示例 二、解決使用scanf報錯的方法 解決方法1&#xff08;不推薦&#xff09; 解決方法2&#xff08;不推薦&#xff09; 解決方法3&#xff08;強烈推薦&#xff09; 第一步 第二步 第三步 三、效果演示&#xff08;方法三&#xff09; …

根據一棵樹的兩種遍歷構造二叉樹

題目 給定兩個整數數組 preorder 和 inorder &#xff0c;其中 preorder 是二叉樹的先序遍歷&#xff0c; inorder 是同一棵樹的中序遍歷&#xff0c;請構造二叉樹并返回其根節點。 示例 1: 輸入: preorder [3,9,20,15,7], inorder [9,3,15,20,7] 輸出: [3,9,20,null,null,…

Unity-Linux部署WebGL項目MIME類型添加

在以往的文章中有提到過使用IIS部署WebGL添加MIME類型使WebGL項目在瀏覽器中能夠正常加載&#xff0c;那么如果咱們做的是商業項目&#xff0c;往往是需要部署在學校或者云服務器上面的&#xff0c;大部分情況下如果項目有接口或者后臺管理系統&#xff0c;后臺基本都會使用Lin…

機器學習筆記:李宏毅ChatGPT Finetune VS Prompt

1 兩種大語言模型&#xff1a;GPT VS BERT 2 對于大語言模型的兩種不同期待 2.1 “專才” 2.1.1 成為專才的好處 Is ChatGPT A Good Translator? A Preliminary Study 2023 Arxiv 箭頭方向指的是從哪個方向往哪個方向翻譯 表格里面的數值越大表示翻譯的越好 可以發現專門做翻…

Ceph入門到精通-Linux下Ceph源碼編譯和GDB調試

Ceph版本&#xff1a;14.2.22 Linux版本&#xff1a;ubuntu-server 18.04 第一部分 下載Ceph源碼 1.1 配置Ceph源碼鏡像源 Ceph源碼是托管在Github上&#xff0c;由于某些原因&#xff0c;國內訪問Github網站很慢&#xff0c;所以需要從其他途徑加速獲取源碼。Github官方給出…

【ubuntu18.04】01-network-manager-all.yaml和interfaces和resolv.conf各有什么區別和聯系

文章目錄 01-network-manager-all.yaml、interfaces 和 resolv.conf 是與網絡配置相關的文件&#xff0c;它們在網絡設置中有著不同的作用和使用方式。 01-network-manager-all.yaml: 這是一個配置文件&#xff0c;通常在 Ubuntu 系統上使用 NetworkManager 進行網絡管理時使用…

ChatGPT?保密嗎?它有哪些潛在風險?如何規避?

自2022年11月公開發布以來&#xff0c;ChatGPT已成為許多企業和個人的必備工具&#xff0c;但隨著該技術越來越多地融入我們的日常生活&#xff0c;人們很自然地想知道&#xff1a;ChatGPT是否是保密的。 問&#xff1a;ChatGPT保密嗎&#xff1f; 答&#xff1a;否&#xff0…

C++11并發與多線程筆記(3)線程傳參詳解,detach()大坑,成員函數做線程函數

C11并發與多線程筆記&#xff08;3&#xff09;線程傳參詳解&#xff0c;detach 大坑&#xff0c;成員函數做線程函數 1、傳遞臨時對象作為線程參數1.1 要避免的陷阱11.2 要避免的陷阱21.3 總結 2、臨時對象作為線程參數2.1 線程id概念2.2 臨時對象構造時機抓捕 3、傳遞類對象…

VR時代真的到來了?

業界對蘋果的期待是&#xff0c;打造一臺真正顛覆性的&#xff0c;給頭顯設備奠定發展邏輯底座的產品&#xff0c;而實際上&#xff0c;蘋果只是發布了一臺更強大的頭顯。 大眾希望蘋果回答的問題是“我為什么需要一臺AR或者VR產品&#xff1f;”&#xff0c;但蘋果回答的是“…

從零開始學習 Java:簡單易懂的入門指南之MAth、System(十二)

常見API&#xff0c;MAth、System 1 Math類1.1 概述1.2 常見方法1.3 算法小題(質數)1.4 算法小題(自冪數) 2 System類2.1 概述2.2 常見方法 1 Math類 1.1 概述 tips&#xff1a;了解內容 查看API文檔&#xff0c;我們可以看到API文檔中關于Math類的定義如下&#xff1a; Math類…

每天一道leetcode:300. 最長遞增子序列(動態規劃中等)

今日份題目&#xff1a; 給你一個整數數組 nums &#xff0c;找到其中最長嚴格遞增子序列的長度。 子序列 是由數組派生而來的序列&#xff0c;刪除&#xff08;或不刪除&#xff09;數組中的元素而不改變其余元素的順序。例如&#xff0c;[3,6,2,7] 是數組 [0,3,1,6,2,2,7] …