ひとつまみの疑問が生まれたので。
既知の事実
#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言語における配列が「同じデータ型をもつ、連続に格納された変数」を表しているからです。
メモリ番地は適当です。
上図を見るとわかるように、メモリ番地に1を足し加えることで、次のメモリ番地に移動しています(実際にはメモリアドレスが1加算されただけなのですが、結果的に「移動」したように見えます)。配列であれば連続的に値が格納されているので、次のメモリ番地には次の文字が格納されているというわけです。
と、このように、配列とポインタには切っても切れない関係があります。もとより配列の要素数を[]で示すのはシンタックスシュガーみたいなものです。ちょっと誤解を生む発言ですが、そんな雰囲気のやつ、と思ってもらえれば大丈夫です1。
そういえば、char型って1byteの変数ですよね。だから先ほどの動作が実現できていました。ところで、int型配列ではどうでしょう?
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加算するってわけじゃないんですね。まぁ確かにその方がソースコードは直感的ですけど。なんかインクリメント演算子に対するイメージが壊れて悲しいです。裏切られちゃった。世の中そういうことだらけだなぁ。