具有不同定义的类型结构

Anj*_*ulu 5 c sockets struct posix tcp-ip

我在socket编程中遇到过这个:

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 */
};
Run Code Online (Sandbox Code Playgroud)

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

客户端:

connect(sfd,(struct sockaddr *)&caddr,clen; //type casted one
Run Code Online (Sandbox Code Playgroud)

服务器端:

bind(sfd,(struct sockaddr *)&saddr,slen);
accept(sfd,(struct sockaddr *)&caddr,&clen);
Run Code Online (Sandbox Code Playgroud)

这里有不同定义的结构正在被类型化,这是如何影响变量的?

即使我是类型转换,我可以访问这样的变量:

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);
Run Code Online (Sandbox Code Playgroud)

这种类型转换有什么用?即使我已经使用了类型,我如何访问其他结构的成员(这里是addr_in)?我想知道这些操作是如何发生的,并且理解对类型化不同结构的需求.

Nom*_*mal 10

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

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

POSIX.1定义getaddrinfo()有一个程序示例,该程序查找UDP 的IPv4或IPv6套接字地址(struct sockaddr_in以及struct sockaddr_in6分别为类型).如<sys/socket.h>定义中所述struct sockaddr_storage,当套接字类型未知时,类型可用于静态存储.

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

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

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

这种类型转换有什么用?

采用指针参数的函数有两个选项.如果指针参数是类型void *,编译器将很乐意将任何对象指针转换为void *没有警告或投诉.如果我们只想接受某些类型的指针,我们需要指定一种类型.

套接字地址有很多种类,每种套接字地址类型都有自己的结构类型.没有办法告诉编译器接受指向十几种结构之一的指针.因此,在这种情况下,必须将指针强制转换为"通用"类型struct sockaddr.

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

简而言之,当函数接受指向某些类型的指针时,这种类型惩罚很有用,并且您希望确保仅提供这些类型.在我看来,演员表可以提醒开发人员确保使用正确的类型.

如何访问其他类型的成员?

好吧,你做不到.

事实上,每个套接字地址结构类型都有一个公共sa_family_t字段,它被设置为与它定义的套接字地址类型相对应的值.如果使用a sockaddr_in,则值为AF_INET; 如果使用a sockaddr_in6,则值为AF_INET6; if 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至)检查结构的类型.如果它是包含您感兴趣的成员的类型,可以访问它.

例如,要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;
}
Run Code Online (Sandbox Code Playgroud)

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


历史背景:

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

指向结构对象的指针(适当地强制转换)指向其初始成员(或者如果该成员是位字段,则指向它所在的单元),反之亦然.因此,在结构对象中可能存在未命名的孔,但不是在其开始处,以实现适当的对齐.

与后来的C标准(C99和C11)相比,ANSI C放松了关于类型惩罚的规则,允许指针类型之间来回转换而没有问题.特别是3.3.4,

但是,可以保证,指向给定对齐的对象的指针可以转换为指向相同对齐或不太严格对齐的对象的指针,然后再返回; 结果应该等于原始指针.

这意味着在向一个或多个转换套接字地址结构指针时,ANSI C中没有问题struct sockaddr *; 铸件中没有信息丢失.

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


在当今时代,我们需要struct sockaddr稍微区别地定义结构(确切地说),以确保与C标准的兼容性.

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

首先,各个套接字地址结构不需要进行任何更改.(这也意味着向后兼容性没有问题.)例如,在GNU C库中,a struct sockaddr_in和a struct 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 */
};
Run Code Online (Sandbox Code Playgroud)

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

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

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

    }  u;
};
Run Code Online (Sandbox Code Playgroud)

上面让POSIX.1套接字接口在标准C中工作(从ANSI C或ISO C89到C99到C11版本).

你看,ANSI C 3.3.2.3说"如果一个union包含几个共享一个公共初始序列的结构,并且如果union对象当前包含这些结构中的一个,则允许检查它们中任何一个的公共初始部分"随后的标准增加"任何地方都可以看到完整类型的联合声明".标准继续,"如果相应的成员具有一个或多个初始成员序列的兼容类型,则两个结构共享一个共同的初始序列."

在上面,sin_familysin6_family(类型的sa_family_t)成员是这样一个共同的初始部分,并且可以通过其中的任何成员进行检查struct sockaddr.

ANSI C 3.5.2.1表示"指向联合对象的指针,适当地强制转换,指向其每个成员,[..],反之亦然." C标准的后续版本具有相同(或类似的)语言.

这意味着如果您有一个可以使用指向任何struct sockaddr_类型的指针的接口,则可以将其struct sockaddr *用作"通用指针".例如struct sockaddr *sa,如果您有,则可以使用sa->u.sa_in.sin_familysa->u.sa_in6.sin6_family访问公共初始成员(指定所讨论的套接字地址的类型).因为struct sockaddr是联合(或者更确切地说,因为它是以union作为其初始成员的结构),所以您还可以使用((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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

        return bind_inet(sockfd, &(addr->u.sa_in));
Run Code Online (Sandbox Code Playgroud)

        return bind_inet6(sockfd, &(addr->u.sa_in6));
Run Code Online (Sandbox Code Playgroud)

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


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

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

  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;
};
Run Code Online (Sandbox Code Playgroud)

为了递归地遍历这样的树,可以使用例如

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;
}
Run Code Online (Sandbox Code Playgroud)

在那里你提供在一个(或多个)被称为每个节点上的功能preorder,inorder,postorder参数; custom只有当你希望提供一些上下文的功能时才在那里.

请注意node *root:

  • root->type 指所有亚型的联合.

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

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

  • root->type.of.left并且root->type.of.right无论节点的类型如何都可用,并且只在遍历树时使用,而不关心节点的确切类型.

  • root->type.long_data指的是具有类型的union成员struct node_long(但是你应该只尝试访问它root->type.of.data == DATA_LONG).因此,root->type.long_data.value是一个long value成员struct node_long.

  • root->type.double_data指的是具有类型的union成员struct node_double(但是你应该只尝试访问它root->type.of.data == DATA_DOUBLE).因此,root->type.double_data.value是一个double value成员struct node_long.

  • root->type.of.data == root->type.long_data.data == root->type.double_data.data,root->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中明确允许它通过联合中的任何类型访问它们的值.

请注意,上面的遍历函数只是一个例子; 它为深树使用了大量的堆栈,甚至没有尝试检测循环.因此,有许多增强功能可以使其成为"具有库值的"功能.

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

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;
}
Run Code Online (Sandbox Code Playgroud)

它旨在与树遍历功能一起使用.您可以tree使用顺序(从左到右)打印树的值到标准输出

node_traverse(tree, NULL, node_print, NULL, stdout);
Run Code Online (Sandbox Code Playgroud)

希望上面的例子向您展示了足够的想法,但也足以让您小心并仔细思考您正在设计的界面类型.

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

注:2016-17-11重写.

  • C**标准**是C11.NMot ANSI-C,而不是K&R-C.所以,根据标准,**是**UB.编译器可以(并且modrn编译器将)对代码不保证的类型做出假设.我强烈怀疑有人可以与编译器争论接受一厢情愿的想法. (3认同)
  • 你说:_因为结构共享相同的初始成员......,它们的对齐方式是相同的.反例:`struct a1 {char c; 双d; `和`struct a2 {char c; char d; 这两个结构的对齐要求完全不同(几乎肯定需要分别对齐8和1),尽管它们共享"相同的初始成员".这是答案中的一个重要缺陷; 答案并没有完全令人虚弱. (3认同)
  • 我希望那些低估这个答案的人也会*指出他们看到的缺点,而不是仅仅表达他们的蔑视.我不是想在这里受欢迎; 我的意图只是展示一个(我自己的)推理,为什么以及如何在过去使用过的结构OP(在C标准的早期版本中),以及它们如何在C标准中进行调整和适应,如C和POSIX进步了.@Olaf:我很乐意看到你在UB索赔背后的推理.我非常尊重你,看到你放弃这个简单的"相信我,我有很多声誉"的讽刺. (3认同)

小智 3

完美的术语是“类型双关”,而不是称之为类型转换。

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

您可以搜索类型双关并可以获得更多信息。