简体   繁体   中英

How to allocate memory of triple pointer in a C function

I am new to C and have a case where I want to read in a simple text file with one word per row and I want to save this into an array. However, I want to declare a double pointer in function main and then pass that by reference to another function to allocate the memory and populate the double pointer. I can't seem to get it to work without throwing segmentation faults. Any advice would be much appreciated.

Here is what I've tried:

#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;
}

To answer your question, it is possible to allocate a triple pointer by type ***ptr (as @David C. Rankin stated), but it is simply not practical. The function can simply allocate memory with malloc or calloc and store the result, at the end write the pointer to the result to the pointer given by the argument by dereferencing it, like *argument_ptr = result_pointer . I've rewritten the code to avoid segmentation faults. Here's the code:

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
}

I can't seem to get it to work without throwing segmentation faults.

The segmentation faults on line 35 and 53 are caused by wrong dereferencing. On line 35, you should simply use tptr[r] to access the char** pointers, and it is the same case on line 53.

While the example linked was quite a bit more involved than what you require here, the principles are the same. When you declare a function void and eliminate the ability to return a pointer to a newly allocated block of memory, you will need to pass the address of the pointer you need to allocate storage for.

Why the address of ?

When you initially allocate, or reallocate, the address for the block of memory will change. If you simply pass a pointer to your function to allocate, the function receives a copy of the original pointer with its own, and very different address. In C all parameters are passed by value and the function receives a copy. Any changes made to the copy in your function are lost on the function return.

However, if you pass the address of the variable (one additional level of pointer indirection), the function receives a copy of the pointer to your variable, but that pointer holds the address of the original pointer. So now you can make changes and assign the new block of memory to the dereferenced pointer in your function, which assigns the new beginning address for the block of memory to the original pointer and the changes will be seen back in the calling function.

In your case, you want to read an unknown number of strings from a file and add them your allocated collection of strings within a void function. Were you to simply allocate/reallocate in main() , you could simply declare a char **ptrs; (a double-pointer), and then allocate/reallocate the number of pointers you have with each string read from your file, allocate storage for the string, and assign the address for the block to your newly allocated pointer, and then copy the string to your pointer -- and repeat until you run out of strings.

The only thing that changes when allocating and copying in a void function is you will pass the address of char **ptrs; to the function adding one-level of pointer indirection (so the function parameter type will be char *** , and then within the function you must remove that one additional level of indirection to operate on the pointer. You will also need to pass the address of a variable holding the current number of pointers allocated -- so you know how many you need to realloc() within the function.

Putting it altogether, your function can be written as:

/* 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 */
}

( note: on allocation failure, the program simply exits -- but for actual code, you would need some way to know whether both allocations occurred and the string was copied. Saving a before/after count of nptrs can give part of the picture, but can't tell you which allocation failed -- if the realloc() failed, then since you used a tmp pointer to realloc() , your pointers and strings as they existed before the function call are preserved and are still reachable though the original pointer)

( note2: the parenthesis around *strings in (*strings)[*nptrs] are required because in C Operator Precedence , [..] has higher precedence than the dereference operator '*' )

Putting an example together to read your file and store the string is your allocated collection of pointers and strings, you could do:

#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 */
}

Example Use/Output

$ ./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

Memory Use/Error Check

In any code you write that dynamically allocates memory, you have 2 responsibilities regarding any block of memory allocated: (1) always preserve a pointer to the starting address for the block of memory so, (2) it can be freed when it is no longer needed.

It is imperative that you use a memory error checking program to ensure you do not attempt to access memory or write beyond/outside the bounds of your allocated block, attempt to read or base a conditional jump on an uninitialized value, and finally, to confirm that you free all the memory you have allocated.

For Linux valgrind is the normal choice. There are similar memory checkers for every platform. They are all simple to use, just run your program through it.

$ 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)

Always confirm that you have freed all memory you have allocated and that there are no memory errors.

Look things over and let me know if you have further questions (and remember, being called a 3-Star Programmer isn't a compliment -- return a pointer from your function instead :)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM