C語言僅憑自學能到什麼高度?面試官:測測你的宏寫的怎麼樣先

C語言僅憑自學能到什麼高度?面試官:測測你的宏寫的怎麼樣先

  • 先來測測你現在的C語言水平怎麼樣…假如現在你去一家公司面試,
  • 要求:定義一個宏,求兩個數中的最大數。
  • 此處不要再往下看,停頓5分鐘,寫出你的答案,然後跟後面的答案對比。

----------------------------------停頓5分鐘------------------------------------

合格

對於學過C語言的同學,寫出這個宏基本上不是什麼難事,使用條件運算符就能完成:

#define  MAX(x,y)  x > y ? x : y

這是最基本的C語言語法,如果連這個也寫不出來,估計場面會比較尷尬。面試官爲了緩解尷尬,一般會對你說:小夥子,你很棒,回去等消息吧,有消息,我們會通知你!這時候,你應該明白:不用再等了,趕緊把這篇文章看完,接着面下家。這個宏能寫出來,也不要覺得你很牛X,因爲這隻能說明你有了C語言的基礎,但還有很大的進步空間。比如,我們寫一個程序,驗證一下我們定義的宏是否正確:

#define MAX(x,y) x > y ? x : y
int main(void)
{
    printf("max=%d",MAX(1,2));
    printf("max=%d",MAX(2,1));
    printf("max=%d",MAX(2,2));
    printf("max=%d",MAX(1!=1,1!=2));
    return 0;
}

測試程序麼,我們肯定要把各種可能出現的情況都測一遍。這不,測試第4行語句,當宏的參數是一個表達式,發現實際運行結果爲max=0,跟我們預期結果max=1不一樣。這是因爲,宏展開後,就變成了這個樣子:

printf("max=%d",1!=1>1!=2?1!=1:1!=2);

因爲比較運算符 > 的優先級爲6,大於 !=(優先級爲7),所以展開的表達式,運算順序發生了改變,結果就跟我們的預期不一樣了。爲了避免這種展開錯誤,我們可以給宏的參數加一個小括號()來防止展開後,表達式的運算順序發生變化。這樣的宏才能算一個合格的宏:

#define MAX(x,y) (x) > (y) ? (x) : (y)

中等

上面的宏,只能算合格,但還是存在漏洞。比如,我們使用下面的代碼測試:

#define MAX(x,y) (x) > (y) ? (x) : (y)
int main(void)
{
    printf("max=%d",3 + MAX(1,2));
    return 0;
}

在程序中,我們打印表達式 3 + MAX(1, 2) 的值,預期結果應該是5,但實際運行結果卻是1。我們展開後,發現同樣有問題:

3 + (1) > (2) ? (1) : (2);

因爲運算符 + 的優先級大於比較運算符 >,所以這個表達式就變爲4>2?1:2,最後結果爲1也就見怪不怪了。此時我們應該繼續修改這個宏:

#define MAX(x,y) ((x) > (y) ? (x) : (y))

使用小括號將宏定義包起來,這樣就避免了當一個表達式同時含有宏定義和其它高優先級運算符時,破壞整個表達式的運算順序。如果你能寫到這一步,說明你比前面那個面試合格的同學強,前面那個同學已經回去等消息了,我們接着面試下一輪。

良好

上面的宏,雖然解決了運算符優先級帶來的問題,但是仍存在一定的漏洞。比如,我們使用下面的測試程序來測試我們定義的宏:

#define MAX(x,y) ((x) > (y) ? (x) : (y))
int main(void)
{
    int i = 2;
    int j = 6;
    printf("max=%d",MAX(i++,j++));
    return 0;
}

在程序中,我們定義兩個變量 i 和 j,然後比較兩個變量的大小,並作自增運算。實際運行結果發現max = 7,而不是預期結果max = 6。這是因爲變量 i 和 j 在宏展開後,做了兩次自增運算,導致打印出 i 的值爲7。

遇到這種情況,那該怎麼辦呢? 這時候,語句表達式就該上場了。我們可以使用語句表達式來定義這個宏,在語句表達式中定義兩個臨時變量,分別來暫儲 i 和 j 的值,然後進行比較,這樣就避免了兩次自增、自減問題。

#define MAX(x,y)({     \
    int _x = x;        \
    int _y = y;        \
    _x > _y ? _x : _y; \
})
int main(void)
{
    int i = 2;
    int j = 6;
    printf("max=%d",MAX(i++,j++));
    return 0;
}

在語句表達式中,我們定義了2個局部變量_x、_y來存儲宏參數 x 和 y 的值,然後使用 _x 和 _y 來比較大小,這樣就避免了 i 和 j 帶來的2次自增運算問題。

你能堅持到了這一關,並寫出這樣自帶BGM的宏,面試官心裏可能已經有了給你offer的意願了。但此時此刻,千萬不要驕傲!爲了徹底打消面試官的心理顧慮,我們需要對這個宏繼續優化。

優秀

在上面這個宏中,我們定義的兩個臨時變量數據類型是int型,只能比較兩個整型的數據。那對於其它類型的數據,就需要重新再定義一個宏了,這樣太麻煩了!我們可以基於上面的宏繼續修改,讓它可以支持任意類型的數據比較大小:

#define MAX(type,x,y)({     \
    type _x = x;        \
    type _y = y;        \
    _x > _y ? _x : _y; \
})
int main(void)
{
    int i = 2;
    int j = 6;
    printf("max=%d\n",MAX(int,i++,j++));
    printf("max=%f\n",MAX(float,3.14,3.15));
    return 0;
}

在這個宏中,我們添加一個參數:type,用來指定臨時變量 _x 和 _y 的類型。這樣,我們在比較兩個數的大小時,只要將2個數據的類型作爲參數傳給宏,就可以比較任意類型的數據了。如果你能在面試中,寫出這樣的宏,面試官肯定會非常高興,他一般會跟你說:小夥子,稍等,待會HR會跟你談待遇問題。

還能不能更牛逼?

如果你想薪水拿得高一點,待遇好一點,此時不應該驕傲,你應該大手一揮:且慢,我還可以更牛逼!

上面的宏定義中,我們增加了一個type類型參數,來兼容不同的數據類型,此時此刻,爲了薪水,我們應該把這個也省去。如何做到?使用typeof就可以了,typeof是GNU C新增的一個關鍵字,用來獲取數據類型,我們不用傳參進去,讓typeof直接獲取!

#define max(x, y) ({    \
    typeof(x) _x = (x); \
    typeof(y) _y = (y); \
    (void) (&_x == &_y);\
    _x > _y ? _x : _y; })

在這個宏定義中,使用了typeof關鍵字用來獲取宏的兩個參數類型。乾貨在(void) (&x == &y);這句話,簡直是天才般的設計!一是用來給用戶提示一個警告,對於不同類型的指針比較,編譯器會給一個警告,提示兩種數據類型不同;二是,當兩個值比較,比較的結果沒有用到,有些編譯器可能會給出一個warning,加個(void)後,就可以消除這個警告!

此刻,面試官看到你的這個宏,估計會倒吸一口氣:乖乖,果然是後生可畏,這傢伙比我還牛逼!你等着,HR待會過來跟你談薪水!恭喜你,拿到offer了!

打造一個趨近完美的宏

以上的宏解決了自增自減運算符 ++/-- 帶來的一系列問題。但也不是十全十美,通過與 @左江 的激情討論,發現還是有漏洞:在宏內部的語句表達中,我們定義了2個臨時變量 _x 和 _y解決了 ++/-- 帶來的問題,但是也引入了一個新漏洞,比如當我們使用下面的代碼時:

max(x, _x)

當宏展開後,第二個參數就與宏內部定義的臨時變量同名了,這就影響宏最後的結果。因此,爲了防止用戶傳入的參數跟宏內部的臨時變量產生同名衝突,我們可以將宏內部的臨時變量儘量定義得複雜一些,降低同名的概率,比如Linux 內核中max宏的定義:

#define max(x, y) ({				\
	typeof(x) _max1 = (x);			\
	typeof(y) _max2 = (y);			\
	(void) (&_max1 == &_max2);		\
	_max1 > _max2 ? _max1 : _max2; })

在上面的宏定義中,雖然臨時變量 _max1 和 max2 比我們上面的 _x 和 _y 好點,也只是更進一步降低跟用戶的傳參同名衝突的概率,但是還是不能完全杜絕。極端一點,我們可以把這兩個變量定義得無比長、無比奇葩,只要不超過C標準規定以的標識符最大長度j就可以:

_____________tmp______________________for_______________________max______

再奇葩的程序員,再豬一樣的隊友,哪怕是團隊毒瘤、代碼殺手,估計也不會定義這樣的變量吧!這樣同名衝突的概率就大大降低了,但是還是不能完全杜絕,算是Linux內核的一個小漏洞吧。

還好,下載新版本的Linux內核,發現已經堵住了這個漏洞:

#define __max(t1, t2, max1, max2, x, y) ({              \
	t1 max1 = (x);                                  \
	t2 max2 = (y);                                  \
	(void) (&max1 == &max2);                        \
	max1 < max2 ? max1 : max2; })

#define ___PASTE(a,b) a##b
#define __PASTE(a,b) ___PASTE(a,b)

#define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__)

#define max(x, y)                                       \
	__max(typeof(x), typeof(y),                     \
	      __UNIQUE_ID(max1_), __UNIQUE_ID(max2_),   \
	      x, y)

在新版的宏中,內部的臨時變量不再由程序員自己定義,而是讓編譯器生成一個獨一無二的變量,這樣就避免了同名衝突的風險。宏__UNIQUE_ID的作用就是生成了一個獨一無二的變量,確保了臨時變量的唯一性。

是不是已經完美了?

新版本Linux內核堵住了臨時變量可能帶來的同名衝突的漏洞,但是是不是就完美了呢?還是不一定!針對Linux內核中宏的新版本,最近又引發各種爭論,比如針對常量、變長數組問題等。

#define __single_eval_max(t1, t2, max1, max2, x, y) ({	\
 	t1 max1 = (x);					\
 	t2 max2 = (y);					\
 	(void) (&max1 == &max2);			\
 	max1 > max2 ? max1 : max2; })

    #define __max(t1, t2, x, y)						\
	__builtin_choose_expr(__builtin_constant_p(x) &&		\
			      __builtin_constant_p(y),			\
			      (t1)(x) > (t2)(y) ? (t1)(x) : (t2)(y),	\
			      __single_eval_max(t1, t2,			\
						__UNIQUE_ID(max1_),	\
						__UNIQUE_ID(max2_),	\
						x, y))

    #define max(x, y)	__max(typeof(x), typeof(y), x, y)The joy of max()   #define __single_eval_max(t1, t2, max1, max2, x, y) ({	\
 	t1 max1 = (x);					\
 	t2 max2 = (y);					\
 	(void) (&max1 == &max2);			\
 	max1 > max2 ? max1 : max2; })

    #define __max(t1, t2, x, y)						\
	__builtin_choose_expr(__builtin_constant_p(x) &&		\
			      __builtin_constant_p(y),			\
			      (t1)(x) > (t2)(y) ? (t1)(x) : (t2)(y),	\
			      __single_eval_max(t1, t2,			\
						__UNIQUE_ID(max1_),	\
						__UNIQUE_ID(max2_),	\
						x, y))

    #define max(x, y)	__max(typeof(x), typeof(y), x, y)

還有這種更加複雜的max宏的實現:

#define __typecheck(x, y) \
		(!!(sizeof((typeof(x)*)1 == (typeof(y)*)1)))

    #define __is_constant(x) \
	(sizeof(int) == sizeof(*(1 ? ((void*)((long)(x) * 0l)) : (int*)1)))

    #define __no_side_effects(x, y) \
		(__is_constant(x) && __is_constant(y))

    #define __safe_cmp(x, y) \
		(__typecheck(x, y) && __no_side_effects(x, y))

    #define __cmp(x, y, op)	((x) op (y) ? (x) : (y))

    #define __cmp_once(x, y, op) ({	\
		typeof(x) __x = (x);	\
		typeof(y) __y = (y);	\
		__cmp(__x, __y, op); })

    #define __careful_cmp(x, y, op)			\
		__builtin_choose_expr(__safe_cmp(x, y),	\
				      __cmp(x, y, op), __cmp_once(x, y, op))
 
    #define max(x, y)	__careful_cmp(x, y, >)

小結:

上面以一個宏爲例子,意在說明,對一門語言的掌握是永無止境的,就算你把當前所有的C語言知識點、編程技能都掌握了,C語言也是不斷更新的、C標準也是不斷更新變化的。編程技巧、編程技能也是不斷進步的。
而自學往往是最有效的學習方法,但是前提是你要有好的學習資料、學習方法、學習目標,再加上刻意練習和實時反饋。否則,就是兩眼一抹黑,不知道自己學得怎麼樣、學到什麼水平了、學了有什麼用、學得對不對。其實還有一種比較有效的學習方法,找個行業內的工程師帶一帶、參考優秀的書籍、教程學一學、再結合幾個項目練一練,就知道什麼該學、要學到什麼程度,而且可以大大提高學習效率。

TIPS:

本文題所涉及到的C語言知識點:

  • 自增自減運算符
  • 宏定義
  • 預處理過程
  • 運算符的優先級與結合性
  • 語句表達式:({…})
  • GNU C的擴展語法:typeof關鍵字
  • 內建函數:_builtin
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章