C++程式設計從零開始之語句(轉)

ba發表於2007-08-15
C++程式設計從零開始之語句(轉)[@more@]前面已經說過程式就是方法的描述,而方法的描述無外乎就是動作加動作的賓語,而這裡的動作在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()else 或者 if()
long a = 0, b = 1;
P1:
a++;
b *= a;
if( a < 10 )
goto P1;
long c = b;

上面的程式碼就表示只有當a的值小於10時,才跳轉到P1以重複執行,最後的效果就是c的值為10的階乘。

上面的表示可以在“if”後的括號中放一數字,即表示式,而當此數字的值非零時,即邏輯真,程式跳轉以執行,如果為零,即邏輯假,則執行。即也可如此:if( a – 10 ) goto P1;,其表示當a – 10不為零時才執行goto P1;。這和前面的效果一樣,雖然最後c仍然是10的階乘,但意義不同,程式碼的可讀性下降,除非出於效率的考慮,不推薦如此書寫程式碼。

而和由於是語句,也就可以放任何是語句的東西,因此也可以這樣:

if( a ) long c;

上面可謂吃飽了撐了,在此只是為了說明實際可以放任何是語句的東西,但由於前面已經說過,標號的定義以及註釋語句和預編譯語句其實都不是語句,因此下面試圖當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 :。它在它所對應的位置定義了一個標號,即前面goto語句使用的東西,表示如果和相等,程式就跳轉到“case :”所標識的位置,否則接著執行後續的語句。

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語句。編譯器為我們構建一個儲存地址的陣列,這個陣列的每個元素都是一個地址,其表示的是某條語句的地址,這樣,透過不同的偏移即可實現跳轉到不同的位置以執行不同的語句進而表現出狀態的選擇。

現在應該瞭解為什麼上面必須是了,因為這些數字將用於陣列的下標或者是偏移,因此必須是整數。而必須是常數,因為其由編譯時期告訴編譯器它現在所在位置應放在地址陣列的第幾個元素中。

瞭解了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(;;)。其中的同上,即可接單句也可接複合語句。而、和由於是數字,就是表示式,進而可以做表示式語句能做的所有的工作——運算子的計算。for語句的意思是先計算,相當於初始化工作,然後計算。如果的值為零,表示邏輯假,則退出迴圈,執行for後面的語句,否則執行,然後計算,相當於每次迴圈的例行公事,接著再計算,並重復。上面的一般被稱作迴圈體。

上面的設計是一種程式導向的設計思想,將迴圈體看作是一個過程,則這個過程的初始化()和必定執行()都表現出來。一個簡單的迴圈,如下:

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++提出了一種特殊語法,即上面的可以不是數字,而是一變數定義語句,即可如此:for( long a = 1, b = 1; a < 10; ++a, ++b );。其中就定義了變數a和b。但是也只能接變數定義語句,而結構定義、類定義及函式定義語句將不能寫在這裡。這個語法的提出是更進一步地將for語句定義為記數式迴圈的過程,這裡的變數定義語句就是用於定義此迴圈中充當計數器的變數(上面的a)以實現迴圈固定次數。

最後還應注意上面寫的、和都是可選的,即可以:for(;;);。

while 其格式為while(),其中的和都同上,意思很明顯,當非零時,執行,否則執行while後面的語句,這裡的被稱作迴圈體。

do while 其格式為dowhile();。注意,在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在第一個迴圈的迴圈體內,被多次執行豈不是要多次定義?這屬於變數的作用域的問題,下篇將說明。

本篇的內容應該是很簡單的,重點只是應該理解原始碼編譯成機器指令後,在執行時也放在記憶體中,故每條語句都對應著一個地址,而透過跳轉語句即可改變程式的執行順序。下篇將對此提出一系列的概念,並重點說明型別的意義。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617731/viewspace-957695/,如需轉載,請註明出處,否則將追究法律責任。

相關文章