第一章:关于对象
#C++在布局以及存取时间上的额外负担主要是由虚函数机制和虚基类造成的。
#虚函数不计算在类对象的sizeof()内,但是虚函数会让类对象的sizeof()增加4个字节以容纳虚函数表指针
#一个类对象(包括空类)至少占用1个字节的内存空间。C++编译器会给每个空对象分配一个字节的内存空间,是为了区分空对象占用的内存位置,每个空对象都有独一无二的内存地址。
#类中不管有几个虚函数,类内都只会产生一个虚函数指针(指针大小为4),指向虚函数表,表内存放虚函数地址。
1.1 C++对象模式
#类成员变量:static,nonstatic
#类成员函数:static,nonstatic,virtual
#只有非静态成员变量(int a=1)和虚函数指针(vptr)才属于类的对象内。vptr指向虚函数表,表内依次存放每个非静态成员函数(包括虚函数和非静态成员函数)的地址。静态成员函数位于类对象外部。
1.2 关键词带来的差异
#struct class
1.3 对象的差异
第二章:构造函数语意学
2.1 默认构造的构造操作
#c++新手的两个误解:
1)任何类如果没有定义默认构造函数,就会被合成出来一个。
2)编译器合成出来的默认构造函数会显式设定“类内每一个数据成员的默认值”。
上述两种说法都是错误的!
#C++编译器在以下四种情况下会给类生成默认构造函数:
一、这个类中有成员是另一个类的对象,且该类对象内含有默认构造函数。那么C++编译器会给这个类也生成一个默认构造函数,用来调用其成员对象的构造函数,完成该成员的初始化构造。
二、这个类的父类有默认构造函数。那么C++编译器也会帮你生成该子类的默认构造函数,以调用父类的默认构造函数,完成父类的初始化。
三、这个类中存在虚函数(自己声明或者继承得到)。那么C++编译器会为你生成默认构造函数,在编译期生成函数虚表和虚函数指针指向虚函数表。
四、这个类存在虚基类(有直接虚基类或者继承链上有虚基类)。那么C++编译器会为你生成默认构造函数,以初始化虚基类表(vbtable)。
2.2 拷贝构造的构造操作
#如果class中展现了位逐次拷贝,编译器就不会产生出拷贝构造函数。当没有产生拷贝构造的时候,我们的类怎么产生呢。就是通过位逐次拷贝来搞定,也就是将源类中成员变量中的每一位都逐次拷贝到目标类中,这样我们的类就构造出来了。
#C++编译器在以下四种情况下会给类生成拷贝构造函数(即不展现位逐次拷贝):
一、这个类中有成员是另一个类的对象,且该类对象内含有拷贝构造函数。
二、这个类的父类有拷贝构造函数。
三、这个类中存在虚函数(自己声明或者继承得到)。
四、这个类存在虚基类(有直接虚基类或者继承链上有虚基类)。
2.3 程序转化语意学
#类的构造函数必须使用成员初始化列表来进行初始化的四种情况:
一、初始化引用类型成员时
二、初始化const成员时
三、这个类继承于一个含有有参构造函数的父类
四、这个类的内部含有另一个类对象,且这个类对象含有有参构造函数
#初始化顺序由声明顺序决定,而与初始化列表中的顺序无关,因此编译器会对初始化列表进行处理并有可能重新排序。
#使用初始化列表进行初始化的好处:提高效率。
赋值操作 A::A(int age){m_age=age;}。这种赋值版本会首先调用默认构造函数,然后再对变量赋新值,默认构造的一切作为都浪费了。
第三章 Data语意学
# class X{}; //sizeof(X)=1 空类占一个字节
class Y:public virtual X{}; //sizeof(Y)=1+3=4 Y为空类占一个字节,内存对齐填充3个字节
class Z:public virtual X{}; //sizeof(Z)=1+3=4 Z为空类占一个字节,内存对齐填充3个字节
class A:public Y,public Z{}; //sizeof(A)=8 上诉的sizeof都视具体编译器而定
3.4 继承对类内数据的影响
#单纯简单的继承(无多态),不会增加内存空间和存取上的额外负担(注意是不会增加额外负担,但是子类内存空间=父类的内存+子类自身的内存)
#加上多态即有虚函数时,会有额外时间空间开销。当类中有虚函数时,会在类中导入一个虚函数指针(虚函数表指针位于对象内存的开头)指向虚函数表,表中存放每个虚函数的地址。还会加强构造函数,为了在编译阶段给虚函数指针设置初值,也会加强析构函数,为了处理虚函数指针。
#一个类只有包含虚函数才会存在虚函数表,同属于一个类的对象共享虚函数表,但是有各自的vptr(虚函数表指针),当然所指向的地址(虚函数表首地址)相同。
#父类中有虚函数就等于子类中有虚函数。
#但不管是父类还是子类,(单继承时)都只会有一个虚函数表,不能认为子类中有一个虚函数表+父类中有一个虚函数表
#如果子类中完全没有新的虚函数,则我们可以认为子类的虚函数表和父类的虚函数表内容相同.但仅仅是内容相同,这两个虚函数表在内存中处于不同位置,换句话来说,这是内容相同的两张表
#虚函数表中每一项,保存着一个虚函数的首地址,但如果子类的虚函数表某项和父类的虚函数表某项代表同一个函数(这表示子类没有覆盖父类的虚函数),则该表项所执行的该函数的地址应该相同。
3.5 数据成员布局
#比较晚出现的成员变量在内存中有更高的地址;
第四章 函数语意学
#4.1 类的函数调用方式
一、普通成员函数调用方式:编译器内部实际上是将对普通成员函数的调用转换成了对全局函数的调用。具体来说就是将函数名称进行重写,编译器额外增加一个this指针指向生成的类对象,普通成员变量的存取也通过this指针来进行。
二、虚函数的调用方式:通过虚函数表指针查找虚函数表,通过虚函数表再找到虚函数的入口地址,完成对虚函数的调用。
三、静态成员函数的调用方式:可以用类对象调用,也可以被类直接调用;静态成员函数没有this指针,这点最重要;无法直接存取类中普通的非静态成员变量(只能存取类中静态成员变量);静态成员函数不能被const和virtual修饰;静态成员函数,由于没有this指针,所以可以作为回调函数或者作为线程的主函数。
PS:静态成员函数不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。而且静态成员函数没有this指针,无法访问虚函数指针,也就无法访问虚函数表。
PS:当声明一个非静态成员函数为const时,对this指针会有影响。对于一个Test类中的const修饰的成员函数,this指针相当于Test const *, 而对于非const成员函数,this指针相当于Test *。而static成员函数没有this指针,所以使用const来修饰static成员函数没有任何意义。
#4.2 inline函数:避免函数调用带来的开销。
eg) inline int min(int i,int j){return i<j?i:j;}
a=min(v1,v2);被拓展成a=v1<v2?v1:v2; //代码不会暴涨
b=min(12,23);被拓展成b=12; //代码不会暴涨
c=min(f(),g()+1);被拓展成c=(temp1=f()),(temp2=g()+1),temp1<temp2?temp1:temp2;
当inline函数内部有局部变量或者调用inline时参数为临时对象时,调用inline函数太多次的话,会产生大量的拓展代码(临时对象),使程序大小暴涨。
第五章:构造、析构、拷贝语意学
5.2 继承体系下的对象构造
#先构造父类再构造子类,先析构子类再析构父类
第六章:执行期语意学
6.2 new/delete/placement new
# A *pa = new A(); //函数调用
A *pa2 = new A;
带括号的初始化会把一些和成员变量有关的内存清0,但不是整个对象的内存全部清0
new 干了两个事:一个是调用operator new(malloc),一个是调用了类A的构造函数
delete干了两个事:一个是调用了类A的析构函数,一个是调用operator delete(free)
# int* p=new int (5); delete p;
p指向的对象会因delete而结束,但可以继续把p当做一个void*指针来用。
# int* q=new int[100]; //这个预先分配出来的指针类型可以和Person指针类型一样,也可以不一样
Person* p=new(q) Person;//p指向新产生出来的Person类对象,这个类对象放在q指向的内存块里面。
6.3 临时对象
#class a,class b;class c=a+b;//编译器会产生一个临时对象放置a+b的结果,然后再使用class的拷贝构造函数将临时对象当做c的初始值。优化方法是直接以拷贝构造的方式将a+b的值放到c中。
PS:内存池
malloc:内存浪费,额外cookie,频繁分配小块内存,则浪费更加显得明显。减少malloc()调用次数就意味着减少对内存的浪费
内存池的实现原理:用malloc申请一大块内存,当你要分配的时候,我从这一大块内存中一点一点的分配给你,当一大块内存分配的差不多的时候,我再用malloc再申请一大块内存,然后再一点一点的分配给你。这样减少了额外cookie内存开销,避免浪费内存,提高运行效率。
第七章:站在对象模型的尖端
7.2 异常处理
#C++的异常处理由三个主要的组件构成
一、一个throw子句。主要是用来抛出异常。它在程序某处发出一个异常,被抛出去的异常可以是内置类型也可以是自定义类型。
二、一个或多个catch子句。每一个catch子句都是一个异常处理,处理某种类型的异常,并在大括号区段中提供实际的处理程序。将异常类型和每一个catch子句的类型进行比较,匹配成功就进入catch内部的处理程序来处理异常。catch的匹配过程是找最先匹配的,不是最佳匹配。如果throw中抛出一个对象,那么无论是catch中使用什么接收(基类对象、引用、指针或者子类对象、引用、指针),在传递到catch之前,编译器都会另外构造一个对象的副本。也就是说,如果你以一个throw语句中抛出一个对象类型,在catch处通过也是通过一个对象接收,那么该对象经历了两次复制,即调用了两次复制构造函数。一次是在throw时,将“抛出到对象”复制到一个“临时对象”(这一步是必须的),然后是因为catch处使用对象接收,那么需要再从“临时对象”复制到“catch的形参变量”中; 如果你在catch中使用“引用”来接收参数,那么不需要第二次复制,即形参的引用指向临时变量。
PS:析构函数应该从不抛出异常。如果析构函数中需要执行可能会抛出异常的代码,那么就应该在析构函数内部将这个异常进行处理,而不是将异常抛出去。
原因:在为某个异常进行栈展开时,析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库 terminate 函数。而默认的terminate 函数将调用 abort 函数,强制从整个程序非正常退出。
PS:构造函数中可以抛出异常。但是要注意到:如果构造函数因为异常而退出,那么该类的析构函数就得不到执行。所以要手动销毁在异常抛出前已经构造的部分。
C++的异常处理_daheiantian的博客-CSDN博客_c++ 异常
三、一个try区段。