「慣 C」當然可以進行物件導向 Copyright (慣C) 2016 宅色夫
Deeply Introspect C Knowledge
學不好物件導向程式設計的人,往往是無法「把故事說好」者
$ git clone https://github.com/jserv/doxygen-oop-in-c.git
$ cd doxygen-oop-in-c
$ make
$ make doc
用網頁瀏覽器打開 doc/html/index.html
點擊 "Classes" -> "Object"
檔案 manual.c 定義了物件的繼承
這跟一般的 C 語言程式有什麼差別? (你乾脆問「你媽媽跟林志玲絕大多數的生理機能都一樣,有什麼差別?」)
(圖片來源)
「資料 + 動作」包裝成「物件」
只要有心,Brainf*ck 語言也能作 Object-Oriente d Programming (OOP)!
善用 forward declaration 與 function pointer,用 C 語言實現 Encapsulation,示範程式碼如下:
#include <stdio.h>
#include <stdlib.h>
/* forward declaration */
typedef struct object Object;
typedef int (*func_t)(Object *);
struct object {
int a, b;
func_t add, sub;
};
static int add_impl(Object *self) { // method
return self->a + self->b;
}
static int sub_impl(Object *self) { // method
return self->a - self->b;
}
// & : address of
// * : value of // indirect access
int init_object(Object **self) { // call-by-value
if (NULL == (*self = malloc(sizeof(Object)))) return -1;
(*self)->a = 0; (*self)->b = 0;
(*self)->add = add_impl; (*self)->sub = sub_impl;
return 0;
}
int main(int argc, char *argv[])
{
Object *o = NULL;
init_object(&o);
o->a = 9922; o->b = 5566;
printf("add = %d, sub = %d\n", o->add(o), o->sub(o));
return 0;
}
注意,為何要 Object **self
呢?
透過 pointer to pointer 才能確認是否有效
延伸閱讀: 探尋C語言名稱空間
designated initializers [C99]
C99 [3.14] object
C99 [6.2.4] Storage durations of objects
注意生命週期 (lifetime) 的概念,中文講「初始化」時,感覺像是「盤古開天」,很容易令人誤解。其實 initialize 的英文意義很狹隘: "to set (variables, counters, switches, etc.) to their starting values at the beginning of a program or subprogram."
在 object 的生命週期以內,其存在就意味著有對應的常數記憶體位址。注意,C 語言永遠只有 call-by-value
作為 object 操作的「代名詞」(alias) 的 pointer,倘若要在 object 生命週期以外的時機,去取出 pointer 所指向的 object 內含值,是未知的。考慮以下操作
ptr = malloc(size); free(ptr);
倘若之後做*ptr
,這個 allocated storage 已經超出原本的生命週期
C99 [6.2.5] Types
注意到術語!這是 C 語言只有 call-by-value 的實證,函式的傳遞都涉及到數值
這裡的 "incomplete type" 要注意看,稍後會解釋。要區分
char []
和char *
注意 "scalar type" 這個術語,日後我們看到
++
,--
,+=
,-=
等操作,都是對 scalar (純量)
[來源] 純量只有大小,它們可用數目及單位來表示(例如溫度=30oC)。純量遵守算數和普通的代數法則。注意:純量有「單位」(可用
sizeof
操作子得知單位的「大小」),假設ptr
是個 pointer type,對ptr++
來說,並不是單純ptr = ptr + 1
,而是遞增或遞移 1 個「單位」
這是 C/C++ 常見的 forward declaration 技巧的原理,比方說我們可以在標頭檔宣告
struct GraphicsObject;
(不用給細部定義) 然後struct GraphicsObject *initGraphics(int width, int height);
是合法的,但struct GraphicsObject obj;
不合法
這句話很重要,看起來三個不相關的術語「陣列」、「函式」,以及「指標」都稱為 derived declarator types,讀到此處會覺得驚訝的人,表示不夠理解 C 語言
再以「你所不知道的 C 語言:指標篇」提到的 oltk 為例,Polymorphism 的應用在於顯示裝置的支援。試想,如果我們能為 oltk 設計一組高階的 API ("tk" 就是 toolkit 的意思),然後實做本身可支援 fbdev (Linux framebuffer device) 和 X11 (Linux 桌面系統),那就可以確保在開發階段和實際在裝置執行的一致性。
[ gr_impl.h ]
struct gr_backend {
const char *name;
struct gr *(*open)(const char *, int width, int height, int nonblock);
};
extern struct gr_backend gr_x11_backend;
extern struct gr_backend gr_fb_backend;
雖然我們看到 x11 與 fb 字樣,顯然表示兩種不同的環境,但有趣的是,根本沒看到具體實做,僅有 struct gr_backend 以及裡頭的 open 與 name,我們稱 open 為 method / virtual function (OOP 用語)。而在 oltk 銜接的地方,是這樣作:
[ gr.c ]
static const struct gr_backend *backends[] = { &gr_x11_backend, &gr_fb_backend, };
static const int n_backends = sizeof(backends) / sizeof(backends[0]);
struct gr *gr_open(const char *dev, int nonblock)
{
struct gr *gr = NULL;
for (int i = 0; i < n_backends; i++) {
gr = backends[i]->open(dev, 480, 640, nonblock);
if (gr) break;
else { printf("gr_open: backend %d bad\n", i); }
}
return gr;
}
在此可見到,oltk 初始化時,會逐一去呼叫 graphics backend (也可以說 "provider") 的 open 這個 method,界面都是一樣的,但實際上的平臺相依程式碼卻大相逕庭。oltk 可專注在視覺表現和事件處理,而 framebuffer 到底怎麼提供呢?自行看 gr_fb.c 的實做細節即可。
值得注意的是,無論是 gr_fb.c 或 gr_x11.c,這兩個平臺相依的實做,在定義實做本體時,都有 gr 的實例 (instance):
struct gr_fb {
struct gr gr;
...
};
struct gr_x11 {
struct gr gr;
...
};
以 OOP 術語來看,我們可說 gr_fb 和 gr_x11 「繼承」(inherit) 了 gr,熟悉 C++ 程式設計的朋友可能會不以為然,這是 "has-a",並非 "is-a" 的關聯。的確,我們需要更精確的說法,這裡只是展示概念。
無論實做怎麼變動,oltk 大部份的程式碼只需要在意將按鈕、文字方塊、底圖等視覺元件描繪在 gr_open
所指向的真正實做中,定義在 gr.h 的界面:
[ gr.h ]
struct gr {
int width; int height;
int bytes_per_pixel; int depth;
void (*close)(struct gr *);
void (*update)(struct gr*, struct gr_rectangle *, int);
void (*set_color)(struct gr *, unsigned int, struct gr_rgb *, unsigned int);
int (*get_color)(struct gr *, unsigned int);
int (*sample)(struct gr* gr, struct gr_sample *);
};
然後在 oltk.c 中,如果想變更按鈕視覺元件的顏色,可以這麼實做:
[ oltk.c ]
void oltk_button_set_color(struct oltk_button *b,
enum oltk_button_state_type state,
enum oltk_button_color_type color,
int rgb)
{
struct gr_rgb grrgb;
struct gr *gr = b->oltk->gr;
grrgb.red = ((rgb >> 16) & 0xff) << 8; grrgb.green = ((rgb >> 8) & 0xff) << 8; grrgb.blue = (rgb & 0xff) << 8;
gr->set_color(gr, b->oltk->color_index, &grrgb, 1);
顯然,上述程式呼叫的 "set_color",也是個 method,完全看 gr 這個 instance 指向哪一種平臺相依的裝置。
更多的案例可見以下:
==抽象化==各種常見的問題,並提供抽象化的==解決方案==
https://github.com/QMonkey/OOC-Design-Pattern.git
cd OOC-Design-Pattern
make
Name: Observer
Context: 你在購物網站上看到一個很熱門且價格超便宜的產品,正當你想要下單購買的時候,你發現這個產品目前「售完補貨中」。
Problem: 如何得知商品已到貨?
Force:
Solution: 請購物網站 (Subject) 提供「貨到通知」功能,讓你 (Observer) 可以針對有興趣的產品主動加入 (attach) 到「貨到請通知我」的名單之中。你必須將你的電子郵件地址 (update) 提供給購物網站,以便貨品到貨的時候購物網站可以立即通知 (notify) 你。當你對產品不再有興趣的時候,購物網站也要讓你可以從「貨到請通知我」的名單中移除 (detach)。
[ include/iobserver.h ]: i
means interface
typedef struct _IObserved IObserved;
typedef struct _IObserver IObserver;
struct _IObserved { /* 商品 */
void (*registerObserver)(IObserved *, IObserver *);
void (*notifyObservers)(IObserved *);
void (*removeObserver)(IObserved *, IObserver *);
};
struct _IObserver { /* 顧客 */
void (*handle)(IObserver *);
};
[ include/observed.h ]
typedef struct _Observed Observed;
struct _Observed {
IObserver **observers;
size_t count;
size_t size;
union {
IObserved;
IObserved iobserved;
};
};
extern Observed *Observed_construct(void *);
extern void Observed_destruct(Observed *);
[ include/observer.h ]
#include "iobserver.h"
typedef struct _Observer Observer;
struct _Observer {
union {
IObserver;
IObserver iobserver;
};
};
extern Observer *Observer_construct(void *);
extern void Observer_destruct(Observer *);
Observer 是種被動呼叫的典範,當有份資料會不定期的更新,而許多人都想要最新版的資料時,有兩種做法,最簡單的是寫個執行緒不斷去確認,但這致使 CPU 的 busy wait;另一種做法就是當資料更新時,由資料擁有者去通知大家「嘿!新貨到!」,這時在意的人就會跑過來看,如此模式即是 "observer"。
要留意的是,現實生活中要讓跑過來的眾人不要撞在同一個時間點,如果是網路模組也是,可能會造成阻塞。
[ src/main.c ]
const int OBSERVER_SIZE = 10;
int main()
{
Observed *observed = new (Observed);
IObserved *iobserved = &observed->iobserved;
Observer *observers[OBSERVER_SIZE];
for (int i = 0; i < OBSERVER_SIZE; ++i) {
observers[i] = new (Observer);
observed->registerObserver(iobserved, &observers[i]->iobserver);
printf("handle: %p\n", &observers[i]->iobserver);
} printf("\n");
iobserved->notifyObservers(iobserved);
for (int i = 0; i < OBSERVER_SIZE; ++i)
delete (Observer, observers[i]);
delete (Observed, observed);
return 0;
}
Name: Strategy
Context: 為了達到相同的目的,物件可以因地制宜,讓行為擁有多種不同的實作方法。例如,一個壓縮檔案物件,可以採用 zip、arj、rar、tar、7z 等不同的演算法來執行壓縮工作。
Problem: 如何讓物件自由切換演算法或行為實作?
Force:
Solution: ==將演算法從物件 (Context) 身上抽離出來。== 為演算法定義一個 Strategy 介面,針對每一種演算法,新增一個實作 Strategy 介面的 ConcreteStrategy 物件。把物件中原本的演算法實作程式碼移到相對應的 ConcreteStrategy 物件身上。讓 Context 物件擁有一個指向 Strategy 介面的成員變數,在執行期間藉由改變 Strategy 成員變數所指向的 ConcreteStrategy 物件,來切換不同的演算法演算法。
[ include/itravel_strategy.h ]
typedef struct _ITravelStrategy ITravelStrategy;
struct _ITravelStrategy {
void (*travel)(ITravelStrategy *);
};
[ include/person.h ]
typedef struct _Person Person;
struct _Person {
ITravelStrategy* travelStrategy;
void (*setTravelStrategy)(Person*, ITravelStrategy*);
void (*travel)(Person*);
};
extern Person *Person_construct(void *, ITravelStrategy *);
extern void Person_destruct(Person *);
旅行時留下深刻的像 (?!)
[ src/main.c ]
#include "base.h"
#include "itravel_strategy.h"
#include "airplane_strategy.h"
#include "train_strategy.h"
#include "person.h"
int main() {
TrainStrategy *trainStrategy = new (TrainStrategy);
ITravelStrategy *itravelStrategy = &trainStrategy->itravelStrategy;
Person* person = new (Person, itravelStrategy);
person->travel(person);
AirplaneStrategy *airplaneStrategy = new (AirplaneStrategy);
itravelStrategy = &airplaneStrategy->itravelStrategy;
person->setTravelStrategy(person, itravelStrategy);
person->travel(person);
delete (Person, person);
delete (AirplaneStrategy, airplaneStrategy);
delete (TrainStrategy, trainStrategy);
return 0;
}
Strategy pattern 的重要範本,很多 pattern 其實是從 Strategy pattern 延伸,基本概念是:當一件事情有很多種做法的時候,可在執行時期選擇不同的作法,而非透過 if-else 去列舉,這樣能讓程式碼更容易維護、增修功能。
取自《Beautiful Code》
[ source ]
定義一個通用的通訊界面:
typedef struct {
int (*open)(void *self, char *fspec);
int (*close)(void *self);
int (*read)(void *self, void *buff, size_t max_sz, size_t *p_act_sz);
int (*write)(void *self, void *buff, size_t max_sz, size_t *p_act_sz);
} tCommClass;
tCommClass commRs232; /* RS-232 communication class */
commRs232.open = &rs232Open;
commRs232.write = &rs232Write;
tCommClass commTcp; /* TCP communication class */
commTcp.open = &tcpOpen;
commTcp.write = &tcpWrite;
對應的通訊實做: [TCP subclass]
static int tcpOpen (tCommClass *tcp, char *fspec) {
printf ("Opening TCP: %s\n", fspec);
return 0;
}
static int tcpInit (tCommClass *tcp) {
tcp->open = &tcpOpen;
return 0;
}
當然你也可以定義 HTTP subclass
static int httpOpen (tCommClass *http, char *fspec) {
printf ("Opening HTTP: %s\n", fspec);
return 0;
}
static int httpInit (tCommClass *http) {
http->open = &httpOpen;
return 0;
}
測試程式:
int main (void) {
int status;
tCommClass commTcp, commHttp;
// Same ’base’ class but initialized to
// different sub-classes.
tcpInit (&commTcp);
httpInit (&commHttp);
// Called in exactly the same manner.
status = (commTcp.open)(&commTcp, "bigiron.box.com:5000");
status = (commHttp.open)(&commHttp, "[](images/www.microsoft.com");
return 0;
}
[ source ]
namespace:
stack_push(thing *)
stack::push(thing *); // C++
用 C++ 表示:
class stack {
public:
stack();
void push(thing *);
thing *pop();
static int this_is_here_as_an_example_only;
private:
...
};
「慣 C」表示:
struct stack {
struct stack_type *my_type; // *self*
/* Put the stuff that you put after private: here */
};
struct stack_type {
void (*construct)(struct stack *this); /**< initialize memory */
struct stack *(*operator_new)(); /**< allocate a new struct to construct */
void (*push)(struct stack *this, thing *t);
thing * (*pop)(struct stack * this);
int this_is_here_as_an_example_only;
} Stack = {
.construct = stack_construct,
.operator_new = stack_operator_new,
.push = stack_push,
.pop = stack_pop
};
使用方式:
struct stack * st = Stack.operator_new(); /* a new stack */
if (!st) {
/* Do something about it */
} else {
stack_push(st, thing0); /* non-virtual call */
Stack.push(st, thing1); // cast *st to a Stack and push
st->my_type.push(st, thing2); /* a virtual call */
}
$ git clone https://github.com/embedded2016/server-framework.git
$ cd server-framework
$ make
測試 async
$ ./test-async
wrote task 8190
wrote task 8191
Hi! 16377
Hi! 16378
# signal finish at (711.426132) ms
# elapsed time: (711.728594) ms
測試 reactor
$ ./test-reactor
Serving HTTP on 127.0.0.1 port 8080
now open up the URL [](images/localhost:8080 on your browser
$ ./test-protocol-server
(pid 4130 : 8 threads) Listening on port 8080 (max sockets: 65536, ~5 used)
telnet localhost 8080
,可以發現原本執行 test-protocol-server 的終端機印出以下訊息:A connection was accepted on socket #7.
Trying 127.0.0.1...
Connected to localhost.
Escape character is ’^]’.
Simple echo server. Type ’bye’ to exit.
測試網頁伺服器的效能
確保之前的測試程式均已關閉,而且 port 8080 沒被佔用
$ pkill -9 httpd 和 $ pkill -9 "test-"
來強制關閉伺服器執行 $ ./httpd
,預期將看到類似以下輸出:
(pid 4223 : 1 threads) Listening on port 8080 (max sockets: 65536, ~5 used)
Clients: 0
# Total Clients: 0
$ ab -c 32 -n 100 [](images/
This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, [](images/
Licensed to The Apache Software Foundation, [](images/
Concurrency Level: 32
Time taken for tests: 15.621 seconds
Complete requests: 100
Failed requests: 6
(Connect: 0, Receive: 0, Length: 6, Exceptions: 0)
Total transferred: 9900 bytes
HTML transferred: 1300 bytes
Requests per second: 6.40 [#/sec] (mean)
Time per request: 4998.873 [ms] (mean)
Time per request: 156.215 [ms] (mean, across all concurrent requests)
Transfer rate: 0.62 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 2 0.7 2 3
Processing: 3619 3877 177.2 3996 4001
Waiting: 0 1 0.5 0 3
Total: 3621 3879 177.2 3998 4001
server done
(4223) Stopped listening for port 8080
使用 Doxygen 自動產生文件
Doxygen 可掃描給定原始程式碼內容、註解,之後將自動產生程式文件,其中一種輸出就是 HTML,可透過瀏覽器來閱讀文件和程式碼
執行 make doc,之後會產生新的目錄 html,可用 Firefox 或 Chrome browser 開啟該目錄底下的 index.html 檔案
類似以下網頁畫面,注意右邊有搜尋功能:
按下 “Data Structures” 頁面可探究物件相依性示意圖:
搭配閱讀以下材料:
CHAPTER 6. UNIT- AND REGRESSION TESTING
能用 Gtk+ 來舉例說明物件導向程式設計嗎?
struct 沒有private的機制,這樣會不會造成安全上的問題?
動作及資料封裝後的連動,在程式開始寫之前,應該如何去規劃?
不是很懂 GTK 的 interface 實作中的這個 G_DECLARE_INTERFACE macro 到底是怎麼做到的這麼多事情的
所以對OS來說可以區別函數指標或者資料指標嘛?兩者的差別是否只在於其指向之記憶體位置之屬性(不可執行/可執行)?
使用function pointer是不是會增加額外的記憶體消耗?(原本可以直接執行function,封裝後需要透過function pointer)
由於function pointer所執行的function是執行期決定的,會不會造成編譯時期最佳化的阻礙?
在C語言中去sizeof一個function有意義嗎?如果要求function的大小,是不是要去disassembly才能知道確切大小呢?
Callback function 的使用時機?
有些function會在function 最末端寫"return; " 不是return 0 也不是 return NULL請問這樣設計的意義何在?
eg:
void func(){
/* do something */
return;
}
我猜他應該是說void function中有時候會加return,有沒有加會有什麼差[name=S60203] TODO: look up C99 spec[name=jserv] C99 6.8.6.4 The return statement Constraints A return statement with an expression shall not appear in a function whose return type is void. A return statement without an expression shall only appear in a function whose return type is void.[name=楊日新]
所以Observer Pattern就是,原本多人去觀察一個『事件』,變成由擁有者去觀察『事件』,再通知有追蹤的人?所以,擁有者還是有需要一個Loop去觀察這個『事件』?
我想Observer Pattern想解決的問題是「減少追蹤者的麻煩」,事件擁有者如何觀察事件並不是Observer Pattern想要關心的。若事件擁有者是事件的起源(或即事件本身),更新事件與通知同時一起做。[name=楊日新]
用C寫物件導向程式的實做方法有許多種,是否有推薦的撰寫方式?例如GTK+有自己的物件導向風格,new(或initialazition)是用回傳的方式;而第一個範例是用指標的指標獲得物件。
ZeroMQ 的C語言設計原則:http://rfc.zeromq.org/spec:21 也相當值得一讀,其他極簡化的C library有:
如果希望程式能在32bit跟64bit機器上都能最佳化,有什麼要注意的?
Visitor pattern 是個相當實用的模式 (例如實現一個解釋執行 AST 的 Interpreter,我們能讓樹中不同類型的節點都有個名為 eval 的 method,該 method 的作用是對節點自身解釋求值,只要遞迴地呼叫各個結點的 eval,最終就能得到執行結果),請問如何用 C 漂亮地實現這個模式?