C语言重点突破(四)自定义类型详解

前言

本文意在介绍C语言里的常规自定义类型,它是C语言里最重要的概念之一,是我们从简单使用C语言到综合运用必不可少的知识之一,在C语言中具有重要的地位和作用,掌握自定义类型的使用方法和技巧对于写出高质量的C程序是非常重要的。

目录

本章重点
结构体
结构体类型的声明
结构的自引用
结构体变量的定义和初始化
结构体内存对齐
结构体传参
结构体实现位段(位段的填充&可移植性)
枚举
枚举类型的定义
枚举的优点
枚举的使用
联合
联合类型的定义
联合的特点
联合大小的计算

一.结构体

1.结构体类型的声明

C语言里已经内含了一些基本的数据类型(整型,字符型等),但在实际编程中,我们会碰到一些复杂的数据类型,例如描述一个学生,或者是一辆汽车等一些实际事物光靠基础的类型是不能简单描述的,这时候结构体就派上了用场。

结构体说白了,就是数据的集合,里面的成员可以有多种类型,例如描述一个学生,得有名字,性别,年龄,学号等一些信息,这时就可以用结构体来进行定义。

结构体定义如下

定义一个学生:

复制代码
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢

上面是常规的声明,缺点是每次定义时都要将struct关键字写入,影响编写效率,下面有一种特殊的声明,此时省略了结构体标签(匿名结构体类型,只能使用一次)

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

要注意,这时候定义的结构体变量(x,a【20】,*p)都是全局变量,而在主函数里进行定义则是局部变量(对主函数全局)。

那么问题来了,如果我此时再加上p = &x这一行代码,阁下又该如何应对呢?

可以试着编译一下,运行是没有问题的,但编译器会报警告,尽管两个结构体组成是一样的,但编译器会把它们当作不同的类型进行编译,这种做法不建议。

2.结构体的引用

既然结构体能存放不同的类型,那能不能存放结构体类型呢?

答案是可以的。

复制代码
//代码1
struct Node
{
int data;
struct Node next;
};
//可行否?
如果可以,那sizeof(struct Node)是多少?

编译一下你会发现,甚至都无法编译,这相当于一个结构体里存放一个自己的结构体,同时这个结构体也可能会存放和自己的结构体,大小根本计算不了,那该如何引用呢?

那就引用地址嘛,通过地址就可以找到该结构体并进行引用,而同时存放地址的指针在编译器里的大小是确定的,这样一来也能计算该结构体的大小。

复制代码
//代码2
struct Node
{
int data;
struct Node* next;
};

讲到这里我们再看看下面一段代码

复制代码
//代码3
typedef struct
{
int data;
Node* next;
}Node;
//这样写代码,可行否?

答案显然是不行的,虽然是匿名结构体,但体内已经有了Node类型的指针,后面才生成Node类型,这就导致指针的类型是未定义的,要注意编译的先后顺序。

正确代码

复制代码
//解决方案:
typedef struct Node
{
int data;
struct Node* next;
}Node;

3.结构体变量的定义与初始化

下面是结构体变量的定义与初始化

复制代码
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};//结构体嵌套初始化

4.结构体内存对齐

前面我们留下了一个问题,就是关于结构体的大小应该如何计算,同时这也是一些大厂笔试特别热门的考点:结构体内存对齐

下面介绍一下结构体的对齐规则:

1.第一个成员在与结构体变量偏移量为0的地址处(偏移量就是地址相较于起始地址的差值)

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

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

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

看到这里,你可能还有一点懵,我们来个例子解释一下:

复制代码
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));

我们来分析一下:(在vs环境里默认对齐数是8)

char类型在内存里占1个字节,由于起始地址是0,与8比较起来1较小,所以对齐数是1,而内存起始地址我们设为0,可以看出,结构体的第一个成员永远放在内存的起始地址。

接下来是int类型,在内存中占4个字节,但0的下一位就是1,不是4的整数倍,根据对齐规则,就得对齐到4的位置进行存放4个字节。

最后是char,和第一个一样,直接存放下一个(8)即可。

现在结构体占的大小是0~8,一共九个字节,而结构体成员的最大对齐数是4,还得对齐到4的整数倍上才能算结构体的大小,就是12.

很多人会有疑问了,为什么会存在内存对齐这种说法呢?

结构体内存对齐是为了使结构体的访问更加高效。当结构体中的字段内存对齐后,CPU 可以更快地访问字段所对应的内存地址,因为它们与 CPU 的缓存结构更加匹配。如果结构体的字段没有进行内存对齐,则会导致 CPU 访问内存的效率较低,这会影响程序的性能。

此外,一些计算机体系结构需要结构体内存对齐才能正确工作。例如,一些处理器需要对 4 字节或 8 字节的内存地址进行访问,这意味着结构体中的字段必须按照 4 字节或 8 字节的边界进行对齐才能被正确访问。

因此,结构体内存对齐是为了提高程序的性能和可靠性,确保结构体中的字段可以被正确访问。

简单的来说就是:内存对齐是一种舍弃空间换取时间的方法。

不同的编译器默认的对齐数是不一样的,但可以通过 #pragma 这个预处理指令,改变我们的默认对齐数。

复制代码
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为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));
  printf("%d\n", sizeof(struct S2));
  return 0;
}

结论:

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

5,结构体传参

关于结构体传参,有以下两种方式:

复制代码
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;
}

传结构体和传地址都能实现传参的功能,但那一种会比较好呢?

函数传参传地址和传变量是两种不同的方式。

当使用传地址方式时,函数的参数将是指向变量内存地址的指针。这意味着函数将直接访问变量的内存地址,对变量的操作将在原始地址上进行。这种方式通常用于需要在函数内部修改变量的情况。这种方式可以避免在函数内部对变量进行拷贝,从而提高性能和效率。

当使用传变量方式时,参数是变量本身。这意味着函数将使用变量的副本进行操作,并不会直接改变原始变量。这种方式通常用于不需要修改变量的情况,或者对变量进行操作时不需要改变原始值的情况。

总的来说,传地址方式更加灵活,可以实现更复杂的操作,但需要注意避免因为指针操作不当而导致的错误。传变量方式相对简单,使用起来更为直观,但不能直接在函数内部修改变量的值。

6.结构体实现位段(位段的填充&可移植性)

位段是一种数据结构,它允许程序员在内存中为字段指定特定数量的位数,而不是以字节为单位。这样做有时可以节省内存空间。

在C语言中,可以使用位段来定义一个包含多个字段的结构体。例如,假设我们要定义一个结构体来存储一个16位的数据包,其中包含4个不同的字段,每个字段分别占用4位,可以使用位段来定义这个结构体。

需要注意的是,使用位段可能会导致一些不便之处。例如,不能使用 sizeof 运算符来计算结构体的大小,因为它计算的是按字节对齐的大小。而且不同编译器可能会对位段的实现有所不同,导致可移植性问题。因此,使用位段时需要仔细考虑其适用性和安全性。

位段的声明

复制代码
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};

位段的内存分配

首先要知道的就是位段是比特为单位进行分配空间的

举个例子

如下图,a是char类型,占1个字节(8比特)在主函数里,给a赋值10,但位段要求,只能保留3为比特位,所以要进行截取保留3位,以此类推,当存放的位数已满足一个字节或剩余的比特位空间不够,此时就得再开辟一个字节进行存储。

位段在不同编译器和不同平台上的实现是有所不同的,这可能会导致跨平台问题。

最常见的问题之一是,如何对位段进行按位运算。在一些平台上,位段是定义为无符号整数,可以直接进行按位运算;但在另一些平台上,则需要将位段转换为整数类型,才能进行按位运算。

此外,位段的顺序和字节对齐方式也可能会发生变化。例如,在某些平台上,位段的顺序是从左向右,而在其他平台上,顺序是从右向左。同时,一些平台可能会对位段进行字节对齐,而其他平台则不会。

为了避免位段的跨平台问题,可以采取以下措施:

  1. 避免在位段中使用多个类型。

  2. 明确指定位段的顺序和字节对齐方式。

  3. 避免使用位段进行按位运算,或者使用平台无关的按位运算规则。

  4. 在不同平台上进行测试和调试,确保代码的可移植性和正确性。

总之,位段虽然能够节省内存空间,但也需要考虑其在不同平台上的实现和兼容性,以保证代码的正确性和可移植性。

二.枚举

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

1.枚举类型的定义

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

上面的Day、Sex、Color、都是枚举类型,括号里的叫做枚举常量,这些常量都是有值的,默认从0开始,一次递增1,也可以在定义的时候进行赋值。

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

2.枚举的优点

  1. 程序可读性强。枚举类型可以使用具有意义的符号名称来表示常量,使得程序的可读性更高,增加代码的可维护性。

  2. 减少代码中的魔数。枚举类型可以减少代码中出现的"魔数"(没有明确含义的数字),从而提高代码的可读性和可维护性。

  3. 编译器提供类型检查。枚举类型被视为一种类型,因此编译器可以进行类型检查,从而避免一些常见的错误,例如将一个枚举类型的值赋给另一个类型的变量。

  4. 枚举类型可以实现类型安全的类型别名。实际上,枚举类型可以用来实现一些类型安全的类型别名,例如使用枚举类型来定义一个有限的整数集合。

3.枚举的使用

复制代码
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5; //ok??

三.联合体(共用体)

1.联合类型的定义

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

复制代码
//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));

2.联合的特点

联合体的成员是共用一块内存空间的,所以一个联合变量的大小,至少是最大成员的大小。

3.联合体的大小计算

要注意的问题:

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

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

相关推荐
u0103055271 天前
AI驱动的安卓UI自动生成功能实现
人工智能·1024程序员节
u0103055272 天前
长株潭智能体赋能腾讯元器链接共享
1024程序员节
云贝贝贝3 天前
OceanBase认证体系介绍与备考指南
运维·服务器·oceanbase·1024程序员节
u0103055273 天前
Java图像处理实战指南
人工智能·1024程序员节
u0103055274 天前
Java AWT鼠标事件全解析
人工智能·1024程序员节
u0103055276 天前
Java实用类应用精讲
人工智能·1024程序员节
u0103055276 天前
Java I/O核心操作实战
人工智能·1024程序员节
u0103055277 天前
ArrayList操作详解与实战应用
人工智能·1024程序员节
u0103055278 天前
Java模块化编程实战指南
人工智能·笔记·1024程序员节
u0103055279 天前
使用BufferedReader读取控制台输入
人工智能·1024程序员节