1. 程式人生 > >C++類中的一些細節(過載、重寫、覆蓋、隱藏,建構函式、解構函式、拷貝建構函式、賦值函式在繼承時的一些問題)

C++類中的一些細節(過載、重寫、覆蓋、隱藏,建構函式、解構函式、拷貝建構函式、賦值函式在繼承時的一些問題)

1 函式的過載、重寫(重定義)、函式覆蓋及隱藏

其實函式過載與函式重寫、函式覆蓋和函式隱藏不是一個層面上的概念。前者是同一個類內,或者同一個函式作用域內,同名不同引數列表的函式之間的關係。而後三者是基類和派生類函式不同情況下的關係。

1.1 函式過載

正如上文說的函式的過載是指類內部,同名不同引數列表函式之間的關係。如下:

void show();
void show(int);
void show(int , double);

以上是多個同名引數列表不同的函式,這種情況就是函式過載。不同的函式返回值不作為判斷函式過載的依據,比如如下兩個函式會被判定為函式重定義。

void
show(); int show();

編譯器錯誤:“錯誤(活動) E0311 無法過載僅按返回型別區分的函式”。

不過函式過載要注意的是有預設引數和型別的隱式轉換造成的問題,這裡需要了解一下編譯器是如何選擇使用那個版本的函式的。其大致過程是

  • 第一步,建立候選函式列表。其中包含與被呼叫函式的名稱相同的函式
  • 第二步,使用候選函式列表建立可行函式列表,這些都是引數數目正確的函式,為此有一個隱式轉換序列,其中包括實引數型別與相應的形參型別完全匹配的情況。例如,使用float引數的函式呼叫可以將該引數轉換為double,從而與double形參匹配。
  • 第三步,確定是否有最佳的可行函式。如果有,則使用它,否則該函式調用出錯(沒有匹配項或者有多個匹配項都出錯)

所以再過載函式的時候要注意有預設引數值和隱式轉換,如下:

#include <iostream>
void show()
{
  std::cout << "無引數" << std::endl;
}
void show(int n = 1)
{
  std::cout << "有引數" << std::endl;
}
void show(double d)
{
    std::cout << "有引數 double" << std::endl;
}
void show(double & d)
{
    std
::cout << "有引數 double 引用" << std::endl; } int mian() { show(); //E0308 有多個 過載函式 "show" 例項與引數列表匹配: double d = 10; show(d); //E0308 有多個 過載函式 "show" 例項與引數列表匹配 }

對於有預設引數值的情況不必多說,對於隱式轉換則要注意在匹配的第三步編譯器確認那些是最佳的。它檢視為使函式呼叫引數與可行的時候選函式的引數匹配所需要進行的轉換。通常從最佳到最差的順序如下所述。
1. 完全匹配
2. 提升轉換(如:char自動轉換為int,float自動轉換為double)
3. 標準轉換(如:int轉換為char,long轉換為double)
4. 使用者定義的轉換(如:類種定義的轉換建構函式)

進行完全匹配時,C++允許一些無關緊要的轉換,如下表:

實參 形參
Type Type &
Type & Type
Type[] Type *
Type(argument-list) Type(*)(argument-list)
Type const Type
Type volatile Type
Type * const Type *
Type * volatile Type *
1.2 函式覆蓋

這個概念都是描述基類和派生類之間函式關係的。函式覆蓋:是基類虛擬函式在派生類種被重新定義。如:

class Base
{
  virtual void show()
  {
    ...
  }
}

class BasePlus : pulic Base
{
  void show()
  {
    ...
  }
}

這種形式是基類函式有virtual關鍵字,且派生類與基類函式名相同,引數列表也相同。如果引數列表不相同的話就是函式的隱藏了。而這種型別其實就是派生類函式把基類函式隱藏了。總之就是,在派生類種重新定義函式,將不是使用相同的函式特徵標覆蓋基類宣告,而是隱藏同名的基類方法,不管引數特徵標如何。所以,如果重新定義繼承的方法,應確保與原來的原型完全相同,但如果返回型別是基類引用或指標,則可以修改為指向派生類的引用或指標。如果基類宣告被過載了,則應在派生類種重新定義所有的基類版本。因為如果只定義一個版本,則另外兩個版本將被隱藏,派生類物件將無法使用它們。如果不需要修改,則可以在派生類定義函式種只顯示呼叫基類方法即可。

1.3 函式隱藏

函式隱藏則分兩種情況,一種是基類有virtual關鍵字,但引數裡列表不同。另一種是在基類中無virtual關鍵字,但是派生類中有同名函式(引數列表相不相同都無所謂)。這時如果派生類物件呼叫該函式則執行派生類的同名函式,所有基類的同名函式都會被隱藏。此時派生類是不能呼叫基類的任何同名函式的。如下:

class Base
{
public:
    void show()
    {
        std::cout << "Base is running!" << std::endl;
    }
    void show(int n)
    {
        std::cout << "Base : " << n << std::endl;
    }
};

class BasePlus:public Base
{
public:
    void show()
    {
        std::cout << "BasePlus is running !" << std::endl;
    }
};

int main()
{
    BasePlus ob;
    ob.show();
    // ob.show(1); // 錯誤    1   error C2660: “BasePlus::show”: 函式不接受 1 個引數
    system("pause");
    return 0;
}

如果基類物件呼叫該函式則執行基類函式。如果是指向派生類物件的基類引用或指標則呼叫基類的函式。

1.4 小結

其實瞭解一下類的動態繫結就沒有這麼麻煩了,本來就是基類物件執行基類的函式,派生類物件執行派生類函式。如果基類有virtual關鍵字,則基類中則會有一個指向虛擬函式表的指標,此時如果派生類定義了同名函式,則虛擬函式標中的函式指標則定位到了派生類的函式。這就是函式覆蓋的現象。至於函式隱藏則更簡單了,沒有virtual關鍵字的情況下都是靜態聯編,自然是基類物件呼叫基類函式,派生類物件呼叫派生類函數了。如果有virtual關鍵字時,但是引數列表不一樣,自然會呼叫派生類的函數了。

2 建構函式、解構函式、拷貝建構函式、過載=在繼承的時候的一些問題

2.1 建構函式和拷貝建構函式

建構函式不能是虛擬函式。建立派生類物件時,會先呼叫基類的建構函式,然後呼叫派生類的建構函式。如果基類沒有預設建構函式,則基類需要通過引數列表顯示地呼叫基類的建構函式,並傳遞引數給基類建構函式。

class Base_1
{
public:
    Base_1()
    {
        std::cout << "Base_1 defaut Constructor" << std::endl;
    }
};

class Base_2
{
public:
    Base_2(int n)
    {
        std::cout << "Base_2 self define Constructor:" << n << std::endl;
    }
};

class BasePlus_1 : public Base_1
{
public:
    BasePlus_1()
    {
        std::cout << "BasePlus_1 defaut Constructor" << std::endl;
    }

};

class BasePlus_2 : public Base_2
{
public:
    BasePlus_2():Base_2(2) // 預設建構函式
    {
        std::cout << "BasePlus_2 defaut Constructor" << std::endl;
    }
};

class BasePlus_3 : public Base_1
{
public:
    BasePlus_3(int n)
    {
        std::cout << "BasePlus_3 self define Constructor:" << n << std::endl;
    }
};

class BasePlus_4 : public Base_2
{
public:
    BasePlus_4(int n):Base_2(n)
    {
        std::cout << "BasePlus_4 self define Constructor : " << n << std::endl;
    }
};
int main()
{
    BasePlus_1 ob_1;
    std::cout << "------------------------------------" << std::endl;
    BasePlus_2 ob_2;
    std::cout << "------------------------------------" << std::endl;
    BasePlus_3 ob_3(3);
    std::cout << "------------------------------------" << std::endl;
    BasePlus_4 ob_4(4);

    system("pause");
    return 0;
}

執行結果如下:
這裡寫圖片描述

同樣的拷貝建構函式也是如此的。不可是虛擬的,不可被繼承。呼叫機制和建構函式一樣。拷貝建構函式通常會在一下情況被呼叫:

  • 用一個物件來初始化的時候
Base ob_0;
Base ob_1 = ob_0; // 此時會呼叫拷貝建構函式
  • 用一個物件來構造另一個物件的時候
Base ob_0;
Base ob_1(ob_0); // 此時會呼叫拷貝建構函式
  • 傳遞物件的時候
 Base Base::fun()
 {
   ....
   return * this; // 此時會呼叫拷貝建構函式
 }
  • 建立臨時物件時
Base ob_0;
Base(ob_0); // 此時會呼叫拷貝建構函式,該物件為臨時物件
2.2 解構函式

解構函式應當是虛擬函式,除非類不用作基類。如果派生類物件銷燬時,會呼叫派生類解構函式然後呼叫基類解構函式,這樣執行時正確的。但如果基類的引用或指標指向派生類物件的時候,這時候該物件銷燬的時候會只調用基類的解構函式,此時如果派生類建構函式裡動態分配了記憶體的話,這部分記憶體就洩漏了。如果基類的解構函式是虛的,則會先呼叫派生類解構函式,然後再呼叫基類的解構函式。

class Base_3
{
public:
    ~Base_3()
    {
        std::cout << "Base_3 destructor" << std::endl;
    }
};

class Base_4
{
public:
    virtual ~Base_4()
    {
        std::cout << "Base_3 destructor" << std::endl;
    }
};

class BasePlus_5 : public Base_3
{
public:
    ~BasePlus_5()
    {
        std::cout << "BasePlus_5 destructor" << std::endl;
    }
};

class BasePlus_6 : public Base_4
{
public:
    ~BasePlus_6()
    {
        std::cout << "BasePlus_6 destructor" << std::endl;
    }
};

int main()
{
    Base_3 * pBase_3= new BasePlus_5;
    Base_4 * pBase_4 = new BasePlus_6;

    delete pBase_3;
    std::cout << "------------------------------------" << std::endl;
    delete pBase_4;

    system("pause");
    return 0;
}

執行結果如下:
這裡寫圖片描述

通常應給基類提供一個虛解構函式,即使它並不需要解構函式

2.2 過載操作符=函式(賦值函式)

=操作符號與拷貝建構函式的呼叫情況是相同的,如果派生類沒有過載該操作符的時候。會呼叫基類的操作符函式(無論是過載的還是預設的),如果派生類定義了則會呼叫派生類過載的函式。所以為了保持基類部分資料的正確賦值,必須在派生類的過載函式中顯示呼叫基類的過載函式。如:

BasePlus & BasePlus::operator= (const BasePlus & ob)
{
  Base::operator= (ob); // 顯示呼叫基類建構函式
  ...
}

這裡唯一不同的是,operator=(),函式可以是虛擬函式。那再什麼情況下我們需要operator=是虛擬函式呢?只有在兩個指向派生類物件的基類引用賦值的時候需要賦值派生類部分資料的時候我們希望通過多型呼叫派生類的賦值函式。但是很不幸目前這種操作實現不了,如下:

virtual Base & operator= (const Base & ob);

BasePlus & operator= (const BasePlus & ob);

雖然基類賦值函式是虛擬函式,但是派生類函式並不是該函式的重新定義。也許是編譯器內部並沒有把virtual Base & operator= (const Base & ob)對映到虛擬函式表的原因,總之兩個指向派生類物件的基類引用賦值的時候呼叫的是基類的建構函式。如下:

// 基類
class Base
{
public:
    virtual Base & operator= (const Base & ob)
  {
    std::cout << "BasePlus operator = is running" << std::endl;
    return * this;
  }
};
// 派生類
class BasePlus : public Base
{
    BasePlus & operator= (const BasePlus & ob)
  {
    Base::operator= (ob);// 顯示呼叫基類函式
      std::cout << "BasePlus operator = is running" << std::endl;
      return * this;
  }
};

int main()
{
    BasePlus objBasePlus_0;
    BasePlus objBasePlus_1;

    Base & obj_0 = objBasePlus_0;
    Base & obj_1 = objBasePlus_1;

    obj_0 = obj_1;

  system("pause");
    return 0;
}

輸出結果如下:
這裡寫圖片描述