一文速通C++中宏的现代用法

一文带你速通宏的现代用法!从基本的定义/取消宏,逻辑控制开始,我们会逐步介绍如何通过预定义宏实现静态反射、如何拼接字符串、可变参数、如何保证头文件安全、良好的使用习惯等初看起来十分神奇的用法。

宏定义&取消#define #undef

#defined #undef

如果要查看多个宏是否定义过,可使用下面的预处理指令

  • #define 定义宏
// 定义一个宏,其内容为空
//#if (MACRO) 报错
#define PXLW_IRIS
// 定义一个宏,其内容不为空(允许 bool和int)
// #if (MACRO) 为 true
#define PXLW_IRIS_VAL_TRUE		7
#define PXLW_IRIS_BOOL_TRUE		true
// #if (MACRO) 为 false
#define PXLW_IRIS_VAL_FALSE		0
#define PXLW_IRIS_BOOL_FALSE	false
  • #undef 取消宏
#define MAX 200
printf("MAX = %d\n", MAX);
#undef MAX

逻辑控制

通过和预处理指令配合,达到一定的代码开关控制,常见的比如在跨平台开发时,对不同的操作系统启用不同的代码。

if else ifdef全家桶

#ifdef MACRO		// MACRO宏定义过则满足条件

#if (condition)
#elif (condition)
#else
#endif

#if 后面的 condition 是否为真了。

#ifdef _WIN32 // 查看是否定义了该宏,Windows默认会定义该宏
    // 如果是Windows系统则会编译此段代码
    OutputDebugString("this is a Windows log");
#else
    // 如果是mac,则会编译此段代码
    NSLog(@"this is a mac log");
#endif

condition

可以被if判断的condition只能是 整数 / bool量,定义为其他类型会编译错误。

#define MACRO true
if (std::is_same<bool, decltype(MACRO)>::value) {
    std::cout << "MACRO can be bool" << std::endl;
} else if (std::is_same<int, decltype(MACRO)>::value) {
    std::cout << "MACRO bool is fake, trans to int" << std::endl;
} else {
    std::cout << "MACRO bool is fake, i dont know what it is" << std::endl;
}

# MACRO can be bool
字面量

#if 为真

  1. 非 0 整数:-100-11
  2. 布尔量:true

为假

  1. 整数 0:0
  2. 布尔量:false
// 真
#if true
#elif 1
// 假
#elif false
#elif 0
#endif
简单的表达式
整数运算

支持对整数进行的简单运算

>===<=<>

#define PXLW_CHIP_VERSION 7

#if (PXLW_CHIP_VERSION > 7)
#elif (PXLW_CHIP_VERSION < 7)
#elif (PXLW_CHIP_VERSION == 7)
#endif
与或非
含义符号
&&
`
! not
#if (condition_and_1) && (condition_and_2)
#elif (condition_or_1) || (condition_or_2)
#elif ! (condition_not_1)
#elif not (condition_not_2)
#else 
#endif
#if defined(_WIN32) || defined(WIN32)
    // 如果是Windows系统则会编译此段代码
    OutputDebugString("this is a Windows log");
#elif !defined(WIN32) && !defined(__WATCOMC__) && \
	!defined(__VXWORKS__)
	OutputDebugString("this is unkown log");
#endif
宏的内容
#define MACRO 1

#if (MACRO)

#elif not (MACRO)

#elif ! (MACRO)

#endif
宏的存在 #ifdef defined

宏定义过就满足条件

  • #ifdef
#ifdef _WIN32 // 查看是否定义了该宏,Windows默认会定义该宏
    // 如果是Windows系统则会编译此段代码
    OutputDebugString("this is a Windows log");
#else
    // 如果是mac,则会编译此段代码
    NSLog(@"this is a mac log");
#endif
  • defined 判断是否定义过
// 定义过
#if defined(IRIS_MTK_SPECIFIC)
#elif defined MACRO
// 没定义过
#elif not defined(IRIS_MTK_SPECIFIC)

#elif ! defined(IRIS_MTK_SPECIFIC)

#endif  // IRIS_MTK_SPECIFIC

用途举例

控制编译

头文件控制

#if defined (PXLW_IRIS)

#if defined (IRIS_VERSION_7)
#include "iris7.h"
constexpr int iris_num = 7;
#elif defined (IRIS_VERSION_8)
#include "iris8.h"
constexpr int iris_num = 7;
#elif defined (IRIS_VERSION_NONE)
#include "iris_not_exists.h"
constexpr int iris_num = ABCDEFG;
#else 
#include "qcom.h"
#endif


控制打印

只在debug版本打印

把宏的定义改成#define ENABLE_LOG 0,那么就不会满足条件了,也就不会打印日志了。

#if NDEBUG
#define CHECK_GL(x) do { \
	(x); \
} while(0)
#else
#define CHECK_GL(x) do { \
	(x); \
	check_gl_error(__FILE__, __LINE__, #x); \
} while(0)
#endif

#转字符串 ##拼接(展开)

#define STR(x) #x   //转字符串

//二级
#define CONTACT2(x,y) CONTACT(x,y)
#define STR2(x) STR(x) 

# 转字符串

#符号:把宏参数,转换为字符串

#define MESSAGE(x) #x
const char * str = MESSAGE(test);
// const char * str = "test";

## 拼接(展开)

基本使用

##符号:连接两边的值,从而产生一个新的值

#define CONTACT(x, y) x##y   //拼接
#define CONTACT2(x,y) CONTACT(x,y)  // 对宏进行二次展开


#define MARCO_1front(index) 1##index
int mInt_2 = MARCO_1front(23);	// int mInt_2 = 123;

#define MARCO_1end(index) index##1
int mInt_3 = MARCO_1end(23);   // int mInt_3 = 231;
## 拼接规则(默认不展开)

这一过程中,左右两边既可以是 输入的参数、常量(字面量)但不可以是参数之外其它的宏(否则会不展开)

  • ##两侧,只做参数的替换,不进行任何宏的展开

    • 非参数的宏:
      • 不展开
    • 参数:
      • 仅仅会对参数进行替换,不展开
  • 不在 ##两侧,所有的宏都会展开到最终形态(和平时一样)。

    • 非参数的宏:

      • 会多次展开,直到成为最终的样子。

        (宏展开成为新的宏,新的宏会继续展开)

    • 参数:

      • 会对参数进行替换

      • 若替换后仍然是宏,会多次展开,直到成为最终的样子。

// ## 两侧只做参数的替换,不进行任何宏的展开

// 示例:不展开
//  既没有把非参数的INT_当作宏进行展开
//  又没有把参数的ONE当作宏进行展开
#define INT_   pyzhang_
#define ONE		1
#define MARCO_1(index) INT_##index
int MARCO_1(ONE);	// 对应int INT_ONE; 而不是 int pyzhang_1;

#define MARCO_1front(index) 1##index
int mInt_2 = MARCO_1front(23);	// int mInt_2 = 123;

#define MARCO_1end(index) index##1
int mInt_3 = MARCO_1end(23);   // int mInt_3 = 231;

## 两侧二次展开
#define CONTACT(x, y) x##y	// 简单拼接
#define CONTACT2(x,y) CONTACT(x,y)  // 宏的二次展开

如果需要对 ## 两侧的宏进行展开,需要使用二级的宏。

#define THREE 999
#define TWO THREE
#define ONE TWO

/* 不使用二级的宏 */
#define MARCO_1(param) ONE##param
//  ## 两边的宏(/参数),是不展开的
MARCO_1(ONE);  // ONE##ONE -> ONEONE 
/* 使用二级的宏 */
#define CONTACT(x, y) x##y	// 简单拼接
#define CONTACT2(x,y) CONTACT(x,y)  // 宏的二次展开

#define THREE 999
#define TWO THREE
#define ONE TWO

#define MARCO_2(param) CONTACT2(ONE, param)

// 参数展开后传到二级宏,##两侧已经是展开过的宏了
MARCO_2(ONE);  // CONTACT2(ONE, ONE) -> CONTACT(999, 999) -> 999##999 -> 999999
示例

下面是一些示例:

#define IRIS_ATTR(_name, _mode, _show, _store) \
struct kobj_attribute iris_attr_##_name = __ATTR(_name, _mode, _show, _store)

// 则
static IRIS_ATTR(chip_version, 0644, chip_version_show, chip_version_store);
// 被展开为
struct kobj_attribute iris_attr_chip_version = __ATTR(_name, _mode, _show, _store) // 这里没有展开__ATTR这个宏,虽然不是参数,但是不在##两侧,实际上会继续展开
#define CONCAT(a, b, c) a##b##c
#define MESSAGE(x) #x

int main() {
    printf("%s\\\\n", MESSAGE(CONCAT(Hello, World, !)));
 // printf("%s\\\\n", "HelloWorld!");
    return 0;
}

switch case中使用

打印枚举字符串
// 以数字的形式进行打印(展开)
#define PER_GL_ERROR(x) case (x); return (x)

// 以字符串的形式对枚举的名称进行打印(展开)
#define PER_GL_ERROR(x) case (x); return (#x)
// 如果x本身就是一个宏,可以先求值x,变成数值后再打印
#define STR(x) (#x)
#define PER_GL_ERROR(x) case (x); return STR(x)
std::string get_errno_name(int err) {
    switch(err) {
    PER_GL_ERROR(GL_NO_ERROR);
    PER_GL_ERROR(GL_INVALID_ENUM);
    PER_GL_ERROR(GL_INVALID_VALUE);
    }
    return "unknown error: " + std::tostring(err);
}

希望不再重复加前缀

// 省略前缀
#define PER_GL_ERROR(x) case GL_##x; return (x)
#define PER_GL_ERROR(x) case GL_##x; return (#x)
std::string get_errno_name(int err) {

#define PER_GL_ERROR(x) case GL_##x; return (#x)
    switch(err) {
    PER_GL_ERROR(NO_ERROR);		// GL_NO_ERROR
    PER_GL_ERROR(INVALID_ENUM); // GL_INVALID_ENUM
    PER_GL_ERROR(INVALID_VALUE);// GL_INVALID_VALUE
    }
    return "unknown error: " + std::tostring(err);
}

延迟宏的求值,避免因为宏的二次指向出现问题。

std::string get_errno_name(int err) {
#define CONCAT(x, y) x##y	// 延迟宏的求值,避免因为宏的二次指向出现问题。
#define PER_GL_ERROR(x) case GL_##x; return (#x)
    switch(err) {
    PER_GL_ERROR(NO_ERROR);		// GL_NO_ERROR
    PER_GL_ERROR(INVALID_ENUM); // GL_INVALID_ENUM
	#define newMacro INVALID_VALUE
    PER_GL_ERROR(newMacro);		// GL_INVALID_VALUE
    }
    return "unknown error: " + std::tostring(err);
}
更多信息
std::string get_errno_name(int err) {
#define CONCAT(x, y) x##y	// 延迟宏的求值,避免因为宏的二次指向出现问题。
#define PER_GL_ERROR(x) case GL_##x; return (#x)
    switch(err) {
    PER_GL_ERROR(NO_ERROR);		// GL_NO_ERROR
    PER_GL_ERROR(INVALID_ENUM); // GL_INVALID_ENUM
	#define newMacro INVALID_VALUE
    PER_GL_ERROR(newMacro);		// GL_INVALID_VALUE
    }
    return "unknown error: " + std::tostring(err);
}
#define CHECK_GL(x) do \
{\
    get_errno_name(#x);\
    check_gl_error(__FILE__, __LINE__, #x);\
} while (0)

void check_gl_error(const char* filename, int lineno, const char *expr) {
    auto err = glGetError();
    if (err != 0) {
        cout << filename << lineno << expr << "faild" << get_errno_name(err) << endl;
    }
}

... 可变参数

这个特性可以用来对类似printf这样的函数进行封装。

基本使用

...传递给 __VA_ARGS__

// __VA_ARGS__ 接收 可变参数...
// ...传递给 __VA_ARGS__, 调用JNI的内置函数__android_log_print,打印
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGD(...) cout << __VA_ARGS__ << endl

,问题

general

在调用宏时,如果可变参数为空,之前的逗号如果不会删除,导致编译错误,

// 错误
#define LOG(format, ...) printf(format, __VA_ARGS__)

LOG("got a number %d", 34); ==>  printf("got a number", 34); // 有参数不报错
trace("got a number");      ==>  printf("got a number",); // 无参数则错误

正确做法见如下章节,简单来说

#define LOG(format, ...) 	printf(format, ##__VA_ARGS__)
#define PRINT(x, ...) 		printf(format __VA_OPT__(,) __VA_ARGS__)
##__VA_ARGS__

##__VA_ARGS__ 是 C/C++ 语言中的宏定义技巧:用于支持可变长参数

  • 忽略某个可变参数为空时产生的逗号(将逗号去掉,)从而避免编译错误
  • __VA_ARGS__之前添加##符号,使预处理器去除掉它前面的那个逗号。
// 正确:解决多参数输入的,无参数输入时的 `,` 问题:
#define LOG(format, ...) printf(format, ##__VA_ARGS__)
// 这样我们就可以使用我们自己定义的宏 trace 来打印日志了
LOG("got a number %d", 34); ==>  printf("got a number", 34);
LOG("got a number %d"); ==>  printf("got a number");
__VA_OPT__(,)

C++20的标准库中,支持了这个宏

__VA_ARGS__不为空,之中的内容才会显现出来。

#define PRINT(x, ...) do { \
	printf(x __VA_OPT__(,) __VA_ARGS__); \
} while(0)
PRINT("answer is \n"); // 错误,必须有参数
PRINT("answer is %d\n", 98);
PRINT("answer is %d, %d\n", 98, 100);

预定义宏

常用 __FILE__ etc

除了使用#define预处理器来定义宏之外,也可以通过编译器参数来定义宏,

__FILE__const char*,当前所编译文件名称(绝对路径)

__FILE_NAME__const char*,当前所编译文件名称(仅仅是文件名,如test.cpp

__LINE__int,当前所在的行号

__FUNCTION__const char*,当前所在函数的名称

__DATE__const char*,当前的日期

__TIME__const char*,当前的时间

平台

const char* system_name() {
    return;
#if defined(__linux__)
        "linux"
#elif defined(__WIN32)
        "windows"
#elif defined(__APPLE__)
        "windows"
#elif defined(__unix__)
        "otherunix"
#else
        "unknown"
#endif
        "unknown"
;
}
const char* system_name() {
    return;
#if defined(__x86_64__)
        "x86_64"
#elif defined(__i386)
        "i386"
#elif defined(__aarch64__)
        "aarch64"
#elif defined(__arm__)
        "arm"
#else
        "unknown"
#endif
;
}

判断编译器

#if defined(_MSC_VER) // MSVC
#define _LIBCXX_UNREACHABLE() __assume(0)
#elif defined(__clang__)  // Clang
#define _LIBCXX_UNREACHABLE() __builtin_unreachable()
#elif defined(__GNUC__)   // GCC
#define _LIBCXX_UNREACHABLE() __builtin_unreachable()
#else
#define _LIBCXX_UNREACHABLE() do {} while (1)
#endif

判断跨平台

面对不跨平台的宏

// 禁止内联优化  gnuc 
__attribute__((noinline)) void func(){}
// 禁止内联优化  微软
__declspec(noinline) void func(){}
#if defined(__MY_MAC)
__attribute__((noinline))
#elif defined(__MAC2) || defined(__clang__)
__declspec(noinline)
#endif
void func(){}

更棒的写法:

!!变量,将包括指针的任何类型转换为 bool类型。

#if defined(__MY_MAC)
#define MY_NOINLINE __attribute__((noinline))
#define LIKELY(x)
#define UNLIKELY(x)

#elif defined(__MAC2) || defined(__clang__)
#define MY_NOINLINE __declspec(noinline)
#define LIKELY(x)   __builtin_expect(!!(x)), 1)
#define UNLIKELY(x) __builtin_expect(!!(x)), 0)

#else
#define MY_NOINLINE
#define LIKELY(x)
#define UNLIKELY(x)
#endif

MY_NOINLINE void func(){}

__PRETTYFUNC__

可以用于反射,获取函数的参数信息。

展示
#define LOG(x) (cout << __FILE__ << ":" << __LINE__ << ": " << __FUNCTION__ << ": " << __PRETTY_FUNCTION__ << ": " << (x) << endl)

#define LOG_TYPE(x) (cout << __PRETTY_FUNCTION__ << ": " << (x) << endl)
无模板
void func()
{
    LOG_TYPE("func()");
}

void func(int a, int b)
{
    LOG("func(int a, int b)");
    LOG_TYPE("func(int a, int b)");
}
// 无模板
func();
/*
Cpp_Reflection.cpp:16: func: void func(): func()
void func(): func()
*/


func(1, 2);
/*
Cpp_Reflection.cpp:22: func: void func(int, int): func(int a, int b)
void func(int, int): func(int a, int b)
*/
有模板
template <class T>
void func(T a)
{
    LOG("func(T a)");
    LOG_TYPE("func(T a)");
}
// 模板
func(3);
/*
Cpp_Reflection.cpp:29: func: void func(T) [with T = int]: func(T a)
void func(T) [with T = int]: func(T a)
*/

func(3.14f);
/*
Cpp_Reflection.cpp:29: func: void func(T) [with T = float]: func(T a)
void func(T) [with T = float]: func(T a)
*/
编译期反射
类型名 get_type_name

获取传入模板参数的类型 实际的类型名(获取T是什么)。

例如:string到底是什么。

template <typename T>
string get_type_name()
{
    string s = __PRETTY_FUNCTION__;
    // cout << " s = __PRETTY_FUNCTION__  " << s << endl;
    auto pos = s.find("T = ");
    pos += 4;
    auto pos2 = s.find_first_of(";]", pos);
    return s.substr(pos, pos2 - pos);
}
cout << get_type_name<int>() << endl;
/*
int
*/
cout << get_type_name<char>() << endl << endl;
/*
char
*/
值的名称 get_type_value

获取传入模板参数的值

  • 可以获取传入的enum值的名称
  • 以字符串的形式返回
template <typename T, T N>
string get_type_value()
// string get_enum_name()
{
    string s = __PRETTY_FUNCTION__;
    // cout << " s = __PRETTY_FUNCTION__  " << s << endl;
    auto pos = s.find("N = ");
    pos += 4;
    auto pos2 = s.find_first_of(";]", pos);
    return s.substr(pos, pos2 - pos);
}
cout << get_type_value<int, 1>() << endl;
/*
1
*/
cout << get_type_value<char, 'a'>() << endl;
/*
'a'
*/
cout << get_type_value<Color, (Color)2>() << endl;
/*
GREEN
*/
cout << get_type_value<Color, RED>() << endl << endl;
/*
RED
*/
编译期不能做的事

实例化 模板

  • 尖括号内不可以放运行时的东西。
  • 模板会根据参数类型,生成多个实例。

实例化 lambda

  • 参数编译和模板函数是一样的,可以根据参数的不同,实例化多次。
  • func(1)func(2)func(3)等很多遍
运行期反射 - 编译期实例化

反射:运行时确定变量的类型。

对枚举变量而言:给一个枚举变量的值,返回一个其对应的字符串

对某个enum,实例化所有组合。

1 显式多次实例化模板

使用暴力的方法,多次的实例化模板。

目标:

  • 希望每个参数N的值都会生成对应的实例,实例化多次。

  • 从而允许运行期就可以调用对应的实例化了。

如何模板实例化多次?实例化规则之:

  • 让变量作为形参,放入尖括号<>

    <T, T(1)><T, T(2)>

  • 注意:(T) 2是静态的,编译期的变量。

    • 可以放入模板的尖括号内 myFunc<T, (T) 2>(lambda){}
    • 会引起对应的模板实例化。

缺点:需要逐个填写判断,特别麻烦。

// 反射:运行时确定变量的类型,上面都是编译期确定的
// 获取传入模板参数的值  实际传入的值(值以字符串的形式返回) 可以获取enum的值
template <typename T>
string get_enum_name_dynamic(T n)
{
    // 模板可以生成3个实例
    if (n == (T)1)
        return get_enum_name<T, (T)1>();
    if (n == (T)2)
        return get_enum_name<T, (T)2>();
    if (n == (T)3)
        return get_enum_name<T, (T)3>();
    return "";
}
Color c1 = RED;
cout << get_enum_name_dynamic(c1) << endl;
cout << get_enum_name_dynamic(RED) << endl << endl;
/*
RED
*/
2 static_for

给一个枚举变量的值,返回一个其对应的字符串。

不需要逐个填写判断

  • 普通的for循环是动态的

    for(int i = 0; i < ...; i++)循环

    • 其中的i是运行时变量
    • (T)i不可以放入模板的尖括号<>中的。
  • 编译期的for循环:static_for

    • 可以用来实例化多次模板
    • static_for<Beg, End>中,
      • <1,3>将Beg End作为参数传入,
      • 显式地指定实例化的enum范围/次数。

注意这里的auto,如果使用int则会让lambda成为一个实实在在的函数,只实例化一次。

从而解析PRETTY_FUNCTION只能得到一种返回值。

template <typename T>
string get_enum_name_dynamic_new(T n)
{
    string ret;
    // C++17以内,lambda只有()参数,C++20支持模板参数
    // [&]<typename T>(T i){} 。
    // int的话,lambda作为一个切切实实的函数只实例化一次.auto才会实例化多次。
    static_for<0, 256>([&](auto i) {
        if (n == (T)i.value)
            ret = get_enum_name<T, (T)i.value>();
    });
    return ret;
}
cout << get_enum_name_dynamic_new(RED) << endl;
/*
RED
*/
static_for实现
实现1(if constexpr)
template <int N>
struct int_constant // 可以使用 std::intergrad_constant
{
    static constexpr int value = N;
};

template <int Beg, int END, class F>
void static_for(F const &func)
{
    if constexpr (Beg == END)
    {
        return;
    }
    else
    {
        func(intergrad_constant<int, Beg>());
        // func(int_constant<Beg>());
        static_for<Beg + 1, END>(func);
    }
}
实现2(SFAINE)
template <
	int Beg, int End,
	class F,
	typename enable_if<Beg != End>::type = 0
>
void static_for_n(F const &func)
{
    // int_constant如果自己写,不使用std提供的intergrad_constant
    // 放在static_for中,可以避免污染当前名称空间。
    struct int_constant
    {
        enum
        {
            value = Beg;
        };
    };
    func(int_constant());
    static_for_n<Beg + 1, End>(func);
}
实现3(C++ 98)

static_for_n C++98的实现

template <bool Cond>
struct my_enable_if{};

template <>
struct my_enable_if<true>
{
    typedef int type;
};

template <class T, T N>
const char *get_enum_name_static()
{
    return __PRETTY_FUNCTION__;
}
template <int Beg, int End, class F, typename my_enable_if<Beg == End>::type = 0>
void static_for_n(
    F const &func) {}

template <int Beg, int End, class F, typename my_enable_if<Beg != End>::type = 0>
void static_for_n(
    F const &func)
{
    struct int_constant
    {
        enum
        {
            value = Beg
        };
    };
    func(int_constant());
    static_for_n<Beg + 1, End>(func);
}


My::Color c2 = My::RED;
cout << get_enum_name_new(c2) << endl << endl;
/*
RED
*/
反向 由string获知enum int
template <typename T, int Beg = 0, int End = 256>
T enum_from_name(string const &s)
{
    for (int i = Beg; i < End; ++i)
    {
        if (s == get_enum_name_new((T)i))
            return (T)i;
    }
    // throw;
    // std::out_of_range();what不直观
    std::cout << "out_of_range" << std::endl;
    throw std::runtime_error("out_of_range");

    // throw;
}
cout << "case 5   " << enum_from_name<Color>("RED") << endl;

头文件安全

ifndef endif #pragma once

同一个文件只能将同一个头文件包含一次,但是有可能因为包含了另外一个头文件的头文件,而违反这一规则。

为了避免同一头文件被包含多次,C/C++中有两种宏实现方式:

解决方案:

  • ifndef endif
  • #pragma once

#ifndef

/* 
  当且仅当以前没有使用预处理器编译指令#define定义名称COORDIN_H_时
  才处理 #ifndef和#endif之间的语句
  下面的处理#define COORDIN_H_可以保证文件内容只被包含一次
*/
#ifndef  __SOMEFILE_H__ // 或 #if defined(__SOMEFILE_H_)
#define   __SOMEFILE_H__
 ... ... // 声明、定义语句
#endif
  • 优点
    • 可以保证同一个文件不会被包含多次,
    • 也可以保证内容完全相同的文件不会被不小心同时包含
    • 语言支持所以移植性好
  • 缺点:
    • 宏名重复 “撞车”
      • 如果不同头文件中的宏,“撞车”,可能导致头文件明明存在却找不到声明的状况。
    • 编译大型项目时,ifndef会使编译时间相对较长
      • 编译器每次都需要打开头文件才能判定是否有重复定义
      • 并不能防止编译器将文件包含两次,只是让它忽略除第一次包含之外的所有内容。

#pragma once

#pragma once
 ... ... // 声明、定义语句
  • 优点:

    • 可以避免名字冲突

      不需要再担心宏名冲突,不会出现宏名冲突引发的问题

    • 当预处理器第二次读到同一个文件时,就会自动跳过通常头文件都不想被重复导入,

  • 缺点:

    • 同一文件不会被包含多次(物理上的同一文件),如果某个头文件存在多个拷贝,不能保证它们不被重复包含
    • 不受一些较老版本编译器的支持

使用习惯

do{ }while(0)

经常见到宏定义中有一个do{ }while(0)语句,这是为什么呢?

使用do{….}while(0) 把宏包裹起来,成为一个独立的语法单元,从而不会与上下文发生混淆。

同时因为绝大多数的编译器都能够识别do{…}while(0)这种无用的循环并进行优化,所以使用这种方法也不会导致程序的性能降低。

// 如果宏的内容很长,可以进行换行,每行的末尾添加`\`,以表明后面的一行依然是宏的内容
// 宏的内容比较长,也没有缩进,易读性较差,因此转为多行
#define ADD(x, y) do { int sum = (x) + (y); return sum; } while (0)
// do{….}while(0) 多行
#define ADD(x, y) \
do \
{\
    int sum = (x) + (y);\
    return sum;\
} while (0)
  • 避免变量名的重复

比如我们定义一个交换两个值的宏

#define swapint(x, y) int tmp = x; x = y; y = tmp;

在大部分情况下可以工作,但是如果之前已经定义了tmp这个变量,则就会出错了,那我们可以把tmp换成平时不常用的名字,就大大降低了重名的概率了,这确实是一个办法,但不完美。因为即使这样,依然无法用在switch语句中。case语句中不允许声明变量

int x = 1, y = 2;
switch (value)
{
    case 1:
        // 编译出错,因为case语句中不允许声明变量
        swapint(x, y);
        break;
}
  • 支持switch语句

定义宏的时候,加上一层大括号,这样便可以用在switch语句中了。

但是会与;使用习惯相悖,多加一个;会导致编译报错。也影响IDE的自动代码格式化。

#define swapint(x, y) {int tmp = x; x = y; y = tmp;}
int x = 1, y = 2;
if (x < y)
    swapint(x, y);
else
    someaction();
// 上面的代码展开
if (x < y)
    {int tmp = x; x = y; y = tmp;};
else
    someaction();
// 编译出错,因为在else之前多了一个分号,导致语法错误,那么能不能不加分号
// 可以,但是C++程序员一般都习惯在末尾添加分号,而且不过不加分号,也会影响
// IDE的自动代码格式化

定义时 (x)

在宏定义时,给宏加上括号,参数也都加上括号

(宏)

给宏加上括号:避免 运算的优先级被改变

// 正确的宏定义
#define ADD(x, y) ((x) + (y))
// 错误的宏定义
#define ADD(x, y) (x) + (y)
// 输入
int value = ADD(2, 3) * ADD(4, 5);
// 正确的宏定义 输出:  正确
int value = ((2) + (3)) * ((4) + (5));	// 45

// 错误的宏定义 输出:  运算级错误
int value = (2) + (3) * (4) + (5);		// 19
(参数)

给参数加括号:避免 宏参数中含有逗号 ,从而错误分割了参数。

宏可以携带参数,而参数并没有什么要求,如果宏参数中含有逗号,那么就会带来歧义了

// 该宏本身没什么实际使用意义,只是为了说明问题
#define segment(seg) seg

// 宏参数中不含有逗号,没有问题
segment(int x = 1; int y = 3);

// 宏参数中含有逗号,编译错误,
// 因为宏展开时把","视为参数间的分隔符,但是这里的宏只接受一个参数
segment(int x = 1, y = 3);

// 解决办法就是给宏参数加上括号,使其为一体
segment((int x = 1, y = 3));

重复求值 auto

对于需要重复求值的情况

do while 没有返回值 ++

linux

#define ASSERT_GT(x,y) do{\
	auto ___x = (x);\
	if (!(__x) > (y)) { \
		std::cerr << "Assert failed : " << #x << "(" << __x < < ")" <<  
	}\

windows

typeof(x) __x = (x);

宏的调试

使用IDE

IDE会自动检查宏的展开情况。

编译时打印信息

#pragam message debug

如果想要在编译时打印一些信息,可以使用如下预处理指令:

// 在编译时报告和显示信息。
// 在编译期间,将一个文字串(messagestring)发送到标准输出窗口。
#pragma message("will print this message")

如果宏太过于复杂需要调试,可以利用宏自身的特性把宏展开后的内容打印出来。

  • 如果想要打印某个宏的内容,会发现编译器会报错。

  • 难点:宏不支持在运行时调试。

  • 原因:#pragma message指令必须接收一个字符串,

解决方法:

可使用如下代码协助输出SOMEMACRO的内容。

#define SOMEMACRO 123456

#define MACROTOSTR2(x) #x
#define PRINTMACRO(x) #x " = " MACROTOSTR2(x)

#pragma message(PRINTMACRO(SOMEMACRO))

编译上述代码便会在输出窗口打印SOMEMACRO = 123456的内容。

#pragma message: SOMEMACRO = 123456

带参数的宏同样适用:

#define SOMEMACRO 123456

#define MACROPARAM(x) new int(x);
#define MACROTOSTR2(x) #x
#define PRINTMACRO(x) #x " = " MACROTOSTR2(x)

#pragma message(PRINTMACRO(MACROPARAM(SOMEMACRO)))
# 上述代码块在编译时会打印出
MACROPARAM(SOMEMACRO) = new int(123456); 
#pragma message: MACROPARAM(SOMEMACRO) = new int(123456);

#就是宏展开后的内容。

当宏出现问题时,可以使用该方法打印出宏展开后的内容,然后调试展开后的内容,找到错误原因,接着同步修正宏本身的错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值