联合体
联合体在c语言中用的很少。
联合体的外在形式跟结构体非常类似,但它们有一个本质的区别:结构体中的各个成员是各自独立的,而联合体中的各个成员却共用同一块内存,因此联合体也称为共用体。
- 整个联合体变量的尺寸,取决于联合体中尺寸最大的成员。
- 给联合体的某个成员赋值,会覆盖其他的成员,使它们失效。
- 联合体各成员之间形成一种“互斥”的逻辑,在某个时刻只有一个成员有效。
联合体的定义和声明联合体变量
// 定义了一种称为 union attr 的联合体类型
union attr
{
int x;
char y;
double z;
};int main()
{
// 定义联合体变量
union attr n;
}
联合体的初始化
// 普通初始化:第一个成员有效(即只有100是有效的,其余成员会被覆盖)
union attr at = {100, 'k', 3.14};// 指定成员初始化:最后一个成员有效(即只有3.14是有效的,其余成员会被覆盖)
union attr at = {
.x = 100,
.y = 'k',
.z = 3.14,
};
联合体成员的引用和声明联合体类型指针
at.x = 100;
at.y = 'k';
at.z = 3.14; // 只有最后一个赋值的成员有效,前面两个赋值都被覆盖了,打印出来是无效数据。
printf("%d\n", at.x);
printf("%c\n", at.y);
printf("%lf\n", at.z);
union attr *p = &at;
p->x = 100;
p->y = 'k';
p->z = 3.14; // 只有最后一个赋值的成员有效printf("%d\n", p->x);
printf("%c\n", p->y);
printf("%lf\n", p->z);
联合体的使用
联合体一般很少单独使用,而经常以结构体的成员形式存在,用来表达某种互斥的属性。联合体一般都是嵌套在结构体里面来作为成员变量,并且经常采用的直接嵌套定义的方式,联合体以匿名标签形式存在,不会显示定义我们的联合体标签(因为联合体一般不单独使用)。
比如:
struct MyStruct {
int type; // 用于标识联合体中存储的数据类型
union {
int i;
float f;
char str[20];
} data;
};
枚举
认识枚举
C语言枚举(enum)是一种用户自定义的数据类型,用于定义一组命名的整数常量。枚举通过关键字enum声明,列举一系列标识符(枚举成员)并赋予整数值。枚举提供了一种更易读、更安全的方式来管理一组相关常量。
默认情况下,枚举成员从0开始自动增量赋值,但也可以显式指定值。枚举变量只能取枚举成员中已定义的值。详细用法可以来看下面这篇文章:
C语言枚举类型enum(全面详细直观)_c enum-CSDN博客
枚举适用情况:
c语言中用到枚举还是非常少的,用到的话也是为了代替掉一些宏的用法,c语言中宏是很常用的,因为宏可以用一些浅显的名称来指代一些值,这样就增加了代码的可读性。
枚举和宏的关系
枚举可以代替一些宏,但是枚举并不是可以随便替代宏的,它仅在某些方面来代替宏会更好一点,枚举适合用在哪些有限情况呢?
比如一周有7天,就可以用枚举来指代1-7这几个数字,从而利用枚举让我们的代码更具有可读性,又或者是一年有12个月,也可以用枚举来指代1-12这几个数字。对于这种有几个连续的集中的有很强相关意义的整数值用宏定义就比较稍稍麻烦一点,此时就可以用枚举来集中定义。
对于零散,不相关的信息,那还是用宏来定义更加合适。枚举类型(enum)只能用于整数类型的常量。它们不能用于表示字符串、路径或其他非整数类型的数据。如果你需要定义字符串常量,比如文件路径,通常使用宏定义。
枚举中的赋值规则
- 默认赋值:如果不显式赋值,枚举成员的值从 0 开始,并按顺序递增。
- 任意顺序显式赋值:可以在枚举中以任意顺序显式赋值成员,显式赋值的成员将具有指定的值,未显式赋值的成员将根据其前一个成员的值加 1 进行递增。
举个例子
typedef enum {
SUNDAY, // 0
MONDAY, // 1
TUESDAY = 5, // 5
WEDNESDAY, // 6
THURSDAY, // 7
FRIDAY = 10, // 10
SATURDAY // 11
} Weekday;
typedef enum {
SUNDAY = 1, // 1
MONDAY, // 2
TUESDAY, // 3
WEDNESDAY = 10,// 10
THURSDAY, // 11
FRIDAY = 20, // 20
SATURDAY // 21
} Weekday;
特殊例子
typedef enum {
saturday, // 默认值为0
sunday = 0, // 显式地将sunday的值设置为0
monday, // 值为1(前一个成员值加1)
tuesday // 值为2(前一个成员值加1)
} Days;
在枚举中,多个枚举成员可以具有相同的值。例如,在上面的枚举定义中,saturday
和 sunday
都有值0。C语言允许这种情况,因为枚举成员只是命名的整数常量。如果多个枚举成员具有相同的值,编译器不会报错,但需要注意这种情况容易混淆,不要这样使用,尽量避免。
示例
枚举实际上是建立了一组符号名称(标识符)与整数值之间的对应关系,通过使用有意义的名称代替直接使用整数值,可以提高代码的可读性和减少出错的可能性。
// 定义枚举类型 Weekday
typedef enum {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
} Weekday;
// 定义函数,根据枚举值输出对应的字符串
const char* getDayName(Weekday day) {
switch (day) {
case SUNDAY: return "Sunday";
case MONDAY: return "Monday";
case TUESDAY: return "Tuesday";
case WEDNESDAY: return "Wednesday";
case THURSDAY: return "Thursday";
case FRIDAY: return "Friday";
case SATURDAY: return "Saturday";
default: return "Invalid day";
}
}
这样来写与我们直接用数字来作为case的匹配条件相比 更加清晰。
宏
检查宏错误的时间
编辑器阶段:这是你在代码编辑器中编写代码时的阶段。现代的代码编辑器(如Visual Studio Code、CLion等)通常会提供即时的语法检查和代码提示功能。编辑器可以检测到明显的语法错误,并在你编写代码时即时提示你。这种检查通常是基于编译器前端的一些功能,并不会涉及到预处理器指令的实际展开和替换,更不会对宏进行计算。
宏的使用需要小心,因为宏的检查并不发生在编辑器阶段。而是发生在编译过程,更具体来说是发生在编译过程中的编译阶段,错误不容易被发现。
c语言的编译过程分为多个阶段:预处理阶段,编译阶段,优化阶段……
其中预处理阶段是C语言编译过程中的第一步,是编译器真正编译代码之前的一步,这一步由预处理器负责,处理所有以#
开头的指令,例如宏定义、文件包含、条件编译等。预处理器会对这些指令进行处理并进行相应的文本替换。此阶段并不检查语法,而是仅仅进行简单的文本替换。
下一阶段是编译阶段,此阶段是对预处理后的代码进行语法分析,检查代码是否符合语言的语法规则。(宏错误的检查就发生在这一阶段)。
所以总之我们可以发现宏的检查报错是发生在我们编译代码的过程,即我们点击编译按钮之后 。编辑器中的即时检查可以帮助你捕捉一些明显的错误,但宏的实际错误(例如语法错误或逻辑错误)要等到编译阶段才能被发现。编译器阶段发现不了宏的错误,只有在编译或者运行的时候才能发现错误,不好找错,这也是为什么在使用宏时需要格外小心,确保宏定义的正确性。
宏的介绍
它指的是一段预定义的代码或指令序列,可以被编译器或解释器在编译或执行时自动替换成相应的代码。宏的使用可以提高代码的复用性、可读性,并减少编码的工作量。
宏主要分为两种类型:
- 对象宏(Object-like Macros):用于定义常量或简单的文本替换。
- 函数宏(Function-like Macros):类似于函数,但在预处理阶段进行文本替换,而不是在运行时调用。
宏的常见作用
- 使得程序更具可读性:字串单词一般比纯数字更容易让人理解其含义。
- 使得程序修改更易行:修改宏定义,即修改了所有该宏替换的表达式。
- 提高程序的运行效率(对于函数宏,但函数宏用的很少):程序的执行不再需要函数切换开销,而是就地展开。
对象宏
语法: #define 常量名 值
#define MAX 1000
示例:
#define ARR_SIZE 1000
当我们在代码中要频繁地用到一个边界大小或者边界条件的时候,我们可以将它定义为一个宏,比如说数组元素的数目,当后面发现数组大小不够用的话可以直接来修改这个宏,从而改变代码中这个宏对应的所有值,十分方便。
再者比如说文件或者图片路径,当图片路径改变了之后,凡是用到这个图片路径的地方都要更改,而且用宏定义图片更加直观清晰,比如APPLE_PHOTO和实际的这个图片对应的路径相比,显然更加直观,增强了代码可读性。
函数宏
语法:#define 宏名(参数) 代码
#define SQUARE(x) ((x) * (x))
示例:
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
int main() {
int a = 5;
printf("Square of %d is %d\n", a, SQUARE(a));
return 0;
}
运算符优先级问题
这里我们可能会有一些疑惑,就是函数宏为什么代码中的参数x都要使用()括起来呢?其实主要是为了避免一些问题,比如说传递的x是1+1,由于宏是先直接替换的,所以实际替换后的代码为1+1*1+1,显然这个最终运算结果是3,而不是我们预想的4 ,为了避免这种情况,所以定义函数宏的时候在函数体中,我们通常都会用()来将传入的参数括起来。避免在宏展开过程中发生运算优先级问题,确保结果符合预期。
函数宏的优点和缺点
优点:函数宏通过文本替换直接插入代码,避免了函数来回调用的开销。对于小型、简单的操作,这可能带来一些性能上的优势。
缺点:
- 缺乏类型安全:函数宏不会进行类型检查,这可能导致难以发现的错误。例如,传递给宏的参数类型不正确时,编译器不会报错。
- 调试困难:因为函数宏在预处理阶段被替换成实际代码,调试时可能难以跟踪和定位错误。
- 副作用:如果传递给宏的参数包含副作用(例如自增操作),宏展开可能导致意外的行为。
- 可读性差:宏展开后可能导致代码难以理解和维护。
- 空间浪费:宏将在所有出现它的地方展开,也就是说所有用函数宏的地方都会将对应的代码复制到这里一份,这一方面浪费了内存空间。在普通函数调用的时候是跳转到函数部分来执行代码,并不会将代码复制过来。
函数宏在C语言中有一定的使用场景,但由于其缺乏类型安全、调试困难以及可能引发副作用等问题,在现代C编程中使用函数宏的频率已经大大减少。内联函数提供了一种更安全、更可维护的替代方案,因此在现代编程实践中更为常用。
宏编写时的注意:
1.宏的名字我们习惯上用全大写字母,这是一个公认的约定,这也能快速让我们知道这个名字代表的是一个宏。
2.宏的定义语句结尾不要加 ; ,因为宏是直接替换,后面加的;也会被添加到宏代表的地方
#define MAX 1000;
#define MAX 1000if(condition)
max = MAX;
else
max = 0;如果使用带;的宏定义,那么将会变成if(condition) max = MAX;;
这个其实就是两条语句了,很容易造成意想不到的结果,所以宏的定义语句结尾不要加 ;
3.对象宏在我们实际编程中应用广泛,但是函数宏已经用的比较少了,容易出错且调试困难,仅可能用在一些频繁调用的函数调用的情景,此时用函数宏来代替节省函数调用的时间,提升程序的执行效率。