1. 程式人生 > >C語言面向物件程式設計(二):繼承詳解

C語言面向物件程式設計(二):繼承詳解

    C++ 中的繼承,從派生類與基類的關係來看(出於對比 C 與 C++,只說公有繼承):

  • 派生類內部可以直接使用基類的 public 、protected 成員(包括變數和函式)
  • 使用派生類的物件,可以像訪問派生類自己的成員一樣訪問基類的成員
  •  對於被派生類覆蓋的基類的非虛擬函式,在派生類中可以通過基類名和域作用符(::)來訪問
  • 當使用基類指標呼叫虛擬函式時,會呼叫指標指向的實際物件實現的函式,如果該物件未過載該虛擬函式,則沿繼承層次,逐級回溯,直到找到一個實現

    上面的幾個特點,我們在 C 語言中能否全部實現呢?我覺得可以實現類似的特性,但在使用方法上會有些區別。後面我們一個一個來說,在此之前呢,先說繼承的基本實現。

    先看 C 語言中通過“包含”模擬實現繼承的簡單程式碼框架:

  1. struct base{

  2. int a;

  3. };

  4. struct derived{

  5. struct base parent;

  6. int b;

  7. };

  8. struct derived_2{

  9. struct derived parent;

  10. int b;

  11. };

    上面的示例只有資料成員,函式成員其實是個指標,可以看作資料成員。 C 中的 struct 沒有訪問控制,預設都是公有訪問(與 java 不同)。

    下面是帶成員函式的結構體:

  1. struct base {

  2. int a;

  3. void (*func1)(struct base *_this);

  4. };

  5. struct derived {

  6. struct base parent;

  7. int b;

  8. void (*func2)(struct derived* _this;

  9. };

    為了像 C++ 中一樣通過類例項來訪問成員函式,必須將結構體內的函式指標的第一個引數定義為自身的指標,在呼叫時傳入函式指標所屬的結構體例項。這是因為 C 語言中不存在像 C++ 中那樣的 this 指標,如果我們不顯式地通過引數提供,那麼在函式內部就無法訪問結構體例項的其它成員。

    下面是在 c 檔案中實現的函式:

  1. static void base_func1(struct base *_this)

  2. {

  3. printf("this is base::func1\n");

  4. }

  5. static void derived_func2(struct derived *_this)

  6. {

  7. printf("this is derived::func2\n");

  8. }

    C++ 的 new 操作符會呼叫建構函式,對類例項進行初始化。 C 語言中只有 malloc 函式族來分配記憶體塊,我們沒有機會來自動初始化結構體的成員,只能自己增加一個函式。如下面這樣(略去標頭檔案中的宣告語句):

  1. struct base * new_base()

  2. {

  3. struct base * b = malloc(sizeof(struct base));

  4. b->a = 0;

  5. b->func1 = base_func1;

  6. return b;

  7. }

    好的,建構函式有了。通過 new_base() 呼叫返回的結構體指標,已經可以像類例項一樣使用了:

  1. struct base * b1 = new_base();

  2. b1->func1(b1);

    到這裡我們已經知道如何在 C 語言中實現一個基本的“類”了。接下來一一來看前面提到的幾點。

   第一點,派生類內部可以直接使用基類的 public 、protected 成員(包括變數和函式)。具體到上面的例子,我們可以在 derived_func2 中訪問基類 base 的成員 a 和 func1 ,沒有任何問題,只不過是顯式通過 derived 的第一個成員 parent 來訪問:

  1. static void derived_func2(struct derived *_this)

  2. {

  3. printf("this is derived::func2, base::a = %d\n", _this->parent.a);

  4. _this->parent.func1(&_this->parent);

  5. }

    第二點,使用派生類的物件,可以像訪問派生類自己的成員一樣訪問基類的成員。這個有點變化,還是隻能通過派生類例項的第一個成員 parent 來訪問基類的成員(通過指標強制轉換的話可以直接訪問)。程式碼如下:

  1. struct derived d;

  2. printf("base::a = %d\n",d.parent.a);

  3. struct derived *p = new_derived();

  4. ((struct base *)p)->func1(p);

    第三點,對於被派生類覆蓋的基類的非虛擬函式,在派生類中可以通過基類名和域作用符(::)來訪問。其實通過前兩點,我們已經熟悉了在 C 中訪問“基類”成員的方法,總是要通過“派生類”包含的放在結構體第一個位置的基類型別的成員變數來訪問。所以在 C 中,嚴格來講,實際上不存在覆蓋這種情況。即便定義了完全一樣的函式指標,也沒有關係,因為“包含”這種方式,已經從根本上分割了“基類”和“派生類”的成員,它們不在一個街區,不會衝突。

    下面是一個所謂覆蓋的例子:

  1. struct base{

  2. int a;

  3. int (*func)(struct base * b);

  4. };

  5. struct derived {

  6. struct base b;

  7. int (*func)(struct derived *d);

  8. };

  9. /* usage */

  10. struct derived * d = new_derived();

  11. d->func(d);

  12. d->b.func((struct base*)d);

    如上面的程式碼所示,不存在名字覆蓋問題。

    第四點,虛擬函式。虛擬函式是 C++ 裡面最有意義的一個特性,是多型的基礎,要想講明白比較困難,我們接下來專門寫一篇文章講述如何在 C 中實現類似虛擬函式的效果,實現多型。

    回顧一下: