C语言笔记(自定义类型:结构体、枚举、联合体 )

前言

        本文对自定义类型的结构体创建、使用、结构体的存储方式和对齐方式,枚举的定义、使用方式以及联合体的定义、使用和存储方式展开叙述,如有错误,请各位指正。

目录

前言

1 结构体

1.1 结构体的声明

1.2 结构体的自引用

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

1.4 结构体变量的内存对齐

1.5 修改默认对齐数

1.6 结构体传参

2.位段

2.1 位段的内存分配

2.2 位段的跨平台问题

3.枚举

3.1枚举类型的定义

3.2枚举的优点

3.3枚举的使用

4. 联合(共用体)

4.1 联合类型的定义

4.2 联合的特点

4.3 联合大小的计算

1 结构体

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

1.1 结构体的声明
struct tag
{
    member-list;
}variable-list;

tag                结构体标签

member-list  成员列表

variable-list   变量列表

注意:创建结构提变量的时候最后的不能丢掉

结构体就创建一个自定义的包含多种数据类型的数据类型

示例

创建一个描述学生的结构体数据类型 Stu 

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

特殊声明 :匿名结构体类型,没有结构体的标签,没有给结构体起名字。

示例1(创建没有标签的结构体)

struct
{
	int a;
	char b;
	float c;
}x;//x创建的结构体变量

        这种结构的使用只能现创现用,在不同的位置是无法创建同一类型的结构体变量的,创建变量需要结构体标签,没有标签就是创建不了变量了。

示例2(创建结构体的指针)

struct
{
    int a;
    char b;
    float c;
} *p;

示例1和示例2中下方等式成立吗?

p = &x;

示例3(使用typedef简化结构体名称,创建结构体变量)

typedef struct Stu
{
	char name[20];
	int age;
	char sex;
	char id[20];
}Stu;
int main()
{
	Stu s1, s2;
	return 0;
}
1.2 结构体的自引用

        结构体的自引用就是结构体中包含同一个结构体的指针。

示例:在数据结构中数据的存放有链表性结构,链表中的每一处的节点都会存放一个数据,还可以根据节点找到下一个节点的地址,然后创建结构体;那么我结构体存放节点的数据和指向下一个节点的指针就可以了,这就是结构体的自引用了。

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

int main()
{
	Node n1, n2;
	n1.next = &n1;
	return 0;
}

        结构的创建变量的时候名字较为复杂还有 unsigned int 、unsigned char等等 都可以使用 typedef重新定义名字,这样在创建变量的时候就简单了,如下

//重新定义变量
typedef unsigned int uint;
typedef unsigned char uchar;
typedef char u8;
typedef struct Stu Stu;
//创建变量
unit a;
unchar b;
u8 c;
Stu x;

需要注意

在结构体创新命名的时候,需要结构体有标签。

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

        结构体定义就是创建结构体变量,以下是创建结构提变量的方式:


//方式1 创建结构体的时候创建全局变量
struct Point
{
	int x;
	int y;
}a1;
//方式2
struct Point a2;
//方式3
int main()
{
	struct Point a3;

	return 0;
}

        初始化和和数组的初始化较为相似,是应用{ }来初始换结构体创建的变量,在后面附上值就可以了,完全初始化,

struct Point
{
	int x;
	int y;
}a1 = {10,20};
struct Point a2 = {5,9};

int main()
{
	struct Point a3 = { 2,3 };

	return 0;
}

        不完全初始化(也可以一个一个的给变量赋值)

struct S s1= { .num=10,.ch='q',.p.x=6, .p.y=10 };

嵌套结构体的初始化,有几个结构体就用几个{ }来创建变量,示例如下:

struct Point1
{
	int x;
	int y;
};
struct Point2
{
	int z;
	char ch;
	struct Point1 a1;
	float d;
};
int main()
{
	struct Point2 s = { 1,'a',{1,2},6.14f };
	return 0;
}

结构体的访问方式,分为两种,一种是使用 . 符号来访问,一种是使用 -> 符号来访问

struct Point
{
	int x;
	int y;
};
struct S
{
	int num;
	char ch;
	struct Point p;
	float d;
};

int main()
{
	//	初始化
	struct S s = { 3,'w',{1,2},3.15f };
	//访问
	printf("%d %c %d %d %0.1f\n", s.num,s.ch,s.p.x, s.p.y, s.d);

	return 0;
}
struct Stu
{
	char name[20];
	int age;
	char sex[20];
	float score;
};
int main()
{
	//打印结构体信息
	struct Stu s = { "张三", 20, "男", 95.0f };
	struct Stu *ps = &s;
	printf(" %s %d %s %.1f\n", ps->name, ps->age, ps->sex, ps->score);
	return 0;
}
1.4 结构体变量的内存对齐

        结构体的内存对齐决定了结构体的在内存中所占用的空间大小。

引入

        创建两个结构体,结构体中的变量类型相同,但是顺序不同,其内存大小一样吗?

struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	printf("%d\n", sizeof(struct S1));//12
	printf("%d\n", sizeof(struct S2));//8
	return 0;
}

1.结构体的顺序不一样,内存不一样;

2.内存的大小和成员的数据类型有关吗?

结构体的对齐方式按照以下几条

1.结构体的第一成员,对齐到结构体在内存中的存放位置的0偏移处;

2.从第二个成员开始,每个成员都要对齐到(一个对齐数)的整数倍处;

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

VS:默认对齐数为8;

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

3.结构体的总大小,必须是所有成员的对齐数中最大对齐数的整数倍。

4.如果结构体中嵌套了结构体成员,要将嵌套的结构体成员的对齐到自己的成员中最大的对齐数的整数。(结构体的总大小必须是最大对齐数的整数倍,包含请嵌套结构体成员的对齐数,是所有对齐数的最大值)

S1的结构体存储的方式

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

 (1)c1是第一个成员,从0偏移量存储,占用一个字节(灰色);

(2)i为整型变量,对齐数为4,默认对齐数为8,取较小值为4,偏移量为4,偏移量 1,2,3处内存浪费掉了(黄色);

(3)c2字符变量,对齐数为1,默认对齐数为8,取较小值为1,占用偏移量为8的位置(红色);

(4)确定结构体的大小,从c1到c2处,占用了9个字节的空间,取结构体成员的最大对齐数4,因此内存要占用12个字节,浪费6个字节(蓝色)。

S1的结构体存储的方式

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

(1)c1是第一个成员,从0偏移量存储,占用一个字节(灰色);

(2)c2字符变量,对齐数为1,默认对齐数为8,取较小值为1,偏移量是对齐数的倍数 2,占用偏移量为2的位置(红色);

(3)i为整型变量,对齐数为4,默认对齐数为8,取较小值为4,偏移量为4,去偏移量的倍数 4,偏移量 2,3处内存浪费掉了(黄色);

(4)确定结构体的大小,取结构体成员的最大对齐数4,因此内存要占用8个字节,浪费2个字节空间(蓝色)。

使用 offsetof函数来验证上述的偏移量是否正确,这个函数就是计算偏移量的,头文件是stddef,

验证程序

struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	printf("%d\n", sizeof(struct S1));//12
	printf("%d\n", offsetof(struct S1, c1));//0
	printf("%d\n", offsetof(struct S1,i));//4
	printf("%d\n", offsetof(struct S1,c2));//8
	return 0;
}

嵌套结构体内存对齐,

#include <stdio.h>
#include <stddef.h>
struct S2
{
	char c1;
	char c2;
	int i;
};
struct S3 
{
	double d;
	struct S2 s2;
	int a;
};
int main()
{
	printf("%d\n", sizeof(struct S3));//24
	printf("%d\n", offsetof(struct S3,d));//0
	printf("%d\n", offsetof(struct S3,s2));//8
	printf("%d\n", offsetof(struct S3,a));//16
	return 0;
}

S3是如何对齐呢?

struct S3 
{
	double d;
	struct S2 s2;
	int a;
};

(1)d是第一个成员,从0偏移量存储,占用8个字节(灰色);

(2)s3为结构体变量,结构体成员最大的对齐数为4,默认对齐数为8,取较小值为4,偏移量是对齐数的倍数 8,占用偏移量为8的位置(黄色);

(3)a为整型变量,对齐数为4,默认对齐数为8,取较小值为4,偏移量取16,取偏移量的倍数 16(黄色);

(4)确定结构体的大小,取结构体成员的最大对齐数8,因此内存要占用24个字节,浪费4个字节空间(蓝色)。

为什么要对齐呢?

1. 程序移植: 不是所有的平台数据的存储方式和访问方式是一样的。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总结:结构体的对齐就是利用内存换取运行时间的手段。

如何节省空间?

        将占用空间小的成员集中到一起。

1.5 修改默认对齐数
#pragma pack()//恢复默认对齐数
#pragma pack(1)//设置对齐数为1
#include <stdio.h>
#include <stddef.h>
#pragma pack(1)//设置对齐数为1
struct S1
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//恢复默认对齐数

int main()
{
	printf("%d\n", sizeof(struct S1));//6
	printf("%d\n", offsetof(struct S1,c1));//0
	printf("%d\n", offsetof(struct S1,i));//1
	printf("%d\n", offsetof(struct S1,c2));//5
	return 0;
}

此时S1的空间对齐方式

(1)c1是第一个成员,从0偏移量存储,占用一个字节(灰色);

(2)i为整型变量,对齐数为1,偏移量为1,(黄色);

(3)c2字符变量,对齐数为1,占用偏移量为5的位置(红色);

(4)确定结构体的大小,从c1到c2处,占用了6个字节的空间。

1.6 结构体传参

        结构体传参一般采用地址传参的方式,在传递值传参的时候,需要重新创建变量,浪费大量的内存空间,传址调用较为省空间。

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

2.位段

位段的声明和结构体类似,有两种不同的标志

1.位段的成员必须是 int 、unsigned int、或者是signed int;

2.位段的成员名后有一个冒号和一个数字。

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

A是一个位段类型,位段的大小是多少呢?

printf("%d", sizeof(struct A));//8

原本4个整型数据,占据16个字节,如何在内存中限制到8个字节的呢?

2.1 位段的内存分配

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;
};
    struct S s = {0};
    s.a = 10;
    s.b = 12;
    s.c = 3;
    s.d = 4;

        位段开辟空间一次开辟一个字节,不够使用的话再开辟另一个字节;位段是从低地址到高地址存储的,在每个字节中也是从低位向高位存储;

2.2 位段的跨平台问题

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

特点:位段和结构体相比,位段的存储空间为共用空间,位段更加节省空间,但是存在跨平台问题。

3.枚举

3.1枚举类型的定义

可以被一一列举的变量,语法形式和结构体类似,例如生活中的,

1.一周的星期一到星期日有限的7天,可以一一列举;

2.性别有:男、女分别;

3.一年有12个月,可以一一列举。

enum Day
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};
enum Sex
{

    MALE,
    FEMALE,
    SECRET
};

三原色

enum Color
{
    RED,
    GREEN,
    BLUE

};

上述是有可能取到的值,默认从零开始一次递增,在定义的时候可以赋初值,示例

#include <stdio.h>
enum Day
{
	Mon = 1,
	Tues = 2,
	Wed = 3,
	Thur = 4 ,
	Fri = 5,
	Sat = 6,
	Sun = 7 
};
int main()
{
	printf("%d\n", Mon);//1
	printf("%d\n", Tues);//2
	return 0;
}

        枚举常量就是给特定的字符赋予一定的数值,在后续的使用中Mon和1等价。

3.2枚举的优点

和#define相比枚举创建的常量可以在调试中显示,而宏定义是不可以的。

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

3.3枚举的使用

下述代码是用于C51单片机的按键控制LED亮灭的程序,创建的枚举变量使用的时候程序较好理解,如果将KEY1_PRESS为1的话,并不是特别的直观的表达出按键1以将按下。

#include <REGX52.H>
#include <stdio.h>
//引脚定义
#define  SMG_A_DP_PORT  P1 
sbit KEY1 = P0 ^ 0;
sbit KEY2 = P0 ^ 1;
sbit KEY3 = P0 ^ 2;
sbit KEY4 = P0 ^ 3;
sbit LED1 = P2 ^ 0;
sbit LED2 = P2 ^ 1;
sbit LED3 = P2 ^ 2;
sbit LED4 = P2 ^ 3;

enum KEY
{
	KEY_UNPRESS = 0,
	KEY1_PRESS = 1,
	KEY2_PRESS = 2,
	KEY3_PRESS = 3,
	KEY4_PRESS = 4,
};//枚举
char key_scan(mode);//当mode=0的时候 单次扫描 mode=1 连扫
void main()
{

	unsigned char  ret = 0;

	while (1)
	{
		ret = key_scan(0);
		switch (ret)
		{
		case KEY1_PRESS:
		{
			LED1 = !LED1;
			break;
		}
		case KEY2_PRESS:
		{
			LED2 = !LED2;
			break;
		}
		case KEY3_PRESS:
		{
			LED3 = !LED3;
			break;
		}
		case KEY4_PRESS:
		{
			LED4 = !LED4;
			break;
		}

		}
	}

}

//延时函数 当1=ten_us,延时10us
void delay_10us(unsigned int ten_us)
{
	while (ten_us--);
}
char key_scan(mode)//当mode=0的时候 单次扫描 mode=1 连扫
{
	static char key = 1;
	if (mode)
	{
		key = 1;
	}
	if (key == 1 && (KEY1 == 0 || KEY2 == 0 || KEY3 == 0 || KEY4 == 0))
	{
		key = 0;
		delay_10us(1000);
		if (KEY1 == 0)
			return KEY1_PRESS;
		else if (KEY2 == 0)
			return KEY2_PRESS;
		else if (KEY3 == 0)
			return KEY3_PRESS;
		else if (KEY4 == 0)
			return KEY4_PRESS;
	}
	else if (KEY1 == 1 && KEY2 == 1 && KEY3 == 1 && KEY4 == 1)
	{
		key = 1;
	}
	return KEY_UNPRESS;
}

4. 联合(共用体)

4.1 联合类型的定义

联合是一种特殊的自定义类型,定义的变量也包含一系列的成员这些成员会共用一块内存空间。

union UN 
{
	char c;
	int i;
};
int main()
{
	union UN un;
	printf("%d\n", sizeof(un));//4
	printf("%p\n", &un);//00EFFB08
	printf("%p\n", &(un.c));//00EFFB08
	printf("%p\n", &(un.c));//00EFFB08
	return 0;
}

        三次取地址的结果是一样的,说明c和i的起始地址的是一样,可以得出,两个变量占用得空间开始是一样得,当然两个变量是不可以同时使用得,c占用4个字节的第一个字节,i占用四个字节,如图

4.2 联合的特点

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

int main()
{
	union Un
	{
		int i;
		char c;
	};
	union Un un;
	//下面输出的结果是什么?
	un.i = 0x11223344;
	un.c = 0x55;
	printf("%x\n", un.i);//11223355

	return 0;
}

        un的前四个字节为11223344,后有将44修改为了55,因为VS2017上位小端存储。

可以封装成函数判断大小端存储:

#include<stdio.h>
int check_sys()
{
	union Un
	{
		int i;
		char c;
	}un;
	un.i = 1;
	return un.c;//判断变量,低位是否为1或者0
}
int main()
{
	int ret = check_sys();
	if (ret)
	{
		printf("小端存储");
	}
	else
		printf("大端存储");
	return 0;
}
4.3 联合大小的计算

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

int main()
{
	union Un1
	{
		char c[5];
		int i;
	};
	union Un2
	{
		short c[7];
		int i;
	};
	//下面输出的结果是什么?
	printf("%d\n", sizeof(union Un1));//8
	printf("%d\n", sizeof(union Un2));//16
}

Un1的内存大小为8

(1).c[5]的对齐数位1,占用5个字节的空间

(2).i的对齐数为4,占用内存4个字节的空间,

(3)最大对齐数为4,再取4的倍数,内存大小为8个字节;

Un2的内存大小为16

(1).c[7]的对齐数位2,占用14个字节的空间

(2).i的对齐数为4,占用内存4个字节的空间,

(3)最大对齐数为4,再取4的倍数,内存大小为16个字节;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值