c语言进阶-第4节-自定义类型:结构体,枚举,联合

目录

1.结构体

1.1.结构体类型的声明

1.1.1.结构的声明

1.1.2.特殊的声明(匿名结构体声明)

1.2.结构体的自引用和结构体类型重命名

1.2.1.补充数据结构的知识

1.2.2.结构体的自引用 

1.2.3.结构体类型重命名

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

1.3.1.结构体变量的定义

1.3.2.结构体变量的初始化

1.4.结构体内存对齐

1.4.1.offsetof函数

1.4.2.结构体内存对齐

1.4.3.为什么存在结构体内存对齐

1.4.4.高效率设计结构体

1.4.5.修改默认对齐数

1.5.结构体传参

1.6.结构体练习:通讯录

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.1.1.结构的声明

struct tag
{
  member - list ;
variable - list  ;

注:

1.tag:结构体标签或结构体名字

2.member-list:成员列表,每一个成员为一个成员变量

3.variable-list:变量列表(变量列表有没有都可以)

结构声明的例子:

#include<stdio.h>

struct Stu
{

    char name[20];
    char sex[5];
    int age;
    int height;

}s2,s3,s4;   //结构体变量(全局变量)

struct Stu s5;    //结构体变量(全局变量)

int main()
{

    struct Stu s1   //结构体变量(局部变量)

    return 0;
}

1.1.2.特殊的声明(匿名结构体声明)

在声明结构的时候,可以省略结构体名字,这叫做匿名结构体声明(匿名结构体只能使用一次,使用时即创建结构体变量,因为没有名字,后面无法找到该结构体类型)

注:1.匿名结构体声明的话,必须在结构体后面紧跟着定义结构体变量

       2.匿名结构体因为没有名字,无法单独在后面定义该结构体变量

匿名结构体声明的例子:

#include<stdio.h>

struct 
{

    char c;
    int a;
    double b;

}sa;   

int main()
{
    return 0;
}

注:

1.如果一个程序里面有两个完全相同的匿名结构体声明,系统也会认为是两种结构体声明,如下面代码所示是错误的(编译器认为ps指针指向的结构体类型和sa结构体类型是两种不同的类型)

#include<stdio.h>

struct 
{

    char c;
    int a;
    double b;

}sa;   

struct 
{

    char c;
    int a;
    double b;

}*ps;

ps=&sa;

int main()
{
    return 0;
}

1.2.结构体的自引用和结构体类型重命名

1.2.1.补充数据结构的知识

数据结构:数据在内存中的存储结构

顺序表结构:连续存储

链表结构:找可用空间进行存储,不连续存储,每一个数据存好之后他们之间可以找到(每一个数据在存储的同时还要存储下一个数据存储的地址,这样就可以连起来,最后一个数据存储的同时,存储一个空指针NULL表示结束)

1.2.2.结构体的自引用 

错误的自引用方式:

如果用结构体模仿链表的形式,每一次存一个数据并且存下一个存储数据,如下代码所示

struct Node
{
    int data;
    struct Node next;
}

int main()
{
    return 0;
}

注:这种形式是错误的,结构体里面又包含一个自己类型的结构体,其大小无法计算。

正确的自引用方式:

正确的用结构体模仿链表的方式,是上一个结构体成员中存储下一个结构体的地址,如下面代码所示:

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

int main()
{
    return 0;
}

1.2.3.结构体类型重命名

错误的类型重命名代码:

#include<stdio.h>

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

int main()
{
    Node n={0};
    return 0;
}

注:

1.这段代码的意思是将匿名结构体类型typedef struct{int data;Node* next;}类型重命名为Node

2.这段代码是有问题的,会报错,如下图所示。因为我们对结构体类型重命名为Node的时候,结构体内的类型必须是清晰已知的,必须得先有结构体内的Node类型,才能重命名为Node,这里面先后出现了问题。

 3.如果重定义的时候,结构体内用到了我们正在命名的结构体类型名时,我们不能用匿名的方式,正确的方式如下代码所示

正确的类型重命名代码:

#include<stdio.h>

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

int main()
{
    Node n={0};
    return 0;
}

注:

1.结构体内用到了我们正在定义的结构体类型名时,我们不能用匿名的方式,正确代码如上面代码所示,我们对typedef struct Node{int data;struct Node* next;}类型重命名为Node,结构体内struct Node类型是定义过存在的

2.经过这样重命名后,既可以用前面的struct Node定义结构体变量,也可以用重命名后的Node定义结构体变量

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

1.3.1.结构体变量的定义

struct Point
{

    int x;
    int y; 

}p1;      //声明类型的同时定义变量p1,p1为全局变量

struct Point p2;    //定义结构体变量p2,p2为全局变量


int main
{

    struct Point p3    //定义结构体变量p3,p3为局部变量

    return 0;
}

1.3.2.结构体变量的初始化

#include<stdio.h>

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

struct Stu
{
	char name[20];
	char sex[5];
	int age;
	int hight;
};

struct Data
{
	struct Stu s;
	char ch;
	double d;
};

int main()
{
	struct Node n2 = {100, NULL};
	struct Stu s1 = { "zhangsan", "nan", 20, 180 };
	struct Data d = { {"lisi", "nv", 30, 166},'w', 3.14};

	return 0;
}

1.4.结构体内存对齐

1.4.1.offsetof函数

参数:

structName:结构体名

memberName:成员名

返回:

返回一个偏移量,一个成员在其结构体起始位置的偏移量,偏移量从0开始,偏移量的单位是字节

功能:

计算一个结构体的成员相较于这个结构体起始位置的偏移量

注:

1.使用offsetof需要stddef.h头文件

代码:

#include<stdio.h>
#include <stddef.h>

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


int main()
{
	printf("%d\n", offsetof(struct S1, c1));
	printf("%d\n", offsetof(struct S1, i));
	printf("%d\n", offsetof(struct S1, c2));

	return 0;
}

运行结果:

1.4.2.结构体内存对齐

1.结构体的第一个成员,存放在结构体变量开始位置的0偏移处

2.从第二个成员开始,都要对齐到对齐数的整数倍偏移量地址处

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

3.结构体的总大小,必须是最大对齐数的整数倍

最大对齐数:所有成员的对齐数中最大的那个

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

注:默认对齐数在vs编译器中为8,linux环境没有默认对齐数,对齐数就是成员自身的大小

代码1:

#include<stdio.h>
#include <stddef.h>

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


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

	printf("%d\n", offsetof(struct S1, c1));
	printf("%d\n", offsetof(struct S1, i));
	printf("%d\n", offsetof(struct S1, c2));

	return 0;
}

运行结果1:

 注:

1.由计算的结构体大小、各成员大小和各成员偏移量,可以得到内存的占用情况如下图所示

 我们发现,结构体申请了12个字节的空间,但是有6个字节没有用到,这就涉及到结构体内存对齐

2.第一个结构体成员c1存储在偏移量为0的地址处,c1是char类型,占一个字节。

第二个结构体成员i存储在偏移量为4的地址处,因为成员i自身是4字节小于vs默认对齐数8字节,存储在4的整数倍偏移量地址处。

第三个结构体成员c2存储在偏移量为8的地址处,因为成员c2自身是1字节小于vs默认对齐数8字节,存储在1的整数倍偏移量地址处。

3.从0偏移量地址到8偏移量地址9个字节空间,存完了全部的结构体成员。所有结构体成员中最大的对齐数是i成员的4,因此结构体的总大小必须为4的整数倍为12,偏移量地址从0到11。

代码2:

#include<stdio.h>
#include <stddef.h>

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

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

	printf("%d\n", offsetof(struct S2, c1));
	printf("%d\n", offsetof(struct S2, c2));
	printf("%d\n", offsetof(struct S2, i));

	return 0;
}

运行结果2:

注:

1.由计算的结构体大小、各成员大小和各成员偏移量,可以得到内存的占用情况如下图所示

2.第一个结构体成员c1存储在偏移量为0的地址处,c1是char类型,占一个字节。

第二个结构体成员c2存储在偏移量为1的地址处,因为成员c2自身是1字节小于vs默认对齐数8字节,存储在1的整数倍偏移量地址处。

第三个结构体成员i存储在偏移量为4的地址处,因为成员i自身是4字节小于vs默认对齐数8字节,存储在4的整数倍偏移量地址处。

3.从0偏移量地址到7偏移量地址8个字节空间,存完了全部的结构体成员。所有结构体成员中最大的对齐数是i成员的4,因此结构体的总大小必须为4的整数倍为8,偏移量地址从0到7。

4.这里我们看出,改变结构体成员的顺序,节省了4个字节空间,因此适当调整结构体成员顺序可以优化代码。

代码3:

#include<stdio.h>
#include <stddef.h>

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

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

	printf("%d\n", offsetof(struct S3, d));
	printf("%d\n", offsetof(struct S3, c));
	printf("%d\n", offsetof(struct S3, i));

	return 0;
}

运行结果3:

 注:

1.由计算的结构体大小、各成员大小和各成员偏移量,可以得到内存的占用情况如下图所示

2.第一个结构体成员d存储在偏移量为0的地址处,d是double类型,占8个字节。

第二个结构体成员c存储在偏移量为8的地址处,因为成员c自身是1字节小于vs默认对齐数8字节,存储在1的整数倍偏移量地址处。

第三个结构体成员i存储在偏移量为12的地址处,因为成员i自身是4字节小于vs默认对齐数8字节,存储在4的整数倍偏移量地址处。

3.从0偏移量地址到15偏移量地址16个字节空间,存完了全部的结构体成员。所有结构体成员中最大的对齐数是d成员的8,因此结构体的总大小必须为8的整数倍为16,偏移量地址从0到15。

代码4:

#include<stdio.h>
#include <stddef.h>

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

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

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

	printf("%d\n", offsetof(struct S4, c1));
	printf("%d\n", offsetof(struct S4, s3));
	printf("%d\n", offsetof(struct S4, d));

	return 0;
}

运行结果4:

 注:

1.由计算的结构体大小、各成员大小和各成员偏移量,可以得到内存的占用情况如下图所示

2.第一个结构体成员c1存储在偏移量为0的地址处,c1是char类型,占1个字节。

第二个结构体成员s3存储在偏移量为8的地址处,因为成员s3自身是一个结构体变量,该结构体变量占16字节,因为如果嵌套了结构体的情况,嵌套的结构体对齐到自己成员的最大对齐数的整数倍处,其自己成员的最大对齐数是8(其成员变量d的对齐数),那么结构体变量s3的对齐数是8,存储在8的整数倍偏移量地址处。

第三个结构体成员d存储在偏移量为24的地址处,因为成员d自身是8字节等于vs默认对齐数8字节,存储在8的整数倍偏移量地址处。

3.从0偏移量地址到31偏移量地址32个字节空间,存完了全部的结构体成员。如果嵌套了结构体的情况,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。c1的对齐数为1,s3和d的对齐数都是8,因此结构体的总大小必须为8的整数倍为32,偏移量地址从0到31。

1.4.3.为什么存在结构体内存对齐

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

 32位机读取地址是每四个字节进行读取。如果没有对齐,要读取i时需要读取2次(第一次读取了i的前三个字节,第二次读取了i的最后一个字节);如果对齐了,要读取i时,i的四个字节1次就可以读取完。对齐之后读取效率更高,。

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

1.4.4.高效率设计结构体

在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
代码1:
#include<stdio.h>
#include <stddef.h>

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


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

	printf("%d\n", offsetof(struct S1, c1));
	printf("%d\n", offsetof(struct S1, i));
	printf("%d\n", offsetof(struct S1, c2));

	return 0;
}

运行结果1:

代码2:

#include<stdio.h>
#include <stddef.h>

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

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

	printf("%d\n", offsetof(struct S2, c1));
	printf("%d\n", offsetof(struct S2, c2));
	printf("%d\n", offsetof(struct S2, i));

	return 0;
}

运行结果2:

注:

1.如上面两个代码进行对比,将占用空间较小的结构体成员放在一起,结构体占用的空间较少

2.结构体我们必须要自己设计好,做到高效率,release版本并不会帮我们改变结构体成员顺序,不会对结构体成员顺序进行优化

1.4.5.修改默认对齐数

结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。(一般修改的值为 2^{n}

 代码1:

#include<stdio.h>
#include <stddef.h>

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


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

运行结果1

代码1:(将默认对齐数改成1):

#include<stdio.h>
#include <stddef.h>

#pragma pack(1)
struct S1
{
	char c1;
	int i;
	char c2;
};
#pragma pack()


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

运行结果1:(将默认对齐数改成1):

注:

1.#pragma pack(1)是将默认对齐数设置成1

2.#pragma pack()是取消掉对默认对齐数的设置

3.将默认对齐数改成1,那么所有结构体成员的对齐数将都为1,所有结构体成员存储在1的整数倍偏移量地址处,也就是说结构体成员将逐字节进行存储,存完所有成员,结构体占6个字节,结构体最大对齐数为1,6是1的倍数,因此结构体占6字节,下面是修改前后内存占用情况

修改前偏移量修改后偏移量

c1

0c10
1i1
22
33
i44
5c25
66
77
c288
99
1010
1111
1212
1313

代码2:

#include<stdio.h>
#include <stddef.h>

struct S
{
	char c1;
	double d;
	char c2;
};

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

运行结果2:

 代码2(将默认对齐数改成4):

#include<stdio.h>
#include <stddef.h>

#pragma pack(4)
struct S
{
	char c1;
	double d;
	char c2;
};
#pragma pack()

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

运行结果2(将默认对齐数改成4):

 注:修改前后内存占用情况如下表所示

修改前偏移量修改后偏移量
c10c10
11
22
33
4c14
55
66
77
d88
99
1010
1111
12c212
1313
1414
1515
c21616
1717
1818
1919
2020
2121
2222
2323
2424

1.5.结构体传参

结构体传值代码:

#include<stdio.h>
#include <stddef.h>

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

int main()
{
	print1(s); 

	return 0;
}

结构体传址代码:

#include<stdio.h>
#include <stddef.h>

struct S 
{
	int data[1000];
	int num;
};
struct S s = { {1,2,3,4}, 1000 };

void print2(struct S* ps) 
{
	printf("%d\n", ps->num);
}

int main()
{
	print2(&s); 

	return 0;
}

注:

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

2.结构体传参如果传值,形参是对结构体的临时拷贝,将无法对结构体变量进行修改

结构体传参如果传址,将可以对结构体变量进行修改,如果不想对结构体变量进行更改,形参使用const放在*的左边,对其进行保护即可。

可以看出在功能方面,传址调用功能包含传值调用,因此尽量使用传址调用

1.6.结构体练习:通讯录

通讯录:

1.可以存放1000个人的信息

2.人的信息:名字、年龄、性别、电话、住址

功能:

1.增加联系人(完成)

2.删除联系人(完成)

3.查找联系人(先不完成)

4.修改联系人(先不完成)

5.排序(名字/年龄)(先不完成)

6.展示所有人信息(完成)

0.退出(完成)

test.c代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include "contact.h"

enum Oprion
{
	EXIT,//0
	ADD,
	DEL,
	SEARCH,
	MODIFY,
	SORT,
	SHOW
};
void menu()
{
	printf("***************************************\n");
	printf("********     1.add      2.del     *****\n");
	printf("********     3.search   4.modify  *****\n");
	printf("********     5.sort     6.show    *****\n");
	printf("********     0.exit               *****\n");
	printf("***************************************\n");
}

int main()
{
	int input = 0;
	Contact con = { 0 };//通讯录
	//初始化通讯录
	InitContact(&con);

	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case ADD:
			AddContact(&con);
			break;
		case DEL:
			DeleteContact(&con);
			break;
		case SEARCH:
			break;
		case MODIFY:
			break;
		case SORT:
			break;
		case SHOW:
			ShowContact(&con);
			break;
		case EXIT:
			printf("退出通讯录\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);

	return 0;
}

contact.h代码:

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
#include <string.h>
#include <assert.h>

#define MAX 1000

#define NAME_MAX 20
#define SEX_MAX 5
#define ADDR_MAX 30
#define TELE_MAX 12

typedef struct PeoInfo
{
	char name[NAME_MAX];
	int age;
	char sex[SEX_MAX];
	char addr[ADDR_MAX];
	char tele[TELE_MAX];
}PeoInfo;


//通讯录的结构体
typedef struct Contact
{
	PeoInfo data[MAX];//存放数据
	int sz;//通讯录中有效信息的个数
}Contact;


//初始化通讯录
void InitContact(Contact* pc);

//增加联系人到通讯录
void AddContact(Contact* pc);

//打印通讯录中的信息
void ShowContact(const Contact* pc);

void DeleteContact(Contact* pc);

contact.c代码:

#define _CRT_SECURE_NO_WARNINGS 1

#include "contact.h"

void InitContact(Contact* pc)
{
	assert(pc);
	pc->sz = 0;
	memset(pc->data, 0, sizeof(pc->data));
}

void AddContact(Contact* pc)
{
	assert(pc);
	if (pc->sz == MAX)
	{
		printf("通讯录满了,无法添加\n");
		return;
	}
	//输入联系人
	printf("请输入名字:>");
	scanf("%s", pc->data[pc->sz].name);
	printf("请输入年龄:>");
	scanf("%d", &(pc->data[pc->sz].age));
	printf("请输入性别:>");
	scanf("%s", pc->data[pc->sz].sex);
	printf("请输入电话:>");
	scanf("%s", pc->data[pc->sz].tele);
	printf("请输入地址:>");
	scanf("%s", pc->data[pc->sz].addr);

	pc->sz++;
	printf("增加联系人成功\n");
}

void ShowContact(const Contact* pc)
{
	assert(pc);
	int i = 0;
	printf("%-10s\t%-5s\t%-5s\t%-13s\t%-20s\n", "名字", "年龄", "性别", "电话", "地址");
	for (i = 0; i < pc->sz; i++)
	{
		printf("%-10s\t%-5d\t%-5s\t%-13s\t%-20s\n",
			pc->data[i].name, pc->data[i].age, pc->data[i].sex, pc->data[i].tele, pc->data[i].addr);
	}
}

int FindByName(const Contact* pc, char name[])
{
	int i = 0;
	for (i = 0; i < pc->sz; i++)
	{
		if (strcmp(pc->data[i].name, name) == 0)
		{
			return i;
		}
	}

	return -1;//找不到
}

void DeleteContact(Contact* pc)
{
	char name[NAME_MAX] = { 0 };
	if (pc->sz == 0)
	{
		printf("通讯录为空,无法删除\n");
		return;
	}
	printf("请输入要删除人的名字:>");
	scanf("%s", name);
	//查找指定联系人
	int pos = FindByName(pc, name);
	if (pos == -1)
	{
		printf("要删除的人不存在\n");
		return;
	}
	else
	{
		//删除
		int j = 0;
		for (j = pos; j < pc->sz - 1; j++)
		{
			pc->data[j] = pc->data[j + 1];
		}
		pc->sz--;

		printf("删除指定联系人成功\n");
	}
}


2.位段

2.1.位段的概念

位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 、signed int 或char(属于整型家族)
2.位段的成员名后边有一个冒号和一个数字
注:位段相比于结构体在一定程度上节省了空间(也存在一些空间浪费)

位段的例子:

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

代码:

#include <stdio.h>

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

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

	return 0;
}

运行结果:

注:

1.位段的位代表二进制位,代码 int _a : 2表示的意思是_a只占2个比特位,代码 int _b : 5表示的意思是_b只占5个比特位,依次类推。

2.2.位段的内存分配

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

代码1:

#include <stdio.h>

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

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

	return 0;
}

 运行结果1:

注:

1.遇到代码int _a:2代码首先开辟4个字节32比特位;int _a : 2需要2个比特位,还剩30个比特位;int _b : 5需要5个比特位,还剩25个比特位;int _c : 10需要10个比特位,还剩15个比特位;int _d : 30需要30个比特位,此时剩余了15个比特位不够了,因此再开辟4个字节32比特位;因此总共开辟8个字节

2.位段涉及不确定因素,不要跨平台使用,比如上面代码运行到int _d : 30时,需要30个比特位,此时只剩下15个比特位,因此再开辟4个字节32比特位,此时开辟的32比特位已经够存_d了,但是存储到底是第一次开辟剩余的15比特位+第二次开辟32比特位中15比特位还是全部存在第二次开辟的32比特位中30比特位是不确定的,因为c语言标准未规定,完全取决于不同编译器的实现

代码2:

#include <stdio.h>

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

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

	return 0;
}

运行结果2:

注:

1.遇到代码char a : 3代码首先开辟1个字节8比特位;char a : 3需要3个比特位,还剩5个比特位;char b : 4需要4个比特位,还剩1个比特位;char c : 5需要5个比特位,此时剩余了1个比特位不够了,因此再开辟一个字节8比特位,此时分为两种情况:

第一种情况(剩余不够不再使用):第一次开辟剩余的1个比特位不用了,使用第二次开辟的8比特位中5比特位,还剩3比特位;char d : 4需要4比特位,此时剩余3比特位不够,再开辟1字节8比特位,前面第二次开辟剩余的3比特位不用了,使用第三次开辟的8比特位中4比特位;这种情况总共开辟3个字节

第二种情况(剩余不够继续使用):第一次开辟剩余的1个比特位继续使用,并且还需要第二次开辟的8比特位中4比特位,第二次开辟的还剩4比特位;char d : 4需要4比特位,刚好用完;这种情况总共开辟2个字节

2.根据运行结果,我们可以推测vs编译器是以第一种情况进行存储的

代码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.假设一个字节中,是从低位往高位进行存储的(c语言标准中没有规定),也就是从右往左进行存储

s.a = 10,10的二进制位为1010,而a只开辟3个比特位,因此a的3个比特位为010,并且从右往左进行存储,如下表所示

000000100000000000000000

s.b = 12,12的二进制位为1100,而b开辟4个比特位,因此b的4比特位为1100,并且接着前一次从右往左进行存储,如下表所示

011000100000000000000000

s.c = 3,3的二进制位为011,而c开辟5个比特位,因此c的5个比特位为00011,根据前面的推测应该在第二次开辟的一个字节中从右往左的5个比特位进行存储,如下表所示

011000100000001100000000

s.d = 4,4的二进制位为100,而d开辟4个比特位,因此d的四个比特位为0100,根据前面的推测应该在第三次开辟的一个字节中从右往左的4个比特位进行存储,如下表所示

011000100000001100000100

因为内存数据显示出来为16进制的,此时这三个字节显示的数值应该是62 03 04,与上面内存中显示数据完全相同

2.我们可以得到位段的结论:(在vs编译器下成立)

(1)若一次申请中存储完数据后剩下的比特位不够进行存储时,此次申请剩下的比特位会浪费               掉,重新申请一次,再进行存储

(2)开辟的一个字节中空间使用是从低位到高位进行存储的,也就是从右往左进行存储

2.3.位段的跨平台问题

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

2.4.位段的应用

注:用位段设置数据包格式,使数据包尽可能小(比如将版本号设置为4比特,如果不用位段,最小的char类型也需要8比特)


3.枚举

枚举顾名思义就是一一列举。
把可能的取值一一列举。
比如我们现实生活中:
一周的星期一到星期日是有限的7天,可以一一列举。
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举
这里就可以使用枚举了。

3.1.枚举类型的定义

代码1:

#include<stdio.h>

enum Day//星期
{
    //枚举的可能取值

    Mon,
    Tues,
    Wed,
    Thur,
    Fri,
    Sat,
    Sun
};

enum Sex//性别
{
    MALE,
    FEMALE,
    SECRET
};

int main()
{
    enum Day d = Sun;
    enum Sex s = SECRET;
    printf("%d\n", MALE);
    printf("%d\n", FEMALE);
    printf("%d\n", SECRET);

    return 0;
}

运行结果1:

代码2:

#include<stdio.h>

enum Day//星期
{
    //枚举的可能取值

    Mon,
    Tues,
    Wed,
    Thur,
    Fri,
    Sat,
    Sun
};

enum Sex//性别
{
    MALE=4,
    FEMALE=8,
    SECRET
};

int main()
{
    enum Day d = Sun;
    enum Sex s = SECRET;
    printf("%d\n", MALE);
    printf("%d\n", FEMALE);
    printf("%d\n", SECRET);

    return 0;
}

运行结果2:

注:

1.以上定义的 enum Day enum Sex 都是枚举类型。{ }中的内容是枚举类型的可能取值,也叫枚举常量 。 这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。

2.若枚举常量没有赋值,会默认赋值前一个枚举常量的值+1,如代码2所示

3.必须使用枚举常量给枚举变量赋值,这样才不会出现类型的差异,如下图所示,clr是枚举变量,将5赋值给clr是有问题的

3.2.枚举类型重命名

#include<stdio.h>

typedef enum Sex
{
    MALE=4,
    FEMALE=8,
    SECRET
}Sex;

int main()
{
    Sex s2 = MALE;
    return 0;
}

3.3.枚举的优点

我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
1. 增加代码的可读性和可维护性
2. #define 定义的标识符比较,枚举有类型检查,更加严谨(#define定义的标识符没有类型,其就是一个符号,运行时仅进行替换)
3. 防止了命名污染(封装):枚举是将枚举常量这些符号放在大括号内封装起来了;#define定义的标识符散落在外面
4. 便于调试:枚举定义的枚举常量是可以进行调试的;#define定义的常量无法进行调试,因为在调试的时候已经进行了替换
5. 使用方便,一次可以定义多个常量

4.联合(共用体)

4.1.联合类型的定义

联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)
代码1:
#include<stdio.h>

union Un
{
	char c;//1
	int i;//4
};

int main()
{
	union Un u;
	printf("%d\n", sizeof(u));
	printf("%p\n", &u);
	printf("%p\n", &(u.c));
	printf("%p\n", &(u.i));

	return 0;
}

运行结果1:

注:

1.联合体变量u的地址、u里字符成员c的地址、u里整型成员i的地址相同,可以看出,成员变量c和i共用了第一个字节,所以叫做共用体。

2.当给c赋值的时候i的值会改变;当给i赋值的时候c的值也会改变,因此联合体成员同一时间只能用一个

3.联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)

代码2:

#include<stdio.h>

union Un
{
	char c;//1
	int i;//4
};

int main()
{
	union Un u = {0};
	u.c = 'w';
	u.i = 0x11223344;

	return 0;
}

运行调试结果2:

4.2.联合大小的计算

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

代码1:

#include<stdio.h>

union Un1
{
	char c[5];//5 1,8 - 1
	int i;    //4 4,8 - 4
};

int main()
{
	printf("%d\n", sizeof(union Un1));

	return 0;
}

运行代码1:

注:数组c,占5个字节,其每个元素都是char类型,因此其自身大小为1,默认对齐数为8,较小值为1,因此其对齐数为1;整型变量i,占4个字节,其自身大小为4,默认对齐数为8,较小值为4,因此其对齐数为4;因为最大成员大小数组c占5个字节,而最大对齐数为4,不是最大对齐数的整数倍,因此要对齐到最大对齐数的整数倍为8

代码2:

#include<stdio.h>

union Un2
{
	short c[7];//14 2,8-2
	int i;     //4  4,8-4
};

int main()
{
	printf("%d\n", sizeof(union Un2));

	return 0;
}

运行代码2:

注:数组c,占14个字节,其每个元素都是short类型,因此其自身大小为2,默认对齐数为8,较小值为2,因此其对齐数为2;整型变量i,占4个字节,其自身大小为4,默认对齐数为8,较小值为4,因此其对齐数为4;因为最大成员大小数组c占14个字节,而最大对齐数为4,不是最大对齐数的整数倍,因此要对齐到最大对齐数的整数倍为16

4.3.练习

判断当前计算机的大小端存储

代码1:

#include<stdio.h>

int cheak_sys()
{
	int a = 1;
	if (*(char*)&a == 1)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

int main()
{
	int ret = cheak_sys();
	if (1 == ret)
		printf("小端\n");
	else
		printf("大端\n");

	//如果返回1,表示小端
	//如果返回0,表示大端
	return 0;
}

代码2:(代码1改进)

#include<stdio.h>

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


int main()
{
	int ret = cheak_sys();
	if (1 == ret)
		printf("小端\n");
	else
		printf("大端\n");

	//如果返回1,表示小端
	//如果返回0,表示大端
	return 0;
}

代码3:(使用联合体)

#include<stdio.h>

int cheak_sys()
{
	union Un
	{
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
}

int main()
{
	int ret = cheak_sys();
	if (1 == ret)
		printf("小端\n");
	else
		printf("大端\n");

	//如果返回1,表示小端
	//如果返回0,表示大端
	return 0;
}

代码4:(使用匿名联合体)

#include<stdio.h>

int cheak_sys()
{
	union
	{
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
}

int main()
{
	int ret = cheak_sys();
	if (1 == ret)
		printf("小端\n");
	else
		printf("大端\n");

	//如果返回1,表示小端
	//如果返回0,表示大端
	return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

随风张幔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值