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 总是有一些规则。一种是在结构的实例中,结构本身的地址必须等于 的地址a,因此您可以将指向该结构的指针转换为指向 的指针int,并a以明确定义的结果进行访问。另一个是成员在内存中的排列顺序必须与它们在结构中定义的顺序相同(尽管编译器可以自由地在它们之间插入填充)。

对于 C++,有维护这一点的意图,尤其是对于现有的 C 结构。同时,有一个明显的意图是,如果编译器想要在运行时强制执行private(and protected),这样做应该很容易(相当有效)。

因此,给定以下内容:

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::a并且Y::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):

标准布局类是这样的类:

  • 没有非标准布局类(或此类类型的数组)或引用类型的非静态数据成员,// OK:非静态数据成员的类型为“int”
  • 没有虚函数 (10.3) 也没有虚基类 (10.1), // OK
  • 对所有非静态数据成员具有相同的访问控制(第 11 条),// OK,所有非静态数据成员 (1) 都是“私有的”
  • has no non-standard-layout base classes, // 好的,没有基类
  • 要么在派生最多的类中没有非静态数据成员,并且最多有一个具有非静态数据成员的基类,要么没有具有非静态数据成员的基类,并且// OK,再次没有基类
  • 没有与第一个非静态数据成员相同类型的基类。// 好的,又没有基类了

这样一来,您就可以允许reinterpret_cast该类为其第一个成员 ( §9.2 [class.mem] p20) 的类型:

指向标准布局结构对象的指针,使用 a 进行适当转换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++-visible布局兼容S可以跨语言边界使用 C++ 端具有更高的访问安全性。访问安全颠覆是一个不幸但必要的必然结果。Sreinterpret_cast

顺便说一句,对所有成员具有相同访问控制的限制是因为允许实现相对于具有不同访问控制的成员重新排列成员。据推测,为了整洁,某些实现将具有相同访问控制的成员放在一起;它也可以用来减少填充,尽管我不知道有任何编译器可以做到这一点。

于 2012-08-23T14:22:18.960 回答
1

如果您尝试过int *pp = &cc.cc,编译器会给您一个错误,编译器会告诉您您无法访问私有成员。

在您的代码中,您将 cc 的地址重新解释为指向 int 的指针。您以 C 风格的方式编写它,而 C++ 风格的方式将是int* pp = reinterpret_cast<int*>(&cc);. reinterpret_cast 始终是一个警告,表明您正在两个不相关的指针之间进行强制转换。在这种情况下,您必须确保您做得对。您必须知道底层内存(布局)。编译器不会阻止您这样做,因为如果经常需要这样做。

在进行演员表时,您会丢弃有关该课程的所有知识。从现在开始,编译器只看到一个 int 指针。当然,您可以访问指针指向的内存。在您的情况下,在您的平台上,编译器碰巧将 cc 放在 TestClass 对象的前 n 个字节中,因此 TestClass 指针也指向 cc 成员。

于 2012-08-23T14:19:20.000 回答
1

reinterpret_cast(并且 C 风格的演员表比 a 更强大)的全部目的reinterpret_cast是提供绕过安全措施的逃生路径。

于 2012-08-23T14:26:16.773 回答
0

这是因为您正在操作您的类在内存中所在的内存。在您的情况下,它恰好将私有成员存储在此内存位置,因此您可以更改它。这不是一个好主意,因为您现在知道对象将如何存储在内存中。

于 2012-08-23T13:59:44.207 回答