|
||||
|
Неделя №2Основные вопросыМы завершили первую неделю обучения и научились основным принципам и средствам программирования на C++. Для вас теперь не должно составлять труда написание и компиляция небольшой программы. Также вы должны четко представлять, что такое классы и объекты, составляющие основу объект-ориентированного программирования. Что дальшеВторую неделю начнем с изучения указателей. Указатели традиционно являются сложной темой для освоения начинающими программистами на C++. Но в этой книге вы найдете подробные и наглядные разъяснения того, что такое указатель и как он работает, поэтому, мы надеемся, что через день вы уже свободно будете владеть этим средством программирования. На занятии 9 вы познакомитесь со ссылками, которые являются близкими родственниками указателей. На занятии 10 вы узнаете как замешать функции, а занятие 11 будет посвящено наследованию и разъяснению фундаментальных принципов объект-ориентированного программирования. На занятии 12 вы узнаете как создавать структуры данных от простых массивов до связанных списков. Занятие 13 расширит ваши представления об объект-ориентированном программировании и познакомит с полиморфизмом, а занятие 14 завершит вторую неделю обучения рассмотрением статических функций и функций друзей класса. День 8-й. УказателиВозможность непосредственного доступа к памяти с помощью указателей — одно их наиболее мощных средств программирования на C++. Сегодня вы узнаете: • Что такое указатели • Как объявляются и используются указатели • Как работать с памятью При работе с указателями программисты подчас сталкиваются с довольно специфическими проблемами, поскольку в некоторых ситуациях механизм работы указателей может оказаться весьма запутанным. Кроме того, в ряде случаев нельзя однозначно ответить на вопрос о необходимости применения указателей. На этом занятии последовательно, шаг за шагом, вы освоите основные принципы работы с указателями. Однако осознать всю мощь этих средств вы сможете, только прочитав книгу до конца. Что такое указательУказатель — это переменная, в которой записан адрес ячейки памяти компьютера. Чтобы понять, как работают указатели, необходимо хотя бы в общих чертах, ознакомиться с базовыми принципами организации машинной памяти. Машинная память состоит из последовательности пронумерованных ячеек. Значение каждой переменной хранится в отдельной ячейке памяти, которая называется ее адресом. На рис. 8.1 изображена структура размещения в памяти четырехбайтового целого значения переменной theAge. Для разных компьютеров характерны различные правила адресации памяти, имеющие свои особенности. Однако в большинстве случаев программисту не обязательно знать точный адрес какой-либо переменной — эту задачу выполняет компьютер. При необходимости такую информацию можно получить с помощью оператора адреса (&). Пример использования этого оператора приведен в листинге 8.1. Рис. 8.1. Сохранение в памяти переменной theAge Листинг 8.1. Оператор адреса 1: // Листинг 8.1. Пример использования 2: // оператора адреса 3: 4: #include <iostream.h> 5: 6: int main() 7: { 8: unsigned short shortVar=5; 9: unsigned long longVar=65535; 10: long sVar = -65535; 11: 12: cout << "shortVar:\t" << shortVar; 13: cout << " Address of shortVar:\t"; 14: cout << &shortVar << "\n"; 15: 16: cout << "longVar:\t" << longVar; 17: cout << " Address of longVar:\t" 18: cout << &longVar << "\n"; 19: 20: cout << "s.Var:\t" << sVar; 21: cout << " Address of sVar:\t" 22: cout << &sVar << "\n"; 23: 24: return 0; 25:} Результат: shortVar: 5 Address of shortVar: 0x8fc9:fff4 longVar: 65535 Address of longVar: 0x8fc9:fff2 sVar: -65535 Address of sVar: 0x8fc9:ffee (Ваши результаты могут отличаться от приведенных в листинге.) Анализ: В начале программы объявляются и инициализируются три переменные: в строке 8 — переменная типа unsigned short, в строке 9 — типа unsigned long, а в строке 10 — типа long. Затем в строках 12-16 выводятся значения и адреса этих переменных, полученные с помощью оператора адреса (&). При запуске программы на компьютере с процессором 80386 значение переменной shortVar равно 5, а ее адрес — 0x8fc9:fff4. Адрес размещения переменной выбирается компьютером и может изменяться при каждом последующем запуске программы. Поэтому ваши результаты могут отличаться от приведенных. Причем разница между двумя первыми адресами будет оставаться постоянной. При двухбайтовом представлении типа short эта разница составит 2 байта, а разница между третьим и четвертым адресами — 4 байта при четырехбайтовом представлении типа long. Порядок размещения этих переменных в памяти показан на рис. 8.2. В большинстве случаев вам не придется непосредственно манипулировать адресами переменных. Важно лишь знать, какой объем памяти занимает переменная и как получить ее адрес в случае необходимости. Программист лишь указывает компилятору объем памяти, доступный для размещения статических переменных, после чего размещение переменной по определенному адресу будет выполняться автоматически. Обычно тип long имеет четырехбайтовое представление. Это означает, что для хранения переменной этого типа потребуется четыре байта машинной памяти. Использование указателя как средства хранения адресаКаждая переменная программы имеет свой адрес, для хранения которого можно использовать указатель на эту переменную. Причем само значение адреса знать не обязательно. Допустим, что переменная howOld имеет тип int. Чтобы объявить указатель pAge для хранения адреса этой переменной, наберите следующий фрагмент кода: int *pAge = 0; Этой строкой переменная pAge объявляется указателем на тип int. Это означает, что pAge будет содержать адрес значения типа int. Отметим, что pAge ничем не отличается от любой другой переменной. При объявлении переменной целочисленного типа (например, int) мы указываем на то, что в ней будет храниться целое число. Когда же переменная объявляется указателем на какой-либо тип, это означает, что она будет хранить адрес переменной данного типа. Таким образом, указатели являются просто отдельным типом переменных. В данном примере переменная pAge инициализируется нулевым значением. Указатели, значения которых равны 0, называют пустыми. После объявления указателю обязательно должно присваиваться какое-либо значение. Если заранее неизвестно, какой адрес должен храниться в указателе, ему присваивается значение 0. Неинициализированные указатели в дальнейшем могут стать причиной больших неприятностей. Поскольку при объявлении указателю pAge было присвоено значение 0, далее ему нужно присвоить адрес какой-либо переменной, например howOld. Как это сделать, показано ниже: unsigned short int howOld = 50; // объявляем переменную unsigned short int *pAge = 0; // объявляем указатель pAge = &howOld; // Присвоение указателю pAge адреса переменной howOld Рис. 8.2. Схема сохранения переменной в памяти В первой строке объявлена переменная howOld типа unsigned short int и ей присвоено значение 50. Во второй строке объявлен указатель pAge на тип unsigned short int, которому присвоено значение 0. Символ "звездочка" (*), стоящий после наименования типа, указывает на то, что описанная переменная является указателем. В последней строке указателю pAge присваивается адрес переменной howOld. На это указывает оператор адреса (&) перед именем переменной howOld. Если бы этого оператора не было, присваивался бы не адрес, а значение переменной, которое также может являться корректным адресом. В нашем случае значением указателя pAge будет адрес переменной howOld, значение которой равно 50. Две последние строки рассмотренного фрагмента программы можно объединить в одну: unsigned short int howOld = 50; // объявляем переменную unsigned short int * pAge = &how01d; // объявляем указатель на переменную howOld Теперь указатель pAge содержит адрес переменной howOld. С помощью этого указателя можно получить и значение переменной, на которую он указывает. В нашем примере это значение равно 50. Обращение к значению how01d посредством указателя pAge называется операцией разыменования или косвенного обращения, поскольку осуществляется неявное обращение к переменной how01d, адрес которой содержится в указателе. Далее вы узнаете, как с помощью разыменовывания возвращать значения переменных. Косвенное обращение подразумевает получение значения переменной, адрес которой содержится в указателе, а оператор разыменования позволяет извлечь это значение. Имена указателейПоскольку указатели являются обычными переменными, называть их можно любыми корректными для переменных именами. Для выделения указателей среди других переменных многие программисты используют перед их именами символ "p" (от англ. pointer), например pAge или pNumber. Оператор разыменовыванияОператор косвенного обращения (или оператор разыменования) позволяет получить значение, хранящееся по адресу, записанному в указателе. В отличие от указателя, при обращении к обычной переменной осуществляется доступ непосредственно к ее значению. Например, чтобы объявить новую переменную типа unsigned short int, а затем присвоить ей значение другой переменной, можно написать следующее: unsigned short int yourAge; yourAge = howOld; При косвенном доступе будет получено значение, хранящееся по указанному адресу. Чтобы присвоить новой переменной yourAge значение how01d, используя указатель pAge, содержащий ее адрес, напишите следующее: unsigned short int yourAge; yourAge = *pAge; Оператор разыменования (*) перед переменной pAge может рассматриваться как "значение, хранящееся по адресу". Таким образом, вся операция присваивания означает: "получить значение, хранящееся по адресу, записанному в pAge, и присвоить его переменной yourAge". Примечание:Оператор разыменования можно использовать с указателями двумя разными способами: для объявления указателя и для его разыменовывания. В случае объявления указателя символ звездочки сигнализирует компилятору, что это не простая переменная, а указатель, например: unsigned short << pAge = 0; // объявляется указатель // на переменную типа unsigned short В случае разыменовывания указателя символ звездочки означает, что операция должна производиться не над самим адресом, а над значением, сохраненным по адресу, который хранится в указателе: *pAge = 5; //присваивает значение 5 переменной по адресу в указателе pAge Также не путайте оператор разыменовывания с оператором умножения (*). Компилятор по контексту определяет, какой именно оператор используется в данном случае. Указатели, адреса и переменныеЧтобы овладеть навыками программирования на C++, вам в первую очередь необходимо понимать, в чем различие между указателем, адресом, хранящимся в указателе, и значением, записанным по адресу, хранящемуся в указателе. В противном случае это может привести к ряду серьезных ошибок при написании программ. Рассмотрим еще один фрагмент программы: int theVariable = 5; int * pPointer = &theVariable ; В первой строке объявляется переменная целого типа theVariable. Затем ей присваивается значение 5. В следующей строке объявляется указатель на тип int, которому присваивается адрес переменной theVariable. Переменная pPointer является указателем и содержит адрес переменной theVariable. Значение, хранящееся по адресу, записанному в pPointer, равно 5. На рис. 8.3 схематически показана структура этих переменных. Рис. 8.3. Схема распределения памяти Обращение к данным через указателиПосле того как указателю присвоен адрес какой-либо переменной, его можно использовать для работы со значением этой переменной. В листинге 8.2 показан пример обращения к значению локальной переменной через указатель на нее. Листинг 8.2. Обращение к данным через указатели 1: // Листинг 8.2. Использование указателей 2: 3: #include<iostream.h> 4: 5: typedef unsigned short int USHORT; 6: int main() 7: { 8: USHORT myAge; // переменная 9: USHORT * pAge = 0; // указатель 10: myAge = 5; 11: cout << "myAge: " << myAge << "\n"; 12: pAge = &myAge; // заносим в pAge адрзс myAge 13: cout << "*pAge: " << *pAge << "\n\n"; 14: cout << "*pAge = 7\n"; 15: *pAge = 7; // присваиваем myAge значение 7 16: cout << "*pAge: " << *pAge << "\n"; 17: cout << "myAge: " << myAge << "\n\n"; 18: cout << "myAge = 9\n"; 19: myAge = 9; 20: cout << "myAge: " << myAge << "\n"; 21: cout << "*pAge: " << *pAge << "\n"; 22: 23: return 0; 24: } Результат: myAge: 5 *pAge: 5 *pAge: = 7 *pAge: 7 myAge: 7 myAge = 9 myAge: 9 *pAge: 9 Анализ: В программе объявлены две переменные: myAge типа unsigned short и pAge, являющаяся указателем на этот тип. В строке 10 переменной pAge присваивается значение 5, а в строке 11 это значение выводится на экран. Затем в строке 12 указателю pAge присваивается адрес переменной myAge. С помощью операции разыменования значение, записанное по адресу, хранящемуся в указателе pAge, выводится на экран (строка 13). Как видим, полученный результат совпадает со значением переменной myAge. В строке 15 переменной, адрес которой записан в pAge, присваивается значение 7. После выполнения такой операции переменная myAge будет содержать значение 7. Убедиться в этом можно после вывода этих значений (строки 16, 17). В строке 19 значение myAge опять изменяется. Теперь этой переменной присваивается число 9. Затем в строках 20 и 21 мы обращаемся к этому значению непосредственно (через переменную) и путем разыменования указателя на нее. Использование адреса, хранящегося в указателеПри работе с указателями в большинстве случаев не приходится иметь дело со значениями адресов, записанных в указателях. В предыдущих разделах отмечалось, что после присвоения указателю адреса переменной значением указателя будет именно этот адрес. Почему бы не проверить это утверждение? Для этого можно воспользоваться программой, приведенной в листинге 8.3. Листинг 8.3. Что же записано в указателе? 1: // Листинг 8.3. Что же хранится в указателе? 2: 3: #include <iostream.h> 4: 5: 6: int main() 7: { 8: unsigned short int myAge = 5, yourAge = 10; 9: unsigned short int * pAge = &myAge; // Указатель 10: cout << "myAge:\t" << myAge << "\t yourAge:\t" << yourAge << "\n"; 11: cout << "&myAge:\t" << &myAge << "\t&yourAge;\t" << &yourAge << "\n"; 12: cout << "pAge;\t" << pAge << "\n"; 13: cout << "*pAge:\t" << *pAge << "\n"; 14: pAge = &yourAge; // переприсвоение указателя 15: cout << "myAge:\t" << myAge << "\t yourAge;\t" << yourAge << "\n"; 16: cout << "&myAge:\t" << &myAge << "\t&yourAge:\t" << &yourAge << "\n"; 17: cout << "pAge:\t" << pAge << "\n"; 18: cout << "*pAge:\t" << *pAge << "\n"; 19: cout << "&pAge:\t" << &pAge << "\n"; 20: return 0; 21: } Результат: myAge: 5 yourAge: 10 &myAge: 0x355C &yourAge: 0x355E pAge: 0x355C *pAge: 5 myAge: 5 yourAge: 10 &myAge: 0x355C &yourAge: 0x355E pAge: 0x355E *pAge: 10 &pAge: 0x355A (Ваши результаты могут отличаться от приведенных.) Анализ: В строке 8 объявляются две переменные типа unsigned short — myAge и yourAge. Далее, в строке 9, объявляется указатель на этот тип (pAge). Этому указателю присваивается адрес переменной myAge. В строках 10 и 11 значения и адреса переменных pAge и myAge выводятся на экран. Обращение к значению переменной myAge путем разыменования указателя pAge выполняется в строке 13. Перед тем как перейти к дальнейшему изучению материала, подумайте, все ли вам понятно в рассмотренном примере. Еще раз проанализируйте текст программы и результат ее выполнения. В строке 14 указателю pAge присваивается адрес переменной yourAge. После этого на экран выводятся новые значения и адреса переменных. Проанализировав результат программы, можно убедиться, что указатель pAge действительно содержит адрес переменной youtAge, а с помощью разыменования этого указателя можно получить ее значение. Строка 19 выводит на экран значение адреса указателя pAge. Как любая другая переменная, указатель также имеет адрес, значение которого может храниться в другом указателе. О хранении в указателе адреса другого указателя речь пойдет несколько позже. Рекомендуется:Используйте оператор разыменовывания (*) для получения доступа к данным, сохраненным по адресу, содержащемуся в указателе. Инициализируйте указатель нулевым значением при объявлении, если заранее не известно, для указания на какую переменную он будет использоваться. Помните о разнице между адресом в указателе и значением переменной, на которую ссылается этот указатель. Использование указателей Чтобы объявить указатель, запишите вначале тип переменной или объекта, на который будет ссылаться этот указатель, затем поместите символ звездочки (*), а за ним — имя нового указателя, например: unsigned short int * pPointer =0; Чтобы присвоить указателю адрес переменной, установите перед именем переменной оператор адреса (&), как в следующем примере: unsigned short int theVariable = 5; unsigned short int * pPointer = & theVariable; Чтобы разыменовать указатель, установите перед его именем оператор разыменовывания (*): unsigned short int theValue = *pPointer Для чего нужны указателиВ предыдущих разделах мы детально рассмотрели процедуру присвоения указателю адреса другой переменной. Однако на практике такое использование указателей встречается достаточно редко. К тому же, зачем задействовать еще и указатель, если значение уже хранится в другой переменной? Рассмотренные выше примеры приведены только для демонстрации механизма работы указателей. Теперь, после описания синтаксиса, используемого в C++ для работы с указателями, можно переходить к более профессиональному их применению. Наиболее часто указатели применяются в следующих случаях: • для размещения данных в свободных областях памяти и доступа к ним; • для доступа к переменным и функциям классов; • для передачи параметров в функции по ссылке. Оставшаяся часть главы посвящена динамическому управлению данными и операциям с переменными и функциями классов. Память стековая и динамически распределяемаяЕсли вы помните, на занятии 5 приводились условное разделение памяти на пять областей: • область глобальных переменных; • свободная, или динамически распределяемая память; • регистровая память (регистры); • сегменты программы; • стековая память. Локальные переменные и параметры функций размещаются в стековой памяти. Программный код хранится в сегментах, глобальные переменные — в области глобальных переменных. Регистровая память предназначена для хранения внутренних служебных данных программы, таких как адрес вершины стека или адрес команды. Остальная часть памяти составляет так называемую свободную память — область памяти, динамически распределяемую между различными объектами. Особенностью локальных переменных является то, что после выхода из функции, в которой они были объявлены, память, выделенная для их хранения, освобождается, а значения переменных уничтожаются. Глобальные переменные позволяют частично решить эту проблему ценой неограниченного доступа к ним из любой точки программы, что значительно усложняет восприятие текста программы. Использование динамической памяти полностью решает обе проблемы. Чтобы понять, что же такое динамическая память, попытайтесь представить область памяти, разделенную на множество пронумерованных ячеек, в которых записана информация. В отличие от стека переменных, ячейкам свободной памяти нельзя присвоить имя. Доступ к ним осуществляется посредством указателя, хранящего адрес нужной ячейки. Чтобы лучше понять изложенное выше, рассмотрим пример. Допустим, вам дали номер телефона службы заказов товара по почте. Придя домой, вы занесли этот номер в память вашего телефона, а листок бумаги, на котором он был записан, выбросили. Нажимая на кнопку телефона, вы соединяетесь со службой заказа. Для вас не имеет значения номер и адрес этой службы, поскольку вы уже получили доступ к интересующей вас информации. Служба заказов в данном случае является моделью динамической памяти. Вы не знаете, где именно находится нужная вам информация, но знаете, как ее получить. Для обращения к значению используется его адрес, роль которого играет телефонный номер. Причем помнить адрес (или номер) не обязательно — достаточно лишь записать его значение в указатель (или телефон). После этого, используя указатель, можно извлечь нужное значение, даже не зная место его расположения. Что касается стека переменных, то по завершении работы функции он очищается. В результате все локальные переменные оказываются вне области видимости и их значения уничтожаются. В отличие от стека, динамическая память не очищается до завершения работы программы, поэтому в таком случае освобождением памяти должен заниматься программист. Важным преимуществом динамической памяти является то, что выделенная в ней облаять памяти не может использоваться до тех пор, пока явно не будет освобождена. Поэтому, если память в динамической области выделяется во время работы функции, ее можно будет использовать даже после завершения работы. Еще одним преимуществом динамического выделения памяти перед использованием глобальных переменных является то, что доступ к данным можно получить только из функций, в которых есть доступ к указателю, хранящему нужный адрес. Такой способ доступа позволяет жестко контролировать характер манипулирования данными, а также избегать нежелательного или случайного их изменения. Для работы с данными описанным способом прежде всего нужно создать указатель на ячейки динамической области памяти. О том, как это сделать, читайте в следующем разделе. Оператор newДля выделения памяти в области динамического распределения используется ключевое слово new. После new следует указать тип объекта, который будет размещаться в памяти. Это необходимо для определения размера области памяти, требуемой для хранения объекта. Написав, например, new unsigned short int, мы выделим два байта памяти, а строка new long динамически выделит четыре байта. В качестве результата оператор new возвращает адрес выделенного фрагмента памяти. Этот адрес должен присваиваться указателю. Например, для выделения памяти в области динамического обмена переменной типа unsigned short можно использовать такую запись: unsigned short int * pPointer; pPointer = new unsigned short int; Или выполнить те же действия, но в одной сороке: unsigned short int * pPointer = new unsigned short int; В каждом случае указатель pPointer будет указывать на ячейку памяти в области динамического обмена, содержащую значение типа unsigned short. Теперь pPointer можно использовать как любой другой указатель на переменную этого типа. Чтобы занести в выделенную область памяти какое-нибудь значение, напишите такую строку: *pPointer = 72; Эта строка означает следующее: "записать число 72 в память по адресу, хранящемуся в pPointer". Ввиду того что память является ограниченным ресурсом, попытка выделения памяти оператором new может оказаться неудачной. В этом случае возникнет исключительная ситуация, которая рассматривается на занятии 20. Оператор deleteКогда память, выделенная под переменную, больше не нужна, ее следует освободить. Делается это с помощью оператора delete, после которого записывается имя указателя. Оператор delete освобождает область памяти, определенную указателем. Необходимо помнить, что указатель, в отличие от области памяти, на которую он указывает, является локальной переменной. Поэтому после выхода из функции, в которой он был объявлен, этот указатель станет недоступным. Однако область памяти, выделенная оператором new, на которую сослался указатель, при этом не освобождается. В результате часть памяти окажется недоступной. Программисты называют такую ситуацию утечкой памяти. Такое название полностью соответствует действительности, поскольку до завершения работы программы эту память использовать нельзя, она как бы "вытекает" из вашего компьютера. Чтобы освободить выделенную память, используйте ключевое слово delete, например: delete pPointer; На самом деле при этом происходит не удаление указателя, а освобождение области памяти по адресу, записанному в нем. При освобождении выделенной памяти с самим указателем ничего не происходит и ему можно присвоить другой адрес. Листинг 8.4 показывает, как выделить память для динамической переменной, использовать ее, а затем освободить. Предупреждение:Когда оператор delete применяется к указателю, происходит освобождение области динамической памяти, на которую этот указатель ссылается. Повторное применение оператора delete к этому же указателю приведет к зависанию программы. Рекомендуется при освобождении области динамической памяти присваивать связанному с ней указателю нулевое значение. Вызов оператора delete для нулевого указателя пройдет совершенно безболезненно для программы, например: Animal *pDog = new Animal; delete pDog; // освобождение динамической памяти pDog = 0 // присвоение указателю нулевого значения // ... delete pDog; // бессмысленная, но совершенно безвредная строка Листинг 8.4. Выделение, использование и освобождение динамической памяти 1; // Листинг 8, 4, 2; // Выделение, использование и освобождение динамической памяти 3; 4: #include <iostream.h> 5: int main() 6: { 7: int localVariable = 5; 8: int * pLocal= &localVariable; 9: int * pHeap = new int; 10: рНеар = 7; 11: cout << "localVariable: " << localVariable << "\n"; 12: cout << "*pLocal: " << *pLocal << "\n"; 13: cout << "*pHeap; " << *pHeap << "\n"; 14: delete рНеар; 15: рНеар = new int; 16: *pHeap = 9; 17: cout << "*pHeap: " << *pHeap << "\n"; 18: delete рНеар; 19: return 0; 20: } Результат: localVariable: 5 *pLocal: 5 *pHeap: 7 *pHeap: 9 Анализ: В строке 7 объявляется и инициализируется локальная переменная localVariable. Затем объявляется указатель, которому присваивается адрес этой переменной (строка 8). В строке 9 выделяется память для переменной типа int и адрес выделенной области помещается в указатель рНеар. Записав по адресу, содержащемуся в рНеар, значение 7, можно удостовериться в том, что память была выделена корректно (строка 10). Если бы память под переменную не была выделена, то при выполнении этой строки появилось бы сообщение об ошибке. Чтобы не перегружать примеры излишней информацией, мы опускаем всякого рода проверки. Однако во избежание аварийного завершения программы при решении реальных задач такой контроль обязательно должен выполняться. В строке 10, после выделения памяти, по адресу в указателе записывается значение 7. Затем в строках 11 и 12 значения локальной переменной и указателя pLocal выводятся на экран. Вполне понятно, почему эти значения равны. Далее, в строке 13, выводится значение, записанное по адресу, хранящемуся в указателе рНеар. Таким образом, подтверждается, что значение, присвоенное в строке 10, действительно доступно для использования. Освобождение области динамической памяти, выделенной в строке 9, осуществляется оператором delete в строке 14. Освобожденная память становится доступной для дальнейшего использования, и ее связь с указателем разрывается. После этого указатель рНеар может использоваться для хранения нового адреса. В строках 15 и 16 выполняется повторное выделение памяти и запись значения по соответствующему адресу. Затем в строке 17 это значение выводится на экран, после чего память освобождается. Вообще говоря, строка 18 не является обязательной, так как после завершения работы программы вся выделенная в ней память автоматически освобождается. Однако явное освобождение памяти считается как бы правилом хорошего тона в программировании. Кроме того, это может оказаться полезным при редактировании программы. Что такое утечка памятиПри невнимательной работе с указателями может возникнуть эффект так называемой утечки памяти. Это происходит, если указателю присваивается новое значение, а память, на которую он ссылался, не освобождается. Ниже показан пример такой ситуации. 1: unsigned short int * pPointer = new unsigned short int; 2: *pPointer = 72; 3: delete pPointer; 4: pPointer = new unsigned short int; 5: *pPointer = 84; В строке 1 объявляется указатель и выделяется память для хранения переменной типа unsigned short int. В следующей строке в выделенную область записывается значение 72. Затем в строке 3 указателю присваивается адрес другой области памяти, в которую записывается число 84 (строка 4). После выполнения таких операций память, содержащая значение 72, оказывается недоступной, поскольку указателю на эту область было присвоено новое значение. В результате невозможно ни использовать, ни освободить зарезервированную память до завершения программы. Правильнее было бы написать следующее: 1: unsigned short int * pPointer = new unsigned short int; 2: *pPointer = 72; 3: pPointer = new unsigned short int; 4: *pPointer = 84; В этом случае память, выделенная под переменную, корректно освобождается (строка 3). Примечание:Каждый раз, когда в программе используется оператор new, за ним должен следовать оператор delete. Очень важно следить, какой указатель ссылается на выделенную область динамической памяти, и вовремя освобождать ее. Размещение объектов в области динамической памятиАналогично созданию указателя на целочисленный тип, можно динамически размещать в памяти любые объекты. Например, если вы объявили объект класса Cat, для манипулирования этим объектом можно создать указатель, в котором будет храниться его адрес, — ситуация, абсолютно аналогичная размещению переменных в стеке. Синтаксис этой операции такой же, как и для целочисленных типов: Cat *pCat = new Cat; В данном случае использование оператора new вызывает конструктор класса по умолчанию, т.е. конструктор, использующийся без параметров. Важно помнить, что при создании объекта класса конструктор вызывается всегда, независимо от того, размещается объект в стеке или в области динамического обмена. Удаление объектовПри использовании оператора delete, за которым следует идентификатор указателя на объект, вызывается деструктор соответствующего класса. Это происходит еще до освобождения памяти и возвращения ее в область динамического обмена. В деструкторе, как правило, освобождается вся память, занимаемая объектом класса. Пример динамического размещения и удаления объектов показан в листинге 8.5. Листинг 8.5. Размещение и удаление объектов в области динамического обмена 1: // Листинг 8.5. 2: // Размещение и удаление объектов в области динамического обмена 3: 4: #include <iostream.h> 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat(); 10: ~SimpleCat(); 11: private: 12: int itsAge; 13: }; 14: 15: SimpleCat::SimpleCat() 16: { 17: cout << "Constructor called.\n"; 18: itsAge = 1; 19: } 20: 21: SimpleCat::~SimpleCat() 22: { 23: cout << "Destructor called.\n"; 24: } 25: 26: int main() 27: { 28: cout << "SimpleCat Frisky...\n"; 29: SimpleCat Frisky; 30: cout << "SimpleCat *pRags = new SimpleCat...\n"; 31: SimpleCat * pRags = new SimpleCat; 32: cout << "delete pRags...\n"; 33: delete pRags; 34: cout << "Exiting, watch Frisky go...\n"; 35: return 0; 36: } Результат: SimpleCat Frisky... Constructor called. SimpleCat *pRags = new SimpleCat.. Constructor called. delete pRags... Destructor called. Exiting, watch Frisky go... Destructor called. Анализ: В строках 6—13 приведено описанИе простейшего класса SimpleCat. Описание конструктора класса находится в строке 9, а его тело — в строках 15-19. Деструктор описан в строке 10, его тело — в строках 21-24. В строке 29 создается экземпляр описанного класса, который размешается в стеке. При этом происходит неявный вызов конструктора класса SimpleCat. Второй объект класса создается в строке 31. Для его хранения динамически выделяется память и адрес записывается в указатель pRags. В этом случае также вызывается конструктор. Деструктор класса SimpleCat вызывается в строке 33 как результат применения оператора delete к указателю pRags. При выходе из функции переменная Frisky оказывается за пределами области видимости и для нее также вызывается деструктор. Доступ к членам классаДля локальных переменных, являющихся объектами класса, доступ к членам класса осуществляется с помощью оператора прямого доступа (.). Для экземпляров класса, созданных динамически, оператор прямого доступа применяется для объектов, полученных разыменованием указателя. Например, для вызова функции-члена GetAge нужно написать: (*pRags).GetAge(); Скобки указывают на то, что оператор разыменования должен выполняться еще до вызова функции GetAge(). Такая конструкция может оказаться достаточно громоздкой. Решить эту проблему позволяет специальный оператор косвенного обращения к члену класса, по написанию напоминающий стрелку (->). Для набора этого оператора используется непрерывная последовательность двух символов: тире и знака "больше". В C++ эти символы рассматриваются как один оператор. Листинг 8.6 иллюстрирует пример обращения к переменным и функциям класса, экземпляр которого размещен в области динамического обмена. Листинг 8.6. Доступ к данным объекта в области динамического обмена 1: // Листинг 8.6. 2: // Доступ к данным объекта в области динамического обмена 3: 4: #include <iostream.h> 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat() { itsAge = 2; } 10: ~SimpleCat() { } 11: int GetAge() const { return itsAge; > 12: void SetAge(int age) { itsAge = age; } 13: private: 14: int itsAge; 15: }; 16: 17: int main() 18: { 19: SimpleCat * Frisky = new SimpleCat; 20: cout << "Frisky " << Frisky->GetAge() << " years old\n"; 21: Frisky->SetAge(5); 22: cout << "Frisky " << Frisky->GetAge() << " years old\n"; 23: delete Frisky; 24: return 0; 25: } Результат: Frisky 2 years old Frisky 5 years old Анализ: В строке 19 в области динамического обмена выделяется память для хранения экземпляра класса SimpleCat. Конструктор, вызываемый по умолчанию, присваивает новому объекту возраст два года. Это значение получено как результат выполнения функции-члена GetAge(), которая вызывается в строке 20. Поскольку мы имеем дело с указателем на объект, для вызова функции используется оператор косвенного обращения к члену класса (->). В строке 21 для установки нового значения возраста вызывается метод SetAge(), а повторный вызов функции GetAge() (строка 22) позволяет получить это значение. Динамическое размещение членов классаВ качестве членов класса могут выступать и указатели на объекты, размещенные в области динамического обмена. В таких случаях выделение памяти для хранения этих объектов осуществляется в конструкторе или в одном из методов класса. Освобождение памяти происходит, как правило, в деструкторе (листинг 8.7.). Листинг 8.7. Указатели как члены класса 1: // Листинг 8.7. 2: // Указатели как члены класса 3: 4: #include <iostream.h> 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat(); 10: ~SimpleCat(); 11: int GetAge() const { return *itsAge; } 12: void SetAge(int age) { *itsAge = age; } 13: 14: int GetWeight() const { return *itsWeight; } 15: void setWeight (int weight) { *itsWeight = weight; } 16: 17: private: 18: int * itsAge: 19: int * itsWeight; 20: }; 21: 22: SimpleCat::SimpleCat() 23: { 24: itsAge = new int(2); 25: itsWeight = new int(5); 26: } 27: 28: SimpleCat::~SimpleCat() 29: { 30: delete itsAge; 31: delete itsWeight; 32: } 33: 34: int main() 35: { 36: SimpleCat *Frisky = new SimpleCat; 37: cout << "Frisky " << Frisky->GetAge() << " years old\n"; 38: Frisky->SetAge(5); 39: cout << "Frisky " << Frisky->GetAge() << " years old\n"; 40: delete Frisky; 41: return 0; 42: } Результат: Frisky 2 years old Frisky 5 years old Анализ: Объявляем класс, переменными-членами которого являются два указателя на тип int. В конструкторе класса (строки 22—26) выделяется память для хранения этих переменных, а затем им присваиваются начальные значения. Выделенная под переменные-члены память освобождается в деструкторе (строки 28—32). После освобождения памяти в деструкторе присваивать указателям нулевые значения не имеет смысла, поскольку уничтожается и сам экземпляр класса. Такая ситуация является одним из тех случаев, когда после освобождения памяти указателю можно не присваивать значение 0. При выполнении функции, из которой осуществляется обращение к переменным класса (в данном случае main()), вы можете и не знать, каким образом выполняется это обращение. Вы лишь вызываете соответствующие методы класса (GetAge() и SetAge()), а все операции с памятью выполняются внутренними механизмами класса. При уничтожении объекта Frisky (строка 40) вызывается деструктор класса SimpleCat. В деструкторе память, выделенная под члены класса, освобождается. Если один из членов класса является объектом другого определенного пользователем класса, происходит вызов деструктора этого класса. Вопросы и ответы:Если я объявляю объекг класса, хранящийся в стеке, а этот объект, в свою очередь, имеет переменные-члены, хранящиеся в области динамического обмена, то какие части объекта будрт находиться в стеке, а какие — в области динамического обмена? #include <iostream.h> class SimpleCat { public: SimpleCat(); ~SimpleCat(); int GetAge() const { return *itsAge; } // другие методы private: int * itsAge; int * itsWeight; }; SimpleCat::SimpleCat() { itsAge = new int(2); itsWeight = new int(5); } SimpleCat::~SimpleCat() { delete itsAge; delete itsWeight; } int main() { SimpleCat Frisky; cout << "Frisky is " << Frisky.GetAge() << " years old\n"; return 0; } В стеке будет находиться локальная переменная Frisky. Эта переменная содержитдва указателя, каждый из которых занимает по четыре байта стековой памяти для хранения адресов целочисленных значений, размещенных в области динамического обмена. Таким образом, объект Frisky займет восемь байтов стековой памяти и восемь— в области динамического обмена. Конечно, для данного примера динамическое размещение в памяти переменных- членов не обязательно. Однако в реальных программах такой способ хранения данных может оказаться достаточно эффективным. Важно четко поставить задачу, которую необходимо решить. Помните, что любая программа начинается с проектирования. Допустим, например, что требуется создать класс, членом которого является объект другого класса, причем второй объект может создаваться еще до возникновения первого и оставаться после его уничтожения. В этом случае доступ ко второму объекту должен осуществляться только по ссыпке, т.е. с использованием указателя. Допустим, первым объектом является окно, а вторым — документ. Вполне понятно, что окно должно иметь доступ к документу. С другой стороны, продолжительность существования документа никак не контролируется окном. Поэтому для окна важно хранить лишь ссылку на этот документ. Об использовании ссылок речь идет на затянии 9. Указатель thisКаждый метод класса имеет скрытый параметр — указатель this. Этот указатель содержит адрес текущего объекта. Рассмотренные в предыдущем разделе функции GetAge() и SetAge() также содержат этот параметр. В листинге 8.8 приведен пример использования указателя this в явном виде. Листинг 8.8. Указатель this 1: // Листинг 8.8. 2: // Указатель this 3: 4: #include <iostream.h> 5: 6: class Rectangle 7: { 8: public: 9: Rectangle(); 10: ~Rectangle(); 11: void SetLength(int length) { this->itsLength = length; } 12: int GetLength() const { return this->itsLength; } 13: 14: void SetWidth(int width) { itsWidth = width; } 15: int GetWidth() const { return itsWidth; } 16: 17: int itsLength 18: int itsWidth; 20: }; 21: 22: Rectangle::Rectangle() 23: { 24: itsWidth = 5; 25: itsLength = 10; 26: } 27: Rectangle::~Rectangle() 28: {} 29: 30: int main() 31: { 32: Rectangle theRect; 33: cout << "theRect is " << theRect.GetLength() << " meters long.\n"; 34: cout << "theRect is " << theRect.GetWidth() << " meters wide.\n"; 35: theRect.SetLength(20); 36: theRect.SetWidth(10); 37: cout << "theRect is " << theRect.GetLength() << " meters long.\n"; 38: cout << "theRect is " << theRect.GetWidth() << " meters wide.\n"; 39: return 0; 40: } Результат: theRect is 10 meters long. theRect s 5 meters wide. theRect is 20 meters long. theRect is 10 meters wide. Анализ: В функциях SetLength() и GetLength() при обращении к переменным класса Rectangle указатель this используется в явном виде. В функциях SetWidth() и GetWidth() такое обращение осуществляется неявно. Несмотря на различие в синтаксисе, оба варианта идентичны. На самом деле роль указателя this намного важнее, чем это может показаться. Поскольку this является указателем, он содержит адрес текущего объекта и в этой роли может оказаться достаточно мощным инструментом. При обсуждении проблемы перегрузки операторов (занятие 10) будет приведено несколько реальных примеров использования указателя this. В данный момент вам необходимо понимать, что this — это указатель, хранящий адрес объекта, в котором он используется. Память для указателя this не выделятся и не освобождается программно. Эту задачу берет на себя компилятор. Блуждающие, дикие или зависшие указателиБлуждающие указатели являются достаточно распространенной ошибкой программистов, обнаружить которую довольно сложно. Блуждающий (либо, как его еще называют, дикий или зависший) указатель возникает, если после удаления объекта оператором delete этому указателю не присвоить значение 0. При попытке использовать такой указатель в дальнейшем результат может оказаться непредсказуемым. В лучшем случае программа завершится сообщением об ошибке. Возникает ситуация, подобная следующей. Почтовая служба переехала в новый офис, а вы все еще продолжаете звонить по ее старому номеру телефона. Если этот номер будет просто отключен, это не приведет ни к каким негативным последствиям. А теперь представьте себе, что этот номер отдан какому-то военному заводу... Одним словом, будьте осторожны при использовании указателей, для которых вызывался оператор delete. Указатель по-прежнему будет содержать адрес области памяти, однако по этому адресу уже могут находиться другие данные. В этом случае обращение по указанному адресу может привести к аварийному завершению программы. Или, что еще хуже, программа может продолжать работать, а через несколько минут "зависнет". Такая ситуация получила название "мины замедленного действия" и является достаточно серьезной проблемой при написании программ. Поэтому во избежание неприятностей после освобождения указателя присваивайте ему значение 0. Пример возникновения блуждающего указателя показан в листинге 8.9. Листинг 8.9. Пример возникновения блуждающего указателя 1: // Листинг 8.9. 2: // Пример возникновения блуждающего указателя 3: typedef unsigned short int USHORT; 4: #include <iostream.h> 5: 6: int main() 7: { 8: USHORT * pInt = new USHORT; 9: *pInt = 10; 10: cout << "*pInt; " << *pInt << endl; 11: delete pInt; 12: 13: long * pLong = new long; 14: *pLong = 90000; 15: cout << "*pLong: " << *pLong << endl; 16: 17: *pInt = 20; // Присвоение освобожденному указателю 18: 19: cout << "*pInt: " << *pInt << endl; 20: cout << "*pLong: " << *pLong << endl; 21: delete pLong; 22: return 0; 23: } Результат: *pInt: 10 *pLong: 90000 *pInt: 20 *pLong: 65556 (Ваши результаты могут отличаться от приведенных.) Анализ: В строке 8 переменная pInt объявляется как указатель на тип USH0RT. Выделяется память для хранения этого типа данных. В строке 9 по адресу в этом указателе записывается значение 10, а в строке 10 оно выводится на экран. Затем память, выделенная для pInt, освобождается оператором delete. После этого указатель оказывается зависшим, или блуждающим. В сроке 13 объявляется новый указатель (pLong) и ему присваивается адрес выделенной оператором new области памяти. В строке 14 по адресу в указателе записывается число 90000, а в строке 15 это значение выводится на экран. В строке 20 по адресу, занесенному в pInt, записывается значение 20. Однако, вследствие того что выделенная для этого указателя память была освобождена, такая операция является некорректной. Последствия такого присваивания могут оказаться непредсказуемыми. В строке 19 новое значение pInt выводится на экран. Это значение, как и ожидалось, равно 20. В строке 20 выводится значение указателя pLong. К удивлению, обнаруживаем, что оно равно 65556. Возникает два вопроса. 1. Как могло измениться значение pLong, если мы его даже не использовали? 2. Куда делось число 20, присвоенное в строке 17? Как вы, наверное, догадались, эти два вопроса связаны. При присвоении в строке 17 число 20 записывается в область памяти, на которую до этого указывал pInt. Но, так как память была освобождена в строке 11, компилятор использовал эту область для записи других данных. При объявлении указателя pLong (строка 13) была зарезервирована область памяти, на которую раньше ссылался указатель pInt. Заметим, что на некоторых компьютерах этого могло не произойти. Записывая число 20 по адресу, на который до этого указывал pInt, мы искажаем значение pLong, хранящееся по этому же адресу. Вот к чему может привести некорректное использование блуждающих указателей. Локализовать такую ошибку достаточно сложно, поскольку искаженное значение никак не связано с блуждающим указателем. Результатом некорректного использования указателя pInt стало изменение значения pLong. В больших программах отследить возникновение подобной ситуации особенно сложно. В качестве небольшого отступления рассмотрим, как по адресу в указателе pLong оказалось число 65556. 1. Указатель pInt ссылается на область памяти, в которую мы записали число 10. 2. Оператором delete мы позволяем компилятору использовать эту память для хранения других данных. Далее по этому же адресу записывается значение pLong. 3. Переменной *pLong присваивается значение 90000. Поскольку в компьютере использовалось четырехбайтовое представление типа long с перестановкой байтов, на машинном уровне число 90000 (00 01 5F 90) выглядело как 5F 90 00 01. 4. Затем указателю pInt присваивается значение 20, что эквивалентно 00 14 в шестнадцатеричной системе. Вследствие того что указатели ссылаются на одну и ту же область памяти, два первых байта числа 90000 перезаписываются. В результате получаем ЧИСЛО 00 14 00 01. 5. При выводе на экран значения в указателе pLong порядок байтов изменяется на 00 01 00 14, что эквивалентно числу 65556. Рекомендуется:Создавайте объекты в области динамического обмена. Применяйте оператор delete для освобождения областей динамического обмена, которые больше не используются. Проверяйте значения, возвращаемые оператором new. Не рекомендуется:Не забывайте каждое выделение свободной памяти с помощью оператора new сопроводить освобождением памяти с помощью оператора delete. Незабывайте присваивать освобожденным указателям нулевые значения. Вопросы и ответы: Какая разница между пустым и блуждающим указателями? Когда вы применяете к указателю оператор delete, освобождается область динамической памяти; на которую ссылался этот указатель, но сам указатель продолжает при этом существовать, становясь блуждающим. Присваивая указателю нулевое значение, например следующим выражением: myPtr - 0:, вы тем самым превращаете блуждающий указатель в нулевой. Еще одна опасность блуждающих указателей состоит в том, что, дважды применив к одному и тому же указателю оператор delete, вы тем самым создадите неопределенную ситуацию, которая может привести к зависанию программы. Этого не случится, если освобожденному указателю будет присвоено нулевое значение. Присвоение освобожденному указателю — как блуждающему, так и нулевому — ново- го значения (т.е. использование выражения myPt r = 5) недопустимо, но если в случае с пустым указателем об этом вам сообщит компилятор, то в случае с блуждающим указателем вы узнаете об этом по зависанию программы в самый неподходящий момент. Использование ключевого слова const при объявлении указателейПри объявлении указателей допускается использование ключевого слова const перед спецификатором типа или после него. Корректны, например, следующие варианты объявления: const int * pOne; int * const pTwo; const int * const pThree; В этом примере pOne является указателем на константу типа int. Поэтому значение, на которое он указывает, изменять нельзя. Указатель pTwo является константным указателем на тип int. В этом случае значение, записанное по адресу в указателе, может изменяться, но сам адрес остается неизменным. И наконец, pThree объявлен как константный указатель на константу типа int. Это означает, что он всегда указывает на одну и ту же область памяти и значение, записанное по этому адресу, не может изменяться. В первую очередь необходимо понимать, какое именно значение объявляется константой. Если наименование типа переменной записано после ключевого слова const, значит, объявляемая переменная будет константой. Если же за словом const следует имя переменной, константой является указатель. const int * p1; // Укаэатоль на коисттпу типа ini int * const p2; // Константный указаюль, всегда указывающий на одну и ту же область памяти Использование ключевого слова const при объявлении указателей и функций-членовИспользование ключевого шва const при объявлении указателей и функции-членов На занятии 4 мы обсудили вопрос об использовании ключевого слова const при объявлении функций-членов классов. При объявлении функции константной попытка внести изменения в данные объекта с помощью этой функции будут пресекаться компилятором. Если указатель на объект объявлен константным, он может использоваться для вызова только тех методов, которые также объявлены со спецификатором const (листинг 8.10). Листинг 8.10. Указатели на константные объекты 1: // Листинг 8.10. 2: // Вызов константных методов с помощью указателей 3: 4: flinclude <iostream.h> 5: 6: class Rectangle 7: { 8: public: 9: Rectangle(); 10: ~Rectangle(); 11: void SetLength(int length) { itsLength = length; } 12: int GetLength() const { return itsLength; } 13: void SetWidth(int width) { itsWidth = width: } 14: int GetWidth() const { return itsWidth; } 15: 16: private: 17: int itsLength; 18: int itsWidth; 19: }; 20: 21: Rectangle::Rectangle() 22: { 23: itsWidth = 5; 24: itsLength = 10; 25: } 26: 27: Rectangle::~Rectangle() 28: { } 29: 30: int main() 31: { 32: Rectangle* pRect = new Rectangle; 33: const Rectangle * pConstRect = new Rectangle; 34: Rectangle * const pConstPtr = new Rectangle; 35: 36: cout << "pRect width; " << pRect->GetWidth() << " meters\n"; 37: cout << "pConstRect width: " << pConstRect-> GetWidth() << " meters\n"; 38: cout << "pConstPtr width: " << pConstPtr-> GetWidth() << " meters\n"; 39: 40: pRect->SetWidth(10); 41: // pConstRect->SetWidth(10); 42: pConstPt r->SetWidth(10); 43: 44: cout << "pRect width: " << pRect->GetWidth() << " meters\n"; 45: cout << "pConstRect width:"<< pConstRect->GetWidth() << " meters\n"; 46: cout << "pConstPtr width: " << pConstPtr->GetWidth() << " meters\n"; 47: return 0; 48: } Результат: pRect width: 5 meters pConstRect width: 5 meters pConstPtr width: 5 meters pRect width: 10 meters pConstRect width: 5 meters pConstPtr width: 10 meters Анализ: В строках 6—19 приведено описание класса Rectangle. Обратите внимание, что метод GetWidth(), описанный в строке 14, имеет спецификатор const. Затем в строке 32 объявляется указатель на объект класса Rectangle, а в строке 33 — на константный объект этого же класса. Константный указатель pConstPrt описывается в строке 34. В строках 36—38 значения переменных класса выводятся на экран. Метод SetWidth(), вызванный для указателя pRect (строка 40), устанавливает значение ширины объекта. В строке 41 показан пример использования указателя pConstRect для вызова метода класса. Но, так как pConstRect является указателем на константный объект, вызов методов без спецификатора const для него недоступен, поэтому данная строка закомментирована. В строке 42 происходит вызов метода SetWidth() для указателя pConstPrt. Этот указатель константный и может ссылаться только на одну область памяти, однако сам объект константным не является, поэтому данная операция полностью корректна. Рекомендуется:Проверяйте значения, возвращаемые функцией malloc(). Защищайте объекты, которые не должны изменяться в программе, с помощью ключевого слова const в случае передачи их как ссылок. Передавайте как ссылки те объекты, которые должны изменяться в программе. Передавайте как значения небольшие объекты, которые не должны изменяться в программе. Указатель const thisПосле объявлении константного объекта указатель this также будет использоваться как константный. Следует отметить, что использование указателя const this допускается только в методах, объявленных со спецификатором const. Более подробно этот вопрос рассматривается на следующем занятии при изучении ссылок на константные объекты. Вычисления с указателямиОдин указатель можно вычитать из другого. Если, например, два указателя ссылаются на разные элементы массива, вычитание одного указателя из другого позволяет получить количество элементов массива, находящихся между двумя заданными. Наиболее эффективно эта методика используется при обработке символьных массивов (листинг 8.11). Листинг 8.11. Выделение слов из массива символов 1: #include <iostream.h> 2: #include <ctype.h> 3: #include <string.h> 4: bool GetWord(char* string, char* word, int& wordOffset); 5: // основная программа 6: int main() 7: { 8: const int bufferSize = 255; 9: char buffer[bufferSize+1]; // переменная для хранения всей строки 10: char word[bufferSize+1]; // переменная для хранения слова 11: int wordOffset = 0; // начинаем с первого символа 12: 13: cout << "Enter а string: "; 14: cin.getline(buffer,bufferSize); 15: 16: while (GetWord(buffer,word,wordOffset)) 17: { 18: cout << "Got this word: " << word << endl; 19: } 20: 21: return 0; 22: 23: } 24: 25: 26: // Функция для выделения слова из строки символов. 27: bool GetWord(char* string, char* word, int& wordOffset) 28: { 29: 30: if (!string[wordOffset]) // определяет конец строки? 31: return false; 32: 33: char *p1, *p2; 34: p1 = p2 = string+wordOffset; // указатель на следующее слово 35: 36: // удаляем ведущие пробелы 37: for (int i = 0; i<(int)strlen(p1) && !isalnum(p1[0]); i++) 38: p1++; 39: 40: // проверка наличия слова 41: if (!iKalruj[n(pl[0])) 42: return false; 43: 44: // указатель р1 показание начало сдолующего слова 45: // iа к жо как и p2 46: p2 = p1; 47: 48: // перпмещавм p2 и конец олова 49: while (isalnum(p2[0])) 50: p2++; 51: 62: // p2 указывает на конец слова 53: // а p1 - в начало 54: // разность указатолой показываот длину слова 55: int len = int (p2 - p1); 56: 57: // копируем слово в буфер 58: strncpy (word,p1,len); 59: 60: // и добавляем символ разрыва сроки 61: word[len]='\0'; 62: 63: // ищем начало следующего слова 64: for (int i = int(p2-string); K(int)strlen(string) && !isalnum(p2[0]); i++) 65: p2++; 66: 67: wordOffset = int(p2-string); 68: 69: return true; 70: } Результат: Enter а string: this code first appeared jn C++ Report Got this word: this Got this word: code Got this word: first Got this word: appeared Got this word: in Got this word: C Got this word: Report Анализ: В строке 13 пользователю предлагается ввести строку. Строка считывается функцией GetWord(), параметрами которой является буферизированная переменная для хранения первого слова и целочисленная переменная WordOffset. В строке 11 переменной WordOffset присваивается значение 0. По мере ввода строки (до тех пор пока GetWord() не возвратит значение 0) введенные слова отображаются на экране. При каждом вызове функции GetWord() управление передается в строку 27. Далее, в строке 30, значение string[wordOffset ] проверяется на равенство нулю. Выполнение условия означает, что мы находимся за пределами строки. Функция GetWord() возвращает значение false. В строке 33 объявляются два указателя на переменную символьного типа. В строке 34 оба указателя устанавливаются на начало следующего слова, заданное значением переменной WordOffset. Исходно значение WordOffset равно 0, что соответствует началу строки. С помощью цикла в строках 37 и 38 указатель р1 перемещается на первый символ, являющийся буквой или цифрой. Если такой символ не найден, функция возвращает false (строки 41 и 42). Таким образом, указатель p1 соответствует началу очередного слова. Строка 46 присваивает указателю p2 то же значение. В строках 49 и 50 осуществляется поиск в строке первого символа, не являющегося ни цифрой, ни буквой. Указатель p2 перемещается на этот символ. Теперь p1 и p2 указывают на начало и конец слова соответственно. Вычтем из значения указателя p2 значение р1 и преобразуем результат к целочисленному типу. Результатом выполнения такой операции будет длина очередного слова (строка 55). Затем на основании данных о начале и длине полученное слово копируется в буферную переменную. Строкой 61 в конец слова добавляется концевой нулевой символ, служащий сигналом разрыва строки. Далее указатель p2 перемещается на начало следующего слова, а переменной WordOffset присваивается значение смещения начала очередного слова относительно начала строки. Возвращая значение true, мы сигнализируем о том, что слово найдено. Чтобы как можно лучше разобраться в работе программы, запустите ее в режиме отладки и последовательно, шаг за шагом, проконтролируйте выполнение каждой строки. РезюмеУказатели являются мощным средством непрямого доступа к данным. Каждая переменная имеет адрес, получить который можно с помощью оператора адреса (t). Для хранения адреса используются указатели. Для объявления указателя достаточно установить тип объекта, адрес которого он будет содержать, а затем ввести символ "*" и имя указателя. После объявления указатель следует инициализировать. Если адрес объекта неизвестен, указатель инициализируется значением 0. Для доступа к значению, записанному по адресу в указателе, используется оператор разыменования (*). Указатель можно объявлять константным. В этом случае не допускается присвоение данному указателю нового адреса. Указатель, хранящий адрес константного объекта, не может использоваться для изменения этого объекта. Чтобы выделить память для хранения какого-либо объекта, используется оператор new, а затем полученный адрес присваивается указателю. Для освобождения зарезервированной памяти используется оператор delete. Сам указатель при освобождении памяти не уничтожается, поэтому освобожденному указателю необходимо присвоить нулевое значение, чтобы обезопасить его. Вопросы и ответыВ чем состоит преимущество работы с указателями? На этом занятии вы узнали, насколько удобно использовать доступ к объекту по его адресу и передавать параметры как ссылки. На занятии 13 будет рассмотрена роль указателей в полиморфизме классов. Чем удобно размещение объектов в динамической области памяти? Объекты, сохраненные в области динамического обмена, не уничтожаются при выходе из функции, в которой они были объявлены. Кроме того, это дает возможность уже в процессе выполнения программы решать, какое количество объектов требуется объявить. Более подробно этот вопрос обсуждается на следующем занятии. Зачем ограничивать права доступа к объекту, объявляя его константным? Следует использовать все средства, позволяющие предотвратить появление ошибок. На практике достаточно сложно отследить, в какой момент и какой функцией изменяется объект. Использование спецификатора const позволяет решить эту проблему. КоллоквиумВ этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний, а также рассматривается ряд упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из предложенных ниже вопросов. Контрольные вопросы1. Какой оператор используется для получения адреса переменной? 2. Какой оператор позволяет получить значение, записанное по адресу, содержащемуся в указателе? 3. Что такое указатель? 4. В чем различие между адресом, который хранится в указателе, и значением, записанным по этому адресу? 5. В чем различие между оператором разыменования и оператором получения адреса? 6. В чем различие между следующими объявлениями: const int * ptrOne и int * const ptrTwo? Упражнения1. Объясните смысл следующих объявлений переменных: • int * pOne • int vTwo • int * pThree = &vTwo 2. Допустим, в программе объявлена переменная yourAge типа unsigned short. Как объявить указатель, позволяющий манипулировать этой переменной? 3. С помошью указателя присвойте переменной yourAge значение 50. 4. Напишите небольшую программу и объявите в ней переменную типа int и указатель на этот тип. Сохраните адрес переменной в указателе. Используя указатель, присвойте переменной какое-либо значение, 5. Жучки: найдите ошибку в следующем фрагменте программы. #include <iostream,h> int main() { int *pInt; *pInt = 9; cout << "The value at pInt: " << *pInt; return 0; } 6. Жучки: найдите ошибку в следующем фрагменте программы. int main() { int SomeVariable = 5; cout << "SomeVariable: " << SomeVariable << "\n"; int *pVar = & SomeVariable; pVar = 9; cout << "SomeVariable: " << *pVar << "\n"; return 0; } День 9-й. СсылкиНа предыдущем занятии вы узнали, как пользоваться указателями для управления объектами в свободной памяти и как ссылаться на эти объекты косвенным образом. Ссылки, которые обсуждаются на этом занятии, обладают почти теми же возможностями, что и указатели, но при более простом синтаксисе. Сегодня вы узнаете: • Что представляют собой ссылки • Чем ссылки отличаются ог указателей • Как создать ссылки и использовать их • Какие ограничения есть у ссылок • Как по ссылке передаются значения и объекты в функции и из функций Что такое ссылкаСсылка — это то же, что и псевдоним. При создании ссылки мы инициализируем ее с помощью имени другого объекта, адресата. С этого момента ссылка действует как альтернативное имя данного объекта, поэтому все, что делается со ссылкой, в действительности происходит с этим объектом. Для объявления ссылки нужно указать типа объекта адресата, за которым следует оператор ссылки (&), а за ним — имя ссылки. Для ссылок можно использовать любое легальное имя переменной, но многие программисты предпочитают со всеми именами ссылок использовать префикс "г". Так, если у вас есть целочисленная переменная с именем someInt, вы можете создать ссылку на эту переменную, написав int &rSomeRef = someInt; Это читается следующим образом: rSomeRef — это ссылка на целочисленное значение, инициализированная адресом переменной someInt. Создание и использование ссылок показано в листинге 9.1. Примечание:Обратите внимание на то, что оператор ссылки (&) выглядит так же, как оператор адреса, который используется для возвращения адреса при работе с указателями. Однако это не одинаковые операторы, хотя очевидно, что они родственны. Листинг 9.1. Создание и использование ссылок 1: // Листинг 9.1. 2: // Пример использования ссылок 3: 4: #include <iostream.h> 5: 6: int main() 7: { 8: int intOne; 9: int &rSomeRef = intOne; 10: 11: intOne = 5; 12: cout << "intOne: " << intOne << endl; 13: cout << "rSomeRef: " << rSomeRef << endl; 14: 15: rSomeRef = 7; 16: cout << "intOne: " << intOne << endl; 17: cout << "rSomeRef: " << rSomeRef << endl; 18: return 0; 19: } Результат: intOne: 5 rSomeRef: 5 intOne: 7 rSomeRef: 7 Анализ: В строке 8 объявляется локальная целочисленная переменная intOne, а в строке 9 — ссылка rSomeRef на некоторое целое значение, инициализируемая адресом переменной intOne. Если объявить ссылку, но не инициализировать ее, будет сгенерирована ошибка компиляции. Ссылки, в отличие от указателя, необходимо инициализировать при объявлении. В строке 11 переменной intOne присваивается значение 5. В строках 12 и 13 выводятся на экран значения переменной intOne и ссылки rSomeRef - они, конечно же, оказываются одинаковыми. В строке 15 ссылке rSomeRef присваивается значение 7. Поскольку мы имеем дело со ссылкой, а она является псевдонимом для переменной intOne, то число 7 в действительности присваивается переменной intOne, что и подтверждается выводом на экран в строках 16 и 17. Использование оператора адреса (&) при работе со ссылкамиЕсли использовать ссылку для получения адреса, она вернет адрес своего адресата. В этом и состоит природа ссылок. Они являются псевдонимами для своих адресатов. Именно это свойство и демонстрирует листинг 9.2. Листинг 9.2. Взятие адреса ссылки 1: // Листинг 9.2. 2: // Пример использования ссылок 3: 4: #include <iostream.h> 5: 6: int main() 7: { 8: int intOne; 9: int &rSomeRef = intOne; 10: 11: intOne = 5; 12: cout << "intOne: " << intOne << endl; 13: cout << "rSomeRef: " << rSomeRef << endl; 14: 15: cout << "&intOne: " << &mtOne << endl; 16: cout << "&rSomeRef: " << &rSomeRef << endl; 17: 18: return 0; 19: } Результат: intOne: 5 rSomeRef: 5 &intOne: 0x3500 &rSomeRef: 0x3500 Примечание:Результаты работы программы на вашем компьютере могут отличаться от приведенных в последних двух строках. Анализ: И вновь-таки ссылка rSomeRef инициализируется адресом переменной intOno. На этот раз выводятся адреса двух переменных, и они оказываются идентичными. В языке C++ не предусмотрено предоставление доступа к адресу самой ссылки, поскольку в этом нет смысла. С таким же успехом для этого можно было бы использовать указатель или другую переменную. Ссылки инициализируются при создании, и они всегда действуют как синонимы для своих адресатов, даже в том случае, когда применяется оператор адреса. Например, если у вас есть класс с именем City, вы могли бы объявить объект этого класса следующим образом: City boston; Затем можно объявить ссылку на некоторый объект класса City и инициализировать ее, используя данный конкретный объект: City &beanTown = boston; Существует только один класс City; оба идентификатора ссылаются на один и тот же объект одного и того же класса. Любое действие, которое вы предпримите относительно ссылки beanTown, будет выполнено также и над объектом boston. Обратите внимание на различие между символом & в строке 9 листинга 9.2, который объявляет ссылку rSomeRef на значение типа int, и символами & в строках 15 и 16, которые возвращают адреса целочисленной переменной intOne и ссылки rSomeRef. Обычно для ссылки оператор адреса не используется. Мы просто используем ссылку вместо связанной с ней переменной, как показано в строке 13. Ссылки нельзя переназначатьДаже опытных программистов, которые хорошо знают правило о том, что ссылки нельзя переназначать и что они всегда являются псевдонимами для своих адресатов, может ввести в заблуждение происходящее при попытке переназначить ссылку. То, что кажется переназначением, оказывается присвоением нового значения адресату. Этот факт иллюстрируется в листинге 9.3. Листинг 9.3. Присвоение значения ссылке 1: // Листинг 9.3 2: // Присвоение значения ссылке 3: 4: #include <iostream.h> 5: 6: int main() 7: { 8: int intOne; 9: int &rSomeRef = intOne; 10: 11: intOne: 5 12: cout << "intOne:\t" << intOne << endl; 13: cout << "rSomeRef:\t" << rSomeRef << endl; 14: cout << "&intOne:\t" << &intOne << endl; 15: cout << "&rSomeRef:\t" << &rSomeRef << endl; 16: 17: int intTwo = 8; 18: rSomeRef = intTwo; // не то что вы думаете 19: cout << "\nintOne:\t" << intOne << endl; 20: cout << "intTwo:\t" << intTwo << endl; 21: cout << "rSomeRef:\t" << rSomeRef << endl; 22: cout << "&intOne:\t" << &intOne << endl; 23: cout << "&intTwo:\t" << &intTwo << endl; 24: cout << "&rSomeRef:\t" << &rSomeRef << endl; 25: return 0; 26: } Результат: intOne: 5 rSomeRef: 5 &intOne: 0x213e &rSomeRef: 0x213e intOne: 8 int:Two: 8 rSomeRef: 8 &intOne: 0x213e &intTwo: 0x2130 &rSomeRef: 0x213e Анализ: Вновь в строках 8 и 9 объявляются целочисленная переменная и ссылка на целое значение. В строке 11 целочисленной переменной присваивается значение 5, а в строках 12-15 выводятся значения переменной и ссылки, а также их адреса. В строке 17 создается новая переменная intTwo, которая тут же инициализируется значением 8. В строке 18 программист пытается переназначить ссылку rSomeRef так, чтобы она стала псевдонимом переменной intTwo, но этого не происходит. На самом же деле ссылка rSomeRef продолжает действовать как псевдоним переменной intOne, поэтому такое присвоение эквивалентно следующей операции: intOne = intTwo; Это кажется достаточно убедительным, особенно при выводе на экран значений переменной intOne и ссылки rSomeRef (строки 19— 21): их значения совпадают со значением переменной intTwo. На самом деле при выводе на экран адресов в строках 22—24 вы видите, что ссылка rSomeRef продолжает ссылаться на переменную intOne, а не на переменную intTwo. Не рекомендуется:Не пытайтесь переназначить ссылку. Не путайте оператор адреса с оператором ссылки. Рекомендуется:Используйте ссылки для создания псевдонимов объектов. Инициализируйте ссылки при объявлении. На что можно ссылатьсяСсылаться можно на любой объект, включая нестандартные (определенные пользователем) объекты. Обратите внимание, что ссылка создается на объект, а не на класс. Нельзя объявить ссылку таким образом: int & rIntRef = int; // неверно Ссылку rIntRef нужно инициализировать, используя конкретную целочисленную переменную, например: int howBig = 200: int & rIntRef = howBig; Точно так же нельзя инициализировать ссылку классом CAT: CAT & rCatRef = CAT; // неверно Ссылку rCatRef нужно инициализировать, используя конкретный объект класса CAT: CAT frisky; CAT & rCatRef = frisky; Ссылки на объекты используются точно так же, как сами объекты. Доступ к данным-членам и методам осуществляется с помощью обычного оператора доступа к членам класса (.), и, подобно встроенным типам, ссылка действует как псевдоним для объекта. Этот факт иллюстрируется в листинге 9.4. Листинг 9.4. Ссылки на объекты класса 1: // Листинг 9.4. 2: // Ссылки на объекты класса 3: 4: #include <iostream.h> 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat(int age, int weight); 10: ~SimpleCat() {} 11: int GetAge() { return itsAge; } 12: int GetWeight() { return itsWeight; } 13: private: 14: int itsAge; 15: int itsWeight; 16: } 17: 18: SimpleCat::SimpleCat(int age, int weight) 19: { 20: itsAge = age; 21: itsWeight = weight; 22: } 23: 24: int main() 25: { 26: SimpleCat Fnsky(5,3); 27: SimpleCat & rCat = Fnsky; 28: 29: cout << "Frisky: "; 30: cout << Frisky.GetAge() << " years old. \n"; 31: cout << "И Frisky весит: "; 32: cout << rCat.GetWeight() << " kilograms. \n"; 33: return 0; 34: } Результат: Frisky: 5 years old. И Frisky весит: 3 kilograms. Анализ: В строке 26 объявляется переменная Frisky в качестве объекта класса SimplcCat. В строке 27 объявляется ссылка rCat на некоторый объект класса SimpleCat, и эта ссылка инициализируется с использованием уже объявленного объекта Frisky. В строках 30 и 32 вызываются методы доступа к членам класса SimpleCat, причем сначала это делается с помощью объекта класса SimpleCat (Frisky), а затем с помощью ссылки на объект класса SimpleCat (rCat). Обратите внимание, что результаты идентичны. Снова повторюсь: ссылка — это всего лишь псевдоним реального объекта. Объявление ссылок Ссылка объявляется путем указания типа данных, за которым следует оператор ссылки (&) и имя ссылки. Ссылки нужно инициализировать при объявлении. Пример 1: int hisAge; int &rAge = hisAge; Пример 2: CAT boots; CAT &rCatRef = boots; Нулевые указатели и нулевые ссылкиКогда указатели не инициализированы или когда они освобождены, им следует присваивать нулевое значение (0). Это не касается ссылок. На самом деле ссылка не может быть нулевой, и программа, содержащая ссылку на нулевой объект, считается некорректной. Во время работы некорректной программы может случиться все что угодно. Она может внешне вести себя вполне пристойно, но при этом удалит все файлы на вашем диске или выкинет еще какой-нибудь фокус. Большинство компиляторов могут поддерживать нулевой объект, ничего не сообщая по этому поводу до тех пор, пока вы не попытаетесь каким-то образом использовать этот объект. Однако не советую пользоваться поблажками компилятора, поскольку они могут дорого вам обойтись во время выполнения программы. Передача аргументов функций как ссылокНа занятии 5 вы узнали о том, что функции имеют два ограничения: аргументы передаются как значения и теряют связь с исходными данными, а возвращать функция может только одно значение. Преодолеть эти два ограничения можно путем передачи функции аргументов как ссылок. В языке C++ передача данных как ссылок осуществляется двумя способами: с помощью указателей и с помощью ссылок. Не запутайтесь в терминах: вы можете передать аргумент как ссылку, используя либо указатель, либо ссылку. Несмотря на то что синтаксис использования указателя отличается от синтаксиса использования ссылки, конечный эффект одинаков. Вместо копии, создаваемой в пределах области видимости функции, в функцию передается реальный исходный объект. На занятии 5 вы узнали, что параметры, передаваемые функции, помешаются в стек. Если функции передается значение как ссылка (с помощью либо указателей, либо ссылок), то в стек помещается не сам объект, а его адрес. В действительности в некоторых компьютерах адрес хранится в специальном регистре, а в стек ничего не помещается. В любом случае компилятору известно, как добраться до исходного объекта, и при необходимости все изменения производятся прямо над объектом, а не над его временной копией. При передаче объекта как ссылки функция может изменять объект, просто ссылаясь на него. Вспомните, что в листинге 5.5 (см. Занятие 5) демонстрировалось, что после обращения к функции swap() значения в вызывающей функции не изменялись. Исключительно ради вашего удобства этот листинг воспроизведен здесь еще раз (листинг 9.5). Листинг 9.5. Демонстрация передачи по значению 1: // Листинг 9.5. Передача параметров как значений 2: 3: #include <iostrearn.h> 4: 5: void swap(int x, int у); 6: 7: int main() 8: { 9: int x = 5, у = 10; 10: 11: cout << "Main. Before swap, x: " << x << " у: " << у << "\n"; 12: swap(x,у); 13: cout << "Main. After swap, x: " << x << " у: " << у << "\n"; 14: return 0; 15: } 16: 17: void swap(int x, int у); 18: { 19: int temp; 20: 21: cout << "Swap. After swap, x; " << x << " у: " << у << "\n"; 22: 23: temp = x; 24: x = у; 25: у = temp; 26: 27: cout << "Swap. Before swap, x: " << x << " у: " << у << "\n"; 28: 29: } Результат: Main. Before swap, x: 5 у: 10 Swap. Before swap, x: 5 у: 10 Swap. After swap, x: 10 у: 5 Main. After swap, x: 5 у: 10 Эта программа инициализирует две переменные в функции main(), а затем передает их функции swap(), которая, казалось бы, должна поменять их значения. Однако после повторной проверки этих переменных в функции main() оказывается, что они не изменились. Проблема здесь в том, что переменные x и у передаются функции swap() по значению, т.е. в данном случае локальные копии этих переменных создаются прямо в функции. Чтобы решить проблему, нужно передать значения переменных x и у как ссылки, В языке C++ существует два способа решения этой проблемы: можно сделать параметры функции swap() указателями на исходные значения или передать ссылки на исходные значения. Передача указателей в функцию swap()Передавая указатель, мы передаем адрес объекта, а следовательно, функция может манипулировать значением, находящимся по этому переданному адресу. Чтобы заставить функцию swap() изменить реальные значения с помощью указателей, ее нужно объявить так, чтобы она принимала два указателя на целые значения. Затем путем разыменования указателей значения переменных x и у будут на самом деле меняться местами. Эта идея демонстрируется в листинге 9.6. Листинг 9.6. Передача аргументов как ссылок с помощью указателей 1: // Листинг 9.6. Пример передечи аргументов как ссылок 2: 3: #include <iostream.h> 4: 5: void swap (int *x, int *y) 6: 7: int main() 8: { 9: int x = 5, у = 10; 10: 11: cout << "Main. Before swap, x: " << x << " у: " << у << "\n"; 12: swap(&x,&y); 13: cout << "Main. After swap, x: " << x << " у: " << у << "\n"; 14: return 0; 15: } 16: 17: void swap (int *px, int *py) 18: { 19: int temp; 20: 21: cout << "Swap. Before swap, *рх: " << *px << " *py: " << *py << "\n"; 22: 23: temp = *px; 24: *px = *py; 25: *py = temp; 26: 27: cout << "Swap. After swap, *px: " << *px << " *py: " << *py << "\n"; 28: 29: } Результат: Main. Before swap, x: 5 y: 10 Swap. Before swap, *px: 5 *py: 10 Swap. After swap, *px: 10 *py: 5 Main. After swap, x: 10 y: 5 Анализ: Получилось! В строке 5 изменен прототип функции swap() где в качестве параметров объявляются указатели на значения типа int, а не сами переменные типа int. При вызове в строке 12 функции swap() в качестве параметров передаются адреса переменных x и у. В строке 19 объявляется локальная для функции swap() переменная temp, которой вовсе не обязательно быть указателем: она будет просто хранить значение *px (т.е. значение переменной x в вызывающей функции) в течение жизни функции. После окончания работы функции переменная temp больше не нужна. В строке 23 переменной temp присваивается значение, хранящееся по адресу px. В строке 24 значение, хранящееся по адресу px, записывается в ячейку с адресом py. В строке 25 значение, оставленное на время в переменной temp (т.е. исходное значение, хранящееся по адресу px), помещается в ячейку с адресом py. В результате значения переменных вызывающей функции, адреса которых были переданы функции swap(), успешно поменялись местами. Передача ссылок в функцию swap()Приведенная выше программа, конечно же, работает, но синтаксис функции swap() несколько громоздок. Во-первых, необходимость неоднократно разыменовывать указатели внутри функции swap() создает благоприятную почву для возникновения ошибок, кроме того, операции разыменовывания трудно читаются. Во-вторых, необходимость передавать адреса переменных из вызывающей функции нарушает принцип инкапсуляции выполнения функции swap(). Суть программирования на языке C++ состоит в сокрытии от пользователей функции деталей ее выполнения. Передача параметров с помощью указателей перекладывает ответственность за получение адресов переменных на вызывающую функцию, вместо того чтобы сделать это в теле вызываемой функции. Другое решение той же задачи предлагается в листинге 9.7, в котором показана работа функции swap() с использованием ссылок. Листинг 9.7. Та же фцнкция swap(), но с использованием ссылок 1: // Листинг 9.7. Пример передачи аргументов как 2: // ссылок с помощью ссылок! 3: 4: #include <iostream.h> 5: 6: void swap(int &x, int &y); 7: 8: int main() 9: { 10: int x = 5, у = 10; 11: 12: cout << "Main. Before swap, x: " << x << " у: " << у << "\n"; 13: swap(x,у); 14: cout << "Main. After swap, x: " << x << " у: " << у << "\n"; 15: return 0; 16: } 17: 18: void swap(int &rx, int &ry) 19: { 20: int temp; 21: 22: cout << "Swap. Before swap, rx: " << rx << " ry: " << ry << "\n"; 23: 24: temp = rx; 25: rx = ry; 26: ry = temp; 27: 28: cout << "Swap. After swap, rx: " << rx << " ry: " << ry << "\n"; 29: 30: } Результат: Main. Before swap, x:5 y: 10 Swap. Before swap, rx:5 ry:10 Swap. After swap, rx:10 ry:5 Main. After swap, x:10, y:5 Анализ: Точно так же, как и в примере с указателями, в строке 10 объявляются две переменные, а их значения выводятся на экран в строке 12. В строке 13 вызывается функция swap(), но обратите внимание на то, что ей передаются именно значения x и у, а не их адреса. Вызывающая функция просто передает свои переменные. После вызова функции swap() выполнение программы переходит к строке 18, в которой эти переменные идентифицируются как ссылки. Их значения выводятся на экран в строке 22, но заметьте, что для этого не требуется никаких специальных операторов, поскольку мы имеем дело с псевдонимами для исходных значений и используем их в этом качестве. В строках 24—26 выполняется обмен значений, после чего они выводятся на экран в строке 28. Управление программой вновь возвращается в вызывающую функцию, и в строке 14 эти значения опять выводятся на экран, но уже в функции main(). Поскольку параметры для функции swap() объявлены как ссылки, то и переменные из функции main() передаются как ссылки, следовательно, они также изменяются и в функции main(). Таким образом, благодаря использованию ссылок функция приобретает новую возможность изменять исходные данные в вызывающей функции, хотя при этом сам вызов функции ничем не отличается от обычного! Представления о заголовках функций и прототипахВ листинге 9.6 показана функция swap(), использующая в качестве аргументов указатели, а в листинге 9.7 — та же функция, но с использованием ссылок. Использовать функцию, которая принимает в качестве параметров ссылки, легче, да и в программе такая функция проще читается, но как вызывающей функции узнать о том, каким способом переданы параметры — по ссылке или по значению? Будучи клиентом (или пользователем) функции swap(), программист должен быть уверен в том, что функция swap() на самом деле изменит параметры. Самое время вспомнить о прототипе функции, для которого в данном контексте нашлось еще одно применение. Изучив параметры, объявленные в прототипе, который обычно располагается в файле заголовка вместе со всеми другими прототипами, программист будет точно знать, что значения, принимаемые функцией swap(), передаются как ссылки, следовательно, обмен значений произойдет должным образом. Если бы функция swap() была функцией-членом класса, то объявление этого класса, также расположенное в файле заголовка, обязательно содержало бы эту информацию. В языке C++ клиенты классов и функций всю необходимую информацию черпают из файлов заголовков. Этот файл выполняет роль интерфейса с классом или функцией, действительная реализация которых скрыта от клиента. Это позволяет программисту сосредоточиться на собственных проблемах и использовать класс или функцию, не вникая в детали их работы. Когда Колонел Джон Роблинг (Colonel John Roebling) проектировал свой Бруклинский мост (Brooklyn Bridge), он интересовался деталями процесса литья и изготовления проводов. Он глубоко вникал в подробности механических и химических процессов, которые требовалось обеспечить для создания необходимых материалов. Но в наши дни инженеры более эффективно используют свое рабочее время, доверяя информации о строительных материалах и не интересуясь подробностями их изготовления. В этом и состоит цель языка C++ — позволить программистам полагаться на описанные классы и функции, не вникая во внутренние механизмы их действия. Эти составные части можно собрать в одну программу, подобно тому как строители из отдельных блоков, проводов, труб, кирпичей и других элементов создают дома и мосты. Подобно инженеру, изучающему технические характеристики трубы, чтобы узнать ее пропускную способность, объем, размеры арматуры и пр., программист читает интерфейсы функций и классов, чтобы определить, какой сервис предоставляет данный компонент, какие параметры он принимает и какие значения возвращает. Возвращение нескольких значенийКак упоминалось выше, функции могут возвращать только одно значение. Что же делать, если нужно получить от функции сразу два значения? Один путь решения этой проблемы — передача функции двух объектов как ссылок. В ходе выполнения функция присвоит этим объектам нужные значения. Факт передачи объектов как ссылок, позволяющий функции изменить исходные объекты, равносилен разрешению данной функции возвратить два значения. В этом случае мы обходимся без возвращаемого значения, которое (зачем же пропадать добру) можно использовать для сообщения об ошибках. И вновь одинакового результата можно достичь, используя как ссылки, так и указатели. В листинге 9.8 показана функция, которая возвращает три значения: два в виде параметров-указателей и одно в виде возвращаемого значения функции. Листинг 9.8. Возвращение значений с помощью указателей 1: // Листинг 9.8. 2: // Возвращение нескольких значений из функции с помощью указателей 3: 4: #include <iostream.h> 5: int 6: short Factor(int n, int* pSquared, int* pCubed); 7: 8: int main() 9: { 10: int number, squared, cubed; 11: short error; 12: 13: cout << "Enter a number (0 - 20): "; 14: cin >> number; 15: 16: error = Factor(number, &squared, &cubed); 17: 18: if (!error) 19: { 20: cout << "number: " << number << "\n"; 21: cout << "square: " << squared << "\n"; 22: cout << "cubed: " << cubed << "\n"; 23: } 24: else 25: cout << "Error encountered!!\n"; 26: return 0; 27: } 28: 29: short Factor(int n, int *pSquared, int *pCubed) 30: { 31: short Value = 0; 32: if (n > 20) 33: Value = 1; 34: else 35: { 36: *pSquared = n*n; 37: *pCubed = n*ri*n; 38: Value = 0; 39: } 40: return Value; 41: } Результат: Enter a number (0-20): 3 number: 3 square: 9 cubed: 27 Анализ: В строке 10 переменные number, squared и cubed определяются с использованием типа int. Переменной number присваивается значение, введенное пользователем. Это значение, а также адреса переменных squared и cubed передаются функции Factor() в виде параметров. В функции Factor() анализируется первый параметр, который передается как значение. Если он больше 20 (максимальное значение, которое может обработать эта функция), то возвращаемое значение Value устанавливается равным единице, что служит признаком ошибки. Обратите внимание на то, что возвращаемое значение из функции Factor() может принимать либо значение 1, либо 0, являющееся признаком того, что все прошло нормально, а также заметьте, что функция возвращает это значение лишь в строке 40. Итак, искомые значения (квадрат и куб заданного числа) возвращаются в вызывающую функцию не путем использования механизма возврата значений, а за счет изменения значений переменных, указатели которых переданы в функцию. В строках 36 и 37 посредством указателей переменным в функции main() присваиваются возвращаемые значения. В строке 38 переменной Value присваивается значение возврата, означающее успешное завершение работы функции. В строке 40 это значение Value возвращается вызывающей функции. Эту программу можно слегка усовершенствовать, дополнив ее следующим объявлением: enum ERROR_VALUE { SUCCESS, FAILURE} ; Затем вместо возврата значений 0 или 1 эта программа сможет возвращать SUCCESS ИЛИ FAILURE. Возвращение значений с помощью ссылокНесмотря на то что листинг 9.8 прекрасно работает, его можно упростить как для чтения, так и в эксплуатации, если вместо указателей использовать ссылки. В листинге 9.9 показана та же самая программа, но вместо указателей в качестве параметров функции в ней используются ссылки, а также добавлено упомянутое выше перечисление ERROR. Листинг 9.9. Возвращение значений с помощью ссылок 1: // Листинг 9.9. 2: // Возвращение нескольких значений из функции 3: // с помощью ссылок 4: 5: #include <iostream.h> 6: 7: typedef unsigned short USHORT; 8: enum ERR_CODE { SUCCESS, ERROR } 9: 10: ERR_CODE Factor(USHORT, USHORT&, USHORT&); 11: 12: int main() 13: { 14: USHORT number, sguared, cubed; 15: ERR__CODE result; 16: 17: cout << "Enter а number (0 - 20): "; 18: cin >> number; 19: 20: result = Factor(number, squared, cubed); 21: 22: if (result == SUCCESS) 23: { 24: cout << "number: " << number << "\n"; 25: cout << "square: " << squared << "\n"; 26: cout << "cubed: " << cubed << "\n"; 27: } 28: else 29: cout << "Error encountered!!\n"; 30: return 0; 31: } 32: 33: ERR_CODE Factor(USHORT n, USHORT &rSquared, USHORT &rCubed) 34: { 35: if (n > 20) 36: return ERROR; // simple error code 37: else 38: { 39: rSquared = n*n; 40: rCubed = n*n*n; 41: return SUCCESS; 42: } 43: } Результат: Enter a number (0 - 20): 3 number: 3 square: 9 cubed: 27 Анализ: Листинг 9.9 идентичен листингу 9.8 с двумя исключениями. Перечисление ERR_CODE делает сообщение об ошибке более явным (см. строки 36 и 41), как, впрочем, и его обработку (строка 22). Однако более существенные изменения коснулись функции Factor(). Теперь эта функция объявляется для принятия не указателей, а ссылок на переменные squared и cubed, что делает манипуляции над этими параметрами гораздо проще и легче для понимания. Передача ссылок на переменные как средство повышения эффективностиПри каждой передаче объекта в функцию как значения создается копия этого объекта. При каждом возврате объекта из функции создается еще одна копия. На занятии 5 вы узнали о том, что эти объекты копируются в стек и на этот процесс расходуется время и память. Для таких маленьких объектов, как базовые целочисленные значения, цена этих расходов незначительна. Однако для больших объектов, создаваемых пользователем, расходы ресурсов существенно возрастают. Размер такого объекта в стеке представляет собой сумму всех его переменных-членов. Причем каждая переменная-член может быть, в свою очередь, подобным объектом, поэтому передача такой массивной структуры путем копирования в стек может оказаться весьма дорогим удовольствием как по времени, так и по занимаемой памяти. Кроме того, существуют и другие расходы. При создании временных копий объектов классов для этих целей компилятор вызывает специальный конструктор-копировщик. ,Ha следующем занятии вы узнаете, как работают конструкторы-копировщики и как можно создать собственный конструктор-копировщик, но пока достаточно знать, что конструктор-копировщик вызывается каждый раз, когда в стек помещается временная копия объекта. При разрушении временного объекта, которое происходит при возврате из функции, вызывается деструктор объекта. Если объект возвращается функцией как значение, копия этого объекта должна быть сначала создана, а затем разрушена. При работе с большими объектами эти вызовы конструктора и деструктора могут оказать слишком ощутимое влияние на скорость работы программы и использование памяти компьютера. Для иллюстрации этой идеи в листинге 9.10 создается пользовательский объект SimpleCat. Реальный объект имел бы размеры побольше и обошелся бы дороже, но и этого примера вполне достаточно, чтобы показать, насколько часто вызываются конструктор-копировщик и деструктор. Итак, в листинге 9.10 создается объект SimpleCat, после чего вызываются две функции. Первая функция принимает объект Cat как значение, а затем возвращает его как значение. Вторая же функция принимает указатель на объект, а не сам объект, и возвращает указатель на объект. Листинг 9.10. Передача объектов как ссылок с помощью указателей 1: // Листинг 9.10. 2: // Передача указателей на объекты 3: 4: #include <iostream.h> 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat (); // конструктор 10: SimpleCat(SimpleCat&); // конструктор-копировщик 11: ~SimpleCat(); // деструктор 12: }; 13: 14: SimpleCat::SimpleCat() 15: { 16: cout << "Simple Cat Constructor...\n"; 17: } 18: 19: SimpleCat::SimpleCat(SimpleCat&) 20: { 21: cout << "Simple Cat Copy Constructor...\n"; 22: } 23: 24: SimpleCat::~SimpleCat() 25: { 26: cout << "Simple Cat Destructor...\n"; 27: } 28: 29: SimpleCat Function0ne (SimpleCat theCat); 30: SimpleCat* FunctionTwo (SimpleCat *theCat); 31: 32: int main() 33: { 34: cout << "Making a cat,.,\n"; 35: SimpleCat Frisky; 36: cout << "Calling FunctionOne,,,\n"; 37: FunctionOne(Frisky); 38: cout << "Calling FunctionTwo..,\n"; 39: FunctionTwo(&Frisky); 40: return 0; 41: } 42: 43: // Функция FunctionOne, передача как значения 44: SimpleCat FunctionOne(SimpleCat theCat) 45: { 46: cout << "Function One. Roturning,,,\ri"; 47: return theCat; 48: } 49: 50: // Функция FunctionTwo, передача как ссылки 51: SimpleCat* FunctionTwo (SimpleCat *theCat) 52: { 53: cout << "Function Two. Returning...\n"; 54: return theCat; 55: } Результат: Making a cat... Simple Cat Constructor... Calling FunctionOne... Simple Cat Copy Constructor... Function One. Returning... Simple Cat Copy Constructor... Simple Cat Destructor... Simple Cat Destructor... Calling FunctionTwo... Function Two. Returning... Simple Cat Destructor... Примечание:Номера строк не выводятся. Мы добавили их для удобства проведения анализа программы. Анализ: В строках 6-12 объявляется весьма упрошенный класс SimpleCat. Конструктор, конструктор-копировщик и деструктор — все компоненты класса выводят на экран свои информативные сообщения, чтобы было точно известно, когда они вызываются. В строке 34 функция main() выводит свое первое сообщение, которое является первым и в результатах работы программы. В строке 35 создается экземпляр объекта класса SimpleCat. Это приводит к вызову конструктора, что подтверждает сообщение, выводимое этим конструктором (строка 2 в результатах работы программы). В строке 36 функция main() "докладывает" о вызове функции FunctionOne, которая выводит свое сообщение (строка 3 в результатах работы программы). Поскольку функция FunctionOne() вызывается с передачей объекта класса SimpleCat по значению, в стек помещается копия объекта SimpleCat как локального для вызываемой функции. Это приводит к вызову конструктора копии, который "вносит свою лепту" в результаты работы программы (сообщение в строке 4). Выполнение программы переходит к строке 46, которая принадлежит телу вызванной функции, выводящей свое информативное сообщение (строка 5 в результатах работы программы). Затем эта функция возвращает управление программой вызывающей функции, и объект класса SimpleCat вновь возвращается как значение. При этом создается еще одна копия объекта за счет вызова конструктора-копировщика и, как следствие, на экран выводится очередное сообщение (строка 6 в результатах работы программы). Значение, возвращаемое из функции FunctionOne(), не присваивается ни одному объекту, поэтому ресурсы, затраченные на создание временного объекта при реализации механизма возврата, просто выброшены на ветер, как и ресурсы, затраченные на его удаление с помощью деструктора, который заявил о себе в строке 7 в результатах работы программы. Поскольку функция FunctionOne() завершена, локальная копия объекта выходит за область видимости и разрушается, вызывая деструктор и генерируя тем самым сообщение, показанное в строке 8 в результатах работы программы. Управление программой возвращается к функции main(), после чего вызывается функция FunctionTwo(), но на этот раз параметр передается как ссылка. При этом никакой копии объекта не создается, поэтому отсутствует и сообщение от конструктора- копировщика. В функции FunctionTwo() выводится сообщение, занимающее строку 10 в результатах работы программы, а затем выполняется возвращение объекта класса SimpleCat (снова как ссылки), поэтому нет никаких обращений к конструктору и деструктору. Наконец программа завершается и объект Frisky выходит за область видимости, генерируя последнее обращение к деструктору, выводящему свое сообщение (строка 11 в результатах работы программы). Проанализировав работу этой программы, можно сделать вывод, что при вызове функции FunctionOne() делается два обращения к конструктору копии и два обращения к деструктору, поскольку объект в эту функцию передается как значение, в то время как при вызове функции FunctionTwo() подобных обращений не делается. Передача константного указателяНесмотря на то что передача указателя функции FunctionTwo() более эффективна, чем передача по значению, она таит в себе немалую опасность. При вызове функции FunctionTwo() совершенно не имелось в виду, что разрешается изменять передаваемый ей объект класса SimpleCat, задаваемый в виде адреса объекта SimpleCat. Такой способ передачи открывает объект для изменений и аннулирует защиту, обеспечиваемую при передаче объекта как значения. Передачу объектов как значений можно сравнить с передачей музею фотографии шедевра вместо самого шедевра. Если какой-нибудь хулиган испортит фотографию, то никакого вреда при этом оригиналу нанесено не будет. А передачу объекта как ссылки можно сравнить с передачей музею своего домашнего адреса и приглашением гостей посетить ваш дом и взглянуть в вашем присутствии на драгоценный шедевр. Решить проблему можно, передав в функцию указатель на константный объект класса SimpleCat. В этом случае к данному объекту могут применяться только константные методы, не имеющие прав на изменение объекта SimpleCat. Эта идея демонстрируется в листинге 9.11. Листинг 9.11. Передача константных указателей 1: // Листинг 9.11. 2: // Передача константных указателей на объекты 3: 4: #include <iostream.h> 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat(); 10: SimpleCat(SimpleCat&); 11: ~SimpleCat(); 12: 13: int GetAge() const { return itsAge; } 14: void SetAge(int age) { itsAge = age; } 15: 16: private: 17: int itsAge; 18: }; 19: 20: SimpleCat::SimpleCat() 21: { 22: cout << "Simple Cat Constructor...\n"; 23: itsAge = 1; 24: } 25: 26: SimpleCat::SimpleCat(SimpleCat&) 27: { 28: cout << "Simple Cat Copy Constructor...\n"; 29: } 30: 31: SimpleCat::~SimpleCat() 32: { 33: cout << "Simple Cat Destructor...\n"; 34: } 35: 36: const SimpleCat * const FunctionTwo (const SimpleCat * const theCat); 37: 38: int main() 39: { 40: cout << "Making а cat...\n"; 41: SimpleCat Frisky; 42: cout << "Frisky is " ; 43: cout << Frisky.GetAge(); 44: cout << " years old\n"; 45: int age = 5: 46: Frisky.SetAge(age); 47: cout << "Frisky is " ; 48: cout << Frisky.GetAge(); 49: cout << " years old \n"; 50: cout << "Calling FunctionTwo...\n"; 51: FunctionTwo(&Frisky); 52: cout << "Frisky is "; 53: cout << Frisky.GetAge(); 54: cout << " years_ald\n"; 55: rsturn 0; 56: } 57: 58: // functionTwo, passes a const pointer 59: const SimpleCat * const FunctionTwo (const SimpleCat * const theCat) 60: { 61: cout << "Function Two, Returning...\n"; 62: cout << "Frisky is now " << theCat->GetAge(); 63: cout << " years old \n"; 64: // theCat->SotAge(8): const! 65: return theCat; 66: } Результат: Making a cat... Simple Cat constructor... Frisky is 1 years old Frisky is 5 years old Calling FunctionTwo... FunctionTwo. Returning... Frisky is now 5 years old Frisky is 5 years old Simple Cat Destructor... Анализ: В класс SimpleCat были добавлены две функции доступа к данным: метод GetAge() (строка 13), который является константной функцией, и метод SetAge() (строка 14), который не является константным. В этот класс была также добавлена переменная-член itsAge (строка 17). Конструктор, конструктор-копировщик и деструктор по-прежнему определены для вывода на экран своих сообщений. Однако конструктор-копировщик ни разу не вызывался, поскольку объект был передан как ссылка и поэтому никаких копий объекта не создавалось. В строке 41 был создан объект со значением возраста, заданным по умолчанию. Это значение выводится на экран в строке 42. В строке 46 переменная itsAge устанавливается с помощью метода доступа SetAge, а результат этой установки выводится на экран в строке 47. В этой программе функция FunctionOne не используется, но вызывается функция FunctionTwo(), которая слегка изменена. Ее объявление занимает строку 36. На этот раз и параметр, и значение возврата объявляются как константные указатели на константные объекты. Поскольку и параметр, и возвращаемое значение передаются как ссылки, никаких копий не создается и конструктор-копировщик не вызывается. Однако указатель в функции FunctionTwo() теперь является константным, следовательно, к нему не может применяться неконстантный метод SetAge(). Если обращение к методу SetAge() в строке 64 не было бы закомментировано, программа не прошла бы этап компиляции. Обратите внимание, что объект, создаваемый в функции main(), не является константным и объект Frisky может вызвать метод SetAge(). Адрес этого обычного объекта передается функции FunctionTwo(), но, поскольку в объявлении функции FunctionTwo() заявлено, что передаваемый указатель должен быть константным указателем на константный объект, с этим объектом функция обращается так, как если бы он был константным! Ссылки в качестве альтернативыПри выполнении программы, показанной в листинге 9.11, устранена необходимость создания временных копий, что сокращает число обращений к конструктору и деструктору класса, в результате чего программа работает более эффективно. В данном примере использовался константный указатель на константный объект, что предотвращало возможность изменения объекта функцией. Однако по-прежнему имеет место некоторая громоздкость синтаксиса, свойственная передаче в функции указателей. Поскольку, как вам известно, объект никогда не бывает нулевым, внутреннее содержание функции упростилось бы, если бы ей вместо указателя передавалась ссылка. Подтверждение этим словам вы найдете в листинге 9.12. Листинг 3.12. Передача ссылок на объекты 1: // Листинг 9.12. 2: // Передача ссылок на объекты 3: 4: #include <iostream.h> 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat(); 10: SimpleCat(SimpleCat&); 11: ~SimpleCat(); 12: 13: int GetAge() const { return itsAge; } 14: void SetAge(int age) { itsAge = age: } 15: 16: private: 17: int itsAge; 18: }; 19: 20: SimpleCat::SimpleCat() 21: { 22: cout << "Simple Cat Constructor...\n"; 23: itsAge = 1; 24: } 25: 26: SimpleCat::SimpleCat(SimploCat&) 27: { 28: cout << "Simple Cat Copy Cunstructor...\n"; 29: } 30: 31: SimpleCat::~SimpleCat() 32: { 33: cout << "Simple Cat Destructor...\n"; 34: } 35: 36: const SimpleCat & FunctionTwo (const SimpleCat & theCat); 37: 38: int main() 39: { 40: cout << "Making a cat...\n"; 41: SimpleCat Frisky; 42: cout << "Frisky is " << Frisky.GetAge() << " years old\n"; 43: int age = 5; 44: Frisky,SetAge(age); 45: cout << "Frisky is " << Frisky.GetAge() << " years old\n"; 46: cout << "Calling FunctionTwo...\n"; 47: FunctionTwo(Frisky); 48: cout << "Frisky is " << Frisky.GetAge() << " years old\n"; 49: return 0; 50: } 51: 52: // functionTwo, passes a ref to a const object 53: const SimpleCat & FunctionTwo (const SimpleCat & theCat) 54: { 55: cout << "Function Two. Returning...\n"; 56: cout << "Frisky is now " << theCat.GetAge(); 57: cout << " years old \n"; 58: // theCat.SetAge(8); const! 59: return theCat; 60: } Результат: Making a cat... Simple Cat constructor... Frisky is 1 years old Frisky is 5 years old Calling FunctionTwo... FunctionTwo. Returning... Frisky is now 5 years old Frisky is 5 years old Simple Cat Destructor... Анализ: Результат работы этой программы идентичен результату, показанному после листинга9.11. Единственное существенное изменение — функция FunctionTwo() теперь принимает и возвращает ссылки на константный объект. И вновь-таки работа со ссылками несколько проще, чем работа с указателями, хотя при этом достигается та же экономия средств и эффективность выполнения, а также обеспечивается надежность за счет использования спецификатора const. Константные ссылки Программисты, работающие с языком C++, обычно не видят разницы между константной ссылкой на объект SimpleCat и ссылкой на константный объект SimpleCat. Сами ссылки нельзя переназначать, чтобы они ссылались на другой объект, поэтому они всегда константны. Если к ссылке применено ключевое слово const, то это делает константным объект, с которым связана ссылка. Когда лучше использовать ссылки, а когда - указателиОпытные программисты безоговорочно отдают предпочтение ссылкам, а не указателям. Ссылки проще использовать, и они лучше справляются с задачей сокрытия информации, как вы видели в предыдущем примере. Но ссылки нельзя переназначать. Если же вам нужно сначала указывать на один объект, а затем на другой, придется использовать указатель. Ссылки не могут быть нулевыми, поэтому, если существует хоть какая-нибудь вероятность того, что рассматриваемый объект может быть нулевым, вам нельзя использовать ссылку. В этом случае необходимо использовать указатель. В качестве примера рассмотрим оператор new. Если оператор new не сможет выделить память для нового объекта, он возвратит нулевой указатель. А поскольку ссылка не может быть нулевой, вы не должны инициализировать ссылку на эту память до тех пор, пока не проверите, что она не нулевая. В следующем примере показано, как это сделать: int *pInt = new int; if (pInt != NULL) int &rInt = *pInt; В этом примере объявляется указатель pInt на значение типа int, который инициализируется областью памяти, возвращаемой оператором new. Адрес этой области памяти (в указателе pInt) тестируется, и, если он не равен значению null, указатель pInt разыменовывается. Результат разыменования переменной типа int представляет собой объект типа int, и ссылка rInt инициализируется этим объектом. Следовательно, ссылка rInt становится псевдонимом для переменной типа int, возвращаемой оператором new. Рекомендуется:Передавайте функциям параметры как ссылке везде, где это возможно. Обеспечивайте возврат значений как ссылок везде, где это возможно. Используйте спецификатор const для защиты ссылок и указателей везде, где это возможно. Не рекомендуется: Не используйте указатели, если вместо них можно использовать ссылки. Не возвращайте ссылки на локальные объекты. Коктейль из ссылок и указателейНе будет ошибкой в списке параметров одной функции объявить как указатели, так и ссылки, а также объекты, передаваемые как значения, например: CAT * SomeFunction (Person &theOwner, House *theHouse, int age); Это объявление означает, что функция SomeFunction принимает три параметра. Первый является ссылкой на объект типа Person, второй — указателем на объект типа House, а третий — целочисленным значением. Сама же функция возвращает указатель на объект класса CAT. Следует также отметить, что при объявлении соответствующих переменных можно использовать разные стили размещения операторов ссылки (&) и косвенного обращения (*). Вполне законной будет любая из следующих записей: 1: CAT& rFrisky; 2: CAT & rFrisky; 3: CAT &rFrisky; Примечание:Символы пробелов в программах на языке C++ полностью игнорируются, поэтому везде, де вы видите пробел, можно ставить несколько пробелов, символов табуляции или символов разрывов строк. Оставив в покое вопросы свободного волеизъявления, попробуем разобраться в том, какой вариант все же лучше других. Как ни странно, можно найти аргументы в защиту каждого из трех вариантов. Аргумент в защиту первого варианта состоит в следующем. rFrisky — это переменная с именем rFrisky, тип которой можно определить как ссылку на объект класса CAT. Поэтому вполне логично, чтобы оператор & стоял рядом с типом. Однако есть и контраргумент. CAT — это тип. Оператор & является частью объявления, которое включает имя переменной и амперсант. Но следует отметить, что слияние вместе символа & и имени типа CAT может привести к возникновению следующей ошибки: CAT& rFrisky, rBoots; Поверхностный анализ этой строки может натолкнуть на мысль, что как переменная rFrisky, так и переменная rBoots являются ссылками на объекты класса CAT. Однако это не так. На самом деле это объявление означает, что rFrisky является ссылкой на объект класса CAT, а rBoots (несмотря на свое имя с характерным префиксом) — не ссылка, а обыкновенная переменная типа CAT. Поэтому последнее объявление следует переписать по-другому: CAT &rFrisky, rBoots; В ответ на это возражение стоит порекомендовать, чтобы объявления ссылок и обычных переменных никогда не смешивались в одной строке. Вот правильный вариант той же записи: CAT& rFrisky; CAT Boots; Примечание:Наконец, многие программисты не обращают внимания на приведенные аргументы и, считая, что истина находится посередине, выбирают средний вариант (средний, кстати, в двух смыслах), который иллюстрируется случаем 2: 2: CAT & rFrisky; Безусловно, все сказанное до сих пор об операторе ссылки (&) относится в равной степени и к оператору косвенного обращения (<<). Выберите стиль, который вам подходит, и придерживайтесь его на протяжении всей программы, ведь ясность текста программы — одна из основных составляющих успеха. Многие программисты при объявлении ссыпок и указателей предпочитают придерживаться двух соглашений. 1. Размещать амперсант или звездочку посередине, окаймляя этот символ пробелами с двух сторон. 2. Никогда не объявлять ссылки, указатели и переменные в одной и той же строке программы. Не возвращайте ссылку на объект, который находиться вне области видимости!Научившись передавать аргументы как ссылки на объекты, программисты порой теряют чувство реальности. Не стоит забывать, что все хорошо в меру. Помните, что ссылка всегда служит псевдонимом некоторого объекта. При передаче ссылки в функцию или из нее не забудьте задать себе вопрос: "Что представляет собой объект, псевдонимом которого я манипулирую, и будет ли он существовать в момент его использования?" В листинге 9.13 показан пример возможной ошибки, когда функция возвращает ссылку на объект, которого уже не существует. Листинг 9.13. Возвращение ссылки на несуществующий объект 1: // Листинг 9.13. 2: // Возвращение ссылки на объект, 3: // которого больше не существует 4: 5: #include <iostream.h> 6: 7: class SimpleCat 8: { 9: public: 10: SimpleCat (int age, int weight); 11: ~SimpleCat() { } 12: int GetAge() < return itsAge; } 13: int GetWeight() { return itsWeight; } 14: private: 15: int itsAge; 16: int itsWeight; 17: }; 18: 19: SimpleCat::SimpleCat(int age, int weight) 20: { 21: itsAge = age; 22: itsWeight = weight; 23: } 24: 25: SimpleCat &TheFunction(); 26: 27: int main() 28: { 29: SimpleCat &rCat = TheFunction(); 30: int age = rCat.GetAge(); 31: cout << "rCat " << age << " years old!\n" 32: return 0; 33: } 34: 35: SimpleCat &TheFunction() 36: { 37: SimpleCat Frisky(5,9); 38: return Frisky; 39: } Результат: Compile error: Attempting to return a reference to a local object! (Ошибка компиляции: попытка возвратить ссылку на локальный объект!) Предупреждение:Эта программа не компилируется на компиляторе фирмы Borland, но для нее подходят компиляторы компании Microsoft. Однако профессиональный программист никогда не станет полагаться на уступки компилятора. Анализ: В строках 7—17 объявляется класс SimpleCat. В строке 29 инициализируется ссылка на объект класса SimpleCat с использованием результатов вызова функции TheFunction(), объявленной в строке25. Согласно объявлению эта функция возвращает ссылку на объект класса SimpleCat. В теле функции TheFunction() объявляется локальный объект типа SimpleCat и инициализируется его возраст и вес. Затем этот объект возвращается по ссылке. Некоторые компиляторы обладают достаточным интеллектом, чтобы распознать эту ошибку, и не позволят вам запустить данную программу на выполнение. Другие же (сразу видно, кто настоящий друг) спокойно разрешат вам выполнить эту программу с непредсказуемыми последствиями. По возвращении функции TheFunction() локальный объект Frisky будет разрушен (надеюсь, безболезненно для самого объекта). Ссылка же, возвращаемая этой функцией, останется псевдонимом для несуществующего объекта, а это явная ошибка. Возвращение ссылки на в области динамического обменаМожно было бы попытаться решить проблему, представленную в листинге9.13, сориентировав функцию TheFunction() на размещение объекта Frisky в области динамического обмена. В этом случае после возврата из функции TheFunction() объект Frisky будет все еще жив. Новый подход порождает новую проблему: что делать с памятью, выделенной для объекта Frisky, после окончания обработки этого объекта? Эта проблема показана в листинге 9.14. Листинг 9.14. Утечка памяти 1: // Листинг 9.14. 2: // Разрешение проблемы утечки памяти 3: #include <iostream.h> 4: 5: class SimpleCat 6: { 7: public: 8: SimpleCat (int age, int weight); 9: ~SimpleCat() { } 10: int GetAge() { return itsAge; } 11: int GetWeight() { return itsWeight; } 12: 13: private: 14: int itsAge; 15: int itsWeight; 16: }; 17: 18: SimpleCat::SimpleCat(int age, int weight) 19: { 20: itsAge = age; 21: itsWeight = weight; 22: } 23: 24: SimpleCat & TheFunction(); 25: 26: int main() 27: { 28: SimpleCat & rCat = TheFunction(); 29: int age = rCat.GetAge(); 30: cout << "rCat " << age << " years old!\n"; 31: cout << "&rCat: " << &rCat << endl; 32: // Как освободить эту память? 33: SimpleCat * pCat = &rCat; 34: delete pCat; 35: // Боже, на что же теперь ссылается rCat?? 36: return 0; 37: } 38: 39: SimpleCat &TheFunction() 40: { 41: SimpleCat * pFrisky = new SimpleCat(5,9); 42: cout << "pFrisky: " << pFrisky << endl; 43: return *pFrisky; 44: } Результат: pFrisky: 0x00431C60 rCat 5 years old! &rCat: 0x00431C60 Предупреждение:Эта программа компилируется, компонуется и, кажется, работает. Но мина замедленного действия уже ожидает своего часа. Анализ: Функция TheFunction() была изменена таким образом, чтобы больше не возвращать ссыпку на локальную переменную. В строке 41 вычисляется некоторая область динамически распределяемой памяти и ее адрес присваивается указателю. Этот адрес выводится на экран, после чего указатель разыменовывается и объект класса SimpleCat возвращается по ссылке. В строке 28 значение возврата функции TheFunction() присваивается ссылке на объект класса SimpleCat, а затем этот объект используется для получения возраста кота, и полученное значение возраста выводится на экран в строке 30. Чтобы доказать, что ссылка, объявленная в функции main(), ссылается на объект, размещенный в области динамической памяти, выделенной для него в теле функции TheFunction(), к ссылке rCat применяется оператор адреса (&). Вполне убедителен тот факт, что адрес объекта, на который ссылается rCat, совпадает с адресом объекта, расположенного в свободной области памяти. До сих пор все было гладко. Но как же теперь освободить эту область памяти, которая больше не нужна? Ведь нельзя же выполнять операции удаления на ссылках. На ум приходит одно решение: создать указатель и инициализировать его адресом, полученным из ссылки rCat. При этом и память будет освобождена, и условия для утечки памяти будут ликвидированы. Все же одна маленькая проблема остается: на что теперь ссылается переменная rCat после выполнения строки 34? Как указывалось выше, ссылка всегда должна оставаться псевдонимом реального объекта; если же она ссылается на нулевой объект (как в данном случае), о корректности программы говорить нельзя. Примечание:Не будет преувеличением определение программы как некорректной, если она содержит ссылку на нулевой объект (несмотря на то что она успешно компилируется), поскольку результаты ее выполнения непредсказуемы. Для решения этой проблемы есть три пути. Первый состоит в объявлении объекта класса SimpleCat в строке 28 и возвращении этого объекта из функции TheFunction как значения. Второй — в объявлении класса SimpleCat в свободной области (в теле функции TheFunction()), но сделать это нужно так, чтобы функция TheFunction() возвращала указатель на данный объект. Затем, когда объект больше не нужен, его можно удалить в вызывающей функции с помощью оператора delete. Третье решение (возможно, самое правильное) — объявить объект в вызывающей функции, а затем передать в функцию TheFunction() ссылку на него. А где же уазатель?При выделении в программе памяти в области динамического обмена возвращается указатель. Важно сохранить указатель на эту область памяти, поскольку при его утрате эту память нельзя удалить, что приводит к ее утечке. При передаче данных, хранящихся в этом блоке памяти, между функциями, необходимо следить, кому принадлежит этот указатель. Обычно ответственность за освобождение ячеек памяти в области динамического обмена ложится на ту функцию, которая их зарезервировала. Но это не догма, а лишь рекомендация для программистов. Весьма небезопасно, если одна функция создает объект с выделением для него некоторой памяти, а другая занимается освобождением этой памяти. Неопределенность относительно владельцев указателя может привести к одной из двух проблем: можно забыть освободить память или применить оператор delete дважды к одному и тому же указателю. Любая из этих проблем может стать причиной больших неприятностей в вашей программе. Именно поэтому целесообразно придерживаться принципа, что память освобождает та функция, которая ее зарезервировала. Если вы пишете функцию, которая требует выделения памяти в области динамического обмена, а затем возвращаете этот объект в вызывающую функцию, пересмотрите свой интерфейс. Пусть лучше вызывающая функция выделяет память, а затем передает в другую функцию этот объект как ссылку. Затем, после возвращения объекта из функции, его можно будет удалить в вызывающей функции, где он и был создан. Рекомендуется:Передавайте параметры функции как значениятолько тогда, когда в этом есть необходимость. Возвращайте результат работы функции как значение только тогда, когда в этом есть необходимость. Не рекомендуется:Не используйте ссылки на объекты, которые могут выйти в программе за пределы области видимости. Не создавайте ссылки на нулевые объекты. РезюмеСегодня вы узнали, что представляют собой ссылки и чем они отличаются от указателей. Важно уяснить для себя, что ссылки всегда инициализируют существующие объекты и их нельзя переназначить до окончания программы. Ссылка выступает псевдонимом объекта, и любое действие, выполненное над ссылкой, выполняется над ее адресатом. Доказательством этого может служить тот факт, что при взятии адреса ссылки возвращается адрес связанного с ней объекта. Вы убедились, что передача объектов в функции как ссылок может быть более эффективной, чем передача их как значений. Передача объектов как ссылок позволяет вызываемой функции изменять значения переменных вызывающей функции. Вы также узнали, что аргументы, передаваемые функции, и значения, возвращаемые из функций, могут передаваться как ссылки и этот процесс можно реализовать как с помощью указателей, так и с помощью ссылок. Теперь вы научились для безопасной передачи значений между функциями использовать константные указатели на константные объекты или константные ссылки, благодаря чему достигается как эффективность, так и безопасность работы программы. Вопросы и ответыЗачем использовать ссыпки, если указатели могут делать ту же работу? Ссылки легче использовать, и они проще для понимания. Косвенность обращений при этом скрывается, и отсутствует необходимость в многократном разыменовании переменных. Зачем нужны указатели, если со ссыпками легче работать? Ссылки не могут быть нулевыми, и их нельзя переназначать. Указатели предлагают большую гибкость, но их сложнее использовать. Зачем вообще результат функции возвращать как значение? Если возвращается объект, который является локальным в данной функции, необходимо организовать возврат его именно как значения, в противном случае возможно появление ссылки на несуществующий объект. Если существует опасность от возвращения объекта как ссылки, почему бы тогда не сделать обязательным возврат по значению? При возвращении объекта как ссылки достигается гораздо большая эффективность, которая заключается в экономии памяти и увеличении скорости работы программы. КоллоквиумВ этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний, а также ряд упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из предложенных ниже вопросов. Контрольные вопросы1. В чем разница между ссылкой и указателем? 2. Когда нужно использовать именно указатель, а не ссылку? 3. Что возвращает оператор new, если для создания нового объекта недостаточно памяги? 4. Что представляет собой константная ссылка? 5. В чем разница между передачей объекта как ссылки и передачей ссылки в функцию? Упражнения1. Напишите программу, которая объявляет переменную типа int, ссылку на значение типа int и указатель на значение типа int. Используйте указатель и ссылку для управления значением переменной типа int. 2. Напишите программу, которая объявляет константный указатель на постоянное целое значение. Инициализируйте этот указатель, чтобы он указывал на целочисленную переменную varOne. Присвойте переменной varOne значение 6. Используйте указатель, чтобы присвоить переменной varOne значение 7. Создайте вторую целочисленную переменную varTwo. Переназначьте указатель, чтобы он указывал на переменную varTwo. Пока не компилируйте это упражнение. 3. Скомпилируйте программу, написанную в упражнении 2. Какие действия компилятор считает ошибочными? Какие строки генерируют предупреждения? 4. Напишите программу, которая создает блуждающий указатель. 5. Исправьте программу из упражнения 4, чтобы блуждающий указатель стал нулевым. 6. Напишите программу, которая приводит к утечке памяти. 7. Исправьте программу из упражнения 6. 8. Жучки: что неправильно в этой программе? 1: #include <iostream.h> 2: 3: class CAT 4: { 5: public: 6: CAT(int age) { itsAge = age; } 7: ~CAT(){ } 8: int GetAge() const { return itsAge;} 9: private: 10: int itsAge; 11: }; 12: 13: CAT & MakeCat(int age); 14: int main() 15: { 16: int age = 7; 17: CAT Boots = MakeCat(age); 18: cout << "Boots is " << Boots.GetAge() << " years old!\n"; 19: return 0; 20: } 21: 22: CAT & MakeCat(int age) 23: { 24: CAT * pCat = new CAT(age); 25: return *pCat; 26: } 9. Исправьте программу из упражнения 8. День 10-й. Дополнительные возможности использования функцииНа занятии 5 вы познакомились с основными принципами использования функций. Теперь, когда вы знаете, как работают указатели и ссылки, перед вами открываются дополнительные возможности. Сегодня вы узнаете: • Как перегружать функции-члены • Как перегружать операторы • Как создавать функции для поддержания классов с динамическим выделением памяти для переменных Перегруженные функции-членыНа занятии 5 вы получили общие представления о полиморфизме, или перегружаемости функций. Имеется в виду объявление двух или более функций под одним именем но с разными параметрами. Функции-члены класса можно перегружать точно так же. В классе Rectangle (листинг 10.1) объявляются две функции DrawShape(). Первая, которая не содержит списка параметров, вычерчивает прямоугольник, основываясь на текущих значениях класса. Вторая принимает два значения — ширину и длину — и в соответствии с ними создает прямоугольник, игнорируя текущие значения класса. Листинг 10.1. Перегрузка функций-членов 1: //Листинг 10.1. Перегрузка функций-членов 2: #include <iostream.h> 3: 4: int 5: // Обьявление класса Rectangle 6: class Rectangle 7: { 8: public: 9: // конструкторы 10: Rectangle(int width, int height); 11: ~Rectangle(){ } 12: 13: // перегрузка функции-члена класса DrawShape 14: void OrawShape() const; 15: void DrawShape(int aWidth, int aHeight) const; 16: 17: private: 18: int itsWidth; 19: int itsHeight; 20: }; 21: 22: // Применение конструктора 23: Rectangle::Rectangle(int width, int height) 24: { 25: itsWidth = width; 26: itsHeight = height; 27: } 28: 29: 30: // Перегруженная функция DrawShape - вариант без передачи данных 31: // Создание прямоугольника по значениям, заданным по умолчанию 32: void Rectangle::DrawShape() const 33: { 34: DrawShape( itsWidth, itsHeight); 35: } 36: 37: 38: // Перегруженная функция DrawShape - передача двух значений 39: // Создание прямоугольника по значениям, переданным с параметрами 40: void Rectangle:;DrawShape(int width, int height) const 41: { 42: for (int i = 0; i<height; i++) 43: { 44: for (int j = 0; j< width; j++) 45: { 46: cout << "<<"; 47: } 48: cout << "\n"; 49: } 50: } 51: 52: // Выполняемая программа, демонстрирующая использование перегруженных функций 53: int main() 54: { 55: // создание прямоугольника с размерами 30 и 5 56: Rectangle theRect(30,5); 57: cout << "DrawShape(): \n"; 58: theRect.DrawShape(); 59: cout << "\nDrawShape(40,2): \n"; 60: theRect.DrawShape(40,2); 61: return 0; 62: } Результат: DrawShape(): ****************************** ****************************** ****************************** ****************************** ****************************** DrawShape(40,2): **************************************** **************************************** Анализ: Листинг 10.1 представляет собой усеченную версию проекта, рассмотренного в главе подведения итогов за первую неделю. Чтобы сократить размер программы, был удален блок контроля за соответствием значений заданным типам. Основной код был упрощен до простой выполняемой программы без показа пользовательского меню. Сейчас для нас важны строки 14 и 15, где происходит перегрузка функции DrawShape(). Использование перегруженных вариантов этой функции показано далее, в строках с 30 по 50. Обратите внимание, что версия функции DrawShape() без параметров обращается к варианту функции, содержащей два параметра, и передает в нее текущие значения переменных-членов. При программировании всегда следует избегать дублирования одинаковых программных кодов. В противном случае придется держать в памяти все созданные копии функций, чтобы при изменении программного кода в одной из них внести соответствующие изменения во все копии. В строках программы с 52 по 62 создается прямоугольный объект и вызывается функция DrawShape(). В первый раз в функцию не передаются параметры, а во второй раз передается два значения типа unsigned short integer. Компилятор выбирает правильное объявление функции по количеству и типу заданных параметров. Дополнительно можно задать в этой же программе еще одно объявление функции DrawShape(), в параметрах которой будет одно значение размера и переменная перечисления, позволяющая пользователю указать, что обозначает данный размер — ширину или длину прямоугольника. Использование значений, заданных по умолчаниюФункции-члены класса, подобно обычным функциям, могут использовать значения, заданные по умолчанию. При объявлении функций-членов с аргументами, задаваемыми по умолчанию, используется уже знакомый вам синтаксис, как показано в листинге 10.2 Листинг 10.2. Использование значений, заданных по умолчанию 1: //Листинг 10.2. Использование значений, заданных по умолчанию 2: #include <iostream.h> 3: 4: int 5: 6: // Объявление класса Rectangle 7: class Rectangle 8: { 9: public: 10: // конструкторы 11: Rectangle(int width, int height); 12: ~Rectangle() { } 13: void DrawShape(int aWidth, int aHeight, bool UseCurrentVals = false) const; 14: 15: private: 16: int itsWidth; 17: int itsHeight; 18: }; 19: 20: //Применение конструктора 21: Rectangle::Rectangle(int width, int height): 22: itsWidth(width), // инициализация 23: itsHeight(height) 24: { } // пустое тело 25: 26: 27: // для третьего параметра используются значения по умолчанию 28: void Rectangle::DrawShape( 29: int width, 30: int height, 31: bool UseCurrentValue 32: ) const 33: { 34: int printWidth; 35: int printHeight; 36: 37: if (UseCurrentValue == true) 38: { 39: printWidth = itsWidth; // используется значение текущего класса 40: printHeight = itsHeight; 41: } 42: else 43: { 44: printWidth = width; // используются значения параметра 45: printHeight = height; 46: } 47: 48: 49: for (int i = 0; i<printHeight; i++) 50: { 51: for (int j = 0; j< printWidth; j++) 52: { 53: cout << "*"; 54: } 55: cout << "\n"; 56: } 57: } 58: 59: // Выполняемая программа показывает использование перегруженных функций 60: int main() 61: { 62: // создание прямоугольника 30 на 5 63: Rectangle theRect(30,5); 64: cout << "DrawShape(0,0,true)...\n"; 65: theRect.DrawShape(0,0,true); 66: cout << "DrawShape(40,2)...\n"; 67: theRect.DrawShape(40,2); 68: return 0; 69: } Результат: DrawShape(0,0,true)... ****************************** ****************************** ****************************** ****************************** ****************************** DrawShape(40,2)... **************************************** **************************************** Анализ: В листинге 10.2 перегруженная функция DrawShape() заменена простой функцией с параметрами, задаваемыми по умолчанию. Функция определена в строке 13 с тремя параметрами. Первые два, aWidth и aHeigth, относятся к типу USH0RT, а третий представляет собой логическую переменную UseCurrentVals, которой по умолчанию присваивается значение false. Выполнение этой немного громоздкой функции начинается со строки 28. Сначала проверяется значение переменной UseCurrentVals. Если эта переменная содержит значение true, то для присвоения значений локальным переменным printWidth и printHeigth используются соответственно переменные-члены itsWidth и itsHeigth. Если окажется, что переменная UseCurrentVals содержит значение false либо по умолчанию, либо оно является результатом установок, сделанных пользователем, то переменным printWidth и printHeigth присваиваются значения параметров функции, заданные по умолчанию. Обратите внимание, что если UseCurrentVals истинно, то значения параметров функции просто игнорируются. Выбор между значениями по умолчанию и перегруженными функциямиВ листингах 10.1 и 10.2 выполняются одни и те же задачи, но использование перегруженных функций в листинге 10.1 делает программу более естественной и читабельной. Кроме того, если в программе потребуется третий вариант функции, например, для того, чтобы пользователь мог задать только один размер геометрической фигуры, а другой оставить по умолчанию, не составит труда добавить новую перегруженную функцию. Как решить, что следует использовать в программе — перегруженные функции или значения по умолчанию? Примите к сведению следующие положения. Использование перегруженных функций предпочтительнее, если: • не существует стандартных общепринятых значений, которые можно было бы использовать по умолчанию; • в программе в зависимости от ситуации необходимо использовать различные алгоритмы; • необходимо иметь возможность изменять тип значений, передаваемых в функцию. Конструктор, принятый по умолчаниюНа шестом занятии, изучая базовые классы, вы узнали, что в случае отсутствия явного объявления конструктора класса используется конструктор по умолчанию, который не содержит параметров и никак себя не проявляет в программе. Не составляет труда создать собственный конструктор, применяемый по умолчанию, который также не будет принимать никаких параметров, но позволит управлять созданием объектов класса. Конструктор, предоставляемый компилятором, называется заданным по умолчанию. В то же время конструктором по умолчанию называется также любой другой конструктор класса, не содержащий параметров. Это может показаться странным, но ситуация прояснится, если посмотреть на дело с точки зрения применения данного конструктора на практике. Примите к сведению, что если в программе был создан какой-либо конструктор, то компилятор не будет предлагать свой конструктор по умолчанию. Поэтому, если вам нужен конструктор без параметров, а в программе уже создан один конструктор, то конструктор по умолчанию нужно будет создать самостоятельно! Перегрузка конструкторовКонструктор предназначен для создания объекта. Например, назначение конструктора Rectangle состоит в создании объекта прямоугольник. До запуска конструктора прямоугольник в программе отсутствует. Существует только зарезервированная для него область памяти. По завершении выполнения конструктора в программе появляется готовый для использования объект. Конструкторы, как и все другие функции, можно перегружать. Перегрузка конструкторов — мощное средство повышения эффективности и гибкости программы. Например, рассматриваемый нами объект Rectangle может иметь два конструктора. В первом задается ширина и длина прямоугольника, а второй не имеет параметров и для установки размеров использует значения по умолчанию. Эта идея реализована в листинге 10.3. Листинг 10.3. Перегрузка канструктора 1: // Листинг 10.3. 2: // Перегрузка конструктора 3: 4: #include <iostream.h> 5: 6: class Rectangle 7: { 8: public: 9: Rectangle(); 10: Rectangle(int width, int length); 11: ~Rectangle() { } 12: int GetWidth() const { return itsWidth; } 13: int GetLength() const { return itsLength; } 14: private: 15: int itsWidth; 16: int itsLength; 17: }; 18: 19: Rectangle::Rectangle() 20: { 21: itsWidth = 5; 22: itsLength = 10; 23: } 24: 25: Rectangle::Rectangle (int width, int length) 26: { 27: itsWidth = width; 28: itsLength = length; 29: } 30: 31: int main() 32: { 33: Rectangle Rect1; 34: cout << "Rect1 width: " << Rect1.GetWidth() << endl; 35: cout << "Rect1 length: " << Rect1.GetLength() << endl; 36: 37: int aWidth, aLength; 38: cout << "Enter a width: "; 39: cin >> aWidth; 40: cout << "\nEnter a length: "; 41: cin >> aLength; 42: 43: Rectangle Rect2(aWidth, aLength); 44: cout << "\nRect2 width: " << Rect2.GetWidth() << endl; 45: cout << "Rect2 length: " << Rect2.GetLength() << endl; 46: return 0; 47: } Результат: Rect1 width: 5 Rect1 length: 10 Enter a width: 20 Enter a length: 50 Rect2 width: 20 Rect2 length: 50 Анализ: Класс Rectangle объявляется в строках с 6 по 17. В классе представлены два конструктора: один использует значения по умолчанию (строка 9), а второй принимает значения двух целочисленных параметров (строка 10). В строке 33 прямоугольный объект создается с использованием первого конструктора. Значения размеров прямоугольника, принятые по умолчанию, выводятся на экран в строках 34 и 35. Строки программы с 37 по 41 выводят на экран предложения пользователю ввести собственные значения ширины и длины прямоугольника. В строке 43 вызывается второй конструктор, использующий два параметра с только что установленными значениями. И наконец, значения размеров прямоугольника, установленные пользователем, выводятся на экран в строках 44 и 45. Как и при использовании других перегруженных функций, компилятор выбирает нужное объявление конструктора, основываясь на числе и типе параметров. Инициализация объектовДо сих пор переменные-члены объектов задавались прямо в теле конструктора. Выполнение конструктора происходит в два этапа: инициализация и выполнение тела конструктора. Большинство переменных может быть задано на любом из этих этапов: как во время инициализации, так и во время выполнения конструктора. Но логически правильнее, а зачастую и эффективнее, инициализировать переменные-члены во время инициализации конструктора. В следующем примере показана инициализация переменных-членов: CAT(): // имя конструктора и список параметров itsAge(5), // инициализация списка itsWeigth(8) {} // тело конструктора После скобки закрытия списка параметров конструктора ставится двоеточие. Затем перечисляются имена переменных-членов. Пара круглых скобок со значением за именем переменной используется для инициализации этой переменной. Если инициализируется сразу несколько переменных, то они должны быть отделены запятыми. В листинге 10.4 показана инициализация переменных конструкторов, взятых из листинга 10.3. В данном примере инициализация переменных используется вместо присвоения им значений в теле конструктора. Листинг 10.4. Фрагмент программного кода с инициализацией переменных-членов 1: Rectangle::Rectangle(): 2: itsWidth(5), 3: itsLength(10) 4: { 5: } 6: 7: Rectangle::Rectangle (int width, int length): 8: itsWidth(width), 9: itsLength(length) 10: { 11: } Результат: Отсутствует Анализ: Некоторые переменные можно только инициализировать и нельзя присваивать им значения: например, в случае использования ссылок и констант. Безусловно, переменной-члену можно присвоить значение прямо в теле конструктора, но для упрощения программы лучше по возможности устанавливать значения переменных-членов на этапе инициализации конструктора. Конструктор-копировщикПомимо конструктора и деструктора, компилятор по умолчанию предоставляет также конструктор-копировщик, который вызывается всякий раз, когда нужно создать копию объекта. Когда объект передается как значение либо в функцию, либо из функции в виде возврата, всегда создается его временная копия. Если в программе обрабатывается объект, созданный пользователем, то для выполнения этих операций вызывается конструктор- копировщик класса, как было показано на предыдущем занятии в листинге 9.6. Все копировщики принимают только один параметр — ссылку на объект в том же классе. Разумно будет сделать эту ссылку константной, так как конструктор не должен изменять передаваемый в него объект. Например: CAT(const CAT & theCat); В данном случае конструктор CAT принимает константную ссылку на объект класса CAT. Цель использования конструктора-копировщика состоит в создании копии объекта theCat. Копировщик, заданный компилятором по умолчанию, просто копирует все переменные-члены из указанного в параметре объекта в переменные-члены нового объекта. Такое копирование называется поверхностным; и, хотя оно подходит для большинства случаев, могут возникнуть серьезные проблемы, если переменные-члены окажутся указателями на ячейки динамической памяти. Поверхностное копирование создает несколько переменных-членов в разных объектах, которые ссылаются на одни и те же ячейки памяти. Глубинное копирование переписывает значения переменных по новым адресам. Например, класс CAT содержит переменную-член theCat, которая указывает на ячейку в области динамической памяти, где сохранено некоторое целочисленное значение. Копировщик по умолчанию скопирует переменную theCat из старого класса CAT в переменную theCat в новом классе CAT. При этом оба объекта будут указывать на одну и ту же ячейку памяти (рис. 10.1). Рис. 10.1. Использование копировщика, заданного по умолчанию Проблемы могут возникнуть, если программа выйдет за область видимости одного из классов CAT. Как уже отмечалось при изучении указателей, назначение деструктора состоит в том, чтобы очищать память от ненужных объектов. Если деструктор исходного класса CAT очистит свои ячейки памяти, а объекты нового класса CAT все так же будут ссылаться на эти ячейки, то над программной нависнет смертельная опасность. Эта проблема проиллюстрирована на рис. 10.2. Рис. 10.2 Возникновение ошибочного указателя Чтобы предупредить возникновение подобных проблем, нужно вместо копировщика по умолчанию создать и использовать собственный копировщик, который будет осуществлять глубинное копирование с перемещением значений переменных-членов в новые адреса памяти. Этот процесс показан в листинге 10.5 Листинг 10.5. Конструктор-копировщик 1: // Листинг 10.5. 2: // Конструктор-копировщик 3: 4: #include <iostream.h> 5: 6: class CAT 7: { 8: public: 9: CAT(); // конструктор по умолчанию 10: CAT (const CAT &); // конструктор-копировщик 11: ~CAT(); // деструктор 12: int GetAge() const { return *itsAge; } 13: int GetWeight() const { return *itsWeight; } 14: void SetAge(int age) { *itsAge = age; } 15: 16: private: 17: int *itsAge; 18: int *itsWeight; 19: }; 20: 21: CAT::CAT() 22: { 23: itsAge = new int; 24: itsWeight = new int; 25: *itsAge = 5; 26: *itsWeight = 9; 27: } 28: 29: CAT::CAT(const CAT & rhs) 30: { 31: itsAge = new int; 32: itsWeight = new int; 33: *itsAge = rhs.GetAge(); // открытый метод доступа 34: *itsWeight = *(rhs.itsWeight); // закрытый метод доступа 35: } 36: 37: CAT::~CAT() 38: { 39: delete itsAge; 40: itsAge = 0; 41: delete itsWeight; 42: itsWeight = 0; 43: } 44: 45: int main() 46: { 47: CAT frisky; 48: cout << "frisky's age: " << frisky.GetAge() << endl; 49: cout << "Setting frisky to 6...\n"; 50: frisky.SetAge(6); 51: cout << "Creating boots from frisky\n"; 52: CAT boots(frisky); 53: cout << "frisky's age: " << frisky.GetAge() << endl; 54: cout << "boots' age; " << boots.GetAge() << endl; 55: cout << "setting frisky to 7...\n"; 56: frisky.SetAge(7); 57: cout << "frisky's age: " << frisky.GetAge() << endl; 58: cout << "boot's age: " << boots.GetAge() << endl; 59: return 0; 60: } Результат: frisky's age: 5 Setting frisky to 6... Creating boots from frisky frisky's age: 6 boots' age: 6 setting frisky to 7... frisky's age: 7 boots' age: 6 Анализ: В строках программы с 6 по 19 объявляется класс CAT. Обратите внимание, что в строке 9 объявляется конструктор по умолчанию, а в строке 10 — конструктор-копировщик. В строках 17 и 18 объявляется две переменные-члены, представляющие собой указатели на целочисленные значения. В реальной жизни трудно вообразить, для чего может понадобиться создание переменных-членов как указателей на целочисленные значения. Но в данном случае такие переменные являются отличными объектами для демонстрации методов управления переменными-членами, сохраненными в динамической области памяти. Конструктор по умолчанию в строках с 21 по 27 выделяет для переменных области динамической памяти и инициализирует эти переменные. Работа копировщика начинается со строки 29. Обратите внимание, что в копировщике задан параметр rhs. Использовать в параметрах копировщиков символику rhs, что означает right-hand side (стоящий справа), — общепринятая практика. Если вы посмотрите на строки 33 и 34, то увидите, что в выражениях присваивания имена параметров копировщика располагаются справа от оператора присваивания (знака равенства). Вот как работает копировщик. Строки 31 и 32 выделяют свободные ячейки в области динамической памяти. Затем, в строках 33 и 34 в новые ячейки переписываются значения из существующего класса CAT. Параметр rhs соответствует объекту классу CAT, который передается в копировщик в виде константной ссылки. Как объект класса CAT, rhs содержит все переменные- члены любого другого класса CAT. Любой объект класса CAT может открыть доступ к закрытым переменным-членам для любого другого объекта класса CAT. В то же время для внешних обращений всегда лучше создавать открытые члены, где это только возможно. Функция-член rhs.GetAge() возвращает значение, сохраненное в переменной-члене itsAge, адрес которой представлен в rhs. Процедуры, осуществляемые программой, продемонстрированы на рис. 10.3. Значение, на которое ссылалась переменная-член исходного класса CAT, копируется в новую ячейку памяти, адрес которой представлен в такой же переменной-члене нового класса CAT. В строке 47 вызывается объект frisky из класса CAT. Значение возраста, заданное в frisky, выводится на экран, после чего в строке 50 переменной возраста присваивается новое значение — 6. В строке 52 методом копирования объекта frisky создается новый объект boots класса CAT. Если бы в качестве параметра передавался объект frisky, то вызов копировщика осуществлялся бы компилятором. В строках 53 и 54 выводится возраст обеих кошек. Обратите внимание, что в обоих случаях в объектах frisky и boots записан возраст 6, тогда как если бы объект boots создавался не методом копирования, то по умолчанию было бы присвоено значение 5. В строке 56 значение возраста в объекте было изменено на 7 и вновь выведены на экран значения обоих объектов. Значение объекта frisky действительно изменилось на 7, тогда как в boots сохранилось прежнее значение возраста 6. Это доказывает, что переменная объекта frisky была скопирована в объект boots по новому адресу. Рис. 10.3 Пример глубинного копирования Когда выполнение программы выходит за область видимости класса CAT, автоматически запускается деструктор. Выполнение деструктора класса CAT показано в строках с 37 по 43. Оператор delete применяется к обоим указателям — itsAge и itsWeigth, после чего оба указателя для надежности обнуляются. Перегрузка операторовЯзык C++ располагает рядом встроенных типов данных, включая int, real, char и т.д. Для работы с данными этих типов используются встроенные операторы — суммирования (+) и умножения (<<). Кроме того, в C++ сушествует возможность добавлять и перегружать эти операторы для собственных классов. Чтобы в деталях рассмотреть процедуру перегрузки операторов, в листинге 10.6 создается новый класс Counter. Объект класса Counter будет использоваться в других приложениях для подсчета циклов инкрементации, декрементации и других повторяющихся процессов. Листинг 10.6. Класс Counter 1: // Листинг 10.6. 2: // Класс Counter 3: 4: int 5: #include <iostream.h> 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: ~Counter(){ } 12: int GetItsVal()const { return itsVal; } 13: void SetItsVal(int x) { itsVal = x; } 14: 15: private: 16: int itsVal; 17: 18: }; 19: 20: Counter::Counter(): 21: itsVal(0) 22: { } 23: 24: int main() 25: { 25: Counter i; 27: cout << "The value of i is " << i.GetItsVal() << endl; 28: return 0; 29: } Результат: The value of i is 0 Анализ: Судя по определению в строках программы с 7 по 18, это совершенно бесполезный класс. В нем объявлена единственная переменная-член типа int. Конструктор по умолчанию, который объявляется в строке 10 и выполняется в строке 20, инициализирует переменную-член нулевым значением. В отличие от обычной переменной типа int, объект класса Counter не может использоваться в операциях приращения, прибавляться, присваиваться или подвергаться другим манипуляциям. В связи с этим выведение значения данного объекта на печать также сопряжено с рядом трудностей. Запись Функции инкрементаОграничения использования объекта нового класса, которые упоминались выше, можно преодолеть путем перегрузки операторов. Например, существует несколько способов восстановления возможности приращения объекта класса Counter. Один из них состоит в том, чтобы перегрузить функцию инкрементации, как показано в листинге 10.7. Листинг 10.7. Добавление в класс оператора инкремента 1: // Листинг 10.7. 2: // Добавление в класс Counter оператора инкремента 3: 4: int 5: #include <iostream.h> 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: ~Counter(){ } 12: int GetItsVal()const { return itsVal; } 13: void SetItsVal(int x) { itsVal = x; } 14: void Increment() { ++itsVal; } 15: 16: private: 17: int itsVal; 18: 19: }; 20: 21: Counter::Counter(): 22: itsVal(0) 23: { } 24: 25: int main() 26: { 27: Counter i; 28: cout << "The value of i is " << i.GetItsVal() << endl; 29: i.Increment(); 30: cout << "The value of i is " << i.GetItsVal() << endl; 31: return 0; 32: } Результат: The value of i is 0 The vglue of i is 1 Анализ: В листинге 10.7 добавляется функция оператора инкремента, определенная в строке 14. Хотя программа работает, выглядит она довольно неуклюже. Программа из последних сил старается перегрузить ++operator, но это можно реализовать другим способом. Перегрузка префиксных операторовЧтобы перегрузить префиксный оператор, можно использовать функцию следующего типа: returnType Operator op (параметры) В данном случае ор — это перегружаемый оператор. Тогда для перегрузки оператора преинкремента используем функцию void operator++ () Этот способ показан в листинге 10.8. Листинг 10.8 Перегрузка оператора преинкремента 1: // Листинг 10.8. 2: // Перегрузка оператора преинкремента в классе Counter 3: 4: int 5: #include <iostream.h> 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: ~Counter(){ } 12: int GetItsVal()const { return itsVal; } 13: void SetItsVal(int x) { itsVal = x; } 14: void Increment() { ++itsVal; > 15: void operator++ () < ++itsVal; } 16: 17: private: 18: int itsVal; 19: 20: }; 21: 22: Counter::Counter(): 23: itsVal(0) 24: { } 25: 26: int main() 27: { 28: Counter i; 29: cout << "The value of i is " << i.GetItsVal() << endl; 30: i.Increment(); 31: i cout << "The value of i is " << i.GetItsVal() << endl; 32: ++i; 33: cout << "The value of i is " << i.GetItsVal() << endl; 34: return 0; 35: } Результат: The value of i is 0 The value of i is 1 The value of i is 2 Анализ: В строке 15 перегружается operator++, который затем используется в строке 32 в результате объект класса Counter получает функции, которые можно было ожидать судя по его названию. Далее объекту сообщаются дополнительные возможности, призванные повысить эффективность его использования, в частности возможность контроля за максимальным значением, которое нельзя превышать в ходе приращения. Но в работе перегруженного оператора инкремента существует один серьезный недостаток. В данный момент в программе не удастся выполнить следующее выражение: Counter а = ++i; В этой строке делается попытка создать новый объект класса Counter — а, которому присваивается приращенное значение переменной i. Хотя встроенный конструктор- копировщик поддерживает операцию присваивания, текущий оператор инкремента не возвращает объект класса Counter. Сейчас он возвращает пустое значение void. Невозможно присвоить значение void объекту класса Counter. (Невозможно создать что-то из ничего!) Типы возвратов перегруженных функций операторовВсе, что нам нужно, — это возвратить объект класса Counter таким образом, чтобы ero можно было присвоить другому объекту класса Counter. Как это сделать? Один подход состоит в том, чтобы создать временный объект и возвратить его. Он показан в листинге 10.9. Листинг 10.8. Возвращение временного объекта 1: // Листинг 10.9. 2: // Возвращение временного объекта 3: 4: int 5: #include <iostream.h> 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: ~Counter(){ } 12: int GetItsVal()const { return itsVal; } 13: void SetItsVal(int x) { itsVal = x; } 14: void Increment() { ++itsVal; } 15: Counter operator++ (); 16: 17: private: 18: int itsVal; 19: 20: }; 21: 22: Counter::Counter(): 23: itsVal(0) 24: { } 25: 26: Counter Counter::operator++() 27: { 28: ++itsVal; 29: Counter temp; 30: temp.SetItsVal(itsVal); 31: return temp; 32: } 33: 34: int main() 35: { 36: Counter i; 37: cout << "The value of i is " << i.GetItsVal() << endl; 38: i.Incrernent(); 39: cout << "The value of i is " << i.GetItsVal() << endl; 40: ++i; 41: cout << "The value of i is " << i.GetItsVal() << endl; 42: Counter а = ++i; 43: cout << "The value of a: " << a.GetItsVal(); 44: cout << " and i: " << i.GetItsVal() << endl; 45: return 0; 46: } Результат: The value of i is 0 The value of i is 1 The value of i is 2 The value of a: 3 and i: 3 Анализ: В данной версии программы operator++ объявлен в строке 15 таким образом, что может возвращать объекты класса Counter. В строке 29 создается временный объект ternp, и ему присваивается значение текущего объекта Counter. Значение временной переменной возвращается и тут же, в строке 42, присваивается новому объекту а. Возвращение безымянных временных объектовВ действительности нет необходимости присваивать имя временному объекту, как это было сделано в предыдущем листинге в строке 29. Если в классе Counter есть принимающий значение конструктор, то параметру этого конструктора можно просто присвоить значение возврата оператора инкремента. Эта идея реализована в листинге 10.10. Листинг 10.10. Возвращение безымянного временного объекта 1: // Листинг 10.10. 2: // Возвращение безымянного временного объекта 3: 4: int 5: #include <iostream.h> 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: Counter(int val); 12: ~Counter(){ } 13: int GetItsVal()const { return itsVal; } 14: void SetItsVal(int x) { itsVal = x; } 15: void Increment() { ++itsVal; } 16: Counter operator++ (); 17: 18: private: 19: int itsVal; 20: 21: }; 22: 23: Counter::Counter(): 24: itsVal(0) 25: { } 26: 27: Counter::Counter(intval): 28: itsVal(val) 29: { } 30: 31: CounterCounter::operator++() 32: { 33: ++itsVal; 34: return Counter (itsVal); 35: } 36: 37: int main() 38: { 39: Counter i; 40: cout << "The value of i is" << i.GetItsVal() << endl; 41: i.Increment(); 42: cout << "The value of i is" << i.GetItsVal() << endl; 43: ++i; 44: cout << "The value of i is" << i.GetItsVal() << endl; 45: Counter a = ++i; 46: cout << "The value of a: " << a.GetItsVal(); 47: cout << " and i: " << i.GetItsVal() << endl; 48: return 0; 49: } Результат: The value of i is 0 The value of i is 1 The value of i is 2 The value of a: 3 and i: 3 Анализ: В строке 11 определен новый конструктор, который принимает значение типа int. Данный конструктор выполняется в строках с 27 по 29. Происходит инициализация переменной itsVal значением, переданным в параметре. Выполнение оператора инкремента в данной программе упрощено. В строке 33 осуществляется приращение переменной itsVal. Затем в строке 34 создается временный объект класса Counter, которому присваивается значение переменной itsVal. Это значение затем возвращается как результат выполнения оператора инкремента. Подобное решение выглядит более элегантно, но возникает вопрос, для чего вообще нужно создавать временные объекты. Напомним, что создание и удаление временного объекта в памяти компьютера требует определенных временных затрат. Кроме того, если объект i уже существует и имеет правильное значение, почему бы просто не возвратить его? Реализуем эту идею с помощью указателя this. Использование указателя thisНа прошлом занятии уже рассматривалось использование указателя this. Этот указатель можно передавать в функцию-член оператора инкремента точно так же, как в любую другую функцию-член. Указатель this связан с объектом i и в случае разыменовывания возвращает объект, переменная которого itsVal уже содержит правильное значение. В листинге 10.11 показано возвращение указателя this, что снимает необходимость создания временных объектов. Листинг 10.11. Возвращение указателя this 1: // Листинг 10.11. 2: // Возвращение указателя this 3: 4: int 5: #include <iostream.h> 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: ~Counter(){ } 12: int GetItsVal()const { return itsVal; } 13: void SetItsVal(int x) { itsVal = x; } 14: void Increment() { ++itsVal; } 15: const Counter& operator++ (); 16: 17: private: 18: int itsVal; 19: 20: }; 21: 22: Counter::Counter(): 23: itsVal(0) 24: { }; 25: 26: const Counter& Counter::operator++() 27: { 28: ++itsVal; 29: return *this; 30: } 31: 32: int main() 33: { 34: Counter i; 35: cout << "The value of i is " << i.GetItsVal() << endl; 36: i.Increment(); 37: cout << "The value of i is " << i.GetItsVal() << endl; 38: ++i; 39: cout << "The value of i is " << i.GetItsVal() << endl; 40: Counter а = ++i; 41: cout << "The value of a: " << a.GetItsVal(); 42: cout << " and i: " << i.GetItsVal() << endl; 43: return 0; 44: } Результат: The value of i is 0 The value of i is 1 The value of i is 2 The value of a: 3 and i: 3 Анализ: Выполнение оператора приращения в строках с 26 по 30 заменено разыменовыванием указателя this и возвращением текущего объекта. В результате объект класса Counter присваивается новому объекту а этого же класса. Как уже отмечалось выше, если объект класса Counter требует выделения памяти, необходимо заместить конструктор-копировщик. Но в данном случае конструктор- копировщик, заданный по умолчанию, отлично справляется со своими задачами. Обратите внимание, что возвращаемое значение представляет собой ссылку класса Counter, благодаря чему отпадает необходимость в создании каких-либо дополнительных временных объектов. Ссылка задана как const, поскольку не должна меняться при использовании в функции. Перегрузка постфиксных операторовДо сих пор рассматривалась перегрузка оператора преинкремента. Что если перегрузить оператор постинкремента? Тут перед компилятором встает проблема: как различить между собой операторы постинкремента и преинкремента. Существует договоренность, что при определении функции оператора постинкремента устанавливается целочисленный параметр. Значение параметра не имеет смысла. Он используется только как флаг, который сообщает, что перед нами оператор постинкремента. Различия между преинкрементном и постинкрементномПрежде чем приступить к перегрузке оператора постинкремента, следует четко понять, чем он отличается от оператора преинкремента. Подробно эта тема рассматривалась на занятии 4 (см. листинг 4.3). Вспомните, преинкремент означает прирастить, затем возвратить значение, а постинкремент — возвратить значение, а потом прирастить. Точно так же и в нашем примере оператор преинкремента приращивает значение, после чего возвращает объект, а оператор постинкремента возвращает объект с исходным значением. Чтобы проследить этот процесс, нужно создать временный объект, в котором будет сохранено исходное значение, затем выполнить приращение в исходном объекте и вновь вернуть его во временный объект. Давайте все это повторим еще раз. Посмотрите на следующее выражение: а = x++; Если исходно переменная x равнялась 5, то в этом выражении переменной а будет присвоено значение 5, зато переменная x станет равной 6. Если x не просто переменная, а объект, то его оператор постинкремента должен сохранить исходное значение 5 во временном объекте, прирастить значение объекта x до 6, после чего возвратить значение временного объекта и присвоить его объекту а. Обратите внимание, что, поскольку речь идет о временном объекте, его следует возвращать как значение, а не как ссылку, так как временный объект выйдет из области видимости как только функция возвратит свое значение. В листинге 10.12 показано использование обоих операторов. Листинг 10.12. Операторы преинкремента и постинкремента 1: // Листинг 10.12. 2: // Возвращение разыменованного указателя this 3: 4: int 5: #include <iostream.h> 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: ~Counter(){ } 12: int GetItsVal()const { return itsVal; } 13: void SetItsVal(int x) { itsVal = x; } 14: const Counter& operator++ (); // оператор преинкремента 15: const Counter operator++ (int); // оператор постинкремента 16: 17: private: 18: int itsVal; 19: }; 20: 21: Counter::Counter(): 22: itsVal(0) 23: { } 24: 25: const Counter& Counter::operator++() 26: { 27: ++itsVal; 28: return *this; 29: } 30: 31: const Counter Counter::operator++(int x) 32: { 33: Counter temp(*this); 34: ++itsVal; 35: return temp; 36: } 37: 38: int main() 39: { 40: Counter i; 41: cout << "The value of i is " << i.GetItsVal() << endl; 42: i++; 43: cout << "The value of i is " << i.GetItsVal() << endl; 44: ++i; 45: cout << "The value of i is " << i.GetItsVal() << endl; 46: Counter а = ++i; 47: cout << "The value of а: " << a.GetItsVal(); 48: cout << " and i: " << i.GetItsVal() << endl; 49: а = i++; 50: cout << "The value of а: " << a.GetItsVal(); 51: cout << " and i: " << i.GetItsVal() << endl; 52: return 0; 53: } Результат: The value of i is 0 The value of i is 1 The value of i is 2 The value of a: 3 and i: 3 The value of a: 3 and i: 4 Анализ: Оператор постинкремента объявляется в строке 15 и выполняется в строках с 31 по 36. Обратите внимание, что в объявлении оператора преинкремента в строке 14 не задан целочисленный параметр x, выполняющий роль флага. При определении оператора постинкремента используется флагх, чтобы указать компилятору, что это именно постинкремент. Значение параметра x нигде и никогда не используется. Синтаксис перегрузки операторов с одним операндом Объявление перегруженных операторов выполняется так же, как и функций. Используйте ключевое слово operator, за которым следует сам перегружаемый оператор. В функциях операторов с одним операндом параметры не задаются, за исключением операторов no- стинкремента и постдекремента, в которых целочисленный параметр играет роль флага. Пример перегрузки оператора преинкремента: const Counter&Countcr::operator++ (); Пример перегрузки оператора постдекремента: const Counter&Counter::operator-- (int); Оператор суммированияОператоры приращения, рассмотренные выше, оперируют только с одним операндом. Оператор суммирования (+) — это представитель операторов с двумя операндами. Он выполняет операции с двумя объектами. Как выполнить перегрузку оператора суммирования для класса Counter? Цель состоит в том, чтобы объявить две переменные класса Counter, после чего сложить их, как в следующем примере: Counter переменная_один, переменная_два, переменная_три; переменная_три= переменная_один + переменная_два; Начнем работу с записи функции Add(), в которой объект Counter будет выступать аргументом. Эта функция должна сложить два значения, после чего возвратить Counter с полученным результатом. Данный подход показан в листинге 10.13. Листинг 10.13. Функция Add() 1: // Листинг 10.13. 2: // Функция Add 3: 4: int 5: #include <iostream.h> 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: Counter(int initialValue); 12: ~Counter(){ } 13: int GetItsVal()const {return itsVal; } 14: void SetItsVal(int x) {itsVal = x; } 15: Counter Add(const Counter &); 16: 17: private: 18: int itsVal; 19: 20: }; 21: 22: Counter::Counter(int initialValue): 23: itsVal(initialValue) 24: { } 25: 26: Counter::Counter(); 27: itsVal(0) 28: { } 29: 30: Counter Counter::Add(const Counter & rhs) 31: { 32: return Counter(itsVal+ rhs.GetItsVal()); 33: } 34: 35: int main() 36: { 37: Counter varOne(2), varTwo(4), varThree; 38: varThree = varOne.Add(varTwo); 39: cout << "var0ne: " << varOne.GetItsVal()<< endl; 40: cout << "varTwo: " << varTwo.GetItsVal() << endl; 41: cout << "varThree: " << varThree.GetItsVal() << endl; 42: 43: return 0; 44: } Результат: varOne: 2 varTwo: 4 varThree: 6 Анализ: Функция Add() объявляется в строке 15. В функции задана константная ссылка на Counter, представляющая число, которое нужно добавить к текущему объекту. Функция возвращает объект класса Counter, представляющий собой результат суммирования, который присваивается операнду слева от оператора присваивания (=), как показано в строке 38. Здесь переменная varOne является объектом, varTwo — параметр функции Add(), а varThree — адресный операнд, которому присваивается результат суммирования. Чтобы создать объект varThree без исходной инициализации каким-либо значением, используется конструктор, заданный по умолчанию. Он присваивает объекту varThree нулевое значение, как показано в строках 22—24. Иначе эту проблему можно было решить, присвоив нулевое значение конструктору, определенному в строке 11. Перегрузка оператора суммированияТело функции Add() показано в строках 30—33. Программа работает, но несколько замысловато. Перегрузка оператора суммирования (+) сделала бы работу класса Counter более гармоничной (листинг 10.14). Листинг 10.14. Перегрузка оператора суммирования 1: // Листинг 10.14. 2: //Перегрузка оператора суммирования (+) 3: 4: int 5: #include <iostream.h> 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: Counter(int initialValue); 12: ~Counter(){ } 13: int GetItsVal()const { return itsVal; } 14: void SetItsVal(int x) { itsVal = x; } 15: Counter operator+ (const Counter &); 16: private: 17: int itsVal; 18: }; 19: 20: Counter::Counter(int initialValue): 21: itsVal(initialValue) 22: { } 23: 24: Counter::Counter(): 25: itsVal(0) 26: { } 27: 28: Counter Counter::operator+ (const Counter & rhs) 29: { 30: return Counter(itsVal + rhs.GetItsVal()); 31: } 32: 33: int main() 34: { 35: Counter varOne(2), varTwo(4), varThree; 36: varThree = varOne + varTwo; 37: cout << "varOne: " << varOne.GetItsVal()<< endl; 38: cout << "varTwo: " << varTwo.GetItsVal() << endl; 39: cout << "varThree: " << varThree.GetItsVal() << endl; 40: 41: return 0; 42: } Результат: varOne: 2 varTwo: 4 varThree: 6 Анализ: В строке 15 объявлен оператор суммирования (operator+), функция которого определяется в строках 28—31. Сравните эту функцию с объявлением и определением функции Add() в предыдущем листинге. Они почти идентичны. В то же время далее в программе эти функции используются совершенно по разному. Посмотрите, следующая запись с оператором (+) выглядит естественней и понятнее varThree = varOne + varTwo; чем строка с функцией Add(): varThree = varOne.Add(varTwo); Для компилятора различия не принципиальные, но программа при этом становится более понятной и читабельной, что облегчает работу программиста. Примечание:Метод, используемый для перегрузки оператора суммирования (operator++), можно применять также с другими операторами, например, оператором вычитания (operator--). Перегрузка операторов с двумя операндами Операторы с двумя операндами объявляются так же, как и операторы с одним операндом, за исключением того, что функции этих операторов содержат параметры. Параметры представляют собой константные ссылки на объекты таких же типов. Пример перегрузки оператора суммирования для класса Ciuinio-: Counter Counter::operator+ (const Counter & rhs); Пример перегрузки оператора вычитания для этого же класса: Counter Counter::operator- (const Counter & rhs); Основные принципы перегрузки операторовПерегруженные операторы могут быть функциями-членами, как в примерах этой главы, либо задаваться функциями-друзьями, не принадлежащими классу. Более подробно такие операторы будут рассматриваться на занятии 14 во время изучения специальных классов и функций. Ряд операторов могут быть исключительно членами класса. Это операторы присваивания (=), индексирования ([]), вызова функции (()) и косвенного обращения к члену класса (->). Оператор индексирования [ ] будет рассмотрен на следующем занятии, а оператор косвенного обращения к члену класса — на занятии 14 во время изучения дополнительных возможностей указателей. Ограничения перегрузки операторовНельзя перегружать операторы стандартных типов данных (такие как int). Также нельзя изменять установленные приоритеты и ассоциативности операторов. Например, нельзя оператор с одним операндом перегрузить так, чтобы использовать его с двумя операндами. Кроме того, методом перегрузки нельзя создавать новые операторы; например, бинарный оператор умножения (**) не удастся объявить как оператор возведения в квадрат. Количество операндов, которыми может манипулировать оператор, — важная характеристика каждого оператора. Различают операторы, используемые с одним операндом (например, оператор инкремента: myValue++), и операторы, для работы которых необходимо указать два операнда (например, оператор суммирования: a+b). Сразу тремя операндами управляет только условный оператор ?, синтаксис использования которого показан в следующем примере: (а > b ? x : у). Что можно перегружатьВозможность перегрузки операторов — это то новое средство программирования, предоставляемое C++, которое наиболее широко используют (а часто и злоупотребляют им) начинающие программисты. Новичков захватывает азарт присвоения новых интересных функций самым обычным и заурядным операторам. В результате код программы может оказаться непонятным и нечитабельным даже для создателя, а не то что для другого программиста. Безусловно, если в программе оператор + начнет осуществлять вычитание, а оператор * — суммирование, это может тешить самолюбие начинающего программиста, но профессионал никогда такого не допустит. Вполне можно понять желание использовать оператор + для конкатенации строк и символов, а оператор / для разделения строк, но такая перегрузка операторов таит в себе подводные рифы, на которые может совершенно неожиданно напороться программа во время выполнения. Возможно, было бы не плохо уделить больше внимания особенностям использования перегруженных операторов, но еще лучше начать с формулировки основных предостережений. Прежде всего следует помнить, что основная цель перегрузки операторов состоит в том, чтобы сделать программу эффективнее, а ее код проще и понятнее. Рекомендуется:Перегружайте операторы, если код программы после этого станет четче и понятнее. Возвращайте объекты класса из перегруженных операторов. Не рекомендуется:Не увлекайтесь созданием перегруженных операторов, выполняющих несвойственные им функции. Оператор присваиванияЧетвертая, и последняя, функция, предоставляемая компилятором для работы с объектами, если, конечно, вы не задали никаких дополнительных функций, это функция оператора присваивания (operator=()). Этот оператор используется всякий раз, когда нужно присвоить объекту новое значение, например: CAT catOne(5,7); CAT catTwo(3,4); //...другие строки программы catTwo = catOne В данном примере создан объект catOne, переменной которого itsAge присвоено значение 5, а переменной itsWeigth — 7. Затем создается объект catTwo со значениями переменных соответственно 3 и 4. Через некоторое время объекту catTwo присваиваются значения объекта catOne. Что произойдет, если переменная itsAge является указателем, и что происходит со старыми значениями переменных объекта catTwo? Работа с переменными-членами, которые хранят свои значения в области динамической памяти, рассматривалась ранее при обсуждении использования конструктора- копировщика (см. также рис. 10.1 и 10.2). В C++ различают поверхностное и глубинное копирование данных. При поверхностном копировании происходит передача только адреса от одной переменной к другой, в результате чего оба объекта указывают на одни и те же ячейки памяти. В случае глубинного копирования действительно происходит копирование значений переменных из одной области памяти в другую. Различия между этими методами копирования показаны на рис. 10.3. Все вышесказанное справедливо для присвоения данных. В случае использования оператора присваивания, процесс обмена данных протекает с некоторыми особенностями. Так, объект catTwo уже существует вместе со своими переменными, для каждой из которых выделены определенные ячейки памяти. В случае присвоения объекту новых значений предварительно необходимо освободить эти ячейки памяти. Что произойдет, если выполнить присвоение объекта catTwo самому себе: catTwo = catTwo Вряд ли такая строка в программе может иметь смысл, но в любом случае программа должна уметь поддерживать подобные ситуации. Дело в том, что присвоение объекта самому себе может произойти по ошибке в случае косвенного обращения к указателю, который ссылается на тот же объект. Если не предусмотреть поддержку такой ситуации, то оператор присваивания сначала очистит ячейки памяти объекта catTwo, а затем попытается присвоить объекту catTwo свои собственные значения, которых уже не будет и в помине. Чтобы предупредить подобную ситуацию, ваш оператор присваивания прежде всего должен определить, не совпадают ли друг с другом объекты по обе стороны от оператора присваивания. Это можно осуществить с помощью указателя this, как показано в листинге 10.15. Листинг 10.15. Оператор присваивания 1: // Листинг 10.15. 2: // Конструктор-копировщик 3: 4: #include <iostream.h> 5: 6: class CAT 7: { 8: public: 9: CAT(); // конструктор по умолчанию 10: // конструктор-копировщик и деструктор пропущены! 11: int GetAge() const { return *itsAge; } 12: int GetWeight() const { return *itsWeight; } 13: void SetAge(int age) { *itsAge = age; } 14: CAT & operator=(const CAT &); 15: 16: private: 17: int *itsAge; 18: int *itsWeight; 19: }; 20: 21: CAT::CAT() 22: { 23: itsAge = new int; 24: itsWeight = new int; 25: *itsAge = 5; 26: *itsWeight = 9; 27: } 28: 29: 30: CAT & CAT::operator=(const CAT & rhs) 31: { 32: if (this == &rhs) 33: return *this; 34: *itsAge = rhs.GetAge(); 35: *itsWeight = rhs.GetWeight(); 36: return *this; 37: } 38: 39: 40: int main() 41: { 42: CAT frisky; 43: cout << "frisky's age: " << frisky.GetAge() << endl; 44: cout << "Setting frisky to 6...\n"; 45: frisky.SetAge(6); 46: CAT whiskers; 47: cout << "whiskers' age: " << whiskers.GetAge() << endl; 48: cout << "copying frisky to whiskers...\n"; 49: whiskers = frisky; 50: cout << "whiskers' age: " << whiskers.GetAge() << endl; 51: return 0; 52: } Результат: frisky's age: 5 Setting frisky to 6. . . whiskers' age: 5 copying frisky to whiskers... whiskers' age: 6 Анализ: В листинге 10.15 вновь используется класс CAT. Чтобы не повторяться, в данном коде пропущены объявления конструктора-копировщика и деструктора. В строке 14 объявляется оператор присваивания, определение которого представлено в строках 30—37. В строке 32 выполняется проверка того, не является ли объект, которому будет присвоено значение, тем же самым объектом класса CAT, чье значение будет присвоено. Чтобы проверить это, сравниваются адреса в указателях rhs и this. Безусловно, оператор присваивания (=) может быть произвольно перегружен таким образом, чтобы отвечать представлениям программиста, что означает равенство объектов. Операторы преобразованийЧто происходит при попытке присвоить значение переменой одного из базовых типов, таких как int или unsigned short, объекту класса, объявленного пользователем? В листинге 10.16 мы опять вернемся к классу Counter и попытаемся присвоить объекту этого класса значение переменной типа int. Предупреждение:Листинг 10.16 не компилируйте! Листинг 10.16. Попытка присвоить объекту класса Counter значение переменной типа int 1: // Листинг 10.16. 2: // Эту программу не компилируйте! 3: 4: int 5: #include <iostream.h> 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: ~Counter(){ } 12: int GetItsVal()const { return itsVal; } 13: void SetItsVal(int x) { itsVal = x; } 14: private: 15: int itsVal; 16: 17: }; 18: 19: Counter::Counter(): 20: itsVal(0) 21: { } 22: 23: int main() 24: { 25: int theShort = 5; 26; Counter theCtr = theShort; 27: cout << "theCtr: " << theCtr.GetItsVal() << endl; 28: return 0; 29: } Результат: Компилятор покажет сообщение об ошибке, поскольку не сможет преобразовать тип int в Counter. Анализ: Класс Counter, определенный в строках 7—17, содержит только один конструктор, заданный по умолчанию. В нем не определено ни одного метода преобразования данных типа int в тип Counter, поэтому компилятор обнаруживает ошибку в строке 26. Компилятор ничего не сможет поделать, пока не получит четких инструкций, что данные типа int необходимо взять и присвоить переменной-члену itsVal. В листинге 10.17 эта ошибка исправлена с помощью оператора преобразования типов. Определен конструктор, который создает объект класса Counter и присваивает ему полученное значение типа int. Листинг 10.17. Преобразование int в Counter 1: // Листинг 10.17. 2: // Использование конструктора в качестве оператора преобразования типа 3: 4: int 5: #include <iostream.h> 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: Counter(int val); 12: ~Counter(){ } 13: int GetItsVal()const { return itsVal; } 14: void SetItsVal(int x) { itsVal = x; } 15: private: 16: int itsVal; 17: 18: }; 19: 20: Counter::Counter(): 21: itsVal(0) 22: { } 23: 24: Counter::Counter(intval): 25: itsVal(val) 26: { } 27: 28: 29: int main() 30: { 31: int theShort = 5; 32: Counter theCtr = theShort; 33: cout << "theCtr: " << theCtr.GetItsVal() << endl; 34: return 0; 35: } Результат: the Ctr: 5 Анализ: Важные изменения произошли в строке 11, где конструктор перегружен таким образом, чтобы принимать значения типа int, а также в строках 24—26, где данный конструктор применяется. В результате выполнения конструктора переменной-члену класса Counter присваивается значение типа int. Для присвоения значения программа обращается к конструктору, в котором присваиваемое значение передается в качестве аргумента. Процесс осуществляется в несколько шагов. Шаг 1: создание переменной класса Counter с именем theCtr. Это то же самое, что записать: int x = 5, где создается целочисленная переменная x и ей присваивается значение 5. Но в нашем случае создается объект theCtr класса Counter, который инициализируется переменной theShortTHna short int. Шаг 2: присвоение объекту theCtr значения переменной theShort. Но переменная относится к типу short, а не Counter! Первое, что нужно сделать, — это преобразовать ее к типу Counter. Компилятор может делать некоторые преобразования автоматически, но ему нужно точно указать, чего от него хотят. Именно для инструктирования компилятора создается конструктор класса Counter, который содержит единственный параметр, например типа short: class Counter { Counter (short int x); // ... }; Данный конструктор создает объект класса Counter, используя временный безымянный объект этого класса, способный принимать значения типа short. Чтобы сделать этот процесс более наглядным, предположим, что для значений типа short создается не безымянный объект, а объект класса Counter с именем wasShort. Шаг 3: присвоение значения объекта wasShort объекту theCtr, что эквивалентно записи "theCtr = wasShort"; На этом шаге временный объект wasShort, созданный при запуске конструктора, замещается на постоянный объект theCtr, принадлежащий классу Counter. Другими словами, значение временного объекта присваивается объекту theCtr. Чтобы понять, как происходит этот процесс, следует четко уяснить принципы работы, справедливые для ВСЕХ перегруженных операторов, определенных с помощью ключевого слова operator. В случае с операторами с двумя операндами (такими как = или +) находящийся справа операнд объявляется как параметр функции оператора, заданной в конструкторе. Так, выражение а = b объявляется как a.operator=(b); Что произойдет, если изменить порядок присвоения, как в следующем примере: 1: Counter theCtr(5); 2: int theShort = theCtr; 3: cout << "theShort : " << theShort << endl; Вновь компилятор покажет сообщение об ошибке. Хотя сейчас компилятор уже знает, как создать временный объект Counter для принятия значения типа int, но он не знает, как осуществить обратный процесс. Операторы преобразования типовЧтобы разрешить эту и подобные ей проблемы, в C++ есть специальные операторы преобразования типов, которые можно добавить в пользовательский класс. В результате появится возможность явного преобразования типа пользовательского класса к любому из базовых типов данных языка программирования. Реализация этой возможности показана в листинге 10.18. Только одно замечание: в операторах преобразований не задается тип возврата. Даже если их работа напоминает возврат функции, в действительности они возвращают преобразованное значение. Листинг 10.18. Преобразования данных типа Counter в тип unsigned short() 1: #include <iostream.h> 2: 3: class Counter 4: { 5: public: 6: Counter(); 7: Counter(int val); 8: ~Counter(){ } 9: int GetItsVal()const { return itsVal; } 10: void SetItsVal(int x) { itsVal = x; } 11: operator unsigned short(); 12: private: 13: int itsVal; 14: 15: }; 16: 17: Counter::Counter(): 18: itsVal(0) 19: { } 20: 21: Counter::Counter(int val): 22: itsVal(val) 23: { } 24: 25: Counter::operator unsigned short () 26: { 27: return ( int (itsVal) ); 28: } 29: 30: int main() 31: { 32: Counter ctr(5); 33: int theShort = ctr; 34: cout << "theShort: " << theShort << endl; 35: return 0; 36: } Результат: theShort: 5 Анализ: В строке 11 объявляется оператор преобразования типа. Обратите внимание, что в нем не указан тип возврата. Функция оператора преобразования выполняется в строках 25—28. В строке 27 возвращается значение объекта itsVal, преобразованное в тип int. Теперь компилятор знает, как присвоить объекту класса значение типа int и как возвратить из объекта класса текущее значение, чтобы присвоить его внешней переменной типа int. РезюмеСегодня вы научились перегружать функции-члены пользовательского класса. Вы также узнали, как передавать в функции значения, заданные по умолчанию, и в каких случаях вместо значений по умолчанию лучше использовать перегруженные функции. Перегрузка конструкторов класса позволяет более гибко управлять классами и создавать новые классы, содержащие объекты других классов. Лучше всего инициализацию объектов класса осуществлять во время инициализации конструктора, вместо того чтобы делать это в теле конструктора. Конструктор-копировщик и оператор присваивания по умолчанию предоставляются компилятором, если в классе эти объекты не были созданы пользователем. Но при использовании копировщика и оператора присваивания, заданных по умолчанию, осуществляется только поверхностное копирование данных. В тех классах, где в числе членов класса используются указатели на области динамической памяти, вместо поверхностного копирования лучше использовать глубинное, при котором копируемые данные размещаются по новым адресам. Хотя в языке C++ можно произвольно перегружать все операторы, настоятельно рекомендуем не создавать таких операторов, функции которых противоречат их традиционному использованию. Кроме того, невозможно изменить ассоциативность оператора, а также создавать собственные операторы, не представленные в языке C++. Указатель this ссылается на текущий объект и является невидимым параметром для всех функций-членов. Разыменованный указатель this часто возвращается перегруженными операторами. Операторы преобразования типов позволяют настраивать классы для использования в выражениях, осуществляющих обмен данными разных типов. Данные операторы являются исключением из правила, состоящего в том, что все функции возвращают явные значения, как, например, конструктор и деструктор. В данных операторах тип возврата не устанавливается. Вопросы и ответыЗачем использовать значения, заданные по умолчанию, если можно перегрузить функцию? Проще иметь дело с одной функцией, чем с двумя. Кроме того, зачастую проще понять работу функции, использующей значения, заданные по умолчанию, чем каждый раз внимательно изучать тело функции, чтобы понять ее назначение. Кроме того, обновление одной версии функции без обновления другой версии часто бывает причиной ошибок в работе программы. Почему бы тогда постоянно не использовать только значения, заданные по умолчанию? Перегрузка функций предоставляет ряд возможностей, которые нельзя реализовать, используя только значения, заданные по умолчанию. Например, изменять не только число параметров в списке, но и их типы. Какие переменные-члены следует инициализировать одновременно с инициализацией конструктора, а какие оставлять для тела конструктора? Используйте следующее простое правило: одновременно с конструктором следует инициализировать как можно больше переменных-членов. Только некоторые из них, такие как переменные для текущих вычислений и управления выводом на печать следует инициализировать в теле конструктора. Может ли перегруженная функция содержать параметры, заданные по умолчанию? Конечно. Нет никакой причины, по которой не следовало бы использовать это мощное средство. Одна или несколько версий перегруженных функций могут иметь собственные значения, заданные по умолчанию. При установке значений по умолчанию для перегруженных функций нужно следовать тем же общим правилам, что и при установке значений по умолчанию для обычных функций. Почему одни функции-члены определяются в описании класса, а другие нет? Если функция определяется в описании класса, то далее она используется в режиме inline. Впрочем, встраивание кода функции по месту вызова происходит только в том случае, если функция достаточно простая. Также следует отметить, что задать встраивание кода функции-члена в код программы можно с помощью ключевого слова inline, даже если эта функция была описана отдельно от класса. КоллоквиумВ этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний и приводится несколько упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из предложенных ниже вопросов. Контрольные вопросы1. Если вы перегрузили функцию-член, как потом можно будет различить разные варианты функции? 2. Какая разница между определением и объявлением? 3. Когда вызывается конструктор-копировщик? 4. Когда вызывается деструктор? 5. Чем отличается конструктор-копировщик от оператора присваивания (=)? 6. Что представляет собой указатель this? 7. Как отличается перегрузка операторов предварительного и последующего действия? 8. Можно ли перегрузить operator+ для переменных типа short int? 9. Допускается ли в C++ перегрузка operator++ таким образом, чтобы он выполнял в классе операцию декремента? 10. Как устанавливается тип возврата в объявлениях функций операторов преобразования типов? Упражнения1. Представьте объявление класса SimpleCircle с единственной переменой-членом itsRadius. В классе должны использоваться конструктор и деструктор, заданные по умолчанию, а также метод установки радиуса. 2. Используя класс, созданный в упражнении 1, с помощью конструктора, заданного по умолчанию, инициализируйте переменную itsRadius значением 5. 3. Добавьте в класс новый конструктор, который присваивает значение своего параметра переменной itsRadius. 4. Перегрузите операторы преинкремента и постинкремента для использования в вашем классе SimpleCircle с переменной itsRadius. 5. Измените SimpleCircle таким образом, чтобы сохранять itsRadius в динамической области памяти и фиксировать существующие методы. 6. Создайте в классе SimpleCircle конструктор-копировщик. 7. Перегрузите в классе SimpleCircle оператор присваивания. 8. Напишите программу, которая создает два объекта класса SimpleCircle. Для создания одного объекта используйте конструктор, заданный по умолчанию, а второму экземпляру при объявлении присвойте значение 9. С каждым из объектов используйте оператор инкремента и выведите полученные значения на печать. Наконец, присвойте значение одного объекта другому объекту и выведите результат на печать. 9. Жучки: что неправильно в следующем примере использования оператора присваивания? SQUARE SQUARE::operator=(const SQARE & rhs) { itsSide = new int; *itsSide = rgs.GetSide(); return << this; } 10. Жучки: что неправильно в следующем примере использования оператора суммирования? VeryShort VeryShort::operator+ (const VeryShort& rhs) { itsval += rhs.GetItsVal(); return *this; } День 11-й. НаследованиеФундаментальной основой человеческого мышления является поиск, выявление и построение взаимоотношений между различными концепциями. Чтобы постичь хитросплетения отношений между вещами и явлениями, мы используем иерархические построения, матрицы, сети и прочие средства визуализации. Чтобы лучше выразить суть отношений между объектами, в C++ используется иерархическая система наследования. Сегодня вы узнаете: • Что представляет собой наследование • Как произвести один класс из другого • Что такое защищенный доступ и как его использовать • Что такое виртуальные функции Что такое наследованиеЧто такое собака? Что вы видите, когда смотрите на своего питомца? Я вижу четыре лапы, обслуживающие зубастую пасть. Биолог увидит систему взаимодействующих органов, физик — стройную систему атомов и совокупность разных видов энергии, а ученый, занимающийся систематикой млекопитающих, — типичного представителя вида Canis familiaris. Каждый смотрит на объект со своей точки зрения, но сегодня нас будет интересовать последнее утверждение, а именно: собака является представителем семейства волчьих, класса млекопитающих и т.д. С точки зрения систематики любой объект живой природы рассматривается в плане принадлежности одной системе иерархических таксонов: царству, типу, классу, отряду, семейству, роду и виду. Иерархия представляет собой вид отношений подчиненности типа принадлежности частного общему. Так, человек является видом приматов. Подобный тип отношений можно видеть повсюду. Грузовик является видом машин, а машина, в свою очередь, является видом транспортных средств. Пирожное является видом сладких блюд, а сладкие блюда являются видом пищи. Когда мы говорим, что нечто является видом чего-то, то подразумеваем большую детализацию объявления объекта. Так, отмечая, что машина — это вид транспортных средств, мы из всевозможных средств передвижения (от повозки до самолета) выбираем только четырехколесные устройства с двигателем. Иерархия и наследованиеГоворя о собаке, как представителе класса млекопитающих, мы подразумеваем, что она наследует все признаки, общие для класса млекопитающих. Поскольку собака — млекопитающее, можно предположить, что это подвижный вид животных, дышащих воздухом. Все млекопитающие по определению двигаются и дышат воздухом. Если определить некий объект как собаку, это добавит к объявлению способность вилять хвостом, грызть рукопись книги, которую я как раз собрался нести в редакцию, бегать по дому и лаять, когда я сплю... о извините, куда меня занесло! Ну так вот, продолжим. В свою очередь, собак можно разделить на служебных, спортивных и охотничьих. Потом можно пойти дальше и описать породу собаки: спаниель, лабрадор и т.д. Таким образом, мы можем сказать, например, что фокстерьер — это порода охотничьих собак, в которой представлены все признаки, общие для собак вообще, а также все признаки, общие для млекопитающих и т.д., включая признаки всех таксонов, к которым относится фокстерьер. Пример такой иерархии показан на рис. 11.1, где стрелками связаны категории более низкого уровня с категориям следующего порядка. Рис. 11.1. Иерархия млекопитающих В C++ иерархичность реализована в концепции классов, где один класс может происходить, или наследоваться от класса более высокого уровня. В наследовании классов реализуются принципы их иерархической подчиненности. Предположим, мы производим новый класс Dog (Собака) от класса Mammal (Млекопитающее). Другими словами, класс Mammal является базовым для класса Dog. Точно так же, как описание вида собака несет в себе признаки, детализирующие описание млекопитающих в целом, так и класс Dog содержит ряд методов и данных, дополняющих методы и данные, которые представлены в классе Mammal. Как правило, с базовым классом связано несколько производных классов. Поскольку собаки, кошки и лошади являются представителями млекопитающих, то с точки зрения C++ можно сказать, что все эти классы произведены от класса Mammal. Царство животныхЧтобы более наглядно раскрыть смысл наследования классов, рассмотрим эту тему на примере отношений между многочисленными представителями животного мира. Представим себе, что программисту, поступил заказ на создание детской игры " Ферма" . Когда вы приступите к созданию животных, обитающих на ферме, включая лошадей, коров, собак, кошек, овец и т.д., вам потребуется снабдить каждый их класс такими методами, благодаря которым они смогут вести себя на экране так, как этого ожидает ребенок. Но на данном этапе каждый метод снабжен только функцией вывода на печать. Это общая практика программирования, когда сначала выполняется только формулировка набора методов, а детальная проработка их откладывается на потом. Вы вправе использовать все примеры программ, приведенные в этой главе, как основу для дальнейшей доработки с тем, чтобы все животные вели себя так, как вам хочется. Синтаксис наследования классовДля создания нового производного класса используется ключевое слово class, после которого указывается имя нового класса, двоеточие, тип объявления класса (public или какой-нибудь другой), а затем имя базового класса, как в следующем примере: class Dog : public Mammal Типы наследования классов рассматриваются далее в этой книге. Пока будем использовать только открытое наследование. Класс, из которого производится новый класс, должен быть объявлен раньше, иначе компилятор покажет сообщение об ошибке. Пример наследования класса Dog от класса Mammal показан в листинге 11.1. Листинг 11.1. Простое наследование 1: //Листинг 11.1. Простое наследование 2: 3: #include <iostream.h> 4: enum BREED { GOLDEN, CAIRN, DANDIE, SHETLAND, 00BERMAN, LAB } 5: 6: class Mammal 7: { 8: public: 9: // Конструкторы 10: Mammal(); 11: ~Mammal(); 12: 13: // Методы доступа к данным 14: int GetAge()const; 15: void SetAge(int); 16: int GetWeight() const; 17: void SetWeight(); 18: 19: // Другие методы 20: void Speak() const; 21: void Sleep() const; 22: 23: 24: protected: 25: int itsAge; 26: int itsWeight; 27: }; 28: 29: class Dog : public Mammal 30: { 31: public: 32: 33: // Конструкторы 34: Dog(); 35: ~Dog(); 36: 37: // Методы доступа к данным 38: BREED GetBreed() const; 39: void SetBreed(BREED); 40: 41: // Другие методы 42: WagTail(); 43: BegForFood(); 44: 45: protected: 46: BREED itsBreed; 47: }; Результат: Данная программа ничего не выводит на экран, так как пока содержит только объявления и установки классов. Никаких функций эта программа пока не выполняет. Анализ: Класс Mammal объявляется в строках 6—27. Обратите внимание, что класс Mammal не производится ни от какого другого класса, хотя в реальной жизни можно сказать, что класс млекопитающих производится от класса животных. Но в C++ всегда отображается не весь окружающий мир, а лишь модель некоторой его части. Действительность слишком сложна и разнообразна, чтобы отобразить ее в одной, даже очень большой программе. Профессионализм состоит в том, чтобы с помощью относительно простой модели воспроизвести объекты, которые будут максимально соответствовать своим реальным эквивалентам. Иерархическая структура нашего мира берет свое начало неизвестно откуда, но наша конкретная программа начинается с класса Mammal. В связи с этим некоторые переменные-члены, которые необходимы для работы базового класса, должны быть представлены в объявлении этого класса. Например, все животные независимо от вида и породы имеют возраст и вес. Если бы класс Mammal производился от класса Animals, то можно было бы ожидать, что он унаследует эти атрибуты. При этом атрибуты базового класса становятся атрибутами произведенного класса. Чтобы облегчить работу с программой и ограничить ее сложность разумными рамками, в классе Mammal представлены только шесть методов: четыре метода доступа, а также функции Speak() и Sleep(). В строке 29 класс Dog наследуется из класса Mammal. Все объекты класса Dog будут иметь три переменные-члена: itsAge, itsWeight и itsBreed. Обратите внимание, что в объявлении класса Dog не указаны переменные itsAge и itsWeight. Объекты класса Dog унаследовали эти переменные из класса Mammal вместе с методами, объявленными в классе Mammal, за исключением копировщика, конструктора и деструктора. Закрытый или защищенныйВозможно, вы заметили, что в строках 24 и 45 листинга 11.1 используется новое ключевое слово protected. До сих пор данные класса определялись с ключевым словом private. Но члены класса, объявленные как private, недоступны для наследования. Конечно, можно было в предыдущем листинге определить переменные-члены itsAge и itsWeight как public, но это нежелательно, поскольку прямой доступ к этим переменным получили бы все другие классы программы. Нашу цель можно сформулировать следующим образом: сделать переменную-член видимой для этого класса и для всех классов, произведенных от него. Именно таковыми являются защищенные данные, определяемые ключевым словом protected. Защищенные данные доступны для всех произведенных классов, но недоступны для всех внешних классов. Обобщим: существует три спецификатора доступа — public, protected и private. Если в функцию передаются объекты класса, то она может использовать данные всех переменных-членов и функций-членов, объявленных со спецификатором public. Функция-член класса, кроме того, может использовать все закрытые данные этого класса (объявленные как private) и защищенные данные любого другого класса, произведенного от этого класса (объявленные как protected). Так, в нашем примере функция Dog::WagTail() может использовать значение закрытой переменной itsBreed и все переменные класса Mammal, объявленные как public и protected. Даже если бы класс Dog был произведен не от класса Mammal непосредственно, а от какого-нибудь промежуточного класса (например, DomesticAnimals), все равно из класса Dog сохранился бы доступ к защищенным данным класса Mammal, правда только в том случае, если класс Dog и все промежуточные классы объявлялись как public. Наследование класса с ключевым словом private будет рассматриваться на занятии 15. В листинге 11.2 показано создание объекта в классе Dog с доступом ко всем данным и функциям этого типа. Листинг 11.2. Использование унаследованных объектов 1: // Листинг 11.2. Использование унаследованных объектов 2: 3: #include <iostream.h> 4: enum BREED < GOLDEN, CAIRN, DANDIE, SHETLAMD, DOBERMAN, LAB }; 5: 6: class Mammal 7: { 8: public: 9: // Конструкторы 10: Mammal():itsAge(2), itsWeight(5){ } 11: ~Mammal(){ } 12: 13: //Методы доступа 14: int GetAge()const { return itsAge; } 15: void SetAge(int age) { itsAge = age; } 16: int GetWeight() const { return itsWeight; } 17: void SetWeight(int weight) { itsWeight = weight; } 18: 19: //Другие методы 20: void Speak()const { cout << "Mammal sound!\n"; } 21: void Sleep()const { cout << "shhh. I'm sleeping.\n"; } 22: 23: 24: protected: 25: int itsAge; 26: int itsWeight; 27: }; 28: 29: class Dog : public Mammal 30: { 31: public: 32: 33: // Конструкторы 34: Dog():itsBreed(GOLDEN){ } 35: ~Dog(){ } 36: 37: // Методы доступа 38: BREED GetBreed() const { return itsBreed; } 39: void SetBreed(BREED breed) { itsBreed = breed; } 40: 41: // Другие методы 42: void WagTail() const { cout << "Tail wagging...\n"; } 43: void BegForFood() const { cout << "Begging for food...\n"; } 44: 45: private: 46: BREED itsBreed; 47: }; 48: 49: int main() 50: { 51: Dog fido; 52: fido.Speak(); 53: fido.WagTail(); 54: cout << "Fido is " << fido.GetAge() << " years old\n"; 55: return 0; 56: } Результат: Mammal sound! Tail wagging... Fido is 2 years old Анализ: В строках 6-27 объявляется класс Mammal (для краткости тела функций вставлены по месту их вызовов). В строках 29—47 из класса Mammal производится класс Dog. В результате объекту Fido этого класса доступны как функция производного класса WagTail(), так и функции базового класса Speak() и Sleep(). Конструкторы и деструкторыОбъекты класса Dog одновременно являются объектами класса Mammal. В этом суть иерархических отношений между классами. Когда в классе Dog создается объект Fido, то для этого из класса Mammal вызывается базовый конструктор, называемый первым. Затем вызывается конструктор класса Dog, который завершает создание объекта. Поскольку объект Fido не снабжен никакими параметрами, в обоих случаях вызывается конструктор, заданный по умолчанию. Объект Fido не существует до тех пор, пока полностью не будет завершено его создание с использованием обоих конструкторов класса Mammal и класса Dog. При удалении объекта Fido из памяти компьютера сначала вызывается деструктор класса Dog, а затем деструктор класса Mammal. Каждый деструктор удаляет ту часть объекта, которая была создана соответствующим конструктором производного или базового классов. Не забудьте удалить из памяти объект, если он больше не используется, как показано в листинге 11.3. Листинг 11.3. Вызов конструктора и деструктора 1: //Листинг 11.3. Вызов конструктора и деструктора. 2: 3: #include <iostream.h> 4: enum BREED { GOLDEN, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB }; 5: 6: class Mammal 7: { 8: public: 9: // конструкторы 10: Mammal(); 11: ~Mammal(); 12: 13: //Методы доступа 14: int GetAge() const { return itsAge; } 15: void SetAge(int age) { itsAge = age; } 16: int GetWeight() const { return itsWeight; } 17: void SetWeight(int weight) { itsWeight = weight; } 18: 19: //Другие методы 20: void Speak() const { cout << "Mammal sound!\n"; } 21: void Sleep() const { cout << "shhh. I'm sleeping.\n"; } 22: 23: 24: protected: 25: int itsAge; 26: int itsWeight; 27: }; 28: 29: class Dog : public Mammal 30: { 31: public: 32: 33: // Конструкторы 34: Dog(): 35: ~Dog(); 36: 37: // Методы доступа 38: BREED GetBreed() const { return itsBreed; } 39: void SetBreed(BREED breed) { itsBreed = breed; } 40: 41: // Другие методы 42: void WagTail() const { cout << "Tail wagging...\n"; } 43: void BegForFood() const { cout << "Begging for food...\n"; } 44: 45: private: 46: BREED itsBreed; 47: }; 48: 49: Mammal::Mammal(): 50: itsAge(1), 51: itsWeight(5) 52: { 53: cout << "Mammal constructor...\n"; 54: } 55: 56: Mammal::~Mammal() 57: { 58: cout << "Mammal destructor...\n"; 59: } 60: 61: Dog::Dog(): 62: itsBreed(GOLDEN) 63: { 64: cout << "Dog constructor...\n"; 65: } 66: 67: Dog::~Dog() 68: { 69: cout << "Dog destructor...\n"; 70: } 71: int main() 72: { 73: Dog fido; 74: fido.Speak(); 75: fido.WagTail(); 76: cout << "Fido is " << fido.GetAge() << " years old\n": 77: return 0; 78: } Результат: Mammal constructor... Dog constructor... Mammal sound! Tail wagging... Fido is 1 years old Dog destructor... Mammal destructor... Анализ: Листинг 11.3 напоминает листинг 11.2 за тем исключением, что вызов конструктора и деструктора сопровождается сообщением об этом на экране. Сначала вызывается конструктор класса Mammal, затем класса Dog. После этого объект класса Dog полноценно существует и можно использовать все его методы. Когда выполнение программы выходит за область видимости объекта Fido, вызывается пара деструкторов, сначала из класса Dog, а затем из класса Mammal. Передача аргументов в базовые конструкторыПредположим, нужно перегрузить конструкторы, заданные по умолчанию в классах Mammal и Dog, таким образом, чтобы первый из них сразу присваивал новому объекту определенный возраст, а второй — породу. Как передать в конструктор класса Mammal значения возраста и веса животного? Что произойдет, если вес не будет установлен конструктором класса Mammal, зато его установит конструктор класса Dog? Чтобы выполнить инициализацию базового класса, необходимо записать имя класса, после чего указать параметры, ожидаемые базовым классом, как показано в листинге 11.4. Листинг 11.4. Перегрузка конструкторов в производных классах 1: //Листинг 11.4. Перегрузка конструкторов в производных классах 2: 3: #include <iostream.h> 4: enum BREED { GOLDEN, CAIRN, DANDIE, SHETLAND, D0BERMAN, LAB }; 5: 6: class Mammal 7: { 8: public: 9: // Конструкторы 10: Mammal(); 11: Mammal(int age); 12: ~Mammal(); 13: 14: // Методы доступа 15: int GetAge() const { return itsAge; } 16: void SetAge(int age) { itsAge = age; } 17: int GetWeight() const { return itsWeight; } 18: void SetWeight(int weight) { itsWeight = weight; } 19: 20: //Другие методы 21: void Speak() const { cout << "Mammal sound!\n"; } 22: void Sleep() const { cout << "shhh. I'm sleeping.\n"; } 23: 24: 25: protected: 26: int itsAge; 27: int itsWeight; 28: }; 29: 30: class Dog : public Mammal 31: { 32: public: 33: 34: // Конструкторы 35: Dog(); 36: Dog(int age); 37: Dog(int age, int weight); 38: Dog(int age, BREED breed); 39: Dog(int age, int weight, BREED breed); 40: ~Dog(); 41: 42: // Методы доступа 43: BREED GetBreed() const { return itsBreed; } 44: void SetBreed(BREED breed) { itsBreed = breed; } 45: 46: // Другие методы 47: void WagTail() const { cout << "Tail wagging,..\n"; } 48: void BegForFood() const { cout << "Begging for food...\n"; } 49: 50: private: 51: BREED itsBreed; 52: }; 53: 54: Mammal::Mammal(): 55: itsAge(1), 56: itsWeight(5) 57: { 58: cout << "Mammal constructor...\n"; 59: } 60: 61: Mammal::Mammal(int age): 62: itsAge(age), 63: itsWeight(5) 64: { 65: cout << "Mammal(int) constructor...\n"; 66: } 67: 68: Mammal::~Mammal() 69: { 70: cout << "Mammal destructor...\n"; 71: } 72: 73: Dog::Dog(); 74: Mammal(), 75: itsBreed(GOLDEN) 76: { 77: cout << "0og constructor...\n"; 78: } 79: 80: Dog::Dog(int age): 81: Mammal(age), 82: itsBreed(GOLDEN) 83: { 84: cout << "Dog(int) constructor...\n"; 85: } 86: 87: Dog::Dog(int age, int weight): 88: Mammal(age), 89: itsBreed(GOLDEN) 90: { 91: itsWeight = weight; 92: cout << "Dog(int, int) constructor...\n"; 93: } 94: 95: Dog::Dog(int age, int weight, BREED breed): 96: Mammal(age), 97: itsBreed(breed) 98: { 99: itsWeight = weight; 100: cout << "Dog(int, int, BREED) constructor...\n"; 101: } 102: 103: Dog::Dog(int age, BREEDbreed): 104: Mammal(age), 105: itsBreed(breed) 106: { 107: cout << "Dog(int, BREED) constructor...\n"; 108: } 109: 110: Dog::~Dog() 111: { 112: cout << "Dog destructor...\n"; 113: } 114: int main() 115: { 116: Dog fido; 117: Dog rover(5); 118: Dog buster(6,8); 119: Dog yorkie (3,GOLDEN); 120: Dog dobbie (4,20,DOBERMAN); 121: fido.Speak(); 122: rover.WagTail(); 123: cout << "Yorkie is " << yorkie.GetAge() << " years old\n"; 124: cout << "Dobbie weighs "; 125: cout << dobbie.GetWeight() << " pounds\n"; 126: return 0; 127: } Примечание:Для удобства дальнейшего анализа строки вывода программы на экран пронумерованы. Результат: 1: Mammal constructor... 2: Dog constructor... 3: Mammal(int) constructor... 4: Dog(int) constructor... 5: Mammal(int) constructor... 6: Dog(int, int) constructor... 7: Mammal(int) constructor... 8: Dog(int, BREED) constructor.... 9: Mammal(int) constructor... 10: Dog(int, int, BREED) constructor... 11: Mammal sound! 12: Tail wagging... 13: Yorkie is 3 years old. 14: Dobbie weighs 20 pounds. 15: Dog destructor.., 16: Mammal destructor... 17: Dog destructor... 18: Mammal destructor... 19: Dog destructor... 20: Mammal destructor... 21: Dog destructor... 22: Mammal destructor... 23: Dog destructor, . . 24: Mammal destructor... Анализ: В листинге 11.4 конструктор класса Mammal перегружен в строке 11 таким образом, чтобы принимать целочисленные значения возраста животного. В строках 61—66 происходит инициализация переменной itsAge значением 5, переданным в параметре конструктора. В классе Dog в строках 35—39 создается пять перегруженных конструкторов. Первый — это конструктор, заданный по умолчанию. Второй принимает возраст и использует для этого тот же параметр, что и конструктор класса Mammal. Третий принимает возраст и вес, четвертый — возраст и породу, а пятый — возраст, вес и породу. Обратите внимание, что в строке 74 конструктор по умолчанию класса Dog вызывает конструктор по умолчанию класса Mammal. Хотя в этом нет необходимости, но данная запись лишний раз документирует намерение вызвать именно базовый конструктор, не содержащий параметров. Базовый конструктор будет вызван в любом случае, но в данной строке это было сделано явно. В строках 80—85 выполняется конструктор класса Dog, который принимает одно целочисленное значение. Во время инициализации (строки 81 и 82) возраст принимается из базового класса в виде параметра, после чего присваивается значение породы. Другой конструктор класса Dog выполняется в строках 87—93. Этот конструктор принимает два параметра. Первое значение вновь инициализируется обращением к соответствующему конструктору базового класса, тогда как второе берется из переменной базового класса itsWeight самим конструктором класса Dog. Обратите внимание, что присвоение значения переменной базового класса не может осуществляться на стадии инициализации конструктора произведенного класса. Поскольку в классе Mammal нет конструктора, присваивающего значение этой переменной, то присвоение значения должно выполняться в теле конструктора класса Dog. Самостоятельно проанализируйте работу остальных конструкторов в программе, чтобы закрепить полученные знания. Обращайте внимание, какие переменные можно инициализировать одновременно с инициализацией конструктора, а в каких случаях инициализацию следует выполнять в теле конструктора. Для удобства анализа работы программы строки вывода были пронумерованы. Первые две строки вывода соответствуют инициализации объекта Fido с помощью конструкторов, заданных по умолчанию. Строки 3 и 4 соответствуют созданию объекта rover, а строки 5 и 6 — объекта buster. Обратите внимание, что в последнем случае из конструктора класса Dog с двумя целочисленными параметрами происходит вызов конструктора класса Mammal, содержащего один целочисленный параметр. После создания всех объектов программа использует их и наконец выходит за область видимости этих объектов. Удаление каждого объекта сопровождается обращением к деструктору класса Dog, после чего следует обращение к деструктору класса Mammal. Замещение функцийОбъект класса Dog имеет доступ ко всем функциям-членам класса Mammal, а также к любой функции-члену, чье объявление добавлено в класс Dog, например к функции WagTaill(). Но кроме этого, базовые функции могут быть замещены в производном классе. Под замещением базовой функции понимают изменение ее выполнения в производном классе для объектов, созданных в этом классе. Если в производном классе создается функция с таким же возвратом и сигнатурой как и в базовом классе, но выполняемая особым образом, то имеет место замещение метода. В случае замещения функций должно сохраняться соответствие между типом возврата и сигнатурой функций в базовом классе. Под сигнатурой понимают установки, заданные в прототипе функции, включая ее имя, список параметров и, в случае использования, ключевое слово const. В листинге 11.5 показано замещение в классе Dog функции Speak(), объявленной в классе Mammal. Для экономии места знакомые по предыдущим листингам объявления методов доступа в этом примере были опущены. Листинг 11.5. Замещение метода базового класса в производном классе 1: //Листинг 11.5. Замещение метода базового класса в производном классе 2: 3: #include <iostream.h> 4: enum BREED { GOLDEN, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB }; 5: 6: class Mammal 7: { 8: public: 9: // Конструкторы 10: Mammal() { cout << "Mammal constructor...\n"; } 11: ~Mammal() { cout << "Mammal destructor...\n"; } 12: 13: //Другие методы 14: void Speak()const { cout << "Mammal sound!\n"; } 15: void Sleep()const { cout << "shhh. I'm sleeping.\n"; } 16: 17: 18: protected: 19: int itsAge; 20: int itsWeight; 21: }; 22: 23: class Dog : public Mammal 24: { 25: public: 26: 27: // Конструкторы 28: Dog(){ cout << "Dog constructor...\n"; } 29: ~Dog(){ cout << "Dog destructor...\n"; } 30: 31: // Другие методы 32: void WagTail() const { cout << "Tail wagging...\n"; } 33: void BegForFood() const { cout << "Begging for food...\n"; } 34: void Speak() const { cout << "Woof!\n"; } 35: 36: private: 37: BREED itsBreed; 38: }; 39: 40: int main() 41: { 42: Mammal bigAnimal; 43: Dog fido; 44: bigAnimal.Speak(); 45: fido.Speak(); 46: return 0; 47: } Результат: Mammal constructor... Mammal constructor... Dog constructor... Mammal sound! Woof! Dog destructor... Mammal destructor... Mammal destructor... Анализ: В строке 34 в классе Dog происходит замещение метода базового класса Speak(), в результате чего в случае вызова этой функции объектом класса Dog на экран выводится Woof!. В строке 42 создается объект bigAnimal класса Mammal, в результате чего вызывается конструктор класса Mammal и на экране появляется первая строка. В строке 43 создается объект Fido класса Dog, что сопровождается последовательным вызовом сначала конструктора класса Mammal, а затем конструктора класса Dog. Соответственно на экран выводится еще две строки. В строке 44 объект класса Mammal вызывает метод Speak(), а в строке 45 уже объект класса Dog обращается к этому методу. На экран при этом выводится разная информация, так как метод Speak() в классе Dog замещен. Наконец выполнение программы выходит за область видимости объектов и для их удаления вызываются соответствующие пары деструкторов. Перегрузка или замещение Эти схожиеподходы приводят почти к одинаковым результатам. При перегрузке метода создается несколько вариантов этогометода с одним и тем же именем, но с разными сигнатурами. При замещении в производном классе используется метод с тем же именем и сигнатурой, что и в базовом классе, но с изменениями в теле функции. Сокрытие метода базового классаВ предыдущем примере при обращении к методу Speak() из объекта класса Dog программа выполнялась не так, как было указано при объявлении метода Speak() в базовом классе. Казалось бы, это то, что нам нужно. Если в классе Mammal есть некоторый метод Move(), который замещается в классе Dog, то можно сказать, что метод Move() класса Dog скрывает метод с тем же именем в базовом классе. Однако в некоторых случаях результат может оказаться неожиданным. Усложним ситуацию. Предположим, что в классе Mammal метод Move() трижды перегружен. В одном варианте метод не требует параметров, в другом используется один целочисленный параметр (дистанция), а в третьем — два целочисленных параметра (скорость и дистанция). В классе Dog замещен метод Move() без параметров. Тем не менее попытка обратиться из объекта класса Dog к двум другим вариантам перегруженного метода класса Mammal окажется неудачной. Суть проблемы раскрывается в листинге 11.6. Листинг 11.6. Сокрытие методов 1: //Листинг 11.6. Сокрытие методов 2: 3: #include <iostream.h> 4: 5: class Mammal 6: { 7: public: 8: void Move() const { cout << "Mammal move one step\n"; } 9: void Move(int distance) const 10: { 11: cout << "Mammal move "; 12: cout << distance <<" steps.\n"; 13: } 14: protected: 15: int itsAge; 16: int itsWeight; 17: }; 18: 19: class Dog : public Mammal 20: { 21: public: 22: // Возможно, последует сообщение, что функция скрыта! 23: void Move() const { cout << "Dog move 5 steps.\n"; } 24: }; 25: 26: int main() 27: { 28: Mammal bigAnimal; 29: Dog fido; 30: bigAnimal.Move(); 31: bigAnimal.Move(2); 32: fido.Move(); 33: // fido.Move(10); 34: return 0; 35: } Результат: Mammal move one step Mammal move 2 steps. Dog move 5 steps. Анализ: В данном примере из программы были удалены все другие методы и данные, рассмотренные нами ранее. В строках 8 и 9 в объявлении класса Mammal перегружаются методы Move(). В строке 23 происходит замещение метода Move() без параметров в классе Dog. Данный метод вызывается для объектов разных классов в строках 30 и 32, и информация, выводимая на экран, подтверждает, что замещение метода прошло правильно. Однако строка 33 заблокирована, так как она вызовет ошибку компиляции. Хотя логично было предположить, что в классе Dog свободно можно использовать метод Move(int), поскольку замещен был только метод Move(), но в действительности в данной ситуации, чтобы использовать Move(int), его также нужно заместить в классе Dog. В случае замещения одного из перегруженных методов скрытыми оказываются все варианты этого метода в базовом классе. Если вы хотите использовать в производном классе другие варианты перегруженного метода, то их также нужно заместить в этом классе. Часто случается ошибка, когда после попытки заместить метод в производном классе данный метод оказывается недоступным для класса из-за того, что программист забыл установить ключевое слово const, используемое при объявлении метода в базовом классе. Вспомните, что слово const является частью сигнатуры, а несоответствие сигнатур ведет к скрытию базового метода, а не к его замещению. Замещение и сокрытие В следующем разделе главы будут рассматриваться виртуальные методы. Замещение виртуальных методов ведет к полиморфизму, а сокрытие методов разрушает поли- морфизм. Скоро вы узнаете об этом больше. Вызов базового методаДаже если вы заместили базовый метод, то все равно можете обратиться к нему, указав базовый класс, где хранится исходное объявление метода. Для этого в обращении к методу нужно явно указать имя базового класса, за которым следуют два символа двоеточия и имя метода. Например: Mammal: :Move(). Если в листинге 11.6 переписать строку 32 так, как показано ниже, то ошибка во время компиляции больше возникать не будет: 32: fido.Mammal::Move(); Такая запись, реализованная в листинге 11.7, называется явным обрашением к методу базового класса. Листинг 11.7. Явное обращение к методу базового класса 1: //Листинг 11.7. Явное обращение к методу базового класса 2: 3: #include <iostream.h> 4: 5: class Mammal 6: { 7: public: 8: void Move() const { cout << "Mammal move one step\n"; } 9: void Move(int distance) const 10: { 11: cout << "Mammal move " << distance; 12: cout << " steps.\n"; 13: } 14: 15: protected: 16: int itsAge; 17: int itsWeight; 18: }; 19: 20: class Dog : public Mammal 21: { 22: public: 23: void Move()const; 24: 25: }; 26: 27: void Dog::Move() const 28: { 29: cout << "In dog move...\n"; 30: Mammal::Move(3); 31: } 32: 33: int main() 34: { 35: Mammal bigAnimal; 36: Dog fido; 37: bigAnimal.Move(2); 38: fido.Mammal::Move(6); 39: return 0; 40: } Результат: Mammal move 2 steps. Mammal move 6 steps. Анализ: В строке 35 создается объект bigAnimal класса Mammal, а в строке 36 — объект fido класса Dog. В строке 37 вызывается метод Move(int) из базового класса для объекта класса Dog. В предыдущей версии программы мы столкнулись с проблемой из-за того, что в классе Dog доступен только один замещенный метод Move(), в котором не задаются параметры. Проблема была разрешена явным обращением к методу Move(int) базового класса в строке 38. Рекомендуется:Повышайте функциональные возможности класса путем создания новых производных классов. Изменяйте выполнение отдельных функций в производных классах с помощью замещения методов. Не рекомендуется:Не допускайте сокрытие функций базового класса из-за несоответствия сигнатур. Виртуальные методыВ этой главе неоднократно подчеркивалось, что объекты класса Dog одновременно являются объектами класса Mammal. До сих пор под этим подразумевалось, что объекты класса Dog наследуют все атрибуты (данные) и возможности (методы) базового класса. Но в языке C++ принципы иерархического построения классов несут в себе еще более глубинный смысл. Полиморфизм в C++ развит настолько, что допускается присвоение указателям на базовый класс адресов объектов производных классов, как в следующем примере: Mammal* pMammal = new Dog; Данное выражение создает в области динамической памяти новый объект класса Dog и возвращает указатель на этот объект, который является указателем класса Mammal. Это вполне логично, так как собака — представитель млекопитающих. Примечание:В этом суть полиморфизма. Например, можно объявить множество окон разных типов, включая диалоговые, прокручиваемые окна и поля списков, после чего создавать их в программе с помощью единственного виртуального метода draw(). Создав указатель на базовое окно и присваивая этому указателю адреса объектов производных классов, можно обращаться к методу draw() независимо от того, с каким из объектов в данный момент связан указатель. Причем всегда будет вызываться вариант метода, специфичный для класса выбранного объекта. Затем этот указатель можно использовать для вызова любого метода класса Mammal. Причем если метод был замещен, скажем, в классе Dog, то при обращении к методу через указатель будет вызываться именно вариант, указанный в данном производном классе. В этом суть использования виртуальных функций. Листинг 11.8 показывает, как работает виртуальная функция и что происходит с не виртуальной функцией. Листинг 11.8. Использование виртуальных методов 1: //Листинг 11.8. Использование виртуальных методов 2: 3: #include<iostream.h> 4: 5: class Mammal 6: { 7: public: 8: Mammal():itsAge(1) { cout << "Mammal constructor...\n"; } 9: virtual ~Mammal() { cout << "Mammal destructor...\n"; } 10: void Move() const { cout << "Mammal move one step\n"; } 11: virtual void Speak() const { cout << "Mammal speak!\n"; } 12: protected: 13: int itsAge; 14: 15: }; 16: 17: class Dog : public Mammal 18: { 19: public: 20: Dog() { cout << "Dog Constructor...\n"; } 21: virtual ~Dog() { cout << "Dog destructor...\n"; } 22: void WagTail() { cout << "Wagging Tail...\n"; } 23: void Speak()const { cout << "Woof!\n"; } 24: void Move()const { cout << "Dog moves 5 steps...\n"; } 25: }; 26: 27: int main() 28: { 29: 30: Mammal *pDog = new Dog; 31: pDog->Move(); 32: pDog->Speak(); 33: 34: return 0; 35: } Результат: Mammal constructor... Dog Constructor... Mammal move one step Woof! Анализ: В строке 11 объявляется виртуальный метод Speak() класса Mammal. Предполагается, что данный класс должен быть базовым для других классов. Вероятно также, что данная функция может быть замещена в производных классах. В строке 30 создается указатель класса Mammal (pDog), но ему присваивается адрес нового объекта производного класса Dog. Поскольку собака является млекопитающим, это вполне логично. Данный указатель затем используется для вызова функции Move(). Поскольку pDog известен компилятору как указатель класса Mammal, результат получается таким же, как при обычном вызове метода Move() из объекта класса Mammal. В строке 32 через указатель pDog делается обращение к методу Speak(). В данном случае метод Speak() объявлен как виртуальный, поэтому вызывается вариант функции Speak(), замещенный в классе Dog. Это кажется каким-то волшебством. Хотя компилятор знает, что указатель pDog принадлежит классу Mammal, тем не менее происходит вызов версии функции, объявленной в другом производном классе. Если создать массив указателей базового класса, каждый из которых указывал бы на объект своего производного класса, то, обращаясь попеременно к указателям данного массива, можно управлять выполнением всех вариантов замещенного метода. Эта идея реализована в листинге 11.9. Листинг 11.9. Произвольное обращение к набору виртуальных функций 1: //Листинг 11.9. Произвольное обращение к набору виртуальных функций 2: 3: #include <iostream.h> 4: 5: class Mammal 6: { 7: public: 8: Mammal():itsAge(1) { } 9: virtual ~Mammal() { } 10: virtual void Speak() const { cout << "Mammal speak!\n"; } 11: protected: 12: int itsAge; 13: }; 14: 15: class Dog : public Mammal 16: { 17: public: 18: void Speak()const { cout << "Woof!\n"; } 19: }; 20: 21: 22: class Cat : public Mammal 23: { 24: public: 25: void Speak()const { cout << "Meow!\n"; } 26: }; 27: 28: 29: class Horse : public Mammal 30: { 31: public: 32: void Speak()const { cout << "Whinny!\n"; } 33: }; 34: 35: class Pig : public Mammal 36: { 37: public: 38: void Speak()const < cout << "Oink!\n"; } 39: }; 40: 41: int main() 42: { 43: Mammal* theArray[5]; 44: Mammal* ptr; 45: int choice, i; 46: for ( i = 0; i<5; i++) 47: { 48: cout << "(1)dog (2)cat (3)horse (4)pig: "; 49: cin >> choice; 50: switch (choice) 51: { 52: case 1: ptr = new Dog; 53: break; 54: case 2; ptr = new Cat; 55: break; 56: case 3: ptr = new Horse; 57: break; 58: case 4: ptr = new Pig; 59: break; 60: default: ptr = new Mammal; 61: break; 62: } 63: theArray[i] = ptr; 64: } 65: for (i=0;i<5;i++) 66: theArray[i]->Speak(); 67: return 0; 68: } Результат: (1)dog (2)cat (3)horse (4)pig: 1 (1)dog (2)cat (3)horse (4)pig: 2 (1)dog (2)cat (3)horse (4)pig: 3 (1)dog (2)cat (3)horse (4)pig; 4 (1)dog (2)cat (3)horse (4)pjg: 5 Woof! Meow! Whinny! 0ink! Mammal speak! Анализ: Чтобы идея использования виртуальных функций была понятнее, в данной программе этот метод раскрыт наиболее явно и четко. Сначала определяется четыре класса — Dog, Cat, Horse и Pig, которые являются производными от базового класса Mammal. В строке 10 объявляется виртуальная функция Speak() класса Mammal. В строках 18, 25, 32 и 38 указанная функция замещается во всех соответствующих производных классах. Пользователю предоставляется возможность выбрать объект любого производного класса, и в строках 46—64 создается и добавляется в массив указатель класса Mammal на вновь созданный объект. Вопросы и ответы Если функция-член была объявлена как виртуальная в базовом классе, следует ли повторно указывать виртуальность при объявлении этого метода в произ- водном классе? Нет. Если метод уже был объявлен как виртуальный, то он будет оставаться таким, несмотря на замещение его в производном классе. В то же время для повышения читабельности программы имеет смысл (но не требуется) и в производных классах продолжать указывать на виртуальность данного метода с помощью ключевого слова virtual. Примечание:Во время компиляции неизвестно, объект какого класса захочет создать пользователь и какой именно вариант метода Speak() будет использоваться. Указатель ptr связывается со своим объектом только во время выполнения программы. Такое связывание указателя с объектом называется динамическим, в отличие от статического связывания, происходящего во время компиляции программы. Как работают виртуальные методыПри создании объекта в производном классе, например в классе Dog, сначала вызывается конструктор базового, а затем — производного класса. Схематично объект класса Dog показан на рис. 11.2. Обратите внимание, что объект производного класса состоит как бы из двух частей, одна из которых создается конструктором базового класса, а другая — конструктором производного класса. Рис. 11.2. Созданный объект класса Dog Рис. 11.3. Таблица виртуальных функций класса Mammal Если в каком-то из объектов создается обычная не виртуальная функция, то всю полноту ответственности за эту функцию берет на себя объект. Большинство компиляторов создают таблицы виртуальных функций, называемые также v-таблицами. Такие таблицы создаются для каждого типа данных, и каждый объект любого класса содержит указатель на таблицу виртуальных функций (vptr, или v-указатель). Хотя детали реализации выполнения виртуальных функций меняются в разных компиляторах, сами виртуальные функции будут работать совершенно одинаково, независимо от компилятора. Рис. 11.4. Таблица виртуальных функций класса Dog Итак, в каждом объекте есть указатель vptr, который ссылается на таблицу виртуальных функций, содержащую, в свою очередь, указатели на все виртуальные функции. (Более подробно указатели на функции рассматриваются на занятии 14.) Указатель vptr для объекта класса Dog инициализируется при создании части объекта, принадлежащей базовому классу Mammal, как показано на рис. 11.3. После вызова конструктора класса Dog указатель vptr настраивается таким образом, чтобы указывать на замещенный вариант виртуальной функции (если такой есть), существующий для класса Dog (рис. 11.4). В результате при использовании указателя на класс Mammal указатель vptr по- прежнему ссылается на тот вариант виртуальной функции, который соответствует реальному типу объекта. Поэтому при обращении к методу Speak() в предыдущем примере выполнялась та функция, которая была задана в соответствующем производном классе. Нельзя брать там, находясь здесьЕсли для объекта класса Dog объявлен метод WagTail(), который не принадлежит классу Mammal, то невозможно получить доступ к этому методу, используя указатель класса Mammal (если только этот указатель не будет явно преобразован в указатель класса Dog). Поскольку функция WagTail() не является виртуальной и не принадлежит классу Mammal, то доступ к ней можно получить только из объекта класса Dog или с помощью указателя этого класса. Поскольку любые преобразования чреваты ошибками, создатели C++ допустили только явные преобразования типов. Всегда можно преобразовать любой указатель класса Mammal в указатель класса Dog, но есть более надежный и безопасный способ вызова метода WagTail(). Чтобы разобраться в тонкостях упомянутого метода, необходимо освоить множественное наследование, о котором речь пойдет на следующем занятии, или научиться работе с шаблонами, что будет темой занятия 20. Дробление объектаСледует обратить внимание, что вся магия виртуальных функций проявляется только при обращении к ним с помощью указателей и ссылок. Если передать объект как значение, то виртуальную функцию вызвать не удастся. Эта проблема показана в листинге 11.10. Листинг 11.10. Дробление объекта при передаче его как значения 1: //Листинг 11.10. Дробление объекта при передачи его как значения 2: 3: #include <iostream.h> 4: 5: class Mammal 6: { 7: public: 8: Mammal():itsAge(1) { } 9: virtual ~Mammal() { } 10: virtual void Speak() const { cout << "Mammal speak!\n"; } 11: protected: 12: int itsAge; 13: }; 14: 15: class Dog : public Mammal 16: { 17: public: 18: void Speak()const { cout << "Woof!\n"; } 19: }; 20: 21: class Cat : public Mammal 22: { 23: public: 24: void Speak()const { cout << "Meow!\ri"; > 25: }; 26: 27: void ValueFunction (Mammal); 28: void PtrFunction (Mammal*); 29: void RefFunction (Mammal&); 30: int main() 31: { 32: Mammal* ptr=0; 33: int choice; 34: while (1) 35: { 36: bool fQuit = false; 37: cout << "(1)dog (2)cat (0)Quit: "; 38: cin >> choice; 39: switch (choice) 40: { 41: case 0: fQuit = true; 42: break; 43: case 1: ptr = new Dog; 44: break; 45: case 2: ptr = new Cat; 46: break; 47: default: ptr = new Mammal; 48: break; 49: } 50: if (fQuit) 51: break; 52: PtrFunction(ptr); 53: RefFunction(*ptr); 54: ValueFunction(*ptr); 55: } 56: return 0; 57: } 58: 59: void ValueFunction (Mammal MammalValue) 60: { 61: MammalValue.Speak(); 62: } 63: 64: void PtrFunction (Mammal * pMammal) 65: { 66: pMammal->Speak(); 67: } 68: 69: void RefFunction (Mammal & rMammal) 70: { 71: rMammal.Speak(); 72: } Результат: (1)dog (2)cat (0)Quit: 1 Woof Woof Mammal Speak! (1)dog (2)cat (0)Quit: 2 Meow! Meow! Mammal Speak! (1)dog (2)cat (0)Quit: 0 Анализ: В строках 5—25 определяются классы Mammal, Dog и Cat. Затем объявляются три функции — PtrFunction(), RefFunction() и ValueFunction(). Они принимают соответственно указатель класса Mammal, ссылку класса Mammal и объект класса Mammal. После чего выполняют одну и ту же операцию — вызывают метод Speak(). Пользователю предлагается выбрать объект класса Dog или класса Cat, после чего в строках 43—46 создается указатель соответствующего типа. Судя по информации, выведенной программой на экран, пользователь первый раз выбрал объект класса Dog, который был создан в свободной области памяти 43-й строкой программы. Затем объект класса Dog передается в три функции с помощью указателя, с помощью ссылки и как значение. В том случае, когда в функцию передавался адрес объекта с помощью указателя или ссылки, успешно выполнялась функция-член Dog->Speak(). На экране компьютера дважды появилось сообщение, соответствующее выбранному пользователем объекту. Разыменованный указатель передает объект как значение. В этом случае функция распознает принадлежность переданного объекта классу Mammal, компилятор разбивает объект класса Dog пополам и использует только ту часть, которая была создана конструктором класса Mammal. В таком случае вызывается версия метода Speak(), которая была объявлена для класса Mammal, что и отобразилось в информации, выведенной программой на экран. Те же действия и с тем же результатом были выполнены затем и для объекта класса Cat. Виртуальные деструкторыВ том случае, когда ожидается указатель на объект базового класса, вполне допустима и часто используется на практике передача указателя на объект производного класса. Что произойдет при удалении указателя, ссылающегося на объект производного класса? Если деструктор будет объявлен как виртуальный, то все пройдет отлично — будет вызван деструктор соответствующего производного класса. Затем деструктор производного класса автоматически вызовет деструктор базового класса, и указанный объект будет удален целиком. Отсюда следует правило: если в классе объявлены виртуальные функции, то и деструктор должен быть виртуальным. Виртуальный конструктор-копировщикКонструкторы не могут быть виртуальными, из чего можно сделать вывод, что не может быть также виртуального конструктора-копировщика. Но иногда требуется, чтобы программа могла передать указатель на объект базового класса и правильно скопировать его в объект производного класса. Чтобы добиться этого, необходимо в базовом классе создать виртуальный метод Clone(). Метод Clone() должен создавать и возвращать копию объекта текущего класса. Поскольку в производных классах метод Clone() замещается, при вызове его создаются копии объектов, соответствующие выбранному классу. Программа, использующая этот метод, показана в листинге 11.11. Листинг 11.11. Виртуальный конструктор-копировщик 1: //Листинг 11.11. Виртуальный конструктор-копировщик 2: 3: #include <iostream.h> 4: 5: class Mammal 6: { 7: public: 8: Mammal():itsAge(1) { cout << "Mammal constructor...\n"; } 9: virtual ^Mammal() { cout << "Mammal destructor...\n"; } 10: Mammal (const Mammal & rhs); 11: virtual void Speak() const { cout << "Mammal speak!\n"; } 12: virtual Mammal* Clone() { return new Mammal(*this); } 13: int GetAge()const { return itsAge; } 14: protected: 15: int itsAge; 16: }; 17: 18: Mammal::Mammal (const Mammal & rhs):itsAge(rhs.GetAge()) 19: { 20: cout << "Mammal Copy Constructor...\n"; 21: } 22: 23: class Dog : public Mammal 24: { 25: public: 26: Dog() { cout << "Dog constructor...\n"; } 27: virtual ~Dog() { cout << "Dog destructor...\n"; } 28: Dog (const Dog & rhs); 29: void Speak()const { cout << "Woof!\n"; } 30: virtual Mammal* Clone() { return new Dog(*this); } 31: }; 32: 33: Dog::Dog(const Dog & rhs): 34: Mammal(rhs) 35: { 36: cout << "Dog copy constructor...\n"; 37: } 38: 39: class Cat : public Mammal 40: { 41: public: 42: Cat() { cout << "Cat constructor,,,\n"; } 43: ~Cat() { cout << "Cat destructor...\n"; } 44: Cat (const Cat &); 45: void Speak()const { cout << "Meow!\n"; } 46: virtual Mammal* Clone() { return new Cat(*this); } 47: }; 48: 49: Cat::Cat(const Cat & rhs): 50: Mammal(rhs) 51: { 52: cout << "Cat copy constructor..,\n"; 53: } 54: 55: enum ANIMALS { MAMMAL, D0G, CAT }; 56: const int NumAnimalTypes = 3; 57: int main() 58: { 59: Mammal *theArray[NumAnimalTypes]; 60: Mammal* ptr; 61: int choice, i; 62: for ( i = 0; i<NumAnimalTypes; i++) 63: { 64: cout << "(1)dog (2)cat (3)Mammal: "; 65: cin >> choice; 66: switch (choice) 67: { 68: case DOG: ptr = new Dog; 69: break; 70: case CAT: ptr = new Cat; 71: break; 72: default: ptr = new Mammal; 73: break; 74: } 75: theArray[i] = ptr; 76: } 77: Mammal *OtherArray[NumAnimalTypes]; 78: for (i=0;i<NumAnimalTypes;i++) 79: { 80: theArray[i]->Speak(); 81: OtherArray[i] = theArray[i]->Clone(); 82: } 83: for (i=0;i<NumAnimalTypes;i++) 84: OtherArray[i]->Speak(); 85: return 0; 86: } Результат: 1: (1)dog (2)cat (3)Mammal: 1 2: Mammal constructor... 3: Dog constructor... 4: (1)dog (2)cat (3)Mammal: 2 5: Mammal constructor... 6: Cat constructor... 7: (1)dog (2)cat (3)Mammal: 3 8: Mammal constructor... 9: Woof! 10: Mammal Copy Constructor... 11: Dog copy constructor... 12: Meow! 13: Mammal Copy Constructor... 14: Cat copy constructor... 15: Mammal speak! 16: Mammal Copy Constructor... 17: Woof! 18: Meow! 19: Mammal speak! Анализ: Листинг 11.11 похож на два предыдущих листинга, однако в данной программе в классе Mammal добавлен один новый виртуальный метод — Clone(). Этот метод возвращает указатель на новый объект класса Mammal, используя конструктор-копировщик, параметр которого представлен указателем <<this. Метод Clone() замещается в обоих производных классах — Dog и Cat — соответствующими версиями, после чего копии данных передаются на конструкторы- копировщики производных классов. Поскольку Clone() является виртуальной функцией, то в результате будут созданы виртуальные конструкторы-копировщики, как показано в строке 81. Пользователю предлагается выбрать объект класса 0og, Cat или Mammal. Объект выбранного типа создается в строках 62-74. В строке 75 указатель на новый объект добавляется в массив данных. Затем осуществляется цикл, в котором для каждого объекта массива вызываются методы Speak() и Clone() (см. строки 80 и 81). В результате выполнения функции возвращается указатель на копию объекта, которая сохраняется в строке 81 во втором массиве. В строке 1 вывода на экран показан выбор пользователем опции 1 — создание объекта класса Dog. В создание этого объекта вовлекаются конструкторы базового и производного классов. Эта операция повторяется для объектов классов Cat и Mammal в строках вывода 4-8. В строке 9 вывода показано выполнение метода Speak() для объекта класса Dog. Поскольку функция Speak() также объявлена как виртуальная, то при обращении к ней вызывается та ее версия, которая соответствует типу объекта. Затем следует обращение еще к одной виртуальной функции Clone(), виртуальность которой проявляется в том, что при вызове из объекта класса Dog запускаются конструктор класса Mammal и конструктор-копировщик класса Dog. То же самое повторяется для объекта класса Cat (строки вывода с 12—14) и объекта класса Mammal (строки вывода 15 и 16). В результате создается массив объектов, для каждого из которых вызывается своя версия функции Speak(). Цена виртуальности методовПоскольку объекты с виртуальными методами должны поддерживать v-таблицу, то использование виртуальных функций всегда ведет к некоторому повышению затрат памяти и снижению быстродействия программы. Если вы работаете с небольшим классом, который не собираетесь делать базовым для других классов, то в этом случае нет никакого смысла использовать виртуальные методы. Объявляя виртуальный метод в программе, заплатить придется не только за v- таблицу (хотя добавление последующих записей потребует не так уж много места), но и за создание виртуального деструктора. Поэтому следует подумать, имеет ли смысл преобразовывать методы программы в виртуальные, а если да, то какие именно. Рекомендуется:Используйте виртуальные методы только в том случае, если программа содержит базовый и производные классы. Используйте виртуальный деструктор, если в программе были созданы виртуальные методы. Не рекомендуется:Не пытайтесь создать виртуальный конструктор. РезюмеСегодня вы узнали, как наследовать новые классы от базового класса. В этой главе рассматривалось наследование с ключевым словом public и использование виртуальных функций. Во время наследования в производные классы передаются все открыты e и защищенные данные и функции из базового класса. Защищенные данные базового класса открыты для всех производных классов, но закрыты для всех других классов программы. Но даже производные классы не могут получить доступ к закрытым данным и функциям базового класса. Конструкторы могут инициализироваться до выполнения тела конструктора. При этом вызывается конструктор базового класса, и туда могут быть переданы данные в виде параметров. Функции, объявленные в базовом классе, могут быть замещены в производных классах. Если при этом функция объявлена как виртуальная, а обращение к функции от объекта осуществляется с помощью указателя на объект или ссылки, то вызываться будет тот замещенный вариант функции, который соответствует типу текущего объекта. Методы базового класса можно вызывать явным обращением, когда в строке вызова сначала указывается имя базового класса с двумя символами двоеточия после него. Например, если класс Dog произведен от класса Mammal, то к методу базового класса напрямую можно обратиться следующим выражением: Mammal::walk(). Если в классе используются виртуальные методы, то следует объявить также и виртуальный деструктор. Он необходим для того, чтобы быть уверенным в удалении части объекта, относящейся к производному классу, если удаление объекта осуществлялось с помощью указателя базового класса. Нельзя создать виртуальный конструктор. В то же время можно создать виртуальный конструктор-копировщик и эффективно его использовать с помощью виртуальной функции, вызывающей конструктор-копировщик. Вопросы и ответыНаследуются ли данные и функции-члены базового класса в последующие поколения производных классов? Скажем, если класс Dog произведен от класса Mammal, а класс Mammal произведен от класса Animals, унаследует ли класс Dog данные и функции класса Animals? Да. Если последовательно производить ряд классов, последний класс в этом ряду унаследует всю сумму данных и методов предыдущих базовых классов. Если в предыдущем примере в классе Mammal будет замещена функция, описанная в классе Animals, то какой вариант функции получит класс Dog? Если класс Dog наследуется от класса Mammal, то он получит функцию в том виде, в каком она существует в классе Mammal, т.е. замещенную. Можно ли в производном классе описать как private функцию, которая перед этим была описана в базовом классе как public? Можно. Функция может быть не только защищена в производном классе, но и закрыта. Она останется закрытой для всех последующих классов, произведенных от этого. В каких случаях не следует делать функции класса виртуальными? Описание первой виртуальной функции вызовет создание v-таблицы, что потребует времени и дополнительной памяти. Последующее добавление виртуальных функций будет тривиальным. Многие программисты увлекаются созданием виртуальных функций и полагают, что если в программе есть уже одна виртуальная функция, то и все другие должны быть виртуальными. В действительности это не так. Создание виртуальных функций всегда должно отвечать решению конкретных задач. Предположим, что некоторая функция без параметров была описана в базовом классе как виртуальная, а затем перегружена таким образом, чтобы принимать один и два целочисленных параметра. Затем в производном классе был замещен вариант функции с одним целочисленным параметром. Что произойдет, если с помощью указателя, связанного с объектом производного класса, вызвать вариант функции с двумя параметрами? Замещение в производном классе варианта функции с одним параметром скроет от объектов этого класса все остальные варианты функции. Поэтому в случае обращения, описанного в вопросе, компилятор покажет сообщение об ошибке. КоллоквиумВ этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний и приводится несколько упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из предложенных ниже вопросов. Тест1. Что такое v-таблица? 2. Что представляет собой виртуальный деструктор? 3. Можно ли объявить виртуальный конструктор? 4. Как создать виртуальный конструктор-копировщик? 5. Как вызвать функцию базового класса из объекта производного класса, если в производном классе эта функция была замещена? 6. Как вызвать функцию базового класса из объекта производного класса, если в производном классе эта функция не была замещена? 7. Если в базовом классе функция объявлена как виртуальная, а в производном классе виртуальность функции указана не была, сохранится ли функция как виртуальная в следующем произведенном классе? 8. С какой целью используется ключевое слово protected? Упражнения1. Объявите виртуальную функцию, которая принимает одно целочисленное значение и возвращает void. 2. Запишите объявление класса Square, произведенного от класса Rectangle, который, в свою очередь, произведен от класса Shape. 3. Предположим, что в предыдущем примере объект класса Shape не использует параметры, объект класса Rectangle принимает два параметра (length и width), а объект класса Square — один параметр (length); запишите конструктор для класса Square. 4. Запишите виртуальный конструктор-копировщик для класса Square, взятого из упражнения 3. 5. Жучки: что неправильно в следующем программном коде? void SomeFunction(Shape); Shape * pRect = new Rectangle; SoneFunction(*pRect); 6. Жучки: что неправильно в следующем программном коде? class Shape() { public: Shape(); virtual -Shape(); virtual Shape(const Shape&); }; День 12-й. Массивы и связанные листыВ программах, представленных в предыдущей главе, объявлялись одиночные объекты типов int, char и др. Но часто возникает необходимость создать коллекцию объектов, например 20 значений типа int или кучу объектов типа CAT. Сегодня вы узнаете: • Что представляет собой массив и как его объявить • Что такое строки и как их создавать с помощью массивов символов • Какие существуют отношения между массивами и указателями • Каковы особенности математических операций с указателями, связанными с массивами Что такое массивыМассивы представляют собой коллекции данных одного типа, сохраненные в памяти компьютера. Каждая единица данных называется элементом массива. Чтобы объявить массив, нужно указать его тип, имя и размер. Размер задается числом, взятым в квадратные скобки, и указывает, сколько элементов можно сохранить в данном массиве, например: long LongArray[25]; В этом примере объявляется массив под именем LongArray, который может содержать 25 элементов типа long int. Обнаружив подобную запись, компилятор резервирует в памяти компьютера место, чтобы сохранить 25 элементов указанного типа. Поскольку для сохранения одного значения типа long int требуется четыре байта памяти, то для заданного массива компилятор выделит цельную область памяти размером 100 байт (рис. 12.1). Элементы массиваАдресация элементов массива определяется по сдвигу относительно адреса первого элемента, сохраненного в имени массива. Первый элемент массива имеет нулевой сдвиг. Таким образом, к первому элементу массива можно обратиться следующим об разом: arrayName[0]. Если использовать пример массива, приведенный в предыдущем разделе, то обращение к первому элементу массива будет выглядеть так: LongArray[0], а ко второму — LongArray[1] и т.д. В общем виде, если объявлен массив Массив[n], то к его элементам можно обращаться, указывая индекс от Массив[0] до Массив[n-1]. Рис. 12.1. Объявление массива Так, в нашем примере массива LongArray[25] для обращения к элементам используются индексы от LongArray[0]flo LongArray[24]. В листинге 12.1 показано объявление массива целых чисел из пяти элементов и заполнение его данными. Листинг 12.1. Использование массива целых чисел 1: //Листинг 12.1. Массивы 2: #include <iostream.h> 3: 4: int main() 5: { 6: int myArray[5]; 7: int i; 8: for ( i=0; i<5; i++) // 0-4 9: { 10: cout << "Value for myArray[" << i << "]: "; 11: cin >> myArray[i]; 12: } 13: for (i = 0; i<5; i++) 14: cout << i << ": " << myArray[i] << "\n"; 15: return 0; 16: } Результат: Value for myArray[0] 3 Value for myArray[1] 6 Value for myArray[2] 9 Value for myArray[3] 12 Value for myArray[4] 15 0: 3 1: 6 2: 9 3: 12 4: 15 Анализ: В строке 6 объявляется массив myArray, который может содержать пять целочисленных значений. В строке 8 начинается цикл от 0 до 4, в котором задаются допустимые индексы созданного массива. Пользователю предлагается ввести свое значение для текущего элемента массива, после чего это значение сохраняется в компьютере по адресу, отведенному компилятором для данного элемента массива. Первое значение сохраняется по адресу, указанному в имени массива с нулевым сдвигом, — myArray[0], второе — в ячейке myАггау[1]и т.д. Второй цикл программы выводит сохраненные значения на экран. Примечание:Следует запомнить, что отсчет элементов массива начинается с 0, а не с 1. Это источник частых ошибок новичков в программах на C++. Если используется массив, состоящий из 10 элементов, то для обращения к элементам массива используются индексы от ArrayName[0] до ArrayName[9]. Обращение ArrayName[10] будет ошибочным. Вывод данных за пределами массиваПри записи данных в массив компилятор вычисляет адрес соответствующего элемента, основываясь на размере элемента и указанном сдвиге относительно первого элемента. Предположим, что некоторое значение записывается в шестой элемент рассмотренного нами ранее массива LongArray, для чего используется индекс LongArray[5]. Компилятор умножит указанное значение сдвига 5 на размер элемента (в нашем примере 4 байт) и получит 20 байт. Затем компилятор вычислит адрес шестого элемента массива, добавив к адресу массива 20 байт сдвига и запишет введенное значение по этому адресу. Если при записи данных будет указан индекс LongArray[50], то компилятор не сможет самостоятельно определить, что такого элемента массива просто не существует. Компилятор вычислит, что такой элемент должен находиться по адресу, сдвинутому на 200 байт относительно адреса первого элемента массива, и запишет в эту ячейку памяти введенное значение. В связи с тем, что выбранная область памяти может принадлежать любой другой переменной, результат такой операции для работы программы непредсказуем. Если вам повезет, то программа зависнет сразу же. Если вы неудачник, то программа продолжит работу и через некоторое время выдаст вам совершенно неожиданный результат. Такие ошибки очень сложно локализовать, поскольку строка, где проявляется ошибка, и строка, где ошибка была допущена в программе, могут далеко отстоять друг от друга. Компилятор ведет себя, как слепой человек, отмеряющий расстояние от дома к дому шагами. Он стоит возле первого дома на улице с адресом ГлавнаяУлица[0] и спрашивает вас, куда ему идти. Если будет дано указание следовать до шестого дома, то наш человек-компилятор станет размышлять следующим образом: "Чтобы добраться до шестого дома, от этого дома нужно пройти еще пять домов. Чтобы пройти один дом, нужно сделать четыре больших шага. Следовательно, нужно сделать 20 больших шагов." Если вы поставите задачу идти до дома ГлавнаяУлица[100], а на этой улице есть только 25 домов, то компилятор послушно начнет отмерять шаги и даже не заметит, что улица закончилась и началась проезжая часть с несущимися машинами. Поэтому, посылая компилятор по адресу, помните, что вся ответственность за последствия лежит только на вас. Возможный результат ошибочной записи за пределы массива показан в листинге 12.2. Предупреждение:Ни в коем случае не запускайте эту программу у себя на компьютере. Она может привести к поломке системы. Листинг 12.2. Запись за пределы массива 1: //Листинг 12.2. 2: // Пример того, что может произойти при записи 3: // за пределы массива 4: 5: #include <iostream.h> 6: int main() 7: { 8: // часовые 9: long sentinelOne[3]; 10: long TargetArray[25]; // массив для записи данных 11: long sentinelTwo[3]; 12: int i; 13: for (i=0; i<3; i++) 14: sentinelOne[i] = sentinelTwo[i] = 0; 15: 16: for (i=0; i<25; i++) 17: TargetArray[i] = 0; 18: 19: cout << "Test 1: \n"; // test current values (should be 0) 20: cout << "TargetArray[0]: " << TargetArray[0] << "\n"; 21: cout << "TargetArray[24]: " << TargetArray[24] << "\n\n"; 22: 23: for (i = 0; i<3; i++) 24: { 25: cout << "sentinelOne[" << i << "]: "; 26: cout << sentinelOne[i] << "\n"; 27: cout << "sentinelTwo[" << i << "]: "; 28: cout << sentinelTwo[i]<< "\n"; 29: } 30: 31: cout << "\nAssigning..."; 32: for (i = 0; i<=25; i++) 33: TargetArray[i] = 20; 34: 35: cout << "\nTest 2: \n"; 36: cout << "TargetArray[0]: " << TargetArray[0] << "\n"; 37: cout << "TargetArray[24]: " << TargetArray[24] << "\n"; 38: cout << "TargetArray[25]; " << TargetArray[25] << "\n\n"; 39: for (i = 0; i<3; i++) 40: { 41: cout << "sentinelOne[" << i << "]: "; 42: cout << sintinel0ne[i]<< "\n"; 43: cout << "sentinelTwo[" << i << "]: "; 44: cout << sentinelTwo[i]<< "\n"; 45: } 46: 47: return 0; 48: } Результат: Test 1: TargetArray[0]: 0 TargetArray[24]: 0 Sentinel0ne[0]: 0 SentinelTwo[0]: 0 SentinelOne[1]: 0 SentinelTwo[1]: 0 SentinelOne[2]: 0 SentinelTwo[2]: 0 Assigning... Test 2: TargetArray[0]: 20 TargetArray[24]: 20 TargetArray[25]: 20 Sentinel0ne[0]: 20 SentinelTwo[0]: 0 SentinelOne[1]: 0 SentinelTwo[1]: 0 SentinelOne[2]: 0 SentinelTwo[2]: 0 Анализ: В строках 9 и 11 объявляются два массива типа long по три элемента в каж- '" " ' дом, которые выполняют роль часовых вокруг массива TargetArray. Изначаль но значения этих массивов устанавливаются в 0. Если будет записано значение в массив TargetArray по адресу, выходящему за пределы этого массива, то значения массивов- часовых изменятся. Одни компиляторы ведут отсчет по возрастающей от адреса массива, другие — по убывающей. Именно поэтому используется два вспомогательных массива, расположенных по обе стороны от целевого массива TargetArray. В строках 19-29 проверяется равенство нулю значений элементов массивов-часовых (Test 1). В строке 33 элементу массива TargetArray присваивается значение 20, но при этом указан индекс 25, которому не соответствует ни один элемент массива TargetArray. В строках 36-38 выводятся значения элементов массива TargetArray (Test 2). Обратите внимание, что обрашение к элементу массива TargetArray[25] проходит вполне успешно и возвращается присвоенное ранее значение 20. Но когда на экран выводятся значения массивов-часовых Sentinel0na и SentinelTwo, вдруг обнаруживается, что значение элемента массива 5entinelQne изменилось. Дело в том, что обращение к массиву TargetArray[25] ссылается на ту же ячейку памяти, что и элемент массива SentinelQne[Q]. Таким образом, записывая значения в несуществующий элемент массива TargetArray, программа изменяет значение элемента совсем другого массива. Если далее в программе значения элементов массива SentinelOne будут использоваться в каких-то расчетах, то причину возникновения ошибки будет сложно определить. В этом состоит коварство ввода значений за пределы массива. В нашем примере размеры массивов были заданы значениями 3 и 25 в объявлении массивов. Гораздо безопаснее использовать для этого константы, объявленные где- нибудь в одном месте программы, чтобы программист мог легко контролировать размеры всех массивов в программе. Еще раз отметим, что, поскольку разные компиляторы по-разному ведут отсчет адресов памяти, результат выполнения показанной выше программы может отличаться на вашем компьютере. Ошибки подсчета столбцов для забораОшибки с записью данных за пределы массива случаются настолько часто, что для них используется особый термин — ошибки подсчета столбов для забора. Такое странное название было придумано по аналогии с одной житейской проблемой — подсчетом, сколько столбов нужно вкопать, чтобы установить 10-метровый забор, если расстояние между столбами должно быть 1 м. Многие не задумываясь отвечают — десять. Вот если бы задумались и посчитали, то нашли бы правильный ответ — одиннадцать. Если вы не поняли почему, посмотрите на рис. 12.2. Ошибка на единицу при установке индекса в обращении к массиву может стоить программе жизни. Нужно время, чтобы начинающий программист привык, что при обращении к массиву, состоящему из 25 элементов, индекс не может превышать значение 24 и отсчет начинается с 0, а не с 1. (Некоторые программисты потом настолько к этому привыкают, что в лифте нажимают на кнопку 4, когда им нужно подняться на пятый этаж.) Примечание:Иногда элемент массива ИмяМассива[0] называют нулевым, а не первым. Но и в этом случае легко запутаться. Если элемент ИмяМассива[0] нулевой, то каким тогда будет элемент ИмяМассива[1]? Первым или вторым? И так далее... Каким будет элемент ИмяМассива[24]— двадцать четвертым или двадцать пятым? Правильно будет считать элемент ИмяМассива[0] первым, имеющим нулевой сдвиг. Инициализация массиваИнициализацию массива базового типа (например, int или char) можно проводить одновременно с его объявлением. Для этого за выражением объявления массива нужно установить знак равенства (=) и в фигурных скобках список значений элементов массива, разделенных запятыми. Например: int IntegerArray[5] = {10, 20, 30, 40, 50>; В этом примере объявляется массив целых чисел IntegerArray и элементу IntegerArray[0] присваивается значение 10, элементу IntegerArray[ 1 ] — 20 И т.д. Если вы опустите установку размера массива, то компилятор автоматически вычислит размер массива по списку значений элементов. Поэтому справедливой является следующая запись: int IntegerArray[] = {10. 20, 30, 40, 50}; Рис. 12.2. Ошибка подсчета столбов для забора В результате получим тот же массив значений, что и в предыдущем примере. Если вам потребуется затем установить размер массива, обратитесь к компилятору, используя следующее выражение: const USHORT IntegorArrayLength; IntegerArrayLength = sizeof(IntegerArray)/sizeof(IntegerArray[0]); В этом примере число элементов массива определяется как отношение размера массива в байтах к размеру одного элемента. Результат отношения сохраняется в переменной IntegerArrayLength типа const USHORT, которая была объявлена строкой выше. Нельзя указывать в списке больше значений, чем заданное количество элементов массива, Так, следующее выражение вызовет показ компилятором сообщения об ошибке, поскольку массиву, состоящему из пяти элементов, пытаются присвоить шесть значений: int IntegerArray[5] = {10, 20, 30, 40, 50, 60); В то же время следующее выражение не будет ошибочным: int IntegerArray[5] = {10, 20}; Значения тех элементов массива, которые не были инициализированы при объявлении, не устанавливаются. Обычно считают, что значения неинициализированных элементов массива нулевые. В действительности они могут содержать любой мусор — данные, которые когда-то ранее были занесены в эти ячейки памяти, что, в свою очередь, может оказаться источником ошибки. Рекомендуется:Позвольте компилятору самостоятельно вычислять размер массива. Присваивайте массивам информативные имена, раскрывающие их назначение. Помните, что для обращения к первому элементу массива следует указать индекс 0. Не рекомендуется:Не записывайте данные за пределы массива. Объявление массивовМассиву можно присвоить любое имя, но оно должно отличаться от имени всех других переменных и массивов в пределах видимости зтого массива. Так, нельзя объявить массив myCats[5], если в программе ранее уже была объявлена переменная myCats. Размер массива при объявлении можно задать как числом, так и с помощью константы или перечисления, как показано в листинге 12,3. Листинг 12.3. Использование константы и перечисления при объявлении массива 1: // Листинг 12.3. 2: // Установка размера массива с помощью константы и перечисления 3: 4: #include <iostream.h> 5: int main() 6: { 7: enum WeekDays { Sun, Mon, Tue, 8: Wed, Thu, Fri, Sat, DaysInWeek }; 9: int ArrayWeek[DaysInWeek] = { 10, 20, 30, 40, 50, 60, 70 } 10: 11: cout << "The value at Tuesday is " << ArrayWeek[Tue]; 12: return 0; 13: } Результат: The value at Tuesday is 30 Анализ: В строке 7 объявляется перечисление WeekDays, содержащее восемь членов. Воскресенью (Sunday) соответствует значеню 0, а константе DaysInWeek — значение 7. В строке 11 константа перечисления Tue используется в качестве указателя на элемент массива. Поскольку константе Tue соответствует значение 2, то в строке 11 возвращается и выводится на печать значение третьего элемента массива ArrayWeek[2]. Массивы Чтобы объявить массив, сначала нужно указать тип объектов, которые будут в нем сохранены, затем определить имя массива и задать размер массива. Размер определяет, сколько объектов заданного типа можно сохранить в данном массиве. Пример 1: intMyIntegerArray[90]; Пример 2: long * Array0fPointersToLogs[100]; Чтобы получить доступ к элементам массива, используется оператор индексирования. Пример 1: Int theNinethInteger = MyIntegerArray[8]; Пример 2: long * pLong = Array0fPointersToLogs[8]; Отсчет индексов массива ведется с нуля. Поэтому, для обращения к массиву, содержащему n элементов, используются индексы от 0 до n-1. Массивы объектовЛюбой объект, встроенный или созданный пользователем, может быть сохранен в массиве. Но для этого сначала нужно объявить массив и указать компилятору, для объектов какого типа этот массив создан и сколько объектов он может содержать. Компилятор вычислит, сколько памяти нужно отвести для массива, основываясь на размере объекта, заданном при объявлении класса. Если класс содержит конструктор, заданный по умолчанию, в котором не устанавливаются параметры, то объект класса может быть создан и сохранен в массиве одновременно с объявлением массива. Получение доступа к данным переменных-членов объекта, сохраненного в массиве, идет в два этапа. Сначала с помощью оператора индексирования ([]) нужно указать элемент массива, а затем обратиться к конкретной переменной-члену с помощью оператора прямого обращения к члену класса (.). В листинге 12.4 показано создание массива для пяти объектов типа CAT. Листинг 12.4. Создание массива объектов 1: // Листинг 12.4. Массив объектов 2: 3: #include <iostream.h> 4: 5: class CAT 6: { 7: public: 8: CAT() { itsAge = 1; itsWeight=5; } 9: ~CAT() { } 10: int GetAge() const { return itsAge; } 11: int GetWeight() const { return itsWeight; } 12: void SetAge(int age) { itsAge = age; } 13: 14: private: 15: int itsAge; 16: int itsWeight; 17: }; 18: 19: int main() 20: { 21: CAT Litter[5]; 22: int i; 23: for (i = 0; i < 5; i++) 24: Litter[i].SetAge(2*i +1); 25: 26: for (i = 0; i < 5; i++) 27: { 28: cout << "cat #" << i + 1<< ": "; 29: cout << Litter[i].GetAge() << endl; 30: } 31: return 0; 32: } Результат: cat #1: 1 cat #2: 3 cat #3: 5 cat #4: 7 cat #5: 9 Анализ: В строках 5—17 объявляется класс CAT. Чтобы объекты класса CAT могли создаваться при объявлении массива, в этом классе должен использоваться конструктор, заданный по умолчанию. Вспомните, что если в классе создан какой- нибудь другой конструктор, то конструктор по умолчанию не будет предоставляться компилятором и вам придется создавать его самим. Первый цикл for (строки 23 и 24) заносит значения возраста кошек в объекты класса, сохраненные в массиве. Следующий цикл for (строки 26—30) обращается к каждомуобъек- ту, являющемуся элементом массива, и вызывает для выбранного объекта метод GetAge(). Чтобы применить метод GetAge() для объекта, являющегося элементом массива, используются последовательно операторы индексации ([]) и прямого доступа к члену класса (.), а также вызов функции-члена. Многомерные массивыМожно создать и использовать массив, содержащий более одного измерения. Доступ к каждому измерению открывается своим индексом. Так, чтобы получить доступ к элементу двухмерного массива, нужно указать два индекса; к элементу трехмерного массива — три индекса и т.д. Теоретически можно создать массив любой мерности, но, как правило, в программах используются одномерные и двухмерные массивы. Хорошим примерным двухмерного массива является шахматная доска, состоящая из клеток, собранных в восемь рядов и восемь столбцов (рис. 12.3). Рис. 12.3. Шахматная доска и двухмерный массив Предположим, что в программе объявлен класс SQUARE. Объявление двухмерного массива Board для сохранения объектов этого класса будет выглядеть следующим образом: SQUARE Board[8][8]; Эти же объекты можно было сохранить в одномерном массиве с 64 элементами: SGUARE Board[64]; Использование двухмерного массива может оказаться предпочтительнее, если такой массив лучше отражает положение вещей в реальном мире, например при создании программы игры в шахматы. Так, в начале игры король занимает четвертую позицию в первом ряду. С учетом нулевого сдвига позиция этой фигуры будет представлена объектом массива: Board[0][3]; В этом примере предполагается, что первый индекс будет контролировать нумерацию рядов, а второй — столбцов. Соответствие элементов массива клеткам шахматной доски наглядно показано на рис. 12.3. Инициализация многомерного массиваМногомерный массив также можно инициализировать одновременно с объявлением. При этом следует учитывать, что сначала весь цикл значений проходит индекс, указанный последним, после чего изменяется предпоследний индекс. Таким образом, если есть массив int theArray[5][3]: то первые три значения будут записаны в массив theArray[0], вторые три значения — в массив theArray[1] и т.д. Указанный массив можно инициализировать следующей строкой: int theArray[5][3] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}; Чтобы не запутаться в числах, значения можно сгруппировать с помошью дополнительных фигурных скобок, например: int theArray[5][3] = {{1,2,3}, {4,5,6}, {7,8,9}, {10,11,12}, {13,14,15}}; Компилятор проигнорирует все внутренние фигурные скобки. Все значения должны быть разделены запятыми независимо от того, используете вы дополнительные фигурные скобки или нет. Весь список значений должен быть заключен во внешние фигурные скобки, и после закрывающей скобки обязательно устанавливается символ точки с запятой. Пример создания двухмерного массива показан в листинге 12.5. Первый ряд двухмерного массива содержит целые числа от 0 до 4, а второй — удвоенные значения соответствующих элементов первого ряда. Листинг 12.5. Создание многомерного массива 1: #include <iostream.h> 2: int main() 3: { 4: int SomeArray[5][2] = { { 0,0} , { 1,2} , { 2,4} , { 3,6} , { 4,8} } 5: for (int t = 0; i<5; i++) 6: for (int j=0; j<2; j++) 7: { 8: cout << "SomeArray[" << i << "][" << j << "]: "; 9: cout << SomeArray[i][j]<< endl; 10: } 11: 12: return 0; 13: } Реультат: SomeArray[0][0]: 0 ' '' " SomeArray[0][1]: 0 SomeArray[1][0]: 1 SomeArray[1][1]: 2 SomeArray[2][0]: 2 SomeArray[2][1]: 4 SomeArray[3][0]: 3 SomeArray[3][1]: 6 SomeArray[4][0]: 4 SomeArray[4][1]: 8 Анализ: В строке 4 объявляется двухмерный массив. Первый ряд содержит пять целочисленных значений, а второй ряд представлен двумя значениями. В результате создается конструкция из десяти элементов (5x2), как показано на рис. 12.4. Рис. 12.4. Массив 5x2 Данные вводятся в массив попарно, хотя их можно было записать одной строкой. Затем осуществляется вывод данных с помощью двух вложенных циклов for. Внешний цикл последовательно генерирует индексы первого ряда, а внутренний — индексы второго ряда. В такой последовательности данные выводятся на экран: сначала идет элемент SomeArray[0][0], затем элемент SomeArray[0][1]. Приращение индекса первого ряда происходит после того, как индекс второго ряда становится равным 1, после чего вновь дважды выполняется внутренний цикл. Несколько слов о памятиПри объявлении массива компилятору точно указывается, сколько объектов планируется в нем сохранить. Компилятор зарезервирует память для всех объектов массива, даже если далее в программе они не будут заданы. Если вы заранее точно знаете, сколько элементов должен хранить массив, то никаких проблем не возникнет. Например, шахматная доска всегда имеет только 64 клетки, а от кошки можно ожидать, что она не родит более 10 котят. Если же изначально неизвестно, сколько элементов будет в массиве, то для решения этой проблемы нужно использовать более гибкие средства управления памятью. В этой книге рассматриваются только некоторые дополнительные средства программирования, такие как массивы указателей, массивы с резервированием памяти в области динамического обмена и ряд других возможностей. Больше информации о средствах программирования, открывающих дополнительные возможности, можно прочитать в моей книге C++ Unleashed, выпущенной издательством Sams Publishing. И вообще, всегда следует помнить, что каким бы хорошим программистом вы ни были, всегда остается то, чему следовало бы научиться, и всегда есть источники, откуда можно почерпнуть новую свежую информацию. Массивы указателейВсе массивы, рассмотренные нами до сих пор, хранили значения своих элементов в стеках памяти. Использование стековой памяти связано с рядом ограничений, которых можно избежать, если обратиться к более гибкой области динамической памяти. Это можно сделать, если сначала сохранить все объекты массива в области динамической памяти, а затем собрать в массиве указатели на эти объекты. Этот подход значительно сократит потребление программой стековой памяти компьютера. В листинге 12.6 показан тот же массив, с которым мы работали в листинге 12.4, но теперь все его объекты сохранены в области динамической памяти. Чтобы показать возросшую эффективность использования памяти программой, в этом примере размер массива был увеличен с 5 до 500 и его название изменено с Litter (помет) на Family (семья). Листинг 12.6. Сохранение массива в области динамической памяти 1: // Листинг 12.6. Массив указателей на обьекты 2: 3: #include <iostream.h> 4: 5: class CAT 6: { 7: public: 8: CAT() { itsAge = 1; itsWeight=5; } 9: ~CAT() { } // destructor 10: int GetAge() const { return itsAge; } 11: int GetWeight() const { return itsWeight: } 12: void SetAge(int age) ( itsAge = age; } 13: 14: private: 15: int itsAge; 16: int itsWeight; 17: }; 18: 19: int main() 20: { 21: CAT * Family[500]; 22: int i; 23: CAT * pCat; 24: for (i = 0; i < 500; i++) 25: { 26: pCat = new CAT; 27: pCat->SetAge(2*i +1); 28: Family[i] = pCat; 29: } 30: 31: for (i = 0; i < 500; i++) 32: { 33: cout << "Cat #" << i+1 << ": "; 34: cout << Family[i]->GetAge() << endl; 35: } 36: return 0; 37: } Результат: Cat #1: 1 Cat #2: 3 Cat #3: 5 ... Cat #499: 997 Cat #500: 999 Анализ: Объявление класса CAT в строках 5—17 идентично объявлению этого клас- • ca в листинге 12.4. Но, в отличие от предыдущего листинга, в строке 21 объявляется массив Family, в котором можно сохранить 500 указателей на объекты класса CAT. В цикле инициализации (строки 24-29) в области динамической памяти создается 500 новых объектов класса CAT, каждому из которых присваивается значение переменной itsAge, равное удвоенному значению индекса плюс один. Таким образом, первому объекту класса CAT присваивается значение 1, второму — 3, третьему — 5 и т.д. В этом же цикле каждому элементу массива присваивается указатель на вновь созданный объект. Поскольку тип массива был объявлен как CAT*, в нем сохраняются именно указатели, а не их разыменованные значения. Следующий цикл (строки 31—35) выводит на экран все значения объектов, на которые делаются ссылки в массиве. Обращение к указателю выполняется с помощью индекса: Family[i]. После того как элемент массива установлен, следует вызов метода GetAge(). В данном примере программы все элементы массива сохраняются в стековой памяти. Но в этот раз элементами являются указатели, тогда как сами объекты хранятся в области динамического обмена. Объявление массивов в области динамического обменаСуществует возможность поместить весь массив в область динамического обмена. Для этого используется ключевое слово new и оператор индексирования, как показано в следующем примере, где результатом этой операции является указатель на массив, сохраненный в области динамического обмена: CAT *Family = new CAT[500]; Указатель Family будет содержать адрес в динамической области первого элемента массива из пятисот объектов класса CAT. Другими словами, в указателе представлен адрес объекта Family[0]. Еще одно преимущество подобного объявления массива состоит в том, что в программе с переменной Family теперь можно будет выполнять математические действия как с любым другим указателем, что открывает дополнительные возможности в управлении доступом к элементам массива. Например, можно выполнить следующие действия: CAT *Family = new CAT[500]; CAT *pCat = Family; // pCat указывает на Family[0] pCat->SetAge(10); // присваивает Family[0] значение 10 pCat++; // переход к Family[1] pCat->SetAge(20); // присваивает Family[1] значение 20 В данном примере объявляется новый массив из 500 объектов класса CAT и возвращается указатель на первый элемент этого массива. Затем, используя это указатель и метод SetAge(), объявленный в классе CAT, первому объекту массива присваивается значение 10. Переход к следующему объекту массива осуществляется за счет приращения адреса в указателе на массив, после чего тем же способом присваивается значение 20 второму объекту массива. Указатель на массив или массив указателейРассмотрим следующие три объявления: 1: Cat Family0ne[500]; 2: CAT >> FamilyTwo[500]; 3: CAT * FamilyThree = new CAT[500]; В первом случае объявляется массив FamilyOne, содержащий 500 объектов типа CAT. Во втором случае — массив FamilyTwo, содержащий 500 указателей на объекты класса CAT, и в третьем случае — указатель FamilyThree, ссылающийся на массив из 500 объектов класса CAT. В зависимости от того, какое объявление используется в программе, принципиально меняются способы управления массивом. Как ни странно, но указатель FamilyThree по сути своей гораздо ближе к массиву FamilyOne, но принципиально отличается от массива указателей FamilyTwo. Чтобы разобраться в этом, следует внимательно рассмотреть, что содержат в себе все эти переменные. Указатель на массив FamilyThree содержит адрес первого элемента массива, но ведь это именно то, что содержит имя массива FamilyOne. Имена массивов и указателейВ C++ имя массива представляет собой константный указатель на первый элемент массива. Другими словами, в объявлении CAT Family[50]; создается указатель Family на адрес первого элемента массива &Family[0]. В программе допускается использование имен массивов как константных указателей и наоборот. Таким образом, выражению Family + 4 соответствует обращение к пятому элементу массива Family[4]. Компилятор выполняет с именами массивов те же математические действия сложения, инкремента и декремента, что и с указателями. В результате операция Family + 4 будет означать не прибавление четырех байтов к текущему адресу, а сдвиг на четыре объекта. Если размер одного объекта равен четырем байтам, то к адресу в имени массива будут добавлены не 4, а 16 байт. Если в нашем примере каждый объект класса CAT содержит четыре переменные-члена типа long по четыре байта каждая и две переменные-члена типа short по два байта каждая, то размер одного элемента массива будет равен 20 байт и операция Family + 4 сдвинет адрес в имени указателя на 80 байт. Объявление массива в динамической области памяти и его использование показано в листинге 12.7. Листинг 12.7. Создание массива с использованием ключевого слова new 1: // Листинг 12.7. Массив в динамической области памяти 2: 3: #include <iostream.h> 4: 5: class CAT 6: { 7: public: 8: CAT() { itsAge = 1; itsWeight=5; } 9: ~CAT(); 10: int GetAge() const { return itsAge; } 11: int GetWeight() const { return itsWeight; } 12: void SetAge(int age) { itsAge = age; } 13: 14: private: 15: int itsAgo; 16: int itsWeight; 17: }; 18: 19: CAT::~CAT() 20: { 21: // cout << "Destructor called!\n"; 22: } 23: 24: int main() 25: { 26: CAT * Family = new CAT[500]; 27: int i; 28: 29: for (i = 0; i < 500; i++) 30: { 31: Family[i].SetAge(2*i +1); 32: } 33: 34: for (i = 0; i < 500; i++) 35: { 36: cout << "Cat #" << i+1 << ": "; 37: cout << Family[i].GetAge() << endl; 38: } 39: 40: delete [] Family; 41: 42: return 0; 43: } Результат: Cat #1: 1 Cat #2: 3 Cat #3: 5 ... Cat #499: 997 Cat #500: 999 Анализ: В строке 26 объявляется массив Family для пятисот объектов класса CAT. Благодаря использованию выражения new CAT[500] весь массив сохраняется в области динамической памяти. Удаление массива из области динамической памятиКуда деваются при удалении массива все эти объекты класса CAT, показанные в предыдущем разделе? Не происходит ли здесь утечка памяти? Удаление массива Family с помощью оператора delete[ ] (не забудьте установить квадратные скобки) освобождает все ячейки памяти, отведенные для него. Компилятор достаточно сообразительный, чтобы удалить из памяти все объекты удаляемого массива и освободить динамическую память для нового использования. Чтобы убедиться в этом, измените размер массива в предыдущей программе с 500 на 10 в строках 26, 29 и 34. Затем разблокируйте выражение в строке 21 с оператором cout и запустите программу. Когда будет выполнена строка 43, последует десять вызовов деструктора для удаления каждого объекта класса CAT в массиве Family. Создавая какой-либо объект в области динамической памяти с помощью ключевого слова new, всегда удаляйте его из памяти с помощью оператора delete, если этот объект больше не используется в программе. В случае создания массива в области динамического обмена выражением new <class>[size] удалять его из памяти нужно оператором delete[]. Квадратные скобки указывают, что удаляется весь массив. Если вы забудете установить квадратные скобки, то из памяти будет удален только первый объект массива. В этом можно убедиться, если в нашем примере программы удалить квадратные скобки в строке 38. Если уже были внесены изменения в строку 21, как указывалось выше, то при выполнении программы на экране отобразится вызов только одного деструктора объекта, который удалит первый объект массива. Поздравляем вас! Вы потеряли огромный блок памяти для дальнейшего использования программой. Рекомендуется:Помните, что для обращения к массиву из n элементов используются индексы от 0 до n-1. Используйте свойства математических операций с указателями для управления доступом к элементам массива. Не рекомендуется:Не записывайте данные за пределы массива. Не путайте массив указателей с указателем на массив. Массивы символовСтрока текста представляет собой набор символов. Все строки, которые до сих пор использовались нами в программах, представляли собой безымянные строковые константы, используемые в выражениях с оператором cout, такие как: cout << "hello world.\n"; Но в C++ строку можно представить как массив символов, заканчивающийся кон- цевьш нулевым символом строки. Такой массив можно объявить и инициализировать точно так же, как любой другой массив, например: char Greeting[ ] = { 'H' , 'e' , ' 1' , 'Г , 'o' , ' ' , 'W' , 'o' , 1 r' , '1' , 'd' , 1 \0' }; В последний элемент массива заносится нулевой концевой символ строки (\0), который многие функции C++ распознают как символ разрыва строки. Хотя метод ввода строки текста в массив символ за символом работает нормально, это довольно утомительная процедура, чреватая ошибками. К счастью, C++ допускает упрощенный метод ввода строк текста в массивы: char Greeting[] = "hello world"; Обратите внимание на два момента данного синтаксиса. • Вместо одиночных кавычек вокруг каждого символа, запятых между символами и фигурных скобок вокруг всей строки в данном примере используются только двойные кавычки вокруг строки и ничего более. Нет даже обычных для инициализации массивов фигурных скобок. • Нет необходимости добавлять концевой нулевой символ, так как компилятор сделает это автоматически. Строка Hello World займет 12 байт. Пять байтов пойдет на слово Hello, пять на слово World и по одному байту на пробел и концевой нулевой символ. Инициализацию строкового массива можно оставить на потом. При этом, также как и с другими массивами, нужно следить, чтобы затем в массив не было записано символов больше, чем отводилось для этого места. В листинге 12.8 показан пример использования массива символов, который инициализируется строкой, вводимой пользователем с клавиатуры. Листинг 12.8. Заполнение массива символами 1: //Листинг 12.8. Заполнение массива символами 2: 3: #include <iostream.h> 4: 5: int main() 6: { 7: char buffer[80]; 8: cout << "Enter the string: "; 9: cin >> buffer; 10: cout << "Here is' the buffer: " << buffer << endl; 11: return 0; 12: } Результат: Enter the string: Hello World Here's the buffer: Hello Анализ: В строке 7 объявляется массив buffer, рассчитанный на 80 символов. Taкой массив может содержать строку из 79 букв, включая пробелы, плюс нулевой концевой символ строки. В строке 8 пользователю предлагается ввести строку текста, которая копируется в массив buffer в строке 9. Метод cin автоматически добавит нулевой концевой символ в конце введенной строки. Но при выполнении программы, показанной в листинге 12.8, возникает ряд проблем. Во-первых, если пользователь введет строку, содержащую более 79 символов, то оператор cin введет их за пределами массива, Во-вторых, оператор cin воспринимает пробел как окончание строки, после чего прекращает ввод данных. Чтобы решить эти проблемы, нужно использовать метод get(), применяемый вместе с оператором cin: cin,get(). Для выполнения метода нужно задать три параметра. • Буфер ввода. • Максимальное число символов. • Разделительный символ прерывания ввода. По умолчанию в качестве разделительного задается символ разрыва строки. Использование этого метода показано в листинге 12.9. Листинг 12.9. Заполнение массива 1: //Листинг 12.9. Использование метода cin.get() 2: 3: #include <iostream.h> 4: 5: int main() 6: { 7: char buffer[80]; 8: cout << "Enter the string: "; 9: cin.get(buffer, 79); // ввод завершается после 79 символа или символа разрыва строки 10: cout << "Here's the buffer: " << buffer << endl; 11: return 0; 12: } Результат: Enter the string: Hello World Here's the buffer: Hello World Анализ: В строке 9 осуществляется вызов метода cin.get(). Буфер ввода, заданный в строке 7, передается в функцию как первый аргумент. Второй аргумент задает максимальную длину строки, равную 79 символам. Допускается ввод только 79 символов, поскольку последний элемент массива отводится на концевой нулевой символ строки. Устанавливать третий аргумент не обязательно. В большинстве случаев в качестве разделительного символа подходит задаваемый по умолчанию символ разрыва строки. Функции strcpy() и strncpy()Язык C++ унаследовал от С библиотечные функции, выполняющие операции над строками. Среди множества доступных функций есть две, которые осуществляют копирование одной строки в другую. Это функции strcpy() и strncpy(). Функция strcpy() копирует строку целиком в указанный буфер, как показано в листинге 12.10. Листинг 12.10. Использование функции strcpy() 1: #include <iostream.h> 2: #include <string.h> 3: int main() 4: { 5: char String1[] = "No man is an island"; 6: char String2[80]; 7: 8: strcpy(String2,String1); 9: 10: cout << "String1: " << String1 << endl; 11: cout << "String2: " << String2 << endl; 12: return 0; 13: } Результат: String1: No man is an island String2: No man is an island Анализ: Файл заголовка string.h включается в программу в строке 2. Этот файл содержит прототип функции strcpy(). В качестве аргументов функции указываются два массива символов, первый из которых является целевым, а второй — массивом источника данных. Если массив-источник окажется больше целевого массива, то функция strcpy() введетданные за пределы массива. Чтобы предупредить подобную ошибку, в этой библиотеке функций содержится еще одна функция копирования строк: strncpy(). Эта функция копирует ряд символов, не превышающий длины строки, заданной в целевом массиве. Функция strncpy() также прерывает копирование, если ей повстречается символ разрыва строки. Использование функции strncpy() показано в листинге 12.11. Листинг 12.11. Использование функции strncpy() 1: #include <iostream.h> 2: #include <string.h> 3: int main() 4: { 5: const int MaxLength = 80; 6: char String1[] = "No man is an island"; 7: char String2[MaxLength+1]; 8: 9: 10: strncpy(String2,String1,MaxLength); 11: 12: cout << "String1: " << String1 << endl; 13: cout << "String2: " << String2 << endl; 14: return 0; 15: } Результат: String1: No man is an island String2: No man is an island Анализ: В строке 10 программа вместо функции strcpy() используется функцию strncpy(), третий параметр MaxLength которой задает максимальную длину копируемой строки. Размер массива String2 задан как MaxLength+1. Дополнительный элемент потребовался для концевого нулевого символа строки, который добавляется автоматически обеими функциями — strcpy() и strncpy(). Классы строкМногие компиляторы C++ содержат библиотеки классов, с помощью которых можно решать различные прикладные задачи. Одним из представителей встроенных классов является класс String. Язык C++ унаследовал от С концевой нулевой символ окончания строки и библиотеку строковых функций, куда входит функция strcpy(). Но все эти функции нельзя использовать в объектно-ориентированном программировании. Класс String предлагает набор встроенных функций-членов и переменных-членов, а также методов доступа, которые позволяют автоматически решать многие задачи, связанные с обработкой текстовых строк, получая команды от пользователя. Если в вашем компиляторе нет встроенного класса String, а иногда и в тех случаях, когда он есть, бывает необходимо создать собственный класс работы со строками. Далее в этой главе рассматривается процедура создания и применения класса String и пользовательских классов работы со строками. Как минимум, класс String должен преодолеть ограничения, свойственные использованию массивов символов. Подобно другим массивам, массивы символов статичны. Вам приходится задавать их размер при объявлении или инициализации. Они всегда занимают все отведенное для них пространство памяти, даже если вы используете только по- ловину элементов массива. Запись данных за пределы массива ведет к катастрофе. Хорошо написанный класс работы со строковыми данными выделяет столько памяти, сколько необходимо для текущего сеанса работы с программой, и всегда предусматривает возможность добавления новых данных. Если с выделением дополнительной памяти возникнут проблемы, предусмотрены элегантные пути их решения. Первый пример использования класса String показан в листинге 12.12. Листинг 12.12. Использование класса String 1: // Листинг. 12.12 2: 3: #include <iostream.h> 4: #include <string.h> 5: 6: // Рудиментарный класс string 7: class String 8: { 9: public: 10: // Конструкторы 11: String() 12: Stnng(const char *const), 13: Stnng(const String &), 14: ~Stnng() 15: 16: // Перегруженные операторы 17: char & operator[](unsigned short offset), 18: char operator[](unsigned short offset) const, 19: Stnng operator+(const String&), 20: void operator+=(const String&) 21: Stnng & operator= (const Stnng &), 22: 23: // Основные методы доступа 24: unsigned short GetLen()const { return itsLen, } 25: const char * GetStnng() const { return itsStnng, } 26: 27: private: 28: Stnng (unsigned short), // Закрытый конструктор 29: char * itsStnng, 30: unsigned short itsLen 31: } 32: 33: // Конструктор, заданный no умолчанию, создает строку нулевой длины 34: String String() 35: { 36: itsStnng = new char[1] 37: itsStrmg[0] = '\0' 38: itsLen=0; 39: } 40: 41: // Закрытый (вспомогательный) конструктор 42: // используется только методами класса для создания 43: // строк требуемой длины с нулевым наполнением 4й: String String(unsigned short len) 45: { 46: itsStnng = new char[len+1] 47: for (unsigned short i = 0 i<=len, i++) 48: itsString[i] = \0 , 49: itsLen=len, 50: } 51: 52: // Преобразование массива символов в строку 53: String String(const char * const cString) 54: { 55: itsLen = strlen(cString); 56: itsString = new char[itsLen+1]; 57: for (unsigned short i = 0; i<itsLen: i++) 58: itsString[i] = cString[i]; 59: itsString[itsLen]='\0'; 60: } 61: 62: // Конструктор-копировщик 63: String::String (const String & rhs) 64: { 65: itsLen=rhs.GetLen(); 66: itsString = new char[itsLen+1]; 67: for (unsigned short i = 0; i<itsLen;i++) 68: itsString[i] = rhs[i]; 69: itsString[itsLen] = '\0'; 70: } 71: 72: // Деструктор для освобождения памяти 73: String::~String () 74: { 75: delete [] itsString; 76: itsLen = 0; 77: } 78: 79: // Оператор присваивания освобождает память 80: // и копирует туда string и size 81: String& String::operator=(const String & rhs) 82: { 83: if (this == &rhs) 84: return *this; 85: delete [] itsString; 86: itsLen=rhs.GetLen(); 87: itsString = new char[itsLen+1]; 88: for (unsigned short i = 0; i<itsLen;i++) 89: itsString[i] = rhs[i]; 90: itsString[itsLen] = '\0'; 91: return *this; 92: } 93: 94: //неконстантный оператор индексирования 95: // возвращает ссылку на символ так, что его 96: // можно изменить! 97: char & String::operator[](unsigned short offset) 98: { 99: if (offset > itsLen) 100: return itsString[itsLen-1]; 101: else 102: return itsString[offset]; 103: } 104: 105: // константный оператор индексирования для использования 106: // с константными объектами (см. конструктор-копировщик!) 107: char String::operator[](unsigned short offset) const 108: { 109: if (offset > itsLen) 110: return itsString[itsLen-1]; 111: else 112: return itsString[offset]; 113: } 114: 115: // создание новой строки путем добавления 116: // текущей строки к rhs 117: String String::operator+(const String& rhs) 118: { 119: unsigned short totalLen = itsLen + rhs.GetLen(); 120: String temp(totalLen); 121: unsigned short i; 122: for ( i= 0; i<itsLen; i++) 123: temp[i] = itsString[i]; 124: for (unsigned short j = 0; j<rhs.GetLen(); j++, i++) 125: temp[i] = rhs[j]; 126: temp[totalLen]='\0'; 127: return temp; 128: } 129: 130: // изменяет текущую строку и возвращает void 131: void String::operator+=(const String& rhs) 132: { 133: unsigned short rhsLen = rhs.GetLen(); 134: unsigned short totalLen = itsLen + rhsLen; 135: String temp(totalLen); 136: unsigned short i; 137: for (i = 0; i<itsLen; i++) 138: temp[i] = itsString[i]; 139: for (unsigned short j = 0; j<rhs.GetLen(); j++, i++) 140: temp[i] = rhs[i-itsLen]; 141: temp[totalLen]='\0'; 142: *this = temp; 143: } 144: 145: int main() 146: { 147: String s1("initial test"); 148: cout << "S1:\t" << s1.GetString() << endl; 149: 150: char * temp = "Hello World"; 151: s1 = temp; 152: cout << "S1:\t" << s1.GetString() << endl; 153: 154: char tempTwo[20]; 155: strcpy(tempTwo,"; nice to be here!"); 156: s1 += tempTwo; 157: cout << "tempTwo:\t" << tempTwo << endl; 158: cout << "S1:\t" << s1.GetString() << endl; 159: 160: cout << "S1[4] :\t" << s1[4] << endl; 161: s1[4]='o'; 162: cout << "S1:\t" << s1.GetString() << endl; 163: 164: cout << "S1[999] :\t" << s1[999] << endl; 165: 166: String s2(" Another string"); 167: String s3; 168: s3 = s1+s2; 169: cout << "S3:\t" << s3.GetString() << endl: 170: 171: String s4; 172: s4 = "Why does this work?"; 173: cout << "S4:\t" << s4.GetString() << endl; 174: return 0; 175: } Результат: S1: initial test S1: Hello world tempTwo: ; nice to be here! S1: Hello world; nice to be here! S1[4]: o S1: Hello World; nice to be here! S1[999]: ! S3: Hello World; nice to be here! Another string S4: Why does this work? Анализ: В строках 7—31 объявляется простой класс String. В строках 11—13 объявляются конструктор по умолчанию, конструктор-копировщик и конструктор для приема существующей строки с концевым нулевым символом (стиль языка С). В классе String перегружаются операторы индексирования ([]), суммирования (+) и присваивания с суммой (+=). Оператор индексирования перегружается дважды. Один раз как константная функция, возвращающая значение типа char, а другой — как неконстантная функция, возвращающая указатель на char. Неконстантная версия оператора используется в выражениях вроде строки 161: SomeString[4]=V; В результате открывается прямой доступ к любому символу строки. Поскольку возвращается ссылка на символ, функция получает доступ к символу и может изменить его. Константная версия оператора используется в тех случаях, когда необходимо получить доступ к константному объекту класса String, например при выполнении конструктора-копировщика в строке 63. Обратите внимание, что в этом случае открывается доступ к rhs[i], хотя rhs был объявлен как const String &. К этому объекту невозможно получить доступ, используя неконстантные функции-члены. Поэтому оператор индексирования необходимо перегрузить как константный. Если возвращаемый объект окажется слишком большим, возможно, вам потребуется установить возврат не значения, а константной ссылки на объект. Но поскольку в нашем случае один символ занимает всего один байт, в этом нет необходимости. Конструктор, заданный по умолчанию, выполняется в строках 33—39. Он создает строку нулевой длины. Общепринято, что в классе String длина строки измеряется без учета концевого нулевого символа. Таким образом, строка, созданная по умолчанию, содержит только концевой нулевой символ. Конструктор-копировщик выполняется в строках 63—70. Он задает длину новой строки равной длине существующей строки плюс одна ячейка для концевого нулевого символа. Затем конструктор-копировщик копирует существующую строку в новую и добавляет в конце нулевой символ окончания строки. В строках 53—60 выполняется конструктор, принимающий строку с концевым нулевым символом. Этот конструктор подобен конструктору-копировщику. Длина существующей строки определяется с помощью стандартной функции strlen() из библиотеки String. В строке 28 объявляется еще один конструктор, String(unsigned short), как закрытая функция-член. Он был добавлен для того, чтобы не допустить создания в классе String строк произвольной длины каким-нибудь другим пользовательским классом. Этот конструктор позволяет создавать строки только внутри класса String в соответствии со сделанными установками, как, например, в строке 131 с помощью operator+=. Более подробно этот вопрос рассматривается ниже, при объявлении operator+=. Конструктор String(unsigned short) заполняет все элементы своего массива символов значениями NULL. Поэтому в цикле for выполняется проверка i<=len, а не i<len. Деструктор, выполняемый в строках 73—77, удаляет строку текста, поддерживаемую классом String. Обратите внимание, что за оператором delete следуют квадратные скобки. Если опустить их, то из памяти компьютера будут удалены не все объекты класса, а только первый из них. Оператор присваивания прежде всего определяет, не соответствуют ли друг другу операнды слева и справа. Если операнды отличаются друг от друга, то текущая строка удаляется, а новая копируется в эту область памяти. Чтобы упростить присвоение, возвращается ссылка на строку, как в следующем примере: String1 = String2 = String3; Оператор индексирования перегружается дважды. В обоих случаях проверяются границы массива. Если пользователь попытается возвратить значение из ячейки памяти, находяшейся за пределами массива, будет возвращен последний символ массива (len-1). В строках 117-128 оператор суммирования (+) выполняется как оператор конкатенации. Поэтому допускается создание новой строки из двух строк, как в следующем выражении: String3 = String1 + String2; Оператор (+) вычисляет длину новой строки и сохраняет ее во временной строке temp. Эта процедура вовлекает закрытый конструктор, который принимает целочисленный параметр и создает строку, заполненную значениями NULL. Нулевые значения затем замещаются символами двух строк. Первой копируется строка левого операнда (*this), после чего — строка правого операнда (rhs). Первый цикл for последовательно добавляет в новую строку символы левой строки'. Второй цикл for выполняет ту же операцию с правой строкой. Обратите внимание, что счетчик i продолжает отсчет символов новой строки после того, как счетчик j начинает отсчет символов строки rhs. Оператор суммирования возвращает временную строку temp как значение, которое присваивается строке слева от оператора присваивания (string1). Оператор += манипулирует с уже существующими строками, как в случае string1 += string2. В этом примере оператор += действует так же, как оператор суммирования, но значение временной строки temp присваивается не новой, а текущей строке (*this = temp), как в строке 142. Функция main() (строки 145—175) выполняется для проверки результатов работы данного класса. В строке 147 создается объект String с помощью конструктора, задающего строки в стиле языка С с концевым нулевым символом. Строка 148 выводит содержимое этого объекта с помощью функции доступа GetString(). В строке 150 создается еще одна строка текста в стиле языка С. В строке 151 тестируется перегруженный оператор присваивания, а строка 152 выводит результат. В строке 154 создается третья строка с концевым нулевым символом — tempTwo. В строке 155 с помощью функции strcpy() происходит заполнение буфера строкой символов nice to be here!. В строке 156 с помощью перегруженного оператора += осуществляется конкатенация строки tempTwo к существующей строке s1. Результат выводится на экран в строке 158. В строке 160 возвращается и выводится на экран пятый символ строки — s1. Затем в строке 161 этот символ замещается другим с помощью неконстантного оператора индексирования ([]). Результат выводится строкой 162, чтобы показать, что символ строки действительно изменился. В строке 164 делается попытка получить доступ к символу за пределами массива. Возвращается и выводится на печать последний символ строки, как и было предусмотрено при перегрузке оператора индексирования. В строках 166 и 167 создаются два дополнительных объекта String, и в строке 168 используется перегруженный оператор суммирования. Результат выводится строкой 169. В строке 171 создается еще один объект класса String — s4. В строке 172 используется оператор присваивания, а строка 173 выводит результат. Оператор присваивания перегружен таким образом, чтобы использовать константную ссылку класса String, объявленную в строке 21, но в данном случае в функцию передается строка с концевым нулевым символом. Разве это допустимо? Хотя компилятор, ожидая получить объект String, вместо этого получает массив символов, он автоматически проверяет возможность преобразования полученного значения в ожидаемую строку. В строке 12 объявляется конструктор, который создает объект String из массива символов. Компилятор создает временный объект String из полученного массива символов и передает его в функцию оператора присваивания. Такой процесс называется неявным преобразованием. Если бы в программе не был объявлен соответствующий конструктор, преобразующий массивы символов, то для этой строки компилятор показал бы сообщение об ошибке. Связанные списки и другие структурыМассивы являются отличными контейнерами для данных самых разных типов. Единственный их недостаток состоит в том, что при создании массива необходимо за- дать его фиксированный размер. Если на всякий случай создать слишком большой массив, то попусту будет потрачено много памяти компьютера. Если сэкономить память, то возможности программы по оперированию данными окажутся ограниченными. Один из способов решения этой проблемы состоит в использовании связанных списков. Связанный список представляет собой структуру данных, состоящую из взаимосвязанных блоков, каждый из которых может поддерживать структурную единицу данных. Идея состоит в том, чтобы создать класс, поддерживающий объекты данных определенного типа, такого как CAT или Rectangle, которые, помимо данных, содержали бы также указатели, связанные с другими объектами этого класса. Таким образом, получается класс, содержащий взаимосвязанные объекты, образующие произвольную структуру-список. Такие объекты называют узлами. Первый узел в списке образует голову, а последний — хвост. Существует три основных типа связанных списков. Ниже они перечислены в порядке усложнения. • Однонаправленные списки. • Двунаправленные списки. • Деревья. В однонаправленных связанных списках каждый узел указывает на следующий узел только в одном направлении. Движение по узлам в обратном направлении невозможно. Чтобы найти нужный узел, следует начать с первого узла и двигаться от узла к узлу, подобно кладоискателю, действующему согласно указаниям карты поиска сокровищ: "...от большого камня иди к старому дубу, сделай три шага на восток и начинай копать..." Двунаправленные списки позволяют осуществлять движение в обоих направлениях по цепи. Деревья представляют собой более сложные структуры, в которых один узел может содержать ссылки на два или три следующих узла. Все три типа связанных списков схематично показаны на рис. 12.5. Общие представления о связанных спискахВ данном разделе обсуждаются основные моменты создания сложных структур и, что еще более важно, возможности использования в больших проектах наследования, полиморфизма и инкапсуляции. Делегирование ответственностиОсновная идея объектно-ориентированного программирования состоит в том, что каждый объект специализируется в выполнении определенных задач и передает другим объектам ответственность за выполнение тех задач, которые не соответствуют их основному предназначению. Примером реализации этой идеи в технике может быть автомобиль. Назначение двигателя — вырабатывать свободную энергию. Распределение энергии уже не входит в круг задач двигателя. За это ответственна трансмиссия. И в конце концов, движение автомобиля за счет отталкивания от дороги осуществляется с помощью колес, а двигатель и трансмиссия принимают в этом деле существенное, но косвенное участие. Хорошо сконструированная машина состоит из множества деталей с четким распределением функций и структурным взаимодействием между ними, обеспечивающим решение сложных задач. Так же должна выглядеть хорошо написанная программа: каждый класс вплетает свою нить, а в результате получается шикарный персидский ковер. Рис. 12.5. Связанные списки Компоненты связанных списковСвязанный список состоит из узлов. Узлы представляют собой абстрактные классы. В нашем примере для построения связанного списка используются три подтипа данных. Один узел будет представлять голову связанного списка и отвечать за его инициализацию. Попробуйте догадаться сами, за что отвечает хвостовой узел. Между ними могут быть представлены (либо могут отсутствовать) один или несколько промежуточных узлов, которые отвечают за обработку данных, переданных в список. Обратите внимание, что данные списка и сам список — это не одно и то же. В списке могут быть представлены данные любого типа, но связываются друг с другом не данные, а узлы, которые содержат данные. Выполняемой части программы ничего не известно об узлах, она работает со связанным списком как с единым целым. В то же время функциональная нагрузка на список как таковой весьма ограничена — он просто распределяет ответственность за выполнение задач между узлами. В листинге 12.13 рассматривается пример программы со связанным списком, а затем детально анализируется ее работа. Листинг 12.13. Связанный список 0: // ********************************************** 1: // Листинг 12.13. 2: // 3: // ЦЕЛЬ: Показать использование связанного списка 4: // ПРИМЕЧАНИЯ: 5: // 6: // Авторское право: Copyright (С) 1998 Liberty Associates, Inc. 7: // Все права защищены 8: // 9: // Показан один из подходов обьектно-ориентированного 10: // программирования по созданию связанных списков. 11: // Список распределяет задачи между узлами, 12: // представляющими собой абстрактные типы данных. 13: // Список состоит из трех узлов: головного, 14: // хвостового и промежуточного. Данные содержит 15: // только промежуточный узел. 16: // Все объекты, используемые в списке, относятся 17: // к классу Data. 18: // ********************************************** 19: 20: 21: #include <iostream.h> 22: 23: enum { kIsSmaller, kIsLarger, kIsSame}; 24: 25: // Связанный список основывается на обьектах класса Data 26: // Любой класс в связанном списке должен поддерживать два метода: 27: // Show (отображение значения) и Compare (возвращение относительной позиции узла) 28: class Data 29: { 30: public: 31: Data(intval):myValue(val){ } 32: ~Data(){ } 33: int Compare(const Data &); 34: void Show() { cout << myValue << endl; } 35: private: 36: int myValue; 37: }; 38: 39: // Сравнение используется для определения 40: // позиции в списке для нового узла. 41: int Data::Compare(const Data & theOtherData) 42: { 43: if (myValue < theOtherData.myValue) 44: return kIsSmaller; 45: if (myValue > theOtherData.myValue) 46: return kIsLarger; 47: else 48: return kIsSame; 49: } 50: 51: // Объявления 52: class Node; 53: class HeadNode; 54: class TailNode; 55: class InternalNode; 56: 57: // ADT-представление узловых объектов списка. 58: // В каждом производном классе должны быть замещены функции Insert и Show 59: class Node 60: { 61: public: 62: Node(){ } 63: virtual ~Node(){ } 64: virtual Node * Insert(Data * theData)=0; 65: virtual void Show() = 0; 66: private: 67: }; 68: 69: // Этот узел поддерживает реальные объекты. 70: // В данном случае объект имеет тип Data 71: // 0 другом, более общем методе решения этой 72: // задачи мы узнаем при рассмотрении шаблонов. 73: class InternalNode: public Node 74: { 75: public: 76: InternalNode(Data * theData, Node * next); 77: ~InternalNode() { delete myNext; delete myData; } 78: virtual Node * Insert(Data * theData); 79: virtual void Show() { myData->Show(); myNext->Show(); } // Делегирование! 80: 81: private: 82: Data * myData; // данные списка 83: Node * myNext; // указатель на следующий узел в связанном списке 84: }; 85: 86: // Инициализация, выполняемая каждым конструктором 87: InternalNode::InternalNode(Data * theData, Node * next): 88: myData(theData), myNext(next) 89: { 90: } 91: 92: // Сущность списка. 93: // Когда в список передается новый объект, 94: // программа определяет позицию в списке 95: // для нового узла 96: Node * InternalNode::Insert(Data * theData) 97: { 98: 99: // Этот новенький больше или меньше чем я? 100: int result = myData->Compare(*theData); 101: 102: 103: switch(result) 104: { 105: // По соглашению, если он такой же как я, то он идет первым 106: case kIsSame: // условие выполняется 107: case kIsLarger: // новые данные вводятся перед моими 108: { 109: InternalNode * dataNode = new InternalNode(theData, this); 110: return dataNode; 111: } 112: 113: // Он больше чем я, поэтому передается в 114: // следующий узел, и пусть тот делает с этими данными все, что захочет. 115: case kIsSmaller: 116: myNext = myNext->Insert(theData); 117: return this; 118: } 119: return this; // появляется MSC 120: } 121: 122: 123: // Хвостовой узел выполняет роль часового 124: 125: class TailNode : public Node 126: { 127: public: 128: TailNode(){ } 129: ~TailNode(){ } 130: virtual Node * Insert(Data * theData); 131: virtual void Show() { } 132: 133: private: 134: 135: }; 136: 137: // Если данные подходят для меня, то они должны быть вставлены передо мной, 138: // так как я хвост и НИЧЕГО не может быть после меня 139: Node * TailNode::Insert(Data * theData) 140: { 141: InternalNode * dataNode = ew InternalNode(theData, this); 142: return dataNode; 143: } 144: 145: // Головной узел не содержит данных, он только 146: // указывает на начало списка 147: class HeadNode : public Node 148: { 149: public: 150: HeadNode(); 151: ~HeadNode() { delete myNext; } 152: virtual Node * Insert(Data * theData); 153: virtual void Show() { myNext->Show(); } 154: private: 155: Node * myNext; 156: }; 157: 158: // Как только создается головной узел, 159: // он создает хвост 160: HeadNode::HeadNode() 161: { 162: myNext = new TailNode; 163: } 164: 165: // Ничего не может быть перед головой, поэтому 166: // любые данные передаются в следующий узел 167: Node * HeadNode::Insert(Data * theData) 168: { 169: myNext = myNext->Insert(theData); 170: return this; 171: } 172: 173: // Я только распределяю задачи между узлами 174: class LinkedList 175: { 176: public: 177: LinkedList(); 178: ~LinkedList() { delete myHead; } 179: void Insert(Data * theData); 180: void ShowAll() { myHead->Show(); } 181: private: 182: HeadNode * myHead; 183: }; 184: 185: // Список появляется с созданием головного узла, 186: // который сразу создает хвостовой узел. 187: // Таким образом, пустой список содержит указатель на головной узел, 188: // указывающий, в свою очередь, на хвостовой узел, между которыми пока ничего нет. 189: LinkedList::LinkedList() 190: { 191: myHead = new HeadNode; 192: } 193: 194: // Делегирование, делегирование, делегирование 195: void LinkedList::Insert(Data * pData) 196: { 197: myHead->Insert(pData); 198: } 199: 200: // выполняемая тестовая программа 201: int main() 202: { 203: Data * pData; 204: int val; 205: LinkedList 11; 206: 207: // Предлагает пользователю ввести значение, 208: // которое передается в список 209: for (;;) 210: { 211: cout << "What value? (0 to stop): "; 212: cin >> val; 213: if (!val) 214: break; 215: pData = new Data(val); 216: ll.Insert(pData); 217: } 218: 219: // теперь пройдемся по списку и посмотрим значения 220: ll.ShowAll(); 221: return 0; // 11 выходит за установленные рамки и поэтому удалено! 222: } Результат: What value? (0 to stop) 5 What value? (0 to stop) 8 What value? (0 to stop) 3 What value? (0 to stop) 9 What value? (0 to stop) 2 What value? (0 to stop) 10 What value? (0 to stop) 0 2 3 5 8 9 10 Анализ: Первое, на что следует обратить внимание, — это константное перечисление, в котором представлены константы kIsSmaller, kIsLarger и kIsSame. Любой объект, представленный в списке, должен поддерживать метод Compare('). Константы, показанные выше, возвращаются в результате выполнения этого метода. В строках 28—37 объявляется класс Data, а в строках 39—49 выполняется метод Compare(). Объекты класса Data содержат данные и могут использоваться для сравнения с другими объектами класса Data. Они также поддерживают метод Show(), отображающий значение объекта класса Data. Чтобы лучше разобраться в работе связанного списка, проанализируем шаг за шагом выполнение программы, показанной выше. В строке 201 объявляется выполняемый блок программы, в строке 203 — указатель на класс Data, а в строке 205 определяется связанный список. Для создания связанного списка в строке 189 вызывается конструктор. Единственное, что он делает, — выделяет области памяти для объекта HeadNode и присваивает адрес объекта указателю, поддерживаемому связанным списком и объявленному в строке 182. При создании объекта HeadNode вызывается еще один конструктор, объявленный в строках 160—163, который, в свою очередь, создает объект TailNode и присваивает его адрес указателю myNext, содержащемуся в объекте HeadNode. При создании объекта TailNode вызывается конструктор TailNode, объявленный в строке 128. Тело конструктора содержится в той же строке программы, и он не создает никаких новых объектов. Таким образом, создание связанного списка вызывает последовательность взаимосвязанных процессов, в результате которых для него выделяется область стековой памяти, создаются головной и хвостовой узлы и устанавливаются взаимосвязи между ними, как показано на рис. 12.6. В строке 209 начинается бесконечный цикл. Появляется предложение пользователю ввести значение, которое будет добавлено в связанный список. Ввод новых значений можно продолжать до бесконечности. Ввод значения 0 завершает цикл. Введенное значение проверяется в строке 213. Если введенное значение отличается от 0, то в строке 215 создается новый объект типа Data, а в строке 216 он вводится в связанный список. Предположим, что пользователь ввел число 15, после чего в строке 195 будет вызван метод Insert. Рис. 12.6. Связанный список сразу после создания Связанный лист немедленно передаст ответственность за ввод объекта головному узлу, вызвав в строке 167 метод Insert. Головной узел немедленно делегирует ответственность любому другому узлу (вызывает в строке 139 метод Insert), адрес которого хранится в указателе myNext. Сначала в этом указателе представлен адрес хвостового узла (вспомните, что при создании головного узла автоматически создается хвостовой узел и ссылка на него добавляется в головной узел). Хвостовому узлу TailNode известно, что любой объект, переданный обращением TailNode::Insert, нужно добавить в список непосредственно перед собой. Так, в строке 141 создается объект InternalNode, который добавляется в список перед хвостовым узлом и принимает введенные данные и указатель на хвостовой узел. Эта процедура выполняется с помощью объявленного в строке 87 конструктора объекта InternalNode. Конструктор объекта InternalNode просто инициализирует указатель класса Data адресом переданного объекта этого класса, а также присваивает указателю myNext этого объекта адрес того узла, из которого он был передан. В случае создания первого промежуточного узла этому указателю будет присвоен адрес хвостового узла, поскольку, как вы помните, именно хвостовой узел передал свой указатель this. Теперь, после того как был создан узел InternalNode, адрес этого узла присваивается указателю dataNode в строке 141, и именно этот адрес возвращается теперь методом TailNode::Insert(). Так мы возвращаемся к методу HeadNode::Insert(), где адрес узла InternalNode присваивается указателю myNext узла HeadNode (строка 169). И наконец, адрес узла HeadNode возвращается в связанный список — туда, где в строке 197 он был сброшен (ничего страшного при этом не произошло, так как связанному списку уже был известен адрес головного узла). Зачем было беспокоиться о возвращении адреса, если он не используется? Метод Insert был объявлен в базовом классе Node. Для выполнения метода необходимо задать значение возврата. Если изменить значение возврата функции HeadNode::Insert(), то компилятор покажет сообщение об ошибке. Это все равно что возвратить узел HeadNode и позволить связанному списку выбросить его адрес. Так что же все-таки произошло? Данные были введены в список. Список передал эти данные головному узлу. Головной узел передал их дальше по тому адресу, что хранится в его указателе. В первом цикле в этом указателе хранился адрес хвостового узла. Хвостовой узел немедленно создает новый промежуточный узел, указателю которого присваивается адрес хвостового узла. Затем хвостовой узел возвращает адрес промежуточного узла в головной узел, где он присваивается указателю myNext головного узла. Итак, свершилось то, что требовалось: данные расположились в списке правильным образом (рис. 12.7). После ввода первого промежуточного узла программа переходит к строке 211 и выводит предложение пользователю ввести новое значение. Предположим, что в этот раз было введено значение 3. В результате в строке 215 создается новый объект класса Data и вводится в список в строке 216. Рис. 12.7. Вид связанного списка после того, как был добавлен первый промежуточный узел И вновь в строке 197 список передает новое значение в головной узел HeadNode. Метод HeadNode: :Insert(), в свою очередь, передает эти данные по адресу, хранящемуся в указателе myNext. Как вы помните, теперь он указывает на узел, содержащий объект типа Data со значением 15. В результате в строке 96 вызывается метод InternalNode::Insert(). В строке 100 указатель myData узла InternalNode сообщает объекту этого узла (значение которого сейчас равно 15) о необходимости вызвать метод Compare(), принимающий в качестве аргумеи'- та новый объект Data со значением 3. Метод Compare() объявлен в строке 41. Происходит сравнение значений двух объектов, и, поскольку значение myValue соответствует 15, а theOtherData.myValue равно 3, метод возвращает константу kIsLarget. B соответствии со значением возвращенной константы программа переходит к выполнению строки 109. Создается новый узел InternalNode для нового объекта данных. Новый узел будет указывать на текущий объект узла InternalNode, и адрес нового узла InternalNode возвращается методом InternalNode:;Insert() в головной узел HeadNode. Таким образом, новый узел, значение которого меньше значения текущего объекта, добавляется в связанный список, после чего связанный список выглядит так, как показано на рис, 12.8. Натретьем цикле пользователь ввел значение 8. Оно больше чем 3, но меньше чем 15, поэтому программа должна ввести новый объект данных между двумя существующими промежуточными узлами. Последует та же серия операций, что и в предыдущем цикле, за тем исключением, что при вызове метода Compare() для объекта типа Data, значение кото- рогоравно 3, будет вОзвращена константа kIsSmaller, а не kIsLarger, как в предыдущем случае (поскольку значение текущего объекта 3 меньше значения нового объекта 8). В результате метод InternalNode::Insert() переведет выполнение программы на строку 116. Вместо создания и ввода в список нового узла, новые данные будут переданы в метод Insert того объекта, на который ссылается указатель myNext текущего объекта. В данном случае будет вызван метод InsertNode для промежуточного узла, значение объекта которого равняется 15. Вновь будет проведено сравнение данных, которое в этот раз завершится созданием нового промежуточного узла. Этот новый узел будет ссылаться на тот промежуточный узел, значение которого 15, а адрес нового узла будет присвоен указателю узла со значением 3, как показано в строке 116. Рис. 12.8. Вид связанного списка после того, как был добавлен второй промежуточный узел В результате новый узел вновь будет вставлен в правильную позицию. Если вы переписали эту программу И запустили на своем компьютере, то с помощью средства отладки можно посмотреть, как будет происходить ввод других данных программой. Каждый раз будет проводиться сравнение данных и новые узлы будут добавляться в список строго в порядке возрастания значений. Что мы узнали в этой главеВ программах, рассмотренных выше, не осталось ничего от привычных нам процедурных программ. При процедурном программировании контрольный метод сравнивает данные и вызывает функцию. При объектно-ориентированном программировании каждый отдельный объект служит для выполнения ограниченного набора четко определенных задач. Так, связанный список отвечает за поддержание головного узла: Головной узел немедленно передает данные по адресу своего указателя, не анализируя ни передаваемые данные, ни адресуемый объект. Хвостовой узел создает новые узлы и добавляет их в список, если они содержат данные. Хвостовому узлу известно, что если новые объекты содержат какие-то данные, то они должны располагаться в списке до него. Промежуточные узлы выполняют более сложные функции. Они обращаются к своим текущим объектам и сравнивают их значения со значениями новых объектов. В зависимости от результата сравнения, они либо вставляют объекты перед собой, либо передают их другому узлу. Обратите внимание, что промежуточные узлы сами по себе не имеют никакого представления о данных объектов и о том, как их сравнивать. Сравнение выполняется методами, вызываемыми объектами. Все, за что отвечает промежуточный узел, — это обращение к своему объекту с требованием вызвать метод сравнения текущего значения с новым переданным значением. В зависимости от того, какую константу возвратит метод сравнения, узел либо добавляет объекты перед собой, либо передает их другому узлу, не беспокоясь о том, что будет с этим объектом дальше. Кто же стоит над всем этим? В хорошо сконструированной объектно-ориентированной программе нет необходимости создавать какой-либо всеохватывающий объект контроля. Каждый объект выполняет свою маленькую партию, и результаты работы всех объектов сливаются в стройный хор. Классы массивовПо сравнению с использованием встроенных массивов написание собственного класса массивов дает ряд преимуществ. Так, можно разработать систему контроля за вводом данных в массив для предупреждения ошибок или создать класс массива, динамически изменяющий размер. При создании массив может содержать только один элемент, постепенно прирастая по мере выполнения программы. Можно разработать механизм сортировки или какого-либо другого упорядочения элементов массива либо использовать множество других эффективных вариантов массивов, наиболее популярны среди которых следующие: • отсортированные коллекции: каждый член массива автоматически занимает свое определенное место; • наборы: ни один из членов не повторяется в массиве дважды; • словари: связанные пары элементов массива, где один член выполняет роль ключа для возвращения второго члена; • разреженные массивы: допускается использование произвольных значений индексов, но в массив будут добавляться только реально существующие элементы. Так, можно ввести и использовать элемент с индексом SprseArray[5] или SpcseArray[2Q0], HO 3TO не значит, что в массиве реально существуют все элементы с меньшими индексами; • мультимножества: неупорядоченные коллекции, члены которых добавляются и возвращаются в произвольном порядке. Перегрузив оператор индексирования ([]), можно превратить связанный список в отсортированную коллекцию. Если добавить функцию отслеживания одинаковых членов, то отсортированная коллекция превратится в набор. Если все объекты списка связать попарно, то связанный список превратится в словарь или в разреженный массив. РезюмеСегодня вы узнали, как создавать массивы в C++. Массив представляет собой коллекцию объектов одинакового типа с фиксированным числом элементов. Массивы никак не контролируют свой размер. Поэтому вполне возможно в программе заносить данные за пределы массива, что часто является причиной ошибок. Отсчет индексов массива начинается с 0. Часто допускаемой ошибкой является указание индекса n для массива с размером n. Массивы могут быть одномерными или многомерными. Независимо от размерности, все массивы базовых типов (например, int) или массивы объектов классов с конструкторами, заданными по умолчанию, могут быть инициализированы при объявлении. Объекты массивов или все массивы целиком можно сохранять как в стековой области памяти, так и в области динамического обмена. Если удаляется объект из области динамической памяти, не забудьте установить квадратные скобки после ключевого слова delete[]. Имена массивов представляют собой константные указатели на первый элемент массива. Чтобы получить доступ к другим элементам, имена массивов можно использовать в математических операциях, как при работе с обычными указателями. Если размер коллекции объектов не известен во время компиляции программы, то для поддержания таких коллекций можно использовать связанные списки. Взяв связанный список за основу, можно разработать много других видов массивов и структур, автоматически выполняющих сложные операции. Строки представляют собой массивы символов. В C++ существуют дополнительные средства манипулирования текстовыми строками, включая возможность ввода в массив строки, взятой в двойные кавычки. Вопросы и ответыЧто произойдет, если в массив из 24-х членов вписать значение для 25-го элемента? Значение будет добавлено в ячейку памяти, не принадлежащую массиву, что может вызвать серьезную ошибку в работе программы. Что представляют собой элементы неинициализированного массива? Ячейки памяти, отведенные массиву но не инициализированные, могут содержать любую информацию, ранее сохраненную в этих ячейках. Результат обращения в программе к элементу массива, который не был инициализирован, не предсказуем. Можно ли создавать комбинации массивов? Да. Массив может содержать указатель на другой, более крупный массив. В случае работы со строками можно использовать некоторые стандартные функции, такие как strcat, чтобы создавать комбинации массивов символов. Чем связанные списки лучше массивов? Массивы всегда имеют фиксированный размер, тогда как размер связанного списка может изменяться динамически во время выполнения программы. Всегда ли нужно в классе строк использовать указатель char * для сохранения содержимого строки? Нет. Можно использовать любую область памяти, которая больше подходит для решения конкретных задач. КоллоквиумВ этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний и приводится несколько упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из предложенных ниже вопросов. Контрольные вопросы1. Как обратиться к первому и последнему элементам массива SomeArray[25]? 2. Как объявить многомерный массив? 3. Выполните инициализацию элементов многомерного массива, созданного при ответе на вопрос 2. 4. Сколько элементов содержит массив SomeArray[10][5][20]? 5. Каково максимальное число элементов, которые можно добавить в связанный список? 6. Можно ли в связанном списке использовать индексы? 7. Каким является последний символ в строке "Сергей — хороший парень"? Упражнения1. Объявите двухмерный массив, который представляет поле для игры в крестики и нолики. 2. Запишите программный код, инициализирующий значением 0 все элементы созданного перед этим массива. 3. Объявите класс узла Node, поддерживающего целые числа. 4. Жучки: что неправильно в следующей программе? unsigned short SomeArray[5][4]; for (int i = 0; i<4; i++) for (int j = 0; j<5; ]++) SomeArray[i][j] = i+j; 5. Жучки: что неправильно в следующей программе? unsigned short SomeArray[5][4]; for (int i = 0; i<=5; i++) for (int j = 0; j<=4; j++) SomeArray[i][j] = 0; День 13-й. ПолиморфизмНа прошлом занятии вы узнали, как создавать виртуальные функции в производных классах. На этом занятии речь пойдет об основном составляющем ядре полиморфизма — возможности во время выполнения программы связывать специфические объекты производных классов с указателями базового класса. Сегодня вы узнаете: • Что такое множественное наследование и как его использовать • Что представляет собой виртуальное наследование • Что такое абстрактные типы данных • Что такое чистые виртуальные функции Проблемы с одиночным наследованиемДавайте продолжим работу над программой о животных и предположим, что в ней теперь используется два класса, произведенных от какого-то общего класса. Один — Bird, посвященный птицам, а другой — Mammals, посвященный млекопитающим. Класс Bird содержит функцию-член Fly(), задающую возможность полета. Класс Mammals разбит на ряд подклассов, включая класс лошадей — Horse. Класс содержит две функции- члена — Whinny() и Gallop(), объявляющих ржание и бег галопом соответственно. Но внезапно у вас возникает желание создать новый весьма интересный мифический объект — крылатого Пегаса (Pegasus), который был бы чем-то вроде гибрида между Horse и Bird. Сразу предупредим, что, используя только одиночное наследование, вам сложно будет справиться с этой задачей. Если объявить объект Pegasus как член класса Bird, то для него станут недоступными функции Whinny() и Gallop(). Если Pegasus объявить как объект класса Horse, то ему станет недоступной функция Fly(). Первое решение может состоять в том, чтобы скопировать метод Fly() в класс Horse, после чего в этом классе создать объект Pegasus. При этом оба класса (Bird и Horse) будут содержать один и тот же метод Fly(), и при изменении метода в одном классе нужно будет не забыть внести соответствующие изменения в другом классе. Хорошо, если таких классов будет только два. Если вам придется вносить изменения в программу через некоторое время после ее создания, будет сложно вспомнить, в каких еще классах представлен этот метод. Когда вы захотите создать списки объектов классов Bird и Horse, перед вами возникнет еще одна проблема. Хотелось бы, чтобы объект Pegasus был представлен в обоих списках, но в данном случае это невозможно. Для решения возникшей проблемы можно использовать несколько подходов. Haпример, можно переименовать слишком "лошадиный" метод Gallop() в более обтекаемый Move(), после чего заместить этот метод в объекте Pegasus таким образом, чтобы он выполнял функцию метода Fly(). В других объектах класса Horse метод Move() будет выполняться так же, как раньше выполнялся метод Gallop(). Для объекта Pegasus можно даже определить, что короткие дистанции он должен преодолевать методом Gallop(), а длинные — методом Fly(): Pegasus::Move(long distance) { if (distance > veryFar) fly(distance); else gallop(distance); } Но и этот подход имеет ряд ограничений, поскольку объект уже не сможет летать на короткие дистанции и бегать на длинные. Может быть, все же просто перенести метод Fly() в класс Horse, как показано в листинге 13.1? Проблема состоит в том, что лошади, в большинстве своем, летать не умеют, поэтому во всех объектах этого класса, за исключением объекта Pegasus, данный метод не должен ничего выполнять. Листинг 13.1. Умеют ли лошади летать... 1: // Листинг 13.1. Умеют ли лошади летать... 2: // Фильтрация метода Fly() в классе Horse 3: 4: #include <iostream.h> 5: 6: class Horse 7: { 8: public: 9: void Gallop(){ cout << "Galloping...\n"; } 10: virtual void Fly() { cout << "Horses can't fly.\n"; } 11: private: 12: int itsAge; 13: }; 14: 15: class Pegasus : public Horse 16: { 17: public: 18: virtual void Fly() { cout << "I can fly! I can fly! I can fly!\n"; } 19: }; 20: 21: const int NumberHorses = 5; 22: int main() 23: { 24: Horse* Ranch[NumberHorses]; 25: Horse* pHorse; 26: int choice,i; 27: for (i=0; i<NumberHorses; i++) 28: { 29: cout << "(1)Horse (2)Pegasus: "; 30: cin >> choice; 31: if (choice == 2) 32: pHorse = new Pegasus; 33: else 34: pHorse = new Horse; 35: Ranch[i] = pHorse; 36: } 37: cout << "\n"; 38: for (i=0; i<NumberHorses; i++) 39: { 40: Ranch[i]->Fly(); 41: delete Ranch[i]; 42: } 43: return 0; 44: } Результат: (1)Horse (2)Pegasus; 1 (1)Horse (2)Pegasus: 2 (1)Horse (2)Pegasus: 1 (1)Horse (2)Pegasus: 2 (1)Horse (2)Pegasus: 1 Horses can't fly. I can fly! I can fly! I can fly! Horses can't fly. I can fly! I can fly! I can fly! Horses can't fly. Анализ: Безусловно, эта программа будет работать ценой добавления в класс Horse редко используемого метода Fly(). Это произошло в строке 10. Для объектов данного класса этот метод констатирует факт, что лошади летать не умеют. И только для объекта Pegasus метод замещается в строке 18 таким образом, что при вызове его объект заявляет, что умеет летать. В строке 24 используется массив указателей на объекты класса Horse, с помощью которого метод Fly() вызывается для разных объектов класса. В зависимости от того, для какого из объектов в данный момент вызывается метод, программа выводит на экран разные сообщения. Примечание:Показанный выше пример программы был значительно сокращен, чтобы выделить именно те моменты, которые сейчас рассматриваются. Так, для простоты программы из нее были удалены конструктор и виртуальные деструкторы. Перенос метода вверх по иерархии классовОчень часто для решения подобных проблем объявление метода переносят вверх по иерархическому списку классов, чтобы сделать его доступным большему числу производных классов. Но при этом есть угроза, что базовый класс уподобится кладовке, захламленной старыми вещами. Такой подход делает программу громоздкой и нарушает саму идею иерархии классов в C++, когда производные классы дополняют своими функциями небольшой набор общих функций базового класса. Противоречие состоит в том, что при переносе функции из производных классов вверх по иерархии в базовый класс трудно сохранить уникальность интерфейсов производных классов. Так, можно предположить, что у наших двух классов Bird и Horse есть базовый класс Animal, в котором собраны функции, общие для всех производных классов, например функция питания — Eat(). Перенеся метод Fly() в базовый класс, придется позаботиться о том, чтобы этот метод вызывался только в некоторых производных классах. Приведение указателя к типу производного классаПродолжая держаться за одиночное наследование, эту проблему можно решить таким образом, что метод Fly() будет вызываться только в случае, если указатель в данный момент связан с объектом Pegasus. Для этого необходимо иметь возможность обратиться к указателю и определить, на какой объект он указывает в текущий момент. Такой подход известен как RTTI (Runtime Type Identification — определение типа при выполнении). Но возможность выполнения RTTI была добавлена только в последние версии компиляторов C++. Если ваш компилятор не поддерживает RTTI, можете реализовать его собственными силами, добавив в программу метод, который возвращает перечисление типов каждого класса. Возвращенное значение можно анализировать во время выполнения программы и допускать вызов метода Fly() только в том случае, если возвращается значение Pegasus. Примечание:Не злоупотребляйте использованием RTTI в своих программах, так как этот подход рассматривается как аварийный и свидетельствует о том, что структура программы изначально была плохо продумана. Профессиональный программист предпочтет использование виртуальных функций, шаблонов или множественного наследования, речь о котором пойдет ниже в этой главе. Чтобы вызвать метод Fly(), необходимо во время выполнения изменить тип указателя, определив, что он связан не с объектом Horse, а с объектом производного класса Pegasus. Этот способ называют приведением вниз, поскольку объект базового класса Horse приводится к объекту производного класса Pegasus. Этот подход, хоть и с неохотой, теперь уже официально признан в C++, и для его реализации добавлен новый оператор — dynamic_cast. Если в программе создан указатель на объекты базового класса Horse и ему присвоен адрес объекта производного класса Pegasus, то такой указатель можно использовать полиморфно. Чтобы обратиться к методу производного класса, нужно динамически подменить указатель базового класса указателем производного класса с помощью оператора dynamic_cast. Во время выполнения программы происходит тестирование указателя базового класса. Если устанавливается, что текущий объект, на который ссылается указатель базового класса, в действительности является объектом производного класса, то с этим объектом связывается указатель производного класса. В противном случае указатель производного класса становится нулевым. Пример использования этого подхода показан в листинге 13.2. Листинг 13.2. Приведение вниз 1: // Листинг 13 2 Использование оператора dynamic_cast 2: // Использование rtti 3: 4: #include <iostream h> 5: enum TYPE { HORSE, PEGASUS }; 6: 7: class Horse 8: { 9: public 10: virtual void Gallop(){ cout << "Galloping...\n"; } 11: 12: private: 13: int itsAge; 14: }; 15: 16: class Pegasus : public Horse 17: { 18: public: 19: 20: virtual void Fly() { cout << "I can fly! I can fly! I can fly!\n"; } 21: }; 22: 23: const mt NumberHorse = 5, 24: int main() 25: { 26: Horse* Ranch[NumberHorse]; 27: Horse* pHorse; 28: int choice,i, 29: for (i=0; KNumberHorse, i++) 30: { 31: cout << "(1)Horse (2)Pegasus: "; 32: cin >> choice; 33: if (choice == 2) 34: pHorse = new fegasus; 35: else 36: pHorse = new Horse; 37: Rancfi[i] = pHorse, 38: } 39: cout << "\n"; 40: for (i=0; a<NumberHorses; i++) 41: { 42: Pegasus *pPeg = dynamic_cast< Pegasus *> (Ranch[i]); 43: if (pPeg) 43: pPeg->Fly(); 44: else 45: cout << "Just a horse\n"; 46: 47: delete Ranch[i]; 48: } 49: return 0; 50: } Результат: (1)Horse (2)Pegasus: 1 (1)Horse (2)Pegasus: 2 (1)Horse (2)Pegasus: 1 (1)Horse (2)Pegasus: 2 (1)Horse (2)Pegasus: 1 Just a horse I can fly! I can fly! I can fly! Just a horse I can fly! I can fly! I can fly! Just a horse Вопросы и ответы Во время компиляции появляется сообщение об ошибке C4541: 'dynamic_cast' used on polymorphic type 'class Horse' with/GR-; unpredictable behavior may result Как поступить? Это сообщение MFC действительно может смутить начинающего программиста. Чтобы устранить ошибку, выполните ряд действий. 1. Выберите в окне проекта команду Project->Settings. 2. Перейдите к вкладке С/C++. 3. Выберите в раскрывающемся списке Category опцию C++ Language 4. Установите Enable Runtime Type Information (RTTI). 5. Повторно скомпилируйте весь проект. Анализ: Этот пример программы также будет вполне работоспособным. Метод Fly() не связан напрямую с классом Horse и не будет вызываться для обычных объектов этого класса. Он выполняется только для объектов класса Pegasus, но для этого программе приходится каждый раз анализировать, с каким объектом связан указатель, и приводить текущий указатель к типу производного класса. Все же следует признать, что программы с приведением типа объектов выглядят несколько неуклюже и в них легко запутаться. Кроме того, данный подход идет в разрез с основной идеей полиморфизма виртуальных функций, поскольку выполняемость метода теперь зависит от приведения типа объекта во время выполнения программы. Добавление объекта в два спискаДругая проблема состоит в том, что при объявлении Pegasus как объекта типа Horse становится невозможным добавить его в список объектов класса Bi rd. Приходилось то переносить функцию Fly() вверх по иерархии классов, то выполнять приведение указателя, но так и не удалось в полной мере достичь необходимого функционирования программы. Таким образом, придерживаясь только одиночного наследования, оказалось невозможным элегантно решить эту проблему. Можно перенести все три функции — Fly(), Whinny() и Gallop() — в базовый класс Animal, общий для двух производных классов Bird и Horse. В результате вместо двух списков объектов для классов Bird и Horse получится один общий список объектов класса Animal. Недостаток метода состоит в том, что базовый класс принимает на себя слишком много функций. В качестве альтернативы можно оставить методы там, где они есть, и заняться приведением типов объектов классов Horse, Bird и Pegasus, но результат в конечном итоге будет еще хуже! Рекомендуется:Переносите вверх по иерархии классов функции общего использования. Избегайте использовать коды, основанные на определении типов объектов во время выполнения программы. Вместо этого используйте виртуальные методы, шаблоны и множественное наследование. Не рекомендуется:Не переносите вверх по иерархии классов интерфейсы производных классов. Множественное наследованиеСуществует возможность производить новые классы более чем от одного базового класса. Такой процесс называется множественным наследованием. Чтобы произвести подобный класс, базовые классы в объявлении разделяются запятыми. В листинге 13.3 класс Pegasus объявлен таким образом, что наследует свойства двух базовых классов — Bird и Horse. Затем программа добавляет объект Pegasus в списки объектов обоих классов. Листинг 13.3. Множественное наследование 1: // Листинг 13.3. Множественное наследование. 2: // Множественное наследование 3: 4: #include <iostream.h> 5: 6: class Horse 7: { 8: public: 9: Horse() { cout << "Horse constructor... "; } 10: virtual ~Horse() { cout << "Horse destructor... "; } 11: virtual void Whinny() const { cout << "Whinny!... "; } 12: private: 13: int itsAge; 14: }; 15: 16: class Bird 17: { 18: public: 19: Bird() { cout << "Bird constructor... "; } 20: virtual ~Bird() { cout << "Bird destructor... "; } 21: virtual void Chirp() const { cout << "Chirp... "; } 22: virtual void Fly() const 23: { 24: cout << "I can fly! I can fly! I can fly! "; 25: } 26: private: 27: int itsWeight; 28: }; 29: 30: class Pegasus : public Horse, public Bird 31: { 32: public: 33: void Chirp() const { Whinny(); } 34: Pegasus() { cout << "Pegasus constructor... "; } 35: ~Pegasus() { cout << "Pegasus destructor... "; } 36: }; 37: 38: const int MagicNumber = 2; 39: int main() 40: { 41: Horse* Ranch[MagicNumber]; 42: Bird* Aviary[MagicNumber]; 43: Horse * pHorse; 44: Bird * pBird; 45: int choice,i; 46: for (i=0; i<MagicNumber; i++) 47: { 48: cout << "\n(1)Horse (2)Pegasus: "; 49: cin >> choice; 50: if (choice == 2) 51: pHorse = new Pegasus; 52: else 53: pHorse = new Horse; 54: Ranch[i] = pHorse; 55: } 56: for (i=0; i<MagicNumber; i++) 57: { 58: cout << "\n(1)Bird (2)Pegasus: "; 59: cin >> choice; 60: if (choice == 2) 61: pBird = new Pegasus; 62: else 63: pBird = new Bird; 64: Aviary[i] = pBird; 65: } 66: 67: cout << "\n"; 68: for (i=0; i<MagicNumber; i++) 69: { 70: cout << "\nRanch[" << i << "]: 71: Ranch[i]->Whinny(); 72: delete Ranch[i]; 73: } 74: 75: for (i=0; i<MagicNumber; i++) 76: { 77: cout << "\nAviary[" << i << "] 78: Aviary[i]->Chirp(); 79: Aviary[i]->Fly(); 80: delete Aviary[i]; 81: } 82: return 0; 83: } Результат: (1)Horse (2)Pegasus: 1 Horse constructor... (1)Horse (2)Pegasus: 2 Horse constructor... Bird constructor... Pegasus constructor... (1)Bird (2)Pegasus: 1 Bird constructor... (1)6ird (2)Pegasus: 2 Horse constructor... Bird constructor... Pegasus constructor... Ranch[0]: Whinny!... Horse destructor... Ranch[1]: Whinny!... Pegasus destructor... Bird destructor... Horse destructor... Aviary[0]: Chirp... I can fly! I can fly! I can fly! Bird destructor... Aviary[1]: Whinny!... I can fly! I can fly! I can fly! Pegasus destructor... Bird destructor... Horse destructor... Анализ: В строках 6—14 объявляется класс Horse. Конструктор и деструктор выводят на экран сообщения о своей работе, а метод Whinny() печатает Whinny! (И-го-го). Класс Bird объявляется в строках 16—28. В дополнение к своим конструктору и деструктору этот класс содержит два метода: Chirp() и Fly(), каждый из которых выводит на экран соответствующие сообщения. В реальных программах эти методы могут воспроизводить определенный звуковой файл или управлять анимационными эффектами на экране. Наконец, в строках 30-36 объявляется класс Pegasus. Он производится сразу от двух базовых классов — Bird и Horse. В классе замешается метод Chirp() таким образом, что вызывается метод Whinny(), который унаследован этим классом от класса Horse. Создается два списка: Ranch (конюшня), который в строке 41 связывается с классом Horse, и Aviary (птичник), который в строке 42 связывается с классом Bird. В строках 46—55 в список Ranch добавляются два объекта — Horse и Pegasus. В строках 56—65 в список Aviary добавляются объекты Bird и Pegasus. Вызовы виртуальных методов с помощью указателей классов Bird и Horse одинаково выполняются для объекта Pegasus. Например, в строке 78 метод Chirp() вызывается последовательно для всех объектов, указатели на которые представлены в массиве Aviary. Поскольку этот метод объявлен в классе Bird как виртуальный, он правильно Выполняется для всех объектов списка. По выводимым на экран строкам можно заключить, что при создании объекта Pegasus вызываются конструкторы всех трех классов — Bird, Horse и Pegasus, каждый из которых создает свою часть объекта. При удалении объекта также удаляются его части, относящиеся к классам Bird и Horse, для чего деструкторы в этих классах объявлены как виртуальные. Объявление множественного наследования Чтобы указать, что создаваемый объект наследует свойства более чем одного базового класса после имени создаваемого класса ставится двоеточие, вслед за которым через запятую перечислены имена всех базовых классов. Пример 1: class Pegasus : public Horse, public Bird Пример 2: class Schnoodle : public Schnauzer, public Poodle Из каких частей состоят объекты, полученные в результате множественного наследованияКогда в памяти компьютера создается объект Pegasus, конструкторы обоих классов принимают участие в его построении, как показано на рис. 13.1. Рис. 13.1. Объект, полученный в результате множественного наследования В случае использования множественного наследования возникает ряд непростых и весьма интересных вопросов. Например, что произойдет, если оба базовых класса будут иметь одно и то же имя либо содержать виртуальные функции или данные с одинаковыми именами? Как инициализируются конструкторы разных базовых классов? Что произойдет, если два базовых класса будут произведены от одного и того же родительского класса? Все эти вопросы будут рассмотрены в следующем разделе, после чего можно переходить к практическому использованию множественного наследования. Конструкторы классов, полученных в результате множественного наследованияЕсли класс Pegasus производится от двух базовых классов — Bird и Horse, а в каждом из них объявлены конструкторы со списками параметров, то класс Pegasus инициализирует эти конструкторы. Как это происходит, показано в листинге 13.4. Листинг 13.4. Создание объектов при множественном наследовании 1: // Листинг 13.4. 2: // Создание обьектов при множественном наследовании 3: #include <iostream.h> 4: typedef int HANDS; 5: enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown }; 6: 7: class Horse 8: { 9: public: 10: Horse(COLOR color, HANDS height); 11: virtual ~Horse() { cout << "Horse destructor...\n"; } 12: virtual void Whinny()const { cout << "Whinny!... "; } 13: virtual HANDS GetHeight() const { return itsHeight; } 14: virtual COLOR GetColor() const { return itsColor; } 15: private: 16: HANDS itsHeight; 17: COLOR itsColor; 18: }; 19: 20: Horse::Horse(COLOR color, HANDSheight): 21: itsColor(color),itsHeight(height) 22: { 23: cout << "Horse constructor...\n"; 24: } 25: 26: class Bird 27: { 28: public: 29: Bird(COLOR color, bool migrates); 30: virtual ~Bird() { cout << "Bird destructor...\n"; } 31: virtual void Chirp()const { cout << "Chirp... "; } 32: virtual void Fly()const 33: { 34: cout << "I can fly! I can fly! I can fly! "; 35: } 36: virtual COLOR GetColor()const { return itsColor; } 37: virtual bool GetMigration() const { return itsMigration; } 38: 39: private: 40: COLOR itsColor; 41: bool itsMigration; 42: }; 43: 44: Bird::Bird(COLOR color, bool migrates): 45: itsColor(color), itsMigration(migrates) 46: { 47: cout << "Bird constructor...\n"; 48: } 49: 50: class Pegasus : public Horse, public Bird 51: { 52: public: 53: void Chirp()const { Whinny(); } 54: Pegasus(COLOR, HANDS, bool,long); 55: ~Pegasus() { cout << "Pegasus destructor...\n";} 56: virtual long GetNumberBelievers() const 57: { 58: return itsNumberBelievers; 59: } 60: 61: private: 62: long itsNumberBelievers; 63: }; 64: 65: Pegasus::Pegasus( 66: COLOR aColor, 67: HANDS height, 68: bool migrates, 69: long NumBelieve): 70: Horse(aColor, height), 71: Bird(aColor, migrates), 72: itsNumberBelievers(NumBelieve) 73: { 74: cout << "Pegasus constructor...\n"; 75: } 76: 77: int main() 78: { 79: Pegasus *pPeg = new Pegasus(Red, 5, true, 10); 80: pPeg->Fly(); 81: pPeg->Whinny(); 82: cout << "\nYour Pegasus is " << pPeg->GetHeight(); 83: cout << " hands tall and "; 84: if (pPeg->GetMigration()) 85: cout << "it does migrate."; 86: else 87: cout << "it does not migrate."; 88: cout << "\nA total of " << pPeg->GetNumberBelievers(); 89: cout << " people believe it exists.\n"; 90: delete pPeg; 91: return 0; 92: } Результат: Horse constructor... Bird constructor... Pegasus constructor... I can fly! I can fly! I can fly! Whinny!... Your Pegasus is 5 hands tall and it does migrate. A total of 10 people believe it exists. Pegasus destructor... Bird destructor... Horse destructor... Анализ: Класс Horse объявляется в строках 7—18. Конструктор этого класса принимает два параметра: один из них — это перечисление, объявленное в строке 5, а второй — новый тип, объявленный с помощью typedef в строке 4. Этот конструктор выполняется в строках 20—24. При этом инициализируется одна переменная-член и на экран выводится сообщение о работе конструктора класса Horse. В строках 26—42 объявляется класс Bird, конструктор которого выполняется в строках 45—49. Конструктор этого класса также принимает два параметра. Обратите внимание на интересный факт: конструкторы обоих классов принимают перечисления цветов, с помощью которых в программе можно установить цвет лошади или цвет перьев у птицы. В результате, когда вы попытаетесь установить цвет Пегаса, может возникнуть проблема в работе программы, которая обсуждается несколько ниже. Класс Pegasus объявляется в строках 50—63, а его конструктор — в строках 65—75. Инициализация объекта Pegasus выполняется тремя строками программы. Сначала конструктор класса Horse определяет цвет и рост. Затем конструктор класса Bird инициализируется цветом перьев и логической переменной. Наконец, происходит инициализация переменной-члена itsNumberBelievers, относящейся к классу Pegasus. После всех этих операций вызывается конструктор класса Pegasus. В функции main() создается указатель на класс Pegasus, который используется для получения доступа к функциям-членам базовых объектов. Двусмысленность ситуацииВ листинге 13.4 оба класса — Horse и Bird — имеют метод GetColor(). В программе может потребоваться возвратить цвет объекта Pegasus, но возникает вопрос: какой из двух унаследованных методов при этом будет использоваться? Ведь методы, объявленные в обоих базовых классах, имеют одинаковые имена и сигнатуры. В результате при компилировании программы возникнет неопределенность, которую необходимо разрешить до компиляции. Если просто записать: COLOR currentColor = pPeg->GetColor(); Компилятор покажет сообщение об ошибке Member is ambiguous: ' Horse::GetColor' and ' Bird::GetColor' (Член не определен). Эту неопределенность можно разрешить, явно обратившись к методу того класса, который вам необходим: COLOR currentColor = pPeg->Horse::GetColor(); В любом случае при возникновении подобной ситуации, когда требуется сделать выбор между одноименными методами или переменными-членами разных классов, следует явно указывать имя необходимого базового класса перед именем функции- члена или переменной. Если в классе Pegasus эта функция будет замещена, то проблема решится сама собой, так как в этом случае вызывается функция-член класса Pegasus: virtual COLOR GetColor()const { return Horse::GetColor(); } Таким образом, проблему неопределенности можно обойти благодаря инкапсуляции явного указания базового класса в объявлении замещенной функции. Если возникнет необходимость использовать метод другого класса, то обращение к нему с помощью приведенного ниже выражения не будет ошибкой. COLOR currentColor = pPeg->Bird::GetColor(); Наследование от общего базового классаЧто произойдет, если оба базовых класса, от которых производится другой класс, сами были произведены от одного общего базового класса, как, например, классы Bird и Horse от класса Animal. Эта ситуация показана на рис. 13.2. Рис. 13.2. Общий базовый класс Как показано на рис. 13.2, два класса, являющихся базовыми для класса Pegasus, сами производятся от одного общего класса Animal. Компилятор при этом рассматривает классы Bird и Horse как производные от двух одноименных базовых классов, что может привести к очередной неопределенности. Например, если в классе Animal объявлены переменная-член itsAge и функция-член GetAge(), а в программе делается вызов pGet->GetAge(), то будет ли при этом вызываться функция GetAge(), унаследованная классом Bird от класса Animal или классом Horse от базового класса? Это противоречие разрешается в листинге 13.5. Листинг 13.5. Общий базовый класс 1: // Листинг 13.5. 2: // Общий базовый класс 3: #include <iostream.h> 4: 5: typedef int HANDS; 6: enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown } 7: 8: class Animal // общий базовый класс для классов horse и bird 9: { 10: public: 11: Animal(int); 12: virtual ~Animal() { cout << "Animal destructor...\n"; } 13: virtual int GetAge() const { return itsAge; } 14: virtual void SetAge(int age) { itsAge = age; } 15: private: 16: int itsAge; 17: }; 18: 19: Animal::Animal(int age): 20: itsAge(age) 21: { 22: cout << "Animal constructor...\n"; 23: } 24: 25: class Horse : public Animal 26: { 27: public: 28: Horse(COLOR color, HANDS height, int age); 29: virtual ~Horse() { cout << "Horse destructor...\n"; } 30: virtual void Whinny()const { cout << "Whinny!... "; } 31: virtual HANOS GetHeight() const { return itsHeight; } 32: virtual COLOR GetColor() const { return itsColor; } 33: protected: 34: HANDS itsHeight; 35: COLOR itsColor; 36: }; 37: 38: Horse::Horse(C0L0R color, HANDS height, int age): 39: Animal(age), 40: itsColor(color),itsHeight(height) 41: { 42: cout << "Horse constructor...\n"; 43: } 44: 45: class Bird : public Animal 46: { 47: public: 48: Bird(COLOR color, bool migrates, int age); 49: virtual ~Bird() { cout << "Bird destructor...\n"; } 50: virtual void Chirp()const { cout << "Chirp... "; } 51: virtual void Fly()const 52: { cout << "I can fly! I can fly! I can fly! "; } 53: virtual C0L0R GetColor()const { return itsColor; } 54: virtual bool GetMigration() const { return itsMigration; } 55: protected: 56: COLOR itsColor; 57: bool itsMigration; 58: }; 59: 60: Bird::Bird(COLOR color, bool migrates, int age): 61: Animal(age), 62: itsColor(color), itsMigration(migrates) 63: { 64: cout << "Bird constructor...\n"; 65: } 66: 67: class Pegasus : public Horse, public Bird 68: { 69: public: 70: void Chirp()const { Whinny(); } 71: Pegasus(COLOR, HANDS, bool, long, int); 72: virtual ~Pegasus() { cout << "Pegasus destructor...\n";} 73: virtual long GetNumberBelievers() const 74: { return itsNumberBelievers; } 75: virtual COLOR GetColor()const { return Horse::itsColor; } 76: virtual int GetAge() const { return Horse::GetAge(); } 77: private: 78: long itsNumberBelievers; 79: }; 80: 81: Pegasus::Pegasus( 82: COLOR aColor, 83: HANDS height, 84: bool migrates, 85: long NumBelieve, 86: int age): 87: Horse(aColor, height,age), 88: Bird(aColor, migrates,age), 89: itsNumberBelievers(NumBelieve) 90: { 91: cout << "Pegasus constructor...\n"; 92: } 93: 94: int main() 95: { 96: Pegasus *pPeg = new Pegasus(Red. 5, true, 10, 2); 97: int age = pPeg->GetAge(); 98: cout << "This pegasus is " << age << " years old.\n"; 99: delete pPeg; 100: return 0; 101: } Результат: Animal constructor... Horse constructor... Animal constructor... Bird constructor... Pegasus constructor... This pegasus is 2 years old. Pegasus destructor.,. Bird destructor... Animal destructor... Horse destructor... Animal destructor... Анализ: В листинге содержится ряд интересных решений. Так, в строках 8—17 объявляется новый класс Animal с переменной-членом itsAge и двумя методами — GetAge() и SetAge(). В строке 25 класс Horse производится от класса Animal. Конструктор класса Horse теперь имеет третий параметр age, который передается в базовый класс Animal. Обратите внимание, что в классе Horse метод GetAge() не замещается, а просто наследуется. В строке 46 класс Bird производится от класса Animal. Конструктор этого класса также содержит параметр age, с помощью которого инициализируется базовый класс Animal. Метод GetAge() также наследуется этим классом без замещения. Класс Pegasus производится от двух базовых классов Horse и Bird, поэтому с исходным базовым классом Animal он связан двумя линиями наследования. Если для объекта класса Animal будет вызван метод GetAge(), то для преодоления неопределенности нужно точно указать, к какому базовому классу следует обращаться за этим методом, либо метод GetAge() следует заместить в классе Pegasus. В нашем примере программы метод GetAge() замещается для класса Pegasus таким образом, что в нем явно указывается обращение к аналогичному методу конкретного базового класса. Замещение функции с добавлением обращения к методу базового класса позволяет решить две проблемы. Во-первых, преодолевается неопределенность обращения к базовым классам; во-вторых, функцию можно заместить таким образом, что в производном классе при обращении к этой функции будут выполняться дополнительные операции, которых не было в базовом классе. Причем по желанию программиста эти дополнительные операции могут выполняться до вызова функции базового класса или после вызова с использованием значения, возвращенного функцией базового класса. Конструктор класса Pegasus принимает пять параметров: цвет крылатого коня, его рост (в футах); логическую переменную, которая определяет, мигрирует сейчас это животное или мирно пасется на пастбище; число людей, верящих в существование Пегаса, и возраст животного. В строке 87 конструктор инициализирует переменные, определенные в классе Horse (цвет, рост и возраст). В следующей строке инициализируется часть, относящаяся к классу Bird: цвет, миграции и возраст. Наконец, в строке 89 инициализируется переменная itsNumberBelievers, относящаяся непосредственно к классу Pegasus. Вызов конструктора класса Horse в строке 87 выполняет операторы, записанные в строке 38. С помощью параметра age конструктор класса Horse инициализирует переменную itsAge, унаследованную классом Horse от класса Animal. Затем инициализируются две переменные-члена класса Horse — itsColor и itsHeight. Вызов конструктора класса Bird в строке 88 выполняет операторы, записанные в строке 60. И в данном случае параметр age используется для инициализации переменной-члена, унаследованной классом Bird от класса Animal. Обратите внимание, что значение параметра цвета объекта Pegasus используется для инициализации соответствующих переменных-членов обоих классов, Bird и Horse. Параметр age также инициализирует переменную itsAge обоих этих классов, унаследованную ими от базового класса Animal. Виртуальное наследованиеВ листинге 13.5 решалась проблема неопределенности, а именно: от какого базового класса унаследована функция getAge() в объекте класса Pegasus. Но в действительности этот метод производится от одного общего базового класса Animal. В C++ существует возможность указать, что мы имеем дело не с двумя одноименными классами, как показано в рис. 13.2, а с одним общим базовым классом (рис. 13.3). Рис. 13.3. Виртуальное наследование Для этого класс Animal нужно объявить как виртуальный базовый класс для двух производных классов, Horse и Bird. Класс Animal при этом не подвергается никаким изменениям. В классах Horse и Bird изменения состоят в том, что в их объявлении указывается виртуальность наследования от базового класса Animal. Класс Pegasus изменяется существенно. Обычно конструктор класса инициализирует только собственные переменные и переменные-члены базового класса. Из этого правила делается исключение, если используется виртуальное наследование. Переменные основного базового класса инициализируются конструкторами не следующих производных от него классов, а тех, которые являются последними в иерархии классов. Поэтому класс Animal инициализируется не конструкторами классов Horse и Bird, а конструктором класса Pegasus. Конструкторы классов Horse и Bird также содержат команды инициализации базового класса Animal, но при создании объекта Pegasus эта инициализация перекрывается конструктором данного класса. Листинг 13.6 представляет собой программный код из листинга 13.5, переписанный таким образом, чтобы можно было воспользоваться преимуществами виртуального наследования. Листинг. 13.6. Пример использования виртуального наследования 1: // Листинг 13.6. 2: // Виртуальное наследование 3: #include <iostream.h> 4: 5: typedef int HANDS; 6: enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown } ; 7: 8: class Animal // общий базовый класс для двух производных классов horse и bird 9: { 10: public: 11: Animal(int); 12: virtual ~Animal() { cout << "Animal destructor...\n"; } 13: virtual int GetAge() const { return itsAge; } 14: virtual void SetAge(int age) { itsAge = age; ) 15: private: 16: int itsAge; 17: }; 18: 19: Animal::Animal(int age): 20: itsAge(age) 21: { 22: cout << "Animal constructor...\n"; 23: } 24: 25: class Horse : virtual public Animal 26: { 27: public: 28: Horse(C0L0R color, HANDS height, int age); 29: virtual ^Horse() { cout << "Horse destructor...\n"; } 30: virtual void Whinny()const { cout << "Whinny!... "; } 31: virtual HANDS GetHeight() const { return itsHeight; } 32: virtual COLOR GetColor() const { return itsColor; } 33: protected: 34: HANDS itsHeight; 35: COLOR itsColor; 36: }; 37: 38: Horse::Horse(C0L0R color, HANDS height, intage): 39: Animal(age), 40: itsColor(color),itsHeight(height) 41: { 42: cout << "Horse constructor...\n"; 43: } 44: 45: class Bird : virtual public Animal 46: { 47: public: 48: Bird(COLOR color, bool migrates, int age); 49: virtual ~Bird() { cout << "Bird destructor...\n"; } 50: virtual void Chirp()const { cout << "Chirp... "; } 51: virtual void Fly()const 52: { cout << "I can fly! I can fly! I can fly! "; } 53: virtual COLOR GetColor()const { return itsColor; } 54: virtual bool GetMigration() const { return itsMigration; } 55: protected: 56: COLOR itsColor; 57: bool itsMigration; 58: }; 59: 60: Bird;:Bird(COLOR color, bool migrates, int age): 61: Animal(age), 62: itsColor(color), itsMigration(migrates) 63: { 64: cout << "Bird constructor...\n"; 65: } 66: 67: class Pegasus : public Horse, public Bird 68: { 69: public: 70: void Chirp()const { Whinny(); } 71: Pegasus(COLOR, HANDS, bool, long, int); 72: virtual ~Pegasus() { cout << "Pegasus destructor...\n";} 73: virtual long GetNumberBelievers() const 74: { return itsNumberBelievers; } 75: virtual COLOR GetColor()const { return Horse::itsColor; } 76: private: 77: long itsNumberBelievers; 78: }; 79: 80: Pegasus::Pegasus( 81: COLOR aColor, 82: HANDS heigbt, 83: bool migrates, 84: long NumBelieve, 85: int age): 86: Horse(aColor, height,age), 87: Bird(aColor, migrates,age), 88: Animal(age*2), 89: itsNumberBelievers(NumBelieve) 90: { 91: cout << "Pegasus constructor...\n"; 92: } 93: 94: int main() 95: { 96: Pegasus *pPeg = new Pegasus(Red, 5, true, 10, 2); 97: int age = pPeg->GetAge(); 98: cout << "This pegasus is " << age << " years old.\n"; 99: delete pPeg: 100: return 0; 101: } Результат: Animal constructor... Horse constructor... Bird constructor. . . Pegasus constructor... Tnis pegasus is 4 years old. Pegasus destructor... Bird destructor... Horse destructor... Animal destructor... Анализ: В строке 25 класс Horse виртуально наследуется от класса Animal, а в строке 45 так же наследуется класс Bird. Обратите внимание, что конструкторы обоих классов по-прежнему инициализируют класс Animal. Но как только создается объект Pegasus, конструктор этого класса заново инициализирует класс Animal, отменяя прежние инициализации. Убедиться в этом вы можете по результату, выводимому программой на экран. При первой инициализации переменной itsAge присваивается значение 2, но конструктор класса Pegasus удваивает это значение. В результате строка 98 программы выводит на экран значение 4. Проблемы с неопределенностью наследования метода в классе Pegasus больше не возникает, поскольку теперь метод GetAge() наследуется непосредственно из класса Animal. В то же время при обращении к методу GetColor() по-прежнему необходимо явно указывать базовый класс, так как этот метод объявлен в обоих классах, Horse и Bird. Проблемы с множественным наследованиемХотя множественное наследование дает ряд преимуществ по сравнение с одиночным, многие программисты с неохотой используют его. Основная проблема состоит в том, что многие компиляторы C++ все еще не поддерживают множественное наследование; это осложняет отладку программы, тем более что все возможности, реализуемые этим методом, можно получить и без него. Действительно, если вы решите использовать в своей программе множественное наследование, следует учесть, что с отладкой программы могут возникнуть проблемы и чрезмерное усложнение программы, связанное с использованием этого подхода, не всегда оправдывается полученным эффектом. Указание виртуального наследования при объявлении класса Чтобы быть уверенным, что производные классы будут рассматривать исходный базовый класс как единый источник, виртуальность наследования следует указать во всех промежуточных классах. Пример 1: classHorse : virtual public Animal class Bird : virtual public Animal '. class Pegasus: public Horse,public Bird Пример 2: class Schnauzer : virtual public 0og class Poodle ; virtual public 0og class Schnoodle : public Schnauzer, publiс Poodle Рекомендуется:Используйте множественное наследование в тех случаях, когда в классе необходимо применять данные и методы, объявленные в разных классах. Используйте виртуальное наследование, чтобы как можно элегантнее обойти проблемы с неопределенностью источника наследования метода или данных. Инициализируйте исходный базовый класс конструктором класса, наиболее удаленного от базового по иерархии классов. Не рекоменддется:Не используйте множественное наследование в тех случаях, когда можно обойтись одиночным наследованием. Классы-мандатыПромежуточным решением между одиночным и множественным наследованием классов может быть использование классов-мандатов. Так, класс Horse можно произвести от двух базовых классов — Animal и Displayable, причем последний добавляет только некоторые методы отображения объектов на экране. Классом-мандатом называется класс, открывающий доступ к ряду методов, но не содержащий никаких данных (или, по крайней мере, содержащий минимальный набор данных). Методы класса-мандата передаются в производные классы с помощью обычного наследования. Единственное отличие классов-мандатов от других классов состоит в том, что они практически не содержат никаких данных. Различие довольно субъективное и отражает только общую тенденцию программирования, сводящуюся к тому, что добавление функциональности классам не должно сопровождаться усложнением программы. Использование классов-мандатов также снижает вероятность возникновения неопределенностей при использовании в производном классе данных, унаследованных из других базовых классов. Например, предположим, что класс Horse производится от двух классов — Animal и Displayable, причем последний добавляет только новые методы, но не содержит данных. В таком случае все наследуемые данные класса Horse происходят только от одного базового класса Animal, а методы наследуются от обоих классов. Классы-мандаты (capability class) иногда еще называют миксинами (mixin). Этот термин произошел от названия десерта, представляющего собой смесь пирожного с мороженым, политую сверху шоколадной глазурью. Этот десерт продавался в супермаркетах Sommerville в штате Массачусетс. Видимо, это блюдо когда-то попробовал один из программистов, занимающийся разработкой средств объектно-ориентированного программирования для языка SCOOPS, где этот термин впервые появился. Абстрактные типы данныхВ объектном программировании довольно часто создаются иерархии логически связанных классов. Например, представим класс Shape, от которого произведены классы Rectangle и Circle. Затем от класса Rectangle производится класс Sguare, как частный вид прямоугольника. В каждом из производных классов замещаются методы Draw(), GetArea() и др. Основной костяк программы с классом Shape и производными от него Rectangle и Circle показан в листинге 13.7. Листинг 13.7. Классы семейства Shape 1: // Листинг 13.7. Классы семейства Shape 2: 3: #include <iostream.h> 4: 5: 6: class Shape 7: { 8: public: 9: Shape(){ } 10: virtual ~Shape() { } 11: virtual long GetArea() { return -1; } 12: virtual long GetPerim() { return -1; } 13: virtual void Draw() { } 14: private: 15: }; 16: 17: class Circle : public Shape 18: { 19: public: 20: Circle(int radius):itsRadius(radius) { } 21: ~Circle() { } 22: long GetArea() { return 3 * itsRadius * itsRadius; } 23: long GetPerim() { return 6 * itsRadius; } 24: void Draw(); 25: private: 26: int itsRadius; 27: int itsCircumference; 28: }; 29: 30: void Circle::Draw() 31: { 32: cout << "Circle drawing routine here!\n"; 33: } 34: 35: 36: class Rectangle : public Shape 37: { 38: public: 39: Rectangle(int len, int width); 40: itsLength(len), itsWidth(width) { } 41: virtual ~Rectangle() { } 42: virtual long GetArea() { return itsLength * itsWidth; } 43: virtual long GetPerim() { return 2*itsLength + 2*itsWidth; } 44: virtual int GetLength() { return itsLength; } 45: virtual int GetWidth() { return itsWidth; } 46: virtual void Draw(); 47: private: 48: int itsWidth; 49: int itsLength; 50: }; 51: 52: void Rectangle::Draw() 53: { 54: for (int i = 0; i<itsLength; i++) 55: { 56: for (int j = 0; j<itsWidth; j++) 57: cout << "x "; 58: 59: cout << "\n"; 60: } 61: } 62: 63: class Square : public Rectangle 64: { 65: public: 66: Square(int len); 67: Square(int len, int width); 68: ~Square() { } 69: long GetPerim() { return 4 * GetLength();} 70: }; 71: 72: Square::Square(int len): 73: Rectangle(len,len) 74: { } 75: 76: Square::Square(int len, int width): 77: Rectangle(len,width) 78: 79: { 80: if (GetLength() != GetWidth()) 81: cout << "Error, not a sguare... a Rectangle??\n"; 82: } 83: 84: int main() 85: { 86: int choice; 87: bool fQuit = false; 88: Shape * sp; 89: 90: while ( ! fQuit ) 91: { 92: cout << "(1)Circle (2)Rectangle (3)Square (0)Quit:"; 93: cin >> choice; 94: 95: switch (choice) 96: { 97: case 0: fQuit = true; 98: break; 99: case 1: sp = new Circle(5); 100: break; 101: case 2: sp = new Rectangle(4,6); 102: break; 103: case 3: sp = new Square(5); 104: break; 105: default: cout << "Please enter a number between 0 and 3" << endl; 106: continue; 107: break; 108: } 109: if(! fQuit) 110: sp->Draw(); 111: delete sp; 112: cout << "\n"; 113: } 114: return 0; 115: } Результат: (1)Circle (2)Rectangle (3)Square (0)Quit: 2 x x x x x x X X X X X X X X X X X X X X X X X X (1)Circle (2)Rectangle (3)Square (0)Quit:3 X X X X X X X X X x X X X X X X X X X X X X X X X (1)Circle (2)Rectangle (3)Square (0)Quit:0 Анализ: В строках 6—15 объявляется класс Shape. Методы GetArea() и GetPerim() возвращают -1 как сообщение об ошибке, а метод Draw() не выполняет никаких действий. Давайте подумаем, можно ли в принципе нарисовать форму? Можно нарисовать окружность, прямоугольник или квадрат, но форма — это абстракция, которую невозможно изобразить. Класс Circle производится от класса Shape, и в нем замещаются три виртуальных метода. Обратите внимание, что в данном случае нет необходимости использовать ключевое слово virtual, поскольку виртуальность функций наследуется в производном классе. Тем не менее для напоминания о виртуальности используемых функций не лишним будет явно указать это. Класс Square производится от класса Rectangle и наследует от него все методы, причем метод GetPerim() замещается в новом классе. Все методы должны функционировать нормально в производных классах, но не в базовом классе Shape, поскольку невозможно создать экземпляр формы как таковой. Программа должна быть защищена от попытки пользователя создать объект этого класса. Класс Shape существует только для того, чтобы поддерживать интерфейс, общий для всех производных классов, поэтому об этом типе данных говорят как об абстрактном, или ADT (Abstract Data Туре). Абстрактный класс данных представляет общую концепцию, такую как форма, а не отдельные объекты, такие как окружность или квадрат. В C++ ADT по отношению к другим классам всегда выступает как базовый, для которого невозможно создать функциональный объект абстрактного класса. Чистые виртуальные функцииC++ поддерживает создание абстрактных типов данных с чистыми виртуальными функциями. Чистыми виртуальными функциями называются такие, которые инициализируются нулевым значением, например: virtual void Draw() = 0; Класс, содержащий чистые виртуальные функции, является ADT. Невозможно создать объект для класса, который является ADT. Попытка создания объекта для такого класса вызовет сообщение об ошибке во время компиляции. Помещение в класс чистой виртуальной функции будет означать следующее: • невозможность создания объекта этого класса; • необходимость замещения чистой виртуальной функции в производном классе. Любой класс, произведенный от ADT, унаследует от него чистую виртуальную функцию, которую необходимо будет заместить, чтобы получить возможность создавать объекты этого класса. Так, если класс Rectangle наследуется от класса Shape, который содержит три чистые виртуальные функции, то в классе Rectangle должны быть замещены все эти три функции, иначе он тоже будет ADT. В листинге 13.8 изменено объявление классa Shape таким образом, чтобы он стал абстрактным типом данных. Остальная часть листинга 13.7 не изменилась, поэтому не приводится. Просто замените объявление класса в строках 7—16 листинга 13.7 листингом 13.8 и запустите программу. Листинг 13.8. Абстрактные типы данных 1: класс Shape 2: { 3: public: 4: Shape(){ } 5: ~Shape(){ }. 6: virtual long GetArea() = 0; // ошибка 7: virtual long GetPerim()= 0; 8: virtual void Draw() = 0; 9: private: 10: }; Результат: (1)Circle (2)Rectangle (3)Square (0)Quit: 2 x x x x x x x x x x x x x x x x x x x x x x x x (1)Circle (2)Rectangle (3)Square (0)Quit: 3 x x x x x x x x x x x x x x x x x x x x x x x x x (1)Circle (2)Rectangle (3)Square (0)Quit: 0 Анализ: Как видите, выполнение программы не изменилось. Просто теперь в программе невозможно создать объект класса Shape. Абстрактные типы данных Чтобы объявить класс как абстрактный тип данных.достаточно добавить в него одну или несколько чистых виртуальных функций. Для этогопосле объявления функции необходимо добавить - 0, например: сlass Shape { virtual void Draw() = 0; // чистая виртуальная функция } Выполнение чистых виртуальных функцийОбычно чистые виртуальные функции объявляются в абстрактном базовом классе и не выполняются. Поскольку невозможно создать объект абстрактного базового класса, как правило, нет необходимости и ff выполнении чистой виртуальной функции. Класс ADT существует только как объявление интерфейса объектов, создаваемых в производных классах. Тем не менее все же иногда возникает необходимость выполнения чистой виртуальной функции. Она может быть вызвана из объекта, произведенного от ADT, например чтобы обеспечить общую функциональность для всех замещенных функций. В листинге 13.9 представлен видоизмененный листинг 13.7, в котором класс Shape объявлен как ADT и в программе выполняется чистая виртуальная функция Draw(). Функция замещается в классе Circle, что необходимо для создания объекта этого класса, но в объявлении замещенной функции делается вызов чистой виртуальной функции из базового класса. Это средство используется для достижения дополнительной функциональности методов класса. В данном примере дополнительная функциональность состоит в выведении на экран простого сообщения. В реальной программе чистая виртуальная функция может содержать достаточно сложный программный код, например создание окна, в котором рисуются все фигуры, выбираемые пользователем. Листинг 13.9. Выполнение чистых виртуальных функций 1: // Выполнение чистых виртуальных функций 2: 3: #include <iostream.h> 4: 5: class Shape 6: { 7: public: 8: Shape(){ } 9: virtual ~Shape(){ } 10: virtual long GetArea() = 0; 11: virtual long GetPerim()= 0; 12: virtual void Draw() = 0; 13: private: 14: }; 15: 16: void Shape::Draw() 17: { 18: cout << "Abstract drawing mechanism!\n"; 19: } 20: 21: class Circle : public Shape 22: { 23: public: 24: Circle(int radius):itsRadius(radius) { } 25: virtual ~Circle() { } 26: long GetArea() { return 3 * itsRadius * itsRadius; } 27: long GetPerim() { return 9 * itsRadius; } 28: void Draw(); 29: private: 30: int itsRadius; 31: int itsCircumference; 32: }; 33: 34: voidCircle::Draw() 35: { 36: cout << "Circle drawing routine here!\n"; 37: Shape::Draw(); 38: } 39: 40: 41: class Rectangle : public Shape 42: { 43: public: 44: Rectangle(int len, int width): 45: itsLength(len), itsWidth(width){ } 46: virtual ~Rectangle(){ } 47: long GetArea() { return itsLength * itsWidth; } 48: long GetPerim() { return 2*itsLength + 2*itsWidth; 49: virtual int GetLength() { return itsLength; > 50: virtual int GetWidth() { return itsWidth; } 51: void Draw(); 52: private: 53: int itsWidth; 54: int itsLength; 55: }; 56: 57: void Rectangle::Draw() 58: { 59: for (int i = 0; i<itsLength; i++) 60: { 61: for (int j = 0; j<itsWidth; j++) 62: cout << "x "; 63: 64: cout << "\n"; 65: } 66: Shape::Draw(); 67: } 68: 69: 70: class Square : public Rectangle 71: { 72: public: 73: Square(int len); 74: Square(int len, int width); 75: virtual ~Square(){ } 76: long GetPerim() { return 4 * GetLength();} 77: }; 78: 79: Square::Square(int len): 80: Rectangle(len,len) 81: { } 82: 83: Square::Square(int len, int width): 84: Rectangle(len,width) 85: 86: { 87: if (GetLength() != GetWidth()) 88: cout << "Error, not a square... a Rectangle??\n"; 89: } 90: 91: int main() 92: { 93: int choice; 94: bool fQuit = false; 95: Shape * sp; 96: 97: while (1) 98: { 99: cout << "(1)Circle (2)Rectangle (3)Square (0)Quit: "; 100: cin >> choice; 101: 102: switch (choice) 103: { 104: case 1: sp = new Circle(5); 105: break; 106: case 2: sp = new Rectangle(4,6); 107: break; 108: case 3; sp = new Square (5); 109: break; 110: default: fQuit = true; 111: break; 112: } 113: if (fQuit) 114: break; 115: 116: sp->Draw(); 117: delete sp; 118: cout << "\n"; 119: } 120: return 0; 121: } Результат: (1)Circle (2)Rectangle (3)Square (0)Quit: 2 x x x x x x x x x x x x x x x x x x X X X Х X X Abstract drawing mechanism! (1)Circle (2)Rectangle (3)Square (0)Quit: 3 x x x x x X X X X X X X X X X X X X X X X X X X X Abstract drawing mechanism! (1)Circle (2)Rectangle (3)Square (0)Quit: 0 Анализ: В строках 5—14 объявляется класс абстрактного типа данных Shape с тремя чистыми виртуальными функциями. Впрочем, для того чтобы класс стал ADT, достаточно было объявить в нем хотя бы один из методов как чистую виртуальную функцию. Далее в программе все три функции базового класса замешаются в производных классах Circle и Rectangle, но одна из них — функция Draw() — выполняется как чистая виртуальная функция, поскольку в объявлении замещенного варианта функции в производных классах есть вызов исходной функции из базового класса. В результате выполнение этой функции в обоих производных классах приводит к выведению на экран одного и того же сообщения. Сложная иерархия абстракцийИногда бывает необходимо произвести один класс ADT от другого класса ADT, например для того, чтобы в производном классе ADT преобразовать в обычные методы часть функций, объявленных в базовом классе как чистые виртуальные, оставив при этом другие функции чистыми. Так, в классе Animal можно объявить методы Eat(), Sleep(), Move() и Reproduce() как чистые виртуальные функции. Затем от класса Animal производятся классы Mammal и Fish. Исходя из соображения, что все млекопитающие размножаются практически одинаково, имеет смысл в классе Mammal преобразовать метод Reproduce() в обычный, оставив при этом методы Eat(), Sleep() и Move() чистыми виртуальными функциями. Затем от класса Mammal производится класс Dog, в котором необходимо заместить все три оставшиеся чистые виртуальные функции, чтобы получить возможность создавать объекты класса Dog. Таким образом, наследование одного класса ADT от другого класса ADT позволяет объявлять общие методы для всех следующих производных классов, чтобы не замещать потом эти функции по отдельности в каждом производном классе. В листинге 13.10 показан базовый костяк программы, в котором используется объявленный выше подход. Листинг 13.10. Наследование класса ADT от другого класса ADT 1: // Листинг 13.10. 2: // Deriving ADTs from other ADTs 3: #include <iostream.h> 4: 5: enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown }; 6: 7: class Animal // Общий базовый класс для классов Mammal и Fish 8: { 9: public: 10: Animal(int); 11: virtual ~Animal() { cout << "Animal destructor...\n"; } 12: virtual int GetAge() const { return itsAge; } 13: virtual void SetAge(int age) { itsAge = age; } 14: virtual void Sleep() const = 0; 15: virtual void Eat() const = 0; 16: virtual void Reproduce() const = 0; 17: virtual void Move() const = 0; 18: virtual void Speak() const = 0; 19: private: 20: int itsAge; 21: }; 22: 23: Animal::Animal(int age): 24: itsAge(age) 25: { 26: cout << "Animal constructor...\n"; 27: } 28: 29: class Mammal : public Animal 30: { 31: public: 32: Mammal(int age):Animal(age) 33: { cout << "Mammal constructor...\n";} 34: virtual ~Mammal() { cout << "Mammal destructor...\n";} 35: virtual void Reproduce() const 36: { cout << "Mammal reproduction depicted...\n"; } 37: }; 38: 39: class Fish : public Animal 40: { 41: public: 42: Fish(int age):Animal(age) 43: { cout << "Fish constructor...\n";} 44: virtual ~Fish() { cout << "Fish destructor...\n"; } 45: virtual void Sleep() const { cout << "fish snoring...\n"; } 46: virtual void Eat() const { cout << "fish feeding...\n"; } 47: virtual void Reproduce() const 48: { cout << "fish laying eggs...\n"; } 49: virtual void Move() const 50: { cout << "fish swimming...\n"; } 51: virtual void Speak() const { } 52: }; 53: 54: class Horse : public Mammal 55: { 56: public: 57: Horse(int age, COLOR color ): 58: Mamrnal(age), itsColor(color) 59: { cout << "Horse constructor...\n"; } 60: virtual ~Horse() { cout << "Horse destructor...\n"; } 61: virtual void Speak()const { cout << "Whinny!... \n"; } 62: virtual COLOR GetItsColor() const { return itsColor; } 63: virtual void Sleep() const 64: { cout << "Horse snoring.,.\n"; } 65: virtual void Eat() const { cout << "Horse feeding...\n"; } 66: virtual void Move() const { cout << "Horse running...\n";} 67: 68: protected: 69: COLOR itsColor; 70: }; 71: 72: class Dog : public Mammal 73: { 74: public: 75: Dog(int age, COLOR color ): 76: Mammal(age), itsColor(color) 77: { cout << "Dog constructor...\n"; } 78: virtual ~Dog() { cout << "Dog destructor...\n"; } 79: virtual void Speak()const { cout << "Woof!... \n"; } 80: virtual void 51eep() const { cout << "Dog snoring...\n"; } 81: virtual void Eat() const { cout << "0og eating...\n"; } 82: virtual void Move() const { cout << "Dog running...\n"; } 83: virtual void Reproduce() const 84: { cout << "Dogs reproducing...\n"; } 85: 86: protected: 87: COLOR itsColor; 88: }; 89: 90: int main() 91: { 92: Animal *pAnimal=0; 93: int choice; 94: bool fQuit = false; 95: 96: while (1) 97: { 98: cout << "(1)Dog (2)Horse (3)Fish(0)Quit: "; 99: cin >> choice; 100: 101: switch (choice) 102: { 103: case 1: pAnimal = new Dog(5,Brown); 104: break; 105: case 2: pAnimal = new Horse(4,Black); 106: break; 107: case 3: pAnimal = new 108: break; 109: default: fQuit = true 110: break; 111: } 112: if (fQuit) 113: break; 114: 115: pAnimal->Speak(); 116: pAnimal->Eat(); 117: pAnimal->Reproduce(); 118: pAnimal->Move(); 119: pAnimal->Sleep(); 120: delete pAnimal; 121: cout << "\n"; 122: } 123: return 0; 124: } Результат: (1)Dog (2)Horse (3)Bird (0)Quit: 1 Animal constructor. . . Mammal constructor... Dog constructor... Woof!... Dog eating. . . Dog reproducing.... Dog running... Dog snoring... Dog destructor... Mammal destructor... Animal destructor... (1)Dog (2)Horse (3)Bird (0)Quit: 0 Анализ: В строках 7—21 объявляется абстрактный тип данных Animal. Единственный метод этого класса, не являющийся чистой виртуальной функцией, это общий для объектов всех производных классов метод itsAge. Остальные пять методов — Sleep(), Eat(), Reproduce(), Move() и Speak() — объявлены как чистые виртуальные функции. Класс Mammal производится от Animal в строках 29—37 и не содержит никаких данных. В нем замещается функция Reproduce(), чтобы задать способ размножения, общий для всех млекопитающих. Класс Fish производится непосредственно от класса Animal, поэтому функция Reproduce() в нем замещается иначе, чем в классе Mammal (и это соответствует реальности). Во всех других классах, производимых от класса Mammal, теперь нет необходимости замещать общий для всех метод Reproduce(), хотя при желании это можно сделать для определенного класса, как, например, в нашей программе это было сделано в строке 83 для класса Dog. Все остальные чистые виртуальные функции были замещены в классах Fish, Horse и Dog, поэтому для каждого из них можно создавать соответствующие объекты. В теле программы используется указатель класса Animal, с помощью которого делаются ссылки на все объекты производных классов. В зависимости от того, с каким объектом связан указатель в текущий момент, вызываются соответствующие виртуальные функции. При попытке создать объекты для классов абстрактных типов данных Animal или Mammal компилятор покажет сообщение об ошибке. Когда следует использовать абстрактные типы данныхВ одних примерах программ, рассмотренных нами ранее, класс Animal являлся абстрактным типом данных, в других — нет. В каких же случаях нужно объявлять класс как абстрактный тип данных? Нет никаких правил, которые требовали бы объявления класса как абстрактного. Программист принимает решение о создании абстрактного типа данных, основываясь на том, какую роль играет этот класс в программе. Так, если вы хотите смоделировать виртуальную ферму или зоопарк, то имеет смысл класс Animal объявить как абстрактный и для создания объектов производить от него другие классы, такие как Dog. Если же вы хотите смоделировать виртуальную псарню, то теперь класс Dog будет абстрактным, от которого можно производить подклассы, представляющие разные породы собак. Количество уровней абстрактных классов следует выбирать в зависимости от того, насколько детально вы хотите смоделировать реальный объект или явление. Рекомендуется:Используйте абстрактные типы данных для создания общего интерфейса для всех производных классов. Обязательно замещайте в производных классах все чистые виртуальные функции. Объявляйте все функции, которые нужно замещать в производных классах, как чистые виртуальные функции. Не рекомендуется:Не пытайтесь создать объектабстрактного класса. Логика использования абстрактных классовВ последнее время в программировании на C++ активно используется концепция создания абстрактных логических конструкций. С помощью таких конструкций можно находить решения для многих общих задач и создавать при этом программы, которые легко читаются и документируются. Рассмотрим пример создания логической конструкции с использованием наследования классов. Представим, что нужно создать класс Timer, который умеет отсчитывать секунды. Такой класс может иметь целочисленную переменную-член itsSeconds, а также метод, осуществляющий приращение переменной itsSeconds. Теперь предположим, что программа должна отслеживать и сообщать о каждом изменении переменной itsSeconds. Первое решение, которое приходит на ум, — это добавить в класс Timer метод уведомления об изменении переменной-члена. Но логически это не совсем верно, так как программа уведомления может быть достаточно сложной и по сути своей не является логической частью программы отсчета времени. Гораздо логичнее рассматривать программу отслеживания и информирования об изменении переменной как абстрактный класс, который в равной степени может использоваться как с программой отсчета времени, так и с любой другой программой с периодически изменяющимися переменными. Таким образом, лучшим решением будет создание абстрактного класса обозревателя Observer с чистой виртуальной функцией Update(). Теперь создадим второй абстрактный класс — Subject. Он содержит массив объектов класса Observer. Кроме того, в нем объявлены два дополнительных метода: Register(), который регистрирует объекты класса Observer, и Notify(), который отслеживает изменения указанной переменной. Эта конструкция классов может затем использоваться во многих программах. Те классы, которые будут отслеживать изменения и сообщать о них, наследуются от класса Observer. Класс Timer в нашем примере наследуется от класса Subject. При изменении контролируемой переменной (в нашем примере — itsSeconds) вызывается метод Notify(), унаследованный от класса Subject. Наконец, можно создать новый класс ObserverTimer, унаследованный сразу от двух базовых классов — Observer и Timer, который будет сочетать в себе возможности отсчитывать время и сообщать об этом. Пара слов о множественном наследовании, абстрактных типах данных и языке JavaМногие программисты знают, что в основу языка Java положен C++. Также известно, что создатели языка Java удалили из него возможность множественного наследования потому, что, по их мнению, это средство слишком усложняет программный код и идет в разрез с концепцией упрощения программных кодов, положенной в основу Java. С точки зрения создателей Java, 90% всех возможностей, предоставляемых множественным наследованием, можно получить с помощью интерфейса. Интерфейс в терминологии Java представляет собой нечто подобное абстрактному типу данных, в том смысле, что в нем также определяются функции, которые могут быть реализованы только в производных классах. Но новые классы не производятся непосредственно от интерфейса. Классы производят от других классов и в них передаются функции интерфейса, что напоминает множественное наследование. Так, союз абстрактных классов и множественного наследования породил на свет аналог классов- мандатов, в результате чего удалось избежать чрезмерного усложнения программных кодов, как в случае с множественным наследованием. Кроме того, поскольку интерфейсы не содержат ни выполняемых функций, ни переменных-членов, отпадает необходимость в виртуальном наследовании. Насколько удобны или целесообразны эти изменения, зависит от привычек конкретного программиста. Во всяком случае, если вы хорошо разберетесь в множественном наследовании и абстрактных типах данных языка C++, то это послужит хорошей базой при изучении и освоении последних достижений и тенденций программирования, реализованных в языке Java (если у вас возникнет интерес к нему). Использование логических конструкций в языках C++ и Java подробно рассматривается в следующей статье: Robert Martin, C++ and Java: А Critical Comparison // C++ Report. — January, 1997. РезюмеСегодня вы познакомились с методами преодоления некоторых ограничений одиночного наследования. Вы узнали об опасности передачи вверх по иерархии классов интерфейса производных функций и об ограничениях приведения типа данных объектов базового класса к производным классам во время выполнения программы. Кроме того, вы узнали, когда и как используется множественное наследование классов, какие проблемы при этом могут возникнуть и как их преодолеть. На этом занятии также было представлено объявление абстрактных типов данных и способы создания абстрактного класса с помощью чистых виртуальных функций. Особое внимание уделялось логике использования абстрактных данных для моделирования реальных ситуаций. Вопросы и ответыЧто означает передача функциональности вверх по иерархии классов? Речь идет о переносе описаний общих функций-членов в базовые классы более высокого уровня. Если одна и та же функция используется в производных классах, имеет смысл описать эту функцию в общем для них базовом классе. Во всех ли случаях передача функциональности вверх целесообразна в программе? Если передаются вверх по иерархии только функции общего использования, то это целесообразно, но смысл теряется, если в базовые классы передается специфичный интерфейс производных классов. Другими словами, если метод не может быть использован во всех производных классах, то нет смысла описывать его в базовом классе. В противном случае вам во время выполнения программы придется отслеживать тип текущего объекта, прежде чем вызвать функцию. В чем проблема с контролем типа объекта при выполнении программы? В больших программах для выполнения контроля за типом объекта придется использовать достаточно массивный и сложный программный блок. Идея использования виртуальных функций состоит в том, что тип объекта определяется программой автоматически с помощью виртуальной таблицы, вместо того чтобы использовать для этого специальные программные блоки. Что плохого в приведении типа объектов? Приведение типов объектов к определенному типу данных, используемому конкретной функцией, довольно часто и эффективно используется в программах на C++. Но если программист применяет приведение типов для того, чтобы обойти заложенный в C++ строгий контроль за соответствием типов данных, например в случае приведения типа указателя к установленному во время выполнения программы типу объекта, то это говорит о серьезных недостатках в структуре программы, противоречащих идеологии C++. Почему бы не сделать все функции виртуальными? Для поддержания работы виртуальных функций создается виртуальная таблица, что увеличивает потребление памяти программой и время выполнения программы. Если в программе используется небольшой класс, от которого не производятся подклассы, то в использовании виртуальных функций нет никакого смысла. В каких случаях используются виртуальные деструкторы? Виртуальные деструкторы следует описывать в том случае, если в программе планируется использование указателя базового класса для получения доступа к объектам подклассов. Существует одно простое правило: если в программе описываются виртуальные функции, то обязательно должны использоваться виртуальные деструкторы. Для чего возиться с созданием абстрактных типов данных? Не проще ли создать обычный базовый класс, для которого просто не создавать объектов в программе? При написании программы всегда следует использовать такие подходы, которые гарантировали бы обнаружение ошибок в программе не во время ее выполнения, а во время компиляции. Если класс явно будет описан как абстрактный, то любая попытка создать объект этого класса приведет к показу компилятором сообщения об ошибке. КоллоквиумВ этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний и приводится несколько упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из предложенных ниже вопросов. Контрольные вопросы1. Что такое приведение типа объекта вниз? 2. Что такое v-ptr? 3. Предположим, для создания прямоугольника с закругленными углами используется класс RoundRect, произведенный от двух базовых классов — Rectangle и Circle, которые, в свою очередь, производятся от общего класса Shape. Как много объектов класса Shape создается при создании одного объекта класса RoundRect? 4. Если классы Horse и Bird виртуально наследуются от класса Animal как открытые, будут ли конструкторы этих классов инициализировать конструктор класса Animal? Если класс Pegasus наследуется сразу от двух классов, Horse и Bird, как в нем будет инициализироваться конструктор класса Animal? 5. Объявите класс Vehicle (Машина) как абстрактный тип данных. 6. Если в программе объявлен класс ADT с тремя чистыми виртуальными функциями, сколько из них нужно заместить в производных классах, чтобы получить возможность создания объектов этих классов? Упражнения1. Объявите класс JetPlane (Реактивный самолет), наследуя его отдвух базовых классов — Rocket (Ракета) и Airplane (Самолет). 2. Произведите от класса JetPlane, объявленного в первом упражнении, новый класс 747. 3. Напишите программу, производящую классы Car (Легковой автомобиль) и Bus (Автобус) от класса Vehicle (Машина). Объявите класс Vehicle как абстрактный тип данных с двумя чистыми виртуальными функциями. Классы Car и Bus не должны быть абстрактными. 4. Измените программу из предыдущего упражнения таким образом, чтобы класс Car тоже стал ADT, и произведите от него три новых класса: SportsCar (Спортивный автомобиль), Wagon (Фургон) и Coupe (Двухместный автомобиль-купе). В классе Car должна замещаться одна из виртуальных функций, объявленных в классе Vehicle, с вызовом функции базового класса. День 14-й. Специальные классы и функцииЯзык программирования C++ предлагает несколько способов ограничения области видимости и использования переменных и указателей. В предыдущих главах вы научились создавать глобальные переменные, используемые во всей программе, и локальные переменные, используемые в отдельных функциях. Вы узнали, что собой представляют указатели на переменные и переменные-члены класса. Сегодня вы узнаете: • Что такое статические переменные-члены и функции-члены • Как используются статические переменные-члены и функции-члены • Как создавать и применять указатели на функции и на функции-члены • Как работать с массивами указателей на функции Статические переменные-членыДо настоящего момента вы считали, что всякие данные объекта уникальны для того объекта, в котором используются, и не могут совместно применяться несколькими объектами класса. Другими словами, если было создано пять объектов Cat, то каждый из них характеризуется своим временем жизни, размерами и т.п. При этом время жизни одного не влияет на время жизни остальных. Однако иногда возникает необходимость контроля за накоплением данных программой. Может потребоваться информация о том, сколько всего было создано объектов определенного класса и сколько их существует в данный момент. Статические переменные-члены совместно используются всеми объектами класса. Они являются чем вроде "золотой серединки" между глобальными данными, доступными всем частям программы, и данными членов, доступными, как правило, только одному объекту. Можно полагать, что статические члены принадлежат классу, а не объекту. Если данные обычных членов доступны одному объекту, то статические члены могут использоваться всем классом. В листинге 14.1 объявляется объект Cat со статическим членом HowManyCats. Эта переменная учитывает количество созданных объектов Cat, что реализуется приращением статической переменной HowManyCats при вызове конструктора или отрицательным приращением при вызове деструктора. Листинг 14.1. Статические переменные-члены 1: //Листинг 14.1. Статические переменные-члены 2: 3: #include <iostream.h> 4: 5: class Cat 6: { 7: public: 8: Cat(int age):itsAge(age){ HowManyCats++; } 9: virtual ~Cat() { HowManyCats--; } 10: virtual int 6etAge() { return itsAge; } 11: virtual void SetAge(int age) { itsAge = age; } 12: static int HowManyCats; 13: 14: private: 15: int itsAge; 16: 17: }; 18: 19: int Cat::HowManyCats = 0; 20: 21: int main() 22: { 23: const int MaxCats = 5; 24: int i; Cat *CatHouse[MaxCats]; 25: for (i = 0; i<MaxCats; i++) 26: CatHouse[i] = new Cat(i); 27: 28: for (i = 0; i<MaxCats; i++) 29: { 30: cout << "There are "; 31: cout << Cat::HowManyCats; 32: cout << " cats left!\n"; 33: cout << "Deleting the one which is "; 34: cout << CatHouse[i]->GetAge(); 35: cout << " yea.rs old\n"; 36: delete CatHouse[i]; 37: CatHouse[i] = 0; 38: } 39: return 0; 40: } Результат: There are 5 cats left! Deleting the one which is 0 years old There are 4 cats left! Deleting the one which is 1 years old There are 3 cats left! Deleting the one which is 2 years old There are 2 cats left! Deleting the one which is 3 years old There are 1 cats left! Deleting the one which is 4 years old Анализ: Обычный класс Cat объявляется в строках 5—17. С помощью ключевого слова static в строке 12 объявляется статическая переменная-член HowManyCats типа int. Объявление статической переменной HowManyCats само по себе не определяет никакого целочисленного значения, т.е. в памяти компьютера не резервируется область для данной переменной при ее объявлении, поскольку, по сути, она не является переменной-членом конкретного объекта Cat. Определение и инициализация переменной HowManyCats происходит в строке 19. Не забывайте отдельно определять статическую переменную-член класса (весьма распространенная ошибка среди начинающих программистов). В противном случае редактор связей во время компиляции программы выдаст следующее сообщение об ошибке: undefined symbol Cat::HowManyCats Обратите внимание, что для обычной переменной-члена itsAge не требуется отдельное определение, поскольку обычные переменные-члены определяются автоматически каждый раз при создании объекта Cat, как, например, в строке 26. Конструктор объекта Cat, объявленный в строке 8, увеличивает значение статической переменной-члена на единицу. Деструктор, объявленный в строке 9, уменьшает это значение на 1. Таким образом, в любой момент времени переменная HowManyCats отражает текущее количество созданных объектов класса Cat. В строках программы 21—40 создается пять объектов Cat, указатели на которые заносятся в массив. Это сопровождается пятью вызовами конструктора класса Cat, в результате чего пять раз происходит приращение на единицу переменной HowManyCats, начиная с исходного значения 0. Затем в программе цикл for последовательно удаляет все объекты Cat из массива, предварительно выводя на экран текущее значение переменной HowManyCats. Вывод начинается со значения 5 (ведь было создано пять объектов) и с каждым циклом уменьшается. Обратите внимание: переменная HowManyCats объявлена как public и может вызываться из функции main(). Однако нет веских причин объявлять эту переменную-член таким образом. Если предполагается обращаться к статической переменной только через объекты класса Cat, предпочтительней сделать ее закрытой вместе с другими переменными-членами и создать открытый метод доступа. С другой стороны, если необходимо получать прямой доступ к данным без использования объекта Cat, то можно либо оставить ее открытой, как показано в листинге 14.2, либо создать статическую функцию-член. Реализация последнего варианта рассматривается далее в этой главе. Листинг 14.2. Доступ к статическим членам без использования объектов 1: // Листинг 14.2. Статические переменные-члены 2: 3: #include <iostream.h> 4: 5: class Cat 6: { 7: public: 8: Cat(int age):itsAge(age) { HowManyCats++; } 9: virtual ~Cat() { HowManyCats--; } 10: virtual int GetAge() { return itsAge; } 11: virtual void SetAge(int age) {itsAge = age;} 12: static int HowManyCats; 13: 14: private: 15: int itsAge; 16: 17: }; 18: 19: int Cat::HowManyCats = 0; 20: 21: voidTelepathicFunction(); 22: 23: int main() 24: { 25: const int MaxCats = 5; int i; 26: Cat *CatHouse[MaxCats]; 27: for (i = 0; i<MaxCats; i++) 28: { 29: CatHouse[i] = new Cat(i); 30: TelepathicFunction(); 31: } 32: 33: for ( i = 0; i<MaxCats; i++) 34: { 35: delete CatHouse[i]; 36: TelepathicFunction(); 37: } 38: return 0; 39: } 40: 41: void TelepathicFunction() 42: { 43: cout << "There are "; 44: cout << Cat::HowManyCats << " cats alive!\n"; 45: } Результат: There are 1 cats alive! There are 2 cats alive! There are 3 cats alive! There are 4 cats alive! There are 5 cats alive! There are 4 cats alive! There are 3 cats alive! There are 2 cats alive! There are 1 cats alive! There are 0 cats alive! Анализ: Листинг 14.2 аналогичен листингу 14.1, однако включает новую функцию TelepahicFunction().Она не создает объект СаГ и даже не использует тегов качестве параметра, однако может получить доступ к переменной-члену HowManyCats. Не лишним будет еще раз напомнить, что эта переменная-член относится не к какому-либо определенному объекту, а ко всему классу в целом. Поэтому если она объявлена как public, то может использоваться любой функцией программы. Если статическая переменная-член будет объявлена как закрытая, то доступ к ней можно получить с помощью функции-члена. Но для этого необходимо наличие хотя бы одного объекта данного класса. Именно такой подход реализован в листинге 14.3. Затем мы перейдем к изучению статических функций-членов. Листинг 14.3. Доступ к статическим членам с помощью обычных функций-членов 1: //Листинг 14.3. Закрытые статические переменные-члены 2: 3: #include <iostream.h> 4: 5: class Cat 6: { 7: public: 8: Cat(int age):itsAge(age){ HowManyCats++; } 9: virtual ~Cat() { HowManyCats--; } 10: virtual int GetAge() { return itsAge; } 11: virtual void SetAge(int age) { itsAge = age; } 12: virtual int GetHowMany() { return HowManyCats; } 13: 14: 15: private: 16: int itsAge; 17: static int HowManyCats; 18: }; 19: 20: int Cat::HowManyCats = 0; 21: 22: int main() 23: { 24: const int MaxCats = 5; int i; 25: Cat *CatHouse[MaxCats]; 26: for (i = 0; i<MaxCats; i++) 27: CatHouse[i] = new Cat(i); 28: 29: for (i = 0; i<MaxCats; i++) 30: { 31: cout << "There are "; 32: cout << CatHouse[i]->GetHowMany(); 33: cout << " cats left!\n"; 34: cout << "Deleting the one which is "; 35: cout << CatHouse[i]->GetAge()+2; 36: cout << " years old\n"; 37: delete CatHouse[i]; 38: CatHouse[i] = 0; 39: } 40: return 0; 41: } Результат: There are 5 cats left! Deleting the one which is 2 years old There are 4 cats left! Deleting the one which is 3 years old There are 3 cats left! Deleting the one which is 4 years old There are 2 cats left! Deleting the one which is 5 years old There are 1 cats left! Deleting the one which is 6 years old Анализ: В строке 17 статическая переменная-член HowManyCats объявлена как private. Поэтому теперь доступ к ней закрыт для функций, не являющихся членами класса, например для функции TelepathicFunction из предыдущего листинга. Хотя переменная HowManyCats является статической, она все же находится в области видимости класса. Поэтому любая функция класса, например GetHoqMany(), может получить доступ к ней так же, как к любой обычной переменной-члену. Однако для вызова GetHowMany() функция должна иметь объект, через который осуществляется вызов. Рекомендуется:Применяйте статические переменные-члены для совместного использования данных несколькими объектами класса. Ограничьте доступ к статическим переменным-членам, объявивих как private или protected. Не рекомендуется:Не используйте статические перемен- ные-члены для хранения данных одного объекта. Эти переменные предназначены для обмена данными между объектами. Статические функции-членыСтатические функции-члены подобны статическим переменным-членам: они не принадлежат одному объекту, а находятся в области видимости всего класса. Именно поэтому их можно вызывать даже в тех случаях, когда не было создано ни одного объекта класса, как показано в листинге 14.4. Листинг 14.4. Статические функции-члены 1: // Листинг 14.4. Статические функции-члены 2: 3: #include <iostream.h> 4: 5: class Cat 6: { 7: public: 8: Cat(int age):itsAge(age){ HowManyCats++; } 9: virtual ~Cat() { HowManyCats--; } 10: virtual int GetAge() { return itsAge; } 11: virtual void SetAge(int age) { itsAge = age; } 12: static int GetHowMany() { return HowManyCats; } 13: private: 14: int itsAge; 15: static int HowManyCats; 16: }; 17: 18: int Cat::HowManyCats = 0; 19: 20: void TelepathicFunction(); 21: 22: int main() 23: { 24: const int MaxCats = 5; 25: Cat *CatHouse[MaxCats]; int i; 26: for (i = 0; i<MaxCats; i++) 27: { 28: CatHouse[i] = new Cat(i); 29: TelepathicFunction(); 30: } 31: 32: for ( i = 0; i<MaxCats; i++), 33: { 34: delete CatHouse[i]; 35: TelepathicFunction(); 36: } 37: return 0; 38: } 39: 40: void TelepathicFunction() 41: { 42: cout << "There are " << Cat::GetHowMany() << " cats alive!\n"; 43: } Результат: There are 1 cats alive! There are 2 cats alive! There are 3 cats alive! There are 4 cats alive! There are 5 cats alive! There are 4 cats alive! There are 3 cats alive! There are 2 cats alive! There are 1 cats alive! There are 0 cats alive! Анализ: В строке 15 в объявлении класса Cat создается закрытая статическая переменная-член HowManyCats. В строке 12 объявляется открытая статическая функция-член GetHowMany(). Так как функция GetHowMany() открыта, доступ к ней может получить любая другая функция, а при объявлении ее статической отпадает необходимость в существовании объекта типа Cat. Именно поэтому функция TelepathicFunction() в строке 42 может получить доступ к GetHowMany(), не имея доступа к объекту Cat. Конечно же, к функции GetHowMany() можно было обратиться из блока main() так же, как к обычным методам объектов Cat. Примечание: Статические функции-члены не содержат указателя this. Поэтому они не могут объявляться со спецификатором const. Кроме того, поскольку функции-члены получают доступ к переменным-членам с помощью указателя this, статические функции-члены не могут использовать обычные нестатические переменные-члены! Статические функции-члены Доступ к статическим функциям-членам можно получить, либо вызывая их из объектов класса как обычные функции-члены, либо вызывая их без объектов, явно указав в этом случае имя класса. Пример: class Cat { public: static int GetHowMany() { return HowManyCats; } private: static int HowManyCats; } int Cat::HowManyCats = 0; int main() { int howMany; Cat theCat; // определение обьекта howMany = theCat.GetHowMany(); // доступ через объект howMany = Cat::GetHowMany(); // доступ без объекта } Указатели на функцииТочно так же, как имя массива постоянно указывает на его первый элемент, имя функции является указателем на саму функцию. Можно объявить переменную-указатель функции и в дальнейшем вызывать ее с помощью этого указателя. Такая возможность может оказаться весьма полезной, поскольку позволяет создавать программы, в которых функции вызываются по командам пользователя, вводимым с клавиатуры. Единственная важная деталь для определения указателя на функцию — знание типа объекта, на который ссылается указатель. Указатель типа int обязательно связан с целочисленной переменной. Аналогичным образом указатель на функцию может вызывать только функции с заданными сигнатурой и типом возврата. В объявлении long (*funoPtr) (int); создается указатель на функцию funcPtr (обратите внимание на символ * перед именем указателя), которая принимает целочисленный параметр и возвращает значение типа long. Круглые скобки вокруг (*funcPtr) обязательны, поскольку скобки вокруг (int) имеют больший приоритет по сравнению с оператором косвенного обращения (*). Если убрать первые скобки, то это выражение будет объявлять функцию funcPtr, принимающую целочисленный параметр и возвращающую указатель на значение типа long. (Вспомните, что все пробелы в C++ игнорируются,) Рассмотрим два следующих объявления: long * Function (int); long (*funcPtr) (int); В первой строке Function() — это функция, принимающая целочисленный параметр и возвращающая указатель на переменную типа long. Во втором примере funcPtr — это указатель на функцию, принимающую целочисленный параметр и возвращающую переменную типа long. Объявление указателя на функцию всегда содержит тип возвращаемой переменной и заключенный в скобки список типов формальных параметров, если таковые имеются. Пример объявления и использования указателя на функцию показан в листинге 14.5. Листинг 14.5. Указатели на функцию 1: // Листинг 14.5. Использование указателей на функции 2: 3: #include <iostream.h> 4: 5: void Square (int&,int&); 6: void Cube (int&, int&); 7: void Swap (int&, int &); 8: void GetVals(int&, int&); 9: void PrintVals(int, int); 10: 11: int main() 12: { 13: void (* pFunc) (int &, int &); 14: bool fQuit = false; 15: 16: int val0ne=1, valTwo=2; 17: int choice; 18: while (fQuit == false) 19: { 20: cout << "(0)Quit (1)Change Values (2)Square (3)Cube (4)Swap"; 21: cin >> choice; 22: switch (choice) 23: { 24: case 1: pFunc = GetVals; break; 25: case 2: pFunc = Square; break; 26: case 3: pFunc = Cube; break; 27: case 4: pFunc = Swap; break; 28: default : fQuit = true; break; 29: } 30: 31: if (fQuit) 32: break; 33: 34: PrintVals(valOne, valTwo); 35: pFunc(valOne, valTwo); 36: PrintVals(valOne, valTwo); 37: } 38: return 0; 39: } 40: 41: void PrintVals(int x, int y) 42: { 43: cout << "x: " << x << " y: " << y << endl; 44: } 45: 46: void Square (int & rX, int & rY) 47: { 48: rX *= rX; 49: rY *= rY; 50: } 51: 52: void Cube (int & rX, int & rY) 53: { 54: int tmp; 55: 56: tmp = rX; 57: rX *= rX; 58: rX = rX * tmp; 59: 60: tmp = rY; 61: rY *= rY; 62: rY = rY * tmp; 63: } 64: 65: void Swap(int & rX, int & rY) 66: { 67: int temp; 68: temp = rX; 69: rX = rY; 70: rY = temp; 71: } 72: 73: void GetVals (int & rValOne, int & rValTwo) 74: { 75: cout << "New value for ValOne: "; 76: cin >> rValOne; 77: cout << "New value for ValTwo: "; 78: cin >> rValTwo; 79: } Результат: (0)0uit (1)Change Values (2)Square (3)Cube (4)Swap: 1 x: 1 у: 2 New value for ValOne: 2 New value for ValTwo: 3 x: 2 y: 3 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 3 x: 2 y: 3 x: 8 y: 27 (0)Qult (1 )Change Values (2)Square (3)Cube (4)Swap: 2 x: 8 y: 27 x: 64 y: 729 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 4 x: 64 y: 729 x: 729 y: 64 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 0 Анализ: В строках 5—8 объявляются четыре функции с одинаковыми типами возврата и сигнатурами. Все эти функции возвращают void и принимают ссылки на значения типа int. В строке 13 переменная pFunc объявлена как указатель на функцию, принимающую две ссылки на int и возвращающую void. Этот указатель может ссылаться на каждую из упоминавшихся ранее функций. Пользователю предлагается выбрать функцию, после чего она связывается с указателем pFunc. В строках 34—36 выводятся текущие значения двух целочисленных переменных, вызывается текущая функция и выводятся результаты вычислений. Указатели на функции Обращение к функции через указатель записывается так же, как и обычный вызов функции, на которую он указывает. Просто вместо имени функции используется имя указателя на эту функцию. Чтобы связать указатель на функцию с определенной функцией, нужно просто присвоить ему имя функции без каких-либо скобок. Имя функции. как вы уже знаете, представляет собой константный указатель на саму функцию. Поэтому указатель на функцию используется так же, как и ее имя. При вызове функции через указатель следует задать все параметры. установленные для текущей функции. Пример: long(*pFuncOne) (int,int); long SomeFunction (int,int); pFuncOne = SomeFunction; pFuncOne (5,7); Зачем нужны указатели на функцииВ программе, показанной в листинге 14.5, можно было бы обойтись и без указателей на функции, однако с их помощью значительно упрощается и становится читабельнее код программы: достаточно только выбрать функцию из списка и затем вызвать ее. В листинге 14.6 используются прототипы и объявления функций листинга 14.5, но отсутствуют указатели на функции. Оцените различия между этими двумя листингами. Листинг 14.6. Видоизмененный вариант листинга 14.5 без использования указателей на функции 1: // Листинг 14.6. Видоизмененный вариант листинга 14.5 без использования 2: // указателей на функции 3: #include <iostream.h> 4: 5: void Square (int&,int&); 6: void Cube (int&, int&); 7: void Swap (int&, int &); 8: void GetVals(int&, int&); 9: void PrintVals(int, int); 10: 11: int main() 12: { 13: bool fQuit = false; 14: int valOne=1, valTwo=2; 15: int choice; 16: while (fQuit == false) 17: { 18: cout << << "(0)Quit (1)Change Values (2)Square (3)Cube (4)Swap"; 19: cin >> choice; 20: switch (choice) 21: { 22: case 1: 23: PrintVals(valOne, valTwo); 24: GetVals(valOne, valTwo); 25: PrintVals(valOne, valTwo); 26: break; 27: 28: case 2: 29: PrintVals(valOne, valTwo); 20: Square(valOne,valTwo); 31: PrintVals(valOne, valTwo); 32: break; 33: 34: case 3: 35: PrintVals(valOne, valTwo); 36: Cube(valOne, valTwo); 37: PrintVals(valOne, valTwo); 38: break; 39: 40: case 4: 41: PrintVals(valOne, valTwo); 42: Swap(valOne, valTwo); 43: PrintVals(valOne, valTwo); 44: break; 45: 46: default: 47: fOuit = true; 48: break; 49: } 50: 51: if (fQuit) 52: break; 53: } 54: return 0; 55: } 56: 57: void PrintVals(int x, int y) 58: { 59: cout << "x: " << x << " y: " << y << endl; 60: } 61: 62: void Square (int & rX, int & rY) 63: { 64: rX *= rX; 65: rY *= rY; 66: } 67: 68: void Cube (int & rX, int & rY) 69: { 70: int tmp; 71: 72: tmp = rX; 73: rX *= rX; 74: rX = rX * tmp; 75: 76: tmp = rY; 77: rY *= rY; 78: rY = rY * tmp; 79: } 80: 81: void Swap(int & rX, int & rY) 82: { 83: int temp; 84: temp = rX; 85: rX = rY; 86: rY = temp; 87: } 88: 89: void GetVals (int & rValOne, int & rValTwo) 90: { 91: cout << "New value for ValOne: "; 92: cin >> rValOne; 93: cout << "New value for ValTwo: "; 94: cin >> rValTwo; 95: } Результат: (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 1 х. 1 у. 2 New value for ValOne: 2 New value for ValTwo: 3 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 3 x: 2 y: 3 x: 8 y: 27 (0)Quit (1 )Change Values (2)Square (3)Cube (4)Swap: 2 x: 8 y: 27 x: 64 y: 729 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 4 x: 64 y: 729 x: 729 y: 64 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 0 Анализ: Функции работают так же, как и в листинге 14.5. Информация, выводимая программой на экран, также не изменилась. Но размер программы увеличился с 22 до 46 строк. Причина в том, что вызов функции PrintVals() приходится повторять для каждого блока с оператором case. Заманчивым может показаться вариант размещения функции PrintVals() вверху и внизу цикла while, а не в каждом блоке с оператором case. Но тогда функция PrintVals() будет вызываться даже в случае выхода из цикла, чего быть не должно. Упрощенный вариант вызова функции Имя указателя на функцию вовсе не должно дублировать имя самой функции, хотя вы вполне вправе это сделать. Пусть, например, pFunc—указатель на функцию, принимающую целочисленное значение и возвращающую переменную типа1опд,и имя этой функции — pFunc. У вас есть возможность вызвать ее любым из двух обращений: pFunc(x); или (*pFunc) (x); Оба выражения приведут к одному и тому же результату. Хотя первое выражение ко- роче, второе придает программе больше гибкости. Увеличение размера кода за счет повторяющихся вызовов одной и той же функции ухудшает читабельность программы. Этот вариант приведен специально для того, чтобы показа эффективность использования указателей на функции. В реальных условиях преимущества применения указателей на функции еще более очевидны, так как они позволяют исключить дублирование кода и делают программу более четкой. Например, указатели на функции можно собрать в один массив и вызывать из него функции в зависимости от текущей ситуации. Массивы указателей на функцииАналогично объявлению массива указателей целых чисел можно объявить массив указателей на функции с определенной сигнатурой, возвращающих значения определенного типа. Листинг 14.7 является еще одним вариантом программы из листинга 14.5, в которой все указатели на функции собраны в массив. Листинг 14.7. Использование массива указателей на функции 1: // Листинг 14.7. Пример использования массива указателей на функции 2: 3: #include <iostream.h> 4: 5: void Square (int&,int&); 6: void Cube (int&, int&); 7: void Swap (int&, int &); 8: void GetVals(int&, int&); 9: void PrintVals(int, int); 10: 11: int main() 12: { 13: int valOne=1, valTwo=2; 14: int choice, i; 15: const MaxArray = 5; 16: void (*pFuncArray[MaxArray])(int&, int&); 17: 18: for (i=0;i<MaxArray;i++) 19: { 20: cout << "(1)Change Values (2)Square (3)Cube (4)Swap: "; 21: cin >> choice; 22: switch (choice) 23: { 24: case 1:pFuncArray[i] = GetVals; break; 25: case 2:pFuncArray[i] = Square; break; 26: case 3:pFuncArray[i] = Cube; break; 27: case 4:pFuncArray[i] = Swap; break; 28: default:pFuncArray[i] = 0; 29: } 30: } 31: 32: for (i=0;i<MaxArray; i++) 33: { 34: if ( pFuncArray[i] == 0 ) 35: continue; 36: pFuncArray[i](valOne,valTwo); 37: PrintVals(valOne,valTwo); 38: } 39: return 0; 40: } 41: 42: void PrintVals(int x, int у) 43: { 44: cout << "x: " << x << " у: " << у << endl; 45: } 46: 47: void Square (int & rX, int & rY) 48: { 49: rX *= rX; 50: rY *= rY; 51: } 52: 53: void Cube (int & rX, int & rY) 54: { 55: int tmp; 56: 57: tmp = rX; 58: rX *= rX; 59: rX = rX * tmp; 60: 61: tmp = rY; 62: rY *= rY; 63: rY = rY * tmp; 64: } 65: 66: void Swap(int & rX, int & rY) 67: { 68: int temp; 69: temp = rX; 70: rX = rY; 71: rY = temp; 72: } 73: 74: void GetVals (int & rValOne, int & rValTwo) 75: { 76: cout << "New value for ValOne: "; 77: cin >> rValOne; 78: cout << "New value for ValTwo: "; 79: cin >> rValTwo; 80: } Результат: (1)Change Values (2)Square (3)Cube (4)Swap: 1 (1)Change Values (2)Square (3)Cube (4)Swap: 2 (1)Change Values (2)Square (3)Cube (4)Swap: 3 (1)Change Values (2)Square (3)Cube (4)Swap: 4 (1)Change Values (2)Square (3)Cube (4)Swap: 2 New Value for ValOne: 2 New Value for ValTwo: 3 x: 2 y: 3 x: 4 y: 9 x: 64 y: 729 x: 729 y: 64 x: 531441 y:4096 Анализ: Как и в предыдущем листинге, для экономии места не были показаны выполнения объявленных функций, поскольку сами функции остались теми же, что и в листинге 14.5. В строке 16 объявляется массив pFuncArray, содержащий пять указателей на функции, которые возвращают void и принимают две ссылки на значения типа int. В строках 18-30 пользователю предлагается установить последовательность вызова функций. Каждый член массива связывается с соответствующей функцией. Последовательный вызов функции осуществляется в строках 32-38, причем после каждого вызова на экран сразу выводится результат. Передача указателей на функции в другие функцииУказатели на функции (или массивы указателей) могут передаваться в другие функции для вызова в них с помощью указателя нужной функции. Листинг 14.5 можно усовершенствовать, передав указатель на выбранную функцию другой функции (кроме main()), которая выведет исходные значения на печать, вызовет функцию и вновь напечатает измененные значения. Именно такой подход применен в листинге 14.8. Листинг 14.8. Передана указателя на функцию другой функции 1: // Листинг 14.8. Передача указателя на функцию другой функции 2: 3: #include <iostream.h> 4: 5: void Square (int&,int&); 6: void Cube (int&, int&); 7: void Swap (int&, int &); 8: void GetVals(int&, int&); 9: void PrintVals(void (*)(int&, int&),int&, int&); 10: 11: int main() 12: { 13: int val0ne=1, valTwo=2; 14: int choice; 15: bool fQuit = false; 16: 17: void (*pFunc)(int&, int&); 18: 19: while (fQuit == false) 20: { 21: cout << "(0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: "; 22: cin >> choice; 23: switch (choice) 24: { 25: case 1:pFunc = GetVals; break; 26: case 2:pFunc = Square; break; 27: case 3:pFunc = Cube; break; 28: case 4:pFunc = Swap; break; 29: default:fQuit = true; break; 30: } 31: if (fQuit == true) 32: break; 33: PrintVals ( pFunc, valOne, valTwo); 34: } 35: 36: return 0; 37: } 38: 39: void PrintVals( void (*pFunc)(int&, int&),int& x, int& у) 40: { 41: cout << "x: " << x << " у: " << у << endl; 42: pFunc(x,у); 43: cout << "x: " << x << " у: " << у << endl; 44: } 45: 46: void Square (int & rX, int & rY) 47: { 48: rX *= rX; 49: rY *= rY; 50: } 51: 52: void Cube (int & rX, int &rY) 53: { 54: int tmp; 55: 56: tmp = rX; 57: rX *= rX; 58: rX = rX * tmp; 59: 60: tmp = rY; 61: rY *= rY; 62: rY = rY * tmp; 63: } 64: 65: void Swap(int & rX, int& rY) 66: { 67: int temp; 68: temp = rX; 69: rX = rY; 70: rY = temp; 71: } 72: 73: void GetVals (int & rValOne, int & rValTwo) 74: { 75: cout << "New value for ValOne: "; 76: cin >> rValOne; 77: cout << "New value for ValTwo: "; 78: cin >> rValTwo; 79: } Результат: (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 1 x: 1 у: 2 New value for Val0ne: 2 New value for ValTwo: 3 x: 2 у: 3 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 3 x: 2 у: 3 x: 8 у: 27 (O)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 2 x: 8 у: 27 x: 64 у: 729 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 4 x: 64 у: 729 x: 729 y:64 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 0 Анализ: В строке 17 объявляется указатель на функцию pFunc, принимающую две ссылки на int и возвращающую void. Функция PrintVals, для которой задается три параметра, объявляется в строке 9. Первым в списке параметров стоит указатель на функцию, возвращающую void и принимающую две ссылки на int. Второй и третий параметры функции PrintVals представляют собой ссылки на значения типа int. После того как пользователь выберет нужную функцию, в строке 33 происходит вызов функции PrintVals. Спросите у знакомого программиста, работающего с C++, что означает следующее выражение: void PrintVals(void (*)(int&, int&), int&, int&); Это вид объявлений, который используется крайне редко и заставляет программистов обращаться к книгам каждый раз, когда нечто подобное встречается в тексте. Но временами данный подход позволяет значительно усовершенствовать код программы, как в нашем примере. Использование typedef с указателями на функцииКонструкция void (*)(int&, int&) весьма громоздка. Для ее упрощения можно воспользоваться ключевым словом typedef, объявив новый тип (назовем его VPF) указателей на функции, возвращающие void и принимающие две ссылки на значения типа int. Листинг 14.9 представляет собой переписанную версию листинга 14.8 с использованием этого подхода. Листинг 14.8. Использование оператора typedef для объявления типа указателей на функции 1: // Листинг 14.9. Использование typedef для 2: // объявления типа указателей на функции 3: #include <iostream.h> 4: 5: void Square (int&,int&); 6: void Cube (int&, int&); 7: void Swap (int&, int &); 8: void GetVals(int&, int&); 9: typedef void (*VPF) (int&, int&); 10: void PrintVals(VPF,int&, int&); 11: 12: int main() 13: { 14: int val0ne=1, valTwo=2; 15: int choice; 16: bool fQuit = false; 17: 18: VPF pFunc; 19: 20: while (fQuit == false) 21: { 22: cout << "(0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: "; 23: cin >> choice; 24: switch (choice) 25: { 26: case 1:pFunc = GetVals; break; 27: case 2:pFunc = Square; break; 28: case 3:pFunc = Cube; break; 29: case 4:pFunc = Swap; break; 30: default:fQuit = true; break; 31: } 32: if (fQuit == true) 33: break; 34: PrintVals ( pFunc, valOne, valTwo); 35: } 36: return 0; 37: } 38: 39: void PrintVals( VPF pFunc,int& x, int& y) 40: { 41: cout << "x: " << x << " y: " << y << endl; 42: pFunc(x,y); 43: cout << "x: " << x << " y: " << y << endl; 44: } 45: 46: void Square (int & rX, int & rY) 47: { 48: rX *= rX; 49: rY *= rY; 50: } 51: 52: void Cube (int & rX, int & rY) 53: { 54: int tmp; 55: 56: tmp = rX; 57: rX *= rX; 58: rX = rX * tmp; 59: 60: tmp = rY; 61: rY *= rY; 62: rY = rY * tmp; 63: } 64: 65: void Swap(int & rX, int & rY) 66: { 67: int temp; 68: temp = rX; 69: rX = rY; 70: rY = temp; 71: } 72: 73: void GetVals (int & rValOne, int & rValTwo) 74: { 75: cout << "New value for ValOne: "; 76: cin >> rValOne; 77: cout << "New value for ValTwo: "; 78: cin >> rValTwo; 79: } Результат: (0)Quit (1 )Change Values (2)Square (3)Cube (4)Swap: 1 x: 1 y: 2 New value for ValOne: 2 New value for ValTwo: 3 x: 2 y: 3 (0)Quit (1 )Change Values (2)Square (3)Cube (4)Swap: 3 x: 2 y: 3 x: 8 y: 27 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 2 x: 8 y: 27 x: 64 y: 729 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 4 x: 64 y: 729 x: 729 y: 64 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 0 Анализ: В строке 9 с помощью оператора typedef объявляется новый тип VPF как указатели на функции, возвращающие void и принимающие две ссылки на int. В строке 10 объявляется функция PrintVals(), которая принимает три параметра: VPF и две ссылки на integer. В строке 18 указатель Pfunc объявляется как принадлежащий TnnyVPF. После объявления типа VPF дальнейшее использование указателя pFunc и функции PrintVals() становится проще и понятнее. Информация, выводимая программой на экран, не изменилась. Указатели на функции членыДо настоящего времени все создаваемые указатели на функции использовались для общих функций, не принадлежащих к какому-нибудь одному классу. Однако разрешается создавать указатели и на функции, являющиеся членами классов (методы). Для создания такого указателя используется тот же синтаксис, что и для указателя на обычную функцию, но с добавлением имени класса и оператора области видимости (::). Таким образом, объявление указателя pFunc на функции-члены класса Shape, принимающие два целочисленных параметра и возвращающие void, выглядит следующим образом: void (Shape::*pFunc) (int,int); Указатели на функции-члены используются так же, как и рассмотренные ранее указатели простых функции. Единственное отличие состоит в том, что для вызова функции необходимо наличие объекта соответствующего класса, для которого вызываются функции. В листинге 14.10 показано использование указателя на метод класса. Листинг 14.10. Указатели на функции-члены 1: //Листинг 14.10. Указатели на виртуальные функции-члены 2: 3: #include <iostream.h> 4: 5: class Mammal 6: { 7: public: 8: Mammal():itsAge(1) { } 9: virtual ~Mammal() { } 10: virtual void Speak() const = 0; 11: virtual void Move() const = 0; 12: protected: 13: int itsAge; 14: }; 15: 16: class Dog : public Mammal 17: { 18: public: 19: void Speak()const { cout << "Woof!\n"; } 20: void Move() const { cout << "Walking to heel...\n"; } 21: }; 22: 23: 24: class Cat : public Mammal 25: { 26: public: 27: void Speak()const { cout << "Meow!\n"; } 28: void Move() const { cout << "slinking...\n"; } 29: }; 30: 31: 32: class Horse : public Mammal 33: { 34: public: 35: void Speak()const { cout << "Whinny!\n"; } 36: void Move() const 1 cout << "Galloping...\n"; } 37: }; 38: 39: 40: int main() 41: { 42: void (Mammal::*pFunc)() const =0; 43: Mammal* ptr =0; 44: int Animal; 45: int Method; 46: bool fQuit = false; 47: 48: while (fQuit == false) 49: { 50: cout << "(0)Quit (1)dog (2)cat (3)horse 51: cin >> Animal; 52: switch (Animal) 53: { 54: case 1: ptr = new Dog; break; 55: case 2: ptr = new Cat; break; 56: case 3: ptr = new Horse; break; 57: default: fQuit = true; break; 58: } 59: if (fQuit) 60: break; 61: 62: cout << "(1)Speak (2)Move: "; 63: cin >> Method; 64: switch (Method) 65: { 66: case 1: pFunc = Mammal::Speak; break; 67: default: pFunc = Mammal::Move; break; 68: } 69: 70: (ptr->*pFunc)(); 71: delete ptr; 72: } 73: return 0; 74: } Результат: (0)Quit (1)dog (2)cat (3)horse: 1 (1)Speak (2)Move: 1 Woof! (0)Quit (1)dog (2)cat (3)horse: 2 (1)Speak (2)Move: 1 Meow! (0)Quit (1)dog (2)cat (3)horse: 3 (1)Speak (2)Move: 2 Galloping (0)Quit (1)dog (2)cat (3)horse: 0 Анализ: В строках 4—14 объявляется тип абстрактных данных Mammal с двумя чистыми виртуальными методами Speak() и Move(). От класса Mammal производятся подклассы Dog, Cat и Horse, в каждом из которых замещаются соответствующим образом функции Speak() и Move(). В процессе выполнения тела функции main() пользователю предлагается выбрать животное, после чего в области динамического обмена создается новый подкласс выбранного животного, адрес которого присваивается в строках 54—56 указателю ptr. Затем пользователь выбирает метод, который связывается с указателем pFunc. В строке 70 выбранный метод вызывается для созданного объекта посредством предоставления доступа к объекту с помощью указателя ptr и к функции с помощью указателя pFunc. Наконец, строкой 71 для указателя ptr вызывается функция delete, которая очищает область памяти, занятую созданным ранее объектом. Заметьте, что нет смысла вызывать delete для pFunc, поскольку последний является указателем на код, а не на объект в области памяти. Хотя даже при попытке сделать это вы получите сообщение об ошибке компиляции. Массивы указателейна функции-членыАналогично указателям на обычные функции, указатели на функции-члены могут храниться в массиве. Для инициализации такого массива можно использовать адреса различных функций-членов. В таком случае, чтобы вызвать для объекта тот или иной метод, достаточно просто указать массив и индекс смещения. Именно такой подход применяется в листинге 14.11. Листинг 14.11. Массив указателей на функции-члены 1: // Листинг 14.11. Массивы указателей на функции-члены 2: 3: #include <iostream.h> 4: 5: class Dog 6: { 7: public: 8: void Speak()const { cout << "Woof!\n"; } 9: void Move() const { cout << "Walking to heel...\n"; } 10: void Eat() const { cout << "Gobbling food...\n"; } 11: void Growl() const { cout << "Grrrrr\n"; } 12: void Whimper() const { cout << "Whining noises...\n"; } 13: void RollOver() const { cout << "Rolling over...\n"; } 14: void PlayDead() const { cout << "Is this the end of Little Caesar?\n"; 15: }; 16: 17: typedef void (Dog::*PDF)()const; 18: int main() 19: { 20: const int MaxFuncs = 7; 21: PDF DogFunctions[MaxFuncs] = 22: { Dog::Speak, 23: Dog::Move, 24: Dog::Eat, 25: Dog::Growl, 26: Dog::Whimper, 27: Dog::RollOver, 28: Dog::PlayDead }; 29: 30: Dog* pDog =0; 31: int Method; 32: bool fQuit = false; 33: 34: while (!fQuit) 35: { 36: cout << "(0)Quit (1)Speak (2)Move (3)Eat (4)Growl"; 37: cout << " (5)Whimper (6)Roll Over (7)Play Dead: "; 38: cin >> Method; 39: if (Method == 0) 40: { 41: fQuit = true; 42: } 43: else 44: { 45: pDog = new Dog; 46: (pDog->*DogFunctions[Method-1])(); 47: delete pDog; 48: } 49: } 50: return 0; 51: } Результат: (0)Quit (1)Speak (2)Move (3)Eat (4)Growl (5)Whimper (6)Roll 0ver (7)Play Dead: 1 Woof! (0)Quit (1)Speak (2)Move (3)Eat (4)Growl (5)Whimper (6)Roll 0ver (7)Play Dead: 4 Grrr (0)Quit (1)Speak (2)Move (3)Eat (4)Growl (5)Whimper (6)Roll 0ver (7)Play Dead: 7 Is this the end of Little Caesar? (0)Quit (1)Speak (2)Move (3)Eat (4)Growl (5)Whimper (6)Roll 0ver (7)Play Dead: 0 Анализ: В строках 5—15 создается класс Dog, содержащий семь функций-членов, характеризующихся одинаковыми сигнатурой и типом возврата. В строке 17 с помощью typedef объявляется тип PDF константных указателей на функции-члены Dog, которые не принимают и не возвращают никаких значений. В строках 21-28 объявляется массив DogFunctions, предназначенный для хранения указателей на семь функций-членов. В строках 36 и 37 пользователю предлагается выбрать метод. Выбор любого элемента, кроме Quit, приводит к созданию объекта класса Dog, после чего из массива вызывается соответствующий метод (строка 46). Ниже представлена еще одна строка, которая может немного смутить ваших знакомых программистов, работающих с C++: (pDog->*-DogFunctions[Method-1])(); Это выражение, безусловно, немного экзотично, но с его помощью можно создать таблицу функций-членов, что сделает код программы проще и читабельнее. Рекомендуется:Используйте указатели на функции- члены для вызова методов в объектах класса. Используйте typedef, чтобы упростить объявление указателя на функцию-член. Не рекомендуется:Не злоупотребляйте созданием указателей на функции-члены, если беэ них можно обойтись. РезюмеСегодня вы познакомились с созданием статических переменных-членов класса, которые, в отличие от обычных переменных-членов, принадлежат всему классу, а не отдельному объекту. Если статическая переменная-член объявлена как public, то обратиться к ней можно просто по имени, даже не используя объектов класса, которому принадлежит эта переменная. Статические переменные-члены можно использовать в качестве счетчиков объектов класса. Поскольку они не являются частями объектов, при их объявлении не происходит автоматическое резервирование памяти, как при объявлении обычных переменных-членов. Поэтому в программе за пределами объявления класса обязательно должна быть строка, в которой происходит определение и инициализация статической переменной-члена. Статические функции-члены также принадлежат всему классу, подобно статическим переменным-членам. Вызвать статическую функцию-член класса можно даже в том случае, если не было создано ни одного объекта этого класса. Сами же эти функции могут использоваться для открытия доступа к статическим переменным-членам. Поскольку статические переменные-члены не имеют указателя this, они не могут использовать обычные переменные-члены. Из-за отсутствия указателя this статические функции-члены не могут объявляться как const. Дело в том, что при объявлении функции-члена со спецификатором const устанавливается, что указатель this этой функции является константным. Кроме того, вы узнали, как объявлять и использовать указатели на обычные функции и на функции-члены, а также познакомились с созданием массивов этих указателей и с передачей указателей на функции в другие функции. Как указатели на функции, так и указатели на функции-члены могут использоваться для создания таблиц функций, что облегчает управление их вызовом в процессе выполнения программы. Это придает программе гибкость и делает программный код более читабельным. Вопросы и ответыЗачем использовать статические данные, если есть глобальные? Область видимости статических данных ограничивается классом. Обращаться к статической переменной-члену следует из объектов класса, либо из внешнего кода программы, явно указав имя класса (в случае, если статическая переменная-член описана как public), либо с помощью открытой статической функции-члена этого класса. Статические переменные-члены относятся к типу данных того класса, которому они принадлежат. Ограничение доступа к членам класса, вызванное строгим контролем за типом данных в C++, делает использование статических переменных-членов более безопасным по сравнению с глобальными данными. Зачем использовать статические функции-члены, если можно воспользоваться глобальными функциями? Статические функции-члены принадлежат классу и могут вызываться только с помощью объектов класса или с явным указанием имени класса, например: ClassName::FunctionName(). Насколько часто в программах используются указатели на функции и указатели на функции-члены? Такие указатели используются достаточно редко. Это дело вкуса программиста. Даже в сложных и мощных программах без них можно вполне обойтись. КоллоквиумВ этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний и приводится несколько упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из предложенных ниже вопросов. Контрольные вопросы1. Могут ли статические переменные-члены быть закрытыми? 2. Объявите статическую переменную-член. 3. Объявите статическую функцию. 4. Объявите указатель на функцию, принимающую параметр типа int и возвращающую значение типа long. 5. Измените указатель, созданный в задании 4, на указатель на функцию-член класса Car. 6. Объявите массив из десяти указателей, созданных в задании 5. Упражнения1. Напишите короткую программу, объявляющую класс с одной обычной переменной-членом и одной статической переменной-членом. Создайте конструктор, выполняющий инициализацию переменной-члена и приращение статической переменной-члена. Затем опишите деструктор, который уменьшает на единицу значение статической переменной. 2. Используя программный блок из упражнения 1, напишите короткую выполняемую программу, которая создает три объекта, а затем выводит значения их переменных-членов и статической переменной-члена класса. Затем последовательно удаляйте объекты и выводите на экран значение статической переменной-члена. 3. Измените программу упражнения 2 таким образом, чтобы доступ к статической переменной-члену осуществлялся с помощью статической функции-члена. Сделайте статическую переменную-член закрытой. 4. Создайте в программе упражнения 3 указатель на функцию-член для доступа к значению нестатической переменной-члена и воспользуйтесь им для вывода этих значений на печать. 5. Добавьте две дополнительные переменные-члена к классу из предыдущих заданий. Добавьте методы доступа, возвращающие значения всех этих переменных. Все функции-члены должны возвращать значения одинакового типа и иметь одинаковую сигнатуру. Для доступа к этим методам используйте указатель на функцию-член. Подведение итоговВ этой главе вашему вниманию предлагается достаточно мощная программа, в которой используется большинство средств и подходов программирования, освоенных вами в течение двух недель. В этой программе используются связанные списки, виртуальные функции, чистые виртуальные функции, замещения функций, полиморфизм, открытое наследование, перегрузка функций, вечные циклы, указатели, ссылки и многие другие знакомые вам средства. Обратите внимание, что представленный здесь связанный список отличается от рассмотренных ранее. Язык C++ предоставляет множество способов достижения одной и той же цели. Цель данной программы состоит в создании функционального связанного списка. В узлах созданного списка можно хранить записи о деталях и агрегатах, что позволяет использовать его в реальных прикладных программах баз данных складов. Хотя здесь представлена не окончательная форма программы, она достаточно хорошо демонстрирует возможности создания совершенной структуры накопления и обработки данных. Листинг программы содержит 311 строк. Попробуйте самостоятельно проанализировать код, прежде чем прочтете анализ, приведенный после листинга. Итоги второй недели 1: // ********************************** 2: // 3: // Название: Подведение итогов 4: // 5: // Файл: Week2 6: // 7: // Описание: Демонстрация создания и использования связанного списка 8: // 9: // Классы: PART - содержит идентификационный 10: // номер детали и обеспечивает возможность 11: // добавлять другие данные 12: // PartNode - функционирует как узел в PartsList 13: // 14: // PartsList - реализует механизм связывания 15: // узлов в список 16: // 17: // ********************************** 18: 19: #include <iostream.h> 20: 21: 22: 23: // **************** Part ************ 24: 25: // Абстрактный базовый класс, общий для всех деталей 26: class Part 27: { 28: public: 29: Part():itsPartNumber(1) { } 30: Part(int PartNumber):itsPartNumber(PartNumber) { } 31: virtual ~Part() { }; 32: int GetPartNumber() const { return itsPartNumber; } 33: virtual void Display() const =0; // должна быть замещена как private 34: private: 35: int itsPartNumber; 36: }; 37: 38: // выполнение чистой виртуальной функции в 39: // стандартном виде для всех производных классов 40: void Part::Display() const 41: { 42: cout << "\nНомер детали: " << itsPartNumber << endl; 43: } 44: 45: // ************* Автомобильные детали ********** 46: 47: class CarPart : public Part 48: { 49: public: 50: CarPart():itsModelYear(94){ } 51: CarPart(int year, int partNumber); 52: virtual void Display() const 53: { 54: Part::Display(); cout << "Год создания: "; 55: cout << itsModelYear << endl; 56: } 57: private: 58: int itsModelYear; 59: }; 60: 61: CarPart::CarPart(int year, int partNumber): 62: itsModelYear(year), 63: Part(partNumber) 64: { } 65: 66: 67: // ************* Авиационные детали ********** 68: 69: class AirPlanePart : public Part 70: { 71: public: 72: AirPlanePart():itsEngineNumber(1){ } ; 73: AirPlanePart(int EngineNumber, int PartNumber); 74: virtual void Display() const 75: { 76: Part::Display(); cout << "Номер двигателя: "; 77: cout << itsEngineNumber << endl; 78: } 79: private: 80: int itsEngineNumber; 81: }; 82: 83: AirPlanePart::AirPlanePart(int EngineNumber, intPartNumber): 84: itsEngineNumber(EngineNumber), 85: Part(PartNumber) 86: { } 87: 88: // ************** Узлы списка деталей ********** 89: class PartNode 90: { 91: public: 92: PartNode (Part*); 93: ~PartNode(); 94: void SetNext(PartNode * node) { itsNext = node; } 95: PartNode * GetNext() const; 96: Part * GetPart() const; 97: private: 98: Part *itsPart; 99: PartNode * itsNext; 100: }; 101: 102: // Выполнение PartNode... 103: 104: PartNode::PartNode(Part* pPart): 105: itsPart(pPart), 106: itsNext(0) 107: { } 108: 109: PartNode::~PartNode() 110: { 111: delete itsPart; 112: itsPart = 0; 113: delete itsNext; 114: itsNext = 0; 115: } 116: 117: //Возвращается NULL, если нет следующего узла PartNode 118: PartNode * PartNode::GetNext() const 119: { 120: return itsNaxt; 121: } 122: 123: Part * PartNode::GetPart() const 124: { 125: if (itsPart) 126: return itsPart; 127: else 128: return NULL; // ошибка 129: } 130: 131: // **************** Список деталей ************ 132: class PartsList 133: { 134: public: 135: PartsList(); 136: ~PartsList(); 137: // Необходимо, чтобы конструктор-копировщик и оператор соответствовали друг другу! 138: Part* Find(int & position, int PartNumber) const; 139: int GetCount() const { return itsCount; } 140: Part* GetFirst() const; 141: static PartsList& GetGlobalPartsList() 142: { 143: return GlobalPartsList; 144: } 145: void Insert(Part *); 146: void Iterate(void (Part::*f)()const) const; 147: Part* operator[](int) const; 148: private: 149: PartNode * pHead; 150: int itsCount; 151: static PartsList GlobalPartsList; 152: }; 153: 154: PartsList PartsList::GlobalPartsList; 155: 156: // Выполнение списка... 157: 158: PartsList::PartsList(): 159: pHead(0), 160: itsCount(0) 161: { } 162: 163: PartsList::^PartsList() 164: { 165: delete pHead; 166: } 167: 168: Part* PartsList::GetFirst() const 169: { 170: if (pHead) 171: return pHead->GetPart(); 172: else 173: return NULL; // ловушка ошибок 174: } 175: 176: Part * PartsList::operator[](int offSet) const 177: { 178: PartNode* pNode = pHead; 179: 180: if (!pHead) 181: return NULL; // ловушка ошибок 182: 183: if (offSet > itsCount) 184: return NULL; // ошибка 185: 186: for (int i=0;i<offSet; i++) 187: pNode = pNode->GetNext(); 188: 189: return pNode->GetPart(); 190: } 191: 192: Part* PartsList::Find(int & position, int PartNumber) const 193: { 194: PartNode * pNode = 0; 195: for (pNode = pHead, position = 0; 196: pNode!=NULL; 197: pNode = pNode->GetNext(), position++) 198: { 199: if (pNode->GetPart()->GetPartNumber() == PartNumber) 200: break; 201: } 202: if (pNode == NULL) 203: return NULL; 204: else 205: return pNode->GetPart(); 206: } 207: 208: void PartsList::Iterate(void (Part::*func)()const) const 209: { 210: if (!pHead) 211: return; 212: PartNode* pNode = pHead; 213: do 214: (pNode->GetPart()->*func)(); 215: while (pNode = pNode->GetNext()); 216: } 217: 218: void PartsList::Insert(Part* pPart) 219: { 220: PartNode * pNode = new PartNode(pPart); 221: PartNode * pCurrent = pHead; 222: PartNode >> pNext = 0; 223: 224: int New = pPart->GetPartNumber(); 225: int Next = 0; 226: itsCount++; 227: 228: if (!pHead) 229: { 230: pHead = pNode; 231: return; 232: } 233: 234: // Если это значение меньше головного узла, 235: // то текущий узел становится головным 236: if (pHead->GetPart()->GetPartNumber()->New) 237: { 238: pNode->SetNext(pHead); 239: pHead = pHode; 240: return; 241: } 242: 243: for (;;) 244: { 245: // Если нет следующего, вставляется текущий 246: if (!pCurrent->GetNext()) 247: { 248: pCurrent->SetNext(pNode); 249: return; 250: } 251: 252: // Если текущий больше предыдущего, но меньше следующего, то вставляем 253: // здесь. Иначе присваиваем значение указателя Next 254: pNext = pCurrent->GetNext(); 255: Next = pNext->GetPart()->GetPartNumber(); 256: if (Next > New) 257: { 258: pCurrent->SetNext(pNode); 259: pNode->SetNext(pNext); 260: return; 261: } 262: pCurrent = pNext; 263: } 264: } 265: 266: int main() 267: { 268: PartsList&pl = PartsList::GetGlobalPartsList(); 269: Part * pPart = 0; 270: int PartNumber; 271: int value; 272: int Choice; 273: 274: while (1) 275: { 276: cout << "(0)Quit (1)Car (2)Plane: "; 277: cin >> choice; 278: 279: if (!choice) 280: break; 281: 282: cout << "New PartNumber?: "; 283: cin >> PartNumber; 284: 285: if (choice == 1) 286: { 287: cout << "Model Year?: "; 288: cin >> value; 289: pPart = new CarPart(value,PartNumber); 290: } 291: else 292: { 293: cout << "Engine Number?: "; 294: cin >> value; 295: pPart = new AirPlanePart(value,PartNumber); 296: } 297: 298: pl.Insert(pPart); 299: } 300: void (Part::*pFunc)()const = Part::Display; 301: pl.Iterate(pFunc); 302: return 0; 303: } Результат: (0)Quit (1)Car (2)Plane: 1 New PartNumber?: 2837 Model Year? 90 (0)Quit (1)Car (2)Plane: 2 New PartNumber?: 378 Engine Number?: 4938 (0)Quit (1)Car (2)Plane: 1 New PartNumber?: 4499 Model Year?: 94 (0)Quit (1)Car (2)Plane: 1 New PartNumber?: 3000 Model Year? 93 (0)Quit (1)Car (2)Plane: 0 Part Number: 378 Engine No.: 4938 Part Number: 2837 Model Year: 90 Part Number: 3000 Model Year: 93 Part Number: 4499 Model Year: 94 Представленная программа создает связанный список объектов класса Part. Связанный список — это динамическая структура данных вроде массива, за исключением того, что в список можно добавлять произвольное число объектов указанного типа и удалять любой из введенных объектов. Данный связанный список разработан для хранения объектов класса Part, где Part — абстрактный тип данных, служащий базовым классом для любого объекта с заданной переменной-членом itsPartNumber. В программе от класса Part производятся два подкласса - CarPartHAirPlanePart. Класс Part описывается в строках 26—36, где задаются одна переменная-член и несколько методов доступа. Предполагается, что затем в объекты класса будет добавлена другая ценная информация и возможность контроля за числом созданных объектов в базе данных. Класс Part описывается как абстрактный тип данных, на что указывает чистая виртуальная функция Display(). Обратите внимание, что в строках 40-43 определяется выполнение чистой виртуальной функции Display(). Предполагается, что метод Display() будет замещаться в каждом производном классе, но в определении замещенного варианта допускается просто вызывать стандартный метод из базового класса. Два простых производных класса, CarPart и AirPlanePart, описываются в строках 47—59 и 69—87 соответственно. В каждом из них замещается метод Display() простым обращением к методу Display() базового класса. Класс PartNode выступает в качестве интерфейса между классами Part и PartList. Он содержит указатель на Part и указатель на следующий узел в списке. Методы этого класса только возвращают и устанавливают следующий узел в списке и возвращают соответствующий объект Part. За "интеллектуальность" связанного списка полностью отвечает класс PartsList, описываемый в строках 132—152. Этот класс содержит указатель на головной узел списка (pHead) и, с его помощью продвигаясь по списку, получает доступ ко всем другим методам. Продвижение по списку означает запрашивание текущего узла об адресе следующего вплоть до обнаружения узла, указатель которого на следующий узел равен NULL. Безусловно, в этом примере представлен упрощенный вид связанного списка. В реально используемой программе список должен обеспечивать еще больший доступ к первому и последнему узлам списка или создавать специальный объект итерации, с помощью которого клиенты смогут легко продвигаться по списку. В то же время класс PartsList предлагает ряд интересных методов, упорядоченных по алфавиту. Зачастую такой подход весьма эффективен, поскольку упрощает поиск нужных функций. Функция Find() принимает в качестве параметров PartNumber и значение int. Если найден раздел с указанным значением PartNumber, функция возвращает указатель на Part и порядковый номер этого раздела в списке. Если же раздел с номером PartNumber не обнаружен, функция возвращает значение NULL. Функция GetCount() проходит по всем узлам списка и возвращает количество объектов в списке. В PartsList это значение записывается в переменную-член itsCount, хотя его можно легко вычислить, последовательно продвигаясь no списку. Функция GetFirst() возвращает указатель на первый объект Part в списке или значение NULL, если список пустой. Функция GetGlobalPartsList() возвращает ссылку на статическую переменную-член GiobalPartsList. Описание статической переменной GlobaiPartsList является типичным решением для классов типа PartsList, хотя, безусловно, могут использоваться и другие имена. В законченном виде реализация этой идеи состоит в автоматическом изменении конструктора класса Part таким образом, чтобы каждому новому объекту класса присваивался номер с учетом текущего значения статической переменной GiobalPartsList. Функция Insert принимает значение указателя на объект Part, создает для него PartNode и добавляет объект Part в список в порядке возрастания номеров PartNumber. Функция Iterate принимает указатель на константную функцию-член класса Part без параметров, которая возвращает void. Эта функция вызывается для каждого объекта Part в списке. В описании класса Part таким характеристикам соответствует единственная функция Display(), замещенная во всех производных классах. Таким образом, будет вызываться вариант метода Display(), соответствующий типу объекта Part. Функция Operator[] позволяет получить прямой доступ к объекту Part по заданному смещению. Этот метод обеспечивает простейший способ определения границ списка: если список нулевой, или заданное смещение больше числа объектов в списке, возвращается значение NULL, сигнализирующее об ошибке. В реальной программе имело бы смысл все эти комментарии с описанием назначений функций привести в описании класса. Тело функции main() представлено в строках 266-303. В строке 268 описывается ссылка на PartsList и инициализируется значением GiobalPartsList. Обратите внимание, что GiobalPartsList инициализируется в строке 154. Эта строка необходима, поскольку описание статической переменной-члена не сопровождается ее автоматическим определением. Поэтому определение статической переменной-члена должно выполняться за пределами описания класса. В строках 274—299 пользователю предлагается указать, вводится ли деталь для машины или для самолета. В зависимости от выбора, запрашиваются дополнительные сведения и создается новый объект, который добавляется в список в строке 298. Выполнение метода Insert() класса PartList показано в строках 218—264. При вводе идентификационного номера первой детали — 2837 — создается объект CarPart, который передается в LinkedList::Insert()c введенными номером детали и годом создания 90. В строке 220 создается новый объект PartNode, принимающий значения новой детали. Переменная New инициализируется номером детали. Переменная-член itsCount класса PartsList увеличивается на единицу в строке 226. В строке 228 проверяется равенство указателя pHead значению NULL. В данном случае возвращается значение TRUE, поскольку это первый узел списка и указатель pHead в нем нулевой. В результате в строке 230 указателю pHead присваивается адрес нового узла и функция возвращается. Пользователю предлагается ввести следующую деталь. В нашем примере вводится деталь от самолета с идентификационным номером 37 и номером двигателя 4938. Снова вызывается функция PartsList::Insert() и pNode инициализируется новым узлом. Статическая переменная-член itsCount становится равной 2 и вновь проверяется pHead. Поскольку теперь pHead не равен нулю, то значение указателя больше не изменяется. В строке 236 номер детали, указанный в головном узле, на который ссылается pHead (в нашем случае это 2837), сравнивается с номером новой детали — 378. Поскольку последний номер меньше, условное выражение в строке 236 возвращает TRUE и головным узлом в списке становится новый объект. Строкой 238 указателю pNode присваивается адрес того узла, на который ссылался указатель pHead. Обратите внимание, что в следующий узел списка передается не новый объект, а тот, который был введен ранее. В строке 239 указателю pHead присваивается адрес нового узла. На третьем цикле пользователь вводит деталь для автомобиля под номером 4499 с годом выпуска 94. Происходит очередное приращение счетчика и сравнивается номер текущего объекта с объектом головного узла. В этот раз новый введенный идентификационный номер детали оказывается больше номера объекта, определяемого в pHead, поэтому запускается цикл for в строке 243. Значение идентификационного номера головного узла равно 378. Второй узел содержит объект со значением 2837. Текущее значение — 4499. Исходно указатель pCurrent связывается с головным узлом. Поэтому при обращении к переменной next объекта, на который указывает pCurrent, возвращается адрес второго узла. Следовательно, условное выражение в строке 246 возвратит False. Указатель pCurrent устанавливается на следующий узел, и цикл повторяется. Теперь проверка в строке 246 приводит к положительному результату. Если следующего элемента нет, то новый узел вставляется в конец списка. На четвертом цикле вводится номер детали 3000. Дальнейшее выполнение программы напоминает предыдущий этап, однако в этом случае текущий узел имеет номер 2837, а значение следующего узла равно 4499. Проверка в строке 256 возвращает TRUE, и новый узел вставляется между двумя существующими. Когда пользователь вводит 0, условное выражение в строке 279 возвращает TRUE и цикл while(1) прерывается. В строке 300 функция-член Display() присваивается указателю на функции-члены pFunc. В профессиональной программе присвоение должно проходить динамически, основываясь на выборе пользователем. Указатель функции-члена передается методу Iterate класса PartsList. В строке 208 метод Iterate() проверяет, не является ли список пустым. Затем в строках 213—215 последовательно с помощью указателя функции-члена вызываются из списка все объекты Part. В итоге для объекта Part вызывается соответствующий вариант метода Display(), в результате чего для разных объектов выводится разная информация. |
|
||
Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх | ||||
|