1. 程式人生 > 其它 >基礎知識點 | 1013_多型,繼承等細節知識點及幾個錯題總結

基礎知識點 | 1013_多型,繼承等細節知識點及幾個錯題總結

1.多型之虛擬函式


在面向物件中,多型指的是使用相同的函式名來訪問函式不同的實現方法,即“一種介面,多種方法”,用相同的形式訪問一組通用的運算,

C++語言支援編譯時多型執行時多型

  • 編譯時多型指的是系統在編譯時能確定呼叫哪個函式,它具有執行速度快的優點,運算子過載函式過載就是編譯時多型。
  • 執行時多型指的是系統在執行時動態決定呼叫哪個函式,它為系統提供了靈活和高度問題抽象的優點,通過繼承虛擬函式實現執行時多型。
    • 執行時多型的基礎是基類指標基類指標可以指向任何派生類物件。在基類中的某成員函式被宣告為虛擬函式後,在之後的派生類中可以被重新定義。但在定義時,其函式原型,包括返回型別、函式名、引數個數和引數型別的順序,都必須與基類中的原型完全相同。只要在基類中顯式聲明瞭虛擬函式,那麼在之後的派生類中就不需要用關鍵字virtual
      來顯式聲明瞭,可以略去,因為系統會根據其是否和基類中虛擬函式原型完全相同來判斷是不是虛擬函式。所以,派生類中的虛擬函式如果不顯式宣告也還是虛擬函式。

  1. 因為虛擬函式使用的基礎是賦值相容,賦值相容是指在需要用到基類物件的任何地方都可以用公有派生類的物件來代替,而賦值相容成立的條件是派生類從基類public繼承而來。所以,當使用虛擬函式時,派生類必須是基類public派生的。
  2. 定義虛擬函式時,不一定要在最高層的類中,而是看在需要動態多型性的幾個層次中的最高層類中宣告虛擬函式。
  3. 只有通過基類指標來訪問虛擬函式才能實現執行時多型的特性。
  4. 一個虛擬函式無論被公有繼承了多少次,它仍然是虛擬函式。
  5. 虛擬函式必須是所在類的成員函式,而不能是友元函式,也不能是靜態成員函式。因為虛擬函式呼叫要靠特定的物件類決定該啟用哪一個函式。
  6. 內聯(inline)函式不能是虛擬函式,因為行內函數是不能在執行中動態確定其位置的, 即使虛擬函式在類內部定義,編譯時將其看作非內聯。
  7. 建構函式不能是虛擬函式,但解構函式可以是虛擬函式。

如果呼叫的函式是實函式,則看指標的定義;
如果呼叫的函式是虛擬函式,則看指標的指向(賦值)


// 基類
class Base{
    int x;
public:
    Base(int b): x(b) {}
    // 基類中的display函式是
    virtual void display(){
        cout << x;
    };
};

// 派生類
class Derived: public Base{
    int y;
public:
    Derived(int d): Base(d), y(d) {} 
    void display(){
        cout << y;
    }
};

// main函式
int main()
{
    Base b(1);		// 基類物件
    Derived d(2);	// 派生類物件
    Base *p = & d;	// 把派生類物件的地址賦值給基類物件的指標變數
    
    b.display();	// 呼叫基類物件中的虛擬函式
    d.display();	// 呼叫派生類物件中的虛擬函式
    p->display();	// 通過指向基類物件的的指標,訪問派生類中的重寫的虛擬函式
    
    return 0;
}
  • 通過指向基類物件的指標,只能訪問派生類中的基類成員,而不能訪問派生類增加的成員。

// 父類A
class A
{   
public: 
	void virtual f() {       
        cout << "A" << " ";
    }
};
// 公有繼承A類的子類B
class B : public A
{   
public:
	void virtual f(){       
        cout << "B" << " ";
    }
};
int main(){
    A *pa = new A();	// 基類指標指向基類物件
    pa->f();
    B *pb=(B *)pa;	// 派生類指標指向基類物件
    pb->f();    
    delete pa, pb;
    
    pa=new B();	//基類指標指向派生類物件
    pa->f();
    pb=(B *)pa;	// 派生類指標指向派生類物件
    pb->f();
    return 0;
}
  • 無論指標指向值的資料型別如何改變,物件的內容都不變

// 基類
class Base {
public:
    Base() {
        echo();
    }
    virtual void echo() {
        printf("Base");
    }
};
// 公有繼承基類的派生類
class Derived:public Base {
public:
    Derived() {
        echo();
    }
    virtual void echo() {
        printf("Derived");
    }
};
int main() {
    Base* base = new Derived();
    base->echo();
    return 0;
}
  • 永遠不要在建構函式中呼叫虛擬函式!!!
  • Base* base = new Derived(); 這句話分為兩部分來看:
    • 首先執行左半部分,新建一個基類指標。由於基類建構函式中呼叫虛擬函式,因此輸出 Base
    • 緊接著執行右半部分,新建一個派生類物件。此時呼叫派生類建構函式,輸出 Derived
  • base->echo(); 考察虛擬函式的使用了。此時基類指標指向派生類物件,輸出 Derived


2.繼承之父子類


  1. 子類建構函式呼叫父類建構函式用super
  2. 子類重寫父類方法後,若想呼叫父類中被重寫的方法,用super
  3. 未被重寫的方法可以直接呼叫。


3.__try 和 __finally


int GetResult(int a){
int b = 0;
__try{
    if ( a != 0 ){
        b++;
    }
    // 執行到此處時程式碼返回
    return b;
}
// finally塊中的內容一定會執行
__finally{
    --b;
}
return b;
}

  • __ try{} 塊中無論執行什麼程式碼,即使是 return 和 觸發異常,在程式推出前都會執行 __ finally{} 中的程式碼,這樣設計的目的是便於資源回收。


4.printf 從右向左編譯,從左向右輸出



5.預設引數


定義:即預設引數。

主要規則:呼叫時你只能從最後一個引數開始進行省略,換句話說,如果你要省略一個引數,你必須省略它後面所有的引數,即:帶預設值的引數必須放在引數表的最後面。

最重要的一點:預設引數是靜態繫結的!


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;
}


6.一個常做常錯的題


unsigned int value = 1024;	// value 的二進位制是 1後接10個零
bool condition = *((bool *)(&value));	// 取低8位作為condition,所以此刻 condition 為0
if (condition) {
    value += 1;
    condition = *((bool *)(&value));
}
if (condition) {
    value += 1;
    condition = *((bool *)(&value));
}