为什么C ++允许使用这种方法修改私有成员?

时间:2012-08-23 13:56:24

标签: c++ private-members

几分钟前看到this question 后,我想知道为什么语言设计者允许它,因为它允许间接修改私人数据。作为一个例子

 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

我测试了上面的代码,实际上私有数据已被修改。有没有解释为什么允许这种情况发生,或者这只是对语言的疏忽?它似乎直接破坏了私人数据成员的使用。

6 个答案:

答案 0 :(得分:31)

因为正如Bjarne所说,C ++旨在防范墨菲,而不是马基雅维利。

换句话说,它应该保护你免受意外事故的影响 - 但是如果你完全去任何工作来破坏它(例如使用演员),它甚至都不会试图阻止你。

当我想到这一点时,我有一个不同的类比:它就像锁在浴室门上。它会给你一个警告,你现在可能不会想要走进那里,但是如果你决定从外面解锁门是微不足道的。

编辑:关于@Xeo讨论的问题,关于为什么标准说“具有相同的访问控制”而不是“拥有所有公共访问控制”,答案很长而且有点曲折。

让我们回到开头,考虑一个结构:

struct X {
    int a;
    int b;
};

C总是对这样的结构有一些规则。一个是在结构的实例中,结构本身的地址必须等于a的地址,因此您可以将指向结构的指针强制转换为指向int的指针,并访问{ {1}}具有明确定义的结果。另一个是成员必须按照结构中定义的内存顺序排列(尽管编译器可以在它们之间插入填充)。

对于C ++,有意维护它,特别是对于现有的C结构。与此同时,如果编译器想要在运行时强制执行a(和private),那么应该很容易做到(合理有效地)。

因此,给出了类似的东西:

protected

对于struct Y { int a; int b; private: int c; int d; public: int e; // code to use `c` and `d` goes here. }; Y.a,应该要求编译器保持与C相同的规则。同时,如果它将在运行时强制执行访问,它可能希望将所有公共变量一起移动到内存中,因此布局更像是:

Y.b

然后,当它在运行时强制执行时,它基本上可以执行struct Z { int a; int b; int e; private: int c; int d; // code to use `c` and `d` goes here. };

之类的操作

据我所知,没有人做过这个,而且我不确定标准的其余部分是否真的允许它,但似乎确实至少有一个半成型的想法沿着那条线。

为了强制执行这两者,C ++ 98说if (offset > 3 * sizeof(int)) access_violation();Y::a必须在内存中按顺序排列,并且Y::b必须位于结构的开头(即类似C的规则)。但是,由于干预访问说明符,Y::aY::c不再必须相互依次。换句话说,在它们之间没有访问说明符的情况下定义的所有连续变量被组合在一起,编译器可以自由地重新排列这些组(但仍然必须保留第一个开头)。

直到一些混蛋(即我)指出规则的编写方式还有一个小问题。如果我写代码如下:

Y::e

......你最终得到了一点点自我矛盾。一方面,这仍然是正式的POD结构,所以C类规则应该适用 - 但由于你在成员之间有(无可否认的无意义)访问说明符,它也赋予了编译器重新安排成员的权限,因此打破了他们想要的C式规则。

为了解决这个问题,他们稍微改写了标准,以便谈论所有具有相同访问权限的成员,而不是他们之间是否存在访问说明符。是的,他们本可以公正地宣布规则只适用于公众成员,但似乎没有人看到任何可以从中获得的东西。鉴于这是修改现有标准的大量代码已经使用了很长一段时间,选择他们可以做出的最小改变仍然可以解决问题。

答案 1 :(得分:14)

由于与C的向后兼容性,你可以做同样的事情。


对于所有想知道的人来说,这就是为什么这不是UB并且实际上是标准所允许的:

首先,TestClass标准布局类§9 [class] p7):

  

标准布局类是一个类:

     
      
  • 没有类型为非标准布局类(或此类类型的数组)或引用的非静态数据成员, // OK:非静态数据成员的类型为“int”
  •   
  • 没有虚拟功能(10.3)且没有虚拟基类(10.1), //确定
  •   
  • 对所有非静态数据成员具有相同的访问控制(第11条), // OK,所有非静态数据成员(1)都是'私有'
  •   
  • 没有非标准布局基类, // OK,没有基类
  •   
  • 要么在大多数派生类中没有非静态数据成员,要么最多只有一个带有非静态数据成员的基类,或者没有带有非静态数据成员的基类,并且 // OK,no基类再次
  •   
  • 没有与第一个非静态数据成员相同类型的基类。 //好的,再没有基类
  •   

通过这种方式,您可以reinterpret_cast将该类设置为其第一个成员的类型(§9.2 [class.mem] p20):

  

指向标准布局结构对象的指针(适当地使用reinterpret_cast转换)指向其初始成员(或者如果该成员是位字段,则指向它所在的单元)和副指针反之亦然。

在您的情况下,C风格的(int*)广告系列会解析为reinterpret_cast§5.4 [expr.cast] p4)。

答案 2 :(得分: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-visible S和C ++ - 可见S 布局兼容S可以在语言边界使用C ++方面具有更高的访问安全性。 reinterpret_cast访问安全颠覆是一个不幸但必要的推论。

另外,限制所有成员具有相同的访问控制是因为允许实现相对于具有不同访问控制的成员重新排列成员。为了整洁,大概有些实现将成员具有相同的访问控制在一起;它也可以用来减少填充,虽然我不知道有任何编译器这样做。

答案 3 :(得分:1)

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

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

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

答案 4 :(得分:1)

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

答案 5 :(得分:0)

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

相关问题