§8.4 字符串的指针和指向字符串的指针变量
8.4.1 字符串的表现形式
在C程序中,可以用两种方法实现一个字符串。
1. 用字符数组实现。
[例8.11]
void main(void)
{static char string [ ]="I Love China!";
printf("%s\n",string);
}
运行时输出: I Love China!
和前面介绍的数组属性一样,string是数组名,它代表字符数组的首地址,(见图8.17)。string[4]代表数组中序号为4的元素(v),实际上string[4]就是*(string+4),string+4是指向字符“v”指针。
2. 用字符指针实现。
可以不定义字符数组,而定义一个字符指针。用字符指针指向字符串中的字符。
[例8.12]
void main(void)
{char *string="I Love China!";
printf("%s\n",string);
}
在这里没有定义字符数组,但C语言对字符串常量是按字符数组处理的,实际上在内存开辟了一个字符数组用来存放字符串数组。在程序中定义了一个字符指针变量string。并把字符串首地址(即存放字符串的字符数组的首地址)赋给它(见图8.18)。有人认为string是一个字符串变量,以为定义时把"I Love China!"赋给该字符串变量,这是不确切的。定义string的部分:
char *string="I Love China!";
等价于下面两行:
char *string;
string="I Love China!";
可以看到:string被定义为一个指针变量,它指向字符型数据,请注意只能指向一个字符变量或其它字符类型数据,不能同时指向多个字符数据,更不是把"I Love China!"这些字符存放到string中。只是把"I Love China!"的首地址赋给指针变量string(不是把字符串赋给*string)。因此不要认为上述定义行等价于:
char *string;
*string="I Love China!";
在输出时,用
printf("%s\n",string);
%s表示输出一个字符串,给出字符指针变量名string,则系统先输出它所指向的一个字符数据,然后自动使string加1,使之指向下一个字符,然后再输出一个字符,……,如此直到遇到字符串结束标志‘\0’为止。注意,在内存中,字符串的最后被自动加了一个‘\0’(如图8.18所示),因此在输出时能确定字符串的终止位置。
通过字符数组名或字符指针变量可以输出一个字符串。而对一个数值型数组,是不能企图用数组名输出它的全部元素的。如:
int i[10]
:
printf("%d\n",i);
是不行的,只能逐个元素输出。显然,可以把字符串看作为一个整体来处理,可以对一个字符串进行整体的输入输出。
对字符串中字符的存取,可以用下标方法,也可以用指针方法。
[例8.13]将字符串a复制得字符串b。
void main(void)
{char a[ ]= "I am a boy. ",b[20];
int i;
for(i=0;*(a+i)!=‘\0’;i++)
*(b+i)=*(a+i);
*(b+i)=‘\0’;
printf("string a is:%s\n",a);
printf("string b is: ");
for(i=0;b[i]!=‘\0’;i++)
printf("%c",b[i]);
printf("\n");
}
程序运行结果为:
string a is:I am a boy.
string b is:I am a boy.
程序中a和b都定义为字符数组,可以用地址方法表示数组元素。在for语句中,先检查a[i]是否为’\0’(今a[i]是以*(a+i)形式表示的)。如果不等于’\0’,表示字符串尚未处理完,就将a[i]的值赋给b[i],即复制一个字符。在for循环中将a串全部复制给了b串。最后还应将’\0’复制过去,故有:*(b+i)=‘\0’;
此时的i的值是字符串有效字符的个数n加1。第二个for循环中用下标法表示一个数组元素(即一个字符)。也可以设指针变量,用它的值的改变来指向字符串中的不同的字符。
[例8.14] 用指针变量来处理例8.13问题。
void main(void)
{char a[ ]= "I am a boy. ",b[20],*p1,*p2;
int i;
p1=a;p2=b;
for(;*p1!=‘\0’;p1++,p2++)
*p2=*p1;
*p2=‘\0’;
printf("string a is:%s\n",a);
printf("string b is: ");
for(i=0;b[i]!=‘\0’;i++)
printf("%c",b[i]);
printf("\n");
}
p1,p2是指针变量,它指向字符型数据,先使p1和p2的值分别为字符串a和b的首地址。*p1最初的值为‘i’,赋值语句“*p2=*p1;”的作用是将字符’I’(a串中第一个字符)赋给p2所指向的元素,直到 *p1的值为’\0’止。注意p1和p2的值是不断在改变的,程序必须保证使p1和p2同步移动。
8.4.2 字符串指针作函数参数
将一个字符串从一个函数传递到另一个函数,可以用地址传递的办法,即用字符数组名作参数或用指向字符串的指针作参数。在被调用的函数中可以改变字符串的内容,在主调函数中可以得到改变了的字符串。
[例8.15] 用函数调用实现字符串的复制。
(1)用字符数组作参数
void copy_string (char from[ ],char to[ ])
{int i=0;
while (from[i]!=‘\0’)
{to[i]=from[i];i++;}
to[i]=‘\0’;
}
void main(void)
{char a[ ]= "I am a teacher. ";
char b[ ]= "You are a student. ";
printf("string_a=%s\n string_b=%s\n",a,b);
copy_string(a,b);
printf("\nstring_a=%s\n string_b=%s\n",a,b);
}
程序运行结果如下:
string_a=I am a teacher.
string_b=You are a student.
string_a=I am a teacher.
string_b=I am a teacher.
a和b是字符数组。初值如图8.19(a)所示。copy_string函数的作用是将from[i]赋给to[i],直到from[i]的值为’\0’为止。在调用copy_string函数时,将a和b的首地址分别传递给形参数组form和to。因此from[i]和a[i]是同一个单元,to[i]和b[i]是同一个单元。程序执行完以后,b数组的内容如图8.19(b)所示。可以看出,由于b数组原来的长度大于a数组,因此在a数组复制到b数组后,未能全部覆盖b数组原有内容。b数组最后三个元素仍保留原状。在输出b时由于按%s(字符串)输出,遇’\0’即告结束,因此第一个’\0’后的字符不输出。如果不采取%s格式输出而用%c逐个字符输出是可以输出后面这些字符的。
在main函数中也可以不定义字符数组,而用字符型指针变量。main函数可改写如下:
void main(void)
{char *a=“I am a teacher.”;
char *b= “You are a student.”;
printf(“string_a=%s\n
string_b=%s\n”,a,b);
copy_string(a,b);
printf(“\nstring_a=%s\n
string_b=%s\n”,a,b);
}
与上面程序运行结果相同。
(2)形参用字符指针变量
程序如下:
void copy_string(char *from,char *to)
{
for(;*from!=‘\0’;from++,to++)
*to=*from;
*to=‘\0’;
}
void main(void)
{char *a="I am a teacher. ";
char *b= "You are a student. ";
printf("string_a=%s\n
string_b=%s\n",a,b);
copy_string(a,b);
printf("\nstring_a=%s\n string_b=%s\n",a,b);
}
形参form和to是字符指针变量。它们相当于例8.19中的p1和p2。算法也与例8.19完全相同。在调用copy_string时,将数组a的首地址传给from,把数组b的首地址传给to。在函数copy_string中的for循环中,每次将*from赋给*to,第1次就是将a数组中第1个字符赋给b数组的第1个字符。在执行from++和to++以后,from和to就分别指向a[1]和b[1]。再执行*to=*from,就将a[1]赋给b[1],……。最后将’\0’赋给*to,注意此时to指向哪个单元。
(3)对copy_string函数还可作简化
1. 将copy_string函数改写为:
void copy_string (char *from,char *to)
{
while ((*to=*from)!=‘\0’)
{to++;from++;}
}
请与上面一个程序对比。在本程序中将“*to=*from;”的操作放在while语句的表达式中,把赋值运算和判断是否为’\0’的运算放在表达式中,先赋值后判断。在循环体中to和from增值,指向下一个元素,……,直到*from的值为’\0’为止。
2. copy_string函数的函数体还可改为:
{
while((*to++=*from++)!=‘\0’);
}
把上面程序的to++和from++运算与*to=*from合并,它的执行过程是:先将*from赋给*to,然后使to和from增值。显然这又简化了。
3. 函数体还可写成:
{
while(*from!=‘\0’)
*to++=*from++;
*to=‘\0’;
}
当*from不为’\0’时,使*from赋给*to,然后使to和from增值。
字符可以用其ASCII代码来代替。例如:”ch=‘a’”可以用”ch=97”代替,”while(ch!=‘a’)”可以用”while(ch!=97)”代替。因此,”while(*from!=‘\0’)”可以用”while(*from!=0)代替(’\0’的ASCII代码为0)。而关系表达式”*from!=0”又可简化为”*from”,这是因为若*form的值不等于0,则表达式”*from”为真,同时”*from!=0”也为真。因此”while(*from!=0)”和”while(*from)”是等价的。所以函数体可简化为:
{while(*from)
*to++=*from++;
*to=‘\0’;}
4. 上面的while语句还可以进一步简化为下面的while语句:
while(*to++=*from++)
它与下面语句等价:
while((*to++=*from++)!=‘\0’);
将*from赋给*to,如果赋值后的*to值等于’\0’,则循环终止(’\0’已赋给*to)。
5. 函数体中while语句也可以改用for语句:
for(;(*to++=*from++)!=0;);
或 for(;*to++=*from;);
6. 也可用指针变量,函数copy_string可写为:
void copy_string( char from[ ],char to[ ])
{char *p1,*p2;
p1=from;p2=to;
while((*p2++=*p1++)!=‘\0’);
}
以上各种用法,变化多端,使用十分灵活,初看起来不太习惯,含义不直观。初学者会有些困难,也容易出错。但对C熟练之后,以上形式的使用是比较多的,读者应逐渐熟悉它,掌握它。
归纳起来,作为函数参数,有以下几种情况:
实 参 形 参
1. 数 组 名 数 组 名
2. 数 组 名 字符指针变量
3. 字符指针变量 字符指针变量
4. 字符指针变量 数 组 名
8.4.3 字符指针变量与字符数组
虽然用字符数组和字符指针变量都能实现字符串的存储和运算,但它们二者之间是有区别的,不应混为一谈,主要有以下几点:
1. 字符数组由若干个元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址(字符串的首地址),决不是将字符串放到字符指针变量中。
2. 赋初值的方式。对数组赋初值要用static存储类别,如
static str[ ]={ "I love China! ");
而对字符指针变量不必加static存储类型,如
char *a="I love China! ";
这是因为并没有对数组初始化,只是对指针变量初始化。
3. 赋值方式。对字符数组只能对各个元素赋值,不能用以下办法对字符数组赋值。
char str[14];
str="I love China! ";
而对字符指针变量,可以采用下面方法赋值:
char *a;
a="I love China! ";
但注意赋给a的不是字符,而是字符串的首地址。
4. 赋初值时,对以下的变量定义和赋初值:
char *a="I love China! ";
等价于:
char *a;
a="I love China! ";
而对数组初始化时:
static char str[14]={ "I love China! "};
不是等价于
char str[14];
str[ ]= "I love China! ";
即数组可以在变量定义时整体赋初值,但不能在赋值语句中整体赋值。
5. 在定义一个数组时,在编译时即已分配内存单元,有确定的地址。而定义一个字符指针变量时,给指针变量分配内存单元,在其中可以放一个地址值,也就是说,该指针变量可以指向一个字符型数据,但如果未对它赋以一个地址位,则它并未具体指向哪一个字符数据。如:
char str[10];
scanf("%s",str);
是可以的,而常有人用下面的方法:
char *a;
scanf("%s",a);
目的是输入一个字符串,虽然一般也能运行,但这种方法是危险的,不宜提倡。因为编译时虽然分配给指针变量a一个单元,a的地址(即&a)是已指定了,但a的值并未指定,在a单元中是一个不可预料的值。因此在scanf函数中要求将一个字符串输入到a的值(地址)开始的存储区(这是好的情况),也有可能指向已存放指令或数据的内存段,这就会破坏了程序,甚至会造成严重的后果。在程序规模小时,由于空白地带多,往往可以正常运行,而程序规模大时,出现上述“冲突”的可能性就大多了。应当这样:
char *a,str[10];
a=str;
scanf("%s",a);
先使a有确定值,也就是使a指向一个数组的开头,然后输入字符串到该地址开始的若干单元中。
6. 指针变量的值是可以改变的,如:
[例8.16]
void main(void)
{char *a="I love China! ";
a=a+7;
printf("%s",a);
}
运行结果如下:
China!
指针变量a的值可以变化,输出字符串从a当时所指向的单元开始输出各个字符,直到遇’\0’为止。而数组名虽然代表地址,但它的值是不能改变的。下面是错的:
char str[ ]={ "I love China! "};
str=str+7;
printf("%s",str);
需要说明:若定义了一个指针变量,使它指向一个字符串后,可以用下标形式引用指针变量所指的字符串中的字符。如:
[例8.17]
void main(void)
{char *a="I LOVE CHINA. ";
int i;
printf("The sixth charcter is %c\n",a[5]);
for(i=0;a[i]!=‘\0’;i++)
printf("%c",a[i]);
}
运行结果如下:
The sixth charcter is E
I LOVE CHINA.
程序中虽然并未定义数组a,但字符串在内存中是以字符数组形式存放的。a[5]按*(a+5)执行,即从a当前所指向的元素下移5个元素的位置,取出其单元中的值。
7. 用指针变量指向一个格式字符串,可以用它代替printf函数中的格式字符串。如:
char *format;
format="a=%d,b=%f\n";
printf(format,a,b);
它相当于
printf("a=%d,b=%f\n",a,b);
因此只要改变指针变量format所指向的字符串,就可以改变输入输出的格式。这种printf函数称为可变格式输出函数。
也可以用字符数组。如:
char format[ ]= "a=%d,b=%f\n";
printf(format,a,b);
但由于不能采取赋值语句对数组整体赋值的形式,如:
char format[ ];
format="a=%d,b=%f\n";
因此用指针变量指向字符串的方式更为方便。
y3= 1.29