[ クラス | フォーム | 配列、動的配列 | 文字列 | Delphi の格言 | IDE の小技 | エッセンス ]

up

文字列

長い文字列とは何か

長い文字列型変数は、4 バイトのメモリを占有し、そこには動的に割り当てられた文字列へのポインタが格納されています。長い文字列型変数がカラである(長さゼロの文字列が格納されている)場合、文字列ポインタは nil になり、その文字列変数への動的メモリは割り当てられません。文字列値がカラでない場合、文字列ポインタは、動的メモリブロックを指し、そのメモリブロックには、文字列値の他に 32 ビットの文字列の長さと 32 ビットの参照カウンタが入っています。

文字列定数とリテラルの場合、コンパイラは動的に割り当てられた文字列と同じレイアウトで参照カウンタが -1 のメモリブロックを生成します。長い文字列型変数への文字列定数を代入した場合、その文字列定数用に生成されたメモリブロックのアドレスが文字列ポインタへ代入されます。(ヘルプより)

top

長い文字列への代入と参照カウントの関係とは何か

例1 長い文字列同士の代入

   +----+----+---------+
   |   1|   4| a b c d | // 文字列は 参照カウント+文字数+文字配列
   +----+----+---------+
              ↑
              a

   +----+----+---------+
   |   1|   4| d e f g |
   +----+----+---------+
              ↑
              b

  a := b; とすると

   +----+----+---------+
   |   0|   4| a b c d |   -> 参照カウントが 0 になり、破棄
   +----+----+---------+
              
              a
              ↓
   +----+----+---------+
   |   2|   4| d e f g |
   +----+----+---------+
              ↑
              b

というように代入では文字列のコピーは起きず、文字列の破棄、
参照カウントの更新とアドレスの付け替えが起こります。
ここで、a の文字列を a[2] := 'z'; とかで変更すると


   +----+----+---------+
   |   1|   4| d z f g |   // エリア確保とコピーと'z' の代入
   +----+----+---------+
              ↑
              a
              
   +----+----+---------+
   |   1|   4| d e f g |  // 参照カウント減
   +----+----+---------+
              ↑
              b

というように、ここで初めて文字列のコピーが起こります。
これを Copy on Write といいます。文字列 a, b に修正が
起こるまではメモリの節約と文字列コピーを不要にする
処理方式です。

注) ひょっとすると a, b が指すメモリブロックが
入れ替わるかもしれません。コピー直後はどちらの変数に
どちらのメモリブロックを渡しても同じだからです。

例2

文字列定数を代入する場合は少し話が違ってきます。

   +----+----+---------+
   |   1|   4| a b c d | 
   +----+----+---------+
              ↑
              a

   +----+----+---------+
   |  -1|   4| d e f g | // 文字列定数は参照カウントが -1
   +----+----+---------+ // 静的なメモリ上にある。書き換え不能

  a := 'defg'; とすると

   +----+----+---------+
   |   0|   4| a b c d | -> 参照カウントが 0 になり破棄
   +----+----+---------+
              
              a
              ↓
   +----+----+---------+
   |  -1|   4| d e f g | // 文字列定数は参照カウントが -1
   +----+----+---------+ // 静的なメモリ上にある。書き換え不能

というようになります。この場合、a の持っていた文字列を破棄し
アドレスを付け替えるわけです。これがアドレスが変わる理由です。
文字列定数の参照カウントは

ここで a[2] := 'z'; すると

   +----+----+---------+
   |   1|   4| d z f g |   // エリア確保とコピー&'z' の代入
   +----+----+---------+
              ↑
              a
              
   +----+----+---------+
   |  -1|   4| d e f g |  // 静的なメモリ上にある。書き換え不能
   +----+----+---------+

となります。これも Copy On Write です。

尚、動的配列は長い文字列と同様参照カウントで配列領域を管理していますが、Copy On Write はしません。従って動的配列の代入は配列の「共有」を意味します。つまり代入後、片方の動的配列の要素の変更は、もう片方の動的配列の要素を変更するように見えます。実際には2つの動的配列変数が同じメモリブロックを指しているだけです。[delphi-ml:73107]

procedure TForm1.Button4Click(Sender: TObject);
var
  dynArr1, dynArr2: array of string;
begin
  SetLength(dynArr1, 2);

  dynArr1[0] := 'abc';
  dynArr1[1] := 'def';

  dynArr2 := dynArr1;

  dynArr2[1] := 'change';

  ShowMessage(dynArr1[0]); // 'abc'
  ShowMessage(dynArr1[1]); // 'change'
end;

top

Pascal 文字列をヌルで終わる文字列にキャストできるか

長い文字列を PChar にキャストすることで、ヌルで終わる文字列へのポインタを取得する事が出来ます。

また、長い文字列型の変数をポインタにキャストする場合は、変数に新しい値が代入されるまで、又は変数がスコープからでるまで、ポインタは有効です。そのほかの長い文字列型の式をポインタに型キャストする場合、ポインタは、型キャストが実行される文の中でのみ有効となります。

詳細は、ヘルプの「Pascal 文字列とヌルで終わる文字列の混在」を参照してください。

procedure TForm1.Button1Click(Sender: TObject);
var
  s1, s2: string;
begin
  s1 := 'content';
  s2 := 'title';
  MessageBox(Handle, PChar(s1), PChar(s2), MB_OK);
end;

top

文字列定数と PChar との関係は何か

1) 文字列定数を PChar したものは AnsiString を指していない。

PChar 型の文字列定数は

PChar('abc');

と書くことができますが、これで得られるポインタは
AnsiString 型の定数の中の文字列を指すのではなく、
単に $61 $62 $00 という値を持つメモリ領域の先頭を指します。

2) PChar 型型付定数は AnsiString を指していない

PChar型の型付定数は

const p: PChar = 'abc';

と書くことができますが、これで得られるポインタは
AnsiString 型の定数の中の文字列を指すのではなく、
単に $61 $62 $00 という値を持つメモリ領域の先頭を指します。

尚、
  const p: PChar = 'a';

は $61 $00 という値を持つメモリ領域の先頭を指します(重要)。

3) 文字定数 の PChar 型へのキャストはうまく行かない(バグ?)

PChar('a') は値が $00000061 のポインタになります。
文字列へのポインタにはなりません。PChar('a' + '') や
PChar('') は大丈夫です。

つまり 文字列へのポインタが得られるかという意味で
PChar('abc')    --> OK
PChar('a' + '') --> OK
PChar('')       --> OK (NULL文字を指すポインタが返る)
PChar('a')      --> NG

これはバグとは言い切れませんが、一貫性にかけるし、2)
とも整合がとれません。気色悪いです。
文字定数と1文字の文字列定数の区別が曖昧であることが
この問題を引き起こしていると思います。

この問題は、文字定数を文字列型変数に代入してから
PCharキャストすれば回避できます。

試験環境 Delphi 5 + 最適化 OFF [delphi-ml:64954]

procedure TForm1.Button3Click(Sender: TObject);
const
  p: PChar = 'a';
var
  pCh1: PChar;
  pCh2: PChar;
  pC3: PChar;
  pC3: PChar;
  s: string;
begin
  pCh1 := PChar('a');
  pCh2 := PChar('abc');

  s := 'abc';
  pC3 := PChar(s);

  pC3 := PChar(string('a'));

  MessageBox(Handle, 'abc', p, MB_OK);          // OK
  MessageBox(Handle, 'abc', 'a', MB_OK);        // OK
  MessageBox(Handle, 'abc', PChar('a'), MB_OK); // NG
  MessageBox(Handle, 'abc', pCh1, MB_OK);       // NG
  MessageBox(Handle, 'abc', pCh2, MB_OK);       // OK
  MessageBox(Handle, 'abc', pC3, MB_OK);        // OK
  MessageBox(Handle, 'abc', PC3, MB_OK);        // OK
end;

top

string を PChar にキャストして得られるポインタの有効範囲はどこまでか

String 型を PChar にキャストした際のポインタの有効範囲は、私はその場かぎりだと思って利用するのが一番だと思います。つまり、値として「どこにも保存しない」ということを順守すべきと。

String 型を PChar にキャストするのは、PChar 型を要求する関数パラメータに対して String 型を渡す場合のみ、ということになります。

String 型を PChar にキャストした時に得られるポインタは、その時点の String 型が管理している実体のアドレスです。String 型は実体の変更・管理を隠蔽している型なので、この実体の有効範囲は隠蔽されている、つまり不明ということになります。[delphi-ml:64757]

次のプログラムの実行結果を予測してみてください。
# LStrArrayClr 近辺のコードを読んだことがないとハズすかも?

program stest;
{$APPTYPE CONSOLE}

uses SysUtils;

var
  s: String;

procedure Test1;
var
  p: PChar;
begin
  s := IntToStr(1);

  p := PChar(s);
  s[1] := 'a';
  WriteLn(s);
  WriteLn(p^);
end;

procedure test2;
var
  p: PChar;
begin
  p := PChar(s);
  s[1] := 'b';
  WriteLn(s);
  WriteLn(p^);
end;

begin
  test1;
  test2;
  ReadLn;
end.

top

Result は、暗黙のうちに渡される参照渡しの変数か

Result が参照渡しのパラメータになるケースはAnsiString, WideString, インターフェース型、動的配列型、レコード型でした、(variant は未試験) 。また、順序型、浮動小数点型は参照渡しにはならないようです。

また、Result が参照渡しのパラメータになるケースでグローバル変数が結果を受け取る場合は、暗黙の変数がスタック上に作られて初期化され、それが結果を受け取ってグローバル変数に内容がコピーされた後破棄されるようです。このためインタフェース型では参照カウントが大きめになりなかなか減りません(^^;

で、少なくとも

「Result は ローカル変数のように Initialize/Finalize されると仮定してはいけない」

ということです。[delphi-ml:64786]

top

PChar(string) の有効性

var
  PC:PChar;

function Test:string;
begin
  result := 'TEST STRING';
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  PC := PChar(Test);
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  Label1.Caption := StrPas(PC);
end;

で Button1Click() を実行した後で、Button2Click() で実行されているコードは「正しい」のでしょうか。プログラマの意図としては Label1 に 'TEST STRING' と表示されることを期待しています。Button1Click() 実行後、PC の値は変更されることはなく、Button2Click() が実行されるのはそれから1ヵ月後であるかもしれません。

これは正しくないコードです。関数や式の返す「右辺値」は代入しないとすぐに寿命がつきてしまいます。従ってPChar で取り出した値はすぐに不正なポインタになってしまいます。

で、ここからは多少未確認情報なんですが、最適化のバグかもしれませんが、演算結果として得た文字列をPChar でキャストして、それをそのまま関数に渡した場合、つまり

Bar(Pchar(文字列の式));

でも不正なポインタがわたるケースに遭遇したことがあるので、PChar(右辺値の文字列)という書き方さえすでに危険というのが私の認識です。

 #でも再現できません。単なるバグだったのかも。
 #そのときはローカル変数に代入してから使うことで
 #しのぎました。

[delphi-ml:64796]

top

[ クラス | フォーム | 配列、動的配列 | 文字列 | Delphi の格言 | IDE の小技 | エッセンス ]


up

更新日:2005-02-12