C/C++の学習にCERN ROOT Jupyter notebookを使おう -- ポインタ実験2(配列)

# 概要

CERN ROOT Jupyter notebookはインタプリタなので、簡単にいろいろなことを試すことができます。
今回は配列とポインタについて試します。

#### 環境構築

https://qiita.com/h6591/items/266fa3a6ca3a9a0f98c2

#### 関連記事

https://qiita.com/h6591/items/ad60b38b6858f70a4659

# 実行環境

```bash
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 [2]
入力セルの内容はC言語のコードブロックでファイル名をinput [番号]
```
```:output [2]
出力セルは言語指定なしのコードブロックでファイル名をoutput [inputと同じ番号]
```


# 配列の実験

 ```c: input[1]
 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]);
```
```:output [1]
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**は何を表しているかというと、下記の実行例でわかるように配列の先頭アドレスを表している。

```c:input [2]
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]);
```
```:output [2]
nums1: 0x1297f0000, 0x1297f0000, 0x1297f0004, 0x1297f0008
nums2: 0x1297f000c, 0x1297f000c, 0x1297f0010, 0x1297f0014
```

よって、次のような演算もできる。
```c:input [3]
printf("*nums1 = %d\n", *nums1);

int *pnums1 = nums1;
printf("*pnums1 = %d\n", *pnums1);
```

```:output [3]
*nums1 = 1
*pnums1 = 1
````

しかし、次のような演算はできない。

```c:input [4]
nums1 = nums2;
```
```:output [4]
input_line_60:2:8: error: array type 'int[3]' is not assignable
 nums1 = nums2;
 ~~~~~ ^
 ```
 エラーとなるため、配列を定義した変数の値を変更することはできない。つまり、定数扱いのようなものとなる。


### アドレスの加算・減算

アドレスは加算・減算ができるので計算してみる。ただし、乗算・除算はできません。

```c:input [5]
printf("    nums1 = %p\nnums1 + 1 = %p\n", nums1, nums1 + 1);
```
```:output [5]
    nums1 = 0x1297f0000
nums1 + 1 = 0x1297f0004
```
nums1+1は1バイトではなく4バイト増えています。上記のinput [2],output [2]をみるとnums1+1のアドレスはnums1[1]のアドレスと同じです。アドレスに1を加算するとは配列の添字を1増やした項目のアドレスになります。つまり、型intのバイト数分だけ増えてます。よって、整数値nをアドオレスに加算するとは(配列の型のバイト数)*nだけアドレスに加算されます。
nums1[2]の値をアドレスの計算によって表して見ます。
```c:input [6]
printf("  nums1[2] = %d\n*(nums1+2) = %d\n", nums1[2], *(nums1+2));
```
```:oputput [6]
  nums1[2] = 3
*(nums1+2) = 3
```
\*(nums1+2)を\*nums1+2としないようにしましょう。*nums1+2はnums1[0]+2と同じ意味です。
今回の結果はたまたま同じ3になるのでミスに気が付きません。

アドレス計算は書き換え可能な変数を使って計算するほうが一般的です。
```c:input [7]
int *p = nums1;
p += 2;
printf(" p = %p\n*p = %d\n", p, *p);
```
```:output [7]
 p = 0x1297f0008
*p = 3
```

pに2を加算していますが、アドレスには8バイトが加算されて*pはnums1[2]と同じであることがわかります。

減算もしてみましょう。

```c:input [8]
p--;
printf(" p = %p\n*p = %d\n", p, *p);
```
```:output [8]
 p = 0x1297f0004
*p = 2
```

:::note info
なぜアドレスの加算で正常に配列にアクセスできるのか?
配列はメモリ上に連続のアドレスに配置されるからです。もし、nums1[0]とnums1[1]が飛び飛びで配置されると*(nums1+1)でnums1[1]がアクセスできなくなります。
:::



### ポインタと配列を使ってintのメモリの構成を調べる (エンディアン)

int 型に16進数の0x12345678を代入して、メモリ上に1バイトづつ0x12,0x34,0x56,0x78の順番に入っているかを調べてみる。

```c: input [9]
int num = 0x12345678;
printf("num = 0x%x %d\n", num, num);
unsigned char* c = (unsigned char*)&num;
for(int i = 0; i < 4; i++) {
    printf("c[%d] = 0x%x address = %p\n", i, *c, c);
    c++;
}
```
```:output [9]
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](https://ja.wikipedia.org/wiki/エンディアン)といいCPUに依存している。 上記のように逆の順序で格納されるのを**リトルエンディアン**といいます。

 C言語の１バイトの型はcharなので符号なしのためunsigned charのポインタとしてcを宣言している。 下から２行目c++はcの値であるアドレスに1バイト加算している。今回の加算はint型ではないため4バイトではなく、char型なので１バイト分だけ加算される。
ちなみに、`int **p;`においてp++は4バイト加算ではなく、8バイト加算です。pの型は(int *)のポインタでint *はポインタなので8バイトになります。

変数numを値を変えるにはnumに直接代入しなくても、cに値を代入することで変えることができる。

```c:input [10]
num = 0x12345678;
printf("num = 0x%x %d\n", num, num);
c = (unsigned char*)&num;
for(int i = 0; i < 4; i++, c++) {
    *c = i + 1;
}
printf("num = 0x%08x %d\n", num, num);
```
```:output [10]
num = 0x12345678 305419896
num = 0x04030201 67305985
```

### 多次元配列

配列は4次元以上の配列も定義できますが、以降は３次元配列で実験します。

```c:input [11]
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]);
```
```:output [11]
[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は次元に合わせた初期値の設定の仕方をしていますがメモリの内容はどちらも同じでした。

次にポインタを使って配列にアクセスしてみます。

```c:input [12]
int *parray1 = array1;
printf("%dn", *array1);
```
```:output [12]
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次元配列の様にはできないみたいです。
では、次に上手くいくであろう方法を３つ紹介します。
```c:input [13]
int *parray1 = (int *)array1;
int *parray2 = &array1[0][0][0];
int *parray3 = array1[0][0];
printf("%d, %d, %d\n", *parray1, *parray2, *parray3);
```
```:output [13]
1, 1, 1
```
2番目の方法が一番わかり易いかなと思います。３番目は残りを1次元にすることにより1次元配列のようにアドレスが取れるのかなと思います。

array1の任意の要素array1[i][j][k]にポインタであるparray1を使ってアクセスするにはどうするか。
各次元の添字が１増えたらどのぐらいアドレスを増やすかの計算が必要になる。
kを1増加したらポインタを1増加させ、jを１増加させたらポインタはsize3増加させ、iを１増加させたらsize2*size3増加させるようにプログラムする。
```c:input [14]
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));
```
```:output [14]
7, 7
```

ポインタを使う場合は多次元配列を1次元配列を使うように先頭からの相対位置の計算が必要になる。

# 終わりに

プログラミング言語で配列を扱う場合において添字が0オリジンと1オリジンが言語あり、1オリジンの言語を久々使うとループの１発目でエラーになり1オリジンと気付き添字の計算式を変更することになる。