C/C++宏基本概念

468 阅读5分钟

标题: C/C++宏基本概念

作者: 边城量子 ( shihezichen@live.cn )


介绍C/C++中宏的基本概念, 包括宏的作用,以及现代C++中对宏的建议和替代方案.

写在最前面的

基本原则: 在现代C++编程中, 应尽可能避免使用宏

基本概念

  • 宏是预处理指令

    • 守护头文件
    #ifndef MYPROG_X_H
    #define MYPROG_X_H
    
    #endif
    
    • 调试的预处理信息
    // 预定义的标准宏,例如__FILE__、__LINE__、__DATE__和__TIME__
    
    • #pragma的使用,如 禁止掉某些警告信息
    #pragma warning( push )
    #pragma warning( disable : 4705 )
    #pragma warning( disable : 4706 )
    #pragma warning( error : 164 )// 把164号警告作为错误报出
    // Some code
    #pragma warning( pop )
    
  • 宏的本质是简单的字符串替换,预处理时进行宏替换;

    一个宏陷阱:

//max宏陷阱
#define max(a,b) (a < b) ? b : a

int x = 42;
int y = 43;
int z = max(++x, ++y);
// int z = (++x < ++y) ? ++y : ++x;

  • 可在定义宏时要求它接收参数,宏替换时会代入参数;

  • 宏的名字不允许重载;

  • 宏预处理代码没有能力处理递归调用

  • 宏没有地址

    能得到任何自由函数或成员函数的指针,但不可能得到一个宏的指针,因为宏没有地址。宏之所以没有地址,原因很显然: 是代码,宏不会以自身的形势存在,因为它是一种文本替换规则。

  • 宏有碍调试

    在编译器看到代码之前,宏就会修改相应的代码,因而,他会严重改变变量名称和其他名称;此外,在调试阶段,无法跟踪到宏的内部。

宏的主要功能?

1. 用于连接两个C++特性

有时需要在库中实现某个操作并将其注入到应用程序的类层次结构中。

可惜,c++没有这样的直接机制。也有使用虚拟继承的变通方法,但虚函数的调用具有一定的性能开销。

我们可以定义一个宏,BaseClass 层次结构中的每个类在类定义中使用该宏

例如Eigen库中经常使用的宏 ALIGNED_OPERATOR_NEW 实现内存对齐:

#define ALIGNED_OPERATOR_NEW 
void* operator new (std::size_t count) { 
  void* original = ::operator new(count + 32); 
  void* aligned = reinterpret_cast<void*>((reinterpret_cast<size_t>(original) & ~size_t(32 - 1)) + 32); 
  *(reinterpret_cast<void**>(aligned) - 1) = original; 
  return aligned;
} 
void operator delete (void* ptr) { 
  ::operator delete(*(reinterpret_cast<void**>(ptr) - 1)); 
}

使用时:

class BaseClass { 
    public: BaseClass(Vector4d position) : position(position) { } 
    
    ALIGNED_OPERATOR_NEW 
    
    Vector4d position; 
};
2. 利用宏来减少冗余

在写代码时,如果一段相同的代码需要键入多次,那么使用宏可以减少这种冗余.

在 Fast-LIO 中, 生成 input_ikfomstate_ikfomprocess_ikfom 类的定义的的代码绝大多数是相似的, 因此可以使用宏 MTK_BUILD_MANIFOLD 来生成这三个类的定义:

MTK_BUILD_MANIFOLD(input_ikfom,
  ((vect3, acc))
  ((vect3, gyro))
  );
  
MTK_BUILD_MANIFOLD(state_ikfom,
  ((vect3, pos))
  ((SO3, rot))
  ((SO3, offset_R_L_I))
  ((vect3, offset_T_L_I))
  ((vect3, vel))
  ((vect3, bg))
  ((vect3, ba))
  ((S2, grav))
  );   
  
  MTK_BUILD_MANIFOLD(process_noise_ikfom,
  ((vect3, ng))
  ((vect3, na))
  ((vect3, nbg))
  ((vect3, nba))
  );

宏的功能很多, 但现代C++并不提倡大量使用宏, 对宏的各项功能也有一些替换的方案, 在下一章节详细展开描述.

宏 : 现代C++的建议

  • 宏不是类型安全的,这严重破坏了C++的强类型特征;
  • 宏不会出现在编译器生成的中间源代码中(它在编译初期预处理阶段就已经被替换掉了),因此难以调试;
  • 宏虽然简便,但大型宏非常难以管理(虽然可以用\扩展到下一行);
  • 宏是C风格,不是C++风格;
1. 常量定义:

C++建议: 用 constexpr/const 来定义常量.

《EffectiveC++》的第一个条款,讨论的就是这个宏。由于宏是预编译程序来处理,所以NUM这个名字不会加入到符号表中,如果出现编译错误时,提示信息中就不会出现 NUM或SIZE,而是100,为排除错误增加了额外的障碍。


// 用 `constexpr` 表达式替代常量宏定义
#define SIZE 10 // C-style 
constexpr int SIZE = 10; // modern C++

#define NUM 100
const NUM = 100;

注: const常量放在头文件中,也不必担心存在多个实例的问题,对于const修饰的变量,编译器一般也会对其进行优化,不会出现多重定义的问题。

2. 函数定义

C++建议: 使用inline函数代替函数定义宏

由于宏只是在代码中做字符串替代展开,所以,用宏定义的函数,实际上并没有减少代码的体积。

另外,还有一些天然的缺陷,假设一个求平方的函数宏:

  #definesquare(x) (x*x)
  
  voidf(double d, int i)
  {
      square(d); //OK
      square(i++); //糟糕, (i++*i++)
      square(d+1); //更糟,(d+1*d+1)
  }

纵然可以把参数加上括号来解决,#define square(x) ((x)*(x)),但i++被执行两次这个问题还是无法解决.

inline函数:

  template<classT>
  inline T square(T& value)
  {
      return value*value;
  }

inline函数具有函数的性质,参数传递不管是传值还是传引用,都不会对参数进行重复计算;同时会对参数做类型检查,保证代码的正确性;inline函数也是在代码中做代码展开,效率上并不比宏逊色。

3. 类型重定义

C++建议: 使用 using 或 typedef 代替类型重定义宏, 优先using

#define DWORD unsigned int    // C-style 

using DWORD = unsigned int;   // modern C++
typedef unsigned int DWORD;   
4. 条件编译

C++建议: 使用 template 技术来代替条件编译宏

    #ifdefSystemA
    testA();
    #else  //SystemB
    testB();
    #endif

这种条件编译宏,一般在不同的产品或平台使用同一套代码的情况,大量出现。定义了SystemA的时候,SystemB的代码是不编译的,也就意味着你的代码没有时刻处于编译器的监控中。

可以使用template技术来解决。

constint SystemA = 1;
constint SystemB = 2;
 
template<int T>
void test() {}

//定义不同的系统的特化版本
template<> void test<SystemA>(){ //SystemA的实现 }
template<> void test<SystemB>(){ //SystemB的实现 }

这样,不同的系统使用自己的模板即可,别人的代码也会同时接受编译器的检查,不至于出现遗漏编译错误的情况。

5. 头文件包含

C++建议: 使用 #pragma once 代替头文件包含宏

#ifndef TEST_H
#define TEST_H
    //test.h的实现
#endif

为了防止头文件重复包含. C++中没有完全替换 #ifndef方案, 如果只限于同一个文件不会被包含多次场景, 且编译器版本足够高, 也可以考虑 #pragma once。依赖于编译器实现, 较早版本的编译器(如 gcc 3.4及之前)则不支持,如下:

#pragma once
6. 字符串化与字符串拼接
  • #:构串操作符 构串操作符 # 只能修饰带参数的宏的形参,它将实参的字符序列(而不是实参代表的值)转化为字符串常量; 简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号
#include <stdio.h>

#define STRING(s)	#s
#define TEXT(s)		"class"#s"info"

int main()
{
    int integer = 999;
    printf(STRING(integer)"\n");
    printf(TEXT(integer)"\n");

    return 0;
}

输出:

integer 
classintegerinfo 
  • ##:合并操作符 合并操作符将出现在其左右的字符序列合并成一个新的标识符

注意: 使用合并操作符 ## 时,自身的标识符必须预先有定义,否则编译器会报“未定义标识符”错误, 字符序列合并之后是标识符,不是字符串

#include <iostream>
using namespace std;

#define CLASS_NAME(name)	class##name
#define MERGE(x, y, z)		x##y##z

int main()
{
    int classname = 10;
    int aaabbbccc = 20;

    cout << "classname = " << CLASS_NAME(name) << endl;
    cout << "aaabbbccc = " << MERGE(aaa, bbb, ccc) << endl;

    return 0;
}

输出:

classname = 10 
aaabbbccc = 20