C言語入門 ポインタ、メモリ、配列について




2014年10月より個人の方を対象に、Study C無料提供を開始しました。
C言語を勉強中の方は、学習・教育に最適なC言語インタープリタのStudy Cを使ってみてください(個人の方は無料です)。
大学・高専・高校などの教育機関での採用実績も多数あるロングセラー商品Study Cが、個人向けに無料提供を始めました。
インタープリタの手軽さに加え、ゲームや3Dタートルグラフィックで楽しく勉強したりと、C言語の学習を強力にサポートします。
ブロック崩しゲーム 3Dツリー クリスマスツリー
また、このようなボタンの用意されているページでは、掲載しているプログラムをStudy Cに直接ロードし実行したりすることができます。
Study Cにロードする Study Cにロードし編集する Study Cにロードし実行する
Study C無料利用についての詳細は、このページを参照してください。



1.メモリ

 ポインタを理解するには、コンピュータの内部構造についてある程度の知識が 必要になります。IBM PC互換機などのパーソナルコンピュータの内部は大体次のような 部品で構成されています。

 @CPU(Central Processing Unit:中央処理装置)プログラムで指示されたとおりに 計算を行うコンピュータの心臓部です。

 Aメモリ:データやプログラムを記憶するための部品で、ROMとRAMがあります。 ROM(Read Only Memory)は読出し専用で、内容は出荷時に書き込まれています。 IBM PC互換機ではBIOSと呼ばれる基本プログラムが書き込まれています。一方、 RAM(Randam Access Memory)は読み書きが可能なメモリですが、電源が切れてしまうと その内容は消滅してしまいます。

 B補助記憶装置:代表的なものにフロッピーディスクとハードディスクがあります。 どちらもカセットテープに音を録音するのと同じような原理で、データやプログラムを 記録します。また、RAMとは異なり電源を切っても内容は消えず、後から内容を書き換える こともできます。しかし、CPUは補助記憶装置の内容をいったんメモリにロード(読込み) してからしか、そのデータを扱うことができません。

 この中で、メモリはポインタに深く関係しているので、メモリについてもう少し 説明しておきましょう。メモリが記憶できるデータは0か1かの2進数だけです。 一般にこの2進数を8桁(8ビット)ごとに区切り、メモリ操作の最小単位としています。 2進数で8桁というのは前に説明したとおり、1バイトと呼びます。パソコンの パンフレットの最後のページに、メモリ(RAM)32Mバイトなどと 書かれているのは、このバイト単位でメモリがどれだけ搭載されているかを 示しているもので、Kは1000(厳密には、2進数なので1024です)、Mは1000*1000を 意味しています。したがって640Kバイトは640*1000バイト、32Mバイトは 32*1000*1000バイトの記憶容量を持っていることになります。このように、 メモリは膨大な量が使用可能なので、どの場所のメモリ内容を読み出すといったように 場所を表現する必要があります。メモリの場所は住所と同じようにアドレスと呼ばれ、 0から順番に番地が割り当てられます。たとえば、「100番地のアドレスにデータを書き込む」 といったいい方をします。


2.メモリと変数

 データを記憶するために用いる変数は、すべてメモリに割り当てられています。 たとえば、char c;と宣言された変数Cには、ある番地から1バイト分のメモリが割り当て られます(何番地のアドレスに割り当てられるかは実行されるまでは決定されていないのが 普通です)。またint型変数は、ある番地から4バイト(8/16ビットCPU用のコンパイラなら2バイト)分のメモリが割り当てられます。 この他にも4バイト分のメモリを割り当てるlong, int型という変数も用意されています。

型              割り当てられるメモリ量

char            1バイト
int             2バイト (8/16ビットCPU用のコンパイラ)
int             4バイト (32ビットCPU用のコンパイラ)
long int        4バイト


3.変数の取り扱える数

 C言語では変数の型によって取り扱える最大の数が決まっています。まず、char型について 見てみましょう。char型は2進数で8桁の記憶容量を持っています。8桁の2進数は、次のように 0から255までの256通りの数を表現することができます。

10進数          2進数

0               0000 0000
1               0000 0001
2               0000 0010
3               0000 0011
4               0000 0100
        .
        .
        .
254             1111 1110
255             1111 1111

 しかし、char型は負の数も扱う必要があります。このため2進数の最上位の桁を符号の ように見立て、次のように対応づけられています。

10進数          2進数

0                0000 0000
1                0000 0001
2                0000 0010
        .
        .
        .
126              0111 1110
127              0111 1111

-128             1000 0000
-127             1000 0001
-126             1000 0010
        .
        .
        .
-2               1111 1110
-1               1111 1111

 このようにchar型変数は-128から127までの数を扱うことができます。 int、long int型に付いても同様のことがいえます。int型の10進数と2進数の対応は、 次のようになります。

8/16ビットCPU用のコンパイラ

10進数          2進数

0               0000 0000 0000 0000
1               0000 0000 0000 0001
2               0000 0000 0000 0010
        .
        .
        .
32766           0111 1111 1111  1110
32767           0111 1111 1111 1111

-32768          1000 0000 0000 0000
-32767          1000 0000 0000 0001
        .
        .
        .
-2              1111 1111 1111 1110
-1              1111 1111 1111 1111
32ビットCPU用のコンパイラ

10進数          2進数

0               0000 0000 0000 0000 0000 0000 0000 0000
1               0000 0000 0000 0000 0000 0000 0000 0001
2               0000 0000 0000 0000 0000 0000 0000 0010
        .
        .
        .
2147483646      0111 1111 1111 1111 1111 1111 1111 1110
2147483647      0111 1111 1111 1111 1111 1111 1111 1111

-2147483648     1000 0000 0000 0000 0000 0000 0000 0000
-2147483647     1000 0000 0000 0000 0000 0000 0000 0001
        .
        .
        .
-2              1111 1111 1111 1111 1111 1111 1111 1110
-1              1111 1111 1111 1111 1111 1111 1111 1111

 このようにint型は-32768から32769までの数を扱うことができます。また、同様にlong int型は-2147483648から2147483647までの数を扱うことができます。また、C言語では負の数を使用する必要がない変数も用意しています。このような変数は符号がないという意味のunsignedという単語を変数宣言に付け加えます。unsigned型の変数は負の数用に割り当てているものを正の数に使うことができます。unsigned型変数と扱える数は次のようになります。

unsigned char           0〜255
unsigned int            0〜65535 (8/16ビットCPU用のコンパイラ)
unsigned int            0〜4294967295 (32ビットCPU用のコンパイラ)
unsigned long int       0〜4294967295

 昔のファミコンのゲームで経験値や所持金などが60000までとか65000までと制限されていることがありますが、これはunsigned int型(16または8ビットCPU用のコンパイラだったので)に相当する変数を使っているためでしょう。


4.ポインタ

 C言語ではアドレスを使ってメモリを操作することが可能です。アドレスは、メモリのある場所を指し示すという意味でポインタと呼んでいます。ポインタの機能を用いると変数用に割り当てられていないメモリにもデータを書き込むことができ、誤動作の原因となるので、プログラミングには注意が必要です。まず、ある変数が格納されているアドレスを知る方法を説明します。変数のアドレスはscanfで使った&演算子によって知ることができます。たとえば、変数Cが格納されているアドレスは&iによって知ることができます。それでは、&演算子によって返されるアドレスをprintfで表示させてみましょう。アドレスはただの整数のようですが、int型やlong型などの整数型とは明確に区別され、ポインタ型と呼ばれます。ポインタ型の表示にはprintfの%dではなく%pを使用します。

main()
{
      char c;
      printf("%p\n", &c);
}
Study Cにロードする Study Cにロードし編集する Study Cにロードし実行する ブラウザとの連携機能が使用可能なStudy Cのバージョンなどについて...

このプログラムを実行すると変数Cが格納されているメモリのアドレスが表示されますが、この値はCコンパイラやCインタープリタが勝手に割り付けてしまい、実行する環境によって異なります。また、何番地のアドレスかをプログラマが知ってもあまり意味のないことです。今の例では普通の変数のアドレスを求めましたが、配列の場合は、&演算子が不要になります(scanfの使い方でも配列の場合は&が不要と説明しました)。あえて&演算子を使うのであれば&string[0]とします。

main()
{
      char string[80];
      printf("%p\n", string);
}
Study Cにロードする Study Cにロードし編集する Study Cにロードし実行する ブラウザとの連携機能が使用可能なStudy Cのバージョンなどについて...

 次にポインタ型の変数を使用してみましょう。ポインタ型の変数名の前に*を付けて宣言します。

char *p;

 ポインタ型変数はアドレスを記憶することができるので、&演算子の返す値を記憶することができます。

main()
{
      char *p;
      char c;

      p = &c;
      printf("%p\n", p);
}
Study Cにロードする Study Cにロードし編集する Study Cにロードし実行する ブラウザとの連携機能が使用可能なStudy Cのバージョンなどについて...

このプログラムは、前の例と同じことをいっていますが、プログラムの内容が少し変わったので異なったアドレスが表示されます。厳密にはchar *p;で宣言されたポインタ型の変数はchar型変数のアドレスを記憶するために使用します。int型、long型変数のアドレスを記憶するには、int *p;、long *p;といったように宣言を行います。最後に、あるアドレスにデータを読み書きする方法を説明します。ポインタ型の変数に記憶してあるアドレスが指し示す場所にはなんらかのデータが記憶されていたり、新たに記憶することができます。このような操作は*演算子によって行われています。

main()
{
        int i;
        int *p;

        i = 10;
        p = &i;
        printf("%d\n", *p);
        *p = 15;
        printf("%d\n", i);
        i = 20;
        printf("%d\n", *p);
}
10
15
20
Study Cにロードする Study Cにロードし編集する Study Cにロードし実行する ブラウザとの連携機能が使用可能なStudy Cのバージョンなどについて...

 このプログラムで使われているiは普通のint型変数、pはint型変数のアドレスを記憶するポインタ変数です。まず、i = 10;で変数iに10を代入し、p = &i;でポインタ変数pにint型変数iのアドレスをセットしています。この状態でポインタ変数pに対して*演算子を使うと、あたかも変数iを扱っているように両者が連動して変化します。この例では、*p = 15;でポインタ変数pの指し示すメモリに、つまり変数iに15を代入しています。同様にi = 20;で変数iの内容を変化させれば*pが返す値も20に変化します(変数pの値である変数iのアドレス自体は変化しません)。

 この例におけるiとpの関係をまとめると、次のようになります。

ポインタ変数    普通の変数
 p               &i              変数iのアドレス
 *p              i               変数iに蓄えられている内容
 *p = ??         i = ??          変数iに値を代入




5.ポインタと配列

 C言語ではポインタと配列が極めて類似した扱いになっています。次のプログラムを実行してください。

main()
{
        char a[10];
        char *p;

        strcpy(a, "abcdefg");
        p = a;
        printf("%s, %s\n", a, p);

        printf("%c, %c\n", *a, *p );
        printf("%c, %c\n", a[0], p[0]);
        printf("%c, %c\n", a[1], p[1]);
}
abcdefg, abcdefg
a, a
a, a
b, b
Study Cにロードする Study Cにロードし編集する Study Cにロードし実行する ブラウザとの連携機能が使用可能なStudy Cのバージョンなどについて...

 このように、C言語ではポインタと配列は同じもののように扱うことができます。両者の根本的な類似点は、ポインタ変数も配列名もアドレスを表すポインタであるということです(普通の変数は変数名が、その変数の内容を表します)。この例の配列名aは、配列に割り当てられているメモリの先頭アドレス(ポインタ)を表します。つまり、今まで使ってきた配列の表現である配列名[?]は正確にはアドレス[?](ポインタ[?])となります。したがって、ポインタ変数もアドレスを表すので配列と同じように使用することができます。一方、*演算子も*アドレス(*ポインタ)といったように使用するので、同じように*配列名 とすることもできるわけです。

 また、配列では2番目以降の要素をa[1]、a[2]といったように扱いますが、ポインタでは*(p+1)、*(p+2)とすることで同じように使用することができます。

main()
{
        char a[10];
        char *p;

        strcpy(a, "abcdefg");
        p = a;
        printf("%c, %c\n", a[1], p[1] );
        printf("%c, %c\n", *(a+1), *(p+1) );
        printf("%c, %c\n", a[2], p[2] );
        printf("%c, %c\n", *(a+2), *(p+2) );
}
b, b
b, b
c, c
c, c
Study Cにロードする Study Cにロードし編集する Study Cにロードし実行する ブラウザとの連携機能が使用可能なStudy Cのバージョンなどについて...

この例はchar型(1つの要素が1バイト)の配列なので、ポインタに1を加えると2番目の要素、2を加えると3番目の要素になることが容易に連想できます。一方、intやlong型の配列では、ポインタにint型なら2、long型なら4を加えなければ2番目の要素にならないように思えます。しかし、実際にはchar型と同じように1を加えるだけで2番目の要素を参照することができます。この理由は、int型のポインタに1加えるとアドレスは2増加し、long型では4増加するためです。

main()
{
        char c[10];
        int i[10];
        long l[10];

        char *cp;
        int *ip;
        long *lp;

        cp = c;
        ip = i;
        lp = l;

        printf("%p, %p, %p\n", cp, ip, lp);
        printf("%p, %p, %p\n", cp+1, ip+1, lp+1);
        printf("%p, %p, %p\n", c+1, i+1, l+1);
        printf("%p, %p, %p\n", &c[1], &i[1], &l[1]);
}
1000, 2000, 3000
1001, 2002, 3004
1001, 2002, 3004
1001, 2002, 3004
Study Cにロードする Study Cにロードし編集する Study Cにロードし実行する ブラウザとの連携機能が使用可能なStudy Cのバージョンなどについて...

 前にも述べたように、実際に何番地のアドレスがそれぞれの配列に割り当てられているかは決まっていませんが、配列cのアドレスが(16進数で)1000番地、配列iが2000番地、配列lが3000番地だとすると、実行結果は上記のようになります。

 このようにポインタ変数は宣言した型に応じて自動的に加算される値が変化するため、char型以外でもポインタと配列は同じように使用することができます。
 配列名(a)、ポインタ変数(p)、普通の変数(i)の関係をまとめると次のようになります( char a[10]; char *p; int i;とした場合)。

配列名          ポインタ        普通の変数

a または &a[0]  p               &i              記憶する場所のアドレス
*a              *p              i               変数が記憶している内容
a[??]           p[??]           i               変数が記憶している内容
a[??] = ??      p[??] = ??      i = ??          変数に値を代入
*a = ??         *p = ??         i = ??          変数に値を代入
*(a + ??) = ??  *(p + ??) = ??  i = ??          変数に値を代入

 配列とポインタは多くの類似点を持っていますが、次のような相違点があります。

 @ポインタ変数ではアドレスを変更することができますが、配列はアドレスが固定されていて変更することはできません。

main()
{
        char *p;
        char a[10], b[20];

        p = a;                                正しい
        p = b;                                正しい
        a = b;                                エラー
}
Study Cにロードする Study Cにロードし編集する Study Cにロードし実行する ブラウザとの連携機能が使用可能なStudy Cのバージョンなどについて...

 A配列は宣言すれば記憶する場所も確保されるので、配列の範囲内で自由に値を代入することができます。しかし、ポインタ変数はアドレスが設定されるまで*演算子で値を代入することができません。ポインタ変数自体は記憶する場所を示すだけなので、他の変数や配列が実体として存在する必要があります。

int *p;
int a[10];

main()
{
        *p = 1;                               誤り
        *a = 1;                               正しい
        p = a;
        *p = 1;                               正しい
}
Study Cにロードする Study Cにロードし編集する Study Cにロードし実行する ブラウザとの連携機能が使用可能なStudy Cのバージョンなどについて...

このプログラムは最初の*p = 1;で実体のないアドレスへ*演算子を使って代入しています。ところがC言語の文法としては正しいため、コンパイラやインタープリタはエラーとして知らせてくれません。STUDY Cを使っている場合は、ポインタが実体のないアドレスに書き込みを行おうとすると次のエラーが発生します(自動変数だと値が不定のためエラーになったりならなかったりするためpを関数外に出しグローバル変数にしました)。

ERROR(3112) この場所へのデータ書き込みは禁止されています.

 このエラーチェックも完全なものではありません。自動変数は宣言直後にでたらめな値がセットされているため、偶然に何らかの変数のアドレスと一致している可能性があるためです。しかし、このような偶然が起きる可能性は低いので、STUDY Cはほとんど場合、エラーとしてプログラマに知らせてきます。むしろ注意しなければならないのは、STUDY Cを卒業してMD-DOSでコンパイラを使用してプログラムを作るようになってからです。MS-DOS用として市販されているコンパイラは、通常この種の誤りを実行時に調べないため、何のエラーも発生せずに実行できてしまいます。当然、そのプログラムは正しく実行されず、原因を発見するために多くの労力を要することになります。


6.ポインタと関数

 ポインタの利用方法の1つに関数間のデータ受け渡しがあります。第10章の「関数と変数」ではグローバル変数による受け渡し方法を説明しましたが、ポインタを使っても呼出し側にデータを渡すことができます。10章でも説明したように、関数のパラメータには値しか渡すことができません。このため普通のパラメータでは計算結果などを呼出し側に返すことができなかったわけです。ここでは、ポインタを使って値を受け渡す方法を説明します。

 ポインタの内容はアドレスという一種の値なので、呼出し側が使用できる変数のアドレスを1個のパラメータとして渡すことができます。呼び出された関数は、このアドレスを指し示す場所に計算結果などの値を書き込むことができます。

int     wairzan(int, int, int *);

main()
{
        int s, a;

        s = warizan(10, 3, &a);
        printf("10 / 3 = %d ... %d\n", s, a);
}

warizan(int x, int y, int *amari)
{
        *amari = x%y;
        return (x/y);
}
10 / 3 = 3 ... 1
Study Cにロードする Study Cにロードし編集する Study Cにロードし実行する ブラウザとの連携機能が使用可能なStudy Cのバージョンなどについて...

このプログラムはwarizan()という商と余りを求める関数をmain()から呼び出しています。関数warizan()はreturn文で商を返し、ポインタ変数のパラメータを通して余りをmain()に渡しています。呼出し側の関数main()は余りを格納する変数を用意し、そのアドレスを&演算子を使って求め、パラメータとして渡します。呼び出された関数warizan()は渡されたアドレスをポインタ変数で受け取り、*演算子を使って指定された場所に余りを代入します。scanf()関数で&演算子を使うのもこれと同じ理由です。

 また、ポインタは関数のパラメータとして配列や文字列を扱うときにも使用されます。関数のパラメータ1つには、1つの値しか受け渡すことができません(新しいC言語の規格では例外があります)。文字列や配列は複数の値の集合体であるため、1つのパラメータでは納めることができません。このため、呼出し側は配列や文字列の先頭アドレスを関数に渡します。呼び出された関数は与えられたアドレスをポインタとして受け取り、そのポインタが指し示す内容として配列や文字列を処理します。たとえば、第8章の「文字、文字列」で使用方法を説明したstrlen()関数などもパラメータにポインタ変数を使って実現されています。strlen()関数と同じ働きをするusr_strlen()という関数を作ってみると次のようになります。

int     usr_strlen(char *p);

main()
{
        int i;
        char s[10];

        i = usr_strlen("abcdefg");
        printf("%d\n", i);
        strcpy(s, "hijkl");
        i = usr_strlen(s);
        printf("%d\n", i);
}

usr_strlen(char *p)
{
        int len;

        for (len = 0;; len++) {
                if (*(p+len) == 0) {
                        break;
                }
        }
        return (len);
}
8
5
Study Cにロードする Study Cにロードし編集する Study Cにロードし実行する ブラウザとの連携機能が使用可能なStudy Cのバージョンなどについて...

 usr_strlen()関数のポインタ変数pが文字列や配列の先頭アドレスを受け取ります。このアドレスが指し示す内容をforループの中で調べ、0が見つかったら文字列の最後なので終了します。プログラム中の *(p+len) という部分は p[len] とすることもできます。また、呼出し側が配列を渡すときに、&演算子を使わないということも注意してください。scanf()関数で配列を指定するときも、scanf("%s", a)のように&演算子を使わなかったのもこれと同じ理由です。

 C言語の特徴でもあるポインタですが、非常に奥が深い機能です。 ポインタの記号である「*」と配列の記号である「[n]」を複数組み合わせたりすることもできます。

char *p[10];
また、「()」で結合の優先度を変えることもできます。
char *(*p[10])[20];
これ以外にも関数へのポインタという機能もあります。 下記のようにC言語のポインタ機能について説明した書籍も用意されています。 ポインタでつまずいている方にはおすすめです。

図解C言語ポインタの極意 [ 柴田望洋 ]

価格:2,520円
(2013/1/27 09:38時点)
感想(1件)

C言語ポインタが理解できない理由改訂新版 [ 朝井淳 ]

価格:2,394円
(2013/1/27 09:39時点)
感想(1件)

[an error occurred while processing this directive]