原創 C++之常量(二)

4常量的內存分配

4.1應用程序的內存結構

一個由C++編譯的應用程序,佔用的內存能夠劃分爲以下幾個部分:程序員

  • 棧(stack)。由編譯器自動分配釋放。存放函數參數和函數裏的局部變量(又稱自動變量)。其操做方式相似於數據結構中的棧。例如,聲明在函數中一個局部變量int x; 系統自動在棧中爲x分配一塊空間,該空間存儲x的值。
  • 堆(heap)。用於動態內存空間分配。通常由程序員進行分配和釋放,若程序員不釋放,程序結束時可能由操做系統回收。注意它與數據結構中的堆是兩回事,分配方式相似於鏈表。內存分配在C中使用malloc函數,在C++中用new操做符。如下爲C語言小示例:p1 = (char *)malloc(10);注意p1自己是在棧中的,而malloc(10)的數據存放在堆中,p1只是指向了這些數據。
  • 全局區(靜態區)(static)。全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另外一塊區域。程序結束後由系統釋放。
  • 文字常量區。常量字符串就是放在這裏(全局區)。程序結束後由系統釋放。
  • 程序代碼區。存放函數體的二進制代碼。

具體代碼舉例以下:數組

//main.cpp數據結構

int a = 0;                                      // 全局初始化區函數

char *p1;                                     // 全局未初始化區性能

main()spa

{操作系統

int b;                                    // 棧設計

char s[] = "abc";      // 變量s在棧中,文字常量」abc」在文字常量區。將從文字常量區向棧中執行初始化操做。即:將」abc」從文字常量區拷貝到棧中爲數組s分配的內存區域中。指針

char *p2;                                     // 棧對象

char *p3 = "123456"; // 123456\0在文字常量區,p3在棧上。P3指向文字常量區的地址

static int c =0;       // 全局(靜態)初始化區

p1 = (char *)malloc(10); //分配的10字節的區域在堆區。

p2 = (char *)malloc(20); //分配的20字節的區域在堆區。

strcpy(p1, "123456");     // 123456\0放在文字常量區,p1指向的內容在堆中。將從文字常量區向堆中執行數據拷貝。

free(p1);

free(p2);

}

編譯生成的二進制代碼中,各個變量的名稱將轉換成對應的地址。對於應用程序,其設計的變量一般轉換爲絕對內存地址,即程序運行時這些變量實際對應的地址。做爲動態連接庫等共享庫的代碼中的變量的地址一般爲相對內存地址,當動態連接庫被加載時,這些相對內存地址會進行從新定位,轉換爲對應的內存地址。

4.2文字常量的內存分配

在程序代碼中出現的文字常量,在編譯階段,由編譯器分配內存到文字常量區。文字常量不可尋址,不能對文字常量執行取地址操做。若是在程序代碼中出現多個相同的文字常量,那麼在文字常量區只保持該文字常量的一份拷貝,其餘使用該文字常量的代碼將今後拷貝處取值。具體代碼舉例以下:

Char* pStr = 「lifeng」;

Char arrStr[] = 「lifeng」;//假設arrStr定義在局部做用域中

在文字常量區,只保持文字常量」lifeng」的一份拷貝。指針pStr指向文字常量區中文字常量」lifeng」的首地址;變量arrStr在棧上分配內存空間,在數組初始化的時候,從文字常量區將字符串」lifeng」拷貝到棧上爲該數組分配的內存空間中。

文字常量區具備只讀屬性,任何對該區數據的修改操做都會引發錯誤。

4.3符號常量的內存分配

符號常量的內存分配狀況以下表所示:

 

全局做用域

文件做用域

局部做用域

內存分配

在文字常量區分配內存,該區域內存具備只讀屬性。任何試圖更改該區數據值的操做都會引發錯誤。

不分配內存,符號常量的名稱只保存在名稱列表中。若是在代碼中執行取符號常量地址的操做,也會引發內存分配。

在棧中分配內存,因爲棧不具備只讀屬性,所以,符號常量的只讀性由編譯器在編譯階段保證

常量摺疊

不執行常量摺疊

執行常量摺疊

執行常量摺疊

內存分配的時刻

編譯時

 

運行時

 

符號常量默認具備文件做用域,在編譯時刻,若是其值明確,編譯器將執行常量摺疊,而且不會爲該符號常量分配內存。若是對符號常量執行了取地址操做,或者對符號常量使用了關鍵字extern,那麼在編譯階段,編譯器將爲該符號常量分配內存。

若是想讓符號常量具備全局做用域,或者符號常量的值在編譯時刻不可知,這時候就須要對符號常量使用關鍵字extern。

關鍵字const只是告訴編譯器由它修飾的數據其內容不能更改,這個規則是在編譯階段控制的。在編譯的時候,若是編譯器發現有更改常量數據值的代碼,那麼編譯器就會報錯。

 

5符號常量與指針的關係

符號常量與指針結合之後,將會涉及到三方面的內容,它們分別是:

l  指針常量,即指針自己不可變,定義以後必須初始化;

l  指向常量的指針,即指針所指向的數據值不可變,定義以後能夠不初始化;

l  指向常量的指針常量,即指針自己和指針所指向的數據值均不可變,定義以後必須初始化。

具體的示例代碼以下:

//指針常量,即指針所指向的地址不可變。

Char * const pStr = 「lifeng」;//能夠用文字常量初始化字符類型指針

Int a = 10;

Int * const pInt = &a;//pInt不能再指向其餘的地址,但能夠經過pInt改變a的值

//指向常量的指針,即指針所指向的數據值不可變

Const char* pStr = 「lifeng」;//定義並初始化

Const char* pChar;//定義以後能夠不初始化

pChar = 「lifeng」;//後續賦值

 

Int a = 10;

Const int* pInt = &a;//指向常量的指針能夠存儲變量的地址。不能經過pInt改變a的值。

//指向常量的指針常量,即指針自己和指針所指向的數據值均不可變

Const char * const pStr = 「lifeng」;

Int a = 10;

Const int * const pInt = &a;

這裏有一個規則,關鍵字const位於星號(*)的左邊表示指針所指向的數據的值不可變;關鍵字const位於星號(*)的右邊表示指針自己不可變。

指針與常量和變量之間的關係以下圖所示:

   

指向常量的指針便可以保存符號常量的地址,也能夠保存普通變量的地址。不能經過指向常量的指針去改變它所指向的常量或變量的數據值。指向變量的指針不能存儲符號常量的地址,由於經過指向變量的指針能夠改變它所指向的數據的值。若是指向變量的指針指向了符號常量,那麼就能夠改變該符號常量的數據值,這是不容許的。

關鍵字const所修飾的指針的常量性是由編譯器在編譯階段保證的。

6符號常量與引用的關係

從定義上來講,引用是一個變量的別名,全部對該引用的操做都至關於對這個變量的操做。但從本質上來說,引用就是指針。可是引用是被限制了的指針,它至關於指針常量。引用一旦指向一個變量之後,就不能再次指向其餘的變量。

引用是C++語法範疇的概念,提出引用的概念是爲了方便程序員編寫程序。從彙編語言的角度來看,在處理方式上,引用和指針沒有卻別。也就是說,在彙編的層面上,不存在引用的概念。具體代碼以下:

-------------------------------C++代碼---------------------------------

double pi = 3.14;

 

double& ypi = pi;

 

double* ppi = π

--------------------------------------------彙編代碼----------------------------------------

     double pi = 3.14;//爲浮點數據分配內存,並初始化

00A4141E  fld         qword ptr [__real@40091eb851eb851f (0A45800h)]

00A41424  fstp        qword ptr [pi]

 

     double& ypi = pi;//取出pi的地址,並賦給ypi

00A41427  lea         eax,[pi]

00A4142A  mov         dword ptr [ypi],eax

 

     double* ppi = π//取出pi的地址,並賦給ppi。從這兩處代碼能夠看出,對指針和引用的處理方式是同樣的

00A4142D  lea         eax,[pi]

00A41430  mov         dword ptr [ppi],eax

 

若是關鍵字const不與引用結合,那麼只能採起下面的方式定義和初始化引用:

//只能採用下面的方式定義,並初始化引用

Int a = 10;//定義變量a

Int& b = a;//定義a的引用。

//下面的做法是錯誤的

Int& b = 10;//錯誤

Double pi = 3.14;

Int& c = pi;//錯誤

當關鍵子const與引用結合之後,能夠採用以下的方式定義並初始化引用:

Const Int& a = 10;//使用整型文字常量初始化整型引用

Const Int& b = 3.14;//使用浮點型文字常量初始化整型引用

Double pi = 3.14;

Int& c = pi;//使用浮點型變量初始化整型引用

Double&ypi = pi;

 

可使用不一樣類型的對象初始化Const引用,只要能從一種類型轉換成另一種類型便可;也可使用不可尋址的文字常量。一樣的初始化方式對於非const引用是不合法的。

可以這樣作的緣由是:在編譯階段,編譯器首先生成一個臨時變量,而後將這些數據值,如上面代碼中的10,3.14賦給這個臨時變量,而後再將常量引用指向這個臨時變量。當咱們定義的引用是常量引用的時候,因爲不能修改被引用的對象的值,臨時變量的值和實際的數據值保持一致,不會有錯誤產生;當咱們定義的引用是很是量引用的時候,若是也採用臨時變量的方式處理,由於能夠更改引用所指向的變量的值,但這時候更改的是臨時變量的值,而實際的數據值沒有變化。因此,非const引用不會採起臨時變量的方式處理。

Const引用通常會被看成函數參數來使用,具體代碼舉例以下:

//使用const引用做爲參數的函數

Void dealData(const int& Para);

//能夠有以下的調用方式

dealData(10);

dealData(3.14);

double pi = 3.14;

dealData(pi);

int a = 100;

dealData(a);

 

//使用非const引用做爲函數的參數

Void dealData(int& Para);

//可用的調用方式

Int a = 10;

dealData(a);

由上面的代碼能夠看到,使用const修飾了引用類型的參數之後,能夠採用更靈活的方式來調用該函數。

 

7符號常量與函數的關係

函數的參數有三種形式,分別是:類型對象(內置類型或類類型),指針,引用;函數的返回值也有三種形式,分別是:類型對象(內置類型或類類型),指針,引用。

關鍵字const能夠修飾函數的參數,函數的返回值。若是該函數是類的成員函數,那麼關鍵字還能夠修飾整個函數,表示該函數不會修改類的數據成員。

7.1符號常量修飾函數參數

符號常量與函數參數結合使用的時候,通常有兩種狀況:一種狀況是修飾指針類型的函數參數,另一種狀況是修飾引用類型的參數。

默認狀況下,在傳遞參數的時候,函數採用值傳遞的方式。即:將實參的值複製一份到臨時變量中,而後將臨時變量值傳遞到函數中(壓棧)。在函數體中,當對函數參數操做的時候,如改變參數的值,實際上操做的是臨時變量的數據值,函數的實參不受影響。經過這種方式,起到了對函數實參保護的做用,即:在函數體中不能隨意更改函數的實參值。

這種值傳遞的方式,對於C++內部數據類型,如:整型,浮點型,字符型等,是沒有問題的。由於內部數據類型所佔用的內存較小,在複製的時候,不會引發大的性能問題。

當函數的參數是一個類的對象,而且在這個類類型中包含了大量的成員數據的時候,若是依舊採用值傳遞的方式處理,那麼就會引發大的性能問題。在這種狀況下,咱們須要將傳遞的參數更改爲指針或者引用。

若是函數的參數是指針,雖然依舊執行了值傳遞的規則(傳遞參數的時候,指針被複制了一份,但這兩個指針都指向同一個對象),可是指針只佔用4個字節,不會影響效率;若是函數的參數是引用,函數實參自身被傳遞到函數體中,在這個過程當中,不須要數據複製。不管是採用指針的方式仍是採用引用的方式,在函數體中均可以對指針指向的對象或引用所表明的對象的內容進行修改。

若是須要對函數的實參進行保護,即:在函數體中不能修改函數實參的內容,那麼就須要在指針或引用前面使用關鍵字const。具體的代碼舉例以下:

//函數的聲明

Class A;//聲明類

Void dealData(const A* pA);//採用指針的方式傳遞參數

Void dealData(const A& objA);//採用引用的方式傳遞參數

 

//函數調用

A* pA = new A;//定義類A的指針

A objA;//定義類A的對象。

 

dealData(pA);//此處依舊執行了值傳遞,指針pA被複制一份傳遞到函數中。但指針所指向的內容不可修改。

dealData(objA);//用objA初始化引用參數。實參直接傳遞到函數中,因爲使用關鍵字const修飾參數,引用所表明的實參不能被修改。

對於非內部數據類型的輸入參數,爲了考慮效率,通常採用指針或引用的方式傳遞參數值。若是在函數中不須要對實參的值進行更改,最好將關鍵字const與指針或引用參數結合使用。

對於內部數據類型的輸入參數,採用值傳遞的方式處理便可。因爲值傳遞規則的存在,關鍵字const與非指針或引用類型的參數結合起來是沒有意義的。

另外,關鍵字const與引用參數結合使用之後,當在調用該函數的時候,就能夠才用多種靈活的調用方式,如:直接傳遞文字常量,或其它的數據類型。具體狀況見第六節的描述。

7.2符號常量修飾函數返回值

默認狀況下,當從一個函數中返回一個數據值的時候,採用的是值傳遞的形式。即:在函數中生成一個臨時變量,將要返回的數據值複製到該臨時變量中,而後將該臨時變量返回。當函數的返回值是引用類型的時候,在返回數據值的時候,該函數不產生臨時變量,直接將要返回的數據值返回。

在對二目操做符重載,併產生新對象的狀況下,通常用關鍵字const修飾返回值,而且該返回值爲對象(非指針或引用)。具體代碼舉例以下:

Class myClass

{

Public:

VoidmyClass(int Para1,Para2);

Int m_Data1;

Int m_Data2;

};

 

ConstmyClass operator+ (constmyClass& Para1,const myClass& Para2)

{

Return myClass(Para1.m_Data1+Para2.m_Data1,Para1.m_Data2+Para2.m_Data2);

}

 

這樣作的目的是爲了防止以下狀況的發生:

Class myClass;

myClass A;

myClass B;

myClass C;

(A*B) = C;

除了這種狀況外,通常不多用const修飾返回對象。由於一旦用const修飾了返回的對象,那麼該對象就具備常量性,在該對象上只能調用常量函數。

8符號常量與類的關係

關鍵字const能夠修飾類的成員函數,使之稱爲常量成員函數。常量成員函數不能修改該類型的數據成員。應該把全部不修改數據成員的函數定義爲常量成員函數。若是在常量成員函數中修改了類的數據成員,那麼在編譯階段,編譯器將報錯。

關鍵字const能夠修飾類對象,類指針,類引用,使這些類對象,類指針,類引用具備常量性。不能修改具備常量性的類對象,類指針所指向的類對象,以及類引用所關聯的類對象的數據成員,而且只能使用這些類對象,類指針,類引用調用該類型的常量成員函數,不能調用很是量成員函數。普通的類對象,類指針,類引用能夠調用全部的該類型的成員函數,包括常量成員函數。它們的關係以下圖所示:

     

因爲不能經過具備常量性的類對象,類指針,類引用修改類的數據成員,因此它們只能調用不修改類數據成員的常量成員函數。而普通的類對象,指針,引用沒有這個限制,因此它們能夠調用全部的類成員函數。具體的代碼舉例以下:

Class myClass

{

Public:

Void Func(int Para);//普通函數成員

Void Func(int Para) const;//重載Func,常量函數成員。關鍵字const能夠實現函數重載

Int GetData() const;//常量函數成員.在函數後面加關鍵字const,表示該函數不修改類的數據成員

Void DealData(int Para);//普通函數成員

};

 

MyClass objClass;//定義類對象

Const myClass objConstClass;//定義類的常量對象

objClass.Func(100);//正確。普通對象調用普通的成員函數。

objClass.DealData(50);//正確

objClass.GetData();//正確,普通類對象能夠調用常量函數

objConstClass.Func(200);//正確,常量對象調用常量成員函數

objConstClass.GetData();//正確。

objConstClass.DealData(50);//錯誤,常量對象不能調用很是量函數

 

關鍵字const能夠實現函數的重載。在函數調用的時候,普通類對象調用普通的成員函數(Void Func(int Para);),常量對象調用常量成員函數(Void Func(int Para) const;)。

相關文章
相關標籤/搜索