C++中如何实现私有变量的访问?

13

编译器如何控制变量在内存中的保护?私有变量是否与标签位相关联?它是如何工作的?


1
编译器 = 编译时, 内存 = 运行时, 差别很大 - stijn
2个回答

20
如果你指的是实例的private成员,那么在运行时没有任何保护措施。所有的保护都是在编译时进行的,如果你知道它们在内存中的布局方式,就可以始终访问一个类的私有成员。这需要平台和编译器的知识,并且在某些情况下甚至可能取决于编译器设置,如优化级别。
例如,在我的Linux/x86-64 w/GCC 4.6上,以下程序将打印出你期望的内容。它绝不是可移植的,在奇特的编译器上可能会打印出意想不到的东西,但即使是那些编译器也会有自己特定的方法来访问私有成员。
#include <iostream>

class FourChars {
  private:
    char a, b, c, d;

  public:
    FourChars(char a_, char b_, char c_, char d_)
      : a(a_), b(b_), c(c_), d(d_)
    {
    }
};

int main()
{
    FourChars fc('h', 'a', 'c', 'k');

    char const *p = static_cast<char const *>(static_cast<const void *>(&fc));

    std::cout << p[0] << p[1] << p[2] << p[3] << std::endl;
}

这个复杂的转换是因为void*是任何指针可以强制转换成的唯一类型。然后void*可以被转换成char*而不违反严格别名规则。也可能只需要使用一个reinterpret_cast,但实际上我从不使用这种“肮脏的把戏”,所以对于如何以最快的方式执行它们并不太熟悉 :)


@LuchianGrigore:我不这么认为。鉴于FourChars必须至少有四个大小,打印其前四个字节的结果具有实现相关,但不是未定义的行为。 - Fred Foo
@Mehrdad:reinterpret_cast<> - Vlad
@luchian:当然它不会,但我认为它有权利这样做。 - Vlad
@Vlad 如果它影响可观察行为,那么就不能这样做,而在这种情况下(或任何类似的情况),它会产生影响。 - Luchian Grigore
3
标准草案 N3242 9.2.20:使用 reinterpret_cast 转换的指向标准布局结构体对象的指针指向其初始成员(如果该成员是位域,则指向所在的单元),反之亦然。【注:因此,在标准布局结构体对象内可能存在未命名填充,但不会出现在其开头,仅在必要时用于实现适当对齐。 结束注释】 - TemplateRex
显示剩余20条评论

3

编译器的工作是确保某些成员是私有的,并阻止您使用它们。在编译后,它们与其他成员没有太大区别。

然而,重要的一点是数据成员不需要按照它们在类定义中出现的顺序在内存中布局,但对于访问级别相同的变量,必须按照顺序进行布局。


从纯C++的角度来看,这里的示例 https://dev59.com/Rmgu5IYBdhLWcg3wOUkk#11486632 是UB,因为变量可以通过某种方式进行填充以获得给定的对齐方式(因此,即使已经授予&b > &a(&a)[1]不一定是b)。但一旦给出编译器并定义编译选项,行为就不取决于执行。如果它能工作,总是能工作;如果不能......永远不会。事实上,这是非可移植代码。 - Emilio Garavaglia
2
@EmilioGaravaglia:如果有填充,则我的示例不会显示UB;它的行为只是实现相关。也就是说,它将打印一些任意字节,但不会崩溃。是的,它是不可移植的,也许我应该更明确地说明这一点。 - Fred Foo
根据C++规范对UB的定义(因为UB本身就有定义:-)),UB并不一定意味着“崩溃”。它只是意味着“未被C++规范本身定义”。如果它是“编译器相关的”,则对于ISO C++来说,就是UB。实际上,我们只是用不同的措辞表达了相同的概念,因为我们有不同的视角(语言或编译器)。 - Emilio Garavaglia
@EmilioGaravaglia:我知道UB是什么意思。我并不是说UB会导致崩溃,而是我的程序不会因为它不会引起UB而崩溃。C++标准还留下了一些实现定义的东西,编译器编写者可以选择要实现的行为。对齐就是一个例子;请参见草案标准pp. 1318 ff.以获取完整列表。 - Fred Foo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接