C++ 通过函数、类和名字空间等语言特性的组合以及源码的组织来实现模块化。
名字空间
名字空间(namespace)是 C++ 中用于组织代码和避免名字冲突的一种机制。它允许将一组相关的特性(如类、函数等)封装在一个命名的作用域内。名字空间的成员可以直接相互访问,而从外部访问它们则需要显式指定名字空间。
名字空间形成了一个具名的作用域。在名字空间内部,稍后的声明可以引用之前定义的成员,但从外部引用名字空间成员需要使用特殊方式(如 ::Line
)。
引用名字空间成员的办法:使用完整的限定名字、使用 using
声明、using
指示或参数依赖查找。
显式限定
不允许在名字空间定义之外使用限定符语法为其声明新成员。这样做的目的是为了捕获拼写错误和类型不匹配等错误,并便于在名字空间声明中查找所有名字。
namespace Parser {
double expr(bool); // 成员声明
double term(bool);
double prim(bool);
}
double val = Parser::expr(true); // 使用成员
double Parser::expr(bool b) {} // 定义{}
void Parser::logical(bool); // 错误:Parser中没有logical()
double Parser::trem(bool); // 错误:Parser中没有trem()(拼写错误)
double Parser::prim(int); // 错误:Parser::prim()接受一个bool类型参数(错误类型)
全局作用域也是一个名字空间,可以显式地用 ::
来引用。
using 声明
频繁使用某个名字空间中的名称时,每次都要进行显式限定会很繁琐。using
声明提供了一种简化这种冗长代码的方式,允许在特定作用域内直接使用名字空间中的成员而不需要每次都指定完整的限定名。
考虑以下代码,它将一个字符串分割成空白符分隔的子串:
#include<string>
#include<vector>
#include<sstream>
std::vector<std::string> split(const std::string& s) {
std::vector<std::string> res;
std::istringstream iss(s);
for(std::string buf; iss >> buf;)
res.push_back(buf);
return res;
}
在这个例子中,std::string
被多次使用,使得代码显得冗长且分散注意力。
为了简化上述代码,可以使用 using
声明来引入 std::string
的代用名:
using std::string; // 用“string”表示“std::string”
std::vector<string> split(const string& s) {
std::vector<string> res;
std::istringstream iss(s);
for(string buf; iss >> buf;)
res.push_back(buf);
return res;
}
当 using
声明用于一个重载的名字时,它会应用于其所有重载版本。例如:
namespace N {
void f(int);
void f(string);
};
void g() {
using N::f;
f(789); // N::f(int)
f("Bruce"); // N::f(string)
}
在这个例子中,using
声明将 N::f
的所有重载版本引入到作用域中,允许直接调用 f
而不指定 N::
前缀。
using 指示
using
指示允许在特定作用域内不加限定地使用某个名字空间中的所有成员。这可以简化代码,尤其是当需要频繁使用某个名字空间中的多个成员时。
考虑以下代码,它将一个字符串分割成空白符分隔的子串:
#include <string>
#include <vector>
#include <sstream>
std::vector<std::string> split(const std::string& s) {
std::vector<std::string> res;
std::istringstream iss(s);
for (std::string buf; iss >> buf;)
res.push_back(buf);
return res;
}
在这个例子中,std::
前缀被多次使用,使得代码显得冗长。
为了简化上述代码,可以使用 using
指示来引入整个 std
名字空间:
using namespace std; // 令来自std的每个名字都可访问
vector<string> split(const string& s) {
vector<string> res;
istringstream iss(s);
for (string buf; iss >> buf;)
res.push_back(buf);
return res;
}
现在,代码变得更加简洁,std
名字空间中的所有成员都可以直接使用,而不需要 std::
前缀。
在一个函数中使用 using
指示通常是安全的,但对全局 using
指示必须小心,因为它可能导致名字冲突。
为了避免名字冲突,最好避免在全局作用域中使用 using
指示,特别是在头文件中,因为你永远不知道头文件可能在哪里被包含。
参数依赖查找(ADL)
参数依赖查找(Argument-Dependent Lookup,简称 ADL)允许在调用函数时,根据参数的类型自动在相关的上下文中查找函数定义。这种机制特别适用于运算符重载和模板函数,可以减少代码中的显式限定,同时避免名字空间污染。
#include <string>
namespace Chrono {
class Date {/*...*/ };
bool operator==(const Date&, const std::string&);
std::string format(const Date&); // 创建字符串表示
std::string format(const int&); // 创建字符串表示
}
void f(Chrono::Date d,int i) {
std::string s = format(d); // Chrono::format()
std::string t = format(i); // 错误:作用域中没有format()的定义
std::string t = Chrono::format(i); // OK
}
参数依赖查找的规则
- 如果参数是类成员,则关联名字空间包括类本身及其基类和包含类的名字空间。
- 如果参数是名字空间的成员,则关联名字空间为外层的名字空间。
- 如果参数是内置类型,则没有关联名字空间。
名字空间是开放的
名字空间是开放的,这意味着你可以在多个不同的地方向同一个名字空间添加成员。这种灵活性允许将名字空间的声明分散在多个文件中,使得代码的组织和维护更加方便。
namespace A {
int f(); // 现在A包含成员f()
}
namespace A {
int g(); // 现在A有两个成员,f()和g()
}
在编写新代码时,推荐使用多个小的名字空间而不是将大量代码放入单一的名字空间中。这样做有助于更好地组织代码并提高模块化。
将名字空间成员分散在多个声明中的另一个原因是,有时我们希望将名字空间作为接口的一部分与支持实现的部分区分开来。这有助于隐藏实现细节,只暴露出必要的接口。
需要注意的是,使用名字空间别名无法打开名字空间。
namespace LongNamespaceName {
void func1() {}
// 可以继续在此添加更多成员
}
namespace LNN = LongNamespaceName; // 创建别名 LNN
// 下面的语句会报错,因为不能通过 LNN 添加新成员
namespace LNN {
void func2() {} // 错误,不能通过别名添加新成员
}
// 仍然可以通过原名字空间名 LongNamespaceName 添加新成员
namespace LongNamespaceName {
void func2() {} // 正确
}
模块化与接口
名字空间可以被设计为具有两种不同的接口:用户接口和实现者接口。用户接口是程序呈现给最终用户的简化视图,而实现者接口则包含了实现细节和辅助功能,供开发者在构建模块时使用。
用户接口:
namespace Parser {
double expr(bool);
}
实现者接口:
namespace Parser_impl {
using namespace Parser;
double prim(bool);
double term(bool);
double expr(bool);
using namespace Lexer; // 使用词法分析器提供
using Error::error;
using Table::table;
}
对于大规模程序,引入 _impl
后缀的实现者接口是一种良好的实践。这种设计允许我们将实现细节与用户接口分离,使得用户不需要关心实现细节,同时也使得实现者可以自由地更改实现而不影响用户。
组合使用名字空间
using 声明将名字添加到局部作用域中,而 using 指示则令名字在其所在作用域中可访问,但不添加到局部作用域。
名字空间别名
// 使用名字空间别名缩短名字
namespace ATT = American_Telephone_and_Telegraph;
ATT::String s3 = "Grieg";
ATT::String s4 = "Nielsen";
组合名字空间
我们经常需要将现有的接口组合起来以构建新的接口。这种技术允许我们通过组合不同的名字空间来创建一个复合的名字空间,从而提供更丰富的功能集。
namespace His_string {
class String {/*...*/ };
String operator+(const String&, const String&);
String operator+(const String&, const char*);
void fill(char);
// ...
}
namespace Her_vector {
template<class T>
class Vector {/*...*/ };
// ...
}
namespace My_lib {
using namespace His_string;
using namespace Her_vector;
void my_fct(String&);
}
在这个例子中,My_lib
名字空间通过 using
指示导入了 His_string
和 Her_vector
名字空间,从而继承了这两个名字空间的所有成员。
My_lib::String s = "Byron"; // 寻找 My_lib::His_string::String
My_lib::String
实际上是 My_lib::His_string::String
的简写。
只有当我们需要定义某些实体时,才真的需要了解一个实体的真正名字空间:
void My_lib::fill(char c) {
// 错误:My_lib中并未声明fill()
}
void His_string::fill(char c) {
// 正确:fill()在His_string中声明
}
理想情况下,一个名字空间应该:
- 表达一组逻辑相关的特性;
- 不让用户访问不相关的特性;
- 不给用户增加符号表示上的严重负担。
组合与选择
组合机制(使用 using
指示)和选择机制(使用 using
声明)的结合提供了灵活性,使我们能够在访问特性时解决名字冲突和二义性问题。
namespace His_lib {
class String {/*...*/};
template<class T> class Vector {/*...*/};
// ...
}
namespace Her_lib {
template<class T> class Vector {/*...*/};
class String {/*...*/};
// ...
}
namespace My_lib {
using namespace His_lib;
using namespace Her_lib;
using His_lib::String;
using Her_lib::Vector;
template<class T> class List {/*...*/};
// ...
}
在这个例子中,My_lib
名字空间通过 using
指示导入了 His_lib
和 Her_lib
的所有成员,然后通过 using
声明明确选择了 His_lib
的 String
和 Her_lib
的 Vector
,解决了潜在的名字冲突。
当编译器在一个名字空间中进行查找时,显式声明的名字(包括用 using
声明声明的名字)优先级高于通过 using
指示变为可见的名字。因此,在 My_lib
中,String
和 Vector
的名字冲突被顺利解决,分别使用了 His_lib::String
和 Her_lib::Vector
。而 List
则默认解析为 My_lib::List
,不管 His_lib
或 Her_lib
是否提供了 List
。
有时,我们需要为名字空间中的实体起一个新的名字,这可以通过使用 using
引入别名来实现。
namespace Lib2 {
using namespace His_lib; // His_lib中的所有实体
using namespace Her_lib; // Her_lib中的所有实体
using His_lib::String; // 解决潜在冲突:使用His_lib中的版本
using Her_lib::Vector; // 解决潜在冲突:使用Her_lib中的版本
using Her_string = Her_lib::String; // 重命名
template<class T> using His_vec = His_lib::Vector<T>; // 重命名
template<class T> class List {/*...*/}; // 其他内容
// ...
}
在这个例子中,Lib2
名字空间通过 using
声明引入了 His_lib
和 Her_lib
的所有成员,并为 Her_lib::String
和 His_lib::Vector<T>
提供了别名 Her_string
和 His_vec
。
名字空间和重载
函数重载机制跨越名字空间,这意味着同名函数可以在不同名字空间中存在,而不会相互冲突。
// A.h
namespace A {
void f(int);
// ...
}
// B.h
namespace B {
void f(char);
// ...
}
// user.c
#include "A.h"
#include "B.h"
using namespace A;
using namespace B;
void g() {
f('a'); // 调用 B.h中的 f()
}
重载规则还提供了一种扩展库的机制:
#include <algorithm>
#include <vector>
#include <iostream>
namespace Estd {
using namespace std;
template<class C>
void sort(C& c) {
std::sort(c.begin(), c.end());
}
template<class C, class P>
void sort(C& c, P p) {
std::sort(c.begin(), c.end(), p);
}
}
using namespace Estd;
template<class T>
void print(const vector<T>& v) {
for (auto& x : v) cout << x << ' ';
cout << "\n";
}
int main() {
std::vector<int> v{ 7, 3, 9, 4, 0, 1 };
sort(v); // 直接对容器操作
print(v);
// 其他操作...
}
版本控制
C++ 提供了内联名字空间(inline namespace)机制,以帮助处理不同版本之间的过渡。
namespace Popular {
inline namespace V3_2 { // V3_2提供了Popular的默认含义
double f(double);
int f(int);
template<class T> class C {/*...*/ };
}
namespace V3_0 {
int f(int);
}
namespace V2_4_2 {
double f(double);
template<class T> class C {/*...*/ };
}
}
using namespace Popular;
void g() {
f(1); // 调用 Popular::V3_2::f(int)
V3_0::f(1); // 调用 Popular::V3_0::f(double)
V2_4_2::f(1); // 调用 Popular::V2_4_2::f(double)
V3_2::f(1); // 调用 Popular::V3_2::f(int)
}
template<class T>
void h() {
Popular::C<T*>{/*...*/};
}
为了减少代码复制,可以使用头文件包含技巧。
名字空间嵌套
名字空间嵌套允许将一组声明和定义组织在层次化的名字空间结构中。
namespace X {
void g();
namespace Y {
void f();
void ff();
}
}
void X::Y::ff() {
f();
g();
}
void X::g() {
f();// 错误,X中没有f
Y::f(); // 正确
}
void h() {
f(); // 错误
Y::f(); // 错误
X::f(); // 错误
X::Y::f();
}
无名名字空间
无名(匿名)命名空间是一种特殊的命名空间,不像普通命名空间那样有名字。无名命名空间的主要作用是实现“内部链接”,即限制某些标识符(如变量、函数、类等)在声明它们的文件内可见,而不被外部文件访问。
// file1.cpp
namespace {
int counter = 0;
void increment() {
++counter;
}
}
int main() {
increment();
return 0;
}
每个无名名字空间都有一个隐含的 using 指示(把里面的符号暴露出来)。上面的无名名字空间相当于:
namespace $$$ {
int counter = 0;
void increment() {
++counter;
}
}
using namespace $$$;