Основы информатики Классы. Часть 3. Перегрузка операторов. Шаблоны Заикин Олег Сергеевич [email protected] Перегрузка операторов класса Введем класс String, немного отличающийся от класса string (но похожий по смыслу использования). Желая определить операцию конкатенации двух объектов класса String, мы могли бы реализовать ее в виде функции-члена concat(). Но почему concat(), а не, скажем, append()? Выбранное нами имя логично и легко запоминается, но пользователь все же может забыть, как мы назвали функцию. Выход – перегрузить операторы + и +=, чтобы они смогли работать с экземплярами класса String. Перегруженные операторы позволяют использовать объекты типа класса с уже известными операторами, и манипулировать ими так же интуитивно, как объектами встроенных типов. Перегрузка операторов класса Перегруженный оператор объявляется в теле класса точно так же, как обычная функция-член, только его имя состоит из ключевого слова operator, за которым следует один из множества предопределенных в языке C++ операторов. Так можно объявить operator+=() в классе String: class String { public: // набор перегруженных операторов += String& operator+=( const string & ); String& operator+=( const char * ); // ... private: // ... }; Перегрузка операторов класса Использование перегруженного оператора += для String. #include "String.h" int main() { String name1 = "Sherlock"; String name2 = "Holmes"; name1 += " "; name1 += name2; if (! ( name1 == "Sherlock Holmes" ) ) cout < < "конкатенация не сработала\n"; } Перегрузка операторов класса Перегрузка операторов класса В классе String есть три набора перегруженных операторов. Первый – это набор операторов присваивания: // набор перегруженных операторов присваивания в классе string String& operator=( const String & ); String& operator=( const char * ); // использование оператора operator=( char * ) String name; name = "Sherlock"; // использование оператора operator=( String & ) String name, str; str = "Sherlock“; name = str; Перегрузка операторов класса В классе String есть три набора перегруженных операторов. Первый – это набор операторов присваивания: // набор перегруженных операторов присваивания в классе string String& operator=( const String & ); String& operator=( const char * ); // использование оператора operator=( char * ) String name; name = "Sherlock"; // использование оператора operator=( String & ) String name, str; str = "Sherlock“; name = str; Перегрузка операторов класса Во втором наборе есть всего один оператор – взятия индекса: // перегруженный оператор взятия индекса char& operator[]( int ); string Str = “abc”; str[1] = ‘a’; В третьем наборе определены перегруженные операторы равенства для объектов класса String. Программа может проверить равенство двух таких объектов или объекта и Cстроки: // набор перегруженных операторов равенства bool operator==( const char * ); bool operator==( const String & ); String str1, str2; if (str == “abc”) // вызов ==( const char * ); if (str == str2) // вызов ==( const String & ); Перегрузка операторов класса Перегрузка операторов класса Перегрузка операторов класса Рассмотрим операторы равенства в нашем классе String более внимательно. Первый оператор позволяет устанавливать равенство двух объектов, а второй – объекта и C-строки: #include "String.h" int main() { String flower; // что-нибудь записать в переменную flower if ( flower == "lily" ) // правильно // ... else if ( "tulip" == flower ) // ошибка // ... } Перегрузка операторов класса При первом использовании оператора равенства в main() вызывается перегруженный operator==(const char *) класса String. Однако на второй инструкции if компилятор выдает сообщение об ошибке. В чем дело? Перегруженный оператор, являющийся членом некоторого класса, применяется только тогда, когда левым операндом служит объект этого класса. Поскольку во втором случае левый операнд не принадлежит к классу String, компилятор пытается найти такой встроенный оператор, для которого левым операндом может быть C-строка, а правым – объект класса String. Разумеется, его не существует, поэтому компилятор говорит об ошибке. Имена перегруженных операторов Перегружать можно только предопределенные операторы языка C++ Следующие четыре оператора языка C++ не могут быть перегружены: :: .* . ?: Встроенные типы Предопределенное назначение оператора нельзя изменить для встроенных типов. Например, не разрешается переопределить встроенный оператор сложения целых чисел так, чтобы он проверял результат на переполнение. // ошибка: нельзя переопределить встроенный оператор сложения int int operator+( int, int ); Нельзя также определять дополнительные операторы для встроенных типов данных, например добавить к множеству встроенных операций operator+ для сложения двух массивов. Шаблоны Заикин Олег Сергеевич [email protected] Обобщенное программирование Обобщённое программирование (generic programming) — парадигма программирования, заключающаяся в таком описании данных и алгоритмов, которое можно применять к различным типам данных, не меняя само это описание. Шаблоны В языке C++ обобщённое программирование основывается на понятии «шаблон», обозначаемом ключевым словом template. Широко применяется в стандартной библиотеке шаблонов (STL), а также в сторонних библиотеках, например boost. В C++ возможно создание шаблонов функций и классов. Шаблоны Шаблоны позволяют создавать параметризованные классы и функции. Параметром может быть любой тип или значение одного из допустимых типов (целое число, enum, указатель на любой объект с глобально доступным именем, ссылка). Шаблоны Например, нам нужен какой-то класс: class SomeClass{ int SomeValue; int SomeArray[20]; } Для одной конкретной цели мы можем использовать этот класс. Но, вдруг, цель немного изменилась, и нужен еще один класс. Теперь нужно 30 элементов массива SomeArray и вещественный тип SomeValue элементов SomeArray. Можно абстрагироваться от конкретных типов и использовать шаблоны с параметрами. Шаблоны Синтаксис: в начале перед объявлением класса напишем слово template и укажем параметры в угловых скобках. В нашем примере: template <int ArrayLength, typename SomeValueType> class SomeClass{ SomeValueType SomeValue; SomeValueType SomeArray[ ArrayLength ]; }; ArrayLength – параметр-значение, SomeValueType – параметр-тип. Тогда для первой модели пишем: SomeClass <20, int> SomeVariable; для второй: SomeClass <30, double> SomeVariable2; SomeClass <30, double> SomeVariable2; Шаблоны template <int val> class SomeClass{ int SomeValue; int SomeArray[ val ]; }; template < typename NewType > class SomeClass{ NewType SomeValue; NewType SomeArray[ 10 ]; }; Шаблоны Хотя шаблоны предоставляют краткую форму записи участка кода, на самом деле их использование не сокращает исполнимый код, так как для каждого набора параметров компилятор создаёт отдельный экземпляр функции или класса. SomeClass <20, int> var1; SomeClass <30, double> var2; SomeClass <30, double> var3; Сколько экземпляров класса будет создано? Шаблоны функций Шаблон функции начинается с ключевого слова template, за которым в угловых скобках следует список параметров. Затем следует объявление функции: template< typename T > void sort( T array[], int size ); // прототип: шаблон sort объявлен, но не определён template< typename T > void sort( T array[], int size ) // объявление и определение { T t; for (int i = 0; i < size - 1; i++) for (int j = size - 1; j > i; j--) if (array[j] < array[j-1]) { t = array[j]; array[j] = array[j-1]; array[j-1] = t; } } Шаблоны функций template<int BufferSize> // целочисленный параметр char* read() { char *Buffer = new char[ BufferSize ]; /* считывание данных */ return Buffer; } Пример использования Простейшим примером служит определение минимума из двух величин. Если a меньше b то вернуть а, иначе - вернуть b В отсутствие шаблонов программисту приходится писать отдельные функции для каждого используемого типа данных. Хотя многие языки программирования определяют встроенную функцию минимума для элементарных типов (таких как целые и вещественные числа), такая функция может понадобиться и для сложных (например «время» или «строка») и очень сложных («игрок» в онлайн-игре) объектов. Пример использования Так выглядит шаблон функции определения минимума: template<typename T> T min( T a, T b ) { return a < b ? a : b; } Для вызова этой функции можно просто использовать её имя: min( 1, 2 ); min( 'a', 'b' ); min( string( "abc" ), string( "cde" ) ); Вызов шаблонной функции int i[5] = { 5, 4, 3, 2, 1 }; sort< int >( i, 5 ); char c[] = "бвгда"; sort< char >( c, strlen( c ) ); sort< int >( c, 5 ); // ошибка: у sort< int > параметр int[] а не char[] char *ReadString = read< 20 >(); delete [] ReadString; ReadString = read< 30 >(); Для каждого набора параметров компилятор генерирует новый экземпляр функции. Процесс создания нового экземпляра называется инстанцированием шаблона.