Search code examples
cpointersscanfmodular

Passing an array of strings into a function for fscanf


When I run this code I get a segmentation error I don't know why this happens, I'm trying to read a name and cash amount from each line of a text file, and put it into an array.

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

void ReadFile(int *cash, char **name)
{
    int r, line = 0;

    FILE *fp;
    fp = fopen("coins.txt", "r");

    if (fp == NULL)
    {
        printf ("Error opening the file\n\n'");
        exit(EXIT_FAILURE);
    } else {
        while (line <= 14)
        {
            
            r = fscanf(fp, "%s %d\n", name[line], &cash[line]);
            if (r != 2)
            {
                printf ("Error, line %d in wrong format!\n\n", line);
            }
            //printf("Name: %s, Cash: $%d\n",name, *cash);
            line++;
        }
    fclose(fp);
    }
    
}

int main()
{
    int cash[14];
    char *name[14] = { "" };
    ReadFile(cash,name);
    //printf("%s\n", name[0]);
}

Solution

  • Continuing from the comment, the immediate error is due to char *name[14] declaring an array of 14 pointers to char that are uninitialized. Meaning each pointer points nowhere (e.g. the address the pointer holds as its value points to some indeterminate memory address). Before you can store anything, you must ensure you have a valid memory location to hold whatever value you are dealing with. For a string, such as name[x], that means you need length + 1 characters available (the +1 to provide storage for the nul-terminating character, '\0', equivalent to plain-old 0)

    Your ReadFile() function is woefully inadequate and your read is fragile. Any time you are reading lines of data, you should use a line-oriented input function, such as fgets() (or POSIX getline()). That way you will read (consume) a line of input each time and any input file formatting variance that causes a matching-failure will only affect that one line and not corrupt the read of the file from that point forward.

    Never Hardcode Filenames or use MagicNumbers in your code. You shouldn't have to recompile your program simply to read from a different input file. You can provide a filename to use by default, but otherwise take the filename to read as an argument to your program (that's what int argc, char **argv are for), or prompt the user and take the filename as input. To avoid the hardcoded filenames and magic numbers in your code:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    #define COINS  14       /* if you need a constant, #define one (or more) */
    #define MAXC 1024
    

    and

    int main (int argc, char **argv)
    {
        ...
        FILE *fp = fopen (argc > 1 ? argv[1] : "coins.txt", "r");
        ...
    

    Additionally, you generally want to open and validate the file is open for reading in the calling function and pass the FILE* pointer to the open stream as an argument to the function. If the file open fails, there is no need to make the function call to begin with.

    Putting all the pieces together and using a buffer of MAXC characgters to hold each line read from your file before converting (and validating) the conversion. (good job on validating the return from fscanf()).

    To fix your problem, you should read the name values into a temporary array and then to allocate storage, simply take the strlen() of the string in the temporary array, and malloc (len + 1) characters for name[i] (validating each allocation) and then copy from the temporary array to name[i]. (you will need to free the memory when you are done with the values back in the caller (main() here)

    You will also want separate counters for line and index. Since you report the line on which any failure occurs (good job again!), you will need to increment the line counter on each iteration, but you only want to increment the index for name and cash on a successful conversion, e.g.

    size_t ReadFile (FILE *fp, int *cash, char **name)
    {
        char buf[MAXC];                     /* temporary array to hold line */
        size_t index = 0, line = 0;         /* separate counters for index and line */
        
        while (line < COINS && fgets (buf, MAXC, fp))   /* protect array, read line */
        {
            char coinname[MAXC];            /* temporary array for name */
            int r, value;                   /* declare variables in scope required */
            size_t len;                     /* length of name */
            
            r = sscanf (buf, "%1023s %d", coinname, &value);    /* convert values */
            if (r != 2) {   /* validate conversion */
                fprintf (stderr, "Error, line %zu in wrong format!\n\n", line);
                line++;     /* don't forget to increment line */
                continue;   /* before continuing to read next line */
            }
            
            len = strlen (coinname);                    /* get length of name */
            name[index] = malloc (len + 1);             /* allocate len + 1 chars */
            if (name[index] == NULL) {                  /* validate allocation */
                perror ("malloc-name[index]");
                return index;
            }
            memcpy (name[index], coinname, len + 1);    /* copy to name[index] */
            cash[index] = value;                        /* assign to cash[index] */
            
            index++;        /* increment index */
            line++;         /* increment line */
        }
        
        return index;       /* return index */
    }
    

    (note: the change of the return type from void to size_t to enable the return of the number of names and values read from the file. Always provide a meaningful return type for any function that can succeed or fail, e.g. basically anything except simple print or free functions)

    You have another option for storage for name. Instead of allocating storage with malloc() (or calloc() or realloc()), you can declare name as a 2D array of characters with storage for each row sufficient to hold the longest name (+1) characters. This, depending on the length variation between names, may require quite a bit more storage than exactly allocating to hold each name. It does simplify things a bit by eliminating the need to allocate. Up to you.

    Now you can assign the return from your function to a variable in main() so you know how many name and cash pairs were successfully read, e.g.

    int main (int argc, char **argv)
    {
        int cash[COINS];
        char *name[COINS] = {NULL};
        size_t ncoins = 0;
        /* use filename provided as 1st argument (coins.txt by default) */
        FILE *fp = fopen (argc > 1 ? argv[1] : "coins.txt", "r");
        
        if (!fp) {  /* validate file open for reading */
            perror ("file open failed");
            return 1;
        }
        
        ncoins = ReadFile (fp, cash, name);
        fclose (fp);
        
        for (size_t i = 0; i < ncoins; i++) {
            printf ("%-12s %3d\n", name[i], cash[i]);
            free (name[i]);                             /* free mem when done */
        }
    }
    

    Example Input File

    $ cat dat/name_coins.txt
    penny 20
    nickle 3
    dime 8
    quarter 15
    half-dollar 5
    dollar 4
    

    Example Use/Output

    $ ./bin/name_coins dat/name_coins.txt
    penny         20
    nickle         3
    dime           8
    quarter       15
    half-dollar    5
    dollar         4
    

    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/name_coins dat/name_coins.txt
    ==5870== Memcheck, a memory error detector
    ==5870== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
    ==5870== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
    ==5870== Command: ./bin/name_coins dat/name_coins.txt
    ==5870==
    penny         20
    nickle         3
    dime           8
    quarter       15
    half-dollar    5
    dollar         4
    ==5870==
    ==5870== HEAP SUMMARY:
    ==5870==     in use at exit: 0 bytes in 0 blocks
    ==5870==   total heap usage: 9 allocs, 9 frees, 5,717 bytes allocated
    ==5870==
    ==5870== All heap blocks were freed -- no leaks are possible
    ==5870==
    ==5870== For counts of detected and suppressed errors, rerun with: -v
    ==5870== 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.