?? 第9章 數 組.txt
字號:
for (p = a; p<&a[MAX]; ++p )
{
x=*p;
/* do aomething with x * /
}
這兩種方式有什么區別呢?兩種方式中的初始情況和遞增運算是相同的,作為循環條件的比較表達式也是相同的(下文中將進一步討論這一點)。區別在于“x=a[]”和“x=*p”,前者要確定a[i]的地址,因此需要將i和類型x的大小相乘后再與數組a中第一個元素的地址相加;
后者只需間接引用指針p。間接引用是快速的,而乘法運算卻比較慢。
這是一種“微效率”現象,它可能對程序的總體效率有影響,也可能沒有影響。對方式a來說,如果循環體中的操作是將數組中的元素相加,或者只是移動數組中的元素,那么每次循環中大部分時間就消耗在使用數組下標上;如果循環體中的操作是某種I/O操作,或者是函數調用,那么使用數組下標所消耗的時間是微不足道的。
在有些情況下,乘法運算的開銷會降低。例如,當類型x的大小為1時,經過優化就可以將乘法運算省去(一個值乘以1仍然等于這個值);當類型x的大小是2的冪時(此時類型x通常是系統固有類型),乘法運算就可以被優化為左移位運算(就象一個十進制的數乘以10一樣)。
在方式b中,每次循環都要計算&a[MAX],這需要多大代價呢?這和每次計算a[i]的代價相同嗎?答案是不同,因為在循環過程中&a[MAX]是不變的。任何一種合格的編譯程序都只會在循環開始時計算一次&a[MAX],而在以后的每次循環中重復使用這次計算所得的值。
在編譯程序確認在循環過程中a和MAX都不變的前提下,方式b和以下代碼的效果是相同的:
/* how the compiler implements version (b) */
X *temp = &a[MAX]; /* optimization */
for (p = a; p< temp; ++p )
{
x =*p;
/*do something with x * /
}
遍歷數組元素還可以有另外兩種方式,即以遞減而不是遞增的順序遍歷數組元素。對按順序打印數組元素這樣的任務來說,后兩種方式沒有什么優勢,但是對數組元素相加這樣的任務來說,后兩種方式比前兩種方式更好。通過下標并且以遞減順序遍歷數組元素的方式(方式c)如下所示(人們通常認為將一個值和。比較的代價要比將一個值和一個非零值比較的代價小:
/* version (c) */
for (i = MAX - 1; i>=0; --i)
{
x=a[i];
/* do aomcthing with x * /
}
通過指針并以遞減順序遍歷數組元素的方式(方式d)如下所示,其中作為循環條件的比較表達式顯得很簡潔:
/* version (d) */
for (p = &a[MAX - 1]; p>=a; --p )
{
x =*P;
/*do something with x * /
}
與方式d類似的代碼是很常見的,但不是絕對正確的,因為循環結束的條件是p小于a,而這有時是不可能的(見9.3)。
通常人們會認為“任何合格的能優化代碼的編譯程序都會為這4種方式產生相同的代碼”,但實際上許多編譯程序都沒能做到這一點。筆者曾編寫過一個測試程序(其中類型x的大小不是2的冪,循環體中的操作是一些無關緊要的操作),并用4種差別很大的編譯程序編譯這個程序,結果發現方式b總是比方式a快得多,有時要快兩倍,可見使用指針和使用下標的效果是有很大差別的(有一點是一致的,即4種編譯程序都對&a[MAX]進行了前文提到過的優化)。
那么在遍歷數組元素時,以遞減順序進行和以遞增順序進行有什么不同呢?對于其中的兩種編譯程序,方式c和方式d的速度基本上和方式a相同,而方式b明顯是最快的(可能是因為其比較操作的代價較小,但是否可以認為以遞減順序進行要比以遞增順序進行慢一些呢?);
對于其中的另外兩種編譯程序,方式c的速度和方式a基本相同(使用下標要慢一些),但方式d的速度比方式b要稍快一些。
總而言之,在編寫一個可移植性好、效率高的程序時,為了遍歷數組元素,使用指針比使用下標能使程序獲得更快的速度;在使用指針時,應該采用方式b,盡管方式d一般也能工作,但編譯程序為方式d產生的代碼可能會慢一些。
需要補充的是,上述技巧只是一種細微的優化,因為通常都是循環體中的操作消耗了大部分運行時間,許多C程序員往往會舍本求末,忽視這種實際情況,希望你不要犯相同的錯誤。
請參見:
9.2可以使用數組后第一個元素的地址嗎?
9.3為什么要小心對待位于數組后面的那些元素的地址呢?
9.6 可以把另外一個地址賦給一個數組名嗎?
不可以,盡管在一個很常見的特例中好象可以這樣做。
數組名不能被放在賦值運算符的左邊(它不是一個左值,更不是一個可修改的左值)。一個數組是一個對象,而它的數組名就是指向這個對象的第一個元素的指針。
如果一個數組是用extern或static說明-的,則它的數組名是在連接時可知的一個常量,你不能修改這樣一個數組名的值,就象你不能修改7的值一樣。
給數組名賦值是毫無根據的。一個指針的含義是“這里有一個元素,它的前后可能還有其它元素”,一個數組名的含義是“這里是一個數組中的第一個元素,它的前面沒有數組元素,并且只有通過數組下標才能引用它后面的數組元素”。因此,如果需要使用指針,就應該使用指針。
有一個很常見的特例,在這個特例中,好象可以修改一個數組名的值:
void f(chara[12])
{
++a; /*legal!*/
}
秘密在于函數的數組參數并不是真正的數組,而是實實在在的指針,因此,上例和下例是等價的:
void f(char *a)
{
++a; /*certainlylegal*/
}
如果你希望上述函數中的數組名不能被修改,你可以將上述函數寫成下面這樣,但為此你必須使用指針句法:
void{(char *const a)
{
++a; /*illegal*/
}
在上例中,參數a是一個左值,但它前面的const關鍵字說明了它是不能被修改的。
請參見:
9. 4在把數組作為參數傳遞給函數時,可以通過sizeof運算符告訴函數數組的大小嗎?
9.7 array_name和&array_name有什么不同?
前者是指向數組中第一個元素的指針,后者是指向整個數組的指針。
注意;筆者建議讀者讀到這里時暫時放下本書,寫一下指向一個含MAX個元素的字符數組的指針變量的說明。提示:使用括號。希望你不要敷衍了事,因為只有這樣你才能真正了解C語言表示復雜指針的句法的奧秘。下文將介紹如何獲得指向整個數組的指針。
數組是一種類型,它有三個要素,即基本類型(數組元素的類型),大小(當數組被說明為不完整類型時除外),數組的值(整個數組的值)。你可以用一個指針指向整個數組的值:
char a[MAX]; /*arrayOfMAXcharacters*/
char *p; /*pointer to one character*/
/*pa is declared below*/
pa=&al
p=a; /* =&a[0] */
在運行了上述這段代碼后,你就會發現p和pa的打印結果是一個相同的值,即p和pa指向同一個地址。但是,p和pa指向的對象是不同的。
以下這種定義并不能獲得一個指向整個數組的值的指針:
char *(ap[MAX]);
上述定義和以下定義是相同的,它們的含義都是“ap是一個含MAX個字符指針的數組”;
char *ap[MAX];
9.8 為什么用const說明的常量不能用來定義一個數組的初始大小?
并不是所有的常量都可以用來定義一個數組的初始大小,在C程序中,只有C語言的常量表達式才能用來定義一個數組的初始大小。然而,在C++中,情況有所不同。
一個常量表達式的值在程序運行期間是不變的,并且是編譯程序能計算出來的一個值。在定義數組的大小時,你必須使用常量表達式,例如,你可以使用數字:
char a[512];
或者使用一個預定義的常量標識符:
#define MAX 512
/*... */
char a[MAX];
或者使用一個sizeof表達式:
char a[sizeof(structcacheObject)];
或者使用一個由常量表達式組成的表達式:
char buf[sizeof(struct cacheObject) *MAX];
或者使用枚舉常量。
在C中,一個初始化了的constint變量并不是一個常量表達式:
int max=512; /* not a constant expression in C */
char buffer[max]; /* notvalid C */
然而,在C++中,用const int變量定義數組的大小是完全合法的,并且是C++所推薦的。盡管這會增加C++編譯程序的負擔(即跟蹤const int變量的值),而C編譯程序沒有這種負擔,但這也使C++程序擺脫了對C預處理程序的依賴。
請參見;
9.1數組的下標總是從0開始嗎?
9.2可以使用數組后面第一個元素的地址嗎?
9.9 字符串和數組有什么不同?
數組的元素可以是任意一種類型,而字符串是一種特殊的數組,它使用了一種眾所周知的確定其長度的規則。
有兩種類型的語言,一種簡單地將字符串看作是一個字符數組,另一種將字符串看作是一種特殊的類型。C屬于前一種,但有一點補充,即C字符串是以一個NUL字符結束的。數組的值和數組中第一個元素的地址(或指向該元素的指針)是相同的,因此通常一個C字符串和一個字符指針是等價的。
一個數組的長度可以是任意的。當數組名用作函數的參數時,函數無法通過數組名本身知道數組的大小,因此必須引入某種規則。對字符串來說,這種規則就是字符串的最后一個字符是ASCII字符NUL('\0')。
在C中,int類型值的字面值可以是42這樣的值,字符的字面值可以是‘*’這樣的值,浮點型值的字面值可以是4.2el這樣的單精度值或雙精度值。
注意:實際上,一個char類型字面值是一個int類型字面值的另一種表示方式,只不過使用了一種有趣的句法,例如當42和'*'都表示char類型的值時,它們是兩個完全相同的值。然而,在C++中情況有所不同,C++有真正的char類型字面值和char類型函數參數,并且通常會更仔細地區分char類型和int類型。
,整數數組和字符數組沒有字面值。然而,如果沒有字符串字面值,程序編寫起來就會很困難,因此C提供了字符串字面值。需要注意的是,按照慣例C字符串總是以NUL字符結束,因此C字符串的字面值也以NUL字符結束,例如,“six times nine”的長度是15個字符(包括NUL終止符),而不是你看得見的14個字符。
關于字符串字面值還有一條鮮為人知但非常有用的規則,如果程序中有兩條緊挨著的字符串字面值,編譯程序會將它們當作一條長的字符串字面值來對待,并且只使用一個NUL終止符。也就是說,“Hello,”world”和“Hello,world”是相同的,而以下這段代碼中的幾條字符串字面值也可以任意分割組合:
char message[]=
”This is an extremely long prompt\n”
”How long is it?\n”
”It's so long,\n”
”It wouldn't fit On one line\n”;
在定義一個字符串變量時,你需要有一個足以容納該字符串的數組或者指針,并且要保證為NUL終止符留出空間,例如,以下這段代碼中就有一個問題:
char greeting[12];
strcpy(greeting,”Hello,world”); /*trouble*/
在上例中,greeting只有容納12個字符的空間,而“Hello,world”的長度為13個字符(包括NUL終止符),因此NUL字符會被拷貝到greeting以外的某個位置,這可能會毀掉greetlng附近內存空間中的某些數據。再請看下例:
char greeting[12]=”Hello,world”;/*notastring*/
上例是沒有問題的,但此時greeting是一個字符數組,而不是一個字符串。因為上例沒有為NUL終止符留出空間,所以greeting不包含NUL字符。更好一些的方法是這樣寫:
char greeting[]=”Hello,world”;
這樣編譯程序就會計算出需要多少空間來容納所有內容,包括NUL字符。
字符串字面值是字符(char類型)數組,而不是字符常量(const char類型)數組。盡管ANSIC委員會可以將字符串字面值重新定義為字符常量數組,但這會使已有的數百萬行代碼突然無法通過編譯,從而引起巨大的混亂。如果你試圖修改字符串字面值中的內容,編譯程序是
不會阻止你的,但你不應該這樣做。編譯程序可能會選擇禁止修改的內存區域來存放字符串字面值,例如ROM或者由內存映射寄存器禁止寫操作的內存區域。但是,即使字符串字面值被存放在允許修改的內存區域中,編譯程序還可能會使它們被共享。例如,如果你寫了以下代碼(并且字符串字面值是允許修改的):
char *p="message";
char *q="message";
p[4]='\0'; /* p now points to”mess”*/
編譯程序就會作出兩種可能的反應,一種是為p和q創建兩個獨立的字符串,在這種情況下,q仍然是“message”;一種是只創建一個字符串(p和q都指向它),在這種情況下,q將變成“mess”。
注意:有人稱這種現象為“C的幽默”,正是因為這種幽默,絕大多數C程序員才會整天被自己編寫的程序所困擾,難得忙里偷閑一次。
請參見:
9.1 數組的下標總是從0開始嗎?
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -