本专栏目的
更新C/C++的基础语法,包括C++的一些新特性
前言
- 宏定义是C/C++最伟大的发明之一,甚至有人认为他比指针还伟大,它能够极大简化代码,因此学习宏定义是非常有必要的
- 但是由于他只是简单的替换,故在C++的efficiency书籍中第一条就提到,经量用const、内联函数去代替宏定义,但是实际上,只要运用得好,无论在速度上,还是在代码简化上,都深受大牛程序员的喜欢
- 欢迎点赞 + 收藏 + 关注,本人将会持续更新
文章目录
- 预编译指令
- 预定义宏
- #define宏定义
- 注意
- 带参宏
- 带参宏和函数的区别
- 不用内存分配,速度更快
- 可以传递参数类型
- #undef
- 宏定义中的特殊符号(了解)
- # 参数转字符串
- ## 连接参数
- #@ 参数转字符
- __VA_ARGS__
- #if条件编译
- #include头文件包含
预编译指令
一个C/C++程序,在运行前大概可以经过四个阶段,预处理、编译、连接、汇编(详细会在后面更新计算机组成原理和操作系统时讲解)
预处理程序就是对源文件进行一些文本方面的操作,比如文本替换、文件包含、删除部分代码等,而在C/C++开发中,由于预处理简介,好用,所以在C/C++中会涉及到大量的预处理指令,比如 #include、#define 等,如下表所示(C/C++中凡是一#开头)
指令 | 作用 |
---|---|
#define #undef | 定义宏 取消宏定义 |
#include | 包含头文件 |
#if #else #elif #endif | 条件编译 |
#ifdef #ifndef | 判断是否定义了某个宏 |
#program | 设定编译器的状态或者是指示编译器完成一些特定的动作 |
#error | 当预处理器预处理到*#error*命令时将停止编译并输出用户自定义的错误消息 |
预定义宏
预定义宏是C语言中标准编译器预先定义的宏,在ANSI标准中C程序有5个预定义宏可以直接使用。
宏 | 说明 |
---|---|
_LINE_ | 当前编译的代码的行号 |
_FILE_ | 当前编译文件的源文件名 |
_DATE_ | 当前源程序创建的日期 |
_TIME_ | 当前源程序创建的时间 |
_FUNCTION_ | 当前正在被访问的函数名 |
例如:
#include <stdio.h>
int main()
{
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __FUNCTION__);
return 0;
}
结果:
Sep 12 2024
11:18:04
C:\Users\W Y\Desktop\DesignPatterns\tmp\main.c
10
main
注意:__FUNCTION__调bug特别好用,🤠
#define宏定义
#define 叫做宏定义命令,它也是C语言预处理命令的一种。所谓宏定义,就是用一个标识符来表示一个表达式,如果在后面的代码中出现了该标识符,那么就全部替换成指定的表达式。
宏定义形式:
#define 宏名 stuff //stuff为替换内容
【示例】
#inlcude<stdio.h>
#define NAME "wy"
int main()
{
printf("%s\n",NAME);
return 0;
}
/*
运行结果:wy
*
这个案例很明显,用NAME代替了“wy”
注意
如果宏定义的是一个运算表达式,可能会出现歧义,如下:
【示例】
#include<stdio.h>
#define EXP 2*5+1
int main()
{
int ret = 3 * EXP;
printf("ret:%d\n", ret);
return 0;
}
/*
结果:运行结果:ret:31
*/
这显然是不正确的,应该输出33才对,那为什么是31呢?
请记住宏只是简单地替换,根据这个规则我们来替换一下:
int ret = 3*2*5+1;
原来宏替换不会自动计算值,而是直接复制过来,所以运算的顺序就对了,故解决方法可以添加()
#define EXP (2*5+1)
替换后代码如下:
int ret = 3*(2*5+1);
这个就是我们想要的结果33了。
带参宏
-
C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。
-
对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。
-
带参宏定义的一般形式为:
#define 宏名(形参表) stuff
- 形参列表是一个由逗号分隔的符号列表,它们可能出现在stuff中。参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff 的一部分。
【示例】下面是一个带参宏,它接受一个参数,用来计算数值的平方。
#include<stdio.h>
#define SQUARE(number) (number*number)
int main()
{
printf("%d\n", SQUARE(3));
return 0;
}
那接着看下面的代码,你会新的天地
printf("%d\n",SQUARE(3+2));
我们想要计算(3+2)的平方,即5的平方,但是输出结果却是11,更具宏只是简单地替换原则,可以展开为如下样式
printf("%d\n",(3+2*3+2));
注意记住一句话:宏定义只是简单的替换关系
带参宏和函数的区别
不用内存分配,速度更快
在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。
带参宏非常频繁的用于执行简单的计算,比如在两个表达式中寻找其中较大的一个:
#define MAX(a,b) ((a)>(b)?(a):(b))
为什么不用函数来完成这个任务呢?原因有两个:
- 速度更快
- 不用担心类型,因为他不会为形势参数分配内存,只是简单的代替而已
printf("%d\n",MAX(3,6));
printf("%lf\n",MAX(3.14,5.20));
printf("%c\n",MAX('a','A'));
注意:他只是代替,不会在编译中检查,故不要用它实现太复杂测替换
可以传递参数类型
无法用函数实现对变量类型的传递,比如:下面这个宏,第一个参数是一种类型,它无法作为函数参数进行传递。
#define MALLOC(type,size) malloc(sizeof(type)*size)
...
int*pn = MALLOC(int,10);
char*ps = MALLOC(char,20);
【示例】高难度:用宏定义实现一个foreach循环,用来快捷遍历数组。
#include<stdio.h>
#define foreach(val,arr) \
for (size_t i = 0, ctr = 0; i < sizeof(arr)/sizeof(arr[0]); i++,ctr = 0)\
for (val = arr[i]; ctr < 1; ++ctr)
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
foreach (int a, arr)
{
printf("%d ", a);
}
char* str[] = { "hello","world" };
foreach(char* val, str)
{
puts(val);
}
return 0;
}
宏和函数对比
属性 | #define | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都被插入到程序中。除了非常小的宏之外,程序的长度将大幅度增长. | 函数代码只出现于一个地方;每次使用这个函数时, .都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数调用/返回的额外开销 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除.非它们加上括号,否则邻近操作符的优先级可能会产生不可预料的结果 | 函数参数只在函数调用时求值一次,它的结果值传递给函数。表达式的求值结果更容易预测 |
参数类型 | 宏与类型无关。只要对参数的操作是合法的,它可以使用于任何参数类型 | 函数的参数是与类型有关的。如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务是相同的 |
#undef
这条预处理指令用于移除一一个宏定义。如果一个现存的名字需要被重新定义,那么它的旧定义首先必须用#undef移除。
宏定义中的特殊符号(了解)
# 参数转字符串
使用#可以把宏参数变成一个字符串。
#define toString(value) #value
...
puts(toString(我是顽石老师));
## 连接参数
使用##可以把宏参数连接在一起。
#define VAL(val) val##_maye
...
int VAL(one) = 20;
printf("%d\n", one_maye);
#@ 参数转字符
使用#@可以吧宏参数变成一个字符。
#define toChar(ch) #@ch
...
printf("%c", toChar(1));
VA_ARGS
__VA_ARGS__
是一个可变参数的宏,很少人知道这个宏,这个可变参数的宏是新的C99规范中新增的,目前似乎只有gcc支持(VC6.0的编译器不支持)。
实现思想就是**宏定义中参数列表的最后一个参数为省略号(也就是三个点)。**感兴趣的可以了解一下C语言的含参变量
#if条件编译
一般情况下,源程序中所有的行都参加编译。但有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。有时,希望当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。
先来学习一个别的指令,#error用来输出错误信息并终止编译。
#error 亲,欢迎学习C语言!
常见的条件编译指令
条件编译指令 | 说 明 |
---|---|
#if | 如果条件为真,则执行相应操作 |
#elif | 如果前面条件为假,而该条件为真,则执行相应操作 |
#else | 如果前面条件均为假,则执行相应操作 |
#endif | 结束相应的条件编译指令 |
#ifdef | 如果该宏已定义,则执行相应操作 |
#ifndef | 如果该宏没有定义,则执行相应操作 |
格式(很常用):
#if 条件表达式
程序段1
#else
程序段2
#endif
功能和C语言的条件语句类似,不同的是条件编译中的条件表达式必须为能够在编译期间计算出结果的。(不能为变量)
注意,必须使用 #endif 结束该条件编译指令。
int main()
{
#if 1
printf("#if\n");
#else
printf("#else\n");
#endif
#ifdef SHOW //或者 #ifndef
printf("#ifdef\n");
#else
printf("#else\n");
#endif
#if defined(SHOW) //或者 #if !defined(SHOW)
printf("#if defined\n");
#else
printf("#else\n");
#endif
return 0;
}
#include头文件包含
#inlcude指令我们已经用过很多次了,它会把我们包含的文件全部复制到包含位置。实际上不仅能包含.h文件,.c文件也行,甚至任意文件都行。
标准库文件包含
- 对于编译器已经提供好的库文件,我们可以用过下面这种语法。
#include<filename>
,编译器会去标准库中去查找
本地文件包含
- 对于自定义的库文件,我们可以使用下面这种语法。
#include“filename”
,首先会去本地文件中找,找不到再去标准库中去查找
重复包含头文件
在一个文件中直接或间接多次包含同一文件,可能会导致问题,比如:
- 当文件中有对变量或类型的定义时,多次包含该文件这个变量或类型就会被多次定义。
- 但是这种情况,很容易,被人大学,但是我们看这一种情况:
解决方法:条件编译
以【demo.h】头文件为例
#ifndef _DEMO_H_
#define _DEMO_h_
/*你的代码*/
#endif
在每一次定义.h
文件中,都在前面加上以上上面这种格式的if条件编译