【问题标题】:Saving getline() output to an external array将 getline() 输出保存到外部数组
【发布时间】:2021-08-31 10:10:40
【问题描述】:

外部数组 srclns 应该从文本文件中保存每个读取的行。但是之后阅读它的内容似乎读取的行是空字符串。我在下面的代码中缺少什么?

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

#define MAXSRC 20


char *srclns[MAXSRC];   /* source lines */

size_t read_lines(char *path)
{
  FILE *stream;
  ssize_t read;
  char *lnptr;
  size_t n;
  size_t count;
  stream = fopen(path, "r");
  lnptr = NULL;
  n = 0;
  count = 0;
  if (!stream) {
    fprintf(stderr, "Can't open source '%s'\n", path);
    exit(EXIT_FAILURE);
  }
  while ((read = getline(&lnptr, &n, stream)) != -1) {
    srclns[count++] = lnptr;
  }
  free(lnptr);
  fclose(stream);
  return count;
}


int main()
{

  size_t n = read_lines("foo.txt");
  for (size_t i = 0; i<n; i++)
    printf("%zu %s\n", i, srclns[i]);
  exit(EXIT_SUCCESS);
}

这之后只打印带有看似空字符串的行号:

0 
1 
2 
3 
4 
5 

【问题讨论】:

  • 根据手册,每次调用getline,如果要分配新内存,需要将lnptr重置为NULLman7.org/linux/man-pages/man3/getline.3.html。另外,在打印出这些行之前,您不应该free(lnptr);
  • 我无法阅读手册中有关分配新内存的任何内容。你能指出来吗?至于释放,我是把 lnptr 保存在数组里,不是吗?
  • 指针不是它的内容,会被覆盖。
  • @Student: "如果在调用之前*lineptr 被设置为NULL,那么getline() 将分配一个缓冲区来存储该行。"
  • @Yun 我认为你是对的,我错过了关于在读取失败时必须释放的部分

标签: c getline


【解决方案1】:

因此,据我所知,您的程序不仅无法运行,而且可能存在内存泄漏。这是由于getline 使用动态分配的行为所致。

让我们仔细看看你的程序做了什么,特别是while ((read = getline(&amp;lnptr, &amp;n, stream)) != -1)循环:

getline 将与 &amp;lnptr 一起使用,它的类型为 char**

  • 如果指针是NULL,它将在堆(动态)上分配足够的内存来存储正在读取的行。
  • 如果指针不是NULL,那么它应该指向大小为n 的缓冲区
    • 如果缓冲区足够大(大于或等于行长),它用于存储字符串。
    • 如果缓冲区太小,则内存由getline 重新分配,因此有足够大的缓冲区可用。在重新分配时,n 会更新为新的缓冲区大小。在某些情况下,重新分配意味着lnptr 必须被修改并且将会被修改。 (如果在当前缓冲区之后没有足够的连续可用内存,则可能会发生这种情况。在这种情况下,内存将被分配到堆上的其他地方。如果您对此感兴趣,我建议您进行研究,因为动态内存分配是一个相当复杂的主题,否则只知道指针可能会改变,现在就足够了)。

现在这里是你的程序的问题(至少这是我可以从我所拥有的信息中推断出来的。我可能错了,但这似乎是最合理的解释):

  • 循环的第一次迭代 lnptrNULL。因此 getline 在堆上分配内存并存储该行,并更新 lnptr 以指向新分配的缓冲区。
  • 在循环中,您将指向分配缓冲区的指针存储在srclns[0]
  • 在随后的迭代中缓冲区会被覆盖,并且可能会被 getline 调整大小,而您仍将指针存储到同一缓冲区 srclns[count]
  • 循环之后你释放缓冲区并丢弃srclns中每个指针指向的内存。
  • 当您打印时,您很可能读取了无效的内存区域(即您刚刚释放的指针指向的区域),幸运的是它似乎以终止字符开头(文件的最后一行可能是空行,并且在空闲后没有任何主动更改此内存区域...)

如何解决: 您可以使用malloc 和/或calloc 显式处理动态分配,但这似乎有点复杂,如前所示,getline 可以为您处理它。我的建议如下:

  1. srclns 中的所有元素设置为NULL
    for(int i = 0; i < MAXSRC; ++i)
    {
       srclns[i] = NULL;
    }
    
  2. 然后重新设计 while 循环以在每次迭代中传递 srclns 的新元素。每次调用getline 都会看到一个NULL 指针,从而分配内存并更新srclns 的单元格以指向它。这个实现的好处是你肯定永远不会超出srclns的范围:
    for(int i = 0; i < MAXSRC; ++i)
    {
        n = 0
        if(getline(&srclns[i], &n, stream) == -1)
        {
            break;  // We get out if we reached OEF
        }
    }
    
  3. 在您为printf 访问主内存后,释放所有分配的内存
    for(int i = 0; i < MAXSRC; ++i)
    {
      if(srclns[i] != NULL)
      {
         free(srclns[i]);
      }
    }
    
  4. 调整。我没有对代码进行测试,所以我可能犯了一些错误……请随时纠正。您可能还想调整代码以满足您的需求。

【讨论】:

    【解决方案2】:

    函数getline 只会在lnptrNULL (ref) 时分配内存。这是第一次迭代的情况,但之后需要将其重置为NULL

    while ((read = getline(&lnptr, &n, stream)) != -1) {
      srclns[count++] = lnptr;
      lnptr = NULL;
    }
    

    否则,lnptr 仍将指向在第一次迭代中为所有后续迭代分配的内存,getline 将反复尝试写入该位置。

    即使这不是问题的原因,分配的内存也应该被释放。例如,通过在exit(EXIT_SUCCESS) 之前添加这些行:

    for (size_t i = 0; i<n; i++)
      free(srclns[i]);
    

    是否使用getline 是一种好习惯是您可能想要研究的另一个讨论。它不是最便携的解决方案。

    【讨论】:

    • @Student getline 的问题是它不能保证可用(另见here。便携式替代品是scanf(如果你小心避免缓冲区溢出),并且使用fgets 用于读取,strcpy 用于复制字符串。请注意,这确实需要您将读取行处理到固定大小的缓冲区中。
    • @Student getline 的另一个问题是,至少目前,它似乎使用的最小分配大小为 120 (ref)。根据您的输入,这可能会浪费内存。
    • 也不是主要问题,但仍然...不要忘记检查count &lt; MAXSRC
    • @LoïcNiederhauser 鉴于防御性编程确实会更好,但也许 Student 想提供一个简洁的代码 sn-p 或者绝对确定在这个理论示例中输入永远不会更大。如果您要检查该界限,那么理想情况下也必须有一条错误消息或重新分配,并且最好检查getline 的错误状态,并进行适当的处​​理。无论如何,我只是想提供一个直接的答案并使代码正常工作。下一个讨论点是是否有足够的理由使用不可移植的功能。
    • @Yun 你说得对。你提出的所有观点都是合理的,你提到它们是一件好事。我担心的是,不检查计数是唯一可能导致不安全的内存访问的原因,而 c 可能很难调试。但你也是对的,它确实使代码(有点)更重,并且在一个非常有针对性的练习中没有必要。
    猜你喜欢
    • 1970-01-01
    • 2012-04-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-09
    • 2019-07-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多