C语言——自定义类型:结构体

前言

本篇博客位大家介绍C语言中一块儿重要的内容,那就是结构体,关于结构体的内容,大家需要深入掌握,在后面的学习中依然会用到,如果你对本文感兴趣,麻烦点进来的老铁一键三连。多多支持,下面我们进入正文部分。

1. 结构体类型的声明

1.1 什么是结构体

C语言已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类 型还是不够的,假设我想描述学生,描述⼀本书,这时单⼀的内置类型是不行的。

描述⼀个学生需要名字、年龄、学号、身高、体重等;

描述一本书需要作者、出版社、定价等。C语言为了解决这个问 题,增加了结构体这种自定义的数据类型,让程序员可以自己创造适合的类型。

结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如: 标量、数组、指针,甚至是其他结构体。

上面为大家介绍了结构体的概念,大家需要进行理解;结构体就是用于一些比较复杂的对象,这也给了程序员一定的自由发挥的空间;

1.2 结构体的声明方法

关于结构体的声明方法其实挺简单的,大家来看下面的代码;

struct Stu
{
	char name[20];//名字 
	int age;//年龄 
	char sex[5];//性别 
	char id[20];//学号 
}; //分号不能丢 

这里大家可以看到,上面就是结构体的声明方法,这里要提醒大家一下,结尾的分号一定不能丢,否则程序将报错;

1.3 结构体的特殊声明 

上面说了结构体的一般声明方法,然而还存在一类特殊的声明方法,大家请看下面的代码;

struct
{
	int a;
	char b;
	double c;

}s = { 10,'a',3.14 };
int main()
{
	printf("%d %c %lf", s.a, s.b, s.c);
	return 0;
}

这里大家可以发现,我将结构体的名字省略掉了,这种声明方法声明的结构体叫做匿名结构体;这种声明方法声明的结构体在创建变量的时候,必须和声明同时进行,不能单独拿出来进行创建;

这里建议大家不要去使用匿名结构体类型,这种写法是容易出现问题的;

2. 结构体变量的创建和初始化

2.1 结构体变量的定义/创建

//代码1:变量的定义 
struct Point
{
 int x;
 int y;
}p1; //声明类型的同时定义变量p1 
struct Point p2; //定义结构体变量p2

大家可以看到,上面给出了两种创建方式,一种是在声明类型的同时进行定义;还有一种是单独进行定义;这两种方法都是正确的,大家根据自己的习惯进行选择。

2.2 结构体变量的初始化

//代码2:初始化。 
struct Point p3 = {10, 20};
struct Stu //类型声明
{
 char name[15];//名字 
 int age; //年龄 
};
struct Stu s1 = {"zhangsan", 20};//初始化 
struct Stu s2 = {.age=20, .name="lisi"};//指定顺序初始化
//代码3
#include<stdio.h>
struct s 
{
	int a;
	char b;
}; 
struct b
{
	struct s a;
	int* p;
	char arr[10];
	float sc;
};
int main()
{
	struct b c = { {10,'w'},NULL,"hehe",85.5f };
}

这里大家仔细观察上面的代码,在代码2中,展示了两种初始化的方法,一种是常规的初始化方法,按照声明时的顺序进行初始化;第二种初始化方法是按照指定顺序进行初始化,这里大家需要注意其写法,用到了“.”操作符,这是结构体成员访问操作符,这个后面还会为大家介绍;

下面来说说代码3,前面说过,结构体的成员可以是不同类型的变量,这里当然就包括结构体了;那么大家来看代码3,这里大家可以看到,我声明了两个不同的结构体,并且用第一个结构体定义了一个变量将其放到第二个结构体中,当我对第二个变量进行初始化的时候,大家注意嵌套结构体初始化的方法,对于嵌套的结构体,我们需要使用{}进行初始化,这里希望大家可以根据上面的代码理解嵌套结构体初始化的方法。

3. 结构体成员访问操作符

3.1 结构体成员的直接访问

结构体成员的直接访问是通过点操作符(.)访问的;点操作符接受两个操作数;如下所示:

#include <stdio.h>
struct Point
{
 int x;
 int y;
}p = {1,2};
int main()
{
 printf("x: %d y: %d\n", p.x, p.y);
 return 0;
}

 

上面大家看到,使同方式:结构体变量.成员名,这就是对结构体成员直接访问的方法;

那么这里就有一个问题,如果是嵌套结构体呢?我们应该如何进行访问,大家来看下面的代码;

#include<stdio.h>
struct s 
{
	int a;
	char b;
}; 
struct b
{
	struct s a;
	int* p;
	char arr[10];
	float sc;
};
int main()
{
	struct b c = { {10,'w'},NULL,"hehe",85.5f };
	printf("%c\n", c.a.b);
}

 

大家注意在访问嵌套结构体的时候,我们是按照顺序依次访问的,大家可以通过上面的代码进行理解,相信大家可以掌握。

3.2 结构体成员的间接访问 

有时候我们得到的不是⼀个结构体变量,而是得到了⼀个指向结构体的指针。如下所示:

#include <stdio.h>
struct Point
{
 int x;
 int y;
};
int main()
{
 struct Point p = {3, 4};
 struct Point *ptr = &p;
 ptr->x = 10;
 ptr->y = 20;
 printf("x = %d y = %d\n", ptr->x, ptr->y);
 return 0;
}

这里大家注意,间接访问的方法:使用方式:结构体指针->成员名;

这里创建了一个结构体指针接受了我们创建了结构体变量p,我们将p的地址传给结构体指针,通过结构体指针来间接访问结构体的内容;这种访问方法大家也需要理解并且掌握。

4. 结构体的内存对齐

前面我们了解了结构体的基本内容,下面我们要讨论一个问题:计算结构体的大小;这就是我们接下来要说到的内容——结构体的内存对齐

4.1 对齐规则

首先为大家展示结构体的内存对齐规则,下面用一个例子来为大家解释;

struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	struct S1 s;
	printf("%zd\n", sizeof(s));
}

大家来看这段代码,这里我想要求出代码中的结构体的大小,那么就需要遵循上面说的对齐规则;

 

大家仔细来看上面的图,右边的数字就是偏移量。根据第一条规则,第一个成员对齐到结构体变量起始位置偏移量为0的地址处,如上图所示;

下面轮到第二个成员,这里大家要理解对齐数的概念,在VS里默认对齐数为8,而最终的对齐数是在默认对齐数和成员变量大小中取较小的那一个,比如这里的第二个成员,是int类型。大小为4字节,而VS默认值为8,所以结果就是4,也就是说第二个成员要对齐到4的整数倍的地址处,那么最近的就是从偏移量为4的地方开始存储,向后存4个字节;

第三个成员,和第二个同理,char类型的大小是1字节,VS默认值是8,所以对于第三个成员来说,对齐数就为1,那么第三个成员就需要对齐到1的整数倍的地址处,看一下图,就是偏移量为8的位置,在这里存储第三个成员,占1字节;

综上,可能有的同学就认为这个结构体的大小是9字节了,那么这里大家要注意第三条规则,结构体的总大小为最大对齐数的整数倍,那么在这个结构体中,第一个成员的对齐数为1,第二个成员的对齐数为4,第三个成员的对齐数为1,那么最大对齐数就为4了,所以整个结构体的大小就必须为4的整数倍,那么我们看图,占内存最小的就是12,那么这个结构体的大小就为12字节;

大家可以看到,上面是在VS2022环境下算出的结构体的大小,与我们上面的推理结果一致;希望大家可以通过这个例子理解对齐规则;

为了让大家更好地掌握对齐规则,下面有几道例题,大家一起来看一下;

​//例题1
#include<stdio.h>
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	struct S2 s;
	printf("%zd\n", sizeof(s));
}

​

大家来看一下这道题,和前面的分析方法一样,首先来对齐第一个成员,第一个成员是char类型,占1字节;

第二个成员,依然是char类型,VS默认值为8,char为1字节,所以其对齐数就是1,那么第二个成员将对齐到偏移量为1的地址处;

第三个成员,为int类型,大小4字节,VS默认值是8,所以其对齐数为4,那么第三个成员就需要对齐到4的整数倍的地址处,那么最近的就是偏移量为4的地址处,从那里往后占4字节;

综上,我们将三个成员都存进去了,下面我们要确定整个结构体的大小,很容易知道,最大对齐数为4,那么整个结构体的大小就必须为4的倍数,我们可以发现,前面我们刚好占用了8个字节,8又刚好是4的整数倍,所以整个结构体的大小就是8字节。

//例题2
#include<stdio.h>
struct S3
{
	double d;
	char c;
	int i;
};
int main()
{
	struct S3 s;
	printf("%zd\n", sizeof(s));
}

这道题还是同样的分析方法;

首先来看第一个成员,double类型,我们知道它占8字节;

第二个成员,char类型,大小为1字节,默认值是8,那么其对齐数就是1,那么第二个成员就要对齐在1的整数倍的地址处,前面已经存过了第一个成员,那么第二个成员就可以放在偏移量为8的地址处;

第三个成员,int类型,大小4字节,默认值为8,其对齐数就为4,那么就应该对齐在偏移量为12的地址处,占4字节;

那么最终大家可以发现,我们一共占用了16个字节,我们知道最大对齐数为8,那么16是8的整数倍,所以这个结构体的大小就为16字节。

//例题3
#include<stdio.h>
struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
int main()
{
	struct S4 s;
	printf("%zd\n", sizeof(s));
}

这道题大家要注意它和前两道是不一样的,它出现了嵌套结构体的情况,那么这个时候大家就要注意上面对齐规则中的第四条规则了;

第一个成员,char类型。大小1字节,直接存到起始位置;

第二个成员,这是一个结构体,那么第四条规则告诉我们:结构体成员对齐到自己的成员中最大对齐数的整数倍处,那么从例题2我们知道,S3结构体中最大对齐数是8,所以S3必须对齐到8的整数倍的地址处,在这里就需要对齐到偏移量为8的地址处,占16字节;

第三个成员,double类型,大小8字节,VS默认值为8,所以其对齐数为8,那么成员三就需要对齐到8的整数倍的地址处,大家自己画图会发现,就是偏移量为24的地址处,占8字节;

综上,我们一共占用了32个字节,那么整个结构体的大小是多少呢?再来看一下第四条规则,整个结构体的大小必须为所有最大对齐数的整数倍,那么我们知道,在S4结构体中,最大的对齐数为8,所以整个S4结构体的大小就要为8的整数倍,刚才说一共占用了32字节,32刚好是8的整数倍,那么S4结构体的大小就是32字节。

 

OK,上面我们讨论了几道关于结构体内存对齐的例题,希望大家通过这些例题能够更好地掌握内存对齐的规则;

不知道大家是否注意到,在内存对齐的过程中,其实造成了空间的浪费,那么我们为什么还要遵循内存对齐规则呢?下面我们继续来讨论为什么会有内存对齐?

4.2 为什么存在内存对齐 

4.2.1 平台原因(移植原因)

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

4.2.2 性能原因

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用⼀个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。

总体来说:结构体的内存对齐是拿空间来换取时间的做法

那么,我们有没有一种方式,既满足对齐,又能节省空间呢?
大家来看下面的代码;

struct S1
{
 char c1;
 int i;
 char c2;
};
struct S2
{
 char c1;
 char c2;
 int i;
};

大家观察这段代码,并且取计算一下这两个结构体的大小;

这里大家发现,两个结构体虽然成员一样,但是最终大小却不一样;这里大家就要注意一点,在创建结构体的过程中我们要让占有空间小的成员尽量集中在一起,这样我们就可以在满足对齐的情况下尽量节省空间。

4.3 修改默认对齐数

#pragma 这个预处理指令,可以改变编译器的默认对齐数。

结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。

5. 结构体传参 

#include<stdio.h>
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;
}

大家看看上面的代码,一个是传结构体本身;还有一个是传结构体的地址;那么哪种方式更好呢?答案是print2;

为什么呢?

结论: 结构体传参的时候,要传结构体的地址。 

6.结构体实现位段

6.1 位段的概念

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

1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型。

2. 位段的成员名后边有⼀个冒号和⼀个数字。

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

大家可以发现,位段是专门用来节省内存的;

这里还有一个问题,位段A的大小的多少呢? 

大家可以自己去测试一下,相信很多同学的第一反应是6(因为有一共47个bit);

大家发现,这个结果并不是6,那这是什么原因呢?咱们继续看下面的内容;

6.2 位段的内存分配 

1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型

 2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

 3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段

这里给大家举个例子,咱们来看下面的代码;

#include<stdio.h>
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;
	printf("%zd", sizeof(s));
	return 0;
}

 这里大家可以思考一下结果;​​​​​​​

 大家仔细看上面这张图,它描述了位段申请空间的过程;这里大家注意,每一个小格子代表一个bit;

这里一次开辟一个字节(8bit),在VS 中,在一个字节内部空间是从右向左进行使用的;那么剩余的空间是继续使用,还是浪费掉呢?在VS中,答案是浪费掉。最终我们可以发现,一共申请了3个字节的空间。

每个方块儿中存放的是对应数据的二进制,由于空间有限,所以数据并不能完整地存放到空间中,就会发生截断的现象;所以最终我们通过调试就可以看见位段s中的数据;

大家注意这里是十六进制的形式进行表示的;OK,到这里,关于位段内存分配的内容就为大家介绍完了;

6.3 位段的跨平台问题

1. int位段被当成有符号数还是无符号数是不确定的。

2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。

3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较大,无法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

总结: 跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

6.4 位段的应用

这里的内容大家作了解即可,在这里不需要深入研究,后面在学习网络的相关知识的时候,

6.5 位段使用的注意事项

位段的几个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位 置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。

所以不能对位段的成员使⽤&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在⼀个变量中,然后赋值给位段的成员。

int main()
{
 struct A sa = {0};
 scanf("%d", &sa._b);//这是错误的 
 
 //正确的⽰范 
 int b = 0;
 scanf("%d", &b);
 sa._b = b;
 return 0;
}

大家看一下上面的一段代码,我们是不能对位段的成员进行取地址操作的。

7. 总结 

本篇文章为大家介绍了结构体的相关内容,结构体在C语言中也是一种重要的自定义类型,在后面的学习中大家会经常使用,所以本篇内容,建议大家认真阅读,注意其中的细节,掌握结构体的使用方法以及关于结构体的对齐规则;最后,希望本篇博客可以为大家带来帮助,谢谢!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值