目录
- 编译预处理
- 编译预处理指令
- 预处理时究竟发生了什么
- 宏
- 定义宏
- 使用宏
- 没有值的宏
- 预定义的宏
- 像函数的宏
- 大程序结构
- 多个C文件
- 编译单元
- IDE
- 头文件
- 使用
- 预处理指令
- 使用的细节
- 误区
- 头文件的使用规范
- 全局变量
- 声明
- 声明和定义
- 头文件那些事儿
- 规则
- 问题
- 标准头文件结构
编译预处理
编译预处理指令
所有预处理指令都以#
开头
“#”对应的英文是”pound”
电话键盘上的”#”则是”hash”
注意: 编译预处理部分并不是C语言的成分(例如include不是C语言的关键字), 但是C语言程序离不开他们
预处理时究竟发生了什么
编译C语言代码时, 编译器会产生一系列临时文件
按照顺序来看, .c
->.i
->.s
->.o
->.exe
+ 第一步, 首先编译预处理, 所有预处理完成后产生.i
中间结果文件(注释也会被忽略掉)
+ 第二步, 真正由C语言编译器编译, 产生.s
汇编代码文件
+ 第三步, 由汇编代码文件汇编, 产生.o
目标代码文件
+ 第四步, 根据链接, 将多个目标代码文件链接起来, 产生.exe
可执行文件(linux是.out
)
宏
定义宏
#define
指令的作用是定义宏
#define PI 3.1415926
上面的这个指令定义了一个宏, PI
是这个宏的名字, 3.1415926
是这个宏的值
可以理解为,
PI
是个符号, 宏是他的具有高级感的别名在编译预处理中, 在代码中出现的所有的宏的名字都会被直接替换为对应的值(换言之, 是 原始的/直接的/原封不动 的文本替换工作)
注意
+ 宏的定义不是C语言代码, 因此后面不要加;
封号
使用宏
- 如果一个宏中有其他的宏的名字, 同样会被替换
- 如果宏的值超过一行, 需要在最后一行之前的行末加上
\
反斜杠 - 宏的值后面可以写注释, 不会被当做是宏的值
#define HW "Hello world!" // 你好世界
#define PRT \
printf("你好世界!\n"); \
printf("%s", HW)
int main()
{
PRT;
return 0;
}
注意观察
;
因为在C语言代码中, 每一行指令最后要带有封号结尾
因此宏的名字后面要加封号的话, 宏的值最后不能有封号
但是, 对于这种用来替代成段代码的宏, 其值就是代码
因此该写封号还得写总的一句, 宏的值最后不能有封号
没有值的宏
这一类宏的存在只想说明一件事: 这个宏被定义过了
会有其他预处理指令通过这一点判断”这个宏是否被定义”
这样就能实现”条件编译”(视情况而编译不同的代码)
预定义的宏
| 宏的名称 | 宏的值 |
| :——–: | :————————: |
| __LINE__
| 当前代码所在的行号 |
| __FILE__
| 源代码文件的文件名(全路径) |
| __DATE__
| 编译时的日期 |
| __TIME__
| 编译时的时间 |
注意, 这里的
__LINE__
是个整数, 其余都是字符串
像函数的宏
名字中带有圆括号的宏能实现类似于函数的功能
这样的宏可以带有参数
#define cube(x) ((x)*(x)*(x))
注意: 圆括号中的参数 没有类型
这里仍旧是纯粹的文本替换!!!
例如cube(i+5)
会转变为:
((i+5)*(i+5)*(i+5))
易错点
这种函数宏很容易犯错!!!
例如#define DOUBLE(x) x+x
在计算3*DOUBLE(2)
的时候就会得到8
这种错误答案
错误的原因就是被替换的结果是
3*2+2
, 显然运算优先级被搅乱了
因此, 一切都要带括号
+ 整个值要有括号
+ 参数出现的每一处都要有括号
此外, 函数宏可以有多个参数, 同样也可以嵌套使用其他宏
特点
带参数的宏在大型程序的代码中使用极为普遍
因为其处理效率比函数更高(更直接), 但代码大小会更大, 是牺牲空间换取效率的方式
在#
和##
两个运算符的帮助下, 甚至可以实现”产生”函数的功能(函数工厂)
使用宏的习惯上存在着中西方差异(外国人更习惯于使用宏)
宏的弊端是, 宏不会检查数据类型
拓展来说, inline函数可以取代宏的功能(会检查类型)
大程序结构
多个C文件
一个main太长, 于是分出多个函数
一个C源代码太长了, 于是也想分成多个文件
但问题就在于, 多个文件不能直接被编译为可执行文件
一些软件(如Dev C++), 可以新建一个项目, 在项目中加入多个源代码
在编译时, 所有源代码文件都会被链接起来, 编译为一个可执行文件
编译单元
一个.c
文件是一个 编译单元
编译器每次编译只处理一个编译单元
IDE
IDE(集成开发环境 Integrated Development Environment)中一般会有两个功能
+ 编译(compile) 负责处理一个编译单元, 形成.o
文件
+ 构建(build) 负责将所有.o
文件链接起来, 形成最终的可执行文件
Dev C++是一个传统的IDE, 上述两个功能在这个软件中被合并了
头文件
使用
将函数原型放在一个头文件(.h
文件)中
相当于是跨文件的声明
在需要调用函数的源代码(.c
文件)中调用时, 在头部加入
#include "function.h"
这么做可以让编译器在编译时检查函数的原型声明
头文件可以看做是个桥梁, 有助于检查函数的原型和使用
预处理指令
#include
和宏一样, 也是一个编译预处理指令
也和宏一样, include实质上只是把.h
文件原封不动的搬到了.i
文件头部的位置
终究只是个搬运工
使用的细节
#include
有两种寻找文件的方式
分别是: 双引号""
和 尖括号<>
其区别是:
+ 双引号""
会优先在当前目录下寻找文件, 如果找不到, 再到系统路径中寻找
+ 尖括号<>
直接去系统路径中寻找
因此, 一般来说, 系统标准库提供的头文件使用尖括号<>
, 自己定义的头文件使用双引号""
系统指定的目录一般是指环境变量, 且编译器自己知道
误区
#include
其实不是用来引入库的, ta只是用来原封不动的搬运代码
.h
中只有个原型, 其中定义的如printf这类函数的源代码在某个.lib
文件(Windows)或是.a
文件(Unix)中
在编译时, 这些东西会由编译器自动链接进去这也就解释了, 为什么, 不引入头文件使用函数, 只会warning但函数仍旧正常工作
这是因为只是原型丢失(给warning), 编译器会去猜原型, 正好猜中了, 于是正常运行
之所以要做#include <stdio.h>
这个动作, 只是为了让编译器知道函数的原型
保证调用的时候给出的数据类型正确
头文件的使用规范
因此, 在使用和定义函数的时候都应当有头文件!
规范的操作一般是
function.c
和function.h
两个文件成对出现
头文件中一般包含:
+ 所有公开的函数的原型
+ 所有全局变量的声明
全局变量是可以在多个
.c
文件中共享的
不对外公开的函数和全局变量
如果希望一个函数只在这一个.c
文件中使用
可以字啊函数声明前加上static关键字
那么这个函数只能在当前的编译单元中使用
这样不对外公开的函数一般称为局部函数
局部的全局变量也是同理
注意ta仍旧是全局变量, 只是在这个文件中被使用, 所以叫局部的全局变量
全局变量
声明
和函数一样, 要使得一个项目中所有的.c
文件都能访问到公用的全局变量
同样也要做出 声明
变量的声明是为了让程序知道变量的类型
因为只有知道了变量类型, 才能让函数在处理变量前有所准备
声明在头文件中实现, 语法如下
extern int number;
声明和定义
要区别开变量的 声明 和 定义
+ 定义: int i = 0;
+ 声明: extern int i;
声明的时候不能初始化!
那是定义的时候干的事情
声明这个行为是 不会产生代码 的
能够被声明的东西有很多:
+ 函数原型
+ 变量声明
+ 结构声明
+ 宏声明
+ 枚举声明
+ 类型声明
+ inline函数
相对应的, 定义会产生代码
而能被定义的只有:
+ 函数定义
+ 变量定义
除此以外都不会产生代码
所谓 产生代码, 就是编译器要去编译从而形成可执行的指令
而当编译器看到声明的时候, ta只会去做一个类似于登记的行为
以便在编译实质性代码的时候明确变量等的类型
头文件那些事儿
规则
只有声明可以被写入头文件!!!
上面这条只是规则, 算不上是法律
如果不这么做会造成一个项目中出现多个编译单元中有重名实体的情况
这很可能产生问题
问题
对于结构而言, 其声明不得重复
但是在复杂的程序结构中, 一个头文件被多次引用的情况非常常见, 甚至是难以避免的
下面就是个常见的例子:
头文件”A.h”
typedf struct point{
int x;
int y;
} Point;
头文件”B.h”
#include "A.h"
extern int NUMBER;
总程序”main.c”
#include "A.h"
#include "B.h"
int main(){
/* code here */
return 0;
}
此时, 依据前文的特性, 编译预处理完成后的实际代码是这样的:
typedf struct point{
int x;
int y;
} Point;
typedf struct point{
int x;
int y;
} Point;
extern int NUMBER;
int main(){
/* code here */
return 0;
}
显然, 结构被重复定义了, 自然少不了报错
标准头文件结构
运用条件编译和宏的配合, 就能保证一个头文件实际只会被include一次
具体语法如下:
#ifndef __A_HEAD__
#define __A_HEAD__
typedf struct point{
int x;
int y;
} Point;
#endif
之所以这个宏的左右带上了双下划线
是为了保证不和正常的宏冲突
这也是程序员的约定俗成的习惯在Visual Studio中,
#pragma once
也可以起到相同的效果
但是不是所有编译器都支持