![](https://habrastorage.org/getpro/habr/post_images/8cb/ec5/374/8cbec5374a94510dfe872599c18afb75.png)
もう一度、プログラマーはプログラムを完全に不注意に書くと確信しました。 そして、彼らは彼らのメリットのためではなく、状況の成功した組み合わせとMicrosoftまたはIntelのコンパイラ開発者のケアのために働いています。 はい、はい、彼らは私たちの曲がったサイドプログラムの代わりに松葉杖を気にし、適切なタイミングで置き換えます。
祈って、コンパイラとその開発者のために祈ってください。 多くの欠点や間違いさえありますが、彼らは私たちのプログラムに多大な労力を費やしています。 さらに、彼らの仕事は難しく、目に見えません。 彼らは私たち全員にとってコーディングと守護天使の高貴な騎士です。
Microsoftには、新しいバージョンのオペレーティングシステムと古いアプリケーションとの最大限の互換性を確保するための部署があることを知っていました。 Windowsの新しいバージョンで動作しなければならない最も有名な古いプログラムが10,000以上あります。 こうした努力のおかげで、最近、64ビットWindows Vistaを実行しているHeroes of Might and Magic II(1996ゲーム)を問題なくプレイできました。 ゲームはWindows 7で正常に起動すると思います。互換性についてのAlexey Pakhunovの興味深いメモ[
1、2、3 ]をお読みください。
しかし、どうやら私たちのひどいC / C ++コードの仕事、仕事、仕事を支援している部門もあります。 この話を最初から始めます。
私は、アプリケーションのソースコードを分析するための
PVS-Studioツールの開発に携わっています。 静かな仲間、静かな-これは広告ではありません。 今回、これは間違いなく慈善的な原因です。無料の汎用静的アナライザーを作成し始めたからです。 これまでのところ、アルファ版も遠いですが、作業はゆっくり進行しており、いつかこのアナライザーについてHabrahabrに投稿します。 最も興味深い典型的な間違いを収集し、それらを診断する方法を学び始めたので、私はこれについて話し始めました。
多くのエラーは、プログラムでの楕円の使用に関連しています。 理論的背景:
説明には、すべての有効なパラメーターの数とタイプを示すことができない関数があります。 次に、仮パラメータのリストは省略記号(...)で終わります。これは、「そして、おそらく、さらにいくつかの引数」を意味します。 例:int printf(const char * ...);
そのような不快だが簡単に診断されるエラーの1つは、文字列へのポインターではなく、クラス型のオブジェクトの可変数の引数を持つ関数への転送です。 このエラーの例を次に示します。
wchar_t buf [100];
std :: wstring ws(L "12345");
swprintf(buf、L "%s"、ws);
このようなコードは、バッファ内のゴミの形成またはプログラムのクラッシュにつながります。 もちろん、実際のプログラムでは、コードはより複雑になるため、Visual C ++とは異なり、GCCコンパイラが引数をチェックして警告するというコメントを書かないでください。 文字列はリソースまたは他の関数から取得でき、何も検証できません。 ここでは、診断は簡単です-クラスオブジェクトが行形成関数に渡され、エラーが発生します。
コードの正しいバージョンは次のようになります。
wchar_t buf [100];
std :: wstring ws(L "12345");
swprintf(buf、L "%s"、ws.c_str());
可変数の引数を持つ関数では、好きなものを渡すことができ、C ++プログラミングに関するほとんどすべての書籍での使用は推奨されないためです。 代わりに、boost ::形式などの安全なメカニズムを使用することをお勧めします。 ただし、推奨事項は推奨事項であり、printf、sprintf、CString :: Formatの異なるコードが多数あり、非常に長い間使用されます。 そのため、このような危険な構造を識別する診断ルールを実装しました。
上記のコードが間違っていることを理論的に理解しましょう。 彼は二度間違っています。
- 引数が指定された形式と一致しません。 「%s」を指定しているため、文字列へのポインターを渡す必要があります。 ただし、理論的にはsprintf関数を記述できます。この関数は、std :: wstringクラスのオブジェクトが渡されたことを認識し、正しく印刷します。 ただし、これは理由番号2により不可能です。
- 省略記号「...」の引数は、PODタイプのみです。 また、std :: string PODはタイプではありません。
PODタイプに関する理論的背景:
PODは「Plain Old Data」の略で、「Simple C-style data」として翻訳できます。 PODタイプは次のとおりです。
- すべての組み込み算術型(wchar_tおよびboolを含む)。
- enumキーワードを使用して宣言された型。
- ポインター;
- 以下の要件を満たすPOD構造(構造またはクラス)およびPODユニオン(ユニオン):
- カスタムコンストラクタ、デストラクタ、またはコピー割り当て演算子は含まれません。
- 基本クラスはありません。
- 仮想機能が含まれていません。
- 保護された(保護された)またはプライベート(プライベート)の非静的データメンバーを含まない;
- 非POD型(またはそのような型の配列)の非静的データメンバー、および参照は含まれません。
したがって、std :: wstringクラスは、コンストラクター、基本クラスなどがあるため、PODタイプには適用されません。
さらに、PODタイプではない省略記号オブジェクトを渡すと、未定義の動作が発生します。 したがって、少なくとも理論的には、std :: wstring型のオブジェクトを引数の省略記号として正しく渡すことはできません。
CStringクラスのFormat関数で同じ画像を観察する必要があります。 無効なコードオプション:
Cstring s;
CString arg(L "OK");
s.Format(L "Test CString:%s \ n"、arg);
コードの正しいバージョン:
s.Format(L "Test CString:%s \ n"、arg.GetString());
または、MSDN [
4 ]で提案されているように、文字列へのポインターを取得するには、CStringクラスに実装された明示的なキャスト演算子LPCTSTRを使用できます。 MSDNの正しいコードの例:
CString kindOfFruit = "bananas";
int howmany = 25;
printf( "You have%d%s \ n"、howmany、(LPCTSTR)kindOfFruit);
したがって、すべてが透明で明確なようです。 ルールを明確にする方法。 可変数の引数を持つ関数を使用するときにタイプミスを検出します。
これは行われました。 そして、ここで私はその結果にショックを受けました。 ほとんどの開発者は、これらの問題についてまったく考えず、次の形式のコードを冷静に記述します。
クラスCRuleDesc
{
CString GetProtocol();
CString GetSrcIp();
CString GetDestIp();
CString GetSrcPort();
CString GetIpDesc(CString strIp);
...
CString CRuleDesc :: GetRuleDesc()
{
CString strDesc;
strDesc.Format(
_T( "%s <br>%sからのすべてのネットワークトラフィック"
「on%s <br> to%s on%s <br> for the%s」)、
GetAction()、GetSrcIp()、GetSrcPort()、
GetDestIp()、GetDestPort()、GetProtocol());
return strDesc;
}
// ---------------
CString strText;
CString _strProcName(L "");
...
strText.Format(_T( "%s")、_strProcName);
// ---------------
CString m_strDriverDosName;
CString m_strDriverName;
...
m_strDriverDosName.Format(
_T( "\\\\。\\%s")、m_strDriverName);
// ---------------
CString __stdcall GetResString(UINT dwStringID);
...
_stprintf(acBuf、_T( "%s")、
GetResString(IDS_SV_SERVERINFO));
// ---------------
//明らかだと思う
//例を引用および引用できること。
そして、いくつかは思慮深いが、忘れられている。 したがって、次のコードはとても感動的です。
CString sAddr;
CString m_sName;
CString sTo = GetNick(hContact);
sAddr.Format(_T( "\\\\%s \\メールスロット\\%s")、
sTo、(LPCTSTR)m_sName);
そして、私たちがPVS-Studioをテストしているプロジェクトにはそのような例が非常に多かったので、これがどのようになるのかさえ分かりませんでした。 それでも、テストプログラムを作成し、CStringを使用するためのさまざまなオプションを試すことで検証できたため、これはすべてうまくいきます。
問題は何ですか? どうやら、コンパイラ開発者は、CStringを使用するヒンドゥー教のプログラムが機能しないという無限の質問と、「文字列で正しく機能しないコンパイラ」の非難に耐えることができなかったようです。 そして彼らは静かにエクソシズムの神聖な儀式を行い、CStringから悪を追放した。 彼らは不可能を可能にしました。 つまり、CStringクラスは特別なトリッキーな方法で実装されているため、printf形式の関数で渡すことができます。
これは非常に巧妙に行われ、興味のある人は誰でもCStringTクラスのソースコードを読むことができ、「
CStringをprintfに渡す? 」[5]に関するこれらの広範な議論に精通することができます。 詳細は説明しません。 重要な点だけに注意します。 CStringの特別な実装では十分ではありません;理論的には、非POD型の転送は予測できない動作を引き起こします。 そのため、Visual C ++の開発者は、Intel C ++を使用して、予測不可能な動作が常に正しい結果になるようにしました。 :)結局のところ、プログラムの正しい動作は予測不可能な動作のかなりのサブセットです。 :)
そして今、64ビットプログラムをビルドする際のコンパイラの動作の奇妙な機能について考え始めています。 コンパイラの開発者は、特定のパターンを認識する単純なケースで、プログラムの動作を理論的ではなく実用的(実行可能)に意図的にする疑いがあります。 最も理解しやすい例は、ループパターンです。 無効なコードの例:
size_t n = BigValue;
for(unsigned i = 0; i <n; i ++){...}
理論的には、n> UINT_MAXの値が大きい場合、無限ループが発生するはずです。 ただし、変数「i」は64ビットレジスタを使用するため、リリースバージョンでは発生しません。 もちろん、コードがより複雑な場合、無限ループが発生しますが、少なくともいくつかの場合、プログラムは幸運になります。 これについては、記事「
カウントできる64ビットの馬 」で詳しく説明しました[6]。
プログラムのこのような予想外に成功した動作は、リリースバージョンの最適化の機能にのみ関連していると考えていました。 しかし、今はよくわかりません。 おそらくこれは、少なくとも時々実行できないプログラムを実行可能にするための意識的な試みです。 もちろん、私は知りません、理由は最適化または兄貴の世話です、しかし、これは哲学する理由の波です。 :)まあ、誰が知っている、それは言うことはほとんどありません。 :)
コンパイラが障害のあるプログラムに手を出すときが他にもあると確信しています。 他に何か面白いものがあれば、ぜひ教えてあげましょう。
バグのないコードをお祈りします!
書誌リスト
- ブログAlexey Pakhunov。 下位互換性は深刻です。 http://www.viva64.com/go.php?url=390
- ブログAlexey Pakhunov。 AppCompat。 http://www.viva64.com/go.php?url=391
- ブログAlexey Pakhunov。 Windows 3.xは生きていますか? http://www.viva64.com/go.php?url=392
- MSDN Cスタイルの文字列に関連するCString操作。 トピック:変数引数関数でのCStringオブジェクトの使用。 http://www.viva64.com/go.php?url=393
- eggheadcafe.comでの議論。 CStringをprintfに渡しますか? http://www.viva64.com/go.php?url=394
- アンドレイ・カルポフ。 カウントできる64ビットの馬。 http://www.viva64.com/art-1-1-1064884779.html