ポインタ②(配列や文字列との関係性) | 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 };
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 ずつ増えていることです。この 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型の大きさは、環境によって異なる可能性がありますが、符号付きの整数型であることは確かです。そのため、printf関数や scanf関数で ptrdiff_t型の値を使う際には、"%d" や "%ld" を使えますが、int型や long int型の限界値を越えないように注意してください。
C99 では、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 };
int* p;
for( 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 };
int i;
for( 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 i;
int* p = &array[0];
for( 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;」のように書いたときは、添字を伴っているので、配列が単独で表れていませんから、特別なことは起こりません。しかし、「func( array );」のような場合、配列だけで表れているため、ポインタに変換されてしまいます。
第25章で配列を説明したとき、配列を引数や戻り値には使えないと書きましたが、その理由がこれです。配列は配列のままでは扱えず、関数に渡す際にも、関数から戻される際にも、ポインタに置き換えられてしまうのです。この話題については、次章であらためて説明します。
もう1つ例を見ておきましょう。
int array[5];
int* p;
p = array; /* p = &array[0]; と同じ */
配列」という代入式は適切には思えませんが、これは問題ないですし、非常によく使うコードですらあります。
ここで疑問なのは、添字演算子の役割です。ポインタ変数に対しても、 p[i] のような記述が許されるというのはどういうことでしょうか。
実は、次の2つの文は同じ意味です。
printf( "%d\n", p[i] ); /* ただし p は array の先頭要素を指すポインタ */
printf( "%d\n", *(p+i) );
アドレス計算の話のところで説明したように、ポインタ変数に対する加算は、指し示す先にある要素の型の大きさの分だけ加算されます。ですから、p が配列の要素を指しているのなら、(p+i) は、p から i要素分だけ先の要素を指します。その位置に対して、間接演算子を適用するという式は、結局のところ array[i] のことを意味します。
つまり、添字演算子を使わなくても、*(p+i) のような記述で書くことも可能なのです。しかし明らかに面倒ですから、添字演算子が用意されています。
a[3] と *(a+3) が同じということは、*(3+a) も同じ意味である訳です。そのため、実は a[3] は 3[a] とも書けます。そんなことをする理由はまったくありませんが。
突然ですが、"abcde" のような文字列リテラルは、何型なのでしょうか?
これもよく勘違いされているようですが、文字列リテラルの正体は char型の配列です。char* のようなポインタ型ではありません。
C++ の場合は const char型の配列です(Modern C++編【言語解説】第2章)
文字列リテラルが配列であることは、次のコードからも確認できます。
printf( "%u\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 のイメージは次の図のようになります。
この図が示すように、配列版とポインタ版は明確に異なります。
文字列リテラルは char型の配列ですから、書き換える行為ができてしまいます。
"abcde"[2] = 'x';
不自然なコードですが、これはコンパイルが通ります。ただし、文字列リテラルを書き換えようとする行為は、未定義の動作とされていますから、このようなことはしてはいけません。
C++ の場合は、コンパイルエラーになります。これは、文字列リテラルが const char型の配列に変更されたからです(Modern C++編【言語解説】第2章)。
文字列リテラルはこのような不親切な仕様ですが、もし書き換えることが不適切なのであれば、明示的にコンパイルエラーにしてほしいところです。我々が書くプログラムでは、そういうことが可能です。そのためには、変数の宣言時に const修飾子を付加します。
const 型名 変数名 = 初期値;
この後、変数num に何らかの値を代入しようとすると、コンパイルエラーになります。つまり、const修飾子は、その変数の値を「書き換え不可とする」という意味があります。
C言語では、const を付加して宣言された変数は、書き換えられない変数という扱いであり、定数とは異なります。一方、C++ では定数を意味しますから、C++プログラマは、両言語で意味が異なることに注意してください。例えば、C言語では、const付きの変数を、配列を宣言する際の要素数の指定に使うことができません(C++編【言語解説】第15章)。
実際のプログラムで確認してみましょう。
#include <stdio.h>
int main(void)
{
const int num = 100;
#if 0
num = 0;
#endif
printf( "%d\n", num );
return 0;
}
「num = 0;」の部分を有効にすると、コンパイルエラーになります。
const は、バグの少ない安全なプログラムを書くために大きな助けになる優れた機能です。初期値として与えた値を変更するべきでないときには、常に const を付けて宣言するようにすべきです。
const は、static や extern のような、ほかの修飾子と同時に使えます。
グローバル変数に const を付加した場合、C言語では普段のルール通り、外部結合になりますが、C++ では内部結合になります(C++編【言語解説】第15章)。そのため、C言語でも C++ でも同じ意味を持たせるためには、外部結合にしたいときは常に extern を補い、内部結合にしたいときは常に static を補うようにします。
ポインタ変数を const付きで宣言するときには注意が必要です。保持する値を書き換えられないようにするには、次のように宣言します。
int* const p = 初期値;
const修飾子を置く位置に注意してください。ポインタ変数を書き換えられないようにするには、「*」よりも右側に const を置くのだと理解してください「*」を型名にくっつける「int*」派の人と、変数名にくっつける「*p」派の人がいるので、どちらでも読めるようにしておくと良いです。
置く位置を間違えると、違う意味になることがあります。例えば、次のように書くと意味が異なります。
const int* p = 初期値;
int const* p = 初期値;
この2つの文は同じ意味です。このように、「*」よりも左側に const を置くと、ポインタ変数が指し示す先にあるものを変更できない、という意味になります。
int num = 100;
const int* p = #
*p = 200; /* コンパイルエラー */
また、const付きのポインタを、const が付いていないポインタ変数へ代入することは不適切な行為です。残念ながらコンパイルエラーにならないのですが、大抵のコンパイラは警告を発するはずです。
int num = 100;
const int* cp = #
int* p = cp; /* 正しくない */
C++ ではコンパイルエラーになります。
const付きのポインタを、const が付いていないポインタ変数へ代入する必要性は普通はないはずですが、必要であれば、キャストを行えば許可されます「int* p = (int*)cp;」
また、これらの2種類の const修飾子は、同時に使うことも可能です。この場合、2つの特性が同時に適用されますから、ポインタ変数自体を書き換えできませんし、ポインタが指し示す先にあるものも書き換えできません。
以下の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( "%u\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( "%u\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( "%u\n", strlen("abc\0de") );
return 0;
}
実行結果:
3
問題① 配列に文字列を代入するとき、strcpy関数を使わないといけない理由を説明してください。
問題② 次の4つの printf関数の出力結果をそれぞれ答えてください。
char str1[] = "abcd";
char* str2 = "abcd";
printf( "%u\n", sizeof(str1) );
printf( "%u\n", strlen(str1) );
printf( "%u\n", sizeof(str2) );
printf( "%u\n", strlen(str2) );
問題③ strlen関数と同じことをする処理を書いてください(関数化する必要はありません)。
問題④ 次のように定義された配列があります。
int table[] = { 0, 10, 20, 30, 40, 50, 60, 70 };
この配列table のメモリアドレスを printf関数の "%p" 変換指定子を用いて調べたところ、0x0013D684 でした。このとき、配列table の末尾の要素70 のメモリアドレスがいくつであるか答えてください。ただし、sizeof(int) は 4 であるものとします。
------------------------------------------------------------------------
'2018/6/1 第38章から練習問題④を移動してきた。
'2018/5/14 章のタイトルを変更(「ポインタ② 基礎」->「ポインタ② 配列や文字列との関係性」)
'2018/4/5 VisualStudio 2013 の対応終了。
'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。
'2018/3/9 「const修飾子」の項に、const のポインタに関する説明を追加。
≪さらに古い更新履歴を展開する≫
Programming Place Plus のトップページへ
はてなブックマーク に保存 | Pocket に保存 | Facebook でシェア |
Twitter でツイート | Twitter をフォロー | LINE で送る |
![]() |
管理者情報 | プライバシーポリシー |