Cでは、ヘッダーファイル内に関数の定義/実装を含めることはできません。ただし、C++では、ヘッダーファイル内に完全なメソッド実装を含めることができます。なぜ動作が異なるのですか?
Cでは、ヘッダーファイルで関数を定義すると、その関数は、そのヘッダーファイルを含むコンパイル済みの各モジュールに表示され、関数のパブリックシンボルがエクスポートされます。したがって、関数additupがheader.hで定義されており、foo.cとbar.cの両方にheader.hが含まれている場合、foo.oとbar.oの両方にadditupのコピーが含まれます。
これら2つのオブジェクトファイルをリンクすると、リンカはシンボルadditupが複数回定義されていることを確認し、それを許可しません。
関数を静的であると宣言すると、シンボルはエクスポートされません。オブジェクトファイルfoo.oとbar.oには、どちらも関数のコードの個別のコピーが含まれており、それらを使用することはできますが、リンカーは関数のコピーを表示できません。文句を言わない。もちろん、他のモジュールも関数を見ることができません。そして、あなたのプログラムは、同じ関数の2つの同一のコピーで肥大化します。
ヘッダーファイルで関数を宣言するだけで、それを定義せず、1つのモジュールでのみ定義すると、リンカーは関数の1つのコピーを参照し、プログラム内のすべてのモジュールがそれを参照できます。これを使って。そして、コンパイルされたプログラムには、関数のコピーが1つだけ含まれます。
したがって、あなたはcanの関数定義をCのヘッダーファイルに含めます。それは、スタイル、フォーム、およびあらゆる面で悪い考えです。
(「宣言」とは、本体なしの関数プロトタイプを提供することを意味します。「定義」とは、関数本体の実際のコードを提供することを意味します。これは標準のC用語です。)
CとC++はこの点でほとんど同じように動作します。ヘッダーにinline
関数を含めることができます。 C++では、本体がクラス定義内にあるメソッドはすべて暗黙的にinline
です。 Cで同じことを行う場合は、関数static inline
を宣言します。
ヘッダーファイルの概念について少し説明する必要があります。
コンパイラのコマンドラインでファイルを指定するか、 '#include'を実行します。ほとんどのコンパイラは、ソースファイルとして拡張子c、C、cpp、c ++などのコマンドファイルを受け入れます。ただし、これらには通常、ソースファイルに対する任意の拡張子の使用を有効にするコマンドラインオプションも含まれています。
通常、コマンドラインで指定されたファイルは「ソース」と呼ばれ、含まれているファイルは「ヘッダー」と呼ばれます。
プリプロセッサのステップは実際にそれらすべてを取り、コンパイラーにすべてを単一の大きなファイルのように見せます。ヘッダーやソースにあったものは、実際にはこの時点では関係ありません。通常、このステージの出力を表示できるコンパイラーのオプションがあります。
そのため、コンパイラーのコマンドラインで指定された各ファイルについて、巨大なファイルがコンパイラーに渡されます。これには、メモリを占有したり、他のファイルから参照されるシンボルを作成したりするコード/データがある場合があります。これらのそれぞれが「オブジェクト」イメージを生成します。リンカは、一緒にリンクされている3つ以上のオブジェクトファイルに同じシンボルが見つかった場合、「重複したシンボル」を与える可能性があります。おそらくこれが理由です。オブジェクトファイルにシンボルを作成できるヘッダーファイルにコードを配置することはお勧めしません。
通常、「インライン」はインライン化されますが、デバッグ時にはインライン化されない場合があります。では、なぜリンカは複数定義のエラーを出さないのでしょうか?シンプル...これらは「弱い」シンボルであり、すべてのオブジェクトからの弱いシンボルのすべてのデータ/コードが同じサイズとコンテンツである限り、リンクは1つのコピーを保持し、他のオブジェクトからコピーをドロップします。できます。
これはC99で行うことができます。inline
関数は別の場所で提供されることが保証されているため、関数がインライン化されなかった場合、その定義は宣言に変換されます(つまり、実装は破棄されます)。そしてもちろん、static
を使用できます。
C++標準引用符
C++ 17 N4659標準ドラフト 10.1.6「インライン指定子」は、メソッドが暗黙的にインラインであることを示しています。
4クラス定義内で定義された関数は、インライン関数です。
さらに下に行くと、すべての翻訳単位でインラインメソッドを定義できるだけでなく、mustを定義できることがわかります。
6インライン関数または変数は、それがodrで使用されるすべての翻訳単位で定義され、すべてのケースで正確に同じ定義を持つ必要があります(6.2)。
これは、12.2.1「メンバー関数」の注記でも明示的に言及されています。
1メンバー関数は、そのクラス定義で定義されている場合があります(11.4)。その場合、インラインメンバー関数です(10.1.6)[...]
3 [注:プログラム内には、非インラインメンバー関数の定義を1つだけ含めることができます。プログラム内に複数のインラインメンバー関数定義がある場合があります。 6.2と10.1.6を参照してください。 —エンドノート]
GCC 8.3実装
main.cpp
struct MyClass {
void myMethod() {}
};
int main() {
MyClass().myMethod();
}
シンボルをコンパイルして表示します。
g++ -c main.cpp
nm -C main.o
出力:
U _GLOBAL_OFFSET_TABLE_
0000000000000000 W MyClass::myMethod()
U __stack_chk_fail
0000000000000000 T main
次に、man nm
そのMyClass::myMethod
シンボルは、ELFオブジェクトファイルで弱いとマークされています。これは、複数のオブジェクトファイルで表示される可能性があることを意味します。
"W" "w"シンボルは、特に弱いオブジェクトシンボルとしてタグ付けされていない弱いシンボルです。弱い定義のシンボルが通常の定義のシンボルとリンクされている場合、通常の定義のシンボルがエラーなしで使用されます。弱い未定義のシンボルがリンクされていて、そのシンボルが定義されていない場合、シンボルの値はシステム固有の方法でエラーなしに決定されます。一部のシステムでは、大文字はデフォルト値が指定されていることを示します。