C ++の内部および外部リンク

すべての人に良い一日を!



「C ++ Developer」コースの一環として用意された興味深い記事の翻訳を提供します。 リスナーだけでなく、あなたにとっても便利で面白いものになることを願っています。



行こう



内部および外部コミュニケーションという用語に出会ったことはありますか? externキーワードの用途、または静的な宣言がグローバルスコープにどのように影響するかを知りたいですか? 次に、この記事はあなたのためです。



一言で言えば



翻訳単位(.c / .cpp)とそのすべてのヘッダーファイル(.h / .hpp)は、翻訳単位に含まれています。 オブジェクトまたは関数が翻訳単位内に内部バインディングを持っている場合、このシンボルはこの翻訳単位内でのみリンカーに表示されます。 オブジェクトまたは関数に外部リンケージがある場合、リンカーは他の翻訳単位を処理するときにそれを見ることができます。 グローバル名前空間でstaticキーワードを使用すると、文字の内部バインディングが提供されます。 externキーワードは、外部バインディングを提供します。

デフォルトのコンパイラは、文字に次のバインディングを与えます。









基本



最初に、バインディングについて説明するために必要な2つの簡単な概念について話しましょう。





また、名前に注意してください。たとえば、変数または関数(またはクラス/構造体を使用しますが、それらに焦点を合わせません)でリンカが動作する「コードエンティティ」については、「シンボル」の概念を使用します。



告知対。 定義



宣言とシンボル定義の違いについて簡単に説明します:アナウンス(または宣言)は、特定のシンボルの存在についてコンパイラーに通知し、正確なメモリアドレスまたはシンボルストレージを必要としない場合にこのシンボルへのアクセスを許可します。 この定義は、関数の本体に含まれているもの、または変数が割り当てる必要があるメモリの量をコンパイラに伝えます。



状況によっては、たとえばクラスデータ要素にリンクまたは値の型がある(つまり、リンクではなく、ポインターでもない)場合、コンパイラーにとって宣言だけでは不十分です。 同時に、宣言された(ただし未定義の)型へのポインターは、それが指す型に関係なく、一定量のメモリ(64ビットシステムでは8バイトなど)を必要とするため、許可されます。 このポインターで値を取得するには、定義が必要です。 また、関数を宣言するには、すべてのパラメーター(値、参照、またはポインターのいずれによって取得されるか)および戻り値の型を宣言する必要があります(定義はしません)。 戻り値とパラメーターのタイプの判別は、関数を定義するためにのみ必要です。



機能



関数の定義と宣言の違いは非常に明白です。



int f(); //  int f() { return 42; } // 
      
      





変数



変数では、少し異なります。 宣言と定義は通常共有されません。 主なものは次のとおりです。



 int x;
      
      





x



宣言するだけでなく、定義します。 これは、デフォルトのコンストラクターintの呼び出しによるものです。 (C ++では、Javaとは異なり、単純型(intなど)のコンストラクタはデフォルトで値を0に初期化しません。上記の例では、xはコンパイラによって割り当てられたメモリアドレスにあるガベージに等しくなります)。



ただし、 extern



キーワードを使用して、変数宣言とその定義を明示的に分離できます。



 extern int x; //  int x = 42; // 
      
      





ただし、 extern



を初期化して宣言に追加すると、式は定義に変わり、 extern



キーワードは使用できなくなります。



 extern int x = 5; //   ,   int x = 5;
      
      





広告プレビュー



C ++には、文字を事前宣言するという概念があります。 これは、その定義を必要としない状況で使用するために、シンボルのタイプと名前を宣言することを意味します。 したがって、明らかに必要な場合を除き、キャラクター(通常はヘッダーファイル)の完全な定義を含める必要はありません。 したがって、定義を含むファイルへの依存を減らします。 主な利点は、定義を使用してファイルを変更するときに、このシンボルを事前に宣言するファイルが再コンパイルを必要としないことです(つまり、それを含む他のすべてのファイル)。







値によるClass



型のオブジェクトをとるfの関数宣言(プロトタイプと呼ばれる)があるとします。



 // file.hpp void f(Class object);
      
      





Class



定義をすぐに含めることは単純です。 しかし、 f



を宣言したばかりなので、コンパイラにClass



宣言を与えるだけで十分です。 したがって、コンパイラーはプロトタイプによって関数を認識でき、class.hppなどのClass



の定義を含むファイルに対するfile.hppの依存関係を取り除くことができます。



 // file.hpp class Class; void f(Class object);
      
      





file.hppが他の100個のファイルに含まれているとします。 そして、class.hppのClassの定義を変更したとしましょう。 class.hppをfile.hppに追加する場合、file.hppおよびそれを含む100個のファイルすべてを再コンパイルする必要があります。 Classの事前宣言のおかげで、再コンパイルが必要なファイルはclass.hppとfile.hppだけです(そこにfが定義されていると仮定)。



使用頻度



宣言と定義の重要な違いは、シンボルは何度も宣言できますが、一度しか定義できないことです。 そのため、関数やクラスを何度でも事前宣言できますが、定義は1つだけです。 これはRule of One Definitionと呼ばれます。 C ++では、以下が機能します。



 int f(); int f(); int f(); int f(); int f(); int f(); int f() { return 5; }
      
      





そして、これは機能しません:



 int f() { return 6; } int f() { return 9; }
      
      





放送ユニット



プログラマは通常、ヘッダーファイルと実装ファイルを使用します。 ただし、コンパイラではなく、翻訳単位(略して翻訳単位-TU)で動作します。これはコンパイル単位とも呼ばれます。 そのようなユニットの定義は非常に単純です-事前処理後にコンパイラに転送されるファイル。 正確には、これは、 #ifdef



および#ifndef



式に依存するソースコード、およびすべての#include



ファイルのコピーアンドペーストを含む、拡張マクロプリプロセッサの作業の結果として取得されるファイルです。



以下のファイルが利用可能です。



header.hpp:



 #ifndef HEADER_HPP #define HEADER_HPP #define VALUE 5 #ifndef VALUE struct Foo { private: int ryan; }; #endif int strlen(const char* string); #endif /* HEADER_HPP */
      
      





program.cpp:



 #include "header.hpp" int strlen(const char* string) { int length = 0; while(string[length]) ++length; return length + VALUE; }
      
      





プリプロセッサは次の翻訳単位を生成し、コンパイラに渡します。



 int strlen(const char* string); int strlen(const char* string) { int length = 0; while(string[length]) ++length; return length + 5; }
      
      





コミュニケーションズ



基本について話し合った後、関係を始めることができます。 一般に、通信は、ファイルを処理するときのリンカーの文字の可視性です。 通信は、外部または内部のいずれかです。



外部コミュニケーション



シンボル(変数または関数)に外部接続がある場合、他のファイルからリンカーに表示されます。つまり、「グローバルに」表示され、すべての変換ユニットにアクセスできます。 これは、1つの翻訳単位の特定の場所、通常は実装ファイル(.c / .cpp)でそのようなシンボルを定義する必要があることを意味します。 シンボルの宣言と同時にシンボルを同時に決定しようとした場合、または宣言用のファイルに定義を配置した場合、リンカーを怒らせる危険があります。 ファイルを複数の実装ファイルに追加しようとすると、複数の翻訳単位に定義が追加されます—リンカが泣きます。



CおよびC ++のexternキーワードは、(明示的に)文字に外部リンクがあることを宣言します。



 extern int x; extern void f(const std::string& argument);
      
      





両方のキャラクターに外部接続があります。 constグローバル変数にはデフォルトで内部バインディングがあり、非constグローバル変数には外部バインディングがあることに注意してください。 これは、int x; -extern int xと同じ、そうですか? そうでもない。 int x; 実際にはextern int x {}に類似しています。 (int x以来、最も不快な構文解析(最も厄介な構文解析)を回避するために、ユニバーサル/ブラケット初期化構文を使用); 宣言するだけでなく、xも定義します。 したがって、int xにexternを追加しないでください。 globallyは、外部宣言するときに変数を定義するのと同じくらい悪いです:



 int x; //   ,   extern int x{}; //      . extern int x; //      ,  
      
      





悪い例



file.hppで外部リンクを使用して関数f



を宣言し、そこで定義します。



 // file.hpp #ifndef FILE_HPP #define FILE_HPP extern int f(int x); /* ... */ int f(int) { return x + 1; } /* ... */ #endif /* FILE_HPP */
      
      





すべての関数は明示的にexternであるため、ここでexternを追加する必要はないことに注意してください。 宣言と定義の分離も必要ありません。 次のように書き直してみましょう。



 // file.hpp #ifndef FILE_HPP #define FILE_HPP int f(int) { return x + 1; } #endif /* FILE_HPP */
      
      





このようなコードは、この記事を読む前、またはアルコールや重い物質(シナモンロールなど)の影響下で読んだ後に書くことができます。



なぜこれが価値がないのか見てみましょう。 これで、a.cppとb.cppの2つの実装ファイルができました。両方ともfile.hppに含まれています。



 // a.cpp #include "file.hpp" /* ... */
      
      







 // b.cpp #include "file.hpp" /* ... */
      
      





コンパイラを動作させて、上記の2つの実装ファイルに対して2つの翻訳単位を生成します( #include



文字通りコピー/貼り付けを意味することに#include



):



 // TU A, from a.cpp int f(int) { return x + 1; } /* ... */
      
      





 // TU B, from b.cpp int f(int) { return x + 1; } /* ... */
      
      





この時点で、リンカーが介入します(コンパイル後にバインディングが発生します)。 リンカは文字f



を受け取り、定義を探します。 今日彼は幸運であり、彼は2人も見つけました! 1つは翻訳単位Aに、もう1つはBにあります。リンカーは幸福にフリーズし、次のように表示されます。



 duplicate symbol __Z1fv in: /path/to/ao /path/to/bo
      
      





リンカは、1つのf



文字に対して2つの定義を見つけます。 f



には外部バインディングがあるため、AとBの両方を処理するときにリンカーに表示されます。明らかに、これは1つの定義の規則に違反し、エラーを引き起こします。 より正確には、これにより重複シンボルエラーが発生します。これは、シンボルを宣言するときに発生するが、定義を忘れた未定義のシンボルエラーと同じです。



使用する



外部変数を宣言する標準的な例は、グローバル変数です。 自炊ケーキに取り組んでいるとします。 おそらく、プログラムのさまざまな部分で利用できるグローバルなケーキ関連の変数があります。 ケーキの中の食用回路のクロック周波数を考えてみましょう。 この値は、当然すべてのチョコレートエレクトロニクスの同期動作のために、さまざまな部分で必要です。 そのようなグローバル変数を宣言する(邪悪な)Cの方法は、マクロとしてです。



 #define CLK 1000000
      
      





マクロにうんざりしているC ++プログラマは、実際のコードをより適切に記述できます。 たとえば、これ:



 // global.hpp namespace Global { extern unsigned int clock_rate; } // global.cpp namespace Global { unsigned int clock_rate = 1000000; }
      
      





(最新のC ++プログラマは、分離リテラルを使用したいと思うでしょう:unsigned int clock_rate = 1'000'000;)



インターホン



シンボルに内部接続がある場合、現在の翻訳単位内でのみ表示されます。 可視性とプライベートなどのアクセス権を混同しないでください。 可視性とは、リンカがこのシンボルを使用できるのは、シンボルが宣言された翻訳単位を処理するときだけであり、後で(外部通信のあるシンボルの場合のように)使用できないことです。 実際には、これは、ヘッダーファイルで内部リンクを持つシンボルを宣言するときに、このファイルを含む各ブロードキャストユニットがこのシンボルの一意のコピーを受け取ることを意味します。 各翻訳単位でそのような各シンボルを事前定義したかのように。 オブジェクトの場合、これはコンパイラーが各翻訳単位に文字通り完全に新しい一意のコピーを割り当てることを意味し、これは明らかに高いメモリコストにつながる可能性があります。



相互接続されたシンボルを宣言するには、CおよびC ++にstaticキーワードが存在します。 この使用法は、クラスおよび関数(または一般に、任意のブロック)での静的の使用とは異なります。







以下に例を示します。



header.hpp:



 static int variable = 42;
      
      





file1.hpp:



 void function1();
      
      





file2.hpp:



 void function2();
      
      





file1.cpp:



 #include "header.hpp" void function1() { variable = 10; }
      
      







file2.cpp:



 #include "header.hpp" void function2() { variable = 123; }
      
      





main.cpp:



 #include "header.hpp" #include "file1.hpp" #include "file2.hpp" #include <iostream> auto main() -> int { function1(); function2(); std::cout << variable << std::endl; }
      
      





header.hppを含む各翻訳単位は、その内部接続により、変数の一意のコピーを取得します。 3つの翻訳単位があります。



  1. file1.cpp
  2. file2.cpp
  3. main.cpp


function1が呼び出されると、file1.cpp変数のコピーは値10を取得します。function2が呼び出されると、file2.cpp変数のコピーは値123を取得します。ただし、main.cppで返される値は変わらず、42のままです。



匿名の名前空間



C ++では、1つ以上の内部リンク文字を宣言する別の方法があります:匿名の名前空間。 このようなスペースは、その中で宣言された文字が現在の翻訳単位でのみ表示されることを保証します。 基本的に、これは複数の静的文字を宣言する方法にすぎません。 しばらくの間、静的キーワードを使用して内部リンク文字を宣言することは、匿名の名前空間を支持して放棄されました。 ただし、内部通信で1つの変数または関数を宣言するのが便利なため、彼らは再び使用を開始しました。 その他のいくつかの小さな違いについては、ここでは説明しません。



いずれにせよ、これは次のとおりです。



 namespace { int variable = 0; }
      
      





(ほぼ)同じことを行います:



 static int variable = 0;
      
      





使用する



それでは、どのような場合に内部接続を使用しますか? オブジェクトにそれらを使用するのは悪い考えです。 各翻訳単位のコピーのため、大きなオブジェクトのメモリ消費は非常に高くなる可能性があります。 しかし、基本的には、奇妙で予測不可能な動作を引き起こすだけです。 シングルトン(1つのインスタンスのみのインスタンスを作成するクラス)があり、突然「シングルトン」の複数のインスタンス(各翻訳単位に1つ)が表示されることを想像してください。



ただし、内部通信を使用して、翻訳ユニットをローカルヘルパー関数のグローバルエリアから隠すことができます。 file1.cppで使用するfooヘルパー関数がfile1.hppにあるとします。 同時に、file2.cppで使用されるfile2.hppのfoo関数があります。 最初のfooと2番目のfooは互いに異なりますが、他の名前を見つけることはできません。 したがって、静的に宣言できます。 file1.hppとfile2.hppの両方を同じ翻訳単位に追加しない場合、これによりfooが互いに非表示になります。 これを行わないと、暗黙的に外部接続が行われ、最初のfooの定義が2番目のfooの定義に遭遇し、1つの定義の規則に違反することに関するリンカーエラーが発生します。



終わり



いつでもここにコメントや質問を残したり、 公開日に私たちを訪問することができます



All Articles