天天看點

嵌入式 C 語言的5個炫酷技巧,誰還沒用過?

作者:西安華清遠見

C 語言常常讓人覺得它所能表達的東西非常有限。它不具有類似第一級函數和模式比對這樣的進階功能。但是C非常簡單,并且仍然有一些非常有用的文法技巧和功能,隻是沒有多少人知道罷了。

今天就和小華一起來了解幾個炫酷的技巧吧~

1.指定的初始化

很多人都知道像這樣來靜态地初始化數組:

int fibs[] = {1, 1, 2, 3, 5};           

C99标準實際上支援一種更為直覺簡單的方式來初始化各種不同的集合類資料(如:結構體,聯合體和數組)。

2.數組

我們可以指定數組的元素來進行初始化。這非常有用,特别是當我們需要根據一組#define來保持某種映射關系的同步更新時。來看看一組錯誤碼的定義,如:

/* Entries may not correspond to actual numbers. Some entries omitted. */

#define EINVAL 1

#define ENOMEM 2

#define EFAULT 3

/* ... */

#define E2BIG 7

#define EBUSY 8

/* ... */

#define ECHILD 12

/* ... */           

現在,假設我們想為每個錯誤碼提供一個錯誤描述的字元串。為了確定數組保持了最新的定義,無論頭檔案做了任何修改或增補,我們都可以用這個數組指定的文法。

char *err_strings[] = {
[0] = "Success",
[EINVAL] = "Invalid argument",
[ENOMEM] = "Not enough memory",
[EFAULT] = "Bad address",
/* ... */
[E2BIG ] = "Argument list too long",
[EBUSY ] = "Device or resource busy",
/* ... */
[ECHILD] = "No child processes"
/* ... */
};           

這樣就可以靜态配置設定足夠的空間,且保證最大的索引是合法的,同時将特殊的索引初始化為指定的值,并将剩下的索引初始化為0。

3.結構體與聯合體

用結構體與聯合體的字段名稱來初始化資料是非常有用的。假設我們定義:

struct point {
int x;
int y;
int z;
}           

然後我們這樣初始化struct point:

struct point p = {.x = 3, .y = 4, .z = 5};           

當我們不想将所有字段都初始化為0時,這種作法可以很容易的在編譯時就生成結構體,而不需要專門調用一個初始化函數。

對聯合體來說,我們可以使用相同的辦法,隻是我們隻用初始化一個字段。

4.宏清單

C中的一個慣用方法,是說有一個已命名的實體清單,需要為它們中的每一個建立函數,将它們中的每一個初始化,并在不同的代碼子產品中擴充它們的名字。這在Mozilla的源碼中經常用到,我就是在那時學到這個技巧的。例如,在我去年夏天工作的那個項目中,我們有一個針對每個指令進行标記的宏清單。其工 作方式如下:

#define FLAG_LIST(_) \
_(InWorklist) \
_(EmittedAtUses) \
_(LoopInvariant) \
_(Commutative) \
_(Movable) \
_(Lowered) \
_(Guard)           

它定義了一個FLAG_LIST宏,這個宏有一個參數稱之為 _ ,這個參數本身是一個宏,它能夠調用清單中的每個參數。舉一個實際使用的例子可能更能直覺地說明問題。假設我們定義了一個宏DEFINE_FLAG,如:

#define DEFINE_FLAG(flag) flag,
enum Flag {
None = 0,
FLAG_LIST(DEFINE_FLAG)
Total
};
#undef DEFINE_FLAG           

對FLAG_LIST(DEFINE_FLAG)做擴充能夠得到如下代碼:

enum Flag {
None = 0,
DEFINE_FLAG(InWorklist)
DEFINE_FLAG(EmittedAtUses)
DEFINE_FLAG(LoopInvariant)
DEFINE_FLAG(Commutative)
DEFINE_FLAG(Movable)
DEFINE_FLAG(Lowered)
DEFINE_FLAG(Guard)
Total
};           

接着,對每個參數都擴充DEFINE_FLAG宏,這樣我們就得到了enum如下:

enum Flag {
None = 0,
InWorklist,
EmittedAtUses,
LoopInvariant,
Commutative,
Movable,
Lowered,
Guard,
Total
};           

接着,我們可能要定義一些通路函數,這樣才能更好的使用flag清單:

#define FLAG_ACCESSOR(flag) \
bool is##flag() const {\
return hasFlags(1 << flag);\
}\
void set##flag() {\
JS_ASSERT(!hasFlags(1 << flag));\
setFlags(1 << flag);\
}\
void setNot##flag() {\
JS_ASSERT(hasFlags(1 << flag));\
removeFlags(1 << flag);\
}
FLAG_LIST(FLAG_ACCESSOR)
#undef FLAG_ACCESSOR           

一步步的展示其過程是非常有啟發性的,如果對它的使用還有不解,可以花一些時間在gcc –E上。

5.編譯時斷言

這其實是使用C語言的宏來實作的非常有“創意”的一個功能。有些時候,特别是在進行核心程式設計時,在編譯時就能夠進行條件檢查的斷言,而不是在運作時進行,這非常有用。不幸的是,C99标準還不支援任何編譯時的斷言。

但是,我們可以利用預處理來生成代碼,這些代碼隻有在某些條件成立時才會通過編譯(最好是那種不做實際功能的指令)。有各種各樣不同的方式都可以做到這一點,通常都是建立一個大小為負的數組或結構體。最常用的方式如下:

/* Force a compilation error if condition is false, but also produce a result
* (of value 0 and type size_t), so it can be used e.g. in a structure
* initializer (or wherever else comma expressions aren't permitted). */
/* Linux calls these BUILD_BUG_ON_ZERO/_NULL, which is rather misleading. */
#define STATIC_ZERO_ASSERT(condition) (sizeof(struct { int:-!(condition); }) )
#define STATIC_NULL_ASSERT(condition) ((void *)STATIC_ZERO_ASSERT(condition) )
/* Force a compilation error if condition is false */
#define STATIC_ASSERT(condition) ((void)STATIC_ZERO_ASSERT(condition))           

如果(condition)計算結果為一個非零值(即C中的真值),即! (condition)為零值,那麼代碼将能順利地編譯,并生成一個大小為零的結構體。如果(condition)結果為0(在C真為假),那麼在試圖生成一個負大小的結構體時,就會産生編譯錯誤。

它的使用非常簡單,如果任何某假設條件能夠靜态地檢查,那麼它就可以在編譯時斷言。例如,在上面提到的标志清單中,标志集合的類型為uint32_t,是以,我們可以做以下斷言:

STATIC_ASSERT(Total <= 32)           

它擴充為:

(void)sizeof(struct { int:-!(Total <= 32) })           

現在,假設Total<=32。那麼-!(Total <= 32)等于0,是以這行代碼相當于:

(void)sizeof(struct { int: 0 })           

這是一個合法的C代碼。現在假設标志不止32個,那麼-!(Total <= 32)等于-1,是以這時代碼就相當于:

(void)sizeof(struct { int: -1 } )           

因為位寬為負,是以可以确定,如果标志的數量超過了我們指派的空間,那麼編譯将會失敗。