【cpluscplus教程翻译】Special members

发布时间 2023-05-23 16:19:03作者: xiaoweing

特殊成员

特殊成员函数是那些在特定情况被隐式定义的成员函数:默认构造、析构、拷贝构造、拷贝赋值、移动构造、移动赋值(注意构造和赋值的区别,只要是内存有没有新增),让我们逐个学习一下

默认构造函数(default constructor)

默认构造函数在没给任何参数初始化对象时调用
如果一个类的定义没有构造函数,编译器会隐式生成默认构造函数,考虑如下代码:

class Example {
  public:
    int total;
    void accumulate (int x) { total += x; }
};

编译器会生成一个默认构造函数,因此可以这样构造一个对象Example ex;
一旦类显式定义了任何有参数的构造函数,编译器就不会再生成默认构造函数,

class Example2 {
  public:
    int total;
    Example2 (int initial_value) : total(initial_value) { };
    void accumulate (int x) { total += x; };
};

我们声明了一个参数为int的构造函数,于是可以这样使用Example2 ex (100); // ok: calls constructor,但是这样不行Example2 ex; // not valid: no default constructor
因此,如果一个对象需要不用参数进行构造,那么合适的默认构造函数也需要声明

// classes and default constructors
#include <iostream>
#include <string>
using namespace std;

class Example3 {
    string data;
  public:
    Example3 (const string& str) : data(str) {}
    Example3() {}
    const string& content() const {return data;}
};

int main () {
  Example3 foo;
  Example3 bar ("Example");

  cout << "bar's content: " << bar.content() << '\n';
  return 0;
}

这里默认构造函数的函数体为空,Example3() {},正常来说,如果没有其他构造函数,默认构造函数会被隐式定义。

析构函数

析构函数就是构造函数的反面,他们负责在对象的生命周期结束时进行必要的清理工作,我们之前定义的类没有分配任何资源,因此不需要清理。
但是如果一个类分配了动态内存,来存数据成员,比如说字符串,在这种情况下,释放资源就十分重要,因此需要使用析构函数,析构函数和默认构造函数非常像:它没有参数和返回值

// destructors
#include <iostream>
#include <string>
using namespace std;

class Example4 {
    string* ptr;
  public:
    // constructors:
    Example4() : ptr(new string) {}
    Example4 (const string& str) : ptr(new string(str)) {}
    // destructor:
    ~Example4 () {delete ptr;}
    // access content:
    const string& content() const {return *ptr;}
};

int main () {
  Example4 foo;
  Example4 bar ("Example");

  cout << "bar's content: " << bar.content() << '\n';
  return 0;
}

在构造时,分配了动态内存,析构时,释放了对应的内存
对象的析构函数在对象生命周期结束调用:上面这个例子是main函数的结尾(考虑作用域

拷贝构造函数(copy constructor)

当一个对象被传递一个自己类型的命名对象作为参数时,它的复制构造函数会被调用以构造一个副本。(函数传参会用到,避免无限自己调用,无名对象用的是move
复制构造函数是一种构造函数,它的第一个参数是对类本身的类型引用(可能是const限定的),并且可以用这种类型的单个参数调用。例如,对于类MyClass,复制构造函数可能具有以下签名:MyClass::MyClass (const MyClass&);
如果一个类没有自定义的拷贝或移动 构造函数或运算符,隐式的拷贝函数会自动生成。拷贝函数会简单逐个赋值成员

class MyClass {
  public:
    int a, b; string c;
};

隐式拷贝构造函数自动生成,行为和浅拷贝一致,等价于MyClass::MyClass(const MyClass& x) : a(x.a), b(x.b), c(x.c) {}
默认的拷贝构造函数可能适合大部分类的需求,但是浅拷贝某些情况可能有问题,尤其是对数据类型是指针的情况,可能会导致double free的问题,因此最好是深拷贝

// copy constructor: deep copy
#include <iostream>
#include <string>
using namespace std;

class Example5 {
    string* ptr;
  public:
    Example5 (const string& str) : ptr(new string(str)) {}
    ~Example5 () {delete ptr;}
    // copy constructor:
    Example5 (const Example5& x) : ptr(new string(x.content())) {}
    // access content:
    const string& content() const {return *ptr;}
};

int main () {
  Example5 foo ("Example");
  Example5 bar = foo;

  cout << "bar's content: " << bar.content() << '\n';
  return 0;
}

深拷贝不简单是指针赋值,而是内存拷贝,这种情况下,拷贝对象和原始对象内存是不同的

拷贝赋值运算符(copy assignment)

对象不仅是在构造的时候拷贝,也可以在赋值的时候拷贝

MyClass foo;
MyClass bar (foo);       // object initialization: copy constructor called
MyClass baz = foo;       // object initialization: copy constructor called
foo = bar;               // object already initialized: copy assignment called

注意第三行,虽然用的是赋值运算符,但是直接上拷贝构造函数,回想语法形式,等价于单参数的构造函数。(Class c = 5,这种也是可以的
赋值运算符不会声明新的对象,用现存的对象
拷贝赋值运算符其实是运算符的重载形式,参数是值或引用,返回值通常是*this,MyClass& operator= (const MyClass&);
拷贝赋值运算符也是一个特殊的函数,因此编译器会生成默认版本
同样的,隐式版本是浅拷贝,只要数据成员不是指针就够用,风险不仅是double free,还会造成内存泄漏,因为没有把指向的内存先释放再去拷贝,深拷贝同样可以解决

Example5& operator= (const Example5& x) {
  delete ptr;                      // delete currently pointed string
  ptr = new string (x.content());  // allocate space for new string, and copy
  return *this;
}

如果string不是常量,还可以直接复用空间

Example5& operator= (const Example5& x) {
  *ptr = x.content();
  return *this;
}

Move constructor and assignment

和拷贝相似,移动也是使用一个对象来设置另一个对象的值,但是不像移动,对象的内容实际上从一个对象转移到另一个对象,而且移动也只发生在源对象是无名对象时(主要目的是效率,无名对象,函数返回值,直接用构造函数构造,注意编译器RVO返回值优化也能达到这个效果
无名对象是指临时的、没有名字的对象,最典型的例子就是函数返回值或者类型转换值
使用无名对象就可以不用复制,因为这些对象反正也不会被用到,可以对应的内存移动到目的,这个过程会调用移动构造函数和移动赋值运算符
常见场景如下,注意构造和赋值的区别

MyClass fn();            // function returning a MyClass object
MyClass foo;             // default constructor
MyClass bar = foo;       // copy constructor
MyClass baz = fn();      // move constructor
foo = bar;               // copy assignment
baz = MyClass();         // move assignment 

fn和MyClass的函数返回值都是无名对象,因此可以用来move从而节省空间
注意函数签名MyClass (MyClass&&); // move-constructor MyClass& operator= (MyClass&&); // move-assignment
参数类型是右值引用,右值引用就是无名对象的类型
move这个概念对于自己管理内存的对象十分有空,比如使用new和delete进行内存管理的对象,对于这样的对象:拷贝和移动是非常不同的操作:
-从A复制到B意味着将新内存分配给B,然后将A的全部内容复制到分配给B的新内存中。
-从A移动到B意味着已经分配给A的存储器被转移到B,而不分配任何新的存储器。它只需要复制指针。

// move constructor/assignment
#include <iostream>
#include <string>
using namespace std;

class Example6 {
    string* ptr;
  public:
    Example6 (const string& str) : ptr(new string(str)) {}
    ~Example6 () {delete ptr;}
    // move constructor
    Example6 (Example6&& x) : ptr(x.ptr) {x.ptr=nullptr;}
    // move assignment
    Example6& operator= (Example6&& x) {
      delete ptr; 
      ptr = x.ptr;
      x.ptr=nullptr;
      return *this;
    }
    // access content:
    const string& content() const {return *ptr;}
    // addition:
    Example6 operator+(const Example6& rhs) {
      return Example6(content()+rhs.content());
    }
};


int main () {
  Example6 foo ("Exam");
  Example6 bar = Example6("ple");   // move-construction
  
  foo = foo + bar;                  // move-assignment

  cout << "foo's content: " << foo.content() << '\n';
  return 0;
}

编译器已经优化了许多正式需要move构造调用的情况,即所谓的返回值优化。最值得注意的是,当函数返回的值用于初始化对象时。在这些情况下,move构造函数实际上可能永远不会被调用。
请注意,尽管右值引用可以用于任何函数参数的类型,但除了move构造函数之外,它很少用于其他用途。Rvalue引用很棘手,不必要的使用可能是很难跟踪的错误来源。

隐式成员

上面描述的六个特殊成员函数是在某些情况下在类上隐式声明的成员:

需要注意不是每个特殊成员函数的情况都相同,主要是考虑兼容之前的C代码和早期版本的C++代码,幸运的是每个类都可以选择保留默认情况还是删除对应的版本
function_declaration = default; function_declaration = delete;

// default and delete implicit members
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    Rectangle (int x, int y) : width(x), height(y) {}
    Rectangle() = default;
    Rectangle (const Rectangle& other) = delete;
    int area() {return width*height;}
};

int main () {
  Rectangle foo;
  Rectangle bar (10,20);

  cout << "bar's area: " << bar.area() << '\n';
  return 0;
}

这里,Rect可以使用两个整数或者默认构造函数进行构造,但是不能被拷贝构造,因为这个函数被删除了,因此这句话是不合法的Rectangle baz (foo);,当然也可以使用default关键字定义这个函数Rectangle::Rectangle (const Rectangle& other) = default; ,等价于Rectangle::Rectangle (const Rectangle& other) : width(other.width), height(other.height) {}
请注意,default关键字并没有定义类似默认构造函数的成员函数,而是默认的拷贝构造函数
一般来说,出于兼容性考虑,一般来说,为了将来的兼容性,鼓励明确定义一个复制/移动构造函数或一个复制或移动赋值但不同时定义这两个构造函数的类,在它们没有明确定义的其他特殊成员函数上指定delete或default。