當(dāng)前位置:首頁 > 公眾號精選 > 嵌入式云IOT技術(shù)圈
[導(dǎo)讀]1、C陷阱與缺陷概述 C語言像一把雕刻刀,鋒利,并且在技師手中非常有用。和任何鋒利的工具一樣,C會傷到那些不能掌握它的人。本文介紹C語言傷害粗心的人的方法,以及如何避免傷害。 第一部分研究了當(dāng)程序被劃分為記號時會發(fā)生的問題。第二部分繼續(xù)研究了當(dāng)程

1、C陷阱與缺陷概述

C語言像一把雕刻刀,鋒利,并且在技師手中非常有用。和任何鋒利的工具一樣,C會傷到那些不能掌握它的人。本文介紹C語言傷害粗心的人的方法,以及如何避免傷害。

第一部分研究了當(dāng)程序被劃分為記號時會發(fā)生的問題。第二部分繼續(xù)研究了當(dāng)程序的記號被編譯器組合為聲明、表達式和語句時會出現(xiàn)的問題。第三部分研究了由多個部分組成、分別編譯并綁定到一起的C程序。第四部分處理了概念上的誤解:當(dāng)一個程序具體執(zhí)行時會發(fā)生的事情。第五部分研究了我們的程序和它們所使用的常用庫之間的關(guān)系。在第六部分中,我們注意到了我們所寫的程序也許并不是我們所運行的程序;預(yù)處理器將首先運行。最后,第七部分討論了可移植性問題:一個能在一個實現(xiàn)中運行的程序無法在另一個實現(xiàn)中運行的原因。

詞法分析器(lexical analyzer):檢查組成程序的字符序列,并將它們劃分為記號(token)一個記號是一個由一個或多個字符構(gòu)成的序列,它在語言被編譯時具有一個(相關(guān))統(tǒng)一的意義。

C程序被兩次劃分為記號,首先是預(yù)處理器讀取程序,它必須對程序進行記號劃分以發(fā)現(xiàn)標(biāo)識宏的標(biāo)識符。通過對每個宏進行求值來替換宏調(diào)用,最后,經(jīng)過宏替換的程序又被匯集成字符流送給編譯器。編譯器再第二次將這個流劃分為記號。

1.1 =不是==

C語言則是用=表示賦值而用==表示比較。這是因為賦值的頻率要高于比較,因此為其分配更短的符號。C還將賦值視為一個運算符,因此可以很容易地寫出多重賦值(如a = b = c),并且可以將賦值嵌入到一個大的表達式中。

1.2 & 和| 不是 && 和 ||

1.3 多字符記號

C語言參考手冊說明了如何決定:“如果輸入流到一個給定的字符串為止已經(jīng)被識別為記號,則應(yīng)該包含下一個字符以組成能夠構(gòu)成記號的最長的字符串” “最長子串原則”

1.4 例外

組合賦值運算符如+=實際上是兩個記號。因此,a + /* strange */ = 1和 a += 1是一個意思??雌饋硐褚粋€單獨的記號而實際上是多個記號的只有這一個特例。特別地,p - > a是不合法的。它和 p -> a不是同義詞。另一方面,有些老式編譯器還是將=+視為一個單獨的記號并且和+=是同義詞。

1.5 字符串和字符

包圍在單引號中的一個字符只是編寫整數(shù)的另一種方法。這個整數(shù)是給定的字符在實現(xiàn)的對照序列中的一個對應(yīng)的值。而一個包圍在雙引號中的字符串,只是編寫一個有雙引號之間的字符和一個附加的二進制值為零的字符所初始化的一個無名數(shù)組的指針的一種簡短方法。

使用一個指針來代替一個整數(shù)通常會得到一個警告消息(反之亦然),使用雙引號來代替單引號也會得到一個警告消息(反之亦然)。但對于不檢查參數(shù)類型的編譯器卻除外。

由于一個整數(shù)通常足夠大,以至于能夠放下多個字符,一些C編譯器允許在一個字符常量中存放多個字符。這意味著用'yes'代替"yes"將不會被發(fā)現(xiàn)。后者意味著“分別包含y、e、s和一個空字符的四個連續(xù)存儲器區(qū)域中的第一個的地址”,而前者意味著“在一些實現(xiàn)定義的樣式中表示由字符y、e、s聯(lián)合構(gòu)成的一個整數(shù)”。這兩者之間的任何一致性都純屬巧合。

2、句法缺陷

理解這些記號是如何構(gòu)成聲明、表達式、語句和程序的。

2.1 理解聲明

每個C變量聲明都具有兩個部分:一個類型和一組具有特定格式的、期望用來對該類型求值的表達式。 float *g(), (*h)();表示*g()和(h)()都是float表達式。由于()比綁定得更緊密,g()和(g())表示同樣的東西:g是一個返回指float指針的函數(shù),而h是一個指向返回float的函數(shù)的指針。

當(dāng)我們知道如何聲明一個給定類型的變量以后,就能夠很容易地寫出一個類型的模型(cast):只要刪除變量名和分號并將所有的東西包圍在一對圓括號中即可。

float *g();聲明g是一個返回float指針的函數(shù),所以(float *())就是它的模型。

(*(void(*)())0)();硬件會調(diào)用地址為0處的子程序(*0)(); 但這樣并不行,因為運算符要求必須有一個指針作為它的操作數(shù)。另外,這個操作數(shù)必須是一個指向函數(shù)的指針,以保證的結(jié)果可以被調(diào)用。需要將0轉(zhuǎn)換為一個可以描述“指向一個返回void的函數(shù)的指針”的類型。(Void(*)())0在這里,我們解決這個問題時沒有使用typedef聲明。通過使用它,我們可以更清晰地解決這個問題:

typedef void (*funcptr)();// typedef funcptr void (*)();指向返回void的函數(shù)的指針

(*(funcptr)0)();//調(diào)用地址為0處的子程序

2.2 運算符并不總是具有你所想象的優(yōu)先級

綁定得最緊密的運算符并不是真正的運算符:下標(biāo)、函數(shù)調(diào)用和結(jié)構(gòu)選擇。這些都與左邊相關(guān)聯(lián)。

接下來是一元運算符。它們具有真正的運算符中的最高優(yōu)先級。由于函數(shù)調(diào)用比一元運算符綁定得更緊密,你必須寫(*p)()來調(diào)用p指向的函數(shù);*p()表示p是一個返回一個指針的函數(shù)。轉(zhuǎn)換是一元運算符,并且和其他一元運算符具有相同的優(yōu)先級。一元運算符是右結(jié)合的,因此*p++表示*(p++),而不是(*p)++。在接下來是真正的二元運算符。其中數(shù)學(xué)運算符具有最高的優(yōu)先級,然后是移位運算符、關(guān)系運算符、邏輯運算符、賦值運算符,最后是條件運算符。需要記住的兩個重要的東西是:

  • 1.所有的邏輯運算符具有比所有關(guān)系運算符都低的優(yōu)先級。
  • 2.移位運算符比關(guān)系運算符綁定得更緊密,但又不如數(shù)學(xué)運算符。

乘法、除法和求余具有相同的優(yōu)先級,加法和減法具有相同的優(yōu)先級,以及移位運算符具有相同的優(yōu)先級。還有就是六個關(guān)系運算符并不具有相同的優(yōu)先級:==和!=的優(yōu)先級比其他關(guān)系運算符要低。在邏輯運算符中,沒有任何兩個具有相同的優(yōu)先級。按位運算符比所有順序運算符綁定得都緊密,每種與運算符都比相應(yīng)的或運算符綁定得更緊密,并且按位異或(^)運算符介于按位與和按位或之間。三元運算符的優(yōu)先級比我們提到過的所有運算符的優(yōu)先級都低。這個例子還說明了賦值運算符具有比條件運算符更低的優(yōu)先級是有意義的。另外,所有的復(fù)合賦值運算符具有相同的優(yōu)先級并且是自右至左結(jié)合的具有最低優(yōu)先級的是逗號運算符。賦值是另一種運算符,通常具有混合的優(yōu)先級。

2.3 看看這些分號!

或者是一個空語句,無任何效果;或者編譯器可能提出一個診斷消息,可以方便除去掉它。一個重要的區(qū)別是在必須跟有一個語句的if和while語句中。另一個因分號引起巨大不同的地方是函數(shù)定義前面的結(jié)構(gòu)聲明的末尾,考慮下面的程序片段:

struct foo {
    int x;
}

f() {
    ...
}

在緊挨著f的第一個}后面丟失了一個分號。它的效果是聲明了一個函數(shù)f,返回值類型是struct foo,這個結(jié)構(gòu)成了函數(shù)聲明的一部分。如果這里出現(xiàn)了分號,則f將被定義為具有默認(rèn)的整型返回值[5]。

2.4 switch語句

C中的case標(biāo)簽是真正的標(biāo)簽:控制流程可以無限制地進入到一個case標(biāo)簽中??纯戳硪环N形式,假設(shè)C程序段看起來更像Pascal:

switch(color) {
case 1: printf ("red");
case 2: printf ("yellow");
case 3: printf ("blue");
}

并且假設(shè)color的值是2。則該程序?qū)⒋蛴ellowblue,因為控制自然地轉(zhuǎn)入到下一個printf()的調(diào)用。這既是C語言switch語句的優(yōu)點又是它的弱點。說它是弱點,是因為很容易忘記一個break語句,從而導(dǎo)致程序出現(xiàn)隱晦的異常行為。說它是優(yōu)點,是因為通過故意去掉break語句,可以很容易實現(xiàn)其他方法難以實現(xiàn)的控制結(jié)構(gòu)。尤其是在一個大型的switch語句中,我們經(jīng)常發(fā)現(xiàn)對一個case的處理可以簡化其他一些特殊的處理。

2.5 函數(shù)調(diào)用

和其他程序設(shè)計語言不同,C要求一個函數(shù)調(diào)用必須有一個參數(shù)列表,但可以沒有參數(shù)。因此,如果f是一個函數(shù),f(); 就是對該函數(shù)進行調(diào)用的語句,而f;什么也不做。它會作為函數(shù)地址被求值,但不會調(diào)用它[6]。

2.6 懸掛else問題

一個else總是與其最近的if相關(guān)聯(lián)。

3 連接

一個C程序可能有很多部分組成,它們被分別編譯,并由一個通常稱為連接器、連接編輯器或加載器的程序綁定到一起。由于編譯器一次通常只能看到一個文件,因此它無法檢測到需要程序的多個源文件的內(nèi)容才能發(fā)現(xiàn)的錯誤。

3.1 你必須自己檢查外部類型

假設(shè)你有一個C程序,被劃分為兩個文件。其中一個包含如下聲明:

int n;

一個包含如下聲明:

long n;

這不是一個有效的C程序,因為一些外部名稱在兩個文件中被聲明為不同的類型。然而,很多實現(xiàn)檢測不到這個錯誤,因為編譯器在編譯其中一個文件時并不知道另一個文件的內(nèi)容。因此,檢查類型的工作只能由連接器(或一些工具程序如lint)來完成;如果操作系統(tǒng)的連接器不能識別數(shù)據(jù)類型,C編譯器也沒法過多地強制它。那么,這個程序運行時實際會發(fā)生什么?這有很多可能性:

  • 1.實現(xiàn)足夠聰明,能夠檢測到類型沖突。則我們會得到一個診斷消息,說明n在兩個文件中具有不同的類型。

  • 2.你所使用的實現(xiàn)將int和long視為相同的類型。典型的情況是機器可以自然地進行32位運算。在這種情況下你的程序或許能夠工作,好像你兩次都將變量聲明為long(或int)。但這種程序的工作純屬偶然。

  • 3.n的兩個實例需要不同的存儲,它們以某種方式共享存儲區(qū),即對其中一個的賦值對另一個也有效。這可能發(fā)生,例如,編譯器可以將int安排在long的低位。不論這是基于系統(tǒng)的還是基于機器的,這種程序的運行同樣是偶然。

  • 4.n的兩個實例以另一種方式共享存儲區(qū),即對其中一個賦值的效果是對另一個賦以不同的值。在這種情況下,程序可能失敗。

這種情況發(fā)生的另一個例子出奇地頻繁。程序的某一個文件包含下面的聲明: char filename[] = "etc/passwd"; 而另一個文件包含這樣的聲明: char *filename; 盡管在某些環(huán)境中數(shù)組和指針的行為非常相似,但它們是不同的。在第一個聲明中,filename是一個字符數(shù)組的名字。盡管使用數(shù)組的名字可以產(chǎn)生數(shù)組第一個元素的指針,但這個指針只有在需要的時候才產(chǎn)生并且不會持續(xù)。在第二個聲明中,filename是一個指針的名字。這個指針可以指向程序員讓它指向的任何地方。如果程序員沒有給它賦一個值,它將具有一個默認(rèn)的0值(NULL)([譯注]實際上,在C中一個為初始化的指針通常具有一個隨機的值,這是很危險的?。?。

這兩個聲明以不同的方式使用存儲區(qū),它們不可能共存。

避免這種類型沖突的一個方法是使用像lint這樣的工具(如果可以的話)。為了在一個程序的不同編譯單元之間檢查類型沖突,一些程序需要一次看到其所有部分。典型的編譯器無法完成,但lint可以。

避免該問題的另一種方法是將外部聲明放到包含文件中。這時,一個外部對象的類型僅出現(xiàn)一次[7]。

4 語義缺陷

4.1 表達式求值順序

一些C運算符以一種已知的、特定的順序?qū)ζ洳僮鲾?shù)進行求值。但另一些不能。例如,考慮下面的表達式: a < b && c < dC語言定義規(guī)定a < b首先被求值。如果a確實小于b,c < d必須緊接著被求值以計算整個表達式的值。但如果a大于或等于b,則c < d根本不會被求值。要對a < b求值,編譯器對a和b的求值就會有一個先后。但在一些機器上,它們也許是并行進行的。

C中只有四個運算符&&、||、?:和,指定了求值順序。&&和||最先對左邊的操作數(shù)進行求值,而右邊的操作數(shù)只有在需要的時候才進行求值。而?:運算符中的三個操作數(shù):a、b和c,最先對a進行求值,之后僅對b或c中的一個進行求值,這取決于a的值。,運算符首先對左邊的操作數(shù)進行求值,然后拋棄它的值,對右邊的操作數(shù)進行求值[8]。

C中所有其它的運算符對操作數(shù)的求值順序都是未定義的。事實上,賦值運算符不對求值順序做出任何保證。出于這個原因,下面這種將數(shù)組x中的前n個元素復(fù)制到數(shù)組y中的方法是不可行的:

i = 0;
while(i < n)
    y[i] = x[i++];

其中的問題是y[i]的地址并不保證在i增長之前被求值。在某些實現(xiàn)中,這是可能的;但在另一些實現(xiàn)中卻不可能。另一種情況出于同樣的原因會失?。?/p>

i = 0;
while(i < n)
    y[i++] = x[i];

而下面的代碼是可以工作的:

i = 0;
while(i < n) {
    y[i] = x[i];
    i++;
}

當(dāng)然,這可以簡寫為:

for(i = 0; i < n; i++)
    y[i] = x[i];

4.2 &&、||和!運算符

4.3 下標(biāo)從零開始

在很多語言中,具有n個元素的數(shù)組其元素的號碼和它的下標(biāo)是從1到n嚴(yán)格對應(yīng)的。但在C中不是這樣。具有n個元素的C數(shù)組中沒有下標(biāo)為n的元素,其中的元素的下標(biāo)是從0到n - 1。因此從其它語言轉(zhuǎn)到C語言的程序員應(yīng)該特別小心地使用數(shù)組:

int i, a[10];
for(i = 1; i <= 10; i++)
    a[i] = 0;

4.4 C并不總是轉(zhuǎn)換實參

下面的程序段由于兩個原因會失?。?/p>

double s;
s = sqrt(2);
printf("%g\n", s);

第一個原因是sqrt()需要一個double值作為它的參數(shù),但沒有得到。第二個原因是它返回一個double值但沒有這樣聲明改正的方法只有一個:

double s, sqrt();
s = sqrt(2.0);
printf("%g\n", s);

C中有兩個簡單的規(guī)則控制著函數(shù)參數(shù)的轉(zhuǎn)換:

  • (1)比int短的整型被轉(zhuǎn)換為int;
  • (2)比double短的浮點類型被轉(zhuǎn)換為double。所有的其它值不被轉(zhuǎn)換。確保函數(shù)參數(shù)類型的正確性是程序員的責(zé)任。

因此,一個程序員如果想使用如sqrt()這樣接受一個double類型參數(shù)的函數(shù),就必須僅傳遞給它float或double類型的參數(shù)。常數(shù)2是一個int,因此其類型是錯誤的。

當(dāng)一個函數(shù)的值被用在表達式中時,其值會被自動地轉(zhuǎn)換為適當(dāng)?shù)念愋?。然而,為了完成這個自動轉(zhuǎn)換,編譯器必須知道該函數(shù)實際返回的類型。沒有更進一步聲名的函數(shù)被假設(shè)返回int,因此聲名這樣的函數(shù)并不是必須的。然而,sqrt()返回double,因此在成功使用它之前必須要聲明

這里有一個更加壯觀的例子:

main() {
    int i;
    char c;
    for(i = 0; i < 5; i++) {
        scanf("%d", &c);
        printf("%d", i);
    }
    printf("\n");
}

表面上看,這個程序從標(biāo)準(zhǔn)輸入中讀取五個整數(shù)并向標(biāo)準(zhǔn)輸出寫入0 1 2 3 4。實際上,它并不總是這么做。譬如在一些編譯器中,它的輸出為0 0 0 0 0 1 2 3 4。

為什么?因為c的聲明是char而不是int。當(dāng)你令scanf()去讀取一個整數(shù)時,它需要一個指向一個整數(shù)的指針。但這里它得到的是一個字符的指針。scanf()并不知道它沒有得到它所需要的:它將輸入看作是一個指向整數(shù)的指針并將一個整數(shù)存貯到那里。由于整數(shù)占用比字符更多的內(nèi)存,這樣做會影響到c附近的內(nèi)存。

附近確切是什么是編譯器的事;在這種情況下這有可能是i的低位。因此,每當(dāng)向c中讀入一個值,i就被置零。當(dāng)程序最后到達文件結(jié)尾時,scanf()不再嘗試向c中放入新值,i才可以正常地增長,直到循環(huán)結(jié)束。

往期精彩

C語言的數(shù)組為什么要從0開始編號

C語言數(shù)組結(jié)合位運算實戰(zhàn)-位移與查表

分批讀取文件中數(shù)據(jù)的程序流程及其C代碼實現(xiàn)

ESP8266透傳:利用STM32f103zet6發(fā)送數(shù)據(jù)到HTTP服務(wù)器

覺得本次分享的文章對您有幫助,隨手點[在看]并轉(zhuǎn)發(fā)分享,也是對我的支持。

免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫毥谦F公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關(guān)鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險,如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應(yīng)對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅持高質(zhì)量發(fā)展策略,塑強核心競爭優(yōu)勢...

關(guān)鍵字: 通信 BSP 電信運營商 數(shù)字經(jīng)濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術(shù)學(xué)會聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(shù)(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉