动态分配指针数组(K&R 练习 5-13)

Dynamically allocate an array of pointers (K&R Exercise 5-13)

我一直在研究 K&R 并试图解决练习 5-13,其中指出

Write the program tail, which prints the last n lines of its input. By default, n is 10, say, but it can be changed by an optional argument, so that

tail -n

prints the last n lines. The program should behave rationally no matter how unreasonable the input or the value of n. Write the program so it makes the best use of available storage; lines should be stored as in the sorting program of Section 5.6, not in a two-dimensional array of fixed size.

这是我的算法

这是我为此编写的代码

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

#define MAXLEN 1000

int my_getline(char line[], int maxline)
{
  int c, n = 0;

  while (((c = getchar()) != EOF) || (c != '\n')) 
    line[n++] = c;
  if (c == '\n')
    line[n++] = c;
  line[n] = '[=10=]';

  return n;
}

int readlines(int n, char **pa)
{
  int len, nlines = -1;
  char *p, line[MAXLEN];

  nlines = 0;
  while ((len = my_getline(line, MAXLEN)) > 0) {
    if ((p = (char *) malloc(len)) == NULL)
      return -1;
    else {
      line[len-1] = '[=10=]';
      strcpy(p, line);

      pa[++nlines % n] = p;
    }
  }
  return nlines;
}

void writelines(char **pa, int n, int nlines)
{
  int j;

  for (j = nlines - n; j < nlines; j++) {
    printf("%s\n", *pa[j % n]);
  }
}

int main(int argc, char *argv[])
{
  int n, nlines;
  char **pa;

  (argc == 1) ? (n = 10) : (n = atoi(*++argv));
  pa = (char *) malloc(n * sizeof(char*));
  nlines = readlines(n, &pa);
  writelines(&pa, n, nlines);

  free(pa);
  return 0;
}

我有两个问题

  1. 很明显,我对 pa(指针数组)的解释在某处是错误的,因为当我将 pa 传递给 readlines 和 writelines 并尝试写入时,我遇到了很多错误。我该如何解决这些问题?
  2. 我知道完成后,您应该释放内存。我可以使用 free(pa) 释放指针数组 (pa) 的内存,但是如何在 readlines 中释放 p 的内存。问题指出我应该使 "best use of available storage" 意味着在我阅读了 n 行之后,理想情况下我应该在第 11 行时释放第一行,在读取第 12 行时释放第二行,依此类推。但是我不知道该怎么做。

提前致歉。我是 C 的新手,这个指向指针业务的指针以及动态内存分配真的让我很困惑。

本次回答只关注这部分:

how would one go about freeing the memory of p in readlines.

原则上只需要遍历pa指向的内存中的指针,一个一个释放即可。喜欢

for (int i = 0; i < n; ++i) free(pa[i]);
free(pa);

但是,有一个小问题:您无法知道在 readlines 中有多少指针被分配了 malloced 值。

要解决该问题,您可以将所有指针初始化为 NULL。然后在所有指针上调用 free 是安全的,因为用 NULL 指针调用 free 总是有效的。

喜欢:

pa = malloc(n * sizeof(char*));           // or better: pa = malloc(n * sizeof *pa);
for (int i = 0; i < n; ++i) pa[i] = NULL; // make all pointers equal to NULL

... do your stuff ...

for (int i = 0; i < n; ++i) free(pa[i]);
free(pa);

注意:您可以使用calloc代替malloc并避免初始化循环。但是,为了简单起见,我继续 malloc

也就是说,这里还有一个问题:

pa[++nlines % n] = p;

这里覆盖pa指向的指针。因此,您可能会覆盖指向某些 malloced 内存的指针 - 这很糟糕。请务必先致电 free

int tmp = ++nlines % n;
free(pa[tmp]);            // pa[tmp] may be NULL but that is OK
pa[tmp] = p;

此解决方案需要对 pa 指向的指针进行 NULL 初始化。

顺便说一句:此代码行可以工作

(argc == 1) ? (n = 10) : (n = atoi(*++argv));

但在我看来它有一个 "smell"。

我会保持更简单,例如:

int n = 10;
if (argc == 2)
{
    n = atoi(argv[1]);  
}

此外,atoi 不是最佳解决方案 - 请参阅 Why shouldn't I use atoi()?

好吧,很明显您正在沿着正确的逻辑线路思考以达到模拟的 tail 线路,但您似乎在如何处理内存分配、重新分配和释放方面遇到困难。 (这很可能是练习的重点)。

虽然没有什么可以阻止您在 main() 中为 pa 分配指针并将该参数传递给 readlines(),但这样做有点笨拙。当您考虑创建一个将为对象分配存储空间的函数时,让该函数为完整的对象分配 return 一个指向该对象的指针,如果成功,或者 return NULL 失败。这样调用函数就知道函数 return 是否是一个有效的指针,它负责释放与对象关联的内存(而不是在不同的地方分配一些内存)。如果函数 returns NULL -- 调用者知道函数失败并且不需要担心对象的任何内存。

这也使您不必为对象传递参数。由于您将在函数中分配一个完整的对象,只需将 return 类型更改为您的对象类型(此处为 char**)并传递一个 指向 [=97 的指针=] 保存要输出的行数的内存。为什么是指针?如果存储的行数少于该行数(因为正在读取的文件行数较少,或者您 运行 在存储所有行之前内存不足),您可以使用实际行数更新该地址的值存储并使该号码在呼叫者中可用(main() 此处)。

通过这些更改,您可以将函数声明为:

char **readlines (int *n)
{

在你的函数中你需要声明一个行计数器,一个缓冲区来保存从文件中读取的行(我假设你的 MAXLEN 是为了)并为你的对象声明和分配指针,验证每个分配。例如:

    int ndx = 0;    /* line counter */
    char buf[MAXLEN], **pa = malloc (*n * sizeof *pa);  /* allocate pointers */

    if (!pa) {                      /* validate pointer allocation */
        perror ("malloc-pa");
        return pa;
    }
    for (int i = 0; i < *n; i++)    /* initialize all pointers NULL */
        pa[i] = NULL;

注意上面,所有指针都已初始化 NULL,这将允许 realloc() 处理初始分配和任何需要的重新分配。另请注意,您可以使用 calloc 而不是对指针使用 malloc ,这会将所有字节设置为零(对于我知道的所有编译器,导致指针评估为 NULL 而无需显式循环设置它们)。然而,标准并未保证运行——因此循环是正确的。

这里 fgets() 用于读取每一行, strcspn() 用于 trim 和 '\n' 并获取每行的长度 - 你可以使用任何你喜欢的功能。读取行后,trimmed 和 length 获得,分配(或重新分配)内存以保存该行,并将该行复制到新的内存块。您的 nlines % n 索引是正确的,但是在分配和赋值之前您不会增加 nlines,例如

(注意: 在下面编辑以将任何行重新分配失败视为终端,并释放所有内存 returning NULL 如评论中所讨论@4386427 -- 由于索引的循环使用以及最初分配的所有行之后的任何失败将导致部分结果不可用(非顺序行输出))

    while (fgets (buf, MAXLEN, stdin)) {        /* read each line of input */
        void *tmp;                              /* tmp to realloc with */
        size_t len;                             /* line length */
        buf[(len = strcspn (buf, "\n"))] = 0;   /* trim '\n', get length */

        /* always realloc to a temporary pointer, validate before assigning */
        if (!(tmp = realloc (pa[ndx % *n], len + 1))) {
            int rm = ndx > *n ? *n : ndx;       /* detrmine no. of lines to free */
            perror ("realloc-pa[ndx % *n]");

            while (rm--)                        /* loop freeing each allocated line */
                free (pa[rm]);
            free (pa);                          /* free pointers */

            return NULL;
        }
        pa[ndx % *n] = tmp;                     /* assign new block to pa[ndx%n] */
        memcpy (pa[ndx % *n], buf, len + 1);    /* copy line to block of memory */

        ndx++;      /* increment line count */
    }

(注意: 如果任何已分配行的分配失败,所有已分配的行将与指针一起被释放,并且 NULL 被 returned 避免任何内存泄漏。您继续用每个新分配的块的新地址覆盖每个指针,并且不断泄漏无法再释放的内存——当您覆盖指针时,您丢失了原始块的起始地址)

在 return 分配的对象之前,您要做的最后一件事是检查您的索引是否小于 *n' 的值,如果是,则更新该地址的值,以便实际存储的行数在调用者中可用,例如

    if (ndx < *n)   /* if less than *n lines read */
        *n = ndx;   /* update number at that address with ndx */

    return pa;      /* return allocated object */
}

你的功能基本上就这些了。将其与简单地从 main() 写入的输出放在一起,您将拥有:

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

#define NLINES   10     /* default number of lines */
#define MAXLEN 1000     /* max characters per-line */

/* create and store last *n lines from stdin in allocated object,
 * returning pointer to object on success, and updating value at n,
 * if less than NLINES lines read. Return NULL on failure. Caller
 * is responsible for freeing allocated memory.
 */
char **readlines (int *n)
{
    int ndx = 0;    /* line counter */
    char buf[MAXLEN], **pa = malloc (*n * sizeof *pa);  /* allocate pointers */

    if (!pa) {                      /* validate pointer allocation */
        perror ("malloc-pa");
        return pa;
    }
    for (int i = 0; i < *n; i++)    /* initialize all pointers NULL */
        pa[i] = NULL;

    while (fgets (buf, MAXLEN, stdin)) {        /* read each line of input */
        void *tmp;                              /* tmp to realloc with */
        size_t len;                             /* line length */
        buf[(len = strcspn (buf, "\n"))] = 0;   /* trim '\n', get length */

        /* always realloc to a temporary pointer, validate before assigning */
        if (!(tmp = realloc (pa[ndx % *n], len + 1))) {
            int rm = ndx > *n ? *n : ndx;       /* detrmine no. of lines to free */
            perror ("realloc-pa[ndx % *n]");

            while (rm--)                        /* loop freeing each allocated line */
                free (pa[rm]);
            free (pa);                          /* free pointers */

            return NULL;
        }
        pa[ndx % *n] = tmp;                     /* assign new block to pa[ndx%n] */
        memcpy (pa[ndx % *n], buf, len + 1);    /* copy line to block of memory */

        ndx++;      /* increment line count */
    }

    if (ndx < *n)   /* if less than *n lines read */
        *n = ndx;   /* update number at that address with ndx */

    return pa;      /* return allocated object */
}

int main (int argc, char **argv) {

    char *p = NULL, **lines = NULL;         /* pointers for strtol, and lines */
    int n = argc > 1 ? (int)strtol (argv[1], &p, 0) : NLINES;

    if (n != NLINES && (errno || p == argv[1])) {   /* validate conversion */
        fprintf (stderr, "error: invalid no. of lines '%s'\n", argv[1]);
        return 1;
    }

    if (!(lines = readlines(&n))) {             /* read lines validate return */
        fputs ("error: readlines failed.\n", stderr);
        return 1;
    }

    for (int i = 0; i < n; i++) {               /* loop over each stored line */
        puts (lines[i]);                        /* output line */
        free (lines[i]);                        /* free storage for line */
    }
    free (lines);                               /* free pointers */
}

(您可以根据需要添加您喜欢的功能,用 fgets() 替换读取和 main() 中的输出循环)。

例子Use/Output

默认行为:

$ printf "%s\n" line{1..20} | ./bin/tail
line11
line12
line13
line14
line15
line16
line17
line18
line19
line20

仅输出 5 行而不是默认值:

$ printf "%s\n" line{1..20} | ./bin/tail 5
line16
line17
line18
line19
line20

处理少于文件中的默认行数:

$ printf "%s\n" line{1..5} | ./bin/tail
line1
line2
line3
line4
line5

内存Use/Error检查

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

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

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

$ printf "%s\n" line{1..20} | valgrind ./bin/tail 5
==25642== Memcheck, a memory error detector
==25642== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==25642== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==25642== Command: ./bin/tail 5
==25642==
line16
line17
line18
line19
line20
==25642==
==25642== HEAP SUMMARY:
==25642==     in use at exit: 0 bytes in 0 blocks
==25642==   total heap usage: 23 allocs, 23 frees, 5,291 bytes allocated
==25642==
==25642== All heap blocks were freed -- no leaks are possible
==25642==
==25642== For counts of detected and suppressed errors, rerun with: -v
==25642== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

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

检查一下,如果您还有其他问题,请告诉我。