目录
一、预处理
在C/C++中,预处理是编译过程的第一阶段,主要由预处理器完成。预处理的主要功能是根据预处理指令对源代码进行相应的文本处理,然后输出到下一阶段。下面,我将详细介绍预处理中的三种主要操作:文件包含、宏定义和条件编译。
1.1 文件包含(#include)
文件包含是预处理中最常用的指令之一,它允许程序员在一个源文件中包含另一个文件的内容。这通常用于包含头文件(.h
或 .hpp
),这些头文件通常包含函数声明、类定义、宏定义等。文件包含有两种形式:
- 尖括号形式 (
#include <filename>
): 用于包含标准库头文件或系统头文件。预处理器会在标准库路径中查找这些文件。 - 双引号形式 (
#include "filename"
): 用于包含用户自定义的头文件。预处理器首先在当前目录或指定的项目目录中查找这些文件,如果找不到,则可能回退到标准库路径中查找。
文件包含的主要目的是实现代码的模块化,避免重复编写相同的代码,同时提高代码的可维护性和可重用性。
1.2 宏定义(#define)
宏定义是预处理的另一个重要功能,它允许程序员定义常量或宏函数。宏定义在预处理阶段进行文本替换,不占用任何存储空间,也不分配内存地址。
1.2.1 常量宏定义
常量宏定义通常用于表示一个不会改变的值。例如:
#define PI 3.14159
在代码中,每当出现PI
时,预处理器都会将其替换为3.14159
。
1.2.2 宏函数
宏函数类似于函数,但它实际上只是文本替换,没有函数调用的开销。例如:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
使用宏函数时,要注意参数周围的括号,以避免出现运算优先级和结合性错误。
1.2.3 宏取消
宏取消使用#undef
预处理指令。它用于取消之前定义的宏,使该宏在后续代码中不再可用。
例如:
#define DEBUG 1
// ... 一些使用DEBUG宏的代码 ...
#undef DEBUG
在这个例子中,DEBUG
宏最初被定义并赋值为1
。然后,在代码中使用DEBUG
宏进行条件编译或其他操作。最后,使用#undef DEBUG
取消DEBUG
宏的定义,以确保在后续代码中不会意外地使用到它。
宏定义可以提高代码的可读性和可维护性,但也要注意避免过度使用宏定义,因为宏只是简单的文本替换,没有类型检查,可能会导致一些不易察觉的错误。
注意事项:
- 没有类型检查:宏只是简单的文本替换,因此预处理器不会检查类型是否正确。
- 多次求值:如果宏的参数在替换后的代码中多次出现,那么它会被多次求值,这可能导致不期望的副作用。
- 括号问题:在定义带参数的宏时,必须注意参数的括号问题,以确保宏替换后的表达式具有正确的优先级和结合性。
- 作用域:宏定义在定义它的文件以及包含该文件的任何文件中都可见,直到它被
#undef
取消或到达文件的末尾。因此,要小心避免在不同文件中定义同名宏而导致的冲突。
1.3 条件编译
条件编译允许程序员根据特定的条件决定是否包含某段代码。这在跨平台编程、调试代码或根据不同的配置选项构建程序时非常有用。
条件编译的主要预处理指令包括:
#if
: 判断后面的条件是否为真,如果为真则编译下面的代码,直到遇到#else
、#elif
或#endif
。#ifdef
: 判断是否定义了某个宏,如果已定义则编译下面的代码。#ifndef
: 与#ifdef
相反,判断某个宏是否未定义。#else
: 与#if
、#ifdef
或#ifndef
配对使用,当前面的条件不满足时,编译#else
后面的代码。#elif
: 类似于#else if
,用于提供额外的条件判断。#endif
: 结束条件编译块。
例如:
#ifdef DEBUG
// 这段代码只在定义了DEBUG宏时才会被编译
printf("Debug information\n");
#endif
条件编译使得程序员能够灵活地控制哪些代码应该被编译和执行,从而满足不同的需求和环境。
1.4 预处理使用例子
条件编译:应用在条件编译中的条件表达式不必加括号。
1.4.1 extern "C" 块的应用
#ifdef __cplusplus
extern "C" {
#endif
/* Assume C declarations for C++ */
#ifdef __cplusplus
}
#endif
__cplusplus是C++的预定义宏,表示当前开发环境是C++。在C++语言中,为了支持重载机制,在编译生成的汇编代码中,会对函数名字进行进行一些处理(通常称为函数名字改编),如加入函数的参数类型或返回类型等,而在C语言中,只是简单的函数名字而已,并不加入其它信息:
void show(int demo);
void show(double demo);
C无法区分上面两个函数的不同,因为C编译器产生的函数名都是 _show,而C++编译器产生的名字则可能是 _show_Fi 和 _show_Fd,这样就很好地把函数区别开了。
所以,在C/C++混合编程的环境下, extern "C" 块的作用就是告诉C++编译器这段代码要按C标准编译,以尽可能地保持C++与C的兼容。
再比如说用C++开发了一个DLL库,为了能够让C语言也能够调用该DLL输出(Export)的函数,需要用extern "C"来强制编译器不要修改你的函数名。
1.4.2 选择编译
有时候程序中的某些调试代码,只需要在调试的时候被编译,而不希望在程序的发行版中被编译,你可能会看到类似这样的代码段:
#ifdef _DEBUG_
printf("File: %s, Line: %d | x = %d\n", __FILE__, __LINE__, x);
#else
printf("x = %d\n", x);
#endif
当符号 _DEBUG_ 没有被定义的时候,仅编译 #else 与 #endif 之间的代码;定义了 _DEBUG_ 符号之后,只编译 #ifdef 与 #else 之间的代码。要想定义一个符号很简单,只需要在文件头部加上像这样的一条语句:
#define _DEBUG_
1.4.3 注释大段代码
注释大段代码可以使用/*--*/,但不支持嵌套注释,有时容易出错。通常忽略大段代码的正确方法是使用条件编译指令,示例代码如下:
#if 0
......
......
#endif
1.4.4 避免头文件被重复引用
头文件通常包含有声明及定义语句,头文件还要以包含其它的头文件,即头文件嵌套,因此需要注意“重复引用”的情况,因为某些声明或定义如typedef,在一次编译中仅允许出现一次,重复包含会导致编译器提示出错信息。头文件嵌套如:
//a.h
#include "b.h"
//main.cpp
#include "a.h"//已经包含了b.h
#include "b.h"
技巧是定义一个与头文件相关的宏定义符号,如果预处理器已经见过这个符号,那么就不会重复嵌入文件的内容:
// b.h
#ifndef __B_H__
#define __B_H__
<定义或声明>
#endif
二、命名空间
2.1 命名空间介绍
在C++中,预处理器指令、静态变量和函数等特性可能会引起命名冲突,因为它们在编译时会直接插入到代码中,而不会考虑命名空间。然而,命名空间提供了一种机制来解决这种问题,通过将相关的标识符(如变量、函数、类等)组织在同一个命名空间中,可以避免命名冲突。
命名空间是一个逻辑上的类型组织系统,它允许你对程序中的类型进行逻辑上的分组,并使定义在同一个命名空间中的类可以直接相互调用。标准的C++有命名空间机制,使用namespace
关键字来定义命名空间。库或程序中的每一个C++定义集都被封装在一个命名空间中,如果其他的定义中有相同的名字,但它们在不同的命名空间,则不会产生命名冲突。
命名空间的主要优点是它可以避免全局作用域中的命名冲突,并提供了一种组织和管理大型代码库的方法。通过使用命名空间,你可以将相关的代码和标识符封装在一个特定的上下文中,这样可以减少命名冲突的可能性,并使代码更易于理解和维护。
2.2 需要命名空间的原因
在C++中,命名空间是ANSI C++引入的一种机制,它允许用户创建可以由用户命名的作用域,以处理程序中常见的同名冲突。这种机制在C语言中已经存在,但C++在此基础上进行了扩展,引入了类作用域。
在C语言中,定义了三个层次的作用域:文件(编译单元)、函数和复合语句。在C++中,除了这三个作用域外,还引入了类作用域。类作用域是出现在文件内的,这意味着在一个文件中可以定义多个类,每个类都有自己的作用域。
在不同的作用域中,可以定义相同名字的变量,这些变量在各自的作用域内是互不干扰的,系统能够区分它们。这就是命名空间的基本概念。通过使用命名空间,可以创建一个独立的命名空间,在这个命名空间中定义的变量、函数、类等在该命名空间内是唯一的,不会与其他命名空间中的同名标识符发生冲突。
2.3 命名空间的定义
格式:
namespace 命名空间名{
//命名空间成员(其他命名空间或类的定义)
}
命名空间定义说明:
-
命名空间只能在全局范围内定义。
-
“命名空间名”是C++合法的标识符,也可以是用作用域分解运算符(::)来构成命名空间的完全限定名。可以没有命名空间名(无名的命名空间),其成员的名称不需要限定就可以使用。
-
不允许在定义命名空间时使用任何访问修饰符,系统默认命名空间具有public访问属性。
-
命名空间的成员可以是另一个命名空间(嵌套)或类型(类、结构、接口、枚举等)的定义。
-
一个命名空间可以在多个头文件中用一个标识符来定义。
-
一个命名空间的名字可以用另一个名字来作它的别名。
-
不能像类那样去创建一个命名空间的实例。
-
可以在一个编译单元中包含一个未命名的命名空间,但只能包含一个未命名的命名空间。
-
命名空间在命名空间的结尾,右花括号的后面可以跟一个分号(;)。
代码示例:
#include<iostream>
using namespace std;
//命名空间Outer的定义
namespace Outer {
int nNumA;
//子命名空间Inner的内部定义
namespace Inner {
void fun_a() { nNumA++; } //Outer::nNumA
int nNumA;
void fun_b() { nNumA++; } //Inner::nNumA
void fun_c();
}
void fun_c();
//namespace Inner2;//错误,不能声明了命名空间
}
void Outer::fun_c() { nNumA--; }//命名空间outer成员fun_c()的外部定义
void Outer::Inner::fun_c() { nNumA--; }//命名空间Inner成员fun_c()的外部定义
//namespace Outer::Inner2 {/**/ }//错误,不能在外部定义子命名
int main() {
cout << "hello" << endl;
system("pause");
return 0;
}
2.4 使用命名空间和别名
2.4.1 使用
访问一个命名空间中的成员可以采用下面三种方法:
A. 直接访问命名空间的成员(完全限定名):用作用域分解运算符(:),例如:
std::cout<<"hello world!"<<std::endl;
缺点:输入繁琐
B. 使用声明:用using声明一次性引用名字,例如:
using std::cout;
cout<<"hello world!"<<std: :endl;
缺点:缺乏类型信息,不能克服重载问题
C. 使用指令:用using指令把所有名字引入到命名空间中,例如:
using namespace std;
cout<< "hello world!"<<endl;
缺点:可能会产生命名冲突
2.4.2 别名
标准C++引入命名空间,主要是为了避免成员的名称冲突。若果用户都给自己的命名空间取简短的名称,那么这些(往往同是全局级的)命名空间本身,也可能发生名称冲突。如果为了避免冲突,而为命名空间取很长的名称,则使用起来就会不方便。这是一个典型的两难问题。
标准C++为此提供了一种解决方案--命名空间别名,格式为
namespace 别名 = 命名空间名;