C++從零開始(三)——何謂變量

    本篇說明內容是C++中的關鍵,基本大部分人對於這些內容都是昏的,但這些內容又是編程的基礎中的基礎,必須詳細說明。

    數字表示

    數學中,數只有數值大小的不同,絕不會有數值佔用空間的區別,即數學中的數是邏輯上的一個概念,但電腦不是。考慮算盤,每個算盤上有很多列算子,每列都分成上下兩排算子。上排算子有2個,每個代表5,下排算子有4個,每個代表1(這並不重要)。因此算盤上的每列共有6個算子,每列共可以表示0到14這15個數字(因爲上排算子的可能狀態有0到2個算子有效,而下排算子則可能有0到4個算子有效,故爲3×5=15種組合方式)。

    上面的重點就是算盤的每列並沒有表示0到14這15個數字,而是每列有15種狀態,因此被人利用來表示數字而已(這很重要)。由於算盤的每列有15個狀態,因此用兩列算子就可以有15×15=225個狀態,因此可以表示0到224.阿拉伯數字的每一位有0到9這10個圖形符號,用兩個阿拉伯數字圖形符號時就能有10×10=100個狀態,因此可以表示0到99這100個數。

    這裏的算盤其實就是一個基於15進制的記數器(可以通過維持一列算子的狀態來記錄一位數字),它的一列算子就相當於一位阿拉伯數字,每列有15種狀態,故能表示從0到14這15個數字,超出14後就必須通過進位來要求另一列算子的加入以表示數字。電腦與此一樣,其並不是數字計算機,而是電子計算機,電腦中通過一根線的電位高低來表示數字。一根線中的電位規定只有兩種狀態——高電位和低電位,因此電腦的數字表示形式是二進制的。

    和上面的算盤一樣,一根電線只有兩個狀態,當要表示超出1的數字時,就必須進位來要求另一根線的加入以表示數字。所謂的32位電腦就是提供了32根線(被稱作數據總線)來表示數據,因此就有2的32次方那麼多種狀態。而16根線就能表示2的16次方那麼多種狀態。

    所以,電腦並不是基於二進制數,而是基於狀態的變化,只不過這個狀態可以使用二進制數表示出來而已。即電腦並不認識二進制數,這是下面“類型”一節的基礎。

    內存

    內存就是電腦中能記錄數字的硬件,但其存儲速度很快(與硬盤等低速存儲設備比較),又不能較長時間保存數據,所以經常被用做草稿紙,記錄一些臨時信息。

    前面已經說過,32位計算機的數字是通過32根線上的電位狀態的組合來表示的,因此內存能記錄數字,也就是能維持32根線上各自的電位狀態(就好象算盤的算子撥動後就不會改變位置,除非再次撥動它)。不過依舊考慮上面的算盤,假如一個算盤上有15列算子,則一個算盤能表示15的15次方個狀態,是很大的數字,但經常實際是不會用到變化那麼大的數字的,因此讓一個算盤只有兩列算子,則只能表示225個狀態,當數字超出時就使用另一個或多個算盤來一起表示。

    上面不管是2列算子還是15列算子,都是算盤的粒度,粒度分得過大造成不必要的浪費(很多列算子都不使用),太小又很麻煩(需要多個算盤)。電腦與此一樣。2的32次方可表示的數字很大,一般都不會用到,如果直接以32位存儲在內存中勢必造成相當大的資源浪費。於是如上,規定內存的粒度爲8位二進制數,稱爲一個內存單元,而其大小稱爲一個字節(Byte)。就是說,內存存儲數字,至少都會記錄8根線上的電位狀態,也就是2的8次方共256種狀態。所以如果一個32位的二進制數要存儲在內存中,就需要佔據4個內存單元,也就是4個字節的內存空間。

    我們在紙上寫字,是通過肉眼判斷出字在紙上的相對橫座標和縱座標以查找到要看的字或要寫字的位置。同樣,由於內存就相當於草稿紙,因此也需要某種定位方式來定位,在電腦中,就是通過一個數字來定位的。這就和旅館的房間號一樣,內存單元就相當於房間(假定每個房間只能住一個人),而前面說的那個數字就相當於房間號。爲了向某塊內存中寫入數據(就是使用某塊內存來記錄數據總線上的電位狀態),就必須知道這塊內存對應的數字,而這個數字就被稱爲地址。而通過給定的地址找到對應的內存單元就稱爲尋址。

    因此地址就是一個數字,用以唯一標識某一特定內存單元。此數字一般是32位長的二進制數,也就可以表示4G個狀態,也就是說一般的32位電腦都具有4G的內存空間尋址能力,即電腦最多裝4G的內存,如果電腦有超過4G的內存,此時就需要增加地址的長度,如用40位長的二進制數來表示。

    類型

    在本系列最開頭時已經說明了何謂編程,而剛纔更進一步說明了電腦其實連數字都不認識,只是狀態的記錄,而所謂的加法也只是人爲設計那個加法器以使得兩個狀態經過加法器的處理而生成的狀態正好和數學上的加法的結果一樣而已。這一切的一切都只說明一點:電腦所做的工作是什麼,全視使用的人以爲是什麼。

    因此爲了利用電腦那很快的“計算”能力(實際是狀態的變換能力),人爲規定了如何解釋那些狀態。爲了方便其間,對於前面提出的電位的狀態,我們使用1位二進制數來表示,則上面提出的狀態就可以使用一個二進制數來表示,而所謂的“如何解釋那些狀態”就變成了如何解釋一個二進制數。

    C++是高級語言,爲了幫助解釋那些二進制數,提供了類型這個概念。類型就是人爲制訂的如何解釋內存中的二進制數的協議。C++提供了下面的一些標準類型定義。

 signed char  表示所指向的內存中的數字使用補碼形式,表示的數字爲-128到+127,長度爲1個字節
 unsigned char  表示所指向的內存中的數字使用原碼形式,表示的數字爲0到255,長度爲1個字節
 signed shor  表示所指向的內存中的數字使用補碼形式,表示的數字爲–32768到+32767,長度爲2個字節
 unsigned short  表示所指向的內存中的數字使用原碼形式,表示的數字爲0到65535,長度爲2個字節
 signed long  表示所指向的內存中的數字使用補碼形式,表示的數字爲-2147483648到+2147483647,長度爲4個字節
 unsigned long  表示所指向的內存中的數字使用原碼形式,表示的數字爲0到4294967295,長度爲4個字節
 signed int
 表示所指向的內存中的數字使用補碼形式,表示的數字則視編譯器。如果編譯器編譯時被指明編譯爲在16位操作系統上運行,則等同於signedshort;如果是編譯爲32位的,則等同於signedlong;如果是編譯爲在64位操作系統上運行,則爲8個字節長,而範圍則如上一樣可以自行推算出來。
 unsigned int  表示所指向的內存中的數字使用原碼形式,其餘和signedint一樣,表示的是無符號數。
 bool  表示所指向的內存中的數字爲邏輯值,取值爲false或true。長度爲1個字節。
 float  表示所指向的內存按IEEE標準進行解釋,爲real*4,佔用4字節內存空間,等同於上篇中提到的單精度浮點數。
 double  表示所指向的內存按IEEE標準進行解釋,爲real*8,可表示數的精度較float高,佔用8字節內存空間,等同於上篇提到的雙精度浮點數。
 long double  表示所指向的內存按IEEE標準進行解釋,爲real*10,可表示數的精度較double高,但在爲32位Windows操作系統編寫程序時,仍佔用8字節內存空間。

    標準類型不止上面的幾個,後面還會陸續提到。

    上面的長度爲2個字節也就是將兩個連續的內存單元中的數字取出並合併在一起以表示一個數字,這和前面說的一個算盤表示不了的數字,就進位以加入另一個算盤幫助表示是同樣的道理。

    上面的signed關鍵字是可以去掉的,即char等同於signed char,用以簡化代碼的編寫。但也僅限於signed,如果是unsigned char,則在使用時依舊必須是unsigned char.

    現在應該已經瞭解上篇中爲什麼數字還要分什麼有符號無符號、長整型短整型之類的了,而上面的short、char等也都只是長度不同,這就由程序員自己根據可能出現的數字變化幅度來進行選用了。

    類型只是對內存中的數字的解釋,但上面的類型看起來相對簡單了點,且語義並不是很強,即沒有什麼特殊意思。爲此,C++提供了自定義類型,也就是後繼文章中將要說明的結構、類等。

    變量

    在本系列的第一篇中已經說過,電腦編程的絕大部分工作就是操作內存,而上面說了,爲了操作內存,需要使用地址來標識要操作的內存塊的首地址(上面的long表示連續的4個字節內存,其第一個內存單元的地址稱作這連續4個字節內存塊的首地址)。爲此我們在編寫程序時必須記下地址。

    做5+2/3-5*2的計算,先計算出2/3的值,寫在草稿紙上,接着算出5*2的值,又寫在草稿紙上。爲了接下來的加法和減法運算,必須能夠知道草稿紙上的兩個數字哪個是2/3的值哪個是5*2的值。人就是通過記憶那兩個數在紙上的位置來記憶的,而電腦就是通過地址來標識的。但電腦只會做加減乘除,不會去主動記那些2/3、5*2的中間值的位置,也就是地址。因此程序員必須完成這個工作,將那兩個地址記下來。

    問題就是這裏只有兩個值,也許好記一些,但如果多了,人是很難記住哪個地址對應哪個值的,但人對符號比對數字要敏感得多,即人很容易記下一個名字而不是一個數字。爲此,程序員就自己寫了一個表,表有兩列,一列是“2/3的值”,一列是對應的地址。如果式子稍微複雜點,那麼那個表可能就有個二三十行,而每寫一行代碼就要去翻查相應的地址,如果來個幾萬行代碼那是人都不能忍受。

    C++作爲高級語言,很正常地提供了上面問題的解決之道,就是由編譯器來幫程序員維護那個表,要查的時候是編譯器去查,這也就是變量的功能。

    變量是一個映射元素。上面提到的表由編譯器維護,而表中的每一行都是這個表的一個元素(也稱記錄)。表有三列:變量名、對應地址和相應類型。變量名是一個標識符,因此其命名規則完全按照上一篇所說的來。當要對某塊內存寫入數據時,程序員使用相應的變量名進行內存的標識,而表中的對應地址就記錄了這個地址,進而將程序員給出的變量名,一個標識符,映射成一個地址,因此變量是一個映射元素。而相應類型則告訴編譯器應該如何解釋此地址所指向的內存,是2個連續字節還是4個?是原碼記錄還是補碼?而變量所對應的地址所標識的內存的內容叫做此變量的值。

    有如下的變量解釋:“可變的量,其相當於一個盒子,數字就裝在盒子裏,而變量名就寫在盒子外面,這樣電腦就知道我們要處理哪一個盒子,且不同的盒子裝不同的東西,裝字符串的盒子就不能裝數字。”上面就是我第一次學習編程時,書上寫的(是BASIC語言)。對於初學者也許很容易理解,也不能說錯,但是造成的誤解將導致以後的程序編寫地千瘡百孔。

    上面的解釋隱含了一個意思——變量是一塊內存。這是嚴重錯誤的!如果變量是一塊內存,那麼C++中著名的引用類型將被棄置荒野。變量實際並不是一塊內存,只是一個映射元素,這是致關重要的。

    內存的種類

    前面已經說了內存是什麼及其用處,但內存是不能隨便使用的,因爲操作系統自己也要使用內存,而且現在的操作系統正常情況下都是多任務操作系統,即可同時執行多個程序,即使只有一個CPU.因此如果不對內存訪問加以節制,可能會破壞另一個程序的運作。比如我在紙上寫了2/3的值,而你未經我同意且未通知我就將那個值擦掉,並寫上5*2的值,結果我後面的所有計算也就出錯了。

    因此爲了使用一塊內存,需要向操作系統申請,由操作系統統一管理所有程序使用的內存。所以爲了記錄一個long類型的數字,先向操作系統申請一塊連續的4字節長的內存空間,然後操作系統就會在內存中查看,看是否還有連續的4個字節長的內存,如果找到,則返回此4字節內存的首地址,然後編譯器編譯的指令將其記錄在前面提到的變量表中,最後就可以用它記錄一些臨時計算結果了。

    上面的過程稱爲要求操作系統分配一塊內存。這看起來很不錯,但是如果只爲了4個字節就要求操作系統搜索一下內存狀況,那麼如果需要100個臨時數據,就要求操作系統分配內存100次,很明顯地效率低下(無謂的99次查看內存狀況)。因此C++發現了這個問題,並且操作系統也提出了相應的解決方法,最後提出瞭如下的解決之道。

    棧(Stack)

    任何程序執行前,預先分配一固定長度的內存空間,這塊內存空間被稱作棧(這種說法並不準確,但由於實際涉及到線程,在此爲了不將問題複雜化才這樣說明),也被叫做堆棧。那麼在要求一個4字節內存時,實際是在這個已分配好的內存空間中獲取內存,即內存的維護工作由程序員自己來做,即程序員自己判斷可以使用哪些內存,而不是操作系統,直到已分配的內存用完。

    很明顯,上面的工作是由編譯器來做的,不用程序員操心,因此就程序員的角度來看什麼事情都沒發生,還是需要像原來那樣向操作系統申請內存,然後再使用。

    但工作只是從操作系統變到程序自己而已,要維護內存,依然要耗費CPU的時間,不過要簡單多了,因爲不用標記一塊內存是否有人使用,而專門記錄一個地址。此地址以上的內存空間就是有人正在使用的,而此地址以下的內存空間就是無人使用的。之所以是以下的空間爲無人使用而不是以上,是當此地址減小到0時就可以知道堆棧溢出了(如果你已經有些基礎,請不要把0認爲是虛擬內存地址,關於虛擬內存將會在《C++從零開始(十八)》中進行說明,這裏如此解釋只是爲了方便理解)。而且CPU還專門對此法提供了支持,給出了兩條指令,轉成彙編語言就是push和pop,表示壓棧和出棧,分別減小和增大那個地址。

    而最重要的好處就是由於程序一開始執行時就已經分配了一大塊連續內存,用一個變量記錄這塊連續內存的首地址,然後程序中所有用到的,程序員以爲是向操作系統分配的內存都可以通過那個首地址加上相應偏移來得到正確位置,而這很明顯地由編譯器做了。因此實際上等同於在編譯時期(即編譯器編譯程序的時候)就已經分配了內存(注意,實際編譯時期是不能分配內存的,因爲分配內存是指程序運行時向操作系統申請內存,而這裏由於使用堆棧,則編譯器將生成一些指令,以使得程序一開始就向操作系統申請內存,如果失敗則立刻退出,而如果不退出就表示那些內存已經分配到了,進而代碼中使用首地址加偏移來使用內存也就是有效的),但壞處也就是只能在編譯時期分配內存。

    堆(Heap)

    上面的工作是編譯器做的,即程序員並不參與堆棧的維護。但上面已經說了,堆棧相當於在編譯時期分配內存,因此一旦計算好某塊內存的偏移,則這塊內存就只能那麼大,不能變化了(如果變化會導致其他內存塊的偏移錯誤)。比如要求客戶輸入定單數據,可能有10份定單,也可能有100份定單,如果一開始就定好了內存大小,則可能造成不必要的浪費,又或者內存不夠。

    爲了解決上面的問題,C++提供了另一個途徑,即允許程序員有兩種向操作系統申請內存的方式。前一種就是在棧上分配,申請的內存大小固定不變。後一種是在堆上分配,申請的內存大小可以在運行的時候變化,不是固定不變的。

 

    那麼什麼叫堆?在Windows操作系統下,由操作系統分配的內存就叫做堆,而棧可以認爲是在程序開始時就分配的堆(這並不準確,但爲了不複雜化問題,故如此說明)。因此在堆上就可以分配大小變化的內存塊,因爲是運行時期即時分配的內存,而不是編譯時期已計算好大小的內存塊。 

    變量的定義

    上面說了那麼多,你可能看得很暈,畢竟連一個實例都沒有,全是文字,下面就來幫助加深對上面的理解。

    定義一個變量,就是向上面說的由編譯器維護的變量表中添加元素,其語法如下:

    long a;

    先寫變量的類型,然後一個或多個空格或製表符(/t)或其它間隔符,接着變量的名字,最後用分號結束。要同時定義多個變量,則各變量間使用逗號隔開,如下:

    long a, b, c; unsigned short e, a_34c;

    上面是兩條變量定義語句,各語句間用分號隔開,而各同類型變量間用逗號隔開。而前面的式子5+2/3-5*2,則如下書寫。

    long a = 2/3, b = 5*2; long c = 5 + a – b;

    可以不用再去記那煩人的地址了,只需記着a、b這種簡單的標識符。當然,上面的式子不一定非要那麼寫,也可以寫成:long c = 5 + 2 / 3 – 5 * 2; 而那些a、b等中間變量編譯器會自動生成並使用(實際中編譯器由於優化的原因將直接計算出結果,而不會生成實際的計算代碼)。

    下面就是問題的關鍵,定義變量就是添加一個映射。前面已經說了,這個映射是將變量名和一個地址關聯,因此在定義一個變量時,編譯器爲了能將變量名和某個地址對應起來,幫程序員在前面提到的棧上分配了一塊內存,大小就視這個變量類型的大小。如上面的a、b、c的大小都是4個字節,而e、a_34c的大小都是2個字節。

    假設編譯器分配的棧在一開始時的地址是1000,並假設變量a所對應的地址是1000-56,則b所對應的地址就是1000-60,而c所對應的就是1000-64,e對應的是1000-66,a_34c是1000-68.如果這時b突然不想是4字節了,而希望是8字節,則後續的c、e、a_34c都將由於還是原來的偏移位置而使用了錯誤的內存,這也就是爲什麼棧上分配的內存必須是固定大小。

    考慮前面說的紅色文字:“變量實際並不是一塊內存,只是一個映射元素”。可是隻要定義一個變量,就會相應地得到一塊內存,爲什麼不說變量就是一塊內存?上面定義變量時之所以會分配一塊內存是因爲變量是一個映射元素,需要一個對應地址,因此纔在棧上分配了一塊內存,並將其地址記錄到變量表中。但是變量是可以有別名的,即另一個名字。這個說法是不準確的,應該是變量所對應的內存塊有另一個名字,而不止是這個變量的名字。

    爲什麼要有別名?這是語義的需要,表示既是什麼又是什麼。比如一塊內存,裏面記錄了老闆的信息,因此起名爲Boss,但是老闆又是另一家公司的行政經理,故變量名應該爲Manager,而在程序中有段代碼是老闆的公司相關的,而另一段是老闆所在公司相關的,在這兩段程序中都要使用到老闆的信息,那到底是使用Boss還是Manager?其實使用什麼都不會對最終生成的機器代碼產生什麼影響,但此處出於語義的需要就應該使用別名,以期從代碼上表現出所編寫程序的意思。

    在C++中,爲了支持變量別名,提供了引用變量這個概念。要定義一個引用變量,在定義變量時,在變量名的前面加一個“&”,如下書寫:

    long a; long &a1 = a, &a2 = a, &a3 = a2;

    上面的a1、a2、a3都是a所對應的內存塊的別名。這裏在定義變量a時就在棧上分配了一塊4字節內存,而在定義a1時卻沒有分配任何內存,直接將變量a所映射的地址作爲變量a1的映射地址,進而形成對定義a時所分配的內存的別名。因此上面的Boss和Manager,應該如下(其中Person是一個結構或類或其他什麼自定義類型,這將在後繼的文章中陸續說明):

    Person Boss; Person &Manager = Boss;

    由於變量一旦定義就不能改變(指前面說的變量表裏的內容,不是變量的值),直到其被刪除,所以上面在定義引用變量的時候必須給出欲別名的變量以初始化前面的變量表,否則編譯器編譯時將報錯。

    現在應該就更能理解前面關於變量的紅字的意思了。並不是每個變量定義時都會分配內存空間的。而關於如何在堆上分配內存,將在介紹完指針後予以說明,並進而說明上一篇遺留下來的關於字符串的問題。

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