C 的兩個型別:array & pointer 大概是觀念上最常被誤解誤用的型別了吧,雖然很多書上、文章裡也很常看到強調這兩個型別不同,不過因為 compiler 在某些情況下會把 array 退化成 pointer,也因此初學者分不清這兩個好像也滿正常的,畢竟差異的細微之處就算有一定經驗也不一定能說得很清楚。
這次剛好有人問了一個問題就是犯了 array 跟 pointer 型別混用、再加上用了 extern,連鎖效應導致遇上 segmentation fault。後來想想搭配圖跟轉出 assembly 或許比較容易了解那細微的不同之處吧。
先看出事情的例子吧
// main.cpp
#include <cstdio>
extern int* data;
// extern int data[2];
int main()
{
printf("%d\n", data[1]);
return 0;
}
// other.cpp
int data[2];
差異之處就在標顏色的兩行:在 other.cpp 中 data 是一個有兩個元素的陣列、但在 main.cpp 中卻說他是一個指標,正確的 extern 寫法應該要是下面註解掉的那行。這個差異其實編成 assembly 就很明顯了:
.file "main.cpp" .text .section .rodata .LC0: .string "%d\n" .text .globl main .type main, @function main: .LFB0: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 <<<<<<<<<<<<<<<< array type <<<<<<<<<<<<<<<< movl 4+data(%rip), %eax //
moves a quadword (64-bits) from source to destination. ================ pointer type ================ movq data(%rip), %rax //
moves a long (32-bits) from source to destination. addq $4, %rax movl (%rax), %eax >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> movl %eax, %esi leaq .LC0(%rip), %rdi movl $0, %eax call printf@PLT movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu 9.3.0-10ubuntu2) 9.3.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4:
標顏色的地方就是不同之處,其實也就是 array 跟 pointer 怎麼拿資料的差異:
- array:直接用 array 所在的位置 + offset (這例子是 4 bytes) 後拿資料 (藍色 movl 那行)
- pointer:先從指標所指位置讀出來 (第一行 movq),然後加上 offset (第二行 addq),再把該位置的值讀出來 (第三行 movl)
問題就出在 pointer 會比 array 多做一步:pointer 會先去所在位置拿資料真正的 address,然而當型別是 array 時不需要這一步。多做這一步就導致去錯的 address 拿資料,結果就 segmentation fault 啦。
詳細的過程可以用 gdb 單步執行 assembly 把 register 的值印出來會更清楚這個 "跑錯地方取值" 的流程。
沒有留言:
張貼留言