簡體   English   中英

C 宏有什么用?

[英]What are C macros useful for?

我已經寫了一點 C,我可以很好地閱讀它以大致了解它在做什么,但是每次我遇到一個宏時,它都讓我徹底崩潰。 我最終不得不記住宏是什么,並在閱讀時將其替換在腦海中。 我遇到的那些直觀易懂的總是像小迷你函數,所以我一直想知道為什么它們不只是函數。

我可以理解需要在預處理器中為調試或跨平台構建定義不同的構建類型,但定義任意替換的能力似乎只會使已經很難理解的語言變得更加難以理解。

為什么要為 C 引入如此復雜的預處理器? 有沒有人有一個使用它的例子,這會讓我理解為什么它似乎仍然用於#debug 樣式條件編譯以外的其他目的?

編輯:

閱讀了許多答案,我仍然不明白。 最常見的答案是內聯代碼。 如果 inline 關鍵字不這樣做,那么要么它有充分的理由不這樣做,要么實現需要修復。 我不明白為什么需要一種完全不同的機制,這意味着“真正內聯這段代碼”(除了在內聯之前編寫的代碼之外)。 我也不理解提到的“如果它太愚蠢而不能放入函數中”的想法。 當然,任何接受輸入並產生 output 的代碼都最好放在 function 中。 我想我可能沒有得到它,因為我不習慣編寫 C 的微優化,但預處理器感覺就像是一些簡單問題的復雜解決方案。

我最終不得不記住宏是什么,並在閱讀時將其替換在腦海中。

這似乎很難反映宏的命名。 如果它是log_function_entry()宏,我會假設您不必模擬預處理器。

我遇到的那些直觀易懂的總是像小迷你函數,所以我一直想知道為什么它們不只是函數。

通常它們應該是,除非它們需要對泛型參數進行操作。

#define max(a,b) ((a)<(b)?(b):(a))

將適用於具有<運算符的任何類型。

不僅僅是函數,宏允許您使用源文件中的符號執行操作。 這意味着您可以創建一個新的變量名稱,或引用宏所在的源文件和行號。

在 C99 中,宏還允許您調用可變參數函數,例如printf

#define log_message(guard,format,...) \
   if (guard) printf("%s:%d: " format "\n", __FILE__, __LINE__,__VA_ARGS_);

log_message( foo == 7, "x %d", x)

其中格式類似於printf 如果守衛為真,它會輸出消息以及打印消息的文件和行號。 如果它是 function 調用,它不會知道您從中調用它的文件和行,並且使用vaprintf會更有效。

通過比較使用C宏的幾種方式,以及如何在D中實現它們,這段摘錄幾乎總結了我對此事的看法。

從 DigitalMars.com 復制

早在發明C時,編譯器技術還很原始。 在前端安裝文本宏預處理器是添加許多強大功能的簡單直接的方法。 程序的規模和復雜性不斷增加,這表明這些特性伴隨着許多固有的問題。 D沒有預處理器; 但是D提供了一種更具可擴展性的方法來解決相同的問題。

預處理器宏為C添加了強大的功能和靈活性。 但它們有一個缺點:

  • 宏沒有scope的概念; 它們從定義點到源代碼結束都是有效的。 他們在 .h 文件、嵌套代碼等方面進行了大量的處理。當#include執行數萬行宏定義時,避免無意中的宏擴展成為問題。
  • 調試器不知道宏。 嘗試使用符號數據調試程序會被調試器破壞,只知道宏擴展,而不知道宏本身。
  • 宏使得無法對源代碼進行標記,因為早期的宏更改可以任意重做標記。
  • 宏的純文本基礎導致任意和不一致的使用,使得使用宏的代碼容易出錯。 (通過C++中的模板引入了一些解決此問題的嘗試。)
  • 宏仍然用於彌補語言表達能力的不足,例如 header 文件周圍的“包裝器”。

下面列舉了宏的常見用途,以及 D 中的相應功能:

  1. 定義文字常量:

    • C預處理器方式

      #define VALUE 5
    • D方式

      const int VALUE = 5;
  2. 創建值或標志列表:

    • C預處理器方式

      int flags: #define FLAG_X 0x1 #define FLAG_Y 0x2 #define FLAG_Z 0x4... flags |= FLAG_X;
    • D方式

      enum FLAGS { X = 0x1, Y = 0x2, Z = 0x4 }; FLAGS flags; ... flags |= FLAGS.X;
  3. 設置 function 調用約定:

    • C預處理器方式

      #ifndef _CRTAPI1 #define _CRTAPI1 __cdecl #endif #ifndef _CRTAPI2 #define _CRTAPI2 __cdecl #endif int _CRTAPI2 func();
    • D方式

      可以在塊中指定調用約定,因此無需為每個 function 更改它:

       extern (Windows) { int onefunc(); int anotherfunc(); }
  4. 簡單的泛型編程:

    • C預處理器方式

      根據文本替換選擇要使用的 function:

       #ifdef UNICODE int getValueW(wchar_t *p); #define getValue getValueW #else int getValueA(char *p); #define getValue getValueA #endif
    • D方式

      D啟用作為其他符號別名的符號聲明:

       version (UNICODE) { int getValueW(wchar[] p); alias getValueW getValue; } else { int getValueA(char[] p); alias getValueA getValue; }

DigitalMars 網站上有更多示例。

它們是 C 之上的一種編程語言(一種更簡單的語言),因此它們對於在編譯時進行元編程很有用......換句話說,您可以編寫生成 C 代碼的宏代碼,所需的行數和時間更少直接寫在C中。

它們對於編寫“多態”或“重載”的“函數式”表達式也非常有用; 例如,一個 max 宏定義為:

#define max(a,b) ((a)>(b)?(a):(b))

適用於任何數字類型; 在 C 中你不能寫:

int max(int a, int b) {return a>b?a:b;}
float max(float a, float b) {return a>b?a:b;}
double max(double a, double b) {return a>b?a:b;}
...

即使你想要,因為你不能重載函數。

更不用說條件編譯和文件包括(也是宏語言的一部分)......

宏允許某人在編譯期間修改程序行為。 考慮一下:

  • C 常量允許在開發時修復程序行為
  • C 變量允許在執行時修改程序行為
  • C 宏允許在編譯時修改程序行為

在編譯時意味着未使用的代碼甚至不會 go 進入二進制文件,並且構建過程可以修改這些值,只要它與宏預處理器集成。 示例:make ARCH=arm(假設轉發宏定義為 cc -DARCH=arm)

簡單例子:(來自glibc limits.h,定義long的最大值)

#if __WORDSIZE == 64
#define LONG_MAX 9223372036854775807L
#else
#define LONG_MAX 2147483647L
#endif

如果我們正在編譯 32 位或 64 位,則在編譯時驗證(使用#define __WORDSIZE)。 使用 multilib 工具鏈,使用參數 -m32 和 -m64 可能會自動更改位大小。

(POSIX 版本請求)

#define _POSIX_C_SOURCE 200809L

編譯期間的請求 POSIX 2008 支持。 標准庫可能支持許多(不兼容的)標准,但通過此定義,它將提供正確的 function 原型(例如:getline()、無 gets() 等)。 例如,如果庫不支持該標准,它可能會在編譯時給出#error,而不是在執行期間崩潰。

(硬編碼路徑)

#ifndef LIBRARY_PATH
#define LIBRARY_PATH "/usr/lib"
#endif

在編譯期間定義硬代碼目錄。 例如,可以使用 -DLIBRARY_PATH=/home/user/lib 進行更改。 如果那是一個 const char *,你會在編譯期間如何配置它?

(pthread.h,編譯時的復雜定義)

# define PTHREAD_MUTEX_INITIALIZER \
  { { 0, 0, 0, 0, 0, 0, { 0, 0 } } }

可能會聲明大量文本,否則可能不會被簡化(總是在編譯時)。 使用函數或常量(在編譯時)是不可能做到這一點的。

為了避免真正使事情復雜化並避免暗示編碼不良 styles,我不會給出在不同的、不兼容的操作系統中編譯的代碼示例。 為此使用您的交叉構建系統,但應該清楚的是,預處理器允許在沒有構建系統幫助的情況下這樣做,而不會因為缺少接口而中斷編譯。

最后,想想條件編譯在嵌入式系統上的重要性,處理器速度和 memory 是有限的,系統是非常異構的。

現在,如果您問,是否可以用正確的定義替換所有宏常量定義和 function 調用? 答案是肯定的,但它不會簡單地使在編譯 go 期間更改程序行為的需要消失。 仍然需要預處理器。

請記住,宏(和預處理器)來自 C 的早期。 它們曾經是執行內聯“函數”的唯一方法(因為,當然,inline 是一個非常新的關鍵字),並且它們仍然是強制內聯某些內容的唯一方法。

此外,宏是您可以在編譯時執行諸如將文件和行插入字符串常量之類的技巧的唯一方法。

這些天來,宏曾經是唯一方法的許多事情通過更新的機制得到了更好的處理。 但他們仍然有自己的位置,有時。

除了內聯提高效率和條件編譯外,宏還可用於提高低級 C 代碼的抽象級別。 C 並沒有真正使您免受 memory 和資源管理和數據精確布局的基本細節的影響,並且支持非常有限的 forms 管理大型系統和機制的信息隱藏和機制。 使用宏,您不再局限於僅使用 C 語言中的基本結構:您可以定義自己的數據結構和編碼結構(包括類和模板!),同時仍然名義上編寫 C!

預處理器宏實際上提供了在編譯時執行的圖靈完備語言。 C++ 方面的一個令人印象深刻(而且有點可怕)的例子已經結束: Boost 預處理器庫使用C99 / C++98預處理器來構建(相對)安全的編程結構,然后將其擴展到任何底層聲明和代碼您輸入,無論是 C 還是 C++。

在實踐中,我建議將預處理器編程作為最后的手段,當您沒有自由使用更安全的語言中的高級構造時。 但有時,如果你的背靠在牆上,而黃鼠狼正在逼近……,知道你能做什么是件好事!

來自計算機愚蠢

我在許多 UNIX 的免費游戲程序中看到了這段代碼摘錄:

/*
* 位值。
*/
#define BIT_0 1
#define BIT_1 2
#define BIT_2 4
#define BIT_3 8
#define BIT_4 16
#define BIT_5 32
#define BIT_6 64
#define BIT_7 128
#define BIT_8 256
#define BIT_9 512
#define BIT_10 1024
#define BIT_11 2048
#define BIT_12 4096
#define BIT_13 8192
#define BIT_14 16384
#define BIT_15 32768
#define BIT_16 65536
#define BIT_17 131072
#define BIT_18 262144
#define BIT_19 524288
#define BIT_20 1048576
#define BIT_21 2097152
#define BIT_22 4194304
#define BIT_23 8388608
#define BIT_24 16777216
#define BIT_25 33554432
#define BIT_26 67108864
#define BIT_27 134217728
#define BIT_28 268435456
#define BIT_29 536870912
#define BIT_30 1073741824
#define BIT_31 2147483648

實現這一點的一個更簡單的方法是:

#define BIT_0 0x00000001
#define BIT_1 0x00000002
#define BIT_2 0x00000004
#define BIT_3 0x00000008
#define BIT_4 0x00000010
...
#define BIT_28 0x10000000
#define BIT_29 0x20000000
#define BIT_30 0x40000000
#define BIT_31 0x80000000

更簡單的方法仍然是讓編譯器進行計算:

#define BIT_0 (1)
#define BIT_1 (1 << 1)
#define BIT_2 (1 << 2)
#define BIT_3 (1 << 3)
#define BIT_4 (1 << 4)
...
#define BIT_28 (1 << 28)
#define BIT_29 (1 << 29)
#define BIT_30 (1 << 30)
#define BIT_31 (1 << 31)

但是為什么 go 還要麻煩定義 32 個常量呢? C 語言也有參數化宏。 您真正需要的是:

#define BIT(x) (1 << (x))

無論如何,我想知道編寫原始代碼的人是使用計算器還是只是在紙上計算出來。

這只是宏的一種可能用途。

我將補充已經說過的內容。

因為宏適用於文本替換,它們允許您做非常有用的事情,而這些事情是使用函數無法做到的。

以下是宏真正有用的幾種情況:

/* Get the number of elements in array 'A'. */
#define ARRAY_LENGTH(A) (sizeof(A) / sizeof(A[0]))

這是一個非常流行且經常使用的宏。 例如,當您需要遍歷數組時,這非常方便。

int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int i;
    for (i = 0; i < ARRAY_LENGTH(a); ++i) {
        printf("a[%d] = %d\n", i, a[i]);
    }
    return 0;
}

在這里,如果另一個程序員在聲明中向a添加五個更多元素並不重要。 for循環將始終遍歷所有元素。

C 庫用於比較 memory 和字符串的函數很難使用。

你寫:

char *str = "Hello, world!";

if (strcmp(str, "Hello, world!") == 0) {
    /* ... */
}

或者

char *str = "Hello, world!";

if (!strcmp(str, "Hello, world!")) {
    /* ... */
}

檢查str是否指向"Hello, world" 我個人認為這兩種解決方案看起來都非常丑陋和令人困惑(尤其是.strcmp(...) )。

這是一些人(包括我)在需要使用strcmp / memcmp比較字符串或 memory 時使用的兩個簡潔的宏:

/* Compare strings */
#define STRCMP(A, o, B) (strcmp((A), (B)) o 0)

/* Compare memory */
#define MEMCMP(A, o, B) (memcmp((A), (B)) o 0)

現在您可以編寫如下代碼:

char *str = "Hello, world!";

if (STRCMP(str, ==, "Hello, world!")) {
    /* ... */
}

這里的意圖更清楚了!

這些是宏用於功能無法完成的事情的情況。 宏不應該用來替換函數,但它們還有其他很好的用途。

宏真正發揮作用的一種情況是使用它們進行代碼生成。

我曾經在一個舊的 C++ 系統上工作,該系統使用插件系統以他自己的方式將參數傳遞給插件(使用自定義的類似地圖的結構)。 一些簡單的宏被用來處理這個怪癖,並允許我們在插件中使用具有正常參數的真正的 C++ 類和函數而沒有太多問題。 所有由宏生成的膠水代碼。

與常規函數不同,您可以在宏中執行控制流(if、while、for、...)。 這是一個例子:

#include <stdio.h>

#define Loop(i,x) for(i=0; i<x; i++)

int main(int argc, char *argv[])
{
    int i;
    int x = 5;
    Loop(i, x)
    {
        printf("%d", i); // Output: 01234
    } 
    return 0;
} 

鑒於您問題中的評論,您可能不完全理解調用 function 可能需要相當多的開銷。 參數和鍵寄存器可能必須在傳入的過程中復制到堆棧中,而堆棧在傳出的過程中展開。 較舊的英特爾芯片尤其如此。 宏讓程序員保留 function 的抽象(幾乎),但避免了 function 調用的昂貴開銷。 inline 關鍵字是建議性的,但編譯器可能並不總是正確。 “C”的優點和危險在於您通常可以根據自己的意願彎曲編譯器。

在你的面包和黃油中,日常應用程序編程這種微優化(避免 function 調用)通常更糟然后沒用,但如果你正在編寫一個時間關鍵的 function 由 Z504284C019F1AFDAD38 操作系統調用,那么它可以產生巨大的影響。

它有利於內聯代碼並避免 function 調用開銷。 如果您想稍后更改行為而不編輯很多地方,也可以使用它。 它對復雜的事情沒有用,但對於你想要內聯的簡單代碼行,它還不錯。

宏可以讓您擺脫復制粘貼的片段,這是您無法以任何其他方式消除的。

例如(真正的代碼,VS 2010 編譯器的語法):

for each (auto entry in entries)
{
        sciter::value item;
        item.set_item("DisplayName",    entry.DisplayName);
        item.set_item("IsFolder",       entry.IsFolder);
        item.set_item("IconPath",       entry.IconPath);
        item.set_item("FilePath",       entry.FilePath);
        item.set_item("LocalName",      entry.LocalName);
        items.append(item);
    }

這是您將同名字段值傳遞到腳本引擎的地方。 這是復制粘貼的嗎? 是的。 DisplayName用作腳本的字符串和編譯器的字段名稱。 那不好嗎? 是的。 如果您重構代碼並將LocalName重命名為RelativeFolderName (就像我所做的那樣)並且忘記對字符串執行相同的操作(就像我所做的那樣),腳本將以您意想不到的方式工作(實際上,在我的示例中取決於您是否忘記在單獨的腳本文件中重命名該字段,但是如果該腳本用於序列化,那將是一個100%的錯誤)。

如果為此使用宏,則該錯誤將沒有空間:

for each (auto entry in entries)
{
#define STR_VALUE(arg) #arg
#define SET_ITEM(field) item.set_item(STR_VALUE(field), entry.field)
        sciter::value item;
        SET_ITEM(DisplayName);
        SET_ITEM(IsFolder);
        SET_ITEM(IconPath);
        SET_ITEM(FilePath);
        SET_ITEM(LocalName);
#undef SET_ITEM
#undef STR_VALUE
        items.append(item);
    }

不幸的是,這為其他類型的錯誤打開了大門。 您可以在編寫宏時打錯字,並且永遠不會看到損壞的代碼,因為編譯器在所有預處理后都不會顯示它的外觀。 其他人可以使用相同的名稱(這就是我使用#undef盡快“發布”宏的原因)。 所以,明智地使用它。 如果您看到另一種擺脫復制粘貼代碼(例如函數)的方法,請使用這種方法。 如果您發現使用宏刪除復制粘貼的代碼不值得,請保留復制粘貼的代碼。

通過利用 C 預處理器的文本操作,可以構造 C 等效的多態數據結構。 使用這種技術,我們可以構建一個可靠的原始數據結構工具箱,可以在任何 C 程序中使用,因為它們利用了 C 語法而不是任何特定實現的細節。

這里給出了如何使用宏來管理數據結構的詳細說明 - http://multi-core-dump.blogspot.com/2010/11/interesting-use-of-c-macros-polymorphic.html

一個明顯的原因是,通過使用宏,代碼將在編譯時擴展,並且您會得到一個偽函數調用而沒有調用開銷。

否則,您也可以將其用於符號常量,這樣您就不必在多個地方編輯相同的值來更改一件小事。

我沒有看到任何人提到這一點,關於 function 之類的宏,例如:

#define MIN(X, Y) ((X) < (Y)? (X): (Y))

通常建議在不必要時避免使用宏,原因有很多,可讀性是主要問題。 所以:

什么時候應該在 function 上使用這些?

Almost never, since there's a more readable alternative which is inline , see https://www.greenend.org.uk/rjk/tech/inline.html or http://www.cplusplus.com/articles/2LywvCM9/ (the第二個鏈接是 C++ 頁面,但據我所知,這一點適用於 c 編譯器)。

現在,細微的差別是宏由預處理器處理,而內聯由編譯器處理,但現在沒有實際區別。

什么時候適合使用這些?

適用於小型功能(最多兩個或三個襯墊)。 目標是在程序運行時獲得一些優勢,因為 function 之類的宏(和內聯函數)是在預處理(或內聯的情況下編譯)期間完成的代碼替換,而不是存在於 memory 中的實際函數,所以沒有 function 調用開銷(鏈接頁面中的更多詳細信息)。

宏 .. 當您的 &#(*$& 編譯器拒絕內聯某些內容時。

那應該是一張勵志海報,不是嗎?

嚴肅地說,谷歌預處理器濫用(你可能會看到一個與#1 結果類似的 SO 問題)。 如果我正在編寫一個超出 assert() 功能的宏,我通常會嘗試查看我的編譯器是否真的會內聯類似的 function。

其他人會反對使用 #if 進行條件編譯.. 他們寧願你:

if (RUNNING_ON_VALGRIND)

而不是

#if RUNNING_ON_VALGRIND

.. 出於調試目的,因為您可以在調試器中看到 if() 但看不到 #if。 然后我們深入研究#ifdef 與#if。

如果它的代碼少於 10 行,請嘗試內聯它。 如果不能內聯,試着優化一下。 如果它太傻以至於不能成為 function,那就做一個宏。

雖然我不是宏的忠實粉絲,並且不再傾向於寫太多 C,但根據我目前的任務,這樣的事情(顯然可能有一些副作用)很方便:

#define MIN(X, Y)  ((X) < (Y) ? (X) : (Y))

現在我已經好幾年沒有寫過這樣的東西了,但是這樣的“函數”遍布我在職業生涯早期維護的代碼中。 我想擴展可以被認為是方便的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM