挑战一个月基本掌握C++(第十二天)了解命名空间,模板,预处理器

一 命名空间

假设这样一种情况,当一个班上有两个名叫 Zara 的学生时,为了明确区分它们,我们在使用名字之外,不得不使用一些额外的信息,比如他们的家庭住址,或者他们父母的名字等等。

同样的情况也出现在 C++ 应用程序中。例如,您可能会写一个名为 xyz() 的函数,在另一个可用的库中也存在一个相同的函数 xyz()。这样,编译器就无法判断您所使用的是哪一个 xyz() 函数。

因此,引入了命名空间这个概念,专门用于解决上面的问题,它可作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。

我们举一个计算机系统中的例子,一个文件夹(目录)中可以包含多个文件夹,每个文件夹中不能有相同的文件名,但不同文件夹中的文件可以重名。

1.1 定义命名空间

命名空间的定义使用关键字 namespace,后跟命名空间的名称,如下所示:

cpp 复制代码
namespace namespace_name {
   // 代码声明
}

为了调用带有命名空间的函数或变量,需要在前面加上命名空间的名称,如下所示:

cpp 复制代码
name::code;  // code 可以是变量或函数

命名空间如何为变量或函数等实体定义范围:

cpp 复制代码
#include <iostream>
using namespace std;
 
// 第一个命名空间
namespace first_space{
   void func(){
      cout << "Inside first_space" << endl;
   }
}
// 第二个命名空间
namespace second_space{
   void func(){
      cout << "Inside second_space" << endl;
   }
}
int main ()
{
 
   // 调用第一个命名空间中的函数
   first_space::func();
   
   // 调用第二个命名空间中的函数
   second_space::func(); 
 
   return 0;
}

输出:

cpp 复制代码
Inside first_space
Inside second_space

1.2 using 指令

您可以使用 using namespace 指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。

cpp 复制代码
#include <iostream>
using namespace std;
 
// 第一个命名空间
namespace first_space{
   void func(){
      cout << "Inside first_space" << endl;
   }
}
// 第二个命名空间
namespace second_space{
   void func(){
      cout << "Inside second_space" << endl;
   }
}
using namespace first_space;
int main ()
{
 
   // 调用第一个命名空间中的函数
   func();
   
   return 0;
}

输出:

cpp 复制代码
Inside first_space

using 指令也可以用来指定命名空间中的特定项目。例如,如果您只打算使用 std 命名空间中的 cout 部分:

cpp 复制代码
using std::cout;

随后的代码中,在使用 cout 时就可以不用加上命名空间名称作为前缀,但是 std 命名空间中的其他项目仍然需要加上命名空间名称作为前缀,如下所示:

cpp 复制代码
#include <iostream>
using std::cout;
 
int main ()
{
 
   cout << "std::endl is used with std!" << std::endl;
   
   return 0;
}

输出:

cpp 复制代码
std::endl is used with std!

using 指令引入的名称遵循正常的范围规则。名称从使用 using 指令开始是可见的,直到该范围结束。此时,在范围以外定义的同名实体是隐藏的。

1.3 不连续的命名空间

命名空间可以定义在几个不同的部分中,因此命名空间是由几个单独定义的部分组成的。一个命名空间的各个组成部分可以分散在多个文件中。

所以,如果命名空间中的某个组成部分需要请求定义在另一个文件中的名称,则仍然需要声明该名称。下面的命名空间定义可以是定义一个新的命名空间,也可以是为已有的命名空间增加新的元素:

cpp 复制代码
namespace namespace_name {
   // 代码声明
}

1.4 嵌套的命名空间

命名空间可以嵌套,您可以在一个命名空间中定义另一个命名空间,如下所示:

cpp 复制代码
namespace namespace_name1 {
   // 代码声明
   namespace namespace_name2 {
      // 代码声明
   }
}

可以通过使用 :: 运算符来访问嵌套的命名空间中的成员:

cpp 复制代码
// 访问 namespace_name2 中的成员
using namespace namespace_name1::namespace_name2;
 
// 访问 namespace_name1 中的成员
using namespace namespace_name1;

在上面的语句中,如果使用的是 namespace_name1,那么在该范围内 namespace_name2 中的元素也是可用的,如下所示:

cpp 复制代码
#include <iostream>
using namespace std;
 
// 第一个命名空间
namespace first_space{
   void func(){
      cout << "Inside first_space" << endl;
   }
   // 第二个命名空间
   namespace second_space{
      void func(){
         cout << "Inside second_space" << endl;
      }
   }
}
using namespace first_space::second_space;
int main ()
{
 
   // 调用第二个命名空间中的函数
   func();
   
   return 0;
}

输出:

cpp 复制代码
Inside second_space

二 模板

模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。

模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。

每个容器都有一个单一的定义,比如 向量 ,我们可以定义许多不同类型的向量,比如 vector <int>vector <string>

可以使用模板来定义函数和类,接下来让我们一起来看看如何使用。

2.1 函数模板

模板函数定义的一般形式如下所示:

cpp 复制代码
template <typename type> ret-type func-name(parameter list)
{
   // 函数的主体
}

在这里,type 是函数所使用的数据类型的占位符名称。这个名称可以在函数定义中使用。

下面是函数模板的实例,返回两个数中的最大值:

cpp 复制代码
#include <iostream>
#include <string>
 
using namespace std;
 
template <typename T>
inline T const& Max (T const& a, T const& b) 
{ 
    return a < b ? b:a; 
} 
int main ()
{
 
    int i = 39;
    int j = 20;
    cout << "Max(i, j): " << Max(i, j) << endl; 
 
    double f1 = 13.5; 
    double f2 = 20.7; 
    cout << "Max(f1, f2): " << Max(f1, f2) << endl; 
 
    string s1 = "Hello"; 
    string s2 = "World"; 
    cout << "Max(s1, s2): " << Max(s1, s2) << endl; 
 
    return 0;
}

输出:

cpp 复制代码
Max(i, j): 39
Max(f1, f2): 20.7
Max(s1, s2): World

2.2 类模板

正如我们定义函数模板一样,我们也可以定义类模板。泛型类声明的一般形式如下所示:

cpp 复制代码
template <class type> class class-name {
.
.
.
}

在这里,type 是占位符类型名称,可以在类被实例化的时候进行指定。您可以使用一个逗号分隔的列表来定义多个泛型数据类型。

下面的实例定义了类 Stack<>,并实现了泛型方法来对元素进行入栈出栈操作:

cpp 复制代码
#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>
 
using namespace std;
 
template <class T>
class Stack { 
  private: 
    vector<T> elems;     // 元素 
 
  public: 
    void push(T const&);  // 入栈
    void pop();               // 出栈
    T top() const;            // 返回栈顶元素
    bool empty() const{       // 如果为空则返回真。
        return elems.empty(); 
    } 
}; 
 
template <class T>
void Stack<T>::push (T const& elem) 
{ 
    // 追加传入元素的副本
    elems.push_back(elem);    
} 
 
template <class T>
void Stack<T>::pop () 
{ 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::pop(): empty stack"); 
    }
    // 删除最后一个元素
    elems.pop_back();         
} 
 
template <class T>
T Stack<T>::top () const 
{ 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::top(): empty stack"); 
    }
    // 返回最后一个元素的副本 
    return elems.back();      
} 
 
int main() 
{ 
    try { 
        Stack<int>         intStack;  // int 类型的栈 
        Stack<string> stringStack;    // string 类型的栈 
 
        // 操作 int 类型的栈 
        intStack.push(7); 
        cout << intStack.top() <<endl; 
 
        // 操作 string 类型的栈 
        stringStack.push("hello"); 
        cout << stringStack.top() << std::endl; 
        stringStack.pop(); 
        stringStack.pop(); 
    } 
    catch (exception const& ex) { 
        cerr << "Exception: " << ex.what() <<endl; 
        return -1;
    } 
}

输出:

cpp 复制代码
7
hello
Exception: Stack<>::pop(): empty stack

三 预处理器

预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。

所有的预处理器指令都是以井号(#)开头,只有空格字符可以出现在预处理指令之前。预处理指令不是 C++ 语句,所以它们不会以分号(;)结尾。

我们已经看到,之前所有的实例中都有 #include 指令。这个宏用于把头文件包含到源文件中。

C++ 还支持很多预处理指令,比如 #include、#define、#if、#else、#line 等,让我们一起看看这些重要指令。

3.1 #define 预处理

#define 预处理指令用于创建符号常量。该符号常量通常称为,指令的一般形式是:

cpp 复制代码
#define macro-name replacement-text 

当这一行代码出现在一个文件中时,在该文件中后续出现的所有宏都将会在程序编译之前被替换为 replacement-text。例如:

cpp 复制代码
#include <iostream>
using namespace std;
 
#define PI 3.14159
 
int main ()
{
 
    cout << "Value of PI :" << PI << endl; 
 
    return 0;
}

现在,让我们测试这段代码,看看预处理的结果。假设源代码文件已经存在,接下来使用 -E 选项进行编译,并把结果重定向到 test.p。现在,如果您查看 test.p 文件,将会看到它已经包含大量的信息,而且在文件底部的值被改为如下:

cpp 复制代码
$ gcc -E test.cpp > test.p

...
int main ()
{
 
    cout << "Value of PI :" << 3.14159 << endl; 

    return 0;
}

3.2 参数宏

可以使用 #define 来定义一个带有参数的宏,如下所示:

cpp 复制代码
#include <iostream>
using namespace std;
 
#define MIN(a,b) (a<b ? a : b)
 
int main ()
{
   int i, j;
   i = 100;
   j = 30;
   cout <<"较小的值为:" << MIN(i, j) << endl;
 
    return 0;
}

输出:

cpp 复制代码
较小的值为:30

3.3 条件编译

有几个指令可以用来有选择地对部分程序源代码进行编译。这个过程被称为条件编译。

条件预处理器的结构与 if 选择结构很像。请看下面这段预处理器的代码:

cpp 复制代码
#ifdef NULL
   #define NULL 0
#endif

可以只在调试时进行编译,调试开关可以使用一个宏来实现,如下所示:

cpp 复制代码
#ifdef DEBUG
   cerr <<"Variable x = " << x << endl;
#endif

如果在指令 #ifdef DEBUG 之前已经定义了符号常量 DEBUG,则会对程序中的 cerr 语句进行编译。您可以使用 #if 0 语句注释掉程序的一部分,如下所示:

cpp 复制代码
#if 0
   不进行编译的代码
#endif

实例:

cpp 复制代码
#include <iostream>
using namespace std;
#define DEBUG
 
#define MIN(a,b) (((a)<(b)) ? a : b)
 
int main ()
{
   int i, j;
   i = 100;
   j = 30;
#ifdef DEBUG
   cerr <<"Trace: Inside main function" << endl;
#endif
 
#if 0
   /* 这是注释部分 */
   cout << MKSTR(HELLO C++) << endl;
#endif
 
   cout <<"The minimum is " << MIN(i, j) << endl;
 
#ifdef DEBUG
   cerr <<"Trace: Coming out of main function" << endl;
#endif
    return 0;
}

输出:

cpp 复制代码
Trace: Inside main function
The minimum is 30
Trace: Coming out of main function

3.4 # 和 ## 运算符

和 ## 预处理运算符在 C++ 和 ANSI/ISO C 中都是可用的。# 运算符会把 replacement-text 令牌转换为用引号引起来的字符串。

请看下面的宏定义:

cpp 复制代码
#include <iostream>
using namespace std;
 
#define MKSTR( x ) #x
 
int main ()
{
    cout << MKSTR(HELLO C++) << endl;
 
    return 0;
}

输出:

cpp 复制代码
HELLO C++

让我们来看看它是如何工作的。不难理解,C++ 预处理器把下面这行:

cpp 复制代码
cout << MKSTR(HELLO C++) << endl;

转换成了:

cpp 复制代码
cout << "HELLO C++" << endl;

运算符用于连接两个令牌。下面是一个实例:

cpp 复制代码
#define CONCAT( x, y )  x ## y

当 CONCAT 出现在程序中时,它的参数会被连接起来,并用来取代宏。例如,程序中 CONCAT(HELLO, C++) 会被替换为 "HELLO C++",如下面实例所示。

cpp 复制代码
#include <iostream>
using namespace std;
 
#define concat(a, b) a ## b
int main()
{
   int xy = 100;
   
   cout << concat(x, y);
   return 0;
}

输出:

cpp 复制代码
100

让我们来看看它是如何工作的。不难理解,C++ 预处理器把下面这行:

cpp 复制代码
cout << concat(x, y);

转换成了:

cpp 复制代码
cout << xy;

3.5 C++中的预定义宏

描述
LINE 这会在程序编译时包含当前行号。
FILE 这会在程序编译时包含当前文件名。
DATE 这会包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。
TIME 这会包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。

实例:

cpp 复制代码
#include <iostream>
using namespace std;
 
int main ()
{
    cout << "Value of __LINE__ : " << __LINE__ << endl;
    cout << "Value of __FILE__ : " << __FILE__ << endl;
    cout << "Value of __DATE__ : " << __DATE__ << endl;
    cout << "Value of __TIME__ : " << __TIME__ << endl;
 
    return 0;
}

输出:

cpp 复制代码
Value of __LINE__ : 6
Value of __FILE__ : test.cpp
Value of __DATE__ : Feb 28 2011
Value of __TIME__ : 18:52:48
相关推荐
新手小袁_J3 分钟前
java.lang.IllegalStateException: Error processing condition on org.springframework.boot.autoconfigur
java·开发语言·spring·spring cloud·bootstrap·maven·mybatis
墨鸦_Cormorant3 分钟前
Java 创建图形用户界面(GUI)组件详解之下拉式菜单(JMenu、JMenuItem)、弹出式菜单(JPopupMenu)等
java·开发语言·gui
cccccc语言我来了4 分钟前
c++-----------------多态
java·开发语言·c++
南鸢1.06 分钟前
11张思维导图带你快速学习java
java·开发语言
sunny-ll7 分钟前
【C++】explicit关键字详解(explicit关键字是什么? 为什么需要explicit关键字? 如何使用explicit 关键字)
c语言·开发语言·c++·算法·面试
泯泷8 分钟前
JS代码混淆器:JavaScript obfuscator 让你的代码看起来让人痛苦
开发语言·javascript·ecmascript
轩源源12 分钟前
C++草原三剑客之一:继承
开发语言·数据结构·c++·算法·青少年编程·继承·组合
未知陨落17 分钟前
leetcode题目(1)
c++·leetcode
每天写点bug2 小时前
【go每日一题】 责任链模式的实现
开发语言·golang·责任链模式
半盏茶香3 小时前
C语言勘破之路-最终篇 —— 预处理(下)
c语言·开发语言·c++·算法