1. 程式人生 > >教科書不應該再過多介紹的C語言語法

教科書不應該再過多介紹的C語言語法

屏幕 檢查 展示 個數 鍵盤 上下 作用域 除法 vol

C語言也許挺簡單,但是C標準有700頁,所以,如果你不想花費畢生精力去研究它,那麽你應該知道哪些部分可以被忽略。
讓我們從二合字母或者三合字母開始,如果你的鍵盤缺少{}鍵,你可以用<%和%>來替代,就像是int main()<%...%>。這在20世紀90年代還是有用的,因為那個時候世界上的鍵盤有不同的樣式,但是今天你很難找到沒有{ }的鍵盤了。三合字母類似於??<和??>,這個東西更沒用,以至於gcc和clang的作者都沒有花任何時間去編碼解析它們。
像三合字母這種,語言中過時的東西很容易就被忽略掉了,因為根本沒有人提到它們。但是語言的其他部分在教科書裏面卻被反復地提及,只是用來滿足舊的C89規範的要求,或者為了適應20世紀90年代計算機硬件的限制。限制越少,我們寫代碼的效率就越高,如果你能從刪除代碼並去掉那些沒用的冗余中獲得快樂,那麽本文適合你。

1.1 不需要明確地從main函數返回

我們首先去除一行幾乎在每個程序中都會遇到的代碼,並以此熱身。
所有程序必須有一個main函數,它的返回類型是int,因此在程序中必須要有下面這條代碼:
技術分享圖片
讀者會覺得其中必然有一條return語句,表示main函數所返回的整數值。但是,C標準知道這個返回值極少使用,所以不想麻煩我們。C標準要求:“……到達結束main函數的}之前返回一個0值。”[C99和C11,§5.1.2.2(3)]。也就是說,如果我們在程序最後一行沒有編寫return 0;,C會默認加上它。
想起來了嗎?當你運行完你的程序後,你可以使用echo$?來了解這個程序的返回值;你可以用它來查看你的main函數是否運行完畢,並返回了零值。

本文的最開始已經向讀者展示了這個版本的hello.c程序,可以看到其中只包括了一條#include語句和一行代碼[1]:
技術分享圖片
自己動手:檢查自己的程序,從main函數中刪除return 0這一行,觀察這樣做會不會有區別。

1.2 讓聲明的位置更靈活

讓我們回想一下以前閱讀劇本時的情況。在劇本的一開始是人物表,裏面列出了劇中出場的所有人物。但在開始閱讀劇本之前,一串人名列表對於我們而言並沒有太大的意義。因此,我們大多會跳過這一部分,直接閱讀正文的內容。當我們沈浸在情節中並忘了Benvolio是誰時,可以很方便地返回到劇本的最前面,看看人物表中對他的簡單介紹(哦,他是Romeo的朋友,Montague的侄子)。但是,前提是我們所閱讀的是紙版的劇本。如果我們是在屏幕上閱讀劇本,就必須搜索Benvolio最早出現的地方。

簡而言之,人物表對於讀者而言並不是非常有用的。在人物第一次出場時再介紹他們顯然更為合適。
我經常看到類似下面這樣的代碼:
技術分享圖片
首先是3~4行的介紹性材料(要看是不是算上空行),然後才是實質性的程序。
這是ANSI C89的復古風格,它要求所有的聲明都出現在代碼塊的頭部,這樣做的原因是早期編譯器的技術限制。現在,我們仍然必須聲明所有的變量,但可以盡量減輕作者和讀者的負擔,在第一次使用變量時才聲明它們:
技術分享圖片
在上述代碼中,聲明恰好出現在需要的位置,因此聲明的責任降低到最低限度,相當於在第一次使用變量前加上它的類型名。如果使用了彩色語法高亮顯示,這種聲明仍然很容易被發現(如果你的編輯器不支持彩色,最好還是換一種支持它的,這類編輯器數不勝數)。
在閱讀不熟悉的代碼時,我看到一個變量的第一直覺就是回過頭去看看它是在什麽地方聲明的。如果它是在第一次使用時或者在第一次使用之前的那一行被聲明的,我就可以省掉這幾秒瀏覽的時間。另外,根據應該使變量的作用域盡可能小的規則,我們要想方設法降低活動變量對前面代碼的依賴,這對於較長的函數而言是非常重要的。
在上面這個例子中,聲明出現在它們各自代碼塊的起始處,然後是非聲明性的代碼行。這正是這個例子所顯示的結果,我們可以自由地混合聲明行和非聲明行。
我把denom的聲明放在函數的頭部,但我們也可以把它移動到循環的內部(因為它只是在循環內部使用)。我們可以信任編譯器能夠充分地理解,而不會浪費時間和精力在每次循環叠代時對這個變量進行銷毀和重新分配[盡管在理論上會這樣,參見C99和C11,6.8(3)]。站在索引的角度,它應該和循環同時結束。因此,把它的作用域限制在循環之內是非常自然的做法。
這種新語法會拖慢程序嗎?
不會。
編譯器所執行的第1個步驟是把代碼解析為獨立於語言的內部表示形式。這樣gcc(GNU編譯器集合)在解析步驟的最後才能夠產生C、C++、ADA和FORTRAN的可兼容目標文件,它們看上去都是相同的。因此,C99為了閱讀方便而在語法上所提供的便利一般在可執行文件被生成之前就已經被抽掉了。
同時,運行程序的目標設備所看到的只是經過編譯的機器指令,因此不管源代碼所遵循的是C89、C99還是C11標準,對它來說是沒有區別的。
在運行時設置數組的長度
除了把聲明放在任意位置外,你也可以在運行時分配數組,並根據聲明之前的計算結果來確定它的長度。
同樣,這個方法在以前是不允許的。25年前,我們要麽必須在編譯時知道數組的長度,要麽使用malloc。
例如,假設想創建一組線程,但線程的數量是用戶通過命令行設置的。舊式的方法就是通過atoi(argv[1])(也就是把第1個命令行參數轉換為一個整數)從用戶那裏獲取數組的長度。接著,在運行時確定了長度之後,就分配一個具有正確長度的數組。
技術分享圖片
我們可以寫得更緊湊些:
技術分享圖片
後面的寫法由於行數少,所以更不容易出錯,它看上去像是聲明一個數組,而不是對內存寄存器進行初始化。我們必須釋放手工分配的數組,但可以簡單地把自動分配的數組放下不管,它在程序離開特定的作用域後會被自動清理[2]。

1.3 減少類型轉換

在20世紀七八十年代,malloc返回的是一個char*指針,我們必須對它的結果進行類型轉換(除非是為字符串分配內存),其形式類似:
技術分享圖片
現在不需要這樣做了,因為malloc向我們返回的是一個void指針,編譯器可以很方便地將它自動轉換為任何類型。最簡單的轉換方式是聲明一個具有正確類型的新變量。例如,必須接受一個void指針為參數的函數,一般可以用下面這樣的形式開始:技術分享圖片
在更普遍的情況下,如果把一種類型的數據項賦值給另一種類型的變量是合法的,即使我們沒有指定顯式的類型轉換,C也會為我們完成這個任務。如果它對於給定的類型是不合法的,就必須編寫一個函數設法完成轉換。這對於C++而言並不正確,後者對類型更為依賴,因此需要顯式地指定每個類型轉換。
另外還有兩個原因支持使用C的類型轉換語法把一個變量從一種類型轉換為另一種類型。
首先,當兩個數相除時,一個整數除以另一個整數總是返回一個整數,因此下面這兩條語句都是正確的:技術分享圖片
第2個除法是許多錯誤的來源。這個錯誤很容易修正:如果i是個整數,則i + 0.0就是與這個整數匹配的浮點數。不要忘了括號,這樣就簡單地解決了問題。如果常量2是個整數,那麽2.0或簡單的2.就是浮點數。因此,下面這些變型都是可行的:技術分享圖片
我們也可以使用類型轉換的形式:
技術分享圖片
站在美學的角度,我傾向於加零的形式。當然,讀者也可以選用轉換為double的形式。但是每次在使用“/”操作符時都要養成其中一種習慣,因為這是許多錯誤的來源(並不僅僅限於C,許多其他語言也堅持int/int的結果應該是int類型,而不會自動予以修正)。
其次,數組的索引必須是整數。這是規定[C99和C11,§6.5.2.1(1)],如果發送了一個浮點數的索引,gcc就會報錯。因此,我們可能必須去轉換,即使我們知道在當前情況下實際提供的總是一個整數值的表達式。技術分享圖片
從上面可以看到,盡管存在少數合理的理由需要類型轉換,但我們也可以選擇避免使用類型轉換的語法:加上0.0或為數組索引聲明一個整數變量。
這不僅僅是為了減少書寫混亂的問題。你的編譯器為你進行類型檢查並相應地發出一些警告,但是一個顯式的轉換就是對編譯器說:別管我,我知道我正在做什麽!例如,考慮下面的程序,試圖設置list[7]=12,但是犯了兩次典型的錯誤,應該用一個指針指向的值,但是這裏卻直接使用了指針的值。
技術分享圖片

1.4 枚舉和字符串

枚舉的出發點是好的,但是它走上了歧路。
它的優點是足夠清楚:整數值並不容易記憶,因此當我們在代碼中使用一個簡短的整數列表時,最好對它們進行命名。下面是一種甚至更糟的方法,在不使用enum關鍵字的情況下用#define進行定義:
技術分享圖片
使用enum,我們可以把上面這4行代碼縮減為1行,並且調試器更容易知道EAST的含義。下面是對#define序列的改進:
技術分享圖片
但是,現在全局空間中有了5個新符號:directions、NORTH、SOUTH、EAST和WEST。
為了讓枚舉發揮作用,它一般必須是全局的(在一個將被整個項目中多次包含的一個頭文件中聲明)。例如,我們常常在函數庫的公共頭文件中看到枚舉類型的typedef聲明。創建全局變量會產生相應的責任。為了盡可能地減少名字空間的沖突,函數庫的作者會使用像GCONVERTERRORNOTABSOLUTE_PATH這樣的名字,或者是相對簡潔的CblasConjTrans。
此刻,單純而感性的想法就此破滅。我不想手工輸入這些紊亂的名字,對它們的使用頻率之少使我在每次使用之前都必須進行查閱(尤其是其中有許多是不常用的錯誤值或輸入標誌,因此要相隔相當長的一段時間才會再次使用)。另外,全大寫的寫法看上去像有人在向你吼叫。
我個人的習慣是使用單個字符,用‘t‘來標記轉置,用‘p‘來表示路徑錯誤。我覺得這已經具備足夠的提醒作用。事實上,我覺得‘p‘的方案在拼寫上也比那些采用全大寫的方案容易記憶得多,並且它不需要在名字空間中添加新項。
在提到老生常彈的效率問題之前,記住枚舉一般是個整數,而char事實上只是C對於單字節的說法。因此,對枚舉進行比較時,很可能需要比較16個狀態位甚至更多。但是如果使用了char,只需要比較8位。因此,即使在速度問題相當重要的情況下,使用單個char的方案也勝於枚舉。
我們有時候需要對標誌進行組合。當我們使用系統調用open打開一個文件時,我們可能需要發送ORDWR|OCREAT,這是兩個枚舉的逐位組合。我們很可能不會非常頻繁地直接使用open,而是使用POSIX的fopen,後者更為友好。它並沒有使用枚舉,而是使用了一個單字母或雙字母的字符串(例如"r"或"r+")來表示某個文件是可讀的、可寫的、同時可讀寫的等。
在這個上下文環境中,我們知道"r"表示讀取。即使沒有記住這些約定,在使用過幾次fopen之後,我們也可以對這些標記了如指掌。反之,對於CblasTrans或CblasTranspose,我每次在使用之前都必須進行查閱。
枚舉當然也有優點,它表示一個小型的固定符號集,因此如果我們誤輸入了其中一個,編譯器就會停止,並迫使我們修正打字錯誤。對於字符串,在運行時之前是無法知道輸入錯誤的。但反過來,字符串並不是小型的固定符號集,因此我們可以更輕松地對枚舉集進行擴展。例如,我曾經看到過一個錯誤處理程序,將它自身提供給其他系統所使用,前提是新系統所產生的錯誤要與原系統的枚舉所包含的錯誤匹配。如果這些錯誤是短字符串,其他人對此進行擴展也是很簡單的。
有一些理由支持使用枚舉:有時候我們有一個數組,沒理由把它作為結構,但是它又需要使用命名元素;或者在完成內核層次的工作時,為位模式提供名字具有重要意義。但是,當枚舉用於表示一個簡短的選項列表或者一個簡短的錯誤代碼列表時,單字符或者短字符串同樣可以完成任務,同時又不至於擾亂名字空間和用戶的記憶。
本文摘自《C程序設計新思維(第2版)》
技術分享圖片
深入解析C語言特性
塑造編程新思維

本書展現了傳統C語言教科書所不具有的最新的相關技術。全書分為開發環境和語言兩個部分,從編譯、調試、測試、打包、版本控制等角度,以及指針、語法、文本、結構、面向對象編程、函數庫等方面,對C程序設計的核心知識進行查缺補漏和反思。本書鼓勵讀者放棄那些對大型機才有意義的舊習慣,拿起新的工具來使用這門與時俱進的簡潔語言。
本書適合有一定基礎的C程序員和C語言學習者閱讀,也適合想要深入理解C語言特性的讀者參考。
作者簡介:

Ben Klemens 自從於加州理工學院獲得社會科學博士後,就一直從事統計分析和人口的計算機輔助建模工作。他的觀點是,寫代碼一定應該是趣味橫生的,並先後非常愉快地為布魯金斯學會、世界銀行、美國國家精神健康中心等機構寫過分析和建模代碼(主要是C代碼)。他作為布魯金斯學會的非常駐研究員,與自由軟件基金會一道,做了很多工作來確保有創意的程序員擁有保留其作品使用權的權利。他目前為美國聯邦政府工作。

延伸推薦
技術分享圖片
點擊關鍵詞閱讀更多新書:
Python|機器學習|Kotlin|Java|移動開發|機器人|有獎活動|Web前端|書單
技術分享圖片
在“異步圖書”後臺回復“關註”,即可免費獲得2000門在線視頻課程;推薦朋友關註根據提示獲取贈書鏈接,免費得異步圖書一本。趕緊來參加哦!
點擊閱讀原文,查看本書更多信息
掃一掃上方二維碼,回復“關註”參與活動!
技術分享圖片
點擊閱讀原文,查看《C程序設計新思維(第2版)》

教科書不應該再過多介紹的C語言語法