一.结构体
1.结构体的含义
结构体是一种自定义类型,是一些值的集合,它允许将不同类型的数据项组合成一个单一的数据类型。区别于数组,结构体的每个成员可以是不同类型的变量
2.结构体的创建和声明
(1)结构体的声明
声明的格式:
struct tag
{
member - list;
}variable - list;
举例说明:
struct student
{
char name[20];
int age;
char sex[5];
char id[20];
};
(2)结构体创建
【1】在main函数当中进行创建(局部变量)
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[5];
char id[20];
};
int main()
{
struct student s1;
struct student s2;
return 0;
}
【2】直接在结构体声明之后进行创建(全局变量)
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[5];
char id[20];
}s3,s4;
int main()
{
struct student s1;
struct student s2;
return 0;
}
【3】匿名对象的声明
普通的结构体类型。删除结构体名称
struct
{
member - list;
}variable - list;
//删除tag标签
//举例:
struct
{
char name[20];
int age;
char sex[5];
char id[20];
}s1, s2;
匿名结构体类型只能使用一次
#include<stdio.h>
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}* p;
//上面的两个结构在声明的时候省略掉了结构体标签(tag)。那么问题来了?
在上面代码的基础上,下面的代码合法吗?
//p = &x;
//答案是不等价
//原因是匿名结构体类型只能使用一次
3.结构体变量的初始化
【1】结构体创建的时候就可以初始化
#include<stdio.h>
struct Point
{
int x;
int y;
}p1;//声明类型的同时创建变量p1
struct Point p2;//创建结构体变量p2
struct Point
{
int x;
int y;
}p3 = {5,6};
struct Point p4 = {3,5};//初始化:创建变量的同时赋初值。
【2】结构体中含有数组
struct Stu
{
char name[15];
int age;
};
struct Stu s = { "zhangsan", 20 };//初始化
【3】结构体当中嵌入结构体
struct Point
{
int x;
int y;
};
struct node
{
int data;
struct Point p;
struct Node* next;
}n1 = { 10, {4,5}, NULL };
4.结构体的自引用
(1)结构体的自引用只能是引用自身的指针,不能引用结构体自身(原因是会导致无限循环,结构体里装结构体,会导致结构的大小无法进行计算)
#include<stdio.h>
// 代码1
struct Node
{
int data;
struct Nodenext;
};
//可行否?
//如果可以,那sizeof(struct Node)是多少?
//正确写法
struct Node
{
int data;
struct Node* next;
};
(2)结构体的自引用注意:先有鸡还是还有蛋的问题
//这样写代码,可行否?
typedef struct Node
{
int data;
Node* next;
}Node;
//解决方案:
typedef struct Node
{
int data;
struct Node* next;
}Node;
5.结构体的内存对齐
(1)计算结构体大小的时候通常会使用到内存对齐
结构体的内存对齐是指编译器按照一定的规则对结构体中的数据成员在内存中的存放位置进行调整,以确保数据成员的地址满足特定的对齐要求。这种对齐不是指数据类型本身的大小,而是指数据在内存中的位置对齐到某个边界,通常是数据大小的倍数。(对齐的方式主要和起始位置和数据类型的大小有关)
(2)内存对齐的规则
【1】第一个成员对齐到结构体偏移量为0的地址处:结构体的第一个成员总是从结构体的起始地址开始存放。
【2】从第二个成员变量开始,其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数:结构体自身成员大小和默认对齐数的较小值
vs编译器:默认对齐数是8
(在一个32位系统中,默认的对齐数可能是4字节,而在64位系统中,默认的对齐数可能是8字节。)
【3】结构体总大小为结构体成员最大对齐数的整数倍:这意味着结构体的总大小可能是成员大小之和加上一些填充字节,以满足最大成员的对齐要求。
【4】嵌套结构体的对齐:如果结构体中嵌套了其他结构体,嵌套的结构体成员也要对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
(3)内存对齐的原因
【1】平台原因

6.结构体传参
(1)结构体传参和结构体地址传参
#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;
}
(2)结构体传参和结构体地址传参的选择
二.位段
1.位段的含义
位段的声明和结构是类似的,有两个不同:
(1)位段的成员必须是int、unsigned int 或signed int。(整形家族)
(有些平台上也可以是char类型)
(2)位段的成员名后边有一个冒号和一个数字(冒号后的数字代表的是成员变量所占据的二进制位)
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
A就是一个位段类型
2.位段的内存分配
(1)位段的成员可以是整形家族或者char类型
(2)位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来进行开辟的
(3)位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
3.位段的优缺点
跟结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在:
(1)int 位段被当成有符号数还是无符号数是不确定的。
(2)位段中最大位的数目不能确定。
(3)位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
(4)当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
三.枚举
1.枚举的含义和创建
通常用于表示一组命名的整数常量,这些常量在逻辑上属于同一类型。枚举类型变量的值只能是定义时指定的这些常量之一。(一组常量的定义)
enum 枚举类型名
{
枚举常量1,
枚举常量2,
...
枚举常量n
};
2.枚举的初始化
枚举类型名是用户自定义的类型名,枚举常量是用户为该类型定义的常量名。
枚举的初始化,默认给0,依次向下递增1。枚举常量默认从0开始依次递增,但也可以显式地指定某个枚举常量的值,后面的枚举常量将在此基础上递增。
3.枚举的优点
(1)增加代码的可读性和可维护性
(2)和#define定义的标识符比较枚举有类型检查,更加严谨。
(3)防止了命名污染(封装)
(4)便于调试
(5)使用方便,一次可以定义多个常量
四.联合(共用体)
1.联合的含义和创建
联合(Union)是一种自定义的数据类型,它允许在相同的内存位置存储不同类型的数据。
联合的主要特点是,其所有成员共享同一块内存空间(所以联合也叫共用体)因此,在任何时候,联合只能存储其成员中的一个值。这意味着,当给一个联合的成员赋值时,其他成员的值将变得不确定,因为它们共享相同的内存位置。
union 联合类型名
{
数据类型1 成员1;
数据类型2 成员2;
...
数据类型n 成员n;
};
2.代码举例和解析
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
int main()
{
union Data data;
data.i = 42;
printf("Integer: %d\n", data.i);
data.f = 3.14;
printf("Float: %f\n", data.f);
strcpy(data.str, "Hello, World!");
printf("String: %s\n", data.str);
return 0;
}
//在这个示例中,我们定义了一个名为Data的联合类型,
//并在main函数中声明了一个Data类型的变量data。
//然后,我们分别给data的整型成员、浮点型成员和字符数组成员赋值,并打印出它们的值。
//注意,每次赋值都会覆盖之前存储的值。