>我想了解在另一个struct
中引用struct
时使用指针和值之间的区别。
我的意思是,我可以有这两个声明:
struct foo {
int bar;
};
struct fred {
struct foo barney;
struct foo *wilma;
}
看来我可以从barney
和wilma
条目中获得相同的行为,只要我在访问它们时相应地取消引用即可。barney
案直觉上感觉"不对",但我不能说为什么。
我只是依靠一些 C 未定义的行为吗?如果没有,选择一种风格而不是另一种风格的原因是什么?
下面的代码显示了我如何得出结论,这两个用例是等价的;clang
和gcc
都没有抱怨任何事情。
#include <stdio.h>
#include <stdlib.h>
struct a_number {
int i;
};
struct s_w_ptr {
struct a_number *n;
};
struct s_w_val {
struct a_number n;
};
void store_via_ptr(struct s_w_ptr *swp, struct s_w_val *swv) {
struct a_number *i = malloc(sizeof(i));
i->i = 1;
swp->n = i;
swv->n = *i;
}
void store_via_val(struct s_w_ptr *swp, struct s_w_val *swv) {
struct a_number j;
j.i = 2;
swp->n = &j;
swv->n = j;
}
int main(void) {
struct s_w_ptr *swp = malloc(sizeof(swp));
struct s_w_val *swv = malloc(sizeof(swv));
store_via_ptr(swp, swv);
printf("p: %d | v: %dn", swp->n->i, swv->n.i);
store_via_val(swp, swv);
printf("p: %d | v: %dn", swp->n->i, swv->n.i);
}
在结构中同时具有结构成员并在结构中具有指向结构的指针是完全有效的。它们必须以不同的方式使用,但两者都是合法的。
为什么结构中有结构
?一个原因是将事物组合在一起。例如:
struct car
{
struct motor motor; // a struct with several members describing the motor
struct wheel wheel; // a struct with several members describing the wheels
...
}
struct car myCar = {....initializer...};
myCar.wheel = SomeOtherWheelModel; // Replace wheels in a single assign
myCar.wheel.pressure = 2.1; // Change a single wheel member
为什么结构中有结构指针?
一个非常明显的原因是,通过使用 N 倍结构大小的动态分配,可以用作 N 个结构的数组。
另一个典型的示例是链表,其中有一个指向与包含指针的结构类型相同的结构的指针。
在struct
中使用struct
而不是在struct
中使用指向struct
的指针有几个优点:
- 它需要较少的内存分配。 在
struct
中有一个指向struct
的指针的情况下,编译器将分配内存以将指向父struct
的指针存储在父struct
中,并为子struct
单独分配内存。 - 通常需要其他说明才能访问子
struct
的内容。 例如,假设程序正在读取子struct
的内容。 如果使用struct
中的struct
,程序将对变量的地址应用偏移量并读取该内存位置的内容。 在指针指向struct
中的struct
的情况下,程序实际上会对父struct
变量地址应用偏移量,获取子struct
的地址,然后从内存中读取子struct
的内容。 - 需要为父
struct
和子声明一个单独的变量,如果使用初始值设定项,则需要单独的初始值设定项。 对于struct
中的struct
,只需声明一个变量并使用单个初始值设定项。 - 在使用动态内存分配的情况下,开发人员必须记住在变量超出范围之前为子对象和父对象释放内存。 在
struct
中struct
的情况下,必须仅为一个变量释放内存。 - 最后,如示例中所示,如果使用指针,则可能需要 Null 检查以确保指向子
struct
的指针已初始化。
在struct
中使用指向struct
的指针的主要优点是,如果需要将子struct
替换为程序中的另一个struct
,例如链表。 不太常见的情况可能是子struct
可以是多个类型。 在这种情况下,您可以为子项使用void *
类型。 我还可以在结构中使用指针来指向数组,以防指向的数组在实例之间大小可能不同。
根据我对上面示例中所示情况的了解,我倾向于在struct
中使用struct
,因为两个对象的大小和类型都是固定的,并且因为它们似乎不需要分开。
C 结构可用于对相关数据进行分组,例如书籍的标题、作者、分配的书号等。但是我们使用结构的大部分内容是在内存中创建数据结构(在"结构"一词的不同意义上
)。考虑到这本书的作者有一个名字、出生日期、其他传记信息、他们写过的书清单等等。我们可以在struct book
中包含一个包含所有这些信息的struct author
。但是,如果作者写了一百本书,我们可以拥有所有这些信息的 100 份副本,每份struct book
一份。此外,我们不能继续使用struct author
"直接包含结构中的数据"模型,因为如果这些struct book
成员还必须包含作者的struct author
,则它不能包含作者出版的每本书的struct book
- 每个对象都必须包含自身。
创建一个struct author
并让该作者的每个struct book
链接到他们的struct author
会更有效。
另一个例子是,我们使用指针来创建数据结构,以便有效地访问数据。如果我们正在读取数千个项目的数据并希望按名称对它们进行排序,一种选择是为一定数量的结构分配内存、读取数据并对数据进行排序。当读取新数据并且我们用完了分配的所有内存时,我们分配新内存,如有必要,将所有旧数据复制到新内存中,并移动一些数据,以便我们可以将新数据插入适当的位置。但是,我们有许多比这更好的选择。我们可以使用链表、二叉树、其他类型的树和哈希表。
这些数据结构实际上需要使用指针。二叉树将有一个根节点,每个节点包含两个指针,一个指向排序顺序上早于它的节点的子树,另一个指向晚于它的节点的子树。我们可以通过遵循指向早期或后期节点的指针来查找树中的项目,以找到正确的位置。我们可以通过更改一些指针来插入项目。如果树碰巧变得不平衡,我们可以通过更改指针来重新排列树中的节点。节点中的大量数据不必更改或复制,只需一些指针即可。
我们还可以使用指针为同一数据提供多个结构。有关书籍的所有数据都可以存储在一个地方,按名称排序的树可以包含节点,其中每个节点包含一个指向书籍结构的指针和两个指向子树的指针。我们可以有一棵这样的树按书名排序,另一棵树按作者姓名排序,另一棵树按分配的书号排序。然后,我们可以按标题、作者或编号有效地查找书籍,但在struct book
对象中只有一个完整书籍数据的主副本。查找数据位于树中,树中仅包含指针。这比复制每棵树的所有struct book
数据要有效得多。
因此,我们在使用结构或指针作为成员之间进行选择的原因不是 C 语法是否允许我们引用数据 - 我们可以在这两种情况下获取数据。原因是因为一种方法需要嵌入数据,该方法不灵活且需要复制数据,而另一种方法灵活高效。
让我们首先考虑这个函数
void store_via_ptr(struct s_w_ptr *swp, struct s_w_val *swv) {
struct a_number *i = malloc(sizeof(i));
i->i = 1;
swp->n = i;
swv->n = *i;
}
本声明
struct a_number *i = malloc(sizeof(i));
等效于以下声明
struct a_number *i = malloc(sizeof( struct a_number * ));
因此,通常,当sizeof( struct a_number )
大于sizeof( struct a_number * )
时,该函数可以调用未定义的行为。
看来你的意思是
struct a_number *i = malloc(sizeof( *i ) );
^^^
如果您将函数拆分为两个函数,每个函数的参数如下
void store_via_ptr1( struct s_w_ptr *swp ) {
struct a_number *i = malloc(sizeof( *i ) );
i->i = 1;
swp->n = i;
}
和
void store_via_ptr( struct s_w_val *swv ) {
struct a_number *i = malloc(sizeof( *i));
i->i = 1;
swv->n = *i;
}
然后在第一个函数中,指针指向的对象需要记住swp
以释放函数中分配的内存。否则会出现内存泄漏。
第二个函数已经产生内存泄漏,因为分配的内存未释放。
现在让我们考虑第二个函数
void store_via_val(struct s_w_ptr *swp, struct s_w_val *swv) {
struct a_number j;
j.i = 2;
swp->n = &j;
swv->n = j;
}
在这里,指针swp->n
将指向一个本地对象j
。因此,退出函数后,此指针将无效,因为指向的对象将不活动。
所以这两个函数都是不正确的。相反,您可以编写以下函数
int store_via_ptr(struct s_w_ptr *swp ) {
swp->n = malloc( sizeof( *swp->n ) );
int success = swp->n != NULL;
if ( success ) swp->n->i = 1;
return success;
}
和
void store_via_val( struct s_w_val *swv ) {
swv->n.i = 2;
}
何时将结构类型的整个对象包含在结构类型的另一个对象中,或在结构类型的其他对象中使用指向结构类型的对象的指针,取决于使用此类对象的设计和上下文。
例如,考虑结构结构点
struct Point
{
int x;
int y;
};
在这种情况下,如果你想声明一个结构结构矩形,那么很自然地定义它,就像
struct Rectangle
{
struct Point top_left;
struct Point bottom_right;
};
另一方面,如果你有一个双面单链表,那么它看起来像
struct Node
{
int value;
struct Node *next;
};
struct List
{
struct Node *head;
struct Node *tail;
};
两个问题:
-
在
store_via_ptr
中,您可以动态地为i
分配内存。使用s_w_val
时,复制结构,然后离开指针。这意味着指针将丢失,以后无法传递给free
。 -
在
store_via_val
中,您将swp->n
指向局部变量j
。一个变量,其生存期将在函数返回时结束,留下无效指针。
第一个问题可能会导致内存泄漏(在简单的示例问题中,您从不关心这一点)。
第二个问题更糟,因为当您取消引用指针swp->n
时,它会导致未定义的行为。
与此无关,在main
函数中,您不需要为结构动态分配内存。您可以将它们定义为普通结构对象,并在调用函数时使用指针运算符&
。