@(读书笔记)
中间项目耽误了很大一段时间,今天学校算是正式放假了,放松一下(虽然我们依然没有假期)只是心里的感觉,可以暂时放下一会项目,做一些自己喜欢的事情,比如今天终于把《深入探索C++对象模型》看的差不多了,写一些读书笔记。
执行期语义学
这一章其实是看了两遍,第一遍没看懂这个执行期的语义学到底是是个什么东西,第二遍是在今天早上看的(实在不想写某项目的代码),果然还是早上的头脑清楚,效率很高,思路非常清晰,这一章的内容就是讲了程序在运行期间的临时变量以及对象如何构造与析构的。
本章开始用一个程序例子来说明,一个简单的语句在被编译器理解后变成了由一些“原子操作”构成的一个“胖子“,以及各种临时变量的生成。
本章的内容就是在我们写的程序在执行期间是被编译器如何解释的,相对于前几章的内容,这些更”动态“,比如第一节是讲的对象的在执行期间是如歌调用构造和析构函数,第二节是介绍的new
和delete
是在堆上分配空间,如何调用构造和析构函数,最后一节则是重点介绍了程序常遇到的临时对象的问题。这些东西相对与对象在内存中分布,函数在内存中的地址等,更抽象一些,更像是一些”动作“,而前面的知识更像是这些”动作“的”基本要领“,或者称作是”技术指标“。
对象的构造与析构
这里主要是介绍对象是在什么时候调用构造与析构函数的,换做是编译器语言:就是对象的构造函数和析构函数在编译期间被安插在程序的具体位置(因为对象的构造函数和析构函数是编译器来调用的,程序中还是不好直接显示直接调用他们)
举个最简单的例子,在声明对象后很快就要对它初始化(构造),在它的生命周期结束前要对它销毁(析构)1
2
3
4
5
6{
Point pt;
// Constructor may be here
...
// Destructor may be here
}
当然了,程序结构不是都这么简单的,比如常见的程序结构:条件,循环,Switch
结构,甚至还有goto
语句,对象出现的位置只有一个,但是消失的位置可能是多个中的一个(不确定),编译器会在任何可能的位置添加析构函数。
全部变量
上面这些还是动态的局部变量,非常普通,不需要额外的处理。但是当遇到全局对象(非内置类型)就不这么简单了。
- 普通的变量不需要有初始值,只要在程序运行到之前给它初始值就可以了,也就是在执行期执行构造函数。
- 普通的动态变量是存放在堆栈中的,他们在程序执行的整个周期的一小部分,但是全局变量是伴随整个程序执行的,甚至在
main
函数之前他们就已经初始化好了,他们不是放在堆栈中,而是放在程序的data segment
也就是数据段中。 - 全局变量是通过常量表达式设定初始值的,所谓的常量表达式是指在变异期间就知道表达式值的表达式(构造函数不是常量表达式)。
- 全局变量是程序的实体的一部分,是在编译期间就初始化的,所以全局变量是必须静态初始化的。
那么怎么做呢?cfront的方法还是比较典型的。
- 为每个需要静态初始化的对象产生一个
_sti()
函数,在这样的函数里面直接调用其构造函数,或者用内联的方式执行初始化工作。需要注意的是,这里每个变量的_sti()
都是经过重命名处理的,比如这里是添加文件名和变量名。 - 同样,为每个需要静态内存释放的对象产生一个
_std()
函数,在这个函数里面直接调用其析构函数,或者内联的方式展开函数。同样这个函数做了重命名处理。 - 提供这样的函数,在程序开始的时候调用所有的
_sti
函数,:_main()
函数提供这样的操作,将所有的_sti
组合在一起。同样也提供一个_exit()
函数,将所有的_std()
函数。 - 书中的一个图看失去是非常形象的,但是仔细的想象,全局变量是静态初始化的,而书中给的图的意思是,在
main
函数内执行_main()
函数,在main
函数结尾前执行_exit()
函数,但是全局变量应该是在main
函数执行前就已经完成初始化了,这里的图有点迷惑,还是看看《程序员的自我修养—编译、链接、装载》,那里讲的比较详细点。 - 暂且不考虑上面的问题,还有一个问题是怎么收集所有的
_sti()
和_std()
函数,书中将的有点乱,我认为是有 程序来读取每个object文件,读取其中的符号表,然后将符号表中关于_sti()
和_std()
的函数提取出来,存储在一个”跳离表格“中,然后,重新启动编译器,重新编译,将包含”跳离表格“的文件一起编译链接。
以上是对于类的对象,但是对于noclass
对象和对于支持虚基类的对象,都是不同的,其中noclass
对象没有构造函数,虚基类的指针比较复杂。书中也没有 详细的介绍这里。
最后就是静态初始化的缺点:
- 这些对象不能放在
try
块内。 - 有些全局变量的依赖顺序要考虑好,复杂度是个问题。
局部静态对象
静态局部对象有写特点:
- 存在一个函数内,生命周期是整个程序的生命周期,但是作用域只是在这个函数内部。
- 这个函数可能被调用多次,但是静态局部变量只能初始化一次,析构一次。
怎么办?
设置一个辅助变量,用来标识静态局部变量是否已经被构建和析构。
静态局部变量的生命周期是整个程序的生命周期,但是作用范围确是只是在函数体内。在程序结束的时候是没办法直接调用静态局部变量的析构函数的。
怎么办?
保存静态局部变量的指针。
最后还提到了,如何根据构建顺序,然后逆序析构,显然,在静态编译期间是无法预知顺序的,只有在程序执行的时候保存一个初始化顺序表。
对象数组
1 | Point knots[10]; |
若一个数组是由一些带构造函数的类的对象构成,那么在初始化数组的时候,需要挨个初始化每个元素。当然,要是类没有定义构造函数和析构函数,那就无所谓了,它们的初始化与初始化一个内置类型的数组的消耗是一样的。
在早期的Cfront中,是专门有个函数来完成初始化的(调用构造函数):vec_new
,比较新的编译器会提供两种函数,另外一种是vec_vnew
,是用来初始化带有虚基类的类的对象数组的。
其中一个函数原型如下:1
2
3
4
5
6
7void*
vec_new(
void* array, // 起始地址
size_t elem_size, // 每个对象大小
int elem_count, // 元素个数
void (*constructor)(void*),
void (*destructor)(void*,char))
其中array
可以是具名数组的地址,也可以是new
在堆上分配的空间,此时array=0
。
这里有一个析构函数的函数指针,我一直怀疑是不是写错了。因为会有一个类似的函数:vec_delete
或者一个vec_vdelete
来调用析构函数。
在写程序的时候,也会给程序赋初值,也许会只有一部分有初值。那就把有初始值的部分设定好,剩余的部分用vec_new
函数来完成。
new 和delete运算符
int *pi = new int(5);
其实这个语句是分为两个步骤完成的:1
2int *pi = __new(sizeof(int)); // 分配空间
*pi = 5; // 赋值
但是实际上是要判断是否分配内存成功的:1
2
3int *pi;
if(pi == __new(sizeof(int)))
*pi = 5;
delete也是类似的:1
2if(pi != 0)
__delete(pi);
delete 操作是不会对0
指针做删除操作的。而且也不会把指针修改为0
的。
对于数组也有类似的操作,需要注意的是函数vec_new
是只有在类对象数组,且该类提供了默认构造函数的时候才会调用。
删除数组的时候需要提供[]
在数组名前,其实也可以在这里面指定元素的个数,不过现代的编译器都是忽略这个元素个数。
什么是cookie
?
不知道个数怎么析构呢,原来是在函数vec_new
的时候会存储一个cookie
用来存放元素个数。
placement Operator new
实际是重载new
操作符。1
Point *pt = new(arena)Point;
其中arena
应开辟的空间的地址,用来存储一个新创建的Point
对象。
这个函数实际上只是返回了这个地址,函数的原型可以理解为这样1
2
3
4void* operator new(size_t,void* p)
{
return p;
}
表面是看起来确实如此,但是实际上还有另外一层语义,就是类型转换,并且执行构造函数。实际的代码可能类似于这样的:1
2
3Point *pt = (Point*)arena;
if(pt !=0)
pt->Point::Point();
上面的解释其实是基于这样的假设:arena
是新开辟的空间,没有任何污染。
但是若要使这个arena
空间已经存在了一个对象怎么办?
需要保证原来的对象被析构,delete这块区域,早期的delete会将这块内存也删除,这块内存也没法用了,不可行
直接调用destructor
,可以解决这个问题。
这还不算是麻烦的,当这块区域是一个基类的对象。。。
临时对象
这块讲的有点拗口,还不是特别清楚作者的思路,先不写了。
彩蛋
今天女神张俪来我理工拍新戏,大晚上的,蚊子满天飞,一个镜头拍了N遍,好不容易啊!