C語言的高級技巧

c語言是一門古老的語言,可以看下下面的C語言的介紹:

1969-1973年在美國電話電報公司(AT&T)貝爾實驗室開始了C語言的最初研發。根據C語言的發明者丹尼斯·裏奇 (Dennis Ritchie) 說,C 語言最重要的研發時期是在1972年。
說明:丹尼斯·裏奇(Dennis Ritchie),C語言之父,UNIX之父。1978年與布萊恩·科爾尼幹(Brian Kernighan)一起出版了名著《C程序設計語言(The C Programming Language)》,現在此書已翻譯成多種語言,成爲C語言方面最權威的教材之一。2011年10月12日(北京時間爲10月13日),丹尼斯·裏奇去世,享年70歲。
C語言之所以命名爲C,是因爲C語言源自Ken Thompson發明的 B語言,而B語言則源自BCPL語言。
C語言的誕生是和UNIX操作系統的開發密不可分的,原先的UNIX操作系統都是用匯編語言寫的,1973年UNIX操作系統的核心用C語言改寫,從此以後,C語言成爲編寫操作系統的主要語言。

C語言既簡單又複雜,說它簡單是因爲它的關鍵字少,語法規則簡單,看看就可以編寫個hello,world程序;說它複雜是因爲它接近於底層,有指針,可以直接操作內存,由此引起的一堆麻煩事情調試起來有非常的複雜,而且沒有豐富的庫的支持,很多東西都要自己手寫,比較麻煩。

C語言的另外複雜點在於,你也許對C的語法早就瞭然於胸,但是仍然對開源的庫代碼閱讀起來非常喫力,除了算法和數據結構複雜之外,C語言還有自己的奇技淫巧,常在開源的代碼中應用,但是卻很少有書去總結,這個文章算是對C的常用技巧做一個總結吧,資料來源於網上和自己看代碼的一些體會。

一 編譯器判斷優化技巧

#define likely(x)       __builtin_expect(!!(x),1)
#define unlikely(x)     __builtin_expect(!!(x),0)

likely這個宏的期望x是非0,是在絕大多數情況下,x都是非0,比如我們在內存申請後判斷指針是否爲空可以用,而unlikely正好相反,是絕大多數情況下,x爲空時候使用。

char * p =(char*)malloc(sizeof(int));
if (likely(p)) {
  do_something();
}

引入這兩個宏,可以增加條件判斷的分支預測準確性,cpu會提前裝載後面的指令。在彙編級別的表現是預測大多數可能發生的條件是順序的指令,而少數可能發生的情況是跳轉指令,順序指令在執行的時候可以利用CPU的緩存優勢。
極端情況下,性能可以提升30%左右。

二 定長類型

在Java這種語言中,byte是8個位一個字節,short是16位,2個字節,int是32位,4個字節這些都是確定的。C語言中,經常出現同一個類型在不同的平臺的字節長度是不一樣的,比如long在32位系統中爲4個字節,在64位系統中爲8個字節。這就給我們編寫跨平臺的系統產生了麻煩,
爲了跨平臺,很多系統定義了自己的一套類型。stdint.h頭文件到了確定大小的類型,比如:

1字節     uint8_t  
2字節     uint16_t  
4字節     uint32_t  
8字節     uint64_t

編程中,我們應該多使用這些類型,少使用int,long等。

三 利用宏實現日誌功能

日誌功能很常用,但是我們如何獲取打印日誌的位置和行號那,我特意和同事討論了下,在Java中這個功能是通過定義異常來實現的,那麼在C中如何實現,廢話不說,看下代碼吧:

#define LOG_PRINTF(pres, fmt, ...)                                                  \
log_printf(pres "[%s:%s:%d]" fmt "\n", __FILE__, __func__, __LINE__, ##__VA_ARGS__)

#define log_info(logLevel,fmt,...)  do { \
        if (INFO_LEVEL >= logLevel) {\
                 LOG_PRINTF_S("[INFO]", fmt, ##__VA_ARGS__);\
            }\
        }while (0);

void log_printf(const char * fmt, ...) {
    va_list ap;
    // 每條日誌大小, 按照系統緩衝區走
    char str[BUFSIZ];
    int len = times_fmt("["STR_TIMES"]", str, sizeof str);
    time_t now_time = time(NULL);
    //每到新的一天切換日誌
    if (!time_day(now_time,last_time)) {
        log_init(g_app_conf.app_conf.common_conf_data.log_file_name);
    }
    // 日誌內容填充
    va_start(ap, fmt);
    vsnprintf(str + len, sizeof str - len, fmt, ap);
    va_end(ap);

    // 數據寫入到文件中
    fputs(str, log_file);
    if (is_write_to_console == 1) {
        fputs(str, stderr);
    }
}

說明:
1) FILE表示正在運行程序文件名,func正在執行的函數,LINE是程序文件中的行號,這些是C的編譯器內置的宏。
2) 對於#宏解釋下,#用在預編譯語句裏面可以把預編譯函數的變量直接格式成字符串。
比如:

#define my_printf(x) printf(#x" is %d\n", x)
int a = 100;
my_printf(a);
/*打印a is 100*/

可以用在switch語句中,將enum轉成相關的字符串,非常方便:

#define CASE_CODE(E)  case E: return #E
const char * PacketProfileDetectIdToString(PacketProfileDetectId id)
{
    switch (id) {
        CASE_CODE (PROF_DETECT_SETUP);
        CASE_CODE (PROF_DETECT_GETSGH);
        CASE_CODE (PROF_DETECT_IPONLY);
        default:
            return "UNKNOWN";
    }
}

3) 對於##宏解釋:

/* ## 是變量連接符,將兩個字符連接成一個變量 */
#define FUN(a) printf("The square of " #a " is %d.\n",b##a)  
int bm = 2
FUN(m)

/* 打印 The square of m is 2.*/

4) VA_ARGS是可變參數宏,表示可變參數列表。

 #define Debug(...) printf(__VA_ARGS__)

 Debug("Y = %d\n", y);
/*自動替換成:
printf("Y = %d\n", y);
*/

5) 對於 ##__VA_ARGS__宏 作用是如果最後的可變變量爲空忽略後面的逗號。
6) vsnprintf 是按照特定格式將可變參數打印到字符串中,方便後面的輸出。

順便說一下,宏在C語言中真是離不開,雖然很多書都推薦不要用宏,因爲不便調試,但是宏可以簡化代碼,提高效率,使用範圍是相當的廣。

四 變長結構體

在C語言中,本身是不支持動態數組的,但是有些技巧可以實現類似動態數組的效果,一般人可能這樣定義:

typedef struct Arry {
  int arry_len;
  char * content;
} * Arry;
//申請內存
Arry*p_ptr = (Arry*)malloc(sizeof(parry));
p_ptr ->content = (char *)malloc(100);
//釋放內存
free(p_ptr ->content);
free(p_ptr);

這裏面我們注意到這個這個結構體有兩個變量,一個是int,一個是char ,char 裏面保存的是申請內存的指針,如果用sizeof去求的話,會發現整個結構體的大小爲4+4 = 8。
p_ptr 指針和p_ptr ->content 指針指向的內存沒有任何關係。

變長結構體定義如下:

typedef struct
{
    int a;
    char b[0];
} * DArray;
//申請內存 100
DArray p_darray = (DArray)malloc(sizeof(*DArray)+100);
//釋放內存
free(p_darray);

相比上面的定義好處如下:
1)只需要申請和釋放內存一次即可。
2)內存分配是連續的,可以減少內存碎片。
3)節省內存,sizeof(*DArray) = = 4.
4) 可以方便用來做socket數據包傳輸,解析數據,等。

五 求數組和枚舉的小技巧

對於一些情況,我們需要用到數組成員的個數和枚舉的大小,一般對數組求大小可以這樣做:
sizeof(array)/sizeof(array[0])得到;對於枚舉類型,我們可以在最後定義一個最大宏,標識宏的結束:

enum { ONE,TWO,THREE,MAX};

我們在循環的時候就可以用MAX,以後添加變量在MAX前面添加即可,相關代碼不用改變。

六 宏定義的小技巧

#define EXAMPLE do{
   xxxx \
   xxxxx \
 }while(0);
//這樣好處把宏定義封裝起來,後面多加分號不容易出錯。

七 NULL 判斷顛倒處理

我們判斷NULL指針的時候,一般用if (p == NULL),但是這種寫法,有可能少寫一個=號,而程序不報錯,可以改成 if (NULL == p) 這樣寫之後如果少寫一個=號,程序顯然會報錯的。

八 其他提示

C語言的告警一定要多注意,儘量讓代碼零告警,不僅僅看起來清爽,還可以避免不少難查的Bug,還有一點就是程序寫好之後,用valgrind --tool=memcheck --leak-check=full 運行檢查,
看看是否有內存泄漏,還有就是invaild 讀和寫,這往往是程序運行core的根源,切記切記!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章