首页 > 其他分享 >关于字符串输入输出的若干函数

关于字符串输入输出的若干函数

时间:2023-11-21 13:34:44浏览次数:46  
标签:字符 函数 限定 输入输出 读入 字符串 长度 gets 输入

在 C 语言中,通过 <stdio.h> 可以使用一些非常有帮助的函数来从标准输入流 (或文件流,本篇不涉及) 中读入字符串,或者向标准输出流 (或文件流) 中写入字符串。这篇笔记整理的是这些相关函数的异同以及适用场景。

标准输入流的使用

使用场景

区别一:是否限定读入字符数量

为了分析它们的用途,我们要考虑具体的使用场景和限制。通常,读入字符串的操作可以分为 限定长度不限定长度 两种。后者因为可能导致缓冲区溢出 (buffer overflow) 进而产生对未分配内存的占用或是擦出程序中其它代码的数据,最终导致程序出错。所以前者应该是更为常用的,并且不易出错。

需要说明的是,这里的 限定长度 不是指定义存储字符串的容器变量时给定长度 (如定义一个字符数组),而是要求控制输入的函数限制从标准输入流中取用字符的数量。

从这个角度,处理标准输入流输入的函数包括两类:

  • 强制要求限定长度的: fgets()gets_s()
  • 不要求限定长度的:
    • 可选限定长度的: scanf()
    • 不能限定长度的: gets()

让我们来观察一段 C 语言代码和对应的汇编语句,然后分析为什么产生下列的输入和输出:

char str1[5];
char str2[5];
gets(str1);
gets(str2);
printf("[%s]", str1);
printf("[%s]", str2);
var_A= byte ptr -0Ah
var_5= byte ptr -5

push    rbp
mov     rbp, rsp
sub     rsp, 30h
call    __main
lea     rax, [rbp+var_5]
mov     rcx, rax
call    gets
lea     rax, [rbp+var_A]
mov     rcx, rax
call    gets

以下是输入和输出内容:

>>>> str1
>>>> str2 rewrite      
<<<< [rewrite][str2 rewrite]

>>>> 表示输入,<<<< 表示输出

从输入的内容来看,两个变量 str1str2 的内容本应该分别是

  • ['s', 't', 'r', '1', '\0']
  • ['s', 't', 'r', '2', ' ']

但是却输出了:

  • ['r', 'e', 'w', 'r', 'i', 't', 'e', '\0']
  • ['s', 't', 'r', '2', ' ', 'r', 'e', 'w', 'r', 'i', 't', 'e', '\0']

显然,第一个字符串 str1 的值被第二个的内容给覆盖了。这一点从汇编代码中 var_5 的地址比 var_A 高 5 个字节可以看出。gets() 函数读取 str2 的输入 "str2 rewrite" 时,本身的 5 个字节全部被用尽 (['s', 't', 'r', '2', ' ']),后续读入的字符覆盖到了 str1 的地址空间 (['r', 'e', 'w', 'r', 'i']),剩下的字符继续从栈顶地址开始往高地址处覆盖。最终进行打印时,str2 会打印完整内容,str1 会从被覆盖的地址处开始打印,直到超过栈顶遇到 '\0' 终结符。参见下面的图示可以了解这个过程:

从程序安全的角度来说,gets() 函数因为其潜在导致程序出错的风险,所以被 C11 标准废弃[1]。如果使用 scanf("%s", str) 这样的形式,并且标准输入流当中不包含空白字符[2],本质上和 gets(str) 没有区别,都存在导致缓冲区溢出的风险。

因此,从标准输入流中读入字符串时,应该尽量使用带有长度限定的函数,或是使用长度限定标记。比如 scanf("%10s", str) 就相当于 gets_s(str, 10),都实现了从标准输入流读入 10 个字符。

区别二:是否保留换行符

有时我们需要保留原始输入中的换行,便于在输出的时候保持原来输入的段落结构。但有时候我们只是需要每一行本身的内容,而不需要保留换行符 '\n'。针对这种场景,可以将这些函数划分为两类:

  • 保留换行符: fgets()
  • 不保留换行符: gets(), gets_s()scanf()

值得注意的是,考虑到前面讨论过的各个函数对输入长度的限制,fgets() 只有在本次读取没有耗尽全部允许读入的字数时才能够保留换行符。一旦超过,也会被保留在输入流中,等待后续取用。

区别三:对空白字符的处理

输入字符串的长度超过允许读入的字符限制 时 (也就意味着排除了 gets() 函数),如果读入的字符部分包含了空白字符[2:1],根据处理方式可以分为:

  • 无差别结束输入捕获: scanf()
  • '\n' 时结束输入捕获: fgets()gets_s()

这一特性使得 scanf() 可以作为 Latin 字符集输入的最佳单词拆分函数,根据空格和换行进行拆分读取。而 fgets()gets_s() 更适合按行处理的场景。

区别四:输入字符串超过长度限定的处理

注意,这里的字符串有一个前置条件:在长度不超过限定的范围内,不能包含换行符。我们面向这个场景提出对比,最主要是针对 fgets()gets_s() 两个函数。

既然不能包含换行符,那么意味着输入字符串在超长之前会被毫无丢弃地保留下来。但是,当超长时:

  • fgets() 会正常中断读入,剩余字符保留在输入缓冲区中;
  • gets_s() 会使用 '\0' 将字符串清空 (只处理首字符),然后 丢弃输入缓冲区剩余字符,并调用相应的“处理函数”。

所以,如果我们希望 在超长的情况下仍然能正常获取字符串,然后丢弃剩余内容,或者 只读入输入中的第一行,剩余内容不处理,就只能使用 fgets() 函数。但是如果一开始的前置条件不成立怎么办?让我们来进一步完善它吧!

进一步完善 fgets()

我们知道 fgets() 可以从标准输入流和文件流读入字符串,此时按照是否超过长度限定和是否包含换行符 '\n'

  1. 如果没有超过长度限定
    1. 遇到 '\n',会结束读入,然后包含 '\n' 符号;
    2. 没有遇到 '\n',会持续读入,直到输入耗尽;
  2. 如果超过长度限定
    1. 遇到 '\n',会结束读入,然后包含 '\n' 符号;
    2. 没有遇到 '\n',会持续读入,直到输入到达长度限定。

那么我们的逻辑就是:

  • Step 1: 当读入结束时,判断 fgets() 函数的返回值:
    • 如果是 NULL,结束;
    • 如果不是 NULL,跳转 Step 2
  • Step 2: 从头开始遍历长度限定范围内的全部字符,直到当前字符为 '\0''\n'
    • 如果是 '\n',说明遇到换行。此时应该把当前字符改成 '\0',然后结束 (剩余字符不处理);
    • 如果是 '\0',说明已经结束了,循环消耗流中的剩余字符直到为空。

以上是 C 语言中使用 stdio.h 提供的若干输入函数时需要注意的它们的限制、适用条件。请继续阅读另一部分关于输出函数的分享。

(未完待续)


  1. 注意,是否允许带有 gets() 的函数取决于你所使用的库是否遵从 C11 标准,而不是具体的 GCC 编译器。 ↩︎

  2. 空白字符包括但不限于空格符、空行符、换行符、制表符等。参见维基百科中的定义 Whitespace_character↩︎ ↩︎

标签:字符,函数,限定,输入输出,读入,字符串,长度,gets,输入
From: https://www.cnblogs.com/zhongdongy/p/input-output-of-c-streams.html

相关文章

  • 无涯教程-Interactive Ruby (irb) −函数
    交互式Ruby或irb是Ruby附带的交互式编程环境。它是由石冢启十先生撰写的。使用语法要调用它,请在shell或命令提示符下键入irb,然后开始输入Ruby语句和表达式。使用退出或退出退出irb。$irb[.rb][options][programfile][arguments]这是options的完整列表-Sr.No.Comma......
  • 无涯教程-RubyGems −函数
    RubyGems是Ruby的软件包实用程序,它可以安装Ruby软件包并使它们保持最新。使用语法$gemcommand[arguments...][options...]Example检查是否安装了RubyGems-$gem--version0.9.0RubyGems命令这是RubyGems所有重要命令的列表-Sr.No.Command&Description1build......
  • 多线程创建函数
    1、CreateThread()函数  CreateThread是一种微软在WindowsAPI中提供了建立新的线程的函数,该函数在主线程的基础上创建一个新线程。     线程终止运行后,线程对象仍然在系统中,必须通过CloseHandle函数来关闭该线程对象。1HANDLECreateThread(2LPSECURITY_ATT......
  • Linux中execl函数详解与日常应用!
    Linux中execl函数详解与日常应用execl是Linux系统中的一个系统调用,用于执行指定路径下的可执行文件。本文将详细介绍execl函数的使用方法和参数含义,并探讨其在日常开发中的常见应用场景和注意事项。1.execl函数概述execl函数属于Linux系统调用之一,其原型为:intexecl(constc......
  • php提前返回数据,后面代码继续执行封装函数
    /*中断并返回数据,后面程序继续执行,避免用户等待(immediate)*可用于返回值后,继续执行程序,但程序占得所以自由没有释放,一致占用,务必注意,最好给单独脚本执行*@paramstring|array$data字符串或数组,数组将被转换成json字符串*@paramintval$set_ti......
  • java to json / json to java使用FastJson对JSON字符串、JSON对象及JavaBean之间的相
    目录1、准备 2、javato json 3、jsontojava1、准备 1、json格式在线查看2.下载 阿里巴巴json 解析库alibaba/fastjson下载最新的jar包并且放在项目libs目录下,addaslib````2、javato json 比如我们想使用java 编写以下json格式数据{   "creatT......
  • Keil(关闭 函数和关键字灰色下划线)
    例如: 原因是使用了keil自带的软件包,不使用用keil自带的库就没有了, 可以去Project==》Manage==》Run-TimeEnvironment把使用的软件包去掉勾选去掉勾选使用的CMSIS里的CORE,重新编译就可以了。......
  • 函数
    一,定义对代码块和功能的封装。形式为:def函数名():函数体return(会返回结果,可有可无)注意,return后面的函数会不再执行。关于返回值:如果return什么都不写或者⼲脆不写return.那么返回的就是None如果return后⾯写了⼀个值.则调⽤者可以接收⼀个......
  • AutoCAD(VBA)引用excel函数
    AutoCAD(VBA)要引用excel可以把它定义成为一个对象,进行引用。'CAD调用EXCEL程序PublicFunctionexcelActive()AsObjectDimxlappAsObjectSetxlapp=GetObject(,"Excel.application")SetexcelActive=xlapp.ActiveSheetEndFunction......
  • 【数据结构】数组和字符串(四):特殊矩阵的压缩存储:稀疏矩阵——三元组表
    4.2.1矩阵的数组表示【数据结构】数组和字符串(一):矩阵的数组表示4.2.2特殊矩阵的压缩存储  矩阵是以按行优先次序将所有矩阵元素存放在一个一维数组中。但是对于特殊矩阵,如对称矩阵、三角矩阵、对角矩阵和稀疏矩阵等,如果用这种方式存储,会出现大量存储空间存放重复信息或零......