首页 > 解决方案 > C:动态分配表

问题描述

我正在开发一个从文件中读取数字并将它们存储在表中的小代码。该文件在开始时告诉用户表格的行数(列数是固定的:3)
例如文件:
3
1 2 20.5
1 4 9.22
2 6 11.7
1 3 12.1

正如标题所说,我需要根据文件中出现的行数来分配表。谁能帮我一把?
到目前为止,我这样做了:
分配

int** table = malloc(r * sizeof(int)); //r = num of rows
    for (i = 0; i < r; i++)
        table[i] = malloc(sizeof(int*) * 3); //3 is the number of columns 

填充

while (fscanf(fp, "%d %d %lf", &a, &b, &c) == 3) {
    for (i = 0; i < r; i++) {
    ->  table[i][0]=a;
    ->  table[i][1]=b;
    ->  table[i][2]=c;
    }
}

但是在我用“->”指出的行中,我收到一条错误消息:表达式必须具有指向对象的指针类型。

标签: arrayscmultidimensional-arraymalloc

解决方案


选择正确的类型

首先,如果您需要从文件中读取数字,例如:

1 2 20.5

然后读入一个int值不会捕获最后一个浮点列。您将需要在 twoint和 one之间创建一个结构float,或者简单地使用float(or double) 作为类型。

选择正确的聚合对象类型

虽然您可以使用指针到指针(例如float **)并分配一些指针,然后float为每行分配一块内存 3- 然后分配给每个分配的指针——如果列数固定为三,然后使用指向数组的指针允许单一分配/单一释放,这比分配一个指针一个块来float为每一行保存 3- 容易得多。

而不是使用float **table;你会使用float (*table)[3](a pointer-to-array of float[3])

使用指向数组的分配/重新分配

每当您需要读取和存储未知数量的任何对象类型时,您通常会分配一些数字开始,保持可用数字的计数器,然后使用数字,然后何时used == availablerealloc()为您的对象集合添加更多存储空间和继续。对于任何内存分配,您必须通过 *在代码中使用该内存块之前检查返回来验证分配是否成功。

选择每次添加内存的方式realloc()。您可以每次都为一些新的固定数量的对象添加存储(例如,每次都添加10 * sizeof object到您(对于一个小的未知数来说很好)),但是分配/重新分配是相对昂贵的操作。availablerealloc

为了平衡内存大小的合理增长与您必须重新分配的次数,一般方案是每次重新分配时将数量加倍available(例如,您可以最初分配2对象,然后每次重新分配都会导致4, 8, 16, 32, 64, 128, 256, 512, etc...重新20分配会导致存储超过2,000,000对象,而如果10-more每次添加,您将拥有200.

您如何从文件中读取数据

如果您使用格式化输入函数(例如fscanf()从文件中读取),那么这是一个脆弱的方案。文件中的一个无关字符可能会破坏您从该点开始的阅读。

更好的方法是使用面向行的输入函数,例如fgets()POSIX getline(),它将一次从文件中读取一行数据到一个足够大小的字符数组中,然后您可以从中解析所需的值。这样,您可以独立验证您对一行的读取,以及从该行中提取所需的值 - 并且 - 如果您在该行中有额外的字符,最糟糕的情况是您无法从该 1 中获取值-line,但从所有其他人的读取和提取不受影响。

sscanf()您有多种方法可以从读入字符数组、、、、或任何其他字符串函数的数据行中提取值strtol / strtof, etc..strtok / strsep或者只需使用一对指针将您需要的字符括起来。

stdin要使用作为程序的第一个参数给出的文件名打开文件(如果没有给出参数,则默认读取),您可以执行以下操作:

#define COLS   3       /* if you need a constant, #define one (or more) */
#define MAXC 256
...
    char line[MAXC];        /* buffer to hold each line read */
    ...
    while (fgets (line, MAXC, fp)) {        /* read / validate each line */
       ...
        /* parse / validate 3 floats from line into arr */
        if (sscanf (line, "%f %f %f", &arr[used][0], &arr[used][1], &arr[used][2]) != 3)
            continue;
        
        used++;                             /* increment no. of used rows */
    }

在上面,您会将每一行读入line,然后尝试使用 解析float该行中的 3- 值sscanf()。如果失败,您只需continue阅读下一行。如果成功,您已将值提取到数组(表)中,您只需增加行数used并阅读下一行。

当您分配初始数量的值时,开始为未知数量的数据行分配存储空间,例如

    float (*arr)[COLS];     /* pointer-to-array float[3] */
    size_t  used  = 0,      /* allocated rows used */
            avail = 2;      /* allocated rows available */
    ...
    /* allocate initial 'avail' rows / validate EVERY allocaiton */
    if ((arr = malloc (avail * sizeof *arr)) == NULL) {
        perror ("malloc-avail");
        return 1;
    }

然后在你的循环中,在你的数组中添加一行之前,你通过检查是否需要重新分配来确保有可用的存储空间if (used == avail)。如果需要重新分配,则重新分配给临时指针,因此当(不是如果)realloc()返回失败时NULL,您不会覆盖指向数据的指针而NULL丢失指向该内存块开头的指针——这意味着它可以不再被释放——造成内存泄漏

只有在您验证重新分配成功后,您才会将重新分配的内存块分配给您的指针,例如

    while (fgets (line, MAXC, fp)) {        /* read / validate each line */
        if (used == avail) {                /* is arr full, needs reallocation? */
            /* always realloc to a temporary pointer */
            void *tmp = realloc (arr, 2 * avail * sizeof *arr);
            if (!tmp) {                     /* validate EVERY reallocation */
                perror ("realloc-arr");
                break;                      /* break, don't exit, arr still good */
            }
            arr = tmp;                      /* assign reallocated block to arr */
            avail *= 2;                     /* update no. of available rows */
        }
        ...

上面只是使用了每次realloc()调用内存块大小加倍的重新分配方案。如果重新分配成功,则通过将其乘以 2 来更新可用数字。

如果realloc()失败,由于您在对原始指针的调用中使用了临时指针realloc(),因此仍然指向您在调用realloc(). 所以不需要退出程序,此时你收集到的数据还是不错的(你只是内存不足)。因此,只需break;您的读取循环并处理您拥有的数据。

总而言之

如果你把它放在一起,你会得到类似的东西:

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

#define COLS   3       /* if you need a constant, #define one (or more) */
#define MAXC 256

int main (int argc, char **argv) {
    
    char line[MAXC];        /* buffer to hold each line read */
    float (*arr)[COLS];     /* pointer-to-array float[3] */
    size_t  used  = 0,      /* allocated rows used */
            avail = 2;      /* allocated rows available */
    /* 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;
    }
    
    /* allocate initial 'avail' rows / validate EVERY allocaiton */
    if ((arr = malloc (avail * sizeof *arr)) == NULL) {
        perror ("malloc-avail");
        return 1;
    }
    
    if (!fgets (line, MAXC, fp))            /* read / validate / discard 1st line */
        return 1;
    
    while (fgets (line, MAXC, fp)) {        /* read / validate each line */
        if (used == avail) {                /* is arr full, needs reallocation? */
            /* always realloc to a temporary pointer */
            void *tmp = realloc (arr, 2 * avail * sizeof *arr);
            if (!tmp) {                     /* validate EVERY reallocation */
                perror ("realloc-arr");
                break;                      /* break, don't exit, arr still good */
            }
            arr = tmp;                      /* assign reallocated block to arr */
            avail *= 2;                     /* update no. of available rows */
        }
        /* parse / validate 3 floats from line into arr */
        if (sscanf (line, "%f %f %f", &arr[used][0], &arr[used][1], &arr[used][2]) != 3)
            continue;
        
        used++;                             /* increment no. of used rows */
    }
    if (fp != stdin)                        /* close file if not stdin */
        fclose (fp);

    for (size_t i = 0; i < used; i++)       /* output used rows */
        printf (" %4g  %4g  %6.2f\n", arr[i][0], arr[i][1], arr[i][2]);
    
    free (arr);                             /* don't forget to free what you allocate */
}

注意:上面的第一行只是读取并丢弃。由于您是动态分配内存,您可以简单地读取每个数据行并将其添加到您的,在需要时重新分配,并重复直到您用完行来读取或用完内存,以先到者为准)

问题:为什么可以删除第一行的read和discard,程序还是一样的运行?例如,为什么可以删除这两行if (!fgets (line, MAXC, fp)) return 1;而不影响程序运行?

示例使用/输出

使用文件中的数据dat/rowsfloats.txt并将程序编译为bin/readrowsfloat,您将收到:

$ ./bin/readrowsfloat dat/rowsfloats.txt
    1     2   20.50
    1     4    9.22
    2     6   11.70
    1     3   12.10

所有行都已成功读取,并且所有值都存储在您的数组中。

内存使用/错误检查

在您编写的任何动态分配内存的代码中,对于分配的任何内存块,您有两个责任:(1)始终保留指向内存块起始地址的指针,(2)它可以在没有时被释放更需要。

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

对于 Linuxvalgrind是正常的选择。每个平台都有类似的内存检查器。它们都易于使用,只需通过它运行您的程序即可。

$ valgrind ./bin/readrowsfloat dat/rowsfloats.txt
==25760== Memcheck, a memory error detector
==25760== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==25760== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==25760== Command: ./bin/readrowsfloat dat/rowsfloats.txt
==25760==
    1     2   20.50
    1     4    9.22
    2     6   11.70
    1     3   12.10
==25760==
==25760== HEAP SUMMARY:
==25760==     in use at exit: 0 bytes in 0 blocks
==25760==   total heap usage: 5 allocs, 5 frees, 5,744 bytes allocated
==25760==
==25760== All heap blocks were freed -- no leaks are possible
==25760==
==25760== For counts of detected and suppressed errors, rerun with: -v
==25760== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

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

慢慢来

这里包含了很多信息,而且比我预期的要长,所以放慢速度,花点时间了解每一行发生的事情以及原因。动态内存分配并不难,但必须循序渐进。

总之,你所做的一切都是分配一块内存来存储东西。如果您需要更大的内存块,您只需重新分配。当你重新分配时,你使用一个临时指针来避免在realloc()失败时造成内存泄漏。完成后,您只需free()分配已分配的内存。其余的只是算术跟踪您分配了多少以及可用的数量。

如果您还有其他问题,请仔细查看并告诉我。


推荐阅读