鍍金池/ 教程/ C/ 0x06-C語(yǔ)言預(yù)處理器
0x0E-單線程備份(下)
0x11-套接字編程-1
0x05-C語(yǔ)言指針:(Volume-1)
0x13-套接字編程-HTTP服務(wù)器(1)
0x0C-開(kāi)始行動(dòng)
C 語(yǔ)言進(jìn)階
第一部分
0x05-C語(yǔ)言指針(Volume-2)
0x08-C語(yǔ)言效率(下)
0x07-C語(yǔ)言效率(上)
0x04 C代碼規(guī)范
0x0F-多線程備份
0x05-C語(yǔ)言變量
第四部分
0x16-套接字編程-HTTP服務(wù)器(4)
0x0D-單線程備份(上)
總結(jié)
0x01-C語(yǔ)言序言
0x15-套接字編程-HTTP服務(wù)器(3)
0x14-套接字編程-HTTP服務(wù)器(2)
0x17-套接字編程-HTTP服務(wù)器(5)
第三部分
我的C語(yǔ)言
0x06-C語(yǔ)言預(yù)處理器
0x09-未曾領(lǐng)略的新風(fēng)景
0x0A-C線程和Glib的視角
第二部分
0x10-網(wǎng)絡(luò)的世界
0x12-套接字編程-2
0x03-C代碼
0x0B-C語(yǔ)言錯(cuò)誤處理

0x06-C語(yǔ)言預(yù)處理器

0x06-C語(yǔ)言預(yù)處理器

預(yù)處理最大的標(biāo)志便是大寫(xiě),雖然這不是標(biāo)準(zhǔn),但請(qǐng)你在使用的時(shí)候大寫(xiě),為了自己,也為了后人。

預(yù)處理器在一般看來(lái),用得最多的還是宏,這里總結(jié)一下預(yù)處理器的用法。

#include <stdio.h>
#define MACRO_OF_MINE
#ifdef MACRO_OF_MINE
#else
#endif

上述五個(gè)預(yù)處理是最??匆?jiàn)的,第一個(gè)代表著包含一個(gè)頭文件,可以理解為沒(méi)有它很多功能都無(wú)法使用,例如C語(yǔ)言并沒(méi)有把輸入輸入納入標(biāo)準(zhǔn)當(dāng)中,而是使用庫(kù)函數(shù)來(lái)提供,所以只有包含了stdio.h這個(gè)頭文件,我們才能使用那些輸入輸出函數(shù)。 #define則是使用頻率第二高的預(yù)處理機(jī)制,廣泛用在常量的定義,只不過(guò)它和const聲明的常量有所區(qū)別:

#define MAR_VA 100
const int Con_va = 100;
...
/*定義兩個(gè)數(shù)組*/
...
for(int i = 0;i < 10;++i)
{
    mar_arr[i] = MAR_VA;
    con_arr[i] = Con_va;
}
  • 區(qū)別1,定義上MAR_VA可以用于數(shù)組維數(shù),而Con_va則不行
  • 區(qū)別2,在使用時(shí),MAR_VA的原理是在文中找到所有使用本身的地方,用值替代,也就是說(shuō)Con_va將只有一分真跡,而MAR_VA則會(huì)有n份真跡(n為使用的次數(shù)) 剩下三個(gè)則是在保護(hù)頭文件中使用頗多。

幾個(gè)比較實(shí)用的用于調(diào)試的宏,由C語(yǔ)言自帶

  • __LINE__和__FILE__ 用于顯示當(dāng)前行號(hào)和當(dāng)前文件名
  • __DATE__和__TIME__ 用于顯示當(dāng)前的日期和時(shí)間
  • __func__(C99) 用于顯示當(dāng)前所在外層函數(shù)的名字

上述所說(shuō)的五種宏直接當(dāng)成值來(lái)使用即可。

  • __STDC__

    • 如果你想檢驗(yàn)?zāi)悻F(xiàn)在使用的編譯器是否遵循ISO標(biāo)準(zhǔn),用它,如果是他的值為1。

      printf("%d\n", __STDC__);

      輸出: 1

    • 如果你想進(jìn)一步確定編譯器使用的標(biāo)準(zhǔn)版本是C99還是C89可以使用__STDC__VERSION__,C99(199901)

      printf("%d\n", __STDC_VERSION__);

      輸出: 199901

可能很多人對(duì)這些宏沒(méi)什么感觸,實(shí)際上一般的確是用不到,但是:

當(dāng)你在寫(xiě)一些隱晦的東西時(shí) volatile int x = 10;

你試試把這個(gè)代碼用 -std=c99 編譯一下,如果不出意外應(yīng)該是出錯(cuò)的

在 ISO 標(biāo)準(zhǔn)里,volatile是用__volatile__來(lái)實(shí)現(xiàn)的,這個(gè)對(duì)GCC,Clang,Visual C++而言都是如此 除此之外還有許多,有待你們自己發(fā)掘。

對(duì)于#define
  1. 預(yù)處理器一般只對(duì)同一行定義有效,但如果加上反斜杠,也能一直讀取下去

      #define err(flag) \
          if(flag) \
            printf("Correctly")

    可以看出來(lái),并沒(méi)有在末尾添加;,并不是因?yàn)楹瓴恍枰?,而是因?yàn)?,我們總是將宏近似?dāng)成函數(shù)在使用,而函數(shù)調(diào)用之后總是需要以;結(jié)尾,為了不造成混亂,于是在宏定義中我們默認(rèn)不添加;,而在代碼源文件中使用,防止定義混亂。

  2. 預(yù)處理同樣能夠帶來(lái)一些便利

       #define SWAP1(a, b) (a += b, b = a - b, a -= b)
       #define SWAP2(x, y) {x ^= y; y ^= x; x ^= y}

    引用之前的例子,交換兩數(shù)的宏寫(xiě)法可以有效避免函數(shù)開(kāi)銷(xiāo),由于其是直接在調(diào)用處展開(kāi)代碼塊,故其比擬直接嵌入的代碼。但,偶爾還是會(huì)出現(xiàn)一些不和諧的錯(cuò)誤,對(duì)于初學(xué)者來(lái)說(shuō):

      int v1 = 10;
      int v2 = 20;
      SWAP1(v1, v2);
      SWAP2(v1, v2);//報(bào)錯(cuò)

    對(duì)于上述代碼塊的情況,為什么SWAP2報(bào)錯(cuò)?對(duì)于一般的初學(xué)者來(lái)說(shuō),經(jīng)常忽略諸如, goto do...while等少見(jiàn)關(guān)鍵字用法,故很少見(jiàn)SWAP1的寫(xiě)法,大多集中于SWAP2的類似錯(cuò)誤,錯(cuò)就錯(cuò)在{}代表的是一個(gè)代碼塊,不需要使用;來(lái)進(jìn)行結(jié)尾,這便是宏最容易出錯(cuò)的地方 宏只是簡(jiǎn)單的將代碼展開(kāi),而不會(huì)做任何處理 對(duì)于此,即便是老手也常有失足,有一種應(yīng)用于單片機(jī)等地方的C語(yǔ)言寫(xiě)法可以在此借鑒用于保護(hù)代碼:

      #define SWAP3(x ,y) do{ \
              x ^= y; y ^= x; x ^= y; \       
              }while(0)

    如此便能在代碼中安全使用花括號(hào)內(nèi)的代碼了,并且如之前所約定的那樣,讓宏的使用看起來(lái)像函數(shù)。

  3. 但正所謂,假的總是假的,即使宏多么像函數(shù),它依舊不是函數(shù),如果真的把它當(dāng)成函數(shù),你會(huì)在某些時(shí)候錯(cuò)的摸不著頭腦,還是一個(gè)經(jīng)典的例子,比較大小:

      #define CMP(x, y) (x > y ? x : y)
      ...
      int x = 100, y = 200;
      int result = CMP(x, y++);
      printf("x = %d, y = %d, result = %d\n", x, y, result);

    執(zhí)行這部分代碼,會(huì)輸出什么呢? 答案是,不知道!至少result的值我們無(wú)法確定,我們將代碼展開(kāi)得到

      int result = (x > y++ ? x : y++);

    看起來(lái)似乎就是y遞增兩次,最后result肯定是200。真是如此?C語(yǔ)言標(biāo)準(zhǔn)對(duì)于一個(gè)確定的程序語(yǔ)句中,一個(gè)對(duì)象只能被修改一次,超過(guò)一次那么結(jié)果是未定的,由編譯器決定,除了三目操作符?:外,還有&&, ||或是,之中,或者函數(shù)參數(shù)調(diào)用,switch控制表達(dá)式,for里的控制語(yǔ)句 由此可看出,宏的使用也是有風(fēng)險(xiǎn)的,所以雖然宏強(qiáng)大,但是依舊不能濫用。

  4. 對(duì)于宏而言,前面說(shuō)過(guò),它只是進(jìn)行簡(jiǎn)單的展開(kāi),這有時(shí)候也會(huì)帶來(lái)一些問(wèn)題:

      #define MULTI(x, y) (x * y)
      ...
      int x = 100, y = 200;
      int result = MULTI(x+y, y);

    看出來(lái)問(wèn)題了吧?展開(kāi)之后會(huì)變成: int result = x+y * y; 完全違背了當(dāng)初我們?cè)O(shè)計(jì)時(shí)的想法,一個(gè)比較好的修改方法是對(duì)每個(gè)參數(shù)加上括號(hào): #define MULTI(x, y) ((x) * (y))如此,展開(kāi)以后:

      int result = ((x+y) * (y));

    這樣能在很大程度上解決一部分問(wèn)題。

  5. 如果對(duì)自己的宏十分自信,可以嵌套宏,即一個(gè)表達(dá)式中使用宏作為宏的參數(shù),但是宏只展開(kāi)這一級(jí)的宏,對(duì)于多級(jí)宏另有辦法展開(kāi)

      int result = MULTI(MULTI(x, y), y);

    展開(kāi)成:int result = ((((x) * (y))) * (y));

實(shí)際上,并不要太追求用宏去替換函數(shù),例如這個(gè)交換函數(shù),老老實(shí)實(shí)寫(xiě)函數(shù),有時(shí)候比宏更好

對(duì)宏的應(yīng)用
  1. 由于我們并不明白,在某些情況下宏是否被定義了,所以我們可以使用一些預(yù)處理保護(hù)機(jī)制來(lái)防止錯(cuò)誤發(fā)生

      #ifndef MY_MACRO
      #define MY_MACRO 10000
      #endif

    如果定義了MY_MACRO那就不執(zhí)行下面的語(yǔ)句,如果沒(méi)定義那就執(zhí)行。

  2. 在宏的使用中有兩個(gè)有用的操作符,姑且叫它操作符#, ##

    • 對(duì)于# 我們可以認(rèn)為#操作符的作用是將宏參數(shù)轉(zhuǎn)化為字符串。

          #define HCMP(x, y) printf(#x" is equal to" #y" ? %d\n", (x) == (y))
          ...
          int x = 100, y = 200;
          HCMP(x, y);

      展開(kāi)以后

          printf("x is equal to y ? %d\n", (100) == (200));
      • 注:可以自行添加編譯器選項(xiàng),來(lái)查看宏展開(kāi)之后的代碼,具體可以查詢GCC的展開(kāi)選項(xiàng),這里不再詳述。特別是在多層宏的嵌套使用情況下,但是我不太推薦,故不做多介紹。

        • 能說(shuō)的就是如何正確的處理一些嵌套使用,之所以不愿意多說(shuō)也不愿意多用,是因?yàn)?strong>C預(yù)處理器就是一個(gè)奇葩
        • 舉一個(gè)典型的例子,__LINE____FILE__的使用。

              /* 下方會(huì)說(shuō)到的 # 預(yù)處理指示器,這里先用,實(shí)在看不懂,可以自己動(dòng)手嘗試 */
              #define WHERE_AM_I #__LINE__ " lines in " __FILE__
              ...
              fputs(WHERE_AM_I, stderr);

          這樣能工作嗎?如果能我還講干嘛

              /* 常理上這應(yīng)該能工作,但是編譯器非說(shuō)這錯(cuò)那錯(cuò)的 */
              /* 好在有前人踏過(guò)了坑,為我們留下了解決方案 */
              #define DEPAKEGE(X) #X
              #define PAKEGE(X) DEPAKEGE(X)
              #define WHERE_AM_I PAKEGE(__LINE__) " lines in " __FILE__
              ...
              fputs(WHERE_AM_I, stderr);

          不要問(wèn)我為什么,因?yàn)槲乙膊恢繡預(yù)處理器的真正工作機(jī)制是什么。

          第一次看見(jiàn)這種解決方案是在 Windows 核心編程 中,這本書(shū)現(xiàn)在還能給我許多幫助,雖然已經(jīng)漸漸淡出了書(shū)架

          總結(jié)起來(lái),即將宏參數(shù)放于#操作符之后便由預(yù)處理器自動(dòng)轉(zhuǎn)換為字符串常量,轉(zhuǎn)義也由預(yù)處理器自動(dòng)完成,而不需要我們自行添加轉(zhuǎn)義符號(hào)。

  • 對(duì)于##
    它實(shí)現(xiàn)的是將本操作符兩邊的參數(shù)合并成為一個(gè)完整的標(biāo)記,但需要注意的是,由于預(yù)處理器只負(fù)責(zé)展開(kāi),所以程序員必須自己保證這種標(biāo)記的合法性,這里涉及到一些寫(xiě)法問(wèn)題,都列出來(lái)

          #define MERGE(x, y) have_define_ ## x + y
          #define MERGE(x, y) have_define_##x + y
          ...
          result = MERGE(1, 3);

    這里首先說(shuō)明,上述寫(xiě)法由于習(xí)慣原因,我使用第二種,但是無(wú)論哪種都無(wú)傷大雅,效果一樣。上述代碼展開(kāi)以后是什么呢?

          result = have_define_1 + 3;

    在我看來(lái),這就有點(diǎn)C++中模版的思想了,雖然十分原始,但是總是有了一個(gè)方向,憑借這種方法我們能夠使用宏來(lái)進(jìn)行相似卻不同函數(shù)的調(diào)用,雖然我們可以使用函數(shù)指針數(shù)組來(lái)存儲(chǔ),但需要提前知曉有幾個(gè)函數(shù),并且如果要實(shí)現(xiàn)動(dòng)態(tài)增長(zhǎng)還需要消耗內(nèi)存分配,但宏則不同。

          inline int func_0(int arg_1, int arg_2) { return arg_1 + arg_2; }
          inline int func_1(int arg_1, int arg_2) { return arg_1 - arg_2; }
          inline int func_2(int arg_1, int arg_2) { return arg_1 * arg_2; }
          inline int func_3(int arg_1, int arg_2) { return arg_1 / arg_2; }
          #define CALL(x, arg1, arg2) func_##x(arg1, arg2)
          ...
              printf("func_%d return %d\n",0 ,CALL(0, 2, 10));
              printf("func_%d return %d\n",1 ,CALL(1, 2, 10));
              printf("func_%d return %d\n",2 ,CALL(2, 2, 10));
              printf("func_%d return %d\n",3 ,CALL(3, 2, 10));

    十分簡(jiǎn)便的一種用法,在我們?cè)黾訙p少函數(shù)時(shí)我們不必考慮如何找到這些函數(shù)只需要記下每個(gè)函數(shù)對(duì)應(yīng)的編號(hào)即可,但還是那句話,不可濫用。

          #define CAT(temp, i) (cat##i)
          //...
          for(int i = 0;i < 5;++i)
          {
              int CAT(x,i) = i*i;
              printf("x%d = %d \n",i,CAT(x,i));
          }
    1. 對(duì)于宏,在使用時(shí)一定要注意,宏只能展開(kāi)當(dāng)前層的宏,如果你嵌套使用宏,即將宏當(dāng)作宏的參數(shù),那么將導(dǎo)致宏無(wú)法完全展開(kāi),即作為參數(shù)的宏只能傳遞名字給外部宏

          #define WHERE(value_name, line) #value_name #line
          ...
          puts(WHERE(x, __LINE__)); //x = 11

      輸出: 11__LINE__

  1. 對(duì)于其他的預(yù)編譯器指令,如:#pragma, #line, #error和各類條件編譯并不在此涉及,因?yàn)槭褂蒙喜⑽从邢葳寮半y點(diǎn)。

  2. C和C++混合編程的情況

    • 經(jīng)常能在源代碼中看見(jiàn) extern "C" 這樣的身影,這是做什么的?
    • 這是為了混合編程而設(shè)計(jì)的,常出現(xiàn)在 C++的源代碼中,目的是為了讓 C++能夠成功的調(diào)用 C 的標(biāo)準(zhǔn)或非標(biāo)準(zhǔn)函數(shù)。

          #if defined(__cplusplus) || defined(_cplusplus)
                  extern "C" {
          #endif
      
                  /**主體代碼**/
      
          #if defined(__cplusplus) || defined(_cplusplus)
                  }
          #endif

      這樣就能在C++中調(diào)用C的代碼了。

    • C 中調(diào)用 C++ 的函數(shù)需要注意,不能使用重載功能,否則會(huì)失敗,原因詳見(jiàn)C++對(duì)于重載函數(shù)的實(shí)現(xiàn)。也可以稱為 mangle
  3. 還有一種可以被稱之為宏的小應(yīng)用的技巧

    • 對(duì)于一個(gè)宏而言,是否有考慮過(guò)它的返回值是什么
    • 或者如何令其有一個(gè)函數(shù)那樣的功能
    • 其實(shí)很簡(jiǎn)單

          #define TEST_RET(val, continues) ({continues = 19;val = 11;})
          ...
          {
              __attribute__((unused)) int oldval = 10;
              __attribute__((unused)) int newval = 18;
              fprintf (stderr, "%d\n", TEST_RET(oldval, newval));
          }
    • 可以嘗試一下這個(gè)方法,其中原理自然就知道了。具體操作就是用({})包裹你想要的東西。

對(duì)宏的敬畏

  1. 為什么有這么一說(shuō),因?yàn)槭褂煤暾娴氖翘幪幬kU(xiǎn),而且代碼難以調(diào)試
  2. 經(jīng)常會(huì)遇到這種情況,你將代碼寫(xiě)成函數(shù)的時(shí)候沒(méi)有任何問(wèn)題,但是改成宏卻出現(xiàn)了問(wèn)題
    • 當(dāng)然更可能的是你一開(kāi)始就寫(xiě)宏,卻發(fā)現(xiàn)總是得不到到預(yù)期的結(jié)果!
  3. 不知道諸位對(duì)反轉(zhuǎn)鏈表這種知識(shí)點(diǎn)掌握的如何?
    1. 如果很有信心不妨挑戰(zhàn)一下下面的東西,看看是否能在我說(shuō)出原由之前意識(shí)到問(wèn)題
    2. 如果不太懂,那就跟著看下去,一定有收獲!

舉個(gè)例子最好說(shuō)明問(wèn)題

  • 假設(shè)要寫(xiě)一個(gè)雙向鏈表的插入操作
    • 我想要提供的是兩個(gè)功能,后方插入,前方插入
    • 我的設(shè)計(jì)原型是Linux內(nèi)核的鏈表原型。

所謂的Linux內(nèi)核的鏈表原型 就是在內(nèi)核編程中使用的鏈表數(shù)據(jù)結(jié)構(gòu),我以它為例子,自己寫(xiě)了一個(gè)插入操作

#define _list_add_inner(_add_pos, _add_node) \
do {\
    (_add_node)->next = (_add_pos)->next;\
    (_add_node)->prev = (_add_pos);\
    (_add_pos)->next->prev = (_add_node);\
    (_add_pos)->next = (_add_node);\
} while(0)

static inline void list_add_after(struct list * add_pos, struct list * add_node) {
    _list_add_inner(add_pos, add_node);
}

static inline void list_add_before(struct list * add_pos, struct list * add_node) {
    _list_add_inner(add_pos->prev, add_node);
}
  • 很好,可以試著測(cè)試一下最后這兩個(gè)函數(shù)list_add_after,list_add_before看看是否達(dá)到預(yù)期目的?

有時(shí)候代碼真的就是要測(cè)試才行

  • 不啰嗦,這樣是不行的!
    • 為何?問(wèn)題就出在list_add_before這個(gè)函數(shù)的add_pos->prev參數(shù)上,原因就是宏只是做一個(gè)簡(jiǎn)單的替換,而不是值代入
    • 這里需要自己體會(huì)一下。修正一下代碼

替換和值代入可是大不相同的

#define _list_add_inner(_add_pos, _add_node) \
do {\
    struct list * tmp = _add_pos;\
    (_add_node)->next = tmp->next;\
    (_add_node)->prev = tmp;\
    tmp->next->prev = (_add_node);\
    tmp->next = (_add_node);
} while(0)
  • 不知是否看出了什么門(mén)道,這就是關(guān)鍵所在,構(gòu)造一個(gè)值,而不是簡(jiǎn)單的替換。可以自己動(dòng)手畫(huà)一畫(huà)流程圖。