Передача массива строк в функцию для fscanf

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

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

person Jared Stanbrook    schedule 22.05.2021    source источник
comment
Где хранилище для каждой строки? На что указывает каждый из ваших 14 указателей? (например, какой действительный адрес является значением каждого адреса)   -  person David C. Rankin    schedule 22.05.2021
comment
Выделена ли память под имя? Также поделитесь содержимым файла, который вы читаете. некоторые указатели есть в приведенной ниже ссылке: stackoverflow.com/questions/12864603 / feof-or-fscanf-error   -  person geek_bs    schedule 22.05.2021


Ответы (3)


Продолжая комментарий, немедленная ошибка связана с char *name[14] объявлением массива из 14 указателей на char, которые являются неинициализированными. Это означает, что каждый указатель никуда не указывает (например, адрес, который указатель удерживает, поскольку его значение указывает на некоторый неопределенный адрес памяти). Прежде чем вы сможете что-либо сохранить, вы должны убедиться, что у вас есть допустимая ячейка памяти для хранения того значения, с которым вы имеете дело. Для строки, такой как name[x], это означает, что вам нужно length + 1 символов (+1 для обеспечения памяти для завершающего нулем символа, '\0', эквивалентно старому 0)

Ваша ReadFile() функция ужасно неадекватна, а ваше чтение непрочно. Каждый раз, когда вы читаете строки данных, вы должны использовать строчно-ориентированную функцию ввода, такую ​​как fgets() (или POSIX getline()). Таким образом, вы будете читать (использовать) строку ввода каждый раз, и любое отклонение форматирования входного файла, которое вызывает сопоставление-сбой, повлияет только на эту строку и не повлияет на чтение файла с этой точки. вперед.

Никогда не жестко кодируйте имена файлов и не используйте MagicNumbers в своем коде. Вам не нужно перекомпилировать вашу программу просто для чтения из другого входного файла. Вы можете указать имя файла для использования по умолчанию, но в противном случае взять имя файла для чтения в качестве аргумента вашей программы (для этого используется int argc, char **argv) или запросить пользователя и принять имя файла в качестве входных данных. Чтобы избежать жестко запрограммированных имен файлов и магических чисел в вашем коде:

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

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

а также

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

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

Соединение всех частей вместе и использование буфера из MAXC символов для хранения каждой строки, прочитанной из вашего файла, перед преобразованием (и проверкой) преобразования. (хорошая работа по подтверждению возврата от fscanf()).

Чтобы решить вашу проблему, вы должны прочитать значения name во временный массив, а затем, чтобы выделить хранилище, просто возьмите strlen() строки во временном массиве и символы malloc (len + 1) для name[i] (проверка каждого распределения), а затем скопируйте из временного массива до name[i]. (вам нужно будет освободить память, когда вы закончите со значениями обратно в вызывающей стороне (main() здесь)

Вам также понадобятся отдельные счетчики для line и index. Поскольку вы сообщаете строку, в которой произошел какой-либо сбой (снова хорошая работа!), Вам нужно будет увеличивать счетчик line на каждой итерации, но вы хотите увеличивать индекс для name и cash только при успешном преобразовании, например

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

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

У вас есть другой вариант хранения для name. Вместо выделения памяти с помощью malloc() (или calloc(), или realloc()) вы можете объявить name как 2D-массив символов с объемом памяти для каждой строки, достаточным для хранения самых длинных символов имени (+1). Это, в зависимости от разницы в длине между именами, может потребовать немного больше памяти, чем точное выделение для хранения каждого name. Это немного упрощает работу, избавляя от необходимости выделять ресурсы. Вам решать.

Теперь вы можете присвоить результат вашей функции переменной в main(), чтобы вы знали, сколько пар name и cash было успешно прочитано, например

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

Пример входного файла

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

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

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

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

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

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

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

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

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

Просмотрите все и дайте мне знать, если у вас возникнут дополнительные вопросы.

person David C. Rankin    schedule 22.05.2021
comment
Дэвид, ты уже знаешь, что ты козел, большое тебе спасибо, это лучший ответ, который я когда-либо имел удовольствие читать, ты исправил мою проблему, и я знаю, как это работает, благодаря твоему удивительному объяснению. - person Jared Stanbrook; 22.05.2021
comment
Вы очень кстати. Удачи в кодировании! (ты даже справился с моими опечатками :) - person David C. Rankin; 22.05.2021

gdb отлично подходит для поиска подобных проблем.

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

Используя char *name[14] в main(), вы создаете массив указателей символов, но не устанавливаете указатели. Следовательно, они могут указывать куда угодно, скорее всего, на недопустимое местоположение (что может вызвать segfault при попытке чтения / записи этого местоположения).

В моей системе мы видим, что нам посчастливилось иметь наш неинициализированный name[0], указывающий на 0x0 (NULL). Это не гарантируется, и здесь могло быть гораздо больше интересных ошибок, чем segfault. Представьте, например, что если бы name[0] вместо этого указал на место в памяти, где хранится line. Вы бы перезаписали эту память и получили бы очень странное поведение из вашего цикла.

Вы должны инициализировать каждый указатель в массиве namemain()), чтобы все они указывали на допустимую область достаточного размера.

person Snild Dolkow    schedule 22.05.2021

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

person Terak Soft    schedule 22.05.2021