简体   繁体   中英

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]);
}

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 (eg 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, eg

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, eg 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, eg

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.

gdb is great for finding problems like this.

segfault$ gcc -ggdb segfault.c -o segfault
segfault$ gdb segfault 
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
[...]
Reading symbols from segfault...done.
(gdb) break segfault.c:20
Breakpoint 1 at 0x896: file segfault.c, line 20.
(gdb) run
Starting program: /tmp/segfault/segfault 

Breakpoint 1, ReadFile (cash=0x7fffffffdd80, name=0x7fffffffddc0) at segfault.c:20
20              r = fscanf(fp, "%s %d\n", name[line], &cash[line]);
(gdb) print line
$1 = 0
(gdb) print &cash[line]
$2 = (int *) 0x7fffffffdd80
(gdb) print name[line]
$3 = 0x0

With char *name[14] in main() , you're creating an array of char pointers, but you don't set the pointers. Therefore, they may be pointing anywhere, most likely to an invalid location (which would cause a segfault when you try to read/write that location).

On my system, we can see that we were lucky enough to have our uninitialized name[0] pointing to 0x0 ( NULL ). This is not guaranteed, and we could've had much more interesting errors than a segfault here. Imagine, for example, if name[0] had instead pointed to the location in memory where line is stored. You would've overwritten that memory, and gotten very strange behavior from your loop.

You must initialize each pointer in your name array (in main() ), so that they all point to a valid area of sufficient size.

Additionally, you generally need to open and validate the report is open for studying inside the calling function and bypass the FILE* pointer to the open circulation as a controversy to the function page . If the document open fails, there's no need to make the feature name first of all.

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