所有代码在x64下运行
一.类对象所占用的空间大小
详细介绍字节对齐:c+字节对齐分析
#include <iostream>
using namespace std;
class A
{
public:
void func() {}; //成员函数
void func1() {}; //成员函数
void func2() {}; //成员函数
//char ab; //成员变量,char类型占一个字节内存
int ab; //int类型占4个字节
};
class B {
};
//类对象所占用的空间
int main()
{
A obja, objb;
int ilen = sizeof(obja);
B b;
int blen = sizeof(b);
cout << blen << endl;//1 ,为什么sizeof(空类) = 1 而不等于0?
cout << ilen << endl;//4
//类的成员函数 不占用 类对象的内存空间
//我们已经知道两点:
//(1)成员函数不占用类对象的内存空间
//(2)一个类对象至少占用1个字节的内存空间
//obja.ab = 'c';
//(3)成员变量是占用对象的内存空间
obja.ab = 12;
objb.ab = 24;
//结论:成员变量是包含在每个对象中的,是占用对象字节的。
//而成员函数虽然也写在类的定义中,但成员函数不占对象字节数的(不占内存空间);
//成员函数 每个类只诞生 一个(跟着类走),而不管你用这个类产生了多少个该类的对象;
return 1;
}
二.对象结构的发展和演化
#include <iostream>
using namespace std;
class A
{
public:
int a = 100; //非静态成员变量(普通成员变量)
static int aa; //静态成员变量是跟着类走的;
static int bb;
static void sfunc() {}; //静态成员函数
void myfunc() {}; //普通成员函数
virtual void myfunc3() {};
virtual void myfunc4() {};
virtual void myfunc5() {};
virtual void myfunc6() {};
virtual void myfunc7() {};
};
int main()
{
//对象结构的发展和演化
//c++对象模型逐步建立起来
//(1)非静态的成员变量(普通成员变量)跟着类对象走(存在对象内部),也就是每个类对象都有自己的成员变量;
A aobj;
int ilen = sizeof(aobj);
cout << ilen << endl; //16 (字节对齐)
//(2)静态成员变量跟对象没有什么关系,所以肯定不会保存在对象内部,是保存在对象外面(表示所占用的内存空间和类对象无关)的。
//(3)成员函数:不管静态的还是非静态,全部都保存在类对象之外。所以不管几个成员函数,不管是否是静态的成员函数,对象的sizeof的大小都是不增加的;
//(4)虚函数:不管几个虚函数,sizeof()都是多了8个字节。
//(4.1)类里只要有一个虚函数(或者说至少有一个虚函数),这个类 会产生一个 指向 虚函数 的指针。
//有两个虚函数,那么这个类 就会产生两个指向虚函数的指针。
//类本身 指向虚函数的 指针(一个或者一堆)要有地方存放,存放在一个表格里,这个表格我们就称为“虚函数表(virtual table【vtbl】)”;
//这个虚函数表一般是保存在可执行文件中的,在程序执行的时候载入到内存中来。
//虚函数表是基于类的,跟着类走的;
//(4.2)说说类对象,这四个字节的增加,其实是因为虚函数的存在;因为有了虚函数的存在,导致系统往类对象中添加了一个指针,
//这个指针正好指向这个虚函数表,很多资料上把这个指针叫vptr;这个vptr的值由系统在适当的时机(比如构造函数中通过增加额外的代码来给值);
//---------------总结:对于类中
//(1)静态数据成员不计算在类对象sizeof()内;
//(2)普通成员函数和静态成员函数不计算在类对象的sizeof()内
//(3)虚函数不计算在类对象的sizeof()内,但是虚函数会让类对象的sizeof()增加4个字节以容纳虚函数表指针。
//(4)虚函数表[vtbl]是基于类的(跟着类走的,跟对象没关系,不是基于对象的);
//(5)如果有多个数据成员,那么为了提高访问速度,某些编译器可能会将数据成员之间的内存占用比例进行调整。(内存字节对齐)
//(6)不管什么类型指针char *p,int *q;,该指针占用的内存大小是固定的
/*int ilen2 = sizeof(char *);
int ilen3 = sizeof(int *);*/
return 1;
}
#include <iostream>
using namespace std;
class myobject
{
public:
myobject() {};//构造函数不占用
~myobject() {}; //析构函数不占用
float getvalue() const //普通成员函数
{
return m_value;
}
static int s_getcount() //静态成员函数
{
return ms_scount;
}
virtual void vfrandfunc() {}; //虚函数
protected:
float m_value; //普通成员变量 4字节
static int ms_scount; //静态成员变量
};
int main()
{
myobject obj;
int ilen = sizeof(obj);
cout << ilen << endl; //16字节,成员变量m_value占4字节,虚函数表指针占8字节。字节对齐。
return 1;
}
三.this指针调整
#include <iostream>
using namespace std;
class A {
public:
int a;
A()
{
printf("A的构造函数,A::A()的this指针是:%p!\n", this);
}
void funcA()
{
printf("A::funcA()的this指针是:%p!\n", this);
}
};
class B {
public:
int b;
B()
{
printf("B的构造函数,B::B()的this指针是:%p!\n", this);
}
void funcB()
{
printf("B::funcB()的this指针是:%p!\n", this);
}
};
class C : public A, public B
{
public:
int c;
C()
{
printf("C的构造函数,C::C()的this指针是:%p!\n", this);
}
void funcC()
{
printf("C::funcC()的this指针是:%p!\n", this);
}
void funcB()
{
printf("C::funcB()的this指针是:%p!\n", this);
}
};
int main()
{
//this指针调整:多重继承
cout << sizeof(A) << endl;//4
cout << sizeof(B) << endl;//4
cout << sizeof(C) << endl;//12
C myc;//子类实例化,会调用父类和子类的构造函数
//输出如下:
//A的构造函数,A::A()的this指针是:000000AE2FB0FC08!
//B的构造函数,B::B()的this指针是:000000AE2FB0FC0C!
//C的构造函数,C::C()的this指针是:000000AE2FB0FC08!
//可以看到A对象首地址和C对象首地址相同!和B差4字节
myc.funcA();
myc.funcB(); //已经被子类C覆盖了
myc.B::funcB();
myc.funcC();
//输出如下:
//A::funcA()的this指针是:000000AE2FB0FC08!
//C::funcB()的this指针是:000000AE2FB0FC08!
//B::funcB()的this指针是:000000AE2FB0FC0C!
//C::funcC()的this指针是:000000AE2FB0FC08!
return 1;
}
总结:
派生类对象 它是包含基类子对象的。
如果派生类只从一个基类继承的话,那么这个派生类对象的地址和基类子对象的首地址相同。
但如果派生类对象同时继承多个基类,那么大家就要注意:
第一个基类子对象的开始地址和派生类对象的开始地址相同。
后续这些基类子对象的开始地址 和派生类对象的开始地址相差多少呢?那就得吧前边那些基类子对象所占用的内存空间干掉。
你调用哪个子类的成员函数,这个this指针就会被编译器自动调整到对象内存布局中 对应该子类对象的起始地址那去;
四.分析obj目标文件,构造函数语义
// project100.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
using namespace std;
class M0TX
{
public:
M0TX() //默认构造函数
{
cout << "wodeceshi" << endl;
}
};
class MATX
{
public:
MATX() //默认构造函数
{
cout << "goodHAHAHAHA" << endl;
}
};
class MBTX
{
public:
int m_i;
int m_j;
M0TX m0; //类类型成员变量
MATX ma; //类类型成员变量
void funct()
{
cout << "IAmVeryGood" << endl;
}
};
int main()
{
cout << sizeof(M0TX) << endl;//1
cout << sizeof(MATX) << endl;//1
cout << sizeof(MBTX) << endl;//12 (4+4+1+1)==>12 ??
MBTX myb;
//wodeceshi
//goodHAHAHAHA
return 1;
}
构造函数:
默认构造函数(缺省构造函数):没有参数的构造函数
传统认识认为:如果我们自己没定义任何构造函数,那么编译器就会为我们隐式自动定义 一个默认的构造函数,我们称这种构造函数为:“合成的默认构造函数”
结论:“合成的默认构造函数”,只有在 必要的时候,编译器才会为我们合成出来,而不是必然或者必须为我们合成出来。
必要的时候 是什么时候?
每个.cpp源文件会编译生成一个.obj(.o) linux下gcc -c(汇编阶段),最终把很多的.obj(.o)文件链接到一起生成一个可执行(链接文件)。介绍在windows下怎么看obj文件。
五.构造函数语义续
编译器什么时候会自动合成默认构造函数
(1)
(上一节代码)该类MBTX没有任何构造函数,但包含一个类类型的成员ma,而该对象ma所属于的类MATX 有一个缺省的构造函数。这个时候,编译器就会为该类MBTX生成一个 “合成默认的构造函数”,合成的目的是为了调用MATX里的默认构造函数。
换句话说:编译器合成了默认的MBTX构造函数,并且在其中 安插代码,调用MATX的缺省构造函数;
(2)
父类带缺省构造函数,子类没有任何构造函数,那因为父类这个缺省的构造函数要被调用,所以编译器会为这个子类合成出一个默认构造函数。 合成的目的是为了调用这个父类的构造函数。换句话说,编译器合成了默认的构造函数,并在其中安插代码,调用其父类的缺省构造函数。
(3)
如果一个类含有虚函数,但没有任何构造函数时:因为虚函数的存在,
a)编译器会给我们生成一个基于该类的虚函数表vftable。
b)编译给我们合成了一个构造函数,并且在其中安插代码: 把类的虚函数表地址赋给类对象的虚函数表指针 (赋值语句/代码);
我们可以把 虚函数表指针 看成是我们表面上看不见的一个类的成员函数,
为什么这么麻烦,因为虚函数的调用存在一个多态问题,所以需要用到虚函数表指针。第三章详细讨论。
编译器给我们往MBTX缺省构造函数中增加了代码:
(a)生成了类MBTX的虚函数表
(b)调用了父类的构造函数
(c)因为虚函数的存在,把类的虚函数表地址赋给对象的虚函数表指针。
当我们有自己的默认构造函数时,编译器会根据需要扩充我们自己写的构造函数代码,比如调用父类构造函数,给对象的虚函数表指针赋值。
编译器干了很多事,没默认构造函数时必要情况下帮助我们合成默认构造函数,如果我们有默认构造函数,编译器会根据需要扩充默认构造函数里边的代码。
(4)
如果一个类带有虚基类,编译器也会为它合成一个默认构造函数。
虚基类:通过两个直接基类继承同一个简介基类。所以一般是三层 ,有爷爷Grand,有两个爹A,A2,有孙子C
vbtable虚基类表。 vftalble(虚函数表);
虚基类结构,编译器为子类和父类都产生了“合成的默认构造函数”
#include <iostream>
using namespace std;
class Grand //爷爷类
{
public:
};
class A : virtual public Grand
{
public:
};
class A2 : virtual public Grand
{
public:
};
class C :public A, public A2 //这里不需要virtual
{
public:
C()
{
int aa;
aa = 1;
}
};
int main()
{
C cc;
return 1;
}
六.拷贝构造函数语义
#include <iostream>
using namespace std;
class CTB
{
public:
CTB(const CTB&)
{
cout << "CTB()的拷贝构造函数执行了" << endl;
}
CTB()
{
}
virtual void mvirfunc() {}
};
class CTBSon:public CTB
{
public:
//virtual void mvirfunc() {}
};
class ASon
{
public:
int m_testson;
};
class A
{
public:
int m_test;
ASon asubobj;
CTB m_ctb;
};
//虚继承
class Grand //爷爷类
{
public:
};
class A1 :virtual public Grand
{
public:
};
class A2 :virtual public Grand
{
public:
};
class C :public A1, public A2
{
public:
};
int main()
{
//***********************情况一********************
A mya1;
mya1.m_test = 15;
mya1.asubobj.m_testson = 120;
A mya2 = mya1;
//这个mya2.m_test = 15,这个其实是编译器内部的一个手法:没有调用拷贝构造函数!
//成员变量初始化手法,比如int这种简单类型,直接就按值就拷贝过去,编译器不需要合成拷贝构造函数的情况下就帮助我们把这个事情办了;
//a) A mya2 = mya1; 是拷贝构造一个对象。
//b)我们自己也没有写类A的拷贝构造函数,编译器也没有帮助我们生成拷贝构造函数。
//c)我们却发现mya1对象的一些成员变量值确实被拷贝到mya2中去。这是编译器内部的一些直接拷贝数据的实现手法,
//比如类A中有类类型ASon成员变量asubobj,也会递归是的去拷贝类ASon的每个成员变量。
//***********************情况二********************
CTBSon myctbson1;
CTBSon myctbson2 = myctbson1;
//如果一个类CTBSon没有拷贝构造函数,但是它有一个父类CTB,父类有拷贝构造函数,
//当代码中有涉及到类CTBSon的拷贝构造时,编译器会为CTBSon合成一个拷贝构造函数 ,调用父类的拷贝构造函数。
//***********************情况三********************
//如果一个类没有拷贝构造函数, 但是该类含有虚基类(解决多继承问题)
//当代码中有涉及到类的拷贝构造时,编译器会为该类合成一个拷贝构造函数;
C cc;
C cc2 = cc; //当代码中有涉及到类的拷贝构造时
return 1;
}
传统上,大家认为:如果我们没有定义一个自己的拷贝构造函数,编译器会帮助我们合成 一个拷贝构造函数
那编译器在什么情况下会帮助我们合成出拷贝构造函数来呢?那这个编译器合成出来的拷贝构造函数又要干什么事情呢?
(1)
如果一个类A没有拷贝构造函数,但是含有一个类类型CTB的成员变量m_ctb。该类型CTB含有拷贝构造函数,那么
当代码中有涉及到类A的拷贝构造时,编译器就会为类A合成一个拷贝构造函数。
编译器合成的拷贝构造函数往往都是干一些特殊的事情。如果只是一些类成员变量值的拷贝这些事,编译器是不用专门合成出拷贝构造函数来干的,编译器内部就干了;
(2)
如果一个类CTBSon没有拷贝构造函数,但是它有一个父类CTB,父类有拷贝构造函数,当代码中有涉及到类CTBSon的拷贝构造时,编译器会为CTBSon合成一个拷贝构造函数 ,调用父类的拷贝构造函数。
(3)
如果一个类CTBSon没有拷贝构造函数,但是该类声明了虚函数或者继承了虚函数(父类有虚函数,父类合成拷贝构造,子类也会合成,以处理虚函数表指针)当代码中有涉及到类CTBSon的拷贝构造时,编译器会为CTBSon合成一个拷贝构造函数 ,往这个拷贝构造函数里插入语句:这个语句的含义 是设定类对象myctbson2的虚函数表指针值。虚函数表指针,虚函数表等概念,详细可以学习第三章。
(4)
如果 一个类没有拷贝构造函数, 但是该类含有虚基类(解决多继承问题)当代码中有涉及到类的拷贝构造时,编译器会为该类合成一个拷贝构造函数
七.程序转化语义
// project100.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
using namespace std;
class X
{
public:
int m_i;
X(const X &tmpx)
{
m_i = tmpx.m_i;
cout << "拷贝构造函数被调用" << endl;
}
X()
{
m_i = 0;
cout << "构造函数被调用" << endl;
}
~X()
{
cout << "析构函数被调用" << endl;
}
void functest()
{
cout << "functest()被调用" << endl;
}
};
/*******************************************/
//人类视角
X func()
{
X x0;
//....
return x0; //系统产生临时对象并把x0的内容拷贝构造给了临时对象。返回的是临时对象,真的x0这个函数解束被析构
}
//编译器角度的func
void func(X &extra)
{
X x0; //从编译器角度,这行不调用X的构造函数
//...
//...
extra.X::X(x0);
return;
}
/************************************************/
int main()
{
情形(1)定义时初始化对象(程序员视角)
X x0;
x0.m_i = 15;
X x1 = x0; //定义的时候初始化
X x2(x0);
X x3 = (x0);
/*
输出:
构造函数被调用
拷贝构造函数被调用
拷贝构造函数被调用
拷贝构造函数被调用
析构函数被调用
析构函数被调用
析构函数被调用
析构函数被调用
*/
//切换到编译器角度,编译器会拆分成两个步骤(编译器视角)
//cout << "---------------" << endl;
//X x100; //步骤一:定义一个对象,为对象分配内存。从编译器视角来看,这句是不调用X类的构造函数。
//x100.X::X(x0); //步骤二:直接调用对象的拷贝构造函数去了;
//----------------------------------------------------------------------
情形(2)参数的初始化(程序员视角/现代编译器)
X x0;
func(x0);
老编译器视角
//X tmpobj; //编译器产生一个临时对象为对象分配内存。从编译器视角来看,这句是不调用X类的构造函数。
//tmpobj.X::X(x0); //调用拷贝构造函数
//func(tmpobj); //用临时对象调用func
//tmpobj.X::~X(); //func()被调用完成后,本析构被调用。
//----------------------------------------------------------------------
//情形(3)返回值初始化(程序员角度)
//X my = func();
//编译器对上述代码的理解(编译器角度)
//X my; //不会调用X的构造函数,分配内存
//func(my);
//人类视角
//func().functest();//临时对象调用成员函数
//切换到编译器视角
//X my; //不会调用X的构造函数
//(func(my), my).functest(); //逗号表达式:先计算表达式1,再计算表达式2,整个逗号表达式的结果是表达式2的值;
//程序员视角
//X(*pf)(); //定义个函数指针
//pf = func;
//pf().functest();
//编译器视角
//X my; //不调用构造函数,分配内存
//void(*pf)(X &);
//pf = func;
//pf(my);
//my.functest();
return 1;
}
八.程序的优化
// project100.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include "pch.h"
#include <iostream>
#include <time.h >
using namespace std;
class CTempValue
{
public:
int val1;
int val2;
public:
CTempValue(int v1 = 0, int v2 = 0) :val1(v1), val2(v2) //构造函数
{
/* cout << "调用了构造函数!" << endl;
cout << "val1 = " << val1 << endl;
cout << "val2 = " << val2 << endl;*/
}
CTempValue(const CTempValue &t) :val1(t.val1), val2(t.val2) //拷贝构造函数
{
//cout << "调用了拷贝构造函数!" << endl;
}
virtual ~CTempValue()
{
//cout << "调用了析构函数!" << endl;
}
};
//函数(开发者视角)
CTempValue Double(CTempValue &ts)
{
//CTempValue tmpm; //消耗一个构造函数,一个析构函数
//tmpm.val1 = ts.val1 * 2;
//tmpm.val2 = ts.val2 * 2;
//return tmpm; //生成一个临时对象,然后调用拷贝构造函数把tmpm的内容拷贝构造到这个临时对象中去,然后返回临时对象。
// //这个临时对象也消耗了一个拷贝构造函数 ,消耗了一个析构函数;
//
return CTempValue(ts.val1 * 2, ts.val2 * 2); //生成一个临时对象。
}
//double(编译器视角)
void Double(CTempValue &tmpobj, CTempValue &ts) //编译器会插入第一个参数
{
tmpobj.CTempValue::CTempValue(ts.val1 * 2, ts.val2 * 2);
return;
}
int main()
{
//第八节 程序的优化
//从开发者层面
//从编译器层面
//(1)开发者层面的优化(开发者视角)
CTempValue ts1(10, 20);
//CTempValue ts2 = Double(ts1);
//Double(ts1);
CTempValue tmpobj;
clock_t start, end;
start = clock(); //程序开始到本行执行时所用的毫秒数
cout << "start = " << start << endl;
for (int i = 0; i < 1000000; i++)
{
//Double(ts1);
Double(tmpobj, ts1);
}
end = clock();
cout << "end = " << end << endl;
cout << end - start << endl;
//编译器视角
/*CTempValue ts1;
ts1.CTempValue::CTempValue(10, 20);
CTempValue tmpobj;
Double(tmpobj, ts1);*/
//(2)从编译器层面的优化
//linux编译器g++优化,针对 与返回临时对象这种情况。 NRV优化(Named Return Value)。
//RVO(Return Value Optimization);
//g++ -fno-elide-constructors 2_8.cpp -o 2_8
//优化说明:
//(1)编译器是否真优化了,不好说,你要做各种测试才知道;
//(2)如果你的代码很复杂,编译器可能放弃不优化;
//(3)不要过度优化;
//(4)优化可能使用你犯错误
return 1;
}
九.程序优化续、拷贝构造续,深浅拷贝
#include <iostream>
#include <cstring>
using namespace std;
class X
{
public:
int m_i;
int *p_mi;
X(const X& tmpx)
{
p_mi = new int(100); //我们自己创建内存
memcpy(p_mi, tmpx.p_mi, sizeof(int)); //把目标对象的内存内容拷贝过来,叫深拷贝。
m_i = tmpx.m_i;
cout << "copy constructor" << endl;
}
X() //缺省构造函数
{
p_mi = new int(100);
m_i = 0;
cout << "default constructor" << endl;
}
~X()
{
delete p_mi;
cout << "delete constructor" << endl;
}
//explicit X(int value) :m_i(value) //类型转换构造函数
X(int value) :m_i(value) //类型转换构造函数
{
p_mi = new int(100);
cout << "X(int) constructor" << endl;
}
};
int main()
{
cout << "--------begin------" << endl;
X x10(1000);
cout << "-------------------" << endl;
X x11 = 1000; //隐式类型转换
cout << "-------------------" << endl;
X x12 = X(1000);
cout << "-------------------" << endl;
X x13 = (X)1000;
cout << "-------------------" << endl;
X x14 = x13;
cout << "--------end--------" << endl;
//输出:
/*
vs2017自带优化,把其中一些拷贝构造函数优化没了
--------begin------
X(int) constructor
-------------------
X(int) constructor
-------------------
X(int) constructor
-------------------
X(int) constructor
-------------------
copy constructor
--------end--------
*/
//总结:当编译器面临用一个类对象作为另外一个类对象初值的情况,各个编译器表现不同。但是所有编译器都为了提高效率而努力。
//我们也没有办法确定我们自己使用的编译器是否一定会调用拷贝构造函数。
//拷贝构造函数是否必须有? 不一定,视情况而定。
//如果你只有一些简单的成员变量类型,int,double,你会发现你根本不需要拷贝构造函数;编译器内部本身就支持成员变量的
//bitwise(按位) copy 按位拷贝
//当需要处理很复杂的成员变量类型的时候(有指针等)。
//因为我们增加了自己的拷贝构造函数,导致编译器本身的bitwise拷贝能力失效,所以结论:
//如果你增加了自己的拷贝构造函数后,就要对各个成员变量的值的初始化负责了;
//深浅拷贝问题;
return 1;
}
在linux下g++ t.cpp -o t -fno-elide-constructors运行
十.成员初始化列表
何时必须用成员初始化列表:
a)如果这个成员是个引用
b)如果是个const类型成员
c)如果你这个类是继承一个基类,并且基类中有构造函数,这个构造函数里边还有参数。
d)如果你的成员变量类型是某个类类型,而这个类的构造函数带参数时;
class Base
{
public:
int ba;
int bb;
Base(int tmpa, int tmpb)
{
}
};
class CSub
{
public:
CSub(int tmpv)
{
}
};
class A:public Base
{
public:
int m_x;
int m_y;
int &m_yy;
const int m_myc;
CSub cmysub;
//A() :m_x(0), m_y(0)
A(int &tmpvalue):m_yy(tmpvalue), m_myc(tmpvalue),Base(tmpvalue, tmpvalue), cmysub(tmpvalue)
{
//m_myc = tmpvalue;
//m_yy = tmpvalue;
m_x = 0;
m_y = 0;
m_yy = 180;
}
};
使用初始化列表的优势(提高效率):
除了必须用初始化列表的场合,我们用初始化列表还有什么其他目的? 有,就是提高程序运行效率。 对于类类型成员变量xobj放到初始化列表中能够比较明显的看到效率的提升 但是如果是个简单类型的成员变量 比如 int m_test,其实放在初始化列表或者放在函数体里效率差别不大; 提醒:成员变量初始化尽量放在初始化列表里,显得 高端,大气上档次初始化列表细节探究:
(3.1)初始化列表中的代码可以看作是被编译器安插到构造函数体中的,只是这些代码有些特殊;
(3.2)这些代码 是在任何用户自己的构造函数体代码之前被执行的。所以大家要区分开构造函数中的用户代码 和 编译器插入的 初始化所属的代码。
(3.3)这些列表中变量的初始化顺序是 定义顺序,而不是在初始化列表中的顺序。
老师 不建议 在初始化列表中 进行 两个 都在初始化列表中出现的成员之间的初始化