【C++】多態深入分析

目錄

一,多態的原理

1,虛函數表與虛函數表指針

2,原理調用

3,動態綁定與靜態綁定

二,抽象類

三,單繼承和多繼承關系的虛函數表

1,單繼承中的虛函數表

2,多繼承中的虛函數表

3,菱形繼承、菱形虛擬繼承

四,繼承和多態常見的經典題型

1,概念邏輯考察

2,問答題


一,多態的原理

1,虛函數表與虛函數表指針

? ? ? ? 虛函數表:存放虛函數指針的表,簡稱虛表。? ? ? ? ? ? 虛函數表指針:指向虛函數表的指針。

????????一個含有虛函數的類中,至少都有一個虛函數表指針__vfptr,因為虛函數的地址要被放到虛函數表中,而虛函數表本質是一個存虛函數指針的指針數組,一般情況這個數組最后面放了一個nullptr。注意:類似于友元函數、靜態函數不能為虛函數,因為它們都不屬于類,即都不是類的成員函數,無法放到虛函數表中。

? ? ? ? 派生類的虛表生成一共有以下步驟:a.先將基類中的虛表內容拷貝一份到派生類虛表中 b.如果派生類重寫了基類中某個虛函數,用派生類自己的虛函數覆蓋虛表中基類的虛函數 c.派生類自己新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的最后(注意:在vs監視窗口下可能在虛表中看不到新增的虛函數,但是通過內存窗口下可以觀察到)。

#include <iostream>
using namespace std;
class Base
{
public:
?? ?virtual void Fun1() {? ?//虛函數,地址存放到虛表中
?? ??? ?cout << "Base::Fun1()" << endl;
?? ?}
?? ?virtual void Fun2() {? //虛函數,地址存放到虛表中
?? ??? ?cout << "Base::Fun2()" << endl;
?? ?}
?? ?void Fun3() {? //不是虛函數,地址沒有存放到虛表中
?? ??? ?cout << "Base::Fun3()" << endl;
?? ?}
private:
?? ?int _a = 1;
?? ?char _ch = 'a';
};
class Derive : public Base
{
public:
?? ?virtual void Func1() {? //虛函數,與子類后成重寫,將子類的Func1覆蓋
?? ??? ?cout << "Derive::Func1()" << endl;
?? ?}
private:
?? ?int _d = 2;
};
int main()
{
?? ?Base bb;
?? ?Derive dd;
?? ?return 0;
}

原理圖如下:

? ? ? ? 我們通過以上知識來計算下基類與派生類的存儲大小。觀察以下代碼:

#include <iostream>
using namespace std;
class Base
{
public:
?? ?virtual void Fun1() {
?? ??? ?cout << "Base::Fun1()" << endl;
?? ?}
?? ?virtual void Fun2() {
?? ??? ?cout << "Base::Fun2()" << endl;
?? ?}
?? ?void Fun3() {
?? ??? ?cout << "Base::Fun3()" << endl;
?? ?}
private:
?? ?int _a = 1;
?? ?char _ch = 'a';
};
class Derive : public Base
{
public:
?? ?virtual void Func1()
?? ?{
?? ??? ?cout << "Derive::Func1()" << endl;
?? ?}
private:
?? ?int _d = 2;
};
int main()
{
?? ?//因為有了虛函數,所以這里多了虛函數表指針。虛表指針也存儲在類中
?? ?cout << sizeof(Base) << endl; //輸出12

?? ?cout << sizeof(Derive) << endl; ?//輸出16
?? ?return 0;
}

? ? ? ? 這里說明一下,虛表指針存放的位置與平臺有關,有些平臺可能會放到對象的最后面,有些平臺可能會放到對象的最前面。這里的測試是放在對象的前面且是32位機器,也就是說虛表指針占用4字節空間,然后這里再根據空間對齊規則,計算出總空間大小。

? ? ? ? 下面,我們研究下虛表存放的區域(注意:這里研究的不是虛表指針存放的區域,即研究虛表指針指向的地址,不是虛表指針本身的地址,這里不要搞錯)。這里可以使用對比法來觀察。具體做法是先輸出各大區域的代表地址,然后輸出虛表地址(即虛表指針),通過對比觀察與哪個區域代表地址相差最小的就是存儲在哪塊區域,因為各大區域的地址相差非常大。我們先創造以下類

class Base
{
public:
?? ?virtual void Fun1() {
?? ??? ?cout << "Base::Fun1()" << endl;
?? ?}
?? ?virtual void Fun2() {
?? ??? ?cout << "Base::Fun2()" << endl;
?? ?}
?? ?void Fun3() {
?? ??? ?cout << "Base::Fun3()" << endl;
?? ?}
private:
?? ?int _a = 1;
?? ?char _ch = 'a';
};

內部結構圖如下:?

?#include <iostream>
using namespace std;
class Base
{
public:
?? ?virtual void Fun1() {
?? ??? ?cout << "Base::Fun1()" << endl;
?? ?}
?? ?virtual void Fun2() {
?? ??? ?cout << "Base::Fun2()" << endl;
?? ?}
?? ?void Fun3() {
?? ??? ?cout << "Base::Fun3()" << endl;
?? ?}
private:
?? ?int _a = 1;
?? ?char _ch = 'a';
};
int main()
{
?? ?Base bb;
?? ?//創造熟為認知的四大區域的代表
?? ?int a = 0;
?? ?int* b = new int;
?? ?static int c = 1;
?? ?const char* d = "a";
?? ?fprintf(stdout, "棧區: %p\n", &a);
?? ?fprintf(stdout, "堆區: %p\n", b);
?? ?fprintf(stdout, "靜態區(數據段): %p\n", &c);
?? ?fprintf(stdout, "常量區(代碼段): %p\n", d);
?? ?//因為在32位下,內存中一個地址占用四個字節,所以這里解引用需解出bb對象開頭的前四字節內容。這里可轉換成int*,一次性可解引用出4個字節
?? ?Base* pb = &bb;
?? ?fprintf(stdout, "虛表存放區域: %p\n", *(int*)pb); ?//地址與常量區代表地址最相近,即虛表存放在常量區中
?? ?return 0;
}

2,原理調用

????????虛函數的重寫在某種意義上來講也叫做覆蓋。我們通常所說的重寫是語法層上的概念,而覆蓋是原理層上的概念,這也就是父類虛函數覆蓋子類虛函數。

? ? ? ? 這里要說明一下,虛表是在編譯的時候就已經生成,但虛表指針是在構造函數中初始化,通常在初始化列表的最開始階段。若平臺將虛表指針放在最后面也不排除在初始化列表的最后才初始化。但一般情況下平臺都將虛表指針放在最前面,這里可通過監視窗口可觀察到。

? ? ? ? 多態以后的函數調用,不是在編譯時確定的,是運行起來以后到對象的中取找的。不滿足多態的函數調用時編譯時是確認好的。

? ? ? ? 多態調用的本質是在運行時去虛函數表中找函數的地址進行調用,因此調用前必須先確定虛表以及虛表指針,虛表指針又是在構造函數階段才生成,多態調用的機制也是在構造函數之后才生效,也就是說多態的調用是在構造函數之后才調用,所以指向父類調用的是父類的虛函數,指向子類調用的是子類的虛函數。普通調用的本質是系統直接通過調用者類型確定函數地址,也就是在某一具體空間作用域中去查找。對于多態調用而言,即便實例化出多個對象,它們的虛表指針以及存放的虛函數地址都是一樣的,如下:

測試一:

#include <iostream>
using namespace std;
class A
{
public:
? ? virtual void func(int val = 1)
? ? { std::cout << "A->" << val << std::endl; }
? ? virtual void test()?
? ? { func(); }
? ? A()
? ? {
? ? ? ? func();?
? ? }
};

class B : public A
{
public:
? ? B()
? ? {
? ? ? ? func();
? ? }
? ? virtual void func(int val = 0)
? ? { std::cout << "B->" << val << std::endl; }
};

int main(int argc, char* argv[])
{
? ? A* p = new B;
? ? p->test();
? ? //以次輸出A->1 ?B->0 ?B->1
? ? return 0;
}

? ? ? ? 首先,當new B時先調用父類A的構造函數執行func時,由于此時還處于對象構造階段,多態機制還沒有生效,所以,此時執行的func函數為父類的func函數,輸出A->1。構造完父類后執行子類構造函數,又要調用func,同理執行子類的func,輸出B->0。當構造函數結束后再次調用,多態機制生成,后面就正常調用。

測試二:

#include <iostream>
using namespace std;
class A
{
public:
? ? A()?
? ? : m_iVal(0) {?
? ? ? ? test();
? ? }
? ? virtual void func() {
? ? ? ? std::cout << m_iVal << " ";
? ? }
? ? void test() {
? ? ? ? func();
? ? }
public:
? ? int m_iVal;
};
class B : public A
{
public:
? ? B() {?
? ? ? ? test();
? ? }
? ? virtual void func()
? ? {
? ? ? ? ++m_iVal;
? ? ? ? std::cout << m_iVal << " ";
? ? }
};
int main()
{
? ? //以下輸出0 0 1
? ? A a;
? ? B b; //當調用完A的構造后,A中的基表就確定了。
? ? //子類B的虛表是拷貝父類的虛表內容,也就是說子類調用完構造函數之后多態機制就已經生成
? ? return 0;
}

測試三:

#include <iostream>
using namespace std;
class Base
{
public:
?? ?virtual void Fun1() {
?? ??? ?cout << "Base::Fun1()" << endl;
?? ?}
?? ?virtual void Fun2() {
?? ??? ?cout << "Base::Fun2()" << endl;
?? ?}
?? ?void Fun3() {
?? ??? ?cout << "Base::Fun3()" << endl;
?? ?}
private:
?? ?int _a = 1;
?? ?char _ch = 'a';
};
class Derive : public Base
{
public:
?? ?virtual void Func1()
?? ?{
?? ??? ?cout << "Derive::Func1()" << endl;
?? ?}
private:
?? ?int _d = 2;
};
int main()
{
?? ?Base bb1;
?? ?Base bb2;
?? ?Base bb3;

?? ?Derive dd1;
?? ?Derive dd2;
?? ?Derive dd3;
?? ?return 0;
}

內部結構圖:

3,動態綁定與靜態綁定

????????1,靜態綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態多態,比如:函數重載

????????2,動態綁定又稱后期綁定(晚綁定),是在程序運行期間,根據具體拿到的類型并確定程序的具體行為,調用具體的函數,也稱為動態多態。

? ? ? ? 以上兩個概念只需了解即可。


二,抽象類

????????在虛函數的后面寫上 =0 ,則這個函數為純虛函數。包含純虛函數的類叫做抽象類(也叫接口 類),抽象類不能實例化出對象。派生類繼承后也不能實例化出對象,只有重寫純虛函數,派生 類才能實例化出對象。也就是說純虛函數規范了派生類必須重寫。

#include <iostream>
using namespace std;
class Car //抽象類
{
public:
?? ?virtual void Drive() = 0 { ?//純虛函數
?? ??? ?cout << "Car" << endl;
?? ?};
};
class Benz : public Car
{
public:
?? ?virtual void Drive() //重寫抽象類的純虛函數,可以被實例化
?? ?{
?? ??? ?cout << "Benz" << endl;
?? ?}
};
class BMW : public Car
{?? ?};
int main()
{
?? ?//重寫純虛函數,實例化成功
?? ?Benz a;
?? ?a.Drive();
?? ?//沒有重寫純虛函數,實例化報錯
?? ?BMW b;
?? ?return 0;
}

? ? ? ? 最后說明一下接口繼承和實現繼承。普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實現。虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是為了重寫,達成多態,繼承的是接口。所以如果不實現多態,不要把函數定義成虛函數。


三,單繼承和多繼承關系的虛函數表

1,單繼承中的虛函數表

? ? ? ? 之前說過,在vs監視窗口下可能在虛表中看不到新增的虛函數,這里我們實踐下,如下:

class Base?
{
public:
?? ?virtual void func1() {
?? ??? ?cout << "Base::func1" << endl;
?? ?}
?? ?virtual void func2() {?
?? ??? ?cout << "Base::func2" << endl;?
?? ?}
private:
?? ?int a;
};
class Derive : public Base?
{
public:
?? ?virtual void func1() {?
?? ??? ?cout << "Derive::func1" << endl;?
?? ?}
?? ?virtual void func3() {
?? ??? ?cout << "Derive::func3" << endl;
?? ?}
?? ?virtual void func4() {
?? ??? ?cout << "Derive::func4" << endl;
?? ?}
private:
?? ?int b;
};

????????觀察上圖中的監視窗口中我們發現看不見func3和func4。這里是編譯器的監視窗口故意隱藏了這兩個函數,也可以認為是它的一個小bug。那么我們如何查看d的虛表呢?下面我們使用代碼打印出虛表中的函數。

#include <iostream>
using namespace std;
class Base?
{
public:
?? ?virtual void func1() {
?? ??? ?cout << "Base::func1" << endl;
?? ?}
?? ?virtual void func2() {?
?? ??? ?cout << "Base::func2" << endl;?
?? ?}
private:
?? ?int a;
};
class Derive : public Base?
{
public:
?? ?virtual void func1() {?
?? ??? ?cout << "Derive::func1" << endl;?
?? ?}
?? ?virtual void func3() {
?? ??? ?cout << "Derive::func3" << endl;
?? ?}
?? ?virtual void func4() {
?? ??? ?cout << "Derive::func4" << endl;
?? ?}
private:
?? ?int b;
};
typedef void(*VFPTR) (); //聲明函數指針VFPTR
void PrintVTable(VFPTR vTable[]) {
?? ?for (int i = 0; vTable[i] != nullptr; ++i) { ?//vs下的虛表最后一個存儲單元為nullptr
?? ??? ?cout << "vTable[" << i << "]: " << vTable[i] << endl;
?? ?} ? ?
?? ?cout << endl;
}
int main()
{
?? ?Base b;
?? ?Derive d;
?? ?VFPTR* vTableb = (VFPTR*)(*(int*)&b); ?//用函數二級指針來表示函數指針數組
?? ?PrintVTable(vTableb);
?? ?VFPTR* vTabled = (VFPTR*)(*(int*)&d); ?//與上同理
?? ?PrintVTable(vTabled);
?? ?return 0;
}

? ? ? ? 這里需說明的是這個打印虛表的代碼經常會崩潰,因為編譯器有時對虛表的處理不干凈,虛表最后面沒有放nullptr,導致越界,這是編譯器的問題。我們只需要點目錄欄的-生成-清理解決方案,再編譯就好了。

2,多繼承中的虛函數表

? ? ? ? 在多繼承中,要謹記派生類的虛表內容是將基類中的虛表內容拷貝一份到派生類虛表中。從這里不難發現,在多繼承中,派生類不止有一個虛表,即繼承多少個類就有多少個虛表。當派生類的虛函數往虛表中添加地址時,會往第一個繼承基類虛表中添加。當派生類對象/指針/引用賦值給基類時,這里會將基類以及包含基類虛表的那一部分一并切割。

#include <iostream>
using namespace std;
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個虛函數地址 :0X%x,->", i, vTable[i]);
?? ??? ?VFPTR f = vTable[i];
?? ??? ?f(); //函數指針的調用,調用類中的虛函數
?? ?}
?? ?cout << endl;
}
int main()
{
?? ?Derive d;
?? ?cout << sizeof(Derive) << endl; //輸出20,因為繼承了兩個父類,有兩個虛表

?? ?//這里的p1和p2的值不一樣,因為這里要發生切片,將父類包含特有的子類切出來
?? ?Base1* p1 = &d; ?//切出Base1
?? ?Base2* p2 = &d; ?//切出Base2
?? ?
?? ?VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
?? ?PrintVTable(vTableb1);
?? ?//這里要先跳轉到包含Base2的虛表上,然后找到虛表指針
?? ?VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));?
?? ?PrintVTable(vTableb2);
?? ?return 0;
}

????????觀察下面結構圖可以看出:多繼承派生類的未重寫的虛函數放在第一個繼承基類部分的虛函數表中。

3,菱形繼承、菱形虛擬繼承

? ? ? ??實際中我們不建議設計出菱形繼承及菱形虛擬繼承,一方面太復雜容易出問題,另一方面使用這樣的模型訪問基類成員有一定得性能損耗。所以菱形繼承、菱形虛擬繼承的虛表我們可不做研究,一般我們也不需要研究清楚,因為實際中很少用。我們只需知道,菱形繼承往多繼承方向理解,菱形虛擬繼承就很復雜了,這里有很多問題,不必深思。


四,繼承和多態常見的經典題型

? ? ? ? 繼承與多態這方面有許多坑,由于內部結構多變,通常可設計出一些邏輯上的運算。這里我們來一一觀察這方面的典型問題和經典面試問題。

1,概念邏輯考察

1. 下面哪種面向對象的方法可以讓你變得富有(?A?)
????????A: 繼承 B : 封裝 C : 多態 D : 抽象

2.?(?D?)是面向對象程序設計語言中的一種機制。這種機制實現了方法的定義與具體的對象無關,
而對方法的調用則可以關聯于具體的對象。
????????A : 繼承 B : 模板 C : 對象的自身引用 D : 動態綁定

3. 面向對象設計中的繼承和組合,下面說法錯誤的是?(?C?
????????A:繼承允許我們覆蓋重寫父類的實現細節,父類的實現對于子類是可見的,是一種靜態復
用,也稱為白盒復用
????????B:組合的對象不需要關心各自的實現細節,之間的關系是在運行時候才確定的,是一種動
態復用,也稱為黑盒復用
????????C:優先使用繼承,而不是組合,是面向對象設計的第二原則
????????D:繼承可以使子類能自動繼承父類的接口,但在設計模式中認為這是一種破壞了父類的封
裝性的表現

4. 以下關于純虛函數的說法, 正確的是(?A?)
????????A:聲明純虛函數的類不能實例化對象 ????????B:聲明純虛函數的類是虛基類
????????C:子類必須實現基類的純虛函數 ????????D:純虛函數必須是空函數

5. 關于虛函數的描述正確的是(?B?)
????????A:派生類的虛函數與基類的虛函數具有不同的參數個數和類型? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? B:內聯函數不能是虛函數
????????C:派生類必須重新定義基類的虛函數? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? D:虛函數可以是一個static型的函數

6. 關于虛表說法正確的是( D )? ? ? ? ? ???
????????A:一個類只能有一張虛表
????????B:基類中有虛函數,如果子類中沒有重寫基類的虛函數,此時子類與基類共用同一張虛表
????????C:虛表是在運行期間動態生成的
????????D:一個類的不同對象共享該類的虛表

7. 假設A類中有虛函數,B繼承自A,B重寫A中的虛函數,也沒有定義任何虛函數,則( D
????????A:A類對象的前4個字節存儲虛表地址,B類對象前4個字節不是虛表地址
????????B:A類對象和B類對象前4個字節存儲的都是虛基表的地址
????????C:A類對象和B類對象前4個字節存儲的虛表地址相同
????????D:A類和B類虛表中虛函數個數相同,但A類和B類使用的不是同一張虛表

8. 下面程序輸出結果是什么? ( A )? ? ? ? ?

#include <iostream>
using namespace std;
class A {
public:
?? ?A(const char* s) { cout << s << endl; }
?? ?~A() {}
};
class B :virtual public A
{
public:
?? ?B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
?? ?C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
?? ?D(const char* s1, const char* s2, const char* s3, const char* s4) :B(s1, s2), C(s1, s3), A(s1)
?? ?{
?? ??? ?cout << s4 << endl;
?? ?}
};
int main() {
?? ?D* p = new D("class A", "class B", "class C", "class D");
?? ?delete p;
?? ?return 0;
}? ? ?
//菱形虛擬繼承由于只有一個A,所以當開始調用D的構造函數時,會首先調用A的析構函數,在B、C中不會調用A的構造函數。

????????A:class A class B class C class D ????????B:class D class B class C class A
????????C:class D class C class B class A ????????D:class A class C class B class D

9. 多繼承中指針偏移問題?下面說法正確的是( C )? ? ?

class Base1 { public: ?int _b1; };
class Base2 { public: ?int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main() {
?? ?Derive d;
?? ?Base1* p1 = &d;
?? ?Base2* p2 = &d;
?? ?Derive* p3 = &d;
?? ?return 0;
}

????????A:p1 == p2 == p3 ????????B:p1 < p2 < p3 ????????C:p1 == p3 != p2 ????????D:p1 != p2 != p3

10. 以下程序輸出結果是什么( B

class A
{
public:
?? ?virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
?? ?virtual void test() { func(); }
};

class B : public A
{
public:
?? ?void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};

int main(int argc, char* argv[])
{
?? ?B* p = new B;
?? ?p->test();
?? ?return 0;
}

????????A: A->0 ????????B: B->1 ????????C: A->1 ????????D: B->0 ????????E: 編譯出錯 ????????F: 以上都不正確

2,問答題

1. 什么是多態?答:多態分為靜態多態和動態多態,靜態多態是編譯器在編譯時就能確定函數調用的是哪個實現,如:函數重載,而動態多態則是在運行時才能確定,如:派生類重寫虛函數時的調用。

2. 什么是重載、重寫(覆蓋)、重定義(隱藏)?答:參考以上圖片內容

3. 多態的實現原理?答:多態的實現原理主要依賴于虛函數表和虛函數表指針,通過虛函數表指針找到虛函數,在運行時進行動態綁定,以具體確定調用哪個函數。

4. inline函數可以是虛函數嗎?答:可以,不過編譯器就忽略inline屬性,這個函數就不再是
inline,因為inline修飾后,若編譯器看成內聯函數,那么此函數是沒有地址的,無法存入虛表中,而虛函數是要放到虛表中去。

5. 靜態成員可以是虛函數嗎?答:不能,因為類中存儲的數據一切調用都需通過this指針,包括虛函數表。靜態成員函數沒有this指針,使用 類型::成員函數 的調用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。

6. 構造函數可以是虛函數嗎?答:不能,因為對象中的虛函數表指針是在構造函數初始化列表
階段才初始化的。

7. 析構函數可以是虛函數嗎?什么場景下析構函數是虛函數?答:可以,并且最好把基類的析
構函數定義成虛函數。因為這里可能存在內存泄漏問題。當使用基類指針指向派生類地址空間時,由于切割,指針指向的是派生類中基類的地址,所以這里結束時默認調用基類析構函數,不會調用派生類析構函數,這將會導致派生類中部分空間沒有釋放,導致內存泄漏問題。因此,這里需使用虛函數,使其調用派生類的析構函數。

8. 對象訪問普通函數快還是虛函數更快?答:首先如果是普通調用,是一樣快的。如果是多態調用,則普通函數快,因為多態調用構成多態,運行時調用虛函數需要到虛函數表中去查找指定地址才能調用,而普通函數直接調用。

9. 虛函數表是在什么階段生成的,存在哪的?答:虛函數表是在編譯階段就生成的,一般情況
下存在代碼段(常量區)的。

10. C++菱形繼承的問題?虛繼承的原理?答:菱形繼承有二義性和數據冗余的問題。在繼承前加上關鍵字virtual的繼承是虛繼承。虛繼承是一種解決多重繼承中菱形繼承問題的方法(注意這里不要把虛函數表和虛基表搞混了。)

11. 什么是抽象類?抽象類的作用?答:包含純虛函數(即在虛函數的后面寫上 =0 )的類叫做抽象類。作用:抽象類強制重寫了虛函數,另外抽象類體現出了接口繼承關系。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/718724.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/718724.shtml
英文地址,請注明出處:http://en.pswp.cn/news/718724.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

“編碼迷宮中的探險者:探索程序員職業賽道的無限可能“

在這個信息技術飛速發展的時代&#xff0c;程序員的職業賽道就像是一座錯綜復雜的迷宮&#xff0c;它既充滿了挑戰&#xff0c;又蘊藏著無限的機遇。這座迷宮中&#xff0c;有前端的美麗花園&#xff0c;后端的黑暗洞穴&#xff0c;還有數據科學的神秘密室。每一條路徑都有其獨…

內網搭建mysql8.0并搭建主從復制詳細教程!!!

一、安裝mysql 1.1 mysql下載鏈接&#xff1a; https://downloads.mysql.com/archives/community/ 1.2 解壓包并創建相應的數據目錄 tar -xvf mysql-8.2.0-linux-glibc2.28-x86_64.tar.xz -C /usr/local cd /usr/local/ mv mysql-8.2.0-linux-glibc2.28-x86_64/ mysql mkdir…

Python繪圖-9餅圖(上)

餅圖&#xff08;Pie Chart&#xff09;是一種用于表示數據分類和相對大小的可視化圖形。在餅圖中&#xff0c;整個圓形代表數據的總和&#xff0c;而圓形內的各個扇形則代表不同的分類或類別&#xff0c;扇形的面積大小表示該類別在整體中所占的比例。餅圖通常用于展示數據的分…

FW, IPS, IDS

文章目錄 FW (Firewall, 防火墻)IPS (Intrusion Prevention System, 入侵防御系統)IDS (Intrusion Detection System, 入侵檢測系統)IDS vs. FWIPS FW (Firewall, 防火墻) 產品定位&#xff1a; 防火墻的主要作用是進行網絡訪問控制。它充當網絡的門衛&#xff0c;控制進入和離…

《人間值得》讀書筆記

人的一生說短不短&#xff0c;說長不長。蕓蕓眾生&#xff0c;為了生活努力的掙扎&#xff0c;太少的人能衣食無憂&#xff0c;所以我們每天為了碎銀幾兩&#xff0c;為了生活奔波。 《人間值得》的主人公是一個90歲的老奶奶&#xff0c;她的生活經歷很豐富&#xff0c;她的人…

ObjectProvider學習

簡介 ObjectProvider 是 Spring Framework 5.0 之后引入的一個新接口&#xff0c;它提供了一個靈活的方式來訪問由 Spring 容器管理的 Bean。ObjectProvider 提供了一種更加類型安全的方式來查找和注入依賴項&#xff0c;同時還支持 Null 值的處理以及延遲初始化。 ObjectProv…

Window部署Jaeger

參考&#xff1a;windows安裝使用jaeger鏈路追蹤_windows安裝jaeger-CSDN博客 下載&#xff1a;Releases jaegertracing/jaeger GitHub Jaeger – Download Jaeger 目錄 1、安裝nssm 2、安裝運行 elasticsearch 3、安裝運行 3.1部署JaegerAgent 3.2部署JaegerCollec…

【全志D1-H 哪吒開發板】Debian系統安裝調教和點燈指南

全志D1-H開發板【哪吒】使用Deabian系統入門 特別說明&#xff1a; 因為涉及到操作較多&#xff0c;博文可能會導致格式丟失 其中內容&#xff0c;會根據后續使用做優化調整 目錄&#xff1a; 參考資料固件燒錄啟動調教點燈問題 〇、參考資料 官方資料 開發板-D1開發板【…

C++:函數模板整理

函數模板: 找到函數相同的實現思路&#xff0c;區別于函數的參數類型。 使用函數模板使得函數可容納不同類型的參數實現函數功能&#xff0c;而不是當類型不同時便編譯大量類型不同的函數&#xff0c;產生大量重復代碼和內存占用 函數模板格式&#xff1a; template<typ…

[Vulnhub]靶場 Red

kali:192.168.56.104 主機發現 arp-scan -l # arp-scan -l Interface: eth0, type: EN10MB, MAC: 00:0c:29:d2:e0:49, IPv4: 192.168.56.104 Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan) 192.168.56.1 …

ARM64匯編02 - 寄存器與指令基本格式

最近的文章可能會有較多修改&#xff0c;請關注博客哦 異常級別 ARMv8處理器支持4種異常等級&#xff08;Exception Level&#xff0c;EL&#xff09;。 EL0 為非特權模式&#xff0c;用于運行應用程序&#xff0c;其他資源訪問受限&#xff0c;權限不夠。 EL1 為特權模式&…

【王道操作系統】ch1計算機系統概述-06虛擬機

文章目錄 【王道操作系統】ch1計算機系統概述-06虛擬機01傳統計算機02虛擬機的基本概念&#xff08;1&#xff09;第一類虛擬機管理程序&#xff08;2&#xff09; 第二類虛擬機管理程序&#xff08;3&#xff09; 兩類虛擬機管理程序的對比 【王道操作系統】ch1計算機系統概述…

效果炸裂、刷爆各大視頻網站的EMO到底是怎么做到的?

文章鏈接&#xff1a;https://arxiv.org/abs/2402.17485 今天分享的工作是刷爆各大視頻平臺的EMO的背后的工作原理。提出的初衷是著手解決增強發言者的頭部特寫視頻生成中的現實感和表現力的挑戰&#xff0c;重點關注音頻提示與面部動作之間的動態和微妙關系。傳統技術具有局限…

【HarmonyOS】鴻蒙開發之Stage模型-UIAbility的啟動模式——第4.4章

UIAbi lity的啟動模式簡介 一共有四種:singleton,standard,specified,multion。在項目目錄的:src/main/module.json5。默認開啟模式為singleton(單例模式)。如下圖 singleton&#xff08;單實例模式&#xff09;啟動模式 每個UIAbility只存在唯一實例。任務列表中只會存在一…

測試管理進階 | 量力而行:避免成為替罪羊

職場中,我們常常面臨是否幫助他人的抉擇。盡管善良是美德,但過度的好人卡可能會給自己帶來麻煩。本文將探討如何在職場中量力而行,避免成為替罪羊,以及如何保持高效和合理的職責劃分。 我們在工作中常常會遇到一些需要幫助他人的情況,作為團隊的一員,我們希望能夠積極地協…

Linux運維_Bash腳本_編譯安裝libGD-2.3.3

Linux運維_Bash腳本_編譯安裝libGD-2.3.3 Bash (Bourne Again Shell) 是一個解釋器&#xff0c;負責處理 Unix 系統命令行上的命令。它是由 Brian Fox 編寫的免費軟件&#xff0c;并于 1989 年發布的免費軟件&#xff0c;作為 Sh (Bourne Shell) 的替代品。 您可以在 Linux 和…

六、繼承(一)

1 繼承的引入 以往我們想分別實現描述學生、老師的類&#xff0c;可能會這樣子做&#xff1a; class Student {string _name;string _number;int _tel;int id;string _address;int _age; }; class Teacher {string _name;int _level;int _tel;int id;string _address;int _ag…

【歸并排序】 詳細解析 動圖演示 逐圖解析 洛谷P1177【模板】排序 sort【快速排序】

文章目錄 歸并排序1.歸并排序的復雜度分析2.細節解釋3.歸并排序動圖演示3(1) 我們的拆分過程如下↓ 4.code↓ 洛谷P1177【模板】排序數據規模與約定code&#xff08;歸并排序&#xff09;↓code&#xff08;sort排序【快速排序】&#xff09; 完結撒花(&#xffe3;▽&#xff…

閱讀筆記 | REFORMER: THE EFFICIENT TRANSFORMER

閱讀論文&#xff1a; Kitaev, Nikita, ?ukasz Kaiser, and Anselm Levskaya. “Reformer: The efficient transformer.” arXiv preprint arXiv:2001.04451 (2020). 背景與動機 這篇論文發表較早&#xff0c;主要關注Transformer的效率問題。標準的Transformer模型在許多自然…

數據中臺:數字中國戰略關鍵技術實施

這里寫目錄標題 前言為何要建設數據中臺數據中臺建設痛點數據中臺學習資料聚焦前沿&#xff0c;方法論體系更新與時俱進&#xff0c;緊跟時代熱點深入6大行業&#xff0c;提煉實踐精華大咖推薦&#xff0c;數字化轉型必備案頭書 前言 在數字中國這一國家戰略的牽引下&#xff0…