[注:本章要求的正确理解
动态分配的内存
]
特殊的成员函数
是被隐式定义为在某些情况下类成员的成员函数。有六种:
让我们来看看每一种:
默认构造方法
该
默认构造函数是当一个类的对象声明,但不与任何参数进行初始化称为构造函数。
如果一个类定义没有构造函数,则编译器假定类有一个隐含定义的
默认构造函数。因此,宣称这样的课后:
1
2
3
4
5
| class Example {
public:
int total;
void accumulate (int x) { total += x; }
}; | |
编译器假定
Example
有一个
默认的构造函数
。因此,这个类的对象可以通过简单地宣告他们不带任何参数来构建:
但只要一类有一些构造采取任何数量的显式声明的参数,编译器不再提供一个隐含的默认构造函数,并不再允许不带参数的类的新对象的声明。例如,下面的类:
1
2
3
4
5
6
| 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 | |
会不会是有效的,因为类已被宣布与明确的构造函数带一个参数,并且取代了隐含
的默认构造函数采取无。
因此,如果需要无参数构造这个类的对象,合适的
默认构造函数也应在类中声明。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // 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
没有参数构造(如
foo
在本例中被宣布)。通常情况下,这样的默认构造有没有其他的构造函数,因此不需要明确定义所有的类隐含定义。但是,在这种情况下,
Example3
还有另外一个构造函数:
| Example3 (const string& str); | |
而当任何构造函数的类显式声明,没有任何隐含的
默认构造函数自动提供。
析构函数
析构函数实现的功能相反的
构造:他们负责,当其生命周期结束的一类所需要的必要的清理。我们在前面的章节中定义的类没有分配任何资源,因此并不真正需要的任何清理。
但现在,让我们想象一下,在最后一个例子类动态分配的内存来存储它有数据成员字符串; 在这种情况下,这将是有一个在电荷释放该存储器的对象的使用寿命结束自动调用功能非常有用的。要做到这一点,我们使用
的析构函数。析构函数是一个成员函数非常相似,
默认的构造函数:它没有参数,没有返回值,甚至没有
void
。它还使用类名作为自己的名称,但前面有一个波浪符号(
~
):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // 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&); | |
如果一个类没有自定义
副本,也没有
移动构造函数(或转让)的定义,一个隐含的
拷贝构造函数提供。这个拷贝构造函数简单地执行自己的成员的副本。例如,对于一类,例如:
1
2
3
4
| class MyClass {
public:
int a, b; string c;
}; | |
隐式
的拷贝构造函数被自动定义。假设这个函数的定义,执行一个
浅拷贝,大致相当于:
| MyClass::MyClass(const MyClass& x) : a(x.a), b(x.b), c(x.c) {} | |
这个默认
的拷贝构造函数可能适合很多阶层的需求。但
浅拷贝只有类本身的成员副本,这可能不是我们所期望像类的类
Example4
上面定义,因为它包含指针,其中它处理它的存储。对于该类,执行
浅拷贝意味着该指针值被复制,而不是内容本身; 这意味着两个对象(副本和原件)将共享一个
string
对象(他们都指向同一个对象),并在某些时候(毁灭),这两个对象会尝试删除相同的内存块,可能导致程序崩溃的运行时间。这可以通过定义下列自定义解决
拷贝构造函数执行一个
深拷贝:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // 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;
} | 酒吧的内容:例 |
|
该
深拷贝这个执行
拷贝构造一个新的字符串,它被初始化为包含原始对象的副本分配存储空间。以这种方式,两个对象(复制和原始)具有存储在不同位置的内容的不同拷贝。
拷贝赋值
对象不仅复制在结构中,当它们被初始化:它们也可在任何分配操作复制。看到不同:
1
2
3
4
| 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
。在这种情况下,不仅类即被删除尖锐的物体的两倍的风险,但分配通过不删除由分配之前指向的对象的对象创建内存泄漏。这些问题可以用一个解决
拷贝赋值一个删除以前的对象,并执行
深度复制:
1
2
3
4
5
6
| 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
对象:
1
2
3
4
| Example5& operator= (const Example5& x) {
*ptr = x.content();
return *this;
} | |
移动构造函数和赋值
到复制类似,也移动使用对象的值的值设置为另一个目的。但是,不同于复制,内容实际上是从一个对象(源)连接到其它(目标)传送:源失去该内容,这是由目的地接管。该移动仅当值的来源是一个发生
未命名的对象。
未命名的对象是在临时性质,因此还没有被命名的对象。典型的例子
无名对象是功能或类型强制转换返回值。
使用临时对象的值,如这些初始化另一个对象或分配它的值,并不真正需要的副本:对象是永远不会被用于别的,并且因此,其值可以被
移动到目标目的。这些案件引发的
移动构造函数和
移动作业:
在
移动构造函数当一个对象被施工用临时无名初始化被调用。同样,
移动分配当一个对象被分配一个临时的无名的值称为:
1
2
3
4
5
6
| 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
是无名的临时对象。在这些情况下,没有必要进行复印,由于未命名的对象是非常短暂的,并且可以由其他对象来获取时,这是一个更有效的操作。
此举构造函数和移动赋值是利用类型的参数成员
右值引用的类本身:
1
2
| MyClass (MyClass&&); // move-constructor
MyClass& operator= (MyClass&&); // move-assignment | |
一个
右值引用是由两个&符号以下类型指定的(
&&
)。作为一个参数,一个
右值引用符合这种类型的临时的参数。
移动的概念是用于管理他们所使用的存储对象,诸如与新和删除分配存储对象最有用的。在这样的对象,复制和移动真正不同的操作:
-复制从A到B意味着新的内存分配给B,然后A的全部内容复制到分配给B.这个新的记忆
-从A移动到B已经分配给内存不分配任何新的存储转移到B。它涉及到简单的复制指针。
例如:
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
| // 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引用可用于任何功能参数的类型,它是比其他用途很少有用
动构造。右值引用是棘手的,和不必要的用途可能是错误的根源非常难以跟踪。
隐式成员
六个
特殊成员函数如上所述成员隐含宣布在某些情况下类:
成员函数 | 隐式定义: | 默认的定义: |
---|
默认构造方法 | 如果没有其他的构造函数 | 什么也没做 |
析构函数 | 如果没有析构函数 | 什么也没做 |
复制构造 | 如果没有移动构造函数和移动不分配 | 副本所有成员 |
拷贝赋值 | 如果没有移动构造函数和移动不分配 | 副本所有成员 |
移动构造 | 如果没有析构函数,没有拷贝构造函数和无可复制,也没有移动分配 | 将所有成员 |
移动分配 | 如果没有析构函数,没有拷贝构造函数和无可复制,也没有移动分配 | 将所有成员 |
请注意如何并不是所有的
特殊成员函数在同一个案件隐含定义。这主要是由于与C结构的向后兼容性和更早的C ++版本,而实际上有些过时包括案件。幸运的是,每个类都可以明确地选择这些成员与它们的默认定义,或存在使用关键字被删除
default
和
delete
分别。语法中的任一个:
function_declaration =默认值;
function_declaration =删除;
|
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // 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
可以构造或者与2
int
的参数或者
默认构造(不带参数)。它然而,不能
复制构造从另一个
Rectangle
目的,因为该功能已被删除。因此,假设的最后一个实施例的目的,下面的语句将不能有效的:
它可以,但是,可以通过定义它的拷贝构造为取得明确有效的:
| Rectangle::Rectangle (const Rectangle& other) = default; | |
这将基本上等同于:
| Rectangle::Rectangle (const Rectangle& other) : width(other.width), height(other.height) {} | |
需要注意的是,关键字
default
没有定义一个成员函数等于
默认构造函数(即,在
默认构造函数是指构造不带参数),但等于将如不删除,将隐式定义构造函数。
一般情况下,并为未来的兼容性,明确地定义一个复制/移动构造函数或一个拷贝/移动分配但不同时,鼓励指定要么类
delete
或
default
对其他特殊的成员函数,他们并没有明确界定。