因為早期C版本不支持const,所以人們在過去編寫了大量不支持const的代碼。因此,舊的C代碼有很大的改進潛力。此外,許多用慣了早期C版本的程序員在使用ANSI C時,仍不使用const,這將使他們失去建立良好軟件工程的機會。當然也要注意,儘管ANSI C對const做了很好的定義,但仍有某些系統不支持const。
在討論使用const之前,先來看看左值和右值問題。
14.5.1 左值和右值
變量是一個指名的存儲區域,左值是指向某個變量的表達式。名字「左值」來源於賦值表達式「A=B」,其中左運算份量「A」必須能被計算和修改。左值表達式在賦值語句中既可以作為左操作數,也可以作為右操作數,例如「x=56」和「y=x」,x既可以作為左值(x=56),又可以作為右值(y=x)。但右值「56」只能作為右操作數,而不能作為左操作數。
某些運算符可以產生左值。例如,如果「p」是一個被正確初始化的指針類型的表達式,則「p」就是左值表達式,可以通過「p=」改變這個指針的指向。同理,「*p」也是一個左值表達式,它代表由p指向的變量,並且可以通過「*p=」改變這個變量的值。假如有變量a,因為可以執行「a=*p」的操作,所以「*p」又是一個右值表達式。當然,也可以把「p」的值作為右值賦給一個指針變量,所以「p」也是一個右值表達式。
【例14.12】改正下面程序中的錯誤。
#include <stdio.h> void main ( ) { char a=\"We are here !\" ,b[16] ; b=a ; printf (b ); printf (\"n\" ); }
編譯給出出錯信息:
error C2106 : \'=\' : left operand must be l-valu
值可以作為右值,例如整數、浮點數、字符串、數組的一個元素等。在C語言中,右值是以單一值的形式出現。字符數組a和b的每個元素均可以作為右值,即「a[0]=b[0]」是正確的,但在這裡,a和b都不是字符串的單個元素,所以是錯誤的。可以將它們修改成如下的程序,即使用具體的元素作為左值和右值。
#include <stdio.h> void main ( ) { char a=\"We are here !\" ,b[16] ; int i=0 ; while (b[i]=a[i] ) i++ ; printf (b ); printf (\"n\" ); }
可能有人會說,a和b是可以作為右值的啊!確實,a和b可以作為數組首地址的值賦給指針變量,但它們不能作為左值。請看下面的程序。
【例14.13】下面是一個使用數組首地址作為右值的例子,目的是將數組a的內容複製到b中,運行輸出的結果是
We are here ! We are here !
請問這個程序正確嗎?
#include <stdio.h> void main ( ) { char a=\"We are here !\" ,b[16] ,*p1 ,*p2 ; int i=0 ; p1=a ; p2=b ; p2=p1 ; printf (p1 ); printf (\"n\" ); printf (p2 ); }
【分析】「p2=b;p2=p1;」最終是將指針p2指向字符串a,所以輸出的不是字符串b的內容。如果執行「printf(b);」,就可以驗證這一點。將「p2=p1;」改為
while (p2[i]=p1[i] ) i++ ;
即可。其實,用一個指針即可驗證這個問題。
// 將數組b 的首地址作為右值的例子。 #include <stdio.h> void main ( ) { char a=\"We are here !\" ,b[16] ,*p ; int i=0 ; p=b ; while (p[i]=a[i] ) i++ ; printf (a ); printf (\"n\" ); printf (b ); printf (\"n\" ); }
由此可見,在C語言中,左值是一個具體的變量,右值一定是一個具體類型的值,所以有些既可以作為左值,也可以作為右值,但有些只能作為右值。
14.5.2 推薦使用const定義常量
在C語言中,宏定義是一個重要內容。無參數的宏作為常量,而帶參數的宏則可以提供比函數調用更高的效率。但預處理只是進行簡單的文本代替而不做語法檢查,所以會存在一些問題。例如:
#define BUFSIZE 100
這裡的BUFSIZE只是一個名字,並不佔用存儲空間並且能被放在一個頭文件中。在編譯期間編譯器將用字符串「100」來代替所有的BUFSIZE。這種簡單的置換常常會隱藏一些很難發現的錯誤,並且這種方法還存在類型問題。比如這個BUFSIZE究竟是整數還是浮點數?而使用const,則把值代入編譯過程即可解決這些問題,和上面宏定義等效的語句如下:
const int BUFSIZE=100 ;
這樣就可以在任何編譯器需要知道這個值的地方使用BUFSIZE。並且編譯器在編譯過程中可以通過必要的計算把一個複雜的常量表達式縮減成簡單的,這在定義數組時尤其突出。但是對於某些更複雜的情況來說,宏定義往往不如常量來得簡潔清楚,可以用const來完全代替無參數的宏。
對基本數據類型的變量,一旦加上const修飾符,編譯器就將其視為一個常量,不再為它分配內存,並且每當在程序中遇到它時,都用在說明時所給出的初始值取代它。使用const可以使編譯器對處理內容有更多的瞭解,從而允許對其進行類型檢查,同時還能避免對常量的不必要的內存分配並可改善程序的可讀性。
因為被const修飾的變量的值在程序中不能被改變,所以在聲明符號常量時,必須對符號常量進行初始化,除非這個變量是用extern修飾的外部變量。例如:
const int i=8 ; // 正確 const int d ; // 錯誤 extern const int d ; // 正確
const的用處不僅僅是在常量表達式中代替宏定義。如果一個變量在生存期中的值不會改變,就應該用const來修飾這個變量,以提高程序的安全性。
【例14.14】找出下面程序中的錯誤。
#include <stdio.h> #define NUM1 2 #define NUM2 3 #define NUM3 6 void mul2 () { int i ; for (i=1 ;i<NUM3 ;i++ ) { printf (\"NUM1*%d=%dn\" ,i ,i*NUM1 ); } } void mul3 () { int i ; for (i=1 ;i<NUM3 ;i++ ) { printf (\"NUM2*%d=%dn\" ,i ,i*NUM2 ); } } void main () { mul2 (); mul3 (); }
程序引用宏定義的方法不對,語句
printf (\"NUM1*%d=%dn\" ,i ,i*NUM1 );
中的「\"」表示後面是字符,所以NUM1作為字符串,這個函數將會輸出為:
NUM1*1=2 NUM1*2=4 … … NUM2*5=15
應改為
printf (\"%d*%d=%dn\" , NUM1 ,i ,i*NUM1 ); printf (\"%d*%d=%dn\" , NUM2 ,i ,i*NUM1 );
因為都是常數,推薦的做法是使用const,下面是修改後的程序。
#include <stdio.h> int const NUM1= 2 ; int const NUM2 =3 ; int const NUM3 =6 ; void mul2 () { int i ; for (i=1 ;i<NUM3 ;i++ ) { printf (\"%d*%d=%dn\" ,NUM1 ,i ,i*NUM1 ); } } void mul3 () { int i ; for (i=1 ;i<NUM3 ;i++ ) { printf (\"%d*%d=%dn\" ,NUM2 ,i ,i*NUM2 ); } } void main () { mul2 (); mul3 (); }
輸出結果如下。
2*1=2 2*2=4 2*3=6 2*4=8 2*5=10 3*1=3 3*2=6 3*3=9 3*4=12 3*5=15
推薦使用const代替無參數的宏來定義常量。在編程中注意不要再使用宏定義常量。當然,常量是不能被改變的,它只能作為右值。
14.5.3 對函數傳遞參數使用const限定符
採用const聲明傳遞函數參數,可以避免被調用函數修改實參的值。
【例14.15】演示在被調函數中不可改變傳遞參數的例子。
#include <stdio.h> void swap (int* , const int ); void main ( ) { int a=23 , b=85 ; b=a+b ; // 在主程序裡可以修改變量b ,將它作為參數傳遞 swap (&a ,b ); // 只允許被調函數使用b ,但不允許修改b 的值 printf (\" 返回調用函數:a=%d , b=%dn\" , a , b ); b=b-a ; // 可以繼續修改變量b printf (\" 可改變b 的值:a=%d , b=%dn\" , a , b ); } void swap (int* a , const int b ) { // b=a+b ; // 這個語句修改了b 的值,編譯出錯 *a=*a+b ; printf (\" 在調用函數中:a=%d , b=%dn\" , *a , b ); }
運行結果如下。
在調用函數中:a=131 , b=108 在調用函數後:a=131 , b=108 可以改變b 值:a=131 , b=-23
在主程序聲明的變量b,可以改變它的值。將它作為函數swap的參數傳遞時,為了保證在函數swap中只使用這個傳遞的值而不修改它,可以將這個參數聲明為「const int b」,在被調函數swap中,語句「b=a+b;」企圖改寫b,則編譯系統就會報錯。
退出swap函數,保證了b的原來值。當然,這時候就可以改變b的值。
用const修飾傳遞參數,意思是通知函數,它只能使用參數而無權修改它。這主要是為了提高系統的自身安全。
例如設計的數組參數,不是供一個函數調用,所以要求任何函數調用時,都不能改變數組的內容。這時就要把數組參數用const限定。
【例14.16】不允許改變作為參數傳遞的數組內容的例子。
#include <stdio.h> int add (const int a ) { int i=0 , sum=0 ; for (i=0 ; i<5 ; i++ ) sum=sum+a[i] ; return sum ; } int mul (const int a ) { int i=0 , mul=1 ; for (i=0 ; i<5 ; i++ ) mul=mul*a[i] ; return mul ; } void main ( ) { int i , a={1 ,2 ,3 ,4 ,5} ; printf (\"add=%d ,mul=%dn\" ,add (a ),mul (a )); for (i=0 ; i<5 ; i++ ) printf (\"%d\" ,a[i] ); printf (\"n\" ); }
程序運行結果如下。
add=15 ,mul=120 1 2 3 4 5
主函數使用同一個數組分別作為add和mul函數的參數,當然add和mul函數只能使用這個數組的內容,而不允許改變數組的內容。
14.5.4 對指針使用const限定符
可以用const限定符強制改變訪問權限。用const正確地設計軟件可以大大減少調試時間和不良的副作用,使程序易於修改和調試。
1.指向常量的指針
如果想讓指針指向常量,就要聲明一個指向常量的指針,聲明的方式是在非常量指針聲明前面使用const,例如:
const int *p ; // 聲明指向常量的指針
因為目的是用它指向一個常量,而常量是不能修改的,即「*p」是常量,不能將「*p」作為左值進行操作,這其實是限定了「*p=」的操作,所以稱為指向常量的指針。當然,這並不影響P既可作為左值,也可作為右值,因此可以改變常量指針指向的常量。下面是在定義時即初始化的例子。
const int y=58 ; // 常量y 不能作為左值 const int *p=&y ; // 因為 y 是常量,所以*p 不能作為左值
指向常量的指針p指向常量y,*p和y都不能作為左值,但可以作為右值。
如果使用一個整型指針p1指向常量y,則編譯系統就要給出警告信息。這時可以使用強制類型轉換。例如:
const int y=58 ; // 常量y 不能作為左值 int *p1 ; //*p1 既可以作為左值,也可以作為右值 p1= (int * )&y ; // 因為 y 是常量,p1 不是常量指針,所以要將&y 進行強制轉換
如果在聲明p1時用常量初始化指針,也要進行轉換。例如:
int *p1= (int * )&y ; // 因為 y 是常量,p1 不是常量指針,所以要將&y 進行強制轉換
在使用時,對於常量,要注意使用指向常量的指針。
2.指向常量的指針指向非常量
因為指向常量的指針可以先聲明,後初始化,所以也會出現在使用時將它指向了非常量。所以這裡用一個例子演示一下如果指向常量的指針p是指向普通變量,會發生什麼情況。
【例14.17】使用指向常量的指針和非常量指針的例子。
#include <stdio.h> void main ( ) { int x=45 ; // 變量x 能作為左值和右值 const int y=58 ; // 常量y 不能作為左值,但可以作為右值 const int *p ; // 聲明指向常量的指針 int *p1 ; // 聲明指針 p=&y ; // 用常量初始化指向常量的指針,*p 不能作為左值 printf (\"%d\" ,*p ); p=&x ; //p 作為左值,使常量指針改為指向變量x ,*p 不能作為左值 printf (\"%d\" ,*p ); x=256 ; // 用x 作為左值間接改變*p 的值,使*p=x=256 printf (\"%d\" ,*p ); p1= (int * )&y ; // 非常量指針指向常量需要強制轉換 printf (\"%dn\" ,*p1 ); }
運行結果如下。
58 45 256 58
使用指向常量的指針指向變量時,雖然「*p」不能作為左值,但可以使用「x=」改變x的值,x改變,則也改變了*p的值,也就相當於把「*p」間接作為左值。所以說,這個const僅是限制直接使用「*p」作為左值,但可以間接使用「*p」作為左值,而「*p」仍然可以作為右值使用。
與使用非常量指針一樣,也可以使用運算符「&」改變常量指針的指向,這當然也同時改變了「*p」的值。
必須使用指向常量的指針指向常量,否則就要進行強制轉換。當然也要避免使用指向常量的指針指向非常量,以免產生操作限制,除非是有意為之。
以上結論可以從程序的運行結果中得到驗證。
3.常量指針
把const限定符放在*號的右邊,就使指針本身成為一個const指針。因為這個指針本身是常量,所以編譯器要求給它一個初始化值,即要求在聲明的同時必須初始化指針,這個值在指針的整個生存期中都不會改變。編譯器把「p」看作常量地址,所以不能作為左值(即「p=」不成立)。也就是說,不能改變p的指向,但「*p」可以作為左值。
【例14.18】使用常量指針的例子。
#include <stdio.h> void main ( ) { int x=45 ,y=55 ,*p1 ; // 變量x 和y 均能作為左值和右值 int const sum=100 ; // 常量sum 只能作為右值 int * const p=&x ; // 聲明常量指針並使用變量初始化 int * const p2= (int * )&sum ; // 使用常量初始化常量指針,需要強制轉換 printf (\"%d %d\" ,*p ,*p2 ); x=y ; // 通過左值x 間接改變*p 的值,使*p=55 printf (\"%d\" ,*p ); *p=sum ; // 直接用*p 作為左值,使*p=100 printf (\"%d\" ,*p ); *p2=*p2+sum+*p ; //*p2 作為左值,使*p2=300 printf (\"%d\" ,*p2 ); p1=p ; //p 作為左值,使指針p1 與常量指針p 的指向相同 printf (\"%dn\" ,*p1 ); }
運行結果如下。
45 100 55 100 300 100
語句「x=y;」和「*p=sum;」,都可以改變x的值,但p指向的地址不能改變。
顯然,常量指針是指這個指針p是常量,既然p是常量,當然p不能作為左值,所以定義時必須同時用變量對它初始化。對常量而言,需用使用指向常量的指針指向它,含義是這個指針指向的是不能做左值的常量。不要使用常量指針指向常量,使用常量指針就需要進行強制轉換。
4.指向常量的常量指針
也可以聲明指針和指向的對象都不能改動的「指向常量的常量指針」,這時也必須初始化指針。例如:
int x=2 ; const int* const p=&x ;
告訴編譯器,*p和p都是常量,都不能作為左值。這種指針限制了「&」和「*」運算符,所以在實際的應用中很少用到這種特殊指針。
5.void指針
一般情況下,指針的值只能賦給相同類型的指針。Void類型不能聲明變量,但可以聲明void類型的指針,而且void指針可以指向任何類型的變量。
【例14.19】演示void指針的例子。
#include <stdio.h> void main ( ) { int x=56 , y=65 ,*p=&x ; void *vp=&x ; //void 指針指向x printf (\"%d ,%d ,%dn\" ,vp ,p ,x ); vp=&y ; //void 指針改為指向y p= (int * )vp ; // 強制將void 指針賦值給整型指針 printf (\"%d ,%d ,%dn\" ,vp ,p ,*p ); }
雖然void指針指向整型變量對像x,但不能使用*vp引用整型對象的值。要引用這個值,必須強制將void指針賦值給與值相對應的整型指針類型。程序輸出如下。
1245052 ,1245052 ,56 1245048 ,1245048 ,65