结构基础知识

数组是相同类型的元素的集合.

结构也是一些值的集合, 这些值称为它的成员(member), 各个成员可以具备不用的 类型.

结构的成员通过名字访问.

结构变量在表达式使用中, 并不被替换成一个指针.

结构变量是一个标量, 可以声明指针, 做函数参数, 作为函数返回, 可以声明结构 数组.

结构声明

声明结构时, 必须列出它包含的所有成员. 这个列表包括每个成员的类型和名字

struct tag {member list} variable-list;

加粗部分为可选部分, 所有可选部分不能全部省略-----它们只要要出现两个.

struct {
    int a;
    char b;
    float c;
} x; // 创建了x结构变量,

struct {
    int a;
    char b;
    float c;
} y[20], *z; // 声明创建了 y 数组和 z 指针.

标签字段允许为成员列表提供一个名字, 这样它就可以在后续的声明中使用.

struct SIMPLE {
    int a;
    char b;
    float c;
};

struct SIMPLE x;
struct SIMPLE y[20], *z;

他们创建和最初的两个例子一样, 但存在一个重要区别---- 现在x,y和z都是统一类型的结构变量

另一种良好的技巧使用 typedef 创建一种新类型

typedef struct {
    int a;
    char b;
    float c;
} Simple;

Simple x;
Simple y[20], *z;

结构成员

结构成员可以是标量,数组,指针甚至是其他结构

struct SOMPLEX{
    float   f;
    int     a[20];
    long    *lp;
    struct  SIMPLE s;
    struct  SIMPLE sa[10];
    struct  SIMPLE *sp;
};

结构成员的直接访问

结构成员变量通过操作符"." 访问. 点操作符接受两个操作数, 左操作数就是结构变量的名字, 右操作数就是需要访问的成员的名字

结构成员的间接访问

结构指针通过 -> 操作符访问. -> 操作符同样接受两个操作数, 左操作数是指向结构的指针, 右操作数是需要访问的成员的名字

结构自引用

struct SELF_REF1 {
    int a;
    struct SELF_REF1 b;
    int c;
};

结构包含自身的结构是非法的, 因为成员 b 是另一个完整的结构, 其内部还将包 含它自己的成员b, 第2个成员又是一个完整的结构, 他还将包括它自己的b. 这样重复下去永无止境.

但是包含指向自己的指针是合法的

struct SELF_REF2{
    int     a;
    struct SELF_REF2 *b;
    int c;
};
编译器在结构的长度确定之前已经知道指针的长度, 所以这种类型的自引用是合法的 .

注意, 如果省略结构标签名则结构无法引用自己, 下面例子是 非法 的:

typedef struct {
    int a;
    SELF_REF3 *b;
    int c;
} SELF_REF3;
因为类型名直到声明的末尾才定义. 解决方案是定义一个结构标签:
typedef struct SELF_REF3_TAG {
    int a;
    struct SELF_REF3_TAG *b;
    int c;
} SELF_REF3;

不完整声明

如需声明一个包含了另一个结构的一个或多个成员的结构. 至少有一个结构必须 在另一个结构内部以指针的形式存在.

如果两个结构存在相互引用, 可以使用不完整声明(incomplete declaration), 它声明一个作为结构标签的标识符. 我可以可以把这个标签用在不需要知道这个 结构长度的生命中, 如声明指向这个结构的指针.

struct B;
struct A{
    struct B *partner;
    /*...*/
};

struct B{
    struct A *partner;
    /*...*/
}

结构初始化

结构初始化和数组的初始化很相似. 一个位于一对花括号内部, 由逗号分割的初始值 列表可用于结构各个成员初始化, 这些值根据结构成员列表的顺序写出.如果初始 列表值不够, 剩余的从结构成员列表将用缺省值进行初始化.

结构, 指针成员

typedef struct {
    int a;
    short b[2];
}Ex2;

typedef struct EX {
    int a;
    char b[3];
    Ex2 c;
    struct EX *d;
} Ex;
Ex x = {10, "Hi", {5, {-1, 25}}, 0};
Ex *px = &x;

访问指针

px + 1 并不是一个合法的左值, 因为它的值并不存储与任何可标识的内存位置. 如果 px 指向一个结构数组的严肃, 这个表达式将指向该数组的下一个结构. 就算如此, 此表达式还是非法的.

访问结构

*px 的右值是 px 所指向的整个结构. 你可以把这个表达式赋值给另一个类型相同 的机构, 你也可以把它作为点操作符的左操作数, 访问一个指定的成员.

*px + 1 是非法的.

访问结构成员

访问指针成员

结构的存储分配

编译器按照成员列表的顺序一个接一个地给每个成员分配内存.只有当存储成员需要 满足正确的边界对齐要求时, 成员之间才可能出现用于填充额外内存的空间.

struct ALIGN {
    char a;
    int b;
    char c;
};

不过某个机器整型长度为4字节, 并且起始存储位置必能能够被4整除, 那么 a 和 c 后面将会空 3 字节用于边界对齐.

系统禁止编译器在一个结构起始的位置跳过几个字节来满足边界对齐要求, 一次所有 的结构起始存储位置必须是结构中边界要求最严格的数据类型所要求的位置.

让边界对齐要求最严格的成员首先出现, 对边界要求最弱的成员最后出现, 可以最大 限度的减少边界对齐带来的空间损失.

struct ALIGN2 {
    int b;
    char a;
    char c;
};
所包含的成员和前面那个结构一样, 但它只占用8个字节. 两个字符可以紧挨着存储. 所以这个结构组后面还要跳过两个字节才被浪费.

sizeof 操作符能够得出一个结构体的整体长度, 包括因边界对齐而跳过的字节.

如果你必须确定某个成员的实际位置, 应该考虑边界对齐因素, 可以使用 offsetof 宏(stddef.h):

offsetof(type, member)

type 就是结构的类型, member 就是你需要的那个成员名. 表达式的结果是一个 size_t 值, 表示这个指定成员开始存储的位置距离结构开始存储的位置偏移几个 字节

offsetof(strcut ALIGN, b);

对于前面那个成员, 上面表达式返回 4.

作为函数参数的结构

把结构作为参数传递给一个函数是合法的, 但这种做法往往并不适宜. 因为 C 语言 是传值操作, 会将形参的一份拷贝传递给函数, 如果结构体较大, 则会影响性能. 使用结构指针传参代价就小的多.

防止程序修改结构参数的唯一办法就是向函数传递一份结构的拷贝.

位段

位段的声明和结构类似, 但它的成员是一个或多个位的段. 这些不同长度的字段 实际上都存储与一个或多个整型变量.

位段的声明和任何普通结构成员声明相同, 但有两个例外.

  1. 位段成员必须声明为 int signed int 或 unsigned int 类型
  2. 成员名的后面是一个冒号和一个整数, 这个整数指定该位段所占用位的数目.

下面是一个位段声明的例子:

struct CHAR {
    unsigned ch :7;
    unsigned font:6;
    unsigned size: 19;
};

struct CHAR ch1;
这个声明取自一个文本格式化程序, 它可以处理多达 128 个不同的字符值 (需要7个位), 64中不同的字体(需要6个位), 以及 0 到 524287 个单位的长度.

许多16位整数的机器的编译器会吧这个声明标志为非法, 因为最后一个位段的长度 超过了整型的长度.

位段能够把长度为奇数的数据包装在一起, 节省存储空间. 也可以很方便的访问一个 整型值的部分内容.

联合

联合所有成员引用的是 内存中的相同位置. 用于不同时刻把不同东西存储于同一 位置.

union {
    float f;
    int i;
} fi;
在一个浮点型和整型都是32位的机器上, 变量 fi 只占内存中一个 32 位的字.

如果 f 被使用这个字就作为浮点值访问, 如果成员 i 被使用, 这个字就作为 整型访问.

比较

struct VARIABLE {
    enum {INT, FLOAT, STRING} type;
    int i;
    float f;
    char *s;
};
struct VARIABLE {
    enum {INT, FLOAT, STRING} type;
    union {
        int i;
        float f;
        char *s;
    } value;
};

联合的长度就是它最长成员的长度

变体记录

在一个成员长度不同的联合里, 分配给联合的内存数量取决于它最长的成员的长度.

如果这些成员长度相差悬殊, 当存储较短的成员时, 浪费的空间是相当可观的. 这种情况下可以使用指向成员的指针.

联合的初始化

联合可以被初始化, 但这个初始值必须是联合的第一个成员的类型, 而且它必须 位于一个花括号里.

union {
    int a;
    float b;
    char c[4];
} x = {5};