<strsafe.h> で追加された文字列操作関数について

VC7.1 には (VC7.0にも?) 安全に文字列操作を行うための関数群が追加されました。

今まで文字列操作をするとなると strcpy や lstrcpy 等を使っていましたが、これらはいとも簡単に確保したバッファ以上の領域に文字列を書き込んでしまいます。
領域以上に書き込んでしまい、アプリが落ちるだーの、ほかの変数の値がおかしくなるだーの…ということは、Cプログラマであれば誰もが経験をしている事でしょう…^^;

また、このようなバッファオーバーランはセキュリティホールとなる可能性が強いです。その為、事故を減らす意味でも、strcpyなどの関数の使用はオススメできません。

がー、そうなると、文字列操作するための関数がなくなってしまいます。仕方なしに自前で安全な文字列操作関数を製作するのも良いでしょうが…… 自分で打ったソースコードってのは絶対に信用できない訳です。(ぉ

そこで(?)、Microsoft自身がオーバーランをしないよう考慮した関数を作ってくれました。その関数群が入っているのが strsafe.h です。

新しい関数群と使い方

strsafe.h で定義されてた新しい関数は以下の30個です。

一見大量にあり「ウボァー」という感じで嫌になりますが、実はそんなに嫌がるものでもなかったりします。

基本となる関数は8つで後は派生物

この8つの関数が基本としてあり、それぞれにEx版があり 且つ Cch/Cb系がある… というような感じです。 Length だけは例外で Ex版はありません。

Cch系とCb系の違い

各関数は 「出力先のバッファの量」 を引数に取ります。この時 Cch/Cb とで指定する値の意味が変わっています。次の表のようになっています。

Cch系 バッファの量を文字数で指定します。
Cb系 バッファの量をバイト数で指定します。

戻り値について

これら関数の戻り値は全て HRESULT です。 よって従来の lstrcpy や strcpy とは違って「関数が成功したか、失敗したか」 という処理結果を返す点に注意する必要があります。

特に Length については注意が必要で、戻り値をそのまま文字列の長さとして使用することは出来ません。 そのままキャストして文字の長さとして使用するのは死亡確定ですので注意しましょう。

なお HRESULT なので、戻り値を SUCCEEDED, FAILED マクロの引数として与え、関数が成功したか失敗したかを手軽に判断する事が可能です。

Ex系について

処理を行う時の詳細な動作を指定する事が出来ます。…が、少々ややこしいので別セクションを設けました。 Ex版の関数について を参照してください。

使用例

てきとーに書いてみました。



void check(const HRESULT hr)
{
    if(FAILED(hr))
        puts("--- バッファが足りないッスよ〜。");
}

int main()
{
    CUINT   nBuffSize = 20;
    tchar   szDest[nBuffSize];
    size_t  nNowLength;
    HRESULT hr;

    // コピー
    hr = ::StringCchCopy(szDest, nBuffSize, _T("Manna "));
    check(hr);
    puts(szDest);

    // 連結
    hr = ::StringCchCat(szDest, nBuffSize, _T("Rina Ena "));
    check(hr);
    puts(szDest);

    // 文字数取得
    hr = ::StringCchLength(szDest, nBuffSize, &nNowLength);
    check(hr);
    printf("Length = %d\n", nNowLength); 

    // バッファの量足りないよ。
    hr = ::StringCchCat(szDest, nBuffSize, _T("Honoka Koyuki Fuko Tubomi"));
    check(hr);
    puts(szDest);
    puts("");


    // sprintf …つか snprintf と同じじゃ…?
    hr = ::StringCchPrintf(szDest, nBuffSize, _T("Num = %d"), 169);
    check(hr);
    puts(szDest);

    // sprintf 長すぎ
    hr = ::StringCchPrintf(szDest, nBuffSize, _T("String = %s"), _T("Seria Tina May"));
    check(hr);
    puts(szDest);

    getchar();
    return 0;
}

出力結果


Manna
Manna Rina Ena
Length = 15
--- バッファが足りないッスよ〜。
Manna Rina Ena Hono

Num = 169
--- バッファが足りないッスよ〜。
String = Seria Tina

このように、使い方としてはバッファの量を指定する点を除いて、基本的に従来の関数と同じです。ただし、StringCchLength は長さの取得方法が変わっている為、ちょっと注意ですね。

Ex版の関数について

処理を行うときの詳細な動作を指示したり、より詳細な処理結果を取得できたりします。 その為引数が多く、若干メンドウかもしれません。(ぉ ^^;

引数について

非Ex版より引数が3つほど増えています。それぞれの意味は次のとおりです。

LPTSTR* ppszDestEnd 処理先の文字列の終端 を取得します。NULLの指定も可。
size_t* [Cch系]pcchRemaining
[Cb系]pcbRemaining
処理を行った後の残りのバッファ量を取得します。NULLの指定も可。
DWORD dwFlgs 処理を行うときの詳細な動作を指定します。
また、下位1バイトは任意に指定する事が可能で、 STRSAFE_FILL_BEHIND_NULL と STRSAFE_FILL_ON_FAILURE で使用されます。

フラグについての詳細は次の「フラグについて」を参照してください。

フラグについて

処理を行うときの詳細な動作を指定する事が出来ます。

なお、STRSAFE_FILL_BEHIND_NULL, STRSAFE_FILL_ON_FAILURE は、特別に下位1バイトを使用します。 おそらくサンプルコードを参照した方が解りやすいかもしれません。

STRSAFE_FILL_BEHIND_NULL
処理が完了した後バッファの余った領域を、下位1バイトで埋めます。
なお、下位1バイトしか有効でないため、wchar_t で使用する場合に注意が必要です。
STRSAFE_IGNORE_NULLS
処理元となる文字列引数 (pszSrc) に NULL が渡された場合、"" が渡されたとして処理します。
STRSAFE_FILL_ON_FAILURE
関数が失敗した場合、下位1バイトでバッファを埋めます。 バッファの量が足りなかった場合の失敗も、指定した値で埋め尽くされます。
STRSAFE_NULL_ON_FAILURE
関数が失敗した場合、出力先を "" にします。
STRSAFE_NO_TRUNCATION
Cat系関数で関数が失敗した場合、文字列結合を行いません。他の関数の時は STRSAFE_NULL_ON_FAILURE と同じです。(多分^^;)

使用例

やっぱりコレもてきとーに。


void output(const HRESULT hr, const size_t nRemain, const tchar* pszMsg)
{
    if(FAILED(hr))
        _putts(_T("--- バッファが足りないッスよ〜。"));

    _tprintf(_T("残りBuff = %d\t: %s\n"), nRemain, pszMsg);
}

void dump(const tchar* szOut, CUINT nLength)
{
    _putts(_T("--- Buff dump..."));
    for(UINT i = 0; i < nLength; ++i)
    {
        CINT c = szOut[i];

        if(c == _T('\0'))
            _puttchar(_T('\\')), _puttchar(_T('0'));
        else
            _puttchar(szOut[i]);
    }

    _puttchar(_T('\n'));
}

int main()
{
    CUINT   nBuffSize = 20;
    tchar   szDest[nBuffSize];
    tchar*  pszEnd;
    size_t  nRemain;
    HRESULT hr;

    // 普通にコピー
    hr = ::StringCchCopyEx(szDest, nBuffSize, _T("Manna "), &pszEnd, &nRemain, 0);
    output(hr, nRemain, szDest);

    // 連結…とはいえ 元が NULL("") なので何も変わらない
    hr = ::StringCchCatEx(szDest, nBuffSize, NULL, &pszEnd, &nRemain, STRSAFE_IGNORE_NULLS);
    output(hr, nRemain, szDest);
//  ↓コレでは、コピー元がNULLで NULLを無視するフラグを立てていないので落ちてしまう。
//  hr = ::StringCchCatEx(szDest, nBuffSize, NULL, &pszEnd, &nRemain, 0);

    // 連結
    // バッファの余った領域は # で埋める。
    hr = ::StringCchCatEx(szDest, nBuffSize, _T("Rina Ena "), &pszEnd, &nRemain, '#' | STRSAFE_FILL_BEHIND_NULL);
    output(hr, nRemain, szDest);
    dump(szDest, nBuffSize);
    _puttchar(_T('\n'));


    // STRSAFE_NO_TRUNCATION 指定での失敗なので、何もしません。
    hr = ::StringCchCatEx(szDest, nBuffSize, _T("Honoka Koyuki Fuko Tubomi"), NULL, &nRemain, STRSAFE_NO_TRUNCATION);
    output(hr, nRemain, szDest);

    // フラグ指定なしでの失敗
    hr = ::StringCchCatEx(szDest, nBuffSize, _T("Honoka Koyuki Fuko Tubomi"), NULL, &nRemain, 0);
    output(hr, nRemain, szDest);

    // STRSAFE_NULL_ON_FAILURE 指定なので、バッファがNULL文字になる。
    hr = ::StringCchCatEx(szDest, nBuffSize, _T("Honoka Koyuki Fuko Tubomi"), NULL, &nRemain, STRSAFE_NULL_ON_FAILURE);
    output(hr, nRemain, szDest);
    _puttchar(_T('\n'));



    // コピーに失敗で、$ で埋める。
    hr = ::StringCchCopyEx(szDest, nBuffSize, _T("Honoka Koyuki Fuko Tubomi"), NULL, &nRemain, '$' | STRSAFE_FILL_ON_FAILURE);
    output(hr, nRemain, szDest);

    // sprintf に失敗で NULL文字列化。STRSAFE_NULL_ON_FAILURE 指定でも同様
    hr = ::StringCchPrintfEx(szDest, nBuffSize, NULL, &nRemain,
        STRSAFE_NO_TRUNCATION, _T("Num = %d, String = %s"), 169, _T("Seria Tina May"));
    output(hr, nRemain, szDest);

    getchar();
    return 0;
}

出力結果


残りBuff = 14   : Manna
残りBuff = 14   : Manna
残りBuff = 5    : Manna Rina Ena
--- Buff dump...
Manna Rina Ena \0####

--- バッファが足りないッスよ〜。
残りBuff = 5    : Manna Rina Ena
--- バッファが足りないッスよ〜。
残りBuff = 1    : Manna Rina Ena Hono
--- バッファが足りないッスよ〜。
残りBuff = 20   :

--- バッファが足りないッスよ〜。
残りBuff = 1    : $$$$$$$$$$$$$$$$$$$
--- バッファが足りないッスよ〜。
残りBuff = 20   :

こんな感じで、失敗時の処理などがちょっと高度に行う事が出来ます。 …まぁでも、あんまり使わないかも。(ぉ

従来の関数が使えなくなる!?

#include <strsafe.h> することにより、新しい関数を使用できるようになりますが、逆に従来の strcpy や lstrcpy が使用出来なくなるような感じになります。
正確には、VC6で利用不可に、VC7.1では「つかっちゃイヤン警告」が出ます。

折角新しくて安全な関数が導入されたんですから、そっちを使用するような方向に持っていくのは当然といえば当然です。…が、時には昔の関数も使用しなきゃいけない時がくるかもしれません。

そんな時は STRSAFE_NO_DEPRECATE を、#include <strsafe.h> の前に定義してあげると、従来の関数も同時に使用できるようになります。


#define STRSAFE_NO_DEPRECATE // 前で定義すると混在可能になる
#include <strsafe.h>

ぼやき

いろいろ書いてきましたが…多分こんな感じでしょう。間違ってたらご連絡ください。^^;

…まぁでも、結局のところ、素直に std::string やら CString 使おうぜ。(今までの文章一気に台無し......)

▲このページのTOP▲