C++ 中 const 的實現原理是什麼?

C++ 中 const 的實現原理是什麼?其存儲形式是否與non-const相同?其只讀屬性只是compiler檢查並加以保護,還是與non-const有其他本質區別?如果只是compiler檢查並保護其不被寫,那麼為什麼在用const_cast去除掉constness以後,對原const變數寫操作會是未定義行為?非常感謝各位大牛答疑!


據我所知VC++所有標記上const的指針/引用/變數是不會有什麼運行時保護的,只有編譯的時候做檢查而已。至於const char* x="abcde";為什麼VC++有保護,那是因為"abcde"被編譯在了一個readonly的memory page上面,跟x聲明成const char*是沒關係的。

而且有些const變數在聲明時候的值會被「inline」在各個使用他的地方。這個時候如果你const_cast了之後強行修改,編譯器是不會管你的,他高興inline就inline,C++標準沒有對此作出任何限制,大概可以算undefined behavior了吧。


由編譯器檢查。在PC上,const變數在RAM里,但是像

const char *a="hello world";

這樣聲明的變數,系統會載入到內存並把頁面屬性設成只讀,const_cast後改寫程序會崩潰(如果實在想改可以用系統API把頁面屬性改成讀寫)。如果是const傳遞的C++對象,const_cast後改寫沒什麼問題。

在某些設備上,const修飾的變數可能會存在ROM/Flash里,這些存儲介質不能通過內存寫操作這屆改寫,因此萬一存在只讀介質上,const_cast以後強制寫會發生未定義的行為,一般是崩潰掉。


基本就是樓上那樣,不過const在不再RAM這倒不一定,從這裡也可以看出這是未定義行為,很危險。

#include &

const int N = 1000;
int main(){
printf("%d
", N);
*const_cast&(N) = 102;
printf("%d
", N);
printf("%d
", *const_cast&(N));
}

生成的彙編代碼是

[yangff@Yangff OI]$ cat const_test.s
.file "const_test.cpp"
.section .rodata
.LC0:
.string "%d
"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $1000, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
movl $_ZL1N, %eax
movl $102, (%rax)
movl $1000, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
movl $_ZL1N, %eax
movl (%rax), %eax
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.section .rodata
.align 4
.type _ZL1N, @object
.size _ZL1N, 4
_ZL1N:
.long 1000
.ident "GCC: (GNU) 4.8.2 20140206 (prerelease)"
.section .note.GNU-stack,"",@progbits


不是大牛,拋磚引玉一下。

const只是在編譯期的保護,編譯器會檢查const變數有沒有被修改,如果有代碼嘗試修改一個const變數,編譯器會報錯。但是你總是可以通過直接訪問地址的形式去修改它的值。取決於編譯器的不同,它有可能被放在某種被操作系統保護的區域內,嘗試寫入會導致程序崩潰之類的錯誤。

至於在內存中是怎麼存放的,就只能等大牛來解答了。


C++中的const有雙重含義

1 用在編譯時就可以計算出的常量表達式初始化的const變數,編譯的時候,編譯器會用常量表達式替換這些const變數。典型的如數組長度等地方。這種const變數可以在頭文件中定義,每一個包含了頭文件的源文件都有自己file local的東西,然後編譯的時候被替換。

2 在某一作用於里不希望被改變的量。比如作為函數參數傳入等。

在C++11中,第一個作用已經被constexpr取代。


有下面一段測試程序:(測試環境gcc4.8.4, ubuntu14.04)

#include&

const int m = 0x12345678;
const int n = 0x11223344;

int main(){
int a = 1;
int b = 2;
const int c = 3;
int *pa = a;
printf("a:%x b:%x c:%x
", a, b, c);
printf("*(pa+2):%d
", *(pa+2));
*(pa+1) = 12;
*(pa+2) = 13;
printf("*(pa+2):%d
", *(pa+2));
printf("c:%x pa+2:%x
", c, pa+2);
printf("a:%d b:%d c:%d
", a,b,c);
int k1 = c;
printf("k1:%d
", k1);
int k2 = b;
printf("k2:%d
", k2);
}

輸出如下:

a:a2918c44 b:a2918c48 c:a2918c4c
*(pa+2):3
*(pa+2):13
c:a2918c4c pa+2:a2918c4c
a:1 b:12 c:3
k1:3
k2:12

可以看出pa+2的值和c的地址是完全一樣的,通過*(pa+1)=12的賦值也確實改變了b的值,但是*(pa+2)=13的賦值卻沒能改變c的值。 反彙編後:

g++ -g const.cpp -o const
objdump -S const

int main(){
40052d: 55 push %rbp
40052e: 48 89 e5 mov %rsp,%rbp
400531: 48 83 ec 20 sub $0x20,%rsp
int a = 1;
400535: c7 45 e4 01 00 00 00 movl $0x1,-0x1c(%rbp)
int b = 2;
40053c: c7 45 e8 02 00 00 00 movl $0x2,-0x18(%rbp)
const int c = 3;
400543: c7 45 ec 03 00 00 00 movl $0x3,-0x14(%rbp)
int *pa = a;
40054a: 48 8d 45 e4 lea -0x1c(%rbp),%rax
40054e: 48 89 45 f8 mov %rax,-0x8(%rbp)
printf("a:%x b:%x c:%x
", a, b, c);
400552: 48 8d 4d ec lea -0x14(%rbp),%rcx
400556: 48 8d 55 e8 lea -0x18(%rbp),%rdx
40055a: 48 8d 45 e4 lea -0x1c(%rbp),%rax
40055e: 48 89 c6 mov %rax,%rsi
400561: bf c4 06 40 00 mov $0x4006c4,%edi
400566: b8 00 00 00 00 mov $0x0,%eax
40056b: e8 a0 fe ff ff callq 400410 &


printf("*(pa+2):%d
", *(pa+2));
400570: 48 8b 45 f8 mov -0x8(%rbp),%rax
400574: 48 83 c0 08 add $0x8,%rax
400578: 8b 00 mov (%rax),%eax
40057a: 89 c6 mov %eax,%esi
40057c: bf d7 06 40 00 mov $0x4006d7,%edi
400581: b8 00 00 00 00 mov $0x0,%eax
400586: e8 85 fe ff ff callq 400410 &


*(pa+1) = 12;
40058b: 48 8b 45 f8 mov -0x8(%rbp),%rax
40058f: 48 83 c0 04 add $0x4,%rax
400593: c7 00 0c 00 00 00 movl $0xc,(%rax)
*(pa+2) = 13;
400599: 48 8b 45 f8 mov -0x8(%rbp),%rax
40059d: 48 83 c0 08 add $0x8,%rax
4005a1: c7 00 0d 00 00 00 movl $0xd,(%rax)
printf("*(pa+2):%d
", *(pa+2));
4005a7: 48 8b 45 f8 mov -0x8(%rbp),%rax
4005ab: 48 83 c0 08 add $0x8,%rax
4005af: 8b 00 mov (%rax),%eax
4005b1: 89 c6 mov %eax,%esi
4005b3: bf d7 06 40 00 mov $0x4006d7,%edi
4005b8: b8 00 00 00 00 mov $0x0,%eax
4005bd: e8 4e fe ff ff callq 400410 &


printf("c:%x pa+2:%x
", c, pa+2);
4005c2: 48 8b 45 f8 mov -0x8(%rbp),%rax
4005c6: 48 8d 50 08 lea 0x8(%rax),%rdx
4005ca: 48 8d 45 ec lea -0x14(%rbp),%rax
4005ce: 48 89 c6 mov %rax,%rsi
4005d1: bf e3 06 40 00 mov $0x4006e3,%edi
4005d6: b8 00 00 00 00 mov $0x0,%eax
4005db: e8 30 fe ff ff callq 400410 &


printf("a:%d b:%d c:%d
", a,b,c);
4005e0: 8b 55 e8 mov -0x18(%rbp),%edx
4005e3: 8b 45 e4 mov -0x1c(%rbp),%eax
4005e6: b9 03 00 00 00 mov $0x3,%ecx ;經過多次測試,這裡被移入ecx的永遠是c的初值
4005eb: 89 c6 mov %eax,%esi
4005ed: bf f2 06 40 00 mov $0x4006f2,%edi
4005f2: b8 00 00 00 00 mov $0x0,%eax
4005f7: e8 14 fe ff ff callq 400410 &


int k1 = c;
4005fc: c7 45 f0 03 00 00 00 movl $0x3,-0x10(%rbp) ;這裡直接將c的初值賦值給了k1
printf("k1:%d
", k1);
400603: 8b 45 f0 mov -0x10(%rbp),%eax
400606: 89 c6 mov %eax,%esi
400608: bf 02 07 40 00 mov $0x400702,%edi
40060d: b8 00 00 00 00 mov $0x0,%eax
400612: e8 f9 fd ff ff callq 400410 &


int k2 = b;
400617: 8b 45 e8 mov -0x18(%rbp),%eax ;這裡卻將b的當前值賦值給了eax
40061a: 89 45 f4 mov %eax,-0xc(%rbp)
printf("k2:%d
", k2);
40061d: 8b 45 f4 mov -0xc(%rbp),%eax
400620: 89 c6 mov %eax,%esi
400622: bf 09 07 40 00 mov $0x400709,%edi
400627: b8 00 00 00 00 mov $0x0,%eax
40062c: e8 df fd ff ff callq 400410 &


}

*(pa+1)能訪問到b,*(pa+2)能訪問到c,從*(pa+1) = 12; *(pa+2) = 13;兩句的彙編代碼看,兩者並無區別,但是從printf("a:%d b:%d c:%d
", a,b,c);和int k1=c;int k2=b;看對b,c的操作有明顯區別,在輸出c的時候,編譯器永遠會把c的初值賦值給ecx,可能是為了後面的printf,k1=c的賦值也永遠是把c的初值賦值給了k1,但是k2=b確是重新將b的值賦值給k2.個人猜想gcc在處理const作用的局部變數的時候,雖然該變數有地址,但是使用該變數的時候確是永遠使用該變數的初始化值,而不會重新取值。對於全局變數中const作用的變數,gcc會將該變數寫入只讀數據段,如果像上述一樣去嘗試修改m,n的值會出現Segmentation fault。

Disassembly of section .rodata:

00000000004006c0 &<_IO_stdin_used&>:
4006c0: 01 00 add %eax,(%rax)
4006c2: 02 00 add (%rax),%al
4006c4: 26 es
4006c5: 61 (bad)
4006c6: 3a 25 78 20 26 62 cmp 0x62262078(%rip),%ah # 62662744 &<_end+0x620616fc&>
4006cc: 3a 25 78 20 26 63 cmp 0x63262078(%rip),%ah # 6366274a &<_end+0x63061702&>
4006d2: 3a 25 78 0a 00 2a cmp 0x2a000a78(%rip),%ah # 2a401150 &<_end+0x29e00108&>
4006d8: 28 70 61 sub %dh,0x61(%rax)
4006db: 2b 32 sub (%rdx),%esi
4006dd: 29 3a sub %edi,(%rdx)
4006df: 25 64 0a 00 26 and $0x26000a64,%eax
4006e4: 63 3a movslq (%rdx),%edi
4006e6: 25 78 20 70 61 and $0x61702078,%eax
4006eb: 2b 32 sub (%rdx),%esi
4006ed: 3a 25 78 0a 00 61 cmp 0x61000a78(%rip),%ah # 6140116b &<_end+0x60e00123&>
4006f3: 3a 25 64 20 62 3a cmp 0x3a622064(%rip),%ah # 3aa2275d &<_end+0x3a421715&>
4006f9: 25 64 20 63 3a and $0x3a632064,%eax
4006fe: 25 64 0a 00 6b and $0x6b000a64,%eax
400703: 31 3a xor %edi,(%rdx)
400705: 25 64 0a 00 6b and $0x6b000a64,%eax
40070a: 32 3a xor (%rdx),%bh
40070c: 25 64 0a 00 78 and $0x78000a64,%eax

0000000000400710 &<_ZL1m&>: ;m的值
400710: 78 56 js 400768 &<_ZL1n+0x54&>
400712: 34 12 xor $0x12,%al

0000000000400714 &<_ZL1n&>:
400714: 44 33 22 xor (%rdx),%r12d ;n的值
400717: 11 .byte 0x11

以上對原因的分析只是個人猜想,上面的彙編碼自己並不能完全看懂,如果有錯誤還望各位大V不吝指出。


其實於C++中的常量摺疊這一問題有關,在編譯階段對該常量的引用會被替換,同時也會為該常量分配內存空間,你想複雜了。


那麼為什麼在用const_cast去除掉constness以後,對原const變數寫操作會是未定義行為

因為 C++ 只規定了編譯器的行為:C++ 要求編譯器拒絕所有直接修改 const 對象的情況。

但是你在經過 const_cast 之後會碰到什麼與 C++ 無關,是編譯器和具體 OS 的事情。是不是有 .text 只讀區,是不是會設置內存區間只讀,還是全部放開可寫,都跟 C++ 沒有關係。

C++ 是一個假裝有運行時和類型強保證的語言,所以……意思意思就好。


推薦閱讀:

庫代碼中是否應該檢查malloc的返回值?
python如何畫出這樣漂亮的地圖呢?
C++中編寫強異常安全的代碼真的有必要麼?
能不能對QVariant進行引用/指針讀寫數據?
eclipse為什麼不能做的好用一點?好看一點?

TAG:編程 | C編程語言 | C |