不同结构类型之间指针的强制类型转换是否合法?(例如,将struct sockaddr* 转换为struct sockaddr_in6*)?

6
这里有一个程序,它在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 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

1
你正在将一个子指针转换为父指针,然后再将其向下转换回完全对应的子指针。通过使用强制转换,你要求编译器相信你。因此,没有警告。 - blackpen
如果我没记错的话,这只允许使用union - edmz
1
可能是 C语言中的结构体和强制类型转换 的重复问题。 - edmz
1
@Lone Learner:虽然你在这里所做的可能是有效的(由于“公共初始序列”规则),但通常情况下,仅仅因为“程序编译并运行没有任何警告”并不意味着它在某种程度上是“有效”的。在C世界中,“编译和运行良好”绝对不能说明你的代码的有效性。 - AnT stands with Russia
1
@AnT 通用的初始序列规则不适用于结构体不是联合体成员的情况。目前还不清楚这是否属于严格别名违规:有些人认为 r->type 意味着 (*r).type,而 *r 违反了规则;另一些人(包括我)认为它不是因为 r->type 是唯一访问的内容,它是类型为 int 的内容并且只读取了一个 int - M.M
显示剩余7条评论
6个回答

3
可以。C语言明确提供了这个功能:
“可以将指向对象类型的指针转换为指向不同对象类型的指针。如果所得指针未正确对齐以适用于引用的类型,则行为是未定义的。否则,在再次转换时,结果应与原始指针相等。”(C2011,6.3.2.3/7)
正如其他答案所指出的那样,问题不在于转换本身,而在于您对结果的处理。这就涉及到严格别名规则:
“一个对象只能通过具有以下类型之一的lvalue表达式访问其存储值:
- 与对象的有效类型兼容的类型, - [...加上几个在此情况下无法应用的其他选择...]”(C2011,6.5/7;强调添加)
因此,主要问题是struct sockaddr *指向的对象的有效类型是什么?重要的是要理解,在此声明getaddrinfo()struct addrinfo的情况下,我们无法确定。特别是,没有理由认为有效类型是struct sockaddr
事实上,考虑到你所问的转换是访问地址详细信息的标准和预期方法,因此有充分的理由认为getaddrinfo()通过确保有效类型与相关的ai_family代码指示的类型相匹配来支持它。然后,相应的转换产生与地址信息的有效类型匹配的指针。在这种情况下,通过转换获得的指针访问地址信息并没有任何问题。
我观察到上面的理由是合理的,即指针指向动态分配的对象。这样一个对象的有效类型取决于其存储值最后设置的方式(C2011,6.5/6)。很可能getaddrinfo()会以使其具有所需有效类型的方式设置该值。例如,沿着与您的形状示例相同的线路的代码将这样做。
最终,将struct sockaddr *强制转换为特定于地址族的结构体的指针,并从中转换出来是预期的用法,在实践中没有理由认为提供getaddrinfo()的环境会允许这些行为是可疑的。如果必要,POSIX(由谁指定函数)可以纳入一个特殊规则允许这些转换。但在这种情况下不需要这样的规则,尽管POSIX让你信任这一点。

3
编译器会在去除显式类型转换后忠实地检测到错误。
struct rectangle *r = (struct rectangle *) s;

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

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

更有趣的是,为什么main()函数可以在运行时工作,一旦你迫使编译器屈服,以便它允许转换。

代码之所以起作用,是因为所有三个struct的第一个成员都是相同类型的。一个struct的地址等于其第一个成员的地址,除了类型不同(即指向struct rectangle的指针与指向int的指针具有不同的类型)。因此,(如果我们忽略不同的类型),测试s == &(s->type)将为真。使用类型转换处理这个问题,因此(int *)s == &s->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->z将访问不存在的成员。


我理解程序员必须跟踪正确的类型,并确保只有指向“struct triangle”对象的指针被强制转换为此类。这在套接字编程中也是我们所做的。请参见http://beej.us/guide/bgnet/output/print/bgnet_A4.pdf(第10页)-“这是重要的一点:可以将指向struct sockaddr_in的指针强制转换为指向struct sockaddr的指针,反之亦然。”那么这个建议好吗?在套接字编程中,这是常见的安全实践吗? - Lone Learner
如果类型转换有效(即程序员正确地跟踪了“正确类型”),则是安全的。否则,它是不安全的(即未定义行为)。 - Peter

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

当将指向 sockaddr_storage 结构的指针强制转换为指向 sockaddr 结构的指针时,sockaddr_storage 结构的 ss_family 字段将映射到 sockaddr 结构的 sa_family 字段。当将指向 sockaddr_storage 结构的指针强制转换为指向协议特定地址结构的指针时,ss_family 字段将映射到该结构的一个类型为 sa_family_t 并标识协议地址族的字段。

它还指出,对于struct sockaddr_in,“应用程序必须将此类型的指针转换为struct sockaddr *以便与套接字函数一起使用。” bind()connect()等函数的接口只有在库查找其收到的const struct sockaddr*并确定其指向的套接字类型时才能正常工作。
一个特定的编译器可能需要魔法来实现这一点,但是这个库必须为您完成这项工作。

请问您能否在POSIX标准中添加一些参考资料(部分编号或短语),以说明该行为是保证可行的? - Lone Learner
@LoneLearner 已完成,并进行了一处更正:标准实际上保证了关于 sockaddr_storage 结构的内容。 - Davislor

1
你的问题存在几个术语混淆。
首先,仅仅因为你的程序“编译和运行没有任何警告”,甚至产生了你期望的结果,这并不意味着你在代码中所做的事情是“有效的”。
其次,看起来你正在询问转换本身的有效性。实际上,转换本身并不重要。在C语言中,有很多东西可以相互“类型转换”。然而,语言并不保证您可以对此类转换的结果进行什么操作。转换本身可能是完全有效的,但您对结果应用的进一步操作可能是非常无效的。
第三,这显然是你真正关心的问题:在不同结构类型之间进行指针转换,这些结构类型共享一个公共初始子序列,然后通过结果指针访问该公共子序列的成员。这里的问题不是转换,而是随后的访问。答案是:不,语言没有将其定义为有效技术。语言允许您检查联合中统一的不同结构类型的公共初始子序列,但在没有公共联合的情况下,这是不允许的。

关于在struct sockaddr *struct sockaddr_in *struct sockaddr_in6 *之间使用强制类型转换的流行技巧 - 这些只是一些与C语言无关的黑科技。它们在实践中可以工作,但就C语言而言,这种技术是无效的。


很好的回答,所以类型转换是合法的,因为结构体的第一个成员都是相同类型的,但是你不能对这样的指针进行解引用,不是吗? - David Ranieri
是的,这就是重点:实现允许使用“特定于实现的细节”,也就是肮脏的技巧。其中之一是transparent_union,它确实允许将结构体与不是联合体的联合体分组在一起。 - edmz
@AnT 我现在有一个关于这个问题的后续问题,可以在https://dev59.com/0PD0s4cB2Jgan1znRgma上回答吗? - Lone Learner

0

实际上并不能保证它能正常工作。如果编译器看到一个具有三种类型的联合体的声明,那么它是可以保证正常工作的;只要编译器看到了声明就足够了。在这种情况下,访问结构体公共前导元素的代码是可以的。显然,最重要的公共元素是"type"成员。

因此,如果您声明了一个形状、矩形和三角形的结构体联合体,您可以获取一个指向其中三个结构体之一的指针,进行强制类型转换,访问"type"字段,然后从那里开始操作。


1
联合体替代方案在实践中并不保证能够正常工作,甚至有人质疑它是否在理论上提供任何保证。标准中唯一提到联合体应该允许这种别名的非规范性脚注似乎与严格别名规则的规范性规定相冲突。此外,即使是脚注也存在争议,是否对未经过联合体对象的访问做出任何声明。 - John Bollinger
@JohnBollinger:关于完整联合类型可见的规则,如果没有意图强制编译器通过任何涉及的类型识别CIS访问,则该规则将毫无意义。任何不支持CIS访问的方言都应被视为与20世纪90年代流行的语言不兼容的分支。 - supercat
@supercat,我认为你指的是C2011 6.5.2.3/6,它允许访问属于联合成员公共初始序列的对象,而不考虑联合体实际包含哪个成员。尽管这确实对存储布局有影响,但它并不违反严格别名规则(6.5/7)。你可以认为通过->.运算符访问结构体成员并不构成访问结构体本身,因此可以逃避SAR,但你不能简单地忽略SAR。 - John Bollinger
1
@JohnBollinger:在指针别名规则被添加之前,该规则及其对结构指针的影响是C语言的基本部分。标准的声明目的是描述一种现有的语言,而不是定义一种根本新的语言。C99语言规定CIS保证仅在完整联合类型可见时才有效,这显然旨在限制CIS保证的范围,但我没有看到任何理由相信投票支持别名规则的委员会成员中的大多数人将其理解为削弱CIS保证。 - supercat
@JohnBollinger:虽然C89的作者们没有觉得有必要明确说明,但标准并不试图禁止实现可能表现出的每一种不合理的方式。对于一个目标平台和应用领域来说合理的行为,在另一个领域可能是不合理的,但高质量的实现将努力以适合其预期使用的方式进行行为。我猜作者认为这些原则应该是不言自明的,但一些现代编译器的编写者似乎已经完全失去了这些原则。如果90%的实现在某种情况下定义了一种行为... - supercat
显示剩余10条评论

-4

但这在任何语言中都不起作用。在C++中,您应该将所有变量包含在基类中,并在基类中声明虚函数。 与其转到形状然后转到矩形,不如转到void*然后转到矩形 然后这是一种面向对象的范例。继承,多态和其他正是将语言定位于对象的东西。要在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语言中使用对象的方式。面向对象的程序包含一些范例,可以在大型复杂程序中简化程序员的工作。


你似乎绕着答案打转,却从未给出一个。 - edmz
答案究竟是什么?C语言允许把任何指针转换成任何其他类型的指针。它只是保存地址。如果您将其转换为double,然后再次转换为struct rectangle,代码也可以正常工作。如果想要保留一些无用的结构体,则最好转换为void*。否则最好改进程序的结构。 - jurhas
我的意思是这里的问题是概念性的,而不是它是否能够工作或者工作得更少。他正在寻找一些用C语言无法实现的东西。虽然它可以得到一个结果,但是这个概念是完全错误的。 - jurhas
@jurhas 但这意味着几乎所有依赖此行为的套接字程序都是错误的!所以你真的认为我们进行套接字编程的方式是不正确的吗?例如,请参见 bgnet.pdf 的第10页,其中写道:“而这是重要的一点:可以将指向结构体 sockaddr_in 的指针强制转换为指向结构体 sockaddr 的指针,反之亦然。” - Lone Learner
C是一种“中级”程序语言。正是在这个目的上,你可以看出它的“低级性”。当你声明一个类型指针时,你只是告诉计算机:“好的,这个随机地址在内存中是int。所以如果我告诉你去引用它,请取4个字节并将它们视为int。如果我告诉你移动到下一个插槽,请移动4个字节。”程序不会控制任何东西,它完全盲信你。在你编写的程序中,你可以只获取一个指针并将其转换为int。它只认为你得到了一个int数组。(事实上,它不知道下一个插槽是否也属于你) - jurhas
显示剩余2条评论

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