C&C++学习笔记(3)——operator

操作运算符operator在C++中运用的很频繁,各种类的运算符重载都要用到它,但是本章是为函数对象做铺垫。

本文仅供个人记录和复习,不用于其他用途

函数运算符operator()

重载运算符的部分不再赘述,我们这里只针对函数运算符operator()进行分析。函数运算符一般用于标准模板库(STL)中,通常是STL算法中。一般来说,函数对象可分为单目谓词和双目谓词。

简单举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <string>
using namespace std;
class CDisplay
{
public:
void operator () (string Input) const
{
cout << Input << endl;
}
};
int main()
{
CDisplay mDisplayFuncObject;
mDisplayFuncObject("Dispaly this string!");
}

上面是一个十分简单的例子,我们在CDisplay类中对()进行了定义,使得对象可以像函数一样。事实上,这个运算符被称作operator()函数,对象CDisplay也被称为函数对象或者functor

移动构造函数和移动赋值运算符

这两个主要用于性能优化,避免复制不必要的临时值。

问题引入

1
2
3
4
5
6
7
8
9
10
11
12
class Date
{
private:
int Day, Month, Year;
public:
Date operator + (int DaysToAdd)
{
Date newDate(Day + DaysToAdd, Month, Year);
return newDate;
}
};

类似于上面的这个类,如果对加法运算符进行了重载,那么一般就会返回一个拷贝,减法运算符也是如此。如果我们新建一个MyString类,也使用这种重载方法会怎么样呢?

1
2
3
4
5
6
MyString Hello("Hello ");
MyString World("World");
MyString CPP(" of C++");
MyString sayHello(Hello + World + CPP);
MyString sayHelloAgain("overwrite this");
sayHelloAgain = Hello + World + CPP;

如果一个类重载了加法运算符,那么就会返回一个拷贝。虽然加法运算符可以轻松地拼接字符串,不过也会导致一些性能问题。创建sayHello时,需要执行加法运算符两次,每次都要创建一个按值返回的临时拷贝。也就是说,我们在这里使用了两次复制构造函数,执行了深复制。但事实上,临时拷贝在表达式执行完毕之后就不需要了,我们并不需要深复制。

代码实例

为了解决这个问题,我们需要使用移动构造函数和移动赋值运算符:

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
class MyClass
{
private:
Type *PtrResource;
public:
MyClass();
MyClass(const MyClass &CopySource);
MyClass &operator = (const MyClass &CopySource);
MyClass(MyClass &&MoveSource)
{
PtrResource = MoveSource.PtrResource;
MoveSource.PtrResource = NULL;
}
MyClass &operator = (MyClass &&MoveSource)
{
if(this != &MoveSource)
{
delete [] PtrResource;
PtrResource = MoveSource.PtrResource;
MoveSource.PtrResource = NULL;
}
}
};

相比于常规赋值构造函数和复制赋值运算符的声明,移动构造函数和移动赋值运算符的不同之处就在于,输入参数的类型为MyClass &&。这两种只是将资源从源移动到目的地。两个&&代表右值引用,不会进行复制。

这里为了详细的解释这四种函数,提供了一个模板:

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include <iostream>
using namespace std;
class MyString
{
private:
char *Buffer;
MyString(): Buffer(NULL)
{
cout << "Default constructor called" << endl;
}
public:
~MyString()
{
if(Buffer != NULL)
{
delete [] Buffer;
}
}
int GetLength()
{
return strlen(Buffer);
}
operator const char *()
{
return Buffer;
}
MyString operator + (const MyString &AddThis)
{
cout << "operator + called:" << endl;
MyString NewString;
if(AddThis.Buffer != NULL)
{
NewString.Buffer = new char[GetLength() + strlen(AddThis.Buffer) + 1];
strcpy(NewString.Buffer, Buffer);
strcat(NewString.Buffer, AddThis.Buffer);
}
return NewString;
}
MyString(const char *InitialInput)
{
cout << "Constructor called for:" << InitialInput << endl;
if(InitialInput != NULL)
{
Buffer = new char[strlen(InitialInput) + 1];
strcpy(Buffer, InitialInput);
}
else
{
Buffer = NULL;
}
}
MyString &operator = (const MyString &CopySource)
{
cout << "Copy assignment operator to copy from:" << CopySource.Buffer << endl;
if((this != &CopySource) && (CopySource.Buffer != NULL))
{
if(Buffer != NULL)
{
delete [] Buffer;
}
Buffer = new char[strlen(CopySource.Buffer) + 1];
strcpy(Buffer, CopySource.Buffer);
}
return *this;
}
MyString(MyString &&MoveSource)
{
cout << "Move constructor to move from:" << MoveSource.Buffer << endl;
if(MoveSource.Buffer != NULL)
{
Buffer = MoveSource.Buffer;
MoveSource.Buffer = NULL;
}
}
MyString &operator = (MyString &&MoveSource)
{
cout << "Move assignment operator to move from:" << MoveSource.Buffer <<endl;
if((MoveSource.Buffer != NULL) && (this != &MoveSource))
{
delete Buffer;
Buffer = MoveSource.Buffer;
MoveSource.Buffer = NULL;
}
return *this;
}
};
int main()
{
MyString Hello("Hello ");
MyString World("World");
MyString CPP(" of C++");
MyString sayHelloAgain("overwrite this");
sayHelloAgain = Hello + World + CPP;
return 0;
}

控制台输出如下:

Constructor called for:Hello
Constructor called for:World
Constructor called for: of C++
Constructor called for:overwrite this
operator + called:
Default constructor called
Move constructor to move from:Hello World
operator + called:
Default constructor called
Move constructor to move from:Hello World of C++
Move assignment operator to move from:Hello World of C++

事实上,移动的就是传入参数为MyClass &&。另外呢,由于输入参数是要移动的源对象,所以不能使用cosnt限定。总之,只要创建临时右值时,C++编译器便会使用移动的函数。

移动的函数其实就是接管移动源中的资源所有权,而复制的则是分配新的空间,然后将移动源的信息拷贝进来。

题外话

事实上,把构造函数、复制赋值运算符、复制构造函数等等声明为私有,是为了实现单例模式。当然,如果单纯只是想阻止栈上实例化(比如占据内存较大的数据库类),就可以将析构函数声明为私有。这样一来,编译器在编译过程中就会因为析构函数为私有,从而终止栈上实例化。但是呢,堆上的实例化是可行的,不过我们需要定义一个类成员函数,用来执行delete