如何在C语言中只接受特定长度的字符串输入,否则要求用户重新输入字符串

4
如何在C语言中接受一组字符串作为输入,并在超过某个长度时提示用户重新输入字符串。我尝试了以下代码:
#include<stdio.h>
int main()
{
    char arr[10][25]; //maximum 10 strings can be taken as input of max length 25
    for(int i=0;i<10;i=i+1)
    {
        printf("Enter string %d:",i+1);
        fgets(arr[i],25,stdin);
    }
}

然而,fgets函数允许输入比指定长度更长的字符串,如果用户敲击回车键,则需要将第二个字符串作为输入。我对C语言不太熟悉。


4
将输入传递到一个单独的、更大的缓冲区。如果输入中不包含换行符,则被截断,因此应该继续读取直到遇到换行符,并将其舍弃,就像整行过长时一样。当您获取到一个可接受的字符串后,将其复制到数组中(可能要减去换行符)。 - Weather Vane
4
请记住,如果fgets()由于长度而截断一行,那么该行的其余部分不会被丢弃,而是留在输入中。fgets()保留换行符,因此如果没有换行符(并且缓冲区已满),则该行太长了。 - Weather Vane
1
@WeatherVane 我经常使用稍大一些的缓冲区来解决这个问题,但是这样会增加内存管理和文件结尾问题。因此,我尝试了下面的解决方案:可以使用原始目标缓冲区。 - chux - Reinstate Monica
@chux-ReinstateMonica 我按照早期评论中所描述的方式发布了一个解决方案。 - Weather Vane
3个回答

1
如何仅接受特定长度的字符串输入?
形成一个辅助函数来处理各种边缘情况。 使用 fgets(),然后去掉潜在的 '\n'(fgets() 会保留它)并检测长输入。
以下是一些未经测试的代码,供 OP 参考:
#include <assert.h>
#include <stdio.h>

// Pass in the max string _size_.
// Return NULL on end-of-file without input.
// Return NULL on input error.
// Otherwise return the buffer pointer.
char* getsizedline(size_t sz, char *buf, const char *reprompt) {
  assert(sz > 0 && sz <= INT_MAX && buf != NULL); // #1
  while (fgets(buf, (int) sz, stdin)) {
    size_t len = strlen(buf);
    // Lop off potential \n
    if (len > 0 && buf[--len] == '\n') {   // #2
      buf[len] = '\0';
      return buf;
    }
    // OK if next ends the line
    int ch = fgetc(stdin);
    if (ch == '\n' || feof(stdin)) {       // #3 
      return buf;
    }

    // Consume rest of line;
    while (ch != '\n' && ch != EOF) {      // #4
      ch = fgetc(stdin);
    }
    if (ch == EOF) {                       // #5
      return NULL;
    }

    if (reprompt) {
      fputs(reprompt, stdout);
    }
  }
  return NULL;
}

不常见的问题:读取null字符仍然是一个待定问题。

针对学习者的详细说明:

  1. 一些对于合理输入参数的测试。大小为零不允许任何输入保存为一个null字符终止的字符串。缓冲区可以比INT_MAX更大,但fgets()不能直接处理它。代码可以被修改以处理0和巨大的缓冲区,但留待以后再说。

  2. fgets()并不总是读取'\n'。缓冲区可能首先被填满,或者在文件结束之前最后一行可能缺少'\n'。不常见地,可能会读取null字符 - 甚至是第一个字符,因此需要进行len > 0测试,使strlen()无法确定读取的字符长度。代码需要重大改变来适应确定大小,如果null字符输入需要详细支持。

  3. 如果之前的fgets()填充了它的缓冲区,并且下一个读取字符的尝试导致文件结束或'\n',则此测试为真,这是可以的,因此返回成功。

  4. 如果之前的fgetc()导致输入错误,则此循环立即退出。否则,我们需要消耗剩余的行,寻找'\n'EOF(这可能是由于文件结束或输入错误引起的)。

  5. 如果返回了EOF(由于文件结束或输入错误),没有继续的理由。返回NULL

用法

// fgets(arr[i],25,stdin);
if (getsizedline(arr[i], sizeof(arr[i]), "Too long, try again.\n") == NULL) {
  break;
}

我认为尝试使用一个函数来做它本不应该做的事情可能有些过头了。fgets 用于文本输入。如果您在一个安全环境中工作,其中坏的输入(例如输入流中的空值)是一个问题,那么就不要使用 fgets。否则,您可以安全地接受一些输入可能会丢失到您的程序中,包括因为达到 EOF 或简单地没有需要读取的输入而导致程序失败。 - Dúthomhas
@Dúthomhas回复:“可能有点过度了”:此答案特别提到它存在处理_null字符_的问题。该函数没有广泛的补救关注点。_Null字符_的存在不仅是一个安全环境的问题。_Null字符_也会出现在UTF16文本文件中。用户很容易错误地尝试从其中重定向输入。这个函数与 '\0' 一起使用不太好,除非它没有展示UB,否则可能会发生。换句话说,len>0 是这段代码中唯一与读取null字符相关的部分。 - chux - Reinstate Monica
你正在尝试使用fgets()读取UTF-16数据? - Dúthomhas
@Dúthomhas 不,代码并不是为了读取UT16而设计的。只是有些文本文件是UTF16格式的,读取这样的文件不应该导致UB(未定义行为),只是可能无法正常工作。 - chux - Reinstate Monica
同意。我只是认为在尝试使用这样的函数读取文件之前,应该先处理那个条件。 - Dúthomhas

0

这段代码使用的缓冲区比所需的最大长度略大。如果一个文本行 和换行符 无法读入缓冲区,它会读取剩余的行并将其丢弃。如果可以读入,它会再次丢弃过长(或过短)的行。

#include <stdio.h>
#include <string.h>
#include <stdbool.h>

#define INPUTS  10
#define STRMAX  25

int main(void) {
    char arr[INPUTS][STRMAX+1];
    char buf[STRMAX+4];
    for(int i = 0; i < INPUTS; i++) {
        bool success = false;
        while(!success) {
            printf("Enter string %d: ", i + 1);
            if(fgets(buf, sizeof buf, stdin) == NULL) {
                exit(1);                    // or sth better
            }

            size_t index = strcspn(buf, "\n");
            if(buf[index] == '\0') {        // no newline found
                // keep reading until end of line
                while(fgets(buf, sizeof buf, stdin) != NULL) {
                    if(strchr(buf, '\n') != NULL) {
                        break;
                    }
                }
                if(feof(stdin)) {
                    exit(1);                // or sth better
                }
                continue;
            }

            if(index < 1 || index > STRMAX) {
                continue;                   // string is empty or too long
            }

            buf[index] = '\0';              // truncate newline
            strcpy(arr[i], buf);            // keep this OK string
            success = true;
        }
    }
    
    printf("Results:\n");
    for(int i = 0; i < INPUTS; i++) {
        printf("%s\n", arr[i]);
    }
    return 0;
}

如果输入的最后一行足够短,但以结束符而不是 '\n' 结尾,那么此答案将不会将该行视为成功。当然,最后一行没有 '\n' 是一个实现定义的问题。 - chux - Reinstate Monica
@chux-ReinstateMonica 这可能是从文件重定向输入的情况,但否则输入不太可能在没有换行符的情况下终止。 - Weather Vane
也许不太可能,但是检测和处理并不那么困难。 - chux - Reinstate Monica
边界情况:while(fgets(buf,...) { if(feof(stdin)) 测试忽略了由于输入错误导致的 fgets() 返回NULL。当然,输入错误甚至比上述问题更少见。 - chux - Reinstate Monica
理想情况下,您是正确的,字符串长度也应该被检查。正如原始评论所述,如果缓冲区没有满并且缺少换行符,则可以接受。 - Weather Vane

0

fgets() 的好处在于它会将行终止符换行符 ('\n') 放入输入缓冲区中。你所要做的就是寻找它。如果存在,你就得到了一整行的输入。如果不存在,那么还有更多内容需要读取。

因此,策略是:

fgets( s, size_of_s, stdin );
char * p = strpbrk( s, "\r\n" );
if (p)
{
  // end of line was found.
  *p = '\0';
  return s; (the complete line of input)
}

如果 pNULL,则还有更多的工作要做。由于您希望简单地忽略过长的行,这与丢弃输入是相同的。使用简单的循环来进行处理:
int c;
do c = getchar(); while ((c != EOF) && (c != '\n'));

流通常在后台由C库或操作系统(或两者)进行缓冲,但即使它们没有,这也不是太大的开销。(在玩“我是优化编译器”之前,请使用分析器。不要对C库做出不好的假设。)

一旦你把不想要的东西都扔掉了(到EOL),确保你的输入不是EOF并循环询问用户再试一次。

将所有内容放在一起

char * prompt( const char * message, char * s, size_t n )
{
  while (!feof( stdin ))
  {
    // Ask for input
    printf( "%s", message );
    fflush( stdout );  // This line _may_ be necessary.

    // Attempt to get an entire line of input
    if (!fgets( s, n, stdin )) break;
    char * p = strpbrk( s, "\r\n" );

    // Success: return that line (sans newline character(s)) to the user
    if (p)
    {
      *p = '\0';
      return s;
    }

    // Failure: discard the remainder of the line before trying again
    int c;
    do c = getchar(); while ((c != EOF) && (c != '\n'));
  }

  // If we get this far it is because we have 
  // reached EOF or some other input error occurred.
  return NULL;
}

现在你可以轻松地使用这个实用函数:

char user_name[20];  // artificially small

if (!prompt( "What is your name (maximum 19 characters)? ", user_name, sizeof(user_name) )) 
{
  complain_and_quit(); 
  // ...because input is dead in a way you likely cannot fix.
  // Feel free to check ferror(stdin) and feof(stdin) for more info.
}

这个小的prompt函数只是一种您可以编写的辅助实用程序函数的示例。您可以做一些像为用户不遵守您的指令而添加额外提示的事情:

你叫什么名字?John Jacob Jingleheimer Schmidt
唉,我只能输入19个字符。请再试一次:
你叫什么名字?John Schmidt
你好 John Schmidt。


不带 '\n' 的适合于 message[] 的输入,但由于文件结束而结束的输入在此处不被视为成功。这是重新定向输入时的一个合理关注点。罕见的是:如果 c = getchar(); 由于输入错误返回 EOF,则代码会错误地继续执行,因为输入错误并不一定是粘性的。 - chux - Reinstate Monica
什么?① message 被标记为 const。② 文本文件应以换行符结尾,一直都是这样的。③ 即使它们没有,fgets() 仍然可以正常工作;因此,这个函数也是如此。④ 不,fgets()getchar() 都适用于字节输入。任何在两次调用之间神奇地解决的错误都比这段代码可以合理处理的问题更大。⑤ 你为什么生气呢? - Dúthomhas
  1. 打字错误,应该是在s[]中而不是带有'\n'
  2. "文本文件应以换行符结尾,并且一直如此。"缺乏支持。C规范指出:“文本流是由组成“行”的有序字符序列组成的,每行由零个或多个字符加上终止的新行字符组成。最后一行是否需要终止新行字符是实现定义的。”这清楚地允许实现进行决定。
  3. “当它们不这样做时;”不清楚。
- chux - Reinstate Monica
  1. 由于输入错误不是“粘性”的,而且一旦出现EOF,代码就不会再尝试读取,因此处理起来并不难。
  2. 不要生气。问题在于代码和答案,而不是回答者。最好保持这种方式。
- chux - Reinstate Monica
这个答案的限制在于,由于用户不想在最终目标缓冲区中保存 '\n',因此将 "John Schmidt" 读入到 char user_name[13] 中需要一个大小为14的缓冲区来临时读取和保存 '\n' - chux - Reinstate Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接