Вступление
Конструкторы C ++ более сложны, чем их аналоги из C #. С одной стороны, существует пять типов конструкторов. Хотя вы редко будете писать все пять типов для какого-либо конкретного класса или структуры, вам нужно знать, что они, что они делают и как они выглядят. Если нет, вы можете столкнуться с некоторыми очень запутанными ошибками или ошибками компилятора.
Причина, по которой конструкторы C ++ более вовлечены, чем конструкторы C #, заключается в разнообразии длительности хранения, которое может иметь класс C ++. По умолчанию объекты C ++ имеют семантику значений, а классы C # имеют ссылочную семантику. Вот пример:
1
|
Vehicle someVehicle = vehicle;
|
Давайте предположим, что vehicle не является нулевым, но является допустимым объектом типа Vehicle, и что тип Vehicle является типом класса, а не структурой или чем-то еще.
Давайте рассмотрим предыдущий оператор кода, как если бы это был код C #. В C # объект, на который ссылается транспортное средство, живет где-то в куче, управляемой GC. Предыдущий оператор кода сохраняет ссылку на этот объект в переменной someVehicle, которую он получает из существующей ссылки транспортного средства на объект. Все еще есть только один экземпляр этого объекта с двумя ссылками на него.
Давайте теперь рассмотрим предыдущее утверждение, как если бы это был код C ++. В C ++ объект, на который ссылается транспортное средство, скорее всего, является автоматическим объектом продолжительности, но это может быть статический объект продолжительности или даже объект продолжительности потока. Предыдущая инструкция кода по умолчанию создаст копию объекта транспортного средства и сохранит ее в адресе автоматической переменной someVehicle duration. Это делается с помощью чего-то, называемого оператором копирования, близким родственником конструктора копирования.
Сейчас есть две копии, где когда-то была одна. Если у класса Vehicle нет указателя, std :: shared_ptr, ссылки или чего-то подобного в качестве переменной-члена, две копии полностью разделены. Изменение или даже уничтожение одного не окажет никакого влияния на другого.
Конечно, если вы хотите, чтобы someVehicle являлся ссылкой на объект транспортного средства, вы можете немного изменить код и выполнить это. Но что, если бы вы хотели, чтобы какое-то транспортное средство стало транспортным средством, а транспортное средство перестало существовать — было уничтожено, не забрав при этом свои прежние ресурсы? C ++ делает это возможным благодаря так называемому конструктору перемещения и оператору присваивания перемещения.
Если вы хотите намеренно отключить семантику копирования (назначение и конструирование, вместе) или переместить семантику, это тоже возможно.
Конструктор по умолчанию
Начнем с конструкторов по умолчанию. Конструктор по умолчанию может быть вызван без аргументов. Класс может иметь не более одного конструктора по умолчанию. Если вы определяете класс и не включаете конструкторы, вы создаете неявный конструктор по умолчанию, который компилятор создает для вас. Однако, если у вас есть переменная-член класса, которая является классом, структурой или объединением, у которого нет конструктора по умолчанию, вы должны определить хотя бы один конструктор, поскольку компилятор не может создать неявный конструктор по умолчанию.
Если вы определяете конструктор без параметров, это явный конструктор по умолчанию. Также возможно определить конструктор, который принимает параметры и при этом сделать его конструктором по умолчанию, используя аргументы по умолчанию, которые мы обсудим чуть позже.
Если вы определите конструктор, который имеет хотя бы один обязательный параметр, то компилятор не будет генерировать конструктор по умолчанию. Вы должны определить явный конструктор по умолчанию, если хотите.
Аргументы по умолчанию в объявлениях функций
Конструкторы в C ++ и функции в целом могут иметь аргументы по умолчанию, определенные для некоторых или всех их параметров как часть их объявления (аналогично необязательным аргументам C #). В функциях C ++ все параметры с аргументами по умолчанию должны находиться справа от любого параметра без аргумента по умолчанию в объявлении функции. C ++ не поддерживает именованные аргументы в стиле C #, но вы можете сделать нечто подобное, используя именованный параметр idiom.
Зачем здесь указывать аргументы по умолчанию? Что ж, получается, что если у вас есть конструктор, в котором все параметры имеют аргументы по умолчанию, этот конструктор является конструктором по умолчанию. Это имеет смысл, поскольку, если у вас есть конструктор с подписью Vehicle (VehicleType type = VehicleType :: Car, double odometerReading = 0.0); затем вы можете вызвать этот конструктор с пустыми скобками, и эти аргументы по умолчанию будут применены. Если вы определяете конструктор по умолчанию, у вас может быть только один, независимо от того, у него нет параметров или у всех его параметров есть аргументы по умолчанию.
Этот принцип идет еще дальше. Никакие две функции с одинаковыми именами, объявленные в одной и той же области видимости, не могут иметь одинаковые типы параметров в одинаковых позициях. Как вы уже догадались, параметры с аргументами по умолчанию не учитываются для определения сигнатур функций.
Все это имеет смысл, потому что аргументы по умолчанию делают невозможным различение double Add (double a, double b = 0.0); и двойной Add (double a, int a = 0); учитывая, что оба могут быть названы как double dbl = Add (5.0) ;. Компилятор не может знать, что вы намеревались в этом случае, поэтому он просто не может скомпилировать и отображает сообщение об ошибке.
Параметризованные Конструкторы
Параметризованный конструктор имеет один или несколько параметров. Параметризованный конструктор, в котором все параметры имеют аргументы по умолчанию, также является конструктором по умолчанию для класса. В параметризованных конструкторах C ++ нет ничего особенного.
Конструкторы преобразования
Конструктор преобразования имеет хотя бы один параметр. Если их несколько, то эти дополнительные параметры должны иметь аргументы по умолчанию.
Если вы не хотите, чтобы конструктор был конструктором преобразования, вы можете пометить его спецификатором функции: явный. Давайте посмотрим на пример:
01
02
03
04
05
06
07
08
09
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
|
#include <string>
class SomeClass
{
public:
SomeClass(const wchar_t* value) :
m_strValue(value),
m_intValue()
{
}
explicit SomeClass(int value) :
m_strValue(),
m_intValue(value)
{
}
~SomeClass(void) { }
const wchar_t* GetStringValue(void)
{
return m_strValue.c_str();
}
int GetIntValue(void) { return m_intValue;
private:
std::wstring m_strValue;
int m_intValue;
};
void DoSomething(void)
{
// Normal constructor use.
SomeClass sc1 = SomeClass(L»Hello World»);
// Fine because the const wchar_t* constructor
// is a conversion constructor.
SomeClass sc2 = L»Hello World»;
// Normal constructor use.
SomeClass sc3 = SomeClass(1);
//// Illegal since the int constructor is not a
//// conversion constructor.
//SomeClass sc4 = 1;
// …
}
|
Как вы можете видеть, конструктор преобразования позволяет нам создавать s2, непосредственно устанавливая его равным строковому значению. Компилятор видит этот оператор, проверяет, есть ли у SomeClass конструктор преобразования, который получит такое значение, и продолжает вызывать соответствующий конструктор SomeClass. Если бы мы попробовали это с закомментированной строкой sc4, компилятор потерпел бы неудачу, потому что мы использовали явное, чтобы сообщить компилятору, что конструктор, который просто принимает int, не должен рассматриваться как конструктор преобразования, а вместо этого должен быть похож на любой другой параметризованный конструктор.
Конструкторы преобразования могут быть полезны, но они также могут привести к ошибкам. Например, вы можете случайно создать новый объект и назначить его существующей переменной, когда вы просто неправильно набрали имя переменной и действительно имели в виду присвоение. Компилятор не будет жаловаться, если есть допустимый конструктор преобразования, но будет жаловаться, если его нет. Так что имейте это в виду и не забудьте пометить конструкторы с одним параметром как явные, за исключением случаев, когда у вас есть веские основания для предоставления конструктора преобразования.
Инициализация данных и базовых классов
К этому моменту мы увидели довольно много конструкторов, поэтому важно обсудить странный синтаксис, с которым вы столкнулись. Давайте рассмотрим образец:
Примечание. В этом примере используются очень плохие методы кода, чтобы проиллюстрировать, как C ++ выполняет инициализацию. В частности, порядок инициализации в определениях конструктора в некоторых местах вводит в заблуждение. Всегда проверяйте, чтобы ваши конструкторы упорядочивали свою инициализацию базовых классов и параметров в том порядке, в котором инициализация будет происходить во время выполнения программы. Это поможет вам избежать ошибок и сделает ваш код более удобным для отладки и понятным.
Пример: InitializationSample \ InitializationSample.cpp
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
#include <iostream>
#include <ostream>
#include «../pchar.h»
using namespace std;
int CallingMsg(const wchar_t* cls)
{
wcout << L»Calling » << cls << L» constructor.»
return 0;
}
int InitializingIntMsg(int value, const wchar_t* mvarName)
{
wcout << L»Initializing » << mvarName << L».»
return value;
}
class A
{
public:
A(void) :
m_value(InitializingIntMsg(5, L»DEFAULT m_value»))
{
wcout << L»DEFAULT Constructing A. m_value is ‘» <<
m_value << L»‘.»
}
explicit A(int) :
m_value(InitializingIntMsg(20, L»m_value»))
{
wcout << L»Constructing A. m_value is ‘» <<
m_value << L»‘.»
}
virtual ~A(void)
{
wcout << L»Destroying A.»
}
private:
int m_value;
};
class B : virtual public A
{
public:
explicit B(int) :
A(CallingMsg(L»A»)),
m_b(InitializingIntMsg(2, L»m_b»)),
m_a(InitializingIntMsg(5, L»m_a»))
{
wcout << L»Constructing B. m_a is ‘» <<
m_a << L»‘ and m_b is ‘» << m_b << L»‘.»
}
virtual ~B(void)
{
wcout << L»Destroying B.»
}
private:
int m_a;
int m_b;
};
class C
{
public:
explicit C(int) :
m_c(InitializingIntMsg(0, L»m_c»))
{
wcout << L»Constructing C. m_c is ‘» <<
m_c << L»‘.»
}
virtual ~C(void)
{
wcout << L»Destroying C.»
}
private:
int m_c;
};
class D
{
public:
explicit D(int) :
m_d(InitializingIntMsg(3, L»m_d»))
{
wcout << L»Constructing D. m_d is ‘» <<
m_d << L»‘.»
}
virtual ~D(void)
{
wcout << L»Destroying D.»
}
private:
int m_d;
};
class Y : virtual public B, public D, virtual public C
{
public:
explicit Y(int value) :
C(CallingMsg(L»C»)),
m_someInt(InitializingIntMsg(value, L»m_someInt»)),
D(CallingMsg(L»D»)),
B(CallingMsg(L»B»))
{
wcout << L»Constructing Y. m_someInt is ‘» <<
m_someInt << L»‘.»
}
virtual ~Y(void)
{
wcout << L»Destroying Y.»
}
int GetSomeInt(void) { return m_someInt;
private:
int m_someInt;
};
class Z : public D, virtual public B, public C
{
public:
explicit Z(int value) :
D(CallingMsg(L»D»)),
A(CallingMsg(L»A»)),
C(CallingMsg(L»C»)),
m_someInt(InitializingIntMsg(value, L»m_someInt»)),
B(CallingMsg(L»B»))
{
wcout << L»Constructing Z. m_someInt is ‘» <<
m_someInt << L»‘.»
}
virtual ~Z(void)
{
wcout << L»Destroying Z.»
}
int GetSomeInt(void) { return m_someInt;
private:
int m_someInt;
};
int _pmain(int /*argc*/, _pchar* /*argv*/[])
{
{
Y someY(CallingMsg(L»Y»));
wcout << L»Y::GetSomeInt returns ‘» <<
someY.GetSomeInt() << L»‘.»
}
wcout << endl << «Break between Y and Z.»
<< endl;
{
Z someZ(CallingMsg(L»Z»));
wcout << L»Z::GetSomeInt returns ‘» <<
someZ.GetSomeInt() << L»‘.»
}
return 0;
}
|
Первое, что мы сделали, это определили две вспомогательные функции, которые пишут сообщения, поэтому мы можем легко следить за порядком, в котором все происходит. Вы заметите, что у каждого из классов есть конструктор, который принимает int, хотя его используют только Y и Z. A, B, C и D даже не указывают имя для int в своих конструкторах параметров int; они просто указывают, что есть параметр int. Это совершенно допустимый код C ++, и мы использовали его вместе с закомментированными именами параметров в _pmain.
Класс A имеет два конструктора: конструктор по умолчанию и параметризованный конструктор, который принимает int. Остальные классы имеют только параметризованные конструкторы, каждый из которых принимает целое число.
- Класс B наследуется от A практически.
- Класс C наследуется из ничего.
- Класс D наследует из ничего.
- Класс Y наследует от B виртуально, непосредственно от D и виртуально от C, в этом порядке.
- Класс Z также наследуется от D напрямую, от B виртуально и от C напрямую, в этом порядке.
Давайте посмотрим на результаты, которые дает нам эта программа, а затем обсудим, что происходит и почему.
Вызов конструктора Y Инициализация DEFAULT m_value. ПО УМОЛЧАНИЮ Построение A. m_value равно '5'. Вызов конструктора B. Инициализация м_а. Инициализация m_b. Построение B. m_a равно 5, а m_b равно 2. Вызов конструктора C Инициализация m_c. Построение C. m_c равно '0'. Вызов конструктора D. Инициализация m_d. Построение D. m_d равно 3. Инициализация m_someInt. Построение Y. m_someInt равно '0'. Y :: GetSomeInt возвращает '0'. Уничтожая Y. Уничтожая Д. Уничтожение С. Уничтожая Б. Уничтожая А. Разрыв между Y и Z. Вызов конструктора Z Вызов конструктора. Инициализация m_value. Построение A. m_value равно '20'. Вызов конструктора B. Инициализация м_а. Инициализация m_b. Построение B. m_a равно 5, а m_b равно 2. Вызов конструктора D. Инициализация m_d. Построение D. m_d равно 3. Вызов конструктора C Инициализация m_c. Построение C. m_c равно '0'. Инициализация m_someInt. Построение Z. m_someInt равно '0'. Z :: GetSomeInt возвращает '0'. Уничтожение З. Уничтожение С. Уничтожая Д. Уничтожая Б. Уничтожая А.
В нашей функции _pmain сначала мы создаем объект типа Y в своей области видимости. Затем мы вызываем его функцию-член GetSomeInt. Это помогает гарантировать, что компилятор не будет оптимизировать создание Y в сборке релиза, если вы возитесь с кодом. Он также служит маркером между строительством и разрушением.
Затем мы выходим из сферы действия Y, вызывая его разрушение. После этого мы пишем еще одну строку маркера, чтобы отделить создание экземпляра Y от последующего создания экземпляра Z. Мы создаем экземпляр Z в его собственной области видимости, чтобы мы могли следить за его созданием и уничтожением так же, как с Y.
Итак, что мы видим? Достаточно много. Давайте сосредоточимся на Y в первую очередь.
Когда мы вызываем конструктор Y, первое, что он делает, это вызывает конструктор по умолчанию для A. Это может показаться ужасно неправильным по нескольким причинам, но на самом деле это правильно. Конструктор Y говорит следовать этому порядку:
- Инициализировать базовый класс C.
- Инициализируйте переменную-член Y m_someInt.
- Инициализировать базовый класс D.
- Инициализируйте базовый класс B.
Вместо этого мы получаем следующий заказ:
- Инициализируйте базовый класс A через его конструктор по умолчанию.
- Инициализируйте базовый класс B.
- Инициализировать базовый класс C.
- Инициализировать базовый класс D.
- Инициализируйте переменную-член Y m_someInt.
Поскольку мы знаем, что B наследует виртуально от A, и что B является единственным источником наследования от A, мы можем заключить, что B имеет приоритет над другими, и что A имеет приоритет над B.
Ну, мы наследуем от B, прежде чем мы наследуем от других классов. Так может быть, но почему D не инициализируется сразу после B? Это потому, что D наследуется напрямую, а C виртуально наследуется. Виртуалы на первом месте.
Вот правила:
- Виртуальные базовые классы создаются в порядке слева направо, так как они записаны в списке базовых классов.
- Если вы не вызываете конкретный конструктор для базового класса, от которого вы фактически унаследовали, компилятор автоматически вызовет его конструктор по умолчанию в соответствующее время.
- При определении порядка построения базовых классов базовые классы инициализируются перед их производными классами.
- Когда все виртуальные базовые классы созданы, тогда прямые базовые классы создаются в порядке их объявления слева направо — так же, как виртуальные базовые классы.
- Когда все его базовые классы созданы, переменная-член класса:
- По умолчанию инициализируется, если для него нет инициализатора.
- Значение инициализируется, если инициализатор представляет собой пустой набор скобок.
- Инициализируется в результате выражения в круглых скобках инициализатора.
- Переменные-члены инициализируются в порядке, в котором они объявлены в определении класса.
- После запуска всех инициализаторов в конструкторе будет выполнен любой код внутри тела конструктора.
Когда вы соединяете все эти правила вместе, вы сначала находите порядок B, C, D, потому что B и C имеют виртуальное наследование и, следовательно, предшествуют D. Затем мы добавляем A перед B, потому что B наследуется от A. Таким образом, мы получаем A , B, C, D.
Из-за правила, согласно которому базовые классы инициализируются перед производными классами, а также потому, что A входит через виртуальное наследование, A инициализируется с помощью конструктора по умолчанию, прежде чем мы даже получим его инициализатор в конструкторе B, который мы вызываем. Как только мы дойдем до конструктора B, поскольку A уже инициализирован, его инициализатор в конструкторе B просто игнорируется.
Переменные-члены класса B инициализируются в порядке m_a, m_b, потому что это порядок, в котором они объявлены в классе, хотя в конструкторе мы перечисляем их инициализации в обратном порядке.
Делегирующий конструктор
Примечание. Visual C ++ не поддерживает делегирование конструкторов в Visual Studio 2012 RC.
Делегирующий конструктор вызывает другой конструктор того же класса (целевой конструктор). Делегирующий конструктор может иметь только один оператор инициализатора, который является вызовом целевого конструктора. Его тело может иметь заявления; они будут запущены после того, как целевой конструктор будет полностью завершен. Вот пример:
01
02
03
04
05
06
07
08
09
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
|
#include <iostream>
#include <ostream>
using namespace std;
class SomeClass
{
public:
SomeClass(void) : SomeClass(10)
{
// Statements here will execute after the
// statements in the SomeClass(int) constructor
// body have finished executing.
wcout << «Running SomeClass::SomeClass(void).»
}
SomeClass(int value)
: m_value(value)
{
// Statements here will execute after the m_value
// initializer above.
wcout << «Running SomeClass::SomeClass(int).»
}
int GetValue(void) { return m_value;
private:
int m_value;
};
int main(int argc, char* argv[])
{
SomeClass someC;
wcout << L»SomeClass::GetValue() = » << someC.GetValue() << endl;
return 0;
}
|
Если вы скомпилируете и запустите этот код с помощью компилятора, такого как GCC, вы увидите следующий вывод:
Запуск SomeClass :: SomeClass (int). Запуск SomeClass :: SomeClass (void). SomeClass :: GetValue () = 10
Копировать конструктор
Конструктор копирования имеет только один обязательный параметр: ссылку на переменную того же типа, что и класс конструктора. Конструктор копирования может иметь другие параметры, если все они снабжены аргументами по умолчанию. Его цель — позволить вам создать копию объекта.
Компилятор предоставит вам конструктор копирования по умолчанию, если это возможно, и может, если все переменные-члены, являющиеся классами, структурами или объединениями, имеют конструктор копирования. Предоставляемый им конструктор копирования является конструктором поверхностного копирования. Например, если у вас есть указатель на массив данных, копия получает копию указателя, а не новый массив, содержащий копию тех же данных.
Если бы тогда в деструкторе этого класса был оператор удаления для этого класса, одна копия была бы в недопустимом состоянии, когда другая была уничтожена, и у вас возникла бы ошибка во время выполнения, когда вы пытались удалить память во второй раз, когда оставшаяся копия была уничтожена, при условии, что ваша программа еще не потерпела крах. Это одна из многих причин, по которой вы всегда должны использовать умные указатели. Мы расскажем о них в главе о RAII.
Если вы не хотите использовать предоставленный компилятором конструктор копирования, или если компилятор не может его предоставить, но вы все равно хотите его иметь, вы можете написать конструктор копирования. Например, возможно, вам нужна более глубокая копия данных, или, возможно, у вашего класса есть std :: unique_ptr, и вы решаете, какой будет его «приемлемая» копия для целей вашего класса. Мы увидим пример этого в ConstructorsSample.
Конструктор копирования обычно должен иметь следующее объявление: SomeClass (const SomeClass &) ;. Чтобы избежать странных ошибок, конструктор всегда должен принимать постоянную ссылку на класс, из которого вы копируете. Нет причины, по которой вы должны изменить класс, из которого вы копируете, в конструкторе копирования. Создание этого const не вредит и обеспечивает некоторые гарантии о вашей работе. Конструктор копирования не должен быть определен как явный.
Оператор назначения копирования
Если вы определяете пользовательский конструктор копирования, вы также должны определить пользовательский оператор назначения копирования. Результатом этого оператора должно быть то, что возвращаемое значение является копией класса, который он копирует. Это то, что вызывается, когда у вас есть такое утверждение, как a = b; где a и b имеют одинаковый тип (например, SomeClass).
Этот оператор является нестатической функцией-членом своего класса, поэтому он вызывается только при назначении копии существующему экземпляру класса. Если у вас было что-то вроде SomeClass a = b; тогда это будет конструкция копирования, а не назначение копирования.
Оператор присваивания копии должен иметь следующую декларацию: SomeClass & operator = (const SomeClass &) ;.
Переместить конструктор
В C ++, если все, что у вас было, это конструктор копирования, и вы хотите передать экземпляр класса в std :: vector (аналог. .NET List <T>) или вернуть его из функции, вам нужно будет сделать копия этого. Даже если вы не собираетесь использовать его снова, вам все равно придется потратить время на копирование. Если вы добавляете много элементов в std :: vector, или если вы написали фабричную функцию, которой вы часто пользуетесь, это сильно ухудшит производительность.
Вот почему C ++ имеет конструктор перемещения. Конструктор перемещения является новым в C ++ 11. Есть некоторые обстоятельства, при которых компилятор предоставит вам их, но обычно вы должны написать свой собственный для классов, для которых он вам понадобится.
Это легко сделать. Обратите внимание, что если вы пишете конструктор перемещения, вам также необходимо написать конструктор копирования. Visual C ++ не применяет это правило в Visual Studio 2012 RC, но оно является частью стандарта языка C ++. Если вам нужно скомпилировать вашу программу с другим компилятором, вы должны убедиться, что вы пишете конструкторы копирования и операторы копирования при написании конструктора перемещения.
Конструктор перемещения обычно должен иметь следующее объявление: SomeClass (SomeClass &&) ;. Он не может быть постоянным (потому что мы будем его модифицировать) или явным.
Функция std::move
Функция std::move
помогает писать конструкторы перемещения и операторы присваивания перемещения. Он находится в заголовочном файле <utility>. Он принимает один аргумент и возвращает его в состоянии, подходящем для перемещения. Переданный объект будет возвращен как ссылка rvalue, если семантика перемещения для него не была отключена, в этом случае вы получите ошибку компилятора.
Оператор назначения перемещения
Всякий раз, когда вы пишете конструктор перемещения для класса, вы также должны написать оператор присваивания перемещения. Результатом этого оператора должно быть то, что возвращаемое значение содержит все данные старого класса. Правильный оператор присваивания перемещения может быть вызван из вашего конструктора перемещения, чтобы избежать дублирования кода.
Оператор присваивания перемещения должен иметь следующую декларацию: SomeClass & operator = (SomeClass &&) ;.
Удаление семантики копирования или перемещения
Если вам нужно удалить или переместить семантику, есть два способа сделать это. Во-первых, чтобы удалить семантику копирования, вы можете объявить конструктор копирования и оператор присваивания копии как закрытые и оставить их нереализованными. C ++ заботится только о реализации функции, если вы пытаетесь ее использовать. При этом любая попытка скомпилировать программу, которая пытается использовать семантику копирования, потерпит неудачу, что приведет к появлению сообщений об ошибках, в которых говорится, что вы пытаетесь использовать закрытый член класса, и для него нет реализации (если вы случайно используете его в сам класс). Та же самая схема создания приватности конструктора и оператора присваивания одинаково хорошо работает для семантики перемещения.
Второй способ является новым для C ++ 11 и в настоящее время не поддерживается Visual C ++ начиная с Visual Studio 2012 RC. Таким образом, вы объявите функциональность как явно удаляемую. Например, чтобы явно удалить конструктор копирования, вы должны написать SomeClass (const SomeClass &) = delete ;. Тот же синтаксис = delete применяется к операторам присваивания, конструктору перемещения и любым другим функциям. Однако до тех пор, пока Visual C ++ его не поддержит, вам придется придерживаться первого пути.
Деструкторы и Виртуальные Деструкторы
Любой класс, служащий базовым классом другого класса, должен иметь виртуальный деструктор. Вы объявляете виртуальный деструктор, используя ключевое слово virtual (например: virtual ~ SomeClass (void);). Таким образом, если вы приведете объект к одному из его подклассов, а затем впоследствии уничтожите объект, будет вызван соответствующий конструктор, обеспечивающий освобождение всех захваченных классом ресурсов.
Правильные деструкторы имеют решающее значение для идиомы RAII, которую мы вскоре рассмотрим. Вы никогда не должны позволять создавать исключение в деструкторе, если вы не перехватите и не обработаете это исключение в деструкторе. Если есть исключение, которое вы не можете обработать, вы должны выполнить безопасную регистрацию ошибок и затем выйти из программы. Вы можете использовать функцию std::terminate
в заголовочном файле <exception> для вызова текущего обработчика завершения. По умолчанию обработчик завершения вызывает функцию abort из заголовка <cstdlib>. Мы обсудим эту функциональность далее в нашем исследовании исключений стандартной библиотеки C ++.
Перегрузка оператора
Перегрузка операторов — мощная, расширенная функция C ++. Вы можете перегружать операторы для каждого класса или глобально с помощью отдельной функции. Почти каждый оператор в C ++ может быть перегружен. Мы вскоре увидим примеры перегрузки операций копирования, назначения перемещения, &, | и | =. Для получения списка других операторов, которые вы можете перегрузить, и как эти перегрузки работают, я рекомендую посетить документацию MSDN по этому вопросу. Это полезная функция, но вы не можете ее использовать. Поиск, когда вам это нужно, часто быстрее, чем попытка запомнить его сразу.
Совет: не перегружайте оператор, чтобы придать ему значение, которое может ввести в заблуждение и противоречить чьим-то ожиданиям относительно того, что делает эта перегрузка. Например, оператор + обычно должен выполнять операцию сложения или объединения. Если вычесть, вычесть, разделить, разделить, умножить или что-то еще, что могло бы показаться странным, это запутает других и создаст большой потенциал для ошибок. Это не означает, что вы не должны помещать операторы без четкого семантического значения в конкретную ситуацию для использования. std::wcin
ввода-вывода std::wcout
и std::wcin
из стандартной библиотеки C ++ позволяют использовать операторы >> и << при записи и чтении данных.
Поскольку битовое смещение не будет иметь особого значения применительно к потокам, перепрофилирование этих операторов таким образом кажется странным и разным, но не поддается каким-либо явно ошибочным выводам об их назначении. Когда вы поймете, что делают операторы применительно к потокам, их перепрофилирование добавляет функциональность к языку, который в противном случае потребовал бы больше кода.
Образец: ConstructorsSample \ Flavor.h
01
02
03
04
05
06
07
08
09
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
|
#pragma once
namespace Desserts
{
enum class Flavor
{
None,
Vanilla,
Chocolate,
Strawberry
};
inline Flavor operator|=(Flavor a, Flavor b)
{
return static_cast<Flavor>(static_cast<int>(a) | static_cast<int>(b));
}
inline const wchar_t* GetFlavorString(Flavor flavor)
{
switch (flavor)
{
case Desserts::Flavor::None:
return L»No Flavor Selected»;
break;
case Desserts::Flavor::Vanilla:
return L»Vanilla»;
break;
case Desserts::Flavor::Chocolate:
return L»Chocolate»;
break;
case Desserts::Flavor::Strawberry:
return L»Strawberry»;
break;
default:
return L»Unknown»;
break;
}
}
}
|
Образец: ConstructorsSample \ Toppings.h
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
|
#pragma once
#include <string>
#include <sstream>
namespace Desserts
{
class Toppings
{
public:
enum ToppingsList : unsigned int
{
None = 0x00,
HotFudge = 0x01,
RaspberrySyrup = 0x02,
CrushedWalnuts = 0x04,
WhippedCream = 0x08,
Cherry = 0x10
} m_toppings;
Toppings(void) :
m_toppings(None),
m_toppingsString()
{
}
Toppings(ToppingsList toppings) :
m_toppings(toppings),
m_toppingsString()
{
}
~Toppings(void)
{
}
const wchar_t* GetString(void)
{
if (m_toppings == None)
{
m_toppingsString = L»None»;
return m_toppingsString.c_str();
}
bool addSpace = false;
std::wstringstream wstrstream;
if (m_toppings & HotFudge)
{
if (addSpace)
{
wstrstream << L» «;
}
wstrstream << L»Hot Fudge»;
addSpace = true;
}
if (m_toppings & RaspberrySyrup)
{
if (addSpace)
{
wstrstream << L» «;
}
wstrstream << L»Raspberry Syrup»;
addSpace = true;
}
if (m_toppings & CrushedWalnuts)
{
if (addSpace)
{
wstrstream << L» «;
}
wstrstream << L»Crushed Walnuts»;
addSpace = true;
}
if (m_toppings & WhippedCream)
{
if (addSpace)
{
wstrstream << L» «;
}
wstrstream << L»Whipped Cream»;
addSpace = true;
}
if (m_toppings & Cherry)
{
if (addSpace)
{
wstrstream << L» «;
}
wstrstream << L»Cherry»;
addSpace = true;
}
m_toppingsString = std::wstring(wstrstream.str());
return m_toppingsString.c_str();
}
private:
std::wstring m_toppingsString;
};
inline Toppings operator&(Toppings a, unsigned int b)
{
a.m_toppings = static_cast<Toppings::ToppingsList>(static_cast<int>(a.m_toppings) & b);
return a;
}
inline Toppings::ToppingsList operator&(Toppings::ToppingsList a, unsigned int b)
{
auto val = static_cast<Toppings::ToppingsList>(static_cast<unsigned int>(a) & b);
return val;
}
inline Toppings::ToppingsList operator|(Toppings::ToppingsList a, Toppings::ToppingsList b)
{
return static_cast<Toppings::ToppingsList>(static_cast<int>(a) | static_cast<int>(b));
}
inline Toppings operator|(Toppings a, Toppings::ToppingsList b)
{
a.m_toppings = static_cast<Toppings::ToppingsList>(static_cast<int>(a.m_toppings) | static_cast<int>(b));
return a;
}
}
|
Образец: ConstructorsSample \ IceCreamSundae.h
01
02
03
04
05
06
07
08
09
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
|
#pragma once
#include «Flavor.h»
#include «Toppings.h»
#include <string>
namespace Desserts
{
class IceCreamSundae
{
public:
IceCreamSundae(void);
IceCreamSundae(Flavor flavor);
explicit IceCreamSundae(Toppings::ToppingsList toppings);
IceCreamSundae(const IceCreamSundae& other);
IceCreamSundae& operator=(const IceCreamSundae& other);
IceCreamSundae(IceCreamSundae&& other);
IceCreamSundae& operator=(IceCreamSundae&& other);
~IceCreamSundae(void);
void AddTopping(Toppings::ToppingsList topping);
void RemoveTopping(Toppings::ToppingsList topping);
void ChangeFlavor(Flavor flavor);
const wchar_t* GetSundaeDescription(void);
private:
Flavor m_flavor;
Toppings m_toppings;
std::wstring m_description;
};
}
|
Образец: ConstructorsSample \ IceCreamSundae.cpp
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
|
#include «IceCreamSundae.h»
#include <string>
#include <sstream>
#include <iostream>
#include <ostream>
#include <memory>
using namespace Desserts;
using namespace std;
IceCreamSundae::IceCreamSundae(void) :
m_flavor(Flavor::None),
m_toppings(Toppings::None),
m_description()
{
wcout << L»Default constructing IceCreamSundae(void).»
endl;
}
IceCreamSundae::IceCreamSundae(Flavor flavor) :
m_flavor(flavor),
m_toppings(Toppings::None),
m_description()
{
wcout << L»Conversion constructing IceCreamSundae(Flavor).»
endl;
}
IceCreamSundae::IceCreamSundae(Toppings::ToppingsList toppings) :
m_flavor(Flavor::None),
m_toppings(toppings),
m_description()
{
wcout << L»Parameter constructing IceCreamSundae(\
Toppings::ToppingsList).» << endl;
}
IceCreamSundae::IceCreamSundae(const IceCreamSundae& other) :
m_flavor(other.m_flavor),
m_toppings(other.m_toppings),
m_description()
{
wcout << L»Copy constructing IceCreamSundae.»
}
IceCreamSundae& IceCreamSundae::operator=(const IceCreamSundae& other)
{
wcout << L»Copy assigning IceCreamSundae.»
m_flavor = other.m_flavor;
m_toppings = other.m_toppings;
return *this;
}
IceCreamSundae::IceCreamSundae(IceCreamSundae&& other) :
m_flavor(),
m_toppings(),
m_description()
{
wcout << L»Move constructing IceCreamSundae.»
*this = std::move(other);
}
IceCreamSundae& IceCreamSundae::operator=(IceCreamSundae&& other)
{
wcout << L»Move assigning IceCreamSundae.»
if (this != &other)
{
m_flavor = std::move(other.m_flavor);
m_toppings = std::move(other.m_toppings);
m_description = std::move(other.m_description);
other.m_flavor = Flavor::None;
other.m_toppings = Toppings::None;
other.m_description = std::wstring();
}
return *this;
}
IceCreamSundae::~IceCreamSundae(void)
{
wcout << L»Destroying IceCreamSundae.»
}
void IceCreamSundae::AddTopping(Toppings::ToppingsList topping)
{
m_toppings = m_toppings |
}
void IceCreamSundae::RemoveTopping(Toppings::ToppingsList topping)
{
m_toppings = m_toppings & ~topping;
}
void IceCreamSundae::ChangeFlavor(Flavor flavor)
{
m_flavor = flavor;
}
const wchar_t* IceCreamSundae::GetSundaeDescription(void)
{
wstringstream str;
str << L»A » << GetFlavorString(m_flavor) <<
L» sundae with the following toppings: » << m_toppings.GetString();
m_description = wstring(str.str());
return m_description.c_str();
}
|
Образец: ConstructorsSample \ ConstructorsSample.cpp
01
02
03
04
05
06
07
08
09
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
|
#include <iostream>
#include <ostream>
#include «IceCreamSundae.h»
#include «Flavor.h»
#include «Toppings.h»
#include «../pchar.h»
using namespace Desserts;
using namespace std;
typedef Desserts::Toppings::ToppingsList ToppingsList;
int _pmain(int /*argc*/, _pchar* /*argv*/[])
{
const wchar_t* outputPrefixStr = L»Current Dessert: «;
IceCreamSundae s1 = Flavor::Vanilla;
wcout << outputPrefixStr << s1.GetSundaeDescription() << endl;
s1.AddTopping(ToppingsList::HotFudge);
wcout << outputPrefixStr << s1.GetSundaeDescription() << endl;
s1.AddTopping(ToppingsList::Cherry);
wcout << outputPrefixStr << s1.GetSundaeDescription() << endl;
s1.AddTopping(ToppingsList::CrushedWalnuts);
wcout << outputPrefixStr << s1.GetSundaeDescription() << endl;
s1.AddTopping(ToppingsList::WhippedCream);
wcout << outputPrefixStr << s1.GetSundaeDescription() << endl;
s1.RemoveTopping(ToppingsList::CrushedWalnuts);
wcout << outputPrefixStr << s1.GetSundaeDescription() << endl;
wcout << endl <<
L»Copy constructing s2 from s1.»
IceCreamSundae s2(s1);
wcout << endl <<
L»Copy assignment to s1 from s2.»
s1 = s2;
wcout << endl <<
L»Move constructing s3 from s1.»
IceCreamSundae s3(std::move(s1));
wcout << endl <<
L»Move assigning to s1 from s2.»
s1 = std::move(s2);
return 0;
}
|
Вывод
Теперь у вас должно быть базовое понимание конструкторов, деструкторов и операторов в C ++. В следующей статье мы обсудим RAII или Resource Acquisition Is Initialization.
Этот урок представляет собой главу из C ++ Succinctly , бесплатной книги от команды Syncfusion .