前面已經說過程序就是方法的描述,而方法的描述無外乎就是動作加動作的賓語,而這裡的動作在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種:聲明語句、定義語句、表達式語句、指令語句、預編譯語句和注釋語句。其中的聲明語句下篇說明,預編譯語句將在另文中說明,而定義語句就是前面已經見過的定義變量,後面還將說明定義函數、結構等。表達式語句則就是一個表達式直接接一個“;”,如: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在第一個循環的循環體內,被多次執行豈不是要多次定義?這屬於變量的作用域的問題,下篇將說明。
本篇的內容應該是很簡單的,重點只是應該理解源代碼編譯成機器指令後,在執行時也放在內存中,故每條語句都對應著一個地址,而通過跳轉語句即可改變程序的運行順序。下篇將對此提出一系列的概念,並重點說明類型的意義。