C++中的命名冲突和命名空间简介

在C++中,命名冲突是指在同一作用域中存在两个或更多具有相同名称的标识符(如变量、函数、类等)。这种情况可能会导致编译器无法确定使用哪个标识符,从而引发编译错误。为了解决这种问题,C++引入了命名空间(namespace)的概念。

在C++中,命名冲突是指在同一作用域中存在两个或更多具有相同名称的标识符(如变量、函数、类等)。这种情况可能会导致编译器无法确定使用哪个标识符,从而引发编译错误。为了解决这种问题,C++引入了命名空间(namespace)的概念。

命名空间是一种用于组织代码的机制,它允许你在一个命名空间中定义变量、函数、类等,以防止命名冲突。命名空间提供了一种将相关的标识符组织在一起的方式,使得不同的命名空间中可以具有相同名称的标识符,而不会发生冲突。

如果冲突的标识符被引入到同一个文件中,结果将是编译器错误。如果冲突标识符被引入到属于同一程序的不同文件中,则结果将是链接器错误。

命名冲突的示例

a.cpp:

#include <iostream>

void myFcn(int x)
{
    std::cout << x;
}

main.cpp:

#include <iostream>

void myFcn(int x)
{
    std::cout << 2 * x;
}

int main()
{
    return 0;
}

编译器编译这个程序时,会独立编译a.cppmain.cpp,每个文件都会编译没有问题。

然而,当链接器执行时,它会将a.cppmain.cpp中的所有定义链接在一起,并发现函数myFcn的冲突定义。然后链接器将因错误而中止。请注意,即使从未调用myFcn ,也会发生此错误!

大多数命名冲突发生在两种情况下:

  1. 两个(或多个)相同名称的函数(或全局变量)被引入到属于同一程序的单独文件中。这将导致链接器错误,如上所示。
  2. 两个(或多个)相同名称的函数(或全局变量)被引入到同一个文件中。这将导致编译器错误。

随着程序变得越来越大并且使用更多的标识符,引入命名冲突的可能性显着增加。好消息是 C++ 提供了大量避免命名冲突的机制。局部作用域就是这样一种机制,它可以防止函数内部定义的局部变量相互冲突。但局部作用域不适用于函数名称。那么我们如何防止函数名称相互冲突呢?

什么是命名空间?

命名空间是一个区域,允许您在其中声明名称以消除歧义。命名空间为在其内部声明的名称提供了一个作用域区域(称为命名空间作用域)——这仅仅意味着在命名空间内声明的任何名称都不会被误认为是其他作用域中的相同名称。

在命名空间内,所有名称必须是唯一的,否则将导致命名冲突。

命名空间通常用于对大型项目中的相关标识符进行分组,以帮助确保它们不会无意中与其他标识符发生冲突。例如,如果您将所有数学函数放在名为math 的命名空间中,那么您的数学函数不会与数学命名空间外的同名函数发生冲突。

我们将在以后的课程中讨论如何创建您自己的命名空间。

全局命名空间

在 C++ 中,任何未在类、函数或命名空间内定义的名称都被视为隐式定义的命名空间的一部分,称为全局命名空间有时也称为全局作用域)。

在本课程顶部的示例中,函数 main() 和 myFcn() 的两个版本都在全局命名空间内定义。示例中遇到的命名冲突是因为 myFcn() 的两个版本最终都位于全局命名空间内,这违反了命名空间中所有名称必须唯一的规则。

只有声明和定义语句可以出现在全局命名空间中。这意味着我们可以在全局命名空间中定义变量,尽管这通常应该避免。这也意味着其他类型的语句(例如表达式语句)不能放置在全局命名空间中(全局变量的初始化程序除外):

#include <iostream> // 由预处理器处理

// 以下所有语句都属于全局命名空间
void foo();    // 可行:在全局命名空间中的函数前向声明
int x;         // 编译通过但强烈不建议:在全局命名空间中未初始化的变量定义
int y { 5 };   // 编译通过但不建议:在全局命名空间中带有初始化器的变量定义
x = 5;         // 编译错误:全局命名空间中不允许可执行语句

int main()     // 可行:在全局命名空间中的函数定义
{
    return 0;
}

void goo();    // 可行:在全局命名空间中的另一个函数前向声明

std 命名空间

当 C++ 最初设计时,C++ 标准库中的所有标识符(包括 std::cin 和 std::cout)都可以在没有std:: 的情况下使用前缀(它们是全局命名空间的一部分)。但是,这意味着标准库中的任何标识符都可能与您为自己的标识符选择的任何名称(也在全局命名空间中定义)发生冲突。当您 #included 标准库中的新文件时,正在运行的代码可能会突然出现命名冲突。或者更糟糕的是,可以在某一版本的 C++ 下编译的程序可能无法在未来版本的 C++ 下编译,因为引入标准库的新标识符可能与已编写的代码发生命名冲突。因此,C++ 将标准库中的所有功能移至名为“std”(标准的缩写)的命名空间中。

事实证明,std::cout的名称实际上并不是std::cout。它实际上只是cout,而std是标识符cout所属的命名空间的名称。因为cout是在std命名空间中定义的,所以名称cout不会与我们在全局命名空间中创建的任何名为cout的对象或函数冲突。

类似地,当访问命名空间中定义的标识符(例如std::cout)时,您需要告诉编译器我们正在寻找命名空间(std)内定义的标识符。

有几种不同的方法可以做到这一点。

显式命名空间限定符 std::

告诉编译器我们想要使用std命名空间中的cout 的最直接方法是显式使用std::前缀。例如:

#include <iostream>

int main()
{
    std::cout << "Hello world!"; // 当我们说 cout 时,我们指的是在 std 命名空间中定义的 cout
    return 0;
}

:: 符号是一个称为范围解析运算符的运算符。 :: 符号左侧的标识符标识 :: 符号右侧的名称所在的命名空间。 如果 :: 符号左侧没有提供标识符,则假定为全局命名空间。

因此,当我们说 std::cout 时,我们说的是“位于命名空间 std 中的 cout”。

这是使用 cout 的最安全方法,因为我们引用哪个 cout(std 命名空间中的 cout)没有歧义。

当标识符包含名称空间前缀时,该标识符称为限定名称

使用命名空间 std(以及为什么要避免它)

访问命名空间内的标识符的另一种方法是使用using 指令语句。这是我们最初的“Hello world”程序,带有using 指令

#include <iostream>

using namespace std; // 这是一个使用指令,允许我们在不使用命名空间前缀的情况下访问 std 命名空间中的名称

int main()
{
    cout << "Hello world!";
    return 0;
}

using 指令允许我们访问命名空间中的名称,而无需使用命名空间前缀。因此,在上面的示例中,当编译器确定cout标识符是什么时,它将与std::cout匹配,由于 using 指令,它可以作为cout进行访问。

许多文本、教程甚至某些 IDE 都推荐或在程序顶部使用 using 指令。然而,以这种方式使用,这是一种不好的做法,并且非常不鼓励。

考虑以下程序:

#include <iostream> // 导入 std::cout 的声明

using namespace std; // 使 std::cout 可以作为 "cout" 访问

int cout() // 在全局命名空间中定义了我们自己的 "cout" 函数
{
    return 5;
}

int main()
{
    cout << "Hello, world!"; // 编译错误!这里我们想要哪一个 "cout"?std 命名空间中的还是上面我们自己定义的?

    return 0;
}

上面的程序无法编译,因为编译器现在无法判断我们是否需要我们定义的cout函数,或者std命名空间内定义的cout

当以这种方式使用 using 指令时,我们定义的任何标识符都可能与std命名空间中任何同名的标识符冲突。更糟糕的是,虽然标识符名称现在可能不会冲突,但它可能与未来语言修订中添加到 std 命名空间的新标识符发生冲突。这就是首先将标准库中的所有标识符移至std命名空间的全部目的!

原创文章,作者:jkhxw,如若转载,请注明出处:https://www.jkhxw.com/cpp-naming-collisions/

(0)
上一篇 2023年10月8日 下午11:14
下一篇 6天前

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注