C語言陷阱與技巧20節,自定義「編譯時」assert方法,在代碼編譯階段...

2020-12-25 IT劉小虎

在C語言程序開發中,程式設計師寫代碼時應該考慮的「面面俱到」,這樣才能寫出功能穩定的程序。例如,在實現 open() 函數時,先完成它的功能固然是重要的,但是程式設計師還需要考慮各種「意外」,比如下面這種情況。

假設不存在 /dev/sth 這個文件,仍然調用 open() 函數打開它:

int fd = open("/dev/sth", O_RDONLY);此時 open() 函數不應該感到迷惑,而是具備處理這種「意外」的能力。標準庫的 open() 函數在遇到這種情況時,會返回一個錯誤碼,對應著「文件不存在」的錯誤信息。

所以我們在開發C語言程序的過程中,寫出的代碼也應具備這種處理「意外」的能力。處理「意外」最常用的方式之一就是返回一個錯誤碼,輸出一段錯誤提示信息,這一點其實之前的文章討論過。

使用 assert

在C語言程序開發階段,為了方便,我們可以在可能出現不預期的「意外」處使用 assert()。assert() 的C語言原型如下:

#include <assert.h>void assert(scalar expression);

使用它需要包含 assert.h,assert() 接收一個參數 expression,可以是一個表達式,如果 expression 為真,則什麼都不會發生。如果 expression 為假,則 assert() 會終止C語言程序,並且輸出 assert 失敗的代碼位置。

例如下面這段C語言代碼:

int fd = open("/dev/sth", O_RDONLY);assert(fd > 0);printf("fd = %d\n", fd);

編譯並執行,得到如下結果:

# gcc t.c# ./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語言程序開發中是非常好用的,因為無需程式設計師再去手工調試代碼,排查出錯代碼的位置了。

不過,assert() 在遇到假參數時,直接將C語言程序終止太過於死板。比如某個C語言程序有兩套邏輯,第一套邏輯在 open() 函數成功打開文件時運行,第二套邏輯則在 open() 函數打開文件失敗時運行。要是使用 assert() 判斷 open() 函數是否成功打開文件,則第二套邏輯永遠沒有機會運行。

所以,assert() 一般僅用於開發階段幫助程式設計師定位錯誤,不能依賴 assert() 處理「意外」。事實上,為了便於使用,在定義了 NDEBUG 宏之後,assert() 就不再生成代碼了,此時 assert() 相當於一個空格。請看下面這段C語言代碼:

#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#define NDEBUG#include <assert.h>int main(){int fd = open("/dev/sth", O_RDONLY); assert(fd > 0);printf("fd = %d\n", fd);return0; }

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

# gcc t.c# ./a.out fd = -1編譯時 assert

可以看出,assert() 用於處理C語言程序可能出現諸多預期之外的「意外」時很有用,它能夠自己輸出究竟哪一個「意外」發生。但是 assert() 也是死板的,它在遇到假條件時直接把程序終止,剩餘的代碼邏輯不再有機會執行。

另外還有一點要說明,assert() 本身也會影響C語言程序的運行效率,這也是它常常只被使用在開發階段的另一個原因。

其實仔細想想,使用 assert() 的目的其實只是希望它能夠在C語言程序遇到不預期的「意外」時提醒程式設計師,我們並不關心 assert() 是否參與程序運行。如果使用 assert() 判斷的是常量表達式,那我們可以自己定義一個 static_assert() 宏,並且讓它在編譯時就判斷條件表達式是否成立,這樣的宏可能在某些場合更加好用。

那該如何實現編譯時 assert 這個功能呢?

其實很簡單,首先應該明白數組的長度不可能是負數,基於這一點,static_assert() 宏就容易實現了,請看下面的C語言代碼:

#define static_assert(expr) \do{ char tmp[(expr)?1:-1]; }while(0)如果條件表達式為真,則 static_assert() 宏會定義一個長度為 1 的數組,否則就會嘗試定一個長度為 -1 的數組,此時必定無法編譯通過。這裡值得一提的一個小技巧是使用 {} 符號將定義的 tmp 數組的作用域限定在本次調用的 static_assert 宏裡,避免多次調用 static_assert 時出現重複定義。

寫出如下C語言代碼測試之:

int main() { static_assert(2>1);printf("assert 2>1\n");static_assert(2<1);printf("assert 2<1\n");return0; }

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

顯然,static_assert() 宏在編譯階段就將假條件表達式找出來了。可能有些讀者會覺得如果 assert 成功,就會定義一個 tmp 數組,雖然它的長度很短,但是仍然浪費了棧空間。其實這裡可以把長度為零的數組,即:

#define static_assert(expr) \do{ char tmp[(expr)?0:-1]; }while(0)在 assert 成功時會執行 char tmp[0];,它的長度為 0,感興趣的讀者可以使用 sizeof() 測試一下。到這裡,我們就較為粗略的定義好了 static_assert 宏,它在編譯階段就能發現假條件。

小結

本節主要介紹了 assert() 的使用,應該能夠發現,在開發階段,它能夠幫助程式設計師快速的定位「意外」,也討論了 assert() 的不足之處,並在此基礎上自己定義了「編譯時」的static_assert 宏。按照這樣的思路,其實還有很多定義 static_assert() 宏的其他方法,具體哪些方法留給讀者自己思考了。

相關焦點

  • Keil編譯警告:function "assert_param" declared implicitly的...
    1 問題描述新建STM32的keil工程,在編譯時出現警告「..FWLIBsrcstm32f10x_rcc.c(273): warning: #223-D: function 「assert_param」 declared implicitly」,接下來一堆警告和錯誤。
  • C語言程序開發中常用的assert,到底有什麼用,有哪些好處?
    如果表達式的計算結果為真,assert() 不執行任何操作,反之,如果表達式的計算結果為假,assert() 將在 stderr 上顯示錯誤信息並且終止程序運行。assert() 一般用於跟蹤C語言程序的運行時(與編譯時不同)錯誤,一般這樣的錯誤不是語法錯誤,所以能夠編譯通過,但是最終得到的C語言程序在執行時,可能會給出不預期的錯誤結果。
  • 快速上手系列-C語言之預編譯命令、宏定義及條件編譯
    上一篇寫了C語言中變量的存儲類別,提到普通局部變量、普通全局變量和靜態局部變量及靜態全局變量,這裡簡單了解一下C語言的預編譯命令、宏定義和條件編譯。預編譯命令(預編譯處理--->編譯---->彙編--->連接)1、預處理:預處理是C語言的一個重要功能,如文件包含、常量定義都屬於預處理命令,C語言提供的預處理功能主要有以下三種:1)文件包含 #include2)宏定義 #define3)條件編譯 #if #endif4)防止頭文件重複包含2、文件包含處理
  • C語言陷阱與技巧第18節,函數式宏定義的「缺陷」,沒有參數類型檢查...
    在之前的文章裡,我們曾討論C語言程序開發中 define 宏定義的「陷阱」之一就是可能會產生多次「副作用」,這也是C語言中函數式宏定義與真正函數的主要區別之一。顯然,define 宏定義的這種「陷阱」會導致程序存在隱患,而且這種隱患造成的危害不亞於「野指針」。
  • Linux下C語言編譯的問題
    連結時缺失了相關目標文件(.o)  測試代碼如下: test.h test.c(調用func.c的函數) func.c main.c(調用test.c的函數)  然後編譯。,在連結命令中給出所依賴的庫時,需要注意庫之間的依賴順序,依賴其他庫的庫一定要放到被依賴庫的前面,這樣才能真正避免undefined reference的錯誤,完成編譯連結。
  • C語言陷阱與技巧第8節,輸出適當的信息,有利於定位錯誤和異常代碼
    之所以要這麼做,是因為若直接編寫C語言代碼一次性解決複雜任務,往往會讓整個代碼「揉作一團」,不僅開發時容易引入 bug,而且後期維護起來也比較痛苦。想像一下,若將任務拆分成若干個子模塊,以後發現問題時,可能只需要修改某個子模塊就可以了。但是若沒有這麼做,查找和解決問題就必須對全部代碼下手了。
  • 終於理解了編譯是怎麼回事!從C語言到機器語言的升華過程!
    下面說一下將C語言編譯為機器語言的整個過程: 首先,我們寫出一份C程序代碼,命名該代碼為hello.c,這個代碼文件,我們稱之為原始碼(Srouce Code)。
  • Java代碼的編譯與反編譯
    一、什麼是編譯1、利用編譯程序從源語言編寫的源程序產生目標程序的過程。
  • C語言的預編譯,程式設計師必須懂的知識!
    由「原始碼」到「可執行文件」的過程包括四個步驟:預編譯、編譯、彙編、連結。所以,首先就應該清楚的首要問題就是:預編譯只是對程序的文本起作用,換句話說就是,預編譯階段僅僅對原始碼的單詞進行變換,而不是對程序中的變量、函數等。預編譯指令的基本知識不作詳細介紹,只稍作匯總,重點是後面的我能想到的 使用時的注意事項。
  • 深度理解C語言的編譯機制和語言標準,萬物皆可C!
    用 C 語言編寫一個程序時,你將編寫的內容保存在一個被稱為原始碼文件的文本文件中。  大多數 的系統,都需要該文件的名稱以 .c 結尾。例如,hello world.c 。名稱中小點前的部分被稱為基本名,小點後的部分被稱為擴展名。因此,hello world 是一個基本名,c 是一個擴展名。組合在一起的 hello world.c 是文件名。
  • 使用Gradle編譯Java工程之自定義Plugins篇
    我們可以自定義實現自己的Gradle插件,然後分享給其他人使用。我們可以使用不同的語言來實現,不過本章中作者選擇使用Groovy來實現的,大家也可以使用像java、scala等喜歡的別的語言來實現。Gradle自定義插件的實現和自定義任務類型非常的相似,而它兩一般都是結合使用的。
  • STM32中C語言知識點:初學者必看,老鳥複習(長文總結)
    STM32中的assert_param默認是不使用的,即:如果要使用,需要定義USE_FULL_ASSERT宏,並且需要自己實現assert_failed函數。特別的,使用STM32CubeMX生成代碼的話,會在main.c生成:
  • 【本質】你知道C語言編譯的過程嗎?
    那麼,你知道從原始碼到可執行文件經歷了哪些過程嗎。僅僅是編譯?下面以windows環境下的test.c為例,test.c裡的代碼為::(1)預處理;(2)編譯;(3)彙編;(4)連結。使用預處理器把源文件test.c經過預處理生成test.i文件,預處理用於將所有的#include頭文件以及宏定義替換成其真正的內容。
  • 深入淺出iOS編譯
    前言兩年前曾經寫過一篇關於編譯的文章《iOS編譯過程的原理和應用》,這篇文章介紹了iOS編譯相關基礎知識和簡單應用,但也很有多問題都沒有解釋清楚
  • 以Linux系統上的gcc為例,解密C語言編譯背後的全過程!
    在這個網際網路時代,很多人都選擇了計算機專業,而只要是計算機專業的小夥伴,可能都會學C語言,但是大家是否都清楚C語言編譯的完整過程呢,今天我就帶著大家一起來做個解密吧。 C語言相對於彙編語言是一種高級語言,要想在系統上運行,需要通過編譯器把它轉換成機器能夠讀懂的可執行的代碼。
  • 面試官:Linux下如何編譯C程序?
    本文轉載自【微信公眾號:嵌入式大雜燴】,經微信公眾號授權轉載,如需轉載與原文作者聯繫Windows下常用IDE來編譯,Linux下直接使用gcc來編譯,編譯過程是Linux嵌入式編程的基礎,也是嵌入式高頻基礎面試問題
  • C語言陷阱與技巧第12節,重要數據怎麼保存?如何判斷數據是否損壞?
    C語言中的結構體非常有用例如,利用C語言描述人的身高、體重、年齡、性別、姓名時,使用結構體時非常方便的,相關C語言代碼可以如下定義:struct person{float height;float weight;int age;char gender;char name[128];
  • 長文 | 花了兩天時間整理了STM32中的一些C語言知識點,初學者福利!老鳥複習
    STM32中的assert_param默認是不使用的,即:如果要使用,需要定義USE_FULL_ASSERT宏,並且需要自己實現assert_failed函數。特別的,使用STM32CubeMX生成代碼的話,會在main.c生成:
  • 動手編譯自定義版本的最新 jQuery 類庫?
    編譯需要的工具 在我們編譯前,我們需要準備3個編譯工具: git 1.7或者更新:用來克隆抓取jQuery代碼庫 npm:這個在我們以前的node.js入門中介紹過,如果你安裝了node.js就包含了這個工具 grunt:一個基於任務的命令行javascript編譯工具
  • Notepad++編譯和運行C語言(GCC)
    我們在學習C語言的時候,實際上只需要編譯器和編輯器就能開搞了。(初學者過早接觸IDE不利於理解程序構建的過程)在看這篇文章的時候,假設你已經知道如何把GCC配置到環境變量,並且會在命令行/終端下使用gcc 編譯C代碼文件 並運行了。