一、前言
在嵌入式開發(fā)中,C/C++語(yǔ)言是使用最普及的,在C++11版本之前,它們的語(yǔ)法是比較相似的,只不過C++提供了面向?qū)ο蟮?a href="http://wenjunhu.com/v/tag/1315/" target="_blank">編程方式。
雖然C++語(yǔ)言是從C語(yǔ)言發(fā)展而來的,但是今天的C++已經(jīng)不是當(dāng)年的C語(yǔ)言的擴(kuò)展了,從2011版本開始,更像是一門全新的語(yǔ)言。
那么沒有想過,當(dāng)初為什么要擴(kuò)展出C++?C語(yǔ)言有什么樣的缺點(diǎn)導(dǎo)致C++的產(chǎn)生?
C++在這幾個(gè)問題上的解決的確很好,但是隨著語(yǔ)言標(biāo)準(zhǔn)的逐步擴(kuò)充,C++語(yǔ)言的學(xué)習(xí)難度也逐漸加大。沒有開發(fā)過幾個(gè)項(xiàng)目,都不好意思說自己學(xué)會(huì)了C++,那些左值、右值、模板、模板參數(shù)、可變模板參數(shù)等等一堆的概念,真的不是使用2,3年就可以熟練掌握的。但是,C語(yǔ)言也有很多的優(yōu)點(diǎn):
其實(shí)最后一個(gè)優(yōu)點(diǎn)是最重要的:使用的人越多,生命力就越強(qiáng)。就像現(xiàn)在的社會(huì)一樣,不是優(yōu)者生存,而是適者生存。
這篇文章,我們就來聊聊如何在C語(yǔ)言中利用面向?qū)ο蟮乃枷雭砭幊?。也許你在項(xiàng)目中用不到,但是也強(qiáng)烈建議你看一下,因?yàn)槲抑霸谔鄣臅r(shí)候就兩次被問到這個(gè)問題。## 二、什么是面向?qū)ο缶幊?/p>
有這么一個(gè)公式:程序=數(shù)據(jù)結(jié)構(gòu)+算法。
C語(yǔ)言中一般使用面向過程編程,就是分析出解決問題所需要的步驟,然后用函數(shù)把這些步驟一步一步調(diào)用,在函數(shù)中對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行處理(執(zhí)行算法),也就是說數(shù)據(jù)結(jié)構(gòu)和算法是分開的。
C++語(yǔ)言把數(shù)據(jù)和算法封裝在一起,形成一個(gè)整體,無(wú)論是對(duì)它的屬性進(jìn)行操作、還是對(duì)它的行為進(jìn)行調(diào)用,都是通過一個(gè)對(duì)象來執(zhí)行,這就是面向?qū)ο缶幊趟枷搿?/p>
如果用C語(yǔ)言來模擬這樣的編程方式,需要解決3個(gè)問題:
- 數(shù)據(jù)的封裝
- 繼承
- 多態(tài)
第一個(gè)問題:封裝
封裝描述的是數(shù)據(jù)的組織形式,就是把屬于一個(gè)對(duì)象的所有屬性(數(shù)據(jù))組織在一起,C語(yǔ)言中的結(jié)構(gòu)體類型天生就支持這一點(diǎn)。
第二個(gè)問題:繼承
繼承描述的是對(duì)象之間的關(guān)系,子類通過繼承父類,自動(dòng)擁有父類中的屬性和行為(也就是方法)。這個(gè)問題只要理解了C語(yǔ)言的內(nèi)存模型,也不是問題,只要在子類結(jié)構(gòu)體中的第一個(gè)成員變量的位置放置一個(gè)父類結(jié)構(gòu)體變量,那么子類對(duì)象就繼承了父類中的屬性。
另外補(bǔ)充一點(diǎn):學(xué)習(xí)任何一種語(yǔ)言,一定要理解內(nèi)存模型!#### 第三個(gè)問題:多態(tài)
按字面理解,多態(tài)就是“多種狀態(tài)”,描述的是一種動(dòng)態(tài)的行為。在C++中,只有通過基類引用或者指針,去調(diào)用虛函數(shù)的時(shí)候才發(fā)生多態(tài),也就是說多態(tài)是發(fā)生在運(yùn)行期間的,C++內(nèi)部通過一個(gè)虛表來實(shí)現(xiàn)多態(tài)。那么在C語(yǔ)言中,我們也可以按照這個(gè)思路來實(shí)現(xiàn)。
如果一門語(yǔ)言只支持類,而不支持多態(tài),只能說它是基于對(duì)象的,而不是面向?qū)ο蟮摹?/p>
既然思路上沒有問題,那么我們就來簡(jiǎn)單的實(shí)現(xiàn)一個(gè)。
三、先實(shí)現(xiàn)一個(gè)父類,解決封裝的問題
Animal.h
// 定義父類結(jié)構(gòu)
typedef struct {
int age;
int weight;
} Animal;
// 構(gòu)造函數(shù)聲明
voidAnimal_Ctor(Animal *this, int age, int weight);
// 獲取父類屬性聲明
intAnimal_GetAge(Animal *this);
intAnimal_GetWeight(Animal *this);
Animal.c
// 父類構(gòu)造函數(shù)實(shí)現(xiàn)
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;
}
測(cè)試一下:
#include
#include "Animal.h"
#include "Dog.h"
int main()
{
// 在棧上創(chuàng)建一個(gè)對(duì)象
Animal a;
// 構(gòu)造對(duì)象
Animal_Ctor(&a, 1, 3);
printf("age = %d, weight = %d \\n",
Animal_GetAge(&a),
Animal_GetWeight(&a));
return 0;
}
可以簡(jiǎn)單的理解為:在代碼段有一塊空間,存儲(chǔ)著可以處理Animal對(duì)象的函數(shù);在棧中有一塊空間,存儲(chǔ)著a對(duì)象。
與C++對(duì)比:在C++的方法中,隱含著第一個(gè)參數(shù)this指針。當(dāng)調(diào)用一個(gè)對(duì)象的方法時(shí),編譯器會(huì)自動(dòng)把對(duì)象的地址傳遞給這個(gè)指針。所以,在Animal.h中函數(shù)我們就模擬一下,顯示的定義這個(gè)this指針,在調(diào)用時(shí)主動(dòng)把對(duì)象的地址傳遞給它,這樣的話,函數(shù)就可以對(duì)任意一個(gè)Animal對(duì)象進(jìn)行處理了。
四、 實(shí)現(xiàn)一個(gè)子類,解決繼承的問題
Dog.h
// 定義子類結(jié)構(gòu)
typedef struct {
Animal parent; // 第一個(gè)位置放置父類結(jié)構(gòu)
int legs; // 添加子類自己的屬性
}Dog;
// 子類構(gòu)造函數(shù)聲明
voidDog_Ctor(Dog *this, int age, int weight, int legs);
// 子類屬性聲明
intDog_GetAge(Dog *this);
intDog_GetWeight(Dog *this);
intDog_GetLegs(Dog *this);
Dog.c
// 子類構(gòu)造函數(shù)實(shí)現(xiàn)
void Dog_Ctor(Dog *this, int age, int weight, int legs)
{
// 首先調(diào)用父類構(gòu)造函數(shù),來初始化從父類繼承的數(shù)據(jù)
Animal_Ctor(&this->parent, age, weight);
// 然后初始化子類自己的數(shù)據(jù)
this->legs = legs;
}
int Dog_GetAge(Dog *this)
{
// age屬性是繼承而來,轉(zhuǎn)發(fā)給父類中的獲取屬性函數(shù)
return Animal_GetAge(&this->parent);
}
int Dog_GetWeight(Dog *this)
{
return Animal_GetWeight(&this->parent);
}
int Dog_GetLegs(Dog *this)
{
// 子類自己的屬性,直接返回
return this->legs;
}
測(cè)試一下:
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;
}
在代碼段有一塊空間,存儲(chǔ)著可以處理Dog對(duì)象的函數(shù);在棧中有一塊空間,存儲(chǔ)著d對(duì)象。由于Dog結(jié)構(gòu)體中的第一個(gè)參數(shù)是Animal對(duì)象,所以從內(nèi)存模型上看,子類就包含了父類中定義的屬性。
Dog的內(nèi)存模型中開頭部分就自動(dòng)包括了Animal中的成員,也即是說Dog繼承了Animal的屬性。## 五、利用虛函數(shù),解決多態(tài)問題
在C++中,如果一個(gè)父類中定義了虛函數(shù),那么編譯器就會(huì)在這個(gè)內(nèi)存中開辟一塊空間放置虛表,這張表里的每一個(gè)item都是一個(gè)函數(shù)指針,然后在父類的內(nèi)存模型中放一個(gè)虛表指針,指向上面這個(gè)虛表。
上面這段描述不是十分準(zhǔn)確,主要看各家編譯器的處理方式,不過大部分C++處理器都是這么干的,我們可以想這么理解。
子類在繼承父類之后,在內(nèi)存中又會(huì)開辟一塊空間來放置子類自己的虛表,然后讓繼承而來的虛表指針指向子類自己的虛表。
既然C++是這么做的,那我們就用C來手動(dòng)模擬這個(gè)行為:創(chuàng)建虛表和虛表指針。#### 1. Animal.h為父類Animal中,添加虛表和虛表指針
#ifndef _ANIMAL_H_
#define _ANIMAL_H_
struct AnimalVTable; // 父類虛表的前置聲明
// 父類結(jié)構(gòu)
typedef struct {
struct AnimalVTable *vptr; // 虛表指針
int age;
int weight;
} Animal;
// 父類中的虛表
struct AnimalVTable{
void (*say)(Animal *this); // 虛函數(shù)指針
};
// 父類中實(shí)現(xiàn)的虛函數(shù)
void Animal_Say(Animal *this);
#endif
2. Animal.c
#include
#include "Animal.h"
// 父類中虛函數(shù)的具體實(shí)現(xiàn)
static void _Animal_Say(Animal *this)
{
// 因?yàn)楦割怉nimal是一個(gè)抽象的東西,不應(yīng)該被實(shí)例化。
// 父類中的這個(gè)虛函數(shù)不應(yīng)該被調(diào)用,也就是說子類必須實(shí)現(xiàn)這個(gè)虛函數(shù)。
// 類似于C++中的純虛函數(shù)。
assert(0);
}
// 父類構(gòu)造函數(shù)
void Animal_Ctor(Animal *this, int age, int weight)
{
// 首先定義一個(gè)虛表
static struct AnimalVTable animal_vtbl = {_Animal_Say};
// 讓虛表指針指向上面這個(gè)虛表
this->vptr = &animal_vtbl;
this->age = age;
this->weight = weight;
}
// 測(cè)試多態(tài):傳入的參數(shù)類型是父類指針
void Animal_Say(Animal *this)
{
// 如果this實(shí)際指向一個(gè)子類Dog對(duì)象,那么this->vptr這個(gè)虛表指針指向子類自己的虛表,
// 因此,this->vptr->say將會(huì)調(diào)用子類虛表中的函數(shù)。
this->vptr->say(this);
}
在??臻g定義了一個(gè)虛函數(shù)表animal_vtbl,這個(gè)表中的每一項(xiàng)都是一個(gè)函數(shù)指針,例如:函數(shù)指針say就指向了代碼段中的函數(shù)_Animal_Say()。 > 對(duì)象a的第一個(gè)成員vptr是一個(gè)指針,指向了這個(gè)虛函數(shù)表animal_vtbl。#### 3. Dog.h不變
4. Dog.c中定義子類自己的虛表
#include "Dog.h"
// 子類中虛函數(shù)的具體實(shí)現(xiàn)
static void _Dog_Say(Dog *this)
{
printf("dag say \\n");
}
// 子類構(gòu)造函數(shù)
void Dog_Ctor(Dog *this, int age, int weight, int legs)
{
// 首先調(diào)用父類構(gòu)造函數(shù)。
Animal_Ctor(&this->parent, age, weight);
// 定義子類自己的虛函數(shù)表
static struct AnimalVTable dog_vtbl = {_Dog_Say};
// 把從父類中繼承得到的虛表指針指向子類自己的虛表
this->parent.vptr = &dog_vtbl;
// 初始化子類自己的屬性
this->legs = legs;
}
5. 測(cè)試一下
int main()
{
// 在棧中創(chuàng)建一個(gè)子類Dog對(duì)象
Dog d;
Dog_Ctor(&d, 1, 3, 4);
// 把子類對(duì)象賦值給父類指針
Animal *pa = &d;
// 傳遞父類指針,將會(huì)調(diào)用子類中實(shí)現(xiàn)的虛函數(shù)。
Animal_Say(pa);
}
內(nèi)存模型如下:
對(duì)象d中,從父類繼承而來的虛表指針vptr,所指向的虛表是dog_vtbl。
在執(zhí)行**Animal_Say(pa)**的時(shí)候,雖然參數(shù)類型是指向父類Animal的指針,但是實(shí)際傳入的pa是一個(gè)指向子類Dog的對(duì)象,這個(gè)對(duì)象中的虛表指針vptr指向的是子類中自己定義的虛表dog_vtbl,這個(gè)虛表中的函數(shù)指針say指向的是子類中重新定義的虛函數(shù)_Dog_Say,因此this->vptr->say(this)最終調(diào)用的函數(shù)就是_Dog_Say。
基本上,在C中面向?qū)ο蟮拈_發(fā)思想就是以上這樣。這個(gè)代碼很簡(jiǎn)單,自己手敲一下就可以了。如果想偷懶,請(qǐng)?jiān)诤笈_(tái)留言,我發(fā)給您。
六、C面向?qū)ο笏枷朐陧?xiàng)目中的使用
1. Linux內(nèi)核
看一下關(guān)于socket的幾個(gè)結(jié)構(gòu)體:
struct sock {
...
}
struct inet_sock {
struct sock sk;
...
};
struct udp_sock {
struct sock sk;
...
};
sock可以看作是父類,inet_sock和udp_sock的第一個(gè)成員都是是sock類型,從內(nèi)存模型上看相當(dāng)于是繼承了sock中的所有屬性。#### 2. glib庫(kù)
以最簡(jiǎn)單的字符串處理函數(shù)來舉例:
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函數(shù)的第一個(gè)參數(shù)都是一個(gè)GString對(duì)象指針,指向需要處理的那個(gè)字符串對(duì)象。
GString *s1, *s2;
s1 = g_string_new("Hello");
s2 = g_string_new("Hello");
g_string_append(s1," World!");
g_string_append(s2," World!");
3. 其他項(xiàng)目
還有一些項(xiàng)目,雖然從函數(shù)的參數(shù)上來看,似乎不是面向?qū)ο蟮模窃跀?shù)據(jù)結(jié)構(gòu)的設(shè)計(jì)上看來,也是面向?qū)ο蟮乃枷耄热纾?/p>
-
編程
+關(guān)注
關(guān)注
88文章
3615瀏覽量
93731 -
嵌入式開發(fā)
+關(guān)注
關(guān)注
18文章
1030瀏覽量
47578 -
C/C++
+關(guān)注
關(guān)注
1文章
57瀏覽量
4631
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論