自定义类型的详解

目录

1.结构体

1.1结构体

1.2结构体的声明

 1.3特殊声明

1.4结构体的自引用

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

1.6 结构体内存对齐

1.6.1结构体的对齐方式是什么?

1.6.2为什么要对齐?

1.7 修改默认对齐数

1.8 结构体传参

2.位段

2.1什么是位段

2.2位段的内存分配

2.3位段的跨平台问题

2.4位段的应用

3.枚举

3.1枚举类型的定义

3.2枚举的优点

3.3枚举的使用

4.联合体

4.1联合体的定义

4.2联合体的特点

4.3联合体大小的计算


1.结构体

1.1结构体

结构体是一些值的集合,这些值被称为成员变量,这些成员变量可以是其他类型

1.2结构体的声明

struct tag
{
member - list ;//成员列表
} variable - list ;//变量列表

 让我们来描述一个学生

struct stu
{
    char name[30];//姓名
    char gender[20];//性别
    int grade;//成绩
    int age;//年龄
};//不要忘记结构体的分号

 1.3特殊声明

我们在声明结构体的时候可以不进行命名,这种被称为匿名结构体,只可以被使用一次

我们可以忽略这里面的tag

struct tag
{
member - list ;//成员列表
} variable - list ;//变量列表

 例如:

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

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

这两个结构体在声明的时候忽略了tag,

那么我们有一个问题,如果我们这样表述对吗?

p = &x;

这个代码是合理的吗?

答案是不合法的,即是他们两个内容是相同的,但是编译器会把他们当做2个不同的类型,是错误的。

1.4结构体的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?
struct Node
{
 int data;
 struct Node next;
};

答案是不可以的,如果我们在结构体里面套入一个结构体,那么原来这个结构体本身将无限大,这是不合理的。正确的做法应该是这样

struct node
{
	int data;
	struct node* next;
};
在结构体⾃引⽤使⽤的过程中,夹杂了typedef对匿名结构体类型重命名,也容易引⼊问题
typedef struct
{
 int data;
 Node* next;
}Node;
因为Node是对前⾯的匿名结构体类型的重命名产⽣的,但是在匿名结构体内部提前使
⽤Node类型来创建成员变量,这是不⾏的。

解决方法就是不使用匿名结构体了

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

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

struct Point
{
 int x;
 int y;
}p1; //声明类型的同时定义变量p1

struct Point p2; //定义结构体变量p2

//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};

struct Stu        //类型声明
{
 char name[15];//名字
 int age;      //年龄
};

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

struct Node
{
 int data;
 struct Point p;
 struct Node* next; 
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化

struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

1.6 结构体内存对齐

下面我们来探讨一下结构体的内存大小

如果有2个内容相同但是,顺序不同的结构体他们的内存大小是不是一样的呢?

struct s1
{
	char a;
	int i;
	char c;
};

struct s2
{
	char a;
	char c;
	int i;
};


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

我们来打印看一下

我们发现居然是不同的这到底是为什么呢?

这是因为结构体的对齐方式的问题,

1.6.1结构体的对齐方式是什么?

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

我们根据这个规则先计算这个结构体的大小 

struct S1
{
 char c1;
 int i;
 char c2;
};
该成员变量⼤⼩
vs默认对齐数对齐数
char        1        8        1
int        4        8        4
char        1        8                1

char类型存入偏移量为0的位置处,int存入其对齐数为4的倍数的偏移量处,也就是4;char再存入其对齐数为1的倍数处的偏移量处,也就是8处。

那么我们一共占用了9个字节,并非是最大对齐数的整数倍,下一个整数倍是12,所以这个结构体的大小就是12字节

下面有几个练习

//练习2
struct S2
{
 char c1;
 char c2;
 int i;
};
printf("%d\n", sizeof(struct S2));
//练习3
struct S3
{
 double d;
 char c;
 int i;
};
printf("%d\n", sizeof(struct S3));
//练习4-结构体嵌套问题
struct S4
{
 char c1;
 struct S3 s3;
 double d;
};
printf("%d\n", sizeof(struct S4))

请读着手算,会在文章最后给出答案

1.6.2为什么要对齐?

1. 平台原因 ( 移植原因 )
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因
数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体内存对齐是拿空间换时间的做法

 所以我们在使用结构体的时候尽量,利用好结构体对齐

struct s1
{
    char s;
    char a;
    int v;
};

struct s2
{
    char a;
    int v;
    char s;
};

两个结构体的内容相同,但s1比s2更节省空间一些。 

1.7 修改默认对齐数

之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。

#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));
	return 0;
}
结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。

1.8 结构体传参

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

所以我们结构体传参时要使用指针。

2.位段

2.1什么是位段

位段与结构体类似,但有2个不同

1.位段的成员名后面必须有“:”和数字

2.位段的成员被限制为整型家族类型,包括有符号和无符号的字符型、短整型、整型等。

例如:

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

那么这个位段的大小是多少?

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

 我们运行:

 

 这里“:”后面代表的是占的比特位,本来一个int类型是占4个字节,32个比特位,这里我们只让其占用2个字节,在一些情况下(比如说:用a表示的数字只有0,1,2,3,其二进制最多2位)可以极大的节省空间,那么我们把上面代码的数字相加等于2+5+10+30 = 47,一共是47个比特,那么应该是6个字节才对,为什么是8个字节呢?

2.2位段的内存分配

1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
2. 位段的空间上是按照需要以 4 个字节( int )或者 1 个字节( char )的方式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
//一个例子
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;
}//空间是如何开辟的?

1.内存是按需分配的,char就给1个字节,不够再加;

2. 内存是由低到高使用的

就拿上面的内容来说,a是char类型,先给结构体一个字节的空间,再从这一字节中拿了3比特的空间给a,此时a只占3个字节。

在主函数中给a赋值10,10的2进制表达是1010,那么只有010被存到a中

给了b 4个比特的空间,刚才一个字节中的8比特有3个被使用了,还有5个,够给b开辟空间,12的二进制序列是1100,则都可以存进去。还剩下1个比特不够c存的,那么这1个比特的空间我们就舍去了,再开辟一个字节存入c 5个比特,d 4个比特就不够存了,再开辟1个字节让d的4个字节存入,正好3个字节,那么这个位段的大小是3个字节

 那么我们换成0x的形式来看,会是什么情况呢?(在之前的博客中我们已知我们的电脑是小端存储

 这里我们来说一下什么是小端存储,数据的低位字节存储在内存的低地址处

http://t.csdnimg.cn/I2eJy 具体请参考前文。)

 

2.3位段的跨平台问题

1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。( 16 位机器最大 16 32 位机器最大 32 ,写成 27 ,在 16 位机器会出问题。)
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

2.4位段的应用

在网络协议栈中,可以使用位段节省空间。

就好比说网络是高速公路,如果都是大卡车,占了所有的车道,那么速度就会很慢,如果既有小车也有大车就会很快,节省了空间。

3.枚举

我们之前学过一些常量,

1.字面常量

2.#define,定义的标识常量

3.const修饰的常变量

4.枚举常量(关键字enum)

3.1枚举类型的定义

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,当然在定义的时候也可以赋初值。

 

enum Color//颜色
{
 RED=1,
 GREEN=2,
 BLUE=4
};

 

3.2枚举的优点

我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
1. 增加代码的可读性和可维护性
2. #define 定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量

我们再使用#define的时候我们如果调试,#define 定义的标识符就会直接转换成为数字,不方便我们阅读

例如:#define male 3 在调试的时候直接把所有male转换为3

且#define的内容没有类型检查

而枚举类型就会有类型检查 

3.3枚举的使用

枚举的值是从0开始的,且依次递增一,可以在创建时就给枚举的内容赋值。

enum Color//颜色
{
 RED=1,
 GREEN=2,
 BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5;    这个代码有没有问题呢?

 clr是enum color类型的,而5是int类型,类型不同 ,这是不可以的

4.联合体

4.1联合体的定义

联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
union un
{
	char i;
	int a;

};//联合体的定义

int main()
{
	union un a;//联合体变量的定义
	printf("%zd\n", sizeof(union un));//计算联合体的大小
	return 0;
}

4.2联合体的特点

联合体的特点就是共用同一块空间,

union un
{
	char a;
	int b;
};​

 a会使用1一个字节的空间,而b会使用4个字节的空间且他们使用的是同一块空间

我们可以利用这个特性来判断计算机是什么方式存储,我们以前的方法是

int check()
{
	int a = 1;
	return *(char*)&a;
}

int main()
{
	int ret = check();
	if (ret == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

int a = 1;0x 00 00 00 01 这是a的数据,如果我们把a的地址取出,再读取第一个字节,就可以判断了,所以我们强制类型转换成char*类型的指针,再对其进行解引用则读取的是第一个字节,就可以判断,在内存中是如何存储的了。

我们现在用新的方法来判断大小端存储了:

int check()
{
	union un
	{
		char a;
		int b;
	}s;
	s.b = 1;
	return s.a;
}

这里的联合体共用一块空间, 我们让int b等于1,那么相当于把0x 00 00 00 01存入这块空间。

我们再读取s.b也就是里面char类型的数据,如果是小端存储,那么从低地址开始读取数据时,就会读到1,如果是大端存储,从低地址读到的就是0。

4.3联合体大小的计算

 很多人会说联合体的大小就是,最大的那个成员的大小这是错误的!

联合体的大小至少是那个最大成员的大小。当最大成员大小,不是对齐数的整数倍时,就要对齐到那最大对齐数的整数倍哦!

举个例子:

union un
{
	char a[5];
	int b;
};
int main()
{
	printf("%d\n", sizeof(union un));
	return 0;
}

 再上述代码中如果联合体的大小是最大成员的大小,那么我们打印出来应该是char a[5]的大小,是5个字节,我们来运行这个代码

我们发现最终的数字是8个字节。这就证明了并非是最大成员的大小。

在这个联合体中,最大对齐数应该是4,成员最大是5字节,不是整数倍,则必须换成最小整数倍,则是8字节。

还望大家指正。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值