電子產(chǎn)業(yè)一站式賦能平臺(tái)

PCB聯(lián)盟網(wǎng)

搜索
查看: 67|回復(fù): 0
收起左側(cè)

C 也能玩轉(zhuǎn)面向?qū)ο?一文掌握PLOOC核心

[復(fù)制鏈接]

491

主題

491

帖子

3115

積分

四級(jí)會(huì)員

Rank: 4

積分
3115
跳轉(zhuǎn)到指定樓層
樓主
發(fā)表于 2024-8-27 11:38:00 | 只看該作者 |只看大圖 回帖獎(jiǎng)勵(lì) |倒序?yàn)g覽 |閱讀模式
【說在前面的話】

“為什么要使用C語言來實(shí)現(xiàn)面向?qū)ο箝_發(fā)?”

“直接用C++不就好了么?”
想必很多人在第一次面對(duì) OOPC(Object-Oriented-Programming-with-ANSI-C)的時(shí)候,都會(huì)情不自禁的發(fā)出類似的疑問。其實(shí),任何針對(duì)上述問題的討論,其本身都是充滿爭(zhēng)議的——換句話說,無論我給出怎樣的答案,都無法令所有人滿意——正因如此,本文也無意去趟這攤渾水。
我寫這篇文章的目的是為那些長(zhǎng)期在MDK環(huán)境下從事C語言開發(fā)的朋友介紹一種方法:幫助大家在偶爾需要用到“面向?qū)ο蟆备拍畹臅r(shí)候,能簡(jiǎn)便快捷的使用C語言“搞定”面向?qū)ο箝_發(fā)。
在開始后續(xù)內(nèi)容之前,我們需要約定和強(qiáng)調(diào)一些基本原則:
  • “零消耗”原則:即,我們所要實(shí)現(xiàn)的所有面向?qū)ο蟮奶匦远紤?yīng)該是“零資源消耗”或至少是“極小資源消耗”。這里的原理是:能在編譯時(shí)刻(Compiletime)搞定的事情,絕不拖到運(yùn)行時(shí)刻(Runtime)。
  • 務(wù)實(shí)原則:即,我們不在形式上追求與C++類似,除非它的代價(jià)是零或者非常小。
  • “按需實(shí)現(xiàn)”原則:即,對(duì)任何類的實(shí)現(xiàn)來說,我們并不追求把所有的OO特性都實(shí)現(xiàn)出來——這完全沒有必要——我們僅根據(jù)實(shí)際應(yīng)用的需求來實(shí)現(xiàn)最小的、必要的面向?qū)ο蠹夹g(shù)。
  • “傻瓜化”原則:即,類的建立和使用都必須足夠傻瓜化。最好所見即所得。

    在上述前提下,我們就快速進(jìn)入到今天的內(nèi)容吧。
    【僅需一次的準(zhǔn)備階段】首先,我們要下載 PLOOC的 CMSIS-Pack,具體鏈接如下:
    https://github.com/GorgonMeducer/PLOOC/releases
    當(dāng)然,如果你因?yàn)槟承┰驘o法訪問Github,也可以在關(guān)注【裸機(jī)思維】公眾號(hào)后發(fā)送關(guān)鍵字 “PLOOC” 來獲取網(wǎng)盤鏈接。
    下載成功后,直接雙擊安裝包即可。

    一般來說,部署會(huì)非常順利,但如果出現(xiàn)了安裝錯(cuò)誤,比如下面這種:




    則很可能是您所使用的MDK版本太低導(dǎo)致的——是時(shí)候更新下MDK啦。關(guān)注【裸機(jī)思維】公眾號(hào)后發(fā)送關(guān)鍵字"MDK",即可獲得最新的MDK網(wǎng)盤鏈接。

    PLOOCProtected-Low-overhead-Object-Oriented-programming-with-ansi-C 的縮寫,顧名思義,是一個(gè)強(qiáng)調(diào)地資源消耗且為私有類成員提供保護(hù)的一個(gè)面向?qū)ο竽0濉?br /> 它是一個(gè)開源項(xiàng)目,如果你喜歡,還請(qǐng)多多Star哦!
    https://github.com/GorgonMeducer/PLOOC

    【如何快速嘗鮮】
    為了簡(jiǎn)化用戶對(duì) OOC 的學(xué)習(xí)成本,PLOOC提供了一個(gè)無需任何硬件就可以直接仿真執(zhí)行的例子工程。該例子工程以隊(duì)列類為例子,展示了:類的定義方式
    如何實(shí)現(xiàn)類的方法(Method)
    如何為類定義接口(Interface)
    如何定義派生類
    如何重載接口
    如何在派生類中訪問基類受保護(hù)的成員(Protected Member)
  • ……

    很多時(shí)候千言萬語敵不過代碼幾行——學(xué)習(xí)OOC確是如此。
    例子工程的獲取非常簡(jiǎn)單。首先打開 Pack-Installer,在Device列表中找到Arm,選擇任意一款Cortex-M內(nèi)核(比如 Arm Cortex-M3)。在列表中選擇ARMCMx(比如下圖中的ARMCM3)。

    此時(shí),在右邊的Example選項(xiàng)卡中,就可以看到最底部出現(xiàn)了一個(gè)名為 plooc_example (uVision Simulator)的例子工程。單擊Copy,在彈出窗口中選擇一個(gè)目錄位置來保存工程:


    單擊OK后將打開自動(dòng)打開如下所示的 MDK 界面:


    直接單擊編譯,如果一切順利,應(yīng)該沒有任何編譯錯(cuò)誤:


    此時(shí),我們可以直接進(jìn)入調(diào)試模式:


    可以看到,調(diào)試指針停在了 main() 函數(shù)的起始位置。我們先不著急開始全速運(yùn)行。通過菜單打開 "Debug (printf) Viewer" 窗口:


    一開始該窗口會(huì)出現(xiàn)在屏幕下方的窗體中,通過拖動(dòng)的方式,我們可以將其挪到醒目的位置。此時(shí),全速運(yùn)行就可以看到例子工程所要展示的效果了:



    如果你無法在Debug (printf) Viewer 中看到輸出,則很可能是沒有正確配置調(diào)試的方式:

    選擇 Models Cortex-M Debugger 以后,單擊 Settings 按鈕。單擊Command 文本框邊上的"..."按鈕,找到 Keil_v5 安裝目錄下的VHT(或者FVP)所在的路徑。注意要選名稱中 Cortex-M3 為后綴的可執(zhí)行文件。需要注意的是,不同版本的MDK VHT/FVP 的路徑可能存在差異,但一定是保存在Keil_v5/Arm目錄下的一個(gè)子目錄里。設(shè)置好路徑后,我們單擊 Target 邊上的 "..." 按鈕來測(cè)試 FVP 是否工作正常:


    如果一切順利,我們會(huì)看到如下的窗口,注意勾選 armcortexm3ct 選項(xiàng)(默認(rèn)就是勾選的)。單擊OK即可。


    如果你不幸看到的是這樣的錯(cuò)誤窗口:


    則說明你的MDK不是Professional License。安裝Professional License一般都可以解決。
    一切設(shè)置妥當(dāng)后,單擊OK關(guān)閉配置頁面即可。

    該例子只展示了C99模式下使用PLOOC所構(gòu)建的隊(duì)列類(enhanced_byte_queue_t)的效果:
  • static enhanced_byte_queue_t s_tQueue;
        printf("Hello PLOOC!\r
    \r
    ");        static uint8_t s_chQueueBuffer[QUEUE_BUFFER_SIZE];        enhanced_byte_queue_t *ptQueue         = __new_class(enhanced_byte_queue,                       s_chQueueBuffer,                      sizeof(s_chQueueBuffer));
        //! you can enqueue    ENHANCED_BYTE_QUEUE.Enqueue(&s_tQueue, 'p');    ENHANCED_BYTE_QUEUE.Enqueue(&s_tQueue, 'L');    ENHANCED_BYTE_QUEUE.Enqueue(&s_tQueue, 'O');    ENHANCED_BYTE_QUEUE.Enqueue(&s_tQueue, 'O');    ENHANCED_BYTE_QUEUE.Enqueue(&s_tQueue, 'C');        ENHANCED_BYTE_QUEUE.use_as__i_byte_queue_t.Enqueue(&s_tQueue.use_as__byte_queue_t, '.');    ENHANCED_BYTE_QUEUE.use_as__i_byte_queue_t.Enqueue(&s_tQueue.use_as__byte_queue_t, '.');    ENHANCED_BYTE_QUEUE.use_as__i_byte_queue_t.Enqueue(&s_tQueue.use_as__byte_queue_t, '.');
        //! you can dequeue    do {        uint_fast16_t n = ENHANCED_BYTE_QUEUE.Count(&s_tQueue);        uint8_t chByte;        printf("There are %d byte in the queue!\r
    ", n);        printf("let's peek!\r
    ");                while(ENHANCED_BYTE_QUEUE.Peek.PeekByte(&s_tQueue, &chByte)) {            printf("%c\r
    ", chByte);        }        printf("There are %d byte(s) in the queue!\r
    ",                 ENHANCED_BYTE_QUEUE.Count(&s_tQueue));        printf("Let's remove all peeked byte(s) from queue... \r
    ");        ENHANCED_BYTE_QUEUE.Peek.GetAllPeeked(&s_tQueue);        printf("Now there are %d byte(s) in the queue!\r
    ",                 ENHANCED_BYTE_QUEUE.Count(&s_tQueue));    } while(0);        __free_class(enhanced_byte_queue, ptQueue);
    其輸出為:


    enhanced_byte_queue_t 實(shí)際上是從基類 byte_queue_t 基礎(chǔ)上派生出來的,并添加了一個(gè)非常有用的功能:可以連續(xù)的偷看(Peek)隊(duì)列里的內(nèi)容,并可以在需要的時(shí)候,要么1)將已經(jīng)偷看的內(nèi)容實(shí)際都取出來;要么2)從頭開始偷看——上述代碼就展示了這一功能。
    PLOOC 相較普通的OOC模板來說,除了可以隱藏類的私有成員(private member)以外,還能夠以零運(yùn)行時(shí)成本實(shí)現(xiàn)重載(Overload)——用通俗的話說就是:PLOOC允許擁有不同參數(shù)數(shù)量、不同參數(shù)類型的多個(gè)函數(shù)擁有相同的名字。
    要獲得這樣的功能,就要打開 C11(最好是GNU11)的支持。當(dāng)我們打開工程配置,在“C/C++”選項(xiàng)卡中將 Language C 設(shè)置為 c11(最好是gnu11):

    重新編譯后,進(jìn)入調(diào)試模式,將在輸出窗口中看到額外的信息:



    這些信息實(shí)際上對(duì)應(yīng)如下的代碼:
  • #if defined(__STDC_VERSION__) && __STDC_VERSION__ > 199901L    LOG_OUT("\r
    -[Demo of overload]------------------------------\r
    ");    LOG_OUT((uint32_t) 0x12345678);    LOG_OUT("\r
    ");    LOG_OUT(0x12345678);    LOG_OUT("\r
    ");    LOG_OUT("PI is ");    LOG_OUT(3.1415926f);    LOG_OUT("\r
    ");        LOG_OUT("\r
    Show BYTE Array:\r
    ");    LOG_OUT((uint8_t *)main, 100);
        LOG_OUT("\r
    Show Half-WORD Array:\r
    ");    LOG_OUT((uint16_t *)(((intptr_t)&main) & ~0x1), 100/sizeof(uint16_t));
        LOG_OUT("\r
    Show WORD Array:\r
    ");    LOG_OUT((uint32_t *)(((intptr_t)&main) & ~0x3), 100/sizeof(uint32_t));#endif
    你看,同一個(gè)函數(shù) LOG_OUT() 當(dāng)我們給它不同數(shù)量和類型的參數(shù)時(shí),居然可以實(shí)現(xiàn)不同的輸出效果,是不是特別神奇——這就是面向?qū)ο箝_發(fā)中重載的魅力所在。請(qǐng)記。此時(shí)我們?nèi)匀皇褂玫氖荂語言,而不是C++;
  • 在C99下,我們可以實(shí)現(xiàn)擁有不同參數(shù)個(gè)數(shù)的函數(shù)共享同一個(gè)名字;
  • 在C11下,我們可以實(shí)現(xiàn)擁有相同參數(shù)個(gè)數(shù)但類型不同的函數(shù)共享同一個(gè)名字;
  • 我們?cè)谶\(yùn)行時(shí)刻的開銷是0,一切在編譯時(shí)刻就已經(jīng)塵埃落定了。我們并沒有為這項(xiàng)特性犧牲任何代碼空間。

    例子工程可以幫助我們快速的熟悉 OOC 的開發(fā)模式,那么在任意的普通工程中,我們要如何使用 PLOOC模板呢?
    【PLOOC在任意普通工程中的部署】PLOOC 模板其實(shí)是一套頭文件,既沒有庫(lib)也沒有C語言源代碼,更別提匯編了。
    在任意的MDK工程中,只要你已經(jīng)安裝了此前我們提到過的CMSIS-Pack,就可以通過下述工具欄中標(biāo)注的按鈕,打開RTE配置界面:


    找到 Language Extension選項(xiàng),將其展開后勾選PLOOC,單擊OK關(guān)閉窗口。

    此時(shí),我們就可以在工程管理器中看到一個(gè)新的列表項(xiàng)“Language Extension”:



    它是不可展開的,別擔(dān)心,這就足夠了。打開工程配置,如果你使用的是 Arm Compiler 6(armclang)

    則我們需要在 C/C++選項(xiàng)中:
  • 將Language C設(shè)置為 gnu11(或者最低c99):
  • (推薦,而不是必須)在Misc Controls中添加對(duì)微軟擴(kuò)展的支持
  • -fms-extensions



    如果你使用的是 Arm Compiler 5(armcc)


    則需要在 C/C++ 選項(xiàng)卡中開啟對(duì) GNU ExtensionC99的支持:


    遺憾的是作為一款已經(jīng)停止更新的編譯器,Arm Compiler 5 既不支持C11,也不支持微軟擴(kuò)展(-fms-extensions),這意味著PLOOC中的重載特性無法發(fā)揮最大潛能,著實(shí)有點(diǎn)遺憾(但擁有不同參數(shù)數(shù)量的函數(shù)還是允許共享同一個(gè)名稱的)。
    至此,我們就完成了PLOOC在一個(gè)工程中的部署。是不是特別簡(jiǎn)單?

    也許文章到了一半我才問,已經(jīng)有點(diǎn)遲了——大家都熟悉基本的面向?qū)ο蟾拍畎?比如?br /> 類(class)
    私有成員(private member)
    公共成員(public member)
    保護(hù)成員(protected member)
    構(gòu)造函數(shù)(constructor)
    析構(gòu)函數(shù)(destructor)
    類的方法(method)
    ……

    如果不熟悉,還請(qǐng)找本C#或者C++的書略微學(xué)習(xí)一下為好。后面的內(nèi)容,我將假設(shè)你已經(jīng)對(duì)面向?qū)ο蟮幕鹃_發(fā)要素較為熟悉。
    那么,我們?nèi)绾慰焖俚脑贑語言工程中構(gòu)建一個(gè)類呢?
    新建一個(gè)類從未如此簡(jiǎn)單】假設(shè)我們要?jiǎng)?chuàng)造一個(gè)新的類,叫做 my_class1

    第一步:引入模板在工程管理器中,添加一個(gè)新的group,命名為 my_class1



    右鍵單擊 my_class1,并在彈出的菜單中選擇 "Add New Item to Group my_class1":



    在彈出的對(duì)話框中選擇 User Code Template:



    展開 Language Extension,可以看到有兩個(gè) PLOOC模板,分別對(duì)應(yīng):
    基類和普通類(Base Class Template)派生類(Derived Class Template)

    由于我們要?jiǎng)?chuàng)建的是一個(gè)普通類(未來也可以作為基類),因此選擇“Base Class Template”。單擊Location右邊的 "..." 按鈕,選擇一個(gè)保存代碼文件的路徑后,單擊“Add”。

    此時(shí)我們可以看到,class_name.c 被添加到了 my_class1中,且MDK自動(dòng)在編輯器中為我們打開了兩個(gè)模板文件:class_name.hclass_name.c。




    第二步:格式化在編輯器中打開或者選中 class_name.c。通過快捷鍵CTRL+H打開 替換窗口:
    在Look in中選擇Current Document去掉Find Opitons屬性框中的 Match whold word前的勾選這一步驟很重要


    接下來,依次:
  • 將小寫的 替換為 my_class1
  • 將大寫的 替換為 MY_CLASS1
    完成上述步驟后,保存class_name.c。打開class_name.h,重復(fù)上述過程,即:
  • 將小寫的 替換為 my_class1
  • 將大寫的 替換為 MY_CLASS1
    完成后保存 class_name.h.
    第三步:加入工程編譯在工程管理器中展開 my_class1,并將其中的 class_name.c 刪除:




    打開class_name.c 所在文件目錄:




    找到我們剛剛編輯好的兩個(gè)文件 class_name.cclass_name.h



    用我們的類為這兩個(gè)文件命名:my_class1.cmy_class1.h



    在MDK工程管理器中,將這兩個(gè)文件加入 my_class1 下:



    如果此前你的工程就是可以正常編譯的話,在加入了上述文件后,應(yīng)該依然可以正常編譯:


    第四步:如何設(shè)計(jì)你的類成員變量打開 my_class1.h,找到 def_class 所在的代碼片斷:

  • //!
    ame class my_class1_t//! @{declare_class(my_class1_t)
    def_class(my_class1_t,
        public_member(        //!    )
        private_member(        //!    )        protected_member(        //!    ))
    end_def_class(my_class1_t) /* do not remove this for forward compatibility  *///! @}
    很容易注意到:
  • 類所對(duì)應(yīng)的類型會(huì)自動(dòng)在尾部添加 "_t" 以表示這是一個(gè)自定義類型,當(dāng)然這不是強(qiáng)制的,當(dāng)你熟悉模板后,如果確實(shí)看它不順眼,可以改成任何自己喜歡的類型名稱。這里,由于我們的類叫做 my_class1,因此對(duì)應(yīng)的類型就是 my_class1_t。

  • declare_class(或者也可以寫成 dcl_class)用于類型的“前置聲明”,它的本質(zhì)就是
  • typedef struct my_class1_t my_class1_t;因此并沒有什么特別神秘的地方。
  • def_class用于定義類的成員。其中 public_member用于存放公共可見的成員變量;private_member用于存放私有成員;protected_member用于存放當(dāng)前類以及派生類可見的成員。這三者的順序任意,可以缺失,也可以存在多個(gè)——非常靈活。

    第四步:如何設(shè)計(jì)構(gòu)造函數(shù)

    找到 typedef struct my_class1_cfg_t 對(duì)應(yīng)的代碼塊:
  • typedef struct my_class1_cfg_t {        //! put your configuration members here    } my_class1_cfg_t;

    可以看到,這是個(gè)平平無奇的結(jié)構(gòu)體。它用于向我們的構(gòu)造函數(shù)傳遞初始化類時(shí)所需的參數(shù)。在類的頭文件中,你很容易找到構(gòu)造函數(shù)的函數(shù)原型:

  • /*! \brief the constructor of the class: my_class1 */externmy_class1_t * my_class1_init(my_class1_t *ptObj, my_class1_cfg_t *ptCFG);
    可以看到,其第一個(gè)參數(shù)是指向類實(shí)例的指針,而第二個(gè)參數(shù)就是我們的配置結(jié)構(gòu)體。在類的C源代碼文件中,可以找到構(gòu)造函數(shù)的實(shí)體:

  • #undef this#define this        (*ptThis)/*! \brief the constructor of the class: my_class1 */my_class1_t * my_class1_init(my_class1_t *ptObj, my_class1_cfg_t *ptCFG){    /* initialise "this" (i.e. ptThis) to access class members */    class_internal(ptObj, ptThis, my_class1_t);    ASSERT(NULL != ptObj && NULL != ptCFG);
        return ptObj;}
    此時(shí),在構(gòu)造函數(shù)中,我們可以通過 this.xxxx 的方式來訪問類的成員,以便根據(jù)配置結(jié)構(gòu)體中傳進(jìn)來的內(nèi)容對(duì)類進(jìn)行初始化。

    也許你已經(jīng)注意到了,我們的模板中并沒有任何為類申請(qǐng)空間的代碼。這是有意為之。原因如下:
  • 面向?qū)ο蟛⒎且欢ㄒ褂脛?dòng)態(tài)內(nèi)存分配,這是一種偏見
  • 我們只提供構(gòu)造函數(shù),而類的用戶可以自由的決定如何為類的實(shí)例分配存儲(chǔ)空間。
  • 由于我們創(chuàng)造的類(比如 my_class1_t)本質(zhì)上是一個(gè)完整的結(jié)構(gòu)體類型,因此可以由用戶像普通結(jié)構(gòu)體那樣:
    進(jìn)行靜態(tài)分配:即定義靜態(tài)變量,或是全局變量
    使用池分配:直接為目標(biāo)類構(gòu)建一個(gè)專用池,有效避免碎片化。
  • 進(jìn)行堆分配:使用普通的malloc()進(jìn)行分配,類的大小可以通過sizeof() 獲得,比如:
  • my_class1_cfg_t tCFG = {    ...};my_class1_t *ptNewItem = my_class1_init(     (my_class1_t *)malloc(sizeof(my_class1_t),     &tCFG);if (NULL == ptNewItem) {    printf("Failed to new my_class1_t \r
    ");}
    ...
    free(ptNewItem);
    當(dāng)然,如果你說我就是要那種形式主義,那你完全可以使用關(guān)鍵字__new_class:
  • __new_class(類型名稱>,構(gòu)造函數(shù)的參數(shù)>);__free_class(類型名稱>, 實(shí)例地址>)
    比如:
  • my_class1_t *ptItem = __new_class(my_class, );if (NULL == ptItem) {    printf("Failed to new my_class1_t \r
    ");}
    ...
    __free_class(my_class, ptItem);
    第五步:如何設(shè)計(jì)構(gòu)類的方法(method)
    我們開篇說過,實(shí)踐面向?qū)ο笞钪匾氖枪δ埽切问街髁x。假設(shè)有一個(gè)類的方法叫做 method1,理想中,大家一定覺得如下的使用方式是最“正統(tǒng)”的:
  • my_class1_t *ptItem = new_class(my_class, );if (NULL == ptItem) {    printf("Failed to new my_class1_t \r
    ");}
    ptItem.method1();
    free_class(my_class, ptItem);在C語言中,我們完全可以實(shí)現(xiàn)類似的效果——只要你在類的定義中加入函數(shù)指針就行了——其實(shí)很多OOC的模板都是這么做的(比如lw_oopc)。但你仔細(xì)思考一下,在類的結(jié)構(gòu)體中加入函數(shù)指針究竟有何利弊:
    先來說好處:可以用“優(yōu)雅”的方式來完成方法的調(diào)用;
    支持運(yùn)行時(shí)刻的多態(tài)(Polymorphism);

    再來說缺點(diǎn):
    在嵌入式應(yīng)用中,大部分類的方法都不需要多態(tài),更別說是運(yùn)行時(shí)刻的多態(tài)了;
    函數(shù)指針會(huì)占用4個(gè)字節(jié);
    通過函數(shù)指針來實(shí)現(xiàn)的間接調(diào)用,其效率低于普通的函數(shù)直接調(diào)用。

    換句話說,對(duì)大部分類的大部分情況來說,我們都不需要考慮類的方法多態(tài)問題,就算有,很多時(shí)候也都是編譯時(shí)刻的靜態(tài)多態(tài)(plooc_example就展示了靜態(tài)多態(tài)的實(shí)現(xiàn)方式),那么在不考慮運(yùn)行時(shí)刻動(dòng)態(tài)多態(tài)的應(yīng)用場(chǎng)景下,直接用普通函數(shù)來實(shí)現(xiàn)類的方法就是務(wù)實(shí)的一個(gè)選擇了。
    基于這種考慮,上述例子實(shí)際上應(yīng)該寫為:
  • my_class1_t *ptItem = new_class(my_class, );if (NULL == ptItem) {    printf("Failed to new my_class1_t \r
    ");}
    my_class1_method1(ptItem,);
    free_class(my_class, ptItem);這里,my_class1_method1()my_class1.h 提供聲明、my_class1.c 提供實(shí)現(xiàn)的一個(gè)函數(shù)。前綴 my_class1_ 用于防止命名空間污染。
    另外一個(gè)值得注意的細(xì)節(jié)是,OOPC中,任何類的方法,其函數(shù)的第一個(gè)參數(shù)一定是指向類實(shí)例的指針——也就是我們常說的 this 指針。以 my_class1_method1() 為例,它的形式為:
  • #undef this#define this        (*ptThis)
    void my_class1_method(my_class1_t *ptObj, ){    /* initialise "this" (i.e. ptThis) to access class members */    class_internal(ptObj, ptThis, my_class1_t);        ...    }這里,class_internal() 用于將 ptObj轉(zhuǎn)變成我們所需的 this指針(這里的ptThis),借助宏的幫助,我們就可以實(shí)現(xiàn)  this.xxxx 這樣無成本的形式主義了。
    第六步:如何設(shè)計(jì)類的接口(Interface)我們的模板還為每個(gè)類都提供了一個(gè)接口,并默認(rèn)將構(gòu)造和析構(gòu)函數(shù)都包含在內(nèi),比如,我們可以較為優(yōu)雅的對(duì)類進(jìn)行構(gòu)造和析構(gòu):

  • static my_class1_t s_tMyClass;...MY_CLASS.Init(&s_tMyClass, ...);...MY_CLASS.Depose(&s_tMyClass);
    在 my_class1.h 中,我們可以找到這樣的結(jié)構(gòu):
  • //!
    ame interface i_my_class1_t//! @{def_interface(i_my_class1_t)    my_class1_t *  (*Init)       (my_class1_t *ptObj, my_class1_cfg_t *ptCFG);    void           (*Depose)     (my_class1_t *ptObj);    /* other methods */
    end_def_interface(i_my_class1_t) /*do not remove this for forward compatibility *///! @}
    假設(shè)我們要加入一個(gè)新的方法,則只需要在 i_my_class1_t 的接口定義中添加對(duì)應(yīng)的函數(shù)指針即可,比如:
  • //!
    ame interface i_my_class1_t//! @{def_interface(i_my_class1_t)    my_class1_t *  (*Init)       (my_class1_t *ptObj, my_class1_cfg_t *ptCFG);    void           (*Depose)     (my_class1_t *ptObj);    /* other methods */    void           (*Method1)    (my_class1_t *ptObj, );end_def_interface(i_my_class1_t) /*do not remove this for forward compatibility *///! @}接下來,我們要在 my_class1.h 中添加對(duì)應(yīng)方法的函數(shù)聲明:
  • externvoid my_class1_method1(my_class1_t *ptObj, );這里,值得注意的是,習(xí)慣上函數(shù)的命名上與接口除大小寫歪,還有一個(gè)簡(jiǎn)單的對(duì)應(yīng)關(guān)系:即,所有的"."直接替換成"_",比如,使用上:
  • MY_CLASS1.Method1()就對(duì)應(yīng)為:
  • my_class1_method1()
    與此同時(shí),我們需要在 my_class1.c 中添加 my_class1_method1() 函數(shù)的實(shí)體:
  • void my_class1_method1(my_class1_t *ptObj, ){    class_internal(ptObj, ptThis, my_class1_t);    ...}并找到名為 MY_CLASS1 的接口實(shí)例:
  • const i_my_class1_t MY_CLASS1 = {    .Init =             &my_class1_init,    .Depose =           &my_class1_depose,        /* other methods */};在其中初始化我們的新方法(新函數(shù)指針) Method1
  • const i_my_class1_t MY_CLASS1 = {    .Init =             &my_class1_init,    .Depose =           &my_class1_depose,        /* other methods */    .Method1 =          &my_class1_method1,};至此,我們就完成了類方法的添加和初始化。以后,在任何地方,都可以通過
  • 類名大寫>.接口中方法名>()的形式來訪問類的操作函數(shù)了——這也算某種程度上的優(yōu)雅了吧。
    第六步:如何設(shè)計(jì)派生類(Derived Class)派生類的創(chuàng)建在基本步驟上與普通類基本一致,除了在模板選擇階段使用對(duì)應(yīng)的模板外,還需要在“格式化”階段額外添加以下兩個(gè)替換步驟:
  • [B] 替換為 基類的大寫名稱;
  • 替換為基類的小寫名稱;
    在類的定義階段,我們注意到:
  • //!
    ame class _t//! @{declare_class(_t)
    def_class(_t,  which(implement(_t))    ...)
    end_def_class(_t) /* do not remove this for forward compatibility  *///! @}派生類在原有的類定義基礎(chǔ)上多出了的結(jié)構(gòu),以"," 與類的類型名隔開:
  • which(implement(base_class_name>_t))這里,which() 其實(shí)是一個(gè)列表,它允許我們實(shí)現(xiàn)多重繼承。假設(shè)我們有多個(gè)基類,或是要繼承多個(gè)接口,則可以寫成如下的形式:
  • which(    implement(_t)    implement(_t)    implement(_t)    implement(_t))需要注意的是,如果基類或是接口中存在名稱沖突(重名)的成員,則可以將 implement() 替換為  inherit() 來避免這種沖突。比如 _t_t 都有一個(gè)叫做 wID 的成員,則可以通過將其中之一的implement() 替換為 inherit的方式在新的派生類中避免沖突:
  • which(    inherit(_t)    implement(_t)    implement(_t)    implement(_t))
    就像這里所展示的那樣,PLOOC支持多繼承,這也是 使用C語言來實(shí)現(xiàn)OO的魅力之一,具體方法,這里就不再贅述。
    大家都知道,在面向?qū)ο笾,有一類成員只有當(dāng)前類和派生類能夠訪問——我們稱之為受保護(hù)成員(protected member)。在類的定義中,可以通過 protected_member() 將這些成員囊括起來,比如:
  • //!
    ame class byte_queue_t//! @{declare_class(byte_queue_t)
    def_class(byte_queue_t,
        private_member(        implement(mem_t)                    //!        void        *pTarget;               //!    )        protected_member(        uint16_t    hwHead;                 //!        uint16_t    hwTail;                 //!        uint16_t    hwCount;                //!    ))
    end_def_class(byte_queue_t) /* do not remove this for forward compatibility  *///! @}這里,hwHead、hwTail和hwCount 都只有當(dāng)前類和派生類能訪問。
    對(duì)于那些只允許派生類訪問的方法(函數(shù))來說,我們一般會(huì)使用預(yù)編譯宏的形式將其有條件的保護(hù)起來:
  • protected_method(    extern mem_t byte_queue_buffer_get(byte_queue_t *ptObj);)這里,受到關(guān)鍵字 protected_method() 的保護(hù),函數(shù) byte_queue_buffer_get() 僅能夠允許類 byte_queue_t 自身極其派生類才能訪問了。
    在我們前面創(chuàng)建的 my_class1.h 中我們也有一個(gè)類似的例子:
  • protected_method(    extern void my_class1_protected_method_example1(my_class1_t *ptObj);
        extern void my_class1_protected_method_example2(my_class1_t *ptObj);)
    函數(shù) my_class1_protected_method_example() 就是一個(gè)僅供 my_class1 極其派生類訪問的 受保護(hù)的方法。
    在派生類中,如果要訪問基類的受保護(hù)成員,則可以借助 protected_internal() 的幫助,例如:
  • #undef this#define this        (*ptThis)
    #undef base#define base        (*ptBase)
    void enhanced_byte_queue_peek_reset(enhanced_byte_queue_t *ptObj){    /* initialise "this" (i.e. ptThis) to access class members */    class_internal(ptObj, ptThis, enhanced_byte_queue_t);    /* initialise "base" (i.e. ptBase) to access protected members */    protected_internal(&this.use_as__byte_queue_t, ptBase, byte_queue_t);        ASSERT(NULL != ptObj);    /* ------------------atomicity sensitive start---------------- */    this.hwPeek = base.hwTail;    this.hwPeekCount = base.hwCount;    /* ------------------atomicity sensitive end---------------- */}這里,派生類借助 this.use_as__byte_queue_t 獲得了對(duì)基類的“引用”,并借助  protected_internal() 將其轉(zhuǎn)化為了名為 ptBase 的指針。在 base 宏的幫助下,我們得以通過  base.xxxx 來訪問基類的成員。在例子中,我們看到,base.hwTailbase.hwCount 正是前面所展示過的 byte_queue_t 的受保護(hù)成員。
    【說在后面的話】
    無論使用何種模板,OOPC來發(fā)的一個(gè)核心理念應(yīng)該是“務(wù)實(shí)”,即:以最小的成本(最好是零成本),占最大的便宜(來自O(shè)O所帶來的好處)。

    此前,我曾經(jīng)在文章《真刀真槍模塊化(2.5)—— 君子協(xié)定》詳細(xì)介紹過PLOOC的原理和手動(dòng)部署技術(shù)。借助CMSIS-PackMDKRTE的幫助,原本繁瑣的手動(dòng)部署和類的創(chuàng)建過程得到了空前的簡(jiǎn)化,使用OOPC進(jìn)行開發(fā)從未如此簡(jiǎn)單過——幾乎與直接使用C++相差無幾了。

    不知不覺間,從2年前第一次將其公開算起,PLOOC已經(jīng)斬獲了三百多個(gè)Star——算是我倉庫中的明星工程了。從日志上來看,PLOOC相當(dāng)穩(wěn)定。距離我上一次“覺得其有必要更新”還是整整一年多前的事情,而加入CMSIS-Pack只是一件錦上添花的事情。


    最后,感謝大家的支持——是你們的Star支撐著我一路對(duì)項(xiàng)目的持續(xù)更新。謝謝!
    猜你喜歡:
    WiFi6+藍(lán)牙+星閃,三合一開發(fā)板,真香!
    Github上熱門 C 語言項(xiàng)目匯總!
    嵌入式,可測(cè)試性軟件設(shè)計(jì)!
    一些低功耗軟件設(shè)計(jì)的要點(diǎn)!
    嵌入式 C 保護(hù)結(jié)構(gòu)體的方式
    實(shí)用 | 10分鐘教你通過網(wǎng)頁點(diǎn)燈
    談?wù)勄度胧杰浖募嫒菪裕?/strong>
  • 發(fā)表回復(fù)

    本版積分規(guī)則


    聯(lián)系客服 關(guān)注微信 下載APP 返回頂部 返回列表