先頭へ戻る

ポインタ②(配列や文字列との関係性) | Programming Place Plus C言語編 第32章

Programming Place Plus トップページ -- C言語編

先頭へ戻る

この章の概要

この章の概要です。


アドレス計算

配列の各要素のメモリアドレスを調べてみましょう。

#include <stdio.h>

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

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

    for( int 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 ずつ増えていることです。この 4 という値の正体は、sizeof(int) です。そのため、sizeof(int) が 4以外の環境であれば結果は変わりますが、とにかく、int型の大きさ分ずつずれていきます。

これは、配列の特徴の1つです。配列は、各要素がメモリ上で連続的に隙間なく並ぶことが保証されています。この性質を利用して、配列の要素数を調べる方法があります。

#include <stdio.h>

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

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

    return 0;
}

実行結果:

5

このように、同じ配列の要素を指すポインタ同士を減算すると、2つのポインタの間にある要素数が取得できます。ポインタの値はメモリアドレスですから、単純に減算すると「0x002DF808 - 0x002DF7F4」のような計算になって、20 が得られそうですが、そうはならないということです。

なお、異なる配列を指しているポインタ同士での減算は、未定義の動作となるので注意してください

ポインタ同士の減算で得られる値は、ptrdiff_t型です。ptrdiff_t型の大きさは、環境によって異なる可能性がありますが、符号付き整数型であることは確かです。

ptrdiff_t型を printf関数や scanf関数で使用する際には、"%td" という変換指定を使います。

#include <stdio.h>

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

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

    return 0;
}

実行結果:

5

ところで、「&array[5] - &array[0]」という式は、配列の末尾を超えたところを使っているように見えます。array の要素数は 5 なので、添字に 5 を使うのは不正でないのでしょうか。

これは実は問題ありません。ポインタ同士で減算を行う際には、配列の末尾要素の1つ後ろまでは使っても良いことになっています。

ただし、array[5] の位置に有効な要素はありませんから、その要素を参照しようとする行為は未定義の動作です。

また、ポインタの加算も有効です。

#include <stdio.h>

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

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

    for( int* p = &array[0]; p != &array[SIZE_OF_ARRAY(array)]; ++p ){
        printf( "%d\n", *p );
    }

    return 0;
}

実行結果:

0
10
20
30
40

ポインタ変数に対するインクリメント操作では、メモリアドレスが +1 されるのではなく、sizeof(指し示す先の型) 分だけ加算されます。デクリメント操作も同様ですし、2以上の加算や減算でも同様です。このため、配列の要素を指すポインタをインクリメントすると、「次の要素へ」という感覚で扱えます。

ところで、ポインタ変数に対するインクリメントやデクリメントに関して、優先順位の問題があります。

*p++;

これは、ポインタ変数がインクリメントされてから間接参照するのか、間接参照した先にある変数をインクリメントするのか、という問題です。

この場合は「ポインタ変数がインクリメントされてから間接参照される」が正解ですが、非常に分かりづらいです。コンパイラが優秀でなかった時代には、このような書き方をすると、効率が良くなることもあったようですが、いまや時代錯誤です。現代的には、こういう小手先の最適化は避けて、読みやすさを重視すべきです。

たとえば、次のように ( ) を補うと明確です。

*(p++);    // p をインクリメントしてから、間接参照
(*p)++;    // 間接参照した先にある値をインクリメント

配列とポインタの関係性

当たり前といえば当たり前のことですが、「配列とポインタはまったく異なるものです」。一見、どちらでも同じように動作する場面があるため混乱している人もいるようですが、明確に異なるものであるという認識をもつことが大切です。

まず、配列を使ったサンプルプログラムを挙げます。

#include <stdio.h>

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

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

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

    return 0;
}

実行結果:

0
10
20
30
40

このプログラムは、ポインタを使って、次のように書き換えることが可能です。

#include <stdio.h>

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

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

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

    return 0;
}

実行結果:

0
10
20
30
40

ポインタ変数p を追加し、配列の先頭要素のメモリアドレスで初期化しています。printf関数を呼び出す際には、array[i] の代わりに、p[i] と記述しましたが、先ほどの配列版プログラムとまったく同じ実行結果になります。

配列とポインタが同じものである錯覚する原因の1つがここにあります。実のところ、以下の2つは本当に同じ意味です。

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

しかし、配列=ポインタではありません。

上記の2つの文が同じ意味になる理由は、「式の中で array のような配列の名前が現れたときは、暗黙的に、その配列の先頭を指すポインタに置き換わる」からです。

たとえば、array[0] = 10; のように書いたとき、array の部分がポインタに置き換わり(仮に p という名前とします)p[0] = 10; と同じになります。

func(array); であれば、func(p); のように置き換わって、ポインタを関数に渡していることになります。第25章で配列を説明したとき、配列を引数や戻り値には使えないと書きましたが、その理由がこれです。配列は配列のままでは扱えず、関数に渡す際にも、関数から戻される際にも、ポインタに置き換えられてしまうのです。この話題については、次章であらためて説明します

もう1つ例を見ておきましょう。

int array[5];
int* p;

p = array;    // p = &array[0]; と同じ

配列がポインタに変換されるので、このコードのように、ポインタ変数に配列を代入するような式が書けます。「ポインタ変数 = 配列」という代入式は適切には思えませんが、これは問題ないですし、非常によく使うコードですらあります。


ところで、ポインタ変数 p が array の先頭を指しているのなら、array[i]p[i] は同じです。では、array[i + 5]p[i + 5] ならどうでしょう?

これもやはり同じになります。添字演算子は配列に対して使っても、(配列を指している)ポインタに対して使っても同じ結果になります。添字演算子の "添字" という響きから、配列に対してしか適用できないように思えるかもしれませんが、そうではないということです。

配列はポインタに置き換えられているのだから、添字演算子の適用対象はむしろポインタであるといえます。

ところで、アドレス計算の話のところで説明したように、ポインタ変数に対する加算は、指し示す先にある要素の型の大きさの分だけ加算されます。ですから、p が配列の要素を指しているのなら、p + i は、p から i要素分だけ先の要素を指します。それはつまり、配列の i番目の要素ということですから、p + ip[i] は同じです。

printf( "%d\n", p[i] );     // ただし p は array の先頭要素を指すポインタ
printf( "%d\n", *(p+i) );   // 上と同じ意味

ですから、添字演算子を使わなくても、*(p+i) のような方法で書くことも可能です。しかし明らかに面倒ですし素直ではありません。

【上級】a[3] と *(a+3) が同じということは、*(3+a) も同じ意味である訳です。そのため、実は a[3] は 3[a] とも書けます。そんなことをする理由はまったくありませんが。


文字列

突然ですが、"abcde" のような文字列リテラルは、何型なのでしょうか?

これもよく勘違いされているようですが、文字列リテラルの正体は char型の配列です。char* のようなポインタ型ではありません。

【上級】C++ の場合は const char型の配列です。

文字列リテラルが配列であることは、次のコードからも確認できます。

printf( "%zu\n", sizeof("abcde") );

この出力結果が 6 になることから分かります(末尾に '\0' があるので 5 ではありません)。もし文字列リテラルがポインタであり、ポインタ変数の大きさが 32ビットの環境であれば、出力結果は 4 になるはずです。

文字列リテラルがポインタ型であるという勘違いが生まれる原因の1つは、次の2つがともに有効であることでしょう。

char str[] = "abcde";
char* str = "abcde";

この2つの文の初期値は同じように見えるかもしれませんが、意味は違います。

ポインタ変数str を初期化する際に現れた "abcde" は、メモリ上のどこかにある文字列リテラルです。文字列リテラルが char型の配列であり、"abcde" のような要素を持っている以上、メモリのどこかにあるはずなのです。

一方、配列の str を初期化する際に現れた "abcde" は、「配列の各要素に与える初期値」というだけの存在です。どこか別のところに、"abcde" があるわけではなく、今まさに、配列の要素に与えようとしている文字の並びにすぎません。次の2つが同じ意味だということを思い出すと、言わんとすることが分かるでしょうか?

char str[] = "abcde";
char str[] = { 'a', 'b', 'c', 'd', 'e', '\0' };

ポインタ変数 str の初期値 "abcde" は、char型の配列ですが、「配列とポインタの関係性」のところで説明したとおり、配列は暗黙的にポインタに変換されます。そのため、char型のポインタ変数の初期値として、文字列リテラルを使えます。

これはもう少し言い換えると、メモリ上のどこかにある文字列リテラル "abcde" の先頭を指すポインタに変換され、そのポインタを初期値としています。

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

配列とポインタの概念図
配列とポインタの概念図

この図が示すように、配列版とポインタ版は明確に異なります。

const修飾子

文字列リテラルは char型の配列ですから、書き換える行為ができてしまいます。しかし、文字列リテラルを書き換えようとする行為は、未定義の動作とされていますから、このようなことはしてはいけません。

"abcde"[2] = 'x';  // コンパイルできるが、未定義動作

【上級】C++ の場合は、コンパイルエラーになります。これは、文字列リテラルが const char型の配列に変更されたからです。

このように、文字列リテラルは不親切な仕様になっていますが、もし書き換えることが不適切なのであれば、明示的にコンパイルエラーになってほしいところです。そうすれば間違いようがありませんから。

文字列リテラルに関しては無理ですが、const修飾子を使うと、変数の値の書き換えを明示的に禁止することができます。

const 型名 変数名 = 初期値;

この後、変数num に何らかの値を代入しようとすると、コンパイルエラーになります。つまり、const修飾子は、その変数の値を「書き換え不可とする」という意味があります。

【上級】C言語では、const を付加して宣言された変数は、書き換えられない変数という扱いであり、定数とは異なります。一方、C++ では定数を意味しますから、C++プログラマーは、両言語で意味が異なることに注意してください。たとえば、C言語では、const付きの変数を、配列を宣言する際の要素数の指定に使うことができません(C++編【言語解説】第15章)。

実際のプログラムで確認してみましょう。

#include <stdio.h>

int main(void)
{
    const int num = 100;

    num = 0;  // コンパイルエラー
    printf( "%d\n", num );

    return 0;
}

const は、バグの少ない安全なプログラムを書くために大きな助けになる優れた機能です。初期値として与えた値を変更するべきでないときには、いつも const を付けて変数宣言する習慣は悪くありません。

const は、static や extern のような、ほかの修飾子と同時にも使えます。記述の順番も自由です。

【上級】グローバル変数に const を付加した場合、C言語では普段のルールどおり、外部結合になりますが、C++ では内部結合になります(C++編【言語解説】第15章)。そのため、C言語でも C++ でも同じ意味を持たせるためには、外部結合にしたいときはいつも extern を補い、内部結合にしたいときはいつも static を補うようにします。

ポインタ変数を const付きで宣言するときには注意が必要です。保持する値を書き換えられないようにするには、次のように宣言します。

int* const p = 初期値;

const修飾子を書く位置に注意が必要です。ポインタ変数の値を書き換えられないようにするには、「*」よりも右側に const を置くのだと理解してください

int num = 100;
int* const p = &num;  // p が保持するメモリアドレスを変更できない
*p = 200;  // OK
p = NULL;  // コンパイルエラー

「*」よりも左側に const を置くと、ポインタ変数が指し示す先にあるものを変更できない]{.s}、という意味になります。

int num = 100;
int num2 = 200;
const int* p = &num;  // p が指し示す先にあるものを変更できない
p = &num2;  // OK
*p = 0;     // コンパイルエラー

次の2つの文は同じ意味になることも注意してください。

const int* p = 初期値;
int const* p = 初期値;

ともかく、const* の位置関係が重要であって、それ以外の表記(int など)の登場順序や、空白の有無などに惑わされないようにしましょう。

なお、これらの2種類の const修飾子を同時に使用することも可能です。この場合、2つの特性が同時に適用されますから、ポインタ変数自体を書き換えできませんし、ポインタが指し示す先にあるものも書き換えできません。

int num = 100;
const int* const p = &num;  // p が保持するメモリアドレスも、指し示す先にあるものも変更できない
*p = 200;  // コンパイルエラー
p = NULL;  // コンパイルエラー


また、const付きのポインタを、const が付いていないポインタ変数へ代入することは危険を伴う行為です。コンパイルエラーにはならないのですが、たいていのコンパイラは警告を発するはずです。

int num = 100;
const int* cp = &num;
int* p = cp;  // 大丈夫?

【上級】C++ では、constポインタを const でないポインタに暗黙的に型変換しようとするコードは、コンパイルエラーになります。

int* p = (int*)cp; のように、キャストを行えば許可されますが、それによって正しさが保証されるわけではありません。次のコードはキャストで正当性を主張していますが、*p = 200; が安全とはいえません。

const int num = 100;
const int* cp = &num;
int* p = (int*)cp;  // 大丈夫?
*p = 200;     // p が指し示す先は const int だが書き換えていいのか?

配列とポインタの実用上の違い

以下の2つの文の意味は異なる訳ですが、実用上の違いはあるのでしょうか?

char str[] = "abcde";
char* str = "abcde";

まず、配列版は要素を書き換えられますが、ポインタ版はできません(してはいけません)。

#include <stdio.h>

int main(void)
{
#if 1
    char str[] = "abcde";
#else
    char* str = "abcde";
#endif

    str[2] = 'x';  // 配列版では OK。ポインタ版ではコンパイルできるが未定義の動作
    puts( str );

    return 0;
}

実行結果:

abxde

配列版ならば、このプログラムは問題なく実行できますが、ポインタ版だと未定義の動作です。

一方で、ポインタ版の方は、指し示す先を切り替えることが可能です。

#include <stdio.h>

int main(void)
{
#if 0
    char str[] = "abcde";
#else
    char* str = "abcde";
#endif

    str = "xyz";  // 配列版ではコンパイルエラー。ポインタ版では OK
    puts( str );

    return 0;
}

実行結果:

xyz

こちらは配列版だとコンパイルエラーになります。配列版での以下の文、

str = "xyz";

これは、str も "xyz" も配列なので、それぞれポインタに変換されます。そのため、以下のように書いたことと同じです。

&str[0] = &"xyz"[0];

これはコンパイルできないわけですが、その理由は、代入の左辺側が何かを受け取れるような形ではないからです。普通、配列の先頭要素へ何かを代入するときには、

str[0] = 100;

のように書きます。このとき、&演算子は付いていませんが、先ほどの形では &演算子が付いてしまっています。

このように、配列が先頭要素を指すポインタに置き換えられるというルールのため、配列で表現された文字列の内容が一致しているか調べる際に、等価演算子が使えません。代わりに、strcmp関数を使います。

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

int main(void)
{
#if 1
    char str[] = "abcde";
#else
    char* str = "abcde";
#endif

    if( str == "abcde" ){
        puts( "==演算子による比較" );
    }
    if( strcmp( str, "abcde" ) == 0 ){
        puts( "strcmp関数による比較" );
    }

    return 0;
}

実行結果(配列版):

strcmp関数による比較

配列版で等価演算子を使うと、等価演算子の左辺も右辺もポインタに変換されますから、次のように書いたのと同じです。

if( &str[0] == &"abcde"[0] )

変数str のメモリアドレスと、文字列リテラル"abcde" のメモリアドレスは異なるため、この if文は絶対に真になりません。目的はメモリアドレスが一致しているかどうかを調べることではなく、文字列の内容が一致しているかどうかを調べることですから、これは間違っています。

そこで、strcmp関数を使います。strcmp関数は、2つの文字列の先頭から1文字ずつ順番に比較処理を行ってますから、これが正しいです。ポインタ版でも、strcmp関数を使えば文字列の内容を比較できますから、正しく比較できます。

ポインタ版での等価演算子の使用は、うまくいくかどうか分かりません。ポインタ変数str の初期値として与えた "abcde" と、if文のところに書いた "abcde" が、メモリ上の同じものを指すようにコンパイルされていれば、if文は真となるはずです。しかし、文字列リテラルの内容が同一だからといって、メモリ上で同じものを共有する保証はありません

結局どうすればいいのかという答えではなく、何が起きるのか、理屈を理解するようにしてください。文字列の内容を後から書き換えるのであれば、配列版を使うのが適切です。ポインタ版では、文字列リテラルを指し示すので、書き換える行為は未定義の動作となり不適切です。

また、文字列の比較を行うときには、文字列のメモリアドレスの一致を知りたいのか、文字列の内容の一致を知りたいのかをきちんと区別してください

文字列の長さ

ある文字列に含まれている文字数を知りたい場面はよくあります。配列として宣言された文字列変数であれば、sizeof演算子を利用することが考えられます。

#include <stdio.h>

int main(void)
{
#if 1
    char str[] = "abcde";
#else
    char* str = "abcde";
#endif

    printf( "%zu\n", sizeof(str)-1 );

    return 0;
}

実行結果(配列版):

5

実行結果(ポインタ版):

3

文字列は、末尾の '\0' の分まで確保されているので、sizeof演算子の返す値から -1 する必要があります。

sizeof演算子を使う方法だと、ポインタ版の方では文字数を取得できません。ポインタはポインタに過ぎないのであって、どこを指し示していようと、同じ大きさです。32ビット環境であれば、恐らく、4 を返すことでしょう。

文字数を知るための汎用的な手段は、strlen関数を使うことです。strlen関数を使うには、<string.h> をインクルードします。

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

int main(void)
{
#if 1
    char str[] = "abcde";
#else
    char* str = "abcde";
#endif

    printf( "%zu\n", strlen(str) );

    return 0;
}

実行結果(配列版):

5

実行結果(ポインタ版):

5

strlen関数は、実引数で指定した文字列の文字数を、size_t型で返します。strlen関数は '\0' までの文字数をカウントして返します。

"abc\0de";

このように、間に '\0' が挟み込まれているような文字列を渡すと、全体の文字数を返してくれないことには注意が必要です

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

int main(void)
{
    printf( "%zu\n", strlen("abc\0de") );

    return 0;
}

実行結果:

3

複合リテラル

第26章で、構造体に対する複合リテラルを解説しました。今回は、配列に対する複合リテラルを解説します。

とはいえ基本的には同じです。次の構文によって、名前のない配列型のリテラルを記述できます。

(型名){ 初期化子並び }

「型名」には配列型の名前を入れます。要素数を指定しなかった場合は「初期化子並び」に記述した初期値の個数で決定されます。文字の配列の場合は、「初期化子並び」のところに文字列リテラルを記述できます。また、要素指示子(第25章)を使っても構いません。

なお、可変長配列(第25章)には使えません。

複合リテラルを関数内で使った場合、生成されたオブジェクトは、記述した位置を囲むブロック内でだけ有効であり、自動記憶域期間(第22章)をもちます。一方、関数の外側で使った場合は、静的記憶域期間(第22章)をもちます。

複合リテラルによって生成されたオブジェクトは配列型ですから、その値を代入する際には、ポインタ型に変換されます。そのため、変数に受け取るのならポインタ型で受け取らなければなりません。

#include <stdio.h>

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

    for( int i = 0; i < 4; ++i ){
        printf( "%d\n", pa[i] );
    }

    return 0;
}

実行結果:

0
1
2
3

複合リテラルによって作られた配列は、どうあってもポインタを経由して扱うことになるので、このサンプルプログラムのように、後から配列の要素数が必要になるケースでは不便です。要素数を表す記号定数をつくった方がいいかもしれません。

関数の実引数に渡す例としては、たとえば次のようになります。

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

int max_element(const int* array, size_t size)
{
    assert(array != NULL);
    assert(size >= 1);

    int max = array[0];

    for( size_t i = 1; i < size; ++i ){
        if( max < array[i] ){
            max = array[i];
        }
    }
    return max;
}

int main(void)
{
    printf("%d\n", max_element( (int[]){ 4, 7, 2, -1, 3 }, 5 ) );

    return 0;
}

実行結果:

7


練習問題

問題① 配列に文字列を代入するとき、strcpy関数を使わないといけない理由を説明してください。

問題② 次の4つの printf関数の出力結果をそれぞれ答えてください。

char str1[] = "abcd";
char* str2 = "abcd";

printf( "%zu\n", sizeof(str1) );
printf( "%zu\n", strlen(str1) );
printf( "%zu\n", sizeof(str2) );
printf( "%zu\n", strlen(str2) );

問題③ strlen関数と同じことをする処理を書いてください(関数化する必要はありません)。

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

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

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


解答ページはこちら

参考リンク


更新履歴

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



前の章へ (第31章 ポインタ①(概要))

次の章へ (第33章 ポインタ③(引数や戻り値への利用))

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
Twitter でツイート Twitter をフォロー LINE で送る
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー