読者です 読者をやめる 読者になる 読者になる

気まま研究所ブログ

思ったことをてきとーに書きます。

ポインタのお話

C プログラミング ポインタ

こんにちは、涼月蒼菜です。
まず全く関係ない話なんですが、語尾をですますで統一します。
今まで気分で使ってましたが、統一感がなさすぎました。

さて、本題です。
知り合いと話していると、ポインタって何だよという話になったので記事として載せることにしました。
私自身勉強中の身なので間違いなどがありましたらご指摘いただけると幸いです。

なお、記載しているソースのincludeは全てstdio.hのみ(一部追加あり)です。

#include <stdio.h>





ポインタって?

C言語を使う上で避けて通れないのがポインタです。
ポインタを使わないC言語って何?というくらい便利で面白い機能です。
しかしながら、初学者にとってはなかなか理解できない機能であるもの事実です。
一体ポインタとは何なのでしょうか。

定義としてのポインタは、メモリのアドレスを記憶しておく変数です。
もっとわかりやすく言うと、値を保存している場所を保存している変数です。

全くわかりやすくないですね。
コンピュータにはRAMが搭載されています。そのメモリーには連続したアドレス、つまり番地が割り当てられています。
変数は名前と値をこのメモリ上のどこかにOSによって割り当てられ、それを参照することで値の変更や取得ができます。
ポインタはそういった割り当てられるアドレスを記録し、直接触るための変数です。


ポインタの宣言

百聞は一見にしかず、実際にコードで見ていきましょう。

ポインタの宣言は変数宣言の際に型または変数名に*を付け加えるだけです。
なお、どちらにつけても意味は同じです。

int *p;
int* p;

なお、このままではポインタのアドレスを格納するための領域が確保されるだけその中身は不明です。
このまま値の変更をかけると不明なアドレスに変更をかけることになり、予測不能な場所の値を書き換えることになります。
宣言の後は必ず正しいアドレスを格納する必要があります。


蛇足ですが、連続宣言をする際は要注意です。

int* p, p2;

一見ポインタ型を2つ宣言しているように見えますが、int*とintで宣言されます。
分解すると

int* p;
int p2;

このように宣言されることになるので気をつけてください。
もし連続宣言をしたい場合は

int *p, *p2;

とする必要があります。




ポインタの利用

上を踏まえて、ポインタにアドレスを格納します。

int main() {
    int i = 0; // ローカル変数iの宣言
    int *p; // ポインタpの宣言
    p = &i; // ポインタpにiのアドレスを代入
    printf("アドレス: i = %p, p = %p\n", &i, p);
    printf("値: i = %d, p = %d\n", i, *p);

    return 0;
}
アドレス: i = 0041F790, p = 0041F790
値: i = 0, p = 0

3行目で&を通常の変数の前に置いて代入演算子(=)で代入しています。
この時の&をアドレス演算子といい、変数がある場所のメモリアドレスを返します。
もっと言うと、アドレス演算子は対応する型のポインタ型を返します。
これを踏まえて、3行目ではポインタpにiのアドレスを代入しています。
結果を見ても同じアドレスと値を示していますね。
なお、実行環境によってアドレスの値は変わるので違ってもあまり気にしないでください。

f:id:AonaSuzutsuki:20160213023032p:plain

これでポインタpの初期化は完了しました。
この時、pにはiのアドレスが格納されていますので、pの値を変更するとiの値も変わります。

int main() {
    int i = 0; // ローカル変数iの宣言
    int *p; // ポインタpの宣言
    p = &i; // ポインタpにiのアドレスを代入
    printf("アドレス: i = %p, p = %p\n", &i, p);
    printf("値: i = %d, p = %d\n", i, *p);

    *p = 1; // pの持つアドレス内の値を1に変更
    printf("値: i = %d, p = %d\n", i, *p);

    return 0;
}
アドレス: i = 004CFD44, p = 004CFD44
値: i = 0, p = 0
値: i = 1, p = 1

iの値も1になったと思います。
これはpはiのアドレスを保持しており、そのアドレスにある値を変更したのでiも変更されます。
pとiは見かけ上違いますが、指し示す場所は同じです。
つまり、pとiは同じものという認識で問題ありません。
なお、アドレスの場所にある値を変更するまたは取得する時に用いる*は間接演算子と言います。

f:id:AonaSuzutsuki:20160213023034p:plain

更に変更してみましょう。

int main() {
    int i = 0; // ローカル変数iの宣言
    int *p; // ポインタの宣言
    p = &i; // ポインタにiのアドレスを代入
    printf("アドレス: i = %p, p = %p\n", &i, p);
    printf("値: i = %d, p = %d\n", i, *p);

    *p = 1; // pのアドレスの値を1に変更
    printf("i = %d, p = %d\n", i, *p);

    i = 2;
    printf("i = %d, p = %d\n", i, *p);

    return 0;
}
アドレス: i = 0022FB54, p = 0022FB54
値: i = 0, p = 0
i = 1, p = 1
i = 2, p = 2

次はiの値を変更してみました。
結果はiもpも2になりましたね。
聞き飽きたかもしれませんが、pはiのアドレスを指している、つまり、pはiであるためpを変更すればiを変更することになります。
もちろんその逆もです。


長くなりましたが、まとめます。

  • ポインタとはメモリアドレスを記録しておく変数
  • ポインタの宣言は型にを付けるか、変数名にを付けることででできる
int *p;
int* p;
  • ポインタへアドレスを渡す場合は渡す側にアドレス演算子&を付ける
int i = 0;
p = &i;
  • ポインタが持つアドレス内の値を参照する場合は間接演算子*を付ける
*p = 1;

これらがポインタの基礎的部分です。
これらを応用するとより機能的なコードが書けるのでおさえておきましょう。


ポインタって実際何に使うの?

ここまでポインタについて説明してきましたが、実際ポインタなんて使わなくてもプログラムを書くことができます。
ではなぜポインタを使うのでしょうか。
簡単に言うと、効率が良いので使います。
ポインタはメモリの値を直接管理できるので簡潔なコードを書くことができます。
また、パフォーマンス向上も図ることができます。

例えば、参照渡しを行いたい時に効果を発揮します。
C言語の関数の引数は値渡しのみサポートしており、参照渡しを行うにはポインタを使うかグローバル変数で実現する(参照渡しではありませんが)くらいしかありません。
参照渡しを用いると関数に生のデータを渡すことができ、生のデータが必要な場合や巨大なデータを効率よく扱うことができます。
※参照渡しと値渡しは最下部にて説明しています。

void test(int *arg) {
    *arg = 1;
}
int main() {
    int num = 0;
    test(&num);
    printf("num = %d\n", num);
    return 0;
}
num = 1

test関数は引数にポインタを受け付けるようになっており、関数内でのargは引数として渡された変数のアドレスを持ちます。
今回はnumのことを指していますおり、このコードを実行するとnumはtest関数が実行された段階で1となります。

もう少し例をあげてみましょう。
参照渡しを応用することで複数の値を関数から取得することができます。

void test(int *arg1, int *arg2) {
    *arg1 = 1;
    *arg2 = 2;
}
int main() {
    int num1 = 0;
    int num2 = 0;
    test(&num1, &num2);
    printf("num1 = %d, num2 = %d\n", num1, num2);
    return 0;
}
num1 = 1, num2 = 2

やってることは先ほどと全く同じです。
複数の値を返したい場合にこれを用いると実現できます。

また、配列を渡す時でも使えます。

// int nums[]でも可
void test(int *nums, int lenght) {
    for (int i = 0; i < lenght; ++i) {
        printf("nums[%d] = %d\n", i, nums[i]);
    }
}
int main() {
    int i[2] = { 0, 1 };
    test(i, 2);
    return 0;
}
nums[0] = 0
nums[1] = 1

このように、配列を渡す際もポインタが使えます。
実際は配列とポインタは別物ですが、性質上配列をポインタのように扱うことができます。
その逆も可能で、ポインタを配列のように扱うことも可能です。


こんなコードには要注意

少々難しい話になりますが、以下のようなコードには要注意です。

int* test() {
    int i = 0;
    int *p = &i;
    return p;
}
int main() {
    int *p = test();
    printf("*p = %d\n", p);
    return 0;
}
*p = 4062392

このコードはtest関数内でポインタにローカル変数のアドレスを代入していますが、実行すると予想とは違った挙動をする可能性があります。
というのも、ローカル変数はスタック領域と呼ばれるメモリの管理領域に割り当てられます。
スタック領域に生成された変数は関数から抜けると同時に未使用領域として割り当てられるため、どこかのタイミングでその領域の値が書き換えられる可能性があり、そのアドレスにある値は保証されません。
そのため、このままこのポインタを使うと値が保証されないアドレスにアクセスすることになるため予期せぬ不具合が発生するかもしれないというわけです。
もしかすると正しく動作するかもしれませんが、単にそのアドレスの値が書き換えられなかっただけの話であって、いつどこで値が書き換えられるかわからないので気をつけましょう。

もし上のコードを正しく動作させたい場合はmalloc関数で動的にメモリを確保するか静的宣言してしまうといいと思います。
以下はtest1をmalloc、test2を静的宣言とします。
なお、malloc関数を用いる場合はstdlib.hをincludeする必要があります。

int* test1() {
    int i = 0;
    int *p = (int *)malloc(sizeof(int));
    *p = i;
    return p;
}
int* test2() {
    static int i = 0;
    int *p = &i;
    return p;
}
int main() {
    int *p1 = test1();
    printf("*p1 = %d\n", *p1);
    free(p1);

    int *p2 = test2();
    printf("*p2 = %d\n", *p2);

    return 0;
}
*p1 = 0
*p2 = 0

これで関数から出た後も値は保持され、予期せぬ不具合は起こりません。
もう少し見てみると、static修飾子をつけた変数は静的領域に割り当てられ、プログラム終了まで残り続けます。
malloc関数はヒープ領域に必要な数だけメモリを自由に確保できます値の寿命はプログラマが決めることができます。
なお、malloc関数で確保したメモリは必ずfree関数で解放してください。
プログラムが終了するまで永遠に残り続けます。

しかしながら、あまり多用しすぎるとまた別の問題も起きます。
staticは静的領域に割り当てられるため、あまりに宣言の量が多すぎると無駄なリソース消費になりえます。
mallocはヒープ領域に割り当てられ、アプリケーション実行中の管理はプログラマに一任されます。つまり、解放処理などをしっかりしなければメモリリークや不正なアクセスを起こしかねません。
また、mallocを多用しすぎるとヒープ領域で断片化が起き、あるタイミングで確保できなくなる可能性があります。
ただ、この辺はmalloc関数がなんとかしてるみたいです。

一例としてあげましたが、スタック領域で事足りる今回のようなプログラムでは利用すべきではないように思えます。
戻り値を利用する方法が最適です。
もちろん使う必要性があるのであれば使うといいと思いますし、使う必要性があると思った時に使うという感じでいいと思います。


参照渡しと値渡し

かなり後回しになりましたが、参照渡しと値渡しです。
蛇足ですので飛ばしても問題ありません。

値渡しと参照渡しの違いは値を渡すかアドレスを渡すかの違いです。
要するに、値渡しの場合は、引数で渡した際に新たに値がコピーされるのでその変数を弄っても渡した側の値は変わりません。
対して参照渡しはアドレスを渡すので、その引数の値を弄ると渡した側も変更されます。
実際はC言語においてはどちらも値渡しといえますが、アドレスを渡すことを参照渡しと呼んでいます。

上で巨大なデータにも使えると書きましたが、実は一定数を超えるデータは値渡しでは渡せないんです。
値渡しの場合引数の値はスタック領域に積まれていきますが、スタック領域には上限があるため、あまりに大きいデータは入りきらずにオーバーフローしてしまいます。
これをスタックオーバーフローと呼びます。
こういった場合はmalloc関数でヒープ領域に作成するか静的宣言で静的領域に作成し、参照渡しで渡すことが望ましいです。


おわりに

以上がポインタの基礎部分と応用的な使い方でした。
私自身ポインタで苦戦した覚えがあるので同じ境遇の方の助けになればと思います。