C語言const是如何保證變數不被修改的?

這小段文章要理清楚的是,在C語言中,$const$是如何保證變數不被修改的?

我們可以想到兩種方式:

第一種,由編譯器來阻止修改$const$變數的語句,讓這種程式不能通過編譯;

第二種,由作業系統來阻止,即把$const$ 的記憶體地址訪問許可權標記為「只讀」,一旦運行中的程式試圖修改它,就會產生異常,終止進程。

上面想到的這兩種方式,都能達到讓某一變數的值不被修改的目的,那麼究竟是哪一種呢?我們寫兩個例子來看一看。

先來看一個簡單的例子,源文件const.c:

#include <stdio.h>
const int a=10;
int main()
{
	int *p=&a;
	printf("initial: %d\n",a);
	*p=1;
	printf("modified: %d\n",a);
	return 0;
}

編譯,會收到一個 warning:

$ gcc -o const1 const1.c
const.c: In function 『main』:
const.c:7:12: warning: initialization discards 『const』 qualifier from pointer target type [-Wdiscarded-qualifiers]
     int *p=&a;

忽略之,運行程式:

$ ./const1
initial: 10
Segmentation fault (core dumped)

運行出錯了,報錯是「segmentation fault」,即「段錯誤」,它是在提醒我們,程式中用錯誤的許可權訪問了記憶體某區域。這說明,作業系統把變數$a$載入到了一段只讀記憶體區域之中,因此對該區域地址的寫操作將引發異常,這是由作業系統的記憶體保護機制決定的。

也就是說,在這段程式里,$const$的只讀屬性是由作業系統來實現的,而不是由編譯器來實現的(編譯器只拋出了warning,並沒有阻止編譯通過)。

這對嗎?不完全對,我們來看另一個例子,源文件const2.c:

#include <stdio.h>
int main()
{
	const int a=10;
	int *p=&a;
	printf("initial: %d\n",a);
	*p=1;
	printf("modified: %d\n",a);
	return 0;
}

編譯,還是收到同樣的warning:

$ gcc -o const2 const2.c
const.c: In function 『main』:
const.c:6:12: warning: initialization discards 『const』 qualifier from pointer target type [-Wdiscarded-qualifiers]
     int *p=&a;

忽略之,運行程式:

./const2
initial: 10
modified: 1

咦?怎麼成功運行了,而且$a$的值還被順利修改了?

結合以上兩個例子,我們可以得出以下結論:

$const$只是C語言中的一種對變數的修飾符,例子中的$a$,與其說是「常量」,不如說是「不打算修改的變數」。它只是語法上的一種聲明,它的作用就是告訴編譯器「我不想修改它」,因此編譯器會從語法上檢查程式中是否有修改它的語句(例如「a=1;」),一旦發現這種「違背初衷」的語句,就會報錯阻止你。

然而,編譯器所阻止的僅僅是對$a$這個符號對應值的修改而已,卻並不阻止對這個地址的值的修改,源文件「const2.c」之所以能順利通過編譯且正常運行,就是因為它利用一個名字不叫$a$的指針指向它,從而繞過了編譯器的語法檢查。

打個比方,周樹人的筆名叫魯迅,警察只知道要抓魯迅,這時候他就可以用一句「你們抓魯迅跟我周樹人有什麼關係?」來騙過他們。

從這個角度來說,$const$的作用是靠編譯器僅僅從語法檢查來實現的,因此存在運行時的漏洞。

那麼為什麼「const1.c」就不能正常運行呢?

仔細看這兩個源程式,區別僅僅在於,在const1.c中,$a$被聲明為全局變數,而在const2.c中,它被聲明為main函數中的一個局部變數。全局變數與局部變數的區別在於,前者會在程式開始運行之前就被載入,載入後會一直留在記憶體中,且載入的位置在數據區,直到程式退出;後者只有在運行到它時才會被載入,且載入的位置是運行時的棧幀,一旦超出作用於就會被回收。

因此,編譯器會對被聲明為全局變數的$const\ int\ a$進行優化,把它放到只讀記憶體區內,這一記憶體區的許可權是$read\ only$,許可權資訊由作業系統所維護的段表來保存,程式每訪問某地址時,作業系統都會檢測其訪問許可權是否合法。const2.c中企圖用「寫」的方式來訪問「只讀」的段,自然會報出「segment fault”的錯了。

從這個角度來說,當$a$是全局變數時,編譯器把原本只是「不打算修改的變數」優化成了「真正的常量」,然後交給作業系統去維持其不變屬性。

綜上所述,C的初衷只是讓編譯器去保證$const$的不變屬性,這一屬性有漏洞(可以用指針去騙過編譯器修改它),所以當const修飾的對象是全局變數時(全局變數很重要,因為很多源文件都要訪問它,牽一髮而動全身,所以不應輕易更改),編譯器知道自己的能力有限,只能管得了編譯,管不了運行時如何,所以優化了語句把它編程真正的常量,讓作業系統的記憶體保護功能來履行這一職責。

這一優化,並不是C規定的,而是編譯器廠商出於實際應用的考慮作出的選擇。

歡迎交流。