C語言陷阱與技巧第17節,有個條件很少發生,還要不要寫if判斷?

2020-12-13 IT劉小虎

在學習C語言程序開發時,很多初學者常常會有一種「編程也不過如此」的錯覺,這種感覺通常出現在剛剛學完C語言語法,且能夠獨立完成一些課後練習題的時候,初學者的信心會在這一時期達到頂峰。可能會覺得程序無非就是各種 if 條件判斷,加上相應的邏輯處理。

程式設計師編寫程序就是為了服務人的,程序能夠提供的服務越多,這個程序的功能也就越強大。不過,程序是死板的,它需要接收外界(比如人)輸入的指令,才知道要做什麼。

例如,你打開瀏覽器,你得點擊我的文章才能看到這些內容。頭條不會在你沒有輸入時,自動的把我的文章顯示到手機。

這麼看來,「程序是各種條件判斷,加上相應的邏輯處理」這句話並沒有錯。程序會根據條件的不同,做出不同的響應。事實上,在C語言程序開發中也是如此——例如,程式設計師常常需要根據被調用函數不同的返回值,做出不同的處理,這其實就是「條件判斷」+「相應邏輯處理」。

不過,從上一節介紹的 3 種風格的C語言代碼應該可以看出,同樣一個功能,有經驗的程式設計師總是能夠寫出緊湊易讀的代碼,以更小的開銷,實現更高的執行效率。

以下這種C語言代碼常常出現在C語言程序開發中,請看:

if(cond){ ... statements; ...}else{ ... statements; ...}可是有時候 cond 只在極少的情況下發生,例如:隨機生成一個隨機數,該隨機數的範圍是 0~100000,如果隨機數小於 2,則將 val 賦值為 -1,否則將 val 賦值為當前UTC時間,相關C語言代碼如下,請看:

if(myrand() < 2) val = -1;else val = time(NULL);從上述代碼可以看出, val = -1; 其實只有 2/100000 的機率會被執行,但是為了這 2/100000 的機率,程序每次都需要判斷 if 條件是否成立,這會造成一定的性能損失。

可是,不寫 if 判斷代碼又會導致最終得到的C語言程序有可能不按照預期執行,該怎麼辦呢?類似的情況還有,某個條件非常可能成立,只會在極少情況下才不成立,但是同樣得寫上 if 語句每次判斷。

針對這種情況,其實可以參考 Linux 內核的C語言代碼,請看下面這兩個宏:

#define likely(x) __builtin_expect(!!(x), 1)#define unlikely(x) __builtin_expect(!!(x), 0)其實看宏的名字應該就能明白它們的作用:likely(x) 會告訴編譯器 x 很可能成立,unlikely(x) 則會告訴編譯器 x 不太可能成立,然後編譯器會據此優化代碼,生成效率更高的程序,稍後我們會看到一個實例。

這一過程是由編譯器內置函數__builtin_expect實現的,應該能夠發現,Linux 內核使用該函數時用到了一個小技巧——使用 「!!」 將條件轉換為 bool 值( 0 或 1)。

相信讀者應該已經明白,在C語言中任何非零值都會被認為是「真」,所以下面這樣的C語言代碼:

if(32) printf("true");elseprintf("false");編譯後會輸出 「true」。但是有時候有些程式設計師在開發中會忽略這一點,掉進「陷阱」,例如:

有兩個函數 fun1() 和 fun2() 會返回任意整數,要求只有當它們一個返回真,一個返回假的時候,才列印「success」。

有些程式設計師會直接寫:

int a = fun1();int b = fun2();if( ( a && (!b) || ( (!a)&&b ) ) printf("success")還有些程式設計師注意到了 fun1() 和 fun()2 要麼返回真,要麼返回假,他覺得上面這種寫法太羅嗦,於是寫可能會寫出這樣的C語言代碼:

if( fun1() != fun2() ) printf("success");看起來,似乎只有一個真一個假的時候,fun1() 和 fun2() 才會不相等,所以上面這種簡潔的寫法更好?

不過要是 fun1() 和 fun2() 函數一個返回 3,一個返回 4 ,上面這種寫法就會輸出不符合預期的結果了。所以這種思路正確的寫法如下,請看相關C語言代碼:

if( !!fun1() != !!fun2() ) printf("success");「!!」 可以將條件轉換為 bool 值,下面這兩種寫法是等價的:

b = cond?1:0;// 等價於b = !!cond;likely 與 unlikely 宏的實例

現在我們一起看一下 likely 與 unlikely 宏的作用,寫出C語言代碼如下,請看:

#define likely(x) (__builtin_expect(!!(x), 1))#define unlikely(x) (__builtin_expect(!!(x), 0))int test_likely(int x){ if(likely(x==0)) x = 6; else x = 9; return x;}int test_unlikely(int x){ if(unlikely(x==0)) x = 6; else x = 9; return x;}int test_normal(int x){ if(x==0) x = 6; else x = 9; return x;# gcc -fprofile-arcs -O2 -c t.c# objdump -d t.o

其實從這裡也能夠看出,如果程式設計師將 likely 宏與 unlikely 宏使用反了,是會降低C語言程序的效率的,因此在使用這兩個宏之前,一定要弄清楚條件是很大可能發生,還是基本不會發生,否則會適得其反。

小結

本節討論了C語言程序開發中條件語句的重要性,介紹了使用 「!!」 將條件轉換為 bool 值的小技巧,並在此基礎上討論了 Linux 內核中常用的 likely 和 unlikely 兩個宏,正確使用這兩個宏是能夠提高最終得到的C語言程序運行效率的。

相關焦點

  • C語言陷阱與技巧第12節,重要數據怎麼保存?如何判斷數據是否損壞?
    C語言中的結構體是非常有用的複合數據類型,正是有了結構體,C語言在描述複雜問題時才能夠得心應手。事實上,當初 Dennis Ritchie 開發C語言用於替換 B 語言,其中一個主要原因就是 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語言陷阱與技巧第18節,函數式宏定義的「缺陷」,沒有參數類型檢查...
    在之前的文章裡,我們曾討論C語言程序開發中 define 宏定義的「陷阱」之一就是可能會產生多次「副作用」,這也是C語言中函數式宏定義與真正函數的主要區別之一。顯然,define 宏定義的這種「陷阱」會導致程序存在隱患,而且這種隱患造成的危害不亞於「野指針」。
  • C語言陷阱與技巧25節,常說的「回調函數」是什麼?為何要用它?
    上一節主要討論了C語言中的函數指針在「運行時」代碼選擇中的應用,這其實是一個小技巧,僅需在需要切換代碼的時候重新確定函數指針的指向,之後的代碼就幾乎不用動了。粗略來說,只需一次 if 判斷,就可以將所有C語言代碼涉及到的代碼切換完成。這樣的代碼風格顯然有利於程式設計師維護,也能提升C語言程序的運行效率。
  • C語言陷阱與技巧第15節,為什麼每調用一次函數,就需要一次if判斷...
    在C語言程序開發中,調用一個有返回值的函數時,一般要對函數的返回值做判斷,以確定函數是否按照預期執行。如果被調用函數沒有按照預期執行,最好加上相應的錯誤處理代碼,否則最終編譯得到的C語言程序穩定性就不夠好,遇到一點點意外,可能就不會正常工作了。沒有判斷C語言函數的返回值,會有什麼問題?
  • C語言陷阱與技巧第29節,很多程式設計師不知道,C語言也能「繼承」父類
    之前兩節探討了C語言進行「面向對象」編程的可能性。現在已經了解,結合C語言的指針和結構體語法,基本能夠實現對象語法最核心的部分,即成員函數和成員變量。另外,上一節討論了如何利用指針,將公開的成員變量,封裝成 private(私有)變量,由此也可以看出C語言指針語法的強大。
  • C語言陷阱與技巧第8節,輸出適當的信息,有利於定位錯誤和異常代碼
    把複雜的任務拆分成簡單的子模塊在C語言程序開發中,程式設計師常常會把一個複雜的任務拆分成若干個較為簡單的子模塊,這些子模塊可以看做是複雜任務的各個組成部分。因此,程式設計師將子模塊逐個完成後,就可以將其像「積木」一樣搭建起來,進而解決複雜任務。
  • C語言陷阱與技巧第13節,1位元組(Byte)一定等於8位(bit)嗎?C語言操作...
    在這些項目中,以嵌入式項目為代表,一般都需要嚴格控制內存的使用——使用 1 個字節(Byte)就能存放的值,絕對不定義 2 個字節寬度的變量。甚至,一些「摳門」的C語言程式設計師會將 1 個字節掰成若干個位(bit)使用。
  • C語言陷阱與技巧31節,都說void*指針是「萬能指針」,它萬能在哪
    在C語言程序開發中,一些比較成熟的庫函數常常會被使用。畢竟,如果手邊就有不錯的「輪子」可以用,沒有程式設計師願意再花費精力憑空造一個輪子出來。那為什麼還要使用 void 指針呢?例如,在實際的C語言項目開發中,操作某個對象時,常常先構建結構體 struct S 描述該對象,然後使用 init() 函數獲取相應信息,因為接下來的操作函數 handle() 需知道要操作哪個對象,所以要使用 init() 函數返回的信息,C語言代碼似乎可以這麼寫:struct S *p = init();handle(p);從上面兩行C語言代碼可以看出,其實外界調用可以不用關心 p
  • C語言學習推薦書籍
    題圖:來自網絡關於C關於C編程,我覺得有下面3個層次:基礎 - 基本語法進階 - 避免常見錯誤
  • C語言快速入門系列(三)
    本節引言:在上一節中,對C語言的基本語法進行了學習,類比成學英語的話,我們現在只是會單詞而已,組成一個個句子還需要學習一些語法
  • 硬體工程師必知的10個C語言技巧
    這10個C語言技巧(C語言仍然是常見的選擇)可以幫助設計師避免因基礎性錯誤而導致某些缺陷的產生並造成維護方面的困擾。為了成功的推出一個產品,軟體開發過程本身需要經歷無數的實踐風險和障礙。任何工程師最不希望的事情就是因所使用語言或工具而帶來的挑戰。因此,這就需要硬體設計師編寫代碼來測試硬體的工作狀況,在資源受限的情況下,還需要開發硬體和嵌入式軟體。
  • C語言簡明教程(四)選擇程序設計
    知識點條件判斷案例用 if 語句實現選擇結構關係運算符和關係表達式邏輯運算符和邏輯表達式條件運算符和條件表達式用 switch 語句實現多分支選擇結構接下來,我們將通過一些實際的例子,來熟悉 C 語言的選擇結構,以及相關語言的語法特點。
  • C語言面試54題
    第3題, 解釋一下語義錯誤。在寫程序的時候會有很多語義錯誤,比如說,拼錯了命令,一個函數的參數個數錯了, 數據類型不匹配,等等。第4題, C語言中如何使用增加和減少語句?第11題, 單等號和雙等號的區別是什麼?單等號表示賦值運算符。雙等號是等於條件判斷運算符。第12題,解釋一下c語言的原型函數。原型函數是對一個函數的聲明。
  • 刻意練習—第十節《LInux c宏處理&動靜態連結庫》
    2.2.C語言預處理代碼實戰本節向大家演示幾種常見的預處理,通過代碼實例讓大家明白這幾種預處理技巧的使用方法和使用環境,目的是使大家真正學會使用這些預處理技巧。2.3.宏定義1本節首先講解宏定義的一般規則和使用方法,然後用兩個宏定義向大家演示宏定義中的關鍵點和易錯點,這兩個宏定義都是面試題中非常常見的典型題目。
  • C語言陷阱與技巧第26節,一文弄懂「函數指針數組」,為什麼不直接...
    其實,這就是將C語言中的指針「特殊化」了。在分析指針問題時,一個小技巧就是將指針當作C語言中的「普通數據類型」,例如可以將 int* 變量看作是「int指針型」變量。答案是肯定的,請看:typedefint (*funs[8])(int *data);上面這行C語言代碼就定義了一個函數指針數組 funs,funs 可以管理 8 個函數指針。
  • C語言陷阱與技巧第2節,使用inline函數可以提升程序效率,但是讓...
    打開 Linux 內核原始碼,會發現內核在定義C語言函數時,有很多都帶有 「inline」關鍵字,請看下圖,那麼這個關鍵字有什麼作用呢?inline 關鍵字的作用在C語言程序開發中,inline 一般用於定義函數,inline 函數也被稱作「內聯函數」,C99 和 GNU C 均支持內聯函數。那麼在C語言中,內聯函數和普通函數有什麼不同呢?
  • python條件判斷語句
    %d為佔位,%c為引用,這時的變量c就為%d,數字類型 運算符 假設變量a為10,變量b為21比較運算符: 賦值運算符: 邏輯運算符: 以上是我們要掌握的運算符,還有一些位運算符、成員運算符、身份運算符等等 大家可以自己搜索了解一下 運算符的優先級 條件判斷語句
  • C符號陷阱
    1.1  =不同於==在C語言中符號=作為賦值運算,符號==作為比較,一般而言賦值運算相對於比較運算出現得更頻繁,因此字符較少的符號=就被賦予了更常用的含義——賦值操作。此外,在C語言中賦值符號被作為一種操作符對待,因而重複進行賦值操作(如a=b=c)可以很容易地書寫,並且賦值操作還可以被嵌入到更大的表達式中。
  • C語言頭文件被include後都發生了什麼?為何不能在頭文件定義變量
    相信讀者大都使用過C語言的頭文件,不過還是有可能對其理解不透徹,這會導致讀者在遇到一些問題時不知道如何解決。本文將較為詳細的討論C語言頭文件的特點,並在此基礎上,分析幾個初學者常會跳進的「陷阱」,以及相應的解決辦法。