【MFC】CStringのメモリ管理: GetBuffer / ReleaseBufferの作法

【MFC】CString のメモリ管理: GetBuffer / ReleaseBuffer の作法

Win32 APIにLPTSTR(書き込み可能な文字列ポインタ)を渡す場面で、CStringの内部バッファをそのまま使いたくなることがあります。
この記事では、GetBuffer / ReleaseBufferの正しい使い方と、やってはいけないパターンを解説します。


目次

なぜGetBufferが必要なのか

CStringは内部でバッファの参照カウントやサイズを管理しています。operator LPCTSTR() で取得できるポインタはconstであり、直接書き換えることは想定されていません。

Win32 APIの中には、引数に書き込み用のバッファポインタを要求するものが多数あります。たとえばGetWindowText、GetModuleFileName、GetEnvironmentVariableなどです。こうしたAPIにCStringを渡すには、GetBufferで書き込み可能なポインタを取得する必要があります。


基本パターン:GetBufferで確保し、API呼び出し後にReleaseBuffer

手順は3ステップです。

CString strPath;

// 1. 必要な文字数分のバッファを確保して、書き込み可能ポインタを取得
LPTSTR pBuf = strPath.GetBuffer(MAX_PATH);

// 2. API にバッファを渡す
DWORD len = GetModuleFileName(NULL, pBuf, MAX_PATH);

// 3. CString に制御を返す(内部の長さ情報を再計算させる)
strPath.ReleaseBuffer(len);

// 以降は通常の CString として使える
AfxMessageBox(strPath);

GetBuffer(nMinBufLength) は、最低でもnMinBufLength文字を格納できるバッファを返します。ここで指定する値に終端 '\0' のぶんは含みません。ReleaseBufferは、書き換え後の長さをCString側へ反映するための呼び出しです。


ReleaseBufferを呼ばないとどうなるか

GetBufferで取得したポインタ経由で内容を変更したあとは、ReleaseBufferを呼んでから他のCStringメソッドを使う必要があります。Microsoft Learnでも、他のCSimpleStringTメソッドを使う前にReleaseBufferで内部状態を更新することが前提になっています。

CString str;
LPTSTR p = str.GetBuffer(256);
lstrcpy(p, _T("test"));

// NG: ReleaseBuffer を呼ばずに他の操作をする
str += _T(" suffix");

これは単なる作法ではなく、ドキュメント上の契約です。ReleaseBufferを飛ばすと長さ情報が更新されず、その後の動作は実装依存になります。例外経路も含めて、必ずGetBufferと対で扱ってください。


ReleaseBufferの引数

ReleaseBufferには引数を渡すことができます。

呼び出し方動作
ReleaseBuffer()(引数なし)バッファ内の '\0' を基準に長さを決める
ReleaseBuffer(n)長さをn文字として反映する

APIが戻り値で書き込んだ文字数を返す場合は、ReleaseBuffer(len) のように明示的に渡す方が効率的です。'\0' の検索が不要になります。

デバッガでも、ReleaseBuffer() 後にstrと内部バッファの内容が同期していることを確認できます。

Visual StudioのウォッチウィンドウでCStringの内部バッファを確認している画面

GetBufferSetLengthとの違い

GetBufferSetLength(n) は、バッファをちょうどn文字ぶんにそろえてからポインタを返します。終端文字の自動探索に頼らず、最終的な文字数を自分で管理したいときに使います。こちらも、返されたポインタ経由で内容を書き換えたなら、他のCStringメソッドを使う前にReleaseBufferを呼ぶ必要があります。


よくある間違い

間違い1:GetBufferのサイズ指定が小さすぎる

CString str;
LPTSTR p = str.GetBuffer(10);
// API が 10 文字以上書き込む可能性がある → バッファオーバーラン
GetWindowText(hWnd, p, 256);  // ← 危険
str.ReleaseBuffer();

GetBufferに渡すサイズは、APIが書き込む最大文字数以上でなければなりません。

間違い2:GetBufferで取得したポインタを保持し続ける

CString str(_T("Hello"));
LPTSTR p = str.GetBuffer(256);
str.ReleaseBuffer();

// NG: ReleaseBuffer 後も p を使い続ける
// CString が再割り当てすると p は無効なアドレスになる
lstrcpy(p, _T("Danger"));  // ← 未定義動作

ReleaseBufferの後にCStringがバッファを再割り当てすると、以前のポインタは無効になります。GetBufferで取得したポインタはReleaseBufferまでの間だけ使ってください。

間違い3:LPCTSTRで取得したポインタをconst_castする

CString str(_T("Hello"));
// NG: const を外して書き込む
LPTSTR p = const_cast<LPTSTR>((LPCTSTR)str);
lstrcpy(p, _T("World"));

LPCTSTRで得たポインタは読み取り専用として扱う前提です。const_castで無理に書き込むとCStringの内部管理を壊します。書き込みが必要なら、必ずGetBufferを経由してください。


実務での使用例

レジストリから文字列を読む

CString ReadRegString(HKEY hKey, LPCTSTR valueName)
{
    DWORD cbData = 0;
    LSTATUS rc = RegGetValue(
        hKey, NULL, valueName,
        RRF_RT_REG_SZ,
        NULL, NULL, &cbData);
    if (rc != ERROR_SUCCESS || cbData == 0)
        return CString();

    CString result;
    LPTSTR p = result.GetBuffer(cbData / sizeof(TCHAR));
    rc = RegGetValue(
        hKey, NULL, valueName,
        RRF_RT_REG_SZ,
        NULL, p, &cbData);
    if (rc != ERROR_SUCCESS)
    {
        result.ReleaseBuffer(0);
        return CString();
    }

    result.ReleaseBuffer();
    return result;
}

RegQueryValueExはREG_SZでもnull終端を保証しません。文字列として読む用途なら、終端文字を検査してくれるRegGetValueの方が安全です。

環境変数を取得する

CString GetEnv(LPCTSTR varName)
{
    CString result;
    DWORD need = GetEnvironmentVariable(varName, NULL, 0);
    if (need > 0)
    {
        LPTSTR p = result.GetBuffer(need);
        GetEnvironmentVariable(varName, p, need);
        result.ReleaseBuffer();
    }
    return result;
}

実行すると、PATHの先頭部分と全体の文字数を確認できます。GetEnvironmentVariableは必要サイズを「終端 '\0' を含む文字数」で返すため、この2段階取得と相性が良いAPIです。

GetBufferとReleaseBufferの実行結果がコンソールに表示されている画面

まとめ

  • GetBufferはWin32 APIにCStringの書き込み用バッファを渡すための正規の手段です
  • ReleaseBufferはGetBuffer / GetBufferSetLengthと必ずセットで呼びます。呼び忘れると長さ情報が更新されません
  • GetBufferに渡すサイズは、APIが書き込む最大文字数以上を指定します
  • LPCTSTRをconst_castして書き込むのは禁止です。書き込みが必要なら必ずGetBufferを経由してください
目次