わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

改めて,文字列リテラルは配列であることを確認する

さて,文字列リテラルの正体が文字型の配列であるとすると,"hello!\n"という記述は何を示しているのでしょうか.実は,"hello!\n"は,実体となる配列の先頭位置を指し示すポインタなのです.
図2.20の"hello!\n"がポインタであることは,図2.22のプログラムで確かめられます.図2.20では,"hello!\n"を右辺として,左辺の文字列へのポインタ*strに,文字列リテラルの格納番地を代入しています.
(図2.22: 省略)
このように,C言語では入門第一歩目のプログラムから,文字列の定数というポインタを利用しているのです.
(『Cによるソフトウェア開発の基礎―データ構造とアルゴリズムの基礎から』, p.50)

久しぶりに見たひどい記述です.同書の『C言語では配列はポインタで実装されています.極端な言い方をすると,C言語の配列は,ポインタを読みやすくするための表記法に過ぎません』(p.52),『C言語における多次元配列は,一次元配列同様,ポインタによって作り出されるデータ構造であり,本当の意味での多次元配列とは言えません』(p.53)にもびっくりです*1
文字列リテラルが配列かポインタかというと,配列でしょう.以下のプログラムで検証できます.

#include <stdio.h>

int main(void)
{
  char *wakayama = "wakayama";
  char *city = "city";
  
  if (sizeof("wakayama") == sizeof("city")) {
    printf("maybe pointer\n");
  } else {
    printf("maybe array\n");
  }

  if (sizeof(wakayama) == sizeof(city)) {
    printf("maybe pointer\n");
  } else {
    printf("maybe array\n");
  }

  return 0;
}

あとは規格.JIS X 3010 : 2003*2のp.45です.

補足説明 単純文字列リテラル(character string literal)は,二重引用符で囲まれた0個以上の多バイト文字の並びとする(例えば"xyz".).ワイド文字列リテラル(wide string literal)は,文字Lという接頭語をもつことを除いて,単純文字列リテラルと同一とする.

翻訳フェーズ(7)において,文字列リテラル又はその並びから得られる多バイト文字の並びの最後に値0のバイトまたはコードを付加する(脚注65).結果の多バイト文字の並びは,静的記憶域期間をもち,かつその並びを含むのにちょうど十分な長さの配列を初期化するのに用いる.単純文字列リテラルの場合,配列の要素は型charをもち,多バイト文字の並びの個々のバイトで初期化する.ワイド文字列リテラルの場合(略).実行文字集合で表現できない多バイト文字又は逆斜線表記を含む文字列リテラルの値は,処理系定義とする.
それらの要素が適切な値をもっている場合,これらの配列同士が別個であるかどうかは未規定とする.プログラムがこれらの配列を変更しようとした場合,その動作は未定義とする.

上記引用の「配列」は,いわゆる配列変数のことでも,配列型の変数に結びつけられる値のことでもありません.int i=1;としたときの変数iに格納される値「1」と,1+2+3という式の中の「1」が違うのと同様に,配列も,変数に格納するものと,変数に格納しなくても使用できるものがあり,文字列リテラルは,「変数に格納しなくても使用できる」「文字配列」に位置づけられます.
なお,『これらの配列同士が別個であるかどうかは未規定』というのは,以下のコードが「same」か「different」のいずれを出力するかは保証されないということです.(手元のCygwin + gccでは「same」になりましたが.)

#include <stdio.h>

void compare(char *s)
{
  char *t = "wakayama";

  if (s == t) {
    printf("same\n");
  } else {
    printf("different\n");
  }
}

int main(void)
{
  char *s = "wakayama";

  compare(s);

  return 0;
}

また,『プログラムがこれらの配列を変更しようとした場合,その動作は未定義とする.』は,授業で「ポインタが指し示す文字列リテラルは書き換えられない」として教えていることです.

*1:手短に反論すると,配列は,“式の中で,かつ「&配列」「sizeof(配列)」のいずれでもないとき”という条件を満たせば,その先頭アドレスを指すポインタに変換されます.int x[3][3];と宣言された2次元配列に対して,int **y=x;はうまくいかず(警告が出ますし,x[1][1]とy[1][1]は一致しません),int (*z)[3]=x;とする必要があります.この変数zはポインタですが,*zあるいはz[0]は配列であり,実際,sizeof(z)とsizeof(*z)の値が異なり,その一方で,sizeof(*z)とsizeof(int)*3が等しくなります.

*2:アクセス方法と,読む際の注意:http://d.hatena.ne.jp/takehikom/20090104/1231013450