一维字符数组和数值数组有如下两个重要区别。
(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 ic c d d e e f f g gf fghig ghiabcdefghi
下面是有错误的程序,找出错误并改正之,使其满足上述输出。
#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 10static 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 10static 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 字符串:qazwsource:qazw,k=4buffer:qazwbuffer:qazw输入字符串:erdfc字符数:5 字符串:erdfcsource:erdfc,k=5buffer:qazwerdfcbuffer:qazwerdfc
实验满的情况,第1次输入“12345678”8个字符,缓冲区还有2个字符空间。第2次输入“ABC”3个字符,所以只能写入2个。写满缓冲区,调flushbuffer函数输出缓冲区内容“12345678AB”并将指针置缓冲区首地址buffer,然后从头写入最后一个字符C。因为并没有清除内容,所以只是改写缓冲区第1个单元的内容,即将字符1改写为字符C,所以现在缓冲区的内容是“C2345678AB”,但缓冲区还有9个字符空间。可以增加for循环的次数验证这一点。下面是运行示范,注意有一次缓冲区已满信息。
输入字符串:12345678字符数:8 字符串:12345678source:12345678,k=8buffer:12345678buffer:12345678输入字符串:ABC字符数:3 字符串:ABCsource:ABC,k=2buffer:12345678AB已满:12345678ABsource:C,k=1buffer:C2345678ABbuffer:C2345678AB