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