讀古今文學網 > C語言解惑 > 18.2 一維字符數組和指針 >

18.2 一維字符數組和指針

一維字符數組和數值數組有如下兩個重要區別。

(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