C/C++如何写调试宏

· 浏览次数 : 0

小编点评

**宏定义方案** ```c++ #ifndef DEBUG_H__ #define DEBUG_H__ #include #include #include // 定义日志级别枚举 enum LogLevel { DEBUG, INFO, WARN, ERROR, FATAL }; // 全局日志级别变量声明 extern LogLevel globalLogLevel; // 定义日志宏1 #define LOG1(level, message) do { if (level >= globalLogLevel) { std::cout << "[\"" #level "\"] " << __func__ << \":\" << __LINE__ << \" \" << message << std::endl; } } while (0) // 定义日志宏2 #define LOG2(level, format, args...) do { if (level >= globalLogLevel) { fprintf(stderr, "[\"" #level "\"] %s:%d \" format \"\\r\\", __func__, __LINE__, ##args); } } while (0) // 通过宏控制代码段是否执行 #define EXECUTE#ifdef EXECUTE #define DEBUG_EXECUTE(code) {code} #else#define DEBUG_EXECUTE(code)#endif // 在main文件进行宏定义测试,需要定义全局日志级别,以INFO为例,则DEBUG信息不打印 #define DEBUG_EXECUTE(code) { LOG1(INFO, code); } ``` **测试文件** ```c++ #include "debug.h" int main(void) { LOG1(DEBUG, "DEBUG message\"); LOG1(INFO, "INFO message\"); LOG1(WARN, "WARN message\"); LOG1(ERROR, "ERROR message\"); LOG1(FATAL, "FATAL message\"); int num = 10; LOG2(INFO, "num: %d", num); DEBUG_EXECUTE(LOG2(ERROR, "debug execute")); return 0; } ``` **运行结果** ``` DEBUG message INFO message WARN message ERROR message FATAL message num: 10 debug execute ```

正文

1. 调试宏以及测试

在写代码时,不可避免需要打印提示、警告、错误等信息,且要灵活控制打印信息的级别。另外,还有可能需要使用宏来控制代码段(主要是调试代码段)是否执行。为此,本文提供一种调试宏定义方案,包括打印字符串信息LOG1宏和格式化打印LOG2宏,且能通过宏控制代码段执行。完整代码如下:

#ifndef __DEBUG_H__
#define __DEBUG_H__

#include <iostream>
#include <string>
#include <stdio.h>

// 定义日志级别枚举
enum LogLevel
{
    DEBUG,
    INFO,
    WARN,
    ERROR,
    FATAL
};

// 全局日志级别变量声明
extern LogLevel globalLogLevel;

// 定义日志宏1
#define LOG1(level, message) do { \
    if (level >= globalLogLevel) { \
        std::cout << "[" #level "] " << __func__ << ":" << __LINE__ << " " << message << std::endl; \
    } \
} while (0)

// 定义日志宏2
// stdout带缓冲,按行刷新,fflush(stdout)强制刷新
// stderr不带缓冲,立刻刷新到屏幕
#define LOG2(level, format, args...) do { \
    if (level >= globalLogLevel) { \
        fprintf(stderr, "[" #level "] %s:%d " format "\r\n", __func__, __LINE__, ##args); \
    } \
} while (0)

// 通过宏控制调试代码是否执行
#define EXECUTE

#ifdef EXECUTE
#define DEBUG_EXECUTE(code) {code}
#else
#define DEBUG_EXECUTE(code)
#endif

#endif

在main文件进行宏定义测试,需要定义全局日志级别,以INFO为例,则DEBUG信息不打印。测试文件如下:

#include "debug.h"

// 全局日志级别变量定义
LogLevel globalLogLevel = INFO;

int main(void)
{
    LOG1(DEBUG, "DEBUG message");
    LOG1(INFO, "INFO message");
    LOG1(WARN, "WARN message");
    LOG1(ERROR, "ERROR message");
    LOG1(FATAL, "FATAL message");

    int num = 10;
    LOG2(INFO, "num: %d", num);

    DEBUG_EXECUTE(
        LOG2(ERROR, "debug execute");
    )
}

2. 宏定义小细节

2.1 #和##

两者都是预处理运算符

  • #是字符串化运算符,将其后的宏参数转换为用双括号括起来的字符串。
  • ##是符号连接运算符,用于连接两个标记(标记不一定是宏变量,可以是标识符、关键字、数字、字符串、运算符)为一个标记。

在第一章中使用#把日志级别变量转为字符串,##的作用是在可变参数为0是,删除前面的逗号,只输出字符串。

2.2 do while(0)

do while常用来做循环,而while参数为0,表示这样的代码肯定不是做循环用的,它有什么用呢?

  1. 辅助定义复杂宏,避免宏替换出错

假如你定义一个这样宏,本意是调用DOSOMETHING时执行两个函数。

#define DOSOMETHING() \
			func1(); \
			func2();

但在类似如下使用宏的代码,宏展开时func2无视判断条件都会执行。

if (0 < a)
	DOSOMETHING();

// 宏展开后
if (0 < a)
    func1();
func2();

优化一下,用{}包裹宏是否可行呢?如下:

#define DOSOMETHING() { \
			func1(); \
			func2();}

由于我们写代码习惯在语句后加分号,你可能会有如下的展开后编译错误。

if(0 < a)
    DOSOMETHING();
else
   ...

// 宏展开后

if(0 < a)
{
    func1();
    func2();
}; // 错误处
else
    ...

而do while (0)则能避免这些错误,所以复杂宏定义经常使用它。

  1. 消除分支语句或者goto语句,提高代码的易读性

如果在一个函数中开始要分配一些资源,然后在中途执行过程中如果遇到错误则退出函数,当然,退出前先释放资源,我们的代码可能是这样:

bool Execute()
{
   // 分配资源
   int *p = new int;
   bool bOk(true);
 
   // 执行并进行错误处理
   bOk = func1();
   if(!bOk) 
   {
      delete p;   
      p = NULL;
      return false;
   }
 
   bOk = func2();
   if(!bOk) 
   {
      delete p;   
      p = NULL;
      return false;
   }
 
   // 执行成功,释放资源并返回
    delete p;   
    p = NULL;
    return true;
   
}

这里一个最大的问题就是代码的冗余,而且我每增加一个操作,就需要做相应的错误处理,非常不灵活。于是我们想到了goto:

bool Execute()
{
   // 分配资源
   int *p = new int;
   bool bOk(true);
 
   // 执行并进行错误处理
   bOk = func1();
   if(!bOk) goto errorhandle;
 
   bOk = func2();
   if(!bOk) goto errorhandle;
 
   // 执行成功,释放资源并返回
    delete p;   
    p = NULL;
    return true;
 
errorhandle:
    delete p;   
    p = NULL;
    return false;
   
}

代码冗余是消除了,但是我们引入了C++中身份比较微妙的goto语句,虽然正确的使用goto可以大大提高程序的灵活性与简洁性,但太灵活的东西往往是很危险的,它会让我们的程序捉摸不定,那么怎么才能避免使用goto语句,又能消除代码冗余呢,请看do...while(0)

bool Execute()
{
   // 分配资源
   int *p = new int;
 
   bool bOk(true);
   do
   {
      // 执行并进行错误处理
      bOk = func1();
      if(!bOk) break;
 
      bOk = func2();
      if(!bOk) break;
 
   }while(0);
 
    // 释放资源
    delete p;   
    p = NULL;
    return bOk;
   
}
  1. 使用代码块,代码块内定义变量,不用考虑变量重复问题

显而易见。

4. 参考博文

https://blog.csdn.net/keep_contact/article/details/127838298

与C/C++如何写调试宏相似的内容:

C/C++如何写调试宏

1. 调试宏以及测试 在写代码时,不可避免需要打印提示、警告、错误等信息,且要灵活控制打印信息的级别。另外,还有可能需要使用宏来控制代码段(主要是调试代码段)是否执行。为此,本文提供一种调试宏定义方案,包括打印字符串信息LOG1宏和格式化打印LOG2宏,且能通过宏控制代码段执行。完整代码如下: #i

如何洞察 .NET程序 非托管句柄泄露

## 一:背景 ### 1. 讲故事 很多朋友可能会有疑问,C# 是一门托管语言,怎么可能会有非托管句柄泄露呢? 其实一旦 C# 程序与 C++ 语言交互之后,往往就会被后者拖入非托管泥潭,让我们这些调试者被迫探究 `非托管领域问题`。 ## 二:非托管句柄泄露 ### 1. 测试案例 为了方便讲述

如何使用csproj构建C#源代码组件NuGet包?

一般我们构建传统的NuGet包,都是打包和分发dll程序集文件。 至于打包和分发C#源代码文件的做法,比较少见。 那么这种打包源代码文件的做法,有什么优点和缺点呢? 优点: 方便阅读源代码。 方便断点调试。 减少 Assembly 程序集模块加载个数。 更利于发布期间的剪裁(PublishTrimm

Visual Studio Code调试和发布ASP.NET Core Web应用

前言 上一篇文章主要讲了Visual Studio Code安装C#开发工具包并编写ASP.NET Core Web应用有兴趣的同学可以去看看,今天咱们主要是要讲讲如何在VS Code中调试和发布ASP.NET Core Web应用。 Visual Studio Code安装C#开发工具包并编写AS

WinDBG详解进程初始化dll是如何加载的

一:背景 1.讲故事 有朋友咨询个问题,他每次在调试 WinDbg 的时候,进程初始化断点之前都会有一些 dll 加载到进程中,比如下面这样: Microsoft (R) Windows Debugger Version 10.0.25200.1003 X86 Copyright (c) Micro

C++指针和地址偏移在HotSpot VM中的应用

在前面我们介绍过new运算符,这个操作实际上上包含了如下3个步骤: 调用operator new的标准库函数。此函数会分配一块内存空间以便函存储相应类型的实例; 调用相应类的构造函数; 返回一个指向该对象的指针。 在第一步中,其实我们可以自己写个operator new函数对标准库函数进行重载,通常

一文教你如何调用Ascend C算子

本文分享自华为云社区《一文教你如何调用Ascend C算子》,作者: 昇腾CANN。 Ascend C是CANN针对算子开发场景推出的编程语言,原生支持C和C++标准规范,兼具开发效率和运行性能。基于Ascend C编写的算子程序,通过编译器编译和运行时调度,运行在昇腾AI处理器上。使用Ascend

c#如何使用WASM跨语言调用?

## 介绍Wasm(WebAssembly) WebAssembly(简称Wasm)是一种用于基于堆栈的虚拟机的二进制指令格式。Wasm被设计为编程语言的可移植编译目标,支持在web上部署客户端和服务器应用程序。 ### 什么是wasmtime (WebAssembly Time)?它和WASM(W

C++多态与虚拟:C++编译器对函数名的改编(Name Mangling)

如果函数名称都相同(也就是被overloaded),编译器在面对你的函数唤起动作时,究竟是如何确定调用哪个函数实体呢?事实上,编译器把所有同名的overloaded functions视为不同的函数,并且以特殊方式对它们的函数名称做了手脚,以四个Add()函数为例: 1 int Add(int a,

聊一聊如何截获 C# 程序产生的日志

一:背景 1.讲故事 前段时间分析了一个dump,一顿操作之后,我希望用外力来阻止程序内部对某一个com组件的调用,对,就是想借助外力实现,如果用 windbg 的话,可以说非常轻松,但现实情况比较复杂,客户机没有windbg,也不想加入任何的手工配置,希望全自动化来处理。 真的很无理哈。。。不过这