Stanley
B. Lippman
屋檐下的水滴
--读书笔记系列
http://blog.csdn.net/dwater
前
言
Stanley B.Lippman
1.
任何对象模型都需要的三种转换风味:
ü
与编译器息息相关的转换
ü
语言语义转换
ü
程序代码和对象模型的转换
2.
C++
对象模型的两种解释
ü
语言中直接支持面向对象程序设计的部分
ü
对于各种支持的底层实现机制
3.
C++ class
的完整
virtual functions
在编译时期就固定下来了,程序员没有办法在执行期动态增加或取代其中某一个。这使得虚拟函数调用操作得以有快速的派送结果,付出的却是执行期的弹性。
4.
目前所有编译器对于
virtual function
的实现都是使用各个
class
专属的
virtual table
,大小固定,并且在程序执行前就构造好了。
5.
C++
对象模型的底层机制并未标准化,它会因实现品(编译器)和时间的变动而不同。
2002-6-23
关于对象
Object Lessons
1.1 C++
对象模式
1.
C++
在布局以及存取时间上主要的额外负担是由
virtual
引起的,包括
virtual function
机制和
virtual base class
机制,还有一些发生在“一个
derived class
和其第二或后继之
base class
的转换”上的多重继承。
2.
在
C++
对象模型中,
nonstatic data members
被配置于每一个
class object
之内,
static data members
则被存放在所有的
class object
之外,
static
和
nonstatic function members
也被放在所有的
class object
之外,
virtual functions
则以两个步骤支持:每个
class
产生一堆指向
virtual functions
的指针,放在
virtual table (vtbl)
中;每个
class object
被添加一个指针
vptr
,指向相关的
virtual table
。每个
class
所关联的
type_info object
也经由
vtbl
指出,通常是放在
vtbl
的第一个
slot
处。
vptr
由每一个
class
的
construtor
、
destructor
以及
copy assignment operator
自动完成。以上模型的主要优点在于空间和存取时间的效率,主要缺点是,只要应用程序所用到的
class object
的
nonstatic data members
有所修改,那么应用程序代码就必须重新编译。
3.
C++
最初所采用的继承模型并不运用任何间接性,
base class subobject
的
data members
直接放置于
derived class object
中。优点是提供对
base class members
紧凑且高效的存取,缺点是
base class members
的任何改变,都将导致使用其
derived class
的
object
的应用程序代码必须重新编译。
4.
virtual base class
的原始模型是在
class object
中为每一个有关联的
virtual base class
加上一个指针,其他演化出来的模型不是导入一个
virtual base class table
,就是扩充原已存在的
vtbl
,用以维护每一个
virtual base class
的位置。
1.2
关键词所带来的差异
1.
可以说关键词
struct
的使用伴随着一个
public
接口的声明,也可以说它的用途只是为了方便
C
程序员迁徙至
C++
部落。
2.
C++
中凡处于同一个
access section
的数据,必定保证以声明次序出现在内存布局中,然而被放在多个
access sections
中的各笔数据排列次序就不一定了。同样,
base classes
和
derived classes
的
data members
的布局也没有谁先谁后的强制规定。
3.
组合
composition
而非继承才是把
C
和
C++
结合在一起的唯一可行方法。
1.3
对象的差异
1.
C++
程序设计模型支持三种程序设计典范
programming paradigms
:
ü
程序模型
procedural model
ü
抽象数据类型模型
abstract data type model
,
ADT
ü
面向对象数据模型
object-oriented model
,
OO
2.
虽然可以直接或间接处理继承体系中的一个
base class object
,但只有通过
pointer
或
reference
的间接处理,才能支持
OO
程序设计所需的多态性质。
3.
C++
中,多态只存在于
public class
体系中,
nonpublic
的派生行为以及类型为
void*
的指针可以说是多态,但它们没有被语言明白地支持,必须由程序员通过显示的转型操作来管理。
4.
C++
以下列方法支持多态:
ü
经由一组隐含的转化操作,如把一个
derived class
指针转化为一个指向其
public base type
的指针;
ü
经由虚拟机制;
ü
经由
dynamic_cast
和
typeid
运算符。
5.
多态的主要用途是经由一个共同的接口来影响类型的封装,这个接口通常被定义在一个抽象的
base class
中。这个接口是以
virtual function
机制引发的,它可以在执行期根据
object
的真正类型解析出到底是哪一个函数实体被调用。
6.
一个
class object
所需的内存,一般而言由以下部分组成:
ü
nonstatic data members
的总和大小;
ü
任何由于
alignment
需求而填补上去的空间;
ü
为支持
virtual
而由内部产生的任何额外负担。
7.
一个
pointer
或
reference
,不管它指向哪一种数据类型,指针本身所需的内存大小是固定的。本质上,一个
reference
通常是以一个指针来实现,而
object
语法如果转换为间接手法,就需要一个指针。
8.
指向不同类型之指针的差异,既不在其指针表示法不同,也不在其内容不同,而是在其所寻址出来的
object
类型不同,亦即指针类型会教导编译器如何解释某个特定地址中的内存内容及大小。它们之所以支持多态,是因为它们并不引发内存中任何与类型有关的内存委托操作,会受到改变的只是它们所指向的内存的大小和内容的解释方式。
9.
转型
cast
操作其实是一种编译指令,大部分情况下它并不改变一个指针所含的真正地址,它只是影响被指向之内存的大小和内容的解释方式。
10.
一个
base class object
被直接初始化或指定为一个
derived object
时,
derived object
就会被切割
sliced
,以塞入较小的
base type
内存中,多态于是不再呈现。一个严格的编译器可以在编译时期解析一个通过该
object
而触发的
virtual function
调用操作,从而回避
virtual
机制。这时,如果
virtual function
被定义为
inline
,则会有效率上的收获。
11.
C++
通过
class
的
pointer
和
reference
来支持多态,这种程序设计风格就是所谓的
OO
;
C++
也支持具体的
ADT
程序风格,如今被称为
object-based OB
,不支持多态,不支持类型的扩充。
2002-6-25
构造函数语意学
The Semantics of Constructors
1.
Jerry Schwarz
,
iostream
函数库建构师,曾为了让
cin
能够求得一个真假值,于是他为它定义了一个
conversion
运算符
operator int()
。但在语句
cin << intVal
中,其行为出乎意料:程序原本要的是
cout
而不是
cin
!但是编译器却找到一个正确的诠释:将
cin
转型为整型,现在
left shift operator <<
就可以工作了!这就是所谓的“
Schwarz Error
”。
Jerry
最后以
operator void *()
取代
operator int()
。
2.
引入关键词
explicit
的目的,就是为了提供程序员一种方法,使他们能够制止单一参数的
constructor
被当作一个
conversion
运算符。其引入是明智的,但其测试应该是残酷的!
2.1 Default Constructor
的建构操作
1.
global objects
的内存保证会在程序激活的时候被清为
0
。
local objects
配置于程序的堆栈中,
heap objects
配置于自由空间中,都不一定会被清为
0
,它们的内容将是内存上次被使用后的遗迹。
2.
在各个不同的版本模块中,编译器避免合成出多个
default constructor
的方法:把合成的
default constructor
、
copy constructor
、
assignment copy operator
都以
inline
方式完成。一个
inline
函数有静态链接,不会被档案以外者看到。如果函数过于复杂,不适合做成
inline
,就会合成一个
explicit non-inline static
实体。
3.
以下四种情况,编译器必须为未声明
constructor
的
classes
合成一个
implicit nontrivial default constructor
:带有
default constructor
的
member class object
,带有
default constructor
的
base class
,带有
virtual function
,带有
virtual base class
。其它各种情况且没有声明任何
constructor
的
classes
,它们拥有的是
implicit trival default constructors
,它们实际上并不会被合成出来。
4.
编译器合成
implicit nontrivial default constructor
,不过是暗地里作了一些重要的事情以保证程序正确合理地运行。如果程序员提供了多个
constructors
,但其中都没有
default constructor
,编译器同样会在这些
constructors
中插入一些相同功能的代码,这些代码都将被安插在
explicit user code
之前。
2002-6-26
2.2 Copy Constructor
的建构操作
1.
有三种情况,会以一个
class
的内容作为另一个
class object
的初值:
ü
对一个
object
作明确的初始化操作,如:
someClass obt = obtb;
ü
一个
object
被当作参数交给某个函数时,如:
foo(obt);
ü
当函数返回一个
class object
时。
若
class
设计者明确定义了一个
copy constructor
,大部分情况下,该
constructor
会被调用。这可能导致一个暂时性的
class object
的产生或程序代码的蜕变,或者两者皆有。
2.
如果
class
没有提供一个
explicit copy constructor
,当
class object
以相同
class
的另一个
object
作为初值时,其内部是以所谓的
default memberwise initialization
手法完成的,即把每一个内建的或派生的
data member
的值,从一个
object
拷贝到另一个
object
。不过,它并不会拷贝其中的
member class object
,而是以递归的方式施行
memberwise initialization
。
3.
一个
class object
可以从两种方式复制得到:初始化和指定,从概念上而言,这两个操作分别是以
copy constructor
和
copy assignment operator
完成的。
4.
如果
class
没有声明一个
copy constructor
,就会有隐含的声明
implicitly declared
或隐含的定义
implicitly defined
出现。
C++
把
copy constructor
分为
trivial
和
nontrivial
两种。只有
nontrivial
的实体才会被合成出来。决定一个
copy constructor
是否为
trivial
的标准在于
class
是否展现出所谓的“
bitwise copy semantics
”。
5.
以下四种情况,一个
class
不展现
bitwise copy semantics
:
ü
class
内含一个
member object
而后者的
class
声明有或被编译器合成有一个
copy constructor
时;
ü
class
继承自一个
base class
而后者存在或被编译器合成有一个
copy constructor
时;
ü
当
class
声明了一个或多个
virtual functions
时;
ü
当
class
派生自一个继承串链,其中有一个或多个
virtual base classes
时。
前两种情况中,编译器必须将
member
或
base class
的
copy constructors
调用操作安插到被合成的
copy constructor
中。
6.
一旦一个
class object
中必须引入
vptr
,编译器就必须为它的
vptr
正确地设置好初值。此时,该
class
就不再展现
bitwise semantics
。
7.
当一个
base class object
以其
derived class object
内容作初始化操作时,其
vptr
复制操作必须保证安全。
8.
每一个编译器对于虚拟继承的承诺,都表示必须让
derived class object
中的
virtual base class subobject
的位置在执行期准备妥当。维护位置的完整性是编译器的责任。
2002-6-27
2.3
程序转化语意学
1.
每一个明确的初始化操作都会有两个必要的程序转化阶段:先重写每一个定义,剥除其中的初始化操作,然后安插
class
的
copy constructor
调用操作。
2.
把一个
class object
当作参数传给一个函数或是作为一个函数的返回值,相当于以下形式的初始化操作:
X xx = arg;
其中
xx
代表形式参数或返回值,而
arg
代表真正的参数值。
3.
函数定义如下:
X bar(){X xx; return xx;}
,
bar()
的返回值通过一个双阶转化从局部对象
xx
中拷贝出来:
ü
首先为
bar
添加一个额外参数,类型是
class object
的一个
reference
,这个参数用来放置被拷贝构建而得的返回值。
ü
然后在
return
指令之前安插一个
copy constructor
调用操作,以便将欲传回之
object
的内容当作上述新增参数的初值,同时重写函数使它不返回任何值。
4.
Named Return Value(NRV)
优化如今被视为是标准
C++
编译器的一个义不容辞的优化操作,它的特点是直接操作新添加的额外参数。注意只有
copy constructor
的出现才会激活
C++
编译器的
NRV
优化!
NRV
优化虽然极大地改善了效率,但还是饱受批评:一是优化由编译器默默完成,而是否完成以及其完成程度完全透明;二是一旦函数变得比较复杂,优化就变得较难施行;三是优化由可能使程序产生错误——有时并不是对称地调用
constructor
和
destructor
,而是
copy constructor
未被调用!
5.
在编译器提供
NRV
优化的前提下,如果可以预见
class
需要大量的
memberwise
初始化操作,比如以
by value
的方式传回
objects
,那么提供一个
explicit inline copy constructor
的函数实体就非常合理。此种情况下,没有必要同时提供
explicit assignment operator
定义。
6.
copy constructor
的应用迫使编译器多多少少对程序代码作部分优化,尤其是当一个函数以
by value
的方式传回一个
class object
,而该
class
有一个
copy constructor
(或定义或合成)时,无论在函数的定义还是在使用上将导致深奥的程序转化。此外,编译器将实施
NRV
优化。
7.
注意正确使用
memset()
和
memcpy()
,它们都只有在
classes
不含任何由编译器产生的内部
members
如
vptr
时才能有效运行!
2002-6-30
2.4
成员初始化列表
1.
当写下一个
constructor
时,就有机会设定
class members
的初值。不是经由
member initialization list
,就是在
constructor
函数本身之内。
2.
下列情况,为了让程序能被顺利编译,必须使用
member initialization list
:
ü
初始化一个
reference member
时;
ü
初始化一个
const member
时;
ü
调用一个
base class
的
constructor
,而它拥有一组参数时;
ü
调用一个
member class
的
constructor
,而它拥有一组参数时。
3.
编译器会对
initialization list
一一处理并可能重新排序,以反映出
members
的声明次序,它会安插一些代码到
constructor
内,并置于任何
explicit user code
之前。
4.
一个忠告:请使用“存在于
constructor
体内的一个
member
”,而不是“存在于
member initialization list
中的一个
member
”,来为另一个
member
设定初值。
2002-7-1
Data
语意学
The Semantics of Data
讨论如下继承体系:
class X{};
class Y : public virtual X{};
class Z : public virtual X{};
class A: public Y, public Z{};
1.
一个
empty class
如
class X{}
,它有一个隐晦的
1 byte
,那是被编译器安插进去的一个
char
,使得这个
class
的两个
objects
得以在内存中配置独一无二的地址。
2.
Y
和
Z
的大小受到三个因素的影响:
ü
语言本身所造成的额外负担
overhead
。语言支持
virtual base classes
时导致的额外负担反映在某种形式的指针身上,它要么指向
virtual base class subobject
,要么指向一个存放
virtual base class subobject
地址或者其偏移量
offset
的表格。
ü
编译器对于特殊情况所提供的优化处理。
virtual base class X 1 byte
大小的
subobject
也出现在
class Y
和
Z
身上。传统上它被放在
derived class
的固定部分的尾端。某些编译器对
empty virtual base
提供特殊处理,将它视为
derived class object
最开头的一部分,它不用会任何的额外空间,也就是前面提到的
1 byte
。
ü
Alignment
的限制。
Alignment
就是将数值调整到某数的整数倍,在
32
位计算机上,通常该数为
4 bytes(32
位
)
,以使
bus
的运输量达到最高效率。
3.
一个
virtual base class subobject
只会在
derived class
中存在一份实体,不管它在
class
继承体系中出现了多少次,
class A
的大小由下列几点决定:
ü
被大家共享的唯一一个
class X
实体,大小为
1 byte
;
ü
Base Y
、
Z
的大小减去因
virual base class
而配置的大小;
ü
class A
自己的大小;
ü
class A
的
alignment
数量。
4.
C++ standard
并不强制规定
base class subobjects
、不同存取级别的
data members
的排列次序这种琐碎细节,它也不规定
virtual function
以及
virtual base classes
的实现细节。
5.
C++
对象模型尽量以空间优化和存取速度优化来表现
nonstatic data members
,并且保持和
C
语言
struct
数据配置的兼容性。它把数据直接存放在每一个
class object
中,对于继承而来的
nonstatic data members
,不管是
virtual
或
nonvirtual base class
也是如此。至于
static data members
则被放置在程序的一个
global data segment
中,不会影响个别
class object
的大小。
static data member
永远只存在一份实体,但是一个
template class
的
static data member
的行为稍有不同。
3.1 Data Member
的绑定
inline member function
躯体内的
data member
绑定操作,会在整个
class
声明完成后才发生,而
argument list
中的名称还是会在它们第一次遭遇时被适当地决议
resolved
完成。基于这种状况,请始终把
nested type
声明放在
class
的起始处。
2002-7-2
3.2 Data Member
的布局
1.
每一个
private
、
protected
、
public
区段就是一个
access section
。
C++ Standard
要求,在同一个
access section
中,
members
的排列只需满足“较晚出现的
members
在
class object
中有较高的地址”这一条件即可。也就是说各个
members
并不一定的连续排列,
alignment
可能需要的
bytes
以及编译器可能合成供内部使用的
data members
都可能介于被声明的
members
之间。
2.
C++ Standard
也允许编译器将多个
access sections
之中的
data members
自由排列,不必在乎它们出现在
class
声明中的次序。当前各家编译器都是把一个以上的
access sections
连锁在一起,依照声明的次序成为一个连续区块。
access sections
的多寡不会导致额外负担。
3.
vptr
传统上会被放在所有明确声明的
members
的最后,不过如今也有一些编译器把
vptr
放在
class object
的最前端。
4.
一个用来判断哪一个
section
先出现的
template function
:
template <class class_type, class data_type1, class data_type2>
char* access_order(data_type1 class_type::*mem1, data_type2 class_type::*mem2)
{
assert(mem1 != mem2);
return mem1 < mem2 ? “member 1 occurs first” : “member 2 occurs first”;
}
(我在
VC++ 6.0
下测试该函数,编译尚未通过。这个,应该怪罪编译器)
2002-7-6
3.3 Data Member
的存取
1.
不管什么情况,每一个
static data member
只有一个实体,放在程序的
data segment
之中,每次程序取用
static member
,不管是通过
operator::
还是
member selection operator
,都会被内部转化为对该唯一
extern
实体的直接参考操作。每一个
static member
的存取以及与
class
的关联不会导致任何执行时间或空间上的额外负担。如果有两个
classes
,每一个都声明了一个
static member freeList
,那么当它们都放在程序的
data segment
时,就会导致名称冲突,编译器的解决方法是使用
name-mangling
,暗中对每一个
static data member
编码,以获得一个独一无二的程序识别代码。
2.
有多少个编译器,就有多少种
name-mangling
做法,任何
name-mangling
做法都有两个要点:
ü
一种算法,推导出独一无二的名称;
ü
如果编译系统或者环境工具必须和使用者交谈,那些独一无二的名称可被轻易推导回原先的名称。
3.
取一个
static data member
的地址,会得到一个指向其数据类型的常量指针,而不是指向其
class member
的指针。
4.
nonstatic data members
直接放在每一个
class object
之中,除非经过显示的
explicit
或隐含的
implicit class object
,没有办法直接存取它们。只要程序员在一个
member function
中直接处理一个
nonstatic data member
,所谓
implicit class object
就会发生,其实质是编译器会为这个
member function
增添一个
const this
指针,而在函数体内通过这个
this
指针来存取
nontatic data member
。
5.
欲对一个
nonstatic data member
进行存取操作,编译器需要把
class object
的起始地址加上
data member
的编译量
offset
,如地址
&someObject.someMember
等于
&someobject + (&theClass::someMember – 1);
指向
data member
的指针,其
offset
值总是会被加上
1
,这样可以使编译系统区分出一个指向
class
第一个
data member
的指针和一个没有指向任何
data member
的指针。
6.
每一个
nonstatic data member
的偏移量在编译时期即可获知,甚至如果
member
属于一个单一或多重继承体系中
base class subobject
也是一样,因此其存取效率和一个
C struct member
或一个
nonderived class
的
member
的存取效率是一样的。但是在虚拟继承的情况下就另当别论了:如果该
nonstatic data member
是一个
virtual base class
的
member
,并且通过指针来存取的话,在编译时期就不会得知这个
member
真正的
offset
位置,所以这个存取操作必须延迟至执行期,经由一个额外的间接导引才能够解决。
2002-7-7
3.4
“继承”与
Data Member
1.
在
C++
继承模型中,一个
derived class object
所表现出来的东西,是其自己的
members
加上其
base classes members
的总和。
C++
并未规定
derived class members
和
base classes members
的排列次序。不过,在大部分编译器上,除
virtual base class
外,
base class members
总是先出现。
2.
一般而言,具体继承
concrete inheritance
并不会增加空间或存取时间上的额外负担。
3.
把两个原本独立不相干的
classes
凑成一对
type/subtype
,并带有继承关系容易犯两个错误。一是可能会重复设计一些相同操作的函数,一般而言,选择某些函数做成
inline
函数,是设计
class
的一个重要课题;二是把一个
class
分解为多层,有可能会为了表现
class
体系之抽象化,因为编译器的边界调整而膨胀所需空间。其根本原因是
C++
保证出现在
derived class
中的
base class subobject
有其完整原样性。
4.
C++
最初问世时,许多编译器把
vptr
放在
class object
的尾端,这样可以保留
base class C struct
的对象布局。此后,某些编译器开始把
vptr
放在
class object
的开始处,这样会给多重继承下通过指向
class members
之指针调用
virtual function
带来一些帮助,否则,在执行期不仅必须备妥从
class object
起点处开始量起的
offset
,而且必须备妥
class vptr
之间的
offset
。
5.
单一继承提供了一种自然多态的形态,是关于
class
体系中
base type
和
derived type
之间的转换。一般来说,
base class
和
derived class objects
都是从相同的地址开始。但若将
vptr
放在
class object
的起始处,如果
base class
没有
virtual function
而
derived class
有,那么单一继承的自然多态就会打破。此时,把一个
derived object
转换为其
base
类型就需要编译器的介入,用以调整地址。而在既是多重继承又是虚拟继承的情况下,编译器的介入则更有必要。
6.
多重继承的复杂度在于
derived class
和其上一个
base class
乃至上上一个
base class
之间的非自然关系,其主要问题发生在
derived class objects
和其第二或后继的
base class objects
之间的转换。对一个多重派生对象,将其地址指定给最左端
base class
的指针,情况将和单一继承相同,而第二个或后继的
base class
的地址指定操作则需要修改地址,加上或减去(若是
downcast
)介于中间的
base class subobjects
的大小。
C++
并未要求多重继承时
derived class object
中各个
base class subjectes
的排列次序,目前各个编译器都是根据声明次序来排列它们。
7.
class
内如果内含一个或多个
virtual bass class subobjects
,将被分割为两部分:一个不变局部和一个共享局部。不变局部总是拥有固定的
offset
,其数据用以指定共享局部的位置,可以直接存取;而共享局部表现的就是
virtual base class subobject
,其位置会因为每次的派生操作而变化,只可间接存取。各家编译器实现技术之间的差异就在于间接存取的方法不同。
8.
一般而言,
virtual base class
最有效的一种运用方式是:一个没有任何
data member
的抽象
class
。
2002-7-14
3.5
对象成员的效率
如果没有把优化开关打开,就很难猜测一个程序的效率表现,因为程序代码潜在性的受到某些与编译器有关的东西的影响。程序员如果关心效率,应该实际测试,不要光凭推论或常识判断或假设。优化操作并不一定总是能够有效运行。
2002-7-15
3.6
指向
Data Members
的指针
指向
data members
的指针可用来详细调查
class members
的底层布局,可用来决定
vptr
是放在
class
的起始处还是尾端,还可用来决定
class
中
access sections
的次序。
取一个
nonstatic data member
的地址,将会得到它在
class
的
offset
;而取一个
static data member
的地址或者取一个绑定于真正
class object
身上的
data member
的地址,将会得到该
member
在内存中的真正地址。这也正是
someType someClass::*
和
someTye *
潜在的区别。
2002-7-16
Function
语意学
The Semantics of Function
C++
支持三种类型的
member functions
:
static
、
nonstatic
和
virtual
,每一种类型的调用方式都不同。
4.1 Members
的各种调用方式
1.
C++
的设计准则之一便是
nonstatic member function
至少必须和一般的
nonmember function
有着相同的效率。编译器内部会将
member
函数实体转换为对等的
nonmember
函数实体,其步骤为:
ü
改写函数原型
signature
以安插一个额外的参数
this
到
member function
中,使得
class object
可以调用该函数。其中,
this
是
const
指针,若该函数为
const
,则反映在
this
上面的结果是
this
指向的
data
也为
const
;
ü
将每一个对
nonstatic data member
的存取操作改为经由
this
指针来存取;
ü
将
member function
重新写成一个外部函数,对函数名称进行
mangling
处理;
此后,每一个函数调用操作也都必须转换,用以提供相应的实参。
2.
关于虚拟函数的内部转换步骤:若
normalize
是一个
virtual member function
,
ptr->normalize();
会被内部转化为
(*ptr->vptr[t])(ptr);
事实上,
vptr
名称也会被
mangled
,因为可能存在有多个
vptrs
;
t
是
vitrual table slot
的索引值,关联到
normalize
函数;第二个
ptr
表示
this
指针。
3.
使用
class scope operator
明确调用一个
vitual function
,或经由一个
class object
调用一个
vitual function
其决议方式会和
nontatic member function
一样!故
virtual function
的一个
inline
函数实体可被扩展开来,因而提供极大的效率利益。
4.
static member function
的主要特征是没有
this
指针,这导致它不能直接存取其
class
中的
nonstatic members
,不能被声明为
const
、
volatile
或
virtual
,也不需要经由
class object
才能调用。
static member function
会被提出于
class
声明之外,并给予一个经过
mangled
的适当名称。如果取一个
static member function
的地址,得到的将是其在内存中的地址,其地址类型并不是一个指向
class member function
的指针,而是一个
nonmember
函数指针。
static member function
的一个意想不到的好处是可以成为一个
callback
函数,也可以成功地应用在
thread
函数身上。
2002-07-17
4.2 Virtual Member Functions
虚拟成员函数
1.
C++
中,多态
polymorphism
表示以一个
public base class
指针或
reference
寻址出一个
derived class object
。识别一个
class
是否支持多态,唯一适当的方法试看它是否有任何
virtual function
。只要
class
拥有一个
virtual function
,它就需要一份额外的执行期型别判断信息。
2.
一个
class
只会有一个
virtual table
,其中内含对应
class object
中所有的
active virtual functions
的函数实体的地址。这些
active virtual functions
包括:
ü
一个
class
定义的函数实体。它会改写
overriding
一个可能存在的
base class virtual function
。
ü
继承自
base class
的函数实体。此时该
class
不改写
base class virtual function
。
ü
一个
pure_virtual_called()
函数实体,它既可以扮演
pure virtual function
的空间保卫者,也可以当作执行期异常处理函数。如果该函数被调用,通常的操作是结束程序。
3.
每一个
virtual function
都被指派一个固定不变的索引值,该值在整个继承体系中保持与特定
virtual function
的关联。这样就可以在编译时期设定
virtual function
的调用。
2002-7-20
4.
多重继承下,一个上层
basse classes
数目为
n
的
derived class
,它将内含
n-1
个额外的
virtual tables
。其主要实体与最左端的
base class
共享,其中包含所有
virtual functios
的地址;
n-1
个次要实体与其它
base classes
有关,其中只包含出现在对应
base class
中
virtual functions
的地址。
5.
在多重继承中支持
virtual function
,其复杂度围绕在第二个及后继
base class
上,以及执行期
this
指针调整上。第二(或后继)
base class
会影响对
virtual function
支持的
3
种情况:
ü
通过指向第二个
base class
的指针,调用
derived class virtual function
;
ü
通过指向
derived class
的指针,调用第二个
base class
中一个继承而来的
virtual function
;
ü
允许
virtual function
函数的返回值类型有所变化,可能是
base type
,也可能是
publicly derived type
。
6.
关于执行期
this
指针调整比较有效率的解决方法是
thunk
。所谓
thunk
是一小端
assembly
码,用来以适当的
offset
值来调整
this
指针并跳到相应的
virtual function
。
thunk
技术允许
virtual table slot
继续内含一个简单的指针,此时多重继承将不需要任何空间上的额外负担!
slots
中的地址可以直接指向
virtual function
,也可以指向一个相关的
thunk
。
4.3
函数的效能
nonmember
、
static member
和
nonstatic member function
在内部都会转化为完全相同的形式,三者效率相同。
2002-08-08
4.4
指向
Member Function
的指针
对一个
nonstatic member function
取址,得到的是该函数在内存中的地址;而面对一个
virtual function
,得到的将是一个索引值。这个值是不完整的,必须被绑定于一个
class object
上,才能够通过它调用函数。指向
member function
的指针的声明语法,以及指向
member selection
运算符的指针,其作用是作为
this
指针的空间保留者。因此,
static member function
的类型是函数指针,而不是指向
member function
的指针。
使用一个
member function
指针,如果并不用于
virtual function
、多重继承、
virtual base class
等情况的话,其成本并不比使用一个
nonmember function
指针要高。
4.5 Inline Functions
关键词
inline
只是一项请求。如果在某个层次上,函数的执行成本比一般的函数调用及返回机制所带来的负荷低,那么该请求被接受,编译器就用一个表达式合理地将函数扩展开来。真正的
inline
函数扩展操作是在函数调用的那一点上。在
inline
扩展期间,每一个形式参数会被对应的实际参数所取代,
inline
函数中的每一个局部变量都必须被放在函数调用的一个封闭区段中,并拥有一个独一无二的名称。这会带来参数的求值操作以及临时性对象的管理。
2002-08-11
构造、解构、拷贝语意学
Semantics of Construction, Destruction, and Copy
1.
一般而言,
class
的
data member
应该被初始化,而且只在
constructor
中或其它
member functions
中初始化,其它任何操作都将破坏其封装性质,使其维护和修改更加困难。
2.
可以定义并调用
invoke
一个
pure virtual function
,但它只能被静态调用,不能经由虚拟机制调用。每一个
derived class destructor
会被编译器加以扩展,静态调用每一个
virtual base class
以及上一层
base class
的
destructor
。因此,不管
base class
的
virtual destructor
是否声明为
pure
,它必须被定义。
5.1
无继承情况下的对象构造
C++ Standard
要求编译器尽量延迟
nontrivial members
的实际合成操作,直到真正遇到其使用场所为止。
5.2
继承体系下的对象构造
一般而言,继承体系下编译器对
constructor
所作的扩充操作以及次序大约如下:
ü
所有
virtual base class constructors
必须从左到右、从深到浅被调用:如果
class
被列于
member initialization list
中,那么任何明确指定的参数都必须传递过去,否则如果
class
有一个
default constructor
,也应该调用它;
class
中的每一个
virtual base class subobject
的偏移量
offset
必须在执行期可被存取;如果
class object
是最底层
most-derived
的
class
,其
constructors
可能被调用,某些用以支持这个行为的机制必须被方进来。
ü
以
base class
的声明次序调用上一层
base class constructors
:如果
base class
被列于
member initialization list
中,那么任何明确指定的参数都必须传递过去,否则若它有
default constructor
或
default memberwise copy constructor
,那么就调用它;如果
base class
是多重继承下的第二或后继的
base class
,那么
this
指针必须有所调整。
ü
如果
class object
有
virtual table pointer(s)
,它(们)必须被设定初值,指向适当的
virtual table(s)
。
ü
如果有一个
member
没有出现在
member initialization list
中,但它有
default constructor
,调用之。
ü
将
member initialization list
中的
data members
的初始化操作以
members
的声明次序放进
constructor
的函数本身。
2002-8-18
5.3
对象复制语意学
Object Copy Semantics
1.
只有在默认行为所导致的语意不安全或者不正确以致发生别名化
aliasing
或者内存泄漏
memory leak
时,才需要设计一个
copy assignment operator
。否则,程序反倒会执行得较慢。
2.
如果仅仅是为了把
NRV
优化开关打开而提供一个
copy constructor
,那么就没有必要一定要提供一个
copy assignment operator
。
3.
copy assignment operator
有一个非正交情况,那就是它缺乏一个平行于
member initialization list
的
member assignment list
。调用
base class
的
copy assignment operator
示例:
Point::operator = (p3d);
或
(*(Point*)this) = p3d;
或
(Point &)(*this) = p3d;
4.
事实上,
copy assignment operator
在虚拟继承情况下行为不佳,需要小心设计和说明。许多编译器甚至并不尝试取得正确的语意,它们在每一个中间的
copy assignment operator
中调用每一个
base class instance
,于是造成
virtual base copy assignment operator
的多个实体被调用。建议尽可能不要允许一个
virtual base class
的拷贝操作,并不要在任何
virtual base class
中声明
data member
。
5.5
解构语意学
Semantics of Destruction
如果
class
没有定义
destructor
,那么只有在其内带的
member object
或
base class
拥有
destructor
时,编译器才会自动合成出一个
destructor
。一个由程序员定义的
destructor
被扩展的方式类似
constructors
被扩展的方式,只是顺序相反:
ü
destructor
的函数本体首先被执行;
ü
如果
class
拥有
member class objects
,而后者拥有
destructors
,那么它们将以声明的相反顺序而调用;
ü
如果
object
内带一个
vptr
,则现在被重新设定以指向适当
base class
之
virtual table
;
ü
如果有任何直接的
nonvirtual base classes
拥有
destructor
,它们将以声明的相反顺序而调用;
ü
如果有任何
virtual base classes
拥有
destructor
,而前面讨论的这个
class
是
most-derived class
,那么它们会以原先构造顺序的相反顺序被调用。
2002-8-19
执行期语意学
Runtime Semantics
6.1
对象的构造和解构
1.
一般而言,
constructor
和
destructor
的安插都如你所预期。但如果一个区段或函数中有一个以上的离开点,情况就会复杂一些,
destructor
会放在每一个离开点之前。通常,我们要求将
object
尽可能放在使用它的那个程序区附近,这样做可以节省不必要的对象产生和销毁操作。
2.
C++
程序中所有的
global objects
都被放置在程序的
data segment
中,如果不明确指定初值,
object
所配置的内存内容将为
0
(
C
并不自动设定初值)。如果
global object
有
constructor
和
destructor
的话,我们说它需要静态的初始化和内存释放操作。
2002-8-20
3.
virtual base class
的
subobject
在每个
derived class
中的位置可能会变动,不能在编译时期确定。以一个
derived class
的
pointer
或
reference
来存取
virtual base class subobject
,是一种
nonconstant expression
,必须在执行期方可评估求值。
4.
使用静态初始化的
object
有一些缺点。其一,无法放入
try
区段,任何
throw
操作必将触发
exception handling library
的默认函数
terminate()
;其二,程序员必须为控制“需要跨越模块做静态初始化”
objects
的依赖顺序而产生的复杂度付出代价。建议根本就不要使用那些需要静态初始化的
global objects
。
5.
新的
C++
标准要求编译单位中的
static local class objects
必须在相应函数第一次被调用时才被构造,而且必须以相反的次序销毁。由于这些
objects
是在需要时才被构造,因此编译时期无法预期其集合和顺序。为支持新标准,可能要对被产生出来的
static local class objects
保持一个执行期链表。
2003-8-1
6.
对于对象数组定义,晚近的编译器一般会提供两个函数,分别用于处理没有
virtual base class
的
class
,以及内带
virtual base class
的
class
,它们通常被称为
vec_new
、
vec_vnew
。前者类型通常为:
void* vec_new( //
初始化程序员未提供初值的连续元素
void *array, //
数组起始地址若为
0
,则动态分配
size_t elem_size, //
每一个
class object
的大小
int elem_count, //
数组中的元素数目
void (*constructor) (void *), // class
的
default
constructor
指针
void (*destructor) (void *, char) // class
的
destructor
指针,以
0
填入
);
如果程序员提供带有默认参数值的
default constructor
,编译器要做特殊处理,以传入默认参数值!
对应销毁数组的两个函数分别为
vec_delete
、
vec_vdelete
。前者类型通常为:
void* vec_delete(
void *array, //
数组起始地址
size_t elem_size, //
每一个
class object
的大小
int elem_count, //
数组中的元素数目
void (*destructor) (void *, char) // class
的
destructor
指针
);
6.2 new
和
delete
运算符
注意区分
operator new
和
new operator
!前者负责分配内存;后者先调用前者分配内存,然后调用
constructor
以实施初始化。
完