预处理指令
程序员所编写的C代码不能直接被编译器编译,需要一段程序把C代码翻译成
标准C代码。
翻译的过程叫预处理,执行翻译的程序叫做预处理器,被翻译的代码叫做预处
理执行指令,以#开头的都是预处理指令。
查看预处理结果:
gcc —E code.c 把预处理结果直接显示到终端上
gcc -E code.c -o code.i 把预处理的结果存储到.i结尾的文件中
一、预处理指令的分类
1、文件包含
#include
#include <> 从系统指定的目录下查找头文件并导入
#include "" 先从当前目录下查找头文件,如果找不到,再从系统
指定的目录下查找头文件并导入
编译参数 -I /路径 来指定头文件的加载路径
系统指定目录是通过设置环境变量来指定 ~/.bashrc
2、宏常量
#define 宏名 字面值数据 (末尾不加分号) 例:#define MAX 100
如果在代码中使用了宏,在预处理时会把所有的宏替换成宏名后面的
字面值数据
优点:提高可扩展性(批量性修改)、因为是常量,提高了安全性、
提高可读性、可以与case配合使用
注意: 一般宏名全部大写,末尾不加分号,不能直接换行(可以
使用续行符\)
预定义的宏:
__func__ 获取函数名 %s
__FILE__ 获取文件名 %s
__DATE__ 获取当前日期 %s
__TIME__ 获取当前时间 %s
__LINE__ 获取当前行号 %d
3、宏函数
#define FUNC(arg) arg*10
带参数的宏,不是真正的函数,使用宏函数的位置会被替代成宏函数
后面的代码,提供的参数会被替换到相应的参数的位置
注意:宏函数也可以使用大括号来保护代码
不检查参数的类型,没有传参,没有返回值,只有计算结果
#define SUM (a,b) a+b
1、把代码中使用的宏函数替换为宏函数后面的代码 a+b
2、把宏函数代码中使用的参数替换为调用者提供的参数
注意:宏函数不能直接换行,如果需要换行要在末尾加续行符\
宏的二义性:
由于宏代码所处的位置、参数不同导致宏有不同的功能,这叫做宏的二义性
如何避免宏的二义性:
宏函数整体加小括号、每个参数都要加小括号
注意:使用宏函数时不要提供带自变运算符的变量作为参数
运算符:
# 把宏函数的参数变成字符串
##合并两个参数变成标识符
实现一个交换两个变量的宏函数
#define swap(a,b) {typeof(a) temp = (a);(a) = (b);(b) = temp;}
4、条件编译:(顶格写)
根据条件让代码是否参与最终的编译
头文件卫士:防止头文件被重复包含
#ifndef FILENAME_H
#define FILENAME_H
#endif//FILENAME_H
版本控制
#if VERSON >=3
#elif VERSON >=2
#elif VERSON >=1
#else
#endif
编译器控制
#if __cplusplus
printf("C++编译器\n");
#else
printf("C编译器\n");
#endif
#if __x86_64__
printf("64位操作系统\n");
#elif __i386__
printf("32位操作系统\n");
#endif
判断、调试、上线 gcc code.c -DDBUG //-D编译时定义宏
#ifdef
#else
#endif
定义打印调试信息的宏函数
#include <stdio.h>
#ifdef DEBUG
#define debug(...) printf(__VA_ARGS__)
#else
#define debug(...)
#endif
定义打印错误信息的宏函数
以上两个宏函数会使用即可
#define error(...) printf("%s %s %s:%d:%m %s %s\n",\
__FILE__,__func__,__VA_ARGS__,__LINE__,__DATE__,__TIME__)
int main(int argc,const char* argv[])
{
int num = 10;
int ret = scanf("%d",&num);
debug("ret=%d\n",ret);
}
5、常见的笔试面试题:
1、定义一个100年有多少秒的宏
#define YEAR_SEC (3600*24*365*100u)
2、在类型重定义时#define 和 typedef 区别?
如果是普通类型,他们的功能没有任何区别,
#define INT int
typedef int INT
如果是指针类型
#define INTP int*
typedef int* INTP
例:
INTP p1,p2,p3;
如果是#define 只有p1是指针变量,p2 p3都是int类型变量
如果是typedef p1p2p3都是指针变量
如果:
typedef int* pint;
#define PINT int*
那么:
const pint p ;//p不可更改,但p指向的内容可更改
const PINT p ;//p可更改,但是p指向的内容不可更改。
pint是一种指针类型 const pint p 就是把指针给锁住了 p不可更改
而const PINT p 是const int* p 锁的是指针p所指的对象。
3、定义常量时,#define和const的区别
(1) 编译器处理方式不同
define宏是在预处理阶段展开。
const常量是编译运行阶段使用。
(2) 类型和安全检查不同
define宏没有类型,不做任何类型检查,仅仅是展开。
const常量有具体的类型,在编译阶段会执行类型检查。
(3) 存储方式不同
define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。
(宏定义不分配内存,变量定义分配内存。
const常量会在内存中分配(可以是堆中也可以是栈中)。
(4)const 可以节省空间,避免不必要的内存分配。
4、宏函数与函数的区别?
他们是什么?
宏函数:不是真正的函数,只是代码替换,只是用法像函数
函数:一段具有某项功能代码的集合,会被编译成二进制指令,存储到
代码段中,函数名就是该段内存的首地址、有独立的命名空间、栈空间
有什么不一样?
函数: 有返回值、 类型检查、安全、入栈、出栈、速度慢、跳转
宏函数:只有运算结果、 通用、 危险、替换、 、速度快、冗余
二、头文件
重点:头文件中只能编写声明语句,不能有定义语句
全局变量的声明
函数声明
宏常量
宏函数
typedef 类型重定义
结构、联合、枚举的类型声明
头文件的编写规则:
1、为每个.c文件写一份.h文件,.h文件是对.c文件的说明
2、如果需要用到某个.c文件的变量、函数、宏,只需要把它的头文件导入即可
3、.c文件也需要导入自己的.h文件,目的是为了让声明与定义一致
头文件的相互包含:
假如a.h中包含了b.h的内容,而b.h又需要包含a.h的内容,这种会导致编译出错
错误 :未知的类型名‘xxx’,一般都是头文件相互包含所导致的,也可能是复制
粘贴时头文件宏名忘记改
解决方法:把a.h中需要的内容和b.h中需要的内容提取出来,写成一个c.h
三、Makefile
Makefile是由一系列编译指令组成的可执行文件,也叫做编译脚本
在终端执行make命令会自动执行Makefile脚本中的编译指令,它可以根据文件的最后修改时间,
来判断哪些文件需要被编译,哪些文件不需要被编译,从而大大的提高编译效率
编译规则:
1、如果该项目没有被编译过,则编译全部的.c文件,并链接成可执行文件
2、如果某些.c文件被修改,则只编译修改过的.c文件,并重新链接新的可执行文件
3、如果某些.h文件被修改,所有依赖它的.c都要重新编译并链接
一个最简单的Makefile脚本:
执行目标:依赖
编译指令
被依赖的目标1:依赖的文件
编译指令
被依赖的目标2:依赖的文件
编译指令
…
负责清理的执行目标:
rm …
通用的Makefile脚本
CC=gcc
STD=-std=gnu99
LIB=-lm
INC=-I./include
BIN=SAMS
FLAG=-Wall -Werror
OBJ=admin.o dao.o list.o main.o sams.o student.o teacher.o tools.o
all:$(OBJ)
$(CC) $(OBJ) $(LIB) -o $(BIN)
%.o:%.c
$(CC) $(STD) $(FLAG) $(INC) -c $< -o $@
clean:
rm -rf $(OBJ) $(BIN)