面向?qū)ο蟮奶卣魇浅橄蟆?a class="article-link" target="_blank" href="/baike/492719.html">封裝、繼承、多態(tài),理論很多,前面關(guān)于軟件設(shè)計思想的有多篇拙見,如《嵌入式軟件的設(shè)計模式(上)》、《嵌入式軟件的設(shè)計模式(下)》。對于嵌入式C軟件開發(fā)采用面向?qū)ο蟮姆椒?,只是停留在文字描述。結(jié)合個人經(jīng)驗和周立功《抽象接口技術(shù)和組件開發(fā)規(guī)范及其思想》,循序漸進(jìn)的用代碼范例說明,最好有一點(diǎn)點(diǎn)C++基礎(chǔ)。間接說明理論指導(dǎo)實踐的意義。紙上得來終覺淺,絕知此事要躬行。
1 面向?qū)ο缶幊袒A(chǔ)
面向?qū)ο缶幊躺婕暗饺齻€重要的特性:封裝、繼承與多態(tài)。部分 C 程序員,特別是嵌入式 C 程序員有一種誤解,C 語言不是面向?qū)ο蟮木幊陶Z言,C++、Java、Python 等更高級的才是,使用 C 語言無法實現(xiàn)面向?qū)ο缶幊獭_@種誤解致使他們沒有動力學(xué)習(xí)一些優(yōu)秀的面向?qū)ο缶幊谭椒?,例如設(shè)計模式、設(shè)計原則、軟件架構(gòu)設(shè)計等等,進(jìn)而很難開發(fā)出易維護(hù)、易部署、易重用、易管理的軟件,很難面對項目需求的變更、擴(kuò)展,很難開發(fā)和維護(hù)大型的復(fù)雜項目。
1.1 對象
面向?qū)ο缶幊蹋皩ο蟆笔钦麄€編程過程的關(guān)鍵。其常見的解釋是“數(shù)據(jù)與函數(shù)的組合”。每個對象都是由一組數(shù)據(jù)(用以描述對象的狀態(tài))和一組函數(shù)(對象支持的操作,用以描述對象的行為)組成的。對象實現(xiàn)了數(shù)據(jù)和操作的結(jié)合,使數(shù)據(jù)和操作可以封裝于“對象”這個統(tǒng)一體中。
在面向過程編程中,程序設(shè)計注重的是“過程”,先做什么,后做什么;在外界看來,整個程序由一系列散亂的數(shù)據(jù)和函數(shù)組合而成。而在面向?qū)ο缶幊讨?,程序設(shè)計注重的是“對象”,在外界看來,整個程序由一系列“對象”塊組合而成,數(shù)據(jù)和函數(shù)封裝到了對象內(nèi)部。
1.2 類
對象是有“類型”的,即類?!邦悺笔菍σ唤M對象共性的抽象,表示一類對象,而對象是某個類的一個具體化的個例,通常稱之為類的實例。對象通常是由數(shù)據(jù)和函數(shù)組成的,相應(yīng)的類也具有兩部分內(nèi)容:屬性(數(shù)據(jù)的抽象)和方法(對象行為的抽象)。
除了封裝屬性和操作外,類還具有訪問控制的能力,如某些屬性和方法是私有的,不能被外界訪問。通過訪問控制,能夠?qū)?nèi)部數(shù)據(jù)提供不同級別的保護(hù),以防止外界意外地改變或使用私有部分。
1. 屬性
類具有屬性,它是對數(shù)據(jù)(對象的狀態(tài))的抽象。在 C 程序設(shè)計時,通常使用結(jié)構(gòu)體類型來表示一個類,相關(guān)屬性即包含在相應(yīng)的結(jié)構(gòu)體類型中。例如學(xué)生具有屬性:姓名、學(xué)號、性別、身高、體重等信息,可以使用如下結(jié)構(gòu)體類型表示“學(xué)生類”:
//微信公眾號【嵌入式系統(tǒng)】
struct?student
{
????char?name[10];???/*?姓名?(假定最長10字符)?*/
????unsigned?int?id;??/*?學(xué)號?*/
????char?sex;????/*?性別:'M',男;'F'?,女?*/
????float?height;???/*?身高?*/
????float?weight;???/*?體重?*/
};
//微信公眾號【嵌入式系統(tǒng)】提示,關(guān)于結(jié)構(gòu)體、枚舉等復(fù)雜類型定義推薦使用關(guān)鍵字?typedef
微信公眾號【嵌入式系統(tǒng)】提示,關(guān)于結(jié)構(gòu)體、枚舉等復(fù)雜類型定義推薦使用關(guān)鍵字 typedef,更多C關(guān)鍵字了解可以參考《C語言關(guān)鍵字應(yīng)用技巧》、《高質(zhì)量嵌入式軟件的開發(fā)技巧》。
2. 方法
類具有方法,它是對象行為的抽象,在 C 程序中,方法可以看作普通函數(shù),不過其通常有一個特點(diǎn) ,函數(shù)的第一個參數(shù)為類型的指針,指向了一個確定的對象,用以表明此次操作針對哪個對象,在方法實現(xiàn)時,即可通過該指針訪問到對象中的各個屬性。(微信公眾號【嵌入式系統(tǒng)】這是C面向?qū)ο蟊仨毜?,類似C++的this)
針對學(xué)生對象,為了對外展現(xiàn)學(xué)生自身的信息,自我介紹的格式是對外輸出一個固定格式的字符串:
"Hi! My name is xxx, I'm a (boy/girl). My school number is xxx. My height is xxxcm and weight is xxxkg . "
其中的 xxx 對應(yīng)學(xué)生實際的信息,基于此,可以為學(xué)生類定義并實現(xiàn)一個“自我介紹”的方法:
//微信公眾號:嵌入式系統(tǒng)
void?student_self_introduction(struct?student?*p_this)
{
????printf("Hi!?My?name?is?%s,?I'm?a?%s.?My?school?number?is?%d.?My?height?is?%fcm?and?weight?is?%fkg",
???????????p_this->name,
???????????(p_this->sex?==?'M')???"boy"?:?"girl",
???????????p_this->id,
???????????p_this->height,
???????????p_this->weight);
}
對于外界來講,調(diào)用學(xué)生的“自我介紹”方法可以獲知學(xué)生的全部信息?;谠擃惖亩x,一個簡易的應(yīng)用程序范例詳如下:
//微信公眾號:嵌入式系統(tǒng)
void?main(void)
{
????struct?student?chengj?=?{"chengj",?2024001,?'M',?173,?68};
????struct?student?hehe?=?{"hehe",?2024002,?'M',?150,?45};
????student_self_introduction(&chengj);
????student_self_introduction(&hehe);
????//?...
}
類中的方法 student_self_introduction 可以作用于任一學(xué)生類對象,對于程序員來講,編寫的代碼將適用于一組對象,而非特定的某一個對象,提高了代碼利用率。
在實際應(yīng)用中,對比代碼《嵌入式算法14---數(shù)據(jù)流與環(huán)形隊列》,不少程序員都喜歡編寫出一堆非常類似的接口,它們僅通過某一個數(shù)字后綴(0、1、2……)來區(qū)分,如系統(tǒng)使用到 3 個棧,初級程序員可能實現(xiàn) 3 個入棧函數(shù),不良示意代碼如下:
//微信公眾號:嵌入式系統(tǒng)
//三個棧入棧的不良范例,引以為戒
int?push_stack0(int?data)
{
????//...
}
int?push_stack1(int?data)
{
????//...
}
int?push_stack2(int?data)
{
????//...
}
三個操作可能除了極小部分的差異外,其它處理完全相同,這就是沒有面向?qū)ο缶幊痰乃季S,沒有定義對象類型的概念,將操作直接針對每個具體對象(棧 0、棧 1、棧 2),而不是一組同類的對象(所有棧對象)。顯然,3個棧的特性和行為都基本類似,因而可以定義一個“棧類型”,如此一來,入棧操作將屬于棧類型中的一個方法,適用于所有棧對象。例如:
//數(shù)據(jù)壓入棧,?p_stack?指向具體的棧對象
int?push_stack(stack?*p_stack,?int?data);
//微信公眾號:嵌入式系統(tǒng)
//三個棧的入棧操作均可使用同一個方法
push_stack(p_stack0,?1);
push_stack(p_stack1,?2);
push_stack(p_stack2,?3);
這只是示意性代碼,說明使用“類”的設(shè)計解決問題所帶來的優(yōu)勢。
1.3 UML 類圖
在面向?qū)ο蟮脑O(shè)計和開發(fā)過程中,通常使用 UML 工具來進(jìn)行分析與設(shè)計。最基本的就是使用 UML 類圖來表示類以及描述類之間的關(guān)系。
在 UML 類圖中,一個矩形框表示一個類,矩形框內(nèi)部被分隔為上、中、下三部分,上部為類的名字,中部為類的屬性,下面部分為類的方法。對于屬性和方法,還可以使用“+”、“-”修飾符來表示訪問權(quán)限,“+”為公有屬性、“-”為私有屬性。如前面的學(xué)生類,其類名為 student,屬性包括姓名、學(xué)號、性別、身高、體重,方法有“自我介紹”方法,則其對應(yīng)的類圖如下:通常情況下,類中的所有屬性均為私有屬性,不建議直接訪問,所有屬性的訪問都通過類提供的方法?;诖?,假定了學(xué)生類中的所有屬性均為私有屬性,因而在所有屬性前都增加了“-”修飾符。
UML 類圖主要用于輔助分析和設(shè)計,設(shè)計類時應(yīng)聚焦在與當(dāng)前問題有關(guān)的重要屬性和行為,無關(guān)的屬性和方法可去掉,確保簡潔。由于私有屬性僅在內(nèi)部使用,外界無需關(guān)心,因此UML 類圖中通常不體現(xiàn)私有屬性和方法,除非某些特殊的私有屬性和方法影響到問題的理解或者類的實現(xiàn)?;诖丝梢院喕?img decoding="async" class="aligncenter" src="https://wximg.eefocus.com/forward?url=https%3A%2F%2Fmmbiz.qpic.cn%2Fsz_mmbiz_png%2F4W1T4tmuLNzHbGuF657CGOIsCARfGV6RRYEHEl6XhhOOdHA843vPmiasefFFgcLugJZWczv2wHcic6owyM5ReAkw%2F640%3Fwx_fmt%3Dpng%26amp%3Bfrom%3Dappmsg&s=6eb2f7" />2 封裝
類是對一組對象共性的抽象,封裝了屬性和方法;即把一組關(guān)聯(lián)的數(shù)據(jù)和函數(shù)圈起來,使圈外的代碼只能看見部分函數(shù),數(shù)據(jù)則完全不可見(微信公眾號【嵌入式系統(tǒng)】一般建議數(shù)據(jù)的訪問都應(yīng)通過類提供的方法,而不是全局變量滿天飛)。
2.1 “封裝”示例
在C語言中,可使用一個 C 文件(*.c 文件)和 H 文件(*.h 文件)完成“類”的定義,將所有需要封裝的東西都存于 C 文件中,H 文件中只展現(xiàn)“對外可見、無需封裝”的內(nèi)容。
以棧的實現(xiàn)為例,將所有實現(xiàn)代碼都存于 C 文件中,H 文件只包含與棧相關(guān)接口的聲明,比如入棧和出棧等。頭文件和源文件的示意內(nèi)容分別詳見如下:
stack.h文件
#ifndef?__STACK_H
#define?__STACK_H
//微信公眾號:嵌入式系統(tǒng)?所有頭文件都必須防止重復(fù)引用
/*?類型聲明,無需關(guān)心類定義的具體細(xì)節(jié)?*/
struct?stack;
/*?創(chuàng)建棧,并指定??臻g的大小*/
struct?stack?*?stack_create(int?size);
/*?入棧?*/
int?stack_push(struct?stack?*p_stack,?int?val);
/*?出棧?*/
int?stack_pop(struct?stack?*p_stack,?int?*p_val);
/*?刪除棧?*/
int?stack_delete(struct?stack?*p_stack);
#endif
stack.c文件
//微信公眾號:嵌入式系統(tǒng)
#include?"stack.h"
#include?"stdlib.h"
struct?stack
{
????int?top;???/*?棧頂*/
????int?*p_buf;???/*?棧緩存*/
????unsigned?int?size;??/*?棧緩存的大小?*/
};
unsigned?int?size;?/*?棧緩存的大小*/
struct?stack?*?stack_create(int?size)
{
????struct?stack?*p_stack?=?(struct?stack?*)malloc(sizeof(struct?stack));
????if(p_stack?!=?NULL)
????{
????????p_stack->top?=?0;
????????p_stack->size?=?size;
????????p_stack->p_buf?=?(int?*)malloc(sizeof(int)?*?size);
????????if(p_stack->p_buf?!=?NULL)
????????{
????????????return?p_stack;
????????}
????????free(p_stack);?/*?分配棧內(nèi)存失敗*/
????}
????return?NULL;?/*?創(chuàng)建棧失敗,返回?NULL*/
}
int?stack_push(struct?stack?*p_stack,?int?val)
{
????if(p_stack->top?!=?p_stack->size)?//未滿可入棧
????{
????????p_stack->p_buf[p_stack->top++]?=?val;
????????return?0;
????}
????return?-1;
}
int?stack_pop(struct?stack?*p_stack,?int?*p_val)
{
????if(p_stack->top?!=?0)??//非空可出棧
????{
????????*p_val?=?p_stack->p_buf[--p_stack->top];
????????return?0;
????}
????return?-1;
}
int?stack_delete(struct?stack?*p_stack)
{
????if(p_stack?==?NULL)
????{
????????return?-1;
????}
????if(p_stack->p_buf?!=?NULL)
????{
????????free(p_stack->p_buf);
????}
????free(p_stack);
????return?0;
}
使用 stack.h 的程序沒有 struct stack 結(jié)構(gòu)體成員的訪問權(quán)限的,只能調(diào)用stack.h 文件中聲明的方法。對于外界用戶來說,struct stack 結(jié)構(gòu)體的內(nèi)部細(xì)節(jié),以及各個函數(shù)的具體實現(xiàn)方式都是不可見的。這正是完美的封裝!
由于所有細(xì)節(jié)都封裝到了 C 文件內(nèi)部,用戶通過 stack.h 文件并不能看到 struct stack 結(jié)構(gòu)體的具體定義,因此也無法訪問 stack 結(jié)構(gòu)體中的成員。若用戶嘗試訪問 struct stack結(jié)構(gòu)體中的成員,將會編譯報錯。(微信公眾號【嵌入式系統(tǒng)】?C 語言不是面向?qū)ο蟮木幊陶Z言,實現(xiàn)封裝有擴(kuò)展性的犧牲)。
C語言實現(xiàn)封裝的一般做法為:在頭文件中進(jìn)行數(shù)據(jù)結(jié)構(gòu)以及函數(shù)定義的前置聲明,在源文件中完成各函數(shù)的具體實現(xiàn)以及數(shù)據(jù)結(jié)構(gòu)的定義。這樣所有函數(shù)實現(xiàn)及定義細(xì)節(jié)均封裝到了源文件中,對使用者來說是完全不可見的。
2.2 創(chuàng)建對象
2.2.1 內(nèi)存分配的問題
基于前面創(chuàng)建棧方法,可以創(chuàng)建多個棧對象,例如:
struct?stack?*p_stack1?=?stack_create(20);
struct?stack?*p_stack2?=?stack_create(30);
struct?stack?*p_stack3?=?stack_create(50);
每個棧對象需要兩部分內(nèi)存:
一是棧對象本身的內(nèi)存(內(nèi)存大小為 sizeof(struct stack));
二是該棧對象用于存儲數(shù)據(jù)的緩存(內(nèi)存大小為 sizeof(int) * size,其中,size 由用戶在創(chuàng)建 棧時通過參數(shù)指定)。
在棧對象的創(chuàng)建函數(shù)中,使用 malloc()分配了該對象所需的內(nèi)存空間,使用 malloc()分配內(nèi)存空間非常方便,但這種做法也限制了對象內(nèi)存的來源——必須使用動態(tài)內(nèi)存。但對于嵌入式系統(tǒng),內(nèi)存往往是很大的瓶頸,很多應(yīng)用場合可能并不太適合使用動態(tài)內(nèi)存,主要有以下幾個因素:
1)內(nèi)存資源不足。運(yùn)行嵌入式軟件的硬件平臺普遍內(nèi)存小甚至只有幾k RAM。這種條件下管理使用動態(tài)內(nèi)存是比較浪費(fèi)的行為,可能產(chǎn)生內(nèi)存碎片,且內(nèi)存分配的軟件算法本身也會占用一定的內(nèi)存空間。
2)實時性要求高。部分嵌入式應(yīng)用對實時性要求很高,但由于資源的限制,集成的動態(tài)內(nèi)存分配算法不是很完善,使得很難確保動態(tài)內(nèi)存分配的實時性。
3)內(nèi)存泄漏。動態(tài)內(nèi)存分配可能出現(xiàn)內(nèi)存泄漏。
4)軟件編程復(fù)雜。在可靠的設(shè)計中,必須考慮內(nèi)存分配失敗的情況并對其進(jìn)行異常處理,如果存在大量的動態(tài)內(nèi)存分配,則處處都需考慮分配失敗的情況。
將對象內(nèi)存的來源限制為動態(tài)內(nèi)存分配,限制了該類的應(yīng)用場合,致使部分應(yīng)用場合因為內(nèi)存來源的問題不得不放棄該類的使用。
2.2.2 內(nèi)存來源的探索
在 C 程序開發(fā)中,除了使用 malloc()得到一段內(nèi)存空間外,還可以使用“直接定義變量”的形式分配一段內(nèi)存。直接定義變量的形式,內(nèi)存在編譯階段由編譯器負(fù)責(zé)分配,無需用戶作任何干預(yù)。根據(jù)變量定義位置的不同,實際內(nèi)存的開辟位置存在一定的區(qū)別,主要有兩類:
局部變量:內(nèi)存開辟在棧中;
靜態(tài)變量(static 修飾的變量)或全局變量:內(nèi)存開辟在全局靜態(tài)存儲區(qū)。
兩種變量主要是生命周期的不同:局部變量在退出當(dāng)前作用域后(比如函數(shù)返回),內(nèi)存自動釋放;靜態(tài)變量或全局變量內(nèi)存開辟在全局靜態(tài)存儲區(qū),它們在程序的整個生命周期均有效。
內(nèi)存可以有 3 種來源,它們的優(yōu)缺點(diǎn)對比詳見下表:
內(nèi)存類別 | 內(nèi)存位置 | 生命周期 | 優(yōu)點(diǎn) | 缺點(diǎn) |
---|---|---|---|---|
動態(tài)內(nèi)存 | 系統(tǒng)堆 Heap | 直到調(diào)用free()釋放內(nèi)存 | 靈活,可以隨時按需分配和釋放 | 內(nèi)存分配可能失敗,花費(fèi)的時間可能不確定;需要處理內(nèi)存分配失敗的情況,增加程序的復(fù)雜性 |
靜態(tài)內(nèi)存 | 全局靜態(tài)存儲區(qū)(.data、.bss存儲段) | 程序的整個運(yùn)行周期 | 確定性好,只要程序能夠編譯、鏈接成功,內(nèi)存一定能夠分配成功 | 需要編程時確定內(nèi)存的大??;一直占用內(nèi)存,無法釋放 |
棧內(nèi)存 | 系統(tǒng)棧(或任務(wù)棧) | 函數(shù)調(diào)用周期 | 自動完成內(nèi)存的分配和回收 | 內(nèi)存太大會導(dǎo)致棧溢出 |
微信公眾號:嵌入式系統(tǒng) |
不同來源的內(nèi)存各有優(yōu)劣。前面提到,stack_create()函數(shù)將內(nèi)存的來源限制為僅動態(tài)內(nèi)存不太合理。為了避免內(nèi)存來源受限,“內(nèi)存的分配”這一步交由用戶實現(xiàn),以便用戶根據(jù)實際需要自由選擇內(nèi)存的來源?;诖?,可以將對象的創(chuàng)建拆分為兩個獨(dú)立的步驟,分配對象所需的內(nèi)存和初始化對象。
2.2.3 分配對象所需的內(nèi)存
內(nèi)存分配的工作交由用戶完成,以便用戶根據(jù)實際需要自由選擇。用戶能夠完成內(nèi)存分配的前提是:用戶知道應(yīng)該分配的內(nèi)存大小。前面提到,每個棧對象需要兩部分內(nèi)存:一是棧對象本身的內(nèi)存(內(nèi)存大小為:sizeof(struct stack));二是該棧對象用于存儲數(shù)據(jù)的緩存(內(nèi)存大小為 sizeof(int) * size,其中,size 由用戶在創(chuàng)建棧時通過參數(shù)指定)。
1、棧對象本身的內(nèi)存
棧對象本身的內(nèi)存大小為 sizeof(struct stack),若用戶直接采用靜態(tài)內(nèi)存分配的方式(直接定義一個變量),則形式如下:
struct?stack?my_stack;
也可以繼續(xù)采用動態(tài)內(nèi)存的分配方式,例如:
struct?stack?*p_stack?=?(struct?stack?*)malloc(sizeof(struct?stack));
但是,若將這兩行代碼直接放到主程序中會無法編譯,因為之前描述的“封裝”特性,使外界看不到 struct stack 的具體定義,也就是說,對于外界而言,該類型僅僅只是聲明并未定義,該類型對應(yīng)變量的大小對外也是未知的。
在 C 語言中定義一個變量時,編譯器將負(fù)責(zé)該變量所占用內(nèi)存的分配。內(nèi)存的大小與類型相關(guān),要完成變量內(nèi)存的分配,編譯器必須知道變量所占用的存儲空間大小。當(dāng)一個變量的類型未定義時,無法完成該類型對應(yīng)變量的定義,因此,如下語句在編譯時會出錯:
struct?stack?my_stack;
同理,sizeof 語句用于獲得相應(yīng)類型數(shù)據(jù)的大小,而未定義的類型顯然是不知道其大小的,動態(tài)內(nèi)存分配中所使用的 sizeof(struct stack)語句也是錯誤的。
也許部分人會有疑問,既然該類型未定義,為什么在主程序中定義該類型的指針變量卻可以呢?
struct?stack?*p_stack?=?//...
雖然 struct stack 類型未定義,但在之前已經(jīng)聲明,因此,編譯器知道它是一個“合法的結(jié)構(gòu)體類型”。此外,這里定義的是一個指針變量,在特定系統(tǒng)中,指針變量所占用的內(nèi)存大小是確定的,例如,在 32 位系統(tǒng)中,指針通常占用 4 個字節(jié)。即指針變量所占用的內(nèi)存空間大小與其指向的數(shù)據(jù)類型無關(guān),編譯器無需知道其指向的數(shù)據(jù)類型,就可完成指針變量內(nèi)存的分配。因此,一個類型未定義,只要其聲明了,就可以定義該類型的指針變量。但需要注意的是,在完成該類型的定義之前,不得嘗試訪問該指針?biāo)赶虻膬?nèi)容。
完成內(nèi)存的分配,提供三種方案。
(1) 將類的具體定義放到 H 文件中
為了使用戶知道對象內(nèi)存的大小,一種最簡單的辦法是直接將類型的定義放在 H 文件中。更新后的 H 文件示意代碼如下:
stack.h文件
#ifndef?__STACK_H
#define?__STACK_H
/*?類型定義?*/
struct?stack
{
????int?top;???/*?棧頂*/
????int?*p_buf;???/*?棧緩存*/
????unsigned?int?size;??/*?棧緩存的大小?*/
};
//?......?其它函數(shù)聲明
#endif
此時,對于外界,類型已經(jīng)定義,如下語句均可正常使用:
struct?stack?my_stack;//?靜態(tài)內(nèi)存分配
struct?stack?*p_stack?=?(struct?stack?*)malloc(sizeof(struct?stack));//?動態(tài)內(nèi)存分配
由于類型的定義存放到了 H 文件中,暴露了類中的成員,在一定程度上破壞了類的“封裝”性。此時外界可以直接訪問類中的數(shù)據(jù)成員。犧牲一定的封裝性,換來內(nèi)存分配的靈活性,這也是在嵌入式系統(tǒng)中,基于 C 語言實現(xiàn)面向?qū)ο缶幊痰囊话阕龇ǎ〝?shù)據(jù)結(jié)構(gòu)定義存放在 H 文件中更加符合程序員的編程風(fēng)格)。嵌入式軟件大多數(shù)類定義在 H文件中,并沒有封裝在 C 文件中。
雖然類的定義存放在 H 文件中,但出于封裝性考慮,外界任何時候都不應(yīng)直接訪問對象中的數(shù)據(jù),應(yīng)該將其視為使用 C 語言實現(xiàn)面向?qū)ο缶幊痰囊粭l準(zhǔn)則。軟件開發(fā)需要遵守兩個規(guī)則:一是在設(shè)計類時,應(yīng)考慮到用戶可能訪問的數(shù)據(jù),并為這些數(shù)據(jù)提供相應(yīng)的訪問接口;二是在使用別人提供的類時,除非有特殊說明,否則都不應(yīng)該嘗試直接訪問類中的數(shù)據(jù)。
這種方法是目前嵌入式系統(tǒng)中使用得最為廣泛的一種方法,因此后文使用這種方法討論。
(2) 在 H 文件中定義一個新的結(jié)構(gòu)體類型
為了繼續(xù)保持類的封裝性,類的定義依然保留在 C 文件中。只不過與此同時,在 H 文件中定義一個新的結(jié)構(gòu)體類型。在該結(jié)構(gòu)體類型中,各個成員的順序和類型與類定義完全一致,僅命名不同。
struct?stack_mem
{
????int?dummy1;
????int?*dummy2;
????unsigned?int?dummy3;
};
各成員的順序和類型均與 struct stack 的定義完全相同,以此保證兩個類型數(shù)據(jù)所需要的內(nèi)存空間完全一致。同時,為了屏蔽各個成員的具體含義,所有成員均以 dummy 開頭進(jìn)行命名。對于外界來講,可以基于 struct stack_mem 類型完成內(nèi)存的分配,例如:
struct?stack_mem?my_stack;//?靜態(tài)內(nèi)存分配
struct?stack?*p_stack?=?(struct?stack?*)malloc(sizeof(struct?stack_mem));//?動態(tài)內(nèi)存分配
使用這種方案,類的實際定義依然沒有暴露給外界,繼續(xù)保持了良好的封裝。(微信公眾號【嵌入式系統(tǒng)】實際上FreeRTOS中,很多地方都采用了這種方法)。但這里定義了一個新的類型,給用戶理解上造成了一定的困擾,此外,為確保兩個類型完全一致,就要求類的設(shè)計者在修改類的定義時,必須確保 struct stack_mem 類型也同步修改,這給類的維護(hù)工作帶來了挑戰(zhàn);稍有不慎,某一個類型沒有同步修改就可能造成嚴(yán)重的錯誤,且這種錯誤編譯器不會給出任何提示,非常隱蔽。關(guān)于代碼審查可以參考《代碼審查那些事》、《代碼的保養(yǎng)》。
(3) 使用宏的形式告知對象所需的內(nèi)存大小
既然外界只需要知道對象內(nèi)存的大小,可以在開發(fā)過程中使用 sizoeof()獲得struct stack 類型的大小,然后將其以宏的形式定義在 H 文件中。例如在 32 位系統(tǒng)中,使用 sizeof()獲知 struct stack 類型的長度為 12,則可以在 H 文件中定義一個宏,例如:
#define?STACK_MEM_SIZE?12
用戶使用該宏完成內(nèi)存分配,例如:
unsigned?char?stack_mem[STACK_MEM_SIZE];
這種做法僅僅在頭文件中新增了一個宏定義,類的定義依然保持的 C 文件中,“封裝”完全沒有被破壞,看起來也非常完美。但這種做法也存在一些問題,因而很少采用。
a)對于同一個類型,不同系統(tǒng)中 sizeof()的結(jié)果可能不同。類型的長度與系統(tǒng)和編譯器均相關(guān)。以 int 類型為例,在 32 位系統(tǒng)中為 32 位(4 字節(jié)),但 16 位系統(tǒng)中,其位寬可能為 16 位(2 字節(jié))。因此,同樣是 sizeof(int),結(jié)果可能為 4,也可能為 2。使用 sizeof()獲取類型的長度時,不同系統(tǒng)中獲取的結(jié)果可能并不相同。這就導(dǎo)致 H 文件中的宏定義,切換平臺需要重新測試驗證。同時,由于類型的定義封裝到了 C 文件中,因此修改過程只能有類的開發(fā)者完成,一般用戶還無法完成,這就使得該類的跨平臺特性很差,移植有風(fēng)險。
b)內(nèi)存不僅有大小的要求,還有內(nèi)存對齊的要求。
因此,通過一個宏告知用戶需要分配的內(nèi)存空間大小并不是十分合適,會遇到跨平臺、內(nèi)存對齊等多個注意事項,用戶可能在不經(jīng)意間出錯。在實際嵌入式系統(tǒng)中很少使用。一些編碼技能可以參考《高質(zhì)量嵌入式軟件的開發(fā)技巧》。
2、存儲數(shù)據(jù)的緩存
存儲數(shù)據(jù)的緩存大小為 sizeof(int) *size,其中的 size 本身就是由用戶指定的,這部分內(nèi)存的大小用戶很容易得知,進(jìn)而完成內(nèi)存的分配??梢圆捎渺o態(tài)內(nèi)存分配的方式(直接定義一個變量)完成內(nèi)存的分配:
int?buf[20];
也可以采用動態(tài)內(nèi)存分配的方式完成內(nèi)存的分配:
int?*p_buf?=?(int?*)malloc(sizeof(int)?*?20);
2.2.4 內(nèi)存小曲
內(nèi)存的來源主要有三種:動態(tài)內(nèi)存、靜態(tài)內(nèi)存和棧內(nèi)存,具體如何選擇按實際情況。
對象類別 | 應(yīng)用場合 |
---|---|
動態(tài)對象 | 不會頻繁創(chuàng)建、銷毀對象的應(yīng)用;內(nèi)存占用太大的對象 |
靜態(tài)對象 | 確定性要求較高,長生命周期的對象 |
棧對象 | 函數(shù)內(nèi)部使用的臨時對象;對象內(nèi)存占用較小的對象 |
一些入式應(yīng)用對確定性要求較高,建議優(yōu)先使用靜態(tài)對象。如此一來只要能夠編譯(包含鏈接)成功,應(yīng)用程序往往就可以按照確定的流程正確執(zhí)行;若使用動態(tài)對象,則必須考慮對象創(chuàng)建失敗的情況。偶爾使用的大塊內(nèi)存則建議使用動態(tài)內(nèi)存,使用注意和防范可參考《動態(tài)內(nèi)存管理及防御性編程》。
2.3 初始化對象
初始化對象的具體細(xì)節(jié)用戶不需要關(guān)心,指定棧對象的地址、緩存地址及緩存大小,基于此,可以定義初始化函數(shù)的原型為:
int?stack_init?(struct?stack?*p_stack,?int?*p_buf,?int?size);
對于棧來講,棧頂索引(top)的初始值恒為 0,因此該值無需通過初始化函數(shù)的參數(shù)傳遞。int 類型的返回值常用于表示執(zhí)行的結(jié)果(微信公眾號【嵌入式系統(tǒng)】建議非指針類型的返回值,以0表示成功,負(fù)數(shù)表示失?。?。該函數(shù)的實現(xiàn)示意如下:
//微信公眾號:嵌入式系統(tǒng)
int?stack_init(struct?stack?*p_stack,?int?*p_buf,?int?size)
{
????p_stack->top?=?0;
????p_stack->size?=?size;
????p_stack->p_buf?=?p_buf;
????return?0;
}
(微信公眾號【嵌入式系統(tǒng)】該初始化函數(shù)的實現(xiàn)僅作為原理性展示,沒有做過多的錯誤處理或參數(shù)檢查,實際應(yīng)用中,p_stack 為 NULL 或 p_buf 為 NULL 等情況都是錯誤情況,后續(xù)范例也會省去部分參數(shù)校驗)。
至此,完成了將創(chuàng)建對象分離為“分配對象所需的內(nèi)存”和“初始化對象”兩個步驟,對象內(nèi)存的來源交由用戶決定,用戶根據(jù)需要獲得內(nèi)存后,再將相關(guān)內(nèi)存的首地址傳遞給初始化函數(shù)。
2.4 銷毀對象
實現(xiàn) stack_create()以及對應(yīng)的stack_delete(),設(shè)計該函數(shù)的初衷是當(dāng)一個棧對象不會再被使用時,可以通過該函數(shù)釋放棧占用的資源,比如釋放在 stack_create()函數(shù)中使用 malloc()分配的內(nèi)存資源。
當(dāng)將 stack_create()拆分為兩步后,內(nèi)存的分配將由用戶決定,對應(yīng)地內(nèi)存的釋放也應(yīng)由用戶決定?;仡?stack_delete()函數(shù)的實現(xiàn),該函數(shù)目前只做了內(nèi)存釋放相關(guān)的操作,當(dāng)不需要釋放內(nèi)存時,該函數(shù)看起來沒有存在的必要。實際上,stack_delete()和 stack_create()函數(shù)是對應(yīng)的,當(dāng)將 stack_create()拆分為“分配對象所需的內(nèi)存”和“初始化對象”兩個步驟后,stack_delete()也應(yīng)該相應(yīng)的拆分為兩個步驟:“釋放對象占用的內(nèi)存”和“解初始化對象”(微信公眾號【嵌入式系統(tǒng)】解初始化或者反初始化,不用太在意這個操作的名稱,只要理解表達(dá)的意思是初始化的逆過程即可,init:deinit,關(guān)于命名的英文集客參考《嵌入式軟件命名常用英文集》)。
1. 釋放對象占用的內(nèi)存
前面已經(jīng)提到,釋放內(nèi)存交由用戶處理,釋放方法與內(nèi)存的來源相關(guān)。
動態(tài)內(nèi)存的釋放?動態(tài)內(nèi)存分配應(yīng)使用相應(yīng)的釋放內(nèi)存函數(shù)(如 free())進(jìn)行釋放。在釋放時應(yīng)確保分配的內(nèi)存全部被有效釋放。若某一部分內(nèi)存被遺漏,將造成內(nèi)存泄漏。隨著程序的長期運(yùn)行,內(nèi)存不斷泄漏可能導(dǎo)致系統(tǒng)崩潰。
靜態(tài)內(nèi)存的釋放?使用靜態(tài)內(nèi)存(定義變量的形式),則內(nèi)存的釋放是系統(tǒng)自動完成的。若將對象定義為局部變量,內(nèi)存開辟在系統(tǒng)棧中,則退出當(dāng)前作用域后(函數(shù)返回)自動釋放;若將對象定義為靜態(tài)變量(static)或全局變量,則內(nèi)存開辟在全局靜態(tài)區(qū),該區(qū)域的內(nèi)存在應(yīng)用程序的整個生命周期均有效,無法釋放。
2. 解初始化對象
釋放內(nèi)存已交由用戶處理,對于類的設(shè)計來講,重點(diǎn)是設(shè)計“解初始化對象”對應(yīng)的函數(shù),該函數(shù)與 stack_init()函數(shù)對應(yīng),通常命名為“*_deinit”,即:stack_deinit()。該函數(shù)通常用于釋放在初始化對象時占用的其它資源。
對于純軟件對象(與硬件無關(guān)的軟件),通常其只會占用內(nèi)存資源,不會額外占用其它資源,對這類對象解初始化時可能無需做任何事情。例如前面關(guān)于棧的實現(xiàn),在stack_init()函數(shù)中僅對幾個屬性進(jìn)行了賦值,沒有額外占用其它任何資源,此時,stack_deinit()可能無需做任何事情,成為一個空函數(shù)。
int?stack_deinit(struct?stack?*p_stack)
{
????return?0;
}
在嵌入式系統(tǒng)中,經(jīng)常會遇到與硬件相關(guān)的對象,其初始化時往往會占用一定的硬件資源:I/O 引腳、系統(tǒng)中斷、系統(tǒng)總線。在解初始化這種對象時,應(yīng)同時釋放占用的資源??芍攸c(diǎn)關(guān)注對象的初始化函數(shù),查看其中是否分配、占用了某些資源。若有,則在解初始化函數(shù)中作相應(yīng)的釋放操作;若無,則解初始化函數(shù)留空。為了提高軟件的簡潔性,也可刪除了空的解初始化函數(shù),但這里為了展示軟件結(jié)構(gòu),依然保留了解初始化函數(shù)。
將原 H 文件中的創(chuàng)建接口更新為初始化接口,刪除接口更新為解初始化接口,更新后的 H 文件內(nèi)容和 C 文件如下:
stack.h?文件
//微信公眾號:嵌入式系統(tǒng)
#ifndef?__STACK_H
#define?__STACK_H
/*?類型定義*/
struct?stack
{
????int?top;???/*?棧頂*/
????int?*p_buf;???/*?棧緩存*/
????unsigned?int?size;??/*?棧緩存的大小?*/
};
/*?初始化*/
int?stack_init(struct?stack?*p_stack,?int?*p_buf,?int?size);
/*?入棧*/
int?stack_push(struct?stack?*p_stack,?int?val);
/*?出棧*/
int?stack_pop(struct?stack?*p_stack,?int?*p_val);
/*?解初始化*/
int?stack_deinit(struct?stack?*p_stack);
#endif
?stack.c?文件
//微信公眾號:嵌入式系統(tǒng)
#include?"stack.h"
int?stack_init(struct?stack?*p_stack,?int?*p_buf,?int?size)
{
????p_stack->top?=?0;
????p_stack->size?=?size;
????p_stack->p_buf?=?p_buf;
????return?0;
}
int?stack_push(struct?stack?*p_stack,?int?val)
{
????if(p_stack->top?!=?p_stack->size)
????{
????????p_stack->p_buf[p_stack->top++]?=?val;
????????return?0;
????}
????return?-1;
}
int?stack_pop(struct?stack?*p_stack,?int?*p_val)
{
????if(p_stack->top?!=?0)
????{
????????*p_val?=?p_stack->p_buf[--p_stack->top];
????????return?0;
????}
????return?-1;
}
int?stack_deinit(struct?stack?*p_stack)
{
????return?0;
}
3. 銷毀對象的順序
創(chuàng)建對象時是先分配對象所需內(nèi)存,再初始化對象,因為在初始化對象時,需要傳遞相應(yīng)內(nèi)存空間的首地址作為初始化函數(shù)的參數(shù)。這就保證了在初始化對象之前,必須完成相關(guān)內(nèi)存的分配。而銷毀一個對象時,釋放內(nèi)存與調(diào)用解初始化函數(shù)并不能通過接口進(jìn)行制約,銷毀過程與創(chuàng)建恰恰相反,應(yīng)先解初始化對象,再釋放對象占用的內(nèi)存。因為在解初始化對象時,還會使用到對象中的數(shù)據(jù),若先釋放對象占用的內(nèi)存,則對象在被解初始化之前,就被徹底銷毀了,對象已經(jīng)不存在了,顯然無法再進(jìn)行解初始化操作。
若內(nèi)存來源于動態(tài)內(nèi)存分配,則完整的應(yīng)用程序范例如下:
//微信公眾號:嵌入式系統(tǒng)
#include?"stack.h"
#include?"stdio.h"
#include?"stdlib.h"
int?main()
{
????int?val;
????struct?stack?*p_stack?=?(struct?stack?*)malloc(sizeof(struct?stack));
????int?*p_buf?=?(int?*)malloc(sizeof(int)?*?20);
?//?初始化
????stack_init(p_stack,?buf,?20);
?//依次壓入數(shù)據(jù):2、4、5、8
????stack_push(p_stack,?2);
????stack_push(p_stack,?4);
????stack_push(p_stack,?5);
????stack_push(p_stack,?8);
?//依次彈出各個數(shù)據(jù),并打印
????stack_pop(p_stack,?&val);
????printf("%d?",?val);
????stack_pop(p_stack,?&val);
????printf("%d?",?val);
????stack_pop(p_stack,?&val);
????printf("%d?",?val);
????stack_pop(p_stack,?&val);
????printf("%d?",?val);
?//?解初始化
????stack_deinit(p_stack);
?//?釋放內(nèi)存
????free(p_stack);
????free(p_buf);
?
????return?0;
}
若內(nèi)存來源于靜態(tài)內(nèi)存分配,則內(nèi)存的分配和釋放完全由系統(tǒng)自行完成,如內(nèi)存以“局部變量”的形式分配,范例程序如下:
//微信公眾號:嵌入式系統(tǒng)
#include?"stack.h"
#include?"stdio.h"
int?main()
{
????int?val;
????int?buf[20];
????struct?stack?stack;
????struct?stack?*p_stack?=?&stack;
????stack_init(p_stack,?buf,?20);
?//?依次壓入數(shù)據(jù):2、4、5、8
????stack_push(p_stack,?2);
????stack_push(p_stack,?4);
????stack_push(p_stack,?5);
????stack_push(p_stack,?8);
?//?依次彈出各個數(shù)據(jù),并打印
????stack_pop(p_stack,?&val);
????printf("%d?",?val);
????stack_pop(p_stack,?&val);
????printf("%d?",?val);
????stack_pop(p_stack,?&val);
????printf("%d?",?val);
????stack_pop(p_stack,?&val);
????printf("%d?",?val);
????stack_deinit(p_stack);
????return?0;
}
從形式上看,雖然棧類的代碼變得復(fù)雜了一些,但對象內(nèi)存的來源更具有靈活性,使得棧的適用范圍更加廣泛。在部分系統(tǒng)中,在保證對象內(nèi)存來源不受限制的同時,為了特殊情況下的便利性,往往還保留了基于動態(tài)內(nèi)存分配創(chuàng)建對象的方法,在這種情況下,將同時提供create 和 init 兩套接口。
以 FreeRTOS 為例,其提供了兩套創(chuàng)建任務(wù)的接口:xTaskCreate()和 xTaskCreateStatic()。其中,xTaskCreate()函數(shù)中采用動態(tài)內(nèi)存分配的方法獲得了任務(wù)相關(guān)內(nèi)存;而 xTaskCreateStatic()函數(shù)即用于以“靜態(tài)”的方式創(chuàng)建任務(wù),任務(wù)相關(guān)的內(nèi)存需要用戶通過函數(shù)的參數(shù)傳遞(實際上該函數(shù)的作用就類似于 init 初始化函數(shù),只不過其命名為了 Create)。freeRTOS可以作為RTOS開發(fā)入門的基礎(chǔ),具體可參考《FreeRTOS及其應(yīng)用,萬字長文,基礎(chǔ)入門》、《基于RTOS的軟件開發(fā)理論》。
在絕大部分面向?qū)ο缶幊陶Z言中,也有類似于初始化和解初始化的接口,以C++為例,在定義類時,每個類都有構(gòu)造函數(shù)和析構(gòu)函數(shù)兩個特殊的函數(shù)。構(gòu)造函數(shù)就相當(dāng)于這里的初始化函數(shù),其在創(chuàng)建對象時自動調(diào)用;析構(gòu)函數(shù)就相當(dāng)于這里的解初始化函數(shù),其在銷毀對象時自動調(diào)用。例如,以局部變量的形式定義一個對象,則在定義對象時,會自動調(diào)用構(gòu)造函數(shù);在退出當(dāng)前作用域(函數(shù)返回)時,會自動調(diào)用析構(gòu)函數(shù)。高級的面向?qū)ο缶幊陶Z言,為很多操作提供了語法特性上的原生支持,給實際編程帶來了極大的便利。
3 繼承
繼承表示了一種類與類之間的特殊關(guān)系,即 is-a 關(guān)系,例如蘋果是一種水果。A is-a B,表明了 A 只是 B 的一個特例,并不是 B 的全部,A(蘋果)是子類,B(水果)是父類(又稱基類、超類)。
子類是父類的一個特例,可以看作是在父類的基礎(chǔ)上作了一些屬性或方法的擴(kuò)展,子類依然具有父類的屬性和方法。使用繼承關(guān)系在一個已經(jīng)存在的類的基礎(chǔ)上,定義一個新類。新類將自動繼承已存在類的屬性和方法,并可根據(jù)需要添加新的屬性和方法。繼承使子類可以重用父類中已經(jīng)實現(xiàn)的屬性和方法,無需再重復(fù)設(shè)計和編程,以此實現(xiàn)代碼最大限度的復(fù)用。
3.1 “繼承”示例
在 C 語言編程中,在定義子類(子類結(jié)構(gòu)體類型)時,通過將父類作為子類的第一個成員實現(xiàn)繼承。之所以這樣做,是因為在 C 語言結(jié)構(gòu)體中,第一個成員(父類)的地址和結(jié)構(gòu)體自身(子類)的地址相同,當(dāng)子類需要復(fù)用父類的方法時,子類的地址也可以作為父類的地址使用(微信公眾號【嵌入式系統(tǒng)】這是后續(xù)繼承操作取巧的基礎(chǔ))。
例如在一個系統(tǒng)中具有多個棧,為便于區(qū)分,每個棧可以具有不同的名稱(系統(tǒng)棧、數(shù)據(jù)棧、符號棧……)。基于該需求,可以實現(xiàn)一個帶名稱的棧(為便于和前文普通棧區(qū)分,后文將其稱為“命名?!保丛谄胀5幕A(chǔ)上,增加一個“名稱”屬性,該屬性使每個棧都具有一個可供識別的名稱,該棧類型的定義及接口聲明如下:
stack_named.h文件
//微信公眾號:嵌入式系統(tǒng)
#ifndef?__STACK_NAMED_H
#define?__STACK_NAMED_H
#include?"stack.h"????/*?包含基類頭文件*/
struct?stack_named
{
????struct?stack?super;??/*?基類(超類)*/
????const?char?*p_name;??/*?棧名*/
};
/*?初始化?*/
int?stack_named_init(struct?stack_named?*p_stack,?int?*p_buf,?int?size,?const?char?*p_name);
/*?設(shè)置名稱*/
int?stack_named_set(struct?stack_named?*p_stack,?const?char?*p_name);
/*?獲取名稱*/
const?char?*?stack_named_get(struct?stack_named?*p_stack);
/*?解初始化*/
int?stack_named_deinit(struct?stack_named?*p_stack);
#endif
stack_named.c文件
//微信公眾號:嵌入式系統(tǒng)
#include?"stack_named.h"
int?stack_named_init(struct?stack_named?*p_stack,?int?*p_buf,?int?size,?const?char?*p_name)
{
????stack_init(&p_stack->super,?p_buf,?size);?/*?初始化基類*/
????p_stack->p_name?=?p_name;?/*?初始化子類成員?*/
????return?0;
}
int?stack_named_set(struct?stack_named?*p_stack,?const?char?*p_name)
{
????p_stack->p_name?=?p_name;
????return?0;
}
const?char?*?stack_named_get(struct?stack_named?*p_stack)
{
????return?p_stack->p_name;
}
int?stack_named_deinit(struct?stack_named?*p_stack)
{
????return?stack_deinit(&p_stack->super);?/*?解初始化基類*/
}
實現(xiàn)“命名?!睍r,除初始化函數(shù)和解初始化函數(shù)外,僅為新增的屬性p_name 提供了設(shè)置和獲取方法,棧的核心邏輯相關(guān)函數(shù)(入棧、出棧)無需重復(fù)實現(xiàn),入棧和出棧方法作為“命名?!备割惖姆椒?,可以被復(fù)用。使用“命名?!钡膽?yīng)用程序范例如下:
//微信公眾號:嵌入式系統(tǒng)
#include?"stack_named.h"
#include?"stdio.h"
int?main()
{
????int?val;
????int?buf[20];
????struct?stack_named?stack_named;
????struct?stack_named?*p_stack_named?=?&stack_named;
????stack_named_init(p_stack_named,?buf,?20,?"chengj");
????printf("The?stack?name?is?%s!n",?stack_named_get(p_stack_named));
????//?依次壓入數(shù)據(jù):2、4、5、8
????stack_push((struct?stack?*)p_stack_named,?2);??//強(qiáng)制棧類型轉(zhuǎn)換
????stack_push((struct?stack?*)p_stack_named,?4);
????stack_push((struct?stack?*)p_stack_named,?5);
????stack_push((struct?stack?*)p_stack_named,?8);
????//?依次彈出各個數(shù)據(jù),并打印
????stack_pop((struct?stack?*)p_stack_named,?&val);??//強(qiáng)制棧類型轉(zhuǎn)換
????printf("%d?",?val);
????stack_pop((struct?stack?*)p_stack_named,?&val);
????printf("%d?",?val);
????stack_pop((struct?stack?*)p_stack_named,?&val);
????printf("%d?",?val);
????stack_pop((struct?stack?*)p_stack_named,?&val);
????printf("%d?",?val);
????stack_named_deinit(p_stack_named);
????return?0;
}
程序中,因為父類(struct stack)和子類(struct stack_named)對應(yīng)的類型并不相同,所以當(dāng)父類方法(stack_push()、stack_pop())作用于子類對象(stack_named)時,為了避免編譯器輸出“類型不匹配”的警告,必須對類型進(jìn)行強(qiáng)制轉(zhuǎn)換。
在 C 語言中,大量的使用類型強(qiáng)制轉(zhuǎn)換存在一定的風(fēng)險,如兩個類之間沒有繼承關(guān)系,使用強(qiáng)制轉(zhuǎn)換將屏蔽編譯器輸出的警告信息,導(dǎo)致這類錯誤在編譯階段無法發(fā)現(xiàn)。為了避免使用強(qiáng)制類型轉(zhuǎn)換,可以多做一步操作,從子類中取出父類的地址進(jìn)行傳遞,保證參數(shù)類型一致:
stack_push((struct?stack?*)p_stack_named,?2);
//改為
stack_push(&p_stack_named->super,?2);
但無論使用哪種方法,看起來都不是很完美。這類問題的存在主要是因為 C語言并非真正的面向?qū)ο缶幊陶Z言,使用 C 語言實現(xiàn)面向?qū)ο缶幊虝r,需要使用到一些看似“投機(jī)取巧”的手段。在真正的面向?qū)ο缶幊陶Z言中,編譯器可以識別繼承關(guān)系,無需任何強(qiáng)制轉(zhuǎn)換語句,父類的方法可以直接作用于子類。
3.2 初始化函數(shù)
回顧前面命名棧初始化函數(shù):
int?stack_named_init(struct?stack_named?*p_stack,?int?*p_buf,?int?size,?const?char?*p_name)
{
????stack_init(&p_stack->super,?p_buf,?size);?/*?初始化基類*/
????p_stack->p_name?=?p_name;?/*?初始化子類成員?*/
????return?0;
}
先調(diào)用了父類的初始化函數(shù)(stack_init()),再初始化命名棧特有的 p_name 屬性。這里指出了一個隱含的規(guī)則:先初始化基類的成員,再初始化派生類特有的成員。該規(guī)則與面向?qū)ο缶幊陶Z言中構(gòu)造函數(shù)的調(diào)用順序是一致的:在建立一個對象時,首先調(diào)用基類的構(gòu)造函數(shù),然后再調(diào)用派生類的構(gòu)造函數(shù)。
3.3 解初始化函數(shù)
解初始化的順序與初始化的順序是恰好相反的,應(yīng)先對派生類中特有的數(shù)據(jù)“解初始化”,再對基類作解初始化操作。解初始化函數(shù)的實現(xiàn)詳見程序如下:
int?stack_deinit(struct?stack?*p_stack)
{
????p_stack->top?=?0;
????return?0;
}
int?stack_named_deinit(struct?stack_named?*p_stack)
{
????p_stack->p_name?=?NULL;
????return?stack_deinit(&p_stack->super);?/*?解初始化基類在后*/
}
3.4 最少知識原則
所謂 “最少知識原則”就是,對使用者而言,不管類的內(nèi)部如何,只調(diào)用提供的方法,其他的一概不管。(微信公眾號【嵌入式系統(tǒng)】更多編碼原則可以參考《嵌入式軟件設(shè)計原則隨想》)顯然前面的“命名?!辈⒎侨绱?,對于命名棧的使用者,其必須知道命名棧與普通棧之間的繼承關(guān)系,進(jìn)而才可以正確的使用普通棧的入棧方法,操作命名棧,例如:
stack_push((struct?stack?*)p_stack_named,?2);?//類型轉(zhuǎn)換關(guān)系
這對用戶來說并不友好,因為其使用的是“命名?!鳖悾╯tack_named.h),卻還要關(guān)心“普通?!鳖悾╯tack.h)。為滿足“最少知識原則”,命名棧也可以提供入棧和出棧方法,使用戶僅需關(guān)心命名棧的公共接口就可以完成命名棧的所有操作。
stack_named.h文件
#ifndef?__STACK_NAMED_H
#define?__STACK_NAMED_H
#include?"stack.h"
/*?包含基類頭文件*/
struct?stack_named
{
????struct?stack?super;?/*?基類(超類)*/
????const?char?*p_name;?/*?棧名*/
};
/*?初始化?*/
int?stack_named_init(struct?stack_named?*p_stack,?int?*p_buf,?int?size,?const?char?*p_name);
/*?設(shè)置名稱?*/
int?stack_named_set(struct?stack_named?*p_stack,?const?char?*p_name);
/*?獲取名稱?*/
const?char?*?stack_named_get(struct?stack_named?*p_stack);
//微信公眾號:嵌入式系統(tǒng)
static?inline?int?stack_named_push(struct?stack_named?*p_stack,?int?val)
{
????return?stack_push(&p_stack->super,?val);
}
static?inline?int?stack_named_pop(struct?stack_named?*p_stack,?int?*p_val)
{
????return?stack_pop(&p_stack->super,?p_val);
}
/*?解初始化?*/
int?stack_named_deinit(struct?stack_named?*p_stack);
#endif
頭文件中增加了兩個方法:stack_named_push()和 stack_named_pop(),由于這兩個函數(shù)非常簡單,只是調(diào)用了其父類中相應(yīng)的方法,僅一行代碼,因而使用了內(nèi)聯(lián)函數(shù)的形式,如此可以優(yōu)化代碼大小和執(zhí)行速度。經(jīng)過簡單的包裝后,用戶使用的所有方法都是作用于“命名?!睂ο蟮?,無需再使用類型強(qiáng)制轉(zhuǎn)換等特殊的方法。更新后的“命名棧”使用范例片段如下:
//?壓入數(shù)據(jù)
stack_named_push(p_stack_named,?2);
//?彈出數(shù)據(jù)并打印
stack_named_pop(p_stack_named,?&val);?printf("%d?",?val);
從用戶角度看,包裝后的“命名?!睂τ脩魜碇v更加友好(無需類型強(qiáng)制轉(zhuǎn)換)。但在實際開發(fā)過程中,若所有繼承關(guān)系都再次封裝一遍會顯得累贅。因此,只對用戶開放的類才需要這樣做,如果某些類無需對用戶開放,僅在內(nèi)部使用,則可以酌情省略包裝過程。
> 預(yù)知后事如何,且看下集分解 ...