1. 程式人生 > >C++-命名空間

C++-命名空間

顯式 int 能夠 基本 邏輯 作用域 界面 別名 var

from http://www.cnblogs.com/yabin/p/6411746.html

1 模塊化和界面

任何實際程序都是有一些部分組成的。通過將程序進行模塊化可以使我們的程序更加清晰,有助於多人合作和維護。

將一個程序進行模塊化以後,當其中一個模塊調用另一個模塊時,它不需要知道其具體實現,只需要調用它提供的接口即可。因此一個模塊應該是由兩個部分組成:具體實現和提供給外部的接口。

2 命名空間

2.1命名空間的作用

命名空間相當於一個容器,它裏面包含了邏輯結構上互相關聯的一組類、模板、函數等。也就是說如果某些“對象”在邏輯上有關系,我們就可以將它們放到一個命名空間裏用以和外界進行區分。命名空間一個顯著的特點是命名空間內的變量(類等)名可以和命名空間以外的重名。這可以用來將不同人寫的代碼進行整合。


命名空間的使用格式如下:

namespace A
{
  void Fun1(){...};
  void Fun2(){...};
}

上面的組織形式我們將函數的具體實現和聲明放到了一起,有時候我們並不想看到函數的具體實現,只希望能一眼看到的全部都是函數的接口界面。我們可以采用如下的方式將函數的界面和具體實現分開。

namespace A
{
  void Fun1();
  void FUn1();
}
void A::Fun1(){/*...*/}
void A::Fun2(){/*...*/}
  1. 如果一個函數的定義沒有在其對應的命名空間裏,必須要使用作用域解析符::來指定函數的命名空間。
  2. 不可以在命名空間以外定義一個命名空間中不存在的新成員。例如:
void A:Fun3();    //錯誤,A裏並沒有Fun3()
  1. 一個良好的程序應該將程序中的所有實體(變量,類,函數)都放到某個命名空間裏。當然除了main()函數之外。

2.2命名空間作用域規則

命名空間是一個作用域,因此它具有普通作用域的規則。

  1. 如果一個變量之前在該命名空間或其外圍作用域聲明過,則它可以直接使用。
  2. 使用來自另外一個命名空間的變量,需要加上作用域解析符。
  3. 如果頻繁的使用另外一個命名空間的實體,可以使用using將其引入,之後再使用時就不用加作用域解析符了。
double A::Fun()
{
  using B::Fun1;    //使用B命名空間的函數Fun1;
  using C::Var1;    //使用C命名空間的變量Var1;
  
  void Fun1(Var1);  //B::Fun1
}
  1. 在函數中使用using則外部變量的作用域限於函數,如果在命名空間中使用using,則作用範圍擴大到整個命名空間。如果使用到一個命名空間中的多個實體,可以直接用using namespace Name將命名空間中所有的實體都引入,但是讓一個命名空間的所有實體都可以被另一個命名空間訪問並不是一個安全的做法,應盡量避免。
namespace A
{
  using namespace B;    //使用命名空間B中所有實體
}
  1. 如果使用using引入的實體之間或者和本命名空間中的實體有重名的。那麽加上作用域解析符能幫助我們更好的對它們進行區分。

2.3多重界面

有時候我們同一個命名空間在面向不同的用戶時,可能需要提供不同的界面。比如我們有一個命名空間裏面定義了關於串口的一些實體。我們給一個開發中的程序提供的接口可能包括:打開串口,設置波特率,設置校驗位等。但是面向一個最終用戶時,我們可能只需要給他提供一個打開串口接口就夠了。這便是使用多重界面的意義。

  1. 實現多重界面的方法有很多,首先可能想到的是使用不同的命名空間。
namespace A
{
  void Fun1();
  void Fun2();
  void Fun3();
}
namespace A_Interface1
{
  using A::Fun1;
}
  1. 上面界面實現的過程中,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編譯器的名字查找

  1. 一個函數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);  //不行
}

當然,命名空間本身必須在作用域裏,函數也必須在尋找和使用之前聲明。

  1. 如果調用的函數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命名空間的組合

前面我們說了命名空間裏可以包含其他的命名空間,下面給出命名空間組合時的一些具體細節。

  1. 再次強調在命名空間裏沒有聲明的實體,不能在別處定義。即使這個實體在其包含的命名空間裏聲明過了。
namespace A
{
  using namespace B;
}
namespace B
{
  fill(char C);
}

A::fill(char C)   //錯誤 A中並沒有聲明fill函數
{...}
  1. 引入一個命名空間的實體時可以引入它的所有重載。
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中所定義的全部(兩個)+運算
}
  1. 顯示的使用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  
  //其他實體  
}
  1. 當然如果在LibC中聲明了String或者Vector的話就沒有LibA和LibB什麽關系了。
  2. 如果我們想要使用被重載覆蓋的LibAVector和LibBString.除了加上作用域解析符之外,可以使用typedef的形式和繼承的形式。
template<class T>class A_Vector:public LibA::Vector<T>{...};
typedef LibB::String B_String;
  1. 對標準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* ...);
  //...
}
  1. 命名空間是開放的,你可以在不同的文件中或同一個文件中的不同地方給命名空間加新的成員。也非常推薦使用這種方法將命名空間進一步分類,而不是將所有的實體都放到一個大塊的命名空間中。
//file1.c
namespace A
{
  int a;
}
//其他代碼
namespace A
{
  int b;
}

//file2.h
namespace A
{
  int c;
}

C++-命名空間