C语言定义类型分为,内置的数据类型常见有(int、float、short、char、long、double),这些都是最基本的对单个参数进行定义类型,而当出现对象时,对一个对象拥有多种基本属性,所以就有了自定义类型的需求,进而也有了这类的编译出现。自定义类型常见的有(结构体、枚举和联合体)。
本篇博客主要对结构体的相关总结和编写笔记。
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型变量,因此结构体在定义时就有以下注意点:
- 结构体类型的声明
- 结构体的初始化与自jie引用
- 结构体内存对齐
- 结构体传参
- 结构体实现位段(位段的填充&可移植性)
结构体声明
结构体Stu声明:
struct Stu(关键字)
{
成员变量
} stu(全局变量);
//其中最后的stu表示全局变量,便于后面对结构体的调用,此处也可省略
初始变量:struct Stu s = {成员变量};或者 stu = {成员变量};
匿名声明:
struct
{
成员变量
}stu;
//此处的stu就为此结构体的变量声明,匿名的时候,不可省略,不然无法调用。
初始变量:stu = {成员变量};
以类型声明:
typedef struct
{
成员变量
}stu;
//此处的stu可以作为淡出的类型去初始化成为其他的变量
例如:stu s = {成员变量};
以学生类型为例子:
struct Stu
{
int Num[20];
char name[20];
int age;
}stu;
初始定义: struct Stu s = {'123456','yoyo',21};//初始化Stu并赋予局部变量s;
也可以直接:stu = {'123456','yoyo',21};
struct
{
int Num[20];
char name[20];
int age;
}stu;
初始定义:stu = {'123456','yoyo',21};
typedef struct
{
int Num[20];
char name[20];
int age;
}stu;
初始定义:stu s = {'123456','yoyo',21};
2.结构体的初始化和引用
结构体的初始化,通过初始化结构体的内存和授予局部变量或者全局变量并分配内存 结构体的引用,结构体可以引用别的结构体。而结构体进行自引用的话,只能引用结构体的指针,而不能自引用整个结构体,因为在自定义结构体的时候会对结构体进行开辟空间,如果引用自己的整个结构体代表着所有成员变量将被不断重复地进行创造,会使得空间不断开辟最终溢出空间。
//结构体的初始化
//分数结构体
struct Goal
{
int CH_G; //语文
int MA_G; //数学
int EN_G; //英语
};
//学生结构体
struct Stu
{
int Num[20]; //学号
char name[20]; //姓名
int age; //年龄
};
//成绩单结构体
struct Stu_G
{
struct Stu stu;
struct Goal g;
};
struct Goal g = {80,80,80}; //初始化分数结构体全局变量 g
int main()
{
struct Stu stu = {'123456','yoyo',21}; // 初始化学生结构体局部变量
struct Stu_G sg; //初始化成绩单结构体
return 0;
}
//结构体的自引用
struct Node
{
int data;
struct Node* next; //引用下一个指针的指针的地址,地址为int型,只有4个字节
struct Node n; //不合法写法,不断地对结构体的成员变量进行初始化,不断开辟data类型
}
3.结构体内存对齐
结构体内存对齐指的是在结构体的成员变量定义的时候根据类型和对齐原则进行内存分配。
结构体内存对齐原理:
1.第一个成员在与结构体变量偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对奇数)的整数倍的地址处
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值
(vs 中默认的值为8)
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数的整数倍。
具体的例子解释可以如图所示:
为什么存在内存对齐
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取得特定类型的数据,否则抛出硬件异常;
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐,原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐内存访问仅需要一次访问。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
设计结构体的时候,满足对齐,又要节省空间:让占用空间小的成员尽量集中在一起。
以下代码可以测试下自己是否理解了对齐原则:
struct s1
{
char c2;
char c;
int a;
};
struct s2
{
double a;
char b;
int c;
};
struct s3
{
char a;
struct s2 s2;
double b;
};
#pragma pack()
int main()
{
struct s3 s = { 0 }; //将s3改成s1跟s2便可以依次出打出 s1、s2、s3结构体各占的内存
printf("%dn", sizeof(s));
r
4.结构体传参
结构体传参也分为传值跟传址,也就是传实际的参数值还是传参数值。
struct s
{
int a;
char c;
double d;
};
void Init(struct s* ps)
{
ps->a = 100;
ps->c = 'w';
ps->d = 3.14;
}
//传值:函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销,
//如果结构体过大时,参数压栈的系统开销比较大,所以就会导致性能的下降
void printf1(struct s tmp)
{
printf("%d %c %0.2lfn", tmp.a, tmp.c, tmp.d);
}
//传址:只传地址,就不需要对参数进行压栈的过程
void printf2(struct s* tmp)
{
printf("%d %c %0.2lfn", tmp->a, tmp->c, tmp->d);
}
int main()
{
struct s s = {0};
Init(&s);
printf1(s);
printf2(&s);
return 0;
}
压栈的意思:
程序可以将数据压入栈中,也可以将数据从栈顶弹出,在i386机器中,栈顶由称为esp的寄存器进行定位。压栈的操作使得栈顶的地址减小,弹出的操作使得栈顶的地址增大。
栈在程序的运行中有着举足轻重的作用,最重要的是栈保存了一个函数调用时所需要的维护信息,这常常称之为堆栈帧或者活动记录,一般包含如下几方面的信息:
1. 函数的返回地址和参数
2. 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量。
5. 结构体实现位段
位段的声明和结构是类似的,有两个不同:
1. 位段的成员必须是int,unsigned int或者signed int。
2. 位段的成员名后面有一个冒号和一个数字
位段的定义:位段冒号后面表示的就是变量有多少个比特(1个字节 = 8个比特);
struct A
{
int _a : 2; //a占2个bite
int _b : 5; //b占5个bite
int _c : 10; //c占10个bite
int _d : 30; //d占30个bite
//全部加起来就是47个bite位
};
位段的理解可以如下图所示:
位段的内存分配
1.位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的
2.位段涉及很多不确定因素,位段是不跨平台的,注重可移植性的程序应该避免使用位段。
位段的存在就是为了节省空间
比较常见用位段作为储存的就是进行网络传输时的IP头,因为IP头含有多种不同长度的类型在里面,而且最长的为32位,刚好一个int类型。IP头如下图所示:
结构体的学习记录到此,如有表述不清或者不对的欢迎私聊批评指正!!!



