C語言陷阱與技巧第18節,函數式宏定義的「缺陷」,沒有參數類型檢查...

2020-12-11 IT劉小虎

在之前的文章裡,我們曾討論C語言程序開發中 define 宏定義的「陷阱」之一就是可能會產生多次「副作用」,這也是C語言中函數式宏定義與真正函數的主要區別之一。顯然,define 宏定義的這種「陷阱」會導致程序存在隱患,而且這種隱患造成的危害不亞於「野指針」。

C語言函數式宏定義的缺陷

例如這面這個經典的例子,請看相關C語言代碼:

#define max(a, b) ( (a)>(b)?(a):(b) )max 宏接收兩個參數,並且返回較大的參數值。如果該宏在一個較大的C語言項目中較為頻繁的使用,很難保證每次傳遞給 max 的兩個參數不是計算表達式,也就是說 max 宏的參數 a 和 參數 b 有可能是一個計算表達式,例如:

int val = 3;int m = max(val++, 2);上面這兩行C語言代碼常常會給程式設計師一種 val++ 只會執行一次的錯覺,但是事實上編譯器會將上述代碼預處理為:

int val = 3;int m = ( (val++)>(2)?(val++):(2) );也就是說,val++ 會被執行兩次(即產生兩次副作用),執行完這兩條語句後,val 是等於 5 ,而不是等於 4 的。編寫C語言代碼測試之:

編譯並執行這段C語言代碼,得到如下輸出:

# gcc t.c# ./a.out val = 5, m = 4這樣的錯誤雖然很簡單,但是人常常會對這種「擺在眼前的錯誤」視而不見,所以花費大量時間才能定位到它也不足為奇。另外,這樣的錯誤又會顯得「飄忽不定」,因為如果傳遞給 max 的兩個參數,後一個數比前一個數大,則 val++ 又會只執行一次了,例如:

int val = 3; int m = max(val++, 6);// val=4, m=6這種類型的錯誤在實際的C語言項目開發中,相當煩人。

事實上,我就遇到過這樣的錯誤,而且花了一些時間才找到問題代碼。

避免多次副作用

C99 對 C語言做了一定的擴展,」({ … })」 就是其中之一(這個符號我們之前討論過),可以把這個符號包裹的代碼理解為一句,例如:

val = ({ a = 3; c = a+b; c;})上面這段C語言代碼相當於下面這句:

a = 3;val = a+b;所以基於此,我們可以對前面提到的有「缺陷」的 max 宏做一點改進,請看:

#define maxint(a, b) ({ int _a = a, _b = b; _a>_b?_a:_b; })使用中間變量 _a 和 _b 看似麻煩,但是有兩個好處:可以防止傳入計算表達式時產生的多次「副作用」,而且還使 maxint 宏具備了參數類型檢查的功能。

C語言是一門高效的程式語言,因此它關心數據的類型,不同類型的數據相比較有時候會產生不預期的結果。這其實也屬於C語言中宏的「缺陷」,因此一般能夠使用函數完成的工作都不建議再使用宏。如果某個功能的代碼比較簡單,希望提升其效率,可以使用 inline 函數(內聯函數)定義。

總之,除非某個宏能夠提供非常大的便利,否則非常不建議使用宏。

經過改進的 maxint 宏能夠提供參數類型檢查,這主要得益於中間變量的使用。因此如果傳遞給 maxint 宏一個浮點數,maxint 宏會將其截斷成 int 型再做比較,例如:

val = maxint(5, 8.14);執行完畢後,val 是等於 8 的。

另外一個小技巧

在使用三目運算符「?:」時,可以考慮下面這個小技巧,請看:

p = x>y?:y;將 ?: 之間的數值省去,也是C99中的一個新特性,至於該技巧有哪些性質,以及可以應用於哪些場合,留給讀者自己思考了。

小結

本節主要討論了C語言中 define 宏的兩個「缺陷」——可能產生多次「副作用」,以及難以提供參數的類型檢查。不過也應該明白,這些缺點有時候會成為非常有用的特點,它們可以與函數互補,提供更加靈活的功能。但是如果不希望某個宏具有這兩個特點,可以考慮本節提供的小技巧。

相關焦點

  • ...函數式宏定義不能用普通函數代替嗎?為什麼要使用do{}while(0...
    C語言中的「函數式宏定義」C語言中的 define 宏定義可以像函數那樣接收參數(這種宏定義常被稱作「函數式宏定義」),不過不能像函數那樣提供參數的類型檢查,這個特點在有些程式設計師看來是不安全的。但是,函數式宏定義不關心參數類型這個特點,有時候也會被利用起來,寫出一些適用性更廣的C語言代碼,例如:#define max(__a, __b) ( (a)>(b)?
  • C語言陷阱與技巧20節,自定義「編譯時」assert方法,在代碼編譯階段...
    /a.out a.out: t.c:11: main: Assertion `fd > 0' failed.Aborted可以看出,第 12 行的 printf() 函數並沒有被執行。這是因為程序運行環境裡並沒有 「/dev/sth」 這個文件,所以 open() 函數執行失敗,傳遞給 assert() 的參數為假,C語言程序被終止,並且輸出 t.c 源文件第 11 行代碼 assert 失敗。assert() 可以輸出出錯的代碼位置,這個特性在較為大型的C語言程序開發中是非常好用的,因為無需程式設計師再去手工調試代碼,排查出錯代碼的位置了。
  • C語言陷阱與技巧第2節,使用inline函數可以提升程序效率,但是讓...
    因此,建議把那些對時間要求比較高,且C語言代碼長度比較短的函數定義為內聯函數。如果在C語言程序開發中的某個函數比較大,又會被反覆調用,並且沒有特別的時間限制,是不適合把它做成內聯函數的。前文提到內聯函數的表現有些像 define 宏定義,但是為了類型安全和易讀性,應優先使用內聯函數而不是複雜的宏。下面通過實例進一步分析 inline 內聯函數的特性。inline內聯函數的「展開代碼」是什麼意思?
  • 快速上手系列-C語言之預編譯命令、宏定義及條件編譯
    宏定義1、宏定義在進行文本編輯時,「替換」是一個很有用的功能。C語言編譯預處理程序也提供類似的功能:在源程序中,允許一個標識符(稱為宏名)來表示一個語言符號字符串。在C語言中,「宏」分為無參數的宏和有參數的宏。
  • 刻意練習—第十節《LInux c宏處理&動靜態連結庫》
    2.2.C語言預處理代碼實戰本節向大家演示幾種常見的預處理,通過代碼實例讓大家明白這幾種預處理技巧的使用方法和使用環境,目的是使大家真正學會使用這些預處理技巧。2.3.宏定義1本節首先講解宏定義的一般規則和使用方法,然後用兩個宏定義向大家演示宏定義中的關鍵點和易錯點,這兩個宏定義都是面試題中非常常見的典型題目。
  • C/C++可變參數函數
    但在某些情況下希望函數的參數個數可以根據需要確定,因此c語言引入可變參數函數。這也是c功能強大的一個方面,其它某些語言,比如fortran就沒有這個功能。典型的可變參數函數的例子有大家熟悉的printf()、scanf()等。二、c/c++如何實現可變參數的函數?
  • C語言編程規範 clean code
    這裡常量如前文定義,是指基本數據類型、枚舉、字符串類型的全局 const 變量。函數式宏,如果功能上可以替代函數,也可以與函數的命名方式相同,使用大駝峰命名風格。這種做法會讓宏與函數看起來一樣,容易混淆,需要特別注意。
  • C語言學習推薦書籍
    第10章 文本處理第11章 分離用戶界面與內部實現第12章 撰寫設計文檔附錄C語言中接口定義的不同形式《華為技術有限公司c語言編程規範》有一句話說的很好,「規範大於配置」。如果你需要為一個團隊制定C編程規範,《華為技術有限公司c語言編程規範》可以作為你的參考。
  • C語言陷阱與技巧第12節,重要數據怎麼保存?如何判斷數據是否損壞?
    C語言中的結構體是非常有用的複合數據類型,正是有了結構體,C語言在描述複雜問題時才能夠得心應手。事實上,當初 Dennis Ritchie 開發C語言用於替換 B 語言,其中一個主要原因就是 B 語言不支持「結構體」式數據結構。
  • C語言宏定義
    宏定義的一般形式為: #define 宏名 字符串#表示這是一條預處理命令,所有的預處理命令都以#開頭。define是預處理命令。宏名是標識符的一種,命名規則和標識符相同。字符串可以是常數、表達式等。這裡所說的字符串是一般意義上的字符序列,不要和C語言中的字符串等同,它不需要雙引號。
  • C語言陷阱與技巧25節,常說的「回調函數」是什麼?為何要用它?
    上一節主要討論了C語言中的函數指針在「運行時」代碼選擇中的應用,這其實是一個小技巧,僅需在需要切換代碼的時候重新確定函數指針的指向,之後的代碼就幾乎不用動了。粗略來說,只需一次 if 判斷,就可以將所有C語言代碼涉及到的代碼切換完成。這樣的代碼風格顯然有利於程式設計師維護,也能提升C語言程序的運行效率。
  • C語言中可變參數的用法
    我們在C語言編程中會遇到一些參數個數可變的函數,例如printf()這個函數,它的定義是這樣的:   int printf( const char* format, ...)va_list arg_ptr, type );   void va_end( va_list arg_ptr );  va在這裡是variable-argument(可變參數)的意思.這些宏定義在stdarg.h中,所以用到可變參數的程序應該包含這個頭文件.下面我們寫一個簡單的可變參數的函數,改函數至少有一個整數參數,第二個參數也是整數,是可選的.函數只是列印這兩個參數的值
  • C語言宏定義的特殊用法以及避坑指南
    總結一下C語言中宏的一些特殊用法和幾個容易踩的坑。由於本文主要參考GCC文檔,某些細節(如宏參數中的空格是否處理之類)在別的編譯器可能有細微差別,請參考相應文檔。宏基礎宏僅僅是在C預處理階段的一種文本替換工具,編譯完之後對二進位代碼不可見。
  • C語言陷阱與技巧第29節,很多程式設計師不知道,C語言也能「繼承」父類
    之前兩節探討了C語言進行「面向對象」編程的可能性。現在已經了解,結合C語言的指針和結構體語法,基本能夠實現對象語法最核心的部分,即成員函數和成員變量。另外,上一節討論了如何利用指針,將公開的成員變量,封裝成 private(私有)變量,由此也可以看出C語言指針語法的強大。
  • 嵌入式C語言編程規範
    說明:宏能提供比函數優越的速度,但是沒有參數檢查機制,不當的使用可能產生非預期後果。 5.3 類型及類型轉換規則5.3-1(強制):應該使用標明了大小和符號的typedef代替基本數據類型。不應使用基本數值類型char、int、short、long、float和double,而應使用typedef進行類型的定義。
  • 深度剖析C語言的main函數
    這可能是因為 在 C 和 C++ 中,不接收任何參數也不返回任何信息的函數原型為「void foo(void);」。可能正是因為這個,所以很多人都誤認為如果不需要程序返回值時可以把main函數定義成void main(void) 。然而這是錯誤的!main 函數的返回值應該定義為 int 類型,C 和 C++ 標準中都是這樣規定的。
  • C語言陷阱與技巧第17節,有個條件很少發生,還要不要寫if判斷?
    頭條不會在你沒有輸入時,自動的把我的文章顯示到手機。這麼看來,「程序是各種條件判斷,加上相應的邏輯處理」這句話並沒有錯。程序會根據條件的不同,做出不同的響應。事實上,在C語言程序開發中也是如此——例如,程式設計師常常需要根據被調用函數不同的返回值,做出不同的處理,這其實就是「條件判斷」+「相應邏輯處理」。
  • 《C語言入門指南》中篇
    字符串中可以含任何字符,它可以是常數、表達式、if 語句、函數等,預處理程序對它不作任何檢查,如有錯誤,只能在編譯已被宏展開後的源程序時發現。2、宏定義不是說明或語句,在行末不必加分號,如加上分號則連分號也一起替換 3、宏定義必須寫在函數之 外,其作用域為宏定義命令起到源程序結束。
  • c語言不定參數宏,va_start,va_arg的來歷解釋
    宏定義如下:#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ),這裡的va_list的類型是char *,v是輸入的第一個參數。
  • C語言陷阱與技巧第8節,輸出適當的信息,有利於定位錯誤和異常代碼
    將任務拆分成子模塊後,每個子模塊常常被封裝成一個C語言函數,所以,最後的「堆積木」其實就是調用各個C語言函數。不過,每一個子模塊都有可能得到正常結果,也有可能得到異常結果,這通常用C語言函數的返回值區分。在「堆積木」階段調用各個函數時,應該根據被調用函數的返回值做不同的處理。