指针成员与拷贝构造

在 C++中,如果一个类中有一个指针成员,就要特别注意拷贝构造函数,一不小心就会出现内存泄露。一个经典的浅拷贝例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>

using namespace std;

class HasPtrMem {
  public:

    HasPtrMem() : d(new int(0)) {
    }

    HasPtrMem(const HasPtrMem& h) : d(h.d) {
    // 直接赋值,导致a.d和b.d指向同一位置
    }

    ~HasPtrMem() {
    delete d;
    }

    int* d;


};


int main() {
    HasPtrMem a;
    HasPtrMem b(a); // 调用隐式拷贝构造函数

    cout << *a.d << endl;
    cout << *b.d << endl; // delete悬空指针,出现异常


}

稍微改下实现深拷贝

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>

using namespace std;

class HasPtrMem {
  public:

    HasPtrMem() : d(new int(0)) {
    }

    HasPtrMem(const HasPtrMem& h) : d(new int(*h.d)) {
    // new 一块内存,把分配来的指针交还给d,这样做能避免悬挂指针
    }

    ~HasPtrMem() {
    delete d;
    }

    int* d;


};


int main() {
    HasPtrMem a;
    HasPtrMem b(a);

    cout << *a.d << endl;
    cout << *b.d << endl;


}

移动语义

先看例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>

using namespace std;

class HasPtrMem {

public:

  HasPtrMem() : d(new int(3)) {
    cout << "Construct:" << ++n_cstr << endl;
  }

  // 拷贝构造函数
  HasPtrMem( const HasPtrMem& h) : d(new int(*h.d)) {
    cout << "Copy construct" << ++n_cptr << endl;
  }

  // 移动构造函数
  HasPtrMem(HasPtrMem&& h) : d(h.d) {
    h.d = nullptr; // 将临时值的指针成员赋值为空
    cout << "Move construct : " << ++n_mvtr << endl;
  }

  ~HasPtrMem() {
    delete d;
    cout << "Destruct:" << ++n_dstr << endl;
  }


  int* d;

  static int n_cstr;
  static int n_dstr;
  static int n_cptr;
  static int n_mvtr;

};

int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;

HasPtrMem GetTemp() {
  HasPtrMem h;
  cout << "Resource from " << __func__ << ":" << hex << h.d << endl;
  return h;
}

int main() {
  HasPtrMem a = GetTemp();
  cout << "Resource from " << __func__ << ":" << hex << a.d << endl;
  cout << "*d = " << *a.d << endl;
}
1
2
3
4
5
6
7
Construct:1
Resource from GetTemp:0x21d8d016130
Move construct : 1
Destruct:1
Resource from main:0x21d8d016130
*d = 3
Destruct:2

当调用 GetTemp()后,返回一个临时变量,把临时变量的值赋值给 a 时会调用移动构造函数,移动构造函数把 d 的值赋值给 a,然后把 h.d 赋值为 nullptr,这样 a.d 就指向原先 h.d 的堆内存,当调用 h 的析构函数时,并不会析构 h.d 原先指向的堆,因为 h.d 为空。这样 a 就不用重新分配堆内存。

注意:g++ 编译时要添加 -fno-elide-constructors 选项关闭构造函数优化才能看到上面的结果。