C/C++如何寫除錯宏

wangxinzhi發表於2024-04-30

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

相關文章