从指向其第二个成员的指针获取指向结构的指针是否合法?

Is it legal C to obtain the pointer to a struct from the pointer to its 2nd member?

我想知道注释“Is this legal C?”前面的行(在底部的函数 dumpverts() 中)是否是合法的 C 或不是:

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>

struct  stvertex 
    {
    double  x;
    double  y;
    char    tag;
    };
    
struct  stmesh
    {
    size_t      nverts;
    struct stvertex verts[]; /* flexible array member */
    };
    

void    dumpverts(struct stvertex *ptr);

int main(int argc, char **argv)
    {
    size_t f;
    size_t usr_nverts=5; /* this would come from the GUI */
    
    struct stmesh *m = malloc(sizeof(struct stmesh) + usr_nverts*sizeof(struct stvertex));
    if(m==NULL) return EXIT_FAILURE;
    
    m->nverts=usr_nverts;
    for(f=0;f<m->nverts;f++)
        {
        m->verts[f].x = f*10.0; /* dumb values just for testing */
        m->verts[f].y = f*7.0;
        m->verts[f].tag = 'V';
        }
    
    dumpverts( &(m->verts[0]) );
    
    return EXIT_SUCCESS;
    }


void    dumpverts(struct stvertex *ptr) /* Here is were the juice is */
    {
    size_t f;
    
    /* Is this legal C? */
    struct stmesh   *themesh = (struct stmesh *)((char *)ptr - offsetof(struct stmesh, verts));
    
    for(f=0;f<themesh->nverts;f++)
        {
        printf("v[%zu] = (%g,%g) '%c'\n", f, themesh->verts[f].x, themesh->verts[f].y, themesh->verts[f].tag);
        }
    fflush(stdout);
    }

我倾向于认为这是合法的,但我不能 100% 确定严格的别名规则是否允许从 char *struct stmesh * 的转换,就像 [=11] 中有趣的行=] 函数体在做什么。

基本上,该行是从指向其第二个成员的指针获取指向 struct stmesh 的指针。我没有看到任何与对齐相关的潜在问题,因为整个 struct stmesh 的内存来自 malloc(),因此结构的开头是“适当对齐的”。但正如我所说,我不确定严格的别名规则。

如果它打破了严格的别名,是否可以在不更改 dumpverts() 函数原型的情况下使其兼容?

如果你想知道我要这个是干什么的,主要是为了了解offsetof()的极限在哪里。是的,我知道 dumpverts() 应该接收指向 struct stmesh 的指针。但我想知道是否可以通过合法的方式以编程方式获取 struct stmesh 指针。

是的,有效。您可以将任何 non-function 指针与 char * 相互转换:标准的明确部分允许:

C17,第 6.3.2.3 节,第 7 条:

When a pointer to an object is converted to a pointer to a character type, the result points to the lowest addressed byte of the object. Successive increments of the result, up to the size of the object, yield pointers to the remaining bytes of the object.

允许这样做的原因正是为了让您可以像您展示的那样玩把戏。但是请注意,这仅在指针首先来自 struct stmesh 时才有效(即使您在执行此操作时在范围内没有该结构)。

旁注:您的示例中根本不需要 offsetof(struct stmesh, nverts)。它保证为零。第 6.7.2.1 节,第 15 条:

A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.

迂腐地说,C 标准中没有任何内容明确说明代码是 well-defined。我会说它介于可疑行为和未定义行为之间。

  • 严格的别名问题:不是问题。通过指向结构的指针de-reference某些地址就严格的别名而言很好,只要实际存储在该位置的内容是正确的有效类型(C17 6.5 §6 和 §7)。

  • 字符指针转换:有问题。 C 中的任何类型都可以通过使用字符指针逐字节检查。这符合“严格别名”C17 6.5 §7 以及 C17 6.3.2.3 中的指针转换规则,强调我的:

    A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned for the referenced type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer. When a pointer to an object is converted to a pointer to a character type, the result points to the lowest addressed byte of the object. Successive increments of the result, up to the size of the object, yield pointers to the remaining bytes of the object.

    您的指针未指向周围结构类型中寻址最低的字节。您也不使用连续增量。对齐是另一个问题,但我认为这不会成为您的问题。

  • 指针运算:有问题。 指针运算由加法运算符 C17 6.5.6 定义,严格来说只允许对数组类型进行指针运算。其中单个结构变量可以被视为 1 个这样的结构项的数组。为了在指针运算方面理解前面引用的 6.3.2.3,我认为它必须被解释为 sizeof(the_struct) 字节的字符数组。减少指向结构中间的字符指针不在指针算术规则的范围内 - 严格来说它在 §8 下排序“......否则,行为未定义”。

  • 初始结构member/initial公共序列规则:不适用。有一个特殊规则允许我们在结构指针和指针之间进行转换到其 first 元素 (C17 6.7.2.1 §15) 但这不适用于此处。还有一个联合中两个结构的“公共初始序列”的特殊规则,这里也不适用。


这可能是一个更 well-defined 的版本:

dumpverts( (uintptr_t) &(m->verts[0]) );
...
void dumpverts (uintptr_t ptr) 
{
  struct stmesh* themesh = (struct stmesh *)(ptr - offsetof(struct stmesh, verts));

这是简单的整数运算。您在这里唯一关心的是对齐和严格的别名,这应该没问题。使用 uintptr_t 的整数 to/from 指针转换在其他方面很好(impl.defined),C17 6.3.2.3 §5 和 §6.