[温故而知新] 《深度探索c++对象模型》——构造、析构、拷贝的语义
2016-04-11 01:08
405 查看
前言
base class 的virtual或者 pure virtual 虚析构函数需要实现
C的pure virtual function 可以有body
两种初始化方式的效率比较
虚拟继承下virtual base class 的构造
在构造函数中调用虚函数
赋值操作符
这一章的知识点相对零散,书也翻译得乱七八糟的。所以下面只列举一些我觉得相对重要的知识点。
书中给出的理由是,在derived class的析构函数中,一定会调用base class的destructor,如果没有声明,就会导致链接失败。
对于
在目前的Java版本中,base class的抽象方法是没有body的。为什么C++的抽象函数可以有body呢?设计者怎么想的呢?这牵扯到C++的一个特性。
1.编译器兼容;
2.让derived class实现者注意到默认实现。注意,pure virutual function只能在derived class中静态调用,也就是derived class的实现者还是能用base class的默认实现的 。
注意,这里为float类型!
书中说第一种初始化的方式会比第二种方式的效率高,为什么呢?
看看汇编代码,第一种方式编译后的汇编代码如下:
再看看第二种方式的汇编代码:
很明显,第二种方式比第一种方式多了一次内存拷贝的操作。所以第一种效率会高一点。我这里做了个简单的测试,分别用两种方法初始化,执行1亿次,在我的环境里,两者之间的差距在0.2~0.4s之间。
那么为什么第二种方式会比第一种多一次内存拷贝呢?浮点数的汇编我不太熟悉,还没搞明白,清楚的同学麻烦告知下,谢谢。
但是,如果我们把这里的float类型改为int类型呢?
上面的代码在同样的环境还是执行一亿次,结果让我大吃一惊,第二种的速度反而比第一种快了0.2s左右!
第一种的汇编代码如下:
第二种:
这两种方式的初始化不同点就在于,第一种调用的是
所以,尽信书不如无书啊。
无论如何,第一种初始化有三个限制:
1.member 必须都是 public
2.只能指定常量
3.编译器并没有保证会按理论上说的那种方式,在函数的active recode放进栈的时候就把常量一起放进去。
vptr的初始化时机:
1.先构造所有virtual base class 以及上一层base class
2.初始化vptr
3.member initialization list
4.执行用户的代码
从上面看出,在class 的 member initialization list中调用该class的virtual function是安全的,但有个例外,如果
像这样:
Point3d:Point3d(float x, float y,float z):Point(x,y),_z(z){}
在member initialiation list 中去初始化Point,那如果这些参数是通过调用 virtual function 而来的,那就是不安全的!此时的vptr还没被初始化!
赋值操作符还能用函数指针来用
base class 的virtual或者 pure virtual 虚析构函数需要实现
C的pure virtual function 可以有body
两种初始化方式的效率比较
虚拟继承下virtual base class 的构造
在构造函数中调用虚函数
赋值操作符
前言
好久没写博,已经好几个月花在为公司的项目填坑上,最近稍微能抽出点时间来写啦。这一章的知识点相对零散,书也翻译得乱七八糟的。所以下面只列举一些我觉得相对重要的知识点。
1. base class 的virtual或者 pure virtual 虚析构函数需要实现
class Point { public: Point(); virtual test()=0; virtual ~Point();//或者virtual ~Point()=0; }; //如果继承了Point这个类,那么Point的析构函数需要实现,即使是空的什么也不做: Point::~Point(){ //do nothing }
书中给出的理由是,在derived class的析构函数中,一定会调用base class的destructor,如果没有声明,就会导致链接失败。
对于
virtual ~Point();这种情况,析构函数需要body(函数体)定义,这是可以理解的,而对于
virutal ~Point()=0;这种情况,析构函数也需要body定义,写Java的同学一定会觉得这相当蛋疼和不可思议,因为C++的纯虚函数跟Java的abstract非常类似:
//Java没有析构函数的概念,垃圾回收机制自己回收,这里的析构函数只是假想的例子 abstract class Point{ abstract _point_destructor(); }
在目前的Java版本中,base class的抽象方法是没有body的。为什么C++的抽象函数可以有body呢?设计者怎么想的呢?这牵扯到C++的一个特性。
2. C++的pure virtual function 可以有body!
具体原因,可以参考这篇文章,写的很详细:《 pure Virtual Functions Difficulty》其中有两个重要的原因:1.编译器兼容;
2.让derived class实现者注意到默认实现。注意,pure virutual function只能在derived class中静态调用,也就是derived class的实现者还是能用base class的默认实现的 。
class Vetex : public Point{ ... void test(){ Point::test(); ... } }
3.两种初始化方式的效率比较
class Point{ public: Point(float x1, float y1, float z1):x(x1),y(y1),z(z1){}; virtual ~Point(); float x; float y; float z; } //两种初始化方式: //1. void test(){ Point local1 = {1.0,2.0,3.0}; //注意,c++ 11标准之后才支持! } //2. void test(){ Point local2 ; local2.x = 1.0; local2.y = 2.0; local2.z = 3.0; }
注意,这里为float类型!
书中说第一种初始化的方式会比第二种方式的效率高,为什么呢?
看看汇编代码,第一种方式编译后的汇编代码如下:
0000000000000030 <__Z4testv>: 30: 55 push %rbp 31: 48 89 e5 mov %rsp,%rbp 34: 48 83 ec 20 sub $0x20,%rsp 38: 48 8d 7d e8 lea -0x18(%rbp),%rdi 3c: f3 0f 10 05 24 00 00 movss 0x24(%rip),%xmm0 # 68 <__Z4testv+0x38> 43: 00 44: f3 0f 10 0d 20 00 00 movss 0x20(%rip),%xmm1 # 6c <__Z4testv+0x3c> 4b: 00 4c: f3 0f 10 15 1c 00 00 movss 0x1c(%rip),%xmm2 # 70 <__Z4testv+0x40> 53: 00 54: e8 00 00 00 00 callq 59 <__Z4testv+0x29> 59: 48 8d 7d e8 lea -0x18(%rbp),%rdi 5d: e8 00 00 00 00 callq 62 <__Z4testv+0x32> 62: 48 83 c4 20 add $0x20,%rsp 66: 5d pop %rbp 67: c3 retq
再看看第二种方式的汇编代码:
0000000000000030 <__Z4testv>: 30: 55 push %rbp 31: 48 89 e5 mov %rsp,%rbp 34: 48 83 ec 20 sub $0x20,%rsp 38: 48 8d 7d e8 lea -0x18(%rbp),%rdi 3c: e8 00 00 00 00 callq 41 <__Z4testv+0x11> 41: 48 8d 7d e8 lea -0x18(%rbp),%rdi 45: f3 0f 10 05 2b 00 00 movss 0x2b(%rip),%xmm0 # 78 <__Z4testv+0x48> 4c: 00 4d: f3 0f 10 0d 27 00 00 movss 0x27(%rip),%xmm1 # 7c <__Z4testv+0x4c> 54: 00 55: f3 0f 10 15 23 00 00 movss 0x23(%rip),%xmm2 # 80 <__Z4testv+0x50> 5c: 00 5d: f3 0f 11 55 f0 movss %xmm2,-0x10(%rbp) 62: f3 0f 11 4d f4 movss %xmm1,-0xc(%rbp) 67: f3 0f 11 45 f8 movss %xmm0,-0x8(%rbp) 6c: e8 00 00 00 00 callq 71 <__Z4testv+0x41> 71: 48 83 c4 20 add $0x20,%rsp 75: 5d pop %rbp 76: c3 retq
很明显,第二种方式比第一种方式多了一次内存拷贝的操作。所以第一种效率会高一点。我这里做了个简单的测试,分别用两种方法初始化,执行1亿次,在我的环境里,两者之间的差距在0.2~0.4s之间。
那么为什么第二种方式会比第一种多一次内存拷贝呢?浮点数的汇编我不太熟悉,还没搞明白,清楚的同学麻烦告知下,谢谢。
但是,如果我们把这里的float类型改为int类型呢?
class Point{ public: Point(int x1, int y1, int z1):x(x1),y(y1),z(z1){}; virtual ~Point(); int x; int y; int z; } //两种初始化方式: //1. void test(){ Point local1 = {1,2,3}; //注意,c++ 11标准之后才支持! } //2. void test(){ Point local2 ; local2.x = 1; local2.y = 2; local2.z = 3; }
上面的代码在同样的环境还是执行一亿次,结果让我大吃一惊,第二种的速度反而比第一种快了0.2s左右!
第一种的汇编代码如下:
void test(){ 30: 55 push %rbp 31: 48 89 e5 mov %rsp,%rbp 34: 48 83 ec 20 sub $0x20,%rsp 38: 48 8d 7d e8 lea -0x18(%rbp),%rdi 3c: be 01 00 00 00 mov $0x1,%esi 41: ba 02 00 00 00 mov $0x2,%edx 46: b9 03 00 00 00 mov $0x3,%ecx Point p = {1,2,3}; 4b: e8 00 00 00 00 callq 50 <__Z4testv+0x20> 50: 48 8d 7d e8 lea -0x18(%rbp),%rdi } 54: e8 00 00 00 00 callq 59 <__Z4testv+0x29> 59: 48 83 c4 20 add $0x20,%rsp 5d: 5d pop %rbp 5e: c3 retq
第二种:
void test(){ 30: 55 push %rbp 31: 48 89 e5 mov %rsp,%rbp 34: 48 83 ec 20 sub $0x20,%rsp 38: 48 8d 7d e8 lea -0x18(%rbp),%rdi Point p; 3c: e8 00 00 00 00 callq 41 <__Z4testv+0x11> 41: 48 8d 7d e8 lea -0x18(%rbp),%rdi p.x = 1; 45: c7 45 f0 01 00 00 00 movl $0x1,-0x10(%rbp) p.y = 2; 4c: c7 45 f4 02 00 00 00 movl $0x2,-0xc(%rbp) p.z = 3; 53: c7 45 f8 03 00 00 00 movl $0x3,-0x8(%rbp) } 5a: e8 00 00 00 00 callq 5f <__Z4testv+0x2f> 5f: 48 83 c4 20 add $0x20,%rsp 63: 5d pop %rbp 64: c3 retq
这两种方式的初始化不同点就在于,第一种调用的是
Point(int,int,int),看汇编代码,在跳转到Point的构造函数之前,先把常量值存到寄存器,在构造函数的执行过程再把值从寄存器存到内存。而第二种调用的是默认构造函数,而值是直接通过bp存到栈里,比第一种少了一次拷贝。
所以,尽信书不如无书啊。
无论如何,第一种初始化有三个限制:
1.member 必须都是 public
2.只能指定常量
3.编译器并没有保证会按理论上说的那种方式,在函数的active recode放进栈的时候就把常量一起放进去。
4. 虚拟继承下,virtual base class 的构造
这种情况下,编译器需要做额外的工作,比如在一个菱形的继承结构中,virtual base class的构造需要编译器加参数来决定在继承体系中由谁来初始化它。5. 在构造函数中调用虚函数
这种情况下,编译器需确保函数实例是正在构造中的class,而在其他情况下,就走正常的虚拟调用的机制。vptr的初始化时机:
1.先构造所有virtual base class 以及上一层base class
2.初始化vptr
3.member initialization list
4.执行用户的代码
从上面看出,在class 的 member initialization list中调用该class的virtual function是安全的,但有个例外,如果
像这样:
Point3d:Point3d(float x, float y,float z):Point(x,y),_z(z){}
在member initialiation list 中去初始化Point,那如果这些参数是通过调用 virtual function 而来的,那就是不安全的!此时的vptr还没被初始化!
6. 赋值操作符
赋值操作符重写,不支持member initialization list赋值操作符还能用函数指针来用
typedef Point3d& (Point3d::*pmfPoint3d)(const Point3d&); pmfPoint3d pmf = &Point3d:operator=; (x.*pmf)(x);
相关文章推荐
- c++继承详解之一——继承的三种方式、派生类的对象模型
- C++中解析XML tinyXML2
- 大型分布式C++框架《二:大包处理过程》
- c语言基础<5>
- c++第二次作业
- C++实验3-4-多分段函数求值
- C++中了类继承和调用父类的构造函数方法
- C++内存管理
- Effective C++ : delete和模板成员函数以及模板函数.
- C++实验3-个人所得税计算器
- C++作业3
- c语言条件运算符的结合性
- c++第三次作业
- c++实验3--个人所得税计算器
- c++作业3
- c++实验2--学生成绩
- C++作业3
- 指针数组和数组指针的区别
- leetcode----Best Time to Buy and Sell Stock
- C++第3次上机作业