一 :C语言预处理
1:由源码到可执行程序的过程
(1)源码.c->(编译)->目标文件.o->elf可执行程序
(2)源码.c->(编译)->目标文件.o->(链接)->elf可执行程序
(3)源码.c->(编译)->汇编文件.s->(汇编)-> 目标文件.o->(链接)->elf可执行程序
(4)源码.c->(预处理)->预处理过的.c源文件->(编译)->汇编文件.s->(汇编)-> 目标文件.o->(链接)->elf可执行程序
预处理用预处理器,编译用编译器,汇编用汇编器,链接用链接器,这几个工具再加上其他会额外用到的可用工具,合起来叫工具编译链。gcc就是一个编译工具链。
2:预处理的意义
(1)编译器本身的只要目的是编译源代码,将c的源代码转化成.s的汇编代码。编译器聚焦核心功能后,就把一些非核心的功能剥离到预处理器去了。
(2)预处理器帮编译器做一些编译前的杂事
3:编程中的常见预处理
(1)#include (#include <> 和 #include ""的区别)
1、引用的头文件不同
#include< >引用的是编译器的类库路径里面的头文件。
#include“ ”引用的是你程序目录的相对路径中的头文件。
2、用法不同
#include< >用来包含标准头文件(例如stdio.h或stdlib.h).
#include“ ”用来包含非标准头文件。
3、调用文件的顺序不同
#include< >编译程序会先到标准函数库中调用文件。
#include“ ”编译程序会先从当前目录中调用文件。
4、预处理程序的指示不同
#include< >指示预处理程序到预定义的缺省路径下寻找文件。
#include“ ”指示预处理程序先到当前目录下寻找文件,再到预定义的缺省路径下寻找文件。
5、更深层次来说:<>的话C语言编译器只会到系统指定目录(编译器中配置的或者操作系统配置的寻找目标,譬如在ubuntu中是/usr/include目录)去寻找这个头文件(隐含意思就是不会到当前目录下找),如果找不到就会提示文件不存在;而" "的话编译器默认会在当前目录下寻找相应的头文件,如果没有找到的然后再到系统指定目录去寻找,如果还没找到则提示文件不存在
6、总结+注意:系统虽然允许双引号(" ")来包含系统指定目录,但是一般的使用原则是,如果是系统指定自带的用< > ,如果是自己写的放在当前目录下的用" ",如果是自己写的但是集中放在了一起存放头文件的目录下将来在编译器中用-I参数来寻找,这种情况下用<>。
(2)注释
1:注释是给人看的
2:编译器是不看注释的,编译器不看注释。实际上在预处理阶段,预处理器会拿掉程序中所有的注释语句,到了编译器编译阶段程序中其实已经没有注释了
(3)条件编译 #if #elif #endif #ifdef
1:有时候我们希望程序有多种配置,我们在源代码编写时写好了各种配置的代码,然后给一个配置开关,在源代码级别去修改配置 开关来让程序编译出不同的效果。
#include <stdio.h>
#define num
int main(void)
{
int a = 0;
#ifdef num //如果有定义num符号,则执行a = 111;语句
a = 111;
#else //如果未定义num符号,则执行a = 222;语句
a = 222;
#endif
printf("%d\n",a); //输出a的值
return 0;
}
运行结果:
2: 条件编译中用的两张判定方法分别是#ifdef和#if
区别:xxx判定条件成立与否时主要是看xxx这个符号 在本语句之前有没有被定义,只要定义了(我们可以直接#define xxx 或者#define xxx 12或者 #define xxx yyy)这个符号就是成立的
#if(条件表达式),它的判定标准是()中的表达式是true还是flase,跟C语言中的if语句类似
#include <stdio.h>
#define NUM (0)
int main(void)
{
int a = 0;
#if (NUM ==0)
a = 111;
#else
a = 222;
#endif
printf("a = %d\r\n",a);
return 0;
}
运行结果:
(4)宏定义
#include <stdio.h>
#define pchar char *
typedef char * PCHAR;
int main(void)
{
pchar p1,p2; //定义了一个字符类型的指针变量p1和字符类型的变量 p2
PCHAR P1,P2; //定义了一个字符类型的指针变量P1和字符类型的指针变量P2
char a = 1; //定义了一个字符类型的变量a
printf("a = %d\r\n",); //输出a的值
p1 = &a; //将指针p1指向a
*p1 = 3; //通过解引用改变变量a的值
printf("*p1 = %d\r\n a = %d\r\n",*p1,a); //输出指针p1执行的地址存放的值
//输出变量a的值
//p2 = &a; //错误的例子,p2是字符类型的变量,而&a是a的地址(char *)
//*p2 = 5; //这里是错的,因为*的操作必须是指针而p2是char
//printf("*p2 = %d\r\n a = %dr\n",*p2,a); //上面两个表达式都是错的,这里更不用说
P1 = &a; //将指针P1指向a
*P1 = 4; //通过解引用改变变量a的值
printf("*P1 = %d\r\n a = %drn",*P1,a);
P2 = &a; //将指针P2指向a
*P2 = 6; 通过解引用改变变量a的值
printf("*P2 = %d\r\n a = %d\r\n",*P2,a);
return 0;
}
运行结果:
4:gcc中之预处理不编译的方法
(1)gcc编译时可用给一些参数来做一些设置,譬如gcc xx.c -o xx可以指定可执行程序的名称
譬如:gcc xx.c -c -o xx.o可以指定只编译不链接,也可以生成.o的目标文件
(2)gcc -E xx.c -o xx.i 可以实现之预处理不编译。一般情况下不需要只预处理不编译,但有时候这种技巧可以帮助研究预处理的过程,帮助debug程序
总结:宏定义被预处理时的现象有:第一,宏定义语句本身不见了(可见编译前根本就不认识#define,编译器不知道还有个宏定义);第二,typedef语句还在,说明typedef 和宏定义有本质的区别(说明typedef不是由预处理器处理的,而是由编译器处理的);
二 :宏定义
1 :宏定义的规则和使用解析
(1)宏定义的解析规则就是:在于处理阶段由预处理器进行替换,这个替换是原封不动的替换
#define N 10 //预处理后这句消失,所以说编译器不认识宏
int main(void)
{
int a[N] = {1,2,3}; //预处理后[] 里面的N变成了10
int b[10] = {2,3,4};
return 0;
}
(2)宏定义替换会递归进行,直到替换出来的值本身不在是一个宏为止
#define N 10 //预处理后这句消失,所以说编译器不认识宏
#define M N //这句也消失
int main(void)
{
int a[M] = {1,2,3}; //预处理后[] 里面的M变成了10,进行了递归替换
int b[10] = {2,3,4};
return 0;
}
(3)一个正确的宏定义式子本身分三个部分:第一部分是#define(后面有个空格),第二部分是宏名,剩下的所有为第三部分(宏体)
(4)宏可以带参数,称为带参宏。带参宏的使用和带参函数非常像,但是使用上有一些差异。在定义带参宏时,每一个参数在宏体中引用都必须加括号,最后整体再加括号,括号缺一不可
#include <stdio.h>
#define x(x,y) x+y //预处理后这句消失
int main(void)
{
int a = 2,b = 3,c = 0;
c = x(a,b); //这句表达式变为 c = a + b
printf("c = %d\r\n",c); //输出c = 5
}
运行 结果:
#include <stdio.h>
#define x(x,y) x+y
int main(void)
{
int a = 2,b = 3,c = 0;
c =3 * x(a,b); //预处理后这里有一个细微的差别,预处理后表达式为 3 * a + b
//*优先级比+高所以先进行 3*a 的计算
printf("c = %d\r\n",c); //输出结果为 9
}
运行结果
#include <stdio.h>
#define x(x,y) (x+y)
int main(void)
{
int a = 2,b = 3,c = 0;
c =3 * x(a,b); //这里表达式展开为: c = 3 * (a + b)
//()优先级比*高所以先进行a + b
printf("c = %d\r\n",c); //输出结果为15
}
运行 结果:
2 :宏定义示例(面试笔试会出现的题目)
(1)MAX,求两个数中较大的一个
注意:要想到用三目运算符来完成和注意括号的使用
#define MAX(x,y) (((x)>(y)) ? (x) : (y))
#include <stdio.h>
#define MAX(x,y) (((x)>(y)) ? (x) : (y))
int main(void)
{
int a = 2,b = 3,c = 0;
c = MAX(a,b); //表达式展开后为: c = ((a)> (b) ? (a) : (b))
printf("c = %d\r\n",c);
}
运行结果:
(2)SEC_PER_YEAR,用宏定义表示一年有多少秒
注意:
#include <stdio.h>
#define SEC_PER_YEAR (355 * 24 * 60 * 60UL)
int main(void)
{
return 0;
}
第一点:当一个数字直接出现在程序中时,它的类型默认是int
第二点:一年有多少秒,这个数字 刚好超过了int类型的存储范围
3 : 带参宏和带参函数的区别 (宏定义的缺陷)
(1)宏定义是在预处理期间进行处理的,函数是编译期间进行处理的。这个区别带来的实质差异是:宏定义最终在调用宏的地方把宏体原地展开,而函数是在调用函数处跳转到函数中去执行,执行完之后再跳转回来
#include <stdio.h>
#define MAX(x,y) (((x)>(y)) ? (x) : (y))
int max(int a,int b)
{
if(a>b)
return a;
else
return b;
}
int main(void)
{
int a = 3,b = 5;
int macro_ = 0;
int func_ = 0;
macro_ = MAX(a,b); //原地展开
macro_ = max(a,b); //无法展开,只能调用
printf("is macro_ = %d\n",macro_);
printf("is func_ = %d\n",macro_);
return 0;
}
注意:宏定义和函数的最大差别:宏定义是原地展开,因此没有调用开销;而函数是跳转执行再返回,因此函数有比较大的调用开销。所以宏定义和函数相比,优势就是没有调用开销,没有传参开销,所以当函数体很短(尤其只有一句话时)可以用宏定义来代替,这样效率高
(2)带参宏和带参函数的一个重要差别就是:宏定义不会检查参数类型,返回值也不会附带类型;而函数有明确的参数类型和返回值类型。当我们调用函数时,编译器会帮我们做参数的静态类型检查,如果编译器发现我们实际传参和参数声明不同时会报警告或者错误
注:用函数的时候程序员不太用操心类型不匹配因为编译器会检查,如果不匹配编译器会报警告;而用宏时程序员必须注意实际传参和宏所希望的参数类型一致,否则可能编译不报错但是运行有误。
总结:如果代码多用函数适合而且不影响效率;但是对于只有一两句的函数开销就太大了,适合使用带参宏。但是用带参宏又有缺点:编译器不检查参数类型,需要程序员自己注意
4:内联函数和inline关键字
(1)内联函数通过在函数定义前加inline关键字实现
(2)内联函数本质上是函数,所以有函数的优点(内联函数是编译器负责处理的编译器可以帮我们做参数的静态类型检查),但是它同时又有带参宏的优点(不用调用开销,而是原地展开) 所以似乎可以这样认为:内联函数就是带了静态类型检查的宏
(3)当我们的函数体很短(只有一句话的时候),我们希望利用编译器的参数类型检查来排错,我还希望没有调用开销时,最适合使用内联函数
5:宏定义来实现条件编译 (#define #undef #ifdef)
(1)程序中有DEBUG版本和RELEASE版本,区别就是编译时有无定义DEBUG宏
#include <stdio.h>
#define DEBUG //定义一个宏
//#undef DEBUG //若声明了这句,则注销DEBUG宏,则下面执行的是没有宏体的宏
#ifdef DEBUG
#define debug(x) printf(x) //如果定义了DEBUG,则执行这句
#else
#define debug(x) //没定义则执行这句没有宏体的宏
#endif
int main(void)
{
debug("this debug!\n"); //当定义了DEBUG时输出信息,没定义则没有
return 0;
}
运行结果: