自定义类型——结构体

目录

1.结构体的基础知识

1.1结构体的声明

1.2结构体的匿名

1.3结构体的自引用

1.4结构体的初始化 

1.5结构体内存对齐

1.6修改默认对齐数

1.7结构体传参

2.位段

2.1位段的基本概念

2.2位段的内存分配


1.结构体的基础知识

首先让我们了解一下什么是结构体,结构体有什么知识点,结构体的计算原理又是怎么样的,结构体又有什么应用?

1.1结构体的声明

 结构体是一些值的集合,这些值称为成员变量,结构体的每个成员可以是不同类型的变量

例如:

struct tag//tag是自定义类型的名字,是一种标签,取什么取决于你
{
    char name[10];//这两个是结构体的成员
    int age;
};

struct tag是你自己自定义结构体类型,如果按照通俗易懂的话那么我们就好打个比方,struct tag = int或者char或者...等等,这是简单粗暴的理解,如果你对于结构体类型理解不来,那你就是暂时这么理解也是可以的,大括号里面包括的就是结构体的成员,所有的成员都写在这个列表里

注意最重要的是:别忘了大括号结尾的' ; '分号!!!!

1.2结构体的匿名

结构体当中其实包含了一个匿名的功能,但是不推荐使用,而且结构体的匿名功能用的很少,可以说是很少的用的是,大家最好按照正规的要求去书写结构体就好了

例子:

struct            //所谓匿名就是tag这个标签的位置不进行命名,让它空着
{
    char name[5];
    int age;
};

在tag标签的位置不进行任何命名,然后直接去写结构体的成员

重点:匿名的情况的结构体只能使用一次,用完一次之后这个结构体就直接废掉了!!!

这个时候肯定就有小机灵鬼想到了,如果我先匿名然后在利用typedef重新命名这样不就行了吗?

我的回答是大错特错!!!

//严重错误代码!!!!
typedef struct
{
    int age;
    Node ch[5]; 
}Node;

首先匿名的情况下你只能用一次,其次你重新命名的时候首先读取的是结构体里面成员,最后再读取你重新命名的名字,那么在读取成员的时候你都不知道Node是个什么东西,因为读取成员在重新命名之前,所以这样的想法是行不通的,错误的想法!!!

1.3结构体的自引用

其实呢,我们对于结构体的自引用(自己引用自己)是有讲究的,如果按照普通的逻辑去自引用这样是行不通的,举个例子:

struct Node
{
    int data;
    struct Node next;
};

这样的自引用按照常理来说应该才对呀?为什么这样行不通呢?

原来在我们自引用的时候,如果按照如上代码sturct Node next这么写的话,那么当我们自引用的时候读取下一个结构体的内容,然后接着继续读取下一个结构体内容,这样如此反复读取下去,我们压根就不知道结构体里面有多大,有多少数据,所以这种写法行不通,就算你放到编译器底下也不会让你通过的,那么结构体自引用打开的正确姿势是什么呢?

答案是:

采用指针的方法自引用!

例如:

struct Node
{
    int data;
    struct Node* next;
};

通过指针找到下一个结构体的地址,就能知道结构体里有什么,这样就能一个个连接下去,通过结构体的自引用,我们清楚认识一下结构体里成员和指针之间的明确的分布,如图:

为什么要这么分布呢?这种分布就涉及到数据结构里的顺序表,链表等等,我们在数据结构当中就会讲到这种分布,现在就不过多展开

1.4结构体的初始化 

首先结构体的初始化分为局部变量和全局变量的初始化,例如:

struct Node
{
    int data;
}s1;//此时s1命名的struct Node类型的变量就是全局变量

int main()
{
    struct Node s2;//此时s2命名的struct Node类型的变量就是局部变量
    return 0;
}

放在int主函数外命名,则此时s1就是全局变量,放在int主函数内命名就是局部变量

struct Node
{
    int data;
}s1 = {114514};

int main()
{
    struct Node s2 = {1919};
    return 0;
}

接着用一个大括号括起来进行内容填写就可以啦,当然这是很简单的命名和初始化,那么遇上复杂的初始化该如何呢?其中有两个,一个是结构体嵌套结构体,另外一种是对于多重嵌套结构体的访问,例子如下:

struct Node
{    
    int data;
    char name[20];
};


struct txt
{    
    int date;
    struct Node s1;
};

int main()
{
    struct txt s2 = {1919,{1145,"浩二"}};//1919 ->struct txt类型的data成员,后面再用个大括号括起来,因为嵌套了一个结构体,所以按照结构体初始化的情况同样进行一次对应的初始化1145->date 浩二->name[20]
    printf("%d %d %s",s2.date,s2.s1.data,s2.s1.name);结构体嵌套访问的时候首先从外表开始一点点往里面剥皮先从s2->s1->成员如此访问
    return 0;
}

结构体嵌套就像吃芒果一样,先从剥皮再到吃果肉的过程,嵌套访问也是如此,里面有多少层的结构体,就先从外表一点点往里面进行访问,如这次代码中s2->s1再到s1结构体里的int data和char name[20]成员

1.5结构体内存对齐

关于结构体内存对齐中我们要学习一个宏offsetof(),这个宏可以告诉我们结构体在内存当中是如何存储的,是如何计算结构体的内存大小的

 我们通过查阅cplusplus这个网站可以知道offsetof()括号里第一个放的是结构体的类型,第二个放的是结构体里面的成员,通过这个offsetof()这个宏我们可以计算结构体成员相较于结构体起始位置偏移量,什么是偏移量?

偏移量是程序的 逻辑地址 与段首的差值,通俗来说相较于第一个成员的起始位置到我们选择成员所在地址位置的长度.

那么结构体的计算规则有什么呢?

1.第一个成员在与结构体变量偏移量为0的地址处。


2. 其他成员变量要对齐到某个数字 (对产数)的整数倍的地址处
对齐数=编译器默认的一个对产数 与该成员大小的较小值


3.结构体总大小为最大对产数 (每个成员变量都有一个对产数)的整数倍

如下图:

 vs中默认对齐数为8,int 类型的大小为4个字节和对齐数取较小值就是4,切记一定是从0开始。char 类型的大小为1个宁节,和对齐数取最小值就是1,第三个char类型也是一样取较小值就是1.如果第二个是int类型就要从4的整数倍开始存储,中间多出的空间也要计算

下面看一下这段代码:

struct A
{
   int a;//0 1 2 3
   short b;//4 5
   int c;//8 9 10 11
   char d;//12
   //总共占据13个字节,需要16个字节
};

这个结构体总共大小是16个字节!!!我为什么挑出这段代码呢?因为我曾经就在这里踩了坑,char d偏移量是12,不要以为这个就是占用了12个字节!其实算上开头的所占的第一个起始位置0,总共是13个字节,所有这段代码总共是16个字节大小!13个字节的情况挑选成员里最大的占用4个字节int的类型,进行整数对齐的话那就要4*4=16个字节的大小

1.6修改默认对齐数

我们知道在VS编译器底下默认对齐数是8,但是呢这个默认值是是可以修改的,当我们要修改的情况下就要用的一个新的预处理指令pragma pack(对齐数的大小)。

#pragma pack(1)// vs默认对齐数是8
//使用#prama预处理指令可以修改默认对齐数
struct S
{
    char c1;// 1 1 1
    int a;//4 1 1
    char c2;//1 1 1
};
#pragma pack()//取消修改的默认对齐数
int main()
{
    printf("%d\n", sizeof(struct S));
    return 0;
}

计算结构体大小的方法如上


那么为什么存在内存对产?其实有下面这些原因:
1.平台原因(移植原因)

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定型的数据,否则抛出硬件异常。


2.性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对产的内存,处理器需要作两次内存访问;而对产的内存访问仅需要一次访问。


总结:
结构体的内存对齐是拿空间来换取时间的做法,为的就是读取内存效率

1.7结构体传参

关于结构体传参也有一些特定的问题,其实这部分就涉及函数栈帧的部分,我们知道函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

所以当我们结构体传参的时候,尽可能的选择指针,地址进行传参等等,这样结构体通过指针直接访问到数据的地址,能够直接找到目的地并进行操作,而且还不需要进行压栈,系统的性能就能尽可能的得到保留

传参例子:

//两种结构体传参的方法
//1.结构体传参
//2.结构体指针传参(避免再次创建空间
struct S
{
    int data[100];
    int num;
};
void printl(struct S tmp)
{
    printf("%d\n",tmp.num);
}

void print2(struct S* ps)
{
    printf("%d\n", (*ps).num);
}
int main()
{
    struct S s = { {1,2,3},100 };
    print1(s);
    print2(&s);
    return 0;
}

2.位段

2.1位段的基本概念

1.位段的成员可以是 int unsigned int signed int 或者是 char(属于整形家族)类型
2.位段的成员名后边有一个冒号和一个数字

struct A
{
    int _a:2;
    int _a:5;
    int _a:10;
    int _a:30;
}

2.2位段的内存分配

位段的成员可以是 int unsigned int signed int 或者是 char(属于整形家族)类型。空间上是按照需要以4个字节 ( int )或者1个字节 ( char ) 的方式来开辟的。位段涉及很多不确定因素,其中不跨平台的注重可移植的程序应该避免使用位段。

接下来我用例图来讲解:

char a:3所占空间的大小就是花3个bit位开辟一块空间,char类型占用8个bit位=1个字节

a占用了3个bit位这时还剩下5个bit位,char 占用了4个bit位,很明显已经不够char c占用了,所以只能在开辟一块大小为一个字节的空间,我们不确定c会不会使用上一个空间留下来的那一个bit位,我们假设不会便用,那么char c就占用了第二个空间的5个bit位,按照同样的逻辑,char 类型就不能存储,仍需开辟一块空间然后把他们各自对应值的二进制位存进去

这张图对于位段的内存分配就十分清晰明了

希望我的博客对你能够有所帮助,早日成为大牛~~~


                                                         京子小可爱压轴~~~~~~~

  • 11
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值