在编写程序之前,有没有想到程序是如何运行起来的呢
1.首先程序员编写代码
2.预处理
3.编译器将代码转换为计算机能够执行的机器语言
4.进行不同文件的链接
一.预处理
#include
一个简单的hello world程序,你能想到其在预处理的时候竟然会展开上万行代码吗?
但事实就是如此,为了统一的标准和代码的可重用行,C语言中提供了标准输入输出等头文件,在使用时候只需要这样
#include <stdio.h>
#是预处理符号,告诉编译器说:我需要对这一行进行预处理
所谓的预处理可以理解为文本的替换,于是编译器就将标准输入输出头文件中的内容展开到当前.c
文件中
一个最简单的例子,假设我们有这样的一段代码
int main()
{
printf("hello world\n");
并且有这样的一个头文件
//something.h
return 0;
}
预处理实际就是将文本展开
于是可以这样写
int main()
{
printf("hello world\n");
#include <something.h>
就是这样的展开
int main()
{
printf("hello world\n");
return 0;
}
顺便说一下关于编译和链接的东西
编译的基本单元是.c文件,头文件不被编译,只是被预处理展开,链接器负责将编译后的程序链接起来
#define
这告诉编译器定以一个明示常量,用于宏替换,或者仅仅定义一个宏与其他的预处理指令组合使用,用于条件编译
用法
#define INT int
#define a 3
#define a_string "hello,world"
#define HRLLO_H
像INT a a_string这样的文本叫做宏, 后面跟着的叫做替换体,编译器会把出现宏的地方全部替换为替换体
在出现INT的地方编译iq会替换为int,以下同理,仅仅定义一个常量串除外(他只是说明的程序员要定义一个那样的宏)
宏函数
C可以用宏定义一个函数
语法规则如下
#define sum(a,b) a+b
编译器会将出现sum(a,b)的地方换为a+b
经过上面的例子现在假定你已将很清晰的了解这样的预处理仅仅是文本替换而已
那这就引发了一些问题
#define SIX 3+3
...
SIX * SIX
SIX * SIX 会被编译器这样替换 3 + 3 * 3 + 3
实际的结果并不是36,而是15,注意下类似宏函数的问题
宏函数的优点,内联的展开函数,不用执行机器码入口的的跳转和记录,减小了开销并且提升了运行速度。
C允许在字符串中包含宏参数
printf("this string is X");
此代码的执行结果就是其所表示的字符串
#define Print(X) printf("this string is" #X "\n");
此代码的执行结果是将X进行宏替换的结果,#X 告诉编译器,要将宏参数直接替换而不是解析,
因为两对双引号 写在一起表示链接字符串 #X其实隐式的生成了一个字符串和其拼接
简单的来说,#X是对宏参数的文本替换并自动加上“”
头文件保护机制
如果在一个.c文件中重复定义头文件会导致变量或者函数的重复定义,可以使用头文件保护来避免这个问题
#ifndef AHEAD_FILE_H
#define AHEAD_FILE_H
do something
#endif //AHEAD_FILE_H
#ifndef 和 #endif是预处理指令,如果#ifndef判定为真,执行下面的语句直到#endif,结合预处理指令#include,就能避免头文件重复包含,因为再次包含重复的头文件时,#ifndef会判定为假就不会再执行下面的语句了
预处理粘合剂:##
#include <stdio.h>
#define INT(n) x##n
int main()
{
int INT(1) = 10; // equal to int x1 = 10
int INT(2) = 20; // equal to int x2 = 20
printf("%d %d",x1, x2);
return 0;
}
将x与n进行粘合操作后进行替换
变参宏:... 和__VA_ARGS__
一些函数接受可变参数,像printf函数C语言提供的宏定义可以做到这一点
通过将宏参数写成 ...的形式,再将宏替换中接受参数的部分变为__VA_ARGS__即可
#include <stdio.h>
#define PR(...) printf(__VA_ARGS__)
int main()
{
PR("Hello world\n");
int a, b;
a = 10;
b = 20;
PR("a is %d, b is %d\n", a, b);
return 0;
}
第一次调用PR宏函数,将"Hello World\n"替换为__VA_ARGS__
第二次使用PR函数有三个参数,分别是字符串"a is %d, b is %d\n", a, b替换了__VA_ARGS__
简单的demo
#include <stdio.h>
#define PR(X, ...) printf("Message " #X " is : " __VA_ARGS__)
int main() {
PR(1,"hello world\n");
PR(2,"a number is %d, and another number is %.2f", 2, 3.14);
return 0;
}
注意,...后面不能再跟着参数,也就是说,不能这样做
#define PR(X, ..., Y) printf(/*something*/)
宏的其他一些操作
一般来说,由于宏的一些奇怪的特性,宏最好只定义一行,迫不得已u的情况下,可以定义多行,但是要用 \来连接两行之间的内容
#include <stdio.h>
#define func(x)\
((x)*(x) + 3 *(x) + 2.3)
#define STR "this is a string\
haha"
int main() {
printf("%.2f\n", func(3.14));
printf(STR"\n");
return 0;
}
运行结果
21.58
this is a string haha
注意是有空格的
宏函数的选择
宏的优点上面已经说过了,额外的再补充一些
宏函数是对字符串的处理,而不是对变量类型的处理,因此,在泛型方面比普通函数有优势
当然,宏函数还有他的缺点
假设一个宏函数调用了50次,那么他会生成50份文本替换,会过多的占用空间,而且滥用宏函数会使程序引发一些奇怪的bug。
因此,一些使用宏的建议
尽可能用精炼的表示方式来创建宏函数
代码快大的函数不要写成宏函数,短小的函数可以写成宏函数
宏名中不能有空格
计算操作应用括号准确表示
用大写字母表示宏,提高程序的可读性并且其也增加对程序员的警告作用
可以考虑将宏函数包装成一个头文件,方便代码复用和检查
条件编译
正如其名所言,根据一些条件选择是否编译某些代码
关键词
#define 经介绍的够详细了,在条件编译中主要的作用就是定义宏名
#ifndef 如果没有定义了某个宏则编译其下一直到endif的代码,可和#else成对使用
# if defined() 作用同#ifdef
#ifdef 如果定义了某个宏则编译其下一直到endif的代码 ,可和#else成对使用
#undef 取消定义的宏名
#if 判定整形常量或者常量表达式,不用括号,如果为真,则编译下面的代码直到 endif
#else 与#if配对使用
#elif 与#if配对使用
#endif 与其他预处理指令成对使用
#line #error #pragma
一些演示
#define DEBUG 1
//#ifdef DEBUG
# if defined(DEBUG)
#include "some.h"
#endif
#if DEBUG == 1
//some debug code
#endif
#undef DEBUG
#define EXP 3
#if EXP == 1
//do something
#elif EXP == 2
//do something
#else
//do something
#endif
C语言中已经为我们定义了一些宏,并且这些宏是不能被取消定义的
见C/c++几个预定义的宏:__DATE__,__TIME__,__FILE__,__LINE___C语言中文网http://c.biancheng.net/cpp/html/2552.html
#pragma的作用就是把对编译器的指令放入源码中,进行对编译器的一些控制,这里就不继续深入了
#line 重置当前行号或者文件名称
#include <stdio.h>
int main()
{
printf("hello world\n");
printf("Date : %s\n", __DATE__);
printf("Time : %s\n", __TIME__);
printf("File : %s\n", __FILE__);
#line 1000
printf("Line : %d\n", __LINE__);
#line 10 "hello.c"
printf("Line : %d\n", __LINE__);
printf("File : %s\n", __FILE__);
return 0;
}
注意不能单独这样使用
#line "hello.c"
#errorr让编译器发出一条错误消息,该消息包含指令中的文本,如果可以的话,编译器会中断编译
注意:消息是编译器发出的,不是标准错误发送到屏幕上的,会给人一种写的程序本身有语法错误或者逻辑错误的感觉,实际上不是这样的。
#if __STDC_VERSION__ != 201112L
#error not C11
#endif
C11下不会有错误
放在C90下会有这样的错误
总结
合理的使用宏操作会带来一些好处
合理的使用宏函数会使程序的性能更强,可读性更好
但是不要滥用和宏,会导致莫名其妙的错误
条件编译被广泛用于各种库或者是大型工程,在实际开发中是很有用的