读过linux内核源码的同学都知道,在linux内核中,宏使用的非常多,且比较复杂,如果我们对自己进行宏展开的正确性没有信心的话,可以使用下面我介绍的这个方式,使用它,你可以非常容易的得到任意文件宏展开后的结果...我们知道,一个程序的构建分为预处理、编译、汇编、链接这些阶段,而宏展开就发生在预处理阶段。...依据该思路,我们只要在编译比如上面的net/socket.c文件时,加上这些参数,我们就能得到这些临时文件,也就可以查看其预处理之后的宏展开是什么样子的了。...但是,如果只是为了查看单个文件的宏展开后结果,就保存下整个内核中,所有源文件编译时的临时文件,这是非常耗时且不划算的,那有没有办法可以想查看哪个文件的宏展开,就单独编译一次那个文件呢? 还真有。...上文讲到,get_current方法里的this_cpu_read_stable方法宏展开后主要是一条asm语句,可能有些同学对该语句不太熟悉,它其实并不是c语言标准规范里的语法,而是gcc对c标准的扩展
, v); 我们在使用宏的时候,可以使用(), [], {},都是可以的。但是一般都是按照约定成俗的方式来使用。例如:vec![1,2,3],而不是使用 vec!{1,2,3}。...宏也被展开了, 但是并没有完全展开,其中还包含了一个format_args! 宏,我们来看一下,是否和println宏的定义一样。 // println宏的定义 macro_rules!...[…]`` 属性内部的数据。 tt,单个的 token 树。 vis,可能为空的一个 Visibility 修饰符。比如 pub、pub(crate) 声明式宏还算比较简单。...宏的缺点 宏目前的编写无法得到IDE很好的支持,另外一点就是如无必要,就不要编写宏。如果要编写,那么尽量编写声明式宏,而不是过程宏。...不利于错误检查:宏展开发生在编译期间,因此错误信息可能不够明确和直观,难以定位宏展开后的具体错误位置。 难以调试:宏展开过程对于开发者不是透明的,因此在调试过程中可能会遇到难以解决的问题。
保证宏函数按照预期运行 由于宏函数仅仅是完成替换操作,将参数替换并拼接到替换体的表达式中。而不是先让参数运算得到结果后,再进行运算。...此外,宏函数展开后的表达式,如果作为一个更大表达式的子表达式,那么它有可能受到左右两边运算符优先级的影响。因此,为了保证宏函数展开后的表达式能够优先计算,请在替换体两边加上括号。...它也 有可能在另一个平台上,仅占用2字节大小,数据范围为-32768到32767。 如果我们要求程序需要满足在不同的平台上均能正确的运行,不会因为整型数据范围不同而产生数据溢出。...,意为是否定义了某某宏。...#define PERSON_H戳,用于记录是否定义 预处理指令#ifndef用于测试其后跟着的宏是否没有被定义。 若没有被定义,则保留从#ifndef到#endif之间的代码。
保证宏函数按照预期运行 由于宏函数仅仅是完成替换操作,将参数替换并拼接到替换体的表达式中。而不是先让参数运算得到结果后,再进行运算。因此,为了保证参数不被其他运算符优先级影响,请在参数两边加上括号。...此外,宏函数展开后的表达式,如果作为一个更大表达式的子表达式,那么它有可能受到左右两边运算符优先级的影响。因此,为了保证宏函数展开后的表达式能够优先计算,请在替换体两边加上括号。...它也 有可能在另一个平台上,仅占用2字节大小,数据范围为-32768到32767。 如果我们要求程序需要满足在不同的平台上均能正确的运行,不会因为整型数据范围不同而产生数据溢出。...,意为是否定义了某某宏。...#define PERSON_H戳,用于记录是否定义 预处理指令#ifndef用于测试其后跟着的宏是否没有被定义。 若没有被定义,则保留从#ifndef到#endif之间的代码。
这⾥我们就得展开开讲解⼀下翻译环境所做的事情。 其实翻译环境是由编译和链接两个大的过程组成的,而编译又可以分解成:预处理(有些书也叫预编译)、编译、汇编三个过程。...或保留所有的#pragma的编译器指令,编译器后续会使⽤。 经过预处理后的.i⽂件中不再包含宏定义,因为宏已经被展开。并且包含的头⽂件都被插⼊到.i⽂件中。...所以当我们⽆法知道宏定义或者头⽂件是否包含正确的时候,可以查看预处理后的.i⽂件来确认。...经过预处理,会产生一个没有头文件(都已经被展开了)、宏定义(都已经替换了),没有条件编译指令(该屏蔽的都屏蔽掉了),没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。...编译:将预处理完的文件逐一进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。编译是针对单个文件编译的,只校验本文件的语法是否有问题,不负责寻找实体。
因为宏定义只是简单的字符串代换,在预处理阶段完成,而typedef是在编译时处理的,它不是作简单的代换,而是对类型说明符重新命名,被命名的标识符具有类型定义说明的功能。...加这层宏是为了把所有宏的参数在这层里全部展开,那么在转换宏里的那一个宏(如_STR)就能得到正确的宏参数。...由于头文件包含可以嵌套,C文件就有可能多次包含同一个头文件;或者不同的C文件都包含同一个头文件,编译时就可能出现重复包含(重复定义)的问题。...(3) C语言和C++语言连接结果不同,可能是在进行编译时,C++语言将全局变量默认为强符号,所以连接出错。C语言则依照是否初始化进行强弱的判断的(仅供参考)。...宏参数被完全展开后再替换入宏体,但当宏参数被字符串化(#)或与其它子串连接(##)时不予展开。在替换之后,再次扫描整个宏体(包括已替换宏参数)以进一步展开宏。
比如我们代入a+1,而当a=1时 得到的两种结果: 3//得到的表达式是:a+1*a+1 4//得到的表达式是:(a+1)*(a+1) 所以我们最保险的方法就是给整个宏都加上括号: #define SQUARE...实际上对于它们的使用也有很大的相似之处,但是它们之间的差异也是显而易见的。 宏: 预处理阶段替换:宏是在预处理阶段被替换为其定义的内容,只需要直接运算,而不是像函数那样需要先调用再运算再返回。...宏的缺陷 可能引起宏展开后的代码过长,影响可读性。 可能导致宏的滥用,使代码变得难以理解和维护。...宏无法调试,不能很好的检索错误 宏无法像函数那样递归,不能嵌套宏 宏展开可能导致意外的副作用,如参数多次计算等。...而如果我们不使用##运算符,宏参数和其他文本会被简单地拼接在一起,而不会进行连接操作。 得到的结果就是 Result: 0 #和##在实际运用中其实很少,所以只作介绍。
所以我不想为了弥补你老师犯下的错,我就不想重复了,有一点需要注意使用#include的时候包含文件的时候是不能递归包含的,例如a.h文件包含b.h,而b.h就不能再包含a.h了;还有就是重复包含(比如a.h...可能这么说并不是很能让人理解,但是大部分宏(特别是函数宏)背后都有一些自己的故事,挖掘这些故事和设计的思想会是一件很有意思的事情。...接下来条件比较得到真以后又触发了一次a++,此时a已经是2,于是b得到2,最后a再次自增后值为3。出错的根源就在于我们预想的是a++只执行一次,但是由于宏展开导致了a++被多执行了,改变了预想的逻辑。...我们先美化一下这宏,首先是最后那个__NSMIN_IMPL__内容实在是太长了。我们知道代码的话是可以插入换行而不影响含义的,宏是否也可以呢?...__FILE__返回当前文件的绝对路径,__LINE__返回展开该宏时在文件中的行数,__func__是改宏所在scope的函数名称。
可以看到 目录名称sample-trace由TRACE_SYSTEM这个宏定义,所以通过查找这个宏,就能知道有多少events的大类 每一个TRACE_EVENT都有一个自己的目录 源文件中trace_XXX...可以看到,一个trace event的定义需要涉及到起码两个头文件。 史上最长宏定义 你以为就这么简单吗?当然不是,作为有多年阅读c语言代码的老司机,看到真正的定义,我都差点没有吐出来。。。...大家如果真的想要看实际代码中展开后的代码,可以运行 make samples/trace_events/trace-events-sample.i 生成的文件是经过预处理后得到的源代码。...哪怕有了上面这个图,我想大部分人也是不会去看的。或者说,看了可能也不知道这些宏展开究竟定义了些什么?...先来看看trace_XXX这个函数的定义,它也藏在了我们刚才宏定义的展开中,这次我们仔细看一眼 ? 每次我们调用traceXXX()函数的时候,先检查key是否使能了,如果使能了才继续往下走。
正如上图所示的那样,预编译阶段的产物是单个的“.c”文件;编译阶段将这些“.c”文件一个一个彼此独立的编译为对应的对象("*.obj")文件;这些对象文件就像乐高积木一样会在最终的链接阶段按照事先约定好的图纸...学会使用"-E"选项,是检测自己缩写的宏是否正确的最有效工具。 知道这一知识有什么用呢?首先,你会明白,宏本身是与C语言的其它语法毫无关联的。宏有自己的语法,且非常简单。...实际上,有大量C语言老鸟特别喜欢在其它C语言以外的文本文件里使用“宏”(其实还有条件编译之类的),最典型的例子就是在Arm Compiler 6的scatter-script中用宏来定义一些地址常数:...---- 总的来说,“宏不属于C语言”并非空穴来风,事实上,只要你有兴趣去写脚本,包括宏在内的所有预编译语法可以在一切文本文件中使用。...(比如0~3),比如: USART_INIT(USART1_idx); 由于USART1_idx直接对应于字符串 “1”,因此,实际会被展开为: usart1_init(); 很多时候,我们可能会希望代码有更多的灵活性
CannotBeNameOfMacro:无法作为宏名称的错误。 ArgumentNotAttributes:参数不是属性时发生的错误。...这些trait的作用如下: Tracker:这个trait表示宏展开时的跟踪器。它定义了一些在展开宏时可能调用的方法,用于跟踪宏的展开过程。...宏展开过程中,可能会引入新的绑定变量,BinderInfo用于追踪这些绑定信息。 MacroState: 该结构体存储了宏展开过程中的状态信息。...等结构体用于表示宏的信息和处理展开的过程,而AstFragment、AstFragmentKind、SupportsMacroExpansion、InvocationKind和AddSemicolon等枚举类型用于描述展开的结果和行为...该函数验证属性的名称和参数是否正确,并根据属性的具体含义来决定是否接受这个属性。如果属性验证失败,编译器可能会报错并中断编译过程。
Substitution枚举表示可能的替换类型,例如字符串、整数、浮点数等。 Num枚举表示一个数字的类型,可以是无符号整数、有符号整数或者浮点数。...这些结构体用于表示宏展开的结果,提供了不同情况下的信息。 Success结构体:表示宏展开成功,其中包含了展开后的代码片段和可能出现的警告信息。...添加#[start]属性后,编译器将生成一个在程序启动时调用的函数,而不是标准的main函数。 no_main宏:当代码不需要显示的入口点时,可以使用该宏。...通常情况下,根类型是整个数据结构的入口,并且可能包含其他类型的字段。通过使用#[auto_decode_root],我们可以为根类型生成反序列化代码,并确保整个数据结构可以正确地反序列化。...HasDefaultAttrOnVariant 结构体用于检测是否有字段带有 #[default] 属性,但该字段属于一个枚举变体。它用来检测是否有非变体字段,并报告错误。
在expander.rs文件中,有一些重要的结构和枚举类型用于表示宏展开过程中的绑定和代码片段,它们在整个宏展开的过程中起着不同的作用。...卫生处理帧栈是一个包含多个HygieneFrame结构体的栈,每个帧表示一层宏展开。 HygieneFrame结构体用于表示单个宏展开帧的信息,其中包含HygieneInfo字段和其他相关字段。...hir-expand是Rust编译器的内部工具,用于展开宏并进行编译时代码分析。 在name.rs文件中,有一些结构体和枚举类型,用于表示名称和标识符的不同形式。...展开得到的代码将替换掉原有的宏调用处,从而在后续的代码分析、编辑和编译过程中,可以基于宏展开后的代码进行进一步处理和优化。...而enum部分的作用如下: PatternRefutability:表示模式的可拒绝性,即表示一个变量是否可以被拒绝匹配。
语法错误 当使用参数调用宏时,会将参数替换为宏主体,并与其他输入文件一起检查结果,以进行更多的宏调用,可以将部分来自宏主体和部分自变量的宏调用组合在一起。...所以整个宏定义的括号可防止此类问题。...* x) →(2 *(4 + y)) 当每个宏出现在另一个宏的定义中时,它们将被展开,但是当它间接出现在其自己的定义中时,则不会被展开。...(BUFSIZE)扩展为X_1024而不是X_TABLESIZE,预扫描始终会进行完整的扩展。...(ignore_second_arg行),即使有问题的代码来自第五行。
: 将所有的 #define 删除,并展开所有的宏定义,比如使用宏定义了一个常量,我们一般会这样写: //使用宏定义了一个常量 #define N 100 //使用宏 int arr[N]; 那么经过预处理之后...,#define N 100这条语句就会被删除,并且这个宏定义将会被展开,在这里就是将所有N替换成100,如下: //预处理后,宏定义语句被删除 //展开宏定义,在这里就是将N替换成100 int arr...,因为宏已经被展开,并且包含的头⽂件都被插⼊到 .i⽂件中,当我们⽆法知道宏定义或者头⽂件是否包含正确的时候,可以查看预处理后的 .i ⽂件来确认 预处理这部分内容还有许多的知识点需要我们掌握,...,而右边是结构体,就会报错 而且最关键的一点是,我们通过语义分析已经知道了代码的含义,那么把它翻译成汇编代码也不是难事了,所以在这个阶段会正在将源代码翻译成汇编代码,并做相关的优化 汇编 ...这就要涉及到链接了,我们在编译阶段会将我们的源代码翻译成机器指令,生成后缀名为.obj的目标文件,但是我们要注意的是,编译是针对于单个文件的 什么意思呢?
宏中可能嵌套的concat!宏调用。 整个实现非常复杂,因为它需要考虑到各种可能的输入和边缘情况。...AssertOne 结构体用于检查一个类型是否可以被判断为 true(即而不是 Option类型),并在不能判断为 true 时产生编译错误。...它定义了两种可能的值,包括Single和Slice。Single表示对单个参数的引用,而Slice表示对一个参数切片的引用。这些值用于指定在生成格式化函数调用时如何引用参数。...在Rust中,宏是一种元编程的工具,可以在编译时生成代码,因此其语法形式可能相对复杂。 该文件实现了log_syntax!宏,它是一个帮助开发人员调试和理解宏展开过程的辅助工具。...例如,某些属性可能是用于优化编译器的提示,而另一些属性可能是用于宏扩展等目的。 AttributeGate是一个enum,用于定义属性的激活条件。
有以下三类:宏定义,文件包含,条件编译。 宏定义(分为带参数与不带参数两种) 宏定义是用宏名代替一个字符串,也是简单的置换,不作正确性检查。...而宏只是进行简单的字符替换。 函数调用是在程序运行时处理的,为形参分配临时的内存单元。而宏展开则是编译前进行的,在展开时不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。...对函数中的实参和形参都要定义类型,二者要求一致。而宏不存在类型问题,宏名无类型。宏定义时,字符串可以是任何类型的数据。 调用函数只可得到一个返回值,而用宏定义可以设法得到几个结果。...使用宏次数多时,宏展开后源程序变长,而函数调用不会。 宏替换不占运行时间,只占编译时间。...为了尽可能地兼容,一般都遵循#define定义“可读”的常量以及一些宏语句的任务,而typedef则常用来定义关键字、冗长的类型的别名。
那怎么让他得到36呢,其实这里加个括号就可以了。...50,但结果是否会是50呢?...宏参数的展开: 宏参数在替换时会展开,这意味着如果宏参数本身是一个宏,它也会被展开(替换)。这个过程称为宏的展开或宏的宏展开。...宏展开的顺序: 当宏参数中包含其他宏时,预处理器会按照它们在宏定义中出现的顺序进行替换。如果宏A中使用了宏B,而宏B又使用了宏C,那么预处理器首先会替换宏C,然后是宏B,最后是宏A。...宏展开的深度: 宏展开的深度是有限的。如果一个宏展开后仍然是一个宏(即宏的宏),这个过程会继续,但是有一个深度限制,以避免无限循环。 宏定义的顺序: 宏定义的顺序可能会影响宏替换的结果。
多个源文件 最早的C语言仅仅用来编写小而美的代码,总共不超过100行,随着计算机软件的发展,小程序变成了大型软件工程,整个项目是由多人协同开发完成的,一个人显然已经玩不动了,这时候也就出现了模块化编程的概念...1 gcc t1.c t2.c main.c -o main 这里有几点需要注意 头文件和.c源文件放到一个文件夹下 我们自己本地的头文件,在包含时应当写英文双引号,而不是尖括号 有了头文件以后,我们的声明都可以放到头文件中...错误的使用宏函数,可能得到预期之外的结果,上例在预处理之后,被替换为如下代码,i会被加两次: 1 max = ((i++) > (j)?...(i++):(j)); 关于小括号的注意事项 1、如果宏替换列表中有运算符号,那么必须将整个替换列表放入小括号中 #define TOW_PI (2*3.14) 2、如果宏有参数,那么每个参数在替换列表中出现时...defined运算符 1#define DEBUG 2 3#if defined DEBUG 4... 5#endif 检测其后的标识符是否有定义过,若定义过则返回1,否则返回0 #ifdef和#ifndef
其次宏没有类型检查,也就不安全,容易出错且不易发现。 C++从C而来,也对C做出了一些改进。那么C++是否选择了C语言的这种采用宏的方法呢?...inline对于编译器来说只是一个建议或请求,不同的编译器堆inline的实现机制可能不同,编译器是否接受我们发出的请求也不受我们控制,而是由编译器自己决定。...,而不知道Add函数具体定义,所以编译器没有办法在main函数中调用Add函数处展开。...内联函数分离和不分离的比较; 对于内联函数前面已经知道:内联函数与其主调函数在同一源文件或内联函数在头文件中,内联函数都可以正常展开。...C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一 个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得到。
领取专属 10元无门槛券
手把手带您无忧上云