1. 程式人生 > >[珠璣之櫝]淺談程式碼正確性:迴圈不變式、斷言、debug

[珠璣之櫝]淺談程式碼正確性:迴圈不變式、斷言、debug

  這個主題和程式碼的實際寫作有關,而且內容和用法相互交織,以下只是對於其內容的一個劃分。《程式設計珠璣》上只用了兩個章節20頁左右的篇幅介紹,如果希望能獲得更多的例項和技巧,我比較推崇《程式設計實踐》 (Practise of Programming)、《程式設計精粹:編寫高質量C語言程式碼》(Writing Solid Code)這兩本書,只要有一般的C語言基礎就能讀懂,而且讀起來比較快,讀完後能提高不少coding的實踐水平。

    目錄

 迴圈不變式(invariant)

  迴圈不變式主要用來幫助理解演算法的正確性,具體來看,比較針對於迴圈迭代。形式上很類似與數學歸納法,它是一個需要保證正確斷言。對於迴圈不變式,必須證明它的三個性質:

初始化:它在迴圈的第一輪迭代開始之前,應該是正確的。

保持:如果在迴圈的某一次迭代開始之前它是正確的,那麼,在下一次迭代開始之前,它也應該保持正確。

終止:迴圈能夠終止,並且可以得到期望的結果。

debug之腳手架

  聽起來挺玄乎,其實所謂的腳手架,就是在debug版本里加入的為了驗證程式是否正確的額外程式碼,比如一條為了確定迴圈中臨時變數是否按期望變化的printf語句,這比複雜的偵錯程式更快。我相信很多人在寫程式碼時都這樣做過,看到這裡,我們並不需要為自己過去“簡陋”的除錯方式而不安,而是繼續合理地使用它。

  腳手架能完成更多的工作,不僅限於變數追蹤。利用斷言,可以建立腳手架進行程式的自動測試而不是人為的追蹤,也可以利用clock()建立腳手架,把待測的程式碼放在兩個clock()中間測試執行時間(相比之下,我更喜歡用gettimeofday())。以上三種腳手架的用法是《程式設計珠璣》提到的例子,關於這種程式碼可以做更多的引申,以下內容結合了Practise of Programming和Writing Solid Code的相關內容。

1.#ifdef DEBUG

  很多人都使用過下面這樣的程式碼:

#ifdef DEBUG
    ...
#endif

  這種想法是同時維護除錯和非除錯(即交付)兩個版本,在除錯版本中自動地查錯;最後提交交付版本。當然,這種方法的關鍵是保證除錯程式碼不在最終產品中出現。只是這種程式碼在原來的程式碼裡看起來十分突兀(排版不好看,而且可能看上去喧賓奪主),使用斷言assert()是一個代替方案之一,它只在定義了DEBUG時才有效,在我的Ubuntu的assert.h裡是如果定義了NDEBUG就無效。關於斷言會在後面詳寫,這裡點到為止。

2.外殼函式/包裹函式

  做法是把待測試或者可能發生錯誤而需要檢測的函式用一段程式碼加一個外殼,或者說是包裹起來,在其中增加出錯檢查和處理程式碼,並提供合理的返回值。這兩種名稱前者出自《程式設計實踐》,後者見於UNP,舉一個UNP上的簡單的例子:

int Socket(int family, int type, int protocol)
{
    int n;
    if( (n = socket(family,type,protocol)) < 0)
        err_sys("socket error");
    return n;
}

3.用不同的演算法驗證正確性

   如果編寫了一個較快的演算法又擔心其不正確,想要檢查其正確性怎麼辦?在腳手架中構造一個包括能提供同樣功能並且正確但速度較慢的演算法(往往是舊版本中所留下的),比較兩者的執行結果。

4.提高程式碼覆蓋度

在if判斷中,總有一部分程式碼未必執行。如果想要測試不常執行的程式碼的正確性,可以用腳手架強制執行這部分程式碼。

5.消除隨機性,使錯誤重現

利用腳手架對分配的記憶體塊用garbage值0xA3而不是0填充,這樣當發現指標指向0xA3A3或內容是連續的0xA3A3時,顯然是個未定義的值,需要排錯。具體的取值和系統有關,0xA3是早期macintosh的建議用的garbage值。

6.不要擔心腳手架帶來的效能損失

正如Writing Solid Code所言,不要把對交付版本的約束應用到相應的除錯版本上,要用大小和速度來換取錯誤檢查能力。腳手架只是為了查錯,在交付版本中它們是不存在的。

斷言(assert)

   正如在腳手架中提到的,斷言可以對程式正確性的測試。除此以外,在一段小型的程式碼demo中,編寫斷言遠比精心編制一套完整的出錯處理機制或者繁複的#ifdef DEBUG要簡單的多。

  為了幫助理解這個巨集,Writing Solid Code探討了assert巨集的一種實現機制:

#ifdef DEBUG
    void _Assert(char*, unsigend);
#define ASSERT(f)    \
    if(f)                        \
        NULL;                \
    else
        _Assert(__FILE__,__LINE__)
#else
    #define ASSERT(f)    NULL
#endif

  而真正的處理函式是

void _Assert(char* strFile, unsigned uLine)
{
    fflush(stdout);
    fprintf(stderr,"\nAssertion failed:%s, line %u\n",strFile,uLine);
    fflush(stderr);
    abort();
}

為什麼要用巨集定義+函式實現,並將巨集中的__LINE__傳遞給後面實現的函式而不是僅僅靠巨集本身實現?因為__LINE__用其所在的行號替換內容,使用函式則只會變成該函式內部的行號,而使用巨集則只是把__LINE__放到對應需要檢查的位置。更具體的說明可以參考以下連結:

  另外,這個斷言巨集可以作為《程式設計珠璣(續)》習題3.5的完善解答。

往期回顧:

相關推薦

[珠璣]程式碼正確性迴圈斷言debug

  這個主題和程式碼的實際寫作有關,而且內容和用法相互交織,以下只是對於其內容的一個劃分。《程式設計珠璣》上只用了兩個章節20頁左右的篇幅介紹,如果希望能獲得更多的例項和技巧,我比較推崇《程式設計實踐》 (Practise of Programming)、《程式設計精粹:編寫高質量C語言程式碼》(Writin

程式碼分層構建模組化程式

Example C中的模組組織 很多C程式碼寫得少的C++程式設計師甚至對一個大型C程式中的模組組織毫無概念。這是對其他技術接觸少帶來的視野狹窄的可怕結果。 在C語言的世界裡,並不像某些C++教材中指出的那樣,佈滿全域性變數。當然全域性變數的使用也並不是糟糕設計的標誌(goto不是魔鬼)。一個良好設計的C語

tuple元組的“”性

tuple和list非常類似,但是tuple一旦初始化就不能修改,比如同樣是列出同學的名字:程式碼如下:>>> classmates =('Michael','Bob','Tracy')現在,classmates這個tuple不能變了,它也沒有append(

演算法導論 第二章演算法入門 筆記 (插入排序迴圈演算法分析最好和最壞時間複雜度選擇排序分治法合併排序)

插入排序: 排序問題的定義如下: 輸入:N個數{a1, a2,..., an }。 輸出:輸入序列的一個排列{a'1 ,a'1 ,...,a'n },使得a'n <=a' n<=...<

如何寫出正確的二分查詢?——利用迴圈理解二分查詢及其體的正確性以及構造方式

序言   本文以經典的二分查詢為例,介紹如何使用迴圈不變式來理解演算法並利用迴圈不變式在原始演算法的基礎上根據需要產生演算法的變體。謹以本文獻給在理解演算法思路時沒有頭緒而又不甘心於死記硬背的人。   二分查詢究竟有多重要?《程式設計之美》第2.16節的最長遞增子序列

[珠璣]字串和序列左移雜湊最長重複子序列的字尾陣列解法最大連續子序列

  字串和陣列在儲存上是類似的,把它們歸為同一主題之下。本文主要介紹三大類問題和它們衍生的問題,以及相應演算法。   本文主要介紹和討論的問題和介紹的演算法(點選跳轉): 字串迴圈移位(左旋轉)問題 問題敘述:   將一個n元一維向量向左旋轉i個位置。例如,當n=8且i=3時,"abcde

工具系列郵件--工具怎樣改變你的工作效率

ear 驅動 tracking 有一個 develop prop 第一次 ren 關聯 關於提高個人工作效率有非常多方法。如計劃工作、時間意識。集中精力、避免並行;

Java學習final

重寫 四種方法 變量 無法 設計 三種 fin 改變 img final的意思就是“這個值不能變”。 Final修飾變量時: final的變量可以直接賦值; 可以先聲明,後賦值; 也可以指向一個引用,但是一旦指向一個引用後則不能更改到其他的引用。 用來修飾數據,包括成員

商品推薦如何猜中用戶的心思?

維度 活動 不同 -i 鄰居 表示 找到 包括 一起 去商場前,你告訴自己今天只買T恤,出商場時,你還是拎了大包小包……導購員看你摸了摸連衣裙,讓你免費試穿,結果你這一穿就不願脫下了,而且一件接一件。導購猜測顧客喜歡什麽,推薦顧客試穿,滿足雙方各自的心理訴求來達成交易。電

51CTO微職位一次通過PMP經驗

基本 屬於 真題 培訓 ppt 做的 完全 重要性 提交 參加工作已有十余年,期間做過IDC數據中心運維,WEB產品研發,做過前端、框架和方案設計,做過IT類開發、實施、系統集成以及地產智能化建設等大小項目幾十個,隨著年齡增長,轉到技術支持和運維管理,工作重心也逐步轉向項目

程式碼規範&&基礎除錯&&幾道面試題

廢話篇:本文由CSUST的FINAL實驗室的LX創作,用途是給予CSUST的小鮮肉們一些關於C語言程式碼規範的一些基本知識,若本文有什麼錯誤或是表述不清之處,歡迎留言討論指正。 程式碼規範: 在講程式碼規範之前,我想給大家看一句感人肺腑的註釋名言來告誡各位以後的優秀程

董春陽竹簡app績效暖心,工資暖胃

大小企業都感覺員工越來越難招、越來越難管、越來越難滿足,企業似乎已經反轉到了僱傭關係的弱勢一方。 當我們覺得人力資源管理工作越來越難的時候,很可能就是我們的管理思路與企業人力資源實際情況脫節,不同的人力資源模組工作沒有形成合力,甚至是在互相抵消。 01 劃分:績

Lyndon的量化修煉路——趨勢指標取參方法

//文章內容為中州期貨上海分公司所有 //期市妖風大,小心被刮飛。本文不構成任何實質性建議,也不對任何依此進行的交易結果負責 目前市場多許多投資者仍然依託趨勢指標作為交易參考,其中,指標計算過程中給定的引數對交易結果具有相當大的影響,恰當的引數可以讓本來可謂之

HadoopHDFS---DNNNSNN

Hadoop淺談         在瞭解HDFS之前必須介紹一下hadoop以及hadoop和HDFS之前的關係:Hadoop是一個由Apache基金會所開發的分散式系統基礎架構。使用者可以在不瞭解分散式底層細節的情況下,開發分散式程式。充分利用叢集的威力進行高速運算和儲存

iOS開發Warning

Warning 對於一個coding有潔癖的人來說,warning在他們眼中和error沒什麼區別,就像是一口痰卡在喉嚨中,吐不出來,咽不下去,甚是難受。 我雖然不是一個“處女座”特性的人,但是在專案上線之前,還是要儘量保證 0 bug,0 error和 0 warning

珠璣”系列簡介與索引

系列博文主要目的:   收集《程式設計珠璣》和《程式設計珠璣(續)》(以下簡稱《續》)上的演算法和思想,幷包括了一些自己的思考和對相關問題的引申,以備複習和查用。 內容提要:   主要是演算法收集,結合了《程式設計實踐》 (Practise of Programming)、《程式設計精粹:編寫高質量C語

[珠璣]隨機數函式取樣與概率

  本節主要受到《程式設計珠璣》第12章隨機取樣問題的啟發,但不僅僅限於隨機取樣問題,進一步地,研究討論了一些在筆試面試中常見的和隨機函式以及概率相關的問題。   閱讀本文所需的知識:     1.對C語言中或其他語言中等價的rand()、srand()有所瞭解。本文不討論種子的設定和偽隨機數的問題;

[珠璣]位向量/點陣圖的定義和應用

  位向量/點陣圖是一個很有用的資料結構,在充分利用小空間儲存大量資料方面非常具有優勢,Linux核心中很多地方都是用了點陣圖。同時,它不但基礎,而且用到了很多程式語言的知識,以及對細節的把握,常常作為面試題出現。這裡將要介紹它的實現、操作、應用。   與點陣圖(bitmap)比,我更傾向於用位向量(bit

[珠璣]二分思想與分治法排序思想

#include <stdio.h> #include <assert.h> int BitCheck(int total,int n,int last) { FILE *input,*output0,*output1; char filename[10

[珠璣]估算的應用與Little定律

  估算的資料主要依賴於所能獲得的資料和常識,有時還包括實踐而不僅僅是理論。它常常作為一個大問題中的子問題,恰當地估算可以省去精確計算的時間和開銷。在計算機領域,所謂常識的內容很寬泛,比如硬碟的傳輸速度、CPU每秒能執行多少指令、各種資料結構的大小甚至每分鐘錄入的單詞數。有些資料是能夠從各種資料中查得的,但僅