自定义类型:结构体2.0(初阶+进阶)+位段+枚举+联合

目录

一.结构体

🐓1.1什么是结构体🏀🐔

1.2结构体的声明

1.3结构体成员的类型与创建变量🚅

1.4结构体成员的访问🏠

1.5结构体和typedef

错误使用☀️

正确使用☁️

不同点⚡

1.6特殊的声明

匿名结构体定义变量🐶

匿名结构体成员相同但类型不同🐰

 1.7结构的自引用📍

1.8结构体变量的定义和初始化✈️

1.9结构体内存对齐🚁

1.9.1先放int,再放char📣

1.9.2调换顺序:先放char,再放int🔈

🔎如何计算两个结构体变量的偏移量差距🔎

1.9.3先放两个char,再放一个int🔏

1.9.4先放一个double,再放一个char,最后放一个int💡

1.9.5嵌套结构体计算 🔦

1.9.6回到整个题目🔨

2.0修改默认对齐数⭕

2.1 结构体传参⭕

二.位段

2.1 什么是位段⁉️

2.2 位段的内存分配❗

2.3 位段的跨平台问题➰

2.4 位段的应用✅

 三. 枚举

3.1 枚举类型的定义❎

 3.2 枚举的优点❗

四. 联合(共用体)

4.1 联合类型的定义

4.2 联合的特点⭕

4.3 联合大小的计算 ⭕

先放一个char数组,再放一个int变量

先放一个short数组,再放一个int变量          


一.结构体

🐓1.1什么是结构体🏀🐔

同一种类型的数据的集合是数组,和数组不同,结构体是多种类型的数据的集合。

现实生活中,我们要汇总学生的体检信息时,我们会为名字、身高、体重分别单独建表吗?显然不会。通常是给每人发一张"体检卡",在资料上面分别记录着名字、身高、体重等信息。如果一个班上有60个学生,那么50张"体检卡"即为一个集合。

下图即为"体检卡":

1.2结构体的声明

上图4个数据进行结构体的声明

  • char[64]型的姓名
  • int型的身高
  • float型的体重
  • long型的奖学金

其中,结构体的名字student称为结构名。{}中声明的name、height等称为结构体成员

注意结构体声明的末尾也要加上分号。

1.3结构体成员的类型与创建变量🚅

结构的成员可以是标量、数组、指针,甚至是其他结构体。

#include<stdio.h>
//定义学生类型
struct Stu
{
	//成员变量
	char name[20];
	int age;
	float weight;
} s4, s5, s6;//全局变量

int main()
{
	//int num = 0;
	//通过类型创建变量
	struct Stu s1;//局部变量
	struct Stu s2;
	struct Stu s3;

	return 0;
}

1.4结构体成员的访问🏠

  • 结构体变量访问成员
  • 结构变量的成员是通过点操作符(.)访问的。点操作符接受两个操作数

 我们可以看到 s 有成员name和age

那我们如何访问s的成员?🍌

struct S s;

strcpy(s.name, "zhangsan");//使用.访问name成员
s.age = 20;//使用.访问age成员
结构体指针访问指向变量的成员
有时候我们得到的不是一个结构体变量,而是指向一个结构体的指针。
那该如何访问成员?
struct Stu
{
 char name[20];
 int age;
};
void print(struct Stu* ps)
{
 printf("name = %s   age = %d\n", (*ps).name, (*ps).age);
    //使用结构体指针访问指向对象的成员
 printf("name = %s   age = %d\n", ps->name, ps->age);
}
int main()
{
    struct Stu s = {"zhangsan", 20};
    print(&s);//结构体地址传参
    return 0;
}

两种写法同时展现:

struct Book
{
	char name[20];
	int price;
};

void print(struct Book* p)
{
	printf("%s %d\n", p->name, p->price);
}

int main()
{
	struct Book b1 = {"C语言", 66};
	struct Book b2 = { .price = 80, .name = "java"};
	//结构体变量.结构体成员
	printf("%s %d\n", b1.name, b1.price);
	printf("%s %d\n", b2.name, b2.price);
	//结构体指针->结构体成员
	//struct Book* p1 = &b1;
	print(&b1);
	return 0;
}

1.5结构体和typedef

typedef声明是创建数据类型的同义词的声明(而非创建新的数据类型)🐔

错误使用☀️

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

 应该先有一个完整的类型名才能使用Node,而成员变量里面已经使用了,存在先后顺序问题

正确使用☁️

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

为简化创建变量的过程,可以讲类型名重定义,就会使用到typedef

typedef struct Book
{
	char name[20];
	int price;
}Book;//不能再将全局变量加在这里后面

Book b3;//全局
Book b4;//全局

int main()
{
	struct Book b1;
	struct Book b2;
	Book b3;//struct Book的别名
	return 0;
}

类型名的重命名也可以用到

struct Book
{
	char name[20];
	int price;
};
typedef struct Book Book;

不同点⚡

这里是两个不同的东西
struct 
{
	char c;
	int a;
	double d;
}s1;//匿名结构体变量


typedef struct
{
	int data;
	char c;
} S;//结构体类型,本来是没有名字的,现在重新起名字

1.6特殊的声明

匿名结构体定义变量🐶

struct  //省略了结构名 struct+结构名=类型名
{
	char c;
	int a;
	double d;
}s1;//匿名结构体只能这样定义变量
int main()
{
  struct s2;//错误,不能这样写
  return 0;
}

匿名结构体是一次性用品

匿名结构体成员相同但类型不同🐰

struct 
{
	char c;
	int a;
	double d;
}s1;

struct
{
	char c;
	int a;
	double d;
}* ps;

int main()
{
	ps = &s1;//err

	return 0;
}

  

  • 相同结构体类型:

通过同一个结构体 定义出来的变量 就相当于 int a; int b 
a和b是两个不同的变量,但是它们的类型都是int类型

  • 结构体类型不同: (上图就是结构体类型不一样)

用一个结构体类型的指针 指向另一个结构体类型的指针时  编译器会报出警告,类型不兼容

虽然两个匿名结构体类型的成员都完全相同,但是他们的类型还是不同的

 1.7结构的自引用📍

//这是一个错误的示范
struct Node
{
	int data;
	struct Node n;//自己类型的变量 
};

这里是结构体中的一个成员是它这个结构体本身。 那它无限套娃 永远也停止不了

这属于线性数据结构,一个结点包括数据域和指针域

正确写法:

//当一个结构体要找到另外一个同类型的结构体的时候
//应该在自己类型里面包含一个自己类型的指针,而不是自己类型的变量 
struct Node
{
	int data;//4
	struct Node* next;//4/8
};

int main()
{
	struct Node n1;
	struct Node n2;
	n1.next =  &n2;

	return 0;
}

1.8结构体变量的定义和初始化✈️

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

struct S s3;//这样定义也是全局变量

struct B
{
	float f;
	struct S s;
};

  int main()//在这里定义的全是局部变量
{
	//int arr[10] = {1,2,3};
	//int a = 0;//有了值才有确定性
    //结构体怎么初始化呢?
	struct S s2 = {100, 'q'};
	
	struct B sb = { 3.14f, {200, 'w'}};//结构体的嵌套初始化正常来说要按照顺序,
//不按照顺序初始化
    struct S s3 = {.c = 'r', .a = 2000};//指定顺序   

	printf("%f,%d,%c\n", sb.f, sb.s.a, sb.s.c);//结合体成员的访问,后面两个是因为是嵌套了一个结构体

   return 0;
}

另一个例子:

struct S
{
	char c;
	char arr[10];
	int* p;
}s1, s2;

struct S s3;

struct B
{
	int a;
	struct S s;
	double d;
}sb1, sb2;

struct B sb3;

int main()
{
	struct S s4 = {'c', "zhangsan", NULL};
	int a = 10;
	struct B sb4 = { 100, {'q', "lisi", &a}, 3.14};

	return 0;
}

1.9结构体内存对齐🚁

该如何计算结构体的大小:结构体内存对齐🐔🏀👖
这也是一个特别热门的考点: 结构体内存对齐
#include<stdio.h>
struct S1
{
	int a;
	char c;
};

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

struct S3
{
	char c1;
	int a;
	char c2;
	char c3;
}; 

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

运行结果: 

 按以往的知识来说,从S1到S3的值分别推测为5,6,7,可是结果却不是这样的,并且S3比S2还多了一个成员变量,但是它们的大小却是相等的

 为

为什么呢?计算结构体,需要用到以下规则  

 接下来,需要详细说明一下结果是如何求出来的

1.9.1先放int,再放char📣

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

1.9.2调换顺序:先放char,再放int🔈

那么我们讲char c和int a的位置调换一下,结果又会不会不一样呢?

struct S1
{
	char c;
	int a;

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

c和a之间真的浪费了3个字节的空间吗?

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

struct S
{
	char c1;
	int a;

};
int main(){
	struct S s = {0};
	printf("%d\n", offsetof(struct S, c1));//0
	printf("%d\n", offsetof(struct S, a));//4

	return 0;
}

🔎如何计算两个结构体变量的偏移量差距🔎

方法1:取地址计算,4和8之间差4

可以发现它们之间差了4个字节

方法2:offsetof函数

使用之前要引用一下头文件

可以看到是返回成员变量的偏移量

 执行效果:

1.9.3先放两个char,再放一个int🔏

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

1.9.4先放一个double,再放一个char,最后放一个int💡

struct S3
{
	double d;
	char c;
	int i;
};
int main()
{	
printf("%d\n", sizeof(struct S3));

	return 0;
}

执行效果:

1.9.5嵌套结构体计算 🔦

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

1.9.6回到整个题目🔨

#include<stdio.h>
struct S1
{
	int a;
	char c;
};

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

struct S3
{
	char c1;
	int a;
	char c2;
	char c3;
}; 

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

为什么存在内存对齐?

大部分的参考资料都是如是说的:

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

总结:

1.对于一个int 类型,若是按照内存对齐来存储,处理器只需要访存一次就可以读取完4个字节

2.若没有按照内存对其来读取,如上图所示,就需要访问内存两次才能读取出一个完整的int 类型变量

总体来说:

结构体的内存对齐是拿 空间 来换取 时间 的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起
#include<stdio.h>
struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));
	return 0;
}

 调整一下顺序就可以减少空间的浪费

2.0修改默认对齐数

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

#pragma pack(1)//设置默认对齐数为1
struct S
{
	char c1;//1 1 1
	int i;  //4 1 1
	char c2;//1 1 1
};
#pragma pack()//取消设置的默认对齐数,还原为默认


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

	return 0;
}

因为它们每一个对齐数都是1,所以只要是1的倍数的偏移量位置,它们都可以存放,所以就不会有空余的内存

 结论:

结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。(我们不想对齐的时候就把对齐数设置成1)

2.1 结构体传参

 函数压栈问题

结构体传参时,结构体压栈所需要的空间开销就会大一些

若是传地址,无非就是4/8byte,开销会大大减少

例子1:

struct S
{
	int arr[100];
	int n;
};

void print1(struct S ss)
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ss.arr[i]);
	}
	printf("\n%d\n", ss.n);
}

void print2(struct S* ps)
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("\n%d\n", ps->n);
}

int main()
{
	struct S s = { {1,2,3,4,5}, 100 };
	print1(s);
	print2(&s);

	return 0;
}

例子2:

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)//这里加一个const避免不小心改掉实参
{
 printf("%d\n", ps->num);
}
int main()
{
 print1(s);  //传结构体
 print2(&s); //传地址
 return 0;
}
上面的 print1 print2 函数哪个好些?
答案是:首选 print2 函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降
结论:
结构体传参的时候,要传结构体的地址

二.位段

2.1 什么是位段⁉️

位段 :二进制位

位段的声明和结构是类似的,有两个不同:
1. 位段的成员必须是 int unsigned int signed int
2. 位段的成员名后边有一个冒号和一个数字

比如:

//位段 - 二进制位
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};//47bit --> 6*8=48 --> 6byte ?
int main()
{
	struct A sa = { 0 };
	printf("%d\n", sizeof(sa));

	return 0;
}

 A就是一个位段类型。以上结果猜测为47bit,可是结果却是8byte -->64bit,还浪费了空间?

注意:
struct A
{
    int _a;
    int _b;
    int _c ;
    int _d;
};//这里面是结构体成员
这里每一个int 占4byte,总共16byte -->128bit对比64bit都节省了一半的空间

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

假设:

内存开辟如下:

整体思路:先开辟一个字节的空间,如果放不下就逐渐向高地址开辟,证实了假设是正确的

2.3 位段的跨平台问题

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

2.4 位段的应用

 三. 枚举

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

这里就可以使用枚举了

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
};
#include<stdio.h>
enum Sex
{
   MALE,
   FEMALE,
   SECRET
};
int main()
{
	enum Sex s = FEMALE;//1,若赋值成1在c语言中是没问题的,在c++会出现错误,就拿上面定义的可能取值给它赋值
	printf("%d\n", MALE);
	printf("%d\n", FEMALE);
	printf("%d\n", SECRET);
}

 3.2 枚举的优点

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

1. 增加代码的可读性和可维护性

enum OPTION
{
	EXIT,//表示0
	PLAY,//1
	ADD,//2
	DEL//3
};

2. #define定义的标识符比较枚举有类型检查,更加严谨。

#define EXIT 0
#define PLAY 1
  • #define宏常量是在预编译阶段进行简单替换,枚举常量则是在编译的时候确定其值。

3. 防止了命名污染(封装)

4. 便于调试

  • 一般在编译器里,可以调试枚举常量,但是不能调试宏常量

 5. 使用方便,一次可以定义多个常量

  • 枚举可以一次定义大量相关的常量,而#define 宏一次只能定义一个
  • 枚举可以自增1,这样不用每一个值都定义,而宏必须每个值都定义
  • 枚举是一个集合,代表一类值,像你代码中的颜色归为一类,方便使用,而宏不能形成集合。

四. 联合(共用体)

4.1 联合类型的定义

联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
union Un
{
	char c;
	int i;
};
int main()
{
	union Un u;
	printf("%d\n", sizeof(u));
	printf("%p\n", &u);
	printf("%p\n", &(u.i));
	printf("%p\n", &(u.c));

	return 0;
}

4.2 联合的特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联
合至少得有能力保存最大的那个成员)。
面试题: 判断当前计算机的大小端存储
union Un
{
	char c;
	int i;
};

int main()
{
	union Un u;
	u.i = 1;
	if (u.c == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

4.3 联合大小的计算 ⭕

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

先放一个char数组,再放一个int变量


union Un
{
	char arr[5];//1 4 1 -->共5个字节
	int n;// 4 8 4
};

int main()
{

	printf("%d\n",sizeof(union Un));

	return 0;
}

​编辑

先放一个short数组,再放一个int变量          

union Un
{
	short s[7];
	int n;

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

同理:结果必须也是4的倍数,short数组已经占了14个byte,所以联合体大小为16byte

本章完。欢迎各位大佬补充!    

  • 19
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 30
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 30
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Dream_Chaser~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值