概要
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
関数の仮引数のポインタ
int型
ポインタ実験3で実験したようにconstを含めたポインタのバリエーションを試す。
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 = #
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);
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のアドレスとすべての仮引数のアドレスは異なっている。
アドレスが渡されると、間接参照よって元の値の変更が可能になる。以下、それを実験する。
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 = #
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);
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にして同じように実験する。
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 = #
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);
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でコンパイルしてみました。
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 = #
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;
}
> 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が合っていない引数を除いて実行します。
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 = #
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);
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で実験したように*の数を増やしてダブルポインタ、トリプルポインタの実験するのも良いかもしれません。
1次元配列
1次元配列とconstの組み合わせの仮引数について実験します。
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);
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
を使って書き換えを行います。
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]);
}
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については省きます。
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);
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
です。
int main(int argc, char *argv[]) {
return 0;
}
argv
はポインタの配列です。そしてargc
が要素数です。さらに今まで見てきたようにargvはchar **argv
と書いても問題なく使えます。
構造体(struct)
構造体は関数の引数に使う場合は注意を要することがあります。
まずは以下のコードを実行してみます。
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]);
}
arry[0] = 1
arry[1] = 2
arry[2] = 3
arry[3] = 4
arry[4] = 5
同一構造体どうしの代入が可能で、値がコピーされます。通常、配列から配列に1回でコピーすることが出来ず、要素ごとに代入(コピー)が必要です。
そこで構造体を関数の引数にしたときの実験を次のように行います。
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]);
}
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セルに1関数しか定義できないようなので複数セルを使って関数定義をします。
int add1(int n) {
printf("enter add1\n");
return n + 1;
}
int add2(int n) {
printf("enter add2\n");
return n + 2;
}
int (*add_func)(int) = &add1;
printf("add_func = %d\n", add_func(5));
add_func = &add2;
printf("add_func = %d\n", (*add_func)(5));
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(...)
では意味が異なります。
次は関数のダブルポインタを実験します。
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));
enter add1
add_func2 = 6
enter add2
add_func2 = 7
関数の呼び出しに*が1個でも2個でも良いというのはちょっと紛らわしいと思います。しかし、呼び出し以外に代入とか参照とかに変数として使う場合はint型同様に*の数は正確に書かなければなりません。
関数ポインタの定義は長くなるので1度しか書かないのであればそれほど面倒でもありませんが、何度も書かなければならなかったり、ある関数の引数として用いたりすると見づらかったりします。そこでtypedefを使って型定義をします。
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));
enter add1
add_func = 6
enter add2
add_func2 = 7
typedefの書き方は関数ポインタの書き方と同じで、変数名が型名になるだけです。
- typedef 戻り値の型 (*型名)(仮引数1の型,...);
型名を使う場合はint型を使うのとほぼ同じです。このように型を定義して使って関数呼び出しをするとシングルポインタは*なし、ダブルポインタは*1個のほうが理解しやすい。もちろん(*add_func)(...)
とか(**add_func2)(...)
とも書けるが何か不自然に感じる。
終わりに
これでポインタについてはある程度使えるようになったと思います。さらに、メモリの動的割当(malloc)などありますが、これはポインタというよりメモリ管理の理解が重要だと思いますので省きました。
C++には参照(&)がありますがこれも内部的にはポインタを使っていると考えれば理解しやすいのではないでしょうか。