Как выделить память тройного указателя в функции C - PullRequest
0 голосов
/ 19 февраля 2020

Я новичок в C и у меня есть случай, когда я хочу прочитать в простом текстовом файле по одному слову в строке, и я хочу сохранить это в массив. Однако я хочу объявить двойной указатель в функции main, а затем передать его по ссылке на другую функцию, чтобы выделить память и заполнить двойной указатель. Кажется, я не могу заставить его работать без ошибок сегментации. Любой совет будет высоко ценится.

Вот что я попробовал:

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

/*
test.txt = content between the "---"
---
first
second
third
fourth
fifth
sixth
seventh
---
*/

void allocmem (char ***tptr) {

    FILE *infile = fopen("test.txt", "r");
    int bufferLength = 5;
    char *buffer = malloc(sizeof(char) * bufferLength);

    int c, i, row, rows;
    c = i = row = 0;
    rows = 7;

/*
    I want to allocate an array of char strings to store the list from test.txt in
    I have tried different variants here
*/

    tptr = malloc(sizeof(char**) * rows);
    for (int r = 0; r < rows; r++) {
        *tptr[r] = malloc(sizeof(char*) * bufferLength);
    }

/*
    // another attempt
    *tptr = malloc(sizeof(char*) * rows);
    for (int r = 0; r < rows; r++) {
        *tptr[r] = malloc(sizeof(char) * bufferLength);
    }
*/

    while ((c = getc(infile)) != EOF) {

        if (c == '\n') {
            buffer[++i] = '\0';
            printf("buffer: %s\n", buffer);

            // I am also a little unsure how to append my buffer to the pointer array here too
            strcpy(*tptr[row++], buffer);
            memset(buffer, '\0', bufferLength);
            i = 0;
        } else {
            buffer[i++] = c;
        }

    }

    fclose(infile);

}

int main () {

    // double pointer to hold array value from test.txt in
    char **dptr;

    // allocate memory and read test.txt and store each row into the array
    allocmem(&dptr);

/*  
    print each element of dptr here yielding the expected results of:

    first
    second
    third
    fourth
    fifth
    sixth
    seventh
*/

    return 0;
}

Ответы [ 2 ]

2 голосов
/ 19 февраля 2020

Чтобы ответить на ваш вопрос, можно выделить тройной указатель на type ***ptr (как сказал @David C. Rankin), но это просто не практично. Функция может просто выделить память с помощью malloc или calloc и сохранить результат, в конце записать указатель на результат в указатель, заданный аргументом , путем разыменования его как *argument_ptr = result_pointer. Я переписал код, чтобы избежать ошибок сегментации. Вот код:

void allocmem (char ***tptr) {
    // The aim of this function is to return the array of strings read from the file
    FILE *infile = fopen("test.txt", "r"); // Read text file
    const int bufferLength = 10, number_of_strings = 7; // Read string 7 times, for a maximum length of 10
    char **array = (char**)calloc(sizeof(char*), number_of_strings); // Allocate memory for a bunch of strings (char*), but the memory for the characters are not initialized
    for (int i = 0; i < number_of_strings; i++){
        char *string = (char*)calloc(sizeof(char), bufferLength); // Memory for the string, a.k.a. character array
        fgets(string, bufferLength, infile); // Read the string (word) from file
        char *c;
        if (c = strchr(string, '\n')) *c = '\0'; // Remove the newline character if exists
        array[i] = string; // Assign the string to the array
    }
    fclose(infile); // Close the file
    *tptr = array; // Return the array created by modifying the given pointer
}

Кажется, я не могу заставить его работать без выброса ошибок сегментации.

Вызываются ошибки сегментации в строках 35 и 53 неправильной разыменовкой. В строке 35 вы должны просто использовать tptr[r] для доступа к указателям char**, и это аналогично случаю в строке 53.

1 голос
/ 19 февраля 2020

В то время как приведенный пример был немного более сложным, чем то, что вам требуется, принципы все те же. Когда вы объявляете функцию void и исключаете возможность возврата указателя на вновь выделенный блок памяти, вам нужно будет передать адрес указателя, для которого нужно выделить хранилище.

Почему адрес ?

При первоначальном выделении или перераспределении адрес для блока памяти изменится. Если вы просто передаете указатель на свою функцию для выделения, функция получает копию исходного указателя со своим собственным и совершенно другим адресом. В C все параметры передаются значением , и функция получает копию. Любые изменения, внесенные в копию в вашей функции, теряются при возврате функции.

Однако, если вы передадите адрес переменной (один дополнительный уровень косвенности указателя), функция получит копия указателя на вашу переменную, но этот указатель содержит адрес исходного указателя. Так что теперь вы можете вносить изменения и назначать новый блок памяти для разыменованного указателя в вашей функции, которая присваивает новый начальный адрес для блока памяти исходному указателю, и изменения будут видны обратно в вызывающей функции.

В вашем случае вы хотите прочитать неизвестное количество строк из файла и добавить их в свою выделенную коллекцию строк в функции void. Если бы вы просто выделяли / перераспределяли в main(), вы могли бы просто объявить char **ptrs; (двойной указатель), а затем распределить / перераспределить количество указателей, которые у вас есть с каждой строкой, прочитанной из вашего файла, выделить хранилище для строка и назначьте адрес для блока только что выделенному указателю, а затем скопируйте строку в свой указатель - и повторяйте до тех пор, пока у вас не закончатся строки.

Единственное, что изменяется при выделении и копировании в void функция - вы передадите адрес char **ptrs; в функцию, добавив одноуровневую косвенность указателя (таким образом, тип параметра функции будет char ***, а затем внутри функции вы должны удалить этот дополнительный уровень косвенное обращение для работы с указателем. Вам также нужно будет передать адрес переменной, содержащей текущее количество выделенных указателей, чтобы вы знали, сколько вам нужно realloc() внутри функции.

Помещение в нее в целом, ваша функция может быть записана как:

/* allocate pointer for *strings, allocate/copy s to (*strings)[*nptr] */
void allocmem (char ***strings, size_t *nptrs, const char *s)
{
    size_t len = strlen (s);    /* determine length of s */
    /* reallocate *string to a temporary pointer, adding 1 pointer */
    void *tmp = realloc (*strings, (*nptrs + 1) * sizeof **strings);

    if (!tmp) { /* validate EVERY allocation/reallocation */
        perror ("realloc-allocptr-strings");
        exit (EXIT_FAILURE);
    }
    *strings = tmp;     /* assign newly allocated block of pointers to *strings */

    (*strings)[*nptrs] = malloc (len + 1);      /* allocate storage for s */
    if (!(*strings)[*nptrs]) {  /* ditto */
        perror ("malloc-(*strings)[*nptrs]");
        exit (EXIT_FAILURE);
    }
    memcpy ((*strings)[*nptrs], s, len + 1);    /* copy s to allocated storage */

    *nptrs += 1;    /* increment pointer count after successful allocation/copy */
}

( примечание : при неудачном размещении программа просто завершает работу - но для реального кода вам понадобится какой-то способ узнать, произошло ли оба выделения и была ли скопирована строка. Сохранение счетчика до / после nptrs может дать часть картины, но не может сказать вам, какое распределение не удалось - если произошел сбой realloc(), то, поскольку вы использовали указатель tmp на realloc(), ваш указатели и строки в том виде, в каком они существовали до вызова функции, сохраняются и по-прежнему доступны через оригинальный указатель)

( note2: скобки вокруг *strings в (*strings)[*nptrs] требуются, поскольку в C Приоритет оператора , [..] имеет более высокий приоритет, чем оператор разыменования '*')

Объединение примера для чтения вашего файла и сохранения строки - это ваша выделенная коллекция указателей и строки, которые вы могли бы сделать:

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

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

/* allocate pointer for *strings, allocate/copy s to (*strings)[*nptr] */
void allocmem (char ***strings, size_t *nptrs, const char *s)
{
    size_t len = strlen (s);    /* determine length of s */
    /* reallocate *string to a temporary pointer, adding 1 pointer */
    void *tmp = realloc (*strings, (*nptrs + 1) * sizeof **strings);

    if (!tmp) { /* validate EVERY allocation/reallocation */
        perror ("realloc-allocptr-strings");
        exit (EXIT_FAILURE);
    }
    *strings = tmp;     /* assign newly allocated block of pointers to *strings */

    (*strings)[*nptrs] = malloc (len + 1);      /* allocate storage for s */
    if (!(*strings)[*nptrs]) {  /* ditto */
        perror ("malloc-(*strings)[*nptrs]");
        exit (EXIT_FAILURE);
    }
    memcpy ((*strings)[*nptrs], s, len + 1);    /* copy s to allocated storage */

    *nptrs += 1;    /* increment pointer count after successful allocation/copy */
}

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

    char buf[MAXC],         /* temp storage for each line read from file */
         **strings = NULL;  /* must initialize pointer NULL */
    size_t nptrs = 0;       /* number of pointers allocated */
    /* use filename provided as 1st argument (stdin by default) */
    FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;

    if (!fp) {  /* validate file open for reading */
        perror ("file open failed");
        return 1;
    }

    while (fgets (buf, MAXC, fp)) {         /* read each line into buf */
        buf[strcspn (buf, "\n")] = 0;       /* trim \n from end of buf */
        allocmem (&strings, &nptrs, buf);   /* add string to strings */
    }
    if (fp != stdin)   /* close file if not stdin */
        fclose (fp);

    for (size_t i = 0; i < nptrs; i++) {    /* output all stored strings */
        printf ("strings[%zu] : %s\n", i, strings[i]);
        free (strings[i]);      /* free storage for string */
    }
    free (strings);             /* free pointers */
}

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

$ ./bin/3starstrings dat/test1-7.txt
strings[0] : first
strings[1] : second
strings[2] : third
strings[3] : fourth
strings[4] : fifth
strings[5] : sixth
strings[6] : seventh

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

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

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

Для Linux valgrind это нормальный выбор. Есть похожие проверки памяти для каждой платформы. Все они просты в использовании, просто запустите вашу программу через нее.

$ valgrind ./bin/3starstrings dat/test1-7.txt
==23799== Memcheck, a memory error detector
==23799== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==23799== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==23799== Command: ./bin/3starstrings dat/test1-7.txt
==23799==
strings[0] : first
strings[1] : second
strings[2] : third
strings[3] : fourth
strings[4] : fifth
strings[5] : sixth
strings[6] : seventh
==23799==
==23799== HEAP SUMMARY:
==23799==     in use at exit: 0 bytes in 0 blocks
==23799==   total heap usage: 17 allocs, 17 frees, 5,942 bytes allocated
==23799==
==23799== All heap blocks were freed -- no leaks are possible
==23799==
==23799== For counts of detected and suppressed errors, rerun with: -v
==23799== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

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

Посмотрите вещи и дайте мне знать, если у вас есть дополнительные вопросы (и помните, что называться 3-звездочным программистом не является комплиментом - вместо этого верните указатель из вашей функции :)

...