结构体类型的声明
struct tag
{
member-list;
}variable-list;
tag表示结构体的类型名
member-list表示结构体的成员列表
variable-list表示结构体的变量类型
例如描述一个学生
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
这里为什么年龄没有字符数组?名字、性别、学号:本质是文本信息,在C语言中,文本(字符串)需要用字符数组(如char name[20])来存储,因为一个字符串本身就是由一系列字符组成的序列。数组能容纳多个字符,并预留空间用于存放结束符\0
年龄:本质是一个整数值。它表示的是一个数量(岁数),是单一的数值数据。在C语言中,直接用int(整型)这种基本数据类型来存储是最自然、最高效的方式
结构体变量的创建和初始化
#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 = "李四", .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;
}
这里我们定义了一个结构体,但我们没有在结构体结束的位置定义变量,我们是在初始化的时候定义了一个's'变量,但这时候定义变量就要加结构体类型,就好比我们的"int a"一样
'.'运算度是结构体中用于读取对应的数据所发明的,可以按照结构体的顺序进行初始化,也可以按照自定义顺序初始化,在自定义顺序中可以省略变量

typedef
typedef关键字用于为已有的数据类型定义一个新的名字(别名)。它的主要作用是简化复杂数据类型的声明,提高代码的可读性和可维护性
还是拿上面的代码作为示例
#include <stdio.h>
typedef struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}S;
int main()
{
//按照结构体成员的顺序初始化
S 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);
//按照指定的顺序初始化
S s2 = { .age = 18, .name = "李四", .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;
}
给结构体通过typedef创建了一个新的名字"S",然后就可以通过"S"作为结构体类型创建变量,从而达到简化代码的效果
结构的特殊声明
在声明结构的时候,可以不完全的声明
//匿名结构体类型
#include<stdio.h>
struct
{
int a;
char b;
float c;
}x = {100,'s',3.14};
int main()
{
printf("%d %c %.2lf",x.a,x.b,x.c);
return 0;
}
匿名结构体类型是没有(tag)标签,但在结构体尾部创建了一个变量,然后给它初始化,但匿名结构体类型只能使用一次

#include<stdio.h>
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}* p;
int main()
{
p = &x;
return 0;
}
这个代码我定义了两个匿名结构体类型,编译器就不知道是哪一个匿名结构体,代码会报错
结构体的自引用
在结构中包含⼀个类型为该结构本身的成员是否可以呢?
这里就要给大家简单介绍一下链表,等后面我们会单独讲链表
struct Node
{
int data;
struct Node* next;
}
这是链表的基本结构,里面存放的是数据和指向下一个节点的指针,那我们也要存储12345这五个位数字就需要5个链表,并且链表里面有指向下一个节点的指针,而最后一个节点指向的就是空指针了
结构体内存对齐方式
#include<stdio.h>
struct S1
{
char c1;//1个字节
int i; //4个字节
char c2;//1个字节
};
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
在这里我们可能认为结构体在内存中的占用空间是6个字节,但输出结果是......

结构体的大小规则
1.结构体的第一个成员对齐结构体变量起始位置偏移量为0的地址
2.其他成员变量要对齐到对齐数的整数倍的地址
3.对齐数=编译器默认的一个对齐数与成员变量大小的最小值
4.VS中默认为8(可通过#pragma修改)
5.结构体总大小为最大对齐数的倍数
6.若结构体中嵌套了其他结构体,就嵌套的结构体成员对齐到自己的最大对齐数
首先看一个,那第一个成员的地址就是偏移量为0的地址处

再看第二个,对齐数就是编译器和成员变量大小的最小值,i大小=4个字节,VS默认为8,所以对齐数就是4,所以'i'存储的位置就是偏移量为4的地址处,4的一倍是4,所以就可以存放在偏移量位4的地址处

c2占1个字节,和8作比较就是1,所以c2存放的地址就是偏移量为1的地址处

此时结构体占9个字节,现在看第五条,此时 三个数据的最大对齐数为4,所以最后结构体大小就是4的倍数,而如果是4的2倍就还有一个值没有存进来,所以就要浪费三个空间凑成4的整数倍12

所以最后占用的空间就是12个字节
这里给大家留几个题目,可以自己下去算一下
typedef struct s
{
char c1;
char c2;
int i;
}s;
int main()
{
printf("%zd\n", sizeof(s));
return 0;
}
#include<stdio.h>
struct S3
{
double d;
char c;
int i;
};
int main()
{
printf("%zd\n", sizeof(struct S3));
return 0;
}
#include<stdio.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%zd\n", sizeof(struct S4));
return 0;
}
为什么存在内存对齐?
大部分的参考资料都是这样说的:
平台原因
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中
总体来说
结构体的内存对齐是拿空间来换取时间的做法
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到?
让占用空间小的成员尽量集中在⼀起
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
}
S1 和 S2 类型的成员⼀模⼀样,但是S1和 S2所占空间的大小有了⼀些区别
修改默认对齐数
#pragma这个预处理指令,可以改变编译器的默认对齐数
#include<stdio.h>
#pragma pack(1) //设置VS默认对齐数为1
struct S
{
double d;
char c;
int i;
};
#pragma pack()//取消VS默认对齐数为1
int main()
{
printf("%zd\n", sizeof(struct S));
return 0;
}

结构体传参
#include <stdio.h>
struct S
{
int arr[1000];
int n;
double d;
};
void print1(struct S tmp)
{
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", tmp.arr[i]);
}
printf("%d ", tmp.n);
printf("%.2lf\n", tmp.d);
}
int main()
{
struct S s = { {1,2,3,4,5}, 100, 3.14 };
print1(s);
return 0;
}
这里写了一个print1的函数,实现输出操作,在这里把s这个结构体变量传过去,就要用一个结构体的空间去接收,再把结构体的数据传过去,这时候就会存在时间和空间上的浪费,所以可以改进一下代码
#include <stdio.h>
struct S
{
int arr[1000];
int n;
double d;
};
void print2(struct S* tmp)
{
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", tmp->arr[i]);
}
printf("%d ", tmp->n);
printf("%.2lf\n", tmp->d);
}
int main()
{
struct S s = { {1,2,3,4,5}, 100, 3.14 };
print2(&s);
return 0;
}
现在我就把s的地址传过去,再创建一个结构体指针变量,一个指针变量的大小无非是4或8个字节,后期我们会讲"函数栈帧"会细讲这个部分

结构体实现位段
什么是位段
位段的声明和结构是类似的,有两个不同:
位段的成员必须是int,unsigned int 或signed int,在C99中位段成员的类型也可以 选择其他类型
位段的成员名后边有⼀个冒号和⼀个数字
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
这样写就能很清楚的告诉别人这是一个"位段式的结构"表示二进制位,和结构体相比就是在后面多了一个冒号和数字,如果这里面的整型变量存储的数据可以用少于32个比特位表示出来,那四个整型浪费的空间就太大了,所以后面的数就表示的是该变量占用的二进制位数
第一个就是占2个比特位......,所以位段就是用来节省空间的,而这里这个位段式的结构占用空间是47个比特位,就是6个字节,那占用空间真的是6个字节吗?

很显然不是6个字节,虽然没有我们算出来这么极致的节省空间,但也达到了我们的目的,那为什么会出现这种情况呢?
位段的内存分配
- 位段的成员可以是int,unsigned int 或signed int,char等
- 位段的空间上是按照需要以4个字节或1个字节的方式来开辟的
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
先举一个例子
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 = 12;
s.c = 3;
s.d = 4;
return 0;
}
首先给结构体中的变量赋值,赋值以后再取要占用的比特位

最后输出的结果就是3个字节

但我们赋值的值有些超过了占用的空间大小,就把多余的二进制位丢弃

我们来调试看一下

所以上面的代码大家就知道为什么是8了
位段的跨平台问题
int 位段被当成有符号数还是无符号数是不确定的
位段中最大位的数目不能确定(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)
位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第⼀个位段剩余的位时,是舍弃 剩余的位还是利用,这是不确定的
总结:跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在
位段的注意事项
位段的几个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的,内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的,所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在⼀个变量中,然后赋值给位段的成员