2
1

ポインタをインクリメント演算子で操作する違和感

Last updated at Posted at 2023-12-20

 ひとつまみの疑問が生まれたので。

既知の事実

#include<stdio.h>
#include<string.h>

int main(void) {

	char str[] = "abcde";
	char* c = str;

    for (int i = 0 ; i < strlen(str); i++) {
		printf("str[%d]:%c\n", i, *c++);
	}

	return 0;
}
出力結果
str[0]:a
str[1]:b
str[2]:c
str[3]:d
str[4]:e

 文字列(char型の配列)内の文字を1文字ずつ読み出すことができています。これは、ポインタの指す番地に1を加算することで実現しています(文字列を表示するためだけにポインタの参照先を移動させているので、個人的には*c++よりは*(c+i)とかの方が好きですけどね)。

 文字列は、実際に以下のように連続したメモリに格納されています。これは、C言語における配列が「同じデータ型をもつ、連続に格納された変数」を表しているからです。
image.png
 メモリ番地は適当です。
 上図を見るとわかるように、メモリ番地に1を足し加えることで、次のメモリ番地に移動しています(実際にはメモリアドレスが1加算されただけなのですが、結果的に「移動」したように見えます)。配列であれば連続的に値が格納されているので、次のメモリ番地には次の文字が格納されているというわけです。

 と、このように、配列とポインタには切っても切れない関係があります。もとより配列の要素数を[]で示すのはシンタックスシュガーみたいなものです。ちょっと誤解を生む発言ですが、そんな雰囲気のやつ、と思ってもらえれば大丈夫です1

 そういえば、char型って1byteの変数ですよね。だから先ほどの動作が実現できていました。ところで、int型配列ではどうでしょう?
image.png
 int型は4byte変数なので、変数1つあたりに使用するメモリサイズがchar型の4倍になります。これ、先ほどと同様にポインタをインクリメントしても、正しく値が取得できそうになくないですか?なんかint型を4分割した変な値が取得できそうじゃないですか?

解決

 というわけでやってみました。

#include<stdio.h>

int num[2] = {2, 1};
int* count = num;

int func() {
	printf("count:%d\n", *count);
	count++;
	printf("count:%d\n", *count);
}

int main(void) {

	func();
	return 0;
}
出力結果
count:2
count:1

 あれ、ちゃんとカウントできてますね。まぁ、そりゃ当然といえば当然か。これまでのC言語人生で同じようなコードは書きましたが、一切違和感なく使用できてますもんね。
 うーん、でも、やっぱりこれはおかしい気がします。だってint型は4byteですよ?こんなの4加算しないと実現できないですよ。

func:
.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movq	count(%rip), %rax
	movl	(%rax), %eax
	movl	%eax, %esi
	leaq	.LC0(%rip), %rdi
	movl	$0, %eax
	call	printf@PLT
	movq	count(%rip), %rax
	addq	$4, %rax            ; ここで4加算
	movq	%rax, count(%rip)
	movq	count(%rip), %rax
	movl	(%rax), %eax
	movl	%eax, %esi
	leaq	.LC0(%rip), %rdi
	movl	$0, %eax
	call	printf@PLT
	nop
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	func, .-func
	.globl	main
	.type	main, @function

 ちゃんとしてますね。+4。なんで?インクリメントだぞ?? 出力したアセンブリはgccデフォルトのAT&T構文のもので、該当場所にコメントを挿入しています。見事に4加算してますね。まじか。
 ポインタ型変数は、それがどんな型の変数を指すかに関わらず、そのサイズは8byte固定です(コンピュータが64bitである場合。32bitであれば半分の4byteになります)。つまり、メモリに格納されているポインタ型のデータから、元々そのポインタ型がint*なのかdouble*なのか判断できるはずがありません。よって、やはりコンパイラがアセンブリに翻訳するときに加算数を判断するのが妥当なんですかね。

 ...ん?ということはつまり、int*じゃなくてchar*でポインタを定義すれば...

#include<stdio.h>

int num[2] = {2, 1};
char* count = num;    // int* → char* に変更

int func() {
	printf("count:%d\n", *count);
	count++;
	printf("count:%d\n", *count);
}

int main(void) {

	func();
	return 0;
}
出力結果
count:2
count:0

 やった!上手くいきました!ポインタはインクリメントでおそらく+1をしています!2
 やっぱり加算する数は1固定じゃなくて、コンパイラが判断してたんですね!

func:
.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movq	count(%rip), %rax
	movzbl	(%rax), %eax
	movsbl	%al, %eax
	movl	%eax, %esi
	leaq	.LC0(%rip), %rdi
	movl	$0, %eax
	call	printf@PLT
	movq	count(%rip), %rax
	addq	$1, %rax           ; ここで1加算
	movq	%rax, count(%rip)
	movq	count(%rip), %rax
	movzbl	(%rax), %eax
	movsbl	%al, %eax
	movl	%eax, %esi
	leaq	.LC0(%rip), %rdi
	movl	$0, %eax
	call	printf@PLT
	nop
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	func, .-func
	.globl	main
	.type	main, @function

 ほんとに+1してますね。えぇ...?

まとめ

 ポインタのインクリメントは絶対に+1加算するってわけじゃないんですね。まぁ確かにその方がソースコードは直感的ですけど。なんかインクリメント演算子に対するイメージが壊れて悲しいです。裏切られちゃった。世の中そういうことだらけだなぁ。

  1. 配列とポインタは意味的には別物です。あくまでs[i]のような記法を*(s+i)と解釈するために、ポインタが配列のように記述できるようになっているだけです。配列がポインタのシンタックスシュガーであるわけではありません(実体が同じだとしても、です)。

  2. 出力がこの形になっているのは、リトルエンディアン方式で格納されていたからです。数値は各byteごとに「0 0 0 2」となっており、それが逆向きに「2 0 0 0」として格納されています。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1