经典废话
下面的所有内容全是我在欣赏一串代码时发出的疑问,之前对宏的了解不多,导致在刚看到下面的这串代码的时候是“地铁 老人 手机”,具体代码如下,如果有对这里解读有问题的欢迎在评论区留言。
一、预定义宏
编译一个程序涉及很多的步骤
第一个就是预处理阶段
预处理器就是在源码编译之前进行一些文本性质的操作
主要任务比如: 删除注释,插入被include 包含的头文件的内容,替换由define定义的符号,以及确认根据条件编译进行编译
Visual c + + 编译器预定义某些预处理器宏,具体取决于语言 (C 或 C + +)、 编译目标,以及选择的编译器选项。
Visual c + + 支持 ANSI/ISO C99 标准和 ISO C + + 14 标准所指定的所需预定义的预处理器宏。 该实现还支持几个更多特定于 Microsoft 的预处理器宏。 仅针对特定的生成环境或编译器选项定义一些宏,宏。 除非另有说明,宏的定义整个翻译单元如同它们指定为 /D 编译器选项参数。 在定义时,宏是由预处理器在编译前扩展为指定的值。 预定义的宏不采用任何参数,并且不能重新定义。
常见的预定义宏:
还有更多的一些看如下网址:
https://learn.microsoft.com/zh-cn/cpp/preprocessor/predefined-macros?view=msvc-170
在顶部的代码里,由于不同环境下的语言可能有所不同,为了统一书写,需要根据不同版本的特性来做不同的调整,而这就是通过每个版本特殊的预定义宏来实现的。
二、decltype(x)
以下参考至http://c.biancheng.net/view/7151.html
什么是decltype
decltype 是 C++11 新增的一个关键字,它和 auto 的功能一样,都用来在编译时期进行自动类型推导。
decltype 是“declare type”的缩写,译为“声明类型”。
既然已经有了 auto 关键字,为什么还需要 decltype 关键字呢?因为 auto 并不适用于所有的自动类型推导场景,在某些特殊情况下 auto 用起来非常不方便,甚至压根无法使用,所以 decltype 关键字也被引入到 C++11 中。
auto 和 decltype 关键字都可以自动推导出变量的类型,但它们的用法是有区别的:
1. auto varname = value; 2. decltype(exp) varname = value;
其中,varname 表示变量名,value 表示赋给变量的值,exp 表示一个表达式。
auto 根据=
右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟=
右边的 value 没有关系。
另外,auto 要求变量必须初始化,而 decltype 不要求。这很容易理解,auto 是根据变量的初始值来推导出变量类型的,如果不初始化,变量的类型也就无法推导了。decltype 可以写成下面的形式:
decltype(exp) varname;
auto 的语法格式比 decltype 简单,所以在一般的类型推导中,使用 auto 比使用 decltype 更加方便.
我们知道,auto 只能用于类的静态成员,不能用于类的非静态成员(普通成员),如果我们想推导非静态成员的类型,这个时候就必须使用 decltype 了。
exp 注意事项
原则上讲,exp 就是一个普通的表达式,它可以是任意复杂的形式,但是我们必须要保证 exp 的结果是有类型的,不能是 void;例如,当 exp 调用一个返回值类型为 void 的函数时,exp 的结果也是 void 类型,此时就会导致编译错误。
C++ decltype 用法举例:
1. int a = 0; 2. decltype(a) b = 1; //b 被推导成了 int 3. decltype(10.8) x = 5.5; //x 被推导成了 double 4. decltype(x + 100) y; //y 被推导成了 double
实际应用
下面有一个模板定义
1. #include <vector> 2. using namespace std; 3. template <typename T> 4. class Base { 5. public: 6. void func(T& container) { 7. m_it = container.begin(); 8. } 9. private: 10. typename T::iterator m_it; //注意这里 11. }; 12. int main() 13. { 14. const vector<int> v; 15. Base<const vector<int>> obj; 16. obj.func(v); 17. return 0; 18. }
单独看 Base 类中 m_it 成员的定义,很难看出会有什么错误,但在使用 Base 类的时候,如果传入一个 const 类型的容器,编译器马上就会弹出一大堆错误信息。原因就在于,T::iterator
并不能包括所有的迭代器类型,当 T 是一个 const 容器时,应当使用 const_iterator。
要想解决这个问题,在之前的 C++98/03 版本下只能想办法把 const 类型的容器用模板特化单独处理,增加了不少工作量,看起来也非常晦涩。但是有了 C++11 的 decltype 关键字,就可以直接这样写:
1. template <typename T> 2. class Base { 3. public: 4. void func(T& container) { 5. m_it = container.begin(); 6. } 7. private: 8. decltype(T().begin()) m_it; //注意这里 9. };
看起来是不是很清爽?
注意,有些低版本的编译器不支持T().begin()这种写法,以上代码在 VS2019 下可以通过,在 VS2015 下失败。
decltype(x) 和decltype((x))的不同之处
以下参考至文章 decltype(x) 和decltype((x))的不同
两者同为左值
如果“是否保留引用”是按照“左值性”来区分的话,那么 int x; 这样的定义, decltype(x) 也要返回 int& 了。这显然会造成很大的困扰, decltype 做函数的返回值就很容易悬垂引用。所以 decltype 的考虑是:参数如果是变量名,就返回其声明的类型;而参数是表达式,再根据表达式的值类型来判断是否保留引用。 decltype(x) 是变量名的规则,而 decltype((x)) 是表达式的规则,井水不犯河水。
实际上,标准并没有把 (x) 单独出来,而是把 (x) 合进去了。
在 N1607 [Decltype and auto (revision 3)] 中,没有提到 decltype((e)) 这种情况。而在 N1705 [Decltype and auto (revision 4)] 中,第一条改动就指出了:
Following is the list of changes from proposal N1607.* The decltype rules now explicitly state that decltype((e)) == decltype(e) (as suggested by EWG).
于是,到了 N1978 [Decltype (revision 5)] 中, decltype ( expression ) 完整的说明是这样的:
1. If e is of the form (e1), decltype(e) is defined as decltype(e1).2. If e is a name of a variable or non-overloaded function, decltype(e) is defined as the type used in the declaration of that variable or function. If e is a name of an overloaded function, the program is ill-formed.3. If e is an invocation of a user-defined function or operator, decltype(e) is the return type of that function or operation.4. Otherwise, where T is the type of e, if T is void or e is an rvalue, decltype(e) is defined as T, otherwise decltype(e) is defined as T&.
可以看到,revision 3 中的 decltype((e)) == decltype(e) 作为第 4 条规则的特例,有排面地成为了 decltype 说明的第 1 条。
然而到了 N2115 [Decltype (revision 6)] , decltype ( expression ) 的说明就变了,并且认真考虑了括号的问题:
1. If e is an id-expression or a class member access (5.2.5 [expr.ref]), decltype(e) is defined as the type of the entity named by e. If there is no such entity, or e names a set of overloaded functions, the program is ill-formed.2. If e is a function call (5.2.2 [expr.call]) or an invocation of an overloaded operator (parentheses around e are ignored), decltype(e) is defined as the return type of that function.3. Otherwise, where T is the type of e, if e is an lvalue, decltype(e) is defined as T&, otherwise decltype(e) is defined as T.
到了这儿, decltype((e)) 的定义已经被包含在了第 3 条规则中。并且 N2115 [Decltype (revision 6)] 特意举例:
Note that parentheses matter:int a;decltype(a) // intdecltype((a)) // int&
综上, (x) 本来就是一个左值表达式, decltype 就应该返回引用。然而有人考虑到 decltype((x)) 的特殊性,强行让他和 decltype(x) 挂钩,给它整了一条去掉括号的特殊规则,不过最后又整回去了。大概是这样一个故事。
其实早期关于 decltype 的提出、讨论、到最后确定下来,有很多都是变来变去的。说白了, decltype 就是在“在哪些情况下保留引用”做权衡。无非就是人为规定而已,没有为什么,也不一定合理。
三、宏define的各种用法
参考至文章 https://zhuanlan.zhihu.com/p/367761694
1. #define命令
#define命令是C语言中的一个宏定义命令,它用来讲一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。该命令有两种格式:一种是简单的宏定义(不带参数的宏定义),另一种是带参数的宏定义。
(1) 简单的宏定义
格式:#define <宏名/标识符> <字符串>
例子:define PI 3.1415926
说明:
①宏名一般用大写
②宏定义末尾不加分好;
③可以用#undef命令终止宏定义的作用域
④宏定义可以嵌套
⑤字符串“”中永远不包含宏
⑥宏替换在编译前进行,不分配内存,变量定义分配内存,函数调用在编译后程序运行时进行,并且分配内存
⑦预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查
⑧使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。例如:数组大小常用宏定义
(2) 带参数的宏定义(除了一般的字符串替换,还要做参数代换)
格式:#define <宏名>(<参数表>) <字符串>
例子:#define S(a,b) a*b
area=S(3,2);
第一步被换为area=a*b;第二步换为area=3*2;
一个标识符被宏定义后,该标识符便是一个宏名。这时,在程序中出现的是宏名,在该程序被编译前,先将宏名用被定义的字符串替换,这称为宏替换,替换后才进行编译,宏替换是简单的替换。
说明:
①实参如果是表达式容易出问题
#define S(r) r*r
area=S(a+b);第一步换为area=r*r;第二步换成area=a+b*a+b;
当定义为#define S(r)((r)*(r))时area=((a+b)*(a+b))
②宏名和参数的括号间不能有空格
③宏替换之作替换不做计算,不做表达式求解
④宏的哑实结合不存在类型,也没有类型转换
⑤宏展开不占用运行时间,只占用编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)
2. 宏定义易错点示例总结
(1)“”内的东西不会被宏替换
#define NAMEzhang
程序中有"NAME"则,它会不会被替换呢?
答:否
(2)宏定义前面的那个必须是合法的用户标识符(可以使关键字)
#define 0x abcd
可以吗?也就是说,可不可以用把标识符的字母替换成别的东西?
答:否
(3)第二位置如果有字符串,必须“”配对
#define NAME "zhang
这个可以吗?
答:否
(4)只替换与第一位置完全相同的标识符
#define NAME "zhangyuncong"
程序中有上面的宏定义,并且,程序里有句:NAMELIST这样,会 不会被替换成"zhangyuncong"LIST
答:否
(5)带参数宏的一般用法
例如:
①#define MAX(a,b) ((a)>(b)?(a):(b))
则遇到MAX(1+2,value)则会把它替换成:
((1+2)>(value)?(1+2):(value))
②#define FUN(a) "a"
则,输入FUN(345)会被替换成什么?
其实,如果这么写,无论宏的实参是什么,都不会影响其被替换成 "a"。也就是说,""内的字符不被当成形参,即使它和一模一样。
③#define N 2+2
1. #define N 2+2 2. 3. void main() 4. { 5. int a=N*N; 6. printf(“%d”,a); 7. }
问题解析:如1节所述,宏展开是在预处理阶段完成的,这个阶段把替换文本只是看作一个字符串,并不会有任何的计算发生,在展开时是在宏N出现的地方 只是简单地使用串2+2来代替N,并不会增添任何的符号,所以对该程序展开后的结果是a=2+2*2+2,计算后=8。
④多行宏定义
#define doit (m,n) for(inti=0;i<(n);++i) { m+=i; }
3. 其他宏定义
#define Conn(x,y) x##y
#define ToChar(x) #@x
#define ToString(x) #x
x##y表示什么?表示x连接y,举例说:
int n = Conn(123,456); 结果就是n=123456;
char* str = Conn("asdf","adf")结果就是 str = "asdfadf";
#@x,其实就是给x加上单引号,结果返回是一个constchar,举例说:
char a = ToChar(1);结果就是a='1';
做个越界试验char a = ToChar(123);结果是a='3';
但是如果你的参数超过四个字符,编译器就给给你报错了!error C2015:too many characters in constant :P
#x是给x加双引号
char* str = ToString(123132);就成了str="123132";
如果有#define FUN(a,b) vo##a##b()那么FUN(idma,in)会被替换成void main()
4、宏定义其他概念
① 预处理功能:
(1)文件包含:可以把源程序中的#define扩展为文件正文,即把包含的.h文件找到并展开到#include所在处。
(2)条件编译:预处理器根据#if和#ifdef等编译命令及其后的条件,把源程序中的某些部分包含进来或排除在外,通常把排除在外的语句转换成空行。
(3)宏展开:预处理器将源程序文件中出现的对宏的引用展开成相应的宏定义,经过预处理器处理的源程序与之前的源程序有所不同,在这个阶段所进行的工作只是纯粹的替换和展开,没有任何计算功能。
②使用带参数的宏定义可完成函数调用的功能,又能减少系统开销,提高运行效率。
正如C语言中所讲,函数的使用可以使程序更加模块化,便于组织,而且可重复利用,但在发生函数调用时,需要保留调用函数的现场,以便子函数执行结束后能返回继续执行,同样在子函数执行完后要恢复调用函数的现场,这都需要一定的时间,如果子函数执行的操作比较多,这种转换时间开销可以忽略,但如果子函数完成的功能比较少,甚至只完成一点操作,如一个乘法语句的操作,则这部分转换开销就相对较大了,但使用带参数的宏定义就不会出现这个问题,因为它是在预处理阶段即进行了宏展开,在执行时不需要转换,即在当地执行。宏定义可完成简单的操作,但复杂的操作还是要由函数调用来完成,而且宏定义所占用的目标代码空间相对较大。所以在使用时要依据具体情况来决定是否使用宏定义。