khurata’s blog

khurata’s blog

C の文字列処理の、ちょっとした何か

(もともと「Yahoo!知恵袋」の「知恵ノート」だったものを転載しています)
(最終更新日時:2014/5/23)投稿日:2013/1/14

はじめに

 「知恵袋」の質問 http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q12100266242 に回答を書こうと思ったのですが、早々に締め切られてしまったので、回答に書くつもりだったプログラムを、ここで簡単に説明したいと思います。

 

プログラム

 下記の画像をご覧下さい。 一見、短くて簡単なこのプログラム、もしかすると、あちこち変に見えるかも知れませんが、私の知る限り、これは、れっきとした規格合致プログラムです(変に見えた方は、出来れば、ご自分で打ち込んでみて、コンパイル・実行もされてみることをお薦めします)。 その事を、以降で説明致します。

f:id:khurata:20190404065328j:plain

 

4行目の説明…char配列の初期化あれこれ

 まず、配列 a の定義では、要素数を省略しています。 元の質問では、ここが問われている点でした。 C の配列宣言は、最外郭に限り、要素数を省略して良い場合があります。 こうした場合、配列の最大要素数コンパイラによって自動的に決定されます。 「後ろの要素を余裕持って空けたい」ような場合は、明示的に要素数を指定する必要があります。

 次に、char 型配列を初期化する書き方は、特別に、文字列リテラルを使う書き方が許されています。
 本来、char a[ ] = "ABC"; は、char a[ ] = { 'A', 'B', 'C', '\0' }; と書かねばなりません。 int型配列の事を考えればお分かりでしょう。 int も char も整数型ですから、基本は、同じ書き方なのです。 文字列で初期化出来るのは、あくまで「char型配列の特例」なのです。

 4行目では、更にもう1つ、あまり見かけない書き方が使われています。 C では、文字列リテラルを「ただ並べて書く」と、連結したものとして扱われます。
 つまり、"ABC" "\0" "DEFG" "\0" "HIJKL" は "ABC\0DEFG\0HIJKL" と同義であり、これは文字列リテラルですから、末尾に '\0' が自動的に付加されて、全体として 15個の要素を持つ配列になります。
 この性質を利用して、文字列定数マクロを混ぜ込む事も出来ます。

 

7行目の説明…配列のバイトサイズ

 実際に実行してみる前に、どんな値が表示されるか、お分かりでしょうか。 答えは、3、……ではありません。

 sizeof 演算子は、配列名を与えると、その全要素のバイトサイズを返します。 strlen( ) は文字数を返しますが、sizeof は、途中に '\0' が有ろうが無かろうが、関係ないのです。
 整数の配列なのですから、要素の値が 0 かどうかは、サイズとは関係無い……と考えれば、当然の結果です。 char型は、文字や文字列と関わりの深い型であり、先述した初期化の特例などもあって、ついつい特別視しがちですが、あくまでも整数型であることを忘れてはなりません。

 従って、ここでは 15 が表示されます。 規格上、char型は1バイトである事が保証されているからです。 もし実行して 15 が表示されないようなら、その処理系は規格合致ではありません。

 なお、元の質問の回答者様が書かれているように、1つの char配列の中で '\0' を区切りに使うのはイレギュラーな事であり、トラブルや誤解を招きかねません。 これは説明用のプログラムであるために、あえてこうしていますが、実地のプログラミングでは、char配列の途中を '\0' で区切るのは避けたほうが良いでしょう。

 

9行目の説明…ポインタ演算

 人によっては、a + sizeof( a ) とか、それを p と比較している所とかが、ちょっと見慣れないかも知れません。 これについては、配列の名前に関する「約束事」と、ポインタ演算について理解する必要があります。

 評価式の中における配列名は、「配列の先頭要素のアドレス」を示す、という約束事が、C には存在します。 つまり、式の中で a と書けば、&a[ 0 ] と同義になるのです。 ただし、この「約束事」には、少なくとも次に示す2つの例外があります(ややこしいですが)。

◆例外1 … 配列の名前を sizeof 演算子の対象として使うと、アドレスではなく配列全体を示す。 sizeof( α ) は、sizeof( &α[ 0 ] ) ではない。 たとえば int b[ 10 ]; について、sizeof( b ) は配列全体のバイトサイズ、すなわち sizeof( int ) * 10 を返す。

◆例外2 … 配列の名前を & 演算子の対象として使うと、配列全体のアドレスを返す。 つまり &α は、&( &α[ 0 ] ) ではない。 たとえば int b[ 10 ]; について、&b は int[ 10 ]型のアドレスを返す。

 従って、上記プログラムの a + sizeof( a ) は、「a[ 0 ] の先頭アドレスに 15 を足す」事を意味します。 また、このような、ポインタやアドレスに整数値を加減算する事を、C では特にポインタ演算と呼んでいます。

 このポインタ演算には、注意すべき重要な事があります。 ある型のポインタに、整数値 N を加算する、という書き方は、単純に ( ポインタ + N ) なのですが、その結果は、「ポインタの持つアドレス値が N だけ増す」……のではないのです。

 ポインタ演算における、アドレス値の加減算の単位は、型のバイトサイズなのです。

 たとえば、int型が 4バイトの処理系で、intポインタに 3 を足すと、そのポインタの持つアドレス値は 12 増えます。 この計算はコンパイラが自動的に行います。

 ポインタ演算には型(のバイトサイズ)が必要ですから、voidポインタのポインタ演算は、御法度です。 voidポインタのポインタ演算結果は予測出来ません。 処理系によってはエラーや警告になる事もあります。 どぉ~しても1バイト刻みのポインタ演算が「欲しい」時は、キャストして char * のポインタにしましょう(おすすめはしませんが)。

 そんなわけで、上記プログラムの a + sizeof( a ) は、必ず「配列 a の最後の要素の次のアドレス」を示します。 従って、p < a + sizeof( a ) という条件では、p は配列 a の最後を越える事はありません。 安全にループ出来るのです。

 ……残りの部分については、本ノートをお読みくださっている方々には、もはや説明不要でしょう(中級者であれば、10~15行目を、3項演算子を使って、1文で書きたくなる事でしょう)。

 

おわりに

 簡単な回答のためにデッチあげたプログラムなのですが、短いながらも、文字列とポインタについてのエッセンスが詰まったプログラムになっていると思います。 これを苦もなく正しく読めるなら、もはや C 初心者ではありません!


(転載以上)