2020年7月4日

[C++] Difference between Pointer and Array

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 的值印出來會更清楚這個 "跑錯地方取值" 的流程。

沒有留言:

張貼留言