预处理:
程序员所编译C代码不能被直接编译,它需要一段程序把它先翻译一下,被翻译过程预处理,负责翻译的程序叫预处理器,被翻译的指令叫预处理指令,C代码中以#开头的都是预处理指令。
gcc -E xxx.c 查看C代码的预处理结果,显示在终端 gcc -E xxx.c -o xxx.i 把预处理的结果保存到文件中,以.i结尾的文件也被称为预处理文件。
文件包含指令:
#include 预处理指令的功能是导入一个头文件到当文件中,它用两中使用方法:
方法1:#include <file_name.h>
从系统指定的路径查找并导致头文件,一般用于导入标准库、系统、第三方库的头文件。getch.h
/usr/include
方法2:#include "file_name.h"
从系统当前路径查找并导致头文件,如果没有再从系统指定的路径查找并导致头文件,一般用于导入自定义头文件。
操作系统通过设置环境变量来指定头文件查找路径,或者设置编译器参数
gcc xxx -I /path(大写i)
宏替换指令:
定义宏名:
#define 宏名 会被替换的内容
使用宏名:
printf("%d\n",宏名);
注意:在预处理阶段,预处理器会把代码使用的宏名替换成宏名后面的那段内容。
宏常量:
#define 宏名 字面值数据
给没有意义的字面值数据,取一个有意义名字,代替它,这样可以提高代码的可读性、可扩展性,还可以方便代码扩展。
#include <stdio.h> #define NUM 100 #define ARR_LEN 10 void show_arr(int arr[],int len) { for(int i=0; i<len; i++) { printf("%d ",arr[i]); } } int main(int argc,const char* argv[]) { int num = 100; int len = 10; printf("%d\n",NUM); printf("%d\n",num); int arr[ARR_LEN] = {1,12,3,4,5}; len = 100; ARR_LEN = 100; show_arr(arr,ARR_LEN); }
宏表达式:
#define 宏名 表达式、操作、更复杂的标识符
#include <stdio.h> typedef struct Student { int id; char name[20]; char sex; float score; }Student; #define STU_FORMAT "%d %s %c %f" #define pf printf #define sf scanf #define YEAR_SEC 3600*24*365lu int main(int argc,const char* argv[]) { Student stu = {1001,"hehe",'w',88.8}; printf("请输入学生信息:"); sf(STU_FORMAT,&stu.id,stu.name,&stu.sex,&stu.score); pf(STU_FORMAT,stu.id,stu.name,stu.sex,stu.score); }
定义宏常量和宏表达式要注意的问题:
由于宏常量和宏表达式可能使用在表达式中,因此在定义宏常量和宏表达式的末尾不要加分号。
一般宏名全部大写以作区分
(局部变量全部小写,全局变量首字母大写,循环变量i、j、k、函数名全部小写+下划线、数组arr、字符串str、指针p)
枚举常量与宏常量的区别:
提前:它们都可以提高代码的可读性,给没有意义字面值数据取一个有意义的名字。
1、从安全角度来说枚举常量要比宏常量安全,因为宏常量是通过替换实现的,在替换过程中可能会导致新的错误,如:有与宏名重名和函数或变量。
2、但枚举常量没有宏常量方便,枚举常量只能是整型数据,而宏常量可以是任何类型的,甚至是复杂的表达式,宏常量的使用范围更广。
总结:如果是大量的整型字面值数据建议定义为枚举常量,如果少量的或整型以外的类型的字面值数据建议定义为宏常量。
预定义的宏:
编译器预定义的宏:
__FILE__ 获取当前文件名 __func__ 获取当前函数名 __LINE__ 获取当前行号 __DATE__ 获取当前日期 __TIME__ 获取当前时间 __WORDSIZE 获取当前编译器的位数 // 适合用来显示警告、错误信息。
#include <stdio.h> int main(int argc,const char* argv[]) { printf("%s\n",__FILE__); printf("%s\n",__func__); printf("%d\n",__LINE__); printf("%s\n",__DATE__); printf("%s\n",__TIME__); printf("%d\n",__WORDSIZE); }
标准库预定义的宏:
// limits.h 头文件中定义的所有整数类型最大值、最小值 #define SCHAR_MIN (-128) #define SCHAR_MAX 127 #define UCHAR_MAX 255 #define SHRT_MIN (-32768) #define SHRT_MAX 32767 #define USHRT_MAX 65535 #define INT_MIN (-INT_MAX - 1) #define INT_MAX 2147483647 #define UINT_MAX 4294967295U #define LLONG_MAX 9223372036854775807LL #define LLONG_MIN (-LLONG_MAX - 1LL) #define ULLONG_MAX 18446744073709551615ULL PATH_MAX // stdlib.h 头文件定义两个结标志 #define EXIT_SUCCESS (0) #define EXIT_FAILURE (-1) // stdbool.h 头文件定义了bool、true、false #define bool _Bool #define true 1 #define false 0 // libio.h 头文件定义了NULL #define NULL ((void*)0)
宏函数:
什么是宏函数:
宏函数不是真正的函数,而是带参数的宏替换,只是使用方法像函数而已。
在代码中使用宏函数,预处理时会经历两次替换,第一次把宏函数替换成它后面的一串代码、表达式,第二次把宏函数中的参数替换到表达式中。
#define 宏名(a,b,c,...) a+b*c
#include <stdio.h> #define ARR_SIZE(a) sizeof(a)/sizeof(a[0]) int main(int argc,const char* argv[]) { int arr[] = {1,2,3,4,5,1,3,4,5,5,3,4,5}; for(int i=0; i<ARR_SIZE(arr); i++) //1、for(int i=0; i<sizeof(a)/sizeof(a[0]); i++) // 第一次替换成宏名后面的表达式 //2、for(int i=0; i<sizeof(arr)/sizeof(arr[0]); i++) // 第二次把a替换成arr { printf("%d ",arr[i]); } }
定义宏函数要注意的问题:
1、假如宏函数执行复杂的多条语句,可能会因为在if分支中缺少大括号而出现问题,可以使用大括号包括,进行保护,避免if的影响。
#define 宏名(a,b,c,...) {代码1; 代码2; ...}
#include <stdio.h> #define SWAP(a,b) int t=a; a=b; b=t; int main(int argc,const char* argv[]) { int n1 = 10,n2 = 20; if(1) SWAP(n1,n2); printf("%d %d\n",n1,n2); } 错误1:只有第一句属于if的内容 错误2:语句末尾多一个分号 解决1: #define SWAP(a,b) {int t=a; a=b; b=t;}
2、可以通过加大括号解决问题1,但是如果if后面有else,也会出现问题
可以通过 do-while(0)
#include <stdio.h> #define SWAP(a,b) {int t=a; a=b; b=t;} int main(int argc,const char* argv[]) { int n1 = 10,n2 = 20; SWAP(n1,n2); if(xxx) SWAP(n1,n2); else printf("hehe"); printf("%d %d\n",n1,n2); } 错误:SWAP后面的分号会被编译器认为是if语句的分号,导致后面的else没有对应的if搭配,同时为了保证编写代码的统一性,又不能去掉分号 解决: #define SWAP(a,b) do{int t=a; a=b; b=t;}while(0)
3、因此linux内核和C++开源的代码中,经常会在宏定义中使用do-while(0)来保证代码安全,除此之外还可以起到:
在宏函数中定义同名变量、而不会冲突(语句块定义)
还可以在解决代码冗余问题上替换goto的效果
if(xxx) {free(p); p=NULL;} if(aaa) {free(p); p=NULL;} if(ccc) {free(p); p=NULL;} dosomething do{ if(xxx) break; if(aaa) break; if(ccc) break; }while(0); free(p); p = NULL;
4、宏函数后面的代码不能直接换行,如果代码确定太长,可以使用续行符换行。
#define 宏名(a,b,c,...) { \ 代码1; \ 代码2; \ ... \ }
#define 宏名(a,b,c,...) do{ \ 代码1; \ 代码2; \ ... \ }while(0)
5、为了防止宏函数出现二义性,对宏参数要尽量多加小括号。
二义性:就是使用宏函数的环境不同、参数不同,造成宏函数有多执行规则,会出现出乎意料的执行结果,这种宏函数的二义性,设计宏函数时要尽量杜绝。
#define SUM(a,b) (a+b) #define MUT(a,b) ((a)*(b)*SUM(a,b)) #define XXX(a,b) 3.4*a+MUT(a,b)*SUM(a,2) int main(int argc,const char* argv[]) { printf("%d\n",SUM(4,5)); int num = SUM(3,4)*10; printf("%d\n",num); printf("%d\n",MUT(3+2,10)); }
调用宏函数要注意的问题:
1、传递给宏函数的参数不能使用自变运算符,因为我们无法知道参数在宏代码中会被替换多少次。
#define SUM(a,b) (a+b) #define MUT(a,b) ((a)*(b)) #define SUM1(a,b) ((a)+(a)+(b)) int main(int argc,const char* argv[]) { printf("%d\n",SUM(4,5)); int num = SUM(3,4)*10; printf("%d\n",num); printf("%d\n",MUT(3+2,10)); int n1=2,n2=10; printf("%d\n",SUM1(n1++,n2)); printf("%d\n",n1) }
2、宏函数没有返回值,只是个别宏函数表达式有计算结果。
普通函数与宏函数的优缺点?常考点
宏函数的优点:
1、执行速度快,它不是真正的函数调用,而是代码替换,不会经历传参、跳转、返回值。
2、不会检查参数的类型,因此通用性强。
宏函数的缺点:
1、由于它不是真正的函数调用,而是代码替换,每使用一次,就会替换出一份代码,会造成代码冗余、编译速度慢、可执行文件变大。
2、没有返回值,最多可以有个执行结果。
3、类型检查不严格,安全性低。
4、无法进行递归调用。(自己调用自己)
普通函数的优点:
1、不存在代码冗余的情况,函数的代码只会在代码段中存储一份,使用时跳转过去执行,执行结束后再返回,还可以附加返回值。
2、安全性高,会对参数进行类型检查。
3、可以进行递归调用,实现分治算法。
函数的缺点:
1、相比宏函数它的执行速度慢,调用时会经历传参、跳转、返回等过程,该过程耗费大量的时间。
2、类型专用,形参什么类型,实参必须是什么类型,无法通用。
什么样的代码适合封装成宏函数?
1、代码量少,即使多次使用也不会造成代码段过度冗余。
2、调用次数少,但执行次数多,也就是宏函数会在循环语句中调用。
for(int i=0; i<10000; i++) { FUNC(a,b); // a+b }
3、函数的功能对返回值没有要求,也就是函数的功能不是通过返回值达到的。
例如:遍历
练习:
1、封装一个malloc、free函数。
my_malloc 它要记录my_malloc的调用位置(文件、函数、行号),申请到的内存地址
my_free 它要记录my_free的调用位置(文件、函数、行号),释放到的内存地址
#include <stdio.h> #include <stdlib.h> void* _my_malloc(const char* filename,const char* func,size_t line,size_t size) { void* ptr = malloc(size); printf("%s %s %u %p\n",filename,func,line,ptr); return ptr; } #define my_malloc(size) _my_malloc(__FILE__,__func__,__LINE__,size) #define my_free(ptr) do{\ printf("%s %s %d %p\n",\ __FILE__,\ __func__,\ __LINE__,\ ptr);\ free(ptr);}while(0) int main(int argc,const char* argv[]) { //int* p = my_malloc(__FILE__,__func__,__LINE__,40); int* p = my_malloc(40); my_free(p); }
2、实现一个通用的变量交换函数。能写多少个、分析优缺点
#include <stdio.h> // 只适合数值型数据、数据可能溢出 #define SWAP(a,b) do{a=a+b; b=a-b; a=a-b;}while(0) // 数据不会溢出,只适合整形数据、并且不能是同一个变量交换 #define SWAP(a,b) do{a=a^b; b=a^b; a=a^b;}while(0) // 不能交换结构变量 浪费内存 #define SWAP(a,b) do{long double t=a;a=b;b=t;}while(0) // 可以交换任意类型,多一个参数 #define SWAP(a,b,type) do{type t=a;a=b;b=t;}while(0) // typeof只能在gnu系列编译器中使用,能够获取数据的类型并返回 #define SWAP(a,b) \ do{typeof(a) (t)=(a); (a)=(b); (b)=(t);}while(0) int main(int argc,const char* argv[]) { int n1 = 10,n2 = 20; SWAP(n1,n1); printf("%d %d\n",n1,n2); }
常考笔试题:请问#define 与 typedef有什么区别
回答是什么? #define宏定义 typedef类型重定义 如果数据是普通类型时,效果没有任何区别 #define INT int typedef int INT; INT num; // num都是int类型 如果数据是指针类型,就有很大区别 #define INTP int* typedef int* INTP; INTP p1,p2,p3; //#define int* p1,p2,p3; p1是int* p2p3是int //typedef INTP p1,p2,p3; p1p2p3都是int*
条件编译:
条件语句(if、switch、for、while、do while)会根据条件选择执行哪些代码,条件编译就是预处理器根据条件选择哪些代码参与下一步的编译。
负责条件编译的预处理指令有:
#if #ifdef #ifndef #elif #else #endif
头文件卫士:
这种固定写法,在头文件中使用,它能防止头文件被重复包含,所有的头文件都要遵循这个规则。
#ifndef FILE_H // 判断FILE_H宏是否正在,不存在则条件为真 #define FILE_H // 定义FILE_H宏 // 头文件卫士能保证此处不重复出现 #endif//FILE_H // #ifndef的结尾
注释代码:
// 只能注释单行代码,早期的编译器不支持该用 /* 多行注释,但不能嵌套 */ #if 0|1 可注释大块代码,可以嵌套 #endif
版本、环境判断:
#if __WORDSIZE == 64 typedef long int int64_t; #else typedef long long int int64_t; #endif // 判断是否是Linux操作系统: #if __linux__ #endif // 判断是否是Windows操作系统: #if __WIN32 | __WIN32__ | __WIN64__ #endif // 判断gcc还是g++: int main(int argc,const char* argv[]) { #if __cplusplus printf("你使用是g++编译器\n"); #else printf("你使用是gcc编译器\n"); #endif }
DEBUG宏:
专门用于调试程序的宏函数,这种宏函数在程序测试、调试、试运行阶段执行,在程序正式上线阶段不执行,这类函数会根据DEBUG宏是否定义确定执行的流程。
一些操作提示,如:xxx操作成功,xxx操作失败,分配内存的记录、释放内存的记录,这类型消息开发人员、测试人员需要看到,但用户不需要看到。
#include <stdio.h> #include <stdlib.h> //#define DEBUG #ifdef DEBUG void* _my_malloc(const char* filename,const char* func,size_t line,size_t size) { void* ptr = malloc(size); printf("%s %s %u %p\n",filename,func,line,ptr); return ptr; } #define my_malloc(size) _my_malloc(__FILE__,__func__,__LINE__,size) #define my_free(ptr) do{\ printf("%s %s %d %p\n",\ __FILE__,\ __func__,\ __LINE__,\ ptr);\ free(ptr);}while(0) #else #define my_malloc malloc #define my_free free #endif
不常用的预处理指令:
#line <常整数> 设置当前代码的行号,目前没有发现它有什么用 #error "在预处理阶段提示错误信息",一旦预处理遇到它,将不再继续编译,不建议单独使用必须与条件判断系列语句配合使用 #warning "在预处理阶段提示警告信息" 不建议单独使用,最好与条件判断系列语句配合使用。 #pragma GCC poison <标识符> 把标识符设置病毒,禁止在代码中使用 #pragma GCC poison goto #pragma pack(n) 设置最大对齐和补齐字节数 每个系统在进行对齐和补齐都有一个最大对齐和补齐字节数n,也就是超出n字节按n字节计算,例如:linux32系统n=4,windows32 n=8 设置要求: 1、n < 系统默认的最大对齐、补齐字节数,往大了调整没有意义,速度不会提升还会导致内存浪费。 2、n必须是2的x次方,也就是必须是1、2、4、8、16这一类的整数
宏函数的变长参数:
#define func(...) __VA_ARGS__
注意:这种用法必须配合,printf/fprintf/sprintf系列支持变长参数的函数使用。
在编译时定义宏:
gcc xxx.c -D ARR_LEN=3 -D ARR_LEN=3 <=> #define ARR_LEN 3 跟在代码中定义宏的效果一样 gcc xxx.c -D DEBUG -D DEBUG <=> #define DEBUG
调试阶段打印调试信息:
#ifdef DEBUG #define debug(...) printf(__VA_ARGS__) #else #define debug(...) #endif #define error(...) printf("%s %s %d %s %s:%m %s\n",__FILE__,__func__,__LINE__,__DATE__,__TIME__,__VA_ARGS__)