13.x – Резюме к главе 13 и небольшой тест
В данной главе мы исследовали темы, связанные с перегрузкой операторов, а также перегруженные преобразования типов и темы, связанные с конструктором копирования.
Краткое резюме
Перегрузка операторов – это вариант перегрузки функций, который позволяет вам перегружать операторы для ваших классов. Когда операторы перегружены, назначение этих операторов должно быть как можно ближе к их исходному назначению. Если значение оператора при применении к пользовательскому классу интуитивно непонятно, используйте вместо этого именованную функцию.
Операторы могут быть перегружены как обычная функция, дружественная функция или функция-член. Следующие практические правила помогут вам определить, какая форма лучше всего подходит для какой-либо конкретной ситуации:
- если вы перегружаете присваивание (
=
), индекс ([]
), вызов функции (()
) или выбор члена (->
), делайте это как функцию-член; - если вы перегружаете унарный оператор, делайте это как функцию-член;
- если вы перегружаете бинарный оператор, изменяющий его левый операнд (например,
operator+=
), делайте это как функцию-член, если можете; - если вы перегружаете бинарный оператор, который не изменяет свой левый операнд (например,
operator+
), делайте это как обычную функцию или дружественную функцию.
Приведения типов можно перегружать для предоставления функций преобразования, которые можно использовать для явного или неявного преобразования вашего класса в другой тип.
Конструктор копирования – это особый тип конструктора, используемый для инициализации объекта из другого объекта того же типа. Конструкторы копирования используются для прямой/унифицированной инициализации из объекта того же типа, копирующей инициализации (Fraction f = Fraction(5,3)
) и при передаче или возврате параметра по значению.
Если вы не предоставите конструктор копирования, компилятор создаст его за вас. Предоставляемые компилятором конструкторы копирования будут использовать поэлементную инициализацию, то есть каждый член копии инициализируется из исходного члена. Конструктор копирования может быть опущен в целях оптимизации, даже если он имеет побочные эффекты, поэтому не полагайтесь на выполнение конструктора копирования.
Конструкторы по умолчанию считаются конструкторами преобразования, что означает, что компилятор будет использовать их для неявного преобразования объектов других типов в объекты вашего класса. Вы можете избежать этого, используя перед конструктором ключевое слово explicit
. Вы также можете удалить функции в своем классе, включая конструктор копирования и перегруженный оператор присваивания, если это необходимо. Если удаленная функция будет вызвана, это вызовет ошибку компилятора.
Оператор присваивания может быть перегружен, чтобы разрешить вашему классу присваивание. Если вы не предоставите перегруженный оператор присваивания, компилятор создаст его за вас. Перегруженные операторы присваивания всегда должны включать проверку на самоприсваивание.
Начинающие программисты часто путаются, когда используются оператор присваивания и конструктор копирования, но это довольно просто:
- если новый объект должен быть создан до того, как может произойти копирование, используется конструктор копирования (примечание: это включает передачу или возврат объектов по значению);
- если новый объект не нужно создавать до того, как может произойти копирование, используется оператор присваивания.
Конструктор копирования и операторы присваивания, предоставляемые компилятором по умолчанию, выполняют поэлементную инициализацию или присваивание, что является поверхностным копированием. Если ваш класс динамически выделяет память, это, вероятно, приведет к проблемам, поскольку несколько объектов в конечном итоге будут указывать на одну и ту же выделенную память. В этом случае вам нужно будет явно определить их, чтобы выполнить глубокое копирование. Более того, по возможности избегайте собственного управления памятью и используйте классы из стандартной библиотеки.
Небольшой тест
Вопрос 1
Предполагая, что Point
является классом, а точка point
является экземпляром этого класса, в какой форме следует использовать перегрузку для следующих операторов (обычная/дружественная функция или функция-член)?
point + point
-point
std::cout << point
point = 5
Ответ
- бинарный оператор
+
лучше всего реализовать как обычную/дружественную функцию;- унарный оператор
-
лучше всего реализовать как функцию-член;- оператор
<<
должен быть реализован как обычная/дружественная функция;- оператор
=
должен быть реализован как функция-член.
Вопрос 2
Напишите класс с именем Average
, который будет отслеживать среднее значение всех переданных ему целых чисел. Используйте два члена: первый должен иметь тип std::int_least32_t
и использоваться для отслеживания суммы всех чисел, которые вы видели на данный момент. Второй должен иметь тип std::int_least8_t
и использоваться для отслеживания того, сколько чисел вы уже видели. Вы можете разделить их, чтобы найти среднее значение.
2a) Напишите все функции, необходимые для запуска следующей программы:
int main()
{
Average avg{};
avg += 4;
std::cout << avg << '\n'; // 4 / 1 = 4
avg += 8;
std::cout << avg << '\n'; // (4 + 8) / 2 = 6
avg += 24;
std::cout << avg << '\n'; // (4 + 8 + 24) / 3 = 12
avg += -10;
std::cout << avg << '\n'; // (4 + 8 + 24 - 10) / 4 = 6.5
(avg += 6) += 10; // два вызова объединены в цепочку
std::cout << avg << '\n'; // (4 + 8 + 24 - 10 + 6 + 10) / 6 = 7
Average copy{ avg };
std::cout << copy << '\n';
return 0;
}
и выдается следующий результат:
4
6
12
6.5
7
7
Подсказка: помните, что 8-битные целые числа обычно являются символами char
, поэтому std::cout
обрабатывает их соответственно.
Ответ
#include <iostream> #include <cstdint> // для целочисленных типов фиксированной ширины class Average { private: // сумма всех чисел, которые мы видели на данный момент std::int_least32_t m_total{ 0 }; // количество чисел, которые мы видели на данный момент std::int_least8_t m_numbers{ 0 }; public: Average() { } friend std::ostream& operator<<(std::ostream &out, const Average &average) { // Наше среднее значение - это сумма полученных нами чисел, // разделенная на их количество // Здесь нужно деление с плавающей запятой, а не целочисленное out << static_cast<double>(average.m_total) / average.m_numbers; return out; } // Поскольку operator+= изменяет свой левый операнд, мы запишем его как член Average& operator+=(int num) { // Увеличиваем нашу сумму на новое число m_total += num; // И увеличиваем счетчик на 1 ++m_numbers; // возвращаем *this на случай, // если кто-то захочет объединить += в цепочку return *this; } }; int main() { Average avg{}; avg += 4; std::cout << avg << '\n'; avg += 8; std::cout << avg << '\n'; avg += 24; std::cout << avg << '\n'; avg += -10; std::cout << avg << '\n'; (avg += 6) += 10; // два вызова объединены в цепочку std::cout << avg << '\n'; Average copy{ avg }; std::cout << copy << '\n'; return 0; }
2b) Нужен ли этому классу явные конструктор копирования и оператор присваивания?
Ответ
Нет. Поскольку здесь можно использовать поэлементные инициализацию/копирование, допустимо использование реализаций, предоставленных компилятором по умолчанию.
Вопрос 3
Напишите свой собственный класс целочисленного массива с именем IntArray
с нуля (не используйте std::array
или std::vector
). Пользователи должны передавать размер массива при его создании, и массив должен размещаться динамически. Используйте инструкции assert
для защиты от недопустимых данных. Создайте любые конструкторы и перегруженные операторы, необходимые для правильной работы следующей программы:
#include <iostream>
IntArray fillArray()
{
IntArray a(5);
a[0] = 5;
a[1] = 8;
a[2] = 2;
a[3] = 3;
a[4] = 6;
return a;
}
int main()
{
IntArray a{ fillArray() };
std::cout << a << '\n';
auto &ref{ a }; // используем эту ссылку, чтобы избежать
// ошибок компиляции, связанных с самоприсваиванием
a = ref;
IntArray b(1);
b = a;
std::cout << b << '\n';
return 0;
}
Эта программа должна напечатать:
5 8 2 3 6
5 8 2 3 6
Ответ
#include <iostream> #include <cassert> // для assert class IntArray { private: int m_length{ 0 }; int *m_array{ nullptr }; public: IntArray(int length): m_length{ length } { assert(length > 0 && "IntArray length should be a positive integer"); m_array = new int[m_length]{}; } // Конструктор копирования, использующий глубокое копирование IntArray(const IntArray &array): m_length{ array.m_length } { // размещаем новый массив m_array = new int[m_length]; // копируем элементы из исходного массива в новый for (int count{ 0 }; count < array.m_length; ++count) m_array[count] = array.m_array[count]; } ~IntArray() { delete[] m_array; } // Если вы получаете здесь непонятные значения, вы, вероятно, забыли // выполнить глубокое копирование в своем конструкторе копирования friend std::ostream& operator<<(std::ostream &out, const IntArray &array) { for (int count{ 0 }; count < array.m_length; ++count) { out << array.m_array[count] << ' '; } return out; } int& operator[] (const int index) { assert(index >= 0); assert(index < m_length); return m_array[index]; } // Оператор присваивания, использующий глубокое копирование IntArray& operator= (const IntArray &array) { // защита от самоприсваивания if (this == &array) return *this; // если этот массив уже существует, удалить его, // чтобы не было утечек памяти delete[] m_array; m_length = array.m_length; // размещаем новый массив m_array = new int[m_length]; // копируем элементы из исходного массива в новый for (int count{ 0 }; count < array.m_length; ++count) m_array[count] = array.m_array[count]; return *this; } }; IntArray fillArray() { IntArray a(5); a[0] = 5; a[1] = 8; a[2] = 2; a[3] = 3; a[4] = 6; return a; } int main() { IntArray a{ fillArray() }; // Если вы получаете здесь непонятные значения, вы, вероятно, забыли // выполнить глубокое копирование в своем конструкторе копирования std::cout << a << '\n'; auto &ref{ a }; // используем эту ссылку, чтобы избежать // ошибок компиляции, связанных с самоприсваиванием a = ref; IntArray b(1); b = a; // Если вы получаете здесь непонятные значения, вы, вероятно, забыли // выполнить глубокое копирование в своем операторе присваивания std::cout << b << '\n'; return 0; }
Вопрос 4
Дополнительный вопрос: немного сложнее. Число с плавающей запятой – это число с дробной частью, где количество цифр после запятой может быть переменным. Число с фиксированной запятой – это число с дробной частью, где количество цифр в дробной части фиксировано.
В этом тесте мы собираемся написать класс для реализации числа с фиксированной запятой с двумя цифрами в дробной части (например, 12.34, 3.00 или 1278.99). Предположим, что диапазон значений класса должен быть от -32768.99 до 32767.99, что дробная часть должен содержать любые две цифры, что нам не нужны ошибки точности, и что мы хотим сэкономить место.
4a) Какой тип переменных-членов, по вашему мнению, мы должны использовать для реализации нашего числа с фиксированной запятой с двумя цифрами после запятой? (Убедитесь, что вы прочитали ответ, прежде чем переходить к следующим вопросам)
Ответ
Есть много разных способов реализовать число с фиксированной запятой. Поскольку число с фиксированной запятой, по сути, представляет собой частный случай числа с плавающей запятой (где количество цифр после запятой является фиксированным, а не переменным), очевидным выбором может показаться использование числа с плавающей запятой. Но у чисел с плавающей запятой есть проблемы с точностью. Имея фиксированное количество цифр после запятой, мы можем перечислить все возможные значения дробной части (в нашем случае от 0.00 до 0.99), поэтому использование типа данных, имеющего проблемы с точностью, – не лучший выбор.
Лучшим решением было бы использовать 16-битное целое число со знаком для хранения целой части числа и 8-битное целое число со знаком для хранения дробной части.
4b) Напишите класс с именем FixedPoint2
, реализующий рекомендованное решение из предыдущего вопроса. Если одна (или обе) из частей числа (целая или дробная) отрицательна, число следует рассматривать как отрицательное. Предоставьте перегруженные операторы и конструкторы, необходимые для запуска следующей программы:
int main()
{
FixedPoint2 a{ 34, 56 };
std::cout << a << '\n';
FixedPoint2 b{ -2, 8 };
std::cout << b << '\n';
FixedPoint2 c{ 2, -8 };
std::cout << c << '\n';
FixedPoint2 d{ -2, -8 };
std::cout << d << '\n';
FixedPoint2 e{ 0, -5 };
std::cout << e << '\n';
std::cout << static_cast<double>(e) << '\n';
return 0;
}
Эта программа должна дать следующий результат:
34.56
-2.08
-2.08
-2.08
-0.05
-0.05
Подсказка: хотя поначалу может показаться, что для этого необходимо больше работы, но полезно хранить как целую, так и дробную части числа с одним и тем же знаком (например, обе положительные, если число положительное, и обе отрицательные, если число отрицательное). Это значительно упрощает математику в дальнейшем.
Подсказка: чтобы вывести число, сначала преобразуйте его в double
.
Ответ
#include <iostream> #include <cstdint> // для целочисленных типов фиксированной ширины class FixedPoint2 { private: std::int_least16_t m_base{}; // здесь наша целая часть std::int_least8_t m_decimal{}; // здесь наша дробная часть public: FixedPoint2(std::int_least16_t base = 0, std::int_least8_t decimal = 0) : m_base{ base }, m_decimal{ decimal } { // Здесь мы должны обработать случай, когда дробная часть >99 или <-99, // но оставим это читателю в качестве упражнения // Если целая или дробная части отрицательные if (m_base < 0 || m_decimal < 0) { // Убеждаемся, что целая часть отрицательная if (m_base > 0) m_base = -m_base; // Убеждаемся, что дробная часть отрицательная if (m_decimal > 0) m_decimal = -m_decimal; } } operator double() const { return m_base + static_cast<double>(m_decimal) / 100.0; } }; // Эта функция не требует доступа к внутренним компонентам класса, // поэтому ее можно определить вне класса std::ostream& operator<<(std::ostream &out, const FixedPoint2 &fp) { out << static_cast<double>(fp); return out; } int main() { FixedPoint2 a{ 34, 56 }; std::cout << a << '\n'; FixedPoint2 b{ -2, 8 }; std::cout << b << '\n'; FixedPoint2 c{ 2, -8 }; std::cout << c << '\n'; FixedPoint2 d{ -2, -8 }; std::cout << d << '\n'; FixedPoint2 e{ 0, -5 }; std::cout << e << '\n'; std::cout << static_cast<double>(e) << '\n'; return 0; }
4c) Теперь добавьте конструктор, который принимает double
. Должна запуститься следующая программа:
int main()
{
// Обработка случаев, когда аргумент представлен напрямую
FixedPoint2 a{ 0.01 };
std::cout << a << '\n';
FixedPoint2 b{ -0.01 };
std::cout << b << '\n';
// Обработка случаев, когда в аргументе есть ошибка округления
FixedPoint2 c{ 5.01 }; // хранится как 5.0099999 ... поэтому нам нужно округлить его
std::cout << c << '\n';
FixedPoint2 d{ -5.01 }; // хранится как -5.0099999 ... поэтому нам нужно округлить его
std::cout << d << '\n';
// Обработка случая, когда дробная часть аргумента округляется до 100
// (необходимо увеличить целую часть на 1)
FixedPoint2 e{ 106.9978 }; // должно сохраниться с целой частью 107 и дробной частью 0
std::cout << e << '\n';
return 0;
}
Эта программа должна дать следующий результат
0.01
-0.01
5.01
-5.01
107
Рекомендация: это будет немного сложно. Разбейте выполнение на три этапа. Во-первых, решите случаи, когда параметр double
может быть представлен напрямую (случаи a
и b
выше). Затем обновите свой код для обработки случаев, когда параметр double
имеет ошибку округления (случаи c
и d
). Наконец, обработайте последний случай, когда дробная часть округляется до 100 (случай e
).
Для всех случаев: Подсказка
Вы можете переместить цифру справа от десятичной запятой влево от запятой, умножив ее на 10. Умножьте на 100, чтобы переместить две позиции.
Для случаев a
и b
: Подсказка
Вы можете получить целую часть числа
double
с помощью статического преобразованияdouble
кint
. Чтобы получить дробную часть, вы можете вычесть целую часть.
Для случаев c
и d
: Подсказка
Вы можете округлить число (слева от десятичной запятой) с помощью функции
std::round()
(включена в заголовокcmath
).
Ответ
#include <iostream> #include <cstdint> // для целочисленных типов фиксированной ширины #include <cmath> // для std::round() class FixedPoint2 { private: std::int_least16_t m_base{}; // здесь наша целая часть std::int_least8_t m_decimal{}; // здесь наша дробная часть public: FixedPoint2(std::int_least16_t base = 0, std::int_least8_t decimal = 0) : m_base{ base }, m_decimal{ decimal } { // Здесь мы должны обработать случай, когда дробная часть >99 или <-99, // но оставим это читателю в качестве упражнения // Если целая или дробная части отрицательные if (m_base < 0 || m_decimal < 0) { // Убеждаемся, что целая часть отрицательная if (m_base > 0) m_base = -m_base; // Убеждаемся, что дробная часть отрицательная if (m_decimal > 0) m_decimal = -m_decimal; } } FixedPoint2(double d): m_base{ static_cast<std::int_least16_t>(std::round(d * 100) / 100) }, m_decimal{ static_cast<std::int_least8_t>(std::round(d * 100) - m_base * 100) } { } operator double() const { return m_base + static_cast<double>(m_decimal) / 100.0; } }; // Эта функция не требует доступа к внутренним компонентам класса, // поэтому ее можно определить вне класса std::ostream& operator<<(std::ostream &out, const FixedPoint2 &fp) { out << static_cast<double>(fp); return out; } int main() { FixedPoint2 a{ 0.01 }; std::cout << a << '\n'; FixedPoint2 b{ -0.01 }; std::cout << b << '\n'; FixedPoint2 c{ 5.01 }; // хранится как 5.0099999 ... поэтому нам нужно округлить его std::cout << c << '\n'; FixedPoint2 d{ -5.01 }; // хранится как -5.0099999 ... поэтому нам нужно округлить его std::cout << d << '\n'; return 0; }
4d) Перегрузите operator==
, operator>>
, operator-
(унарный) и operator+
(бинарный).
Должна запуститься следующая программа:
void testAddition()
{
std::cout << std::boolalpha;
// оба положительные, без переполнения дробной части
std::cout << (FixedPoint2{ 0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 1.98 }) << '\n';
// оба положительные, с переполнением дробной части
std::cout << (FixedPoint2{ 0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 2.25 }) << '\n';
// оба отрицательные, без переполнения дробной части
std::cout << (FixedPoint2{ -0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -1.98 }) << '\n';
// оба отрицательные, с переполнением дробной части
std::cout << (FixedPoint2{ -0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -2.25 }) << '\n';
// второе отрицательное, без переполнения дробной части
std::cout << (FixedPoint2{ 0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -0.48 }) << '\n';
// второе отрицательное, возможно переполнение дробной части
std::cout << (FixedPoint2{ 0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -0.75 }) << '\n';
// первое отрицательное, без переполнения дробной части
std::cout << (FixedPoint2{ -0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 0.48 }) << '\n';
// первое отрицательное, возможно переполнение дробной части
std::cout << (FixedPoint2{ -0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 0.75 }) << '\n';
}
int main()
{
testAddition();
FixedPoint2 a{ -0.48 };
std::cout << a << '\n';
std::cout << -a << '\n';
std::cout << "Enter a number: "; // введите 5.678
std::cin >> a;
std::cout << "You entered: " << a << '\n';
return 0;
}
И должен выдаваться следующий вывод:
true
true
true
true
true
true
true
true
-0.48
0.48
Enter a number: 5.678
You entered: 5.68
Подсказка: сложите два числа FixedPoint2
, используя приведение к double
, а затем преобразуйте результаты обратно в FixedPoint2
.
Подсказка: для operator>>
используйте конструктор double
для создания анонимного объекта типа FixedPoint2
и присвойте его параметру функции FixedPoint2
.
Ответ
#include <iostream> #include <cstdint> // для целочисленных типов фиксированной ширины #include <cmath> // для std::round() class FixedPoint2 { private: std::int_least16_t m_base{}; // здесь наша целая часть std::int_least8_t m_decimal{}; // здесь наша дробная часть public: FixedPoint2(std::int_least16_t base = 0, std::int_least8_t decimal = 0) : m_base{ base }, m_decimal{ decimal } { // Здесь мы должны обработать случай, когда дробная часть >99 или <-99, // но оставим это читателю в качестве упражнения // Если целая или дробная части отрицательные if (m_base < 0 || m_decimal < 0) { // Убеждаемся, что целая часть отрицательная if (m_base > 0) m_base = -m_base; // Убеждаемся, что дробная часть отрицательная if (m_decimal > 0) m_decimal = -m_decimal; } } FixedPoint2(double d): m_base{ static_cast<std::int_least16_t>(std::round(d * 100) / 100) }, m_decimal{ static_cast<std::int_least8_t>(std::round(d * 100) - m_base * 100) } { } operator double() const { return m_base + static_cast<double>(m_decimal) / 100; } friend bool operator==(const FixedPoint2 &fp1, const FixedPoint2 &fp2) { return (fp1.m_base == fp2.m_base && fp1.m_decimal == fp2.m_decimal); } FixedPoint2 operator-() const { // Нам нужно приведение, потому что отрицательный знак (-) // преобразует наши узкие целочисленные типы в int. return { static_cast<std::int_least16_t>(-m_base), static_cast<std::int_least8_t>(-m_decimal) }; } }; // Эта функция не требует доступа к внутренним компонентам класса, // поэтому ее можно определить вне класса std::ostream& operator<<(std::ostream &out, const FixedPoint2 &fp) { out << static_cast<double>(fp); return out; } std::istream& operator >> (std::istream &in, FixedPoint2 &fp) { double d{}; in >> d; fp = FixedPoint2{ d }; return in; } FixedPoint2 operator+(const FixedPoint2 &fp1, const FixedPoint2 &fp2) { return { static_cast<double>(fp1) + static_cast<double>(fp2) }; } void testAddition() { std::cout << std::boolalpha; // оба положительные, без переполнения дробной части std::cout << (FixedPoint2{ 0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 1.98 }) << '\n'; // оба положительные, с переполнением дробной части std::cout << (FixedPoint2{ 0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 2.25 }) << '\n'; // оба отрицательные, без переполнения дробной части std::cout << (FixedPoint2{ -0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -1.98 }) << '\n'; // оба отрицательные, с переполнением дробной части std::cout << (FixedPoint2{ -0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -2.25 }) << '\n'; // второе отрицательное, без переполнения дробной части std::cout << (FixedPoint2{ 0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -0.48 }) << '\n'; // второе отрицательное, возможно переполнение дробной части std::cout << (FixedPoint2{ 0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -0.75 }) << '\n'; // первое отрицательное, без переполнения дробной части std::cout << (FixedPoint2{ -0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 0.48 }) << '\n'; // первое отрицательное, возможно переполнение дробной части std::cout << (FixedPoint2{ -0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 0.75 }) << '\n'; } int main() { testAddition(); FixedPoint2 a{ -0.48 }; std::cout << a << '\n'; std::cout << -a << '\n'; std::cout << "Enter a number: "; // введите 5.678 std::cin >> a; std::cout << "You entered: " << a << '\n'; return 0; }