最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C++之初識多態(tài)(Visual Studio 2019)的使用

 更新時間:2026年04月09日 10:06:58   作者:云雷屯176  
文章主要解釋了C++中的多態(tài)概念、實現(xiàn)細節(jié)及一些相關(guān)特性,如虛函數(shù)、override和final關(guān)鍵字、純虛函數(shù)和抽象類,通過代碼示例展示了多態(tài)原理,包括虛函數(shù)表和動態(tài)綁定的概念,并探討了多繼承下虛函數(shù)表的行為

此文章關(guān)于多態(tài)的代碼全部是使用Visua Studio2019 (x86) 實現(xiàn)的,C++多態(tài)在不同編譯器中的實現(xiàn)細節(jié)可能不同,所以部分情況下相同代碼運行結(jié)果可能不同,在此聲明。

多態(tài)的概念

多態(tài),顧名思義,即多種形態(tài)。具體來說就是對于某個行為,不同的對象去完成該行為會產(chǎn)生不同的狀態(tài)。

就像 “叫” 這個行為,貓來完成時其叫聲是 “喵喵喵” ,而狗則是 “汪汪汪”。

還有坐公交車,老人和小孩坐車是免票,而其他人則是全價票。

同樣的一個動作,不同的對象來執(zhí)行,其實現(xiàn)的方法不同。

多態(tài)的定義與實現(xiàn)

多態(tài)是建立在繼承基礎(chǔ)上的,在不同的繼承關(guān)系下的類對象,調(diào)用同一函數(shù),產(chǎn)生不同的行為。

在C++中,繼承體系下構(gòu)成多態(tài)需要滿足以下兩個條件:

  • 1.必須通過基類指針或引用調(diào)用虛函數(shù)。
  • 2.被調(diào)用函數(shù)必須是虛函數(shù),且派生類必須對基類虛函數(shù)進行重寫。 

虛函數(shù)

虛函數(shù):即被 virtual 修飾的類的成員函數(shù)。

class A {
public:
    virtual int add(int a, int b) {
        return a + b;
    }
}

使用virtual修飾虛函數(shù)時,只需要修飾類內(nèi)部修飾即可。

class A {
public:
	virtual void print_1();
};

virtual void A::print_1() {
	cout << "1 : class A" << endl;
}

 

虛函數(shù)的重寫

虛函數(shù)的重寫指的是:派生類中有一個與基類完全相同的虛函數(shù) (即派生類的虛函數(shù)與基類虛函數(shù)的返回值類型,參數(shù)列表,函數(shù)名完全相同),稱子類的虛函數(shù)重寫了基類的虛函數(shù)。

class A {
public:
	virtual void print_1() {
		cout << "1 : class A" << endl;
	}
};

class B : public A {
public:
	virtual void print_1() {
		cout << "1 : class B" << endl;
	}
};

void func(A& a) {
	a.print_1();
}

int main() {
	A a;
	B b;
	func(a);
	func(b);
	return 0;
}

PS.子類重寫父類虛函數(shù)時子類虛函數(shù)可以不加virtual關(guān)鍵字,這樣也可以構(gòu)成重寫——繼承后基類的虛函數(shù)被繼承下來了,在派生類之中依舊保持虛函數(shù)屬性。但是這種寫法并不規(guī)范,并不建議使用。

虛函數(shù)重寫的兩個例外

1.協(xié)變

  • 基類與派生類虛函數(shù)返回值不同
  • 派生類重寫基類虛函數(shù)時,若基類虛函數(shù)的返回類型是基類的指針或引用,新的返回類型是派生類的指針或引用,依舊可以構(gòu)成重寫。這樣的重寫被稱為返回類型協(xié)變。
class A {
public:
	virtual A& print_1() {
		cout << "1 : class A" << endl;
		return *this;
	}
};

class B : public A {
public:
	virtual B& print_1() {
		cout << "1 : class B" << endl;
		return *this;
	}
};

需要注意的是,只有返回對應類型的指針或引用才會構(gòu)成協(xié)變,單純返回派生類或基類類型并不構(gòu)成協(xié)變。

class A {
public:
	virtual A print_1() {
		cout << "1 : class A" << endl;
		return *this;
	}
};

class B : public A {
public:
	virtual B print_1() {
		cout << "1 : class B" << endl;
		return *this;
	}
};

2.析構(gòu)函數(shù)的重寫

  • 基類與派生類析構(gòu)函數(shù)函數(shù)名不同
  • 當基類的析構(gòu)函數(shù)為虛函數(shù)時,此時派生類的虛構(gòu)函數(shù)只要定義,無論是否被virtual關(guān)鍵字修飾,都與基類析構(gòu)函數(shù)構(gòu)成重寫。
class A {
public:
	virtual ~A() {
		cout << "A" << endl;
	}
};

class B : public A {
public:
	virtual ~B() {
		cout << "B" << endl;
	}
};


int main() {
	A* a = new B;
	delete a;
	return 0;
}

 

析構(gòu)函數(shù)的重寫最常見的應用便是基類指針指向派生類對象的情況下,如果我們所使用的派生類對象是從堆上 new 來的,那么,在沒有重寫析構(gòu)函數(shù)的情況下,在基類對象 delete 時,只會調(diào)用基類的析構(gòu)函數(shù)釋放基類的空間,而派生類對象所占用的空間則不會被釋放,就會導致內(nèi)存泄漏。

C++11 override & final

1. override

  • 在C++中,override關(guān)鍵字用來表示當前虛函數(shù)重寫了基類虛函數(shù)。
  • 在函數(shù)比較多,繼承關(guān)系負責的情況下,override關(guān)鍵字可以起到提示編寫者與其后繼者某個函數(shù)重寫了基類的虛函數(shù)——也代表這個函數(shù)不是派生類中定義的。
  • override關(guān)鍵字可以強制編譯器去檢查某個函數(shù)是否重寫了基類虛函數(shù),若否,則會報錯。
class A {
public:

	virtual void print() {
		cout << "a" << endl;
	}
};

class B : public A {
public:
	virtual void print() override {
		cout << "b" << endl;
	}
};

當override修飾的函數(shù)并非重寫的基類虛函數(shù)時,會報錯:

class A {
public:

	virtual void print() {
		cout << "a" << endl;
	}
};

class B : public A {
public:
	virtual void not_print() override {
		cout << "b" << endl;
	}
};

2.final

  • final用于修飾虛函數(shù),表明該虛函數(shù)不能被重寫
  • 我們是可以直接用final修飾父類虛函數(shù)的,表明該虛函數(shù)不能被重寫,但并不建議這樣做,虛類既然與平常的類不同,其實例化時自然會比普通的類多一份開,而final只是指明了這個虛函數(shù)不可以被重寫,但是虛類的相關(guān)開銷依舊在。
class A {
public:
	int a;
	virtual void print() final{
		cout << "a" << endl;
	}
};

class B : public A {
public:
	int b;
	void a_print() {
		cout << "b" << endl;
	}
};

class C {
public:
	int a;
	void print() {
		cout << "a" << endl;
	}
};

class D : public C {
public:
	int b;
	void a_print() {
		cout << "b" << endl;
	}
};

int main() {
	B b;
	D d;
	b.b = 3;
	b.a = 2;
	d.a = 2;
	d.b = 3;
	while (0);
	return 0;
}

 

重載&重寫&重定義

重載

將語義或功能相似的函數(shù)用同一個名字表示。

條件:

  • 1.兩個函數(shù)在同一個作用域
  • 2.函數(shù)名相同
  • 3.參數(shù)列表不同
  • 4.返回值可以不同

重寫 (覆蓋)

覆蓋是C++中多態(tài)的基礎(chǔ),使派生類可以重新定義基類的虛函數(shù)。

條件:

  • 1.兩個函數(shù)別屬于基類成員函數(shù)與派生類成員函數(shù)
  • 2.兩個函數(shù)都必須是虛函數(shù)
  • 3.函數(shù)名相同 (析構(gòu)函數(shù)除外,至少看起來不同)
  • 4.返回值類型相同 (協(xié)變例外)
  • 5.參數(shù)列表相同
  • 6.函數(shù)的訪問權(quán)限可以不同 (如基類中的虛函數(shù)的訪問權(quán)限是 private ,派生類中可以重寫為public、protected)

重定義 (同名隱藏)

在繼承體系中,子類與父類中有同名成員,子類可以屏蔽父類指針對子類中父類成員的直接訪問。(但是可以使用 :: 訪問父類成員)

條件:

  • 1.在不同作用域
  • 2.函數(shù)名相同
  • 3.返回值可以不同
  • 4.在繼承體系中,只要不構(gòu)成重寫就是重定義
  • 參數(shù)相同時,基類沒有關(guān)鍵字virtual修飾,基類的函數(shù)被隱藏
  • 參數(shù)不同時,無論有沒有virtual關(guān)鍵字修飾,基類函數(shù)都被隱藏
class A {
	void print() {
		cout << "a" << endl;
	}
};

class B : public A {
public:
	int b;
	void print() {
		cout << "b" << endl;
	}
};

int main() {
	A a;
	B b;
	a.print();
	b.print();
	while (0);
	return 0;
}

 

純虛函數(shù)及抽象類

在有些時候,基類的某些行為在沒有被派生類重寫之前會非常不明確,或者說,不合常理,不好定義。比如我們先前舉過的例子——動物的 “叫” 這個行為,在動物這個基類中我們很難定義它具體是一種什么樣的行為,表達方式是怎樣的。我們是無法對這個行為做出具體的定義的,無法提供實現(xiàn)。但是作為一種 “共性” 我們又想要在基類中聲明它。

于是乎,在虛函數(shù)的基礎(chǔ)上,C++又提出了一種新的函數(shù)——純虛函數(shù)。

  • 在虛函數(shù)后面寫上 = 0 ,則這個函數(shù)就是純虛函數(shù)。
  • 包含純虛函數(shù)的類叫做接口類——也被稱為抽象類,抽象類不能實例化對象。
  • 派生類繼承抽象類后,如果不重寫基類的純虛函數(shù),也無法實例化對象。
  • 純虛函數(shù)規(guī)定了派生類必須重寫,同時也實現(xiàn)了接口繼承。
class A {
public:
	virtual void print() = 0;
};

class B : public A {
public:
	int b;
	virtual void print() {
		cout << "b" << endl;
	}
};

int main() {
	B b;
	b.print();
	while (0);
	return 0;
}

接口繼承與實現(xiàn)繼承

普通函數(shù)的繼承是一種實現(xiàn)繼承,派生類繼承了基類的函數(shù),可以使用函數(shù),這種繼承繼承的是函數(shù)的實現(xiàn)。

虛函數(shù)的繼承則是一種接口繼承,派生類繼承的只是虛函數(shù)的接口,目的是為了重寫,從而達成多態(tài),繼承的是一個接口。

所以,如果不實現(xiàn)多態(tài),就不要把函數(shù)設(shè)置為虛函數(shù)。

多態(tài)的原理

class A {
public:
	int _a1;
	int _a2;

	virtual void study() {
		cout << "study" << endl;
	}

	A()
		:_a1(1)
		,_a2(2)
	{}
};


int main() {
	A a;
	return 0;
}

以上代碼中,類A的實例化對象a在實例化后,其內(nèi)存如上圖所示,明顯在我們所定義的變量a1, a2之前,還有著一串神奇的數(shù)字——00 75 9b 34。(據(jù)說有些編譯器這玩意可能在后邊)

單從表面上來看,它應該是一個指針。而實質(zhì)上,它的確是一個指針。

在VS調(diào)試模式下的局部變量窗口中,我們可以很清楚的看到,這玩意還不是一個簡單的指針,它是一個二級指針,指針名為 __vfptr (v——virtual f——function)我們通常稱其為虛函數(shù)表指針。

 進一步將這個虛函數(shù)表指針展開來看

很容易看到,這個虛函數(shù)表指針指向的對象是一個函數(shù)指針,而函數(shù)指針指向的對象則是我們代碼中所定義的虛函數(shù)——A::study(void)

一個含有虛函數(shù)的類中都至少有一個虛函數(shù)表指針,因為虛函數(shù)的地址要被存放到虛函數(shù)表中。

虛函數(shù)表也簡稱虛表。

下面,我們將之放入繼承體系之中來分析。

class A {
public:
	int _a1;
	int _a2;

	virtual void func1() {
		cout << "A::func1" << endl;
	}

	virtual void func2() {
		cout << "A::func2" << endl;
	}

	virtual void func3() {
		cout << "A::func3" << endl;
	}

	A()
		:_a1(1)
		,_a2(2)
	{}
};

class B : public A{
public:
	int _b1;
	int _b2;

	virtual void func1() {
		cout << "B::func1" << endl;
	}

	virtual void func2() {
		cout << "B::func2" << endl;
	}

	B()
		:_b1(3)
		,_b2(4)
	{}
};

int main() {
	A a;
	B b;
	return 0;
}

通過以上調(diào)試圖片,可以看出

1.基類中有一個虛函數(shù)表指針,虛函數(shù)表中存儲著三個基類虛函數(shù)的地址。

2.派生類對象中也有一個虛函數(shù)表指針,且該虛函數(shù)表由兩部分構(gòu)成——派生類重寫的虛函數(shù)以及從基類中繼承的虛函數(shù)。

3.基類對象與派生類對象的虛函數(shù)表中的內(nèi)容是不同的,由于我們在派生類代碼中重寫了虛函數(shù)func1以及func2,所以派生類的虛函數(shù)表中對應位置存儲的是被派生類重寫后的B::func1(void)以及B::func2(void)。所以虛函數(shù)的重寫也被稱為覆蓋,覆蓋指的是虛表中虛函數(shù)的覆蓋,派生類重寫的虛函數(shù)地址覆蓋了基類的虛函數(shù)地址。重寫是語法層面的稱呼,覆蓋是原理層面的稱呼。

4.虛函數(shù)表本質(zhì)是一個存放虛函數(shù)指針的指針數(shù)組,一般情況下這個數(shù)組最后會放一個nullptr。

5.派生類虛表生成的過程如下:首先,將基類中虛表內(nèi)容拷貝一份到派生類之中;而后,如果派生類重寫了某個虛函數(shù),用派生類自身的虛函數(shù)的地址覆蓋虛函數(shù)表中基類虛函數(shù)的地址;最后,派生類將派生類之中新增的虛函數(shù)增加到虛函數(shù)表的最后。

6.虛函數(shù)表中只存放繼承或派生類自身定義的虛函數(shù),非虛函數(shù)不會被存放。

虛函數(shù)存在哪里?虛表存在哪里?

  • 與普通的函數(shù)一樣,虛函數(shù)存放于代碼段中,只是它的指針被存儲到了虛表之中。虛表并不存放于對象之中,對象中存儲的是虛表指針。
  • 虛表存放于代碼段之中。(個人實力原因暫時無法證明)

多態(tài)的原理

分析了這么多,多態(tài)的原理究竟是什么?這里就需要用到我們之前屢試不爽的方法——通過基類指針/引用調(diào)用函數(shù):

void Func(A* tmp) {
	tmp->func1();
}

int main() {
	A a;
	B b;
	Func(&a);
	Func(&b);
	return 0;
}

通過對以上代碼進行反匯編,我們可以看到在使用基類引用調(diào)用虛函數(shù)時的操作如下: 

mov    eax,dword ptr [tmp]

tmp中存儲的是基類對象的指針,將tmp中的值移動到eax寄存器中。

mov    edx,dword ptr [eax]

 將eax的值指向的內(nèi)容存儲到edx中,即將基類對象前4個字節(jié)(虛表指針)移動到edx中。

mov    ecx,dword ptr [tmp]

取tmp指針存儲的值到ecx寄存器中 

mov    eax,dword ptr [edx]

取edx寄存器中的值指向的內(nèi)容,相當于取了虛表的前四個字節(jié),即虛表中存儲的第一個虛函數(shù)的地址。如果我們此處調(diào)用的是fun2或者func3,則會在edx基礎(chǔ)上偏移一定字節(jié)。 

call    eax

調(diào)用eax寄存器中存儲的指針指向的函數(shù)。

動態(tài)綁定與靜態(tài)綁定

  • 靜態(tài)綁定:

靜態(tài)綁定又被稱為前期綁定早綁定,在程序編譯期間便已經(jīng)確定了程序的行為,也被稱為靜態(tài)多態(tài)。

  • 動態(tài)綁定:

動態(tài)綁定又被稱為后期綁定晚綁定,在程序運行期間,根據(jù)具體拿到的類型來確定程序的行為,調(diào)用具體的函數(shù),也稱為動態(tài)多態(tài)。 

多繼承下的虛函數(shù)表

我們上文所主要講解使用的是單繼承情況下的虛函數(shù)表。這里我們就簡單研究下如果在多繼承體系下,虛函數(shù)表又會有怎樣的不同。

是將多個基類的虛函數(shù)表直接合為一個,還是分別存儲與派生類對象中分屬于不同基類的存儲空間里。

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << " 虛表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d個虛函數(shù)地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

可能是之前對VS的編譯器做過太多設(shè)置的修改,這個程序在Debug模式下運行會發(fā)生崩潰,崩潰原因是虛函數(shù)表末尾并沒有按照我們預期的那樣綴上一個nullptr。

但是在release模式下可以正常地運行,并得到一下結(jié)果: 

通過觀察圖示結(jié)果我們可以很直觀的看到,多繼承情況下,派生類重寫的虛函數(shù)會放在第二個繼承基類部分的虛函數(shù)表中,而未重寫虛函數(shù)則會在第一個繼承基類部分的虛函數(shù)表中。

同時,在多個基類中有相同的函數(shù)(函數(shù)名,返回值,參數(shù)列表)時,默認會重寫先繼承的基類中的對應函數(shù)。

總結(jié)

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • 淺析C/C++中動態(tài)鏈接庫的創(chuàng)建和調(diào)用

    淺析C/C++中動態(tài)鏈接庫的創(chuàng)建和調(diào)用

    下面小編就為大家?guī)硪黄獪\析C/C++中動態(tài)鏈接庫的創(chuàng)建和調(diào)用。小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考,一起跟隨小編過來看看吧
    2016-05-05
  • C++分析講解類的靜態(tài)成員函數(shù)如何使用

    C++分析講解類的靜態(tài)成員函數(shù)如何使用

    與靜態(tài)數(shù)據(jù)成員不同,靜態(tài)成員函數(shù)的作用不是為了對象之間的溝通,而是為了能處理靜態(tài)數(shù)據(jù)成員,靜態(tài)成員函數(shù)沒有this指針。既然它沒有指向某一對象,也就無法對一個對象中的非靜態(tài)成員進行默認訪問
    2022-04-04
  • Qt顯示QImage圖像在label上,并保持自適應大小問題

    Qt顯示QImage圖像在label上,并保持自適應大小問題

    這篇文章主要介紹了Qt顯示QImage圖像在label上,并保持自適應大小問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-11-11
  • 詳解C++11中的類型推斷

    詳解C++11中的類型推斷

    C++11中為了更好的支持泛型編程,提供了?auto和decltype兩個關(guān)鍵詞,目的就是提供編譯階段的自動類型推導,這篇文章主要介紹了C++11中的類型推斷,需要的朋友可以參考下
    2023-01-01
  • C語言生成隨機數(shù)以及設(shè)置隨機數(shù)范圍的方法(超詳細)

    C語言生成隨機數(shù)以及設(shè)置隨機數(shù)范圍的方法(超詳細)

    文章介紹了C語言中生成隨機數(shù)的方法,包括使用`rand`和`srand`函數(shù),以及如何通過`time`函數(shù)設(shè)置隨機種子以確保每次運行程序生成的隨機數(shù)序列不同,此外,還詳細講解了如何根據(jù)需要設(shè)置隨機數(shù)的范圍,需要的朋友可以參考下
    2025-02-02
  • c++中用TINYXML解析XML文件

    c++中用TINYXML解析XML文件

    這篇文章主要介紹了c++中如何用TINYXML解析XML文件,文中案例非常詳細,幫助大家更好的了解和學習,感興趣的朋友可以了解下
    2020-06-06
  • 利用C++實現(xiàn)一個線程安全的map

    利用C++實現(xiàn)一個線程安全的map

    這篇文章主要為大家詳細介紹了如何利用C++實現(xiàn)一個線程安全的map(使用ChatCPT生成),代碼是通過兩輪對話完善的,感興趣的小伙伴可以了解一下
    2023-05-05
  • C/C++實現(xiàn)目錄監(jiān)視器的方法詳解

    C/C++實現(xiàn)目錄監(jiān)視器的方法詳解

    這篇文章主要介紹了C/C++ 實現(xiàn)目錄監(jiān)視器的方法,然后網(wǎng)上查到的基本就有三種方法,使用FindFirstChangeNotification等系列函數(shù),使用ReadDirectoryChangesW函數(shù)和使用change journals,本文使用了第二種方式來實現(xiàn)一個目錄監(jiān)視,需要的朋友可以參考下
    2024-04-04
  • C++ Invalidaterect()函數(shù)作用案例詳解

    C++ Invalidaterect()函數(shù)作用案例詳解

    這篇文章主要介紹了C++ Invalidaterect()函數(shù)作用案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下
    2021-08-08
  • c++隱式類型轉(zhuǎn)換示例分享

    c++隱式類型轉(zhuǎn)換示例分享

    這篇文章主要介紹了c++隱式類型轉(zhuǎn)換的二個示例,需要的朋友可以參考下
    2014-03-03

最新評論

广河县| 雅安市| 临洮县| 太和县| 关岭| 綦江县| 武平县| 司法| 广宗县| 太原市| 广水市| 武功县| 广宗县| 南召县| 沁水县| 文水县| 贵溪市| 新余市| 武汉市| 远安县| 西乡县| 泰宁县| 玉龙| 阿图什市| 罗定市| 英山县| 黑龙江省| 确山县| 龙泉市| 乡宁县| 个旧市| 霞浦县| 固阳县| 榕江县| 沁水县| 沙坪坝区| 扶沟县| 江安县| 浦江县| 岳池县| 安平县|