在线观看www成人影院-在线观看www日本免费网站-在线观看www视频-在线观看操-欧美18在线-欧美1级

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

一文了解嵌入式軟件開發的對象

strongerHuang ? 來源:strongerHuang ? 2024-11-15 10:31 ? 次閱讀

以前應用場景很單一,嵌入式開發可能談不上面向對象開發。但現在,做嵌入式開發,沒有面向對象開發,你就有點落伍了。

本文結合個人經驗和周立功《抽象接口技術和組件開發規范及其思想》,循序漸進的用代碼范例說明嵌入式軟件開發的對象,前提你最好有一點點C++基礎。間接說明理論指導實踐的意義。

紙上得來終覺淺,絕知此事要躬行。

1 面向對象編程基礎

面向對象編程涉及到三個重要的特性:封裝、繼承與多態。部分 C 程序員,特別是嵌入式 C 程序員有一種誤解,C 語言不是面向對象的編程語言,C++、JavaPython 等更高級的才是,使用 C 語言無法實現面向對象編程。這種誤解致使他們沒有動力學習一些優秀的面向對象編程方法,例如設計模式、設計原則、軟件架構設計等等,進而很難開發出易維護、易部署、易重用、易管理的軟件,很難面對項目需求的變更、擴展,很難開發和維護大型的復雜項目。

1.1 對象

面向對象編程,“對象”是整個編程過程的關鍵。其常見的解釋是“數據與函數的組合”。每個對象都是由一組數據(用以描述對象的狀態)和一組函數(對象支持的操作,用以描述對象的行為)組成的。對象實現了數據和操作的結合,使數據和操作可以封裝于“對象”這個統一體中。

在面向過程編程中,程序設計注重的是“過程”,先做什么,后做什么;在外界看來,整個程序由一系列散亂的數據和函數組合而成。而在面向對象編程中,程序設計注重的是“對象”,在外界看來,整個程序由一系列“對象”塊組合而成,數據和函數封裝到了對象內部。

1.2 類

對象是有“類型”的,即類。“類”是對一組對象共性的抽象,表示一類對象,而對象是某個類的一個具體化的個例,通常稱之為類的實例。對象通常是由數據和函數組成的,相應的類也具有兩部分內容:屬性(數據的抽象)和方法(對象行為的抽象)。

除了封裝屬性和操作外,類還具有訪問控制的能力,如某些屬性和方法是私有的,不能被外界訪問。通過訪問控制,能夠對內部數據提供不同級別的保護,以防止外界意外地改變或使用私有部分。

1. 屬性

類具有屬性,它是對數據(對象的狀態)的抽象。在 C 程序設計時,通常使用結構體類型來表示一個類,相關屬性即包含在相應的結構體類型中。例如學生具有屬性:姓名、學號、性別、身高、體重等信息,可以使用如下結構體類型表示“學生類”:

//微信公眾號【嵌入式系統】
structstudent
{
charname[10];/*姓名(假定最長10字符)*/
unsignedintid;/*學號*/
charsex;/*性別:'M',男;'F',女*/
floatheight;/*身高*/
floatweight;/*體重*/
};
//提示,關于結構體、枚舉等復雜類型定義推薦使用關鍵字typedef

提示,關于結構體、枚舉等復雜類型定義推薦使用關鍵字 typedef,更多C關鍵字了解可以參考《C語言關鍵字應用技巧》、《高質量嵌入式軟件的開發技巧》。

2. 方法

類具有方法,它是對象行為的抽象,在 C 程序中,方法可以看作普通函數,不過其通常有一個特點 ,函數的第一個參數為類型的指針,指向了一個確定的對象,用以表明此次操作針對哪個對象,在方法實現時,即可通過該指針訪問到對象中的各個屬性。(微信公眾號【嵌入式系統】這是C面向對象必須的,類似C++的this)

針對學生對象,為了對外展現學生自身的信息,自我介紹的格式是對外輸出一個固定格式的字符串:

"Hi! My name is xxx, I'm a (boy/girl). My school number is xxx. My height is xxxcm and weight is xxxkg . "

其中的 xxx 對應學生實際的信息,基于此,可以為學生類定義并實現一個“自我介紹”的方法:

voidstudent_self_introduction(structstudent*p_this)
{
printf("Hi!Mynameis%s,I'ma%s.Myschoolnumberis%d.Myheightis%fcmandweightis%fkg",
p_this->name,
(p_this->sex=='M')?"boy":"girl",
p_this->id,
p_this->height,
p_this->weight);
}

對于外界來講,調用學生的“自我介紹”方法可以獲知學生的全部信息。基于該類的定義,一個簡易的應用程序范例詳如下:

voidmain(void)
{
structstudentchengj={"chengj",2024001,'M',173,68};
structstudenthehe={"hehe",2024002,'M',150,45};
student_self_introduction(&chengj);
student_self_introduction(&hehe);
//...
}

類中的方法 student_self_introduction 可以作用于任一學生類對象,對于程序員來講,編寫的代碼將適用于一組對象,而非特定的某一個對象,提高了代碼利用率。

在實際應用中,對比代碼《嵌入式算法14---數據流與環形隊列》,不少程序員都喜歡編寫出一堆非常類似的接口,它們僅通過某一個數字后綴(0、1、2……)來區分,如系統使用到 3 個棧,初級程序員可能實現 3 個入棧函數,不良示意代碼如下:

//三個棧入棧的不良范例,引以為戒
intpush_stack0(intdata)
{
//...
}

intpush_stack1(intdata)
{
//...
}

intpush_stack2(intdata)
{
//...
}

三個操作可能除了極小部分的差異外,其它處理完全相同,這就是沒有面向對象編程的思維,沒有定義對象類型的概念,將操作直接針對每個具體對象(棧 0、棧 1、棧 2),而不是一組同類的對象(所有棧對象)。顯然,3個棧的特性和行為都基本類似,因而可以定義一個“棧類型”,如此一來,入棧操作將屬于棧類型中的一個方法,適用于所有棧對象。例如:

//數據壓入棧,p_stack指向具體的棧對象
intpush_stack(stack*p_stack,intdata);

//微信公眾號:嵌入式系統
//三個棧的入棧操作均可使用同一個方法
push_stack(p_stack0,1);
push_stack(p_stack1,2);
push_stack(p_stack2,3);

這只是示意性代碼,說明使用“類”的設計解決問題所帶來的優勢。

1.3 UML 類圖

在面向對象的設計和開發過程中,通常使用 UML 工具來進行分析與設計。最基本的就是使用 UML 類圖來表示類以及描述類之間的關系。

在 UML 類圖中,一個矩形框表示一個類,矩形框內部被分隔為上、中、下三部分,上部為類的名字,中部為類的屬性,下面部分為類的方法。對于屬性和方法,還可以使用“+”、“-”修飾符來表示訪問權限,“+”為公有屬性、“-”為私有屬性。如前面的學生類,其類名為 student,屬性包括姓名、學號、性別、身高、體重,方法有“自我介紹”方法,則其對應的類圖如下:

3ff7fd9a-906d-11ef-a511-92fbcf53809c.png

通常情況下,類中的所有屬性均為私有屬性,不建議直接訪問,所有屬性的訪問都通過類提供的方法。基于此,假定了學生類中的所有屬性均為私有屬性,因而在所有屬性前都增加了“-”修飾符。

UML 類圖主要用于輔助分析和設計,設計類時應聚焦在與當前問題有關的重要屬性和行為,無關的屬性和方法可去掉,確保簡潔。由于私有屬性僅在內部使用,外界無需關心,因此UML 類圖中通常不體現私有屬性和方法,除非某些特殊的私有屬性和方法影響到問題的理解或者類的實現。基于此可以簡化。

3ffc0656-906d-11ef-a511-92fbcf53809c.png

2 封裝

類是對一組對象共性的抽象,封裝了屬性和方法;即把一組關聯的數據和函數圈起來,使圈外的代碼只能看見部分函數,數據則完全不可見(微信公眾號【嵌入式系統】一般建議數據的訪問都應通過類提供的方法,而不是全局變量滿天飛)。

2.1 “封裝”示例

在C語言中,可使用一個 C 文件(*.c 文件)和 H 文件(*.h 文件)完成“類”的定義,將所有需要封裝的東西都存于 C 文件中,H 文件中只展現“對外可見、無需封裝”的內容。

以棧的實現為例,將所有實現代碼都存于 C 文件中,H 文件只包含與棧相關接口的聲明,比如入棧和出棧等。頭文件和源文件的示意內容分別詳見如下:

stack.h文件
#ifndef__STACK_H
#define__STACK_H
//微信公眾號:嵌入式系統所有頭文件都必須防止重復引用

/*類型聲明,無需關心類定義的具體細節*/
structstack;

/*創建棧,并指定棧空間的大小*/
structstack*stack_create(intsize);

/*入棧*/
intstack_push(structstack*p_stack,intval);

/*出棧*/
intstack_pop(structstack*p_stack,int*p_val);

/*刪除棧*/
intstack_delete(structstack*p_stack);

#endif
stack.c文件
//微信公眾號:嵌入式系統
#include"stack.h"
#include"stdlib.h"

structstack
{
inttop;/*棧頂*/
int*p_buf;/*棧緩存*/
unsignedintsize;/*棧緩存的大小*/
};

unsignedintsize;/*棧緩存的大小*/

structstack*stack_create(intsize)
{
structstack*p_stack=(structstack*)malloc(sizeof(structstack));
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)
{
returnp_stack;
}
free(p_stack);/*分配棧內存失敗*/
}
returnNULL;/*創建棧失敗,返回NULL*/
}

intstack_push(structstack*p_stack,intval)
{
if(p_stack->top!=p_stack->size)//未滿可入棧
{
p_stack->p_buf[p_stack->top++]=val;
return0;
}
return-1;
}

intstack_pop(structstack*p_stack,int*p_val)
{
if(p_stack->top!=0)//非空可出棧
{
*p_val=p_stack->p_buf[--p_stack->top];
return0;
}
return-1;
}

intstack_delete(structstack*p_stack)
{
if(p_stack==NULL)
{
return-1;
}

if(p_stack->p_buf!=NULL)
{
free(p_stack->p_buf);
}
free(p_stack);
return0;
}

使用 stack.h 的程序沒有 struct stack 結構體成員的訪問權限的,只能調用stack.h 文件中聲明的方法。對于外界用戶來說,struct stack 結構體的內部細節,以及各個函數的具體實現方式都是不可見的。這正是完美的封裝!

由于所有細節都封裝到了 C 文件內部,用戶通過 stack.h 文件并不能看到 struct stack 結構體的具體定義,因此也無法訪問 stack 結構體中的成員。若用戶嘗試訪問 struct stack結構體中的成員,將會編譯報錯。(微信公眾號【嵌入式系統】C 語言不是面向對象的編程語言,實現封裝有擴展性的犧牲)。

C語言實現封裝的一般做法為:在頭文件中進行數據結構以及函數定義的前置聲明,在源文件中完成各函數的具體實現以及數據結構的定義。這樣所有函數實現及定義細節均封裝到了源文件中,對使用者來說是完全不可見的。

2.2 創建對象

2.2.1 內存分配的問題

基于前面創建棧方法,可以創建多個棧對象,例如:

structstack*p_stack1=stack_create(20);
structstack*p_stack2=stack_create(30);
structstack*p_stack3=stack_create(50);

每個棧對象需要兩部分內存:

一是棧對象本身的內存(內存大小為 sizeof(struct stack));

二是該棧對象用于存儲數據的緩存(內存大小為 sizeof(int) * size,其中,size 由用戶在創建 棧時通過參數指定)。

在棧對象的創建函數中,使用 malloc()分配了該對象所需的內存空間,使用 malloc()分配內存空間非常方便,但這種做法也限制了對象內存的來源——必須使用動態內存。但對于嵌入式系統,內存往往是很大的瓶頸,很多應用場合可能并不太適合使用動態內存,主要有以下幾個因素:

1)內存資源不足。運行嵌入式軟件的硬件平臺普遍內存小甚至只有幾k RAM。這種條件下管理使用動態內存是比較浪費的行為,可能產生內存碎片,且內存分配的軟件算法本身也會占用一定的內存空間。

2)實時性要求高。部分嵌入式應用對實時性要求很高,但由于資源的限制,集成的動態內存分配算法不是很完善,使得很難確保動態內存分配的實時性。

3)內存泄漏。動態內存分配可能出現內存泄漏。

4)軟件編程復雜。在可靠的設計中,必須考慮內存分配失敗的情況并對其進行異常處理,如果存在大量的動態內存分配,則處處都需考慮分配失敗的情況。

將對象內存的來源限制為動態內存分配,限制了該類的應用場合,致使部分應用場合因為內存來源的問題不得不放棄該類的使用。

2.2.2 內存來源的探索

在 C 程序開發中,除了使用 malloc()得到一段內存空間外,還可以使用“直接定義變量”的形式分配一段內存。直接定義變量的形式,內存在編譯階段由編譯器負責分配,無需用戶作任何干預。根據變量定義位置的不同,實際內存的開辟位置存在一定的區別,主要有兩類:

局部變量:內存開辟在棧中;
靜態變量(static 修飾的變量)或全局變量:內存開辟在全局靜態存儲區。

兩種變量主要是生命周期的不同:局部變量在退出當前作用域后(比如函數返回),內存自動釋放;靜態變量或全局變量內存開辟在全局靜態存儲區,它們在程序的整個生命周期均有效。

內存可以有 3 種來源,它們的優缺點對比詳見下表:

內存類別 內存位置 生命周期 優點 缺點
動態內存 系統堆 Heap 直到調用free()釋放內存 靈活,可以隨時按需分配和釋放 內存分配可能失敗,花費的時間可能不確定;需要處理內存分配失敗的情況,增加程序的復雜性
靜態內存 全局靜態存儲區(.data、.bss存儲段) 程序的整個運行周期 確定性好,只要程序能夠編譯、鏈接成功,內存一定能夠分配成功 需要編程時確定內存的大小;一直占用內存,無法釋放
棧內存 系統棧(或任務棧) 函數調用周期 自動完成內存的分配和回收 內存太大會導致棧溢出
微信公眾號:嵌入式系統

不同來源的內存各有優劣。前面提到,stack_create()函數將內存的來源限制為僅動態內存不太合理。為了避免內存來源受限,“內存的分配”這一步交由用戶實現,以便用戶根據實際需要自由選擇內存的來源。基于此,可以將對象的創建拆分為兩個獨立的步驟,分配對象所需的內存和初始化對象。

2.2.3 分配對象所需的內存

內存分配的工作交由用戶完成,以便用戶根據實際需要自由選擇。用戶能夠完成內存分配的前提是:用戶知道應該分配的內存大小。前面提到,每個棧對象需要兩部分內存:一是棧對象本身的內存(內存大小為:sizeof(struct stack));二是該棧對象用于存儲數據的緩存(內存大小為 sizeof(int) * size,其中,size 由用戶在創建棧時通過參數指定)。

1、棧對象本身的內存

棧對象本身的內存大小為 sizeof(struct stack),若用戶直接采用靜態內存分配的方式(直接定義一個變量),則形式如下:

structstackmy_stack;

也可以繼續采用動態內存的分配方式,例如:

structstack*p_stack=(structstack*)malloc(sizeof(structstack));

但是,若將這兩行代碼直接放到主程序中會無法編譯,因為之前描述的“封裝”特性,使外界看不到 struct stack 的具體定義,也就是說,對于外界而言,該類型僅僅只是聲明并未定義,該類型對應變量的大小對外也是未知的。

在 C 語言中定義一個變量時,編譯器將負責該變量所占用內存的分配。內存的大小與類型相關,要完成變量內存的分配,編譯器必須知道變量所占用的存儲空間大小。當一個變量的類型未定義時,無法完成該類型對應變量的定義,因此,如下語句在編譯時會出錯:

structstackmy_stack;

同理,sizeof 語句用于獲得相應類型數據的大小,而未定義的類型顯然是不知道其大小的,動態內存分配中所使用的 sizeof(struct stack)語句也是錯誤的。

也許部分人會有疑問,既然該類型未定義,為什么在主程序中定義該類型的指針變量卻可以呢?

structstack*p_stack=//...

雖然 struct stack 類型未定義,但在之前已經聲明,因此,編譯器知道它是一個“合法的結構體類型”。此外,這里定義的是一個指針變量,在特定系統中,指針變量所占用的內存大小是確定的,例如,在 32 位系統中,指針通常占用 4 個字節。即指針變量所占用的內存空間大小與其指向的數據類型無關,編譯器無需知道其指向的數據類型,就可完成指針變量內存的分配。因此,一個類型未定義,只要其聲明了,就可以定義該類型的指針變量。但需要注意的是,在完成該類型的定義之前,不得嘗試訪問該指針所指向的內容。

完成內存的分配,提供三種方案。

(1) 將類的具體定義放到 H 文件中

為了使用戶知道對象內存的大小,一種最簡單的辦法是直接將類型的定義放在 H 文件中。更新后的 H 文件示意代碼如下:

stack.h文件
#ifndef__STACK_H
#define__STACK_H

/*類型定義*/
structstack
{
inttop;/*棧頂*/
int*p_buf;/*棧緩存*/
unsignedintsize;/*棧緩存的大小*/
};

//......其它函數聲明
#endif

此時,對于外界,類型已經定義,如下語句均可正常使用:

structstackmy_stack;//靜態內存分配
structstack*p_stack=(structstack*)malloc(sizeof(structstack));//動態內存分配

由于類型的定義存放到了 H 文件中,暴露了類中的成員,在一定程度上破壞了類的“封裝”性。此時外界可以直接訪問類中的數據成員。犧牲一定的封裝性,換來內存分配的靈活性,這也是在嵌入式系統中,基于 C 語言實現面向對象編程的一般做法(數據結構定義存放在 H 文件中更加符合程序員的編程風格)。嵌入式軟件大多數類定義在 H文件中,并沒有封裝在 C 文件中。

雖然類的定義存放在 H 文件中,但出于封裝性考慮,外界任何時候都不應直接訪問對象中的數據,應該將其視為使用 C 語言實現面向對象編程的一條準則。軟件開發需要遵守兩個規則:一是在設計類時,應考慮到用戶可能訪問的數據,并為這些數據提供相應的訪問接口;二是在使用別人提供的類時,除非有特殊說明,否則都不應該嘗試直接訪問類中的數據。

這種方法是目前嵌入式系統中使用得最為廣泛的一種方法,因此后文使用這種方法討論。

(2) 在 H 文件中定義一個新的結構體類型

為了繼續保持類的封裝性,類的定義依然保留在 C 文件中。只不過與此同時,在 H 文件中定義一個新的結構體類型。在該結構體類型中,各個成員的順序和類型與類定義完全一致,僅命名不同。

structstack_mem
{
intdummy1;
int*dummy2;
unsignedintdummy3;
};

各成員的順序和類型均與 struct stack 的定義完全相同,以此保證兩個類型數據所需要的內存空間完全一致。同時,為了屏蔽各個成員的具體含義,所有成員均以 dummy 開頭進行命名。對于外界來講,可以基于 struct stack_mem 類型完成內存的分配,例如:

structstack_memmy_stack;//靜態內存分配
structstack*p_stack=(structstack*)malloc(sizeof(structstack_mem));//動態內存分配

使用這種方案,類的實際定義依然沒有暴露給外界,繼續保持了良好的封裝。(微信公眾號【嵌入式系統】實際上FreeRTOS中,很多地方都采用了這種方法)。但這里定義了一個新的類型,給用戶理解上造成了一定的困擾,此外,為確保兩個類型完全一致,就要求類的設計者在修改類的定義時,必須確保 struct stack_mem 類型也同步修改,這給類的維護工作帶來了挑戰;稍有不慎,某一個類型沒有同步修改就可能造成嚴重的錯誤,且這種錯誤編譯器不會給出任何提示,非常隱蔽。關于代碼審查可以參考《代碼審查那些事》、《代碼的保養》。

(3) 使用宏的形式告知對象所需的內存大小

既然外界只需要知道對象內存的大小,可以在開發過程中使用 sizoeof()獲得struct stack 類型的大小,然后將其以宏的形式定義在 H 文件中。例如在 32 位系統中,使用 sizeof()獲知 struct stack 類型的長度為 12,則可以在 H 文件中定義一個宏,例如:

#defineSTACK_MEM_SIZE12

用戶使用該宏完成內存分配,例如:

unsignedcharstack_mem[STACK_MEM_SIZE];

這種做法僅僅在頭文件中新增了一個宏定義,類的定義依然保持的 C 文件中,“封裝”完全沒有被破壞,看起來也非常完美。但這種做法也存在一些問題,因而很少采用。

a)對于同一個類型,不同系統中 sizeof()的結果可能不同。類型的長度與系統和編譯器均相關。以 int 類型為例,在 32 位系統中為 32 位(4 字節),但 16 位系統中,其位寬可能為 16 位(2 字節)。因此,同樣是 sizeof(int),結果可能為 4,也可能為 2。使用 sizeof()獲取類型的長度時,不同系統中獲取的結果可能并不相同。這就導致 H 文件中的宏定義,切換平臺需要重新測試驗證。同時,由于類型的定義封裝到了 C 文件中,因此修改過程只能有類的開發者完成,一般用戶還無法完成,這就使得該類的跨平臺特性很差,移植有風險。

b)內存不僅有大小的要求,還有內存對齊的要求。

因此,通過一個宏告知用戶需要分配的內存空間大小并不是十分合適,會遇到跨平臺、內存對齊等多個注意事項,用戶可能在不經意間出錯。在實際嵌入式系統中很少使用。一些編碼技能可以參考《高質量嵌入式軟件的開發技巧》。

2、存儲數據的緩存

存儲數據的緩存大小為 sizeof(int) *size,其中的 size 本身就是由用戶指定的,這部分內存的大小用戶很容易得知,進而完成內存的分配。可以采用靜態內存分配的方式(直接定義一個變量)完成內存的分配:

intbuf[20];

也可以采用動態內存分配的方式完成內存的分配:

int*p_buf=(int*)malloc(sizeof(int)*20);

2.2.4 內存小曲

內存的來源主要有三種:動態內存、靜態內存和棧內存,具體如何選擇按實際情況。

對象類別 應用場合
動態對象 不會頻繁創建、銷毀對象的應用;內存占用太大的對象
靜態對象 確定性要求較高,長生命周期的對象
棧對象 函數內部使用的臨時對象;對象內存占用較小的對象

一些入式應用對確定性要求較高,建議優先使用靜態對象。如此一來只要能夠編譯(包含鏈接)成功,應用程序往往就可以按照確定的流程正確執行;若使用動態對象,則必須考慮對象創建失敗的情況。偶爾使用的大塊內存則建議使用動態內存,使用注意和防范可參考《動態內存管理及防御性編程》。

2.3 初始化對象

初始化對象的具體細節用戶不需要關心,指定棧對象的地址、緩存地址及緩存大小,基于此,可以定義初始化函數的原型為:

intstack_init(structstack*p_stack,int*p_buf,intsize);

對于棧來講,棧頂索引(top)的初始值恒為 0,因此該值無需通過初始化函數的參數傳遞。int 類型的返回值常用于表示執行的結果(微信公眾號【嵌入式系統】建議非指針類型的返回值,以0表示成功,負數表示失敗)。該函數的實現示意如下:

//微信公眾號:嵌入式系統
intstack_init(structstack*p_stack,int*p_buf,intsize)
{
p_stack->top=0;
p_stack->size=size;
p_stack->p_buf=p_buf;
return0;
}

該初始化函數的實現僅作為原理性展示,沒有做過多的錯誤處理或參數檢查,實際應用中,p_stack 為 NULL 或 p_buf 為 NULL 等情況都是錯誤情況,后續范例也會省去部分參數校驗)。

至此,完成了將創建對象分離為“分配對象所需的內存”和“初始化對象”兩個步驟,對象內存的來源交由用戶決定,用戶根據需要獲得內存后,再將相關內存的首地址傳遞給初始化函數。

2.4 銷毀對象

實現 stack_create()以及對應的stack_delete(),設計該函數的初衷是當一個棧對象不會再被使用時,可以通過該函數釋放棧占用的資源,比如釋放在 stack_create()函數中使用 malloc()分配的內存資源。

當將 stack_create()拆分為兩步后,內存的分配將由用戶決定,對應地內存的釋放也應由用戶決定。回顧 stack_delete()函數的實現,該函數目前只做了內存釋放相關的操作,當不需要釋放內存時,該函數看起來沒有存在的必要。實際上,stack_delete()和 stack_create()函數是對應的,當將 stack_create()拆分為“分配對象所需的內存”和“初始化對象”兩個步驟后,stack_delete()也應該相應的拆分為兩個步驟:“釋放對象占用的內存”和“解初始化對象”(微信公眾號【嵌入式系統】解初始化或者反初始化,不用太在意這個操作的名稱,只要理解表達的意思是初始化的逆過程即可,init:deinit,關于命名的英文集客參考《嵌入式軟件命名常用英文集》)。

1. 釋放對象占用的內存

前面已經提到,釋放內存交由用戶處理,釋放方法與內存的來源相關。

動態內存的釋放動態內存分配應使用相應的釋放內存函數(如 free())進行釋放。在釋放時應確保分配的內存全部被有效釋放。若某一部分內存被遺漏,將造成內存泄漏。隨著程序的長期運行,內存不斷泄漏可能導致系統崩潰。

靜態內存的釋放使用靜態內存(定義變量的形式),則內存的釋放是系統自動完成的。若將對象定義為局部變量,內存開辟在系統棧中,則退出當前作用域后(函數返回)自動釋放;若將對象定義為靜態變量(static)或全局變量,則內存開辟在全局靜態區,該區域的內存在應用程序的整個生命周期均有效,無法釋放。

2. 解初始化對象

釋放內存已交由用戶處理,對于類的設計來講,重點是設計“解初始化對象”對應的函數,該函數與 stack_init()函數對應,通常命名為“*_deinit”,即:stack_deinit()。該函數通常用于釋放在初始化對象時占用的其它資源。

對于純軟件對象(與硬件無關的軟件),通常其只會占用內存資源,不會額外占用其它資源,對這類對象解初始化時可能無需做任何事情。例如前面關于棧的實現,在stack_init()函數中僅對幾個屬性進行了賦值,沒有額外占用其它任何資源,此時,stack_deinit()可能無需做任何事情,成為一個空函數。

intstack_deinit(structstack*p_stack)
{
return0;
}

在嵌入式系統中,經常會遇到與硬件相關的對象,其初始化時往往會占用一定的硬件資源:I/O 引腳、系統中斷、系統總線。在解初始化這種對象時,應同時釋放占用的資源。可重點關注對象的初始化函數,查看其中是否分配、占用了某些資源。若有,則在解初始化函數中作相應的釋放操作;若無,則解初始化函數留空。為了提高軟件的簡潔性,也可刪除了空的解初始化函數,但這里為了展示軟件結構,依然保留了解初始化函數。

將原 H 文件中的創建接口更新為初始化接口,刪除接口更新為解初始化接口,更新后的 H 文件內容和 C 文件如下:

stack.h文件
//微信公眾號:嵌入式系統
#ifndef__STACK_H
#define__STACK_H

/*類型定義*/
structstack
{
inttop;/*棧頂*/
int*p_buf;/*棧緩存*/
unsignedintsize;/*棧緩存的大小*/
};

/*初始化*/
intstack_init(structstack*p_stack,int*p_buf,intsize);

/*入棧*/
intstack_push(structstack*p_stack,intval);

/*出棧*/
intstack_pop(structstack*p_stack,int*p_val);

/*解初始化*/
intstack_deinit(structstack*p_stack);

#endif
stack.c文件
//微信公眾號:嵌入式系統
#include"stack.h"

intstack_init(structstack*p_stack,int*p_buf,intsize)
{
p_stack->top=0;
p_stack->size=size;
p_stack->p_buf=p_buf;
return0;
}

intstack_push(structstack*p_stack,intval)
{
if(p_stack->top!=p_stack->size)
{
p_stack->p_buf[p_stack->top++]=val;
return0;
}
return-1;
}

intstack_pop(structstack*p_stack,int*p_val)
{
if(p_stack->top!=0)
{
*p_val=p_stack->p_buf[--p_stack->top];
return0;
}
return-1;
}

intstack_deinit(structstack*p_stack)
{
return0;
}

3. 銷毀對象的順序

創建對象時是先分配對象所需內存,再初始化對象,因為在初始化對象時,需要傳遞相應內存空間的首地址作為初始化函數的參數。這就保證了在初始化對象之前,必須完成相關內存的分配。而銷毀一個對象時,釋放內存與調用解初始化函數并不能通過接口進行制約,銷毀過程與創建恰恰相反,應先解初始化對象,再釋放對象占用的內存。因為在解初始化對象時,還會使用到對象中的數據,若先釋放對象占用的內存,則對象在被解初始化之前,就被徹底銷毀了,對象已經不存在了,顯然無法再進行解初始化操作。

若內存來源于動態內存分配,則完整的應用程序范例如下:

//微信公眾號:嵌入式系統
#include"stack.h"
#include"stdio.h"
#include"stdlib.h"
intmain()
{
intval;
structstack*p_stack=(structstack*)malloc(sizeof(structstack));
int*p_buf=(int*)malloc(sizeof(int)*20);

//初始化
stack_init(p_stack,buf,20);

//依次壓入數據:2、4、5、8
stack_push(p_stack,2);
stack_push(p_stack,4);
stack_push(p_stack,5);
stack_push(p_stack,8);

//依次彈出各個數據,并打印
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);

//釋放內存
free(p_stack);
free(p_buf);

return0;
}

若內存來源于靜態內存分配,則內存的分配和釋放完全由系統自行完成,如內存以“局部變量”的形式分配,范例程序如下:

//微信公眾號:嵌入式系統
#include"stack.h"
#include"stdio.h"
intmain()
{
intval;
intbuf[20];
structstackstack;
structstack*p_stack=&stack;

stack_init(p_stack,buf,20);

//依次壓入數據:2、4、5、8
stack_push(p_stack,2);
stack_push(p_stack,4);
stack_push(p_stack,5);
stack_push(p_stack,8);

//依次彈出各個數據,并打印
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);
return0;
}

從形式上看,雖然棧類的代碼變得復雜了一些,但對象內存的來源更具有靈活性,使得棧的適用范圍更加廣泛。在部分系統中,在保證對象內存來源不受限制的同時,為了特殊情況下的便利性,往往還保留了基于動態內存分配創建對象的方法,在這種情況下,將同時提供create 和 init 兩套接口。

以 FreeRTOS 為例,其提供了兩套創建任務的接口:xTaskCreate()和 xTaskCreateStatic()。其中,xTaskCreate()函數中采用動態內存分配的方法獲得了任務相關內存;而 xTaskCreateStatic()函數即用于以“靜態”的方式創建任務,任務相關的內存需要用戶通過函數的參數傳遞(實際上該函數的作用就類似于 init 初始化函數,只不過其命名為了 Create)。freeRTOS可以作為RTOS開發入門的基礎,具體可參考《FreeRTOS及其應用,萬字長文,基礎入門》、《基于RTOS的軟件開發理論》。

在絕大部分面向對象編程語言中,也有類似于初始化和解初始化的接口,以C++為例,在定義類時,每個類都有構造函數和析構函數兩個特殊的函數。構造函數就相當于這里的初始化函數,其在創建對象時自動調用;析構函數就相當于這里的解初始化函數,其在銷毀對象時自動調用。例如,以局部變量的形式定義一個對象,則在定義對象時,會自動調用構造函數;在退出當前作用域(函數返回)時,會自動調用析構函數。高級的面向對象編程語言,為很多操作提供了語法特性上的原生支持,給實際編程帶來了極大的便利。

3 繼承

繼承表示了一種類與類之間的特殊關系,即 is-a 關系,例如蘋果是一種水果。A is-a B,表明了 A 只是 B 的一個特例,并不是 B 的全部,A(蘋果)是子類,B(水果)是父類(又稱基類、超類)。

子類是父類的一個特例,可以看作是在父類的基礎上作了一些屬性或方法的擴展,子類依然具有父類的屬性和方法。使用繼承關系在一個已經存在的類的基礎上,定義一個新類。新類將自動繼承已存在類的屬性和方法,并可根據需要添加新的屬性和方法。繼承使子類可以重用父類中已經實現的屬性和方法,無需再重復設計和編程,以此實現代碼最大限度的復用。

3.1 “繼承”示例

在 C 語言編程中,在定義子類(子類結構體類型)時,通過將父類作為子類的第一個成員實現繼承。之所以這樣做,是因為在 C 語言結構體中,第一個成員(父類)的地址和結構體自身(子類)的地址相同,當子類需要復用父類的方法時,子類的地址也可以作為父類的地址使用(微信公眾號【嵌入式系統】這是后續繼承操作取巧的基礎)。

例如在一個系統中具有多個棧,為便于區分,每個棧可以具有不同的名稱(系統棧、數據棧、符號棧……)。基于該需求,可以實現一個帶名稱的棧(為便于和前文普通棧區分,后文將其稱為“命名棧”),即在普通棧的基礎上,增加一個“名稱”屬性,該屬性使每個棧都具有一個可供識別的名稱,該棧類型的定義及接口聲明如下:

stack_named.h文件
//微信公眾號:嵌入式系統
#ifndef__STACK_NAMED_H
#define__STACK_NAMED_H

#include"stack.h"/*包含基類頭文件*/

structstack_named
{
structstacksuper;/*基類(超類)*/
constchar*p_name;/*棧名*/
};

/*初始化*/
intstack_named_init(structstack_named*p_stack,int*p_buf,intsize,constchar*p_name);

/*設置名稱*/
intstack_named_set(structstack_named*p_stack,constchar*p_name);

/*獲取名稱*/
constchar*stack_named_get(structstack_named*p_stack);

/*解初始化*/
intstack_named_deinit(structstack_named*p_stack);

#endif
stack_named.c文件
//微信公眾號:嵌入式系統
#include"stack_named.h"

intstack_named_init(structstack_named*p_stack,int*p_buf,intsize,constchar*p_name)
{
stack_init(&p_stack->super,p_buf,size);/*初始化基類*/
p_stack->p_name=p_name;/*初始化子類成員*/
return0;
}

intstack_named_set(structstack_named*p_stack,constchar*p_name)
{
p_stack->p_name=p_name;
return0;
}

constchar*stack_named_get(structstack_named*p_stack)
{
returnp_stack->p_name;
}

intstack_named_deinit(structstack_named*p_stack)
{
returnstack_deinit(&p_stack->super);/*解初始化基類*/
}

實現“命名棧”時,除初始化函數和解初始化函數外,僅為新增的屬性p_name 提供了設置和獲取方法,棧的核心邏輯相關函數(入棧、出棧)無需重復實現,入棧和出棧方法作為“命名棧”父類的方法,可以被復用。使用“命名棧”的應用程序范例如下:

//微信公眾號:嵌入式系統
#include"stack_named.h"
#include"stdio.h"
intmain()
{
intval;
intbuf[20];
structstack_namedstack_named;
structstack_named*p_stack_named=&stack_named;

stack_named_init(p_stack_named,buf,20,"chengj");
printf("Thestacknameis%s!
",stack_named_get(p_stack_named));

//依次壓入數據:2、4、5、8
stack_push((structstack*)p_stack_named,2);//強制棧類型轉換
stack_push((structstack*)p_stack_named,4);
stack_push((structstack*)p_stack_named,5);
stack_push((structstack*)p_stack_named,8);

//依次彈出各個數據,并打印
stack_pop((structstack*)p_stack_named,&val);//強制棧類型轉換
printf("%d",val);
stack_pop((structstack*)p_stack_named,&val);
printf("%d",val);
stack_pop((structstack*)p_stack_named,&val);
printf("%d",val);
stack_pop((structstack*)p_stack_named,&val);
printf("%d",val);

stack_named_deinit(p_stack_named);
return0;
}

程序中,因為父類(struct stack)和子類(struct stack_named)對應的類型并不相同,所以當父類方法(stack_push()、stack_pop())作用于子類對象(stack_named)時,為了避免編譯器輸出“類型不匹配”的警告,必須對類型進行強制轉換。

在 C 語言中,大量的使用類型強制轉換存在一定的風險,如兩個類之間沒有繼承關系,使用強制轉換將屏蔽編譯器輸出的警告信息,導致這類錯誤在編譯階段無法發現。為了避免使用強制類型轉換,可以多做一步操作,從子類中取出父類的地址進行傳遞,保證參數類型一致:

stack_push((structstack*)p_stack_named,2);
//改為
stack_push(&p_stack_named->super,2);

但無論使用哪種方法,看起來都不是很完美。這類問題的存在主要是因為 C語言并非真正的面向對象編程語言,使用 C 語言實現面向對象編程時,需要使用到一些看似“投機取巧”的手段。在真正的面向對象編程語言中,編譯器可以識別繼承關系,無需任何強制轉換語句,父類的方法可以直接作用于子類。

3.2 初始化函數

回顧前面命名棧初始化函數:

intstack_named_init(structstack_named*p_stack,int*p_buf,intsize,constchar*p_name)
{
stack_init(&p_stack->super,p_buf,size);/*初始化基類*/
p_stack->p_name=p_name;/*初始化子類成員*/
return0;
}

先調用了父類的初始化函數(stack_init()),再初始化命名棧特有的 p_name 屬性。這里指出了一個隱含的規則:先初始化基類的成員,再初始化派生類特有的成員。該規則與面向對象編程語言中構造函數的調用順序是一致的:在建立一個對象時,首先調用基類的構造函數,然后再調用派生類的構造函數。

3.3 解初始化函數

解初始化的順序與初始化的順序是恰好相反的,應先對派生類中特有的數據“解初始化”,再對基類作解初始化操作。解初始化函數的實現詳見程序如下:

intstack_deinit(structstack*p_stack)
{
p_stack->top=0;
return0;
}

intstack_named_deinit(structstack_named*p_stack)
{
p_stack->p_name=NULL;
returnstack_deinit(&p_stack->super);/*解初始化基類在后*/
}

3.4 最少知識原則

所謂 “最少知識原則”就是,對使用者而言,不管類的內部如何,只調用提供的方法,其他的一概不管。(微信公眾號【嵌入式系統】更多編碼原則可以參考《嵌入式軟件設計原則隨想》)顯然前面的“命名棧”并非如此,對于命名棧的使用者,其必須知道命名棧與普通棧之間的繼承關系,進而才可以正確的使用普通棧的入棧方法,操作命名棧,例如:

stack_push((structstack*)p_stack_named,2);//類型轉換關系

這對用戶來說并不友好,因為其使用的是“命名棧”類(stack_named.h),卻還要關心“普通棧”類(stack.h)。為滿足“最少知識原則”,命名棧也可以提供入棧和出棧方法,使用戶僅需關心命名棧的公共接口就可以完成命名棧的所有操作。

stack_named.h文件
#ifndef__STACK_NAMED_H
#define__STACK_NAMED_H

#include"stack.h"

/*包含基類頭文件*/

structstack_named
{
structstacksuper;/*基類(超類)*/
constchar*p_name;/*棧名*/
};

/*初始化*/
intstack_named_init(structstack_named*p_stack,int*p_buf,intsize,constchar*p_name);

/*設置名稱*/
intstack_named_set(structstack_named*p_stack,constchar*p_name);

/*獲取名稱*/
constchar*stack_named_get(structstack_named*p_stack);

//微信公眾號:嵌入式系統
staticinlineintstack_named_push(structstack_named*p_stack,intval)
{
returnstack_push(&p_stack->super,val);
}
staticinlineintstack_named_pop(structstack_named*p_stack,int*p_val)
{
returnstack_pop(&p_stack->super,p_val);
}

/*解初始化*/
intstack_named_deinit(structstack_named*p_stack);

#endif

頭文件中增加了兩個方法:stack_named_push()和 stack_named_pop(),由于這兩個函數非常簡單,只是調用了其父類中相應的方法,僅一行代碼,因而使用了內聯函數的形式,如此可以優化代碼大小和執行速度。經過簡單的包裝后,用戶使用的所有方法都是作用于“命名棧”對象的,無需再使用類型強制轉換等特殊的方法。更新后的“命名棧”使用范例片段如下:

//壓入數據
stack_named_push(p_stack_named,2);

//彈出數據并打印
stack_named_pop(p_stack_named,&val);printf("%d",val);

從用戶角度看,包裝后的“命名棧”對用戶來講更加友好(無需類型強制轉換)。但在實際開發過程中,若所有繼承關系都再次封裝一遍會顯得累贅。因此,只對用戶開放的類才需要這樣做,如果某些類無需對用戶開放,僅在內部使用,則可以酌情省略包裝過程。

4 多態

多態字面含義就是具有“多種形式”。從調用者的角度看對象,會發現它們非常相似,但內部處理實際上卻各不相同。換句話說,各對象雖然內部處理不同,但對于使用者(調用者)來講,它們卻是相同的。

4.1 學生的“自我介紹”

在前面提到的學生類,包含姓名、學號、性別、身高、體重等屬性,并對外提供了一個“自我介紹”方法。

voidstudent_self_introduction(structstudent*p_this)
{
printf("Hi!Mynameis%s,I'ma%s.Myschoolnumberis%d.Myheightis%fcmandweightis%fkg",
p_this->name,
(p_this->sex=='M')?"boy":"girl",
p_this->id,
p_this->height,
p_this->weight);
}

假設一個場景,開學第一課所有同學依次作一個簡單的自我介紹,調用所有同學的自我介紹方法即可,范例程序如下:

voidfirst_class(structstudent*p_students,intnum)
{
inti;
for(i=0;i

調用該函數前,需要將所有學生對象創建好,并存于一個數組中,假定一個班級有 50個學生,則調用示意代碼如下:

intmain()
{
structstudentstudent[50];

/*根據每個學生的信息,依次創建各個學生對象*/
student_init(&student[0],"zhangsan",2024001,'M',173,60);
student_init(&student[1],"lisi",2024002,'F',168,65);
//...

/*上第一節課*/
first_class(student,50);
}

上面的實現代碼,假定了學生的“自我介紹”格式是完全相同的,都是將個人信息陳述一遍,顯然,這樣的自我介紹無法體現每個學生的個性和差異。例如,一個名叫張三的學生,其期望這樣介紹自己:

“親愛的老師,同學們!我叫張三,來自湖北仙桃,是一個自信開朗,積極向上的人,我有著廣泛的興趣愛好,喜歡打籃球、看書、下棋、聽音樂……”

每個學生自我介紹的內容并不期望千篇一律。若不基于多態的思想,最簡單粗暴的方式是每個學生都提供一個自我介紹方法,例如 student_zhangsan_introduction()。這種情況下每個學生提供的方法都不相同(函數名不同),根本無法統一調用,此時,第一節課的調用將會大改,需要依次調用每個學生提供的不同的自我介紹方法,例如:

voidfirst_class()
{
student_zhangsan_introduction(&zhangshan);//張三自我介紹
student_lisi_introduction(&lisi);//李四自我介紹
//….
}

無法使用同樣的調用形式(函數)完成不同對象的“自我介紹”。對于調用者來講,需要關注每個對象提供的特殊方法,復雜度將提升。

使用多態的思想即可很好的解決這個問題,進而保證 firstt_class()的內容不變,雖然每個對象方法的實現不同,但可以使用同樣的形式調用它。在 C 語言中,函數指針就是解決這個問題的“利器”。

函數指針的原型決定了調用方法,例如定義函數指針:

int(*student_self_introduction)(structstudent*p_student);

無論該函數指針指向何處,都表示該函數指針指向的是 int 類型返回值,具有一個*p_student 參數的函數,其調用形式如下:

student_self_introduction(p_student);

函數指針的指向代表了函數的實現,指向不同的函數就代表了不同的實現。基于此,為了使每個學生對象可以有自己獨特的介紹方式,在學生類的定義中,可以不實現自我介紹方法,但可以通過函數指針約定自我介紹方法的調用形式。更新學生類的定義:

student.h文件```

```c
//微信公眾號:嵌入式系統
#ifndef__STUDENT_H
#define__STUDENT_H

structstudent
{
int(*student_self_introduction)(structstudent*p_student);/*新增個性化自我介紹*/
charname[10];/*姓名(假定最長10字符)*/
unsignedintid;/*學號*/
charsex;/*性別:'M',男;'F',女*/
floatheight;/*身高*/
floatweight;/*體重*/
};

intstudent_init(structstudent*p_student,
char*p_name,
unsignedintid,
charsex,
floatheight,
floatweight,
int(*student_self_introduction)(structstudent*));

/*學生類提供的自我介紹方法*/
staticinlineintstudent_self_introduction(structstudent*p_student)
{
returnp_student->student_self_introduction(p_student);
}

#endif

此時,對于外界來講,學生類“自我介紹方法”的調用形式并未發生任何改變,函數原型還是一樣的(由于只有一行代碼,因而以內聯函數的形式存放到了頭文件中)。基于此,“第一節課的內容”可以保持完全不變(for循環調用全部)。在這種方式下,每個對象在初始化時,需要指定自己特殊的自我介紹方,例如張三對象的創建過程為:

intstudent_zhangsan_introduction(structstudent*p_student)
{
constchar*str="親愛的老師,同學們!我叫張三,來自湖北仙桃,是一個自信開朗,積極向上的人,我有著廣泛的興趣愛好,喜歡打籃球、看書、下棋、聽音樂……";

printf("%s
",str);
return0;
}

intmain()
{
structstudentstudent[50];

/*根據每個學生的信息,依次創建各個學生對象*/
student_init(&student[0],"zhangsan",2024001,'M',173,60,student_zhangsan_introduction);

//...

/*上第一節課*/
first_class(student,50);
}

多態的核心是:對于上層調用者,不同的對象可以使用完全相同的操作方法,但是每個對象可以有各自不同的實現方式。多態是面向對象編程非常重要的特性,C 語言依賴指針實現多態

很多設計模式或硬件多型號適配都是基于這個基礎,可以參考《嵌入式軟件的設計模式(上)》)。

4.2 I/O 設備驅動

C 程序使用 printf()打印日志信息,在 PC 上運行時,日志信息可能輸出到控制臺,而在嵌入式系統中,信息可能通過某個串口輸出。printf()函數的解釋是輸出信息至 STDOUT(標準輸出)。顯然printf()函數就具有多態性,對于用戶來講,其調用形式是確定的,但內部具體輸出信息到哪里,卻會隨著 STDOUT 的不同而不同。

在一些操作系統中(如Linux),硬件設備(例串口、ADC 等)的操作方法都和文件操作方法類似(一切皆文件),都可以通過 open()、close()、read()、write()等幾個標準函數進行操作。為統一 I/O 設備的使用方法,要求每個 I/O 設備都提供 open、close、read、write 這幾個標準函數的實現,即每個 I/O設備的驅動程序,對這些標準函數的實現在函數調用上必須保持一致。這本質上就是一個多態問題,即以同樣的方法使用不同的 I/O 設備。

通過函數指針解決這個問題,首先定義file_ops結構體,包含了相對應的函數指針,指向I/O 設備針對操作的實現函數。

file_ops.h文件
//微信公眾號:嵌入式系統
//代碼片段只是原理性展示
structfile_ops
{
void(*open)(char*name,intmode);
void(*close)();
int(*read)();
void(*write)();
};

對于 I/O設備,其驅動程序提供這 4個函數的實現,并將 file_ops結構體的函數指針指向對應的函數。

#include"file_ops.h"

staticvoidopen(char*name,intmode)
{
//...
}

staticvoidclose()
{
//...
}

staticintread()
{
//...
}

staticvoidwrite()
{
//...
}

structfile_opsmy_console={open,close,read,write};

所有的函數都使用 static修飾符,避免與外部的函數產生命名沖突。對于該設備,僅對外提供了一個可以使用的 file_ops 對象 my_console。

上面展示了設備 I/O 的一般管理方法,其中的編程方法或技巧正是面向對象編程中多態的基礎,也再一次展現了函數指針在多態中的重要地位,多態可以視為函數指針的一種典型應用。(微信公眾號【嵌入式系統】類似使用是Linux設備驅動的基礎)。

4.3 帶檢查功能的棧

前面范例實現了棧的核心邏輯(入棧和出棧),假設現在增加需求,實現“帶檢查功能的棧”,即在數據入棧之前,必須進行特定的檢查,“檢查通過”后才能壓人棧中。檢查方式有多種:

范圍檢查:必須在特定的范圍之內,比如1 ~ 9,才視為檢查通過;
奇偶檢查:必須是奇數或者偶數,才視為檢查通過;
變化檢查:值必須增加(比上一次的值大),才視為檢查通過。

4.3.1 基于繼承實現“帶范圍檢查功能”的棧

先不考慮多種檢查方式,僅實現范圍檢查。參照“命名棧”的實現,使用繼承方式,在普通棧的基礎上實現一個新類,范例程序如下:

stack_with_range_check.h帶范圍檢查的棧
#ifndef__STACK_WITH_RANGE_CHECK_H
#define__STACK_WITH_RANGE_CHECK_H

#include"stack.h"/*包含基類頭文件*/

structstack_with_range_check
{
structstacksuper;/*基類(超類)*/
intmin;/*最小值*/
intmax;/*最大值*/
};

intstack_with_range_check_init(structstack_with_range_check*p_stack,
int*p_buf,
intsize,
intmin,intmax);

/*入棧*/
intstack_with_range_check_push(structstack_with_range_check*p_stack,intval);

/*出棧*/
intstack_with_range_check_pop(structstack_with_range_check*p_stack,int*p_val);

#endif

帶范圍檢查的棧 C 文件 stack_with_range_check.c

#include"stack_with_range_check.h"

intstack_with_range_check_init(structstack_with_range_check*p_stack,
int*p_buf,
intsize,
intmin,intmax)
{
/*初始化基類*/
stack_init(&p_stack->super,p_buf,size);

/*初始化子類成員*/
p_stack->min=min;
p_stack->max=max;
return0;
}

intstack_with_range_check_push(structstack_with_range_check*p_stack,intval)
{
if((val>=p_stack->min)&&(val<=?p_stack->max))//差異點
{
returnstack_push(&p_stack->super,val);
}
return-1;
}

intstack_with_range_check_pop(structstack_with_range_check*p_stack,int*p_val)
{
returnstack_pop(&p_stack->super,p_val);
}

為了接口的簡潔性,沒有再展示解初始化等函數的定義。新增入棧時作檢查,出棧和普通棧是完全相同的,但基于最小知識原則也封裝了一個 pop 接口,使該類的用戶完全不需要關心普通棧。

依照這個方法,可以實現其它檢查方式的棧。核心是實現帶檢查功能的入棧函數,因而僅簡單展示另外兩種檢查方式下入棧函數的實現,分別如下:

//奇偶檢查入棧函數
intstack_with_oddeven_check_push(structstack_with_oddeven_check*p_stack,intval)
{
if(((p_stack->iseven)&&((val%2)==0))||((!p_stack->iseven)&&((val%2)!=0)))
{
returnstack_push(&p_stack->super,val);//檢查通過:偶校驗且為偶數,或奇校驗且為奇數
}
return-1;
}

//變化檢查入棧函數
intstack_with_change_check_push(structstack_with_change_check*p_stack,intval)
{
if(p_stack->pre_valuepre_value=val;
returnstack_push(&p_stack->super,val);//檢查通過:本次入棧值大于上一次的值
}
return-1;
}

由此可見,這種實現方式存在一定的缺陷,不同檢查方法對應的入棧函數不相同,對于用戶來講,使用不同的檢查功能,就必須調用不同的入棧函數。即操作不同的棧使用不同的接口。但觀察幾個入棧函數,其入棧方法類似,示意代碼如下:

intstack_XXX_push(structstack_XXX*p_stack,intval)
{
if(檢查通過)//不同棧的差異僅是檢測條件不同
{
returnstack_push(&p_stack->super,val);
}
return-1;
}

可使用多態思想,將“檢查”函數的調用形式標準化編寫一個通用的、與具體檢查方式無關的入棧函數。

4.3.2 基于多態實現通用的“帶檢查功能的棧”

使用函數指針表示“檢查功能”,指向不同的檢查函數。可以定義一個包含函數指針的類:

structstack_with_validate
{
structstacksuper;/*基類(超類)*/
int(*validate)(structstack_with_validate*p_this,intval);/*檢查函數*/
};

和其它普通方法一樣,類中抽象方法(函數指針)的第一個成員同樣是指向該類對象的指針。此時,數據入棧前的檢查工作交給 validate 指針所指向的函數實現。假定其指向的函數在檢查數據時,返回 0 表示檢查通過可入棧,其它值表示檢查未通過。完整的帶檢查功能的棧實現范例如下:

帶檢查功能的棧 H 文件(stack_with_validate.h)


#ifndef__STACK_WITH_VALIDATE_H
#define__STACK_WITH_VALIDATE_H

#include"stack.h"/*包含基類頭文件*/
structstack_with_validate
{
structstacksuper;/*基類(超類)*/
int(*validate)(structstack_with_validate*p_this,intval);/*檢查函數*/
};


intstack_with_validate_init(structstack_with_validate*p_stack,
int*p_buf,
intsize,
int(*validate)(structstack_with_validate*,int));

/*入棧*/
intstack_with_validate_push(structstack_with_validate*p_stack,intval);

/*出棧*/
intstack_with_validate_pop(structstack_with_validate*p_stack,int*p_val);

#endif

帶檢查功能的棧 C 文件(stack_with_validate.c)

#include"stack_with_validate.h"
#include"stdio.h"

intstack_with_validate_init(structstack_with_validate*p_stack,
int*p_buf,
intsize,
int(*validate)(structstack_with_validate*,int))

{
/*初始化基類*/
stack_init(&p_stack->super,p_buf,size);
p_stack->validate=validate;//檢查條件,上層說了算
return0;
}

intstack_with_validate_push(structstack_with_validate*p_stack,intval)
{
if((p_stack->validate==NULL)||
((p_stack->validate!=NULL)&&(p_stack->validate(p_stack,val)==0)))
{
returnstack_push(&p_stack->super,val);
}
return-1;
}

intstack_with_validate_pop(structstack_with_validate*p_stack,int*p_val)
{
returnstack_pop(&p_stack->super,p_val);
}

帶某種檢查功能的棧,重點是實現其中的 validate 方法。基于帶檢查的棧,實現帶范圍檢查的棧,程序詳見如下:

帶范圍檢查的棧 H 文件更新(stack_with_range_check.h)

#ifndef__STACK_WITH_RANGE_CHECK_H
#define__STACK_WITH_RANGE_CHECK_H

#include"stack_with_validate.h"/*包含基類頭文件*/

structstack_with_range_check
{
structstack_with_validatesuper;/*基類(超類)*/
intmin;/*最小值*/
intmax;/*最大值*/
};

structstack_with_validate*stack_with_range_check_init(structstack_with_range_check*p_stack,
int*p_buf,
intsize,
intmin,
intmax);

#endif

帶范圍檢查的棧 C 文件更新(stack_with_range_check.c)

#include"stack_with_range_check.h"

staticint_validate(structstack_with_validate*p_this,intval)
{
structstack_with_range_check*p_stack=(structstack_with_range_check*)p_this;

if((val>=p_stack->min)&&(val<=?p_stack->max))
{
return0;/*檢查通過*/
}

return-1;
}

structstack_with_validate*stack_with_range_check_init(structstack_with_range_check*p_stack,
int*p_buf,
intsize,
intmin,
intmax)
{
/*初始化基類*/
stack_with_validate_init(&p_stack->super,p_buf,size,_validate);

/*初始化子類成員*/
p_stack->min=min;
p_stack->max=max;
return0;
}

帶范圍檢查的棧,主要目的就是實現“檢查功能”對應的函數:_validate,并將其作為 validate 函數指針(抽象方法)的值。

在面向對象編程中,包含抽象方法的類通常稱之為抽象類,抽象類不能直接實例化(因為其還有方法未實現),抽象類只能被繼承,且由子類實現其中定義的抽象方法。在 UML 類圖中,抽象類的類名和其中的抽象方法均使用斜體表示,普通棧、帶檢查功能的棧和帶范圍檢查的棧,它們之間的關系詳見圖。

400669c0-906d-11ef-a511-92fbcf53809c.png

帶范圍檢查的棧,其主要作用是實現其父類中定義的抽象方法,進而創建一個真正的“帶檢查功能”的棧對象(此時的抽象方法已實現),該對象即可提交給外部使用。帶范圍檢查的棧并沒有其他特殊的方法,因而在其初始化完成后,通過初始化函數的返回值向外界提供了一個“帶檢查功能”的棧對象,后續用戶即可使用 stack_with_validate.h 文件中的push 和 pop 方法操作該對象。

帶范圍檢查的棧使用范例如下:

//微信公眾號:嵌入式系統
#include"stack_with_range_check.h"
#include"stdio.h"

intmain()
{
intval;
intbuf[20];
inti;
inttest_data[5]={2,4,5,3,10};

structstack_with_range_checkstack;

structstack_with_validate*p_stack=stack_with_range_check_init(&stack,buf,20,1,9);

for(i=0;i

無論何種檢查方式,其主要目的都是創建“帶檢查功能”的棧對象(完成抽象方法的實現)。創建完畢后,對于用戶操作方法都是完全相同的 stack_with_validate_push 和 stack_with_validate_pop ,與檢查方式無關。為避免贅述,這里不再實現另外兩種檢查功能的棧,僅展示出他們的類圖。

40265a32-906d-11ef-a511-92fbcf53809c.png

在這里插入圖片描述

在一些大型項目中,初始化過程往往和應用程序是分離的(即stack_with_range_check_init 內部封閉不可見),也就是說,對于用戶來講,其僅會獲取到一個 struct stack_with_validate *類型的指針,其指向某個“帶檢查功能的棧”,實際檢查什么,用戶可能并不關心,應用程序基于該類型指針編程,將使應用程序與具體檢查功能無關,即使后續更換為其它檢查方式,應用程序也不需要做任何改動。

4.4 抽象分離

如果是硬件資源有限,功能單一或大概率無需擴展的嵌入式軟件開發,進行到這基本可以滿足需求;如果是復雜應用,且硬件資源充足還可繼續優化。

4.4.1 檢查功能抽象

前面的實現中,將檢查功能視為棧的一種擴展(使用繼承),檢查邏輯直接在相應的擴展類中實現。這就使檢查功能與棧綁定在一起,檢查功能的實現無法獨立復用。如果要實現一個“帶檢查功能的隊列”,同樣是上述的 3 種檢查邏輯,期望能夠復用檢查邏輯相關的代碼。顯然,由于當前檢查邏輯的實現與棧捆綁在一起,無法單獨提取出來復用。

檢查功能與棧的綁定,主要在“帶檢查功能的棧”中體現,該類的定義如下:

structstack_with_validate
{
structstacksuper;/*基類(超類)*/
int(*validate)(structstack_with_validate*p_this,intval);/*檢查函數*/
};

super 用于繼承自普通棧,validate 表示一個抽象的數據檢查方法,不同的檢查方法,通過該指針所指向的函數體現。由于檢查方法validate是該類的一個方法,檢查邏輯與棧綁定。為了解綁分離,可以將檢查邏輯放到獨立的與棧無關的類中,額外定義一個抽象的校驗器類,專門表示數據檢查邏輯:

structvalidator
{
int(*validate)(structvalidator*p_this,intval);/*檢查函數*/
};

雖然該類僅包含 validate 函數指針,但需注意該函數指針類型的變化,其第一個參數為指向校驗器的指針,而在“帶檢查功能的棧”中,其第一個參數是指向“帶檢查功能的棧”的指針。通過該類的定義,明確的將檢查邏輯封裝到獨立的校驗器類中,與棧再無任何關聯。不同的檢查邏輯,可以在其子類中實現,校驗器類和各個子類之間的關系如下:

405549d2-906d-11ef-a511-92fbcf53809c.png

由于校驗器類僅包含一個函數指針,因此其只需要在頭文件中定義出類即可,程序如下:

校驗器類定義(validator.h)

#ifndef__VALIDATOR_H
#define__VALIDATOR_H

structvalidator
{
int(*validate)(structvalidator*p_this,intval);
};

staticinlineintvalidator_init(structvalidator*p_validator,
int(*validate)(structvalidator*,int))
{
p_validator->validate=validate;
return0;
}

staticinlineintvalidator_validate(structvalidator*p_validator,intval)/*校驗函數*/
{
if(p_validator->validate==NULL)/*校驗函數為空,視為無需校驗*/
{
return0;
}
returnp_validator->validate(p_validator,val);
}

#endif

初始化函數負責為 validate 賦值,validator_validate 函數是校驗器對外提供的校驗函數,在其實現中僅調用了 validate 函數指針指向的函數。由于函數都比較簡單,因而直接使用了內聯函數的形式進行了定義。接下來以范圍校驗為例,實現一個范圍校驗器。

范圍校驗器 H 文件內容(validator_range_check.h)

#ifndef__VALIDATOR_RANGE_CHECK_H
#define__VALIDATOR_RANGE_CHECK_H

#include"validator.h"
structvalidator_range_check
{
structvalidatorsuper;
intmin;
intmax;
};

structvalidator*validator_range_check_init(structvalidator_range_check*p_validator,intmin,intmax);

#endif

范圍校驗器 C 文件內容(validator_range_check.c)

//微信公眾號:嵌入式系統
#include"validator_range_check.h"

staticint_validate(structvalidator*p_this,intval)
{
structvalidator_range_check*p_stack=(structvalidator_range_check*)p_this;
if((val>=p_stack->min)&&(val<=?p_stack->max))
{
return0;/*檢查通過*/
}
return-1;
}

structvalidator*validator_range_check_init(structvalidator_range_check*p_validator,intmin,intmax)
{
validator_init(&p_validator->super,_validate);
p_validator->min=min;
p_validator->max=max;
return&p_validator->super;
}

由于 validator_range_check 類僅用于實現 validator 抽象類中定義的抽象方法,其初始化函數可以直接對外返回一個標準的校驗器(其中的抽象方法已實現)。按照同樣的方法,可以實現validator_oddeven_check 類和 validator_change_check 類。將檢查功能從“帶檢查功能的棧”中分離出來之后,“帶檢查功能的棧”中就無需再維護檢查功能對應的抽象方法。其可以通過依賴的方式使用檢查功能,即依賴一個校驗器。在類圖中,依賴關系可以使用一個虛線箭頭表示,箭頭指向被依賴的類,示意圖如下:

406bfaf6-906d-11ef-a511-92fbcf53809c.png

“帶檢查功能的棧”類定義如下:

structstack_with_validate
{
structstacksuper;/*基類(超類)*/
structvalidator*p_validator;/*依賴的校驗器*/
};

與先前相比,其核心變化是由一個 validate 函數指針(指向具體的檢查方法)變更為 p_validator 指針(指向抽象的檢查方法),變化雖小,但是兩種截然不同的設計理念。之前的方式是定義了一個抽象方法,而現在的方式是依賴于一個校驗器對象。

基于此更新“帶檢查功能的棧”類的實現如下:

帶檢查功能的棧 H 文件更新(stack_with_validate.h)

#ifndef__STACK_WITH_VALIDATE_H
#define__STACK_WITH_VALIDATE_H

#include"stack.h"/*包含基類頭文件*/
#include"validator.h"

structstack_with_validate
{
structstacksuper;/*基類(超類)*/
structvalidator*p_validator;
};

intstack_with_validate_init(structstack_with_validate*p_stack,
int*p_buf,
intsize,
structvalidator*p_validator);

intstack_with_validate_push(structstack_with_validate*p_stack,intval);
intstack_with_validate_pop(structstack_with_validate*p_stack,int*p_val);

#endif

帶檢查功能的棧 C 文件更新(stack_with_validate.c)

//微信公眾號:嵌入式系統
#include"stack_with_validate.h"
#include"stdio.h"

intstack_with_validate_init(structstack_with_validate*p_stack,
int*p_buf,
intsize,
structvalidator*p_validator)
{
stack_init(&p_stack->super,p_buf,size);
p_stack->p_validator=p_validator;
return0;
}

intstack_with_validate_push(structstack_with_validate*p_stack,intval)
{
if((p_stack->p_validator==NULL)||(validator_validate(p_stack->p_validator,val)==0))//注意差別
{
returnstack_push(&p_stack->super,val);
}
return-1;
}

intstack_with_validate_pop(structstack_with_validate*p_stack,int*p_val)
{
returnstack_pop(&p_stack->super,p_val);
}

“帶檢查功能的棧”的應用接口(push 和 pop)并沒有發生任何改變,應用程序可以被復用,測試更新后的帶檢查功能的棧:

#include"stack_with_validate.h"
#include"validator_range_check.h"
#include"stdio.h"
intmain()
{
intbuf[20];
structstack_with_validatestack;
structvalidator_range_checkvalidator_range_check;

/*獲取范圍檢查校驗器*/
structvalidator*p_validator=validator_range_check_init(&validator_range_check,1,9);
stack_with_validate_init(&stack,buf,20,p_validator);

stack_validate_application(&stack);//使用和先前繼承方式一樣,實現忽略

return0;
}

4.4.2 定義抽象棧

定義校驗器類后,整個系統實現了兩種棧:普通棧和“帶檢查功能的棧”,無論什么棧,對于用戶來講都是實現入棧和出棧兩個核心邏輯。兩種棧提供兩種入棧和出棧方法。

普通棧提供的方法為:

intstack_push(structstack*p_stack,intval);/*入棧*/
intstack_pop(structstack*p_stack,int*p_val);/*出棧*/

“帶檢查功能的棧”提供的方法為:

intstack_with_validate_push(structstack_with_validate*p_stack,intval);/*入棧*/
intstack_with_validate_pop(structstack_with_validate*p_stack,int*p_val);/*出棧*/

用戶執行入棧和出棧操作,使用不同類的棧,調用的函數不同。通過多態思想,將入棧和出棧定義為抽象方法(函數指針),則可以達到這樣的效果:無論使用何種棧,都可以使用相同的方法來實現入棧和出棧。基于此定義抽象棧。

抽象棧類定義(stack.h)

#ifndef__STACK_H
#define__STACK_H

structstack
{
int(*push)(structstack*p_stack,intval);
int(*pop)(structstack*p_stack,int*p_val);
};

staticinlineintstack_init(structstack*p_stack,
int(*push)(structstack*,int),
int(*pop)(structstack*,int*))
{
p_stack->push=push;
p_stack->pop=pop;
}

staticinlineintstack_push(structstack*p_stack,intval)
{
returnp_stack->push(p_stack,val);
}

staticinlineintstack_pop(structstack*p_stack,int*p_val)
{
returnp_stack->pop(p_stack,p_val);
}

#endif

基于抽象棧的定義,使用抽象棧提供的接口實現一個通用的應用程序,該應用程序與底層細節無關,任何棧都可以使用該應用程序進行測試。

基于抽象棧實現的應用程序:

#include"stack.h"
#include"stdio.h"
intstack_application(structstack*p_stack)
{
inti;
intval;
inttest_data[5]={2,4,5,3,10};

for(i=0;i

先有應用層代碼再有底層代碼。在實現具體棧之前,就可以開始編寫應用程序(微信公眾號【嵌入式系統】這就是依賴倒置原則,可參考《嵌入式軟件設計原則隨想》)。實現普通棧:

普通棧 H 文件內容(stack_normal.h)

#ifndef__STACK_NORMAL_H
#define__STACK_NORMAL_H

#include"stack.h"
structstack_normal
{
structstacksuper;
inttop;/*棧頂*/
int*p_buf;/*棧緩存*/
unsignedintsize;/*棧緩存的大小*/
};

structstack*stack_normal_init(structstack_normal*p_stack,int*p_buf,intsize);

#endif

普通棧 C 文件內容(stack_normal.c)

//微信公眾號:嵌入式系統
#include"stack_normal.h"

staticint_push(structstack*p_this,intval)
{
structstack_normal*p_stack=(structstack_normal*)p_this;
if(p_stack->top!=p_stack->size)
{
p_stack->p_buf[p_stack->top++]=val;
return0;
}
return-1;
}

staticint_pop(structstack*p_this,int*p_val)
{
structstack_normal*p_stack=(structstack_normal*)p_this;
if(p_stack->top!=0)
{
*p_val=p_stack->p_buf[--p_stack->top];
return0;
}
return-1;
}

structstack*stack_normal_init(structstack_normal*p_stack,int*p_buf,intsize)
{
p_stack->top=0;
p_stack->size=size;

p_stack->p_buf=p_buf;
stack_init(&p_stack->super,_push,_pop);
return&p_stack->super;
}

基于普通類的實現,測試普通棧類:

#include"stack_normal.h"
intmain()
{
intbuf[20];

structstack_normalstack;
structstack*p_stack=stack_normal_init(&stack,buf,20);
stack_application(p_stack);
return0;
}

“帶檢查功能的棧”是在普通棧的基礎上,增加了檢查功能,實現范例程序如下:

帶檢查功能的棧 H 文件更新(stack_with_validate.h)

#ifndef__STACK_WITH_VALIDATE_H
#define__STACK_WITH_VALIDATE_H

#include"stack.h"/*包含基類頭文件*/
#include"validator.h"


structstack_with_validate
{

structstacksuper;/*基類(超類)*/
structstack*p_normal_stack;/*依賴于普通棧的實現*/
structvalidator*p_validator;
};

structstack*stack_with_validate_init(structstack_with_validate*p_stack,
structstack*p_normal_stack,
structvalidator*p_validator);

#endif

檢查功能的棧 C 文件更新(stack_with_validate.c)

#include"stack_with_validate.h"
#include"stdio.h"
staticint_push(structstack*p_this,intval)
{
structstack_with_validate*p_stack=(structstack_with_validate*)p_this;
if((p_stack->p_validator==NULL)||(validator_validate(p_stack->p_validator,val)==0))
{
returnstack_push(p_stack->p_normal_stack,val);
}
return-1;
}

staticint_pop(structstack*p_this,int*p_val)
{
structstack_with_validate*p_stack=(structstack_with_validate*)p_this;
returnstack_pop(p_stack->p_normal_stack,p_val);
}

structstack*stack_with_validate_init(structstack_with_validate*p_stack,
structstack*p_normal_stack,
structvalidator*p_validator)
{
stack_init(&p_stack->super,_push,_pop);

p_stack->p_validator=p_validator;
p_stack->p_normal_stack=p_normal_stack;
return&p_stack->super;
}

基于“帶檢查功能的棧”的實現,測試范例如下:

#include"stack_normal.h"
#include"stack_with_validate.h"
#include"validator_range_check.h"

intmain()
{
intbuf[20];
structstack_normalstack;
structstack_with_validatestack_with_validate;
structvalidator_range_checkvalidator_range_check;

structstack*p_stack_normal=stack_normal_init(&stack,buf,20);

structvalidator*p_validator=validator_range_check_init(&validator_range_check,1,9);
structstack*p_stack=stack_with_validate_init(&stack_with_validate,
p_stack_normal,
p_validator);

stack_application(p_stack);
return0;
}

由此可見,無論底層的各種棧如何實現,對于上層應用來講,其可以使用同一套接口stack_application操作各種各樣不同的棧。

多種多態示例的核心解決方案都是相同的,即:定義抽象方法(函數指針),使上層應用可以使用同一套接口訪問不同的對象。從類的角度看,每個類中操作的規約都是相同的,而這些類可以用不同的方式實現這些同名的操作,從而使得擁有相同接口的對象可以在運行時相互替換。

同樣的應用程序,可以在多個硬件平臺上運行,更換硬件時應用程序無需作任何改動。在嵌入式系統中,相同功能芯片的更新替換,也是多態應用最多的場景,根據硬件差異多態封裝,應用層無感使用相同接口。基于多態的思想實現“與硬件無關”的應用程序,還可以衍生出兩個概念:抽象接口與依賴倒置,它們的核心都是多態。更多編碼原則可參考《嵌入式軟件設計原則隨想》、《Unix哲學之編程原則》,分層架構《嵌入式軟件分層隔離的典范》。

5 小節

學會了屠龍技,但是沒有龍,怎么辦?有些東西只是一種思維模式,作為日常開發工作中潛移默化的一種偏愛。所以嵌入式軟件開發究竟有沒對象呢?有但少。

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 編程
    +關注

    關注

    88

    文章

    3615

    瀏覽量

    93731
  • 嵌入式軟件
    +關注

    關注

    4

    文章

    240

    瀏覽量

    26646

原文標題:一文了解嵌入式軟件開發的“對象”

文章出處:【微信號:strongerHuang,微信公眾號:strongerHuang】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    嵌入式軟件開發與非嵌入式軟件開發的區別?

    嵌入式軟件開發與非嵌入式軟件開發區別?設備驅動開發與裸機驅動開發區別?
    發表于 04-02 06:29

    嵌入式軟件開發工具

    翻譯自這篇博不同物理設備之間的數字轉換可以說是隨處可見,讓其智能又強大。驅動設備的引擎是嵌入式軟件,它是快速發展的IoT生態系統中不可或缺的部分。本篇博客主要談
    發表于 10-28 08:21

    如何學習嵌入式軟件開發

    1. 如何學習嵌入式軟件開發嵌入式軟件開發定要結合開發板來學習,所有的知識都可以在
    發表于 02-11 07:33

    嵌入式軟件開發是做什么的?

    ,C語言作為門基礎語言,無論對嵌入式軟件還是硬件開發編程,都會有很大的幫助。 2.了解操作系統,對操作系統
    發表于 12-15 16:39

    什么是嵌入式軟件開發

    嵌入式軟件開發又是指什么?   隨著嵌入式軟件系統結構越來越復雜,嵌入式軟件
    發表于 04-20 08:43 ?8782次閱讀

    嵌入式軟件開發與Embedded-GIS

    1.嵌入式軟件開發 市場現狀 嵌入式操作系統 嵌入式軟件開發特點 嵌入式
    發表于 02-28 10:39 ?36次下載

    Eclipse嵌入式軟件開發平臺

    隨著嵌入式系統在通信、網絡設施、航空、航海和航天等領域的大范圍應用,需要功能強大的軟件開發平臺輔助嵌入式應用軟件開發
    發表于 12-01 15:37 ?55次下載
    Eclipse<b class='flag-5'>嵌入式</b><b class='flag-5'>軟件開發</b>平臺

    ARM嵌入式軟件開發

    ARM嵌入式軟件開發ARM嵌入式軟件開發ARM嵌入式軟件開發
    發表于 01-15 17:29 ?65次下載

    嵌入式軟件開發的優勢分析

    為什么當今嵌入式行業會如此受歡迎呢?我們從事嵌入式軟件開發有什么好處嗎?小編就來說說我的看法吧,嵌入式軟件開發的優勢我們可以從幾點來看。
    的頭像 發表于 12-21 15:23 ?5233次閱讀

    嵌入式軟件開發環境

    嵌入式軟件開發環境1 簡介嵌入式軟件定義嵌入式系統構成2 環境搭建2.1 Qt2.2 CLion2.3 Source Insight + v
    發表于 10-21 12:21 ?7次下載
    <b class='flag-5'>嵌入式</b><b class='flag-5'>軟件開發</b>環境

    嵌入式軟件開發做什么?嵌入式開發培訓學哪些

    嵌入式行業可以說從幕后走到前臺,大家對他都很熟悉了,我國嵌入式人才缺口每年在50萬左右,相關調查報告稱嵌入式軟件開發是未來幾年最熱門和最受歡迎的職業之
    發表于 11-03 10:36 ?19次下載
    <b class='flag-5'>嵌入式</b><b class='flag-5'>軟件開發</b>做什么?<b class='flag-5'>嵌入式開發</b>培訓學哪些

    嵌入式軟件開發的特點、設計流程、嵌入式軟件的結構

    ? ? ? ?嵌入式軟件開發的特點、設計流程、嵌入式軟件的結構?嵌入式
    發表于 11-03 15:21 ?38次下載
    <b class='flag-5'>嵌入式</b><b class='flag-5'>軟件開發</b>的特點、設計流程、<b class='flag-5'>嵌入式</b><b class='flag-5'>軟件</b>的結構

    嵌入式軟件開發入門

    1. 如何學習嵌入式軟件開發嵌入式軟件開發定要結合開發板來學習,所有的知識都可以在
    發表于 12-07 17:21 ?17次下載
    <b class='flag-5'>嵌入式</b><b class='flag-5'>軟件開發</b>入門

    嵌入式軟件開發流程

    電子發燒友網站提供《嵌入式軟件開發流程.ppt》資料免費下載
    發表于 11-17 14:37 ?4次下載
    <b class='flag-5'>嵌入式</b><b class='flag-5'>軟件開發</b>流程

    嵌入式軟件開發軟件開發的區別

    嵌入式軟件開發軟件開發是兩個不同的概念,它們在些關鍵方面有著明顯的區別。嵌入式軟件開發是指
    的頭像 發表于 01-22 15:27 ?2285次閱讀
    主站蜘蛛池模板: 五月婷婷网址| 亚洲欧美啪啪| 人人艹人人干| 国产理论在线观看| www.亚洲免费| 五月六月激情| 国产激情片| 8050午夜| 国内精品99| ssswww日本免费网站片| 日本口工福利漫画无遮挡| 一级a性色生活片毛片| 黄色特级毛片| 五月天婷婷免费视频观看| 中文字幕一区二区精品区| 色我网站| 天天爽夜夜爽夜夜爽精品视频| 国产综合免费视频| 一级特黄a大片免费| 国产欧美高清| 黄色欧美| 欧美性野久久久久久久久| 国产亚洲美女| 国产一级特黄aa大片爽爽| 华人被黑人粗大猛然进| 亚洲视频在线一区| 四虎综合九九色九九综合色| www.色.com| 日本不卡视频在线播放| 色婷婷激婷婷深爱五月小说| 色最快国产| 亚洲精品久久久久午夜| 女人的逼毛片| 国产国产人免费人成成免视频| 欧美com| 欧美成人3d动漫在线播放网站| 成 人 免 费 黄 色| 中文字幕一区二区三区免费看| 五月六月婷婷| 色惰网站| 波多野结衣一级特黄毛片|