C言語のポインタのメリットとは?コピーしますかメモリを共有しますか

4, 2020-08-05

目次

C言語のポインタはこんなに便利

C言語ポインタメリットとはなんでしょうか?
結果から言うと、C言語のポインタには↓のようなメリットがあります。

  • プログラムが速くなる

  • 設計がシンプルになる

筆者はC言語を15年近く使っていますが、ポインタのこれらのメリットは肌身で感じるところです。
C言語のポインタを使うとプログラムが高速になり、設計がすっきりとシンプルになるんですね。
なぜそうなるのかと言うのはテストの結果や、リファクタリングをしたコードを見れば明らかです。

ポインタを使うことは開発効率の向上になり、保守性が上がり、問題の解決を高速にします。
多少、扱いには注意が必要ですが、ポインタを使うことはもはやC言語を使う者にとっての宿命とも言えるでしょう。
ポインタを使わなければ実現できないことも多数あります。

それでは、これらのポインタのメリットについて具体的に見ていきたいと思います。

プログラムが速くなる

C言語でポインタを使うと、プログラムを速くすることが出来ます
プログラムが速くなるというのは、プログラムの実行時間、問題解決の時間が短くなるということです。
そのためプログラムの使用者の時間を節約することができます。

C言語でポインタを使って書かれたプログラムは高速に動作します。
そのためC言語で書かれたプログラムは高速で、品質がいいことになります。
C++やRustなど、高速な言語は数えるほどですが、C言語で書かれたプログラムもそれらの言語で書かれたプログラムにも負けません。

特にインタプリタ、つまりRubyやPythonなどの動的言語に比べると、その差は歴然たるものです。
それほどC言語で書かれたプログラムというものは素早く、高速なんですね。
その速さの秘密にあるのが、このポインタという機能です。

これは一体、どういうことでしょうか。
これは具体的に言うと、ポインタを使うと構造体などのオブジェクトのコピーのコストが減るということです。

構造体のコピー

例えば↓のようなコードがあるとします。

struct animal {
    int age;
    int weight;
};

int main(void) {
    struct animal a = {1, 2};
    struct animal b = {3, 4};

    b = a;

    return 0;
}

struct animalという構造体を作り、main関数内でその変数a, bを定義しています。

struct animal {
    int age;
    int weight;
};
...
    struct animal a = {1, 2};
    struct animal b = {3, 4};
...

そして、構造体変数baを代入しています。

...
    b = a;
...

struct animal構造体にはint型のメンバ変数ageweightがあります。
つまり構造体の中にはint型の変数が2つあるわけですね。

このとき、上のコードで、変数bに変数aを代入する時に発生するコピーのコストは変数何個分でしょうか?
代入は1回分ですが、問題にしたいのはそのコストです。
構造体には変数がageweightで2つあるので、全体のコスト的には変数2個分になります。

変数2個分というのは、構造体変数1個のコピーのコストがint型の変数2個分かかるということです。
これは構造体のメンバ変数が増えるほど、コピーのコストが多くなります。

たとえばstruct animal構造体にint型の変数をもう10個追加したとします。
そうするとその構造体の構造体変数のコピーにかかるコストはいくらになるでしょうか?
すでにあるメンバ変数2個にプラス・メンバ変数が10個加わるので、そのコストは全体でint型の変数12個分になりますね。

このように構造体のメンバ変数の数と、そのメンバ変数の型というの変数のコピーのコストに直結するものです。
そしてメンバ変数の多い構造体変数のコピーは基本的にコストが大きくなると覚えておいてください。

ポインタのアドレスのコピー

では次に↓のようなコードを見てみたいと思います。

struct animal {
    int age;
    int weight;
};

int main(void) {
    struct animal a = {1, 2};
    struct animal *b;

    b = &a;

    return 0;
}

先ほどと違うのは、変数bが構造体のポインタ変数になっている点です。
そして、&演算子で変数aのアドレス値をポインタ変数bに代入しています。

このとき、コピーのコストは変数何個分でしょうか?
ポインタ変数にアドレス値を代入する場合、構造体のメンバ変数はコピーされません。
そのため上のコードの代入ではコストは変数1個分になります。

この変数1個分のコピーのコストというのは、つまりアドレス値のコピーのコストです。
変数aのアドレス値を、ポインタ変数bにコピーするコストは変数1個分ということです。

ポインタ変数のバイト数は処理系にもよりますが、4バイトです。
これはint型の変数のバイト数と同じです。
つまりポインタ変数へのアドレス値のコピーのコストはint型の変数のコピーのコストと同じということになります。

ポインタを使うとコピーのコストが減る

コピーのコスト数が違うことはわかりましたが、それで何が嬉しいのでしょうか?
ここまでに示した例はささいなもので、それほど作成するプログラムに大きく影響するようには思えませんよね。
これぐらいの違いだったらなんでもかんでもポインタを使わずに、すべて構造体のコピーでいいのではないかとも思えます。

しかしそれは誤りです。
これまでに示したポインタ変数を使わない場合の構造体のコピーのコストと、ポインタ変数を使った場合の構造体のアドレス値のコピーのコストの違いは、決定的とも言えるものです。
これの違いがわかるかどうかでC言語のプログラムを速く出来るかどうかが決まると言っても過言ではありません。

これは構造体変数を関数に渡したいケースを考えるとよくわかります。

関数へ構造体変数を渡す

たとえば関数show_agestruct animalの構造体変数を渡して、その変数を使って構造体struct animalのメンバ変数ageの値を出力したいとします。
そうすると、struct animalの構造体変数を丸ごとコピーして渡すコードは↓のようになります。

// 引数に構造体変数を丸ごとコピー
void show_age(struct animal a) {
    printf("age %d\n", a.age);
}

int main(void) {
    struct animal a = {1, 2};

    show_age(a);

    return 0;
}

これで構造体変数のメンバageを表示することが出来るようになります。
main関数からshow_age関数を呼び出していますが、この時に引数(実引数)にstruct animalの構造体変数aを渡しています。
引数には変数aのコピーが渡されますが、このときコピーされるコスト数はメンバ変数ageweight変数2個分になります。

ではポインタ変数を使った場合はどうでしょうか?
↓のようなコードを見てみましょう。

void show_age(struct animal *a) {
    printf("age %d\n", a->age);
}

int main(void) {
    struct animal a = {1, 2};

    show_age(&a);

    return 0;
}

show_age関数の引数(仮引数)がポインタ変数になりました。
show_age関数を呼び出す時、構造体変数aのアドレス値を引数(実引数)に渡しています。
するとコピーのコストは、アドレス値の代入だけなので変数1個分になります。

先ほどのコードの場合と、↑のコードの場合、やってる処理はどちらもメンバ変数ageの表示です。
しかし、ポインタ変数を使った方は発生するコスト数が半分になっています。
コストが1/2になっているわけですね。
これがポインタ変数の威力です。

構造体のメンバ変数が多くなればなるほど、この差は大きくなります。
考えてみてください。巨大な構造体を色々な関数に渡すとき、ポインタ変数を使わなかったら?

おそろしいですね

コンパイラの最適化によっては、うまいことコピーが発生しないようにしてくれるときもありますが、やはりポインタ変数を使ったほうが移植性も考えるとスマートな方法と言えるでしょう。

設計がシンプルになる

ポインタ変数を使ったほうがプログラムが速く書けることは分かりました。
では、設計がシンプルになるとはどういうことでしょうか?

ポインタ変数は、色んな関数で同じ変数のメモリ上のアクセスを共有することが出来ます
このメモリ上のアクセスの共有によって設計に柔軟性が生まれます。
この設計の柔軟性は、C言語によるプログラムの自由度を大幅に引き上げるものです。

たとえば構造体変数aの中身を変える処理を別の関数に書きたいとします。

struct animal {
    int age;
};

int main(void) {
    struct animal a = {0};

    // 変数aの中身を変える処理を書きたい

    return 0;
}

ポインタ変数を使わない場合は、これは返り値を使って構造体を丸ごとコピーするしかありません。

struct animal calc_age(void) {
    struct animal ret = {1};
    return ret;
}

int main(void) {
    struct animal a = {0};

    a = calc_age();

    return 0;
}

これはなんとなく不格好な気がしませんか?
設計としてはありですが、returnで構造体のメンバ変数によってコピーのコストが変わるというのはあまり良い気分ではありませんよね。
struct animalの構造体のメンバ変数が膨れ上がったら、コピーのコストが膨大になり、上のコードは非効率なコードになります。
するとcalc_age()を呼び出すごとにそのコストが発生するわけなので、全体の設計にも影響が出ます。

ポインタ変数を使う場合はもっとシンプルに書けます。
例えば↓のようにです。

void calc_age(struct animal *a) {
    a->age = 1;
}

int main(void) {
    struct animal a = {0};

    calc_age(&a);

    return 0;
}

どうでしょうか。先ほどのコードと比べると、ポインタ変数を使っている分シンプルになっている気がしませんか?
戻り値で構造体を丸ごとコピーする場合は、メンバ変数が多くなればそれだけコピーのコストも増えますが、ポインタ変数を使っている場合はポインタ変数のアドレス値の代入と、メンバ変数への値の代入だけで済みます。

関数というのは1つのプロジェクトに無数にあるものです。
まさか巨大なプロジェクトに関数がmain関数1つだけなんていうのは避けたい自体です。
プログラムを分割統治して、美しく効率的なコードを書こうと思えば関数の数は自然と増えるものです。

それらの関数の返り値のコストを考えた時、構造体を返す関数が無数にあるというのはなんとも言えない気分になりそうです。
関数というのは出来る限りコストをかけずに呼び出したいと思うものですが、巨大な構造体を返す関数はその時点で呼び出すのもおっくうになるわけです。

ポインタ変数を使った場合、その構造体のコピーのコストを極力減らすことが可能になります。
それは↑の例を見ていただければわかることだと思います。

大規模なプロジェクトでは必須

ポインタは特に大規模なプロジェクトでは必須と言っても良い機能です。
大きな構造体がいたるところにあるプロジェクトでは、ポインタを使わないとプログラムが遅くなってやってられません。
これは本当の話です。
ポインタを使わないC言語のプログラムは、C言語を使っているにも関わらず遅くなる可能性が非常に大きくなります。

C言語が速いと言われるゆえんに、ポインタの存在があります。
C言語ではポインタを使って柔軟にコピー処理をコントロールできるので、ポインタが無い言語に比べるとプログラムを速くすることが出来ます。
他の言語ではコピーにはディープコピーやシャローコピーなどの概念がありますが、C言語ではポインタを使うことでそういった概念も表現することが可能です。
というかC言語で実装される言語は未だに多いので、それは出来て当然なんですね。

ポインタを使って柔軟で速いプログラムを

C言語のポインタを使うことで↓のようなメリットがあることがわかりました。

  • プログラムが速くなる

  • 設計がシンプルになる

ポインタはデニス・リッチーが発明した偉大な言語機能と言えます。
これからポインタを学ぶ人はぜひモチベーションを上げて、学習に取り組んでみてください。


投稿者名です。64字以内で入力してください。

必要な場合はEメールアドレスを入力してください(全体に公開されます)。

投稿する内容です。