深度探索C++对象模型》读书笔记(3)

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情
***纯虚拟函数***

在设计抽象基类时,需要注意以下几点:
(1)不要将destructor声明为pure virtual function;
如果将destructor声明为pure virtual function,则设计者一定得定义它。因为每一个derived class destructor会被编译器加以扩展,以静态调用得方式调用其“每一个virtual base class”以及“上一层base class”的destructor。
(2)不要将那些函数定义内容并不与类型有关的函数设计为virtual function,因为其几乎不会被后继的derived class改写。
(3)对于其derived class可能修改某一个data member的函数,不应被声明为const。

 

***“无继承”情况下的对象构造***
先定义class Point:
class Point {
public:
Point(
float x = 0.0float y = 0.0) : _x(x),_y(y) {}
virtual float z();
protected:
float _x,_y;
}
;

你可不能小看z()这个virtual function给class Point带来的巨大变化。virtual function的引入促使每一个class Point拥有一个vtpr,这样一来,编译器在constructor中添加了对vptr进行初始化的代码,而copy constructor和copy assignment operator也会对vptr进行设定,而不再是原先简单的bitwise操作了。
请看以下的代码:

Point foobar()
{
Point local;
Point 
*heap = new Point;
*heap = local;
delete heap;
return local;
}

将被内部转化为:

Point foobar(Point &_result)
{
Point local;
local.Point::Point();
Point 
*heap = _new(sizeof(Point));
if(heap != 0)
heap
->Point::Point();
*heap = local;
_result.Point::Point(local);  
// copy constructor的应用
local.Point::~Point();
return;
}

从以上代码的转化可以看出:一般而言,如果你的设计之中有很多函数都需要以传值方式(by value)传回一个local class object,那么提供一个copy constructor就比较合理。

 

***继承体系下的对象构造***
假设class Point3d虚拟继承于class Point,但由于class Point仅存在一份实体,因而class Point3d的constructor需要注意一个问题。
请看下面的继承关系图:

class Point3d : virtual public Point { ... };
class Vertex : virtual public Point { ... };
class Vertex3d : public Point3d, public Vertex { ... };

通常来说,class Point3d和class Vertex的constructor均需调用Point的constructor,然而,当Point3d和Vertex同为Vertex3d的subobject时,它们对Point constructor的调用操作一定不可以发生,而是交由Vertex3d来完成。
那么如何做到这一点呢?其实只需在constructor中添加一个参数就可以了。例如,class Vertex3d在调用Point3d和Vertex的constructor之前,总是会把参数_most_derived设为false,于是就压制了两个constructors中对Point constructor的调用操作。

// 在virtual base class情况下的constructor扩充内容
Vertex3d* Vertex3d::Vertex3d(Vertex3d *thisbool _most_derived, float x, float y, float z)
{
if(_most_derived != false)
this->Point::Point(x,y);

// 在调用上一层base classes的constructor之前设定_most_derived为false
this->Point3d::Point3d(false,x,y,z);
this->Vertex::Vertex(false,x,y,z);

// 设定vptr
// 安插user code

return this;
}

为了控制一个class中有所作用的函数,编译器只要简单地控制住vptr的初始化和设定操作即可。
vptr初始化操作应该如何处理?实际情况是:应该在base class constructors(具体来说,是所有的virtual base classes和上一层的base classes)调用操作之后,但是在程序员供应的码或是“member initialization list中所列的members初始化操作”之前。为什么是这样呢?
如果每一个constructor都一直等待到其base class constructors执行完毕之后才设置其对象的vptr,那么每次它都能够调用正确的virtual function实体。

constructor的执行算法通常如下:
(1)在derived class constructor中,所有virtual base classes的constructor会被调用;
(2)在derived class constructor中,上一层base class的constructor会被调用;
(3)上述完成之后,对象的vptr(s)被初始化,指向相关的virtual table(s);
(4)如果class有member class object,而后者拥有constructor,那么它们会以其声明顺序的相反顺序被调用;
(5)用户所定义的代码。

下面是vptr必须被设定的两种情况:
(1)当一个完整的对象被构造起来时,如果我们声明一个Point对象,Point constructor必须设定其vptr;
(2)当一个subobject constructor调用了一个virtual function(无论是直接调用或间接调用)时。
在明确了哪些情况下vptr必须被设定,我们在声明一个PVertex时,各个vptr不再需要在每一个base class constructor中被设定。解决之道是把constructor分裂为一个完整的object实体和一个subobject实体。在subobject实体中,vptr的设定可以忽略。

如果我们不对Point供应一个copy assignment operator,而光是依赖默认的memberwise copy,编译器通常不会产生出一个实体,除非class不表现出bitwise语意。关于哪些情况class不表现出bitwise语意,请参见读书笔记(2)。
由于在virtual base class的拷贝操作将造成subobject的多重拷贝,并且该问题至今难以解决。因此笔者的建议是:尽量不要允许一个virtual base class的拷贝操作,甚至建议:不要在任何virtual base class中声明数据。

 

***解构语意学***
如果class没有定义destructor,那么只有在class内含的member object(或是class自己的base class)拥有destructor的情况下,编译器才会自动合成出一个来。
其解构顺序与建构顺序正好相反。

 

***对象的构造和解构***
一般而言,我们会把object尽可能放置在使用它的那个程序区段附近,这样做可以节省不必要的对象产生操作和销毁操作。

 

***全局对象***
全局对象的静态初始化策略包括以下几个步骤:
(1)为每一个需要静态初始化的对象产生一个_sti_...()函数,内含必要的constructor调用操作或inline expansions;
(2)为每一个需要静态的内存释放操作的对象产生一个_std_...()函数,内含必要的destructor调用操作或inline expansions;
(3)在main()函数的首尾分别添加一个_main()函数(用以调用可执行文件中的所有_sti()函数)和一个_exit()函数(用以调用可执行文件中的所有_std()函数)。
建议根本不要用那些需要静态初始化的全局对象。 

 

***局部静态对象***
假设我们有以下程序片段:

const Matrix& identity() {
static Matrix mat_identity;
// ...
return mat_identity;
}

此处的local static class object保证了以下语意:
(a)mat_identity的constructor必须只能施行一次,虽然上述函数可能会被调用多次;
(b)mat_identity的destructor必须只能施行一次,虽然上述函数可能会被调用多次。
编译器的实际做法如下:在第一次调用identity()时把mat_identity构造出来,而在与相应文件关联的静态内存释放函数中将其解构。(局部静态对象的地址在downstream component中将会被转换到程序内用来放置global object的data segment中)

 

***对象数组***
如果你想要在程序中取出一个constructor的地址,这是不可以的。然而经由一个指针来激活constructor,将无法存取default argument values。那么,如何支持以下的语句:

complex::complex(double=0.0double=0.0);

当程序员写出:

complex c_array[10];

时,编译器最终需要调用:

   vec_new(&c_array,sizeof(complex),10,&complex::complex,0);

为了解决这个问题,可由编译器产生一个内部的constructor,没有参数,在其函数内调用由程序员提供的constructor,并将default参数值明确地指定过去:

complex::complex()
{
complex(
0.00.0);
}

 

***new和delete运算符***
以constructor来配置一个class object:
Point3d *origin = new Point3d;

被转换为:

Point3d *origin;
if(origin = _new(sizeof(Point3d))) {
try {
origin 
= Point3d::Point3d(origin);
}

catch( ... ) {
_delete(origin);  
// 释放因new而配置的内存
throw;  // 将原来的exception上传
}

}

如果我们配置一个数组,内带10个Point3d objects,我们预期Point和Point3d的constructor被调用各10次,每次作用于数组中的一个元素:

// 完全不是好主意
Point *ptr = new Point3d[10];

如果我们进行如下的释放操作:

// 只有Point::~Point被调用
delete []ptr;

由于其触发的vec_delete()是通过迭代走过每一个数组元素,而本例中被传递过去的是Point class object的大小而不是Point3d class object的大小,整个运行过程将会失败。
解决之道在于程序层面,而非语言层面:

for(int ix = 0; ix < 10; ix++)
{
Point3d 
*= &((Point3d*)ptr)[ix];
delete p;
}
当然,最好还是避免以一个base class指针指向一个derived class objects所组成的数组。

***Template的“具现”行为***
template class中的任何member都只能通过template class的某个实体来存取或操作。

Point<float>::Status s;  // ok
Point::Status s;  // error

如果我们定义一个指针,指向特定的实体,像这样:

Point<float> *ptr = 0;

由于这是一个指向class object的指针,本身并不是一个class object,编译器不需要知道与该class有关的任何members数据。所以将“Point的一个float实体”具现也就没有必要。
如果不是一个pointer而是reference,假设:

Point<float> &ref = 0;

这个定义的真正语意会被扩展为:

// 内部扩展
Point<float> temp(float(0));
Point
<float> &ref = temp;

以上转化是因为reference并不是无物(no object)的代名词,0被视作整数,必须被转换为类型Point<float>的一个对象。
然而,member functions只有在member functions被使用的时候,C++ Standard才要求它们被“具现”出来。这个规则的由来主要有两个原因:
(1)空间和效率的考虑。对于未使用的函数进行“具现”将会花费大量的时间和空间;
(2)尚未实现的功能。并不是一个template具现出来的所有类型一定能够完整支持一组member functions,因而只需具现真正需要的member functions。
举个例子:

Point<float> *= new Point<float>;

只有(a)Point template的float实例、(b)new 运算符、(c)default constructor需要被“具现”。

 

***Template的错误报告***
所有与类型相关的检验,如果涉及到template参数,都必须延迟到真正的具现操作发生。
对于下面的template声明:

template <class T>
class Mumble
{
public:
Mumble(T t 
= 1024) : _t(t)
{
if(tt != t)
throw ex ex;
}

private:
T tt;
}

其中像“T t = 1024”、“tt != t”这样的潜在错误在template声明时并不会报告,而会在每个具现操作发生时被检查出来并记录之,其结果将因不同的实际类型而不同。

Mumble<int> mi;  // 上述两个潜在错误都不存在
Mumble<int*> pmi;  // 由于不能将一个非零的整数常量指定给一个指针,故“T t = 1024”错误

 

***Template中的名称决议方式***
区分以下两种意义:一种是“scope of the template definition”,也就是“定义出template”的程序,另一种是“scope of the template instantiation”,也就是“具现出template”的程序。

// scope of the template definition
extern double foo(double);

template 
<class type>
class ScopeRules
{
public:
void invariant() { _member = foo(_val); }
type type_dependent() 
return foo(_member); }
// ...
private:
int _val;
type _member;
}
;

// scope of the template instantiation
extern int foo(int);

ScopeRules
<int> sr0;

在“scope of the template definition”中,只有一个foo()函数声明位于scope之内;然而在“scope of the template instantiation”中,两个foo()函数声明都位于scope之内。对于以下函数操作:

// scope of the template instantiation
sr0.invariant();

那么,在invariant()中调用的究竟是哪一个foo()函数实体呢?
Template之中,对于一个nonmember name的决议结果是根据这个name的使用是否与“用以具现出该template的参数类型”有关而决定的,如果其使用互不相关,那么就以“scope of the template definition”来决定name,否则就以“scope of the template instantiation”来决定name。

// 因为_val的类型是int,而函数的决议只和函数原型有关,与函数返回值无关
// 被用来具现这个template的真正类型对于_val的类型没有影响
_member = foo(_val);

故此处的调用操作由“scope of the template definition”来决议。
若是如下的函数调用:

sr0.type_dependent();

由于_member的类型与template参数有关,故此处由“scope of the template instantiation”来决议。

 

***Member Function的具现行为***
以手动方式在个别的object module中完成预先具现操作,是唯一有效率的办法。

 

***执行期类型识别***
dynamic_cast运算符可以在执行期决定真正的类型。如果downcast是安全的(也就是说,一个base type pointer指向一个derived class object),这个运算符会传回被适当转型过的指针;如果downcast不是安全的,这个运算符会传回0。

typedef type *ptype;
typedef fct 
*pfct;

simplify_conv_op(ptype pt)
{
if(pfct pf = dynamic_cast<pfct>(pt)) {
...
}

else { ... }
}

什么是dynamic_cast的真正成本?pfct的一个类型描述器会被编译器产生出来,由pt指向之class object类型描述器必须在执行期通过vptr取得。下面是可能的转换:

// 取得pt的类型描述器
((type_info*)(pt->vptr[0]))->_type_description;

其中,type_info是C++ Standard所定义的类型描述器的class名称,该class中放置着待索求的类型信息。virtual table的第一个slot内含type_info object的地址,此type_info object与pt所指之class type有关。
dynamic_cast运算符也适用于reference身上,然而对于一个non-type-safe-cast,其结果不会与施行于指针的情况一样。一个reference不可以像指针那样“把自己设为0便代表了no object”;若将一个reference设为0,会引起一个临时性对象(拥有被参考到的类型)被产生出来,该临时对象的初值为0,这个reference然后被设定为该临时变量的一个别名。
因而,如果reference并不真正是某一种derived class,那么可通过丢出一个bad_cast exception进行处理:

simplify_conv_op(const type &rt)
{
try {
fct 
&rf = dynamic_cast<fct&>(rt);
}

catch(bad cast) {
// ...
}

}

当然,你也可以使用typeid运算符来达到同样的目的:

simplify_conv_op(const type &rt)
{
if(typeid(rt) == typeid(fct))
{
fct 
&rf = dynamic_cast<fct&>(rt);
}

else { ... }
}
深度探索C++对象模型:5.构造、析构、拷贝语意学 第五章:构造、析构、拷贝语意学 考虑下面这个abstract base class声明: class Abstract_base{ public: virtual ~Abstract_base() = 0; virtual void interface() const =0; virtual const char* mumble() const {return_m... 阅读详情

相关推荐

深入探索C++对象模型()

构造、解构、拷贝语意学(Semantics of Construction,Destruction, and Copy) 一般而言,class的data member应该被初始化,并且只在constructor中或是在class的其他member functions中指定初值。其他任何操作都将破坏封装性质,使class的维护和修改更加困难。 纯虚函数的存在(Presence of a P...

anrui0096的博客 291

C++对象模型学习——构造、析够、拷贝语意学

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_34321977的博客 235

深度探索C++对象模型读书笔记第五章:构造析构拷贝语意学

深度探索C++对象模型读书笔记第五章:构造析构拷贝语意学 对于abstract base class(抽象基类)class中的data member应该被初始化,并且只在constructor中或者在其他member functions中指定初值。 纯虚函数的存在 纯虚函数,虚函数在声明后加上 = 0,纯虚函数的意义在于只定义一个可继承的接口,并没有实现(虚函数意义在于定义一个可继...

yhcuac 1022

深度探索c++对象模型第五章笔记上

构造、解构、拷贝语意学(Semantics of Constuction,Destruction,and Copy) 假设有以下的代码: class Abstract_base { public: virtual ~Abstract_base()=0;//pure virtual function virtual void interface() const=0; virtual const...

Capr1corn的博客 335

c++ ---深度探索C++对象模型读书笔记3

3.Data 语意学 class X {}; // sizeof 得到1 class Y :public virtual X {}; // sizeof 得到8 class Z: public virtual X {}; // sizeof 得到8 class A:public Y, public Z {}; //sizeof 得到12 X中没有任何成员,但是它也不为空,它是有一个隐晦的1byte,那是被编译器安插进去的一个char,这使得这个class 生成的...

heary29的专栏 404

深度探索C++对象模型读书笔记3C++中的多态,这跟指针有什么关系?

  1、  C++的运行时多态因为每个派生类对象中都包含基类部分,所以可将基类类型的引用或者指针绑定到派生类对象中的基类部分,而对象本身并不会改变,仍为派生类对象。通过该指针或者引用调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型相对应的函数,也就是派生类中定义的虚函数实现。而且,只有通过引用或者指针调用,虚函数才会在运行时确定。如果调用的是非虚函数,

宁静致远 1702

深度探索C++对象模型读书笔记3

深度探索C++对象模型读书笔记3)。 <br />  在visual C++ 6.0中测试如下代码:<br /> #include "iostream"<br />using namespace std;<br /><br />class X {};<br />class Y : public virtual X {};<br />class Z : public virtual X {};<br />class A : public Y,public Z {};<br /><br />int mai

Tommy(凌飞)的专栏 997

深度探索C++对象模型读书笔记(3)

深度探索C++对象模型读书笔记(3) 2010年12月20日   巨盾补补,今天你“补”了吗? 巨盾补补:是一款针对个人用户而设计的绿色漏洞补丁软件,集绿色、小巧、全面、精确于一身,是您升级系统漏洞的不二选择。 1、绿色、小巧:下载完双击即可运行,使用完即可完全删除。 2、全面:完全支持Windows XP和Windows 7 系统。 3、精确,采用的是和Windows Update...

qnf782rm 176

深度探索C++对象模型读书笔记3

对于一个类的成员变量的存取,如果成员是静态变量,那么通过指针(->)和通过对象(.)存取是一样的;

午夜飞行 493

读书笔记深度探索c++对象模型》 (3

第四章:Function语意学4.2 虚拟成员函数a) 单一继承下的virtual function    一个class只会有一个virtual table。每一个table内含其对应的class object的所有active virtual functions的函数实体地址。包括:   这个class自己定义的virtual function,它override了一个可能存在的base class virtual function函数实体。   继承自base class的函数实体。   一个纯虚函数

cesc 785

[读书笔记] - 《深度探索C++对象模型》第3章 Data语意学

一个空的class,如: // sizeof X == 1 class X { }; 事实上并不是空的,它有一个隐晦的1 byte,那是被编译器安插进去的一个char。这使得这个class两个objects得以在内存中配置独一无二的地址。 1.Data Memeber的绑定 在下面的程序中,length的类型在两个member function signatuures中都解析为global typedef,也就是int。当后续再有length的nested typedef声明出现时,C++ St

浮生的博客 386

深度探索C++对象模型读书笔记——2.3 程序转化语意学

C++

smallcat20121230的专栏 373

深度探索C++对象模型读书笔记】【第3章】Data语意学

1、例子,用VS 2013编译器 #include<iostream> using namespace std; class X{}; class Y :public virtual X{}; class Z :public virtual X{}; class A :public Y, public Z{}; int main(){ cout << siz...

weixin_30790841的博客 139

3章 Data语意学-《深度探索C++对象模型读书笔记

本章主要讲述了以下内容:Data Member的邦定,Data Member的布局,Data Member的存取,继承与Data Member,对象成员的效率,指向Data Member的指针。Data Member的邦定主要讲述了以下内容:该部分讲述了一个例子,通过例子和说明我们可以得出一个结论。那就是:在一个类一面要把类型定义和变量定义方在最开始的位置,特别是在模板代码中。Data

worldvacuum的专栏 324

深度探索C++对象模型读书笔记3) 数据成员语意学

转自codepub,只做为本人学习使用,如果侵犯了您的权力,请立即通知我删除,谢谢。 在visual C++ 6.0中测试如下代码:  在visual C++ 6.0中测试如下代码: #include "iostream"using namespace std;class X {};class Y : public virtual X {};class Z : public

553

深度探索C++对象模型读书笔记

` C++在布局以及存取时间上主要的额外负担是由virtual引起的,包括:虚函数机制和虚基类机制(即用以实现“多次出现在继承体系中的base class,有一个单一而被共享的尸体”) C++对象模式:在C++中有两中成员数据:static和nonstatic以及三种成员函数:static、nonstatic和virtual C++编译器的采用的对象模型: Nonstatic成员对象...

qq_15054345的博客 200
上一篇: 深度探索C++对象模型》读书笔记(2)
下一篇: 汇编指令集
hankwangwang
博客等级 码龄19年 1897粉丝 200原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值