C++ 雜記

-风间琉璃-發表於2024-12-08

虛繼承

父類中只有一份子類的資料, 父類資料並不直接放在虛繼承的子類中,子類中會定義一個虛基表指標,指向父類.虛基表中存在偏移量,這個量就是表地址到父類地址的距離.位置大概是:

圖片1

虛繼承只有在多繼承關係(隔代繼承)的時候才有比較有效果, 單繼承虛基類效果不顯著. 虛基表中的資料屬於類的公共部分.虛基類和普通類中如果含有一個同名函式(變數), 優先顯示普通類的函式(變數)

注意帶有虛繼承的建構函式與解構函式的呼叫順序:

  1. 先呼叫虛基類的建構函式. 並按照它們被繼承的順序構造.(注:即使不是直接虛繼承, 是父類虛繼承,也是先呼叫父類虛基類的建構函式)

  2. 再呼叫非虛基類的建構函式,並按照它們被繼承的順序構造.

  3. 呼叫類中成員的建構函式,並按照宣告呼叫.

  4. 再呼叫類本身的建構函式

  5. 析構的時候先呼叫派生類本身的解構函式, 再呼叫物件成員的解構函式, 最後呼叫基類解構函式. 基類解構函式先呼叫普通類解構函式, 再呼叫虛基類建構函式

來看這個程式:

#include<iostream>
using namespace std;
class A{
public:
	A(int a):x(a){ cout<<"A constructor..."<<x<<endl;	}
	int f(){return ++x;}
	~A(){cout<<"destructor A..."<<endl;}
private:
	int x;
};
class B:public virtual A{
private:
	int y;
	A Aobj;
public:
	B(int a,int b,int c):A(a),y(c),Aobj(c){ cout<<"B constructor..."<<y<<endl;}
	int f(){
		A::f();
		Aobj.f();
		return ++y;
	}
	void display(){	cout<<A::f()<<"\t"<<Aobj.f()<<"\t"<<f()<<endl;	}
	~B(){cout<<"destructor B..."<<endl;}    
};
class C:public B{
public:
	C(int a,int b,int c):B(a,b,c),A(0){ cout<<"C constructor..."<<endl;}
};
class D:public C,public virtual A{
public:
	D(int a,int b,int c):C(a,b,c),A(c){ cout<<"D constructor..."<<endl;}
	~D(){cout<<"destructor D...."<<endl;}
};
int main()
{
	D d(7,8,9);
	d.f();
	d.display();
return 0;
}

output:

A constructor...9
A constructor...9
B constructor...9
C constructor...
D constructor...
11      11      11
destructor D....
destructor B...
destructor A...
destructor A...
  1. 先呼叫虛基類的建構函式, a(9)
  2. 注意第二個建構函式的呼叫是因為虛基類的建構函式只呼叫一次, 所以開始呼叫 Aobj 類成員變數的建構函式 Aobj(9)
  3. 類成員變數建構函式全部呼叫完後開始呼叫類建構函式 B(a,b,c)
  4. 父類建構函式呼叫完後依次呼叫子類建構函式 C(), D()
  5. 虛基類和普通類中如果含有一個同名函式(變數), 優先顯示普通類的函式(變數), 故呼叫 B::func() , 虛基類 a 中的x = 10, Aobj.x = 10, B.y = 10 , 然後呼叫 B.display(), 呼叫虛基類 A.f() , 返回 ++x = 11 , 再呼叫 AObj.f() , 返回 ++Aobj.x = 11, 再呼叫B.f() , 返回 ++ y = 11
  6. 然後開始析構, 先析構派生類建構函式, 再呼叫父類解構函式; 先呼叫普通類解構函式, 再呼叫虛基類建構函式;故先析構派生類 D, 再析構 C(無輸出), 析構 B的時候,先呼叫 B的解構函式, 再呼叫成員 Aobj 的解構函式, 最後呼叫虛基類 A 的解構函式

虛擬函式與多型

在父類含有虛擬函式,子類重寫父類虛擬函式的情況下, 使用父類指標或者父類引用繫結子類並呼叫虛擬函式, 即為多型.

虛擬函式表

​ 類成員函式並不佔用類物件的記憶體空間. 當物件引入虛擬函式之後,類內會插入一個虛擬函式指標,該指標佔用物件一個指標大小的記憶體空間(4位或者8位)

​ 當存在至少一個虛擬函式時, 編譯期間會為類生成一個虛擬函式表.

​ 當帶有虛擬函式的類生成的時候, 編譯器會為類的建構函式中增加一個為對虛擬函式指標賦值的操作

  1. 把子類直接複製給子類, 只複製子類中父類的部分,子類會重新建立虛擬函式表,所以呼叫的仍為最初的虛擬函式,使用的不是子類的虛擬函式表.
  2. base & b = a;或者 base* b = &a; a 為派生類, 引用賦值實際呼叫的還是派生類, 但是它會把派生類的成員隱藏起來無法呼叫, 但是可以正常呼叫父類成員, 同時如果父類中存在虛擬函式, 子類重寫虛擬函式, 那麼透過 虛擬函式指標呼叫子類重寫的虛擬函式(因為虛擬函式表還是沒改變).如果是虛繼承的父類, 也會直接把虛基類的部分直接繼承, 同理虛擬函式指標也會繼承, 所以會呼叫子類重寫的虛擬函式.
  3. 在父類中再呼叫父類中的虛擬函式, 即使被子類重寫,也會加上base:: 作用域而呼叫父類虛擬函式

c++ 函式過載細節:

很久以前(八十年代),沒有辦法區分++和--運算子的字首與字尾呼叫。這個問題遭到程式設計師的報怨,於是C++語言得到了擴充套件,允許過載increment 和 decrement運算子的兩種形式。

  然而有一個句法上的問題,過載函式間的區別決定於它們的引數型別上的差異,但是不論是increment或decrement的字首還是字尾都只有一個引數。為了解決這個語言問題,C++規定字尾形式有一個int型別引數,當函式被呼叫時,編譯器傳遞一個0做為int引數的值給該函式:

// 過載前置自增運算子
    Counter& operator++() {
        
    }
 
    // 過載後置自增運算子
    Counter& operator++(int) { // 要加入 int 的佔位引數
        
    }

使用引用就可以實現鏈式法則 , 即a++++, 但是注意這裡並無法對前置自增的返回值也進行處理