【C语言】结构体、枚举和联合体

1、结构体

  聚合数据类型能够同时存储超过一个的单独数据,C语言有两种这种类型,数组和结构体。数组是相同类型的集合,而结构体能包括不同的类型。

1.1、结构体的声明

struct tag
{
 member-list;
}variable-list;

如果我们需要描述一个学生:
那他应该有着姓名、年龄、性别、学号这些元素。

struct stu
{
	char name[20];
	int age;
	char sex[10];
	char id[15];
}; //注意分号不能丢

1.2、特殊的声明

在声明结构体类型的时候,可以不完全的声明。
如:
以下这种不加标签(tag)的声明,叫做匿名结构体

struct
{
	char name[20];
	int age;
	char sex[10];
	char id[15];
} x;
//这个声明创建了一个x变量,它包含四个成员变量。
struct
{
	char name[20];
	int age;
	char sex[10];
	char id[15];
} a[20],*p;
//这个声明创建了a和p,a是一个指向了20个这样结构体的数组,p作为指针指向结构体。

这次声明省去了结构体标签(tag),并且在结构体声明之后创建了变量。
值得注意的是:

这两个声明会被编译器当成两个截然不同的类型,虽然成员一样,但如果进行
p=&x操作,程序会报错,原因是它们之间的类型不同。

但是
如果给结构体加上标签,标签允许多个声明使用同一个成员列表,并且创建同一种类型的结构。
如:

struct stu
{
	char name[20];
	int age;
	char sex[10];
	char id[15];
} ;

struct stu x;
struct stu a[20], *p;

现在如果使用p=&x,这是合法的。

1.3、结构体的自引用

看下面一个代码是否正确

struct Node
{
	int a;
	struct Node next;
};

这显然是不正确的,如果正确那么sizeof(struct Node)将是算不清的,因为next是无限伸展的。
下面改进一下。

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

通过改为指针,这样不仅能自引用,也能确定类型struct Node的大小。



我们看到结构体的类型是否有点长?
通过typedef,为结构体定义一个新的类型名,能够更加方便。
比如:

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

通过typedef,将struct Node简化为Node。(注意Node是一个新的类型名,不是变量。)



下面看一个代码是否有问题。

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

这段代码省略了标签,因为是先声明结构体类型再typedef定义新类型名的,所以在声明结构体类型的时候并不知道Node是什么类型,因此这个代码是错误的。

//解决方案:
typedef struct Node
{
 	int data;
 	struct Node* next;
}Node;

先声明结构体类型再typedef定义新类型名,声明没问题,类型名定义也自然没问题。

1.4、结构体变量定义和初始化

第一种:
在声明后的尾部,定义加上初始化。

struct Point
{
 	int x;
 	int y;
}; 

struct Node
{
 	int data;
 	struct Point p;
 	struct Node* next; 
}n1 = {10, {4,5}, NULL}; 

第二种:
在声明后,通过类型+标识符的格式定义,再到=后初始化。

struct Stu        
{
 	char name[15];
 	int age;      
};

struct Stu s = {"zhangsan", 20};//初始化

1.5、结构体内存对齐

在结构体中,如果要求一个结构体类型的大小,那么必须考虑它的内存对齐。

struct S1
{
 	char c1;
 	int i;
 	char c2;
};
printf("%d\n", sizeof(struct S1)); //这个结果是12

你可能会纳闷为什么是12而不是6。
下面来看这个问题
在计算之前,我们得看看内存对齐的规则:

  1. 第一个成员默认从0地址处开始计算。
  2. 其它成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
  3. 对齐数 = 编译器默认(vs默认为8)的一个对齐数与该成员大小的较小值
  4. 结构体总大小为最大对齐数的整数倍。
  5. 如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套的)的整数倍。

下面来分析

struct S1
{
//从0开始,对齐数是1,所以对齐在1的整数倍地址处,0也是1的整数倍,所以存储在0地址的位置。
 	char c1;
//从1开始,对齐数是4(4<8),所以对齐在4的整数倍地址处,1不是,最少4才是,所以从4地址处开始
//4~7(int 4个字节),这时候总共8个字节了。
 	int i;
//从8开始,对齐数也是1,所以在8地址处存储。
 	char c2;
};

c1、i、c2分别在0、4~7、8地址处存储,目前总共9个字节,又因为第4条规则,最大对齐数为4,所以总大小应该为12。
在1~4、9 ~ 12这些空间不使用。

下面再计算以下大小

struct S3
{
 	double d;//0~7 对齐数为8 (8==8),0是8的整数倍
 	char c; //8  对齐数为1 (1<8),8是1的整数倍
 	int i;//12~15 对齐数为4 (4<8),12才是4的整数倍 9不是
 //16是最大对齐数8的整数倍,所以总共 16字节
};
printf("%d\n", sizeof(struct S3));//16

struct S4
{
 	char c1; //0 对齐数1 (1<8), 0是1的整数倍
 	struct S3 s3; //8~23 对齐数8 (16>8), 8是8的整数倍
 	double d; //24~31 对齐数8 (8==8), 24是8的整数倍
 //0~31 32个字节
 //32是最大对齐数8的整数倍,所以总共32字节。
};

printf("%d\n", sizeof(struct S4)); //32

为什么要有内存对齐?

大部分参考资料是这样说的:
1.平台原因
不是所有的硬件平台都能访问任意地址上任意数据的;某些硬件平台只能再某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

在这里插入图片描述

1.6、修改默认对齐数

通过#pragma pack(对齐数) 可以修改默认对齐数
如:

#pragma pack(1)//设置默认对齐数为1
struct S2
{
 char c1;
 int i;
 char c2;
};

这样结果就是:6

1.7、结构体传参和调用

看代码

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;
}

结构体传值时用 结构体变量.结构体成员名进行调用。
结构体传地址时用 结构体变量->结构体成员名进行调用。

两种传参方式哪个好点?
print1(s);
print2(&s);
答案是第二种

因为函数在传参的时候,参数是会压栈的,结构体传地址相对于传值,地址大小通常是更小的,所有时间和空间上的开销是比较小的。

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

2、位段

2.1、介绍位段

位段的声明是由结构体来实现的。
位段的特点:

  1. 位段的成员类型只有unsigned int、signed int、char和int。
  2. 位段的成员名后边有一个冒号和一个数字。
struct A
{
 int _a:2;
 int _b:5;
 int _c:10;
 int _d:30;
};

A就是一个位段。
下面通过解决问题来深入了解它。
A的大小是多少?

2.2、位段的内存分配

  • 位段的成员类型只有unsigned int、signed int、char和int。
  • 所以它的内存分配一次只有4个字节或者1个字节(char)。
  • 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
//一个例子
struct S
{
 char a:3;
 char b:4;
 char c:5;
 char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;

我们来看看位段里的数据存储。
首先 对于char类型,第一次先给s开辟了1个字节。
char a:3; 对应着3个bit位。
所以在第一个字节中,占了3个bit位。
char b:4; 4个bit位 第一个字节还剩5个bit位,所以也能存下。
char c:5; 5个bit位 需要第二次开辟1个字节 在一块新的1个字节空间中占5个bit位。
char d:4; 第二块1个字节空间只剩3个bit空间,所以需要再开辟一块1个字节的空间。
所以总共3个字节。

而在赋值过程中
s.10=10; 但a:3 只有3个bit位,所以1010取断3个bit位,结果为010。
s.b=12; b:4 只有4个bit位,所以1100,结果就是1100。
s.c=3; c:5 有5个bit位,11,结果为00011。
s.d=4; d:4 有4个bit位,100,结果为0100。

在这里插入图片描述

2.3、位段的跨平台问题

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

3、枚举

3.1 枚举类型的定义

枚举由关键词enum构成。
枚举也是列举。
下面是枚举的一些表示。

enum Day//星期
{
 Mon,
 Tues,
 Wed,
 Thur,
 Fri,
 Sat,
 Sun
};
enum Sex//性别
{
 MALE,
 FEMALE,
 SECRET
}enum Color//颜色
{
 RED,
 GREEN,
 BLUE
};

enum Day、enum Sex、enum Color 这些都是类型。
在{}中,都是枚举常量,一般未特定初始化的情况下,第一个枚举常量从0开始,依次增加1。
比如Mon=0,Tues=1,Wed=2…

当然这些取值是可以初始化的。
比如
这里a=1,b=2,c=3,d=200,e=201,f=202。

enum x
{
	a=1,
	b,
	c,
	d=200,
	e,
	f
};

3.2 枚举的优点

我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
  3. 防止了命名污染(封装)
  4. 便于调试 (宏是不会进入调试的)
  5. 使用方便,一次可以定义多个常量

3.3 枚举的使用

enum Color//颜色
{
 RED=1,
 GREEN=2,
 BLUE=4
};
int main()
{
	enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
	clr = 5; //5与clr的类型不同,不能这样赋值。
	return 0;
}

在这里插入图片描述

4. 联合(共用体)

4.1、认识联合

联合也是一种自定义类型。
联合的特点是:联合内的成员都用的是同一块内存空间,空间大小取决于大的类型。
比如

union Un
{
 char c;
 int i;
};

这个联合体的大小只有4字节。

4.2、使用联合

通过联合体的特点,也可以确定大小端。

union Un
{
	char c;
	int i;
};

int main()
{
	union Un n = {0};
	n.i = 0x11223344;
	n.c = 0;
	printf("%x", n.i); //11223300

	return 0;
}

4.3、联合大小的计算

联合体大小的计算有两个规则

  1. 联合的大小至少是最大成员的大小。
  2. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
union Un1
{
 char c[5]; //5字节 对齐数为1
 int i; //对齐数为4
};
union Un2
{
 short c[7]; //14字节 对齐数为2
 int i; //对齐数为4
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1)); //5不是4的整数倍,所以最后大小为8
printf("%d\n", sizeof(union Un2)); //14不是4的整数倍,所以最后大小为16

本章完

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值