|
||||
|
Неделя №1Основные вопросыПри подготовке к первой неделе изучения основ программирования на языке C++ вам понадобится компилятор, текстовый редактор и эта книга. Если у вас нет ни компилятора C++, Ни текстового редактора, вы можете работать с этой книгой теоретически, но результат не будет таким высоким, как при выполнении всех предлагаемых здесь упражнений на компьютере. Лучший способ научиться программировать — самому писать программы! В конце каждой главы вы найдете раздел практических занятий, который содержит вопросы для самопроверки и набор упражнений. Не пожалейте времени, чтобы ответить на все вопросы и выполнить все упражнения. Сверьте полученные результаты с правильными ответами, приведенными в приложении Г. Книга организована так, что последующие главы построены с учетом материала, изложенного в предыдущих занятиях, поэтому прежде чем двигаться вперед, убедитесь в том, что вы до конца понимаете уже прочитанный материал. Несколько слов для программистов на языке CМатериал, изложенный в первых пяти занятиях, вероятно, будет вам знаком. Тем не менее, вам стоит просмотреть содержимое этих занятий и выполнить упражнения, чтобы удостовериться в полном понимании основ, необходимых для усвоения более сложных тем. И только после этого переходите к чтению занятия 6. Что дальшеВ течение первой недели вам предстоит освоить материал, необходимый для первых шагов в программировании вообще и на языке C++ в частности. На первых двух занятиях вы ознакомитесь с базовыми концепциями программирования и с ходом выполнения программ. На занятии 3 вы получите представление о переменных и константах, а также о том, как использовать данные в программах. На занятии 4 рассматриваются возможные ветвления программ на основе используемых данных и условий, проверяемых во время работы программы. На занятии 5 вы узнаете о том, что представляют собой функции и как их использовать, а занятие 6 познакомит вас с классами и объектами. На занятии 7 вы получите более подробную информацию о ходе выполнения программ, а к концу первой недели сможете писать настоящие объектно-ориентированные программы. День 1-й. Первые шагиВведениеДобро пожаловать на страницы книги Освой самостоятельно C++ за 21 день! Предлагаю незамедлительно отправиться в путь, если вы хотите стать профессиональным программистом на языке C++. Сегодня вы узнаете: • Почему C++ стал стандартом в области разработки программных продуктов. • Каковы этапы разработки программы на языке C++. • Как написать, скомпилировать и скомпоновать свою первую программу на языке. Краткий экскурс в историю языка C++Эволюция языков программирования с момента появления первых электронных компьютеров, построенных для выполнения расчетов траектории движения артиллерийских снарядов во время второй мировой войны, была довольно драматической. Раньше программисты работали с самыми примитивными компьютерными командами, представлявшими собой часть машинного языка. Эти команды состояли из длинных строк единиц и нулей. Вскоре были изобретены ассемблеры, которые могли отображать машинные команды в мнемоническом представлении, более понятном для человека (например, команды ADD или MOV). Со временем появились такие языки высокого уровня, как BASIC и COBOL. Благодаря этим языкам появилась возможность программировать, используя логические конструкции из слов и предложений, например Let I = 100. Эти команды переводились в машинный язык интерпретаторами и компиляторами. Интерпретатор по мере чтения программы последовательно превращает ее команды (или код) в команды машинного языка. Компилятор же целиком переводит программный код (листинг программы) в некоторую промежуточную форму — объектный файл. Этот этап называется компиляцией. Затем компилятор вызывает программу компоновки, которая превращает объектный файл в исполняемый файл программы. С интерпретатором работать проще, так как команды программы выполняются в той последовательности, в которой они записаны, что облегчает контроль за выполнением программы. Компилятор же вносит дополнительные этапы компиляции и компоновки программы, в результате чего получается исполняемый файл, недоступный для анализа и редактирования. Однако скомпилированные программы выполняются быстрее, так как перевод команд программы на машинный язык уже произошел на этапе компиляции. Еще одно преимущество компилируемых языков программирования, таких как C++, состоит в том, что полученные программы могут выполняться на компьютерах без компилятора. При работе же с интерпретируемыми языками для выполнения готовой программы нужно обязательно иметь соответствующую программу-интерпретатор. В некоторых языках (например, Visual Basic) роль интерпретатора выполняет динамическая библиотека. Интерпретатором языка Java является виртуальная машина (Virtual Machine, или VM). В качестве виртуальной машины обычно используется браузер (такой как Internet Explorer или Netscape). В течение многих лет основным достоинством программы считалась ее краткость и быстрота выполнения. Программу стремились сделать как можно меньше, поскольку память стоила весьма недешево, да и заинтересованность в высокой скорости выполнения объяснялась высокой стоимостью процессорного времени. Но по мере того как компьютеры становились все портативнее, дешевле (особенно ощутимо дешевела память) и быстрее, приоритеты менялись. Сегодня стоимость рабочего времени программиста намного превышает стоимость большинства компьютеров, используемых в бизнесе. Сейчас большим спросом пользуются профессионально написанные и легко эксплуатируемые программы. Простота эксплуатации означает, что при изменении требований, связанных с решением конкретных задач, программа легко перенастраивается без больших дополнительных затрат. ПрограммыСлово программа используется в двух значениях: для обозначения отдельных блоков команд (или исходного кода), написанных программистом, и для обозначения исполняемого программного продукта как единого целого. Это различие в понятиях может ввести читателя в заблуждение, поэтому постараемся явно определять, что имеется в виду: исходный код или исполняемый продукт. Итак, программу можно определить либо как набор написанных программистом команд, либо как выполняемый на компьютере продукт. Исходный текст программы можно превратить в выполняемую программу двумя способами. В одном случае интерпретаторы переводят исходный код в машинные команды, и компьютер сразу же их выполняет. В качестве альтернативного варианта компиляторы переводят исходный код в исполняемый файл программы, который затем можно использовать самостоятельно. Хотя с интерпретаторами работать легче, большинство серьезных программ создается с использованием компиляторов, поскольку скомпилированный код выполняется намного быстрее. Примером компилируемого языка программирования служит C++. Решение многих проблемС течением времени проблемы, ставящиеся перед программистами, меняются. Двадцать лет назад программы создавались в основном для обработки больших объемов данных. При этом зачастую как те, кто писал программы, так и те, кто их использовал, были профессионалами в компьютерной области знаний. Сегодня многое изменилось. С компьютером нередко работают те, кто даже понятия не имеет о его аппаратном и программном обеспечении. Компьютеры стали инструментом, который используется людьми, больше заинтересованными в решении своих деловых проблем, чем в глубоком освоении компьютера. По иронии судьбы, чтобы облегчить новому поколению пользователей работу с программами, сложность самих этих программ значительно повысилась. Канули в лету те дни, когда пользователи вводили "таинственные знаки" (т.е. команды) в ответ на понятные только посвященным подсказки-приглашения, в результате получая поток "сырых", т.е. совершенно необработанных данных. В современных программах используются высокоорганизованные, дружественные по отношению к пользователю интерфейсы, оснащенные многочисленными окнами, меню, диалоговыми окнами и мириадами визуальных графических средств, с которыми все уже хорошо знакомы. Программы, написанные для поддержки этого нового уровня взаимодействия человека с компьютером, гораздо сложнее написанных всего лишь десять лет назад. С развитием всемирной информационной сети Web для компьютеров началась новая эра проникновения на рынок. Пользователей компьютеров сейчас больше, чем когда бы то ни было, и при этом их претензии чрезвычайно высоки. Даже по прошествии всего нескольких лет с момента выхода первого издания этой книги программы заметно увеличились и усложнились, а необходимость использования методов объектно-ориентированного программирования для решения проблем, ставящихся перед современными программистами, стала просто очевидной. С изменением требований к программированию, претерпели изменение как языки, так и технология написания программ. Хотя в истории эволюции программирования есть много интересного, в этой книге мы остановимся на переходе от процедурного программирования к объектно-ориентированному. Процедурное, структурированное и объектно-ориентированное программированиеДо недавних пор программы рассматривались как последовательности процедур, выполнявших некоторые действия над данными. Процедура, или функция, представляет собой набор определенных команд, выполняемых друг за другом. Данные были отделены от процедур, и главным в программировании было проследить, какая функция, какую вызывает и какие данные при этом меняются. Чтобы внести ясность в эту потенциально запутанную ситуацию, были разработаны принципы структурированного программирования. Основная идея структурированного программирования вполне соответствует принципу "разделяй и властвуй". Компьютерную программу можно представить состоящей из набора задач. Любая задача, которая слишком сложна для простого описания, должна быть разделена на несколько более мелких составных задач, и это деление необходимо продолжать до тех пор, пока задачи не станут достаточно простыми для понимания. В качестве примера возьмем вычисление средней заработной платы всех служащих компании. Это не такая уж простая задача. Однако ее можно разбить на ряд подзадач. 1. Выясняем, сколько зарабатывает каждый служащий. 2. Подсчитываем количество людей в компании. 3. Суммируем все зарплаты. 4. Делим суммарную зарплату на количество служащих в компании. Суммирование зарплат тоже можно разделить на несколько этапов. 1. Читаем запись каждого служащего. 2. Получаем доступ к информации о зарплате. 3. Прибавляем очередное значение зарплаты к накопительной сумме. 4. Читаем запись следующего служащего. В свою очередь, операцию чтения записи каждого служащего можно разбить на еще более мелкие подоперации. 1. Открываем файл служащих. 2. Переходим к нужной записи. 3. Считываем данные с диска. Структурированное программирование остается довольно успешным способом решения сложных проблем. Однако к концу 1980-х годов слишком очевидными стали некоторые недостатки структурированного программирования. Во-первых, не было реализовано естественное желание думать о данных (например, записях служащих) и действиях над ними (сортировке, редактировании и т.п.) как о едином целом. Процедурное программирование, наоборот, отделяло структуры данных от функций, которые манипулировали этими данными. Во-вторых, программисты обнаружили, что они постоянно переизобретают новые решения старых проблем. Такая ситуация часто называется изобретением колеса (или велосипеда). Желание иметь возможность многократного использования рутинных блоков, повторяющихся во многих программах, вполне естественно. Это чем-то напоминает сборку приемника из радиодеталей. Конструктору не нужно каждый раз изобретать диоды и транзисторы. Он просто использует стандартные, заранее подготовленные радиодетали. Но для разработчиков программных продуктов такой возможности долгое время не было. Внедрение в практику дружеского пользовательского интерфейса с рамочными окнами, меню и экранными кнопками определило новый подход к программированию. Программы стали выполняться не последовательно от начала до конца, а отдельными блоками в ответ на выполнение некоторого события. При возникновении определенного события (например, щелчка на кнопке или выбора из меню команды) программа должна отреагировать на него соответствующим образом. При таком подходе программы становятся все более интерактивными, что необходимо учитывать при их разработке. Новый термин: Работая с программами прошлого поколения, пользователь вынужден был проходить через последовательность экранов, чтобы добраться до нужного. В современных программах сразу предоставляется возможность выбора из всех предусмотренных разработчиком вариантов и обеспечивается соответствующая реакция на выбор пользователя. Объектно-ориентированное программирование стремится отвечать этим требованиям, предоставляя технологию управления элементами любой сложности, создавая условия для многократного использования программных компонентов и объединяя данные с методами манипуляции ими. Суть объектно-ориентированного программирования состоит в том, чтобы обращаться с данными и процедурами, которые выполняют действия над этими данными, как с единым объектом, т.е. самодостаточным элементом, который в чем-то идентичен другим таким же объектам, но в то же время отличается от них определенными уникальными свойствами. Язык C++ и объектно-ориентированное программированиеВ языке C++ полностью поддерживаются принципы объектно-ориентированного программирования, включая три кита, на которых оно стоит: инкапсуляцию, наследование и полиморфизм. ИнкапсуляцияКогда инженер использует в своей разработке резистор, он не изобретает его заново, а идет на склад (в магазин, к коллеге — возможны варианты) и в соответствии с заданными параметрами подбирает себе нужную деталь. При этом его не очень-то волнует, как устроен данный конкретный резистор, лишь бы он работал в соответствии с заводскими характеристиками. Именно это свойство скрытости или автономности объектов, используемых во внешних конструкциях, называется инкапсуляцией. С помощью инкапсуляции можно обеспечить сокрытие данных. Это очень важная характеристика, благодаря которой пользователь может не задумываться о внутренней работе используемого объекта. Подобно тому, как использование холодильника не требует знаний о принципах работы рефрижератора, применение хорошо разработанного программного объекта позволяет не заботиться о взаимоотношениях его внутренних переменных-членов. Еще раз приведем аналогию: для эффективного использования резистора инженеру совсем не обязательно знать принципы его работы и внутреннее устройство. Все свойства резистора инкапсулированы (т.е. скрыты) в самом резисторе, важно только, чтобы он правильно справлялся со своими функциями. В языке C++ свойство инкапсуляции поддерживается посредством создания нестандартных (пользовательских) типов данных, называемых классами. О том, как создаются классы, вы узнаете на занятии 6. После создания хорошо определенный класс действует как полностью инкапсулированный объект, т.е. его можно использовать в качестве целого программного модуля. Настоящая же внутренняя работа класса должна быть скрыта. Пользователям хорошо определенного класса не нужно знать, как этот класс работает; им нужно знать только одно — как его использовать. Наследование и многократное использованиеКогда инженеры из компании Acme Motors решили сконструировать новый автомобиль, у них было два варианта: они могли начать с нуля или модифицировать существующую модель Star. Возможно, эта модель почти идеальна, но хотелось бы добавить турбокомпрессор и шестискоростную передачу. Главный инженер выбрал второй вариант, т.е. не начинать с нуля, а построить другую модель автомобиля Star, усовершенствовав ее за счет дополнительных возможностей. При этом он предложил назвать новую модель Quasar, Quasar — это разновидность модели Star, но оснащенная новыми деталями. Язык C++ поддерживает наследование. Это значит, что можно объявить новый тип данных (класс), который является расширением существующего. Об этом новом подклассе говорят, что он унаследован от существующего класса, и называют его производным. Модель Quasar произведена от модели Star, и поэтому она наследует все ее качества, но при необходимости может их расширить. О наследовании и его применении в языке C++ речь пойдет на занятиях 11 и 15. ПолиморфизмНовая модель Quasar, в отличие от Star, может по-другому реагировать на нажатие акселератора. В модели Quasar можно добавить инжекторную систему впрыскивания топлива в двигатель и турбокомпрессор вместо использования карбюратора в модели Star. Однако пользователю не обязательно знать об этих отличиях. Он может просто надавить на газ и ожидать соответствующей реакции автомобиля, за рулем которого он сидит. Язык C++ поддерживает возможность вносить изменения в выполнение одноименных функций для разных объектов благодаря так называемому полиморфизму функций и классов. Поли означает много, морфе — форма, следовательно, полиморфизм означает многообразие форм. Подробно это понятие рассматривается на занятиях 10 и 13. Эволюция языка C++Когда назрела идея объектно-ориентированного анализа, проектирования и программирования, Бьярн Страуструп (Bjarne Stroustrup) взял язык С (наиболее популярный для разработки коммерческих программных продуктов) и расширил его, обогатив средствами, необходимыми для объектно-ориентированного программирования. Хотя язык C++ справедливо называют продолжением С и любая работоспособная программа на языке С будет поддерживаться компилятором C++, при переходе от С к C++ был сделан весьма существенный скачок. Язык C++ выигрывал от своего родства с языком С в течение многих лет, поскольку программисты могли легко перейти от С к использованию C++. Однако многие программисты обнаружили, что для того, чтобы в полной мере воспользоваться преимуществами языка C++, им нужно отказаться от некоторых своих прежних знаний и приобрести новые, а именно: изучить новый способ концептуализации и решения проблем программирования. Нужно ли сначала изучить язык CУ многих возникает вопрос: "Поскольку C++ является продолжением языка С, нужно ли сначала осваивать С?" Страуструп и большинство других программистов, использующих C++, считают, что это не только не нужно, но гораздо лучше этого вовсе не делать. Эта книга не предполагает наличия у читателя предварительного опыта программирования. Но если вы знакомы с программированием на С, первые пять глав вам достаточно лишь просмотреть. Только начиная с занятия 6, мы приступим к настоящей разработке объектно-ориентированных программ. C++ и JavaC++ в настоящее время считается господствующим языком, используемым для разработки коммерческих программных продуктов. В последние годы это господство слегка заколебалось благодаря аналогичным претензиям со стороны такого языка программирования, как Java, но маятник общественного мнения качнулся в другую сторону, и многие программисты, которые бросили C++ ради Java, в последнее время поспешили вернуться к своей прежней привязанности. В любом случае эти два языка так похожи, что изучив один из них, вы на 90% освоите другой. Стандарт ANSIАккредитованный комитет стандартов (Accredited Standards Committee), действующий под руководством Американского национального института стандартов (American National Standards Institute — ANSI), создал международный стандарт для языка C++. Стандарт C++ также именуется в настоящее время как ISO — International Standards Organization (Международная организация по стандартизации). Кроме того, когда говорят о стандарте языка C++, иногда имеют в виду или NCITS (National Committee for Information Technology Standards — Национальный комитет по стандартам на информационные технологии), или X3 (старое название комитета NCITS), или ANSI/ISO. В этой книге мы будем придерживаться стандарта ANSI, поскольку это наиболее популярный термин. Примечание: Аббревиатура ANSI обычно произносится как "анси". Стандарт ANSI — это попытка гарантировать, что язык C++ будет аппаратно независимым (т.е. переносимым с компьютера на компьютер). Это значит, что программа, написанная в соответствии со стандартом ANSI для компилятора компании Microsoft, будет компилироваться без ошибок с использованием компилятора другого производителя. Более того, поскольку приведенные в этой книге программы являются ANSI- совместимыми, они должны компилироваться без ошибок на компьютерах, работающих на платформах Mac, Windows или Alpha. Для большинства изучающих язык C++ стандарт ANSI остается прозрачным. Тем не менее соответствие программного продукта общепринятым стандартам ANSI важно для профессиональных программистов. Мы позаботились о том, чтобы все программы, вошедшие в эту книгу, были ANSI-совместимыми. Подготовка к программированиюЯзык C++, возможно, больше любого другого требует от программиста до написания программы провести подготовительный этап, заключающийся в ее проектировании. При решении тривиальных проблем, рассматриваемых в первых нескольких главах этой книги, можно обойтись и без затрат на проектирование. Однако сложные проблемы, с которыми профессиональные программисты сталкиваются в реальной жизни чуть ли не каждый день, действительно требуют предварительного проектирования, и чем тщательнее оно будет проведено, тем более вероятно, что программа сможет их решить, причем с минимальными затратами времени и денежных средств. При добросовестно проведенном проектировании создается программа, которую легко отладить и изменять в будущем. Было подсчитано, что около 90% стоимости программного продукта составляет стоимость отладки и настройки. Удачно выполненное проектирование может значительно уменьшить эти расходы, а значит, и стоимость проекта в целом. Первый вопрос, который нужно задать при подготовке к проектированию любой программы, звучит примерно так: "Какую проблему я хочу решить?" Каждая программа должна иметь четкую, ясно сформулированную цель, и вы увидите, что это относится даже к простейшим программам, приведенным в этой книге. Второй вопрос каждый уважающий себя программист поставит следующим образом: "Можно ли решить эту проблему с использованием уже имеющихся программных продуктов, т.е. не изобретая собственного колеса?" Может быть, для решения этой проблемы достаточно воспользоваться своей старой программой, ручкой и бумагой или купить у кого-то уже готовую программу? Часто такое решение может оказаться лучше, чем создание абсолютно новой программы. Программист, предлагающий такую альтернативу, никогда не пострадает от отсутствия работы: умение находить экономные решения проблем обеспечит ему популярность в будущем. Уяснив проблему и придя к выводу, что она требует написания абсолютно новой программы, вы будете готовы к этапу проектирования. Создание любого коммерческого приложения требует тщательного анализа проблемы и проектирования ее эффективного решения. Хотя эти этапы логически предваряют этап написания программы, все же лучше начать с изучения базового синтаксиса и семантики языка C++ еще до изучения методов формального анализа и проектирования. Среда разработкиВ этой книге предполагается, что в вашем компиляторе предусмотрен режим работы с командной для непосредственного ввода данных, минуя графический интерфейс таких систем, как Windows или Macintosh. Найдите опцию console или easy window либо обратитесь к документации, прилагаемой к компилятору. Возможно, ваш компилятор имеет собственный встроенный текстовый редактор либо вы можете использовать любой коммерческий текстовый редактор, сохраняющий файлы в текстовом формате без атрибутов форматирования. Примерами таких редакторов могут служить Windows Notepad, команда DOS Edit, Brief, Epsilon, EMACS и vi. Такие коммерческие текстовые процессоры, как WordPerfect, Word и многие другие, также позволяют сохранять файлы в текстовом формате. Файлы, создаваемые с помощью текстовых редакторов, называются файлами источников. Они обычно имеют расширение .cpp, .cp или .с. В этой книге файлы, содержащие листинги программ, имеют расширение . cpp, но всегда лучше просмотреть документацию компилятора, с которым вы собираетесь работать, и выяснить его предпочтения. Примечание: Для большинства компиляторов C++ неважно, какое расширение имеет файл, содержащий исходный текст программы, хотя многие из них по умолчанию используют расширение .cpp. Однако будьте внимательны: некоторые компиляторы рассматривают файлы с расширением . с как программы на языке С, а файлы с расширением . cpp как программы на языке C++. Так что работу с компилятором всегда лучше начать с чтения документации. Компиляция исходного кода программыХотя исходный текст программы, содержащийся в вашем файле, не будет понятен каждому, кто в него заглянет (особенно тем, кто незнаком с языком C++), все же он представлен в таком виде, который может быть воспринят человеком. Файл с исходным текстом программы — это еще не программа, и его нельзя выполнить или запустить. Рекомендуется: Используйте для написания исходного текста программы простой текстовый редактор или редактор, встроенный в компилятор. Сохраняйте свои файлы с расширением .сpp, .cp или .c. Обращайтесь к документации компилятора и компоновщика, чтобы быть уверенным в правильном компилировании и компоновке программы. Не рекомендуется: Не используйте текстовый процессор, который сохраняет форматированный текст. Если вам все-таки приходится обращаться к нему, сохраняйте файлы как текст ASCII. Чтобы превратить исходный текст в программу, используется компилятор. Каким образом вызвать компилятор и как сообщить ему о местонахождении исходного текста программы, зависит от конкретного компилятора, поэтому вновь нужно заглянуть в документацию. После завершения компиляции исходного кода создается объектный файл. Этот файл обычно имеет расширение ,obj. Но это еще не выполняемая программа. Для превращения объектного файла в исполняемый нужно запустить программу компоновки. Создание исполняемого файла с помощью компоновщикаПрограммы на языке C++ обычно создаются путем компоновки одного или нескольких объектных файлов (файлов .obj) с одной или несколькими библиотеками. Библиотекой называется коллекция компонуемых файлов, которые либо поставляются вместе с компилятором, либо приобретаются отдельно, либо создаются и компилируются самим программистом. Все компиляторы C++ поставляются с библиотекой функций (или процедур) и классов, которые можно включить в программу. Функция — это программный блок, который выполняет некоторые служебные действия, например складывает два числа или выводит информацию на экран. Класс можно рассматривать как коллекцию данных и связанных с ними функций. О функциях и классах речь впереди (см. занятия 5 и 6). Итак, чтобы создать исполняемый файл, нужно выполнить перечисленные ниже действия. 1. Создать файл с исходным текстом программы, который будет иметь расширение cpp. 2. Скомпилировать исходный код и получить объектный файл с расширением .obj. 3. Скомпоновать файл .obj с необходимыми библиотеками с целью создания исполняемого файла программы. Цикл разработкиЕсли бы каждая программа заработала должным образом с первой попытки, можно было бы говорить о завершении цикла разработки: написание программы, компиляция исходного кода, компоновка программы и ее выполнение. К сожалению, почти все программы (тривиальные и не очень) содержат ошибки. Одни ошибки обнаружит компилятор, другие — компоновщик, а третьи проявятся только при запуске программы в работу. Любая ошибка должна быть исправлена, и для этого нужно отредактировать исходный текст программы, перекомпилировать его и перекомпоновать, а затем снова выполнить. Этот цикл разработки представлен на рис. 1.1. Первая программа на языке C++ — HELLO.cppТрадиционно в книгах по программированию первые примеры программ начинаются с вывода на зкран слов Hello World или какой-нибудь вариации на тему. В этой книге мы следовали устоявшимся традициям. Введите первую программу с помошью текстового редактора, в точности повторяя все нюансы, Завершив ввод, сохраните файл, скомпилируйте его, скомпонуйте и выполните. Программа должна вывести на экран слова Hello World. Пока не стоит задумываться о том, как работает эта программа. Вы должны получить удовлетворение просто от того, что прошли полный цикл разработки. Все аспекты этой программы будут подробно рассмотрены на следующих занятиях. Примечание: В приведенном ниже листинге слева содержатся номера строк. Эти номера установлены лишь для ссылки в тексте книги на соответствующие строки программы. Их не нужно вводить в окно редактора, Например, в первой строке листинга 1.1 вы должны ввести: #include <iostream.h> Листинг 1.1. Файл HELLO.cpp — программа приветствия. 1: #include <iostream.h> 2: 3: int main() 4: { 5: cout << "Hello World!\n" 6: return 0; 7: } Убедитесь в том, что введенный вами текст программы совпадает с содержимым приведенного здесь листинга. Обратите внимание на знаки препинания. Символ << в строке 5 является оператором перенаправления потока данных. Эти символы на большинстве клавиатур вводятся путем нажатия клавиши <Shift> и двойного нажатия клавиши с запятой. Строка 5 завершается точкой с запятой (;). Не пропустите этот символ завершения строки программного кода! Рис. 1.1. Этапы разработки программы на языке C++ Кроме того, убедитесь, что вы корректно работаете со своим компилятором. Большинство компиляторов переходит к компоновке автоматически, но все-таки стоит свериться с документацией. Если вы получите какие-нибудь сообщения об ошибках, просмотрите внимательно текст своей программы и найдите отличия от варианта, приведенного в книге. Если вы увидите сообщение об ошибке со ссылкой на строку 1, уведомляющее о невозможности найти файл iostream.h (cannot find file iostream.h), обратитесь к документации за указаниями об установке пути для включаемых файлов или переменных окружения. Если вы получите сообщение об ошибке, уведомляющее об отсутствии прототипа для функции main, добавьте строку int main(); сразу перед строкой 3. В этом случае вам придется добавлять эту строку до начала функции main в каждой программе, приведенной в этой книге. Большинство компиляторов не требует наличия прототипа для функции main, но вполне возможно, что именно вам достался компилятор из другой компании. Один из возможных вариантов программы будет выглядеть следующим образом: 1: #include <iostream.h> 2: int main(); // большинство компиляторов не требует этой строки 3: int main() 4: { 5: cout << "Hello World!\n" 6: return 0; 7: } Попробуйте выполнить программу HELLO.exe. Если все правильно, вы должны увидеть на экране приветствие: Hello world! Использование стандартных библиотек Чтобы гарантировать, что все наши читатели, работающие со старыми компиляторами, не будут иметь проблем с программами из этой книги, мы используем старый стиль включения файлов: #include <iostream.h> а не заголовки новых стандартных библиотек: #include <iostream> Такой вариант включения должен работать на всех компиляторах, тем не менее, он имеет ряд недостатков. Если вы предпочитаете использовать новые стандартные библиотеки, просто замените в своей программе строку 1 строкой #include <iostream> и добавьте строку using namespace std; сразу после списка включаемых файлов Нюансы использования пространства имен подробно рассматриваются на занятии 17. Будете вы использовать стандартные заголовочные файлы или нет, программы, приведенные в этой книге, должны работать без каких бы то ни было модификаций. Принципиальное отличие старых библиотек от новых стандартов заключается в использовании библиотеки iostream (см. занятие 16). Но даже эти изменения не должны оказать влияние на программы из этой книги ввиду их незначительности. Кроме того, они выходят за рамки обсуждения круга тем, предусмотренных для начинающих. Примечание: Трудно читать текст программы даже про себя, если не знаешь, как произносить специальные символы и ключевые слова. Советую читать первую строку так: "паунд инклуд (# — символ фунта) ай-оу-стрим-дот (или точка) - эйч". А строку 5 читайте как "си-аут-'Hello world!'". Если увидели, то примите наши поздравления! Вы только что ввели, скомпилировали и запустили свою первую программу на языке C++. Конечно, она не поражает своей грандиозностью, но почти каждый профессиональный программист начинал именно с такой программы. Осваиваем компилятор Visual C++ 6Все программы в этой книге проверены на компиляторе Visual C++ 6.0 и должны прекрасно компилироваться, компоноваться и выполняться при использовании любого компилятора Microsoft Visual C++, по крайней мере, начиная с версии 4.0 и выше, Теоретически, поскольку мы имеем дело с ANSI-совместимым текстом программ, все программы в этой книге должны компилироваться любым ANSI-совместимым компилятором любого производителя, В идеале результаты выполнения программ должны совпадать с приведенными в этой книге, но на практике не всегда так бывает. Чтобы наконец приступить к делу, ознакомьтесь в этом разделе с тем, как редактировать, компилировать, компоновать и выполнять программу, используя компилятор компании Microsoft. Если у вас другой компилятор, на каких-то этапах возможны некоторые отличия. Даже если вы используете компилятор Microsoft Visual C++ 6.0, все равно стоит свериться с документацией и уточнить все детали. Построение проекта приветствияЧтобы создать и протестировать программу приветствия, выполните ряд действий. 1. Запустите компилятор. 2. Выберите из меню File команду New. 3. Выберите опцию Win32 Console Application (Консольное приложение для Win32), введите имя проекта, например Example 1, и щелкните на кнопке ОК. 4. Выберите из меню вариант An Empty Project (Пустой проект) и щелкните на кнопке ОК. 5. Выберите в меню File команду New. 6. Выберите опцию C++ Source File (файл источника C++) и введите имя проекта ex1. 7. Введите текст программы, приведенный выше. 8. Выберите в меню Build команду Build Example1.exe. 9. Убедитесь в отсутствии ошибок компиляции. 10. Нажмите клавиши <Ctrl+F5> для выполнения программы. 11. Нажмите клавишу пробела для завершения программы. Ошибки компиляцииОшибки в процессе компиляции могут возникать по различным причинам. Обычно они являются результатом небрежного ввода и другого рода случайностей. Приличные компиляторы сообщат не только о том, что именно у вас не в порядке, они также укажут точное местоположение обнаруженной ошибки. Самые "продвинутые" компиляторы даже предложат вариант исправления ошибки! В этом можно убедиться, специально сделав ошибку в нашей программе. Давайте удалим в программе HELLO.cpp закрывающую фигурную скобку в строке 7. Ваша программа теперь будет выглядеть так, как показано в листинге 1.2. Перекомпилируйте программу, и вы увидите сообщение об ошибке, которое выглядит примерно следующим образом: Hello.cpp, line 5: Compound statement missing terminating; in function main(). 1: #include <iostream.h> 2: 3: int main() 4: { 5: cout << "Hello world!\n"; 6: return 0; Либо вы можете увидеть такое сообщение об ошибке: F:\Mcp\Tycpp21d\Testing\List0101.cpp(8) : fatal error C1004: unexpected end of file found Error executing cl.exe. В этом сообщении содержится информация о том, где гнездится проблема (указывается имя файла, номер строки и характер проблемы, хотя и в несколько зашифрованном виде). Обратите внимание на то, что в сообщении об ошибке указывается строка 5. Компилятор не уверен в вашем намерении вставить закрывающую фигурную скобку перед или после инструкции, содержащей объект cout. Иногда в сообщениях проблема обрисовывается только в общих чертах. Если бы компилятор мог точно идентифицировать каждую ошибку, то он бы тогда мог сам ее и исправить. РезюмеНадеюсь, прочитав эту главу, вы получили хорошее представление об эволюции языка C++, а также о том, для решения каких проблем он предназначен. У вас не должно остаться сомнений по поводу того, что изучение C++ — правильный выбор для всякого, кто собирается программировать в ближайшие десять лет. В C++ предусмотрены средства объектно-ориентированного программирования, обеспечивающие эффективность языка системного уровня, благодаря чему C++ заслуженно выбирают в качестве языка разработки. Сегодня вы научились вводить, компилировать, компоновать и выполнять свою первую программу на C++ и узнали, что представляет собой цикл разработки. Вы также получили небольшое представление об объектно-ориентированном программировании. Нам предстоит еще не раз коснуться этих тем в течение трех недель. Вопросы и ответыЧто такое текстовый редактор? Текстовый редактор создает и редактирует файлы, содержащие текст. Для написания текстов программ не требуется никаких атрибутов форматирования или специальных символов. Текстовые файлы с листингами программ не обладают такими свойствами, как автоматический перенос слов либо начертание букв полужирным шрифтом или курсивом и т.д. Если мой компилятор имеет встроенный редактор, то обязан ли я использовать его? Почти все компиляторы будут компилировать программы, созданные в любом текстовом редакторе. Однако преимущества использования встроенного текстового редактора состоит в том, что он может быстро переключаться между режимами редактирования и компиляции. Высокоорганизованные компиляторы включают полностью интегрированную среду разработки, позволяя программисту легко получать доступ к справочным файлам, редактировать, компилировать и сразу же исправлять ошибки компиляции и компоновки, не выходя из среды разработки. Могу ли я игнорировать предупреждающие сообщения, поступающие от компилятора? Среди программистов распространено мнение, что на предупреждающие сообщения компилятора можно не обращать внимания, но я придерживаюсь другого мнения. Возьмите за правило реагировать на предупреждения компилятора как на сообщения об ошибках. Компилятор C++ генерирует предупреждающие сообщения в тех случаях, когда, по его мнению, вы делаете то, что не входит в ваши намерения. Внимательно отнеситесь к этим предупреждениям и сделайте все, чтобы они исчезли. Что означает время компиляции? Это время работы вашего компилятора, в отличие от времени компоновки (когда работает компоновщик) или времени выполнения программы (когда выполняется программа). Эти термины придумали программисты, чтобы кратко обозначить временные периоды, в течение которых обычно и проявляются различные ошибки. КоллоквиумВ этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний и приводится несколько упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из предложенных ниже вопросов. Контрольные вопросы1. В чем разница между интерпретатором и компилятором? 2. Как происходит компиляция исходного кода программы? 3. В чем состоит назначение компоновщика? 4. Какова обычная последовательность действий в цикле разработки? Упражнения1. Просмотрите следующую программу и попытайтесь понять, что она делает, не запуская ее на выполнение. 1: #include <iostream.h> 2: int main() 3: { 4: int x = 5; 5: int у = 7; 6: cout << "\n"; 7: cout << x + у << " " << x * у; 8: cout << "\n"; 9: return 0; 10: } 2. Введите программу из упражнения 1, а затем скомпилируйте и запустите ее. Что она делает? Так ли вы все это предполагали? 3. Введите следующую программу и скомпилируйте ее. Какие сообщения об ошибках вы получили? 1: include <iostream.h> 2: int main() 3: { 4: cout << "Hello World\n"; 5: return 0; 6: } 4. Исправьте ошибку в программе из упражнения 3, а затем перекомпилируйте, скомпонуйте и выполните ее. Что делает эта программа? День 2-й. Составные части программы на языке C++Программы на языке C++ состоят из объектов, функций, переменных и других элементов. Большая часть этой книги посвящена подробному описанию каждого из них, но, для того чтобы получить представление о слаженной работе всех этих элементов, нужно рассмотреть какую-нибудь законченную рабочую программу. Сегодня вы узнаете: • Из каких частей состоят программы на языке C++ • Как эти части взаимодействуют друг с другом • Что такое функция и каково ее назначение Простая программа на языке C++Даже простенькая программа HELLO.CPP, приведенная на занятии 1, состоит из нескольких элементов, которые представляют для нас интерес. В этом разделе упомянутая программа рассматривается более подробно. В листинге 2.1 ради удобства обсуждения приведена оригинальная версия файла HELLO.CPP. Листинг 2.1. Демонстрация частей программы C++ на примере программы HELLO. CPP 1: #include <iostream.h> 2: 3: int main() 4: { 5: cout << "Hello World!\n"; 6: return 0; 7: } Результат: Hello World! АНАЛИЗ: В строке 1 выполняется включение файла iostream.h в текущий файл. Первым в программе стоит символ #, который служит сигналом для препроцессора. При каждом запуске компилятора запускается и препроцессор. Он читает исходный текст программы, находит строки, которые начинаются с символа фунта (#), и работает с этими строками до того, как начнется компиляция программы. Подробнее работа препроцессора рассматривается на занятии 21. Include — это команда препроцессору, которую можно расшифровать следующим образом: "За именем команды следует имя файла. Нужно найти этот файл и вставить его содержимое прямо в это место программы". Угловые скобки, в которые заключено имя файла, означают, что этот файл нужно искать во всех папках, отведенных для хранения подобных файлов. Если ваш компилятор настроен корректно, то угловые скобки укажут препроцессору на то, что файл iostream.h следует искать в папке, содержащей все файлы с расширением .h, предназначенные для вашего компилятора. Файл iostream.h (input-output-stream — поток ввода-вывода) используется объектом cout, который обслуживает процесс вывода данных на экран. После выполнения строки 1 файл iostream.h будет включен в эту программу, таким образом, как если бы вы собственноручно ввели сюда его содержимое, Препроцессор запускается перед компилятором и выполняет все строки, начинающиеся с символа (#), подготавливая код программы к компиляции. Основной код программы начинается в строке 3 с вызова функции main(). Каждая программа на языке C++ содержит функцию main(). Функция — это блок программы, который выполняет одно или несколько действий. Обычно функции вызываются другими функциями, но main() — особая функция: она вызывается автоматически при запуске программы. Функция main(), подобно всем другим функциям, должна объявить тип возвращаемого значения. В программе HELLO.CPP функция main() возвращает значение типа int (от слова integer — целый), а это значит, что по окончании работы эта функция возвратит операционной системе целочисленное значение. В данном случае будет возвращено целое значение 0, как показано в строке 6. Возвращение значения в операционную систему не столь важно, и в общем-то это значение самой системой никак не используется, но стандарт языка C++ требует, чтобы функция main() была объявлена по всем правилам (как показано в этом листинге). Примечание: Некоторые компиляторы позволяют объявить функцию main() таким образом, чтобы она возвращала значение типа void. Этого больше нельзя делать в C++, поэтому вам следует избавляться от старых привычек. Позвольте функции main() возвращать значения типа int и ради этого поместите в последней строке этой функции выражение return 0;. Примечание: В некоторых операционных системах предусмотрена возможность проверки значения, возвращаемого программой. Удобно возвращать значение 0 как флаг нормального завершения функции. Все функции начинаются открывающей фигурной скобкой ({) и оканчиваются закрывающей фигурной скобкой (}). Фигурные скобки функции main() помешены в строках 4 и 7. Все, что находится между открывающей и закрывающей фигурными скобками, считается телом функции. Вся функциональность нашей простейшей программы заключена в строке 5. Объект cout используется для вывода сообщений на экран. Об объектах пойдет речь на занятии 6, а объект cout и близкий ему объект cin будут подробно рассмотрены на занятии 16. Эти два объекта, cin и cout, используются в языке C++ для организации соответственно ввода данных (например, с клавиатуры) и их вывода (например, на экран). Вот как используется объект cout: вводим слово cout, за которым ставим оператор перенаправления выходного потока << (далее будем называть его оператором вывода). Все, что следует за этим оператором, будет выводиться на экран. Если вы хотите вывести на экран строку текста, не забудьте заключить ее в двойные кавычки ("), как показано в строке 5. Строка текста — это набор печатаемых символов. Два заключительных символа текстовой строки (\n) означают, что после слов Hello world! нужно выполнить переход на новую строку. Этот специальный код подробно объясняется при рассмотрении объекта cout на занятии 17. Функция main() оканчивается в строке 7. Кратко об объекте coutНа занятии 16 вы узнаете, как использовать объект cout для вывода данных на экран. А пока, не вдаваясь в детали использования объекта cout, скажем, что для вывода значения на экран нужно ввести слово cout, а за ним оператор вывода (<<), который состоит из двух символов "меньше" (<). Несмотря на то что вы вводите два символа, компилятор C++ воспринимает их как один оператор. Листинг 2.2. Использование обьекта cout 1: // Листинг 2.2. Использование объекта cout 2: #include <iostream.h> 3: int main() 4: { 5: cout << "Hello there.\n"; 6: cout << "Here is 5: " << 5 << "\n"; 7: cout << "The manipulator endl writes а new line to the screen."; 8: cout << 9: endl; 10: cout << "Here is a very big number:\t" << 70000 << endl; 11: cout << "Here is the sum of 8 and 5:\t" << 8+5 << endl; 12: cout <<'"Here's a fraction:\t\t" << (float) 5/8 << endl; 13: cout << "And a very very big number:\t"; 14: cout << (double) 7000 * 7000 << 15: endl; 16: cout << "Don't forget to replace Jesse Liberty with your name...\n"; 17: cout << "Jesse Liberty is a C++ programmer!\n"; 18: return 0; 19: } За символом вывода укажите выводимые данные. Использование объекта cout показано в листинге 2.2. Введите текст этой программы в точности так, как написано, за исключением одного: вместо имени Jesse Liberty подставьте свои имя и фамилию, лучше латинскими буквами. Результат: Hello there. Here is 5: 5 The manipulator endl writes а new line to the screen. Here is a very big number: 70000 Here is the sum of 8 and 5: 13 Here's a fraction: 0.625 And a very very big number: 4.9e+07 Don't forget to replace Jesse Liberty with your name... Jesse Liberty is a C++ programmer! Примечание:Некоторые компиляторы требуют, чтобы математические операции в случае использования после объекта cout заключались в круглые скобки. В этом случае строку 11 пришлось бы изменить следующим образом: 11: cout << "Here is the sum of 8 and 5:\t" << (8+5) << endl; В строке 2 по команде #include <iostream.h> препроцессор вставляет содержимое файла iostream.h в исходный текст программы. Включать файл iostream.h необходимо, если в программе используется объект cout и связанные с ним функции-члены. В строке 5 демонстрируется простейший вариант использования объекта cout: вывод строки символов. Символ \n — это специальный символ форматирования, который указывает объекту cout на необходимость вывода на экран символа новой строки (он произносится "слэш-эн" или просто разрыв строки). В строке 6 объекту cout передаются три значения, и каждое из них отделяется оператором вывода. Первое значение представляет собой строку "Here is 5: ". Обратите внимание на наличие пробела после двоеточия: пробел является частью текстовой строки. Затем объекту cout с помощью оператора вывода передается значение 5, а за ним — символ разрыва строки (этот символ всегда должен быть заключен в двойные или в одинарные кавычки). При выполнении этого выражения на экране появится строка Here is 5: 5 Поскольку после первого значения нет символа разрыва строки, следующее значение выводится сразу за предыдущим. Этот процесс называется конкатенацией двух значений. В строке 7 на экран выводится информационное сообщение, после чего используется оператор endl. Этот оператор также выводит на экран символ разрыва строки. (Другое назначение оператора endl рассматриваются на занятии 16.) Примечание:Оператор endl расшифровывается как end line (конец строки) и читается как "энд-эл", а не "энд-один" (иногда букву l принимают за единицу). В строке 10 используется еще один символ форматирования — \t, который вставляет символ табуляции, используемый обычно для выравнивания выводимой информации (строки 10-13). Строка 10 демонстрирует возможность вывода значений типа long int. В строке 11 показано, что объект cout может выводить результат математической операции. Объекту cout передается не значение, а целое математическое выражение 8+5, но на экран выводится число 13. В строке 12 объект cout выводит результат другой математической операции — 5/8. Идентификатор (float) указывает объекту cout, что результат должен выводиться как дробное число. В строке 14 объекту cout передается выражение 7000 * 7000, а идентификатор (double) устанавливает вывод результата в экспоненциальном представлении. Использование идентификаторов double и float для установки типов значений рассматривается на занятии 3. В строке 16 нужно вставить свое имя. Если при выполнении программы вы увидите свое имя на экране, шансы стать профессиональным программистом у вас существенно возрастут, хотя в этом и так нет никаких сомнений. Даже компьютер это знает! КомментарииКогда вы пишете программу, вам всегда ясно, что вы стараетесь сделать. Однако если через месяц вам придется вернуться к этой программе, то, как это ни удивительно, окажется, что вы почти совсем не помните, о чем идет речь, а о деталях и говорить не приходится. Чтобы не казнить себя за пробелы в памяти и помочь другим понять вашу программу, используйте комментарии. Комментарии представляют собой текст, который игнорируется компилятором, но позволяет описать прямо в программе назначение отдельной строки или целого блока. Виды комментариевВ языке C++ используется два вида комментариев: с двойным слешем (//) и сочетанием слеша и звездочки (/*). Комментарий с двойным слешем (его называют комментарием в стиле C++) велит компилятору игнорировать все, что следует за этими символами вплоть до конца текущей строки. Комментарий со слешем и звездочкой (его называют комментарием в стиле С) велит компилятору игнорировать все, что следует за символами (/*) до того момента, пока не встретится символ завершения комментария: звездочка и слеш (*/). Каждой открывающей паре символов /* должна соответствовать закрывающая пара символов */. Нетрудно догадаться, что комментарии в стиле С используются также и в языке С, но следует предупредить, что двойной слеш в языке С не воспринимается как символ комментария. При программировании на C++ для выделения комментариев в основном используются символы двойного слеша, а комментарии в стиле С используются только для временного отключения больших блоков программы. Впрочем, двойной слеш часто используется и для временного отключения отдельных строк программного кода. Использование комментариевРаньше считалось хорошим тоном предварять блоки функций и саму программу комментариями, из которых должно было быть понятно, что делает эта функция и какое значение она возвращает. Исходя из собственного опыта, могу сказать, что такие комментарии не всегда целесообразны. Комментарии в заголовке программы очень быстро устаревают, поскольку практически никто их не обновляет при обновлении текста программы. Функции должны иметь такие имена, чтобы у вас не оставалось ни тени сомнения в том, что они делают, в противном случае имя функции нужно изменить. Зачем использовать бессмысленные и труднопроизносимые имена, чтобы потом раскрывать их смысл с помощью комментариев? Впрочем, одно другому не помеха. Лучше всего использовать понятные имена и дополнительно вносить краткие разъяснения с помощью комментариев. Листинг 2.3 демонстрирует использование комментариев, доказывая, что они не влияют на выполнение программы и ее результаты. Листинг 2.3. Демонстрация комментариев на примере программы HELLO.CPP 1: #include <iostream.h> 2: 3: int main() 4: { 5: /* это комментарий, 6: который продолжается до тех пор, пока не 7: встретится символ конца комментария в виде звездочки и слэша */ 8: cout << "Hello world!\n"; 9: // Этот комментарий оканчивается в конце строки 10: cout << "That comment ended!\n"; 11: 12: // после двойного слеша может не быть никакого текста, 13: /* как, впрочем, и между этими символами */ 14: return 0; 15: } Резултат: Hello world! That comment ended! Комментарии в строках 5—7 полностью игнорируются компилятором, как и комментарии в строках 9, 12 и 13. Комментарий в строке 9 завершается в конце этой строки, но для завершения комментариев, начавшихся в строках 5 и 13, требуется символ окончания комментария (*/). Напоследок предупреждение: осторожнее с комментариями!В комментариях, разъясняющих очевидные места, проку немного. Но они могут даже вводить в заблуждение, если при изменения текста программы вы забудете их скорректировать. Однако очевидность — понятие относительное. То, что очевидно для одного человека, может быть непонятно другому. Всегда старайтесь разумно комментировать свои действия и не забывайте обновлять комментарии при обновлении программы. И последнее, комментарии должны разъяснять, не что это за операторы, а для чего они тут используются. ФункцииВы уже познакомились с функцией main(), правда, это необычная, единственная в своем роде функция. Чтобы приносить пользу, функция должна быть вызвана во время сеанса работы программы. Функция main() вызывается не программой, а операционной системой. Программа выполняется по строкам в порядке их расположения в исходном тексте до тех пор, пока не встретится вызов какой-нибудь функции. Затем управление передается строкам этой функции. После выполнения функции управление возвращается той строке программы, которая следует сразу за вызовом функции. Есть прекрасная аналогия для работы программы с функцией. Например, если во время рисования у вас ломается карандаш, вы прекращаете рисовать и затачиваете его. После этого вы возвращаетесь к тому месту рисунка, где сломался карандаш. Когда программа нуждается в выполнении некоторой сервисной операции, вызывается функция, ответственная за выполнение этой операции, после чего программа продолжает свою работу с того места, где была вызвана функция. Эта идея демонстрируется в листинге 2.4. Листинг 2.4. Пример вызова функции 1: #include <iostream.h> 3: // Функция Demonstration Function 4: // выводит на экран информативное сообщение 5: void DemonstrationFunction() 6: { 7: cout << "In DemonstrationFunction\n"; 8: } 10: // Функция main выводит сообщение, затем 11: // вызывает функцию DemonstrationFunction и 12: // выводит на экран второе сообщение. 13: int main() 14: { 15: cout << "In main\n" ; 16: DemonstrationFunction(); 17: cout << " Back in main\n"; 18: return 0; 19: } Результат: In main In DemonstrationFunction Back in main В строках 5—8 определяется функция DemonstrationFunction(). Она выводит на экран сообщение и возвращает управление программе. Функция main() начинается в строке 13, и в строке 15 выводится на экран сообщение, уведомляющее о том, что сейчас управление программой находится в функции main(). После вывода этого сообщения в строке 16 вызывается функция DemonstrationFunction(). В результате этого вызова выполняются команды, содержащиеся в функции DemonstrationFunction(). В данном случае вся функция состоит из одной команды, содержащейся в строке 7, которая выводит другое сообщение. По завершении выполнения функции DemonstrationFunction() (строка 8) управление программой возвращается туда, откуда эта функция была вызвана. В данном случае выполнение программы продолжается со строки 17, в которой функция main() выводит на экран заключительное сообщение. Использование функцийФункции возвращают либо некоторое реальное значение, либо значение типа void, т.е. ничего не возвращают. Функцию, которая складывает два целых числа и возвращает значение суммы, следует определить как возвращающую целочисленное значение. Функции, которая только выводит сообщение, возвращать нечего, поэтому для нее задается тип возврата void. Функции состоят из заголовка и тела. Заголовок содержит установки типа возвращаемого значения, имени и параметров функции. Параметры позволяют передавать в функцию значения. Следовательно, если функция предназначена для сложения двух чисел, то эти числа необходимо передать в функцию как параметры. Вот как будет выглядеть заголовок такой функции: int Sum(int а, int b) Параметр — это объявление типа данных значения, передаваемого в функцию. Реальное значение, передаваемое при вызове функции, называется аргументом. Многие программисты используют эти два понятия как синонимы. Другие считают смешение этих терминов признаком непрофессионализма. Возможно, это и так, но в данной книге эти термины взаимозаменяемы. Тело функции начинается открывающей фигурной скобкой и содержит ряд строк (хотя тело функции может быть даже нулевым), за которыми следует закрывающая фигурная скобка. Назначение функции определяется содержащимися в ней строками программного кода. Функция может возвращать значение в программу с помощью оператора возврата (return). Этот оператор также означает выход из функции. Если не поместить в функцию оператор возврата, то по завершении функции автоматически возвращается значение типа void. Значение, возвращаемое функцией, должно иметь тип, объявленный в заголовке функции. Примечание: Более подробно функции рассматриваются на занятии 5; типы значений, возвращаемых функциями, — на занятии 3. Информация, представленная на этом занятии, является хотя и обзорной, но вполне достаточной для усвоения последующего материала, поскольку функции будут использоваться практически во всех программах, представленных в этой книге. В листинге 2.5 демонстрируется функция, которая принимает два целочисленных параметра и возвращает целочисленное значение. Не беспокойтесь пока насчет синтаксиса или особенностей работы с целыми значениями (например, int x): эта тема подробно раскрывается на занятии 3. Листинг 2.5. Пример использования простой функции (FUNC.CPP) 1: #include <iostream.h> 2: int Add (int x, int у) 3: { 4: 5: cout << "In Add(), received " << x << " and " << у << "\n"; 6: return (x+y); 7: } 8: 9: int main() 10: { 11: cout << "I'm in main()!\n"; 12: int а, b, с; 13: cout << "Enter two numbers: "; 14: cin >> а; 15: cin >> b; 16: cout << "\nCalling Add()\n"; 17: c=Add(a,b); 18: cout << "\nBack in main().\n"; 19: cout << "с was set to " << с; 20: cout << "\nExiting...\n\n"; 21: return 0; 22: } Результат: I'm in main()! Enter two numbers: 3 5 Calling Add() In Add(), received 3 and 5 Back in main(). c was set to 8 Exiting... АНАЛИЗ: Функция Add() определена в строке 2. Она принимает два целочисленных параметра и возвращает целочисленное значение. Сама же программа начинается в строке 9, выводя на экран первое сообщение. Затем пользователю предлагается ввести два числа (строки 13—15). Пользователь вводит числа, разделяя их пробелом, а затем нажимает, клавишу <Enter>. В строке 17 функция main() передает функции Add() в качестве аргументов два числа, введенные пользователем. Управление программой переходит к функции Add(), которая начинается в строке 2. Параметры а и b выводятся на экран, а затем складываются. Результат функции возвращается в строке 6, и на этом функция завершает свою работу. РезюмеСложность изучения такого предмета, как программирование, состоит в следующем: большая часть изучаемого вами материала во многом зависит от того, что вам еще только предстоит изучить. На этом занятии вы познакомились с основными составляющими частями простой программы на языке C++. Кроме того, вы получили представление о цикле разработки и узнали несколько важных терминов. Вопросы и ответыКакую роль выполняет директива #include? Это команда для препроцессора, который автоматически запускается при вызове компилятора. Данная директива служит для введения содержимого файла, имя которого стоит после директивы, в исходный текст программы. В чем разница между символами комментариев // и /*? Комментарии, выделенные двойным слешем (//), распространяются до конца строки. Комментарии, начинающиеся слешем со звездочкой (/*), продолжаются до тех пор, пока не встретится символ завершения комментария (*/). Помните, что даже конец функции не завершит действие комментария, начавшегося с пары символов (/*). Если вы забудете установить завершение комментария (*/), то получите сообщение об ошибке во время компиляции. В чем разница между хорошими и плохими комментариями? Хороший комментарий сообщит читателю, почему здесь используются именно эти операторы, или объяснит назначение данного блока программы. Плохой комментарий констатирует то, что делается в данной строке программы. Программа в идеале должна писаться так, чтобы имена переменных и функций говорили сами за себя, а логика выражений была проста и понятна без особых комментариев. КоллоквиумВ этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний и приводится несколько упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из предложенных ниже вопросов. Контрольные вопросы1. В чем разница между компилятором и препроцессором? 2. В чем состоит особенность функции main()? 3. Какие два типа комментариев вы знаете и чем они отличаются друг от друга? 4. Могут ли комментарии быть вложенными? 5. Могут ли комментарии занимать несколько строк? Упражнения1. Напишите программу, которая выводит на экран сообщение I love C++. 2. Напишите самую маленькую программу, которую можно скомпилировать, скомпоновать и выполнить. 3. Жучки: введите эту программу и скомпилируйте ее. Почему она дает сбой? Как ее можно исправить? 1: #include <iostream.h> 2: int main() 3: { 4: cout << Is there а bug here?"; 5: return 0; 6: } 4. Исправьте ошибку в упражнении 3, после чего перекомпилируйте ее, скомпонуйте и запустите на выполнение. День 3-й. Переменные и константыПрограммы должны обладать способностью хранить используемые данные. Для представления и манипуляции этими данными используются переменные и константы. Сегодня вы узнаете: • Как объявлять и определять переменные и константы • Как присваивать значения переменным и использовать их в программе • Как выводить значения переменных на экран Что такое переменнаяВ языке C++ переменные используются для хранения информации. Переменную можно представить себе как ячейку в памяти компьютера, в которой может храниться некоторое значение, доступное для использования в программе. Память компьютера можно рассматривать как ряд ячеек. Все ячейки последовательно пронумерованы. Эти номера называют адресами памяти. Переменная занимает одну или несколько ячеек, в которых можно хранить некоторое значение. Имя переменной (например, MyVariable) можно представить себе как надпись на ячейке памяти, по которой, не зная настоящего адреса памяти, можно ее найти. На рис. 3.1 схематически представлена эта идея. Согласно этому рисунку, переменная MyVariable начинается с ячейки с адресом 103. В зависимости от своего размера, переменная MyVariable может занимать одну или несколько ячеек памяти. Примечание:В ОЗУ обеспечивается произвольный доступ к ячейкам памяти. Запускаемая программа загружается в ОЗУ с дискового файла. Все переменные также хранятся в ОЗУ. Когда программисты говорят о памяти, они обычно имеют в виду ОЗУ. Резервирование памятиПри определении переменной в языке C++ необходимо предоставить компилятору информацию о ее типе, например int, chart или другого типа. Благодаря этой информации компилятору будет известно, сколько места нужно зарезервировать для нее и какого рода значение будут хранить в этой переменной. Каждая ячейка имеет размер в один байт. Если для переменной указанного типа требуется четыре байта, то для нее будет выделено четыре ячейки, т.е. именно по типу переменной (например, int) компилятор судит о том, какой объем памяти (сколько ячеек) нужно зарезервировать для этой переменной. Поскольку для представления значений в компьютерах используются биты и байты и память измеряется тоже в байтах, важно хорошо разбираться в этих понятиях. Более полно эта тема рассматривается в приложении В. Размер целыхДля переменных одних и тех же типов на компьютерах разных марок может выделяться разный объем памяти, в то же время в пределах одного компьютера две переменные одинакового типа всегда будут иметь постоянный размер. Переменная типа char (используемая для хранения символов) чаше всего имеет размер в один байт. Примечание: Не прекращаются споры о произношении имени типа char. Одни произносят его как "кар", другие — как "чар". Поскольку это сокращение слова character, то первый вариант правильнее, но вы вольны произносить его так, как вам удобно. В большинстве компьютеров для типа short int (короткий целый) обычно отводится два байта, для типа long int (длинный целый) — четыре байта, а для типа int (без ключевого слова short или long) может быть отведено два или четыре байта. Размер целого значения определяется системой компьютера (16- или 32-разрядная) и используемым компилятором. На современных 32-разрядных компьютерах, использующих последние версии компиляторов (например, Visual C++ 4 или более поздние), целые переменные имеют размер в четыре байта. Эта книга ориентирована на использование 4-байтовых целых, хотя у вас может быть другой вариант. Программа, представленная в листинге 3.1, поможет определить точный размер этих типов на вашем компьютере. Под символом подразумевается одиночная буква, цифра или знак, занимающий только один байт памяти. Листинг 3.1. Определение размеров переменным разных типов на вашем компьютре 1: #include <iostream.h> 2: 3: int main() 4: { 5: cout << "The size of an int is:\t\t" << sizeof(int) << " bytes.\n"; 6: cout << " The size of a short int is:\t\t" << sizeof(short) << " bytes.\n"; 7; cout << " The size of a long int is:\t\t" << sizeof(long) << " bytes.\n"; 8: cout << " The size of a char is:\t\t" << sizeof(char) << " bytes.\n"; 9: cout << " The size of a float is:\t\t" << sizeof(float) << " bytes.\n"; 10: cout << " The size of a double is:\t\t" << sizeof(double) << " bytes.\n"; 11: cout << " The size of a bool is:\t\t" << sizeof(bool) << " bytes.\n"; 12: 13: return 0: 14: }; Результат: The size of an int is: 4 bytes. The size of a short int is: 2 bytes. The size of a long int is: 4 bytes. The size of a char is: 1 bytes. The size of a float is: 4 bytes. The size of a double is: 4 bytes. The size of a bool is: 1 bytes. Примечание:На вашем компьютере размеры переменных разных типов могут быть другими. Большинство операторов листинга З.1 вам знакомо. Возможно, новым для вас будет использование функции sizeof() в строках 5-10. Результат выполнения функции sizeof() зависит от компилятора и компьютера, а ее назначение состоит в определении размеров объектов, переданных в качестве параметра. Например, в строке 5 функции sizeof() передается ключевое слово int. Функция возвращает размер в байтах переменной типа int на данном компьютере. В нашем примере для типов int и long int возвращается значение четыре байта. Знаковые и беззнаковые типыЦелочисленные переменные, используемые в программах, могут быть знаковыми и беззнаковыми. Иногда бывает полезно установить для переменной использование только положительных чисел. Целочисленные типы (short и long) без ключевого слова unsigned считаются знаковыми. Знаковые целые могут быть отрицательными или положительными. Беззнаковые числа всегда положительные. Поскольку как для знаковых, так и для беззнаковых целых отводится одно и то же число байтов, то максимальное число, которое можно хранить в беззнаковом целом, вдвое превышает максимальное положительное число, которое можно хранить в знаковом целом. С помощью типа unsigned short int можно обрабатывать числа в диапазоне 0—65 535. Половина чисел, представляемых знаковым коротким целым типом, отрицательные, следовательно, с помощью этого типа можно представить числа только в диапазоне - 32 768—32 767. Если в этом вопросе вам что-то неясно, прочитайте приложение В. Базовые типы переменныхВ языке C++ предусмотрены и другие типы переменных. Они делятся на целочисленные (которые рассматривались до сих пор), вещественные (с плавающей точкой) и символьные. Вещественные переменные содержат значения, которые могут выражаться в виде дробей. Символьные переменные занимают один байт и используются для хранения 256 символов и знаков ASCII, а также расширенных наборов символов ASCII. Под символами ASCII понимают стандартный набор знаков, используемых в компьютерах. ASCII — это American Standard Code for Information Interchange (Американский стандартный код для обмена информацией). Почти все компьютерные операционные системы поддерживают код ASCII, хотя многие также поддерживают и другие национальные наборы символов. Базовые типы переменных, используемые в программах C++, представлены в табл. 3.1. В ней также приведены обычные размеры переменных указанных типов и предельные значения, которые могут храниться в этих переменных. Вы можете сверить результаты работы программы, представленной в листинге 3.1, с содержимым табл. 3.1. Таблица 3.1. Типы переменных. Примечание:В зависимости от версии компилятора и марки компьютера, размеры переменных могут отличаться от приведенных в табл.3.1. Если результаты, полученные на вашем компьютере, совпадают с теми, что приведены после листинга 3.1, значит, табл. 3.1 применима к вашему компьютеру. В противном случае вам следует обратиться к документации, прилагаемой к компилятору, чтобы получить информацию о значениях, которые могут хранить переменные разных типов в вашей компьютерной системе. Определение переменнойЧтобы создать или определить переменную, нужно указать ее тип, за которым (после одного или нескольких пробелов) должно следовать ее имя, завершающееся точкой с запятой. Для имени переменной можно использовать практически любую комбинацию букв, но оно не должно содержать пробелов, например: x, J23qrsnf и myAge. Хорошими считаются имена, позволяющие судить о назначении переменных, ведь удачно подобранное имя способно облегчить понимание работы программы в целом. В следующем выражении определяется целочисленная переменная с именем myAge: int myAge; Примечание:При объявлении переменной для нее выделяется (резервируется) память. Резервирование памяти не очищает ячейки от значений, которые ранее в них хранились, поэтому если за объявлением переменной не следует ее инициализация, то текущее значение этой переменной будет непредсказуемым, а не нулевым, как думают многие. Далее вы узнаете, как инициализировать переменную (другими словами, присвоить ей новое значение). Уважающие себя программисты стремятся избегать таких нечитабельных имен переменных, как J23qrsnf, а однобуквенные имена (например, x или i) используют только для временных переменных, таких как счетчики циклов. Старайтесь использовать как можно более информативные имена, например myAge или howMany. Такие имена даже три недели спустя помогут вам вспомнить, что вы имели в виду, когда писали те ил и иные программные строки. Поставьте следующий эксперимент. Опираясь лишь на первые пять строк программы, попробуйте догадаться, для чего предназначены объявленные ниже переменные. Примечание:Если вы скомпилируете эту программу, компилятор выведет предупреждение о том, что эти переменные не инициализированы. Чуть ниже вы увидите, как решается эта проблема. Очевидно, что о назначении переменных во втором примере догадаться легче, и неудобство, связанное с необходимостью вводить более длинные имена переменных, впоследствии окупится с лихвой, потому что вам не придется ломать голову, для чего предназначена та или иная переменная. Чувствительность к регистру буквЯзык C++ чувствителен к регистру букв. Другими словами, прописные и строчные буквы считаются разными буквами. Переменные с именами age, Age и AGE рассматриваются как три различные переменные. Примечание: Некоторые компиляторы позволяют отключать чувствительность к регистру букв. Лучше этого не делать, ведь ваши программы не смогут работать с другими компиляторами и другие программисты будут введены в заблуждение такой программой. Существуют различные соглашения по поводу принципов подбора имен переменным. Хотя не так уж важно, каких принципов будете придерживаться вы, желательно оставаться верными им по крайней мере на протяжении работы над одним проектом. Многие программисты предпочитают записывать имена переменных только строчными буквами. Если для имени требуется два слова (например, my car), то в соответствии с самыми популярными соглашениями используются варианты my_car или myCar. Последняя форма записи называется "верблюжьим представлением", поскольку одна прописная буква посередине слова напоминает горб верблюда. Одни считают, что имена переменных с символом подчеркивания внутри слова (my_car) читаются легче. Другим не нравится этот подход, потому что он якобы вызывает трудности при вводе. В этой книге отдельные слова в составных именах переменных начинаются с прописной буквы: myCar, theduickBrownFox и т.д. Но это, конечно, ни к чему вас не обязывает, и вы можете использовать другие подходы при выборе имен. Примечание:Многие профессиональные программисты применяют так называемый венгерский стиль записи переменных. Идея состоит в том, что каждая переменная должна иметь префикс, указывающий на ее тип. Так, имена целочисленных переменных (типа int) должны начинаться со строчной буквы i, длинные целые (типа long int) — со строчной буквы l. Соответствующими префиксами должны быть помечены константы, глобальные переменные, указатели и другие объекты. Однако это имеет более важное значение в программировании на языке С, чем на C++, поскольку последний поддерживает создание нестандартных типов, или типов, определенных пользователем (подробнее об этом см. занятие 6), а также потому, что в языке C++ установлен более строгий контроль за типами данных. Ключевые словаНекоторые слова изначально зарезервированы в языке C++ и поэтому их нельзя использовать в качестве имен переменных. Такие слова называются ключевыми и используются компилятором для управления программой. В их число входят if, while, for и main. В технической документации компилятора должен быть полный список всех зарезервированных слов. Типичный набор ключевых слов языка C++ приведен в приложении Б. Не рекомендуется:Не используйте ключевые слова в качестве имен переменных. Не присваивайте беззнаковым переменным отрицательные числа. Рекомендуется:Указывайте тип переменной перед именем при ее определении. Используйте для переменных информативные имена. Помните, что в языке C++ различаются прописные и строчные буквы. Уточните, сколько байтов занимает в памяти каждый тип переменной на вашем компьютере и какие значения могут храниться в переменных этого типа. Создание нескольких переменных одного типаВ языке C++ предусмотрена возможность создания в строке программы сразу нескольких переменных одного типа. Для этого следует указать тип, за которым перечисляются имена переменных, разделенные запятыми. Например: unsigned int myAge, myWeight; // две переменные типа unsigned int long int area, width, length; // три переменные типа long int В данном примере обе переменные, myAge и myWeight, объявлены как беззнаковые целочисленные. Во второй строке объявляются три переменные с именами area, width и length. Всем этим переменным присваивается один и тот же тип (long), поэтому в одной строке определения переменных нельзя смешивать разные типы. Присваивание значений переменнымС этой целью используется оператор присваивания (=). Так, чтобы присвоить число 5 переменной Width, запишите следующее: unsigned short Width; Width = 5; Примечание:Тип long — зто сокращенное название типа long int, а short — сокращенное название типа short int. Эти две строки можно объединить в одну и инициализировать переменную Width в процессе определения: unsigned short Width = 5; Инициализация напоминает присваивание, особенно в случае инициализации целочисленных переменных. Ниже, при рассмотрении констант, вы узнаете, что некоторые значения обязательно должны быть инициализированы, поскольку по отношению к ним нельзя выполнять операцию присваивания. Существенное отличие инициализации от присваивания состоит в том, что она происходит в момент создания переменной. Подобно тому, как можно определять сразу несколько переменных, можно и инициализировать сразу несколько переменных при их создании. Например: // создаем две переменных типа long и инициализируем их long width = 5, length = 7; В этом примере переменная width типа long int была инициализирована значением 5, а переменная length того же типа — значением 7. При определении нескольких переменных в одной строке, инициализировать можно только некоторые из них: int myAge = 39, yourAge, hisAge = 40; В этом примере создаются три переменных типа int, а инициализируются только первая и третья. В листинге 3.2 показана программа, полностью готовая к компиляции. В ней вычисляется площадь прямоугольника, после чего результат выводится на экран. Листинг 3.2. Демонстрация использования переменных 1: // Демонстрация использования переменных 2: #include <iostream.h> 3: 4: int main() 5: { 6: unsigned short int Width = 5, Length; 7: Length = 10; 8: 9: // создаем переменную Area типа unsigned short и инициализируем ее 10: // результатом умножения значений переменных Width на Length 11: unsigned short int Area = (Width * Length); 12: 13: cout << "Width:" << Width << "\n"; 14: cout << "Length: " << Length << endl; 15: cout << "Area: " << Area << endl; 16: return 0; 17: } Результат: Width: 5 Length: 10 Area: 50 В строке 2 содержится директива препроцессора include, включающаябиблиотеку iostream, которая обеспечивает работоспособность объекта вывода cout. Собственно, программа начинает свою работу в строке 4. В строке 6 переменная Width определяется для хранения значения типа unsigned short int, и тут же выполняется инициализация этой переменной числом 5. В этой же строке определяется еще одна переменная Length такого же типа, но без инициализации. В строке 7 переменной Length присваивается значение 10. В строке 11 определяется переменная Area типа unsigned short int, которая тут же инициализируется значением, полученным в результате умножения значений переменных Width и Length. В строках 13—15 значения всех переменных программы выводятся на экран. Обратите внимание на то, что для разрывов строк используется специальный оператор endl. Ключевое слово typedefПорой утомительно и скучно многократно повторять запись таких ключевых слов, как unsigned short int. (Кроме того, в этих трех словах немудрено наделать еще и кучу ошибок.) В языке C++ предусмотрена возможность создания псевдонима для этой фразы путем использования ключевого слова typedef, которое означает определение типа. При создании псевдонима важно отличать его от создания нового типа (об этом пойдет речь на занятии 6). Чтобы создать псевдоним типа данных, сначала записывается ключевое слово typedef, за которым следует существующий тип, а за ним новое имя с символом точки с запятой. Например, при выполнении строки typedef unsigned short int USHORT; создается новое имя USHORT, которое можно использовать везде, где нужно определить переменную типа unsigned short int. Листинг 3.3 переделан из листинга 3.2 с использованием псевдонима USHORT вместо слов unsigned short int. Листинг 3.3. Пример определения типа с помощью typedef 1: // * * * * * * * * * * * * * * * * * 2: // Пример определения типа с помощью typedef 3: #include <iostream.h> 4: 5: typedef unsigned short int USHORT; //определение псевдонима 6: 7: int main() 8: { 9: USHORT Width = 5; 10: USHORT Length; 11: Length = 10; 12: USHORT Area = Width * Length; 13: cout << "Width:" << Width << "\n"; 14: cout << "Length: " << Length << endl; 15: cout << "Area: " << Area << endl; 16: return 0; 17: } Результат: Width: 5 Length: 10 Area: 50 В строке 5 идентификатор USHORT с помощью ключевого слова typedef определен как псевдоним типа unsigned short int. В остальном эта программа аналогична предыдущей, представленной в листинге 3.2, да и результаты работы обеих программ совпадают. В каких случаях следует использовать типы short и longНачинающим программистам часто бывает трудно принять решение о том, когда объявлять переменную с использованием типа long, а когда — с использованием типа short. Правило довольно простое: если существует хоть малейший шанс, что ваше значение будет слишком большим для предполагаемого типа, используйте тип с большим размером. Приведенные в табл. 3.1 переменные типа unsigned short int, как правило, имеют размер, равный двум байтам, и могут хранить значение, не превышающее 65 535. Знаковые короткие целые делят свой диапазон между положительными и отрицательными числами, поэтому их максимальное значение вдвое меньше, чем у беззнакового короткого целого. Хотя переменные типа unsigned long int могут хранить очень большое число (4 294 967 295), оно все-таки конечно. Если вам нужно работать с еще большими числами, придется перейти к использованию типов float или double, но при этом вы несколько проиграете в точности. Переменные типа float и double могут хранить чрезвычайно большие числа, но на большинстве компьютеров значимыми остаются только первые 7 или 19 цифр, т.е. после указанного количества цифр число округляется. Переменные с меньшим размером используют меньший объем памяти. В наши дни память становится все дешевле, а жизнь не так уж длинна, чтобы тратить ее на экономию памяти. Поэтому отдайте предпочтение типу int, который на большинстве компьютеров имеет размер в четыре байта. Переполнение беззнаковых целыхЧто случится, если при использовании беззнаковых длинных целых превысить их предельный максимум? Когда беззнаковое целое достигает своего максимального значения, при очередном инкременте оно сбрасывается в нуль и отсчет начинается сначала, как в автомобильном одометре. В листинге 3.4 показано, что произойдет при попытке поместить слишком большое число в переменную типа short. Листинг 3.4. Пример переполнения беззнаковой целой переменной. 1: #include <iostream.h> 2: int main() 3: { 4: unsigned short int smallNumber; 5: smallNumber = 65535; 6: cout << "small number:" << smallNumber << endl; 7: smallNumber++; 8: cout << "small number:" << smallNumber << endl; 9: smallNumber++; 10: cout << "small number:" << smallNumber << endl; 11: return 0; 12: } Результат: small number:65535 small number:0 small numbar:1 АНАЛИЗ: В строке 4 объявляется переменная smallNumber типа unsigned short int, которая на моем компьютере является двухбайтовой, способной хранить значение между 0 и 65 535. В строке 5 переменной smallNumber присваивается максимальное значение, которое в строке 6 выводится на экран. В строке 7 переменная smallNumber увеличивается на 1. Приращение осуществляется с помощью оператора инкремента, имеющего вид двух символов плюс (++). Следовательно, значение переменной smallNumber должно стать 65 536. Однако переменная типа unsigned short int не может хранить число, большее 65 535, поэтому ее значение сбрасывается в 0, который и выводится в строке 8. В строке 9 переменная smallNumber вновь увеличивается на единицу, после чего ее новое значение выводится на экран. Переполнение знаковых целочисленных значенийЗнаковые целые отличаются от беззнаковых тем, что половина этих значений всего диапазона — отрицательные числа. При выходе за пределы максимального положительного значения переменная принимает минимальное отрицательное значение. В листинге 3.5 показано, что происходит, если добавить единицу к максимальному положительному числу, хранящемуся в переменной типа short int. Листинг 3.5. Пример переполнения знаковой целой переменной 1: #include <iostream.h> 2: int main() 3: { 4: short int smallNumber; 5: smallNumber = 32767; 6: cout << "small number:" << smallNumber << endl; 7: smallNumber++; 8: cout << "small number:" << smallNumber << endl; 9: smallNumber++; 10: cout << "small number:" << smallNumber << endl; 11: return 0; 12:} Анализ: В строке 4 переменная smallNumber объявляется на этот раз короткой целой (short int) со знаком (если в объявлении переменной ключевое слово unsigned отсутствует, т.е. эта переменная явно не объявляется беззнаковой, то подразумевается ее использование со знаком). В остальном эта программа выполняет те же действия, что и предыдущая, но на экран выводятся совсем другие результаты. Чтобы до конца понять, почему получены именно такие результаты, нужно знать, как представляются числа со знаком в двухбайтовом целом значении. Этот пример показывает, что в случае приращения максимального положительного целого числа со знаком будет получено не нулевое значение (как в случае с беззнаковыми целыми), а минимальное отрицательное число. СимволыСимвольные переменные (типа char) обычно занимают один байт, этого достаточно для хранения 256 значений печатаемых символов (см. приложение В). Значение типа char можно интерпретировать как число в диапазоне 0—255, или символ ASCII. Набор символов ASCII и его эквивалент ISO (International Standards Organization — Международная организация по стандартизации) представляют собой способ кодировки всех букв, цифр и знаков препинания. Например, в коде ASCII английской строчной букве "а" присвоено значение 97. Всем прописным и строчным буквам, всем цифрам и знакам препинания присвоены значения от 1 до 128. Дополнительные 128 знаков и символов зарезервированы для расширения возможностей компьютера, хотя расширенный набор символов IBM стал уже чем-то вроде стандарта. Примечание:ASCII обычно произносится как "аскей". Примечание:Компьютеры не имеют ни малейшего понятия, ни о каких буквах, знаках препинания или предложениях. Все они понимают только числа. В действительности же они оценивают некоторые электрические параметры в определенных точках своих схем. Если значение оцениваемого параметра выше некоторой оговоренной величины, оно представляется внутренне как 1, если нет — как 0. Путем группирования нулей и единиц компьютер способен генерировать кодовые комбинации, которые можно интерпретировать как числа, а те, в свою очередь, можно присвоить буквам и знакам препинания. Символы и числаЕсли поместить какой-нибудь символ, например "а", в переменную типа char, то в действительности она будет хранить число, лежащее в диапазоне между 0 и 255. Однако компилятор знает, как переходить от символов к их цифровым эквивалентам в ASCII и обратно. Взаимосвязь между числом и буквой произвольна, поскольку нет никакой весомой причины для присваивания строчной букве "а" именно значения 97. Если все составляющие компьютера (ваша клавиатура, компилятор и экран) с этим "согласны", никаких проблем не возникнет. Однако важно понимать, что между значением 5 и символом "5" большая разница. Символу "5" в действительности соответствует значение 53, так же как букве "а" соответствует число 97. Листинг 3.6. Вывод на зкран символов по их значениям. 1: #include <iostream.h> 2: int main() 3: { 4: for (int i = 32; i<128; i++) 5: cout << (char) i; 6: return 0; 7: } Результат: !"#$%'()*+,./0123456789:;<>?@ABCDEFGHIJKLMNOP _QRSTUVWXYZ[\]"`abcdefghijklmnopqrstuvwxyz{|{~ Эта простая программа выводит символы, значения которых лежат в диапазоне 32—127. Специальные символыКомпилятор C++ распознает некоторые специальные символы, предназначенные для форматирования текста. (Самые распространенные из них представлены в табл. 3.2.) Чтобы вставить эти символы в программу, используется обратный слеш (называемый символом начала управляющей последовательности), указывающий, что следующий за ним символы является управляющими. Следовательно, чтобы вставить в программу символ табуляции, нужно ввести одиночную кавычку, обратный слеш, букву t и снова одиночную кавычку: char tabCharacter = '\t'; В этом примере объявляется переменная типа char (с именем tabCharacter), которая тут же инициализируется символьным значением \t, распознаваемым как символ табуляции. Специальные символы форматирования используются при выводе информации на экран, в файл или на другое устройство вывода (например, принтер). Символ начала управляющей последовательности изменяет значение символа, который следует за ним. Например, обычно символ n означает букву я, но когда перед ней стоит символ начала управляющей последовательности (\), то он превращается в символ разрыва строки. Таблица 3.2. Управляющие символы. КонстантыПодобно переменным, константы представляют собой ячейки памяти, предназначенные для хранения данных. Но, в отличие от переменных, константы не изменяются (о чем говорит само название — константа). Создаваемую константу нужно инициализировать, поскольку позже ей нельзя присвоить новое значение, В языке C++ предусмотрено два типа констант: литеральные и символьные. Литеральные константыЛитеральная константа — это значение, непосредственно вводимое в самой программе. Например, в выражении int myAge = 39; myAge является переменной типа int, а число 39 — литеральной константой. Нельзя присвоить никакое значение константе 39. Символьные константыСимвольная константа — это константа, представленная именем (точно так же, как именем представляется любая переменная). Однако, в отличие от переменной, значение инициализированной константы изменить нельзя. Если в вашей программе есть одна целочисленная переменная с именем students, а другая — с именем classes, вы могли бы вычислить общее количество учеников школы при условии, что вам известно, сколько классов в школе и сколько учеников в каждом классе (допустим, каждый класс состоит из 15 учеников): students = classes * 15; Примечание:Символ (*) означает умножение. В этом примере число 15 является литеральной константой. Но если эту литеральную константу заменить символьной, то вашу программу будет легче читать и изменять в будущем: students = classes * studentsPerClass Если впоследствии потребуется изменить количество учеников в каждом классе, вы сможете сделать это единожды в той строке программы, где определяется константа studentsPerClass, и вам не придется вносить изменения во все строки программы, где используется это значение. В языке C++ существует два способа объявления символьной константы. Традиционный и ныне уже устаревший способ состоит в использовании директивы препроцессора #define. Определение констант с помощью директивы #deiineДля определения константы традиционным способом введите следующее выражение: #define studentsPerClass 15 Обратите внимание на то, что константа studentsPerClass не имеет никакого конкретного типа (int, char и т.д.). Директива #define выполняет простую текстовую подстановку. Каждый раз, когда препроцессор встречает слово studentsPerClass, он заменяет его литералом 15. Поскольку препроцессор запускается перед компилятором, последний никогда не увидит константу, а будет видеть только число 15. Определение констант с помощью ключевого слова constХотя директива #define и справляется со своими обязанностями, в языке C++ существует новый, более удобный способ определения констант: const unsigned short int studentsPerClass = 15; В этом примере также объявляется символическая константа с именем studentsPerClass, но на сей раз для этой константы задается тип unsigned short int. Этот способ имеет несколько преимуществ, облегчая дальнейшую поддержку вашей программы и предотвращая появление некоторых ошибок. Самое важное отличие этой константы от предыдущей (объявленной с помощью директивы #define) состоит в том, что она имеет тип и компилятор может проследить за ее использованием только по назначению (т.е. в соответствии с объявленным типом). Примечание:Во время работы программы константы изменять нельзя. Если же возникла необходимость в изменении, например, константы studentsPerClass, вам нужно изменить соответствующее объявление в программе и перекомпилировать ее. Рекомендуется:Следите, чтобы значения переменных не превышали допустимый предел. Присваивайте переменным осмысленные имена, отражающие их назначение. Используйте типы short и long, чтобы более эффективно управлять памятью компьютера. Не рекомендуется: Не используйте в качестве имен переменных ключевые слова. Константы перечисленийПеречисления позволяют создавать новые типы данных, а затем определять переменные этих типов, значения которых ограничены набором константных значений. Например, можно объявить C0L0R как перечисление и определить для него пять значений: RED, BLUE, GREEN, WHITE И BLACK. Для создания перечисления используется ключевое слово enum, за которым следуют: имя типа, открывающая фигурная скобка, список константных значений, разделенных запятыми, закрывающая фигурная скобка и точка с запятой. Например: enum COLOR { RED, BLUE, GREEN, WHITE, BLACK }; Это выражение выполняет две задачи. 1. Создается перечисление с именем C0L0R, являющееся новым типом. 2. Определяются символьные константы: RED со значением 0; BLUE со значением 1; GREEN со значением 2 и т.д. Каждой константе перечисления соответствует определенное целочисленное значение. По умолчанию первая константа инициализируется значением 0, а каждая следующая — значением, на единицу большим предыдущего. Однако любую константу можно инициализировать произвольным значением, и в этом случае явно неинициализированные константы продолжат возрастающую последовательность, взяв за точку отсчета значение, стоящее перед ними. Таким образом, если записать enum Color { RED=100, BLUE, GREEN=500, WHITE, BLACK=700 }; то константа red будет иметь значение 100; константа blue — 101; константа GREEN — 500; константа WHITE — 501; константа BLACK — 700. Теперь можно определить переменные типа C0L0R, но каждой из них можно присвоить только одно из перечислимых значений (в данном случае RED, BLUE, GREEN, WHITE или BLACK либо 100, 101, 500, 501 или 700). Переменной C0L0R можно присвоить любое значение цвета. На самом деле этой переменной можно присвоить любое целое значение, даже если оно не соответствует ни одному из разрешенных цветов, но в этом случае приличный компилятор должен возмутиться и показать предупреждающее сообщение. Важно понимать, что переменные перечисления на самом деле имеют тип unsigned int и целочисленным переменным присваиваются заданные константы перечисления. Однако иногда при работе с цветами, днями недели или другими подобными наборами значений неплохо иметь возможность называть эти значения по имени. В листинге 3.7 представлена программа, в которой используется тип перечисления. Листинг 3.7. Использование перечисления 1: #include <iostream.h> 2: int main() 3: { 4: enum Days { Sunday, Monday, Tuesday, 5: Wednesday, Thursday, Friday, Saturday }; 6: int choice; 7: cout << "Enter a day (0-6): "; 8: cin << choice; 9: if (choice = Sunday || choice == Saturday) 10: cout << "\nYou're already off on weekends!\n"; 11: else 12: cout << "\nOkay, I'll put in the vacation day.\n"; 13: return 0; 14: } Результат: Enter a day (0-6): 6 You're already off on weekends! Анализ: В строке 4 определяется перечисление DAYS с семью константными значениями. Все они образуют возрастающую последовательность чисел, начиная с нуля; таким образом, значение вторника (Tuesday) равно 2. Пользователю предлагается ввести значение между 0 и 6. Он не может ввести слово Sunday, поскольку в программе не предусмотрен перевод символов в значение перечисления. Но можно проверить введенное пользователем значение, сравнив его с константными значениями перечисления, как показано в строке 9. Использование перечисления облегчает анализ программы. Того же эффекта можно добиться, используя константы целочисленного типа, как показано в листинге 3.8. Примечание:Для этой и всех небольших программ в данной книге я намеренно не предусматривал включения ряда выражений, которые обычно пишутся для обработки ситуаций, связанных с приемом от пользователя неверных данных. Например, в этой программе отсутствует проверка вводимых чисел, которая должна обязательно присутствовать в настоящей программе и предназначена для предупреждения ввода неразрешенных чисел. Это было сделано намеренно, для того чтобы сэкономить место в книге и сосредоточить ваше внимание только на рассматриваемой в данном разделе теме. Листинг 3.8. Та же программа, но с использованием констант целочисленного типа 1: #include <iostream.h> 2: int main() 3: { 4: const int Sunday = 0; 5: const int Monday = 1; 6: const int Tuesday = 2; 7: const int Wednesday = 3; 8: const int Thursday = 4; 9: const int Friday = 5; 10: const int Saturday = 6: 11; 12: int choice; 13: cout << "Enter a day (0-6): "; 14: cin << choice; 15: 16: if (choice = Sunday || choice == Saturday) 17: cout << "\nYou're already off on weekends!\n"; 18: else 19: cout << "\nOkay, I'll put in the vacation day.\n"; 20: 21: return 0; 22: } Результат: Enter a day (0-6): 6 You're already off on weekends! Результаты работы этой программы идентичны результатам программы из листинга 3.7. Но в этом варианте все константы (Sunday, Monday и пр.) определены в явном виде и отсутствует тип перечисления Days. Обратите внимание, что программа с перечислением короче и логичнее. РезюмеНа этом занятии рассматривались числовые и символьные переменные и константы, которые в C++ используются для хранения данных во время выполнения программы. Числовые переменные могуг быть либо целыми (char, short и long int), либо вещественными (float и double). Кроме того, они могут быть знаковыми и беззнаковыми (unsigned). Хотя на различных компьютерах все эти типы могуг иметь разные размеры, но на одном компьютере переменные одного и того же типа всегда имеют постоянный размер. Переменную нужно объявить до ее использования. При работе с данными необходимо следить, чтобы тип данных соответствовал объявленному типу переменной. Если, например, поместить слишком большое число в целую переменную, возникнет переполнение, которое приведет к неверному результату. Кроме того, вы познакомились с литеральными, символьными и перечислимыми константами, а также с двумя способами объявления символьных констант: с помощью директивы #define и ключевого слова const. Вопросы и ответыЕсли переменные типа short int могут преподнести сюрприз в виде переполнения памяти, то почему бы не использовать во всех случаях переменные типа long int? Как короткие (short), так и длинные (long) целочисленные переменные могут переполняться, но для того чтобы это произошло с типом long int, нужно уж слишком большое число. Например, переменная типа unsigned short int переполнится после числа 65 535, в то время как переменная типа unsigned long int — только после числа 4 294 967 295. Однако на многих компьютерах каждое объявление длинного целого значения занимает вдвое больше памяти по сравнению с объявлением короткого целого (четыре байта против двух), и программа, в которой объявлено 100 таких переменных займет лишних 200 байт ОЗУ. Честно говоря, память сейчас перестала быть проблемой, поскольку большинство персональных компьютеров оснащено многими тысячами (если не миллионами) байтов памяти. Что случится, если присвоить число с десятичной точкой целочисленной переменной, а не переменной типа float, как в следующей программной строке? int aNumber = 5.4; Хороший компилятор выдаст по этому поводу предупреждение, но такое присваивание совершенно правомочно. Присваиваемое число будет в таком случае усечено до целого. Следовательно, если присвоить число 5,4 целочисленной переменной, эта переменная получит значение 5. Однако налицо потеря информации, и если затем хранящееся в этой целой переменной значение вы попытаетесь присвоить переменной типа float, то вещественной переменной придется "довольствоваться" лишь значением 5. Почему вместо литеральных констант лучше использовать символьные? Если некоторое значение используется во многих местах программы, то применение символьной константы позволяет изменять все значения путем изменения одного, лишь определения этой константы. Кроме того, смысл символьной константы проще понять по ее названию. Ведь иногда трудно разобраться, почему некоторое значение умножается на число 360, а если это число будет заменено символьной константой degreesInACircle (градусов в окружности), то сразу становится ясно, о чем идет речь. Что произойдет, если присвоить отрицательное число беззнаковой переменной? Рассмотрите следующую строку программы: unsigned int aPositiveNumber = -1; Хороший компилятор отреагирует на это предупреждением, но такое присвоение вполне законно. Отрицательное число будет представлено в беззнаковом побитовом виде. Так, побитовое представление числа -1 выглядит как 11111111 11111111 (0xFFFF в шестнадцатеричном формате) и соответствует числу 65 535. Если вам что-то непонятно, обратитесь к приложению В. Можно ли работать с языком C++, не имея представления о побитовом выражении числовых значений и арифметике двоичных и шестнадцатеричных чисел? Можно, но эффективность работы снизится. Мощь языка C++ состоит не в том, чтобы уберечь вас от ненужных деталей работы компьютера, а в том, чтобы заставить компьютер работать с максимальной отдачей. В этом основное отличие C++ от других языков программирования. Программисты, которые не имеют представления, как работать с двоичными значениями, часто обескуражены получаемыми результатами. КоллоквиумВ этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний и приводится несколько упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными, хотя бы некоторые из предложенных ниже вопросов. Контрольные вопросы1. В чем разница между целочисленной и вещественной (с плавающей точкой) переменными? 2. Каково различие между типами unsigned short int и long int? 3. Каковы преимущества использования символьной константы вместо литерала? 4. Каковы преимущества использования ключевого слова const вместо директивы #define? 5. Как влияет на работу программы "хорошее" или "плохое" имя переменной? 6. Если перечисление (enum) заданно таким образом, то каково значение его члена Blue? enum COLOR { WHITE, BLACK = 100, RED, BLUE, GREEN = 300 }; 7. Какие из следующих имен переменных можно считать хорошими, плохими или вообще недопустимыми? а) Age б) ! ex в) R79J г) TotalIncome д) _Invalid Упражнения1. Какой тип переменной был бы правильным для хранения следующей информации? • Ваш возраст. • Площадь вашего заднего двора. • Количество звезд в галактике. • Средний уровень выпадения осадков за январь. 2. Создайте подходящие имена переменных для хранения этой информации. 3. Объявите константу для числа pi, равного 3.14159. 4. Объявите переменную типа float и инициализируйте ее, используя константу pi. День 4-й. Выражения и операторыПрограмма представляет собой набор команд, выполняемых в определенной последовательности. Современные программы сильны тем, что выполняются не последовательно, команда за командой от начала до конца программы, а по блокам. Каждый блок программы запускается в зависимости от выполнения заданного условия. Сегодня вы узнаете: • Что такое операторы • Что такое блоки • Что такое выражения • Как реализовать ветвление программы на основе результата выполнения заданного логического условия • Что такое ИСТИННО и ЛОЖНО с точки зрения программиста на C++ ВыраженияВ языке C++ выражения управляют последовательностью выполнения других выражений, возвращают результаты вычислений или ничего не делают (нулевые выражения). Все выражения в C++ оканчиваются точкой с запятой. Нулевое выражение представляет собой просто точку с запятой. Наиболее простой пример выражения — это операция присвоения значения: В отличие от алгебры, это выражение не означает, что x равняется a+b. Данное выражение следует понимать так: присвоим результат суммирования значений переменных а и b переменной x, или присвоим переменной x значение a+b. Несмотря на то что в этом выражении выполняется сразу два действия — вычисление суммы и присвоение значения, после выражения устанавливается только один символ точки с запятой. Оператор (=) присваивает результаты операций, выполняемых над операндами, расположенными справа от знака равенства, операнду, находящемуся слева от него. x = а + b; Символы пробеловСимволы пробелов, к которым относятся не только пробелы, но и символы табуляции и разрыва строки, в выражениях обычно игнорируются. Рассмотренное выше выражение можно записать так: x=a+b; или так: x = a + b ; Хотя последний вариант абсолютно правомочен, выглядит он довольно глупо. Символы пробелов можно использовать для улучшения читабельности программы, что облегчит работу с ней. Но при неумелом использовании эти же пробелы могут совершенно запутать программный код. Создатели C++ предоставили много различных возможностей, а уж насколько эффективно они будут использоваться, зависит от вас. Символы пробелов не отображаются на экране и при печати — видны только различные отступы и промежутки между элементами текста. Блоки и комплексные вараженияИногда для облегчения восприятия программы логически взаимосвязанные выражения удобно объединять в комплексы, называемые блоками. Блок начинается открывающей фигурной скобкой ({) и оканчивается закрывающей фигурной скобкой (}). Хотя каждое выражение в блоке должно оканчиваться точкой с запятой, после символов открытия и закрытия блока точки с запятой не ставятся, как в следующем примере: { temp = а; а = b; b = temp; } Этот блок выполняется как одно выражение, осуществляющее обмен значениями между переменными а и b. Рекомендуется:Незабывайте о закрывающейфигурной скобке каждый раз, когда используется открывающая фигурная скобка. Завершайте выражения в программе символом точки с запятой Не рекомендуется:Используйте разумно символы пробелов, чтобы сделать свою программу более понятной, а не наоборот. ОперацииВсе, в результате чего появляется некоторое значение, в языке C++ называется операцией. Об операциях говорят, что они возвращают значение. Так, операция 3+2; возвращает значение 5. Все операции являются вместе с тем и выражениями. Возможно, вы будете удивлены тому, что многие программные блоки рассматриваются как выражения. Приведем лишь три примера: 3.2 // возвращает значение 3.2 PI // вещественная константа, которая возвращает значение 3.14 SecondsPerMinute // целочисленная константа, которая возвращает 60 Предполагая, что PI — константа, равная 3.14, а SecondsPerMinute — константа, равная 60, можно утверждать, что все три выражения являются операциями. Выражение x = а + b; не только складывает значения переменных а и b, но и присваивает результат переменной x, т.е. возвращает результат суммирования переменной x. Поэтому данное выражение вполне можно назвать операцией. Операции всегда располагаются справа от оператора присваивания: у = x = а + b; Данное выражение выполняет представленную ниже последовательность действий. Прибавляем а к b. Присваиваем результат выражения а + b переменной x. Присваиваем результат выражения присваивания x = а + b переменной у. Если переменные а, b, x и у являются целыми и если а имеет значение 2, а b — значение 5, то переменным x и у будет присвоено значение 7. Пример выполнения некоторых выражений представлен в листинге 4.1. Листинг 4.1. Выполнение сложных операций 1: #include <iostream.h> 2: int main() 3: { 4: int a=0, b=0, x=0, y=35; 5: cout << " a:" << a << " b:" << b; 6: cout << " x:" << x << " y:" << y << endl; 7: a = 9; 8: b = 7; 9: y = x = a+b; 10: cout << " a:" << a << " b:" << b; 11: cout << " x:" << x << " y:" << y << endl; 12: return 0; 13: } Результат: а: 0 b: 0 x: 0 у: 35 а: 9 b: 7 x: 16 у: 16 В строке 4 объявляются и инициализируются четыре переменные. Их значения выводятся в строках 5 и 6. В строке 7 переменной а присваивается значение 9. В строке 8 переменной b присваивается значение 7. В строке 9 значения переменных а и b суммируются, а результат присваивается переменной x. Результат операции x = a+b, в свою очередь, присваивается переменной у. ОператорыОператор — это литерал, который заставляет компилятор выполнять некоторое действие. Операторы воздействуют на операнды. Операндами в C++ могут быть как отдельные литералы, так и целые выражения. Язык C++ располагает двумя видами операторов: • операторы присваивания; • математические операторы. Оператор присваиванияОператор присваивания (=) позволяет заменить значение операнда, расположенного с левой стороны от знака равенства, значением, вычисляемым с правой стороны от него. Так, выражение x = а + b; присваивает операнду x значение, которое является результатом сложения значений переменных а и b. Операнд, который может находиться слева от оператора присваивания, называется адресным операндом, или l-значением (от англ. слова left, т.е. левый). Операнд, который может находиться справа от оператора присваивания, называется операционным операндом, или r-значением (от англ. слова right, т.е. правый). Константы могут быть только r-значениями и никогда не бывают адресными операндами, поскольку в ходе выполнения программы значения констант изменять нельзя. Так, можно записать: x = 35; // правильно Но нельзя записать: 35 = x; // ошибка! Повторим: l-значение — это операнд, который может стоять в левой части выражения присваивания, а г-значение — операнд, который может стоять в правой части этого выражения. Обратите внимание, что все l-значения могут быть r-значениями, но не все r-значения могут быть l-значениями. Примером г-значения, которое не может быть l-значением, служит литеральная константа. Так, можно загасать: x = 5;, но нельзя записать: 5 = x; (x может быть l- или r-значением, а 5 может быть только r-значением). Математические операторыВ C++ используется пять математических операторов: сложения (+), вычитания (-), умножения (*), целочисленного деления (/) и деления по модулю (%). В операциях сложения и вычитания разобраться несложно: они возвращают сумму и разность двух операндов. Хотя следует отметить, что вычитание беззнаковых целых может привести к удивительным результатам, если полученная разность окажется отрицательным числом. Вы уже видели нечто подобное на прошлом занятии при описании переполнения переменных. В листинге 4.2 демонстрируется ситуация, когда из малого беззнакового числа вычитается большое беззнаковое число. Листинг 4.2. Пример вычитания с переполнением целого числа 1: // Листинг 4.2. Пример вычитания с 2: // переполнением целого числа 3: #include <iostream.h> 4: 5: int main() 6: { 7: unsigned int difference; 8: unsigned int bigNumber = 100; 9: unsigned int smallNumber = 50; 10: difference = bigNumber - smallNumber; 11: cout << "Difference is: " << difference; 12: difference = smallNumber - bigNumber; 13: cout << "\nNow difference is: " << difference << endl; 14: return 0; 15: } Результат: Difference is: 50 Now difference is: 4294967246 Анализ: Оператор вычитания используется в строке 10, а результат выводится на экран в строке 11, в данном случае вполне ожидаемый. В строке 12 вновь вызывается оператор вычитания, но на этот раз большое беззнаковое число вычитается из малого беззнакового числа. Результат должен быть отрицательным, но поскольку он вычисляется (и выводится) как беззнаковое число, происходит переполнение, о чем говорилось на прошлом занятии. Эта тема подробно рассматривается в приложении А. Целочисленное деление и деление по модулюЦелочисленное деление несколько отличается от обычного. Целочисленное деление — это то же caмoe деление, которое вы изучали, когда ходили в первый класс. При делении числа 21 на число 4 (21/4) в случае целочисленного деления в ответе получается 5 и остаток 1. Чтобы получить остаток, нужно число 21 разделить по модулю 4 (21 % 4), в результате получим остаток 1. Операция деления по модулю иногда оказывается весьма полезной, например, если вы захотите вывести из ряда чисел каждое десятое значение. Любое число, результат деления которого по модулю 10 равен нулю, является кратным десяти, т.е. делится на 10 без остатка. Так, результат выражения 1 % 10 равен 1; 2 % 10 равен 2 и т.д.; а 10 % 10 равен 0. Результат от деления 11 % 10 снова равен 1; 12 % 10 снова равен 2; и так можно продолжать до следующего числа, кратного 10, которым окажется 20. Мы воспользуемся этим методом при рассмотрении циклов на занятии 7. Вопросы и ответы При делении 5 на 3 я получаю в ответе 1. В чем моя ошибка? При делении одного целого числа на другое в качестве результата вы также получите целое число. Следовательно, 5/3 равно 1. Для получения дробного результата нужно использовать вещественные числа. Выражение 5,0 / 3,0 даст дробный ответ: 1,66667. Если ваш метод принимает в качестве параметров целочисленные значения, нужно привести их к типу float. Вопросы и ответы: Выполняя операцию приведения типа переменной, вы заставляете компилятор изменить ее тип. Приэтом вы как будто говорите своемукомпилятору:"Я знаю, что делаю". Было бы неплохо, если бы это оказалось правдой, поскольку компилятор как бы отвечает вам: "Как скажете, босс: вся ответственность ложится на вас". В данном случае мы хотим сказать компилятору: "Я понимаю, что ты считаешь это значение целым, но я знаю, что делаю: это действительно вещественное значение". Для приведения типа существует два способа. Можно использовать приведение типа в старом стиле С или новый улучшенный оператор ANSIstatic_cast. Оба варианта демонстрируются в листинге 4.3. Листинг 4.3. Приведение переменной к типу float 1: #include <iostream.h> 2: 3: void intDiv(int x, int y) 4: { 5: int z = x / y; 6: cout << "z: " << z << endl; 7: } 8: 9: void floatDiv(int x, int y) 10: { 11: float a = (float)x; // старый стиль 12: float b = static_cast<float>(y); // современный стиль 13: float c = a / b; 14: 15: cout << "c: " << c << endl; 16: } 17: 18: int main() 19: { 20: int x = 5, y = 3; 21: intDiv(x,y); 22: floatDiv(x,y); 23: return 0; 24: } Результат: z: 1 с: 1.66667 Анализ: В строке 20 объявляются две целочисленные переменные. В строке 21 они как параметры передаются функции intDiv, а в строке 22 — функции floatDiv. Вторая функция начинается со строки 9. В строках 11 и 12 целые значения приводятся к вещественному типу и присваиваются переменным типа float. Результат деления присваивается третьей переменной типа float в строке 13 и выводится на экран в строке 15. Совместное использование математических операторов с операторами присваиванияНет ничего необычного в том, чтобы к переменной прибавить некоторое значение, а затем присвоить результат той же переменной. Если у вас есть переменная myAge и вы хотите увеличить ее значение на два, можно записать следующее: int myAge = 5; int temp; temp = myAge + 2; // складываем 5 + 2 и результат помещаем в temp myAge = temp; // значение возраста снова помещаем в myAge Однако этот метод грешит излишествами. В языке C++ можно поместить одну и ту же переменную по обе стороны оператора присваивания, и тогда предыдущий блок сведется лишь к одному выражению: myAge = myAge + 2; В алгебре это выражение рассматривалось бы как бессмысленное, но в языке C++ оно читается следующим образом: добавить два к значению переменной myAge и присвоить результат переменной myAge. Существует еще более простой вариант предыдущей записи, хотя его труднее читать: myAge += 2; Этот оператор присваивания с суммой (+=) добавляет r-значение к l-значению, а затем снова записывает результат в l-значение. Если бы до начала выполнения выражения переменная myAge имела значение 4, то после ее выполнения значение переменной myAge стало бы равным 6. Помимо оператора присваивания с суммой существуют также оператор присваивания с вычитанием (-=), делением (/=), умножением (*=) и делением по модулю (%=). Инкремент декрементОчень часто в программах к переменным добавляется (или вычитается) единица. В языке C++ увеличение значения на 1 называется инкрементом, а уменьшение на 1 — декрементом. Для этих действий предусмотрены специальные операторы. Оператор инкремента (++) увеличивает значение переменной на 1, а оператор декремента (--) уменьшает его на 1. Так, если у вас есть переменная С и вы хотите прирастить ее на единицу, используйте следующее выражение: C++; // Увеличение значения С на единицу Это же выражение можно было бы записать следующим образом: С = С + 1; что, в свою очередь, равносильно выражению. С += 1; Префикс и постфиксКак оператор инкремента, так и оператор декремента работает в двух вариантах: префиксном и постфиксном. Префиксный вариант записывается перед именем переменной (++myAge), а постфиксный — после него (myAge++). В простом выражении вариант использования не имеет большого значения, но в сложном при выполнении приращения одной переменной с последующим присваиванием результата другой переменной это весьма существенно. Префиксный оператор вычисляется до присваивания, а постфиксный — после. Семантика префиксного оператора следующая: инкрементируем значение, а затем считываем его. Семантика постфиксного оператора иная: считываем значение, а затем декрементируем оригинал. На первый взгляд это может выглядеть несколько запутанным, но примеры легко проясняют механизм действия этих операторов. Если x — целочисленная переменная, значение которой равно 5, и, зная это, вы записали int а = ++x; то тем самым велели компилятору инкрементировать переменную x (сделав ее равной 6), а затем присвоить это значение переменной а. Следовательно, значение переменной а теперь равно 6 и значение переменной x тоже равно 6. Если же, после этого вы записали int b = x++; то тем самым велели компилятору присвоить переменной b текущее значение переменной x (6), а затем вернуться назад к переменной x и инкрементировать ее. В этом случае значение переменной b равно 6, но значение переменной x уже равно 7. В листинге 4.4 продемонстрировано использование обоих типов операторов инкремента и декремента. Листинг 4.4. Примеры использования префиксных и постфиксных операторов 1: // Листинг 4.4. Демонстрирует использование 2: // префиксных и постфиксных операторов 3: // инкремента и декремента 4: #include <iostream.h> 5: int main() 6: { 7: int myAge = 39; // инициализируем две целочисленные переменные 8: int yourAge = 39; 9: cout << "I am: " << myAge << " years old.\n"; 10: cout << "You are: " << yourAge << " years old\n"; 11: myAge++; // постфиксный инкремент 12: ++yourAge; // префиксный инкремент 13: cout << "One year passes...\n"; 14: cout << "I am: " << myAge << " years old.\n"; 15: cout << "You are: " << yourAge << " years old\n"; 16: cout << "Another year passes\n"; 17: cout << "I am: " << myAge++ << " years old.\n"; 18: cout << "You are: " << ++yourAge << " years old\n"; 19: cout << "Let's print it again.\n"; 20: cout << "I am: " << myAge << " years old.\n"; 21: cout << "You are: " << yourAge << " years old\n"; 22: return 0; 23: } Результат: I am 39 years old You are 39 years old One year passes I am 40 years old You are 40 years old Another year passes I am 40 years old You are 41 years old Let's print it again I am 41 years old You are 41 years old Анализ: В строках 7 и 8 объявляются две целочисленные переменные и каждая из них инициализируется значением 39. Значения этих переменных выводятся в строках 9 и 10. В строке 11 инкрементируется переменная myAge с помощью постфиксного оператора инкремента, а в строке 12 инкрементируется переменная yourAge с помощью префиксного оператора инкремента. Результаты этих операций выводятся в строках 14 и 15; как видите, они идентичны (обоим участникам нашего эксперимента по 40 лет). В строке 17 инкрементируется переменная myAge (также с помощью постфиксного оператора инкремента), являясь при этом частью выражения вывода на экран. Поскольку здесь используется постфиксная форма оператора, то инкремент выполняется после операции вывода, поэтому снова было выведено значение 40. Затем (для сравнения с постфиксным вариантом) в строке 18 инкрементируется переменная yourAge с использованием префиксного оператора инкремента. Эта операция выполняется перед выводом на экран, поэтому отображаемое значение равно числу 41. Наконец, в строках 20 и 21 эти же значения выводятся снова. Поскольку приращения больше не выполнялись, значение переменной myAge сейчас равно 41, как и значение переменной yourAge (все правильно: стареем мы все с одинаковой скоростью!). Приоритеты операторовКакое действие — сложение или умножение — выполняется первым в сложном выражении, например в таком, как это: X = 5 + 3 * 8; Если первым выполняется сложение, то ответ равен 8 * 8, или 64. Если же первым выполняется умножение, то ответ равен 5 + 24, или 29. Каждый оператор имеет значение приоритета (полный список этих значений приведен в приложении А). Умножение имеет более высокий приоритет, чем сложение, поэтому значение этого "спорного" выражения равно 29. Если два математических оператора имеют один и тот же приоритет, то они выполняются в порядке следования слева направо. Значит, в выражении X = 5 + 3 + 8 * 9 + 6 * 4; сначала вычисляется умножение, причем слева направо: 8*9 = 72 и 6*4 = 24. Теперь то же выражение выглядит проще: x = 5 + 3 + 72 + 24; Затем выполняем сложение, тоже слева направо: 5 + 3 = 8; 8 + 72 = 80; 80 + 24 = 104. Однако будьте осторожны — не все операторы придерживаются этого порядка выполнения. Например, операторы присваивания вычисляются справа налево! Но что же делать, если установленный порядок приоритетов не отвечает вашим намерениям? Рассмотрим выражение: TotalSeconds = NumMinutesToThink + NumMinutesToType * 60 Предположим, что в этом выражении вы не хотите умножать значение переменной NumMinutesToType на число 60, а затем складывать результат со значением переменной NumMinutesToThink. Вам нужно сначала сложить значения двух переменных, чтобы получить общее число минут, а затем умножить это число на 60, получив тем самым общее количество секунд. В этом случае для изменения порядка выполнения действий, предписанного приоритетом операторов, нужно использовать круглые скобки. Элементы, заключенные в круглые скобки, имеют более высокий приоритет, чем любые другие математические операторы. Поэтому для реализации ваших намерений приведенное выше выражение нужно представить в таком виде: TotalSeconds = (NumMinutesToThink + NumMinutesToType) * 60 Вложение круглых скобокПри создании сложных выражений может возникнуть необходимость вложить круглые скобки друг в друга. Например, вам нужно вычислить общее число секунд, затем общее число включенных в рассмотрение людей, а уж потом перемножить эти числа: TotalPersonSeconds = ( ( (NumMinutesToThink + NumMinutesToType) * 60) * (PeopleInTheOffice + PeopleOnVacation) ) Это сложное выражение читается изнутри. Сначала значение переменной NumMinutesToThink складывается со значением переменной NumMinutesToType, поскольку они заключены во внутренние круглые скобки. Затем полученная сумма умножается на 60. После этого значение переменной PeopleInTheOffice прибавляется к значению переменной PeopleOnVacation. Наконец, вычисленное общее количество людей умножается на общее число секунд. Этот пример затрагивает близкую и не менее важную тему. Приведенное выше выражение легко вычисляется компьютером, но нельзя сказать, что человеку его так же легко читать, понимать и модифицировать. Вот как можно переписать это выражение с помощью временных целочисленных переменных: TotalMinutes = NumMinutesToThink + NumMinutesToType; TotalSeconds = TotalMinutes * 60; TotalPeople = PeopleInTheOffice + PeopleOnVacation; TotalPersonSeconds = TotalPeople * TotalSeconds; Для записи этого варианта требуется больше времени и много временных переменных, но он гораздо легче для понимания. Осталось лишь добавить комментарии, разъясняющие назначение этого программного кода, и заменить число 60 символьной константой. И тогда можно считать этот программный фрагмент практически идеальным для чтения и дальнейшей эксплуатации. Рекомендуется:Помните, что выражения оперируют значениями. Используйте префиксный оператор (++переменная) для инкремента или декремента переменной перед ее использованием в выражении. Используйте постфиксный оператор (переменная++) для инкремента или декремента переменной после ее использования в выражении. Используйте круглые скобки для изменения порядка выполнения операторов, обусловленного их приоритетами. He рекомендуется:Не используйте слишком много вложенных круглых скобок, поскольку такие выражения становятся трудными для понимания. Что такое ИСТИННОВ предыдущих версиях языка C++ результаты логических выражений представлялись целочисленными значениями, но в новом стандарте ANSI введен новый тип — bool, имеющий только два возможных значения: true или false. Любое выражение может быть рассмотрено с точки зрения его истинности или ложности. Математические выражения, возвращающие нуль, можно использовать для присвоения значения false логической переменной, а любой другой результат будет означать true. Примечание:Многие компиляторы и раньше были сориентированы на тип bool, который внутренне представлялся с помощью типа long int и поэтому имел размер, равный четырем байтам. Ныне ANSI-совместимые компиляторы часто обеспечивают однобайтовый тип bool. Операторы отношенийТакие операторы используются для выяснения равенства или неравенства двух значений. Выражения сравнения всегда возвращают значения true (истина) или false (ложь). Операторы отношения представлены в табл. 4.1. Примечание:В новом стандарте ANSI предусмотрен новый тип bool, и все операторы отношений теперь возвращают значение типа bool— true и false. В предыдущих версиях языка C++ эти операторы возвращали 0 в качестве false или любое ненулевое значение (обычно 1) в качестве true. Если одна целочисленная переменная myAge содержит значение 39, а другая целочисленная переменная yourAge — значение 40, то, используя оператор равенства (==), можно узнать, равны ли эти переменные: myAge == yourAge; // совпадает ли значение переменной myAge со значением переменной yourAge? Это выражение возвращает 0, или false (ложь), поскольку сравниваемые переменные не равны. Выражение myAge > yourAge; // значение переменной myAge больше значения переменной yourAge? также возвратит 0 (или false). Предупреждение:Многие начинающие программировать на языке C++ путают оператор присваивания (=) с оператором равенства (==). Случайное использование не того оператора может привести к такой ошибке, которую трудно обнаружить. Всего в языке C++ используется шесть операторов отношений: равно (==), меньше (<), больше (>), меньше или равно (<=), больше или равно (>=) и не равно (!=). В табл. 4.1 не только перечислены все операторы отношений, но и приведены примеры их использования. Рекомендуется: Помните, что операторы отношений true или false. He рекомендуется:Не путайте оператор присваивания (=) с оператором равенства (==). Это одна из самых распространенных ошибок программирования на языке C++ — будьте начеку! Таблица 4.1. Операторы отношений Оператор ifОбычно программа выполняется по порядку, строка за строкой. Оператор if позволяет проверить условие (например, равны ли две переменные) и изменить ход выполнения программы, направив ее в другое русло, которое будет зависеть от результата сравнения. Простейшая форма оператора if имеет следующий вид: if(условие) выражение; Условие в круглых скобках может быть любым выражением, но обычно оно содержит операторы отношений. Если это выражение возвращает false, то последующий оператор пропускается. Если же оно возвращает значение true, то оператор выполняется. Рассмотрим следующий пример: if(bigNumber > smallNumber) bigNumber = smallNumber; Здесь сравниваются значения переменных bigNumber и smallNumber. Если значение переменной bigNumber больше, то во второй строке этого программного фрагмента ее значение устанавливается равным значению переменной smallNumber. Поскольку блок выражений, заключенных в фигурные скобки, эквивалентен одному выражению, то это свойство позволяет за строкой с оператором if использовать целые блоки, которые могут быть довольно обширными: if(условие) { выражение 1; выражение2; выражениеЗ; } Вот простой пример применения блока выражений: if(bigNumber > smallNumber) { bigNumber = smallNumber; cout << "bigNumber: " << bigNumber << "\n"; cout << "smallNumber: " << smallNumber << "\n"; } На этот раз, если значение переменной bigNumber больше значения переменной smallNumber, то большая переменная не только устанавливается равной значению меньшей переменной, но на экран выводятся также информационные сообщения. В листинге 4.5 показан еще один пример ветвления программы, основанный на использовании операторов отношений. Листинг 4.5. Пример ветвления с использованием операторов отношений 1: // Листинг 4.5. Демонстрирует использование 2: // инструкции if совместно с операторами отношений 3: #include <iostream.h> 4: int main() 5: { 6: int RedSoxScore, YankeesScore; 7: cout << "Enter the score for the Red Sox: "; 8: cin >> RedSoxScore; 9: 10: cout << "\nEnter the score for the Yankees: "; 11: cin >> YankeesScore; 12: 13: cout << "\n"; 14: 15: if (RedSoxScore > YankeesScore) 16: cout << "Go Sox!\n"; 17: 18: if (RedSoxScore < YankeesScore) 19: { 20: cout << "Go Yankees!\n"; 21: cout << "Happy days in New York!\n"; 22: } 23: 24: if (RedSoxScore == YankeesScore) 25: { 26: cout << "A tie? Naah, can't be.\n"; 27: cout << "Give me the real score for the Yanks: "; 28: cin >> YankeesScore; 29: 30: if (RedSoxScore > YankeesScore) 31: cout << "Knew it! Go Sox!"; 32: 33: if (YankeesScore > RedSoxScore) 34: cout << "Knew it! Go Yanks!"; 35: 36: if (YankeesScore == RedSoxScore) 37: cout << "Wow, it really was a tie!"; 38: 39: } 40: cout << "\nThanks for telling me.\n"; 41: return 0; 42: } Результат: Enter the score for the Red Sox: 10 Enter the score for the Yankees: 10 A tie? Naah, can't be Give me the real score for the Yanks: 8 Knew it! Go Sox! Thanks for telling me.. Анализ: В этой программе пользователю предлагается ввести счет очков для двух бейсбольных команд. Введенные очки сохраняются в целочисленных переменных. Значения этих переменных сравниваются оператором if в строках 15, 18 и 24. Предупреждение:Многие начинающие программисты по невнимательности ставят точку с запятой после выражения с оператором if: if (SomeValue < 10); SomeValue = 10; В этом программном фрагменте было задумано сравнить значение переменной SomeValue с числом 10 и, если окажется, что оно меньше десяти, установить его равным этому числу, т.е. зафиксировать минимальное значение переменной SomeValue на уровне 10. При выполнении этого программного фрагмента обнаруживается, что переменная SomeValue (вне зависимости от ее исходного значения) всегда устанавливается равной 10. В чем же дело? А дело в том, что оператор if, вернее, связанное с ним выражение сравнения, оканчивается точкой с запятой, создавая тем самым бездействующую инструкцию. Помните, что для компилятора отступ не играет никакой роли. Приведенный выше программный фрагмент можно переписать по-другому: if (SomeValue < 10) // проверка ; // пустое выражение, контролируемое оператором if SomeValue = 10; // присваивание При удалении ненужной точки с запятой последняя строка этого фрагмента станет частью конструкции с оператором if и программа заработает в соответствии с намерением программиста. Если очки одной команды больше очков другой, на экран выводится соответствующее сообщение. Если сравниваемые очки равны, программа выполняет блок выражений, который начинается в строке 25 и оканчивается в строке 38. В этом блоке снова запрашивается счет очков для команды из Нью-Йорка, после чего вновь выполняется сравнение результатов игры команд. Обратите внимание: если начальный счет команды Yankees превышает счет команды Red Sox, то оператор if в строке 15 возвратит значение false и строка 16 не будет выполняться. Проверка же в строке 18 даст истинный результат (true) и будут выполнены выражения в строках 20 и 21. Затем с помощью оператора if в строке 24 будет проверено равенство очков; результат этого тестирования должен быть ложным (false) (иначе и быть не может, если проверка в строке 18 дала в результате значение true). В этом случае программа пропустит целый блок и перейдет сразу к выполнению строки 39. В данном примере получение истинного результата одним оператором if не избавляет от выполнения проверок другими операторами if. Использование отступов в программных кодыхОбратите внимание, как в листинге 4.4 используются отступы в конструкции с оператором if. Наверное, фанаты программирования могли бы развязать войну по поводу того, какой стиль выделения программных блоков лучше использовать. И хотя возможны десятки различных стилей, чаще других используются три перечисленных ниже. • Начальная открывающая скобка располагается после условия, а закрывающая фигурная скобка, которая завершает блок операторов, выравнивается по одной линии с оператором if: If (условие) { выражение } • Фигурные скобки располагаются под словом if, выравниваясь по одной линии, а операторы блока записываются с отступом: if (условие) { выражение } • Отступ используется как для обеих фигурных скобок, так и для выражений блока: if (условие) { выражение } Вариант, используемый в этой книге, отражает лишь пристрастие автора и ни к чему вас не обязывает. Ключевое слово elseДовольно часто в программах требуется, чтобы при выполнении некоторого условия (т.е. когда это условие возвратит значение true) программа выполняла один блок команд, а при его невыполнении (т.е. когда это условие возвратит значение false) — другой блок. В листинге 4.4 программист намеревался выводить на экран одно сообщение, если первая проверка (RedSoxScore > Yankees) возвращает значение true, и другое сообщение, если эта проверка возвращает значение false. Показанный выше способ последовательного использования нескольких операторов if для проверки ряда условий прекрасно работает, но слишком громоздкий. Улучшить читабельность программы в подобных случаях можно с помощью ключевого слова else (листинг 4.6): if (условие) выражение; else выражение; Листинг 4.6. Пример использования ключевого слова else 1: // Листинг 4.6. Пример конструкции с ключевыми 2: // словами if и else 3: #include <iostream.h> 4: int main() 5: { 6: int firstNumber, secondNumber; 7: cout << "Please enter a big number: "; 8: cin >> firstNumber; 9: cout << "\nPlease enter a smaller number: "; 10: cin >> secondNumber; 11: if (firstNumber > secondNumber) 12: cout << "\nThanks!\n"; 13: else 14: cout << "\nOops. The second is bigger!"; 15: 16: return 0; 17: } Результат: Please enter a big number: 10 Please enter a smaller number: 12 Oops. The second is bigger! Анализ: В строке 11 проверяется условие, заданное в операторе if. Если это условие истинно, будет выполнена строка 12, после чего работа программы завершится в строке 16. Если же это условие возвратит false, программа продолжит работу со строки 14. Если в строке 13 удалить ключевое слово else, строка 14 будет выполнена в любом случае, вне зависимости от выполнения условия. Но в данной конструкции if-else будет выполняться либо блок после if, либо блок после else. Помните, что в конструкции if-else можно использовать не только отдельные выражения, но и целые блоки выражений, заключенных в фигурные скобки. Сложные конструкции с ifНет никаких ограничений на использование любых операторов в блоках выражений в конструкции if-else, в том числе на использование дополнительных операторов if и else. В этом случае будет получена вложенная конструкция из нескольких операторов if: if (условие1) { if (условие2) выражение1; else { if (условиеЗ) выражение2; else выражениеЗ; } } else выражение4; Смысл этой конструкции из нескольких операторов if можно расшифровать так: если условие1 истинно и условие2 истинно, выполните выражение1. Если условие1 истинно, а условие2 — нет, тогда проверьте условиеЗ и, если оно истинно, выполните выражение2. Если условие1 истинно, а условие2 и условиеЗ — нет, тогда выполните выражениеЗ. Наконец, если условие1 ложно, выполните выражение4. Да, вложенные операторы if могут кого угодно запутать! Пример использования такой сложной конструкции с несколькими операторами if показан в листинге 4.7. Листинг 4.7. Сложные конструкции с вложенными операторами if 1: // Листинг 4.7. Пример сложной конструкции с 2: // вложенными операторами if 3: #include <iostream.h> 4: int main() 5: { 6: // Запрашиваем два числа 7: // Присваиваем числа переменным bigNumber и littleNumber 8: // Если значение bigNumber больше значения littleNumber, 9: // проверяем, делится ли большее число на меньшее без остатка 10: // Если да, проверяем, не равны ли они друг другу 11: 12: int firstNumber, secondNumber; 13: cout << "Enter two numbers.\nFirst: "; 14: cin >> firstNumber; 15: cout << "\nSecond: "; 16: cin >> secondNumber; 17: cout << "\n\n"; 18: 19: if (firstNumber >= secondNumber) 20: { 21: if ( (firstNumber % secondNumber) == 0) // evenly divisible? 22: { 23: if (firstNumber == secondNumber) 24: cout << "They are the same!\n"; 25: else 26: cout << "They are evenly divisible!\n"; 27: } 28: else 29: cout << "They are not evenly divisible!\n"; 30: } 31: else 32: cout << "Hey! The second one is larger!\n"; 33: return 0; 34: } Результат: Eriter two numbers. First: 10 Second: 2 They are evenly divisible! Анализ: Сначала пользователю предлагается ввести два числа (по очереди), затем эти числа сравниваются. С помощью первого оператора if (в строке 19) мы хотим убедиться в том, что первое число больше или равно второму. Если мы убеждаемся в обратном, то выполняется выражение после оператора else, представленного в строке 31. Если первое сравнение возвращает true, то выполняется блок инструкций, начинающийся в строке 20, где с помощью второго оператора if в строке 21 проверяется предположение, что первое число делится на второе без остатка (т.е. с остатком, равным нулю). Если это предположение подтверждается, то первое число либо кратно второму, либо они вообще равны друг другу. Оператор if в строке 23 проверяет версию о равенстве чисел, а затем на экран выводится сообщение, уведомляющее о выявленном соотношении. Если оператор if в строке 21 возвращает значение false, то выполняется оператор else в строке 28. Использование фигурных скобок для вложенных операторов ifФигурные скобки можно не использовать в конструкциях с оператором if, если эта конструкция состоит только из одного выполняемого выражения. Это справедливо и в случае вложения нескольких операторов if, как показано ниже: if (x>у) // если x больше у if (x<z) // и если x меньше z, x = у; // тогда присваиваем x значение у Однако при создании сложных вложенных конструкций без использования фигурных скобок бывает трудно разобраться, какое выражение какому оператору if принадлежит. Не забывайте, что пробелы и отступы делают программу понятнее для программиста, но никак не влияют на работу компилятора. Даже если вы покажете с помощью отступа, что данный оператор else относится к конструкции этого оператора if, компилятор может с вами не согласиться. Данная проблема иллюстрируется в листинге 4.8. Листинг 4.8. Пример использования фигурных скобок для правильного сопоставления операторов else и if 1: // Листинг 4.8. Пример использования фигурных скобок 2: // в конструкциях с вложенными операторами if 3: #include <iostream.h> 4: int main() 5: { 6: int x; 7: cout << "Enter а number less than 10 or greater than 100: "; 8: cin >> x; 9: cout << "\n"; 10: 11: if (x >= 10) 12: if (x > 100) 13: cout << "More than 100, Thanks!\n"; 14: else // к какому оператору if относится этот оператор 15: cout << "Less than 10, Thanks!\n"; 16: 17: return 0; 18: } Результат: Enter a number less than 10 or greater than 100 Less than 10, Thanks! Анализ: Программа запрашивает ввод числа меньше 10 или больше 100 и должна проверить введенное значение на соответствие выдвинутому требованию, а затем вывести сообщение. Если оператор if, расположенный в строке 11, возвращает true, то выполняется следующее выражение (строка 12). В нашем примере строка 12 выполняется в случае, если введенное число больше 10. Однако в строке 12 также содержится оператор if, который возвращает true, если введенное число не больше 100. Если же это число больше 100, выполняется строка 13. Если введенное число меньше 10, оператор if в строке 11 возвратит false. В этом случае должно выполниться выражение за оператором else (строка 15), которое выводит на экран соответствующее сообщение. Но оказывается, что если ввести число меньше 10, то программа просто завершит свою работу. Судя по отступу, оператор else в строке 14 образует единую конструкцию с оператором if в строке 11. Но в действительности оператор else связан с оператором if в строке 12, поэтому программа будет работать не так, как планировал программист. С точки зрения компилятора в этой программе на языке C++ нет никаких ошибок. Программа не работает как хотелось из-за логической ошибки. Более того, даже при тестировании этой программы может создаться впечатление, что она работает правильно. Ведь при вводе числа больше 100 программа работает нормально и дефект не проявляется. В листинге 4.9 показано, как можно исправить эту ошибку с помощью фигурных скобок. Листинг 4.9. Пример надлежащего использования фигурных скобок в конструкции с оператором if 1: // Листинг 4.9. Пример надлежащего использования 2: // фигурных скобок для вложенных операторов if 3: #include <iostream.h> 4: int main() 5: { 6: int x; 7: cout << "Enter а number less than 10 or greater than 100: "; 8: cin >> x; 9: cout << "\n"; 10: 11: if (x >= 10) 12: { 13: if (x > 100) 14: cout << "More than 100, Thanks!\n"; 15: } 16: else // теперь все ясно! 17: cout << "Less than 10, Thanks!\n"; 18: return 0; 19: } Результат: Enter a number less than 10 or greater than 100: 20 Фигурные скобки, поставленные в строках 12 и 15, превращают все, что стоит между ними, в одно выражение, и теперь оператор else в строке 16 явно связан с оператором if, стоящим в строке 11, как и было задумано. Пользователь ввел число 20, поэтому оператор if в строке 11 возвратил значение true; однако оператор if в строке 13 возвратил false, поэтому сообщение не было выведено на экран. Было бы лучше, если бы программист использовал еще один оператор else после строки 14, который выводил бы сообщение о том, что введенное число не отвечает требованиям. Примечание:Программы, приведенные в этой книге, написаны для демонстрации частных вопросов, рассматриваемых в данном разделе. Они намеренно написаны как можно проще; при этом не ставилась цель предусмотреть все возможные ошибки, как это делается в профессиональных программах. Логические операторыДовольно часто у нас возникает необходимость проверять не одно условное выражение, а сразу несколько. Например, правда ли, что x больше у, а также что у больше z? Наша программа, прежде чем выполнить соответствующее действие, должна установить, что оба эти условия истинны либо одно из них ложно. Представьте себе высокоорганизованную сигнальную систему, обладающую следующей логикой. Если сработала установленная на двери сигнализация И время суток после шести вечера, И сегодня НЕТ праздника ИЛИ сегодня выходной, нужно вызывать полицию. Для проверки всех условий нужно использовать три логических оператора C++. Они перечислены в табл. 4.2. Таблица 4.2. Логические операторы Логическое ИЛогический оператор И вычисляет два выражения, и если оба выражения возвращают true, то и оператор И также возвращает true. Если правда то, что вы голодны, И правда то, что у вас есть деньги, значит, справедливо и то, что вы можете пойти в супермаркет и купить себе что-нибудь на обед. Например, логическое выражение if ( (x == 5) && (у == 5) ) возвратит значение true, если и обе переменные — x и у — равны 5. Это же выражение возвратит false, если хотя бы одна из переменных не равна 5. Обратите внимание, что выражение возвращает true только в том случае, если истинны обе его части. Логический оператор И обозначается двумя символами &&. Одиночный символ & соответствует совсем другому оператору, о котором пойдет речь на занятии 21. Логическое ИЛИЛогический оператор ИЛИ также вычисляет два выражения. Если любое из них истинно, то и оператор ИЛИ возвращает true. Если у вас есть деньги ИЛИ у вас есть кредитная карточка, вы можете оплатить счет. При этом нет необходимости в соблюдении двух условий сразу: иметь и деньги, и кредитную карточку. Вам достаточно выполнения одного из них (хотя и то и другое — еще лучше). Например, выражение if ( (x == 5) || (у == 5) ) возвратит значение true, если либо значение переменной x, либо значение переменной у, либо они оба равны 5. Обратите внимание: логический оператор ИЛИ обозначается двумя символами 11. Оператор, обозначаемый одиночным символом |, — это совсем другой оператор, о котором пойдет речь на занятии 21. Логическое НЕТЛогический оператор НЕТ возвращает значение true, если тестируемое выражение является ложным (имеет значение false). И наоборот, если тестируемое выражение является истинным, то оператор НЕТ возвратит false! Например, выражение if ( !(x == 5) ) возвратит значение true только в том случае, если x не равно числу 5. Это же выражение можно записать и по-другому: if (x != 5) Вычисление по сокращенной схемеПредположим, компилятору повстречалось следующее логическое выражение: if ( (x == 5) && (у == 5) ) В таком случае компилятор сначала оценит первое выражение (x == 5) и, если оно возвратит false (т.е. x не равно числу 5), не станет вычислять второе выражение (у == 5), поскольку для истинности всего выражения с оператором И нужно, чтобы обе его составляющие были истинными. Аналогично, если компилятору повстречается выражение с оператором ИЛИ if ( (x == 5) || (у == 5) ) и первое выражение окажется истинным (x == 5), то компилятор также не станет вычислять второе выражение (у == 5), поскольку ему достаточно одного истинного результата, чтобы признать истинным все выражение. Приоритеты операторов отношенийОператоры отношений и логические операторы используются в выражениях языка C++ и возвращают значения true или false. Подобно всем другим операторам, им присущ некоторый уровень приоритета (см. приложение А), который определяет порядок вычисления операторов отношений. Этот момент нужно обязательно учитывать при определении значения такого выражения, как if ( x > 5 && у > 5 || 2 > 5) В данном случае о намерениях программиста можно только догадываться. Возможно, он хотел, чтобы это выражение возвращало значение true, если x и у больше 5 или если z больше 5. С другой стороны, может быть, программист хотел, чтобы это выражение возвращало true только в том случае, если x больше 5 и либо у, либо z больше 5. Если x равен 3, а у и z оба равны 10, то при использовании первой интерпретации намерений программиста это выражение возвратит значение true (z больше 5, поэтому игнорируем значения x и у), но при использовании второй интерпретации вернется значение false (oHp не может дать значение true, поскольку для этого требуется, чтобы значение x было больше 5, а после установления этого факта результат вычисления выражения справа от оператора && не важен, ведь для истинности всего выражения обе его части должны быть истинными). Разобраться в приоритетах операторов в подобных выражениях довольно сложно, поэтому стоит воспользоваться круглыми скобками — ведь с их помощью можно не только изменить последовательность выполнения операторов, обусловленную их приоритетами, но и сделать ясными подобные запутанные выражения: if ( (x > 5) && (у > 5 | | z > 5) ) При использовании предложенных выше значений это выражение возвращает значение false. Поскольку оказалось, что x (его значение равно 3) не больше 5, то выражение слева от оператора И возвращает false, а следовательно, и все выражение целиком тоже возвратит false. Помните, что оператор И возвращает true только в том случае, когда обе части выражения возвращают true. Например, ваш вкус можно считать хорошим только в том случае, если о надетой на вас вещи можно сказать, что она модная и что вам она идет. Примечание:Часто дополнительные круглые скобки стоит использовать только для четкого определения того, что именно вы хотите сгруппировать. Помните, что цель программиста — написать программу, которая прекрасно работает, а также легко читается и понимается. Подробнее об истине и лжиВ языке C++ нуль эквивалентен значению false, а все другие числовые значения эквивалентны значению true. Поскольку любое выражение всегда имеет значение, многие программисты пользуются преимуществами этой эквивалентности значений в выражениях условия оператора if. Такое выражение, как if (x) // если x не равен нулю, то условие истинно x = 0; можно читать следующим образом: если переменная x имеет ненулевое значение, устанавливаем ее равной нулю. Чтобы сделать смысл этого выражения более очевидным, можно записать его так: if (x ! = 0) // если x не нуль x = 0; Оба выражения одинаково правомочны, но последнее понятнее. И еще один момент. Чтобы программа не превратилась в сплошное шаманство, лучше все-таки проверять истинность некоторых логических условий, а не равенство выражения нулю. Следующие два выражения также эквивалентны: if (!x) // истинно, если x равен нулю if (x == 0) // если x равен нулю Однако, второе выражение проще для понимания и гораздо очевиднее, поскольку явно проверяется математическое значение переменной x. Рекомендуется: Используйте круглые скобки, чтобы более четко указать порядок выполнения операторов или изменить их приоритеты. Используйте фигурные скобки в конструкциях с вложенными операторами if, чтобы четко определить взаимосвязи между частями конструкции и избежать ошибок. Не рекомендуется: Не используйте выражение if(x) как эквивалент выражения if(x ! = 0). Последний вариант предпочтительнее, поскольку четче видна логика проверки условия. Не используйте выражение if(!х) как эквивалент выражения if(x == 0). Последний вариант предпочтительнее, поскольку четче видна лотка проверки условия. Условный операторУсловный оператор (?:) — это единственный оператор в языке C++, который работает сразу с тремя операндами. Синтаксис использования условного оператора следующий: (выражение1) ? (выражение2) : (выражениеЗ) Эта строка читается таким образом: если выражение1 возвращает true, то выполняется выражение2, в противном случае выполняется выражениеЗ. Обычно возвращаемое значение присваивается некоторой переменной. В листинге 4.10 показано использование условного оператора вместо оператора if. Листинг 4.10. Пример использования условного оператора 1: // Листинг 4.10. Пример использования условного оператора 2: // 3: #include <iostream.h> 4: int main() 5: { 6: int x. у, z; 7: cout << "Enter two numbers.\n"; 8: cout << "First: "; 9: cin >> x; 10: cout << "\nSecond: "; 11: cin >> у; 12: cout << "\n"; 13: 14: if (x > у) 15: z = x; 16: else 17: z = у; 18: 19: cout << "z: " << z; 20: cout << "\n"; 21: 22: z = (x > у) ? x : у; 23: 24: cout << "z: " << z; 25: cout << "\n"; 26: return 0; 27: } Результат: Enter two numbers. First: 5 Second: 8 z: 8 z: 8 Анализ: Сначала создается три целочисленные переменные: x, у и z. Значения первых двух вводятся пользователем. Затем в строке 14 выполняется инструкция if, которая позволяет узнать, какое из введенных значений больше, причем выявленное большее значение присваивается переменной z. Это значение выводится на экран в строке 19. Ту же самую проверку выполняет в строке 22 условный оператор и присваивает переменной z большее значение. Он читается следующим образом: "Если x больше у, возвращаем значение x; в противном случае возвращаем значение у". Возвращаемое значение присваивается переменной z. Это значение выводится на экран в строке 24. Как видите, инструкция, содержащая условный оператор, является более коротким эквивалентом инструкции if...else. РезюмеДанное занятие охватывает большой объем материала. Вы узнали, что представляют собой операторы и выражения в языке C++, какие разновидности операторов существуют в C++ и как работает оператор if. Теперь вы знаете, что блок выражений, заключенный внутри пары фигурных скобок, можно использовать вместо одиночного выражения. Вы также узнали, что каждое выражение возвращает некоторое значение и что это значение можно проверить с помощью инструкции if или условного оператора. Теперь с помощью логических операторов вы можете проверять ряд условий и сравнивать различные значения с помощью операторов отношений. Кроме того, используя оператор присваивания, вы научились присваивать значения переменным. Вы также познакомились с приоритетами операторов и теперь знаете, как с помощью круглых скобок изменить порядок выполнения операторов, обусловленный их приоритетами, и упростить анализ программного кода. Вопросы и ответыЗачем нужны круглые скобки, если можно определить последовательность выполнения операторов по их приоритетам? Действительно, как программисту, так и компилятору должны быть хорошо известны приоритеты выполнения всех операторов. Но, несмотря на это, стоит использовать круглые скобки, если они облегчают понимание программы, а значит, и дальнейшую работу с ней. Если операторы отношений всегда возвращают значения true или false, то почему любое ненулевое значение считается истинным (true)? Операторы отношений всегда возвращают значения true или false, но в выражениях условий можно использовать любые другие выражения, возвращающие числовые значения. Например: if ( (x = а + b) == 35 ) Это вполне подходящее условие для выражения на языке C++. При его выполнении будет вычислено значение даже в том случае, если сумма переменных а и Ь не равна числу 35. Кроме того, обратите внимание, что переменной x в любом случае будет присвоено значение переменных а и b. Какое воздействие оказывают на программу символы табуляции, пробелы и символы перехода на новую строку? Символы табуляции, пробелы и символы разрывов строк (все их часто называют символами пробелов) никак не влияют на программу, хотя могут сделать ее более читабельной. Отрицательные числа считаются истинными или ложными? Все числа, не равные нулю (как положительные, так и отрицательные), воспринимаются как истинные. КоллоквиумВ этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний и приводится несколько упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из предложенных ниже вопросов. Контрольные вопросы1. Что такое выражение? 2. Является ли запись x = 5 + 7 выражением? Каково его значение? 3. Каково значение выражения 201 / 4? 4. Каково значение выражения 201 % 4? 5. Если переменные myAge, а и b имеют тип int, то каковы будут их значения после выполнения выражения: myAge = 39; а = myAge++; b = ++myAge; 6. Каково значение выражения 8+2*3? 7. Какая разница между выражениями if(x = 3) и if(x == 3)? 8. Будут ли следующие выражения возвращать true или false? а) 0 б) 1 в) -1 г) x = 0 д) x == 0 // предположим, что x имеет значение 0 Упражнения1. Напишите один оператор if, который проверяет две целочисленные переменные и присваивает переменной с большим значением меньшее значение, используя только один дополнительный оператор else. 2. Проанализируйте следующую программу. Представьте, что вы ввели три значения. Какой результат вы ожидаете получить? 1: #include <iostream.h> 2: int main() 3: { 4: int a, b, с; 5: cout << "Please enter three numbers\n"; 6: cout << "a: "; 7: cin >> a; 8: cout << "\nb: "; 9: cin >> b; 10: cout << "\nc: "; 11: cin >> c; 12: 13: if (c = (a-b)) 14: { cout << "a: "; 15: cout << a: 16: cout << "minus b: "; 17: cout << b; 18: cout << "equals c: "; 19: cout << c << endl;} 20: else 21: cout << "a-b does not equal c: " << endl; 22: return 0; 23: } 3. Введите программу из упражнения 2; скомпилируйте, скомпонуйте и запустите ее на выполнение. Введите числа 20, 10 и 50. Вы получили результат, который и ожидали? Почему нет? 4. Проанализируйте эту программу и спрогнозируйте результат: 1: #include <iostream.h> 2: int main() 3: { 4: int а = 1, Ь = 1, с; 5: if (с = (a-b)) 6: cout << "The value of с is: " << с; 7: return 0; 8: } 5. Введите, скомпилируйте, скомпонуйте и запустите на выполнение программу из упражнения 4. Каков был результат? Почему? День 5-й. ФункцииНесмотря на то что при объектно-ориентированном программировании внимание акцентируется не на функциях, а на объектах, функции тем не менее остаются центральным компонентом любой программы. Итак, сегодня вы узнаете: • Что такое функция и из чего она состоит • Как объявлять и определять функции • Как передавать параметры функциям • Как возвращать значение функции Что такое функцияФункция по своей сути — это подпрограмма, которая может манипулировать данными и возвращать некоторое значение. Каждая программа C++ имеет по крайней мере одну функцию main(), которая при запуске программы вызывается автоматически. Функция main() может вызывать другие функции, те, в свою очередь, могут вызывать следующие и т.д. Каждая функция обладает собственным именем, и, когда оно встречается в программе, управление переходит к телу данной функции. Этот процесс называется вызовом функции (или обращением к функции). По возвращении из функции выполнение программы возобновляется со строки, следующей после вызова функции. Такая схема выполнения программы показана на рис. 5.1. Хорошо разработанные функции должны выполнять конкретную и вполне понятую задачу. Сложные задачи следует "разбивать" на несколько более простых, достаточно легко реализуемых с помощью отдельных функций, которые затем могут вызываться по очереди. Различают два вида функций: определяемые пользователем (нестандартные) и встроенные. Встроенные функции являются составной частью пакета компилятора и предоставляются фирмой-изготовителем. Нестандартные функции создаются самим программистом. Рис. 5.1. Когда программа вызывает функцию, управление переходит к телу функции, а затем выполнение программы возобновляется со строки, следующей после вызова Возвращаемые значения, параметры и аргументыФункции могут возвращать значения. После обращения к функции она может выполнить некоторые действия, а затем в качестве результата своей работы послать назад некоторое значение. Оно называется возвращаемым значением, причем тип этого значения обязательно должен быть объявлен. Таким образом, запись int myFunction(); объявляет, что функция myFunction возвращает целочисленное значение. В функцию можно также и посылать некоторые значения. Описание посылаемых значений называется списком параметров. int myFunction(int someValue, float someFloat); Это объявление означает, что функция myFunction не только возвращает целое число, но и принимает два значения в качестве параметров: целочисленное и вещественное. Параметр описывает тип значения, которое будет передано функции при ее вызове. Фактические значения, передаваемые в функцию, называются аргументами. int theValueReturned = myFunction(5,6.7); Здесь целая переменная theValueReturned инициализируется значением, возвращаемым функцией myFunction, и что в качестве аргументов этой функции передаются значения 5 и 6,7. Тип аргументов должен соответствовать объявленным типам параметров. Объявление и определение функцийИспользование функций в программе требует, чтобы функция сначала была объявлена, а затем определена. Посредством объявления функции компилятору сообщается ее имя, тип возвращаемого значения и параметры. Благодаря определению функции компилятор узнает, как функция работает. Ни одну функцию нельзя вызвать в программе, если она не была предварительно объявлена. Объявление функции называется прототипом. Объявление функцииСуществует три способа объявления функции. • Запишите прототип функции в файл, а затем используйте выражение с #include, чтобы включить его в свою программу. • Запишите прототип функции в файл, в котором эта функция используется. • Определите функцию перед тем, как ее вызовет любая другая функция. В этом случае определение функции одновременно и объявляет ее. Несмотря на то что функцию можно определить непосредственно перед использованием и таким образом избежать необходимости создания прототипа функции, такой стиль программирования не считается хорошим по трем причинам. Во-первых, требование располагать функции в файле в определенном порядке затрудняет поддержку программы в процессе изменения условий ее использования. Во-вторых, вполне возможна ситуация, когда функции A() необходимо вызвать функцию B(), но не исключено также, что при некоторых обстоятельствах и функции B() потребуется вызвать функцию A(). Однако невозможно определить функцию A() до определения функции B() и в то же время функцию B() до определения функции A(), т.е. по крайней мере одна из этих функций обязательно должна быть предварительно объявлена. В-третьих, прототипы функций — это хорошее и сильное подспорье при отладке. Если согласно прототипу объявлено, что функция принимает определенный набор параметров или что она возвращает значение определенного типа, а затем в программе делается попытка использовать функцию, не соответствующую объявленному прототипу, то компилятор заметит эту ошибку еще на этапе компиляции программы, что позволит избежать неприятных сюрпризов в процессе ее выполнения. Прототипы функцийПрототипы многих встроенных функций уже записаны в файлы заголовков, добавляемые в программу с помощью #include. Для функций, создаваемых пользователями, программист должен сам позаботиться о включении в программу соответствующих прототипов. Рис. 5.2. Составные части прототипа функции. Прототип функции представляет собой выражение, оканчивающееся точкой с запятой, и состоит из типа возвращаемого значения функции и сигнатуры. Под сигнатурой функции подразумевается ее имя и список формальных параметров. Список формальных параметров представляет собой список всех параметров и их типов, разделенных запятыми. Составные части прототипа функции показаны на рис. 5.2. В прототипе и в определении функции тип возвращаемого значения и сигнатура должны соответствовать. Если такого соответствия нет, компилятор покажет сообщение об ошибке. Однако прототип функции не обязан содержать имена параметров, он может ограничиться только указанием их типов. Например, прототип, приведенный ниже, абсолютно правомочен: long Area(int, int); Этот прототип объявляет функцию с именем Area(), которая возвращает значение типа long и принимает два целочисленных параметра. И хотя такая запись прототипа вполне допустима, это не самый лучший вариант. Добавление имен параметров делает ваш прототип более ясным. Та же самая функция, но уже с именованными параметрами, выглядит гораздо понятнее: long Area(int length, int width); Теперь сразу ясно, для чего предназначена функция и ее параметры. Обратите внимание на то, что для каждой функции всегда известен тип возвращаемого значения. Если он явно не объявлен, то по умолчанию принимается тип int. Однако ваши программы будут понятнее, если для каждой функции, включая main(), будет явно объявлен тип возвращаемого значения. В листинге 5.1 приводится программа, которая содержит прототип функции Area(). Листинг 5.1. Объявление, определение и использование функции 1: // Листинг 5.1. Использование прототипов функций 2: 3: #include <iostream.h> 4: int Area(int length, int width); //прототип функции 5: 6: int main() 7: { 8: int lengthOfYard; 9: int widthOfYard; 10: int areaOfYard; 11: 12: cout << "\nHow wide is your yard? "; 13: cin >> widthOfYard; 14: cout << "\nHow long is your yard? "; 15: cin >> lengthOfYard; 16: 17: areaOfYard= Area(lengthOfYard,widthOfYard); 18: 19: cout << "\nYour yard is "; 20: cout << areaOfYard; 21: cout << " square feet\n\n"; 22: return 0; 23: } 24: 25: int Area(int yardLength', int yardWidth) 26: { 27: return yardLength * yardWidth; 28: } Результат: How wide is your yard? 100 How long is your yard? 200 Your yard is 20000 square feet Анализ: Прототип функции Area() объявляется в строке4. Сравните прототип с определением функции, представленным в строке 25. Обратите внимание, что их имена, типы возвращаемых значений и типы параметров полностью совпадают. Если бы они были различны, то компилятор показал бы сообщение об ошибке. Единственное обязательное различие между ними состоит в том, что прототип функции оканчивается точкой с запятой и не имеет тела. Обратите также внимание на то, что имена параметров в прототипе — length и width — не совпадают с именами параметров в определении: yardLength и yardWidth. Как упоминалось выше, имена в прототипе не используются; они просто служат описательной информацией для программиста. Соответствие имен параметров прототипа именам параметров в определении функции считается хорошим стилем программирования; но это не обязательное требование. Аргументы передаются в функцию в порядке объявления и определения параметров, но без учета какого бы то ни было совпадения имен. Если в функцию Area() первым передать аргумент widthOfYard, а за ним — аргумент lengthOfYard, то эта функция использует значение widthOfYard для параметра yardLength, а значение lengthOfYard — для параметра yardWidth. Тело функции всегда заключается в фигурные скобки, даже если оно состоит только из одной строки, как в нашем примере. Определение функцииОпределение функции состоит из заголовка функции и ее тела. Заголовок подобен прототипу функции за исключением того, что параметры в данном случае именованные и в конце заголовка отсутствует точка с запятой. Рис. 5.3. Заголовок и тело функции Тело функции представляет собой набор выражений, заключенных в фигурные скобки. Заголовок и тело функции показаны на рис. 5.3. Синтаксис прототипа функции: тип_возврата имя_функции ([тип [имя_параметра]...]); { выражения; } Прототип функции сообщает компилятору тип возвращаемого значения, имя функции и список параметров. Наличие параметров не обязательно, но если они все-таки имеются, в прототипе должны быть объявлены их типы. Имена параметров перечислять необязательно. Строка прототипа всегда оканчивается точкой с запятой (;). Определение функции должно соответствовать своему прототипу по типу возвращаемого значения и списку параметров. Оно должно содержать имена всех параметров, а тело определения функции должно заключаться в фигурные скобки. Все выражения внутри тела функции должны оканчиваться точкой с запятой, кроме заголовка функции. Который оканчивается закрывающей круглой скобкой. Если функция возвращает значение, она должна содержать выражение с оператором return. Это выражение может находиться в любой части определения функции, но обычно оканчивает его. Для каждой функции задается тип возвращаемого значения. Если он явно не определен. по умолчанию устанавливается тип возврата lnt. Старайтесь всегда указывать тип возвращаемого значения в явном виде. Если функция не возвращает никакого значения, то в качестве типа возвращаемого значения используйте void. Примеры прототипов функций: long FindArea(long length, long width); // возвращает значение типа long, имеет два параметра void PrintMessage(int messageNumber); // возвращает значение типа void, имеет один параметр int GetChoice(); // возвращает значение типа int, не имеет параметров BadFunction(); // возвращает значение типа int, не имеет параметров Примеры определений функций: long FindArea(long l, iong w) { return 1 * w; } void PrintMessage(int whichMsg) { if (whichMsg == 0) cout << "Hello.\n"; if (whichMsg == 1) cout << "Goodbye.\n"; if (whlchMsg > 1) cout << "I'm confused.\n"; } Выполнение функцийПри вызове функции ее выполнение начинается с выражения, которое стоит первым после открывающей фигурной скобки ({). В теле функции можно реализовать ветвление, используя условный оператор if (и некоторые другие операторы, которые рассматриваются на занятии 7). Функции могут также вызывать другие функции и даже самих себя (о рекурсии речь пойдет ниже в этой главе). Локальные переменныеВ функции можно не только передавать значения переменных, но и объявлять переменные внутри тела функции. Это реализуется с помощью локальных переменных, которые называются так потому, что существуют только внутри самой функции. Когда выполнение программы возвращается из функции к основному коду, локальные переменные удаляются из памяти. Локальные переменные определяются подобно любым другим переменным. Параметры, переданные функции, гоже считаются локальными переменными, и их можно использовать как определенные внутри тела функции. В листинге 5.2 представлен пример использования параметров функции и переменных, локально определенных внутри функции. Листинг 5.2. Использование локальных переменных u параметров функции 1: #include <iostream.h> 2: 3: float Convert(float); 4: int main() 5: { 6: float TempFer; 7: float TempCel; 8: 9: cout << "Please enter the temperature in Fahrenheit: "; 10: cin >> TempFer; 11: TempCel = Convert(TempFer); 12: cout << "\nHere's the temperature in Celsius: "; 13: cout << TempCel << endl; 14: return 0; 15: } 16: 17: float Convert(float TempFer) 18: { 19: float TempCel; 20: TempCel = ((TempFer - 32) * 5) / 9; 21: return TempCel; 22: } Результат: Please enter the temperature in Fahrenheit: 212 Here's the temperature in Celsius: 100 Please enter the temperature in Fahrenheit: 32 Here's the temperature in Celsius: 0 Please enter the temperature in Fahrenheit: 85 Here's the temperature in Celsius: 25.4444 Анализ: В строках 6 и 7 объявляются две переменные типа float: одна (TempFer) для хранения значения температуры в градусах по Фаренгейту, а другая (TempCel) — в градусах по Цельсию. В строке 9 пользователю предлагается ввести температуру по Фаренгейту, и это значение затем передается функции Convert(). После вызова функции Convert() программа продолжает выполнение с первого выражения в теле этой функции, представленного строкой 19, где объявляется локальная переменная, также названная TempCel. Обратите внимание, что эта локальная переменная — не та же самая переменная TempCel, которая объявлена в строке 7. Эта переменная существует только внутри функции Convert(). Значение, переданное в качестве параметра TempFer, также является лишь переданной из функции main() локальной копией одноименной переменной. В функции Convert() можно было бы задать параметр FerTemp и локальную переменную CelTemp, что не повлияло бы на работу программы. Чтобы убедиться в этом, можете ввести новые имена и перекомпилировать программу. Локальной переменной TempCel присваивается значение, которое получается в результате выполнения следующих действий: вычитания числа 32 из параметра TempFer, умножения этой разности на число 5 с последующим делением на число 9. Результат вычислений затем возвращается в качестве значения возврата функции, и в строке 11 оно присваивается переменной TempCel функции main(). В строке 13 это значение выводится на экран. В нашем примере программа запускалась трижды. В первый раз вводится значение 212, чтобы убедиться в том, что точка кипения воды по Фаренгейту (212) сгенерирует правильный ответ в градусах Цельсия (100). При втором испытании вводится значение точки замерзания воды. В третий раз — случайное число, выбранное для получения дробного результата. В качестве примера попробуйте запустить программу снова с другими именами переменных, как показано ниже. Должен получиться тот же результат. Каждая переменная характеризуется своей областью видимости, определяющей время жизни и доступность переменной в программе. Переменные, объявленные внутри некоторого блока программы, имеют область видимости, ограниченную этим блоком. К ним можно получить доступ только в пределах этого блока, и после того, как выполнение программы выйдет за пределы, все его локальные переменные автоматически удаляются из памяти. Глобальные же переменные имеют глобальную область видимости и доступны из любой точки программы. Обычно область видимости переменных очевидна по месту их объявления, но некоторые исключения все же существуют. Подробнее об этом вы узнаете при рассмотрении циклов в занятии 7. 1: #include <iostream.h> 2: 3: float Convert(float); 4: int main() 5: { 6: float TempFer; 7: float TempCel; 8: 9: cout << "Please enter the temperature in Fahrenheit: "; 10: cin >> TempFer; 11: TempCel = Convert(TempFer); 12: cout << "\nHere's the temperature in Celsius: "; 13: cout << TempCel << endl; 14: return 0; 15: } 16: 17: float Convert(float Fer) 18: { 19; float Cel; 20; Cel = ((Fer - 32) * 5) / 9; 21: return Cel; 22: } Обычно с использованием переменных в функциях не возникает больших проблем, если ответственно подходить к присвоению имен и следить за тем, чтобы в пределах одной функции не использовались одноименные переменные. Глобальные переменныеПеременные, определенные вне тела какой-либо функции, имеют глобальную область видимости и доступны из любой функции в программе, включая main(). Локальные переменные, имена которых совпадают с именами глобальных переменных, не изменяют значений последних. Однако такая локальная переменная скрывает глобальную переменную. Если в функции есть переменная с тем же именем, что и у глобальной, то при использовании внутри функции это имя относится к локальной переменной, а не к глобальной. Это проиллюстрировано в листинге 5.3. Листинг 5.3. Демонстрация использования ело глобальных и локальных переменных 1: #include <iostream.h> 2: void myFunction(); // прототип 3: 4: int x = 5, у = 7; // глобальные переменные 5: int main() 6: { 7: 8: cout << "x from main: " << x << "\n"; 9: cout << "у from main; " << у << "\n\n"; 10: myFunction(); 11: cout << "Back from myFunction!\n\n"; 12: cout << "x from main: " << x << '\n"; 13: cout << "y from main: " << y << "\n"; 14: return 0; 15: } 16: 17: void myFunction() 18: { 19: int y = 10; 20: 21: cout << "x from myFunction: " << x << "\n"; 22: cout << "y from myFunction: " << y << "\n\n"; 23: } Результат: x from main: 5 y from main: 7 x from myFunction: 5 y from myFunction: 10 Back from myFunction! x from main: 5 y from main: 7 Анализ: Эта простая программа иллюстрирует несколько ключевых моментов, связанных с использованием локальных и глобальных переменных, на которых часто спотыкаются начинающие программисты. В строке 4 объявляются две глобальные переменные — x и у. Глобальная переменная x инициализируется значением 5, глобальная переменная у — значением 7. В строках 8 и 9 в функции main() эти значения выводятся на экран. Обратите внимание, что хотя эти переменные не объявляются в функции main(), они и так доступны, будучи глобальными. При вызове в строке 10 функции myFunction() выполнение программы продолжается со строки 18, а в строке 19 объявляется локальная переменная у, которая инициализируется значением 10. В строке 21 функция myFunction() выводит значение переменной x. При этом используется глобальная переменная x, как и в функции main(). Однако в строке 22 при обращении к переменной у на экран выводится значение локальной переменной у, в то время как глобальная переменная с таким же именем оказывается скрытой. После завершения выполнения тела функции управление программой возвращается функции main(), которая вновь выводит на экран значения тех же глобальных переменных. Обратите внимание, что на глобальную переменную у совершенно не повлияло присвоение значения локальной переменной в функции myFunction(). Глобальные переменные; будьте начекуСледует отметить, что в C++ глобальные переменные почти никогда не используются. Язык C++ вырос из С, где использование глобальных переменных всегда было чревато возникновением ошибок, хотя обойтись без их применения также было не возможно. Глобальные переменные необходимы в тех случаях, когда программисту нужно сделать данные доступными для многих функций, а передавать данные из функции в функцию как параметры проблематично. Опасность использования глобальных переменных исходит из их общедоступности, в результате чего одна функция может изменить значение переменной незаметно для другой функции. В таких ситуациях возможно появление ошибок, которые очень трудно выявить. На занятии 14 вы познакомитесь с мощной альтернативой использованию глобальных переменных, которая предусмотрена в C++, но недоступна в языке С. Подробнее о локальных переменныхО переменных, объявленных внутри функции, говорят, что они имеют локальную область видимости. Это означает, как упоминалось выше, что они видимы и пригодны для использования только в пределах той функции, в которой определены. Фактически в C++ можно определять переменные в любом месте внутри функции, а не только в ее начале. Областью видимости переменной является блок, в котором она определена. Таким образом, если в теле функции будет блок, выделенный парой фигурных скобок, и в этом блоке объявляется переменная, то она будет доступна только в пределах блока, а не во всей функции, как показано в листинге 5.4. Листинг 5.4. Видимиость локальных переменных 1: // Листинг 5.4. Видимость переменных, 2: // обьявленных внутри блока 3: 4: #include <iostream.h> 5: 6: void nyFunc(); 7: 8: int main() 9: { 10: int x = 5; 11: cout << "\nIn main x is: " << x; 12: 13: myFunc(); 14: 15: cout << "\n8ack in main, x ts: " << x; 16: return 0; 17: } 18: 19: void myFunc() 20: { 21: 22: int x = 6; 23: cout << "\nIn myFunc. local x: " << x << endl; 24: 25: { 26: cout << "\nIn block in myFunc, x is: " << x; 27: 28: int x = 9; 29: 30: cout << "\nVery local x: " << x; 31: } 32: 33: cout << "\nOut of block, in myFunc, x: " << x << endl; 34: } Результат: In main x is: 5 In myFunc, local x: 8 In block in myFunc, x is Very local x; 9 Out of block, in myFunc, Back in main, x is: 5 Анализ: Эта программа начинается с инициализации локальной переменной x в функции main() (строка 10). Выведенное в строке 11 значение переменной x позволяет убедиться, что переменная х действительно была инициализирована числом 5. Затем в программе вызывается функция MyFunc(), в теле которой в строке 22 объявляется локальная переменная с тем же именем x и инициализируется значением 8. Это значение выводится на экран в строке 23. Блок, заключенный в фигурные скобки, начинается в строке 25, и в строке 26 снова выводится значение локальной переменной x. Но в строке 28 создается новая переменная с таким же именем x, которая является локальной по отношению к данному блоку. Эта переменная тут же инициализируется значением 9. Значение последней созданной переменной x выводится на экран в строке 30. Локальный блок завершается строкой 31, и переменная, созданная в строке 28, выходит за пределы видимости и удаляется из памяти. В строке 33 на экран выводится значение той переменной x, которая была объявлена в строке 22. На нее никоим образом не повлияло определение новой переменной x в строке 28, и ее значение по-прежнему равно 8. В строке 34 заканчивается область видимости функции MyFunc() и ее локальная переменная x становится недоступной. Управление программой возвращается к строке 15, в которой выводится значение локальной переменной -x, созданной в строке 10. Вы сами можете убедиться в том, что на нее не повлияла ни одна из одноименных переменных, определенных в функции MyFunc(). Нужно ли специально говорить о том, что эта программа была бы гораздо менее путаной, если бы все три переменные имели уникальные имена! Операторы, используемые в функцияхФактически на количество или типы операторов, используемых в функциях, никаких ограничений не накладывается. И хотя внутри функции нельзя определить другую функцию, но из одной функции можно вызывать сколько угодно других функций; именно этим и занимается функция main() почти в каждой программе C++. Более того, функции могут вызывать даже самих себя (эта ситуация рассматривается в разделе, посвященном рекурсии). Хотя на размер функции в C++ также никакого ограничения не накладывается, лучше, чтобы тело функции не разрасталось до неограниченных масштабов. Многие специалисты советуют сохранять небольшой размер функций, занимающий одну страницу экрана, позволяя тем самым видеть всю функцию целиком. Конечно же, это эмпирическое правило, часто нарушаемое даже очень хорошими программистами, но следует помнить: чем меньше функция, тем она проще для понимания и дальнейшего обслуживания. Каждая функция должна выполнять одну задачу, которую легко понять. Если вы замечаете, что функция начинает разрастаться, подумайте о том, не пора ли создать новую функцию. Подробнее об аргументах функцийАргументы функции могут быть разного типа. Вполне допустимо написать функцию, которая, например, принимает в качестве своих аргументов одно значение типа int, два значения типа long и один символьный аргумент. Аргументом функции может быть любое действительное выражение C++, включающее константы, математические и логические выражения и другие функции, которые возвращают некоторое значение. Использование функций в качестве параметров функцийНесмотря на то что вполне допустимо для одной функции принимать в качестве параметра вторую функцию, которая возвращает некое значение, такой стиль программирования затрудняет чтение программы и ее отладку. В качестве примера предположим, что у вас есть функции double(), triple(), square() и cube(), возвращающие некоторое значение. Вы могли бы записать следующую инструкцию: Answer = (double(triple(square(cube(myValue))))); Эта инструкция принимает переменную myValue и передает ее в качестве аргумента функции cube(), возвращаемое значение которой (куб числа) передается в качестве аргумента функции square(). После этого возвращаемое значение функции square() (квадрат числа), в свою очередь, передается в качестве аргумента функции triple(). Затем значение возврата функции triple() (утроенное число) передается как аргумент функции double(). Наконец, значение возврата функции double() (удвоенное число) присваивается переменной Answer. Вряд ли можно с полной уверенностью говорить о том, какую задачу решает это выражение (было ли значение утроено до или после вычисления квадрата?); кроме того, в случае неверного результата выявить "виноватую" функцию окажется весьма затруднительно. В качестве альтернативного варианта можно было бы каждый промежуточный результат вычисления присваивать промежуточной переменной: unsigned long myValue = 2; unsigned long cubed = cube(myValue); // 2 в кубе = 8 unsigned long squared = square(cubed); // 8 в квадрате = 64 unsigned long tripled = triple(squared); // 64 * 3 = 192 unsigned long Answer = double(tripled); // 192 *2 = 384 Теперь можно легко проверить каждый промежуточный результат, и при этом очевиден порядок выполнения всех вычислений. Параметры - это локальные переменныеАргументы, переданные функции, локальны по отношению к данной функции. Изменения, внесенные в аргументы во время выполнения функции, не влияют на переменные, значения которых передаются в функцию. Этот способ передачи параметров известен как передача как значения, т.е. локальная копия каждого аргумента создается в самой функции. Такие локальные копии внешних переменных обрабатываются так же, как и любые другие локальные переменные функции. Эта идея иллюстрируется в листинге 5.5. Листинг 5.5. Передача параметров как значений 1: // Листинг 5.5. Передача параметров как значений 2: 3: #include <iostream.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,y); 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. Before swap, x: " << x << " у: " << у << "\n"; 22: 23: temp = x; 24: x = у; 25: у = temp; 26: 27: cout << "Swap. After swap, x: " << x << " у: и << у << "\n"; 28: 29: } Результат: Main. Before swap, x: 5 y 10 Swap. Before swap, x: 5 y: 10 Swap. After swap, x: 10 y: 5 Main. After swap, x: 5 y: 10 Анализ: В программе инициализируются две переменные в функции main(), а затем их значения передаются в функцию swap(), которая, казалось бы, должна поменять их значения. Однако после повторной проверки этих переменных в функции main() оказывается, что они не изменились. Эти переменные инициализируются в строке 9, а отображение их значений на экране выполняется в строке 11. Затем вызывается функция swap(), и эти переменные передаются ей в качестве аргументов. Выполнение программы переносится в функцию swap(), где в строке 21 снова выводятся значения тех же, уже знакомых нам переменных. Как и ожидалось, их значения от передачи в функцию не изменились. В строках 23—25 переменные меняются своими значениями, что подтверждается очередной проверкой в строке 27. Но это положение сохраняется лишь до тех пор, пока программа не вышла из функции swap(). Затем управление программой передается строке 13, принадлежащей функции main(), которая показывает, что переменные получили назад свои исходные значения и все изменения, произошедшие в функции, аннулированы! Напомним, что в данном случае переменные передаются в функцию swap() как значения, т.е. в функции swap() были созданы копии этих значений, которые являются локальными по отношению к этой функции. Обмен значениями, выполненный в строках 23—25, был реализован на этих локальных переменных, но это никак не повлияло на переменные, оставшиеся в функции main(). На занятиях 8 и 10 вы узнаете альтернативные способы передачи параметров функциям, которые позволят изменять исходные переменные в функции main(). Подробнее о возвращаемых значенияхФункции возвращают либо реальное значение, либо значение типа void, которое служит сигналом для компилятора, что никакое значение возвращено не будет. Чтобы обеспечить возврат значения из функции, напишите ключевое слово return, а за ним значение, которое должно быть возвращено. В качестве возврата можно задавать как константные значения, так и целые выражения, например: return 5; return (x > 5); return (MyFunction()); Все приведенные выше выражения являются правомочными установками возврата функций, если исходить из того, что функция MyFunction() сама возвращает некоторое значение. Второе выражение, return (x > 5), будет возвращать false, если x не больше 5, или true, если x больше 5. Таким образом, если в возврате задается логическое выражение, то возвращаются не значения переменной x, а логические значения false или true (ложь или истина). После того как в функции встретится ключевое слово return, будет выполнено выражение, стоящее за этим ключевым словом, и его результат будет возвращен в основную программу по месту вызова функции. После выполнения оператора return программа немедленно переходит к строке, следующей после вызова функции, и любые выражения, стоящие в теле функции после ключевого слова return, не выполняются. Однако функция может содержать несколько операторов return. Эта идея иллюстрируется в листинге 5.6. Листинг 5.6. Использование нескольких операторов return 1: // Листинг 5.6. Использование нескольких 2: // операторов return в теле Функции 3: 4: #include<iostream.h> 5: 6: int Doubler(int AmountToDouble); 7: 6: int main() 9: { 10: 11: int result = 0: 12: int input; 13: 14: cout << "Enter а number between 0 and 10,000 to double: ": 15: cin >> input; 16: 17: cout << "\nBefore doubler is called... "; 18: cout << "\ninput: " << input << M doubled: " << result << "\n"; 19: 20: result = Doubler(input); 21: 22: cout << "\nBack from Doubler...\n"; 23: cout << "\ninput: " << input << " doubled: " << result << "\n"; 24: 25: 26: return 0; 27: } 28: 29: int 0oubler(int original) 30: { 31: if (original <= 10000) 32: return original * 2; 33: else 34: return -1; 35: cout << "Vou can't get here!\n"; 36: } Результат: Enter a number between 0 and 10,000 to double: 9000 Before doubler is called... input: 9000 doubled: 0 Back from doubler... input: 9000 doubled: 18000 Enter a number between 0 and 10.000 to double: 11000 Before doubler is called... input: 11000 doubled: 0 Back from doubler... input: 11000 doubled: -1 Анализ: В строках 14 и 15 программа предлагает пользователю ввести число и coхраняет его в переменной input. В строке 18 отображается только что введенное число вместе со значением локальной переменной result. В строке 20 вызывается функция Doubler() и введенное значение передается ей как параметр. Результат выполнения функции присваивается локальной переменной result, и в строке 23 снова выводятся значения тех же переменных. В строке 31, относящейся к функции Doubler(), значение переданного параметра сравнивается с числом 10 000. Если окажется, что оно не превышает 10 000, функция возвращает удвоенное значение исходного числа. Если оно больше 10 000, функция возвращает число -1 в качестве сообщения об ошибке. Выражение в строке 35 никогда не будет достигнуто, потому что при любом значении переданного параметра (большем 10 000 или нет) возврат из функции будет осуществлен либо в строке 32, либо в строке 34, но в любом случае до строки 35. Хороший компилятор сгенерирует предупреждение, что это выражение не может быть выполнено, и хороший программист должен принять соответствующие меры! Вопросы и ответы В чем состоит разница между объявлениями int main() и void main() и какое из них лучше использовать? Ведь оба варианта работают одинаково хорошо, поэтому стоит ли применять первый вариант int main(){ return 0; }? Оба объявления будут работать с большинством компиляторов, но только вариант int main() является ANSI-совместимым, следовательно, только объявление int main() гарантирует работу программы. По существу, отличие состоит в следующем. При использовании объявления int функция main()возвращает значение для операционной системы. После завершения работы вашей программы это значение могут перехватить, например, программы пакетной обработки. И хотя вы вряд ли будете использовать возвращаемое значение, стандарт ANSI требует его присутствия. Значения параметров, используемые по умолчаниюДля каждого параметра, объявляемого в прототипе и определении функции, должно быть передано соответствующее значение в вызове функции. Передаваемое значение должно иметь объявленный тип. Следовательно, если некоторая функция объявлена как long myFunction(int); то она действительно должна принимать целочисленное значение. Если тип объявленного параметра не совпадет с типом передаваемого аргумента, компилятор сообщит об ошибке. Из этого правила существует одно исключение, которое вступает в силу, если в прототипе функции для параметра объявляется стандартное значение. Это значение, которое используется в том случае, если при вызове функции для этого параметра не установлено никакого значения. Несколько изменим предыдущее объявление: long myFunction (int x = 50); Этот прототип нужно понимать следующим образом. Функция myFunction возвращает значение типа long и принимает параметр типа int. Но если при вызове этой функции аргумент предоставлен не будет, используйте вместо него число 50. А поскольку в прототипах функций имена параметров не обязательны, то последний вариант объявления можно переписать по-другому: long myFunction (int = 50); Определение функции не изменяется при объявлении значения параметра, задаваемого по умолчанию. Поэтому заголовок определения этой функции будет выглядеть по-прежнему: long myFunction (int x) Если при вызове этой функции аргумент не устанавливается, то компилятор присвоит переменной x значение 50. Имя параметра, для которого в прототипе устанавливается значение по умолчанию, может не совпадать с именем параметра, указываемого в заголовке функции: значение, заданное по умолчанию, присваивается по позиции, а не по имени. Установку значений по умолчанию можно назначить любым или всем параметрам функции. Но одно ограничение все же действует: если какой-то параметр не имеет стандартного значения, то ни один из предыдущих по отношению к нему параметров также не может иметь стандартного значения. Предположим, прототип функции имеет вид long myFunction (int Param1, int Param2, int Param3); тогда параметру Param2 можно назначить стандартное значение только в том случае, если назначено стандартное значение и параметру Param3. Параметру Param1 можно назначить стандартное значение только в том случае, если назначены стандартные значения как параметру Param2, так и параметру Param3. Использование значений, задаваемых параметрам функций по умолчанию, показано в листинге 5.7. Листинг 5.7. Использование значений, заданных по умолчанию для параметров функций 1: // Листинг 5.7. Использование стандартных 2: // значений параметров 3: 4: #include <iostream.h> 5: 6: int VolumeCube(int length, int width = 25, int height = 1); 7: 8: int main() 9: { 10: int length = 100; 11: int width = 50; 12: int height = 2; 13: int volume; 14: 15: volume = VolumeCube(length, width, height); 16: cout << "First volume equals: " << volume << "\n"; 17: 18: volume = VolumeCube(length, width); 19: cout << "Second time volume equals: " << volume << "\n"; 20: 21: volume = VolumeCube(length); 22: cout << "Third time volume equals: " << volume << "\n"; 23: return 0; 24: } 25: 26: VolumeCube(int length, int width, int height) 27: { 28: 29: return (length * width * height); 30: } Результат: First volume equals: 10000 Second time volume equals: 5000 Third time volume equals: 2500 Анализ: В прототипе функции VolumeCube() B строке 6 объявляется, что функция принимает три параметра, причем последние два имеют значения, устанавливаемые по умолчанию. Эта функция вычисляет объем параллелепипеда на основании переданных размеров. Если значение ширины не передано, то ширина устанавливается равной 25, а высота — 1. Если значение ширины передано, а значение высоты нет, то по умолчанию устанавливается только значение высоты. Но нельзя передать в функцию значение высоты без передачи значения ширины. В строках 10—12 инициализируются переменные, предназначенные для хранения размеров параллелепипеда по длине, ширине и высоте. Эти значения передаются функции VolumeCube() в строке 15. После вычисления объема параллелепипеда результат выводится в строке 16. В строке 18 функция VolumeCube() вызывается снова, но без передачи значения для высоты. В этом случае для вычисления объема параллелепипеда используется значение высоты, заданное по умолчанию, и полученный результат выводится в строке 19. При третьем вызове функции VolumeCube() (строка 21) не передается ни значение ширины, ни значение высоты. Поэтому вместо них используются значения, заданные по умолчанию, и полученный результат выводится в строке 22. Рекомендуется:Помните, что параметры функции действуют внутри нее, подобно локальным переменным. Не рекомендуется:Не устанавливайте значение по умолчанию для первого параметра, если для второго параметра используемого по умолчанию значения не предусмотрено. Не забывайте, что аргументы, переданные в функцию как значения, не могут повлиять на переменные, используемые при вызове функции. Не забывайте, что изменения, внесенные в глобальную переменную в одной функции, изменяют значение этой переменной для всех функций. Перегрузка функцийВ языке C++ предусмотрена возможность создания нескольких функций с одинаковым именем. Это называется перегрузкой функций. Перегруженные функции должны отличаться друг от друга списками параметров: либо типом одного или нескольких параметров, либо различным количеством параметров, либо и тем и другим одновременно. Рассмотрим следующий пример: int myFunction (int, int); int myFunction (long, long); int myFunction (long); Функция myFunction() перегружена с тремя разными списками параметров. Первая и вторая версии отличаются типами параметров, а третья — их количеством. Типы возвращаемых значений перегруженных функций могут быть одинаковыми или разными. Следует иметь в виду, что при создании двух функций с одинаковым именем и одинаковым списком параметров, но с различными типами возвращаемых значений, будет сгенерирована ошибка компиляции. Перегрузка функций также называется полиморфизмом функций. Поли (гр. poly) означает много, морфе (гр. morphe) — форма, т.е. полиморфическая функция — это функция, отличающаяся многообразием форм. Под полиморфизмом функции понимают существование в программе нескольких перегруженных версий функции, имеющих разные назначения. Изменяя количество или тип параметров, можно присвоить двум или нескольким функциям одно и то же имя. При этом никакой путаницы при вызове функций не будет, поскольку нужная функция определяется по совпадению используемых параметров. Это позволяет создать функцию, которая сможет, например, усреднять целочисленные значения, значения типа double или значения других типов без необходимости создавать отдельные имена для каждой функции — AverageInts(), AverageDoubles() и т.д. Предположим, вы пишете функцию, которая удваивает любое передаваемое ей значение. При этом вы бы хотели иметь возможность передавать ей значения типа int, long, float или double. Без перегрузки функций вам бы пришлось создавать четыре разные функции: int DoubleInt(int); long DoubleLong(long); float DoubleFloat(float); double DoubleDouble(double); С помощью перегрузки функций можно использовать следующие объявления: int Double(int); long Double(long); float Double(float); double Double(double); Благодаря использованию перегруженных функций не нужно беспокоиться о вызове в программе нужной функции, отвечающей типу передаваемых переменных. При вызове перегруженной функции компилятор автоматически определит, какой именно вариант функции следует использовать. Перегрузка функции показана в листинге 5.8. Листинг 5.8. Полиморфизм функций 1: // Листинг 5.8. Пример 2: // полиморфизма функций 3: 4: #include <iostream.h> 5: 6: int Double(int); 7: long Double(long); 8: float Double(float); 9: double Double(double); 10: 11: int main() 12: { 13: int myInt = 6500; 14: long myLong = 65000; 15: float myFloat = 6.5F; 16: double myDouble = 6.5e20; 17: 18: int doubledInt; 19: long doubledLong; 20: float doubledFloat; 21: double doubledDouble; 22: 23: cout << "myInt: " << myInt << "\n"; 24: cout << "myLong: " << myLong << "\n"; 25: cout << "myFloat: " << myFloat << "\n"; 26: cout << "myDouble: " << myDouble << "\n"; 27: 28: doubledInt = Double(myInt); 29: doubledLong = Double(myLong); 30: doubledFloat = Double(myFloat); 31: doubledDouble = Double(myDouble); 32: 33: cout << "doubledInt: " << doubledInt << "\n"; 34: cout << "doubledLong: " << doubledLong << "\n"; 35: cout << "doubledFloat: " << doubledFloat << "\n"; 36: cout << "doubledDouble: " << doubledDouble << "\n"; 37: 38: return 0; 39: } 40: 41: int Double(int original) 42: { 43: cout << "In Double(int)\n"; 44: return 2 * original; 45: } 46: 47: long Double(long original) 48: { 49: cout << "In Double(long)\n"; 50: return 2 * original; 51: } 52: 53: float Double(float original) 54: { 55: cout << "In Double(float)\n"; 56: return 2 * original; 57: } 58: 59: double Double(double original) 60: { 61: cout << "In Double(double)\n"; 62: return 2 * original; 63: } Результат: myInt: 6500 myLong: 65000 myFloat: 6.5 myDouble: 6.5e+20 In Double(int) In Double(long) In Double(float) In Double(double) DoubledInt: 13000 DoubledLong 130000 DoubledFLoat: 13 DoubledDouble: 1.3e+21 Анализ: Функция Double() перегружается для приема параметров четырех типов: int, long, float и double. Прототипы функций занимают строки 6—9, а определения — строки 41-63. В теле основной программы объявляется восемь локальных переменных. В строках 13-16 инициализируются первые четыре переменные, а в строках 28-31 остальным четырем переменным присваиваются результаты передачи значений первых четырех переменных функции Double(). Обратите внимание, что по виду вызова эти функции ничем не отличаются друг от друга. Но удивительное дело: вы передаете аргумент — и вызывается нужная функция! Дело в том, что компилятор определяет тип переданного аргумента, на основании которого выбирает соответствующий вариант функции Double(). А результаты работы этой программы подтверждают ожидаемую очередность вызова вариантов этой перегруженной функции. Дополнительные сведения о функцияхПоскольку функции являются важным элементом программирования, то было бы весьма полезно рассмотреть некоторые специальные темы, интерес к которым возрастает при возникновении нестандартных ситуаций. К числу таких специальных тем, которые способны оказать неоценимую услугу программисту, относятся подставляемые inline-функции и рекурсия функций. Что касается рекурсии, то это замечательное изобретение программистов время от времени позволяет решать такие проблемы, которые практически не решаются никакими другими способами. Подставляемые inline-функцииОбычно при определении функции компилятор резервирует в памяти только один блок ячеек для сохранения операторов функции. После вызова функции управление программой передается этим операторам, а по возвращении из функции выполнение программы возобновляется со строки, следующей после вызова функции. Если эту функцию вызывать 10 раз, то каждый раз ваша программа будет послушно отрабатывать один и тот же набор команд. Это означает, что существует только одна копия функции, а не 10. Но каждый переход к области памяти, содержащей операторы функции, замедляет выполнение программы. Оказывается, что, когда функция невелика (т.е. состоит лишь из одной-двух строк), можно получить некоторый выигрыш в эффективности, если вместо переходов от программы к функции и обратно просто дать компилятору команду встроить код функции непосредственно в программу по месту вызова. Когда программисты говорят об эффективности, они обычно подразумевают скорость выполнения программы. Если функция объявлена с ключевым словом inline (т.е. подставляемая), компилятор не создает функцию в памяти компьютера, а копирует ее строки непосредственно в код программы по месту вызова. Это равносильно вписыванию в программе соответствующих блоков вместо вызовов функций. Обратите внимание, что использование подставляемых функций чревато и некоторыми издержками. Если функция вызывается 10 раз, то во время компиляции в программу будет вставлено 10 копий этой функции. За увеличение скорости выполнения программы нужно будет расплатиться размерами программного кода, в результате чего ожидаемого повышения эффективности программы может и не произойти. Так какой же напрашивается вывод? Если в программе часто вызывается маленькая функция, состоящая из одной-двух строк, то это первый кандидат в подставляемые функции. Но если функция велика, то лучше воздержаться от ее многократного копирования в программе. Использование подставляемой функции демонстрируется в листинге 5.9. Листинг 5.3. Использование подставляемых inline-функций 1: // Листинг 5.9. Подставляемые inline-функции 2: 3: <<include <iostгеагп.h> 4: 5: inline mt Double(int); 6: 7: int main() 8: { 9: int target; 10: 11: cout << "Enter а number to work with: 12: cin >> target; 13: cout << "\n"; 14: 15: target = Double(target); 16: cout << "Target: " << target << endl. 17: 18: target = Double(target): 19: coul << "Target: " << target << endl; 20: 21: 22: target = Double(target): 23: cout << "Target: " << target << endl; 24: return 0; 25: } 26: 27: int Double(int target) 28: { 29: return 2'target; 20: } Результат: Enter a number to work with: 20 Target: 40 Target: 80 Target: 160 Анализ: В строке 5 объявляется подставляемая функция Double(), принимающая параметр типа int и возвращающая значение типа int. Это объявление подобно любому другому прототипу за исключением того, что прямо перед типом возвращаемого значения стоит ключевое слово inline. Результат компиляции этого прототипа равносилен замене в программе строки: target = 2 * target; вызовом функции Double(): target = Double(target); К моменту выполнения программы копии функции уже расставлены по своим местам и программа готова к выполнению без частых переходов к функции и обратно. Примечание: Ключевое слово inline служит для компилятора рекомендацией пользователя скопировать код функции в программу по месту вызова. Компилятор волен проигнорировать ваши рекомендации и сохранить обычное обращение к функции. РекурсияФункция может вызывать самое себя. Это называется рекурсией, которая может быть прямой или косвенной. Когда функция вызывает самое себя, речь идет о прямой рекурсии. Если же функция вызывает другую функцию, которая затем вызывает первую, то в этом случае имеет место косвенная рекурсия. Некоторые проблемы легче всего решаются именно с помощью рекурсии. Так рекурсия полезна в тех случаях, когда выполняется определенная процедура над данными, а затем эта же процедура выполняется над полученными результатами. Оба типа рекурсии (прямая и косвенная) выступают в двух амплуа: одни в конечном счете заканчиваются и генерируют возврат, а другие никогда не заканчиваются и генерируют ошибку времени выполнения. Программисты считают, что последний вариант весьма забавен (конечно же, когда он случается с кем-то другим). Важно отметить, что, когда функция вызывает самое себя, выполняется новая копия этой функции. При этом локальные переменные во второй версии независимы от локальных переменных в первой и не могут непосредственно влиять друг друга, по крайней мере не больше, чем локальные переменные в функции main() могут влиять на локальные переменные в любой другой функции, которую она вызывает, как было показано в листинге 5.4. Чтобы показать пример решение проблемы с помощью рекурсии, рассмотрим ряд Фибоначчи: 1,1,2,3,5,8,13,21,34... Каждое число ряда (после второго) представляет собой сумму двух стоящих впереди чисел. Задача может состоять в том, чтобы, например, определить 12-й член ряда Фибоначчи. Один из способов решения этой проблемы лежит в тщательном анализе этого ряда. Первые два числа равны 1. Каждое последующее число равно сумме двух предыдущих. Таким образом, семнадцатое число равно сумме шестнадцатого и пятнадцатого. В общем случае n-e число равно сумме (n-2)-го и (n-l)-го при условии, если n > 2. Для рекурсивных функций необходимо задать условие прекращения рекурсии. Обязательно должно произойти нечто, способное заставить программу остановить рекурсию, или же она никогда не закончится. В ряду Фибоначчи условием останова является выражение n < 3. При этом используется следующий алгоритм: 1. Предлагаем пользователю указать, какой член в ряду Фибоначчи следует рассчитать. 2. Вызываем функцию fib(), передавая в качестве аргумента порядковый номер члена ряда Фибоначчи, заданный пользователем. 3. В функции fib() выполняется анализ аргумента (n). Если n < 3, функция возвращает значение 1; в противном случае функция fib() вызывает самое себя (рекурсивно), передавая в качестве аргумента значение n-2, затем снова вызывает самое себя, передавая в качестве аргумента значение п-1, а после этого возвращает сумму. Если вызвать функцию fib(1), она возвратит 1. Если вызвать функцию fib(2), она также возвратит 1. Если вызвать функцию fib(3), она возвратит сумму значений, возвращаемых функциями fib(2) и fib(l). Поскольку вызов функции fib(2) возвращает значение 1 и вызов функции fib(1) возвращает значение 1,то функция fib(3) возвратит значение 2. Если вызвать функцию fib(4), она возвратит сумму значений, возвращаемых функциями fib(3) и fib(2). Мы уже установили, что функция fib(3) возвращает значение 2 (путем вызова функций fib(2) и fib(1)) и что функция fib(2) возвращает значение 1, поэтому функция fib(4) просуммирует эти числа и возвратит значение 3, которое будет являться четвертым членом ряда Фибоначчи. Сделаем еще один шаг. Если вызвать функцию fib(5), она вернет сумму значений, возвращаемых функциями fib(4) и fib(3). Как мы установили, функция fib(4) возвращает значение 3, а функция fib(3) — значение 2, поэтому возвращаемая сумма будет равна числу 5. Описанный метод — не самый эффективный способ решения этой задачи (при вызове функции fib(20) функция fib() вызывается 13 529 раз!), тем не менее он работает. Однако будьте осторожны. Если задать слишком большой номер члена ряда Фибоначчи, вам может не хватить памяти. При каждом вызове функции fib() резервируется некоторая область памяти. При возвращении из функции память освобождается. Но при рекурсивных вызовах резервируются все новые области памяти, а при таком подходе системная память может исчерпаться довольно быстро. Реализация функции fib() показана в листинге 5.10. Предупреждение: При запуске программы, представленной в листинге 6.10, задавайте небольшие номера членов ряда Фибоначчи (меньше 15). Поскольку в этой программе используется рекурсия, возможны большие затраты памяти. Листинг 5.10. Пример использования рекурсии для нахождения члена ряда Фибоначчи 1: #include <iostream.h> 2: 3: int fib (int n); 4: 5: int main() 6: { 7: 8: int n, answer; 9: cout << "Enter number to find: "; 10: cin >> n; 10: 11: cout << "\n\n"; 12: 13: answer = fib(n); 14: 15: cout << answer << " is the " << n << "th Fibonacci number\n"; 17: return 0, 16: } 17: 18: int fib (int n) 19: { 20: cout << "Processing fib(" << n << ")... "; 23: 21: if (n < 3 ) 22: { 23: cout << "Return 1!\n"; 24: return (1); 25: } 26: else 27: { 28: cout << "Call fib(" << n-2 << ") and fib(" << n-1 << ").\n"; 29: return( fib(n-2) + fib(n-l)); 30: } 31: } Результат: Enter number lo find: 6 Processing fib(6)... Call fib(4) and fib{S) Processing fib(4)... Call fit>(2) and fib(3) Processing fib(2)... Return 1! Processing fib(3)... Call fib(l) and fiO<2) Processing fib(D... Return 1! Processi ng fib(2)... Return 1! Processing fib(5)... Call fib(3) and fib{4) Processing fib(3}... Call fib(1) and fib(2) Processing flb(1)... Return 1! Processi ng fib(2)... Return 1! Processing fib(4)... Call fib(2) and fib(3) Processing fib(2)... Return 1! Processing fib(3)... Call fib(1) and fib(2) Processing fib(l)... Return 1! Processing fib(2)... Return 1! 8 is the 6th Fibonacci number Примечание:Некоторые компиляторы испытывают затруднения с использованием операторов в выражениях с объектом cout. Если вы получите предупреждение в строке 28, заключите операцию вычитания в круглые скобки, чтобы строка 28 приняла следующий вид: 28: cout << "Call fib(" << (n-2) << ") and fib(" << n-1 << ").\n"; Анализ: В строке 9 программа предлагает ввести номер искомого члена ряда и присваивает его переменной n. Затем вызывается функция fib() с аргументом n. Выполнение программы переходит к функции fib(), где в строке 20 этот аргумент выводится на экран. В строке 21 проверяется, не меньше ли аргумент числа 3, и, если это так, функция fib() возвращает значение 1. В противном случае выводится сумма значений, возвращаемых при вызове функции fib() с аргументами n-2 и п-1. Таким образом, эту программу можно представить как циклический вызов функции fib(), повторяющийся до тех пор, пока при очередном вызове этой функции не будет возвращено некоторое значение. Единственными вызовами, которые немедленно возвращают значения, являются вызовы функций fib(1) и fib(2). Рекурсивное использование функции fib() проиллюстрировано на рис. 5.4 и 5.5. В примере, изображенном на рисунках, переменная n равна значению 6, поэтому из функции main() вызывается функция fib(6). Выполнение программы переходит в тело функции fib(), и в строке 30 значение переданного аргумента сравнивается с числом 3. Поскольку число 6 больше числа 3, функция fib(6) возврашает сумму значений, возвращаемых функциями fib(4) и fib(5): 38: return( fib(n-2) + fib(n-1)); Это означает, что выполняется обращение к функциям fib(4) и fib(5) (поскольку переменная n равначислу 6, то fib(n-2) — это то же самое, что fib(4), а fib(n-1) — то же самое, что fib(5)). После этого функция fib(6), которой в текущий момент передано управление программой, ожидает, пока сделанные вызовы не возвратят какое-нибудь значение. Дождавшись возврата значений, эта функция возвратит результат суммирования этих двух значений. Рис. 5.4. Использование рекурсии Рис. 5.5. Возвращение из рекурсии Поскольку при вызове функции fib(5) передается аргумент, который не меньше числа 3, функция fib() будет вызываться снова, на этот раз с аргументами 4 и 3. А функция flb(4) вызовет, в свою очередь, функции fib(3) и fib(2). Результаты и промежуточные этапы работы программы, представленной в листинге 5.10, выводятся на экран. Скомпилируйте, скомпонуйте и выполните эту программу, введя сначала число 1, затем 2, 3, и так доберитесь до числа 6, внимательно наблюдая за отображаемой информацией. Работа с этой программой предоставляет вам прекрасный шанс проверить возможности своего отладчика. Разместите точку останова в строке 20, а затем заходите в тело каждой вызываемой функции fib(), отслеживая значение переменной n при каждом рекурсивном вызове функции fib(). В программировании на языке C++ рекурсия не частый гость, но в определенных случаях она является мощным и весьма элегантным инструментом. Примечание:Рекурсия относится к одной из самых сложных тем программирования. Данный раздел полезен для понимания основных идей ее реализации, однако не следует слишком расстраиваться, если вам не до конца ясны все детали работы рекурсии. Работа функций - приподнимаем завесу тайныПри вызове функции управление программой передается вызванной функции. Происходит передача параметров, после чего начинается выполнение операторов, составляющих тело функции. По завершении выполнения функции возвращается некоторое значение (если не определено, что функция возвращает тип void) и управление передается вызывающей функции. Как же реализуется эта задача? Откуда программе известно, к какому блоку ей сейчас нужно перейти? Где хранятся переменные при передаче их в качестве аргументов? Что происходит с переменными, которые объявляются в теле функции? Как передается назад возвращаемое значение? Откуда программе известно, с какого места ей нужно продолжить работу? Многие авторы даже не делают попыток ответить на все эти вопросы, но без понимания принципов работы функций программирование вам покажется сплошным шаманством. Объяснение же потребует краткого освещения вопросов, связанных с памятью компьютера. Уровни абстракцииОдно из основных препятствий для начинающих программистов — преодоление нескольких уровней абстрагирования от реальности. Компьютеры, конечно, всего лишь электронные машины. Они ничего не знают об окнах и меню, о программах или командах, они даже ничего не знают о единицах и нулях. Все, что происходит в действительности, связано лишь с измерением напряжения в различных точках интегральных микросхем. И даже это является абстракцией. Само электричество представляет собой лишь умозрительную концепция, обобщающую поведение элементарных частиц. Некоторых программистов пугает любой уровень детализации, опускающийся ниже понятий о значениях, хранящихся в ОЗУ. В конце концов, вам не нужно понимать физику элементарных частиц, чтобы управлять автомобилем, печь пироги или бить по мячу. Точно так же, чтобы программировать, можно обойтись без понимания электроники компьютера. Однако вы должны понимать, как в компьютере организована память. Без четкого представления о том, где располагаются ваши переменные после их создания и как передаются значения между функциями, программирование останется для вас непостижимой тайной. Разбиение памятиКогда вы начинаете работу со своей программой, операционная система (например, DOS или Microsoft Windows) выделяет различные области памяти в ответ на требования компилятора. Как программисту на C++, вам часто придется интересоваться пространством глобальных имен, свободной памятью, регистрами, памятью сегментов программы и стеками. Глобальные переменные хранятся в пространстве глобальных имен. Подробнее о пространстве глобальных имен и свободной памяти речь пойдет на следующих уроках, а пока сосредоточимся на регистрах, оперативной памяти и стеках. Регистры представляют собой специальную область памяти, встроенную прямо в центральное процессорное устройство, или центральный процессор (Central Processing Unit — CPU). На их плечи возложена забота о выполнении внутренних вспомогательных функций, описание большей части которых выходит за рамки этой книги. Но мы все-таки остановимся на рассмотрении набора регистров, ответственных за указание на следующую строку программы в любой момент времени. Назовем эти регистры (все вместе) указателями команд. Именно на указатель команды ложится ответственность следить за тем, какая строка программы должна выполняться следующей. Сама программа находится в памяти компьютера, которая специально отведена для того, чтобы хранить операторы программы в двоичном формате. Каждая строка исходного текста программы транслируется в ряд команд, а каждая из этих команд хранится в памяти по своему адресу. Указатель команды содержит адрес следующей команды, предназначенной для выполнения. Эта идея иллюстрируется на рис. 5.6. Рис. 5.6. Указатель команды Стек — это специальная область памяти, выделенная для хранения данных вашей программы, требуемых каждой вызываемой функцией. Она называется стеком потому, что представляет собой очередь типа "последним пришел — первым вышел" и напоминает стопку тарелок в руках официанта (рис. 5.7). Принцип "последним пришел — первым вышел" означает, что элемент, добавленный в стек последним, будет вынут из него первым. Большинство же очередей функционирует подобно очереди в театр: первый, кто занял очередь, первым из нее и выйдет (и войдет в театр). Стек скорее напоминает стопку монет, удерживаемых специальным приспособлением. Если расположить в нем 10 монет достоинством в 1 копейку, а затем попытаться вынуть несколько монет, то первой вы достанете ту, что была вставлена последней. При помещении данных в стек он расширяется, а при возвращении данных из стека — сужается. Невозможно из стопки достать одну тарелку, не вынув предварительно все тарелки, помещенные в стопку перед ней. То же справедливо для данных в стеке памяти. Рис. 5.7. Стек Аналогия со стопкой тарелок приводится чаще всего. Такое сравнение довольно наглядно, но не вполне верно в смысле техники выполнения. Более точное представление позволит создать ряд прямоугольных полей, выровненных сверху вниз. Вершиной стека будет служить любое поле, на которое указывает в данный момент указатель вершины стека (эту роль выполняет другой регистр). Все поля имеют последовательные адреса, и один из этих адресов хранится в регистре указателя вершины стека. Все, что находится ниже вершины стека, относится к стеку. Все, что находится выше вершины стека, игнорируется, как показано на рис. 5.8. Рис. 5.8. Указатель вершины стека При помещении некоторого значения в стек оно размещается в поле, расположенном над вершиной стека, после чего указатель вершины изменяется таким образом, чтобы указывать на новое значение. При удалении значения из стека в действительности происходит лишь изменение адреса указателя вершины стека таким образом, чтобы он указывал на подлежащий удалению элемент стека. Принцип действия схематически показан на рис. 5.9. Рис. 5.9. Перемещение указателя вершины стека Стек и функцииНиже перечислены действия, происходящие с программой, выполняемой под управлением DOS, при переходе к телу функции. 1. Увеличивается адрес, содержащийся в указателе команды, чтобы указывать на инструкцию, следующую после вызова функции. Затем этот адрес помещается в стек и будет служить адресом возврата по завершении выполнения функции. 2. В стеке резервируется место для возвращаемого функцией значения объявленного вами типа. Если в системе с двухбайтовыми целыми для возвращаемого значения объявлен тип int, то к стеку добавляются еще два байта, но в эти байты ничего пока не помещается. 3. В указатель команды загружается адрес вызванной функции, который хранится в отдельной области памяти, отведенной специально для этих целей. Поэтому следующей выполняемой командой будет первый оператор вызванной функции. 4. Текущая вершина стека помечается и содержится в специальном указателе, именуемом указателем стека. Все, что добавляется в стек с этого момента и до тех пор, пока функция не завершится, рассматривается как локальные данные этой функции. 5. В стек помещаются все аргументы, передаваемые функции. 6. Выполняется команда, адрес которой находится в данный момент в указателе команды, т.е. первая строка кода функции. 7. По мере определения в стеке размещаются локальные переменные и функции. Когда функция завершается, возвращаемое значение помещается в область стека, зарезервированную на этапе 2. Затем из стека удаляется все содержимое вплоть до указателя стека, благодаря чему стек очищается от локальных переменных и аргументов, переданных функции. Затем из стека извлекаются значение возврата функции, которое присваивается переменной, вызвавшей функцию, и адрес команды, сохраненный в стеке на этапе 1, который присваивается указателю команд. Таким образом, программа продолжает свою работу со следующей строки после обращения к функции, владея уже значением, возвращенным из функции. Некоторые детали этого процесса изменяются при переходе от компилятора к компилятору или от компьютера к компьютеру, но основная идея остается прежней независимо от операционной среды. В общем случае при вызове функции адрес возврата и ее параметры всегда помещаются в стек: На протяжении жизненного цикла функции в стек добавляются локальные переменные. По возвращении из функции все они удаляются из стека. На следующих занятиях рассматриваются некоторые особенности других областей памяти, которые используются для хранения глобальных данных программы. РезюмеНа этом занятии вы познакомились с функциями. Функция в действительности представляет собой подпрограмму, которой можно передавать параметры и из которой можно возвращать значение. Каждый запуск программы C++ начинается с выполнения функции main(), которая, в свою очередь, может вызывать другие функции. Функция объявляется с помощью прототипа функции, который описывает возвращаемое значение, имя функции и типы ее параметров. При желании функцию можно объявить подставляемой (с помощью ключевого слова inline). В прототипе функции можно также объявить значения, используемые по умолчанию для одного или нескольких параметров функции. Определение функции должно соответствовать прототипу функции по типу возвращаемого значения, имени и списку параметров. Имена функций могут быть перегружены путем изменения количества или типа параметров. Компилятор находит нужную функцию на основе списка параметров. Локальные переменные функции и аргументы, передаваемые функции, локальны по отношению к блоку, в котором они объявлены. Параметры, передаваемые как значения, представляют собой копии реальных переменных и не могут влиять на значения этих переменных в вызывающей функции Вопросы и ответыПочему бы не сделать все переменные глобальными? Когда-то именно так и поступали. Но по мере усложнения программ стало очень трудно находить в них ошибки, поскольку значения глобальных переменных могли быть изменены любой из функций, поэтому сложно было определить, какой именно блок программы виновен в ошибке. Многолетний опыт убедил программистов, что данные должны храниться локально (насколько это возможно) и доступ к изменению данных должен быть определен как можно более узким кругом. Когда следует использовать в прототипе функции ключевое слово inline? Если функция невелика (занимает не более одной-двух строк) и встраивание ее в код программы по всем местам вызова не увеличит существенно размер этой программы, то, возможно, имеет смысл объявить ее как inline. Почему изменения, вносимые в теле функции в переменные, переданные как аргументы, не отражаются на значениях этих переменных в основном коде программы? Аргументы обычно передаются в функцию как значения, т.е. аргумент в функции является на самом деле копией оригинального значения. Данная концепция подробно разъяснялась на этом занятии. Как поступить, если необходимо, чтобы изменения, внесенные в функции, сохранились после возвращения из функции? Эта проблема рассматривается на занятии 8. Использование указателей не только решает эту проблему, но также предоставляет способ обойти ограничение на возврат только одного значения из функции. Что произойдет, если объявить следующие две функции: int Area (int width, int length = 1); int Area (int size); Будут ли они перегруженными? Условие уникальности списков параметров соблюдено, но в первом варианте для параметра определено значение, используемое по умолчанию. Эти объявления будут скомпилированы, но, если вызвать функцию Area () с одним параметром, будет сгенерирована ошибка компиляции, обусловленная неопределенностью между функциями Area(int, int) и Area(int). КоллоквиумВ этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний и приводится несколько упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из вопросов, предложенных ниже. Контрольные вопросы1. В чем разница между объявлением прототипа функции и определением функции? 2. Должны ли имена параметров, указанные в прототипе, определении и вызове функции соответствовать друг другу? 3. Если функция не возвращает значение, как следует объявить такую<функцию? 4. Если не объявить тип возврата, то какой тип будет принят по умолчанию для возвращаемого значения? 5. Что такое локальная переменная? 6. Что такое область видимости? 7. Что такое рекурсия? 8. Когда следует использовать глобальные переменные? 9. Что такое перегрузка функции? 10. Что такое полиморфизм? Упражнения1. Запишите прототип для функции с именем Perimeter, которая возвращает значение ranaunsigned long int ипринимаетдвапараметратипаипБгдпей short int. 2. Запишите определение функции Perimeter согласно объявлению в упражнении 1. Два принимаемых ею параметра представляют длину и ширину прямоугольника, а функция возвращает его периметр (удвоенная длина плюс удвоенная ширина). 3. Жучки: что неправильно в этой функции? #include <iostream.h> void myFunc(unsigned short int x); int main() { unsigned short int x, y; y = myFunc(int); cout << "x: " << x << " y: " << y << "\n"; } void myFunc(unsigned short int x) { return (4-х); } 4. Жучки: что неправильно в этой функции? #include <iostrearc.h> int myFunc(unsigned short int x); int main() { unsigned short int x, у; у = myFunc(x); cout << "x: " << x << " у: " << у << "\n"; } int myFunc(unsigned short int x); { return (4*x); } 5. Напишите функцию, которая принимает два параметра типа unsigned short int и возвращает результат деления первого параметра на второй. Функция не должна выполнять операцию деления, если второе число равно нулю, но в этом случае она должна возвратить значение -1. 6. Напишите программу, которая запрашивает у пользователя два числа и вызывает функцию, записанную при выполнении упражнения 5. Выведите результат или сообщение об ошибке, если функция возвратит значение, равное -1. 7. Напишите программу, которая запрашивает число и показатель степени. Напишите рекурсивную функцию, которая возводит число в степень путем многократного умножения числа на самое себя, т.е. если число равно 2, а показатель степени равен 4, то эта функция должна возвратить число 16. День 6-й. Базовые классыБазовые классы расширяют встроенные средства языка C++, что способствует решению сложных проблем, которые ставит перед программистами реальная жизнь. Сегодня вы узнаете: • Что представляют собой классы и объекты • Как определить новый класс и создать объекты этого класса • Что представляют собой функции-члены и переменные-члены • Что такое конструктор и как его использовать Создание новых типовВы уже познакомились с типами переменных, включая беззнаковые целые и символы. Тип переменной несет в себе немало информации. Например, если объявить переменные Height и Width как беззнаковые короткие целые (unsigned short int), то каждая из них сможет хранить целое число в диапазоне 0—65 535, занимая при этом только два байта. Если же вы попытаетесь присвоить такой переменной значение, отличное от беззнакового целого числа, то получите сообщение об ошибке. Это значит, что с помощью такой переменной вы не сможете хранить свое имя, так что даже и не пытайтесь сделать это. Лишь объявив переменные Height и Width беззнаковыми короткими целыми, вы получаете возможность сложить их или присвоить одной из них значение другой. Итак, тип переменной определяет: • ее размер в памяти; • тип данных, которые она может хранить; • операции, которые могут выполняться с ее участием. Тип данных является категорией. К нему можно отнести автомобиль, дом, человека, фрукты, геометрическую фигуру и т.п. В языке C++ программист может создать любой нужный ему тип, и каждый из этих типов может совмещать свойства и функциональные возможности встроенных базовых типов. Зачем создавать новый типПрограммы обычно пишут для решения таких реальных проблем, как отслеживание информации о служащих или имитация работы отопительной системы. И хотя решать сложные проблемы можно с помощью программ, написанных только с использованием одних целочисленных значений и символов, решения выглядели бы значительно проще, если бы можно было создавать уникальные типы для различных объектов. Другими словами, имитацию работы отопительной системы было бы гораздо легче реализовать, если бы можно было создавать переменные, представляющие помещения, тепловые датчики, термостаты и бойлеры. И чем ближе эти переменные соответствуют реальности, тем легче написать такую программу. Классы и члены классовНовый тип создается путем объявления класса. Класс — это просто коллекция переменных (причем часто различных типов), скомбинированная с набором связанных функций. Автомобиль можно представлять себе по-разному, например как коллекцию, состоящую из колес, дверей, сидений, окон и т.д. Или же, думая об автомобиле, можно представить себе его способность двигаться, увеличивать скорость, тормозить, останавливаться, парковаться и т.д. Класс позволяет инкапсулировать различные запчасти автомобиля и его разнообразные функции в одну коллекцию, которая называется объектом. Инкапсуляция всего, что мы знаем об автомобиле, в один класс имеет для программиста ряд преимуществ. Ведь все сведения собраны вместе в одном объекте, на который легко ссылаться, копировать и манипулировать его данными. Клиенты вашего класса, т.е. части программы, работающие с этим классом, могут использовать ваш объект, не беспокоясь о том, что находится в нем или как именно он работает. Класс может состоять из любой комбинации типов переменных, а также типов других классов. Переменные в классе называют переменными-членами или данными- членами. Класс Car может иметь переменные-члены, представляющие сидения, радиоприемник, шины т.д. Переменные-члены, известные также как данные-члены, принадлежат только своему классу. Переменные-члены — это такие же составные части класса, как колеса и мотор — составные части автомобиля. Функции в классе обычно выполняют действия над переменными-членами. Они называются функциями-членами или методами класса. В число методов класса Car могут входить Start() и Break(). Класс Cat может иметь такие данные-члены, которые представляют возраст и вес животного, а функциональная часть этого класса может быть представлена методами Sleep(), Meow() и ChaseMice(). Функции-члены принадлежат своему классу, как и переменные-члены. Они оперируют переменными-членами и определяют функциональные возможности класса. Объявление классаДля объявления класса используйте ключевое слово class, за которым следует открывающая фигурная скобка, а за ней — список данных-членов и методов класса. Объявление завершается закрывающей фигурной скобкой и точкой с запятой. Вот, например, как выглядит объявление класса Cat: class Cat { unsigned int itsAge; unsigned int itsWeight; void Meow(); }; При объявлении класса Cat память не резервируется. Это объявление просто сообщает компилятору о существовании класса Cat, о том, какие данные он содержит (itsAge и itsWeight), а также о том, что он умеет делать (метод Meow()). Кроме того, данное объявление сообщает компилятору о размере класса Cat, т.е. сколько места должен зарезервировать компилятор для каждого объекта класса Cat. Поскольку в данном примере для целого значения требуется четыре байта, то размер объекта Cat составит восемь байтов (четыре байта для переменной itsAge и четыре — для itsWeight). Метод Meow() не требует выделения памяти в объекте. Несколько слов об используемых именахНа программиста возложена ответственность за присвоение имен переменным- членам, функциям-членам и классам. Как упоминалось на занятии 3, всегда следует давать понятные и осмысленные имена. Например, Cat (Кот), Rectangle (Прямоугольник) и Employee (Служащий) — вполне подходящие имена для классов, а Meow() (Мяу), ChaseMice() (ДогониМышку) и StopEngine() (Остановка Двигателя) — прекрасные имена для методов, поскольку из их названий понятно, что они делают. Многие программисты сопровождают имена своих переменных-членов префиксами its (например, itsAge, itsWeight, itsSpeed). Это помогает отличить переменные-члены от переменных, не являющихся членами класса. В языке C++ имеет значение регистр букв, и все имена классов должны следовать одному образцу. Исходя из этого, вам никогда не придется вспоминать, как именно пишется название вашего класса: Rectangle, rectangle или RECTANGLE. Некоторые программисты любят добавлять к имени каждого класса однобуквенный префикс с (от слова class) например, cCat или cPerson, в то время как другие используют для имени только прописные или же только строчные буквы. Я предпочитаю начинать имена классов с прописной буквы, например Cat или Person. Также многие программисты начинают имена функций с прописных букв, а для имен всех остальных переменных используют только строчные буквы. Слова, являющиеся составными частями имен, разделяют обычно символами подчеркивания (например, Chase_Mice) или просто начинают каждое слово с прописной буквы (например, ChaseMice или DrawCircle). Важно придерживаться одного стиля на протяжении всей программы. По мере приобретения опыта программирования ваш собственный стиль написания программ включит в себя соглашения не только по присвоению имен, но также и по отступам, выравниванию фигурных скобок и оформлению комментариев. Примечание:Обычно солидные компании по разработке программных продуктов имеют специальные отделы, которые занимаются вопросами стандартизации, охватывающими и стилевые особенности программ. Это гарантирует, что все разработчики смогут легко читать программы, созданные их коллегами. Определение объектаОбъект нового типа определяется таким же способом, как и любая целочисленная переменная: unsigned int GrossWeight; // определяем беззнаковое целое Cat Frisky; // определяем объект Cat В этих программных строках определяется переменная с именем GrossWeight, которая имеет тип unsigned int, а также определяется объект Frisky, который является объектом класса (или имеет тип) Cat. Классы в сравнении с объектамиВам никогда не придет в голову поиграть с кошкой как с абстрактным понятием, скорее вы приласкаете свою настоящую мурку. Не нужно много говорить о том, какая разницу между кошкой вообще и конкретным котом, от которого шерсть по всей комнате и царапины на ножках стульев. Точно такая же разница между классом Cat, представляющим собой некую абстракцию, и отдельным объектом класса Cat. Следовательно, Frisky — это объект типа Cat в том самом смысле, в котором GrossWeight — переменная типа unsigned int. Итак, мы пришли к тому, что объект — это отдельный экземпляр некоторого класса. Получение доступа к членам классаПосле определения реального объекта класса Cat, например Frisky, у нас может возникнуть необходимость в получении доступа к членам этого объекта. Для этого используется оператор прямого доступа (.). Следовательно, чтобы присвоить число 50 переменной-члену Weight объекта Frisky, можно записать Frisky.Weight = 50; Аналогично, для вызова метода Meow() достаточно использовать следующую запись: Frisky.Meow(); Когда нужно использовать некоторый метод класса, выполняется вызов этого метода. В данном примере вызывается метод Meow() объекта Frisky. Значения присваиваются объектам, а не классамВ языке C++ нельзя присваивать значения типам данных, они присваиваются только переменным. Например, такая запись неверна: int = 5; // неверно Компилятор расценит это как ошибку, поскольку нельзя присваивать число типу int. Вместо этого нужно определить целочисленную переменную и присвоить число 5 этой переменной. Например: int x; // определяем x как переменную типа int x = 5: // присвоение переменной x значения 5 Таким образом, число 5 присваивается переменной x, которая имеет тип int. Из тех же соображений недопустима следующая запись: Cat.itsAge=5; // неверно ??? Если Cat — это класс, а не объект, то компилятор отметит это выражение как ошибочное, поскольку нельзя присвоить число 5 переменной itsAge класса (т.е. типа) Cat. Вместо этого нужно определить объект класса Cat и присвоить число 5 соответствующей переменной-члену этого объекта. Например: Cat Frisky; // это определение аналогично int x; Frisky.itsAge = 5; // это присвоение аналогично x = 5; Что объявишь, то и будешь иметьПредставьте себе, что вы гуляете со своим трехлетним ребенком, показываете ему кошку и говорите: "Это Фриски, чудесная кошка, ну-ка Фриски, залай". Даже маленький ребенок рассмеется и скажет: "Нет, кошки не умеют лаять". Если вы запишете: Cat Frisky; // создаем кошку (объект) no имени Frisky Frisky.Bark(); // велим Frisky залаять то компилятор тоже сообщит вам, что даже виртуальные кошки лаять не умеют, поскольку для них не объявлен такой метод. В классе Cat есть метод Meow() (мяукать). Если же вы не определите в классе Cat метод Meow(), то компилятор не позволит вашей кошке даже мяукать. Рекомендуется:Используйте ключевое слово class для объявления класса. Используйте оператор прямого доступа (.) для получения доступа к переменным-членам и методам класса. Не рекомендуется:Не путайте объявление с определением. Объявление заявляет о существовании класса, а определение резервирует память для объекта. Не путайте класс с объектом. Не присваивайте значения классу. Присваивайте значения переменным-членам объекта. Ограничение доступа к членам классаВ объявлении класса используются и другие ключевые слова. Двумя самыми важными из них являются public (открытый) и private (закрытый), определяющие доступ к членам класса. Все члены класса — данные и методы — являются закрытыми по умолчанию. К закрытым членам можно получить доступ только с помощью методов самого класса. Открытые члены доступны для всех других функций программы. Определение доступа к членам класса имеет очень важное значение, и именно при решении этой задачи начинающие программисты часто сталкиваются с трудностями. Чтобы прояснить ситуацию, рассмотрим пример, который уже приводился выше в этой главе: class Cat { unsigned int itsAge; unsigned int itsWeight; void Meow(); }; В этом объявлении переменные itsAge и itsWeight, а также метод Meow() являются закрытыми, поскольку все члены класса закрытые по умолчанию. Если требуется изменить доступ к членам класса, то это следует сделать явно. Если в программе будет описан класс Cat, как показано выше, то обращение к переменной-члену itsAge из функции main() вызовет ошибку компиляции: Cat Boots; Boots.itsAge = 5; // Ошибка! Нельзя обращаться к закрытым данным И в самом деле, сначала компилятору указывается, что члены itsAge, itsWeight и Meow() можно использовать только внутри класса Cat, а затем делается попытка использовать во внешней функции переменную-член itsAge, безраздельно принадлежащую объекту Boots класса Cat. Хотя объект Boots реально существует в программе, это не означает, что можно получать доступ к членам данного объекта, закрытым для постороннего глаза. Именно эти моменты с определением доступа к членам класса служат источником бесконечных недоразумений у начинающих программистов. Я прямо-таки слышу ваш удивленный вопрос: "Если в программе объявлен реальный объект Boots класса Cat, почему же нельзя присвоить значение переменной-члену этого объекта, даже обратившись к ней с помощью оператора прямого доступа?" Дело в Том, что в объявлении класса Cat ничего не говорится о ваших правах обращаться к членам этого класса, а это значит, что вы таких прав не имеете. Только собственные методы объекта Boots всегда имеют доступ ко всем данным класса, как открытым, так и закрытым. Даже несмотря на то, что вы сами создали класс Cat, это не дает вам права возвращать или изменять в программе его данные, которые являются закрытыми. Однако из любого положения есть выход. Чтобы получить доступ к переменным- членам класса Cat, откройте их следующим способом: class Cat { public: unsigned int itsAge; unsigned int itsWeight; void Meow(); }; Теперь благодаря ключевому слову public все члены класса (itsAge, itsWeight и Meow()) стали открытыми. В листинге 8.1 показано объявление класса Cat с открытыми переменными-членами. Листинг 8.1. Доступ к открытым членам простого класса 1: // Пример объявление класса с 2: // открытыми членами 3: 4: #include <iostream.h> // для использования cout 5: 6: class Cat // объявляем класс 7: { 8: public: // следующие члены являются открытыми 9: int itsAge; 10: int itsWeight; 11: }; 12: 13: 14: int main() 15: { 16: Cat Frisky; 17: Frisky.itsAge =5; // присваиваем значение переменной-члену 18: cout << "Frisky is а cat who is "; 19: cout << Frisky.itsAge << " years old.\n"; 20: return 0; 21: } Результат: Frisky is а cat who is 5 years old. Анализ: В строке 6 содержится ключевое слово class. Оно сообщает компилятору о том, что следующий после него блок является объявлением класса. Имя нового класса стоит сразу после ключевого слова class. В данном случае у нас объявляется класс Cat. Тело объявления класса начинается с открывающей фигурной скобки в строке 7 и заканчивается закрывающей фигурной скобкой и точкой с запятой в строке 11. Строка 8 содержит ключевое слово public, которое означает, что до тех пор, пока не встретится ключевое слово private или конец объявления класса, все последующие члены объявляются открытыми. В строках 9 и 10 объявляются переменные-члены itsAge и itsWeight. В строке 14 начинается функция main() программы. Frisky определяется в строке 16 как экземпляр класса Cat, т.е. как объект класса Cat. В строке 17 возраст объекта Frisky (значение переменной itsAge) устанавливается равным 5. А в строках 18 и 19 переменная-член itsAge используется для вывода данных на экран. Примечание:Попробуйте заблокировать символом комментария строку 8 и перекомпилировать программу. Компилятор покажет сообщение об ошибке в строке 17, поскольку к переменной itsAge больше нет открытого доступа, ведь по умолчанию все члены класса объявляются как закрытые. Оставьте данные класса закрытымиСогласно общей стратегии использования классов переменные-члены класса следует оставлять закрытыми. Благодаря этому достигается инкапсуляция данных внутри класса. Доступ следует открывать только к функциям-членам класса, обеспечивающим доступ к его закрытым данным (эти функции еще называют методами доступа). Эти методы можно вызывать из любого места в программе для возвращения или установки значений закрытых переменных-членов. Зачем же используются в программе такие посредники между закрытыми членами класса и остальной программой? Не проще ли открыть данные класса для внешнего доступа, вместо того чтобы работать с методами доступа? Дело в том, что применение методов доступа позволяет скрыть от пользователя детали хранения данных в объектах, в то же время, снабжая их методами использования этих данных. В результате можно модернизировать способы хранения и обработки данных внутри класса, не переписывая при этом методы доступа и вызовы их во внешнем программном коде. Если для некоторой внешней функции в программе, возвращающей возраст объекта Cat, открыть непосредственный доступ к переменной itsAge, то эту функцию пришлось бы переписывать в том случае, если автор класса Cat решит изменить способ хранения этого компонента данных. Однако если между внешней функцией и данными класса будет стоять функция-член GetAge(), то класс Cat можно будет модернизировать сколько угодно раз, что никак не повлияет на способ вызова функции GetAge() в основном коде программы. При вызове в программе метода доступа не нужно знать, хранится ли нужное значение в переменной типа unsigned integer или long либо оно вычисляется при запросе. Такой подход облегчает эксплуатацию вашей программы и ее поддержку в будущем. Можно сказать, что он продлевает жизнь программе, поскольку, изменяя классы, можно существенно модернизировать выполнение программы, не затрагивая при этом основного кода. В листинге 6.2 показан класс Cat, в котором в этот раз объявлены закрытые переменные-члены и открытые методы доступа к закрытым данным. Обратите внимание, что перед вами не выполняемый вариант программы, а только объявление класса. Листинг 6.2. Объявление методов доступа к данным класса 1: // Объявление класса Cat 2: // Переменные-члены объявляются закрытыми, а открытые методы доступа 3: // обеспечивают инициализацию переменных-членов и возвращение их значений 4: 5: class Cat 6: { 7: public: 8: // открытые методы доступа 9: unsigned int GetAge(); 10: void SetAge(unsigned int Age); 11: 12: unsigned int GetWeight(); 13: void SetWeight(unsigned int Weight); 14: 15: // открытые функции-члены 16: void Meow(); 17: 18: // закрытые переменные-члены 19: private: 20: unsigned int itsAge; 21: unsigned int itsWeight; 22: 23: }; Анализ: Этот класс имеет пять открытых методов. В строках 9 и 10 содержатся :/:<<**v>>>>* объявления методов обеспечения доступа к переменной-члену itsAge. А в строках 12 и 13 объявляются методы доступа к переменной-члену itsWeight. Эти функции-члены инициализируют переменные и возвращают их значения. В строке 16 объявляется открытая функция-член Meow(). Функция Meow() не является методом доступа. Она не получает и не устанавливает значение переменной-члена, а выполняет другой вид сервиса для класса, выводя слово Meow на экран. Сами переменные-члены объявляются в строках 20 и 21. Чтобы установить возраст кота Frisky, нужно передать соответствующее значение методу SetAge(): Cat Frisky; Frisky.SetAge(5); // устанавливаем возраст Frisky с помощью открытого метода-доступа Ограничение доступа к данным - это не способ защиты данных, а лишь средство облегчения программированияОбъявление методов или данных закрытыми позволяет компилятору заблаговременно находить ошибки программирования. Начинающие программисты часто ошибочно полагают, что объявляя данные закрытыми, тем самым скрывают некоторую секретную информацию от пользователей, не имеющих соответствующих прав доступа. В действительности это не так. По этому поводу Страустрап (Stroustrup), изобретатель языка C++, сказал: "Механизмы управления доступом в C++ обеспечивают защиту от несчастного случая, но не от мошенника" (ARM, 1990). Рекомендуется:Объявляйте закрытыми переменные- члены класса (с помощью ключевого слова private). Объявляйте открытыми методы доступа к закрытым данным-членам класса. Используйте для обработки данных-членов закрытые функции-члены класса. Не рекомендуется:Не пытайтесь использовать закрытые переменные-члены вне класса. Ключевое слово class Ключевое слово class имеет следующий синтаксис: class имя_класса { // здесь находятся ключевые слова управления доступом // здесь объявляються переменные и методы класса }; Ключевое слово class используется для объявления новых типов. Класс — это коллекция данных-членов класса, которые представляют собой переменные различных типов, включая другие классы. Класс также содержит функции класса, или методы, которые используются для выполнения действий над данными класса, а также для выполнения других видов сервиса внутри класса. Определение объектов нового типа во многом подобно определению любых переменных. Сначала указывается тип (класс), а затем имя переменной (объект). Для обращения к членам класса Данным и функциям) используется оператор точки (.). Для объявления открытых или закрытых разделов класса используются ключевые слова управления доступом public или private. По умолчанию действует закрытый режим доступа. Каждое ключевое слово изменяет режим управления доступом с момента использования этого ключевого слова и до конца объявления класса или до тех пор, пока не встретится следующее ключевое слово управления доступом. Все объявления классов оканчиваются закрывающей фигурной скобкой и точкой с запятой. Пример 1: class Cat { public: unsigned int Age; unsigned int Weight; void Meow(); } Cat Frisky; Frisky.Age = 8; Frisky.Weight = 18; Frisky.Meow(); Пример 2: class Car { public: void Start(); void Accelerate(); void Brake(); void SetYear(int year); int GetYear(); private: int Year; Char Model[255]; }; Car OldFaithful; int bought; OldFaithful.SetYear(84); bought = OldFaithful.GetYear(); OldFaithful.Start(); Определение методов классаКак упоминалось выше, методы доступа обеспечивают интерфейс для работы с закрытыми переменными-членами класса. Для методов доступа, как и для всех других объявленных методов класса, следует определять выполнение. Таким образом, методы объявляются и определяются в классе. Определение функции-члена начинается с имени класса, за которым следуют два двоеточия, имя функции и ее параметры. В листинге 6.3 показано объявление простого класса Cat, в котором присутствуют определения ранее объявленных методов доступа к данным и одной обычной функции-члена. Листинг 6.3. определение методов простого класса 1: // Пример определения методов в 2: // объявлении класса 3: 4: #include <iostream.h> // для объекта cout 5: 6: class Cat // начало объявления класса 7: { 8: public: // начало раздела public 9: int GetAgeO; // метод доступа 10: void SetAge (int age); // метод доступа 11: void Meow(); // обычный метод 12: private: // начало раздела 13: int itsAge; // переменная-член 14: }; 15: 16: // GetAge, открытая функция доступа, 17:// возвращает значение переменной-члена itsAge 18: int Cat::GetAge() 19: { 20: return itsAge; 21: } 22: 23: // Определение открытой функции доступа SetAge 24: // Функция SetAge 25: // инициирует переменную-член itsAge 26: void Cat::SetAge(int age) 27: { 28: // устанавливаем переменную-член itsAge равной 29: // значению, переданному с помощью параметра age 30: itsAge = age; 31: } 32: 33: // Определение метода Meow 34: // возвращает void 35: // параметров нет 36: // используется для вывода на экран текста "Meow" 37: void Cat::Meow() 38: { 39: cout << "Meow.\n"; 40: } 41: 42: // Создаем виртуальную кошку, устанавливаем ее возраст, разрешаем 43: // ей мяукнуть, сообщаем ее возраст, затем снова "мяукаем". 44: int main() 45: { 46: Cat Frisky; 47: Frisky.SetAge(5); 48: Frisky.Meow(); 49: cout << "Frisky is а cat who is "; 50: cout << Frisky.QetAge() << " years old.\n"; 51: Frisky.Meow(); 52: return 0; 53: } Результат: Meow. Frisky is а cat who is 5 years old. Meow. Анализ: В строках 6—14 содержится определение класса Cat. Строку 8 занимает ключевое слово public, которое сообщает компилятору, что за ним следует набор открытых членов класса. В строке 9 содержится объявление открытого метода GetAge(), который предоставляет доступ к закрытой переменной-члену itsAge, объявляемой в строке 13. В строке 10 объявляется открытая функция доступа SetAge(), которая принимает в качестве аргумента целочисленное значение и присваивает переменной itsAge значение этого аргумента. В строке 11 объявляется метод Meow(). Этот метод не является функцией доступа к данным-членам класса, а используется для вывода на экран слова Meow. В строке 12 начинается закрытый раздел, который включает только одно объявление закрытой переменной-члена itsAge (строка 13). Объявление класса завершается закрывающей фигурной скобкой и точкой с запятой в строке 14. Строки 18—21 содержат определение функции-члена GetAge(). Этот метод не принимает никаких параметров и возвращает целое значение. Обратите внимание на то, что при определении методов класса используется имя класса, за которым следуют два двоеточия и имя функции (строка 18). Благодаря этому-синтаксису компилятор узнает, что определяемая здесь функция GetAge() — это функция, объявленная в классе Cat. За исключением строки заголовка, GetAge() создается точно так же, как и другие функции. Определение функции GetAge() занимает только одну строку, в которой указывается, что эта функция возвращает значение переменной-члена itsAge. Обратите внимание, что функция main() не может получить доступ к этой переменной, поскольку она объявлена в закрытом разделе класса Cat. При этом из функции main() можно обратиться к открытому методу GetAge(). А поскольку метод GetAge() является функцией-членом класса Cat, то он имеет все права доступа к переменной-члену itsAge. В результате функция GetAge() возвращает значение переменной itsAge в функцию main(). В строке 26 начинается определение функции-члена SetAge(). Она принимает целочисленный параметр и присваивает переменной itsAge значение этого параметра (строка 30). Являясь членом класса Cat, функция SetAge() имеет прямой доступ к переменной-члену itsAge. В строке 37 начинается определение метода Meow() класса Cat. Этот метод занимает всего одну строку, в которой выводится на экран слово Meow, а затем выполняется переход на новую строку. Помните, что для перехода на новую строку используется символ \n. В строке 44 начинается тело функции main(); она не принимает никаких аргументов. В строке 46 в функции main() объявляется объект класса Cat с именем Frisky. В строке 47 переменной-члену itsAge присваивается значение 5 с помощью метода доступа SetAge(). Обратите внимание, что в вызове этого метода указывается имя объекта (Frisky), за которым следует оператор прямого доступа (.), и имя самого метода (SetAge()). Таким способом можно вызывать любые другие методы класса. В строке 48 вызывается функция-член Meow(), а в строке 49 на экран выводится значение переменной-члена с использованием функции доступа GetAge(). В строке 51 функция Meow() вызывается снова. Конструкторы и деструкторыСуществует два способа определения целочисленной переменной. Во-первых, можно определить переменную, а затем (несколько ниже в программе) присвоить ей некоторое значение, например: int Weight; // определяем переменную // здесь следуют другие выражения Weight = 7; // присваиваем значение переменной Можно также определить переменную и немедленно ее инициализировать, например: int Weight = 7; // определяем и инициализируем значением 7. Операция инициализации сочетает в себе определение пербмбнной с присвоением начального значения. Причем ничто не может помешать вам впоследствии изменить это значение. Кроме того, инициализация, проведенная одновременно с определением, гарантирует, что переменная не будет содержать мусор, оставшийся в выделенных переменной ячейках памяти. Как же инициализировать переменные-члены класса? Для этого в классе используется специальная функция-член, называемая конструктором. При необходимости конструктор может принимать параметры, но не может возвращать значения даже типа void. Конструктор — это метод класса, имя которого совпадает с именем самого класса. Объявив конструктор, вам также стоит объявить и деструктор. Если конструкторы служат для создания и инициализации объектов класса, то деструкторы удаляют из памяти отработавшие объекты и освобождают выделенную для них память. Деструктору всегда присваивается имя класса с символом тильды (~) вначале. Деструкторы не принимают никаких аргументов и не возвращают никаких значений. Объявление деструктора класса Cat будет выглядеть следующим образом: ~Cat(); Конструкторы и деструкторы, заданные по умолчаниюЕсли вы не объявите конструктор или деструктор, то компилятор сделает это за вас. Стандартные конструктор и деструктор не принимают аргументов и не выполняют никаких действий. Вопросы и ответы: Конструктор называется стандартным из-за отсутствия аргументов или из-за того, что создается компилятором в том случае, если в классе не объявляется никакой другой конструктор? Стандартный конструктор, или конструктор по умолчанию, характеризуется тем, что не принимает никаких аргументов, причем неважно, создан ли этот конструктор автоматически компилятором или самим программистом. Стандартный конструктор всегда используется по умолчанию. Однако что касается деструкторов, то тут есть свои отличия. Стандартный деструктор предоставляется компилятором. Поскольку все деструкторы не имеют параметров, то главной отличительной чертой стандартного деструктора является то, что он не выполняет никаких действий, т.е. имеет пустое тело функции. Использование конструктора, заданного по умолчаниюКакая же польза от конструктора, который ничего не выполняет? Зачастую это нужно только для протокола. Все объекты должны быть локализованы в программе, поэтому их создание и удаление сопровождается вызовом соответствующей функции, которая при этом может ничего и не делать. Так, для объявления объекта без передачи параметров, например: Cat Rags; // Rags не получает никаких параметров необходимо иметь следующий конструктор: Cat(); Конструктор вызывается при определении объекта класса. Если для создания объекта класса Cat следует передать два параметра, то конструктор класса Cat определяется следующим образом: Cat Frisky (5,7); Если конструктор принимает один параметр, определение объекта будет иметь следующий вид: Cat Frisky (3); В случае, когда конструктор вообще не принимает параметров (т.е. является стандартным), отпадает необходимость использования круглых скобок: Cat Frisky; Этот случай является исключением из правила, гласящего, что все функции требуют наличия круглых скобок, даже если они вовсе не принимают параметров. Вот почему можно спокойно записать такое определение: Cat Frisky; Эта запись интерпретируется как обращение к стандартному конструктору. В ней отсутствует передача параметров и, как следствие, круглые скобки. Обратите внимание, что вы не обязаны постоянно использовать стандартный конструктор, предоставляемый компилятором. Всегда можно написать собственный стандартный конструктор, т.е. конструктор без параметров. Вы вольны наделить свой стандартный конструктор телом функции, в котором будет выполняться инициализация класса. Чтобы придать законченность своему труду, при объявлении конструктора не забудьте объявить и деструктор, даже если вашему деструктору нечего делать. И хотя справедливо то, что и стандартный конструктор будет корректно работать, отнюдь не повредит объявить собственный деструктор. Это сделает вашу программу более ясной. В листинге 6.4 в знакомый уже вам класс Cat добавлены конструктор и деструктор. Конструктор используется для инициализации объекта Cat и установки его возраста равным предоставляемому вами значению. Обратите внимание на то, в каком месте программы вызывается деструктор. Листинг 6.4. Использование конструкторов и деструкторов. 1: // Пример объявления конструктора и 2: // деструктора в классе Cat 3: 4: #include <iostream.h> // для объекта cout 5: 6: class Cat // начало объявления класса 7: { 8: public: // начало открытого раздела 9: Cat(int initialAge); // конструктор 10: ~Cat(); //деструктор 11: int GetAge(); // метод доступа 12: void SetAge(int age); // метод доступа 13: void Meow(); 14: private: // начало закрытого раздела 15: int itsAge; // переменная-член 16: }; 17: 18: // конструктор класса Cat 19: Cat::Cat(int initialAge) 20: { 21: itsAge = initialAge; 22: } 23: 24: Cat::~Cat() // деструктор, не выполняющий действий 25: { 26: } 27: 28: // GetAge, открытая функция обеспечения доступа, 29: // возвращает значение переменной-члена itsAge 30: int Cat::GetAge() 31: { 32: return itsAge; 33: } 34: 35: // Определение SetAge, открытой 36: // функции обеспечения доступа 37: 38: voidCat::SetAge(int age) 39: { 40: // устанавливаем переменную-член itsAge равной 41: // значению, переданному параметром age 42: itsAge = age; 43: } 44: 45: // Определение метода Meow 46: // возвращает void 47: // параметров нет 48: // используется для вывода на экран текста "Meow" 49: void Cat::Meow() 50: { 51: cout << "Meow.\n"; 52: } 53: 54: // Создаем виртуальную кошку, устанавливаем ее возраст, разрешаем 55: // ей мяукнуть, сообщаем ее возраст, затем снова "мяукаем" и изменяем возраст кошки. 56: int main() 57: { 58: Cat Frisky(5); 59: Frisky.Meow(); 60: cout << "Frisky is а cat who is "; 61: cout << Frisky.QetAge() << " years old.\n"; 62: Frisky.Meow(); 63: Frisky.SetAge(7); 64; cout << "Now Frisky is "; 65: cout << Frisky. GeMje() << " years old.\n"; 66: return 0; 67: } Результат: Meow. Frisky is a cat who is 5 years old. Meow. Now Frisky is 7 years old. Анализ: Листинг 6.4 подобен листингу б.З за исключением того, что в строке 9 добавляется конструктор, который принимает в качестве параметра целочисленное значение. В строке 10 объявляется деструктор, который не принимает никаких параметров. Помните, что деструкторы никогда не принимают параметров; кроме того, ни конструкторы, ни деструкторы не возвращают никаких значений — даже значения типа void. В строках 19—22 определяется выполнение конструктора, аналогичное выполнению функции доступа SetAge(), которая также не возвращает никакого значения. В строках 24—26 определяется деструктор ~Cat(). Эта функция не выполняет никаких действий, но коль вы объявляете ее в классе, нужно обязательно включить и ее определение. В строке 58 содержится определение объекта класса Cat с именем Frisky. В конструктор объекта Frisky передается значение 5. В данном случае нет никакой необходимости вызывать функцию-член SetAge(), поскольку объект Frisky создавался с использованием значения 5, присвоенного переменной-члену itsAge, как показано в строке 61. В строке 63 переменной itsAge объекта Frisky присваивается значение 7, на этот раз с помощью функции SetAge(). Новое значение выводится на экран в строке 65. Рекомендуется:Используйте конструкторы для инициализации объектов. He рекомендуется:Не пытайтесь с помощью конструктора или деструктора возвращать какое бы то ни было значение. Не передавайте деструкторам параметры. Объявление функций-членов со спецификатором constВ языке C++ предусмотрена возможность объявить метод класса таким образом, что такому методу будет запрещено изменять значения переменных-членов класса. Для этого в объявлении функции используется ключевое слово const, стоящее после круглых скобок, но перед точкой с запятой. Например, объявим таким образом функцию-член SomeFunction(), которая не принимает аргументов и возвращает значение типа void: void SomeFunction() const; Функции доступа к данным часто объявляются со спецификатором const. В классе Cat есть две функции доступа: void SetAge(int anAge); int GetAge(); Функция SetAge() не может быть объявлена со спецификатором const, поскольку она изменяет значение переменной-члена itsAge. А в объявлении функЦии.^Аде() может и даже должен использоваться спецификатор const, поскольку она не должна ничего изменять в классе. Функция GetAge() просто возвращает текущее значение переменной-члена itsAge. Следовательно, объявление этих функций необходимо записать в таком виде: void SetAge(int anAge); int GetAge() const; Если некоторая функция объявлена с использованием спецификатора const, а в ее выполнении происходит изменение какого-либо члена объекта, то компилятор покажет сообщение об ошибке. Например, если записать функцию GetAge() таким образом, чтобы она подсчитывала, сколько раз запрашивался возраст объекта Cat, будет обязательно сгенерирована ошибка компиляции, поскольку при таком подсчете (т.е. при вызове функции GetAge()) происходит изменение объекта Cat. Примечание:Используйте спецификатор const везде в объявлениях функций-членов, если они не должны изменять объект. Это позволит компилятору лучше отслеживать ошибки и поможет вам при отладке программы. Использовать const в объявлениях методов, не изменяющих объект, считается хорошим стилем программирования. Это позволяет компилятору лучше отслеживать ошибки еще до запуска программы на выполнение. Чем отличается интерфейс от выполнения классаКак уже упоминалось, клиенты — это составные части программы, которые создают и используют объекты вашего класса. Открытый интерфейс класса (объявление класса) можно представить себе в виде соглашения с этими клиентами, в котором указываются способы взаимодействия клиентов с классом. Например, в объявлении класса Cat указывается, что программа-клиент может инициализировать любой возраст объекта этого класса с помощью функции доступа SetAge() и возвратить это значение с помощью функции доступа GetAge(). При этом гарантируется, что каждый объект класса Cat сможет вывести сообщение Meow на экран с помощью функции-члена Meow(). Обратите внимание, что в открытом интерфейсе класса ничего не говорится о закрытой переменной-члене itsAge, которая используется при выполнении класса и не должна интересовать клиентов. Значение возраста можно возвратить из объекта с помощью GetAge() и установить с помощью SetAge(), но сама переменная itsAge, в которой хранится это значение, скрыта от клиентов. Если объявить функцию GetAge() со спецификатором const, а именно этого требуют правила хорошего тона программирования, в соглашение также будет внесен пункт о том, что функцию GetAge() нельзя использовать для изменения значений объекта класса Cat. В языке C++ осуществляется строгий контроль за типами данных, поэтому подобное соглашение между классом и клиентами будет законом для компилятора, который сгенерирует ошибку компиляции в случае нарушения этого соглашения. В листинге 6.5 показан пример программы, которую не удастся скомпилировать из-за нарушения этих самых соглашений. Предупреждение:Листинг 6.5 не компилируется! Листинг 6.5. Пример нарушения соглашений интерфейса 1: // Пример ошибки компиляции, связанной 2: // с нарушениями соглашений интерфейса класса 3: 4: #include <iostream.h> // для объекта cout 5: 6: class Cat 7: { 8: public: 9: Cat(int initialAge); 10: ~Cat(); 11: int GetAge() const; // метод доступа const 12: void SetAge (int age); 13: void Meow(); 14: private: 15: int itsAge; 16: }; 17: 18: // конструктор класса Cat 19: Cat::Cat(int initialAge) 20: { 21: itsAge = initialAge; 22: cout << "Cat constructor\n"; 23: } 24: Cat::~Cat() // деструктор, который не выполняет никаких действий 25: { 26: cout << "Cat destructor\n"; 27: } 28: // функция GetAge объявлена как const, 29: // но мы нарушаем это условие! 30: int Cat::GetAge() const 31: { 32: return (itsAge++); // это нарушение соглашения интерфейса! 33: } 34: 35: // определение функции SetAge как открытого 36: // метода доступа к данным класса 37: 38: void Cat::SetAge(int age) 39: { 40: // присваиваем переменной-члену itsAge 41: // значение переданного парйметра age 42: itsAge = age; 43: } 44: 45: // Определение метода Meow 46: // возвращает void 47: // параметров нет 48: // используется для вывода на экран текста "Meow" 49: void Cat::Meow() 50: { 51: cout << "Meow.\n"; 52: } 53: 54: // демонстрирует различные нарушения 55: // интерфейса, что приводит к ошибкам компиляции 56: int main() 57: { 58: Cat Frisky; // не соответствует обьявлению 59: Frisky.Meow(); 60: Frisky.Bark(); // Нет, кошки не лают. 61: Frisky.itsAge = 7; // переменная itsAge закрыта 62: return 0; 63: } Анализ: Как упоминалось выше, эта программа не компилируется. Поэтому и отсутствуют результаты ее работы. Эту программу было забавно писать, поскольку в нее специально закладывались ошибки. В строке 11 GetAge( )объявляется как функция доступа к данным-членам класса без права их изменения, на что указывает спецификатор const. Однако в теле функции GetAge(), а именно в строке 32, выполняется приращение переменной-члена itsAge. А поскольку этот метод объявлен как const, он не имеет права изменять значение переменной itsAge. Следовательно, во время компиляции программы на этой строке будет зафиксирована ошибка. В строке 13 объявляется метод Meow(), в этот раз без использования ключевого слова const. И хотя такое упущение не является ошибкой, это далеко не лучший стиль программирования. Если учесть, что этот метод не должен изменять значения переменных-членов класса Cat, то его следовало бы определить со спецификатором const. В строке 58 показано определение объекта класса Cat с именем Frisky. В этом варианте программы класс Cat имеет конструктор, который принимает в качестве параметра целочисленное значение. Это означает обязательность передачи параметра заданного типа. Поскольку в строке 58 никакой параметр не передается, компилятор зафиксирует ошибку. Примечание:Если в классе объявляется какой-либо конструктор, компилятор в этом случае не станет предлагать со своей стороны никакого другого конструктора даже если определение объекта по форме не будет coответствовать объявленному конструктору. В подобных случаях компилятор покажет сообщение об ошибке. В строке 60 вызывается метод Bark(). Этот метод вообще не был объявлен, следовательно, ни о каком его использовании и речи быть не может. В строке 61 делается попытка присвоить переменной itsAge значение 7. Поскольку переменная itsAge относится к числу закрытых данных-членов, то при компиляции программы здесь будет зафиксировано покушение на частную собственность класса. Почему для отслеживания ошибок лучше использовать компилятор Кажется невероятным написать программу не допуская никаких ошибок. Тем не менее некоторые программисты способны на подобные чудеса, хотя, конечно, таких кудесников очень немного. Большинство из них, как и все нормальные люди делают ошибки. Поэтому нашлись программисты, которые разработали систему, способную помочь в отслеживании ошибок путем перехвата и исправления их на ранней стадии создания программ. Хотя сообщения об ошибках, выявленных компилятором, действуют на нервы, это намного лучше возникновения ошибок при выполнении программы. Если бы компилятор был менее дотошный, то велика вероятность, что ваша программа дала бы сбой в самый неподходящий момент, например во время презентации. Ошибки компиляции, т.е. ошибки, выявленные на стадии компиляции, гораздо безобиднее ошибок выполнения, которые проявляются после запуска программы. Компилятор будет дотошно и однотипно сообщать об обнаруженной им ошибке. Напротив, ошибка выполнения может не обнаруживать себя до поры до времени, но потом проявиться в самый неподходящий момент. Поскольку ошибки компиляции заявляют о себе при каждом сеансе компиляции, то их легко идентифицировать и исправить, чтобы больше о них не вспоминать. Чтобы добиться создания программ, которые не станут со временем выкидывать фокусы, программист должен помочь компилятору в отслеживании ошибок, используя спецификаторы в объявлениях для предупреждения возможных сбоев. Где следует распологать в программе объявления классов и определения методовКаждая функция, объявленная в классе, должна иметь определение. Определение также называется выполнением функции. Подобно другим функциям, определение метода класса состоит из заголовка и тела функции. Определение должно находиться в файле, который компилятор может легко найти. Большинство компиляторов C++ предпочитают, чтобы такой файл имел расширение .c или .cpp. В этой книге используется расширение .cpp, но вам стоит выяснить предпочтения собственного компилятора. Примечание: Многие компиляторы полагают, что файлы с расширением .c содержат программы, написанные на языке С, а файлы с расширением .cpp — программы на C++. Вы можете использовать любое расширение, но именно .cpp сведет к минимуму возможные недоразумения в программах на C++. Объявления классов можно поместить в один файл с программой, но это не считается хорошим стилем программирования. В соглашении, которого придерживаются многие программисты, рекомендуется помещать объявление в файл заголовка, имя которого обычно совпадает с именем файла программы, но в качестве расширения используются такие варианты, как .h, .hp или .hpp. В этой книге для имен файлов заголовков используется расширение .hpp, но вам стоит выяснить предпочтения собственного компилятора. Например, можно поместить объявление класса Cat в файл с именем CAT, hpp, а определение методов класса — в файл с именем CAT .cpp. Затем нужно включить файл заголовка в код файла с расширением .cpp. Для этого в начале программного кода в файле CAT.cpp используется следующая команда: #include "Cat.hpp" Эта команда дает указание компилятору ввести содержимое файла CAT.hpp в данном месте программы. Результат выполнения команды include такой же, как если бы вы переписали с клавиатуры в это место программы полное содержимое соответствующего файла заголовка. Имейте в виду, что некоторые компиляторы чувствительны к регистру букв и требуют точного соответствия написания имен файла в директиве #include и на диске. Зачем же нужно отделять файл заголовка с расширением .hpp от файла программы с расширением cpp, если мы все равно собираемся вводить содержимое файла заголовка назад в файл программы? Как показывает практика, большую часть времени клиентов вашего класса не волнуют подробности его выполнения. При чтении небольшого файла заголовка они получают всю необходимую информацию и могут игнорировать файл с подробностями выполнения этого класса. Кроме того, не исключено, что содержимое файла заголовка с расширением .hpp вам захочется включить не в один, а в несколько файлов программ. Примечание:Объявление класса сообщает компилятору, что представляет собой этот класс, какие данные он содержит и какими функциями располагает. Объявление класса называется его интерфейсом, поскольку оно сообщает пользователю, как взаимодействовать с классом. Интерфейс обычно хранится в файле с расширением .hpp, который называется файлом заголовка. Из определения функции компилятор узнает, как она работает. Определение функции называется выполнением метода класса и хранится в файле с расширением .cpp. Подробности выполнения класса касаются только автора класса. Клиентам же класса, т.е. частям программы, использующим этот класс, не нужно знать, как выполняются функции. Выполнение с подстановкойМожно выполнять подстановку методов с помощью ключевого слова inline точно так же, как это делалось с обычными функциями. Для этого ключевое слово inline нужно разместить перед типом возвращаемого значения. Например, определение подставляемой функции-члена GetWeight() имеет следующий вид: inline intCat::GetWeight() { return itsweight; // возвращает переменную-член Weight } Можно также поместить определение функции в объявление класса, что автоматически делает такую функцию подставляемой: class Cat { public: int GetWeight() { return itsWeight; } // подставляемая функция void SetWeight(int aWeight); }; Обратите внимание на синтаксис определения функции GetWeight(). Тело подставляемой функции начинается сразу же после объявления метода класса, причем после круглых скобок нет никакой точки с запятой. Подобно определению обычной функции, определение метода начинается с открывающей фигурной скобки и оканчивается закрывающей фигурной скобкой. Как обычно, пробелы значения не имеют, и то же самое определение можно записать несколько иначе: class Cat { public: int GetWeight() const { return itsWeight; } // подставляемая функция void SetWeight(int aWeight); }; В листингах 6.6 и 6.7 вновь создается класс Cat, но в данном случае объявление класса содержится в файле CAT.hpp, а выполнение — в файле CAT.cpp. Кроме того, в листинге 6.7 метод доступа к данным класса и метод Meow() являются подставляемыми. Листинг 6.6. Объявление класса CAT в файле CAT.hpp 1: #include <iostream.h> 2: class Cat 3; { 4: public: 5: Cat (int initialAge); 6: ~Cat(); 7: int GetAge() const { return itsAge;) // подставляемая функция! 8: void SetAge (int age) { itsAge = age;} // подставляемая функция! 9: void Meow() const { cout << "Мяу.\n";} // подставляемая функция! 10: private: 11: int itsAge; 12: }; Листинг 6.7. Выполнение масса Cat в файле CAT.cpp 1: // Пример использования подставляемых функций 2: // и включения файла заголовка 3: 4: #include "cat.hpp" // не забудьте включить файл заголовка! 5: 6: 7: Cat::Cat(int initialAge) //конструктор 8: { 9: itsAge = initialAge; 10: } 11: 12: Cat::~Cat() // деструктор, не выполняет никаких действий 13: { 14: } 15: 16: // Создаем виртуальную кошку, устанавливаем ее возраст, разрешаем 17: // ей мяукнуть, сообщаем ее возраст, затем снова "мяукаем" и изменяем возраст кошки. 18: int main() 19: { 20: Cat Frisky(5); 21: Frisky.Meow(); 22: cout << "Frisky is а cat who is "; 23: cout << Frisky.QetAge() << " years old.\n"; 24: Frisky.Meow(); 25: Frisky.SetAge(7); 26: cout << "Now Frisky is " ; 27: cout << Frisky.GetAge() << " years old.\n"; 28: return 0; 29: } Результат: Meow. Frisky is а cat who is 5 years old. Meow. Now Frisky is 7 years old. Анализ: Программа, представленная в листингах 6.6 и 6.7, аналогична программе из листинга 6.4 за исключением того, что три метода класса объявляются подставляемыми, а само объявление класса вынесено в файл заголовка CAT.hpp. В строке 7 объявляется функция GetAge() и тут же следует определение ее выполнения. Строки 8 и 9 занимают объявления еще двух встроенных функций, но их определения содержатся в другом файле. В строке 4 листинга 6.7 с помощью команды #include "cat.hpp" в программу включается содержимое файла CAT.hpp. Компилятор получает команду считать содержимое файла cat .hpp и ввести его в данный файл, начиная со строки 5. Возможность встраивания файлов в другие файлы позволяет хранить объявления классов отдельно от их выполнения и использовать тогда, когда в этом возникает необходимость. Это стандартный прием при создании программ на языке C++. Обычно объявления классов хранятся в файле с расширением .hpp, который затем включается в соответствующий файл .cpp с помощью директивы #include. В строках 18—29 в точности повторяется тело функции main() из листинга 6.4. Цель этого повторения — показать, что применение подставляемых вариантов функций не внесло изменений в использование этих функций. Классы содержащие другие классы в качестве данных-членовНет ничего необычного в построении сложного класса путем объявления более простых классов и последующего включения их в объявление сложного класса. Например, можно объявить класс колеса, класс мотора, класс коробки передач и т.д., а затем объединить их в класс автомобиля. Тем самым объявляются и взаимоотношения между классами. Автомобиль имеет мотор, колеса и коробку передач. Рассмотрим второй пример. Прямоугольник состоит из линий. Линия определяется двумя точками. Каждая точка определяется координатами x и у. В листинге 6.8 показано объявление класса Rectangle, которое содержится в файле RECTANGLE.hpp. Поскольку прямоугольник определяется четырьмя линиями, соединяющими четыре точки, и каждая точка имеет координаты на графике, то сначала будет объявлен класс Point для хранения координат x,y каждой точки. Листинг 6.9 содержит объявления обоих классов. Листинг 6.8. Объявление классов точки и прямоугольника 1: // Начало файла Rect.hpp 2: #include <iostream.h> 3: class Point // хранит координаты x,y 4: { 5: // нет конструктора, используется конструктор по умолчанию 6: public: 7: void SetX(int x) { itsX = x; > 8: void SetY(int у) { itsY = у; } 9: int GetX() const < return itsX;} 10: int GetY() const { return itsY;} 11: private: 12: int itsX; 13: int itsY; 14: }; // конец объявления класса Point 15: 16: 17: class Rectangle 18: { 19: public: 20: Rectangle(int top, int left, int bottom, int right):.; 21: ~Rectangle() {} 22: 23: int GetTop() const { return itsTop; } 24: int GetLeft() const { return itsLeft; } 25: int GetBottom() const { return itsBottom; } 26: int GetRight() const { return itsRight; } 27: 28: Point GetUpperLeft() const { return itsUpperLeft; } 29: Point GetLowerLeft() const { return itsLowerLeft; } 30: Point GetUpperRight() const { return itsUpperRight; } 31: Point GetLowerRight() const { return itsLowerRight; } 32: 33: void SetUpperLeft(Point Location) {itsUpperLeft = Location; } 34: void SetLowerLeft(Point Location) {itsLowerLeft = Location; } 35: void SetUpperRight(Point Location) {itsUpperRight = Location; } 36: void SetLowerRight(Point Location) {itsLowerRight = Location; } 37: 38: void SetTop(int top) { itsTop = top; } 39: void SetLeft (int left) { itsLeft = left; } 40: void SetBottorn (int bottom) { itsBottom = bottom; } 41: void SetRight (int right) { itsRight = right; } 42: 43: int GetArea() const; 44: 45: private: 46: Point itsUpperLeft; 47: Point itsUpperRight; 48: Point itsLowerLeft; 49: Point itsLowerRight; 50: int itsTop; 51: int itsLeft; 52: int itsBottom; 53: int itsRight; 54: }; 55: // конец файла Rect.hpp Листинг 6.9. Содержимое файла RECT.cpp 1: // Начало файла rect.cpp 2: #include "rect.hpp" 3: Rectangle::Rectangle(int top, int left, int bottom, int right) 4: { 5: itsTop = top; 6: itsLeft = left; 7: itsBottom = bottom; 8: itsRight = right; 9: 10: itsUpperLeft.SetX(left); 11: itsUpperLeft.SetY(top); 12: 13: itsUpperRight.SetXtright); 14: itsUpperRight.SetY(top); 15: 16: itsLowerLeft.SetX(left); 17: itsLowerLeft.SetY(bottom); 18: 19: itsLowerRight.SetX(right); 20: itsLowerRight.SetY(bottom); 21: } 22: 23: 24: // Вычисляем площадь прямоугольника, отыскивая его стороны 25: // определяем его длину и ширину, а затем перемножаем их 26: int Rectangle::GetArea() const 27: { 28: int Width = itsRignt - itsLeft; 29: int Height = itsTop - itsBottom; 30: return (Width >> Height); 31: } 32: 33: int main() 34: { 35: //инициализируем локальную переменную Rectangle 36: Rectangle MyRectangle (100, 20, 50, 80 ); 37: 38: int Area = MyRectangle.GetArea(); 39: 40: cout << "Area: " << Area << "\n"; 41: cout << "Upper Left X Coordinate:"; 42: cout << MyRectangle.GetUpperLeft().GetX(); 43: return 0; 44: } Результат: Area: 3000 Upper Left X Coordinate: 20 Анализ: В строках 3-14 листинга 6.8 объявляется класс Point, который используется для хранения конкретных координат x,y на графике. В данной программе класс Point практически не используется. Однако в других методах рисования он незаменим. Внутри объявления класса Point (в строках 12 и 13) объявляются две переменные- члена (itsX и itsY). Эти переменные хранят значения координат точки. При увеличении координаты x мы перемещаемся на графике вправо. При увеличении координаты у мы перемещаемся на графике вверх. В других графиках могут использоваться другие системы координат (с другой ориентацией). Например, в некоторых программах построения окон значение координаты у увеличивается при перемещении в области окна вниз. В классе Point используются подставляемые inline-функции доступа, предназначенные для чтения и установки координат точек X и Y. Эти функции объявляются в строках 7-10. В объектах класса Point используются стандартные конструктор и деструктор, предоставляемые компилятором по умолчанию. Следовательно, координаты точек должны устанавливаться в программе. В строке 17 начинается объявление класса Rectangle, который включает четыре точки, представляющие углы прямоугольника. Конструктор класса Rectangle принимает четыре целочисленных параметра, именуемых top (верхний), left (левый), bottom (нижний) и right (правый). Эти четыре параметра, передаваемые конструктору, копируются в соответствующие четыре пере- менные-члена (см. листинг 6.9), после чего устанавливаются четыре точки (четыре объекта класса Point). Помимо обычных функций доступа к данным-членам класса, в классе Rectangle предусмотрена функция GetArea(), объявленная в строке 43. Вместо хранения значения площади в виде переменной эта функция вычисляет площадь в строках 28 и 29 листинга 6.9. Для этого сначала вычисляются значения длины и ширины прямоугольника, а затем полученные результаты перемножаются. Для получения координаты верхнего левого угла прямоугольника нужно получить доступ к точке UpperLeft и запросить ее значение X. Поскольку функция GetUpperLeft() является методом класса Rectangle, она может непосредственно получить доступ к закрытым данным этого класса, включая и доступ к переменной itsUpperLeft. Поскольку переменная itsUpperLeft является объектом класса Point, а переменная itsX этого объекта закрытая, функция GetUpperLeft() не может прямо обратиться к этой переменной. Вместо этого для получения значения переменной itsX она должна использовать открытую функцию доступа GetX(). В строке 33 листинга 6.9 начинается тело основной части программы. До выполнения строки 36 никакой памяти не выделялось и ничего, по сути, не происходило. Все, сделанное до сих пор, служило одной цели — сообщить компилятору, как создается точка и как создается прямоугольник (на случай, если в этом появится необходимость). В строке 36 определяется прямоугольник (объект класса Rectangle) путем передачи реальных значений для параметров Top, Left, Bottom и Right. В строке 37 создается локальная переменная Area типа int. Она предназначена для хранения площади созданного прямоугольника. Переменной Area присваивается значение, возвращаемое функцией-членом GetArea() класса Rectangle. Клиент класса Rectangle может создать объект Rectangle и возвратить его площадь, не заботясь о нюансах выполнения функции GetArea(). В листинге 6.8 показано содержимое заголовочного файла Rect.hpp. Только лишь просмотрев заголовочный файл, который содержит объявление класса Rectangle, программист будет знать, что функция GetArea() возвращает значение типа int. Пользователя класса Rectangle не волнуют "производственные" секреты функции GetArea(). И в самом деле, автор класса Rectangle мог бы спокойно изменить выполнение функции GetArea(), и это бы не повлияло на программы, использующие класс Rectangle. Вопросы и ответы: Каково различие между объявлением и определением? Объявление вводит имя некоторого объекта, но не выделяет для него память, а вот с помощью определения как раз и выделяется память для конкретного объекта. СтруктурыОчень близким родственником ключевого слова class является ключевое слово struct, которое используется для объявления структуры. В языке C++ структура — это тот же класс, но с открытыми по умолчанию членами. Структуру можно объявить подобно тому, как объявляется класс, наделив ее такими же переменными-членами и функциями. И в самом деле, если исповедовать хороший стиль программирования и всегда в явном виде объявлять открытые и закрытые разделы класса, то никаких отличий не должно быть. Попытаемся повторно ввести содержимое листинга 6.8 с учетом следующих изменений: • в строке 3 заменим объявление class Point объявлением struct Point; • в строке 17 заменим объявление class Rectangle объявлением struct Rectangle. Теперь вновь запустим нашу программу и сравним результаты. При этом никакой разницы вы заметить не должны. Почему два ключевых слова несут одинаковую смысловую нагрузкуВы, вероятно, удивлены тем, что два различных ключевых слова создают практически идентичные объявления. Так сложилось исторически. Язык C++ строился как расширение С. В языке С были структуры, но эти структуры не имели методов класса. Создатель C++, Бьерн Страуструп, опирался на структуры, но заменил имя типа данных struct типом class, чтобы тем самым заявить о новых расширенных функциональных возможностях этого нового образования. Рекомендуется:Используйте спецификатор const везде, где это возможно. Убедитесь, что вам полностью понятны классы, прежде чем переходить к следующему занятию. Помещайте объявление класса в файл с расширением .hpp, а его выполнение — в файл с расширением .cpp. РезюмеСегодня вы научились создавать новые типы данных, именуемые классами. Вы узнали, как определять переменные этих новых типов, которые называются объектами. Класс содержит данные-члены, которые представляют собой переменные различных типов, включая другие классы. Кроме того, в состав класса входят функции- члены, известные также как методы. Эти функции-члены используются для выполнения действий над данными-членами и обеспечения иного сервиса. Члены класса — как данные, так и функции — могут быть открытыми и закрытыми. Открытые члены доступны для любой части программы, а закрытые — только для функций-членов данного класса. Хорошим стилем программирования считается вынесение интерфейса, или объявления класса, в файл заголовка, который обычно имеет расширение .hpp. Выполнение класса записывается в файл с расширением .cpp. Для инициализации объектов используются конструкторы класса. Когда эти объекты больше не нужны, они удаляются с помощью деструкторов, которые используются для освобождения памяти, выделенной для этих объектов методами класса. Вопросы и ответыКак определяется размер объекта класса? Размер объекта класса в памяти определяется суммой размеров переменных- членов. Методы класса не занимают место в области памяти, выделенной для объекта. Некоторые компиляторы так располагают переменные в памяти, что двухбайтовые переменные в действительности занимают несколько больше двух байтов памяти. При желании вы можете уточнить этот момент в документации на свой компилятор, но на данном этапе эти подробности, по всей вероятности, не будут иметь для вас большого значения. Если объявить класс Cat с закрытым членом itsAge, а затем определить два объекта класса Cat с именами Frisky и Boots, то может ли объект Boots получить доступ к переменной-члену itsAge объекта Frisky? Да. Закрытые данные доступны для функций-членов класса, и различные экземпляры одного класса могут обращаться к данным друг друга. Иными словами, если Frisky и Boots являются экземплярами класса Cat, то функции-члены объекта Frisky могут получить доступ как к своим данным (данным объекта Frisky), так и к данным объекта Boots. Почему не следует делать все данные-члены открытыми? Объявление данных-членов закрытыми позволяет клиенту класса использовать данные, не волнуясь о том, как они хранятся или вычисляются. Например, если класс Cat имеет метод GetAge(), клиенты класса Cat могут возвратить значение возраста кошки (объекта класса Cat), не заботясь о том, хранится ли оно в какой-нибудь переменной-члене определенного типа или вычисляется по запросу. Если применение функции const для изменения класса вызывает ошибку компилятора, то почему бы просто не использовать ключевое слово const и тем самым гарантированно избежать сообщений об ошибках? Если ваша функция-член логически не должна изменять класс, то использование ключевого слова const — прекрасный способ заручиться поддержкой компилятора при отыскании случайных ошибок, Например, у функции GetAge() нет видимых причин для изменения класса Cat, но в выполнении класса может присутствовать следующая строка: if (itsAge = 100) cout << "Ого Тебе уже сто лет\n"; Объявление функции GetAge() с использованием ключевого слова const заставило бы компилятор обнаружить ошибку. Вы ведь имели в виду сравнение значения переменной itsAge с числом 100, а вместо этого случайно выполнили операцию присвоения числа 100 переменной itsAge. Поскольку это присвоение изменяет класс, а вы (с помощью ключевого слова const) заявили, что этот метод не будет изменять класс, компилятор смог найти ошибку. Ошибки такого рода, как правило, трудно найти простым просмотром текста программы. Мы часто видим то, что хотим увидеть. Гораздо опаснее, если на первый взгляд вам покажется, что программа работает правильно (даже после установки такого странного значения), но рано или поздно эта неприятность превратится в проблему. Существует ли резон использовать структуры в программах на C++? Многие программисты используют ключевое слово struct для классов, которые не имеют функций. Можно расценивать это как ностальгию по устаревшим структурам языка С, которые не могли иметь функций. Лично я считаю это ненужным и даже плохим стилем программирования. Ведь если сегодня данной структуре не нужны методы, то не исключено, что они могут понадобиться ей завтра. И тогда вам придется либо заменять этот тип классом, либо нарушать свое же правило и работать со структурой, которая "не брезгует" присутствием в ней методов. КоллоквиумВ этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний и приводится несколько упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из предложенных ниже вопросов. Контрольные вопросы1. Что представляет собой оператор прямого доступа и для чего он используется? 2. Что резервирует память — объявление или определение? 3. Объявление класса является его интерфейсом или выполнением? 4. Какова разница между открытыми (public) и закрытыми (private) данными- членами? 5. Могут ли функции-члены быть закрытыми? 6. Могут ли переменные-члены быть открытыми? 7. Если объявить два объекта класса Cat, могут ли они иметь различные значения их переменных-членов itsAge? 8. Нужно ли объявления класса завершать точкой с запятой? А определения методов класса? 9. Как бы выглядел заголовок функции-члена Meow класса Cat, которая не принимает никаких параметров и возвращает значение типа void? 10. Какая функция вызывается для выполнения инициализации класса? Упражнения1. Напишите программу, которая объявляет класс с именем Employee (Служащие) с такими переменными-членами: age (возраст), yearsOfService (стаж работы) и Salary (зарплата). 2. Перепишите класс Employee, чтобы сделать данные-члены закрытыми и обеспечить открытые методы доступа для чтения и установки всех данных-членов. 3. Напишите программу с использованием класса Employee, которая создает два объекта класса Employee; устанавливает данные-члены age, YearsOfService и Salary, а затем выводит их значения. 4. На основе программы из упражнения 3 создайте метод класса Employee, который сообщает, сколько тысяч долларов зарабатывает служащий, округляя ответ до 1 000 долларов. 5. Измените класс Employee так, чтобы можно было инициализировать данные-члены age, YearsOfService и Salary в процессе "создания" служащего. 6. Жучки: что неправильно в следующем объявлении? class Square { public: int Side; } 7. Жучки: что весьма полезное отсутствует в следующем объявлении класса? class Cat { int GetAge() const; private: int itsAge; }; 8. Жучки: какие три ошибки обнаружит компилятор в этом коде? class TV { public: void SetStation(int Station); int GetStation() const; private: int itsStation; }; main() { TV myTV; myTV.itsStation = 9; TV.SetStation(10); TV myOtherTv(2); } День 7-й. ЦиклыСтруктура любой программы состоит из комбинации множества ветвлений и циклов. На четвертом занятии вы научились организовывать ветвление программы с помощью оператора if. Сегодня вы узнаете: • Что такое циклы и как они используются • Каковы методы организации циклов • Как избежать чрезмерной вложенности конструкций if/else Организация цикловДля решения ряда задач часто требуется многократное выполнение одних и тех же действий. На практике это реализуется с помощью рекурсивных (см. занятие 5) или итеративных алгоритмов. Суть итеративного процесса заключается в повторении последовательности операций нужное количество раз. История оператора gotoВ те годы, когда программирование находилось еще на начальной стадии развития, использовались только небольшие по размеру и достаточно примитивные программы. Нельзя было назвать приятным и сам процесс их разработки. В таких программах циклы состояли из метки, последовательности команд и оператора безусловного перехода. В C++ меткой называют идентификатор, за которым следует двоеточие (:). Метка всегда устанавливается перед оператором, на который необходимо будет передать управление. Для перехода на нужную метку используется оператор goto, за которым следует имя метки. Пример использования оператора goto приведен в листинге 7.1. Листинг 7.1. Организация цикла с помощью оператора goto 1: // Листинг 7.1. 2: // Организация цикла с помощью goto 3: 4: #include <iostream.h> 5: 6: int main() 7: { 8: int counter = 0; // инициализация счетчика 9: loop: counter++; // начало цикла 10: cout.<< "counter: " << counter << "\n"; 11: if (counter < 5) // проверка значения 12: goto loop; // возвращение к началу 13: 14: cout << "Complete. Counter: " << counter << ".\n"; 15: return 0; 16: } Результат: counter: 1 counter: 2 countor: 3 counter: 4 counter: 5 Complete. Counter: 5. Анализ: В строке 8 переменная counter инициализируется нулевым значением. Метка loop: в строке 9 показывает начало цикла. На каждой итерации значение counter yвeличивaeтcя на единицу и выводится на экран. В строке 11 выполняется проверка значения переменной counter. Если оно меньше пяти, значит условие выполняется и управление передается оператору goto, в результате чего осуществляется переход на строку 9. Итеративный процесс выполняется до тех пор, пока значение переменной counter не достигнет пяти. После этого программа выходит за пределы цикла и на экран выводится окончательный результат. Почему следует избегать оператора gotoСо временем нелестные высказывания в адрес оператора goto участились, впрочем, вполне заслуженно. С помощью оператора goto можно осуществлять переход в любую точку программы — вперед или назад. Такое беспорядочное использование этого оператора привело к появлению запутанных и абсолютно непригодных для восприятия программ, получивших жаргонное название "спагетти". Поэтому последние двадцать лет преподаватели программирования во всем мире твердили студентам одну и ту же фразу: "Никогда не используйте оператор goto". На смену оператору goto пришли конструкции с несколько более сложной структурой, но и с более широкими возможностями: for, while и do...while. Несмотря на то что после полного искоренения оператора goto структура программ значительно прояснилась, негативные высказывания в его адрес следует признать преувеличенными. Как любой инструмент программирования, при правильном использовании оператор goto может оказаться достаточно полезным. В силу этого комитет ANS1 принял решение оставить этот оператор в языке. Правда, вместе с этим родилась шутка: "Дети! Использование этого оператора в домашних условиях небезопасно!" Организация циклов с помощью оператора whileВ циклах, организованных с помощью оператора while, выполнение последовательности операций продолжается до тех пор, пока условие продолжения цикла истинно. В примере программы в листинге 7.1 значение переменной counter увеличивалось до тех пор, пока не стало равным пяти. Листинг 7.2 демонстрирует тот же алгоритм, реализованный с помощью оператора while. Листинг 7.2. Организация цикла с помощью оператора while 1: // Листинг 7.2. 2: // Организация цикла с помощью оператора while 3: 4: #include <iostream.h> 5: 6: int main() 7: { 8: int counter = 0; // присвоение начального значения 9: 10: while(counter < 5) // проверка условия продолжения цикла 11: { 12: counter++; // тело цикла 13: cout << " counter: " << counter << "\;n"; 14: } 15: 16: cout << " Complete. Counter: " << counter << ".\n"; 17: return 0; 18: } Результат: counter: 1 counter: 2 counter: 3 counter: 4 counter: 5 Complete. Counter: 5. Анализ: Эта несложная программа показывает пример организации цикла с помощью оператора while. В начале каждой итерации проверяется условие, и, если оно выполняется, управление передается на первый оператор цикла. В нашем примере условию продолжения цикла удовлетворяют все значения переменной counter, меньшие пяти (строка 10). Если условие выполняется, запускается следующая итерация цикла. В строке 12 значение счетчика увеличивается на единицу, а в строке 13 выводится на экран. Как только значение счетчика достигает пяти, тело цикла (строки 11 — 14) пропускается и управление передается в строку 15. Сложные конструкции с оператором whileСложность логического выражения, являющегося условием в операторе while, не ограничена. Это позволяет использовать в конструкции while любые логические выражения C++. При построении выражений допускается использование логических операций: && (логическое И), 11 (логическое ИЛИ), а также ! (логическое отрицание). В листинге 7.3 показан пример использования более сложных условий в конструкциях с оператором while. Листинг 7.3. Сложные условия в конструкциях while 1: // Листинг 7.3. 2: // Сложные условия в конструкциях while 3: 4: include <iostream.h> 5: 6: int main() 7: { 8: unsigned short small; 9: unsigned long large; 10: const unsigned short MAXSMALL=65535; 11: 12: cout << "Enter a small number: "; 13: cin >> small; 14: cout << "Enter a large number: "; 15: cin >> large; 16: 17: cout << "small: " << small << "..."; 18: 19: // на каждой итерации проверяются три условия 20: while (small < large && large > 0 && small < MAXSMALL) 21: { 22: if (small % 5000 == 0) // после каждых 5000 строк выводится точка 23: cout << "."; 24: 25: small++; 26: 27: large-=2; 28: } 39: 30: cout << "\nSmall: " << small << " Large: " << large << endl; 31: return 0; 32: } Результат: Enter а small number: 2 Enter а large number: 100000 small: 2 Small: 33335 Large: 33334 Анализ: Программа представляет собой простую логическую игру. Вначале предлагается ввести два числа — small и large. После этого меньшее значение увеличивается на единицу, а большее уменьшается на два до тех пор, пока они не "встретятся". Цель игры: угадать число, на котором значения "встретятся". В строках 12—15 осуществляется ввод значений. В строке 20 проверяется три условия продолжения цикла. 1. Значение переменной small не превышает значения large. 2. Значение переменной large неотрицательное и не равно нулю. 3. Значение переменной small не превышает значения константы MAXSMALL. Далее, в строке 23, вычисляется остаток от деления числа small на 5000, причем значение переменной small не изменяется. Если small делится на 5000 без остатка, результатом выполнения этой операции будет 0. В этом случае для визуального представления процесса вычислений на экран выводится точка. Затем в строке 26 значение переменной small увеличивается на 1, а в строке 28 значение large уменьшается на 2. Цикл завершается, если хотя бы одно из условий перестает выполняться. После этого управление передается в строку 29, следующую за телом цикла. Операторы break и continueЧасто бывает необходимо перейти на следующую итерацию цикла еще до завершения выполнения всех операторов тела цикла. Для этого используется оператор continue. Кроме того, в ряде случаев требуется выйти за пределы цикла, даже если условия продолжения цикла выполняются. В этом случае используется оператор break. Пример использования этих операторов приведен в листинге 7.4. Это несколько усложненный вариант уже знакомой игры. В этом случае, кроме меньшего и большего значений, предлагается ввести шаг и целевое значение. Как и в предыдущем примере, на каждой итерации цикла значение переменной small увеличивается на единицу. Значение large уменьшается на два, если меньшее число не кратно значению переменной шага (skip). Игра заканчивается, когда значение переменой small становится больше, чем значение large. Если значение переменной large совпадает с целевым значением (target), выводится сообщение и игра прерывается. Цель игры состоит в том, чтобы угадать число, в которое "попадет" значение target. Листинг 7.4. Использование break и continue 1: // Листинг 7.4. 2: // Пример использования операторов break и continue 3: 4: #include <iostream.h> 5: 6: int main() 7: { 8: unsigned short small; 9: unsigned long large; 10: unsigned long skip; 11: unsigned long target; 12: const unsigned short MAXSMALL=65535; 13: 14: cout << "Enter a smail number: "; 15: cin >> small; 16: cout << "Enter a large number: "; 17: cin >> large; 18: cout << "Enter a skip number: "; 19: cin >> skip; 20: cout << "Enter a target number; "; 21: cin >> target; 22: 23: cout << "\n" 24: 25: // установка условий продолжения цикла 26: while (small < large && large > 0 && small < MAXSMALL) 27: 28: { 29: 30: small++; 31: 32: if (small % skip == 0) // уменьшить значение large? 33: { 34: cout << "skipping on:" << small << endl; 35: continue; 36: } 37: 38: if (large == target) // проверка попадания в цель 39: { 40: cout << " Target reached!"; 41: break; 42: } 43: 44: large-=2; 45: } // конец цикла 46: 47: cout << "\nSmall: " << small << " Large: " << large << endl; 48: return 0; 49: } Результат: Enter a small number: 2 Enter a large number: 20 Enter a skip number: 4 Enter a target number: 6 skipping on 4 skipping on 8 Small: 10 Large: 8 Анализ: Как видим, игра закончилась поражением пользователя, поскольку меньшее значение превысило большее, а цель так и не была достигнута. В строке проверяются условия продолжения цикла. Если значение переменной small меньше значения large, а также если large больше нуля и small не превышает значение константы SMALLINT, управление передается первому оператору тела цикла. В строке 32 вычисляется остаток от деления значения переменной small на значение skip. Если значение small кратно skip, оператор continue запускает следующую итерацию цикла (срока 26). В результате такого перехода пропускается проверка целевого значения и операция уменьшения значения переменной large. Сравнение значений target и large выполняется в строке 38. Если эти значения равны, игра заканчивается победой пользователя. В этом случае программа выводит сообщение о победе, работа цикла прерывается оператором break и управление передается в строку 46. Использование конструкции while(true)В качестве условия, проверяемого при переходе на очередную итерацию цикла, может выступать любое выражение, корректное с точки зрения синтаксиса языка C++. Цикл выполняется до тех пор, пока это выражение истинно. Для организации так называемых бесконечных циклов в качестве такого выражения применяется логическая константа true. Листинг 7.5 демонстрирует пример бесконечного цикла, выполняющего счет до десяти. Листинг 7.5. Еще один пример использования оператора while 1: // Листинг 7.5. 2: // Пример "бесконечного" цикла 3: 4: #include <iostream.h> 5: 6: int main() 7: { 8: int counter = 0; 9: 10: while (true) 11: { 12: counter++; 13: if (counter > 10) 14: break; 15: } 16: cout << "Counter: " << counter << "\n"; 17: return 0; 18: } Результат: Counter: 11 Анализ: Понятно, что условие продолжения цикла, заданное в строке 10, будет выполняться всегда. В теле цикла (строка 12) значение переменной counter увеличивается на единицу. Работа цикла продолжается до тех пор, показначение counter не превысит 10. Выполнение цикла прерывается оператором break в строке 14, и на экран выводится значение переменной counter (строка 16). Несмотря на то что данная программа работает, ее структуру нельзя назвать оптимальной. Это типичный пример некорректного использования оператора while. Правильным решением была бы организация проверки значения counter в условии продолжения цикла. Гибкий синтаксис языка C++ позволяет решить одну и ту же задачу множеством различных способов. Поэтому важно научиться выбирать средство, наиболее подходящее в конкретной ситуации. Организация циклов с помощью конструкции do...whileПри организации циклов с помощью оператора while возможна ситуация, когда тело цикла вообще не будет выполняться. Поскольку условие продолжения цикла проверяется в начале каждой итерации, при нарушении истинности выражения, задающего это условие, выполнение цикла будет прервано еще до запуска первого оператора тела цикла. Пример такой ситуации приведен в листинге 7.6. Листинг 7.6. Преждевременное завершение цикла с while 1: // Листинг 7.6. 2: // Если условие продолжения цикла не выполняется, 3: // тело цикла пропускается. 4: 5: #include <iostream.h> 6: 7: int main() 8: { 9: int counter; 10: cout << "How many hellos?: "; 11: cin >> counter; 12: while (counter > 0) 13: { 14: cout << "Hello!\n"; 15: counter--; 16: } 17: cout << "Counter is OutPut: " << counter; 18: return 0; 19: } Результат: How many hellos?: 2 Hello! Hello! Counter is 0utPut: 0 How many hellos?: 0 Counter is 0utPut: 0 Анализ: В строке 10 вам предлагается ввести начальное значение счетчика, которое записывается в переменную counter. В строке 12 это значение проверяется, а затем в теле цикла уменьшается на единицу. При первом запуске программы начальное значение счетчика равнялось двум, поэтому тело цикла выполнялось дважды. Во втором случае было введено число 0. Понятно, что в этом случае условие продолжения цикла не выполнялось и тело цикла было пропущено. В результате приветствие не было выведено ни разу. Как же поступить, чтобы сообщение выводилось по крайней мере один раз? С помощью оператора while это сделать невозможно, так как условие проверяется еще до выполнения тела цикла. Один из способов решения этой проблемы — использование оператора if для контроля начального значения переменной counter. If (counter < 1) // контроль начального значения counter = 1; Правда, это довольно <<корявый>> выход из ситуации. Использование конструкции do...whileПри использовании конструкции do...while условие проверяется после выполнения тела цикла. Это гарантирует выполнение операторов цикла по крайней мере один раз. В листинге 7.7 приведен измененный вариант предыдущей программы, в котором вместо оператора while используется конструкция do...while. Листинг 7.7. Использование конструкции do...while 1: // Листинг 7.7. 2: // Пример использования конструкции do...while 3: 4: include <iostream.h> 5: 6: int main() 7: { 8: int counter; 9: cout << "How many hellos? "; 10: cin >> counter; 11: do 12: { 13: cout << "Hello\n"; 14: counter--; 15: } while (counter >0 ); 16: cout << "Counter is: " << counter << endl; 17: return 0; 18: } Результат: How many hellos? 2 Hello Hello Counter is: 0 Анализ: В строке 9 пользователю предлагается ввести начальное значение счетчика, которое записывается в переменную counter. В конструкции do.. while условие проверяется в конце каждой итерации, что гарантирует выполнение тела цикла по меньшей мере один раз. В строке 13 на экран выводится текст приветствия, а в строке 14 значение переменной counter уменьшается на единицу. Условие продолжения цикла проверяется в строке 15. Если оно истинно, выполняется следующая итерация цикла ,(строка 13). В противном случае цикл завершается и управление передается в строку 16. При использовании в конструкциях do.. .while операторы break и continue дают тот же результат, что и при использовании с оператором while. Единственное различие этих двух методов организации циклов состоит в проверке условия продолжения цикла. В первом случае оно контролируется перед выполнением тела цикла, а во втором — после него. Оператор forДля организации цикла с помощью оператора while необходимо выполнить три обязательных действия: установить начальные значения переменных цикла, а затем на каждой итерации проконтролировать выполнение условия продолжения цикла и изменить значение переменной цикла (листинг 7.8). Листинг 7.8. Еще один пример использования оператора while 1: // Листинг 7.8. 2: // Еще один пример использования оператора while 3: 4: #include <iostream.h> 5: 6: int main() 7: { 8: int counter = 0; 9: 10: while(counter < 5) 11: { 12: counter++; 13: cout << " Looping! "; 14: } 15: 16: cout << "\nCounter: " << counter << " \n"; 17: return 0; 18: } Результат: Looping! Looping! Looping! Looping! Looping! Counter: 5. Анализ: В строке 8 переменная цикла counter инициализируется нулевым значением. Затем в строке 10 проверяется условие продолжения цикла, а в строке 12 значение счетчика увеличивается на единицу. В строке 13 на экран выводится сообщение, наглядно иллюстрирующее циклический процесс. Конечно, в цикле вашей программы могут выполняться и более серьезные операции. Оператор for позволяет объединить три операции, необходимые для работы цикла, в одну. Кратко эти операции можно охарактеризовать так: инициализация, проверка условия и приращение счетчика цикла. Выражение с оператором for состоит из самого этого оператора, за которым в круглых скобках следуют три выражения, устанавливающих параметры выполнения цикла. Выражения в круглых скобках разделяются символами точки с запятой. Первое выражение цикла for устанавливает начальное значение счетчика цикла. Счетчик, как правило, представляет собой целочисленную переменную, которая объявляется и инициализируется прямо в цикле for, хотя в C++ допускается использование в этом месте любого выражения, выводящего начальное значение счетчика каким-то косвенным путем. Второй параметр цикла for определяет условие продолжения цикла, которое также может быть представлено любым выражением. Это условие выполняет те же функции, что и в конструкции while. Третий параметр устанавливает значение приращения счетчика цикла (по умолчанию шаг приращения равен единице). В этой части также может использоваться любое корректное выражение или оператор C++. Нужно заметить, что, хотя параметры цикла for могут задаваться любыми корректными выражениями C++, для установки второго параметра обязательно должно использоваться выражение, возвращающее логическое значение. Пример использования цикла for приведен в листинге 7.9. Листинг 7.8. Пример использования цикла for 1: // Листинг 7.9. 2: // Пример использования цикла for 3: 4: #include <iostream.h> 5: 6: int main() 7: { 8: int counter; 9: for (counter = 0; counter < 5; counter++) 10: cout << " Looping! "; 11: 12: cout << "\nCounter: " << counter << ".\n"; 13: return 0; 14: } Результат: Looping! Looping! Looping! Looping! Looping! Counter: 5. Анализ: В строке 9 программы с помощью оператора for задается цикл, отсчитывающий число итераций с помощью переменной counter. После каждого цикла проверяется условие продолжения цикла и значение переменной counter увеличивается на единицу. Тело цикла состоит из одного оператора, записанного в строке 10. В реальных программах тело цикла может состоять из любого количества операторов. Сложные выражения с оператором forПри профессиональном использовании цикл for становится мощным и гибким инструментом программирования. Тот факт, что оператор for допускает установку трех независимых параметров цикла (инициализацию, условие продолжения и шаг), открывает неограниченные возможности в управлении работой цикла. Параметры цикла for Синтаксис установок параметров цикла for следующий: for (инициализация, проверка, операция) выражения; Выражение инициализации используется для установки начального значения счетчика цикла или для выполнения какой-нибудь другой операции, подготавливающей работу цикла. Под проверкой понимают некое выражение на языке C++, которое выполняется перед каждой новой итерацией цикла и возвращает логическое значение. Если возвращается значение true, то программа выполняет строки тела цикла. после чего выполняется третье выражение в параметрах цикла, которое, как правило/приращивает значение счетчика на заданную величину. Пример 1: // напечатать Hello десять раз for(int 1=0; i<10; i++) cout << "Hello!" << endl; Пример 2: for(int 1=0; i<10; i++) { cout << "Hello!" << endl; cout << "the value of i is: " << i << endl; } Цикл for работает в такой последовательности: 1. Присваивается начальное значение счетчику цикла. 2. Вычисляется значения выражения, устанавливающего условие продолжения цикла. 3. Если выражение условия возвращает true, то сначала выполняется тело цикла, а затем заданная операция над счетчиком цикла. На каждой итерации шаги 2 и 3 повторяются. Множественные инициализации и приращения счетчиков циклаСинтаксис задания цикла for позволяет инициализировать несколько переменных- счетчиков, проверять сложные условия продолжения цикла или последовательно выполнять несколько операций над счетчиками цикла. Если присваиваются значения нескольким счетчикам или выполняется несколько операций, они записываются последовательно и разделяются запятыми. В листинге 7.10 инициализируются два счетчика, значения которых после каждой итерации увеличиваются на единицу. Листинг 7.10. Использование нескольких счетчиков в цикле for 1: // Листинг 7.10. 2: // Использование нескольких счетчиков 3: // в цикле for 4: 5: #include <iostream.h> 6: 7: int main() 8: { 9: for (int i=0, j=0; i<3; i++, j++) 10: cout << "i: " << i << " j: " << j << endl; 11: return 0; 12: } Результат: i: 0 j: 0 i: 1 j: 1 i: 2 j: 2 Анализ: В строке 9 переменные i и j инициализируются нулевыми значениями. Затем проверяется условие i<3 и, так как оно справедливо, выполняется первая итерация цикла. На каждой итерации осуществляется вывод значений счетчиков на экран. После этого выполняется третья часть конструкции for, в которой значения переменных-счетчиков увеличиваются на единицу. После выполнения строки 10 и изменения значений переменных условие проверяется снова. Если условие все еще справедливо, запускается следующая итерация цикла. Это происходит до тех пор, пока условие продолжения цикла не нарушится. В этом случае значения переменных не изменяются и управление передается следующему после цикла оператору. Нулевые параметры цикла forЛюбой параметр цикла for может быть опущен. Пропуск означает использование так называемого нулевого параметра. Нулевой параметр, как и любой другой, отделяется от остальных параметров цикла for символом точки с запятой (;). Если опустить первый и третий параметры цикла for, как показано в листинге 7.11, результат его применения будет аналогичен полученному при использовании оператора while. Листинг 7.11. Нулевые параметры цикла for 1: // Листинг 7.11. 2: // Нулевые параметры цикла for 3: 4: #include <iostream.h> 5: 6: int main() 7: { 8: int counter = 0; 9: 10: for( ; counter < 5; ) 11: { 12: counter++; 13: cout << "Looping! "; 14: } 15: 16: cout << "\nCounter: " << counter << ".\n"; 17: return 0; 18: } Результат: Looping! Looping! Looping! Looping! Looping! Counter: 5. Анализ: Очевидно, что результат выполнения такого цикла в точности совпадает с результатом выполнения цикла while из листинга 7.8. В строке 8 присваивается значение переменной counter. Установки параметров цикла for, показанные в строке 10, содержат только проверку условия продолжения цикла. Операция над переменной цикла в конструкции for также опущена. Таким образом, этот цикл можно представить в виде while (counter < 5). Рассмотренный пример еще раз показывает, что возможности языка C++ позволяют решить одну и ту же задачу множеством способов. Листинг 7.11 приведен скорее для иллюстрации гибкости возможностей C++, поскольку ни один опытный программист не будет использовать цикл for подобным образом. Тем не менее можно опустить даже все три параметра цикла for, а для управления циклом использовать операторы break и continue. Пример использования конструкции for без параметров приведен в листинге 7.12. Листинг 7.12. Использование оператора for без параметров 1: //Листинг 7.12. 2: // Использование оператора for без параметров 3: 4: #include <iostream.h> 5: 6: int nain() 7: { 8: int counter=0; ,// установка начального значения счетчика 9: int max; 10: cout << " How many hellos?"; 11: cin >> max; 12: for (;;) // задание бесконечного цикла 13: { 14: if (counter < max) // проверка значения 15: { 16: cout << "Hello!\n"; 17: counter++; // приращение значения счетчика 18: } 19: else 20: break; 21: } 22: return 0; 23: } Результат: How many hellos? Hello! Hello! Hello! Анализ: В этом примере набор параметров оператора for максимально минимизирован. Опущены все три параметра — инициализация, условие и операция. Начальное значение счетчика присваивается в строке 8 еще до начала работы цикла. Условие продолжения цикла также проверяется в отдельной строке (строка 14), и, если оно истинно, выполняется операция тела цикла, после чего в строке 17 увеличивается значение счетчика. Если условие не выполняется, оператор break в строке 20 прерывает выполнение цикла. Несмотря на то что рассмотренная программа выглядит достаточно нелепо, встречаются ситуации, когда конструкции for(;;) и while(true) оказываются просто необходимыми. Более полезный пример использования таких конструкций будет приведен далее в этой главе после рассмотрения оператора switch. Использование пустых циклов forПоскольку синтаксис оператора for позволяет использовать при его описании цикла достаточно сложные конструкции, необходимость в теле цикла иногда вообще отпадает. Это означает, что тело цикла будет состоять из пустой строки, заканчивающейся символом точки с запятой (;). Данный символ можно размещать в одной строке с оператором for. Пример пустого цикла приведен в листинге 7.13. Листинг 7.13. Использование оператора for для организации пустого цикла 1: // Листинг 7.13. 2: // Использование оператора for 3: // для организации "пустого" цикла 4: 5: #include <iostream.h> 6: int main() 7: { 8: for (int i = 0; i<5; cout << "i: " << i++ << endl) 9: ; 10: return 0; 11: } Результат: i: 0 i: 1 i: 2 i: 3 i: 4 Анализ: Оператор for в строке 8 содержит все три параметра. Инициализация в данном случае состоит из описания переменной i и присвоения ей значения 0. Затем проверяется условие i<5, и, если оно выполняется, в третьей части оператора for значение переменной выводится на экран и увеличивается на единицу. Поскольку все необходимые операции выполняются в самом операторе for, тело цикла можно оставить пустым. Такой вариант нельзя назвать оптимальным, так как запись в одной строке большого количества операций значительно усложняет восприятие программы. Правильнее было бы записать этот цикл таким образом: 8: for (int i = 0; i<5; i++) 9: cout << "i: " << i << endl; Оба варианта записи равноценны, однако второй вариант гораздо читабельнее и понятнее. Вложенные циклыЦикл, организованный в теле другого цикла, называют вложенным. В этом случае внутренний цикл полностью выполняется на каждой итерации внешнего цикла. Листинг 7.14 демонстрирует заполнение элементов матрицы с помощью вложенного цикла. Листинг 7.14. Вложенные циклы 1: // Листинг 7.14. 2: // Вложенные циклы с оператором for 3: 4: #include <iostream.h> 5: 6: int main() 7: { 8: int rows, columns; 9: char theChar; 10: cout << "How many rows? "; 11: cin >> rows; 12: cout << "How many columns? "; 13: cin >> columns; 14: cout << "What character? "; 15; cin >> theChar; 16: for (int i = 0; i<rows; i++) 17: { 18: for (int j = 0; j<columns; j++) 19: cout << theChar; 20: cout << "\n"; 21: } 22; return 0; 23: } Результат: How many rows? 4 How many columns? 12 What character? x xxxxxxxxxxxx xxxxxxxxxxxx xxxxxxxxxxxx xxxxxxxxxxxx Анализ: В начале программы пользователю предлагается ввести количество строк и столбцов матрицы, а также символ, которым будет заполняться матрица. В строке 16 задается начальное значение переменной i, после чего начинается выполнение тела внешнего цикла. В первой строке тела внешнего цикла (строка 18) инициализируется еще один цикл. Переменной j присваивается значение 0 и начинается выполнение тела внутреннего цикла. В строке 19 символ, введенный при начале работы программы, выводится на экран. На этом первая итерация внутреннего цикла заканчивается. Вывод одной строки матрицы продолжается до тех пор, пока выполняется условие внутреннего цикла (j<columns). Как только значение переменной j становится равным значению columns, выполнение внутреннего цикла прекращается. После вывода на экран строки матрицы (12 символов "x") управление передается в строку 20 и выводится символ новой строки. После этого проверяется условие внешнего цикла (i<rows) и, если оно справедливо, выполняется следующая итерация. Обратите внимание: во второй итерации внешнего цикла внутренний цикл начинает выполняться с начала. Переменной j присваивается нулевое значение, что позволяет повторно выполнить все итерации внутреннего цикла. Основная идея вложенных циклов состоит в том, что на каждой итерации внешнего цикла внутренний цикл выполняется полностью. Таким образом, результат выполнения данной программы будет следующим: заданный символ выводится для каждой строки столько раз, сколько указано в переменной columns, а количество выводимых строк определяется переменной rows. Область видимости переменных-счетчиков циклов forДо недавнего времени область видимости переменных, описанных в цикле for, распространялась на весь текущий блок. Согласно новому стандарту, установленному ANSI, область видимости переменных, описанных в таком цикле, должна распространяться только на тело цикла. Следует заметить, что, несмотря на внесенные изменения, многие компиляторы продолжают поддерживать только старый стандарт. Набрав приведенный ниже фрагмент программного кода, можно проверить свой компилятор на соответствие новому стандарту. #include <iostream.h> int main() { // Проверка области видимости переменной i for (int i = 0; i<5; i++) { cout << "i: " << i << endl; } i = 7; // i находится за пределами области видимости return 0; } Если такая программа будет компилироваться без ошибок, значит, ваш компилятор еще не поддерживает нового стандарта ANSI. Компиляторы, соответствующие новому стандарту, должны сгенерировать сообщение об ошибке для выражения i = 7. После внесения некоторых изменений программа будет восприниматься всеми компиляторами без ошибок. #include <iostream.h> int main() { int i; //объявление переменной за пределами цикла for (int i = 0; i<5; i++) { cout << "i: " << i << endl; } i = 7; // теперь переменная i будет корректно восприниматься всеми компиляторами return 0; } Обобщение сведений о циклахНа занятии 5 рассматривался пример построения ряда чисел Фибоначчи с использованием рекурсивного алгоритма. Напомним, что этот ряд начинается числами 1, 1, 2, 3, а все последующие его члены являются суммой двух предыдущих. 1,1,2,3,5,8,13,21,34... Таким образом, n-й член ряда Фибоначчи вычисляется сложением (rt-l)-TO и (n-2)-го членов. Рассмотрим вариант решения этой задачи с помощью циклов (листинг 7.15). Листинг 7.15. Нахождение n-го члена ряда Фибоначчи с помощью цикла 1: // Листинг 7.15. 2: // Нахождение n-ro члена ряда Фибоначчи 3: // с помощью цикла 4: 5: #include <iostream.h> 6: 7: 8: int fib(int position); 9: 10: int main() 11: { 12: int answer, position; 13: cout << "Which position? "; 14: cin >> position; 15: cout << "\n"; 16: 17: answer = fib(position); 18: cout << answer << " is the "; 19: cout << position << "Fibonacci number.\n"; 20: return 0; 21: } 22: 23: int fib(int n) 24: { 25: int minusTwo=1, minusOne=1, answer=2; 26: 27: if (n < 3) 28: return 1; 29: 30: for (n -= 3; n; n--) 31: { 32: minusTwo = minusOne; 33: minusOne = answer; 34: answer = minusOne + minusTwo; 35: } 36: 37: return answer; 38: } Результат: Which position? 4 3 is the 4th Fibonacci number. Which position? 5 5 is the 5th Fibonacci number. Which position? 20 6765 is the 20th Fibonacci number. Which position? 100 3314859971 is the 100th Fibonacci number. Анализ: Программа, представленная в листинге 7.15, позволяет найти значение любого члена ряда Фибоначчи. Использование рекурсии заменено циклом, организованным с помощью конструкции for. Кроме того, применение цикла уменьшает объем используемой памяти и время выполнения программы. В строке 13 пользователю предлагается ввести порядковый номер искомого члена ряда Фибоначчи. Для нахождения этого значения используется функция fib(), в качестве параметра которой передается введенный порядковый номер. Если он меньше трех, функция возвращает значение 1. Для вычисления значений, порядковый номер которых превышает 2, используется приведенный ниже алгоритм. 1. Пpиcвaивaютcянaчaльныeзнaчeнияпepeмeнным:minusTwo=1, minus0ne=1, answer=2. Значение переменной, содержащей номер искомой позиции, уменьшается на 3, поскольку две первые позиции обрабатываются выше. 2. Для каждого значения n вычисляем значение очередного члена последовательности. Делается это следующим образом: • переменной minusTwo присваивается значение переменной minusOne; • переменной minusOne присваивается значение переменной answer; • значения переменных minusOne и minusTwo суммируются и записываются в answer; • значение n уменьшается на единицу. 3. Как только n достигнет нуля, возвращается значение переменной answer. Следуя описанному алгоритму, можно воспроизвести на листе бумаги ход выполнения программы. Для нахождения, к примеру, пяти первых членов последовательности на первом шаге записываем 1, 1, 2, Остается определить еще два члена ряда. Следующий член будет равен (2+1=3), а для вычисления искомого члена теперь нужно сложить значения только что полученного члена и предыдущего — числа 2 и 3, в результате чего получаем 5. В сущности, на каждом шаге мы смещаемся на один член вправо и уменьшаем количество искомых значений. Особое внимание следует уделить выражению условия продолжения цикла for, записанному как n. Это одна из особенностей синтаксиса языка C++. По-другому это выражение можно представить в виде n'=0. Поскольку в C++ число 0 соответствует значению false, при достижении переменной n нуля условие продолжения цикла не будет выполняться. Исходя из сказанного, описание цикла может быть переписано в виде for (n-=3; n!=0; n--) Подобная запись значительно облегчит его восприятие. С другой стороны, первоначальный вариант программы иллюстрирует общепринятую для C++ форму записи условия, поэтому не стоит умышленно ее избегать. Скомпилируйте и запустите полученную программу. Сравните время, затрачиваемое на вычисление 25-го числа рекурсивным (см. занятие 5) и циклическим методами. Несомненно, рекурсивный вариант программы более компактный, однако многократный вызов функции, использующийся в любом рекурсивном алгоритме, заметно снижает его быстродействие. Поэтому использование цикла более приемлемо с точки зрения скорости выполнения. Кроме того, благодаря оптимизации арифметических операций в большинстве современных микропроцессоров превосходство не рекурсивных алгоритмов в скорости становится все более очевидным. Испытывая программу, не вводите слишком большие номера членов ряда Фибоначчи. Значения членов ряда возрастают довольно быстро и ввод большого порядкового номера может привести к переполнению регистра памяти. Оператор switchНа занятии 4 вы познакомились с операторами if и if/else. Однако в некоторых ситуациях применение оператора if может привести к возникновению конструкций с большим числом вложений, значительно усложняющих как написание, так и восприятие программы. Для решения этой проблемы в языке C++ предусмотрен оператор switch. Основным его отличием от оператора if является то, что он позволяет проверять сразу несколько условий, в результате чего ветвление программы организуется более эффективно. Синтаксис оператора switch следующий: switch (выражение) { case ПервоеЗначение: оператор; break; case ВтороеЗначение: оператор; break; .... case Значение_N: оператор: break; default: оператор; } В скобках за оператором switch может использоваться любое выражение, корректное с точки зрения синтаксиса языка. Вместо идентификатора оператор допускается использование любого оператора или выражения, а также последовательности операторов или выражений, результатом выполнения которых является целочисленное значение (или значение, которое может быть однозначно приведено к целочисленному типу). Поэтому использование логических операций или выражений сравнения здесь не допускается. Оператор switch Синтаксис использования оператора switch следующий: switch (выражение) { case ПервоеЗначение: оператор; break; case ВтороеЗначение: оператор; break; .... case Значение_N: оператор: break; default: оператор; } Оператор switch позволяет осуществлять ветвление программы по результатам выражения, возвращающего несколько возможных значений. Значение, возвращенное выражением, заданным в скобках оператора switch, сравнивается со значениями, указанными за операторами case, и в случае совпадения значений выполняется выражение в строке соответствующего оператора case. Будут выполняться все строки программы после выбранного оператора до тех пор, пока не закончится тело блока оператора switch, или не повстречается оператор break. Если ни одно из значений операторов case не совпадет с возвращенным значением, то выполняются строки программы, стоящие после оператора default, в случае же отсутствия этого оператора в теле блока switch. управление будет передано следующей за этим блоком строке программы. Пример 1: switch (choice) { case 0: cout << "Zero!" << endl; break; case 1: cout << "One!" << endl; break; case 2: cout << "Two!" << endl; break; default: cout << "Default!" << endl; break; } Пример 2: switch (choice) { case 0: case 1: case 2: cout << "Less than 3!" << endl; break; case 3: cout << "Equals 3!" << endl; break; default: cout << "Greater that 3!" << endl; } При отсутствии оператора break после оператора или выражения, следующего за case, будет выполняться выражение очередного блока case. В большинстве случаев такая ситуация возникает, когда оператор break пропущен по ошибке. Поэтому, если break опускается умышленно, рекомендуем вставлять в соответствующую строку комментарий. Пример использования оператора switch приведен в листинге 7.16. Листинг 7.16. Использование оператора switch 1: //Листинг 7.16. 2: // Использование оператора switch 3: 4: #include <iostream.h> 5: 6: int main() 7: { 8: unsigned short int number; 9: cout << "Enter а number between 1 and 5: "; 10: cin >> number; 11: switch (number) 12: { 13: case 0: cout << "Too small, sorry!"; 14: break; 15: case 5: cout << "Good job!\n"; // fall through 16: case 4: cout << "Nice Pick!\n"; // fall through 17: case 3: cout << "Excellent!\n"; // fall through 18: case 2: cout << "Masterful!\n"; // fall through 19: case 1: cout << "Incredible!\n"; 20: break; 21: default: cout << "Too large!\n"; 22: break; 23: } 24: cout << "\n\n"; 25: return 0; 26: } Результат: Enter a number between 1 and 5: 3 Excellent! Masterful! Incredible! Enter a number between 1 and 5: 8 Too large! Анализ: Сначала программа предлагает ввести число. Затем введенное число обрабатывается оператором switch. Если вводится 0, то это соответствует значению оператора case из строки 13 и на экран выводится сообщение Too small, sorry!, после чего оператор break завершает выполнение конструкции switch. Если вводится число 5, управление передается в строку 15 и выводится соответствующее сообщение. Затем выполняется строка 16, в которой также выводится сообщение, и так до строки 20. В этой строке оператор break завершает выполнение блока с оператором switch. Таким образом, при вводе чисел от 1 до 5 на экран будет выводиться ряд сообщений. Если же вводится число, превышающее 5, выполняется строка 21 с оператором default, в которой выводится сообщение Too large!. Обработка комманд менб с помощью оператора switchВернемся к теме циклов с оператором for(;;). Такие конструкции называют бесконечными циклами, поскольку, если выполнение такого цикла не прервать оператором break, он будет работать бесконечно. Циклы подобного типа удобно использовать для обработки команд меню (листинг 7.17). Пользователь выбирает одну из предложенных команд, затем выполняется определенное действие и осуществляется возврат в меню. Так продолжается до тех пор, пока пользователь не выберет команду выхода. В бесконечных циклах не существует условия, при нарушении которого цикл прерывается. Поэтому выйти из такого цикла можно только посредством оператора break. Листинг 7.17. Пример бесконечного цикла 1: //Листинг 7.17. 2: //Обработка диалога с пользователем 3: //посредством бесконечного цикла 4: #include <lostream,h> 5: 6: // прототипы функций 7: int menu(); 8: void DoTaskOne(); 9: void DoTaskMany(int); 10: 11: int main() 12: { 13: 14: bool exit = false; 15: for (;;) 16: { 17: int choice = menu(); 18: switch(choice) 19: { 20: case (1): 21: DoTaskOne(); 22: break; 23: case (2): 24: DoTaskOne(2); 25: break; 26: case (3): 27: DoTaskOne(3); 28: break; 29: case (4): 30: continue; 31: break; 32: case (5): 33: exit=true; 34: break; 35: default : 36: cout << " Please select again!\n"; 37: break; 38: } // конец блока switch 39: 40: if (exit) 41: break; 42: } // и так до бесконечности 43: return 0; 44: } // конец функции main() 45: 46: int menu() 47: { 48: int choice; 49: 50: cout << " **** Menu **** \n\n"; 51: cout << "(1) Choice one\n"; 52: cout << "(2) Choice two\n"; 53: cout << "(3) Choice three\n"; 54: cout << "(4) Redisplay menu.\n"; 55: cout << "(5) Quit.\n\n"; 56: cout << ": "; 57: cin >> choice; 58: return choice; 59: } 60: 61: void DoTaskOne() 62: { 63: cout << "Task One!\n"; 64: } 65: 66: void DoTaskMany(int which) 67: { 68: if (which == 2) 69: cout << "Task Two!\n"; 70: else 71: cout << "Task Three!\n"; 72: } Результат: **** Menu **** (1) Choice one. (2) Choice two. (3) Choice three. (4) Redisplay menu. (5) Quit. : 1 Task One! **** Menu **** (1) Choice one. (2) Choice two. (3) Choice three. (4) Redisplay menu. (5) Quit. : 3 Task Three! **** Menu **** (1) Choice one. (2) Choice two. (3) Choice three. (4) Redisplay menu. (5) Quit. : 5 Анализ: В данной программе используются многие средства программирования, рассмотренные на этом и предыдущих занятиях. Тут же вы найдете пример использования конструкции switch. Работа бесконечного цикла начинается в строке 15. Функция menu() обеспечивает вывод на экран команд меню и возвращает номер выбранной пользователем команды. Обработка введенного номера осуществляется в конструкции switch в строках 18—38. При выборе первой команды управление передается следующему после строки case (1): оператору (строка 21). Далее, в строке 21, вызывается функция DoTaskOne(), которая выводит на экран сообщение о выборе пользователя. После завершения работы функции осуществляется возврат в точку вызова и выполняется оператор break (строка 22). Оператор break прерывает работу блока switch и управление передается в строку 39. Далее, в строке 40, проверяется значение переменной exit. Если оно истинно, бесконечный цикл прерывается оператором break в строке 41. В противном случае выполняется следующая итерация цикла (строка 15). Особое внимание следует уделить оператору continue в строке 30. Внимательно проанализировав структуру программы, вы заметите, что этот оператор можно опустить, причем работа программы не изменится. Если бы строки с этим оператором не было, выполнялся бы оператор break, затем оператор if и, так как переменная exit содержала бы значение false, запускалась следующая итерация цикла. Использование оператора continue просто позволяет перейти на новую итерацию без проверки значения exit. РезюмеВ языке C++ существует множество способов организации циклических процессов. Оператор while проверяет условие и, если оно истинно, передает управление телу цикла. В конструкции do...while условие проверяется уже после выполнения тела цикла. Оператор for позволяет инициализировать переменные цикла, после чего проверяется выполнение условия. Если оно истинно, выполняется тело цикла, а затем операция, являющаяся третьей частью заголовка конструкции for. Перед началом каждой следующей итерации условие проверяется заново. Оператора goto следует по возможности избегать, поскольку он позволяет осуществить переход в любую точку программы, что значительно усложняет ее восприятие и анализ. С помощью оператора continue можно осуществить переход на следующую итерацию цикла while, do...while или for, а break позволяет мгновенно завершить работу цикла. Вопросы и ответыКак определить, какой из операторов, if/else или switch, лучше использовать в конкретной ситуации? Если приходится использовать более двух вложений операторов if, то лучше воспользоваться конструкцией с оператором switch. Как выбрать между операторами while и do...while? Если тело цикла должно выполняться хотя бы один раз, используйте цикл do...while. Во всех остальных случаях используйте оператор while. Как выбрать между операторами while и for? В тех случаях, когда переменная счетчика еще не инициализирована и ее значение изменяется после каждой итерации цикла на постоянную величину, используйте оператор for. В остальных случаях предпочтительнее while. В каких случаях лучше использовать рекурсию, а в каких итерацию? Несомненно, в большинстве случаев итеративный метод предпочтительнее, однако, если один и тот же цикл приходится повторять в разных частях программы, удобнее использовать рекурсию. Какой из операторов, for(;;) или while(true) работает эффективнее? Существенного различия между ними нет. КоллоквиумВ этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний и приводится несколько упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из предложенных ниже вопросов. Контрольные вопросы1. Можно ли в цикле for инициализировать сразу несколько переменных-счетчиков? 2. Почему следует избегать использование оператора goto? 3. Можно ли с помощью оператора for организовать цикл, тело которого не будет выполняться? 4. Можно ли организовать цикл while внутри цикла for? 5. Можно ли организовать цикл, который никогда не завершится? Приведите пример. 6. Что происходит при запуске бесконечного цикла? Упражнения1. Каким будет значение переменной x после завершения цикла for (int x = 0; x < 100; x++)? 2. Создайте вложенный цикл for, заполняющий нулями массив размером 10x10. 3. Организуйте цикл for, счетчик которого изменяется от 100 до 200 с шагом 2. 4. Организуйте цикл while, счетчик которого изменяется от 100 до 200 с шагом 2. 5. Организуйте цикл do...while, счетчик которого изменяется от 100 до 200 с шагом 2. 6. Жучки: найдите ошибку в приведенном фрагменте программы. int counter = 0; while (counter < 10) { cout << "counter: " << counter; } 7. Жучки: найдите ошибку в приведенном фрагменте программы. for(int counter = 0; counter < 10; counter++); cout << counter << " "; 8. Жучки: найдите ошибку в приведенном фрагменте программы. int counter = 100; while (counter < 10) { cout << "counter: " << counter; counter--; } 9. Жучки: найдите ошибку в приведенном фрагменте программы. cout << "Enter а number between 0 and 5: "; cin >> theNumber; switch (theNumber) { case 0: doZero(); case 1: // идем дальше case 2: // идем дальше case 3: // идем дальше case 4: // идем дальше case 5: doOneToFive(); break; default: doDefault(); break; } Подведение итоговЛистинг. Итоги первой недели 1: #include <iostream.h> 2: intintboolfalsetrue 3: enum CHOICE { DrawRect = 1, GetArea, 4: GetPerim, ChangeDimensions, Quit} ; 5: // Объявление класса Rectangle 6: class Rectangle 7: { 8: public: 9: // constructors 10: Rectangle(int width, int height); 11: ~Rectangle(); 12: 13: // Методы доступа 14: int GetHeight() const { return itsHeight; } 15: int GetWidth() const { return itsWidth; } 16: int GetArea() const { return itsHeight * itsWidth; } 17: int GetPerim() const { return 2*itsHeight + 2*itsWidth; } 18: void SetSize(int newWidth, int newHeight); 19: 20: // Прочие методы 21: 22: 23: private: 24: int itsWidth; 25: int itsHeight; 26: }; 27: 28: // Выполнение методов класса 29: void Rectangle::SetSize(int newWidth, int newHeight) 30: { 31: itsWidth = newWidth; 32: itsHeight = newHeight; 33: } 34: 35: 36: Rectangle::Rectangle(lnt width, int height) 37: { 38: itsWidth = width; 39: itsHeight = height; 40: } 41: 42: Rectangle::~Rectangle() { } 43: 44: int DoMenu(); 45: void DoDrawRect(Rectangle); 46: void DoGetArea(Rectangle); 47: void DoGetPerim(Rectangle); 48: 49: int main () 50: { 51: // Инициализация объекта rectangle значением 30,5 52: Rectangle theRect(30,5); 53: 54: int choice = DrawRect; 55: int fQuit = false; 56: 57: while (!fQuit) 58: { 59: choice = DoMenu(); 60: if (choice < DrawRect || choice > Quit) 61: { 62: cout << "\nInvalid Choice, please try again.\n\n" 63: continue; 64: } 65: switch (choice) 66: { 67: case DrawRect: 68: DoDrawRect(theRect); 69: break; 70: case GetArea: 71: DoGetArea(theRect); 72: break; 73: case GetPerim: 74: DoGetPerim(theRect); 75: break; 76: case ChangeDimensions: 77: int newLength, newWidth; 78: cout << "\nNew width: "; 79: cin >> newWidth; 80: cout << "New height: "; 81: cin >> newLength; 82: theRect.SetSize(newWidth, newLength); 83: DoDrawRect(theRect); 84: break; 85: case Quit: 86: fQuit = true; 87: cout << "\nExiting...\n\n"; 88: break; 89: default: 90: cout << "Error in choioe!\n"; 91: fQuit = true; 92: break; 93: } // end switch 94: } // end while 95: return 0; 96: } // end main 97: 98: int DoMenu() 99: { 100: int choice; 101: cout << "\n\n *** Menu *** \n"; 102: cout << "(1) Draw Rectangle\n"; 103: cout << "(2) Area\n"; 104: cout << "(3) Perimeter\n"; 105: cout << "(4) Resize\n"; 106: cout << "(5) Quit\n"; 107: 108: cin >> choice; 109: return choice; 110: } 111: 112: void DoDrawRect(Rectangle theRect) 113: { 114: int height = theRect.GetHeight(); 115: int width = theRect.GetWidth(); 116: 117: for (int i = 0; i<height; i++) 118: { 119: for (int j = 0; j< width; j++) 120: cout << "*"; 121: cout << "\n"; 122: } 123: } 124: 125: 126: void DoGetArea(Rectangle theRect) 127: { 128: cout << "Area: " << theRect.GetArea() << endl; 129: } 130: 131: void DoGetPerim(Rectangle theRect) 132: { 133: cout << "Perimeter: " << theRect.GetPerim() << endl; 134: } Результат: *** Menu*** (1) Draw Rectangle (2) Area (3) Perimeter (4) Resize (5) Quit 1 ****************************** ****************************** ****************************** ****************************** ****************************** *** Menu*** (1) Draw Rectangle (2) Area (3) Perimeter (4) Resize (5) Quit 2 Area: 150 *** Menu*** (1) Draw Rectangle (2) Area (3) Perimeter (4) Resize (5) Quit 3 Perimeter: 70 *** Menu*** (1) Draw Rectangle (2) Area (3) Perimeter (4) Resize (5) Quit 4 New Width: 10 New height: 8 ********** ********** ********** ********** ********** ********** ********** ********** *** Menu*** (1) Draw Rectangle (2) Area (3) Perimeter (4) Resize (5) Quit 2 Area: 80 >>** Menu*** (1) Draw Rectangle (2) Area (3) Perimeter (4) Resize (5) Quit 3 Perimeter: 36 *** Menu*** (1) Draw Rectangle (2) Area (3) Perimeter (4) Resize (5) Quit 5 Exiting. . . Анализ: В данной программе сведено большинство тех средств и подходов программирования, с которыми вы познакомились в течение первой недели. Вы должны не только уметь ввести программный код, скомпилировать, скомпоновать и запустить эту программу, но также и понимать, что и как в ней работает. Если все это вам удалось, значит, неделя прошла не зря. В первых шести строках делаются объявления новых типов данных и важные определения, которые затем будут использоваться на протяжении всей программы. В строках 6—26 объявляется класс Rectangle. Он содержит открытые методы доступа для возвращения и установки ширины и высоты прямоугольника, а также для вычисления его площади и периметра. Строки 29-40 содержат определения тех функций-членов класса, которые не объявлялись с ключевым словом inline. Прототипы обычных функций, не являющихся членами класса, находятся в строках 44—47, а основной блок программы начинается со строки 49. Суть программы состоит в построении виртуального прямоугольника с выводом меню, предлагающего выбор из пяти опций: вывод прямоугольника на экран, определение его площади, определение периметра, изменение размера прямоугольника и выход из программы. Флаг устанавливается в строке 55, и если пользователь установит неверное значение, то вывод меню на экран повторится. Это будет продолжатся до тех пор, пока пользователь правильно не укажет один из режимов работы либо не выберет завершение программы. В случае выбора одного из режимов работы, за исключением ChangeDimensions, будет вызываться соответствующая функция, выбираемая с помощью оператора switch. Выбор константы ChangeDimensions не вызывает никакой функции, поскольку в этом случае пользователь должен ввести новые значения размера прямоугольника. Если предположить, что для изменения размеров прямоугольника в программе существовала бы специальная функция DoChangeDimensions(), в которую объект Rectangle передавался бы как значение, то все изменения в функции производились бы над копией существующего объекта, а сам объект в функции main() оставался бы неизменным. На занятии 8, посвященном указателям, и на занятии 10, где речь идет о разработке более сложных функций, вы узнаете, как обойти это ограничение, передавая объекты в функции как ссылки. Но пока все изменения значения объекта можно осуществлять только в функции main(). Обратите внимание, что использование перечисления сделало конструкцию оператора switch более понятной. Если бы вместо констант, о назначении которых можно судить по их именам, проверялись бы вводимые пользователем числовые значения от 1 до 5, нам бы пришлось каждый раз возвращаться к описанию меню, чтобы не запутаться в том, какой номер соответствует той или иной опции. В строке 60 осуществляется проверка, входит ли значение, введенное пользователем, в диапазон допустимых значений. Если это не так, будет показано сообщение об ошибке и вывод меню на экран повторится. Тем не менее обратите внимание, что конструкция оператора switch содержит оператор default, хотя в этой программе он никогда не будет выполняться. Этот оператор добавлен исключительно для облегчения отладки программы, а также на случай будущих изменений в программе. Итоги первой неделиПоздравляем вас! Вы завершили первую неделю обучения программированию на C++! Теперь вы вполне готовы не только к пониманию, но и к созданию довольно сложных программ. Конечно, еще многое нужно узнать, и следующая неделя начнется с довольно сложной и запутанной темы — использование указателей. Не расслабляйтесь, вам предстоит еще более углубиться в пучину объектно-ориентированного программирования, виртуальных функций и многих других современных и мощных средств языка программирования C++. Немного передохните, наградите себя шоколадной медалью за проделанный путь и, перелистнув страницу, приступайте к следующей неделе. Примечания:int main() { unsigned short x; unsigned short у; unsigned short z; z = x * у; return 0; } int main() { unsigned short Width; unsigned short Length; unsigned short Area; Area = Width * Length; return 0; } Ниже приводится синтаксис оператора if. Форма 1: if (условие) выражение; следующее выражение; Если условие возвращает true, то выполняется выражение, а за ним и следующее выражение. Если условие возвратит false, то выражение игнорируется, а программа переходит к выполнению следующего выражения. Помните, что вместо выражения может использоваться целый блок, заключенный в фигурные скобки. Форма 2: if (условие) выражение1; else выражение2; следующее выражение; Если условие возвращает значение true, выполняется выражение1, в противном случае выполняется выражение2. После этого выполняется следующее выражение. Пример: if (SomeValue < 10); cout << "SomeValue is less than 10"; else cout << "SomeValue is not less than 10!"; cout << "Done." << endl; |
|
||
Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх | ||||
|