众所周知,C++中private成员只能在类内部访问,就算是继承也无法直接访问到。想在其他类/函数中访问private成员,几乎只能通过友元(friend)实现。由于友元是侵入性的,当你引用了第三方类库,又不能对其修改时,就没法利用友元了。
然而,C++作为世界上拥有特性最多的语言,特别是指针、模板等怪兽级特性,成为了许多人拿来挑战其中条条框框的武器,private禁咒也被各种形式突破。下面盘点一下访问private成员的各种奇技淫巧。
最为人所知的应该是使用宏指令的define大法了。在类定义之前(或者#include
之前),加入#define private public
,这样类成员就全部变成了public成员。但是不要忘记用完之后加上#undef
有了指针,就有了理论上完全控制内存的能力。比如下面的代码:
class A
{
public:
int x;
private:
int y;
};
int main(int argc, char* argv[])
{
A a;
//a.x = 1;
//a.y = 1; error
int* p = (int*)&a;
*p = 1; //a.x = 1
*(p + 1) = 1; //a.y = 1
return 0;
}
利用指针+偏移能访问一个类中的任一成员。但是,如果类中存在非POD成员,或者存在继承关系,想找出一个通用的计算偏移量的方法几乎不可能。
为了克服指针+偏移量的不足,可以手动构造一个除了访问修饰以外完全相同的类,那么这个类的内存布局和我们想要访问的类自然是一样的。比如下面的代码:
class A
{
public:
A():x(0), y(2.0){}
private:
int x;
double y;
};
class PA
{
public:
PA():x(0), y(2.0){}
int x;
double y;
};
int main(int argc, char* argv[])
{
A *p1 = new A;
PA * p2 = reinterpret_cast<PA*>(p1);
p2->m_y;
...
return 0;
}
下面轮到模板出场了。假设一个类的定义中存在公有模板函数:
class A
{
public:
A() : x(10)
{ }
template <typename T>
void foo()
{
// Do some stuff.
}
private:
int x;
};
由于foo()
是模板函数,意味着可以存在不限数量的foo()
实例,我们可以强行向A
中插入一个成员函数:
class key;
template <>
void A::foo<key>()
{
x = 5;
std::cout << x;
}
int main()
{
A a;
a.foo<key>();
return 0;
}
这样一来,就通过模板无中生有的把我们的代码插入到A
的定义中,然后就可以为所欲为了!但是局限也很明显,需要A
中存在一个公有的模板函数。
下面该轮到语言律师们登场了!这篇文章提出了一种完美解法:Back-door to private members and methods。原理还是利用模板,但和上面的方法不同,此方法几乎不存在限制。
直接看代码,我把链接里面得代码改了下:
#include <iostream>
struct safe {
private:
int data = 42;
};
template<int safe::* MEMBER_INT_PTR>
struct GenerateThiefFunction {
friend void steal_from(safe& victim_object) {
victim_object.*MEMBER_INT_PTR = 100;
std::cout << victim_object.*MEMBER_INT_PTR << std::endl;
}
};
template struct GenerateThiefFunction<&safe::data>;
void steal_from(safe& victim_object);
int main() {
safe the_one;
steal_from(the_one);
return 0;
}
编译运行上述代码:
$ g++ -o pb pb.cpp
$ ./pb
100
我们可以看到,the_one
当中的私有成员data
被steal_from()
修改了,并且编译运行过程中没有任何错误。用g++/clang++ ,并且设置c++版本11/14/17都没有任何问题,可以说是终极方案了。
上述代码的原理,链接中讲的很清楚了。C++标准规定了在模版参数和函数声明中,可以使用private types or objects:
ISO/EIC 14882:2011 14.7.2 paragraph 12
The usual access checking rules do not apply to names used to specify explicit instantiations. [ Note: In particular, the template arguments and names used in the function declarator (including parameter types, return types and exception specifications) may be private types or objects which would normally not be accessible and the template may be a member template or member function which would not normally be accessible. - end note ]
为了调用到GenerateThiefFunction
中的steal_from()
,又用到了C++标准中的友元查找规则:
ISO/EIC 14882:2011 7.3.1.2 paragraph 3
If a friend declaration in a non-local class first declares a class or function the friend class or function is a member of the innermost enclosing namespace. The name of the friend is not found by unqualified lookup (3.4.1) or by qualified lookup (3.4.3) until a matching declaration is provided in that namespace scope (either before or after the class definition granting friendship). If a friend function is called, its name may be found by the name lookup that considers functions from namespaces and classes associated with the types of the function arguments (3.4.2).
为了访问private成员,必须在GenerateThiefFunction
内部定义一个函数,然而要想合法的调用这个函数,就只能定义成GenerateThiefFunction
中的友元,然后外部声明这个友元函数。这样就能在main
中调用steal_from()
,实现我们想要的功能。