如何在C语言中初始化结构体内的动态二维数组?

4

我希望使用一个结构体来包含一些数据,并在程序中的不同函数之间传递它们,这个结构体必须包含一个动态的二维数组(我需要一个矩阵),其维度根据程序参数而变化。

这是我的结构体:

    struct mystruct {
        int **my2darray;

    }

我有一个从文件读取数字并将每个数字赋值给结构体数组中的单元格的函数。

我尝试这样做:

    FILE *fp = fopen(filename, "r");
    int rows;
    int columns;
    struct mystruct *result = malloc(sizeof(struct mystruct));
    result->my2darray = malloc(sizeof(int)*rows); 
    int tmp[rows][columns];
    for(int i = 0;i<rows;i++) {
        for(int j = 0;j<columns;j++) {
            fscanf(fp, "%d", &tmp[i][j]); 
        }
        result->my2darray[i]=malloc(sizeof(int)*columns);
        memcpy(result->my2darray[i],tmp[i],sizeof(tmp[i]));
    }

但是这给我带来了奇怪的结果:除了第一行,所有行都被正确存储。(我确定问题不在扫描文件上)。如果我将代码的第四行更改为以下内容,则情况会有所改善:
    result->my2darray = malloc(sizeof(int)*(rows+1)); 

它运行良好,现在我的问题是为什么会发生这种情况?


2
您展示的代码从未为“rows”或“columns”赋值。 如果这是您的实际代码,则它是错误的。 如果这不是重新生成问题的实际代码,则必须提供[mcve]。 - Eric Postpischil
1
不是直接相关的,但我认为struct mystruct应该包含行数和列数。 - Jabberwocky
2
考虑一个类似于 struct mystruct { size_t x,y; int my2darray[]; } 的结构体。 - chux - Reinstate Monica
@chux,这里应该是“my2darray是一个由int行组成的数组”,而不是“my2darray是一个int数组”。 - Paul Ogilvie
变量行和列也是动态的,取决于程序参数。我省略了执行此操作的代码部分,因为它可以忽略不计。 - Adl
@PaulOgilvie 评论中的“something like”是对VLA一般概念的含糊描述,使用了诡辩用语 - chux - Reinstate Monica
3个回答

3

以下是使用语言的一些“新”功能提供的答案:灵活的数组成员和指向VLA的指针。

首先,请查看正确分配多维数组。您需要一个2D数组,而不是某个查找表。

为了分配这样一个真正的2D数组,您可以利用灵活的数组成员:

typedef struct
{
  size_t x;
  size_t y;
  int flex[];
} array2d_t;

它将被分配为一个真正的数组,虽然会“混淆”成为一个维度:

size_t x = 2;
size_t y = 3;
array2d_t* arr2d = malloc( sizeof *arr2d + sizeof(int[x][y]) );

因为灵活数组成员的问题是它们既不能是VLA,也不能是二维的。虽然将其转换为另一个整数数组类型是安全的(关于别名和对齐性),但语法非常恶心:
int(*ptr)[y] = (int(*)[y]) arr2d->flex;  // bleh!

可以通过宏隐藏所有邪恶的语法:

#define get_array(arr2d) \
  _Generic( (arr2d),     \
            array2d_t*: (int(*)[(arr2d)->y])(arr2d)->flex )

阅读方式:如果 arr2d 是类型为 array2d_t * 的指针,则访问该指针以获取flex成员,然后将其转换为适当类型的数组指针。
完整示例:
#include <stdlib.h>
#include <stdio.h>

typedef struct
{
  size_t x;
  size_t y;
  int flex[];
} array2d_t;

#define get_array(arr2d) \
  _Generic( (arr2d),     \
            array2d_t*: (int(*)[(arr2d)->y])(arr2d)->flex )

int main (void)
{
  size_t x = 2;
  size_t y = 3;

  array2d_t* arr = malloc( sizeof *arr + sizeof(int[x][y]) );
  arr->x = x;
  arr->y = y;
  

  for(size_t i=0; i<arr->x; i++)
  {
    for(size_t j=0; j<arr->y; j++)
    {
      get_array(arr)[i][j] = i+j;
      printf("%d ", get_array(arr)[i][j]);
    }
    printf("\n");
  }

  free(arr);
  return 0; 
}

相较于指向指针的优势:

  • 实际的 2D 数组,可以使用单个函数调用进行分配/释放,并且可以传递给像 memcpy 这样的函数。

    例如,如果你有两个指向已分配内存的 array2d_t*,你可以通过单个 memcpy 调用复制所有内容,而不需要访问单个成员。

  • 结构体中没有额外的杂乱信息,只有数组。

  • 由于内存在堆上被分段,因此没有缓存未命中导致的数组访问问题。


谢谢,虽然其他答案的代码也可以正常运行,但这个解释最全面、有趣。 - Adl
“灵活数组成员的问题在于它们既不是一维的也不是二维的。”这句话不太清楚。typedef struct { size_t x; size_t y; int flex[][6]; } array2d6_t; 不就是一个反例吗?它的灵活数组成员不就是二维的吗?也许你的意思是它不能在两个维度上都是灵活的? - chux - Reinstate Monica
在使用宏时,我遇到了一个错误:“表达式必须具有结构体或联合类型,但它具有类型“Matrix *””。在我的情况下,我已经将array2d_t*命名为Matrix* - Harshit Singh
@HarshitSingh,所以请提出一个关于那个问题的单独问题。你可能犯了一些语法错误,没有人能在看不到代码的情况下帮助你。 - Lundin

1
上面的代码从未设置rowscolumns,因此从读取这些值中存在未定义行为
假设您正确设置了这些值,这不会分配适当数量的内存:
result->my2darray = malloc(sizeof(int)*rows);

您实际上正在为一个int数组分配空间,而不是int *数组。如果后者更大(很可能是这样),则您没有为数组分配足够的空间,并且再次通过写入超出分配内存的末尾来调用未定义的行为。
您可以像这样分配正确数量的空间:
result->my2darray = malloc(sizeof(int *)*rows);

甚至更好的是,因为这不依赖于实际类型:

result->my2darray = malloc(sizeof(*result->my2darray)*rows);

另外,无需创建临时数组来读取值。只需直接读入 my2darray 即可:
for(int i = 0;i<rows;i++) {
    result->my2darray[i]=malloc(sizeof(int)*columns);
    for(int j = 0;j<columns;j++) {
        fscanf(fp, "%d", &result->my2darray[i][j]); 
    }
}

0
在您提供的代码示例中,变量rowscolumns在使用之前未初始化,因此它们可能包含任何值,但很可能是等于0。无论哪种方式,按照现有的写法,结果总是不可预测的。
当需要一个2D数组时,在C语言中,将内存分配和释放内存封装到函数中可以简化任务并提高可读性。例如,在您的代码中,以下行将创建一个指向20个int存储位置的5个指针数组:(创建100个索引可寻址的int位置。)
int main(void)
{
    struct mystruct result = {0}; 

    result.my2darray = Create2D(5, 20);

    if(result.my2darray)
    {
        // use result.my2darray 
        result.my2darray[0][3] = 20;// for simple example, but more likely in a read loop                         
        // then free result.my2darray
        free2D(result.my2darray, 5);
    }
    return 0;
}

使用以下两个函数:
int ** Create2D(int c, int r)
{   
    int **arr;
    int    y;

    arr   = calloc(c, sizeof(int *)); //create c pointers (columns)
    for(y=0;y<c;y++)
    {
        arr[y] = calloc(r, sizeof(int)); //create r int locations for each pointer (rows)
    }
    return arr;
}

void free2D(int **arr, int c)
{
    int i;
    if(!arr) return;
    for(i=0;i<c;i++)
    {
        if(arr[i]) 
        {
            free(arr[i]);
            arr[i] = NULL;
        }
    }
    free(arr);
    arr = NULL;
}

请记住,使用这种技术创建的实际上是指向一组20个int位置的5个不同指针位置。这就是支持类似数组索引的原因,即我们可以说result.my2darray[1][3]表示一个5X20数组的第二列、第四行元素,而它实际上并不是一个数组。
int some_array[5][20] = {0};//init all elements to zero

在C语言中,通常所说的int数组,允许通过索引访问每个元素。实际上(尽管通常称为数组),它不是一个数组。该变量中元素的位置在内存中存储在一个连续的位置。
|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0...  (~ 82 more)

但是C语言保持这些位置,使它们都可以作为二维数组进行索引。

请参阅正确分配多维数组 - Lundin
@Lunden - 我看到的相似之处比不同之处多,除了一个明显的问题。你在每次调用calloc时都检查成功,而我没有。这是我在这里改进的唯一一件事。你还指出了我错过的其他什么吗? - ryyker
1
阅读答案,而不是问题。我指出,在您的答案中实际上没有分配任何二维数组,这使得此代码变得不必要地缓慢。 - Lundin
1
@Lunden - 非常好。我已经将它标记为收藏,并且很可能会改变我创建内存的方式。感谢分享。写得很好!(我将保留这个答案,不是因为它是最好的方式,而是因为它至少涉及了其他几个相关点,即未初始化的变量,更重要的是对我来说,代码可读性。) - ryyker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接