编译
常见IDE
的编译器:
VS
的编译器为:MSVC
。Linux
的编译器为:gcc
。
C语言标准库提供的函数的定义,由系统中一个动态库提供:libc.so
。
C++语言标准库提供的函数的定义,由系统中一个动态库提供:libstdc++.so
。
预处理详解
预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间,而不是当前时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
这些预定义符号都是语言内置的。 比如可以通过如下写法证明:
printf("file:%s line:%d\n", __FILE__, __LINE__);
输出结果为:
file : hello.c line : 3
输出了当前文件名与当前行号。
预定义符号在实际应用中方便打印日志,博客下面会讲到。
#define
一般用法:
- 宏定义一般都要求写在一行中
#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
特殊用法:
- 可以使用宏定义来简化代码,宏定义完成后直接在程序中进行文本替换即可。
- 如果宏定义的 stuff 过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符):
\
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
#define CHECK(fp) if(fp == NULL){ \
perror("fopen"); \
return 1; \
}
从第二个宏定义可以发现:其实宏定义也可以传参数。
用
函数
同样可以起到简化代码的功能,但是宏定义相对函数有一个优势:
可以使用宏定义打印日志,这也能体现宏的本质:文本替换
- 定义功能相同的宏定义与函数,进行比较:
- 运行结果比较,我们希望返回代码中
调用函数
或使用宏定义
时的行号:
使用宏定义:返回的正如我们希望的情景
函数:返回的是函数内部代码行的行号,而不是我们希望的调用函数时的行号
【所以在这种情况下使用宏定义就略优于使用函数】
下面是宏的声明方式:
#define name( parament-list ) stuff
其中的 parament-list
是一个由逗号隔开的符号表,它们可能出现在stuff
中。
- 注: 参数列表的左括号必须与
name
紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。如:
#define SQUARE(x) x * x
这个宏接收一个参数 x
. 如果在上述声明之后,将SQUARE(5);
置于程序中,预处理器就会用5 * 5
替换上面的表达式。
但是如果宏接受的参数是a + 1
(此时a = 5),乍一看这段代码将打印36
这个值。但实际会打印11
,正是因为替换文本时,参数x
被替换成a + 1
,所以这条语句实际上变成了:printf ("%d\n",a + 1 * a + 1 );
与预大相径庭。
为了避免这种情况,建议在宏定义上加上一对括号,这样无论接受什么参数处理方式都是相同的了,问题便轻松的解决:
#define SQUARE(x) (x) * (x)
- 小结
- 【宏定义】:
- 可以定义常量
- 可以定义类型
- 可以定义某段代码,使之类似于一个函数
- 【宏定义相对于函数】:
- 打印日志时需要打印文件名与行号等情况
- 宏定义没有类型检查,所以同一个宏可以应用不同类型的参数
- 宏是直接的代码展开,相比于函数调用开销更小
- 宏无法递归调用,函数存在递归调用
#和##
实例演示:
int main(){
char *p = "aa""bb""cc";
printf("%s\n",p);
}
程序运行结果为aabbcc
,所以说明字符串有自动连接的功能。所以就有如下写法:
代码一
#define PRINT(FORMAT, VALUE)\
printf("the value is "FORMAT"\n", VALUE);
int main(){
PRINT("%d", 10);
}
这样的书写风格是没有问题的,与我们预期相同,会输出the value is 10
,这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中。
# 的使用
代码二
将宏定义中加入#
,把一个宏参数变成对应的字符串。写法如下:
printf("the value is "#FORMAT"\n", VALUE);
例如
int i = 10;
#define PRINT(FORMAT, VALUE)\
printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
int main(){
PRINT("%d", i+3);
}
程序运行结果即为the value of i+3 is 13
## 的使用
##
可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。借助这样的文本拼接,可以去生成一些变量名、函数名、类名等。
#define ADD_TO_SUM(num, value)
sum##num += value;
int main(){
ADD_TO_SUM(5, 10);
}
这段程序的作用就是:给sum5
变量增加10
个单位大小。
但是这种使用方法不可以用于循环中希望达到不同下标的数组元素统一定为某值的目的:
for(int i = 1,i < 5,i++){
ADD_TO_SUM(i,10);
}
这个宏就变成了sumi += 10
,而sumi
变量未定义,所以无法传递数组下标达到效果,而应该直接指定某个具体的数字。
宏和函数
上面简单讲了一下宏定义和函数的区别,下表就系统的罗列了二者的不同点:
属性 | #define 定义宏 |
函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
注意事项
-
命名约定:通俗约定宏名全部大写,函数名不要全部大写。
-
undef
:用于移除一个宏定义,如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。 使用方式如下:#undef NAME
-
命令行定义:可以在程序源文件外部进行宏定义,其效果与在源文件内部定义等效,使用方式如下:
gcc -DSIZE=10 test.c
,相当于在程序内定义了语句#define SIZE 10
条件编译
条件编译也属于预处理阶段进行的操作,它的目的就是为了避免多次包含头文件,即重复编译,为什么重复编译会出现问题?
因为头文件中如果一旦出现了定义函数的语句,如void func(){...}
,进行多次编译就会出现函数重复定义的错误,代码执行出现问题。
解决方案:
- 在头文件中包含一个预处理指令
#pragma once
,只被包含一次。 - 在头文件中使用
ifndef
预处理指令判断选择是否需要再次定义。
我们之前在C
语言入门阶段的博客中进行过这个话题的解析。所以就不重复讨论了。这里提及仅供读者知晓条件编译也是预处理阶段进行的操作。
【https://blog.csdn.net/qq_42351880/article/details/85244894 】