免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開(kāi)APP
userphoto
未登錄

開(kāi)通VIP,暢享免費(fèi)電子書(shū)等14項(xiàng)超值服

開(kāi)通VIP
關(guān)于C的思考

關(guān)于C的思考

Cong Wang
May, 2006

 

Network Engineering Department

Institute of Post and Telecommunications, Xi'an, P.R.China

引言

    C語(yǔ)言結(jié)合了匯編的所有威力,它的抽象程度碰巧既滿(mǎn)足了程序員的要求, 又容易實(shí)現(xiàn)。因其獨(dú)特的靈活性和強(qiáng)大的可移植性,系統(tǒng)程序員和黑客們更是對(duì)它鐘愛(ài)有加。無(wú)疑,C語(yǔ)言獲得了空前的成功,在某種程度上甚至比UNIX還要成功。然而,“C語(yǔ)言就像一把刻刀,簡(jiǎn)單,鋒利,并且在技師手中非常有用。和任何鋒利的工具一樣,C會(huì)傷到那些不能駕馭它的人。” [8]C語(yǔ)言靈活性的背后,有著很多的要注意和避免的地方,一不小心你就會(huì)陷入bug的泥潭。所以也無(wú)怪乎只有C語(yǔ)言才有像《The C Puzzle Book》,《Obfuscated C and Other Mysteries》,《C Traps and Pitfalls》之類(lèi)的書(shū)。下面的文章不是對(duì)那些書(shū)中提到的問(wèn)題的重復(fù),而是對(duì)其它一些細(xì)小的地方和部分C庫(kù)函數(shù)進(jìn)行討論。 大部分都會(huì)集中到標(biāo)準(zhǔn)C上,參考的標(biāo)準(zhǔn)是ISO C99,一小部分可能會(huì)涉及到具體平臺(tái)。

一. sizeofreturn

    sizeof不是函數(shù),而是一元操作符,沒(méi)必要給它后面的表達(dá)式加括號(hào)。但如果計(jì)算的是一種類(lèi)型的大小,sizeof就需要加一個(gè)圓括號(hào),但這不是說(shuō)它是一個(gè)函數(shù)。 return不是函數(shù),而只是關(guān)鍵字,也不需要圓括號(hào)。還要特別注意的是sizeof操作符“返回”的是無(wú)符號(hào)整數(shù)(size_t),如果把它的“返回值”和一個(gè)int類(lèi)型比較,先要把它轉(zhuǎn)化成有符號(hào)整數(shù)。另外,Unix系統(tǒng)調(diào)用read/write的返回類(lèi)型是ssize_t,而不是size_tssize_t代表signed size。

二. 0.0, 0, '/0' vs NULL,NUL

    NULL表示的是空指針,它和任何非空指針值都不相等。而NUL就是'/0',表示的是值為0的空字符。雖然0.0, 0, '/0', NULL都是完全由0比特組成,但是它們的長(zhǎng)度不同。0.0是雙精度,通常是8個(gè)字節(jié);'/0'是字符常量,通常只有1個(gè)字節(jié);NULL表示空指針,它的長(zhǎng)度由系統(tǒng)決定,也就是系統(tǒng)中儲(chǔ)存一個(gè)內(nèi)存地址所需的字節(jié)數(shù);0表示整數(shù),一般是4個(gè)字節(jié)。它們的類(lèi)型不同,在用它們給變量賦值時(shí)應(yīng)該小心,有必要時(shí)進(jìn)行強(qiáng)制轉(zhuǎn)換。注意:C語(yǔ)言不保證char總是8個(gè)比特,也有7個(gè)或9個(gè)比特的字符!雖然沒(méi)有規(guī)定short是16個(gè)比特,int是32個(gè)比特,但是在目前的所有構(gòu)架上都是如此。 long并不總和int一致,它與機(jī)器字長(zhǎng)一致,也就是說(shuō)在32位機(jī)上它是4個(gè)字節(jié),而在64位機(jī)上是8個(gè)字節(jié)。 long long是擴(kuò)展類(lèi)型,并不是每個(gè)編譯器都支持,在32位機(jī)上它是8個(gè)字節(jié)。

三. 關(guān)于可移植性

    C語(yǔ)言在可移植性方面設(shè)計(jì)得非常好,而且又不失高效率,這也是GTK+和Linux Kernel 都青睞C的重要原因。但是,用C語(yǔ)言編寫(xiě)的程序并不能保證能“一次編寫(xiě),到處運(yùn)行”,即使你使用的是ANSI C。原因很簡(jiǎn)單,因?yàn)镃語(yǔ)言的設(shè)計(jì)初衷是為了編寫(xiě)UNIX方便,難免帶點(diǎn)“底層特性”,不同機(jī)器上C的實(shí)現(xiàn)多少都有些不同。這實(shí)在讓人不安。

1.大尾(big endian)vs.小尾(little endian)

    歷史上,關(guān)于此曾有一場(chǎng)圣戰(zhàn)[9],最終以和平而告終。所謂大尾就是最高有效位(MSB)先被放置,依次再放其它。小尾恰好反過(guò)來(lái)。比如:數(shù)66563,被當(dāng)做32位int儲(chǔ)存時(shí),在大尾和小尾機(jī)器上分別如下:

Address	Big Endian	Little Endian0	00000000	000000111	00000001	000001002	00000100	000000013	00000011	00000000
IA-32構(gòu)架是小尾,而其它多數(shù)構(gòu)架是大尾。下面的程序可以測(cè)試機(jī)器是大尾還是小尾。
int x = 1;if (*(char *)&x == 1)    /* little endian */else    /* big endian */
字節(jié)順序在網(wǎng)絡(luò)傳輸中非常重要,因?yàn)榫W(wǎng)絡(luò)上的機(jī)器有不同的字節(jié)順序。為了方便,TCP/IP套接字為整數(shù)類(lèi)型定義了一個(gè)統(tǒng)一的網(wǎng)絡(luò)字節(jié)順序(大尾),也為此提供一套轉(zhuǎn)換函數(shù)。所以,要編寫(xiě)可移植的代碼,不要假設(shè)任何特定的字節(jié)順序。

2.位字段的分配

    “結(jié)構(gòu)和聯(lián)合中允許有定義為位字段(bit-field)的成員。位字段可以定義為int,signed int,unsigned int之一,并且可以給出附加的寬度值?!?strong>[3]中的程序可以確定你的硬件系統(tǒng)和編譯器處理位字段的方式:

#include <stdio.h>

typedef struct DEMO{
    unsigned int one:1;
    unsigned int two:3;
    unsigned int three:10;
    unsigned int four:5;
    unsigned int :2;
    unsigned int five:8;
    unsigned int six:8;
    }demo_type;

int main(void){
    int k;
    unsigned char* bptr;
    demo_type bit={1,5,513,17,129,0x81};

    printf("/nsizeof demo_type =%u/n",sizeof(demo_type));
    printf("initial values:bit=%u,%u,%u,%u,%u,%u/n",bit.one,bit.two,bit.three,bit.four,bit.five,bit.six);
    bptr=(unsigned char *)&bit;
    printf("hex dump of bit: %02x %02x %02x %02x %02x %02x %02x %02x /n",bptr[0],bptr[1],bptr[2],bptr[3],bptr[4],bptr[5],bptr[6],bptr[7]);
    bit.three=1023;
    printf("/nassign 1023 to bit.three: %u,%u,%u,%u,%u,%u/n",bit.one,bit.two,bit.three,bit.four,bit.five,bit.six);
    k=bit.two;
    printf("assign bit.two to k:k=%i/n",k);
    return 0;
    }
IA-32 GCC4.1.0上運(yùn)行結(jié)果如下:
sizeof demo_type =8
initial values:bit=1,5,513,17,129,129
hex dump of bit: 1b 60 24 10 81 00 00 00
assign 1023 to bit.three: 1,5,1023,17,129,129
assign bit.two to k:k=5
如上所示,GCC編譯器給位字段分配內(nèi)存時(shí)仍以字節(jié)為單位分配,所以分配了8個(gè)字節(jié)。在儲(chǔ)存位字段時(shí),編譯器是從右向左分配的,這當(dāng)然會(huì)因機(jī)器不同而異。C語(yǔ)言中,關(guān)于編譯器如何安排位字段的規(guī)定很少。確實(shí)存在某些種類(lèi)的分配單元,而且分配單元大小也取決于編譯器,但編譯器可以從高位或低位開(kāi)始分配位字段。要編寫(xiě)可移植的程序,不要假設(shè)位字段是怎樣分配的。

3.內(nèi)存對(duì)齊

     大多數(shù)情況下,你不必去關(guān)注內(nèi)存對(duì)齊,因?yàn)榫幾g器已經(jīng)為我們做好了。但是,如果你在編寫(xiě)編譯器,對(duì)齊問(wèn)題就是你關(guān)注的主要問(wèn)題之一了。因?yàn)閷?duì)齊具有明顯的優(yōu)點(diǎn)──提高了內(nèi)存訪(fǎng)問(wèn)速度,雖然有時(shí)會(huì)浪費(fèi)一些空間。而且,有的RISC體系上要求嚴(yán)格對(duì)齊,否則就會(huì)觸發(fā)異常,這對(duì)內(nèi)核程序員非常重要。即使在Intel這樣的CISC體系上,一些 SSE、SSE2指令對(duì)對(duì)齊還是要求很?chē)?yán)格的,比如:movdqa(SSE 指令),movapd(SSE2指令)。所以,我們還是“偷窺”一下內(nèi)存對(duì)齊。

    編譯器通常會(huì)為我們做好對(duì)齊的工作,但是,當(dāng)程序員在編譯器預(yù)期之外使用指針來(lái)訪(fǎng)問(wèn)數(shù)據(jù)時(shí),對(duì)齊就會(huì)出問(wèn)題。下面的程序[11]可能就不能正常工作。

char foo[10];
char *p = &foo[1];
unsigned long l = *(unsigned long *)p;
因?yàn)橹羔榩可能不是4的倍數(shù)。對(duì)齊問(wèn)題在結(jié)構(gòu)體中更為明顯。
struct foo_struct {        char foo;             /* 1 byte */        unsigned long baz;    /* 4 bytes */        unsigned short bar;   /* 2 bytes */        char foobar;          /* 1 byte */};
上面的結(jié)構(gòu)體在內(nèi)存中并不像你看到的那樣儲(chǔ)存,編譯器會(huì)在其中填充空為來(lái)實(shí)現(xiàn)對(duì)齊,baz的偏移可能會(huì)是4而不是1!解決的方法手工去填充或者重新安排結(jié)構(gòu)體成員順序。
struct foo_struct {        unsigned long baz;    /* 4 bytes */        unsigned short bar;   /* 2 bytes */        char foo;             /* 1 byte */        char foobar;          /* 1 byte */};
ISO C規(guī)定編譯器絕不能改變結(jié)構(gòu)體成員的順序,這工作只能由程序員來(lái)做。

4.字符集

    大多數(shù)系統(tǒng)使用ASCII或者Unicode來(lái)編碼字符,而Unicode是兼容ASCII的。所以,如果你只使用英文字符,很多時(shí)候你沒(méi)必要擔(dān)心字符的編碼問(wèn)題。但是,確實(shí)存在其它字符集,和上述兩種字符集不兼容,比如:DBCS字符集和IBM的EBCDIC字符集。下面的程序在使用EBCDIC字符集的機(jī)器上并不能正常工作!

for(i=0; i < strlen(foo); i++)
{if(foo[i]>=32 && foo[i] <=126) *(ptr++) = foo[i];}
C99只保證:所有位都是0的字節(jié)應(yīng)該在字符集中存在,它是用來(lái)結(jié)束字符串的空字符;26個(gè)大寫(xiě)英文字母及其小寫(xiě)字母, 10個(gè)十進(jìn)制數(shù)字,下面29個(gè)圖形字符,
! " # % & ' ( ) * + , - . / :; < = > ? [ / ] ^ _ { | } ~
空格字符,和表示垂直制表,水平制表,換頁(yè)的控制符,應(yīng)該在最基本的源字符集(source character set)和執(zhí)行字符集(execution character set)中; 0后面的數(shù)字字符的值都應(yīng)該比它前面一個(gè)大;每個(gè)字符都應(yīng)該是一個(gè)字節(jié)大??;源字符集中應(yīng)該有表示行結(jié)束的字符;執(zhí)行字符集中應(yīng)該有表示響鈴,退格,回車(chē)和換行的字符。超出此范圍的情況是未定義的。

四. 糟糕的“匈牙利表示法”

    多年前,匈牙利程序員Charles Simonyi設(shè)計(jì)了一種在變量名中加上特定的前綴來(lái)辨別變量類(lèi)型的命名方法,它的優(yōu)點(diǎn)很顯然,就是可以直接通過(guò)變量名來(lái)辨認(rèn)其類(lèi)型,而不用去查找它的定義。微軟后來(lái)采用了這種思想,可以在VC中看到大量的這種表示。但這不僅使變量的名字很古怪,很難記,而且還有個(gè)很大的缺點(diǎn),那就是可能會(huì)使改變變量類(lèi)型的工作變得十分艱巨。設(shè)想一下,我們?cè)谝粋€(gè)大型項(xiàng)目中定義了一個(gè)全局變量,經(jīng)過(guò)幾十多個(gè)函數(shù)使用后,我們突然發(fā)現(xiàn)這種類(lèi)型的字節(jié)數(shù)不夠用,我們不得不去改變它的類(lèi)型。好了,你得從頭開(kāi)始把它的名字都得改一遍!現(xiàn)在很多程序員都放棄了它,除了一些頑固的Windows程序員。關(guān)于變量的命名,Unix系統(tǒng)調(diào)用的“全部小寫(xiě)”風(fēng)格或許很值得借鑒,簡(jiǎn)單而優(yōu)雅;如果要使用大寫(xiě)字母,不妨學(xué)一下Qt的命名風(fēng)格。

五. 頭文件

1.文件包含宏

    有不少人認(rèn)為,在#include宏命令中<>和""是等價(jià)的,他們錯(cuò)了!在#include宏命令中,頭文件名稱(chēng)兩側(cè)如果是<>則說(shuō)明頭文件及其安裝在硬盤(pán)中編譯程序的標(biāo)準(zhǔn)庫(kù)區(qū)域(在Linux上,這個(gè)目錄是/usr/include);而如果是""則說(shuō)明頭文件保存在程序員自己的磁盤(pán)目錄中,而不是在標(biāo)準(zhǔn)系統(tǒng)目錄中的本地庫(kù)。這點(diǎn)應(yīng)該引起高度重視,并不是所有的C語(yǔ)言教程中都會(huì)明確指出這一點(diǎn),而忽視它的后果將是缺少相應(yīng)的頭文件。不幸的是,有的編譯器在缺少頭文件時(shí)并不給出警告,應(yīng)該常使用lint來(lái)進(jìn)一步檢查我們的程序。

2.內(nèi)容

    我們知道,在預(yù)處理階段,C預(yù)處理器把#include后面的頭文件拷貝到源代碼中去。所以,在頭文件中放入些什么內(nèi)容應(yīng)該值得注意。通常,我們把常量,類(lèi)型和函數(shù)的聲明都放入頭文件中。但要注意:千萬(wàn)不要把變量的聲明也放進(jìn)去了!否則可能會(huì)產(chǎn)生多次定義的變量!如果可以,“頭文件中最好不要再包含頭文件。多重包含是系統(tǒng)編程的禍根。在一個(gè)C源文件中把文件包含了5次以上去編譯并不少見(jiàn)。Unix的/usr/include/sys非常糟糕。 ”[10]#ifdef可能會(huì)解決點(diǎn)問(wèn)題,但是通常它也會(huì)被誤用。 C的標(biāo)準(zhǔn)庫(kù)里面有很多頭文件都定義了標(biāo)識(shí)自己的宏,在包含之前應(yīng)該先用#ifdef去檢查。比如,stdio.h中有這樣的宏:

#ifndef _STDIO_H
#define _STDIO_H
/*omit other code*/
#endif
這樣,我們就可以通過(guò)宏來(lái)檢查有沒(méi)有包含stdio.h這個(gè)頭文件了:
#ifdef _STDIO_H
/*if stdio.h has already been included...*/
#endif
不注意這一點(diǎn)的后果就是數(shù)百行無(wú)用的代碼被包含進(jìn)去,傳遞給詞法分析器,消耗大量寶貴的編譯時(shí)間。

六. 函數(shù)返回值

1.困惑

    有些編程語(yǔ)言不允許放棄函數(shù)的返回值,而C不是其中之一。我們通常都這樣像void類(lèi)型的函數(shù)一樣使用scanf函數(shù),即使我們知道它的原型是int,而且這沒(méi)有任何錯(cuò)誤!

...
scanf(...);
...

其實(shí)這也并不奇怪,你也不經(jīng)常這樣使用下面的語(yǔ)句嗎?

int x=9;
++x;
...

表達(dá)式++x也有它的值,但是你也沒(méi)有使用它的“返回”值(這里為10)!正如++x,scanf也是既有值,也有作用效果(把輸入的值放入某塊內(nèi)存中,而++x的作用效果是把x的值加一),我們只是用到它們的作用效果,而不使用它們的值。為了明確地表示不使用返回值,最好這樣來(lái)寫(xiě):

...
(void)scanf(...);
...

2.歷史遺留問(wèn)題

    因?yàn)?em>ISO C早期并沒(méi)有void類(lèi)型,它允許我們像void類(lèi)型那樣使用某些int類(lèi)型的函數(shù),編譯器放松了對(duì)這些函數(shù)原型的檢查,不僅如此而且如果你在函數(shù)標(biāo)題中的返回類(lèi)型字段保持空白,那么返回類(lèi)型缺省為int,這是ISO C為了維持與舊版本之間的兼容性。這一點(diǎn)應(yīng)該引起足夠重視,因?yàn)槿绻覀冞z漏原型或原型在第一次函數(shù)調(diào)用后出現(xiàn),編譯器可能使用第一次函數(shù)調(diào)用中的形參類(lèi)型構(gòu)建一個(gè)原型,返回類(lèi)型永遠(yuǎn)是int!這可能對(duì),當(dāng)然也可能不對(duì)。

 

七. 當(dāng)心calloc函數(shù)

    到底calloc分配內(nèi)存成功時(shí)到底做了一些什么,相信很多人都認(rèn)為它為數(shù)組分配了塊內(nèi)存,而且還把其所有數(shù)組元素初始化為0。這也難怪,因?yàn)楹芏郈語(yǔ)言教程中都是那么講的??墒?em>calloc真的像我們想的那樣做了嗎?答案是不確定的。[4]中第7.20.3.1節(jié),關(guān)于calloc描述后面的Note不知你是否注意到了。 Note是這樣的:"Note that this need not be the same as the representation of floating-point zero or a null pointer constant." 好了,這就對(duì)了。C語(yǔ)言不保證所有位為0是指針(null)還是浮點(diǎn)數(shù)(0.0)的零表示!如果可移植性對(duì)你真的很重要,那么不要倚賴(lài)calloc把變量初始化為0;如果數(shù)組元素是指針或浮點(diǎn)數(shù)(或者你正創(chuàng)建包含浮點(diǎn)數(shù)或指針的結(jié)構(gòu)體或聯(lián)合數(shù)組),則可以利用循環(huán)自己進(jìn)行初始化。初始化真的很重要,尤其是你對(duì)可移植性要求高時(shí),指針是否初始化為null,數(shù)組元素是否都被初始化等有時(shí)決定著程序的成敗,而查出這些錯(cuò)誤又是那么的困難。我們唯一能做的就是格外小心。

 

八. 奇怪的sprintf函數(shù)

    [4]中第7.19.6.6節(jié)關(guān)于sprintf函數(shù)的描述如下:

"#include <stdio.h>

int sprintf(char * restrict s,const char * restrict format,...);

sprintf函數(shù)等價(jià)于fprintf,除了輸出是寫(xiě)入一個(gè)數(shù)組(記為參數(shù)s)而不是一個(gè)流。NUL字符被寫(xiě)入所寫(xiě)字符組的結(jié)尾;它不被計(jì)入返回值。如果復(fù)制發(fā)生在重疊的對(duì)象之間,行為則是未定義的。"


這倒是沒(méi)什么奇怪的,可是有人曾想利用sprintf()給一個(gè)字符數(shù)組添加不同類(lèi)型的值:

sprintf(mystring, "%s%d%s%f", string, j, otherstring, d);

很可惜,它并不能真正地將其它類(lèi)型加入字符數(shù)組中(聽(tīng)起來(lái)像Matlab中的元胞數(shù)組),而是統(tǒng)統(tǒng)將它們轉(zhuǎn)化成字符后又寫(xiě)入的。

char tempstring[50]={0};
char* otherstring;
int d=5;
float j=3.14;

otherstring="other";
sprintf(tempstring,"%f%s%d",j,otherstring,d);
printf("%s/n",tempstring);
printf("%d/n",tempstring[13]);

把一種其它類(lèi)型的數(shù)據(jù)轉(zhuǎn)換成字符數(shù)組儲(chǔ)存,使用sprintf函數(shù)真的是一個(gè)不錯(cuò)的方式。然而,sprintf同樣是一個(gè)問(wèn)題函數(shù)。它致命的缺陷就是它并不知道寫(xiě)入的字節(jié)數(shù)是否超出了緩沖區(qū)的大??!

int showmsg(int line, unsigned int err, char* msg){
    char buf[100];
    if(msg==NULL)return -1;
    else
        sprintf(buf,"Err occurred in line %d, %u is %s.",line,err,msg);
    return 0;
    }

在上面的程序中,提示占了28個(gè)字節(jié),line可能會(huì)占10個(gè)字節(jié),err可能會(huì)占11個(gè)字節(jié)。也就是說(shuō),msg的大小不能超過(guò)50個(gè)字節(jié)!我們從sprintf的返回值中得不到任何buf是否夠用的信息。這很危險(xiǎn),而且?guī)缀鯖](méi)有安全的使用它的方式!
    Windows上提供了一個(gè)_snprintf函數(shù),它的原型是:

int _snprintf(char* buffer, size_t count, const char* format[,argument]...);

sprintf不同的是,當(dāng)緩沖區(qū)不夠用時(shí),_snprintf會(huì)返回一個(gè)負(fù)數(shù),但它并不保證目的緩沖區(qū)以NUL結(jié)尾,你必須手工去做。這倒是挺好用,遺憾的是,_snprintf并不是ISO C99的一部分。

 

九. strncatstrncpy函數(shù)并不可靠

    “strcpystrcat是不安全的,應(yīng)該用strncpystrncat來(lái)代替?!边@是很多C程序員掛在嘴邊的話(huà)。其實(shí),說(shuō)這句話(huà)的人往往也不清楚strncpystrncat是如何工作的,而這往往會(huì)更糟糕!關(guān)于strncpy最大的誤解就是:它會(huì)用NUL(或'/0')來(lái)結(jié)束目的字符串。而實(shí)際上僅當(dāng)源字符串的長(zhǎng)度小于參數(shù)n時(shí)上面的那句話(huà)才正確,這時(shí)你還要當(dāng)心它會(huì)用NUL來(lái)填充空缺位。當(dāng)拷貝源字符串的一部分時(shí),正確的做法是使用strncpy之后,自己手工添加NUL來(lái)結(jié)束字符串。更準(zhǔn)確的說(shuō)是,沒(méi)有必要去手工結(jié)束一個(gè)static字符串或者通過(guò)calloc分配的字符數(shù)組,因?yàn)樗鼈冊(cè)诜峙鋾r(shí)就被初始化為0。關(guān)于strncat最大的誤用就是錯(cuò)誤地使用長(zhǎng)度參數(shù)n。雖然strncat保證以NUL來(lái)結(jié)束字符串,但你不應(yīng)該將NUL也計(jì)算在參數(shù)n內(nèi)。更重要的是,n不是目的字符串的長(zhǎng)度,而是可以利用的空間的大小,它通常是一個(gè)應(yīng)該被計(jì)算出來(lái)的變量,而不是一個(gè)常量。最后,你可能會(huì)說(shuō):“這些我都知道了。”但是,仍然不推薦你使用strncatstrncpy函數(shù),因?yàn)樗鼈冊(cè)愀獾脑O(shè)計(jì)??纯聪旅娴某绦蛴卸嗝绰闊?/p>

strncpy(path, homedir,sizeof(path) - 1);
path[sizeof(path) - 1] = '/ 0';
strncat(path, "/",sizeof(path) - strlen(path) - 1);
strncat(path, ".foorc",sizeof(path) - strlen(path) - 1);
len = strlen(path);

[6]中推薦使用strlcpystrlcat函數(shù)(關(guān)于這兩個(gè)函數(shù),可以查看在ftp.openbsd.org服務(wù)器上/pub/OpenBSD/src/lib/libc/string的目錄中的源代碼以及相關(guān)手冊(cè)),因?yàn)樗鼈兛偸潜WC以NUL結(jié)束字符串,它們都把目的字符數(shù)組的全部長(zhǎng)度作為參數(shù),而且它們返回的是程序員想得到的字符串的總長(zhǎng)度。它們的原型是:

size_t strlcpy(char *dst, const char *src, size_t size);
size_t strlcat(char *dst, const char *src, size_t size);

上面的程序可以用strlcpystrlcat來(lái)重寫(xiě),非常方便。

strlcpy(path, homedir, sizeof(path));
strlcat(path, "/", sizeof(path));
strlcat(path, ".foorc", sizeof(path));
len = strlen(path);

可惜,它們只是OpenBSD上的。希望未來(lái)的C標(biāo)準(zhǔn)委員會(huì)會(huì)把這兩個(gè)方便的函數(shù)加入標(biāo)準(zhǔn)之中。

 

十. 難以控制的strtok函數(shù)

    strtok函數(shù)真的不好用,不是嗎?在ISO C99中關(guān)于此函數(shù)的描述長(zhǎng)達(dá)6條(見(jiàn)[4]中第7.21.5.8節(jié)),連Linux Programmer's Manual中都說(shuō),永遠(yuǎn)不要使用這個(gè)函數(shù)(還有下面講提到的strtok_r函數(shù))。strtok雖然可以毫不含糊地將每一行分解為單個(gè)字段,并且沒(méi)有空字段,但也會(huì)使代碼不可重入。因?yàn)?em>strtok函數(shù)擁有一塊與之相關(guān)的靜態(tài)數(shù)據(jù)――如果傳遞給它一個(gè)空指針,它將“不斷搜索”(你不妨試著編寫(xiě)自己的strtok函數(shù)。)。這會(huì)帶來(lái)不小的麻煩!如果在一個(gè)遞歸函數(shù)中使用strtok就更應(yīng)該小心了,因?yàn)閹в徐o態(tài)數(shù)據(jù)的對(duì)象可能不能與遞歸函數(shù)默契合作。而且這個(gè)函數(shù)也沒(méi)有為我們提供抵達(dá)其內(nèi)部緩沖區(qū)的方式。同樣,如果在同一個(gè)程序中出現(xiàn)多個(gè)解析不同字符串的strtok函數(shù),對(duì)各自的字符串的解析就可能會(huì)互相干擾,不能正常工作。[2]建議試一下擴(kuò)展函數(shù)strsep,但是它不是可移植的。它與strtok功能相似,但它沒(méi)有使用靜態(tài)緩沖區(qū)。它的原型是:

#include <string.h>

char *strsep(char **stringp, const char *delim);

如果*stringpNULL,strsep什么都不做,安靜地返回NULL。否則,strsep*stringp中搜尋第一個(gè)出現(xiàn)的限定符delim中的任意字符,并且用NUL來(lái)結(jié)束它之前的字符串,讓*stringp指向它的后面一個(gè)字符。如果沒(méi)發(fā)現(xiàn)任何字符,*stringp被設(shè)為NULL。它返回的是解析出的字符串。
    另外,如果你在Unix/Linux系統(tǒng)上編寫(xiě)程序,當(dāng)兩個(gè)線(xiàn)程都調(diào)用strtok時(shí),還會(huì)導(dǎo)致線(xiàn)程不安全。POSIX定義了一個(gè)線(xiàn)程安全的函數(shù)strtok_r,用來(lái)替代strtok?!癬r”表示可重入(reentrant)。它的原型是:

#include <string.h>

char *strtok_r(char *restrict s, const char *restrict delim, char **restrict lasts);

除了額外的參數(shù)lasts之外,strtok_rstrtok表現(xiàn)類(lèi)似。lasts是用戶(hù)提供的一個(gè)指針,指向strtok_r用來(lái)存放下一次解析的起始地址的那個(gè)單元??伤膊辉趺春糜谩?偵希灰褂?em>strtok函數(shù),除非你對(duì)它非常了解。但在環(huán)境適宜時(shí),它仍不失為一個(gè)優(yōu)秀的工具。更進(jìn)一步說(shuō),除非是在受控的情況下,不要使用靜態(tài)變量。

 

十一. 避免分段錯(cuò)誤

    Segmentation Fault,也就是分段錯(cuò)誤,是我們編寫(xiě)C程序時(shí)很常見(jiàn)的錯(cuò)誤,尤其是在Linux上。然而很多人只是知道它與指針有關(guān),卻不知道引發(fā)錯(cuò)誤的細(xì)節(jié)。我想在這里簡(jiǎn)單地說(shuō)說(shuō)我的理解。我們知道操作系統(tǒng)管理虛擬內(nèi)存有兩種方式:一種是分段,另一種是分頁(yè)。分頁(yè)的意思是內(nèi)存被分成相等大小的頁(yè)面來(lái)管理,每個(gè)頁(yè)中包含有內(nèi)存的字。分段的意思是每個(gè)進(jìn)程都有一個(gè)所需大小的內(nèi)存段,段與段之間以空白的存儲(chǔ)塊相間隔。操作系統(tǒng)知道每一段的上界,并且每個(gè)段都以虛擬0地址開(kāi)始。當(dāng)程序訪(fǎng)問(wèn)一個(gè)內(nèi)存塊時(shí),它調(diào)用的是虛擬地址,但MMUMemory Management Unit)會(huì)把它映射到一個(gè)真實(shí)的地址。如果操作系統(tǒng)發(fā)現(xiàn)請(qǐng)求地址與有效的段地址都不匹配,它就會(huì)向進(jìn)程發(fā)送一個(gè)終止信號(hào)。 Segmentation Fault也就這樣產(chǎn)生了。如果Segmentation Fault發(fā)生,說(shuō)明你的程序中有錯(cuò)誤指針,內(nèi)存泄露,或者其它訪(fǎng)問(wèn)錯(cuò)誤內(nèi)存地址的錯(cuò)誤。請(qǐng)你再仔細(xì)地檢查你的指針,數(shù)組是否正確,一般是使用malloc分配了多少內(nèi)存就使用多少,不使用未分配內(nèi)存的指針進(jìn)行strcpy等操作。

十二. 再談命名

    在閱讀這一節(jié)之前,你必須了解ISO C99中的幾個(gè)概念。一個(gè)源文件及其包含的頭文件或源文件叫做預(yù)處理翻譯單元(preprocessing translation unit)。完成預(yù)處理后,之前的預(yù)處理翻譯單元就改叫翻譯單元(translation unit)。使在不同作用域中聲明的,或在同一作用域中聲明多次的一個(gè)標(biāo)識(shí)符指向同一個(gè)目標(biāo)或函數(shù),叫做鏈接(linkage)。在構(gòu)成整個(gè)程序的一系列的翻譯單元和庫(kù)中,同一個(gè)擁有外部鏈接(external linkage,外部指的是翻譯單元的外部)的標(biāo)識(shí)符(叫做外部名字(external name))的每次聲明都代表同一個(gè)目標(biāo)或函數(shù);在一個(gè)翻譯單元中,同一個(gè)擁有內(nèi)部鏈接(internal linkage)的標(biāo)識(shí)符的每次聲明也都代表同一個(gè)目標(biāo)或函數(shù)。宏名或沒(méi)有外部鏈接的標(biāo)識(shí)符叫做內(nèi)部名字(internal name)。 ISO C99規(guī)定:至少內(nèi)部名字的前31字符必須是唯一的,外部名字的前6個(gè)字符是必須唯一的,大小寫(xiě)可以不區(qū)分。所以,你在定義名字時(shí)一定要確保不與C預(yù)留的標(biāo)識(shí)符沖突。否則行為是未定義的。所以,下面的外部名字看起來(lái)不同,但它們對(duì)編譯器來(lái)說(shuō)是可能是相同的:

    cleared()clearerr(),CallOccasionally()calloc(),reallocationrealloc()

當(dāng)然了,可能你的編譯器能區(qū)分6個(gè)以上的字符,能區(qū)分大小寫(xiě)。但是上述規(guī)則還是值得遵循的。關(guān)于C和C++預(yù)留的標(biāo)識(shí)符,[7]中作了詳細(xì)的整理,是很好的參考。

十三. 可變參數(shù)

    標(biāo)準(zhǔn)庫(kù)stdarg.h提供了一種可移植的方式,讓程序員編寫(xiě)帶可變參數(shù)的函數(shù),就像printf??伤烤故窃趺磳?shí)現(xiàn)的呢?下面分析一下可變參數(shù)在IA-32平臺(tái)上的具體實(shí)現(xiàn)。一種實(shí)現(xiàn)如下:

#ifndef STDARG_H#define STDARG_Htypedef char* va_list;#define va_rounded_size(type) /(((sizeof (type) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))#define va_start(ap, v) /((void) (ap = (va_list) &v + va_rounded_size (v)))#define va_arg(ap, type) /(ap += va_rounded_size (type), *((type *)(ap - va_rounded_size (type))))#define va_end(ap) ((void) (ap = 0))#endif
內(nèi)部宏va_rounded_size計(jì)算指定類(lèi)型是int類(lèi)型的多少倍,結(jié)果向上取整,因?yàn)镮A-32中棧是4字節(jié)對(duì)齊的。在宏va_start中,v是你聲明的最后一個(gè)參數(shù),va_startap指向它后面的參數(shù),并且什么都不返回。宏va_arg訪(fǎng)問(wèn)參數(shù)列表中的下一個(gè)元素,讓ap先指向下一個(gè),再返回它的前一個(gè),即原來(lái)ap指向的參數(shù)。 va_endap置為零,什么都不返回。以這三個(gè)宏為接口,就能實(shí)現(xiàn)可變參數(shù)函數(shù)。

十四. 類(lèi)型修飾符

    C99中定義了三個(gè)類(lèi)型修飾符(type quali?er),分別是:const,volatile,restrict

    const的使用并不像你想像得那么簡(jiǎn)單。不能簡(jiǎn)單的把const認(rèn)為就是constant。const真正的意思是只讀(read-only),使用它是為了防止一些變量被修改,比如:字符串拷貝時(shí)的源字符串。const的位置很重要,比如const int* x;是定義一個(gè)指向只讀整數(shù)的指針,整數(shù)不能被修改而指針本身可以;而int const* x;是定義一個(gè)指向一個(gè)整數(shù)的只讀指針,整數(shù)可以被修改而指針不能。注意,“不能被修改” 不是說(shuō)永遠(yuǎn)不能被修改,而是不能通過(guò)這個(gè)符號(hào)修改,而通過(guò)別的可以。下面的程序是正確的:

const int a=10;
int *p;
p=(int*)&a;
(*p)++;
讓人吃驚的是,const char**char**并不兼容,[5]中很好地解釋了原因,這里就不再贅述。

    volatile關(guān)鍵字把變量標(biāo)記為可以改變而且沒(méi)有警告,它通知編譯器每次遇到被標(biāo)記的變量都需要重新加載,而不是儲(chǔ)存起來(lái)去訪(fǎng)問(wèn)它的拷貝。使用volatile的最好的例子莫過(guò)于處理硬件中斷,寄存器,和同步進(jìn)程共享變量。

    最難以理解的修飾符是restrict,C99標(biāo)準(zhǔn)第6.7.3.1中的對(duì)它的定義非常隱晦。其實(shí),restrict的作用就是限制一個(gè)指針對(duì)一塊內(nèi)存的訪(fǎng)問(wèn),進(jìn)一步說(shuō)就是如果一塊內(nèi)存區(qū)域通過(guò)一個(gè)受限制指針訪(fǎng)問(wèn),那么它就不能通過(guò)另一個(gè)受限指針訪(fǎng)問(wèn)??梢?jiàn),與前兩個(gè)不同,restrict只能用于指針。引入restrict的目的是確保同一塊內(nèi)存上沒(méi)有其它引用,讓編譯器更好地優(yōu)化指令,生成更有效的匯編代碼。


參考資料:

[1]《The C Programming Language,2nd Ed》
Brian W Kernighan and Dennis M Ritchie, Prentice Hall, 1988, ISBN 0-13-110362-8.
[2]《C Unleashed》
Richard Heathfield, Lawrence Kirby Etc.
[3]《Applied C: An Introduction and More》
Alice E.Fischer and David W.Eggert, McGraw Hill, ISBN 7-5053-6931-8.
[4]《ISO 1999 Programming languages-C》
[5]《Expert C Programming》
Peter van der Liden, Prentice Hall, ISBN 0-13-177429-8.
[6]《Strlcpy and strlcat - consistent, safe, string copy and concatenation》
Todd C. Miller and Theo de Raadt, 1999 USENIX Annual Technical Conference.
[7]《C Reserved Identifiers》
Stan Brown, 15 Sep 2003.
[8]《C Traps and Pitfalls》
Andrew Koening, ISBN 0-201-17928-8, Addison-Wesley.
[9]《On Holy Wars and A Plea for Peace》
Danny Cohen, IEEE Computer, 14(10):48-54, October 1981.
[10]《Notes on Programming in C》
Rob Pike, February 21, 1989.
[11]《Linux Kernel Development, Second Edition》
Robert Love, January 12, 2005, Sams Publishing, ISBN 0-672-32720-1.

本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶(hù)發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)。
打開(kāi)APP,閱讀全文并永久保存 查看更多類(lèi)似文章
猜你喜歡
類(lèi)似文章
C語(yǔ)言測(cè)試題的講解分析
字符數(shù)組,字符指針,Sizeof總結(jié)(轉(zhuǎn)載) - 小團(tuán)的日志 - 網(wǎng)易博客
【轉(zhuǎn)】C/C++中的Split函數(shù)(字符串自動(dòng)分割)
【騰訊C++面試題】如何才能獲得騰訊的offer?掌握這20道終身受益!
C/C+常見(jiàn)面試題整理
C語(yǔ)言 字符串常用函數(shù) 示例
更多類(lèi)似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長(zhǎng)圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服