前言
前面我們已經(jīng)介紹過了C++中的類與函數(shù),不熟悉的,可以去看看
NDK開發(fā)---C++學(xué)習(xí)(三):類與函數(shù)(上)
NDK開發(fā)---C++學(xué)習(xí)(四):類與函數(shù)(中)
NDK開發(fā)---C++學(xué)習(xí)(五):類與函數(shù)(下)
本篇將介紹C++中的繼承(多繼承、虛擬繼承)、多態(tài)、虛函數(shù)等。
繼承
繼承是代碼重用的基本工具,是類的一個重要特征。它克服了面向過程程序設(shè)計語言沒有軟件復(fù)用語言機(jī)制的缺點(diǎn),使軟件復(fù)用變得簡單、易行,可以通過繼承復(fù)用已有的程序資源,提高軟件開發(fā)的效率,縮短軟件開發(fā)的周期。
繼承是在已知類的基礎(chǔ)上創(chuàng)建新類的過程,已有類稱為基類,新類稱為派生類。派生類不僅能夠繼承基類的功能,而且還能夠?qū)惖墓δ苓M(jìn)行補(bǔ)充、修改或重定義。
C++的繼承可分為公有繼承、保護(hù)繼承和私有繼承,也稱為公有派生、保護(hù)派生和私有派生。在C++中,繼承的語法形式如下(下文中關(guān)于基類和派生類的,一律采用父類和子類去替代。):
class 子類類名:[繼承方式] 父類類名 {
子類成員聲明與定義;
}
其中,繼承方式可以是public、protected、private,分別對應(yīng)公有繼承、保護(hù)繼承和私有繼承。如果省略繼承方式,C++默認(rèn)為private繼承。
相信作為Android開發(fā)的你,對于繼承,應(yīng)該是了然于胸,那么接下來我們來看看C++繼承的一個簡單例子
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Human {
protected:
char* name;
int age;
public:
void say() {
cout << "說話" << endl;
}
};
class Man : public Human {
private:
char* brother;
public:
void chasing() {
cout << "泡妞" << endl;
}
};
void work(Human &h) {
h.say();
}
void main() {
Man m;
m.say(); //說話
//1.父類類型的引用或指針
Human* h_p = &m;
h_p->say();
Human &h1 = m;
h1.say();
work(m);
//子類對象初始化父類類型的對象
Human h2 = m;
getchar();
}
在C++中沒有extend,取而代之的是:,可以調(diào)用父類的方法
Man m;
m.say();
父類類型的引用或指針指向子類對象
Human* h_p = &m;
h_p->say();
Human &h1 = m;
h1.say();
子類對象初始化父類類型的對象
Human h2 = m;
1. 公有繼承
繼承方式為public的繼承稱為公有繼承。
1. 公有繼承不改變父類成員在子類中的訪問權(quán)限。在公有繼承方式下,父類中的public、private和protected成員在子類中保持它們在基類中相同的訪問權(quán)限。
2. 在子類中定義的成員函數(shù)不能直接訪問父類的私有成員,只能通過父類的public成員或protected成員訪問它們。
2. 私有繼承
繼承方式為private的繼承稱為私有繼承。
1. 在私有繼承方式下,父類的public和protected成員在子類中都變成了private成員,不再是子類的公有接口函數(shù),不能被父類的外部函數(shù)訪問。
2. 雖然是父類的public和protected成員在子類中都變成了private成員,但它們與父類本身的private成員是有區(qū)別的,它們可被子類的成員函數(shù)直接訪問,而父類中的private成員不能被子類直接訪問。
3. 保護(hù)繼承
繼承方式為protected的繼承稱為保護(hù)繼承。
在保護(hù)繼承方式下,父類的public成員在子類中的訪問權(quán)限被修改為protected權(quán)限,父類的protected成員在子類中仍為protected成員,父類的private成員在子類中仍為private成員。
向父類構(gòu)造函數(shù)傳參
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Human {
protected:
char* name;
int age;
public:
Human(char* name, int age) {
this->name = name;
this->age = age;
}
void say() {
cout << "說話" << endl;
}
};
class Man : public Human {
public:
void chasing() {
cout << "泡妞" << endl;
}
private:
char* brother;
};
void main() {
Man m;
getchar();
}
運(yùn)行報錯,Man沒有合適的構(gòu)造函數(shù),這是為什么呢?明明默認(rèn)是有無參構(gòu)造函數(shù)的啊。因?yàn)?code>Man繼承于Human,Human只有一個有參構(gòu)造參數(shù),看過我之前的博客的同學(xué)肯定知道,只要有有參構(gòu)造函數(shù),就會覆蓋無參構(gòu)造函數(shù)。所以Man的無參構(gòu)造函數(shù)也已經(jīng)被覆蓋了。
我們還可以這樣理解:子類的構(gòu)造函數(shù)除了要負(fù)責(zé)本類成員的初始化外,還要調(diào)用父類和成員對象的構(gòu)造函數(shù),并向它們傳遞參數(shù),以完成父類子對象和成員對象的建立和初始化,而此例中的子類Man的無參構(gòu)造顯然不能完成上述操作,需要一個有參構(gòu)造函數(shù)來完成父類Human的初始化。
那么針對這個問題,我們可以這樣做,給Man添加一個有參構(gòu)造函數(shù)
Man(char* brother, char* h_name, int h_age) : Human(h_name, h_age){
this->brother = brother;
}
void main() {
Man m("jack", "john", 23);
getchar();
}
編譯運(yùn)行通過。
看到這里大家是不是覺得特別像之前我介紹的構(gòu)造函數(shù)的成員初始化列表NDK開發(fā)---C++學(xué)習(xí)(四):類與函數(shù)(中),不熟悉的,我已插眼,可以隨時TP。
我們在Man類中添加Human對象屬性,去復(fù)習(xí)下構(gòu)造函數(shù)的成員初始化列表
class Man : public Human {
private:
char* brother;
Human h1;
public:
Man(char* brother, char* h_name, int h_age, char* s_name, int s_age) : Human(h_name, h_age), h1(s_name, s_age){
this->brother = brother;
}
void chasing() {
cout << "泡妞" << endl;
}
};
void main() {
Man m("jack", "john", 23, "rose", 25);
getchar();
}
構(gòu)造函數(shù)與析構(gòu)函數(shù)調(diào)用順序
#include<iostream>
using namespace std;
class Human {
protected:
char* name;
int age;
public:
Human(char* name, int age) {
this->name = name;
this->age = age;
cout << "父類構(gòu)造函數(shù)" << endl;
}
~Human() {
cout << "父類析構(gòu)函數(shù)" << endl;
}
void say() {
cout << "說話" << endl;
}
};
class Man : public Human {
private:
char* brother;
public:
//給父類構(gòu)造函數(shù)傳參
Man(char* brother, char* h_name, int h_age) : Human(h_name, h_age) {
this->brother = brother;
cout << "子類構(gòu)造函數(shù)" << endl;
}
~Man() {
cout << "子類析構(gòu)函數(shù)" << endl;
}
void chasing() {
cout << "泡妞" << endl;
}
};
void func() {
Man m("jack", "john", 23);
}
void main() {
func();
getchar();
}
運(yùn)行打印結(jié)果為:
父類構(gòu)造函數(shù)
子類構(gòu)造函數(shù)
子類析構(gòu)函數(shù)
父類析構(gòu)函數(shù)
也就是父類構(gòu)造函數(shù)先調(diào)用,子類析構(gòu)函數(shù)先調(diào)用。
子類對象調(diào)用父類的成員
子類不僅可以添加父類沒有的新成員,而且還可以對父類的成員函數(shù)進(jìn)行重定義(類似于Java中的重寫)或重載(類似于Java中的重載)。
重定義是指子類可以定義與父類具有相同函數(shù)原型的成員函數(shù)(即具有相同的函數(shù)名、參數(shù)列表和返回值類型),而重載則要求成員函數(shù)名相同、參數(shù)列表不同,與返回值類型無關(guān)。
需要指出的是:子類對父類成員函數(shù)的重定義或重載會影響父類成員函數(shù)在子類中的可見性,父類的同名成員函數(shù)會被子類重載的同名函數(shù)所隱藏。
using namespace std;
class Human {
public:
char* name;
int age;
public:
Human(char* name, int age) {
this->name = name;
this->age = age;
cout << "父類構(gòu)造函數(shù)" << endl;
}
~Human() {
cout << "父類析構(gòu)函數(shù)" << endl;
}
void say() {
cout << "Human說話" << endl;
}
};
class Man : public Human {
private:
char* brother;
public:
//給父類構(gòu)造函數(shù)傳參
Man(char* brother, char* h_name, int h_age) : Human(h_name, h_age) {
this->brother = brother;
cout << "子類構(gòu)造函數(shù)" << endl;
}
~Man() {
cout << "子類析構(gòu)函數(shù)" << endl;
}
void chasing() {
cout << "泡妞" << endl;
}
void say() {
cout << "Man說話" << endl;
}
};
void main() {
Man m("jack", "john", 23);
m.say();
getchar();
}
運(yùn)行打印輸出
Man說話
很明顯m調(diào)用say函數(shù)的時候,找的是子類的say函數(shù),因?yàn)樽宇惖?code>say函數(shù)重定義了父類中的say函數(shù),父類的say函數(shù)隱藏,如果我們想要調(diào)用父類的say函數(shù)的話,該怎樣去做呢?別著急,讓我逐一敘述:
m.Human::say();
這樣調(diào)用的就是父類的say函數(shù),當(dāng)然我們還可以修改父類屬性的值
m.Human::age = 10;
多繼承
C++允許一個類從一個或多個父類派生。如果一個類只有一個父類,就稱為單繼承;如果一個類具有兩個以上的父類,就稱為多繼承。多繼承的形式如下:
class 子類類名:[繼承方式] 父類類名1,[繼承方式] 父類類名2,... {
子類成員聲明或定義;
}
其中,繼承方式可以是private、protected、private,分別對應(yīng)公有繼承、保護(hù)繼承和私有繼承,其含義與單繼承相同。
C++在解析子類的成員函數(shù)調(diào)用時,按照以下次序查找成員函數(shù)所屬的類。
1. 在子類中查找該函數(shù),若找到就確定該函數(shù)是子類的成員函數(shù)。
2. 如果在子類中沒有找到該成員函數(shù),就在父類中查找該成員函數(shù)。
多繼承方式下的二義性
在多繼承中,子類繼承了多個父類的成員,當(dāng)兩個不同父類擁有同名成員時,容易產(chǎn)生命名沖突問題。
class A {
public:
char* name;
};
class B1 : public A {
};
class B2 : public A {
};
class C : public B1, public B2 {
};
如果有下面的成員引用:
C c;
c.name = "john"; //錯誤,二義性沖突
C類中擁有A類數(shù)據(jù)成員和成員函數(shù)的兩份備份,在引用來源于A的成員時容易產(chǎn)生二義性。
對于成員屬性name的調(diào)用,編譯器首先在C類中查找,結(jié)果沒有找到,接下來編譯器會在C的父類中查找,但在B1和B2的兩個父類中都找到了name,編譯器不能確定調(diào)用哪個父類的name,因此產(chǎn)生二義性的命名沖突。
解決上述二義性命名沖突的辦法是指明成員調(diào)用所屬的類。例如,可以這樣:
C c;
//指定父類顯示調(diào)用
c.B1::name = "john"; //正確
但這樣的調(diào)用方式并未解決本質(zhì)問題,在同一個對象c中存在A的兩份不同數(shù)據(jù)成員,不僅浪費(fèi)存儲空間,而且還容易產(chǎn)生數(shù)據(jù)的不一致性。為了解決這類問題,C++引用了虛擬繼承。
虛擬繼承
利用C++提供的關(guān)鍵字virtual限定繼承方式,將公共父類指定為虛父類,就可以使該父類的成員在子類中只有一份備份。虛父類的定義形式如下:
class 子類類名 : virtual [繼承方式] 父類類名1, virtual [繼承方式] 父類類名2, ... {
子類成員聲明或定義;
}
針對上述例子
class A {
public:
char* name;
};
class B1 : virtual public A {
};
class B2 : virtual public A {
};
class C : public B1, public B2 {
};
void main() {
C c;
c.name = "john";
getchar();
}
通過對公共父類的虛擬繼承,子類只保留了虛父類的一份數(shù)據(jù)成員備份,現(xiàn)在通過子類對象引用虛父類中的成員就不會產(chǎn)生二義性的命名沖突了。
虛函數(shù)
我們先看一個例子:
創(chuàng)建一個頭文件Plane.h
#pragma once
//普通飛機(jī)
class Plane {
public:
void fly();
void land();
};
創(chuàng)建cpp文件Plane.cpp
#include "Plane.h"
#include <iostream>
using namespace std;
void Plane::fly() {
cout << "普通飛機(jī)起飛" << endl;
}
void Plane::land() {
cout << "普通飛機(jī)著陸" << endl;
}
再創(chuàng)建一個頭文件Helicopter.h
#pragma once
#include"Plane.h"
//直升飛機(jī)
class Helicopter : public Plane {
public:
void fly();
void land();
};
創(chuàng)建cpp文件Helicopter.cpp
#include "Plane.h"
#include "Helicopter.h"
#include <iostream>
using namespace std;
void Helicopter::fly() {
cout << "直升飛機(jī)起飛" << endl;
}
void Helicopter::land() {
cout << "直升飛機(jī)著陸" << endl;
}
業(yè)務(wù)邏輯
#include "Plane.h"
#include "Helicopter.h"
void bizPlay(Plane &p) {
p.fly();
p.land();
}
void main() {
Plane p1;
bizPlay(p1);
Helicopter p2;
bizPlay(p2);
getchar();
}
運(yùn)行打印結(jié)果為:
普通飛機(jī)起飛
普通飛機(jī)著陸
普通飛機(jī)起飛
普通飛機(jī)著陸
這顯然不是我們想要看見的,我們想要在調(diào)用bizPlay(p2)調(diào)用子類的函數(shù)。C++給出了一種更好的解決方案——虛函數(shù)。
只有類的成員函數(shù)才能被定義為虛函數(shù),不屬于任何類的普通函數(shù)不能被定義成虛函數(shù)。虛函數(shù)的運(yùn)行機(jī)制可以概括如下:如果父類中的非靜態(tài)成員函數(shù)被定義為虛函數(shù),且子類重寫了父類的虛函數(shù),則通過指向父類對象的指針或引用調(diào)用子類對象中的虛函數(shù)時,就會調(diào)用到該指針(或引用)實(shí)際所指對象的成員函數(shù)。
虛函數(shù)的定義方法非常簡單,把限定詞virtual加在類成員函數(shù)的聲明前面,就將此函數(shù)指定為虛函數(shù)了,形式如下:
class X {
virtual 返回值類型 函數(shù)名(參數(shù)表);
}
virtual的意義在于指示編譯器,對這類函數(shù)采取遲后聯(lián)編(動態(tài)綁定)(關(guān)于多態(tài)與聯(lián)編這方面的知識可閃現(xiàn)到文末進(jìn)行了解)的方法,在程序運(yùn)行過程中才確定與之相對應(yīng)的函數(shù)。而沒有用virtual限定的函數(shù)則采用早期聯(lián)編(靜態(tài)綁定)的方式,在編譯過程中就把函數(shù)調(diào)用與它的函數(shù)實(shí)現(xiàn)關(guān)聯(lián)起來。
#pragma once
//普通飛機(jī)
class Plane {
public:
virtual void fly();
virtual void land();
};
#pragma once
#include"Plane.h"
//直升飛機(jī)
class Helicopter : public Plane {
public:
void fly();
void land();
};
再次運(yùn)行打印結(jié)果為:
普通飛機(jī)起飛
普通飛機(jī)著陸
直升飛機(jī)起飛
直升飛機(jī)著陸
虛函數(shù)的特性:
一旦將某個類成員函數(shù)聲明為虛函數(shù)后,該成員函數(shù)在子類的繼承體系中就永遠(yuǎn)為虛函數(shù)了。即使子類在重寫該函數(shù)時并沒有將它聲明為虛函數(shù),它仍然是虛函數(shù)。
發(fā)生動態(tài)多態(tài)的條件:
1. 繼承
2. 父類的引用或者指針指向子類的對象
3. 函數(shù)的重寫
純虛函數(shù)
純虛函數(shù)是指在聲明時被初始化為0的虛類成員函數(shù)。純虛函數(shù)在父類中聲明,但它在父類中沒有具體的函數(shù)實(shí)現(xiàn)代碼,要求繼承它的子類為純虛函數(shù)提供實(shí)現(xiàn)代碼。
純虛函數(shù)的聲明形式如下:
class X {
virtual 返回值類型 函數(shù)名(參數(shù)列表) = 0;
}
接著我們來看一個例子:
//形狀
class Shape {
//純虛函數(shù)
virtual void sayArea() = 0;
};
//圓
class Circle : public Shape {
private:
int r;
public:
Circle(int r) {
this->r = r;
}
};
void main() {
Circle c(10); //錯誤
getchar();
}
上述代碼中Circle c(10)是錯誤的,因?yàn)?code>Circle繼承Shape這個抽象類了,必須重寫里面的純虛函數(shù)。
//形狀
using namespace std;
class Shape {
//純虛函數(shù)
virtual void sayArea() = 0;
};
//圓
class Circle : public Shape {
private:
int r;
public:
Circle(int r) {
this->r = r;
}
void sayArea() {
cout << (3.14 * r * r) << endl;
}
};
void main() {
Circle c(10);
getchar();
}
重寫之后就不存在問題了。
接口
class Drawable {
virtual void draw() = 0;
};
多態(tài)與聯(lián)編
可能很多人還不清楚什么是多態(tài)和聯(lián)編,下面我將逐一介紹:
多態(tài)就是指不同對象收到相同消息時會執(zhí)行不同的操作。通俗地講,就是用一個相同的名字定義許多不同的函數(shù),這些函數(shù)可以針對不同數(shù)據(jù)類型實(shí)現(xiàn)相同或相似的功能,即所謂的“一個接口,多種實(shí)現(xiàn)”。
C++中的多態(tài)性與聯(lián)編這一概念密切相關(guān)。一個源程序需要經(jīng)過編譯、連接才能形成可執(zhí)行文件,在這個過程中要把調(diào)用函數(shù)名與對應(yīng)函數(shù)關(guān)聯(lián)在一起,這個過程就是綁定,又稱聯(lián)編。
聯(lián)編又分為靜態(tài)聯(lián)編和動態(tài)聯(lián)編,靜態(tài)聯(lián)編(靜態(tài)綁定)是指在程序執(zhí)行前,編譯器根據(jù)函數(shù)調(diào)用提供的信息,在程序編譯時就把調(diào)用函數(shù)名與具體函數(shù)綁定在一起。
動態(tài)聯(lián)編(動態(tài)綁定)是指在程序編譯時還不能確定函數(shù)調(diào)用所對應(yīng)的具體函數(shù),只有在程序運(yùn)行過程中根據(jù)具體的數(shù)據(jù)類型才能夠確定函數(shù)調(diào)用所對應(yīng)的具體函數(shù),即在程序運(yùn)行時才把函數(shù)名與具體函數(shù)綁定在一起。
靜態(tài)聯(lián)編和動態(tài)聯(lián)編都能夠?qū)崿F(xiàn)多態(tài)性,采用靜態(tài)聯(lián)編實(shí)現(xiàn)的多態(tài)就稱為靜態(tài)多態(tài)性,采用動態(tài)聯(lián)編實(shí)現(xiàn)的多態(tài)就稱為動態(tài)多態(tài)性。靜態(tài)多態(tài)性是通過函數(shù)重載和運(yùn)算符重載在程序編譯時通過靜態(tài)綁定實(shí)現(xiàn)的。動態(tài)多態(tài)性是通過繼承和虛函數(shù)在程序執(zhí)行時通過動態(tài)綁定實(shí)現(xiàn)的。平常所說的面向?qū)ο蟪绦蛟O(shè)計的多態(tài)性,常指運(yùn)行時的多態(tài)性。
靜態(tài)多態(tài)性在編譯時就確定了調(diào)用的具體函數(shù),不需要在執(zhí)行程序時從多個同名函數(shù)中匹配調(diào)用函數(shù),所以執(zhí)行速度快。而動態(tài)多態(tài)性需要在執(zhí)行程序時從多個函數(shù)中匹配調(diào)用函數(shù),所以它比靜態(tài)多態(tài)性的執(zhí)行效率低,但它提供了更多的靈活性、問題的抽象性和程序的可維護(hù)性。
展望
關(guān)于C++的知識目前已經(jīng)基本介紹完了,接下來將介紹NDK相關(guān)的技術(shù)文章,敬請期待!
喜歡本篇博客的簡友們,就請來一波點(diǎn)贊,您的每一次關(guān)注,將成為我前進(jìn)的動力,謝謝!