あらきけいすけのメモ帳

あらきけいすけの雑記帳2

C言語の文字列 char a[] char *pのふるまい

授業用のコードを書いて困惑したので、C99を読み返す。
初期化を伴う char 型の配列 char a[ ] の宣言によって a は「配列」として使える

The declaration
char s [ ] = "abc", t[3] = "abc";
defines ‘‘plain’’ char array objects s and t whose elements are initialized with character string literals.
This declaration is identical to
char s [ ] = { 'a', 'b', 'c', '\0' }, t [ ] = { 'a', 'b', 'c' };
The contents of the arrays are modifiable.
Programming languages — C, ISO/IEC 9899:TC2, p.130, §6.7.8, 下線はあらき
ところが初期化をともなう char * 型の宣言では p は「配列」としては使えない;
On the other hand, the declaration
char *p = "abc";
defines p with type ‘‘pointer to char’’ and initializes it to point to an object with type ‘‘array of char’’ with length 4 whose elements are initialized with a character string literal. If an attempt is made to use p to modify the contents of the array, the behavior is undefined.
Programming languages — C, ISO/IEC 9899:TC2, p.130, §6.7.8, 下線はあらき
以上を踏まえると、柴田, 『新・明解C言語 入門編』, SB Creative (2014) の「11-1 文字列とポインタ」 p.286ff はかなりミスリーディングに書かれているように思われる;まず初期化をともなう char 型の宣言として配列添字演算子 char s[ ] を用いると、「代入」のつもりで書かれたプログラム

//柴田(2014), p.288, List 11-2, 余計なものは消した
int main (void) {
  char s[] = "ABC";
  s = "DEF";
}

コンパイラがエラーを吐く(手元の MinGW gcc 8.1.0 のエラーメッセージは "assignment to expression with array type")なぜなら(配列 s の各要素 s[0], ... ではなく)配列 s は変更できないから*1。けれども、1文字ずつ「代入」を実行するプログラム

//my code 1
int main (void) {
  char s[] = "ABC";
  s[0] = 'D';
  s[1] = 'E';
  s[2] = 'F';
}

は「sが{'A','B','C','\0'}で初期化された配列」なので「配列の各要素への文字を代入」は期待通りに動作する。
 その一方で、初期化をともなう char 型の宣言として間接演算子 char *p を用いると、「代入(メモリ上のデータ"123"をデータ"456"で上書き)」のように見えるプログラム

//柴田(2014), p.288, List 11-3, 余計なものは消した
#include <stdio.h>
int main (void) {
  char *p = "123";
  p = "456";
  printf("%s",p);
}

はポインタ変数 p に格納されていた「文字リテラル "123" の先頭アドレス」を「(メモリ上の別の場所にいる)文字リテラル "456" の先頭アドレス」で上書きしてしまう操作なので、実行すると「456」(どこかにある '4' '5' '6' '\0')が表示される*2のだが、1文字ずつの「代入」のつもりで書いたプログラム

//my code 2
int main (void) {
  char *p = "123";
  p[0] = '4';
  p[1] = '5';
  p[2] = '6';
}

はC99の仕様書によれば「動作が定義されていない」(手元の MinGW gcc 8.1.0 では生成した実行ファイル a.exe を動かすと"a.exe は動作を停止しました"という表示を出して落ちる)。
次のコード

//my code 3
#include <stdio.h>
int main (void) {
  char *p = "123";
  printf("%c\n",p[1]);
  printf("%s\n",&p[1]);
}

では「 p は文字リテラル '1' '2' '3' '\0' の先頭アドレスの値」であるから、「p[1]は文字リテラルの2個目の要素の値を返す」ので、printf("%c\n",p[1]); は「2」を表示する。さらに「&p[1]は文字リテラルの2個目の要素のアドレスを返す」ので、printf("%s\n",&p[1]); は「23」('2' '3' '\0' すなわち2文字目から後ろの文字リテラル)を表示する。一方で

//my code 4
#include <stdio.h>
int main (void) {
  char s[] = "123";
  printf("%c\n",s[1]);
  printf("%s\n",&s[1]);
}

では「 s は char 型の配列の先頭アドレスの値」であるから、「s[1]は配列の2個目の要素の値を返す」ので、printf("%c\n",s[1]); は「2」を表示する。さらに「&p[1]は配列の2個目の要素のアドレスを返す」ので、printf("%s\n",&s[1]); は「23」(s[1], s[2], s[3], すなわち2個目から後ろの連続した配列の要素を文字リテラルとして)表示する。つまりこの2個のプログラムの動作は見た目が同じになる。
 注意すべきは char *p が配列として使えないのは「文字リテラル」で初期化した場合である。次のコード(文字列"abcde"を逆順"edcba"にする)の b は「配列のように」動作する:

//my code 5
#include <stdio.h>
#include <stdlib.h>
int getLength(char *a){int i=-1; while(a[++i]); return i;}
char *reverseString (char *a) {
  int iMax = getLength(a);
  char *b = (char*) malloc( sizeof(char) * ( iMax + 1 ) );
  if (b) {
    for( int i = 0; i < iMax; i++ ) b[i] = a[ iMax - i - 1 ];
    b[iMax] = '\0';
    return b;
  }
  return 0;
}
int main (void) {
  char *a = "abcde", *b = reverseString(a);
  if (b) {
    b[3] = 'X';
    printf("%s\n%s\n",a,b);
    free(b);
  }
}

このコードでは main() の a, b はどちらも char* 型で宣言されているが、b[3] = 'X' を a[3] = 'X' に書き換えると、とたんにコードが落ちる。

*1:A modifiable lvalue is an lvalue that does not have array type, does not have an incomplete type, does not have a const-qualified type, and if it is a structure or union, does not have any member (including, recursively, any member or element of all contained aggregates or unions) with a const-qualified type. C99 §6.3.2.1 下線はあらき

*2:アドレス情報を失った文字リテラル"123"はプログラムからアクセスできぬままメモリ上に取り残されてしまう。