2013年4月6日

C++ 儲存期間, 範疇, 連結性。

Hi, I am Victor  : )

儲存期間, 範疇, 連結性。

對於儲存資料 C++使用四種不同的方式,

而每一種方式會讓資料存在記憶體的時間有所不同。

1. 自動儲存期間 (automatic storage duration)

2. 靜態儲存期間 (static storage duration)

3. 執行緒儲存期間 (thread storage duration)

4. 動態儲存期間 (dynamic storage duration)



自動儲存期間 (automatic storage duration) :

     函數參數或者宣告在函數中的變數在預設上都為自動儲存期間,

他們屬於區域的範疇 (scope),且並無連結性,也就是說在main裡宣告一個變數,

然後又在main裡面的函式中宣告同樣名稱的變數,這時會產生兩個不同的變數,

也就是說是兩個相同名稱但是記憶體位址不同的變數,在函式內對該變數存取對於

main裡面同名稱的變數而言是不影響的,且當離開其scope該變數就會失效。

例如下例所示:


//main//
#include <iostream>
void autoscp();
int main(){
    using namespace std;
    int in_main=100;   
    int SameName=5;   //在函數main裡的區域變數
    cout<<"In the main SameName= "<<SameName<<"\t"<<
    "The SameName address is "<<&SameName<<endl;      //顯示函數main中的變數值與記憶體位址
    autoscp();   //進入函式
    cout<<"After the function SameName= "<<SameName<<"\t"<<
    "The SameName address is "<<&SameName<<endl;  //證明main中相同名稱的區域變數未被改變
    return 0;
}

void autoscp(){
    using namespace std;
    int SameName=0;   //宣告在此函式中的區域變數,生命周期只存在於這個函式中
    SameName=50;
    cout<<"In the function SameName= "<<SameName<<"\t"<<   //顯示此函數中的變數值與記憶體位址
    "The SameName address is "<<&SameName<<endl;
    {   //這是在函數中的一個block
        int SameName=999;   //在block中選告一個同名稱的變數
        cout<<"In the function's block SameName= "<<SameName<<"\t"<<  
    "The SameName address is "<<&SameName<<endl;   //顯示此函數之block中的變數值與記憶體位址
    }//block之區域變數生命結束
}//函式之區域變數生命週期結束

輸出結果為:
n the main SameName= 5 The SameName address is 0x7fff5bafdc18
In the function SameName= 50    The SameName address is 0x7fff5bafdbec
In the function's block SameName= 999   The SameName address is 0x7fff5bafdbe8
After the function SameName= 5  The SameName address is 0x7fff5bafdc18

由註解可看出,在區塊中的變數會遮蔽(hide)先前的定義,當程式離開後

又會回到新的定義了,由記憶體位址不難看出他們是不同份但相同變數名稱的資料。

自動變數與堆疊:

C++是如何處理自動變數的呢?   由於不難看出自動變數隨著程式的執行由多變少,

所以通常C++在管理自動變數時,是預先配置一塊記憶體位置給他們,

大小由linker預設決定(也可以自行調整),由於這些來來去去的資料是使用疊加的方式,

在進行管控,所以此區塊也稱為堆疊(stack),使用兩個指標分別指向頭端與尾端,

堆疊的設計為LIFO(先進後出),即是後進來的變數最先被移出。

靜態儲存期間 (static storage duration)

靜態儲存期間的變數具有三種連結性:

1. 外部連結性 (檔案之間可存取)

2. 內部連結性 (同一檔案之間的存取)

3. 無連結性 (在函數或區塊內存取)


為初始化的靜態變數會自動將其所有位元設為0 (zero-initialized),

0會被轉成適當的型態,比方來說當為指標時會被轉為null。

先說第一種吧,靜態儲存期間,外部連結性:

具有外便連結性的變數通常稱為外部變數 (external variable),

外部變數定義在函數之外,因此外部變數又稱之全域變數

外部變數必須在要使用該變數的各檔案內做宣告,這又衍生了一個繞口的東西稱為

單一定義規則 (one definition),講白話一點就是只能定義一次啦 ...

以及 參考宣告 (referencing declaration),也就是宣告的意思。

注意! 參考宣告需要加上關鍵字 extern ,參考宣告並不提供初始化

int A;  //定義A,A被初始化為0

extern int B;  //參考宣告不初始化

extern int C=20;  //參考宣告被初始化為20

然而假如數個檔案都使用到全域變數的話,則定義只能有一次(單一定義規則),

而其他檔案如需使用到該全域變數則透過關鍵字 extern 來宣告。

例如下例所示:
//staticscp.h//
#ifndef STATICSCP_H   //前置處理器
#define STATICSCP_H
extern int number;    //參考宣告要使用區域變數
void show();
#endif

//staticscp.cpp//
#include "staticscp.h"
#include <iostream>
void show(){
    using namespace std;
    number=number+100;   //對區域變數做存取
    cout<<"In staticscp.cpp "<<number<<" In staticscp.cpp number address= "<<&number<<endl;
}

//main.cpp//
#include <iostream>
#include "staticscp.h"
int number=100;   //定義區域變數
int main(){
    using namespace std;
    cout<<"In main() "<<number<<" In main() number address= "<<&number<<endl;
    show();
    cout<<"In main() after show() "<<number<<" In main() after show() number address= "<<&number<<endl;
    return 0;
}

輸出結果:
In main() 100 In main() number address= 0x60126c
In staticscp.cpp 200 In staticscp.cpp number address= 0x60126c
In main() after show() 200 In main() after show() number address= 0x60126c

不難看出都是使用同一份資料,且具有外部連結性。

假如又有另一個相同名稱的變數定義在另一個函式內,當程式執行至該函式時

全域變數會被hide起來,也就是產生了另一份自動變數。

當一份資料會被許多函式所利用時,你可以把該資料宣告為全域變數。

甚至可以用const關鍵字保護資料不備更動。

例如:
const char * const number[5]=
{
   "one","two","three","four","five"
};


內部連結性 (同一檔案之間的存取)

使用static修飾字可讓該變數提供內部連結性,而內部連結性代表只有該檔案可存取他,

當然他仍然要滿足單一定義規則 (指同一檔案之範疇之內),

假如另一個檔案也有相同名稱之變數,這並沒有違反單一定義規則,因為他並沒有外部定

義,所以不用擔心會互相衝突。

如下例所示:
//staticscp//
#include <iostream>
static int number=10;   //複寫全域變數
static int staticnumber=1234;
void show(){
    using namespace std;
    number=number+10;
    cout<<"In staticscp.cpp "<<number<<" In staticscp.cpp number address= "<<&number<<endl;
    
    cout<<"In staticscp.cpp "<<staticnumber<<" In staticscp.cpp staticnumber address= "<<&staticnumber<<endl;
}

//main//
#include <iostream>
int number=100;
static int staticnumber=500;
void show();
int main(){
    using namespace std;
    cout<<"In main() "<<staticnumber<<" In main() staticnumber address= "<<&staticnumber<<endl;
    show();
    cout<<"In main() after show() "<<staticnumber<<" In main() after show() staticnumber address= "<<&staticnumber<<endl;
    
    return 0;
}

輸出結果為:
In main() 500 In main() staticnumber address= 0x601270
In staticscp.cpp 20 In staticscp.cpp number address= 0x601274
In staticscp.cpp 1234 In staticscp.cpp staticnumber address= 0x601278
In main() after show() 500 In main() after show() staticnumber address= 0x601270

可以很明顯的看出全域變數與內部連結性沒有互相衝突。

無連結性 (在函數或區塊內存取)

使用static修飾字宣告至函式或區塊內,這意思是在函式或區塊內知道該變數的存在,

但是一旦離開了該區域該變數仍然會存在,因此靜態區域變數對於資料重現是有用的,

值得一提的是,在函式或者區塊中宣告並初始化區域變數,而程式只會初始化一次,

往後程式都不會在初始化該變數了。

如下例所示:
//main.cpp//
#include <iostream>
void local();
int main(){
    using namespace std;
    int count=99999;
    local();
    local();
    cout<<"count= "<<count<<endl;   //看不見靜態區域變數
    cout<<"address is "<<&count<<endl;   
    return 0;
}

void local(){
    using namespace std;
    static int count=0;   //初始化一次,之後就一直存在至記憶體中直到程式結束
   for(int i=0;i<100;i++){
       count++;
   }
   cout<<"count= "<<count<<endl;
   cout<<"address is "<<&count<<endl;
}

輸出結果:
count= 100
address is 0x600fe4
count= 200
address is 0x600fe4
count= 99999
address is 0x7fff792528cc

const問題

     在C++中const預設為內部連結性,所以當你在具有外部連結性的變數上使用const關鍵字

時,const會悄悄的改變該變數的連結性,也就是說全域const將被視為使用static修飾字,例如

你有個變數想放置在標頭檔,而該標頭檔你時常會include它,這時假如你講該變數設為

全域變數,當你include多次標頭檔時就會產生錯誤,這是因為全域變數具有外部連結性,

造成了違反單一定義規則,也就是說只有一個檔案可以對該變數宣告,其他檔案必須使用

參考宣告 (extern),使用const可以在每個檔案宣告,因為它是具有內部連結性。

然而使用const並不代表共用該變數,而是每個檔案各自有一份,假如你有某些理由

想要變數具有外部連結性可使用extern覆蓋const預設的內部連結性:

extern const int number = 10 ;

與一般定義不同的地方在於,一般變數定義不需要使用extern關鍵字,單獨使用extern之關鍵字

變數也不可以初始化,但是在這邊可以,因為你是初始化 extern const 變數。

同樣在區塊中宣告const 變數 也是具有區塊範疇,不用擔心會與其他地方變數衝突。


函式與連結性

     C++函式同樣也會有連結性,基本上函式都自動具有靜態儲存期間,且預設為外部連結

性,你可以再函式圓形中使用extern,這是隨意的代表此函數定義在另一個檔案。也可以

使用關鍵字static提供函式內部連結性,也就是說它只能限制使用在該檔案中,外面也會有

同樣名稱的函式存在,但靜態函式會覆蓋外部定義,也就是該檔案優先使用該版本函式,

如: static int test(int x);

C++函式也有個單一定義,就是非"內嵌"(inline)函式只能有一份定義,對於具外部連結性的

函式則只能有一個檔案有定義,其他需使用該函式的檔案需具有函式原型才可以使用。



動態儲存期間 (dynamic storage duration)

我們稱這類記憶體為動態記憶體,大家都知道動態記憶體是要用new與delete控制的,

所以它不受範疇與連結性規範。     float *a=new float [20];   一共配置了80個bytes(假設float佔

4bytes),記憶體直到使用delete才會被釋放,但是指向其空間的指標卻會隨著其包含函式消失

而消失,所以需要傳遞其位址 (除非你宣告指標為全域,這樣每個檔案其實都可以用)。

使用new運算子初始化

double *pi=new double(3.1415);     //利用括號封裝達成

初始化結構可以這樣做,例如有個結構為:     struct Coordinate {double x;double y;double z;};

則可,  Coordinate *first =new Coordinate {1.2,2.4,4.8};

定位放置new運算子

new只需要幫我們找出一塊足夠大的記憶體空間就好了,但是new有一個特殊的東西,

稱為定位放置 new (placement new) , 可以讓我們使用正在使用的位置。

你可能需要建立自己的記憶體管理程序,或處理特定位址做存取的硬體。

要使用必須先匯入new 標頭檔,如下例所示:

///main..cpp///
#include <iostream>
#include <new>
const int size=512;
const int N=5;
char buffer[size];   //宣告靜態的全域記憶體空間
int main(){
    using namespace std;
    int *ptr1,*ptr2;   //宣告int指標
    ptr1= new int[N];  //宣告動態記憶體空間
    ptr2= new (buffer) int[N];   //使用placement new,利用已經宣告好的buffer空間
    cout<<"heap at "<<ptr1<<endl;  //heap內空間起始記憶體位址
    cout<<"buffer at "<<&buffer<<endl;   //buffer空間起始記憶體位址
    for(int i=0;i<N;i++){
        ptr1[i]=ptr2[i]=i*10;
    }
    for(int i=0;i<N;i++){
        cout<< ptr1[i] <<" at "<< &ptr1[i]<<" ";
        cout<< ptr2[i] <<" at "<< &ptr2[i] <<endl;
    }
    delete []ptr1;  //釋放記憶體
    int *ptr3;
    ptr3=new (buffer) int[N];
    for(int i=0;i<N;i++){
        ptr3[i]=i*10;
        cout<< ptr3[i] <<" at "<< &ptr3[i]<<endl;
    }
    ptr1= new int[N];  //重新宣告記憶體空間
    ptr2= new (buffer+(N*sizeof(int))) int[N];
    for(int i=0;i<N;i++){
        ptr1[i]=i*10;
        ptr2[i]=i*10;
        cout<< ptr1[i] <<" at "<< &ptr1[i]<<" ";
        cout<< ptr2[i] <<" at "<< &ptr2[i] <<endl;
    }
    delete []ptr1;
    return 0;
}



輸出結果:
heap at 0x1cf8010
buffer at 0x6013a0
0 at 0x1cf8010 0 at 0x6013a0
10 at 0x1cf8014 10 at 0x6013a4
20 at 0x1cf8018 20 at 0x6013a8
30 at 0x1cf801c 30 at 0x6013ac
40 at 0x1cf8020 40 at 0x6013b0
0 at 0x6013a0
10 at 0x6013a4
20 at 0x6013a8
30 at 0x6013ac
40 at 0x6013b0
0 at 0x1cf8010 0 at 0x6013b4
10 at 0x1cf8014 10 at 0x6013b8
20 at 0x1cf8018 20 at 0x6013bc
30 at 0x1cf801c 30 at 0x6013c0
40 at 0x1cf8020 40 at 0x6013c4

由結果可看出使用一般動態記憶體需要自行釋放,而placement new方法則要看你是使用哪類

型空間,此例為使用靜態的空間所以也無須釋放,由結果可看出最後placement new的方式,

可以利用偏移的方式選擇要將資料放置在哪段記憶體位置。


本文參考   C++ Primer Plus

程式部分仿造書中例子重寫。

謝謝

By   Victor

沒有留言:

張貼留言