一維字符數組和數值數組有如下兩個重要區別。
(1)字符數組需要一個結束符,所以定義長度為n的字符數組能存儲的有效字符只有n-1個。
(2)字符數組能作為整體輸出。
18.2.1 字符數組的偏移量
【例18.5】要求程序的輸出結果如下:
a a b b c c d d e e f f g g h h i i c c d d e e f f g g f fghi g ghi abcdefghi
下面是有錯誤的程序,找出錯誤並改正之,使其滿足上述輸出。
#include <stdio.h> int main ( ) { char *p , a[10]=\"abcdefghi\" ; int i ; p = a ; for (i=0 ; a[i]==\'0\' ; i++ ) printf (\"%c %c \" ,a[i] ,* (a+i )); printf (\"n\" ); p=&a[5] ; for (i=-2 ; i<3 ; i++ ) printf (\"%c %c \" ,p[i] ,* (p+i )); printf (\"n\" ); for (i=0 ; i<2 ; i++ ,p++ ) printf (\"%c %sn\" ,*p ,p ); p=a ; printf (\"pn\" ); return 0 ; }
第1個for語句中有兩個錯誤。字符串的結束符是\'\0\',不是\'0\'。判斷要用「!=」,即
for (i=0 ; a[i] !=\'\0\' ; i++ )
也可以使用i判斷,雖然「i<10」也能正確運行,但正確的形式是「i<9」,即
for (i=0 ; i<9 ; i++ )
從第2個循環語句的對稱輸出可知p[0]為字符e,應該是a[4]。即將「p=&a[5];」改為
p = &a[4] ;
第3個循環語句是從f開始輸出一個字符,然後輸出從f開始的整體字符串,所以要調整指針的指向。可以增加一句
p = &a[5] ;
也可以在for語句中置p的初始值,即修改for循環語句為
for (i=0 , ++p ; i<2 ; i++ , p++ )
最後輸出的是a的全部內容,而語句「printf(\"pn\");」是將p作為字符輸出,即輸出「p」。可以改為
printf ( p ); printf ( \"n\" );
或者使用如下的等效語句。
printf (\"%sn\" , p ); // 完整的參考程序 #include <stdio.h> int main ( ) { char *p , a[10]=\"abcdefghi\" ; int i ; p = a ; for (i=0 ; a[i] !=\'\0\' ; i++ ) printf (\"%c %c \" ,a[i] ,* (a+i )); printf (\"n\" ); p=&a[4] ; for (i=-2 ; i<3 ; i++ ) printf (\"%c %c \" ,p[i] ,* (p+i )); printf (\"n\" ); for (i=0 ,++p ; i<2 ; i++ ,p++ ) printf (\"%c %sn\" ,*p ,p ); p=a ; printf (p ); printf (\"n\" ); return 0 ; }
由此可見,字符數組的特點與數值數組的一樣,數組下標從0開始,而指針則可正可負。數值數組沒有結束符,而字符數組有結束符。這就決定了數值數組不能作為整體輸出,而字符數組不僅可以作為整體輸出,而且結束符還可以作為編程的依據。
【例18.6】下面程序計算字符串的長度,程序對嗎?
#include <stdio.h> int main ( ) { char *p , *s ; s = \"abcdefghijklmnopqrstuvwxyz\" ; p=s ; while ( *p != \'\0\' ) p++ ; printf ( \"%dn\" , (p-s+1 ) ); return 0 ; }
循環結束條件是到結束符為止,使用p-s+1的計算是不對的,因為字符的有效長度比數組的少1個。將其改為p-s即可。字符串長度為26個。這正是非對稱邊界的優點。
18.2.2 字符數組不對稱編程綜合實例
【例18.7】假設用數組buffer[N]模擬一個緩衝區,將另一個數組a的內容寫入緩衝區。使用不對稱方法編程,模擬演示使用緩衝區的兩種主要情況:一種是分兩次寫入緩衝區,緩衝區尚沒寫滿;另一種也是分兩次寫入緩衝區,第1次沒寫滿,第2次的數據大於緩衝區剩餘的空間。
【解答】為了便於演示,將緩衝區定義的小一點(N=10)。將接收數據的字符數組定義為a[16](大於緩衝區),以便方便演示。假設設計一個函數bufwrite,用以將長度不等的輸入數據送到緩衝區buffer(看做能容納N個字符的內存)中,當這塊內存被「填滿」時,就將緩衝區的內容輸出。考慮使用如下方法聲明緩衝區和定義指針變量。
#define N 10 static char buffer[N] ; static char* bufptr ;
可以讓指針bufptr始終指向緩衝區中最後一個已佔用的字符。不過,這裡使用「不對稱邊界」編程,所以讓它指向緩衝區中第1個未佔用的字符。根據「不對稱邊界」的慣例,使用語句
*bufptr++ = c ;
就把輸入字符c放到緩衝區中,然後指針bufptr遞增1,又指向緩衝區中第1個未佔用的字符。因此,可以用語句
Bufptr = &buffer[0] ;
聲明緩衝區為空,或者直接寫成:
Bufptr = buffer ;
甚至在聲明時直接使用如下語句:
static char* bufptr = buffer ;
在任何時候,緩衝區中已存放的字符數都是bufptr-buffer,將這個表達式與N比較,就可以判斷緩衝區是否已滿。當緩衝區全部「填滿」時,表達式bufptr-buffer就等於N,而緩衝區中未被佔用的字符數為N-(bufptr-buffer)。假設函數bufwrite初步具有如下形式:
void bufwrite (char *p , int n ) { while (-- n > = 0 ) { if (bufptr == &buffer[N] ) flushbuffer (); // 輸出緩衝區內容並將指針置緩衝區首地址 *bufptr ++ = *p++ ; // 向緩衝區寫入 }
指針變量p指向要寫入緩衝區的第1個字符,也就是數組a的首地址。n是一個整數,代表將要寫入緩衝區的字符數,也就是數組a的字符數。重複執行表達式「--n>=0」,共循環n次,寫入n個字符。
如果n>N,當寫入N個字符時,緩衝區已滿,調用flushbuffer函數,將緩衝區內容輸出並執行bufptr=buffer,將指針指向緩衝區中第1個未佔用的字符,以便繼續將後續的字符寫入緩衝區。比較語句
if (bufptr == &buffer[N] )
中引用了不存在的地址&buffer[N]。雖然緩衝區buffer沒有buffer[N]這個元素,但是卻可以引用這個元素的地址&buffer[N]。buffer中實際不存在的「溢界」元素的地址位於buffer所佔內存之後,這個地址可以用來進行賦值和比較(引用該元素的值則是非法的)。
函數flushbuffer的定義如下所示,其實它的定義也很簡單。
void flushbuffer () { printf (\"%sn\" , buffer ); // 輸出已滿緩衝區內容 bufptr = buffer ; // 緩衝區滿將指針置緩衝區首地址 }
不過,一次移動一個字符太麻煩,可以有更好的辦法。例如,如果n<N,可以將n個字符一次連續移入緩衝區。如果2×N>n>N,可以先移動N個字符,輸出緩衝區內容後,再移動剩下的字符。其實,庫函數memcpy能夠一次移動k個字符,這裡定義一個自己的函數,目的是在函數里面加入調試信息以方便觀察運行過程。
void memcpy1 (char *dest ,const char *source ,int k ) { printf (\"source :k=%d ,%sn\" ,k ,source ); // 調試語句 while (--k >= 0 ) *dest++ = *source++ ; printf (\"buffer :%sn\" ,buffer ); // 調試語句 }
需要計算一次能移動的次數k。這要根據緩衝區還有多少空間rem來計算。
rem = N - (bufptr - buffer ); // 求緩衝區尚有空間大小 k = n > rem ? rem : n ; // 求一次移動的字符數
一次移動的個數k由緩衝區空間rem和要移動的字符數n決定。如果n<rem,則緩衝區裝得下n個字符,即k=n。如果n>rem,則只能移入rem個字符,即k=rem。
這需要重寫bufwrite函數。
void bufwrite (char *p , int n ) { while (n > 0 ) { int k ,rem ; if (bufptr == &buffer[N] ) // 若緩衝區滿,輸出緩衝區內容 flushbuffer (); // 並將指針置緩衝區首地址 rem = N - (bufptr - buffer ); // 求緩衝區尚有空間大小 k = n > rem ? rem : n ; // 求一次移動的字符數k memcpy1 (bufptr , p , k ); // 一次移動k 個字符 bufptr += k ; // 將指向緩衝區的指針前移k 個字符 n-=k ; // 緩衝區減少k 個字符容量 p+=k ; // 將輸入字符串的指針前移k 個字符 } }
這裡的n就是要寫入的字符串的個數,也就是字符串數組a中的字符數目,所以要先計算n值。為了演示連續寫入,使用for循環語句即可。
for (i=0 ;i<2 ;i++ ){ scanf (\"%s\" ,a ); n=strlen (a ); // 字符數 bufwrite (a ,n ); // 將要寫入緩衝區的數組a 及字符個數n 作為參數 }
下面給出加入調試信息以便演示操作過程的完整程序。
// 注意調試語句 #include <stdio.h> #include <string.h> #define N 10 static char buffer[N] ; static char* bufptr = buffer ; void memcpy1 (char *dest ,const char *source ,int k ) { printf (\"source :%s ,k=%dn\" ,source ,k ); // 調試語句 while (--k >= 0 ) *dest++ = *source++ ; printf (\"buffer :%sn\" ,buffer ); // 調試語句 } void flushbuffer () { printf (\" 已滿:%sn\" ,buffer ); // 輸出已滿緩衝區的內容 bufptr = buffer ; // 將指針置緩衝區首地址 } void bufwrite (char *p ,int n ) { while (n > 0 ) { int k ,rem ; if (bufptr == &buffer[N] ) // 若緩衝區滿,輸出緩衝區內容 flushbuffer (); // 並將指針置緩衝區首地址 rem = N - (bufptr - buffer ); // 求緩衝區尚有空間大小 k = n > rem ? rem : n ; // 求一次移動的字符數k memcpy1 (bufptr , p , k ); // 一次移動K 個字符 bufptr += k ; // 將指向緩衝區的指針前移k 個字符 n-=k ; // 減少緩衝區k 個字符容量 p+=k ; // 將輸入字符串的指針前移k 個字符 } } int main () { char a[16] ; int n ,i ; for (i=0 ;i<2 ;i++ ){ printf (\" 輸入字符串:\" ); scanf (\"%s\" ,a ); n=strlen (a ); // 字符數 printf (\" 字符數:%d 字符串:%sn\" ,n ,a ); // 調試信息 bufwrite (a ,n ); printf (\"buffer :%sn\" ,buffer ); // 調試信息 } return 0 ; }
設N=10,第1次輸入「qazw」4個字符,第2次輸入「erdfc」5個字符,兩次共9個,緩衝區尚剩1個字符空間,其內容為兩次輸入的拼接「qazwerdfc」,運行示範如下。
輸入字符串: qazw 字符數:4 字符串:qazw source :qazw ,k=4 buffer :qazw buffer :qazw 輸入字符串: erdfc 字符數:5 字符串:erdfc source :erdfc ,k=5 buffer :qazwerdfc buffer :qazwerdfc
實驗滿的情況,第1次輸入「12345678」8個字符,緩衝區還有2個字符空間。第2次輸入「ABC」3個字符,所以只能寫入2個。寫滿緩衝區,調flushbuffer函數輸出緩衝區內容「12345678AB」並將指針置緩衝區首地址buffer,然後從頭寫入最後一個字符C。因為並沒有清除內容,所以只是改寫緩衝區第1個單元的內容,即將字符1改寫為字符C,所以現在緩衝區的內容是「C2345678AB」,但緩衝區還有9個字符空間。可以增加for循環的次數驗證這一點。下面是運行示範,注意有一次緩衝區已滿信息。
輸入字符串:12345678 字符數:8 字符串:12345678 source :12345678 ,k=8 buffer :12345678 buffer :12345678 輸入字符串:ABC 字符數:3 字符串:ABC source :ABC ,k=2 buffer :12345678AB 已滿:12345678AB source :C ,k=1 buffer :C2345678AB buffer :C2345678AB