C Primer Plus学习笔记(十)- 字符串和字符串函数

getchar() 和 putchar()

getchar() 函数不带任何参数,它从输入队列中返回下一个字符

下面的语句读取下一个字符输入,并把该字符的值赋给变量 ch

ch =getchar();

相当于

scanf("%c", &ch);

putchar() 函数打印它的参数

下面的语句把之前赋给 ch 的值作为字符打印出来

putchar(ch);

相当于

printf("%c", ch);

getchar() 和 putchar() 不需要转换说明,因为它们只处理字符

表示字符和字符串 I/O

字符串是以空字符(\0)结尾的 char 类型数组

在程序中定义字符串

1.字符串字面量(字符串常量)

用双引号括起来的内容称为字符串字面量(string literal),也叫作字符串常量(string constant)

双引号中的字符和编译器自动加入末尾的 \0 字符,都作为字符串储存在内存中

从 ANSI C 标准起,如果字符串字面量之间没有间隔,或者用空白字符分隔,C 会将其视为串联起来的字符串字面量

char strings[50] = "Hello, and" " how are" "you"
				   " today!";

等价于

char strings[50] = "Hello, and how are you today!";

如果要在字符串内部使用双引号,必须在双引号前面加上一个反斜杠(\)

字符串常量属于静态存储类别(static storage class),这说明如果在函数中使用字符串常量,该字符串只会被储存一次,在整个程序的生命期内存在,即使函数被调用多次

用双引号括起来的内容被视为指向该字符串储存位置的指针

2.字符串数组和初始化

定义字符串数组时,必须让编译器知道需要多少的空间

一种方法是用足够空间的数组储存字符串

char strs[20] = "Hello World";

等价于

char strs[20] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\0'};

注意最后的空字符。没有这个空字符,这就不是一个字符串,而是一个字符数组

在指定数组大小时,要确保数组的元素个数至少比字符串长度多 1(为了容纳空字符)

所有未被使用的元素都被自动初始化为 0(这里的 0 指的是 char 形式的空字符,不是数字字符 0)

让编译器计算数组的大小只能用在初始化数组时

如果创建一个稍后再填充的数组,就必须在声明时指定大小

字符数组名和其他数组名一样,是该数组首元素的地址

char car[10] = "Tata";

car == &car[0];
*car == 'T';
*(car+1) == car[1] == 'a';

还可以用指针表示法创建字符串

char * pt1 = "Hello World";
char pt1[] = "Hello World";

pt1 和 ar1 都是该字符串的地址

3.数组和指针

数组形式在计算机的内存中分配,每个元素对应一个字符,还加上一个末尾的就、空字符 \0

通常,字符串都作为可执行文件的一部分储存在数据段中

当把程序载入内存时,也载入了程序中的字符串

字符串储存在静态储存区(static memory)中,但是,程序在开始运行时才会为该数组分配内存,此时才将字符串拷贝到数组中。此时的字符串有两个副本,一个是在静态内存中的字符串字面量,另一个是储存在数组中的字符串

指针形式也使得编译器为字符串在静态储存区为空字符预留个元素空间

一旦开始执行程序,它会为指针变量留出一个储存位置,并把字符串的地址储存在指针变量中。该变量最初指向该字符串的首字符,但是它的值可以改变,可以使用递增运算符来改变

字符串字面量被视为 const 数据

如果把一个字符串字面量拷贝给一个数组,就可以随意改变数据,除非把数组声明为 const

初始化数组把静态储存区的字符串拷贝到数组中,而初始化指针只把字符串的地址拷贝给指针

#include <stdio.h>
#define MSG "Hello World"

int main()
{
	char ar[] = MSG;
	const char *pt = MSG;
	printf("address of \"Hello World\": %p \n", "Hello World");
	printf("           address of ar: %p\n", ar);
	printf("           address of pt: %p\n", pt);
	printf("          address of MSG: %p\n", MSG);
	printf("address of \"Hello World\": %p \n", "Hello World");

	return 0;
}

运行结果

pt 和 MSG 的地址相同,而 ar 的地址不同

虽然字符串字面量 "Hello World" 在程序的两个 printf() 函数中出现了两次,但是编译器只使用了一个存储位置,而且与 MSG 的地址相同,别的编译器可能在不同的位置储存

编译器可以把多次使用的相同字面量储存在一处或多处

静态数据使用的内存与 ar 使用的动态内存不同。不仅值不同,特定编译器甚至使用不同的位数表示两种内存

4.数组和指针的区别

“指向字符串”的意思是指向字符串的首字符

char ar[] = "Hello World";
const char *pt = "Hello World";

两者主要的区别是:数组名 ar 是常量,指针名 pt 是变量

两者都可以使用数组表示法:

#include <stdio.h>

int main()
{
	char ar[] = "Hello World";
	const char *pt = "Hello World";

	for (int i = 0; i < 6; i++)
		putchar(ar[i]);
	putchar('\n');

	for (int i = 0; i < 6; i++)
		putchar(pt[i]);
		putchar('\n');

	return 0;
}

运行结果

两者都能进行指针加法操作

#include <stdio.h>

int main()
{
	char ar[] = "Hello World";
	const char *pt = "Hello World";

	for (int i = 0; i < 6; i++)
		putchar(*(ar + i));
	putchar('\n');

	for (int i = 0; i < 6; i++)
		putchar(*(pt + i));
		putchar('\n');

	return 0;
}

运行结果

只有指针表示法可以进行递增操作

#include <stdio.h>

int main()
{
	const char *pt = "Hello World";
	while (*(pt) != '\0')  // 在字符串末尾处停止
		putchar(*(pt++));  // 打印字符,指针指向下一个位置
	putchar('\n');

	return 0;
}

运行结果

想让 ar 和 pt 统一

pt = ar;  // pt 现在指向数组 ar

这使得 pt 指针指向 ar 数组的首元素

这不会导致 pt 指向的字符串消失,这样只是改变了储存在 pt 中的地址

除非已经保存了 "Hello World" 的地址,否则当 pt 指向别处时,就无法再访问该字符串

但是不能这么做:

ar = pt;

这类似于 x = 3; 和 3 = x; 的情况

可以改变 ar 数组中的元素信息

ar[1] = 'a';

或者

*(ar + 1) = 'a';

数组的元素是变量(除非数组被声明为 const),但是数组名不是变量

char * word = "frame";
word[1] = 'l';

这样的行为是未定义的,可能导致内存访问错误

因为,编译器可以使用内存中的一个副本来表示所有完全相同的字符串字面量

char * p1 = "Klingon";
p1[0] = 'F';
printf("Klingon");
printf(": Beware the %ss!\n", "Klingon");

编译器可以用相同的地址替换每个 "Klingon" 实例

如果编译器使用这种单次副本表示法,并允许 p1[0] 修改 'F',那将影响所有使用该字符串的代码

建议在把指针初始化为字符串字面量时使用 const 限定符:

const char p1 = "Klingon";

把非 const 数组初始化为字符串字面量却不会导致类似的问题,因为数组获得的是原始字符串的副本

如果打算修改字符串,就不要用指针指向字符串字面量

指针和字符串

字符串的绝大多数操作都是通过指针完成的

#include <stdio.h>

int main(void)
{
	const char * mesg = "Don't be a fool!";
	const char * copy;
	copy = mesg;
	printf("%s\n", copy);
	printf("mesg = %s; &mesg = %p; value = %p\n", mesg, &mesg, mesg);
	printf("copy = %s; &copy = %p; value = %p\n", copy, &copy, copy);
	return 0;
}

运行结果

指针 mesg 和 copy 分别储存在地址为 0061FF2C 和 0061FF28

最后两个指针的值,mesg 和 copy 的值都是 00405064,说明它们都指向同一个位置,指针的值就是它储存的地址

程序并未拷贝字符串

语句 copy = mesg; 把 mesg 的值赋给 copy,即让 copy 也指向 mesg 指向的字符串

字符串输入

分配空间

如果想把一个字符串读入程序,首先必须预留储存该字符串的空间,然后用输入函数获取该字符串

分配空间有两种方法

第一种,在声明时显示指明数组的大小

char name[5];

现在 name 是一个已分配 5 字节的地址

还有一种方法是使用 C 库函数来分配内存

不幸的 gets() 函数

gets() 函数读取整行输入,直至遇到换行符,然后丢弃换行符,储存其余字符,并在这些字符的末尾添加一个空字符使其成为一个 C 字符串

gets() 函数经常和 puts() 函数配对使用

puts() 用于显示字符串,并在末尾添加换行符

#include <stdio.h>
#define STLEN 81

int main(void)
{
	char words[STLEN];
	puts("Enter a string, please.");
	gets(words);  // 典型用法
	printf("Your string twice:\n");
	printf("%s\n", words);
	puts(words);
	puts("Done.");
	
	return 0;
}

运行结果

gets() 函数只知道数组的开始处,并不知道数组中有多少个元素

如果输入的字符串过长,会导致缓冲区溢出(buffer overflow),即多余的字符超出了指定的目标空间

gets() 的替代品

fgets() 函数(和 fputs())

fgets() 函数通过第 2 个参数限制读入的字符数来解决溢出的问题

fgets() 和 gets() 的区别:

  • fgets() 函数的第 2 个参数指明了读入字符的最大数量。如果该参数的值是 n,那么 fgets() 将读入 n-1 个字符,或者读到遇到的第一个换行符为止
  • 如果 fgets() 读到一个换行符,会把它储存在字符串中,gets() 会丢弃换行符
  • fgets() 函数的第 3 个参数指明要读入的文件。如果读入从键盘输入的数据,则以 stdin(标准输入)作为参数,该标识符定义在 stdio.h 中

因为 fgets() 函数把换行符放在字符串的末尾(假设输入行不溢出),通常要与 fputs() 函数(和 puts() 类似)配对使用,除非该函数不在字符串末尾添加换行符。fputs() 函数的第 2 个参数指明它要写入的文件

如果要显示在计算机显示器上,应使用 stdout(标准输出)作为该参数

#include <stdio.h>
#define SELEN 14

int main(void)
{
	char words[SELEN];

	puts("Enter a string, please.");
	fgets(words, SELEN, stdin);
	printf("Your string twice (puts(), then fputs()):\n");
	puts(words);
	fputs(words, stdout);
	puts("Enter another string, please.");
	fgets(words, SELEN, stdin);
	printf("Your string twice (puts(), then fputs()):\n");
	puts(words);
	fputs(words, stdout);
	puts("Done.");

	return 0;
}

运行结果

第 1 次输入 apple pie,比 fgets() 读入的整行输入短,因此 apple pie\n\0 被储存在数组中。当 puts() 显示该字符串时又在末尾添加了换行符,apple pie 后面有一行空行。因为fputs()不在字符串末尾添加换行符,所以并未打印出空行

第 2 行输入 strawberry shortcake,超过了指定的大小,所以 fgets() 只读入了 13 个字符,并把 strawberry sh\0 储存在数组中

fputs() 函数返回指向 char 的指针

如果一切进行顺利,该函数返回的地址与传入的第 1 个参数相同

如果函数读到文件结尾,它将返回一个特殊的指针:空指针(null pointer),该指针保证不会指向有效的数据

在代码中,可以用数字 0 来代替,不过在 C 语言中用宏 NULL 来代替更常见(如果在读入数据时出现某些错误,该函数也返回 NULL)

空字符和空指针

空字符(或'\0')是用于标记 C 字符串末尾的字符,其对应字符编码是 0。由于其他字符的编码不可能是 0,所以不可能是字符串的一部分

空指针(或 NULL)有一个值,该值不会与任何数据的有效地址对应。通常,函数使用它返回一个有效地址表示某些特殊情况发生,例如遇到文件结尾或未能按预期执行

空字符是整数类型,而空指针是指针类型

空字符和空指针都可以用数值 0 来表示,但是两者是不同类型的 0

空字符是一个字符,占 1 字节;而空指针是一个地址,通常占 4 字节

gets_s() 函数

gets_s() 只从标准输入中读取数据,所以不需要第 3 个参数

gets_s() 会丢弃读到的换行符

如果gets_s()读到最大字符数都没有读到换行符,会执行以下几步

首先把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或文件结尾,然后返回空指针。接着,调用依赖实现的“处理函数”(或选择其他函数),可能会中止或退出程序

如果输入行太长,使用 gets() 会擦写现有数据,存在安全隐患

gets_s() 函数很安全,但是,如果不希望程序中止或退出,就要知道如何编写特殊的“处理函数”

如果打算让程序继续运行,gets_s() 会丢弃该输入行的其余字符,无论你是否需要

posted @ 2018-06-19 23:23  Sch01aR#  阅读(314)  评论(0编辑  收藏  举报