在 C 中读取 csv 和 return 二维数组的函数

Function to read csv and return a 2d array in C

我刚开始接触 C,一整天都在想办法解决这个问题,这让我发疯了。我正在尝试创建一个函数来读取像这样的 CSV 文件:

10190935A;Sonia;Arroyo;Quintana;M;70
99830067Q;Josefa;Cuenca;Orta;M;42
28122337F;Nuria;Garriga;Dura;M;43
03265079E;Manuel;Orts;Robles;H;45

并创建一个二维数组并 return 它稍后在其他函数中使用。这是函数:

void cargarPacientes ()
{

    FILE *file = fopen (".\pacientes.csv", "r");
    char buffer[256 * 500];
    char *arrayOfLines[500];
    char *line = buffer;
    size_t buf_siz = sizeof (buffer);
    int i = 0, n;

    while (fgets (line, buf_siz, file)) {
        char *p = strchr (line, '\n');
        if (p) {
            *p = '[=12=]';
        } else {
            p = strchr (line, '[=12=]');
        }
        arrayOfLines[i++] = line;
        buf_siz -= p - line + 1;
        if (p + 1 == buffer + sizeof (buffer)) {
            break;
        }
        line = p + 1;
    }
    fclose (file);
    n = i;
    int y = 0;
    char *pacientes[20][6];
    for (i = 0; i < n; ++i) {
        char *token;
        char *paciente[6];
        int x = 0;
        token = strtok (arrayOfLines[i], ";");
        while (token != NULL) {
            paciente[x] = token;
            pacientes[y][x] = token;
            token = strtok (NULL, ";");
            x++;
        }
        y++;
    }
    // return pacientes;
}

我也尝试过使用结构,但我真的不知道它们是如何工作的。这是结构:

struct Paciente {
    char dni[9];
    char nombre[20];
    char primerApellido[20];
    char segundoApellido[20];
    char sexo[1];
    int edad;
};

无论如何,return 该函数的数组还是有任何其他更简单的方法来做同样的事情?我也试过 this,但我遇到了问题,甚至无法编译。

    void cargarPacientes(size_t N, size_t M, char *pacientes[N][M]
    void main(){
        char *pacientes[20][6];
        cargarPacientes(20, 6, pacientes);
    }

这些是编译器错误(抱歉,它们是西班牙语):

C:\Users\Nozomu\CLionProjects\mayo\main.c(26): error C2466: no se puede asignar una matriz de tama¤o constante 0
C:\Users\Nozomu\CLionProjects\mayo\main.c(26): error C2087: 'pacientes': falta el sub¡ndice
C:\Users\Nozomu\CLionProjects\mayo\main.c(88): warning C4048: sub¡ndices de matriz distintos: 'char *(*)[1]' y 'char *[20][6]'

如果我理解你想要读取你的文件并将每一行分隔成一个 struct Paciente,那么最简单的方法就是简单地分配一个包含一些预期数量的 [=25] 的内存块=],用从文件中读取的数据填充每个,跟踪填充的结构数量。当填充的结构数量等于您分配的数量时,您只需 realloc 增加可用结构的数量并继续...

由于您的 struct Paciente 包含完全定义的成员并且不需要任何进一步的单独分配,因此这变得更容易。

基本方法很简单。您将在 cargarPaciente() 中分配一块内存来保存从文件中读取的每个结构。您将采用一个指针作为参数,并使用您填充的结构数更新该内存位置的值。你 return 一个指向你分配的内存块的指针,其中包含你的结构元素,使它们在调用者中可用,并且你可以通过作为参数传递的指针填充可用的结构数量。

您通常还希望将一个开放的 FILE* 指针作为参数传递给您的函数以从中读取数据。 (如果您无法在调用者中成功打开文件,那么就没有理由首先调用函数来填充您的结构)。稍微改变你的函数调用以适应开放的 FILE* 指针和指向填充的结构数的指针,你可以这样做:

struct Paciente *cargarPacientes (FILE *fp, size_t *n)

(或者为方便起见在你的结构中创建一个 typedef 之后 [见下文],你可以这样做)

Paciente *cargarPacientes (FILE *fp, size_t *n)

查看读取文件的设置,在 main() 中,您需要声明一个指向结构的指针、一个保存读取的结构数量计数的变量,以及一个 FILE*指向您的文件流的指针,例如

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

    Paciente *pt = NULL;
    size_t n = 0;
    /* use filename provided as 1st argument (stdin by default) */
    FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;
    ...
    pt = cargarPacientes (fp, &n);  /* call function assign allocated return */

除了 fopencargarPacientes() 的 return 上的验证之外,这就是您在 main().

中所需要的全部内容

这项工作将在您的 cargarPacientes() 函数中完成。首先,简单地声明一个足够大的缓冲区来容纳每一行,你的变量来跟踪结构 分配的数量 ,然后是指向保存你的 [=25] 集合的内存块的指针=]. (MAXC 定义为 1024 的常量,NP 定义为 2 最初为 2 struct Paciente 分配存储空间)

Paciente *cargarPacientes (FILE *fp, size_t *n)
{
    char buf[MAXC];     /* buffer to read each line */
    size_t npt = NP;    /* no. Paciente struct allocated */
    Paciente *pt = malloc (npt * sizeof *pt);   /* allocate initial NP struct */

与任何分配一样,在使用分配的块之前,始终验证分配是否成功,例如

    if (!pt) {          /* validate every allocation */
        perror ("malloc-pt");
        return NULL;
    }

注意: 出错时,您的函数 returns NULL 而不是分配的内存块的地址以指示失败)

现在只需读取每一行并将 分号 分隔的值解析为临时结构。这允许您在将结构分配给您分配的内存块中的已分配结构之一之前验证您是否能够将值解析为结构的各个成员,例如

    while (fgets (buf, MAXC, fp)) {     /* read each line into buf */
        Paciente tmp = { .dni = "" };   /* temp struct to hold values */
        /* parse line into separate member values using sscanf */
        if (sscanf (buf, FMT, tmp.dni, tmp.nombre, tmp.primerApellido,
                    tmp.segundoApellido, &tmp.sexo, &tmp.edad) == 6) {

注意: FMT 在上面定义为字符串文字,还要注意 ndi 的大小已从 9 个字符增加到 10 个字符因此它可以被视为一个字符串值,并且 sexo 已声明为单个 char 而不是 char [1] 数组,例如

#define FMT "%9[^;];%19[^;];%19[^;];%19[^;];%c;%d"

如果您成功将数据行解析到临时结构中,接下来检查您填充的结构数量是否等于分配的数量,如果是,realloc 可用内存量。 (您可以添加少至 1 个额外的结构 [低效],或者您可以通过某些因素缩放分配的内存量 - 这里我们只是将分配的结构数量从 2 开始加倍)

            /* check if used == allocated to check if realloc needed */
            if (*n == npt) {
                /* always realloc using temporary pointer */
                void *ptmp = realloc (pt, 2 * npt * sizeof *pt);
                if (!ptmp) {    /* validate every realloc */
                    perror ("realloc-pt");
                    break;
                }
                pt = ptmp;      /* assign newly sized block to pt */
                npt *= 2;       /* update no. of struct allocated */
            }

(注意: 你必须 realloc 使用临时指针,因为如果 realloc 失败 returns NULL如果您分配给原始指针会造成 内存泄漏 由于丢失了现在无法再释放的原始内存块的地址)

剩下的就是将临时结构分配给分配的内存块并更新填充的数字,例如

            pt[(*n)++] = tmp;   /* assign struct to next struct */
        }
    }

就是这样,return 指向您分配的块的指针,您就完成了:

    return pt;  /* return pointer to allocated block of mem containing pt */
}

为了避免在代码中散布 幻数 并避免 硬编码文件名,为 [=55] 定义了一组常量=] 使用全局 enum。您可以为每个使用单独的 #define 语句来完成相同的事情,全局 enum 只是方便在一行中定义多个整数常量。

enum { NP = 2, DNI = 10, NAME = 20, MAXC = 1024 };

#define FMT "%9[^;];%19[^;];%19[^;];%19[^;];%c;%d"

现在您的结构定义中不再有单独的数字,如果您需要更改结构中任何成员的大小,只需更改常量和 FMT 字符串(您不能在 sscanf 格式字符串中使用常量或变量,因此始终需要单独的数字。

typedef struct Paciente {
    char dni[DNI];
    char nombre[NAME];
    char primerApellido[NAME];
    char segundoApellido[NAME];
    char sexo;
    int edad;
} Paciente;

为避免对文件名进行硬编码,我们将要读取的文件名作为程序的第一个参数(如果未提供参数,则从 stdin 读取)。这避免了每次输入文件的名称更改时都必须重新编译程序。

总而言之,你可以做到:

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

enum { NP = 2, DNI = 10, NAME = 20, MAXC = 1024 };

#define FMT "%9[^;];%19[^;];%19[^;];%19[^;];%c;%d"

typedef struct Paciente {
    char dni[DNI];
    char nombre[NAME];
    char primerApellido[NAME];
    char segundoApellido[NAME];
    char sexo;
    int edad;
} Paciente;

Paciente *cargarPacientes (FILE *fp, size_t *n)
{
    char buf[MAXC];     /* buffer to read each line */
    size_t npt = NP;    /* no. Paciente struct allocated */
    Paciente *pt = malloc (npt * sizeof *pt);   /* allocate initial NP struct */

    if (!pt) {          /* validate every allocation */
        perror ("malloc-pt");
        return NULL;
    }

    while (fgets (buf, MAXC, fp)) {     /* read each line into buf */
        Paciente tmp = { .dni = "" };   /* temp struct to hold values */
        /* parse line into separate member values using sscanf */
        if (sscanf (buf, FMT, tmp.dni, tmp.nombre, tmp.primerApellido,
                    tmp.segundoApellido, &tmp.sexo, &tmp.edad) == 6) {
            /* check if used == allocated to check if realloc needed */
            if (*n == npt) {
                /* always realloc using temporary pointer */
                void *ptmp = realloc (pt, 2 * npt * sizeof *pt);
                if (!ptmp) {    /* validate every realloc */
                    perror ("realloc-pt");
                    break;
                }
                pt = ptmp;      /* assign newly sized block to pt */
                npt *= 2;       /* update no. of struct allocated */
            }
            pt[(*n)++] = tmp;   /* assign struct to next struct */
        }
    }

    return pt;  /* return pointer to allocated block of mem containing pt */
}

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

    Paciente *pt = NULL;
    size_t n = 0;
    /* 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;
    }

    pt = cargarPacientes (fp, &n);  /* call function assign allocated return */
    if (!pt) {  /* validate the return was no NULL */
        fputs ("cargarPacientes-empty\n", stderr);
        return 1;
    }

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

    for (size_t i = 0; i < n; i++) {    /* output all struct saved in pt */
        printf ("%-9s %-10s %-10s %-10s  %c  %d\n", pt[i].dni, pt[i].nombre,
                pt[i].primerApellido, pt[i].segundoApellido, pt[i].sexo,
                pt[i].edad);
    }

    free (pt);    /* don't forget to free the memory you have allocated */
}

示例Use/Output

使用文件 dat/patiente.csv 中的样本数据,程序产生以下输出:

$ ./bin/readpatiente dat/patiente.csv
10190935A Sonia      Arroyo     Quintana    M  70
99830067Q Josefa     Cuenca     Orta        M  42
28122337F Nuria      Garriga    Dura        M  43
03265079E Manuel     Orts       Robles      H  45

内存Use/Error检查

在您编写的任何动态分配内存的代码中,您对分配的任何内存块负有 2 责任:(1) 始终保留指向内存块的起始地址 因此,(2) 当不再需要它时可以释放

您必须使用内存错误检查程序来确保您不会尝试访问内存或写入 beyond/outside 您分配的块的边界,尝试读取或基于未初始化的条件跳转值,最后,确认您释放了所有已分配的内存。

对于Linux valgrind是正常的选择。每个平台都有类似的内存检查器。它们都很简单易用,只需运行你的程序就可以了。

$ valgrind ./bin/readpatiente dat/patiente.csv
==1099== Memcheck, a memory error detector
==1099== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==1099== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==1099== Command: ./bin/readpatiente dat/patiente.csv
==1099==
10190935A Sonia      Arroyo     Quintana    M  70
99830067Q Josefa     Cuenca     Orta        M  42
28122337F Nuria      Garriga    Dura        M  43
03265079E Manuel     Orts       Robles      H  45
==1099==
==1099== HEAP SUMMARY:
==1099==     in use at exit: 0 bytes in 0 blocks
==1099==   total heap usage: 5 allocs, 5 frees, 6,128 bytes allocated
==1099==
==1099== All heap blocks were freed -- no leaks are possible
==1099==
==1099== For counts of detected and suppressed errors, rerun with: -v
==1099== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

始终确认您已释放所有分配的内存并且没有内存错误。

这比尝试对固定二维数组进行硬编码以尝试处理从文件中解析值要简单得多。查看所有内容,如果您还有其他问题,请告诉我。