进阶C语言:自定义类型

简介: C语言中的结构体、位段、枚举、联合的详细讲解。

自定义类型有结构体、枚举、联合体,关于结构体和枚举在之前的文章中提到过,但没有进行详细的介绍,那么在本片博客中将会细致的带大家了解自定义类型

紫蓝色几何渐变科技互联网微信公众号封面 (1).gif

目录

1.结构体

1.1结构体的声明

1.2特殊声明

1.3结构体的自引用

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

1.5结构体内存对齐

1.6修改默认对齐数

1.7结构体传参

2.位段

2.1什么是位段

2.2位段的内存分配

2.3 位段的跨平台问题

3.枚举

3.1枚举类型的定义

3.2枚举的优点

3.3枚举的使用

4.联合(共用体)

4.1联合类型的定义

4.2联合的特点

4.3联合体的大小

5.通讯录


1.结构体

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

1.1结构体的声明

struct tag
{
    member-list;
}variable-list;

image.gif

例如描述一个学生:

struct Student
{
  char name[20];  //姓名
  int age;    //年龄
  char sex[5];  //性别
  float height; //身高
  float weight; //体重
};//分号不能丢

image.gif

1.2特殊声明

在声明结构体的过程中,可以不完全声明结构体

//匿名结构体类型
struct
{
  int a;
  char b;
  float c;
}x;
struct
{
  int a;
  char b;
  float c;
}x[20], *p;

image.gif

上面的结构体在声明的时候省略了结构体的标签,那么如果这样使用,会不会有风险呢?

p = &x;  //是否合理?

image.gif

这样的使用方法是不合理的,在编译的过程中,编译器会将上面的两个结构体当成两个完全不同的类型,所以不能这样子使用。

1.3结构体的自引用

结构体的自引用就是在一个结构体里面再包含一个与其类型相同的结构体,那该怎么去引用呢?

struct Node
{
  int data;
  struct Node;
};
//如果这样自引用会出现什么问题呢?

image.gif

如果这样写结构体的自引用,那么在计算结构体的大小时就出现了问题,一个结构体里面永远都包含这个结构体,所以就乱套了,因此这种方法是不可取的,我们都知道可以通过地址来找到一块空间,所以关于结构体的自引用得换一种写法:

//正确的结构体自引用写法
struct Node
{
  int data;
  struct Node* n;  //使用结构体指针来指向一个同类型的结构体
};

image.gif

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

struct Point
{
  int x;
  int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = { x, y };
struct Stu //类型声明
{
  char name[15];//名字
  int age; //年龄
};
struct Stu s = { "zhangsan", 20 };//初始化
struct Node
{
  int data;
  struct Point p;
  struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化
struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化

image.gif

1.5结构体内存对齐

关于结构体内存的对齐很直接的问题就是结构体大小的计算,在计算一个结构体的大小时该怎么去计算呢?是直接将结构体中的类型大小全部加起来吗?

//结构体大小
#include <stdio.h>
struct S
{
  char x;
  int y;
  char z;
};
int main()
{
  int sz = sizeof(struct S);
  printf("%d\n", sz);
  return 0;
}

image.gif

这个结构体里面有三个变量,分别是char和int类型,那么计算它的大小是不是一个int类型大小加上两个char类型的大小呢?我们可以来验证一下:

image.gif编辑

这时就要涉及到结构体内存的对齐了,那结构体的对齐规则是什么样子呢?

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

2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

       对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。

       VS中默认的值为8。

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

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

那么对于上面的一段代码就可以分析一下:

image.gif编辑

如果在一个结构体里面再嵌套一个结构体,那它的大小又该是多少呢?

#include <stdio.h>
struct S1
{
  char x;
  char z;
};
struct S2
{
  struct S1 s;
  int y;
  short z;
};
int main()
{
  int sz = sizeof(struct S2);
  printf("%d\n", sz);   //12
  return 0;
}

image.gif

如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。所以s的对齐数为S1中最大的对齐数

为什么要内存对齐?

1. 平台原因(移植原因):

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

2. 性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。

原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总结:

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

因此在设计结构体时尽量让所占空间较小的变量在一起

//例如:
struct S1
{
  char c1;
  int i;
  char c2;
};
struct S2
{
  char c1;
  char c2;
  int i;
};

image.gif

上述代码结构体S2就比结构体S1更省空间

1.6修改默认对齐数

在VS平台上面默认对齐数是8,那在别的平台上面我们不了解它的默认对齐数时,可以修改默认对齐数成为我们想要的值,然后使用完再恢复。

可以使用#pragma这个预处理指令来修改默认对齐数

#include <stdio.h>
#pragma pack(4)//设置默认对齐数为8
struct S1
{
  char c1;
  int i;
  char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
  char c1;
  int i;
  char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
  printf("%d\n", sizeof(struct S1));   //12
  printf("%d\n", sizeof(struct S2));   //6
  //设置默认对齐数为1,就表示没有对齐,所有变量都是紧挨着放的
  return 0;
}

image.gif

结论:

结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。

1.7结构体传参

关于结构体传参在之前也提到过,直接上代码:

#include <stdio.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);
}
//结构体地址传参
void print2(struct S* ps)
{
  printf("%d\n", ps->num);
}
int main()
{
  print1(s); //传结构体
  print2(&s); //传地址
  return 0;
}

image.gif

结构体在传参的时候最好使用传值调用:

1.函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

2.如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

2.位段

位段是一种和结构体很相似的一种类型

2.1什么是位段

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

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

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

例如:

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

image.gif

A就是一个位段类型的,后面的数字表示比特位,那么这个位段的大小是多少呢?

2+5+10+30 = 47,47个比特位是不是最多使用6个字节呢?接下来就来看看位段在内存中是怎么分配空间的:

2.2位段的内存分配

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

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

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

现在再来分析上面的代码:

a占2个比特位,所以先要在内存中开辟4个字节大小的空间,也就是32个比特位,这时a占2个比特位,b占5个比特位,c占10个比特位,这时还剩下了15个比特位,但是d要占30个比特位,所以又得开辟4个字节的大小,这时就有一个问题,那要不要用剩下的15个比特位,还是直接在重新开辟的4个字节的空间里面使用,C语言也没有明确规定,这主要取决于编译器。还存在一个问题,在这些变量存储的时候是从高地址往低地址存还是低地址往高地址开始存,这也是不确定的,C语言也没有明确的规定,还是取决于编译器。

练习:(VS2019环境下测试)

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

image.gif

在VS2019的环境下:

1.分配在内存中的比特位是从右向左开始使用的。

2.分配的内存剩余比特位在不够使用时,浪费掉!

就上面这段代码,来简单的分析一下:

image.gif编辑

image.gif编辑image.gif编辑

2.3 位段的跨平台问题

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

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

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

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

总结:

跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

3.枚举

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

把可能的取值一一列举。

比如我们现实生活中:

一年有12个月,可以一一列举

一周有七天,从周一到周日

性别,男、女

这样子就可以使用枚举

3.1枚举类型的定义

//枚举
enum Day//星期
{
  Mon,
  Tues,
  Wed,
  Thur,
  Fri,
  Sat,
  Sun
};
enum Sex//性别
{
  MALE,
  FEMALE,
  SECRET
};
enum Color//颜色
{
  RED,
  GREEN,
  BLUE
};

image.gif

以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。

{ }中的内容是枚举类型的可能取值,也叫枚举常量。

枚举常量也是有值的,它们的值默认从上往下是从0开始依次递增1,就比如Color中的RED为0、GREEN为1、BLUE为2,当然,也可以进行赋值:

enum Color//颜色
{
  RED = 1,
  GREEN = 2,
  BLUE = 4
};

image.gif

3.2枚举的优点

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

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

       #define定义的标识符常量是没有类型的

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

4. 便于调试

       枚举常量在调试时都是可以观察到的

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

3.3枚举的使用

enum Color//颜色
{
  RED = 1,
  GREEN = 2,
  BLUE = 4
};
int main()
{
  enum Color clr1 = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
  enum Color clr2 = 1; //这样的代码在C语言的语法中是支持的,但是在C++的语法中是不支持的
             //1是一个整形,而clr2是一个枚举类型,两个类型不相同
  return 0;
}

image.gif

在之前写的C语言小游戏里面菜单都是可以使用枚举来改进的

4.联合(共用体)

4.1联合类型的定义

联合也是一种特殊的自定义类型

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

比如:

//联合
//联合类型的声明
#include <stdio.h>
union Un
{
  char c;
  int i;
};
int main()
{
  //联合变量的定义
  union Un un;
  //计算联合变量的大小
  printf("%d\n", sizeof(un));  //因为是共用,所以联合体的大小至少是最大成员的大小
  return 0;
}

image.gif

4.2联合的特点

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

#include <stdio.h>
union Un
{
  int i;
  char c;
};
int main()
{
  union Un un;
  //联合的大小是多少呢?
  printf("%d\n", sizeof(un));
  // 下面输出的结果是一样的吗?
  printf("%p\n", &(un.i));
  printf("%p\n", &(un.c));
  return 0;
}

image.gif

image.gif编辑

为什么大小是4呢?并且它们两个的地址还是一样的?

是因为联合就是共用体,共用同一块空间,所以共用体里面的变量都是在同一块内存空间里面存放的。因此在同一时间联合体里面的变量只能使用一个,修改其中的一个变量,其他的也会被改变。

在前面我们写过一个判断大小端的代码,那么使用联合体该怎么实现这个代码呢?

#include <stdio.h>
int check_sys()
{
    int i = 1;
    return (*(char*)&i);   //强制类型转化并且解引用就得到了第一个字节的数据
}
int main()
{
    int ret = check_sys();
    if (ret == 1)
    {
        printf("小端\n");
    }
    else
    {
        printf("大端\n");
    }
    return 0;
}

image.gif

//联合体实现
union Un
{
  char c;
  int i;
};
int main()
{
  union Un n;
  n.i = 1;
  if (n.c == 1)
    printf("小端\n");
  else
    printf("大端\n");
  return 0;
}

image.gif

这个代码就很神奇了,将联合体中的i赋值为1,然后再以c的方式拿出来,如果是小端存放,拿出来的就是01,如果是大端,拿出来的就是00

image.gif编辑

4.3联合体的大小

联合体的内存也是要对齐的:

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

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

#include <stdio.h>
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;
}

image.gif

image.gif编辑

5.通讯录

有关通讯录的实现大家可以查看我的另一篇博客,里面有对通讯录的实现的详细介绍:

C语言小程序:通讯录(静态版)

关于自定义类型的C语言知识就分享到此,如果大家喜欢可以点赞加关注,感谢大家的阅读,谢谢!

目录
相关文章
|
1天前
|
C语言
C语言进阶21收尾(编程练习)(atoi,strncpy,strncat,offsetof模拟实现+找单身狗+宏交换二进制奇偶位)(下)
C语言进阶21收尾(编程练习)(atoi,strncpy,strncat,offsetof模拟实现+找单身狗+宏交换二进制奇偶位)
6 0
|
1天前
|
程序员 编译器 C语言
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)(中)
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)
11 0
|
1天前
|
存储 编译器 C语言
C语言进阶⑱(文件上篇)(动态通讯录写入文件)(文件指针+IO流+八个输入输出函数)fopen+fclose(下)
C语言进阶⑱(文件上篇)(动态通讯录写入文件)(文件指针+IO流+八个输入输出函数)fopen+fclose
7 0
|
1天前
|
存储 数据库 C语言
C语言进阶⑱(文件上篇)(动态通讯录写入文件)(文件指针+IO流+八个输入输出函数)fopen+fclose(上)
C语言进阶⑱(文件上篇)(动态通讯录写入文件)(文件指针+IO流+八个输入输出函数)fopen+fclose
8 0
|
1天前
|
C语言 C++
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(中)
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free
8 0
|
1天前
|
编译器 数据库 C语言
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(上)
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free
9 0
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(上)
|
1天前
|
编译器 C语言
C语言进阶⑯(自定义类型)项目:静态通讯录,增删查改排序打印。
C语言进阶⑯(自定义类型)项目:静态通讯录,增删查改排序打印。
11 1
|
1天前
|
存储 C语言
C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)(下)
C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)
8 0
|
1天前
|
存储 C语言
C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)(中)
C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)
8 0
|
1天前
|
编译器 C语言 C++
C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)(上)
C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)
7 0