C/C++の学習にCERN ROOT Jupyter notebookを使おう -- ポインタの実験4(関数の引数、関数ポインタ)

# 概要

CERN ROOT Jupyter notebookはインタプリタなので、簡単にいろいろなことを試すことができます。
今回は関数の引数でのポインタおよび関数自身のポインタについて試します。
ポインタの実験はこれが最後の予定です。

### 環境構築

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

### 関連記事

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

https://qiita.com/h6591/items/5143cdf6eb7fb38aa700

https://qiita.com/h6591/items/29af70d491179f3cdf17

# 実行環境

```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
```

# 関数の仮引数のポインタ

### int型

[ポインタ実験3](https://qiita.com/h6591/items/29af70d491179f3cdf17)で実験したように**const**を含めたポインタのバリエーションを試す。

```c:input [1]
void print_ptr(int arg1, const int arg2, int *arg3, const int *arg4, 
            int * const arg5, const int * const arg6) {
    printf("arg1: val = %d, addr = %p\n", arg1, &arg1);
    printf("arg2: val = %d, addr = %p\n", arg2, &arg2);
    printf("arg3: val = %p, addr = %p, *arg3 = %d\n", arg3, &arg3, *arg3);
    printf("arg4: val = %p, addr = %p, *arg4 = %d\n", arg4, &arg4, *arg4);
    printf("arg5: val = %p, addr = %p, *arg5 = %d\n", arg5, &arg5, *arg5);
    printf("arg6: val = %p, addr = %p, *arg6 = %d\n", arg6, &arg6, *arg6);
}
int num = 123;
int *pnum = &num;
printf("num: val = %d, addr = %p\n", num, &num);
printf("pnum: val = %p, addr = %p *pnum = %d\n", pnum, &pnum, *pnum);
print_ptr(num, num, pnum, &num, pnum, &num);
```

```:output [1]
num: val = 123, addr = 0x145c00118
pnum: val = 0x145c00118, addr = 0x145c00120 *pnum = 123
arg1: val = 123, addr = 0x1758da4bc
arg2: val = 123, addr = 0x1758da4b8
arg3: val = 0x145c00118, addr = 0x1758da4b0, *arg3 = 123
arg4: val = 0x145c00118, addr = 0x1758da4a8, *arg4 = 123
arg5: val = 0x145c00118, addr = 0x1758da4a0, *arg5 = 123
arg6: val = 0x145c00118, addr = 0x1758da498, *arg6 = 123
```
関数の引数は、基本的に値渡しである。よって、通常の代入に似ている。基本の型の場合はその値が仮引数に渡され、ポインタ型の場合はアドレス(**&num** または **pnum**)を渡さなければならない。 **num**のアドレスと**arg3**から**arg6**に渡された値(val)は同じアドレスであることが確認できる。また、当然ではあるが**num**のアドレスとすべての仮引数のアドレスは異なっている。

アドレスが渡されると、間接参照よって元の値の変更が可能になる。以下、それを実験する。

```c:input [2]
void print_ptr(int arg1, const int arg2, int *arg3, const int *arg4, 
            int * const arg5, const int * const arg6) {
    *arg3 = 789;
    printf("arg1: val = %d, addr = %p\n", arg1, &arg1);
    printf("arg2: val = %d, addr = %p\n", arg2, &arg2);
    printf("arg3: val = %p, addr = %p, *arg3 = %d\n", arg3, &arg3, *arg3);
    printf("arg4: val = %p, addr = %p, *arg4 = %d\n", arg4, &arg4, *arg4);
    printf("arg5: val = %p, addr = %p, *arg5 = %d\n", arg5, &arg5, *arg5);
    printf("arg6: val = %p, addr = %p, *arg6 = %d\n", arg6, &arg6, *arg6);
}
int num = 123;
int *pnum = &num;
printf("num: val = %d, addr = %p\n", num, &num);
printf("pnum: val = %p, addr = %p *pnum = %d\n", pnum, &pnum, *pnum);
print_ptr(num, num, pnum, &num, pnum, &num);
printf("num: val = %d, addr = %p\n", num, &num);
printf("pnum: val = %p, addr = %p *pnum = %d\n", pnum, &pnum, *pnum);
```

```:output [2]
num: val = 123, addr = 0x145bf8118
pnum: val = 0x145bf8118, addr = 0x145bf8120 *pnum = 123
arg1: val = 123, addr = 0x1758da45c
arg2: val = 123, addr = 0x1758da458
arg3: val = 0x145bf8118, addr = 0x1758da450, *arg3 = 789
arg4: val = 0x145bf8118, addr = 0x1758da448, *arg4 = 789
arg5: val = 0x145bf8118, addr = 0x1758da440, *arg5 = 789
arg6: val = 0x145bf8118, addr = 0x1758da438, *arg6 = 789
num: val = 789, addr = 0x145bf8118
pnum: val = 0x145bf8118, addr = 0x145bf8120 *pnum = 789
```
間接参照で元の値を変更した後は、int型で渡されるものは値に変更はないがアドレスが渡されているものは間接参照で値が変更されているのが確認できる。

次に**num**を**const**にして同じように実験する。

```c:input [3]
void print_ptr(int arg1, const int arg2, int *arg3, const int *arg4, 
            int * const arg5, const int * const arg6) {
    printf("arg1: val = %d, addr = %p\n", arg1, &arg1);
    printf("arg2: val = %d, addr = %p\n", arg2, &arg2);
    printf("arg3: val = %p, addr = %p, *arg3 = %d\n", arg3, &arg3, *arg3);
    printf("arg4: val = %p, addr = %p, *arg4 = %d\n", arg4, &arg4, *arg4);
    printf("arg5: val = %p, addr = %p, *arg5 = %d\n", arg5, &arg5, *arg5);
    printf("arg6: val = %p, addr = %p, *arg6 = %d\n", arg6, &arg6, *arg6);
}
const int num = 123;
const int *pnum = &num;
printf("num: val = %d, addr = %p\n", num, &num);
printf("pnum: val = %p, addr = %p *pnum = %d\n", pnum, &pnum, *pnum);
print_ptr(num, num, pnum, &num, pnum, &num);
```
```:output [3]
input_line_187:15:1: error: no matching function for call to 'print_ptr'
print_ptr(num, num, pnum, &num, pnum, &num);
^~~~~~~~~
input_line_187:1:6: note: candidate function not viable: 3rd argument ('const int *') would lose const qualifier
void print_ptr(int arg1, const int arg2, int *arg3, const int *arg4, 
     ^
input_line_83:1:6: note: candidate function not viable: requires 4 arguments, but 6 were provided
void print_ptr(int arg1, const int arg2, const int *arg4, 
     ^
```

上記のエラーメッセージは`print_ptr(num, num, pnum, &num, pnum, &num);`の呼び出しにマッチする関数**print_ptr**は無いと言っている思います。 CERN ROOTは基本C++なので、オーバーロード定義が可能なのためのエラーメッセージだと思います。

以下のようにMacのgccでコンパイルしてみました。

```c:test2.c
include <stdio.h>

void print_ptr(int arg1, const int arg2, int *arg3, const int *arg4,
            int * const arg5, const int * const arg6) {
    printf("arg1: val = %d, addr = %p\n", arg1, &arg1);
    printf("arg2: val = %d, addr = %p\n", arg2, &arg2);
    printf("arg3: val = %p, addr = %p, *arg3 = %d\n", arg3, &arg3, *arg3);
    printf("arg4: val = %p, addr = %p, *arg4 = %d\n", arg4, &arg4, *arg4);
    printf("arg5: val = %p, addr = %p, *arg5 = %d\n", arg5, &arg5, *arg5);
    printf("arg6: val = %p, addr = %p, *arg6 = %d\n", arg6, &arg6, *arg6);
}

int main() {
    const int num = 123;
    const int *pnum = &num;
    printf("num: val = %d, addr = %p\n", num, &num);
    printf("pnum: val = %p, addr = %p *pnum = %d\n", pnum, &pnum, *pnum);
    print_ptr(num, num, pnum, &num, pnum, &num);

    return 0;
}
```
```bash
> gcc test2.c
test3.c:19:22: warning: passing 'const int *' to parameter of type 'int *' discards qualifiers [-Wincompatible-pointer-types-discards-qualifiers]
   19 |         print_ptr(num, num, pnum, &num, pnum, &num);
      |                             ^~~~
test3.c:4:47: note: passing argument to parameter 'arg3' here
    4 | void print_ptr(int arg1, const int arg2, int *arg3, const int *arg4, 
      |                                               ^
test3.c:19:34: warning: passing 'const int *' to parameter of type 'int *' discards qualifiers [-Wincompatible-pointer-types-discards-qualifiers]
   19 |         print_ptr(num, num, pnum, &num, pnum, &num);
      |                                         ^~~~
test3.c:5:25: note: passing argument to parameter 'arg5' here
    5 |             int * const arg5, const int * const arg6) {
      |                         ^
2 warnings generated.
```
`output [3]`と違って、**const**が合っていないところにちゃんと警告で出ています。


**const**が合っていない引数を除いて実行します。
```c:input [4]
void print_ptr(int arg1, const int arg2, const int *arg4, 
             const int * const arg6) {
    printf("arg1: val = %d, addr = %p\n", arg1, &arg1);
    printf("arg2: val = %d, addr = %p\n", arg2, &arg2);
    printf("arg4: val = %p, addr = %p, *arg4 = %d\n", arg4, &arg4, *arg4);
    printf("arg6: val = %p, addr = %p, *arg6 = %d\n", arg6, &arg6, *arg6);
}

const int num = 123;
const int *pnum = &num;
printf("num: val = %d, addr = %p\n", num, &num);
printf("pnum: val = %p, addr = %p *pnum = %d\n", pnum, &pnum, *pnum);
print_ptr(num, num, pnum, &num);
```

```:output [4]
num: val = 123, addr = 0x145c080c4
pnum: val = 0x145c080c4, addr = 0x145c080c8 *pnum = 123
arg1: val = 123, addr = 0x1758da4bc
arg2: val = 123, addr = 0x1758da4b8
arg4: val = 0x145c080c4, addr = 0x1758da4b0, *arg4 = 123
arg6: val = 0x145c080c4, addr = 0x1758da4a8, *arg6 = 123
```
問題なく実行できました。

さらに、[ポインタ実験3](https://qiita.com/h6591/items/29af70d491179f3cdf17)で実験したように*の数を増やしてダブルポインタ、トリプルポインタの実験するのも良いかもしれません。

### 1次元配列 

1次元配列と**const**の組み合わせの仮引数について実験します。

```c:input [5]
void print_array(int size, int *arg1, int arg2[], int arg3[6], const int *arg4,
                const int arg5[], const int arg6[1], const int * const arg7) {
    printf("size = %d\n", size);
    printf("arg1: val = %p, addr = %p,     *arg1 = %d\n", arg1, &arg1, *arg1);
    printf("arg2: val = %p, addr = %p,   arg2[1] = %d\n", arg2, &arg2, arg2[1]);
    printf("arg3: val = %p, addr = %p,   arg3[2] = %d\n", arg3, &arg3, arg3[2]);
    printf("arg4: val = %p, addr = %p, *(arg4+3) = %d\n", arg4, &arg4, *(arg4+3));
    printf("arg5: val = %p, addr = %p, *(arg5+4) = %d\n", arg5, &arg5, *(arg5+4));
    printf("arg6: val = %p, addr = %p,   arg6[5] = %d\n", arg6, &arg6, arg6[5]);
    printf("arg7: val = %p, addr = %p,   arg7[6] = %d\n", arg7, &arg7, arg7[6]);
}

int arry[] = {1,2,3,4,5,6,7};
int *parry = arry;
print_array(sizeof(arry)/sizeof(arry[0]), arry, &arry[0], parry, arry, &arry[0], parry, arry);
```
```:output [5]
size = 7
arg1: val = 0x158658138, addr = 0x171f064b8,     *arg1 = 1
arg2: val = 0x158658138, addr = 0x171f064b0,   arg2[1] = 2
arg3: val = 0x158658138, addr = 0x171f064a8,   arg3[2] = 3
arg4: val = 0x158658138, addr = 0x171f064a0, *(arg4+3) = 4
arg5: val = 0x158658138, addr = 0x171f06498, *(arg5+4) = 5
arg6: val = 0x158658138, addr = 0x171f06490,   arg6[5] = 6
arg7: val = 0x158658138, addr = 0x171f06488,   arg7[6] = 7
```

すべての仮引数についてエラーなく実行できました。
ここで`const int const arg5[]`とは出来ません。これは**const**の二重指定になします。理由は`const int`と`int const`は同じ意味だからです。また、`arg2 []`など配列を意味するカッコを記述すると`arg2`自身は変更不可で**const**の意味も含んでいます。なので、`arg5`と`arg6`実質`arg7`と同じ様な扱いになります。 
仮引数`arg3[7]`,`arg6[1]`でカッコ内の要素数はあまり意味がありません。`arg6`の`printf`において`arg6[5]`でもちゃんとアクセス出来ていますから。要素数が可変の場合はさらに意味がありません。要素数が固定で要素数を書いても後で仕様変更等で要素数が変わると、プログラム修正が面倒になります。 `arg2[]`の形式で要素数は別引数(sizeのように)にしたほうが良いと思います。

---
次に配列要素の書き換えを実験します。当然、書き換え可能な引数は`arg1,arg2,arg3`です。ここでは`arg1`を使って書き換えを行います。

```c:input [6]
void print_array(int size, int *arg1, int arg2[], int arg3[6], const int *arg4,
                const int arg5[], const int arg6[1], const int * const arg7) {
    printf("size = %d\n", size);
    for(int i = 0; i < size; i++) {
        arg1[i] = i+100;
    }
    printf("arg1: val = %p, addr = %p,     *arg1 = %d\n", arg1, &arg1, *arg1);
    printf("arg2: val = %p, addr = %p,   arg2[1] = %d\n", arg2, &arg2, arg2[1]);
    printf("arg3: val = %p, addr = %p,   arg3[2] = %d\n", arg3, &arg3, arg3[2]);
    printf("arg4: val = %p, addr = %p, *(arg4+3) = %d\n", arg4, &arg4, *(arg4+3));
    printf("arg5: val = %p, addr = %p, *(arg5+4) = %d\n", arg5, &arg5, *(arg5+4));
    printf("arg6: val = %p, addr = %p,   arg6[5] = %d\n", arg6, &arg6, arg6[5]);
    printf("arg7: val = %p, addr = %p,   arg7[6] = %d\n", arg7, &arg7, arg7[6]);
}

int arry[] = {1,2,3,4,5,6,7};
int *parry = arry;
int num_arry = sizeof(arry)/sizeof(arry[0]);
print_array(num_arry, arry, &arry[0], parry, arry, &arry[0], parry, arry);
for(int i = 0; i < num_arry; i++) {
    printf("%d ", arry[i]);
}
```
```:output [6]
size = 7
arg1: val = 0x1487f4140, addr = 0x173716468,     *arg1 = 100
arg2: val = 0x1487f4140, addr = 0x173716460,   arg2[1] = 101
arg3: val = 0x1487f4140, addr = 0x173716458,   arg3[2] = 102
arg4: val = 0x1487f4140, addr = 0x173716450, *(arg4+3) = 103
arg5: val = 0x1487f4140, addr = 0x173716448, *(arg5+4) = 104
arg6: val = 0x1487f4140, addr = 0x173716440,   arg6[5] = 105
arg7: val = 0x1487f4140, addr = 0x173716438,   arg7[6] = 106
100 101 102 103 104 105 106 
```

正常に書き換えが行われていることが確認できます。

さらに、`const int arry[]`としたときの実験をするときには上記のint型の実験を参考にしてください。

### 多次元配列

2次元配列で実験します。 なお、**const**については省きます。

```c:input [7]
void print_array(int size1, int size2, int *arg1, int arg2[], int arg3[][4], int arg4[2][4]){
    printf("size1 = %d, size2 = %d\n", size1, size2);

    printf("arg1: val = %p, addr = %p,      *arg1 = %d\n", arg1, &arg1, *arg1);
    printf("arg2: val = %p, addr = %p,    arg2[7] = %d\n", arg2, &arg2, arg2[7]);
    printf("arg3: val = %p, addr = %p, arg3[0][3] = %d\n", arg3, &arg3, arg3[0][3]);
    printf("arg4: val = %p, addr = %p, arg4[1][0] = %d\n", arg4, &arg4, arg4[1][0]);
}

int arry[][4] = {{1,2,3,4},{5,6,7,8}};
int *parry = &arry[0][0];
int num_arry1 = sizeof(arry)/sizeof(arry[0]);
int num_arry2 = sizeof(arry[0])/sizeof(arry[0][0]);
print_array(num_arry1, num_arry2, arry[0], &arry[0][0], arry, arry);
```
```:output [7]
size1 = 2, size2 = 4
arg1: val = 0x1216840c8, addr = 0x1739624b8,      *arg1 = 1
arg2: val = 0x1216840c8, addr = 0x1739624b0,    arg2[7] = 8
arg3: val = 0x1216840c8, addr = 0x1739624a8, arg3[0][3] = 4
arg4: val = 0x1216840c8, addr = 0x1739624a0, arg4[1][0] = 5
```

`arg1`,`arg2`は2次元配列を1次元配列として扱います。そのときのアドレスの取得方法は`arry[0]`、`&arry[0][0]`になります。
`arg3`、`arg4`のように2次元配列として扱う場合は必ず2番目の要素数を書かなければなりません。理由はとしては、例えば`arg3[i][j]`にアクセスしようとしても2番目の要素数`k`がないと内部的に`arg2[i*k+j]`と同様の計算が出来ないためです。なので、2番目の要素数は正解に書かなければなりません。

3次元配列なども実験してみてください。 `int arg1[][3][4]`はOKですが、`int arg2[][][3]`は上記と同様の理由でエラーになります。
また、C/C++学習者が必ず見たことのある配列があります。それは`main`の`argv`です。
```c:test3.c
int main(int argc, char *argv[]) {
    return 0;
}
```    
`argv`はポインタの配列です。そして`argc`が要素数です。さらに今まで見てきたようにargvは`char **argv`と書いても問題なく使えます。

### 構造体(struct)

構造体は関数の引数に使う場合は注意を要することがあります。
まずは以下のコードを実行してみます。

```c:input [8]
struct struct1 {
    int arry[5];
};
struct struct1 nums1 = {{1,2,3,4,5}};
struct struct1 nums2 = nums1;
for(int i = 0; i < 5; i++) {
    printf("arry[%d] = %d\n", i, nums2.arry[i]);
}
```
```:output [8]
arry[0] = 1
arry[1] = 2
arry[2] = 3
arry[3] = 4
arry[4] = 5
```
同一構造体どうしの代入が可能で、値がコピーされます。通常、配列から配列に1回でコピーすることが出来ず、要素ごとに代入（コピー）が必要です。

そこで構造体を関数の引数にしたときの実験を次のように行います。
```c:input [9]
void print_struct(struct struct1 arg1, struct struct1 *arg2) {
    printf("arg1: addr = %p, arry[0] = %d\n", &arg1, arg1.arry[0]);
    printf("arg2: addr = %p, val = %p, arg2->arry[0] = %d\n", &arg2, arg2, arg2->arry[0]);
    arg1.arry[0] = 100;
    arg2->arry[1] = 200;
}
struct struct1 nums1 = {{1,2,3,4,5}};
printf("nums1 addr = %p\n", &nums1);
print_struct(nums1, &nums1);
for(int i = 0; i < 5; i++) {
    printf("arry[%d] = %d\n", i, nums1.arry[i]);
}
```

```:output [9]
nums1 addr = 0x1217c8070
arg1: addr = 0x1739624a0, arry[0] = 1
arg2: addr = 0x173962458, val = 0x1217c8070, arg2->arry[0] = 1
arry[0] = 1
arry[1] = 200
arry[2] = 3
arry[3] = 4
arry[4] = 5
```

`input [8]`でも確かめたように`arg1`は`nums1`全体がコピーされているので`arry[0]`を書き換えても`nums1`に反映されていません。しかし、ポインタを使った場合は書き換えが行われています。
単に参照するだけでも全体をコピーするコストを考えると**const**付きのポインタを使ったほうが良いでしょう。

# 関数ポインタ

関数ポインタはフレームワークやライブラリに**コールバック関数**を渡すときなどによく使います。

まず、関数を定義しますが、1セルに１関数しか定義できないようなので複数セルを使って関数定義をします。

```c:input [10]
int add1(int n) { 
    printf("enter add1\n");
    return n + 1;
}
```
```c:input [11]
int add2(int n) { 
    printf("enter add2\n");
    return n + 2;
}
```

```c:input [12]
int (*add_func)(int) = &add1;
printf("add_func = %d\n", add_func(5));
add_func = &add2;
printf("add_func = %d\n", (*add_func)(5));
```
```:output [12]
enter add1
add_func = 6
enter add2
add_func = 7
```

関数ポインタは以下のように定義します。

- 戻り値の型 (\*変数名)(仮引数1の型,...);


`add_func`は`int (*add_func)(int n);`と仮引数(n)を書いてもOKです。
**const**は`int (* const add_func)(int);`と書け、`add_func`の書き換えができなくなります。
関数ポインタを使って関数を呼び出すときは`add_func(...)`または`(*add_func)(...)`のどちらでもOKですが、前者の書き方が一般的かなと思います。ポインタの使い方からすると後者のように思います。後者の書き方は`()`が重要です。`(*add_func)(...)`と`*add_func(...)`では意味が異なります。

---

次は関数のダブルポインタを実験します。
```c:input [13]
int (*add_func)(int) = &add1;
int (**add_func2)(int) = &add_func;
printf("add_func2 = %d\n", (*add_func2)(5));
add_func = &add2;
printf("add_func2 = %d\n", (**add_func2)(5));
```
```:output [13]
enter add1
add_func2 = 6
enter add2
add_func2 = 7
```

関数の呼び出しに\*が1個でも2個でも良いというのはちょっと紛らわしいと思います。しかし、呼び出し以外に代入とか参照とかに変数として使う場合はint型同様に*の数は正確に書かなければなりません。

---

関数ポインタの定義は長くなるので１度しか書かないのであればそれほど面倒でもありませんが、何度も書かなければならなかったり、ある関数の引数として用いたりすると見づらかったりします。そこで**typedef**を使って型定義をします。

```c:input [14]
typedef int (*addf)(int);
addf add_func = &add1;
addf *add_func2 = &add_func;
printf("add_func  = %d\n", add_func(5));
add_func = &add2;
printf("add_func2 = %d\n", (*add_func2)(5));
```

```:output [14]
enter add1
add_func  = 6
enter add2
add_func2 = 7
```

**typedef**の書き方は関数ポインタの書き方と同じで、変数名が型名になるだけです。

- typedef 戻り値の型 (\*型名)(仮引数1の型,...);

型名を使う場合はint型を使うのとほぼ同じです。このように型を定義して使って関数呼び出しをするとシングルポインタは\*なし、ダブルポインタは\*1個のほうが理解しやすい。もちろん`(*add_func)(...)`とか`(**add_func2)(...)`とも書けるが何か不自然に感じる。


# 終わりに

これでポインタについてはある程度使えるようになったと思います。さらに、メモリの動的割当(**malloc**)などありますが、これはポインタというよりメモリ管理の理解が重要だと思いますので省きました。
C++には参照(&)がありますがこれも内部的にはポインタを使っていると考えれば理解しやすいのではないでしょうか。