【C語言】深入理解預處理

文章目錄

  • 一、預定義符號
  • 二、#define定義常量:便捷的符號替換
    • 常見用法示例:
    • 注意事項:
  • 三、#define定義宏:帶參數的文本替換
    • 關鍵注意點:
  • 四、帶有副作用的宏參數
  • 五、宏替換的規則:預處理的執行步驟
    • 重要注意:
  • 六、宏與函數的對比:各有優劣
    • 宏的獨特優勢:
  • 七、#和##運算符:字符串化與記號粘合
    • 7.1 #運算符:字符串化
    • 7.2 ##運算符:記號粘合
  • 八、命名約定:區分宏與函數
  • 九、#undef:移除宏定義
  • 十、命令行定義:編譯時動態配置
  • 十一、條件編譯:選擇性編譯代碼
    • 常見條件編譯指令:
    • 應用示例:調試代碼控制
  • 十二、頭文件的包含:正確引入外部代碼
    • 12.1 包含方式及查找策略
    • 12.2 避免頭文件重復包含
  • 十三、其他預處理指令

C語言的預處理階段是代碼編譯前的重要環節,它負責處理以 #開頭的各種指令,為后續的編譯過程做好準備。預處理看似簡單,實則包含了豐富的功能和細節,掌握這些知識能讓我們寫出更高效、更靈活的代碼。

一、預定義符號

C語言為我們內置了一些預定義符號,它們在預處理期間就會被處理,我們可以直接在代碼中使用,無需額外定義。這些符號能為我們提供很多有用的信息:

  • __FILE__:進行編譯的源文件的文件名
  • __LINE__:當前代碼在文件中的行號
  • __DATE__:文件被編譯的日期(格式為“Mmm dd yyyy”)
  • __TIME__:文件被編譯的時間(格式為“hh:mm:ss”)
  • __STDC__:如果編譯器遵循ANSI C標準,其值為1;否則未定義
    在這里插入圖片描述

這些符號在調試代碼時非常有用,例如:

printf("Error in file: %s at line: %d\n", __FILE__, __LINE__);

當程序運行到這里時,會自動打印出錯誤所在的文件名和行號,幫助我們快速定位問題。

二、#define定義常量:便捷的符號替換

#define最基本的用法是定義常量,其基本語法為:

#define name stuff

常見用法示例:

  • 定義數值常量:#define MAX 1000
  • 為關鍵字創建簡短別名:#define reg register
  • 替換復雜實現為更形象的符號:#define do_forever for(;;)
  • 簡化代碼編寫:#define CASE break;case(在switch語句中自動添加break)

注意事項:

  1. 不要隨意添加分號
    例如#define MAX 1000;這樣的定義是不推薦的。當在
if(condition)max = MAX; else max = 0;

中使用時,替換后會變成

if(condition) max = 1000;; 
else max = 0;

導致if和else之間出現兩條語句,引發語法錯誤。

  1. 長內容的分行處理
    如果定義的內容過長,可以分成多行書寫,除最后一行外,每行末尾都加上反斜杠(續行符)。反斜杠后面不能有任何內容
   #define DEBUG_PRINT printf("file:%s\tline:%d\t \date:%s\ttime:%s\n", \__FILE__, __LINE__, \__DATE__, __TIME__)

三、#define定義宏:帶參數的文本替換

#define還允許我們定義帶參數的宏(macro),實現更靈活的文本替換。宏的聲明方式為:

#define name(parameter-list) stuff

其中parameter-list是由逗號分隔的參數列表,它們會出現在stuff中。

關鍵注意點:

  • 參數列表與名稱緊連:參數列表的左括號必須與name緊鄰,中間不能有空白,否則參數列表會被解釋為stuff的一部分。

  • 運算符優先級問題
    例如#define SQUARE(x) x * x這個宏,當傳入SQUARE(a + 1)時,會被替換為a + 1 * a + 1,結果為a + a + 1而非預期的(a+1)^2。解決方法是給參數和整體加上括號:#define SQUARE(x) (x) * (x)

    更復雜的情況:#define DOUBLE(x) (x) + (x),當調用10 * DOUBLE(5)時,會被替換為10 * (5) + (5),結果為55而非100。正確的定義應為#define DOUBLE(x) ((x) + (x))

    結論:用于數值表達式求值的宏,應給參數和整體都加上括號,避免運算符優先級導致的意外結果。

四、帶有副作用的宏參數

當宏參數在宏定義中出現多次,且參數帶有副作用時,可能會產生不可預測的結果。副作用指表達式求值時產生的永久性效果(如x++會改變x的值,而x+1則不會)。

例如:

#define MAX(a, b) ((a) > (b) ? (a) : (b))int x = 5, y = 8, z;
z = MAX(x++, y++);

預處理后會變成:

z = ((x++) > (y++) ? (x++) : (y++));

執行后,x的值變為6,y變為10,z變為9,與直觀預期可能不符。這就是副作用參數帶來的問題。

五、宏替換的規則:預處理的執行步驟

在擴展#define定義的符號和宏時,預處理器遵循以下步驟:

  1. 調用宏時,首先檢查參數是否包含#define定義的符號,若有則先替換。
  2. 將替換文本插入到原位置,宏的參數名被其值替換。
  3. 再次掃描結果文件,若包含#define定義的符號,重復上述過程。

重要注意:

  • 宏參數中可以包含其他#define定義的符號,但宏不能遞歸。
  • 預處理器不會搜索字符串常量的內容(即字符串中的符號不會被替換)。

六、宏與函數的對比:各有優劣

宏和函數都能實現代碼復用,但它們在多個方面存在差異:

屬性#define定義宏函數
代碼長度每次使用都會插入代碼,可能大幅增加程序長度(除非宏很短)代碼只出現一次,每次調用都使用同一份代碼
執行速度更快(無函數調用和返回開銷)較慢(存在函數調用和返回的額外開銷)
操作符優先級可能受周圍表達式優先級影響,需謹慎使用括號參數只在傳參時求值,結果傳遞給函數,表達式求值可預測
副作用參數參數若多次出現,副作用可能導致不可預料的結果參數只在傳參時求值一次,副作用易控制
參數類型與類型無關,只要操作合法即可用于任何類型與類型相關,不同類型可能需要不同函數(即使功能相同)
調試不方便調試(預處理階段已替換)可逐語句調試
遞歸不能遞歸可以遞歸

宏的獨特優勢:

宏可以接受類型作為參數,而函數無法做到。例如:

#define MALLOC(num, type) (type *)malloc(num * sizeof(type))// 使用
int *p = MALLOC(10, int);
// 替換后:(int *)malloc(10 * sizeof(int));

七、#和##運算符:字符串化與記號粘合

7.1 #運算符:字符串化

#運算符能將宏的參數轉換為字符串字面量,僅用于帶參數的宏的替換列表中。

例如:

#define PRINT(n) printf("the value of "#n " is %d", n);// 調用
int a = 10;
PRINT(a); // 替換后:printf("the value of ""a"" is %d", a);

輸出結果為:the value of a is 10

7.2 ##運算符:記號粘合

##可以將兩邊的符號合并為一個符號,允許從分離的文本片段創建標識符(稱為“記號粘合”)。

為不同類型定義求最大值的函數:

#define GENERIC_MAX(type)\
type type##_max(type a,type b)\
{\return a > b ? a : b;\
}GENERIC_MAX(int);//int_max()
GENERIC_MAX(float);//float_max()int main()
{int r1 = int_max(3, 9);float r2 = float_max(4.6, 8.9);printf("%d  %f", r1, r2);return 0;
}

八、命名約定:區分宏與函數

宏和函數的使用語法相似,為了區分二者,通常遵循以下約定:

  • 宏名全部大寫(如MAXSQUARE
  • 函數名不要全部大寫(如int_maxadd

九、#undef:移除宏定義

#undef指令用于移除已有的宏定義,語法為:

#undef NAME

如果需要重新定義一個已存在的宏,應先使用#undef移除其舊定義。

十、命令行定義:編譯時動態配置

許多C編譯器允許在命令行中定義符號,用于啟動編譯過程。這在根據同一源文件編譯程序的不同版本時非常有用。

根據內存大小動態調整數組長度:

// 源文件program.c
#include <stdio.h>
int main() {int array[ARRAY_SIZE];for (int i = 0; i < ARRAY_SIZE; i++) {array[i] = i;printf("%d ", array[i]);}return 0;
}

編譯時在命令行定義ARRAY_SIZE

# Linux環境
gcc -D ARRAY_SIZE=10 program.c  # 定義數組長度為10

十一、條件編譯:選擇性編譯代碼

條件編譯指令允許我們選擇性地編譯或放棄某些語句,常用于調試代碼的開關控制。

常見條件編譯指令:

  1. 基本形式
   #if 常量表達式// 代碼段#endif

預處理器會求值常量表達式,為真則編譯代碼段。

  1. 多分支形式
   #if 常量表達式// 代碼段1#elif 常量表達式// 代碼段2#else// 代碼段3#endif
  1. 判斷符號是否定義:只判斷是否被定義,不判斷真假
   #if defined(symbol)   // 等價于 #ifdef symbol// 代碼段#endif#if !defined(symbol)  // 等價于 #ifndef symbol// 代碼段#endif
  1. 嵌套指令
   #if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif#endif

應用示例:調試代碼控制

#define __DEBUG__ 1  // 定義調試符號int main() {int arr[10] = {0};for (int i = 0; i < 10; i++) {arr[i] = i;#ifdef __DEBUG__printf("arr[%d] = %d\n", i, arr[i]);  // 僅調試時編譯#endif}return 0;
}

十二、頭文件的包含:正確引入外部代碼

頭文件包含是預處理階段的重要操作,用于將其他文件的內容插入到當前文件中。

12.1 包含方式及查找策略

  • 本地文件包含#include "filename"
    查找策略:先在源文件所在目錄查找,若未找到,再到標準庫目錄查找。

  • 庫文件包含#include <filename.h>
    查找策略:直接到標準庫目錄查找,效率更高。

注意:庫文件也可以用""包含,但會降低查找效率,且不易區分是本地文件還是庫文件。

12.2 避免頭文件重復包含

頭文件被多次包含會導致代碼冗余、編譯時間增加,甚至出現重復定義錯誤。解決方法是使用條件編譯:

  1. 方式一:ifndef/define/endif
   // test.h#ifndef __TEST_H__  // 如果未定義__TEST_H__#define __TEST_H__  // 定義__TEST_H__// 頭文件內容(函數聲明、結構體定義等)void test();struct Stu { int id; char name[20]; };#endif  // __TEST_H__
  1. 方式二:#pragma once
    更簡潔的方式,直接在頭文件開頭添加:
   #pragma once  // 確保頭文件只被包含一次// 頭文件內容

十三、其他預處理指令

除了上述內容,C語言還有一些其他預處理指令,如:

  • #error:在編譯時輸出錯誤信息,終止編譯
  • #pragma:用于向編譯器提供額外信息(如#pragma pack()控制結構體對齊)
  • #line:修改當前行號和文件名

預處理是C語言編譯過程中的第一個環節,它通過#define#include、條件編譯等指令,為代碼的編譯做好準備。掌握預處理的各種特性,不僅能幫助我們寫出更高效、更靈活的代碼,還能讓我們在調試和維護程序時更得心應手。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/918366.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/918366.shtml
英文地址,請注明出處:http://en.pswp.cn/news/918366.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

展銳平臺(Android15)WLAN熱點名稱修改不生效問題分析

前言 在展銳Android V項目開發中&#xff0c;需要修改softAp/P2P熱點名稱時&#xff0c;發現集成GMS后直接修改framework層代碼無效。具體表現為&#xff1a; 修改packages/modules/Wifi/WifiApConfigStore中的getDefaultApConfiguration方法編譯燒錄后修改不生效 問題根源在…

wsl ubuntu訪問(掛載)vmware vmdk磁盤教程

之前使用VMware Workstation 虛擬機跑了個ubuntu&#xff0c;現在改用wsl了&#xff0c; 想把vmware的磁盤掛載到wsl ubuntu。一、磁盤合并我原先的vmware跑的ubuntu存在多個vmdk文件&#xff08;磁盤文件&#xff09;&#xff0c;需要先將磁盤合并成一個才方便掛載。首先你電腦…

UGUI源碼剖析(3):布局的“原子”——RectTransform的核心數據模型與幾何學

UGUI源碼剖析&#xff08;第三章&#xff09;&#xff1a;布局的“原子”——RectTransform的核心數據模型與幾何學 在前幾章中&#xff0c;我們了解了UGUI的組件規范和更新調度機制。現在&#xff0c;我們將深入到這個系統的“幾何學”核心&#xff0c;去剖析那個我們每天都在…

c++注意點(15)----設計模式(橋接模式與適配器模式)

一、結構型設計模式兩者有點相似&#xff0c;都是為了做到解耦的功能。適配器模式是一種結構型設計模式&#xff0c; 它能使接口不兼容的對象能夠相互合作。橋接模式是一種結構型設計模式&#xff0c; 可將一個大類或一系列緊密相關的類拆分為抽象和實現兩個獨立的層次結構&…

DuoPlus支持導入文件批量配置云手機參數,還優化了批量操作和搜索功能!

作為我常用的一款還不錯的跨境工具&#xff0c;DuoPlus云手機幫我高效完成了很多跨境工作&#xff0c;它的功能也在逐步完善和優化&#xff0c;今天來聊聊它最近新更新的一些功能。功能更新一覽新增導入文件配置參數&#xff1a;批量初始化代理、批量修改參數支持導入文件一鍵配…

PLC如何實現通過MQTT協議物聯網網關接入管理云平臺

在工業4.0與智能制造浪潮下&#xff0c;企業亟需實現設備數據的高效采集與云端協同&#xff0c;以支撐遠程監控、預測性維護等場景。工業智能網關憑借其強大的協議解析能力、邊緣計算功能及安全傳輸機制&#xff0c;成為PLC接入云平臺的核心解決方案。本文將從技術架構、功能模…

通過sealos工具在ubuntu 24.02上安裝k8s集群

一、系統準備&#xff08;1&#xff09;安裝openssh服務 sudo apt install openssh-server sudo systemctl start ssh sudo systemctl enable ssh&#xff08;2&#xff09;放通防火墻 sudo ufw allow ssh&#xff08;3&#xff09;開通root直接登錄 vim /etc/ssh/sshd_config#…

nginx+Lua環境集成、nginx+Lua應用

nginxluaredis實踐 概述 nginx、lua訪問redis的三種方式&#xff1a; 1。 HttpRedis模塊。 指令少&#xff0c;功能單一 &#xff0c;適合簡單的緩存。只支持get 、select命令。 2。 HttpRedis2Module模塊。 功能強大&#xff0c;比較靈活。 3。 lua-resty-redis庫 OpenResty。…

機器學習 K-Means聚類 無監督學習

目錄 K-Means 聚類&#xff1a;從原理到實踐的完整指南 什么是 K-Means 聚類&#xff1f; 應用場景舉例 K-Means 算法的核心原理 K-Means 算法的步驟詳解 可視化理解 K-Means 的優缺點分析 優點 缺點 如何選擇合適的 K 值&#xff1f; 1. 肘部法&#xff08;Elbow Me…

RabbitMQ面試精講 Day 16:生產者優化策略與實踐

【RabbitMQ面試精講 Day 16】生產者優化策略與實踐 開篇 歡迎來到"RabbitMQ面試精講"系列第16天&#xff0c;今天我們聚焦RabbitMQ生產者優化策略與實踐。在消息隊列系統中&#xff0c;生產者的性能表現直接影響整個系統的吞吐量和可靠性。掌握生產者優化技巧不僅能…

Android 系統的安全 和 三星安全的區別

維度Android&#xff08;AOSP 通用&#xff09;Samsung&#xff08;Knox 強化&#xff09;本質差異一句話信任根標準 Verified Boot&#xff08;公鑰由谷歌或 OEM 托管&#xff09;額外在 自家 SoC 里燒錄 Knox 密鑰 熔絲位&#xff0c;一旦解鎖即觸發 Knox 0x1 熔斷&#xff…

開源大模型實戰:GPT-OSS本地部署與全面測評

文章目錄一、引言二、安裝Ollama三、Linux部署GPT-OSS-20B模型四、模型測試4.1 AI幻覺檢測題題目1&#xff1a;虛假歷史事件題目2&#xff1a;不存在的科學概念題目3&#xff1a;虛構的地理信息題目4&#xff1a;錯誤的數學常識題目5&#xff1a;虛假的生物學事實4.2 算法題測試…

【無標題】命名管道(Named Pipe)是一種在操作系統中用于**進程間通信(IPC)** 的機制

命名管道&#xff08;Named Pipe&#xff09;是一種在操作系統中用于進程間通信&#xff08;IPC&#xff09; 的機制&#xff0c;它允許不相關的進程&#xff08;甚至不同用戶的進程&#xff09;通過一個可見的文件系統路徑進行數據交換。與匿名管道&#xff08;僅存在于內存&a…

Baumer相機如何通過YoloV8深度學習模型實現危險區域人員的實時檢測識別(C#代碼UI界面版)

《------往期經典推薦------》 AI應用軟件開發實戰專欄【鏈接】 序號 項目名稱 項目名稱 1 1.工業相機 + YOLOv8 實現人物檢測識別:(C#代碼,UI界面版) 2.工業相機 + YOLOv8 實現PCB的缺陷檢測:(C#代碼,UI界面版) 2 3.工業相機 + YOLOv8 實現動物分類識別:(C#代碼,U…

本文章分享一個本地錄音和實時傳輸錄音給app的功能(杰理)

我用的是杰理手表sdk&#xff0c;該功能學會就可自行在任何杰里sdk上做&#xff0c;庫函數大致一樣&#xff0c;學會運用這個方向就好。1.我們要驗證這個喇叭和麥是否正常最簡單的的辦法&#xff0c;就是直接萬用表測試&#xff0c;直接接正負極&#xff0c;看看是否通路&#…

Netty-Rest搭建筆記

0.相關知識Component、Repository、ServiceRepository //Scope設置bean的作用范圍 Scope("singleton")//單例 prototype每次創建都會給一個新實例。 public class BookDaoImpl implements BookDao { //生命周期public void save() {System.out.println("book d…

工作筆記-----lwip網絡任務初始化問題排查

工作筆記-----基于FreeRTOS的lwIP網絡任務初始化問題排查 Author&#xff1a;明月清了個風Date&#xff1a; 2025/8/10PS&#xff1a;新項目中在STMF7開發板上基于freeRTOS和lwIP開發網口相關任務&#xff0c;開發過程中遇到了網口無法連接的問題&#xff0c;進行了一系列的排查…

Kotlin動態代理池+無頭瀏覽器協程化實戰

我看到了很多作者展示了Kotlin在爬蟲領域的各種高級用法。我需要從中提取出最"牛叉"的操作&#xff0c;也就是那些充分利用Kotlin語言特性&#xff0c;使爬蟲開發更高效、更強大的技巧。 我準備用幾個主要部分來組織內容&#xff0c;每個部分會突出Kotlin特有的"…

PDF編輯工具,免費OCR識別表單

軟件介紹 今天推薦一款功能全面的PDF編輯工具——PDF XChange Editor&#xff0c;支持文本、圖片編輯及OCR識別&#xff0c;還能一鍵提取表單信息&#xff0c;滿足多樣化PDF處理需求。 軟件優勢 該軟件完全免費&#xff0c;下載后雙擊圖標即可直接運行&#xff0c;無需安裝&…

OpenEnler等Linux系統中安裝git工具的方法

在歐拉系統中安裝 Git使用 yum 包管理器安裝&#xff08;推薦&#xff0c;適用于歐拉等基于 RPM 的系統&#xff09;&#xff1a;# 切換到 root 用戶&#xff08;若當前不是&#xff09; su - root# 安裝 Git yum install -y git驗證安裝是否成功&#xff1a;git --version若輸…