Динамически распределять массив указателей (упражнение 5-13 K & R) - PullRequest
1 голос
/ 24 апреля 2020

Я работал через K & R и пытаюсь выполнить упражнение 5-13, в котором говорится:

Напишите хвост программы, который печатает последние n строк ее ввода. По умолчанию, например, n равно 10, но его можно изменить с помощью необязательного аргумента, так что

tail -n

выводит последние n строк. Программа должна вести себя рационально независимо от того, насколько необоснованным является ввод или значение n. Напишите программу, чтобы она максимально эффективно использовала доступное хранилище; строки должны храниться как в программе сортировки из Раздела 5.6, а не в двумерном массиве фиксированного размера.

Вот мой алгоритм

  • Если аргумент c == 1, затем установите n = 10, иначе n - второй аргумент
  • Динамически создайте массив символьных указателей размера n. Это будет содержать указатели на строки, которые должны быть напечатаны.
  • Вызовите readlines. Readlines начинает принимать данные от пользователя, пока не встретится EOF. Каждый раз, когда встречается новая строка, это строка, и mallo c вызывается с длиной этой строки в качестве аргумента. Это возвращает указатель, который приведен как символьный указатель, и динамически созданный массив указателей содержит этот указатель в соответствии с этим - array_of_pointers [nlines% n] (где nlines - номер текущей строки).
  • После все строки были прочитаны, readlines возвращает nlines.
  • writelines вызывается с n (аргумент командной строки), nlines и массив указателей в качестве аргументов, и он печатает строки соответственно - j = nlines - n ; j

Вот код, который я написал для этого

#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] = '\0';

  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] = '\0';
      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. Я знаю, что после того, как вы закончите, вы должны освободить свою память. Я могу освободить память массива указателей (pa), используя free(pa), но как бы go об освобождении памяти p в readlines. Вопрос гласит, что я должен «наилучшим образом использовать доступное хранилище», что означает, что после прочтения n строк я в идеале должен освободить 1-ю строку, когда 11-я строка, 2-я строка, когда 12-я строка прочитана, и так далее. Но я не знаю, как это сделать.

Извините заранее. Я новичок ie с C, и это указатели на дело указателей наряду с динамическим c распределением памяти действительно запутывают мой мозг.

Ответы [ 2 ]

2 голосов
/ 24 апреля 2020

Ну, очевидно, что вы думаете по правильным линиям логики c, чтобы добраться до симулированных tail строк, но вы, похоже, спотыкаетесь о том, как подходить к обработке распределения, перераспределения и освобождения памяти. (что, вероятно, указывает на упражнение).

Хотя ничто не мешает вам выделить ваши указатели для pa в main() и передать этот параметр в readlines(), это несколько неловко способ сделать это. Когда вы думаете о создании функции, которая будет выделять хранилище для объекта, пусть функция выделяет для всего объекта и возвращает указатель на объект в случае успеха, или возвращает NULL в случае ошибки. Таким образом, вызывающая функция знает, что если функция возвращает действительный указатель, она отвечает за освобождение памяти, связанной с объектом (вместо того, чтобы часть памяти была выделена в разных местах). Если функция возвращает NULL - вызывающая сторона знает, что функция не выполнена, и ей не нужно беспокоиться о какой-либо памяти для объекта.

Это также освобождает вас от необходимости передавать параметр для объекта. Так как вы выделите полный объект в функции, просто измените тип возвращаемого значения на тип вашего объекта ((char** здесь)) и передайте pointer-to память, содержащую количество строк в вывод. Почему указатель? Если сохранено меньше этого количества строк (либо потому, что в читаемом файле меньше строк, либо вам не хватило памяти перед сохранением всех строк), вы можете обновить значение по этому адресу фактическим количеством сохраненных строк и сделать так, чтобы номер, доступный обратно в вызывающей стороне (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() обрабатывать как начальное распределение, так и любые необходимые перераспределения. Также обратите внимание, что вместо использования malloc для указателей, вы можете использовать calloc, который установит все байты равными нулю (и для всех известных мне компиляторов, чтобы указатели оценивались как NULL без явного указания l oop установка их). Однако это не гарантируется стандартом - так что все oop является правильным.

Здесь fgets() используется для чтения каждой строки, а strcspn() используется для обрезки '\n' и получения длина каждой строки - вы можете использовать любую функцию, которая вам нравится. После того как строка прочитана, обрезана и получена длина, память выделяется (или перераспределяется) для хранения строки, и строка копируется в новый блок памяти. Ваш индекс nlines % n работает правильно, но вы не увеличиваете nlines до тех пор, пока после выделения и назначения не будет, например,

( note: Отредактировано ниже, чтобы рассматривать любой сбой перераспределения строк как терминал и Free All Memory возвращают 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, исключая любую утечку памяти. Продолжается перезапись каждого из них. указатель с новым адресом для каждого вновь выделенного блока с непрерывной утечкой памяти, которая больше не может быть освобождена - вы потеряли начальный адрес для исходного блока при перезаписи указателя)

Последнее, что вы делаете раньше возвращать выделенный объект, чтобы проверить, меньше ли ваш индекс, чем значение для *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() и вывод l oop в main(), по желанию).

Пример использования / Вывод

Поведение по умолчанию:

$ 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

Проверка использования памяти / ошибок

В любом коде Вы пишете, что динамически распределяет память, у вас есть 2 обязанностей относительно любого выделенного блока памяти: (1) всегда сохраняйте указатель на начальный адрес для блока памяти, поэтому, (2 ) он может быть освобожден , когда он больше не нужен.

Крайне важно, чтобы вы использовали программу проверки ошибок памяти, чтобы убедиться, что вы не пытаетесь получить доступ к памяти или писать за пределами / за ее пределами. границы вашего выделенного блока, попытка прочитать или основать условный j Возьмите неинициализированное значение и, наконец, подтвердите, что вы освобождаете всю выделенную память.

Для 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)

Всегда подтверждайте, что вы освободили всю выделенную память и что ошибок памяти нет.

Посмотрите вещи и дайте мне знать, если у вас есть дополнительные вопросы.

1 голос
/ 24 апреля 2020

Этот ответ сосредоточен только на этой части:

Как бы go об освобождении памяти p в readlines.

В принципе все, что вам нужно, это итерировать указатели в памяти, на которые указывает pa, и освобождать их один за другим. Как и

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

Однако есть одна небольшая проблема: вы не можете знать, сколько из тех указателей, которым было присвоено malloc ed значение в readlines.

Чтобы обойти эту проблему, вы можете инициализировать все указатели в NULL. Тогда безопасно вызывать free для всех указателей, потому что всегда допустимо вызывать free с указателем NULL.

Как:

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 и избежать инициализации l oop. Однако для простоты я продолжил с malloc

Тем не менее, здесь есть еще одна проблема:

pa[++nlines % n] = p;

Здесь вы перезаписываете указатели, которые pa указывают к. Таким образом, вы можете перезаписать указатель на некоторую память malloc - это плохо. Обязательно сначала наберите free:

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

Это решение требует инициализации NULL указателей, на которые указывает pa.

Кстати: эта строка кода будет работать

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

но в моем мнении он имеет "запах".

Я бы сказал проще:

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

Далее, atoi не самый лучший решение - см. Почему я не должен использовать atoi ()?

...