C语言之结构体

C语言本身提供了内置类型如int、char之类的,但是这些是单一的数据类型,如果想要描述一个个体比如学生或者是一本书这种具体的,就没有办法用这些单一的内质类型去定义了,所以需要结构体来包含这些个体的信息,比如学生需要年龄性别学号等,书需要页数字数价格等,大概是这个用途。

1.结构体的声明和定义:struct tag
                             {
                                     member-list;//成员列表
                              }variable-list;//变量列表

结构是一些值的集合,这些值被称为成员变量,结构的每个成员可以是不同类型的变量,如:标量、数组,甚至可以是其他的结构体。以前学的数组是一组相同类型元素的集合,结构体里面就可以是不同类型的元素。

例如:声明一个学生类型:学生有名字、性别、年龄、学号,名字、性别和学号可以是字符串,放到字符串数组中,年龄是整数,那么就可以这样创建:

struct Student
{
	char name[20];//一个汉字是宽字符,占两个字节
	char sex[10];
	int age;
	char id[16];
};

注意分号。变量列表可以不写,但是它有作用,现在有这个学生类型,但是它只是一个类型,就像是int、char这样子的东西,没有创建变量,没有开辟空间。所以需要创建一个变量来开辟存放名字性别年龄学号这些东西的空间。创建方式:

struct Student
{
	char name[20];//一个汉字是宽字符,占两个字节
	char sex[10];
	int age;
	char id[16];
};

int main()
{
	struct Student s1;
	return 0;
}

 所以,分号前面的就可以类似的写上变量名,可以写多个,用逗号隔开,但是是全局变量

2.结构体的初始化:数组中初始化用大括号,结构体也一样使用大括号来初始化:

struct Student
{
	char name[20];//一个汉字是宽字符,占两个字节
	char sex[10];
	int age;
	char id[16];
};

int main()
{
	struct Student s1 = { "张三","男",18,"20230503" };//写法一
	struct Student s2 = { .age = 20,.id = "20240525",.name = "李四",.sex = "男" };//写法二
	return 0;
}

以上两种都可以。结构体还可以嵌套使用和初始化,就像是:

struct P
{
	int x;
	int y;
};
struct Note
{
	int data;
	struct P;
	struct Node* next;
};
int main()
{
	struct Node n1 = { 200,{17,25},NULL };

	return 0;
}

 3访问结构体:现在有了这个人的信息,那么就可以打印了,打印方式如下:

struct Student
{
	char name[20];//一个汉字是宽字符,占两个字节
	char sex[10];
	int age;
	char id[16];
};
int main()
{
	struct Student s1 = { "张三","男",18,"20230503" };
	struct Student s2 = { .age = 20,.id = "20240525",.name = "李四",.sex = "男" };
	printf("%s %s %d %s", s1.name, s1.sex, s1.age, s1.id);
	return 0;
}

 以上用“.” 的方式是直接访问,还有间接访问,是要利用指针的。在使用函数对结构体进行传参时,可以使用指针接收,这时候就可以使用结构体的间接访问来访问结构体了。例如:

struct P
{
	int x;
	int y;
};
void Print(struct P* p1)
{
	printf("%d %d", p1->x, p1->y);
}
int main()
{
	struct P p = { 4,5 };
	Print(&p);
	return 0;
}

利用箭头->来实现间接访问。

4.(1)匿名结构体:使用匿名结构体时,不写结构体的名字,且只能使用一次,接下来就不能用了。 

struct 
{
	int x;
	int y;
}s = { 4,5 };

int main()
{
	printf("%d %d", s.x, s.y);
	return 0;
}

虽然可以打印,但是往后编译的话就使用不了了。如果是这样的话:

struct
{
	int x;
	int y;
}s;
struct
{
	int x;
	int y;
}*ps;

int main()
{
	ps = &s;
	return 0;
}

虽然两个结构体一模一样,但是编译器会警告,

因为它是匿名的,编译器无法分辨这两个类型是不是同一个类型,所以它会把上面两个声明当作是两个不同的类型,所以是非法的,如果不对匿名结构体类型重命名的话基本上只能使用一次。

(2)结构体自引用:有一种线性数据结构叫做链表,比如现在有数字12345,但是这5个数字不是在连续的一块内存空间中的,是分散的,现在要求1可以找到2,2找到3,以此类推找到5,就像链条一样,所以找到1的时候就找到2345,虽然不是同一块空间但是可以通过某种方式联系起来。

而这些链接起来的点就叫节点,节点不仅要储存数字,还要有能力找到下一个节点,所以这个节点有两个部分,一个是值,一个是下一个节点的相关信息。因此,链表节点可以这样划分:

上面放数据,下面放下个节点的地址,最后一个节点后面没有了放空指针就可以了。所以可以这样定义:

struct Node
{
	int date;//数据
	struct Node* next;//下一个节点地址
};

这个结构体包含了一个跟自己同类型的指针,这就是自引用。 

5.结构体内存对齐:

问题引出:计算结构体占内存多少个字节?

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

	return 0;
}

最后运行结果是12。c1占一个字节,i占四个字节,c2占一个字节,最后共6个字节,最后12个,是因为结构体内存中有一套自己的对齐规则。

(1)对齐规则:

偏移量:以字节为单位计算,对于结构体起始地址而言。

*第一个成员在结构体变量偏移量为0的地址处
*其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处,对齐数=编译器默认的对齐数与该成员的一个较小值(vs默认对齐数为8)
*结构体的总大小为最大对齐数的(所有成员都有对齐数,所有对齐数中最大的)整数倍

*对于嵌套结构体,嵌套的那块对齐到自己最大对齐数的整数倍,结构体整体大小就是所
有最大对齐数(含嵌套结构体的对齐数)的整数倍 。

就拿上面的s来说,第一个成员是字符c1,放到偏移量为0的地址处,第二个成员是整型i,大小为4个字节,vs默认对齐数为8,i比8小,所以对齐数是4,所以i存的位置必须是4的倍数,往下走偏移量到4的位置是4的倍数,所以i从4的位置开始存,由于4个字节,就存到偏移量为7的位置。最后是c2,它走到8的位置,但是第三条规则最大对齐数是4,所有要往下走一直到11,0~11总大小为12才是4的整数倍。

再看一例:

struct s1
{
	char c1;//放到0处
	int n;//int是4个字节,小于8,则放到偏移量为4的地址
	char c2;
};
struct s2
{
	char c3;//放到0处
	struct s1 S;//找里面的最大对齐数—4,就从4的整数倍开始,最小是4,就从4开始,占12
	double d1;//8个字节,从8的整数倍开始,从16开始一直到23
};
int main()
{
	struct s2 s = { 0 };
	printf("%d", sizeof(s));//24
	return 0;
}

先算S,总大小为12,然后进行对齐,c3在0的位置,然后就是S,里面的最大对齐数是4,就从偏移量为4的地址开始,大小12一直走到11,最后是d1,大小是8个字节,所有对齐数是8,就从8的倍数的偏移量开始存放,往下找,16是8的倍数,于是就从16开始往下走8个字节,一直走到23,0~23大小是24个字节。

(2)为什么存在内存对齐:

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

性能问题:访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存只需做一次访问。假设给一个结构体,有两个成员分别是char类型和int类型的,按照对齐规则总共占8个字节。如果不对齐的话,char后面连着int就占5个字节。假设在32位平台一次读4个字节,都从偏移量为0的位置开始放,当读到i的时候,对齐的情况是一次就可以完整的读完个字节,不对齐的情况是先读了i的三个字节,还剩下一个就要再读一次。内存对齐就是利用空间换取时间的做法,让它效率高一点。 

(3)修改默认对齐数:#pragma pack( ),括号里是你要修改为多少的对齐数。

6.结构体传参: 类似于内置类型数据的传参,既可以直接传,也可以传指针

struct S
{
	int date[1000] ;
	int num;
};
void print1(struct S ss)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", ss.date[i]);
	}
	printf("%d\n", ss.num);
}
void print2(struct S* ps)//由于传地址可能会误操作导致s被改,故加const
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", ps->date[i]);
	}
	printf("%d", ps->num);
}
int main()
{
	struct S s = { {1,2,3},100 };
	//打印该结构体:
	print1(s);//传值调用
	print2(&s);//传址调用(省空间)
	return 0;
}

首先看第一种直接传参,这个结构体很大,有一个1000个元素的数组,还有一个整型,如果直接传参的话,print1函数会复制一份一样大小的形参,所有内存空间占比会很大。

所有就有了第二种,传址调用,把地址传给函数print2,这样子print2有了一个地址就可以往下去访问这个结构体了。所以以后在结构体传参时,尽量选择第二种传址调用。

7.结构体实现位段:

(1)位段的概念:是基于结构体的,位段的声明和结构体类似,有两个不同:

*成员必须是int、unsigned int和signed int,在C99中位段成员可以是其他类型,比如char类型。

*位段的成员名后面有一个冒号和数字。

struct A
{
    int _a:2;
    int _b:5;
    int _c:10;//下划线只是变量名而已,加不加无所谓
};
位段的位是二进制位的意思。一个整型4个字节,也就是32个比特位,假设这个a的值不大,只有0123这样的大小,那么0只有两个2进制位,1和2和3也是只有两个,如果它的取值仅仅只有0123那么它虽然占32个比特位但是只需要用到两个,所有白白浪费了30个比特位,所以就有这种位段的写法,后面的数字不是值而是比特位,a只占2个比特位,b只占5个,c只占10个,所以在一定程度上节省了空间。

(2)位段内存分配:如果成员是int,就一次开辟4个字节,不够再开,位段涉及很多不确定的因素,不跨平台的,注意可移植的程序应避免使用位段。首先有如下位段:

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;

	return 0;
}

内存:

 首先是char类型的,就先开辟一个字节共8个比特位,由于它只需要3个比特位,但是这就存在分歧了,是从左向右分配还是从右向左分配。vs上采用的是从右向左分配的,所以a的三个就在右边开始,接下来是b,b需要4个,但是旁边左边还有5个,够用就继续从右向左分配,到了c,需要五个,但是剩下的空间不足了,又有歧异,是继续使用还是废弃,vs上是废弃,就从右边再申请一个字节,以此类推,最后总共申请了三个字节。接下来就是main函数的赋值操作,a里面放10,转换为二进制就是00001010,但是a只有3个比特位,所以只能截出010给a,12的二进制位是00001100,存到b里,但是b只有4个比特位,继续截出1100给b,然后废弃掉的空间默认为0,以此类推。且每4个二进制位可以写成一个十六进制位,0110就是6,0010就是2,0000就是0,0011就是3,0000是0,0100是4,最后连起来内存里面放的是十六进制的620304。

(3)位段跨平台问题:在32位和64位平台下int整型是4个字节,16位机器是2个字节,所以位段的数目不确定,平台一换就不行了,而且int位段是有符号还是无符号是不确定的,有些编译器做有符号处理和无符号处理也是不确定的。位段成员从左向右或从右向左分配也是不确定的,最后的空位舍弃还是不舍弃也是不确定的,所以这些原因导致不同编译器产生不同效果,因此不跨平台。

总结一下,虽然位段和结构体有同样的效果且可以很好节省空间,但是不跨平台。

(4)应用与注意事项:可以应用于网络中

位段的成员共用一个字节,且有些成员的起始位置并不在该字节的起始位置,地址是每一个字节给一个地址,一个字节的起始位置给一个地址,一个字节内部的比特位上没有地址,所以这些成员并没有放在起始地址处,那么这些位置处就没有地址。就比如:

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
int main()
{
	struct S s = { 0 };
	scanf("%d", &(s.a));

	return 0;
}

正确做法:创建另一个变量,输入变量的值再赋值给这个位段成员。

以上就是本期全部内容了。

  • 38
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值