真正的C程序要從可移植性開始

注意以寫的文章,全部理論都是基於類Unix系統的C程序編寫。linux

##平臺可移植性

好的程序即便切換到任意操做系統平臺並使用任意的編譯器編譯,也不該該出現太多的警告,更不能出現編譯不經過的狀況了。編程

  • 要編寫可移植性的代碼,首先要知道當前編譯的系統是什麼系統,通常都是經過編譯器內置的預編譯宏進行確認,以下:
//平臺檢測
#if !defined(__LINUX__) && (defined(__linux__) || defined(__KERNEL__) \
	|| defined(_LINUX) || defined(LINUX) || defined(__linux))
  #define  __LINUX__    (1)
#elif !defined(__APPLE__) && (defined(__MacOS__) || defined(__apple__))
  #define  __APPLE__    (1)
#elif !defined(__CYGWIN__) && (defined(__CYGWIN32__) || defined(CYGWIN))
  #define  __CYGWIN__   (1)
#elif !defined(__WINDOWS__) && (defined(_WIN32) || defined(WIN32) \
	|| defined(_window_) || defined(_WIN64) || defined(WIN64))
  #define __WINDOWS__   (1)
#elif !(defined(__LINUX__) || defined(__APPLE__) \
  || defined(__CYGWIN__) || defined(__WINDOWS__))
  #error "`not support this platform`"
#endif
  • 肯定平臺後,定義一個宏。而後使用這個宏去進行預編譯處理,好比只有linux支持<sys/epoll.h>這個頭文件,以下:
#ifdef __LINUX__
  #include <sys/epoll.h>
#endif

##編譯器可移植性

  • 除了平臺會關聯移植性,編譯器也會關聯,好比類Unix系統通常都使用gcc編譯器,而該編譯器提供了不少特性,首先肯定編譯器類型,以下:
//編譯器版本
/* gcc version. for example : v4.1.2 is 40102, v3.4.6 is 30406 */
#define GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__)
  • 編譯器特性多線程

    本人不是很厲害,如今僅僅使用下面兩個頗有用的特性。app

    • 特性一:有的時候咱們須要知道printf()類函數輸入的參數是否合法,這樣能夠在編譯時就發出警告,而不至於由於格式錯誤發生運行時錯誤,那麼可使用以下編譯器提供的特性:
    //字符串格式化參數檢查
    #if !defined(__printflike) && defined(GCC_VERSION)
        #define __printflike(fmtarg, firstvararg) __attribute__((__format__(__printf__, fmtarg, firstvararg)))
    #endif
    • 特性二:因爲cpu在處理指令時,能夠讀取多條,特別是在處理條件跳轉指令時,若是能預判斷條件發送的機率,那麼就能夠提升處理指令的速度(這個原理我只只知其一;不知其二,因此不能詳細說出原理,瞭解到讀者請指教),以下:
      //邏輯跳轉優化
      #if GCC_VERSION
      /*條件大多數爲真,與if配合使用,直接執行if中語句*/
          #define likely(x)     __builtin_expect(!!(x), 1)
      /*條件大多數爲假,與if配合使用,直接執行else中語句*/
          #define unlikely(x)   __builtin_expect(!!(x), 0)
      #else
          #define likely(x)     (!!(x))
          #define unlikely(x)   (!!(x))
      #endif
  • 使用編譯器特性進行編程函數

    • 定義一個格式化日誌輸出函數,請類比printf()的使用方式;在函數聲明後跟__printflike(6,7) 表面咱們要檢查類格式化函數的格式字符串從第六個參數開始,而被格式化的變參從第七個參數開始,以下:

    之後我討論個人這套日誌輸出系統,這裏的聲明中函數有些不一樣,之後我會說明。優化

    // 寫日誌
    // @param cfg 日誌配置數據
    // @param level 當前須要打印的日誌級別
    // @param func 該條日誌的輸出所在函數名
    // @param file 該條日誌的輸出所在文件名
    // @param line 該條日誌的輸出所在文件行
    // @param formate 該條日誌輸出的格式
    // @param ... 依據格式輸出的邊參列表
    // @return 0 成功; -1 失敗

int (SLogWrite)(SLogCfgT *cfg, const SLogLevelT *level, const char *func, const char *file, int line, const char *formate, ...) __printflike(6, 7); //正確的使用方式 ... /*默認日誌級別定義表/ extern const SLogLevelT g_slogLevels[]; /*默認日誌配置/ extern SLogCfgT g_slogCfg; (SLogWrite)(&g_slogCfg, &g_slogLevels[1], func, file, line, "%s", "test macro of __printflike(x, y)"); //錯誤的使用方式 (SLogWrite)(&g_slogCfg, &g_slogLevels[1], func, file, line, "%d", "test macro of __printflike(x, y)");ui

第一條調用不會發生警告,而第二條會發生警告,本來是但願將變參看成整數處理,但是卻傳入了一個字符串,這會產生一條編譯警告,而警告可能會使程序運行產生未知行爲,擔任有時候也不會出什麼問題,但有時會使程序蹦掉,因此不能存在僥倖心理。

- 條件預判斷。幾乎全部的C程序都須要檢查變量的合法性,好比咱們定義了一個結構體,可是可能忘記初始化結構體了,若是這樣交給函數處理,可能發送未知行爲,而致使錯誤的結果,甚至發生程序蹦掉。以下:
```c
...
struct T {
    void *data;
    ...
};
void deal(struct T *ptr) {
  int *data = (int*)ptr->data;
  printf("%d\n", *data);
}
struct T data;
deal(&data);
...

若是未初始化的data.data指向的是一個不可讀寫的指針地址,那麼在deal()中解引用必定會使程序引起段錯誤而蹦掉。下面咱們定義一系列宏,進行判斷,可是發生錯誤的狀況是甚少的,因此爲效率考慮,咱們在宏中使用likely()條件預判斷:this

//魔數
// 結構體中設置一個magic的成員變量,以檢查結構體是否被正確初始化
#if !defined(OBJMAGIC)
#define OBJMAGIC (0xfedcba98)
//設置魔術
#define REFOBJ(obj)				 \
	({					 \
		bool _ret = false;		 \
		if (likely((obj))) {		 \
			(obj)->magic = OBJMAGIC; \
			_ret = true;		 \
		}				 \
		_ret;				 \
	})
//重置魔數
#define UNREFOBJ(obj)			     \
	({				     \
		bool _ret = false;	     \
		if (likely((obj) &&	     \
		(obj)->magic == OBJMAGIC)) { \
			(obj)->magic = 0;    \
			_ret = true;	     \
		}			     \
		_ret;			     \
	})
//驗證魔數
#define ISOBJ(obj) \
	(likely((obj) && (obj)->magic == OBJMAGIC))
//斷言魔數
#define ASSERTOBJ(obj) \
	(assert(ISOBJ((obj))))
#endif	/* if !defined(OBJMAGIC) */

使用以下:操作系統

struct T {
    int magic;
    void *data;
    ...
};
 void deal(struct T *ptr) {
    if(!ISOBJ(ptr)) return;
    int *data = (int*)ptr->data;
    printf("%d\n", *data);
}
void initobj(struct T *ptr, void *data) {
    if(!REFOBJ(&data) || !data) return;
    ptr->data = data;
}
...
struct T data;
int value = 1;
initobj(&data, (void*)&value);
//下個初始化不會再次初始化結構體
initobj(&data, (void*)&value);
deal(&data);

雖然每一個結構體必需要保留一個magic這個字段,會佔用一些時間和空間,但這樣作是值得的,至少你能夠少些不少代碼來證實這個結構體被正確的初始化過,能夠放心的使用。你應該保證你的初始化函數是預期的執行過。固然你也可使用ASSERTOBJ ()判斷結構體,在發現未初始化後,立馬abort()程序,而後根據日誌糾正錯誤,這至少比莫名的段錯誤要好不少。C就是這樣,你要作不少工做來保證你的程序的正確性,這個是你的職責所在,不能嫌麻煩。關鍵是你要以何種靈活簡單高效的方法來作這個事情。線程

##接下來討論什麼

在多核cpu流行的今天,多線程編程是必不可少的技能,而在原子操做是多線程裏佔很重要的地位,下一節咱們將討論原子操做的運行,並用原子操做實現一個比較高級的同步鎖,該鎖能很好的檢查死鎖,並將其修復,並且速度比起POSIX的互斥量快不少,而切比起自旋鎖也多了檢查死鎖和能夠遞歸操做的特性。

相關文章
相關標籤/搜索