众所周知,在C++中类成员能够具有三种访问权限,分别为public
/protected
/private
:
[ISO/IEC 14882:2014]A member of a class can be
- private: that is, its name can be used only by members and friends of the class in which it is declared.
- protected: that is, its name can be used only by members and friends of the class in which it is declared, by classes derived from that class, and by their friends (see 11.4).
- public: that is, its name can be used anywhere without access restriction.
从标准意图上来看,是希望隐藏类的实现细节和底层数据,即为封装。但是我们也可以通过一些特殊的方式来突破访问权限的限制。
先来矫正一个观念:C++中类的访问控制所限制的是成员的访问权限,而不是可见权限。
[ISO/IEC 14882:2014] It should be noted that it is access to members and base classes that is controlled, not their visibility.
一个类中的任何成员对于任何可以看到其类实现的代码都是可见的,问题是是否可以访问。即,当你直接访问一个类内成员的名字(标识符)时会被检查是否有访问该成员的权限,如果你标记为private
且在类定义的外部或友元外部访问会提示编译错误。关于访问控制的可见性与可访问性的具体描述请看我的另一篇文章:访问控制机制的可见性与可访问性。
所以我们可以通过不使用成员名字也就不会触发访问控制机制,不使用类成员名字却可以访问类成员的方式为——指向类成员的指针。关于C++中指向类成员的指针的具体内容可以看我之前的一篇文章:C++中指向类成员的指针并非指针。
从侵入式实现角度来看,我们可以通过类成员函数主动提供类private
成员访问的方法。
比较常用的方式为——返回private
数据成员的引用或者返回指向类成员的指针,而我比较推崇返回指向成员的指针,因为如果返回引用后期可以访问到的只是某一个具体对象的成员,而函数指针则可以访问到任何一个该类对象的成员。
1 | class A{ |
因为这里获取到的是对象地址(数据成员/函数对象)则在外部均可以通过一个类对象或者类对象的指针访问,因为对指向类成员指针的访问实际上是通过this来计算该成员的偏移值,所以不会触发任何名字查找以及可访问性检查:
1 | A example(456); |
可以看到,类A的private
数据成员在外部被修改了,而且也可以在外部调用其private
成员函数,也可以将其组合在STL的算符库中批量对类对象执行某种操作。
但是这是基于类的实现者主动提供访问权限这一前提的。
还有一些出于某些原因有时候我们并不希望自己手动修改类的代码(增加或删除类的接口),但是仍然有访问private
成员的需求,那么可以用以下两种方式。
假定我们具有以下类:
1 | class A{ |
因为其具有一个模板成员函数,我们就可以从这个模板成员函数来突破类的访问权限。
方法就是,在外部添加一个该成员函数的特化版本,因为其是类成员的特化,它也具有访问类内所有成员的权限:
1 | namespace { |
这里,当我们使用ZZZ
作为参数来调用A::func
成员函数时,就能够访问到private成员,也就突破了C++类的访问限制。在里面直接操作private
也好,传递出成员指针也好(传递给传入对象),在外部都可以访问到类A的private
成员了。
1 | A example(456); |
如有类具有友元模板也同样可以实现上面的行为,而且这种方式是完全符合C++标准的。
下面说一种,依赖于编译器实现的突破访问权限的方法:
同样使用上面的类A作为突破的目标。
可以模仿出和突破目标相同布局的类,但将对应的private
成员的访问标号改为public
或者protected
等更宽松的访问权限(实际上这个是依赖于实现的)。
1 | // 唯一的区别就是private_在类B中为public |
然后就可以用来搞事情了:
1 | A example(456); |
最核心的就是reinterpret_cast<B&>(example)
,强制让编译器把一个类A对象解释为类B的对象,然后访问类B中的public成员private_
,但是实际上改动的是类A的private
成员private_
。
但是这里也是依赖于编译器的实现,因为我们假定修改了访问标号的类B和类A的布局完全相同,但是这是不一定的(在G++中运行通过)。
[TC++PL4th]A compiler may reorder sections of a class with separate access specifiers.
也可以将类A
和类B
的对象放入一个union
中,以A的方式存入,以B的方式读取:
1 | union U{ |
还可以通过另一种奇淫巧技来访问类的私有成员,直接看代码如下(最新代码会放在gist上:hack-private-data-member.cpp):
1 |
|
上面的代码实现是通过模板来获取成员函数的函数指针,展开之后是这样的:
1 | class A{ |
使用模板的原因就是要在编译时绕过访问权限检查。
因为我们要获取的是A::func
的地址,在运行时&A::func
具有访问权限检查,是无法通过编译的,所以需要模板来进行提前执行。
所以新建一个类的定义,将要获取的成员的类型作为一个typedef
(MemType),从而将我们不能访问的成员类型A::func
,变成可以访问的成员类型A_func::MemType
,最重要的是将模板的实参(&A::func
)传递给它(T::MemType M
),来突破访问类的控制权限。
并且,多态也是支持的:
1 | class A{ |
结语:可以从第一种突破类访问控制的方式中看到成员函数模板和类访问控制之间的影响,可以通过成员模板来绕过访问控制的机制,而且最重要的是它是可移植的,并且可以进行代码升级的(只要成员名字没有变化),但是不建议任何突破成员访问限制的行为。