C/C++左值性精髓
(三)左值轉換
2. 從數組到指標的轉換
數組和指標這兩種實體,是最令初學者感到痛苦和糾結的一對“冤家”。對兩者內涵及聯絡的不斷挖掘的過程,就相當於一次思維風暴。只有徹底理解對象、類型派生方式、左值性和常量等幾種低層語言設施,才能獲得對數組和指標的完整認識。那麼,數組與指標之間千絲萬縷的聯絡究竟是什麼原因產生的呢?根本原因就在於下面要談到的從數組到指標的轉換條款。
C和C++的數組到指標轉換條款涵義大體相同,但C90和C99有些差別。C90規定:
Except when it is the operand of the sizeof operator or the unary & operator, or is a character string literal used to initialize an array of character type. or is a wide string literal used to initialize an array with element type compatible
with wchar-t, an lvalue that has type “array of type” is converted to an expression that has type “pointer to type” that points to the initial element of the array object and is not an lvalue.
除了作為sizeof、&及用於初始化字元數組的字串字面量等幾種情況外,一個具有數群組類型的左值運算式被轉換為指向數組首元素的右值指標。這是一個隱式轉換過程。這個條款不僅規定了首元素地址這個數值結果,還規定了轉換結果的類型:元素指標。例如:
int a[10];
int *p = a;
上式中的a先從數群組類型int[10]隱式轉換為int*指標,所代表的值為a[0]的地址,然後用這個int*類型的地址初始化p。
正由於數組到指標轉換條款的存在,運算式中的數組名(除幾種情況外)與指標具有結果等效性。請看樣本:
char a[10];
char *p = a;
char *q;
q = a + 1;
q = p + 1;
a + 1與p + 1是等效的。要注意的是,這種等效性是體現在運算式計算中的,數組到指標的轉換條款表述的僅僅是數組在運算式中的行為,而非本質,轉換的目的是將數群組類型的運算式數值化,使它們能夠參與運算式計算,從而極大地豐富運算式的內容。
可惜的是,由於對此條款認識不足或者根本不瞭解有此條款,關於數組的本質產生了種種誤解。最典型的一種誤解是:數組名是一個指標常量,它屬於右值。這種誤解源於三類錯誤:
1. 將數組與指標的等效關係理解成等價關係。等價是相同事物的不同表現形式,而等效是不同事物的相同效果。數組與指標是互不相同的兩種實體,它們在運算式中的行為體現的是等效而非等價,僅從某一方面相似的表面文法就將兩者的本質簡單等同是錯誤的,數組名不是指標,數組名僅僅是可以轉換為指標而已。
2. 將數學中變數和常量概念的慣性思維生硬套到C/C++上,以為不變或者不可變的量就是常量。實際上,C/C++關於變數和常量的概念與數學有很大差別,不變的量不一定是常量,可變的量也不一定是變數。C/C++的變數涵義是一個有名對象,由對象的聲明產生,對象的名字就是變數名。數組名作為數組對象的名字其實是符合C/C++關於變數的定義的,因此數組名其實是一個變數,但轉換的結果是一個符號地址。
3. 錯誤地將賦值運算式的行為作為左值定義。前面在“左值的前世今生”一節中已經討論過,標準C/C++的左值定義是基於物件模型的,在判斷一個運算式是否左值時並不以賦值運算式中的行為為依據。對於資料抽象,C/C++關於左值的定義是具有物件類型或非void不完整類型的運算式,數組名作為具有數群組類型的運算式,符合左值的定義。
因此,數組名不是指標常量,但在運算式中及一定條件下,它可以隱式轉換為右值指標,轉換的結果不一定是常量,要視情況而定。數組名屬於左值,不是右值,而且是一個不可修改的左值,因為數群組類型屬於聚集類型,不是標量類型,數組對象的內容無法視作一個數值。
在本節第二段的條款內容中,提到了三種不進行轉換的例外情形,請看例子:
int a[ 10 ];
char *p = “abcdefg”; //A
char b[] = “abcdefg”; //B
size_t size = sizeof( a ); //C
int ( *q )[ 10 ] = &a; //D
int *k = a; //E
由於C/C++將字串字面量實現為字元數組,因此字串字面量的類型實際上是數群組類型,運算式中的字串字面量也可以轉換為指向其首元素的右值指標,語句A正反映了這種轉換,p被“abcdefg”的首元素地址初始化;語句B中的“abcdefg”作為字元數組b的初始化器,這是條款所規定的例外情形,此時“abcdefg”不轉換為指標,B相當於如下初始化形式:
char b[] = { ‘a’,’b’,’c’,’d’,’e’,’f’,’g’,’\0’};
對於C和D,sizeof及&的運算元a也不進行轉換,所以sizeof( a )的結果是整個數組的大小,&a是數組的首地址,其地址值與E中的a的轉換結果一樣,但兩者的類型是不一樣的,&a作為數組首地址,類型是指向數組的指標:int( * )[10],而a的轉換結果是指向首元素的指標,因此類型是int*。
C90的條款限定了只對左值數組進行轉換,但事實上,也存在右值數組,右值數組並不是內因的,而是受到了外界的影響使數組呈現出右值性,例如作為右值對象的一部份。C99和C++的轉換條款皆允許左值和右值數組的轉換,而C90禁止右值數群組轉換,請看筆者從自己的blog中節選出來的一段代碼:
struct Test
{
int a[10];
};
struct Test fun( struct Test* );
int main( void )
{
struct Test T;
int *p = fun( &T ).a; //A
int (*q)[10] = &fun( &T ).a; //B
printf( "%d", sizeof( fun( &T ).a ) ); //C
return 0;
}
struct Test fun( struct Test *T )
{
return *T;
}
在這個例子裡,fun( &T )返回一個Test類型的右值對象,fun( &T ).a就是一個右值數組,是一個右值運算式。在C89/90中,由於規定左值數組才能進行數組到指標的轉換,因此A中的fun( &T ).a不進行數組到指標的轉換,A語句在C90中是非法的,但C99和C++不再區分數組的左右值性,因此A在C99和C++中都是合法的;語句C中的fun( &T ).a是sizeof運算子的運算元,這種情況下fun( &T ).a並不進行數組到指標的轉換,因此C在所有C/C++標準中都是合法的;B語句中的a作為&運算子的運算元屬於轉換的例外情況,雖然不進行轉換,但B仍然是非法的!為什嗎?其實B違反了另一條規定,對於資料抽象,&的運算元要求是左值,而fun(
&T ).a是右值。