参考:c++新经典对象模型,王建伟编著
1.类对象所占用的空间
1.1 空类对象大小为1
class A
{
public:
};
int main()
{
A a;
int ilen = sizeof(a); // res: ilen=1
}
对象是有地址的,即便是一个空类的对象,因为在内存中是有起始地址,既然这个地址属于该对象,该对象必然最少能存的下一个字节。
1.2 类中的成员函数不占类对象内存空间
总结:
1.成员变量是包含在每个对象中的,占字节的
2.成员函数写在类定义中,但成员函数不占类对象字节空间。
2.c++对象模型简介
类对象大小:非静态成员变量+虚函数表指针, 静态变量+成员函数与类对象无关
-
非静态的成员变量跟着对象走
-
静态成员变量与对象无关,不计算在sizeof内
-
成员函数,不管静态与非静态,都保存在对象之外,不计算在sizeof内
-
虚函数不计算在对象的sizeof内,虚函数让类对象多了一个4字节的虚函数表指针
-
虚函数表是基于类的,不是基于对象的
一个类有一个虚函数,就会产生一个指向虚函数的指针,有两个虚函数,则会有两个指向虚函数的指针,这些指向虚函数的指针,都会放在虚函数表里,同时,类对象会有一个指向虚函数表的虚函数表指针。
类的对象要调用类的虚函数时,实际是通过虚函数表指针找到类的虚函数表,这是“多态”的实现的原理
总结:
类对象大小的组成:
1.非静态成员变量所占内存 + 字节对齐占用内存
2.虚函数表指针(如果类有虚函数的话)
3.this指针调整
this指针发生在多重继承下
两个基类A,B , 一个派生类C,不存在同名函数覆盖的情况
代码示例:
#include<iostream>
class A{
public:
int a;
A()
{
std::cout << "A::A() this=" << this <<std::endl;
}
void funA()
{
std::cout << "A::funA() this=" << this <<std::endl;
}
};
class B{
public:
int b;
B()
{
std::cout << "B::B() this=" << this <<std::endl;
}
void funB()
{
std::cout << "B::funB() this=" << this <<std::endl;
}
};
class C: public A, public B{
public:
int c;
C()
{
std::cout << "C::C() this=" << this <<std::endl;
}
void funC()
{
std::cout << "C::funC() this=" << this <<std::endl;
}
};
int main()
{
C oc;
oc.funA();
oc.funB();
oc.funC();
};
//output
A::A() this=0x7ffee29a0420
B::B() this=0x7ffee29a0424
C::C() this=0x7ffee29a0420
A::funA() this=0x7ffee29a0420
B::funB() this=0x7ffee29a0424
C::funC() this=0x7ffee29a0420
前三行输出:A构造函数的this指针与B构造函数指针不同,相差4字节, 基类A构造函数的this指针与派生类C构造函数的this指针相同。
后三行输出:执行funcB时,this指针从原来的0x7ffee29a0420变为0x7ffee29a0424,属于编译自内部自动调整this指针
总结:
1.如果派生类只继承一个父类,则派生类对象地址与基类子对象地址相同
2.如果派生类继承多个父类,则第一个继承的基类的子对象地址与派生类对象起始地址相同,后续的基类子对象的起始地址与派生类对象起始地址相差前面基类子对象所占用的内存空间。注意:继承顺序很重要,例如class C: public A, public B,C的this与A的this相同.
C类覆盖B类的同名函数funcB:
#include<iostream>
class A{
public:
int a;
A()
{
std::cout << "A::A() this=" << this <<std::endl;
}
void funA()
{
std::cout << "A::funA() this=" << this <<std::endl;
}
};
class B{
public:
int b;
B()
{
std::cout << "B::B() this=" << this <<std::endl;
}
void funB()
{
std::cout << "B::funB() this=" << this <<std::endl;
}
};
class C: public A, public B{
public:
int c;
C()
{
std::cout << "C::C() this=" << this <<std::endl;
}
void funB()
{
std::cout << "C::funB() this=" << this <<std::endl;
}
void funC()
{
std::cout << "C::funC() this=" << this <<std::endl;
}
};
int main()
{
C oc;
oc.funA();
oc.funB();
oc.funC();
oc.B::funB();
};
//output
A::A() this=0x7ffc5d1108a0
B::B() this=0x7ffc5d1108a4
C::C() this=0x7ffc5d1108a0
A::funA() this=0x7ffc5d1108a0
C::funB() this=0x7ffc5d1108a0
C::funC() this=0x7ffc5d1108a0
B::funB() this=0x7ffc5d1108a4
第五行:C::funB() this=0x7ffc5d1108a0
总结:
调用哪个子类的成员函数,这个this指针就会被编译器自动调整到对象内存布局中对应该子类的起始地址那去
4.编译器生成合成默认构造函数时机
当程序员没有定义任何构造函数时,不要想当然编译器会隐式的自动合成默认构造函数,只有在必要的时候才会去生成合成默认构造函数。
时机1:
1.如果该类A没有任何构造函数
2.该类A包含一个类类型B的成员变量
3.B类型有一个默认构造函数
编译器生成合成默认构造函数的目的:在合成默认构造函数中安插代码来调用类B的默认构造函数
时机2:
1.父类有默认构造函数
2.子类没有任何构造函数
当创建一个子类对象时,父类的这个默认构造函数是要被调用的,所以编译器生成合成默认构造函数,用来安插代码来调用类B的默认构造函数
时机3:
1.一个类有虚函数
2.该类没有任何构造函数
编译器生成合成默认构造函数的目的:因为虚函数的存在,编译器会为该类生成一个虚函数表,所以会自动生成合成默认构造函数,在其中安插代码来对虚函数表指针赋值,虚函数表值相当于一个隐藏的成员变量
理解:
如果程序员编写了自己的默认构造函数时,编译器会根据需要扩充该构造函数,向其中安插必要代码:1.加入调用父类的构造函数代码 2.加入给虚函数表指针赋值的代码
时机4:
1.一个类有虚基类,编译器也会为生成一个“合成默认构造函数”
编译器生成合成默认构造函数的目的:在合成默认构造函数中安插代码给虚基类表指针赋值以及调用父类的构造函数,
虚基类构成条件:三层结构,爷爷类Grand,两个父类A和A2, 一个孙子类C,Grand是C的虚基类。
虚基类解决的问题:基类对象Grand在孙子类C中被继承两次,解决名字冲突问题。
时机5:
1.如果在定义成员变量的时候赋初值,c++11新语法
class A{
public:
int bb{0}; //定义并初始化成员变量
};
编译器生成合成默认构造函数的目的:在合成默认构造函数中安插代码,初始化bb成员变量的值为0(因为代码中{}给的初值为0)
5.编译器生成拷贝构造函数的时机
当类A只有简单类型的成员变量或者A的成员变量为类类型B,B中只有简单类型变量,这种情况下:
编译器手法:直接就按值拷贝过来了,不需要合成拷贝构造函数。如果只一些类成员变量的值复制这些简单的事情,编译器不用专门生成拷贝构造函数
时机1:
1.如果类A没有拷贝构造函数
2.该类A有一个类类型B成员变量,类 B有拷贝构造函数
编译器生成拷贝构造函数目的:向其中插入代码,从而调用类B的拷贝构造函数
时机2:
1.类A没有拷贝构造函数,但他有个父类B
2.父类B有拷贝构造函数
编译器生成拷贝构造函数目的:生成类A的拷贝构造函数,在该函数内插入调用父类B的拷贝构造函数。
时机3:
1.如果类A没有拷贝构造函数
2.类A定义了虚函数或者类A 的父类定义了虚函数
编译器生成拷贝构造函数目的:生成类A的拷贝构造函数,在该函数内插入代码来给被复制的对象的虚函数表指针赋值。
时机4:
1.如果一个类A没有拷贝构造函数
2.但是该类A有虚基类
编译器生成拷贝构造函数的目的:在拷贝构造函数中安插代码给虚基类表指针赋值以及调用父类的拷贝构造函数
6.编译器生成移动构造函数的时机
不生成时机
如果一个类A定义了自己的拷贝构造函数、拷贝赋值运算符、或者析构函数(任意一个),表示:程序员要自己处理对象的复制或者释放问题,则编译器不会生成该类的移动构造函数和移动复制运算符。
生成时机
类A没定义任何自己版本的拷贝构造函数、拷贝赋值运算符、析构函数,且类的每个非静态成员都可以移动时,编译器才会自动合成移动构造函数和移动复制运算符。
成员可以移动
1.内置类型的成员变量可以移动
2.如果成员变量是一个类类型,如果这个类有对应的移动操作相关的函数,则该成员变量可以移动
7.程序员视角vs编译器视角
1.定义时初始化对象
#include<iostream>
class A{
public:
int m_i;
A(){ std::cout<<"A::()";}
A(const A& a){ m_i = a.m_i;}
};
int main()
{
A a0;
A a1 = a0;
}
程序员视角:
A a11=a0; 调用A的拷贝构造函数
编译器视角:
1.定义一个对象,为其分配内存,不会调用构造函数。
A a11;
2.直接调用对象的拷贝构造函数
a11.A::A(a0)
没有显示调用构造函数或者拷贝构造函数的代码,编译器是不会自动去调用构造函数或者拷贝构造函数的
2.参数的初始化
#include<iostream>
class A{
public:
int m_i;
A(){ std::cout<<"A::()";}
A(const A& a){ m_i = a.m_i;}
};
void func(A tmpa)
{
return;
}
int main()
{
A a0;
func(a0);
}
程序员视角:
func(a0); //形参调用拷贝构造函数,函数返回后调用析构函数
编译器视角:
编译器在func的函数空间内构造tmpa对象,然后func函数返回前,再把tmpa对象析构掉
A tmpobj;
tmpobj.A::A(a0);
func(tmpobj); //func(A &tmpa); 函数类型会变为引用类型
tmpobj.A::~A()
3.返回值初始化
#include<iostream>
class A{
public:
int m_i;
A(){ std::cout<<"A::()";}
A(const A& a){ m_i = a.m_i;}
};
A func()
{
A a;
return a;
}
int main()
{
A my = func();
}
程序员视角:
因为return a; 产生了临时对象,这个临时对象直接构造到了my中,可以把my当成临时对象
编译器视角:
void func(A& tmp){
A a;
tmp.A::A(a);
return;
}
int main(){
A my; //这里编译器只是创建内存空间,不调用构造函数
func(my);
}
新增一个成员函数 A::functest()
程序员视角:
func.functest();
编译器视角:
X my;
(func(my), my).functest(); //逗号表达式,表达式的值为表达式2的值
8.临时对象优化
CTemp TEST(CTemp &tt)
{
CTemp tmp; // 这里消耗一个构造函数和一个析构函数
tmp.a = tt.a;
tmp.b = tt.b;
return tmp; //这里调用了拷贝构造函数和析构函数,表示生成了临时对象。
}
int main(){
CTemp ts(1,2); //一次构造函数和一次析构函数
TEST(ts); //临时对象没有接收者
}
临时对象没有接收者:
TEST函数返回的临时对象return tmp;没有接收者,所以这个临时对象在TEST函数调用后被立即释放掉
临时对象有接收者:
CTmp a = TEST(ts);
TEST返回的临时对象不会被立即释放掉,而是直接构造到a中,并且一直到a的作用域结束后才会析构该临时对象
函数返回return优化:
程序员视角优化:
CTemp TEST(CTemp &tt)
{
return CTemp(tt.a, tt.b); //临时对象
}
编译器视角优化:
void TEST(CTemp &a, CTemp &tt) /编译器插入一个参数a
{
a.CTemp::CTemp(tt.a, tt.b);
return;
}
ROV(return value optimizaiton) 编译器针对临时变量的优化
9.成员初始化列表
必须使用成员初始化列表场景:
1.成员变量是引用类型
2.成员变量是const类型
3.该类继承一个基类,并且基类中有构造函数,这个构造函数有参数
4.成员变量是类类型,且,这个类类型的构造函数有参数