MMGamesロゴ  MMGames
Twitterシェアボタン  Facebookシェアボタン   
しんで覚えるC言語
しんで覚えるC言語

配列とポインタの奇妙な関係

配列のような使い方
前節では、関数に配列を渡すには配列の先頭要素のアドレスを渡していたと説明しました。
ところで、次のプログラムは、ポインタ型の引数にした前節のプログラムですが、
このプログラムを見て、どこか不自然な部分は見あたらないでしょうか?

ソースコード
#include <stdio.h>

int getaverage(int *data);

int main(void)
{
    int average, array[10] = {15, 78, 98, 15, 98, 85, 17, 35, 42, 15};
    average = getaverage(array);
    printf("%d\n", average);
    return 0;
}

int getaverage(int *data)
{
    int i, average = 0;
    
    for (i = 0; i < 10; i++)
    {
        average += data[i]; /* ポインタ変数なのに? */
    }

    return average / 10;
}

この中で明らかに不自然なのは、コメントで示した行の data[i] です。
だって、この変数dataはポインタ変数であって、配列ではありません。
にも関わらず、[]を使って要素番号が指定できるとはどういうことでしょう?

このことは、3節 でも簡単に説明しています。
[]の役割は、配列の要素番号を指定する演算子なのですが、
その仕組みは、単に、配列名というアドレスに足し算を行っているだけです。
つまり、別に配列でなくても、アドレス値なら何でも良いと言うことになります。

細かく説明すると、数式の中に配列名を記述した場合、[]の記号の有無にかかわらず、
配列名は、配列の先頭要素へのアドレス(ポインタ値)として扱われます。
そして、その配列名に[]をつけた場合、そのアドレスに番号の値だけ足し算を行い、
その結果として、足し算された分の番号の要素として扱われているのです。

宣言時と数式との違い
配列を宣言する時には、<>で要素数を指定し、
配列の要素を使う時は、<>で番号を指定するのですが、
実は、この2つもまったく別の記号です。

宣言時の<>は要素数を指定するという意味を持ちますが、
数式の中で使用する<>は、アドレスに足し算する演算子です。

C言語では、似た使い方には同じ記号を使いたがる傾向があり、
その為、異なる意味に同じ記号を割り当てている部分が多いようです。

このことから、使えるメモリならばポインタ変数を配列のように使えることがわかります。
次のプログラムは、ポインタ変数を配列のように使用する例です。

ソースコード
#include <stdio.h>

int main(void)
{
    int *data;
    int i, average = 0, array[10] = {15, 78, 98, 15, 98, 85, 17, 35, 42, 15};

    data = array; /* ポインタ変数に配列のアドレスを代入 */

    for (i = 0; i < 10; i++)
    {
        average += data[i]; /* 配列みたいに使える */
    }
    
    printf("%d\n", average / 10);
    return 0;
}


実行結果
49

普段はこの様なややこしいことをする必要はまったくありませんが、
いずれ、動的メモリ確保をするようになった時に必要になります。

配列とポインタはまったく別物
多くの人が、配列とポインタを勘違いしてしまうようです。
配列とは、多数の変数を順番つけでまとめて扱う方法であり、
ポインタとは、変数のショートカットを作る方法です。

それなのに、似たような使い方ができるのは配列の設計と関係あります。
C言語では、配列を実現する手段として、ポインタを利用しているからです。
したがって、ポインタ変数では、配列と同等のことができてしまいます。

そのため、ポインタと配列は混同しやすいのですが、
配列はあくまでも多数の変数の先頭を示す固定された変数であり、
ポインタ変数は、好きな変数のアドレスを代入して、
好きなメモリ領域を使うことができる可変的な変数です。

ポインタ専用の書き方
前項では、ポインタ変数に配列のアドレスを代入すると、同じように使えると説明しました。
同じように使えるとは、[]演算子で要素番号の指定ができるという意味です。

しかし、実を言えば、ポインタ変数には、ポインタ変数用の書き方があります。
これは、ポインタ演算と呼ばれる書き方で、次のように書きます。

ポインタ演算
*(ポインタ変数 + 要素番号)

先頭にある*は、ポインタ変数を通常変数モードに切り替えるための演算子です。
かっこをつけて、ポインタ変数のアドレス値に要素番号分の足し算を行い、
その足し算されたアドレス値を通常変数モードに切り替えることで、
先頭アドレスから指定数だけ進んだ先のメモリにアクセスする方法です。

キーワード
【ポインタ演算】

ポインタ変数に加減算を行って配列の要素を使う書き方。
昔は、この書き方のほうが高速だったため、広く使用されていました。


次のプログラムは、この書き方で先ほどのプログラムを書き換えた例です。

ソースコード
#include <stdio.h>

int main(void)
{
    int *data;
    int i, average = 0, array[10] = {15, 78, 98, 15, 98, 85, 17, 35, 42, 15};

    data = array; /* ポインタ変数に配列のアドレスを代入 */

    for (i = 0; i < 10; i++)
    {
        average += *(data + i); /* ポインタ演算 */
    }
    
    printf("%d\n", average / 10);

    return 0;
}


[] を使わずに、ポインタ演算を使って配列にアクセスしています。
もちろん、実行結果は先ほどとまったく同じになります。
さらに、ポインタ変数は値を変更できることを利用した次のような書き方もあります。
一般的にポインタ演算と言えば、こちらを指すことが多いようです。

ソースコード
#include <stdio.h>

int main(void)
{
    int *data;
    int average = 0, array[10] = {15, 78, 98, 15, 98, 85, 17, 35, 42, 15};

    for (data = array; data != &array[10]; data++)
    {
        /* ここに注目 */
        average += *data;
    }

    printf("%d\n", average / 10);

    return 0;
}


このプログラムはかなりややこしい部分が多いので、説明が必要です。
まず、for文の開始時に、ポインタ変数dataに配列のアドレスを代入しています。
そして、更新として、data++、が指定されていますが、
この意味は、今までの変数の時と同じで、data内のアドレスを1つ分増やす演算です。
(正確には、そのポインタ変数の指す型のサイズ分だけ増加させる)
そして、ポインタ変数が(0から数えて)10番目の要素と同じ値になるまで繰り返します。

つまり、ポインタ変数の値そのものを増加させてアクセスすることで、
配列の要素1つ1つに順番にアクセスしていくという方法なのです。

これは、高速に動作する(時代もあった)ことから、C言語では良く使われていた書き方です。
なぜなら、普通に[]で配列を使う場合、その配列にアクセスする毎に足し算が必要です。
しかし、ポインタ演算なら、足し算はループの時に1回ずつ行うだけで済むからです。
古き悪きポインタ演算
前項では、ポインタ変数で配列要素へアクセスする書き方のポインタ演算を説明しました。
ですが、皆さんにお聞きします。あの書き方はわかりやすいと思いますか?
少なくとも、筆者にはわかりやすいとは思えません

まず、次の2つはまったく同じ意味なのですが、どちらがわかりやいすかは歴然です。

ソースコード
data[5]
*(data + 5)

また、++を使って増加していく方のポインタ演算などはさらにひどいです。
次の2つを見て、下の方がわかりやすいと思う人類は存在しないと思います。

ソースコード
for (i = 0; i < 10; i++)
{
    average += data[i];
}

for (data = array; data != &array[10]; data++)
{
    average += *data;
}


さらに、++を使って増加していく方のポインタ演算は高速だと書きましたが、 実は、これは昔の話だったりします。

現在では、コンパイラの性能が飛躍的にアップしています。
そして、現代のコンパイラは、[]でアクセスしているようなループを見つければ、
自動的に、++で増加するポインタ演算のような書き方に置き換えてコンパイルします。

C言語が作られたばかりの頃は、そんなコンパイラはなかったのですが、
現代では多くのコンパイラがその程度の工夫は行ってくれます。

組み込みでは
ほとんどのパソコン向けのコンパイラは適切な最適化を行ってくれます。
さらに、パソコン用のCPUは内部のキャッシュの仕組みが複雑かつ高性能であり、
繰り返し処理をCPUが独自に最適化して、速度を向上するような仕組みがあります。
そのため、現代的なパソコンでは、どちらの書き方でも、結局同じ速度になります。

しかし、組み込み(家電などに内蔵される低性能なコンピュータ)ではそうとは限りません。
コンパイラによる最適化は十分には機能しないことも多いですし、
CPUの仕組みが単純なので、プログラムの書き方が、速度にそのまま反映されやすくなっています。
そういった場合には、ポインタ演算は現代でも有用です。

昔は++ポインタ演算が結構使われており、その名残から現在でも使う人は多いのですが、
筆者としては、わかりやすい[]を使って配列にアクセスすることをオススメします。
アドレスのことは忘れましょう
ここまでで、ポインタ変数の機能はほぼ説明し尽くしましたし、
ポインタ変数はアドレスを記憶する変数であることを重視して、
それにまつわるさまざまな現象を説明してきました。

しかし、実際にプログラムを作るときには、
ポインタ変数がアドレスを記憶する変数であることはさっぱりと忘れてください。

なぜなら、ポインタ変数の本当の使い方とは、変数のショートカットとして使うことです。
決して、アドレスを操作することではありません。
言い換えれば、ポインタ変数がアドレスを記憶するのはあくまでも仕組みであり、使い方ではないからです。
使い方さえわかっていれば、内部処理がどんな仕組みだろうと無関係だからです。
コンピュータの仕組みをまったく知らなくてもコンピュータが使えるのと同じことです。

多くの人がポインタ変数でつまづくのは、アドレスを記憶することばかり意識するためです。
そんな内部の仕組みなど知らなくても、ポインタ変数は簡単に使えます。
変数に&を付けてショートカットを設定し、*記号を付けて通常変数モードにして使用する。
この手順にしたがって使う限り、アドレスなどなんの関係もないのです。

ただし、C言語ではときおり非常に不可解なバグがでることがあります。
この場合、その原因の多くはポインタ変数の使い方を間違えているためです。
バグ修正の時にはポインタ変数がアドレスを記憶する変数であることを思い出し
どこかで間違ったアドレスが代入されていないか調べなくてはならないでしょう。


本サイトについて

苦しんで覚えるC言語(苦C)は
C言語入門サイトの決定版です。
C言語の基本機能を体系立てて解説しており、
市販書籍と同等以上の完成度です。

第0部:プログラム概要編
  1. プログラムとは何か?
2章:プログラムの書き方
  1. 書き方のルール
  2. 書き方の慣習
  3. 練習問題2
3章:画面への表示
  1. 文字列の表示
  2. 改行文字
  3. 練習問題3
6章:キーボードからの入力
  1. 入力用の関数
  2. 入力の恐怖
  3. 練習問題6
9章:回数が決まっている繰り返し
  1. 繰り返しを行う文
  2. ループ動作の仕組み
  3. 練習問題9
10章:回数がわからない繰り返し
  1. 回数不明ループ
  2. 入力チェック
  3. 練習問題10
13章:複数の変数を一括して扱う
  1. 複数の変数をまとめて扱う
  2. 配列の使い方
  3. 練習問題13
20章:複数のソースファイル
  1. 最小限の分割
  2. 分割の定石
  3. 練習問題20

コメント
COMMENT

💬 コメント投稿欄を開く