跳转到内容

C++ 编程

来自维基教科书,开放的书籍,为一个开放的世界

预处理器

[编辑 | 编辑源代码]

预处理器是一个独立的程序,由编译器调用,或者作为编译器本身的一部分。它执行中间操作,在编译器尝试编译生成的源代码之前修改原始源代码和内部编译器选项。

预处理器解析的指令称为指令,有两种形式:预处理器指令和编译器指令。预处理器指令指示预处理器如何处理源代码,而编译器指令指示编译器如何修改内部编译器选项。指令用于简化编写源代码(例如,使其更具可移植性),并使源代码更易于理解。它们也是使用 C++ 标准库提供的设施(类、函数、模板等)的唯一有效方法。

注意
查阅编译器/预处理器的文档,了解其如何实现预处理阶段以及任何标准未涵盖的附加功能。有关解析主题的深入信息,您可以阅读 "编译器构造" (https://wikibooks.cn/wiki/Compiler_Construction)

所有指令在行首都以 '#' 开头。标准指令为

  • #define
  • #elif
  • #else
  • #endif
  • #error
  • #if
  • #ifdef
  • #ifndef
  • #include
  • #line
  • #pragma
  • #undef

包含头文件(#include)

[编辑 | 编辑源代码]

#include 指令允许程序员将一个文件的内容包含到另一个文件中。这通常用于将多个程序部分所需的信息分离到一个单独的文件中,以便可以重复包含这些信息,而无需将所有源代码重新键入到每个文件中。

C++ 通常要求您在使用之前声明要使用的内容。因此,名为头文件的文件通常包含要使用的声明,以便编译器能够成功编译源代码。这在本书的文件组织部分中有更详细的说明。标准库(包含在每个符合标准的 C++ 编译器中的代码库)和第三方库使用头文件,以便将必要的声明包含在您的源代码中,允许您使用语言本身不包含的功能或资源。

任何源文件的第一行通常应该类似于以下内容

#include <iostream>
#include "other.h"

以上代码行导致文件iostreamother.h 的内容被包含,以供您的程序使用。通常,这是通过将iostreamother.h 的内容插入到您的程序中来实现的。当在指令中使用尖括号 (<>) 时,预处理器会收到指令,在编译器特定的位置搜索指定的文件。当使用双引号 (" ") 时,预处理器预计将在一些额外的、通常是用户定义的位置搜索头文件,并且如果在这些额外位置中找不到头文件,则返回到标准包含路径。通常,当使用这种形式时,预处理器还会在包含#include指令的文件所在的同一目录中搜索。

Theiostream头文件包含使用称为的 I/O 机制抽象进行输入/输出 (I/O) 的各种声明。例如,有一个名为std::cout(其中“cout”是“控制台输出”的缩写)的输出流对象,它用于将文本输出到标准输出,这通常会在计算机屏幕上显示文本。

注意
包含标准库时,允许编译器在给定名称的头文件实际上是否存在为物理文件,还是仅仅是一个逻辑实体,导致预处理器修改源代码,并具有与实体存在为物理文件时相同的最终结果,方面做出例外。查阅您的预处理器/编译器的文档,了解任何特定于供应商的 #include 指令实现以及标准和用户定义头文件的特定搜索位置。这会导致可移植性问题和混乱。

下面列出了标准 C++ 头文件列表


标准模板库

以及

标准 C 库
  1. a b c d e f g h i j k l m n o p q r s t u v 仅在 C++11 中

C++ 标准库中的所有内容都包含在std:命名空间中。

旧的编译器可能会包含带有.h后缀的头文件(例如,非标准的<iostream.h>与标准的<iostream>相比)。这些名称在 C++ 标准化之前很常见,一些编译器仍然包含这些头文件以确保向后兼容性。与使用std:命名空间相比,这些旧的头文件会污染全局命名空间,并且可能只以有限的方式实现标准。

一些供应商使用 SGI STL 头文件。这是标准模板库的第一个实现。

非标准但比较常见的 C++ 库
  1. 基于 stdio.h 中 FILE* 的流。
  2. iostream 的前身。旧的流库主要为了向后兼容性而保留,即使在旧的编译器中也是如此。
  3. 使用 **char***,而 sstream 使用 string。建议使用标准库 sstream。


注意
在头文件标准化之前,它们被呈现为分离的文件,比如 <iostream.h> 等等。这可能仍然是旧的(不符合标准的)编译器的要求,但更新的编译器将接受这两种方法。标准中也没有要求头文件必须以文件形式存在。将标准库引用为独立文件的旧方法已经过时了。

#pragma

[edit | edit source]

pragma(实用信息)指令是标准的一部分,但任何 pragma 指令的含义都取决于所使用的标准软件实现。

Pragma 指令在源程序中使用。

#pragma token(s)

你应该查看要使用的 C++ 标准的软件实现,以获取支持的令牌列表。

例如,最广泛使用的预处理器 pragma 指令之一,#pragma once,当放在头文件的开头时,表示如果预处理器多次包含该文件,则会跳过该文件。

注意
还存在另一种方法,通常称为包含守卫,它提供相同的功能,但使用其他包含指令。

在 GCC 文档中,#pragma once 被描述为一个过时的预处理器指令。

C++ 预处理器包含定义“宏”的功能,这大致意味着能够将命名宏的使用替换为一个或多个令牌。这在定义简单常量(尽管在 C++ 中更常使用const来实现这一点)、条件编译、代码生成等方面有各种用途——宏是一个强大的功能,但如果不小心使用,也会导致代码难以阅读和调试!

注意

宏不仅取决于 C++ 标准或你的操作。它们可能由于使用了外部框架、库,甚至由于你使用的编译器和特定操作系统而存在。我们不会在这本书中介绍这些信息,但你可以在 ( http://predef.sourceforge.net/ ) 的 预定义 C/C++ 编译器宏 页面中找到更多信息,该项目维护着与编译器和操作系统无关的宏的完整列表。

#define 和 #undef
[edit | edit source]

#define 指令用于定义预处理器用来在编译源代码之前操作程序源代码的值或宏。

#define USER_MAX (1000)

#undef 指令删除当前的宏定义。

#undef USER_MAX

使用#define更改宏的定义是错误的,但使用#undef尝试取消定义当前未定义的宏名称不是错误。因此,如果你需要覆盖之前的宏定义,首先要#undef它,然后使用#define设置新的定义。

注意
由于预处理器定义是在编译器处理源代码之前进行替换的,因此由#define引入的任何错误都难以追踪。例如,使用与某些现有标识符相同的数值或宏名称会导致细微的错误,因为预处理器会替换源代码中的标识符名称。

如今,出于这个原因,#define主要用于处理编译器和平台差异。例如,一个定义可能包含一个常量,该常量是系统调用的适当错误代码。因此,应该尽量限制#define的使用,除非绝对必要;typedef 语句、常量变量、枚举、模板和 内联函数 通常可以更有效、更安全地完成相同目标。

按照惯例,使用#define定义的值用大写字母和 "_" 分隔符命名,这使读者清楚地知道该值是不可改变的,在宏的情况下,该结构需要小心。尽管这样做不是必须的,但这样做被认为是极其不好的做法。这允许在阅读源代码时轻松识别这些值。

尝试使用 constinline 而不是 #define

\ (行延续)
[edit | edit source]

如果由于某种原因需要将给定语句分成多行,请使用\(反斜杠)符号来“转义”行尾。例如,

#define MULTIPLELINEMACRO \
        will use what you write here \
        and here etc...

等效于

#define MULTIPLELINEMACRO will use what you write here and here etc...

因为预处理器将以反斜杠(“\”)结尾的行与它们后面的行连接起来。即使在处理指令(如 #define)之前也会发生这种情况,因此它几乎适用于所有目的,而不仅仅是用于宏定义。反斜杠有时被认为是换行的“转义”字符,它改变了换行的解释。

在某些(相当罕见)情况下,宏在跨多行分割时可能更具可读性。好的现代 C++ 代码只会谨慎地使用宏,因此对多行宏定义的需求不会经常出现。

当然有可能过度使用此功能。例如,编写以下代码是完全合法的,但完全站不住脚

int ma\
in//ma/
()/*ma/
in/*/{}

但这是一种对该功能的滥用:虽然转义的换行符可以出现在令牌的中间,但永远不应该有任何理由在那里使用它。不要尝试编写看起来像是属于国际混淆 C 代码竞赛的代码。

警告:使用转义换行符有时会出现一个“陷阱”:如果反斜杠之后有任何不可见字符,行将不会连接,并且几乎肯定会在以后产生错误消息,尽管它可能并不明显是什么原因导致的。

类函数宏
[edit | edit source]

#define 命令的另一个功能是它可以接受参数,使其作为伪函数创建者相当有用。考虑以下代码

#define ABSOLUTE_VALUE( x ) ( ((x) < 0) ? -(x) : (x) )
// ...
int x = -1;
while( ABSOLUTE_VALUE( x ) ) {
// ...
}

注意

通常使用额外的括号来包装宏参数是一个好主意,它可以避免参数以意外的方式解析。但有一些例外情况需要考虑

  1. 由于逗号运算符的优先级低于任何其他运算符,这消除了出现问题的可能性,因此不需要额外的括号。
  2. 使用 ## 运算符连接令牌,使用 # 运算符转换为字符串,或者连接相邻的字符串文字时,不能单独对参数加括号。

请注意,在上面的示例中,变量“x”始终在其自己的括号内。这样,它将在整体上进行评估,然后再与 0 进行比较或乘以 -1。此外,整个宏都被括号包围,以防止它受到其他代码的污染。如果你不小心,你就有可能让编译器误解你的代码。

宏将文本中使用的每个宏参数的出现替换为宏参数的字面内容,而不会进行任何验证检查。编写不当的宏会导致代码无法编译或产生难以发现的错误。由于存在副作用,因此使用上面描述的宏函数被认为是一个非常糟糕的主意。但是,与任何规则一样,可能存在宏是实现特定目标的最有效方法的情况。

int z = -10;
int y = ABSOLUTE_VALUE( z++ );

如果 ABSOLUTE_VALUE() 是一个真正的函数,“z”现在将具有 -9 的值,但因为它是在宏中作为参数使用的,所以z++ 被扩展了 3 次(在这种情况下),因此(在这种情况下)执行了 2 次,将 z 设置为 -8,并将 y 设置为 9。在类似的情况下,很容易编写具有“未定义行为”的代码,这意味着它所做的事情在 C++ 标准看来是完全不可预测的。

// ABSOLUTE_VALUE( z++ ); expanded
( ((z++) < 0 ) ? -(z++) : (z++) );


注意
使用 GCC 编译器扩展称为“语句表达式”(不是标准 C++),允许在表达式中使用语句,请查阅编译器手册以了解其他注意事项,这样就可以只评估它一次

#define ABSOLUTE_VALUE( x ) ( { typeof (x) temp = (x); (temp < 0) ? -temp : temp; } )

使用内联模板函数可以作为宏的替代方法,消除了宏参数内部副作用的问题。

通常最好避免使用特定于编译器的扩展,除非计划使用依赖关系。

// An example on how to use a macro correctly

#include <iostream>
 
#define SLICES 8
#define PART(x) ( (x) / SLICES ) // Note the extra parentheses around '''x'''
 
int main() {
   int b = 10, c = 6;
   
   int a = PART(b + c);
   std::cout << a;
   
   return 0;
}

--“a”的结果应该是“2”(将 b + c 传递给 PART -> ((b + c) / SLICES) -> 结果是“2”)

注意

可变参数宏
可变参数宏是预处理器的功能,通过该功能,可以声明宏以接受可变数量的参数(类似于可变参数函数)。

它们目前不是 C++ 编程语言的一部分,尽管许多最新的 C++ 实现都支持可变参数宏作为扩展(即:GCC、MS Visual Studio C++),并且预计可变参数宏可能会在以后添加到 C++ 中。

可变参数宏是在 1999 年的 C 编程语言标准 ISO/IEC 9899:1999(C99)修订版中引入的。

# 和 ##
[edit | edit source]

### 运算符与#define宏一起使用。使用 # 会导致 # 后的第一个参数作为带引号的字符串返回。例如

#define as_string( s ) # s

将使编译器将

std::cout << as_string( Hello  World! ) << std::endl;

变成

std::cout << "Hello World!" << std::endl;

注意
观察来自#参数的前导和尾随空格被删除,令牌之间的连续空格序列被转换为单个空格。

使用 #### 之前的部分与之后的部分连接起来;结果必须是格式良好的预处理令牌。例如

#define concatenate( x, y ) x ## y
...
int xy = 10;
...

将使编译器将

std::cout << concatenate( x, y ) << std::endl;

变成

std::cout << xy << std::endl;

当然,这将显示10到标准输出。

不能使用 ## 连接字符串文字,但好消息是这不是问题:只需编写两个相邻的字符串文字就足以使预处理器连接它们。

宏的危险
[edit | edit source]

为了说明宏的危险,请考虑这个简单的宏

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

以及代码

i = MAX(2,3)+5;
j = MAX(3,2)+5;

看一下这个,并考虑执行后的值可能是多少。语句变成了

 
int i = 2>3?2:3+5;
int j = 3>2?3:2+5;

因此,执行后i=8j=3而不是预期的结果i=j=8!这就是为什么之前建议你使用额外的括号,但即使使用这些括号,道路也充满危险。警觉的读者可能会很快意识到,如果a,b包含表达式,则定义必须在宏定义中对每个a,b的使用加括号,如下所示

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

这有效,前提是a,b没有副作用。实际上,

 
 i = 2;
 j = 3;
 k = MAX(i++, j++);

将导致k=4, i=3j=5。这对于任何期望MAX()像函数一样工作的任何人来说都是非常令人惊讶的。

那么正确的解决方案是什么?解决方案是不使用宏。一个全局的内联函数,像这样

inline int max(int a, int b) { return a>b?a:b }

没有上面提到的任何缺点,但不能与所有类型一起使用。模板(见下文)可以解决这个问题

template<typename T> inline max(const T& a, const T& b) { return a>b?a:b }

实际上,这是 STL 库中用于 std::max() 的定义(其变体)。该库包含在所有符合标准的 C++ 编译器中,因此理想的解决方案是使用它。

std::max(3,4);

使用宏的另一个危险是它们被排除在类型检查之外。在 MAX 宏的情况下,如果与字符串类型变量一起使用,它不会生成编译错误。

MAX("hello","world")

因此,最好使用内联函数,它将进行类型检查。如果内联函数按上述方式使用,则允许编译器生成有意义的错误消息。

字符串文字连接

[edit | edit source]

预处理器的一个次要功能是将字符串连接在一起,“字符串文字连接”--将代码

std::cout << "Hello " "World!\n";

变成

std::cout << "Hello World!\n";

除了晦涩的使用之外,这在编写长消息时最有用,因为正常的 C++ 字符串文字不允许在源代码中跨越多行(即,在其中包含换行符)。对此的例外是 C++11 原始字符串文字,它可以包含换行符,但不会解释任何转义字符。使用字符串文字连接也有助于将程序行保持在合理的长度内;我们可以编写

 function_name("This is a very long string literal, which would not fit "
               "onto a single line very nicely -- but with string literal "
               "concatenation, we can split it across multiple lines and "
               "the preprocessor will glue the pieces together");

请注意,这种连接发生在编译之前;编译器只看到一个字符串文字,并且在运行时没有做任何工作,即,你的程序不会因为这种字符串的连接而运行得更慢。

连接也适用于宽字符串文字(以 L 为前缀)

 L"this " L"and " L"that"

被预处理器转换为

 L"this and that".

注意
为了完整起见,请注意 C99 针对此操作与 C++98 具有不同的规则,并且 C++0x 似乎几乎肯定会匹配 C99 更宽容的规则,这些规则允许将窄字符串文字连接到宽字符串文字,这是 C++98 中无效的操作。

条件编译

[edit | edit source]

条件编译主要用于两个目的

  • 允许在编译程序时启用/禁用某些功能
  • 允许以不同方式实现功能,例如在不同平台上编译时

它有时也用于临时“注释掉”代码,尽管使用版本控制系统通常是更有效的方法。

  • 语法:
#if condition
  statement(s)
#elif condition2
  statement(s)
...
#elif condition
  statement(s)
#else
  statement(s)
#endif

#ifdef defined-value
  statement(s)
#else
  statement(s)
#endif

#ifndef defined-value
  statement(s)
#else
  statement(s)
#endif

#if 指令允许对预处理器值进行编译时条件检查,例如使用 #define 创建的预处理器值。如果condition 非零,则预处理器将包括所有statement(s),直到输出中处理的 #else#elif#endif 指令。否则,如果 #if condition 为假,则将按顺序检查所有 #elif 指令,第一个为真的condition 将在其输出中包含其statement(s)。最后,如果 #if 指令的condition 和所有存在的 #elif 指令都为假,则如果存在,#else 指令的statement(s) 将被包含在输出中;否则,将不包含任何内容。

在 **#if** 后使用的表达式可以包含布尔和整数常量以及算术运算,以及宏名称。允许的表达式是 C++ 表达式完整范围的子集(有一个例外),但足以满足许多目的。**#if** 可用的一个额外运算符是 **defined** 运算符,它可以用来测试是否当前定义了给定名称的宏。

#ifdef 和 #ifndef
[编辑 | 编辑源代码]

**#ifdef** 和 **#ifndef** 指令是 '#if defined(defined-value)' 和 '#if !defined(defined-value)' 的简写形式。**defined**(identifier) 在预处理器评估的任何表达式中都有效,如果用 #define 定义了名称为 identifier 的预处理器变量,则返回真(在此上下文中,等效于 1),否则返回假(在此上下文中,等效于 0)。事实上,圆括号是可选的,也可以在没有圆括号的情况下写 **defined** identifier

(可能 **#ifndef** 最常见的用法是创建头文件的“包含保护”,以确保头文件可以安全地包含多次。这将在头文件部分进行解释。)

**#endif** 指令结束 **#if**、**#ifdef**、**#ifndef**、**#elif** 和 **#else** 指令。

  • 示例:
 #if defined(__BSD__) || defined(__LINUX__)
 #include <unistd.h>
 #endif

这可以用来例如提供多个平台支持,或者为不同的程序版本设置一个通用的源文件集。另一个使用示例是使用它代替(非标准的)**#pragma once**。

  • 示例:

foo.hpp

 #ifndef FOO_HPP
 #define FOO_HPP
 
  // code here...
 
 #endif // FOO_HPP

bar.hpp

 #include "foo.h"
 
  // code here...

foo.cpp

 #include "foo.hpp"
 #include "bar.hpp"
 
  // code here

当我们编译 **foo.cpp** 时,由于使用了包含保护,只会包含一份 **foo.hpp**。当预处理器读取行 #include "foo.hpp" 时,将扩展 **foo.hpp** 的内容。由于这是第一次读取 **foo.hpp**(并且假设不存在宏 **FOO_HPP** 的现有声明),**FOO_HPP** 尚未声明,因此代码将正常包含。当预处理器在 foo.cpp 中读取行 #include "bar.hpp" 时,将按常例扩展 **bar.hpp** 的内容,并且文件 **foo.h** 将再次扩展。由于之前声明了 **FOO_HPP**,因此不会插入 **foo.hpp** 中的任何代码。因此,这可以实现我们的目标 - 避免文件内容被包含超过一次。

编译时警告和错误

[编辑 | 编辑源代码]
  • 语法:
 #warning message
 #error message
#error 和 #warning
[编辑 | 编辑源代码]

**#error** 指令会导致编译器停止并输出遇到它时的行号和消息。**#warning** 指令会导致编译器输出遇到它时的行号和消息的警告。这些指令主要用于调试。

注意
**#error** 是标准 C++ 的一部分,而 **#warning** 则不是(尽管它得到了广泛支持)。

  • 示例:
 #if defined(__BSD___)
 #warning Support for BSD is new and may not be stable yet
 #endif
 
 #if defined(__WIN95__)
 #error Windows 95 is not supported
 #endif

源文件名和行号宏

[编辑 | 编辑源代码]

可以使用预定义的宏 __FILE__ 和 __LINE__ 来检索正在执行预处理的当前文件名和行号。行号是在任何转义换行符被移除之前测量的。可以使用 **#line** 指令覆盖 __FILE__ 和 __LINE__ 的当前值;在手写代码中这样做很少是合适的,但对于根据其他输入文件创建 C++ 代码的代码生成器来说很有用,这样(例如)错误消息将参考原始输入文件而不是生成的 C++ 代码。

华夏公益教科书