Karyna
Karyna

Reputation: 205

Saving values to 2D array

according to my task I need to read a file passed as a command line argument using C and store its content (each character) to an 2D array to be able change array's values later and save the changed content to another file. NVM some custom functions.

Here is an example of a file I need to read:

#,#,#,#,#,#,.,#,.,.,.$
#,.,#,.,.,#,.,#,#,#,#$
#,.,#,.,.,.,.,.,.,#,#$
#,.,#,.,.,#,#,#,#,#,#$
#,.,.,#,.,.,.,.,.,.,#$
#,.,.,.,#,.,#,#,.,.,#$
#,.,.,.,.,#,.,.,.,.,#$
#,.,.,.,.,#,.,.,.,.,#$
#,.,.,.,.,.,.,.,.,.,#$
#,#,#,#,#,#,#,#,#,.,#$

Here is what I've tried:

    int main(int argc, char *argv[]) {
        int startX = 3;
        int startY = 3;
        int endX = 6;
        int endY = 6;
        int count = 0;
        int x = 0;
        int y = 0;
        int fd = open(argv[1], O_RDONLY);
        char ch;

        if (fd == -1) {
            mx_printerr("map does not exist\n");
            exit(-1);
        }

        int targetFile =
            open("path.txt", O_CREAT | O_EXCL | O_WRONLY, S_IWUSR | S_IRUSR);

        while (read(fd, &ch, 1)) {
            if (ch == '\n') {
                x++;
            }
            if (ch != ',') {
                count++;
            }
        }

        fd = open(argv[1], O_RDONLY);
        y = (count - x) / x;
        char **arr;
        arr = malloc(sizeof(char *) * x);
        for (int i = 0; i < x; i++) arr[i] = malloc(y);
        int tempX = 0, tempY = 0, tempCount = 0;
        char tempString[count - x];

    // the loop in question >>>>>


       for (int i = 0; i < 10; i++) {
         for (int j = 0; j < 11; j++) {
          while (read(fd, &ch, 1)) {
            if (ch != ',') {
                arr[i][j] = ch;
                // mx_printchar(arr[i][j]);
             }
           }
         }
       }
      for (int i = 0; i < 10; i++) {
         for (int j = 0; j < 11; j++) {
            mx_printchar(arr[i][j]);
       }
     }
            for (int i = 0; i < x; i++) free(arr[i]);
            free(arr);
            close(fd);
            close(targetFile);
            exit(0);
        }

The last while loop should be saving the file's content to an array. However, when I try to print the array's content to console, I get some garbage values: ���pp ����8��

Please help me understand what is wrong here or should I use another approach to save the data to the array.

Upvotes: 1

Views: 1128

Answers (1)

David C. Rankin
David C. Rankin

Reputation: 84561

You have started off well, but then strayed into an awkward way of handling your read and allocations. There are a number of ways you can approach a flexible read of any number of characters and any number of rows into a dynamically allocated pointer-to-pointer-to char object that you can index like a 2D array. (often incorrectly referred to an a "dynamic 2D array") There is no array involved at all, you have a single-pointer to more pointers and you allocate a block of storage for your pointers (rows) and then allocate separate blocks of memory to hold each row worth of data and assign the beginning address to each such block to one of the pointers in turn.

An easy way to eliminate having to pre-read each row of characters to determine the number is to simply buffer the characters for each row and then allocate storage for and copy that number of characters to their final location. This provides the advantage of not having to allocate/reallocate each row starting from some anticipated number of characters. (as there is no guarantee that all rows won't have a stray character somewhere)

The other approach, equally efficient, but requiring the pre-read of the first row is to read the first row to determine the number of characters, allocate that number of characters for each row and then enforce that number of characters on every subsequent row (handling the error if additional characters are found). There are other options if you want to treat each row as a line and then read and create an array of strings, but your requirements appear to simply be a grid of characters) You can store your lines as strings at this point simply by adding a nul-terminating character.

Below we will use a fixed buffer to hold the characters until a '\n' is found marking the end of the row (or you run out of fixed storage) and then dynamically allocate storage for each row and copy the characters from the fixed buffer to your row-storage. This is generally a workable solution as you will know some outer bound of the max number of characters than can occur per-line (don't skimp). A 2K buffer is cheap security even if you think you are reading a max of 100 chars per-line. (if you are on an embedded system with limited memory, then I would reduce the buffer to 2X the anticipated max number of chars) If you define a constant up top for the fixed buffer size -- if you find you need more, it's a simple change in one location at the top of your file.

How does it work?

Let's start with declaring the counter variables to track the number of pointers available (avail), a row counter (row) a column counter (col) and a fixed number of columns we can use to compare against the number of columns in all subsequent rows (cols). Declare your fixed buffer (buf) and your pointer-to-pointer to dynamically allocate, and a FILE* pointer to handle the file, e.g.

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

#define NCHARS 2048     /* if you need a constant, #define one (or more) */

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

    size_t  avail = 2,      /* initial no. of available pointers to allocate */
            row = 0,        /* row counter */
            col = 0,        /* column counter */
            cols = 0;       /* fixed no. of columns based on 1st row */
    char buf[NCHARS],       /* temporary buffer to hold characters */
        **arr = NULL;       /* pointer-to-pointer-to char to hold grid */

    /* use filename provided as 1st argument (stdin by default) */
    FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;

(note: if no argument is provided, the program will read from stdin by default)

Next we validate the file is open for reading and we allocate an initial avail number of pointers:

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

    /* allocate/validate initial avail no. of pointers */
    if (!(arr = malloc (avail * sizeof *arr))) {
        perror ("malloc-arr");
        return 1;
    }

Next rather than looping while ((c = fgetc(fp)) != EOF), just continually loop - that will allow you to treat a '\n' or EOF within the loop and not have to handle the storage of the last line separately after the loop exits. Begin by reading the next character from the file and checking if you have used all your available pointers (indicating you need to realloc() more before proceeded):

    while (1) { /* loop continually */
        int c = fgetc(fp);                      /* read each char in file */
        if (row == avail) {                     /* if all pointers used */
            /* realloc 2X no. of pointers using temporary pointer */
            void *tmp = realloc (arr, 2 * avail * sizeof *arr);
            if (!tmp) {                         /* validate reallocation */
                perror ("realloc-arr");
                return 1;                       /* return failure */
            }
            arr = tmp;                          /* assign new block to arr */
            avail *= 2;                         /* update available pointers */
        }

(note: always realloc() using a temporary pointer. When realloc() fails (not if it fails) it returns NULL and if you reallocate using arr = realloc (arr, ..) you have just overwritten your pointer to your current block of memory with NULL causing the loss of the pointer and inability to free() the prior allocated block resulting in a memory-leak)

Now check if you have reached the end of line, or EOF and in the case of EOF if your col count is zero, you know you reached EOF after a previous '\n' so you can simply break the loop at that point. Otherwise, if you reach EOF with a full column-count, you know your file lacks a POSIX end-of-file and you need to store the last line of character, e.g.

        if (c == '\n' || c == EOF) {            /* if end of line or EOF*/
            if (c == EOF && !col)               /* EOF after \n - break */
                break;
            if (!(arr[row] = malloc (col))) {   /* allocate/validate col chars */
                perror ("malloc-arr[row]");
                return 1;
            }
            memcpy (arr[row++], buf, col);      /* copy buf to arr[row], increment */
            if (!cols)                          /* if cols not set */
                cols = col;                     /* set cols to enforce cols per-row */
            if (col != cols) {                  /* validate cols per-row */
                fprintf (stderr, "error: invalid no. of cols - row %zu\n", row);
                return 1;
            }
            if (c == EOF)                       /* break after non-POSIX eof */
                break;
            col = 0;                            /* reset col counter zero */
        }

If your character isn't a '\n' or EOF it's just a normal character, so add it to your buffer, check your buffer has room for the next and keep going:

        else {  /* reading in line */
            buf[col++] = c;                     /* add char to buffer */
            if (col == NCHARS) {                /* if buffer full, handle error */
                fputs ("error: line exceeds maximum.\n", stderr);
                return 1;
            }
        }
    }

At this point you have all of your characters stored in a dynamically allocated object you can index as a 2D array. (you also know it is just storage of characters that are not nul-terminated so you cannot treat each line as a string). You are free to add a nul-terminating character if you like, but then you might as well just read each line into buf with fgets() and trim the trailing newline, if present. Depends on your requirements.

The example just closes the file (if not reading from stdin), outputs the stored characters and frees all allocated memory, e.g.

    if (fp != stdin)                            /* close file if not stdin */
        fclose (fp);

    for (size_t i = 0; i < row; i++) {          /* loop over rows */
        for (size_t j = 0; j < cols; j++)       /* loop over cols */
            putchar (arr[i][j]);                /* output char */
        putchar ('\n');                         /* tidy up with newline */
        free (arr[i]);                          /* free row */
    }
    free (arr);                                 /* free pointers */
}

(that's the whole program, you can just cut/paste the parts together)

Example Input File

$ cat dat/gridofchars.txt
#,#,#,#,#,#,.,#,.,.,.$
#,.,#,.,.,#,.,#,#,#,#$
#,.,#,.,.,.,.,.,.,#,#$
#,.,#,.,.,#,#,#,#,#,#$
#,.,.,#,.,.,.,.,.,.,#$
#,.,.,.,#,.,#,#,.,.,#$
#,.,.,.,.,#,.,.,.,.,#$
#,.,.,.,.,#,.,.,.,.,#$
#,.,.,.,.,.,.,.,.,.,#$
#,#,#,#,#,#,#,#,#,.,#$

Example Use/Output

$ ./bin/read_dyn_grid dat/gridofchars.txt
#,#,#,#,#,#,.,#,.,.,.$
#,.,#,.,.,#,.,#,#,#,#$
#,.,#,.,.,.,.,.,.,#,#$
#,.,#,.,.,#,#,#,#,#,#$
#,.,.,#,.,.,.,.,.,.,#$
#,.,.,.,#,.,#,#,.,.,#$
#,.,.,.,.,#,.,.,.,.,#$
#,.,.,.,.,#,.,.,.,.,#$
#,.,.,.,.,.,.,.,.,.,#$
#,#,#,#,#,#,#,#,#,.,#$

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/read_dyn_grid dat/gridofchars.txt
==29391== Memcheck, a memory error detector
==29391== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==29391== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==29391== Command: ./bin/read_dyn_grid dat/gridofchars.txt
==29391==
#,#,#,#,#,#,.,#,.,.,.$
#,.,#,.,.,#,.,#,#,#,#$
#,.,#,.,.,.,.,.,.,#,#$
#,.,#,.,.,#,#,#,#,#,#$
#,.,.,#,.,.,.,.,.,.,#$
#,.,.,.,#,.,#,#,.,.,#$
#,.,.,.,.,#,.,.,.,.,#$
#,.,.,.,.,#,.,.,.,.,#$
#,.,.,.,.,.,.,.,.,.,#$
#,#,#,#,#,#,#,#,#,.,#$
==29391==
==29391== HEAP SUMMARY:
==29391==     in use at exit: 0 bytes in 0 blocks
==29391==   total heap usage: 17 allocs, 17 frees, 6,132 bytes allocated
==29391==
==29391== All heap blocks were freed -- no leaks are possible
==29391==
==29391== For counts of detected and suppressed errors, rerun with: -v
==29391== 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.

Upvotes: 1

Related Questions