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