《深度探索c++对象模型》chapter2 构造函数语义学
关于c++,最常听到的一个抱怨是,编译器背着程序员做了太多事情,conversion运算符是最常被引用的一个例子:jerry schwarz,iostream函数库的建筑师,就曾经说过一个故事,他说他最早的意图是支持一个iostream class object的纯量测算(scalar test),类似这样:
if(cin) ...
为了让cin能够求得一个真假值,jerry首先为它定义了一个conversion运算符operator int(),运行良好,但是在下面的这种错误的程序设计中,它的行为就势必令人大吃一惊了:
//wow,应该是cout,而不是cin
cin<<intVal;
Class层次结构的“type-safe”填写应该能够捕捉这类输出运算符的错误运用,然而,带有几分唯物主义色彩的编译器,比较喜欢找到一个正确的诠释(如果有的话)。而不只是把程序表示为错误就可以了。在上例中,内建的左移运算符《《,只有在“cin改变为和一个整数值同义”时才适用,编译器会检查可以使用的各个conversion运算符,然后找到了operator int(),ok,当做左移。
int tmp=cin.operator int();
tmp<<intVal;
Jerry如何解决这个意想不到的行为,他以operator void*()取代了operator int().这种错误有时被戏称为“Schwarz Error”。
(if (cin)会调用operator void*();
operator void*是istream的转换函数,if要求操作数具有标量类型,而void*就是标量类型,它是最佳匹配函数,于是就调用它进行转换了。 If the stream is valid, the result of the conversion is a non-null pointer. The result is the null pointer if the stream isn't valid.(参考:http://www.parashift.com/c++-faq/istream-and-while.html)
class Foo
{
public:
int val;
Foo *pnext;
};
void foo_bar()
{
//wow,程序要求bar's member都被清0
Foo bar;
if(bar.val || bar.pnext) //do something
}
(vs运行报错,bar is used without being initilaized)
在这个例子中,正确的程序语义是要求Foo有一个defautl constructor,可以将他的2个members初始化为0,然后,编译器合成的构造函数不会将成员清0,为了让上一段代码正确运行,必须提供一个明显的default constructor,将2个member适当地初始化。
(个人注解:
如果不要Foo foo这种形式,采用new来
Foo *foo=new Foo();
cout<<foo->val<<ends<<foo->pnext<<endl;
可以发现,输出:0。使用new是初始化了类的数据成员了的。
new过程:
1. 调用operator new分配内存 ;
2. 调用构造函数生成类对象;
3. 返回相应指针。
)
通常很多C++程序员存在两种误解:
- 没有定义默认构造函数的类都会被编译器生成一个默认构造函数。
- 编译器生成的默认构造函数会明确初始化类中每一个数据成员。
C++标准规定:如果类的设计者并未为类定义任何构造函数,那么会有一个默认 构造函数被暗中生成,而这个暗中生成的默认构造函数通常是不做什么事的(无 用的),trivial下面四种情况除外。
换句话说,有以下四种情况编译器必须为未声明构造函数的类生成一个会做点事 的默认构造函数。(nontrivial constructor)我们会看到这些默认构造函数仅“忠于编译器”,而可能不会按 照程序员的意愿程效命。
1.包含有带默认构造函数的对象成员的类(带有default constructor的member class object)
如果一个class没有任何constructor,但他含有一个member object,而后者有defautl constructor,那么这个class的implicit
defautl constructor就是“nontrivial”,编译器需要为此class合成出一个default constructor,不过这个合成操作只有在constructor真正需要被调用时发生。
classFoo {public: Foo(),Foo(int);...};
classBar {public: Foofoo; charstr;};
void foo_bar(){
Bar bar; //Bar::foo应在此处被初始化
if(str){...}
}
被合成的Bar default ctor内含必要的代码,能够调用class Foo的default ctor来处理member object Bar::foo,但并不处理Bar::str。即被合成的default ctor只是为了满足编译器的需要(编译器需要有个地方来初始化Bar::Foo,因为它有自己的default ctor),而不是程序的需要(初始化Bar::str是程序员的动作)。
如果程序员在ctor只显式初始化了Bar::str,则一些代码会被插入到这个ctor中。例如
Bar::Bar(){ str=0; }
会被编译器扩展为
Bar::Bar(){
foo.Foo::Foo(); //附加上的编译器代码
str=0;//显式的程序员代码
}
(虽然default constructor已经被明确定义,编译器没办法合成第二个,但是我们需要初始化member object foo,怎么办?
编译器的行动是“如果class A内含一个或以上的member class objects,那么classA的每一个constructor必须调用每一个member classs的defautl constructor,编译器会扩张已存在的constructor。在其中安插一些代码。使得user code在被执行之前,先调用必要的default constructor,)
如果有多个class member object都要求constructor的初始化操作,将如何呢?c++要求以“member objects在class中的声明次序”来调用各个constructor。这一点有编译器完成。
2.带有defautl constructor“的base class。
如果一个没有定义任何构造函数的类派生自带有默认构造函数的基类,那么编译 器为它定义的默认构造函数,将按照声明顺序为之依次调用其基类的默认构造函 数。若该类没有定义默认构造函数而定义了多个其他构造函数,那么编译器扩充 它的所有构造函数——加入必要的基类默认构造函数。另外,编译器会将基类的默 认构造函数代码加在对象成员的默认构造函数代码之前。
3.带有一个virtual function的class
另有2中情况,也需要合成出defautl constructor。
1. 这个类自己声明(或者继承)了Virtual Function。
2. 这个类继承自一个继承串链,其中有一个或多个virtual base class。
不管哪一种情况,由于缺乏有user声明的constructor,编译器会详细记录合成一个defautl constructor的必要信息,以下面的程序片段为例;
class Widget{
public:
virtual void flip()=0;
//...
};
void flip(const Wideget& widget) { widget.fiip();}
//假设Bell和whistle都派生子widget
void foo()
{
Bell b;
Whistle w;
flip(b);
flip(w);
}
这种情况下,编译时,要做如下工作:
1.编译器需要生成一个virtual function table(vtbl)并填充。
2.在class object中,一个额外的pointer member(就是vptr,指向vtbl)会被编译器合成出来。此外虚拟调用会被替换(w.vf() => w.vprt[1])。
为了支持这种功能,编译器必须为每个w对象设置它的vptr(这是成员变量,此时需要指向合适的vtbl),因此编译器需要在default ctor中安插一些代码来完成这种工作。
4.带有一个virtaul Base class的class
virtual base class的实现法在不同编译器之间有极大的差异,然而,每一种实现法的共同点在于必须使virtaul base classs在其每一个derived class object中的位置,能够于执行期准备妥当,例如下面这段程序代码中:
考虑这样的代码:
classX { public: inti; };
classA : publicvirtualX { public: in tj; };
classB : publicvirtualX { public: double d; };
classC : publicA, publicB { public: int k; };
//无法在编译期间解析出 pa->X::i 的位置(给一个pa无法确定i的地址)。
void foo( constA* pa ) { pa->i = 1024; }
main() {
foo( new A );
foo( new C );
// ...
}
由于pa的真正类型不确定,所以某些编译器会记录一个指针例(如 __vbcX)来记录X,然后通过这个指针来定位pa指向的i。上述
void foo( constA* pa ) { pa->i = 1024; }
变成了:
void foo( constA* pa ) { pa-> __vbcX ->i = 1024; }
因此,__vbcX这个指针(执行virtual base class)需要在object构造期间设置好。于是编译器需要一个default ctor来完成这个工作。
《深度探索c++对象模型》chapter2 构造函数语义学的更多相关文章
- 【深度探索C++对象模型】data语义学
class X{}; class Y :public virtual X{}; class Z :public virtual X{}; class A :public Y, public Z{}; ...
- 【深度探索c++对象模型】Function语义学之虚函数
虚函数的一般实现模型:每一个class有一个virtual table,内含该class中的virtual function的地址,然后每个object有一个vptr,指向virtual table. ...
- 【深度探索c++对象模型】Function语义学之成员函数调用方式
非静态成员函数 c++的设计准则之一就是:非静态成员函数至少和一般的非成员函数有相同的效率.编译器内部已将member函数实体转换为对等的nonmember函数实体. 转化步骤: 1.改写函数原型以安 ...
- 《深度探索C++对象模型》读书笔记(一)
前言 今年中下旬就要找工作了,我计划从现在就开始准备一些面试中会问到的基础知识,包括C++.操作系统.计算机网络.算法和数据结构等.C++就先从这本<深度探索C++对象模型>开始.不同于& ...
- [读书系列] 深度探索C++对象模型 初读
2012年底-2014年初这段时间主要用C++做手游开发,时隔3年,重新拿起<深度探索C++对象模型>这本书,感觉生疏了很多,如果按前阵子的生疏度来说,现在不借助Visual Studio ...
- 深度探索C++对象模型
深度探索C++对象模型 什么是C++对象模型: 语言中直接支持面向对象程序设计的部分. 对于各个支持的底层实现机制. 抽象性与实际性之间找出平衡点, 需要知识, 经验以及许多思考. 导读 这本书是C+ ...
- c++学习书籍推荐《深度探索C++对象模型》下载
百度云及其他网盘下载地址:点我 百度云及其他网盘下载地址:点我 编辑推荐 如果你是一位C++程序员,渴望对于底层知识获得一个完整的了解,那么这本<深度探索C++对象模型>正适合你 作者简介 ...
- 读书笔记《深度探索c++对象模型》 概述
<深度探索c++对象模型>这本书是我工作一段时间后想更深入了解C++的底层实现知识,如内存布局.模型.内存大小.继承.虚函数表等而阅读的:此外在很多面试或者工作中,对底层的知识的足够了解也 ...
- 柔性数组-读《深度探索C++对象模型》有感 (转载)
最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...
- 柔性数组-读《深度探索C++对象模型》有感
最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...
随机推荐
- WTL 自定义 Button类-自绘
WTL 自绘Button类,支持按钮三种形态,正常模式,hover模式,鼠标按下模式,支持png图片.使用方法很简单: MyButton* pButton = new MyButton; pBut ...
- dll注册到GAC还是bin - sharepoint程序
通常来说程序在使用dll的时候,会先去GAC中查找是否有存在合适的dll,然后才会到应用程序下的bin目录去查找: 前几天遇到了一个奇葩问题,web项目工程添加了一个第三方dll的引用,然后把这个第三 ...
- 利用Inltellj创建javadoc ,用jd2chm创建chm
现在有些框架都不带javadoc 就需要自己去生成,而且真正用起来还是chm的最方便,所以写篇日志记录一下 下面我就拿struts2的源码来来举个栗子 1.第一步:创建一个空的java项目,导入框架源 ...
- python基础知识四
函数是重用的程序段.它们允许你给一块语句一个名称, 然后你可以在你的程序的任何地方使用这个名称多次地运行这个语句块.这被成为调用函数.我们已经使用了许多内建的函数,比如len和range. 函数通过d ...
- 手把手教你Windows下Go语言的环境搭建
1.想写GO语言首先得下载go语言的开发包 官方下载地址:https://code.google.com/p/go/downloads/list 我用的是Win7 64位的操作系统,截图如下: 2.把 ...
- Eclipse 经验之谈(一):快速打war包
如何快速打一个war包: 具体步骤: 单击右键[在项目名称上]——>Export -->War File . 完成war包的导出了.嘻嘻
- [转]toString()方法
文章转自:http://blog.sina.com.cn/s/blog_85c1dc100101bxgg.html 今天看JS学习资料,看到一个toString()方法,在JS中,定义的所有对象都具有 ...
- Windows7 QT5.6.0(64位)使用mysql(64位)环境搭建详解
1 说明 使用环境为:Windows7 VS2015 QT5.6.0(64位),MYSQL 5.7.13(64位). 网上各种错误.模糊.抽象的资料,配置环境花了半天,痛定思痛,总结出来,方便后来人. ...
- Ubuntu1404+Django1.9+Apache2.4部署配置2配置文件设置
转载注明出处,个人博客:http://www.cnblogs.com/wdfwolf3/ Django首要的部署平台是WSGI,它是Python Web服务器和应用的标准.使用Apache和mod_w ...
- Binary Tree Level Order Traversal II 解题思路
思路: 与Binary Tree Level Order Traversal I 几乎一样.只是最后将结果存放在栈里,然后在栈里再传给向量即可. 再次总结思路: 两个queue,先把第一个放进q1,循 ...