3.1 語句與程式塊
在表達式之後加上一個分号(;),它們就變成了語句。
用一對花括号“{”與“}”把一組聲明和語句括在一起就構成了程式塊,在文法上等價于單條語句。
3.2 if-else語句
每個else與最近的前一個沒有else配對的if進行比對。
if (n > 0)
if (a > b)
z = a;
else
z = b;
程式的縮進結構明确表明了設計意圖,但編譯器無法獲得這一資訊,它會将else部分與内層的if配對。
是以最好經常把if else語句塊括起來!
二分查找:
int binsearch ( int x, int v[ ], int n )
{
int low, high, mid;
low = 0;
high = n - 1;
while ( low <= high ) {
mid = ( low + high ) / 2;
if ( x < v[mid] )
high = mid - 1;
else if ( x > v[mid] )
low = mid + 1;
else
return mid;
}
return -1;
}
練習3-1 上面折半查找的例子中,while循環語句内執行了兩次測試。重寫該函數,
使循環内部隻執行一次測試。比較兩種版本函數的運作時間。
答:
while (low <= high) {
mid = (low + high) / 2;
if (x < v[mid])
high = mid - 1;
else
low = mid ;
}
if (x == v[mid])
return mid;
else
return -1;
3.4 switch語句
s w i t c h語句是一種多路判定語句,它根據表達式是否與若幹常量整數值中的某一個比對來相
應地執行有關的分支動作。
switch ( 表達式) {
case 常量表達式: 語句序列
case 常量表達式: 語句序列
default: 語句序列
}
如果沒有default情形,且沒有任何一個情景比對,那麼該switch語句不執行任何動作。各個case的執行順序是任意的。
在s w i t c h語句中c a s e情形的作用就像标号一樣,在某個c a s e情形之後的代碼執行完後,就進入下一個c a s e情形執行,除非顯式控制轉出。轉出
s w i t c h語句最常用的方法是使用b r e a k語句與r e t u r n語句。為了防止直接進入下一個情形,最好在每個情形後以break結尾。
練習3-2 編寫一個函數escape(s, t),将字元串t複制到字元串s中,并在複制過程中将換行符、
制表符等不可見字元分别轉換為\n、\t等相應可見的轉義字元。再編寫一個相反功能的函數。
答:
#include
void escape(char s[], char t[])
{
int i, j;
for (i = 0, j = 0; s[i] != '\0'; i++) {
switch (s[i]) {
case '\n':
t[j++] = '\\';
t[j++] = 'n';
break;
case '\t':
t[j++] = '\\';
t[j++] = 't';
break;
default:
t[j++] = s[i];
break;
}
}
t[j] = '\0';
}
void escape2(char s[], char t[])
{
int i, j;
for (i = 0, j = 0; s[i] != '\0'; j++) {
switch (s[i]) {
case '\\':
if (s[i+1] == 'n') {
t[j] = '\n';
i += 2;
}
else if (s[i+1] == 't') {
t[j] = '\t';
i += 2;
}
else {
t[j] = s[i];
i++;
}
default:
t[j] = s[i++];
break;
}
}
}
練習3-3 編寫函數expand(s1, s2),将字元串s1中類似于a-z一類的速記符号在字元串s2中
擴充為等價的完整清單abc...xyz。該函數可以處理大小寫字母和數字,并可以處理a-b-c、
a-z0-9與-a-z等類似的情況。作為前導和尾随的-字元原樣排印。
答:
#include
void expand(char s1[], char s2[])
{
int i, j, k;
i = j = 0;
while (s1[i] != '\0') {
if (s1[i] == '-' && 0 < i && s1[i+1] != '\0' &&
s1[i-1] != '-' && s1[i+1] != '-') {
j--; // avoid duplicate letter
for (k = s1[i-1]; k <= s1[i+1]; k++, j++)
s2[j] = k;
i += 2;
} else {
s2[j++] = s1[i++];
}
}
s2[j] = '\0';
}
main()
{
char s1[] = "-a-b-hAbC-G0-8---";
char s2[100];
expand(s1, s2);
printf("before expand:%s\nafter expand: %s\n", s1, s2);
}
3.5 while循環與for循環
for (表達式1; 表達式2; 表達式3)
語句
等價于=>
表達式1;
while (表達式2) {
語句
表達式3;
}
如果表達式1與表達式3 被省略了,那麼它退化成了w h i l e循環語句。
如果用于測試的表達式2 不存在,那麼就認為表達式2 的值永遠是真的,進而,f o r循環語句
for ( ; ; ) {
…
}
就是一個“無限”循環語句,這種語句要用其他手段(如b r e a k語句或r e t u r n語句)才能終止執行。
例子:atoi 函數的實作,将字元轉化為數字,除去前面的空白字元,處理+ - 操作,
#include
int atoi ( char s[ ])
{
int i, n , sign;
for ( i = 0; isspace ( s[i] ); i++ )
;
sign = ( s[i] == '-' ) ? -1 : 1;
if (s[i] == '+' || s[i] == '-' )
i++;
for ( n = 0; isdigit ( s[i] ); i++)
n = 10 * n + (s[i] - '0' );
return sign * n;
}
shell排序:
void shellsort ( int v[ ], int n )
{
int gap, i, j, temp;
for ( gap = n/2; gap > 0; gap /= 2 )
for ( i = gap; i < n; i++ )
for ( j = i-gap; j >= 0 && v[j] > v[j+gap]; j -= gap ) {
temp = v[j];
v[j] = v[j+gap];
v[j+gap] = temp;
}
}
}
将整型轉化為字元串itoa的實作:
void itoa ( int n, char s[ ] );
{
int i, sign;
if ( ( sign = n ) < 0 )
n = -n;
i = 0;
do {
s[i++] = n % 10 + '0';
} while ( (n /= 10) > 0);
if (sign < 0)
s[i++] = '-';
s[i] = '\0';
reverse(s);
}
因為即使n為0也要至少把一個字元放到數組 s中,是以在這裡有必要使用 d o - w h i l e語句,至
少使用d o - w h i l e語句要友善一些。
atof 将字元串轉化為浮點數 比如123.45 ---與atoi相比,在小數點.之後開始記錄小數點位數pow*=10.0
double atof( char s[ ])
{
double val, power;
int i, sign;
for ( i = 0; isspace(s[i]); i++ )
;
sign = (s[i] == '-' ) ? -1 : 1;
if ( s[i] == '+' || s[i] == '-' )
i++;
for (val = 0.0; isdigit(s[i]); i++)
val = 10.0 * val +(s[i] -'0' );
if (s [i] ] = = '.')
i++;
for ( power = 1.0; isdigit(s[i]); i++) {
val = 10.0 * val +(s[i] -'0' );
power *= 10.0;
}
return sign * val / power;
}
練習3-4 在數的對二的補碼表示中,上面的itoa函數不能處理最大的負數-2的(字長-1)次方
的情況。解釋其原因,并修改函數使它在任何機器上運作時都能列印出正确的值。
答:
例如char字長為8位,則對二補碼範圍為-128~127。
128的二進制源碼為10000000,-128通過補碼的負數轉換規則也得到10000000,即-128二進制碼為80(可用prinf("%hhx);驗證)。即-128的二進制表示實際上市128,是以值為-128的char,n=-n;後值仍為-128。
修改函數,不将n轉為正數,而是将每次取模運算的結果轉為正數。進而避開無法将最大負數轉為正數的問題。
#include
#define abs(x) ((x) < 0 ? -(x) : (x))
void itoa(int n, char s[])
{
int i, sign;
sign = n;
i = 0;
do {
s[i++] = abs(n % 10) + '0';
} while ((n /= 10) != 0);
if (sign < 0)
s[i++] = '-';
s[i] = '\0';
//reverse(s);
}
main()
{
char s[20];
itoa(10, s);
printf("%s\n", s);
itoa(-128, s);
printf("%s\n", s);
}
練習3-5 編寫函數itob(n, s, b),将整數n轉換為以b為底的數,并将轉換結果以字元的形式
儲存到字元串s中。例如,itob(n, s, 16)把整數n格式化為十六進制整數儲存在s中。
答:
#include
#include "reverse.c"
#define abs(x) (x) < 0 ? -(x) : (x)
void itob(int n, char s[], int b)
{
int i, x, sign;
sign = n;
i = 0;
do {
x = abs(n % b);
if (x >= 10)
s[i++] = (x - 10) + 'A'; else
s[i++] = x + '0';
} while ((n /= b) != 0);
if (sign < 0)
s[i++] = '-';
s[i] = '\0';
reverse(s);
}
main()
{
char s[20];
itob(29, s, 2);
printf("%s\n", s);
itob(-257, s, 16);
printf("%s\n", s);
}
練習 3-6 修改itoa函數,使得該函數可以接收三個參數。第三個參數為最小字段寬度。
為了保證轉換後結果至少具有第三個參數指定的最小寬度,必要時在結果左邊填充一定的空格。
答:
...
if (sign < 0)
s[i++] = '-';
while (i <= w-1) // fill space
s[i++] = ' ';
...
3.7 break和continue語句
在f o r循環語句中,c o n t i n u e語句的執行則意味着使控制傳遞到增量部分。 c o n t i n u e語句隻
能用于循環語句,不能用于 s w i t c h語句。如果某個 c o n t i n u e語句位于s w i t c h語句中,而後者又位
于循環語句中,那麼該c o n t i n u e語句用于控制下一次循環。
在循環的某些部分比較複雜時常常要使用 c o n t i n u e語句。如果不使用 c o n t i n u e語句,那麼就
可能要把測試反過來,或嵌入另一層循環,而這又會使程式的嵌套更深。
3.8 goto語句與标号
最常見的用法是在某些深度嵌套的結構中放棄處理,例如一次中止兩層或多層循環。 b r e a k語句不能直接用于這一目的,它隻能用于從最内層循環退出。下面是使用g o t o語句的一個例子:
for ( ⋯ )
for ( ⋯ ) {
⋯
if (disaster)
goto error;
}
⋯
error:
清理操作
如果錯誤處理比較重要并且在好幾個地方都會出現錯誤,那麼使用這種組織就比較靈活友善。
标号的形式與變量名字相同,其後要跟一個冒号。标号可以用在任何語句的前面,但要與
相應的g o t o語句位于同一函數中。标号的作用域是整個函數
看一個例子,考慮判定在兩個數組 a與b中是否具有相同元素的問題。一種可能的解決方 法是: for (i = 0; i < n; i++) for (j = 0; j < m; j++) if (a[i] == b[j]) 第3章 控 制 流計計 53 下載下傳 goto found; ⋯ found: ⋯ 所有帶有g o t o語句的程式代碼都可以改寫成不包含 g o t o語句的程式,但這可能需要以增加一 些額外的重複測試或變量為代價。例如,可将這個判定數組元素是否相同的程式段改寫成如下 形式: found = 0; for (i = 0; i < n && !found; i++) for (j = 0; j < m && !found; j++) if (a[i] == b[j]) found = 1; if (found) ⋯ else ⋯ 除了以上介紹的幾個程式段外,依賴于 g o t o語句的程式段一般都比不使用 g o t o語句的程式段 難以了解與維護。雖然不特别強調這一點,但我們還是建議盡可能減少 g o t o語句的使用。