每個類都會定義它自己的作用域。在類的作用域之外,普通的數據和函數成員只能由對象、引用或者指針使用成員訪問運算符來訪問。對於類類型成員則使用作用域運算符訪問。不論哪種情況,跟在運算符之后的名字都必須是對應類的成員:
Screen::pos ht=24,wd=80; //使用Screen定義的pos類型
Screen scr(ht,wd,' ');
Screen *p=&scr;
char c=scr.get(); //訪問scr對象的get成員
c=p->get(); //訪問p所指對象的get成員
作用域和定義在類外部的成員
一個類就是一個作用域的事實能夠很好地解釋為什么當我們在類的外部定義成員函數時必須同時提供類名和函數名。在類的外部,成員的名字被隱藏起來了。
一旦遇到了類名,定義的剩余部分就在類的作用域之內了,這里的剩余部分包括參數列表和函數體。結果就是,我們可以直接使用類的其他成員而無須再次授權了。
例如,Window_mgr類的clear成員,該函數的參數用到了Window_mgr類定義的一種類型:
void Window_mgr::clear(ScreenIndex i) { Screen &s=screens[i]; s.contents=string(s.height*s.width,' '); }
因為編譯器在處理參數列表之前已經明確了我們當前正位於Window_mgr類的作用域中,所有我們不必再專門說明ScreenIndex是Window_mgr類定義的。出於同樣的原因,編譯器也能知道函數體中用到的screens也是在Window_mgr類中定義的。
另一方面,函數的返回類型通常出現在函數名之前。因此當成員函數定義在類的外部時,返回類型中使用的名字都位於類的作用域之外。這時,返回類型必須指明它是哪個類的成員。例如,我們可能想Window_mgr類添加一個新的名為addScreen的函數,它負責先顯示器添加一個新的屏幕。這個成員的返回類型將是ScreenIndex,用戶可以通過它定位到指定的Screen:
class Window_mgr{ public: //向窗口添加一個Screen,返回它的編號 SceenIndex addScreen(const Screen&); }; //首先處理返回類型,之后我們才進入Window_mgr的作用域 Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s) { screens.push_back(s); return screens.size()-1; }
因為返回類型出現在類名之前,所有事實上它是位於Window_mgr類的作用域之外的。在這種情況下,要想使用ScreenIndex作為返回類型,我們必須明確指定哪個類定義了它。
名字查找與類的作用域
到目前為止,我們編寫的程序中,名字查找的過程比較直截了當:
- 首先,在名字所在的塊中尋找其聲明語句,值考慮在名字的使用之前出現的聲明
- 如果沒找到,繼續查找外層作用域
- 如果最終沒有找到匹配的聲明,則程序報錯
對於定義的類內部的成員函數來說,解析其中名字的方式與上述的查找規則有所區別。類的定義分兩步處理:
- 首先,編譯成員的聲明。
- 直到類全部可見后編譯函數體。
編譯器處理完類中的全部聲明后才會處理成員函數的定義。
按照這種兩階段的方式處理類可以簡化類代碼的組織方式。因為成員函數體直到整個類可以后才會被處理,所以它能使用類中定義的任何名字。相反,如果函數的定義和成員的聲明被同時處理,那么我們將不得不在成員函數中只使用那些已經出現的名字。
用於類成員聲明的名字查找
這種兩階段的處理方式只適用於成員函數中使用的名字。聲明中使用的名字,包括返回類型或者參數列表中使用的名字,都必須在使用前確保可見。如果某個成員的聲明使用了類中尚未出現的名字,則編譯器將會在定義該類的作用域中繼續查找。例如:
typedef double Money; string bal; class Account{ public: Money balance() { return bal;} private: Money bal; //... };
當編譯器看到balance函數的聲明語句時,它將在Account類的范圍內尋找對Money的聲明。編譯器只考慮Account中在使用Money前出現的聲明,因為沒找到匹配的成員,所以編譯器會接着到Account的外層作用域中查找。在這個例子中,編譯器會找到Money的typedef語句,該類型被這樣balance函數的返回類型以及數據成員bal的類型。另一方面,balance函數體在整個類可見后才被處理,因此,該函數的return語句返回名為bal的成員,而非外層作用域的string對象。
類型名要特殊處理
一般來說,內層作用域可以重新定義外層作用域中的名字,即使該名字已經在內層作用域中使用過。然而在類中,如果成員使用了外層作用域中的某個名字,而該名字代表一種類型,則類不能在之后重新定義該名字:
typedef double Money; string bal; class Account{ public: Money balance() { return bal;} //使用外層作用域的Money private: typedef double Money; //錯誤:不能重新定義Money Money bal; //... };
需要特別注意的是,即使Account中定義的Money類型與外層作用域一致,上述代碼仍然是錯誤的。
盡管重新定義類型名字是一種錯誤的行為,但是編譯器並不為此負責。一些編譯器仍將順序通過這樣的代碼,而忽略代碼有錯的事實。
類型名的定義通常出現在類的開始處,這樣就能確保所有使用該類型的成員都出現在類名的定義之后。
成員定義中的普通塊作用域的名字查找
成員函數中使用的名字按照如下方式解析:
- 首先,在成員函數內查找名字的聲明。和前面一樣,只有在函數使用之前出現的聲明才被考慮。
- 如果在成員函數內沒有找到,則在類內繼續查找,這時類的所有成員都可以被考慮。
- 如果類內也沒用找到該名字的聲明,在成員函數定義之前的作用域內繼續查找。
一般來說,不建議使用其他成員的名字作為某個成員函數的參數。不過為了更好的理解名字的解析過程,我們不妨在dummy_fcn函數中暫時違反一下這約定:
//通常情況下不建議為參數和成員使用同樣的名字
int height; //定義了一個名字,稍后將在Screen中使用
class Screen{
public:
typedef string::size_type pos;
void dump_fcn(pos height){
cursor=width*height; //哪個height?是哪個參數
}
private:
pos cursor=0;
pos height=0,width=0;
};
當編譯器處理dummy_fcn中的乘法表達式時,它首先在函數作用域內查找表達式中用到的名字,函數的參數位於函數作用域內,因此,dummy_fcn函數體內用到的名字height指的是參數聲明。
在此例中,height參數隱藏了同名的成員。如果想繞開上面的查找規則,應該將代碼變為:
//不建議的寫法:成員函數中的名字不應該隱藏同名的成員
void Screen::dummy_fcn (pos height){
cursor=width*this->height; //成員height
//另外一種表示該成員的方式
cursor=width*Screen::height; //成員height
}
盡管類的成員被隱藏了,但我們仍然可以通過加上類的名字或顯式地使用this指針來強制訪問成員。
其實最好的確保我們使用height成員的方法是給參數起個其他的名字:
//建議的寫法:不要把成員名字作為參數或其他局部變量使用
void Screen::dummy_fcn(pos ht){
cursor=width*height; //成員height
}
在此例中,當編譯器查找名字height時,顯然在dummy_fcn函數內部是找不到的。編譯器接着會在Screen內查找匹配的聲明,即使height的聲明出現在dummy_fcn使用它之后,編譯器也能正確地解析函數使用的是名為height的成員。
類作用域之后,在外圍的作用域中查找
如果編譯器在函數和類的作用域中都沒有找到名字,它將接着在外圍的作用域中查找。在我們的例子中,名字height定義在外層的作用域中,且位於Screen的定義之前。然而,外層作用域中的對象被名為height的成員隱藏了。因此,如果我們需要的是外層作用域中的名字,可以顯式地通過作用域運算符來進行請求:
//不建議的寫法:不要隱藏外層作用域中可能被用到的名字
void Screen::dummy_fcn(pos ht){
cursor=width*::height; //成員height
}
盡管外層的對象被隱藏了,但我們仍然可以用作用域運算符訪問它。
在文件中名字的出現處對其進行解析
當成員定義在類的外部時,名字查找的第三步不僅要考慮類定義之前的全局作用域中的聲明,還需要考慮在成員函數定義之前的全局作用域中的聲明。例如:
int height; //定義了一個名字,稍后將在Screen中使用 class Screen{ public: typedef string::size_type pos; void setHeight(pos); pos height=0; //隱藏了外層作用域中的height }; Screen::pos verify(Screen::pos); void Screen::setHeight(pos var) { //var: 參數 //height:類的成員 //verify:全局函數 height=verify(var); }
請注意,全局函數verify的聲明在Screen類的定義之前是不可見的。然而,名字查找的第三步包括了成員函數出現之前的全局作用域。在此例中,verify的聲明位於setHeight的定義之前,因此可以被正常使用。
