前置知识

0x00 cmd编译运行程序

https://blog.csdn.net/WWIandMC/article/details/106265734

0x01 --save-temps

gcc main.c --save-temps		#save和temps中间是一个减号

文件后缀(按照生成先后顺序排序) 释义
.i 编译器将所有的预处理指令替换完之后生成的中间文件
.s 汇编代码文件
.o 目标代码文件



0x02 tail

cmd中并没有tail命令,可以在git中使用

$ tail main.i			#在终端显示main.i末尾几行
$ tail main.i -n 50 #指明显示main.i末尾50行

cmd使用tail命令的方法:在System32目录下添加tail.exe,这种方法拓展的tail命令不支持-n选项
https://blog.csdn.net/sishi22/article/details/82285707

预定义符号

#include <stdio.h>

int
main( int argc, char **argv )
{
printf("%s\n", __FILE__ ); /*源代码文件名*/
printf("%d\n", __LINE__ ); /*出现这个符号的行号*/
printf("%s\n", __DATE__ ); /*文件被编译时的系统日期*/
printf("%s\n", __TIME__ ); /*文件被编译时的系统时间*/
printf("%d\n", __STDC__ ); /*编译器遵循ANSI C为1*/ return 0;
}

预定义符号的位置会被替换成字符串常量或数值

宏 Macro

所有用于对数值表达式进行求值的宏定义都应加上括号,避免在使用宏的时候,参数中的操作符或邻近的操作符之间不可预料的相互作用——《C和指针》

为了将函数和宏区分开,一般约定宏的名字全部大写——《C和指针》

0x00 宏的用途

  1. 定义一个常量
#define PI 3.1415926
  1. 给一个经常要使用的表达式一个字面意义
#define SIZE ( sizeof(a) / sizeof(int) )
  1. 执行简单的计算
#define MAX(a,b) ( (a) > (b) ? (a) : (b) )
  1. 改造现有的函数
#define MALLOC( type, size ) ( type* )malloc( sizeof( type ) * size )
  1. 更多…

0x01 宏定义常见错误

  1. 在末尾带了分号
#define PRINT_LOS printf("u lost");
#define PRINT_WIN printf("u win"); if( a )
PRINT_LOS;
else
PRINT_WIN;


编译器报错:没有和else级联的if
原因:宏定义中多余的分号,相当于在if和else之间插入了多余的一行

  1. 在定义含参数的宏时,宏名称和括号之间带了空格
#define CUBE (x) ( (x) * (x) * (x) )


空格的存在让编译器把(x)也当成了要替换进去的内容

3.在带参数的宏中使用++操作符

#define CUBE(x) ( (x) * (x) * (x) )

printf("a ^ 3 = %d\n", CUBE( a++ ) );
printf("a = %d\n", a);

如果把带参数的宏不加区分的当成函数用的话,我们预测将打印出8和3

而实际上打印出了24和5

查看main.i之后一目了然

4.更多…

0x02 宏的三个技巧

  1. 当字符串常量作为宏参数时给出时,可以利用相邻字符串的自动连接的特性
#include <stdio.h>

#define MYPRINT( FORMAT, VALUE )	\
printf( "The value is" FORMAT "\n", VALUE ) int
main( int argc, char **argv )
{
MYPRINT( "%d", 17); return 0;
}

  1. 使用#让预处理器将一个宏参数转换为一个字符串常量
#define MYPRINT( FORMAT, VALUE )	\
printf("The value of" #VALUE "is" FORMAT "\n", VALUE ) MYPRINT( "%d", x +3); /*特地在+后面不留空格*/

  1. 使用##连接两个相邻的符号
#include <stdio.h>

#define ADD_TO_SUM( sum_number, value )	\
sum ## sum_number += value int
main( int argc, char **argv )
{
int sum5 = 3;
ADD_TO_SUM( 5, 10 );
printf("%d", sum5); return 0;
}


注意:##是单纯的字符连接,即使sum_number是一个有值的int变量,也只是连接变量名而已

#include <stdio.h>

#define ADD_TO_SUM( sum_number, value )	\
sum ## sum_number += value int
main( int argc, char **argv )
{
int sum5 = 3;
int i = 5;
ADD_TO_SUM( i, 10 );
printf("%d", sum5); return 0;
}


0x03 删除宏定义

#undef DEBUG

重新定义某个已定义的宏之前,必须要用#undef移除旧定义

函数和宏的对比

0x00 执行效率对比

#include <stdio.h>

#define MAX(a,b) ( (a) > (b) ? (a) : (b) )

int
main( int argc, char **argv )
{
int a = 1;
int b = 2;
printf("%d", MAX( a, b ) ); return 0;
}

宏版本对应的反汇编代码

00F4144E  mov         dword ptr [a],1						;[]取地址符, dword ptr 指向双字的指针

00F41455  mov         dword ptr [b],2  

00F4145C  mov         eax,dword ptr [a]  					;将a的内容送入eax寄存器
00F4145F cmp eax,dword ptr [b] ;b的内容和eax的内容做比较, 比较结果作用于下一条jle指令
00F41462 jle main+3Fh (0F4146Fh) ;a大于b为假则跳转到0F4146F 00F41464 mov ecx,dword ptr [a]
00F41467 mov dword ptr [ebp-0DCh],ecx ;将ecx的内容送入内存地址为ebp-0DCh的双字单元
00F4146D jmp main+48h (0F41478h) ;无条件跳转到0F41478 00F4146F mov edx,dword ptr [b]
00F41472 mov dword ptr [ebp-0DCh],edx
00F41478 mov esi,esp ;esi存放源数据串的偏移地址, esp存放指向当前堆栈的栈顶偏移地址
00F4147A mov eax,dword ptr [ebp-0DCh]
00F41480 push eax ;eax内容入栈
00F41481 push offset string "%d" (0F45A00h) ;内存地址0x0F45A00的格式字符串入栈
00F41486 call dword ptr [__imp__printf (0F482B0h)] ;调用printf函数
#include <stdio.h>

int
max( int a, int b ); int
main( int argc, char **argv )
{
int a = 1;
int b = 2;
printf("%d", max( a, b ) ); return 0;
} int
max( int a, int b )
{
return ( (a) > (b) ? (a) : (b) );
}

函数版本对应的反汇编代码

010F10EB  jmp         max (10F1420h)				;跳转到函数体部分

...

010F13AE  mov         dword ptr [a],1
010F13B5 mov dword ptr [b],2 010F13BC mov eax,dword ptr [b]
010F13BF push eax
010F13C0 mov ecx,dword ptr [a]
010F13C3 push ecx
010F13C4 call @ILT+230(_max) (10F10EBh)
010F13C9 add esp,8
010F13CC mov esi,esp
010F13CE push eax
010F13CF push offset string "%d" (10F573Ch)
010F13D4 call dword ptr [__imp__printf (10F82B0h)] ... 010F1420 push ebp
010F1421 mov ebp,esp
010F1423 sub esp,0C4h
010F1429 push ebx
010F142A push esi
010F142B push edi
010F142C lea edi,[ebp-0C4h]
010F1432 mov ecx,31h
010F1437 mov eax,0CCCCCCCCh
010F143C rep stos dword ptr es:[edi] 010F143E mov eax,dword ptr [a] ;准备完毕开始执行
010F1441 cmp eax,dword ptr [b]
010F1444 jle max+31h (10F1451h) 010F1446 mov ecx,dword ptr [a]
010F1449 mov dword ptr [ebp-0C4h],ecx
010F144F jmp max+3Ah (10F145Ah) 010F1451 mov edx,dword ptr [b]
010F1454 mov dword ptr [ebp-0C4h],edx
010F145A mov eax,dword ptr [ebp-0C4h] ;将结果存入eax寄存器 010F1460 pop edi ;pop出栈操作
010F1461 pop esi
010F1462 pop ebx
010F1463 mov esp,ebp
010F1465 pop ebp
010F1466 ret

从宏版本的汇编代码和函数版本的汇编代码的长度对比来看,用宏来执行一些简单的计算,所需的开销比函数小

0x01 参数使用对比

在前面常见错误部分,我们讲到在宏当中使用++是有风险的。我们的本意是想在完成所有操作之后再让参数++, 但实际的执行结果是++执行了很多次。其原因是宏进行简单的文本替换,++作为参数的一部分参与文本替换,有多个参数时就会有多个++

而对于函数来说,函数获得的是参数的拷贝而不是参数本身。函数结束之后,后缀++才作用于参数。

0x02 参数类型对比

宏执行的操作是文本的替换,它与类型无关,我们可以把int, float这些关键字作为参数进行传递,如下面这个例子

#define MALLOC( type, size ) ( type* )malloc( sizeof( type ) * size )

a = MALLOC( int, 10 );

而函数的参数与类型是有关的。如果参数类型不同就需要不同的函数,即使代码一模一样

0x03 代码长度对比

每次使用宏时,宏代码都被插入到代码中。会增加代码的长度。函数则不存在长度变化的问题。

0x04 总结

属性 函数
执行效率 较慢
参数类型 无关 有关
代码长度 增加 不变
++等操作符的副作用 存在 不存在

条件编译

0x00 #if

#if 常量
语句
#endif

和if-else一样,当常量为1时,语句正常编译;如果常量为0,编译器则将它们删除。
为什么要强调是常量呢?来看一个例子

#include <stdio.h>

#define OP +
int
main( int argc, char *argv[] )
{
int option;
scanf("%d", &option); #if option #undef OP
#define OP - #endif printf("%d", 2 OP 1 );
return 0;
}

现在我们在#if后面跟了一个变量,我们希望option赋值为1之后,OP能换成减号,从而输出1。编译运行

两次结果都是3。查看.i文件

在生成可执行文件之前需要进行编译预处理,在执行程序之前就已经完成了宏文本的替换,而对option赋值的操作是在执行程序时。所以在#if使用变量是没有意义的。

如果要让程序不执行某些语句,又不想把它们从源文件中删除,除了注释之外,还可以使用#if

#define DEBUG 0

#if DEBUG

#endif

#if也支持类似else if的级联结构

#if DEBUG1

#elif DEBUG2

#else

#endif

0x01 #ifdef和#ifndef

#ifdef和#ifndef的字面意思就是 if defined 和 if not defined

#ifdef DEBUG

#endif

#ifndef DEBUG

#endif
#define DEBUG	/*此时DEBUG被定义为一个空字符串*/
#ifdef DEBUG #endif

0x02 命令行定义

gcc main.c -DDEBUG		# -D后面是宏的名字
gcc main.c -DDEBUG=3 # DEBUG定义为3
gcc main.c -Dmian=main # 把main替换为main 这是一个历史悠久的bug
gcc main.c -UDEBUG # 忽略DEBUG的初始定义
int
fact( int n )
{
int fact = 1;
while( n > 1 ){
fact *= n--;
#ifdef DEBUG
printf("%d\n", fact );
#endif
} return fact;
}


文件包含

0x00 #include做了什么

创建一个头文件head.h

/*
** head.h
*/
void f( void );

在main.c添加include

/*
** main.c
*/
#include "head.h"


head.h的内容插入了main.c,可见#incldue和#define一样,都是做文本替换的工作

在.i文件中,#表示此行是注释的意思

0x01 三种#include方式

#include <stdio.h>

编译器在默认的“函数库”中查找头文件

#include "head.h"
#include "stdio.h"

编译器在源文件所在目录查找,如果找不到回到默认的函数库再找一遍,这也是为什么"stdio.h"能通过编译的原因

#include "C:\Users\Administrator\Desktop\folder\head.h"

给出头文件的路径

0x02 嵌套文件包含

现在有两个头文件a.h和b.h

#include "a.h"
#include "b.h"

其中b.h中#include了a.h

/*
**b.h
*/
#include "a.h"

这就意味着实际上a.h被替换了两次,这种嵌套的文件包含会影响编译的速度

在C++中,如果头文件里有class的声明,嵌套文件包含引起的class重复声明,将会引起编译错误

禁止套娃的办法就是使用#ifndef

#ifndef HEAD_H_
#define HEAD_H_ #endif

0x03 一道思考题


ppt内容

其他

0x00 #error

用于生成错误信息

#ifdef OPTION1
#define OP1
#elif OPTION2
#define OP2
#else
#error No option select!
#endif

0x01 #line

			#line 100 "head.h"
/*100*/![在这里插入图片描述](https://img-blog.csdnimg.cn/20200522180321585.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dXSWFuZE1D,size_16,color_FFFFFF,t_70)
/*101*/
/*102*/ #include <stdio.h>
/*103*/
/*104*/ int
/*105*/ main( int argc, char *argv[] )
/*106*/ {
/*107*/ printf("%d\n", __LINE__);
printf("%s\n", __FILE__); return 0;
}


#line会修改__LINE__和__FILE__的值,其中#line会指定下一行为指定的行号

参考链接

https://www.icourse163.org/learn/ZJU-9001?tid=9001#/learn/content?type=detail&id=176002&sm=1
https://study.163.com/course/courseLearn.htm?courseId=271005#/learn/video?lessonId=380125&courseId=271005
https://www.icourse163.org/learn/ZJU-9001?tid=9001#/learn/content?type=detail&id=175002&cid=191088

C 编译预处理和宏的更多相关文章

  1. 编译预处理 -- 带参数的宏定义--【sky原创】

    原文:编译预处理 -- 带参数的宏定义--[sky原创] 如有转载请注明出处   编译预处理  --  带参数的宏定义 前面为输出文件,后面为输入文件 gcc -E -o test.i test.c ...

  2. Verilog学习笔记基本语法篇(十二)········ 编译预处理

    h Verilog HDL语言和C语言一样也提供编译预处理的功能.在Verilog中为了和一般的语句相区别,这些预处理语句以符号"`"开头,注意,这个字符位于主键盘的左上角,其对应 ...

  3. c语言编译预处理和条件编译执行过程的理解

    在C语言的程序中可包括各种以符号#开头的编译指令,这些指令称为预处理命令.预处理命令属于C语言编译器,而不是C语言的组成部分.通过预处理命令可扩展C语言程序设计的环境. 一.预处理的工作方式 1.1. ...

  4. C++的编译预处理

    C++中,在编译器对源程序进行编译之前,首先要由预处理对程序文本进行预处理.预处理器提供了一组预编译处理指令和预处理操作符.预处理指令实际上不是C++语言的一部分,它只是用来扩充C++程序设计的环境. ...

  5. C预编译, 预处理, C/C++头文件, 编译控制,

    在所有的预处理指令中,#Pragma 指令可能是最复杂的了,它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作.#pragma指令对每个编译器给出了一个方法,在保持与C和C++语言完全兼容的 ...

  6. C语言条件编译及编译预处理阶段(转)

    一.C语言由源代码生成的各阶段如下: C源程序->编译预处理->编译->优化程序->汇编程序->链接程序->可执行文件 其中 编译预处理阶段,读取c源程序,对其中的 ...

  7. VerilogHDL编译预处理

    编译预处理语句 编译预处理是VerilogHDL编译系统的一个组成部分,指编译系统会对一些特殊命令进行预处理,然后将预处理结果和源程序一起在进行通常的编译处理.以”`” (反引号)开始的某些标识符是编 ...

  8. 【转】C语言条件编译及编译预处理阶段

    原文: http://www.cnblogs.com/rusty/archive/2011/03/27/1996806.html 1. 宏定义(宏代换,宏替换,宏: 宏定义是C语言提供的3中预处理功能 ...

  9. C++——多文件结构和编译预处理命令

    [toc] 一.多文件结构 1.一个工程可以划分为多个源文件 类声明文件(.h文件) 类实现文件(.cpp文件) 类的使用文件(main函数所在的.cpp文件) 2.利用工程来组合各个文件 //Poi ...

随机推荐

  1. centos7 wget安装Tomcat7

    2021-07-15 1.环境介绍 操作系统:centos7 jdk版本:jdk1.8.0.211 tomcat版本:tomcat7.0.109 2. 检查系统中是否已经安装 jdk ,如未安装, 请 ...

  2. QT学习日记篇-03-仿写一个智能家居界面

    课程大纲: <1>让界面漂亮起来,仿写一个智能家居界面 ->第一:给QT工程添加图片 进入下一步: <注意路径和名称一定不能有中文>                   ...

  3. Metasploit用法详解

    Metasploit简介 1. Auxiliaries(辅助模块) 该模块不会直接在测试者和目标主机之间建立访问,它们只负责执行扫描.嗅探.指纹识别等相关功能以辅助渗透测试. 2. Exploit(漏 ...

  4. C#多线程开发-任务并行库04

    你好,我是阿辉. 之前学习了线程池,知道了它有很多好处. 使用线程池可以使我们在减少并行度花销时节省操作系统资源.可认为线程池是一个抽象层,其向程序员隐藏了使用线程的细节,使我们可以专心处理程序逻辑, ...

  5. Python常见问题 - 写入数据到 excel 报 ValueError: invalid literal for int() with base 10 错误

    背景 在上写入数据到excel中,报了以下错误 出现原因 对于写入excel场景下出现该错误的话,很大概率是写入数据的单元格原本的数据格式有问题 解决方法 清理掉单元格的旧数据,然后再写入就可以了

  6. L2TP协议简介

    传送门:L2TP代码实现 1. L2TP 概述 L2TP(Layer 2 Tunneling Protocol,二层隧道协议)是 VPDN(Virtual Private Dial-up Networ ...

  7. 深入理解SpringBoot核心机制《spring-boot-starter》

    深入理解SpringBoot核心机制<spring-boot-starter> 前言: 对于这几年java火爆天的springBoot我相信大家都有所使用过,在springBoot的项目中 ...

  8. MySQL实战45讲(06--10)-笔记

    目录 06 | 全局锁和表锁 :给表加个字段怎么有这么多阻碍? 全局锁 表级锁 小结 07 | 行锁功过:怎么减少行锁对性能的影响? 死锁和死锁检测 08 | 事务到底是隔离的还是不隔离的? &quo ...

  9. sync 修饰符在Vue中如何使用

    在有些情况下,我们可能需要对一个 prop 进行"双向绑定".不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源.   这 ...

  10. OpenGL渲染管道,Shader,VAO&VBO&EBO

    OpenGL渲染管线 (也就是)OpenGL渲染一帧图形的流程 以下列举最简单的,渲染一个三角形的流程,你可以将它视为 精简版OpenGL渲染管线 更复杂的流程也仅仅就是:在此基础上的各个流程中 添加 ...