C语言结构体讲解

1:结构体

在我们之前的学习当中我们学了很多的类型:整形(int,unsigned int,long·····)。浮点型(float,double)。数组:(int arr[ ],char arr[ ]···)等一系列的类型。但是在实际生活中单单只有这些类型当中的一种类型来修饰是不够的,就比如我们要修饰一名学生的信息,我们总不能只用一个int类型或者一个数组来修饰这个学生吧,这个学生的基本信息包括,姓名,年龄,性别,身份证。而这些信息是集合了多种类型的,并且这些类型是存在关联的。这个时候我们就需要一种类型,可以将多种类型联合起来的类型——结构体。

结构体——是多种类型的数据的集合。

1.1:结构体类型的声明

//声明一个结构体类型
struct student
{
	char name[20];   //名字
	int age;         //年龄
	char sex[5];     //性别
	char tele[12];   //身份证
};//注意这里的分号不能省略。

注意:大括号后面的分号不能省略,这是一条完整的语句。

其中,struct是结构体关键字,student称为结构体名(也叫结构体标签),它包含了4个结构体成员,分别是name,age,sex,tele。结构体定义的方式和数组的定义方式是一样的,只是结构体定义成员是不初始化的。

接下来我们来逐个分析:
1.首先是关键字struct,说明这是个结构体。
2.后面的student结构体名(标签),它是用来快速引用结构体的标签。
3.接下来就是后面的花括号,用来括起结构体成员列表,也就是成员变量,而成员的类型是根据自己所需定义的,也就说成员可以使任意c数据结构甚至是其他的结构体。
4,在最后花括号后面又分号,表示结构体定义结束了。

1.2:结构体的自引用

在上面我们说过了结构体成员可以是结构体,那问题来了,结构体本身也是一个结构体,那能不能是结构体本身呢?

struct node
{
	int date;
	struct node n;
};
int main()
{
	sizeof(struct node);//如果这个可以那么ret是多少呢?
	return 0;
}

在上面定义了一个结构体类型,结构体成员有date和结构体身n,那我们就想啊,当我们从结构体进去到n的时候,n又包含了一个n,以此类推,这样的话那他怎么停下来呢?这样是不是就形成了一个死循环了,所以结构体包含一个结构体显然是不行的。
那说是不是一定不行呢?其实不是的,我们我们上面分析了之所以上面的结构体包含结构体不行,关键原因是停不下来,那我们就想啊,有什么办法可以让其停下来。
在这里插入图片描述
我们看上的分析,我们要找到1后可以找到2,找到2后可以找3,以此类推。在这里1就相当于结构体,而要找到后面的2相当于结构体自引用,我们就想了一个办法,把1分成两个部分一个部分存1另一个部分存下一个地址,一直到4,最后存一个空指针使其停下。就有了下面代码:

struct node
{
	int date;
	struct node n;
};
int main()
{
	sizeof(struct node);//如果这个可以那么ret是多少呢?
	return 0;
}struct node
{
	int date;
	struct node* next;
};
int main()
{
	return 0;
}

1.3:结构体变量的定义和初始化

在上面我们结构体定义(结构体的声明)只是告诉编译器我们要表示的数据,但是并没有让内存开辟空间,就相当于你要造房子,但是你现在只是把图纸给了工人,并没有告诉工人在哪里做房子。这个时候如果想要使用结构体,就必须创建变量,也就是结构体变量。struct student stu;这里可以类比 int num;这里的类型是int,变量名是 num 而结构体的类型是struct student,这里就可以创建一个变量stu了。这样就创建了一个结构体变量stu,当编译器运行到这条语句的时候,编译器就会开始分配内存给stu,并且这里储存空间都是以这个变量结合在一起的,这就是后面访问结构体成员的时候要使用结构体变量名来访问的原因。

我们结构体定义变量我三种方式:

1:先定义结构体类型,再定义结构体变量

struct student
{
	char name[20];   //名字
	int age;         //年龄
	char sex[5];     //性别
	char tele[12];   //身份证
};//注意这里的分号不能省略。
struct student stu;

2:定义结构体类型的同时定义结构体变量

struct student
{
	char name[20];   //名字
	int age;         //年龄
	char sex[5];     //性别
	char tele[12];   //身份证
}stu;//注意这里的分号不能省略。

3:直接定义结构体变量

struct
{
	char name[20];   //名字
	int age;         //年龄
	char sex[5];     //性别
	char tele[12];   //身份证
}stu;//注意这里的分号不能省略。

在上面的三种写法中其实

struct student
{
········
};
struct student sut
等价于
struct student
{
········
}stu;

而第三种是比较特殊的一种写法,这里就要引入一种概念:匿名结构体类型。
所谓匿名结构体类型就是去掉了结构体名,就像就像上面的那样子,省略了student,只剩下了struct了。

struct 
{
········
}stu;
1:这里要注意了,这里不再是定义结构体类型了,而是直接定义了结构体变量,也就是说编译器给
其分配了空间
2:这里直接创建了结构体变量,而他的局限性就是它只能用一次,你想再次使用就不行了,因为这
个表达式把结构体的声明和结构体创建变量结合在一起了,而且还没有初始化。
3:所以如果你想多次使用结构体,省略结构体名不是一个明智的选择。

除了上面的一种可以省略结构体名之外还有一种写法也可以省略结构体名,就是使用关键字typedef。

typedef struct
{
 		·······
}stu;
stu s1;

typedef我们之前学过了,是重命名的意思,就是用新的类型名代替已有的类型名。

结构体的初始化:
在我们之前学习数组的时候,我们给数组进行初始化的时候,是由花括号{}进行初始化的,而到了我们的结构体其实是一样的,也是在花括号{}里面进行初始化。

struct student
{
	char name[20];   //名字
	int age;         //年龄
	char sex[5];     //性别
	char tele[12];   //身份证
};
int main()
{
	struct student stu={"张三",20,'m',"1234556123456"};
	return 0;
}

那如果我们遇到了一个结构体包含了另一个结构体该怎么初始化呢,这个时候我们还是可以类到数组,只不过这次是二维数组。我们在初始化一个二维数组的时候比如:int arr [3] [3] ={{1,2,3},{2,3,4},{3,4,5}};这里我们在花括号里面再加上了一个花括号,而结构体包含结构体的定义也是相似的。

struct student2
{
	int i;
	char c;
};
struct student
{
	char name[20];   //名字
	int age;         //年龄
	char sex[5];     //性别
	char tele[12];   //身份证
	struct student2 stu2;
	
};
int main()
{
	struct student stu={"张三",20,'m',"1234556123456",{30,'f'}};
	return 0;
}

这里注意一下:

1:各成员的初始值的排列顺序和成员的声明的顺序是一致的。
2:未赋值的元素被初始化为0,这一点也和数组相同。

1.4:结构体成员的访问

(.)运算符:
我们要访问成员,就要用到点运算符(.)。
基本使用方法:<结构体成员变量名>.<成员名>
注意:结构体变量不能全部引用,只能引用变量名。

struct student
{
	char name[20];   //名字
	int age;         //年龄
	char sex[5];     //性别
	char tele[12];   //身份证
};
int main()
{
	struct student stu={"张三",20,'m',"1234556123456"};
	printf("%s %d %c %s",stu.name,stu.sge,stu.sex,stu.tele};
	return 0;
}

也可以在定义的时候赋值:

struct student
{
	char name[20];   //名字
	int age;         //年龄
	char sex[5];     //性别
	char tele[12];   //身份证
}stu={"张三",20,'m',"1234556123456"};
int main()
{
	printf("%s %d %c %s",stu.name,stu.sge,stu.sex,stu.tele};
	return 0;
}

1.5:结构体数组

所谓结构体数组就是数组里面,每个元素都是一个结构体。在我们的实际生活中也是和常见的,就比如一个班的学生信息。
结构体数组的定义和数组的定义其实是很相似的。

struct student
{
	char name[20];   //名字
	int age;         //年龄
	char sex[5];     //性别
	char tele[12];   //身份证
}stu[40];
//表示一个班由40个学生。

这里初始话也有三种:

struct student
{
	char name[20];   //名字
	int age;         //年龄
	char sex[5];     //性别
	char tele[12];   //身份证
}stu[3];
struct student str={{"张三",20,'m',"123456123456"},
					{"李四",30,'m',"123456654321"},
					{"王五",40,'m',"654321123456"}};
struct student
{
	char name[20];   //名字
	int age;         //年龄
	char sex[5];     //性别
	char tele[12];   //身份证
}stu[3]={{"张三",20,'m',"123456123456"},
					{"李四",30,'m',"123456654321"},
					{"王五",40,'m',"654321123456"}};
struct
{
	char name[20];   //名字
	int age;         //年龄
	char sex[5];     //性别
	char tele[12];   //身份证
}stu[3]={{"张三",20,'m',"123456123456"},
					{"李四",30,'m',"123456654321"},
					{"王五",40,'m',"654321123456"}};

不难发现其实和结构体定义变量的三种方法是一样的。

1.6:结构体内存对齐

在结构体的声明中我们知道了,只有当结构体创建了变量之后,编译器才会给其分配空间,那么既然会给其分配空间的话,那结构体的空间大小怎么计算的呢?

这个时候我们就要引入一个新的概念:对齐数

1:对齐数=编译器默认的一个对齐数与该成员大小的较小值。
2:VS中的默认值是8。
注意:不是所有的编译器都有对齐数。

首先先掌握结构体的对齐规则:

1:第一个成员在与结构体变量偏移量为9的地址处。
2:其他成员变量要对齐到某个数组(对齐数)的整数倍的地址处。
3:结构体总大小我最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4:如果嵌套了结构体的情况,嵌套的结构体对齐字节的最大对齐数的整数倍处,简单点说就是结构体的
总大小是所有对齐数(包含嵌套结构体的对齐数)的整数倍。

我们来实现一下:

struct S1
{
	char c1;//char占一个字节比标准8小,对齐数取1,放在偏移量我0处的地址。
	int a;//int占4个字节比标准8小,对齐数取4,但是char c1放进内存后偏移量变成了1,1不是4的
	      //倍数,所以要进行跳跃,跳跃到4的倍数4处,此时空间大小是1+4+4=9。
	char c2;//同理对齐数取1,此时的偏移量是9,1是所有数的整数倍数,所以直接放在后面,此时空间按大小来到了10.
	       //以上三个的最大对齐数是4,而10不是对齐数的整数倍,所以空间还得进行跳跃,跳跃到12,12是4的整数倍。
	       //此时结构体的总大小就是12个字节。
};
struct S2 
{
	//换一下位置
	char c1;//这里同样的道理对齐数是1,放在偏移量我0处的地址。
	char c2;//这里同样的道理对齐数是1,由上一步操作偏移量变成了1,1是所有数的整数倍数,所以直接放在c1的后面。
	int a;//这里对齐数是4,此时的偏移量是2,不是4的整数倍,进行跳跃,跳跃到偏移量为4处,开始存放a,最后空间是2+2+4=8;
	//而上面的最大对齐数是4,是4的整数倍,所以结构体大小是8个字节。
};
int main()
{
	struct S1 s1 = { 0 };
	printf("%d ", sizeof(s1));
	struct S2 s2= { 0 };
	printf("%d ", sizeof(s2));
	return 0;
}

这里我们可以发现s1和s2的成员其实是一样的,不同之处就在于他们的顺序不同,但是就是因为他们的顺序不同导致了他们所占的空间不一样。
第一个结构体:
在这里插入图片描述
第二个结构体:
在这里插入图片描述

struct S3
{
	double d;//首先第一个double的大小是8个字节,他和对齐数一样,那取下的对齐数还是8
	char c;//这里char类型的大小是1,1是所有数的整数倍,所以直接就在double的后面,此时字节大小来到了9
	int i; //int类型的字节大小是4比默认值8小所以对齐数取4,而此时空间大小是9,不是4的整数
	        //倍,那就得空开3的空间到12,12是4的整数倍,所以int的空间排序从12开始
	        //最后的空间大小就是9+3+4=16,以上的对齐数中最大的是8,而16就是8的整数倍,那
	        //么结构体的空间大小就是16个字节了
};
int main()
{
	struct S3 s3;
	printf("%d", sizeof(s3));//16
	return 0;
}

在这里插入图片描述
嵌套式:

struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;//如果嵌套了结构体的情况,嵌套的结构体对齐字节的最大对齐数的整数倍处
	             //s3的最大对齐数是8
	             //其他情况相同
	double d;
};
int main()
{
	struct S4 s4;
	printf("%d", sizeof(s4));//32
	return 0;
}

为什么要存在内存对齐呢?

大部分的参考资料都是如下所说的:
1:平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据:
某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2:新能问题:数据结构(尤其是栈)应该尽可能的在自然边界上对齐。原因在于,为了访问未对其的内存
处理器需要做两次内存访问;而对齐的内存访问仅需要一次的访问。
总的来说:结构体的内存对齐就是拿空间来换时间的做法。

注意:
在上面的结构体计算的时候,一个是12,一个是8,但是里面放的类型是一样的阿,只是放的顺序不同。 所以在设计结构体的时候,我们既要满足对齐,又要节省空间:所以让占用空间小的成员尽量集中在一起。

默认对齐数是可以修改的:

之前我们见过了#pragma pack()这个预处理指令,他就是用来修改对齐数。
#pragma pack(4)//设置对齐数
struct S1
{
	char c;
	double d;
};
#pragma pack()//取消设置的对齐数
int main()
{
	struct S1 s1;
	printf("%d", sizeof(s1));//没有设置之前是16,设置后是12
	return 0;
}

总结:结构在对齐凡是不合适的时候,我们可以自己更改默认对齐数一般我们设置2的次方数。

offsetof:(不是函数而是宏)作用:计算成员的偏移量(结构体成员相对于结构体起始位置的偏移量)

size_t offsetof(struceName(结构体类型名),memberName)
#include<stddef.h>
struct s
{
	char c;
	int i;
	double d;
};
int main()
{
	printf("%d\n", offsetof(struct s, c));//0
	printf("%d\n", offsetof(struct s, i));//4
	printf("%d\n", offsetof(struct s, d));//8
	return 0;
}

在这里插入图片描述

1.7:结构体传参

可定义指向结构体的指针:
定义类型:结构体名* 结构体指针名。

struct student* stu;

定义之后stu指针存放着结构体变量的地址。

stu=&student;

而在访问结构体成员的时候我们要用到(->)操作符。
我们使用一下:

struct S
{
	char a;
	int i;
	double d;
};
//传值
void Print1(struct S ret)
{
	printf("%c %d %lf\n", ret.a, ret.i, ret.d);
}
//传址
void Print2(const struct S* ret)
{
	printf("%c %d %lf\n", ret->a, ret->i, ret->d);
}
int main()
{
	struct S s = { 0 };
	Print1(s);
	Print2(&s);
	return 0;
}

上面的print1和print2中使用print2传址会更好一点,因为print2传的是地址,而地址只占4/8个字节,而print1传的是结构体对象,当结构体对象过于大的时候,就有可能压栈。

总的来讲就是:
函数传参的时候,参数时需要压栈的,会有时间和空间上的系统开销的。
如果传递一个结构体对象的时候,结构体过大,参数压栈的时候系统开销比较大,所以会导致新能的下降。
总结:结构体传参的时候,要传结构体的地址。

1.8:结构体实现位段(位段的填充和可以可移植性)

位段的声明和结构体是相似的,有两个不同:

1:位段的成员必须是int ,unsigned int或者signed int(实际上只要是整形就可以实现位段)
2:位段的成员名后边有一个冒号和数字。

比如:

struct a
{
	int _a : 1;
	int _b : 2;
	int _c : 3;
};

那我们来计算一下他的大小:

struct S
{
	int a:2;
	int b:5;
	int c:10;
	int d:30;
};
int main()
{
	struct S s;
	printf("%d,\n",sizeof(s));
	return 0;
}

所谓位段,里面有个字叫位,这个位指的是二进制位。我们在定义一个结构体类型的时候,必然会涉及到各种各样类型的数据,比如说int整形类型的。比如说int a:2;这个a的取值不可能超级大,他只由0,1,2,3这四种可能性,那a就么必要占一个整形的空间,因为一个整形空间是32个bit位,其所表达的可能性是2的32次方种可能。所这里我们发现如果a的取值只有很少中状态,比如只有4种状态,3种状态甚至是1种状态,其实只要给a两个bit位就可以表达所有的状态。所以这个(:)后面的数字表示的是给的bit位。
所以上面的结构体占的是2+5+10+30=47个bit位相当于6个字节。但是其实答案不是6个字节,而是8个字节,接下来我们来分析一下。

首先来了解一下位段的内存分配:

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

位段,假设我们我们这里创建一个变量s,struct S s;现在我们位s开辟空间,但是我们在开辟空间的时候是以整形的方式来开辟空间的,一次就开辟一个整形的空间,。那我们对这个整形空间的使用习惯的时候是这样的:一个整形的空间大小是32个bit位,这里我们发现上面的结构体中a需要2个bit位,b需要5个bit位,而c需要10个bit位,这个时候还剩下15个bit位,但是d需要30个bit位,而剩下的15个bit空间不够用,这个时候剩下的15个bit位就浪费掉了,就不要了,它会重新开辟一个整形的空间来存放d,而新开辟的空间存放d后还剩下2个bit位怎么办,还是会浪费掉。
在这里插入图片描述
这里就开辟了两个整形的空间,也就是8个字节。
注意:这里(:)后面的数字是不能大于32的,因为一个整形的大小就是32个bit位,如果超过了32是会报错的。

我们发现其实a其实是要占4个字节的,但是这里就占了2个bit位,这里其实是节省了很多空间的,想象一下如果不使用位段的话,这个结构体需要占用16个字节,而现在却只占用了8个字节,节省了一半的空间。
现在我们来做一个例子:

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 = 20;
	s.c = 3;
	s.d = 4;
	return 0;
}

这里我们是按照char类型一个字节一个字节的大小来开辟空间的。
这里首先我们要给s开辟空间,之后先给a用

第一个空间 
0   0   0  0  0  0  0  0  0

这里先开辟一个字节的空间,但是我们就有问题了,这里给a三个bit位,但是我们不知道是从左往右开始给呢?还是从右往左给,这里就假设是从右往左给空间吧。
这里a占了3个bit位,b占了4给bit位这个时候一个字节的空间只剩下了1个bit位,但是c要5个bit位,显然是不行的,所以那1个bit位的空间就浪费掉了,这个时候就得在开辟一个字节的空间

第二个空间
0   0   0  0  0  0  0  0  0

这个空间开始存放c,存放后还剩下3个bit位,这个时候d要存放a,但是d要4个bit位,村放不下,所以那3个bit位的空间就浪费掉了,那我们就得在开辟一个字节的大小来存放d

第三个空间
0   0   0  0  0  0  0  0  0

这个空间就可以存放下d,最后的4个空间也浪费掉了。
在这里插入图片描述
这个时候就得给其赋值了我们看到s.a=10的二进制位是1010,但是我只个a三个bit位的空间,所以a其实只把010那进空间了。s.b=20的二进制位是10100但是b只给了4个bit位,所以只拿到了0100。s.c=3的二进制位是011而c给了5个bit位,可以存下,所以存的是00011。
s.d=4的二进制位是100,而给d了4个bit位可以存下,所以存的是0100.
现在我们从内存里看一下,但是内存的存放方式是16进制,所以我们要将他转换成16进制的数字。

在这里插入图片描述
所以我们在内存中看到的因该是22 03 04
在这里插入图片描述
但是位段其实有很多的局限性的:
位段的跨平台问题:

1;int 位段当作有符号数还是无符号数是不确定的。
2:位段中最大位的数目不确定。(16位机器最大是16,32位机器最大时32,写成27,在16位机器就会出问题)。
3:位段中的成员在内存中从左向右分配还是从右向左分配标准未定义。
4:当一个结构体包含两个位段,第二给位段成员比较大无法容纳第一个位段剩余的位时是舍弃剩余的位还是利用,这是不确定的。

总结:
更结构体相比,位段可以达到相同的效果,但是可以很好的节省空间,但是右胯平台的问题存在。

位段的使用:
我们在上网的时候,那面接触到数据传输,就比如我们发了一条消息,但是难道我们就仅仅发了一条消息吗?那当然不是了,我们在发消息的其实还附带了很多的数据,比如你要发的地址,谁发的,要发给谁等等,都要放到数据包里面去,这个时候就要给每个数据打包,给他们分配内存,比如:在这里插入图片描述
这里我们本来最小的类型就是char占一个字节,但是肯定有数据是不要一个字节大么大的,所以这个时候位段就可以有效地节省空间了。

  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

初阳hacker

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值