###概要 ######
今日は C の移植性について話します。普段開発に C を使用していて、C の移植性の問題についてよくわからない場合は、このシリーズを参照することをお勧めします。現在クロスプラットフォーム開発の必要がない場合でも、移植性について知っておくと役立ちます。
C の移植性は大きなトピックであり、コンパイラ、オペレーティング システム、ハードウェア システムなど多くの側面をカバーしており、それぞれの側面に多くの内容があります。私の限られた能力とエネルギーを考慮して、参考までに各側面で最も一般的な問題を紹介することしかできません。
後ほど、コンパイラ、C 構文、オペレーティング システム、サードパーティ ライブラリ、補助ツール、開発プロセスなどの側面から紹介します。
コンパイラ
クロスプラットフォーム開発プロセスでは、多くの問題がコンパイラに関連します。それでは、最初にコンパイラ関連の問題について話しましょう。
コンパイラの選択
まず第一に、GCC はほぼすべてのオペレーティング システム プラットフォームで利用できるため、GCC のサポートが優先されます。基本的にはユニバーサルコンパイラになります。コードをプラットフォーム A 上の GCC でコンパイルして渡し、その後プラットフォーム B 上の同様のバージョンの GCC でコンパイルできる場合、通常は大きな問題はありません。したがって、GCC は必ずサポートを検討する必要があります。
次に、ローカル コンパイラをサポートするかどうかを検討します。いわゆるローカル コンパイラは、オペレーティング システムの製造元によって製造されたコンパイラです。たとえば、Windows のネイティブ コンパイラは Visual C です。 Solaris に関連するローカル コンパイラは SUN の CC です。パフォーマンスにさらに敏感な場合、または一部のローカル コンパイラーの高度な機能を使用したい場合は、GCC だけでなくローカル コンパイラーのサポートを検討する必要がある場合があります。
コンパイル警告
コンパイラはプログラマの友人です。多くの潜在的な問題 (移植性を含む) がコンパイラによって検出され、警告が表示されます。これらの警告メッセージに普段から注意を払っていれば、問題の多くを減らすことができます。トラブル。したがって、次のことを強くお勧めします:
1 コンパイラの警告レベルを上げる;
2 コンパイラの警告メッセージを安易に無視しないでください。
クロスコンパイラ
クロスコンパイラの定義については、「Wikipedia」を参照してください。平たく言えば、プラットフォーム A でバイナリ プログラムをコンパイルし、プラットフォーム B で実行することを意味します。開発したいアプリケーションが Solaris 上で実行されるが、Solaris を実行できる SPARC マシンがない場合、クロスコンパイラが役に立ちます。通常、クロスコンパイラの作成には GCC が使用されますが、紙面の都合上、ここでは詳しく説明しません。興味のある方は「こちら」をご覧ください。
例外処理
前回の記事「文法」では紙面の都合上、例外について話す時間がありませんでしたので、今回は例外に関わる部分を分けてお話します。
メモリ割り当ての新たな失敗に注意してください
初期の旧式コンパイラによって生成されたコードは、new が失敗すると null ポインタを返します。私が使っていたBorland C 3.1はこんな感じで、この手のコンパイラは今では珍しいはずです。現在使用しているコンパイラが依然としてこのような動作をする場合は、問題があります。例外処理を容易にするために、new 演算子をオーバーロードして bad_alloc 例外をスローすることを検討することもできます。
少し新しいコンパイラは、単に null ポインタを返すだけではありません。 new オペレーターは、標準 (C 03 標準セクション 18.4.2 を参照) に従って、メモリーが制御不能になったことを検出すると、new_handler 関数を呼び出す必要があります (プロトタイプは typedef void (*new_handler)();)。標準では、new_handler 関数が次の 3 つのことを実行することを推奨しています:
1. より多くのメモリを取得しようとする;
2. bad_alloc 例外をスローする;
3. abort() または exit() を呼び出して、プロセス。
new_handler 関数は (set_new_handler を呼び出すことで) リセットできるため、上記の動作が発生する可能性があります。
要約すると、new がメモリの割り当てに失敗した場合、3 つの可能性があります:
1. null ポインタを返す;
2. 例外をスローする;
3. プロセスはただちに終了します。
コードの移植性を高めたい場合は、3 つの状況をすべて考慮する必要があります。
例外仕様は慎重に使用してください
私の意見では、例外仕様は良いことではありません。信じられない場合は、「C コーディング標準 - 101 のルール、ガイドライン、ベスト プラクティス」の第 75 条を読んでください。 。 (具体的な欠点については、後で C の例外とエラー処理に関する別の投稿で説明します。) より身近なところでは、標準 (03 標準の第 18.6.2 章を参照) に従って、関数によってスローされた例外が含まれていない場合です。 function の例外仕様で、 unexced() を呼び出す必要があります。ただし、コンパイラで生成されたすべてのコードが標準に準拠しているわけではありません (VC コンパイラの一部のバージョンなど)。サポートする必要があるコンパイラが例外仕様と矛盾する動作をする場合は、例外仕様宣言を削除することを検討してください。
モジュール間で例外をスローしないでください
ここで言及されているモジュールは、動的ライブラリを指します。プログラムに複数の動的ライブラリが含まれている場合は、モジュールのエクスポートされた関数の外側で例外をスローしないでください。結局のところ、C には ABI 標準がまだ存在せず (将来的には存在しない可能性があると推定されています)、モジュール間で例外をスローすると、多くの予測不可能な動作が発生することになります。
構造化例外処理 (SEH) を使用しないでください
SEH について聞いたことがない場合は、私が SEH について言及しなかったことにして、この段落を飛ばしてください。以前に SEH を使用することに慣れている場合は、クロスプラットフォーム コードを作成する前に、この習慣を変更する必要があります。 SEH を含むコードは Windows プラットフォームでのみコンパイルでき、クロスプラットフォームではありません。
catch(…) について
論理的に言えば、catch(…) ステートメントは C 例外タイプのみをキャプチャでき、アクセス違反やゼロ除算エラーなどの非 C 例外には無力です。ただし、場合によっては (一部の VC コンパイラなど)、アクセス違反やゼロ除算エラーなども catch(...) で捕捉できます。したがって、コードを移植可能にしたい場合は、プログラム ロジックで上記の catch(...) の動作に依存することはできません。
ハードウェアシステム関連
今回は主にハードウェアシステムに関する話題です。たとえば、プログラムが異なるタイプの CPU (x86、SPARC、PowerPC)、または異なるワード長を持つ同じタイプの CPU (x86 と x86-64 など) をサポートする必要がある場合は、ハードウェア システムに注意する必要があります。 。
基本型のサイズ
C の基本型のサイズ (占有バイト数) は、CPU のワード長が変化するにつれて変化します。したがって、int が占めるバイト数を表現したい場合は、「4」を直接書くのではなく (ちなみに、「4」を直接書くこともマジックナンバーのタブーに違反します。詳しくはこちらを参照してください)、 " sizeof(int)"; 逆に、サイズが 4 バイトでなければならない符号付き整数を定義したい場合は、int を直接使用せず、typedef 前の固定長型 (ブースト ライブラリ、ACE の int32_t など) を使用します。ライブラリ ACE_INT32 など)。
忘れるところでしたが、ポインタのサイズにも上記の問題があるので注意してください。
エンディアンネス
「エンディアンネス」について聞いたことがない場合は、「Wikipedia」を読んでください。よく使われる例えで言えば、ビッグエンディアンマシンに 4 バイトの整数 0x01020304 があった場合、ネットワークまたはファイルを介してリトルエンディアンマシンに送信されると 0x04030201 になります。エンディアンマシン(私は触ったことがありませんが)の場合、上記の整数は 0x02010403 になります。
作成するアプリケーションにネットワーク通信が含まれる場合は、ホスト コードとネットワーク コードを忘れずに変換する必要があります。マシン間でバイナリ ファイルを転送する必要がある場合は、同様の変換を実行することも忘れずに必要です。
メモリ アライメント
「メモリ アライメント」が何なのかわからない場合は、「Wikipedia」を読んでください。簡単に言うと、CPU の処理性能を考慮すると、構造体のデータは互いに近接せず、ある程度のスペースを空ける必要があります。この場合、構造内の各データのアドレスは、特定のワード長の正確な整数倍になります。
メモリ アライメントの詳細は C 標準で定義されていないため、コードはアライメントの詳細に依存できません。構造体のサイズが計算される場合は常に、sizeof() が正直に記述されます。
一部のコンパイラでは、#pragma Pack プリペアド ステートメント (アラインメントの語長の変更に使用できます) をサポートしていますが、この構文はすべてのコンパイラでサポートされているわけではないため、使用には注意が必要です。
シフト演算
符号付き整数の右シフト演算では、一部のシステムはデフォルトで算術右シフトを使用します (最上位の符号ビットは変更されません)。また、一部のシステムはデフォルトで論理右シフトを使用します (最上位の符号ビットは次のように埋められます)。 0)。したがって、符号付き整数に対して右シフトを実行しないでください。ちなみに、移植性に問題がない場合でも、コード内ではシフト演算子をできるだけ使用しないようにしてください。シフト操作を使用してパフォーマンスを向上させようとしている学生は、より注意を払う必要がありますが、これは可読性が低いだけでなく、非常に有益です。コンパイラーがあまりにも精神薄弱でない限り、プログラマーが心配する必要なく、コンパイラーがこの最適化を自動的に処理します。
オペレーティング システム
前回は「ハードウェア システム」に関する話題をご紹介しましたが、今回はオペレーティング システムに関連した話題についてお話します。 C クロスプラットフォーム開発ではOSに関する些細なことが多いので、今日は長々と長々と書きますが、ご容赦ください:-)
負けないように、Linuxや各種Unix以下では、総称して Posix システムと呼びます。
ファイル システム (FileSystem、以下 FS)
クロスプラットフォーム開発を始めたばかりのほとんどの初心者は、FS に関連した問題に遭遇するでしょう。それではまずFSについて話しましょう。要約すると、開発中に遭遇しやすい FS の違いには主に、ディレクトリ区切り文字の違い、大文字と小文字の区別の違い、パス内の禁止文字の違いが含まれます。
上記の違いに対処するには、次の点に注意する必要があります:
1. ファイルとディレクトリの命名は標準化する必要があります
ファイルとディレクトリに名前を付けるときは、文字と数字のみを使用するようにしてください。類似した名前 (foo.cpp と Foo.cpp など、名前の大文字と小文字が異なるだけ) の 2 つのファイルを同じディレクトリに配置しないでください。一部の OS の予約語 (aux、con、nul、prn など) をファイル名またはディレクトリ名として使用しないでください。
さらに付け加えますと、今述べた名前には、ソース コード ファイル、バイナリ ファイル、および実行時に作成されるその他のファイルが含まれます。
2. #include ステートメントは標準化する必要があります
#include ステートメントを記述するときは、バックスラッシュ "\" (Windows でのみ使用可能) の代わりにスラッシュ "/" (より一般的) を使用するように注意してください。 #include ステートメント内のファイル名とディレクトリ名は、大文字と小文字が実際の名前とまったく同じである必要があります。
3. コードに FS 操作が含まれる場合は、既製のライブラリを使用してみてください
FS 用の成熟したサードパーティ ライブラリはすでに多数存在します (boost::filesystem など)。コードに FS 操作 (ディレクトリ トラバーサルなど) が含まれる場合は、これらのサードパーティ ライブラリを使用してみてください。これにより、作業を大幅に節約できます。
★テキスト ファイルのキャリッジ リターン CR/ライン フィード LF
この迷惑な問題は、いくつかのよく知られたオペレーティング システムによるキャリッジ リターン/ライン フィードの処理が一貫していないために発生します。現在の状況は、Windows は CR と LF の両方を使用し、Linux とほとんどの Unix は LF を使用し、Apple の Mac シリーズは CR を使用します。
ソース コード管理については、幸いなことに、多くのバージョン管理ソフトウェア (CVS、SVN など) がこの問題をインテリジェントに処理し、コード ベースからローカル ソース コードを取得してローカル形式に適合させることができます。
プログラムが実行時にテキスト ファイルを処理する必要がある場合は、テキスト モードで開く場合とバイナリ モードで開く場合の違いに注意してください。また、異なるシステム間でテキスト ファイルを転送する必要がある場合は、適切な処理を検討してください。
★ファイル検索パス (実行可能ファイルやダイナミック ライブラリの検索を含む)
Windows では、ファイルを実行するかダイナミック ライブラリをロードする場合、通常は現在のディレクトリを検索しますが、常にそうであるとは限りません。 Posix システムでは。したがって、アプリケーションにプロセスの開始や動的ライブラリのロードが含まれる場合は、この違いに注意してください。
★環境変数
上記の検索パスの問題に対して、PATH や LD_LIBRARY_PATH を変更して現在のパスを導入したい学生もいます。この方法を使用する場合は、プロセス レベルの環境変数のみを変更し、システム レベルの環境変数は変更しないことをお勧めします (システム レベルでの変更は、同じマシン上の他のソフトウェアに影響を及ぼし、副作用が発生する可能性があります)。
★ダイナミック ライブラリ
アプリケーションでダイナミック ライブラリを使用する場合は、ダイナミック ライブラリが標準 C スタイル関数をエクスポートすることを強くお勧めします (クラスはエクスポートしないようにしてください)。 Posix システムにダイナミック ライブラリをロードする場合は、RTLD_GLOBAL フラグを注意して使用してください。このフラグはグローバル シンボル テーブルを有効にします。これにより、複数の動的ライブラリ間でシンボル名の競合が発生する可能性があります (これが発生すると、信じられないほどのランタイム エラーが発生し、デバッグが非常に困難になります)。
★サービス/ガーディアンプロセス
サービスとガードプロセスの概念がよくわからない場合は、Wikipedia (こことここ) を読んでください。以下、説明の便宜上、これらを総称してサービスと呼びます。
C で開発されたモジュールのほとんどはバックグラウンド モジュールであるため、サービスの問題が頻繁に発生します。サービスを作成するには、いくつかのシステム関連 API を呼び出す必要があるため、オペレーティング システムとの密接な結合が生じ、1 セットのコードを使用することが困難になります。したがって、より良い方法は、一般的なサービス シェルを抽象化し、その下にビジネス ロジック コードを動的ライブラリとしてマウントすることです。この場合、少なくとも 1 セットのビジネス ロジック コードが確保されます。2 セットのサービス シェル コード (Windows 用と Posix 用に 1 つ) が必要ですが、これらはビジネスに依存しないため、簡単に再利用できます。
★デフォルトのスタックサイズ
オペレーティングシステムが異なると、デフォルトのスタックサイズは数十KB(Symbianは12Kしかないと言われており、本当にケチです)から数MBまで大きく異なります。したがって、ターゲット システムのデフォルトのスタック サイズを事前に確認し、Symbian のようなケチなシステムに遭遇した場合は、コンパイラ オプションを使用してスタック サイズを増やすことを検討してください。もちろん、「スタック上に大きな配列や大きなオブジェクトを定義しない」という良い習慣を身につけることも重要です。そうしないと、スタックがどんなに大きくても、スタックがバーストしてしまいます。
マルチスレッド
ここ 1 か月ほどで私が書いた投稿は非常に複雑だったので、このシリーズは長期間更新されませんでした。その結果、別のネチズンがコメントで私を促したので、少し恥ずかしくなりました。今日は急いでマルチスレッドの章を完了してください。前回オペレーティング システムについて話したときは、OS 関連の話題は比較的些細なものだったので、雑多な話題をたくさん話しました。そのとき、記事が少し長かったので、マルチプロセスとマルチスレッドの部分は後回しにしました。
★コンパイラ
◇C ランタイム ライブラリのオプションについて
まず、非常に基本的な質問である C ランタイム ライブラリ (以下、CRT: C Run-Time) の設定についてお話します。そんなレベルの低い話はしたくなかったのですが、私の周りにもこの場で被害に遭った人が何人かいるので、話した方が良いかもしれません。
ほとんどの C コンパイラには CRT (おそらく複数) が付属しています。一部のコンパイラに付属の CRT は、スレッドのサポートに基づいてシングルスレッド CRT とマルチスレッド CRT に分類される場合があります。マルチスレッド開発を実行する場合は、関連する C プロジェクトがマルチスレッド CRT を使用していることを確認することを忘れないでください。そうでなければ、それは醜い死となるでしょう。
特に、Visual C を使用してエンジニアリング プロジェクトを作成する場合は、より注意する必要があります。新しく作成したプロジェクトに MFC (コンソール プロジェクトや Win32 プロジェクトを含む) が含まれていない場合、プロジェクトのデフォルト設定では、次の図に示すように「シングル スレッド CRT」が使用されます。
◇最適化オプションについて
「最適化オプション」もコンパイラ関連の重要なトピックです。一部のコンパイラは、優れていると言われている最適化オプションを提供していますが、一部の最適化オプションには潜在的なリスクがある可能性があります。コンパイラは、自らの判断で命令の実行順序を混乱させ、予期しないスレッド競合状態を引き起こす可能性があります (競合状態、詳細な説明については、「こちら」を参照してください)。 Liu Weipeng は、「C マルチスレッド メモリ モデル」でいくつかの典型的な例を示しています。参照してください。
コンパイラの通常の速度最適化オプションのみを使用することをお勧めします。他の派手な最適化オプションによる追加効果は明らかではないかもしれませんが、潜在的なリスクは小さくありません。本当にリスクを冒す価値はありません。
GCC を例にとると: -O2 オプションを使用することをお勧めします (実際、-O2 は多数のオプションの集合です)。理由)。 -O2 と -O3 に加えて、GCC にはその他の多数 (数百と推定される) の最適化オプションがあります。いずれかのオプションを使用する予定がある場合は、まずその特性と起こり得る副作用を理解する必要があります。そうしないと、将来どのように死ぬかがわかりません。
★スレッドライブラリの選択
現在の C 03 標準にはスレッド関連の内容がほとんど含まれていないため (将来 C 0x にスレッド標準ライブラリが含まれるとしても、コンパイラ メーカーのサポートは短期的には包括的ではない可能性があります) )、したがって、今後長い間、クロスプラットフォームのマルチスレッドのサポートは依然としてサードパーティのライブラリに依存することになります。したがって、スレッド ライブラリの選択は非常に重要です。ここでは、いくつかのよく知られたクロスプラットフォーム スレッド ライブラリを簡単に紹介します。
◇ACE
まず、長い歴史を持つライブラリであるACEについてお話しましょう。これまでに遭遇したことがない場合は、まず「ここ」のリテラシーを見てください。 ACE(Adaptive Communication Environment)の正式名称から判断すると、「コミュニケーション」をベースにしたものでしょう。ただし、「マルチスレッド」のサイド ビジネスに対する ACE のサポートは、ミューテックス ロック (ACE_Mutex)、条件変数 (ACE_Condition)、セマフォ (ACE_Semaphore)、バリア (ACE_Barrier)、アトミック操作 (ACE_Atomic_Op) など、依然として非常に包括的です。 。 ACE_Mutex などの特定のタイプは、スレッド読み取り/書き込みロック (ACE_RW_Thread_Mutex)、スレッド再帰ロック (ACE_Recursive_Thread_Mutex) などにも細分されます。
包括的なサポートに加えて、ACE にはもう 1 つの明白な利点があります。それは、さまざまなオペレーティング システム プラットフォームと独自のコンパイラを適切にサポートしていることです。一部の古いコンパイラ (VC6 など) もサポートできます (ここで言うサポートとは、コンパイルできるだけでなく、安定して実行できることを意味します)。この利点は、クロスプラットフォーム開発では明らかです。
欠点は何ですか? ACE は非常に初期に (おそらく 1990 年代半ばに) 開始されたため、C の古い機能の多くは (新しい機能は言うまでもなく) まだリリースされていなかったため、ACE の全体的なスタイルはかなり時代遅れで、boost よりもはるかに劣っているように感じられます。 . とてもファッショナブルで前衛的です。
◇boost::thread
boost::thread は ACE とは対照的です。これはboostバージョン1.32から導入されたもののようで、ACEよりも新しいものです。ただし、boost の専門家グループのサポートのおかげで、開発は非常に迅速に行われています。現在の boost バージョン 1.38 では、多くの機能もサポートできます (ただし、ACE ほど多くはないようです)。 C 標準委員会の多くのメンバーが boost コミュニティに集まっているという事実を考慮すると、時間が経つにつれて、boost::thread は最終的には C スレッドの新星となり、明るい未来が待っています。
boost::thread の欠点は、多くのコンパイラ、特に一部の旧式のコンパイラをサポートしていないことです (多くの boost サブライブラリにはこの問題がありますが、これは主に高度なテンプレート構文を使用しているためです)。これはクロスプラットフォームにとって明らかな問題です。
◇wxWidgets と QT
wxWidgets と QT はどちらも GUI インターフェイス ライブラリですが、スレッドのサポートも組み込まれています。 wxWidgets スレッドの概要については「こちら」を、QT スレッドの概要については「こちら」をご覧ください。これら 2 つのライブラリはスレッドに対して同様のサポートを備えており、両方ともミューテックス、条件、セマフォなどの一般的に使用されるメカニズムを提供します。ただし、機能は ACE ほど豊富ではありません。
◇重み付け方法
GUI ソフトウェアを開発していて、すでに wxWidgets または QT を使用している場合は、それらの組み込みスレッド ライブラリを直接使用できます (基本的なスレッド関数のみを使用する場合)。スレッドライブラリが組み込まれているため、機能は少し薄いです。高度なスレッド機能が必要な場合は、boost::thread または ACE に置き換えることを検討してください。
boost::thread と ACE のどちらを選択するかについては、主にソフトウェアのニーズによって異なります。多数の複雑なプラットフォームをサポートする必要がある場合は、コンパイラでサポートされない問題を回避するために ACE を使用することをお勧めします。少数の主流プラットフォーム (Windows、Linux、Mac など) のみをサポートする必要がある場合は、boost::thread を使用することをお勧めします。結局のところ、主流のオペレーティング システムのコンパイラは依然としてブーストを適切にサポートしています。
★プログラミング上の注意事項
実際、マルチスレッド開発では注意すべき点がたくさんありますが、特に印象に残っている点をいくつか簡単に挙げておきます。
◇volatile について
マルチスレッドプログラミングで遭遇する可能性のある罠について語るとき、volatile キーワードについて言及する必要があります。よく分からない方は、まずは「こちら」を読んで学んでください。 C 98 規格も C 03 規格もマルチスレッド メモリ モデルを定義していないため、この規格は揮発性メモリとスレッドにのみ関係します。その結果、C コミュニティの多くの唾液は揮発性のものに集中しています (多くの C 界の偉人の唾液を含む)。このことを考慮して、ここでは詳細には触れません。素晴らしい記事をいくつかお勧めします。Andrei Alexandrescu の記事「こちら」、Hans Boehm の記事「こちら」と「こちら」。皆さんも、自分で読んでみてください。
◇アトミック操作について
学生の中には、複数のスレッドによる競合する書き込みにはロックが必要であることだけを知っていて、複数の読み取りと単一の書き込みも保護する必要があることを知らない人もいます。たとえば、整数 int nCount = 0x01020304 があり、同時状態では、書き込みスレッドがその値 nCount = 0x05060708 を変更し、別の読み取りスレッドが値を取得します。それでは、読み取りスレッドが「不正な」データ (0x05060304 など) を読み取ることは可能でしょうか?
データが破損しているかどうかは、nCount の読み取りと書き込みがアトミック操作であるかどうかによって決まります。これは、多くのハードウェア関連の要因 (CPU の種類、CPU のワード長、メモリ アライメントのバイト数など) によって決まります。場合によっては、実際にデータ破損が発生する可能性があります。
私たちはクロスプラットフォーム開発について話しているので、あなたのコードが将来どのようなハードウェア環境で実行されるかは神のみぞ知るです。したがって、同様の問題に対処する場合でも、安全性を確保するために、サードパーティのライブラリ (ACE の Atomic_Op など) によって提供されるアトミック操作クラス/関数を使用する必要があります。
◇オブジェクトの破棄について
前回の一連の投稿「C オブジェクトはどのように死ぬのか?」では、Win32 プラットフォームと Posix プラットフォームにおけるスレッドの不自然な死の問題をそれぞれ紹介しました。
上記のクロスプラットフォーム スレッド ライブラリの最下層は依然としてオペレーティング システムに付属のスレッド API を呼び出す必要があるため、すべてのスレッドが自然に終了できるように全員が最善を尽くす必要があります。
関連する推奨事項:
以上がC++ の移植性とクロスプラットフォーム開発 (長い記事)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。