C 中指向 void 指针的指针 - 我可以使用 void** 来实现基本多态性吗?
我可以理解 void**
在内存中的样子,但我想知道我使用它是否正确。我下面描述的内容是否存在根本性缺陷?例如,虽然我可以说“它对我有用”,但我是否以某种方式创建了糟糕/不可移植的代码?
所以我有一个小行星克隆。共有三个实体可以发射子弹,即玩家 (SHIP *player_1
、SHIP *player_2
) 和 UFO (UFO *ufo
)。当子弹发射时,知道谁发射了子弹是很重要的;如果是玩家,当它击中某物时,他们的分数需要增加。因此,子弹将存储它属于哪种实体(owner_type
)以及直接指向所有者的指针(owner
):
enum ShipType
{
SHIP_PLAYER,
SHIP_UFO
};
typedef struct Bullet
{
// ...other properties
enum ShipType owner_type;
void **owner;
} BULLET;
然后,当玩家点击按钮时或者 UFO 看到目标,将调用以下函数之一:
void ship_fire(SHIP **shipp)
{
BULLET *bullet = calloc(1, sizeof(BULLET));
bullet->owner_type = SHIP_PLAYER;
bullet->owner = (void**)shipp;
// do other things
}
void ufo_fire(UFO **ufop)
{
BULLET *bullet = calloc(1, sizeof(BULLET));
bullet->owner_type = SHIP_UFO;
bullet->owner = (void**)ufop;
// do other things
}
...它们可能会被调用,例如,如下所示:
ship_fire(&player_1);
最后,当子弹击中目标(例如小行星)时,我们取消引用所有者。如果是一艘船,我们可以立即增加分数。
void hit_asteroid(ASTEROID *ast, BULLET *bullet)
{
SHIP *ship_owner;
if (bullet->owner_type == SHIP_PLAYER && *bullet->owner != NULL)
{
ship_owner = (SHIP*)*bullet->owner;
ship_owner->score += 1000;
}
}
这看起来是一个合理的方法吗?就像我说的,它对我有用,但我只有几个月的 C 经验。
最后一点:为什么我不使用 void*
而是使用 void**
?因为我想避免悬空指针。换句话说,假设 player_1
死亡并获得自由,但他们的子弹继续前进并击中了小行星。如果我只有一个 void*
,则 hit_asteroid
函数无法知道 bullet->owner
指向已取消分配的内存。但是使用 void**
,我可以有效地检查它是否为 NULL;如果 player_1
为 NULL,则 *bullet->owner
也将为 NULL。
编辑:到目前为止,所有受访者都同意这里可能没有必要使用 void**,因为我可以避免悬空指针问题(例如,通过静态分配基础对象)。他们是正确的,我将重构。但我仍然有点想知道我是否使用 void** 的方式可能会破坏某些东西,例如在内存分配/转换方面。但我想,如果没有人举手宣布它有缺陷,那么它至少看起来在技术上是可行的。
谢谢!
I can understand how a void**
might look in memory, but I'm wondering if I'm using it quite right. Are there any fundamental flaws in what I describe below? For example, although I can say "it works for me", am I creating bad / unportable code in some way?
So I have an Asteroids clone. There are three entities that can fire bullets, the players (SHIP *player_1
, SHIP *player_2
) and the UFO (UFO *ufo
). When a bullet is fired, it's important to know who fired the bullet; if it was a player, when it hits something their score needs to be incremented. So, the bullet will store what kind of entity it belongs to (owner_type
) and also a pointer directly to the owner (owner
):
enum ShipType
{
SHIP_PLAYER,
SHIP_UFO
};
typedef struct Bullet
{
// ...other properties
enum ShipType owner_type;
void **owner;
} BULLET;
Then, when the player hits the button or the UFO sees a target, one of these functions will be called:
void ship_fire(SHIP **shipp)
{
BULLET *bullet = calloc(1, sizeof(BULLET));
bullet->owner_type = SHIP_PLAYER;
bullet->owner = (void**)shipp;
// do other things
}
void ufo_fire(UFO **ufop)
{
BULLET *bullet = calloc(1, sizeof(BULLET));
bullet->owner_type = SHIP_UFO;
bullet->owner = (void**)ufop;
// do other things
}
... they may be called, for example, like this:
ship_fire(&player_1);
Finally, when the bullet hits a target (such as an asteroid), we dereference the owner. If it's a ship, we can increment the score there and then.
void hit_asteroid(ASTEROID *ast, BULLET *bullet)
{
SHIP *ship_owner;
if (bullet->owner_type == SHIP_PLAYER && *bullet->owner != NULL)
{
ship_owner = (SHIP*)*bullet->owner;
ship_owner->score += 1000;
}
}
Does that seem a reasonable approach? Like I say, it works for me, but I only have a couple of months of C experience.
A final note: why do I not use a void*
instead of a void**
? Because I want to avoid dangling pointers. In other words, say that player_1
dies and is free'd, but their bullet keeps going and hits an asteroid. If I only have a void*
, the hit_asteroid
function has no way of knowing that bullet->owner
points to de-allocated memory. But with a void**
, I can validly check to see if it's NULL; if player_1
is NULL, then *bullet->owner
will be NULL too.
EDIT: All respondents so far concur that using a void** probably isn't necessary here because I can avoid the dangling pointers issue (by just statically allocating the base object, for instance). They're correct and I will refactor. But I'm still kinda interested to know if I've used void** in a way that might break something e.g. in terms of memory allocation / casting. But I guess if no-one has thrown their hands in the air and declared it faulty, it at least resembles something that would technically work.
Thanks!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
即使您想继续按照原来的方式进行操作,也不需要(也不应该)使用
void **
。虽然
void *
是通用指针类型,但void **
不是通用指针到指针类型 - 它应该始终指向真正的void *
对象。您的代码通过void **
类型的左值取消引用SHIP **
或UFO **
指针 - 从技术上讲,这不能保证有效。 (当您执行(SHIP*)*bullet->owner
时会发生这种情况)。然而,好消息是您可以继续使用双指针方法,使用普通的
void *
来完成这项工作。void *
可以愉快地存储指向指针的指针(因为毕竟,这只是另一种指针)。如果将owner
更改为void *
,那么在ship_fire
中,您将执行以下操作:在
hit_asteroid
中,您将执行 以下操作:一般而言,使用指针强制转换的规则是:首先将指针强制转换回您知道的实际指针类型,然后取消引用。
Even if you wanted to continue doing it the way you were, you don't need to use
void **
(and shouldn't).Although
void *
is a generic pointer type,void **
is not a generic pointer-to-pointer type - it should always point to a genuinevoid *
object. Your code dereferences aSHIP **
orUFO **
pointer through an lvalue of typevoid **
- that's technically not guaranteed to work. (This happens when you do(SHIP*)*bullet->owner
).However, the good news is that you could continue to use the double-pointer method, using a plain
void *
to do the job.void *
can happily store a pointer-to-a-pointer (because that, after all, is just another kind of pointer). If you changeowner
tovoid *
, then inship_fire
you would do this:and in
hit_asteroid
you would do this:In general, the rule for working with pointer casts is: First cast the pointer back to the pointer type that you know it really is, then dereference.
Linux 内核以一种有趣的方式做到了这一点。类似于
阅读 linux 内核源代码以获取该技术的许多示例。
The linux kernel does this in an interesting way. It would be something like
Read the linux kernel source code for lots of examples of this tecnique.
由于您只有两种可能的类型,因此我将使用联合来处理此类事情,如下所示:
请注意,我没有使用您使用的指针到指针方案。我不太相信它的必要性,而且我建议的代码不需要这样的技术。
Since you only have two possible types, I'd use a union for this sort of thing, like so:
Note that I didn't use the pointer-to-a-pointer scheme that you used. I'm not really convinced of the necessity of it, and the code I suggested doesn't require such a technique.
首先,检查 mipadi 建议的 union 结构;这是一种非常可读且有效的处理多态性的方法。
更接近您的片段/问题,快速浏览一下,我没有看到需要/使用指针到指针引入的双重间接。如果 xxxx_fire() 方法的参数是指向 xxxx 对象的[直接]指针(并且其余逻辑中的类型转换等也相应遵循),则整个逻辑将工作相同。
指向指针的指针在以下情况下很有用:中间指针的值可能会在某个时刻发生变化,例如,如果底层对象被移动,或者它被完全替换为不同的对象(例如游戏中新关卡的装备更好的船舶部分等)。 ...)
编辑:(关于使用双重间接寻址来管理可能被释放的对象“队列”。
回应您的评论,请不要重构,以便当对象被杀死/销毁(作为游戏的一部分)时,对象不会被解除分配(从内存中)。相反,请查看如下内容,因为这确实是一个指针到指针构造有很大帮助的示例。它的工作原理如下:
Insight,C 语言中的一个简短片段可能更明确;抱歉,我用文字描述了这一点......
First off, check the union construct suggested by mipadi; that's a very readable and efficient way of dealing with polymorphism.
Closer to your snippet/question, at a quick glance, I don't see the need/use for the double indirection introduced by pointer-to-pointers. The whole logic would work the same if the arguments to xxxx_fire() methods were [direct] pointers to xxxx objects (and if the typecast etc. in the rest of the logic were to follow accordingly.
Pointers to pointers are useful when the value of the intermediate pointer may be changed at some point. For example if the underlying object is moved, or if it replace by a different object altogether (say a better-equipped ship part of a new level in game etc...)
Edit: (on the use of double indirection to manage "fleets" of objects which may be deallocated.
Responding to your comment, do not refactor so that the objects are not de-allocated (from memory) when they get killed/detroyed (as part of the game). Instead, look into something like the following, as this is indeed an example where the pointer-to-pointer construct helps a lot. Here's how it could work:
In insight, a short snippet in C language may have been more explicit; sorry I described this in words...
如果你的项目符号所有者经常改变(例如释放),指针到指针的方法是合适的。工会解决方案并没有直接解决这个问题;如前所述,它不支持在不触摸该船的每个子弹上的指针的情况下取消分配船。当然,这在某些实现中实际上可能是一个实用的解决方案,例如,如果您需要查找给定玩家的所有子弹,您可以维护它们的链表:每个子弹的“next_bullet”指针和“last_bullet” ” 指向每个玩家列表头部的指针。
我不会单独分配每个项目符号,而是会遵循 mjv 的建议,预先分配其中一些项目符号并选择下一个可用的项目符号。在链表实现中,您可以使用相同的“next_bullet”指针来维护当前未使用的预分配项目符号列表。这种方法的优点是,如果您用完它们,您可以轻松分配更多,而不是维护数组,即,如果可用项目符号列表为空,只需根据需要将它们添加到列表中。同样,将“过期”(爆炸?)的子弹放回到可用子弹列表中,分配的数量将自动适应所需的数量。
我想到的另一件事是,您可能不需要知道哪个特定的 UFO(或其他敌人)拥有给定的子弹;只需为拥有玩家设置一个指针(例如
SHIP **
),并将所有非玩家子弹的指针设置为 NULL。如果这不合适,您还可以考虑将每个所有者的类型存储在所有者结构本身的开头,例如:请注意,此技巧依赖于指向可互换的不同类型结构的指针,并且单个枚举存储在相同的位置每种类型的结构中的偏移量。实际上,这些并不是不合理的假设,但我不确定标准 C 中是否严格保证这种行为(但是,例如 struct sockaddr 使用相同的技巧,并且它被各种 POSIX 网络函数使用,例如
绑定
)。If your bullet owners are frequently changed (e.g. deallocated), the pointer-to-pointer approach is suitable. The union solution does not address this concern directly; as presented, it does not support deallocating ships without touching the pointer on each of that ship's bullets. Of course, that may actually be a practical solution in some implementations, e.g. if you have a need to find all the bullets of a given player, you could maintain a linked list of them: a “next_bullet” pointer for each bullet and “last_bullet” pointer to the head of the list for each player.
And instead of allocating each bullet separately, I would also follow mjv's suggestion of pre-allocating some number of them and picking the next available one. In the linked list implementation, you could use the same “next_bullet” pointers to maintain one list of pre-allocated bullets not currently in use. The advantage of this approach is that you could easily allocate more if you ran out of them, instead of maintaining an array, i.e. if the list of available bullets is empty just add them to the list on demand. Similarly, put “expired” (exploded?) bullets back into the list of available ones and the amount allocated will automatically adapt to however many is required.
Another thing that comes to mind is that you might not need to know which particular UFO (or other enemy) owns a given bullet; just have a single pointer (e.g.
SHIP **
) for the owning player and set it to NULL for all non-player bullets. If this is not suitable, you could also consider storing the type of each owner in the beginning of owner struct itself, e.g.:Note that this trick relies on pointers to different types of structs being interchangeable, and the single enum being stored at the same offset in each type of struct. In practice these are not unreasonable assumptions, but I'm not certain that this behaviour is strictly guaranteed in standard C (however, e.g.
struct sockaddr
uses the same trick, and it's used by various POSIX networking functions likebind
).我会这样做:
如果只有玩家,最好为每个玩家设置一个
dead
标志,并在他们死亡时进行设置。 (并让它们静态分配。)或者你可以有一个数组,或者...
编辑:非常量玩家,一个可怕的过度设计的解决方案:
或者,引用计数可能会很好地工作,而不需要 O(n) 内存。
而且,这些都不是线程安全的。
I would do like this:
If there's only <constant> players, you would be better off having a
dead
flag for each and setting when they die. (And having them statically allocated.)Or you could have an array, or...
Edit: Nonconstant players, a horrifically overengineered solution:
Alternatively, a reference count might work well, without the O(n) memory.
Also, neither of these is threadsafe.