在C语言中为结构体和指针分配内存的方法是malloc。

108

假设我想定义一个结构来表示向量的长度和其值:

struct Vector{
    double* x;
    int n;
};

现在,假设我想定义一个向量y并为其分配内存。

struct Vector *y = (struct Vector*)malloc(sizeof(struct Vector));

我在互联网上搜索发现,我应该单独为x分配内存。

y->x = (double*)malloc(10*sizeof(double));

但是,看起来我在为 y->x 分配内存时,会分别为 y 和 y->x 两次分配内存,这似乎浪费了内存。 如果您能让我知道编译器实际上是如何工作的,以及正确初始化 y 和 y->x 的方法,那将不胜感激。


8
正如paxdiablo突出指出的那样,请勿在C语言中将malloc()的返回值转换为类型。我永远不会理解为什么每个人都感觉需要这样做。 :( - unwind
17
@unwind,也许他们是老的C++程序员正在升级到C语言 :-) - paxdiablo
2
@unwind 当我在C代码中使用Nvidia的nvcc编译器时,如果我不对malloc的结果进行强制类型转换,它会抛出一个错误。 - Nubcake
根据此链接,这可能是因为nvcc在C++模式下运行底层编译器,因为他们的CUDA接口是C++。在C中,您不会因此而出现错误。在C++中,void *不会自动转换为其他指针,需要进行强制转换(或者,在C++中,当然可以不使用malloc())。 - unwind
@unwind 是的,我后来发现了这个问题 :) 只是想说明一种情况,如果你没有将结果转换,那么它会抛出一个错误。 - Nubcake
显示剩余2条评论
8个回答

193

不,你并没有为 y->x 分配内存两次。

相反,你为这个结构体(其中包括一个指针)分配了内存,为指针所指向的内容分配了内存。

可以这样理解:

         1          2
        +-----+    +------+
y------>|  x------>|  *x  |
        |  n  |    +------+
        +-----+

实际上,您需要两个分配(12)才能存储您所需的所有内容。

此外,您的类型应该是struct Vector *y,因为它是一个指针,在C中不应该转换malloc的返回值。

这可能会隐藏您不想要的某些问题,而且C完全可以隐式地将void*返回值转换为任何其他指针。

当然,您可能希望封装这些向量的创建以使其更易于管理,例如使用以下头文件vector.h

struct Vector {
    double *data;    // Use readable names rather than x/n.
    size_t size;
};

struct Vector *newVector(size_t sz);
void delVector(struct Vector *vector);
//void setVectorItem(struct Vector *vector, size_t idx, double val);
//double getVectorItem(struct Vector *vector, size_t idx);

然后,在vector.c中,你有用于管理向量的实际函数:

#include "vector.h"

// Atomically allocate a two-layer object. Either both layers
// are allocated or neither is, simplifying memory checking.

struct Vector *newVector(size_t sz) {
    // First, the vector layer.

    struct Vector *vector = malloc(sizeof (struct Vector));
    if (vector == NULL)
        return NULL;

    // Then data layer, freeing vector layer if fail.

    vector->data = malloc(sz * sizeof (double));
    if (vector->data == NULL) {
        free(vector);
        return NULL;
    }

    // Here, both layers worked. Set size and return.

    vector->size = sz;
    return vector;
}

void delVector(struct Vector *vector) {
    // Can safely assume vector is NULL or fully built.

    if (vector != NULL) {
        free(vector->data);
        free(vector);
    }
}

通过这样封装向量管理,您可以确保向量要么完全构建,要么根本没有构建——不存在它们半构建的机会。

这还允许您在不影响客户端的情况下完全更改未来的基础数据结构。例如:

  • 如果您想将它们变为稀疏数组以在空间和速度之间进行平衡。
  • 如果您希望在每次更改时将数据保存到持久存储中。
  • 如果您希望确保所有向量元素都初始化为零。
  • 如果您想为效率而将向量大小与向量容量分开。(1)

您还可以添加更多功能,例如安全地设置或获取向量值(请参见标头中的注释代码),随着需要的出现而增加。

例如,您可以(作为一种选项)在有效范围之外的设置值时默默地忽略并在获取这些值时返回零。或者您可以引发某种错误,或者尝试在幕后自动扩展向量。(1)


在使用向量方面,一个简单的示例是以下内容(非常基本的)main.c

#include "vector.h"

#include <stdio.h>

int main(void) {
    Vector myvec = newVector(42);
    myvec.data[0] = 2.718281828459;
    delVector(myvec);
}

(1) 可扩展向量的潜力需要进一步说明。

许多向量实现将容量与大小分开。前者是在需要重新分配之前可以使用的元素数量,后者是实际向量大小(始终<=容量)。

在扩展时,你希望通常以这样的方式扩展,即尽量不频繁进行,因为它可能是一项昂贵的操作。例如,你可以添加比严格必要的多5%,以便在连续添加一个元素的循环中,它不必为每个单个项重新分配。


51
我的ASCII艺术技能很厉害,让你惊叹不已 :-) - paxdiablo
3
在 if(retval == NULL) 中,retval 应该改为 retVal。 - Legion Daeth
@paxdiablo 谢谢你清晰的解释,以及创建函数的妙点子。 您能否添加一个 main 函数来展示如何调用 newVector() 函数? - MA Bisk
1
@MABisk:完成了。我还在其他一些方面添加了更多的阐述。 - paxdiablo
@paxdiablo 非常感谢您。您真的重写了整个答案! - MA Bisk

5
第一次使用Vector时,你会为变量x和n分配内存。 然而,x并没有指向任何有用的东西。 这就是为什么需要进行第二次内存分配的原因。

4

原则上你已经做得正确了。对于你想要的,你确实需要两个malloc()

仅提供一些注释:

struct Vector y = (struct Vector*)malloc(sizeof(struct Vector));
y->x = (double*)malloc(10*sizeof(double));

应该是

struct Vector *y = malloc(sizeof *y); /* Note the pointer */
y->x = calloc(10, sizeof *y->x);

在第一行,您为Vector对象分配内存。malloc()返回指向分配的内存的指针,因此y必须是一个Vector指针。在第二行,您为10个双精度数组分配内存。
在C中,您不需要显式转换,并且使用sizeof *y而不是sizeof(struct Vector)更加安全,而且可以节省打字时间。
您可以重新排列结构并执行单个malloc(),如下所示:
struct Vector{    
    int n;
    double x[];
};
struct Vector *y = malloc(sizeof *y + 10 * sizeof(double));

“节省打字”从来不是编程决策的有效论据。你会选择使用 *y 的真正原因是出于安全考虑,确保为相应的变量分配足够的空间。 - Lundin
@Lundin 我已经更新了我的回答,但对我来说,“类型安全”这个论点几乎与编写if(NULL == foo)一样。 - Wernsey
1
@Lundin 写 sizeof *y 可以帮助你避免像写 sizeof(Vector) 时实际想要的是 sizeof(Matrix) 这样的错误。你有多经常犯这样的错误?当你犯错时,你会多快地找到并修复它们呢?我同意这可以增加类型安全性,并且我在自己的代码中也会写 sizeof *y,但这几乎和写 if(NULL == foo) 一样谨慎,目的是为了防止 == 的拼写错误。 - Wernsey
2
它们在功能上是等价的。y是指向struct Vector的指针,因此sizeof * y表示“y所指向的大小”,即sizeof struct Vector - Wernsey
2
@ShmuelKamensky 在其他讨论中,有人告诉我在这种情况下 *y 处于未求值的上下文中,因此不会被解引用。可能与此相关:https://dev59.com/Y1sW5IYBdhLWcg3wVmGV#35088909 - Seiko Santana
显示剩余2条评论

3
一些要点: struct Vector y = (struct Vector*)malloc(sizeof(struct Vector)); 不正确。
应该是 struct Vector *y = (struct Vector*)malloc(sizeof(struct Vector));,因为 y 持有 struct Vector 的指针。
第一个 malloc() 仅分配足够容纳 Vector 结构体(包括指向 double 和 int 的指针)的内存。
第二个 malloc() 实际上分配了可容纳 10 个 double 的内存。

1
当您为struct Vector分配内存时,您只为指针x分配内存,即为其值分配空间,该值包含地址。因此,您不会为y.x所引用的块分配内存。请注意保留html标签。

1

第一个 malloc 分配内存给结构体,包括指向 double 类型的指针 x 所需的内存。第二个 malloc 分配内存给 x 指向的 double 值。


0
您可以通过一次性分配Vector和数组来完成单个malloc。例如:
struct Vector y = (struct Vector*)malloc(sizeof(struct Vector) + 10*sizeof(double));
y->x = (double*)((char*)y + sizeof(struct Vector));
y->n = 10;

这将分配向量'y',然后使y->x指向向量结构体之后立即分配的额外数据(但在同一内存块中)。
如果需要调整向量大小,则应按建议使用两个分配进行。这样,内部y->x数组就可以在保持向量结构体'y'不变的情况下调整大小。

为什么你特意将y强制转换为char类型?为什么不使用(double*)y + sizeof(struct Vector)呢? - Vishnu Prasath
sizeof返回结构体大小(以字节为单位),指针算术运算符“+”将添加到“y”指针的倍数sizeof(* y)。如果我们像上面那样做,y将增加sizeof(double)* sizeof(struct),这太多了。将y转换为char 可以让我们通过sizeof(char) sizeof(struct)= 1 * sizeof(struct)递增y。 - PQuinn
1
不要这样做。这并不能确保 y->x 能够正确对齐以容纳一个 double 类型的值。如果不能,那么你将会得到未定义的行为。 - real-or-random

-1

当您执行 malloc(sizeof(struct_name)) 时,它会自动为结构体的完整大小分配内存,您无需为内部的每个元素单独执行 malloc。

使用 -fsanitize=address 标志来检查您如何使用程序内存。


1
错误。 x只是一个指针,你必须为x指向的值分配内存。 阅读其他答案以获取更多细节。 (例如:https://dev59.com/ZWUq5IYBdhLWcg3wHs6Y#14768280) - Maxime Ashurov

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