10808

Пособие по языку С++

Книга

Информатика, кибернетика и программирование

Введение Данное пособие составлено для того чтобы помочь студентам научиться разрабатывать программное обеспечение компьютеров. В нем рассматриваются такие базовые понятия как алгоритм компьютер и программа. Для разработки программ используется алгоритмический я...

Русский

2013-04-02

997.5 KB

5 чел.


Введение

Данное пособие составлено для того, чтобы помочь студентам научиться разрабатывать программное обеспечение компьютеров.  В нем рассматриваются такие базовые понятия, как алгоритм, компьютер и программа. Для разработки программ используется алгоритмический язык С++. Несмотря на появление в последнее время систем программирования, которые применяют другие языки программирования (например, такие, как Pascal, Basic, Java), язык программирования С++ остается основным алгоритмическим языком, который используют для создания большей части коммерческого программного обеспечения. Студенты практически всех западных университетов, имеющие отношение к дисциплине Computer Science, изучают программирование на языке С++. Этот язык дает полную свободу в программировании компьютеров, и программа составленная на этом языке может быть выполнена в любой операционной системе и на любом типе компьютера.

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

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

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

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

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

Глава 1. Алгоритм и компьютер

В этой главе будут рассмотрены основные понятия, знание которых необходимо для разработки программного обеспечения. Такими понятиями являются:

  •  алгоритм - описание последовательности решения задачи;
  •  компьютер - устройство, которое может выполнить алгоритм;
  •  язык программирования - способы записи алгоритмов.

1.1. Понятие алгоритма

Мы достаточно часто в повседневной жизни сталкиваемся с необходимостью объяснять другим людям, как решать различные задачи. Эти задачи могут быть очень простыми, например, как добраться до нужного места в городе или заменить колесо у автомобиля, или более сложными, например, как решить задачу по математике или составить бухгалтерский отчет. Лучший способ пояснить решение любой задачи состоит в следующем:

  1.  разделить весь процесс ее решения на простые действия - инструкции, которые будут понятны совершенно одинаково всем, кому объясняется решение задачи;
  2.  составить такую последовательность этих инструкций, выполнение которой приведет к решению задачи.

Такая запись порядка решения задачи путем выполнения последовательности простых инструкций получила название - алгоритм.

Новый Термин: алгоритмом называется последовательность простых однозначно понимаемых инструкций, выполнение которых приводит к решению поставленной задачи.

Алгоритм является точным термином и определяет не всякое решение задачи, а только то, которое удовлетворяет следующим основным условиям:

  •  однозначность – каждая инструкция должна пониматься правильно любым, кто ее выполняет, и не должна допускать различных толкований;
  •  результативность – выполнение алгоритма всегда должно приводить к какому-то результату – либо к решению задачи, либо к заключению, что задача не имеет решения.

В связи с этим, например, кулинарный рецепт нельзя считать алгоритмом, так как инструкции, записанные в нем, могут пониматься различными людьми по-разному (например, такая инструкция – “посолить по вкусу”).

Среди задач, постоянно решаемых человеком, есть множество задач, связанных с обработкой информации. Это не обязательно математические задачи. К таким задачам относятся и поиск номера телефона в справочнике, и составление отчета по расходу денег и т.п. Все эти задачи связаны с обработкой информации – чисел и текстов. Для решения подобных задач также составляются алгоритмы, которые называются вычислительными. Ранее подобные алгоритмы мог выполнить только человек, но в середине XX века было создано устройство, которое также может выполнить любой правильно составленный вычислительный алгоритм. Таким устройством явилась вычислительная машина, или компьютер.

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

Непосредственно программу в компьютере выполняет его центральная часть - процессор. Процессор может выполнять только фиксированный набор элементарных инструкций, которые называются машинными инструкциями или командами. Каждая машинная инструкция описывает простое действие, которое может выполнить процессор. Эти инструкции записываются и хранятся в виде длинного число, разные части этого числа имеют различный смысл для процессора. Например, инструкцией может быть такое число:  0101250130. Структура машинных инструкций различная для разных типов процессоров. Например, она может быть следующей:

Из этой схемы видно, что каждая инструкция включает две основные части – кода операции и операнды. Все типы инструкций, которые может выполнить процессор, пронумерованы (закодированы) и код операции имеет одно из этих значений (например, 01 - сложение, 02 - вычитание, и т.д.). Операнды (их может быть несколько, от 1 до 3) задают адреса ячеек оперативной памяти или регистров (ячейки памяти, расположенные в процессоре), в которых хранятся данные.  

В нашем условном примере первые две цифры задают код операции – 01, а два следующих четырехзначных числа задают адреса первого и второго операнда – 0125 и 0130, т.е. в этой команде будут использоваться данные, записанные в ячейках оперативной памяти с адресами 0125 и 0130. Процессоры различных типов понимают различные наборы машинных инструкций, поэтому программа, выполняемая на одном типе процессоров (например, на компьютерах фирмы Intel), не будет выполняться на компьютерах с процессором другого типа.

1.2. Языки программирования

Когда появились первые компьютеры, запись программы для них выполнялась в виде машинных инструкций. Это было чрезвычайно сложно, так как человеку очень неудобно работать с большим количеством чисел. Любая ошибка в записи инструкций (в записи чисел) приводила к сбою в работе программы. Человек удобнее работать с символьной информацией. Поэтому программисты старались облегчить себе работу и вначале записывали программы не в виде машинных инструкций, а, например, в виде последовательности простых символьных записей, которые однозначно соответствуют машинным инструкциям, но записываются с помощью коротких буквенных сокращений. Например, вместо кода операции 01 (сложить) можно написать ее символьное название ADD, вместо адресов, таких как 0125, можно записывать символьные осмысленные имена, например,  NUMBER, STEP, VALUE и т.п. В этом случае предыдущую машинную инструкцию можно записать следующим образом: ADD NUMBER,VALUE.

Такая запись более понятна человеку, но абсолютно непонятна компьютеру. Для того чтобы сделать записанный таким способом алгоритм понятным компьютеру, приходилось после составления всего алгоритма переписывать его заново в виде машинных инструкций. Для этого составлялись специальные кодировочные таблицы, которые применяли для формирования машинных инструкций. Например, если составить такую кодировочную таблицу:

Название

Код

ADD

01

NUMBER

0125

VALUE

0130

то не трудно выполнить следующее преобразование:

ADD NUMBER,VALUE  =>  0101250130

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

Такой символьный способ записи инструкций назвали ассемблерным языком, а программу перекодировки – транслятором, т.е. переводчиком с одного языка на другой.

Особенность языка ассемблер состоит в том, что каждой машинной инструкции соответствует инструкция языка ассемблер. Это ненамного облегчило работу программиста, ему все равно приходилось записывать алгоритм в виде очень простых инструкций. Однако человеку неудобно описывать решение задачи в виде таких простых действий. Он привык оперировать более сложными и абстрактными понятиями, хотя бы такими, как формулы. Человеку проще записать формулу f = (a + 2b)/2, чем написать три или четыре инструкции для последовательного выполнения всех операций для ее вычисления. Поэтому в середине 50-х годов специалисты разработали первую специальную программу, которая могла переводить сложные записи в виде формул в наборы машинных инструкций. Этот язык получил название переводчик формул - Formula Translator или короче Fortran. Это был первый язык высокого уровня для записи алгоритмов - алгоритмический язык. После этого появилось большое количество алгоритмических языков. Наиболее известными являются: Cobol, Algol, Basic, PL/1, Pascal, C/C++, Java.

Существует два типа трансляторов: интерпретатор и компилятор.

  •  интерпретатор последовательно преобразует инструкции языка, которые составляют программу, в машинные инструкции и сразу же передает их процессору на выполнение (обычно язык Basic использует интерпретатор);
  •  компилятор преобразует все инструкции языка, которые составляют программу, в машинные инструкции и записывает их в промежуточный файл. Этот шаг называется компиляцией, и в результате его выполнения получается так называемый объектный файл. Затем компилятор вызывает специальную программу - редактор связей, которая преобразует объектный файл в выполняемую компьютером  программу в машинных кодах.

Так как интерпретаторы читают инструкции программы в той форме, как они записаны и сразу же выполняют их, то программисту легче работать с интерпретаторами. Компиляторы требуют выполнения дополнительных шагов по компиляции и редактированию кода, что составляет неудобство в работе с ними. Однако компиляторы создают программы, которые при запуске выполняются очень быстро. Другим преимуществом компиляторов является то, что можно передавать выполняемые программы пользователям, которые не имеют компилятора. В случае с интерпретируемыми языками пользователь должен иметь интерпретатор для того, чтобы выполнить программу.

Глава 2. Алгоритмический язык С++

2.1. История развития языка C++

Одним из наиболее популярных языков для создания  программного обеспечения является язык С++. Этот язык программирования является расширением алгоритмического языка С. Язык С был разработан в 1972 году сотрудником фирмы AT&T Bell Laboratories Денисом Ритчи. Язык С был компромиссом между уровнем языка ассемблер и такими алгоритмическими языками, как Algol и Pascal. Он позволяет создавать программы, которые используют все возможности процессора, и в тоже время использовать инструкции высокого уровня, которых нет в ассемблере. Еще одним большим достоинством этого языка является его переносимость, то есть алгоритм, написанный на языке С, можно выполнить на компьютерах самых различных платформ. Используя язык С, программист может создавать очень эффективные программы, но, к сожалению, у него и больше возможности ошибиться.

В начале 80-х годов специалист этой же фирмы Бьерн Страуструп расширил язык С. Он включил в него дополнительные возможности, которые необходимы для объектно-ориентированного программирования, и назвал новый язык C++. Этот язык приобрел очень большую популярность при разработке программ самого различного назначения.

В августе 1998 году международная организация стандартов ISO утвердила международный стандарт языка С++. А это означает, что все разработчики компиляторов языка С++ будут одинаково понимать все элементы данного языка и программа составленная на языке С++ будет правильно обрабатываться с помощью любого компилятора.

2.2. Последовательность разработки программы

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

Хотя существуют различные подходы к разработке программ, но почти все они включают пять основных этапов:

  1.  постановка задачи;
  2.  разработка алгоритма;
  3.  составление программы;
  4.  проверка и отладка программы;
  5.  документирование и сопровождение программы.

2.2.1. Постановка задачи

Основная цель постановки задачи заключается в том, чтобы точно определить, что должна делать разрабатываемая программа. Должно быть определено, какие данные требуются для работы программы, откуда они берутся, и какие выходные данные должны быть получены. А также определяется - кто и как будет работать с программой.

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

2.2.2. Разработка алгоритма

На втором этапе процесса проектирования программы разрабатывается алгоритм решения поставленной задачи. При этом вначале определяется метод решения задачи, а затем этот метод записывается в виде алгоритма. При составлении алгоритма учитывается его эффективность, т.е. как быстро алгоритм приведет к решению задачи. Как правило, алгоритм вначале пишется не на алгоритмическом языке. Для записи алгоритма могут использоваться или естественный язык, или графические символы -  блок схемы.

2.2.3. Составление программы

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

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

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

2.2.4. Тестирование и отладка

На данном этапе проверяется, что программа работает правильно и надежно. Если программа выдает неверные результаты или останавливается во время выполнения (“зависает”), то должна быть выполнена отладка программы - поиск логических, или других скрытых ошибок. Ошибки, которые вызывают остановку в работе программы или ее аварийное завершение, называются ошибками времени выполнения (a run-time error). Такие ошибки возникают, когда программа содержит инструкции, которые процессор не может выполнить.

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

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

Тестирование и отладка программы занимает большую часть времени работы программиста.

2.2.5. Документирование и сопровождение программы

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

  •   документация в программе – это пояснения и замечания, которые программист записывает непосредственно в программу, с помощью так называемых комментариев (см. раздел  3.4);
  •   документация вне программы – это специальные документы, которые содержат описание решаемой задачи, логики работы программы и ее структуру;
  •   пользовательская документация – это подробные инструкции о порядке работы пользователей с программой.

Другой важной частью данного этапа является сопровождение программы. Как правило, программа разрабатывается для того, чтобы помочь пользователям в решении реальных задач. Но так как со временем эти задачи будут меняться, либо в работе программы будут обнаруживаться ранее не выявленные ошибки, то очень вероятно, что пользователь будет просить исправить или доработать программу. Сопровождение программы является неизбежным и важным этапом в процессе ее разработки.

2.3. Средства разработки программ

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

Файлы с текстами программ, которые создаются с помощью редактора текста, называются исходными кодами, и они обычно имеют названия с расширением .CPP.

Для того чтобы превратить исходный код в выполняемую компьютером программу используется компилятор. После компиляции исходного кода, создается объектный файл. Этот файл имеет название с расширением .OBJ. Однако он также не является программой, которую может выполнить компьютер. Дело в том, что любая, даже самая простая программа использует различные функции (ранее составленные части программы). В объектном файле этих функций еще нет, а есть только таблицы с их списком.  Для того чтобы превратить этот файл в программу, нужно будете запустить на выполнение еще одну специальную программу - редактор связей (linker).

Данная программа просматривает в указанных объектных файлах списки нужных функций, находит их и присоединяет к общей программе. Во время своей работы редактор связей ищет функции в различных библиотеках. Библиотека является набором объектных файлов, которые поставляются вместе с компилятором, либо приобретаются отдельно, либо создаются самими программистами. Все компиляторы C++ поставляются со стандартной библиотекой полезных функций и классов, которые можно использовать при разработке программ.

В результате работы редактора связей получается выполняемая программа, которая записывается в файл с расширением .EXE.

Для проверки работы программы используются специальные программы отладчики (debugger), которые позволяют программисту следить за тем, как работает программа, и какие значения получают переменные во время ее работы. Без использования отладчика практически нельзя добиться того, чтобы программа работала надежно.

Рис. 1. Шаги разработки программы на языке С++

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

В настоящее время существуют специальные программные системы, которые объединяют в себе возможности редактора текста, компилятора, редактора связей и отладчика. Такие программы называются Интегрированными Средами Разработки (Integrated Development Environment). Наиболее известными примерами таких систем являются: Visual C++(фирма Microsoft), C++ Builder (фирма Inprise) и VisualAge for C++(фирма IBM). В данном пособии работа с такими интегрированными системами не рассматривается, но все примеры могут быть выполнены с помощью любой из них.Глава 3. Основные части программы на языке C++

Программа на языке C++ включает в себя различные части. Данное пособие посвящено их подробному пояснению. Однако для того, чтобы понять, как все эти части работают вместе вначале нужно рассмотреть пример полностью работающей программы.

3.1. Простая программа

Первая программа показана в примере 3.1. Она очень простая, но  дает возможность увидеть общую структуру программ на языке С++.  При выполнении эта программа предлагает пользователю ввести его возраст (age) в годах, а затем рассчитывает соответствующее количество дней  и выводит полученный результат на экран. Вы можете не понимать все, что содержится в этой программе, но это будет хорошим началом для изучения языка.

Пример 3.1. Простая программа.

1:/*       Простая С++ программа.

2:   Вычисляет возраст пользователя в днях. */

3:#include <iostream>

4:using namespace std;

5:int main( void )

6:{

7:  int years, days;

8://  вводим количество лет

9:  cout << "How old are you in years? ";

10:  cin >> years;

11://  вычисляем количество дней

12:  days = years * 365;

13://  выводим на экран результат

14:  cout<<"Your age in days is about: " << days<<endl;

15:  return 0;

16:}

Напечатайте эту программу (без номеров строк) в текстовом редакторе точно так, как она показана в примере 3.1. Файлу с программой можно дать название SIMPLE.CPP. Убедитесь, что ввели текст программы точно так, как напечатано в пособии. Особенное внимание уделите знакам пунктуации. Все строки программы заканчивается точкой с запятой (;). Заметьте, что весь текст программы записан строчными (маленькими) буквами. Строчные буквы являются стандартными в С++. При желании, в названии переменных можно использовать и прописные (большие) буквы, но все специальные (ключевые) слова языка должны записываться только маленькими буквами.

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

How old are you in years?

Теперь нужно напечатать свой возраст в годах, например – 20, и нажать клавишу “Enter”. В ответ программа напечатает в следующей строке экрана сообщение:

Your age in days is about: 7300

3.2. Структура программы

Данная программа включает такие общие для всех программ на языке С++ части, как

  •  директиву препроцессора (в строке 3);
  •  директиву задания пространства имен using (в строке 4);
  •  определение функции main (в стоках 5-16)
  •  ввод дынных и вывод результатов из программы (9,10 и 14);
  •  комментарии (в строках 1,2 ,8,11 и 13)

В строке 3 файл iostream включается (переписывается) в файл с программой. Первый символ строки # является сигналом для так называемого препроцессора. Каждый раз, когда компилятор запускается на выполнение, начинает работать специальная часть компилятора - препроцессор. Препроцессор читает исходный текст, ищет строки, которые начинаются с символа решетки (#), и обрабатывает эти строки, прежде чем начнет работать компилятор. Слово “include” является директивой препроцессора, которая вызывает поиск файла с заданным именем и переписывание всего, что в нем содержится, в программу (начиная со строки, где стоит эта директива). Угловые скобки вокруг имени файла дают команду препроцессору искать этот файл в специальном подкаталоге INCLUDE, где обычно такие файлы находятся. Этот подкаталог содержит все заголовочные файлы (часть с расширением H, часть без расширения), которые используются компилятором. В заголовочных файлах содержатся объявления стандартных функций, классов и констант, которые можно использовать в программе. В данном файле объявлены потоки ввода-вывода cin и cout, с помощью которых выполняется ввод исходных данных с клавиатуры и вывод сообщений на экран дисплея. Результатом выполнения строки 3 является включение содержимого файла iostream в программу так, как если бы программист переписал его в программу вручную.

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

Полные имена объектов, которые хранятся в библиотеках, включают название пространства имен, где они определены. Пространство имен это просто название группы имен. Например, объект cout имеет полное имя std::cout. Здесь, std - название пространства имен, в котором описаны все элементы стандартной библиотеки языка С++, а cout имя объекта. Символы “::“ называются операцией уточнения области видимости. Если использовать только имя объекта, то компилятор, будет выдавать сообщение, что данный объект не объявлен. Поэтому, мы должны либо использовать полные имена библиотечных объектов в программе (что может быть утомительным), либо заранее сказать компилятору, чтобы он искал все стандартные объекты в пространстве имен std с помощью директивы: using namespace std; Эта директива будет использоваться во всех примерах программ данного пособия.

Со строки 5 действительно начинается программа с функции, которая  называется main(). Каждая программа на языке C++ имеет функцию с таким названием. В самом общем виде, функция является блоком инструкций программы, который решает какую-то конкретную задачу. Обычно функции вызываются на выполнение другими функциями, но функция main() является специальной. Когда программа начинает работать, эта функция  вызывается автоматически операционной системой.

Для функции main(), так же как и для других функций, должно быть указано, результат какого типа она возвращает. Подробно о типах будет говориться в главе 4 “Переменные и константы”. Типом возвращаемой величины для функции main() в SIMPLE.CPP является int, означающее, что результатом работы функции является целое число. Возврат значений из функций будет обсуждаться в главе 6 "Функции".

Инструкции, которые входят в функцию, заключаются в фигурные скобки. Фигурные скобки для функции main() находятся в строках 6 (открывающая скобка - {) и 16 (закрывающая скобка - }). Все, что находится между открывающей и закрывающей скобками называется телом функции.

Основным содержимым этой программы является строки 7-14. Имена cin и cout используется в языке C++ для описания ввода данных с клавиатуры и вывода текста и значений на экран.  Объекты cin и cout называются потоками ввода и вывода. Работа с ними подробнее поясняется в следующем разделе. Если требуется вывести на экран текст (строку символов), то его нужно обязательно заключать в двойные апострофы ("), как показано в строке 9.

Обычно функция main() возвращает значение int. Это значение передается операционной системе, когда программа завершает работу. Некоторые программисты сообщают об ошибке, возвращая значение 1. В данном пособии, функция main() будет всегда возвращать 0.

3.3. Краткое пояснение потоков ввода и вывода

Можно использовать потоки cin и cout без полного понимания как они работает. Подробно работа с потоками рассматривается в главе 15.

Для ввода значений с клавиатуры нужно написать слово “cin”, за которым следует операция “получить значение” (>>). Хотя это два символа, компилятор обрабатывает их как один. За этим символом пишется имя переменной, которой присваивается значение. Например:  

cin >> a;

При выполнении этой инструкции программа будет ждать, когда пользователь введет значение и нажмет клавишу “Enter”. После этого введенное значение будет присвоено указанной переменной. При выполвыполнении этой инструкции никаких подсказок на экран выводиться не будет, поэтому программист сам должет позаботиться о выводе подсказки пользователю, о том, какие данные ожидает программа. В программе SIMPLE.CPP такая подсказка выводится в строке 9. Если требуется ввести значения сразу для нескольких переменных, то это можно сделать в одной инструкции, например:

cin >> year >> month >> day;

В данном случае пользователь должен ввести три значения. Разделителем значений является либо нажатие клавиши “Enter” после каждого значения, либо пробел между значениями и нажатие клавиши “Enter” после последнего значения. Например:

1999 “Enter”

12 “Enter”

8 “Enter”

или

1999  12  8“Enter”

В обоих случаях значение 1999 будет присвоено переменной year, значение 12 переменной month, и значение 8 переменной day. 

Для вывода значения на экран напишите слово “cout”, за которым следует операция “вывести значение” (<<). Данные, которые нужно вывести на экран, пишутся за этими символами. Например:

cout << “Result = ”<< x;

3.4. Комментарии

Для того чтобы документировать программу используются комментарии. Комментарии являются обычным текстом, который игнорируется (не обрабатывается) компилятором, но которые могут сообщить программисту, работающему с программой, о том, что и почему делается в конкретном месте программы. В языке C++ имеются два типа комментарий:

  1.  комментарии, начинающиеся с двойной наклонной черы “//”;
  2.  комментарии между символами “/*” и “*/”.

Комментарий, который начинается с двойной наклонной черты “//”,  называется комментарием в стиле языка C++. Он заканчиваются в конце строки, на которой появляется (строка 11 в примере 3.1). Комментарий, который начинается с символов “/*” продолжается до тех пор, пока не встретятся символы “*/”. Такие комментарии еще называются комментариями в стиле языка C. Каждая пара символов “/*” должна обязательно иметь соответствующую им пару символов “*/” (строки 1 и 2).

Многие программисты чаще используют комментарии в стиле C++ и оставляют комментарии в стиле C только для того, чтобы компилятор не обрабатывал большой блок программы. Можно включать комментарии в стиле C++ в  комментарии в стиле C. Но нельзя комментарии в стиле С включать друг в друга. В этом случае комментарий будет заканчиваться при первом обнаружении компилятором символов “*/”.

Хорошим стилем программирования является запись блока комментариев в начале каждого создаваемого файла с исходным текстом.

3.5. Использование имен в программе 

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

При составлении имен любых компонент программы применяются одни и те же правила:

  1.  в именах можно использовать только ниже перечисленные символы:

- большие и маленькие буквы латинского алфавита (от a-z и A-Z);

- цифры (от 0 до 9);

- символ подчерка (_) ;

  1.  имя должно начинаться с буквы;
  2.  имя не может включать пробел.

Имя может начинаться с символа подчерк (_), однако это делать не рекомендуется. Например, правильными именами являются  x, a123, и myAge. Имя может быть любой длины. Хорошо составленные имена должны сообщать программисту, для чего используется переменная или что делает функция, например, такие как myAge, years или PrintResult. Использование хороших имен облегчает понимание логики программы. Нужно стараться не использовать безсмысленные имена (например, a123), и применять однобуквенные имена (такие как x или i) только для переменных, которые используются на небольшом участке программы.

Язык C++ чувствителен к регистру. Другими словами, буквы верхнего регистра (заглавные, прописные) и буквы нижнего регистра (строчные, маленькие) рассматриваются как совершенно разные. И поэтому, например имя day отличается от имен Day и DAY.

Многие программисты предпочитают использовать маленькие буквы при записи имен. Если в имени желательно использовать несколько слов (например, my car), то существут два широко используемых способа составления таких имен:

  1.  использовать символ подчерк, для разделения слов (например, my_car);
  2.  начинать каждое следующее слово с большой буквы (например,  myCar).

Некоторые слова нельзя использовать в качестве имен компонент программы. Эти слова являются ключевыми словами языка и используются для записи порядка выполнения программы. Примерами таких слов являются if, while, for, или main. В описании языка содержится полный список ключевых слов.

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

3.6. Формат записи программы

В отличие от некоторых других языков программирования, программа на языке C++ имеет свободный формат записи. Это означает, что инструкции программы могут начинаться в любой позиции на любой строке. Между ключевыми словами и переменными должен быть хотя бы один пробел, или переход на новую строку. Пробелы и переходы на новые строки не могут стоять в именах и ключевых словах программы. В других местах их можно использовать, таким образом, чтобы программа лучше читалась.

Глава 4. Переменные и константы

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

4.1. Типы переменных

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

Каждая переменная имеет тип. Понятие “тип переменных” является очень важным для понимания работы программы. Тип переменной определяет:

  •  какие значения может хранить переменная;
  •  в каких операциях (например: арифметических, логических, и т.п.)  она может использоваться;
  •  сколько ячеек памяти требуется для ее хранения.

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

Существует четыре основных встроенных типов данных:

  •  целый тип, который хранит числа без дробной части, например: 45,-932, 0;
  •  вещественный тип, который хранит числа с дробной частью, например: 45.12, 234.542,  0.0,  .04594;
  •  символьный тип, который хранит буквы, символы цифр и различные другие символы, например: a, x, 5 (символ, а не цифра), +;
  •  логический тип, который может иметь два значения – истина или ложь.

Названия этих типов в языке С++ приведены в табл. 1.

Таблица 1

Названия типов данных

Типы данных

Название типа в языке С++

целый

int

вещественный

float (обычная точность)

double (двойная точность)

символьный

char

логический

bool

Целые переменные используются для хранения целых чисел (без дробной части).

Вещественные переменные (или их еще называют переменными с плавающей точкой) могут хранить значения с дробной частью. Как видно из таблицы они имеют два типа в зависимости от требуемой точности хранения значений - float (с точностью до 7 значащих цифр) и double (с точностью до 15 значащих цифр).

Символьные переменные используются для хранения целых чисел от 0 до 255. В связи с этим возникает вопрос: если в этих переменных хранятся числа, то, как в них сохраняются буквы и символы? Дело в том, что все буквы и символы закодированы, т.е. им в соответствие поставлены целые числа - коды, которые компьютер и хранит, вместо символов. Большинство компьютеров разных типов назначает символам коды в соответствии с Американским Стандартным Кодом Обмена Информацией (American Standard Code for Information Interchange – ASCII).

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

При объявлении переменной необходимо  указать компилятору ее имя и тип. Например:

char ch;       // создаем переменную символьного типа

int count;     // создаем переменную целого типа

double pi;     // создаем переменную вещественного типа

В одной инструкции можно объявить более чем одну переменную одного и того же типа. Например:

int year, myAge, myWeight; // целые переменные

double width, length;  // три переменных типа double

Здесь три переменные year, myAge и myWeight объявлены как целые, а переменные width и length объявлены как вещественные двойной точности. В одной инструкции объявления всем переменным задается один и тот же тип.

При объявлении переменные (за исключением глобальных и статических) не получают никакого начального значения (например 0). Содержимое переменных после их объявления считается неопределенным. Переменным можно задать значение, используя операцию присваивания (=). Эта операция присваивает то, что стоит с правой стороны от знака равно (=), тому, что стоит с левой стороны (подробно операция присваивания рассматривается в следующей главе). Например, можно присвоить значение переменной следующим образом:

int year;   //  объявляем переменную

year = 1999; //  присваиваем переменной значение

Эти два шага можно объединить и инициализировать значение переменной, когда она объявляется, например:

int year = 1999;

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

В соответствии со стандартом языка можно использовать и другой синтаксис инициализации переменной:

int year(1999); // другой способ инициализации

Если в одной инструкции объявляются несколько переменных, то они все могут инициализироваться. Например:

int width = 5, length = 7;  

или

int width(5), length(7);  

В этом примере переменная width инициализируется значением  5, а переменная length – значением 7.

4.2. Дополнительные описатели переменных целых типов

Дополнительные описатели уточняют значение некоторых базовых типов. В языке имеются четыре описателя: long, short, signed и unsigned.

Описатели long и short уточняют минимальное и максимальное значения, которые могут храниться в целом типе данных int. Этот описатель ставится перед названием типа. Тип данных short int занимает меньше памяти и может хранить только числа до 32767. Тип данных long int может хранить значения до 2147483647. На персональных компьютерах если ни один из этих описателей не задан, то тип int совпадает с типом long int. Пример использования описателей short и long показан ниже:

short int i = 1;

long int k = 0;  

Когда используется дополнительные описатели short или long, то название типа (int) можно не писать, как показано ниже:

short iis = 0; // тоже самое, что и short int

long count = 10; // тоже самое, что и long int

Для целого и символьного типов можно использовать описатели: signed и unsigned. Эти описатели уточняют, будут ли храниться отрицательные значения в переменных этих типов. Указание целого типа (short и long) с описателем "unsigned" (беззнаковый) означает, что значения переменной не будут иметь знак. Без этого описателя переменные хранят и отрицательные и положительные числа. При этом для хранения знака числа используется один бит. Поэтому если указать, что отрицательные числа не будут храниться в целой переменной, то она может хранить в два раза большее положительное значение. Например, безнаковая короткая целая переменная (тип unsigned short int) может хранить числа 0 до 65535. Для короткой переменной со знаком (signed short int) половина этих чисел будет отрицательной, и тогда переменная может хранить только значения от -32768 до 32767.

4.3. Размеры и адреса переменных

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

Пример 4.1 показывает, как можно определить точный размер памяти для различных типов данных.

Пример 4.1. Определение размера типов переменных.

1:#include <iostream>

2:using namespace std;

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 is:\t\t" <<  sizeof(short) << " bytes.\n";

7:  cout << "The size of a long 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"  << sizeof(double) << " bytes.\n";

11:

12:  return 0;

13:}

Результат работы программы:

The size of an int is:           4 bytes.

The size of a short is:      2 bytes.

The size of a long 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:         8 bytes.

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

Таблица 2

Размеры типов данных

Тип

Размер

Возможные значения

unsigned short

2 байта

от 0 до 65535

short int (или short)

2 байта

от –32768 до 32767

unsigned long

4 байта

от 0 до 4294967295

long int (или long)

4 байта

от –2147483648 до 2147483647

int

4 байта

от –2147483648 до 2147483647

unsigned int

2 байта

от 0 до 4294967295

char

1 байт

от 0 до 255

float

4 байта

0 и от 1.2e-38 до 3.4e38  

(7 значащих цифр)

double

8 байт

0 и от 2.2e-308 до 1.8e308

(15 значащих цифр)

Каждая переменная размещается в конкретном месте оперативной памяти и имеет адрес. Адресом переменной является номер первой ячейки (байта) выделенного для нее. Можно узнать этот адрес (номер байта) с помощью специальной операции получения адреса - &. Например, если x является объявленной переменной, то тогда &x является адресом этой переменной в памяти компьютера. Мы можем вывести этот адрес на печать:

cout << “x = “ << x;

cout << “address x = “ << &x;

или сохранить его в какой-то другой переменной (подробно об этом в главе 9).

4.4. Ключевое слово typedef

Многократная запись в программе длинных типов, например, такого, как unsigned short int, может быть утомительным занятием. В связи с этим язык C++ дает возможность создавать синонимы (другие имена) для таких длинных типов с помощью ключевого слова typedef (сокращение от type definition).

Новое имя типа можно создать, написав ключевого слова typedef, за которым стоит существующий тип, а затем синоним для этого типа. Например, инструкция

typedef unsigned short int USHORT;

создает новое имя USHORT, которое можно использовать везде, где надо было бы писать unsigned short int. Создание новых имен типов используется в примерах данного пособия (смотри пример 6.1).

4.5. Символьные переменные

Символьные переменные (тип char) занимают 1 байт, что достаточно, для хранения 256 различных значений. Значение переменной типа char можно рассматривать как маленькое число (от 0 до 255) или как значение из набора кодов ASCII. Например, в таблице ASCII строчной латинской букве ‘a‘ присвоено значение 97. Всем буквам английского алфавита нижнего и верхнего регистров, всем цифрам и всем знакам пунктуации присвоены числа от 0 до 127. Другие 128 значений (из 256 возможных) используются для хранения национальных алфавитов, например, кириллицы. В табл. 3 приведены примеры кодирования символов.

Таблица 3

Примеры ASCII кодов

Символы

Десятичное число

Двоичное число

*

42

0010 1010

A

65

0100 0001

B

66

0100 0010

a

97

0110 0001

b

98

0110 0010

1

49

0011 0001

2

50

0011 0010

4.5.1. Символы и числа

Когда присваивается символ, например 'a', символьной переменной, то в действительности присваивается целое значение из интервала между 0 и 255. Компилятор знает, как преобразовывать символы в один из кодов ASCII.

Связь между кодом и символом является произвольной; не существует специальной причины, по которой строчной букве латинского алфавита ‘a’ присвоено значение 97. Когда все программы и устройства компьютера (компилятор, клавиатура, экран и т.д.) используют одну и ту же систему кодирования символов, то проблемы не возникает.

Кроме этого важно понимать, что существует огромная разница между целой константой 2 и символом '2'. Символ '2' в действительности является значением 50 точно так же, как буква 'a' является значением 97.

4.5.2. Специальные символы печати

Язык C++ использует специальные символы для форматирования текста, выводимого на экран или в файл. Если в тексте встречается такой символ, то на печать он не выводится, а выполняются некоторое специальное действие с выводимым текстом. В табл. 4 показаны наиболее часто применяемые символы.

Таблица 4

Управляющие Escape символы

Символ

Что он значит

\n

переход на новую строку

\t

перемещение к следующей метке табуляции

\b

перемещение на один символ назад

\"

вывод двойных кавычек

\'

вывод одинарных кавычек

\a

выдача звукового сигнала

\\

вывод наклонной черты

Для того чтобы поместить эти символы в программу необходимо, напечатать обратную наклонную черту ‘\‘ (называемую escape символом), за которым поставить требуемый символ. Например:

char newLine = ’\n’;

В этом примере объявляется переменная (newLine) символьного типа (char) и инициализируется символом ‘\n’, который распознается как символ табуляции.

4.6. Константы

Как и переменные, константы являются местом для хранения данных. Но в отличие от переменных, константы обязательно должны инициализироваться при создании, и затем новые значения им присвоить уже нельзя. В C++ имеются два типа констант: литеральные и символические.

4.6.1. Литеральные константы

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

int myAge = 39;

myAge является переменной типа int, а 39 является литеральной константой. Константе нельзя присвоить другое значение. Существуют следующие основные типы литеральных констант:

  •  целые - например, 10 или 102367; целые константы не содержат точки и всегда занимают 4 байта памяти (тип long);
  •  вещественные - например, 10.2 или 0.102е+2 (при такой записи после буквы е стоит степень 10, и действительное число равно 0.102 * 100); вещественные константы занимают 8 байт (тип double); для того чтобы вещественная константа занимала 4 байта памяти, нужно после нее поставить букву F , например, 10.2F;
  •  символьные – например, 'a' или '#'; символьные  константы заключены в одинарные кавычки ('); нельзя написать 'ab', так как, символьная константа может содержать только один символ;
  •  строковые – это последовательность символов, заключеная в двойные кавычки ("), например, "text";
  •  логические – в языке имеются две логические константы true (истина) и false (ложь).

4.6.2. Символические константы

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

Например, если в программе имеются две целые переменные – students (количество студентов) и groups (количество групп), то можно вычислить, сколько всего учится студентов, зная количество групп и то, что в каждой группе по 25 студентов:

students = groups * 25;

Здесь символ * означает операцию умножения, а 25 является литеральной константой. Код программы будет легче читать и поддерживать, если использовать символическую константу вместо этого числового значения:

students = classes * studentsPerGroups;

Если позже потребуется изменить количество студентов в каждой группе, то можно сделать это в том месте, где объявлена константа studentsPerGroups, без необходимости делать изменения во всех местах, где это значение используется.

Существует два способа объявления символической константы в языке C++. Традиционный, а теперь устаревший способ, с помощью директивы препроцессора #define. И новый способ объявления констант, с помощью ключевого слова const.

Для того чтобы объявить константу традиционным способом нужно в начале текста программы записать следующую директиву препроцессора:

#define studentsPerGroups 25

Отметим, что studentsPerGroups не имеет какого либо типа (int, char, и т.п.). Эта директива выполняет простую подстановку текста. Каждый раз, когда препроцессор встретит слово “studentsPerGroups“, он заменит его текстом “25”. Так как препроцессор выполняется до начала работы компилятора, то компилятор никогда не увидит символической константы; он будет видеть только число 25.

Хотя директива #define работает, существует новый, более удобный способ объявления констант в языке C++ с помощью ключевого слова const:

const unsigned short studentsPerGroups = 25;

Этот пример также объявляет символическую константу, называемую studentsPerGroups, но теперь эта константа имеет тип - unsigned short. Этот способ имеет несколько достоинств в использовании: он делает код программы более легким для сопровождения и защищает от возможных ошибок при использовании констант. Основное достоинство этого способа в том, что константа имеет тип, и компилятор может проверять, что ее использование согласуется с ее типом.

4.6.3. Перечисляемые константы

Перечисляемые константы (перечисления) дают возможность создать новые типы и затем объявлять переменные этих типов, чьи значения ограничены набором возможных величин. Например, можно описать COLOR как перечисление и задать пять возможных его значений: RED, BLUE, GREEN, WHITE и BLACK. Объявление перечисляемых констант заключается в использовании ключевого слова enum. Синтаксис данного объявления можно понять из следующего примера:

enum COLOR { RED, BLUE, GREEN, WHITE, BLACK };

Эта инструкция выполняет две задачи:

  1.  она делает слово COLOR именем перечисления, то есть именем нового типа;
  2.  она делает слово RED символической константой со значением 0, BLUE-символической константой со значением 1, GREEN-символической константой со значением 2 и т.д.

Используя такие перечисляемые константы в программе можно объявлять переменные типа COLOR. Например:

COLOR clr;

Переменной этого типа можно будет присваивать только одно из возможных значений (в данном случае RED, BLUE, GREEN, WHITE, или BLACK). Например:

clr = GREEN;

4.7. Сбрасывание значений целых переменных

Целые переменные могут хранить только значения из заданного интервала. Возникает вопрос, что произойдет, если будет превышено  предельное значение? Когда переменная типа unsigned int достигает максимальное значение, то ее значение сбрасывается и начинается заново с нуля. Это схоже с тем как автомобильный счетчик пройденного пути сбрасывается на 0 при достижении 99999 километров.

Знаковая (signed) целая переменная отличается от беззнаковой целой переменной тем, что половина ее значений представляется в виде отрицательных чисел. Теперь вместо рассмотрения обычного автомобильного счетчика пройденного пути можно представить счетчик, который может показывать как положительные, так и отрицательные числа. Один километр от нуля может быть 1, или -1. Когда будет превышено наибольшее положительное количество километров, то счетчик будет сбрасываться к наименьшему отрицательному значению, и затем последовательно увеличиваться до нуля и выше.

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

short int smallNumber; // объявляем целую переменную

smallNumber = 32767; // задаем максимально число

smallNumber++;  // значение превышает допустимое

cout << "small number:" << smallNumber << endl

на экран будет выведено сообщение

small number:-32768 // минимально допустимое значение

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


Глава 5. Выражения и инструкции

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

5.1. Инструкции

Инструкции программы выполняются последовательно в том порядке, в котором они записаны, если нет специальных указаний о переходах. Одной из наиболее часто используемых инструкций является инструкция-выражение. Другой большой группой инструкций являются инструкции управления порядком выполнения программы. Запись всех инструкций обязательно заканчивается точкой с запятой.

В любом месте, где можно записать одну инструкцию, также можно записать составную инструкцию, которая называется блоком. Блок начинается с открывающей фигурной скобки ({) и заканчивается закрывающей фигурной скобкой (}). Хотя каждая инструкция в блоке должна заканчиваться точкой с запятой, сам блок не заканчивается точкой с запятой. Например:

{

    temp = a;

    a = b;

    b = temp;

}

Этот участок кода действует как одна инструкция и меняет местами значения в переменных a и b.

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

5.2. Выражения

В языке С++ все, что может вычисляться (оцениваться) и возвращает какой-то значение является выражением. Любая переменная и константа возвращает значение и, следовательно, является выражением. Чаще всего выражение представляет собой последовательность переменных и констант, объединенных знаками операций. Таким образом, 3+2 возвращает значение 5 и является выражением. Выражения, после которых стоит точка с запятой, являются инструкциями. Самыми простыми примерами инструкций является:

3.2; //возвращает значение 3.2

PI;   //веществ. константа, возвращает значение 3.14

Если предположить, что PI является константой равной 3.14, эти два выражения являются инструкциями.

Существует два типа выражений:

  •  арифметическое выражение (в результате вычисления получаем число; например: a + b);
  •  логическое выражение, в результате вычисления которого  получается логическое значение – true (истина) или false (ложь) (например: (c < 0)).

5.3. Операции

Операции это некоторые элементарные действия, которые можно выполнить над переменными и константами. Компилятор знает, как их выполнять над объектами встроенных типов. В языке С++ имеется большой набор операций (более 50). Примерами операций являются - сложение, умножение, присваивание, получение значение по индексу (о работе с массивами в главе 11) и т.п. Обычно операции задаются одним символом (например: +,*,=,[ ]). Однако есть операции, которые записываются в виде вызова специальных функций (например, sizeof()). Операции действуют над операндами, и все операнды должны быть выражениями. В данной главе будут рассмотрены три группы операций:

  •  операция присваивания;
  •  арифметические операции;
  •  операции отношения и логические операции.

5.3.1. Операция присваивания

Присваивание в языке С++ является не инструкцией, а операцией. Эта операция сохраняет то, что стоит с правой стороны от знака равно (=), в операнде, которая стоит с левой стороны. Например, выражение:

x = a + b;

присваивает значение, которое является результатом сложения a и b, переменной x.

Эта операция не только присваивает значение переменной, но также возвращает это значение в качестве результата. Таким образом, выше приведенная инструкция также является выражением. Так как это выражение, то оно может стоять с правой стороны другой операции присваивания:

y = x = a + b;

Операнд, который может стоять на левой стороне операции присваивания, называется l-value (левая величина). То, что может стоять с правой стороны называется (как можно догадаться) r-value (правая величина).

Константы могут быть правой величиной (r-value), но они не могут быть левой величиной (l-value). Т.е. можно записать:

x = 35;          // ok

но нельзя написать

35 = x;          // ошибка, не l-value!

5.3.2. Арифметические операции

В языке С++ используется пять арифметических операций:

Название операции

Символ

Пример

Результат

сложение

+

a + b

2 + 1 == 3

вычитание

-

ab

5 – 2 == 3

умножение

*

a * b

2 * 3 == 6

деление

/

a / b

8 / 4 == 2

деление по модулю

%

a % b

12 % 5 == 2

Операция деления по модулю (%) возвращает в качестве результата остаток от деления двух целых чисел. Например, в результате деления 23 на 5 по модулю  (23 % 5)  будем получать 3.

5.3.3. Объединение  операций 

Если имеется переменная myAge и необходимо увеличить ее значение на два, то это можно записать следующим образом:

myAge = myAge + 2;

Однако в языке C++ это же действие может быть записано проще:

myAge += 2;

Такая операция сложения (+=) добавляет r-value к l-value и затем записывает результат в l-value. Если myAge имеет начальное значение 4, то она будет иметь значение 6 после выполнения этой операции.

Также имеются объединенные операции вычитания (-=), деления (/=), умножения (*=), и деления по модулю (%=).  

5.3.4. Увеличение и уменьшение значения

Единица являет наиболее часто используемым значением для добавления (или вычитания) и затем присваивания заново полученного результата. В C++ увеличение значения на 1 называется инкрементом, и уменьшение на 1 называется декрементом. Существуют специальные операции для выполнения этих действий. Операция инкремента (++) увеличивает значение переменной на 1, и операция декремента (--) уменьшает его на 1. То есть, если имеется переменная a и нужно увеличить ее значение на единицу, то можно использовать следующую инструкцию:

a++;  // увеличить значение величины a на 1

Эта инструкция эквивалентна более длинной инструкции

a = a + 1;

или немного более короткой

a += 1;

5.3.5. Префикс и постфикс

Обе операции, инкремента (++) и декремента (--), могут использоваться в двух вариантах: префиксном и постфиксном. В префиксном варианте знак операции записывается перед именем переменной (++myAge); в постфиксном варианте знак операции записывается после имени переменной (myAge++).

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

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

Вначале это может показаться запутанным. Рассмотрим следующий пример: пусть x является целой переменной, значение которой равно 5, и  записана инструкция

int a = ++x;

В этом случае компилятор увеличит x на единицу (сделав его значение равным 6) и затем присвоит его a. Таким образом, значение a теперь равно 6 и значение x также равно 6. Однако если инструкция записана в виде

int b = x++;

то теперь компилятор вначале присвоит значение x (равное 6) переменной b, и только затем вернется назад и увеличит значение x. В этом случае b имеет значение 6, а переменная x теперь равна 7.

5.3.6. Приоритеты

В сложном выражении

x = 5 + 3 * 8;

операции могут выполняться в разной последовательности. Что здесь будет выполняться вначале - сложение или умножение? Если вначале выполнить сложение, то результатом будет 8 * 8, или 64. Если вначале выполняется умножение, то ответом будет 5 + 24, или 29. Для того чтобы избежать таких неопределенностей в вычислении выражений, все операция в языке С++ имеют приоритет. Если в выражении встречается несколько различных операций, то вначале будут выполняться операции с наименьшим значением приоритета, а уже затем - с большим значением приоритетом. Приоритеты наиболее часто используемых операций приведены в табл. 5.

Таблица 5

Приоритеты операций

Приоритет

Название операции

Обозначение

1

операция уточнения области видимости

::

2

выбор члена, выбор члена по адресу, доступ по индексу,

. -> []

2

вызов функции,

()

2

постфиксный инкремент и декремент

++ --

3

префиксный инкремент и декремент,

++ --

3

отрицание, унарный минус и плюс,

! - +

3

получение адреса и разыменование,

& *

3

new, delete, преобразование типа, размер объекта-sizeof(),

5

умножение, деление, деление по модулю

* / %

6

сложение, вычитание

+ -

8

операции отношения

< <= > >=

9

равенство, неравенство

== !=

13

логическое И (AND)

&&

14

логическое ИЛИ (OR)

||

15

условное выражение

?:

16

операции присваивания

= *= /= %=

+= -=

Чем меньше значение приоритета, тем раньше выполняется операция. В приведенном выше выражении операция умножения имеет значение приоритета меньше, чем сложение, и поэтому выполниться раньше. Таким образом, результатом выражения будет 29.

Когда две арифметические операции имеют одинаковый приоритет, то они будут выполняться в таком порядке - слева направо. Но операция присваивания, выполняются в обратном порядке - справа налево!

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

x = (5 + 3) * 8;

вначале вычисляется значение в скобках (5 + 3), а затем выполняется произведение (8 * 8). В этом случае результат будет равен 64.

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

5.4. Преобразование встроенных типов данных 

Язык С++ является строго типизированным языком. Это значит, что все объекты программы имеют тип, и компилятор строго следит за тем, чтобы в операциях использовались только допустимые типы данных. Компилятор разрешает применять различные типы данных в выражении только в тех случаях, когда он знает, как преобразовать один тип данных в другой. Операция преобразования типов данных называется операцией приведения (cast) .

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

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

(простой) char->short->int->long->float->double (сложный).

Здесь самый простой тип - char и самый сложный - double. Например, если в выражении участвуют переменные типа short, float и long, то результат будет иметь тип float.

Поясним наиболее сложные из преобразований встроенных типов:

  1.  при преобразовании вещественного типа к целому, дробная часть отбрасывается (не округляется), например:

 int i = 1.75; // i получит значение 1 (а не 2)

  1.  при преобразовании целого типа к вещественному, добавляется нулевая дробная часть, например:

 float f = 2;  // f получает значение 2.0

  1.  при преобразовании типа double в тип float может произойти потеря точности (о чем компилятор будет сообщать), например:

double d = 123.456789567;  // 12 значащих цифр

float  f = d; // получит значение 123.4568 (7

  //  значащих цифр)

  1.  при пребразовании целого типа к логическому результат будет true, если значение не равно нулю и false, если значение равно нулю, например:

 bool b = 2; //  b получит значение true

 bool c = 0; //  c получит значение false

  1.  при преобразовании логического типа к целому результат будет следующий:

 int i = true;   // i получает значение 1

 int j = false;  // j получает значение 0

Особо следует обратить внимание на то, что результат выполнения операции будет иметь тот же тип, что и переменные и константы, которые в ней участвовали. Например, при выполнении выражения

float f = 1/2;

результатом будет не 0.5, а 0. Константы 1 и 2 имеют тип long (длинные целые), и, следовательно, компилятор назначит результату тоже тип long, но этот тип не хранит дробную часть числа, а только целую, и поэтому в результате деления получаем 0. При этом безразлично, какой переменной будет присваиваться результат - целой или вещественной. Результат вначале вычисляется, а только потом присваивается.

Преобразование типов (casting) можно записать явно следующими тремя способами:

(новый_тип) выражение

или

 новый_тип(выражение)

или

 static_cast<новый_тип>(выражение)

Здесь “новый_тип” означает любой правильный в языке C++ тип данных (включая и все дополнительные описатели), а “выражение” может быть переменной, константой или каким-то выражение. Например, следующая инструкция преобразует переменную целого типа age в вещественную переменную типа  double:

int age = 25;

double b = (double) age;

Второй способ записи  преобразования показан ниже:

b = double(age);

И третий способ использует ключевое слово static_cast и треугольные скобки вокруг типа и круглые скобки вокруг переменной:

b = static_cast<double>(age);

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

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

5.5. Операции отношения

Операции отношения используются для того, чтобы определить, являются ли два числа равными, или одно из них больше или меньше  другого. Каждая операция отношения в результате вычисления возвращает или true(истина) или false(ложь). Операции отношения представлены ниже в таблице 5.

Если целая переменная myAge имеет значение 39, а целая переменная yourAge имеет значение 40, то можно определить, являются ли они равными, используя операцию отношения "равно":

myAge == yourAge;//равны ли значения myAge и yourAge?

Это выражение в качестве результата вернет константу false, так как эти переменные не равны. Результатом выражения:

myAge < yourAge;  // больше ли myAge чем yourAge?

будет значение true.

Существует шесть операций отношения. Они показаны в табл. 6 и тут же приведены примеры их использования.

Таблица 6

Операции отношения

Название

Операция

Пример

Результат

равно

==

100 == 50;

false

50 == 50;

true

не равно

!=

100 != 50;

true

50 != 50;

false

больше чем

>

100 > 50;

true

50 > 50;

false

больше чем

или равно

>=

100 >= 50;

true

50 >= 50;

true

меньше чем

<

100 < 50;

false

50 < 50;

false

меньше чем

или равно

<=

100 <= 50;

false

50 <= 50;

true

Предупреждение: начинающие программировать на C++ часто путают операцию присваивания (=), с операцией равенства (==). Это может привести к серьезной ошибке в программе. Помните, что операции отношения возвращают значение true или false.

5.6. Инструкция if

Обычно, инструкции в программе выполняются последовательно,  в том порядке, как они записаны. Инструкции if дает возможность проверить условие (например, равны ли две переменные) и сделать разветвление в выполнении программы в зависимости от результата.

Простейший тип инструкции if следующий:

if(выражение)  // после скобок точка с запятой не ставится

  инструкция;

Выражение в скобках может быть любым, но обычно оно является одним из выражений отношения. Если выражение имеет значение false (или 0), то инструкция, которая стоит после скобок пропускается (не выполняется). Если выражение имеет значение true (или не нулевое значение), то инструкция выполняется. Рассмотрим следующий пример:

if (bigNumber > smallNumber)

    bigNumber = smallNumber;

Этот код сравнивает значения переменных bigNumber и smallNumber. Если переменная bigNumber больше, то во второй строке ее значение меняется на значение smallNumber.

Так как блок инструкций, выделенный фигурными скобками, соответствует одной инструкции, то можно записать:

if (выражение)

{

инструкция1;

инструкции2;

}

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

if (bigNumber > smallNumber)

{

    bigNumber = smallNumber;

    cout << "bigNumber: " << bigNumber << "\n";

    cout << "smallNumber: "<<smallNumber<< "\n";

}

Часто в программе требуется выполнить одну ветвь кода, если условие истинно, и другую, если оно ложно. В этом случае можно использовать ключевое слово else:

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;

5.7. Логические операции

Часто возникает необходимость в одной записи проверить более чем одно отношение между значениями переменных. Например, "Правда ли, что x больше чем y, а y больше чем z?". В языке C++ используются три логических операции, для того чтобы выполнить такого типа проверку. Эти операции приведены в табл. 7.

Таблица 7

Логические операции

Операция

Символ

Пример

Логическое И (AND)

&&

выражение1 && выражение2

Логическое ИЛИ (OR)

||

выражение1 || выражение2

Логическое НЕТ (NOT)

!

!выражение

Операция “логическое И” оценивает два выражения, и если оба выражения истинны, то и результат этой операции является истинным. Таким образом, выражение ((x == 5) && (y == 5)) будет оцениваться как истина, если как x, так и y равны 5, и он будет оцениваться как ложь, если одно из значений не равно 5. Отметим также, что данная операция записывается двумя символами &&.

Операция “логическое ИЛИ” оценивает два выражения. Если хотя бы одно из них истинно, то результатом этой операции будет истина. Таким образом, выражение: ((x == 5) || (y == 5)) будет оцениваться как истина - true, если или x или y равны 5, или оба равны 5. Отметим, что эта операция записывается как два символа ||.

Результатом операции “логическое НЕТ” является истина - true, если проверяемое выражение является ложным - false. И наоборот, если проверяемое выражение является истинным, то результатом операции будет ложь! Тогда выражение (!(x == 5))является истинным только в том случае, если x не равен 5. Это то же самое, что и следующая запись: (x != 5)

Операции отношения и логические операции, также как и все операции,  имеют приоритеты (смотри табл. 5). Приоритеты определяют, какие операции вычисляются раньше, а какие позже. Это важно при определении значения следующего выражения ( x > 5 &&  y > 5  || z > 5).

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

(((x > 5) &&  (y > 5))  || (z > 5))

5.8. Условная операция 

Условная операция (?:) в языке C++ является единственной операцией, которая использует три операнда:

(выражение1) ? (выражение2) : (выражение3)

Первое выражение является логическим, которое может быть либо истинным, либо ложным. Условная операция выполняется следующим образом: "Если выражение1 является истинным, то в качестве результата операции вернуть значение выражения2; в противном случае, вернуть значение выражения3".  Обычно результат этой операции присваивается какой-то переменной. Например, в следующем выражении вычисляется  наибольшее из значений двух переменных x и y:

max =  (x > y) ? x : y; 

Глава 6. Функции

Хотя объектно-ориентированное программирование переключило внимание разработчиков от функций к классам, функции, тем не менее, остаются основными компонентами любой программы.

6.1. Что такое функция?

Функция является составным блоком, который:

  •  имеет имя, с помощью которого можно передать ему управление;
  •  имеет набор локальных переменных, которые можно инициализировать при вызове;
  •  может возвращать результат.

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

Каждая функция имеет свое имя, и когда это имя встречается в программе, выполнение программы переходит к инструкциям, записанным в теле этой функции. Когда функция возвращает управление, выполнение передается инструкции, которая стоит после вызова функции. Этот поток выполнения показан на рис. 2.

Рис. 2. Иллюстрация потока выполнения

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

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

6.2. Объявление и определение функций

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

  •  записать прототип в файле, в котором она и используется.
  •  записать ее прототип в заголовочный файл, и затем с помощью директивы #include включить его в свою программу;

Все стандартные функции уже имеют  запись их прототипов в специальных заголовочных файлах, которые вставляются в программу с помощью директивы препроцессора #include. Для функций, которые составляет программист, необходимо самим писать их прототипы.

Прототип или объявление функции имеет следующий синтаксис:

тип_результата имя_функции([тип [имя_параметра]]...);

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

long FindArea(long length, long width);

void PrintMessage(int messageNumber);

int GetChoice();

BadFunction(); // возвращает int, не имеет параметров

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

Определения функции имеет следующий синтаксис:

тип_результата имя_функции ([тип имя параметра]...)

{

  инструкции;

}

Определение функции должно согласовываться по типу результата и списку параметров с ее прототипом. Оно должно содержать имена для всех параметров, и тело функции должно всегда выделяться фигурными скобками. Все инструкции в теле функции должны заканчиваться точкой с запятой, но сама функция не заканчивается точкой с запятой; она завершается закрывающей фигурной скобкой. Если функция возвращает значение, она должна заканчиваться специальной инструкцией return, хотя инструкция return может записываться в любом месте тела функции.

Каждая функция имеет тип результата. Если он явно не указан, типом результата будет int. Рекомендуется явно задавать для каждой функции тип результата. Если функция не возвращает значение, то в качестве типа ее результата указывается void. Например, функция может быть определена следующим образом:

void PrintMessage(int whichMsg)

{

    if (whichMsg == 0)

         cout << "Hello.\n";

    if (whichMsg == 1)

         cout << "Goodbye.\n";

    if (whichMsg > 1)

         cout << "I'm confused.\n";

}

Рассмотрим пример 6.1, в котором записана программа с прототипом и определением функции FindArea().

Пример 6.1. Использование прототипа функции.

1:typedef unsigned short USHORT;

2:#include <iostream>

3:using namespace std;

4://прототип функции

5:USHORT FindArea(USHORT length, USHORT width);

6:int main()

7:{

8:  USHORT lengthOfYard;

9:  USHORT widthOfYard;

10:  USHORT areaOfYard;

11:

12:  cout << "How wide is your yard? ";

13:  cin >> widthOfYard;

14:  cout << "How long is your yard? ";

15:  cin >> lengthOfYard;

16:

17:  areaOfYard= FindArea(lengthOfYard,widthOfYard);

18:

19:  cout << "Your yard is ";

20:  cout << areaOfYard;

21:  cout << " square feet\n";

22:  return 0;

23:}

24:

25:USHORT FindArea(USHORT l, USHORT w)

26:{

27:  return l * w;

28:}

Результат работы программы:

How wide is your yard? 30

How long is your yard? 40

Your yard is 12000 square feet

Прототип функции FindArea() записан в строке 5. Сравните прототип с определением функции в строке 25. Отметьте, что имя, тип результата и типы параметров те же самые. Если они будут отличаться, будет выдано сообщение компилятора. Фактически, единственным различием является то, что прототип заканчивается точкой с запятой и не имеет тела. Также отметим, что имена параметров в прототипе length и width, а в определении функции - l и w. Имена в прототипе не используются; они ставятся в прототип только как информация для программиста. Они могут быть такими же, как и в определении функции. Это не требуется, а только является хорошим стилем программирования и уменьшает путаницу.

Аргументы передаются в функцию в том порядке, в каком они объявлены и определены, но, при этом, не поддерживается никакого соответствия имен. Если передать widthOfYard, а затем lengthOfYard, функция FindArea() будет использовать значения переменной widthOfYard (ширина двора) для length(длина) и lengthOfYard (длина двора) для width(ширина). Тело функции всегда заключается в фигурные скобки, даже когда оно состоит только из одной инструкции, как в данном случае.

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

6.3. Локальные и глобальные переменные

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

Новый Термин: Переменная имеет область видимости, которая определяет как долго она доступна в программе и где к ней имеется доступ. Для переменных объявленных в блоке, областью видимости является этот блок (вернее часть блока стоящая после их объявления); к ним имеется доступ только в пределах блока и они "перестают существовать", когда блок заканчивается.

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

Локальные переменные с тем же именем, как и глобальные, не конфликтуют с глобальными переменными. Однако эти переменные скрывают глобальные переменные. Если функция имеет переменную с тем же именем, как и глобальная переменная, то это имя теперь ссылается на локальную переменную, а не на глобальную, когда используется в функции.

Глобальные переменные являются допустимыми, но их рекомендуется использовать как можно реже. Язык C++ вырос из языка C, а в языке C глобальные переменные являются опасным, но необходимым инструментом. Они необходимы, так как иногда программисту нужно сделать данные доступными во многих функциях, но он не хочет передавать эти данные в качестве параметров из функции в функцию.

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

6.4. Подробнее о локальных переменных

Говорят, что переменные, объявленные в функции, имеют "локальную область видимости". Это означает, что они видимы и могут использоваться только в пределах той функции, в которой они объявлены.

Фактически, в языке C++ можно описать переменные в любом месте функции, а не только в ее начале. Областью видимости переменной является блок, в котором она объявлена. Таким образом, если переменная объявляется внутри фигурных скобок в функции, то она доступна только в пределах этого блока. Пример 6.2 показывает это.

Пример 6.2 Использование переменных с областью видимости в пределах блока.

1:#include <iostream>

2:using namespace std;

3:void myFunc();

4:int main()

5:{

6:  int x = 5;

7:  cout << "\nIn main x is: " << x;

8:  myFunc();

9:  cout << "\nBack in main, x is: " << x;

10:  return 0;

11:}

12:void myFunc()

13:{

14:  int x = 8;

15:  cout<<"\nIn myFunc, local x: " << x << endl;

16:  {

17:    cout<<"\nIn block in myFunc, x is: " << x;

18:    int x = 9;

19:    cout << "\nVery local x: " << x;

20:  }

21:  cout<<"\nOut of block, in myFunc,x: "<<x<< endl;

22:}

Результат работы программы:

In main x is: 5

In myFunc, local x: 8

In block in myFunc, x is: 8

Very local x: 9

Out of block, in myFunc, x: 8

Back in main, x is: 5

Эта программа начинается с инициализации локальной переменной x в строке 6 функции main(). Вывод на экран в строке 7 подтверждает, что x была инициализирована значением 5. Далее вызывается функция myFunc(), и в строке 14 локальная переменная с таким же именем x объявляется и инициализируется значением 8. Ее значение выводится в строке 15. Со строки 16 начинается блок, и переменная x, объявленная в функции, печатается в строке 17. А затем объявляется новая переменная с именем x, но локальная для этого блока, она создается в строке 18 и инициализируется значением 9.

Значение последней созданной переменной x печатается в строке 19. Локальный блок заканчивается в строке 20, и переменная, созданная в строке 18, выходит из "области видимости" и далее не видима. Когда x печатается в строке 21, то это та переменная x, которая была объявлена в строке 14, ее значение до сих пор равно 8.

В строке 22 функция myFunc() завершается, и ее локальная переменная x становится недоступной. Выполнение возвращается к строке 9, и значение локальной переменной x, которая была создана в строке 6, выводится на экран. Оно не было изменено переменными с таким же именем в функции myFunc().

Не нужно говорить, что эта программа была бы значительно менее запутанной, если бы этим трем переменным были даны различные имена!

Каждый раз, когда функция начинает выполняться, локальные переменные создаются заново. Однако можно создать переменные, которые будут создаваться в функции только один раз. Такие переменные называются статическими и они при завершении функции не исчезают. Для объявления статической переменной нужно использовать специальное ключевое слово static перед объявлением переменной. Например:

static int param;

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

int func()

{

 static int count = 0;// создается только один раз

 count++;  // увеличивается при каждом вызове функции

 cout << “calls = ” << count;

 . . .  //  другие инструкции

 return 0;

}

6.5. Аргументы функций

Значения, которые передаются в функцию при ее вызове, называются фактическими параметрами или аргументами функции. Любое правильное выражение языка C++ может быть  аргументом функции, включая константы, арифметические и логические выражения и вызов других функций, которые возвращают значение. Например:

x = myFunc(1.5, x, a+b, sin(x));

Если говорить точнее, то аргументы вычисляются и полученные значения используются для инициализации параметров функции. Раз аргументы используются для инициализации параметров, то получается, что они в функцию не передаются, а параметры являются их копиями. Такой способ передачи переменных в функцию называется передачей по значению. А это значит, что изменения значений этих переменных, сделанные в функции, не оказывают влияния на значения переменных в вызывающей функции. Локальные копии передаваемых значений используются точно также как и любые другие локальные переменные. Это показано в примере 6.3.

Пример 6.3. Демонстрация передачи параметров по значению.

1:#include <iostream>

2:using namespace std;

3:void swap(int x, int y);

4:

5:int main()

6:{

7:  int x = 5, y = 10;

8:

9:  cout << "Main. Before swap, x: "<<x<<" y: " <<y<<"\n";

10:  swap(x,y);

11:  cout<<"Main. After swap, x: "<<x<<" y: "<<y<<"\n";

12:  return 0;

13:}

14:

15:void swap (int x, int y)

16:{

17:  int temp;

18:

19:  cout<<"Swap. Before swap, x: "<<x<<" y: "

20:   <<y<<"\n";

21:  temp = x;

22:  x = y;

23:  y = temp;

24:

25:  cout<<"Swap. After swap, x: "<<x<<" y: "<<y<<"\n";

26:

27:}

Результат работы программы:

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(), то оказывается, что их значения не изменились! Переменные инициализируются в строке 7, и их значения показываются в строке 9. Функция swap() вызывается, и ей передаются объявленные переменные. Выполнение программы переходит в функцию swap(), где в строке 19 значения печатаются опять. Переменные имеют те же значения, какие они имели в функции main(), что и ожидалось. В строках с 21 по 23 значения переменных меняются местами, т.е. переменная x получает значение переменной y, а переменная y получает значение переменной x, и этот обмен подтверждается выводом на экран в строке 25. Действительно, в функции swap(), значения переменных поменялись местами. Затем выполнение возвращается в строку 11 функции main(), где оказывается, что значения переменных, которые передавались в функцию, не изменились.

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

В главах 9 и 10 будет рассмотрен другой способ передачи параметров не по значению, который позволит изменять значения переменных в функции main().

Функции возвращают или значение, или void. Ключевое слово void сообщает компилятору, что никакое значение возвращаться в вызывающую программу не будет.

Для того чтобы вернуть значение из функции используется инструкция return, за которой ставится возвращаемое значение. Этим значением может быть какое-нибудь выражение, которое дает результат. Например:

return 5;

return (x > 5);

return (MyFunction());

Это все правильные инструкции return, если предположить, что функция MyFunction() также возвращает некоторое значение. Значение во второй инструкции, return (x > 5), будет равно false, если x не больше чем 5, или равно true, если x больше чем 5.

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

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

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

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

long myFunction (int x = 50);

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

long myFunction (int = 50);

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

long myFunction (int x)

Если при вызове функции не будет указан параметр, то компилятор будет присваивать x значение по умолчанию, равное 50. Имя параметра со значением по умолчанию в прототипе необязательно должно быть тем же самым, как и имя в заголовке функции; значение по умолчанию присваивается позиции, а не имени.

Любому или всем параметрам функции может быть присвоено значение по умолчанию. Имеется только следующее ограничение: если какой-либо из параметров не имеет значения по умолчанию, то ни один из предшествующих ему параметров уже не может иметь значения по умолчанию. Если прототип функции выглядит следующим образом:

long myFunction (int Param1, int Param2, int Param3);

то можно присвоить значение по умолчанию параметру Param2, только если уже присвоено значение по умолчанию параметру Param3. Также можно присвоить значение по умолчанию параметру Param1, только если уже присвоены значения по умолчанию параметрам Param2 и Param3.

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

 

6.6. Перегрузка функций

Язык C++ дает возможность создавать более чем одну функцию с тем же самым именем. Это называется перегрузкой функции. Функции должны отличаться списком их параметров: или по типу параметров, или по количеству параметров или тем и другим. Ниже приведен пример:

int myFunction (int, int);

int myFunction (long, long);

int myFunction (long);

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

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

Новый Термин: перегрузка функции (overloading) также называется полиморфизмом функции (polymorphism). “Poly” означает "много", и “morph” означает “форма”, т.е. полиморфная функция - у которой имеется много форм.

Изменяя количество или тип параметров, можно дать двум или более функциям то же самое имя, и при обращении к этой функции будет вызываться та функция, у которой набор параметров лучше соответствует передаваемым значениям. Предположим, что нужно написать функцию, которая удваивает передаваемое ей значение. Требуется иметь возможность передавать переменные следующих типов – int и float. Если бы в языке не было возможности перегрузки функции (как в языке С), то потребовалось бы создать две функции с разными именами:

int DoubleInt(int);

float DoubleFloat(float);

Так как такая возможность имеется, то делаются следующие объявления:

int Double(int);

float Double(float);

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

Пример 6.4. Демонстрация перегрузки функций.

1:#include <iostream>

2:using namespace std;

3:int Double(int);

4:float Double(float);

5:int main()

6:{

7:  int      myInt = 6500;

8:  float    myFloat = 6.5F;

9:  int      doubledInt;

10:  float    doubledFloat;

11:  cout << "myInt: " << myInt << "\n";

12:  cout << "myFloat: " << myFloat << "\n";

13:  doubledInt = Double(myInt);

14:  doubledFloat = Double(myFloat);

15:  cout << "doubledInt: " << doubledInt << "\n";

16:  cout << "doubledFloat: " << doubledFloat<<"\n";

17:  return 0;

18:}

19:int Double(int original)

20:{

21:  cout << "In Double(int)\n";

22:  return 2 * original;

23:}

24:float Double(float original)

25:{

26:  cout << "In Double(float)\n";

27:  return 2 * original;

28:}

Результат работы программы:

myInt: 6500

myFloat: 6.5

In Double(int)

In Double(float)

doubledInt: 13000

doubledFloat: 13

Функция Double()является перегружаемой функцией, и она имеет два варианта для параметров разных типов: int и float. Прототипы функций записаны в строках 3-4, а определения этих функций приведены в строках 19-28.

В теле основной программы, объявлены четыре локальных переменных. В строках 7-8 две переменные инициализируются, и в строках 13-14 другим двум переменным присваиваются результаты вызова функции Double()с первыми двумя переменными в качестве параметров. Отметим, что когда функция Double() вызывается, то вызывающая функция main() не указывает, какую конкретно функцию вызывать; она просто передает аргументы, и вызывается правильная функция.

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

6.7. Встраиваемые функции

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

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

Если функция объявляется с ключевым словом inline, то компилятор не создает действительной функции: он копирует код из inline функции в вызывающую функцию.  Например:

inline float Cube (float a)

{

 return a*a*a;

}

Переходы к такой функции не делаются; это то же самое, как если бы записать инструкции составляющие функцию (в данном случае выражение a*a*a) непосредственно в вызывающую функцию.

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

6.8. Стандартная библиотека

В состав системы программирования на языке С++ входит стандартная библиотека. Она включает в себя набор уже разработанных функций и классов, которые программист может использовать в своей программе. Объявления всех этих стандартных компонент сделано в специальных заголовочных файлах. В заголовочных файлах содержатся только определения констант, типов и объявления функций, в них нет самих программ. Все программы стандартной библиотеки разделены на группы, и каждая группа объявлена в отдельном заголовочном файле.  В табл. 8 приведены примеры некоторых стандартных заголовочных файлов.

        Таблица 8

Примеры заголовочных файлов стандартной библиотеки

Название файла

Содержание

<iostream>

стандартные потоки  и операции с потоками ввода/вывода

<cmath>

общие математические функции (sin, cos, log, exp, abs и т.п.)

<cstdlib>

функции поиска, сортировки и работы c 

символами

<fstream>

потоки ввода/вывода в файлы

<cstdio>

семейство функций ввода/вывода printf

<vector>

одномерный массив элементов

<string>

класс для работы со строками текста

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


Глава 7. Введение в классы

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

7.1. Создание новых типов

Ранее уже рассматривались различные встроенные в язык типы переменных, такие как целые, вещественные и символьные. Тип переменной достаточно полно определяет свойства и поведение переменной. Говоря более обобщенно, тип является конкретной реализацией некоторого понятия. Например, встроенный тип float вместе с операциями +,-,*, и т.д. является реализацией математической понятия вещественного числа.

В обычной жизни для человека привычными являются другие понятия, например такие, как автомобиль, банковский счет или параграф текста. Для их представления язык C++ дает программисту средства создания любых нужных ему типов, которые настолько же удобны в использовании, как и встроенные.

Новый термин: Класс – это определяемый пользователем тип данных.

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

7.1.1. Классы и члены классов

Новый тип создается с помощью определения класса. Класс является объединением переменных и связанных с ними функций.

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

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

Класс может состоять из любой комбинации переменных различных типов, а также объектов других классов. Переменные, включенные в класс, называются членами-переменными или членами-данными класса. Например, класс Car может иметь члены-данные, описывающие двигатель, тип радиоприемника, шины и т.п.

Новый Термин: члены- переменные, или иначе члены-данные, или свойства класса, это переменные объявленные в классе. Члены-переменные являются частью класса, точно также как колеса и двигатель являются частью автомобиля.

 

Функции класса работают с членами-данными класса. В этих функциях имеется прямой доступ ко всем членам-данным класса. Функции класса называются членами-функциями или методами класса. Класс автомобиль Car может включать такие методы, как Start()(начать движение) и Brake()(тормозить). Класс, описывающий собак Dog, может иметь такие члены-данные, как возраст и вес; а его методами могут быть: спать Sleep(), лаять Bark(), и приносить палку BringStick().

Новый Термин: члены-функции или, иначе, методы это функции, которые входят в состав класса. Члены-функции являются такой же важной частью класса, как и члены-переменные. Они определяют, что может делать объект класса.

7.1.2. Определение класса

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

Пример 1:

class Dog  // начало определения класса

{

public:

 unsigned int Age;    // возраст собаки

 unsigned int Weight;  // вес собаки

 void Bark();

};      // конец определения класса

Пример 2:

class Car // начало определения класса

{

public: // следующие элементы имеют открытый доступ

 void Start();

 void Accelerate();

 void Brake();

 void SetYear(int year);

 int  GetYear();

private:      // к остальным доступ закрыт

 int Year;

 char Model [255];

};    // конец определения класса

Определение класса не производит выделение памяти для объекта Dog. Оно просто поясняет компилятору, что собой представляет класс Dog, какие данные он содержит (Age и Weight) и что он может делать (Bark()). Он также сообщает компилятору размер объекта класса Dog, т.е. сколько места в памяти компилятор должен выделять для каждого объекта класса Dog при его создании. Размер объектов класса будет равен суммарному размеру всех членов-данных, которые в него входят. В данном примере, если тип int занимает четыре байта, то каждый объект класса Dog будет занимать только восемь байт: Age занимает первые четыре байта и Weight занимает вторые четыре байта. Функции класса (если только они не виртуальные, подробнее смотри раздел 14.8), не занимают места в объектах класса .

7.2. Объявление объектов класса 

Объекты нового типа объявляются точно так же, как переменные встроенных типов:

short length; // объявляем переменную типа short

Car OldAvto;  // объявляем объект класса Car

Dog Rex;    // объявляем объект класса Dog

Здесь объявляется переменная с именем length, тип которой - короткая целая (short). А также объявляется переменная Rex, которая является объектом класса (или типа) Dog и переменная OldAvto, которая является объектом класса Car. 

В языке C++ делается различие между классом Dog, который является определением понятия собаки, и каждым конкретным объектом класса Dog. Переменная Rex является объектом класса Dog точно так же, как length является переменной типа short.

Новый Термин: объект является конкретным экземпляром класса.

Каждый объект класса Dog содержит две переменные - Age и Weight. Для того чтобы обратиться к этим переменным нужно использовать операцию выбора члена класса (.), которая ставится после имени объекта. Следовательно, для того чтобы присвоить значение 15 члену-переменной объекта, нужно написать:

Rex.Weight = 15;

Точно так же, для того чтобы вызвать член-функцию объекта Bark(), нужно написать: Rex.Bark();

Используйте ключевое слово class для того, чтобы определить класс. Не путайте определение класса с объявлением объектов класса. Определение класса говорит о том, что собой представляет класс, а объявление объектов выделяет для них память. Не путайте класс с объектом. Используйте операцию выбор члена (.) для доступа к членам-данным и методам объекта.

7.3. Открытые и закрытые части класса

При определении класса используются специальные ключевые слова, которые задают режим доступа к членам класса. Например: public и private. На русский язык они переводятся следующим образом: public - открытый, а private закрытый. Под режимом доступа к членам класса понимается возможность использовать эти данные и функции в разных частях программы.

По умолчанию (если ничего не говорится о режиме доступа) все члены класса получают режим доступа private.

Замечание: для определения класса кроме ключевого слова class можно использовать ключевое слово struct. В этом случае все члены класса по умолчанию получают режим доступа public .

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

Рассмотрим ранее приведенный пример класса:

class Dog

{

   unsigned int  Age;

   unsigned int  Weight;

   Bark();

};

В этом объявлении Age, Weight и Bark() имеют тип доступа private, так как если не указано обратное,  то все члены класса имеют тип доступа private по умолчанию. Теперь, если написать:

Dog  Agata;

Agata.Age=5; // ошибка! это закрытые данные!

то компилятор будет помечать эту запись как ошибочную. Сам объект Agata может использовать эту переменную в своих функциях, а функция, которая использует этот объект не имеет доступа к ней. Объект Agata в его собственных методах может использовать любые части класса, public и private. Даже то, что функция создает объект класса Dog, не означает, что она можете видеть или менять те его части, доступ к которым определен как private.

Для того чтобы можно было использовать какие-то члены класса Dog нужно записать, что эти члены являются открытыми (public) для всех частей программы, следующим образом:

class Dog

{

public:

 unsigned int Age;

 unsigned int Weight;

 Bark();

};

Теперь данные Age, Weight и функция Bark() являются открытыми (public). В этом случае инструкция Agata.Age=5;  будет компилироваться без всяких проблем. В примере 7.1 показывается объявление класса Dog с открытыми членами-данными.

Пример 7.1. Определение класса и объявление его объекта.                    

1:#include <iostream> // для работы с cout

2:using namespace std;

3:class Dog           // определяется класс объектов

4:{

5:public:  //члены класса, стоящие далее открытые

6:  unsigned int Age;

7:  unsigned int Weight;

8:};

9:

10:void main()

11:{

12:  Dog Rex;

13:  Rex.Age = 5; //задаем значение переменной класса

14:  cout << " Rex is a Dog who is " ;

15:  cout << Rex.Age << " years old.\n";

16:}

Результат работы программы:

Rex is a Dog who is 5 years old.

Строка 3 содержит ключевое слово class. Оно сообщает компилятору, что далее записывается определение класса. Имя класса стоит после ключевого слова class. В данном случае имя класса - Dog. В строке 5 записано ключевое слово public, которое указывает компилятору, что все члены класса, которые стоят после него, являются открытыми, действие этого ключевого слова продолжается до тех пор, пока не встретится ключевое слово private или пока не встретится конец определения класса. В строке 10 начинается основная функция программы. Объект Rex объявлен в строке 12 как объект класса Dog. В строке 13 открытой переменной Age объекта Rex присваивается значение, равное 5. В строке 15 переменная Age используется для вывода сообщения об объекте Rex.

Замечание: попробуйте закомментировать строку 5 и заново откомпилировать программу. Будет выдано сообщение об ошибках в строках 13 и 15, так как переменная Age теперь не будет иметь открытой доступ. По умолчанию все члены классов имеют закрытый (private) доступ.

Общим правилом проектирования классов является следующее: нужно объявлять все члены-данные класса закрытыми (private). В связи с этим, может возникнуть необходимость создать открытые (public) функции класса, которые называются методами доступа, для того, чтобы задавать и получать значения закрытых членов-данных, когда это необходимо. Эти методы доступа являются членами-функциями класса, которые другие части программы вызывают для того, чтобы задавать и получать значения закрытых членов-данных.

Новый Термин: открытым методом доступа является функция класса, которая используется для получения значения закрытой переменной класса, или для задания ей нового значения.

Зачем возиться с этим дополнительным уровнем опосредованного доступа к переменным класса? Ведь понятно, что проще и легче использовать сами переменные, вместо того чтобы создавать специальные функции доступа и работать с их помощью.

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

Если функция, которой нужно знать значение Age объекта класса Dog, будет обращаться напрямую к этой переменной, то эту функцию нужно будет переписывать каждый раз, когда автор класса Dog решит изменить способ хранения этой переменной (например, решит использовать для хранения возраста переменную не тип int, а тип short). В случае если имеется функция GetAge(), которая возвращает в качестве результата значение этой переменой Age, независимо от ее типа, то класс Dog может легко возвращать переменную требуемого типа. Для функции, которая вызывает функцию доступа, нет необходимости знать сохраняется ли возвращаемое значение как unsigned int или long, или вычисляется при необходимости.

Такой подход (объявление всех переменных класса как private и написание функций доступа) облегчает сопровождение программы. Он дает коду более долгую жизнь, так как изменения в проекте будут меньше влиять на него и реже требовать внесения изменений.

В примере 7.2 показан измененный класс Dog, который включает закрытые (private) члены-данные и открытые методы доступа. Заметим, что это не полностью программа, а только объявление одного класса.

Пример 7.2. Класс с объявлением методов доступа.

1://Объявление класса Dog

2://Члены-данные - закрытые,методы доступа - открытые

3://Методы доступа задают и получают значения закрытых данных

4:

5:class Dog

6:{

7:public:

8:// отрытые функции доступа

9:  unsigned int GetAge();

10:  void SetAge(int Age);

11:

12:  unsigned int GetWeight();

13:  void SetWeight(int Weight);

14:

15:// открытые члены-функции

16:  Bark();

17:

18:// открытые члены-данные

19:private:

20:  unsigned int  Age;

21:  unsigned int  Weight;

22:

23:}; 

Этот класс имеет пять открытых методов. В строках 9 и 10 записаны методы доступа для переменной Age. В строках 12 и 13 содержатся методы доступа для переменной Weight. Эти функции доступа задают значения закрытым переменным класса или возвращают их значения. Открытая функция класса Bark() объявлена в строке 16. Функция Bark() не является функцией доступа. Она не возвращает и не устанавливает значения переменных класса; она решает другую задачу для класса, печатая слово “Bark”(Гав). Сами переменные класса объявлены в строках 20 и 21.

Для того чтобы задать возраст для объекта Rex, нужно передать значение функции SetAge(), как показано ниже:

Dog Rex;

Rex.SetAge(5); //задаем значение Age используя

     // открытый метод доступа

Обязательно объявляйте члены-данные класса закрытыми, со временем вы поймете достоинство такого решения. Используйте открытые методы доступа (функции) к закрытым членам-данным, когда это требуется. Не пытайтесь использовать закрытые члены-данные вне определения функций класса. Используйте закрытые переменные класса в членах-функциях класса.

7.4. Реализация методов класса

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

Определение членов-функций начинается с имени класса, за которым стоят два двоеточия (::), имя функции, и в скобках объявляются ее параметры. Операция “::” называется операцией уточнения области видимости, и она определяет, к какому классу относится функция. Пример 7.3 демонстрирует полное определение простого класса Dog и реализацию (определение) его функций доступа и одного метода общего назначения.

Пример 7.3. Реализация методов простого класса.

1:// Демонстрируется определение класса

2:// и его методов

3:#include <iostream> // для потока вывода cout

4:using namespace std;

5:class Dog           // начало определения

6:{

7:public:             // начало открытого раздела

8:  unsigned int GetAge();       // функция доступа

9:  void SetAge (unsigned int age); // функция доступа

10:  void Bark();      // функция общего назначения

11:private:            // начало закрытого раздела

12:  unsigned int Age; // член-переменная

13:};

14:// определение GetAge - открытой функции доступа

15:// возвращает значение переменной Age

16:unsigned int Dog::GetAge()

17:{

18:  return Age;

19:}

20:// описание SetAge - открытой функции доступа

21:// устанавливает значение переменной Age

22:void Dog::SetAge(unsigned int age)

23:{

24:// задаем значение переменной класса Age

25:  Age = age;

26:}

27:// описание метода Bark 

28:// выполняет вывод слова "bark" на экран

29:void Dog::Bark()

30:{

31:  cout << "Bark.\n";

32:}

33:// создаем объект класса Dog, задаем значение Age

34:// и выполняем действия с этим объектом

35:int main()

36:{

37:  Dog Rex;

38:  Rex.SetAge(5);

39:  Rex.Bark();

40:  cout << "Rex age is "<< Rex.GetAge() << ".\n";

41:  Rex.Bark();

42:  return 0;

43:}

Результат работы программы:

Bark.

Rex age is 5.

Bark.

В строках 6-13 содержится определение класса Dog. В строке 7 стоит ключевое слово public, которое сообщает компилятору, что следующие за ним члены класса являются открытыми. В строке 8 записано объявление открытого метода доступа GetAge(). Этот метод обеспечивает доступ к закрытой переменной класса Age. Строка 9 содержит объявление функции доступа SetAge(). Эта функция принимает один аргумент целого типа и задает его значение этой же переменной Age. В строке 10 записано объявление метода класса Bark(). Этот метод не является функцией доступа. Это функция общего назначения, которая печатает слово “Bark“(Гав) на экран.

Со строки 11 начинается закрытая часть класса, которая включает объявление только одной переменной Age в строке 12. Объявление класса заканчивается закрывающей фигурной скобкой и двоеточием в строке 13.

Строки 16-19 содержат определение функции класса GetAge(). Этот метод не имеет параметров, а в качестве результата возвращает целое значение. Отметим, что определение методов класса включают имя класса, за которым стоят два двоеточия и имя функции (в строке 16). Этот синтаксис сообщает компилятору, что функция GetAge()является одной из функций, которые объявлены в классе Dog. При записи функции класса, можно использовать все переменные класса без какого-либо их описания. Все члены-данные класса считаются локальными переменными во всех функциях класса. За исключением записи заголовка функции класса, и использования переменных класса, во всем остальном определение функции класса точно такое же, как и определение любой другой функции (глобальной).

Тело функции GetAge() занимает только одну строку; в ней возвращается значение переменной класса Age. Заметим, что функция main() не имеет доступа к переменной класса Age, так как она является закрытым членом класса Dog. Функция main()имеет доступ к отрытому методу класса GetAge(). Так как этот метод является членом-функцией класса Dog, то она имеет полный доступ к переменной Age и может вернуть значение этой переменной в функцию main().

Со строки 22 начинается определение члена-функции SetAge(). Она принимает один параметр целого типа и присваивает переменной Age значение переданного параметра в строке 25. Так как эта функция член класса Dog, то она имеет прямой доступ к переменной класса Age.

Со строки 29 начинается определение класса Bark(). Эта функция выводится на экран слово “Bark”.

В строке 35 начинается сама программа с функции main(). В строке 37 функции main() объявляется объект класса Dog с именем Rex. В строке 38 значение 5 присваивается переменной объекта Age с помощью метода доступа SetAge(). Заметим, что метод вызывается с помощью имени объекта (Rex), за которым стоит знак операции точка (.) и имя вызываемой функции (SetAge()). Тем же самым способом можно вызвать любой другой открытый метод класса. В строке 39 вызывается член-функция Bark(), а в строке 40 печатается сообщение с помощью функции доступа GetAge(). В строке 41 снова вызывается функция Bark().

7.5. Конструкторы и деструкторы

Инициализация объединяет объявление переменной и присваивание ей начального значения (об этом говорилось в главе 4). Ничто не мешает позже изменить это значение, но инициализация гарантирует, что переменная никогда не окажется без имеющего смысл значения.

Для инициализации объектов класса используются специальные члены-функции класса, которые называются конструкторами. Конструктор может иметь список параметров, но он не может возвращать результат, даже типа void. Конструктор является методом класса, который имеет такое же имя, как и сам класс. Например: Dog();

Точно так же, как конструктор инициализирует объекты класса, другой специальный метод - деструктор выполняет все требуемые завершающие действия перед уничтожением объекта. Имя деструктора всегда совпадает с именем класса, перед которым стоит символ ~ (тильда). Деструктор не принимает ни каких параметров и не возвращает никакого значения. Следовательно, для класса Dog объявление деструктора будет следующим: ~Dog();

Если программист не объявил в классе ни одного конструктора, то компилятор создаст для него конструктор по умолчанию. Тоже самое выполняется и для деструктора. Конструкторы и деструкторы по умолчанию не имеют параметров и ничего не делают.

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

Dog Cesar;    // Cesar не получает параметров

Для такой записи нужно иметь конструктор следующего вида:

Dog();

Когда объявляется объект класса, то конструктор этого класса вызывается на выполнение. Если объявить конструктор класса Dog, который принимает два параметра, то нужно будет при создании объекта этого класса передавать в конструктор два параметра, как показано ниже:

Dog Rex(5,7);

Если конструктор принимает один параметр, то можно объявить объект следующим образом:

Dog Rex(3);

Если конструктор совсем не имеет параметров, то можно записать так:

Dog Rex();

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

Dog Rex;

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

Правило хорошего стиля программирования заключается в том, что если объявляется конструктор, то нужно объявить и деструктор, даже если деструктор ничего и не делает. Хотя деструктор по умолчанию работает правильно, лучше объявить свой собственный деструктор. Это сделает код программы более понятным.

В примере 7.4 класс Dog переписан с использованием конструктора для инициализации объектов этого класса путем задания переменной Age начального значения, заданного при объявлении.

Пример 7.4. Использование конструкторов и деструкторов.

1:#include <iostream>  // для потока вывода cout

2:using namespace std;

3:class Dog              // начало объявление класса

4:{

5:public:                // начало открытого раздела

6:  Dog(int initialAge); // конструктор

7:  ~Dog();              // деструктор

8:  unsigned int GetAge();  // функция доступа

9:  void SetAge(unsigned int age);// функция доступа

10:  void Bark();

11:private:            // начало закрытого раздела

12:  unsigned int Age; // переменная класса

13:};

14:Dog::Dog(int initialAge)//конструктор класса Dog

15:{

16:  Age = initialAge;

17:}

18:Dog::~Dog()     // деструктор, ничего не делает

19:{

20:}

21:// Определение открытой функция доступа GetAge,

22:// возвращает значение переменной Age

23:unsigned int Dog::GetAge()

24:{

25:  return Age;

26:}

27:// Определение открытой функции доступа SetAge,

28:void Dog::SetAge(unsigned int age)

29:{

30:// присваиваем переменной класса Age

31:  Age = age;

32:}

33:// Определение метода Bark 

34:void Dog::Bark()

35:{

36:  cout << "Bark.\n";

37:}

38://создаем объект класса Dog,

39://выполняем с ним некоторые действия

40:int main()

41:{

42:  Dog Rex(5);

43:  Rex.Bark();

44:  cout << "Rex age is "<< Rex.GetAge() << ".\n";

45:  Rex.Bark();

46:  Rex.SetAge(7);

47:  cout << "Now Rex age is "<< Rex.GetAge() << ".\n";

48:  return 0;

49:}

Результат работы программы:

Bark.

Rex age is 5.

Bark.

Now Rex age is.

Этот пример аналогичен примеру 7.3, за исключением того, что в строке 6 добавлен конструктор, который принимает один параметр целого типа. В строке 7 объявляется деструктор, который не имеет параметров. Деструктор никогда не имеет параметров, и ни конструктор, ни деструктор не возвращает никакого значения, даже типа void. В строках 14-17 показана реализация конструктора. Его запись схожа с записью реализации функции доступа SetAge(). Однако, конструктор не возвращает никакого значения.

В строках 18-20 показана реализация деструктора ~Dog(). Эта функция ничего не делает, но она должна быть определена, если записана в определении класса. Строка 42 содержит объявление объекта класса Dog с именем Rex. Значение 5 передается в конструктор объекта Rex. Теперь нет необходимости вызывать функцию SetAge(), так как объект Rex уже был создан со значением 5 в его переменной класса Age, что демонстрируется в результате выполнения строки 44. В строке 46, переменной Age объекта Rex присваивается новое значение равное 7. В строке 62 это новое значение выводится на экран.

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

7.6. Константные члены-функции

Если метод класса объявлен как const (т.е. константный), то это  значит,  что данный метод не будет менять значение ни у одной переменной класса. Для того чтобы объявить метод класса константным, поставьте ключевое слово const после круглых скобок с параметрами, но до точки с запятой. Например:

void SomeFunction() const;

Функции доступа часто объявляются константными функциями с помощью модификатора const. Класс Dog имеет две функции доступа:

void SetAge(int anAge);

unsigned int GetAge();

Функция SetAge() не может быть константной, потому, что она изменяет значение переменной класса Age. С другой стороны, функция GetAge() может и должна быть объявлена константной const, так как она никак не меняет значения переменных класса. Функция GetAge() просто возвращает текущее значение переменной класса Age. Поэтому, объявления этих функций должны записываться следующим образом:

void SetAge(int age);

int GetAge() const;

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

Замечание: используйте ключевое слово const везде, где это возможно. Объявляйте члены-функции как константные, когда они не должны изменять объект. Это позволит компилятору помочь обнаружить ошибки; это быстрее и проще, чем находить ошибки в программе самому.

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

7.7. Интерфейс и реализация

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

Например, открытая часть объявления класса Dog, соответствует заключению контракта о том, что каждый объект этого класса Dog будет иметь переменную Age, которой можно задать начальное значение в конструкторе класса; этой переменной так же может быть присвоено значение с помощью функции доступа SetAge(), а ее значение можно получить с помощью функции доступа GetAge(). Объявление класса также обещает, что каждый объект класса Dog может  выполнять функцию Bark().

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

Язык C++ является строго типизированным языком, а это означает, что компилятор следит за выполнением данного контракта, выдавая ошибки компиляции всякий раз, как будет попытка его нарушить. Пример 7.5 демонстрирует программу, которая не компилируется из-за нарушений такого контракта.

Пример 7.5. Демонстрируются сообщений компилятора об ошибках.

1:#include <iostream>  // для потока вывода cout

2:using namespace std;

3:class Dog

4:{

5:  public:

6:   Dog(int initialAge);

7:  ~Dog();

8:  unsigned int GetAge() const;// const функция

9:  void SetAge (unsigned int age);

10:  void Bark();

11:  private:

12:  unsigned int Age;

13:};

14:Dog::Dog(int initialAge) // конструктор класса Dog

15:{

16:  Age = initialAge;

17:  cout << "Dog Constructor\n";

18:}

19:Dog::~Dog()    // деструктор, ничего не выполняет

20:{

21:  cout << "Dog Destructor\n";

22:}

23:// GetAge, константная функция

24:// но она нарушаем условие const!

25:unsigned int Dog::GetAge() const

26:{

27:  return (Age++);// нарушаем условие,

28:}       // не изменяемости объект!

29:// Определение SetAge, открытой функции доступа

30:void Dog::SetAge(unsigned int age)

31:{

32:// задаем значение переменной класса Age

33:  Age = age;

34:}

35:// Определение метода Bark 

36:void Dog::Bark()

37:{

38:  cout << "Bark.\n";

39:}

40:// демонстрируются различные нарушения интерфейса

41:// и полученные в результате ошибки компиляции

42:int main()

43:{

44:  Dog Rex; //не соответств.определению конструктора

45:  Rex.Meow();// Dog не умеет мяукать

46:  Rex.Age = 7;    // переменная Age закрытая

47:  return 0;

48:} 

Сообщения компилятора об ошибках:

1: error C2166: l-value specifies const object

2: error C2512: 'Dog': no appropriate default constructor available

3: error C2039: 'Meow': is not a member of 'Dog'

declaration of 'Dog'

4: error C2248: 'Age': cannot access private member declared in class 'Dog'

Эта программа компилируется с ошибками, поэтому нет результатов ее работы, а есть сообщения компилятора. Эта программа интересна тем, что в ней сделано много ошибок. Первое сообщение об ошибке информирует, о том, что в строке 8 функция GetAge() объявляется константной, каковой она и должна быть. Однако в теле функции GetAge(), в строке 27, значение переменной Age увеличивается. Так как этот метод объявлен константным, то он не может менять значение переменной Age.

В строке 10, функция Bark() не объявлена константной. Это не ошибка, а плохой стиль программирования. При хорошем проектировании программы нужно учитывать, что этот метод не меняет члены-данные объекта, и объявлять его константным (const).

В строке 44 сделано объявление объекта класса Dog с именем Rex. В классе Dog определен один конструктор, который требует один обязательный параметр целого типа. Так как в строке 44 при описании объекта параметр не передается, то компилятор выдает сообщение 2 об этой ошибке.

В строке 45 показан вызов функции Meow()для объекта класса Dog. Эта функция не объявлена в классе Dog и, следовательно, обращение к ней вызывает ошибку компиляции 3.

В строке 46 показано, что переменной Age присваивается значение 7. Так как Age является закрытой (private) переменной класса, то это также вызывает ошибку компиляции 4.

7.8. Размещение определений класса и методов

Как уже рассматривалось ранее, каждая функция класса должна быть определена. Определения функций классов, как правило, располагаются в отдельном файле с расширением “.CPP”. Можно в файл, где находятся определения функций, также помещать и объявления функций и определения классов, но это является плохим стилем программирования и пользоваться таким файлом неудобно. Большинство программистов придерживаются следующего простого правила:

  1.  определение класса помещается в так называемый заголовочный файл (header file) обычно с таким же именем, как и файл с определениями функций, но имеющий расширение “.H”. Например, можно поместить определение класса Dog в заголовочный файл с именем DOG.H;
  2.  определения функций класса помещаются в файл с расширением “.CPP”, например DOG.CPP.

Но затем нужно включить заголовочный файл с расширением “.H” в файл с расширением .CPP” с помощью инструкции препроцессора записанной в начале файла DOG.CPP:

#include "Dog.h"

Эта инструкция заставляет компилятор вставить содержимое файла DOG.H в данный файл DOG.CPP, как если бы программист сам напечатал его содержимое, начиная с этой строки. Может возникнуть вопрос: “Зачем было заниматься разделением этих файлов, если они опять объединяются в один файл?”. Дело в том, что клиентам созданного класса нет дела до его реализации, определения функций. Для их работы нужен только заголовочный файл с определением класса, этого им вполне достаточно; им не нужны файлы с реализацией методов класса. Поэтому они будут использовать заголовочные файлы, а не будут каждый раз заново записывать определения класса в начале всех файлов, где используются объекты данного класса.

Замечание: определения классов обычно хранятся в файлах с расширением “.H”, которые называются заголовочными файлами. Определения функций класса хранятся в файле с расширением “.CPP”. Детали реализации класса касаются только разработчика данного класса. Клиентам класса, то есть частям программы, которые используют данный класс, не нужно знать, как функции класса реализованы. Клиенты используют только заголовочные файлы.

7.9. Встроенные функции 

Можно сделать методы класса встроенными (inline), точно так же, как  делаются встроенными обычные (глобальные) функции. Для этого перед указанием типа возвращаемого значения в определении функции, нужно поставить ключевое слово inline. Например, реализация встроенной функции GetWeight() выглядит следующим образом:

inline int Dog::GetWeight()

{

 return Weight;// возвращаем перем.класса Weight

}

Можно также поместить определение функции в определение класса, что автоматически делает эту функцию встроенной. Например,

class Dog

{

public:

 int GetWeight() {return Weight;}//inline функция

 void SetWeight(int Weight);

};

Отметим синтаксис определения функции GetWeight(). Ее тело начинается немедленно после объявления; после закрывающей круглой скобки нет точки с запятой. Так же, как и у других функций, определение начинается с открывающей фигурной скобки и заканчивается - закрывающей.

7.10. Включение в классы объектов других классов

Редко определение сложного класса содержит определения всех более простых понятий, нужных для него. Например, можно определить класс автомобиль, который включает в себя определения таких понятий, как колесо, двигателей, руль и всех других частей автомобиля. Однако удобнее будет отдельно определить класс колес, класс двигателей и т.д. и только затем при определении класса автомобиль включить в него объекты ранее определенных классов. Такая взаимосвязь между классами называется агрегацией. Например, если имеется класс дат:

class Date //  класс, который определяет новый

{    //  тип данных - дата

private:

 short day,month,year;

public:

 void Date();

 bool isValid();

 Date getCurrent();

};

то объект этого класса можно включить в определение класса автомобилей Car:

class Car

{

private:

 Date productDate; // дата выпуска автомобиля

 string Model;   // объект класса строк (см.главу 12)

public:

 void Start();

 void Accelerate();

 void Brake();

};    // конец определения класса

Глава 8. Инструкции управления

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

8.1. Циклы

Многие задачи программирования решаются путем многократного повторения одного и того же участка программы - цикла. Самая простая реализация цикла состоит из метки, нескольких инструкций и перехода к метке. Меткой является любое правильное имя, за которым следует двоеточие (:). Метка помещается слева от какой-нибудь инструкции, и переход к метке выполняется с помощью инструкции goto, за которой следует имя метки.

Пример 8.1 Цикл с использованием инструкции goto

1:#include <iostream>

2:using namespace std;

3:int main()

4:{

5:  int counter = 0;// инициализация счетчика

6:  loop:  counter ++;    // вершина цикла

7:  cout << "counter: " << counter << "\n";

8:  if (counter < 3)      // проверка значения

9:    goto loop;          // переход к началу цикла

10:  cout<<"Complete. Counter: "<<counter<<".\n";

12:  return 0;

13:}

Результат работы программы:

counter: 1

counter: 2

counter: 3

Complete. Counter: 3.

В строке 5 переменная counter инициализируется значением 0. Метка loop в строке 6 отмечает начало цикла. Значение переменной counter увеличивается на 1, и ее новое значение выводится на экран. Далее значение переменной counter проверяется в строке 8. Если оно меньше чем 3, то есть выражение в инструкции if истинно, то инструкция goto выполняется. Это приводит к тому, что выполнение программы вернется назад к строке 6. Программа продолжает выполнять циклы до тех пор, пока значение переменной counter не станет равно 3, и тогда выполнение "выпадает" из цикла и конечное сообщение печатается.

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

Для того чтобы исключить применение goto, разработаны другие хорошо контролируемые инструкции для создания циклов: for, while, и do...while. Их применение позволяет создавать более ясные и понятные программы.

8.2. Инструкция цикла while

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

while (условие)

 инструкция;

следующая_инструкция;

Здесь “условием” является любое выражение C++, а “инструкцией” является любая правильная инструкция языка C++ или блок инструкций. Когда условие оценивается как истинное - true, то инструкция выполняется, и затем условие проверяется снова. Эти действия продолжаются до тех пор, пока условие не будет оценено как ложное - false, в этом случае цикл прекращается и выполнение продолжается со следующей_инструкции.

В примере 8.1 для инструкции goto счетчик увеличивался до тех пор, пока он не становился равным 3. Пример 8.2 показывает ту же самую программу, но переписанную с использованием инструкции цикла while.

Пример 8.2. Создание цикла с помощью инструкции цикла while.

1:#include <iostream>

2:using namespace std;

3:int main()

4:{

5:  int counter = 0; // задается начальное значение

6:  while(counter < 3) // проверяется условие

7:  {

8:    counter++;       // тело цикла

9:    cout << "counter: " << counter << "\n";

10:  }

11:  cout << "Complete. Counter: " << counter << ".\n";

12:  return 0;

13:}

Результат работы программы:

counter: 1

counter: 2

counter: 3

Complete. Counter: 3.

Условие, проверяемое в цикле while, может быть достаточно сложным правильным выражением C++. Оно может использовать логические операции && (И), || (ИЛИ), и ! (НЕТ).

8.3. Инструкции continue и break

Иногда нужно вернуться к началу цикла до того, как будет выполнен весь набор инструкций, включенных в цикл while. В этом случае поможет инструкция continue, после которой выполнение программы перейдет к первой инструкции, составляющей тело цикла. Например:

while (условие)

{

  инструкция1;

 if (условие2)

   continue;

  инструкция2;

}

Если условие в инструкции  if (условие2) выполняется, то выполняется переход к первой инструкции цикла (инструкция1), если условие не выполняется, то следующей выполняется инструкция2.

Возникают ситуации, когда требуется завершить цикл (выйти из цикла) еще до того, как будет нарушено условие продолжения цикла. Инструкция break вызывает немедленное завершение цикла while, и после него выполнение программы продолжится с инструкции, стоящей после закрывающей скобки. Например:

while (условие)

{

 if (условие2)

   break;

 инструкция1;

}

инструкция2;

Если условие в инструкции if(условие2) выполняется, то выполняется переход к инструкции стоящей за циклом (инструкция2), если условие не выполняется, то следующей выполняется инструкция1.

8.4. Бесконечный цикл while 

Условие, которое проверяется в цикле while, может быть любым правильным выражением языка C++. До тех пор, пока это условие будет оставаться истинным, выполнение цикла while будет продолжаться. Можно создать цикл, который никогда не будет заканчиваться, используя значение true (или 1, так как 1 преобразуется к true) в качестве проверяемого выражения. Так как true всегда означает истину, то цикл никогда не закончится, если в теле цикла не встретится инструкция break. Пример 8.3 демонстрирует подсчет до 10, используя эту конструкцию.

Пример 8.3. Применение бесконечного цикла while.

1:#include <iostream>

2:using namespace std;

3:int main()

4:{

5:  int counter = 0;

6:  while(true)

7:  {

8:    counter ++;

9:    if (counter > 10)

10:      break;

11:  }

12:  cout << "Counter: " << counter << "\n";

13:  return 0;

14:}

Результат работы программы:

Counter: 11

В строке 6, задан цикл while с условием, которое никогда не будет ложным. В цикле значение переменной counter увеличивается на 1 и затем выполняется проверка - превышает ли значение переменной counter величину 10. Если условие не выполняется, то цикл while продолжает повторяться. Если значение counter больше чем 10, то выполнение инструкции  break завершает работу цикла while, и выполнение программы переходит к строке 12, где печатается полученный результат.

8.5. Инструкция цикла do...while

В цикле do...while вначале выполняются все инструкции, составляющие тело цикла, и только затем проверяется условие окончания цикла.  Это гарантирует, что тело цикла всегда выполнится, по крайней мере, один раз. Синтаксис этой инструкции следующий:

do

 инструкция;

while (условие);

Сначала инструкция цикла выполняется, а затем проверяется условие. Если условие выполняется (true), то выполнение цикла повторяется; в противном случае цикл заканчивается. Требования к инструкциям и условиям такие же, как и в цикле while. Например:

// счет до 10

int x = 0;

do

 cout << "x: " << x++;

while (x < 10)

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

8.6. Инструкции цикла for

При программировании циклов while часто требуется задавать начальное значение некоторой переменной, проверять является ли какое-то условие (которое включает эту переменную) истинным, и увеличивать на единицу (или на другое значение) величину переменной при каждом выполнении цикла. В языке С++ есть еще одна инструкция цикла - for, который объединяет в себе все эти три действия: задание начального значения, проверку и увеличение. Она имеет следующий синтаксис:

for (задание начальных условий; проверка; действие)

 инструкция;

Внутри скобок записываются три инструкции, разделенные точкой с запятой. Инструкция задания начальных условий используется для задания переменной цикла некоторого начального значения, то есть готовит ее к началу цикла. Этот инструкция может быть также инструкцией объявления и инициализации переменной цикла. Проверкой является любое выражение языка C++, оно будет вычисляться перед каждым выполнением цикла. Эта инструкция играет ту же роль, что и условие в цикле while. Если результатом проверки будет true, то выполняется тело цикла, а затем действие, записанное после проверки (обычно действием является увеличение значения переменной цикла). Если результатом проверки будет false, то работа цикла завершается и выполняется переход к первой инструкции, который стоит после тела цикла. Отметим, что первая и третья инструкции могут быть любыми допустимыми инструкциями C++, а вторая инструкция должна быть выражением, т.е. инструкцией языка C++, которая возвращает значение.

Пример 1:

// вывод на экран слова Hello десять раз

for (int i = 0; i<10; i++)

  cout << "Hello! ";

Пример 2:

for (int i = 0; i < 10; i++)

{

   cout << "Hello!" << endl;

   cout << "the value of i is: " << i << endl;

}

Логику работы инструкции for можно описать следующим образом:

  1.  Выполняется задание начальных значений.
  2.  Проверяется условие.
  3.  Если условие выполняется (true), то выполняется тело цикла, иначе выполнение цикла завершается.
  4.  Выполняется инструкция действия.
  5.  Выполняется переход к шагу 2.

Пример 8.4. Организация цикла с помощью инструкции for.

1:#include <iostream>

2:using namespace std;

3:int main()

4:{

5:  int counter;

6:  for (counter = 0; counter < 3; counter++)

7:    cout << "Looping! ";

8:  cout << "\nCounter: " << counter << ".\n";

9:  return 0;

10:}

Результат работы программы:

Looping!  Looping!  Looping!

Counter: 3.

Инструкция for в строке 6 включает в себя задание начального значения переменной counter, проверку – меньше ли значение переменной counter чем 3 и увеличение значения counter на единицу. Тело инструкции цикла for располагается в строке 6.

Циклы могут быть вложенными, то есть один цикл может быть помещен в тело другого цикла. Внутренний цикл будет выполняться полностью при выполнении  каждой итерации внешнего цикла. Пример 8.5 демонстрирует вывод на экран символов в виде прямоугольной таблицы, с помощью вложенных циклов for.

Пример 8.5. Использование вложенных циклов.

1:#include <iostream>

2:using namespace std;

3:int main()

4:{

5:  int rows, columns;

6:  char theChar;

7:  cout << "How many rows? ";

8:  cin >> rows;

9:  cout << "How many columns? ";

10:  cin >> columns;

11:  cout << "What character? ";

12:  cin >> theChar;

13:  for (int i = 0; i<rows; i++)

14:  {

15:    for (int j = 0; j<columns; j++)

16:      cout << theChar;

17:    cout << "\n";

18:  }

19:  return 0;

20:}

Результат работы программы:

How many rows? 3

How many columns? 12

What character? x

xxxxxxxxxxxx

xxxxxxxxxxxx

xxxxxxxxxxxx

Программы предлагает пользователю ввести количество строк rows и столбцов columns матрицы, а также символ, который будет выводиться. Первый цикл for в строке 13 инициализирует счетчик (i) значением 0, и затем тело внешнего цикла начинает выполняться. В строке 15, первой строке тела внешнего цикла for, задается другой цикл for. Второй счетчик (j) также инициализируется значением 0, и начинает выполняться тело внутреннего цикла for. В строке 16 заданный пользователем символ печатается, и управление возвращается заголовку внутреннего цикла for. Отметим, что тело внутреннего цикла for состоит только из одной инструкции (вывод на экран символа). Далее проверяется условие внутреннего цикла (j < columns), и если оно оценивается как истинное, т.е. выполняется, то j увеличивается на единицу,  и следующий символ выводится на экран. Эти действия продолжаются до тех пор, пока значение переменной j не станет равным количеству столбцов. Как только условие внутреннего цикла for не выполнится, в данном случае после того, как 12 символов ‘x‘ выведутся на экран, управление будет передано строке 17, и на экран будет выведен символ перехода на новую строку (т.е. все последующие символы будут выводиться с начала следующей строки). После этого внешний цикл for вернется к своему заголовку, в котором проверяется его условие выполнения (i < rows). Если это условие выполняется, то есть оценивается как истинное, то значение переменной i увеличивается на единицу и тело этого цикла будет выполняться снова.

На второй итерации внешнего цикла for внутренний цикл for начинает выполняться заново. Следовательно, переменная j заново инициализируется значением 0, и весь внутренний цикл повторяется.

Основной идеей вложенности циклов является то, что внутренний цикл выполняется полностью на каждой итерации внешнего цикла. Таким образом, заданный символ выводится на экран столько раз, сколько задано столбцов columns для каждой строки.

8.7. Инструкция выбора switch (переключатель)

В главе 5 рассматривалось, как использовать инструкции if и if/else. В том случае, когда используются несколько вложенных друг в друга инструкций условия, программа становится достаточно запутанной. С помощью инструкции выбора switch эту же задачу можно решить проще. В отличие от инструкций if, которая каждый раз проверяет одно условие, инструкция switch позволяет проверить сразу несколько условий и в соответствии с их выполнением разветвить выполнение программы. Синтаксис  инструкции switch следующий:

switch (выражение)

{

case значений1: инструкция;

               break;

case значений2: инструкция;

               break;

....

case значенийN: инструкция;

               break;

default: инструкция;

}

Выражение в скобках после инструкции switch может быть любым допустимым выражением языка C++, результатом которого является целое значение (например, char или int). Инструкция switch оценивает выражение и сравнивает полученный результат на равенство с каждым значением, которое стоит после ключевых слов case. Эти значения должны быть целыми величинами или символами. Если одно из значений case равно значению выражения, то выполнение переходит к инструкции стоящей после него, и выполнение инструкций продолжается до тех пор, пока не встретится инструкция break или не закончится инструкция switch. Если встретится инструкция break, то выполнение передается первой инструкции, которая стоит после закрывающей скобки. Если ни одно из значений не соответствует значению выражения, то выполняется переход к инструкции, стоящей после ключевого слова default, если оно присутствует в переключателе. Если нет ключевого слова default, и ни одно из значений case не совпадает со значением выражения, то выполнение передается инструкции, которая стоит после закрывающей скобки.

Пример 1:

switch (choice)

{

case 0:

       cout << "Zero!" << endl;

       break

case 1:

       cout << "One!" << endl;

       break;

case 2:

       cout << "Two!" << endl;

default:

       cout << "Default!" << endl;

}

Пример 2:

switch (choice)

{

choice 0:

choice 1:

choice 2:

      cout << "Less than 3!";

      break;

choice 3:

      cout << "Equals 3!";

      break;

default:

      cout << "greater than 3!";

}

Необходимо отметить, что если нет инструкции break в конце инструкции case, то выполнение будет переходить к инструкции стоящим после следующего ключевого слова case. Иногда это нужно, но обычно является ошибкой. Если позволяется выполнять инструкции сразу после нескольких case, то нужно писать специальные комментарии, показывающие, что это не просто ошибочный пропуск инструкции break.

 


Глава 9. Указатели

Одним из наиболее мощных инструментов, доступных программисту в языке C++, является его способность манипулировать памятью компьютера напрямую с помощью указателей.

9.1. Что такое указатель?

Указатель это переменная, которая хранит адрес памяти компьютера. Как уже пояснялось, память состоит из пронумерованных ячеек. Каждая переменная может занимать несколько ячеек - участок памяти. Номер первой ячейки участка является адресом переменной. Обычно программистам не нужно знать конкретные адреса переменных, потому что компилятор сам управляет ими. Компилятору сообщается, сколько памяти нужно выделить для переменной, путем объявления ее типа. Для того чтобы узнать адрес переменной используется операция получения адреса - & (смотри раздел 4.3.).

При объявлении указателей задается один из ранее рассматриваемых встроенных типов, или имя класса, и затем записывается символ звездочка (*), который и сообщает о том, что далее будет записываться имя не просто переменной, а указателя. Например, для того чтобы описать указатель с именем pAge, который может хранить адрес переменной целого типа, нужно написать:

int *pAge = 0;

Отметим, что указатель pAge является переменной и ему тоже выделяется участок памяти. Когда объявляется переменная типа int, она предназначается для хранения целых чисел. Когда объявляется указатель, подобный pAge, то он предназначается для хранения адреса какой-то переменной целого типа. В данном примере pAge инициализируется нулем. Указатель, который имеет значение нуль, называется нулевым указателем. Хорошим стилем программирования считается инициализация указателей. Если не известно, какое значение присвоить указателю, то ему присваивается значение 0.

Если указатель инициализирован значением 0, то далее в программе будет нужно присвоить ему адрес какой-нибудь переменной. Ниже показано, как это можно сделать:

short howOld = 50;//создаем переменную

short * pAge = 0; //создаем указатель

pAge = &howOld;   //помещаем адрес howOld в pAge

В первой строке создается переменная howOld, типа short, и инициализируется значением 50. Во второй строке объявляется переменная pAge, как указатель на тип short и инициализируется нулем. Понятно, что pAge является указателем, потому что звездочка (*) стоит после типа и перед именем переменной. В последней строке указателю pAge присваивается адрес переменной howOld. Можно понять, что присваивается адрес переменной howOld, так как перед именем переменной стоит операция получения ее адреса (&). Если не использовать операцию получения адреса, то будет присваиваться значение переменной howOld. Это может быть, а может и не быть, правильным адресом. С этого момента, указатель pAge имеет своим значением адрес переменной howOld. Переменная howOld, в свою очередь, имеет значение 50. Можно сделать то же самое за меньшее количество шагов:

short howOld = 50;  //создаем переменную

short *pAge=&howOld;//создаем указатель на howOld

Указатели могут иметь любые имена, которые являются правильными для переменных. Часто придерживаются следующего соглашения при составлении имен указателей: их имена начинаются с буквы p (от слова pointer), как, например, pAge или pNumber. Это делается для того, чтобы было проще различать обычные переменные и указатели.

9.2. Операция разыменования

Основной операцией над указателями является разыменование (*). Когда указатель разыменуется, то в результате получается значение, которое хранится по адресу, который хранится в указателе. Эта операция называется также косвенным доступом. Указатель позволяет выполнить косвенный доступ к значению переменной, чей адрес он хранит. Для того чтобы присвоить значение переменной howOld новой переменной yourAge с помощью указателя pAge, нужно написать:

short howOld = 5;

short *pAge = &howOld;

short yourAge = *pAge;

Операция разыменования (*) перед переменной pAge означает "значение, которое хранится по адресу". Это присваивание имеет следующий смысл: "Взять значение, хранящееся по адресу, который записан в pAge, и присвоить его переменной yourAge."

Замечание: Операция звездочка (*) используется в двух различных случаях с указателем: при объявлении и при разыменовании. Когда указатель объявляется, то звездочка показывает, что это - указатель, а не просто обычная переменная. Когда указатель разыменуется, то эта операция показывает, что будет использоваться переменная, адрес которой хранится в указателе, а не сам указатель.

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

Пример 9.1 Работа с данными с помощью указателей.

1:#include <iostream>

2:using namespace std;

3:typedef unsigned short USHORT;

4:int main()

5:{

6:  USHORT myAge;       // создаем переменную

7:  USHORT * pAge = 0;  // создаем указатель

8:  myAge = 5;

9:  cout << "myAge: " << myAge << "\n";

10:  pAge = &myAge;//присваиваем pAge адрес myAge

11:  cout << "*pAge: " << *pAge << "\n";

12:  *pAge = 7;    // задаем myAge значение 7

13:  cout << "*pAge: " << *pAge << "\n";

14:  cout << "myAge: " << myAge << "\n";

16:  myAge = 9;    // меняем значение переменной

17:  cout << "myAge: " << myAge << "\n";

18:  cout << "*pAge: " << *pAge << "\n";

19:  return 0;

20:}

Результат работы программы: 

myAge: 5

*pAge: 5

*pAge: 7

myAge: 7

myAge: 9

*pAge: 9

В программе объявляются две переменные: myAge типа unsigned short, и указатель на тип unsigned short - pAge. Переменной myAge присваивается значение 5 в строке 8; это проверяется выводом на экран в строке 9. В строке 10 указателю pAge присваивается адрес переменной myAge. В строке 11 указатель pAge разыменуется и печатается, показывая, что значение хранящееся по адресу, который содержится в pAge равно тому значению, которое хранится в переменной myAge, т.е. 5. В строке 12 значение 7 присваивается переменной, чей адрес хранится в pAge. Таким образом, значение переменной myAge становится равным 7, и вывод на экран в строках 13-14 подтверждает это. В строке 16 значение 9 присваивается переменной myAge. Это значение мы получаем непосредственно (через переменную myAge) в строке 17 и косвенно (путем разыменования указателя pAge) в строке 18.

Используйте операцию разыменования (*) для того, чтобы получить доступ к данным, которые находятся по адресу, хранящемуся в указателе. Инициализируйте все указатели или правильным адресом, или значением 0. Помните разницу между адресом, который содержится в указателе, и значением, которое хранится по этому адресу.

9.3. Для чего нужны указатели?

До сих пор использовалось пошаговое присваивание адреса переменной указателю. Однако на практике это делается редко. Единственное, для чего нужны такие манипуляции с указателем на локальную переменную - это объяснить, как работают указатели. Наиболее часто указатели используются для решения других задач:

  •  для работы с данными, размещаемыми в свободной памяти;
  •  для получения доступа к членам-данным и членам-функциям объектов класса;
  •  для передачи переменных по ссылке в функции.

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

9.4. Использование памяти компьютера

В С++ есть три способа использования памяти компьютера:

  •  статическая память;
  •  автоматическая память (или стек);
  •  свободная память.

В статической памяти располагаются глобальные переменные и статические переменные функций и классов. Эти переменные создаются один раз и сохраняются до окончания программы.

В автоматической памяти размещаются параметры функций и локальные переменные. Память для этих переменных автоматически выделяется и уничтожается (отсюда и название).

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

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

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

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

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

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

Достоинством свободной памяти является то, что участок выделенной памяти остается доступной до тех пор, пока не будет явно освобожден. Если участок свободной памяти будет выделен при работе  функции, то эта память будет также доступна и после того, как функция завершит работу. Но адрес этого участка памяти необходимо сохранить в каком-то указателе.

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

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

9.5. Операция new

В языке С++ можно выделить участок в свободной памяти, используя операцию new. После слова new должен стоять тип объекта, для которого требуется зарезервировать память для того, чтобы компилятор знал, сколько ячеек памяти нужно выделить. Следовательно, new short выделяет два байта в свободной памяти, а new long выделяет четыре байта.

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

short * pPointer;

pPointer = new short;

Указатель можно инициализировать при создании:

short * pPointer = new short;

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

*pPointer = 72;

Это означает: "Поместить значение 72 в область свободной памяти, адрес которой хранится в pPointer”.

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

9.6. Операция delete

Когда программист заканчивает работу с выделенным ему участком памяти, то он должны выполнить операцию delete, после которого записан указатель. Операция delete выполняет освобождение памяти, то есть возвращает ее области свободной памяти. Запомните, что указатель сам по себе, в отличие от памяти, адрес которой он хранит, является локальной переменной. Когда функция, в которой он объявлен, завершит работу, то этот указатель выходит из области видимости и исчезает. Однако память, выделенная с помощью операции new, не освобождается автоматически. Эта память становится недоступной: такая ситуация называется утечкой памяти. Она называется так, потому что выделенный участок памяти в этом случае не может уже использоваться до тех пор, пока программа не завершится.

Для того чтобы освободить память, то вернуть ее области свободной памяти, используется операция delete. Например:

delete pPointer;

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

Предупреждение: когда вызывается операция delete для указателя, то память, на которую он указывает, будет освобождена. Повторный вызов delete для этого же указателя приведет к аварийному завершению программы! Поэтому после вызова операции delete задавайте используемому указателю значение 0. Вызов delete для указателя со значением 0 не приведет к ошибке. Например:

Animal *pDog = new Animal;

delete pDog; //освобождаем память   

pDog = 0;    //устанавливаем указатель на 0

//...

delete pDog;    //  ничего опасного

В примере 9.2 показано размещение переменной в области свободной памяти, использование этой переменной и ее освобождение.

Пример 9.2. Выделение, использование и освобождение памяти.

1:#include <iostream>

2:using namespace std;

3:int main()

4:{

5:  int localVariable = 5;

6:  int * pLocal= &localVariable;

7:  int * pHeap = new int;

8:  if (pHeap == 0)

9:  {

10:    cout << "Error! No memory for pHeap!!";

11:    return 0;

12:  }

13:  *pHeap = 7;

14:  cout << "localVariable: "<<localVariable<<"\n";

15:  cout << "*pLocal: " << *pLocal << "\n";

16:  cout << "*pHeap: " << *pHeap << "\n";

17:  delete pHeap;

18:  pHeap = new int;

19:  if (pHeap == 0)

20:  {

21:    cout << "Error! No memory for pHeap!!";

22:    return 0;

23:  }

24:  *pHeap = 9;

25:  cout << "*pHeap: " << *pHeap << "\n";

26:  delete pHeap;

27:  return 0;

28:}

Результат работы программы:

localVariable: 5

*pLocal: 5

*pHeap: 7

*pHeap: 9

В строке 5 объявляется и инициализируется локальная переменная localVariable. В строке 6 объявляется указатель pLocal и инициализируется адресом этой локальной переменной. В строке 7 объявляется другой указатель pHeap, который инициализируется адресом, полученным в результате использования операции new int. Эта операция выделяет участок свободной памяти для хранения переменной типа int. В строке 8 делается проверка того, что, память выделена, и значение указателя правильное (не равна 0). В строке 13 во вновь выделенную память записывается значение 7. В строке 14 на экран выводится значение локальной переменной, а в строке 15 выводится значение переменной, адрес которой хранит указатель pLocal. Как и ожидалось, это одно и то же значение. В строке 16 на экран выводится значение, хранящееся в памяти, адрес которой содержится в указателе pHeap. Это показывает, что значение, присвоенное в строке 13 действительно можно получить с помощью операции разыменования. В строке 17, память полученная в строке 7, возвращается области свободной памяти с помощью операции delete. В результате выполнения этой операции происходит освобождение выделенной памяти и открепление (логическое) указателя от нее. Теперь указатель pHeap свободен для работы с другой памятью. Ему присваивается другой адрес в строке 18, и в строке 24 по этому адресу записывается значение 9. В строке 25 значение памяти, на которую указывает pHeap, выводится на экран. В строке 26 выделенная в последний раз память опять освобождается. Хотя строка 26, в некотором смысле, лишняя (по завершении программы вся выделенная в ней память будет освобождаться) такое явное освобождение выделенной памяти является проявлением хорошего стиля.

9.7. Создание объектов в свободной памяти

Точно также как выделяется память для переменных встроенных типов, можно выделить память и для объекта любого класса. Например, если определен класс объектов Dog, то можно объявить указатель на объекты этого класса и создать экземпляр объекта класса Dog в области свободной памяти точно так же, как создается объект в стеке. Синтаксис размещения объекта в свободной памяти точно такой же, как и для переменных встроенных типов:

Dog *pDog = new Dog;

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

9.8. Удаление объектов

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

Пример 9.3. Создание и удаление объектов в свободной памяти.

1:#include <iostream>

2:using namespace std;

3:class SimpleDog

4:{

5:public:

6:  SimpleDog();

7:  ~SimpleDog();

8:private:

9:  unsigned int Age;

10:};

11:SimpleDog::SimpleDog()

12:{

13:  cout << "Constructor called.\n";

14:  Age = 1;

15:}

16:SimpleDog::~SimpleDog()

17:{

18:  cout << "Destructor called.\n";

19:}

20:int main()

21:{

22:  cout<<"SimpleDog Rex...\n";

23:  SimpleDog Rex;

24:  cout<<"SimpleDog *pCesar = new SimpleDog...\n";

25:  SimpleDog * pCesar = new SimpleDog;

26:  cout << "delete pCesar...\n";

27:  delete pCesar;

28:  cout << "Exiting, watch Rex go...\n";

29:  return 0;

30:}

Результат работы программы:

SimpleDog Rex...

Constructor called.

SimpleDog *pCesar = new SimpleDog..

Constructor called.

delete pCesar...

Destructor called.

Exiting, watch Rex go...

Destructor called.

В строке 23 объект класса SimpleDog с именем Rex создается в стеке, и при этом происходит вызов конструктора данного объекта. В строке 25 объявляется указатель pCesar на объекты класса SimpleDog, и ему присваивается адрес объекта, который создается в свободной памяти; при создании объекта снова вызывается его конструктор. В строке 27 вызывается операция delete для указателя pCesar, то есть объект, размещенный в свободной памяти, удаляется, а память, которую он занимал, освобождается. До того, как объект удалится, вызывается его деструктор. Когда функция заканчивает работу, объект Rex, размещенный в стеке, выходит из области видимости и удаляется. В связи с этим вызывается его деструктор.

9.9. Доступ к членам-данным

Ранее рассматривался метод получения доступа к членам-данным и членам-функциям локальных объектов класса Dog (то есть созданных в стеке) с помощью операции выбора члена (.). Для того, чтобы получить доступ к объекту класса Dog в свободной памяти, нужно разыменовать указатель и использовать эту операцию для объекта, на который показывает указатель. Следовательно, для того, чтобы получить доступ к члену-функции GetAge(), можно записать:

(*pCesar).GetAge();

Скобки здесь использованы для того, чтобы убедиться, что указатель pCesar разыменуется до того как будет вызвана функция GetAge().

Так как эта запись выглядит громоздко, язык C++ предоставляет более короткую операцию доступа через указатель: операция выбор члена через указатель  (->), которая создается путем печати символа тире (-) за которым сразу же (без пробела) следует символ больше чем (>). Язык C++ рассматривает эти два символа как один символ. Пример 9.4 показывает выполнение доступа к членам-данным и членам-функциям объекта, созданного в свободной памяти.

Пример 9.4 Доступ к членам объекта, расположенного в свободной памяти.

1:#include <iostream>

2:using namespace std;

3:class SimpleDog

4:{

5:public:

6:  SimpleDog() {Age = 2;}

7:  ~SimpleDog() {}

8:  int GetAge() const {return Age;}

9:  void SetAge(int age) {Age = age;}

10:private:

11:  unsigned int Age;

12:};

13:

14:int main()

15:{

16:  SimpleDog * pRex = new SimpleDog;

17:  cout<<"Age is "<<pRex->GetAge()<<"\n";

18:  pRex->SetAge(5);

19:  cout<<"Age is "<<pRex->GetAge()<<"\n";

20:  delete pRex;

21:  return 0;

22:}

Результат работы программы:

Age is 2

Age is 5

В строке 16, в свободной памяти создается объект класса SimpleDog и его адрес запоминается в указателе pRex. При создании объекта выполняется конструктор по умолчанию, который задает члену объекта Age значение, равное 2. В строке 17 вызывается функция данного объекта GetAge(). Так как для доступа к объекту используется указатель pRex, то для обращения к членам объекта используется операция доступа через указатель (->). В строке 18 вызывается метод класса SetAge(), а в строке 19 снова выполняется обращение к функции GetAge().

9.10. Указатель this

Каждая функция, входящая в класс, имеет дополнительный скрытый параметр - указатель this. Он указывает на конкретный объект, для которого вызывается эта функция. Следовательно, при каждом вызове функций GetAge() или SetAge(), им передается еще указатель this. Используя этот указатель, функция получает доступ к членам объекта. Можно явно использовать указатель в любой функции класса.

9.11. Константные указатели

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

const int * pOne;

int * const pTwo;

const int * const pThree;

Переменная pOne является указателем на константную переменную целого типа. Значение переменной, на которую pOne указывает, не может быть изменено, она константная.

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

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

Можно запомнить следующее правило для быстрого понимания того, что является константой: посмотрите, что стоит сразу же после ключевого слова const: если это название типа, то значение переменной, на которую указывают, является константой; если это имя указателя, то указатель сам является константным, т.е. ему нельзя будет присвоить другой адрес.

const int *p1;//p1-указатель на целую константу

int *const p2 = new int;//p2 является константой

Глава 10. Ссылки

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

10.1. Что такое ссылка?

Ссылка является другим именем (синонимом) переменной или объекта; когда ссылка создается, то она должна инициализироваться  именем другого объекта (цели). С этого момента ссылка действует альтернативным именем (синонимом) цели, и все, что будет делать со ссылкой, в действительности будет делаться с объектом, который был указан при объявлении ссылки.

Можно создать ссылку, написав название типа объекта, за которым стоит операция ссылки (&), и имя ссылки. Ссылка обязательно должна инициализироваться во время создания. Например:

int hisAge;

int &rAge = hisAge;

DOG Agata;

DOG &rDogRef = Agata;

Ссылки могут иметь любое правильное имя, но часто имена ссылок начинают с буквы ‘r’ (от слова reference). Тогда, если имеется  переменную названную someInt, то можно создать ссылку rSomeRef на эту переменную:

int &rSomeRef = someInt;

Эта запись читается следующим образом "переменная rSomeRef является ссылкой на переменную целого типа, которая инициализирована указанием на переменную someInt".

Замечание: отметим, что операция ссылки (&) является тем же самым символом, который используется для операции получения адреса переменной. Однако, эта не та же самая операция, хотя понятно, что они взаимосвязаны.

Пример 10.1 показывает, как создаются и используются ссылки.

Пример 10.1. Создание и использование ссылок.

1:#include <iostream>

2:using namespace std;

3:int main()

4:{

5:   int  intOne;

6:   int &rSomeRef = intOne;

7:   intOne = 5;

8:   cout << "intOne: " << intOne << endl;

9:   cout << "rSomeRef: " << rSomeRef << endl;

10:   rSomeRef = 7;

11:   cout << "intOne: " << intOne << endl;

12:   cout << "rSomeRef: " << rSomeRef << endl;

13:   return 0;

14:}

Результат работы программы:

intOne: 5

rSomeRef: 5

intOne: 7

rSomeRef: 7

В строке 5 объявляется локальная целая переменная intOne. В строке 6 объявляется ссылка на переменную целого типа rSomeRef, и инициализируется указанием на переменную intOne. Если объявить ссылку, но не инициализировать ее, то будете выдаваться сообщение компилятора об ошибке. Ссылка всегда должна быть инициализирована.
В строке 7 переменной
intOne присваивается значение 5. В строках 8 и 9, значения переменных intOne и rSomeRef печатаются, и они конечно одни и те же.

В строке 10 значение 7 присваивается переменной rSomeRef. Так как  это другое имя переменной intOne, то число 7 в действительности присваивается переменной intOne, что и показано при выводе на экран в строках 11 и 12.  

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

10.2. На что можно ссылаться?

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

DOG & rDogRef = DOG;   // неверно

Нужно инициализировать rDogRef конкретным объектом класса DOG:

DOG Rex;

DOG & rDogRef = Rex;

Ссылки на объекты используются точно так же как и сами объекты. Члены-данные и методы доступны с помощью обычной операции доступа к членам класса (.), и ссылка действует как другое имя объекта. Пример 10.2 показывает это.

Пример 10.2. Ссылки на объекты класса.

1:#include <iostream>

2:using namespace std;

3:class SimpleDog

4:{

5:public:

6:  SimpleDog (int age, int weight);

7:  ~SimpleDog() {}

8:  int GetAge() {return Age;}

9:  int GetWeight() {return Weight;}

10:private:

11:  int Age;

12:  int Weight;

13:};

14:SimpleDog::SimpleDog(int age, int weight)

15:{

16:  Age = age;

17:  Weight = weight;

18:}

19:int main()

20:{

21:   SimpleDog Rex(5,8);

22:   SimpleDog & rDog = Rex;

23:   cout << "Age = "<< Rex.GetAge() << "\n";

24:   cout << "Weigh = "<< rDog.GetWeight() << "\n";

25:   return 0;

26:}

Результат работы программы:

Age = 5

Weight = 8

В строке 21, объявляется переменная Rex как объект класса SimpleDog. В строке 22 объявляется ссылка rDog на объекты класса SimpleDog и инициализируется указанием на объект Rex. В строках 23 и 24, вызываются открытые методы класса SimpleDog вначале с помощью объекта класса SimpleDog, а затем - ссылки на объект класса SimpleDog. Заметим, что способ доступа одинаковый. Опять ссылка является другим именем существующего объекта.

10.3. Передача аргументов функции по ссылке

В главе 6 "Функции" пояснялось, что функции имеют два ограничения: аргументы передаются в функцию по значению, и инструкция return может возвращать только одно значение.

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

Вспомним, что в примере 6.3 было показано, что вызов функции swap() (обмен значений параметров местами) не оказывает влияние на значения переменных вызывающей функции. Для того чтобы решить эту задачу можно сделать параметры функции swap() указателями на передаваемые значения, или можно вместо самих значений передавать ссылки на них.

10.3.1. Функции swap() с использованием указателей

Когда в функцию передается указатель, то это значит, что передается адрес объекта и, используя его, функция может манипулировать значениями, которые хранятся по этому адресу. Для того чтобы функция swap() обменивалась не значениями параметров, а указателями (адресами), необходимо описать, что она принимает два указателя на тип int. Затем, с помощью разыменования указателей, значения x и y будут, действительно, заменены местами. Пример 10.3 демонстрирует эту идею.

Пример 10.3. Передача параметров функции с помощью указателей.

1:#include <iostream>

2:using namespace std;

3:void swap(int *x, int *y);

4:int main()

5:{

6:  int x = 5, y = 10;

7:  cout<<"Main.Before, x: "<<x<<" y: "<<y<<"\n";

8:  swap(&x,&y);

9:  cout<<"Main.After, x: "<<x<<" y: "<<y<<"\n";

10:  return 0;

11:}

12:void swap (int *px, int *py)

13:{

14:  int temp;

15:  cout<<"Swap.Before,*px: "<<*px

16:  <<" *py: " <<*py<<"\n";

17:  temp = *px;

18:  *px = *py;

19:  *py = temp;

20:  cout<<"Swap.After,*px: "<<*px

21:  <<" *py: "<<*py<<"\n";

22:}

Результат работы программы:

Main.Before, x: 5 y: 10

Swap.Before,*px: 5 *py: 10

Swap.After,*px: 10 *py: 5

Main.After, x: 10 y: 5

В данном примере функции работает так, как и ожидается! В строке 3, прототип функции swap() изменен следующим образом: он объявляет, что два параметра являются указателями на тип int, а не переменными типа int, как было раньше. Когда функция swap() вызывается в строке 8, то адреса переменных x и y передаются в качестве аргументов. В строке 17 функции swap()объявляется локальная переменная temp. Переменная temp не должна быть указателем; она просто будет хранить значение *px (т.е. значение переменной x в вызывающей функции) в течение работы функции swap(). В строках 17 - 19 значения переменных в вызывающей функции main(), чьи адреса были переданы функции swap(), действительно меняются местами.

10.3.2. Функция swap() с использованием ссылок

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

Пример 10.4. Передачи параметров функции с использованием ссылок.

1:#include <iostream>

2:using namespace std;

3:void swap(int &x, int &y);

4:int main()

5:{

6:  int x = 5, y = 10;

7:  cout<<"Main.Before, x: "<<x<<" y: "<<y<<"\n";

8:  swap(x,y);

9:  cout<<"Main.After, x: "<<x<<" y: "<<y<<"\n";

10:  return 0;

11:}

12:void swap (int &rx, int &ry)

13:{

14:  int temp;

15:  cout << "Swap.Before, rx: "<<rx

16:   <<" ry: "<< ry <<"\n";

17:  temp = rx;

18:  rx = ry;

19:  ry = temp;

20:  cout << "Swap.After, rx: " << rx

21:   << " ry: " << ry << "\n";

22:}

Результат работы программы:

Main.Before, x:5 y: 10

Swap.Before, rx:5 ry:10

Swap.After, rx:10 ry:5

Main.After, x:10 y:5

Точно так же, как и в примере с указателями, две переменные объявляются в строке 6, и их значения печатаются в строке 7. В строке 8, вызывается функция swap(), но отметим, что передаются переменные x и y, а не их адреса. Когда функция swap()вызывается, то выполнение программы переходит к строке 12, где переменные объявлены как ссылки. Их значения печатаются в строке 15, но отметим, что здесь не требуется использовать специальные операции, чтобы получить их значения.

Основным назначением ссылок как раз и является передача значений в функцию. При этом в функции создаются синонимы переданных объектов, не их копии. Это очень эффективно при передаче объектов больших размеров. Однако возникает другая проблема, как гарантировать, что значения параметров не будут изменяться в функции, когда это не допускается. Для этого нужно объявлять параметры функции константными ссылками (const). Например:

int Inspect(const Car& car);

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

Глава 11. Массивы

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

11.1. Понятие массива

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

Можно объявить массив следующим образом: написать тип данных, за которым следует имя массива и его размер в квадратных скобках. Размер массива - это количество элементов, которые могут быть сохранены в массиве. Например:

long LongArray[25];

Здесь объявляется массив, который может хранить 25 длинных целых значений, с названием LongArray. Когда компилятор встречает такое объявление, он выделяет столько памяти, сколько требуется для хранения 25 элементов. Так как каждая длинная целая величина требует для своего хранения 4 байта, то это описание выделяет 100 последовательных байтов памяти.

Массивы могут иметь любые правильные имена, но они не могут иметь имя, совпадающее с именем другой переменной или массива в пределах их области видимости. Следовательно, нельзя иметь массив с именем myVar[5] и в то же время переменную с тем же самым именем myVar.

Можно создавать массивы любых типов – встроенных и пользовательских (классов). Для того чтобы создать массив объектов класса нужно в классе определить конструктор по умолчанию, который будет выполняться для всех элементов массива.

11.2. Элементы массива

Любой элемент массива можно получить путем указания его порядкового номера - индекса. Этот номер указывается после имени массива в квадратных скобках. Элементы массива нумеруются, начиная с 0. Следовательно, первым элементом массива является [0]. В рассматриваемом примере первый элемент массива записывается как LongArray[0], второй элемент - LongArray[1], и т.д.

К примеру, массив SomeArray[3] имеет три элемента, такие как SomeArray[0], SomeArray[1], и SomeArray[2]. В общем случае, массив SomeArray[n] имеет n элементов, которые нумеруются от SomeArray[0] до SomeArray[n-1].

Следовательно, элементы массива LongArray[25] нумеруются от LongArray[0] до LongArray[24]. Пример 11.1 показывает, как описать массив, состоящий из трех целых элементов и как присвоить каждому элементу значение.

Пример 11.1. Использование массива целых значений.

1:#include <iostream>

2:using namespace std;

3:int main()

4:{

5:  int myArray[3];

6:  int i;

7:  for ( i=0; i<3; i++)  // 0,1,2

8:  {

9:    cout << "Value for myArray[" << i << "]: ";

10:    cin >> myArray[i];

11:  }

12:  for (i = 0; i<3; i++)

13:    cout << i << ": " << myArray[i] << "\n";

14:  return 0;

15:}

Результат работы программы:

Value for myArray[0]:  3

Value for myArray[1]:  6

Value for myArray[2]:  9

0: 3

1: 6

2: 9

В строке 5 объявляется массив названный myArray, который содержит три целых переменных. В строке 7 начинается цикл, который повторяется при значениях переменной цикла от 0 до 2, что соответствует правильным индексам массива из трех элементов. Пользователю предлагается ввести значение, которое затем сохраняется в соответствующем элементе массива. Первое введенное значение сохраняется в элементе myArray[0], второе  - в myArray[1], и т.д. Второй цикл for выводит все значения массива на экран.

11.3. Выход за пределы массива

Когда значения записываются в элементы массива, то компилятор рассчитывает место в памяти, где их сохранить, основываясь на размере каждого элемента и адреса первого элемента. Предположим, что требуется изменить значение элемента LongArray[5], который является шестым элементом в массиве. Компилятор умножает отступ от начала массива (5) на размер одного элемента, в данном случае - 4. Затем он перемещается по памяти на рассчитанное количество байт (20) от начала массива и записывает новое значение в найденное место.

Если попытаться записать в элемент LongArray[50], то компилятор проигнорирует тот факт, что такого элемента нет. Он опять вычислит, как далеко от начала должен находится этот элемент (в данном случае это 200 байт), и затем запишет в найденное месте заданное значение. При этом те данные, которые в этом месте памяти находились, будут потеряны. Этими данными может быть хоть что, и поэтому такие действия могут привести к непредсказуемым результатам. Если повезет, то программа закончится аварийно сразу же. Если нет, то можно получите странные результаты работы программы намного позже, и у возникнет трудная задача – определить, почему программа работает неправильно.

11.4. Инициализация массивов

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

int Array[5] = {10, 20, 30, 40, 50};

Здесь объявляется массив Array, содержащий пять целых значений. При этом элементу Array[0] присваивается значение 10, элементу Array[1] - значение 20, и т.д.

Если не указать размер массива, то размер массива будет равен количеству значений, стоящих в скобках. Следовательно, если записать:

int Array[] = {10, 20, 30, 40, 50};

то будет создан массив такого же размера, как и в предыдущем примере.

Если требуется узнать размер массива, то можно вычислить его следующим образом:

short ArrayLength = sizeof(Array) / sizeof(Array[0]);

Здесь константной переменной ArrayLength типа short присваивается результат, полученный от деления размера всего массива на размер одного его элемента. Это значение равно количеству элементов в массиве.

Если попытаться инициализировать больше элементов, чем указывалось при объявлении этого массива, то компилятор будет выдавать сообщение об ошибке. Например, при следующей записи:

int Array[5] = {10,20,30,40,50,60}; // error

А такая запись будет правильной:

int Array[5] = {10,20};

Оставшиеся не инициализированными элементы массива, на самом деле, будут инициализированы по умолчанию значением 0. Если совсем не инициализировать элементы массива, то они не будут получать значение 0 при объявлении. В этом случае, их значения не определенны.

Позволяйте компилятору определять размер инициализируемых массивов. Не пишите за пределами массива. Помните, что первый элемент массива имеет индекс 0.

11.5. Многомерные массивы

Можно создать массивы, которые имеют более чем одну размерность. Каждая размерность представляется в виде индекса массива. Следовательно, двухмерные массивы имеют два индекса; трехмерные массивы имеют три индекса; и т.д. Массивы могут иметь любую размерность, хотя наиболее вероятно, что большинство массивов, которые используются в программе, будут одно или двухмерными

Хорошим примером двухмерного массива является шахматная доска. Одна размерность представляет 8 строк, другая размерность представляет восемь столбцов.

Предположим, что имеется класс с названием SQUARE. Тогда можно следующим образом описать массив с названием Board, который соответствует шахматной доске

SQUARE Board[8][8];

11.6. Инициализация многомерных массивов

Многомерные массивы также можно инициализировать. При этом нужно учитывать, что при инициализации последующий индекс массива изменяется быстрее, чем предыдущий. В случае двухмерного массива,  сначала устанавливается номер строки 0 (первый индекс), а затем номер столбца будет меняться от начального значения до конечного, и только потом номер строки будет увеличен. Например, для массива:

int Array[3][2];

первые три элемента при инициализации попадут в первую строку Array[0]; следующие три - во вторую Array[1] и т.д. Этот массив можно инициализировать таким образом:

int Array[3][2] = {1,2,3,4,5,6};

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

int Array[3][2] = {{1,2},{3,4},{5,6}};

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

11.7. Размещение массивов в свободной памяти

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

Dog *Family = new Dog[500];

объявляется указатель Family, который содержит адрес первого элемента массива размером 500. Другими словами, Family указывает на нулевой элемент Family[0].

Преимущество использования указателя Family состоит в том, что можно использовать ссылочную арифметику, чтобы получить доступ к каждому члену массива Family. Например, можете написать

Dog *Family = new Dog [500];

Dog *pDog = Family;//pDog указывает на Family[0]

pDog->SetAge(10);  //задаем возраст объекту Family[0]

pDog++;            //переходим к Family[1]

pDog->SetAge(20); //задаем возраст объекту Family[1]

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

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

Family[2].SetAge(10);

Для того чтобы удалить массив из свободной памяти, нужно использовать операцию delete, но при этом нужно указать, что удаляется не одно значение, а массив. Например:

delete [] pDog;

Квадратные скобки сообщают компилятору, что будет удаляться массив значений. Если скобки опустить, то из памяти будет удален только первый элемент массива.

11.8. Связь указателей и массивов

В C++ имя массива является константным указателем на первый элемент массива. Следовательно, в объявлении

Dog  Family[50];

имя Family является константным указателем, хранящим значение &Family[0], что является адресом первого элемента массива Family.

Разрешается использовать имена массивов в качестве константных указателей и указателей в качестве имен массивов. Следовательно, Family+4 является допустимым способом доступа к значению в элементе массива Family[4].

Компилятор выполняет все арифметические преобразования, когда к указателю добавлятся значение, например, увеличивается на единицу. Адрес, который получит указатель, когда пишется Family+4 не будет соответствовать адресу на 4 байта больше чем адрес, который хранится в переменной Family, это будет адрес четвертого объекта массива. Если каждый объект длиной 4 байта, то при записи Family+4 к адресу в Family будет добавлено 16 байт. Если объектами массива являются объекты класса Dog, которые имеют четыре члена-данных типа long (длиной по 4 байта), и два члена-данных типа short (длиной по 2 байта), то каждый объект данного класса Dog имеет длину в 20 байт. В этом случае значением Family+4 является адрес на 80 байт больше чем адрес начала массива. Такое изменение адресов называется адресной арифметикой.

11.9. Массивы символов

Строка это последовательность символов. До сих пор мы работали только со строками, которые были текстовыми константами, заключенными в двойные кавычки ("). Например:

cout << "hello world.\n";

В C++ строки можно хранить в массивах типа char. Последним элементом строки обязательно должен быть код 0 (не символ, а число). Такие массивы называются "С-строками”. Можно описать и инициализировать строку точно так же, как объявляется любой массив. Например:

char Greeting[] = {’H’,’e’,’l’,’l’,’o’,’ ’,’W’, ’o’,’r’,’l’,’d’, 0 };

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

char Greeting[] = "Hello World";

Следует сделать два замечания по используемому здесь синтаксису:

  •  вместо использования символьных констант, выделенных одиночными кавычками ('), здесь используется символьная строка, выделенная двойными кавычками (") и она не заключена в фигурные скобки;
  •  в конце строки не добавляется число 0, так как компилятор добавит его сам.

Размер массива Greeting должен быть не менее 12 элементов, чтобы сохранить эту строку: 11 элементов для хранения букв и один элемент для хранения завершающего строку нуля.

11.10. Функции для работы с массивами символов

Язык C++ наследовал от языка C набор функций для работы с массивами символов. Все эти функции объявлены в заголовочном файле cstring. Если программа использует такие функции, то этот файл нужно включить в программу:

#include <cstring>

Примерами таких функций являются:

  •  int strlen(str) - определяет количество символов в массиве, не включая нулевой код конца строки;
  •  char* strcpy(str1,str2) – копирует все символы до нуля из символьного массива str2 в символьный массив str1;
  •  char* strcat(str1,str2) – добавляет содержимое символьного массива str2 к концу строки в символьном массиве str1 (конкатенаця); массив str1 должен иметь достаточно места для хранения всех символов;
  •  int strcmp(str1,str2)- сравнивает поэлементно строки в символьных массивах str1 и str2 в соответствии с алфавитным порядком; если очередной символ в str1 по алфавитному порядку следует ранее, чем соответствующий символ в str2, то функция возвращает отрицательное значение; если строки совпадают, возвращается 0; если очередной символ в str1 следует по алфавиту после соответствующего символа в str2 , то функция возвращает положительное значение;
  •  и т.п.

В примере 11.2 показано использование стандартных функций для работы с С-строками.

Пример 11.2. Использование функций для работы со строками.

1:#include <iostream>// для потоков ввода-вывода

2:#include <cstring> // для работы с С-строками

3:using namespace std;

4:int main()

5:{

6:  char first_name[20], last_name[40], full_name[70];

7:  cout << "Enter your first name - "; // имя

8:  cin >> first_name;  // например, Bart

9:  cout << "Enter your last name - "; // фамилия

10:  cin >> last_name;  // например, Simpson

11:  strcpy(full_name, last_name);// копируем фамилию

12:  strcat(full_name, ", "); // добавляем запятую и пробел 

13:  strcat(full_name, first_name); // добавляем имя

14:  int len = strlen(full_name);   // длина строки

15:  cout<<"Your full name is " << full_name << endl;

16:  cout<<"which contains ”<<len<<" characters."<<endl;

17:  return 0;

18:}

Результат работы программы:

Enter your first name - Bart

Enter your last name – Simpson

Your full name is Simpson, Bart

which contains 13 characters.

В строке 2 в программу включен заголовочный файл cstring, в котором объявлены функции для работы с символьными массивами. В строке 6 объявляются три символьных массива размерами в 20, 40 и 70 символов. Программист должен следить за тем, чтобы эти размеры не были превышены при работе программы. Такая проверка в данной программе не делается для того, чтобы сделать ее проще. В строках 11-14 вызываются функции стандартной библиотеки, для выполнения операций над строками. Конечно такая запись операций не очень понятная и удобная.

Данный пример с использованием нового типа данных string, переписан в разделе 12.4. Из сравнения этих программ можно увидеть, насколько могут быть полезными новые пользовательские типы данных.

В заголовочном файле <cstdlib> определены полезные функции для преобразования строк, представляющих численные значения в сами численные значения:

double atof(const char* p);// преобразует строку в p в double

int atoi(const char* p);// преобразует строку в p в int

long atol(const char* p);// преобразует строку в p в long

Например:

char p[] = “12.85”; // строка текста

double d = atof(p); // d получает числовое значение 12.85

Такие преобразования могут например использоваться при работе с параметрами командной строки (смотри раздел 14.7).

Глава 12. Класс  string

В стандартной библиотеке компилятора описаны классы общего назначения, которыми можно пользоваться при составлении программ. Примерами таких классов являются - потоки ввода-вывода, контейнеры данных, и т.п. Одним из наиболее используемых классов является класс строк – string. В языке C++ нет типа данных, который может хранить строки текста. Для этой цели ранее использовались массивы типа char (С-строки). Но работать с ними не всегда удобно и безопасно. Нужно постоянно следить за тем, чтобы не выйти за пределы массива. Объекты класса string хранят строки текста и имеют набор методов для работы с ними. Этот класс является хорошим примером того, как можно создать в языке С++ новый тип данных, который практически ни чем не отличается от встроенного типа. Класс string определен в заголовочном файле с именем <string>. Поэтому нужно включить этот заголовочный файл в программу, которая работает с объектами данного класса с помощью инструкции препроцессора.

12.1. Объявление и инициализация строк

Строки объявляются точно так же, как и объекты любого класса. В отличие от встроенных переменных, которые не имеют начального значения, строки создаются пустыми (если они не – инициализируются) и их длина равна 0. Можно создать пустую строку, с помощью следующей инструкции

string emptyStr;

Строку можно инициализировать при создании самими различными способами

  •  задав ее начальное значение в виде строковой константы в круглых скобках:

string name("Компьютер”);

  •  задав ее начальное значение в виде другой строки в круглых скобках:

string anothername(name); // конструктор копирования

  •  используя операцию присваивания при объявлении строки:

string question = "Результат равен:";

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

string nothingDoing('a'); //Ошибка !!!

12.2. Члены-функции класса string

В классе string определено много членов-функций, с помощью которых можно выполнять различные действия со строкой. Некоторые из них описаны ниже:

Имя функции

Описание функции

length

size

size_type length() const;

size_type size() const;

Обе эти функции в качестве результата возвращают длину строки (количество символов). Здесь size_type является синонимом беззнакового целого типа. Например:

string str = "Hello";

size_type len;

len = str.length(); // len == 5

len = str.size();   // len == 5

c_str

char* c_str() const;

Для совместимости с некоторыми библиотечными функциями языка С, иногда необходимо преобразовывать объект string в символьный массив ("строка в стиле C "). Данная функция выполняет такое преобразование. (смотри пример )Например, можно открыть файл для вывода данных, используя указанное пользователем имя:

string filename;

cout << "Enter file name: ";

cin >> filename;

ofstream outfile (filename.c_str());

outfile << "Data" << endl;

insert

string& insert(size_type pos, const string& str);

Эта функция вставляет строку str в текущую строку, начиная с указанной позиции.

string str1 = "abcdefghi";

string str2 = "0123";

str1.insert (3,str2);

cout << str1 << endl; // "abc0123defghi"

str2.insert (1,"XYZ");

cout << str2 << endl; // "0XYZ123"

erase

string& erase(size_type pos, size_type n);

Эта функция удаляет подстроку из текущей строки.

string str1 = "abcdefghi";

str1.erase (5,3);

cout << str1 << endl; // "abcdei"

replace

string& replace(size_type pos, size_type n, const string& str);

Данная функция заменяет одну подстроку в текущей строке на другую подстроку.

string str1 = "abcdef";

string str2 = "XYZ";

str1.replace (2,2,str2);

cout << str1 << endl; // "abXYZef"

find

rfind

size_type find (const string& str, size_type pos);

Эта функция ищет первое вхождение подстроки str в текущую строку, начиная с указанной позиции pos. Если найдет, то возвращает номер позиции первого символа. Если не найдет, то возвращает специальное значение string::npos. Функция rfind делает то же самое, что и find, но возвращает позицию последнего вхождения указанной подстроки.

string str1 = "abcdefghi";

string str2 = "def";

string::size_type pos = str1.find (str2,0);

cout << pos << endl; // 3

pos = str1.find ("AB",0);

if (pos == string::npos)

   cout << "Не найдено" << endl;

substr

substr (size_type pos, size_type n);

Возвращает подстроку текущей строки, начиная с позиции pos и длиной n символов:

string str1 = "abcde"

string str2 = str1.substr (2,2);

cout << str2 << endl; // "cd"

12.3. Функция getline для ввода данных в строку

Кроме функций-членов класса string, в стандартной библиотеке имеются также функции не являющиеся методами класса, но которые используются для работы со строками. Наиболее часто используемой является функция getline:

istream& getline (istream& is, string& str, char delim = '\n');

Эта функция читает символы из входного потока в строку string. Ввод данных останавливается в следующих ситуациях:

  •  если данные в потоке закончатся (событие “конец файла”);
  •  если указанное максимальное количество символов уже введено;
  •  если очередной прочитанный символ совпадает с указанным завершающим символом delim (по умолчанию это конец строки); этот символ убирается из входных данных, но в строку не записывается.

При успешном завершении чтения данных, эта функция в качестве результата возвращает ссылку на входной поток.

Эта функция наиболее часто используется для чтения "строка за строкой" данных из файла. Отметим, что обычная операция извлечения данных из потока (>>) останавливается при обнаружении первого пробела во входных данных, что не обязательно является концом вводимых данных. Функция getline может читать строки текста с пробелами в них. Например:

string str;

cin << str;  // еcли ввести “Привет всем!”

cout >> str; // получаем результат “Привет”, введен                                 // текст до пробела

getline(cin,str); // еcли ввести “Привет всем!”

cout >> str; // получаем результат “Привет всем”,

// введен текст полностью

12.4. Операции над объектами класса string

В классе строк описано достаточно много операций, которые можно выполнять с объектами класса. К ним относятся: присваивание, сложение, объединенные операции, операции отношения, операции ввода-вывода.

Операция присваивания может использоваться для задания значения строке различными способами:

  •  присваивание одной строки другой

string string_one = "Hello";

string string_two;

string_two = string_one;

  •  присваивание строке строковой константы

string string_three;

string_three = "Goodbye";

  •  присваивание строке символьного литерала

string string_four;

char ch = 'A';

string_four = ch;

string_four = 'Z';

Операцию сложения (конкатенация), в результате которой различные строки объединяются, также можно выполнять разными способами:

  •  сложение двух строк

string str1 = "Привет ";

string str2 = "студент";

string str3 = str1 + str2; // "Привет студент"

  •  сложение строки и строковой константы

string str1 = "Привет ";

string str4 = str1 + "студент";

  •  сложение строки и символьного литерала

string str5 = "Ура";

string str6 = str5 + '!'; // “Ура!”

Операция "+=" объединяет две выше описанных операции присваивания и сложения

string str1 = "Hello ";

str1 += "there"; //  получаем "Hello there"

Операции сравнения (== != < > <= >= ) возвращают логическое значение истина или ложь (true/false), которое показывает - выполняется ли заданное отношение между двумя операндами. Операндами в этих операциях могут быть:

  •  два объекта string;
  •  объект string и строковая константа.

Операция вывода “<<”, которая выводит строку, хранящуюся в объекте в поток вывода:

string str1 = "Hello there";

cout << str1 << endl;

Операция ввода “>>”, которая читает строку текста из входного потока и присваивает ее объекту. Выполнение данной операции заканчивается, когда из входного потока будет считан первый символ пробела.

string str1;

cin >> str1;

Операция доступа по индексу “[ ]” позволяет получить символ, хранящийся в заданной позиции строки:

string str = "abcde";

char ch = str[3];

cout << ch << endl; // 'd'

str[2] = 'X';

cout << str10 << endl; // "abXde"

Программа, показанная в примере 11.2, может быть переписана с использованием объектов класса string.

Пример 12.1. Использование объектов класса string.

1:#include <iostream>// для потоков ввода-вывода

2:#include <string>  // для работы с классом string

3:using namespace std;

4:int main()

5:{

6:  string first_name, last_name, full_name;

7:  cout << "Enter your first name - ";

8:  cin >> first_name;  // например, Bart

9:  cout << "Enter your last name - ";

10:  cin >> last_name;  // например, Simpson

11:  full_name = last_name;// копируем фамилию

12:  full_name += ", "; // добавляем запятую и пробел 

13:  full_name += first_name; // добавляем имя

14:  int len = full_name.size(); // длина строки

15:  cout<<"Your full name is " << full_name << endl;

16:  cout<<"which contains "<<len<<" characters."<<endl;

17:  return 0;

18:}

В строке 2 в программу включен заголовочный файл с определением класса string, после того, как компилятор обработает этот файл, он знает, как работать с новым типом данных. В строке 6 объявляются три объекта класса string. Эти объекты могут хранить строки текста и выполнять операции над ними. В строке 8 выполняется ввод данных в строку. Так как операция получения данных (>>) в классе string определена (перегружена), то она будет выполняться с новым типом данных. Для любого нового типа данных можно задать, как будет выполняться данная операция. В строках 11-13 так же выполняются операции над объектами класса string. Все эти операции перегружены в определении класса. В строке 14 для объекта full_name вызывается функция, определяющая количество символов, которые хранятся в этой строке.

Класс string является прекрасным примером того, как можно создать в языке С++ новый полезный тип данных.

Глава 13. Функции класса

В данной главе будем рассматривать более детально наиболее общие вопросы, связанные с функциями классов.

13.1. Перегрузка конструкторов

В главе 6, рассматривалось, как выполнить перегрузку функций, определив две или более функции с тем же самым именем, но с различными наборами параметров. Функции класса также могут перегружаться аналогичным способом. Наиболее часто перегрузка выполняется для конструкторов класса, для того чтобы дать пользователю различные возможности при объявлении объектов класса. Пример 13.1 показывает, как выполнить перегрузку конструкторов класса.

Пример 13.1. Перегрузка конструктора класса.

1:#include <iostream>

2:using namespace std;

3:class Rectangle

4:{

5:public:

6:  Rectangle();

7:  Rectangle(int width, int length);

8:  ~Rectangle() {}

9:  int GetWidth() const { return itsWidth; }

10:  int GetLength() const { return itsLength; }

11:private:

12:  int itsWidth;

13:  int itsLength;

14:};

15:

16:Rectangle::Rectangle()

17:{

18:  itsWidth = 5;

19:  itsLength = 10;

20:}

21:Rectangle::Rectangle (int width, int length)

22:{

23:  itsWidth = width;

24:  itsLength = length;

25:}

26:

27:int main()

28:{

29:  Rectangle Rect1;

30:  cout << "Rect1 width: " << Rect1.GetWidth() << endl;

31:  cout << "Rect1 length: " << Rect1.GetLength() << endl;

32:

33:  Rectangle Rect2(20,50);

34:  cout << "Rect2 width: "<<Rect2.GetWidth() << endl;

35:  cout << "Rect2 length: "<<Rect2.GetLength() << endl;

36:  return 0;

37:}

Результат работы программы:

Rect1 width: 5

Rect1 length: 10

Rect2 width: 20

Rect2 length: 50

Так же как и с обычными перегружаемыми функциями, компилятор сам определяет правильный конструктор, основываясь на количестве и типе параметров.

13.2. Инициализация объектов

До сих пор значения членов данных класса задавалось в теле функции. Однако конструкторы выполняются в два этапа: вначале выполняется этап инициализации переменных класса, а затем выполняется содержимое тела конструктора.

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

Car():       // имя конструктора и параметры

wheels(4),   // начало списка инициализации

weight(8.5)

{ }         // тело конструктора

Как видно из этого примера, для инициализации переменных после закрывающей скобки с объявлениями параметров пишется двоеточие.  После двоеточия, через запятую, перечисляются имена переменных и их начальные значения, заключенные в круглые скобки. Например:

Rectangle::Rectangle():

itsWidth(5),

itsLength(10)

{};

Rectangle::Rectangle (int width, int length):

itsWidth(width),

itsLength(length)

{};   

Переменные класса, которые являются ссылками или константами должны инициализироваться на этапе инициализации. Им нельзя присваивать значения в теле конструктора. Другим переменным значения можно присваивать, но лучше, как можно чаще, использовать инициализацию.

13.3. Копирующий конструктор

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

Копирующий конструтор имеет один параметр - ссылку на объект того же самого класса. Так как эта функция не должна менять исходный объект, то ссылка, как правило, объявляется константной. Например:

Car(const Car & theCar);

Целью данного конструктора является создание новый объект, который является копией переданного объекта theCar.

Копирующий конструктор, который создается компилятором, просто переписывает все значения переменных класса из переданного объекта в новый объект (покомпонентное копирование). Хотя такое копирование является правильным для большинста переменных класса, оно является неудовлетворительным для переменных, которые являются указателями на объекты расположенные в свободной памяти. Такое копирование указателей будет приводить к ошибкам, если объект, копия которого создается, будет выходить из области видимости и уничтожаться. Деструктор этого объекта будет освобождать все участки выделеные ему в свободной памяти. В связи с этим указатели в новом объекте, созданном с помощью конструктора копирования, будут содержать адреса освобожденных участков памяти, в которые может быть записано все, что угодно. Для того чтобы этого избежать, нужно создать свой собственный копирующий конструктор, а не использовать тот, который создается компилятором. В собственном копирующем конструкторе нужно не присваивать значения указателей, а выделять новые участки свободной памяти и копировать в них все, что содержалось в исходном объекте. Пример 13.2 показывает, как это сделать.

Пример 13.2. Создание и использование копирующего конструктора.

1:#include <iostream>

2:using namespace std;

3:class Car  // класс автомобилей

4:{

5:public:

6:  Car ();             // конструктор по умолчанию

7:  Car (int wh, float wg); // констр. с параметрами 

8:  Car (const Car &);  // конструктор копирования

9:  ~Car ();            // деструктор

10:  int GetWheels() const { return *wheels; }

11:  void SetWheels(int wh) { *wheels = wh; }

12:  float GetWeight() const { return *weight; }

13:  void SetWeight (float wg) { *weight = wg; }

14:private:

15:  int *wheels;

16:  float *weight;

17:};

18:

19:Car::Car ()

20:{//  выделяем память и задаем значения

21:  wheels = new int(4); 

22:  weight = new float(1.2F);

23:}

24: // конструктор c параметрами
25:Car (int wh,
 float wg)
26:{
27:  wheels = new int(wh);
28:  weight = new float(wg);
29:};

30:

31:Car::Car (const Car & rhs)

32:{//  выделяем память и переписываем значения

33:  wheels = new int;

34:  weight = new float;

35:  *wheels = rhs.GetWheels();

36:  *weight = rhs.GetWeight();

37:}

38:

39:Car::~Car()

40:{//  удаляем выделенную память

41:  delete wheels;

42:  wheels = 0;

43:  delete weight;

44:  weight = 0;

45:}

46:

47:int main()

48:{

49:  Car myAvto;

50:  cout<<"myAvto has "<<myAvto.GetWheels()<<” wheels\n”;

51:  cout << "Creating yourAvto from myAvto\n";

52:  Car yourAvto(myAvto);

53:  cout<<"yourAvto has "<<yourAvto.GetWheels()<<” wheels\n”;

54:  cout<<"setting myAvto’s wheels to 6...\n";

55:  myAvto.SetWheels(6);

56:  cout<<"myAvto has "<<myAvto.GetWheels()<<“ wheels\n”;

57:  cout<<"yourAvto has "<<yourAvto.GetWheels()<<”

wheels\n”;

58:  return 0;

59:}

Результат работы программы:

myAvto has 4 wheels

Creating yourAvto from myAvto

yourAvto has 4 wheels

setting myAvto’s wheels to 6...

myAvto has 6 wheels

yourAvto has 4 wheels

В строках 3-17 определяется класс Car. Отметим, что в строке 6 объявляется конструктор по умолчанию, а строке 8 – копирующий конструктор. В строках 15 и 16, объявляются две переменные класса, как указатели целого и вещественного типа. Обычно такие указатели в классах не используются, но в примере это сделано для того, чтобы показать, как работать с переменными расположенными в свободной области памяти.

Конструктор по умолчанию, в строках 21-22, выделяет место в свободной памяти и записывает туда значения.

Определение копирующего конструктора начинается в строке 31. В строках 33 и 34, выделяется место в области свободной памяти. А затем, в строках 35 и 36, туда переписываются значения переменных из переданного объекта Car. Обратите внимание, что здесь не выполняется присваивание указателей, а выделяется новая память и в нее переписываются значения переменных существующего объекта.

В функции main объявляется объект myAvto класса Car. При этом будет вызываться конструктор по умолчанию. В строке 52, создается новый объект yourCar с помощью копирующего конструктора и ему передается ранее созданный объект myAvto. В строке 53, значение переменной wheels нового объекта выводится на печать. Можно удостовериться, что значения переменной wheels в обоих объектах совпадает. В строке 55, значение переменной wheels объекта myAvto изменяется на 6, и затем выводится на экран. Теперь значения переменной wheels в объектах различные. Когда объекты выходят из области видимости, то автоматически вызываеются их деструкторы. Реализация деструктора класса Car показана в строках 39-45. В нем вызывается операция delete для обоих указателей, тем самым, освобождая выделенную память

13.4. Дружественные классы и функции

Иногда требуется создать несколько классов, которые должны работать вместе. Например, класс автомобилей Car и класс водителей Driver. В этом случае эффективность работы программы повысится, если члены одного такого класса будут иметь прямой доступ к закрытым членам другого класса. Для того чтобы добиться этого, нужно объявить один класс другом (friend) другого. Например:

class Car

{

public:

 friend class Driver; // Driver является другом Car

 . . .

};

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

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

class Car

{

public:

 // объявляем функцию другого класса другом

 friend void Driver::Repare(int a);  

 // объявляем глобальную функцию другом класса

  friend int SomeFunction(const Car& pCar)

. . .

private:

 int wheels;

. . .

}

Дружественная функция будет иметь доступ к зарытому члену wheels класса Car.

int SomeFunction(const Car& a)

{// прямой доступ к закрытому члену класса

cout << “Car has “ << a.wheels << “ wheels.”;

}

13.5. Перегрузка операций

Язык C++ имеет множество операций, которые компилятор умеет выполнять с переменными встроенных типов. Примерами таких операций являются: присваивание (=), сложение (+), умножение (*). Например:

int i=0,j=1;

int k = i + j;

j = i + 1;

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

j = i + 1;  //  символьная запись операции

и

j = operator+(i,1); // функциональная запись операции

и

operator=( j, operator+(i,1) );

являются, по смыслу, совершенно идентичными. Просто первый способ записи более привычен и удобен.

Для типов создаваемых пользователями (классов) можно определить, как будут выполняться все операции, которые имеются в языке. Например: если задан класс коробок – Box, то можно определить, как сложить два объекта, или как добавить целое число  к такому объекту, например:

Box b1,b2;

Box b3 = b2 + b2;

b2 = b1 + 1;

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

Перегрузка операций выполняется путем объявления в классе функций со специальными именами:  operator<знак операции>. Например:

operator=(…)  //  перегрузка операции присваивания

operator+(…)  //  перегрузка операции сложения

operator*(…)  //  перегрузка операции умножения

operator==(…)  //  перегрузка операции сравнения

operator+=(…)  //  перегрузка операции +=

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

Функции перегрузки операций могут быть членами класса или функциями дружественными классу.

В следующем примере операция “==” перегружается функцией, которая является членом класса:

class Date  //  класс дат - (год,месяц,день)

{

private:

 int day,month,year;

public:

 bool operator== (const Date & d );// член функция

};

bool Date::operator== (const Date & d )

{

if((day == d.day) && (month == d.month) &&

  (year == d.year))

return true;

else

return false;

} 

Другим вариантом перегрузки операции является создание дружественной функции. Например:

bool operator ==( const Date & d1, const Date& d2); class Date

{

private:

 int day, month, year;

public:

 friend bool operator ==( const Date & d1, const Date& d2);

};

bool operator== (const Date & d1,const Date & d2)

{

if((d1.day == d2.day) && (d1.month == d2.month) &&

  (d1.year == d2.year))

return true;

else

return false;

} 

13.6. Перегрузка операции присваивания

Четвертой и последней функцией, которая добавляется классу автоматически, если она не объявлена явно, является перегруженная операция присваивания (operator=()). Эта функция вызывается всякий раз, когда один объект класса присваивается другому. Например:

Car carOne(5);

Car carTwo(3);

// ... другие инструкции

carTwo = carOne;

Здесь создаются два объекта, а затем значения одного объекта присваиваются другому. Работа перегруженной операции присваивания, которую добавляет компилятор по умолчанию, совпадает с тем, что делает копирующий конструктор, так же добавляемый к классу по умолчанию. Они оба выполняют присвоение членов данных одного объекта другому объекту. И здесь возникает та же самая проблема с указателями на свободную память, которая рассматривалась в разделе 13.3. Значения указателей нужно не присваивать друг другу, а выделять новую память и переписывать в нее значения из присваиваемого объекта. Кроме этого с операцией присваивания может возникать еще одна небольшая проблема: что если пользователь класса попытается присвоить объект самому себе? Например:

Car & anotherCar = carTwo;

carTwo = anotherCar;

Мало вероятно, что кто-то это будет делать, но операция присваивания должна учитывать такую сиуацию. Для того чтобы это не привело к нежелательным последствиям нужно проверять не являются ли объекты стоящие справа и слева от знака присваивания одним и тем же объектом, Если это один и тот же объект, то никакого присваивания выполнять не надо. Ниже приведен фрагмент кода, который показывает пример перегрузки операции присваивания для класса определенного в примере 13.2.

 1:Car Car::operator=(const Car & rhs)

2:{

3:  if (this == &rhs)

4:    return *this;

5:  delete wheels;

6:  delete weight;

7:  wheels = new int;

8:  weight = new float;

9:  *wheels = rhs.GetWheels();

10:  *weight = rhs.GetWeight();

11:  return *this;

12:}

В строке 3, выполняется проверка на совпадение текущего объекта, и переданного объекта. Для проверки используется указатель this. Как уже говорилось, в каждом классе есть такой указатель, который хранит адрес объекта, которому он принадлежит. Если эти адреса совпадают,

(*this == car), то это один и тот же объект.

13.7. Перегрузка операции сложения

В качестве примера бинарной операции, которая возвращает новый объект, рассмотрим операцию сложения (+). Для класса автомобилей Car можно выполнить перегрузку этой операции, так чтобы можно было записывать следующие инструкции:

Car carOne, carTwo, carThree;

carThree = carOne + carTwo;

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

1:Car Car::operator+ (const Car & cr)

2:{

3:  return Car(*wheels + cr.GetWheels(),

4:             *weight + cr.GetWeight());

5:}

Эта функция принимает в качестве параметра константную ссылку на объект класса Car, который является объектом, добавляемым к текущему. В функции создается в строке 3 новый временный объект, который и возвращается в качестве результата.

Глава 14. Производные классы

Классы являются типами данных, которые создаются программистом. Они, обычно, являться моделями понятий реального мира. Однако  ни одно понятие не существует изолировано. Между понятиями существует такие отношения, как агрегация и наследование. Агрегация это отношение целого и части, когда одно понятие входит в состав другого понятия. Агрегация рассматривалась раньше в разделе 7.10. Наследование это такое отношение между понятиями, когда одно понятие уточняет или обобщает  другое понятие. Понятие, которое уточняет другое понятие (базовое), называется производным понятием. Производное понятие повторяет структуру и поведение базового понятия, но вместе с тем добавляет к нему что-то новое. В языке С++ имеются средства, которые позволяют описать отношения наследования.

14.1. Наследования классов

Если какое-то понятие  является производным от базового понятия, то это значит, что оно включает все его особенности. Например, из того, что собака является млекопитающим можно определить, что она может двигаться и дышать воздухом, так как все млекопитающие могут двигаться и дышать воздухом по определению. Кроме этого, понятие “собака” добавляет другие особенности к базовому понятию, например, такие как способность, лаять, вилять хвостом и т.п. Можно дальше уточнять понятие “собака” и выделить их разновидности, например, “охотничьи собаки”, “сторожевые собаки” и т.д. В свою очередь “охотничьих собак” можно разделить на сеттеров, лаек и т.п. Все эти понятия иерархически связаны между собой. В соответствии с этой иерархией производные понятия наследую все свойства базового понятия. Например, сеттер наследует все свойства охотничьих собак, общие свойства собак и млекопитающих.

В языке C++ есть возможность отображать такие взаимосвязи с помощью определения наследования между классами. Можно создать новый класс, определяющий собак Dog, который является производным для класса Mammal, определяющего млекопитающих. При этом в новом классе не нужно явно указывать, что собаки могут двигаться, так как он является производным от класса Mammal, в котором такое поведение уже должно быть определено.

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

Если класс Dog является производным от класса Mammal, тогда класс Mammal является базовым классом для класса Dog. Производные классы являются развитием своих базовых классов. Так же как понятие “собака” добавляет какие-то особенности к понятию “млекопитающие”, точно так же класс Dog будет добавлять некоторые методы и свойства к классу Mammal. Производный класс, в свою очередь, может быть базовым для других классов.

Обычно базовый класс будет иметь более чем один производный класс. Так же как, собаки, кошки и лошади являются подвидами млекопитающих, то классы, которые их определяют, будут производными от одного базового класса Mammal.

14.2.Синтаксис наследования классов

Когда класс определяется, можно указать какой класс для него является базовым. Для этого нужно к первой строке определения класса добавить двоеточие, тип наследования (открытое или другое), и имя класса, от которого выполняется наследование. Например:

class Dog : public Mammal

Тип наследования в пособии не рассматривается и всегда используется открытое наследование - public. Класс, от которого выполняется наследование, должен быть определен ранее, в противном случае будет выдаваться ошибка компилятора. В примере 14.1 показано, как определить класс Dog, который является производным от класса Mammal. В строках 4-21, определяется класс Mammal. Отметим, что в данном примере класс Mammal не является производным от какого-либо другого класса. В реальности млекопитающие принадлежат, т.е. являются подвидом, класса животных. В программе, может быть представлена только часть знаний, которые известны о каком-нибудь конкретном понятии. Реальность слишком сложна, для ее полного отображения, поэтому иерархия классов в программе является только выборочным представлением доступных данных. Иерархия классов где-то должна начинаться. В данной программе она начинается с класса Mammal. В связи с этим, некоторые члены-данные, которые могли бы входить в базовый класс более высокого уровня, теперь объявлены в данном классе. Например, конечно все животные имеют возраст и вес, поэтому, если бы класс Mammal был производным от класса Animal, то он наследовал бы эти свойства. В связи с тем, что класс Mammal в данной программе не имеет базового класса, то рассматриваемые свойства включены в него.

Для того чтобы сделать программу простой и обозримой, только шесть методов объявлены в классе Mammal - четыре функции доступа, и функции, Speak(), и Sleep().

Класс Dog является производным от класса Mammal, что показано в строке 22. Каждый объект класса Dog будет иметь три свойства: itsAge, itsWeight и itsBreed. Отметим, что в определение класса Dog не включены члены-данные itsAge и itsWeight. Объекты класса Dog наследуют эти переменные от класса Mammal, вместе со всеми методами этого класса, за исключением конструкторов и деструктора.

14.3. Различие между закрытым и защищенным доступом

В примере 14.1 при объявлении класса  (строки 19 и 35) используется новый тип доступа к членам класса - защищенный, protected. Ранее при описании доступа к членам класса использовался тип private - закрытый. Однако закрытые  (private) члены базового класса являются недоступными объектам производного класса. Конечно, можно сделать переменные itsAge и itsWeight открытыми, но это не желательно. Тогда все классы получат прямой доступ к этим членам данным. Для производного класса требуется, чтобы члены базового класса были открытыми для него, но закрытыми для пользователей этого класса. Защищенный тип доступа как раз это и обеспечивает. Защищенные члены ведут себя как открытые по отношению к членам производного класса и как закрытые по отношению к другим функциям.

Таким образом, в языке имеется три типа доступа: открытый (public), защищенный (protected) и закрытый (private). Если в некоторой функции объявлен объект класса, то в ней имеется доступ ко всем  открытым членам-данным и членам-функциям. Члены функции класса, в свою очередь, имеют доступ ко всем закрытым свойствам и методам своего собственного класса и ко всем защищенным  свойствам и методам любого  базового для них класса.

В данном примере функция Dog::WagTail() имеет доступ к закрытой переменной своего класса itsBreed и к защищенным свойствам базового класса Mammal.

Даже если бы класс Mammal не был напрямую базовым классом для класса Dog, а имелись бы промежуточные классы (например, класс “домашних животных” DomesticAnimals), то все равно в функциях класса Dog был бы доступ к защищенным членам класса Mammal, если предположить, что все промежуточные классы используют открытый тип наследования. В примере  14.1 показано, как создать объекты класса Dog и работать с ним.

Пример 14.1. Описание производных классов и работа с ними.

1:#include <iostream>

2:using namespace std;

3:enum BREED { SETTER, DOBERMAN, BULDOG};

4:class Mammal

5:{

6:public:

7://конструктор и деструктор

8:  Mammal():itsAge(2), itsWeight(5){}

9:  ~Mammal(){}

10://методы доступа

11:  int GetAge()const { return itsAge; }

12:  void SetAge(int age) { itsAge = age; }

13:  int GetWeight() const { return itsWeight; }

14:  void SetWeight(int weight) {itsWeight=weight;}

15://другие методы

16:  void Speak()const { cout << "Mammal sound!\n"; }

17:  void Sleep()const { cout << "I'm sleeping.\n"; }

18:protected:

19:  int itsAge;

20:  int itsWeight;

21:};

22:class Dog : public Mammal

23:{

24:public:

25://конструктор и деструктор

26:  Dog():itsBreed(SETTER){}

27:  ~Dog(){}

28://методы доступа

29:  BREED GetBreed() const { return itsBreed; }

30:  void SetBreed(BREED breed) { itsBreed = breed; }

31://другие методы

32:  void WagTail() {cout << "Tail wagging...\n";}

33:  void BegFood(){cout<<"Begging for food…\n";}

34:private:

35:  BREED itsBreed;

36:};

37:int main()

38:{

39:  Dog Rex;

40:  Rex.Speak();

41:  Rex.WagTail();

42:  cout<<"Rex is "<< Rex.GetAge()<<" years old\n";

43:  return 0;

44:}

Результат работы программы:

Mammal sound!

Tail wagging...

Rex is 2 years old

Класс Mammal определен в строках 4-21  (для простоты все функции класса сделаны встроенными). В строках 22-36, определен класс Dog, производный от класса Mammal. Таким образом, в соответствии с определением, все объекты класса Dog имеют свойства itsAge, itsWeight (из базового класса) и itsBreed(из производного класса).

В строке 39 объявлен объект класса Dog - Rex. Он имеет все свойства класса Mammal и свойства класса Dog. Поэтому, Rex может не только выполнить функцию WagTail()(функция класса Dog), но так же и как выполнить функции Speak() и Sleep()(функция класса Mammal).

14.4. Конструкторы и деструкторы

Объекты класса Dog являются также объектами класса Mammal. Это является сущностью отношения наследования. Когда создается объект Rex, то вначале вызывается его конструктор для базового класса Mammal. И только затем вызывается конструктор класса Dog для завершения создания объекта этого класса. Так как при объявлении объекта Rex ему не передаются параметры,  то вызывается конструктор по умолчанию. Объект не будет существовать до тех пор, пока он не будет полностью построен, то есть не будет создана его часть класса Mammal и класса Dog. Поэтому, должны вызываться оба конструктора.

Когда объект Rex удаляется, то вначале вызывается деструктор класса Dog, а затем деструктор базового класса Mammal. Каждому деструктору дается возможность выполнить завершающие действия для его класса. Пример 14.2 показывает последовательность вызова конструкторов и деструкторов при создании и уничтожении объектов производного класса.

Пример 14.2. Вызов конструкторов и деструкторов.

1:#include <iostream>

2:using namespace std;

3: enum BREED { SETTER, DOBERMAN, BULDOG};

4:class Mammal

5:{

6:public:

7://конструктор и деструктор

8:  Mammal();

9:  ~Mammal();

10://методы доступа

11:  int GetAge()const { return itsAge; }

12:  void SetAge(int age) { itsAge = age; }

13:  int GetWeight() const { return itsWeight; }

14:  void SetWeight(int weight) {itsWeight=weight;}

15://другие методы

16:  void Speak()const { cout << "Mammal sound!\n"; }

17:  void Sleep()const { cout << "I'm sleeping.\n"; }

18:protected:

19:  int itsAge;

20:  int itsWeight;

21:};

22:class Dog : public Mammal

23:{

24:public:

25://конструктор и деструктор

26:  Dog();

27:  ~Dog();

28://методы доступа

29:  BREED GetBreed() const { return itsBreed; }

30:  void SetBreed(BREED breed) { itsBreed = breed; }

31://другие методы

32:  void WagTail() { cout << "Tail wagging...\n"; }

33:  void BegFood(){cout<<"Begging for food...\n";}

34:private:

35:  BREED itsBreed;

36:};

37:Mammal::Mammal():

38:    itsAge(1),

39:    itsWeight(5)

40:{

41:  cout << "Mammal constructor...\n";

42:}

43:Mammal::~Mammal()

44:{

45:  cout << "Mammal destructor...\n";

46:}

47:Dog::Dog():

48:  itsBreed(SETTER)

49:{

50:  cout << "Dog constructor...\n";

51:}

52:Dog::~Dog()

53:{

54:  cout << "Dog destructor...\n";

55:}

56:int main()

57:{

58:  Dog Rex;

59:  Rex.Speak();

60:  Rex.WagTail();

61:  cout<<"Rex is "<< Rex.GetAge()<<" years old\n";

62:  return 0;

63:}

Результат работы программы:

Mammal constructor...

Dog constructor...

Mammal sound!

Tail wagging...

Rex is 1 years old

Dog destructor...

Mammal destructor...

Пример 14.2 во многом походит на пример 14.1, за исключением того, что конструкторы и деструкторы теперь выводят на экран сообщения, когда выполняются. Вначале вызывается конструктор класса Mammal, а зате