原創 C++之常量(一)

1概述

一個C++程序就是一系列數據與操做的集合。當一個C++程序開始運行的時候,與該程序相關的數據就會被加載到內存中。當數據與內存發生關聯的時候,這些數據就會具備以下的特性:ios

  • 數據在內存中的地址。這個地址決定了數據在內存中的存儲位置。在32位的系統中,每個C++程序都具備4GB大小的內存地址空間,這個4GB大小的內存空間又被劃分爲若干個區域,如:棧區,堆區,全局(靜態)區,文字常量區,以及程序代碼區。不一樣內存地址的數據將會被存儲在不一樣的內存區域中;
  • 數據在內存中的值。若是該值可變,那麼該數據就是變量;若是該值不可變,那麼該數據就是常量;
  • 數據的類型。數據的類型決定了數據佔用內存的多少。如:Int型佔4個字節,Short型佔兩個字節;
  • 內存分配的時間點。有的數據在編譯時由編譯器分配內存,這些數據是:全局變量/常量,文字常量,靜態變量。在編譯階段,編譯器就爲這些數據分配了內存,當程序運行的時候,這些數據就會被直接加載到已分配的內存位置上;而有的數據只能在運行時由系統分配內存,如:函數內部的局部變量,以及使用new操做符分配的變量。這些數據的內存地址在程序運行時刻才能肯定。

在編碼階段,程序員能夠定義各類常量和變量;在編譯階段,編譯器會將程序員定義的變量和常量放到不一樣的數據段中;在程序運行階段,這些變量和常量又會被加載到不一樣的內存區域。如下,將從編碼階段,編譯階段,程序運行階段,三個階段來講明變量/常量在內存,數據段中的分佈狀況。程序員

在C++的編碼階段,程序員能夠定義各類做用域或生命週期的變量或常量,如:全局變量/常量,靜態變量/常量,局部變量/常量,以及使用new操做符分配的變量或常量。數組

在C++的編譯階段,編譯器將在編譯時刻可以肯定內存地址的變量或常量放到不一樣的數據段中。在編譯後的可執行文件中,通常會包含以下的數據段:函數

  • .text段,該數據段存放程序代碼;
  • .bss段,該數據段存放未初始化的全局變量或靜態變量;
  • .data段,該數據段存放已初始化的全局變量或靜態變量;
  • .rdata段,該數據段存放文字常量,以及在全局做用域中定義的字符常量。

在C++程序的執行階段,程序代碼中定義的變量和常量會被加載到不一樣的內存區域。如:全局變量,靜態變量加載到全局區,文字常量加載到文字常量區。編碼

具體狀況以下表所示:spa

建立時刻線程

程序代碼指針

可執行文件對象

內存blog

備註

編譯時刻

已初始化的全局變量,靜態變量

.data段

全局區(初始化)

可讀,可寫

未初始化的全局變量,靜態變量

.bss段

全局區(未初始化)

可讀,可寫

文字常量

.rdata段

文字常量區

只讀

全局符號常量

.rdata段

文字常量區

只讀

運行時刻

局部自動變量,符號常量

 

棧區

屬於線程

New操做符分配的變量,常量

 

堆區

屬於進程

 

2常量的分類

2.1文字常量

當一個數值,字符或者字符串,例如 1,’A’,」Test Const」等,出如今程序代碼中的時候,它們被稱爲文字常量。稱之爲「文字」是由於咱們只能以值的形式表述它們;稱之爲「常量」是由於它們的值不能被改變。文字常量出如今C++代碼中,通常爲其餘變量賦初始值。具體的示例代碼以下:

Int a = 10;//10爲整型文字常量。在編譯時,數值類型文字常量被看成當即數處理。從彙編指令中能夠看到這個狀況

     int a = 10;

001F153D  mov         dword ptr [ebp-14h],A  //10被噹噹即數處理。直接出如今指令中

Double pi = 3.1415;//3.1415爲浮點型文字常量。

Char b = ‘a’;//a爲字符型文字常量。在初始化的時候,從文字常量區將a拷貝到變量b所在的內存地址處。

Char* strName = 「wolf」;//wolf爲字符串型文字常量。指針strName指向文字常量區中字符串wolf的首地址。

Char[] arrName = 「wolf」;//wolf爲字符串型文字常量。數組初始化的時候,從文字常量區將wolf拷貝到數組所在的地址中。若是arrName在局部定義,那麼就拷貝到棧中,若是在全局做用域定義,那麼就拷貝到內存的全局區。

//wolf在文字常量區只有一份拷貝。

在C++代碼中,直接以值的形式出現的各類類型的數據都屬於文字常量。在編譯階段,編譯器爲字符型文字常量分配內存地址,將數值型文字常量處理成當即數;在編譯後的可執行文件(PE文件)中,文字常量被放到.rdata數據段中;在程序運行階段,文字常量被加載到內存的文字常量區。在具體使用的時候,將會從文字常量區將文字常量的值拷貝到使用文字常量的變量的內存地址處,如:Char[] arrName = 「wolf」;或者變量的指針直接指向文字常量區中文字常量的位置,如:char * strName = 「wolf」。若是在代碼中存在多個相同的文字常量,那麼在存儲的時候,在文字常量區只保持該文字常量的一份拷貝,使用該文字常量的多個變量將今後文字常量處取值。

按照數據類型的不一樣,文字常量能夠劃分爲數值常量,字符常量,以及字符串常量。其中,數值常量又包括整型常量和浮點型常量,字符常量又包括普通字符常量和轉義字符常量。具體分類結構以下圖所示:

                       

字符常量用單引號表示,字符串常量用雙引號表示。如:’a’表示字符常量,而」a」則表示字符串常量。

2.2符號常量

在C++代碼中,經常用一個符號名稱來表明一個常量,這個符號名稱被稱爲符號常量。一般有兩種方式定義符號常量,一種方式是使用關鍵字const,另一種方式是使用宏定義。

使用關鍵字const定義符號常量的格式以下:

Const常量的數據類型 符號常量名 = 文字常量;

舉例:

Const double PI = 3.14;//定義double類型的符號常量。PI是符號常量,3.14是文字常量

Const char* strName = 「wolf」;

使用宏定義方式定義符號常量的格式以下:

#define 符號常量名文字常量。

#define PI 3.14   //定義符號常量PI,從這個語句中區分不出數據類型。

#define strName「wolf」

在實際的編碼過程當中,建議儘量多地使用關鍵字const定義符號常量,而不是使用宏定義的方式。

使用關鍵字const定義符號常量之後,必需要進行初始化工做,而且其值在隨後的代碼中不能被改變。C++有兩種方式來保證符號常量的常量性。一種方法是使用內存區域的只讀屬性來保證,這種方法適用於全局常量,若是該符號常量的值發生變化,系統會報錯;另一種方法是在編譯階段,由編譯器利用常量規則來保證,這種方法適用於局部常量。在編譯器編譯的時候,若是在代碼中發現更改符號常量值的狀況,編譯器就會報錯。可是當代碼編譯經過之後,在運行階段,能夠採用一些特殊的方法改變該符號常量的值,而且系統不會報錯。

若是對符號常量執行取地址操做,或者對符號常量使用關鍵字extern,那麼在編譯的時候,編譯器將爲符號常量分配內存;不然,在編譯的時候,符號常量的名稱只被放到符號列表中,不執行內存分配,在使用符號常量的地方執行常量摺疊。

能夠在各類做用域中定義符號常量。

3符號常量與做用域的關係

3.1常量摺疊

若是符號常量的定義在當前文件可見,那麼在編譯的時候,編譯器將執行常量摺疊。在使用符號常量的代碼處,編譯器會用符號常量的實際值去替換符號常量。若是符號常量的定義在當前文件不可見,將沒法進行常量摺疊。具體代碼舉例以下:

----------------------------------------------A.h-------------------------------------------------------

Extern const int fa;//聲明符號常量。該符號常量具備全局做用域

--------------------------------A.cpp---------------------------------------

#include"A.h"

constint fa = 3;//定義符號常量。

--------------------------------------------main.cpp-------------------------------------------------

#include<iostream>

#include"A.h"//在main.cpp文件中,只知道符號常量fa的聲明,不知道它的定義

 

usingnamespace std;

 

int main()

{

constint a = 10;//定義局部常量。在棧中爲該符號常量分配了內存。

 

int b = a + 2;//符號常量定義可見,在編譯時刻執行了常量摺疊。

 

int c = fa + 2;//符號常量不可見,沒有執行常量摺疊。

 

constint * pa = &a;//執行取常量地址操做。該符號常量的常量性由編譯器在編譯階段保證。

  *(const_cast<int*>(pa)) = 100;//取消指針的常量性,改變常量的值。能夠這麼幹,徹底沒有問題

 

//也能夠採用下面的方式更改常量的值。

//int* c = (int*)pa;

//*c = 100;

 

   cout << *pa <<endl;

   cout << a <<endl;//符號常量定義可見,在編譯時刻執行了常量摺疊。

   cout << b <<endl;

 

 

int k;

   cin >> k;

}

 

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

  const int a = 10;

002C13EE  mov         dword ptr [a],0Ah     //在棧中爲a分配了內存,並將10存儲在該內存處

 

  int b = a + 2;//符號常量定義可見,在編譯時刻執行了常量摺疊,已經計算出10+2的值。所以下面彙編代碼中,只給出了當即數0Ch,也就是十進制的數值12。

002C13F5  mov         dword ptr [b],0Ch

 

  int c = fa + 2;//沒有進行常量摺疊,只能在運行時計算結果。

002C13FC  mov         eax,dword ptr [a (2C5800h)]

002C1401  add         eax,2

002C1404  mov         dword ptr [c],eax

 

 

//在現實中,通常不多出現定義了符號常量,而後又改變其值的蛋疼操做。這裏只是說明問題而已。將常量值的改變與常量摺疊進行對比,更能深刻理解常量摺疊這個操做。

 

上面的代碼執行完畢之後,輸出的結果是:100,10,12。在上面的代碼中,*pa輸出了100,那是由於在程序運行階段,咱們改變了符號常量的值;a輸出10,是由於在編譯階段,編譯器執行了常量摺疊,用10替換了a;代碼cout<< a <<endl; 變成了cout<< 10 <<endl;b輸出12也是這個道理。

在局部做用域中定義的符號常量,在運行時刻能夠改變它的值。由於它的內存被分配到棧上,它的常量性是在編譯階段由編譯器經過規則保證的。在運行階段,編譯器就沒有辦法了。

對於在全局做用域中定義的符號常量,如上面代碼中的fa,在運行時刻沒法改變其值。由於在全局做用域中定義的符號常量的內存地址被分配到了文字常量區,該區具備只讀屬性。全局做用域中的符號常量的常量性是經過內存的只讀屬性來控制的。

 

3.2符號常量與文件做用域的關係

頭文件是用來聲明而不是定義函數和變量的,而源文件則是用來實現變量和函數定義的。頭文件中通常包含類的定義,枚舉的定義,符號常量的定義,內聯函數的定義,extern變量的聲明,函數的聲明,typedef聲明。若是將變量定義到頭文件中,那麼當其餘文件引入該頭文件的時候,就會認爲該對頭文件中的變量又執行了一次定義。根據一次定義法則,這是不容許的。可是也有三個例外,類,內聯函數,在編譯時值能夠肯定的符號常量是能夠在頭文件中定義的。

符號常量默認具備文件做用域,因此符號常量能夠定義到頭文件中,而後被其餘源文件引用。在多個源文件中,能夠存在同名的符號常量,它們是獨立的個體,互不影響。具體代碼舉例以下:

------------------------------A.h------------------------------------

Const double PI = 3.14;//定義符號常量,雖然該符號常量被定義在全局做用域中,但它默認只具備文件做用域。在其餘文件中是沒法訪問該符號常量的。

----------------------------B.cpp------------------------------------

#include 「A.h」//引入頭文件,在編譯階段,頭文件A.h和源文件B.cpp被合併爲一個文件,在頭文件A.h中定義的符號常量PI,至關於在新合併後的源文件中被定義。

Double GetMJ(double r)

{

Return r * r * PI;//使用常量,由於符號常量的定義在當前文件可見,因此編譯器用3.14替換符號常量PI。具體的代碼就變成了「return r*r*3.14;」。這個替換的過程叫常量摺疊

}

----------------------------C.cpp------------------------------------------

#include 「A.h」//引入頭文件,至關於在C.cpp源文件中又定義了一次符號常量PI。可是符號常量默認具備文件做用域,B.cpp源文件中的符號常量PI與C.cpp源文件中的PI互不影響。

Double GetZC(double r)

{

Return 2 * PI * r;//編譯時執行常量摺疊。

}

在文件做用域中定義的符號常量,通常其值明確,因此編譯器在編譯階段執行了常量摺疊。而且,沒有爲該符號常量分配內存,只是將該符號常量的名稱保存到了符號列表中而已。符號列表是編譯技術中的一個概念。

3.3符號常量與全局做用域的關係

符號常量默認具備文件做用域。當在編譯時刻符號常量的值已知的狀況下,編譯器只是將符號常量的名稱放到符號列表中,並不對該符號常量分配內存。在使用該符號常量的代碼處,編譯器執行了常量摺疊。

若是對符號常量使用了關鍵字extern,那麼該符號常量就具有了全局做用域。編譯器在編譯階段將爲該符號常量分配內存。

若是符號常量的值在編譯階段未知,那麼也須要對該符號常量使用關鍵字extern。在這兩種狀況下,編譯器沒法進行常量摺疊。

具體代碼舉例以下:

-----------------------------------A.h----------------------------------

externconstint a;//在全局做用域中聲明符號常量,使用了關鍵字extern,因此該符號常量具備全局做用域

-------------------------A.cpp------------------------

#include"A.h"

constint a = 10;//在此處定義符號常量,並初始化其值爲10.編譯器爲該符號常量分配內存。

------------------------main.cpp------------------------

 

#include<iostream>

usingnamespace std;

 

#include"A.h"//引用了頭文件,至關於聲明瞭一次符號常量,符號常量的真正定義在A.cpp中。Main.cpp不知道該符號常量的定義。

int main()

{

int b = 5 * a;//這裏沒有進行常量摺疊。在運行時,從符號常量a的內存地址處取得其值。具體狀況見彙編代碼。

Int c = 5*10;

 

   cout << a <<endl;

   cout << b <<endl;

 

int k;

   cin >> k;

}

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

   int b = 5 * a;

0104360E  mov         eax,dword ptr [a (1045800h)]      //將a值從內存地址1045800h出取出,放到eax中。

01043613  imul        eax,eax,5                        //執行乘法操做。若是執行常量摺疊的話,這裏根本不會執行乘法操做。

01043616  mov         dword ptr [b],eax               //將乘法結果放到b中。

 

 

   int c = 5*10;

00AF3619  mov         dword ptr [c],32h               // 編譯階段已經計算完畢,這裏直接給出當即數

 

從上面的示例能夠看出,關鍵字extern使符號常量具有了全局做用域,而且爲該符號常量分配了內存地址,同時,在編譯的時候,編譯器不爲該符號常量進行常量摺疊。由於該符號常量的定義在當前文件不可見。

因此,在使用符號常量的時候,若是該符號常量的值在編譯時刻已知,那麼就將該符號常量處理成具備文件做用域便可。這樣,在編譯階段,編譯器可以爲該符號常量進行了常量摺疊,從而提升了執行階段的效率。

3.4符號常量與類域的關係

能夠在類中定義符號常量,該符號常量做爲該類的數據成員出現。對於每一個類對象來講,符號常量的值是固定的;可是,對於整個類的類型來講,符號常量的值是可變的。

在具體操做的時候,能夠將符號常量定義在實現該類定義的頭文件中,可是,符號常量的初始化操做必須在該類構造函數的初始化列表中實現。

具體代碼以下:

---------------------------------------A.h---------------------------------------

#ifndef _MyClass_H

#define _MyClass_H

 

class MyClass

{

public:

     MyClass(int Para);

 

private:

     constint m_conVal;//定義常量

 

};

 

#endif

 

--------------------------------A.cpp------------------------------

#include"B.h"

 

MyClass::MyClass(int Para):m_conVal(Para)//初始化常量

{

 

}

由於常量是在類的構造函數中被初始化,因此對於每個類對象來講,常量的值是固定的;對於整個類的類型來講,在每次初始化類對象的時候,向構造函數中傳遞的參數可能不一樣,所以,常量的值也會不一樣。

若是想要創建在整個類中都恆定的常量,應該用類中的枚舉常量來實現。具體示例代碼以下:

Class myClass

{

Enum { size1 = 100,size2 = 200,size3 = 300};

Int arr1[size1];

Int arr2[size2];

 

};

枚舉常量不會佔用對象的存儲空間,他們在編譯時被所有求值。可是枚舉常量的隱含數據類型是整數,其最大值有限,且不能表示浮點數。

3.5符號常量與局部做用域的關係

能夠在局部做用域中定義符號常量,在運行時刻,該符號常量在棧中被分配內存。該符號常量的常量性由編譯器在編譯時刻保證,若是在代碼中發現有改變該符號常量的值的語句,那麼編譯器就會報錯。

若是符號常量的定義在當前文件是可見的,那麼在編譯階段,編譯器將會執行常量摺疊;若是符號常量的定義在當前文件不可見,那麼將會在運行時刻,從符號常量的存儲位置取得該值。

程序員能夠改變運行時刻局部符號常量的值。具體代碼舉例以下:

---------------------------------main.cpp------------------------------------------

#include<iostream>

usingnamespace std;

 

int main()

{

constint a = 10;

 

int b = 10;

 

int c = a * 5;

int d = b * 5;

 

int k;

   cin >> k;

}

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

00D713EC  rep stos    dword ptr es:[edi]

  const int a = 10;  //爲a分配了內存

00D713EE  mov         dword ptr [a],0Ah

 

  int b = 10;       //爲b分配了內存

00D713F5  mov         dword ptr [b],0Ah

 

  int c = a * 5;    //執行了常量摺疊,彙編代碼中只給出了當即數32h,也就是十進制的50

00D713FC  mov         dword ptr [c],32h

  int d = b * 5;  //對於變量,執行了計算操做。

00D71403  mov         eax,dword ptr [b]

00D71406  imul        eax,eax,5

00D71409  mov         dword ptr [d],eax

相關文章
相關標籤/搜索