我在理解malloc
的功能时遇到了一些麻烦,也许这就是我看到这个问题的原因,但希望这里有人可以帮助我理解。
我使用以下函数分配一个 2D 数组:
int16_t** create_2d_array(uint8_t num_rows, uint16_t num_cols){
uint8_t i = 0;
int16_t **arr = (int16_t **)malloc(num_rows * sizeof(int16_t *));
for(i=0; i<num_rows; i++) {
arr[i] = (int16_t *)malloc(num_cols * sizeof(int16_t));
}
return arr;
}
我像这样调用这个函数:
twoD = create_2d_array(4, 512);
如果我在 for 循环的开头停止我的代码并使用我的 GDB 终端检查arr
的内容,那么我会得到以下内容:
gdb $ p/x arr[0]
$96 = 0x2001fa00
gdb $ p/x arr[1]
$97 = 0x0
gdb $ p/x arr[2]
$98 = 0x0
gdb $ p/x arr[3]
$99 = 0x0
这意味着,至少对我来说,arr[0]
被正确分配,但没有arr
的其他元素.
malloc
不应该确定是否可以分配请求的大小,如果不能,那么它应该返回一个NULL
指针?至少这是我对malloc
的理解.我做错了什么吗?
作为测试,我执行了以下行:
twoD_temp = create_2d_array(2, 4);
我再次在 for 循环开始时停止执行并打印arr
的内容。
gdb $ p/x arr[0]
$121 = 0x2001fa00
gdb $ p/x arr[1]
$122 = 0x2001fa10
这就是我所期望的。第一个索引是有效的指针,第二个索引也是有效的指针,因为我创建了一个指针数组。
执行 for 循环后,我打印相同的内容:
gdb $ p/x arr[0]
$125 = 0x2001fa00
gdb $ p/x arr[1]
$126 = 0x2001fa10
这仍然是我所期望的。现在唯一的区别是为列分配了内存。
在第一个malloc
之后,arr
是一个指向一个只包含垃圾的内存块的指针。for
循环将各个条目设置为指向行。
因此,arr[0]
和arr[1]
都不应包含任何特定值,直到for
循环将其值设置为指向它创建的各个行。
让我们仔细看看代码:
int16_t **arr = (int16_t **)malloc(num_rows * sizeof(int16_t *));
这将分配一个内存块,其大小足以为每行保存一个指针。变量arr
将指向此内存。内存块包含垃圾。
for(i=0; i<num_rows; i++) {
arr[i] = (int16_t *)malloc(num_cols * sizeof(int16_t));
}
这会将arr[0]
设置为指向大到足以容纳行的内存块。在此for
循环执行之前,数组中的arr[0]
和其他条目仅包含垃圾。
也许图表会有所帮助。在循环(arr
的 ponters 块)之前的第一次分配之后,这就是你所拥有的:
+--------+
arr -> | arr[0] | -> points to some arbitrary location
+--------+
| arr[1] | -> points to some arbitrary location
+--------+
| arr[2] | -> points to some arbitrary location
+--------+
| arr[3] | -> points to some arbitrary location
+--------+
这些指针将指向任意位置,因为malloc
分配内存,但不将其初始化为任何内容。
这就是你检查所有内容的状态,所以每个arr[]
都可以是任何值。一旦你经历了一次循环,你就有了:
+--------+ +----------------+
arr -> | arr[0] | -> | arr[0][0..511] |
+--------+ +----------------+
| arr[1] | -> points to some arbitrary location
+--------+
| arr[2] | -> points to some arbitrary location
+--------+
| arr[3] | -> points to some arbitrary location
+--------+
只有到那时,你的第二级分配才会开始指向一些有用的东西。
你的代码没有问题。 问题是你对malloc
如何工作的理解。
当内存通过malloc
动态分配时,字节的值是不确定的,所以它们可以采用任何值,包括0。 它们不保证包含任何特定值。
在您的示例中:
int16_t **arr = (int16_t **)malloc(num_rows * sizeof(int16_t *));
如果我们假设指针占用 8 个字节,而num_rows
为 4,则分配 32 个字节的空间,足以容纳 4 个类型int16_t *
的值。 此时,数组的每个成员都不包含有意义的值。 稍后执行此操作时:
arr[i] = (int16_t *)malloc(num_cols * sizeof(int16_t));
您将(假设调用成功)将从malloc
返回的有效内存地址分配给数组的成员,覆盖以前碰巧存在的任何垃圾值。 如果你在这一点上看arr[i]
的元素,即arry[i][0]
、arry[i][1]
等,这些值也是不确定的,直到你给它们分配一些东西。
你已经接受了答案(这是正确的,因为它详细解释了错误所在)。我知道这一点,因为我为此做出了贡献:-)
但是,我还想建议另一种分配 2D 数组的方法。将其作为一系列分配执行意味着您负责在完成阵列后清理各个分配。
尽管这可以通过另一个函数完成,但您必须将行数传递到该函数中,以便在释放指针数组分配之前正确释放每个行分配。
我过去使用的一种方法是为指针数组和所有行数组分配一个足够大的内存块,然后对其进行按摩,使其看起来像普通的 2D 数组(以便像array[row][col] = 7
这样的东西仍然有效)。
下面的代码这样做,唯一的要求是int16_t
的对齐要求不比int16_t*
的对齐要求更严格(这种情况很少见,你可以解决这个问题,但在绝大多数环境中可能没有必要):
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
void *create_2d_array(uint8_t num_rows, uint16_t num_cols, int clear_data) {
// Create a single block big enough for both level-1 pointer array
// and level-2 value array.
size_t totalMemSz =
(num_rows * sizeof(uint16_t*)) + // pointers.
(num_rows * num_cols * sizeof(uint16_t)); // values.
void *memBlock = clear_data ? calloc(totalMemSz, 1) : malloc(totalMemSz);
if (memBlock == NULL) return NULL;
// Populate the level-1 pointers to point at the level-2 rows.
for (size_t i = 0; i < num_rows; ++i) {
((int16_t**)memBlock)[i] = (int16_t*)(&(((char*)memBlock)[
num_rows * sizeof(uint16_t*) + // skip pointers.
i * num_cols * sizeof(uint16_t) // select row.
]));
}
return memBlock;
}
#include <stdio.h>
#include <stdbool.h>
void dumpArr(int16_t **addr, size_t num_rows, size_t num_cols) {
for (size_t i = 0; i < num_rows; ++i) {
printf("Pointer[%zd] = %p, data =", i, addr[i]);
for (size_t j = 0; j < num_cols; ++j) {
printf(" %2d", addr[i][j]);
}
putchar('n');
}
}
int main() {
puts("Running ...");
int16_t **arr = create_2d_array(4, 7, true);
arr[0][0] = 1;
arr[1][2] = 42;
arr[3][6] = 77;
dumpArr(arr, 4, 7);
free(arr);
}
该代码是create_2d_array
函数的完整测试程序,因此您可以根据需要对其进行测试。重要的是,当您完成 2D 阵列时,您只需使用free(arr)
释放它,而不必进行任何特殊的大小相关处理。
当前代码的示例运行:
Running ...
Pointer[0] = 0x675440, data = 1 0 0 0 0 0 0
Pointer[1] = 0x67544e, data = 0 0 42 0 0 0 0
Pointer[2] = 0x67545c, data = 0 0 0 0 0 0 0
Pointer[3] = 0x67546a, data = 0 0 0 0 0 0 77
你的代码没有什么真正的问题。下面我将讲几点意见。
malloc 不应该确定是否可以分配请求的大小,如果不能,那么它应该返回一个 NULL 指针吗?
是的,这是真的。但是您没有测试任何返回值,那么您怎么知道呢?
此外,使用变量而不是 sizeof 的类型也是一个好习惯。这减少了代码重复。所以写T *t= malloc(n * sizeof(*t))
而不是T *t = malloc(n * sizeof(T))
.而且强制转换是完全不必要的,也不会带来好处,除非您使用 C++ 编译器编译 C 代码。
因此,考虑到所有这些,我将像这样编写代码:
int16_t** create_2d_array(uint8_t num_rows, uint16_t num_cols)
{
uint8_t i = 0;
int16_t **arr = malloc(num_rows * sizeof(*arr));
if(!arr) {
fprintf(stderr, "Error allocating memoryn");
exit(EXIT_FAILURE);
}
for(i=0; i<num_rows; i++) {
arr[i] = malloc(num_cols * sizeof(*arr[0]));
if(!arr[i]) {
fprintf(stderr, "Error allocating memoryn");
exit(EXIT_FAILURE);
}
}
return arr;
}
如果你想真正硬核,并且能够像测试malloc
一样测试自己的功能,那么你可以这样做。这是为数不多的可接受的goto
用途之一
int16_t** create_2d_array(uint8_t num_rows, uint16_t num_cols)
{
uint8_t i = 0;
int16_t **arr = malloc(num_rows * sizeof(*arr));
if(!arr)
goto cleanup1;
for(i=0; i<num_rows; i++) {
arr[i] = malloc(num_cols * sizeof(*arr[0]));
if(!arr[i])
goto cleanup2;
}
return arr;
cleanup2:
do {
free(arr[i]);
} while(i-- > 0);
cleanup1:
free(arr);
return NULL;
}
然后你可以这样做:
int16_t ** array2d = create_2d_array(5,6);
if(!array2d) { /* Handle error */ }