自定义类型:结构体


在日常生活中,常常会遇到比较复杂的事务,而简单的使用C语言提供的内置类型:int、char、float等类型不足以描述,比如当我们描述一个学生时,需要描述姓名、年龄、学号等等

1.结构体类型

1.1 结构的声明

结构体的原型如下:

struct tag//类型名
{
	member-list;//成员列表
}variable-list;//变量列表(是全局变量)

例如:描述一个学生

struct Stu
{
	char name[20];
	int age;
	int id[10];
};//分号不要丢!!!
  • 其中struct Stu是该结构体的类型名,就像int、char一样是个类型,只不过结构体的类型是自定义的
  • 结构体内的是成员变量,就是一个复杂个体的细分,用来描述复杂个体,成员变量是可以用不同类型的变量
  • 而最后面的变量列表可以不列出,在需要的时候再定义

1.2 结构体成员的定义和初始化

下面以实例来演示结构体成员的定义和初始化

struct Stu1
{
	char name[20];
	int age;
	int id[10];
}s1 = {"zhangsan", 20, 453789};

//一般情况下,需按照成员的顺序依次进行初始化
struct Stu1 s2 = { "lisi", 18, 5793168 };

//如果想乱序进行初始化,可以用成员访问符来指认将哪个成员进行初始化
struct Stu1 s3 = { .age = 35, .id = 861753, .name = "wangwu" };

struct Stu2
{
	char name[20];
	int age;
	struct Stu2* next;//指针
	struct Stu1;//嵌套结构体
}s4 = { "haha", 36, NULL, {"hehe", 20, 4586317} };

1.3 结构体成员访问操作符

结构体里有很多的成员,如果我们需要将他们或他们中的一部分取出来用的时候,该怎么做呢?这时候就需要用到了结构体成员访问操作符

1.3.1 直接访问

结构体直接访问操作符为“.”,通过“.”我们可以直接访问到结构里的某一个成员,使⽤⽅式:结构体变量.成员名

比如:

#include <stdio.h>

struct Stu1
{
	char name[20];
	int age;
	int id[10];
};

int main()
{
	struct Stu1 s = { "zhangsan", 20, 861753 };
	printf("%s\n", s.name);
	printf("%d\n", s.age);
	return 0;
}

1.3.2 间接访问

同样的,我们可以用指针的形式来间接访问结构体里的成员,这时就需要用到“->”操作符,使用方式:指向结构体变量的指针->成员

比如:

#include <stdio.h>

struct Stu1
{
	char name[20];
	int age;
	int id[10];
};

int main()
{
	struct Stu1 s = { "zhangsan", 20, 861753 };
	struct Stu1* ps = &s;
	printf("%s\n", ps->name);
	printf("%d\n", ps->age);
	return 0;
}

1.4 匿名结构体

什么是匿名结构体呢?匿名的又是什么呢?匿名结构体有什么用途吗?
我们先来看看匿名结构体长什么样

struct
{
	char name[20];
	int age;
	int id[10];
}s1;

这就是匿名结构体,有发现它与一般的结构体有什么区别吗?我们不难发现匿名结构体没有标签名(tag),这就意味着,结构体类型不完整,不能在后续使用该结构体,如果要用匿名结构体定义变量时,只能在结构体末尾处定义,就像上面定义s1一样。所以,如果没有用typedef将结构体重命名的话,该匿名结构体类型只能使用这一次

struct s2 = { "zhangsan", 16, 915765 };

而s2定义方式就是错误的!

1.5 结构的自引用

如果在结构里包含一个类型为结构体类型的成员,这样做可以吗?
比如,定义一个链表的节点:

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

其实这样是不可以的,结构体中不能包含自身类型的成员变量,因为这样会导致结构体的大小无限增长,编译器无法确定结构体的大小。如果想实现链表,应该使用指针来指向下一节点,而不是直接包含下一个节点。正确的代码如下:

struct Node
{
	int data;//当前节点的数据
	struct Node* next;//下一节点的地址
};

画图来演示一下该链表:
基本形式:
在这里插入图片描述
串起来:
在这里插入图片描述

typedef重命名自引用类型

除此之外,使用typedef重命名自引用结构体类型,也容易引起问题
分析下面的代码,可行吗?

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

答案是不可行的,这是因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用了Node类型来创建了成员变量,这是不可行的!

因此, 匿名结构体是不能实现这种的结构体自引用的效果的!!!

正确的使用方法:

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

注:typedef重命名结构体时的典型错误分析

错误示例:

typedef SL sl;

typedef struct SeqList
{
	int a;
}SL;

分析:在这段代码中,结构体SeqList被定义在类型别名SL之后,这样在定义sl类型时,编译器并不知道SL是什么。这可能导致编译器报错或警告。
正确示例:

  1. 在结构体定义后再重命名结构体
typedef struct SeqList
{
	int a;
}SL;

typedef SL sl;
  1. 写原始结构体名,有了struct,typedef就会知道重命名的是结构体
typedef struct SeqList sl;

typedef struct SeqList
{
	int a;
}SL;

2. 结构体的内存对齐

当我们了解了结构体的基本概念后,在日常使用中我们还会使用到结构体的大小。这时,我们需要先了解结构体的内存对齐,才能计算出结构体的大小

结构体内存对齐规则:

  • 结构体的第一个成员必须对齐到结构体变量起始位置偏移量为0的地址处
  • 其他成员变量要对齐到它的对齐数的整数倍数处
    对齐数—编译器默认的对齐数和该成员类型大小的较小值
    在VS中,默认对齐数是8
  • 结构体的总大小为最大对齐数(每个成员变量都有一个对齐数,最大对齐数就是所有对齐数中最大的)的整数倍
  • 如果嵌套了结构体,那么它的对齐数就是——嵌套结构体里各成员中最大的对齐数

这些概念看起来都是比较模糊的,下面我们用实例和画图来加深对概念的理解

例题1

#include <stdio.h>

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

int main()
{
	printf("S1:%zd\n", sizeof(struct S1));
	return 0;
}

分析:

  • c1是第一个成员变量,从编号为0的位置开始往下存储,又因为是char类型,占一个字节
  • i是int类型,大小为4,编译器默认对齐数为8,两者较小值为4,因此i的对齐数为4,所以i的起始位置在为4的整数倍处,即从编号为4的位置开始存储,又因为i是int类型,占四个字节
  • c2是char类型,大小为1,编译器默认对齐数为8,两者较小值为1,因此c2的对齐数为1,所以c2的起始位置在为1的整数倍处,即从编号为8的位置开始,又因为c2是char类型,占一个字节
  • 各成员变量“存储”完成后,看一共占了几个字节,看是否为最大对齐数的整数倍,在这个结构体中,各成员变量对齐数分别为1、4、1,最大对齐数为4,原共占9个字节,不是最大对齐数的整数倍,那么继续往后占用,直至编号为11的位置,这时一共占了12个字节,为最大对齐数4的整数倍

注: 为了方便讲解,上述的“编号”都对应于概念中的“距初始位置的偏移量”

图示如下:
在这里插入图片描述
故结果为12

例题2

#include <stdio.h>
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	printf("S1:%zd\n", sizeof(struct S2));
	return 0;
}

运行结果为8,请读者拿起笔画表来演算

例题3

#include <stdio.h>
struct S3
{
	double d;
	char c;
	int i;
};
int main()
{
	printf("S1:%zd\n", sizeof(struct S3));
	return 0;
}

再来演算一道,运行结果为16

图示:
在这里插入图片描述

例题4

下面我们来看看有嵌套结构体的例子

#include <stdio.h>
struct S3
{
	double d;
	char c;
	int i;
};

struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

int main()
{
	printf("S1:%zd\n", sizeof(struct S4));
	return 0;
}

分析:

  • 由例2可知,结构体S3的大小为16,结构体中S3的最大对齐数为double类型——为8,所以struct S3的对齐数为8

图示:
在这里插入图片描述
故结果为32

在设计结构体时,我们可以将占用空间小的成员集中在一起,这样既能满足对齐,又能节省空间

修改默认对齐数

我们在前面了解到了不同地编译器,它的默认对齐数是不一样的,例如:在VS中,默认对齐数是8,如果结构体的对齐方式不合适的时候,我们可以自己修改它的默认对齐数,这时就需要用到 #progma 这个预处理指令
例如:

#include <stdio.h>

#pragma pack(1)//设置默认对齐数为1

struct S
{
	char c1;
	int i;
	char c2;
};

#pragma pack()//取消设置的默认对齐数,还原为默认

int main()
{
	printf("%zd\n", sizeof(struct S));
	return 0;
}

这时的运行结果就不再是12了,而是6了!

3. 结构体实现位段

位段的声明

位段的声明和结构体是类似的,但是又与结构体有以下的区别:

  • 位段成员的类型必须是int、signed int、unsigned int、char
  • 位段成员后面有一个冒号和数字,而这个数字就是要保存数字的二进制位的位数
  • 位段的空间是按照需要以4个字节(int)或者1个字节(char类型)的方式来开辟的

例如:

struct A
{
	int a : 2;
	int b : 5;
	int c : 8;
};

计算位段的大小

了解位段的概念后,那么位段的大小是如何计算的呢?是否与结构类似呢?同样地,我们带着疑问,通过实例和图示来分析。下面以VS编译器环境下为例

#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\n", sizeof(struct S));
	return 0;
}

图示:
在这里插入图片描述
由上图可知,结构体一共占据了三个方框,也就是3个字节,故代码运行结果应该为3!

位段的跨平台问题

在前面的例子中,是在VS编译器下执行得到的结果,但是在不同的编译器下,运行结果可能不同,这是因为位段的跨平台问题

  • int位段被当成有符号数还是无符号数是不确定的
  • 位段中最大位的数目是不能确定的。(16位机器最大是16,32位机器最大是32,写成27,在16位机器会出问题)
  • 位段中的成员在内存中是从左向右分配,还是从右向左分配
  • 当一个结构包含两个位段时,第二个位段成员比较大,无法容纳第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的

在VS编译器底下,位段中的成员在内存中从左向右分配的;当一个结构包含两个位段时,第二个位段成员比较大,无法容纳第一个位段剩余的位时,舍弃剩余的位

位段的不可取地址性

由于位段中一个字节可能存在好几个成员,而内存中每一个字节分配一个地址,并且一个字节内部的比特位是没有地址的,这时就会存在部分成员没有地址,因此位段是不能进行取地址操作的!

那么,如果我们想给位段里的成员通过scanf输入值,该怎么办呢?这时,我们可以通过间接赋值来给各成员输值

例如:

#include <stdio.h>

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

int main()
{
	struct S s = { 0 };
	int temp = 0;
	scanf("%d", &temp);
	s.a = temp;//通过temp间接给位段成员输值
	printf("%d\n", s.a);
	return 0;
}

在这里插入图片描述

  • 34
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值