C 語言中 typedef 可以用來擴充 C 原有的資料型態. 通常我們會將某個資料型態或者將常用的資料型態組合給予一個比較直觀而易懂的別名. 定義別名之後我們就可以像使用原有的資料型態來宣告或定義變數一樣, 直接拿它來宣告或定義(註一, 註二)變數.
註一: 宣告和定義有所不同. 定義變數會實際佔據記憶體空間, 而宣告變數則只產生參考的連結, 稍後連結程式時再連結到在其他模組定義的變數. 我們一般把宣告變數擺放在 header file (.h 檔) 中, 有需要的模組或程式只要 include 即可. 而定義變數則視情況放在主程式或者相關的模組中, 當然它通常也會 include 該 header file.
註二: ANSI C 標準文件說: 會實際佔據記憶體空間的宣告稱為定義. 所以 ANSI C 說的宣告包含了定義及純宣告. 而註一及以下本文中所指的宣告則是指沒有佔據記憶體空間的純宣告, 而不是 ANSI C 原先所指的宣告, 特此說明. 請參考維基網站 Declaration (computer programming) 段落二 'Declaration vs. definition' 及段落三 'Declarations and Definitions')
底下的程式片段是變數定義的部份, 沒有使用 typedef 的樣子:
unsigned char flag1, flag2;
struct _list_node_ {
unsigned long size;
struct _list_node_ *next;
} node0, *free_list;
- 第1行定義了 2 個資料型態為unsigned char變數flag1和flag2.
- 第2~5行則宣告了一個結構, 並以此定義了 1 個結構變數node0和1個指向這種結構的指標變數 free_list.
再來看的是改用 typedef 後的樣子:
typedef unsigned char bool;
typedef struct _list_node_ {
unsigned long size;
struct _list_node_ *next;
} LIST_NODE;
bool flag1, flag2;
LIST_NODE node0, *free_list;
- 第1行的 typedef 為 unsigned char 取了個好記的別名 bool.
- 第2~5行則將結構 _list_node_ 擴充為資料型態並為其命名為 LIST_NODE. 這裡要注意的是結構內部有一個指標指向和自己一樣的結構. 在 typedef 的定義中我們只能使用 struct _list_node_ * 而不可以使用 typedef 的成果 LIST_NODE (因為 LIST_NODE 尚未定義完成. 你也可以把 typedef 的定義和結構的定義拆開來).
- 第7~8行則拿新定義的別名, 來定義原本程式要定義的變數. 如果再把 1~5 行的 typedef 移到標頭檔 (xxx.h), 只留下 7~8 行這二行變數定義的部份, 程式看起來就簡潔多了.
- 上面有關 LIST_NODE 的部份, 也可以換一個寫法:
typedef struct _list_node_ { unsigned long size; struct _list_node_ *next; } LIST_NODE, *pLIST_NODE; LIST_NODE node0; pLIST_NODE free_list;
註三: 使用 , 將多個變數的定義/宣告連結起來時, 要注意 * (指標) 並不算在共同的資料型態這一邊, 而是算在變數名稱這一邊. 所以上面的例子裡的 int32_t a, *p; 和 int32_t *p, a; 以及 int32_t* p, a; 意義上都是一樣的. 初學者需要特別小心最後一種寫法, 非常容易讓人弄錯搞迷糊了.
下面的例子含有例舉 (enum) 別名定義, 應該不用多作解釋.
enum color { black, white, gold, pink };
typedef enum color iPhoneColor;
iPhoneColor x = gold;
再下來是陣列 array 的例子:
typedef uint8_t Buffer[16];
Buffer xBuf;
xBuf[0] = 3;
xBuf[1] = 2;
- 第3行有些小小的怪異, 定義變數時好像沒有指定是陣列, 後面卻可以用陣列的寫法. 其實第3行相當於 uint8_t xBuf[16]
- 用法可能看起來有點奇怪, 卻可以保證每次用 Buffer 定義或宣告的陣列變數 一定是 16 個 uint8_t 元素. 好處是陣列的大小需要改變時, 只要修改 typedef 不必整個專案翻找一遍, 還要擔心是不是有改漏了.
如果遇到看不懂時, 建議你可以把定義中的 typedef 拿掉, 同時資料型態名稱換成變數的名稱, 就會比較容易理解. 例如: 把 typedef uint8_t Buffer[16]; 去掉 typedef, Buffer 換成變數名 xBuf, 變成 uint8_t xBuf[16];
接著, 我們進一步加一些變化
// Examples of typedef a pointer
typedef struct _list_node_ * pLIST_NODE; // (1)
typedef struct _list_node_ (* pLIST_NODE); // (2)
// Examples of typedef a function or pointer of function
typedef int IamFunc (int, int); // (3)
typedef int *IamFunc (int, int); // (4)
typedef int (*IamFunc)(int, int); // (5)
- 式子(1) 是使用了結構指標, 寫法很平常, 看起來應該很習慣.
- 式子(2) 的寫法看起來感覺好像有點玄機... 但其實並沒有, 式子(1)和(2)這二個寫法是一樣的:
在定義或宣告指標 int * ptr 的寫法中, 星號左右兩邊的空白是可有可無的, 所以 int* ptr, int *ptr, int * ptr, int*ptr, 都是正確的而且意義也相同.
而解釋上你可以說 ptr 是一個 int*, 也可以說 *ptr 是一個 int. 所以多加了括號並不會改變它的意義. - 式子(3) 實際是定義一個別名 IamFunc, 它是一個傳回值為整數且需要二個整數參數的函數.
- 式子(4) 和式子(3) 比較, 只是回傳值由 int 變為 int*. 因為在 IamFunc 右邊的函數呼叫運算子 (), 比在左邊的取值運算子 *, 優先權要來得較高.
- 式子(5) 和式子(3) 比較, 多加了括號強制取值運算子 * 先執行, 所以函數變成了指向函數的指標, (名為 IamFunc 的資料型態, 是一個指標指向一個傳回值為整數且需要二個整數參數的函數.) 簡單一點說, 式子(5) 定義的資料型態是一種指向函數的指標, 將來我們可以用它來定義變數, 並用來存放式子(3)這類函數的地址, 或者說是讓該變數指向式子(3)所定義的其中一個函數.
- 有人說 式子(3) 和式子(5) 是對等的, 關於這點我不是很清楚, 需要多一點時間找資料及作些實驗來驗證.
- 答案是錯的: 就 typedef 的定義內容來說, 他們就不一樣的.
- 式子(3) 定義的資料型態是一種函數 (一種傳回值為整數且需要二個整數參數的函數) 所以我們可用它來定義許多這類函數, 而這些函數的功能是都是固定的 (編譯時期確定的);
- 式子(5) 定義的資料型態是指向函數指標, 該函數必需是一種傳回值為整數且需要二個整數參數的函數 (像是資料型態為式子(3)定義的那一種). 當這一類指標附加上 () 時, 它就變成執行所指向的函數. 所以, 它的函數功能是動態的, 端看執行時指向的是哪一個函數.
- 這裡有一篇有關 function pointer 的說明 (英文) Declaring, Assigning, and Using Function Pointers 是 Usenet 上 comp.lang.c FAQ list 的維護人 Steve Summit 先生寫的, 對於 function pointer
的取址運算 & 及取值運算 *為何有上述二種寫法都對的現象有明確的解說, 提供給大家參考. - 另外 C 編譯器對指定數值給 function pointer 以及經由 function pointer 呼叫該函數的處理方式和其他指標並不一樣, 而且還有點另類:
- 指定數值給 function pointer 時: 我們可以將函數取址後 (&) 指定給 function pointer, 也可以直接將函數 (不加 ()) 指定給 function pointer.
- 呼叫 function pointer 所指定的函數時: 我們可以將 function pointer 取值後 (*)再呼叫, 也可以不加取值運算直接呼叫.
extern int f2(int, int); extern int f3(int, int); typedef int (*funcptr)(int, int); funcptr pfi; // 設定函數的地址給函數指標變數 pfi = condition ? &f2 : &f3; // OK, 正式寫法. pfi = condition ? f2 : f3; // 但, 這樣寫也 OK. // 經由函數指標變數呼叫函數 (*pfi)(arg1, arg2); // OK, 正式寫法. pfi (arg1, arg2); // 但, 這樣寫也 OK.
- 答案是錯的: 就 typedef 的定義內容來說, 他們就不一樣的.
來一個完整的使用式子(3) 的例子:
#include <stdio.h>
typedef int IamFunc(int, int);
IamFunc add, sub, mul;
int add(int a, int b) { return a+b; }
int sub(int a, int b) { return a-b; }
int mul(int a, int b) { return a*b; }
int main(void){
int a, b;
IamFunc * func;
func = &add; // 指定數值時, 可以加取址
// 呼叫函數時, 可以不加取值
printf("%s: %d\n", "add", func(5, 3));
func = sub; // 指定數值時, 也可不加取址
// 呼叫函數時, 也可以加取值
printf("%s: %d\n", "sub", (*func)(5, 3));
func = mul;
printf("%s: %d\n", "mul", func(5, 3));
return 0;
}
陷阱 -- 有關 storage class 和 qualifier 經常出現的錯誤
- 由於 typedef 的成果會被視為資料型態的擴充, 定義或宣告變數時可以使用指定儲區類別 (storage class) 的四個 keyword (auto, static, extern, register) 來加以修飾, 因此 typedef 的內容本身是不可以使用這四個 keyword 的.
// 下行的語法是錯誤: static 不可以出現在 typedef 中 typedef static int newINT; newINT x, y, x; // 要改成下列二行才行 (static 必須移到變數定義式) typedef int newINT; static newINT x, y, z;
- 另外二個限制詞 (qualifier, const 和 volatile) 則沒有上述的限制: 可以出現在新資料型態的 typedef 定義中; 也可以不出現在新資料型態的 typedef 定義中, 而改在變數定義或宣告時加上限制. 當然, 同一個限制詞我們不可以二
者邊都加. 不同於儲區類別的只能四選一, 限制詞並沒有二選一的限制, 可以依需要加上. 此二者就經常同時出現於宣告硬體的狀態暫存器, 即該變數是唯讀且揮發性. 同時, 要小心變數定義或宣告內容包含有指標的情況, 此時 keyword const 和 volatile 的限制標的變成有二個: 一個是指標本身另一個是指標所指向的資料. 這個時候到底誰被 keyword 限制, 取決於 keyword 出現的位置.- 下面的式子(1), 式子(2)和式子(3)寫法相通, 是定義一個指標變數指向常數資料 (指標值可變, 資料值不可變).
- 式子(4)和式子(5)寫法相通, 是定義一個常數指標指向可變動的資料 (指標值不可變, 資料值可變).
- 式子(1)的寫法常常會被誤以為應該和式子(5)相等, 所以就錯誤的把式子(1)化簡為式子(5). 但實際上是式子(1)應該以化簡為式子(3). (我們應該把式子(5) const ptr p 中的 ptr 看成和 const int x 中的 int 一樣, 是一個資料型態. 而不要把它以 typedef 的定義 char * 來替代.)
// define a non-const pointer to const data const char * p; // (1) char const * p; // (2) typedef const char * ptr; // (3-1) ptr p; // (3-2) // define a const pointer to non-const data char * const p; // (4) typedef char* ptr; // (5-1) const ptr p; // (5-2)
- 備註:
- 式子(4)和式子(5-2)在實際應用中是不 OK 的, 因為 (指標變數的) 變數值本身是一常數, 必需在定義變數的同時指定其常數值. 實際應用的例子如下: 式子(4a)是指定某一個變數的位址; 式子(4b)是指定一特定位址.
char * const p = &x; // (4a) char * const p = 0x200000; // (4b)
- 不過式子(4b)的用法會多浪費一個指標變數的空間 (即變數 p 本身). 這是因為 0x200000 本身就是一個 const, 所以沒必要用變數來儲存它然後又宣告說該變數是常數不可以更動. 其實我們可以直接用 type casting 的方法把 0x200000 轉型就可以了, 即 ((char *)0x200000). 如果覺得後續使用它的程式敘述會不好讀, 那可以加入 #define CONST_P ((char *)0x200000) 這樣的置換巨集, 然後把程式敘述改成使用 CONST_P 來代替 ((char *)0x200000) 即可.
- 式子(1), 式子(2)和式子(3)在實際應用中是 OK 的, 同時它只是限定不可以經由指標變數 p 來改變其所指到的變數, 而不是限定所指到的變數必需是常數.
typedef const char * ptr; ptr p; char x = 0x20; p = &x; *p = 0x21; // Compiler will alert. x = 0x21; // OK
最常看到的錯誤範例是我們想要寫一個像 strcmp() 那樣的函數, 於是宣告了以下的函數原型 mystrcmp(const char *, const char *), 然後為了想簡化於是又增加了定義 typedef char * pstr; 接著把函數原型宣告改成 mystrcmp(const pstr, const pstr), 然後就掛掉了... (我們希望的是字串比較時不要去動到字串的內容, 而不是指標值不能更動)
// Wrong definition:
typedef char * pstr;
mystrcmp(const pstr, const pstr);
// Correct definition:
typedef const char * cpstr;
mystrcmp(cpstr, cpstr);
轉換化簡
再來看一些用 typedef 轉換化簡的例子: from blog.sina.com.cn typedef的四个用途和两大陷阱
覺得很吃力看不下去了嗎? 先讀一下這一篇 C 語言:輕鬆讀懂複雜的宣告式 (Define and Read the complex declarations)
轉換化簡的原則:
- 不能破壞原有之運算先後次序.
- 轉換化簡沒有固定的答案, 完全視程式的需要取 typedef 的截斷點.
轉換化簡程序:
- 先取一個合適的截斷點
- 將截斷點之後的低優先權運算以 typedef 定義為別名
- 然後用別名定義或宣告截斷點之前的高優先權運算.
例一: 變數為一陣列, 陣列元素內容為 函數指標
// 原始寫法:
int *(*a[5])(int, char*);
// 轉換1:
typedef int *(*pFun)(int, char*);
pFun a[5];
// 轉換2:
typedef int *Func(int, char*);
Func *a[5];
- 轉換1: 在 *a[5] 中, [] 優先權比 * 高, 故把 a[5] 留在原來變數定義的式子, 其餘的轉為 typedef
- 轉換2: 把 *a[5] 整個留在原來的變數定義式, 其餘的轉為 typedef
例二: 變數為一陣列, 陣列元素內容為 函數指標, 函數之參數為 函數指標
// 原始寫法:
void (*b[10])(void (*)());
// 轉換為:
typedef void (*pFunParam)(); // 右半部, 函數的參數
typedef void (*pFunx)(pFunParam); // 左半部的函數
pFunx b[10];
例三: 變數為指向陣列之指標, 陣列元素固定為 9, 陣列元素內容為 函數指標(註四)
// 原始寫法:
double(*)() (*e)[9];
// 轉換為:
typedef double (*pFuny)(); // 左半部
typedef pFuny (*pFunParamy)[9]; // 右半部
pFunParamy e;
註四: 原始寫法 double(*)() (*e)[9]; 有誤. 正確寫法請暫時參看二樓留言.
化簡內含 '函數指標' 的敘述式
函數指標最常見的應用是使用在 callback 的技術上. 由於需要將某一函數的位址當成參數傳送給另一個函數, 因此使用 typedef 替這種 callback 函數的指標定義一個新名字 (新資料型態), 可以大幅提昇程式的可讀性, 日後維護及修改上比較不會出錯.
下面的例子借用自 Wiki 網站對 typedef 的解說
int do_math(float arg1, int arg2) {
return arg2;
}
int call_a_func(int (*call_this)(float, int)) {
int output = call_this(5.5, 7);
return output;
}
int final_result = call_a_func(&do_math);
套用 typedef 之後, typedef 本身易讀而且 call_a_func 的參數部份 (修改後的第7行), 也變得簡單易讀.
typedef int (*MathFunc)(float, int);
int do_math(float arg1, int arg2) {
return arg2;
}
int call_a_func(MathFunc call_this) {
int output = call_this(5.5, 7);
return output;
}
int final_result = call_a_func(&do_math);
再來一個更極端的例子 (也是借用自 Wiki 網站對 typedef 的解說)
void (*signal(int sig, void (*func)(int)))(int);
// 轉換成下面的樣子
typedef void (*sighandler_t)(int);
sighandler_t signal(int sig, sighandler_t func);
雜記
- 習慣上, C 語言 (如: standard C library, POSIX) 會在衍生性型別名的後面加上 _t, 像是 size_t.
- 定義或宣告變數時, 新設的型別不可以和 signed, unsigned 一起合用 (即便是 原始型別是 int, short, long... 之類的型別). 理由很簡單 signed int 和 unsigned int 是分別的基本資料型態, 意即 signed 和 unsigned 這二個 keyword 並不是 int 的 storage class 或者是 qualifier 之類的修飾 keyword.
參考連結
- en.wikipedia.org typedef
- blog.sina.com.cn typedef的四个用途和两大陷阱
- pixnet.net/blog C 語言:輕鬆讀懂複雜的定義 (Define and Read the complex declarations)
- Declaring, Assigning, and Using Function Pointers
留言列表