添加多个字符串时的scanf问题

scanf problem when adding multiple strings

所以我有一个代码,它输入任意数量的任意长度的行。每行通常包含一个或多个由空格分隔的单词。我必须输出那个字符串。

So this my code. I am newbie in C, and don't be so rude for my dumb-code ;)

#include <stdio.h>
#include <stdlib.h>

int main () {
    int i = 1;
    char **list = malloc(1000 * sizeof(char*));
    while(!feof(stdin)){
        list[i] = malloc(1000 * sizeof(char));
        printf("Enter string with num %d: ", i);
        scanf("%99[^\n]", list[i]);
        i++;
    }
    printf("\n");
    for (int j = 1; j < i; j++)
        printf("Your strings by num %d: %s\n", j, list[j]);

    return 0;
}

但是我的 scanf 有问题,我无法在空格后继续输入。 我觉得我的问题是

scanf("%99[^\n]", list[i]);

除此之外,我无法使用 gets()、getchar() 系列以及规范 %c 和 %m。

%99[^\n]扫描直到遇到\n。所以第一行可以顺利阅读。但是由于\n还没有被扫描,它留在了stdin的未读部分。下次扫描的时候又遇到'\n',其实扫描的是一个空字符串。所以使用 %99[^\n]%*c %*c 部分意味着你忽略一个字符。

你做了一件肯定会导致你的逻辑出现问题的第一件事。您将 i 混合为基于 1 的(用于计数),但对于索引应该基于 0。在 C 中,所有数组和存储都是从零开始的。第一个元素是元素0。当您开始将基于 1 的计数器与基于 0 的索引混合使用时,您的逻辑就会变得复杂,并且必须考虑变量用作计数器或索引的每个实例中的偏移 - 尽量避免这种情况。

而不是初始化 int i = 1; 初始化为 int i = 0; 您将其用作索引。当您需要作为计数器输出时,只需使用 i + 1。在您的情况下,这意味着您正在分配 1000 指针,但您从不使用第一个。 (你可以这样做——但它只是马虎)总是从 0 索引并通过添加到索引来根据需要调整输出编号。 (也不仅仅是使用 i 来跟踪分配的指针数量,只是一个更具描述性的变量名称,例如 n_pointersnptrs

如评论中所述,由于您是从键盘获取用户输入,因此在开始时预分配 1000 个指针没有任何好处——没有提高程序效率。在用户键入 "cat" 的时间内,您可以重新分配一个新指针并分配存储 1000 次而不会引入任何延迟。

此处更好的方法是简单地声明一个固定缓冲区(字符数组)以用于所有输入,然后在您验证接收到正确的输入后,只需 realloc() 一个新指针并为输入。这样你就不会过度分配指针。

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

#define MAXC 1000       /* if you need a constant, #define one (or more) */

int main () {
    
    size_t nptrs = 0;       /* counter for no. pointers allocated */
    char **list = NULL,     /* list and fixed buffer for read */
         buf[MAXC];
    ...

由于您显然无法使用 scanf(),如评论中所述,请花一个小时阅读手册页——这将使您以后免去 100 小时的痛苦。 scanf() 对新的 C 程序员来说充满了陷阱,比如知道哪些 转换说明符 丢弃前导白色 space 以及哪些不丢弃……还有 50 个陷阱就是喜欢

此处您要读取用 scanf() 分隔的白色 space 单词,因此您将使用 %[...] 转换说明符读取所有字符,不包括 '\n' 特点。 (课程,%[...]%c%n 是不丢弃前导白色 space 的转换。因此,在您的情况下,要丢弃前导白色 space,您需要在 " %[^\n]".

之前包含一个 space

您还需要知道消耗的字符数,以便您可以对照您的 999 field-width 限制来了解您刚刚尝试的行是否read 太长,放不下。您可以使用伪转换 %n 来获取消耗的字符数,直到它出现在您的 格式字符串 .

中为止

由于您的读取未包含末尾的 '\n' 字符,如果您的缓冲区已填充 999 个字符(加上空终止字符),则该行中的其他字符将保持未读状态您将需要阅读并丢弃,以便您的下一次阅读将从下一行开始。

(您通常会使用 getchar() 进行循环,直到找到 '\n',但由于您不能使用它,您将再次使用 scanf() 但不会丢弃前导白色space)

每当你循环收集用户输入时(尤其是当需要像整数值这样的特定输入时),你将围绕你的输入例程使用一个连续的循环来要求用户提供所需的输入类型以及特定值范围内的任何输入。当您收到并验证是否提供了所需的输入时,您只需 break 您的读取循环。否则,您只需再次循环,直到获得所需的内容。验证任何用户输入的关键是检查return(如果需要,值在限制范围内)。

将读取方法放入固定缓冲区,验证并在行太长时丢弃字符,您可以这样做:

    ...
    puts ("input\n");
    while (1) { /* loop continually until manual EOF */
        int nchar, rtn;     /* no. of chars consumed, scanf return */
        
        printf ("Enter string with num %zu: ", nptrs + 1);  /* prompt */
        rtn = scanf (" %999[^\n]%n", buf, &nchar);          /* save return */
        if (rtn == EOF) {                                   /* check EOF */
            puts ("EOF\n\noutput\n");
            break;
        }
        if (nchar == 999) { /* check line-too-long */
            fputs ("errro: line exceeds MAXC chars.\n", stderr);
            while (nchar == 999) {  /* loop discarding remainder of line */
                if (scanf ("%999[^\n]%n", buf, &nchar) != 1) {
                    goto getnextline;   /* skip over allocation/storage */
                }
            }
        }
        ...

现在为每个输入重新分配一个额外的指针和存储空间,并从固定缓冲区复制到分配的存储空间,您可以这样做:

        ...
        /* always realloc using temp pointer or risk memory leak */
        void *tmp = realloc (list, (nptrs + 1) * sizeof *list);
        if (!tmp) {
            perror ("realloc-list");
            break;
        }
        list = tmp;         /* assign realloc'ed block of ptrs to list */
        
        list[nptrs] = malloc (nchar + 1);           /* allocate for string */
        memcpy (list[nptrs], buf, nchar + 1);       /* don's scan for \n */
        
        nptrs += 1;         /* increment pointer count */
        getnextline:;
    }
    ...

剩下的就是输出你的台词(并移动你的索引以匹配你的输入提示)。由于您要结束程序,因此不要忘记 free() 每行的存储空间,然后在末尾释放指针。是的,当程序退出时会发生这种情况,但您不会总是在 main() 中分配,并且未能释放您在函数中分配的内存将在您的程序中造成内存泄漏。所以要早点养成好习惯。始终跟踪您的内存分配和 free() 不再需要的内存,例如

    ...
    for (size_t j = 0; j < nptrs; j++) {
        printf("Your strings by num %zu: %s\n", j + 1, list[j]);
        free (list[j]);     /* free storage for string */
    }
    free (list);            /* don't forget to free pointers */
}

就是这样。这将获取所有需要的输入,消除对可以输入的行数的任何限制(直到虚拟内存的限制),输出所有存储的行并在退出前释放所有分配的内存。

示例Use/Output*

$  ./bin/scanflines
input

Enter string with num 1: string with   1
Enter string with num 2:       string with   2
Enter string with num 3:      string with   3
Enter string with num 4:     string with   4
Enter string with num 5:    string with   5
Enter string with num 6:   string with   6
Enter string with num 7:  string with   7
Enter string with num 8: string with   8
Enter string with num 9: EOF

output

Your strings by num 1: string with   1
Your strings by num 2: string with   2
Your strings by num 3: string with   3
Your strings by num 4: string with   4
Your strings by num 5: string with   5
Your strings by num 6: string with   6
Your strings by num 7: string with   7
Your strings by num 8: string with   8

内存Use/Error检查

在您编写的任何动态分配内存的代码中,您对分配的任何内存块负有 2 责任:(1) 始终保留指向内存块的起始地址 因此,(2) 当不再需要它时可以 释放

您必须使用内存错误检查程序来确保您不会尝试访问内存或写入 beyond/outside 您分配的块的边界,尝试读取或基于未初始化的条件跳转值,最后,确认您释放了所有已分配的内存。

对于Linux valgrind是正常的选择。每个平台都有类似的内存检查器。它们都很简单易用,只需运行你的程序就可以了。

$ valgrind ./bin/scanflines
==7141== Memcheck, a memory error detector
==7141== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==7141== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==7141== Command: ./bin/scanflines
==7141==
input

Enter string with num 1: string with   1
Enter string with num 2:       string with   2
Enter string with num 3:      string with   3
Enter string with num 4:     string with   4
Enter string with num 5:    string with   5
Enter string with num 6:   string with   6
Enter string with num 7:  string with   7
Enter string with num 8: string with   8
Enter string with num 9: EOF

output

Your strings by num 1: string with   1
Your strings by num 2: string with   2
Your strings by num 3: string with   3
Your strings by num 4: string with   4
Your strings by num 5: string with   5
Your strings by num 6: string with   6
Your strings by num 7: string with   7
Your strings by num 8: string with   8
==7141==
==7141== HEAP SUMMARY:
==7141==     in use at exit: 0 bytes in 0 blocks
==7141==   total heap usage: 18 allocs, 18 frees, 2,492 bytes allocated
==7141==
==7141== All heap blocks were freed -- no leaks are possible
==7141==
==7141== For counts of detected and suppressed errors, rerun with: -v
==7141== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

始终确认您已释放所有分配的内存并且没有内存错误。

这里有很多东西需要消化,所以慢慢来,确保你理解为什么每一行都是这样写的,以及它在做什么。如果您有任何疑问,请在下方发表评论。

最后,要知道在这种情况下,将 fgets() 转换为 buffer 比使用 scanf() 更好,更值得推荐。但由于它似乎是不受限制的,它可以用 scanf() 完成,并且是一个很好的学习练习——但是没有学习练习,fgets() 是更好的选择。

完成程序

为方便起见:

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

#define MAXC 1000       /* if you need a constant, #define one (or more) */

int main () {
    
    size_t nptrs = 0;       /* counter for no. pointers allocated */
    char **list = NULL,     /* list and fixed buffer for read */
         buf[MAXC];
    
    puts ("input\n");
    while (1) { /* loop continually until manual EOF */
        int nchar, rtn;     /* no. of chars consumed, scanf return */
        
        printf ("Enter string with num %zu: ", nptrs + 1);  /* prompt */
        rtn = scanf (" %999[^\n]%n", buf, &nchar);          /* save return */
        if (rtn == EOF) {                                   /* check EOF */
            puts ("EOF\n\noutput\n");
            break;
        }
        if (nchar == 999) { /* check line-too-long */
            fputs ("errro: line exceeds MAXC chars.\n", stderr);
            while (nchar == 999) {  /* loop discarding remainder of line */
                if (scanf ("%999[^\n]%n", buf, &nchar) != 1) {
                    goto getnextline;   /* skip over allocation/storage */
                }
            }
        }
        /* always realloc using temp pointer or risk memory leak */
        void *tmp = realloc (list, (nptrs + 1) * sizeof *list);
        if (!tmp) {
            perror ("realloc-list");
            break;
        }
        list = tmp;         /* assign realloc'ed block of ptrs to list */
        
        list[nptrs] = malloc (nchar + 1);           /* allocate for string */
        memcpy (list[nptrs], buf, nchar + 1);       /* don's scan for \n */
        
        nptrs += 1;         /* increment pointer count */
        getnextline:;
    }
    
    for (size_t j = 0; j < nptrs; j++) {
        printf("Your strings by num %zu: %s\n", j + 1, list[j]);
        free (list[j]);     /* free storage for string */
    }
    free (list);            /* don't forget to free pointers */
}