目录
1. 结构体类型
结构体是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。
1.1 结构体类型的声明

上述的 variable-list 可以有也可以没有,有的意思是直接在这就创建了结构体变量,这里创建的变量可以直接初始化,如下面一段代码:
struct
{
char c;
int i;
double d;
}s = {'x', 100, 3.14};
例如描述一个学生:
struct Stu
{
char name[20];//名字
int age;//年龄
char gender[5];//性别
char id[20];//学号
};//分号不能丢
1.1.1 结构体变量的创建和初始化
#include <stdio.h>
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;
}
1.2 结构体的特殊声明
在声明结构的时候,可以不完全的声明,比如:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
上面的两个结构在声明的时候省略掉了结构体标签(tag)。匿名的结构体类型只能在定义的时候创建变量,不能在后序的程序中单独创建结构体变量,如果没有对结构体类型重命名的话,基本上只能使用一次.。编译器会把上面的两个声明当成完全不同的两个类型。
1.3 结构体的自引用
比如定义一个链表的节点:

上述的代码是错误的,如果这样引用sizeof(struct Node)将会变为无穷大.
正确的自引用方式是通过指针进行自引用:
struct Node
{
int data;
struct Node* next;
};
在结构体自引⽤使⽤的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易引⼊问题,看看下⾯的代码:

因为 Node 是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用 Node 类型来创建成员变量,这样是不行的。
注:为了避免不必要的错误,定义结构体的时候不要使用匿名结构体。
1.4 结构体内存对齐
这一节主要讨论的是如何计算结构体的大小,这涉及到结构体内存的对齐。
1.4.1 对齐规则
(1) 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。
(2) 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值。
vs中默认的值为8。
Linux中gcc没有默认对齐数,对齐数就是成员自身的大小。
(3) 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
(4) 若结构体中嵌套结构体对象,则嵌套的结构体成员的对齐数为该结构体中的最大对齐数,对齐到其对齐数的整数倍处,而外部的结构体的整体大小为最大对齐数(含嵌套结构体成员的对齐数)的整数倍。
以下面这段代码为例:
#include <stdio.h>
struct S
{
char c1; //char类型变量大小1, VS默认对齐数8, 故该成员变量对齐数为1
int i; //int 类型变量大小4, VS默认对齐数8, 故该成员变量对齐数为4
char c2; //char类型变量大小1, VS默认对齐数8, 故该成员变量对齐数为1
};
//结构体总大小要为所以成员变量中最大对齐数的整数倍,上述结构体中成员变量最大对齐数是4,但是成员
//变量已经占了9个字节,为了对齐,故总大小为12个字节。
int main()
{
struct S s;
printf("%zd\n", sizeof(s));
return 0;
}


当遇到嵌入结构体的情况:
#include <stdio.h>
struct S3
{
double d; // 8 8 8
char c; // 1 8 1
int i; // 4 8 4
};
//如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构
//体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
struct S4
{
char c1; // 1 8 1
struct S3 s3; // 8 8 8 //s3大小为16个字节对齐数为8
double d; // 8 8 8
};
int main()
{
struct s4 s4 = { 0 };
printf("%zd\n", sizeof(s4));
return 0;
}

1.4.2 为什么存在内存对齐(仅供参考)
(1) 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
(2) 性能原因:内存对齐是编译器为了提高CPU访问内存的效率而对结构体成员的存储位置进行的调整。CPU 访问内存时,并非一个字节一个字节读取,而是按固定大小的“内存块”(32位 CPU 按 4 字节,64 位 CPU 按 8 字节)读取,若数据的起始地址恰好为内存块的整数倍(如 8 的整数倍),读取一个小于等于 8 的数据时 CPU 可一次完成读取,若未对其,CPU 可能需要两次读取并拼接数据。例如:32 位 CPU 读取一个 4 字节int:若int起始地址是 4 的倍数(如0x0004),一次读取即可;若起始地址是0x0005(未对齐),CPU 需先读0x0004-0x0007,再读0x0008-0x000B,然后提取并拼接0x0005-0x0008的 4 字节数据,耗时加倍。
总结来说:内存对齐是编译器平衡 CPU 访问效率和内存利用率而采用的优化策略。
设计结构体的时候,既要满足对齐,又要节省空间,则让占用空间小的成员尽量集中在一起:

S1和S2类型的成员一模一样,但是S1占12个字节,S2占8个字节。
1.4.3 修改默认对齐数
#pragma这个预处理指令,可以改变编译器的默认对齐数。

结构体在对齐方式不适合的时候,我们可以自己更改默认对齐数。
1.5 结构体传参
#include <stdio.h>
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;
}
但传结构体和传结构体地址时我们选择传地址。
原因:函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能下降。
1.6 结构体实现位段
1.6.1 什么是位段
位段的实现是基于结构体的,位段的声明和结构体是类似的,有两点不同:
1. 位段的成员必须是 int, unsigned int 或signed int,在 C99 中位段成员的类型也可以选择其他类型。
2. 位段的成员名后边有一个冒号和一个数字。
比如:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
A 就是一个位段类型,下面讨论一下位段A所占内存的大小。
1.6.2 位段的内存分配
1. 位段的成员可以是 int,unsigned int,signed int 或者是 char 等类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
//⼀个例⼦
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
//空间是如何开辟的?

位段是专门设计出来节省内存的,成员变量冒号后的数字代表该变量占多少个bit位。如果成员是int类型,就一次开辟四个字节,不够用了之后再继续开辟,同理如果成员类型是char类型,一次开辟一个字节,不够用了之后再继续开辟。
1.6.3 位段的跨平台问题
1. int 位段被当成有符号数还是⽆符号数是不确定的。
2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。
1.6.4 位段的应用
下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络的畅通是有帮助的。

1.6.5 位段使用的注意事项
位段的几个成员共有同一个字节, 这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。
#include <stdio.h>
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
int main()
{
struct A sa = {0};
scanf("%d", &sa._b); //这样是错误的,不能使用&操作符
//正确的是通过变量赋值
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
2. 联合体类型
2.1 联合体类型的声明
像结构体一样,联合体也是由一个或者多个成员构成,这些成员可以是不同的类型。但是编译器只为最大的成员分配足够的内存空间。联合体的特点是所有成员共用一块内存空间。所以联合体也叫:共用体。
#include <stdio.h>
//联合体类型的声明
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un = {0};
//计算联合体变量的大小
printf("%d\n", sizeof(un));
return 0;
}

2.2 联合体的特点
联合的成员是共用同一块内存空间,这样一个联合变量的大小,至少是最大成员的大小。
//代码1
#include <stdio.h>
//联合类型的声明
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un = {0};
// 下⾯输出的结果是⼀样的吗?
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
printf("%p\n", &un);
return 0;
}

上述代码打印出来的地址都是相同的,所以可以说明联合体变量和它的成员变量所占用的空间是同一块空间。
//代码2
#include <stdio.h>
//联合类型的声明
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un = {0};
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
return 0;
}

我们可以看到通过十六进制的形式打印出来的值为11223355,因为在VS里面是小端存储,低位数据存放在低地址处,高位数据存放在高地址处,对超过一个字节的变量取地址都是取的低地址处的地址,但是c变量只占了一个字节,所以c变量的修改影响了i变量的低位。
2.2.1相同成员的结构体和联合体对比

2.3 联合体⼤⼩的计算
联合体的大小至少是最大成员的大小,当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。使用联合体是可以节省空间的。
#include <stdio.h>
union Un1
{
char c[5]; // char变量的对齐数是1, vs默认为8, 所以对齐数为1
int i; // int变量的对齐数是 4, vs默认为8, 所以对齐数为4
};
union Un2
{
short c[7]; // 2 8 2 大小为14个字节
int i; // 4 8 4
};
int main()
{
//下⾯输出的结果是什么?
printf("%zd\n", sizeof(union Un1)); //大小为8个字节
printf("%zd\n", sizeof(union Un2)); //大小为16个字节
return 0;
}

2.4 联合体的一个应用
写一个程序,运用联合体判断机器是大端还是小端:
#include <stdio.h>
int check_sys()
{
union
{
int i;
char c;
}un;
un.i = 1;
return un.c;
}
int main()
{
if (check_sys() == 1)
{
printf("小端");
}
else
{
printf("大端");
}
}
3. 枚举类型
3.1 枚举类型的声明
枚举顾名思义就是一一列举,把可能的取值一一列举出来。比如生活中一周的星期1到星期天:

上述定义的enum Day就是枚举类型。{}中的内容是枚举类型的可能取值,也叫枚举常量。这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值。如果RED赋初值为3,GREEN和BLUE都没有赋初值,那么GREEN和BLUE的值为4和5,是基于RED的值依次增加1的。

枚举其实和#define定义一个常量的效果是一样的。
3.2 枚举类型的优点
我们可以使用#define定义常量为什么要使用枚举呢?因为枚举有以下一些优点:
1.增加代码的可读性和可维护性。(通过枚举可以把下面代码可能用到的数字对应成英语,增加可读性)
2.和#define定义的标识符比较枚举有类型检查,更加严谨。(在C++中枚举类型必须用枚举常量进行赋值,#define定义的符合在预处理阶段就会全部替换为定义的值)
3.便于调试,预处理阶段会删除#define定义的符合,把他替换为#define定义所对应的值。
4.使用方便,一次可以定义多个常量。
5.枚举常量是遵循作用域规则的,枚举声明在函数内,只能在函数内使用。
365

被折叠的 条评论
为什么被折叠?



