C++從零開始(六)
——何謂語句
前面已經說過程式就是方法的描述,而方法的描述無外乎就是動作加動作的賓語,而這裡的動作在C++中就是通過語句來表現的,而動作的賓語,也就是能夠被操作的資源,但非常可惜地C++語言本身只支援一種資源——記憶體。由於電腦實際可以操作不止記憶體這一種資源,導致C++語言實際並不能作為底層硬體程式的編寫語言(即使是C語言也不能),不過各編譯器廠商都提供了自己的嵌入式彙編語句功能(也可能沒提供或提供其它的附加文法以使得可以操作硬體),對於VC,通過使用__asm語句即可實現在C++代碼中加入彙編代碼來操作其他類型的硬體資源。對於此語句,本系列不做說明。
語句就是動作,C++中共有兩種語句:單句和複合陳述式。複合陳述式是用一對大括弧括起來,以在需要的地方同時放入多條單句,如:{ long a = 10; a += 34; }。而單句都是以“;”結尾的,但也可能由於在末尾要插入單句的地方用複合陳述式代替了而用“}”結尾,如:if( a ) { a--; a++; }。應注意大括弧後就不用再寫“;”了,因為其不是單句。
方法就是怎麼做,而怎麼做就是在什麼樣的情況下以什麼樣的順序做什麼樣的動作。因為C++中能操作的資源只有記憶體,故動作也就很簡單的只是關於記憶體內容的運算和賦值取值等,也就是前面說過的運算式。而對於“什麼樣的順序”,C++強行規定只能從上朝下,從左朝右來執行單句或複合陳述式(不要和前面關於運算式的計算順序搞混了,那隻是在一個單句中的規則)。而最後對於“什麼樣的情況”,即進行條件的判斷。為了不同情況下能執行不同的代碼,C++定義了跳躍陳述式來實現,其是基於CPU的運行規則來實現的,下面先來看CPU是如何執行機器代碼的。
機器代碼的運行方式
前面已經說過,C++中的所有代碼到最後都要變成CPU能夠認識的機器代碼,而機器代碼由於是方法的描述也就包含了動作和動作的賓語(也可能不帶賓語),即機器指令和記憶體位址或其他硬體資源的標識,並且全部都是用位元表示的。很正常,這些代表機器代碼的位元出於效率的考慮在執行時要放到記憶體中(實際也可以放在硬碟或其他存放裝置中),則很正常地每個機器指令都能有一個地址和其相對應。
CPU內帶一種功能和記憶體一樣的用於暫時記錄位元的硬體,稱作寄存器,其讀取速度較記憶體要快很多,但大小就小許多了。為了加快讀取速度,寄存器被去掉了定址電路進而一個寄存器只能存放1個32位的位元(對於32位電腦)。而CPU就使用其中的一個寄存器來記錄當前欲啟動並執行機器指令的位置,在此稱它為指令寄存器。
CPU運行時,就取出指令寄存器的值,進而找到相應的記憶體,讀取1個位元組的內容,查看此8位位元對應的機器指令是什麼,進而做相應的動作。由於不同的指令可能有不同數量的參數(即前面說的動作的賓語)需要,如乘法指令要兩個參數以將它們乘起來,而取反操作只需要一個參數的參與。並且兩個8位位元的乘法和兩個16位位元的乘法也不相同,故不同的指令帶不同的參數而形成的機器代碼的長度可能不同。每次CPU執行完某條機器代碼後,就將指令寄存器的內容加上此機器代碼的長度以使指令寄存器指向下一條機器代碼,進而重複上面的過程以實現程式的運行(這隻是簡單地說明,實際由於各種技術的加入,如高速緩衝等,實際的運行過程要比這複雜得多)。
語句的分類
在C++中,語句總共有6種:聲明語句、定義語句、運算式語句、指令語句、先行編譯語句和備註陳述式。其中的聲明語句下篇說明,先行編譯語句將在《C++從零開始(十六)》中說明,而定義語句就是前面已經見過的定義變數,後面還將說明定義函數、結構等。運算式語句則就是一個運算式直接接一個“;”,如:34;、a = 34;等,以依靠操作符的計算功能的定義而產生相應的關於記憶體值操作的代碼。備註陳述式就是用於注釋代碼的語句,即寫來給人看的,不是給編譯器看的。最後的指令語句就是含有下面所述關鍵字的語句,即它們的用處不是操作記憶體,而是實現前面說的“什麼樣的情況”。
這裡的聲明語句、先行編譯語句和備註陳述式都不會轉換成機器代碼,即這三種語句不是為了操作電腦,而是其他用途,以後將詳述。而定義語句也不一定會產生機器代碼,只有運算式語句和指令語句一定會產生代碼(不考慮編譯器的最佳化功能)。
還應注意可以寫空語句,即;或{},它們不會產生任何代碼,其作用僅僅只是為了保證文法上的正確,後面將看到這一點。下面說明備註陳述式和指令語句——跳躍陳述式、判斷語句和迴圈語句(實際不止這些,由於異常和模板技術的引入而增加了一些語句,將分別在說明異常和模板時說明)。
備註陳述式——//、/**/
注釋,即用於解釋的標註,即一些文字資訊,用以向看原始碼的人解釋這段代碼什麼意思,因為人的認知空間和電腦的完全不同,這在以後說明如何編程時會具體討論。要書寫一段話用以注釋,用“/*”和“*/”將這段話括起來,如下:
long a = 1;
a += 1; /* a放的是人的個數,讓人的個數加一 */
b *= a; /* b放的是人均花費,得到總的花費 */
上面就分別針對a += 1;和b *= a;寫了兩條備註陳述式以說明各自的語義(因為只要會C++都知道它們是一個變數的自增一和另一個變數的自乘a,但不知道意義)。上面的麻煩之處就是需要寫“/*”和“*/”,有點麻煩,故C++又提供了另一種備註陳述式——“//”:
long a = 1;
a += 1; // a放的是人的個數,讓人的個數加一
b *= a; // b放的是人均花費,得到總的花費
上面和前面等效,其中的“//”表示從它開始,這一行後面的所有字元均看成注釋,編譯器將不予理會,即
long a = 1; a += 1; // a放的是人的個數,讓人的個數加一 b *= a;
其中的b *= a;將不會被編譯,因為前面的“//”已經告訴編譯器,從“//”開始,這一行後面的所有字元均是注釋,故編譯器不會編譯b *= a;。但如果
long a = 1; a += 1; /* a放的是人的個數,讓人的個數加一 */ b *= a;
這樣編譯器依舊會編譯b *= a;,因為“/*”和“*/”括起來的才是注釋。
應該注意備註陳述式並不是語句,其不以“;”結束,其只是另一種文法以提供注釋功能,就好象以後將要說明的先行編譯語句一樣,都不是語句,都不以“;”結束,既不是單句也不是複合陳述式,只是出於習慣的原因依舊將它們稱作語句。
跳躍陳述式——goto
前面已經說明,原始碼(在此指用C++編寫的代碼)中的語句依次地轉變成用長度不同的位元表示的機器代碼,然後順序放在記憶體中(這種說法不準確)。如下面這段代碼:
long a = 1; // 假設長度為5位元組,地址為3000
a += 1; // 則其地址為3005,假設長度為4位元組
b *= a; // 則其地址為3009,假設長度為6位元組
上面的3000、3005和3009就表示上面3條語句在記憶體中的位置,而所謂的跳躍陳述式,也就是將上面的3000、3005等語句的地址放到前面提過的指令寄存器中以使得CPU開始從給定的位置執行以表現出執行順序的改變。因此,就必須有一種手段來表現語句的地址,C++對此給出了標號(Label)。
寫一標識符,後接“:”即建立了一映射,將此標識符和其所在位置的地址綁定了起來,如下:
long a = 1; // 假設長度為5位元組,地址為3000
P1:
a += 1; // 則其地址為3005,假設長度為4位元組
P2:
b *= a; // 則其地址為3009,假設長度為6位元組
goto P2;
上面的P1和P2就是標號,其值分別為3005和3009,而最後的goto就是跳躍陳述式,其格式為goto <標號>;。此語句非常簡單,先通過“:”定義了一個標號,然後在編寫goto時使用不同的標號就能跳到不同的位置。
應該注意上面故意讓P1和P2定義時獨佔一行,其實也可以不用,即:
long a = 1; P1: a += 1; P2: b *= a; goto P2;
因此看起來“P1:”和“P2:”好象是單獨的一條定義語句,應該注意,準確地說它們應該是語句修飾符,作用是定義標號,並不是語句,即這樣是錯誤的:
long a = 1; P1: { a += 1; P2: b *= a; P3: } goto P2;
上面的P3:將報錯,因為其沒有修飾任何語句。還應注意其中的P1仍然是3005,即“{}”僅僅只是其複合的作用,實際並不產生代碼進而不影響語句的地址。
判斷語句——if else、switch
if else 前面說過了,為了實現“什麼樣的情況”做“什麼樣的動作”,故C++非常正常地提供了條件判斷語句以實現條件的不同而執行不同的代碼。if else的格式為:
if(<數字>)<語句1>else<語句2> 或者 if(<數字>)<語句1>
long a = 0, b = 1;
P1:
a++;
b *= a;
if( a < 10 )
goto P1;
long c = b;
上面的代碼就表示只有當a的值小於10時,才跳轉到P1以重複執行,最後的效果就是c的值為10的階乘。
上面的<數字>表示可以在“if”後的括弧中放一數字,即運算式,而當此數位值非零時,即邏輯真,程式跳轉以執行<語句1>,如果為零,即邏輯假,則執行<語句2>。即也可如此:if( a – 10 ) goto P1;,其表示當a – 10不為零時才執行goto P1;。這和前面的效果一樣,雖然最後c仍然是10的階乘,但意義不同,代碼的可讀性下降,除非出於效率的考慮,不推薦如此書寫代碼。
而<語句1>和<語句2>由於是語句,也就可以放任何是語句的東西,因此也可以這樣:
if( a ) long c;
上面可謂吃飽了撐了,在此只是為了說明<語句1>實際可以放任何是語句的東西,但由於前面已經說過,標號的定義以及備註陳述式和先行編譯語句其實都不是語句,因此下面試圖當a非零時,定義標號P2和當a為零時書寫注釋“錯誤!”的意圖是錯誤的:
if( a ) P2: 或者 if( !a ) // 錯誤!
a++; a++;
但編譯器不會報錯,因為前者實際是當a非零時,將a自增一;後者實際是當a為零時,將a自增一。還應注意,由於複合陳述式也是語句,因此:
if( a ){ long c = 0; c++; }
由於使用了複合陳述式,因此這個判斷語句並不是以“;”結尾,但它依舊是一個單句,即:
if( a )
if( a < 10 ) { long c = 0; c++; }
else
b *= a;
上面雖然看起來很複雜,但依舊是一個單句,應該注意當寫了一個“else”時,編譯器向上尋找最近的一個“if”以和其匹配,因此上面的“else”是和“if( a < 10 )”匹配的,而不是由於上面那樣的縮排書寫而和“if( a )”匹配,因此b *= a;只有在a大於等於10的時候才執行,而不是想象的a為零的時候。
還應注意前面書寫的if( a ) long c;。這裡的意思並不是如果a非零,就定義變數c,這裡涉及到範圍的問題,將在下篇說明。
switch 這個語句的定義或多或少地是因為實現的原因而不是和“if else”一樣由於邏輯的原因。先來看它的格式:switch(<整型數字>)<語句>。
上面的<整型數字>和if語句一樣,只要是一個數字就可以了,但不同地必須是整型數字(後面說明原因)。然後其後的<語句>與前相同,只要是語句就可以。在<語句>中,應該使用這樣的形式:case <整型常數1>:。它在它所對應的位置定義了一個標號,即前面goto語句使用的東西,表示如果<整型數字>和<整型常數1>相等,程式就跳轉到“case <整型常數1>:”所標識的位置,否則接著執行後續的語句。
long a, b = 3;
switch( a + 3 )
case 2: case 3: a++;
b *= a;
上面就表示如果a + 3等於2或3,就跳到a++;的地址,進而執行a++,否則接著執行後面的語句b *= a;。這看起來很荒謬,有什麼用?一條語句當然沒意義,為了能夠標識多條語句,必須使用複合陳述式,即如下:
long a, b = 3;
switch( a + 3 )
{
b = 0;
case 2:
a++; // 假設地址為3003
case 3:
a--; // 假設地址為3004
break;
case 1:
a *= a; // 假設地址為3006
}
b *= a; // 假設地址為3010
應該注意上面的“2:”、“3:”、“1:”在這裡看著都是整型的數字,但實際應該把它們理解為標號。因此,上面檢查a + 3的值,如果等於1,就跳到“1:”標識的地址,即3006;如果為2,則跳轉到3003的地方執行代碼;如果為3,則跳到3004的位置繼續執行。而上面的break;語句是特定的,其放在switch後接的語句中表示打斷,使程式跳轉到switch以後,對於上面就是3010以執行b *= a;。即還可如此:
switch( a ) if( a ) break;
由於是跳到相應位置,因此如果a為-1,則將執行a++;,然後執行a--;,再執行break;而跳到3010地址處執行b *= a;。並且,上面的b = 0;將永遠不會被執行。
switch表示的是針對某個變數的值,其不同的取值將導致執行不同的語句,非常適合實現狀態的選擇。比如用1表示安全,2表示有點危險,3表示比較危險而4表示非常危險,通過書寫一個switch語句就能根據某個怪物當前的狀態來決定其應該做“逃跑”還是“攻擊”或其他的行動以實現遊戲中的人工智慧。那不是很奇怪嗎?上面的switch通過if語句也可以實現,為什麼要專門提供一個switch語句?如果只是為了簡寫,那為什麼不順便提供多一些類似這種邏輯方案的簡寫,而僅僅只提供了一個分支選擇的簡寫和後面將說的迴圈的簡寫?因為其是出於一種最佳化技術而提出的,就好象後面的迴圈語句一樣,它們對邏輯的貢獻都可以通過if語句來實現(畢竟邏輯就是判斷),而它們的提出一定程度都是基於某種最佳化技術,不過後面的迴圈語句簡寫的成分要大一些。
我們給出一個數組,數組的每個元素都是4個位元組大小,則對於上面的switch語句,如下:
unsigned long Addr[3]; Addr[0] = 3006; Addr[1] = 3003; Addr[2] = 3004;
而對於switch( a + 3 ),則使用類似的語句就可以代替:goto Addr[ a + 3 – 1 ];
上面就是switch的真面目,應注意上面的goto的寫法是錯誤的,這也正是為什麼會有switch語句。編譯器為我們構建一個儲存地址的數組,這個數組的每個元素都是一個地址,其表示的是某條語句的地址,這樣,通過不同的位移即可實現跳轉到不同的位置以執行不同的語句進而表現出狀態的選擇。
現在應該瞭解為什麼上面必須是<整型數字>了,因為這些數字將用於數組的下標或者是位移,因此必須是整數。而<整型常數1>必須是常數,因為其由編譯時間期告訴編譯器它現在所在位置應放在地址數組的第幾個元素中。
瞭解了switch的實現後,以後在書寫switch時,應盡量將各case後接的整型常數或其倍數靠攏以減小需產生的數組的大小,而無需管常數的大小。即case 1000、case1001、case 1002和case 2、case 4、case 6都只用3個元素大小的數組,而case 0、case 100、case 101就需要102個元素大小的數組。應該注意,現在的編譯器都很智能,當發現如剛才的後者這種只有3個分支卻要102個元素大小的數組時,編譯器是有可能使用重複的if語句來代替上面數組的產生。
switch還提供了一個關鍵字——default。如下:
long a, b = 3;
switch( a + 3 )
{
case 2:
a++;
break;
case 3:
a += 3;
break;
default:
a--;
}
b *= a;
上面的“default:”表示當a + 3不為2且不為3時,則執行a--;,即default表示預設的狀況,但也可以沒有,則將直接執行switch後的語句,因此這是可以的:switch( a ){}或switch( a );,只不過毫無意義罷了。
迴圈語句——for、while、do while
剛剛已經說明,迴圈語句的提供主要是出於簡寫目的,因為迴圈是方法描述中用得最多的,且演算法並不複雜,進而對編譯器的開發難度不是增加太多。
for 其格式為for(<數字1>;<數字2>;<數字3>)<語句>。其中的<語句>同上,即可接單句也可接複合陳述式。而<數字1>、<數字2>和<數字3>由於是數字,就是運算式,進而可以做運算式語句能做的所有的工作——操作符的計算。for語句的意思是先計算<數字1>,相當於初始化工作,然後計算<數字2>。如果<數字2>的值為零,表示邏輯假,則退出迴圈,執行for後面的語句,否則執行<語句>,然後計算<數字3>,相當於每次迴圈的例行公事,接著再計算<數字2>,並重複。上面的<語句>一般被稱作迴圈體。
上面的設計是一種面向過程的設計思想,將迴圈體看作是一個過程,則這個過程的初始化(<數字1>)和必定執行(<數字3>)都表現出來。一個簡單的迴圈,如下:
long a, b;
for( a = 1, b = 1; a <= 10; a++ )
b *= a;
上面執行完後b是10的階乘,和前面在說明if語句時舉的例子相比,其要簡單地多,並且可讀性更好——a = 1, b = 1是初始化操作,每次迴圈都將a加一,這些資訊是goto和if語句表現不出來的。由於前面一再強調的語句和數位概念,因此可以如下:
long a, b = 1;
for( ; b < 100; )
for( a = 1, b = 1; a; ++a, ++b )
if( b *= a )
switch( a = b )
{
case 1:
a++; break;
case 2:
for( b = 10; b; b-- )
{
a += b * b;
case 3: a *= a;
}
break;
}
上面看著很混亂,注意“case 3:”在“case 2:”後的一個for語句的迴圈體中,也就是說,當a = b返回1時,跳到a++;處,並由於break;的緣故而執行switch後的語句,也就是if後的語句,也就是第二個for語句的++a, ++b。當返回2時,跳到第三個for語句處開始執行,迴圈完後同樣由break;而繼續後面的執行。當返回3時,跳到a *= a;處執行,然後計算b--,接著計算b的值,檢查是否非零,然後重複迴圈直到b的值為零,然後繼續以後的執行。上面的代碼並沒什麼意義,在這裡是故意寫成這麼混亂以進一步說明前面提過的語句和數位概念,如果真正執行,大致看過去也很容易知道將是一個死迴圈,即永遠迴圈無法退出的迴圈。
還應注意C++提出了一種特殊文法,即上面的<數字1>可以不是數字,而是一變數定義語句,即可如此:for( long a = 1, b = 1; a < 10; ++a, ++b );。其中就定義了變數a和b。但是也只能接變數定義語句,而結構定義、類定義及函數定義語句將不能寫在這裡。這個文法的提出是更進一步地將for語句定義為記數式迴圈的過程,這裡的變數定義語句就是用於定義此迴圈中充當計數器的變數(上面的a)以實現迴圈固定次數。
最後還應注意上面寫的<數字1>、<數字2>和<數字3>都是可選的,即可以:for(;;);。
while 其格式為while(<數字>)<語句>,其中的<數字>和<語句>都同上,意思很明顯,當<數字>非零時,執行<語句>,否則執行while後面的語句,這裡的<語句>被稱作迴圈體。
do while 其格式為do<語句>while(<數字>);。注意,在while後接了“;”以表示這個單句的結束。其中的<數字>和<語句>都同上,意思很明顯,當<數字>非零時,執行<語句>,否則執行while後面的語句,這裡的<語句>被稱作迴圈體。
為什麼C++要提供上面的三種迴圈語句?簡寫是一重要目的,但更重要的是可以提供一定的最佳化。for被設計成用於固定次數的迴圈,而while和do while都是用於條件決定的迴圈。對於前者,編譯器就可以將前面提過的用於記數的變數映射成寄存器以最佳化速度,而後者就要視編譯器的智能程度來決定是否能產生最佳化代碼了。
while和do while的主要區別就是前者的迴圈體不一定會被執行,而後者的迴圈體一定至少會被執行一次。而出於簡寫的目的,C++又提出了continue和break語句。如下:
for( long i = 0; i < 10; i++ )
{
if( !( i % 3 ) )
continue;
if( !( i % 7 ) )
break;
// 其他語句
}
上面當i的值能被3整除時,就不執行後面的“其他語句”,而是直接計算i++,再計算i < 10以決定是否繼續迴圈。即continue就是終止當前這次迴圈的執行,開始下一次的迴圈。上面當i的值能被7整除時,就不執行後面的“其他語句”,而是跳出迴圈體,執行for後的語句。即break就是終止迴圈的運行,立即跳出迴圈體。如下:
while( --i ) do
{ {
if( i == 10 ) if( i == 10 )
continue; continue;
if( i > 20 ) if( i > 20 )
break; break;
// 其他語句 // 其他語句
} }while( --i );
a = i; a = i;
上面的continue;執行時都將立即計算—i以判斷是否繼續迴圈,而break;執行時都將立即退出迴圈體進而執行後繼的a = i;。
還應注意嵌套問題,即前面說過的else在尋找配對的if時,總是找最近的一個if,這裡依舊。
long a = 0;
P1:
for( long i = a; i < 10; i++ )
for( long j = 0; j < 10; j++ )
{
if( !( j % 3 ) )
continue;
if( !( j % 7 ) )
break;
if( i * j )
{
a = i * j;
goto P1;
}
// 其他語句
}
上面的continue;執行後,將立即計算j++,而break;執行後,將退出第二個迴圈(即j的迴圈),進而執行i++,然後繼續由i < 10來決定是否繼續迴圈。當goto P1;執行時,程式跳到上面的P1處,即執行long i = a;,進而重新開始i的迴圈。
上面那樣書寫goto語句是不被推薦的,因為其破壞了迴圈,不符合人的思維習慣。在此只是要說明,for或while、do while等都不是迴圈,只是它們各自的用處最後表現出來好象是迴圈,實際只是程式執行位置的變化。應清楚語句的實現,這樣才能清楚地瞭解各種語句的實際作用,進而明確他人寫的代碼的意思。而對於自己書寫代碼,瞭解語句的實現,將有助於進行一定的最佳化。但當你寫出即精簡又執行效率高的程式時,保持其良好的可讀性是一個程式員的素養,應盡量培養自己書寫可讀性高的代碼的習慣。
上面的long j = 0在第一個迴圈的迴圈體內,被多次執行豈不是要多次定義?這屬於變數的範圍的問題,下篇將說明。
本篇的內容應該是很簡單的,重點只是應該理解原始碼編譯成機器指令後,在執行時也放在記憶體中,故每條語句都對應著一個地址,而通過跳躍陳述式即可改變程式的運行順序。下篇將對此提出一系列的概念,並說明聲明和定義的區別。