具有不同定义的类型转换结构

Typecasting structures with different definitions

我在套接字编程中遇到过这个:

struct sockaddr {
    sa_family_t sa_family;
    char        sa_data[14];
}

struct sockaddr_in {
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */
    struct in_addr sin_addr;   /* internet address */
};

这是两种不同类型的结构,这就是我使用它们的方式

客户端:

connect(sfd,(struct sockaddr *)&caddr,clen; //type casted one

服务器端:

bind(sfd,(struct sockaddr *)&saddr,slen);
accept(sfd,(struct sockaddr *)&caddr,&clen);

此处正在对具有不同定义的结构进行类型转换,这对变量有何影响?

即使我进行了类型转换,我也可以像这样访问变量:

printf("File Descriptor : %d\n", fd);
char *p = inet_ntoa(caddr.sin_addr);
unsigned short port_no = ntohs(caddr.sin_port);

printf("Ip address : %s\n", p);
printf("Ephimeral port : %d\n", port_no);

这种类型转换有什么用?即使我已经对其进行了类型转换,我如何访问其他结构的那些成员(addr_in 此处)? 我想知道这些操作是如何发生的,并了解对不同结构进行类型转换的必要性。

请注意,套接字操作不是标准 C,而是由 POSIX.1 标准化,也称为 IEEE 标准。 1003-1。因此,OP 添加的 posix 标签很重要。

特别是 IEEE 标准。 <sys/socket.h> and socket() 的 1003-1 定义要求实现以非常具体的方式运行,无论 C 标准是否声明此类行为实现已定义甚至未定义行为。

POSIX.1定义为getaddrinfo() has an example of a program that looks up an IPv4 or IPv6 socket address (struct sockaddr_in and struct sockaddr_in6 types, respectively) for UDP. As explained in the <sys/socket.h>定义,当socket类型未知时,可以使用struct sockaddr_storage类型进行静态存储。

最初,struct sockaddr 用作不透明类型,以简化套接字接口,同时保持最少的类型检查。问题中显示的表格来自 ANSI C (ISO C89) 时代。由于在后来的 ISO C 标准版本中添加了指针规则,POSIX.1 实现使用的实际结构略有不同; struct sockaddr 现在实际上是一个包含联合的结构。

如果套接字API使用空指针,void *,对于套接字地址结构,将不会进行类型检查。对于通用类型,开发人员必须将他们的套接字地址结构指针转换为 struct sockaddr * 以避免警告(或错误,取决于使用的编译器选项),这足以避免最严重的错误——比如提供例如一个字符串,并想知道为什么它不起作用,即使编译器根本没有抱怨。

通常,这种方法——使用 "generic-ish" 类型而不是特定类型——在 C 中的许多情况下都非常有用。它允许您创建特定于数据的类型,同时保持接口简单,但至少保留一些类型检查。使用精心设计的结构,您可以为任何类型的数据执行通用二叉树结构之类的操作,同时只实现一组函数(与 C 中的 qsort() 函数相比)。因此,稍后我将展示如何在不调用标准 C 中的未定义行为的情况下定义此类 structures/unions。

What is the use of this kind of typecasting?

接受指针参数的函数有两个选项。如果指针参数是 void * 类型,编译器将愉快地将任何对象指针转换为 void * 而不会发出警告或抱怨。如果我们只想接受某些类型的指针,我们需要指定一种类型。

套接字地址有很多种,每种套接字地址类型都有自己的结构类型。无法告诉编译器接受指向可能十几种结构类型之一的指针。因此,必须将指针或 type-punned 转换为 "generic" 类型,在本例中为 struct sockaddr

同样,这种方法通常不会导致标准 C 中的未定义行为,只要结构(特别是 "generic" 类型)以符合 C 标准的方式定义。只是 OP 显示的是历史的,而不是当前的,并且由于严格的别名要求,不能真正按原样在当前 C 中使用。我稍后会解释如何做到这一点。

简而言之,当函数接受指向某些类型的指针并且您希望确保只提供这些类型时,这种类型双关很有用。在我看来,演员阵容提醒开发人员确保他们使用正确的类型。

How can I access members of the other types?

嗯,你不能。

事实是,每个套接字地址结构类型都有一个公共的 sa_family_t 字段,该字段设置为对应于它定义的套接字地址类型的值。如果使用sockaddr_in,则值为AF_INET;如果使用 sockaddr_in6,则值为 AF_INET6;如果 sockaddr_un,则值为 AF_UNIX(或 AF_LOCAL,计算结果与 AF_UNIX 相同),依此类推。

您只能检查这个公共字段,以确定类型。但是,您可以通过 struct sockaddr 类型支持的任何类型来检查它。

例如,如果您有struct sockaddr *foo,您可以使用((struct sockaddr_storage *)foo)->ss_family(甚至((struct sockaddr_in *)foo)->sin_family)来检查结构的类型。如果它是包含您感兴趣的成员的类型,那么您可以访问它。

例如,要return网络字节序中uint32_t对应的IPv4地址(最高字节在前),可以使用

uint32_t ip_address_of(const struct sockaddr *addr, uint32_t none)
{
    /* NULL pointer is invalid. */
    if (!addr)
        return none;

    /* If IPv4 address, return the s_addr member of the sin_addr member. */
    if (((const struct sockaddr_storage *)addr)->ss_family == AF_INET)
        return ((const struct sockaddr_in *)addr)->sin_addr.s_addr;

    /* The pointer did not point to an IPv4 address structure. */
    return none;
}

如果指定了 NULL 指针或指向非 IPv4 套接字地址结构的指针,则第二个参数 none 被 returned。通常(但不是在所有用例中),可以使用对应于广播地址(0U0xFFFFFFFFU)的值。


历史背景:

使用问题中显示的结构不是 ANSI C 中的未定义行为——它们被广泛使用的时代的 C 标准——,因为 3.5.2.1 说

A pointer to a structure object, suitably cast, 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 therefore be unnamed holes within a structure object, but not at its beginning, as necessary to achieve the appropriate alignment.

并且 ANSI C 在类型双关方面比后来的 C 标准(C99 和 C11)放宽了规则,允许在指针类型之间来回转换而不会出现问题。特别是 3.3.4,

It is guaranteed, however, that a pointer to an object of a given alignment may be converted to a pointer to an object of the same alignment or a less strict alignment and back again; the result shall compare equal to the original pointer.

这意味着在将套接字地址结构指针转换为 struct sockaddr * 或从中转换套接字地址结构指针时,在 ANSI C 中没有问题;转换中不会丢失任何信息。

(不同的套接字地址结构可能有不同的对齐要求不是问题。初始成员在任何情况下都可以安全访问,因为指向结构的指针指向初始成员。这主要是一个问题对于希望使用相同代码支持多种不同套接字类型的用户;他们必须为套接字地址结构使用例如联合或动态分配内存。)


在当今时代,我们需要对结构体(struct sockaddr,准确地说)进行一些不同的定义,以确保与 C 标准的兼容性。

请注意,这意味着即使在支持标准 C 的非POSIX系统上,以下方法也是有效的。

首先,不需要对各个套接字地址结构进行更改。 (这也意味着不存在向后兼容性问题。)例如,在 GNU C 库中,struct sockaddr_instruct sockaddr_in6 本质上定义为

struct sockaddr_in {
    sa_family_t     sin_family;    /* address family: AF_INET */
    in_port_t       sin_port;      /* port in network byte order */
    struct in_addr  sin_addr;      /* internet address */
};

struct sockaddr_in6 {
    sa_family_t     sin6_family;   /* address family: AF_INET6 */
    in_port_t       sin6_port;     /* port in network byte order */
    uint32_t        sin6_flowinfo; /* IPv6 flow information */
    struct in6_addr sin6_addr;     /* IPv6 address */
    uint32_t        sin6_scope_id; /* IPv6 scope-id */
};

唯一需要的重要更改是 struct sockaddr 必须包含单个联合(为简单起见,最好是匿名联合,但它需要 C11 或至少使用的 C 编译器支持匿名联合,并且支持当前的 C 标准在 2016 年完全实现):

struct sockaddr {
    union {
        struct sockaddr_in   sa_in;
        struct sockaddr_in6  sa_in6;

        /* ... other sockaddr_ types ... */

    }  u;
};

以上内容让 POSIX.1 套接字接口在标准 C 中工作(从 ANSI C 或 ISO C89 到 C99 到 C11 修订版)。

你看,ANSI C 3.3.2.3 说 "If a union contains several structures that share a common initial sequence, and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them" 后来的标准添加了 "anywhere that a declaration of the completed type of the union is visible" .标准继续,"Two structures share a common initial sequence if corresponding members have compatible types for a sequence of one or more initial members."

上面,sin_familysin6_family成员(sa_family_t类型)是这样一个公共初始部分,可以通过[=28中的任何成员进行检查=].

ANSI C 3.5.2.1 表示 "A pointer to a union object, suitably cast, points to each of its members, [..] and vice versa." C 标准的后续修订版具有相同(或足够相似)的语言。

这意味着如果您有一个可以使用指向任何 struct sockaddr_ 类型的指针的接口,您可以将 struct sockaddr * 用作 "generic pointer"。如果你有,比如 struct sockaddr *sa,你可以使用 sa->u.sa_in.sin_familysa->u.sa_in6.sin6_family 来访问公共初始成员(指定所讨论的套接字地址的类型)。因为 struct sockaddr 是一个联合(或者更确切地说,因为它是一个以联合作为其初始成员的结构),您还可以使用 ((struct sockaddr_in *)sa)->sin_family((struct sockaddr_in6 *)sa)->sin6_family 来访问族类型。因为家庭是共同的初始成员,所以您可以使用任何类型来做到这一点;请记住,只有当家族与成员所属的类型相匹配时,其他成员才能访问。

对于当前的 C,您可以使联合匿名(通过在末尾删除 u 名称),在这种情况下,上面的内容将是 sa->sa_in.sin_familysa->sa_in6.sin_family

至于这个基于联合的 struct sockaddr 在另一边是如何工作的,让我们检查一下 bind() 的可能实现:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
{
    /* Clearly invalid sockfd? */
    if (sockfd == -1) {
        errno = EBADF;
        return -1;
    }

    /* Clearly invalid addr or addrlen? */
    if (addr == NULL || addrlen == 0) {
        errno = EINVAL;
        return -1;
    }

    switch (addr->u.sin_family) {

    case AF_INET:
        if (addrlen != sizeof (struct sockaddr_in)) {
            errno = EINVAL;
            return -1;
        }
        return bind_inet(sockfd, (struct sockaddr_in *)addr);

    case AF_INET6:
        if (addrlen != sizeof (struct sockaddr_in6)) {
            errno = EINVAL;
            return -1;
        }
        return bind_inet6(sockfd, (struct sockaddr_in6 *)addr);

    default:
        errno = EINVAL;
        return -1;
    }
}

依赖于套接字类型的绑定调用可以等效地写为

        return bind_inet(sockfd, &(addr->u.sa_in));

        return bind_inet6(sockfd, &(addr->u.sa_in6));

即获取联合成员的地址,而不是仅仅将指针转换为整个联合。


在设计自己的多子类型结构时,要真正记住四件事,以保持标准 C 兼容:

  1. 使用包含所有子类型作为成员的联合类型作为 "generic" 类型。

  2. 联合一次只包含一个子类型;用于分配给它的那个。

  3. 可选地,添加一个子类型以访问具有简单名称的类型(可能还有所有子类型共有的其他成员),并在文档中一致地使用它。

  4. 始终先检查与实际类型对应的成员。

例如,如果您正在构建某种抽象二叉树——也许是一个计算器? -- 每个节点存储不同类型的数据,你可以使用

/* Our "generic" type is 'node'. */
typedef  struct node  node;

typedef enum {
    DATA_NONE = 0,
    DATA_LONG,
    DATA_DOUBLE,
} node_data;

/* The minimal node type; no data payload. */
struct node_minimal {
    node      *left;
    node      *right;
    node_data  data;
};

struct node_long {
    node      *left;
    node      *right;
    node_data  data;   /* = DATA_LONG */
    long       value;
};

struct node_double {
    node      *left;
    node      *right;
    node_data  data;   /* = DATA_DOUBLE */
    double     value;
};

/* The generic type. */
struct node {
    union {
        struct node_minimal  of;
        struct node_long     long_data;
        struct node_double   double_data;
    } type;
};

要递归地遍历这样一棵树,可以使用例如

int node_traverse(const node *root,
                  int (*preorder)(const node *, void *),
                  int (*inorder)(const node *, void *),
                  int (*postorder)(const node *, void *),
                  void *custom)
{
    int retval;

    if (!root)
        return 0;

    if (preorder) {
        retval = preorder(root, custom);
        if (retval)
            return retval;
    }

    if (root->type.of.left) {
        retval = node_traverse(root->type.of.left, preorder, inorder, postorder, custom);
        if (retval)
            return retval;
    }

    if (inorder) {
        retval = inorder(root, custom);
        if (retval)
            return retval;
    }

    if (root->type.of.right) {
        retval = node_traverse(root->type.of.right, preorder, inorder, postorder, custom);
        if (retval)
            return retval;
    }

    if (postorder) {
        retval = postorder(root, custom);
        if (retval)
            return retval;
    }

    return 0;
}

您在 preorderinorderpostorder 参数中的一个(或多个)参数中提供在每个节点上调用的函数; custom 只有当您希望为函数提供一些上下文时才会出现。

注意 node *root:

  • root->type指所有子类型的并集

  • root->type.of指的是struct node_minimal类型的union成员;我这样命名只是为了好玩。目的是您使用它来访问未知类型的节点。

  • root->type.of.data 仅取决于节点实际使用的类型,DATA_ 枚举之一。

  • root->type.of.leftroot->type.of.right也可以不管节点的类型,当你只是遍历树而不关心具体类型时使用节点数。

  • root->type.long_data 指的是具有类型 struct node_long 的联合成员(但您应该仅在 root->type.of.data == DATA_LONG 时尝试访问它)。因此,root->type.long_data.valuestruct node_longlong value 成员。

  • root->type.double_data 指的是具有类型 struct node_double 的联合成员(但您应该仅在 root->type.of.data == DATA_DOUBLE 时尝试访问它)。因此,root->type.double_data.valuestruct node_longdouble value 成员。

  • root->type.of.data == root->type.long_data.data == root->type.double_data.dataroot->type.of.left == root->type.long_data.left == root->type.double_data.leftroot->type.of.right == root->type.long_data.right == root->type.double_data.right,因为这些都是普通的初始成员,在C中明确允许通过任何方式访问它们的值联合中的类型。

注意上面的遍历函数只是一个例子;它为深树使用了大量堆栈,甚至不尝试检测循环。因此,有很多增强功能可以使其成为 "library-worthy" 函数。

要打印节点的值,例如可以使用

int node_print(const node *n, void *out)
{
    if (!out || !n) {
        errno = EINVAL;
        return -1;
    }

    if (n->type.of.data == DATA_DOUBLE)
        return fprintf((FILE *)out, "%.16g", n->type.double_data.value);

    if (n->type.of.data == DATA_LONG)
        return fprintf((FILE *)out, "%lu", n->type.long_data.value);

    /* Unknown type, so ... return -1 with errno == 0, I guess? */
    errno = 0;
    return -1;
}

这是为了配合树遍历功能而设计的。您可以使用

按顺序(从左到右)将树 tree 的值打印到标准输出
node_traverse(tree, NULL, node_print, NULL, stdout);

希望上面的例子足以给你想法,但也足以让你仔细思考你正在设计的界面类型。

如果您认为(很多人认为)我对 C 标准的解读不正确,请指出您认为与上述内容相矛盾的部分。我的观点不是为了流行,但我确实希望在我错的时候得到纠正。

注:2016-17-11 重写。

完美的术语是 "type punning" 而不是将其称为类型转换。

sockaddr_in 用于基于 IP 的通信,我们在其中指定协议类型、IP 地址、端口等,而 sockaddr 是套接字操作中使用的通用结构。 bind() 使用 sockaddr 因此需要类型双关。

您可以搜索类型双关,获取更多信息。