首页 > 编程语言 >C++中的异类:“#” 符号背后的故事

C++中的异类:“#” 符号背后的故事

时间:2024-05-28 11:04:02浏览次数:23  
标签:符号 代码 C++ 编译 指令 处理器 pragma 异类

最近在写编程语言的书,聊到C++的宏,感觉很有意思,搬运过来。

在C++语言中,# 符号是一个独特的符号。它似乎不在语言核心中,但是在源码里却又无处不在。在语法上,#的语法规则在C++体系里独具一格,和C++语法相比像是两个语言似的。这些差别让我们感受到#背后的故事不简单。

今天,我们一起探讨 # 在C++语言中的所有作用和功能,并思考其设计的优缺点,以及背后的历史渊源。

#的核心功能:预处理器指令

C++预处理器指令是在编译器处理源代码之前执行的一系列指令。一般认为它们不是C++语言的一部分,因为这些语法由预处理器解释和执行,并非编译器。

预处理器指令以 # 开头,常见的指令有:

#include 包含文件

#include 指令用于将一个文件的内容包含到当前文件中。有两种形式:

  • #include <filename>:从标准库或系统目录中包含文件。
  • #include "filename":从当前目录或用户指定的路径中包含文件。

#define 定义宏

#define 指令用于定义宏。宏可以是简单的文本替换,也可以是参数化的宏。例如:

#define PI 3.14159
#define SQUARE(x) ((x) * (x))

宏在代码中被展开,以实现代码的简洁和可读性,但使用不当也可能导致难以调试的错误。

# if 条件编译指令

条件编译指令用于控制哪些部分的代码将被编译。常用的指令包括:

  • #if:如果条件为真,编译其后的代码。
  • #ifdef:如果宏被定义,编译其后的代码。
  • #ifndef:如果宏未被定义,编译其后的代码。
  • #else:与 #if 或 #ifdef 配合使用,条件不成立时编译其后的代码。
  • #elif:else if 的简写形式。
  • #endif:结束条件编译块。
#ifdef DEBUG
    #include <iostream>
    #define LOG(x) std::cout << x << std::endl
#else
    #define LOG(x)
#endif

其他与 # 相关的指令和概念

除了前面讨论的主要预处理器指令,C++还包含其他与 # 相关的指令和概念:

#error

#error 指令用于在编译过程中生成自定义的错误消息。例如:

#ifndef CONFIG_H
    #error "必须包含 config.h"
#endif

当编译器遇到 #error 指令时,会停止编译并显示指定的错误消息。这对于确保特定条件满足时(例如某个必要的头文件已被包含)非常有用。

#line

#line 指令用于改变预处理器报告的行号和文件名。它通常用于调试或生成代码的工具。例如

#line 100 "newfile.cpp"
void someFunction() {
    // 在编译错误报告中,这里显示为 newfile.cpp 的第100行
}

这在生成代码或需要准确的错误报告位置时非常有用。

字符串化操作符 #

字符串化操作符 # 用于将宏参数转化为字符串。例如:

#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)

使用 STRINGIFY 宏时,会将传入的参数转化为一个字符串:

const char* str = TOSTRING(Hello World); // 结果是 "Hello World"

符号拼接操作符 ##

符号拼接操作符 ## 用于将两个标记连接成一个标记。例如:

#define CONCAT(a, b) a##b

使用 CONCAT 宏时,会将两个参数连接在一起:

int CONCAT(my, Variable) = 42; // 结果是 int myVariable = 42;

#pragma

#pragma 指令是编译器特定的指令,用于向编译器提供特定的指令或启用特定的功能。不同的编译器支持不同的 #pragma 指令。常见的 #pragma 指令包括:

  • #pragma once:用于防止头文件被多次包含。它比传统的包含保护更简洁。
  • #pragma pack:用于改变结构体的对齐方式。
#pragma pack(push, 1)
struct MyStruct {
    char a;
    int b;
};
#pragma pack(pop)
  • #pragma warning:用于控制编译器警告的显示。
#pragma warning(disable: 4996) // 禁用特定警告

#pragma 的更多用法

除了之前提到的 #pragma once, #pragma pack, 和 #pragma warning,还有许多其他有用的 #pragma 指令:

#pragma region 和 #pragma endregion:用于在代码中定义一个折叠区域,提供自定义代码结构化折叠结构,提高代码清晰度。

#pragma region 初始化代码
void initialize() {
    // 初始化代码
}
#pragma endregion

#pragma message:用于在编译过程中生成自定义消息。

#pragma message("编译进行中...")

#pragma comment:MSVC编译器特有的指令,用于插入编译器指令。例如,可以用来链接库文件:

#pragma comment(lib, "user32.lib")

预处理器指令的历史渊源

C++预处理器指令的概念源自于C语言,C语言的预处理器由Dennis Ritchie和Brian Kernighan在1970年代早期开发。

在C语言之前,已经存在一些编程语言和工具使用了类似于预处理器的概念,以宏处理器(Macro Processor)为代表,用于提高代码的可重用性和简化编译过程。

宏处理器的历史可以追溯到1950年代和1960年代,用于汇编语言和早期的高级编程语言。宏处理器是一种文本替换工具,允许程序员定义宏,并在代码中使用这些宏进行文本替换。IBM的汇编语言(如IBM 7090和IBM 360汇编语言)中使用了宏处理器来简化复杂的汇编代码。

例如,IBM的7090汇编器中引入了名为“FAP (FORTRAN Assembly Program)”的宏处理器,它允许程序员定义宏并在汇编代码中使用:

MACRO
ADD1 &ARG
    LDA &ARGADD ONE
    STA &ARG
MEND

这样的宏处理器概念在早期的编程实践中非常普遍,因为当时编译理论尚未完善,基于字符串替换的宏系统在实现上具有简单高效的特点,因此在早期的语言中,宏是主要的提高代码复用的手段。

除了宏处理器外,其他一些早期编程语言和工具也包含了类似预处理器的概念。例如:

  • PL/I:PL/I是一种在1960年代开发的编程语言,支持宏扩展和条件编译。PL/I的宏功能允许程序员定义复杂的宏,并在编译过程中进行扩展和替换。
  • M4:M4是一种通用的宏处理器,最早由AT&T贝尔实验室开发。M4可以用于各种编程语言的预处理,并且在许多Unix系统中作为标准工具存在。M4的功能非常强大,支持嵌套宏、参数化宏和条件宏等特性。
  • Lisp:Lisp语言的宏系统也是一种强大的预处理工具。Lisp宏允许程序员在编译时生成和转换代码,提供了高度的灵活性和可扩展性。Lisp宏系统的设计深刻影响了后来的编程语言宏系统的发展。

C语言是第一个系统化使用#符号来标识预处理器指令的编程语言。在C语言的时代,其他语言往往将#用于单行注释。

预处理器的优缺点

优点

  1. 代码重用性:通过 #include 指令,可以实现代码文件的重用,避免重复编写相同的代码。
  2. 条件编译:通过条件编译指令,可以根据不同的编译环境或需求,选择性地编译代码,提高代码的灵活性。
  3. 宏定义:使用宏定义可以简化代码,提高代码的可读性和可维护性。例如,用宏定义常量值或简化复杂的表达式。

缺点

  1. 调试困难:由于预处理器在编译之前处理代码,宏展开后的代码可能变得复杂且难以调试。特别是当使用大量的宏定义和条件编译时,理解和追踪代码的实际执行路径可能变得困难。
  2. 编写困难:宏定义本质上是文本替换,缺乏类型检查和语法检查。遇到复杂的宏的定义和使用上的问题时,编译器的报错往往不便于阅读。
  3. 可读性差:过度使用宏定义和条件编译指令可能导致代码的可读性下降,特别是对于不熟悉预处理器指令的开发者来说,理解代码变得更加困难。
  4. 命名冲突:宏定义没有作用域限制,可能会导致命名冲突。不同的宏可能会在不同的文件中定义相同的名字,从而引发难以追踪的错误。
  5. 性能影响:虽然预处理器指令本身不会直接影响运行时性能,但它们可能会导致编译时间增加,特别是在处理大量的宏展开和条件编译时。例如,大量的 #include 指令可能导致编译器需要处理大量的头文件,从而增加编译时间。

预处理器指令的最佳实践

为了充分发挥预处理器指令的优势,同时避免其带来的问题,开发者在使用预处理器指令时应遵循一些最佳实践:

  1. 尽量减少宏定义的使用:除非必要,尽量使用常量、内联函数和模板来替代宏定义。这样可以提高代码的类型安全性和可读性。
  2. 使用命名空间防止命名冲突:在定义宏时,使用前缀或命名空间来防止命名冲突。例如,可以使用模块名称作为前缀。
  3. 控制条件编译的复杂性:避免在同一文件中使用过多的条件编译指令。可以将不同平台或环境的代码分离到不同的文件中,通过条件编译包含特定的文件。
  4. 注释和文档:为宏定义和条件编译指令添加详细的注释和文档,帮助其他开发者理解代码的意图和使用方法。

从预处理器到现代C++

预处理器指令的出现有着特定的历史背景。早期的编程语言,通过预处理器提供了灵活的宏定义、文件包含和条件编译功能。然而,随着编程语言的发展,宏的缺点也逐渐显现。因此,开发者们开始寻找一种既能提供编译器灵活特性,又能避免宏系统缺点的解决方案。

  • C++98引入了模板。C++的模板机制引入了参数化类型和编译期多态,增强了代码复用性和类型安全性。模板元编程进一步扩展了模板的应用范围,使得编译期计算和代码生成成为可能。
  • constexpr 关键字在C++11中的引入,为编译期运算提供了一种更安全和可读的方式。constexpr 允许开发者在编译期执行函数和表达式,确保代码的高效性和正确性。
  • C++20引入了模块特性,用于代替传统的 #include 指令。模块不仅解决了头文件多重包含和编译时间长的问题,还提供了更好的封装和命名空间管理,增强了代码的可维护性和可扩展性。

随着这些特性的引入,预处理器指令的应用范围逐渐缩小,如今主要用于条件编译和少部分的代码生成方面。尽管预处理器的角色有所减弱,但它依然在特定场景下发挥着重要作用。宏的历史,见证了编程语言从灵活性到安全性、可读性的不断演进。

标签:符号,代码,C++,编译,指令,处理器,pragma,异类
From: https://blog.csdn.net/hebhljdx/article/details/139259837

相关文章

  • c++函数指针
     c/c++函数指针的用法【目录】基本定义c函数指针使用举例c++函数指针使用举例函数指针作为函数参数函数指针作为函数返回值函数指针数组typedef简化函数指针操作 c语言函数指针的定义形式:返回类型 (*函数指针名称)(参数类型,参数类型,参数类型,…);c++函数指针......
  • lambda表达式的用例 c++
    出自:  https://blog.csdn.net/qq_45604814/article/details/132687858一、Lambda表达式概述1.介绍Lambda表达式是C++11标准引入的一种特性,它提供了一种方便的方式来定义匿名函数。Lambda表达式是一种能够捕捉外部变量并使用它们的函数对象。由捕获列表、参数列表、返......
  • 打开编程世界 跟着Mr.狠人一起学C/C++
    打开编程世界跟着Mr.狠人一起学C/C++自我介绍大家好,我是Mr.狠人。我高中就读于墨尔本,学习的方向是会计,因为疫情我回到国内读大学,大学的专业是国贸,可以说我没有任何的计算机基础。但我在海外研究生阶段毅然决然的选择了计算机专业,我本可以选择金融专业,或是更简单的管理专业......
  • C++ ─── string的模拟实现
            本博客将简单实现来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。    下期我们继续讲解完整版string的模拟实现(将不再会是浅拷贝了)        说明:下述string类没有显式定义其拷贝构造函数与赋值运......
  • A Simple Problem with Integers(C++)
     【题目描述】这是一道模板题。给定数列 a[1],a[2],…,a[n] ,你需要依次进行q 个操作,操作有两类:C、lrx :给定 l,r,x ,对于所有 i∈[l,r] ,将 a[i] 加上 x (换言之,将 a[l],a[l+1],…,a[r] 分别加上 x );Q、lr :给定l,r ,求 ∑ri=la[i] 的值(换言之,求 a[l]+a[l+......
  • c++设计模式-装饰器模式和代理模式
    namespace_nmsp1{//抽象的控件类classControl{public:virtualvoiddraw()=0;//draw方法,用于将自身绘制到屏幕上。public:virtual~Control(){}//做父类时析构函数应该为虚函数};//列表控件类classListCtrl......
  • 如何在中文输入法模式下,使用英文符号?
    1、为什么有这种需求?当你使用Notion等笔记服务时,它的输入语法命令是斜杠(/),而在中文输入法场景下按健输入的是顿号(、),需要切换输入法才可以输入英文符号。因此,期待可以在中文输入法场景下,输入英文符号。减少切换的烦恼。2、如何解决?Windows自带的微软输入法支持......
  • 【C++】旋转字符串——精准与否,就是屠宰和手术的区别
    ✨题目链接:NC114旋转字符串✨题目描述 字符串旋转:给定两字符串A和B,如果能将A从中间某个位置分割为左右两部分字符串(可以为空串),并将左边的字符串移动到右边字符串后面组成新的字符串可以变为字符串B时返回true。例如:如果A=‘youzan’,B=‘zanyou’,A按‘you’‘zan’......
  • C++系列-operator new和operator delete函数
    ......
  • 图像处理之基于标记的分水岭算法(C++)
    图像处理之基于标记的分水岭算法(C++)文章目录图像处理之基于标记的分水岭算法(C++)前言一、基于标记点的分水岭算法应用1.实现步骤:2.代码实现总结前言传统分水岭算法存在过分割的不足,OpenCV提供了一种改进的分水岭算法,使用一系列预定义标记来引导图像分割的定义方式......