第十天(內存模型和名稱空間)

       看日期,隔了10天了……痛恨自己的懶惰。來個長篇幅的,以稍補愧疚之心。


2011-10-22(Memory Models And Namespace)
1、頭文件一般包括:函數原型、符號常數(#define和const定義)、結構體聲明、模板聲明、內聯函數、類聲明。不能有函數的定義或者變量的聲明。因爲如果在一個大的程序中,有兩個子文件同時include了這個頭文件,則在同一個程序中包含了兩個函數的定義模式不允許的。有一種方法可以避免這樣的“重複定義”發生。這個方法是爲了避免在不知情的情況下在同一源文件在包含多次同一頭文件,而不是作爲可以在頭文件中定義函數的理由。

#ifndef RARE_NAME
#define RARE_NAME
//... ...
#endif
意思是:如果編譯器沒有定義(#ifndef ->if not define)這個RARE_NAME這個變量,則從下一行至#endif之間的代碼編譯。這個RARE_NAME是其他地方不太可能被定義的名稱。
2、C++存儲數據的三種方案。這些方案的區別在於保留在內存中的時間。
①自動儲存連續性(Automatic storage duration)。創建於所屬函數或者代碼塊被執行之時,釋放於執行完畢。故稱“自動”。C++中有兩種有這樣特性的變量;
②靜態儲存連續性(Static storage duration)。函數外定義的變量或者被修飾爲static,整個程序運行中都存在;
③動態存儲連續性(Dynamic storage duration)。用new操作符分配,直到用delete或者程序結束才釋放。
3、自動儲存連續性。這樣的變量在不同函數(包括main)或代碼塊中互不影響。如果在代碼塊中存在另一個代碼塊,且定義了同一的變量,即:
{
    int a = 0;
    {
        int a = 5;
       //... ...
    }
}
在子代碼塊中的變量塊會屏蔽掉父代碼塊中的。可以使用關鍵字auto去強調(注意是強調作用而不是將一個非自動變量轉變成自動)這個是一個自動儲存連續性的變量。
*自動變量一般用棧來管理。當函數被調用,自動變量加入到棧中,結束之時,棧頂指針重設爲未加入變量位置。
*另一種有這個特性的是寄存器變量(register variable)。即用寄存器直接儲存變量而不是一般的用棧儲存,這樣做的好處是使用和修改快速。使用關鍵字register來提醒:
                        register int fast_counter;
注意是用了字眼“提醒”,編譯器不一定會滿足上述要求。不過這種“不確定性”不影響整體速度或者影響非常小,因爲編譯器會自動使用寄存器來儲存變量,比如for循環的計數器。
4、靜態儲存連續性。有這樣特性變量有三種:外部連接性(external linkage)、內部連接性(internal linkage)和無連接性,區別在於作用域(scope)的大小。當在函數外面定義變量且無static關鍵字修飾,爲外部連接性;當在函數外定義變量且有static關鍵字修飾,爲內部連接性;當在一個函數內或代碼塊定義變量且有static關鍵字修飾,爲無連接性。如: 
int globe = 5;            //external linkage
static int file = 10;     //internal linkage
int main(){}
func()
{
    static int count = 1; //no linkage
}
不管哪種,都必須賦值爲常量(const、enum、sizeof()返回值)。如果沒賦值,將自動賦值爲零。
①外部連接性。有這個性質的變量作用域將是整個程序(可有多個源文件),所以也被稱爲全局變量(global variable)。如此它與自動變量有交集,便有一些新的處理方式。看下例: 
#include <iostream>

void testg();
void modify();
int g = 5;

using namespace std;
int main()
{
    cout << "Before modify() & testg(),g = " << g << endl;
    modify();
    cout << "After modify(), g = " << g << endl;
    testg();
    cout << "After testg(), g = " << g << endl;
}

void modify()
{
    extern int g;
    g += 5;
}

void testg()
{
    int g = 20;
    cout << g << " ";
    cout << ::g++ <<endl;
}
輸出: 
                Before modify() & testg(),g = 5
                After modify(), g = 10
                20 10
                After testg(), g = 11

*先看函數testg(),這裏聲明瞭一個與全局變量同名的局部變量,與前述一致,將屏蔽掉全局變量。操作符"::"作用是調用屏蔽掉的全局變量,對::g的修改即是對全局變量的修改;
*函數modify()出現關鍵字extern,這個關鍵字的作用是說明這個函數裏g不是局部變量,對g的操作即對全局變量操作。說到這裏,有必要說下定義性聲明(defining declaration)和引用性聲明(referencing declaration)。顧名思義,定義性是帶動作的,是要給變量分配內存空間的,簡稱定義;引用性則是引用別的同名變量,簡稱聲明(狹義)。這裏,extern int g;是引用性聲明,聲明這個g是已經存在的,把它引用到這個modify函數;int g = 5;則是定義性聲明。
*另外,語句:
                         extern int g = 10;
是非法的。畢竟,初始化是包括分配內存空間和賦值兩個過程,上面這個語句缺少前者。
②內部鏈接性。內部和外部連接性的關係與內部連接性和自由變量關係相像:當一個文件聲明瞭一個外部連接性變量,另一個文件要使用它,必須使用引用性聲明;如果這個文件定義了一個內部連接性的變量,會屏蔽掉另一個文件中的同名全局變量。
③無連接性。其作用域仍然是代碼塊(局部的),不過在代碼塊不活動時,仍然存在。
5、關鍵字mutablevolatile
mutable這個關鍵字的作用是當一個整體(比如結構體)被修飾爲const的時候,如果裏面的成員被修飾爲mutable,則在整體被修飾爲const的情況下,這個成員的仍可修改的,如:
struct data
{
    mutable int i;
    double d;
}
//... ...
const data num{5, 7.8};
num.i = 10;    //valid
num.d = 12.5;  //invalid
②volatile。用作改善編譯器的優化能力。比如當編譯器發現程序在幾條語句中使用了同一變量,它就會將這個變量存在寄存器中而不是連續訪問變量幾次。但這種優化是建立在變量不被修改的情況下進行的。當然如果是程序自身修改變量,編譯器不會進行這樣的優化,但如果這種修改是來自物理的呢,比如硬件運行的時候會修改某些值。這是如果把這樣不受程序控制,不穩的變量修飾爲volatile,編譯器就必定不會進行這樣的優化。
*mutable、auto、static、extern、register都被稱爲儲存修飾符(storage class specifier);const、volatile被稱爲cv-限定符(cv-qualifier)。
6、默認情況下,在函數外用const修飾變量與用static獲得的連接性等價。這可能考慮到符號常量是定義在頭文件裏面的,一個正常現象是:一個程序裏有多個源文件,每個源文件包含了同一頭文件。如果這個頭文件裏面定義的符號常量是全局變量,則會犯重複定義的錯誤。當然,如果有特殊需要,可以將符號常量設爲全局變量:
                 extern const int gobal = 100;
有與符號常量在聲明的同時必須初始化,所以這裏被extern修飾仍可初始化。
7、函數的鏈接性。C++不允許在一個函數中定義另一個函數,所以所有的函數都爲靜態,即在存在於整個程序生命期間。默認情況下,函數是外部連接性的,即可在文件中共享,但另一個文件想使用別的文件定義的函數必須提供函數原型,這個函數原型可以在前面加個關鍵字extern,表明這個函數是用別人的,不是這個源文件定義的。可以使用static關鍵字定義一個內部連接性的函數:
                 static returnType FName(parameterList);
與變量一樣,內部的可以屏蔽外部的。
*C++關於函數的定義有一條規則:非內聯函數程序中只能定義一次。內聯函數不受這條規則約束,因爲它定義在頭文件中。即便不是在頭文件中定義,在同一程序中不同文件中的內聯函數的定義也要求相同。
8、佈局new操作符(placement new opeartion)。常規new操作符所找到的空間是隨機地,但C++卻可以做到,前提是程序員提供一個有名字的內存空間,後使用new操作符使用這個空間。看個例子:
#include <iostream>
#include <new>

using namespace std;
const int LEN = 512;
char buffer[LEN];

int main()
{
    int *p1, *p2;
    p1 = new int;
    p2 = new (buffer) int;

    cout << "buffer = " << (void *)buffer << endl;
    cout << "p1 = " << p1 << " " << "p2 = " << p2 << endl;
    delete p1;

    p1 = new int;
    p2 = new (buffer) int;
    cout << "p1 = " << p1 << " " << "p2 = " << p2 << endl;
    delete p1;

    p2 = new (buffer + sizeof(int)) int;
    cout << "p2 = " << p2 << endl;
    return 0;
}
輸出:
                    buffer = 00419148
                    p1 = 003954D8 p2 = 00419148
                    p1 = 003953A8 p2 = 00419148
                    p2 = 0041914C

這裏採用定義char數組的方式創建了一個512字節的內存緩衝區,new後面跟這內存緩衝區名稱,即可把這個內存分給p2。使用這個操作需要包含頭文件new #include<new>;
知道了這個分配機制就不難明白輸出結果了;
新式的new不需要使用delete來釋放,因爲它是靜態內存,而delete只作用於動態內存空間。
9、名稱空間。C++加入了名稱空間這一概念是爲了避免同名變量衝突。名稱空間其實是將C++可能的名稱(結構體名稱、類名、變量等)集中管理。
I、基本性質。定義一個名稱空間使用關鍵字namespace,如:
namespace micro
{
    int lik;
    int add(int, int);
    struct st{...};
}
名稱空間可以位於另一個名稱空間中,也可以是全局的,但不能位於代碼塊中。除了用戶定義的名稱空間,還存在一個名稱空間,這就是爲什麼能在全局變量被屏蔽的時候依然可以調用它。
*名稱空間是開放的(open),即任何時候只要想起需要將某名稱加入到一個已存在的名稱空間中都可以加入:
                  namespace micro{ char ch; }
利用這個性質,可以將先前已經存在的函數原型實現:
                  namespace micro{ int add(int a, int b){...} }
II、名稱的調用。有三種方式:直接加空間前綴、using聲明和using編譯指令。三者越來越方便但也問題越來越多。
①第一種,採用操作符"::"調用。比如micro::lik = 5;就是對名稱空間micro中lik的操作。由於名稱空間中名稱遵循名稱的屏蔽原則(如:自由將屏蔽靜態的),所以這麼使用基本不會有什麼問題。
using聲明。如using micro::lik,那麼後面使用變量lik時,實際使用的是micro中的lik。這麼使用,問題也浮現出來了:
void func() {
    using::lik;
    int lik;     //Error, already have local lik
}
如果using聲明在函數外使用,這個名稱將加入到全局名稱空間中,如:
using micro::lik                          //add to globe namespace
int main()
{
    lik = 100;
    std::cout << ::lik << std::endl;      //print 100
    std::cout << micro::lik << std::endl; //print 100, too
}
③using編譯指令作用是使整個名稱空間可用,如
                    using namespace micro;
等價於名稱空間所有名稱都使用了using聲明。這樣做問題更多,問題之一是上面的提到的重複定義問題。問題之二的來源是程序員不知道聲明瞭那些變量,很可能在代碼塊中聲明同名與名稱空間同名的變量,導致被屏蔽。名稱空間的開放性使這個問題嚴重化。
III、高級特性。
①可鑲嵌。如:
namespace n1
{
    namespace n2
    {
       int ele;
    }
}
需調用之時,這麼調用n1::n2::ele,using聲明和編譯指令同理。
②在名稱空間中使用using聲明和編譯指令。
namespace n1
{
    using n2::ele;
    using namespace n3;
}
則,對n1::ele的操作即是對n2::ele的操作;因爲using編譯指令在名稱空間中,如果想使用n3中的名稱,下面的語句是等價的:
                    using namespace n1;
                    using namespace n3;

③可創建別名。如果一定義名稱空間myFavoriteSubject,這個空間名字過長,可以這樣寫:
                    namespace mfs = myFavoriteSubject;
這樣即可如使用myFavoriteSubject一樣使用mfs。另外,上面提到的鑲嵌操作,如果層數較多,比如:n1::n2::n3::n4::ele,則可以這樣寫:
                    namespace nn = n1::n2::n3:‎:n4
                    using nn::ele;

④匿名名稱變量。比如:
namespace
{
    int globe;
}
這樣做的結果是其他源文件不能使用globe,只能在當前文件直接使用。如此相當於使用了static修飾了globe,可用於代替static。事實上,C++標準不鼓勵使用static而是採取匿名名稱空間的方式定義靜態內部連接性變量。


發佈了53 篇原創文章 · 獲贊 0 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章