c++tutorial(十八):特别成员

特别成员
[注意:本章要求正确理解动态分配的内存 ]

特殊成员函数是在某些情况下隐式定义为类成员的成员函数。有六个:

会员功能 课堂的典型形式C:
默认构造函数 C::C();
析构函数 C::~C();
复制构造函数 C::C (const C&);
赋值构造函数 C& operator= (const C&);
移动构造函数 C::C (C&&);
移动赋值构造函数 C& operator= (C&&);

让我们分析一下这些:

默认构造函数
该默认构造函数是当一个类的对象声明,但不使用任何参数进行初始化称为构造函数。

如果类定义没有构造函数,则编译器假定该类具有隐式定义的默认构造函数。因此,在声明这样的类之后:

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

编译器假定它Example具有默认构造函数。因此,可以通过简单地声明它们而不使用任何参数来构造此类的对象:

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;
}

bar的内容:示例
编辑并运行

这里Example3有一个默认构造函数(即没有参数的构造函数)定义为空块:

Example3() {}

这允许Example3在没有参数的情况下构造类的对象(就像foo在这个例子中声明的那样)。通常,为所有没有其他构造函数的类隐式定义这样的默认构造函数,因此不需要显式定义。但在这种情况下,Example3有另一个构造函数:

Example3 (const string& str);

当在类中显式声明任何构造函数时,不会自动提供隐式默认构造函数。

析构函数
析构函数实现了构造函数的相反功能:它们负责一个类在其生命周期结束时所需的必要清理。我们在前几章中定义的类没有分配任何资源,因此并不需要任何清理。

但现在,让我们假设最后一个示例中的类分配动态内存来存储它作为数据成员的字符串; 在这种情况下,在对象的生命周期结束时自动调用一个函数来负责释放这个内存是非常有用的。为此,我们使用析构函数。析构函数是一个非常类似于默认构造函数的成员函数:它不带任何参数并且不返回任何内容,甚至不返回void。它还使用类名作为自己的名称,但前面带有波形符号(~):

// 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;
}

bar的内容:示例
编辑并运行

在施工时,Example4为a分配存储空间string。稍后由析构函数释放的存储。

对象的析构函数在其生命周期结束时被调用; 在这种情况下foo,bar这发生在功能结束时main。

拷贝构造函数

顾名思义:拷贝构造函数是其他对象的拷贝。所以以其他同类型的对象为参数

当对象传递其自己类型的命名对象作为参数时,将调用其拷贝构造函数以构造副本。

复制构造是一个构造,其第一个参数是类型参考类本身(可能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) {}

此默认复制构造函数可能适合许多类的需要。但是浅拷贝只会复制类本身的成员,这可能不是我们对Example4上面定义的类所期望的那样,因为它包含处理其存储的指针。对于该类,执行浅拷贝意味着复制指针值,而不是内容本身; 这意味着两个对象(副本和原始string对象)将共享一个对象(它们都将指向同一个对象),并且在某些时候(在销毁时)两个对象都会尝试删除相同的内存块,可能导致程序在运行时崩溃。这可以通过定义以下自定义来解决执行深层复制的复制构造函数:

// 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;
}

bar的内容:示例
编辑并运行

此复制构造函数执行 的深层复制为新字符串分配存储,该字符串初始化为包含原始对象的副本。以这种方式,两个对象(复制和原始)具有存储在不同位置的内容的不同副本。

拷贝赋值(注意:不是构造函数)
对象不仅在构造时复制,在初始化时:它们也可以在任何赋值操作中复制。看到不同:

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 

请注意,baz使用等号在构造时初始化,但这不是赋值操作!(尽管它可能看起来像一个):对象的声明不是赋值操作,它只是调用单参数构造函数的另一种语法。

赋值foo是一个赋值操作。此处未声明任何对象,但正在对现有对象执行操作; foo。

的拷贝赋值运算符是过载的operator=,这需要值或参考类本身作为参数的。返回值通常是对*this(尽管这不是必需的)的引用。例如,对于一个类MyClass,复制作业可能具有以下签名:

MyClass& operator= (const MyClass&);

该拷贝赋值运算符也是一个特殊的函数,也隐含定义,如果一个类没有自定义副本,也没有移动分配(移动构造函数)来定义。

但同样,隐式版本执行的浅拷贝适用于许多类,但不适用于具有指向它们处理其存储的对象的指针的类,如同的情况一样Example5。在这种情况下,不仅类会产生两次删除指向对象的风险,而且分配通过在分配之前不删除对象指向的对象来创建内存泄漏。这些问题可以通过复制分配来解决删除前一个对象并执行深层复制:

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成员不是常数,它可以重新利用相同的string对象:

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

移动构造函数和赋值
与复制类似,移动也使用对象的值将值设置为另一个对象。但是,与复制不同,内容实际上是从一个对象(源)传输到另一个对象(目标):源丢失了由目标接管的内容。仅当值的源是未命名对象时才会发生此移动。

未命名的对象是临时性的对象,因此甚至没有给出名称。未命名对象的典型示例是函数或类型转换的返回值。

使用诸如这些临时对象的值来初始化另一个对象或分配其值,实际上并不需要复制:该对象永远不会被用于其他任何东西,因此,它的值可以移动到目的地宾语。这些情况会触发移动构造函数和移动赋值:当使用未命名的临时对象在构造时初始化时,将调用移动构造函数。同样,当为对象分配未命名的临时值时,将调用移动分配:
the source loses that content, which is taken over by the destination. This moving only happens when the source of the value is an unnamed object.

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都是未命名的临时值。在这些情况下,不需要复制,因为未命名的对象非常短暂,并且当这是更有效的操作时可以由另一个对象获取。

移动构造函数和移动赋值是将类型为rvalue的参数引用到类本身的成员:

MyClass (MyClass&&);             // move-constructor
MyClass& operator= (MyClass&&);  // move-assignment 

一个右值引用是通过用两个&字符以下的类型指定(&&)。作为参数,右值引用匹配此类临时值的参数。

移动的概念对于管理它们使用的存储的对象最有用,例如使用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;
}

foo的内容:示例
编辑并运行

编译器已经优化了许多正式需要移动构造调用的情况,即所谓的返回值优化。最值得注意的是,当函数返回的值用于初始化对象时。在这些情况下,移动构造函数实际上可能永远不会被调用。

请注意,即使rvalue引用可用于任何函数参数的类型,但它对移动构造函数以外的用途很少有用。Rvalue引用很棘手,不必要的使用可能是很难跟踪的错误源。

隐含成员
在这里插入图片描述
上述六个特殊成员函数是在某些情况下在类上隐式声明的成员:

会员功能 隐式定义: 默认定义:
默认构造函数 如果没有其他建设者 什么也没做
析构函数 如果没有析构函数 什么也没做
复制构造函数 如果没有移动构造函数而没有移动赋值 复制所有成员
复制作业 如果没有移动构造函数而没有移动赋值 复制所有成员
移动构造函数 如果没有析构函数,没有复制构造函数,也没有复制或移动赋值 移动所有成员
移动作业 如果没有析构函数,没有复制构造函数,也没有复制或移动赋值 移动所有成员

请注意,在相同的情况下,如何不是隐式定义所有特殊成员函数。这主要是由于与C结构和早期C ++版本的向后兼容性,实际上一些包括已弃用的案例。幸运的是,每个类都可以明确地选择这些成员中的哪些成员存在其默认定义,或者分别使用关键字default和/ 来删除delete。语法是以下之一:

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;
}
酒吧区:200
编辑并运行

在这里,Rectangle可以用两个int参数构造,也可以构造为默认构造(不带参数)。但是,它不能从另一个对象复制构造Rectangle,因为此函数已被删除。因此,假设最后一个示例的对象,以下语句将无效:

Rectangle baz (foo);

但是,可以通过将其复制构造函数定义为:

Rectangle::Rectangle (const Rectangle& other) = default;

这基本上相当于:

Rectangle::Rectangle (const Rectangle& other) : width(other.width), height(other.height) {}

请注意,关键字default没有定义等于默认构造函数的成员函数(即,默认构造函数表示没有参数的构造函数),但是等于未被删除时将隐式定义的构造函数。

通常,为了将来的兼容性,鼓励明确定义一个复制/移动构造函数或一个复制/移动赋值但不是两者的类,指定其中delete或未default明确定义的其他特殊成员函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值