C++官方参考链接:Special members - C++ Tutorials (cplusplus.com)
特殊成员
[注意:本章要求正确理解动态分配内存( dynamically allocated memory)]
特殊成员函数是在某些情况下隐式定义为类成员的成员函数。有六个:
Member function(成员函数) | typical form for class C :(类C的典型形式) |
---|---|
Default constructor | C::C(); |
Destructor | C::~C(); |
Copy constructor | C::C (const C&); |
Copy assignment | C& operator= (const C&); |
Move constructor | C::C (C&&); |
Move assignment | 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;
}
在这里,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;
}
在构造时,Example4为一个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;
}
此复制构造函数执行的深复制为新字符串分配存储空间,该字符串被初始化以包含原始对象的副本。通过这种方式,两个对象(副本和原始对象)都具有存储在不同位置的内容的不同副本。
复制赋值
当对象被初始化时,对象不仅在构造时复制:也可以在任何赋值操作时复制。查看区别:
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;
}
移动构造函数和赋值
与复制类似,移动也使用一个对象的值将该值设置为另一个对象。但是,与复制不同的是,内容实际上是从一个对象(源)转移到另一个对象(目标):源失去了该内容,它被目标接管。只有当值的源是一个未命名的对象时,才会发生这种移动。
未命名对象本质上是临时的对象,因此甚至没有被赋予名称。未命名对象的典型例子是函数或类型转换的返回值。
使用诸如此类的临时对象的值来初始化另一个对象或赋值,实际上并不需要复制:对象永远不会用于其它任何事情,因此,它的值可以移动到目标对象中。这些情况会触发移动构造函数和移动赋值:
移动构造函数在使用未命名临时对象在构造初始化对象时调用。同样,当一个对象被赋值给一个未命名的临时对象时,会调用移动赋值:
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构造的值都是未命名的临时值。在这些情况下,不需要进行复制,因为未命名对象的生命周期非常短,当这是一种更有效的操作时,其他对象可以获取该对象。
移动构造函数和移动赋值是采用类本身的右值引用类型形参的成员:
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;
}
编译器已经在所谓的返回值优化中优化了许多形式上需要移动构造调用的情况。最值得注意的是,当函数返回的值用于初始化对象时。在这些情况下,移动构造函数可能永远不会被调用。
注意,尽管右值引用可以用于任何函数形参的类型,但它很少用于移动构造函数以外的用途。右值引用是棘手的,不必要的使用可能是非常难以跟踪的错误来源。
隐式成员
上面描述的六个特殊成员函数是在某些情况下在类上隐式声明的成员:
Member function | implicitly defined:(隐式定义) | default definition:(默认定义) |
---|---|---|
Default constructor | if no other constructors(如果没有其他构造函数) | does nothing(什么也不做) |
Destructor | if no destructor(如果没有析构函数) | does nothing(什么也不做) |
Copy constructor | if no move constructor and no move assignment(如果没有移动构造函数和移动赋值函数) | copies all members(复制所有成员) |
Copy assignment | if no move constructor and no move assignment(如果没有移动构造函数和没有移动赋值函数) | copies all members(复制所有成员) |
Move constructor | if no destructor, no copy constructor and no copy nor move assignment(如果没有析构函数,没有复制构造函数和没有复制和移动赋值函数) | moves all members(移动所有成员) |
Move assignment | if no destructor, no copy constructor and no copy nor move assignment(如果没有析构函数,没有复制构造函数和没有复制和移动赋值函数) | moves all members(移动所有成员) |
注意,在相同的情况下,并不是所有特殊成员函数都是隐式定义的。这主要是由于与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;
}
在这里,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。