ポインタ⑥(データ構造の構築) 解答ページ | Programming Place Plus C言語編 第36章

トップページC言語編第36章

問題①

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

#include <stdio.h>

int main(void)
{
    char str[81];
    char* p_str = str;
    char** pp_str = &p_str;

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


間接参照のレベルを整理して考えましょう。

#include <stdio.h>

int main(void)
{
    char str[81];          // レベル0
    char* p_str = str;      // ポインタに変換。メモリアドレスを渡すので +1 されレベル1
    char** pp_str = &p_str;  // &演算子により +1 されレベル2

    puts("80文字以内の文字列を入力してください。");
    fgets(**pp_str, sizeof(str), stdin);  // ** により -2 されレベル0 ???
    printf(str);
}

fgets関数の第1引数は、入力された文字列を受け取るためのメモリアドレスを要求しています。具体的には char* であり、これはすなわち、レベル1 のポインタを必要としているということになります。ところが、実際にはレベル0 の変数を渡してしまっています。

ということで **pp_str となっている個所を、*pp_str に修正するのが正解です。

もちろん、こんなプログラムなら、素直に str をそのまま渡すのが一番ですが。

問題②

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


たとえば、次のようになります。

#include <stdio.h>

int main(void)
{
    int array[9][9];


    // 九九表を作成
    for (int i = 0; i < 9; ++i) {
        for (int j = 0; j < 9; ++j) {
            array[i][j] = (i+1) * (j+1);
        }
    }

    // 九九表を出力
    for (int i = 0; i < 9; ++i) {
        for (int j = 0; j < 9; ++j) {
            printf("%2d ", array[i][j]);
        }
        printf("\n");
    }
}

実行結果:

 1  2  3  4  5  6  7  8  9
 2  4  6  8 10 12 14 16 18
 3  6  9 12 15 18 21 24 27
 4  8 12 16 20 24 28 32 36
 5 10 15 20 25 30 35 40 45
 6 12 18 24 30 36 42 48 54
 7 14 21 28 35 42 49 56 63
 8 16 24 32 40 48 56 64 72
 9 18 27 36 45 54 63 72 81

二次元配列の理解のために、よく使われる題材ですが、特に難しいものではありません。 しかし工夫の余地のあるプログラムでもあります。

今回は、printf関数の “%d” 変換指定子に、幅の指定を含めることで奇麗に整形してあります。

問題③

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


まず、本編で解説した1つ目の方法で試してみます。

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

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

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

    // 各要素のメモリアドレスを出力
    for (int i = 0; i < ARRAY_ROW_NUM; ++i) {
        for (int j = 0; j < ARRAY_COL_NUM; ++j) {
            printf("%p ", &array[i][j]);
        }
        printf("\n");
    }

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

実行結果:

00381300 00381304 00381308 0038130C 00381310 00381314
00381348 0038134C 00381350 00381354 00381358 0038135C
00381390 00381394 00381398 0038139C 003813A0 003813A4
003813D8 003813DC 003813E0 003813E4 003813E8 003813EC
00381420 00381424 00381428 0038142C 00381430 00381434

実行結果のように、列方向に見ると +4 ずつ増加していますが、次の行に移ると、メモリアドレスが飛んでしまっています。本編で確認した、動的に確保されたものではない二次元配列での結果とは異なることが分かります。malloc関数を行・列に分けて呼び出しているため、連続的なメモリアドレスにならないのです。

次に、一次元配列として確保する方法を試します。

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

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

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

    // 各要素のメモリアドレスを出力
    for (int i = 0; i < ARRAY_ROW_NUM; ++i) {
        for (int j = 0; j < ARRAY_COL_NUM; ++j) {
            printf("%p ", &array[i * ARRAY_COL_NUM + j]);
        }
        printf("\n");
    }

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

実行結果:

007112C0 007112C4 007112C8 007112CC 007112D0 007112D4
007112D8 007112DC 007112E0 007112E4 007112E8 007112EC
007112F0 007112F4 007112F8 007112FC 00711300 00711304
00711308 0071130C 00711310 00711314 00711318 0071131C
00711320 00711324 00711328 0071132C 00711330 00711334

この場合、行が変わっても連続的なメモリアドレスが割り当てられていることが分かります。 これは1度の calloc関数呼び出しで、まとまったメモリ領域を一括で確保しているからです。

問題④

問題④ 「配列を指し示すポインタ」が欲しいと考えました。これは可能ですか? どうすれば実現できるでしょうか?


「配列を指し示すポインタ」といった場合、本当は「配列の先頭を指し示すポインタ」が欲しいと言っていることがほとんどです。それであれば、いつものようにポインタとして受け取るだけで十分です。

int array[] = {0, 1, 2, 3, 4};
int* p1 = array;  // 配列の先頭要素を指し示すポインタ

あるいは、配列自身にアドレス演算子を適用することで、文字どおり、配列を指すポインタを作れます。これは、二次元配列で、特定の行(の一次元配列)を指すポインタを作ることと同じことです。つまり、要素数も含めた型 (int[5]) を指すポインタ型を使います。

int array[] = {0, 1, 2, 3, 4};
int(*p2)[5] = &array;  // 配列を指し示すポインタ

p1 と p2 の違いは、ポインタをインクリメントしたときに、保持しているメモリアドレスがどうなるかを確認するとはっきりします。

#include <stdio.h>

int main(void)
{
    int array[] = {0, 1, 2, 3, 4};

    int* p1 = array;  // 配列の先頭要素を指し示すポインタ
    int(*p2)[5] = &array;  // 配列を指し示すポインタ

    printf("%p %p\n", p1, p1 + 1);
    printf("%p %p\n", p2, p2 + 1);
}

実行結果:

006FFC28 006FFC2C
006FFC28 006FFC3C

p1 と (p1+1) のメモリアドレスの差は 4 です。これは、sizeof(int) のことです。

一方、p2 と (p2 + 1) のメモリアドレスの差は 20 です。これは、sizeof(int) * 5 のことです。要素数 5 の配列を指しているため、インクリメントしただけで、一気に配列全体の大きさ分だけ移動するのです。

問題⑤

問題⑤ 次のプログラムは正しいですか? 正しければ実行結果を答えてください。間違っていれば、どう間違っているか指摘してください。

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

int main(void)
{
    int* value;
    int** ptr = &value;

    value = malloc(sizeof(int));
    *value = 100;

    printf("%d\n", *value);
    printf("%d\n", **ptr);

    free(value);
}


このプログラムは正しいです。 実行結果は次のようになります。

実行結果:

100
100

ポインタへのポインタである ptr は、ポインタ変数value を指し示しています。value は、malloc関数によって確保された領域を指し示しているので、**ptr は確保された領域にある値を参照します。

問題⑥

問題⑥ リバーシの盤面を表現する二次元配列を作り、ゲーム開始時点の状態を表現するように初期化してください。初期化処理は1つの関数にまとめ、また状態を確認できるような出力関数を作成してください。


表現の仕方は自由ですが、この解答例では、何も置かれていない場所を□で、白のコマを○、黒のコマを●で表現しています。

#include <stdio.h>
#include <string.h>
#include <assert.h>

#define CHIP_NONE    (0)           // 何も置かれていない
#define CHIP_WHITE   (1)           // 白のコマが置かれている
#define CHIP_BLACK   (2)           // 黒のコマが置かれている

#define COL_NUM      (8)           // 盤面の列数
#define ROW_NUM      (8)           // 盤面の行数

typedef char mass_t;               // マスを表す型


void init_board(mass_t (*board)[COL_NUM]);
void print_board(const mass_t (*board)[COL_NUM]);

int main(void)
{
    mass_t board[ROW_NUM][COL_NUM];  // 盤面

    init_board(board);
    print_board(board);
}

/*
    盤面を初期化。
    引数
        board:      盤面。
*/
void init_board(mass_t (*board)[COL_NUM])
{
    memset(board, CHIP_NONE, sizeof(mass_t) * COL_NUM * ROW_NUM);

    board[3][3] = CHIP_WHITE;
    board[3][4] = CHIP_BLACK;
    board[4][3] = CHIP_BLACK;
    board[4][4] = CHIP_WHITE;
}

/*
    盤面を表示。
    引数
        board:      盤面。
*/
void print_board(const mass_t (*board)[COL_NUM])
{
    for (int row = 0; row < ROW_NUM; ++row) {
        for (int col = 0; col < COL_NUM; ++col) {
            switch (board[row][col]) {
            case CHIP_NONE:
                printf("□");
                break;
            case CHIP_WHITE:
                printf("○");
                break;
            case CHIP_BLACK:
                printf("●");
                break;
            default:
                assert(0);
                break;
            }
        }
        printf("\n");
    }
}

実行結果:

□□□□□□□□
□□□□□□□□
□□□□□□□□
□□□○●□□□
□□□●○□□□
□□□□□□□□
□□□□□□□□
□□□□□□□□

二次元配列の扱い方が理解できていれば、それほど難しくはないでしょう。

せっかくなので、このままリバーシを作ってみるのも良いと思います。相手プレイヤーの思考まで作るのは大変ですが、とりあえず1人で2人分のプレイをするというのでも良いでしょう。コマを置ける場所かどうかの判定、ひっくり返せるコマの判定、勝敗の判定、どこにも置けないときの処置など、いろいろと実装しなければならない処理があります。


参考リンク


更新履歴

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



第36章のメインページへ

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る