概要
CERN ROOT Jupyter notebookはインタプリタなので、簡単にいろいろなことを試すことができます。
今回は配列とポインタについて試します。
環境構築
関連記事
実行環境
root --version
ROOT Version: 6.32.08
Built for macosxarm64 on Nov 14 2024, 09:27:26
From tags/6-32-08@6-32-08
root-config --python-version
3.13.0
python --version
Python 3.13.0
Jupyter notebookでの実行内容の記載について
実行内容は本来ブラウザのスクリーンショットを載せるのですが、ちょっと煩雑なので次のように記載します。
入力セルの内容はC言語のコードブロックでファイル名をinput [番号]
出力セルは言語指定なしのコードブロックでファイル名をoutput [inputと同じ番号]
配列の実験
int nums1[3] = {1,2,3};
int nums2[] = {1,2,3};
printf("nums1 size = %lu, nums1[0] = %d, nums1[1] = %d, nums1[2] = %d\n",
sizeof(nums1), nums1[0], nums1[1], nums1[2]);
printf("nums2 size = %lu, nums2[0] = %d, nums2[1] = %d, nums2[2] = %d\n",
sizeof(nums2), nums2[0], nums2[1], nums2[2]);
nums1 size = 12, nums1[0] = 1, nums1[1] = 2, nums1[2] = 3
nums2 size = 12, nums2[0] = 1, nums2[1] = 2, nums2[2] = 3
nums1とnums2は同じ内容になっています。初期値をとるときは配列数を指定しなくても良い。
では変数nums1と変数nums2は何を表しているかというと、下記の実行例でわかるように配列の先頭アドレスを表している。
printf("nums1: %p, %p, %p, %p\n", nums1, &nums1[0], &nums1[1], &nums1[2]);
printf("nums2: %p, %p, %p, %p\n", nums2, &nums2[0], &nums2[1], &nums2[2]);
nums1: 0x1297f0000, 0x1297f0000, 0x1297f0004, 0x1297f0008
nums2: 0x1297f000c, 0x1297f000c, 0x1297f0010, 0x1297f0014
よって、次のような演算もできる。
printf("*nums1 = %d\n", *nums1);
int *pnums1 = nums1;
printf("*pnums1 = %d\n", *pnums1);
*nums1 = 1
*pnums1 = 1
しかし、次のような演算はできない。
nums1 = nums2;
input_line_60:2:8: error: array type 'int[3]' is not assignable
nums1 = nums2;
~~~~~ ^
エラーとなるため、配列を定義した変数の値を変更することはできない。つまり、定数扱いのようなものとなる。
アドレスの加算・減算
アドレスは加算・減算ができるので計算してみる。ただし、乗算・除算はできません。
printf(" nums1 = %p\nnums1 + 1 = %p\n", nums1, nums1 + 1);
nums1 = 0x1297f0000
nums1 + 1 = 0x1297f0004
nums1+1は1バイトではなく4バイト増えています。上記のinput [2],output [2]をみるとnums1+1のアドレスはnums1[1]のアドレスと同じです。アドレスに1を加算するとは配列の添字を1増やした項目のアドレスになります。つまり、型intのバイト数分だけ増えてます。よって、整数値nをアドオレスに加算するとは(配列の型のバイト数)*nだけアドレスに加算されます。
nums1[2]の値をアドレスの計算によって表して見ます。
printf(" nums1[2] = %d\n*(nums1+2) = %d\n", nums1[2], *(nums1+2));
nums1[2] = 3
*(nums1+2) = 3
*(nums1+2)を*nums1+2としないようにしましょう。*nums1+2はnums1[0]+2と同じ意味です。
今回の結果はたまたま同じ3になるのでミスに気が付きません。
アドレス計算は書き換え可能な変数を使って計算するほうが一般的です。
int *p = nums1;
p += 2;
printf(" p = %p\n*p = %d\n", p, *p);
p = 0x1297f0008
*p = 3
pに2を加算していますが、アドレスには8バイトが加算されて*pはnums1[2]と同じであることがわかります。
減算もしてみましょう。
p--;
printf(" p = %p\n*p = %d\n", p, *p);
p = 0x1297f0004
*p = 2
なぜアドレスの加算で正常に配列にアクセスできるのか?
配列はメモリ上に連続のアドレスに配置されるからです。もし、nums1[0]とnums1[1]が飛び飛びで配置されると*(nums1+1)でnums1[1]がアクセスできなくなります。
ポインタと配列を使ってintのメモリの構成を調べる (エンディアン)
int 型に16進数の0x12345678を代入して、メモリ上に1バイトづつ0x12,0x34,0x56,0x78の順番に入っているかを調べてみる。
int num = 0x12345678;
printf("num = 0x%x %d\n", num, num);
unsigned char* c = (unsigned char*)#
for(int i = 0; i < 4; i++) {
printf("c[%d] = 0x%x address = %p\n", i, *c, c);
c++;
}
num = 0x12345678 305419896
c[0] = 0x78 address = 0x13dd0c000
c[1] = 0x56 address = 0x13dd0c001
c[2] = 0x34 address = 0x13dd0c002
c[3] = 0x12 address = 0x13dd0c003
私が使っているMacではメモリ上に逆順に格納されている。どういう順序でメモリに格納されているのかをエンディアンwikipediaといいCPUに依存している。 上記のように逆の順序で格納されるのをリトルエンディアンといいます。
C言語の1バイトの型はcharなので符号なしのためunsigned charのポインタとしてcを宣言している。 下から2行目c++はcの値であるアドレスに1バイト加算している。今回の加算はint型ではないため4バイトではなく、char型なので1バイト分だけ加算される。
ちなみに、int **p;
においてp++は4バイト加算ではなく、8バイト加算です。pの型は(int *)のポインタでint *はポインタなので8バイトになります。
変数numを値を変えるにはnumに直接代入しなくても、cに値を代入することで変えることができる。
num = 0x12345678;
printf("num = 0x%x %d\n", num, num);
c = (unsigned char*)#
for(int i = 0; i < 4; i++, c++) {
*c = i + 1;
}
printf("num = 0x%08x %d\n", num, num);
num = 0x12345678 305419896
num = 0x04030201 67305985
多次元配列
配列は4次元以上の配列も定義できますが、以降は3次元配列で実験します。
constexpr int size1 = 2, size2 = 3, size3 = 4;
int array1[size1][size2][size3] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24};
int array2[size1][size2][size3] = {{{1,2,3,4},{5,6,7,8},{9,10,11,12}},{{13,14,15,16},{17,18,19,20},{21,22,23,24}}};
for(int i = 0; i < size1; i++)
for(int j = 0; j < size2; j++)
for(int k = 0; k < size3; k++)
printf("[%d][%d][%d] array1 = %d, array2 = %d\n",
i, j, k, array1[i][j][k],array1[i][j][k]);
[0][0][0] array1 = 1, array2 = 1
[0][0][1] array1 = 2, array2 = 2
[0][0][2] array1 = 3, array2 = 3
[0][0][3] array1 = 4, array2 = 4
[0][1][0] array1 = 5, array2 = 5
[0][1][1] array1 = 6, array2 = 6
[0][1][2] array1 = 7, array2 = 7
[0][1][3] array1 = 8, array2 = 8
[0][2][0] array1 = 9, array2 = 9
[0][2][1] array1 = 10, array2 = 10
[0][2][2] array1 = 11, array2 = 11
[0][2][3] array1 = 12, array2 = 12
[1][0][0] array1 = 13, array2 = 13
[1][0][1] array1 = 14, array2 = 14
[1][0][2] array1 = 15, array2 = 15
[1][0][3] array1 = 16, array2 = 16
[1][1][0] array1 = 17, array2 = 17
[1][1][1] array1 = 18, array2 = 18
[1][1][2] array1 = 19, array2 = 19
[1][1][3] array1 = 20, array2 = 20
[1][2][0] array1 = 21, array2 = 21
[1][2][1] array1 = 22, array2 = 22
[1][2][2] array1 = 23, array2 = 23
[1][2][3] array1 = 24, array2 = 24
arary1は配列の先頭から順に1次元配列のように初期値を設定し、array2は次元に合わせた初期値の設定の仕方をしていますがメモリの内容はどちらも同じでした。
次にポインタを使って配列にアクセスしてみます。
int *parray1 = array1;
printf("%dn", *array1);
input_line_151:2:7: error: cannot initialize a variable of type 'int *' with an lvalue of type 'int[2][3][4]'
int *parray1 = array1;
^ ~~~~~~
input_line_151:3:15: warning: format specifies type 'int' but the argument has type 'int (*)[4]' [-Wformat]
printf("%dn", *array1);
エラーになり1次元配列の様にはできないみたいです。
では、次に上手くいくであろう方法を3つ紹介します。
int *parray1 = (int *)array1;
int *parray2 = &array1[0][0][0];
int *parray3 = array1[0][0];
printf("%d, %d, %d\n", *parray1, *parray2, *parray3);
1, 1, 1
2番目の方法が一番わかり易いかなと思います。3番目は残りを1次元にすることにより1次元配列のようにアドレスが取れるのかなと思います。
array1の任意の要素array1[i][j][k]にポインタであるparray1を使ってアクセスするにはどうするか。
各次元の添字が1増えたらどのぐらいアドレスを増やすかの計算が必要になる。
kを1増加したらポインタを1増加させ、jを1増加させたらポインタはsize3増加させ、iを1増加させたらsize2*size3増加させるようにプログラムする。
int i = 0;
int j = 1;
int k = 2;
int add_num = i * size2 * size3 + j * size3 + k;
printf("%d, %d\n", array1[i][j][k], *(parray1+add_num));
7, 7
ポインタを使う場合は多次元配列を1次元配列を使うように先頭からの相対位置の計算が必要になる。
終わりに
プログラミング言語で配列を扱う場合において添字が0オリジンと1オリジンが言語あり、1オリジンの言語を久々使うとループの1発目でエラーになり1オリジンと気付き添字の計算式を変更することになる。