【C语言】自定义类型:结构体|位段|联合体

目录

1.结构体类型

1.1结构

1.2匿名结构体类型

1.3结构体自引用

1.4结构体类型重定义 typedef

1.5结构体变量定义和初始化

1.6结构体内存对齐

1.7结构体传参

2.位段

2.1位段的定义与声明

2.2位段的内存分配 

2.3位段的跨平台问题 

3.枚举

3.1枚举类型的定义

3.2枚举的优点

3.3枚举的使用

4.联合(共同体)

4.1联合的定义与声明

4.2联合的特点

4.3联合大小的计算


1.结构体类型

1.1结构

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

对于一个学生,其相关属性有:姓名,年龄

以下为创建一个学生的结构:

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

	return 0;
}

其中,struct Stu是结构体类型,s1,s2是我们创建的结构体全局变量 (非必要,可以使用时再创建),main函数中s3是我们创建二点结构体局部变量,s1,s2,s3这三个变量的类型都是struct Stu.

1.2匿名结构体类型

匿名结构体类型,创建方式如下:

struct 
{
	char name[20];
	int age;
}s;

匿名结构体类型只能使用一次,就是在声明时创建变量,后续只能使用这个变量,而不能创建这个结构体类型的其他变量

我们看以下一个例子:

匿名结构体1:

struct 
{
	int a;
	char b;
	float c;
}x;

 匿名结构体2:

struct
{
	int a;
	char b;
	float c;
}a[10],*p;

匿名结构体2中:a[10]是一个有十个结构体类型元素的数组,p是一个结构体指针,指向这个匿名结构体

可以发现,这两个结构体的结构成员完全相同 ,那是否证明这两个结构体一样呢?

当我们在VS编译器中运行 p = &x 时,发现编译器报警告

从"*"到"*"的类型不兼容,第一个"*"是&x,x的类型匿名结构体1类型,第二个"*"是p,p是一个结构体指针,指向匿名结构体2 

报警告即说明编译器把这两个声明当作完全不同的两个类型,所以操作非法

由此可见,即使两个匿名结构体的成员变量完全相同,它们也是完全不同的结构体

1.3结构体自引用

顺序表:顺序表的内存空间连续,例如一位数组,二维数组
链表:链表的内存空间不连续。如下图:

链表是如何实现的呢?我们可以使用结构体模拟其过程

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

这个结构体中,成员变量有一个int类型的数据data,还有一个结构体指针类型的变量p, 这两个成员变量分别代表一个节点中的数据域和指针域,对于指针p,它指向一个结构体类型,这个结构体类型就是其所属的结构体

以上过程就是结构体的自引用,可以发现自引用时,包含用类型指针,而不是类型本身

1.4结构体类型重定义 typedef

代码1:

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

int main()
{
	Node d;
	
	return 0;
}

代码2:

typedef struct Node
{
	int data;
	struct Node* next;
}* linklist;
int main()
{
	linklist p;

	return 0;
}

代码1:

我们把结构体类型struct Node类型重定义为Node,所以后续创建结构体变量时 Node s;和struct Node s;是等价的

代码2:

我们把结构体类型指针struct Node*类型重定义为linklist,所以后续创建结构体变量时 linklist s;和struct Node* s;是等价的

1.5结构体变量定义和初始化

结构体变量创建有两种方式,一种是类型定义是创建,另一种是在需要使用是在函数内创建,如下代码:p1,p2,p3都是我们创建的结构体变量,不同之处在于p1,p2是结构体全局变量,p3是结构体局部变量,作用域是main函数内

struct Point
{
	int x;
	int y;
}p1,p2;
int main()
{
	struct Point p3 = { 2,3 };

	return 0;
}

结构体变量初始化也有两种方式,一种是类型定义是创建并初始化,另一种是在需要使用是在函数内创建并初始化,如下代码:

#include<stdio.h>
struct Point
{
	int x;
	int y;
}p1,p2;
struct Stu 
{
	char name[10];
	int age;
	struct Point p;
}s1 = { "zhangsan",18,{2,3} };
int main()
{
	//初始化
	struct Stu s2 = { "lisi",20,{2,5} };
	//d打印
	printf("%s %d %d %d\n", s1.name, s1.age, s1.p.x, s1.p.y);
	printf("%s %d %d %d\n", s2.name, s2.age, s2.p.x, s2.p.y);

	return 0;
}

 输出如下:

以上代码表明结构体可以进行嵌套使用 

1.6结构体内存对齐

计算结构体的大小需要了解结构体内存对齐规则

结构体内存对齐规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处对齐数=min{编译器默认的对齐数,成员大小}。VS编译器中默认对齐数为8
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
  4. 嵌套结构体情况:嵌套结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体)的整数倍。

计算以下结构体的大小:

#include<stdio.h>
struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{

	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));

	return 0;
}

可以发现,这两个结构体的成员变量相同,但成员变量定义的顺序不同 

根据结构体内存对齐规则,我们进行以下分析:

S1:

如上图,我们标出了每一块内存空间的相对偏移量

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

c1是一个char类型的变量,c1的大小为1byte

 2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处对齐数=min{编译器默认的对齐数,成员大小}。VS编译器中默认对齐数为8

i是一个int 类型的变量,其大小为4byte,所以变量i对齐数 = min{8,4} = 4,变量i要对齐到4的整数倍的地址处,距离第一个变量最近的地址且为4的整数倍的地址为偏移量为4的地方

c2是一个char 类型的变量,其大小为1byte,所以变量c1对齐数 = min{8,1} = 1 ,变量i要对齐到1的整数倍的地址处,偏移量为8的地方满足要求

 3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍

结构体的总大小 = n*max{1,4,1} = 4n 

距离最后一个成员变量最近的地址且为4的整数倍的地址为偏移量为12的地方

所以结构体总大小为12byte

S2:

如上图,我们标出了每一块内存空间的相对偏移量

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

c1是一个char类型的变量,c1的大小为1byte 

 2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处对齐数=min{编译器默认的对齐数,成员大小}。VS编译器中默认对齐数为8

注:其他编译器(GCC)没有对齐数,成员对齐数为自身大小

c2是一个char类型的变量,其大小为1byte,所以变量c2对齐数 = min{8,1} = 1,变量c2要对齐到1的整数倍的地址处,偏移量为1的地方满足要求

 i是一个int 类型的变量,其大小为4byte,所以变量i对齐数 = min{8,4} = 4,变量i要对齐到4的整数倍的地址处,距离第一个变量最近的地址且为4的整数倍的地址为偏移量为4的地方

 3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍

结构体的总大小 = n*max{1,1,4} = 4n  

距离最后一个成员变量最近的地址且为4的整数倍的地址为偏移量为8的地方

所以结构体总大小为8byte

C语言中具有函数形式的宏offsetof可以返回结构体成员的偏移量

以上述S1和S2为例:

S1:

S2:

以下我们再计算几个结构体的大小 

例1:

struct S3
{
	double d;
	char c;
	int i;
};

 

如上图,我们标出了每一块内存空间的相对偏移量

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

d是一个double类型的变量,d的大小为8byte

2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处对齐数=min{编译器默认的对齐数,成员大小}。VS编译器中默认对齐数为8

c是一个char 类型的变量,其大小为1byte,所以变量c1对齐数 = min{8,1} = 1 ,变量i要对齐到1的整数倍的地址处,偏移量为8的地方满足要求

i是一个int类型的变量,其大小为4byte,所以变量i对齐数 = min{8,4} = 4 ,变量i要对齐到4的整数倍的地址处,距离上一个变量最近的地址且为4的整数倍的地址为偏移量为12的地方

 3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍

结构体的总大小 = n*max{8,1,4} = 8n 

距离最后一个成员变量最近的地址且为8的整数倍的地址为偏移量为16的地方

所以结构体总大小为16byte

struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3;
	double d1;
};

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

c1是一个char 类型的变量,其大小为1byte

2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处对齐数=min{编译器默认的对齐数,成员大小}。VS编译器中默认对齐数为8

S3是一个结构体类型的变量,其大小为16byte,所以变量S3对齐数 = min{8,16} = 8 ,变量S3要对齐到8的整数倍的地址处,距离上一个变量最近的地址且为8的整数倍的地址为偏移量为8的地方

d1是一个double类型的变量,d的大小为8byte,所以变量d对齐数 = min{8,8} = 8 ,变量d1要对齐到的整数倍的地址处,距离上一个变量最近的地址且为8的整数倍的地址为偏移量为24的地方

 3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍

结构体的总大小 = n*max{1,16,8} = 16n 

距离最后一个成员变量最近的地址且为16的整数倍的地址为偏移量为32的地方

所以结构体总大小为32byte

为什么存在内存对齐? 

1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,一些硬件平台只能在某地址处取某些特定类型的数据,否则抛出硬件异常

2.性能原因:数据结构(尤其是栈)应该尽可能再自然边界上对齐,因为为了访问为对齐的内存,处理器需要作两次内存访问,而对齐的内存只需要访问一次

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

设计结构时,应满足占用空间小+内存对齐

基于所设计的功能,让占用空间小的成员尽量集中在一起

默认对齐数修改:#pragma pack(4)//将默认对齐数修改为4

#pragma pack( )//对齐数恢复原值

#pragma pack(1)//将默认对齐数修改为1,数据可以挨个存放

1.7结构体传参

结构传参有两种方式:传值调用和传址调用

struct S
{
	int data[100];
	int num;
};
void Print1(struct S ss)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", ss.data[i]);
	}
	printf("%d\n", ss.num);
}
void Print2(struct S* ps)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", ps->data[i]);
	}
	printf("%d\n", ps->num);
}
int main()
{
	struct S s = { {1,2,3},100 };
	Print1(s);
	Print2(&s);

	return 0;
}

Print1函数和Print2函数都可以实现结构体内容的打印,但是我们应首选Print2函数

原因:函数传参时,参数需要压栈,会有时间和空间上的系统开销,如果传递一个结构体对象时,结构体过大,参数压栈的系统开销比较大,所以会导致性能下降,所以结构体传参应首选结构体地址 

2.位段

2.1位段的定义与声明

我们一般说,结构体实现位段的能力

位段的声明和结构体类似,有以下俩个不同:

  1. 位段的成员必须是整型家族类型的数据,例如int,signed int,unsigned int......
  2. 位段的成员名后边还有一个冒号和一个数字
struct A
{
	int _a : 2;//_a占2bit
	int _b : 5;//_b占5bit
	int _c : 10;//_c占10bit
	int _d : 30;//_d占30bit
};

2.2位段的内存分配 

位段的内存分配:

  1. 位段的成员是整型家族类型的数据,例如int,char,unsigned int......
  2. 位段在空间上是按照需要以4byte(int)或1byte(char)的方式开辟的
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应避免使用位段

我们举例说明位段如何计算大小

例1:

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("%d\n", sizeof(struct S));

	return 0;
}

定义char a : 3;

首先开辟8bit,a占用3bit

位段在使用空间时从低位到高位使用

定义char b : 4;//b占用4bit

定义char c : 5;

开辟的这一个字节仅剩余1bit,不够变量c使用,所以这1bit浪费掉

重新开辟1byte = 8bit

定义char d : 4;

开辟的这一个字节仅剩余3bit,不够变量d使用,所以这3bit浪费掉

重新开辟1byte = 8bit

综上所述:这个位段的大小为3byte 

s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;

这3byte的内容为:0 1100 010 000 00011 0000 0100

注:a只使用了3bite,数据会发生丢失

十六进制形式为:62 03 04

在VS编译器下,数据是小端存储模式

2.3位段的跨平台问题 

  1. int位段被当成有符号数/无符号数是不确定的
  2. 位段中最大位的数目不能确定(16位机器最大16位,32位机器最大32位;int类型的在16位机器上为2byte,在32位机器上为4byte,写成27,16位的机器会出问题)
  3. 位段中的成员在内存中从左向右分配/从右向左分配标准为定义
  4. 当一个结构包含俩个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用是不确定的

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

3.枚举

3.1枚举类型的定义

枚举:就是列举,把可能的取值一一列举

例如:星期,性别,三原色......

和结构体相同,枚举类型不占用空间,只有使用时才开辟空间

enum Day
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};
int main()
{
	enum Day d = Fri;

	return 0;
}

枚举类型中,枚举常量的取值默认从0开始

修改:Mon = 1,可以修改默认值从1开始

3.2枚举的优点

  • 枚举可以增加代码的可读性和可维护性

在通讯录中,我们打印菜单函数,用户进行输入选择,可以进行优化如下

enum option
{
	EXIT,//默认取值为0
	ADD,
	DELETE,
	SEARCH,
	MODIFY,
	SHOW,
	SORT
};

在switch语句中,我们直接可以使用枚举常量

switch(input)

{

case EXIT:

case ADD:

case DElETE:

... ... ...

}

  • 和#define定义的标识符比较,枚举有类型检查,更加严谨 
  •  防止了命名污染,因为类型有封装
  • 便于调试

#define定义的标识符在预处理阶段会用值替换变量,当我们调试时,变量已经被替换,可能与代码的逻辑产生参差

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

3.3枚举的使用

枚举使用时要注意:只能拿枚举常量给枚举变量赋值,才不会出现类型的差异

例:enum Day d = 5;//cpp语言会报错

4.联合(共同体)

4.1联合的定义与声明

联合:这种类型定义的变量也包含一系列成员,特征是这些成员共用一块空间,所以联合也叫共同体

联合声明 

union Un
{
	int a;
	char c;
};

4.2联合的特点

由上图:可以发现联合所有成员共用一块内存空间,这样一个联合变量的大小,至少是最大成员的大小,因为联合至少有能力保存最大的成员

联合可以节省空间,但也导致其某些成员变量不能同时使用

使用联合可以判断当前机器的存储模式是大端还是小端

创建一个联合

union Un
{
	int a;
	char b;
};

 

union Un
{
	int a;
	char b;
};
int check_system()
{
	union Un u;
	u.a = 1;
	return u.b;//只访问第一个字节
}

int main()
{
	int ret = check_system();
	if (ret == 1)
	{
		printf("小端存储\n");
	}
	else
	{
		printf("大端存储\n");

	}

	return 0;
}

4.3联合大小的计算

定义一个联合如下:

union Un 
{
	char arr[5];//对齐数 = min{8,1} = 1
	int i;//对齐数 = min{8,4} = 4
};

对齐规则:当联合中最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍

最大成员大小:1byte*5 = 5byte 

最大对齐数 = max{1,4} = 4

对齐到偏移量为4n处,且有能力保存最大的成员,所以联合大小为8byte

union Un 
{
	short arr[5];//对齐数 = min{8,2} = 2 
	int i;//对齐数 = min{8,4} = 4
};

最大成员大小:2byte*7 = 14byte

最大对齐数 = max{1,4} = 4

对齐到偏移量为4n处,且有能力保存最大的成员,所以联合大小为16byte

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值