portaldacalheta.pt
  • メイン
  • リモートの台頭
  • アジャイル
  • 財務プロセス
  • 収益と成長
技術

C ++のしくみ:コンパイルを理解する



ビャーネ・ストロヴルプの C ++プログラミング言語 「C ++のツアー:基本」というタイトルの章があります—標準C ++。その章の2.2では、C ++でのコンパイルとリンクのプロセスについて半ページで説明しています。コンパイルとリンクは、C ++ソフトウェア開発中に常に発生する2つの非常に基本的なプロセスですが、奇妙なことに、多くのC ++開発者には十分に理解されていません。

年齢確認クレジットカードをバイパスする

C ++ソースコードがヘッダーファイルとソースファイルに分割されるのはなぜですか?コンパイラーは各部分をどのように認識しますか?それはコンパイルとリンクにどのように影響しますか?あなたが考えたかもしれないが、慣習として受け入れるようになったこれらのような多くの質問があります。



C ++アプリケーションを設計する場合でも、新しい機能を実装する場合でも、バグ(特に特定の奇妙なバグ)に対処しようとする場合でも、CとC ++コードを連携させようとする場合でも、コンパイルとリンクがどのように機能するかを知っていると、時間を大幅に節約できます。それらのタスクをはるかに快適にします。この記事では、まさにそれを学びます。



この記事では、C ++コンパイラがいくつかの基本的な言語構造でどのように機能するかを説明し、プロセスに関連するいくつかの一般的な質問に答え、開発者がC ++開発でよく犯すいくつかの関連する間違いを回避するのに役立ちます。



注:この記事には、からダウンロードできるソースコードの例がいくつかあります。 https://bitbucket.org/danielmunoz/cpp-article

例はCentOSLinuxマシンでコンパイルされました。



$ uname -sr Linux 3.10.0-327.36.3.el7.x86_64

g ++バージョンの使用:

$ g++ --version g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-11)

提供されるソースファイルは他のオペレーティングシステムに移植可能である必要がありますが、自動ビルドプロセス用にそれらに付随するMakefileはUnixライクなシステムにのみ移植可能である必要があります。



ビルドパイプライン:前処理、コンパイル、リンク

各C ++ソースファイルは、オブジェクトファイルにコンパイルする必要があります。複数のソースファイルのコンパイルから得られたオブジェクトファイルは、実行可能ファイル、共有ライブラリ、または静的ライブラリにリンクされます(これらの最後はオブジェクトファイルの単なるアーカイブです)。 C ++ソースファイルには通常、.cpp、.cxx、または.cc拡張子のサフィックスが付いています。

C ++ソースファイルには、ヘッダーファイルと呼ばれる他のファイルを#includeでインクルードできます。指令。ヘッダーファイルには、.h、.hpp、.hxxなどの拡張子が付いているか、C ++標準ライブラリや他のライブラリのヘッダーファイル(Qtなど)のように拡張子がまったくありません。拡張子はC ++プリプロセッサには関係ありません。C++プリプロセッサは文字通り#includeを含む行を置き換えます。インクルードされたファイルのコンテンツ全体を含むディレクティブ。



コンパイラがソースファイルに対して実行する最初のステップは、そのファイルでプリプロセッサを実行することです。ソースファイルのみがコンパイラに渡されます(前処理とコンパイルのため)。ヘッダーファイルはコンパイラに渡されません。代わりに、それらはソースファイルから含まれています。

各ヘッダーファイルは、それらを含むソースファイルの数、またはソースファイルからインクルードされる他のヘッダーファイルの数に応じて、すべてのソースファイルの前処理フェーズ中に複数回開くことができます(多くのレベルの間接参照が存在する可能性があります) 。一方、ソースファイルは、コンパイラ(およびプリプロセッサ)に渡されるときに一度だけ開かれます。



プリプロセッサは、C ++ソースファイルごとに、#includeディレクティブが見つかったときにコンテンツを挿入すると同時に、ソースファイルとヘッダーからコードを削除して変換ユニットを構築します。 条件付きコンパイル ディレクティブがfalseと評価されるブロック。それはまたいくつかをします その他のタスク マクロ置換のように。

プリプロセッサがその(場合によっては巨大な)変換ユニットの作成を完了すると、コンパイラはコンパイルフェーズを開始し、オブジェクトファイルを生成します。



その翻訳単位(前処理されたソースコード)を取得するには、-Eオプションは、-oとともにg ++コンパイラに渡すことができます。前処理されたソースファイルの目的の名前を指定するオプション。

cpp-article/hello-worldでディレクトリには、「hello-world.cpp」サンプルファイルがあります。

#include int main(int argc, char* argv[]) { std::cout << 'Hello world' << std::endl; return 0; }

次の方法で前処理されたファイルを作成します。

$ g++ -E hello-world.cpp -o hello-world.ii

そして、行数を確認してください。

$ wc -l hello-world.ii 17558 hello-world.ii

私のマシンには17,588行あります。 makeを実行することもできますそのディレクトリで、それはあなたのためにそれらのステップを実行します。

コンパイラは、表示されている単純なソースファイルよりもはるかに大きなファイルをコンパイルする必要があることがわかります。これは、ヘッダーが含まれているためです。この例では、ヘッダーを1つだけ含めています。ヘッダーを含め続けると、翻訳単位はどんどん大きくなります。

この前処理とコンパイルのプロセスは、C言語でも同様です。コンパイルのC規則に従い、ヘッダーファイルをインクルードしてオブジェクトコードを生成する方法はほぼ同じです。

ソースファイルがシンボルをインポートおよびエクスポートする方法

cpp-article/symbols/c-vs-cpp-namesのファイルを見てみましょうディレクトリ。

関数の処理方法。

2つの関数をエクスポートするsum.cという名前の単純なC(C ++ではない)ソースファイルがあります。1つは2つの整数を追加するためのもので、もう1つは2つのfloatを追加するためのものです。

int sumI(int a, int b) { return a + b; } float sumF(float a, float b) { return a + b; }

それをコンパイルして(またはmakeを実行し、実行する2つのサンプルアプリを作成するためのすべての手順を実行して)、sum.oオブジェクトファイルを作成します。

$ gcc -c sum.c

次に、このオブジェクトファイルによってエクスポートおよびインポートされたシンボルを確認します。

$ nm sum.o 0000000000000014 T sumF 0000000000000000 T sumI

シンボルはインポートされず、2つのシンボルがエクスポートされます:sumFおよびsumI。これらのシンボルは、.textセグメント(T)の一部としてエクスポートされるため、関数名、実行可能コードです。

他の(CまたはC ++の両方の)ソースファイルがこれらの関数を呼び出したい場合は、呼び出す前にそれらを宣言する必要があります。

それを行う標準的な方法は、それらを宣言し、それらを呼び出したいソースファイルにインクルードするヘッダーファイルを作成することです。ヘッダーには、任意の名前と拡張子を付けることができます。 sum.hを選択しました:

#ifdef __cplusplus extern 'C' { #endif int sumI(int a, int b); float sumF(float a, float b); #ifdef __cplusplus } // end extern 'C' #endif

それらは何ですかifdef / endif条件付きコンパイルブロック? Cソースファイルからこのヘッダーを含める場合は、次のようにします。

int sumI(int a, int b); float sumF(float a, float b);

しかし、C ++ソースファイルからそれらを含める場合は、次のようになります。

extern 'C' { int sumI(int a, int b); float sumF(float a, float b); } // end extern 'C'

C言語はについて何も知りません extern 'C'指令 、しかしC ++はそうします、そしてそれはC関数宣言に適用されるこのディレクティブを必要とします。それの訳は C ++マングル関数(およびメソッド)名 関数/メソッドのオーバーロードをサポートしているのに対し、Cはサポートしていないためです。

これは、print.cppという名前のC ++ソースファイルで確認できます。

#include // std::cout, std::endl #include 'sum.h' // sumI, sumF void printSum(int a, int b) { std::cout << a << ' + ' << b << ' = ' << sumI(a, b) << std::endl; } void printSum(float a, float b) { std::cout << a << ' + ' << b << ' = ' << sumF(a, b) << std::endl; } extern 'C' void printSumInt(int a, int b) { printSum(a, b); } extern 'C' void printSumFloat(float a, float b) { printSum(a, b); }

同じ名前(printSum)の2つの関数があり、パラメーターのタイプのみが異なります。intまたはfloat。 関数のオーバーロードはC ++の機能です これはCには存在しません。この機能を実装してこれらの関数を区別するために、C ++は、エクスポートされたシンボル名に示されているように、関数名をマングルします(nmの出力から関連するものだけを選択します)。

$ g++ -c print.cpp $ nm print.o 0000000000000132 T printSumFloat 0000000000000113 T printSumInt U sumF U sumI 0000000000000074 T _Z8printSumff 0000000000000000 T _Z8printSumii U _ZSt4cout

これらの関数は(私のシステムでは)_Z8printSumffとしてエクスポートされます。フロートバージョンおよび_Z8printSumiiの場合intバージョンの場合。 extern 'C'として宣言されていない限り、C ++のすべての関数名はマングルされます。 print.cppでCリンケージで宣言された2つの関数があります:printSumIntおよびprintSumFloat。

したがって、オーバーロードすることはできません。または、マングルされていないため、エクスポートされた名前は同じになります。名前の末尾にIntまたはFloatを付けて、それらを区別する必要がありました。

マングルされていないため、すぐにわかるように、Cコードから呼び出すことができます。

C ++ソースコードで見られるようなマングルされた名前を見るには、-Cを使用できます。 nmの(デマングル)オプションコマンド。繰り返しますが、出力の同じ関連部分のみをコピーします。

$ nm -C print.o 0000000000000132 T printSumFloat 0000000000000113 T printSumInt U sumF U sumI 0000000000000074 T printSum(float, float) 0000000000000000 T printSum(int, int) U std::cout

このオプションでは、_Z8printSumffの代わりにprintSum(float, float)が表示され、_ZSt4coutの代わりにstd :: coutが表示されます。これは、より人間にわかりやすい名前です。

また、C ++コードがCコードを呼び出していることもわかります。print.cpp呼び出していますsumIおよびsumFは、sum.hでCリンケージを持つと宣言されたC関数です。これは、上記のprint.oのnm出力で確認できます。これは、いくつかの未定義(U)シンボルを通知します:sumF、sumIおよびstd::cout。これらの未定義のシンボルは、リンクフェーズで出力されたこのオブジェクトファイルと一緒にリンクされるオブジェクトファイル(またはライブラリ)の1つで提供されることになっています。

これまでのところ、ソースコードをオブジェクトコードにコンパイルしたばかりですが、まだリンクしていません。インポートされたシンボルの定義を含むオブジェクトファイルをこのオブジェクトファイルと一緒にリンクしないと、リンカは「シンボルがありません」というエラーで停止します。

print.cpp以降も注意してくださいはC ++ソースファイルであり、C ++コンパイラ(g ++)でコンパイルされ、その中のすべてのコードはC ++コードとしてコンパイルされます。 printSumIntのようなCリンケージを持つ関数およびprintSumFloat C ++機能を使用できるC ++関数でもあります。シンボルの名前のみがCと互換性がありますが、コードはC ++です。これは、両方の関数がオーバーロードされた関数(printSum)を呼び出していることからわかります。これは、| _ + _の場合は発生しません。 |またはprintSumInt Cでコンパイルされました。

ここで、printSumFloatを見てみましょう。これはCまたはC ++ソースファイルの両方からインクルードできるヘッダーファイルで、print.hppを許可します。およびprintSumInt CとC ++の両方から呼び出され、printSumFloat C ++から呼び出されます:

printSum

Cソースファイルからインクルードしている場合は、次のことを確認するだけです。

#ifdef __cplusplus void printSum(int a, int b); void printSum(float a, float b); extern 'C' { #endif void printSumInt(int a, int b); void printSumFloat(float a, float b); #ifdef __cplusplus } // end extern 'C' #endif

void printSumInt(int a, int b); void printSumFloat(float a, float b); 名前が壊れているため、Cコードからは見えません。そのため、Cコード用に宣言する(標準的で移植可能な)方法がありません。はい、次のように宣言できます。

printSum

そして、それは私の現在インストールされているコンパイラがそれのために発明した正確な名前なので、リンカは文句を言いませんが、それがあなたのリンカ(あなたのコンパイラが別のマングル名を生成する場合)で機能するかどうか、あるいは私のリンカーの次のバージョン。異なる存在があるため、通話が期待どおりに機能するかどうかさえわかりません 呼び出し規約 (パラメーターが渡され、戻り値が返される方法)コンパイラー固有であり、CおよびC ++呼び出し(特にメンバー関数であり、このポインターをパラメーターとして受け取るC ++関数の場合)では異なる場合があります。

コンパイラーは、通常のC ++関数に対して1つの呼び出し規約を使用し、外部「C」リンケージを持つと宣言されている場合は別の呼び出し規約を使用する可能性があります。したがって、1つの関数が実際にはC ++を使用しているのに、C呼び出し規約を使用していると言ってコンパイラをだますと、コンパイルツールチェーンでそれぞれに使用されている規約が異なる場合、予期しない結果が生じる可能性があります。

がある CとC ++を混合する標準的な方法 コードとCからC ++オーバーロード関数を呼び出す標準的な方法は次のとおりです。 それらをCリンケージを持つ関数でラップします void _Z8printSumii(int a, int b); void _Z8printSumff(float a, float b); をラップして行ったようにprintSumでおよびprintSumInt。

printSumFloatを含めるとC ++ソースファイルからprint.hppプリプロセッサマクロが定義され、ファイルは次のように表示されます。

__cplusplus

これにより、C ++コードでオーバーロードされた関数printSumまたはそのラッパーを呼び出すことができますvoid printSum(int a, int b); void printSum(float a, float b); extern 'C' { void printSumInt(int a, int b); void printSumFloat(float a, float b); } // end extern 'C' およびprintSumInt。

次に、プログラムのエントリポイントであるmain関数を含むCソースファイルを作成しましょう。このCメイン関数はprintSumFloatを呼び出しますprintSumIntは、つまり、Cリンケージを使用して両方のC ++関数を呼び出します。これらはC ++関数(関数本体はC ++コードを実行します)であり、C ++のマングル名しかありません。ファイルの名前はprintSumFloatです。

Windows用のコマンドラインツール
c-main.c

それをコンパイルしてオブジェクトファイルを生成します。

#include 'print.hpp' int main(int argc, char* argv[]) { printSumInt(1, 2); printSumFloat(1.5f, 2.5f); return 0; }

そして、インポート/エクスポートされたシンボルを参照してください。

$ gcc -c c-main.c

mainをエクスポートし、$ nm c-main.o 0000000000000000 T main U printSumFloat U printSumInt をインポートします。予想通り、printSumFloat。

リンクするファイルprintSumIntが少なくとも1つC ++でコンパイルされているため、すべてを実行可能ファイルにリンクするには、C ++リンカー(g ++)を使用する必要があります。

print.o

実行すると、期待される結果が得られます。

$ g++ -o c-app sum.o print.o c-main.o

それでは、$ ./c-app 1 + 2 = 3 1.5 + 2.5 = 4 という名前のC ++メインファイルを試してみましょう。

cpp-main.cpp

#include 'print.hpp' int main(int argc, char* argv[]) { printSum(1, 2); printSum(1.5f, 2.5f); printSumInt(3, 4); printSumFloat(3.5f, 4.5f); return 0; } のインポート/エクスポートされたシンボルをコンパイルして確認しますオブジェクトファイル:

cpp-main.o

メインをエクスポートし、Cリンケージをインポートします$ g++ -c cpp-main.cpp $ nm -C cpp-main.o 0000000000000000 T main U printSumFloat U printSumInt U printSum(float, float) U printSum(int, int) およびprintSumFloat、および両方のマングルバージョンのprintSumInt。

メインシンボルがprintSumのようなマングルシンボルとしてエクスポートされないのはなぜか疑問に思われるかもしれません。これはC ++ソースファイルであり、main(int, char**)として定義されていないため、このC ++ソースから。ええと、extern 'C'です 特別な実装定義関数 私の実装では、CまたはC ++のソースファイルで定義されているかどうかに関係なく、Cリンケージを使用することを選択したようです。

プログラムをリンクして実行すると、期待される結果が得られます。

main

ヘッダーガードのしくみ

これまで、同じソースファイルから直接または間接的にヘッダーを2回含めないように注意してきました。ただし、1つのヘッダーに他のヘッダーを含めることができるため、同じヘッダーを間接的に複数回含めることができます。また、ヘッダーコンテンツは、ヘッダーコンテンツが含まれていた場所に挿入されるだけなので、重複した宣言で簡単に終了できます。

$ g++ -o cpp-app sum.o print.o cpp-main.o $ ./cpp-app 1 + 2 = 3 1.5 + 2.5 = 4 3 + 4 = 7 3.5 + 4.5 = 8 のサンプルファイルを参照してください。

cpp-article/header-guards

違いは、guarded.hppでは、// unguarded.hpp class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; // guarded.hpp: #ifndef __GUARDED_HPP #define __GUARDED_HPP class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; #endif // __GUARDED_HPP の場合にのみ含まれる条件でヘッダー全体を囲むことです。プリプロセッサマクロが定義されていません。プリプロセッサにこのファイルが初めて含まれるときは、定義されません。ただし、マクロはその保護されたコード内で定義されているため、次に(同じソースファイルから直接または間接的に)インクルードされると、プリプロセッサは#ifndefと#endifの間の行を認識し、その間のすべてのコードを破棄します。それら。

このプロセスは、コンパイルするすべてのソースファイルで発生することに注意してください。これは、このヘッダーファイルをソースファイルごとに1回だけインクルードできることを意味します。あるソースファイルからインクルードされたからといって、そのソースファイルのコンパイル時に別のソースファイルからインクルードされることを妨げることはありません。同じソースファイルから複数回インクルードされるのを防ぐだけです。

サンプルファイル__GUARDED_HPP main-guarded.cppを含む2回:

guarded.hpp

ただし、前処理された出力には、クラス#include 'guarded.hpp' #include 'guarded.hpp' int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); } の定義が1つしか表示されません。

A

したがって、問題なくコンパイルできます。

$ g++ -E main-guarded.cpp # 1 'main-guarded.cpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'main-guarded.cpp' # 1 'guarded.hpp' 1 class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; # 2 'main-guarded.cpp' 2 int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }

しかし$ g++ -o guarded main-guarded.cpp ファイルに含まれるものmain-unguarded.cpp 2回:

unguarded.hpp

また、前処理された出力には、クラスAの2つの定義が示されています。

#include 'unguarded.hpp' #include 'unguarded.hpp' int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }

これにより、コンパイル時に問題が発生します。

$ g++ -E main-unguarded.cpp # 1 'main-unguarded.cpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'main-unguarded.cpp' # 1 'unguarded.hpp' 1 class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; # 2 'main-unguarded.cpp' 2 # 1 'unguarded.hpp' 1 class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; # 3 'main-unguarded.cpp' 2 int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }

$ g++ -o unguarded main-unguarded.cpp からインクルードされたファイル:

main-unguarded.cpp:2:0

簡潔にするために、ほとんどが短い例であるため、必要がない場合は、この記事では保護されたヘッダーを使用しません。ただし、ヘッダーファイルは常に保護してください。ソースファイルではなく、どこからも含まれません。ヘッダーファイルだけ。

パラメータの値と一定性による受け渡し

unguarded.hpp:1:7: error: redefinition of 'class A' class A { ^ In file included from main-unguarded.cpp:1:0: unguarded.hpp:1:7: error: previous definition of 'class A' class A { ^ を見てくださいby-value.cppのファイル:

cpp-article/symbols/pass-by

#include #include #include // std::vector, std::accumulate, std::cout, std::endl using namespace std; int sum(int a, const int b) { cout << 'sum(int, const int)' << endl; const int c = a + b; ++a; // Possible, not const // ++b; // Not possible, this would result in a compilation error return c; } float sum(const float a, float b) { cout << 'sum(const float, float)' << endl; return a + b; } int sum(vector v) { cout << 'sum(vector)' << endl; return accumulate(v.begin(), v.end(), 0); } float sum(const vector v) { cout << 'sum(const vector)' << endl; return accumulate(v.begin(), v.end(), 0.0f); } を使用しているのでディレクティブでは、変換ユニットの残りの部分(私の場合はソースファイルの残りの部分)のstd名前空間内のシンボル(関数またはクラス)の名前を修飾する必要はありません。これがヘッダーファイルの場合、ヘッダーファイルは複数のソースファイルからインクルードされることになっているため、このディレクティブを挿入するべきではありませんでした。このディレクティブは、各ソースファイルのグローバルスコープに、ヘッダーが含まれているポイントからstd名前空間全体をもたらします。

楽しいC ++プログラム

それらのファイルで私の後に含まれるヘッダーでさえ、スコープ内にそれらのシンボルがあります。彼らはこれが起こることを予期していなかったので、これは名前の衝突を引き起こす可能性があります。したがって、このディレクティブをヘッダーで使用しないでください。必要な場合にのみ、すべてのヘッダーを含めた後でのみ、ソースファイルで使用してください。

一部のパラメーターがconstであることに注意してください。これは、関数の本体で変更しようとしても変更できないことを意味します。コンパイルエラーが発生します。また、このソースファイルのすべてのパラメーターは、参照(&)やポインター(*)ではなく、値によって渡されることに注意してください。これは、呼び出し元がそれらのコピーを作成し、関数に渡すことを意味します。したがって、呼び出し元がconstであるかどうかは関係ありません。関数本体で変更すると、呼び出し元が関数に渡した元の値ではなく、コピーのみが変更されるためです。

値(コピー)によって渡されるパラメーターの恒常性は呼び出し元にとって重要ではないため、オブジェクトコード(関連する出力のみ)をコンパイルおよび検査した後にわかるように、関数シグネチャでマングルされません。

using namespace std

シグニチャは、コピーされたパラメータが関数の本体でconstであるかどうかを表しません。関係ありません。これらの値が変更されるかどうかを関数本体の読者に一目で示すために、関数定義のみが重要でした。この例では、パラメーターの半分だけがconstとして宣言されているため、コントラストを確認できますが、 const-correct 関数本体で変更されていないため、すべて宣言されている必要があります(変更されるべきではありません)。

呼び出し元に表示される関数宣言は重要ではないため、$ g++ -c by-value.cpp $ nm -C by-value.o 000000000000001e T sum(float, float) 0000000000000000 T sum(int, int) 0000000000000087 T sum(std::vector) 0000000000000048 T sum(std::vector ) を作成できます。このようなヘッダー:

by-value.hpp

ここにconst修飾子を追加することは許可されていますが(定義にconstではないconst変数として修飾することもでき、機能します)、これは必須ではなく、宣言を不必要に冗長にするだけです。

参照渡し

見てみましょう#include int sum(int a, int b); float sum(float a, float b); int sum(std::vector v); int sum(std::vector v); :

by-reference.cpp

参照を渡すときの一貫性は、呼び出し元にとって重要です。これは、呼び出し先が引数を変更するかどうかを呼び出し元に通知するためです。したがって、シンボルは一定の状態でエクスポートされます。

#include #include #include using namespace std; int sum(const int& a, int& b) { cout << 'sum(const int&, int&)' << endl; const int c = a + b; ++b; // Will modify caller variable // ++a; // Not allowed, but would also modify caller variable return c; } float sum(float& a, const float& b) { cout << 'sum(float&, const float&)' << endl; return a + b; } int sum(const std::vector& v) { cout << 'sum(const std::vector&)' << endl; return accumulate(v.begin(), v.end(), 0); } float sum(const std::vector& v) { cout << 'sum(const std::vector&)' << endl; return accumulate(v.begin(), v.end(), 0.0f); }

これは、呼び出し元が使用するヘッダーにも反映されている必要があります。

$ g++ -c by-reference.cpp $ nm -C by-reference.o 0000000000000051 T sum(float&, float const&) 0000000000000000 T sum(int const&, int&) 00000000000000fe T sum(std::vector const&) 00000000000000a3 T sum(std::vector const&)

これまで行っていたように、宣言(ヘッダー)に変数の名前を記述しなかったことに注意してください。この例と前の例では、これも合法です。呼び出し元は変数にどのように名前を付けるかを知る必要がないため、宣言に変数名は必要ありません。ただし、宣言では一般にパラメーター名が望ましいため、ユーザーは各パラメーターの意味と、呼び出しで送信する内容を一目で知ることができます。

驚いたことに、関数の定義には変数名も必要ありません。これらは、関数で実際にパラメーターを使用する場合にのみ必要です。ただし、使用しない場合は、パラメーターをタイプのままにして、名前を付けないでおくことができます。関数が決して使用しないパラメータを宣言するのはなぜですか?関数(またはメソッド)は、オブザーバーに渡される特定のパラメーターを定義するコールバックインターフェイスのように、インターフェイスの一部にすぎない場合があります。オブザーバーは、すべてのパラメーターが呼び出し元によって送信されるため、インターフェースが指定するすべてのパラメーターを使用してコールバックを作成する必要があります。ただし、オブザーバーはそれらすべてに関心があるとは限らないため、「未使用パラメーター」に関するコンパイラー警告を受け取る代わりに、関数定義は名前を付けずにそのままにしておくことができます。

ポインタを渡す

#include int sum(const int&, int&); float sum(float&, const float&); int sum(const std::vector&); float sum(const std::vector&);

const要素(例ではint)へのポインターを宣言するには、次のいずれかとして型を宣言できます。

// by-pointer.cpp: #include #include #include using namespace std; int sum(int const * a, int const * const b) { cout << 'sum(int const *, int const * const)' << endl; const int c = *a+ *b; // *a = 4; // Can't change. The value pointed to is const. // *b = 4; // Can't change. The value pointed to is const. a = b; // I can make a point to another const int // b = a; // Can't change where b points because the pointer itself is const. return c; } float sum(float * const a, float * b) { cout << 'sum(int const * const, float const *)' << endl; return *a + *b; } int sum(const std::vector* v) { cout << 'sum(std::vector const *)' begin(), v->end(), 0); v = NULL; // I can make v point to somewhere else return c; } float sum(const std::vector * const v) { cout << 'sum(std::vector const * const)' begin(), v->end(), 0.0f); }

ポインター自体もconstにしたい場合、つまり、ポインターを変更して他の何かを指すようにできない場合は、スターの後にconstを追加します。

int const * const int *

ポインター自体をconstにしたいが、ポインターが指す要素はしたくない場合:

int const * const const int * const

関数のシグネチャをオブジェクトファイルのデマングルされた検査と比較します。

int * const

ご覧のとおり、$ g++ -c by-pointer.cpp $ nm -C by-pointer.o 000000000000004a T sum(float*, float*) 0000000000000000 T sum(int const*, int const*) 0000000000000105 T sum(std::vector const*) 000000000000009c T sum(std::vector const*) ツールは最初の表記(タイプの後のconst)を使用します。また、エクスポートされ、呼び出し元にとって重要な唯一の定数は、関数がポインターが指す要素を変更するかどうかであることに注意してください。ポインタ自体は常にコピーとして渡されるため、ポインタ自体の恒常性は呼び出し元には関係ありません。この関数は、呼び出し元とは関係のない、別の場所を指すポインターの独自のコピーのみを作成できます。

したがって、ヘッダーファイルは次のように作成できます。

nm

ポインタによる受け渡しは、参照による受け渡しに似ています。 1つの違いは、参照を渡すと、呼び出し元は有効な要素の参照を渡したと見なされ、NULLやその他の無効なアドレスを指していないのに対し、ポインタはNULLを指している可能性があることです。 NULLを渡すことが特別な意味を持つ場合は、参照の代わりにポインタを使用できます。

C ++ 11の値は、 移動セマンティクス 。このトピックはこの記事では扱われませんが、次のような他の記事で調べることができます。 C ++での引数の受け渡し 。

ここで取り上げないもう1つの関連トピックは、これらすべての関数を呼び出す方法です。これらのヘッダーがすべてソースファイルから含まれているが呼び出されていない場合、コンパイルとリンクは成功します。ただし、すべての関数を呼び出したい場合は、一部の呼び出しがあいまいになるため、エラーが発生します。コンパイラーは、特にコピーで渡すか参照(またはconst参照)で渡すかを選択するときに、特定の引数に対して複数のバージョンのsumを選択できます。その分析は、この記事の範囲外です。

異なるフラグでコンパイルする

ここで、このテーマに関連して、見つけにくいバグが発生する可能性のある実際の状況を見てみましょう。

ディレクトリに移動#include int sum(int const* a, int const* b); float sum(float* a, float* b); int sum(std::vector* const); float sum(std::vector* const); そしてcpp-article/diff-flagsを見てください:

Counters.hpp

このクラスには2つのカウンターがあり、これらはゼロから始まり、インクリメントまたは読み取りが可能です。デバッグビルドの場合、これをビルドと呼びます。class Counters { public: Counters() : #ifndef NDEBUG // Enabled in debug builds m_debugAllCounters(0), #endif m_counter1(0), m_counter2(0) { } #ifndef NDEBUG // Enabled in debug build #endif void inc1() { #ifndef NDEBUG // Enabled in debug build ++m_debugAllCounters; #endif ++m_counter1; } void inc2() { #ifndef NDEBUG // Enabled in debug build ++m_debugAllCounters; #endif ++m_counter2; } #ifndef NDEBUG // Enabled in debug build int getDebugAllCounters() { return m_debugAllCounters; } #endif int get1() const { return m_counter1; } int get2() const { return m_counter2; } private: #ifndef NDEBUG // Enabled in debug builds int m_debugAllCounters; #endif int m_counter1; int m_counter2; }; マクロが定義されていないので、3番目のカウンターも追加します。これは、他の2つのカウンターのいずれかがインクリメントされるたびにインクリメントされます。これは、このクラスの一種のデバッグヘルパーになります。多くのサードパーティライブラリクラスまたは組み込みのC ++ヘッダー(コンパイラによって異なります)でさえ、このようなトリックを使用して、さまざまなレベルのデバッグを可能にします。これにより、デバッグビルドは、範囲外になるイテレータや、ライブラリメーカーが考える可能性のあるその他の興味深いことを検出できます。リリースビルドを「NDEBUGのビルド」と呼びます。マクロが定義されています。」

リリースビルドの場合、プリコンパイル済みヘッダーは次のようになります(NDEBUGを使用して空白行を削除します):

grep

デバッグビルドの場合、次のようになります。

$ g++ -E -DNDEBUG Counters.hpp | grep -v -e '^$' # 1 'Counters.hpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'Counters.hpp' class Counters { public: Counters() : m_counter1(0), m_counter2(0) { } void inc1() { ++m_counter1; } void inc2() { ++m_counter2; } int get1() const { return m_counter1; } int get2() const { return m_counter2; } private: int m_counter1; int m_counter2; };

前に説明したように、デバッグビルドにはもう1つのカウンターがあります。

また、いくつかのヘルパーファイルを作成しました。

$ g++ -E Counters.hpp | grep -v -e '^$' # 1 'Counters.hpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'Counters.hpp' class Counters { public: Counters() : m_debugAllCounters(0), m_counter1(0), m_counter2(0) { } void inc1() { ++m_debugAllCounters; ++m_counter1; } void inc2() { ++m_debugAllCounters; ++m_counter2; } int getDebugAllCounters() { return m_debugAllCounters; } int get1() const { return m_counter1; } int get2() const { return m_counter2; } private: int m_debugAllCounters; int m_counter1; int m_counter2; }; // increment1.hpp: // Forward declaration so I don't have to include the entire header here class Counters; int increment1(Counters&); // increment1.cpp: #include 'Counters.hpp' void increment1(Counters& c) { c.inc1(); } // increment2.hpp: // Forward declaration so I don't have to include the entire header here class Counters; int increment2(Counters&); // increment2.cpp: #include 'Counters.hpp' void increment2(Counters& c) { c.inc2(); }

そして// main.cpp: #include #include 'Counters.hpp' #include 'increment1.hpp' #include 'increment2.hpp' using namespace std; int main(int argc, char* argv[]) { Counters c; increment1(c); // 3 times increment1(c); increment1(c); increment2(c); // 4 times increment2(c); increment2(c); increment2(c); cout << 'c.get1(): ' << c.get1() << endl; // Should be 3 cout << 'c.get2(): ' << c.get2() << endl; // Should be 4 #ifndef NDEBUG // For debug builds cout << 'c.getDebugAllCounters(): ' << c.getDebugAllCounters() << endl; // Should be 3 + 4 = 7 #endif return 0; } Makefileのコンパイラフラグをカスタマイズできますのみ:

increment2.cpp

それでは、all: main.o increment1.o increment2.o g++ -o diff-flags main.o increment1.o increment2.o main.o: main.cpp increment1.hpp increment2.hpp Counters.hpp g++ -c -O2 main.cpp increment1.o: increment1.cpp Counters.hpp g++ -c $(CFLAGS) -O2 increment1.cpp increment2.o: increment2.cpp Counters.hpp g++ -c -O2 increment2.cpp clean: rm -f *.o diff-flags を定義せずに、すべてをデバッグモードでコンパイルしましょう。

NDEBUG

今すぐ実行:

$ CFLAGS='' make g++ -c -O2 main.cpp g++ -c -O2 increment1.cpp g++ -c -O2 increment2.cpp g++ -o diff-flags main.o increment1.o increment2.o

出力は期待どおりです。それでは、$ ./diff-flags c.get1(): 3 c.get2(): 4 c.getDebugAllCounters(): 7 を使用してファイルの1つだけをコンパイルしましょう。定義済み。これはリリースモードになり、何が起こるかを確認します。

NDEBUG

出力が期待どおりではありません。 $ make clean rm -f *.o diff-flags $ CFLAGS='-DNDEBUG' make g++ -c -O2 main.cpp g++ -c -DNDEBUG -O2 increment1.cpp g++ -c -O2 increment2.cpp g++ -o diff-flags main.o increment1.o increment2.o $ ./diff-flags c.get1(): 0 c.get2(): 4 c.getDebugAllCounters(): 7 関数は、2つのintメンバーフィールドしかないCountersクラスのリリースバージョンを見ました。したがって、最初のフィールドはincrement1であると考えてインクリメントし、m_counter1について何も知らないため、他には何もインクリメントしませんでした。フィールド。私はそれを言うm_debugAllCounters increment1のinc1メソッドが原因で、カウンターがインクリメントされましたはインラインであるため、Counterでインライン化されました関数本体、それから呼び出されません。 increment1のため、コンパイラはおそらくインライン化することを決定しました。最適化レベルフラグが使用されました。

Python関数は、通常の状況では、その値についてモジュール変数を参照できません。

つまり、-O2インクリメントされることはなく、m_counter1 m_debugAllCountersで誤ってインクリメントされました。そのため、increment1に0が表示されますしかし、m_counter1にはまだ7が表示されます。

多くの静的ライブラリにグループ化された大量のソースファイルがあるプロジェクトで作業しているときに、それらのライブラリの一部がm_debugAllCountersのデバッグオプションなしでコンパイルされ、他のライブラリがそれらのオプションでコンパイルされたことがありました。

おそらくある時点で、すべてのライブラリが同じフラグを使用していましたが、時間が経つにつれて、それらのフラグを考慮せずに新しいライブラリが追加されました(デフォルトのフラグではなく、手動で追加されていました)。 IDEを使用してコンパイルしたため、各ライブラリのフラグを確認するには、タブとウィンドウを掘り下げて、コンパイルモード(リリース、デバッグ、プロファイルなど)ごとに異なる(および複数の)フラグを設定する必要があったため、さらに困難でした。フラグが一貫していないことに注意してください。

これにより、まれに、1セットのフラグでコンパイルされたオブジェクトファイルがstd::vectorを渡すことが発生しました。そのベクターに対して特定の操作を実行する、異なるフラグのセットでコンパイルされたオブジェクトファイルに対して、アプリケーションがクラッシュしました。クラッシュはリリースバージョンで発生したと報告されており、デバッグバージョンでは発生しなかったため(少なくとも報告されたのと同じ状況では)、デバッグが容易ではなかったと想像してみてください。

デバッガーは、非常に最適化されたコードをデバッグしていたため、クレイジーなこともしました。クラッシュは正しくて些細なコードで起こっていました。

コンパイラはあなたが思っている以上のことをします

この記事では、C ++の基本的な言語構造のいくつかと、処理段階からリンク段階まで、コンパイラーがそれらをどのように処理するかについて学習しました。それがどのように機能するかを知ることは、プロセス全体を異なって見るのに役立ち、C ++開発で当たり前と思われるこれらのプロセスについての洞察を深めることができます。

3段階のコンパイルプロセスから、関数名のマングリング、さまざまな状況でのさまざまな関数シグネチャの生成まで、コンパイラは、コンパイルされたプログラミング言語としてC ++の能力を提供するために多くの作業を行います。

この記事の知識がC ++プロジェクトで役立つことを願っています。

関連: CおよびC ++言語を学ぶ方法:究極のリスト

SRVB暗号システムから始める

データサイエンスとデータベース

SRVB暗号システムから始める
プロジェクト管理会議2020の完全なリスト

プロジェクト管理会議2020の完全なリスト

アジャイル

人気の投稿
ElmでWebフロントエンドの信頼性を高める
ElmでWebフロントエンドの信頼性を高める
Googleスプレッドシートに移行する理由
Googleスプレッドシートに移行する理由
シニアRubyonRailsエンジニア
シニアRubyonRailsエンジニア
iOSユーザーインターフェイス:ストーリーボードとNIBとカスタムコード
iOSユーザーインターフェイス:ストーリーボードとNIBとカスタムコード
アニメーション製品の説明ビデオを作成するためのステップバイステップガイド
アニメーション製品の説明ビデオを作成するためのステップバイステップガイド
 
人生の1か月-暫定CFOの役割とベストプラクティス
人生の1か月-暫定CFOの役割とベストプラクティス
C ++のしくみ:コンパイルを理解する
C ++のしくみ:コンパイルを理解する
フォームと機能–トップワイヤーフレームツールのガイド
フォームと機能–トップワイヤーフレームツールのガイド
過去はまだ存在している–時代を超越したデザインの概要
過去はまだ存在している–時代を超越したデザインの概要
仕事をしながら旅行する方法:旅行エンジニアのサバイバルガイド
仕事をしながら旅行する方法:旅行エンジニアのサバイバルガイド
人気の投稿
  • c ++オブジェクトファイル
  • 良いAPIを作るもの
  • 給与を契約レートに変換する
  • モバイルデバイス向けのレスポンシブウェブデザイン
  • 支払い意思額は、需要の弾力性を測定する方法です。
カテゴリー
  • リモートの台頭
  • アジャイル
  • 財務プロセス
  • 収益と成長
  • © 2022 | 全著作権所有

    portaldacalheta.pt