[C语言]自定义类型(结构体~枚举~联合体)

简介: [C语言]自定义类型(结构体~枚举~联合体)

前言

者:小蜗牛向前冲

名言我可以接收失败,但我不能接收放弃

如果觉的博主的文章还不错的话,还请 点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正。

 



在C语言中常见的数据类型:

整形:int  short  long  long long  char(字符类型)

浮点型 flaot  double

这些类型是C语言都帮我们定义好的,下面我们将继续学习自定义类型。

自定义类型

简单的来说就是不由系统定义,而是由程序设计者自己定义的类型,在本篇博主中,我们重点分享结构体,位段,枚举,联合相关知识

结构体

理解

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

结构体的声明

是借助关键字struct来进行的。

struct关键字是用来定义一个新的类型,这个新类型里面可以包含各种其他类型,称为结构体。结构体 (struct)是一种自定义的数据类型,就是把一组需要在一起使用的数据元素组合成一个新的类型。

1形式

struct tag

{

member-list;

}

variable-list;

tag:是结构体的标签

member-list:是结构体的成员

variable-list:这是给结构体取的变量名

注意:

定义结构体时成员是不用初始化的。

结构体的变量名可以在定义是就声明,也可以在需要的时候定义。

代码演示:

#define  _CRT_SECURE_NO_WARNINGS
 
#include<stdio.h>
 
struct studen
{
  char* name;//姓名
  int num;//学号
  int age;//年龄
};

这样我们就简单的声明来一个结构体,结构体的标签为studen,我们可以看到这里并没有给结构体取变量名。

2 特殊声明(匿名结构体)

就是指在声明结构体的时候,不写结构体的标签。因为没有结构体的标签,所以只能用一次。

#include<stdio.h>
//匿名结构体类型
struct
{
  int a;
  float b;
  char c;
}s1;
 
struct
{
  int a;
  float b;
  char c;
}a[20],*p;
 
int main()
{
  p = &a;//?
}

我们可以看的到什么的代码,这样是可以行的吗?我们编译起来

发现出现一个警告,为什么呢?这二个结构体的成员不都是一样的吗?他们不相同吗?

在上面我也说过匿名结构体只能用一次,所以说匿名结构体即使成员相同,但由于是匿名的,只能使用一次,二者结构体是不相同的,我们应该慎重使用匿名结构体(可以在只使用一次的场景使用)。

3结构体的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?

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

其实是可以的,但我们不能直接存储结构体本身,而要存放结构体的地址。

为什么呢?

其实如果存放结构体本身,因为要不断自引用结构体,这使得结构体的大小就不确定了,而且这个结构体的大小将会非常大,无法存储。所有我们存放结构体的地址就这样就使得结构体的大小确定(指针的大小是确定的),我们还可以通过结构体的地址找到结构体的成员,这样链表就得以实现。

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

结构体变量的初始化我把他归类为3类:

#include<stdio.h>
//类型1
struct data
{
  int a;
  int b;
  int c;
}p1;//定义结构体的时候,定义变量名p1
//类型2
struct studen
{
  struct data;
  const char* name;
  int age;
};
//类型3
struct age
{
  int year;
  int month;
  int day;
}p3;
void print(struct age* p3)
{
  p3->year = 1949;
  p3->month = 10;
  p3->day = 1;
  printf("year = %d month = %d day = %d\n", p3->year, p3->month, p3->day);
}
int main()
{
  struct data p1 = { 1,2,3 };//赋值
  printf("%d %d %d\n", p1.a, p1.b, p1.c);
  struct studen p2 = { 4,5,6,"zhangshan",18 };
  printf("%d %d %d\tname = %s age = %d\n", p2.a, p2.b, p2.c, p2.name, p2.age);
  //struct age p3 = { 1949,10,1 };
  print(&p3);
  return 0;
}

类型1

struct data结构体定义的时候为结构体定义了名字p1,初始化直接用大括号初始就可以了。

类型2

struct studen结构体定义的时候并没有为结构体取名字,在初始化的在取名为p2,要是可以的,值得说明的是在结构体在包含结构体初始化也是没说明区别的。

类型3

其实这个类型3和其他类型的区别主要是,他在初始化的时候是在函数中,我们为函数传递了结构体strcut age的地址,所有要初始化结构体要用" -> "

我们知道结构体是怎么定义和初始化的,那么我们又是如何访问结构体中的成员的呢?

结构体变量是指针用:" -> "访问。

结构体变量非是指针用:" . "访问。

5 结构体内存对齐

上面我们提到结构体自引用时,传的是指针,而不是结构体本身是为防止结构体大小过大。那么结构体的大小又是如何计算的呢?

#include<stdio.h>
 
//1
struct studen
{
  const char* name;
  int age;
}p1;
 
//2
struct data
{
  int a;
  double b;
  char c;
}p2;
 
//3
struct datas
{
  char c;
  int a;
  double b;
}p3;
 
int main()
{
  printf("%u %u %u\n", sizeof(p1), sizeof(p2), sizeof(p3));
}

下面这些结构体的大小是多少呢?

是把结构体成员的大小都加起来吗?

结构体1

1+4 =5?

结构体2和结构体3的成员相同

1+4+8=13?

是上面这么结果对面,下面我们打印常出来看看。

8 24 16 为什么啊,这就不得不提结构体大小的计算方法结构体内存对齐。

结构体内存对齐的规则:

1. 第一个成员在与结构体变量偏移量为0的地址处

2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8

3. 结构体总大小最大对齐数(每个成员变量都有一个对齐数)的整数倍

4. 如果嵌套了结构体的情况嵌套的结构体对齐到自己的最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

下面我们将画图来理解为什么结构体1,2,3的大小是8,24,16。

 


图1


 


图2


 


图3


不知道大家观察到没,p2和p3的成员是一样的就是定义的顺序不同,但分配的内存空间大小是不一样的,而且字节大小越小的成员先定义,那么分配到的空间就更少,浪费的内存空间也跟小。

总结

总体来说:

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

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:

让占用空间小的成员尽量集中在一起

为什么要存在内存对齐呢?

1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。

2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

上面我们提到Vs是有默认对齐数(8)的,由于对齐数影响结构体在内存在的分配,有时候我们为了控制分给结构体的内存需要更改默认对齐数,可以通过#pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。

#pragma pack(1)//设置默认对齐数为1
struct day
{
  int a;
  int b;
  int c;
};
#pragma pack();//恢复默认对齐为8

6结构体传参

struct s
{
  int data[10];
  int num;
}p1;
 
//传值
void print1(struct s p)
{
  printf("%d\n", p.num);
}
//传地址
void print2(struct s* p)
{
  printf("%d\n", p->num);
}
 
int main()
{
  struct s p1 = {{1,2,3,4,5},10};
  print1(p1);//传值
  print2(&p1);//传地址
}

对于上面二种传参方式,我们选择那种呢?

肯定是print2

为什么呢?

原因:

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

结论:

结构体传参的时候,要传结构体的地址。

位段

位段又是啥子呢?下面我们先了解位段的声明

位段的声明和结构是类似的,有两个不同:

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

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

1 声明

struct data
{
  char a : 2;
  int b : 2;
  long long c : 4;
};
 
int main()
{
  printf("%u\n", sizeof(struct data));
}

data就是一个位段,那么的占几个字节呢?

从中可以看出位段也是遵循内存对齐规则的。

2 位段的内存分配

1. 位段的成员可以是 int   unsigned int   signed int 或者是 char (属于整形家族)类型

2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

下面我们继续研究一下位段的内存分配。

struct s
{
  char a : 2;
  char b : 4;
  char c : 5;
};
 
int main()
{
  struct s p1 = { 0 };
  p1.a = 2;
  p1.b = 4;
  p1.c = 10;
  return 0;
}

 

从图中我们可以看出,位段其实就是可以自己分配内存给自己使用。其中" : "后面的数字是分配的给内存几个bit位用来存储成员。(vs的分配方式

3位段的跨平台问题

1. int 位段被当成有符号数还是无符号数是不确定的。

2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。

3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。

4 位段的应用

主要运用于数据传输中。

枚举

枚举顾名思义就是一一列举。

把可能的取值一一列举。

1枚举的定义

enum peo//个人信息
{
  name,
  sex,
  Stature,
  age
};

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

2枚举的优点

为什么使用枚举?

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

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

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

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

4. 便于调试

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

对于枚举很多人可能觉的没什么用,其实不然,枚举还是有许多优点的,我们在以后的编译时遇到一定要细细体会。

3 枚举的使用

enum peo//个人信息
{
  name,
  sex,
  Stature,
  age
};
//使用
enum peo mam = name;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。

枚举的使用场景还需大家在合适的场景使用。

联合(共用体)

1 联合类型的定义

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

比如:

//联合体
//声明
union s
{
  char a;
  int b;
};
 
int main()
{
  //联合体变量的定义
  union s un;
  //计算联合体的大小
  printf("%d\n", sizeof(un));
}

2 联合的特点

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

我们思考一个问题?

&(un.a)和&(un.b)得到的地址是一样的吗,可以肯定是一样的,为什么怎么说呢?因为二者公用一个内存的话,二者指针指向的位置必须是一样的这样才能找到相应的空间。

3联合大小的计算

联合的大小至少是最大成员的大小。

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

下面看到思考题,来结束今天的分享。

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

这里就不过多解释了。

 


相关文章
|
22小时前
|
存储 安全 编译器
C语言(13)----联合和枚举
C语言(13)----联合和枚举
9 1
|
2天前
|
C语言
C语言——枚举
C语言——枚举
7 0
|
4天前
|
存储 编译器 C语言
【C语言】自定义类型 -- -- 结构体、位段、枚举、联合体
【C语言】自定义类型 -- -- 结构体、位段、枚举、联合体
6 0
|
6天前
|
编译器 C语言 C++
c语言:自定义类型——结构体
c语言:自定义类型——结构体
8 0
|
6天前
|
存储 开发框架 编译器
C语言进阶—自定义类型:结构体,枚举,联合
C语言进阶—自定义类型:结构体,枚举,联合
|
22小时前
|
C语言
C语言(11)----内存函数
C语言(11)----内存函数
5 1
|
22小时前
|
机器学习/深度学习 编译器 C语言
C语言(6)----函数的递归思想
C语言(6)----函数的递归思想
5 0
|
22小时前
|
算法 C语言 C++
C语言进阶:冒泡排序函数初步实现
C语言进阶:冒泡排序函数初步实现
|
22小时前
|
程序员 C语言 C++
C语言初识函数
C语言初识函数
|
22小时前
|
存储 测试技术 C语言
C语言内存管理函数研究
C语言内存管理函数研究