c++对象模型探索
- 深入理解面向对象:
- c++类对象模型:
- 类中的成员:
- 对象的内存大小:
- 类对象内存的组成:
- 不在对象内存中存放的成员:
- 类与类对象的内存分配:
- 数据部分和代码部分:
- 类对象占用的内存的分配:
- 类对象成员变量:
- 深入c++对象内存布局:
- 指向type_info的指针:
- 虚基类表的内容与虚函数表完全不同:
- *类对象的内存布局:
- 单一类,含有虚函数、成员变量时,内存布局的分析:
- 继承下,对象内存布局分析:
- 单继承:子类单一继承父类
- 多继承:子类继承自多个父类
- 虚继承:单一虚继承、菱形虚继承
- 对象内存布局:(参考文章)
- 虚函数表、虚函数表指针:
- 概念:
- 虚函数表:
- 虚函数表指针:
- *举例分析如下:
- 单继承场景:
- 多继承场景:
- 虚函数表的设计:
- 多重继承中虚函数表的分析:
- 虚函数表的创建时机、程序中的位置:
- 单纯的类不纯时,引发的虚函数调用问题:
- 静态联编、动态联编:
- 不同对象<->不同调用时机:
- 类外调用私有虚成员函数:
- 具有父子关系的两个类,不要在父类构造函数和析构函数中调用虚函数:
- this指针的调整:
- 构造函数语义:
- 默认构造函数语义:
- 拷贝构造函数语义:
- 移动构造函数语义:
- 程序转换语义:
- 程序的优化:
- 开发者角度 --> 编译器角度
- 函数内部构造类对象并返回:
- 初始化列表,来初始化成员变量(保持这种写法的习惯!!!):
- **必须使用初始化列表的场景**:
- 说明:
- 数据语义学:
- 数据成员的绑定时机:
- 数据成员的布局:
- 成员变量的布局规律:
- 边界调整与字节对齐:
- 非静态成员变量与类对象的首地址偏移值:
- 数据成员的存取:
- “单一继承且父类有虚函数”的数据成员布局(非虚继承):
- 多重直接继承,且父类都有虚函数的数据成员布局(非虚继承):
- 函数语义学:
- 普通成员函数(加this参数后,等价于全局函数):
- 虚成员函数的调用方法:
- 调用方法:
- 虚函数地址问题的vcall引入:
- 静态成员函数调用方法:
- 调用方法:
- 总结:
- 静态、动态类型,静态、动态绑定:
- 静态、动态类型:
- 静态、动态绑定:
- 继承中的成员函数绑定:
- **“重写”虚函数的缺省参数:**
- c++中的多态性:
- 事实:
- 代码实现上:
- 表现形式上:
- 多重继承下的虚函数、第二继承与虚析构:
- 多重继承下的虚函数:
- 如何成功删除第二基类指针new出来的子类对象:
- delete指向子类对象的父类指针时:
- 对象指针this:
- this指针调整:
- RTTI运行时类型识别:
- 继承关系的深度增加:
- 成员函数指针:
- inline函数及其扩展细节(不推荐使用):
- 对象构造函数语义:
- 继承体系下对象的构造:
- 对象复制语义学与析构函数语义学:
- 局部对象、全局对象的构造和析构:
- 静态局部对象、对象数组构造、析构和内存分配:
- 临时性对象:
- 模板实例化语义学:
- 函数模板:
- 类模板:
- 虚函数的实例化:
- 显示实例化:
- 类模板的显示实例化:
- 实例化单独的成员函数:
深入理解面向对象:
- 封装:隐藏在内部的实现,
- 对成员变量和成员函数进行封装,并不会带来任何额外的空间开销和执行期效率的影响。
- 只有在虚函数机制(支持执行期绑定,实现了多态)、虚继承(多重继承中保证基类在子类中拥有唯一的实例),才会体现出封装额外的成本。
- 继承:复用现有的代码。
- 多态:改写对象行为(虚函数表)。
c++类对象模型:
类中的成员:
两种成员变量:non_static、static;三种成员函数:non_static、static、virtual。
对象的内存大小:
类对象内存的组成:
-
所有非静态数据成员的大小;
-
由内存对齐而填补的内存大小;
-
为支持virtual成员产生的额外负担,即虚函数表。
每个含有虚函数的类,其产生的对象都含有4bytes的虚函数表指针,用来指向虚函数地址。
总结:对象的地址是类中第一个非静态成员变量的地址,空类也占用1bytes(编译器隐含的增加1byte的占位成员)主因是C++要求每个对象实例在内存中都要有独一无二的地址。
不在对象内存中存放的成员:
- 静态成员变量属于类,且在内存空间的全局区(与全局变量存放在一起)且只有一个副本,故不在对象内存中。
- 成员函数是分开存储的,且在内存空间的代码区(与普通函数存放在一起)且只有一个副本,故不在对象内存中。
类与类对象的内存分配:
数据部分和代码部分:
用类去定义对象时,系统会为每一个对象分配存储空间。
- 用一段空间来存放这个共同的函数代码段,在调用各对象的函数时,都去调用这个公用的函数代码。
每个对象所占用的存储空间只是该对象的数据部分(成员变量(静态变量除外)、虚函数表指针(4字节)和虚基类表指针也属于数据部分)所占用的存储空间,而不包括函数代码所占用的存储空间。
-
普通成员函数和静态成员函数,都不占用对象的存储空间,都存储在代码区。
-
在编译器处理后,成员变量和成员函数是分离的,成员函数的大小不在类的对象里面,且被多个对象共享。
优点:这种成员函数和成员变量分离的设计,存取效率高。
类的非静态类成员函数都隐含了一个指向类对象的指针型参数(即this指针),因而只有类对象才能调用(此时this指针有实值)。
类对象占用的内存的分配:
类内部的成员变量:
- 普通的变量:是要占用内存的,但是要注意对齐原则(这点和struct类型很相似)。
- static修饰的静态变量:不占用内存,原因是编译器将其放在全局变量区(所有对象共用一份),不计入类的内存空间。
类内部的成员函数:
- 普通函数:不占用内存。
- 虚函数:要占用4个字节(32位系统,x86)/8个字节(64位系统,x64),虚函数的入口地址保存在虚函数表中。一个类对象只会含有虚函数表指针,故所占用的地址是不变的(和虚函数的个数无关)。
实例化不同对象时,只给数据分配空间,各个对象调用函数时都跳转到(内联函数例外)函数在代码区的入口执行,可以节省拷贝多份代码的空间。
内联函数(声明和定义都要加inline)也是存放在代码区:
- 内联函数在被调用时,编译器会用内联函数的代码复制插入到函数调用点,避免了函数跳转和保护现场的开销。
- 若不用inline声明,在调用该函数时,流程会转去函数代码段的入口地址,在执行完该函数代码段后,流程会返回函数调用点。
- inline与成员函数是否占用对象的存储空间无关,inline函数和类成员函数(non_static、static)都存储在代码区。
类对象成员变量:
成员变量的内存地址:成员变量拥有真正的内存地址,可通过一个指针来访问和修改成员变量的值。
成员变量指针:
-
&A::m_val
,可以用来打印成员变量的偏移量。int A::*ptr_a = &A::m_val
,成员变量的指针变量ptr_a中保存的只是成员变量的偏移值。 -
成员变量指针、不指向任何成员变量的成员变量指针。
- 成员变量指针指向类中第一个成员变量的地址是
0x00000000
。 - 未经初始化的成员变量指针,地址是
0xcccccccc
。 - 给成员变量指针 0 或者 nullptr,则编译器会给其地址为
0xffffffff
。
- 成员变量指针指向类中第一个成员变量的地址是
深入c++对象内存布局:
指向type_info的指针:
每一个虚函数表前都有一个指针指向type_info,负责对RTTI(runtime type interpret)的支持。
虚基类表的内容与虚函数表完全不同:
-
虚函数表:含有虚函数或其父类含有虚函数的类,编译器都会为其添加一个虚函数表
vptr
(在类对象内存空间会有一个指针指向该虚函数表vptr),用来存放虚函数的地址。注:每个虚函数表末尾为nullptr。 -
虚基类表:虚继承产生虚基类表
vbptr
。第一个条目:虚基类表指针 vbptr 所在地址,相对于该类内存首地址的偏移值。
第二、第三…个条目:依次为该类的最左虚继承父类、次左虚继承父类…的vptr 或 成员变量的位置,相对于虚基类表指针的偏移值。
*类对象的内存布局:
单一类,含有虚函数、成员变量时,内存布局的分析:
#include <iostream>
using namespace std;
class Base
{
public:
Base(int i) : baseI(i) { cout << "Base::Base(int i)" << endl; };
virtual ~Base() { cout << "virtual ~Base()" << endl; }
virtual void print(void) { cout << "virtual void Base::print()" << endl; }
int getl() { return baseI; }
static int countl() { return (++baseS); }
private:
int baseI;
static int baseS;
};
int Base::baseS = 0;
int main()
{
Base b(1000);
long* vptrAddress = (long*)(&b);
cout << "虚函数指针(vptr)的地址是:" << vptrAddress << endl;
cout << "通过Base类对象b,找到虚函数表存放的第一个虚函数Base::~Base()的地址:" << (long *)(*(long*)(&b)) << endl;
typedef void(*Fun)(void);
Fun virfunc_2 = (Fun)(*((long*)((long*)(*(long*)(&b)) + 1)));
cout << "第二个虚函数的地址是:" << (long*)((long*)(*(long*)(&b)) + 1) << endl;
cout << "通过Base类对象b,找到虚函数表存放的虚函数Base::setl()的地址并调用:";
virfunc_2();
return 0;
}
继承下,对象内存布局分析:
单继承:子类单一继承父类
#include <iostream>
using namespace std;
class Base
{
public:
Base(int i) : baseI(i) { cout << "Base::Base(int i)" << endl; };
virtual void print() { cout << "virtual void Base::print()" << endl; }
virtual ~Base() { cout << "virtual ~Base()" << endl; }
int getl() { return baseI; }
static int countl() { return (++baseS); }
private:
int baseI;
static int baseS;
};
int Base::baseS = 0;
class Derive : public Base
{
public:
Derive(int d) : Base(1000), DeriveI(d) { cout << "Derive::Derive(int i)" << endl; };
// overwrite父类虚函数:(override用来指明,这个虚函数是对父类虚函数的重写)
virtual void print() override { cout << "virtual void Derive::print()" << endl; }
// Derive声明的新的虚函数:
virtual void Derive_print() { cout << "virtual void Derive::Derive_print()" << endl; }
virtual ~Derive() { cout << "virtual ~Derive()" << endl; }
private:
int DeriveI;
};
int main()
{
typedef void(*Fun)(void);
Derive d(2000);
// --[0]
{
cout << "[0] Base::vptr\t地址:\t" << (long*)(&d) << endl;
// -vprt[0]
cout << " [0]\t地址:\t" << *((long*)*((long*)(&d))) << "\t==>\t";
Fun fun_0 = (Fun)(*((long*)*((long*)(&d))));
fun_0();
// -vprt[1]析构函数无法通过地址调用(编译器正常调用析构函数时,会传入this指针,这里直接调用并没有传入this指针),故手动输出
cout << " [1]\t" << "virtual Derive::~Derive()" << endl;
// -vprt[2]
cout << " [2]\t地址:\t" << *((long*)*((long*)(&d)) + 2) << "\t==>\t";
Fun fun_2 = (Fun)(*((long*)*((long*)(&d)) + 2));
fun_2();
}
// --[1]
{
cout << "[1] Base::baseI\t地址:\t" << (long*)(&d) + 1 << "\t:\t" << *(long*)((long*)(&d) + 1) << endl;
}
// --[2]
{
cout << "[2] Derive::DeriveI\t地址:\t" << (long*)(&d) + 2 << "\t:\t" << *(long*)((long*)(&d) + 2) << endl;
}
return 0;
}
多继承:子类继承自多个父类
分析子类overwrite父类虚函数、子类定义了新的虚函数,子类对象的内存布局。
非菱形继承:
- 子类的虚函数(非overwrite的虚函数)被放在声明的第一个基类的虚函数表中;
- overwrite时,所有父类的print()函数都会被子类的print()函数覆盖(保证了父类指针指向子类对象时,总能调用真正的函数);
- 内存布局中,父类按照其声明顺序排列;
#include <iostream>
using namespace std;
class Base1
{
public:
Base1(int i) :base1I(i) { cout << "Base1::Base1(int i)" << endl; }
virtual ~Base1() { cout << "virtual Base1::~Base1()" << endl; }
virtual void print(void) { cout << "virtual void Base1::print()" << endl; }
private:
int base1I;
};
class Base2
{
public:
Base2(int i) :base2I(i) { cout << "Base2::Base2(int i)" << endl; };
virtual ~Base2() { cout << "virtual Base2::~Base2()" << endl; }
virtual void print(void) { cout << "virtual void Base2::print()" << endl; }
virtual void others() { cout << "virtual void Base2::others()" << endl; }
private:
int base2I;
};
class Derive_multiBase :public Base1, public Base2
{
public:
Derive_multiBase(int d) :Base1(1000), Base2(2000), derive_multiBaseI(d)
{ cout << "Derive_multiBase::Derive_multiBase(int d)" << endl; };
virtual ~Derive_multiBase() { cout << "virtual Derive_multiBase::~Derive_multiBase()" << endl; }
virtual void print(void) override { cout << "virtual void Derive_multiBase::print" << endl; }
virtual void Derive_print() { cout << "virtual void Derive_multiBase::Derive_print" << endl; }
private:
int derive_multiBaseI;
};
int main()
{
typedef void(*Fun)(void);
Derive_multiBase d(3000);
cout << "........................................." << endl;
// --Base1::
{
cout << "[0] Base1::vptr\t地址: " << (long*)(&d) << endl;
// -vptr[0]析构函数无法通过地址调用(调用构造/析构函数时,编译器会自动插入一个this指针,用作初始化/释放),故手动输出
cout << "\t[0] " << "virtual Derive_multiBase::~Derive_multiBase()" << endl;
// -vptr[1]
cout << "\t[1] 地址: " << *((long*)(*(long*)(&d)) + 1) << " ";
Fun fun_1 = (Fun)(*((int *)*((long*)(&d)) + 1));
fun_1();
// -vptr[2]
cout << "\t[2] 地址: " << *((long*)(*((long*)(&d))) + 2) << " ";
Fun fun_2 = (Fun)*((long*)*((long*)(&d)) + 2);
fun_2();
// Base1::base1I
cout << "Base1::base1I: " << *(long*)((long*)(&d) + 1) << "\t地址: " << (long*)(&d) + 1 << endl;
}
// --Base2::
{
cout << "[2] Base2::vptr\t地址: " << (long*)(&d) + 2 << endl;
// vptr[0]析构函数无法通过地址调用,故手动输出
cout << "\t[0] " << "virtual Derive_multiBase::~Derive_multiBase()" << endl;
// vptr[1]
cout << "\t[1] 地址: " << *(long*)((long*)(*((long*)(&d) + 2)) + 1) << " ";
Fun fun_1 = (Fun)*(long*)((long*)(*((long*)(&d) + 2)) + 1);
fun_1();
// vptr[2]
cout << "\t[1] 地址: " << *(long*)((long*)(*((long*)(&d) + 2)) + 2) << " ";
Fun fun_2 = (Fun)*(long*)((long*)(*((long*)(&d) + 2)) + 2);
fun_2();
cout << "Base2::base2I: " << *(long*)((long*)(&d) + 3) << "\t地址: " << (long*)(&d) + 3 << endl;
}
// --Derive_multiBase::
{
cout << "Derive_multiBase::derive_multiBaseI: " << *(long*)((long*)(&d) + 4) << "\t地址: " << (long*)(&d) + 4 << endl;
}
cout << "........................................." << endl;
return 0;
}
菱形继承:
通过虚继承来解决,因孙子类中存在两个相同的祖先类,导致“二义性”的问题(需要通过引入“虚继承”来解决)。
虚继承:单一虚继承、菱形虚继承
虚继承而来的子类,会生成一个隐藏的虚基类指针(vbptr
)。
- 一个类的虚基类指针指向虚基类表(与虚函数表一样):由多个条目组成,存放的是偏移值。
- 第一个条目存放虚基类表指针 vbptr 所在地址到该类内存首地址的偏移值,这个偏移值为 0(类没有vptr)或 -4(类有虚函数,即有vptr)。
简单虚继承:
#include <iostream>
using namespace std;
class Base
{
public:
Base(int i = 1) :iBase(i) {}
virtual void f_1() { cout << "Base::f_1()" << endl; }
virtual void Base_f() { cout << "Base::Base_f()" << endl; }
private:
int iBase;
};
class Derive : virtual public Base
{
public:
Derive(int i = 20) : Base(10), iDerive(i) {}
virtual void f_1() override { cout << "Derive::f_1()" << endl; }
virtual void f_2() { cout << "Derive::f_2()" << endl; }
virtual void Derive_f() { cout << "Derive::Derive_f()" << endl; }
private:
int iDerive;
};
int main()
{
typedef void(*Func)(void);
Derive derive;
cout << "Derive对象内存大小为:" << sizeof(derive) << endl;
// --[0]
{
// --取得Derive的虚函数表指针所在的地址:
cout << "[0] Derive::vptr的地址: " << (long*)(&derive) << endl;
// 通过虚函数表指针,调用Derive::vptr中的虚函数:
for (int i = 0; i < 2; ++i)
{
cout << "\t[" << i << "] 地址: " << *((long*)(*(long*)(&derive)) + i) << "\t";
Func fun = (Func)(*((long*)(*(long*)(&derive)) + i));
fun();
}
}
// --[1]
{
cout << "[1] vbptr的地址:" << (long*)(&derive) + 1 << endl; //虚表指针的地址
// 输出虚基类指针条目所指的内容
string implications[2] = { "the offset from current position to the first address of object", "the offset from current position to parent class's vptr" };
for (int i = 0; i < 2; i++)
{
cout << "\t[" << i << "] " << implications[i] << ": " << *(long*)((long*)(*((long*)(&derive) + 1)) + i) << endl;
}
}
// --[2]
{
cout << "[2] Derive::iDerive的地址: " << (long*)(&derive) + 2 << "\t" << *(long*)((long*)(&derive) + 2) << endl;
}
// --[3]
{
cout << "[3] 地址: " << (long*)(&derive) + 3 << "\t值=" << *(long*)((long*)(&derive) + 3) << endl;
}
// --[4]
{
cout << "[4] Base::vptr的地址:" << (long*)(&derive) + 4 << endl;
//输出Base::vptr中的虚函数
for (int i = 0; i < 2; ++i)
{
cout << "\t[" << i << "]\t地址: " << *((int *)(*((int *)(&derive) + 4)) + i) << "\t";
Func fun = (Func)(*((int *)(*((int *)(&derive) + 4)) + i));
fun();
}
}
// --[5]
{
cout << "[5] Base::iBase的地址: " << (int *)(&derive) + 5 << "\t" << *(int*)((int *)(&derive) + 5) << endl;
}
return 0;
}
菱形虚继承:
#include<iostream>
using namespace std;
class B
{
public:
B(int i = 10) :ib(i) {}
virtual void f() { cout << "B::f()" << endl; }
virtual void Bf() { cout << "B::Bf()" << endl; }
private:
int ib;
};
class B1 : virtual public B
{
public:
B1(int i = 100) :ib1(i) {}
virtual void f() override { cout << "B1::f()" << endl; }
virtual void f1() { cout << "B1::f1()" << endl; }
virtual void Bf1() { cout << "B1::Bf1()" << endl; }
private:
int ib1;
};
class B2 : virtual public B
{
public:
B2(int i = 1000) :ib2(i) {}
virtual void f() override { cout << "B2::f()" << endl; }
virtual void f2() { cout << "B2::f2()" << endl; }
virtual void Bf2() { cout << "B2::Bf2()" << endl; }
public:
int ib2;
};
class D : public B1, public B2
{
public:
D(int i = 10000) :id(i) {}
virtual void f() override { cout << "D::f()" << endl; }
virtual void f1() override { cout << "D::f1()" << endl; }
virtual void f2() override { cout << "D::f2()" << endl; }
virtual void Df() { cout << "D::Df()" << endl; }
private:
int id;
};
int main()
{
typedef void(*Fun)(void);
D d;
cout << "D对象内存大小为:" << sizeof(d) << endl;
// ---B1
{
// --[0]
{
// B1的虚函数表指针的地址:
cout << "[0] B1::vptr的地址: " << (long*)(&d) << endl;
// 虚函数表指针B1::vptr指向的虚函数表中的虚函数的地址,并调用:
for (int i = 0; i < 3; ++i)
{
cout << "\t[" << i << "] 地址:\t" << *((long*)(*(long*)(&d)) + i) << "\t";
Fun fun = (Fun)(*((long*)(*(long*)(&d)) + i));
fun();
}
}
// --[1]
{
// B1虚基类表指针的地址:
cout << "[1] B1::vbptr的地址: " << (long*)(&d) + 1 << endl;
// B1虚基类指针指向的虚基类表中,条目所指的内容:
long** vftab = (long**)(&d);
for (int i = 0; i < 2; i++)
{
//等价于cout << "\t[" << i << "]\t" << *(long*)((long*)(*((long*)(&d) + 1)) + i) << endl;
cout << "\t[" << i << "]\t" << vftab[1][i] << endl;
}
}
// --[2]
{
cout << "[2] B1::ib1的地址: " << (long*)(&d) + 2 << "\t" << *(long*)((long*)(&d) + 2) << endl;
}
}
// ---B2
{
// --[3]
{
// B2的虚函数表指针的地址:
cout << "[3] B2::vptr的地址:" << (long*)(&d) + 3 << endl;
// 虚函数表指针B2::vptr指向的虚函数表中的虚函数的地址,并调用:
for (int i = 0; i < 2; ++i)
{
cout << "\t[" << i << "] 地址:\t" << *((long*)(*((long*)(&d) + 3)) + i) << "\t";
Fun fun = (Fun)*((long*)(*((long*)(&d) + 3)) + i);
fun();
}
}
// --[4]
{
// B2虚基类表指针的地址:
cout << "[4] B2::vbptr的地址:\t" << (long*)(&d) + 4 << endl;
// B2虚基类指针指向的虚基类表中,条目所指的内容:
for (int i = 0; i < 2; i++)
{
cout << "\t[" << i << "]\t" << *(long*)((long*)(*((long*)(&d) + 4)) + i) << endl;
}
}
// --[5]
{
cout << "[2] B2::ib2的地址: " << (long*)(&d) + 5 << "\t" << *(long*)((long*)(&d) + 5) << endl;
}
}
// ---D
{
// --[6]
cout << "[6] D::id的地址: " << (long*)(&d) + 6 << "\t" << *(long*)((long*)(&d) + 6) << endl;
}
// 0x00000000
{
// --[7]
cout << "[7] 地址: " << (long*)(&d) + 7 << "\t值=" << *(long*)((long*)(&d) + 7) << endl;
}
// ---B间接父类(祖先类)
{
// --[8]
{
// B的虚函数表指针的地址:
cout << "[8] B::vptr的地址: " << (long*)(&d) + 8 << endl;
// 虚函数表指针B::vptr指向的虚函数表中的虚函数的地址,并调用:
for (int i = 0; i < 2; ++i)
{
cout << "\t[" << i << "] 地址:\t" << *((long*)(*((long*)(&d) + 8)) + i) << "\t";
Fun fun = (Fun)*((long*)(*((long*)(&d) + 8)) + i);
fun();
}
}
// --[9]
{
cout << "[9] B::id地址: " << (long*)(&d) + 9 << "\t" << *(long*)((long*)(&d) + 9) << endl;
}
}
return 0;
}
对象内存布局:(参考文章)
参考文章1、参考文章2。
虚函数表、虚函数表指针:
概念:
虚函数表:
(每个含有虚函数的类均有一个虚函数表,但多继承中子类的虚函数表用的是继承顺序中首个继承的类的虚函数表,且根据继承的父类的个数含有不同的虚函数表指针) 。
- 如果类中定义了虚函数,则会用虚函数指针来指向这些虚函数(包含虚函数、纯虚函数、虚析构函数等),且虚函数指针被存放在一个表中,即是虚函数表(virtual table)(虚函数表中顺序记录着每个虚函数的地址,子类如果重写父类的虚函数,则会替换掉该位置的虚函数指针)。
- 虚函数表是属于类的(跟着类走,而不是类对象),但是虚函数表指针是属于类对象的(每个含有虚函数的类的类对象都有一个虚函数表指针,但指向同一个虚函数表)。
- 虚函数表,一般保存在最后生成的可执行文件中,在程序执行的时候载入内存中。
虚函数表指针:
(每个含有虚函数的类的类对象都有一个虚函数表指针,但指向同一个虚函数表)。
- 虚函数表指针
vptr
:用于指向虚函数表的首地址,且该类的每个对象都会增加 4字节或者8字节 用来存放虚函数指针(可以看作隐藏的成员变量,属于类对象)。 - 虚函数指针,一般都在对象内存布局的首位(为了保证多重继承时,能以最高效率找到虚函数表,并完成虚函数的调用)。
- 一个对象,如果所属的类有多个基类,则有多个虚函数表指针分别存放在内存对象空间中。
*举例分析如下:
当类对象要调用虚函数时,会通过虚函数表指针找到类的虚函数表,通过类的虚函数表就能够调用类的虚函数。
单继承场景:
#include <iostream>
using namespace std;
class Base
{
public:
virtual void func1() { cout << "Base::func1()" << endl; }
virtual void func2() { cout << "Base::func2()" << endl; }
virtual void func3() { cout << "Base::func3()" << endl; }
};
class Derive : public Base
{
public:
virtual void func3() override { cout << "Derive::func3()" << endl; }
};
int main()
{
/* 手动调用子类的虚函数表中的虚函数: */
Derive* derive = new Derive();
long* derive_first_ptr = (long*)(derive);
// 将*derive_vftable_ptr的值转换为16进制,即虚函数表指针指向的位置
long* derive_vftable_ptr = (long*)(*derive_first_ptr);
cout << "derive_first_address = " << derive << endl;
cout << "derive_first_address = " << derive_first_ptr << endl;
printf("derive_vftable_ptr = %p\n", *derive_vftable_ptr);
for (int i = 0; i < 3; ++i)
{
printf("derive_vftable_ptr[%d] = %p\n", i, derive_vftable_ptr[i]);
}
// 定义一个函数的指针类型:
typedef void(*Func)(void);
// 依次调用Derive类对象derive的虚函数表中的虚函数
for (int i = 0; i < 3; ++i)
{
Func f = (Func)(derive_vftable_ptr[i]);
f();
}
cout << "..........................\n";
/* 手动调用父类的虚函数表中的虚函数: */
Base* base = new Base();
long* base_first_ptr = (long*)(base);
long* base_vftable_ptr = (long*)(*base_first_ptr);
cout << "base_first_address = " << base << endl;
cout << "base_first_address = " << base_first_ptr << endl;
printf("base_vftable_ptr = %p\n", *base_vftable_ptr);
for (int i = 0; i < 3; ++i)
{
printf("base_vftable_ptr[%d] = %p\n", i, base_vftable_ptr[i]);
}
// 定义一个函数的指针类型:
typedef void(*Func)(void);
// 依次调用Derive类对象derive的虚函数表中的虚函数
for (int i = 0; i < 3; ++i)
{
Func f = (Func)(base_vftable_ptr[i]);
f();
}
return 0;
}
多继承场景:
虚函数表的设计:
主要是为了支持面向对象程序设计时的三大特性之一:多态性。
- 如果将子类对象赋值给父类对象 或 父类指针指向子类对象时,会将子类中属于父类的部分“切割”出来,并拷贝给父类。
- c++通过类的指针和引用,来支持多态(运行时决定函数的调用位置),这是一种程序设计风格。
- 不用动态多态,则函数调用的解析过程发生在编译时期,内存空间更紧凑(因不使用虚函数表、虚函数指针),但设计灵活性更差。
多重继承中虚函数表的分析:
- 派生类对象中,虚函数表指针的个数与该派生类的直接基类的个数相同;
- 虚函数表指针均按照继承顺序依次存放在该类对象的内存空间(栈/堆区)中;
- 该派生类与第一基类共用一个虚函数表指针,即虚函数指针存放在同一个虚函数表中;
- 派生类的虚函数会覆盖掉直接基类的虚函数,并将虚函数的地址,存放在相应的虚函数表中。
虚函数表的创建时机、程序中的位置:
创建过程:
- 虚函数表,是编译期间,就构建出来的,整个执行过程中并不会发生改变。
- 对于含有虚函数的类,在编译时,编译器会往类中加入虚函数表指针这个隐含的成员变量,并在类的构造函数中安插为虚函数指针赋值的语句。
虚函数表在可执行程序中的位置:
可执行程序被载入到内存中时的内存结构(从高地址到低地址),包含了栈区(栈区内存向下增长)、堆区(堆区内存向上增长)、数据段、代码段。
单纯的类不纯时,引发的虚函数调用问题:
静态联编、动态联编:
静态联编:编译的时候,就能确定调用哪个函数,并把调用语句和被调用函数绑定在一起。
动态联编:程序运行的时候,根据实际情况,动态的把调用语句和被调用语句绑定在一起(一般出现在多态和虚函数情况下,虚函数、多态:专门给指针和引用来使用的)。
不同对象<->不同调用时机:
某些情况下, 编译器会在类中增加隐形成员变量(比如有虚函数,该变量会用来存放虚函数表指针),这会导致一个类变得不单纯。
-
这种隐藏的成员变量的赋值时机,往往是在执行构造函数或者拷贝构造函数的函数体之前。
-
在栈中生成一个该类的局部对象,通过该对象能直接调用虚函数,并且能正常析构掉该对象(因为静态联编,即编译时期就已经确认了函数调用的位置)。
-
对象指针指向堆中new一个该类对象实例,通过该对象指针调用虚函数或者delete指针,实际上需要查找“虚函数表指针-虚函数表-虚函数”(因为动态联编,即运行时动态匹配,一般在多态或类中含有虚函数时出现)。
引用栈区的对象实例,并进行虚函数的调用,也属于动态联编,故也需要查找“虚函数表指针-虚函数表-虚函数”。
注意:构造函数 和 拷贝构造函数中,尽可能不要使用 memset()
和 memcpy()
这两个函数。
#include <iostream>
#include <cstring>
using namespace std;
class A
{
public:
A()
{
memset(this, 0, sizeof(A)); // 即此种操作会将this对象指向的内存空间中,虚函数表指针清空
// ,导致在“动态联编”时,无法找到虚函数表,进而也不会找到虚函数
cout << "A::A()" << endl;
}
A(const A* a)
{
memcpy(this, a, sizeof(A));
cout << "A::A(const A& a)" << endl;
}
virtual ~A()
{
cout << "virtual A::~A()" << endl;
}
virtual void virtual_ptfunc()
{
cout << "virtal A::ptfunc()" << endl;
}
};
int main()
{
// 静态联编:编译时就确定了函数的调用位置
A a;
a.virtual_ptfunc();
// 动态联编:类对象隐含的虚函数表指针在构造函数执行前以初始化,但构造函数中清空了类对象的内存空间,故指针/引用的类对象无法在运行时动态通过“虚函数表指针 --> 虚函数表 --> 虚函数”完成调用
//A* aPtr = new A();
//aPtr->virtual_ptfunc();
//delete aPtr;
//A& a_cite1 = a;
//a_cite1.virtual_ptfunc;
//A& a_cite2 = *aPtr;
//a_cite2.virtual_ptfunc;
return 0;
}
类外调用私有虚成员函数:
实质:找到虚函数表中虚函数的地址进行调用的。
A aObj;
(reinterpret_cast<(void*)()>(**(int**)(&aObj)))();
// 本质:找到虚函数表中虚函数的地址并调用
long* objVptr = (long*)(&aobj); // 类对象的首地址,即虚函数表指针所在的地址
long* vptr = (long*)(*objVptr); // 虚函数表的首地址
typedef void(*Func)(void); // 定义函数指针类型
Func func = (Func)(vptr[0]); func(); // 给函数指针赋值为第一个虚函数的地址,并调用该虚函数
注意:类对象的私有成员变量,都可以在类外通过访问类对象的内存直接访问,但由于不同平台/编译器对类对象内存布局可能不同(影响代码的移植问题),可能引发意料不到的问题。公开成员函数/友元函数可以实现类外对私有有成员的访问,但这极大的破坏了封装性,需要具体结合实际情况。
具有父子关系的两个类,不要在父类构造函数和析构函数中调用虚函数:
- 在父类构造函数中,调用虚函数,由于子类还未构造出来,故不能正确调用子类重写的虚函数;
- 在父类析构函数中,调用虚函数,由于子类已经被析构,故不能正确调用子类重写的虚函数;
总结:本质,由于“父子类构造和析构的顺序(父类先构造后析构)”问题带来的,如果强行调用,则会失去虚函数的作用。
this指针的调整:
- this指针调整,一般发生在多重且非虚的继承中。
- 继承的顺序,会影响基类与派生类的this指针的地址。派生类与首个继承的基类的this指针地址相同。
- 派生类会覆盖掉基类的的同名函数。
namespace _nmsp
{
class A
{
public:
int a;
public:
A()
{
cout << "A::A()的this指针的地址:" << this << endl;
}
void funcA()
{
cout << "A::funcA()的this指针的地址:" << this << endl;
}
};
class B
{
public:
int b;
public:
B()
{
cout << "B::B()的this指针的地址:" << this << endl;
}
void funcB()
{
cout << "B::funcB()的this指针的地址:" << this << endl;
}
};
class C : public A, public B
{
public:
int c;
public:
C()
{
cout << "C::C()的this指针的地址:" << this << endl;
}
void funcB() // C类中的funcB()函数,覆盖了B类的同名函数funcB()
{
cout << "C::funcB()的this指针的地址:" << this << endl;
}
void funcC()
{
cout << "C::funcC()的this指针的地址:" << this << endl;
}
};
void test()
{
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
cout << sizeof(C) << endl;
// 构造函数的调用顺序,按照继承的顺序,先基类后派生类
C myc;
myc.funcA();
myc.funcB();
myc.B::funcB();
myc.funcC();
}
// 执行test()后,终端输出:
//4
//4
//12
//A::A()的this指针的地址:0000005CA7CFF608
//B::B()的this指针的地址:0000005CA7CFF60C
//C::C()的this指针的地址:0000005CA7CFF608
//A::funcA()的this指针的地址:0000005CA7CFF608
//C::funcB()的this指针的地址:0000005CA7CFF608
//B::funcB()的this指针的地址:0000005CA7CFF60C
//C::funcC()的this指针的地址:0000005CA7CFF608
}
构造函数语义:
默认构造函数语义:
默认构造函数(缺省构造函数),生成类对象时,如果类中没有默认构造函数,编译器会隐式的生成一个“合成默认构造函数”。
编译器生成“合成默认构造函数”的情况:
- 该类中没有任何构造函数,但包含至少一个成员变量时;
- 基类有默认构造函数,该派生类没有任何构造函数(派生类在生成对象时,会按照继承顺序,依次调用基类的构造函数);
- 该类中含有虚函数,且没有任何构造函数(需要将虚函数表vftable指针通过自动生成的赋值语句赋给每个对象内存空间,故需要合成构造函数);
- 该类带有虚基类(虚基类是通过 “两个直接基类” 虚继承同一个间接基类形成的);
拷贝构造函数语义:
拷贝构造函数,在类对象拷贝赋值时,如果类中没有拷贝构造函数,编译器会隐式的生成一个“合成拷贝构造函数”(编译器内部实现)。
调用拷贝构造函数时,原有的成员变量的值不会被改变,只是值的拷贝而已。
编译器生成“合成拷贝构造函数”的情况:
- 该类中至少有一个成员变量;
- 基类有拷贝构造函数(派生类合成拷贝构造函数,是为了调用基类的拷贝构造函数);
- 该类定义有虚函数或者基类定义了虚函数(该类合成拷贝构造函数,是为了将该类对象的虚函数表指针进行赋值);
- 该类带有虚基类(虚基类是通过 “两个直接基类” 虚继承同一个间接基类形成的(给“两个直接基类”的虚基类表
vbtable
赋值,并将两个直接基类的虚基类表vbtable
赋值到派生类的对象内存空间));
细节问题:
-
如果只是成员变量的赋值,编译器并不会生成拷贝构造函数,内部直接进行赋值。
“合成拷贝构造函数”,一般是用来做一些特殊的事情(调用父类的拷贝构造函数、给类对象虚函数表指针赋值)。
-
只有在满足“合成拷贝构造函数”的前提下,激发拷贝动作编译器才会合成拷贝构造函数。
程序员,如果在类中增加了拷贝构造函数后,就要对类中各个成员变量的初始化负责。因在加入自定义的拷贝构造函数后,编译器内部的bitwise
按位复制能力会失效。
深、浅拷贝的问题(类中含有的成员变量包含指针变量或者其他非内置类类型):
- 浅拷贝 :会将类型中的成员变量直接进行复制(两个指针变量指向同一块内存空间,但如果一个对象释放则另一个对象就会出现访问不到成员指针变量的情况)。
- 深拷贝:会将上面的指针变量中的值,在拷贝构造函数中重新申请一块内存并将该值复制到新的内存空间,这样两个指针变量指向的就是不同的内存空间但是内部的数据相同,相互不影响。
在自定义拷贝构造函数时,为了避免“自我拷贝”,需要进行判断。
移动构造函数语义:
只有一个类中,没有定义任何自己版本的拷贝构造函数、拷贝赋值运算符、析构函数,且类的每个非静态变量都是可以移动时,编译器才会合成移动语义(移动构造函数 或者 移动赋值运算符)。
变量是否是可以移动的:
- 内置类型(如整型、实型等)的成员变量可以移动。
- 如果成员变量是一个类类型(且该类类型中含有相应的移动语义),则该成员变量是可以移动的。、
程序转换语义:
-
程序员的角度看代码 —> 编译器的角度理解代码。
-
定义时,初始化一个对象。
class A { public: A() { m_i = 0; } A(const A &tmpA) { m_i = tmpA.m_i; } public: int m_i; }; int main() { A a1; a1.m_i = 1; A a2 = a1; // 等价于A a2(a1)。编译器内部会分成两步:(只会调用拷贝构造函数,并不会调用默认构造函数) // A a2; // a2.A::A(a1); return 0; }
-
参数的初始化。
class A { public: A() { cout << "默认构造函数" <<endl; } ~ A() { cout << "析构函数" <<endl; } A(const A& tmpA) { cout << "拷贝构造函数" <<endl; } void func(A tmpa) { cout << "void func(A tmpa)" << endl; return; } }; int main() { A a; // 编译器在函数func()的空间中,构造了一个tmpa类对象,并在func()调用完成后析构掉 func(a); return 0; ]
-
返回值初始化。
class A { public: A(int val) : m_val(val) {} // 优化前,调用了构造函数、拷贝构造函数(将a对象拷贝给临时对象)、两次析构函数 /* 该函数内部调用了构造函数得到了类对象a,然后将a对象拷贝给临时对象temp_a并析构掉类对象a。最终函数返回临时对象temp_a给m_a。 如果函数返回的临时对象temp_a没有接收者,则会在函数返回后,立即析构掉这个临时对象。 */ A func() { A a; return a; } // 编译器优化后,仅调用了构造函数!!! void func(A& tmp_a) { A a; tmp_a.A::A(a.m_val); return; } public: int m_val; }; int main() { A m_a = func(); // 编译器视角: // A m_a; // func(m_a); return 0; }
程序的优化:
开发者角度 --> 编译器角度
函数内部构造类对象并返回:
class A
{
public:
A func(const A& a)
{
A tmp_a;
tmp_a.m_a1 = a.m_a1;
tmp_a.m_a2 = a.m_a2;
return tmp_a;
}
// 优化后:
A func_(const A& a)
{
return A(a.m_a1, a.m_a2);
}
// 编译器视角,看待优化后的代码!!!
void func_2(const A& tmp_a, const A& a)
{
tmp_a.A::A(a.m_a1, a.m_a2);
return;
}
private:
int m_a1;
int m_a2;
};
初始化列表,来初始化成员变量(保持这种写法的习惯!!!):
必须使用初始化列表的场景:
- 成员变量是引用类型;
- 成员变量是const类型;
- 如果该类继承自 / 类中含有一个成员变量是,含有有参数构造函数的基类;
说明:
- 初始化列表中的代码,是在构造函数的函数体执行之前就会被执行。
- 初始化列表中成员变量的初始化顺序,看的是“成员变量在类中的定义顺序”(而不是在初始化列表中的顺序)。
- 初始化成员变量时,尽量放在初始化列表中(而不是构造函数中)(特别是类类型对象放在初始化列表中初始化,效率极大的提升;对简单的类型,没有效率的提升),这可以极大地提高程序的效率。
数据语义学:
数据成员的绑定时机:
-
编译器在成员函数中,优先找类内定义的成员变量;全局函数中,优先找全局范围的变量。
-
编译器对成员函数的解析,在整个类定义完毕之后,才开始。
-
成员函数的参数类型:类定义语句(typedef、using … 等),根据“最近碰到的原则”(程序从上到下),故一般在类中最开始位置。
#include <iostream> using namespace std; typedef string mytype; class test1 { public: // “最近碰到的原则”,故这里的mytype==string void func(mytype myVar_) { myVar = myVar_; // 报错,因myVar是int,而myVar_是string } private: typedef int mytype; mytype myVar; }; class test2 { private: /* 建议将typedef、using...等信息,置于类的最前面 */ typedef int mytype; mytype myVar; public: void func(mytype myVar_) { myVar = myVar_; // 不报错,myVar、myVar_均是int } };
数据成员的布局:
成员变量的布局规律:
-
内存中存储的数据,都是按照成员变量的定义顺序来存储的;
-
静态成员变量存放在数据段,一旦生成可执行文件,地址就是固定的;
-
非静成员态变量,根据类对象而定,且类对象所占用的内存是一块连续的内存(栈/堆区);
A a; // 类对象中的数据在栈上 A *a = new A(); // 对象指针指向的是堆区,即类对象的数据在堆区
边界调整与字节对齐:
-
边界调整(目的是提高程序运行的效率,编译器自动去做),会在成员变量之间填补一些字节,使类对象的sizeof是4/8的倍数(这样会导致数据存储时不是紧密排列的);
-
实现各个成员变量在内存空间中紧密的排列:
#pragma pack(1) // 进行“1字节对齐” ... // 需要进行“1字节对齐”的类 #pragma back() // 取消“1字节对齐”
-
多重继承中,由于层次结构的存在,导致在边界调整后,子类占用的内存空间变大;
#include <iostream> using namespace std; class Base { public: int m_base1; char m_base2; }; class Base_ : public Base { public: char m_base_; }; class Derived : public Base_ { public: char m_derived; }; class ptClass { public: int m_pta; char m_ptb; char m_ptc; char m_ptd; }; int main() { printf("sizeof(ptClass) = %d\n", sizeof(ptClass)); /* 多重继承中,存在的层次结构,扩大了类占用的空间 */ printf("sizeof(Base) = %d\n", sizeof(Base)); printf("sizeof(Base_) = %d\n", sizeof(Base_)); printf("sizeof(Derived) = %d\n", sizeof(Derived)); cout << ".......... Base的内存分布 ..........." << endl; printf("m_base1 = %d\n", &Base::m_base1); printf("m_base2 = %d\n", &Base::m_base2); cout << ".......... Base_的内存分布 ..........." << endl; printf("m_base1 = %d\n", &Base_::m_base1); printf("m_base2 = %d\n", &Base_::m_base2); printf("m_base_ = %d\n", &Base_::m_base_); cout << ".......... Derived的内存分布 ..........." << endl; printf("m_base1 = %d\n", &Derived::m_base1); printf("m_base2 = %d\n", &Derived::m_base2); printf("m_base_ = %d\n", &Derived::m_base_); printf("m_derived = %d\n", &Derived::m_derived); return 0; }
非静态成员变量与类对象的首地址偏移值:
- 要打印成员变量的偏移值,就需要
&A::m_val
(而且要用printf来打印输出)、直接使用&m_val,输出的是成员变量的物理地址。 - 如果类中有虚函数,则会在类对象内存空间的首地址,存放4或8字节的虚函数表指针,即首个非静态成员变量的偏移量就是4/8;
#include <iostream>
using namespace std;
// 可以尝试给类,取消/添加“#pragma pack(1) ... #pragma pack()”,来观察各个变量的地址和偏移量的变化
#pragma pack(1)
class A
{
public:
int m_a;
char m_b;
int m_c;
static int m_sa;
static char m_sb;
static int m_sc;
};
int A::m_sa = 0;
char A::m_sb = 'i';
int A::m_sc = 0;
#pragma pack()
int main()
{
A a;
/* 打印对象a中,各个变量的物理地址: */
printf("A::m_a = %p\n", &a.m_a);
printf("A::m_b = %p\n", &a.m_b);
printf("A::m_c = %p\n", &a.m_c);
printf("A::m_sa = %p\n", &a.m_sa);
printf("A::m_sb = %p\n", &a.m_sb);
printf("A::m_sc = %p\n", &a.m_sc);
/* 打印对象a中,各个变量的地址偏移量(相对于对象的首地址而言): */
printf("A::m_a = %d\n", &A::m_a);
printf("A::m_b = %d\n", &A::m_b);
printf("A::m_c = %d\n", &A::m_c);
return 0;
}
数据成员的存取:
静态成员变量(不在类对象中保存,而在内存空间的数据段(可执行文件一旦形成,物理地址是固定的)):
// 编译器会自动将类名和变量名,结合成一个新的名字;可以避免不同的类中存在相同的静态变量名而出现错误
类名::变量名
对象名.变量名
对象指针名->变量名
非静态/普通成员变量(存放在对象中,故存取形式要根据类对象的定义方式变化)的存取:
类名 类对象名; 对象名.变量名;
类名 *对象指针名 = new 类名(); 对象指针名->变量名;
在类中,
- 当成员函数要修改成员变量时,在编译器看来是向类成员函数中插入了
this指针
(对象本身),并通过“this指针 + 成员变量的偏移值”来修改成员变量的值。 - 当要访问一个非静态成员变量(寻找一个成员变量的地址时),编译器通过“&类名+成员变量的偏移量”的地址来访问。
“单一继承且父类有虚函数”的数据成员布局(非虚继承):
一个子类中对象中包含的内容:(父类成员 + 自己的成员)
- 栈/堆区:基类、派生类的虚函数表指针、非静态成员变量。
- 数据区:基类、派生类的静态成员变量(用基类/派生类对象,访问基类静态成员变量,是同一个内存地址)。
- 单一继承“父类带虚函数”与“父类不带虚函数”的数据成员布局是一样的(偏移量的计算是站在父类角度看的):
#include <iostream>
using namespace std;
class Base_nonVirtualTablePtr
{
public:
Base_nonVirtualTablePtr()
{
printf("Base_nonVirtualTablePtr对象的this指针指向的地址:%p\n", this);
}
int m_a;
};
class Derived1 : public Base_nonVirtualTablePtr
{
public:
Derived1()
{
printf("Derived1对象的this指针指向的地址:%p\n", this);
}
virtual ~Derived1() {};
int m_b;
int m_c;
};
class Base_virtualTablePtr
{
public:
Base_virtualTablePtr()
{
printf("Base_noVirtualTablePtr对象的this指针指向的地址:%p\n", this);
}
int m_a;
virtual ~Base_virtualTablePtr() {}
};
class Derived2 : public Base_virtualTablePtr
{
public:
Derived2()
{
printf("Derived1对象的this指针指向的地址:%p\n", this);
}
int m_b;
int m_c;
};
int main()
{
/* Linux下g++测试结果: */
/* 单一继承父类不带虚函数表指针 */
Derived1 son1;
/*
Base_noVirtualTablePtr对象的this指针指向的地址:0x7ffeed977618
Derived1对象的this指针指向的地址:0x7ffeed977610
*/
printf("m_a = %d\n", &Base_noVirtualTablePtr::m_a); // 0
printf("m_b = %d\n", &Derived1::m_b); // 4 + 8(虚函数表指针) ==> 12
printf("m_c = %d\n", &Derived1::m_c); // 16
cout << "..............." << endl;
/* 单一继承父类带虚函数表指针 */
Derived2 son2;
/*
Base_virtualTablePtr对象的this指针指向的地址:0x7ffeed9775f0
Derived2对象的this指针指向的地址:0x7ffeed9775f0
*/
printf("m_a = %d\n", &Base_virtualTablePtr::m_a); // 0 + 8(虚函数表指针) ==> 8
printf("m_b = %d\n", &Derived2::m_b); // 12
printf("m_c = %d\n", &Derived2::m_c); // 16
// 结论:尽管单一继承下父类带/不带虚函数,会影响偏移量,但子类对象的数据在内存中的分布是相同的
return 0;
}
成员变量的定位:
- 通过this指针(编译器根据情况,决定是否调整);
- 该成员变量的偏移量;
单一继承虚函数的调用:只需确认“通过哪个虚函数表”来调用即可。
多重直接继承,且父类都有虚函数的数据成员布局(非虚继承):
多重继承中,由于父类是平级,故各个父类中的首个非静态成员变量的偏移量是相同的。
子类的this指针的地址和继承的第一直接基类相同,即子类的虚函数指针和第一直接基类的虚函数指针存放在同一个虚函数表中(按照先父类后子类的原则)。
通过this指针的调整后,偏移量相同的两个直接基类的非静态成员变量也能正常访问。
两个直接基类和一个派生类的内存(栈区)分布:
- 第一直接基类的虚函数表指针(该虚函数表中包含,第一直接基类的虚函数地址、该派生类的虚函数地址);
- 第一直接基类的非静态成员变量;
- 第二直接基类的虚函数表指针(该虚函数表中包含,第二直接基类的虚函数地址);
- 第二直接基类的非静态成员变量;
- 该派生类的非静态成员变量;
通过“this+偏移值”,可访问非静态成员变量,如下:
#include <iostream>
using namespace std;
class Base1
{
public:
int m_base1;
virtual void virtual_func1() { cout << "Base1::virtual_func1()" << endl; }
Base1() { printf("Base1对象this指针的地址:%p\n", this); }
};
class Base2
{
public:
int m_base2;
virtual void virtual_func2() { cout << "Base2::virtual_func2()" << endl; }
Base2() { printf("Base2对象this指针的地址:%p\n", this); }
};
class Derived : public Base1, public Base2
{
public:
int m_derived1;
int m_derived2;
virtual void virtual_func3() { cout << "Derived::virtual_func3()" << endl; }
Derived() { printf("Derived对象this指针的地址:%p\n", this); }
};
int main()
{
Derived derived;
printf("m_base1的地址偏移量:%d\n", &Derived::m_base1);
printf("m_base2的地址偏移量:%d\n", &Derived::m_base2);
printf("m_derived1的地址偏移量:%d\n", &Derived::m_derived1);
printf("m_derived2的地址偏移量:%d\n", &Derived::m_derived2);
return 0;
}
/*
Base1对象this指针的地址:00D5F87C
Base2对象this指针的地址:00D5F884
Derived对象this指针的地址:00D5F87C
m_base1的地址偏移量:4
m_base2的地址偏移量:4
m_derived1的地址偏移量:16
m_derived2的地址偏移量:20
*/
当父类指针指向子类对象时,通过this指针的调整,就可正常访问到父类的“成员变量/成员函数/被子类重写的虚函数”。
#include <iostream>
using namespace std;
class Base1
{
public:
int m_base1;
virtual void virtual_func1() { cout << "Base1::virtual_func1()" << endl; }
Base1() { printf("Base1对象this指针的地址:%p\n", this); }
};
class Base2
{
public:
int m_base2;
virtual void virtual_func2() { cout << "Base2::virtual_func2()" << endl; }
Base2() { printf("Base2对象this指针的地址:%p\n", this); }
};
class Derived : public Base1, public Base2
{
public:
int m_derived1;
int m_derived2;
virtual void virtual_func3() { cout << "Derived::virtual_func3()" << endl; }
Derived() { printf("Derived对象this指针的地址:%p\n", this); }
};
int main()
{
Derived* derived = new Derived();
printf("derived2指向的地址:%p\n", derived);
Base1* base1 = derived; // 父类Base1指向子类Derived时,this指针不需要调整
printf("base1指向的地址:%p\n", base1);
Base2* base2 = derived; // 父类Base2指向子类Derived时,this指针需要向下调整
printf("base2指向的地址:%p\n", base2);
derived = (Derived*)base2; // 父类Base2给子类Derived赋值时,this指针需要向上调整
printf("derived2指向的地址:%p\n", derived);
delete derived;
return 0;
}
更复杂的继承关系:
虚基类(编译器实现极其复杂),虚继承的消耗是巨大的,非必要不要使用:
- 虚基类必须含有三层结构:一个爷爷类、两个父类、一个子类。
- 虚基类干的一件事是:确保孙子类中只包含有一份Grand类子对象。
- 引入虚继承后,Grand类对象中的成员变量,需要在孙子类构造函数的初始化列表中初始化。
- *虚基类表指针:虚继承父类的子类,会被编译器插入一个虚基类表指针(占用4/8字节)。
单一虚继承:
-
子类虚继承了父类
-
子类中不含有虚函数,则子类对象的内存分布:虚基类表指针、子类成员变量、虚基类的成员变量
-
子类中含有虚函数,则子类对象的内存分布:(this调整过程非常复杂)
三层虚继承的内存布局(vbptr1、vbptr2是虚基类表指针;虚基类子对象被放在了最后)。
只有在对虚基类的成员变量进行处理(如赋值的时候),才会用到虚基类表 — 取其中的值用作偏移值来进行虚基类成员变量首地址的定位运算。
经过以上分析,得出结论:访问虚基类的成员变量要比访问其他成员变量更慢。
函数语义学:
普通成员函数(加this参数后,等价于全局函数):
- 成员函数(普通/静态)与全局函数一样,在编译的时候,就确定了具体的地址,而且都是独立的地址;
- 编译器内部实际上,会在成员函数中(参数列表的开头)额外安插一个this指针(调用该成员函数的类对象的地址);这样调用成员函数,相当于调用全局函数;
- this指针指向对象本身,被传递给所调用的成员函数的主要目的是:使函数中操作的是本对象所属的成员变量。
- 但如果该成员函数中,并没有操作任何的成员变量,则不需要this指针。
虚成员函数的调用方法:
调用方法:
// 等价于直接调用一个普通的成员函数,不需要通过虚函数列表来查找并调用虚函数
对象名.虚函数名(参数列表);
类范围操作符::虚函数名(参数列表); // 仅限于类内调用
// 编译器调用过程:“虚函数表指针 ---> 虚函数表 ---> 虚函数的入口地址”
类对象指针->虚函数名(参数列表);
// 编译器找到虚函数的入口地址后,会插入一个this指针作为虚函数参数列表的第一个参数,之后就相当于调用一个普通函数
虚函数地址问题的vcall引入:
- 虚函数的内存地址也是固定的,在编译阶段就确定的;
- vcall是编译生成的内容,完整的名字称为
vcall thunk
,其调用的是真实的虚函数的地址。 - vcall引入的目的:能够调整this指针,跳转到真正的虚函数中。
静态成员函数调用方法:
调用方法:
- 无论用“类对象”或“类对象指针”调用,都会被编译器转换为“类名::静态成员函数(参数列表)”的调用。
- 静态成员函数属于类,调用时编译器不需要插入this作为形参,但其可用于提示该静态成员函数所属的类。
总结:
- 没有this指针,故无法直接存取类中普通的非静态成员变量;
- 函数声明的尾部,不能用const、virtual修饰;
- 三种调用方法:“类对象”、“类对象指针”、“类名::静态成员函数(参数列表)”;
静态、动态类型,静态、动态绑定:
静态、动态类型:
- 静态类型:对象定义时的类型,编译期间就确定好了。
- 动态类型:对象目前所指向的类型,是运行时才决定的(一般只有父类的指针(指向new出来的派生类)和引用,才有动态类型)。
静态、动态绑定:
-
静态绑定: 绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期。
普通成员函数、缺省参数一般是静态绑定。
-
动态绑定: 绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期。
虚函数是动态绑定。
继承中的成员函数绑定:
继承非虚函数(静态绑定):不应该在一个子类中,“重写”继承来的非虚成员函数,但可以“重载”。
非虚继承父类的虚函数(动态绑定):子类的同名同参的虚函数会覆盖掉继承来的父类的虚函数,并与其他的虚函数一起存放在虚函数表中(相当于该类中多了一个虚拟的成员变量)。
“重写”虚函数的缺省参数:
-
缺省参数是静态绑定的。
-
在给定参数缺省值的情况下,会同时出现:虚函数的动态绑定、缺省参数的静态绑定。
#include <iostream> using namespace std; class Base { public: Base() { } virtual void func(int val = 1) { cout << "Base::func(), val = " << val << endl; } }; class Derive : public Base { public: Derive() { } // 注意:尽量不要在子类中,重定义虚函数的缺省参数的值。 virtual void func(int val = 2) { cout << "Derive::func(), val = " << val << endl; } }; int main() { // 普通函数是静态绑定;虚函数是动态绑定;虚函数的缺省参数是静态绑定; Derive derive; Base *pbase = &derive; pbase->func(); /* 结果Derive::func(), val = 1 */ return 0; }
c++中的多态性:
事实:
存在虚函数且调用虚函数,才会涉及多态(没有虚函数,绝不可能存在多态性)。
代码实现上:
A* a1 = new A();
a1->myVirtualFunc(); // 该调用过程即是多态
A* a3 = &a2;
a3->myVirtualFunc(); // 该调用过程即是多态
A a2;
a2.myVirtualFunc(); // 该调用过程不是多态
-
调用虚函数的过程:“虚函数表 --> 虚函数指针 --> 虚函数的入口地址”,则该调用过程就是多态。
-
通过类对象指针指向、类对象引用对象实例,调用过程为:“查虚函数表 --> 找到虚函数地址 --> 调用虚函数”,才会存在多态。
如果非指针/引用,则编译期就会确定虚函数的调用地址,则不存在多态性。
表现形式上:
Derived derived;
Base* pbase = new Derived();
pbase->virtualfunc(); // Derived::virtualfunc()
Base& citebase = derived;
citebase.virtualfunc(); // Derived::virtualfunc()
- 父类中必须含有虚函数,子类重写父类中的虚函数;
- “父类指针指向子对象” 或 “父类引用绑定(指向)子类对象”;
总结:当以父类指针或引用调用子类中重写了的虚函数时(因为会动态调用子类的虚函数),就能看出多态。
多重继承下的虚函数、第二继承与虚析构:
多重继承下的虚函数:
- 多重继承问题的复杂性体现:第二基类Base2,在派生类对象内存中排在第一基类后,这就存在this指针调整的问题。
- 第二基类的指针pbase2,指向new出来的派生类对象时,pbase2指针指向的是经过this指针调整后的地址。
Base2 *pbase2 = new Derive();
// 等价于
Derive *temp = new Derive();
Base2 *pbase2 = (Base2*)((char*)(temp) + sizeof(Base));
- 但由于new的是整个子类Derive对象,故
delete pbase2
会报错。
Base2 *pbase2 = new Derive();
delete pbase2; // 报错,因pbase2指向的是其所在的内存,而不是整个被new出来的整个子类对象的内存
如何成功删除第二基类指针new出来的子类对象:
- 虚析构函数,是支持多态的;
- 在带有继承关系的类中,一定要定义析构函数为虚析构函数,否则会导致内存泄漏;
- 构造函数时按照继承顺序执行的、析构函数是按照继承顺序的反顺序执行;
- Derive类对象的内存结构中,第二基类的虚函数表指针 -> 虚函数表 -> 虚析构函数的位置(会包含thunk汇编代码块:① 用来调整this指针;② 调用Derive虚析构函数)。
delete指向子类对象的父类指针时:
-
父类的析构函数不是虚函数,则不会触发动态绑定,结果就是只会调用父类的析构函数,从而导致内存泄漏(如果子类的析构函数中存在delete这样的代码的话,内存泄漏是必然的)。
-
父类的析构函数是虚函数,则子类必然是虚析构函数(c++语言的规定,与子类析构函数前是否有修饰virtual无关),则会触发动态绑定。
因new的实际是一个子类对象,所以先执行的是子类的析构函数,同时编译器还向子类的析构函数中插入了调用父类析构函数的代码(实现了先调用子类析构函数,再调用父类析构函数,故可让整个对象完美释放)。
在父类有虚函数的情况下,某个类继承了多少个(含有虚函数的)父类,就会有几个虚函数表。
对象指针this:
指向对象的首地址,故只有对象指针才能正确地调用对象的成员函数:
- 编译器内部会将this指针作为成员函数的第一个参数,用来修改成员函数中一些成员变量的值;
- 成员变量的地址 = this指针 + 成员变量的偏移值;
this指针调整:
- 目的:让对象指针正确地指向对象首地址,从而能正确定位到“成员函数/成员变量的存储位置”。
- 应用场景:
- 第二个基类的指针new出来的子类对象,delete该对象 或者 调用派生类的虚函数时;
- 一个指向派生类的指针调用第二个基类中的虚函数时(通过调整this指针,指向派生类对象的虚函数表中的第二基类对应的虚函数表的表头);
- 允许虚函数的返回值类型有所变化时;
RTTI运行时类型识别:
RTTI中,type_info相关的信息,在编译过后就在可执行文件中。
const typeinfo& typeInfo = typeid(参数); typeInfo.name();
// 等价于:
typeid(参数).name()
注意:基类无虚函数,则不是多态,也就更谈不上RTTI!!!
继承关系的深度增加:
虚函数导致的开销增加:
- 多态中,每个类对象会在有一个虚函数表指针,在执行类的构造函数时,编译器会在函数中增加给虚函数表指针赋值的代码,故每多一层继承关系,就会多执行一次对虚函数表指针赋值的代码。
- 多态中调用虚函数时,是通过虚函数表寻找并调用虚函数的,这会增加调用的开销。
多重继承导致的开销增加:
- 每多一层继承,子类在构造时就需要多调用一个基类的构造函数,导致开销增加。
成员函数指针:
/* 指向成员函数的指针 */
// 非静态成员函数指针:
typedef void(A::*ptrfunc_a)(int val); // 声明
ptrfunc_a = &A::m_func(); // 定义
A a; (a.(*ptrfunc_a))(10); // 类对象调用
A *ptr_a = new A(); (ptr_a->(*ptrfunc_a))(10); // 类对象指针调用
// 静态成员函数指针:
void(*static_func_ptr)(int val) = &A::m_static_func(); // 声明、定义
static_func_ptr(10); // 调用
/* 注意:使用成员函数指针来调用成员函数,必须要对象的介入(静态成员(属于整个类)除外,可不用this指针的参与)*/
/* 指向虚成员函数的指针 */
inline函数及其扩展细节(不推荐使用):
- 开发者写的inline函数只是对编译器的一个建议,但如果编译器评估后发现该inline函数的复杂度过高,则inline的建议会被编译器忽略。
- 如果inline函数被编译器采纳,则编译时会在inline函数的调用处进行扩展。
对象构造函数语义:
继承体系下对象的构造:
不建议,在类的构造函数(析构函数)中调用虚函数:
- 这种情况的调用并不会通过虚函数表,直接在其类/父类中寻找调用)。
- 如果构造函数中调用的虚函数中,所用到的成员变量在构造函数中还未初始化,则会出错。
多层次继承时,构造函数的执行步骤:
- 虚函数表指针的初始化时机,发生在各类的构造函数函数体执行前。因此,不能在构造函数中使用
memset(this, 0, sizeof(类))
或memcpy()
等函数,会导致虚函数表指针为空,导致后续虚函数的调用出错。 - 初始化列表的执行时机。
- 执行顺序:从父类到子类,从根源到末端(父类构造函数执行时,子类对象实体还没构造呢)。
对象复制语义学与析构函数语义学:
对于类中没有拷贝构造函数和拷贝复制运算符的情况,编译器会有一些默认的对象复制行为(只能执行简单的复制操作,且效率高)。
// 拷贝构造函数和拷贝赋值运算符:
A& operator=(const A &temp);
A(const A &temp);
// 禁用拷贝构造函数或者赋值运算符:
// 方法一:将函数声明为private
// 方法二(c++11):
A& operator=(const A &temp) = delete; A(const A &temp) = delete
编译器合成析构函数的情况:
- 继承中,基类中有析构函数,则派生类必然会有析构函数。
- 类中含有类类型的成员变量时,编译器会合成该类的析构函数(是为了调用该类类型变量的析构函数)。
- 如果存在析构函数,编译器则会在适当的情况下扩展析构函数,如(扩展)调用基类的析构函数。
局部对象、全局对象的构造和析构:
局部对象的构造和析构:
-
只要出了局部对象的作用域{ },编译器会在适当的地方插入调用对象析构函数的代码。
-
局部对象,尽量定义在需要用到它的代码段附近,即现用现定义(c与c++不同之处)。
*在函数开头定义对象(如传统的c语言),则需要在所有函数的出口处加析构函数。
全局对象的构造和析构:
- 全局变量的成员变量(一般是数值类型成员变量)在没给初值的情况下,编译器会给一个默认值0(称为静态初始化)(与局部变量最大的不同)。
- 编译阶段,全局变量的地址已经确定,存放在“内存中的数据段”,每次执行程序时内存地址是不变的。
- 全局对象的分配内存 与 堆、栈中分配内存不一样:
- 栈区的内存,只能在离开变量作用域后,自动回收;
- 堆中的内存,需要delete才能释放掉;
- 局对象的内存,在程序运行阶段会一直存在,会在main函数结束后被析构掉;
- 程序在main()函数,执行的前后有许多事情要做:
- 执行前,对全局对象的静态初始化(置0),以及全局对象的构造函数调用;
- 执行结束后,调用全局对象的析构函数;
静态局部对象、对象数组构造、析构和内存分配:
静态局部对象的构造和析构:
- 编译阶段就已经确定了地址和大小,当可执行文件装载到内存中并开始执行时,该地址就会被映射到内存中。
- 首次调用时,被构造且只能构造一次(通过增加标记,来防止静态局部对象被二次构造)。
- 在main函数结束后,会被析构掉。
全局、局部静态变量等所占用的内存大小信息,一般都是在目标文件/在可执行文件中。
静态局部对象数组,如果没有对该数组进行有用的操作时,编译器就不会给这个对象数组分配实际的物理内存。
临时性对象:
拷贝构造函数、拷贝赋值运算符、直接运算产生的临时对象。临时对象很耗费成本,写代码的时候尽量避免!!!
模板实例化语义学:
函数模板:
在调用函数模板时,编译器会实例化一个函数,即再生成一个具体针对这些类型的函数体代码。
类模板:
-
类模板中的静态成员变量,在初始化和调用时,类模板并没有被实例化出来,只能被当作变量使用;
template <typename T> class A { public: static T m_static; ... }; template<class T> T A <T>::m_static = 0;
-
类模板的实例化:
A<int> a(0);
A<int>& a2 = a;
const A<int>& a3 = 0; // 使用了隐式类型转换,等价于:A<int> tmpObj(0); const A<int>& a3 = tmppObj;
- 类模板的成员函数只有在调用时,才会被实例化。
- 在项目中,可能会有多个.cpp源文件调用该类模板,故一般实际在做项目时会把类模板的定义和实现等相关的内容都放在一个.h文件中。
- 在多个obj文件中,可能产生多个重复的类模板对应的具体的实例化类,但链接(链接器)时只会保留一个。
虚函数的实例化:
- 只有实例化出类模板中的每个虚函数,虚函数表中才能有虚函数的地址,才能通过虚函数表正确定位到虚函数。
显示实例化:
类模板的显示实例化:
template class A<int>; // 编译器会将类模板中,所有内容都会被实例化出来
实例化单独的成员函数:
template void A<int>::func();