web-dev-qa-db-ja.com

静的データメンバーを(Javaとは異なり)C ++でクラスの外部で個別に定義する必要があるのはなぜですか?

class A {
  static int foo () {} // ok
  static int x; // <--- needed to be defined separately in .cpp file
};

A::xを.cppファイル(またはテンプレート用の同じファイル)で個別に定義する必要がないと思います。 A::xを同時に宣言および定義できないのはなぜですか?

歴史的な理由で禁止されていませんか?

私の主な質問は、staticデータメンバーが同時に宣言/定義された場合、すべての機能に影響しますか(Javaと同じ)?

41
iammilind

あなたが検討した制限はセマンティクス(初期化が同じファイルで定義されている場合、何か変更が必要なのはなぜですか)ではなく、下位互換性のために簡単に変更できないC++コンパイルモデルに関係があると思います複雑すぎる(新しいコンパイルモデルと既存のコンパイルモデルを同時にサポートする)か、または既存のコードをコンパイルできません(新しいコンパイルモデルを導入して既存のものを削除する)。

C++コンパイルモデルはCのコンパイルモデルに由来します。このモデルでは、(ヘッダー)ファイルを含めることにより、宣言をソースファイルにインポートします。このようにして、コンパイラーは、含まれているすべてのファイルと、それらのファイルから含まれているすべてのファイルを含む、1つの大きなソースファイルを再帰的に参照します。これにはIMOの大きな利点が1つあります。つまり、コンパイラーの実装が容易になります。もちろん、インクルードされたファイル、つまり宣言と定義の両方に何でも書き込むことができます。ヘッダーファイルに宣言を置き、.cファイルまたは.cppファイルに定義を置くことは、良い習慣にすぎません。

一方、であるグローバルシンボルの宣言をインポートする場合、コンパイラが非常によく知っているコンパイルモデルを持つことは可能です。 別のモジュールで定義されている、または定義のコンパイル現在のモジュールによって提供されるグローバルシンボル。後者の場合のみ、コンパイラーはこのシンボル(変数など)を現在のオブジェクトファイルに配置する必要があります。

たとえば、 GNU Pascal では、次のようにユニットaをファイルa.pasに書き込むことができます。

unit a;

interface

var MyStaticVariable: Integer;

implementation

begin
  MyStaticVariable := 0
end.

ここで、グローバル変数は同じソースファイルで宣言および初期化されます。

次に、aをインポートしてグローバル変数MyStaticVariableを使用するさまざまなユニットを使用できます。 aユニットb(b.pas):

unit b;

interface

uses a;

procedure PrintB;

implementation

procedure PrintB;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

ユニットc(c.pas):

unit c;

interface

uses a;

procedure PrintC;

implementation

procedure PrintC;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

最後に、メインプログラムm.pasでユニットbおよびcを使用できます。

program M;

uses b, c;

begin
  PrintB;
  PrintC;
  PrintB
end.

これらのファイルは個別にコンパイルできます。

$ gpc -c a.pas
$ gpc -c b.pas
$ gpc -c c.pas
$ gpc -c m.pas

次に、実行可能ファイルを生成します。

$ gpc -o m m.o a.o b.o c.o

そしてそれを実行します:

$ ./m
1
2
3

ここでのトリックは、コンパイラがプログラムモジュールでusesディレクティブを検出した場合(たとえば、b.pasでaを使用)、対応するものが含まれないことです。 .pasファイル、ただし.gpiファイル、つまり事前にコンパイルされたインターフェイスファイルを探します( ドキュメント を参照)。これらの.gpiファイルは、各モジュールのコンパイル時に.oファイルとともにコンパイラーによって生成されます。したがって、グローバルシンボルMyStaticVariableは、オブジェクトファイルa.oで一度だけ定義されます。

Javaも同様に機能します。コンパイラーがクラスAをクラスBにインポートすると、クラスファイルでAが検索され、A.Javaファイルは必要ありません。したがって、クラスAのすべての定義と初期化を1つのソースファイルに入れることができます。

C++に戻ると、C++で別のファイルに静的データメンバーを定義する必要がある理由は、コンパイラーが使用するリンカーやその他のツールによって課される制限よりも、C++コンパイルモデルに関連しています。 C++では、一部のシンボルをインポートすると、現在のコンパイル単位の一部として宣言を構築することになります。テンプレートのコンパイル方法のため、これは特に重要です。しかし、これはインクルードファイルでグローバルシンボル(関数、変数、メソッド、静的データメンバー)を定義できない、または定義してはならないことを意味します。そうでない場合、これらのシンボルはコンパイル済みオブジェクトファイルで複数定義される可能性があります。

16
Giorgio

静的メンバーはクラスのすべてのインスタンス間で共有されるため、1つの場所でのみ定義する必要があります。実際、これらはいくつかのアクセス制限があるグローバル変数です。

それらをヘッダーで定義しようとすると、それらはそのヘッダーを含むすべてのモジュールで定義され、重複した定義がすべて検出されるため、リンク中にエラーが発生します。

はい、これは少なくとも部分的にcfrontに由来する歴史的な問題です。ある種の隠された「static_members_of_everything.cpp」を作成し、それにリンクするコンパイラーを作成できます。ただし、下位互換性が失われるため、そうすることによる実質的なメリットはありません。

43
mjfgates

C++とJavaの間には大きな違いがあります。

Javaは、すべてを独自のランタイム環境に作成する独自の仮想マシン上で動作します。定義が2回以上見られる場合、ランタイム環境が最終的に認識している同じオブジェクトに作用します。

C++には「究極のナレッジオーナー」はありません。C++、C、Fortran Pascalなどはすべて、ソースコード(CPPファイル)から中間形式(OBJファイル、または「.o」ファイル)への「トランスレーター」です。 OS)ステートメントが機械語命令に変換され、名前がシンボルテーブルによって仲介される間接アドレスになります。

プログラムはコンパイラーではなく、別のプログラム(「リンカー」)によって作成されます。このプログラムは、シンボルに向かうすべてのアドレスを、それらのアドレスに向けて再ポイントすることにより、すべてのOBJを結合します(元の言語に関係なく)効果的な定義。

リンカが機能する方法では、定義(変数の物理スペースを作成するもの)は一意である必要があります。

C++自体はリンクせず、リンカーはC++仕様では発行されないことに注意してください。リンカーは、OSモジュールの構築方法(通常はCおよびASMで)が原因で存在します。 C++は、それをそのまま使用する必要があります。

現在:ヘッダーファイルは、いくつかのCPPファイルに「貼り付けられる」ものです。すべてのCPPファイルは、他のすべてのファイルとは無関係に翻訳されます。異なるCPPファイルを変換するコンパイラは、すべてのsame定義を受け取って、定義されたオブジェクトの "creation code"をすべての結果のOBJに配置します。

コンパイラーは、これらのすべてのOBJが一緒に使用されて単一のプログラムを形成するのか、または別々に使用して異なる独立したプログラムを形成するのかを知りません(そして知ることはありません)。

リンカは、定義が存在する方法と理由、およびそれらがどこから来たのかを知りません(C++についてさえ知りません。すべての「静的言語」は、リンクする定義と参照を生成できます)。与えられた結果のアドレスで「定義された」与えられた「シンボル」への参照があることを知っているだけです。

特定のシンボルに対して複数の定義がある場合(定義と参照を混同しないでください)、リンカはそれらをどうするかについて(言語に依存しない)知識がありません。

これは、いくつかの都市を結合して大きな町を形成するようなものです。「Time square」が2つあり、外部から「Time square "、あなたはそれらを送信する正確な場所で純粋な技術的根拠(それらの名前を割り当て、それらを管理するために責任を負うpoliticsに関する知識なし)を決定することはできません。

6

これは、オブジェクトファイルとリンケージモデルが複数のオブジェクトファイルからの複数の定義のマージをサポートしていない環境で、C++言語を実装可能な状態に保つためと考えられます。

クラス宣言(正当な理由から宣言と呼ばれる)は、複数の翻訳単位に取り込まれます。宣言に静的変数の定義が含まれている場合、複数の翻訳単位で複数の定義が作成されることになります(これらの名前には外部リンケージがあることに注意してください)。

そのような状況は可能ですが、リンカが文句を言わずに複数の定義を処理する必要があります。

(これは、シンボルの種類または配置されているセクションの種類に従って実行できない場合を除き、1つの定義ルールと競合することに注意してください。)

6
Kaz

それ以外の場合、コンパイラーは変数を配置する場所がわからないため、これは必須です。各cppファイルは個別にコンパイルされ、他については認識しません。リンカは変数や関数などを解決します。vtableと静的メンバーの違いは個人的にはわかりません(vtableが定義されているファイルを選択する必要はありません)。

私はたいていコンパイラー作成者がそのように実装する方が簡単だと思います。クラス/構造体の外に静的変数が存在します。これは、一貫性の理由か、コンパイラの作成者にとって「実装が容易」であるためか、標準でその制限を定義したためです。

5
user2528

理由は分かったと思います。 static変数を別のスペースで定義すると、任意の値に初期化できます。初期化されていない場合、デフォルトで0になります。

C++ 11より前は、クラス内初期化はC++では許可されていませんでした。したがって、1つできませんのように記述します。

struct X
{
  static int i = 4;
};

したがって、変数を初期化するには、クラスの外部で次のように記述する必要があります。

struct X
{
  static int i;
};
int X::i = 4;

他の回答でも説明したように、int X::iがグローバルになり、多くのファイルでグローバルを宣言すると、複数のシンボルリンクエラーが発生します。

したがって、別の翻訳単位内でクラスstatic変数を宣言する必要があります。ただし、それでも、次の方法では、複数のシンボルを作成しないようにコンパイラーに指示する必要があると主張できます。

static int X::i = 4;
^^^^^^
2
iammilind

A :: xは単なるグローバル変数ですが、Aに名前空間が設定されており、アクセス制限があります。

他のグローバル変数と同様に、誰かがそれを宣言する必要があります。これは、残りのAコードを含むプロジェクトに静的にリンクされているプロジェクトで行うこともできます。

私はこれらをすべて悪いデザインと呼びますが、この方法で活用できる機能がいくつかあります。

  1. コンストラクターの呼び出し順序... intでは重要ではありませんが、他の静的変数またはグローバル変数にアクセスする可能性のあるより複雑なメンバーでは、重要になる可能性があります。

  2. 静的初期化子-A :: xの初期化先をクライアントに決定させることができます。

  3. c ++およびcでは、ポインタを介してメモリに完全にアクセスできるため、変数の物理的な場所は重要です。変数がリンクオブジェクトのどこに配置されているかに基づいて、悪質なことが利用できます。

これらがこのような「理由」が生じたのではないかと思います。おそらく、CがC++に変わっただけの進化であり、後方互換性の問題が原因で言語を変更できなくなっています。

0
James Podesta