好久没写博,已经好几个月花在为公司的项目填坑上,最近稍微能抽出点时间来写啦。
这一章的知识点相对零散,书也翻译得乱七八糟的。所以下面只列举一些我觉得相对重要的知识点。
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++的一个特性。
具体原因,可以参考这篇文章,写的很详细:《 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();
...
}
}
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放进栈的时候就把常量一起放进去。
这种情况下,编译器需要做额外的工作,比如在一个菱形的继承结构中,virtual base class的构造需要编译器加参数来决定在继承体系中由谁来初始化它。
这种情况下,编译器需确保函数实例是正在构造中的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还没被初始化!
赋值操作符重写,不支持member initialization list
赋值操作符还能用函数指针来用
typedef Point3d& (Point3d::*pmfPoint3d)(const Point3d&);
pmfPoint3d pmf = &Point3d:operator=;
(x.*pmf)(x);