OOPC精要——撩開“對(duì)象”的神秘面紗
前言:
何為面向過(guò)程:
面向過(guò)程,本質(zhì)是“順序,循環(huán),分支”
面向過(guò)程開發(fā),就像是總有人問(wèn)你要后續(xù)的計(jì)劃一樣,下一步做什么,再下一步做什么,意外、事物中斷、突發(fā)事件怎么做。理論上來(lái)說(shuō),任何一個(gè)過(guò)程都可以通過(guò)“順序,循環(huán),分支”來(lái)描述出來(lái),但是實(shí)際上,很多項(xiàng)目的復(fù)雜度,都不是“順序循環(huán)分支”幾句話能說(shuō)清楚的。稍微大一點(diǎn)的項(xiàng)目,多線程,幾十件事情并發(fā), 如果用這種最簡(jiǎn)單的描述方式,要么幾乎無(wú)法使用,缺失細(xì)節(jié)太多,要么事無(wú)巨細(xì),用最簡(jiǎn)單的描述,都會(huì)讓后期復(fù)雜度提升到一個(gè)爆炸的狀態(tài)。
何為面向?qū)ο螅?/span>
面向?qū)ο?,本質(zhì)是“繼承,封裝,多態(tài)”
面向?qū)ο蟮暮诵氖前褦?shù)據(jù)和處理數(shù)據(jù)的方法封裝在一起。面向?qū)ο罂梢院?jiǎn)單的理解為將一切事物模塊化 ,面向?qū)ο蟮拇a結(jié)構(gòu),有效做到了層層分級(jí)、層層封裝,每一層只理解需要對(duì)接的部分,其他被封裝的細(xì)節(jié)不去考慮,有效控制了小范圍內(nèi)信息量的爆炸。然而當(dāng)項(xiàng)目的復(fù)雜度超過(guò)一定程度的時(shí)候,模塊間對(duì)接的代價(jià)遠(yuǎn)遠(yuǎn)高于實(shí)體業(yè)務(wù)干活的代價(jià), 因?yàn)槊嫦驅(qū)ο蟾拍畹膶蛹?jí)劃分,要實(shí)現(xiàn)的業(yè)務(wù)需要封裝,封裝好跟父類對(duì)接。多繼承是萬(wàn)惡之源,讓整個(gè)系統(tǒng)結(jié)構(gòu)變成了網(wǎng)狀、環(huán)狀,最后變成一坨亂麻。
Erlang 的創(chuàng)建者 JoeArmstrong 有句名言:
面向?qū)ο笳Z(yǔ)言的問(wèn)題在于,它們依賴于特定的環(huán)境。你想要個(gè)香蕉,但拿到的卻是拿著香蕉的猩猩,乃至最后你擁有了整片叢林。
能解決問(wèn)題的就是最好的:
程序設(shè)計(jì)要專注于“應(yīng)用邏輯的實(shí)現(xiàn)”本身,應(yīng)該盡量避免被“某種技術(shù)”分心 。《UNIX編程藝術(shù)》,第一原則就是KISS原則,整本書都貫徹了KISS(keep it simple, stupid?。?原則。寫項(xiàng)目、寫代碼,目的都是為了解決問(wèn)題。而不是花費(fèi)或者說(shuō)浪費(fèi)過(guò)多的時(shí)間在考慮與要解決的問(wèn)題完全無(wú)關(guān)的事情上。不管是面向過(guò)程,還是面向?qū)ο螅际菫榱私鉀Q某一類問(wèn)題的技術(shù)。各有各的用武之地:
在驅(qū)動(dòng)開發(fā)、嵌入式底層開發(fā)這些地方,面向過(guò)程開發(fā)模式,干凈,利索,直觀,資源掌控度高。在這些環(huán)境,面向過(guò)程開發(fā)幾乎是無(wú)可替代的。
在工作量大,難度較低、細(xì)節(jié)過(guò)多、用簡(jiǎn)單的規(guī)范規(guī)則無(wú)法面面俱到的環(huán)境下,用面向?qū)ο箝_發(fā)模式,用低質(zhì)量人力砸出來(lái)產(chǎn)業(yè)化項(xiàng)目。
1、面向?qū)ο缶幊?/span>
面向?qū)ο笾皇且环N設(shè)計(jì)思路,是一種概念,并沒(méi)有說(shuō)什么C++是面向?qū)ο蟮恼Z(yǔ)言,java是面向?qū)ο蟮恼Z(yǔ)言。C語(yǔ)言一樣可以是面向?qū)ο蟮恼Z(yǔ)言,Linux內(nèi)核就是面向?qū)ο蟮脑鶪NU C89編寫的,但是為了支持面向?qū)ο蟮拈_發(fā)模式,Linux內(nèi)核編寫了大量概念維護(hù)modules,維護(hù)struct的函數(shù)指針,內(nèi)核驅(qū)動(dòng)裝載等等機(jī)制。而C++和java為了增加面向?qū)ο蟮膶懛?,直接給編譯器加了一堆語(yǔ)法糖。
2、什么是類和對(duì)象
在C語(yǔ)言中,結(jié)構(gòu)體是一種構(gòu)造類型,可以包含若干成員變量,每個(gè)成員變量的類型可以不同;可以通過(guò)結(jié)構(gòu)體來(lái)定義結(jié)構(gòu)體變量,每個(gè)變量擁有相同的性質(zhì)。
在C++語(yǔ)言中,類也是一種構(gòu)造類型,但是進(jìn)行了一些擴(kuò)展,可以將類看做是結(jié)構(gòu)體的升級(jí)版,類的成員不但可以是變量,還可以是函數(shù);不同的是,通過(guò)結(jié)構(gòu)體定義出來(lái)的變量還是叫變量,而通過(guò)類定義出來(lái)的變量有了新的名稱,叫做對(duì)象(Object)在 C++ 中,通過(guò)類名就可以創(chuàng)建對(duì)象,這個(gè)過(guò)程叫做類的實(shí)例化,因此也稱對(duì)象是類的一個(gè)實(shí)例(Instance) 類的成員變量稱為屬性(Property),將類的成員函數(shù)稱為方法(Method)。在C語(yǔ)言中的使用struct這個(gè)關(guān)鍵字定義結(jié)構(gòu)體,在C++ 中使用的class這個(gè)關(guān)鍵字定義類。
結(jié)構(gòu)體封裝的變量都是 public 屬性,類相比與結(jié)構(gòu)體的封裝,多了 private 屬性和 protected ?屬性, private 和protected ?關(guān)鍵字的作用在于更好地隱藏了類的內(nèi)部實(shí)現(xiàn) ,只有類源代碼才能訪問(wèn)私有成員,只有派生類的類源代碼才能訪問(wèn)基類的受保護(hù)成員,每個(gè)人都可以訪問(wèn)公共成員。這樣可以有效的防止可能被不知道誰(shuí)訪問(wèn)的全局變量。
結(jié)構(gòu)體封裝的變量都是 public 屬性,類相比與結(jié)構(gòu)體的封裝,多了 private 屬性和 protected ?屬性, private 和protected ?關(guān)鍵字的作用在于更好地隱藏了類的內(nèi)部實(shí)現(xiàn) ,只有類源代碼才能訪問(wèn)私有成員,只有派生類的類源代碼才能訪問(wèn)基類的受保護(hù)成員,每個(gè)人都可以訪問(wèn)公共成員。這樣可以有效的防止可能被不知道誰(shuí)訪問(wèn)的全局變量。
C語(yǔ)言中的結(jié)構(gòu)體:
1//通過(guò)struct?關(guān)鍵字定義結(jié)構(gòu)體
2struct?object
3{
4????char?*name;?????????????????????????????????????????
5????//指向函數(shù)的指針類型
6????void??(*setname)(struct?object?*this,char?*name);???????????
7};
8void?setname(struct?object?*this,char?*name)
9{
10????this->name=name;
11}
C++語(yǔ)言中的類:
1//通過(guò)class關(guān)鍵字類定義類
2class?object{
3????public:??????????????????
4????????void?setname(char?*name);
5????private:
6????????char?*name;??????
7};
8void?object::setname(char?*name){
9????this->name?=?name;
10}
3、內(nèi)存分布的對(duì)比
不管是C語(yǔ)言中的結(jié)構(gòu)體或者C++中的類,都只是相當(dāng)于一個(gè)模板,起到說(shuō)明的作用,不占用內(nèi)存空間;結(jié)構(gòu)體定義的變量和類創(chuàng)建的對(duì)象才是實(shí)實(shí)在在的數(shù)據(jù),要有地方來(lái)存放,才會(huì)占用內(nèi)存空間。
結(jié)構(gòu)體變量的內(nèi)存模型:
結(jié)構(gòu)體的內(nèi)存分配是按照聲明的順序依次排列,涉及到內(nèi)存對(duì)齊問(wèn)題。
為什么會(huì)存在內(nèi)存對(duì)齊問(wèn)題,引用傻孩子公眾號(hào)裸機(jī)思維的文章《漫談C變量——對(duì)齊》加以解釋:
在ARM Compiler里面,結(jié)構(gòu)體內(nèi)的成員并不是簡(jiǎn)單的對(duì)齊到字(Word)或者半字(Half
Word),更別提字節(jié)了(Byte),結(jié)構(gòu)體的對(duì)齊使用以下規(guī)則:整個(gè)結(jié)構(gòu)體根據(jù)結(jié)構(gòu)體內(nèi)對(duì)齊要求最大的那個(gè)元素來(lái)對(duì)齊。比如,整個(gè)結(jié)構(gòu)體內(nèi)部對(duì)齊要求最大的元素是希望對(duì)齊到WORD,那么整個(gè)結(jié)構(gòu)體就默認(rèn)對(duì)齊到4字節(jié)。
結(jié)構(gòu)體內(nèi)部,成員變量的排列順序嚴(yán)格按照定義的順序進(jìn)行。
結(jié)構(gòu)體內(nèi)部,成員變量自動(dòng)對(duì)齊到自己的大小——這就會(huì)導(dǎo)致空隙的產(chǎn)生。
結(jié)構(gòu)體內(nèi)部,成員變量可以通過(guò) attribute ((packed))單獨(dú)指定對(duì)齊方式為byte。
strut對(duì)象的內(nèi)存模型:
1//通過(guò)struct?關(guān)鍵字定義結(jié)構(gòu)體
2struct?{
3????uint8_t????a;
4????uint16_t???b;
5????uint8_t????c;
6????uint32_t?? d;
7};
memory layout:
class對(duì)象的內(nèi)存模型:
成員變量在堆區(qū)或棧區(qū)分配內(nèi)存,成員函數(shù)放在代碼區(qū)。對(duì)象的大小只受成員變量的影響,和成員函數(shù)沒(méi)有關(guān)系。對(duì)象的內(nèi)存分布按照聲明的順序依次排列,和結(jié)構(gòu)體非常類似,也會(huì)有內(nèi)存對(duì)齊的問(wèn)題。
可以看到結(jié)構(gòu)體和對(duì)象的內(nèi)存模型都是非常干凈的,C語(yǔ)言里訪問(wèn)成員函數(shù)實(shí)際上是通過(guò)指向函數(shù)的指針變量來(lái)訪問(wèn)(相當(dāng)于回調(diào)),那么C++編譯器究竟是根據(jù)什么找到了成員函數(shù)呢?
實(shí)際上C++的編譯代碼的過(guò)程中,把成員函數(shù)最終編譯成與對(duì)象無(wú)關(guān)的全局函數(shù),如果函數(shù)體中沒(méi)有成員變量,那問(wèn)題就很簡(jiǎn)單,不用對(duì)函數(shù)做任何處理,直接調(diào)用即可。
如果成員函數(shù)中使用到了成員變量該怎么辦呢?成員變量的作用域不是全局,不經(jīng)任何處理就無(wú)法在函數(shù)內(nèi)部訪問(wèn)。
C++規(guī)定,編譯成員函數(shù)時(shí)要額外添加一個(gè)this指針參數(shù),把當(dāng)前對(duì)象的指針傳遞進(jìn)去,通過(guò)this指針來(lái)訪問(wèn)成員變量。
this 實(shí)際上是成員函數(shù)的一個(gè)形參,在調(diào)用成員函數(shù)時(shí)將對(duì)象的地址作為實(shí)參傳遞給 this。不過(guò) this 這個(gè)形參是隱式的,它并不出現(xiàn)在代碼中,而是在編譯階段由編譯器默默地將它添加到參數(shù)列表中。
這樣通過(guò)傳遞對(duì)象指針完成了成員函數(shù)和成員變量的關(guān)聯(lián)。這與我們從表明上看到的剛好相反,通過(guò)對(duì)象調(diào)用成員函數(shù)時(shí),不是通過(guò)對(duì)象找函數(shù),而是通過(guò)函數(shù)找對(duì)象。
無(wú)論是C還是C++,其函數(shù)第一個(gè)參數(shù)都是一個(gè)指向其目標(biāo)對(duì)象的指針,也就是this指針,只不過(guò)C++由編譯器自動(dòng)生成——所以方法的函數(shù)原型中不用專門寫出來(lái)而C語(yǔ)言模擬的方法函數(shù)則必須直接明確的寫出來(lái)。
4 掩碼結(jié)構(gòu)體
在C語(yǔ)言的編譯環(huán)境下,不支持結(jié)構(gòu)體內(nèi)放函數(shù)體,除了函數(shù)外,就和C++語(yǔ)言里定義類和對(duì)象的思路完全一樣了。還有一個(gè)區(qū)別是結(jié)構(gòu)體封裝的對(duì)象沒(méi)有好用的private 和protected屬性,不過(guò)C語(yǔ)言也可以通過(guò)掩碼結(jié)構(gòu)體這個(gè)騷操作來(lái)實(shí)現(xiàn)private 和protected的特性。
注:此等操作并不是面向?qū)ο蟊仨毜?,這個(gè)屬于錦上添花的行為,不用也不影響面向?qū)ο蟆?/strong>
先通過(guò)一個(gè)例子直觀體會(huì)一下什么是掩碼結(jié)構(gòu)體,以下例子來(lái)源為:傻孩子的PLOOC的readme,作者倉(cāng)庫(kù)地址:https://github.com/GorgonMeducer/PLOOC
1typedef?struct?__byte_queue_t?{
2????uint8_t???*pchBuffer;
3????uint16_t??hwBufferSize;
4????uint16_t??hwHead;
5????uint16_t??hwTail;
6????uint16_t??hwCount;
7}__byte_queue_t;
8
9typedef?struct?{
10????uint8_t?chMask?[sizeof(struct?__byte_queue_t)];
11}?byte_queue_t;
您甚至可以這樣做…如果您對(duì)內(nèi)容很認(rèn)真的話
1typedef?struct?byte_queue_t?{
2????uint8_t?chMask?[sizeof(struct?{
3????????uint32_t????????:?32;
4????????uint16_t????????:?16;
5????????uint16_t????????:?16;
6????????uint16_t????????:?16;
7????????uint16_t????????:?16;
8????})];
9}?byte_queue_t;
通過(guò)這個(gè)例子,我們可以發(fā)現(xiàn)給用戶提供的頭文件,其實(shí)是一個(gè)固態(tài)存儲(chǔ)器,即使用字節(jié)數(shù)組創(chuàng)建的掩碼,用戶通過(guò)掩碼結(jié)構(gòu)體創(chuàng)建的變量無(wú)法訪問(wèn)內(nèi)部的成員,這就是實(shí)現(xiàn)屬性私有化的方法。至于如何實(shí)現(xiàn)只有類源代碼才能訪問(wèn)私有成員,只有派生類的類源代碼才能訪問(wèn)基類的受保護(hù)成員的特性,這里先埋個(gè)伏筆,關(guān)注本公眾號(hào),后續(xù)文章再深入探討。
還回到掩碼結(jié)構(gòu)體本身的特性上,可以發(fā)現(xiàn)一個(gè)問(wèn)題,單純的掩碼結(jié)構(gòu)體丟失了結(jié)構(gòu)體的對(duì)齊信息:
因?yàn)檠诖a的本質(zhì)是創(chuàng)建了一個(gè)
chMask數(shù)組,我們知道數(shù)組是按照元素對(duì)齊的,因此數(shù)組chMask對(duì)齊到字節(jié),又由于chMask是結(jié)構(gòu)體byte_queue_t的中的對(duì)齊要求最大的那個(gè)元素(也是唯一元素),因此整個(gè)結(jié)構(gòu)體的對(duì)齊就是按字節(jié)對(duì)齊;通過(guò)分析容易發(fā)現(xiàn),原本的結(jié)構(gòu)體中對(duì)齊要求最高的元素是指針
pchBuffer,由于它要求對(duì)齊到word,因此整個(gè)結(jié)構(gòu)體都是按照Word對(duì)齊的。當(dāng)你用掩碼結(jié)構(gòu)體聲明結(jié)構(gòu)體變量的時(shí)候,這個(gè)變量多半不是對(duì)齊到word的而是對(duì)齊到了任意的字節(jié)地址上。更具文章《漫談C變量——對(duì)齊(1)》和《漫談C變量——對(duì)齊(2)》中的介紹,當(dāng)我們我們用指針訪問(wèn)結(jié)構(gòu)體時(shí),如果指針默認(rèn)的對(duì)齊方式與對(duì)象實(shí)際的對(duì)齊方式不符時(shí),就會(huì)引發(fā)“非對(duì)齊訪問(wèn)”——輕則性能下降,重則觸發(fā)hardfault。
為了解決這個(gè)問(wèn)題,可以利用 __alignof__() 來(lái)獲取__byte_queue_t的對(duì)齊值,再使用__attribute__((align))來(lái)指定chMask的對(duì)齊方式。改進(jìn)如下:
1typedef?struct?__byte_queue_t?{????????????????
2????uint8_t???*pchBuffer;
3????uint16_t??hwBufferSize;
4????uint16_t??hwHead;
5????uint16_t??hwTail;
6????uint16_t??hwCount;
7}__byte_queue_t;
8
9typedef?struct?byte_queue_t?{
10????uint8_t?chMask??[sizeof(__byte_queue_t)]??
11????????__attribute__((aligned(__alignof__(__byte_queue_t))));??????????????????
12}?byte_queue_t;
這部分理解起來(lái)可能稍微有點(diǎn)復(fù)雜,但是不理解也沒(méi)關(guān)系,現(xiàn)在先知道有這個(gè)東西,后續(xù)文章還會(huì)有更騷的操作來(lái)更直觀的實(shí)現(xiàn)封裝、繼承和多態(tài)!
5 C語(yǔ)言實(shí)現(xiàn)類的封裝
如果你趟過(guò)了掩碼結(jié)構(gòu)體那條河,那么恭喜你,你已經(jīng)成功上岸了。我們繼續(xù)回到面向?qū)ο蟮膯?wèn)題上,面向?qū)ο蟮暮诵氖前褦?shù)據(jù)和處理數(shù)據(jù)的方法封裝在一起。封裝并不是只有放在同一個(gè)結(jié)構(gòu)體里這一種形式,放在同一個(gè)接口頭文件里(也就是.h)里,也是一種形式——即,一個(gè)接口頭文件提供了數(shù)據(jù)的結(jié)構(gòu)體,以及處理這些數(shù)據(jù)的函數(shù)原型聲明,這已經(jīng)完成了面向?qū)ο笏璧幕疽蟆?/strong>下邊將通過(guò)C語(yǔ)言的具體實(shí)例加以說(shuō)明。
假設(shè)我們要封裝一個(gè)基于字節(jié)的隊(duì)列類,不妨叫做byte_queue_t,因此我們建立了一個(gè)類文件byte_queue.c和對(duì)應(yīng)的接口頭文件byte_queue.h。
byte_queue.h
1//!?the?original?structure?in?class?source?code
2//!?the?masked?structure:?the?class?byte_queue_t?in?header?file
3typedef?struct?byte_queue_t?{
4????uint8_t?chMask??[sizeof(struct?{
5????????uint8_t???*pchBuffer;
6????????uint16_t??hwBufferSize;
7????????uint16_t??hwHead;
8????????uint16_t??hwTail;
9????????uint16_t??hwCount;
10????})]??__attribute__((aligned(__alignof__(struct?{
11????????????uint8_t?*pchBuffer;
12????????????uint16_t??hwBufferSize;
13????????????uint16_t??hwHead;
14????????????uint16_t??hwTail;
15????????????uint16_t??hwCount;
16????????}))));??????????????????
17}?byte_queue_t;
18...
19extern?bool?queue_init(byte_queue_t?*ptQueue,?
20???????????????????????uint8_t?*pchBuffer,?
21???????????????????????uint16_t?hwSize);
22extern?bool?enqueue(byte_queue_t?*ptQueue,?uint8_t?chByte);
23extern?bool?dequeue(byte_queue_t?*ptQueue,?uint8_t?*pchByte);
24extern?bool?is_queue_empty(byte_queue_t?*ptQueue);
25...
byte_queue.c
1#include?"./queue.h"
2
3//!?the?original?structure?in?class?source?code
4typedef?struct?__byte_queue_t?{?
5????uint8_t???*pchBuffer;
6????uint16_t??hwBufferSize;
7????uint16_t??hwHead;
8????uint16_t??hwTail;
9????uint16_t??hwCount;
10}?__byte_queue_t?;
需要注意的是,這里之所以不像前面那樣首先定義類型__byte_queue_t,然后在掩碼結(jié)構(gòu)體byte_queue_t的定義中直接使用__byte_queue_t來(lái)計(jì)算數(shù)組的大小并取得對(duì)齊方式,是因?yàn)椋?/p>
__byte_queue_t 里包含了類的成員信息,我們不希望用戶能夠直接訪問(wèn)這些成員;
用戶使用模塊時(shí)只會(huì)包含 byte_queue.h,因此必然不能直接把__byte_queue_t放置到該頭文件中;
基于上述考慮,byte_queue.h 的掩碼結(jié)構(gòu)體定義只能自己再抄寫一份;
目前這種方式是“防君子不妨小人的”,但如果我們真正不想暴露任何成員信息給用戶時(shí),可以考慮使用前面介紹過(guò)的完全抹去成員變量名稱的方式——在這種情況下就更不能將__byte_queue_t 放置到 byte_queue.h 中了。
可以看到,實(shí)際上類型byte_queue_t是一個(gè)掩碼結(jié)構(gòu)體,里面只有一個(gè)起到掩碼作用的數(shù)組chMask,其大小、對(duì)齊方式和真正后臺(tái)的的類型__byte_queue_t相同——這就是掩碼結(jié)構(gòu)體實(shí)現(xiàn)私有成員保護(hù)的秘密。 解決了私有成員保護(hù)的問(wèn)題,剩下還有一個(gè)問(wèn)題,對(duì)于byte_queue.c的函數(shù)來(lái)說(shuō)byte_queue_t只是一個(gè)數(shù)組,那么正常的功能要如何實(shí)現(xiàn)呢?下面的代碼片斷將為你解釋一切:
1...
2#define?__class(__NAME)??????????????????__##__NAME
3#define?class(__NAME)???????????????????__class(__NAME)???
4#ifndef?this
5#???define?this????????????????????????????(*ptThis)
6#endif
7
8bool?is_queue_empty(byte_queue_t?*ptQueue)
9{
10????class(byte_queue_t)?*ptThis?=?(class(byte_queue_t)?*)ptQueue;
11????if?(NULL?==?ptQueue)?{
12????????return?true;
13????}
14????return?((this.hwHead?==?this.hwTail)?&&?(0?==?this.hwCount));
15}
16...
可以從這里看出來(lái),只有類的源文件才能看到內(nèi)部使用的結(jié)構(gòu)體,而掩碼結(jié)構(gòu)體是模塊內(nèi)外都可以看到的,簡(jiǎn)單來(lái)說(shuō),如果實(shí)際內(nèi)部的定義為外部的模塊所能直接看見,那自然就沒(méi)有辦法起到保護(hù)作用。
從編譯器的角度來(lái)說(shuō),這種從byte_queue_t到__byte_queue_t類型指針的轉(zhuǎn)義是邏輯上的,并不會(huì)因此產(chǎn)生額外的代碼,簡(jiǎn)而言之,使用掩碼結(jié)構(gòu)體幾乎是沒(méi)有代價(jià)的。
再次強(qiáng)調(diào):實(shí)現(xiàn)面向?qū)ο螅诖a結(jié)構(gòu)體并不是必須的,只是錦上添花,所以不理解的話,也不要糾結(jié)
想要更深入了解C語(yǔ)言面向?qū)ο蟮乃枷?,建議參考的書籍:《UML+OOPC嵌入式C語(yǔ)言開發(fā)精講》

如果你覺(jué)得文章還不錯(cuò),就請(qǐng)點(diǎn)擊右上角選擇發(fā)送給朋友或者轉(zhuǎn)發(fā)到朋友圈。您的支持和鼓勵(lì)是我們最大的動(dòng)力。喜歡就請(qǐng)關(guān)注我們吧~
長(zhǎng)按二維碼
關(guān)注我們


