返回介绍

5. 结构

发布于 2024-10-12 21:58:09 字数 5889 浏览 0 评论 0 收藏 0

将构成逻辑单元的不同数据项组合成复合体。

struct <tag> { 
	type member;
};
typedef struct [tag] {
    ...
} name_t;

标签总是和 struct 关键字一起使用,表达完整类型。区分同名标识符。用 typedef 定义类型别名,可与 tag 同名,或省略 tag。

如果类型相同,可以直接 type member1, member2; ,空结构 struct{} 长度(sizeof)为 0。使用 . 访问成员,指针使用 -> 访问。

int main (void)
{
    typedef struct user {
        unsigned int  id;
        char          name[30];
        unsigned char age;
    } user_t;

    struct user u = { 
        .id = 1, 
        .name = "Tom", 
        .age = 10 
    };
    
    user_t *p = &u;
    assert(p->age == u.age);

    return 0;
}

在结构里存储字符串,注意 char[x]char* 的差别。

struct d1 {
    char *s;     // 指针
};

struct d2 {
    char s[30];  // 嵌入数组。
};

assert(sizeof(struct d1) == 8);
assert(sizeof(struct d2) == 30);

赋值,或作为参数时,完整复制。

初始化

顺序初始化,或指定成员。

顺序初始化不要求全部成员赋值,未初始化值为零。如在 .member 后使用顺序初始化,则指向该 .member 后字段。(慎用!)强烈建议全部使用 .member 初始化方式。

struct user {
    int  id;
    char name[30];
    enum { WOMEN, MAN } sex;
};

struct user u1 = { 100, "user1" };
assert(u1.sex == WOMEN);

struct user u2 = { .name = "user1", MAN };
assert(u2.sex == MAN);

直接以复合字面量初始化指针。

struct user *p = &(struct user){ .id = 100 };

不完整类型

可在结构中定义指向自己的指针类型字段,甚至是尚未定义的其他类型指针字段。即便它们尚未完成,也无需前置声明。

不能把自己作为成员,因为类型不完整。但指针可以,这分属两种类型,且指针长度固定可知。同样因为不完整,所以必须用 struct tag,而不能是 typedef 别名。

int main (void)
{
    struct A {
        int      id;
        struct A *a;   // 指向自己类型的指针成员(只能是 struct tag)。
        struct B *b;   // 指向尚未定义,也无前置声明的其他类型。
    };

    struct B {
        int id;
    };

    struct A a = {
        .a = &(struct A){ .id = 100 },
        .b = &(struct B){ .id = 200 },
    };

    assert(a.a->id == 100);
    assert(a.b->id == 200);

    return 0;
}

匿名类型

可省略 tag 和 typedef 别名,定义匿名结构字段或变量。

注意:最后一个字段的分号结尾不能省略,否则编译器警告。

struct data {
    int  id;
    struct { char *s; int x; } value;  // 匿名结构字段。
};

struct data d = { 
    .id = 1, 
    .value = { "abc", 100 } 
};

assert(strcmp(d.value.s, "abc") == 0);
assert(d.value.x == 100);
// 匿名结构变量
struct user { char *name; int age; } u = { .name = "tom", .age = 23 };
assert(strcmp(u.name, "tom") == 0);

偏移和对齐

首成员地址和结构相同,按各字段最长基础类型长度对齐。

对齐目的是提高 CPU 和内存访问效率。

member_address = struct_address + offset(member);
#include <stdio.h>
#include <stddef.h>
#include <stdalign.h>
#include <assert.h>

int main(void)
{
    struct user {
        int  id;
        char name[10];
        struct { int a; long long b; } ext;
    };

    assert(_Alignof(struct user) == 8); // long long
    assert(sizeof(struct user)   == 32); 

    assert(offsetof(struct user, id)    == 0);
    assert(offsetof(struct user, name)  == 4);
    assert(offsetof(struct user, ext)   == 16);
    assert(offsetof(struct user, ext.a) == 16);
    assert(offsetof(struct user, ext.b) == 24);

    return 0;
}
  所有层次基本类型最长的是 longlong = 8,故按 alignof 8 对齐。

  0    4         14       16       20       24        31  ==> offsetof
	+====+=========+========+========+========+=========+
  | id | name    | pad(2) | ext.a  | pad(4) | ext.b   | 
	+====+=========+========+========+========+=========+
  |                       |                 |         |
  |--- 4 + 10 + pad(2) ---|--- 4 + pad(4) --|--- 8 ---|
           2 * 8                   8             8       ==> sizeof 32
           

因对齐缘故,结构长度不一定等于字段之和。且字段排列顺序不同,结构长度也会不同。

struct A {     // alignof(int) = 4
    char a;    // 1 + pad(3)
    int  b;    // 4
    char c;    // 1 + pad(3)
};             // sizeof = 12

assert(_Alignof(struct A) == sizeof(int));
assert(sizeof(struct A)   == 12);
struct B {
    char a;  // 1
    char c;  // 1 + pad(2), 和 a 合并后填充 2 个字节。
    int  b;  // 4
};

assert(sizeof(struct B) == 8);

除此之外,还可人工干预。

#pragma pack(8)       // 指示编译器按 8 字节对齐。
struct data {         // 本应按 long double = 16 对齐。        
    char a;
    long double b;
};
#pragma pack()        // 取消自定义对齐。

assert(sizeof(long double)   == 16);
assert(_Alignof(struct data) == 8);
assert(sizeof(struct data)   == 24);

甚至取消对齐,以实际长度为准。( 不建议使用

struct data {
    char a;
    long double b;
} __attribute__ ((packed));

assert(_Alignof(struct data) == 1);
assert(sizeof(struct data)   == 17);

弹性成员

结构最后一个成员,可以是未指定长度的数组类型(flexible array, T[] )。但要注意,该弹性字段默认不分配内存。计算结构长度(sizeof)时,也不包括该字段。

   header  content
  +=======+=======//========+
  |  len  | str ....        |     string = calloc(sizeof(header) + ?)
  +=======+=======//========+
      4  +  ?                     
  
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

int main (void)
{
    struct string {
        int  len;
        char str[];  // [0] 相同;[1] 对齐后 8 字节,就不是弹性成员了。
    };

    // 总长度 = header + content + NULL;
    // 有了 header.len,确实不需要 NULL 结尾,但要考虑到 C 标准库函数。
    size_t count = sizeof(struct string) + sizeof(char) * 10 + 1;
    assert(count == 15);

    // 分配内存,初始化头信息。
    struct string *s = calloc(1, count); 
    s->len = 10;

    // 填充内容。
    memcpy(s->str, "hello", 5);
    assert(s->str[0] == 'h');
    assert(s->str[4] == 'o');
    
    // 显示字符串,如果没有 NULL 就麻烦了。
    puts(s->str);

    // 释放内存。
    free(s);
    
    return 0;
}

赋值和传参时,弹性成员不会被复制。且不能直接对该成员赋值,因为没有分配内存。

struct string s2 = *s;     // 只复制 sizeof 范围内数据,也就是 len。
memcpy(s2.str, s->str, 5); // *** stack smashing detected ***: terminated

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文