1 模块化和界面
任何实际程序都是有一些部分组成的。通过将程序进行模块化可以使我们的程序更加清晰,有助于多人合作和维护。
将一个程序进行模块化以后,当其中一个模块调用另一个模块时,它不需要知道其具体实现,只需要调用它提供的接口即可。因此一个模块应该是由两个部分组成:具体实现和提供给外部的接口。
2 命名空间
2.1命名空间的作用
命名空间相当于一个容器,它里面包含了逻辑结构上互相关联的一组类、模板、函数等。也就是说如果某些“对象”在逻辑上有关系,我们就可以将它们放到一个命名空间里用以和外界进行区分。命名空间一个显著的特点是命名空间内的变量(类等)名可以和命名空间以外的重名。这可以用来将不同人写的代码进行整合。
命名空间的使用格式如下:
namespace A
{
void Fun1(){...};
void Fun2(){...};
}
上面的组织形式我们将函数的具体实现和声明放到了一起,有时候我们并不想看到函数的具体实现,只希望能一眼看到的全部都是函数的接口界面。我们可以采用如下的方式将函数的界面和具体实现分开。
namespace A
{
void Fun1();
void FUn1();
}
void A::Fun1(){/*...*/}
void A::Fun2(){/*...*/}
- 如果一个函数的定义没有在其对应的命名空间里,必须要使用作用域解析符::来指定函数的命名空间。
- 不可以在命名空间以外定义一个命名空间中不存在的新成员。例如:
void A:Fun3(); //错误,A里并没有Fun3()
- 一个良好的程序应该将程序中的所有实体(变量,类,函数)都放到某个命名空间里。当然除了main()函数之外。
2.2命名空间作用域规则
命名空间是一个作用域,因此它具有普通作用域的规则。
- 如果一个变量之前在该命名空间或其外围作用域声明过,则它可以直接使用。
- 使用来自另外一个命名空间的变量,需要加上作用域解析符。
- 如果频繁的使用另外一个命名空间的实体,可以使用using将其引入,之后再使用时就不用加作用域解析符了。
double A::Fun()
{
using B::Fun1; //使用B命名空间的函数Fun1;
using C::Var1; //使用C命名空间的变量Var1;
void Fun1(Var1); //B::Fun1
}
- 在函数中使用using则外部变量的作用域限于函数,如果在命名空间中使用using,则作用范围扩大到整个命名空间。如果使用到一个命名空间中的多个实体,可以直接用using namespace Name将命名空间中所有的实体都引入,但是让一个命名空间的所有实体都可以被另一个命名空间访问并不是一个安全的做法,应尽量避免。
namespace A
{
using namespace B; //使用命名空间B中所有实体
}
- 如果使用using引入的实体之间或者和本命名空间中的实体有重名的。那么加上作用域解析符能帮助我们更好的对它们进行区分。
2.3多重界面
有时候我们同一个命名空间在面向不同的用户时,可能需要提供不同的界面。比如我们有一个命名空间里面定义了关于串口的一些实体。我们给一个开发中的程序提供的接口可能包括:打开串口,设置波特率,设置校验位等。但是面向一个最终用户时,我们可能只需要给他提供一个打开串口接口就够了。这便是使用多重界面的意义。
- 实现多重界面的方法有很多,首先可能想到的是使用不同的命名空间。
namespace A
{
void Fun1();
void Fun2();
void Fun3();
}
namespace A_Interface1
{
using A::Fun1;
}
- 上面界面实现的过程中,A_Interface1和A有着非常强的关联,修改A中的Fun1会使A_Interface1中的Fun1也修改。有时候我们可能不需要这么强的关联性,让A_Interface1中的函数有着一定的可控性,可以使用下面界面实现的方式。
namespace A
{
void Fun1();
void Fun2();
void Fun3();
}
namespace A_Interface1
{
void Fun1(){A::Fun1();}
}
这里为了书写方便将A_Interface1中Fun1的声明和定义放到一起了。上面这种界面实现形式保证了A_Interface1中的Fun1有一定的自主性,当A中的Fun1改变时,我们可以在A_Interface1中的Fun1进行进一步的调整,或者干脆不用A中的Fun1重新实现Fun1.这种界面处理方式去除了一定的耦合性,基本已经能够满足大部分需求了。
3.当然我们也可以不重新定义一个命名空间直接使用原来命名空间的名字,但是这样容易误导程序设计者以外的人(让看到这个接口的人以外该命名空间只有声明的这些实体)。
namespace A
{
void Fun1();
void Fun2();
void Fun3();
}
//使用接口的文件包含下面这个声明或其所在的文件
namespace A
{
void Fun1();
}
2.4无名命名空间
我们知道不同命名空间的变量名可以重复,这有助于第三方将两个不同人写的代码进行整合。有时候我们并不想我们的某些代码被其他人进行整合,但是也想利用命名空间的优势——可以让变量名重复。这时候使用无名命名空间就很有价值了:第一没有名字,其他地方无法引用进去;第二因为是命名空间它里面的变量可以和其他命名空间中变量的名字重复。
无名命名空间可以在本编译单元(所在文件)处调用,没有这一规则就永远都用不到了。需要注意的是,不同编译单元中的无名命名空间不同。
2.5编译器的名字查找
- 一个函数Fun1如果有一个参数为T类型,那么一般情况下类型T和函数Fun1都在同一个命名空间里。编译器在调用使用T为参数的函数时,也会隐式的从T(和其基类)所在命名空间寻找被调用函数。编译器这种隐式的查找过程可以使程序员省掉许多显式的限定符或using指令。而且这个查找机制在对象的运算符运算和模板参数中特别有用。
namespace A
{
class TypeA{...};
void Fun1(TypeA a){...};
}
void Fun(A::TypeA a)
{
Fun1(a); //可以,会在TypeA所在命名空间A找到Fun1
Fun1(2); //不行
}
当然,命名空间本身必须在作用域里,函数也必须在寻找和使用之前声明。
- 如果调用的函数Fun1中有两个参数,且这两个参数所在命名空间都找到了Fun1函数。那么就会调用重载解析规则。
namespace A
{
class TypeA{...};
bool operator==(const A&,std::string&);
}
void Fun(A::TypeA a,std::string str)
{
if(a == str)
{...}
}
在上例中表达式a==str
中有两种数据类型,且这两种数据类型所在的命名空间A,std都定义了operator(string的运算符的定义在std中),于是重载解析规则就会被调用。由于std::operator不以TypeA为参数,所有最后编译器会调用A::operator.
3. 当一个类的成员调用一个函数时,编译器查找函数时偏向于在同一类和其基类中查找函数,而不是在被调用函数其他参数类型所在的命名空间查找。但这一规则对运算符(如+,-)查找并不适用。
2.6命名空间的别名
我们在给命名空间取名字的时候,如果太短(比如上面的A)很可能出现冲突。起太长又太麻烦。这时候我们将长名字的命名空间在合适的地方取个别名可能会更好些。格式如下:
namespace A = LongNameNamespaceA;
这种替换的方式和C语言中的宏定义非常相似,因此别名另外一个特别用于的地方就是使代码对命名空间的依赖降低。比如我们有一个大型程序依赖于一个命名空间LibA,现在需要版本升级将所有引用LibA中的代码替换为LibA_Plus.如果使用起别名的方式,我们不用再代码中逐个查找将LibA::
替换为LibA_Plus
.而是简单的在命名空间的别名定义处稍作修改即可。
namespace A = LibA;
//替换为
namespace A = LibA_Plus;
但也需要注意过多的使用命名空间别名也容易造成混乱。
2.7命名空间的组合
前面我们说了命名空间里可以包含其他的命名空间,下面给出命名空间组合时的一些具体细节。
- 再次强调在命名空间里没有声明的实体,不能在别处定义。即使这个实体在其包含的命名空间里声明过了。
namespace A
{
using namespace B;
}
namespace B
{
fill(char C);
}
A::fill(char C) //错误 A中并没有声明fill函数
{...}
- 引入一个命名空间的实体时可以引入它的所有重载。
namespace B
{
class String{...};
String operator+(const String&,const String&);
String operator+(const String&,const char*);
}
namespace A
{
using B::String;
using B::operator+; //使用B中所定义的全部(两个)+运算
}
- 显示的使用using关键字可以改变重载的顺序。
namespace LibA
{
class String{...};
template<class T>class Vector{...};
//其他实体
}
namespace LibB
{
class String{...};
template<class T> class Vector{...};
//其他实体
}
namespace LibC
{
using namespace LibA;
using namespace LibB;
using LibA::String; //偏向使用LibA中的String
using LibB::Vector; //偏向使用LIbB中的Vector
//其他实体
}
- 当然如果在LibC中声明了String或者Vector的话就没有LibA和LibB什么关系了。
- 如果我们想要使用被重载覆盖的LibA::Vector和LibB::String.除了加上作用域解析符之外,可以使用typedef的形式和继承的形式。
template<class T>class A_Vector:public LibA::Vector<T>{...};
typedef LibB::String B_String;
- 对标准C库继续支持。以printf为例,在C头文件中使用std命名空间将stdio.h进行包裹。
//stdio.h
namespace std
{
int printf(const char*...);
}
using namespace std; //包含头文件后不需要再重复这句话了
//...
#include <cstdio> //对新库也进行包含
using std::printf;
C++中提供了新的头文件,头文件中没有使用using指令,这使不希望全部的标准库实体都隐式的使用成为了可能。
//cstdio.h
namespace std
{
int printf(const char* ...);
//...
}
- 命名空间是开放的,你可以在不同的文件中或同一个文件中的不同地方给命名空间加新的成员。也非常推荐使用这种方法将命名空间进一步分类,而不是将所有的实体都放到一个大块的命名空间中。
//file1.c
namespace A
{
int a;
}
//其他代码
namespace A
{
int b;
}
//file2.h
namespace A
{
int c;
}