版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡介
1、輕量級的面向?qū)ο驝語言編程框架LW_OOPC介紹金永華、陳國棟2010/03/02摘要:本文介紹一種輕量級的面向?qū)ο蟮腃語言編程框架:LW_OOPC。LW_OOPC是Light-Weight Object-Oriented Programming in(with) C的縮寫,總共一個(gè).h文件,20個(gè)宏,約130行代碼,非常的輕量級,但卻很好的支持了很多面向?qū)ο蟮奶匦?,比如繼承、多態(tài),可以優(yōu)美的實(shí)現(xiàn)面向接口編程。這個(gè)框架系由臺灣的高煥堂先生以及他的MISOO團(tuán)隊(duì)首創(chuàng),之后由我繼續(xù)改進(jìn)優(yōu)化,最后,經(jīng)高煥堂同意以LGPL協(xié)議開源(開源網(wǎng)址參見后文)。用C語言實(shí)現(xiàn)OO?我沒聽錯(cuò)嗎?這聽起來真是太瘋狂了
2、! 大家都知道,C+支持了面向?qū)ο蠛兔嫦蚍盒途幊?,比C要更強(qiáng)大些。那么,為什么要在C語言中實(shí)踐面向?qū)ο竽兀繛槭裁床恢苯邮褂肅+呢?為什么要用面向?qū)ο??面向過程方式開發(fā)的系統(tǒng),代碼復(fù)雜,耦合性強(qiáng),難以維護(hù),隨著我們所要解決的問題越來越復(fù)雜,代碼也變得越來越復(fù)雜,越來越難以掌控,而面向?qū)ο蟾淖兞顺绦騿T的思維方式,以更加符合客觀世界的方式來認(rèn)識世界,通過合理的運(yùn)用抽象、封裝、繼承和多態(tài),更好的組織程序,從而很好地應(yīng)對這種復(fù)雜性。 為什么不直接使用C+?C和C+之爭由來已久,可能要持續(xù)到它們中的一種去世_。C語言以其簡潔明快,功能強(qiáng)大的特點(diǎn),深得開發(fā)人員的喜愛,尤其是在嵌入式開發(fā)領(lǐng)域,C語言更是占據(jù)了
3、絕對老大的地位。在我看來,語言只是工具,作為程序員,我們要做的是:選擇合適的語言,解決恰當(dāng)?shù)膯栴}。我們要尊重事實(shí),考慮開發(fā)環(huán)境(軟硬件環(huán)境),考慮團(tuán)隊(duì)成員的水平,從商用工程的角度講,選擇團(tuán)隊(duì)成員擅長的語言進(jìn)行開發(fā),風(fēng)險(xiǎn)要小很多。一些從Java/C#轉(zhuǎn)到C的程序員們,無法從面向?qū)ο笄袚Q到面向過程,但又必須與C語言同事們在遺留的C系統(tǒng)上開發(fā)軟件,他們有時(shí)會(huì)非常困惑:C語言是面向過程的編程語言,如何實(shí)踐面向?qū)ο?,甚至面向接口編程呢?此時(shí),就非常需要在C語言中實(shí)現(xiàn)面向?qū)ο蟮氖侄?,而LW_OOPC正是應(yīng)對這一難題的解決之道。LW_OOPC是什么?簡而言之:LW_OOPC是一套C語言的宏,總共1個(gè).h文件
4、(如果需要內(nèi)存泄漏檢測支持以及調(diào)試打印支持,那么還需要1個(gè).c文件(lw_oopc.c,約145行),20個(gè)宏,約130行代碼。LW_OOPC是一種C語言編程框架,用于支持在C語言中進(jìn)行面向?qū)ο缶幊?。LW_OOPC宏介紹下面,先通過一個(gè)簡單的示例來展示LW_OOPC這套宏的使用方法。我們要?jiǎng)?chuàng)建這樣一些對象:動(dòng)物(Animal),魚(Fish),狗(Dog),車子(Car)。顯然,魚和狗都屬于動(dòng)物,都會(huì)動(dòng),車子也會(huì)動(dòng),但是車子不是動(dòng)物。會(huì)動(dòng)是這些對象的共同特征,但是,顯然它們不屬于一個(gè)家族。因此,我們首先考慮抽象出一個(gè)接口(IMoveable),以描述會(huì)動(dòng)這一行為特征:INTERFACE(IMo
5、veable) void (*move)(IMoveable* t); / Move行為;INTERFACE宏用于定義接口,其成員(方法)均是函數(shù)指針類型。然后,我們分析Animal,它應(yīng)該是抽象類還是接口呢?動(dòng)物都會(huì)吃,都需要呼吸,如果僅僅考慮這兩個(gè)特征,顯然可以把Animal定為接口。不過,這里,為了展示抽象類在LW_OOPC中如何應(yīng)用。我們讓Animal擁有昵稱和年齡屬性,并且,讓動(dòng)物和我們打招呼(sayHello方法),但,我們不允許用戶直接創(chuàng)建Animal對象,所以,這里把Animal定為抽象類:ABS_CLASS(Animal) char name128; / 動(dòng)物的昵稱(假設(shè)小于
6、128個(gè)字符) int age; / 動(dòng)物的年齡 void (*setName)(Animal* t, const char* name); / 設(shè)置動(dòng)物的昵稱 void (*setAge)(Animal* t, int age); / 設(shè)置動(dòng)物的年齡 void (*sayHello)(Animal* t); / 動(dòng)物打招呼 void (*eat)(Animal* t); / 動(dòng)物都會(huì)吃(抽象方法,由子類實(shí)現(xiàn)) void (*breathe)(Animal* t); / 動(dòng)物都會(huì)呼吸(抽象方法,由子類實(shí)現(xiàn)) void (*init)(Animal* t, const char* name, i
7、nt age);/ 初始化昵稱和年齡;ABS_CLASS宏用于定義抽象類,允許有成員屬性。代碼的含義參見代碼注釋。緊接著,我們來定義Fish和Dog類,它們都繼承動(dòng)物,然后還實(shí)現(xiàn)了IMoveable接口:CLASS(Fish) EXTENDS(Animal); / 繼承Animal抽象類 IMPLEMENTS(IMoveable);/ 實(shí)現(xiàn)IMoveable接口 void (*init)(Fish* t, const char* name, int age);/ 初始化昵稱和年齡;CLASS(Dog) EXTENDS(Animal);/ 繼承Animal抽象類 IMPLEMENTS(IMove
8、able);/ 實(shí)現(xiàn)IMoveable接口 void(*init)(Dog* t, const char* name, int age);/ 初始化昵稱和年齡;為了讓Fish對象或Dog對象在創(chuàng)建之后,能夠很方便地初始化昵稱和年齡,F(xiàn)ish和Dog類均提供了init方法。下面,我們來定義Car,車子不是動(dòng)物,但可以Move,因此,讓Car實(shí)現(xiàn)IMoveable 接口即可:CLASS(Car) IMPLEMENTS(IMoveable); / 實(shí)現(xiàn)IMoveable接口(車子不是動(dòng)物,但可以Move);接口,抽象類,具體類的定義都已經(jīng)完成了。下面,我們開始實(shí)現(xiàn)它們。接口是不需要實(shí)現(xiàn)的,所以IMo
9、veable沒有對應(yīng)的實(shí)現(xiàn)代碼。Animal是抽象動(dòng)物接口,是半成品,所以需要提供半成品的實(shí)現(xiàn):/* 設(shè)置動(dòng)物的昵稱*/void Animal_setName(Animal* t, const char* name) / 這里假定name不會(huì)超過128個(gè)字符,為簡化示例代碼,不做保護(hù)(產(chǎn)品代碼中不要這樣寫) strcpy(t-name, name);/* 設(shè)置動(dòng)物的年齡*/void Animal_setAge(Animal* t, int age) t-age = age;/* 動(dòng)物和我們打招呼*/void Animal_sayHello(Animal* t) printf(Hello! 我是
10、%s,今年%d歲了!n, t-name, t-age);/* 初始化動(dòng)物的昵稱和年齡*/void Animal_init(Animal* t, const char* name, int age) t-setName(t, name); t-setAge(t, age);ABS_CTOR(Animal)FUNCTION_SETTING(setName, Animal_setName);FUNCTION_SETTING(setAge, Animal_setAge);FUNCTION_SETTING(sayHello, Animal_sayHello);FUNCTION_SETTING(init,
11、 Animal_init);END_ABS_CTOR這里出現(xiàn)了幾個(gè)新的宏,我們逐個(gè)進(jìn)行講解。ABS_CTOR表示抽象類的定義開始,ABS_CTOR(Animal)的含義是Animal抽象類的“構(gòu)造函數(shù)”開始。在C語言里邊其實(shí)是沒有C+中的構(gòu)造函數(shù)的概念的。LW_OOPC中的CTOR系列宏(CTOR/END_CTOR,ABS_CTOR/END_ABS_CTOR)除了給對象(在C語言中是struct實(shí)例)分配內(nèi)存,然后,緊接著要為結(jié)構(gòu)體中的函數(shù)指針成員賦值,這一過程,也可以稱為函數(shù)綁定(有點(diǎn)類似C+中的動(dòng)態(tài)聯(lián)編)。函數(shù)綁定的過程由FUNCTION_SETTING宏來完成。對于Fish和Dog類的實(shí)
12、現(xiàn),與Animal基本上是類似的,除了將ABS_CTOR換成了CTOR,直接參見代碼:/* 魚的吃行為 */void Fish_eat(Animal* t) printf(魚吃水草!n);/* 魚的呼吸行為 */void Fish_breathe(Animal* t) printf(魚用鰓呼吸!n);/* 魚的移動(dòng)行為 */void Fish_move(IMoveable* t) printf(魚在水里游!n);/* 初始化魚的昵稱和年齡 */void Fish_init(Fish* t, const char* name, int age) Animal* animal = SUPER_PT
13、R(t, Animal); animal-setName(animal, name); animal-setAge(animal, age);CTOR(Fish)SUPER_CTOR(Animal);FUNCTION_SETTING(Animal.eat, Fish_eat);FUNCTION_SETTING(Animal.breathe, Fish_breathe);FUNCTION_SETTING(IMoveable.move, Fish_move);FUNCTION_SETTING(init, Fish_init);END_CTOR上面是Fish的實(shí)現(xiàn),下面看Dog的實(shí)現(xiàn):/* 狗的吃行
14、為 */void Dog_eat(Animal* t) printf(狗吃骨頭!n);/* 狗的呼吸行為 */void Dog_breathe(Animal* t) printf(狗用肺呼吸!n);/* 狗的移動(dòng)行為 */void Dog_move(IMoveable* t) printf(狗在地上跑!n);/* 初始化狗的昵稱和年齡 */void Dog_init(Dog* t, const char* name, int age) Animal* animal = SUPER_PTR(t, Animal); animal-setName(animal, name); animal-setA
15、ge(animal, age);CTOR(Dog)SUPER_CTOR(Animal);FUNCTION_SETTING(Animal.eat, Dog_eat);FUNCTION_SETTING(Animal.breathe, Dog_breathe);FUNCTION_SETTING(IMoveable.move, Dog_move);FUNCTION_SETTING(init, Dog_init);END_CTOR細(xì)心的朋友可能已經(jīng)注意到了,這里又有一個(gè)陌生的宏:SUPER_CTOR未介紹。這個(gè)宏是提供給子類用的,用于調(diào)用其直接父類的構(gòu)造函數(shù)(類似Java語言中的super()調(diào)用,在這
16、里,其實(shí)質(zhì)是要先調(diào)用父類的函數(shù)綁定過程,再調(diào)用自身的函數(shù)綁定過程),類似Java那樣,SUPER_CTOR如果要出現(xiàn),需要是ABS_CTOR或者CTOR下面緊跟的第一條語句。最后,我們把Car類也實(shí)現(xiàn)了:void Car_move(IMoveable* t) printf(汽車在開動(dòng)!n);CTOR(Car)FUNCTION_SETTING(IMoveable.move, Car_move);END_CTOR下面,我們實(shí)現(xiàn)main方法,以展示LW_OOPC的威力:#include animal.hint main() Fish* fish = Fish_new(); / 創(chuàng)建魚對象 Dog*
17、dog = Dog_new(); / 創(chuàng)建狗對象 Car* car = Car_new(); / 創(chuàng)建車子對象 Animal* animals2 = 0 ; / 初始化動(dòng)物容器(這里是Animal指針數(shù)組) IMoveable* moveObjs3 = 0 ; / 初始化可移動(dòng)物體容器(這里是IMoveable指針數(shù)組) int i = 0; / i和j是循環(huán)變量 int j = 0; / 初始化魚對象的昵稱為:小鯉魚,年齡為:1歲 fish-init(fish, 小鯉魚, 1); / 將fish指針轉(zhuǎn)型為Animal類型指針,并賦值給animals數(shù)組的第一個(gè)成員 animals0 = SU
18、PER_PTR(fish, Animal); / 初始化狗對象的昵稱為:牧羊犬,年齡為:2歲 dog-init(dog, 牧羊犬, 2); / 將dog指針轉(zhuǎn)型為Animal類型指針,并賦值給animals數(shù)組的第二個(gè)成員 animals1 = SUPER_PTR(dog, Animal); / 將fish指針轉(zhuǎn)型為IMoveable接口類型指針,并賦值給moveOjbs數(shù)組的第一個(gè)成員 moveObjs0 = SUPER_PTR(fish, IMoveable); / 將dog指針轉(zhuǎn)型為IMoveable接口類型指針,并賦值給moveOjbs數(shù)組的第二個(gè)成員 moveObjs1 = SUPE
19、R_PTR(dog, IMoveable); / 將car指針轉(zhuǎn)型為IMoveable接口類型指針,并賦值給moveOjbs數(shù)組的第三個(gè)成員 moveObjs2 = SUPER_PTR(car, IMoveable); / 循環(huán)打印動(dòng)物容器內(nèi)的動(dòng)物信息 for(i=0; ieat(animal); animal-breathe(animal); animal-sayHello(animal); / 循環(huán)打印可移動(dòng)物體容器內(nèi)的可移動(dòng)物體移動(dòng)方式的信息 for(j=0; jmove(moveObj); lw_oopc_delete(fish); lw_oopc_delete(dog); lw_oo
20、pc_delete(car); return 0;從上邊的代碼中,我們驚喜地發(fā)現(xiàn),在C語言中,借助LW_OOPC,我們實(shí)現(xiàn)了將不同的動(dòng)物(Fish和Dog對象)裝入Animal容器,然后可以用完全相同的方式調(diào)用Animal的方法(比如eat和breathe方法),而實(shí)際調(diào)用的是具體的實(shí)現(xiàn)類(Fish和Dog)的對應(yīng)方法。這正是面向?qū)ο笾械亩鄳B(tài)的概念。同樣,我們可以將Fish對象,Dog對象,以及Car對象均視為可移動(dòng)物體,均裝入IMoveable容器,然后用完全相同的方式調(diào)用IMoveable接口的move方法??吹搅藛??借助LW_OOPC,在C語言下我們竟然可以輕松地實(shí)現(xiàn)面向?qū)ο蠛兔嫦蚪涌诰?/p>
21、程! 下面,再舉一個(gè)稍微復(fù)雜的例子,它的覆蓋面是足夠全面的,足以一瞥面向?qū)ο缶幊痰?個(gè)要素:數(shù)據(jù)抽象、繼承和多態(tài)。通過這個(gè)例子,我們期望展現(xiàn)出LW_OOPC在遭遇問題本身比較復(fù)雜的情形下,是如何從容應(yīng)對的,以加深讀者對LW_OOPC的認(rèn)識。(備注:該問題來自C+沉思錄第八章的例子,有興趣的讀者可以對照參閱)問題描述:此程序涉及的內(nèi)容是用來表示算術(shù)表達(dá)式的樹。例如,表達(dá)式(-5)*(3+4)對應(yīng)的樹為:一個(gè)表達(dá)式樹包括代表常數(shù)、一元運(yùn)算符和二元運(yùn)算符的節(jié)點(diǎn)。這樣的樹結(jié)構(gòu)在編譯器和計(jì)算器程序中都可能用到。我們希望能通過調(diào)用合適的函數(shù)來創(chuàng)建這樣的樹,然后打印該樹的完整括號化形式。例如,我們希望#in
22、clude stdio.h#include Expr.hint main()Expr* expr1 = Expr_new();Expr* expr2 = Expr_new();Expr* expr3 = Expr_new();Expr* expr = Expr_new();expr1-initUnaryX(expr1, -, 5);expr2-initBinaryX(expr2, +, 3, 4);expr3-initBinary(expr3, *, expr1, expr2);expr-initBinary(expr, *, expr3, expr3);expr3-print(expr3);
23、printf(n);expr-print(expr);printf(n); Expr_delete(expr);Expr_delete(expr3);Expr_delete(expr2);Expr_delete(expr1);return 0;打印(-5)*(3+4)(-5)*(3+4)*(-5)*(3+4)作為輸出。此外,我們不想為這些表達(dá)式的表示形式操心,更不想關(guān)心有關(guān)它們內(nèi)存分配和回收的事宜。這個(gè)程序所做的事情在很多需要處理復(fù)雜輸入的大型程序中是很典型的,例如編譯器、編輯器、CAD/CAM系統(tǒng)等。此類程序中通常要花費(fèi)很大的精力來處理類似樹、圖和類似的數(shù)據(jù)結(jié)構(gòu)。這些程序的開發(fā)者永遠(yuǎn)需要面對
24、諸如內(nèi)存分配、靈活性和效率之類的問題。面向?qū)ο蠹夹g(shù)可以把這些問題局部化,從而確保今后發(fā)生的一系列變化不會(huì)要求整個(gè)程序中的其他各個(gè)部分隨之做相應(yīng)調(diào)整。解決方案:通過考查這個(gè)樹結(jié)構(gòu),會(huì)發(fā)現(xiàn)這里有3種節(jié)點(diǎn)。一種表示整數(shù)表達(dá)式,包含一個(gè)整數(shù)值,無子節(jié)點(diǎn)。另外兩個(gè)分別表示一元表達(dá)式和二元表達(dá)式,包含一個(gè)操作符,分別有一個(gè)或兩個(gè)子節(jié)點(diǎn)。我們希望打印各種節(jié)點(diǎn),但是具體方式需要視要打印節(jié)點(diǎn)的類型而定。這就是動(dòng)態(tài)綁定的用武之地了:我們可以定義一個(gè)虛函數(shù)(print)來指明應(yīng)當(dāng)如何打印各種節(jié)點(diǎn)。動(dòng)態(tài)綁定將會(huì)負(fù)責(zé)在運(yùn)行時(shí)基于打印節(jié)點(diǎn)的實(shí)際類型調(diào)用正確的函數(shù)。首先,我們抽象出“節(jié)點(diǎn)”的概念,抽象類的名字定為Expr_
25、node,它提供了打印的抽象接口,所有的實(shí)際節(jié)點(diǎn)類型均從它派生:ABS_CLASS(Expr_node) void (*print)(Expr_node* t);具體類的情形怎樣?這些具體類型中最簡單的一類是包含一個(gè)整數(shù),沒有子節(jié)點(diǎn)的節(jié)點(diǎn):CLASS(Int_node) EXTENDS(Expr_node); int n; void (*init)(Int_node* t, int k);其他類型又如何呢?每個(gè)類中都必須存儲一個(gè)操作符(這倒簡單,本文中假定操作符最長不超過2個(gè)字符,所以,可以用長度為3的字符數(shù)組來保存),但是如何存儲子節(jié)點(diǎn)呢?在運(yùn)行時(shí)之前,我們并不知道子節(jié)點(diǎn)的類型會(huì)是什么,所以
26、我們不能按值存儲子節(jié)點(diǎn),必須存儲指針。這樣,一元和二元節(jié)點(diǎn)類如下所示:CLASS(Unary_node) EXTENDS(Expr_node);char op3;/假設(shè)操作符最長不超過2個(gè)字符 Expr_node* opnd; void (*init)(Unary_node* t, const char* a, Expr_node* b);CLASS(Binary_node)EXTENDS(Expr_node); char op3;/假設(shè)操作符最長不超過2個(gè)字符 Expr_node* left; Expr_node* right;void (*init)(Binary_node* t, con
27、st char* a, Expr_node* b, Expr_node * c);這個(gè)設(shè)計(jì)方案可以用,不過有一個(gè)問題。用戶要處理的不是值,而是指針,所以必須記住分配和釋放對象。例如,我們需要這么創(chuàng)建表達(dá)式樹:Int_node* int_node1 = Int_node_new();Int_node* int_node2 = Int_node_new();Int_node* int_node3 = Int_node_new();Unary_node* unary_node = Unary_node_new();Binary_node* binary_node1 = Binary_node_new
28、();Binary_node* binary_node = Binary_node_new();int_node1-init(int_node1, 5);int_node2-init(int_node2, 3);int_node3-init(int_node3, 4);unary_node-init(unary_node, -, int_node1);binary_node1-init(binary_node1, +, int_node2, int_node3);binary_node-init(binary_node, *, unary_node, binary_node1);lw_oopc
29、_delete(int_node1);/ 刪除創(chuàng)建的其他節(jié)點(diǎn)也就是說,我們需要去關(guān)心每一個(gè)節(jié)點(diǎn)的創(chuàng)建和釋放。我們不僅把內(nèi)存管理這類煩心事推給了用戶,而且對用戶來說也沒有什么方便的辦法來處理這些事情。我們得好好想想辦法了。這里,提供一種解決內(nèi)存管理問題的思路:引用計(jì)數(shù),這里是針對指針,對指針的狀況進(jìn)行計(jì)數(shù),對象創(chuàng)建的時(shí)候,引用計(jì)數(shù)為1,凡是指針被賦值了,該指針?biāo)笇ο蟮囊糜?jì)數(shù)就自增一,每次指針要釋放,都先檢查對象的引用計(jì)數(shù),讓引用計(jì)數(shù)自減一,如果引用計(jì)數(shù)為0,則釋放該對象。另外,原先的設(shè)計(jì)不夠高層,用戶只能直接針對節(jié)點(diǎn)進(jìn)行操作,沒有提供操作子樹的概念(這也是用戶代碼之所以復(fù)雜的原因之一),我們
30、發(fā)現(xiàn),通過提供子樹的概念,我們不但能夠隱藏Expr_node繼承層次,而且,對于每一個(gè)節(jié)點(diǎn),我們具備了操縱左子樹和右子樹的能力(原來只能操作左子節(jié)點(diǎn)和右子節(jié)點(diǎn))。而這種功能增強(qiáng)完全是建立在面向?qū)ο蟮臋C(jī)制之上,我們并沒有引入耦合,在非常自然和輕松的情形下,我們獲得了更好的軟件組件之間協(xié)作的能力,這正是面向?qū)ο蟮镊攘λ?。這里,我們把子樹的概念用類Expr來表示,由于子樹此時(shí)成了Expr_node具體類的成員,同樣,左右子樹在Expr_node中同樣是以指針的方式保存,所以,對Expr也需要進(jìn)行引用計(jì)數(shù),代碼直接貼上來,細(xì)節(jié)解說參見注釋:/ expr.h#ifndef EXPR_H_INCLUDE
31、D_#define EXPR_H_INCLUDED_#include lw_oopc.h/ 表達(dá)式節(jié)點(diǎn)ABS_CLASS(Expr_node)int use; / 引用計(jì)數(shù) void (*print)(Expr_node* t); / 打印表達(dá)式節(jié)點(diǎn)void (*finalize)(Expr_node* t); / 子類通過覆寫finalize方法,實(shí)現(xiàn)對資源清理行為的定制;/ 表達(dá)式(子樹的概念),其中,init*方法族提供了構(gòu)建子樹的高層API,方便用戶使用CLASS(Expr)int use; / 引用計(jì)數(shù)Expr_node* p; / 子樹的根節(jié)點(diǎn) / 構(gòu)建整數(shù)表達(dá)式(包含一個(gè)整數(shù)值,
32、無子表達(dá)式)void (*initInt)(Expr* t, int); / 構(gòu)建一元表達(dá)式(包含一個(gè)操作符,一個(gè)子表達(dá)式)void (*initUnary)(Expr* t, const char*, Expr*); / 構(gòu)建一元表達(dá)式的重載形式(通過傳入個(gè)整型值參數(shù),構(gòu)造一個(gè)子表達(dá)式為整數(shù)表達(dá)式的一元表達(dá)式)void (*initUnaryX)(Expr* t, const char*, int); / 構(gòu)建二元表達(dá)式(包含一個(gè)操作符,二個(gè)子表達(dá)式)void (*initBinary)(Expr* t, const char*, Expr*, Expr*); / 構(gòu)建二元表達(dá)式的重載形式(
33、通過傳入個(gè)整型值參數(shù),構(gòu)造兩個(gè)子表達(dá)式均為整數(shù)表達(dá)式的二元表達(dá)式)void (*initBinaryX)(Expr* t, const char*, int, int);void (*print)(Expr* t); / 打印子樹;/ 整數(shù)表達(dá)式節(jié)點(diǎn)CLASS(Int_node) EXTENDS(Expr_node); / 繼承Expr_nodeint n; / 整數(shù)值 / 初始化整數(shù)表達(dá)式節(jié)點(diǎn)(傳入整數(shù)值)void (*init)(Int_node* t, int k); ;/ 一元表達(dá)式節(jié)點(diǎn)CLASS(Unary_node) EXTENDS(Expr_node); / 繼承Expr_nod
34、echar op3; / 假設(shè)操作符最長不超過2個(gè)字符 Expr* opnd; / 子表達(dá)式 / 初始化一元表達(dá)式節(jié)點(diǎn)(傳入一個(gè)操作符和一個(gè)子表達(dá)式)void (*init)(Unary_node* t, const char* a, Expr* b);/ 二元表達(dá)式節(jié)點(diǎn)CLASS(Binary_node)EXTENDS(Expr_node); / 繼承Expr_node char op3; / 假設(shè)操作符最長不超過2個(gè)字符 Expr* left; / 左子表達(dá)式 Expr* right; / 右子表達(dá)式 / 初始化二元表達(dá)式節(jié)點(diǎn)(傳入一個(gè)操作符和兩個(gè)子表達(dá)式)void (*init)(Bin
35、ary_node* t, const char* a, Expr* b, Expr* c);#endif/expr.c/ 包含所需頭文件ABS_CTOR(Expr_node)cthis-use = 1; / 構(gòu)造函數(shù)中,將引用計(jì)數(shù)初始化為END_ABS_CTOR/ Expr_node的析構(gòu)函數(shù)(DTOR/END_DTOR用于實(shí)現(xiàn)析構(gòu)函數(shù)語義)DTOR(Expr_node)if (-cthis-use = 0) / 遞減引用計(jì)數(shù),如果計(jì)數(shù)為,釋放自己cthis-finalize(cthis); / 釋放內(nèi)存之前先清理資源(其他需要釋放的對象)return lw_oopc_true;/ 返回tru
36、e,表示析構(gòu)成功,可以釋放內(nèi)存return lw_oopc_false;/ 返回false,表示析構(gòu)失敗,不能釋放內(nèi)存END_DTOR/ 構(gòu)建整數(shù)表達(dá)式(包含一個(gè)整數(shù)值,無子表達(dá)式),n為整數(shù)值void Expr_initInt(Expr* expr, int n)Int_node* intNode = Int_node_new(lw_oopc_file_line);intNode-init(intNode, n);expr-p = SUPER_PTR(intNode, Expr_node);/ 因篇幅所限,構(gòu)建一元表達(dá)式、二元表達(dá)式以及對應(yīng)的重載形式的函數(shù)實(shí)現(xiàn)代碼省略/ 打印表達(dá)式(子樹)v
37、oid Expr_print(Expr* t)Expr_node* p = t-p;p-print(p);CTOR(Expr)FUNCTION_SETTING(initInt, Expr_initInt);FUNCTION_SETTING(initUnary, Expr_initUnary);FUNCTION_SETTING(initUnaryX, Expr_initUnaryX);FUNCTION_SETTING(initBinary, Expr_initBinary);FUNCTION_SETTING(initBinaryX, Expr_initBinaryX);FUNCTION_SETT
38、ING(print, Expr_print);cthis-use = 1; / 構(gòu)造函數(shù)中,將引用計(jì)數(shù)初始化為END_CTOR/ Expr的析構(gòu)函數(shù)(DTOR/END_DTOR用于實(shí)現(xiàn)析構(gòu)函數(shù)語義)DTOR(Expr)if (-cthis-use = 0) / 遞減引用計(jì)數(shù),如果計(jì)數(shù)為,釋放自己Expr_node_delete(cthis-p);return lw_oopc_true;return lw_oopc_false;END_DTOR/ 整數(shù)表達(dá)式節(jié)點(diǎn)的初始化void Int_node_init(Int_node* t, int k)t-n = k;/ 整數(shù)表達(dá)式節(jié)點(diǎn)的打印void I
39、nt_node_print(Expr_node* t) Int_node* cthis = SUB_PTR(t, Expr_node, Int_node);printf(%d, cthis-n); / 整數(shù)表達(dá)式節(jié)點(diǎn)的資源清理void Int_node_finalize(Expr_node* t)/ 什么都不需要做CTOR(Int_node)SUPER_CTOR(Expr_node);FUNCTION_SETTING(init, Int_node_init);FUNCTION_SETTING(Expr_node.print, Int_node_print);FUNCTION_SETTING(E
40、xpr_node.finalize, Int_node_finalize);END_CTOR/ 因篇幅所限,一(二)元表達(dá)式節(jié)點(diǎn)的初始化、打印、資源清理、構(gòu)造等函數(shù)的實(shí)現(xiàn)代碼省略/main.c#include stdio.h#include Expr.hint main()Expr* expr = Expr_new();/ 創(chuàng)建expr1、expr2、expr3的代碼expr1-initUnaryX(expr1, -, 5);expr2-initBinaryX(expr2, +, 3, 4);expr3-initBinary(expr3, *, expr1, expr2);expr-initB
41、inary(expr, *, expr3, expr3);expr3-print(expr3);printf(n);expr-print(expr);printf(n);Expr_delete(expr);/ 刪除expr3、expr2、expr1的代碼return 0;程序運(yùn)行的效果如下:怎么樣?效果還不錯(cuò)吧,最重要的是,我們的C語言代碼現(xiàn)在已經(jīng)完全是面向?qū)ο蟮?。方案的可擴(kuò)展性如何?假設(shè)我們希望添加一種Ternary_node類型來表示三元操作符,如?:(也就是if-then-else操作符),看看,難度有多大?事實(shí)上,正是因?yàn)榍懊娴脑O(shè)計(jì)是面向?qū)ο蟮?,要增加一種節(jié)點(diǎn)類型易如反掌:/ 三元表達(dá)
42、式節(jié)點(diǎn)CLASS(Ternary_node)EXTENDS(Expr_node);char op3; / 假設(shè)操作符最長不超過2個(gè)字符Expr* left;Expr* middle;Expr* right; / 初始化三元表達(dá)式節(jié)點(diǎn)(傳入一個(gè)操作符和三個(gè)子表達(dá)式)void (*init)(Ternary_node* t, const char* op, Expr* left, Expr* middle, Expr* right);在Expr中添加創(chuàng)建三元表達(dá)式的方法:/ 表達(dá)式(子樹的概念),其中,init*方法族提供了構(gòu)建子樹的高層API,方便用戶使用CLASS(Expr)int use;
43、/ 引用計(jì)數(shù)Expr_node* p; / 子樹的根節(jié)點(diǎn)/ 既有實(shí)現(xiàn) / 構(gòu)建三元表達(dá)式(包含一個(gè)操作符,三個(gè)子表達(dá)式)void (*initTernary)(Expr* t, const char*, Expr*, Expr*, Expr*); / 構(gòu)建三元表達(dá)式的重載形式(通過傳入一個(gè)整型值參數(shù),構(gòu)造三個(gè)子表達(dá)式均為整數(shù)表達(dá)式的三元表達(dá)式)void (*initTernaryX)(Expr* t, const char*, int, int, int);/ 既有實(shí)現(xiàn);請讀者參照Binary_node的現(xiàn)有實(shí)現(xiàn),實(shí)現(xiàn)出Ternary_node,這里不再贅述。一旦實(shí)現(xiàn)出Ternary_node
44、,我們就可以這樣創(chuàng)建表達(dá)式樹并打印:/ 創(chuàng)建expr1、expr2、expr3、expr對象(指針)expr1-initUnaryX(expr1, -, 0);expr2-initUnaryX(expr2, -, 5);expr3-initBinaryX(expr3, +, 3, 4);expr-initTernary(expr, ?:, expr1, expr2, expr3);expr-print(expr);printf(n);為了支持新的節(jié)點(diǎn)類型,對原有代碼的更動(dòng)很少(僅對Expr類有增加方法),而且只有新增操作(新增類,新增方法),但沒有修改操作(指修改原有方法),面向?qū)ο蟮脑O(shè)計(jì)賦予
45、了系統(tǒng)極大的彈性,讓程序在應(yīng)對變化時(shí),更加從容。在這個(gè)例子中,LW_OOPC幫助我們在C語言的世界里營造出OO的天地,帶領(lǐng)我們再一次領(lǐng)略了面向?qū)ο蟮娘L(fēng)采。LW_OOPC最佳實(shí)踐說得簡單一點(diǎn),要想使用好LW_OOPC這套宏,還得首先懂面向?qū)ο?,要遵循面向?qū)ο笤O(shè)計(jì)的那些大原則,比如開閉原則等。在C語言中使用面向?qū)ο?,根?jù)實(shí)際使用的情況,給出如下建議:繼承層次不宜過深,建議最多三層(接口、抽象類、具體類,參見 REF _Ref253847604 h 圖 1和 REF _Ref253847612 h 圖 2)繼承層次過深,在Java/C#/C+中均不推崇,在C語言中實(shí)踐面向?qū)ο蟮臅r(shí)候,尤其要遵循這一點(diǎn)
46、,只有這樣,代碼才能簡單清爽。盡量避免多重繼承盡可能使用單線繼承,但可實(shí)現(xiàn)多個(gè)接口(與Java中的單根繼承類似)。盡量避免具體類繼承具體類具體類繼承具體類,不符合抽象的原則,要盡量避免。各繼承層次分別維護(hù)好自己的數(shù)據(jù)子類盡量不要直接訪問祖先類的數(shù)據(jù),如果確實(shí)需要訪問,應(yīng)當(dāng)通過祖先類提供的函數(shù),以函數(shù)調(diào)用的方式間接訪問。圖 SEQ 圖 * ARABIC 1圖 SEQ 圖 * ARABIC 2LW_OOPC的優(yōu)點(diǎn):輕量級廣泛的適應(yīng)性,能夠適應(yīng)各種平臺,各種編譯器(能支持C的地方,基本上都能支持)幫助懂OO的Java/C+程序員寫出面向?qū)ο蟮腃程序。使用C,也能引入OO的設(shè)計(jì)思想和方法,在團(tuán)隊(duì)的C/
47、C+分歧嚴(yán)重時(shí)可能非常有用。LW_OOPC的缺點(diǎn):無法支持重載(C語言不支持所致)不完全的封裝(無法區(qū)分私有、保護(hù)和公有)LW_OOPC的INTERFACE/ABS_CLASS/CLASS三個(gè)宏展開后都是C語言的struct,其成員全是公有的,宏本身并無能力提供良好地封裝層次的支持,所以,只能從編程規(guī)范和編程風(fēng)格上進(jìn)行引導(dǎo)。不支持RTTI 既然不支持RTTI,那么顯然也無法支持安全的向下轉(zhuǎn)型(C+中的dynamic_cast的轉(zhuǎn)型功能)不支持拷貝構(gòu)造以及賦值語義轉(zhuǎn)換成接口的表述有點(diǎn)麻煩,表達(dá)形式相比C+要啰嗦很多。有學(xué)習(xí)成本,需要用戶學(xué)習(xí)并習(xí)慣這套宏前四條缺點(diǎn),實(shí)質(zhì)上并非是LW_OOPC的缺點(diǎn)
48、,而是C相對C+而言的缺點(diǎn),在這里,之所以也一并列上,是希望用戶不要對LW_OOPC抱太高的期望,畢竟它也只是一套C語言的宏而已,C語言有的缺點(diǎn),LW_OOPC并不能夠解決??偨Y(jié):盡管如此,在使用C語言編程的時(shí)候,在某些情形下,你可能想要通過面向?qū)ο髞砀玫慕M織代碼。偶爾,你也想要用用某個(gè)設(shè)計(jì)模式,此時(shí),這套宏能夠幫得上忙,使用它,有助于寫出相對易于理解和維護(hù)的面向?qū)ο蟮拇a。因篇幅所限,本文中沒有介紹LW_OOPC的高級特性,譬如對內(nèi)存泄漏檢測的支持。示例的完整代碼、LW_OOPC的最新版本以及關(guān)于這套宏的更加詳細(xì)的使用指南,請讀者訪問 HYPERLINK 獲取。最后,期望有興趣的讀者,發(fā)揮
49、聰明才智,提出改進(jìn)建議,讓LW_OOPC變得越來越好!幕后花絮:在完善LW_OOPC宏的過程中,我也認(rèn)真研究了參考資料中列出的材料。最初V1.0版本有將近25個(gè)宏,后來,收到一些同事的反饋,認(rèn)為少量宏晦澀難記,而且不易理解,經(jīng)過認(rèn)真考慮,刪除掉5個(gè)宏,形成現(xiàn)在的20個(gè)宏。相比其他用C實(shí)現(xiàn)面向?qū)ο蟮姆桨?,LW_OOPC簡潔優(yōu)雅,每個(gè)宏的命名都經(jīng)過仔細(xì)地推敲,盡可能做到簡單易懂,便于記憶。但愿LW_OOPC真的能夠幫助到奮斗在一線的C程序員們。參考資料:1 高煥堂。UML+OOPC嵌入式C語言開發(fā)精講,電子工業(yè)出版社,2008 年9月。2 Object-oriented Programming w
50、ith ANSI-C,下載地址: HYPERLINK /codecuts/pdfs/ooc.pdf /codecuts/pdfs/ooc.pdf。3 C實(shí)現(xiàn)面向?qū)ο蟮姆椒ǎ?RealtimeMantra/basics/object_oriented_programming_in_c.htm附錄資料:不需要的可以自行刪除C語言編譯環(huán)境中的調(diào)試功能及常見錯(cuò)誤提示調(diào)試功能1常用健 : 激活系統(tǒng)菜單: 將光標(biāo)在編輯窗口和、信息窗口之間切換: 加載一個(gè)文件+: 查看程序運(yùn)行結(jié)果: 得到有關(guān)編輯器在線幫助+: 得到有關(guān)C語言的在線幫助+: 終止正在運(yùn)行的程序2塊操作 KB: 定義塊首 KK: 定義塊尾 K
51、V: 塊移動(dòng) KC: 塊復(fù)制 KY: 塊刪除 KH: 取消塊定義3查找、替換和刪除操作 QF: 查找字符串 QA: 查找并替換字符串 Option: G(全程), B(向文件頭), N(直接替換) Y : 刪除一行 QY: 刪除從光標(biāo)位置到行末的所有字符編譯中的常見錯(cuò)誤例析(1)警告類錯(cuò)誤 XXXdeclare but never used 變量XXX已定義但從未用過。 XXXis assigned a value which is never used 變量XXX已賦值但從未用過。 Code has no effect 程序中含有沒有實(shí)際作用的代碼。 Non-portable pointer
52、 conversion 不適當(dāng)?shù)闹羔樲D(zhuǎn)換,可能是在應(yīng)該使用指針的地方用了一個(gè)非0的數(shù)值。 Possible use of XXXbefore definition 表達(dá)式中使用了未賦值的變量 Redeclaration of main 一個(gè)程序文件中主函數(shù)main不止一個(gè)。 Suspicious pointer conversion 可疑的指針轉(zhuǎn)換。通常是使用了基本類型不匹配的指針。 Unreachable code 程序含有不能執(zhí)行到的代碼。(2)錯(cuò)誤或致命錯(cuò)誤 Compound statement missing in function main 程序結(jié)尾缺少括號。 “”expected;
53、 “(”expected等 復(fù)合語句或數(shù)組初始化的結(jié)尾缺少“)”;“(”。 Case outside of switch case 不屬于Switch結(jié)構(gòu),多由于switch結(jié)構(gòu)中的花括號不配對所致。 Case statement missing : switch結(jié)構(gòu)中的某個(gè)case之后缺少冒號。 Constant expression required 定義數(shù)組時(shí)指定的數(shù)組長度不是常量表達(dá)式。 Declaration syntax error 結(jié)構(gòu)體或聯(lián)合類型的定義后缺少分號。 Declaration was expected 缺少說明,通常是因?yàn)槿鄙俜纸绶缍禾?、分號、右圓括號等所引起的。
54、 Default outside switch Default部分放到了switch結(jié)構(gòu)之外,一般是因?yàn)榛ɡㄌ柌黄ヅ涠鸬摹?do statement must have while do語句中缺少相應(yīng)的while部分。 Expression syntax 表達(dá)式語法錯(cuò)。如表達(dá)式中含有兩個(gè)連續(xù)的運(yùn)算符 Extra parameter in call fun 調(diào)用函數(shù)fun時(shí)給出了多余的實(shí)參。 Function should return a value 函數(shù)應(yīng)該返回一個(gè)值,否則與定義時(shí)的說明類型不匹配。 Illegal use of pointer 指針被非法引用,一般是使用了非法的指針運(yùn)算
55、。 Invalid pointer addition 指針相加非法。一個(gè)指針(地址)可以和一個(gè)整數(shù)相加,但兩個(gè)指針不能相加。 Lvalue required 賦值運(yùn)算的左邊是不能尋址的表達(dá)式。 Misplaced else 程序遇到了沒有配對的else No matching 表達(dá)式中的括號不配對。 Pointer required on left side of_ 在“_”運(yùn)算的左邊只能允許一個(gè)指針而不能是一個(gè)一般的結(jié)構(gòu)體變量或聯(lián)合類型的變量。 Statement missing; 程序遇到了后面沒有分號的語句。 Too few parameters in call 調(diào)用某個(gè)函數(shù)時(shí)實(shí)參數(shù)目不
56、夠。 Unable to open include file XXXXXXXXXXX 頭文件找不到。 Unexpected 或:或 在不希望的地方使用了或:。 Undefined symbol Xin function fun 函數(shù)fun中的變量X沒有定義。5.連接中的常見錯(cuò)誤主要錯(cuò)誤類似于“undefined symbol _print in modula xxx”(print沒有定義),通常是函數(shù)名書寫錯(cuò)誤。6.運(yùn)行中的常見錯(cuò)誤Abnormal program termination 程序異常終止。通常是由于內(nèi)存使用不當(dāng)所致。Floating point error : Domain 或D
57、ivide by 0 運(yùn)算結(jié)果不是一個(gè)數(shù)或被0 除Null pointer assignment 對未初始化的指針賦值,程序有嚴(yán)重錯(cuò)誤。User break 在運(yùn)行程序時(shí)終止。7.程序的跟蹤調(diào)試?yán)肦un菜單可以進(jìn)行程序的跟蹤調(diào)試(1)GO to Cursor ()選擇該選項(xiàng)使程序執(zhí)行到光標(biāo)所在行首先將光標(biāo)移到某行(一般為可執(zhí)行),選擇該功能項(xiàng),則程序執(zhí)行到該行的前一行暫停。此時(shí)程序處于跟蹤調(diào)試狀態(tài),并有亮條顯示在暫停處,此時(shí)可以查詢變量或表達(dá)式的值。(2)Trace into ()執(zhí)行一條語句或一行暫停此時(shí)程序處于跟蹤調(diào)試狀態(tài),并有亮條顯示在暫停處。該選項(xiàng)可跟蹤到被調(diào)函數(shù)的內(nèi)部。(3)Ste
58、p over ()執(zhí)行一條語句或一行暫停此時(shí)程序處于跟蹤調(diào)試狀態(tài),并有亮條顯示在暫停處。該選項(xiàng)將自定義函數(shù)當(dāng)作一個(gè)語句執(zhí)行,不跟蹤到函程序的內(nèi)部。(4)Debug 菜單程序處于跟蹤狀態(tài)時(shí),可使用該菜單的選項(xiàng)。其主要是使用Evaluate目的是查詢或更新變量或表達(dá)式的值。選擇Evaluate功能后,系統(tǒng)彈出一個(gè)對話框。該對話框包含三個(gè)選項(xiàng)區(qū)域:Evaluate域可以輸入一個(gè)含有目前代碼中(程序暫停區(qū)的作用域)正在使用的變量名、或含變量的表達(dá)式、或常量表達(dá)式。按回車鍵后,在Result域中顯示變量或表達(dá)式的值。還可以用New value域進(jìn)行調(diào)試。如果調(diào)試程序時(shí)發(fā)現(xiàn)Result域顯示的某變量或表達(dá)
59、式的值不正確,并能估計(jì)出該變量或表達(dá)式的值,則可以將該值輸入到New value域,繼續(xù)執(zhí)行程序,其目的是肯定錯(cuò)誤發(fā)生處是否在當(dāng)前位置之前。如果輸入這個(gè)正確的值并將程序繼續(xù)執(zhí)行完畢而結(jié)果正確,說明在目前暫停處之前已經(jīng)發(fā)生錯(cuò)誤而之后無錯(cuò)誤。(5)Break/Watch用于設(shè)置斷點(diǎn)和監(jiān)視表達(dá)式。選擇Add Watch功能選項(xiàng),系統(tǒng)將彈出一個(gè)菜單,在Add Watch框中輸入變量名或表達(dá)式,按回車鍵后,系統(tǒng)在屏幕底部開辟一個(gè)窗口并顯示 該變量或表達(dá)式的值。【常見錯(cuò)誤信息語句索引】Ambiguous operators need parentheses:不明確的運(yùn)算需要用括號括起 Ambiguous
60、symbol xxx :不明確的符號 Argument list syntax error:參數(shù)表語法錯(cuò)誤 Array bounds missing in function main 缺少數(shù)組界限符 Array bounds missing :丟失數(shù)組界限符 Array size too large :數(shù)組尺寸太大 Bad character in paramenters :參數(shù)中有不適當(dāng)?shù)淖址?Bad file name format in include directive :包含命令中文件名格式不正確 Bad ifdef directive synatax :編譯預(yù)處理ifdef有語法錯(cuò)
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 高考物理總復(fù)習(xí)專題三牛頓運(yùn)動(dòng)定律第2講牛頓第二定律、兩類動(dòng)力學(xué)問題練習(xí)含答案
- 建筑場地土方運(yùn)輸填筑
- 外墻真石漆工程勞務(wù)分包
- 高中英語 Unit 4 Wildlife protection Speaking and writing教案 新人教版必修2
- 八年級物理下冊 第十二章 簡單機(jī)械12.2 滑輪第2課時(shí) 輪軸和斜面教案 (新版)新人教版
- 高中化學(xué) 第一冊 第一章 打開原子世界的大門 1.2 同位素和相對原子質(zhì)量教案 滬科版
- 2024-2025版新教材高中語文 第三單元 7 短歌行 歸園田居(其一)教案 新人教版必修上冊
- 2023九年級數(shù)學(xué)下冊 第27章 圓27.3 圓中的計(jì)算問題第1課時(shí) 弧長和扇形面積的計(jì)算教案 (新版)華東師大版
- 2024年秋八年級歷史上冊 第六單元 中華民族的抗日戰(zhàn)爭 第18課 從九一八事變到西安事變教案 新人教版
- 有關(guān)圓周率的數(shù)學(xué)家
- 神州數(shù)碼dcfw1800系列安全網(wǎng)關(guān)命令手冊40r4c
- 《創(chuàng)傷失血性休克中國急診專家共識(2023)》解讀課件
- 補(bǔ)貼資金管理辦法
- 食品安全管理制度可打印【7】
- 2024-2030年中國膜行業(yè)市場發(fā)展分析及趨勢前景與投資戰(zhàn)略研究分析報(bào)告
- (新版)糧油倉儲管理員職業(yè)鑒定理論考試題庫(含答案)
- 2024-2025學(xué)年滬科版中考數(shù)學(xué)模擬試卷及答案
- 2024發(fā)電企業(yè)安全風(fēng)險(xiǎn)分級管控和隱患排查治理管理辦法
- 2024-2030年中國甲硫基乙醛肟行業(yè)市場行情監(jiān)測及發(fā)展前景研判報(bào)告
- 《普通高等學(xué)校軍事課教程》課件第5章
- 第四章運(yùn)動(dòng)和力的關(guān)系單元教學(xué)設(shè)計(jì)
評論
0/150
提交評論