下面这样一个程序:(以下程序都是在VS2010下实现)
1: class Base
2: {3: public:
4: virtual void func()5: {6: cout<<"Base virtual func"<<endl;
7: }8: };9:10: class Derived: public Base11: {12: private:
13: virtual void func()14: {15: cout<<"Derived virtual func"<<endl;
16: }17: };18:19: int main()
20: {21: Base *pb = new Derived();
22: pb->func();23: cin.get();24: }
注意第3行和第12行,派生类访问权限为private,但程序输出的结果是:
说明指针pb还是通过多态实现了访问派生类虚函数func。
到底是怎么回事呢?派生类明明是private却还是可以访问?
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
再看一个例子:
class CTest { public: explicit CTest(int prii=111,int proi=222,int pubi=333) { m_prii=prii; m_proi=proi; m_pubi=pubi; } void show() const { cout<<"m_prii="<<m_prii <<" m_proi="<<m_proi<<" m_pubi="<<m_pubi<<endl; } private: int m_prii; protected: int m_proi; public: int m_pubi; }; int main() { CTest tobj; tobj.show(); int *pi= (int *)(&tobj);//*****注意强制转换,如有疑问参读《inside C++ object model》 cout<<"&tobj="<<&tobj<<" ";//tobj对象本身的地址 cout<<pi<<"="<<*pi<<" ";//pi的地址和指向的值 pi++; cout<<pi<<"="<<*pi<<" ";//pi+1后的地址和指向的值 pi++; cout<<pi<<"="<<*pi<<" "<<endl;//pi+1后的地址和指向的值 cin.get(); }
理论上我们是无法访问CTest类m_prii,m_proi两个数据成员的,因为CTest类没有提供public数据操作接口。
上述程序输出:
我们使pi指向tobj对象,所以pi的地址和tobj对象的地址是一样的都是 0x0042FC20;该地址存放的值是111,即tobj对象的m_prii成员的值,
擦,原来即使类没有提供public接口操作private数据成员,我们一样可以通过获取类对象地址来操作私有数据成员。
通过观察上述demo结果,我们发现了两点:
1.类对象数据成员在内存中是连续存放的,和我们在类中数据成员的声明次序一样(以上结果只是MS的VS2010编译器结果)
2.从tobj对象的起始内存地址后,依次连续存放3个数据成员,中间没有什么其他数据存在(比如说用于访问权限设置的数据…);
原来只要通拿到了对象数据成员的内存地址,便可以随意对其进行读写,并没有什么访问权限设置。那为什么我们在程序中使用
tobj.m_prii会出现编译错误呢?
其实所谓的访问权限(public ,protected ,private)从来都只是编译器强加给我们的规定,从而使我们编写出来的程序更符合OO特性,
实际一般程序在内存中数据读取没有任何限制,只要你能拿到数据地址就能读写数据(这也是很多程序外挂的原理,通过抓到的地址直接修改内存数据)。
成员函数的原理则不一样了,我们知道当我们通过tobj.show()调用成员show函数时,其实编译器是把对象的this指针传给在系统内存某处的show()函数,
类似于这样_show(&tobj)调用,可见在类成员函数与对象本身并没有什么关系(即你无法通过类对象本身地址计算出该对象成员函数地址),不像数据成员
无论什么访问权限都可以通过对象本身的地址访问到。成员函数的访问权限的作用就是由编译器决定根据你定义的访问权限,给不给你该成员函数在内存
中的具体地址,如果是public则就可以在程序中通过类似tobj.show()这种由对象调用成员show函数;如果是private,则由编译器控制不给你地址(其实
该函数在内存中仍然存在,不可见不等于不存在),即private成员函数的地址在你的程序中不可见,所以你通过tobj.show()这样访问即错误。所以这一切
都是由编译器控制的,在真实内存中是不存在访问权限这种设置的,(以上讨论仅适用与C++对象对象模型在内存中的分布情况)。而且很重要的一点是编
译器所有的能耐都只能在程序编译期发挥,一旦程序进入运行时,则全权交由计算机控制,按代码指令流程一步一步执行直至over。
下面我们回到起始的问题来解释一下:
21: Base *pb = new Derived();
22: pb->func();
在执行22行时,在编译期如果调用的是虚函数的话,编译器能做的就是判断该基类指针pb所对应的静态类型的func虚函数的访问权限,这是编译器在编译期可以做到
的事,而我们知道虚函数的动态绑定是在程序的运行时,这时候编译器已经不起作用了,所以对应pb所绑定的真正执行的虚函数(一般是派生类对应虚函数)到底是什么
访问权限,超出了编译期编译器已经无法控制,所以即使派生类虚函数是private,pb基类指针则仍可以访问该虚函数,编译器所做的就是设定一个虚函数绑定流程,
一般是根据虚函数在基类vtbl(虚函数表)中的索引(所以虚函数在基类中是public访问权限则可获得该索引,否则获取不到该索引,获取不到该索引值,则无法在vtbl中
获取真正执行的虚函数内存地址,没有函数内存地址,自然无论是基类还是派生类都是无法调用该虚函数的),找到真正绑定的派生类对象的vtbl并拿着之前获得的索
引值得到真正绑定执行的虚函数内存地址,其实这个流程是在程序真正进入内存执行的时候发生的,这时候的内存访问是不受编译器控制的,所以就没用所谓的访问
权限问题了。根据索引找到派生类虚函数内存地址就可以执行了。
小结:
1.数据成员访问权限的作用是:由编译器限制你编码(不可随意访问私有数据成员),从而使你写出的程序更符合OO规范。
2.成员函数访问权限的作用是:由编译器根据你设置的访问权限给不给你该成员函数的真实内存地址,即在你的程序中是否能通过该地址调用该函数。无论你设置什么
访问权限,该函数在内存永远存在,只是你无法在你的程序中访问而已。即不可加不代表不存在。
3.访问权限的作用仅在程序编译期(由编译器控制),一旦超出编译期进入执行期(程序脱离编译器控制),访问权限则消失的无影无踪(即在真实内存中没有任何访问权限设置痕迹)。
一句话总结:
类的访问权限设置仅在程序编译期起作用,一旦程序进入执行期(虚函数动态绑定就发生在这个时候,所以即使派生类为private访问权限),就没有访问权限控制了(这时候编译器不起作用了);
以上内容,纯属个人见解,如有错误,欢迎指出!谢谢
建议阅读书籍《inside C++ object model》