天天看點

認知篇----C語言中面向對象的核心思想

認知篇----C語言中面向對象的核心思想

一、前言

在嵌入式開發中,C/C++語言是使用最普及的,在C++11版本之前,它們的文法是比較相似的,隻不過C++提供了面向對象的程式設計方式。

雖然C++語言是從C語言發展而來的,但是今天的C++已經不是當年的C語言的擴充了,從2011版本開始,更像是一門全新的語言。

認知篇----C語言中面向對象的核心思想

那麼沒有想過,當初為什麼要擴充出C++?C語言有什麼樣的缺點導緻C++的産生?

認知篇----C語言中面向對象的核心思想

C++在這幾個問題上的解決的确很好,但是随着語言标準的逐漸擴充,C++語言的學習難度也逐漸加大。沒有開發過幾個項目,都不好意思說自己學會了C++,那些左值、右值、模闆、模闆參數、可變模闆參數等等一堆的概念,真的不是使用2,3年就可以熟練掌握的。

但是,C語言也有很多的優點:

認知篇----C語言中面向對象的核心思想

其實最後一個優點是最重要的:使用的人越多,生命力就越強。就像現在的社會一樣,不是優者生存,而是适者生存。

認知篇----C語言中面向對象的核心思想

這篇文章,我們就來聊聊如何在C語言中利用面向對象的思想來程式設計。也許你在項目中用不到,但是也強烈建議你看一下,因為面試過程中總監喜歡問。

二、什麼是面向對象程式設計

有這麼一個公式:程式=資料結構+算法。

C語言中一般使用面向過程程式設計,就是分析出解決問題所需要的步驟,然後用函數把這些步驟一步一步調用,在函數中對資料結構進行處理(執行算法),也就是說資料結構和算法是分開的。

C++語言把資料和算法封裝在一起,形成一個整體,無論是對它的屬性進行操作、還是對它的行為進行調用,都是通過一個對象來執行,這就是面向對象程式設計思想。

如果用C語言來模拟這樣的程式設計方式,需要解決3個問題:

  1. 資料的封裝
  2. 繼承
  3. 多态

第一個問題:封裝

封裝描述的是資料的組織形式,就是把屬于一個對象的所有屬性(資料)組織在一起,C語言中的結構體類型天生就支援這一點。

第二個問題:繼承

繼承描述的是對象之間的關系,子類通過繼承父類,自動擁有父類中的屬性和行為(也就是方法)。這個問題隻要了解了C語言的記憶體模型,也不是問題,隻要在子類結構體中的第一個成員變量的位置放置一個父類結構體變量,那麼子類對象就繼承了父類中的屬性。

另外補充一點:學習任何一種語言,一定要了解記憶體模型!

第三個問題:多态

按字面了解,多态就是“多種狀态”,描述的是一種動态的行為。在C++中,隻有通過基類引用或者指針,去調用虛函數的時候才發生多态,也就是說多态是發生在運作期間的,C++内部通過一個虛表來實作多态。那麼在C語言中,我們也可以按照這個思路來實作。

如果一門語言隻支援類,而不支援多态,隻能說它是基于對象的,而不是面向對象的。

既然思路上沒有問題,那麼我們就來簡單的實作一個。

三、先實作一個父類,解決封裝的問題

Animal.h

#ifndef _ANIMAL_H_#define _ANIMAL_H_// 定義父類結構typedef struct {    int age;    int weight;} Animal;// 構造函數聲明void Animal_Ctor(Animal *this, int age, int weight);// 擷取父類屬性聲明int Animal_GetAge(Animal *this);int Animal_GetWeight(Animal *this);#endif      

Animal.c

#include "Animal.h"// 父類構造函數實作void Animal_Ctor(Animal *this, int age, int weight){    this->age = age;    this->weight = weight;}int Animal_GetAge(Animal *this){    return this->age;}int Animal_GetWeight(Animal *this){    return this->weight;}      

測試一下:

#include <stdio.h>#include "Animal.h"#include "Dog.h"int main(){    // 在棧上建立一個對象    Animal a;      // 構造對象    Animal_Ctor(&a, 1, 3);     printf("age = %d, weight = %d \n",             Animal_GetAge(&a),            Animal_GetWeight(&a));    return 0;}      

可以簡單的了解為:在代碼段有一塊空間,存儲着可以處理Animal對象的函數;在棧中有一塊空間,存儲着a對象。

認知篇----C語言中面向對象的核心思想

與C++對比:在C++的方法中,隐含着第一個參數this指針。當調用一個對象的方法時,編譯器會自動把對象的位址傳遞給這個指針。

是以,在Animal.h中函數我們就模拟一下,顯示的定義這個this指針,在調用時主動把對象的位址傳遞給它,這樣的話,函數就可以對任意一個Animal對象進行處理了。

四、 實作一個子類,解決繼承的問題

Dog.h

#ifndef _DOG_H_#define _DOG_H_#include "Animal.h"// 定義子類結構typedef struct { Animal parent; // 第一個位置放置父類結構 int legs; // 添加子類自己的屬性}Dog;// 子類構造函數聲明void Dog_Ctor(Dog *this, int age, int weight, int legs);// 子類屬性聲明int Dog_GetAge(Dog *this);int Dog_GetWeight(Dog *this);int Dog_GetLegs(Dog *this);#endif      

​Dog.c​

#include "Dog.h"// 子類構造函數實作void Dog_Ctor(Dog *this, int age, int weight, int legs){    // 首先調用父類構造函數,來初始化從父類繼承的資料    Animal_Ctor(&this->parent, age, weight);    // 然後初始化子類自己的資料    this->legs = legs;}int Dog_GetAge(Dog *this){    // age屬性是繼承而來,轉發給父類中的擷取屬性函數    return Animal_GetAge(&this->parent);}int Dog_GetWeight(Dog *this){    return Animal_GetWeight(&this->parent);}int Dog_GetLegs(Dog *this){    // 子類自己的屬性,直接傳回    return this->legs;}      

測試一下:

int main(){ Dog d; Dog_Ctor(&d, 1, 3, 4); printf("age = %d, weight = %d, legs = %d \n",  Dog_GetAge(&d), Dog_GetWeight(&d), Dog_GetLegs(&d)); return 0;}      

在代碼段有一塊空間,存儲着可以處理Dog對象的函數;在棧中有一塊空間,存儲着d對象。由于Dog結構體中的第一個參數是Animal對象,是以從記憶體模型上看,子類就包含了父類中定義的屬性。

認知篇----C語言中面向對象的核心思想

Dog的記憶體模型中開頭部分就自動包括了Animal中的成員,也即是說Dog繼承了Animal的屬性。

五、利用虛函數,解決多态問題

在C++中,如果一個父類中定義了虛函數,那麼編譯器就會在這個記憶體中開辟一塊空間放置虛表,這張表裡的每一個item都是一個函數指針,然後在父類的記憶體模型中放一個虛表指針,指向上面這個虛表。

上面這段描述不是十分準确,主要看各家編譯器的處理方式,不過大部分C++處理器都是這麼幹的,我們可以想這麼了解。

子類在繼承父類之後,在記憶體中又會開辟一塊空間來放置子類自己的虛表,然後讓繼承而來的虛表指針指向子類自己的虛表。

認知篇----C語言中面向對象的核心思想

既然C++是這麼做的,那我們就用C來手動模拟這個行為:建立虛表和虛表指針。

1. Animal.h為父類Animal中,添加虛表和虛表指針

#ifndef _ANIMAL_H_#define _ANIMAL_H_struct AnimalVTable; // 父類虛表的前置聲明// 父類結構typedef struct { struct AnimalVTable *vptr; // 虛表指針 int age; int weight;} Animal;// 父類中的虛表struct AnimalVTable{ void (*say)(Animal *this); // 虛函數指針};// 父類中實作的虛函數void Animal_Say(Animal *this);#endif      

2. Animal.c

#include <assert.h>#include "Animal.h"// 父類中虛函數的具體實作static void _Animal_Say(Animal *this){ // 因為父類Animal是一個抽象的東西,不應該被執行個體化。 // 父類中的這個虛函數不應該被調用,也就是說子類必須實作這個虛函數。 // 類似于C++中的純虛函數。 assert(0); }// 父類構造函數void Animal_Ctor(Animal *this, int age, int weight){ // 首先定義一個虛表 static struct AnimalVTable animal_vtbl = {_Animal_Say}; // 讓虛表指針指向上面這個虛表 this->vptr = &animal_vtbl; this->age = age; this->weight = weight;}// 測試多态:傳入的參數類型是父類指針void Animal_Say(Animal *this){ // 如果this實際指向一個子類Dog對象,那麼this->vptr這個虛表指針指向子類自己的虛表, // 是以,this->vptr->say将會調用子類虛表中的函數。 this->vptr->say(this);}      
認知篇----C語言中面向對象的核心思想

在棧空間定義了一個虛函數表animal_vtbl,這個表中的每一項都是一個函數指針,例如:函數指針say就指向了代碼段中的函數_Animal_Say()。  > 對象a的第一個成員vptr是一個指針,指向了這個虛函數表animal_vtbl。

3.  Dog.h不變

4. Dog.c中定義子類自己的虛表

#include "Dog.h"// 子類中虛函數的具體實作static void _Dog_Say(Dog *this){ printf("dag say \n");}// 子類構造函數void Dog_Ctor(Dog *this, int age, int weight, int legs){ // 首先調用父類構造函數。 Animal_Ctor(&this->parent, age, weight); // 定義子類自己的虛函數表 static struct AnimalVTable dog_vtbl = {_Dog_Say}; // 把從父類中繼承得到的虛表指針指向子類自己的虛表 this->parent.vptr = &dog_vtbl; // 初始化子類自己的屬性 this->legs = legs;}      

5. 測試一下

int main(){ // 在棧中建立一個子類Dog對象 Dog d;  Dog_Ctor(&d, 1, 3, 4); // 把子類對象指派給父類指針 Animal *pa = &d; // 傳遞父類指針,将會調用子類中實作的虛函數。 Animal_Say(pa);}      

記憶體模型如下:

認知篇----C語言中面向對象的核心思想
對象d中,從父類繼承而來的虛表指針vptr,所指向的虛表是dog_vtbl。

在執行Animal_Say(pa)的時候,雖然參數類型是指向父類Animal的指針,但是實際傳入的pa是一個指向子類Dog的對象,這個對象中的虛表指針vptr指向的是子類中自己定義的虛表dog_vtbl,這個虛表中的函數指針say指向的是子類中重新定義的虛函數_Dog_Say,是以this->vptr->say(this)最終調用的函數就是_Dog_Say。

基本上,在C中面向對象的開發思想就是以上這樣。這個代碼很簡單,自己手敲一下就可以了。

六、C面向對象思想在項目中的使用

1. Linux核心

看一下關于socket的幾個結構體:

struct sock { ...}struct inet_sock { struct sock sk; ...};struct udp_sock { struct sock sk; ...};      
認知篇----C語言中面向對象的核心思想

sock可以看作是父類,inet_sock和udp_sock的第一個成員都是是sock類型,從記憶體模型上看相當于是繼承了sock中的所有屬性。

2. glib庫

以最簡單的字元串處理函數來舉例:

GString *g_string_truncate(GString *string, gint len)GString *g_string_append(GString *string, gchar *val)GString *g_string_prepend(GString *string, gchar *val)      

API函數的第一個參數都是一個GString對象指針,指向需要處理的那個字元串對象。

GString *s1, *s2;s1 = g_string_new("Hello");s2 = g_string_new("Hello");g_string_append(s1," World!");g_string_append(s2," World!");      

3. 其他項目

繼續閱讀