學習C++
C++ 是一個難學易用的語言!
C++ 的難學,不僅在其廣博的語法,以及語法背後的語意,以及語意背後的深層思維,以及深層思維背後的物件模型;
C++ 的難學,還在於它提供了四種不同(但相輔相成)的程式設計思維模式:基于程序procedural-based,基于對象object-based,面向對象object-oriented,泛型思想/設計generic paradigm。
世上沒有白吃的午餐。又要有效率,又要有彈性,又要前瞻望遠,又要回溯相容,
又要能治大國,又要能烹小鮮,學習起來當然就不可能太簡單。C++ 相關書籍之多,車載斗量;如天上繁星,如過江之鯽。
廣博如四庫全書者有之(The C++ Programming Language、C++ Primer),
深奧如重山復水者有之(The Annotated C++ Reference Manual, Inside the C++ Object Model),
細說歷史者有之(The Design and Evolution of C++, Ruminations on C++),
獨沽一味者有之(Polymorphism in C++, Genericity in C++),
獨樹一幟者有之(Design Patterns,Large Scale C++ Software Design, C++ FAQs),
程式庫大全有之(The C++ Standard Library),
另辟蹊徑者有之(Generic Programming and the STL),
工程經驗之累積亦有之(Effective C++, More Effective C++, Exceptional C++)。
這其中,「工程經驗之累積」對已具 C++ 相當基礎的程式員而言,有著致命的吸引力與立竿見影的幫助。
Scott Meyers 的 Effective C++ 和 More Effective C++ 是此類佼佼,
Herb Sutter 的 Exceptional C++ 則是後起之秀。
適合學生學習時能夠方便的在瀏覽器里直接編c++程序
參考資料
黑馬機器人—C++
雞啄米:C++編程入門系列之目錄和總結
++98基礎上學習C++11新特性
Effective Modern C++
C++ 入門教程
魚C工作室 C++快速入門
C++ Primer 5 代碼
C++設計成這樣的原因 《C++演化和設計》
boost庫學習
C++17 High Performance
C++17 STL Cookbook 代碼
C++ 響應式編程(Reactive Programming)
C++ Template 進階指南
C++17 高性能計算
CPP-Data-Structures-and-Algorithms
數據結構和算法動態可視化
VS2010/MFC編程入門教程之目錄和總結
cpp-tutor Code examples for tutoring modern C++ string Dynamic memory allocation C++ Unit testing Smart pointers
基礎議題
pointers(指針)
references(引用)
casts(類型轉換)
arrays(數組)
constructors(構造) default constructors(默認構造函數)
指針與引用的區別
指針與引用看上去完全不同(指針用操作符“*”和“->”,引用使用操作符“. ”),
但是它們似乎有相同的功能。指針與引用都是讓你間接引用其他對象。引用內部實現為常量指針
string& rs; // 錯誤,引用必須被初始化
string s("xyzzy");
string& rs = s; // 正確,rs 指向 s
指針沒有這樣的限制。
string *ps; // 未初始化的指針// 合法但危險
不存在指向空值的引用這個事實意味著使用引用的代碼效率比使用指針的要高。
因為在使用引用之前不需要測試它的合法性。
void printDouble(const double& rd)
{cout << rd; // 不需要測試 rd,它
} // 肯定指向一個 double 值// 相反,指針則應該總是被測試,防止其為空:
void printDouble(const double *pd)
{if (pd) { // 檢查是否為 NULLcout << *pd;}
}
指針與引用的另一個重要的不同是指針可以被重新賦值以指向另一個不同的對象。但是
引用則總是指向在初始化時被指定的對象,以后不能改變。
string s1("Nancy");
string s2("Clancy");
string& rs = s1; // rs 引用 s1
string *ps = &s1; // ps 指向 s1
rs = s2; // rs 仍舊引用 s1,// 但是 s1 的值現在是 "Clancy"ps = &s2; // ps 現在指向 s2;// s1 沒有改變
在以下情況下你應該使用指針,一是你考慮到存在不指向任何對象的可能(在這種情況下,你能夠設置指針為空),二是你需要能夠在不同的時刻指向不同的對象(在這種情況下,你能改變指針的指向)。如果總是指向一個對象并且一旦指向一個對象后就不會改變指向,那么你應該使用引用。
盡量使用 C++風格的類型轉換
仔細想想地位卑賤的類型轉換功能(cast),其在程序設計中的地位就象 goto 語句一樣令人鄙視。
但是它還不是無法令人忍受,因為當在某些緊要的關頭,類型轉換還是必需的,這時它是一個必需品。C風格的類型轉換,過于粗魯,能允許你在任何類型之間進行轉換.
C風格的類型轉換在程序語句中難以識別。在語法上,類型轉換由圓括號和標識符組成,而這些可以用在 Cpp中的任何地方。
C++通過引進四個新的類型轉換操作符克服了 C 風格類型轉換的缺點,這四個操作符是,static_cast,
const_cast,
dynamic_cast,
和 reinterpret_cast。c強制類型轉換 (type) expression1. static_caststatic_cast<type>(expression) a. 基礎類型之間互轉。如:float轉成int、int轉成unsigned int等。int firstNumber, secondNumber;...double result = ((double)firstNumber)/secondNumber; // c風格 如果用上述新的類型轉換方法,你應該這樣寫:double result = static_cast<double>(firstNumber)/secondNumber;// c++風格static_cast 不能從表達式中去除 const 屬性,因為另一個新的類型轉換操作符 const_cast 有這樣的功能。const_cast 最普通的用途就是轉換掉對象的 const 屬性。b. 指針與void*之間互轉。如:float*轉成void*、CBase*轉成void*、函數指針轉成void*、void*轉成CBase*等c. 派生類指針【引用】轉成基類指針【引用】。如:Derive*轉成Base*、Derive&轉成Base&等d. 非virtual繼承時,可將基類指針【引用】轉成派生類指針【引用】(多繼承時,會做偏移處理)。如:Base*轉成Derive*、Base&轉成Derive&等
class Widget { ... };
class SpecialWidget: public Widget { ... };
void update(SpecialWidget *psw);
SpecialWidget sw; // sw 是一個非 const 對象。
const SpecialWidget& csw = sw; // csw 是 sw 的一個引用// 它是一個 const 對象
update(&csw); // 錯誤!不能傳遞一個 const SpecialWidget* 變量// 給一個處理 SpecialWidget*類型變量的函數// 2.const_cast<type>(expression) 轉換掉對象的 const 屬性====update(const_cast<SpecialWidget*>(&csw));// 正確,csw 的 const 被顯示地轉換掉(// csw 和 sw 兩個變量值在 update//函數中能被更新)
update((SpecialWidget*)&csw);// 同上,但用了一個更難識別//的 C 風格的類型轉換
Widget *pw = new SpecialWidget;
update(pw); // 錯誤!pw 的類型是 Widget*,但是 // update 函數處理的是 SpecialWidget*類型update(const_cast<SpecialWidget*>(pw));// 錯誤!const_cast 僅能被用在影響// constness or volatileness 的地方上。,// 不能用在向繼承子類進行類型轉換。
3. dynamic_cast dynamic_cast<type>(expression) 專門用于處理多態機制,對繼承體系內的對象(類中必須含有至少一個虛函數)的指針【引用】進行轉換,轉換時會進行類型檢查.它被用于安全地沿著類的繼承關系向下進行類型轉換。用 dynamic_cast 把指向基類的指針或引用轉換成指向其派生類或其兄弟類的指針或引用,而且你能知道轉換是否成功。失敗的轉換將返回空指針(當對指針進行類型轉換時)或者拋出異常(當對引用進行類型轉換時):
Widget *pw; // 基類 對象 指針
...
update(dynamic_cast<SpecialWidget*>(pw));// 正確,傳遞給 update 函數一個指針// 是指向變量類型為 SpecialWidget 的 pw 的指針// 如果 pw 確實指向一個對象,// 否則傳遞過去的將使空指針。
void updateViaRef(SpecialWidget& rsw);
updateViaRef(dynamic_cast<SpecialWidget&>(*pw));//正確。 傳遞給 updateViaRef 函數// SpecialWidget pw 指針,如果 pw// 確實指向了某個對象// 否則將拋出異常 int firstNumber, secondNumber;
...
double result = dynamic_cast<double>(firstNumber)/secondNumber;// 錯誤!沒有繼承關系,想在沒有繼承關系的類型中進行轉換,你可能想到 static_cast。
const SpecialWidget sw;
...
update(dynamic_cast<SpecialWidget*>(&sw));// 錯誤! dynamic_cast 不能轉換掉 const。// 為了去除const,你總得用 const_cast。
4.reinterpret_cast 重新解釋reinterpret_cast <new_type> (expression)用來處理無關類型之間的轉換;它會產生一個新的值,這個值會有與原始參數(expressoin)有完全相同的比特位.字面意思:重新解釋(類型的比特位)a.從指針類型到一個足夠大的整數類型b.從整數類型或者枚舉類型到指針類型c.從一個指向函數的指針到另一個不同類型的指向函數的指針d.從一個指向對象的指針到另一個不同類型的指向對象的指針e.從一個指向類函數成員的指針到另一個指向不同類型的函數成員的指針f.從一個指向類數據成員的指針到另一個指向不同類型的數據成員的指針使用reinterpret_casts 的代碼很難移植。reinterpret_casts 的最普通的用途就是在函數指針類型之間進行轉換。
typedef void (*FuncPtr)(); // FuncPtr is 一個指向函數的指針,該函數沒有參數// 返回值類型為 void
FuncPtr funcPtrArray[10]; // funcPtrArray 是一個能容納10 個 FuncPtrs 指針的數組 // 如果要把一個指向下面函數的指針存入 funcPtrArray 數組:
// int doSomething(); // 你不能不經過類型轉換而直接去做,因為 doSomething 函數對于 funcPtrArray 數組來說有一個錯誤的類型。
// 在 FuncPtrArray 數組里的函數返回值是 void 類型,而 doSomething函數返回值是 int 類型。
funcPtrArray[0] = &doSomething; // 錯誤!類型不匹配
// reinterpret_cast 可以讓你迫使編譯器以你的方法去看待它們:
funcPtrArray[0] = // 可編譯通過reinterpret_cast<FuncPtr>(&doSomething);
可以用下面的宏替換來模擬新的類型轉換語法:
#define static_cast(TYPE,EXPR) ((TYPE)(EXPR)) // 后面為 c語言強轉方式
#define const_cast(TYPE,EXPR) ((TYPE)(EXPR))
#define reinterpret_cast(TYPE,EXPR) ((TYPE)(EXPR))
你可以象這樣使用使用:
double result = static_cast(double, firstNumber)/secondNumber;
update(const_cast(SpecialWidget*, &sw));
funcPtrArray[0] = reinterpret_cast(FuncPtr, &doSomething); #define dynamic_cast(TYPE,EXPR) (TYPE)(EXPR)
// 請記住,這個模擬并不能完全實現 dynamic_cast 的功能,它沒有辦法知道轉換是否失敗。
不要對數組使用多態
類繼承的最重要的特性是你可以通過基類(父類) 指針或引用 來 操作 派生類(子類)。
多態和指針算法不能混合在一起來用,所以數組與多態也不能用在一起。
避免無用的缺省構造函數
在一個完美的世界里,無需任何數據即可建立對象的類可以包含缺省構造函數,
而需要數據來建立對象的類則不能包含缺省構造函數。
唉!可是我們的現實世界不是完美的,所以我們必須考慮更多的因素。
特別是如果一個類沒有缺省構造函數,就會存在一些使用上的限制。
C++類成員和數據成員初始化總結
C++為類中提供類成員的初始化列表
類對象的構造順序是這樣的:
1.分配內存,調用構造函數時,隱式/顯示的初始化各數據成員
2.進入構造函數后在構造函數中執行一般計算
規則:1.類里面的任何成員變量在定義時是不能初始化的。2.一般的數據成員可以在構造函數中初始化。3.const數據成員必須在構造函數的初始化列表中初始化。4.static要在類的定義外面初始化。 5.數組成員是不能在初始化列表里初始化的。6.不能給數組指定明顯的初始化。 這6條一起,說明了一個問題:C++里面是不能定義常量數組的!
因為3和5的矛盾。這個事情似乎說不過去啊?沒有辦法,我只好轉而求助于靜態數據成員。到此,我的問題解決。
但是我還想趁機復習一下C++類的初始化:1.初始化列表:CSomeClass::CSomeClass() : x(0), y(1){}2.類外初始化:int CSomeClass::myVar=3;3.const常量定義必須初始化,C++類里面使用初始化列表;4.C++類不能定義常量數組。
淺拷貝:只是將數據成員的值進行簡單的拷貝
深拷貝:在淺拷貝的基礎上,也將堆中的數據也進行拷貝
C++ 泛型技術 泛化技術 增加不確定性 通用性 靈活性
所謂泛型技術,說白了就是試圖使用不變的代碼來實現可變的算法
比如:模板技術,RTTI技術,虛函數技術
要么是試圖做到在編譯時決議,要么試圖做到運行時決議。
【【A】】 RTTI技術
RTTI(Run-Time Type Identification)是面向對象程序設計中一種重要的技術。
現行的C++標準對RTTI已經有了明確的支持。不過在某些情況下出于特殊的開發需要,
我們需要自己編碼來實現。本文介紹了一些關于RTTI的基礎知識及其原理和實現。
RTTI需求:
和很多其他語言一樣,C++是一種靜態類型語言。其數據類型是在編譯期就確定的,
不能在運行時更改。然而由于面向對象程序設計中多態性的要求,C++中的指針或引用
(Reference)本身的類型,可能與它實際代表(指向或引用)的類型并不一致。有時我們需
要將一個多態指針轉換為其實際指向對象的類型,就需要知道運行時的類型信息,
這就產生了運行時類型識別的要求。
C++對RTTI的支持:C++提供了兩個關鍵字typeid(指示類型) 和dynamic_cast(類型強轉)和一個type_info類來支持RTTI
#############################################################
###【1】dynamic_cast操作符: 運行時強制類型轉換
它允許在運行時刻進行類型轉換,
從而使程序能夠在一個類層次結構安全地轉換類型。
dynamic_cast提供了兩種轉換方式,
把基類指針轉換成派生類指針,
或者把指向基類的左值轉換成派生類的引用。
見下例講述:
void company::payroll(employee *pe) {//指針//對指針轉換失敗,dynamic_cast返回NULLif(programmer *pm=dynamic_cast(pe)){ //基類 employee >>> 派生類 programmerpm->bonus(); }}
void company::payroll(employee &re) {//引用 變量別名try{//對引用轉換失敗的話,則會以拋出異常來報告錯誤programmer &rm = dynamic_cast(re);rm->bonus();
}catch(std::bad_cast){}
}
這里bonus是programmer的成員函數,基類employee不具備這個特性。
所以我們必須使用安全的由基類到派生類類型轉換,識別出programmer指針。
int a=1;int *p=&a;//指針是變量的地址 *p 定義時 和 在函數參數中時 是 表示指針變量 其他表示取值int a=1;int &b=a;//引用 是 變量別名 &放在左邊 以及在 函數參數中 是引用 方在右邊是 取地址
上面定義了一個整形變量和一個指針變量p,該指針變量指向a的存儲單元,
即p的值是a存儲單元的地址。
而下面2句定義了一個整形變量a和這個整形a的引用b,
事實上a和b是同一個東西,在內存占有同一個存儲單元。
區別:
【1】可以有const指針,但是沒有const引用;
【2】指針可以有多級,但是引用只能是一級(int **p;合法 而 int &&a是不合法的;
【3】指針的值可以為空,但是引用的值不能為NULL,并且引用在定義的時候必須初始化;
【4】指針的值在初始化后可以改變,即指向其它的存儲單元,而引用在進行初始化后就不會再改變了;
【5】"sizeof引用"得到的是所指向的變量(對象)的大小,而"sizeof指針"得到的是指針本身的大小;
【6】指針和引用的自增(++)運算意義不一樣;
【7】引用作為函數的參數進行傳遞,傳遞的是實參本身,不是實參的一個拷貝;
【8】 用指針傳遞參數,可實現對實參進行改變的目的,是因為傳遞過來的是實參的地址,但是指針不會改變。
#include<iostream>
using namespace std;void test(int *&p)//這里是 指針p的引用 ;如果是 *p 指針 p修改不了 可以修改p指向的內容
{int a=1;p=&a;//可以修改p 這里的& 是取地址cout<<p<<" "<<*p<<endl;//這里的*是取值
}int main(void)
{int *p=NULL;//這里的 *是 指針變量定義test(p);if(p!=NULL)cout<<"指針p不為NULL"<<endl;system("pause");return 0;
}
【2】typeid操作符:它指出指針或引用指向的對象的實際派生類型。
例如:
employee* pe=new manager;typeid(*pe) == typeid(manager) //等于truetypeid的返回是type_info類型。class type_info {private:type_info(const type_info&);type_info& operator=( const type_info& );public:virtual ~type_info();int operator==( const type_info& ) const;int operator!=( const type_info& ) const;const char* name() const;
};
##############################
【【B】】模板
【1】函數模板
函數的重載。例如:
int add(int a, int b)
{ return a + b;
}
double add(double a, double b)
{ return a + b;
}
char add(char a, char b)
{ return a + b;
}
這些函數幾乎相同,每個函數的函數體是相同的,功能也是相同的,
它們之間唯一的不同在于形參的類型和函數返回值的類型。
C++有模板(template)機制,可以使用函數模板解決上述存在的問題。
函數模板(function template)是一個獨立于類型的函數,
可作為一種模式,產生函數的特定類型版本。
template<模板形參表>返回值類型 函數名(形式參數列表)
{ 函數體
}
模板形參表(template parameter list)是用一對尖括號<>括起來的
一個或多個模板形參的列表,不允許為空,形參之間以逗號分隔。
第一種形式如下所示: <typename 類型參數名1,typename 類型參數名2,..>第二種形式如下所示: <class 類型參數名1,class 類型參數名2,...>
在函數模板形參表中,typename和class具有相同含義,可以互換使用,
或者兩個關鍵字都可以在同一模板形參表中使用。
不過由于C++中class關鍵字往往容易與類聯系在一起,
所以使用關鍵字typename比使用class更直觀,
typename可以更加直觀的反映出后面的名字是一個類型名。
模板定義的后面是函數定義,在函數定義中,可以使用模板形參表中的類型參數。
例如:
templateT add(T a, T b)
{
return a + b;
}
函數模板定義語法的含義是一個通用型函數,
這個函數類型和形參類型沒有具體的指定,而是一個類型記號表示
,類型記號由編譯器根據所用的函數而確定,這種通用型函數成為函數模板。
#include<iostream> using namespace std; template<typename T>T add(T a, T b)//函數模板
{ return a + b;
} int main()
{ std::cout << "int_add = " << add(10,20)<< std::endl; std::cout << "double_add = " << add(10.2, 20.5) << std::endl; std::cout << "char_add = " << add(10, 20) << std::endl; std::system("pause"); return 0;
}
【2】類模板
類似于函數模板的做法,類模板對數據成員的
數據類型和成員函數的參數類型進行泛化。
如下是類模板的一個基本定義形式,
關鍵字template說明類型T1~Tn是模本類型, typename 或 class關鍵字
成員函數可在類模板的聲明中定義。
template<class T1,class T2, ... ,class Tn> class 類名
{ //數據成員聲明或定義;
};
template<class T1, class T2, ....., class Tn> 返回值 類名<T1,T2, ....., Tn>::成員函數1
{ //函數定義
}
template<class T1, class T2, ....., class Tn> 返回值 類名<T1, T2, ....., Tn>::成員函數2
{ //函數定義 }
不同于非模板代碼的組織方式,函數模板和類模板的聲明和定義代碼,
一般都編寫在.h頭文件中,以免由于為具現而提示編譯鏈接錯誤。
下面給出一個類模板表示平面上點的示例:
template<class T> //【0】類模板定義 class Point //【1】Point不是類名是模板名 { public: Point::x(0), y(0) {} //【2】默認構造函數 初始化為0 Point(const T a, const T b) :(x)(a), y(b) {}//【3】帶參數的構造函數 a賦值給x b賦值給y void Set(const T a, const T b); void Display(); private: T x; T y;
};
【【C】】虛函數技術
參考
C++中的虛函數的作用主要是實現了多態的機制。
關于多態,簡而言之就是用父類型別的指針指向其子類的實例
,然后通過父類的指針調用實際子類的成員函數。
這種技術可以讓父類的指針有“多種形態”.
class A{int a;// a的后面直接是虛函數,內存中存在為虛函數表指針 public:virtual void f();virtual void g(int);virtual void h(double);
};class B: public A{public:int b;void g(int);// 會覆蓋 A類的 A::gvirtual void m(B*);
};class C: public B{public:int c;void h(double);// 會覆蓋 B的父類A 的 A:hvirtual void n(C*);
}&C, 類C的實例的內存空間大概如下:
變量a
虛函數表指針vptr ------> |&A::f|&B::g|&C::h|&B::m||&C::n|.| 按繼承的先后順序存放函數
變量b
變量c
虛函數表
對C++ 了解的人都應該知道虛函數(Virtual Function)
是通過一張虛函數表(Virtual Table)來實現的。簡稱為V-Table。
這個表中,主是要一個類的虛函數的地址表,這張表解決了繼承、覆蓋的問題,
保證其容真實反應實際的函數。這樣,在有虛函數的類的實例中這個表被分配在了
這個實例的內存中,所以,當我們用父類的指針來操作一個子類的時候,
這張虛函數表就顯得由為重要了,它就像一個地圖一樣,指明了實際所應該調用的函數。
class Base {public:virtual void f() { cout << "Base::f" << endl; }virtual void g() { cout << "Base::g" << endl; }virtual void h() { cout << "Base::h" << endl; }};
我們可以通過Base的實例來得到虛函數表。 下面是實際例程:
typedef void(*Fun)(void);//函數指針 Fun void(void) 返回值為void、輸入值為voidBase b;// 定義Base類 的實例b Fun pFun = NULL;// 定義一個函數指針Fun 變量 pFun, 初始化為 NULLcout << "虛函數表地址:" << (int*)(&b) << endl;// &b 取地址 (int*) 強制轉換成int類型的指針(表id)cout << "虛函數表 — 第一個函數地址:" << (int*)*(int*)(&b) << endl;// (int*)(&b) 虛函數表地址 *(int*)(&b) 取虛函數表地址 內的內容 為 虛函數地址 (int*) 強制轉換成int類型的指針pFun = (Fun)*((int*)*(int*)(&b));//得到第一個函數 *((int*)*(int*)(&b))pFun();
實際運行經果如下:
虛函數表地址:0012FED4
虛函數表 — 第一個函數地址:0044F148
Base::f
(Fun)*((int*)*(int*)(&b)+0); // Base::f()(Fun)*((int*)*(int*)(&b)+1); // Base::g()(Fun)*((int*)*(int*)(&b)+2); // Base::h()
|Base::f()|Base::g()|Base::h()|.|
對象的內存布局 和 虛函數表 :
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-N7M2PFXv-1691788331828)(https://github.com/Ewenwan/ShiYanLou/blob/master/learn_cpp/img/base_class_virtual_table.PNG)]
(&b) 對象的地址
(int*)(&b) 強行把&b 轉成int*,取得 虛函數表 的地址
(int*)*(int*)(&b) 解引用后再強轉 得到 虛函數 的地址注意:在上面這個圖中,我在虛函數表的最后多加了一個結點,
這是虛函數表的結束結點,就像字符串的結束符“/0”一樣,
其標志了虛函數表的結束。這個結束標志的值在不同的編譯器下是不同的。
在WinXP+VS2003下,這個值是NULL。
而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,這個值如果是1,
表示還有下一個虛函數表,如果值是0,表示是最后一個虛函數表。分別說明“無覆蓋”和“有覆蓋”時的虛函數表的樣子。
沒有覆蓋父類的虛函數是毫無意義的。
我之所以要講述沒有覆蓋的情況,主要目的是為了給一個對比。
在比較之下,我們可以更加清楚地知道其內部的具體實現。
【1】一般繼承(無虛函數覆蓋,子類中定義的函數名與父類中的不同)
假設有如下所示的一個繼承關系:
Derive 類 繼承 父類 Base
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-FUTF5a8J-1691788331829)(https://github.com/Ewenwan/ShiYanLou/blob/master/learn_cpp/img/inheritance_class.PNG)]
在這個繼承關系中,子類沒有重載任何父類的函數。那么,在派生類的實例中,
對于子類對象的實例:Derive d; 的虛函數表如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-AHQXR69Q-1691788331829)(https://github.com/Ewenwan/ShiYanLou/blob/master/learn_cpp/img/inheritance_class_virtual_table.PNG)]
我們可以看到下面幾點:
1)虛函數按照其聲明順序放于表中。
2)父類的虛函數在子類的虛函數前面。
|Base::f()|Base::g()|Base::h()|Derive::f1()|Derive::g1()|Derive::h1()|.|
【2】一般繼承(有虛函數覆蓋)
如果子類中有虛函數重載了父類的虛函數 f()
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-m4Brox1y-1691788331830)(https://github.com/Ewenwan/ShiYanLou/blob/master/learn_cpp/img/class_derive_over.PNG)]
1)子類覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。
2)沒有被覆蓋的函數依舊。
|Derive::f1()|Base::g()|Base::h()|Derive::g1()|Derive::h1()|.|
這樣,我們就可以看到對于下面這樣的程序,
Base *b = new Derive();// 父類指針Base* b 指向了子類 Derive()b->f();// 會調用子類中覆蓋了父類的同名的函數 Derive::f()
子類 Derive 的虛函數表:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-XK9hz81n-1691788331830)(https://github.com/Ewenwan/ShiYanLou/blob/master/learn_cpp/img/class_derive_over_virtual_table.PNG)]
由b所指的內存中的虛函數表的f()的位置已經被 Derive::f() 函數地址所取代,
于是在實際調用發生時,是Derive::f()被調用了。這就實現了多態。
【3】多重繼承(無虛函數覆蓋)
Derive 繼承 于 Base1 Base2 Base3
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fQmajIkZ-1691788331830)(https://github.com/Ewenwan/ShiYanLou/blob/master/learn_cpp/img/class_derive_more_class.PNG)]
|Base1::f()|Base1::g()|Base1::h()|Derive::f1()|Derive::g1()|Derive::h1()|.||Base2::f()|Base2::g()|Base2::h()|.||Base3::f()|Base3::g()|Base3::h()|.|
子類內存空間:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ZCrDKsHz-1691788331831)(https://github.com/Ewenwan/ShiYanLou/blob/master/learn_cpp/img/class_derive_more_class_virtual_table.PNG)]
我們可以看到:
1) 在子類內存空間中,存在每個父類的虛表。
2) 子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)
這樣做就是為了解決不同的父類類型的指針指向同一個子類實例,而能夠調用到實際的函數。
【4】多重繼承(有虛函數覆蓋)
Derive 繼承 于 Base1 Base2 Base3 且有 同名函數 f()
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UmheiTX3-1691788331831)(https://github.com/Ewenwan/ShiYanLou/blob/master/learn_cpp/img/class_derive_more_class_over.PNG)]
|Derive::f()|Base1::g()|Base1::h()|Derive::g1()|Derive::h1()|.||Derive::f()|Base2::g()|Base2::h()|.||Derive::f()|Base3::g()|Base3::h()|.|
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-XkQawZoy-1691788331831)(https://github.com/Ewenwan/ShiYanLou/blob/master/learn_cpp/img/class_derive_more_class_over_virtual_table.PNG)]
三個父類虛函數表中的f()的位置被替換成了子類的函數指針。
這樣,我們就可以 用 任一靜態類型的父類來指向 子類,并調用子類的f()了。
如:
Derive d;// 子類Base1 *b1 = &d;// 父類1的指針 b1 指向子類dBase2 *b2 = &d;// 父類2的指針 b2 指向子類dBase3 *b3 = &d;// 父類3的指針 b3 指向子類db1->f(); // Derive::f() 之類d的內存空間中 三個子類的 虛函數表 的第一個函數都是 Derive::f()b2->f(); // Derive::f()b3->f(); // Derive::f()b1->g(); //Base1::g()b2->g(); //Base2::g()b3->g(); //Base3::g()b1->g(); //Base1::h()b2->g(); //Base2::h()b3->g(); //Base3::h()
【5】安全性
一、通過父類型的指針訪問子類自己的虛函數 會出錯
Base1 *b1 = new Derive();b1->g1(); //編譯出錯 g1() 為子類自己的虛函數b1->h1(); //編譯出錯 h1() 為子類自己的虛函數
任何妄圖使用父類指針想調用子類中的未覆蓋父類的成員函數的行為都會被編譯器視為非法。
二、訪問non-public (private 或者 protected)的虛函數
如果父類的虛函數是private或是protected的,但這些非public的虛函數同樣會存在于虛函數表中,
所以,我們同樣可以使用訪問虛函數表的方式來訪問這些non-public的虛函數,這是很容易做到的。
class Base {private:virtual void f() { cout << "Base::f" << endl; }
};
class Derive : public Base{// 子類Derive 繼承 于 Base父類
};
typedef void(*Fun)(void); // 函數指針void main() {Derive d;Fun pFun = (Fun)*((int*)*(int*)(&d)+0);//通過虛函數指針 調用父類的 私有虛函數pFun();// 會打印 Base::f
}
下面是一個關于多重繼承的虛函數表訪問的例程:
多重繼承的虛函數表訪問的例程