1. 结构体
1.1 结构体的基本概念
- 我们学过一种集合——数组,但是数组仅仅是同种类型元素的集合。结构体也是一类集合,但是它的元素类型可以不一样,我们把这些元素叫做结构体成员变量。我们通常把结构体类型称为自定义类型。
1.2 结构体的声明
例如我们描述一个人:
struct Person //这里的 Person 是结构体标签 { char name[ 20]; //姓名 char sex[ 10]; //性别 int age; //年龄 }person; //结构体变量,在 main 函数外定义的是全局变量这种做法是完全声明的结构体。
1.3 结构体的特殊声明
例如我们这样写,这是一种不完全声明的写法:
//匿名结构体类型 struct //省略结构体标签 { char a; int b; float c; }s1; //结构体声明时顺带定义结构体变量 s1匿名结构体的变量只能定义一次,并且只能是在结构体声明的时候定义。
如果我们非要定义第二次变量,或者是不在结构体声明时定义变量,那么编译器就会报错。
#include <stdio.h> //匿名结构体类型 struct //省略结构体标签 { char a; int b; float c; }s1; //结构体声明时顺带定义结构体变量 s1 int main() { struct s2; //看似是定义第二次变量 //实际上使用不了 s2.a = 'b'; s2.b = 2; s2.c = 2.0; return 0; }
介绍了匿名结构体,我们来看一下下面这段代码:
struct { int a; char b; float c; }x; struct { int a; char b; float c; }a[ 20], * p; p = &x; //这条语句合法吗?我们主观上会认为这两个结构体是一样的,但是在编译器看来,两个结构体就是两个不同的自定义类型,即使他们的成员变量都一样。所以变量 x 的地址不能存放在另一种类型的结构体指针变量 p 当中。
1.4 结构体的自引用
我们来判断一下这种写法是否合法:
struct Node { int data; struct Node next; };很明显是不合法的,就比如要使用 sizeof 计算结构体的大小,那么这个大小能计算的出来吗?这就类似陷入了死递归。
改进的方法:
#include <stdio.h> int main() { struct Node { int data; struct Node* next; }; printf( "%d", sizeof( struct Node)); return 0; }这种写法就是链表的写法,一个结点包含数据域和指针域,这个大小是可算的,因为指针的大小很明确。
至于大小为什么是 16 而不是 8 或 12 ,我们在后面的计算结构体大小中会详细解答。
我们再来结合 typedef 关键字来分析一段代码是否正确:
int main() { typedef struct Node { int data; Node* next; }Node; return 0; }这个问题就类似于是先有鸡还是先有蛋了。这里要说的是,成员变量的声明是先于类型重命名的。也就是说,我们使用类型重定义后的类型名来声明指针,那么在编译器看来是非法的。
解决方案:
int main() { typedef struct Node { int data; struct Node* next; //使用类型重命名之前的类型名称 }Node; return 0; }
结构体声明中不能引用自己,但是可以引用其他的结构体:
struct Person { char name[ 20]; char sex[ 10]; int age; }; struct Count { struct Person data[ 100]; //struct Person 类型的数组 int count; }; int main() { return 0; }
1.5 结构体变量的定义和初始化
我们先谈谈定义。
struct Point { int a; int b; //注意结构体成员变量是不需要初始化的 }p1; //这里是一个全局结构体变量 struct Point p2; //这里也是一个全局结构体变量 int main() { struct Point p3; //这里是一个局部结构体变量 return 0; }定义就非常简单了,只需要一个类型+变量名即可。
接下来我们看初始化。什么是初始化呢?初始化就是在定义变量的时候顺带赋值。
struct Person { char name[ 20]; char sex[ 10]; int age; }; struct Person p1 = { "龙兆万", "男", 20 }; int main() { struct Person p2 = { "龙亿万", "男", 21 }; return 0; }赋值的方法与数组是一样的,只需要注意顺序、成员变量的类型即可。
1.6 结构体变量的数据输出
我们给结构体的变量存放了一些数据,现在我们想要通过 printf 函数来打印这些数据,应该如何操作呢?
#include <stdio.h> struct Person { char name[ 20]; char sex[ 10]; int age; }; struct Person p1 = { "龙兆万", "男", 20 }; int main() { struct Person p2 = { "龙亿万", "男", 21 }; printf( "%s %s %d\n", p1.name, p1.sex, p1.age); printf( "%s %s %d\n", p2.name, p2.sex, p2.age); return 0; }可以看到一个全新的字符 '.' 。这就跟数组的原理是一样的,想要输出数组的某个元素,只需要引用这个元素的下标即可。只不过在结构体中,需要用 结构体变量.成员变量。
1.7 结构体的内存对齐
内存对齐决定了结构体类型占用内存多大的空间。就好比有这段代码:
#include <stdio.h> struct S1 { char c1; int i; char c2; }s1; int main() { printf( "%d\n", sizeof(s1)); return 0; }这似乎是有一些违背常理的,因为结构体包含两个 char 类型,一个 int 类型,应该是 6 字节才对啊?为什么会是 12 个字节呢?这就涉及到结构体内存对齐了。
我们来了解一下结构体内存对齐规则:
- 第一个成员在结构体变量(内存)偏移量为 0 的地址处。
- 其他成员要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认对齐数 与 成员变量大小 的较小值
VS 编译器中的默认对齐数为 8
- 结构体的总大小为最大对齐数(每个成员都有一个对齐数)的整数倍。
- 如果嵌套了结构体,那么嵌套的结构体的对齐数是自己的最大对齐数,并且结构体的大小为最大对齐数(包括嵌套结构体的对齐数)的数整数倍。
就例如我们现在分析这个例题为什么是 12 :
我们再看一个例题,也是求结构体的大小:
#include <stdio.h> struct S2 { char c1; char c2; int i; }; int main() { printf( "%d\n", sizeof( struct S2)); return 0; }
接下来我们来学会计算结构体嵌套的问题:
#include <stdio.h> struct S2 { char c1; char c2; int i; }; struct S3 { int i; struct S2 s2; char c3; }; int main() { printf( "%d\n", sizeof( struct S3)); return 0; }我们上面已经计算了 struct S2 的大小为 8 。
至于为什么存在结构体内存对齐,大部分的参考资料给出两个原因:
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:结构体的内存对齐是拿空间换时间的做法。
不过我们需要注意,并不是所有的开发环境都有默认对齐数的。 例如在 gcc 平台下就没有默认对齐数这一说法。
1.8 修改默认对齐数
我们可以使用 #pragma 这个预处理指令修改默认对齐数:
#include <stdio.h> #pragma pack(1)//设编译器默认对齐数为 1 struct S2 { char c1; char c2; int i; }; #pragma pack()//恢复编译器默认对齐数 int main() { printf( "%d\n", sizeof( struct S3)); return 0; }那么这时候就如我们还不知道结构体内存对齐时所料,大小就为两个 char 类型大小 + 一个 int 类型大小。
1.9 结构体传参
这就跟我们普通数据传参一样的道理。如果我们想要在某个函数内改变外部变量的内容,就必须要传地址(使用指针)。
比如说,我们的结构体已经初始化好了,只需要封装一个打印函数打印结构体的内容,不涉及到改变内容,那我们可以不使用地址传参:
#include <stdio.h> typedef struct Person { char name[ 20]; char sex[ 10]; int age; }Person; void print(Person person) { printf( "%s %s %d\n", person.name, person.sex, person.age); } int main() { Person person = { "张三", "男", 20 }; //结构体已经初始化 print(person); return 0; }现在我们想封装一个函数来专门初始化结构体的内容,那么这时候就需要传址调用:
#include <stdio.h> #include <string.h> typedef struct Person { char name[ 20]; char sex[ 10]; int age; }Person; void Init(Person* per) { strcpy(per->name, "张三"); //per->name 相当于 (*per).name ,找到的是数组的地址, strcpy(per->sex, "男"); //如果 per->name = "张三"; 这样赋值的话是相当于在修改数组的地址 per->age = 20; printf( "%s %s %d\n", per->name, per->sex, per->age); } int main() { Person person; Init(&person); return 0; }
2. 位段
- 结构体是有能力实现位段的。位段的成员必须是整形家族。
2.1 位段的声明
我们先看看位段是如何声明的:
struct A { char a : 2; char b : 3; char c : 4; }; int main() { return 0; }
2.2 位段的内存分配
我们应该如何计算位段的大小呢?
#include <stdio.h> struct A { char a : 2; char b : 3; char c : 4; }; int main() { printf( "%d", sizeof( struct A)); return 0; }首先,位段,就是控制位。就比如上面这段代码,我们定义了一个 char 类型变量 a 。这个 a 本来是有 8 个比特位的。但是我使用位段使 a 的比特位只有 2 个了,b 修改成只有 3 个了,c 修改成只有 4 个了。
在定义 a 时就会开辟一个字节的空间大小,即 8 个比特位。但是 a 现在只有 2 个比特位,放进这一个字节当中还剩 6 个比特位,b 只有 3 个比特位还可以往里放,这时空间大小还剩 3 个比特位,这就不够 c 放了,因为 c 有 4 个比特位,那么此时又会单独开辟一块空间一字节的空间大小。所以位段 A 的大小是 2 字节。
了解了如何计算大小,那么我们来研究位段如何存储数据:
#include <stdio.h> #include <string.h> struct A { char a : 2; char b : 3; char c : 4; }; int main() { char arr[ 2]; struct A* p = ( struct A*)arr; memset(arr, 0, 2); p->a = 2; p->b = 3; p->c = 4; printf( "%02x %02x\n", arr[ 0], arr[ 1]); return 0; }分析一下这个程序:我们定义了一个位段指针 p 指向了 arr 强转为位段 struct A* 之后的空间,也就是说数组 arr 不能用 char 类型的方式查看了,而是要用 struct A 的位段形式查看。我们往 arr 数组里存放 2、3、4 这个几个数字。2 的二进制为 10,可以存放至 a 的两个比特位当中,3 的二进制为 11,可以存放至 b 的三个比特位当中,4 的二进制为 110,可以存放至 c 的四个比特位当中。
这里在提一嘴,如果我们定义的数据的二进制位超过了我们定义的位段,那么就会发生截断。这与 整形数据放在字符类型空间里的道理一样。
3. 枚举
3.1 枚举的基本概念
- 顾名思义枚举就是列举,把可能的取值列举出来。
例如在我们的生活中,周一到周日是有限的七天,可以一一列举。
3.2 枚举的定义
enum Day { Mon, Tues, Wed, Thur, Fri, Sat, Sun }; int main() { return 0; }以上就是枚举的定义。我们要补充的是,枚举的默认值从 0 开始,每往下走递增 1 。就好比上面这段代码,Mon 的值默认为 0 ,Tues 的值从 0 递增 1,为 1,Wed 为 2,Thur 为 3……
当然我们可以自定义,不从 0 开始:
enum Color { Red, Yellow, Green = 80, Brown, Black = 90, White }; int main() { return 0; }Red 的值为 0 ,Yellow 为 1,但是 Green 我们给它赋了 80 ,那么 Brown 就应该为 81,同理 White 为 91 。
3.3 枚举的使用
enum Color { Red, Yellow, Green = 80, Brown, Black = 90, White }; enum Color co1 = Red; int main() { enum Color co2 = Brown; enum Color co3 = 66; //此种写法是不推荐的,因为存在类型差异 return 0; }我们只需要注意,尽量把枚举成员赋给枚举变量即可,避免类型差异。
3.4 枚举的优点
我们本可以使用 #define 来定义常量,但为什么要使用枚举?
- 增加代码的可读性和可维护性
- 和 #define 定义的标识符比较,枚举有类型检查,更加严谨
- 防止命名污染
- 便于调试
- 使用方便,一次可以定义多个常量
4. 联合(共用体)
4.1 联合的基本概念
- 联合体也是一种特殊的自定义类型。
- 这种类型定义的变量也包含一系列成员,特征是这些成员共用一块空间
4.2 联合类型的声明
union Un { char i; int c; }; int main() { return 0; }这个就是最基本的联合体声明。
4.3 联合的特点
联合的成员是共用同一块内存空间的,联合的大小至少是最大成员的大小。
为了验证联合是共用同一块内存空间的,我们可以写这样一个程序:
#include <stdio.h> union Un { char i; int c; }Un; int main() { printf( "%p\n%p\n", &(Un.i), &(Un.c)); return 0; }可以看到,两个不一样的变量但是地址却一样,这就说明了联合的成员是共用同一块内存空间的。
那怎么计算联合的大小呢?非常简单:
#include <stdio.h> union Un { char i; int c; }Un; int main() { printf( "%d", sizeof(Un)); return 0; }成员里面谁的类型最大?int ,有 4 个字节,所以联合的大小为 4 。
但是,我们同样不能忽略对齐数。
#include <stdio.h> union Un { char a[ 5]; int c; }Un; int main() { printf( "%d", sizeof(Un)); return 0; }像这个程序,成员最大的是 char a[5]; ,5 个字节,但是 5 个字节显然不合理。所以联合也需要对齐最大对齐数,很明显,最大对齐数为 4 ,所以联合的大小为 8 。
转载:https://blog.csdn.net/weixin_59913110/article/details/125732217