
本文共 30585 字,大约阅读时间需要 101 分钟。
C++对象模型
对象
对象模型分类
在C++中,成员数据(class data member)有两种:static和nonstatic,成员函数(class member function)有三种:static、nonstatic和virtual。
- 简单对象模型
- 表格驱动模型
- c++对象模型(目前采用的对象模型)
即:虚函数采用虚函数表指针和虚函数表存访函数首地址的形式。
类对象所占用的空间
- 一个空类占一个字节
class A { public: }; int main() { A obj; cout << sizeof(obj) << endl; //1 cout << sizeof(A) << endl;//1 }
- 普通成员函数和静态成员函数不计算在sizeof内
一:
class A { public: void fun(){ }//成员函数,静态成员函数也不占用内存空间 }; int main() { A obj; cout << sizeof(obj) << endl;//1 cout << sizeof(A) << endl;//1 }
二:
class A { public: void fun(){ int a; }//成员函数 }; int main() { A obj; cout << sizeof(obj) << endl;//1 cout << sizeof(A) << endl;//1 }
- 静态成员变量不计算在sizeof内
class A { public: static int a; static int b; }; int main() { A obj; cout << sizeof(obj) << endl;//1 cout << sizeof(A) << endl;//1 }
结论:静态成员变量跟着类走,不占用对象内存空间。
- 虚函数不计算在对象的sizeof内,但是会存在一个虚函数表指针
class A { public: virtual void fun1(){ } virtual void fun2(){ } }; int main() { A obj; cout << sizeof(obj) << endl;//4 cout << sizeof(A) << endl;//4 }
结论:不管虚函数有几个,都是占4个字节。
虚函数表:vtbl
虚函数表:跟着类走,用来保存指向类里面每个虚函数的指针,即如果类里面有一个虚函数,那保存的指针就有一个,如果有两个虚函数,那虚函数表里就就保存有两个指针。
针对于上面的结果进行分析,为什么是占用4个字节呢?
答:这4个字节是一个指针(vptr),这个指针用来指向虚函数表。
这个指针的值,系统会在适当的时机,比如调用构造函数时,给这个指针赋值。也就是虚函数表的首地址。
结论:虚函数不计算在类对象的sizeof里,但是会额外增加一个虚函数表指针
另外,虚析构函数也是占用4个字节。
需要说明的是:为什么普通成员函数不需要搞虚函数表,而虚函数例外呢?
因为虚函数的多态性问题,所以虚函数的处理方式与普通的成员函数不一样。
- 字节对齐问题
字节对齐总的来说是为了提高访问速度。
如果类里有的成员变量是指针,例如,int *p,char *str等等,就占用4个字节,,当然Linux平台下可能是8字节。
this指针调整问题(出现在多重继承中,调用哪个子类的成员函数,这个this指针就会被编译器自动调整到对象内存布局中对应改子类对象的起始地址那去
范例一:
class A{ public: A() { printf("A():%p\n", this); } void funA() { printf("funA():%p\n", this); }public: int a;};class B{ public: B() { printf("B():%p\n", this); } void funB() { printf("funB():%p\n", this); }public: int b;};class C :public A, public B //继承的顺序和类C的内存空间布局有关{ public: C() { printf("C():%p\n", this); } void funC() { printf("funC():%p\n", this); }public: int c;};int main(){ cout << sizeof(A) << endl; cout << sizeof(B) << endl; cout << sizeof(C) << endl; C obj; obj.funA(); obj.funB(); obj.funC();}
结论:如果派生类只继承一个基类,那么这个基类的地址和派生类的地址相同。
如果一个类,同时继承多个基类,那么这个子类的对象和它继承顺序的第一个基类的地址相同(这里A类对象首地址和C类对象首地址相同)。
范例二:
class A{ public: A() { printf("A():%p\n", this); } void funA() { printf("A::funA():%p\n", this); }public: int a;};class B{ public: B() { printf("B():%p\n", this); } void funB() { printf("B::funB():%p\n", this); }public: int b;};class C :public A, public B{ public: C() { printf("C():%p\n", this); } void funB() //覆盖掉类B中的funb函数,所以调用该函数时,使用的this指针就会调整,即用类C的this指针去调用该函数。 { printf("C::funB():%p\n", this); } void funC() { printf("C::funC():%p\n", this); }public: int c;};int main(){ cout << sizeof(A) << endl; cout << sizeof(B) << endl; cout << sizeof(C) << endl; C obj; obj.funA(); obj.funB(); obj.funC();}
总结:该案列只有一些简单的成员函数,无虚函数,,所以分析起来也较简单。
这种情况的话,一般时出现在多重继承(继承多个父类)中,,后面的话,,调用那个子类或者父类的成员函数,,就用谁的this指针去调用。
比如说,,这里有3个类,,A和C的this指针是相同的(和继承顺序有关),所以调用A和C的成员函数的this指针相同,,调用B的成员函数,就用B的this指针去调用。
上面,C类覆盖了B里的一个成员函数,所以再调用这个成员函数的话,调用这个函数的话就变成调用C里的这个函数了,也就是用C的this指针去调用。
编译器合成默认构造函数的5种情况
- 如果一个类没有任何构造函数,但包含一个类类型的成员变量,而这个类类型的成员变量有一个默认构造函数.
class A { public: A() { cout << "aaaaa" << endl; } }; class C { public: int c; A a; }; int main() { C c;//会调用A() }
分析:为什么会调用A()呢?其实是编译器为类C合成了一个默认的构造函数,而这个默认构造函数又去调用A(),来初始化a,所以会调用A()
- 父类带有默认构造函数,子类没有任何构造函数
class A { public: A() { cout << "aaaaa" << endl; } }; class B:public A { public: }; int main() { B b; }
父类有一个默认构造函数,而子类没有构造函数时,编译器会为子类合成一个默认构造函数,从而让这个合成的默认构造函数去调用父类中的构造函数。
-
一个类有虚函数,但是该类没有任何构造函数
note:有虚函数,就会存在虚函数表,所以编译器会合成一个默认构造函数,这个默认构造函数的目的是将虚函数表首地址赋给虚函数表指针。
class A { public: virtual void fun() { cout << "aaaaa" << endl; } }; A a;//只是这里不会去调用该虚函数,因为编译器安插的代码中没这么干。
- 一个类带有虚基类(给虚基类表赋值以及调用父类的构造函数)
虚基类(虚继承)只会出现在三层结构中:
class Grand { public: int a; }; class A:virtual public Grand//虚继承 { public: }; class A2 :virtual public Grand//虚继承 { public: }; class C :public A, public A2 { public: }; int main() { C c; }
- 定义成员变量时赋初值(c++11)
class A { public: int a =10; };
总结思考:我们要想确定到底有没有合成默认构造函数,也很简单,只需要,进行单步调试,然后,跳到反汇编(不懂汇编也没关系)中,如果见到了构造函数的特殊子眼,那么肯定就合成了构造函数。
编译器合成拷贝构造函数的4种情况
拷贝构造函数语义:
传统上,大家认为:如果我们没有定义一个自己的拷贝构造函数,编译器会帮助我们合成 一个拷贝构造函数。
但,这个合成的拷贝构造函数,也是在 必要的时候才会被编译器合成出来。 所以 “必要的时候”;是指什么时候?
那编译器在什么情况下会帮助我们合成出拷贝构造函数来呢?那这个编译器合成出来的拷贝构造函数又要干什么事情呢?
(1)如果一个类A没有拷贝构造函数,但是含有一个类类型CTB的成员变量m_ctb。该类型CTB含有拷贝构造函数,那么当代码中有涉及到类A的拷贝构造时,编译器就会为类A合成一个拷贝构造函数。
编译器合成的拷贝构造函数往往都是干一些特殊的事情。如果只是一些类成员变量值的拷贝这些事,编译器是不用专门合成出拷贝构造函数来干的,编译器内部就干了;
(2)如果一个类CTBSon没有拷贝构造函数,但是它有一个父类CTB,父类有拷贝构造函数,
当代码中有涉及到类CTBSon的拷贝构造时,编译器会为CTBSon合成一个拷贝构造函数 ,调用父类的拷贝构造函数。
(3)如果一个类CTBSon没有拷贝构造函数,但是该类声明了或者继承了虚函数,
当代码中有涉及到类CTBSon的拷贝构造时,编译器会为CTBSon合成一个拷贝构造函数 ,往这个拷贝构造函数里插入语句:
(4)如果 一个类没有拷贝构造函数, 但是该类含有虚基类
当代码中有涉及到类的拷贝构造时,编译器会为该类合成一个拷贝构造函数;
(5)(6)其他编译器合成拷贝构造函数的情形留给大家探索。
- 一个类A没有构造函数,但是含有一个类类型B的成员变量m_b,该类型B含有拷贝构造函数,那么当涉及到类A拷贝构造时,编译器会为类A合成一个拷贝构造函数。
class B { public: B(const B&) { cout << "B()的拷贝构造函数执行了" << endl; } class A { public: B m_b; }; A a1; A a2 =a1;//实际累A的拷贝构造时才会合成
- 一个类A没有拷贝构造函数,但是它有一个父类,父类有拷贝构造函数,当代码涉及到类A的拷贝构造时,编译器会类A合成一个拷贝构造函数。
class B { public: B(const B&) { cout << "B()的拷贝构造函数执行了" << endl; } }; class A:public B { public: }; A a1; A a2 =a1;//实际累A的拷贝构造时才会合成
- 一个类A没有拷贝构造函数时,但是该类声明了或者继承了虚函数,当代码中涉及到类A的拷贝构造时,编译器会为类A合成一个拷贝构造函数。(给虚函数表指针值)
class A { public: virtual void mvirfunc() { } }; A a1; A a2 = a1;
合成的原因是,要把a1这个对象的,虚函数表首地址赋值给虚函数表指针,这个动作,,拷贝给a2。
-
如果一个类没有拷贝构造函数,但是该类含有虚基类时,当代码涉及到类的拷贝构造时,编译器会为该类合成一个拷贝构造函数(涉及虚基类表话题)
虚基类主要解决在多重继承时,基类可能被多次继承,虚基类主要提供一个基类给派生类,
#include <iostream> using namespace std; class B0// 声明为基类B0 { int nv;//默认为私有成员 public://外部接口 B0(int n){ nv = n; cout << "Member of B0" << endl; }//B0类的构造函数 void fun(){ cout << "fun of B0" << endl; } }; class B1 :virtual public B0 { int nv1; public: B1(int a) :B0(a){ cout << "Member of B1" << endl; } }; class B2 :virtual public B0 { int nv2; public: B2(int a) :B0(a){ cout << "Member of B2" << endl; } }; class D1 :public B1, public B2 { int nvd; public: D1(int a) :B0(a), B1(a), B2(a){ cout << "Member of D1" << endl; } void fund(){ cout << "fun of D1" << endl; } }; int main(void) { D1 d1(1); d1.fund(); d1.fun(); return 0; }
思考总结:同样,我们要想知道编译器到底有没有合成拷贝构造函数,看对应的汇编代码就知道了。
拷贝构造函数的深浅拷贝问题(同一块内存会释放两次的情形)
前言:和默认构造函数类似,在某些情况下,我们只能自己定义自己的拷贝构造函数,而不能使用系统提供的,因为在某些情况下使用系统提供的拷贝构造函数会带来一定的影响,例如深浅拷贝。
例一:
class Student{ public: int m_age; int *m_heigh;public: Student(int heigh, int age); ~Student();};Student::Student(int heigh,int age){ m_age = age; m_heigh = new int;//申请内存空间 *m_heigh = heigh;//往申请的内存空间里写值}Student::~Student(){ if (m_heigh != nullptr) { delete m_heigh;//在堆上申请的内存需要手动释放 m_heigh = nullptr; }}Student s1(10,20);Student s2 = s1;//由于没有自己定义拷贝构造函数,会造成程序有错。
当一个类中有指针类的成员时,而我们自己是使用的系统给我们提供的拷贝构造函数,在进行了类似于Student s2 = s1
这种类之间的拷贝动作的时候就会造成程序的错误了,为什么?
原因在于指针之间的赋值,是把指针指向了一个共同的内存地址,所以在进行析构的时候,这个共同的内存地址就会析构两次,所以就造成了系统的crash。这就是浅拷贝。
例二:
那么什么是深拷贝呢?利用自己定义的拷贝构造函数就可以解决这个问题,这样一来,s1和s2的指针都会指向不同的内存地址,当然他们各自的内存地址当中的值是一样的,要达到这样的目的,就是我们说的深拷贝。
class Student{ public: int m_age; int *m_heigh;public: Student(int heigh, int age); ~Student(); Student(const Student &s);//拷贝构造函数,const防止对象被改变};Student::Student(int heigh,int age){ m_age = age; m_heigh = new int; *m_heigh = heigh;}Student::~Student(){ if (m_heigh != nullptr) { delete m_heigh; m_heigh = nullptr; }}//拷贝构造函数里,当发生拷贝时,重新申请了一块内存。这样就避免了同一块内存地址被释放两次。Student::Student(const Student &s){ m_age = s.m_age; m_heigh = new int; *m_heigh = *(s.m_heigh);}Student s1(10,20);Student s2 = s1;cout << s1.m_age << endl;cout << *(s1.m_heigh) << endl;cout << s2.m_age << endl;cout << *(s2.m_heigh) << endl;
移动构造函数语义学
程序转化语义(我们写的代码,编译器会对代码进行拆分,拆分成编译器更容易理解和实现的代码)
-
定义时初始化
例如:X X0;
//以下都属于定义时初始化
X X1 = X0;
X X2 = (X0);
X X3 (X0);对于X X3 = X0;
编译器如何解析这行代码?
编译器会对这行代码进行拆分,拆分成以下两行代码,
X X3_3; //在编译器看来,这当然不会调用默认构造函数。
X3_3.X::X(X0); -
参数的初始化
-
函数返回值
程序的优化
成员初始化列表
可参考:[成员初始化列表]
- 何时必须使用成员初始化列表
- 类中含有引用类型的成员
- 类中含有const类型成员
- 一个类继承于另一个类,并且继承的这个类中有构造函数,且构造函数带有参数时
- 一个类,含有一个类类型成员,并且这个类类型成员有构造函数(带参的)
- 使用初始化列表的优势(对于类中含有类类型成员,可以减少一些构造函数或者赋值运算符的调用以提高程序运行效率)
- 初始化列表细节探究
- 初始化列表中的代码可以看作是被编译器安插在构造函数中的
- 初始化列表中的代码是在构造函数的函数体之前被执行的
- 初始化列表成员变量的初始化顺序看的是变量在类中定义的顺序,而不是看在初始化列表中出现的顺序
虚函数
虚函数表指针位置(对象模型的开头)
来感受一下虚函数表指针的存在:
#include "pch.h"#include <iostream>#include <stdio.h>using namespace std;class A{ public: int i; virtual void func() { };};int main(){ A a; char *p1 = reinterpret_cast<char*>(&a); char *p2 = reinterpret_cast<char*>(&a.i); cout << sizeof(a) << endl;//8 if (p1 == p2) { cout << "虚函数表指针位于对象模型末尾" << endl; } else { cout << "虚函数表指针位于对象模型开头" << endl; } /* note: 1:通过运行结果,我们感受到了虚函数表指针的存在 2:虚函数表指针,具体存在什么位置,要看具体的编译器。 */ return 0;}
运行结果:
范例二:
写一个手工调用虚函数的例子:
#include "pch.h"#include <iostream>#include <stdio.h>using namespace std;class A{ public: virtual void g() { cout << "A::g()" << endl; }; virtual void f() { cout << "A::f()" << endl; }; virtual void h() { cout << "A::h()" << endl; };};/*定义一个函数指针*/typedef void (*func)(void);int main(){ A *obj = new A(); long *p = (long *)obj; //把指针指向地址里面的值(虚函数表指针)取出赋给q; long *q = (long *)*p; func i = (func)*q; func j = (func)*(q+1); func k = (func)*(q+2); i(); j(); k(); return 0;}
运行结果:
单继承情况下父类和子类虚函数表指针和虚函数表分析
- 子类中没有覆盖父类虚函数时情况分析
#include "pch.h"#include <iostream>#include <stdio.h> #include <stdlib.h> using namespace std;//父类class A{ public: virtual void ai() { cout << "A::ai()" << endl; } virtual void aj() { cout << "A::aj()" << endl; } virtual void ak() { cout << "A::ak()" << endl; }};//子类class B:public A{ public: virtual void bi() { cout << "B::bi()" << endl; }};//定义一个函数指针typedef void (*fun)(void);int main(){ A a; B b; int size = 0; size = sizeof(a);//4 cout << size << endl; size = sizeof(b);//4 cout << size << endl; cout << "\n-----------------------------------------\n" << endl; //手动的从父类的虚函数表中调用父类中的虚函数 A *p = new A(); //类型转换 long *p1 = (long *)p; //*p1是一个指针,即虚函数表首地址 long *vptr = (long *)*p1; //函数指针指向这个虚函数表地址的第一项 fun f1 = (fun)vptr[0]; fun f2 = (fun)vptr[1]; fun f3 = (fun)vptr[2]; //fun f4 = (fun)vptr[3]; 没有这一项 //这样写只是输出虚函数的首地址,并不会调用虚函数 cout << *f1 << endl; cout << *f2 << endl; cout << *f3 << endl; //这样写才会调用虚函数 f1(); //可以理解为虚函数表的内容是一个函数,所以这样写会调用这个函数,比如和int b=10; int *a = &b; 是一个意思 f2(); f3(); //f4(); 异常 cout << "\n-----------------------------------------\n" << endl; //手动的从子类的虚函数表中调用子类中的虚函数 A *p2 = new B(); //类型转换 long *p3 = (long *)p2; //*p1是一个指针,即虚函数表首地址 long *vptr1 = (long *)*p3; //函数指针指向这个虚函数表地址的第一项 fun f5 = (fun)vptr1[0]; fun f6 = (fun)vptr1[1]; fun f7 = (fun)vptr1[2]; fun f8 = (fun)vptr1[3]; //这样写只是输出虚函数的首地址,并不会调用虚函数 cout << *f5 << endl; cout << *f6 << endl; cout << *f7 << endl; f5(); f6(); f7(); f8(); delete p; delete p2; return 0;}/*总结(单继承子类不覆盖父类的同名函数虚函数布局):1:在单继承中,父类和子类共用一个虚函数表指针,因为他们的size都为42:在单继承中,子类的虚函数表里的虚函数先是父类的,然后才是自己的*/
//运行结果
- 子类中有覆盖父类虚函数时情况分析
#include "pch.h"#include <iostream>#include <stdio.h> #include <stdlib.h> using namespace std;//父类class A{ public: virtual void ai() { cout << "A::ai()" << endl; } virtual void aj() { cout << "A::aj()" << endl; } virtual void ak() { cout << "A::ak()" << endl; }};//子类class B:public A{ public: //覆盖父类的同名函数 virtual void ai() { cout << "B::ai()" << endl; } //子类自己的虚函数 virtual void bi() { cout << "B::bi()" << endl; }};//定义一个函数指针typedef void (*fun)(void);int main(){ A a; B b; int size = 0; size = sizeof(a);//4 cout << size << endl; size = sizeof(b);//4 cout << size << endl; cout << "\n-----------------------------------------\n" << endl; //手动的从父类的虚函数表中调用父类中的虚函数 A *p = new A(); //类型转换 long *p1 = (long *)p; //*p1是一个指针,即虚函数表首地址 long *vptr = (long *)*p1; //函数指针指向这个虚函数表地址的第一项 fun f1 = (fun)vptr[0]; fun f2 = (fun)vptr[1]; fun f3 = (fun)vptr[2]; //fun f4 = (fun)vptr[3]; 没有这一项 //这样写只是输出虚函数的首地址,并不会调用虚函数 cout << *f1 << endl; cout << *f2 << endl; cout << *f3 << endl; //这样写才会调用虚函数 f1(); //可以理解为虚函数表的内容是一个函数,所以这样写会调用这个函数,比如和int b=10; int *a = &b; 是一个意思 f2(); f3(); //f4(); 异常 cout << "\n-----------------------------------------\n" << endl; //手动的从子类的虚函数表中调用子类中的虚函数 A *p2 = new B(); //类型转换 long *p3 = (long *)p2; //*p1是一个指针,即虚函数表首地址 long *vptr1 = (long *)*p3; //函数指针指向这个虚函数表地址的第一项 fun f5 = (fun)vptr1[0]; fun f6 = (fun)vptr1[1]; fun f7 = (fun)vptr1[2]; fun f8 = (fun)vptr1[3]; //这样写只是输出虚函数的首地址,并不会调用虚函数 cout << *f5 << endl; cout << *f6 << endl; cout << *f7 << endl; f5(); f6(); f7(); f8(); delete p; delete p2; return 0;}/*总结(单继承子类覆盖父类的同名函数虚函数布局):1:在单继承中,父类和子类共用一个虚函数表指针,因为他们的size都为42:在单继承中,子类的虚函数表里的虚函数先是父类的,然后才是自己的, 如果子类当中有虚函数覆盖父类的虚函数,那么这个覆盖了的虚函数 在虚函数表当中的位置,就替换掉没有这个父类当中的虚函数的位置, 比如,没有覆盖之前,虚函数表当中,虚函数的位置依次是, A::ai()->A::aj()->A::ak()->B::bi() 现在子类也有一个ai函数,那么子类的虚函表当中,出现虚函数的顺序依次为 B::ai()->A::aj()->A::ak()->B::bi() ,即B::ai()替换A::ai()的位置*/
//运行结果如下所示:
多继承情况下父类和子类虚函数表指针和虚函数表分析
- 子类中没有覆盖父类虚函数时情况分析
#include "pch.h"#include <iostream>#include <stdio.h> #include <stdlib.h> using namespace std;//父类class A{ public: virtual void ai() { cout << "A::ai()" << endl; } virtual void aj() { cout << "A::aj()" << endl; } virtual void ak() { cout << "A::ak()" << endl; }};//父类class B{ public: virtual void bi() { cout << "B::bi()" << endl; }};//多继承,同时继承两个类class C:public A,public B{ public: virtual void ci() { cout << "C::bi()" << endl; } virtual void cj() { cout << "C::bi()" << endl; }};//定义一个函数指针typedef void (*fun)(void);int main(){ A a; B b; C c; int size = 0; size = sizeof(a);//4 cout << size << endl; size = sizeof(b);//4 cout << size << endl; size = sizeof(c);//8,说明存在两个虚函数表指针 cout << size << endl; cout << "-----------------------------------------" << endl; //看下A类的虚函数布局 cout << "看下A类的虚函数布局" << endl; A *a1 = new A(); long *a2 = (long *)a1; long *vptra = (long *)*a2; fun fa1 = (fun)vptra[0]; fun fa2 = (fun)vptra[1]; fun fa3 = (fun)vptra[2]; fa1(); fa2(); fa3(); //看下B类的虚函数布局 cout << "\n\n看下B类的虚函数布局" << endl; B *b1 = new B(); long *b2 = (long *)b1; long *vptrb = (long *)*b2; fun fb1 = (fun)vptrb[0]; fb1(); //因为,子类C有两个虚函数表,所以分别看看这两个虚函数里出现的虚函数是什么以及出现顺序 cout << "\n\n看下C类的第一个虚函数表的虚函数布局" << endl; C *c1 = new C(); //类型转换 long *p1 = (long *)c1; //*p1是一个指针,即虚函数表首地址 long *vptr1 = (long *)*p1; //函数指针指向这个虚函数表地址的第一项 fun f1 = (fun)vptr1[0]; fun f2 = (fun)vptr1[1]; fun f3 = (fun)vptr1[2]; fun f4 = (fun)vptr1[3]; fun f5 = (fun)vptr1[4]; f1(); f2(); f3(); f4(); f5(); //在看看第二个虚函数表 cout << "\n\n看下C类的第二个虚函数表的虚函数布局" << endl; long *p2 = p1 + 1; long *vptr2 = (long*)*p2; fun f6 = (fun)vptr2[0]; f6(); delete a1; delete b1; delete c1; cout << "-----------------------------------------" << endl; return 0;}/*总结(多继承子类不覆盖父类的同名函数虚函数布局): 1:多继承当中,存在多个虚函数表指针,即存在多个虚函数表 2:可以看到子类的虚函表指针与第一个继承的父类共用同一个, 但是,这个虚函数表指针的指向不一样,即子类C的虚函数表内容(第一个虚函数表)与 A类的虚函数表内容不一样。*/
//运行结果如下所示:
- 子类中有覆盖父类或者多个父类虚函数时情况分析
#include "pch.h"#include <iostream>#include <stdio.h> #include <stdlib.h> using namespace std;//父类class A{ public: virtual void ai() { cout << "A::ai()" << endl; } virtual void aj() { cout << "A::aj()" << endl; } virtual void ak() { cout << "A::ak()" << endl; }};//父类class B{ public: virtual void bi() { cout << "B::bi()" << endl; }};//多继承,同时继承两个类class C:public A,public B{ public: virtual void ai()//覆盖A类的同名函数 { cout << "C::ai()" << endl; } virtual void bi()//覆盖B类的同名函数 { cout << "C::bi()" << endl; }public: virtual void ci() { cout << "C::ci()" << endl; } virtual void cj() { cout << "C::cj()" << endl; }};//定义一个函数指针typedef void (*fun)(void);int main(){ A a; B b; C c; int size = 0; size = sizeof(a);//4 cout << size << endl; size = sizeof(b);//4 cout << size << endl; size = sizeof(c);//8,说明存在两个虚函数表指针 cout << size << endl; cout << "-----------------------------------------" << endl; //看下A类的虚函数布局 cout << "看下A类的虚函数布局" << endl; A *a1 = new A(); long *a2 = (long *)a1; long *vptra = (long *)*a2; fun fa1 = (fun)vptra[0]; fun fa2 = (fun)vptra[1]; fun fa3 = (fun)vptra[2]; fa1(); fa2(); fa3(); //看下B类的虚函数布局 cout << "\n\n看下B类的虚函数布局" << endl; B *b1 = new B(); long *b2 = (long *)b1; long *vptrb = (long *)*b2; fun fb1 = (fun)vptrb[0]; fb1(); //因为,子类C有两个虚函数表,所以分别看看这两个虚函数里出现的虚函数是什么以及出现顺序 cout << "\n\n看下C类的第一个虚函数表的虚函数布局" << endl; C *c1 = new C(); //类型转换 long *p1 = (long *)c1; //*p1是一个指针,即虚函数表首地址 long *vptr1 = (long *)*p1; //函数指针指向这个虚函数表地址的第一项 fun f1 = (fun)vptr1[0]; fun f2 = (fun)vptr1[1]; fun f3 = (fun)vptr1[2]; fun f4 = (fun)vptr1[3]; fun f5 = (fun)vptr1[4]; f1(); f2(); f3(); f4(); f5(); //在看看第二个虚函数表 cout << "\n\n看下C类的第二个虚函数表的虚函数布局" << endl; long *p2 = p1 + 1; long *vptr2 = (long*)*p2; fun f6 = (fun)vptr2[0]; f6(); delete a1; delete b1; delete c1; cout << "-----------------------------------------" << endl; return 0;}/*总结(多继承子类覆盖父类的同名函数虚函数布局): 1:把相应的覆盖函数替换之前没有覆盖函数的位置,便 得到了子类的虚函数表的虚函数布局。 */
//运行结果
分析虚函数表的工具与vptr,vtbl创建时机
- 辅助工具(查看虚函数表指针专用工具)
- vptr与vtbl创建时机
vptr也就是虚函数表指针,它可以理解为一个类对象的隐藏变量,所以说,类对象什么时候创建出来,vptr就是什么时候创建出来,即程序运行时,所以,vptr是和对象有关的,vptr跟着对象走。而vbtl也就是虚函数表,它是在编译期间就已经生成的,也就是说它是跟着类走的,类里面有几个虚函数,该怎么在虚函数表里布局,这当然是在编译期间做的事情。所以说,在程序运行时,编译器会在类的构造函数里安插代码,也就是把虚函数表首地址赋给虚函数指针的代码。
单纯的类不纯时引发的虚函数调用问题(memset和memcpy问题)
- 单纯的类:只有一些简单的成员变量
这种情况下,在构造函数和拷贝构造函数里使用memset,memcpy进行赋值和拷贝,这没有什么毛病,但是如果一个类有虚函数的存在,或者有虚基类的存在,在构造函数和拷贝构造函数里使用memset,memcpy使用类似的代码就比较危险了。 - 不纯的类:指类中有一些隐藏的变量,例如虚函数表指针(有虚函数时存在),虚基类表指针
- 涉及静态联编和动态联编概念
来看范例,一个不纯的类在构造函数和拷贝构造函数里使用memset,memcpy类似的代码时会带来什么问题。
#include "pch.h"#include <iostream>#include <stdio.h> #include <stdlib.h> using namespace std;class A{ public: int a; int b; int c;public: A() { memset(this, 0, sizeof(A)); cout << "A()" << endl; } virtual void ai() { cout << "A::ai()" << endl; } virtual ~A() { cout << "~A()" << endl; }};int main(){ A *p = new A(); p->ai(); delete p; cout << "-----------------------------------------" << endl; return 0;}
可以看到程序异常,无法调用相应的虚函数,原因就是因为,编译器本来就在构造函数里安插了给虚函数表指针赋值的代码,然后我们使用memset以后,虚函数表指针的值就变为0了,所以找不到虚函数表了,从而照成无法调用虚函数。
在相应的汇编代码中可以看到,系统给虚函数表指针赋值的代码在memset之前,所以使用了memset以后,就会造成无法调用虚函数的问题。
再来看一个例程:
#include "pch.h"#include <iostream>#include <stdio.h> #include <stdlib.h> using namespace std;class A{ public: int a; int b; int c;public: A() { memset(this, 0, sizeof(A)); cout << "A()" << endl; } virtual void ai() { cout << "A::ai()" << endl; } virtual ~A() { cout << "~A()" << endl; }};int main(){ A p; p.ai(); cout << "-----------------------------------------" << endl; return 0;}/*这种方式下调用虚函数是正常的,因为,在编译期间,函数的地址就已经确定了,所以,即使使用了memset,编译器也一样可以正常调用虚函数,这种调用方式不需要通过虚函数表的方式。我们也得出一个结论,那就是,多态这种问题,明显针对给指针或者引用的。*/
//程序正常
数据语义学
数据成员绑定时机
- 成员函数函数体的解析时机
#include "pch.h"#include <iostream> using namespace std;string var;class A{ public: int func(); //{ // return var;//正常 //}private: int var;};int A::func(){ return var;//正常}int myfunc(){ return var;//错误,var是string类型}int main(){ return 0;}/*在定义类结束以后,在成员函数func里,遇到var变量时,编译器实现会在本类当中查找,如果查找不到,才会在全局当中查找,如果想在类中使用全局的变量,用::即可。*/
结论:成员函数函数体解析是在整个类定义结束以后才解析的。
- 成员函数参数类型的确定时机
通过对比以下几个例程得到一些结论:
例一,程序异常
#include "pch.h"#include <iostream> using namespace std;typedef string mytype;class A{ public: void func(mytype value) //value是string类型 { m_value = value; //string类型给int类型赋值,报错 }private: typedef int mytype; mytype m_value; //m_value是int类型};int main(){ return 0;}
例二,程序异常
#include "pch.h"#include <iostream> using namespace std;typedef string mytype;class A{ public: void func(mytype value); //value是string类型private: typedef int mytype; mytype m_value; //m_value是int类型};void A::func(mytype value)//value 是int类型{ m_value = value;}int main(){ return 0;}
//例三,程序正常
#include "pch.h"#include <iostream> using namespace std;typedef string mytype;class A{ typedef int mytype;public: void func(mytype value); //value是int类型private: mytype m_value; //m_value是int类型};void A::func(mytype value)//value 是int类型{ m_value = value;}void func(mytype value)//全局的string类型{ string str = value;}int main(){ return 0;}
结论:成员函数的参数类型是在编译器最近一次遇到mytype时决定的,从上往下看,最近一次遇到mytype。
进程内存空间布局
数据成员的存取
- 静态成员变量的存取
#include "pch.h"#include <iostream>using namespace std;class A{ public: static int m_k;};int A::m_k = 0;int main(){ A obj; A *p = new A(); A::m_k = 10; obj.m_k = 20; p->m_k = 30; cout << p->m_k << endl; return 0;}/*总结:静态成员变量的访问方式:1:使用类名::2:使用对象名3:使用对象指针在这三种方式当中,对应的汇编代码其实是一致的。在生成相应的汇编代码时,因为静态成员变量可以理解为一种成员变量,所以,静态成员变量是和类名绑定在一起的,这样,如果在其它类当中,也有一个相同的静态成员变量的话,编译器就能够区分出,这个静态成员变量是和那个类挂钩的。其次,静态成员变量保存在可执行程序的数据段当中,在编译期间就确定了存储地址*/
对应的汇编代码:
- 非静态成员变量的存取
对于非静态成员变量的存取,是通过类对象首地址加偏移量来实现的。
1:无继承
#include "pch.h"#include <iostream>using namespace std;class A{ public: int m_i; int m_j;public: void func() { m_i = 1; //偏移量为0 m_j = 2;//偏移量为4 cout<<"void func()"<<endl; }};int main(){ A obj; obj.func(); return 0;}
//eax,eax+4
2:单继承(也是通过偏移量来访问)
#include "pch.h"#include "test.h"#include <iostream>using namespace std;class A{ public: int m_i; int m_j;public: void func() { m_i = 1; m_j = 2; cout<<"void func()"<<endl; }};class Boo:public A{ public: int m_i; int m_j;public: void func() { m_i = 3; m_j = 4; cout << "void func()" << endl; }};int main(){ Boo obj; obj.func(); cout << sizeof(obj) << endl; return 0;}
eax+8,eax+0ch
3:多继承(也是通过偏移量存取)
#include "pch.h"#include <iostream>using namespace std;class A{ public: int m_i; int m_j;public: void func() { m_i = 1; m_j = 2; cout<<"void func()"<<endl; }};class Boo{ public: int m_i1;};class Coo:public A,public Boo{ public: int m_i2; void func() { m_i2 = 10; cout << "void func()" << endl; }};int main(){ Coo obj; obj.func(); return 0;}
总结:普通的成员变量,不管是非继承,单继承还是多继承,访问的效率上都是一样的,当然,虚基类比较特殊,它访问普通的成员变量需要通过一下特殊手段,因此效率会低一些。
数据成员的布局
- 观察成员变量地址规律
#include "pch.h"#include "test.h"#include <iostream>using namespace std;class A{ public: int m_i = 0; int m_j = 0; int m_k = 0;public: int static m_x;//声明};int A::m_x = 10;//定义int main(){ A obj; cout << sizeof(obj) << endl; //打印变量的地址 printf("obj.m_i= %p\n", &obj.m_i); printf("obj.m_j= %p\n", &obj.m_j); printf("obj.m_k= %p\n", &obj.m_k); return 0;}/*总结:1:static变量不占用类对象内存空间,而是保存在数据段当中2:变量出现顺序和他们在类当中出现的顺序有关3:类对象的szieof值与类当中有几个public,private,protected无关*/
- 边界调整与字节对齐
#include "pch.h"#include "test.h"#include <iostream>using namespace std;class A{ public: int m_i = 0; int m_j = 0; char m_o; int m_k = 0;public: int static m_x;//声明};int A::m_x = 10;//定义int main(){ A obj; cout << sizeof(obj) << endl; //打印变量的地址 printf("obj.m_i= %p\n", &obj.m_i); printf("obj.m_j= %p\n", &obj.m_j); printf("obj.m_o= %p\n", &obj.m_o); printf("obj.m_k= %p\n", &obj.m_k); return 0;}
#include "pch.h"#include "test.h"#include <iostream>using namespace std;#pragma pack (1)//一字节对齐class A{ public: int m_i = 0; int m_j = 0; char m_o; int m_k = 0;public: int static m_x;//声明};#pragma pack() //取消一字节对齐int A::m_x = 10;//定义int main(){ A obj; cout << sizeof(obj) << endl; //打印变量的地址 printf("obj.m_i= %p\n", &obj.m_i); printf("obj.m_j= %p\n", &obj.m_j); printf("obj.m_o= %p\n", &obj.m_o); printf("obj.m_k= %p\n", &obj.m_k); return 0;}/*总结:1:字节对齐有利于提高程序执行效率*/
- 成员变量偏移值打印
#include "pch.h"#include <iostream>using namespace std;class A{ public: int m_i = 0; int m_j = 0; char m_o; int m_k = 0;};int main(){ A obj; //打印变量的地址 printf("&A::m_i = %d\n", &A::m_i); printf("&A::m_j = %d\n", &A::m_j); printf("&A::m_o = %d\n", &A::m_o); printf("&A::m_k = %d\n", &A::m_k); //使用成员变量指针,成员变量指针保存的是成员变量的偏移值 int A:: *p1 = &A::m_i; int A:: *p2 = &A::m_j; char A:: *p3 = &A::m_o; int A:: *p4 = &A::m_k; printf("p1 = %d\n", p1); printf("p2 = %d\n", p2); printf("p3 = %d\n", p3); printf("p4 = %d\n", p4); return 0;}
- 单一继承关系下的数据成员布局(父类和子类都不带虚函数)–父类和子类的内存布局
- 单一继承关系下,父类和子类都带虚函数时,子类对象的内存布局
- 单一继承关系下,父类不带虚函数,子类都带虚函数时,子类对象的内存布局
多重继承数据成员布局与this指针偏移话题
- 单一继承数据成员布局this指针偏移知识补充
- 多重继承且父类带虚函数的数据成员布局
虚基类与虚继承
- 虚基类/虚继承的提出(为了解决3层结构中孙子类重复包含爷爷类成员的问题)
- 虚基类探讨
- 两层结构的虚基类表5-8字节内容分析
- 三层结构的虚基类表1-4字节内容分析
成员变量地址,偏移与指针话题深入探讨
- 对象成员变量内存地址及其指针(对象的成员变量是有真正的地址的,这与变量的偏移值不同)
- 成员变量的偏移值及其指针(即:每个数据成员距离对象首地址的距离)
- 没有指向任何数据成员变量的指针(通过对象名/对象指针接成员变量指针的一种方式访问成员变量)
函数语义学
普通成员函数调用方式(编译器在形参上隐藏了一个this指针,性能上和调用全局函数差不多)
#include<iostream>#include<stdio.h>using namespace std;class A{ public: int m_i; public: void func(int abc) { m_i += abc; }};void func(A *p,int abc){ p->m_i += abc; return;}int main(void){ A obj; printf("%p\n",&A::func); obj.func(101); func(&obj,101); return 0;}/*一个是调用全局函数,一个是调用成员函数,这两个调用方式,其实在效率上是一样的,我们从汇编的角度看待这个问题。*/
在调用成员函数处设置断点,可以看到,obj为对象的首地址,被放入到了ecx寄存器当中,也就相当于把this指针放入到了ecx寄存器当中。
接着往下,
可以看到,ecx的值,最后还是给到了this,所以,后续m_i+=abc,其实还是利用了this.
所以,在调用obj.func(101)函数的时候,编译器,其实是这么看待的,obj(&A,101),
这不就是和调用全局函数的方式是一样的吗?所以说,调用成员函数其实是和调用全局函数是一样的。
来看看linux平台:
利用nm命令,,可以看到,在调用成员函数时,编译器对函数名还做了一些封装,其次,从打印出来的函数地址,也可知道,成员函数和全局函数的函数地址,是在编译期间就做好的。
虚函数,静态成员函数调用方式
class A{ public: int a; virtual void fun() { printf("%p\n", this); fun1();//直接调用 A::fun1();//走虚函数表}virtual void fun1() { printf("%p\n", this); }};int main(){ A obj; obj.fun(); A *obj1 = new A(); obj1->fun();}
-
虚函数调用方式
- 通过对象调用是直接调用,和调用全局函数性能一样
- 通过指针调用是走虚函数表
- 虚函数内调用另一个虚函数,如果用类名,则是采用全局函数调用方式,自己用函数名则是走虚函数表调用方式
-
静态成员函数调用方式
#include "pch.h"#include <iostream>using namespace std;class A{ public: int m_i;public: static void func(int abc) { cout << abc << endl; } void fun() { cout << "fun()" << endl; //m_i = 0; }};int main(void){ A obj; A *p = new A(); /*两种调用方式是一样的, 都会被编译器转化为和调用普通函数一样的方式*/ obj.func(11); p->func(12); /*0代表this指针,指向NULL*/ ((A *)0)->func(11); /*异常,因为this指针并不指向实际的类对象,而函数当中对成员变量操作*/ ((A *)0)->fun(); return 0;}/*总结:1:静态成员函数没有this指针2:不能在静态成员函数当中访问非静态成员变量3:静态成员函数不能是const,因为静态函数不属于 这个类的任何一个对象,使用const就表明不能修改调用该函数的对象。 前后矛盾了.4:静态成员函数不能是virtual5:可以用类对象的方式来调用静态成员函数,不过,最后编译器还是会转化为类名::成员函数名 的方式来调用。6:静态成员函数等同于全局函数,都是在编译期间确定函数地址的。*/
虚函数地址问题的vcall引入(为了解决多重继承中this指针调整问题)
静动态类型绑定
- 静态类型与动态类型
#include "pch.h"#include <iostream>#include <stack>using namespace std;class Base{ public: void func() { cout<<"void base::func()"<<endl; } virtual void virfun(int a = 10) { cout << "void base::virfun()" << endl; cout << a << endl; }};class Derive:public Base{ public: void func() { cout << "void Derive::func()" << endl; } void virfun(int a = 20) { cout << "void Derive::virfun()" << endl; cout << a << endl; }};int main(void){ Base base; //静态类型是Base Derive derive; //静态类型是Derive Base *p = new Derive(); //静态类型是Base,动态类型是Derive p = &base;//现在,p的动态类型是Base return 0;}/*定义:静态类型:定义对象时的类型,编译期间决定好的。动态类型:对象目前(可以改变)所指向的类型,有些时才决定的。一般来讲,只有指针和引用才有动态类型的说法,而且一般是父类指针指向子类对象。*/
- 静态绑定与动态绑定
(1)普通成员函数是静态绑定
(2)缺省参数一般是静态绑定
(3)虚函数是动态绑定 - 继承的非虚函数坑
- 虚函数的动态绑定
- 重新定义虚函数的缺省参数坑
#include "pch.h"#include <iostream>using namespace std;class Base{ public: void func() { cout<<"void base::func()"<<endl; } virtual void virfun(int a = 10) { cout << "void base::virfun()" << endl; cout << a << endl; }};class Derive:public Base{ public: void func() { cout << "void Derive::func()" << endl; } void virfun(int a = 20) { cout << "void Derive::virfun()" << endl; cout << a << endl; }};int main(void){ Base *p = new Derive(); p->virfun(); return 0;}
在虚函数当中,函数默认参数是静态绑定,所以即使调用的是子类的虚函数,但是函数默认值,是以父类的虚函数函数默认值为主。
- c++中的多态性(走虚函数表肯定是多态)
多态必须存在虚函数表,没有虚函数表就谈不上多态。
单继承下的虚函数特殊范例演示
多重继承虚函数深释,第二基类与虚析构必加
- 多继承下的虚函数
- 如何成功删除用第二基类指针new出来的子类对象
- 父类非虚析构函数时导致的内存泄漏演示
多继承第二基类虚函数支持与虚继承带虚函数
- 多重继承第二基类对虚函数支持的影响(this指针调整的作用)
- 虚继承下的虚函数
RTTI运行时类型识别与存储位置
- RTTI回顾
范例演示:
#include "pch.h"#include <iostream>#include <time.h >#include <stdio.h>#include <vector>using namespace std;class Base{ public: virtual void f() { cout << "Base::f()" << endl; } virtual void g() { cout << "Base::g()" << endl; } virtual void h() { cout << "Base::h()" << endl; } virtual ~Base() { }};class Derive :public Base { public: virtual void g() { cout << "Derive::g()" << endl; } void myselffunc() { } //只属于Derive的函数 virtual ~Derive() { }};int main(){ Base *pb = new Derive(); pb->g(); Derive myderive; Base &yb = myderive; yb.g(); cout << typeid(*pb).name() << endl; cout << typeid(yb).name() << endl; Derive *pderive = dynamic_cast<Derive *>(pb); //条件成立,说明pderive的实际类型为Derive if (pderive != NULL) { cout << "pb实际是一个Derive类型" << endl; pderive->myselffunc(); //调用自己专属函数 } return 0;}/*note: (1)c++运行时类型识别RTTI,要求父类中必须至少有一个虚函数;如果父类中没有虚函数,那么得到RTTI就不准确; (2)RTTI就可以在执行期间查询一个多态指针,或者多态引用的信息了; (3)RTTI能力靠typeid和dynamic_cast运算符来体现; */
- RTTI实现原理
- vptr,vtbl,RTTI的type_info信息,构造时机
函数调用,继承关系性能说
- 函数调用中编译器的循环代码优化
- 继承关系深度增加,开销也增加
- 继承关系深度增加,虚函数导致的开销增加
指向成员函数的指针以及vcall细节谈
-
指向成员函数的指针
- 指向成员函数的成员函数指针(这也体现了,为什么成员函数指针调用成员函数需要对象的介入,因为,成员函数的调用需要一个隐藏的this指针)
- 指向静态成员函数的函数指针(不需要this指针)
-
特殊代码分享(不通过对象也可以实现成员函数的调用()不需要this指针)
-
指向虚函数的成员函数指针及vcall谈
- 指向虚函数的成员函数指针(虚函数的调用也是需要this指针的)
- vcall(有时我们打印虚函数的地址不是真正的虚函数地址,而是vcall的地址,vcall里放着虚函数在虚函数表里的偏移值,引入vcall是编译器的一种做法)
-
vcall在继承关系中的体现(有虚函数)
inline函数扩展细节(是否真的内联取决于编译器)
- 形参被对应的实参取代
- 局部变量的引入(带来了性能的消耗)
- inline失败情形(例如:递归)
对象构造语义学
继承体系下的对象构造顺序
- 对象的构造顺序(从父到子,析构则相反)
- 构造函数里调用虚函数(直接调用,不是走虚函数表)
对象复制语义学与析构函数语义学
-
对象的默认复制行为(简单的按值拷贝)
-
拷贝赋值运算符与拷贝构造函数
-
如何禁止对象的拷贝构造和赋值
- 声明为private,只写声明,不写函数体
- c++11提供的delete关键字
-
析构函数语言(编译器默认提供析构函数的几种情况)
- 在继承体系中,父类带析构函数,如果子类不带析构函数,编译器会默认合成一个
- 在一个类中,如果带有一个类类型的成员变量,并且这个成员变量带有析构函数。
局部对象,全局对象的构造和析构
- 局部对象的构造和析构(建议现用现定义,减少不必要的构造和析构)
- 全局对象的构造和析构(main函数执行前就开始构造了,main函数结束以后,执行析构函数)
局部静态对象,对象数组的构造析构和内存分配
- 局部静态对象的构造和析构(一个局部的静态对象,如果多次使用,则只会构造一次,编译器采取了标记的方法,以便防止静态的局部对象构造多次。)
- 对象数组的构造和析构(静态对象数组到底在编译时,分配了多少给字节,这并不取决于你的数组有几个元素,而是取决于你的程序干了什么事,这是编译器的一个智能做法。)
new与delete高级话题
-
new/delete的认识
- malloc0个字节的话题
-
重载new/delete
-
new/delete细节探讨
-
new一个类加括号和不加括号的区别
- 类是空时无区别
- 类A中有成员变量则:带括号的初始化会把一些和成员变量有关的内存清0,但不是整个对象的内存全部清0
- 类有构造函数 得到的结果一样
-
new干了什么
- 调用operator new(malloc)
- 调用了类的构造函数
-
delete干了什么
- 调用了·类的析构函数
- 调用了operator delete(free)
-
-
重载operator new/operator delete
-
-
嵌入式指针与内存池
临时性对象的详细探讨
模板实例化语义学
模板及其实例化详细分析
-
函数模板
-
类模板的实例化分析
- 类模板中的枚举类型
- 类模板中的静态成员变量
- 类模板的实例化
- 成员函数的实例化
-
多个源文件中使用类模板
炫技写法
-
不能被继承的类
- c++11的final
- 友元函数+虚继承
-
类外调用私有虚函数(一个private的虚函数可以调用吗?可以使用特殊写法进行调用)
发表评论
最新留言
关于作者
