我能否在不使用友元的情况下从类外访问私有成员?

79

免责声明

是的,我完全意识到我所询问的内容是非常愚蠢的,任何想在生产代码中尝试这样做的人都应该被解雇和/或开除。 我主要是想看看是否能够实现。

既然这件事已经解决了,有没有办法从类外部访问C++中的私有类成员呢?例如,是否可以使用指针偏移量来完成此操作?

(欢迎使用天真和其他非生产准备技术)

更新

如评论所述,我提出这个问题是因为我想写一篇关于过度封装(以及它如何影响测试驱动开发)的博客文章。我想看看是否有一种方式可以说“即使在C ++中,使用私有变量也不是100%可靠的实施封装”的方法。最终,我决定更专注于如何解决问题而不是为什么这是一个问题,因此我没有像计划的那样突出显示这里提出的一些东西,但我仍然放了一个链接。

无论如何,如果有人对结果感兴趣,这是链接:测试驱动开发敌人之一:封装(我建议在认为我疯了之前先阅读它)。


出于好奇,为什么要问这个问题。我唯一能想到的用途就是入侵别人的API,制造混乱。 - SmacL
我正在撰写一篇关于过度封装的博客文章。我只是想看看是否有可能说“即使在C++中,通过私有方法进行保护也不是完美的!”一旦写好了,我会发布链接。 - Jason Baker
27个回答

83
如果类包含任何模板成员函数,您可以针对自己的需求对该成员函数进行特化。即使最初的开发人员没有考虑到它。
safe.h
class safe
{
    int money;

public:
    safe()
     : money(1000000)
    {
    }

    template <typename T>
    void backdoor()
    {
        // Do some stuff.
    }
};

主程序.cpp:

#include <safe.h>
#include <iostream>

class key;

template <>
void safe::backdoor<key>()
{
    // My specialization.
    money -= 100000;
    std::cout << money << "\n";
}

int main()
{
    safe s;
    s.backdoor<key>();
    s.backdoor<key>();
}

输出:

900000
800000

19
有可能会出现关键字冲突。将它放入匿名命名空间中。 - Martin York
@MartinYork 我在想为什么会出现密码学上的问题(密钥/哈希碰撞!) - undefined

72

我已经添加了一个博客条目(见下文),展示了如何完成它。以下是一个关于如何在以下类中使用它的示例

struct A {
private:
  int member;
};

只需声明一个结构体,其中描述它并实例化用于抢劫的实现类

// tag used to access A::member
struct A_member { 
  typedef int A::*type;
  friend type get(A_member);
};

template struct Rob<A_member, &A::member>;

int main() {
  A a;
  a.*get(A_member()) = 42; // write 42 to it
  std::cout << "proof: " << a.*get(A_member()) << std::endl;
}

Rob类模板的定义如下,只需定义一次,无论您计划访问多少个私有成员。

template<typename Tag, typename Tag::type M>
struct Rob { 
  friend typename Tag::type get(Tag) {
    return M;
  }
};

然而,这并不意味着C++的访问规则不可靠。语言规则旨在防止意外错误-如果您尝试窃取对象的数据,语言会通过设计采取措施来防止您这样做。

我使用以下代码:struct Am { typedef int A::* type; }; 它在GCC4.8上运行良好。我认为该方法利用了编译器的漏洞。它存在可移植性问题。 - thomas
@thomas 如果你读了我的博客文章,你会有所启发并且看到这不是编译器的错误。我建议使用我的后续博客文章。因为它对漏洞的技术实现更好。http://bloglitb.blogspot.de/2011/12/access-to-private-members-safer.html - Johannes Schaub - litb
2
如果你觉得答案看起来很棘手,请注意两个主要点:1)template struct Rob<A_member, &A::member>; 这是一个显式的模板类实例化。 &A::member 在这里起作用因为:“显式实例化定义忽略成员访问说明符:参数类型和返回类型可以是私有的”。 - bartolo-otrit
2
  1. a.*get(A_member()) 调用非成员友元函数 get(A_member),该函数返回 &A::member。这是因为 "friend injection"。尝试删除 get() 函数的 "无用" 参数,你应该会得到 "get was not declared in this scope"(我想看到一个简明扼要的解释这种行为)。
- bartolo-otrit
1
A :: member不是int而是一个名为Foo的私有枚举类型时,是否有可能进行修改? - M.M
显示剩余6条评论

32
下面这个做法非常巧妙,但是不合法,依赖于编译器,并且可能由于不同的实现细节而无法正常工作。
#define private public
#define class struct

但这是对你的问题的回答,在那里你明确邀请了一项技术,我引用你的话:“这是完全愚蠢的技术,任何想在生产代码中尝试这样做的人都应该被解雇或开除。”


另一种技术是访问私有成员数据,通过使用从对象开头开始的硬编码/手工编码偏移量构造指针。


7
你还需要使用 #define class struct,否则默认为私有的可能会使你受挫。 - Tom
1
预处理器技巧与指针解引用一样,都不能访问私有成员。 - cletus
1
同时在使用Xcode 6 (LLVM)进行TDD时也表现良好,这个答案是解决我的问题的最佳方案,而且完全不需要修改原始代码。 - Kaa
注意,如果你正在使用Visual Studio,你还必须将_XKEYCHECK_H添加到预处理器定义中。 - sevensevens
1
这在一些情况下不起作用,因为class可以用于模板参数(与typename相同),但struct不能。 - user202729
显示剩余3条评论

24

嗯,不确定这是否有效,但值得一试。创建另一个与具有私有成员相同布局的类,但将 private 更改为 public。创建一个指向此类的指针变量。使用简单的强制转换将其指向您具有私有成员的对象,并尝试调用私有函数。

预计会出现火花,可能会崩溃 ;)


2
“具有相同的布局”。这是难点。编译器在非POD中布置成员的方式上有很大的自由度。但实际上,它可能会起作用。 - Richard Corden
@Richard Corden - 我也有同感。这再次说明,“可能有效”和“应该在发布的一部分中完成”是完全不同的概念。 - SmacL
1
请记住,编译器完全可以根据访问控制重新排序成员(实际上,如果有任何访问控制,它完全可以重新排序)。我不知道有多少实际上这样做了... - David Thornley
大多数编译器不会重新排序成员 - 对于POD结构来说,它们不被允许这样做,否则就会破坏与C结构(struct sockaddr等)的兼容性。实际上,提供自动成员重新排序的动力非常少。 - Tom

12
class A 
{ 
   int a; 
}
class B
{
   public: 
   int b;
}

union 
{ 
    A a; 
    B b; 
};

应该就这样了。

注:对于这种简单的类来说,它可以工作,但通常情况下不行。

《C++程序设计语言》第 C.8.3 节: "一个具有构造函数、析构函数或复制操作的类不能成为联合成员的类型......因为编译器无法知道要销毁哪个成员。"

因此,我们最好声明class B以匹配A的布局并且通过访问类的私有成员进行hack。


10

如果你能够获取一个类成员的指针,那么无论访问权限是什么(甚至是方法),你都可以使用这个指针。

class X;
typedef void (X::*METHOD)(int);

class X
{
    private:
       void test(int) {}
    public:
       METHOD getMethod() { return &X::test;}
};

int main()
{
     X      x;
     METHOD m = x.getMethod();

     X     y;
     (y.*m)(5);
}

当然,我最喜欢的小技巧就是友元模板后门。

class Z
{
    public:
        template<typename X>
        void backDoor(X const& p);
    private:
        int x;
        int y;
};

假设上述代码的创建者已经为正常使用定义了backDoor。但是你想访问这个对象并查看私有成员变量。即使上述类已经编译成静态库,你也可以添加自己的backDoor模板特化来访问成员。

namespace
{
    // Make this inside an anonymous namespace so
    // that it does not clash with any real types.
    class Y{};
}
// Now do a template specialization for the method.
template<>
void Z::backDoor<Y>(Y const& p)
{
     // I now have access to the private members of Z
}

int main()
{
    Z  z;   // Your object Z

    // Use the Y object to carry the payload into the method.
    z.backDoor(Y());
}

@formerlyknownas_463035818 只有从具有访问权限的方法中才能获取指针。如果一个方法可以访问另一个方法,它可以获取该函数的指针并返回它。一旦你有了指针,它就可以被传递和使用,因为该指针没有任何权限限制。 - Martin York
@formerlyknownas_463035818 1:test()private的,而getMethod()(是public的)返回的值可以是任何东西。2:不,你不能从外部获取指针。 - Martin York
感谢您的评论。我花了一些时间才意识到我日常使用的几乎所有类都有后门。我之前知道最好不要打开这扇门,现在我也知道在哪里找到它。如果您不介意的话,我会删除我的评论,我猜您也想删除您的评论。 - 463035818_is_not_a_number

9

在C++中,使用指针偏移量访问私有成员是完全可能的。假设我有以下类型定义,我想要访问它的成员。

class Bar {
  SomeOtherType _m1;
  int _m2;
};

假设Bar中没有虚方法,那么_m1就是一个简单的情况。在C++中,成员变量存储为对象内存位置的偏移量。第一个对象的偏移量为0,第二个对象的偏移量为第一个成员的大小,以此类推... 因此,以下是访问_m1的一种方式。
SomeOtherType& GetM1(Bar* pBar) {
  return*(reinterpret_cast<SomeOtherType*>(pBar)); 
}

现在_m2有点难。我们需要将原始指针从原始位置移动sizeof(SomeOtherType)个字节。将其强制转换为char是为了确保我在递增字节偏移量。

int& GetM2(Bar* pBar) {
  char* p = reinterpret_cast<char*>(pBar);
  p += sizeof(SomeOtherType);
  return *(reinterpret_cast<int*>(p));
}

注意内存填充和对齐问题。但是+1肯定可行! - Mr.Ree

6
此回答基于@Johannes的答案/博客中展示的确切概念,因为这似乎是唯一“合法”的方法。我将该示例代码转换成了一个方便的实用程序。它易于与C++03兼容(通过实现std::remove_reference并替换nullptr)。

#define CONCATE_(X, Y) X##Y
#define CONCATE(X, Y) CONCATE_(X, Y)

#define ALLOW_ACCESS(CLASS, MEMBER, ...) \
  template<typename Only, __VA_ARGS__ CLASS::*Member> \
  struct CONCATE(MEMBER, __LINE__) { friend __VA_ARGS__ CLASS::*Access(Only*) { return Member; } }; \
  template<typename> struct Only_##MEMBER; \
  template<> struct Only_##MEMBER<CLASS> { friend __VA_ARGS__ CLASS::*Access(Only_##MEMBER<CLASS>*); }; \
  template struct CONCATE(MEMBER, __LINE__)<Only_##MEMBER<CLASS>, &CLASS::MEMBER>

#define ACCESS(OBJECT, MEMBER) \     
 (OBJECT).*Access((Only_##MEMBER<std::remove_reference<decltype(OBJECT)>::type>*)nullptr)

API

ALLOW_ACCESS(<class>, <member>, <type>);

使用方法

ACCESS(<object>, <member>) = <value>;   // 1
auto& ref = ACCESS(<object>, <member>); // 2

示例

struct X {
  int get_member () const { return member; };
private:
  int member = 0;
};

ALLOW_ACCESS(X, member, int);

int main() {
  X x;
  ACCESS(x, member) = 42;
  std::cout << "proof: " << x.get_member() << std::endl;
}

我无法在私有部分中使用类型为 enum Foo 的私有成员,因为 Foo 也在私有部分中。由于 Class::Foo 是私有的,所以它不能在 ALLOW_ACCESS 宏中使用!有什么建议吗? - M.M
@M.M 不知道如何让它工作。尝试了几种替代方案,但都无济于事。如果找到解决方案,会进行更新。 - iammilind

3

很酷的问题,以下是我的回答:

using namespace std;

class Test
{

private:

  int accessInt;
  string accessString;

public:

  Test(int accessInt,string accessString)
  {
    Test::accessInt=accessInt;
    Test::accessString=accessString;
  }
};

int main(int argnum,char **args)
{
  int x;
  string xyz;
  Test obj(1,"Shit... This works!");

  x=((int *)(&obj))[0];
  xyz=((string *)(&obj))[1];

  cout<<x<<endl<<xyz<<endl;
  return 0;
}

希望这有所帮助。

3
如果你知道你的C++编译器如何搞乱名称,是可以的。除非是虚函数,但是如果你知道你的C++编译器如何构建VTABLE...编辑:看到其他回答,我意识到我误读了问题,以为是关于成员函数而不是成员数据。然而,重点仍然在于:如果你知道你的编译器如何布局数据,那么你就可以访问这些数据。

1
我觉得这个答案很有趣,因为反对使用Python私有成员的最大论点是它只是名称混淆(尽管是更标准化的名称混淆)。了解到C++中的情况大致相同是很有趣的。 - Jason Baker
2
Jason - 区别在于,在Python中,访问私有成员可能会被警告。而在C++中,你的同事们会因为你这样绕过封装而开枪。 - Tom

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