C++类相关
本文打算通过一些小例子来说明几个关键的知识点。
一:成员函数相关
#include <iostream> using namespace std; class D { public: void printA() { cout<<"printA"<<endl; } static void printB() { cout<<"printB"<<endl; } virtual void printC() { cout << "printC" << endl; } }; int main(int argc, char** argv) { D* d = NULL; d->printA(); d->printB(); d->printC(); return 0; };
刚开始直接懵了,心想,你一个NULL还想怎滴?结果实验了下,结果还是很意外的:
printA printB Segmentation fault
先来分析下为什么printA可以打印出来,本来以为一个NULL什么都干不了,那是潜意识里认为,不管类的非静态成员函数,还是非静态类成员变量都“属于”类实例本身,但事实上是,虽然他们都“属于”类实例,但非静态类成员函数和非静态类成员变量的“属于”意义完全不同,非静态类成员变量属于类实例,是说只属于这个类实例,我们知道,类对象实例化的过程,会给非静态类成员变量分配内存空间,但是对于非静态成员函数来说,其实它是存放在整个进程的代码区的,也就是说是被所有类实例共享的,想想也很简单,如果每个类实例都把代码自己保存一份,那还得了!那么静态成员函数呢,这个就更清楚了,也是放在代码区,供所有类实例公用,那么静态成员函数与非静态成员函数的区别在哪儿呢?区别就在与this指针,静态成员函数不能访问非静态成员数据,相反非静态成员函数可以访问静态成员函数。理解这层意思,也就可以明白为啥可以打印出printB了。
下面看看为啥打印printC时段错误,我们知道要想执行虚函数,首先得去虚函数表查找虚函数的地址,而虚函数表是存在于类实例的内存空间中的,所以这里的NULL没地方去找这张表,自然也就错误。
下面我们来验证下成员函数(包括静态、非静态、虚函数)到底是否占用类实例内存空间呢?
#include <iostream> using namespace std; class A { }; class B { public: B(){} ~B(){} }; class C { public: C(){} ~C(){} void fun(){} }; class D { public: D(){} ~D(){} static void fun(){} }; class E { public: E(){} virtual ~E(){} }; class F { static int i; }; class G { int j; }; int main() { cout<<"A="<<sizeof(A)<<endl; cout<<"B="<<sizeof(B)<<endl; cout<<"C="<<sizeof(C)<<endl; cout<<"D="<<sizeof(D)<<endl; cout<<"E="<<sizeof(E)<<endl; cout<<"F="<<sizeof(F)<<endl; cout<<"G="<<sizeof(G)<<endl; return 0; }
结果如下:
A=1 B=1 C=1 D=1 E=4 F=1 G=4
A、B其实是一样,C++默认会给A添加构造和析构,通过比较A与C,我们看到非静态成员函数并不占用空间,通过比较A与D,我们看到静态成员函数也不占用内存空间,通过比较A与E,我们看到虚函数是占用内存空间的,通过比较A与F、G,可以看到静态成员变量并不占用内存空间,而非静态成员变量是占用内存空间的,通过这个实例可以更加有力的证明上述的论据,另外为什么A=1也是一个比较有意思的问题,总的来说是由于种种原因,C++标准作了这个规定。既然函数并不占用类对象内存空间,那么一个类对象的大小就仅仅取决于成员变量和虚函数表了,再深入就涉及到C++对象内存布局了。
二 成员函数指针
#include<iostream> using namespace std; class A { public: A(int i) : num1(i){} void fun1() { int a; printf("fun1().a = %#08x\n", &a); } virtual void fun2() { int a; printf("fun2().a = %#08x\n", &a); } virtual void fun4() { int a; printf("fun4().a = %#08x\n", &a); } static void fun3(); int num1; int num2; char num3; double num4; static int snum1; static int snum2; }; void A::fun3() { int a; printf("fun3().a = %#08x\n", &a); } int A::snum1 = 5; int A::snum2 = 7; int main(int argc, char** argv) { A a(2); //先看成员变量部分 int A::*q; q = &A::num1; printf("类A成员变量num1(int)相对于类A对象的偏移地址offset = %#08x\n", q); int *p; p = &a.num1; printf("类A成员变量num1(int)的实际内存地址:%#08x\n", p); printf("类A对象的起始地址:%#08x\n", &a); //此处可以看到类对象的成员变量地址 = 类对象的起始地址 + 偏移地址 q = &A::num2; printf("类A成员变量num2(int)相对于类A对象的偏移地址offset = %#08x\n", q); char A::*r = NULL; printf("指向类A成员变量的指针为空时,其相对于类A对象的偏移地址offset = %#08x\n", r); r = &A::num3; printf("类A成员变量num3(char)相对于类A对象的偏移地址offset = %#08x\n", r); double A::*s; s = &A::num4; printf("类A成员变量num4(double)相对于类A对象的偏移地址offset = %#08x\n", s); a.*q = 6;//利用偏移来(访问)修改类对象成员变量值 cout << "a.num2 = " << a.num2 << ", " << a.*q << endl; //获取静态成员数据的地址 int *ptr_static = &A::snum1; printf("类A的静态成员变量snum1(int)的偏移地址 = %#08x\n", ptr_static); cout << "a.snum1 = " << *ptr_static << endl; ptr_static = &a.snum1; printf("类A实例的静态成员变量snum1(int)的偏移地址 = %#08x\n", ptr_static); //成员函数部分 //获取静态函数的地址 void (*ptr_staticfun)() = A::fun3;//直接申明一个指针指向A::fun3 printf("类A的静态成员函数fun3的地址 = %#08x\n", ptr_staticfun); ptr_staticfun();//调用A的静态成员函数fun3() ptr_staticfun = a.fun3; printf("类A实例的静态成员函数fun3的地址 = %#08x\n", ptr_staticfun); ptr_staticfun();//调用A实例的静态成员函数fun3() //获取普通成员函数的地址 typedef void (A::*ptr_commomfun)(); ptr_commomfun ptr = &A::fun1;//取函数A::fun1地址赋值给指针ptr printf("类A的普通成员函数地址 = %#08x\n", ptr);//从地址7位可以看出,这类函数的地址是存放在特定区域的,可能是代码区,从反汇编的代码可以看出 //(ptr)();//必须要.*或者->*才行,所以需要对象或者对象指针才能调用成员函数 (a.*ptr)();//类A实例来调用函数fun1() A b(3); (b.*ptr)();//从这里进一步验证,成员函数的地址与类对象没有关系 //ptr = &a.fun1;//错误的用法 // 既然成员函数是放在代码区被所有实例公用,那么用&A::fun1来取地址是最合理的方式 //虚成员函数部分,关于类继承中的虚函数在后续的内存布局中继续讨论 ptr_commomfun ptr_virtual = &A::fun2; //获取虚函数的地址 printf("类A的虚成员函数1地址 = %#08x\n", ptr_virtual);//从地址可以看出,此处得到的仍然是偏移地址,只不过这个应该是相对于虚函数表的偏移位置 (a.*ptr_virtual)();//调用虚函数 ptr_commomfun ptr_virtual_4 = &A::fun4; //进一步验证虚函数偏移地址,这里是5,显然是1+4,可以猜想虚函数表存放的是具体虚函数表的实际地址,而这里的偏移地址更准确来说是偏移位置,类似于数组的下标 printf("类A的虚成员函数2地址 = %#08x\n", ptr_virtual_4); (a.*ptr_virtual_4)(); //ptr_virtual = &a.fun2; }
执行结果如下:
[root@TransactionTestServer 0525]# ./a.out
类A成员变量num1(int)相对于类A对象的偏移地址offset = 0x000004
类A成员变量num1(int)的实际内存地址:0xbfcde62c
类A对象的起始地址:0xbfcde628
类A成员变量num2(int)相对于类A对象的偏移地址offset = 0x000008
指向类A成员变量的指针为空时,其相对于类A对象的偏移地址offset = 0xffffffff
类A成员变量num3(char)相对于类A对象的偏移地址offset = 0x00000c
类A成员变量num4(double)相对于类A对象的偏移地址offset = 0x000010
a.num2 = 6, 6
类A的静态成员变量snum1(int)的偏移地址 = 0x804a34c
a.snum1 = 5
类A实例的静态成员变量snum1(int)的偏移地址 = 0x804a34c
类A的静态成员函数fun3的地址 = 0x8048766
fun3().a = 0xbfcde5a4
类A实例的静态成员函数fun3的地址 = 0x8048766
fun3().a = 0xbfcde5a4
类A的普通成员函数地址 = 0x8048b90
fun1().a = 0xbfcde5a4
fun1().a = 0xbfcde5a4
类A的虚成员函数1地址 = 0x000001
fun2().a = 0xbfcde5a4
类A的虚成员函数2地址 = 0x000005
fun4().a = 0xbfcde5a4
[root@TransactionTestServer 0525]# ./a.out
类A成员变量num1(int)相对于类A对象的偏移地址offset = 0x000004
类A成员变量num1(int)的实际内存地址:0xbfe0f56c
类A对象的起始地址:0xbfe0f568
类A成员变量num2(int)相对于类A对象的偏移地址offset = 0x000008
指向类A成员变量的指针为空时,其相对于类A对象的偏移地址offset = 0xffffffff
类A成员变量num3(char)相对于类A对象的偏移地址offset = 0x00000c
类A成员变量num4(double)相对于类A对象的偏移地址offset = 0x000010
a.num2 = 6, 6
类A的静态成员变量snum1(int)的偏移地址 = 0x804a34c
a.snum1 = 5
类A实例的静态成员变量snum1(int)的偏移地址 = 0x804a34c
类A的静态成员函数fun3的地址 = 0x8048766
fun3().a = 0xbfe0f4e4
类A实例的静态成员函数fun3的地址 = 0x8048766
fun3().a = 0xbfe0f4e4
类A的普通成员函数地址 = 0x8048b90
fun1().a = 0xbfe0f4e4
fun1().a = 0xbfe0f4e4
类A的虚成员函数1地址 = 0x000001
fun2().a = 0xbfe0f4e4
类A的虚成员函数2地址 = 0x000005
fun4().a = 0xbfe0f4e4
[root@TransactionTestServer 0525]#
执行命令objdump -d a.out可以看到成员函数的内存布局
08048766 <_ZN1A4fun3Ev>: 8048766: 55 push %ebp 8048767: 89 e5 mov %esp,%ebp 8048769: 83 ec 18 sub $0x18,%esp 804876c: 8d 45 fc lea 0xfffffffc(%ebp),%eax 804876f: 89 44 24 04 mov %eax,0x4(%esp) 8048773: c7 04 24 ba 8c 04 08 movl $0x8048cba,(%esp) 804877a: e8 61 fe ff ff call 80485e0 <printf@plt> 804877f: c9 leave 8048780: c3 ret 8048781: 90 nop 08048b90 <_ZN1A4fun1Ev>: 8048b90: 55 push %ebp 8048b91: 89 e5 mov %esp,%ebp 8048b93: 83 ec 18 sub $0x18,%esp 8048b96: 8d 45 fc lea 0xfffffffc(%ebp),%eax 8048b99: 89 44 24 04 mov %eax,0x4(%esp) 8048b9d: c7 04 24 a8 8c 04 08 movl $0x8048ca8,(%esp) 8048ba4: e8 37 fa ff ff call 80485e0 <printf@plt> 8048ba9: c9 leave 8048baa: c3 ret 8048bab: 90 nop 8048bac: 90 nop 8048bad: 90 nop 8048bae: 90 nop 8048baf: 90 nop
从这个例子我们知道:
1.C++中指向成员变量的指针其实是一个相对于类对象(或者相对于整个类而言)的偏移量。
与常规指针不同,一个指向成员变量的指针并不指向一个内存位置。它指向的是一个类的特定成员,而不是指向一个特定对象里的特定成员。通常最清晰的做法是将指向数据成员的指针看作为一个偏移量。这个偏移量告诉你,一个特定成员的位置距离对象的起点有多少个字节。
2.指向成员函数的指针里面存的是函数的实际地址,这个地址比较特殊。
为了对一个指向成员函数的指针进行解引用,需要一个对象或一个指向对象的指针。因为通过指向成员函数的指针调用该函数时,需要将对象的地址用作this指针的值(即便这个值为NULL),以便进行函数调用。
3.虚函数的偏移位置是从1而不是0开始。
同时留下几点疑问:
1.成员函数和静态成员变量究竟是分布在什么地方?
从那几个特殊的地址可以看出是保存在一个特殊的位置,但究竟是在哪儿呢?
2.可以看出函数中的临时变量的地址总是一样的,那么问题来了,临时变量是分布在哪儿呢?
栈空间?那为什么每次都是一样的呢?
3.即使程序重启,打印出的很多值也都还是一样的,究竟内核是怎样分配的呢?
4.多重继承下的虚函数情况?
三:C++内存布局
首先介绍一下C++中有继承关系的类对象内存的布局:
在C++中,如果类中有虚函数,那么它就会有一个虚函数表的指针__vfptr,在类对象最开始的内存数据中。之后是类中的成员变量的内存数据。
对于子类,最开始的内存数据记录着父类对象的拷贝(包括父类虚函数表指针和成员变量)。 之后是子类自己的成员变量数据。
对于子类的子类,也是同样的原理。但是无论继承了多少个子类,对象中始终只有一个虚函数表指针。
为了探讨C++类对象的内存布局,先来写几个类和函数
首先写一个基类:
class Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; }
int base; protected: private: };
然后,我们用多种不同的继承情况来研究子类的内存对象结构。
1. 无虚函数的继承
//子类1,无虚函数重载
class Child1 : public Base { public: virtual void f1() { cout << "Child1::f1" << endl; } virtual void g1() { cout << "Child1::g1" << endl; } virtual void h1() { cout << "Child1::h1" << endl; } int child1; protected: private: };
这个子类Child1没有继承任何一个基类的虚函数,因此它的虚函数表如下图:
我们可以看出,子类的虚函数表中,先存放基类的虚函数,再存放子类自己的虚函数。
2. 有一个虚函数继承
//子类2,有1个虚函数重载
class Child2 : public Base { public: virtual void f() { cout << "Child2::f" << endl; } virtual void g2() { cout << "Child2::g2" << endl; } virtual void h2() { cout << "Child2::h2" << endl; } int child2; protected: private: };
当子类重载了父类的虚函数,则编译器会将子类虚函数表中对应的父类的虚函数替换成子类的函数。
3. 全部虚函数都继承
//子类2,全部虚函数重载
class Child2 : public Base { public: virtual void f() { cout << "Child3::f" << endl; } virtual void g() { cout << "Child3::g" << endl; } virtual void h() { cout << "Child3::h" << endl; } protected: int child1; private: };
4. 多重继承
多重继承,即类有多个父类,这种情况下的子类的内存结构和单一继承有所不同。
我们可以看到,当子类继承了多个父类,那么子类的内存结构和单一继承类似,首先把父类1对象拷贝过来,然后把父类2对象拷贝过来,...,以此类推。当出现继承的时候,类似的,进行对应的覆盖。
在更进一步之前先证明一下,子类会保存每一个父类的虚表指针
#include <iostream> class base1 { public: virtual void f(){}; }; class base2 { public: virtual void g(){}; }; class base : public base1, public base2 { }; int main() { std::cout << sizeof(base1) << std::endl; std::cout << sizeof(base2) << std::endl; std::cout << sizeof(base) << std::endl; }
结果如下,可以证明:
[root@TransactionTestServer 0526]# ./a.out 4 4 8
继续,先看两个父类中没有相同的虚函数,用一个简单的例子说明多重继承中的一个现象:
比如子类 : public 父类1, public 父类2
那么,在虚继承的时候,父类1在覆盖的时候,是在虚表中的原位置覆盖,而在父类2中,并不是在原位置,而是在表的末尾进行追加,但是实现的覆盖效果是一样的,下面给出实例:
#include <iostream> class base1 { public: virtual void f1(){} virtual void g1(){} }; class base2 { public: virtual void f2(){} virtual void g2(){ std::cout << "base2::g2()" << std::endl; } }; class base : public base1, public base2 { public: //virtual void f1(){} //virtual void g1(){} //virtual void f2(){} virtual void g2(){ std::cout << "base:g2()" << std::endl;} virtual void h(){} }; int main() { //base void(base::*base_f1)() = &base::f1; printf("%#08x\n", base_f1); void(base::*base_g1)() = &base::g1; printf("%#08x\n", base_g1); void(base::*base_f2)() = &base::f2; printf("%#08x\n", base_f2); void(base::*base_g2)() = &base::g2; printf("%#08x\n", base_g2); void(base::*base_h)() = &base::h; printf("%#08x\n", base_h); // base b; b.g2(); return 0; }
结果如下:
0x000001 0x000005 0x000001 0x000009 0x00000d base:g2()
下面讨论多重继承中父类们含有相同的虚函数:
#include <iostream> using namespace std; class base1 { public: virtual void f1(){cout << "base1:f1()" << endl;} virtual void g1(){cout << "base1:g1()" << endl;} virtual void h1(){cout << "base1:h1()" << endl;} virtual void t(){cout << "base1:t()" << endl;} }; class base2 { public: virtual void f2(){cout << "base2:f2()" << endl;} virtual void g2(){cout << "base2:g2()" << endl;} virtual void h2(){cout << "base2:h2()" << endl;} virtual void s2(){cout << "base2:s2()" << endl;} virtual void t(){cout << "base2:t()" << endl;} }; class child : public base1, public base2 { public: virtual void f1(){cout << "child:f1()" << endl;} virtual void g1(){cout << "child:g1()" << endl;} virtual void f2(){cout << "child:f2()" << endl;} virtual void g2(){cout << "child:g2()" << endl;} virtual void t(){cout << "child:t()" << endl;} }; int main(int argc, char** argv) { child* ptr = new child(); //base1 void(base1::*pf1_f)() = &base1::f1; printf("%#08x\n", pf1_f); void(base1::*pf1_g)() = &base1::g1; printf("%#08x\n", pf1_g); void(base1::*pf1_h)() = &base1::h1; printf("%#08x\n", pf1_h); void(base1::*pf1_t)() = &base1::t; printf("%#08x\n", pf1_t); //base2 void(base2::*pf2_f)() = &base2::f2; printf("%#08x\n", pf2_f); void(base2::*pf2_g)() = &base2::g2; printf("%#08x\n", pf2_g); void(base2::*pf2_h)() = &base2::h2; printf("%#08x\n", pf2_h); void(base2::*pf2_s)() = &base2::s2; printf("%#08x\n", pf2_s); void(base2::*pf2_t)() = &base2::t; printf("%#08x\n", pf2_t); //child void(child::*child_f1)() = &child::f1; printf("%#08x\n", child_f1); void(child::*child_g1)() = &child::g1; printf("%#08x\n", child_g1); void(child::*child_h1)() = &child::h1; printf("%#08x\n", child_h1); void(child::*child_t)() = &child::t; printf("%#08x\n", child_t); void(child::*child_f2)() = &child::f2; printf("%#08x\n", child_f2); void(child::*child_g2)() = &child::g2; printf("%#08x\n", child_g2); void(child::*child_h2)() = &child::h2; printf("%#08x\n", child_h2); void(child::*child_s)() = &child::s2; printf("%#08x\n", child_s); ptr->f1(); ptr->g1(); ptr->h1(); ptr->f2(); ptr->g2(); ptr->h2(); ptr->t(); return 0; }
结果如下:
0x000001 0x000005 0x000009 0x00000d 0x000001 0x000005 0x000009 0x00000d 0x000011 0x000001 0x000005 0x000009 0x00000d 0x000011 0x000015 0x000009 0x00000d child:f1() child:g1() base1:h1() child:f2() child:g2() base2:h2() child:t()
从结果可以看到,child中存放了两个虚表指针,当父类1和父类2中有相同的虚函数时,首先child会对其中的第一个虚表中对应的偏移进行覆盖,也就是说子类在内存布局的时候,首先是分布拷贝两张父类对象中的内存布局,然后开始根据自己特有的函数进行覆盖操作,首先检查第一张表,然后是第二张,在检查第一张的时候,只要满足覆盖条件就立即覆盖,所以这里的t会覆盖到第一张虚表,如果第一张虚表没有满足条件的,就考虑第二张虚表,这时候的处理方式和第一张有不同,这个时候如果满足覆盖条件,不是在原位置进行覆盖,而是追加在末尾,也可能是直接覆盖在原位置,但是把偏移改变了,每次+4,这样就可以解释结果了,那么这地方为什么是这样呢,我想应该是在指针的类型转换的时候需要考虑的吧,举个例子:
(1)赋值问题
与正常的派生类指针或引用可以赋于基类指针或引用不同,基类成员函数可以赋于派生类成员函数指针(任何情况下都不会出错),反之派生类成员函数在未覆盖基类函数名的情况下也能赋于基类成员函数指针。如下例:
class A class B class C :public A, B { { { public: public: public: void printA(int) virtual void printB(int) void printB(int) { { { } } } }; }; };
void (A::*Pa)(int);
void (B::*Pb)(int);
void (C::*Pc)(int)
pa = &C::printA;(正确)//这个为什么正确呢,因为在C中的这个函数偏移和A中的这个函数的偏移是一样的
pb=&C::printB;(错误)//这个为什么错误呢,因为在C中的这个函数偏移和B中的这个函数的偏移已经不一样了
pc=&A::printA;(正确) //同上理由相同
pc=&B::printB;(正确)//这个为什么又正确呢,我想是第二张虚表中保存了printB的两个偏移,只有这样,才有可能两种方式都正确
总结:只要不被派生类覆盖的函数均可以赋给基类成员函数指针。
(2)多态调用
接上例:
pb=&B::printB;
(b.*pb)(i);(调用b类中的printB)
(c.*pb)(i);(调用c类中的printB)
但是对于pc指针只能由c类对象调用,不能由基类调用。
根据对象实例的类型去判断应该调用的函数。
5. 菱形继承
搞清楚上面的多重继承,菱形继承应该会比较好理解了。
6. 单一虚拟继承
虚拟继承的子类的内存结构,和普通继承完全不同。虚拟继承的子类,有单独的虚函数表, 另外也单独保存一份父类的虚函数表,两部分之间用一个四个字节的0x00000000来作为分界。子类的内存中,首先是自己的虚函数表,然后是子类的数据成员,然后是0x0,之后就是父类的虚函数表,之后是父类的数据成员。
如果子类没有自己的虚函数,那么子类就不会有虚函数表,但是子类数据和父类数据之间,还是需要0x0来间隔。
因此,在虚拟继承中,子类和父类的数据,是完全间隔的,先存放子类自己的虚函数表和数据,中间以0x分界,最后保存父类的虚函数和数据。如果子类重载了父类的虚函数,那么则将子类内存中父类虚函数表的相应函数替换。
7. 菱形虚拟继承
结论:
(1) 对于基类,如果有虚函数,那么先存放虚函数表指针,然后存放自己的数据成员;如果没有虚函数,那么直接存放数据成员。
(2) 对于单一继承的类对象,先存放父类的数据拷贝(包括虚函数表指针),然后是本类的数据。
(3) 虚函数表中,先存放父类的虚函数,再存放子类的虚函数
(4) 如果重载了父类的某些虚函数,那么新的虚函数将虚函数表中父类的这些虚函数覆盖。
(5) 对于多重继承,先存放第一个父类的数据拷贝,在存放第二个父类的数据拷贝,一次类推,最后存放自己的数据成员。其中每一个父类拷贝都包含一个虚函数表指针。如果子类重载了某个父类的某个虚函数,那么该将该父类虚函数表的函数覆盖。另外,子类自己的虚函数,存储于第一个父类的虚函数表后边部分。
(6) 当对象的虚函数被调用是,编译器去查询对象的虚函数表,找到该函数,然后调用。