khurata’s blog

khurata’s blog

配列と文字列とポインタ

(もともと「Yahoo!知恵袋」の「知恵ノート」だったものを転載しています)
(最終更新日時:2015/10/7)投稿日:2012/3/24

はじめに

 C の配列と文字列とポインタの関係を、サンプルプログラムも使って、しつこく説明します。 特に断りが無い限り、このノートにおける諸説明は、規格合致処理系に関するものです。

 なお、この「知恵ノート」は、下記サイトをかなり参考にしています。
http://kmaebashi.com/programmer/pointer.html
上記サイトを見て理解出来る方には、この「知恵ノート」は必要ありません。

 

基礎

 まずは、C の「配列と文字列とポインタ」を学ぶにあたって必須の基礎を挙げます。 これらを全てきちんと理解しておかないと、混乱を引きずります。

a) C で扱う変数定数リテラルは、全てメモリに置かれる。 メモリとは「たくさんの小箱を一列に並べた」ようなもので、それぞれの小箱にアドレスという、それぞれ異なる整数値が付けられ区別されている。 隣り合う小箱のアドレス値は、連続した整数値である。 1つの小箱の大きさ(ビット幅)は、必ず1バイトである。 従って、1つの小箱には1バイトで表せる値が格納出来る。 なお、C の1バイトは少なくとも8ビット以上と規格で決まっており、ほぼ全ての場合において8ビットであるが、何ビットであるかは処理系依存である。
(参考 https://khurata.hatenablog.com/entry/2019/04/04/044010

b) により、必要とする「小箱」の個数が違ってくる。 たとえば char型なら小箱1つ、int 型なら小箱4つ、など。 この「必要な小箱の個数」を「バイト幅」などとも言う。 この個数は sizeof で知ることが出来る。 char型は必ず1バイト幅である、と規格で決められている。

c) 1つの型・変数・定数・リテラルが複数の小箱を要する場合、それらは連続している。 int型が「小箱2つと、別のところにある小箱2つ」という事は無い。 また、1つの配列の要素は全て連続した小箱群に配置される。
(定数とリテラルは異なる;筆者は、リテラルは「即値」、定数は「値が変化しない変数、もしくはリテラルに名前を付けたもの」、と解釈している)

d) C で扱うアドレスは、コンピュータが搭載しているメモリのアドレスとは別物である。つまり、a)で述べた「メモリ」とは、コンピュータが搭載している物理的なメモリそのものではなく、あくまで「C で扱うメモリ」である。 たとえ実機のメモリが1ワード36ビットという「半端なサイズ」であっても、C で扱うメモリは、ひと箱1バイトである。

e) ポインタとは、C で扱うアドレス値と型を保持するための型を付された変数である。 つまり「ポインタ」とは、「ポインタ型変数」の事である。 アドレス値は整数値であるが、NULL(というマクロ定数)である事もある。 NULL とは、「どのアドレスでも無い」ことが保証されている値である(ただし C++ では NULL ではなく 0 を使う)。

f) char型は文字型ではなく「short より短い整数型」であって、unsignedsigned が有るし計算も出来る。 1つの文字を ' で囲むと文字リテラルとなり(これは多くのプログラマに「文字」と呼ばれるが)文字コードという数値を表す。 これは暗黙的に int型になる(ただし C++ では char型)。 だから int a = 'A'; も正しい。
(参考 https://khurata.hatenablog.com/entry/2019/04/04/050217
(2つ以上の文字を ' で囲んだ場合、そのリテラルの型は int となり、値は処理系定義となるが、このような書き方は推奨しない)

g) C における文字列とは、'\0'(ヌル文字文字コード値が 0 である文字のこと)で終わる、連続した0個以上の文字(コード)の事を言う。 つまり文字列は文字(コード)の並び+'\0' であるが、文字(コード)の並びが必ずしも文字列であるわけではない。 最小の文字列は、ヌル文字だけから成る。 なお、ヌル文字は「文字列の長さ」には含めない。 従って最小の文字列の長さは0である。

h) 0個以上の文字を " で囲むと、初期化代入の右辺においては文字列リテラルになり、自動的に末尾にヌル文字が挿入され、暗黙的に char [ ]型になる(ただし C++ では const char [ ]型)。 " を2つ連続して "" と書けば、これはヌル文字だけから成る最小の文字列、ヌル文字列(ヌルストリングとも言う)リテラルとなる。 それ以外の右辺においては文字列リテラルの先頭を指すポインタ型になる。

i) α[β] という配列の書き方は、宣言の時は「メモリ上に連続して配置された要素」を意味するが、式として評価される時は *(α+(β)) というポインタ演算式に読み替えられ、この際の α は「配列先頭要素のアドレス」を表す(※1)。

j) 上記 h)と i)により、文字列リテラルは、その文字列の先頭要素のアドレスとして扱われる。 文字列リテラルには名前が無いので、この char [ ] は「無名配列」であるが、この配列を取り扱えば、それは i)におけるαを扱う事に相当するためである。 これにより、「char * ポインタに文字列リテラル(の示すアドレス)を代入する」、という事ができる。 この事は、 char [ ] 配列の各要素に char 型値を代入する事と見た目は似ているが、内容は全く違う

 ……上記のうち、特に d) は混乱されるかも知れません。 実際、多くの C 処理系におけるアドレスは、OS が提供するメモリのアドレスそのものです。
 しかしそれは処理速度面や実装の容易さから選ばれた1つの解に過ぎません。 たとえば C のインタプリタ仮想マシンを考えた場合、その「メモリ」は配列であるかもしれない、ポインタは構造体であるかもしれない、という事は容易に想像出来るでしょう。
 また、1つのソースプログラムが、16ビット処理系や 32ビット処理系、64ビット処理系それぞれでコンパイルして正しく動くという事は、普通に有る事ですが、そのポインタが 16ビット長なのか 32ビット長なのかはソースを見ても分かりません
 つまり、「C のソースコードの世界」・「C の世界」におけるポインタやアドレスというのは、実際のメモリやアドレスとは直接の関係が無いのです。

 多くの人が「ポインタはメモリアドレス値を持つ」と説明しているのは、「C プログラムをコンパイルして得られた実行可能プログラムではほぼ全てそうなっている」ので、「そう言うほうが説明しやすい」からですが、それは C の説明としては必ずしも正しくありません。

 また、上記 f) から分かる事は、「文字配列」というものは、そもそも C には存在しない、という事です。 char配列で文字列を表してはいますが、それはあくまで整数の配列でしかありません。 int配列を文字配列とは言えないのと同様です。
 つまり、「ヌル文字要素を含む char配列」は文字列を表現出来ますが、これを文字配列と呼ぶ事は正確な言葉遣いではない、という事です(それはそもそも数値配列だから)。 ただ、文字配列という言い方は広く使われていますし、意味も通じますので、分かって使うぶんには差し支えありません

 i) については、これだけ読んでも、どういう事か分かりにくいかも知れません。 実は、配列を書く時に使うカッコ [ と ] は、配列を宣言する時と、配列を使う時とで、意味・働きが違うのです。
 [ ] を使って配列を宣言すると「メモリ上に並んだ各要素」を意味しますが、配列を使う時の [ ] は、ポインタ演算に「置き換えられ」ます。 a[ 1 ] = 5; という式は、コンパイラによって *( a + ( 1 ) ) = 5; というポインタ演算式に「読み替え」られるのです。 この [ ] を配列演算子と呼ぶのですが、これは宣言の時と評価の時とで意味が全く違う演算子です。

 ポインタ演算と、(評価時の)配列演算子のような、「実は同じ意味だけれども違う書き方が出来る」文法を syntax sugar構文糖糖衣構文)と言います。 そして、この際の配列名は、特例を除いては(※1)「配列先頭要素のアドレス」を意味しています。 これはきわめて重要な事ですので、サンプルプログラムでもしつこく説明します。

※1
 配列の名前は、評価されるにあたって、「配列先頭要素のアドレス」を意味するが、ややこしい事に、少なくとも次の2つに関してはそうではない、という特例がある。

◆sizeof 演算子の対象である場合
 配列 int a[10]; における sizeof( a ) は、sizeof( &a[0] ) ではない。 sizeof( a ) は、配列全体のバイトサイズを返す。 従ってこの場合は、sizeof( int ) *10 と等しい値を返す。

◆& 演算子の対象である場合
 配列 int a[10]; における &a は、&( &a[0] ) ではない。 &a は、配列全体の型のアドレスを返す。 従ってこの場合は、「int[10]型のアドレス」を返す。

 

サンプルプログラム

 以下からソースコードをダウンロードして、その内容を眺めつつ、実際に実行してみてください。 上記「基礎」を見て、ピンと来なかった方でも、実際の動きを見れば、納得していただける事もあるでしょう。
about_arraystring.c https://box.yahoo.co.jp/guest/viewer?sid=box-l-xrtebjfldr76wfk3w6iw3nneza-1001&uniqid=bb99a9a5-f9c3-40da-95a4-992ab8948dbe&viewtype=detail
ソースコードだけで1万文字を越えるので、「Yahoo!ボックス」サービスにアップロードしました)

※ 「Yahoo!ボックス」がいつの間にか共有サービスをやめてしまっていたので、下記にアップロードし直しました(2021/01/09)。

http://khurata.dyndns.dk/QA/about_arraystring.zip

 

まとめ

 基礎およびサンプルプログラムから分かる事は、次のようにまとめられます。

1) C の配列の実体は、各配列要素をメモリ上に並べたものであり、これはポインタとは全く違う。 しかし各要素にアクセスする時は、*( α+( β ) ) というポインタ演算を用いる。


2) 宣言の中ではなく評価式の中で使う配列演算子は、上記のポインタ演算を簡略化して書く記法である。 そのため配列演算子はポインタないしアドレス値を必要とする。 従って、評価される α[β] のαとβは、どちらかがポインタかアドレスでなければならず、残る一方が整数値でなければならない。


3) 配列変数の名前だけを評価すると、「配列先頭要素のアドレス」を示す。 これは「配列としての縛りがあるポインタ」とも言える(※1)。


4) 「配列としての縛り」とは、書き換え出来ないということ。 char a[ ] = "文字列"; とした後の a は、配列先頭要素のアドレスを示しているが、このアドレスは当然のことながら書き換える事が出来ない(もし、これが書き換え可能だったら、書き換えた時点で配列の要素がメモリ上を移動する事になってしまうが、C ではそんな事は出来ない)。


5) つまり char a[ ] = "文字列"; の a と、char * const b = "文字列"; の b は、似たように使える。


6) 配列は、それぞれの要素内容を書き換える事はもちろん出来るが、要素数は変えられないし、要素のアドレスを変える事も出来ない。


7) ポインタ変数は配列演算子によって配列のようにも使えるし、内容値を書き換える事も出来る。 また、ポインタは内容値を書き換えて、「指し示すメモリ位置」を変える事も出来る。 よってポインタは、工夫次第で「要素数の変わる配列」のように使う事も出来るし、メモリ上を移動する配列のように使う事も出来る。

 

関連質問

http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q12110460847
(xrqo3 さん https://chiebukuro.yahoo.co.jp/my/xrqo3 の明解な回答に助けられました)

 

おわりに

 なにしろこの「知恵ノート」自体が長いので、1度読んだだけでは、分かりにくいかも知れません。 しかしサンプルプログラムを含め、何度も読んでいただければ、必ず理解にたどり着けるものと思います。
 正確な書き方をしようとすると長文になってしまうのですが、「まとめ」で書いた事が理解出来さえすれば良いと思います。
(const については、また別途説明させていただく機会を考えておりますが、検索していただければ充分にお分かりいただけると思います)

 

★追記1
rank_0ut さん https://chiebukuro.yahoo.co.jp/my/rank_0ut に、本ノートを紹介していただきました。ありがとうございます!
http://note.chiebukuro.yahoo.co.jp/detail/n48069
見ていただけるだけでも充分ありがたいのですが、まさかこうして紹介していただけるとは思ってもいなかったので、本当に嬉しく思います。本ノートは言語理論的な裏付けという点では正直乏しいのですが、実際にプログラミングする人のお役に立てれば幸いです。
もし間違いや改善した方が良い点が有ったら、随時改訂したいと思っています。

★追記2
lehshell さん https://chiebukuro.yahoo.co.jp/my/lehshell からご指摘をいただき、少し改稿しました。

★追記3
表現がまずい所が色々有りましたので、サンプルプログラムと本文共々、手を加えました。
(2012/09/23)

★追記4
文字列定数について間違っていた所をサンプルプログラム共々、多々訂正しました。
(2012/12/13)

★追記5
宣言と定義について混乱したり誤っていた所をサンプルプログラム共々、訂正しました。
(2013/02/06)

★追記6
C と C++ の違いや、暗黙型についての誤り、細かい言い回しについて、サンプルプログラム共々、訂正しました。
(2013/07/29)

★追記7
「基礎」の記載を多少詳細にしました。また、細かい言い回しを修正しました。
(2013/09/19)

★追記8
リテラル」と書くべきところを誤って「定数」と書いていた数カ所を修正しました。
(2014/04/11)


(転載以上)