C言語のポインタのメリットとは?コピーしますかメモリを共有しますか
目次
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}; ...
そして、構造体変数b
にa
を代入しています。
... b = a; ...
struct animal
構造体にはint
型のメンバ変数age
とweight
があります。
つまり構造体の中にはint
型の変数が2つあるわけですね。
このとき、上のコードで、変数b
に変数a
を代入する時に発生するコピーのコストは変数何個分でしょうか?
代入は1回分ですが、問題にしたいのはそのコストです。
構造体には変数がage
とweight
で2つあるので、全体のコスト的には変数2個分になります。
変数2個分というのは、構造体変数1個のコピーのコストがint
型の変数2個分かかるということです。
これは構造体のメンバ変数が増えるほど、コピーのコストが多くなります。
たとえばstruct animal
構造体にint
型の変数をもう10個追加したとします。
そうするとその構造体の構造体変数のコピーにかかるコストはいくらになるでしょうか?
すでにあるメンバ変数2個にプラス・メンバ変数が10個加わるので、そのコストは全体でint
型の変数12個分になりますね。
このように構造体のメンバ変数の数と、そのメンバ変数の型というの変数のコピーのコストに直結するものです。
そしてメンバ変数の多い構造体変数のコピーは基本的にコストが大きくなると覚えておいてください。
関連記事
C言語で構造体を代入する方法
C言語の構造体をコピーする
C言語の構造体のポインタの使い方
ポインタのアドレスのコピー
では次に↓のようなコードを見てみたいと思います。
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言語のポインタの宣言と初期化方法
C言語のポインタのメリットとは?コピーしますかメモリを共有しますか
ポインタを使うとコピーのコストが減る
コピーのコスト数が違うことはわかりましたが、それで何が嬉しいのでしょうか?
ここまでに示した例はささいなもので、それほど作成するプログラムに大きく影響するようには思えませんよね。
これぐらいの違いだったらなんでもかんでもポインタを使わずに、すべて構造体のコピーでいいのではないかとも思えます。
しかしそれは誤りです。
これまでに示したポインタ変数を使わない場合の構造体のコピーのコストと、ポインタ変数を使った場合の構造体のアドレス値のコピーのコストの違いは、決定的とも言えるものです。
これの違いがわかるかどうかでC言語のプログラムを速く出来るかどうかが決まると言っても過言ではありません。
これは構造体変数を関数に渡したいケースを考えるとよくわかります。
関数へ構造体変数を渡す
たとえば関数show_age
にstruct 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
のコピーが渡されますが、このときコピーされるコスト数はメンバ変数age
とweight
の変数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言語で構造体を引数に渡す方法
C言語の構造体のポインタの使い方
設計がシンプルになる
ポインタ変数を使ったほうがプログラムが速く書けることは分かりました。
では、設計がシンプルになるとはどういうことでしょうか?
ポインタ変数は、色んな関数で同じ変数のメモリ上のアクセスを共有することが出来ます。
このメモリ上のアクセスの共有によって設計に柔軟性が生まれます。
この設計の柔軟性は、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言語の難解なバグのほとんどはポインタ関連のバグです。
ポインタに熟達しポインタを使いこなしている人ほど解決困難なバグを生むことが多くなります。
つまりポインタがコードの中で複雑に絡み合ってある種のカオスが生まれているということですね。
これはある程度の規模のプログラムの開発になると必ずこうなると言ってもいいと思います。
(^ _ ^) | カオスの中に |
そのバグのフィックスに長い時間をかけて頭を悩ませる……。
これはC言語を操る者の宿命と言っても良いかもしれません。
関連記事
プログラミングのバグの直し方を考える
プログラミングのテストとバグ取り
解決困難なバグに遭遇したら?
C言語でポインタがらみのエラーを検出するのは至難の業です。
ですのでこういう時はツールに頼りましょう。
たとえばValgrindというメモリチェックツールはポインタがらみのバグを解決するのに役立ちます。
C言語は非常に難しい言語ですので(簡単という人も多いですが)ツールを活用するようにしましょう。
(・ v ・) | ツールを活用しよう |
関連記事
C言語のデバッグの極意【バグを蹴散らす2つの方法】
ポインタを使って柔軟で速いプログラムを
C言語のポインタを使うことで↓のようなメリットがあることがわかりました。
プログラムが速くなる
設計がシンプルになる
ポインタはデニス・リッチーが発明した偉大な言語機能と言えます。
これからポインタを学ぶ人はぜひモチベーションを上げて、学習に取り組んでみてください。
(^ _ ^) | ポインタのメリットを把握! |
(・ v ・) | ポインタマスターに俺はなる! |