見出し画像

【C言語プログラミング9】ポインタはこうやって使うねん

前回はポインタの基本的な説明をしました。今回はポインタの使い方を書きます。前回のロッカーや番号札の話と繋げて書きますので、その頭で読み進めて下さい。私が普段の仕事でポインタを使うのは、ほぼワンパターンしかありません。それは、関数の引数としてポインタを渡すことです。

こんな時どうする?

void main(void)
{
    char sample[1000];
}

こんなコードがあったとします。これでchar型のsampleという名前の配列が1000バイトでメモリに配置されます。変数は宣言した時点では中身に何が入っているか分かりません。なので、sample[0]~[999]にはゴミが入っている状態です。一旦、配列の内容を全て0で初期化しようとした時、どうしましょうか?

void main(void)
{
    char sample[1000];
    int counter;
    
    for (counter = 0; counter < 1000; counter++) {
        sample[counter] = 0;
    }
}

まず思い付くのはこうでしょう。しかし、配列を初期化する場面って何回もあるでしょう。その度にforループを書くんか?それは面倒くさい。そんな時は関数を作ってしまえば良い。この配列(メモリ)を全部0で埋めてくれる関数を作りましょう。

さて、この全部0で埋めてくれる関数の引数はどうすれば良いでしょうか?関数に対して、「メモリの”ここ”から0を埋めてくれ!」とメモリの開始地点を指示しないといけません。では、”ここ”を伝えるにはどうすれば良いか?それがポインタです。メモリの”ここ”はアドレスで表現できます。そしてメモリのアドレスを格納できるのがポインタ変数です。0で埋めてくれる関数の名前をmy_memsetとします。プロトタイプ宣言は以下です。引数はchar型のポインタ変数です。

void my_memset(char *pointer);

my_memset関数を作る

void my_memset(char *pointer)
{
    int counter;
    
    for (counter = 0; counter < 1000; counter++) {
        *pointer = 0;    // pointerには配列のアドレス(番号札)が入っていて、その番号のロッカーに0を入れる
        pointer++;       // pointerのアドレス(ロッカーの番号)を一個進める
    }
}

このmy_memset関数の意味は分かりますか?引数にchar型のポインタ変数(カプセル)を受け取ります。ポインタ変数の中には配列の先頭アドレス(ロッカーの番号札)が入って来ます。そのアドレスの指し示す中身に対して0を入れています。*pointer = 0;これですね。そしてその後、pointer++;としていますね。これはアドレスを一つ進めている(隣のロッカーに移る)のです。これを1000回繰り返します。

my_memset関数を使う

では、my_memset関数をコールしてみましょう。

void main(void)
{
    char sample[1000];
    
    my_memset(&sample[0]);    // 配列の先頭アドレスを渡す時は&sample[0]とします
}

my_memset関数にはchar型のポインタを渡す必要があります。なので、配列の[0]番目のアドレスを引数に指定します。前回の記事ではsampleが配列ではなかったので&sampleとしていましたが、今回は配列なので&sample[0]になります。なお、配列は配列名 = 配列の先頭アドレスを表すので、以下のように記述することも出来ます。

my_memset(sample);

ただし、これだとこの一行だけを見た時に、sampleが何者なのか分からないので、私はあえて&sample[0]と記述するようにしています。こう記述すると、「ああ、sampleは配列でその先頭アドレスを引数にしているんだな」というのが一発で分かります。

my_memset関数の実行が終わると、sample[0]~[999]には0が埋まっている状態になります。如何でしょうか?理解できましたか?

え?sample配列ってローカル変数なんじゃねーの?

以前の記事に、変数の補足説明でローカル変数とグローバル変数について書きました。ローカル変数はその変数を宣言した関数の中でしか使えず、グローバル変数はどの関数からでも使えると言いました。じゃー、my_memsetでsampleの[0]~[999]に0が入れられるのはどう言うことやねん。確かに、sample配列はmain関数内で宣言されているので、別関数であるmy_memset関数から直接はアクセスできないです。

しかし、ポインタ経由ならアクセス出来るんです。言うなれば、sampleというのはロッカーに付けられた名前で、アドレスはロッカーに元々備わっている普遍的な番号なのです。高校時代って自分専用のロッカーってなかったですか?そのロッカーに自分の名前のプレート入れて一年間使いますよね。学年が上がると、そのロッカーは解放されて後輩が新たなプレートを入れて使いますね。プレートは毎年変わりますが、ロッカー自体の番号は変わりません。プレート = sampleでロッカー番号 = アドレスと言うことです。プレートの名前が何であれ、ロッカー番号さえ分かれば誰でもアクセスできるということです。「阿部君のロッカーの中身取り出して!」「阿部って誰?」となりますが、「ロッカー番号1の中身取り出して!」「了解!」という分けです。

そして、このアドレスさえ分かれば何とでも出来るのがC言語の便利な所であり、恐ろしいところでもあります。

ポインタの恐怖(メモリ破壊)

my_memset関数に少しヤバイ改造をしてみましょう。

void main(void)
{
    char sample[1000];
    
    my_memset(&sample[0]);
}

void my_memset(char *pointer)
{
    int counter;
    
    for (counter = 0; counter < 2000; counter++) {
        *pointer = 0;
        pointer++;
    }
}

さて、これを実行したらどうなるでしょう?完璧にメモリ破壊が起こります。main関数内で宣言したsample配列は1000バイトです。その先頭アドレスをmy_memset関数に渡し、my_memset関数では指定されたアドレスから順番に0を入れて行きます。途中まではルンルンで行けるでしょうが、ループが2000回になっています。sample配列は1000バイトなので、当然溢れますね。これがバッファオーバーフローという致命的なエラーになります。昔、Windowsのドライバを作ったことがありますが、これをやると一発でブルースクリーンになります。即死ですね。ポインタはそれぐらい危ないものなんです。

my_memset関数はこれでいいのか?

my_memsetを作りましたが、本当にこれで良いのか?sample配列が1000バイトで、埋める値が0だとこれで良いですが、配列が100バイトの場合もあれば、埋める値を1にしたい場合もありますよね。では、そんな色んな条件に対応できるようにしましょう。

void main(void)
{
    char sample[100];
    my_memset(&sample[0], 1, 100);
}

void my_memset(char *pointer, char data, int size)
{
    int counter;
    
    for (counter = 0; counter < size; counter++) {
        *pointer = data;
        pointer++;
    }
}

引数にchar dataとint sizeを追加しました。これで埋める値と配列のサイズを指定できるようになりました。今回、my_memset関数を作りましたが、実は#include <string.h>をプログラムの先頭に記述すれば、memsetと言う関数が使えるようになります。要するにどこかの誰かが既に用意してくれています。なお、memset関数の引数もmy_memset関数と同じです。恐らく内部処理はmy_memset関数と同じようなことをしているんでしょう。知らんけど。

もしも、ポインタが使えなければ

今回やったことと同じようなことをしようとして、ポインタを扱えなかったとしたらどうなるか?めちゃくちゃ不便ですよ。「ロッカーの開始番号言うから、その中に0埋めて行ってくれ!」みたいな話が出来ないと言うことです。やりようとしては、2つ以上の関数からアクセスする可能性のある変数は全てグローバル変数にするとか、そんな事態になりますね。

しかし、プロとしてやっている人の中には、「これ、なんでポインタ使わないんですか?」「ポインタよく分からないんで・・・」マジかよ!みたいな人も居ます。恐ろしいことに、エンジニアに免許は要りませんからね。本当にピンキリです。話は逸れましたが、試しにグローバル変数でsample配列を宣言した場合をやってみましょうか?

char sample[1000];
char sample2[100];
char sample3[50];

void main(void)
{
    my_memset(0, 1000);
}

void my_memset(char data, int size)
{
    int counter;
    
    for (counter = 0; counter < size; counter++) {
        sample[counter] = data;
    }
}

my_memset関数の引数は、埋める値とサイズになります。my_memset関数内のforループではsample[counter]に対してdataを入れています。sample配列はグローバル変数なのでmy_memset関数からも直接アクセスできます。しかし、これだとsample2配列に値を入れたい時、sample3配列に値を入れたい時使えませんね。forループの中で、sampleという配列名を使っているので、このmy_memset関数はsampleという配列に値を入れる専用関数になってしまいます。要するに全く汎用的ではありません。sample2やsample3配列に値を入れたい場合は、それ専用の関数を作んのかい!そんなアホな話はない。

今になって思うと、ポインタを使わずにプログラムを書けと言われると無理ですね。やりたいことが出来なかったり、無駄な処理を書かないといけなくなったりします。なので、どっかの誰かが作ってくれた関数の引数の多くは、ポインタになっているのです。どっかの誰かは、我々が宣言する変数名なんて絶対に予測することは出来ませんからね。「sampleって言う配列に0埋めて欲しいんで、よろしくお願いします。」と言っても「いや、sampleとか知らんし。アドレスで言うてくれる?」となるわけです。

引数のINとOUT

関数にはご存じ引数がありますね。引数には実は2種類あって、INとOUTがあります。なお、my_memset関数には3つの引数がありますが、2つがINで1つがOUTです。内訳は以下の通り。

OUT  :  char *pointer
IN  :  char data  
IN  :  int size

INの引数はコール先の関数の中で使ったら終わる引数のことです。OUTの引数はコール先の関数から戻って来た時に、内容が変化する引数のことです。my_memset関数の場合、pointerの指し示すアドレスの内容、つまりsample[0]~[999]の内容が0になりますね。引数にはこういう種類があるのだと思っておいて下さい。

最後にmy_memset関数を実行してみる

今回もVisual Studioを使ってプログラムの動きとメモリの変化を観察してみましょう。Visual Studioを起動したらF10を押して、5行目まで実行したのが以下の状態です。sample配列が宣言され、メモリ上に配置されていますね。画面右下にはsample配列の[0]~[22]までが表示されています。中身は-52がズラッと入っています。ゴミですね。なお、sample配列の先頭アドレスは今回0x008ff474です。次にF11を押します。F11はステップイン実行と言って、次の関数に入って行くことができます。

画像1

以下がステップインした状態です。右下のローカルウィンドウを見ると、引数のdata = 0、size = 1000、pointer = 0x008ff474となっていますね。きちんとpointer変数には配列の先頭アドレスが入っています。

画像2

デバッグ ⇒ ウィンドウ ⇒ メモリ ⇒ メモリ(1)でメモリウィンドウを開き、F10を押してforループの14行目まで実行したのが以下です。0x008ff474番地が0になりましたね。つまり、sample[0]に0が入ったことになります。

画像3

更にF10を押して15行目を実行します。左したのローカルウィンドウを拡大したものが以下です。pointer++;の行が実行されて、アドレスが一つ移動して、値が0x008ff4740x008ff475になっていますね。2回目のループでは0x008ff475番地に0が入ることになります。つまり、sample[1] = 0になると言うことです。

ポインタはこうつかうねん

このまま処理を続け、ループを1000回実行した結果が以下の通り。メモリウィンドウは綺麗に0になっていますね。また、ローカルウィンドウのsample配列も[999]まで0になっています。

画像5

まとめ

今回は、ポインタを関数の引数にした使い方を紹介しました。冒頭にも書きましたが、私がポインタを使うのは殆どこのパターンです。「アドレス渡すから、後上手いことやっといて~」ちゅう関数をよく作ります。どうですか?めっちゃ便利でしょ?やりたい放題っすよ。

ポインタに関して残っている話としては、構造体ポインタ、ポインタに型がある理由、リスト構造、それから関数ポインタですね。関数もメモリに配置されるので、当然アドレスがあります。なので関数のアドレスを入れる為の変数があり、それを関数ポインタと言うのです。関数ポインタ自体は大したことはないのですが、それを用いた究極奥義、状態遷移関数マトリックステーブルは凄いです。考えた人は天才やと思います。これらについても、この先書きたいと思います。今回はこの辺で。お疲れ様でした。

この記事が気に入ったらサポートをしてみませんか?