20

数分前にこの質問を見た後、私は言語設計者が個人データの間接的な変更を許可するのになぜそれを許可するのか疑問に思いました。例として

 class TestClass {
   private:
    int cc;
   public:
     TestClass(int i) : cc(i) {};
 };

 TestClass cc(5);
 int* pp = (int*)&cc;
 *pp = 70;             // private member has been modified

上記のコードをテストしましたが、実際にプライベートデータが変更されています。なぜこれが起こることが許されているのか、それとも単に言語の見落としであるのかについての説明はありますか?プライベートデータメンバーの使用を直接損なうようです。

4

6 に答える 6

34

なぜなら、Bjarneが言うように、C ++はマキャヴェリではなく、マーフィーから保護するように設計されているからです。

言い換えれば、それは事故からあなたを守ることになっています-しかし、あなたがそれを破壊するために何か仕事に行く場合(キャストを使用するなど)、それはあなたを止めようとさえしません。

私がそれについて考えるとき、私は幾分異なるアナロジーを念頭に置いています:それは浴室のドアの錠のようなものです。おそらく今はそこに足を踏み入れたくないという警告が表示されますが、必要に応じて外側からドアのロックを解除するのは簡単です。

編集:@Xeoが議論する質問に関して、なぜ標準が「すべてのパブリックアクセス制御を持っている」のではなく「同じアクセス制御を持っている」と言っているのかについて、答えは長くて少し曲がりくねっています。

最初に戻って、次のような構造体について考えてみましょう。

struct X {
    int a;
    int b;
};

Cには、このような構造体に対して常にいくつかのルールがありました。1つは、構造体のインスタンスでは、構造体自体のアドレスがのアドレスと等しくaなければならないため、構造体へのポインターをへのポインターにキャストし、明確に定義された結果intでアクセスできることです。aもう1つは、メンバーを構造体で定義されているのと同じ順序でメモリ内に配置する必要があることです(ただし、コンパイラはそれらの間にパディングを自由に挿入できます)。

C ++の場合、特に既存のC構造体の場合、それを維持する意図がありました。同時に、コンパイラーが実行時に強制(および)したい場合は、それを(合理的に効率的に)簡単に実行できるはずであるという明らかな意図がありました。privateprotected

したがって、次のようなものが与えられます:

struct Y { 
    int a;
    int b;
private:
    int c;
    int d;
public:
    int e;

    // code to use `c` and `d` goes here.
};

コンパイラは、およびに関してCと同じルールを維持する必要がY.aありY.bます。同時に、実行時にアクセスを強制する場合は、すべてのパブリック変数をメモリ内で一緒に移動する必要があるため、レイアウトは次のようになります。

struct Z { 
    int a;
    int b;
    int e;
private:
    int c;
    int d;
    // code to use `c` and `d` goes here.
};

次に、実行時に実行する場合、基本的に次のようなことができます。if (offset > 3 * sizeof(int)) access_violation();

私の知る限り、誰もこれを行ったことがなく、標準の残りの部分が実際にそれを許可するかどうかはわかりませんが、少なくともその線に沿ってアイデアの半分の形の芽​​があったようです。

これらの両方を強制するために、C ++ 98はY::aY::bメモリ内でその順序である必要がY::aあり、構造体の先頭にある必要がありました(つまり、Cのようなルール)。ただし、アクセス指定子が介在しているため、相互に関連している必要はY::cありY::eません。言い換えると、それらの間にアクセス指定子なしで定義されたすべての連続する変数はグループ化され、コンパイラーはそれらのグループを自由に再配置できました(ただし、最初のグループを保持する必要がありました)。

ルールの書き方に別の小さな問題があると指摘するまでは、それで問題ありませんでした。私が次のようなコードを書いた場合:

struct A { 
    int a;
public:
    int b;
public:
    int c;
public:
    int d;
};

...あなたは少しの自己矛盾に終わった。一方では、これはまだ正式にPOD構造体であったため、Cのようなルールが適用されるはずでしたが、メンバー間に(明らかに無意味な)アクセス指定子があったため、コンパイラーにメンバーを再配置する許可も与えました。彼らが意図したCのようなルールを破る。

それを解決するために、彼らは標準を少し言い換えて、メンバー間にアクセス指定子があるかどうかではなく、すべてのメンバーが同じアクセス権を持っていることについて話しました。はい、彼らは規則が公のメンバーにのみ適用されるとただ布告したかもしれません、しかし誰もそれから得られるものを見なかったように見えます。これがかなり長い間使用されていた多くのコードで既存の標準を変更していたことを考えると、彼らが行うことができる最小の変更を選択したので、それでも問題は解決します。

于 2012-08-23T14:13:21.740 に答える
15

同じことができるCとの後方互換性のため。


疑問に思っているすべての人にとって、これがUBではなく、実際に標準で許可されている理由は次のとおりです。

まず、TestClass標準レイアウトクラス§9 [class] p7)です。

標準レイアウトクラスは、次のようなクラスです。

  • タイプnon-standard-layoutクラス(またはそのようなタイプの配列)または参照の非静的データメンバーがありません。//OK:非静的データメンバーはタイプ'int'です。
  • 仮想関数(10.3)と仮想基本クラス(10.1)がない、// OK
  • すべての非静的データメンバーに対して同じアクセス制御(条項11)があります。//OK、すべての非静的データメンバー(1)は「プライベート」です。
  • 非標準レイアウトの基本クラスはありません、// OK、基本クラスはありません
  • 最も派生したクラスに非静的データメンバーがなく、非静的データメンバーを持つ基本クラスが多くても1つないか、非静的データメンバーを持つ基本クラスがなく、// OK、基本クラスが再びない
  • 最初の非静的データメンバーと同じタイプの基本クラスはありません。// OK、基本クラスはもうありません

そしてそれで、あなたはreinterpret_castその最初のメンバーのタイプにクラスを許可することができます(§9.2 [class.mem] p20):

を使用して適切に変換された標準レイアウト構造体オブジェクトへのポインタは、reinterpret_castその最初のメンバー(または、そのメンバーがビットフィールドの場合は、それが存在するユニット)を指し、その逆も同様です。

あなたの場合、Cスタイルの(int*)キャストはreinterpret_cast§5.4 [expr.cast] p4)に解決されます。

于 2012-08-23T14:09:42.663 に答える
2

正当な理由は、Cとの互換性を許可することですが、C++層でのアクセスの安全性を高めることです。

検討:

struct S {
#ifdef __cplusplus
private:
#endif // __cplusplus
    int i, j;
#ifdef __cplusplus
public:
    int get_i() const { return i; }
    int get_j() const { return j; }
#endif // __cplusplus
};

C-visibleSとC++-visibleSレイアウト互換であることを要求することによりS、言語の境界を越えて使用でき、C++側のアクセスの安全性が向上します。アクセスのreinterpret_cast安全性の破壊は、残念ながら必要な結果です。

余談ですが、すべてのメンバーが同じアクセス制御を持つことの制限は、実装が異なるアクセス制御を持つメンバーに対してメンバーを再配置することを許可されているためです。おそらく、いくつかの実装では、整理のために、同じアクセス制御を持つメンバーをまとめています。パディングを減らすためにも使用できますが、それを行うコンパイラはわかりません。

于 2012-08-23T14:22:18.960 に答える
1

試してみた場合、コンパイラーはエラーを出していたint *pp = &cc.ccでしょう。コンパイラーは、プライベート・メンバーにアクセスできないとあなたに言ったでしょう。

あなたのコードでは、ccのアドレスをintへのポインタとして再解釈しています。あなたはそれをCスタイルの方法で書いた、C++スタイルの方法はそうだっただろうint* pp = reinterpret_cast<int*>(&cc);。reinterpret_castは常に、関連のない2つのポインター間でキャストを行っていることを警告します。そのような場合、あなたはあなたが正しくやっていることを確認しなければなりません。基礎となるメモリ(レイアウト)を知っている必要があります。これは頻繁に必要になるため、コンパイラーはユーザーがそうすることを妨げません。

キャストをするとき、あなたはクラスについてのすべての知識を捨てます。今後、コンパイラはintポインタのみを認識します。もちろん、ポインタが指すメモリにアクセスできます。あなたの場合、あなたのプラットフォームでは、コンパイラはたまたまccをTestClassオブジェクトの最初のnバイトに入れたので、TestClassポインタもccメンバーを指しています。

于 2012-08-23T14:19:20.000 に答える
1

reinterpret_cast(そしてCスタイルのキャストはよりも強力です)の全体的な目的はreinterpret_cast、安全対策の周りに逃げ道を提供することです。

于 2012-08-23T14:26:16.773 に答える
0

これは、クラスがメモリ内にあるメモリを操作しているためです。あなたの場合、たまたまこのメモリ位置にプライベートメンバーを保存するので、それを変更します。オブジェクトがメモリにどのように格納されるかがわかったので、これを行うのはあまり良い考えではありません。

于 2012-08-23T13:59:44.207 に答える