突破C++类的访问控制机制

众所周知,在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
2
3
4
5
6
7
8
9
10
class A{
public:
A(int x=0):private_(x){}
void print(){ std::cout<<private_<<std::endl; }
auto retPrivateVal(){ return &A::private_; }
auto retPrivateFunc(){ return &A::privateFunc; }
private:
void privateFunc(){ std::cout<<"A::privateFunc()"<<std::endl; }
int private_;
};

因为这里获取到的是对象地址(数据成员/函数对象)则在外部均可以通过一个类对象或者类对象的指针访问,因为对指向类成员指针的访问实际上是通过this来计算该成员的偏移值,所以不会触发任何名字查找以及可访问性检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
A example(456);
example.print();
auto memPtr=example.retPrivate();
example.*memPtr=123;
example.print();
auto memFuncPtr=example.retPrivateFunc();
(example.*memFuncPtr)();
/*
output:
456
123
A::privateFunc()
*/

可以看到,类A的private数据成员在外部被修改了,而且也可以在外部调用其private成员函数,也可以将其组合在STL的算符库中批量对类对象执行某种操作。
但是这是基于类的实现者主动提供访问权限这一前提的。

还有一些出于某些原因有时候我们并不希望自己手动修改类的代码(增加或删除类的接口),但是仍然有访问private成员的需求,那么可以用以下两种方式。
假定我们具有以下类:

1
2
3
4
5
6
7
8
9
class A{
public:
A(int x=0):private_(x){}
template<typename T>
void func(const T& x){/* something...*/}
void print(){cout<<private_<<endl;}
private:
int private_;
};

因为其具有一个模板成员函数,我们就可以从这个模板成员函数来突破类的访问权限。
方法就是,在外部添加一个该成员函数的特化版本,因为其是类成员的特化,它也具有访问类内所有成员的权限:

1
2
3
4
5
6
7
8
9
10
namespace {
struct ZZZ{
ZZZ(int i):ival(i){}
int ival;
};
}
template<>
void A::func(const ZZZ& z){
private_=z.ival;
}

这里,当我们使用ZZZ作为参数来调用A::func成员函数时,就能够访问到private成员,也就突破了C++类的访问限制。在里面直接操作private也好,传递出成员指针也好(传递给传入对象),在外部都可以访问到类A的private成员了。

1
2
3
4
5
6
7
8
9
10
11
A example(456);
example.print();
example.func(ZZZ(123));
example.print();
/*
output:
456
123
*/

如有类具有友元模板也同样可以实现上面的行为,而且这种方式是完全符合C++标准的。

下面说一种,依赖于编译器实现的突破访问权限的方法:
同样使用上面的类A作为突破的目标。
可以模仿出和突破目标相同布局的类,但将对应的private成员的访问标号改为public或者protected等更宽松的访问权限(实际上这个是依赖于实现的)。

1
2
3
4
5
6
7
8
9
// 唯一的区别就是private_在类B中为public
class B{
public:
B(int x=0):private_(x){}
template<typename T>
void func(const T& x){/* something...*/}
void print(){cout<<private_<<endl;}
int private_;
};

然后就可以用来搞事情了:

1
2
3
4
5
6
7
8
9
10
11
12
A example(456);
example.print();
(reinterpret_cast<B&>(example)).private_=123;
example.print();
/*
output:
456
123
*/

最核心的就是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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
union U{
A aobj;
B bobj;
};
int main()
{
U uobj{456};
uobj.aobj.print();
uobj.bobj.private_=123;
uobj.aobj.print();
}
/*
output:
456
123
*/

还可以通过另一种奇淫巧技来访问类的私有成员,直接看代码如下:

找个时间再来详细分析一下。

结语:可以从第一种突破类访问控制的方式中看到成员函数模板和类访问控制之间的影响,可以通过成员模板来绕过访问控制的机制,而且最重要的是它是可移植的,但是不建议任何突破成员访问限制的行为。

全文完,若有不足之处请评论指正。

扫描二维码,分享此文章

本文标题:突破C++类的访问控制机制
文章作者:ZhaLiPeng
发布时间:2017年05月12日 11时58分
更新时间:2018年12月03日 23时56分
本文字数:本文一共有1,622字
更新历史: Blame, History  文本模式: .md Raw
原始链接:https://imzlp.me/posts/12080/
许可协议: CC BY-NC-SA 4.0
转载请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!