【C语言】自定义类型

目录

结构体

结构体的定义

typedef

匿名结构体

结构体的自引用

结构体变量的初始化

访问结构体成员的方式

结构体的内存对齐

如何设计结构体

位段

枚举

枚举类型的定义

枚举类型的初始化

枚举的优点

共用体

共用体的定义

共用体大小的计算 


结构体

结构体是什么?

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

数组也是一些值的集合,但这些值的类型是相同的。

结构体的定义

struct 结构体名
{
	成员表列;
}变量表列;

定义结构体的例子:

struct Stu
{
	int age;
	char name[20];
}s1,s2;

定义了一种结构体叫 Stu ,它有两个成员:age 和 name ,类型分别是 int 和 char [20],随后定义了两个结构体变量 s1 和 s2。

如果结构体定义在 main 函数之前,那么 s1 和 s2 就是全局变量。结构体也可以在 main 函数内部定义。

定义结构体变量可以在定义结构体时就定义,如 s1 和 s2,也可以在 mian 函数内定义:

struct Stu
	{
		int age;
		char name[20];
	}s1,s2;
int main()
{
	struct Stu s3, s4;
	return 0;
}

typedef

如果每次定义结构体变量的时候不想写 struct ,可以在定义结构体变量时写上一个 typedef,如:

typedef struct student

{...}Stu;

下次定义结构体变量时,就只需要写 Stu s1;而不需要写 struct student s1;

typedef 也可以在定义结构体之后再使用:

struct student

{...};

typedef struct student stu;

 分析以下代码:

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

 代码的原意是将结构体 struct Node 命名为 Node ,在定义结构体时就直接用 Node 来定义 next 指针,但这种做法是不合法的。

匿名结构体

匿名结构体就是在定义结构体时,不指定结构体的名字:

struct 
	{
		int age;
		char name[20];
	}s1,s2;

这种结构体只有在定义结构体后就定义结构体变量,而不能在其他地方定义结构体变量。

分析以下代码:

struct 
	{
		int age;
		char name[20];
	}s1;
struct
	{
		int age;
		char name[20];
	}*p;
int main()
{
	p = &s1;
	return 0;
}

s1 和 p 的结构是一样的,但 p = &s1 ;是不合法的。

结构体的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?

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

 这是不行的,因为这种结构体类型的大小是无穷大的,但是我们可以把 struct Node next 改为 

struct Node * next;这样这种结构体类型的大小就是 8 或 12 了,并且这种结构体就叫作链表。

链表

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

结构体变量的初始化

示例:

struct Stu
	{
		int age;
		char name[20];
	}s1 = { 21,"zhangsan" };

struct Stu s2 = { 12,"wangwu" };
int main()
{
	struct Stu s3 = { 18,"lisi" };
	return 0;
}

即按照定义结构体时成员变量的顺序,将要初始化的内容用大括号括起来,并且用逗号隔开。

如果结构体内定义有结构体,则初始化这种结构体时对结构体内的成员结构体初始化要加大括号:

struct A
	{
		int a;
		char arr1[20];
	};

struct B
	{
		int b;
		struct A a1;
		char arr2[20];
	};

struct B b1 = { 12,{30,"hehe"},"haha" };

 可以不严格地按照定义结构体时成员的顺序来初始化,用成员运算符即可:

struct B b1 = { .a1.a = 30,.a1.arr1 = "hehe",.b = 12,.arr2 = "haha" };

访问结构体成员的方式

使用成员运算符 ‘ . ' 访问:

用上面的结构体变量 b1 为例,用 printf 输出 b1 的内容:

printf("%d %d %s %s\n", b1.b, b1.a1.a, b1.a1.arr1, b1.arr2);

即:结构体变量名 . 结构体成员名,如果要访问结构体内的结构体变量,则需要使用多个成员运算符。

结构体的内存对齐

先来看一个例子:

struct A
	{
		char b;
		int a;
		char c;
	};

struct B
	{
		char b;
		char c;
		int a;
	};


int main()
{
	printf("%d\n",sizeof(struct A));
	printf("%d\n",sizeof(struct B));
	return 0;
}

最后打印的结果是:12 和 8。

产生了两个疑问:

1、为什么结构体 A 和 B 的成员类型都是一样的,只是顺序不同,但打印的结果不同。

2、为什么打印的结果不是 6 而是 12 和 8 ?

结构体的对齐规则

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

2. 其他成员变量要对齐到某个数字 ( 对齐数 ) 的整数倍的地址处。

对齐数 = 编译器默认的一个对齐数与该成员大小的较小值(如果该成员是数组类型,就将数组元素的类型与编译器默认的一个对齐数比较)。
ㅇVS中默认对齐数为 8

3. 结构体总大小为每个成员的对齐数的最大值 (包括第一个成员 ) 与默认对齐数取小后的数的整数倍。

4. 如果嵌套了结构体的情况 , 嵌套的结构体对齐到自己的每个成员的对齐数的最大值的整数倍处 , 结构体的整体大小就是所有成员的对齐数的最大值 ( 含嵌套结构体的对齐数, 嵌套结构体的对齐数就是自己的每个成员的对齐数的最大值) 的整数倍。

易错:第一个成员的偏移量是 0 ! 

具体解释:

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

所谓的偏移量,就是指某个内存单元与结构体变量第一个成员的存储单元的距离。

拿上面的 struct A 举例:

第一个成员的类型占多少字节就占多少格子,如果 struct A 的第一个成员是 int 类型的,就占 4 个格子。

2. 其他成员变量要对齐到某个数字 ( 对齐数 ) 的整数倍的地址处。

对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
ㅇVS中默认对齐数为 8

也就是从第二个成员开始 , 每个成员都要对齐到 ( 一个对齐数 ) 的整数倍处

对齐数 : 结构体成员自身大小和默认对齐数的较小值

VS : 默认对齐数是 8

Linux gcc :  没有默认对齐数 , 对齐数就是结构体成员的自身大小

struct A 的第二个成员,也就是 int a,a 的大小是 4 个字节,现在使用 VS ,默认对齐数是 8,取较小值 4,也就是说 a 的存储空间的偏移量只要是 4 的最小倍数就行了,char c 同理。

 3. 结构体总大小为每个成员的对齐数的最大值 (包括第一个成员 ) 的整数倍。

 这就非常好理解了,struct A 中 b 的对齐数是 1,a 的对齐数是 4,c 的对齐数也是 1,所以最大对齐数是 4, struct A 的总大小应该为 4 的整数倍,而现在 struct A 的总大小是 9,因此 struct A 应该再占 3 个字节,总大小变为 12 才对。

这就是为什么 struct A 的总大小是 12 而不是 6 的原因,同样,为什么结构体 A 和 B 的成员类型都是一样的,只是顺序不同,但大小不同也能解释了。

使用 offsetof 验证

offsetof 是一个宏,使用它需要包含头文件:#include <stddef.h>

它是用来计算结构体成员的偏移量的,它有两个参数,第一个参数是结构体名,第二个参数是结构体成员名,函数返回值就是偏移量,且是整形。以 struct A 为例:

为什么要内存对齐? 

大部分的参考资料都是这样说的:

1. 平台原因 ( 移植原因 ) :

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

2. 性能原因 :

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

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

如何设计结构体

那在设计结构体的时候 , 我们既要满足对齐 , 又要节省空间 , 如何做到?

让占用空间小的成员尽量集中在一起。

例如上述的 struct A 和 struct B,它们的成员一模一样,只是顺序不同,但两者的大小有所区别,struct A 占 12 个字节,而 struct B 只占 8 个字节。

修改默认对齐数

可以使用 #pragma pack( )修改默认对齐数:

#pragma pack(1)//修改默认对齐数为 1
struct B
	{
		char b;
		char c;
		int a;
	};
#pragma pack()//恢复默认对齐数为 8

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

结构体传参

结构体传参建议将函数参数设置为结构体指针变量

原因:函数传参的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

位段

位段的定义

位段的定义和结构体是类似的 , 但有两个不同:

1.位段的成员类型必须是 int、unsigned int 、char、unsigned char 。
2.位段的成员名后边有一个冒号和一个数字(数字称为位,位的单位是比特,位不能超过该类型的大小)

例子:

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

A 就是一个位段 ,_a 是命名习惯。

位段的作用(优点)

有时候一个结构体中的 int 类型的数据不需要 32 个 bit 位来存储数据,而只需要少于 32 个 bit 位就可以完整的存储数据,此时就可以使用位段来节省空间,即指定一个 int 类型的变量的实际大小。上述 A 位段中,a 变量的实际大小就是 1 bit。

位段的空间分配

以上述位段 A 为例:先开辟一个 int 类型,即 32 个 bit 位,_a 使用这 32 个 bit 位的 1 位,而到底是从左开始使用还是从右开始使用取决于编译器(VS 从右开始使用),_b 使用 5 位,_c 使用 10 位,还剩 16 位,而 _d 要使用 30 个 bit 位,明显不够,再开辟一个 int 类型,即 32 位,而 _d 到底是使用剩余的 16 位和新开辟的空间的 14 位,还是直接全部使用新开辟的空间(VS 使用这种方式)取决于编译器。

位段的缺点

1、位段中 int 类型被当成是无符号还是有符号是不确定的。

2、在 16 位机器中 int 类型大小是 16 bit,32 位和 64 位机器中 int 类型的大小是 32 bit,假如指定 位为 27,在 16 位机器上会出现问题。

3、对于新开辟的空间,到底是从左开始使用还是从右开始使用取决于编译器。

4、当空间不够用时,到底是使用剩余的位和新开辟的空间的位,还是直接全部使用新开辟的空间的位取决于编译器。

枚举

枚举类型的定义

enum A
{
	a,b,c
};

枚举类型的初始化

A 是枚举类型名,a,b,c 是 A 枚举类型的可能取值,默认为 0,1,2,3...... ,也可以人为指定它们的数值,如 a = 1,b = 2,c = 3。

如果只初始化 a 的值为 -5 ,则 a 和 b 和 c 会依次递增 1 ,即 b = -4,c = -3。

如果初始化 b 的值为 5,则 a 为 0,c 为 6。

枚举类型的成员的数值一但确定就无法在其他地方更改,比如在 main 函数中令 a = 2 是不合法的。

枚举的优点


我们可以使用 #define 定义常量,为什么非要使用枚举?
1. 增加代码的可读性和可维护性(switch 语句的 case)

2. 和 #define 定义的标识符比较,枚举有类型检查,更加严谨。

3. 防止了命名污染 ( 封装 )

4. 便于调试(#define 定义的标识符在运行时已经被替换)

5. 使用方便,一次可以定义多个常量
 

共用体

共用体的定义

定义的基本形式与结构体相似: 

union A
{
    int a;
    char b;
};

可以看到成员 c 和 i 共用同一段内存。 

共用体大小的计算 

共用体也存在内存对齐

1、共用体的大小至少是最大成员的大小。

2、当最大成员大小不是最大对齐数的整数倍的时候 , 就要对齐到最大对齐数的整数倍。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值