关于C之字符串与从键盘输入

C字符串:

在C中,没有直接的字符串,完全替代之的是字符数组。字符数组中每一个字符占一个字节。

当初始化列表中的值少于数组元素个数时,编译器会把剩余的元素都初始化为0。也就是说,如果不初始化数组,数组元素和未初始化的普通变量一样,其中储存的都是垃圾值;但是,如果部分初始化数组,剩余的元素就会被初始化为0。

可以这样申明一个字符串:

char arr[50] = "Zing went the strings of my heart!"; // method-1
char *parr = "Zing went the strings of my heart!";   // method-2

sizeof(arr)=50,因为字符数组占50个字节的内存空间,装载的每个字符的内存空间都是连续的递增的。

strlen(arr)=34,因为字符串有34个字符(包括空格)。

"x"与'x'是不同的,"x"是个字符串,包含内存块相邻的两个字符'x'、'\0'。

字符数组/字符串,结尾一定是'\0',代表的是ASCII中的空字符null。strlen()函数就是根据'\0'来判断字符串的结尾的。

由于最后一个字符一定是'\0',所以arr[50]实际能装下的字符个数是50-1=49个。在#define LEN value时,就应该是字符数组想要最大包含字符数量+1,以免误认为可以包含的字符总个数是LEN(其实是LEN-1),在循环体中也要注意判断结尾符。

可以这样初始化一个字符串数组:

char *chArr[]={"hello", "boat", "robot", "phone"};

对于char * pch = "hello",[指针]变量被赋值为一个字符串常量?在这里是没错的,是因为字符串字面量(常量)有两层含义叠加了:1.它是一个指向首字符地址的[指针];2.它的字符串字面量(常量)也顺带表示了。 所以可以如上声明一个[指针数组](数组的每个元素都是[指针])。由于字符串字面量是【常量】,所以可以打印chArr[0][0]为'h',修改chArr[0][0]='x'则报错:EXC_BAD_ACCESS,即Read-Only!

从键盘输入:

C库包含了多个输入函数, scanf()是最通用的一个, 因为它可以读取不同格式的数据。 当然, 从键盘输入的都是文本, 因为键盘只能生成文本字符: 字母、 数字和标点符号。 如果要输入整数 2014, 就要键入字符 2、 0、 1、 4。 如果要将其储存为数值而不是字符串, 程序就必须把字符依次转换成数值, 这就是scanf()要做的。 scanf()把输入的字符串转换成整数、 浮点数、 字符或字符串, 而printf()正好与它相反, 把整数、 浮点数、 字符和字符串转换成显示在屏幕上的文本。

C 库函数 int scanf(const char *format, ...) 从标准输入 stdin 读取格式化输入。

参数:

  • format -- 这是 C 字符串,包含了以下各项中的一个或多个:空格字符、非空格字符 和 format 说明符
    format 说明符形式为 [=%[*][width][modifiers]type=]

返回值:如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOF。

一般用scanf()函数指明要从标准键盘输入赋值到指定的变量,要注意:scanf()遇到的第一个空白(空格、Tab、Enter)时会停止,也就是说scanf("%s",...)只能读取“a single word”。如scanf("%s",arr);输入:hello world,arr中只有hello。

1.指定分隔符的情况,如:scanf("%d,%d,%s",&a,&b,arr),这里指定的分隔符是",",所以可以这样输入:12,39,welcome

2.未指定分隔符的情况,如:scanf("%d%d%s",&a,&b,arr),没有指定分隔符,那么就用默认的空白(空格、Tab、Enter)来作为分隔符,所以可以这样输入:12[空格][空格][Tab]39[Enter][空格][Enter]welcome。所有的空白会合并为一个分隔符,所以多输入几个也是一样的。这里scanf()会通过分隔符一直找到3个输入值为止。

3.由于事先声明了整型a、b和字符数组arr,所以输入的数据12、39、welcome会刚好与声明的数据类型自动匹配到。

4.满足输入元素个数后,最后输入回车[Enter]结束scanf()函数。

接下来, 我们更详细地研究scanf()怎样读取输入。 假设scanf()根据一个%d转换说明读取一个整数。 scanf()函数每次读取一个字符, 跳过所有的空白字符, 直至遇到第1个非空白字符才开始读取。 因为要读取整数, 所以scanf()希望发现一个数字字符或者一个符号(+或-) 。 如果找到一个数字或符号, 它便保存该字符, 并读取下一个字符。 如果下一个字符是数字, 它便保存该数字并读取下一个字符。 scanf()不断地读取和保存字符, 直至遇到非数字字符。 如果遇到一个非数字字符, 它便认为读到了整数的末尾。 然后,scanf()把非数字字符放回输入。 这意味着程序在下一次读取输入时, 首先读到的是上一次读取丢弃的非数字字符。 最后, scanf()计算已读取数字( 可能还有符号) 相应的数值, 并将计算后的值放入指定的变量中。

如果第1个非空白字符是A而不是数字, 会发生什么情况? scanf()将停在那里, 并把A放回输入中, 不会把值赋给指定变量。 程序在下一次读取输入时, 首先读到的字符是A。 如果程序只使用%d转换说明, scanf()就一直无法越过A读下一个字符。 另外, 如果使用带多个转换说明的scanf(), C规定在第1个出错处停止读取输入。

scanf()函数还可以指定输入字段的宽度:

#include <stdio.h>
int main(void) {
  char name1[11], name2[11];
  int count;
  printf("Please enter 2 names.\n");
  count = scanf("%5s %10s", name1, name2);
  printf("I read the %d names %s and %s.\n", count, name1, name2);
  return 0;
}
Please enter 2 names.
|Jesse Jukes
I read the 2 names Jesse and Jukes.
Please enter 2 names.
|Liza Applebottham
I read the 2 names Liza and Applebotth.
Please enter 2 names.
|Portensia Callowit
I read the 2 names Porte and nsia.

用其他数值匹配的转换说明读取输入和用%d 的情况相同。 区别在于scanf()会把更多字符识别成数字的一部分。 例如, %x转换说明要求scanf()识别十六进制数a~f和A~F。 浮点转换说明要求scanf()识别小数点、 e记数法( 指数记数法) 和新增的p记数法( 十六进制指数记数法)。如果使用%s 转换说明, scanf()会读取除空白以外的所有字符。 scanf()跳过空白开始读取第 1 个非空白字符, 并保存非空白字符直到再次遇到空白。这意味着 scanf()根据%s 转换说明读取一个单词, 即不包含空白字符的字符串。 如果使用字段宽度, scanf()在字段末尾或第1个空白字符处停止读取。无法利用字段宽度让只有一个%s的scanf()读取多个单词。 最后要注意一点:当scanf()把字符串放进指定数组中时, 它会在字符序列的末尾加上'\0', 让数组中的内容成为一个C字符串。

对于最后一次输入,“Partensia”超过了限宽,读取前5个:“Parte”,然后接着从它的下一个字符开始读取而不是丢弃,即第二个读取到:“nsia”,然后遇到空格停止读取。

scanf()和gets()或fgets()的区别在于它们如何确定字符串的末尾:scanf()更像是“获取单词”函数, 而不是“获取字符串”函数; 如果预留的存储区装得下输入行, gets()和fgets()会读取第1个换行符之前所有的字符。scanf()函数有两种方法确定输入结束。 无论哪种方法, 都从第1个非空白字符作为字符串的开始。 

建议用scanf_s()(C11)代替scanf():

int scanf_s(const char *restrict format, [argument], [size(optional)]);

可选参数size限定输入字符的数量。返回成功赋值的接收参数的数量(可以为零,在首个接收用参数赋值前匹配失败的情况下),或者若输入在首个接收用参数赋值前发生失败,则为EOF。若有运行时制约违规,亦返回EOF。

例如:分配了5字节的空间但是用户输入了10字节,就会导致scanf()读到10个字节。如:char buf[5]={'A'}; scanf("%s", buf);如果输入1234567890,则 以后的部分会被写到别的变量所在的空间上去(C编译器不判断),从而可能会导致程序运行异常。以上代码如果用scanf_s()则可避免此问题:char buf[5]={'A'}; scanf_s("%s",buf,sizeof(buf)); 最多读取5-1=4个字符,因为buf[4]要放'\0',如果输入1234567890,则buf只会接受前4个字符。注:scanf_s最后一个参数size是可选的,size默认值就是接收缓冲区的大小(buf的容量) ,即表示最多读取size-1个字符;此处默认值为4,最多可读取4个字符,只会接收前4个输入,其后的无效,避免了程序的漏洞(以免还有输入缓冲区及写到其他变量空间问题的存在)。

在Win10 Intel x64 vs2019环境下测试,scanf_s最后一个参数设置为sizeof(buf)是最合适的,最多能输入buf容量-1个字符,但超过了,程序出错终止。其他环境下可能结果就不一样了。

由于scanf("%s",...)只能读取“单个单词”,那要读取空白怎么办?

C 库函数 char *gets(char *str) 从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。

参数:str -- 这是指向一个字符数组的指针,该数组存储了 C 字符串。

返回值:如果成功,该函数返回 str。如果发生错误或者到达文件末尾时还未读取任何字符,则返回 NULL。

#include <stdio.h>
#define STLEN 81 // 80+1,这种编程风格很好,知道可以最多输入80个字符
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;
}
Enter a string, please.
|I want to learn about string theory!
Your string twice:
I want to learn about string theory!
I want to learn about string theory!
Done.

整行输入( 除了换行符) 都被储存在 words 中, puts(words)和printf("%s\n, words")的效果相同。 

有个问题,出在 gets()唯一的参数是 words, 它无法检查数组是否装得下输入行。 由于数组名会被转换成该数组首元素的地址, 因此, gets()函数只知道数组的开始处, 并不知道数组中有多少个元素。如果输入的字符串过长, 会导致缓冲区溢出( buffer overflow),即多余的字符超出了指定的目标空间。 如果这些多余的字符只是占用了尚未使用的内存,就不会立即出现问题;如果它们擦写掉程序中的其他数据,会导致程序异常中止;或者还有其他情况。

为了让输入的字符串容易溢出,把程序中的STLEN设置为5, 程序的输出如下:

Enter a string, please.
warning: this program uses gets(), which is unsafe.
|I think I'll be just fine.
Your string twice:
I think I'll be just fine.
I think I'll be just fine.
Done.
Segmentation fault: 11

“Segmentation fault”( 段错误)似乎不是个好提示,的确如此。在UNIX系统中,这条消息说明该程序试图访问未分配的内存。 

C99 标准的委员会把这些建议放入了标准,承认了gets()的问题并建议不要再使用它。C11标准委员会采取了更强硬的态度, 直接从标准中废除了gets()函数。 

回车换行[Enter],gets()函数结束。

同样,在C11中有一个更好的方法:gets_s(),参数与gets()函数类似,多出的第二个参数,也是为了防止不加限制的字符数量输入,这里size=sizeof(buffer)才正好,否则自动为'\0'分配空间的时候会溢出。

如果要能读取空白符+换行符,该用哪个函数呢?——fgets():

C 库函数 char *fgets(char *str, int n, FILE *stream) 从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。

参数:

  • str -- 这是指向一个字符数组的指针,该数组存储了要读取的字符串。
  • n -- 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
  • stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。

返回值:如果成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。如果发生错误,返回一个空指针。

fgets()和gets()的区别如下:

fgets()函数的第2个参数指明了读入字符的最大数量。 如果该参数的值是n, 那么fgets()将读入n-1个字符, 或者读到遇到的第一个换行符为止。如果fgets()读到一个换行符, 会把它储存在字符串中。 fgets()函数如果从文件(不是stdin)读取'\n'并存储,在'\n'后再增加一个'\0'构成字符串,对于从stdin键盘输入也会读取一个换行符'\n'。但fgets函数需要指定读入的字符数,如果指定了n,则最多只能读取n-1个。fgets()在读取了n-1个字符、读到了'\n'或遇到了EOF三种情况之一时都结束读取。这点与gets()不同, gets()会丢弃换行符。fgets()函数的第3 个参数指明要读入的文件。 如果读入从键盘输入的数据, 则以stdin( 标准输入)作为参数, 该标识符定义在stdio.h中。

因为 fgets()函数把换行符放在字符串的末尾( 假设输入行不溢出),通常要与 fputs()函数( 和puts()类似) 配对使用, 除非该函数不在字符串末尾添加换行符。 fputs()函数的第2个参数指明它要写入的文件。 如果要显示在计算机显示器上, 应使用stdout( 标准输出) 作为该参数。

C 库函数 int fputs(const char *str, FILE *stream) 把字符串写入到指定的流 stream 中,但不包括空字符('\0')。

参数:

  • str -- 这是一个数组,包含了要写入的以空字符终止的字符序列。
  • stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符串的流。

返回值:该函数返回一个非负值,如果发生错误则返回 EOF。

对于fgets()函数,可以在标准输入stdin(不是文件)中读取换行符吗?

#include <stdio.h>
#define STLEN 10
int main(void) {
  char words[STLEN];
  puts("Enter strings:");
  fgets(words, STLEN, stdin);
  fputs(words, stdout);
  return 0;
}
Enter strings:
Hello World
Hello WorProgram ended with exit code: 0

这是Xcode下运行的,不同于Win10 vs2019环境,不会因为输入长度过长而崩溃了。可以看到在输入Hello World后直接回车[Enter]在打印的时候并没有打印出换行符。 

#include <stdio.h>
#define STLEN 14
int main(void) {
  char words[STLEN];
  puts("Enter a string, please.");
  fgets(words, STLEN, stdin);
  printf("Your string twice (puts(), then fputs()):\n");
  puts(words);
  fputs(words, stdout);
  puts("Enter another string, please.");
  fgets(words, STLEN, stdin);
  printf("Your string twice (puts(), then fputs()):\n");
  puts(words);
  fputs(words, stdout);
  puts("Done.");
  return 0;
}
Enter a string, please.
|apple pie
Your string twice (puts(), then fputs()):
apple pie

apple pie
Enter another string, please.
|strawberry shortcake
Your string twice (puts(), then fputs()):
strawberry sh
strawberry shDone.

第一行输入,当puts()显示该字符串时又在末尾添加了换行符。因此,apple pie后面有一行空行。因为fputs()不在末尾添加换行符,所以未打印出空行。

第二行输入,strawberry shortcake,超出了大小的限制。所以fgets()只读入13个字符,并把strawberry sh\0存储在数组中。再次注意,puts()函数会在带输出的字符串后面添加一个换行符,而fputs()不会这样做。

如果函数fgets()读到文件结尾,它将返回一个空指针(null pointer)。空指针,在代码中可以用0代替,不过在C语言中用宏NULL来代替更常见。


gets()丢弃末尾回车换行符fgets()获取所有键盘输入字符(包括回车换行符)
puts()添加末尾回车换行符fputs()所有字符原样输出

自定义一个函数s_gets():

如果 fgets()返回 NULL,说明读到文件结尾或出现读取错误,s_gets()函数跳过了这个过程。如果字符串中出现换行符,就用空字符替换它;如果字符串中出现空字符,就丢弃该输入行的其余字符,然后返回与fgets()相同的值。

我们设计的 s_gets()函数并不完美, 它最严重的缺陷是遇到不合适的输入时毫无反应。 它丢弃多余的字符时, 既不通知程序也不告知用户。 但是,用来替换前面程序示例中的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;
}
--------------------或------------------------
char* s_gets(char *st, int n) {
	char *ret_val;
	char *find;
	ret_val = fgets(st, n, stdin);
	if (ret_val) {
		find = strchr(st, '\n'); // 查找换行符
		if (find) // 如果地址不是 NULL,
			*find = '\0'; // 在此处放置一个空字符
		else
			while (getchar() != '\n')
				continue; // 处理剩余输入行
	}
	return ret_val;
}

注:用户在首行按下Enter键(用'\0'进行判断),即相当于输入为null。 

对于单个字符的输入/输出,用getchar()/putchar()就行了。

C 库函数 int getchar(void) 从标准输入 stdin 获取一个字符(一个无符号字符)。这等同于 getc 带有 stdin 作为参数。

返回值:该函数以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF。

C 库函数 int putchar(int char) 把参数 char 指定的字符(一个无符号字符)写入到标准输出 stdout 中。

参数:char -- 这是要被写入的字符。该字符以其对应的 int 值进行传递。

返回值:该函数以无符号 char 强制转换为 int 的形式返回写入的字符,如果发生错误则返回 EOF。

其实,getchar()和 putchar()都与stdio.h头文件相关联,它们都不是真正的函数,它们被定义为供预处理器使用的
宏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值