C言語編 第36章 ポインタ⑥(データ構造の構築)

先頭へ戻る

この章の概要

この章の概要です。

ポインタへのポインタ

ポインタが保持している情報はメモリアドレスです。 また、ポインタ変数自身はメモリ上に置かれています。 ということは、ポインタ変数を使って、ほかのポインタ変数を指し示すことは可能なのではないでしょうか?

結論からいって、これは可能です。 このような、ポインタがポインタを指し示しているような使い方を、ポインタへのポインタと呼ぶことがあります。

「ポインタのポインタ」と表現されることが多いのですが、 「~への~」とした方がイメージしやすいのではないかと思います。

ポインタ型は、指し示す先の型に「*」を付けたものでした。 ポインタへのポインタの場合は、指し示す先にあるオブジェクトの型は「int*」のようなポインタ型ですから、 ここにさらに「*」を追加して、「int**」のようになります。
変数として宣言するときには、次のように書けばよいということになります。

int** pp;

「*」の個数を更に増やせば、「ポインタへのポインタへのポインタ」のようなこともできます。 できるのですが、現実問題、ポインタへのポインタぐらいで既に十分複雑なので、これ以上追加すると手に負えません。

ポインタへのポインタに与える初期値はどうなるでしょう? ポインタを指し示さなければならないので、例えば、次のようになります。

int num  = 100;
int* p   = #  /* 変数num を指すポインタ */
int** pp = &p;    /* ポインタ変数p を指すポインタへのポインタ */

通常のポインタであろうと、ポインタへのポインタであろうと、格納できるものはメモリアドレスであることに変わりはありません。 しかし、変数pp は、型が int* のポインタ変数を指し示さなければなりませんから、 例えば、変数num のメモリアドレスを格納することは不適切です。 ところが、もし格納しようとしてもコンパイルエラーにはなりません。

int num  = 100;
int* p   = #  /* OK */
int** pp = #  /* OK */

これは、基本的には間違っている可能性が高い行為なので、VisualStudio でも clang でも警告は出ますが、コンパイルは通ってしまいます。 自分が何をしようとしているのか、きちんと理解する必要があります。 もし、自信を持って、このような初期化や代入が正しいと言えるのであれば、キャストとコメントによって意図を示すと良いです。

int** pp = (int**)#  /* このキャストは問題ない */

また、ポインタへのポインタが、何も指していない状態は、やはりヌルポインタで表現できます。

int** pp = NULL;

多重間接参照

ポインタへのポインタを間接参照することを考えてみます。

ポインタへのポインタを間接参照すると、そこにあるのは、やはりポインタです。 ということで、そのポインタを更に間接参照すると、ようやく一番先にあるオブジェクトに行き着きます。 間接参照を繰り返すのであれば、間接演算子(*) を重ねて書けばよいです。 このような行為を、多重間接参照と呼ぶことがあります。

#include <stdio.h>

int main(void)
{
    int num  = 100;
    int* p   = &num;
    int** pp = &p;

    printf( "%p\n", pp );   /* pp が保持しているメモリアドレス(p のアドレス) */
    printf( "%p\n", *pp );  /* pp が指し示す先にある p が保持しているメモリアドレス(num のアドレス) */
    printf( "%d\n", **pp ); /* pp が指し示す先にある p が更に指し示す先にある値 (num の値) */

    return 0;
}

実行結果:

00FFFBE0
00FFFBEC
100

多重間接参照も、「*」を更に重ねて3段階以上にできます。 しかし、やはり複雑になり過ぎ、手に負えないのでやめた方が良いです。

多重間接参照を使う際には、間接参照のレベル(階数)を意識して考えると理解しやすいでしょう。 つまり、「int**」として宣言された、ポインタへのポインタがあるとすれば、これは「*」の個数から、2 というレベルを持っていると考えます。 そして、アドレス演算子(&) が登場するたびにレベルが +1 され、間接演算子(*) が登場するたびに -1 されます。
レベルが 0 のときには、メモリアドレスを保持していない通常の変数であるということになります。 レベルが 1 以上であれば、それはメモリアドレスを保持しており、ポインタであることが分かります。

#include <stdio.h>

int main(void)
{
    int num  = 100;         /* どこも指し示していないので レベル0 */
    int* p   = &num;        /* レベル0 の num に &演算子。レベルが +1 されて 1。あるいは int* なのでレベル 1 */
    int** pp = &p;          /* レベル1 の p に &演算子。レベルが +1 されて 2。あるいは int** なのでレベル 2 */


    printf( "%p\n", &pp );  /* レベル2 の pp に対する &演算子。
                               レベルが +1 されて 3。ポインタなので %p で出力 */
                               
    printf( "%p\n", pp );   /* レベル2 の pp。
                               ポインタなので %p で出力 */
                               
    printf( "%p\n", *pp );  /* レベル2 の pp に対する間接演算子。
                               レベルが -1 されて 1。まだポインタなので %p で出力 */
                               
    printf( "%d\n", **pp ); /* レベル2 の pp に対して2つの間接演算子。
                               レベルが -2 されて 0。ポインタではないので %d で出力 */

    return 0;
}

実行結果:

006FF8A8
006FF8B4
006FF8C0
100

文字列へのポインタ

要素の型が文字列型の配列を考えてみます。 文字列を表現するには、char[] のような配列型とする方法と、char* のようなポインタ型とする方法とがありました(第32章)。 ここではポインタによる方法を使ってみます。

const char* messages[] = {
    "OK!",
    "Error!",
    "Hello, World!",
    "Thank you!",
    "Good Bye!"
};

文字列リテラルを使って初期化を行うので、const修飾子を付けて「const char*」で扱っています。 要素の型が「const char*」の配列なので、変数名の後ろには [] があり、初期値も複数あります。 初期値は5個ありますから、要素数は 5 ということになり、 結果、messages の型は「const char*[5]」です。

さて、式の中で配列が単独で現れたとき、暗黙的にポインタに変換されるのでした(第32章)。 では、配列 messages の場合は、どんな型に変換されるのでしょうか? int[5] なら int* になるというルールですから、const char*[5] なら const char** になります。

このように、ポインタで表現された文字列を要素とする配列を使うと、割と簡単に思いがけず、ポインタへのポインタが登場します。 ポインタへのポインタを使う機会として多いのは、この場面かも知れません。

実例として、この messages という配列の各要素を出力する関数を作るとしたら、次のようになります。

#include <stdio.h>

#define SIZE_OF_ARRAY(array)	(sizeof(array)/sizeof(array[0]))

void printMessage(const char** messages, size_t max);

int main(void)
{
    const char* messages[] = {
        "OK!",
        "Error!",
        "Hello, World!",
        "Thank you!",
        "Good Bye!"
    };


    printMessage( messages, SIZE_OF_ARRAY(messages) );

    return 0;
}

/*
    メッセージを一覧表示 
    引数:
        messages:	表示するメッセージへのポインタを保持するポインタ配列。
        max:		messages に含まれる要素数。
*/
void printMessage(const char** messages, size_t max)
{
    size_t i;

    for( i = 0; i < max; ++i ){
        puts( messages[i] );
    }
}

実行結果:

OK!
Error!
Hello, World!
Thank you!
Good Bye!

printMessage関数は、引数を const char*[5] ではなく、const char** として受け取ります。 第33章で説明したように、 関数の仮引数に限っては、[] を使っても * を使っても同じ意味になりますから、 次のようにしても問題ありません。

void printMessage(const char* messages[], size_t max);

ここで、レベルの話を考えてみましょう。 printMessage関数の中では、「messages[i]」のようにして、要素の1つにアクセスしています。 添字演算子の意味は、「*(messages + i)」のような表現と同じことでした(第32章)。 ですから、添字演算子を使ったときには、間接演算子を使ったときと同様、レベルが -1 されると考えられます。

messages は「const char**」なのでレベル 2。 「messages[i]」とすると、-1 されてレベル 1 です。 ですから、まだポインタ(const char*)であることが分かります。 puts関数の仮引数もまた、const char*型ですから、そのまま渡すことができます。

多次元配列

これまでに登場した配列は、メモリ上に、要素が連続的に並んだものでした。 これは図にすると、次のようになります(数字は添字です)。

配列のイメージ

ここで、各要素がそれぞれ配列だったらどうなるでしょうか? 図で書くと、次のようになると考えられます。

二次元配列のイメージ

横方向に要素が並んでいましたが、それぞれの要素から縦方向にも配列が伸びたという状態です。 結果、表のような形になるとイメージできます。 このような配列は、二次元配列と呼ばれます。 これに対して、最初の配列は、一次元配列です。

二次元以上になった配列を、多次元配列と呼びます。 正確にいえば、C言語の多次元配列は、配列の配列と言った方が適切ですが、多次元配列と呼ぶことが多いのも事実なので、今後も多次元配列と表現します。

二次元 "以上" と書いたように、三次元以上にすることが可能です。 例えば、三次元配列であれば、次のようなイメージになるでしょう。

三次元配列のイメージ

これもやはり、4次元、5次元・・・と増やすことが可能ですが、 ほとんどの人は4次元以上をイメージすることができないはずです(現実世界が3次元ですから)。 実際、まともなイメージ図は描けませんし、4次元以上の配列は避けた方が良いです。

多次元配列は、次のように宣言します。

int array[5][6];     /* 二次元配列 */
int array[3][4][4];  /* 三次元配列 */

次元が増えるたびに、要素を特定するための添字の個数も増えていきます。 初期値を与える場合は、分かりやすさのために、次元ごとに { } で囲むように書きます。

int array[5][6] = {
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5}
};
int array[3][4][4] = {
    {
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3}
    },
    {
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3}
    },
    {
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3}
    }
};

実際には、{ } は1組だけあれば認識されますが、分かりづらくなるかも知れません。

int array[5][6] = {
    0, 1, 2, 3, 4, 5,
    0, 1, 2, 3, 4, 5,
    0, 1, 2, 3, 4, 5,
    0, 1, 2, 3, 4, 5,
    0, 1, 2, 3, 4, 5
};
int array[3][4][4] = {
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3
};

また、要素数の指定を省略して、コンパイラに要素数を判断させることができるのは、一番左側の [] だけです。

int array[][6] = {
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5}
};
int array[][4][4] = {
    {
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3}
    },
    {
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3}
    },
    {
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3}
    }
};

多次元配列になっても、メモリ上で、要素が連続的に並ぶ性質は変わりません。

#include <stdio.h>

int array[5][6] = {
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5}
};

int main(void)
{
    int i, j;

    for( i = 0; i < 5; ++i ){
        for( j = 0; j < 6; ++j ){
            printf( "%p ", &array[i][j]);
        }
        printf( "\n" );
    }

    return 0;
}

実行結果:

00F68000 00F68004 00F68008 00F6800C 00F68010 00F68014
00F68018 00F6801C 00F68020 00F68024 00F68028 00F6802C
00F68030 00F68034 00F68038 00F6803C 00F68040 00F68044
00F68048 00F6804C 00F68050 00F68054 00F68058 00F6805C
00F68060 00F68064 00F68068 00F6806C 00F68070 00F68074

要素を順番にアクセスする必要がある場合には、できるだけ、メモリアドレスも連続するような順番で行う方が、処理効率が高まる可能性があります。 ですから、二次元配列の全要素を順番にアクセスするときには、以下のように書くのではなく、

for( i = 0; i < 6; ++i ){
    for( j = 0; j < 5; ++j ){
        array[j][i] = i * j;
    }
}

以下のように書いた方が良いです。

for( i = 0; i < 5; ++i ){
    for( j = 0; j < 6; ++j ){
        array[i][j] = i * j;
    }
}

つまり、array[i][j][k] のように添字を指定する場合なら、後ろにある添字の方が速くカウントが進んでいき、 手前側にある添字のカウントの方が遅れて進む方が良いということになります。

二次元配列を関数に渡すことを考えてみます。
式の中に単独で現れた配列は、暗黙的にポインタに変換されるのでした。 二次元配列ならどうなるでしょうか?

例えば、int[][] であれば、int** でなりそうに思えますが、そうはなりません。 暗黙的に変換できるのは、「配列」から「ポインタ」だけです。 「配列の配列」から一気に「ポインタへのポインタ」に変換されることはないのです。 したがって、int[][] は int[] を指すポインタに変換されます。

これはかなり分かりづらいので、二次元配列のイメージ図で考えてみます。

二次元配列のイメージ

図中の矢印は、「配列」を指し示すポインタを考えたときに、あり得そうな3種類の方法を表しています。 つまり、以下の3通りです。

  1. 二次元配列全体を指すような矢印をイメージする
  2. 横方向(行方向)に、どこか1行を指す矢印をイメージする
  3. 縦方向(列方向)に、どこか1列を指す矢印をイメージする

配列の配列を、(一次元)配列を指すポインタに変換したときのイメージとして正しいのは2番です。

1番は、二次元配列全体を指しているので、int[] を指すポインタのイメージには合いません。 3番は、要素のメモリアドレスが連続的に並ばないので、これでは配列ではなくなってしまいます。

さて、問題なのは、2番のイメージに沿ったポインタは、どんな型で表現されるのかという点です。 「int[]*」でしょうか? それとも「int*[]」でしょうか? どちらでもなく、このイメージ図の場合ならば、「int (*)[6]」が正解です。

この謎めいた表現を理解する考え方は色々ありますが、例えばこう考えてみましょう。
先ほどの図で、2番の矢印は、二次元配列のどこか1つの行を、一次元配列とみなして、その配列を指しているのです。 ここで、ポインタに対するインクリメントの挙動を考えてみます。
int型のポインタに対して ++演算子を適用すると、int1個分の大きさ(sizeof(int)) だけ先へ移動するのでした(第32章)。 同じように考えると、1行分の配列を指しているポインタをインクリメントしたら、次の行へ移ると考えられます。 つまり、「sizeof(int) * 1行に含まれている要素数」だけ加算される訳です。

この計算を実現するには、「1行に含まれている要素数」という情報をきちんと持っていないといけません。 「int**」のような型になってしまったら、要素数が分かりません。 「int (*)[6]」という型で、[6] の部分が、「1行に含まれている要素数(言い換えると列の個数)」です。
要するに、「int (*)[6]」は、「int型の要素を 6個持った一次元配列を指すポインタ型」と解釈すればよいということになります

ここまでを踏まえて、実際のプログラムを見てみましょう。

#include <stdio.h>

#define ARRAY_COL_NUM    6		/* 配列の列の数 */
#define ARRAY_ROW_NUM    5		/* 配列の行の数 */

void printArray(const int (*array)[ARRAY_COL_NUM], int row, int col);

int main(void)
{
    int array[ARRAY_ROW_NUM][ARRAY_COL_NUM];
    int i, j;


    /* 全要素へ値を格納 */
    for( i = 0 ; i < ARRAY_ROW_NUM; ++i ){
        for( j = 0 ; j < ARRAY_COL_NUM; ++j ){
            array[i][j] = i * 10 + j;
        }
    }

    /* 要素を出力 */
    printArray( array, ARRAY_ROW_NUM, ARRAY_COL_NUM );

    return 0;
}

/*
    二次元配列の要素を出力。
    引数:
        array:		二次元配列の先頭アドレス。
        row:		列の数。
        col:		行の数。
*/
void printArray(const int (*array)[ARRAY_COL_NUM], int row, int col)
{
    int i, j;

    for( i = 0 ; i < row; ++i ){
        for( j = 0; j < col; ++j ){
            printf( "%02d ", array[i][j] );
        }
        printf( "\n" );
    }
}

実行結果:

00 01 02 03 04 05
10 11 12 13 14 15
20 21 22 23 24 25
30 31 32 33 34 35
40 41 42 43 44 45

printArray関数の仮引数に注目して下さい。 「const int (*array)[ARRAY_COL_NUM]」という複雑な表現になっていますが、 ARRAY_COL_NUM は、二次元配列の列の個数ですから、先ほどの説明通りです。
const は、関数内で書き換えないことを示すためのものです。

なお、( ) が必要であることに注意して下さい。 ( ) が無いと、意味が変わります。

int (*array)[6];  /* int型で要素数6 の配列 へのポインタ */
int* array[6];    /* int型へのポインタ の配列で要素数は 6 */

動的な二次元配列

今度は、動的に二次元配列を作ることを考えます。

動的な二次元配列を作るには、2つの考え方があります。 1つには、1つの次元ごとに順番に形作っていく方法です。 これは次のように書けます。

#include <stdio.h>
#include <stdlib.h>

#define ARRAY_COL_NUM    6		/* 配列の列の数 */
#define ARRAY_ROW_NUM    5		/* 配列の行の数 */

int main(void)
{
    int** array;
    int i, j;


    /* 二次元配列を動的に確保 */
    array = calloc( ARRAY_ROW_NUM, sizeof(int*) );
    for( i = 0; i < ARRAY_ROW_NUM; ++i ){
        array[i] = calloc( ARRAY_COL_NUM, sizeof(int) );
    }

    /* 二次元配列の要素を出力 */
    for( i = 0; i < ARRAY_ROW_NUM; ++i ){
        for( j = 0; j < ARRAY_COL_NUM; ++j ){
            printf( "%d ", array[i][j] );
        }
        printf( "\n" );
    }

    /* メモリ解放 */
    for( i = 0; i < ARRAY_ROW_NUM; ++i ){
        free( array[i] );
    }
    free( array );

    return 0;
}

実行結果:

0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0

この方法の場合、ポインタへのポインタを使用することになります。 最初の calloc関数malloc関数でも構いません)で、必要な行数分のメモリ領域を確保しています。このときに確保するメモリ領域は、この後、一次元配列を作るための場所なので、int型ではなくて、int のポインタ型 (int*) のためのものであることに注意して下さい。

また、calloc関数の戻り値を受け取るポインタ変数 array は、int* ではなく int** でなければなりません。 確保しようとしているメモリ領域は、前述の通り int* のためのものですから、この領域を指すポインタは int** です。

calloc関数の戻り値の型は void* です。 レベルの考え方でいえばレベル 1 ですが、実際には int** というレベル 2 のポインタを意味しているということになります。 レベルが合わないのは多くの場合、何かが間違っていると考えられますが、ここは自身を持って合っていると言わなければならない場面です。 ただ、汎用ポインタへは、あらゆるポインタ型を代入できるため、キャストは不要です。
このように、汎用ポインタである void* は、レベルの差すら無視した使い方を強いられるので、ここは気を付けなければなりません。

確保されたそれぞれの行ポインタに対して、今度は、その行を構成する要素のための領域を確保します。 今度は、各要素は int型ですので、calloc関数には int型の大きさを指定すればよいです。


もう1つの方法は、行も列もまとめて1つの一次元配列とみなしてしまう方法です。

#include <stdio.h>
#include <stdlib.h>

#define ARRAY_COL_NUM    6		/* 配列の列の数 */
#define ARRAY_ROW_NUM    5		/* 配列の行の数 */

int main(void)
{
    int* array;
    int i, j;


    /* 行と列をまとめた一次元配列を動的に確保 */
    array = calloc( ARRAY_ROW_NUM * ARRAY_COL_NUM, sizeof(int) );

    /* 一次元配列の要素を出力 */
    for( i = 0; i < ARRAY_ROW_NUM; ++i ){
        for( j = 0; j < ARRAY_COL_NUM; ++j ){
            printf( "%d ", array[i * ARRAY_COL_NUM + j] );
        }
        printf( "\n" );
    }

    /* メモリ解放 */
    free( array );

    return 0;
}

実行結果:

0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0

こちらの方法では、確保と解放はすっきりと分かりやすいと思いますが、これだとあくまでも一次元配列です。 なぜこれでもいいのかというと、各要素のメモリアドレスが連続的であるという特徴は、 どちらの考え方でも変わらないからです。

各要素へアクセスする際に添字を工夫して、二次元配列のように見せかけます。 例えば、1行目の 4列目の要素は、以下のように書きたいところですが、

array[1][4];

これはできないので、代わりに次のように書きます。

array[1 * ARRAY_COL_NUM + 4];

二次元配列の場合の各要素のメモリアドレスの並びを思い出して下さい。 このように「行番号×列の個数+列番号」とすれば、目的の要素の位置を割り出せます。

1つ目の方法では、malloc関数や free関数の呼び出し回数が多くなるため、実行効率もメモリの使用効率も、あまり良いものではありません。 その意味では、2つ目の方法の方が、malloc関数も free関数も 1回ずつで済むという利点があります。 他方、要素にアクセスするたびに、添字を計算で求めなければならない点では、2つ目の方法の方が非効率です。
どちらの方法もよく使われているものなので、両方とも理解しておきたいところです。

自己参照構造体

やや高度なデータ構造を扱おうとすると、ある構造体のメンバに、自分自身と同じ型の構造体を含めたいことがあります。

struct Student_tag {
    char*               name;
    int                 grade;
    int                 class;
    int                 score;
    struct Student_tag  next;
};

しかし、これはコンパイルできません。 このように、構造体をネストして使いたい場合には、ポインタを使用します。

struct Student_tag {
    char*               name;
    int                 grade;
    int                 class;
    int                 score;
    struct Student_tag* next;
};

もちろんこれだと、メンバnext は構造体変数そのものではなく、構造体変数を指し示すポインタ変数になるので、 実際に使う際には、自分でメモリアドレスを代入するなり、malloc関数等で確保するなりしなければなりません。

ちなみに、自分自身のメモリアドレスを保持させることも可能です。 このような構造体は、自己参照構造体と呼ばれることがあります。

#include <stdio.h>

struct Student_tag {
    char*               name;
    int                 grade;
    int                 class;
    int                 score;
    struct Student_tag* next;
};

int main(void)
{
    struct Student_tag student = { "Saitou Takashi", 2, 3, 80, NULL };
    student.next = &student;

    printf( "%s\n", student.next->name );

    return 0;
}

実行結果:

Saitou Takashi

定義が終わらないと、メモリアドレスが決定されないので、構造体変数を宣言する時点で、いきなり自分自身のメモリアドレスを使うことはできません。そのため、初期値としては一旦、NULL を与えています。

自己参照構造体は、連結リストというデータ構造を形作る際に必須の手法です。 C言語そのものの学習から外れてしまうので、これ以上深入りしませんが、プログラムを続けていると必ず登場する必須の知識ではありますから、 調べてみると良いと思います(連結リストについては、アルゴリズムとデータ構造編【データ構造】第3章で解説しています)。


練習問題

問題① 次のプログラムの誤りを修正して下さい。

#include <stdio.h>

int main(void)
{
    char str[81];
    char* pStr = str;
    char** ppStr = &pStr;

    puts( "80文字以内の文字列を入力して下さい。" );
    fgets( **ppStr, sizeof(str), stdin );
    printf( str );

    return 0;
}

問題② 二次元配列を使って、九九表を作成・出力するプログラムを作成して下さい。

問題③ 動的な二次元配列の各要素のメモリアドレスを調べるプログラムを作成してください。


解答ページはこちら

参考リンク

更新履歴

'2018/4/20 「NULL」よりも「ヌルポインタ」が適切な箇所について、「ヌルポインタ」に修正。

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/3/12 全面的に文章を見直し、修正を行った。
章のサブタイトルを変更(データ構造 -> データ構造の構築)
多重間接参照」の項の一部を分離して、「ポインタへのポインタ」の項を追加。

'2018/3/7 「*演算子」を「間接演算子」に統一。

'2018/2/21 文章中の表記を統一(bit、Byte -> ビット、バイト)

≪更に古い更新履歴を展開する≫





前の章へ(第35章 ポインタ⑤(動的なメモリの再割り当て))

次の章へ(第37章 ポインタ⑦(関数ポインタ))

C言語編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS