C/C++,不废话的宏使用技巧

简介: C/C++,不废话的宏使用技巧

经典废话

下面的所有内容全是我在欣赏一串代码时发出的疑问,之前对宏的了解不多,导致在刚看到下面的这串代码的时候是“地铁   老人   手机”,具体代码如下,如果有对这里解读有问题的欢迎在评论区留言。

f9596a8609644630b89d8a4811938713.png

一、预定义宏

编译一个程序涉及很多的步骤

第一个就是预处理阶段

预处理器就是在源码编译之前进行一些文本性质的操作

主要任务比如: 删除注释,插入被include 包含的头文件的内容,替换由define定义的符号,以及确认根据条件编译进行编译

Visual c + + 编译器预定义某些预处理器宏,具体取决于语言 (C 或 C + +)、 编译目标,以及选择的编译器选项。

Visual c + + 支持 ANSI/ISO C99 标准和 ISO C + + 14 标准所指定的所需预定义的预处理器宏。 该实现还支持几个更多特定于 Microsoft 的预处理器宏。 仅针对特定的生成环境或编译器选项定义一些宏,宏。 除非另有说明,宏的定义整个翻译单元如同它们指定为 /D 编译器选项参数。 在定义时,宏是由预处理器在编译前扩展为指定的值。 预定义的宏不采用任何参数,并且不能重新定义。

常见的预定义宏:

cb1631d3786246f6bcafd67074a9bef5.png

还有更多的一些看如下网址:

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语言中所讲,函数的使用可以使程序更加模块化,便于组织,而且可重复利用,但在发生函数调用时,需要保留调用函数的现场,以便子函数执行结束后能返回继续执行,同样在子函数执行完后要恢复调用函数的现场,这都需要一定的时间,如果子函数执行的操作比较多,这种转换时间开销可以忽略,但如果子函数完成的功能比较少,甚至只完成一点操作,如一个乘法语句的操作,则这部分转换开销就相对较大了,但使用带参数的宏定义就不会出现这个问题,因为它是在预处理阶段即进行了宏展开,在执行时不需要转换,即在当地执行。宏定义可完成简单的操作,但复杂的操作还是要由函数调用来完成,而且宏定义所占用的目标代码空间相对较大。所以在使用时要依据具体情况来决定是否使用宏定义。



相关文章
|
6月前
|
编译器 Linux C++
【C++ 跨平台开发 】掌握 C++ 跨平台关键宏的使用
【C++ 跨平台开发 】掌握 C++ 跨平台关键宏的使用
130 3
|
6月前
|
存储 缓存 安全
【cmake 生成配置文件】CMake与现代C++:配置文件宏的深度探索与应用
【cmake 生成配置文件】CMake与现代C++:配置文件宏的深度探索与应用
253 0
|
6月前
|
安全 编译器 C语言
【C++ 编译器 版本支持】深度解读C++ 版本以及编译器版本相关宏
【C++ 编译器 版本支持】深度解读C++ 版本以及编译器版本相关宏
125 0
|
编译器 Android开发 C++
[√]build.gradle,mk,c++预处理宏联动关系
[√]build.gradle,mk,c++预处理宏联动关系
80 0
|
C++
C++宏 #与##的区别
C++宏 #与##的区别
54 0
|
JSON C语言 数据格式
【C/C++】防止不必要的局部宏替换
如何避免和防止宏定义在不必要的位置进行替换
224 0
|
存储 安全 编译器
【为什么】C++中的宏
【为什么】C++中的宏
130 0
|
存储 自然语言处理 编译器
C/C++:程序环境和预处理/宏
简单讲解了宏定义、预处理、条件编译等等
C/C++:程序环境和预处理/宏
|
C++
C++ 你会使用cmath库里的宏常量吗?(π、e、ln2、√2、(2/√π) 等等)
C++ 你会使用cmath库里的宏常量吗?(π、e、ln2、√2、(2/√π) 等等)
268 0