C_Primer_Plus11.string
字符串和字符串函数
- 要点
gets(), gets_s(), fgets(), puts(), fputs(), strcat(), strncat(), strcmp(), strncmp(), strcpy(), strncpy(), sprintf(), strchr()
创建并使用字符串
使用 C 库中的字符和字符串函数,并创建自定义的字符串函数
使用命令行参数
- C 库提供了大量的函数用于读写、拷贝、比较、合并、查找字符串等
表示字符串和字符串输入输出
表示字符串的方法:
// 字符串常量
#define MSG "I am a symbolic string constant."
// 用数组表示
char words[40] = "I am a string in an array";
// 用指针表示
const char * pt1 = "Something is pointing at me.";
// 字面量. puts() 只显示字符串,并自动在末尾加上换行符
puts("I am a string literal.");
// 字符串连接
char greeting[50] = "Hello, and"" how" " are you"
" today!";
字符串常量属于静态存储类别(static storage class),如果在函数中使用字符串常量,该字符串只会被存储一次,在整个程序的生命周期内存在。
用双引号引起来的内容被视为指向该字符串存储位置的指针。
printf("%s, %p, %c\n", "we", "are", "space farers");
// output:
// we, 0x100000f61, s
字符串数组
定义字符串数组时,必须要让编译器知道需要多少空间。
// 最后几个空余元素默认初始化为 '\0'
const char m1[10] = "string.";
// 使用标准的数组形式初始化字符串:
// 最后的 '\0' 不能省略
const charm1[10] = {'s', 't', 'r', 'i',
'n', 'g', '.', '\0'};
// 让编译器自己计算字符串大小,只能用在初始化时
// 如果想要创建一个稍后再填充的数组,则应该指定数组大小
const char m2[] = "string.";
数组和指针
数组形式,每个元素被初始化为字符串字面量对应的字符,末尾为 '\0'。通常,字符串都作为可执行文件的一部分存储在数据段中。当程序载入内存时,也载入了程序中的字符串。字符串存储在 静态存储区 (static memory) 中,程序开始运行时才会为该数组分配内存,此时才将字符串拷贝到数组中。
此时,字符串有两个副本,一个是静态内存中的字符串字面量,另一个是存储在数组中的字符串。
对非 const 的数组,可以改变其中的元素。
指针形式也使得编译器为字符串在静态存储区预留存储这个字符串的空间,并把首字符的地址赋给指针。所以,指针形式的字符串是不能改变元素内容的,但可以移动指针的指向,比如 ++ptr
#include <stdio.h>
#define MSG "I'm special"
int main(void){
char ar[] = "I'm special";
const char * pt = "I'm special";
printf("string: %p\n", "I'm special");
printf("array: %p\n", ar);
printf("pointer: %p\n", pt);
printf("define: %p\n", MSG);
return 0;
}
output:
string: 0x4006c4
array: 0x7fff5232b480
pointer: 0x4006c4
define: 0x4006c4
编译器可以把多次使用的相同字面量存储在一处或多处。
静态数据使用的内存和数组使用的动态内存不同。
编译器使用了不同的位数表示两种内存
打印指针数组:
#include <stdio.h>
int main(void){
const char * head = "Hello world!";
while ('\0' != * head){
putchar(* head++);
}
putchar('\n');
return 0;
}
output:
Hello world!
字符串数组
字符串组成的数组
#include <stdio.h>
#define SLEN 40
#define LIM 5
int main(void){
const char * mytalents[LIM] = {
"Adding numbers swiftly",
"Multiplying accurately", "Stashing data",
"Following instructions to the letter",
"Understanding the C language"
};
char yourtalents[LIM][SLEN] = {
"Walking in a straight line",
"Sleeping", "Watching television",
"Mailing letters", "Reading email"
};
int i;
puts("Let's compare talents.");
printf("%-36s %-25s\n", "My Talents", "Your Talents");
for (i = 0; i < LIM; i++)
printf("%-36s %-25s\n", mytalents[i], yourtalents[i]);
printf("\nsizeof mytalents: %zd, sizeof yourtalents: %zd\n",
sizeof(mytalents), sizeof(yourtalents));
return 0;
}
output:
Let's compare talents.
My Talents Your Talents
Adding numbers swiftly Walking in a straight line
Multiplying accurately Sleeping
Stashing data Watching television
Following instructions to the letter Mailing letters
Understanding the C language Reading email
sizeof mytalents: 40, sizeof yourtalents: 200
指针的大小是第一个元素的大小。
对使用率高的字符串,推荐使用指针数组,因为它比二维字符数组效率高。
而要改变字符串中的内容,则应使用数组。
拷贝字符串:
const char * orig = "string.";
const char * copy;
copy = orig;
拷贝后的指针指向相同的地址,这样的拷贝效率高。
字符串输入
首先预留一些空间给该字符串,然后用输入函数获取该字符串。
如下错误代码:
char * pt;
scanf("%s", pt);
该代码可能会通过编译,但是在读入pt时,pt可能会擦掉程序中的数据或代码,导致程序异常终止。因为 scanf() 要把信息拷贝到参数指定的地址上,而 ptr 未被初始化,它可能会指向任何地方。
最简单的方法是,在声明时显式指明数组大小:
char pt[80];
gets() 函数
gets() 函数的使用:
char words[80];
gets(words);
问题:
数组在传递时,实际上传递的是指针,gets() 函数并不知道数组的长度,所以容易出现溢出问题。
出现溢出时,程序会报错:segmentation fault (分段错误),UNIX 系统中,它的意思是程序试图访问未分配的内存。
所以使用 gets() 函数容易出现安全隐患。过去,有些人通过系统编程,利用 gets() 插入和运行一些破坏系统安全的代码。
C99 标准承认了 gets() 的问题并建议不要再使用它,但仍保留这个函数,因为当前程序中仍大量使用该函数。
C11 标准废除了 gets() 函数,新增了 gets_s() 函数,是 stdio.h 函数中的可选扩展,所以 C11 标准的编译器不一定支持它。
fgets() 函数 (和 fputs())
fgets() 函数通过第二个参数限制读入的字符数来解决溢出问题。该函数专门涉及用于处理文件输入,所以一般情况下不太好用。
- fgets() 函数的第2个参数指明了读入字符的最大数量。如果该参数的值是 n,那么 fgets() 将读入 n-1 个字符,或者读到第一个换行符为止
- 如果 fgets() 读到第一个换行符,会把它存储在这个字符串中。这与 gets() 不同,gets() 会丢弃换行符
- fgets() 函数的第3个参数指明了要读入的文件。如果读入从键盘输入的数据,则以 stdin (标准输入) 作为参数,该标识符定义在 stdio.h 中
- 因为 fgets() 函数把换行符放在字符串的末尾,通常要与 fputs() 配合使用,除非该函数不在字符串末尾添加换行符
- fputs() 函数的第2个参数指明了要写入的文件,如果要显示在显示器上,使用 stdout
#include <stdio.h>
#define STLEN 14
int main(void){
char words[STLEN];
puts("Enter a string:");
fgets(words, STLEN, stdin);
fputs(words, stdout);
puts("Enter another string:");
fgets(words, STLEN, stdin);
fputs(words, stdout);
return 0;
}
output:
Enter a string:
helloworld
helloworld
Enter another string:
hello Newton, this is world!
hello Newton,
puts() 函数会在字符串末尾加一个换行符,而 fputs() 不会
fputs() 函数返回指向 char 的指针。如果一切顺利,该函数返回的地址与传入的第一个参数相同。但是,如果函数读到文件结尾,fputs() 会返回一个空指针 (null pointer)。该指针保证不会指向有效的数据,所以可用于标识这种特殊情况。在代码中,可用数字 0 来代替。不过在 C 中一般用宏 NULL 来代替。
fgets() 和 fputs() 的常用方法
#include <stdio.h>
#define STLEN 10
int main(void){
char words[STLEN];
puts("Enter strings (empty line to quit):");
while (fgets(words, STLEN, stdin) != NULL && words[0] != '\n'){
fputs(words, stdout);
}
puts("Done.");
return 0;
}
output:
Enter strings (empty line to quit):
hello world!
hello world!
hello Newton, this is world!
hello Newton, this is world!
Done.
每次 words 会读9个字符(最后一个是 '\0'),然后交给 fputs() 执行,并且未换行。然后 while 循环进入下一轮迭代,继续读入、输出,直到遇到换行符。
系统使用缓冲的 I/O,即用户在按下回车键之前,输入都被存储在临时存储区(缓冲区),按下回车时,在输入中增加了一个换行符,并把整行输入发送给 fgets()。对于输出,fputs() 把字符发送给另一个缓冲区,当发送换行符时,缓冲区中的内容被发送到屏幕上。
fgets() 存储换行符有好处也有坏处。坏处是你可能并不想把换行符存储在字符串中,这样会带来一些麻烦。好处是对于存储的字符串而言,检查末尾的换行符可以判断是否读取了一整行。如果不是一整行,要妥善处理剩下的字符。
gets_s() 函数
C11 新增 gets_s() 函数,和 fgets() 类似,用一个参数限制读入的字符数。区别:
- gets_s() 只从标准输入读取数据,所以它不需要第三个参数
- gets_s() 读到换行符是会丢弃它,而不是存储它
- 如果 gets_s() 读到最大字符数都没有换行符,则会把目标数组的首字符设置为空字符,读取并丢弃随后的输入直到遇到换行符或文件结尾,然后返回空指针。
当输入太长,超过数组可容纳字符数时,fgets() 函数最容易使用,而且可以选择不同的处理方式(手动处理,比如把换行符替换为空字符)。
s_gets() 函数
char * s_gets(char * st, int n){
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val){ // ret_val != NULL
while (st[i] != '\n' && st[i] != '\0')
++i;
if (st[i] == '\n')
st[i] = '\0';
else
while (getchar() != '\n')
continue;
}
return ret_val;
}
如果 fgets() 返回 NULL,说明文件结尾或出现读取错误,如果出现换行符,就用空字符替换它。如果字符串中出现空字符,就丢弃该行的剩余字符,然后返回和 fgets() 相同的值。它会丢弃多余的字符(如果不丢弃,多余的字符就会留在缓冲区,称为下次读取的对象)。
它最严重的缺陷是遇到不合适的输入时会毫无反应。它丢弃多余的字符时,既不通知程序也不告知用户,但是用它来替换 gets() 足够了。
scanf() 函数
scanf() 函数更像是“获取单词”函数,而不是获取字符串函数。scanf() 总是从第一个非空白字符开始读取,读到空白字符结束。如果指定了字段宽度,则最多读取指定长度的字符。scanf() 函数返回一个整数,该值等于 scanf() 成功读取的项数或 EOF
scanf() 函数无法完整读取书名或歌曲名,除非这些名称是一个单词。scanf() 函数的典型用法是读取混合数据类型为某种标准形式。
scanf() 和 gets() 类似,存在一些潜在缺点,如果输入航的内容过长,scanf() 也会导致数据一处,不过在 %s 中使用字段宽度可防止溢出。
字符串输出
- put()
- fputs()
- printf()
puts() 函数
char str[] = "Hello world!";
puts(str);
puts() 函数使用简单,它会一直输出字符直到遇到空字符。但是也会出现问题:
// 这段程序不要模仿!!
char side_a[] = "side a";
char dont[] = {'W', 'O', 'W', '!'};
char side_b[] = "side b";
puts(dont);
由于 dont 缺少表示结束的空字符,所以它不是一个字符串,因此 puts() 不知道在何处停止,他会一直打印,直到发现一个空字符为止。为了让 puts() 尽快读到空字符,我们把 dont 放在两个字符串之间。该段程序的一种可能输出是:
WOW!side a
fputs() 函数
- fputs() 函数的第2个参数指明要写入数据的文件。如果要打印在显示器上,可以用 stdout (stdio.h 文件中)
- 与 puts() 不同,fputs() 不会在输出的末尾添加换行符
注意,gets() 丢弃输入中的换行符,但是 puts() 在输入中添加换行符。fgets() 保留输入中的换行符,fputs() 不在输出中加换行符。
printf() 函数
自定义输入输出函数
#include <stdio.h>
void put1(const char * string){
int count = 0;
while (* string != '\0'){
putchar(* string++);
++count;
}
putchar('\n');
return count;
}
while 循环可以简化:
while (* string)
当 string 指向空字符时,* string 的值是0,即测试条件是假。
这种打印字符串的写法比较常用。
函数形参有两种写法 const char * string 和 const char string[]
两种方式等价,使用数组写法是提醒用户,这个函数处理的是数组。而指针写法则能包含多种情况。
字符串函数
strlen(), strcat(), strcmp(), strncmp(), strcpy(), strncpy()
strlen() 函数
返回字符串中的字符所占的字节数(字符按单字节算,则返回字符数),不包含末尾的空字符串。多字节字符按多个字节算。
strcat() 函数
用于拼接字符串,接受两个字符串参数。拼接后形成的新字符串作为第一个字符串,第二个字符串不变。
char a[20] = "hello";
char b[] = " world!";
strcat(a, b);
// a: hello world!
strncat() 函数
strcat() 函数无法检查第一个数组是否能容纳第二个字符串。若分配给数组1的空间不够大,多出来的字符溢出到相邻存储单元就会出问题。
strncat() 函数需要第三个参数,指定最大添加字符数。strncat(str1, str2, 13) 把 str2 追加到 str1,在加到第13个字符或遇到空字符串时停止。因此,加上末尾的空字符,str1要至少预留14个字符。
char a[20] = "hello";
char b[] = " world!";
strcat(a, b, 6);
// a: hello world
strcmp() 函数
内容相同的两个字符串,可能存储在不同的内存位置,直接比较它们的指针是不可行的,应该比较它们的内容。
#include <stdio.h>
#define MSG "hello"
int main(void){
char a[] = MSG;
if(MSG == a){
printf("ptr equal.\n");
}else{
printf("ptr not equal.\n");
}
if (strcmp(MSG, a)){
printf("str equal.\n");
}else{
printf("str not equal.\n");
}
return 0;
}
output:
ptr not equal
str equal
strcmp(a, b) 函数的返回值:
按照ascii码的大小计算,两个字符串对应字符依次比较,a<b 时返回负数,相等是返回0,a>b 时返回正数。
strncmp() 函数
可以指定要比较的字符数,比如之比较两个字符的前3个字符:
char * a = "string";
char * b = "stdio";
int ret = strcmp(a, b, 3);
它的返回值规则与 strcmp() 函数相同
strcpy() 和 strncpy() 函数
对指针赋值字符串是拷贝的地址:
pts2 = pts1;
想要拷贝整个字符串,要使用 strcpy() 函数:
char target[20];
strcpy(target, "Hello world!");
程序员要确保目标数组有足够的空间容纳字符串的副本。
返回值:
strcpy() 的返回值是 char * 类型,指向第一个参数。另外,第一个参数不必指向数组的开始,比如:
char target[20];
strcpy(target+3, "Hello world!"); // 指向第4个元素
strncpy() 函数:
有3个参数,第三个参数指明可拷贝的最大字符数。
若源字符串字符数小于参数值,则拷贝整个字符串,包括空字符。若源字符串字符数大于参数值,则不会拷贝空字符。
所以,参数值应比目标数组大小少1,然后把数组最后一个元素设置为空字符:
char target[20];
char source[] = "Hello world!";
strncpy(target, source, 19);
target[19] = '\0';
sprintf() 函数
声明在 stdio.h 中,而不是 string.h 。该函数与 printf() 类似,不过它不是打印在显示器上。它是往某个地址上写字符串的函数,第一个参数是目标字符串的地址,其他参数和 printf() 相同:
char target[20];
char source[] = "Hello world!";
sprintf(target, "%s\n", source);
其他函数
char *strcpy(char * restrict s1, const char * restrict s2);
该函数把s2指向的字符串(包括空字符)拷贝至s1指向的位置,返回值是s1。char *strncpy(char * restrict s1, const char * restrict s2, size_t n);
该函数把s2指向的字符串拷贝至s1指向的位置,拷贝的字符数不超过n,其返回值是s1。该函数不会拷贝空字符后面的字符,如果源字符串的字符少于n个,目标字符串就以拷贝的空字符结尾;如果源字符串有n个或超过n个字符,就不拷贝空字符。char *strcat(char * restrict s1, const char * restrict s2);
该函数把s2指向的字符串拷贝至s1指向的字符串末尾。s2字符串的第1个字符将覆盖s1字符串末尾的空字符。该函数返回s1。char *strncat(char * restrict s1, const char * restrict s2, size_t n);
该函数把s2字符串中的n个字符拷贝至s1字符串末尾。s2字符串的第1个字符将覆盖s1字符串末尾的空字符。不会拷贝s2字符串中空字符和其后的字符,并在拷贝字符的末尾添加一个空字符。该函数返回s1。int strcmp(const char * s1, const char * s2);
如果s1字符串在机器排序序列中位于s2字符串的后面,该函数返回一个正数;如果两个字符串相等,则返回0;如果s1字符串在机器排序序列中位于s2字符串的前面,则返回一个负数。int strncmp(const char * s1, const char * s2, size_t n);
该函数的作用和strcmp()类似,不同的是,该函数在比较n个字符后或遇到第1个空字符时停止比较。
char *strchr(const char * s, int c);
如果s字符串中包含c字符,该函数返回指向s字符串首位置的指针(末尾的空字符也是字符串的一部分,所以在查找范围内);如果在字符串s中未找到c字符,该函数则返回空指针。char *strpbrk(const char * s1, const char * s2);
如果 s1 字符中包含 s2 字符串中的任意字符,该函数返回指向 s1 字符串首位置的指针;如果在s1字符串中未找到任何s2字符串中的字符,则返回空字符。char *strrchr(const char * s, int c);
该函数返回s字符串中c字符的最后一次出现的位置(末尾的空字符也是字符串的一部分,所以在查找范围内)。如果未找到c字符,则返回空指针。char *strstr(const char * s1, const char * s2);
该函数返回指向s1字符串中s2字符串出现的首位置。如果在s1中没有找到s2,则返回空指针。size_t strlen(const char * s);
该函数返回s字符串中的字符数,不包括末尾的空字符。
有的系统 size_t 是 unsigned int, 而有的是 unsigned long,为了兼容,在 string.h 中定义了 size_t 类型。
请注意,那些使用const关键字的函数原型表明,函数不会更改字符串。例如,下面的函数原型:
char *strcpy(char * restrict s1, const char * restrict s2);
表明不能更改s2指向的字符串,至少不能在strcpy()函数中更改。但是可以更改s1指向的字符串。这样做很合理,因为s1是目标字符串,要改变,而s2是源字符串,不能更改。
例:
将换行符替换为空字符。
char line[80];
char * find;
fgets(line, 80, stdin);
find = strchr(line, '\n');
if (find)
* find = '\0';
ctype.h 字符函数
处理字符,不能处理字符串。
- toupper() 小写转大写
- 判断是否大写:isupper()
- 对于小写的两个函数:islower(), tolower()
- ispunct() 判断是否标点符号
ToUpper:
#include <ctype.h>
void ToUpper(char * str){
while (* str){
* str = toupper(* str);
++str;
}
}
统计标点符号:
#include <ctype.h>
int PunctCount(const char * str){
int ct = 0;
while (*str){
if (ispunct(* str))
++ct;
++str;
}
retturn ct;
}
命令行参数
例:
// 07cmd.c
#include <stdio.h>
int main(int argc, char*argv[]){
int count;
printf("The command line has %d arguments:\n", argc-1);
for (count = 1; count < argc; ++count){
printf("%d: %s\n", count, argv[count]);
}
printf("\n");
return 0;
}
运行命令:
$ ./07cmd hello world !
The command line has 3 arguments:
1: hello
2: world
3: !
C 编译器允许 main() 没有参数或者有两个参数(有些实现允许 main() 有更多参数,属于对标准的扩展)。使用两个参数时,第一个参数是命令行中的字符串数量。以前,这个 int 类型的参数被称为 argc(表示参数计数 (argument count))。系统用空格表示字符串的结束和下一个字符串的开始。第二个参数是这些字符串组成的数组,程序本身的名字赋给 argv[0],然后把随后的第一个字符串赋给 argv[1],以此类推。
第二个参数 char*argv[] 表明 argv 是一个字符串数组,它可以写成另一种方式 char**argv,但第一种形式表明 argv 是一系列字符串。
用双引号括起来的一串被看做是一个字符串:
$ ./07cmd "I am hungry" now
The command line has 2 arguments:
1: I am hungry
2: now
字符串转数字
atoi() 函数:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char*argv[]){
if (argc < 2){
printf("Usage: %s integer\n", argv[0]);
}else{
printf("argument: %d\n", atoi(argv[1]));
}
return 0;
}
output:
$ ./08atoi 2
argument: 2
atoi() 函数也能处理以整数开头的字符串,它只把开头的整数转换为字符。如果参数是非整数开头的字符串,它返回0,而 C 标准规定,这种情况是未定义的。因此,使用有错误检测功能的 strtol() 函数会更安全。
- atol() 字符串转 long
- atof() 字符串转 double
更智能的函数,能识别和报告字符串中的首字符是否是数字:
- strtol() 字符串转 long,可以指定进制
- strtoul() 字符串转 unsigned long,可以指定进制
- strtod() 字符串转 double
strtol() 的函数原型:
long strtol(const char* restrict nptr, char** restrict endptr, int base);
nptr 是字符串的初始地址,endptr是一个返回的指针,指向被转换为数字的字符串后面的字符:
- 如果 nptr 指向的字符串全部为数字,则 endptr 是一个指向空字符的指针(nptr 字符串末尾的空指针)
- 如果 nptr 指向的字符串包含其他字符,则 endptr 是一个指向 其他字符的指针
#include <stdio.h>
#include <stdlib.h>
#define LIM 10
char * s_gets(char*, int);
int main(void){
char number[LIM];
char* end;
long val;
fputs("Enter a number (empty line to quit): ", stdout);
while(s_gets(number, LIM) && number[0] != '\0'){
val = strtol(number, &end, 10); // 10 decimal
printf("val: %ld, end: %s (%d)\n", val, end, * end);
fputs("Next number: ", stdout);
}
return 0;
}
char* s_gets(char* st, int n){
char* ret;
int i = 0;
ret = fgets(st, n, stdin);
if(ret){
while(st[i] != '\n' && st[i]!= '\0'){
++i;
}
if(st[i] == '\n'){
st[i] = '\0';
}else{
while(getchar() != '\n'){
continue;
}
}
}
return ret;
}
output:
Enter a number (empty line to quit): 12
val: 12, end: (0)
Next number: ff
val: 0, end: ff (102)
Next number: 34atom
val: 34, end: atom (97)
Next number:

浙公网安备 33010602011771号