《深度探索C++对象模型》…
https://blog.youkuaiyun.com/thalo1204/article/details/81571897
https://dsqiu.iteye.com/blog/1669614
第四章 函数语意学
预备知识:
mangling:用C++flit名字Name-mangling是指为了在目标文件符号表中和连接过程中使用的名字通常和编译目标文件的源程序中的名字不一样
第一人:
{放着,感觉看这一章的时候快睡着了。}
第二人:
{
4.1 Member的各种调用方式
Nonstatic Member Functions
实际上编译器是将member function被内化为nonmember的形式,经过下面转化步骤:
1.给函数添加额外参数——this。
2.将对每一个nonstaitc data member的存取操作改为this指针来存取。
3.将member function 重写成一个外部函数。对函数名精选mangling 处理,使之成为独一无二的语汇。
Virtual Member Functions
将
ptr->f(); //f()为virtual member function
内部转化为
(*ptr->vptr1;
其中:
vptr表示编译器产生的指针,指向virtual table。它被安插在每一个声明有(或继承自)一个或多个virtual functions 的class object 中。
1 是virtual table slot的索引值,关联到normalize()函数。
第二个ptr表示this指针。
Static Member Functions
不能被声明为 const volatile 或virtual。
一个static member function 会提出于class声明之外,并给予一个经过mangling的适当名称。如果取一个static member function 的地址,获得的是其在内存的位置也就是地址,而不是一个指向“class member function”的指针,如下:
&Point::count();
会得到一个数值,类型是:
unsigned int(*)();
而不是:
unsigned int(Point:?)();
4.2 Virtual Member Funcitons
C++中,多态表示以“一个public base class 的指针(或reference),寻址出一个derived class object”。
每一个class 只会有一个virtual table,每一个table 含有对应的class object中所有active virtual functions 函数实体地址。这些active virtual function 包括:
1.这个class 所定义的函数实体(改写(overriding)一个可能存在的base class virtual function函数实体。
2.继承自base class 的函数实体(不被derived class改写)
3.一个pure_virtual_called()。
一个类继承函数virtual table的三种可能性:
1.继承base class 所声明的virtual functions的函数实体。正确地说,是该函数实体的地址会被拷贝到derived class的virtual table相对应的slot之中。
2.使用自己的函数实体。这表示它自己的函数实体地址必须放在对应的slot之中。
3.可以加入一个新的virtual function。这时候virtual table 的尺寸会增大一个slot放进这个函数实体地址。
编译时期设定virtual function的调用:
一般而言,我并不知道ptr 所指对象的真正类型。然而可以经由ptr 可以存取到该对象的virtual table。
虽然我不知道哪个Z()函数实体被调用,但知道每一个Z()函数地址都被放置slot 4的索引。
这样我们就可以将
ptr->z();
转化为:(*ptr->vptr[4])(ptr);
唯一一个在执行期才能知道的东西是:slot4所指的到底是哪一个z()函数实体。
多重继承下的 Virtual Functions
在多重继承中支持virtual functions,其复杂度围绕在第二个及其后面的base class 上,以及“必须在执行期调整this 指针”这一点。一般规则是,经由指向“第二或后继base class 的指针”来调用derived class virtual function。调用操作连带的“必要的this指针调整”操作,必须在执行期完成。
虚拟继承下的 Virtual Functions
4.3 函数的效能
***nonmemeber、static member或nonstatic member函数都被转换为完全相同形式,所以三者效率完全相同。***
4.4 指向Member Function的指针
取一个nonstatic data member的地址,得到的结果是该member在class 布局中的bytes位置,所以它需要绑定于某个class object的地址上,才能够被存取。
取一个nonstatic member function的地址,如果该函数是nonvirtual,则得到的是内存的真正地址,然后这个值也是不完全的,也需要绑定于某个class object的地址上,才能够调用函数。
支持“指向Virtual Member Function”之指针
对于一个virtual function,其地址在编译时期是未知的,所能知道的仅是virtual function在其相关之virtual table的索引值,也就是说,对于一个virtual member function 取其地址,所能获得的只是一个索引值。
4.5 Inline Funcitons
形参 传入参数,直接替换 传入常量,连替换都省了,直接变成常量 传入函数运行结果,则需要导入临时变量
局部变量 局部变量会被mangling,以便inline函数被替换后名字唯一 也就是说一次性调用N次,就会出现N个临时变量……程序的体积会暴增
}
第五章 构造、解构、拷贝 语意学
预备知识:
- 深拷贝与浅拷贝 https://www.cnblogs.com/always-chang/p/6107437.html
浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,析构时会造成内存泄漏(内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费(重点),导致程序运行速度减慢甚至系统崩溃等严重后果。 你这个在已经释放过一次后,第二次释放会失败,但是不会对系统造成内存的浪费(可能会直接崩溃),所以说,应该是不能叫内存泄漏的。这种重复释放的应该是直接崩溃的。。。)。深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
-copy constructor and copy assignment operator
https://blog.youkuaiyun.com/lixiang212121/article/details/45509769 - 成员初始化列表:
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。例如:
class CExample {
public:
int a;
float b;
//构造函数初始化列表
CExample(): a(0),b(8.8)
{}
//构造函数内部赋值
CExample()
{
a=0;
b=8.8;
}
};
上面的例子中两个构造函数的结果是一样的。上面的构造函数(使用初始化列表的构造函数)显式的初始化类的成员;而没使用初始化列表的构造函数是对类的成员赋值,并没有进行显式的初始化。
初始化和赋值对内置类型的成员没有什么大的区别,像上面的任一个构造函数都可以。对非内置类型成员变量,为了避免两次构造,推荐使用类构造函数初始化列表。但有的时候必须用带有初始化列表的构造函数:
1、初始一个引用成员
2、初始化一个const 成员
3、当调用一个基类的构造函数且它有一组参数时
4、当调用一个成员类的构造函数,而它有一组参数时
因为const对象或引用类型只能初始化,不能对他们赋值。
- 虚基类
正如这张图片所示,若是若是类A和B继承父类时不加virtual public的话 当C对象产生时就会调用两次费父类的构造函数。- 虚析构函数:
问题:由于通过基类指针删除派生类对象时,只调用基类的析构函数,而不会调用派生类的对象,于是就演变出虚析构函数可以以很好的解决问题。注:类如果定义了虚函数,则最好将析构函数也定义为虚函数。
- 虚析构函数:
5.0 前言
虚函数最好不用const
constructor(用户自定或者被合成)的调用伴随了哪些步骤:
1.初始化列表(member initialization list)的data members初始化操作会被放进constructor的函数本身,并以membs的声明顺序为顺序。
2.如果有一个member并没有在初始化列表中,但它在一个constructor里面,那么该 constructor 必须被调用。
3.在那之前,如果class object有virtual table pointer(s),它(们)必须被设定初始值,指定适当的virtual table(s)。
4.在那之前,所有上一层的base class constructors 必须被调用,以base class 的声明顺序为顺序(与初始化列表的顺序没有关联)。
*****如果base class 被列于初始化列表中,那么任何明确指定参数都应该传递过去。
*****如果base class 没有列于初始化列表,那么调用default constructor。
*****如果base class 是多重继承下的第二或后面的base class ,那么this指针必须有所调整。
5.在那之前,所有 virtual base class constructors 必须被调用,从左到右,从最深到最浅。
如果class 被列于初始化列表中,那么如果有任何明确指定的参数,都应该传递过去,若没有列于初始化列表中,则调用default constructor。
此外,class中的每一个virtual base class subobject的偏移量必须在执行期可存取。
如果class object 是最底层的class,某constructors可能被调用;某些用以支持这个行为的机制必须被放进来。
对象复制语意学
当设计一个class,并以一个class object 指定另一个class object时,有三种选择:
1.什么都不做,实施默认行为。
2.提供一个explicit copy assignment operator。
3.明确拒绝一个class object指定给另一个class object。
一个class对于默认的copy assignment operator,在以下情况下不会表现出 bitwise copy语意:
1.当一个class的base class 有一个copy assignment operator时,
2.当一个class 的member object,而其class 有一个copy assignment operator时,
3.当一个class 声明了任何virtual functions时,
4.当class继承一个virtual base class 时。
vptr语意学
vptr在constructor何时被初始化?在base class constructors调用操作之后,但是在程序员供应的码或是初始化列表中所列的members初始化操作之前。
解构语意学
析构的合成情况:
如果类没有析构,那么只有1、类内 带类成员(此类成员拥有析构函数)2、自己的基类拥有析构函数下,编译器才会合成。
否则,析构被视为不需要,也不需要合成(更不需要调用)反而朱门写一个不应有的析构函数,会影响效率。
destructor被扩展的方式:类似构造函数的扩展,但顺序相反,析构的顺序如下:
1.destructor的函数本身首先被执行。
2.如果class拥有member class objects,而后拥有destructor,那么它们会以声明顺序的相反顺序被调用。
3.如果object内带一个vptr,则现在被重新设定,指向适当的base class virtual table。
4.如果有任何直接的(上一层)nonvirtual base classes 拥有destructor ,它们会以声明顺序相反顺序调用。
5.如果有任何virtual base classes 拥有destructor,而当前讨论的这个class 是最尾端的class,那么它们会以其原来顺序相反顺序被调用。
第六章 执行期语意学
6.1 对象的构造和析构 (前提析构已经定义)
定义了一个对象,什么时候会自动调用其构造函数,什么时候去调用析构函数
- 全局对象 main函数第一次使用全局对象之前,把对象构造出来,main结束之前把对象析构掉
- 局部静态对象 ——只会初始化一次,但是可能会用到很多次,所以什么时候去构造和析构(C++标准已经强制要求,在调用时才选择构造,等程序结束时析构)
- 数组 —— 和new delete那样的,是否会去调用每个元素的构造函数和析构函数,对于只初始化部分元素的呢?没看懂。。
6.2 new与delete运算符
6.3 临时性对象
第七章 站在对象模型的尖端
7.1 Template
7.2 异常处理
C++异常处理有三个主要的语汇组件构成:
1、一个throw子句。它在程序某处发出一个异常。被丢出去的异常可以是内建类型,也可以是使用者自定类型。
2、一个或多个catch子句,每一个catch子句都是一个异常处理。它用来表示说,这个子句准备处理某种类型的异常,并且在封闭的大括号区段中提供实际的处理程序。
3、一个try区段。它被围绕以一系列的叙述句,这些叙述句可能引发catch子句起作用。
7.3 执行期类型识别
7.4 效率有了,弹性呢?
补充:类型向上转型和多态的混淆
构造这样的一个继承体系:
class Base {
public: virtual ~Base() {}
virtual void show() { cout << “Base” << endl; }
};
class Derived : public Base {
public: void show() { cout << “Derived” << endl; }
};
子类Derived类重写了基类Base中的show方法。 编写下面的测试代码:
Base b; Derived d;
b.show(); d.show();
结果是:
Base
Derived
Base的对象调用了Base的方法,而Derived的对象调用了Derived的方法。因为直接用对象来调用成员函数时不会开启多态机制,故编译器直接根据b和d各自的类型就可以确定调用哪个show函数了,也就是在这两句调用中,编译器为它们每一个都确定了一个唯一的入口地址。这实际上类似于一个重载多态,虽然这两个show函数拥有不同的作用域。 那这样呢: Base b; Derived d; b.show(); b = d; b.show(); 现在,一个Base的对象被赋值为子类Derived的对象。
那这样呢:
Base b; Derived d;
b.show(); b = d; b.show();
现在,一个Base的对象被赋值为子类Derived的对象。
结果是:
Base
Base
对于熟悉Java的人而言,这不可理解。但实际上,C++不是Java,它更像C。“b = d”的意思,并不是Java中的“让一个指向Base类的引用指向它的子类对象”,而是“把Base类的子类对象中的Base子对象分割出来,赋值给b”。所以,只要b的类型始终是Base,那么b.show()调用的永远都是Base类中的show函数;换句话说,编译器总是把Base中的那个show函数的入口地址作为b.show()的入口地址。这根本就没用上多态。
单继承下的重写多态
那我们再这样:
Base b; Derived d;
Base *p = &b;
p->show();
p = &d;
p->show();
这时,结果就对了:
Base
Derived
p是一个指向基类对象的指针,第一次它指向一个Base对象,p->show()调用了Base类的show函数;而第二次它指向了一个Derived对象,p->show()调用了Derived类的show函数。
总结:也就是说,只有是指针或者引用才是真正的多态,将子对象赋给父类对象其实类型向上转型……
个人觉得C++容易弄混淆的地方(持续更新):
1.const和指针的修饰问题
const char * a; //一个指针a指向const char
char const *a; //这两个是a指向的内容是常量,不能改变
char * const a; //首先a 是指针然后还是const
const (char*) a; //这两个是a指针本身是常量,指针本身不能改变
其实,可以看出如果const修饰的char(也就是类型本身或者是 *variable对指针的解引用)就是指针指向的内容是常量,反之就是修饰指针本身的。那我们可以总结一个识别方法就是:看const 两边(当然有的只有一边)的类型是类型(指针指向的内容)就是类型变量本身是常量(如const char * a和char const *a 的const两边是char,*a)。
当然两者都是常量就是:const char * const a;第一个const是类型常量,第二个才是指针常量。同样给出 const char &a ;const char *a;在传递参数时使用。
2.数组和指针的组合问题
char * a[M]; 这是指针数组,就是每一个元素是指针的数组,每个元素都要初始化。a[M]一看就是数组,这个数组每一个元素是char *,所以可以将char *扩展为一维数组然后a[M]就是二维数组了。其实就是M个指针。
char (*a)[N]; 这是一个指针,这个指针指向N个char元素,即指向数组的指针,其实就是一个指针。把(*a)看着一个变量,这个变量是指向N个元素的指针,所以只是一个一维数组。把char (*a)[N]看成是char b[N]就可以了。
同理,也可以用修饰的道理来区分,可以自行体会。具体二维数组的动态分配的更多精彩可以查阅我的另一个博客http://dsqiu.iteye.com/blog/1683142
3.C++变量的初始化
对于内置类型局部变量不进行初始化,但是分配地址,全局变量会进行默认初始化。对于类类型局部变量(没有显式初始化)会进行默认初始化(有默认构造函数,否则报错),但其内部的内置数据成员不会进行初始化(如果在默认构造函数没有进行初始化)。数组也是同样。