【问题标题】:Is it legal to type-cast pointers of different struct types (e.g. struct sockaddr * to struct sockaddr_in6 *)?类型转换不同结构类型的指针是否合法(例如 struct sockaddr * 到 struct sockaddr_in6 *)?
【发布时间】:2016-09-10 11:07:19
【问题描述】:

这是一个在 struct shapestruct rectanglestruct triangle 类型的指针之间进行类型转换的程序。

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

enum { RECTANGLE, TRIANGLE, MAX };

struct shape {
    int type;
};

struct rectangle {
    int type;
    int x;
    int y;
};

struct triangle {
    int type;
    int x;
    int y;
    int z;
};

struct shape *get_random_shape()
{
    int type = rand() % MAX;
    if (type == RECTANGLE) {
        struct rectangle *r = malloc(sizeof (struct rectangle));
        r->type = type;
        r->x = rand() % 10 + 1;
        r->y = rand() % 10 + 1;
        return (struct shape *) r;
    } else if (type == TRIANGLE) {
        struct triangle *t = malloc(sizeof (struct triangle));
        t->type = type;
        t->x = rand() % 10 + 1;
        t->y = rand() % 10 + 1;
        t->z = rand() % 10 + 1;
        return (struct shape *) t;
    } else {
        return NULL;
    }
}

int main()
{
    srand(time(NULL));

    struct shape *s = get_random_shape();

    if (s->type == RECTANGLE) {
        struct rectangle *r = (struct rectangle *) s;
        printf("perimeter of rectangle: %d\n", r->x + r->y);
    } else if (s->type == TRIANGLE) {
        struct triangle *t = (struct triangle *) s;
        printf("perimeter of triangle: %d\n", t->x + t->y + t->z);
    } else {
        printf("unknown shape\n");
    }

    return 0;
}

这是输出。

$ gcc -std=c99 -Wall -Wextra -pedantic main.c
$ ./a.out 
perimeter of triangle: 22
$ ./a.out 
perimeter of triangle: 24
$ ./a.out 
perimeter of rectangle: 8

您可以在上面看到程序编译并运行时没有任何警告。我试图了解将struct shape 的指针类型转换为struct rectangle 是否有效,反之亦然,即使两个结构的大小不同。

如果您的回答是这是无效的,那么请考虑网络编程书籍根据套接字系列(AF_INET 与 AF_INET6)在struct sockaddr *struct sockaddr_in *struct sockaddr_in6 * 指针之间进行常规类型转换,然后解释为什么这种类型转换在struct sockaddr * 的情况下是可以的,但在上述struct shape * 的情况下却不行。这是使用struct sockaddr * 进行类型转换的示例。

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int main()
{
    struct addrinfo *ai;

    if (getaddrinfo("localhost", "http", NULL, &ai) != 0) {
        printf("error\n");
        return EXIT_FAILURE;
    }

    if (ai->ai_family == AF_INET) {
        struct sockaddr_in *addr = (struct sockaddr_in *) ai->ai_addr;
        printf("IPv4 port: %d\n", addr->sin_port);
    } else if (ai->ai_family == AF_INET6) {
        struct sockaddr_in6 *addr = (struct sockaddr_in6 *) ai->ai_addr;
        printf("IPv6 port: %d\n", addr->sin6_port);
    }

    return 0;
}

此代码也可以正常编译和运行。此外,这是套接字编程书籍中推荐的编写此类程序的方法。

$ gcc -std=c99 -D_POSIX_SOURCE -Wall -Wextra -pedantic foo.c
$ ./a.out 
IPv6 port: 20480

【问题讨论】:

  • 您正在将子指针转换为父指针,然后将其向下转换回精确对应的子指针。您要求编译器信任您(通过使用强制转换)。所以,没有警告。
  • IIRC,这仅适用于unions。
  • @Lone Learner:虽然您在这里所做的可能是有效的(由于“通用初始序列”规则),但在一般情况下,只是因为“程序编译并运行时没有任何警告”不在任何方式都表明它在某种程度上是“有效的”。 “编译并运行良好”绝对与您的代码在 C 世界中的有效性无关。
  • @AnT 通用初始序列规则不适用,因为结构不是联合的成员。目前尚不清楚这是否是严格的别名违规:有人认为r-&gt;type 意味着(*r).type*r 违反了规则;其他人(包括我)说这不是因为r-&gt;type 是唯一可以访问的东西,它是int 类型并读取int

标签: c pointers struct


【解决方案1】:

对不同结构类型的指针进行类型转换是否合法(例如 struct sockaddr * 到 struct sockaddr_in6 *)?

是的。 C 明确规定:

指向对象类型的指针可以转换为指向不同对象类型的指针。如果结果指针未正确对齐引用的类型,则行为未定义。否则,当再次转换回来时,结果将等于原始指针。

(C2011,6.3.2.3/7)

正如其他答案所指出的,问题不是演员阵容本身,而是你对结果的处理方式。这归结为严格的别名规则:

对象的存储值只能由左值访问 具有以下类型之一的表达式:

  • 与对象的有效类型兼容的类型,

[...加上其他几种在这种情况下不能适用的替代方案...]

(C2011, 6.5/7;已添加重点)

因此,主要问题是struct sockaddr * 指向的对象的有效类型是什么?重要的是要理解我们无法从getaddrinfo() 的声明中分辨出来,也不能从struct addrinfo 的声明中看出。特别是,没有理由假设有效类型是struct sockaddr

事实上,鉴于您询问的演员表是访问地址详细信息的标准和预期方法,完全有理由假设 getaddrinfo() 通过确保有效类型是由关联的ai_family 代码。然后相应的转换产生一个与地址信息的有效类型匹配的指针。在这种情况下,通过转换获得的指针访问地址信息是没有问题的。

我观察到支持上述观点,假设所讨论的指针指向动态分配的对象是合理的。此类对象的有效类型取决于上次设置其存储值的方式(C2011,6.5/6)。 getaddrinfo() 不仅合理而且很可能会以一种为其提供所需有效类型的方式设置该值。例如,与您的形状示例相同的代码行即可。

最终,将struct sockaddr * 与指向地址族特定结构的指针之间进行转换是预期用途,并且没有理由假设提供getaddrinfo() 的环境在实践中会允许这些行为值得怀疑。如果有必要,POSIX(指定函数的人)可以合并一个允许强制转换的特殊规则。但是在这种情况下不需要这样的规则,尽管 POSIX 让你相信它。

【讨论】:

    【解决方案2】:

    如果显式类型转换被删除,编译器会忠实地诊断错误

    struct rectangle *r = (struct rectangle *) s;
    

    或来自

    struct triangle *t = (struct triangle *) s;
    

    在这种情况下,允许显式类型转换工作,因为这是标准所要求的。实际上,通过在这两个语句中使用显式类型转换,您可以有效地指示编译器“闭嘴,我知道我在做什么”。

    更有趣的是为什么main() 函数在运行时工作,一旦你用大棒将编译器提交给它,它就会允许转换。

    代码有效,因为所有三个structs 的第一个成员是相同的类型。 struct 的地址与其第一个成员的地址相同,只是类型不同(即指向 struct rectangle 的指针与指向 int 的指针具有不同的类型)。因此(如果我们忽略不同的类型),测试s == &amp;(s-&gt;type) 将为真。使用类型转换可以解决这个问题,所以(int *)s == &amp;s-&gt;type

    一旦您的代码完成了该测试,它就会对s 进行显式类型转换。碰巧,在声明中

    struct rectangle *r = (struct rectangle *) s;
    

    您的代码已确保s 实际上是(动态分配的)struct rectangle 的地址。因此r 的后续使用是有效的。同样在else if 块中,使用struct triangle

    问题是,如果你犯了错误,比如

    if (s->type == RECTANGLE)
    {
        struct triangle *t = (struct triangle *) s;
        printf("perimeter of triangle: %d\n", t->x + t->y + t->z);
    }
    

    (即使用struct rectangle 就好像它是struct triangle)然后编译器仍将忠实地允许类型转换(如上所述)。但是,由于s 实际上不是struct triangle 的地址,因此行为现在是未定义的。特别是,访问t-&gt;z 会访问一个不存在的成员。

    【讨论】:

    • 我知道程序员必须跟踪正确的类型,并确保只有指向 struct triangle 对象的指针是这样的类型转换。这几乎也是我们在套接字编程中所做的。请参阅beej.us/guide/bgnet/output/print/bgnet_A4.pdf(第 10 页)-“这是重要的一点:指向 struct sockaddr_in 的指针可以转换为指向 struct sockaddr 的指针,反之亦然。”那么这个建议好吗?这种套接字编程中的常见做法安全吗?
    • 如果类型转换有效(即,如果程序员正确地保持“正确类型的跟踪”)是安全的。否则它是不安全的(即未定义的行为)。
    【解决方案3】:

    在 Berkeley 套接字库的特定情况下,POSIX 标准保证您可以将指向 struct sockaddr_storage 的指针转换为指向任何类型套接字的指针,并且标识套接字类型的字段将正确映射。

    具体来说,the POSIX standard 指定了struct sockaddr_storage

    当指向 sockaddr_storage 结构的指针被转换为指向 sockaddr 结构,sockaddr_storagess_family 字段 结构应映射到sockaddrsa_family 字段 结构体。当指向 sockaddr_storage 结构的指针被转换为 指向特定协议地址结构的指针,ss_family 字段 应映射到该结构的类型为sa_family_t 的字段 并标识协议的地址族。

    It also says of struct sockaddr_in,“指向此类型的指针应由应用程序转换为 struct sockaddr * 以用于套接字函数。” bind()connect() 等的接口只有在库查找它得到的const struct sockaddr* 并确定它指向的套接字类型时才能工作。

    一个给定的编译器可能需要魔法来实现它,但这个库必须为你做这件事。

    【讨论】:

    • 能否请您添加一些对 POSIX 标准的引用(部分编号或短语),以解释此行为保证有效?
    • @LoneLearner 完成,并做了一个更正:标准实际上保证了 sockaddr_storage 结构。
    【解决方案4】:

    您的问题有几个术语混淆。

    首先,仅仅因为你的程序以某种方式“编译并运行而没有任何警告”,甚至产生了你所期望的结果,但这并不意味着你在代码中所做的事情在某种程度上是“有效的”。

    其次,您似乎在询问演员表本身的有效性。实际上,演员阵容本身是无关紧要的。 C 中有很多东西可以相互“类型转换”。但是,该语言不保证您可以使用此类转换的结果做什么。强制转换本身可能完全有效,但您对结果应用的进一步操作可能非常无效。

    第三,这显然是您的问题的真正含义:在指向共享公共初始子序列的不同结构类型的指针之间进行转换,然后通过结果指针从该公共子序列中 访问 成员。这里的问题不是演员阵容,而是随后的访问。答案是:不,语言没有将其定义为有效的技术。该语言允许您检查联合在一个公共联合中的不同结构类型的公共初始子序列,但如果没有公共联合,则不允许这样做。

    至于在struct sockaddr *struct sockaddr_in *struct sockaddr_in6 * 之间进行转换的流行技术 - 这些只是与 C 语言无关的技巧。它们只是在实践中起作用,但就 C 语言而言,该技术是无效的。

    【讨论】:

    • 很好的答案,所以演员表是合法的,因为structs 的第一个成员都是相同的类型,但你不能取消引用这样的指针,不是吗?
    • 是的,这就是重点:允许实现使用“特定于实现的细节”,也就是肮脏的黑客。其中之一是transparent_unions,它确实允许将结构与不是联合的联合组合在一起。
    • @AnT 我现在有一个关于这个的后续问题stackoverflow.com/q/39432774/1175080
    【解决方案5】:

    实际上并不能保证有效。如果编译器看到具有三种类型的联合声明,它保证工作的;编译器看到声明就足够了。在这种情况下,访问结构的公共前导元素的代码很好。显然,最重要的共同元素是“类型”成员。

    所以如果你已经声明了一个结构形状、矩形和三角形的联合,你可以把一个确实指向三个结构之一的指针,转换指针,访问类型字段,然后从那里开始。

    【讨论】:

    • 那你觉得bgnet的这个说法有误吗? And this is the important bit: a pointer to a struct sockaddr_in can be cast to a pointer to a struct sockaddr and vice-versa.您对 bgnet 中的这种说法以及套接字编程中的这种做事方式有何评论?
    • union 替代方案在实践中根本无法保证有效,即使在理论上它是否提供任何保证也值得商榷。 non-normative 脚注提供了标准的唯一指示,即联合体应该允许这种别名,这似乎与严格别名规则的 normative 规定相冲突。此外,甚至脚注是否对不通过联合对象进行的访问做出任何声明也是有争议的。
    • @JohnBollinger:如果这种可见性不是为了迫使编译器通过任何涉及的类型识别 CIS 访问,那么关于 complete 联合类型可见的规则将毫无意义.任何不能支持 CIS 访问的方言都应该被视为 1990 年代流行的语言的不兼容分支。
    • @supercat,我认为你是指C2011 6.5.2.3/6,提供对属于联合成员公共初始序列的对象的访问,而不考虑联合实际包含的成员。尽管这肯定对存储布局有影响,但它并不与严格的别名规则 (6.5/7) 相矛盾。您可能会争辩说,通过 -&gt;. 运算符访问结构成员并不构成访问结构本身,因此可以避开 SAR,但您不​​能简单地忽略 SAR。
    • @JohnBollinger:在添加指针别名规则之前,该规则及其对结构指针的影响是 C 的基本部分。该标准的既定目的是描述一种现有的语言,而不是定义一种全新的语言。 C99 语言的目的是说 CIS 保证仅在完整的联合类型可见时才成立,显然是为了限制 CIS 保证的范围,但我认为没有理由相信大多数投票支持别名规则将它们理解为破坏 CIS 保证。
    【解决方案6】:

    但这不适用于任何语言。同样在 C++ 中,您应该在基类中包含所有变量并在基类中声明虚函数。 与其移动到 shape 而不是 rectangle ,不如移动到 void* 而不是 rectangle 那么这是一个面向对象的范例。 Hinerhitance、polimorphimsum 和其他正是将语言定向到对象的原因。要在 C 中使用对象,您应该硬编码。但很值得。我认为程序的平均复杂性并不能证明转向 C++ 是合理的。法拉利和卡车是有区别的。至少你不必费力地工作,C 很有趣。 在你的地方,我会这样做:

    typedef enum shape_type{
    circle,
    rectangle,
    triangle,
    //...
    }S_type;
    
    typedef struct shape
    {
       S_type stype;
       int ar_par[4];//default allocated parameters number
       int* p_par; //to default it is going to contain the ar_par address
                   //and you are going to change it case you needs more  parameters. You save a malloc more
       int n;//count of parameters
       int (*get_perimeter) (struct shape *);//you can also typedef them
       int (*get_area)(struct shape*);
    }*Shape_ptr,Shape;
    

    比这样编码

    Shape_ptr new_rectangle(int a, int b)
    {
       Shape_ptr res=malloc(sizeof(Shape));
       res->stype=rectangle;
       res->p_par=res->ar_par;//no need to allocate anything
       *res->p_par[0]=a;*res->p_par[1]=b;
       res->n=2;
       res->get_perimeter=get_rectangle_perimeter;
       res->get_area=get_rectangle_area;
    
    }
    int get_rectangle_perimeter(Shape_ptr s)
    {
       return s->p_par[0]<<1 + s->p_par[1]<<1; //or multiply by two;
    }
    main() 
    {
        Shape_ptr shp =get_random_shape() ; //this function is going to call     new_rectangle
        printf ("shap area is:%d\n",(*shp->get_area)(shp);
    }
    

    等等...这就是您在 C 中处理对象的方式。面向对象的程序包含一些范式,这些范式在大型重型程序中简化了程序员的生活

    【讨论】:

    • 您似乎在寻找答案,但从未提供答案。
    • 答案到底是什么? C允许将每个指针转换为另一种指针。它只是保存地址。如果您将其转换为 double*,然后再次转换为 struct rectangle*,该代码也将起作用。如果他想保留几个无用的结构,最好强制转换为 void*。否则更好地改进程序的结构。
    • 我的意思是这里的问题是概念性的,而不是它是否有效。他正在寻找用 C 无法实现的东西。它得到了一个结果,但它的概念完全错误。
    • @jurhas 但这意味着几乎所有依赖此行为的套接字程序都是错误的!那么你真的是说我们做socket编程的方式不正确吗?例如,参见bgnet.pdf 的第 10 页,它说:“这是重要的一点:指向 struct sockaddr_in 的指针可以转换为指向 struct sockaddr 的指针,反之亦然。”
    • C 是一种“中级”程序语言。正是在这些目的中,您可以看到他的“低级”。当你声明一个带尖的指针时,你只是在告诉计算机“好的,内存中的这个随机地址是一个 int。所以如果我告诉你要延迟取 4 个字节并将它们视为 int。如果我告诉你移动下一个插槽移动 4"。该程序不会控制任何事情,他相信你蒙着眼睛。在您所做的程序中,您可以取回一个指针并将其转换为 int。他只是认为你得到了一个 int 数组。 (其实他不知道下一个槽是不是也是你的)
    猜你喜欢
    • 1970-01-01
    • 2015-11-30
    • 2017-01-18
    • 1970-01-01
    • 2011-06-16
    • 1970-01-01
    • 2016-05-05
    • 2020-10-01
    相关资源
    最近更新 更多