1. 预处理器概念宏由 C/C 预处理器处理发生在正式编译之前。常见预处理指令#define#undef#if#ifdef#ifndef#elif#else#endif#include#error#pragma宏本质上通常是文本替换不具备普通 C 语言实体的类型系统和作用域规则。2. 对象宏#defineBUFFER_SIZE1024#defineAPP_NAMEDetector使用charbuffer[BUFFER_SIZE];传统 C 代码经常使用宏定义常量但现代 C 更推荐inlineconstexprstd::size_t kBufferSize1024;inlineconstexprcharkAppName[]Detector;优势有类型遵守作用域可被调试器识别减少文本替换副作用。3. 函数宏#defineSQUARE(x)((x)*(x))括号非常重要。错误定义#defineSQUARE_BAD(x)x*x调用intresultSQUARE_BAD(12);展开为12*12结果不是 9。4. 多次求值问题即使加了括号函数宏仍可能重复计算参数。#defineSQUARE(x)((x)*(x))inti3;intvalueSQUARE(i);i会被展开多次行为危险可能涉及未定义行为或不可预期结果。现代 C 推荐templatetypenameTconstexprTsquare(T value){returnvalue*value;}5. 多语句宏错误写法#defineLOG_AND_RETURN(x)\log(x);\return在if/else中可能破坏语法结构。推荐使用do { ... } while (0)#defineLOG_AND_RETURN(x)\do{\log(x);\return;\}while(0)调用if(failed){LOG_AND_RETURN(error);}else{continueWork();}do { ... } while (0)让宏整体表现得像一条语句。6. 字符串化运算符##defineSTRINGIFY_IMPL(x)#x#defineSTRINGIFY(x)STRINGIFY_IMPL(x)使用constchar*textSTRINGIFY(hello);结果hello两层宏常用于先展开参数再转成字符串。7. Token 拼接运算符###defineMAKE_NAME(prefix,id)prefix##idintMAKE_NAME(sensor_,1)10;展开intsensor_110;典型用途生成变量名注册表单元测试框架元编程辅助平台兼容封装。应谨慎使用因为生成符号可能降低可读性和可调试性。8. 可变参数宏#defineLOG(format,...)\std::printf(format,__VA_ARGS__)C20 可使用__VA_OPT__#defineLOG(format,...)\std::printf(format__VA_OPT__(,)__VA_ARGS__)这样无额外参数时不会产生多余逗号。LOG(started\n);LOG(value%d\n,value);9. 条件编译#ifdefDEBUGstd::coutdebug mode\n;#endif平台判断#ifdefined(_WIN32)// Windows#elifdefined(__linux__)// Linux#elifdefined(__APPLE__)// macOS#else#errorUnsupported platform#endif功能开关#defineENABLE_PROFILING1#ifENABLE_PROFILINGstartProfiler();#endif大型工程中功能开关更适合由构建系统传入g-DENABLE_PROFILING1main.cppCMaketarget_compile_definitions( app PRIVATE ENABLE_PROFILING1 )10. 头文件保护传统方式#ifndefPROJECT_CAMERA_DRIVER_H#definePROJECT_CAMERA_DRIVER_H// declarations#endif也可使用#pragmaonce#pragma once简洁且被主流编译器广泛支持但它是编译器扩展不是传统标准预处理指令。11. 预定义宏常见预定义宏__FILE____LINE____DATE____TIME____func____cplusplus注意__func__是函数内可用的预定义标识符不完全等同于普通宏。日志示例#defineLOG_ERROR(msg)\std::cerr[ERROR] \__FILE__:\__LINE__ \msg\nC 标准版本判断#if__cplusplus202002L// C20#elif__cplusplus201703L// C17#endif12.assert与static_assert运行期断言#includecassertassert(pointer!nullptr);定义NDEBUG时assert通常会被移除。因此不要在assert参数中写必须执行的副作用逻辑。错误assert(initializeSystem());发布版本中可能完全不调用initializeSystem()。编译期断言static_assert(sizeof(int)4);static_assert是 C 语言特性不是普通函数宏。13. 宏的作用域问题宏没有命名空间作用域。#definemin(a,b)...即使定义在命名空间文本区域中也可能污染后续代码。因此宏命名通常使用全大写和项目前缀#defineMYPROJECT_MAX_BUFFER_SIZE4096用完可取消#undefMYPROJECT_MAX_BUFFER_SIZE14. 宏与模板、内联函数对比特性宏内联函数 / 模板类型检查无有作用域基本文本级遵守 C 作用域参数求值次数可能多次一次调试体验较差较好泛型能力文本替换模板类型系统编译期计算有限constexpr支持生成标识符支持##通常不支持条件编译支持不能完全替代15. 宏的合理用途宏并非完全不能用以下场景仍较常见条件编译平台兼容编译器属性封装头文件保护日志中插入文件和行号注册框架生成重复代码单元测试框架C 语言中缺少泛型和constexpr时的兼容实现。16. C 中可替代宏的语言特性常量inlineconstexprintkMaxCount100;小函数constexprintmaxValue(inta,intb){returnab?a:b;}泛型templatetypenameTconstexprconstTmaxValue(constTa,constTb){returnab?b:a;}类型别名usingDeviceIdstd::uint32_t;枚举常量enumclassStatus{Offline,Online};条件编译中的分支裁剪ifconstexpr(std::is_integral_vT){}if constexpr不能替代跨平台头文件选择等预处理行为但能减少模板代码中的宏使用。17. 宏设计规范对所有参数加括号。对整个表达式加括号。避免多次求值参数。多语句宏使用do { ... } while (0)。宏名称使用项目前缀。不要定义过于通用的名称如min、max、check。文档说明宏是否可能控制流程如return、break。避免宏参数名称与内部临时变量冲突。能用constexpr、模板、内联函数时优先使用它们。构建选项宏应由 CMake 等构建系统集中管理。18. 常见面试题为什么宏没有类型安全宏在编译器进行语法和类型分析前完成文本替换因此宏本身不理解变量类型。为什么宏参数要加括号避免运算符优先级改变展开后的表达式含义。为什么多语句宏使用do while(0)让宏在语法上表现为单条语句可安全用于if/else。宏和const的区别宏是文本替换const是有类型、有作用域的语言实体。宏和inline函数的区别内联函数具有类型检查、作用域和正常参数求值语义。编译器是否真正内联由优化器决定。19. 总结宏是预处理阶段的文本生成工具适合条件编译、平台兼容和少量代码生成但缺少类型安全、作用域和可靠的参数求值语义。现代 C 中应优先使用constexprinline模板enum classusingif constexpr仅在预处理能力不可替代时使用宏。