一维字符数组和数值数组有如下两个重要区别。
(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