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

      • 前言
      • base class 的virtual或者 pure virtual 虚析构函数需要实现
      • C的pure virtual function 可以有body
      • 两种初始化方式的效率比较
      • 虚拟继承下virtual base class 的构造
      • 在构造函数中调用虚函数
      • 赋值操作符

前言

好久没写博,已经好几个月花在为公司的项目填坑上,最近稍微能抽出点时间来写啦。

这一章的知识点相对零散,书也翻译得乱七八糟的。所以下面只列举一些我觉得相对重要的知识点。

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

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++,汇编)