映月读书网 > 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