自学内容网 自学内容网

【C语言】自定义类型——结构体

目录

一、结构体的类型的声明

二、结构体变量的创建和初始化

三、匿名结构体类型

四、结构体自引用

五、结构体内存对齐

(1)对齐规则

(2)计算结构体大小练习

(3)需要内存对齐的原因

(4)修改默认对齐数

六、结构体传参

七、结构体实现位段

(1)什么是位段

(2)位段的内存分配

(3)位段的跨平台的问题

(4)位段的应用

(5)位段不能使用取地址符&


一、结构体的类型的声明

        形式如下:

struct tag
{
member - list; // 成员列表
}variable - list; // 变量列表,属于全局变量,也可以没有

        例如,定义一个学生结构体类型,并创建了全局变量s1:

struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}s1;

二、结构体变量的创建和初始化

// struct Stu 类型的定义
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s = { "张三", 20, "男", "20230818001" };
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id : %s\n", s.id);

//按照指定的顺序初始化
struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex =
   "⼥" };
printf("name: %s\n", s2.name);
printf("age : %d\n", s2.age);
printf("sex : %s\n", s2.sex);
printf("id : %s\n", s2.id);
return 0;
}

三、匿名结构体类型

        省略掉结构体标签 (tag):

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

struct
{
int a;
char b;
float c;
}a[20], * p;

        因为匿名结构体类型没有名字,所以如果没有对匿名结构体重命名的话,只能使用一次(创建一次变量,即声明结构体类型的时候就创建)。并且两个成员相同的匿名结构体类型不相同,如下:

四、结构体自引用

        结构体中不能包含同类型的结构体成员。因为结构体类型还没完全声明结束就开始使用同类型是不行的(不清楚它的大小),相当于一个类型还不存在的时候就开始使用这个类型,并且仔细想想,这样声明的结构体的大小是无穷大的,如下:

        如果结构体想自引用同类型,只能定义为指针类型。指针类型是内置类型,本来就存在,大小也可知(4 或 8字节),如下:

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

        如果是对匿名结构体重命名,就算是包含同类型的指针类型,也是不行的,因为在重命名 Node 之前都还不知道这个匿名函数叫啥,就使用 Node 的指针,如下:

        因此,结构体自引用不能使用匿名结构体,改为如下就正确了:

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

五、结构体内存对齐

        是计算结构体大小的规则。

(1)对齐规则

① 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。

② 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

        对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值。

        VS 中默认的值为 8。

        Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小。

③ 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的) 的
整数倍。

④ 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

(2)计算结构体大小练习

练习一:

        结构体S1的大小:

        结构体S2的大小:

        S1 和 S2 类型的成员一模一样,但是 S2 比 S1 占的空间更小,是因为变量 c1 和 c2是放在一起的。因此,让占用空间小的成员尽量集中在一起,更节省空间

练习二:

        结构体S3的大小:

        结构体S4的大小:

(3)需要内存对齐的原因

① 平台原因(移植原因):

        不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,比如 int 类型数据只能在固定地址处存取,否则抛出硬件异常。为了提高代码的可移植性(对所有硬件平台都适用),需要内存对齐。

② 性能原因

        访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。比如对于结构体 s:

struct s{
    char c;
    int i;
};

        如果内存不对齐:

        如果内存对齐:

        32位机器上,数据总线是32根,读、写数据的时候,一次就读/写32位(4个字节)。如果不对齐,要读两个字节,才能拼凑出 i;如果对齐,发现第一个字节没有,直接跳到第二个字节,读取一次就可以得到 i 。因此,内存对齐更能节省读取时间(用空间换时间)。

(4)修改默认对齐数

        使用 #pragma 预处理指令,示例:

#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//还原为默认对齐数

        对齐数通常是 2 的 x 次方,如 1,2,4,8,不能随意设置。

六、结构体传参

        一种是传结构体本身,一种是传结构体的地址,如下:

struct S
{
int data[1000];
int num;
};
struct S s = { {1,2,3,4}, 1000 };

//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}

//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}

int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}

        因为函数传参,参数要压栈,会有空间和时间上的系统开销。如果结构体比较大,传结构体本身,开销就比较大;如果传结构体地址,指针只有 4 个字节,开销就比较小。因此,结构体传参,最好传地址

七、结构体实现位段

(1)什么是位段

        与结构体类似,但有两个不同:

  •  成员类型必须是 int、unsigned int、signed int、char,C99 标准中也可以是其它类型。
  •  成员名后是 冒号 + 数字

        形式如下:

struct A
{
    int _a : 2;
    int _b : 5;
    int _c : 10;
    int _d : 30;
};

        位段 A 大小是?如果按照结构体的内存对齐的方法计算,4 * 4 = 16 个字节。看看运行结果:

显然不是按结构体的方式计算内存大小,实际上位段的位表示二进制位冒号后面的数字是该成员的大小,比如 _a 占 2 bit 。但是位段 A 的所有成员的大小加起来是 2+5+10+30 = 47,用 6 个字节(68 bit)就够了,为什么是8 字节呢?请看下节。

(2)位段的内存分配

  • 位段每次开辟 4 个字节(int)或者 1 个字节(char)。
  • 位段的不确定因素很多(比如每次开辟从左还是右存储;每次开辟的空间不够下一个成员使用,剩余的空间要不要接着使用),不可跨平台,注重可移植性的代码要避免用位段

        如下例子:

struct S
{
    char a : 3;
    char b : 4;
    char c : 5;
    char d : 4;
};

int main()
{
    struct S s = { 0 };
    printf("%d\n", sizeof(s));

    s.a = 10;
    s.b = 12;
    s.c = 3;
    s.d = 4;
    return 0;
}

        VS中的规则:

  •  char 类型,每次开辟一个字节。
  •  一个字节内,从右向左使用。
  •  一个字节内剩余的 bit 不够下一个成员使用,浪费掉并开辟新的一个字节存放。

        定义结构体变量 s 并初始化位段成员值为 0,开辟如下的空间(因为位段是 char 类型,每次开辟 1 个字节空间):

        一共是3个字节。再给所有位段成员赋值(超出的截断,不够的补0):

        调试验证,每 4 bit 是一个十六进制数,那么上面的值用十六进制表示就是(62 03 04):

        调试结果与理论一致:

        运行结果:

        解决(1)中遗留的问题:

struct A
{
    int _a : 2;
    int _b : 5;
    int _c : 10;
    int _d : 30;
};

        因为位段成员是 int 类型,所以每次开辟 4 个字节(一共开辟了 8 个字节):

(3)位段的跨平台的问题

  •  int 位段被当成有符号还是无符号数是不确定的。
  •  位段中最大的数目不确定。(32位机器最大位是32,16位机器最大位是16。如果是16位机器,像下面这样写就是错误的,因为 30 位已经超过了最大的 16 位;但在 32 位机器上是正确的。)
struct S
{
    int a : 30;
};
  • 每次开辟的空间,是从左向右,还是从右向左使用是不确定的。
  • 每次开辟的空间,剩余的空间不够下一个位段成员使用时,是浪费掉还是接着使用是不确定的。

总结:位段(可以设置使用的位)比结构体(固定的字节)更节省空间,但存在跨平台的问题。

(4)位段的应用

        数据在网络上传输,需要遵守网络协议,网络协议中有个IP数据报的概念(相当于快递包裹上的各种邮寄信息,有发件人、收件人,才知道包裹从哪发、发给谁),下面就是IP数据报的格式:

        如果不用位段,版本(4位)是整型,分配 4 个字节空间,就会浪费 28 位空间。可以发现每一行信息需要的空间加起来刚好是 32 位(4 个字节),刚好是一个整型的大小,设计成位段,将会没有一点空间浪费。

        IP数据报追求节省空间,因为使用更小的空间,网络越通畅。

(5)位段不能使用取地址符&

        位段中几个成员共用一个字节,而内存中是按字节编址的,所以一个字节内的 bit 没有地址,就不能对位段成员取地址,如下会报错:

        因此,不能使用 scanf 直接给位段成员输入值,只能先输入值放在变量中,变量再赋值给位段成员:


原文地址:https://blog.csdn.net/2401_86272648/article/details/142149828

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!