写真 プログラミングの授業でC言語のポインタが分からないワテの妹(イメージ)
C言語のポインタは鬼門だ。
ポインタが理解出来なくて挫折する人も多いだろう。
ワテの知る限り、C言語系以外の言語にはポインタと言うものが無い。
(注:Fortran 90以降はポインタが使えるようになったらしい)
この記事では、自称プログラミングの変人のワテが、
- 何故C言語にはポインタが登場するのか?
- ポインタとは何なのか?
- ポインタはどういう時に使うのか?
- ポインタを使うと何が良いのか?
- ポインタを使わずにプログラムを書いても良いのか?
などを解説したい。
世の中には、ポインタを説明する為にマンションとかアパートなどの集合住宅の例え話で解説している例を見掛ける。
ワテに言わせれば、そんな説明は逆効果だ。益々ポインタを分かり辛くしている。そんな例え話はポインタを理解出来ている人なら理解出来る話であり、これからポインタを学ぶ人に対しては、混乱の元。
そんな例え話では無くて、歴史的に見てポインタが何故登場したのか?何のために登場したのか?その辺りの事実を理解する事が、ポインタ理解の最短コースだとワテは思うのだ。
当記事では、そのようにポインタの歴史をストレートに解説している。
今までいろんな本を読んでもポインタが理解出来ないと言う人でも、この記事を読んで頂ければ、ポインタを理解する事が出来るだろう(たぶん)。
もし理解出来ない場合には、最低でも10回は読み直して頂きたい。
それでも分からないと言う人は、、、さらに10回読み直して頂きたい。
以降、無限ループ。
つまり、理解出来るまで読み返して頂きたい。
いつかは分るようになるだろう。
ほんまかいな!?
では、本題に入ろう。
C言語を習う前に他の言語を学習するほうが良いかも
初めて習う言語がC言語の人は、新しい言語を習うと同時にC言語特有のポインタも理解しなくてはならないので、二重の難しさがある。
出来れば、C言語以外のポインタが登場しない他のプログラミング言語を有る程度はマスターしてからC言語を学習するのが良いかも知れない。
例えば、手軽に学べる言語としては、JavaScript、C#、VB.NETその他、色々ある。
注意:C#の場合は、C言語系なので実はポインタを使う事も可能だ。しかしながら、ワテの経験で言うと、C#で普通にプログラミングをする限りは、ポインタ変数などは一切使わなくても困らない。
しかしまあ、ワテの意見としては、これからプログラミングを学習する人が、最初に学習する言語としてC言語を選んでも問題は無いと思う。
人間、やる気になれば何とかなるもんだ。
世界のホームラン王、元プロ野球選手の王貞治さんの言葉を紹介したい。
「努力が報われないことなどあるだろうか。
報われない努力があるとすれば、それはまだ努力とは呼べない」
安枝新伍さん著『野球魂―素顔の王監督』より
まあ、これくらいの覚悟が無いと868本もホームランは打てないんだろうなあ。
いずれにしても、自分のパソコンにプログラミング開発作業が出来る学習環境を整える事が重要だ。
ワテの場合は、開発環境は以下の通り。
- Windows 10 Pro (64)
- マイクロソフト社のVisual Studio 2017 Community(無料)
- 良く使うのはC#, C++, JavaScript, TypeScriptなど
を使っている。
VS2017では、JavaScript、C#、VB.NET、C、C++、Python、その他色々な言語が利用出来る。
その辺りのセットアップ方法に関しては、別記事で詳細に紹介しているので参考にして頂きたい。
プログラミングの基本
さて、プログラミングの基本は、以下の三つに単純化出来る。
- 変数を定義して、そこにデータを入れる
- if文による条件分岐
- forループによる繰り返し処理でデータを加工
これらの基本処理を組み合わせれば、原理的にはどんな複雑な処理でも実現できると思う(ワテの理解では)。
変数には単純変数と配列変数がある。
例えばC言語では、
int n;
と宣言すれば整数型の単純変数 n を宣言出来る。
配列変数なら、
int arr[10];
と宣言すれば、10個の部屋が確保されてその中に整数値を保管出来る。
まあ、これはそんなに難しい概念ではないから、プログラミング初心者の人でも違和感なく理解出来るだろう。
さて、あなたがそんな風に気軽に変数を宣言した場合には、C言語のコンパイラさんはその変数を管理しなくてはならない。
具体的には、単純変数nの値を保管する場所をメモリ上のどこかに確保する。
同じく、配列変数 arr[10] の値を保管する場所をメモリ上のどこかに確保する。
この場合には、整数型10要素の配列なので、整数10個分の領域を確保する必要がある。
それらの変数管理作業は、コンパイルした実行プログラムを実行した直後から行われる。
変数はメモリ上にアドレス(番地)で管理されている
さて、あなたのプログラムの中で、
n=100;
を実行した場合には、nの値を保管しているメモリの場所に100と言う整数が書き込まれる。
この辺りの処理は、C言語に限らずどんな言語でも同じである。
では、nの値はメモリのどこの場所に保管されるのか?
普通はそんな事は気にしなくてもプログラムは書ける。
だから、そんな事を気にせずに単純変数でも配列変数でも好きなだけバンバン宣言して使いまくれば良い。
100個でも1000個でも好きなだけ宣言すれば良い。
例えば1000個の変数を宣言したとすると、Cコンパイラが生成する実行プログラムは、それが実行されたらまずメモリ上にそれらの変数の保管するために、1000個分の領域を確保する。
後は、あなたはその変数に値を代入したり、値を読み出したり自由に出来る。
そう言う操作をした場合には、実行プログラムさんは最初に確保したメモリ上のどこに変数の値を保持しているのか知っているので、そこから値を取り出したり書き込んだりしているのだ。裏方の作業と言っても良いだろう。
繰り返しになるが、このようにメモリ上に変数領域を確保して、そこに変数の値を保持して管理する仕組みは、C言語に限らず他の言語でもまあ大体同じである。
ここまでの説明で理解しておく事は、C言語に限らずどんなプログラミング言語でも、あなたが宣言した変数はメモリ上のどこかの番地に保管されている。
その辺りの変数管理を実行プログラムが上手い具合にやってくれている。
何故C言語にはポインタが登場するのか?
さて、C言語特有のポインタはここから登場する。
ポインタとは何なのか?
あなたが変数 n=100 とした場合に、その100の値がメモリ上のどこの場所(アドレス)に保管されているのかを知る手段がポインタである。
nの中身がメモリ上のどこに保管されているのかなど、関心が無い人も多いだろう。
確かにC言語以外の言語なら、nの中身がメモリ上の何番地のアドレスにあるのかを知りたくても、分からない場合のほうが多い。つまり知る手段が提供されていないのだ。
でも、C言語ではメモリ上の何番地に保管されているのか知る事が出来るのだ。
アドレスを知るだけでなく、そのアドレスの値を読み書き出来るのだ。
では、何の為に変数nがメモリ上の何番地のアドレスに保管されているのか知りたいのか?
何のためにメモリ上のアドレスを指定して値を読み書きしたいのか?
その辺りを解説しよう。
何のために変数のアドレスが知りたいのか?
元々C言語はUNIXオペレーティングシステムを記述するために開発された言語だ。
UNIXオペレーティングシステムは、複数のプロセスがあたかも同時に並列実行されているかのように出来る機能を持っている。
マルチプロセスとかマルチタスクと言われる技術だ。
WindowsでもMac OS(今はOS Xか)でも採用している技術だ。
実プロセッサは一個だけだが、そのプロセッサを短時間の間に各プロセスに少しずつ振り分けて、あたかも並列に実行しているかのように見せかける技術。
それがマルチタスクだ。
例えば三つのプロセスをマルチタスクで動かすなら、こんな感じか。
プロセスA → プロセスB → プロセスC → プロセスA → プロセスB → プロセスC → …
こんな切り替えをCPUのクロックの時間間隔で行っているのだ。具体的には、昔の8086CPUの頃ならMHzのオーダー、今のインテルプロセッサならGHzのオーダーか。
ハードウェア寄りの制御を行う為にはアドレス情報は必須
そういうハードウェア寄りの制御を行う為には、例えば実メモリ上の1234番地にあるデータを別の5678番地に移動するなどの処理が必要になってくる事は、お分かり頂けると思う。
あるいは、3456番地のデータをCPUのXレジスタに読み込むとか、逆に、CPUのXレジスタの中身を3456番地に書くなどの機能も必要になる。
何故かと言うと、CPUは今までプロセスAさんの仕事をしていたけれど、次のサイクルではプロセスBさんの仕事をする訳なので、プロセスAさんに関するデータは一旦メモリ上に退避しておくなどの必要性が出て来る。
こんな感じで、マルチタスク仕様のオペレーティングシステムは、CPUとメモリの間でデータをコピーしたり、移動したり、上書きしたりと休む間もなく働いているのだ。
それによって、見かけ上は複数のプロセスが同時に動いているように見える。
UNIXオペレーティングシステムはそう言うローレベルな制御をして、実メモリや実CPUを多数のプロセス間でタイムシェアリングしている。
なお、ここで言う「ローレベルな制御」とはハードウェア寄りの制御という意味であり、機能が低レベルな制御などと言う意味では無い。
さて、そう言う機能を持つUNIXオペレーティングシステムを書くために設計されたC言語であるから、メモリの何番地のデータを読み出すとか、逆にメモリの何番地にデータを書き込むと言ったローレベルな機能が必要になる事は言うまでも無い。
C言語の設計者であるデニスリッチーさんに直接聞いた訳ではないが、ワテの推測を交えて解説するなら、そう言う歴史的背景が有ってC言語にはポインタが導入されたのだ。
ポインタはどういう時に使うのか?
まあ、C言語に於いてはポインタを使わなくてもプログラムを書こうと思えば書けなくは無い。
つまり、普通に単純変数と配列変数を宣言して利用すれば良い。
ポインタが必要無ければ利用しなくても構わない。
int n=100; int arr[10]; for(int n=0;n<10;n++){ arr[n] = n; }
こんな風に書けば良い。
どこにもポインタは登場しない。
ポインタを使うと何が良いのか?
しかしながら、C言語に慣れて来るとポインタを使うほうがスッキリ書ける場面も多い。
ワテの知っている範囲で言うと、具体的には、以下の通り。
- 関数呼び出しの引数に配列を渡したい場合
- 配列を動的に確保したい場合
- ツリー構造やリスト構造を作成したい場合
などか。
これら以外にも沢山あるかも知れないので思い出したらここに追記したい。
関数呼び出しの引数に配列を渡したい場合
例えば以下のようにmain関数からsum_funcと言う関数を呼び出す。
sum_func関数の引数には配列データを渡したい。
int sum_func(int arr[10]); // プロトタイプ宣言 int main() { int n; int arr[10]; int sum = 0; for (int n = 0; n<10; n++) { arr[n] = n; } sum = sum_func(arr); printf("sum=%d\n", sum); // sum=45 と表示される。 return 0; } int sum_func(int arr[10]) { // arr[10] 固定なので使い辛い int sum = 0; int n; for (int n = 0; n<10; n++) { sum += arr[n]; } return sum; }
こんな感じか。
これを実行すると0~9までの総和45が求まる。
しかし、上記のコードには幾つも問題がある。
具体的には、sum_func()の引数に arr[10] のように数字10が固定されている。
また、sum_funcの中身にも n<10 のように定数10が埋め込まれている。
これだと汎用性が無い。
その部分を改良したのが以下の関数だ。
配列の大きさを二番目の引数で与える方式にした。
int sum_func2(int arr[], int arrSize); // プロトタイプ宣言 int main() { int n; int arr[10]; int sum = 0; for (int n = 0; n<10; n++) { arr[n] = n; } sum = sum_func2(arr,10); printf("sum=%d\n", sum); // sum=45 と表示される。 return 0; } int sum_func2(int arr[], int arrSize) { // arr[] に変えた int sum = 0; int n; for (int n = 0; n<arrSize; n++) { sum += arr[n]; } return sum; }
まあ、こちらのほうが汎用性が上がったので、使い勝手は良い。
この時点でもまだポインタは登場していない。
この例では、一次元配列を扱っているだけなので、ポインタを使わなくてもプログラムを書く事が可能だ。
関数呼び出しの引数に配列を渡したい場合(ポインタ変数を使う)
それを敢えてポインタを使って書き換えると以下の通り。
int sum_func3(int *parr, int arrSize); // プロトタイプ宣言 int main() { int n; int arr[10]; int sum = 0; for (int n = 0; n<10; n++) { arr[n] = n; } sum = sum_func3(arr, 10); printf("sum=%d\n", sum); // sum=45 と表示される。 return 0; } int sum_func3(int *parr, int arrSize) { int sum = 0; int n; for (int n = 0; n<arrSize; n++) { sum += parr[n]; } return sum; }
先ほどのコードとの違いは、sum_func3()の引数の部分に初めてポインタが登場した。
int *parr
だ。
整数型のポインタである。
つまり parrの値には、メモリ上の何番地と言うアドレスの値が入っている。
今の例では、main関数内で以下の様に呼び出している。
sum = sum_func3(arr, 10);
arr[10] として宣言した配列の名前 arr を単体で使うと、それはその配列の先頭アドレスを指す事はC言語では決まっている。
その先頭アドレスを渡して sum_func3()をコール。
一方、呼び出されたsum_func3()側では
sum += parr[n];
のように利用している。
この parr[n] としている部分が arr[n] の値になるのだ。
まあ要するにmainで宣言した arr[10]の arr もsub_func3()の parr も同じで、両者は整数型10要素の配列の先頭アドレス(つまり arr[0]のアドレス)を指している。
sum += parr[n]; // これでも良い sum += *(parr + n); // これでも良い
要するにポインタの先頭にアスタリスク記号*を付けるとポインタが指すアドレスの中身の値を意味するので、これらの二つの記述方法は同じものになるのだ。
配列を動的に確保したい場合
C言語でメモリを動的に確保して、使い終わったら解放する場合にはmallocとfreeが使われる。C++の場合なら newとdeleteに相当する関数だ。
mallocでメモリ上に確保した10個の整数領域を配列のように扱って今までと同じsumを求める計算をしてみた。
void malloc1(){ int arrSize = 10; int *p = (int*)malloc(sizeof(int) * arrSize); // 整数10個分のメモリを確保 int sum = 0; for (int n = 0; n < arrSize; n++) { p[n] = n; sum += p[n]; // pを配列のように扱える。 // sum += *(p + n); // これでも良い。 } printf("sum=%d\n", sum); // sum=45 と表示される。 free(p); // 確保したメモリを解放 }
この例では、一次元の領域をmallocで確保しているが、興味のある人は二次元配列をmallocで確保してみると良いだろう。
ポインタの勉強にはmalloc/freeを使いこなすのが一番だ。
ツリー構造やリスト構造を作成したい場合
双方向のリストを作ってみた。
struct MyStruct { MyStruct *prev; MyStruct *next; int data; }; void struct1() { MyStruct *p1 = (MyStruct*)malloc(sizeof(MyStruct)); MyStruct *p2 = (MyStruct*)malloc(sizeof(MyStruct)); MyStruct *p3 = (MyStruct*)malloc(sizeof(MyStruct)); p1->prev = NULL; p1->next = p2; p1->data = 1; p2->prev = p1; p2->next = p3; p2->data = 2; p3->prev = p2; p3->next = NULL; p3->data = 3; MyStruct*p_next; MyStruct*p = p1; while (p) { p_next = p->next; free(p); p = p_next; } }
まあ、双方向のリストを作って直ぐに解放しているので何もしないプログラムである。
C言語でポインタを使ってリスト構造やツリー構造を作ると、ポインタの理解には最適だと思う。
なぜなら、デバッガーで追う時にポインタがメモリの何番地を指しているのかを意識せざるを得ない。
例えば、mallocで確保したポインタの解放忘れがあると、メモリリークチェッカーを有効化してあれば解放忘れを検出してくれる。
上記コードの free(p) の行を無効化して実験してみた。
Visual StudioのCコンパイラの場合には、プログラムの冒頭で以下のように宣言しておいて、
#define _CRTDBG_MAP_ALLOC #include <stdlib.h> #include <crtdbg.h>
main関数の末尾で以下の関数を実行すれば良い。
_CrtDumpMemoryLeaks();
そうすると、Visual StudioのIDEの出力ウインドウに以下のように表示される。
Detected memory leaks! Dumping objects -> e:(プロジェクトのパス)\2017-05-16-ポインタとはconsoleapp.cpp(118) : {86} normal block at 0x00C9A060, 12 bytes long. Data: < > A0 9E C9 00 00 00 00 00 03 00 00 00 e:(プロジェクトのパス)\2017-05-16-ポインタとはconsoleapp.cpp(117) : {85} normal block at 0x00C99EA0, 12 bytes long. Data: < ` > 98 D7 C8 00 60 A0 C9 00 02 00 00 00 e:(プロジェクトのパス)\2017-05-16-ポインタとはconsoleapp.cpp(116) : {84} normal block at 0x00C8D798, 12 bytes long. Data: < > 00 00 00 00 A0 9E C9 00 01 00 00 00 Object dump complete.
三つのポインタが解放されていない事が分る。
そのアドレスも表示されている。
このように、C言語でmallocやfreeを使ってポインタを扱っていると、メモリの何番地に何を書き込んだのかなどを常に意識しておく必要があるからポインタの理解にはとても役立つ。
ポインタを使わずにプログラムを書いても良いのか?
まあ、現実的にはそれは無理だろう。
ポインタは至る所に登場する。
自分はポインタが良く分からないのでポインタを一切使わないでプログラムを書くとしても、他人のプログラムの中にはポインタが普通に登場するだろう。
なので、C言語やC++を本格的にやるのなら、一刻も早くポインタを理解する必要がある。
ポインタを理解する為の課題
多次元配列を関数に渡すプログラムを書く
例えば、二次元の配列
int arr[10][20];
にデータを代入して、その総和を求めるsub関数を作ってみる。
引数で一次元の配列を受け渡す例はこの記事で紹介した。
一方、二次元以上の配列を関数の引数で受け渡す場合には、ポインタ変数を使わざるを得ない。
なので、その辺りを自分で試してみると、C言語のポインタを理解出来るだろう。
時間が有ったので自分で作ってみた。
二次元配列を関数に渡す例
#include <iostream> #define NX 3 #define NY 2 int arr[NX][NY]; void func1(int a[][NY]) { for (int x = 0; x < NX; x++) { for (int y = 0; y < NY; y++) { a[x][y] = x * 1000 + y; } } } void func2(int(*ap)[NY]) { for (int x = 0; x < NX; x++) { for (int y = 0; y < NY; y++) { ap[x][y] = x * 1000 + y; } } } void dump_two_dim_arr(int(*ap)[NY]) { printf("Function name: %s\n", __FUNCTION__); for (int x = 0; x < NX; x++) { for (int y = 0; y < NY; y++) { printf("%10d", ap[x][y]); } printf("\n"); } } int main() { func1(arr); dump_two_dim_arr(arr); func2(arr); dump_two_dim_arr(arr); }
二次元配列を関数に渡す例だ。
二通りの記述が出来るのでfunc1とfunc2を作った。
あとは、関数内で配列のデータを取り出して使えば良い。
では、三次元配列を渡す場合はどうなるのか?
三次元配列を関数に渡す場合はどうなるのか?
まあ、ワテの場合は三次元配列を使う事は滅多に無い。
とりあえず即席で下のコードを作ったが、まあ、間違いは無いと思うが。
forループなどは省略している。
#define NX 4 #define NY 3 #define NZ 2 int arr[NX][NY][NZ]; void func1(int(a[])[NY][NZ]) { a[x][y][z] = x * 10000 + y * 100 + z; } void func2(int(*ap)[NY][NZ]) { ap[x][y][z] = x * 10000 + y * 100 + z; } int main() { func1(arr); func2(arr); }
こんな感じか。
二次元配列を関数に渡す例でも、三次元配列を関数に渡す例でも、NYやNZの大きさを引数に指定する必要があり、それらを角カッコ[]で囲うので見た目がごちゃごちゃしている感じ。
なのでワテの場合はこの方式はあまり好きではない。
もしワテが三次元配列を使う必要が有る場面に遭遇した場合には、以下で紹介するmalloc/freeの方法を使うかな。
malloc/freeを使って動的に多次元配列を作成してみる
例えば、4x3x2=24個のdoubleをmallocで確保してみる。
#include <iostream> #define _CRTDBG_MAP_ALLOC #include <stdlib.h> #include <crtdbg.h> double ***malloc_3dim_array(int nx, int ny, int nz) { double ***ptr; int x, y; ptr = (double ***)malloc(nx * sizeof(double **)); for (x = 0; x < nx; x++) ptr[x] = (double **)malloc(ny * sizeof(double *)); for (x = 0; x < nx; x++) { for (y = 0; y < ny; y++) { ptr[x][y] = (double *)malloc(nz * sizeof(double)); } } return ptr; } void free_3dim_array(double ***ptr, int nx, int ny, int nz) { int x, y; for (x = 0; x < nx; x++) { for (y = 0; y < ny; y++) { free(ptr[x][y]); } } for (x = 0; x < nx; x++) free(ptr[x]); free(ptr); } void dump_3dim_array(double ***ptr, int nx, int ny, int nz) { for (int x = 0; x < nx; x++) { for (int y = 0; y < ny; y++) { for (int z = 0; z < nz; z++) { printf("%10.5f", ptr[x][y][z]); } printf("\n"); } printf("\n"); } } int main() { int nx = 4; int ny = 3; int nz = 2; double ***ptr = malloc_3dim_array(nx, ny, nz); dump_3dim_array(ptr, nx, ny, nz); for (int x = 0; x < nx; x++) { for (int y = 0; y < ny; y++) { for (int z = 0; z < nz; z++) { ptr[x][y][z] = 0.0; // 初期値 } } } dump_3dim_array(ptr, nx, ny, nz); free_3dim_array(ptr, nx, ny, nz); _CrtDumpMemoryLeaks(); // これを無効化するとメモリリークを確認出来る(出力ウインドウ) }
まあ、mallocで確保してfreeするだけの処理である。
確保した4x3x2=24個の部屋には、初期値0.0を代入している。
なお、配列とmallocで作成した配列モドキは厳密には異なるので注意が必要だ。
配列とmallocで作成した配列モドキは厳密には異なる
つまり、配列
int arr[4][3][2];
で確保した24個のintは、メモリ上に連続的に並んでいる。
arr[0][0][0] 0番目 arr[0][0][1] 1番目 arr[0][1][0] 2番目 arr[0][1][1] 3番目 arr[0][2][0] 4番目 arr[0][2][1] 5番目 ・・・ arr[3][2][1] 23番目
一方、mallocで確保した3次元領域は、mallocで確保した部分は連続してメモリ上に確保されている。
ptr = (double ***)malloc(nx * sizeof(double **)); // 一個のdouble**型ポインタ ptr[x] = (double **)malloc(ny * sizeof(double *)); // ny個の連続するdouble*型ポインタ領域 ptr[x][y] = (double *)malloc(nz * sizeof(double)); // nz個の連続するdouble領域
しかし、それらの領域が全てメモリ上に連続しているとは限らない。あくまで一つのmalloc関数で確保した領域は連続域として確保されているというだけである。
まあ、mallocのやり方を工夫すれば、全ての領域を連続してメモリ上に確保する事も出来なくはないが。
いずれにしもて、このmallocで確保したポインタを関数の引数で渡す場合には、配列の場合のような角カッコ[]が登場しないのでスッキリしている。
ワテの場合は、そう言う理由でmalloc方式を良く使う。
ポインタのポインタはポインタ
さて、ポインタを学び始めた当初のワテは、
double ***ptr = malloc_3dim_array(nx, ny, nz);
のように三つもポインタのアスタリスク記号*が書いてあると、もう訳分からんと言う感じだった。
たが、心配する事は無い。
ポインタの基本に戻れば良いのだ。
double dbl = 12.345; double *p1 = &dbl; double **p2 = &p1; double ***p3 = &p2;
アスタリスクが一個のp1はdouble型ポインタなのでdblのアドレスを代入出来る。
アスタリスクが二個のp2はdouble型ポインタのポインタなのでp1のアドレスを代入出来る。
アスタリスクが三個のp3はdouble型ポインタのポインタのポインタなのでp2のアドレスを代入出来る。
まあ要するに
ポインタのポインタ は ポインタ ポインタのポインタのポインタ は ポインタ ・・・
だ。
引っ越しは引っ越しのサカイ
みたいなもんか?
ちょっと違うか。
ここ笑うところ。
まとめ
C言語の鬼門であるポインタの解説をした。
- 何故C言語にはポインタが登場するのか?
- ポインタとは何なのか?
- ポインタはどういう時に使うのか?
- ポインタを使うと何が良いのか?
- ポインタを使わずにプログラムを書いても良いのか?
に関して、ワテ流の説明をした。
この記事を読んでもポインタが全く分からない人は、どうしたら良いかなあ?
それはワテには分からない。
ワテの説明が悪い可能性もある。
ワテの説明が間違っている可能性もある。
もし何かお気付きの点などありましたら、御遠慮なくご指摘下さい。
コメント
分かりやすいです!!!
助かりました!!!
今後とも応援しております
グッチ様
この度は小生のサイトにお越し頂きましてありがとうございました。
私のポインタ講座がグッチ様のお役に立てたようで、安心しました。
今後とも、分かり易い記事を執筆するように、精進したいと思います。