2. ANSI字符串库
到目前为止,对字符串的使用仅限于strlib.h
中定义的操作。商业性的C语言程序,使用定义成ANSI标准一部分的string.h
接口。
在 第9章,已经讨论过strlib.h
接口和string.h
接口的关系,同时也介绍了分层抽象的概念。
string.h
输出的常用函数,如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/2aa0a586399ea1912c33169264317a43.png)
两个字符串库的最大区别在于:怎样为字符串中的字符分配内存空间。
在strlib.h
接口中,函数本身就会分配必要的内存空间,使得strlib.h
接口更容易使用。
在string.h
接口中,当一个新字符串需要内存时,客户要负责提供内存。
为此,客户通常会声明一个足以保存结果的字符数组,并将它作为参数传递给库函数。
然后,该实现会把新字符串值写入调用函数提供的内存空间中。
2.1 strcpy函数
string.h
接口中的strcpy
函数为库中函数如何给调用函数返回一个新的字符串值提供了好方法。
strcpy
函数将一个源(source)字符串中的字符复制到另一个目标(destination)字符串中。
为了和赋值运算符相一致,复制操作是从右向左进行的,strcpy
将目标参数作为第一个参数,因此有以下原型:
void strcpy(string dst, string src);
如果想要操作一个字符串,同时又保留它的原值,strcpy
函数是很有用的。
例如,假设字符串变量line
含有一行从用户处读入的文本,程序要求对line
中的字符做一些修改。
直接用数组选择来更改line
中的元素会破坏line
原来的内容,而且以后还有可能会用到这些内容。
为了避免改变line
,可以将其中的各个字符复制到另外一个数组再进行操作。
保存数据中间副本的数组称为缓冲区(buffer)。
在这种情况下,需要一个字符缓冲区,以便装入line
的数据副本。
当使用ANSI字符串库时,通常的方法是明确声明一个字符数组作为缓冲区,大小足以存放应用中可能遇到的最大字符串。
所以,如果常量MaxLine
含有最长输入串的长度,可以这样声明缓冲区:
char buffer[MaxLine+1];
此处的+1用来为长度正好是MaxLine
的字符串末的空字符留出空间。
重要的是,要牢记不能采用如下赋值语句将line
的字符复制到buffer
中:
buffer = line; (该条语句非法)
因为在C语言中,数组不是左值,不能出现在赋值语句的左边。要拷贝字符,必须调用如下strcpy
函数:
strcpy(buffer, line);
该函数调用将会把line
的字符全部复制到buffer
中,直到遇到表示源字符串结束的空字符串为止。
strcpy
函数总是将空字符随其他字符一起复制到目标字符串中,从而保证了新字符串的正确终止。
可以使用数组或指针实现strcpy
函数。在数组的情况下,实现如下:
void strcpy(char dst[], char src[]) {
int i;
for (i = 0; src[i] != '\0'; i++) {
dst[i] = src[i];
}
dst[i] = '\0';
}
尽管此代码的实现易于解读,但通常将字符串作为指针来考虑,且传统做法也鼓励利用指针操作来编写大多数字符串处理函数。
strcpy
的传统实现方法如下:
void strcpy(char *dst, char *src) {
while (*dst++ = *src++);
}
while语句的循环体只有一个分号,而分号本身构成了C语言中合法但不产生任何作用的语句——空语句(null statement)。
该测试首先从源字符串复制一个字符到目标缓冲区,更新每个指针,使它们指向下一个字符的地址。
由于C语言将所有非零值解释成TRUE,所以只要结果不为0,strcpy
中的while循环就会一直进行下去。
因此,循环只有在字符串末的空字符被复制时才会停止。
使用strcpy
时,作为客户,责任是分配足够大的空间,以保存目标字符串和表示字符串结束的空字符。
若strcpy
函数试图写入的字符比分配的空间多,程序将莫名其妙地失败,且失败的原因很难查找。
例如,假设编写了一个调用strcpy
的程序,但源字符串比目标数组长,如下所示:
char carry[6];
strcpy(carry, "A long string");
执行这一程序时会将"A long string"的前六个字符正确复制到carray
中,但是,程序又继续把剩余的字符复制到内存中carray
之后的字节里面。
因此,调用strcpy
之后,内存如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/68df6079549357eb97aede2072a4a9de.png)
地址1006-1013间的字节存放的也许是其他变量或是程序本身的一部分,所以改变它们肯定会引起程序失败。
写入的数据超出作为缓冲区的数组的大小是一种常见的程序设计错误,这种错误称为缓冲区溢出(buffer overflow)。
- 记:Abort trap
如果目标字符串是一个未妥善初始化的指针变量,就会发生同样严重的问题。例如,假设用语句
string str;
声明了一个字符串变量str
,那么就不能用strcpy
复制字符串到该变量中。如果试图写语句
strcpy(str, "A long string"); (此语句无效)
并且没有事先初始化变量str
使其指向某一字符数组,"A long string"会被复制到内存中无法预知的区域。
如果认为已经分配给目标字符串的空间可能不够,就必须在使用strcpy
复制字符串之前检查数据的长度。
所以,在用strcpy
将line
的内容复制到buffer
之前,应该通过测试语句行确保不会发生缓冲区溢出的情况,测试如下:
if (strlen(line) > MaxLine) Error("Input line too long");
strcpy(buffer, line);
2.2 strncpy函数
为了避免在使用strcpy
函数时缓冲区溢出,ANSI字符串库包含了另一种形式的strcpy
,称为strncpy
。
strncpy
函数允许客户指定一个长度限制,其原型如下:
void strncpy(string dst, string src, int n);
和strcpy
一样,strncpy
将src
指定的字符串中的字符复制到dst
指定的字符数组中。
区别在于,strncpy
最多从src
复制n
个字符,遇到空字符会提前停止。
strncpy
允许指定字符串的最大长度,所以能防止写入的字符超过字符数组的大小。例如,如果通过语句:
char buffer[MaxLine+1];
声明字符数组buffer
,则可以用函数调用
strncpy(buffer, src, MaxLine);
安全地将字符串从line
复制到buffer
。
strncpy
函数的作用受以下几个设计缺点限制:
(1) strncpy(dst, src, n)返回时,只有在源字符串长度少于n
个时,目标数组才会以空字符终止。
如果src
正好含n
个字符,调用就会将那些字符复制到dst
数组中,但不会在结束处保存一个空字符。
为了保证目标字符串的正确终止,必须为dst
分配一个额外的元素,并显式地将dst[n]
初始化为空字符。
(2) 复制源字符串后,函数strncpy(dst, src, n)会在dst
每个字符的位置中都写上空字符,直到填满n
个位置。
所以,如果MaxLine
是1000,即使line
很短,调用strncpy(buffer, line, MaxLine)也会给buffer
的前1000个字符都赋上新值。
不得不用空字符填满目标数组的剩余部分大大降低了strncpy
的效率。
2.3 strcat和strncat函数
strcat
函数常用于将较小的字符串组合成较大字符串。
再考虑前文所述的 Pig Latin 程序,可以同时使用strcpy
和strcat
重组Pig Latin的组成部分。
例如,假定变量head
和tail
分别包含字符串"src"和"am",可以用以下代码将这两个字符串组合成字符串"amscray":
#include <stdio.h>
#include <stdbool.h>
#include <ctype.h>
#include "strlib.h"
#include "string.h"
#define MaxWord 10
/* Main Program */
main() {
int i;
char head[5] = {'s', 'c', 'r'};
char tail[5] = {'a', 'm'};
char pigword[MaxWord + 1];
strcpy(pigword, tail);
strcat(pigword, head);
strcat(pigword, "ay");
for (i = 0; pigword[i] != '\0'; i++) {
printf("%c\n", pigword[i]);
}
}
当声明pigword
时,无法得知其中元素的初始内容,也不能假设它包含的是空字符串。通过调用
strcpy(pigword, tail);
可以确保tail
的字符被复制到了数组pigword
的开头,此时pigword
的内容如下:
![](https://i-blog.csdnimg.cn/blog_migrate/76f18466419f35c0ef641d2fea12cceb.png)
当调用
strcpy(pigword, head);
时,strcat
函数找到pigword
当前内容的结尾,并开始把head
的内容复制其中,结果会产生如下状态:
![](https://i-blog.csdnimg.cn/blog_migrate/2bc2705722f7a719f9d163ef0dc2c830.png)
最后一步,调用
strcpy(pigword, "ay");
完成Pig Latin词汇,结果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/9f2f945008faa1e1c4a67b60497cdaca.png)
必须小心使用strcat
函数,以避免连接过多字符,从而超出缓冲区所能提供的大小。
字符串库包含的函数strncat
有一定的作用。
调用strncat(s1, s2, n)最多能从s2复制n个字符到s1末尾,但为了确定给n赋予何值,还需检查s1的当前长度,才能知道缓冲区还剩余多少空间。
2.4 strlen、strcmp和strncmp函数
string.h
输出的strlen
和strcmp
函数与strlib.h
的StringLength
和StringCompare
完全一样。
但string.h
接口没有包含与StringEqual
直接对应的函数。
要测试两个字符串是否相等,只需调用strcmp
并查看结果是否为0。
当程序能明确地将结果和0作比较时,这个方法不会引起混淆。
但如果依赖C语言中把整数0解释成FALSE,而把其他整数解释成TRUE的话,程序就难以读懂了。
例如,有时会看到如下if语句:
if (strcmp(s1, s2)) {
...语句...
}
当s1和s2不相同时,就会执行if语句的内容。
FindStringInArray
的实现给出了紧凑编程风格的含义更为模糊的例子:
#include <stdio.h>
#include <stdbool.h>
#include <ctype.h>
#include "strlib.h"
#include "string.h"
#include "scanner.h"
/* Function Prototype */
int FindStringInArray(string key, string array[], int n);
/* Main Program */
main() {
string key="e";
string array[10]={"a", "p", "p", "l", "e"};
printf("%d\n", FindStringInArray(key, array, 5));
}
/* Function */
int FindStringInArray(string key, string array[], int n) {
int i;
for (i = 0; i < n && strcmp(key, array[i]); i++);
if (i < n) return i;
return -1;
}
只要i比n小,且key与当前数组项不符,for循环就会一直执行下去。
在两种情况下可以退出for循环:
数组中的元素都已测试完毕,或者是循环在下标i处找到了匹配的元素而终止。
string.h
接口也包含另一个字符串比较函数strncmp
。
调用strncmp(s1, s2, n)最多只考虑两个字符串中的n个字符。
如果字符串的前n个字符都相同,strncmp
返回0,就算以后的字符不同也是如此。
2.5 strchr、strrchr和strstr函数
string.h
接口也输出一些用于搜寻字符串的函数。
strchr
函数从chr中的字符开始返回字符串str,如果没有找到匹配字符,则返回NULL。
函数strrchr
和strchr
的作用大致相同,但它是从字符串末尾开始搜寻,找到最后一个ch,而不是第一个。
函数strstr
和strchr
的作用相同,仅数据类型不同。
2.6 ANSI字符串函数的应用
问题是,将传统顺序方法写的名字,如:
First Middle Last
改写成反向的顺序,即
Last, First Middle
需要写一个函数InvertName
,用于将用传统顺序写的姓名转换成姓在前名在后的顺序。
如果遵循ANSI库对字符串操作的规则来设计,InvertName
必须取两个参数,一个包含原来的姓名,另一个参数为转换后的结果提供内存空间。
其原型如下:
static void InvertName(char result[], char name[]);
运行示例如下:
![](https://i-blog.csdnimg.cn/blog_migrate/824efc1b77a9be7acbbe165e284e9335.png)
代码实现如下:
#include <stdio.h>
#include <stdbool.h>
#include <ctype.h>
#include "strlib.h"
#include "string.h"
#define MaxLine 40
/* Function Prototype */
static void InvertName(char result[], char name[]);
/* Main Program */
main() {
char *name;
char result[MaxLine+1];
while (true) {
printf("Name: ");
fgets(name, 50, stdin);
if (name[strlen(name)-1] == '\n') { // 去除末尾换行符
name[strlen(name)-1] = '\0';
}
if (strlen(name)==0) break;
InvertName(result, name);
printf("%s\n", result);
}
}
/* Function */
static void InvertName(char result[], char name[]) {
int len;
char *sptr;
len = strlen(name);
sptr = strchr(name, ' ');
if (sptr != NULL) len++;
if (len > MaxLine) printf("Name too long\n");
if (sptr == NULL) strcpy(result, name);
else {
strcpy(result, sptr+1);
strcat(result, ", ");
strncat(result, name, sptr - name);
result[len] = '\0';
}
}
参考
《C语言的科学和艺术》 —— 14 再论字符串