C++对象模型
对象
对象模型分类
在C++中,成员数据(class data member)有两种:static和nonstatic,成员函数(class member function)有三种:static、nonstatic和virtual。
-
简单对象模型
class Point
{
public:
Point(float xval);
virtual ~Point();
float x() const;
static int PointCount();
protected:
virtual ostream& print(ostream& os) const;
float _x;
static int _point_count;
}; -
表格驱动模型
-
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© << endl;
C obj;
obj.funA();
obj.funB();
obj.funC();
}
结论:如果派生类只继承一个基类,那么这个基类的地址和派生类的地址相同。
如果一个类,同时继承多个基类,那么这个子类的对象和它继承顺序的第一个基类的地址相同。
范例二:
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© << 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
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; }// 此行的含义,参考下边的 “使用注意5”
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); -
参数的初始化
-
函数返回值
程序的优化
成员初始化列表
https://blog.csdn.net/qq_38158479/article/details/106888318
-
何时必须使用成员初始化列表
- 类中含有引用类型的成员
- 类中含有const类型成员
- 一个类继承于另一个类,并且继承的这个类中有构造函数,且构造函数带有参数时
- 一个类,含有一个类类型成员,并且这个类类型成员有构造函数(带参的)
-
使用初始化列表的优势(对于类中含有类类型成员,可以减少一些构造函数或者赋值运算符的调用以提高程序运行效率)
-
初始化列表细节探究
- 初始化列表中的代码可以看作是被编译器安插在构造函数中的
- 初始化列表中的代码是在构造函数的函数体之前被执行的
- 初始化列表成员变量的初始化顺序看的是变量在类中定义的顺序,而不是看在初始化列表中出现的顺序
虚函数
虚函数表指针位置(对象模型的开头)
单继承情况下父类和子类虚函数表指针和虚函数表分析
- 子类中有覆盖父类虚函数时情况分析
- 子类中没有覆盖父类虚函数时情况分析
多继承情况下父类和子类虚函数表指针和虚函数表分析
- 子类中有覆盖父类或者多个父类虚函数时情况分析
- 子类中没有覆盖父类虚函数时情况分析
分析虚函数表的工具与vptr,vtbl创建时机
- 辅助工具(查看虚函数表指针专用工具)
- vptr与vtbl都是在编译期间创建起来的,而给vptr赋值是在运行期间,即,生成对象时,会调用构造函数进行赋值。
单纯的类不纯时引发的虚函数调用问题(memset和memcpy问题)
- 单纯的类:只有一些简单的成员变量
- 不纯的类:指类中有一些隐藏的变量,例如虚函数表指针(有虚函数时存在),虚基类表指针
- 涉及静态联编和动态联编概念
数据语义学
数据成员绑定时机
- 成员函数函数体的解析时机
- 成员函数参数类型的确定时机
进程内存空间布局
数据成员的存取
- 静态成员变量的存取
- 非静态成员变量的存取
数据成员的布局
- 单一继承关系下的数据成员布局(父类和子类都不带虚函数)–父类和子类的内存布局
- 单一继承关系下,父类和子类都带虚函数时,子类对象的内存布局
- 单一继承关系下,父类不带虚函数,子类都带虚函数时,子类对象的内存布局
多重继承数据成员布局与this指针偏移话题
- 子主题 1
- 子主题 2
- 子主题 3
- 子主题 4
虚基类与虚继承
- 虚基类/虚继承的提出(为了解决3层结构中孙子类重复包含爷爷类成员的问题)
- 虚基类探讨
- 两层结构的虚基类表5-8字节内容分析
- 三层结构的虚基类表1-4字节内容分析
成员变量地址,偏移与指针话题深入探讨
- 对象成员变量内存地址及其指针(对象的成员变量是有真正的地址的,这与变量的偏移值不同)
- 成员变量的偏移值及其指针(即:每个数据成员距离对象首地址的距离)
- 没有指向任何数据成员变量的指针(通过对象名/对象指针接成员变量指针的一种方式访问成员变量)
函数语义学
普通成员函数调用方式(编译器在形参上隐藏了一个this指针,性能上和调用全局函数差不多)
虚函数,静态成员函数调用方式
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();
}
-
虚函数调用方式
- 通过对象调用是直接调用,和调用全局函数性能一样
- 通过指针调用是走虚函数表
- 虚函数内调用另一个虚函数,如果用类名,则是采用全局函数调用方式,自己用函数名则是走虚函数表调用方式
-
静态成员函数调用方式
虚函数地址问题的vcall引入(为了解决多重继承中this指针调整问题)
静动态类型绑定
- 静态类型与动态类型
- 静态绑定与动态绑定
- 继承的非虚函数坑
- 虚函数的动态绑定
- 重新定义虚函数的缺省参数坑
- c++中的多态性(走虚函数表肯定是多态)
单继承下的虚函数特殊范例演示
多重继承虚函数深释,第二基类与虚析构必加
- 多继承下的虚函数
- 如何成功删除用第二基类指针new出来的子类对象
- 父类非虚析构函数时导致的内存泄漏演示
多继承第二基类虚函数支持与虚继承带虚函数
- 多重继承第二基类对虚函数支持的影响(this指针调整的作用)
- 虚继承下的虚函数
RTTI运行时类型识别与存储位置
- 子主题 1
- 子主题 2
- 子主题 3
函数调用,继承关系性能说
- 函数调用中编译器的循环代码优化
- 继承关系深度增加,开销也增加
- 继承关系深度增加,虚函数导致的开销增加
指向成员函数的指针以及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的虚函数可以调用吗?可以使用特殊写法进行调用)