參考:
http://c.biancheng.net/view/2201.html
https://www.runoob.com/cplusplus/cpp-interfaces.html
文章目錄
一、從C語言到C++
C++ inline內聯函數
函數調用是有時間和空間開銷的。程序在執行一個函數之前需要做一些準備工作,要將實參、局部變量、返回地址以及若干寄存器都壓入棧中,然後才能執行函數體中的代碼;函數體中的代碼執行完畢後還要清理現場,將之前壓入棧中的數據都出棧,才能接着執行函數調用位置以後的代碼。
如果函數體代碼比較多,需要較長的執行時間,那麼函數調用機制佔用的時間可以忽略;如果函數只有一兩條語句,那麼大部分的時間都會花費在函數調用機制上,這種時間開銷就就不容忽視。
爲了消除函數調用的時空開銷,C++ 提供一種提高效率的方法,即在編譯時將函數調用處用函數體替換,類似於C語言中的宏展開。這種在函數調用處直接嵌入函數體的函數稱爲內聯函數(Inline Function),又稱內嵌函數或者內置函數。
C++ 重載運算符和重載函數
C++ 中的函數重載
在同一個作用域內,可以聲明幾個功能類似的同名函數,但是這些同名函數的形式參數(指參數的個數、類型或者順序)必須不同。您不能僅通過返回類型的不同來重載函數。
函數的重載的規則:
- 函數名稱必須相同。
- 參數列表必須不同(個數不同、類型不同、參數排列順序不同等)。
- 函數的返回類型可以相同也可以不相同。
- 僅僅返回類型不同不足以成爲函數的重載。
二、類和對象
C++構造函數
在C++中,有一種特殊的成員函數,它的名字和類名相同,沒有返回值,不需要用戶顯式調用(用戶也不能調用),而是在創建對象時自動執行。這種特殊的成員函數就是構造函數(Constructor)。
在《C++類成員的訪問權限以及類的封裝》一節中,我們通過成員函數 setname()、setage()、setscore() 分別爲成員變量 name、age、score 賦值,這樣做雖然有效,但顯得有點麻煩。有了構造函數,我們就可以簡化這項工作,在創建對象的同時爲成員變量賦值,請看下面的代碼(示例1):
#include <iostream>
using namespace std;
class Student{
private:
char *m_name;
int m_age;
float m_score;
public:
//聲明構造函數
Student(char *name, int age, float score);
//聲明普通成員函數
void show();
};
//定義構造函數
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("小明", 15, 92.5f);
stu.show();
//創建對象時向構造函數傳參
Student *pstu = new Student("李華", 16, 96);
pstu -> show();
return 0;
}
運行結果:
小明的年齡是15,成績是92.5
李華的年齡是16,成績是96
在棧上創建對象時,實參位於對象名後面,例如Student stu(“小明”, 15, 92.5f);在堆上創建對象時,實參位於類名後面,例如new Student(“李華”, 16, 96)。
構造函數必須是 public 屬性的,否則創建對象時無法調用。當然,設置爲 private、protected 屬性也不會報錯,但是沒有意義。
構造函數沒有返回值,因爲沒有變量來接收返回值,即使有也毫無用處,這意味着:
- 不管是聲明還是定義,函數名前面都不能出現返回值類型,即使是 void 也不允許;
- 函數體中不能有 return 語句。
構造函數的重載
和普通成員函數一樣,構造函數是允許重載的。一個類可以有多個重載的構造函數,創建對象時根據傳遞的實參來判斷調用哪一個構造函數。
構造函數的調用是強制性的,一旦在類中定義了構造函數,那麼創建對象時就一定要調用,不調用是錯誤的。如果有多個重載的構造函數,那麼創建對象時提供的實參必須和其中的一個構造函數匹配;反過來說,創建對象時只有一個構造函數會被調用。
對示例1中的代碼,如果寫作Student stu或者new Student就是錯誤的,因爲類中包含了構造函數,而創建對象時卻沒有調用。
更改示例1的代碼,再添加一個構造函數(示例2):
#include <iostream>
using namespace std;
class Student{
private:
char *m_name;
int m_age;
float m_score;
public:
Student();
Student(char *name, int age, float score);
void setname(char *name);
void setage(int age);
void setscore(float score);
void show();
};
Student::Student(){
m_name = NULL;
m_age = 0;
m_score = 0.0;
}
Student::Student(char *name, int age, float score){
m_name = name;
m_age = age;
m_score = score;
}
void Student::setname(char *name){
m_name = name;
}
void Student::setage(int age){
m_age = age;
}
void Student::setscore(float score){
m_score = score;
}
void Student::show(){
if(m_name == NULL || m_age <= 0){
cout<<"成員變量還未初始化"<<endl;
}else{
cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<endl;
}
}
int main(){
//調用構造函數 Student(char *, int, float)
Student stu("小明", 15, 92.5f);
stu.show();
//調用構造函數 Student()
Student *pstu = new Student();
pstu -> show();
pstu -> setname("李華");
pstu -> setage(16);
pstu -> setscore(96);
pstu -> show();
return 0;
}
運行結果:
小明的年齡是15,成績是92.5
成員變量還未初始化
李華的年齡是16,成績是96
構造函數Student(char *, int, float)爲各個成員變量賦值,構造函數Student()將各個成員變量的值設置爲空,它們是重載關係。根據Student()創建對象時不會賦予成員變量有效值,所以還要調用成員函數 setname()、setage()、setscore() 來給它們重新賦值。
構造函數在實際開發中會大量使用,它往往用來做一些初始化工作,例如對成員變量賦值、預先打開文件等。
默認構造函數
如果用戶自己沒有定義構造函數,那麼編譯器會自動生成一個默認的構造函數,只是這個構造函數的函數體是空的,也沒有形參,也不執行任何操作。比如上面的 Student 類,默認生成的構造函數如下:
Student(){}
最後需要注意的一點是,調用沒有參數的構造函數也可以省略括號。對於示例2的代碼,在棧上創建對象可以寫作Student stu()或Student stu,在堆上創建對象可以寫作Student *pstu = new Student()或Student *pstu = new Student,它們都會調用構造函數 Student()。
構造函數的一項重要功能是對成員變量進行初始化,爲了達到這個目的,可以在構造函數的函數體中對成員變量一一賦值,還可以採用初始化列表。
C++構造函數的初始化列表使得代碼更加簡潔,請看下面的例子:
#include <iostream>
using namespace std;
class Student{
private:
char *m_name;
int m_age;
float m_score;
public:
Student(char *name, int age, float score);
void show();
};
//採用初始化列表
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
//TODO:
}
void Student::show(){
cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<endl;
}
int main(){
Student stu("小明", 15, 92.5f);
stu.show();
Student *pstu = new Student("李華", 16, 96);
pstu -> show();
return 0;
}
初始化 const 成員變量
構造函數初始化列表還有一個很重要的作用,那就是初始化 const 成員變量。初始化 const 成員變量的唯一方法就是使用初始化列表。
C++ this指針
this 是 C++ 中的一個關鍵字,也是一個 const 指針,它指向當前對象,通過它可以訪問當前對象的所有成員。
#include <iostream>
using namespace std;
class Student{
public:
void setname(char *name);
void setage(int age);
void setscore(float score);
void show();
private:
char *name;
int age;
float score;
};
void Student::setname(char *name){
this->name = name;
}
void Student::setage(int age){
this->age = age;
}
void Student::setscore(float score){
this->score = score;
}
void Student::show(){
cout<<this->name<<"的年齡是"<<this->age<<",成績是"<<this->score<<endl;
}
int main(){
Student *pstu = new Student;
pstu -> setname("李華");
pstu -> setage(16);
pstu -> setscore(96.5);
pstu -> show();
return 0;
}
運行結果:
李華的年齡是16,成績是96.5
this 只能用在類的內部,通過 this 可以訪問類的所有成員,包括 private、protected、public 屬性的。
本例中成員函數的參數和成員變量重名,只能通過 this 區分。以成員函數setname(char *name)爲例,它的形參是name,和成員變量name重名,如果寫作name = name;這樣的語句,就是給形參name賦值,而不是給成員變量name賦值。而寫作this -> name = name;後,=左邊的name就是成員變量,右邊的name就是形參,一目瞭然。
注意,this 是一個指針,要用->來訪問成員變量或成員函數。
this 雖然用在類的內部,但是隻有在對象被創建以後纔會給 this 賦值,並且這個賦值的過程是編譯器自動完成的,不需要用戶干預,用戶也不能顯式地給 this 賦值。本例中,this 的值和 pstu 的值是相同的。
幾點注意:
- this 是 const 指針,它的值是不能被修改的,一切企圖修改該指針的操作,如賦值、遞增、遞減等都是不允許的。
- this 只能在成員函數內部使用,用在其他地方沒有意義,也是非法的。
- 只有當對象被創建後 this 纔有意義,因此不能在 static 成員函數中使用(後續會講到 static 成員)
C++ static靜態成員變量
對象的內存中包含了成員變量,不同的對象佔用不同的內存(已在《C++對象的內存模型》中提到),這使得不同對象的成員變量相互獨立,它們的值不受其他對象的影響。例如有兩個相同類型的對象 a、b,它們都有一個成員變量 m_name,那麼修改 a.m_name 的值不會影響 b.m_name 的值。
可是有時候我們希望在多個對象之間共享數據,對象 a 改變了某份數據後對象 b 可以檢測到。共享數據的典型使用場景是計數,以前面的 Student 類爲例,如果我們想知道班級中共有多少名學生,就可以設置一份共享的變量,每次創建對象時讓該變量加 1。
在C++中,我們可以使用靜態成員變量來實現多個對象共享數據的目標。靜態成員變量是一種特殊的成員變量,它被關鍵字static修飾,例如:
class Student{
public:
Student(char *name, int age, float score);
void show();
public:
static int m_total; //靜態成員變量
private:
char *m_name;
int m_age;
float m_score;
};
這段代碼聲明瞭一個靜態成員變量 m_total,用來統計學生的人數。
static 成員變量屬於類,不屬於某個具體的對象,即使創建多個對象,也只爲 m_total 分配一份內存,所有對象使用的都是這份內存中的數據。當某個對象修改了 m_total,也會影響到其他對象。
靜態成員變量在初始化時不能再加 static,但必須要有數據類型。被 private、protected、public 修飾的靜態成員變量都可以用這種方式初始化。
注意:static 成員變量的內存既不是在聲明類時分配,也不是在創建對象時分配,而是在(類外)初始化時分配。反過來說,沒有在類外初始化的 static 成員變量不能使用。
static 成員變量既可以通過對象來訪問,也可以通過類來訪問。請看下面的例子:
//通過類類訪問 static 成員變量
Student::m_total = 10;
//通過對象來訪問 static 成員變量
Student stu("小明", 15, 92.5f);
stu.m_total = 20;
//通過對象指針來訪問 static 成員變量
Student *pstu = new Student("李華", 16, 96);
pstu -> m_total = 20;
注意:static 成員變量不佔用對象的內存,而是在所有對象之外開闢內存,即使不創建對象也可以訪問。具體來說,static 成員變量和普通的 static 變量類似,都在內存分區中的全局數據區分配內存
下面來看一個完整的例子:
#include <iostream>
using namespace std;
class Student{
public:
Student(char *name, int age, float score);
void show();
private:
static int m_total; //靜態成員變量
private:
char *m_name;
int m_age;
float m_score;
};
//初始化靜態成員變量
int Student::m_total = 0;
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
m_total++; //操作靜態成員變量
}
void Student::show(){
cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<"(當前共有"<<m_total<<"名學生)"<<endl;
}
int main(){
//創建匿名對象
(new Student("小明", 15, 90)) -> show();
(new Student("李磊", 16, 80)) -> show();
(new Student("張華", 16, 99)) -> show();
(new Student("王康", 14, 60)) -> show();
return 0;
}
運行結果:
小明的年齡是15,成績是90(當前共有1名學生)
李磊的年齡是16,成績是80(當前共有2名學生)
張華的年齡是16,成績是99(當前共有3名學生)
王康的年齡是14,成績是60(當前共有4名學生)
本例中將 m_total 聲明爲靜態成員變量,每次創建對象時,會調用構造函數使 m_total 的值加 1。
之所以使用匿名對象,是因爲每次創建對象後只會使用它的 show() 函數,不再進行其他操作。不過使用匿名對象無法回收內存,會導致內存泄露,在中大型程序中不建議使用。
C++ static靜態成員函數
在類中,static 除了可以聲明靜態成員變量,還可以聲明靜態成員函數。普通成員函數可以訪問所有成員(包括成員變量和成員函數),靜態成員函數只能訪問靜態成員。
編譯器在編譯一個普通成員函數時,會隱式地增加一個形參 this,並把當前對象的地址賦值給 this,所以普通成員函數只能在創建對象後通過對象來調用,因爲它需要當前對象的地址。而靜態成員函數可以通過類來直接調用,編譯器不會爲它增加形參 this,它不需要當前對象的地址,所以不管有沒有創建對象,都可以調用靜態成員函數。
普通成員變量佔用對象的內存,靜態成員函數沒有 this 指針,不知道指向哪個對象,無法訪問對象的成員變量,也就是說靜態成員函數不能訪問普通成員變量,只能訪問靜態成員變量。
普通成員函數必須通過對象才能調用,而靜態成員函數沒有 this 指針,無法在函數體內部訪問某個對象,所以不能調用普通成員函數,只能調用靜態成員函數。
靜態成員函數與普通成員函數的根本區別在於:普通成員函數有 this 指針,可以訪問類中的任意成員;而靜態成員函數沒有 this 指針,只能訪問靜態成員(包括靜態成員變量和靜態成員函數)。
C++ const成員變量和成員函數(常成員函數)
在類中,如果你不希望某些數據被修改,可以使用const關鍵字加以限定。const 可以用來修飾成員變量和成員函數。
const成員變量
const 成員變量的用法和普通 const 變量的用法相似,只需要在聲明時加上 const 關鍵字。初始化 const 成員變量只有一種方法,就是通過構造函數的初始化列表。
const成員函數(常成員函數)
const 成員函數可以使用類中的所有成員變量,但是不能修改它們的值,這種措施主要還是爲了保護數據而設置的。const 成員函數也稱爲常成員函數。
我們通常將 get 函數設置爲常成員函數。讀取成員變量的函數的名字通常以get開頭,後跟成員變量的名字,所以通常將它們稱爲 get 函數。
常成員函數需要在聲明和定義的時候在函數頭部的結尾加上 const 關鍵字,請看下面的例子:
class Student{
public:
Student(char *name, int age, float score);
void show();
//聲明常成員函數
char *getname() const;
int getage() const;
float getscore() const;
private:
char *m_name;
int m_age;
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;
}
//定義常成員函數
char * Student::getname() const{
return m_name;
}
int Student::getage() const{
return m_age;
}
float Student::getscore() const{
return m_score;
}
最後再來區分一下 const 的位置:
- 函數開頭的 const 用來修飾函數的返回值,表示返回值是 const 類型,也就是不能被修改,例如const char * getname()。
- 函數頭部的結尾加上 const 表示常成員函數,這種函數只能讀取成員變量的值,而不能修改成員變量的值,例如char * getname() const。
C++友元函數和友元類(C++ friend關鍵字)
友元函數
在當前類以外定義的、不屬於當前類的函數也可以在類中聲明,但要在前面加 friend 關鍵字,這樣就構成了友元函數。友元函數可以是不屬於任何類的非成員函數,也可以是其他類的成員函數。
友元函數可以訪問當前類中的所有成員,包括 public、protected、private 屬性的。
1) 將非成員函數聲明爲友元函數
#include <iostream>
using namespace std;
class Student{
public:
Student(char *name, int age, float score);
public:
friend void show(Student *pstu); //將show()聲明爲友元函數
private:
char *m_name;
int m_age;
float m_score;
};
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
//非成員函數
void show(Student *pstu){
cout<<pstu->m_name<<"的年齡是 "<<pstu->m_age<<",成績是 "<<pstu->m_score<<endl;
}
int main(){
Student stu("小明", 15, 90.6);
show(&stu); //調用友元函數
Student *pstu = new Student("李磊", 16, 80.5);
show(pstu); //調用友元函數
return 0;
}
注意,友元函數不同於類的成員函數,在友元函數中不能直接訪問類的成員,必須要藉助對象。下面的寫法是錯誤的:
void show(){
cout<<m_name<<"的年齡是 "<<m_age<<",成績是 "<<m_score<<endl;
}
成員函數在調用時會隱式地增加 this 指針,指向調用它的對象,從而使用該對象的成員;而 show() 是非成員函數,沒有 this 指針,編譯器不知道使用哪個對象的成員,要想明確這一點,就必須通過參數傳遞對象(可以直接傳遞對象,也可以傳遞對象指針或對象引用),並在訪問成員時指明對象。
2) 將其他類的成員函數聲明爲友元函數
#include <iostream>
using namespace std;
class Address; //提前聲明Address類
//聲明Student類
class Student{
public:
Student(char *name, int age, float score);
public:
void show(Address *addr);
private:
char *m_name;
int m_age;
float m_score;
};
//聲明Address類
class Address{
private:
char *m_province; //省份
char *m_city; //城市
char *m_district; //區(市區)
public:
Address(char *province, char *city, char *district);
//將Student類中的成員函數show()聲明爲友元函數
friend void Student::show(Address *addr);
};
//實現Student類
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(Address *addr){
cout<<m_name<<"的年齡是 "<<m_age<<",成績是 "<<m_score<<endl;
cout<<"家庭住址:"<<addr->m_province<<"省"<<addr->m_city<<"市"<<addr->m_district<<"區"<<endl;
}
//實現Address類
Address::Address(char *province, char *city, char *district){
m_province = province;
m_city = city;
m_district = district;
}
int main(){
Student stu("小明", 16, 95.5f);
Address addr("陝西", "西安", "雁塔");
stu.show(&addr);
Student *pstu = new Student("李磊", 16, 80.5);
Address *paddr = new Address("河北", "衡水", "桃城");
pstu -> show(paddr);
return 0;
}
運行結果:
小明的年齡是 16,成績是 95.5
家庭住址:陝西省西安市雁塔區
李磊的年齡是 16,成績是 80.5
家庭住址:河北省衡水市桃城區
本例定義了兩個類 Student 和 Address,程序第 27 行將 Student 類的成員函數 show() 聲明爲 Address 類的友元函數,由此,show() 就可以訪問 Address 類的 private 成員變量了。
幾點注意:
① 程序第 4 行對 Address 類進行了提前聲明,是因爲在 Address 類定義之前、在 Student 類中使用到了它,如果不提前聲明,編譯器會報錯,提示’Address’ has not been declared。類的提前聲明和函數的提前聲明是一個道理。
② 程序將 Student 類的聲明和實現分開了,而將 Address 類的聲明放在了中間,這是因爲編譯器從上到下編譯代碼,show() 函數體中用到了 Address 的成員 province、city、district,如果提前不知道 Address 的具體聲明內容,就不能確定 Address 是否擁有該成員(類的聲明中指明瞭類有哪些成員)。
這裏簡單介紹一下類的提前聲明。一般情況下,類必須在正式聲明之後才能使用;但是某些情況下(如上例所示),只要做好提前聲明,也可以先使用。
但是應當注意,類的提前聲明的使用範圍是有限的,只有在正式聲明一個類以後才能用它去創建對象。如果在上面程序的第4行之後增加如下所示的一條語句,編譯器就會報錯:
Address addr; //企圖使用不完整的類來創建對象
因爲創建對象時要爲對象分配內存,在正式聲明類之前,編譯器無法確定應該爲對象分配多大的內存。編譯器只有在“見到”類的正式聲明後(其實是見到成員變量),才能確定應該爲對象預留多大的內存。在對一個類作了提前聲明後,可以用該類的名字去定義指向該類型對象的指針變量(本例就定義了 Address 類的指針變量)或引用變量(後續會介紹引用),因爲指針變量和引用變量本身的大小是固定的,與它所指向的數據的大小無關。
③ 一個函數可以被多個類聲明爲友元函數,這樣就可以訪問多個類中的 private 成員。
C++ class和struct到底有什麼區別
C++ 中保留了C語言的 struct 關鍵字,並且加以擴充。在C語言中,struct 只能包含成員變量,不能包含成員函數。而在C++中,struct 類似於 class,既可以包含成員變量,又可以包含成員函數。
C++中的 struct 和 class 基本是通用的,唯有幾個細節不同:
- 使用 class 時,類中的成員默認都是 private 屬性的;而使用 struct 時,結構體中的成員默認都是 public 屬性的。
- class 繼承默認是 private 繼承,而 struct 繼承默認是 public 繼承。
- class 可以使用模板,而 struct 不能。
三、繼承和派生
C++繼承
繼承允許我們依據另一個類來定義一個類,當創建一個類時,您不需要重新編寫新的數據成員和成員函數,只需指定新建的類繼承了一個已有的類的成員即可。這個已有的類稱爲基類,新建的類稱爲派生類。
訪問控制和繼承
派生類可以訪問基類中所有的非私有成員。因此基類成員如果不想被派生類的成員函數訪問,則應在基類中聲明爲 private。
我們可以根據訪問權限總結出不同的訪問類型,如下所示:
訪問 | public | protected | private |
---|---|---|---|
同一個類 | yes | yes | yes |
派生類 | yes | yes | no |
外部的類 | yes | no | no |
一個派生類繼承了所有的基類方法,但下列情況除外:
- 基類的構造函數、析構函數和拷貝構造函數
- 基類的重載運算符
- 基類的友元函數
繼承類型
當一個類派生自基類,該基類可以被繼承爲 public、protected 或 private 幾種類型。繼承類型是通過上面講解的訪問修飾符 access-specifier 來指定的。
我們幾乎不使用 protected 或 private 繼承,通常使用 public 繼承。當使用不同類型的繼承時,遵循以下幾個規則:
- 公有繼承(public):當一個類派生自公有基類時,基類的公有成員也是派生類的公有成員,基類的保護成員也是派生類的保護成員,基類的私有成員不能直接被派生類訪問,但是可以通過調用基類的公有和保護成員來訪問
- 保護繼承(protected): 當一個類派生自保護基類時,基類的公有和保護成員將成爲派生類的保護成員
- 私有繼承(private):當一個類派生自私有基類時,基類的公有和保護成員將成爲派生類的私有成員
前面我們說基類的成員函數可以被繼承,可以通過派生類的對象訪問,但這僅僅指的是普通的成員函數,類的構造函數不能被繼承。構造函數不能被繼承是有道理的,因爲即使繼承了,它的名字和派生類的名字也不一樣,不能成爲派生類的構造函數,當然更不能成爲普通的成員函數。
在設計派生類時,對繼承過來的成員變量的初始化工作也要由派生類的構造函數完成,但是大部分基類都有 private 屬性的成員變量,它們在派生類中無法訪問,更不能使用派生類的構造函數來初始化。
這種矛盾在C++繼承中是普遍存在的,解決這個問題的思路是:在派生類的構造函數中調用基類的構造函數。
下面的例子展示瞭如何在派生類的構造函數中調用基類的構造函數:
#include<iostream>
using namespace std;
//基類People
class People{
protected:
char *m_name;
int m_age;
public:
People(char*, int);
};
People::People(char *name, int age): m_name(name), m_age(age){}
//派生類Student
class Student: public People{
private:
float m_score;
public:
Student(char *name, int age, float score);
void display();
};
//People(name, age)就是調用基類的構造函數
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }
void Student::display(){
cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<"。"<<endl;
}
int main(){
Student stu("小明", 16, 90.5);
stu.display();
return 0;
}
C++多繼承(多重繼承)
C++虛繼承和虛基類
C++虛繼承時的構造函數
C++將派生類賦值給基類(向上轉型)
四、多態和虛函數
C++ 多態
通過基類指針只能訪問派生類的成員變量,但是不能訪問派生類的成員函數。
爲了消除這種尷尬,讓基類指針能夠訪問派生類的成員函數,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)。
上面的代碼中,同樣是p->display();這條語句,當 p 指向不同的對象時,它執行的操作是不一樣的。同一條語句可以執行不同的操作,看起來有不同表現方式,這就是多態。
多態是面向對象編程的主要特徵之一,C++中虛函數的唯一用處就是構成多態。
C++提供多態的目的是:可以通過基類指針對所有派生類(包括直接派生和間接派生)的成員變量和成員函數進行“全方位”的訪問,尤其是成員函數。如果沒有多態,我們只能訪問成員變量。
前面我們說過,通過指針調用普通的成員函數時會根據指針的類型(通過哪個類定義的指針)來判斷調用哪個類的成員函數,但是通過本節的分析可以發現,這種說法並不適用於虛函數,虛函數是根據指針的指向來調用的,指針指向哪個類的對象就調用哪個類的虛函數。
注意:
- 只需要在虛函數的聲明處加上 virtual 關鍵字,函數定義處可以加也可以不加。
- 爲了方便,你可以只將基類中的函數聲明爲虛函數,這樣所有派生類中具有遮蔽關係的同名函數都將自動成爲虛函數。關於名字遮蔽已在《C++繼承時的名字遮蔽》一節中進行了講解。
- 當在基類中定義了虛函數時,如果派生類沒有定義新的函數來遮蔽此函數,那麼將使用基類的虛函數。
- 只有派生類的虛函數覆蓋基類的虛函數(函數原型相同)才能構成多態(通過基類指針訪問派生類函數)。例如基類虛函數的原型爲virtual void func();,派生類虛函數的原型爲virtual void func(int);,那麼當基類指針 p 指向派生類對象時,語句p -> func(100);將會出錯,而語句p -> func();將調用基類的函數。
- 構造函數不能是虛函數。對於基類的構造函數,它僅僅是在派生類構造函數中被調用,這種機制不同於繼承。也就是說,派生類不繼承基類的構造函數,將構造函數聲明爲虛函數沒有什麼意義。
構成多態的條件
站在“學院派”的角度講,封裝、繼承和多態是面向對象的三大特徵,封裝、繼承分別在《C++類成員的訪問權限以及類的封裝》《C++繼承和派生簡明教程》中進行了講解,而多態是指通過基類的指針既可以訪問基類的成員,也可以訪問派生類的成員。
下面是構成多態的條件:
- 必須存在繼承關係;
- 繼承關係中必須有同名的虛函數,並且它們是覆蓋關係(函數原型相同)。
- 存在基類的指針,通過該指針調用虛函數。
下面的例子對各種混亂情形進行了演示:
#include <iostream>
using namespace std;
//基類Base
class Base{
public:
virtual void func();
virtual void func(int);
};
void Base::func(){
cout<<"void Base::func()"<<endl;
}
void Base::func(int n){
cout<<"void Base::func(int)"<<endl;
}
//派生類Derived
class Derived: public Base{
public:
void func();
void func(char *);
};
void Derived::func(){
cout<<"void Derived::func()"<<endl;
}
void Derived::func(char *str){
cout<<"void Derived::func(char *)"<<endl;
}
int main(){
Base *p = new Derived();
p -> func(); //輸出void Derived::func()
p -> func(10); //輸出void Base::func(int)
p -> func("http://c.biancheng.net"); //compile error
return 0;
}
在基類 Base 中我們將void func()聲明爲虛函數,這樣派生類 Derived 中的void func()就會自動成爲虛函數。p 是基類 Base 的指針,但是指向了派生類 Derived 的對象。
- 語句p -> func();調用的是派生類的虛函數,構成了多態。
- 語句p -> func(10);調用的是基類的虛函數,因爲派生類中沒有函數覆蓋它。
- 語句p -> func(“http://c.biancheng.net”);出現編譯錯誤,因爲通過基類的指針只能訪問從基類繼承過去的成員,不能訪問派生類新增的成員。
什麼時候聲明虛函數
首先看成員函數所在的類是否會作爲基類。然後看成員函數在類的繼承後有無可能被更改功能,如果希望更改其功能的,一般應該將它聲明爲虛函數。如果成員函數在類被繼承後功能不需修改,或派生類用不到該函數,則不要把它聲明爲虛函數。
包含純虛函數的類稱爲抽象類(Abstract Class)。之所以說它抽象,是因爲它無法實例化,也就是無法創建對象。原因很明顯,純虛函數沒有函數體,不是完整的函數,無法調用,也無法爲其分配內存空間。
抽象類通常是作爲基類,讓派生類去實現純虛函數。派生類必須實現純虛函數才能被實例化。
在實際開發中,你可以定義一個抽象基類,只完成部分功能,未完成的功能交給派生類去實現(誰派生誰實現)。這部分未完成的功能,往往是基類不需要的,或者在基類中無法實現的。雖然抽象基類沒有完成,但是卻強制要求派生類完成,這就是抽象基類的“霸王條款”。
關於純虛函數的幾點說明
- 一個純虛函數就可以使類成爲抽象基類,但是抽象基類中除了包含純虛函數外,還可以包含其它的成員函數(虛函數或普通函數)和成員變量。
- 只有類中的虛函數才能被聲明爲純虛函數,普通成員函數和頂層函數均不能聲明爲純虛函數。
五、模板
泛型程序設計是一種算法在實現時不指定具體要操作的數據的類型的程序設計方法。所謂“泛型”,指的是算法只要實現一遍,就能適用於多種數據類型。泛型程序設計方法的優勢在於能夠減少重複代碼的編寫。
泛型程序設計的概念最早出現於 1983 年的 Ada 語言,其最成功的應用就是 C++ 的標準模板庫(STL)。也可以說,泛型程序設計就是大量編寫模板、使用模板的程序設計。泛型程序設計在 C++ 中的重要性和帶來的好處不亞於面向對象的特性。
所謂函數模板,實際上是建立一個通用函數,它所用到的數據的類型(包括返回值類型、形參類型、局部變量類型)可以不具體指定,而是用一個虛擬的類型來代替(實際上是用一個標識符來佔位),等發生函數調用時再根據傳入的實參來逆推出真正的類型。這個通用函數就稱爲函數模板(Function Template)。
六、模板
C++ STL(標準模板庫)是一套功能強大的 C++ 模板類,提供了通用的模板類和函數,這些模板類和函數可以實現多種流行和常用的算法和數據結構,如向量、鏈表、隊列、棧。
C++ 標準模板庫的核心包括以下三個組件:
組件 | 描述 |
---|---|
容器(Containers) | 容器是用來管理某一類對象的集合。C++ 提供了各種不同類型的容器,比如 deque、list、vector、map 等。 |
算法(Algorithms) | 算法作用於容器。它們提供了執行各種操作的方式,包括對容器內容執行初始化、排序、搜索和轉換等操作。 |
迭代器(iterators) | 迭代器用於遍歷對象集合的元素。這些集合可能是容器,也可能是容器的子集。 |