自定义数据类型-结构体
Suzhou 38 1

结构体类型声明

结构是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。 (区别于数组,数组是一组相同类型元素的集合。)

自定义数据类型-结构体

自定义数据类型-结构体

上图中的struct Stu是结构体类型,可以用来创建变量。

//声明一个结构体类型
//声明学生类型Stu是想通过该类型创建学生变量(对象)
//描述一个学生需要有相关信息,姓名性别年龄
struct Stu
{
    char name[20];
    char sex[10];
    int age;
}s4,s5,s6;    //创建结构体变量s4,s5,s6(全局变量)
struct Stu s3;    //创建结构体变量s3(全局变量)
int main()
{
    //创建结构体变量s1,s2(局部变量)
    struct Stu s1;
    struct Stu s2;
    return 0;
}
特殊结构体类型的创建

匿名结构体类型: 自定义数据类型-结构体 上图中的结构体类型只有一种创建变量的方式,在变量列表的位置创建变量。 匿名结构体指针类型:

struct
{
    char a;
    int b;
}*pab;

::: tip 结构体指针类型创建变量psa。如果有一个成员变量一模一样的匿名结构体类型,创建变量ab,指针变量psa中不能存放ab的地址,编译器会认为两个声明是两个完全不同的类型。 ::: 匿名结构体类型只能在创建变量的时候使用一次,因为没有名字,后面无法再使用。


结构的自引用

结构体类型不能再内部包含自己类型的变量,例如struct type中不能出现struct type类型的变量。 如下: 自定义数据类型-结构体 原因是这种写法无法计算变量大小,反复包含使得变量非常大,变量的空间无法创建。 ::: warning 上述代码的形式不是递归。 ::: 数据结构的链表中通过一个节点可以找到下一个节点。可以将一个节点分为两个部分,一部分存放节点本身的数据,另一部分存放下一个节点的地址。 自定义数据类型-结构体 当一个节点的类型是struct Node时,下一个节点的类型也是struct Node,下一个节点的地址存放在指针变量next中,next的类型就是struct Node*。如下:

struct Node
{
    char data;
    struct Node* next;
};

int main()
{
    return 0;
}

::: tip 结构体找到同类型的其他变量,使用指针串联。 :::


结构体类型重命名

typedef struct Node
{
    char data;
    struct Node* next;
}Node;

int main()
{
    struct Node n1;
    Node n2;
    return 0;
}

上述代码使用typedef把struct Node重命名为Node,两种类型名都存在,再使用struct Node创建变量时,可以使用新的名字Node创建变量。 ::: tip 结构体重命名时原来变量列表的位置上放置的时类型名,不再是变量名。 ::: 当对函数匿名结构体进行重命名时,不能在结构体内部使用新的命名

typedef struct
{
    char data;
    Node* next;
}Node;

int main()
{
    return 0;
}

上述代码运行失败的原因是,结构体重命名操作结束后才可以使用Node,在上述代码第4行时新的变量名Node还未被定义,不能使用。 ::: tip 结构体重命名时不建议省略原有的tag,以便在结构体内使用struct tag的指针。 :::


结构体变量的定义和初始化

struct T
{
    double e;
    short f;
};
struct S 
{
    int a;
    char b;
    double c;
    char d[50];
    struct T t;
};
int main()
{
    struct S s = { 10,'a',3.14,"hello world",{20.5,3} };
    printf("%d %c %lf %s %lf %d\n", s.a,s.b,s.c,s.d,s.t.e,s.t.f);
    return 0;
}
10 a 3.140000 hello world 20.500000 3

结构体内存对齐

计算结构体大小

struct S1
{
    char a;
    int b;
    char c;
};
struct S2
{
    char a;
    char c;
    int b;
};
int main()
{
    struct S1 s1 = { 0 };
    struct S2 s2 = { 0 };
    printf("%d\n", sizeof(s1));
    printf("%d\n", sizeof(s2));
    return 0;
}

自定义数据类型-结构体 自定义数据类型-结构体

嵌套结构体的结构体大小:

struct S1
{
    char a;
    int b;
    double c;
};
struct S2
{
    char d;
    struct S1 s1;
    int e;
};
int main()
{
    struct S1 s1 = { 0 };
    struct S2 s2 = { 0 };
    printf("%d\n", sizeof(s2));
    return 0;
}

自定义数据类型-结构体

::: tip 内存对齐的意义: 1.平台原因(移植原因)︰不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常。 ⒉性能原因︰数据结构(尤其是栈)应尽可能在自然边界上对齐。原因是为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访问。 ::: 内存对齐是空间换取时间的做法。浪费一部分内存空间,提升内存读取的效率。 ::: warning 在设计结构体的时候,既要满足内存对齐的要求,又要尽量节省空间,需要将占用空间较小的结构体成员尽量集中在一起。 ::: 编译器的默认对齐数可以自行设置:

#pragma pack(1)
//设置默认对齐数为1
struct S1
{
    char a;
    int b;
    double c;
};
#pragma pack()
//取消设置的默认对齐数

::: tip 自行设置的默认对齐数一般是2、4、8、16等,不会设置成3、5、7之类的数字。 :::

offsetof-计算偏移量

offsetof实际是宏,宏的参数可以传递类型。 自定义数据类型-结构体 offsetof的参数为结构体名和成员名,返回偏移量。

#include <stddef.h>
struct S
{
    char c;
    int i;
    double d;
};
int main()
{
    printf("%d\n", offsetof(struct S, c));  //0
    printf("%d\n", offsetof(struct S, i));  //4
    printf("%d\n", offsetof(struct S, d));  //8
}

画图验证如下: 自定义数据类型-结构体


结构体传参

struct S
{
    char a;
    int b;
    double c;
}s;
int main()
{
    s.a = 'a';
    s.b = 10;
    s.c = 3.14;
    printf("%c %d %lf", s.a, s.b, s.c);
    return 0;
}

类似上述代码,对结构体的操作都是在主函数中完成。有的情景选需要将结构体进行传参,让其他函数对结构体进行操作。如下:

struct S
{
    char a;
    int b;
    double c;
}s;
void Init(struct S* ps)
{
    ps->a = 'a';
    ps->b = 10;
    ps->c = 3.14;
}
void Print(struct S tmp)
{
    printf("%c %d %lf", tmp.a, tmp.b, tmp.c);
}
int main()
{
    Init(&s);
    Print(s);
    return 0;
}
a 10 3.140000

::: warning 上述代码第7行应写成“&s”传址调用,如果直接传s,tmp只是函数Init中一份s的临时拷贝,在Init函数中的操作都是针对s的临时拷贝tmp来做的,不改变s的值。(s和tmp的地址不同)。 函数外部想要改变函数内部的变量值时,必须传入函数变量地址。 ::: 上述代码第13行函数结构体打印时,不需要改变结构体成员的值,可以使用传值调用。 ::: tip 在可以使用传值调用的时候,如上述代码的Print函数,也应尽量写成传址调用。 原因是传值调用会在函数中建立一份临时拷贝,如果原数据所占空间过大,函数传参时参数压栈,系统开销过大,影响系统性能。而传址调用时,指针所占的空间是4个或8个字节。

如果担心传址后误操作改变原值,在函数接收时对参数加上const修饰即可。 :::


结构体实现位段

位段是结构体式的类型,位段和结构的区别是,位段的成员必须是int、unsigned int、signed int,有的也有char。位段的成员名后面有冒号和数字。

struct A
{
    int a : 2;
    int b : 5;
    unsigned int c : 10;
    signed int d : 30;
};
int main()
{
    struct A a = { 0 };
    printf("%d\n", sizeof(a));
    return 0;
}

上述代码中,假设a只有1、2、3、4这4种取值,一个整型是4字节32比特,可以表示2^32种状态,而两个比特位就可以表示种状态,占用一个字节过于浪费。 ::: tip 位段的成员里,冒号后的数字就是所占比特位的数量,最大不可以超过一个int所占空间4字节,即≤32。 ::: 上述代码中成员所占比特位之和是47比特,使用6个字节就可以满足需要。

位段的内存分配规则: - 位段的成员可以是int、unsigned int、signed int或者是char(属于整形家族)类型。 - 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。 - 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

位段的作用是节省空间,如下: 自定义数据类型-结构体 本来需要4个int16字节存放的成员,现在只需要两个字节即可存放。 自定义数据类型-结构体 上图中每次开辟一个字节,空间不够时就按需再开辟一个字节。

struct S
{
    char a : 3;
    char b : 4;
    char c : 5;
    char d : 4;
};
int main()
{
    struct S s = { 0 };
    s.a = 10;
    s.b = 20;
    s.c = 3;
    s.d = 4;
    printf("%d\n", sizeof(s));  //3
    return 0;
}

自定义数据类型-结构体 验证内存中的存储情况: 自定义数据类型-结构体 ::: tip 上述代码中位段的成员类型是char,每次开辟一个字节,空间不够时就按需再开辟一个字节。每个字节的使用方式是由右向左填充比特位,先使用低位再使用高位。当当前字节剩余比特位不能满足接下来的比特位填充需求时,将当前字节剩余比特位浪费掉,重新开辟一个字节的空间,直到把所有的变量都填充进去。 ::: 上述代码占用3个字节的内存空间,节省了1个字节。由上证明位段可以节省空间。


位段的跨平台问题 - int位段被当成有符号数还是无符号数是不确定的。 - 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。) - 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。 - 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

总结:跟结构相比,位段可以达到同样的效果?,但是可以很好的节省空间,但是有跨平台的问题存在。

位段应用(TCP协议): 自定义数据类型-结构体

评论区

索引目录