てけもぐ Tech 忘備録

C言語の配列の話の続き

対象読者

C 言語初学者

解決すること

C 言語の配列を少し深堀する

内容

以前、C 言語の配列の話で、配列へのアクセスについて書きましたが、サンプルコードを追加しておきます。

2次元配列について以下4通りのアクセス。結果はどれも 3 。

#include<stdio.h>

int main(){

  int array[2][2] = {{0, 1}, {2, 3}};

  printf("access to array a[2][2] = {{0, 1}, {2, 3}}\n-----\n");
  printf("by array[1][1]          : %d\n", array[1][1]);
  printf("by *(array[1] + 1)      : %d\n", *(array[1] + 1));
  printf("by *((int *)array + 3)  : %d\n", *((int*)array + 3));
  printf("by *(*(array + 1) + 1)) : %d\n", *(*(array + 1) + 1));
}

アセンブルされたコードは以下の様になる(gcc -S -masm=intel のインテル表記)。

.LC1:
	.string	"by array[1][1]          : %d\n"
.LC2:
	.string	"by *(array[1] + 1)      : %d\n"
.LC3:
	.string	"by *((int *)array + 3)  : %d\n"
.LC4:
	.string	"by *(*(array + 1) + 1)) : %d\n"
	.text

...snip...

        sub	rsp, 32
	mov	rax, QWORD PTR fs:40
	mov	QWORD PTR -8[rbp], rax
	xor	eax, eax
	mov	DWORD PTR -32[rbp], 0
	mov	DWORD PTR -28[rbp], 1
	mov	DWORD PTR -24[rbp], 2
	mov	DWORD PTR -20[rbp], 3
	lea	rax, .LC0[rip]
	mov	rdi, rax
	call	puts@PLT
	mov	eax, DWORD PTR -20[rbp]
	mov	esi, eax
	lea	rax, .LC1[rip]
	mov	rdi, rax
	mov	eax, 0
	call	printf@PLT
	mov	eax, DWORD PTR -20[rbp]
	mov	esi, eax
	lea	rax, .LC2[rip]
	mov	rdi, rax
	mov	eax, 0
	call	printf@PLT
	lea	rax, -32[rbp]
	add	rax, 12
	mov	eax, DWORD PTR [rax]
	mov	esi, eax
	lea	rax, .LC3[rip]
	mov	rdi, rax
	mov	eax, 0
	call	printf@PLT
	lea	rax, -32[rbp]
	add	rax, 12
	mov	eax, DWORD PTR [rax]
	mov	esi, eax
	lea	rax, .LC4[rip]
	mov	rdi, rax
	mov	eax, 0
	call	printf@PLT

...snip...

最後のアクセスの仕方は2回 deref を含んでいますが、アドレスの中身を取り出したのは他のと同様に1度だけ。つまり左辺値の処理の時に、対象が配列だと型の昇格はあっても実際の deref はせずにアドレス計算だけをする。

もちろん、int *array[2] とは全く別もの。
こちらは int へのポインタを2つ分確保して、その先頭へのアドレスが array に入ってる。この場合は、
3番めの例の*((int*)array + 3)はうまく行かない。他のアクセスの仕方はきちんと2回 deref するようになる。

配列評価の時には、順に処理して array[] の場合はアドレス計算、ポインタの場合は deref、そして最後の評価の際に deref するかどうかを決めるということでしょうかね。まぁどっちにしても混同しちゃ駄目ってことで、配列は一定の大きさのメモリをスタックにとるだけ。

Tags