Глава №9. Итоговый тест

  Юрий  | 

  |

  Обновл. 19 Фев 2021  | 

 36029

 ǀ   28 

В этой главе мы рассмотрели перегрузку операторов, перегрузку операций преобразования типов данных, а также несколько тем, связанных с конструктором копирования. Пора закрепить полученные знания.

Теория

Перегрузка оператора — это специфическая перегрузка функции, которая позволяет использовать операторы с объектами пользовательских классов. При перегрузке операторов их функционал и назначение следует сохранять максимально приближенно к их первоначальному применению. Если суть применяемого оператора с объектами пользовательских классов интуитивно не понятна, то лучше использовать функцию с именем, вместо перегрузки оператора.

Операторы могут быть перегружены через обычные функции, через дружественные функции и через методы класса. Следующие правила помогут сориентироваться, какой способ перегрузки и когда следует использовать:

   Перегрузку операторов присваивания (=), индекса ([]), вызова функции (()) или выбора члена (->) выполняйте через методы класса.

   Перегрузку унарных операторов выполняйте через методы класса.

   Перегрузку бинарных операторов, которые изменяют свой левый операнд (например, оператор +=) выполняйте через методы класса.

   Перегрузку бинарных операторов, которые не изменяют свой левый операнд (например, оператор +) выполняйте через обычные или дружественные функции.

Перегрузка операций преобразования типов данных используется для явного или неявного преобразования объектов пользовательского класса в другой тип данных.

Конструктор копирования — это особый тип конструктора, используемый для инициализации объекта другим объектом того же класса. Конструкторы копирования используются в прямой/uniform-инициализации объектов объектами того же типа, копирующей инициализации (Fraction f = Fraction(7,4)) и при передаче или возврате параметров по значению.

Если вы не предоставите свой конструктор копирования, то компилятор автоматически его предоставит. Конструкторы копирования по умолчанию (предоставляемые компилятором) используют почленную инициализацию. Это означает, что каждый член объекта копии инициализируется соответствующим членом исходного объекта. Конструктор копирования может быть проигнорирован компилятором в целях оптимизации, даже если он имеет побочные эффекты, поэтому сильно не полагайтесь на свой конструктор копирования.

Конструкторы считаются конструкторами преобразования по умолчанию. Это означает, что компилятор будет использовать их для неявной конвертации объектов других типов данных в объекты вашего класса. Вы можете избежать этого, используя ключевое слово explicit. Вы также можете удалить функции внутри своего класса, включая конструктор копирования и перегруженный оператор присваивания, если это необходимо. И если позже в программе будет вызываться удаленная функция, то компилятор выдаст ошибку.

Оператор присваивания может быть перегружен для выполнения операций присваивания с объектами вашего класса. Если вы не предоставите перегруженный оператор присваивания сами, то компилятор создаст его за вас. Перегруженные операторы присваивания всегда должны иметь проверку на самоприсваивание.

По умолчанию оператор присваивания и конструктор копирования, предоставляемые компилятором, выполняют почленную инициализацию/присваивание, что является поверхностным копированием. Если в вашем классе есть динамически выделенные члены, то это, скорее всего, приведет к проблемам (несколько объектов могут указывать на одну и ту же выделенную память). В таком случае вам нужно будет явно определить свой конструктор копирования и перегрузку оператора присваивания для выполнения глубокого копирования.

Тест


Задание №1

Предположим, что Square — это класс, а square — это объект этого класса. Какой способ перегрузки лучше использовать для следующих операторов?

   square + square

   -square

   std::cout << square

   square = 7;

Ответ №1

   Перегрузку бинарного оператора + лучше всего выполнять через обычную/дружественную функцию.

   Перегрузку унарного оператора - лучше всего выполнять через метод класса.

   Перегрузка оператора << должна выполняться через обычную/дружественную функцию.

   Перегрузка оператора = должна выполняться через метод класса.

Задание №2

Напишите класс Average, который будет вычислять среднее значение всех передаваемых ему целых чисел. Используйте два члена: первый должен быть типа int32_t и использоваться для вычисления суммы всех передаваемых чисел, второй должен быть типа int8_t и использоваться для вычисления количества передаваемых чисел. Чтобы найти среднее значение, нужно разделить сумму на количество.

a) Следующий код функции main():

Должен выдавать следующий результат:

5
7
11
6
7
7

Ответ №2.а)

b) Требуется ли этому классу явный конструктор копирования или оператор присваивания?

Ответ №2.b)

Нет. Использование конструктора копирования и перегруженного оператора присваивания, предоставляемых компилятором по умолчанию, здесь будет достаточно.

Задание №3

Напишите свой собственный класс-массив целых чисел IntArray (не используйте std::array или std::vector). Пользователи должны передавать размер массива при создании объекта этого класса, а сам массив (переменная-член) должен выделяться динамически. Используйте стейтменты assert для проверки передаваемых значений, а также свой конструктор копирования и перегрузку оператора присваивания, если это необходимо, чтобы следующий код:

Выдавал следующий результат:

6 7 3 4 5 8
6 7 3 4 5 8

Ответ №3

Задание №4

Значение типа с плавающей точкой — это число с десятичной дробью, где количество цифр после точки (дробная часть) может меняться. Значение типа с фиксированной точкой — это число с дробью, где дробь (после точки) фиксированная.

Вам нужно написать класс для реализации значений типа с фиксированной точкой с двумя цифрами после точки (например, 11.47, 5.00 или 1465.78). Диапазон класса должен быть от -32768.99 до 32767.99, в дробной части могут быть любые две цифры, не допускайте проблем с точностью.

a) Какого типа данных переменную-член следует использовать для реализации значений типа с фиксированной точкой с 2-мя цифрами после точки? (Обязательно прочитайте ответ, прежде чем приступать к выполнению следующего задания)

Ответ №4.а)

Существует несколько способов реализации значений типа с фиксированной точкой. Поскольку это тот же тип с плавающей точкой (кроме того, что количество цифр после точки является фиксированным), то использование типа float или типа double может показаться очевидным решением. Но значения типа с плавающей точкой имеют проблемы с точностью. С фиксированной точкой мы можем перебрать все возможные числа, которые могут находиться в дробной части значения (в нашем случае, от .00 до .99), поэтому использование типа данных с ошибками в точности не является хорошим выбором.

Лучшее решение: Использовать тип int16_t signed для хранения целой части значения и int8_t signed для хранения дробной части значения.

b) Напишите класс FixedPoint, который реализует рекомендуемое решение из предыдущего задания. Если дробная или целая части значения являются отрицательными, то число должно рассматриваться, как отрицательное. Реализуйте перегрузку необходимых операторов и напишите необходимые конструкторы, чтобы следующий код функции main():

Выдавал следующий результат:

37.58
-3.09
-4.07
-5.07
-0.03
-0.03

Подсказка: Для вывода значения конвертируйте его в тип double, используя оператор static_cast.

Ответ №4.b)

c) Теперь добавьте конструктор, который будет принимать значение типа double. Вы можете округлить целую часть (слева от точки) с помощью функции round() (которая находится в заголовочном файле cmath).

Подсказки:

   Вы можете получить целую часть от числа типа double путем конвертации числа типа double в число типа int.

   Для перемещения одной цифры влево от точки используйте умножение на 10. Для перемещения двух цифр используйте умножение на 100.

Следующий код функции main():

Должен выдавать следующий результат:

0.03
-0.03
4.01
-4.01

Ответ №4.c)

d) Выполните перегрузку следующих операторов: ==, >>, (унарный) и + (бинарный).

Следующая программа:

Должна выдавать следующий результат:

true
true
true
true
true
true
true
true
-0.48
0.48
Enter a number: 5.678
You entered: 5.68

Подсказка: Для выполнения перегрузки оператора >> используйте конструктор с параметром типа double для создания анонимного объекта класса FixedPoint, а затем присвойте этот объект параметру функции перегрузки оператора >>.

Ответ №4.d)

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (112 оценок, среднее: 4,88 из 5)
Загрузка...

Комментариев: 28

  1. Surprizze:

    Интересный язык все же. Писал я 3 задание , дописал значит , смотрю вывод переменной IntArray b совсем не тот. там выводилось что-то вроде -8575575 (мусор). Целый час искал в чем проблема , посмотрел ответ: код одинаковый , все сделал в точности (название немного другие и проверка указателей была). Проверил один раз, второй , третий. Оказалось ошибка была в перегрузке оператора =.
    Опечатался в:

    (Точка с запятой после скобки)
    Казалось бы, такая мелочь , а так много времени на нее уходит:D

  2. Surprizze:

    Юрий , я написал "крестики-нолики", хотел бы критики в свой адрес.
    Код:

    1. Noir:

      ломается при при числах больше 10

  3. Vlad:

    Разве здесь не может возникнуть эллизия?

    Ведь мы получаем анонимный объект IntArray и тогда компилятор может проигнорировать конструктор копирования при копирующей инициализации объекта a. Тогда тот анонимный объект может быть неявно сконвертирован в int чтобы произошла инициализация через IntArray(int) в целях оптимизации. Чтобы такого не произошло мы должны добавить explicit перед конструктором IntArray(int) или я не прав?

    1. Vlad:

      Решил проверить и вот что получил у себя:

      0x7fff498bc850 constructed:
      0x7fff498bc850: 0 0 0 0 0 0 (size 6)
      0x7fff498bc850: 6 7 3 4 5 8

      Как видно, никакого конструктора копирования не вызывалось (даже если добавить explicit перед конструктором IntArray(int) будет тот же результат).
      Мой конструктор копирования и перегруженный оператор присваивания:

      Также обычный конструктор:

      1. Vlad:

        Деструктор тоже не был вызван.

  4. Илья:

    Не очень понял, зачем писать перегрузку оператора <<, если мы уже написали для класса перегрузку преобразования к типу double, и компилятор при выводе класса в cout автоматически использует эту перегрузку к типу double и выводит объект. Спасибо.

  5. Максим:

    Вот мое решение. Может кому интересно. Критика тоже принимается

    Операторы +, — работают раза в 2 быстрее

    1. Владимир:

      У тебя серьезнейшая ошибка что в операторе + что в -, называется переполнение.
      Приведу наглядный пример:
      У нас есть 2 числа fp1= 3.41 и fp2=1.52
      3.41-1.52 = 1.89
      А что будет у тебя?
      fp1.m_base — fp2.m_base 3-1 = 2
      fp1.m_dec — fp2.m_dec 41 — 52 = -8
      И потом ты создаешь объект FixedPoint(2, -8) и возвращаешь его.
      Должно быть 1.52 а у тебя получилось -2.08

      1. Дмитрий:

        А если так:

  6. Константин:

    Сконструировал свой алгоритм для перегрузки оператора +. Громоздко получилось, не спорю. Но работает же!

  7. Александр:

    Долго занимался тестом №4. Файл cmath, как и math.h не содержит round(), причем как в VS2008, так и в VS2011. В интернете встретил подобные жалобы и советы применить modf(). Но эта функция просто отделяет дробную и целую части, а не производит арифметическое округление. А по строке 'Enter a number: 5.678','You entered: 5.68' нужно именно округлять (0.004=0.00, 0.005=0.010). Поэтому сам написал округление в конструкторе FixedPoint(double number). Попутно выяснилось, что надо контролировать диапазон вводимых чисел и переполнение дробной части на краях диапазона. А так же вводимые числа в FixedPoint(short base=0,char decimal=0), при вводе base=32768 можем получить -32767. И если при вводе с клавиатуры ввести ',' вместо '.', программа циклится. И еще. В операторе operator<< надо повысить разрядность вывода <<setprecision(8), иначе cout сам начинает округлять до 6 цифр. В переменных класса 10000 и 99, а вывод 10001.

    —————————————————-

    Вывод такой:
    Enter a number: 0.004
    You entered: 0
    m_base=0 m_decimal=0
    -
    Enter a number: 0.005
    You entered: 0.01
    m_base=0 m_decimal=1
    -
    Enter a number: 4.004
    You entered: 4
    m_base=4 m_decimal=0
    -Enter a number: 4.01
    You entered: 4.01
    m_base=4 m_decimal=1
    -
    Enter a number: 4.015
    You entered: 4.02
    m_base=4 m_decimal=2
    -
    Enter a number: 9.995
    You entered: 10
    m_base=10 m_decimal=0
    -
    Enter a number: 9.99
    You entered: 9.99
    m_base=9 m_decimal=99
    -
    Enter a number: 32767.99
    You entered: 32767.99
    m_base=32767 m_decimal=99
    -
    Enter a number: 32767.995
    Assertion failed: (m_base > -32768)&&"Overload 32767.99"
    (переполнение из-за переноса округления)
    -
    Enter a number: 32768
    Assertion failed: number<32768
    (переполнение из-за слишком большой целой части)

    ———————————————————
    С отрицательными числами так же все правильно до -32768,99

  8. Анастасия:

    По 4 заданию у меня было много вопросов. Сложнее всего далась последняя часть 4го задания, компилятор долго ругался. Посмотрела ответ, но это не сильно помогло, т.к. я изначально делала по-другому. В итоге я на свежую голову пересмотрела и исправила почти весь код и тогда почти сразу заработало. Я не буду задавать вопросы по коду из ответа, но оттуда я почти списала перегрузку ввода и сложения. Вот мой код, сейчас всё работает:

    Пожалуй, по моему коду вопрос остался только один: почему для функции перегрузки сложения двух объектов обязательно friend? Ведь сама функция прописана в классе, а методы класса имеют доступ ко всем его членам. Если убрать friend, всё ломается.

  9. Игорь:

    Не пойму зачем в 3 задании реализовывать конструктор копирования:

    ведь всё работает без него?

    1. Анастасия:

      Без него не пробовала, но т.к. есть динамическая память, по логике предыдущего урока о глубоком копировании, надо именно с ним. Иначе скопируется тот же адрес и после работы деструктора скопированный адрес станет не алё.

  10. kmish:

    Не совсем понимаю зачем перегружать —(унарный) в 4м задании, если конструктор

    может принимать отрицательные значения double. Какой смысл тогда в перегрузке -?

    Сделал без нее, результат аналогичный:

    1. Анастасия:

      Я понимаю, почему надо перегружать унарный минус, но не понимаю, почему у Вас работает без него))
      Вот мои мысли: Чтобы работала эта строчка:

      где а — это объект класса. У нас есть оператор вывода для строк и чисел (стандартный я имею в виду) и перегруженный для объектов класса, но что такое "минус объект"? Этого компилятор без перегрузки оператора "минус" для класса не знает. По идее при Вашем коде он должен как минимум ругнуться на какое-нибудь неявное преобразование в этом месте. Почему он этого не делает — для меня вопрос…

      1. Аноним:

        Компилятор не знает как работать с пользовательским типом в случае с унарными минусом в его примере, но знает, как работать с типом double и еще он знает, как сделать из пользовательского типа тип double. на выходе получаем такую логику работы: (и это мое личное предположение, а не точная информация)

        1)преобразуем в double
        2)проводим операцию
        3)конвертируем обратно

        Возможно более ранние компиляторы такого не умели делать, в нынешнем 2020 мне и перегружать == не надо, он преобразует и сравнивает double.

  11. subj:

    не понял как работает round() в задании 4c, но если его убрать в версии кода из ответов, то выводит 4 вместо 4.01.
    При решении сделал немного по своему конструктор double, который выдавал правильное число:

    1. Анастасия:

      round просто отбрасывает дробную часть. Я тоже сделала по-другому.

  12. Андрей:

    Вы можете что-то сказать о графике С++. Какой заголовок подключить, как реализовать работу и тп?

    1. Фото аватара Юрий:

      Сейчас, к сожалению, уроков на эту тему не предвидится.

  13. Torgu:

    Правильно ли я понял, что эти условия нужны, чтобы если целая или дробная часть отрицательна (только одна из них, не две!), то при выводе в консоль не происходило вычитание между целой и дробной частью, а только сложение?

    1. Анастасия:

      Ваш вопрос не понятен. Если ещё актуально, сформулируйте по-другому как-то.

    2. Григорий:

      Да, все верно.
      p.s. просто сам только что в этом разобрался)

    3. Максим:

      Да, все верно.
      Конструкторы делают так, чтобы обе части нашего FixedPoint (m_base и m_decimal) всегда были одного знака.
      Данный способ позволяет корректно переводить FixedPoint в double:

  14. Shom:

    Сколько же я разных чувств испытал пока решал всё это ))

    1. Фото аватара Юрий:

      Ох понимаю 🙂

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *