Перегрузка операторов C++, часть четвёртая - преобразования типов
newbie
Опубликован: | 2020-03-04T05:35:30.003182Z |
Отредактирован: | 2020-03-04T05:32:33.573126Z |
Продолжаем изучать C++ и его возможности. В очередной демонстрации рассмотрим преобразования типов и сосредоточим внимание на соответствующих инструментах C++ - конструкторах классов и служебных функциях operator, позволяющих реализовать простые механизмы преобразования типов в коде. Все инструменты я продемонстрирую на простых учебных примерах из книги Роберта Лафоре.
1. Общие сведения
Как известно, C++ имеет строгую статическую типизацию, при этом он наследует базовые типы языка C (int, float, double etc.) и даёт возможность разрабатывать собственные пользовательские типы данных посредством декларации и определения классов.
В практическом программировании на С++ достаточно часто приходится иметь дело с выражениями, в которых используются переменные разных типов.
float a, b; int c = 4; a = 1.25F; b = a * c
В выражении b = a * c
переменные a
и b
имеют тип float
, а переменная c
имеет тип int
и при компиляции будет автоматически преобразована в тип float
. В данном случае имеет место неявное преобразование типов, и то, каким образом такое преобразование будет осуществляться, определено в коде компилятора. Не всегда ожидания программиста совпадают с реальными действиями компилятора, поэтому предусмотрено явное преобразование типов, которое кроме всего прочего помогает избежать некоторых каверзных ситуаций.
b = a * static_cast<float>(c);
И явное и неявное преобразования базовых типов между собой определены в коде компилятора. Что делать, когда мы имеем дело с переменными не базовых типов, как определить механизмы преобразования в этом случае? Об этом и поговорим далее...
Все примеры этого обзора я продемонстрирую с помощью своего десктопа с Debian buster на борту и свободного компилятора g++.
2. Преобразования объектов и базовых типов
В рамках этой демонстрации объектами будем считать экземпляры разработанных в конкретной программе классов. Преобразование объектов в базовый тип и базовых типов в объекты рассмотрим на примере кода элементарной программы-конвертера, которая преобразует метры в футы и дюймы и обратно, футы и дюймы в метры. Создаю новый файл.
Для реализации задуманного мне понадобится один заголовочных файл из стандартной библиотеки.
// mconv.cpp #include <iostream>
Декларирую класс Distance
, который будет определять объекты пользовательского типа.
class Distance { private: const float MTF; int feet; float inches; public: // Конструктор без аргументов определяет состояние вновь созданных // объявлением без инициализации объектов и их свойств Distance() : MTF(3.280833F), feet(0), inches(0.0F) {} // Конструктор с одним аргументом определяет // явное и неявное преобразование переменной базового типа float // в экземпляр класса Distance Distance(float meters) : MTF(3.280833F) { float fltfeet = MTF * meters; feet = static_cast<int>(fltfeet); inches = 12.0F * (fltfeet - static_cast<float>(feet)); } // Конструктор с двумя аргументами определяет состояние // вновь созданных объектов объявлением с инициализацией Distance(int f, float i) : MTF(3.280833F), feet(f), inches(i) {} // Функция getdist() даёт возможность инициировать // созданный объект с запросом данных у пользователя программы, // в данной реализации программы использоваться не будет void getdist() { std::cout << "Enter feet: "; std::cin >> feet; std::cout << "Enter inches: "; std::cin >> inches; } // Функция showdist() выведет на экран значение объекта в // заданной форме void showdist() const { std::cout << feet << "\'-" << inches << "\"\n"; } // Функция operator float() определяет преобразование экземпляра // класса Distance в переменную базового типа float operator float() const { float fracfeet = inches / 12.0F; fracfeet += static_cast<float>(feet); return fracfeet / MTF; } };
В коде класса Distance
предусмотрено два инструмента для преобразования типов:
- Конструктор с одним аргументом определяет преобразование переменной базового типа float в экземпляр класса
Distance
; - Функция
operator float()
определяет преобразование экземпляра классаDistance
в переменную базового типа float.
Давайте посмотрим, как это работает, определяю функцию main
программы.
int main() { float mtrs; // Тестируем конструктор класса Distance // с одним аргументом, создаём экземпляр dist1 // и преобразуем при его создании // метры (2.35) в футы и дюймы - неявное преобразование Distance dist1 = 2.35F; // Выводим на экран значение dist1 в заданной форме // в футах и дюймах std::cout << "dist1 = "; dist1.showdist(); // Преобразуем значения переменной dist1 // в переменную базового типа float с помощью соответствующей // функции operator float() - явное преобразование mtrs = static_cast<float>(dist1); // Выводим на экран значение переменной dist1 // в метрах, ожидаем увидеть 2.35 метра std::cout << "dist1 = " << mtrs << " meters\n"; // Тестируем конструктор класса Distance с двумя аргументами, // создаём экземпляр класса Distance Distance dist2(7, 8.51949F); // Преобразуем значения этого экземпляра в // переменную базового типа float - неявное преобразование // опять работает функция operator float() mtrs = dist2; // Выводим на экран значение переменной dist1 // в метрах, ожидаем 2.35 метра std::cout << "dist2 = " << mtrs << " meters\n"; }
Как видно, механизм преобразования достаточно незамысловат. Сохраняю изменения в файл и пробую этот файл скомпилировать и исполнить полученную программу.
g++ -o exe -Wall mconv.cpp ./exe
На снимке экрана выше видно, что компиляция завершилась без ошибок и предупреждений, все выведенные программой тестовые значения соответствуют ожиданиям, а это значит, что преобразование значений экземпляра класса Distance
в значение базового типа float великолепно работает, и также великолепно работает обратное преобразование переменной базового типа float в значения экземпляра класса Distance
.
3. Преобразования объектов и строк
Рассмотрим второй пример преобразования - объект в строку и обратно, как нельзя кстати пригодится рассмотренный в одном из предыдущих выпусков блога класс String
. Создаю новый файл с именем sconv.cpp
.
Для этого примера мне потребуется пара заголовочных файлов стандартной библиотеки.
// sconv.cpp #include <iostream> #include <cstring>
Декларирую класс String
.
class String { private: static const int SZ = 80; char str[SZ]; public: // Конструктор без аргументов определяет состояние // объявленных без инициализации экземпляров этого класса String() { str[0] = '\n'; } // Первый конструктор с одним аргументом принимает // строковый массив и копирует его в соответствующее // свойство экземпляра - преобразование строкового массива в объект String(char s[]) { strcpy(str, s); } // Второй конструктор с одним аргументом принимает // строковую константу и копирует её в соответствующее // свойство экземпляра - преобразование строковой константы в объект String(const char * s) { strcpy(str, s); } // Функция display() выводит на терминал // значение экземпляра в заданной форме void display() const { std::cout << str; } // Функция operator char*() определяет // механизм преобразование экземпляра в строку operator char*() { return str; } };
Класс String
имеет два конструктора с одним аргументом. В первом случае экземпляр класса копирует строковый массив, а во втором случае - строковую константу. Оба конструктора осуществляют преобразование базового типа в экземпляр. Кроме этого класс String
имеет функцию operator char*()
, с помощью которой можно выполнить обратное преобразование. Протестируем функционал класса String
с помощью функции main
.
int main() { // Объявляем экземпляр класса String s1; // Объявляем и инициализируем строковый массив char xstr[] = "Lorem Ipsum Dolor! "; // Копируем данные строкового массива в экземпляр // класса String - работает первый конструктор с одним аргументом s1 = xstr; // Выводим значение экземпляра s1 на экран терминала s1.display(); // Объявляем второй экземпляр класса String // и инициализируем его с помощью строковой константы // работает второй конструктор с одним аргументом String s2 = "Proin Consectetur Mauris."; // Преобразуем экземпляр s2 в строку // и выводим её на экран терминала - работает функция operator char*() std::cout << static_cast<char*>(s2) << std::endl; return 0; }
Компилирую программу и пробую её исполнить.
Компиляция программы завершилась без ошибок и предупреждений, выхлоп на терминале во время исполнения программы полностью соответствует ожиданиям, преобразование строковых массивов и констант в экземпляр класса String
великолепно работает, обратное преобразование тоже работает.
4. Взаимное преобразование двух объектов
Поскольку программы C++ могут иметь пользовательские объекты разных типов, во многих ситуациях появляется необходимость определить взаимное преобразование взаимодействующих объектов, и так как в таком преобразовании будут участвовать два разных объекта, реализовать его можно двумя разными вариантами:
- Механизм преобразования определён в исходном объекте, который преобразуют в другой объект;
- Механизм преобразования определён в объекте, в который преобразуют исходный объект.
Как эти два варианта реализуются, рассмотрим на конкретных примерах. Как известно, есть два стандарта представления текущего времени:
- 24-часовой формат, когда текущее время отображает часы, минуты и секунды разделенные двоеточием -
11:45:38
; - 12-часовой формат, когда текущее время отображает часы, минуты и обозначение
a.m.
илиp.m.
-11:15a.m.
.
Рассмотрим программу, которая преобразует 24-часовой формат отображения текущего времени в 12-часовой формат. Создаю новый файл - tconv1.cpp
. Для реализации задуманного мне необходимы два заголовочных файла стандартной библиотеки.
// tconv1.cpp #include <iostream> #include <string>
Для представления времени в 12-часовом формате создаю класс time12
.
class time12 { private: bool pm; int hrs; int mins; public: time12() : pm(true), hrs(0), mins(0) {} time12(bool ap, int h, int m) : pm(ap), hrs(h), mins(m) {} void display() const; };
Одна из функций этого класса требует дополнительного определения.
void time12::display() const { std::cout << hrs << ":"; if (mins < 10) std::cout << '0'; std::cout << mins << ' '; std::string am_pm = pm ? "p.m." : "a.m."; std::cout << am_pm << std::endl; }
Второй класс - time24
- будет определять представление текущего времени в 24-часовом формате.
class time24 { private: int hours; int minutes; int seconds; public: time24() : hours(0), minutes(0), seconds(0) {} time24(int h, int m, int s) : hours(h), minutes(m), seconds(s) {} void display() const; // Функция operator time12() определяет механизм преобразования // экземпляра time24 в экземпляр time12 operator time12() const; };
Как видно, механизм преобразования объектов time24
в объекты time12
заложен в исходном объекте - time24
. Определяем этот механизм и функцию display
.
void time24::display() const { if (hours < 10) std::cout << '0'; std::cout << hours << ':'; if (minutes < 10) std::cout << '0'; std::cout << minutes << ':'; if (seconds < 10) std::cout << '0'; std::cout << seconds << std::endl; } time24::operator time12() const { int hrs24 = hours; bool pm = hours < 12 ? false : true; int roundMins = seconds < 30 ? minutes : minutes + 1; if (roundMins == 60) { roundMins = 0; ++hrs24; if (hrs24 == 12 || hrs24 == 24) pm = (pm == true) ? false : true; } int hrs12 = (hrs24 < 13) ? hrs24 : hrs24 - 12; if (hrs12 == 0) { hrs12 = 12; pm = false; } return time12(pm, hrs12, roundMins); }
Посмотрим, как это работает, определяю функцию main
.
int main() { // Объявляем переменные h, m, s int h, m, s; while (true) { // Запрашиваем у пользователя ввод показаний часов, минут и секунд std::cout << "Enter 24-hour time:\n"; std::cout << " Hours (0 to 23): "; std::cin >> h; // Если пользователь ввёл показания часов больше 23 if (h > 23) // Прерываем исполнение программы return 1; std::cout << " Minutes: "; std::cin >> m; std::cout << " Seconds: "; std::cin >> s; // С помощью полученных от пользователя данных // создаём экземпляр класса time24 time24 t24(h, m, s); // Для контроля выводим значения полученного экземпляра // на терминал в заданной форме std::cout << "You entered: "; t24.display(); // Создаём экземпляр класса time12 и преобразуем в него // исходный объект time24 - работает функция operator time12() // определённая в исходном объекте (time24) time12 t12 = t24; // Выводим на экран терминала полученный объект // в заданной форме std::cout << "12-hour time: "; t12.display(); std::cout << std::endl; } return 0; }
Компилирую этот код и исполняю полученную программу.
g++ -o exe -Wall tconv1.cpp ./exe
Как видно на снимке экрана выше, компилятор отработал без ошибок и предупреждений. При исполнении программы введённые пользователем часы, минуты и секунды были успешно преобразованы в 12-часовой формат. Здесь следует иметь ввиду, что введённые пользователем данные никак не проверяются, и, если пользователь введёт например 22:87:76
, то может получиться каверза. Для этого надо бы усовершенствовать соответствующий конструктор класса time24
- но это не цель настоящего обзора. А вот преобразование объекта time24
в объект time12
вполне сносно работает.
Рассмотрим второй вариант этой же программы, и на этот раз весь функционал преобразования объекта time24
в объект time12
определим в объекте time12
. Создаю новый файл - tconv2.cpp
. Для этого варианта программы мне понадобятся те же заголовочные файлы.
// tconv2.cpp #include <iostream> #include <string>
Определяю класс time24
, на этот раз этот класс будет иметь три дополнительных функции, которые дадут возможность получать значения соответствующих свойств преобразовываемого объекта.
class time24 { private: int hours; int minutes; int seconds; public: time24() : hours(0), minutes(0), seconds(0) {} time24(int h, int m, int s) : hours(h), minutes(m), seconds(s) {} void display() const; // Три дополнительные функции, которые будут использованы // для реализации механизма преобразования объектов int getHrs() const { return hours; } int getMins() const { return minutes; } int getSec() const { return seconds; } };
Класс time12
получит ещё один дополнительный конструктор, с помощью которого будет создаваться объект time12
преобразованием из объекта time24
.
class time12 { private: bool pm; int hrs; int mins; public: time12() : pm(true), hrs(0), mins(0) {} time12(bool ap, int h, int m) : pm(ap), hrs(h), mins(m) {} // Дополнительный конструктор в котором будет реализован механизм // преобразования объектов time12(time24 t); void display() const; };
Определение дополнительного конструктор класса time12
примет следующий вид.
time12::time12(time24 t) { int hrs24 = t.getHrs(); pm = t.getHrs() < 12 ? false : true; mins = (t.getSec() < 30) ? t.getMins() : t.getMins() + 1; if (mins == 60) { mins = 0; ++hrs24; if (hrs24 == 12 || hrs24 == 24) pm = (pm == true) ? false : true; } hrs = (hrs24 < 13) ? hrs24 : hrs24 - 12; if (hrs == 0) { hrs = 12; pm = false; } }
В этом конструкторе для получения данных из объекта time24
использованы методы getHrs
, getMins
, getSec
.
Функции display
обоих классов и функция main
остались прежними, копирую их без изменений из первого варианта программы. Компилирую программу и пробую её исполнить.
g++ -o exe -Wall tconv2.cpp ./exe
Результат исполнения программы не изменился, взаимное преобразование объектов работает и в этом варианте. Возникает резонный вопрос, какой из двух вариантов использовать? Ответ на этот вопрос весьма неоднозначен и зависит от нескольких обстоятельств. Если оба класса разрабатываются вами, то можно использовать любой из них, а вот если один из классов является заимствованным из какой-либо библиотеки, то второй вариант конверсии будет единственным возможным.
5. Явное и неявное преобразование типов
Вернемся к нашему классу Distance
. Представим ситуацию, когда неявное преобразование базового типа в объект необходимо предотвратить, но при этом сохранить инициализацию экземпляра класса с помощью конструктора с одним аргументом. Создаю новый файл - econv.cpp
, и пишу в нём следующий код.
// explicit.cpp #include <iostream> void fancyDist(Distance d); class Distance { private: const float MTF; int feet; float inches; public: Distance() : MTF(3.280833F), feet(0), inches(0) {} // Конструктор с одним аргументом, с помощью которого // осуществляется преобразование float в экземпляр Distance(float meters) : MTF(3.280833F) { float fltfeet = MTF * meters; feet = static_cast<int>(fltfeet); inches = 12.0F * (fltfeet - static_cast<float>(feet)); } void showdist() { std::cout << feet << "\'-" << inches << "\"\n"; } }; int main() { // Создаём экземпляр и инициализируем его с помощью // конструктора с одним аргументом Distance dist1(2.35F); dist1.showdist(); // Создаём объект путём неявного преобразования типов // этот вариант использования необходимо предотвратить Distance dist2 = 2.35F; std::cout << "dist2 = "; dist2.showdist(); // Создаём переменную типа float float mtrs = 2.35F; std::cout << "dist3 "; // Неявное преобразование float в экземпляр класса Distance // этот вариант использования тоже необходимо предотвратить fancyDist(mtrs); return 0; } void fancyDist(Distance d) { std::cout << "(in feet and inches) = "; d.showdist(); }
В комментариях я обозначил варианты использования конструктора класса Distance
с одним аргументом, которые необходимо предотвратить. Как это сделать? Для этого обозначаю соответствующий конструктор класса Distance
как explicit.
В итоге, поскольку функция main
использует неявное преобразование типов, компилятор выбросит ошибки, и программа не будет скомпилирована.
Так можно предотвратить неявное преобразование и при этом сохранить конструктор с одним аргументом, который такое преобразование реализует, просто обозначив его ключевым словом explicit.
Метки: | gplusplus, cplusplus, newbie, operator_overloading, explicit, type_conversion |