09.05 C語言為什麼要有“->”運算符,為何不使用點運算符代替它呢?

基本上,每一個C語言程序員都明白點運算符“.”和箭頭運算符“->”可以用於訪問結構體的成員,只不過箭頭運算符“->”需要與結構體指針結合使用。事實上按照現在流行的C語言語法,通過結構體指針

直接訪問成員,也只能通過箭頭運算符。

struct test *x;
x.member = 1; // 非法
x->member = 1; // 合法
C語言為什麼要有“->”運算符,為何不使用點運算符代替它呢?

C語言為何要有“->”運算符?

C語言為何要有“->”運算符?

拋開結構體不談,C語言中的指針本身並無需要用到點運算符“.”的地方,因此結構體指針與點運算符“.”結合時,編譯器把這種結合解釋為訪問結構體成員,按理說並不會產生歧義,C語言以語法簡潔聞名,那為什麼還要提供“多餘”的“->”運算符呢?或者說,C語言中的箭頭運算符“->”有什麼歷史淵源嗎?

上述問題其實可以簡化成兩個子問題,一是為什麼C語言要有“->”運算符,再就是為什麼C語言中的“.”運算符不能與結構體指針結合訪問成員。

C語言“->”運算符的歷史

其實,在C語言的第一個版本(相關C參考手冊(C Reference Manual,CRM)在1975年5月隨第6版Unix一起發佈)中,“->”運算符並不像今天一樣與“.”運算符同義,而是另有一種特有的含義。

C語言為什麼要有“->”運算符,為何不使用點運算符代替它呢?

CRM 所描述的C語言在許多方面都與現代C語言有很大的不同

CRM 所描述的C語言在許多方面都與現代C語言有很大的不同,例如 CRM 的結構體成員實現了全局字節偏移的概念,沒有類型限制,可以訪問任意地址。也就是說,當時的C語言中,所有的結構體成員的名字都具有獨立的全局含義,因此所有結構體的成員名都不能一樣。

struct S {
int a;
int b;
};

上面這幾行C語言代碼定義了結構體 S,成員 a 代表 0 偏移,而成員 b 則代表 2 字節偏移(這裡假設 int 變量佔用 2 字節內存,也不考慮內存對齊)。

當時C語言做了這樣的限制:所有結構體的所有成員,要麼有唯一的名字,要麼代表唯一的字節偏移量,例如:

struct X {
int a;
int x;
};

上述代碼定義了結構體 X,它也包含成員 a,它的名字與結構體 S 中的成員 a 重複了,但是沒有問題,因為它們都代表 0 偏移。下面這種定義就屬於非法了:

struct Y {
int b;
int a;
};

因為結構體 Y 中的成員 a 與結構體 S 中的成員 a 重名,並且代表的字節偏移量也不相等。

C語言為什麼要有“->”運算符,為何不使用點運算符代替它呢?

早期箭頭運算符“->”是用於確定偏移量的

在當時的C語言語法中,箭頭運算符“->”就是用於確定偏移量的。既然每個結構體的成員代表的字節偏移量都是全局的,那麼下面這樣的語句也是合法的:

int i = 5;
i->b = 42;
100->a = 0;

上述幾行C語言代碼的意義很明確:i->b 表示以 5 為基準的 2 字節偏移處,因此 i->b=42; 的意思是將地址 7 處的 int 值設置為 42。同樣的道理,100->a=0; 則表示將地址 100 處的 int 值設置為 0。

讀者應注意,在當時版本的C語言中,箭頭運算符“->”並不關心它的左表達式,因此哪怕 100->a 也是合法的。

C語言為什麼要有“->”運算符,為何不使用點運算符代替它呢?

箭頭運算符“->”並不關心它的左表達式

這樣利用結構體成員偏移量的做法對於“* ”和“.”運算符的組合是不可用的,例如

int i = 5;
(*i).b = 42;

*i 本身就是一個無效的表達式,“* ”是一個獨立的運算符,因此對其操作數施加了更加嚴格的類型要求。當時 CRM 引入箭頭運算符“->”就是用於解決這種限制帶來的不便的。

後來,在 K&R 設計的C語言中,許多 CRM 中的功能被重新設計,“結構體成員作為全局偏移標識符”的設計被完全推翻,此後箭頭運算符“->”的功能與“* ”和“.”運算符結合的功能完全相同。

為什麼C語言不支持“.”運算符與結構體指針結合訪問成員?

同樣,在 CRM 描述的C語言中,“.”運算符的左操作數被要求必須是一個左值,這也是它與“->”運算符不同的原因,如上所述。請注意,CRM 不需要“.”運算符的左操作數是結構體類型的,它只要求左操作數是左值。

這裡讀者應該區分“左操作數”和“左值”。
C語言為什麼要有“->”運算符,為何不使用點運算符代替它呢?

應該區分“左操作數”和“左值”

這意味著在 CRM 版本的C語言中,程序員可以編寫下面這樣的代碼:

struct S { int a, b; };
struct T { float x, y, z; };
struct T c;
c.b = 55;

讀者應該注意到結構體 T 並沒有成員 b,但是 c.b=55; 卻仍然是合法的,這是因為編譯器不關心變量 c 的類型,它只關心 c 是否一個左值:某種可寫的內存塊。因此 c.b=55; 的意義是將 55 寫入名為 c 的連續內存塊中字節偏移量 2 處的 int 值中。

因此,如果我們寫了下面這樣的C語言代碼:

S *s;
...
s.b = 42;

編譯器將認為這樣是有效的,因為 s 也是一個左值。最終得到的C語言程序將嘗試將 42 寫到指針變量 s 本身(而不是它指向的結構體)所在連續內存字節偏移量 2 處。不用說,這樣的結果必定會產生預料之外的結果,很可能帶來內存溢出,但是編程語言本身並不關心這些事情。

C語言為什麼要有“->”運算符,為何不使用點運算符代替它呢?

編程語言本身並不關心這些事情

也就是說,在那個版本的C語言中,對“.”運算符重載(使其支持通過結構體指針訪問成員)根本就行不通,因為“.”運算符與指針結合時,已經具備自己的含義了(與左值結合,訪問指定偏移量的內存)。雖然以今天的眼光來看,這樣的設計很古怪,但是當時的確就是這樣設計的。

當然了,這樣的奇怪設計並不是“.”運算符不能與結構體指針結合使用訪問成員的充足理由,但是後來 K&R 在重新設計C語言時沒有考慮重載“.”運算符,應該是需要兼容之前版本的C語言,畢竟歷史遺留下來的C語言代碼也是需要得到支持的。

最後

可能也有讀者認為,即使是今天的C語言,似乎“->”運算符也不是必須的,因為“* ”和“.”運算符結合就能輕易的代替它:

struct S *p;
p->b = 3;
// 完全可以使用下面這樣的語句替換
(*p).b = 3;

既然簡潔是C語言的特點,就應該做到極致,何必提供“多餘的”箭頭運算符“->”呢?的確如此,就功能性而言,“->”完全可以不要,但是在C語言程序開發中,我們還需要考慮程序員的感受,請看下面這兩種寫法:

(*(*(*a).b).c).d
a->b->c->d

它們的功能是一致的,但是第二種寫法無論是書寫,還是閱讀,都要簡潔的多。

C語言為什麼要有“->”運算符,為何不使用點運算符代替它呢?

歡迎在評論區一起討論,質疑。文章都是手打原創,每天最淺顯的介紹C語言、linux等嵌入式開發,喜歡我的文章就關注一波吧,可以看到最新更新和之前的文章哦。


分享到:


相關文章: