- 文章主题:结构体类型详解
- 所属专栏:深入理解C语言
- 作者简介:更新有关深入理解C语言知识的博主一枚,记录分享自己对C语言的深入解读。
- 个人主页:[₽]的个人主页
自定义类型是C语言中很重要的一个知识,很多的程序都离不开自定义类型,下面是我关于自定义类型的详细解析。
结构体是一些值的集合,这些值称为成员变量。和数组不同,结构体的每个成员可以是不同类型的变量。
逻辑上和普通的变量一样既可在main函数之外声明也可在main函数之内声明,外声明才有机会定义全局变量,内声明作用域受限只能定义局部变量。1
struct tag
{
member-list;//成员表
}variable-list//变量表,typedef重命名时则为变量重命名书写处,
//除开数组不能重命名之外,其余变量重名名均在原
//定义变量表时的变量表处
例:
用结构体记录一个学生的个人信息
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号(所有的自定义类型最后一个成员/可能取值的;/,书写时均可加或不加)
}; //分号不能丢
在声明结构的时候,可以不完全的声明(即可以省略某种结构体的具体类型名(自定义类型中叫这种类型名中去掉了类型关键字的部分为该类型的标签(tag),自定义类型在不用typedef的时候默认的类型名的组成格式就是:自定义类型关键字 + 该类型的这种特定组成形式下的标签(tag)),对结构体进行匿名声明)。
例:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
在结构中包含一个类型为该结构本身的成员是否可以呢?
//代码1
struct Node
{
int data;
struct Node next;
};
//可行否?
如果可以,那sizeof(struct Node)是多少?
无法确定,结构体越大时,它自己就会更大。
//代码2
struct Node
{
int data;
struct Node* next;
};
通过结构体指针来自引用同结构体类型的成员,因为指针值的大小和类型无关始终是确定的,只会和运行机器的位数或其选取的运行该程序的位数有关,所以在既确定了该结构体大小的情况下还能够利用不影响其大小的同类型的指针值指向不同内存的同一种类型的结构体变量,在成员为该指针时,因为指针大小不受不同类型而改变编译器也认为结构体类型已经定义好了,从而可以直接通过其指针来进行自引用了。
//代码3
typedef struct
{
int data;
Node* next;
}Node;
//这样写代码,可行否?
//不行结构体在还没重命名的情况下是不能用其重命名之后的变量名来进行自引用的,
//至少得用运行到该句时它已经经过的初始的该结构体的类型名的指针来定义其成员类型
//才能刚好做到编译不错误的前提下又进行了结构体自身的自引用的效果
//解决方案:
typedef struct Node
{
int data;
struct Node* next;
}Node;
//即先用其原始结构体类型名去进行自引用后再符合语法的在其自引用之后';'
//之前的原是变量表的位置写下其重命名,这样才能符合语法的既实现了自引用
//又对该结构体类型进行了重命名
有了结构体类型(通过声明确定好结构体类型后),如何定义变量,其实很简单。
struct Point
{
int x;
int y;
}p1;
//声明类型的同时定义变量p1
struct Point p2;
//定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL};
//结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
结构体内存对齐是属于结构体较为深入的一个知识,也是一个比较热门的考点。
不用说本质上考察的就是结构体在内存中的对齐规则,因为你得先知道结构体在内存中的具体分布规则(整体上叫这种风格的规则为对齐),才能够计算出结构体在内存中的大小
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常,对齐便于提高该结构体部分在不同平台的可移植性。
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐(计算机字长的倍数,计算机是几位的机器就是几位的字长读取方法,因为内存的读取的初始位置只能为从该内存处探取的字长的非负整数倍开始读取,如果用的是对齐的储存方法的话就能够有更大的概率一次性就读取到,本质是通过对齐尽量使储存的位置与计算机读取内存的字长的非负整数倍相契合,浪费一定量的空间使该变量用更少的读取次数就被全部读取到,通过减少读取内存的次数,提高内存读取的效率,花费更少的次数更快地就可以将其所有的数据全部读取到)。
总体来说:结构体内存对齐是拿空间换时间的做法。
让占用空间小的成员尽量集中到一起,这种方法就能够使所有类型的结构体变量都在一定程度上减少其相应的储存空间(单独的一种情况去分析时可能还会出现一些不全其中但省更多空间的做法,但这种集中的做法可以比随意排序相比减少很多的空间,是一种普适的方法)。
例:
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
运用#pragma
这个预处理指令来改变我们的默认对齐数
#include
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
结论: 结构体在对齐方式不合适的时候,我么可以自己更改默认对齐数。
例:
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;
}
上面的 print1 和 print2 函数哪个好些?
答:print2。
原因:
- 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
- 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论: 结构体传参的时候,要传结构体的地址。
是一种运用结构体的框架实现的更节省空间但单位成员储存信息内存得较小的新的类型
顾名思义:对于一个某种类型的成员去一个小于该类型大小的几bit位为一段的内存进行储存(至于大于该数据的存储自然就是把先被截断后的数据给存入进去)
位段的声明和结构是类似的,有两个不同:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
A就是一个位段类型。 那位段A的大小是多少?
printf("%d\n", sizeof(struct A));
//一个例子
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;
//空间是如何开辟的?
在vs中非同类型位段会仍按照结构体对齐的方法排序,集中的同类型位段会先创造一个该类型大小的空间放置多个同类型的成员当单个位段成员放置不下时再按照对齐的方法由下一个位段成员按照对齐规则创造空间放置自己及之前那个未放置的,如果下一个不是位段则将上一个成员还是按照对齐的方法放置完之后再对齐找位置储存下一个成员。
- 简单概括:位段类型的作用相当于只是确定单次创造内存的大小,决定单个成员内存大小的是:后的数字(几bits),同类型位段书写格式成员会趋向尽可能的塞入一个该类型大小的空间中,若塞不下再试着第一个储存再后一个根据对齐规则创造的空间中,如若后一个是不同类型或者不是位段书写形式的成员就按照普通的对齐规则自己先建立一个该类型大小的空间先存着(这时的储存方式就跟将其当做一个普通int型大小变量在一个结构体中对齐存储没有差别了)。
- 结论:位段中只有同类型的位段数据放在一起时才会产生节省空间的塞入效果,不同类型的交错放置不仅不会有该效果,储存方式和普通的结构体对齐没有差别,还会使单个的成员的内存受数字限制,储存不下该类型大小的数据,超过数字限制的数据储存后就会失真,用起来就不会是该内存值了。
注意:
unnamed
)。总结:跟结构体相比,位段可以达到同样的效果,效率上也基本相同,但是可以很好的节省空间,但是有跨平台的问题存在,并接成员类型和内存打下范围上会比结构体的窄。
枚举顾名思义就是一一列举。
把可能的取值一一列举。
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。
{}中的内容是枚举类型的可能取值,也叫 枚举常量 。
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
例如:
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
- 如果赋初值之前还有可能取值就会还是按照从0开始的顺序给这些数据值。
- 赋初值之后还有只就会是从最后一个赋初值的只开始仍以枚举中递增1的形式确定后面的枚举常量的值。
我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5; //ok??
后面这种虽然也行,因为编译器默认枚举的类型特性和
int
完全相同,并且除了数组的自定义类型的定义都只是确定了取值但在内存中没数据调试监测时因为监测只会监测内存中的值和简单表达式的值这三种自定义类型的成员信息哪怕有了不用也是不会被监测到的
联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));
union Un
{
int i;
char c;
};
union Un un;
// 下面输出的结果是一样的吗?
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
//下面输出的结果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
结果:11223355
原因:共用一块空间,由小端字节序,44被覆盖掉了,效果其实就和用不同的指针强制类型转换1块内存中部分内存的内存值,然后再把这一整块内存打印出来的效果一样。
所以根据这个原理,它们两个多和地址指向的内存直接相关和内存中存储的位置也是直接相关,本质上的原理也是一模一样的,所以他们两个都可以被拿来用作检测一个机器是小端还是大端字节序(字节序就是由内存中的值和内存存储位置之间紧密相关而共同作用产生的一个概念)。
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
详见:深入理解C语言(1):数据在内存中的存储
以上就是对自定义类型的深度解析,希望对你的C语言学习有所帮助!作为刚学编程的小白,可能在一些设计逻辑方面有些不足,欢迎评论区进行指正!看都看到这了,点个小小的赞或者关注一下吧(当然三连也可以~),你的支持就是博主更新最大的动力!让我们一起成长,共同进步!
可从该结论进而推出其声明所确定的该变量类型的作用域效果与其生命周期和定义某一变量a时其作用域和生命周期的情况完全相同,原因应该是虽然一个是定义一个已经确立好的具体的变量而一个是刚声明好其的变量类型,但这两者却在内存中的底层实现逻辑中是相同的,都是在以栈区中变量创建与销毁的逻辑进行声明变量和创建变量的,且因为C语言创建变量根据编译器逻辑在不加static
关键字的情况下只会是在栈区创建的,且又因为加了的情况也应会是和在栈区的一模一样(声明逻辑上没有指针,较难考证),所以可以认为其就是遵循的C语言中的变量创建与销毁的逻辑进行声明变量和创建变量的,所以分析其声明的类型的作用域和生命周期时可完全去套用这套逻辑,只是声明的变量类型不存在指针类型在定义在静态区时不能强制访问而已。 ↩︎
老版编译器下 p = &x不符合语法规范因为直接对匿名结构体进行取地址的&x的类型在编译器看来和匿名结构体指针的struct
不同结构体组成的匿名结构体的变量名均为struct ,所以在互相赋值时不会编译报错,但因为其本质所对应的变量类型及其组成肯定不同,运行时肯定会有越界访问类型的运行错误,所以正因为此处逻辑不够清晰,可能会让程序造成一些莫名其妙的错误,或者让编程者写出一些逻辑不清可能错误或者一些编译器读不清的语句,匿名结构体只会出现在一些程序特殊的情况少量的运用,一般不建议用,应较少运用。 ↩︎