目录

前言

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个字节;

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐