C言語編 第38章 理解の定着・小休止④

先頭へ戻る

この章の概要

この章の概要です。

理解の定着・小休止④

この章では、これまでの章内容の理解を再確認しましょう。 また、1章丸ごとを割くほどでも無い細かい部分について、少し触れていきます。

今回は、以下の範囲が対象です。 ポインタがテーマとなります。

ポインタの構文

ポインタ変数は、メモリアドレスを保持し、その位置を指し示すような変数です。 ポインタ変数の宣言は、次のように行います。

int* p;

この場合、int型のオブジェクトのメモリアドレスを保持することができ、 そのオブジェクトを指し示すポインタ変数が宣言されます。

ある変数のメモリアドレスを得るためには、アドレス演算子(&) を使用します。

int num = 100;
int* p = #  /* 変数num のメモリアドレスを保持 */

ポインタ変数を経由して、指し示す先にある値を参照するには、間接演算子(*) を使用します。 この操作は、間接参照(逆参照)と呼ばれます。

printf( "%d\n", *p );  /* ポインタ変数p を間接参照した値を出力する */

アドレス計算

printf関数でメモリアドレスを出力する際には、"%p" 変換指定子を使います。

#include <stdio.h>

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

    printf( "%p\n", ptr );
    printf( "%p\n", &num );

    return 0;
}

実行結果:

0021FE40
0021FE40

配列の場合、各要素がメモリ上に連続的に並ぶことが保証されています

#include <stdio.h>

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

int main(void)
{
    int array[] = { 0, 10, 20, 30, 40 };
    int i;

    for( i = 0; i < SIZE_OF_ARRAY(array); ++i ){
        printf( "%d: %p\n", i, &array[i] );
    }

    return 0;
}

実行結果:

0: 002DF7F4
1: 002DF7F8
2: 002DF7FC
3: 002DF800
4: 002DF804

実行結果をみると、メモリアドレスが 4 ずつ増加していることが分かります。 これは、この実行環境では int型が 4バイトだからです。 このように、隙間なく連続的に並んでいること利用して、配列の要素数を知ることが可能です。

#include <stdio.h>

int main(void)
{
    int array[5] = { 0, 10, 20, 30, 40 };

    printf( "%d\n", &array[5] - &array[0] );

    return 0;
}

実行結果:

5

配列の末尾の更に1つ先のところのメモリアドレスから、先頭の要素のメモリアドレスを減算することで、要素数を計算しています。 なぜこれでうまくいくかと言うと、ポインタに対する加算および減算には、特別なルールがあるからです。

ポインタに対するインクリメント操作では、メモリアドレスが +1 されるのではなく、sizeof(指し示す先の型) 分だけ加算されます。 同様に、デクリメント操作では、sizeof(指し示す先の型) 分だけ減算されます

ヌルポインタ

どこも指し示していないポインタを、ヌルポインタと呼びます。 何も指し示していないので、間接参照を行うと未定義な動作になってしまいます。

ヌルポインタは、NULL というマクロで表現することができます。NULLマクロは、stddef.h など幾つかの標準ヘッダで定義されています。

#include <stddef.h>

int* ptr = NULL;

ヌルポインタは、ヌルポインタ以外のポインタと比較したとき、一致しないことが保証されていますmalloc関数などで動的なメモリ割り当てを行う際、失敗を表す戻り値として NULL が使われるのはこのためです。このように、ポインタを返すべき場面での失敗の意味で、ヌルポインタを使うことがよくあります。

汎用ポインタ

通常、ポインタは、どんな型のオブジェクトを指し示せるのかが決まっています。 例えば、char*型のポインタが指し示すのは、char型のオブジェクトです。

一方、汎用ポインタ(総称ポインタ)と呼ばれるポインタは、どんな型のオブジェクトでも指し示すことができます。 汎用ポインタは、void*型で表現します。

int num = 100;
double f = 15.5;

void* ptr = &num;
void* ptr2 = &f;

汎用ポインタを間接参照することはできません。 これは、間接参照した先にあるオブジェクトがどんな型であるか不明であるためです。

その代わりに、汎用ポインタは他の型のポインタ変数にそのまま代入できます。 型が明確なポインタに変換されれば、そこから間接参照することができます。

int num = 100;
int* int_ptr;

void* ptr = &num;

int_ptr = ptr;                 /* OK。汎用ポインタから他の型のポインタへは暗黙的に型変換できる */
int_ptr = (int*)ptr;           /* OK。明示的にキャストしても構わない (C++ では常にこうしなければならない) */

printf( "%d\n", *ptr );        /* エラー。汎用ポインタは間接参照できない */
printf( "%d\n", *(int*)ptr );  /* OK。一旦、int*型にキャストした後、間接参照する */

構造体変数へのポインタ

構造体変数へのポインタも作れます。

#include <stdio.h>

typedef struct {
    int    x;
    int    y;
} Point;

int main(void)
{
    Point point;
    Point* p = &point;

    point.x = 10;
    point.y = 20;

    printf( "%d %d\n", (*p).x, (*p).y );
    printf( "%d %d\n", p->x, p->y );

    return 0;
}

実行結果:

10 20
10 20

構造体変数を指し示すポインタから、間接参照でメンバにアクセスする際には、 これまで通りに間接演算子を使っても構いませんが、やや面倒な記述になってしまいます。 そこで、->演算子(アロー演算子、矢印演算子)という構文糖が用意されています。 次の2行は同じ意味になります。

x = (*p).x;
x = p->x;

配列とポインタ

配列とポインタは、一見同じように見えることがありますが、両者はまったくの別物です。 例えば、次の2つは同じ結果を生みます。

printf( "%d\n", array[i] );
printf( "%d\n", p[i] );     /* ただし p は配列array の先頭要素を指すポインタ */

これが同じ結果になるのは、「式の中で配列が単独で現れたとき、暗黙的に配列の先頭要素を指すポインタに置き換わる」からです。 そのため、配列だけが現れると、あたかもポインタを使ったかのような結果を生むことがあり、これが両者を混同する一因になっています。

更に、次の2つも同じ結果になります。

printf( "%d\n", p[i] );     /* ただし p は配列の先頭要素を指すポインタ */
printf( "%d\n", *(p+i) );

ポインタ変数に対する加算は、そのポインタ変数が指し示す先の要素の大きさ分だけメモリアドレスを進めるので、 「*(p+i)」の結果は、配列の i番目の要素へのアクセスと同義です。
とはいえ、いちいち *(p+i) と書くのは、ちょっと暗号めいて分かりづらいですし、書きづらくもあります。 添字演算子は、記述を容易にするための構文糖の一種です。

文字列

文字列リテラルの "abcde" は、char型の配列です。 具体的には、5つの文字と、終端に隠されたヌル文字を格納できる分の要素数を持つので、char[6] です。 const が付かない char型の配列なので、書き換えできてしまいそうですが、 文字列リテラルを書き換えようとする行為は未定義の動作です

文字列の変数の表現には、配列を使ったものと、ポインタを使ったものの2通りがあります。 次の宣言はいずれも正しいですが、意味が異なるものも含まれています。

char str1[] = "abcde";
char str2[] = { 'a', 'b', 'c', 'd', 'e', '\0' };
char* str3 = "abcde";

str1 と str2 は配列、str3 はポインタです。 str1 と str3 はいずれも初期値として "abcde" を指定していますが、文字列リテラルの "abcde" なのは str3 の方だけです。 str1 に与えている "abcde" は、「配列の各要素に与える初期値」という意味合いしかありません。 要するに、str2 のように 1文字ずつ指定する行為を一発で行っているだけのことです。

配列による文字列と、ポインタによる文字列のイメージは次の図のようになります。

配列とポインタの概念図

配列版の文字列は各要素を書き換えることが可能ですが、ポインタ版ではそれができません。 ポインタ版の方では、指し示す先を変更することができます。

ポインタ版の方は、配列版の文字列を指し示すように変更すれば、 ポインタ経由で、配列の要素を書き換えることが可能になります。

#include <stdio.h>

int main(void)
{
    char str1[] = "abcde";
    char* str2 = "abcde";


    str1[2] = 'x';

#if 0
    str2[2] = 'x';    /* これは正しくない */
#endif

    str2 = str1;      /* 書き換え可能な配列の方を指し示すように変更 */
    str2[2] = 'z';    /* これは正しい */

    puts( str1 );
    puts( str2 );

    return 0;
}

実行結果:

abzde
abzde

引数や戻り値でのポインタの使用

関数の引数や戻り値で、配列そのまま受け渡しすることはできません。 これは、実引数や戻り値に配列を指定しても、暗黙的にポインタへ変換されてしまうためです。

ポインタとして受け渡しされることは、巨大なデータをやり取りする際には、むしろ好都合です。 例えば、数千バイトにも及ぶ巨大な構造体を、そのまま受け渡すよりも、 その構造体を指し示すポインタ(32ビット環境ならば恐らく 4バイト)を受け渡す方が、ずっと軽い処理で済みます。

引数にポインタを使うことによって、2つ以上の戻り値の代替になります。 結果を受け取る変数のメモリアドレスを実引数として渡すことによって、関数内でそのメモリアドレスに結果を代入してもらいます。 この方法の場合、関数の作成者の意図に反して、NULL が渡される可能性を考慮しなければなりません。

戻り値がポインタとなるケースは、(static が付かない)ローカル変数のメモリアドレスを返してしまうミスを犯さないように注意が必要です。

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

char* getString(int max);

int main(void)
{
    char* str;

    str = getString( 5 );
    puts( str );  /* 受け取った文字列を出力 */

    return 0;
}

/*
    標準入力から max以下の文字数の文字列を受け取る。
    引数:
        max:	最大文字数。1~255
    戻り値:
        受け取った文字列。
*/
char* getString(int max)
{
    char str[256];

    assert( 1 <= max && max <= 255 );

    printf( "%d文字以内の文字列を入力して下さい。\n", max );
    fgets( str, max+1, stdin );		/* 改行文字のことを考えて +1 */

    return str;
}

この例のように、static を付けずに宣言されたローカル変数のメモリアドレスを返すプログラムは危険です。 このようなローカル変数は、自動記憶域期間を持ちますから、関数を抜け出した後は、アクセスしてはいけません。 コンパイルはできるものの、その動作は未定義なものになります。

動的なメモリ割り当て

プログラムの実行中、任意のタイミングでメモリ領域を確保し、任意のタイミングで解放することができます。 このようなメモリ割り当ての手法を、動的メモリ割り当てとか、ダイナミックアロケーションなどと呼びます。

動的メモリ割り当てを行うには、malloc関数calloc関数realloc関数のいずれかを使います。
動的に確保されたメモリ領域は、使い終わった後、free関数で後始末をする必要があります。

malloc関数が最も基本的な関数であり、指定した大きさ分の領域を確保します。
calloc関数は、配列のための領域確保を想定しており、要素1つ分の大きさと、要素数を指定して領域を確保します。 malloc関数の場合と異なり、確保した領域の全ビットが 0 で埋められます。
realloc関数は、1度動的に割り当てた領域の大きさを、拡張あるいは縮小する関数です。

動的なメモリ割り当てを使用する場合、以下の点に注意する必要があります。

malloc関数、calloc関数、realloc関数に関して、

free関数に関して、

これだけ多くのことを考慮しなければならないため、動的なメモリ割り当ては難しい部類の機能と言えます。 本当に必要な場面に限って使用し、不用意に使いすぎないようにするべきです。

多重間接参照

ポインタがポインタを指し示しているような使い方を、ポインタへのポインタと呼びます。 これは例えば、以下のように記述します。

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

ポインタ変数宣言の際の「*」の個数が増えることで、ポインタへのポインタを表しています。

上記のポインタへのポインタpp から、一気に変数num の値を間接参照するような参照の仕方を、多重間接参照と呼びます。

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

多次元配列

配列のイメージは、メモリ上に連続的に要素が並んだ状態です。 例えば、array[6] という配列であれば、次のようなイメージになります。

配列のイメージ

これに対し、多次元配列は、次のようなイメージになります。

二次元配列のイメージ

この図の場合だと、要素が行と列という2つの方向に並ぶ二次元配列です。 最初の図の方は、一次元配列と呼ぶことができます。

次のサンプルは、二次元配列を使ったプログラムです。

#include <stdio.h>

int main(void)
{
    int array[5][6];
    int i, j;


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

    /* 要素を出力 */
    for( i = 0 ; i < 5; ++i ){
        for( j = 0; j < 6; ++j ){
            printf( "%02d ", array[i][j] );
        }
        printf( "\n" );
    }

    return 0;
}

実行結果:

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

この二次元配列は、配列の配列ですから、int型の二次元配列は int型配列へのポインタで受け取れます 少々複雑で読みづらいですが、次のように関数化することができます。

#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

int (*array)[ARRAY_COL_NUM] という少々複雑な表現になっていますが、( ) は必要です。 ( ) が無いと意味が変わってしまいます。

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

const修飾子

const修飾子は、変数の書き換えを不許可にする効果があります。 使い方には次の4通りがあります。

const int num = 100;          /* num は書き換えられない */
int* const ptr = &num;        /* ptr は書き換えられない */
const int* ptr = &num;        /* ptr が指し示す先の値を書き換えられない */
const int* const ptr = &num;  /* ptr自身と指し示す先の値を書き換えられない */

「const」と「*」の位置関係が重要です。 「*」より左側に「const」が登場するのなら、そのポインタ変数が指し示す先が書き換えられなくなり、 「*」より右側に「const」が登場するのなら、そのポインタ変数自身が書き換えられない(指し示す先を変更できない)ようになります

関数ポインタ

関数を指し示すようなポインタ変数を作ることもできます。 これを関数ポインタと呼びます。

例えば、次のような関数が宣言されていたとします。

int func(const char* str);

この関数を指し示す関数ポインタは、次のように宣言できます。

int (*func_ptr)(const char*) = func;

あるいは、typedef を使って、

typedef int (*func_ptr_t)(const char*);  /* 型名を定義 */
func_ptr_t func_ptr;                     /* 定義した型名を使って、変数宣言 */
func_ptr = func;                         /* 変数へ、関数のアドレスを代入 */

このように宣言された関数ポインタfunc_ptr を使って、指し示されている関数func を呼び出すには、次のように記述します。

int ret = func_ptr( "abcde" );
int ret = (*func_ptr)( "abcde" );

この2つの記述は同じ意味になるので、どちらを使っても構いません。


練習問題

まとめとして、多めに練習問題を用意しました。★の数は難易度を表します。

問題① 次のプログラムで、標準出力に出力される内容を答えて下さい。[★]

#include <stdio.h>

int main(void)
{
    int num1 = 10;
    int num2 = 20;
    int* ptr = NULL;


    ptr = &num1;
    printf( "%d\n", *ptr );

    ptr = &num2;
    printf( "%d\n", *ptr );

    num1 = 15;
    num2 = 25;
    printf( "%d\n", *ptr );

    ptr = &num1;
    printf( "%d\n", *ptr );

    return 0;
}

問題② 次のように宣言された変数があります。

const char* str = "abcde";

このとき、「*str」のように間接参照した先にあるものは何ですか? [★]

問題③ 次のプログラムを実行すると、"NG" と出力されます。 "OK" と出力されない理由を説明して下さい。[★]

#include <stdio.h>

int main(void)
{
    const char name1[] = "John";
    const char name2[] = "John";


    if( name1 == name2 ){
        puts( "OK" );
    }
    else{
        puts( "NG" );
    }

    return 0;
}

問題④ 次のように定義された配列があります。

int table[] = { 0, 10, 20, 30, 40, 50, 60, 70 };

この配列table のメモリアドレスを printf関数の "%p" 変換指定子を用いて調べたところ、0x0013D684 でした。 このとき、配列table の末尾の要素70 のメモリアドレスが幾つであるか答えて下さい。 ただし、sizeof(int) は 4 であるものとします。[★★]

問題⑤ 次のような関数を作成しました。

char* strDuplicate(const char* s)
{
    char* dup = malloc( strlen( s ) + 1 );
    strcpy( dup, s );

    return dup;
}

この関数は何をしているか説明して下さい。 また、この関数を実際に使用したプログラムを作成して下さい。[★★]

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

問題⑦ 文字列の中から特定の文字を探し出し、そのメモリアドレスを返す strchr関数という標準ライブラリ関数が存在します。この関数は、次のように宣言されています。

char* strchr(const char* s, int c);

文字列s の先頭から順番に文字を調べ、c と一致するものが登場したら、そのメモリアドレスを返します。 もし、文字列s の末尾までの間に一致するものが登場しなかったら NULL を返します。 c は int型ですが、関数内部では char型として比較されます。 また、文字列s の末尾にある '\0' を探し出すことも可能です。
strchr関数と同じことをする関数を自作して下さい。[★★★]

問題⑧ 2つの文字列を連結する strcat関数という標準ライブラリ関数が存在します。この関数は、次のように宣言されています。

char* strcat(char* s1, const char* s2);

s1 の末尾に s2 を連結させます。戻り値は s1 と同じ値をそのまま返します。 この標準ライブラリ関数と同じことをする関数を自作して下さい。[★★★]

問題⑨ 三角関数sin (サイン)、cos(コサイン)、tan (タンジェント) を求める標準ライブラリ関数が math.h に宣言されています。それぞれの名前は sincostan で、double型の引数と、double型の戻り値を持ちます。引数はラジアン単位で指定します。
これらの関数のいずれかを指し示して呼び出せるような関数ポインタを定義し、実際に使用するプログラムを書いて下さい。[★★]

問題⑩ 標準入力から、int型のデータを次々と受け取り記憶していき、0 が入力されたら終了するとします。 全ての入力を受け取り終えた後、一番大きい数値から順番に上位の 10個を出力するプログラムを作成して下さい。
入力されたデータが 10個に満たない場合は、存在する個数分だけ出力して下さい。[★★★]

問題⑪ 標準入力から、文字列のデータを次々と受け取り記憶していき、"exit" と入力されたら終了するとします。 全ての入力を受け取り終えた後、一番文字数の多い文字列から順番に上位の 10個を出力するプログラムを作成して下さい。
入力されたデータが 10個に満たない場合は、存在する個数分だけ出力して下さい。[★★★]

問題⑫ C言語の関数は、戻り値を1つしか持てません。 複数の戻り値が必要な場合の解決手段を2つ以上挙げて下さい。[★]

問題⑬ 標準ライブラリ関数の memcpy関数を自作して下さい。[★★]

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

#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 );

    return 0;
}

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


解答ページはこちら

参考リンク

更新履歴

'2018/3/14 全面的に文章を見直し、修正を行った。

'2018/3/12 「ポインタのポインタ」を「ポインタへのポインタ」に統一。

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

'2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

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

▼更に古い更新履歴を展開


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

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

次の章へ(第39章 ファイルの利用)

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

Programming Place Plus のトップページへ