您的位置:首页 > 编程语言 > C语言/C++

[温故而知新] 《深度探索c++对象模型》——构造、析构、拷贝的语义

2016-04-11 01:08 405 查看
前言

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);
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: