ИСПОЛЬЗОВАНИЕ КОММЕНТАРИЕВ ДЛЯ УЛУЧШЕНИЯ ЧТЕНИЯ ВАШИХ ПРОГРАММ
По мере усложнения ваших программ количество содержащихся в них операторов может сделать программы слишком трудными для понимания. Поскольку другим программистам может потребоваться понять и, возможно, изменить ваши программы, вам следует делать программы более удобочитаемыми. Способы улучшения удобочитаемости программ включают:
Использование понятных по смыслу имен переменных, описывающих их применение.
Использование подходящих отступов и выравнивания (см. урок 7).
Использование пустых строк для разделения несвязанных операторов.
Использование комментариев, которые объясняют работу программы.
При создании программ вы можете поместить в исходном файле замечания, которые объясняют работу программы. Такие замечания (называемые комментариями) не только помогают другим программистам понять вашу программу, но могут напомнить, почему программа содержит определенные операторы, если вы ее посмотрите через несколько месяцев. Для размещения комментария в своих программах на C++ просто поставьте два знака прямого слеша (//), как показано ниже:
// Это комментарий
Когда компилятор C++ встречает двойной слеш, он игнорирует весь оставшийся на этой строке текст. По крайней мере размещайте в начале каждой программы комментарии, которые указывают, кто написал программу, когда и почему:
// Программа: BUDGET.CPP
// Программист: Kris Jamsa
// дата создания: 1-10-96
//
// Цель: Ежемесячная информация о бюджете.
При выполнении вашей программой различных процессов вам следует поместить до или после определенных операторов комментарии, которые объясняют их назначение. Например, рассмотрим следующий оператор присваивания:
distance_to_the_moon = 238857; // Расстояние в милях
Комментарий справа от оператора присваивания обеспечивает дополнительную информацию всем, кто читает программу. У начинающих программистов часто возникают проблемы, связанные с тем, что и когда комментировать. Как правило, вы включаете немного комментариев в ваши программы. Поэтому убедитесь, что они полезны. Следующие комментарии не дают дополнительной информации программистам, читающим код:
аде =32; // Присвоить 32 переменной age
salary = 25000.75; // Присвоить 25000.75 переменной salary
Цель использования комментариев заключается в том, чтобы объяснить работу программы.
Добавление комментариев в ваши программы
При создании программ включайте комментарии, которые объясняют работу программы. Если другим программистам понадобится изменить вашу программу, они смогут воспользоваться комментариями, чтобы понять поведение программы. Обычно в программах на C++ комментарий начинается с двойного слеша:
// Это комментарий C++
Когда компилятор C++ встречает двойной слеш, он игнорирует весь текст (оставшийся в текущей строке), который следует за слешем. Хорошие программы должны быть легки для чтения и понимания. Комментарии улучшают удобочитаемость вашей программы.
Замечание: В дополнение к использованию комментариев для улучшения удобочитаемости своей программы вам следует использовать пустые строки для разделения несвязанных операторов. Когда компилятор C++ встречает пустую строку, он просто пропускает ее.
Использование констант и макрокоманд
Чтобы улучшить удобочитаемость программы, C++ поддерживает именованные константы и макрокоманды. Например, используя именованную константу, вы можете заменить цифровое значение, такое как 50, внутри вашего исходного кода смысловой константой, такой как CLASS_SIZE. Когда ругой программист читает ваш код, он не сможет предположить, что означает цифровое значение 50. Если же вместо этого он каждый раз будет видеть СLASS_SIZE, то он поймет, что это значение соответствует числу студентов в классе. Аналогично, используя макрокоманды, ваши программы могут заменить сложные выражения типа
result = (х*у-3) * (х*у-3) * (х*у-3);
вызовом функции с именем CUBE, как показано ниже:
result = CUBE(x*y-3);
В данном случае макрокоманда не только улучшает удобочитаемость вашего кода, но и упрощает ваш оператор, уменьшая вероятность ошибки. Этот урок рассматривает именованные константы и макрокоманды более подробно. К концу данного урока вы освоите следующие основные концепции:
Чтобы сделать программы легче для чтения, программисты часто заменяют цифровые значения более понятными по смыслу именованными константами.
Используя именованные константы в вашей программе вместо цифровых значений, вы можете сделать свои программы более легкими для изменения в будущем.
C++ позволяет программам заменять выражения смысловыми именами макрокоманд.
До компиляции программы компилятор C++ запускает специальную программу, называемую препроцессором, чтобы заменить каждую константу или макрокоманду соответствующим значением.
Макрокоманды выполняются быстрее, чем функции, но увеличивают размер выполняемой программы.
Большинство компиляторов C++ имеют предопределенные константы и макрокоманды, которые вы можете использовать в своих программах.
ИСПОЛЬЗОВАНИЕ МАКРОКОМАНД ПРЕДОСТАВЛЯЕТ БОЛЬШУЮ ГИБКОСТЬ
Вы можете использовать макрокоманды в своих программах различным образом. Однако имейте в виду, что цель использования макрокоманд состоит в упрощении кодирования и улучшении восприятия ваших программ. Следующая программа MACDELAY.CPP иллюстрирует гибкость макрокоманд. Кроме того, эта программа поможет вам лучше представить, как препроцессор заменяет имя макрокоманды соответствующими операторами:
#include iostream.h
#define delay(х)
{ \
cout "Задержка на " х endl; \
for (long int i=0; i х; i++) \
; \
}
void main (void)
{
delay(l00000L);
delay(200000L);
delay(300000L);
}
В данном случае, поскольку определение макрокоманды занимает несколько строк, это определение помещает один символ обратного слэша (\) в конце каждой строки, которая имеет продолжение. Когда препроцессор встретит ссылку на макрокоманду, он заменит эту ссылку операторами, которые появляются в определении макрокоманды.
ИСПОЛЬЗОВАНИЕ ОБЩИХ И ЧАСТНЫХ ЭЛЕМЕНТОВ КЛАССА
Следующая программа INFOHIDE.CPP иллюстрирует использование общих и частных элементов класса. Программа определяет объект типа employee как показано ниже:
class employee
{
public:
int assign_values(char *, long, float);
void show_employee(void);
int change_salary(float);
long get_id(void);
private:
char name [64] ;
long employee_id;
float salary;
}
Как видите, класс защищает все свои элементы данных, объявляя их частными. Для доступа к элементам данных программа должна использовать интерфейсные функции. Ниже приведена реализация программы INFOHIDE.CPP:
#include iostream.h
#include string.h
class employee
{
public:
int assign_values(char *, long, float);
void show_employee(void);
int change_salary(float);
long get_id(void);
private:
char name [64];
long employee_id;
float salary;
);
int employee::assign_values(char *emp_name, long emp_id, float emp_salary)
{
strcpy(name, emp_name);
employee_id = emp_id;
if (emp_salary 50000.0)
{
salary = emp_salary;
return(0); // Успешно
}
else
return(-1); // Недопустимый оклад }
void employee::show_employee(void)
{
cout "Служащий: " name endl;
cout "Номер служащего: " employee_id endl;
cout "Оклад: " salary endl;
}
int employee::change_salary(float new_salary)
{
if (new_salary 50000.0)
{
salary = new_salary;
return(0); // Успешно } else return(-1); // Недопустимый оклад }
long employee::get_id(void)
{
return(employee_id) ;
}
void main(void)
{
employee worker;
if (worker.assign_values("Happy Jamsa", 101, 10101.0) == 0)
{
cout "Служащему назначены следующие значения" endl;}
worker.show_employee();
if (worker.change_salary(35000.00) == 0)
{
cout "Назначен новый оклад" endl;
worker.show_employee();
}
}
else
cout "Указан недопустимый оклад" endl;
}
Выберите время, чтобы исследовать операторы программы более подробно. Несмотря на то что программа достаточно длинна, ее функции на самом деле очень просты. Метод assign_values инициализирует частные данные класса. Метод использует оператор if, чтобы убедиться, что присваивается допустимый оклад. Метод show_employee в данном случае выводит частные элементы данных. Методы change_salary и get_id представляют собой интерфейсные функции, обеспечивающие программе доступ к частным данным. После успешной компиляции и запуска этой программы отредактируйте ее и попытайтесь обратиться напрямую к частным элементам данных, используя оператор точку внутри main. Так как вы не можете напрямую обратиться к частным элементам, компилятор сообщит о синтаксических ошибках.
Что такое интерфейсные функции
Для снижения количества возможных ошибок ограничивайте доступ программ к данным класса, определяя элементы данных класса как частные. Таким образом, программа не сможет обратиться к элементам данных класса, используя оператор точку. Вместо этого класс должен определять интерфейсные функции, с помощью которых программа может присваивать значения частным элементам. Интерфейсные функции в свою очередь, могут исследовать и скорректировать значения, которые программа пытается присвоить.
Использование операции НЕ в C++
Вы уже знаете, что, когда программа проверяет определенное условие, в ряде случаев она должна выполнить некоторые операторы, если это условие истинно. С другой стороны, вам может потребоваться, чтобы программа выполнила операторы, если условие является не истинным. Операция C++ НЕ — восклицательный знак (!) — позволяет вашим программам проверить, является ли условие не истинным. Например, следующий оператор проверяет, нет ли у пользователя собаки:
if (! user_owns_a_dog)
cout "вы должны купить собаку" endl;
Операция НЕ превращает ложь в истину, а истину в ложь. Например, предположим, что у пользователя нет собаки. Следовательно, переменная user_owns_a_dog должна содержать значение 0. Если C++ оценивает условие с Помощью операции НЕ, он использует текущее значение переменной (0) и применяет операцию НЕ. Операция НЕ превращает значение 0 в 1 (истину). Таким образом, полное условие превращается в истину и выполняется соответствующий оператор.
Следующая программа USE_NOT.CPP иллюстрирует использование операции НЕ:
#include iostream.h
void main(void)
{
int user_owns_a_dog = 0;
int user_owns_a_cat = 1;
if (! user_owns_a_dog)
cout "Вы должны купить собаку" endl;
if (! user_owns_a_cat)
cout "Вы должны купить кошку" endl;
}
Как и раньше, экспериментируйте со значениями, присваиваемыми переменным user_owns_a_dog и user_owns_a_cat, и обратите внимание на поведение программы. По мере усложнения ваших программ вы будете постоянно использовать операцию НЕ. Например, ваша программа может продолжать повторять обработку, пока не встретит конец файла.
Использование логических операций C++
Если вы указываете условия в программах, то иногда эти условия будут состоять из нескольких частей. Например, ваша программа может проверять, имеет ли служащий почасовую оплату и работал ли он более 40 часов на этой неделе. Если для истинности условия каждая из двух его частей должна быть истиной, вам следует использовать операцию C++ И (). При ,использовании операции И группируйте каждое условие внутри круглых скобок, а затем оба условия заключите в еще одну пару круглых скобок, как показано ниже:
if ((employee_pay == hourly) (employee_hours 40))
оператор;
Когда необходимо, чтобы условие было истинным, если только одна из его частей истинна, вам следует использовать операцию C++ ИЛИ (|[). Например, следующее условие проверяет, есть ли у пользователя машина или мотоцикл:
if ((vehicle == car) II (vehicle == motorcycle))
оператор;
Как и ранее, программа группирует каждое условие внутри скобок. В ряде случаев вам потребуется, чтобы ваши программы выполняли оператор, если условие является не истинным. В таких случаях следует использовать операцию C++ НЕ (!). Операция НЕ превращает истину в ложь, а ложь в истину.
Операции C++ И, ИЛИ и НЕ представляют собой логические операции.
ИСПОЛЬЗОВАНИЕ ОПЕРАТОРА ГЛОБАЛЬНОГО РАЗРЕШЕНИЯ ДЛЯ ЭЛЕМЕНТОВ КЛАССА
Если вы рассмотрите функции в программе INFOHIDE.CPP, вы обнаружите, что имена параметров функции часто предваряются символами етр_, как показано ниже:
int employee::assign_values(char *emp_name, long emp_id, float emp_salary)
Символы етр_ использовались, чтобы избежать конфликта между именами параметров и именами элементов класса. Если подобный конфликт имен всe же происходит, вы можете разрешить его, предваряя имена элементов класса именем класса и оператором глобального разрешения (::). Следующая функция использует оператор глобального разрешения и имя класса перед именем элементов класса. Исходя из этого, любой читающий эти операторы поймет, какие имена соответствуют классу employee:
int employee::assign_values(char *name, long employee_id, float salary)
{
strcpy(employee::name, name) ;
employee::employee_id = employee_id;
if (salary 50000.0)
{
employee::salary = salary;
return(0); // Успешно } else
return(-1); // Недопустимый оклад
}
При создании функций, работающих с элементами класса, вам следует использовать имя класса и оператор глобального разрешения, чтобы таким образом избежать конфликта имен.
Использование оператора глобального разрешения для указания элементов класса
При создании функций-элементов класса возможны ситуации, когда имя локальной переменной, которое вы используете внутри функции, конфликтует с именем элемента класса. По умолчанию имя локальной переменной будет переопределять имя элемента класса. Когда происходит подобный конфликт имен, функция может использовать имя класса и оператор глобального разрешения для доступа к элементам класса, как показано ниже:
class_naine: :member_name = some_value;
ИСПОЛЬЗОВАНИЕ ОПЕРАТОРА new
Оператор C++ new позволяет вашим программам распределять память во время выполнения. Для использования оператора new вам необходимо указать количество байтов памяти, которое требуется программе. Предположим, например, что вашей программе необходим 50-байтный массив. Используя оператор new, вы можете заказать эту память, как показано ниже:
char *buffer = new char[50];
Говоря кратко, если оператор new успешно выделяет память, он возвращает указатель на начало области этой памяти. В данном случае, поскольку программа распределяет память для хранения массива символов, она присваивает возвращаемый указатель переменной, определенной как указатель на тип char. Если оператор new не может выделить запрашиваемый вами объем памяти, он возвратит NULL-указатель, который содержит значение 0. Каждый раз, когда ваши программы динамически распределяют память с использованием оператора new, они должны проверять возвращаемое оператором new значение, чтобы определить, не равно ли оно NULL.
Зачем необходимо динамически распределять память с использованием new
Многие программы интенсивно используют массивы для хранения множества значений определенного типа. При проектировании своих программ программисты обычно пытаются объявить массивы с размерами, достаточными для удовлетворения будущих потребностей программы. К сожалению, если случится так, что потребности программы когда-нибудь превысят подобные ожидания программиста, то кому-то придется редактировать и перекомпилировать такую программу.
Вместо редактирования и перекомпилирования программ, которые просто запрашивают память с запасом, вам следует создавать свои программы таким образом, чтобы они распределяли требуемую им память динамически во время выполнения, используя оператор new. В этом случае ваши программы могут адаптировать использование памяти в соответствии с вашими изменившимися потребностями, избавляя вас от необходимости редактировать и перекомпилировать программу.
Например, следующая программа USE_NEW.CPP использует оператор new для получения указателя на 100-байтный массив:
#include iostream.h
void main(void)
{
char *pointer = new char[100];
if (pointer != NULL) cout "Память успешно выделена" endl;
else cout "Ошибка выделения памяти" endl;
}
Как видите, программа сразу проверяет значение, присвоенное оператором new переменной-указателю. Если указатель содержит значение NULL, значит new не смог выделить запрашиваемый объем памяти. Если же указатель содержит не NULL, следовательно, new успешно выделил память и указатель содержит адрес начала блока памяти.
Если new не может удовлетворить запрос на память, он возвратит NULL
При использовании оператора new для выделения памяти может случиться так, что ваш запрос не может быть удовлетворен, поскольку нет достаточного объема свободной памяти. Если оператор new не способен выделить требуемую память, он присваивает указателю значение NULL. Проверяя значение указателя, как показано в предыдущей программе, вы можете определить, был ли удовлетворен запрос на память. Например, следующий оператор использует new для распределения памяти под массив из 500 значений с плавающей точкой:
float *array = new float[100];
Чтобы определить, выделил ли оператор new память, ваша программа должна сравнить значение указателя с NULL, как показано ниже:
if (array != NULL) cout "Память выделена успешно" endl;
else cout "new не может выделить память" endl;
Предыдущая программа использовала оператор new для выделения 100 байт памяти. Поскольку эта программа "жестко закодирована" на объем требуемой памяти, возможно, вам потребуется ее редактировать и перекомпилировать, если возникнет необходимость, чтобы программа выделила меньше или больше памяти. Как уже кратко обсуждалось, одна из причин для динамического распределения памяти состоит в том, чтобы избавиться от необходимости редактировать и перекомпилировать программу при изменении требований к объему памяти. Следующая программа ASK_MEM.CPP запрашивает у пользователя количество байт памяти, которое необходимо выделить, и затем распределяет память, используя оператор new:
#include iostream.h
void main(void)
{
int size;
char *pointer;
cout " Введите размер массива, до 30000: ";
cin size;
if (size = 30000)
{
pointer = new char[size];
if (pointer != NULL) cout "Память выделена успешно" endl;
else cout "Невозможно выделить память" endl;
}
}
Когда ваши программы используют оператор new для динамического распределения памяти, то вполне вероятно, что они сами знают, сколько памяти необходимо выделить. Например, если программа распределяет память для хранения информации о служащих, она, возможно, сохранила количество служащих в файле. Следовательно, при запуске она может прочитать количество служащих из файла, а затем выделить соответствующее количество памяти.
Следующая программа NOMEMORY.CPP выделяет каждый раз память для 10000 символов до тех пор, пока оператор new не сможет больше выделить память из свободной памяти. Другими словами, эта программа удерживает выделенную память, пока не использует всю доступную свободную память. Если программа успешно выделяет память, она извещает об этом сообщением. Если память больше не может быть выделена, программа выводит сообщение об ошибке и завершается:
#include iostream.h
void main(void)
{
char * pointer;
do
{
pointer = new char[10000];
if (pointer != NULL) cout "Выделено 10000 байт" endl;
else cout "Больше нет памяти" endl;
} while (pointer 1= NULL);
}
Замечание: Если выработаете в среде MS-DOS, то, возможно, будете удивлены тем, что свободная память исчерпается после того, как программа выделит 64 Кбайт, Большинство работающих в MS-DOS компиляторов C++ по умолчанию используют малую модель памяти, которая обеспечивает только 64 К6aйm свободной памяти. Аналогично, если вы используете среду MS-DOS, то наибольшая область памяти, к которой могут обратиться ваши программы, может быть ограничена 64Кбайт.
О свободной памяти
Каждый раз при запуске вашей программы компилятор C++ устанавливает отдельную область неиспользуемой памяти, которая называется свободной памятью. Используя оператор new, ваша программа может выделить память из этой свободной памяти во время выполнения. Используя свободную память для распределения требуемой памяти, ваши программы не стеснены фиксированными размерами массивов. Размер свободной памяти может изменяться в зависимости от вашей операционной системы и модели памяти компилятора. Если увеличивается количество динамической памяти, требуемой вашими программами, вам необходимо убедиться, что вас не сдерживают ограничения свободной памяти вашей системы.
ИСПОЛЬЗОВАНИЕ ОПЕРАТОРА switch
Как вы уже знаете, комбинируя серии операторов if-else, программы могут проверять несколько условий. В предыдущей программе использовались операторы if-else, чтобы определить, находятся ли тестовые очки в данном диапазоне значений. В тех случаях, когда вашим программам необходимо осуществить проверку определенных значений, они могут использовать оператор C++ switch.
Если вы используете оператор switch, вы должны указать условие и затем один или несколько вариантов (case), которые программа попытается сопоставить с условием. Например, следующая программа SWITCH.CPP использует оператор switch для вывода сообщения, основываясь на текущей оценке студента:
#include iostream.h
void main(void)
{
char grade = 'В';
switch (grade)
{
case 'A': cout "Поздравляем, вы получили А" endl; break;
case 'В': cout "Хорошо, у вас В" endl; break;
case 'С': cout "У вас всего лишь С" endl; break;
case 'D': cout "Плохо, у вас D" endl; break;
default: cout "Ужасно! Учите лучше!" endl; break;
}
}
Оператор switch состоит из двух частей. Первая часть оператора switch представляет собой условие, которое появляется после ключевого слова switch. Вторая часть представляет собой возможные варианты соответствия. Когда программа встречает оператор switch, она сначала исследует условие, а затем пытается найти среди возможных вариантов тот, который соответствует условию. Если программа находит соответствие, выполняются указанные операторы. Например, в предыдущей программе выбор варианта 'В' соответствует условию. Таким образом, программа выводит сообщение, что пользователь получил В. Найдите время для эксперимента с этой программой, изменяя оценку и наблюдая поведение программы. Если же ни один из указанных вариантов не соответствует условию, то выполняется вариант default.
Обратите внимание на использование оператора break в каждом варианте предыдущей программы. Оказывается, если C++ встречает вариант, соответствующий условию оператора switch, то он подразумевает, что все последующие варианты тоже соответствуют условию. Оператор break указывает C++ завершить текущий оператор switch и продолжить выполнение программы с первого оператора, следующего за оператором switch. Если вы удалите операторы break из предыдущей программы, то программа выведет не только требуемое сообщение, но и сообщение для всех последующих вариантов (потому что если один вариант является истинным, то и все последующие варианты в C++ рассматриваются как истинные).
ИСПОЛЬЗОВАНИЕ ОПЕРАТОРА throw ДЛЯ ГЕНЕРАЦИИ ИСКЛЮЧИТЕЛЬНОЙ СИТУАЦИИ
Сам C++ не генерирует исключительные ситуации. Их генерируют ваши программы, используя оператор C++ throw. Например, внутри функции file_copy программа может проверить условие возникновения ошибки и сгенерировать исключительную ситуацию:
void file_copy(char *source, char *target)
{
char line[256];
ifstream input_file(source);
ofstream output_file(target);
if (input_file.fail())
throw(file_open_error);
else
if (output_file.fail()) throw(file_open_error);
else
{
while ((! input_file.eof()) (! input_file.fail()))
{
input_file.getline(line, sizeof(line)) ;
if (! input_file.fail()) output_file line endl;
else throw(file_read_error);
if (output_file.fail()) throw (file_write_error) ;
}
}
}
Как видите, программа использует оператор throw для генерации определенных исключительных ситуаций.
Как работают исключительные ситуации
Когда вы используете исключительные ситуации, ваша программа проверяет условие возникновения ошибки и, если необходимо, генерирует исключительную ситуацию, используя оператор throw. Когда C++ встречает оператор throw, он активизирует соответствующий обработчик исключительной ситуации (функцию, чьи операторы вы определили в классе исключительной ситуации). После завершения функции обработки исключительной ситуации C++ возвращает управление первому оператору, который следует за оператором try, разрешившим обнаружение исключительной ситуации. Далее, используя операторы catch, ваша программа может определить, какая именно исключительная ситуация возникла, и отреагировать соответствующим образом.
ИСПОЛЬЗОВАНИЕ ОПЕРАТОРОВ ЯЗЫКА АССЕМБЛЕРА
Как вы знаете из урока 1, программисты могут создавать программы, используя широкий спектр языков программирования. Затем компилятор преобразует операторы программы в машинный код (нули и единицы), который понимает компьютер. Каждый тип компьютеров поддерживает промежуточный язык, называемый языком ассемблера, который попадает в категорию между машинным языком и языком программирования, таким как C++.
Язык ассемблера использует другие символы для представления инструкций машинного языка. В зависимости от назначения ваших программ, возможно, вам потребуется выполнить операции низкого уровня, для которых необходимо использовать операторы языка ассемблера. В таких случаях вы можете использовать оператор C++ asm для встраивания операторов языка ассемблера в программу. Большинство создаваемых вами программ не потребуют операторов языка ассемблера. Следующая программа USE_ASM.CPP использует оператор asm, чтобы вставить операторы языка ассемблера, необходимые для озвучивания динамика компьютера в среде MS-DOS:
#include iostream.h
void main(void)
{
cout "Сейчас будет звонить!" endl;
asm
{
MOV AH,2
MOV DL,7
INT 21H
}
cout "Есть!" endl;
}
Как видите, используя оператор asm, программа комбинирует C++ и операторы языка ассемблера.
ИСПОЛЬЗОВАНИЕ ОТСТУПОВ ДЛЯ УЛУЧШЕНИЯ УДОБОЧИТАЕМОСТИ ВАШЕЙ ПРОГРАММЫ
Рассматривая программы, представленные в этой главе, вы увидите, что в них применяются отступы перед операторами, которые следуют за if, else или левой скобкой. Сдвигая подобным образом свои операторы на одну или две позиции, вы упрощаете процесс чтения ваших программ, выделяя связанные группы операторов, как показано ниже:
if (test_score = 90)
{
cout "Поздравляем, вы получили А!" endl;
cout "Ваши тестовые очки были " test_score endl;
}
else
{
cout "Вы должны работать усерднее!" endl;
cout "Вы потеряли " 100 - test_score " очков " endl;
}
При создании программ используйте подобные отступы, чтобы программы были более удобочитаемыми. Отступы нужны не C++, а программистам, которые будут читать и пытаться понять ваш код.
Использование шаблонов функций
При создании функций иногда возникают ситуации, когда две функции выполняют одинаковую обработку, но работают с разными типами данных (например, одна использует параметры типа int, а другая типа float). Вы уже знаете из урока 13, что с помощью механизма перегрузки функций можно использовать одно и то же имя для функций, выполняющих разные действия и имеющих разные типы параметров. Однако, если функции возвращают значения разных типов, вам следует использовать для них уникальные имена (см. примечание к уроку 13). Предположим, например, что у вас есть функция с именем тах, которая возвращает максимальное из двух целых значений. Если позже вам потребуется подобная функция, которая возвращает максимальное из двух значений с плавающей точкой, вам следует определить другую функцию, например fmax. Из этого урока вы узнаете, как использовать шаблоны C++ для быстрого создания функций, возвращающих значения разных типов. К концу данного урока вы освоите следующие основные концепции:
Шаблон определяет набор операторов, с помощью которых ваши программы позже могут создать несколько функций.
Программы часто используют шаблоны функций для быстрого определения нескольких функций, которые с помощью одинаковых операторов работают с параметрами разных типов или имеют разные типы возвращаемых значений.
Шаблоны функций имеют специфичные имена, которые соответствуют имени функции, используемому вами в программе.
После того как ваша программа определила шаблон функции, она в дальнейшем может создать конкретную функцию, используя этот шаблон для задания прототипа, который включает имя данного шаблона, возвращаемое функцией значение и типы параметров.
В процессе компиляции компилятор C++ будет создавать в вашей программе функции с использованием типов, указанных в прототипах функций, которые ссылаются на имя шаблона.
Шаблоны функций имеют уникальный синтаксис, который может быть на первый взгляд непонятен. Однако после создания одного или двух шаблонов вы обнаружите, что реально их очень легко использовать.
Использование шаблонов классов
Из урока 29 вы узнали, как в C++ использовать шаблоны функций для создания общих, или типонезависимых, функций. Определяя шаблоны функций вы заставляете компилятор C++ создавать в случае необходимости функции, которые отличаются типом возвращаемого значения или типами параметров. Если возникает необходимость создавать подобные функции, отличающиеся только используемыми типами, то может возникнуть необходимость и создания общих классов. А если так, то ваши программы могут определять шаблоны классов. В этом уроке рассмотрены действия вашей программы, необходимые для объявления и дальнейшего использования шаблонов классов. К концу данного урока вы освоите следующие основные концепции:
Используя ключевое слово template и символы типов (например, Т, T1 и Т2) ваши программы могут создать шаблон класса — определение шаблона класса может использовать эти символы для объявления элементов данных, указания типов параметров и возвращаемого значения функций-элементов и т.д.
Для создания объектов класса с использованием шаблонов ваши программы просто ссылаются на имя класса, за которым внутри угловых скобок следуют типы (например, int, float), каждому из которых компилятор назначает символы типов и имя переменной.
Если у класса есть конструктор, с помощью которого вы инициализируете элементы данных, вы можете вызвать этот конструктор при создании объекта с использованием шаблона, например class_nameint,floatvalues(200);.
Если компилятор C++ встречает объявление объекта, он создает класс из шаблона, используя соответствующие типы.
Как и в случае с шаблонами функций, шаблоны классов на первый взгляд могут показаться достаточно сложными, однако если вы хоть раз создали и использовали пару шаблонов классов, то обнаружили, что дело это вполне простое.
Использование символа-заполнителя
Если вы используете манипулятор setw или функцию cout.width для управления шириной вывода, cout будет помещать пробелы до (или после для выровненных влево) значений, как это и требуется. В зависимости от назначения вашей программы вы, возможно, захотите использовать символ, отличный от пробела. Предположим, например, что ваша программа создает такую таблицу:
Таблица информации
Профиль компании................................................ 10
Доходы и убытки компании...................................11
Члены правления компании..................................13
В данном случае вывод предваряет номера страниц точками. Функция cout.fill позволяет вам указать символ, который cout будет использовать для заполнения пустого пространства. Следующая программа COUTFILL.CPP создает таблицу, подобную приведенной выше:
#include iostream.h
#include iomanip.h
void main(void)
{
cout "Таблица информации" endl;
cout.fill (' . ');
cout "Профиль компании" setw(20) 10 endl;
cout "Доходы и убытки компании" setw(12) 11 endl;
cout "Члены правления компании" setw(14) 13 endl;
}
Если вы однажды выбрали символ-заполнитель с помощью cout.fill, он будет оставаться действительным, пока вы не измените его повторным вызовом cout.fill.
ИСПОЛЬЗОВАНИЕ СПЕЦИАЛЬНЫХ СИМВОЛОВ ВЫВОДА
Все программы, созданные вами до сих пор, отображали свой вывод в виде одной строки. Однако большинство программ, которые вы создадите в дальнейшем, будут отображать несколько строк вывода. Например, предположим, что вы пишете программу, которая будет выводить адреса на экран. Вероятно, вы захотите, чтобы адреса появлялись в виде нескольких строк.
Если необходимо переместить курсор в начало следующей строки, можно поместить символ новой строки (\n) в выходной поток. В C++ вам предоставляется два разных способа генерации новой строки. Во-первых, вы можете поместить символы \n внутри символьной строки. Например, следующая программа TWOLINES.CPP отображает свой вывод в виде двух строк, используя символ новой строки:
#include iostream.h
void main(void)
{
cout "Это строка один\nЭто строка два";
}
Когда вы откомпилируете и запустите эту программу, символ новой строки обеспечит вывод двух строк, как показано ниже:
С:\ TWOLINES ENTER
Это строка один
Это строка два
Если вы не выводите символьную строку, можете поместить символ новой строки внутри одинарных кавычек. Например, следующая программа NEWLINES.CPP выводит числа 1, 0, 0 и 1, каждое на своей собственной строке:
#include iostream.h
void main(void)
{
cout 1 '\n' 0 '\n' 0 '\n' 1;
}
В дополнение к использованию символа новой строки для продвижения курсора в начало следующей строки ваши программы могут использовать символ endl (конец строки). Следующая программа ENDL.CPP иллюстрирует использование endl для продвижения курсора в начало новой строки:
#include iostream.h
void main(void)
{
cout "А теперь..." endl
"Учимся программировать на языке C++";
}
Как и ранее, когда вы откомпилируете и запустите эту программу, на экране будет отображен вывод программы в виде двух строк:
C:\ ENDL ENTER
А теперь
Учимся программировать на языке C++
Наконец, следующая программа ADDRESS.CPP выводит адрес издательства "Jamsa Press" в несколько строк:
#include iostream.h
void main(void)
{
cout "Jamsa Press" endl;
cout "2975 South Rainbow, Suite I" endl;
cout "Las Vegas, NV 89102" endl;
}
Использование ссылок в C++
Из урока 10 вы узнали, как изменять параметры внутри функции с помощью указателей. Для использования указателей вы должны предварять имена переменных-указателей звездочкой. Использование указателей досталось в "наследство" от языка С. Чтобы упростить процесс изменения параметров, С++ вводит такое понятие как ссылка. Как вы узнаете из этого урока, ссылка представляет собой псевдоним (или второе имя), который ваши программы могут использовать для обращения к переменной. К концу данного урока вы освоите следующие основные концепции:
• Для объявления и инициализации ссылки внутри программы объявите переменную, размещая амперсанд () сразу же после типа переменной, и затем используйте оператор присваивания для назначения псевдонима, например int alias_name = variable',.
• Ваши программы могут передавать ссылки в функцию в качестве параметров, а функция, в свою очередь, может изменять соответствующее значение параметра, не используя указателей.
• Внутри функции вам следует объявить параметр как ссылку, размещая амперсанд () после типа параметра, затем можно изменять значение параметра внутри функции без помощи указателей.
Как вы узнаете, использование указателей очень упрощает изменение значений параметров внутри функции.
ИСПОЛЬЗОВАНИЕ ССЫЛОК В КАЧЕСТВЕ ПАРАМЕТРОВ
Основное назначение ссылки заключается в упрощении процесса изменения параметров внутри функции. Следующая программа REFERENC.CPP присваивает ссылку с именем number_alias переменной number. Программа передает ссылку на переменную в функцию change_value, которая присваивает переменной значение 1001:
#include iostream.h
void change_value(int alias)
{
alias = 1001;
}
void main(void)
{
int number;
int number_alias = number;
change_value(number_alias);
out "Переменная number содержит " number endl;
}
Как вы видите, программа передает ссылку в функцию change_value. Если вы рассмотрите объявление функции, вы обнаружите, что change_value объявляет параметр alias как ссылку на значение типа int.
void change_value(int alias)
Внутри функции change_value можете изменять значение параметра без помощи указателя. В результате звездочка (*) не используется и операция внутри функции становится легче для понимания.
Использование комментариев для объяснения ссылок внутри ваших программ
Большинство программистов C++ знакомы с языком программирования С, и они привыкли использовать указатели внутри функции, если необходимо изменить значение параметра. В результате, если такие программисты не видят указатели внутри функций, которые используют ссылки, они могут предположить, что значения параметров не изменяются. Для предотвращения подобных промахов не забывайте размещать несколько комментариев до и внутри функций, которые изменяют параметры с помощью ссылок. В таком случае программисты С лучше поймут работу ваших функций.
ИСПОЛЬЗОВАНИЕ СТАТИЧЕСКИХ ФУНКЦИЙ-ЭЛЕМЕНТОВ
Предыдущая программа иллюстрировала использование статических элементов данных. Подобным образом C++ позволяет вам определить статические функции-элементы (методы). Если вы создаете статический метод, ваша программа может вызывать такой метод, даже если объекты не были созданы. Например, если класс содержит метод, который может быть использован для данных вне класса, вы могли бы сделать этот метод статическим. Ниже приведен класс menu, который использует esc-последовательность драйвера ANSI для очистки экрана дисплея. Если в вашей системе установлен драйвер ANSI.SYS, вы можете использовать метод clear_screen для очистки экрана. Поскольку этот метод объявлен как статический, программа может использовать его, даже если объекты типа menu не существуют. Следующая программа CLR_SCR.CPP использует метод clear_screen для очистки экрана дисплея:
#include iostream.h
class menu
{
public:
static void clear_screen(void);
// Здесь должны быть другие методы
private:
int number_of_menu_options;
};
void menu::clear_screen(void)
{
cout '\033' "[2J";
}
void main(void)
{
menu::clear_screen();
}
Так как программа объявляет элемент clear_screen как статический, она может использовать эту функцию для очистки экрана, даже если объекты типа menu не существуют. Функция clear_screen использует esc-последовательность ANSI Esc[2J для очистки экрана.
Использование в ваших программах методов класса
По мере создания методов класса возможны ситуации, когда функция, созданная вами для использования классом, может быть полезна для операций вашей программы, которые не включают объекты класса. Например, в классе menu была определена функция clear_screen, которую вы, возможно, захотите использовать в программе. Если ваш класс содержит метод, который вы захотите использовать вне объекта класса, поставьте перед его прототипом ключевое слово static и объявите этот метод как public:
public:
static void clear_screen(void);
Внутри вашей программы для вызова такой функции используйте оператор глобального разрешения, как показано ниже:
menu::clear_screen();
ИСПОЛЬЗОВАНИЕ СТРОКОВЫХ ФУНКЦИЙ БИБЛИОТЕКИ ЭТАПА ВЫПОЛНЕНИЯ
Из урока 11 вы узнали, что большинство компиляторов C++ обеспечивает обширный набор функций, называемых библиотекой этапа выполнения. Рассматривая библиотеку этапа выполнения, вы обнаружите, что она содержат много разных функций, манипулирующих строками. Например, функция strupr преобразует символьную строку в строку верхнего регистра. Подобно этому, функция strlen возвращает количество символов в строке. Большинство библиотек этапа выполнения обеспечивают даже функции, которые позволяют вам просматривать строки в поисках определенного символа. Например, следующая программа STRUPR.CPP иллюстрирует использование функций strupr и strlwr библиотеки этапа выполнения:
#include iostream.h
#include string.h // Содержит прототипы
// функций strupr и strlwr
void main(void)
{
char title[] = "Учимся программировать на языке C++";
char lesson[] = "Символьные строки";
cout "Верхний регистр: " strupr(title) endl;
cout "Нижний регистр: " strlwr(lesson) endl;
}
Использование библиотечных функций, манипулирующих строками может сохранить вам время, требуемое для программирования. Выберите время напечатать копию заголовочного файла STRING.H для определения функций манипулирования строками, которые поддерживаются библиотекой вашего компилятора.
Вы должны играть по правилам
Как вы уже знаете, большинство функций, которые манипулируют строками, полагаются на символ NULL как на конец строки. Если ваши программы присваивают строки символам, то следует убедиться, что они добавляют символ NULL в качестве последнего символа строки. Если ваши программы не используют NULL соответствующим образом, то функции, которые полагаются на символ NULL, будут сбиваться.
Использование свободной памяти в С++
Как вы уже знаете, если ваша программа объявляет массив, компилятор C++ распределяет память для хранения его элементов. Однако представляется возможным, что до некоторого времени размер массива может быть не так велик, чтобы вместить все необходимые данные. Например, предположим, что вы создали массив для хранения 100 акций. Если позже вам потребуется хранить более 100 акций, вы должны изменить свою программу и перекомпилировать ее. С другой стороны, вместо распределения массива фиксированного размера ваши программы могут запрашивать необходимое количество памяти динамически, т.е. во время выполнения. Например, если программе необходимо следить за акциями, она могла бы запросить память, достаточную для хранения 100 акций. Аналогично, если программе необходимы только 25 акций, она могла бы запросить меньше памяти. Распределяя подобным образом память динамически, ваши программы непрерывно изменяют свои потребности без дополнительного программирования. Если ваши программы запрашивают память во время выполнения, они указывают требуемое количество памяти, а C++ возвращает указатель на эту память. C++ распределяет память из областей памяти, которые называются свободной памятью. В этом уроке рассматриваются действия, которые должна выполнить ваша программа для динамического распределения, а впоследствии освобождения памяти во время выполнения. К концу данного урока вы освоите следующие основные концепции:
Чтобы запросить память во время выполнения, ваши программы должны использовать оператор C++ new.
При использовании оператора new программы указывают количество требуемой памяти. Если оператор new может успешно выделить требуемый объем памяти, он возвращает указатель на начало области выделенной памяти.
Если оператор new не может удовлетворить запрос на память вашей программы (возможно, свободной памяти уже не осталось), он возвращает указатель NULL.
Чтобы позже освободить память, распределенную с помощью оператора new, ваши программы должны использовать оператор C++ delete.
Динамическое распределение памяти во время выполнения является чрезвычайно полезной возможностью. Экспериментируйте с программами, представленными в данном уроке. И вы поймете, что динамическое распределение памяти реально выполняется очень просто.
ИСПОЛЬЗОВАНИЕ УКАЗАТЕЛЕЙ С ДРУГИМИ ТИПАМИ МАССИВОВ
Несмотря на то что указатели широко используются с символьными строками, вы можете использовать указатели с массивами других типов. Например, следующая программа PTRFLOAT.CPP использует указатель на массив типа float для вывода значений с плавающей точкой:
#include iostream.h
void show_float(float *array, int number_of_elements)
{
int i;
for (i = 0; i number_of_elements; i++) cout *array++ endl;
}
void main(void)
{
float values[5] = {1.1, 2.2, 3.3, 4.4, 5.5);
show_float(values, 5);
}
Как видите, внутри функции show_float цикл for использует значение, указываемое с помощью указателя array, а затем увеличивает этот указатель до следующего значения. В данном случае программа должна передать параметр, который задает количество элементов массива, поскольку в отличие от символьных строк массивы типа float (или int, long и т. д.) не используют символ NULL для определения последнего элемента.
ИСПОЛЬЗОВАНИЕ УКАЗАТЕЛЯ НА СИМВОЛЬНУЮ СТРОКУ
Как вы уже знаете, указатель содержит адрес памяти. Когда ваша программа передает массив (например, символьную строку) в функцию, C++ передает адрес первого элемента массива. В результате совершенно обычно для функции использовать указатель на символьную строку. Чтобы объявить указатель на символьную строку, функция просто предваряет имя переменной звездочкой, как показано ниже:
void some_function(char *string);
Звездочка, которая предваряет имя переменной, указывает C++, что переменная будет хранить адрес памяти — указатель. Следующая программа PTR_STR.CPP использует указатель на символьную строку внутри функции show_string для вывода содержимого строки по одному символу за один раз:
#include iostream.h
void show_string(char *string)
{
while (*string != '\0')
{
cout *string;
string++;
}
}
void main(void)
{
show_string( "Учимся программировать на языке C++!");
}
Обратите внимание на цикл while внутри функции show_slring. Условие while (*string != '\0') проверяет, не является ли текущий символ, указываемый с помощью указателя string, символом NULL, который определяет последний символ строки. Если символ не NULL, цикл выводит текущий символ с помощью cout. Затем оператор string++; увеличивает указатель siring таким образом, что он указывает на следующий символ строки. Когда указатель string указывает на символ NULL, функция уже вывела строку и цикл завершается.
20. Сканирование строки с помощью указателя.
Предположим, например, что строка, переданная в функцию, находится в памяти компьютера по адресу 1000. Каждый раз, когда функция увеличивает указатель string, он указывает на следующий символ (адрес 1001,1002, 1003 и т. д.), как показано на 20.
Использование void
Как только ваша программа становится более сложной, вы должны разделить ее на небольшие более легко управляемые части, называемые функциями. Функция представляет собой простой набор операторов внутри программы, выполняющих определенную задачу. Например, при создании программы платежных документов, вы могли бы создать функцию с именем salary, вычисляющую оклад служащих. Аналогичным образом, если вы пишете математическую программу, вы могли бы создать функции с именами square_root или cube, которые возвращают результат определенных математических операций. Если ваша программа использует функцию, функция выполняет свою задачу и затем возвращает свой результат программе.
Каждая функция в вашей программе имеет уникальное имя. А каждая программа имеет по крайней мере одну функцию. Каждая программа из урока 1 имела только одну функцию с именем main. Урок 9 предоставляет более подробный обзор функций. В данный момент просто имейте в виду, что функция состоит из нескольких связанных по смыслу операторов, выполняющих определенную задачу.
При исследовании различных программ на C++ вы будете постоянно сталкиваться со словом void. Программы используют слово void для указания того, что функция не возвращает значения или не имеет значений, передаваемых в нее. Например, если вы используете среду MS-DOS или UNIX, программа может завершить свое выполнение с возвратом операционной системе значения статуса, которое может быть проверено командным файлом. Командные файлы MS-DOS проверяют выходной статус программы, используя команду IF ERRORLEVEL. Например, предположим, что программа с именем PAYROLL. EXE завершается с одним из следующих выходных значений статуса в зависимости от результата обработки:
|
Значение статуса
|
Смысл
/td> |
Успех |
/td>
| Файл не найден |
|
/p>
|
В принтере нет бумаги
Внутри командного файла MS-DOS вы можете проверить результат работы программы, используя команду IF ERRORLEVEL:
PAYROLL
IF ERRORLEVEL 0 IF NOT ERRORLEVEL 1 GOTO SUCCESSFUL
IF ERRORLEVEL 1 IF NOT ERRORLEVEL 2 GOTO NO_FILE
IF ERRORLEVEL 2 IF NOT ERRORLEVEL 3 GOTO NO_PAPER
REM Далее идут другие команды
Большинство простых программ на C++, которые будут созданы вами в процессе изучения этой книги, не возвращают выходное значение статуса операционной системе. Поэтому вы должны размещать слово void перед main, как показано ниже:
void main (void) //—------- Программа не возвращает значение
В следующих уроках вы узнаете, что ваши программы могут использовать информацию (например, имя файла), которую пользователь указывает в командной строке при запуске программы. Если программа не использует информацию командной строки, вы должны разместить слово void внутри круглых скобок после main, как показано ниже:
void main (void) //---------------------- Программа не использует аргументы командной строки
По мере усложнения ваши программы могут возвращать значения в операционную систему или использовать параметры командной строки. Однако в настоящий момент просто используйте void в операторе с main, как показано в этой программе.
ИСПОЛЬЗОВАНИЕ ВОЗВРАЩАЕМОГО ФУНКЦИЕЙ ЗНАЧЕНИЯ
Когда функция возвращает значение, вызвавшая программа может присвоить возвращенное значение переменной, используя оператор присваивания, как показано ниже:
payroll_amount = payroll (employee, hours, salary);
В дополнение к этому вызвавшая программа просто может обращаться к функции. Например, следующий оператор выводит возвращаемое функцией значение, используя cout.
cout "Служащий получил" payroll(employee, hours, salary) endl;
Вызвавшая функция может также использовать возвращаемое значение в условии, как показано ниже:
if (payroll(employee, hours, salary) 500.00)
cout "Этот служащий нуждается в повышении" endl;
Как видите, программа может использовать возвращаемое функцией значение различными способами.
ИСПОЛЬЗОВАНИЕ ЗНАЧЕНИЯ ПЕРЕМЕННОЙ
После присвоения значения переменной ваши программы могут использовать это значение, просто обращаясь к ее имени. Следующая программа SHOWVARS.CPP присваивает значения трем переменным и затем выводит значение каждой переменной, используя cout:
#include iostream.h
void main (void)
{
int age = 32,
float salary = 25000.75;
long distance_to_the_moon = 238857;
cout "Служащему " age " года (лет)" endl;
cout "Оклад служащего составляет $" salary endl;
cout "От земли до луны " distance_to_the_moon " миль" endl;
}
Замечание: Последний оператор cout не помещается на одной строке. В этом случае программа просто переносит слова на следующую строку. Вы можете сделать такой перенос, поскольку C++ использует точку с запятой для указания конца оператора. Если вам необходимо перенести строку, постарайтесь не делать этого в середине символьной строки (внутри двойных кавычек), используйте дополнительный отступ для перенесенной части строки, как показано выше.
Когда вы откомпилируете и запустите эту программу, на экране появится следующий вывод:
С:\ SHOWVARS ENTER
Служащему 32 года (лет)
Оклад служащего составляет $25000.75
От земли до луны 238857 миль
Как видите, для использования значения переменной вы просто обращаетесь к имени переменной в вашей программе.
Изменение и увеличение цикла for
Все представленные до настоящего момента циклы for увеличивали управляющую переменную цикла на 1 на каждой итерации цикла. Однако цикл for не обязывает ваши программы увеличивать эту переменную на единицу. Следующая программа BY_FIVES.CPP выводит каждое пятое число в диапазоне от 0 до 100:
#include iostream.h
void main(void)
{
int count;
for (count = 0; count = 100; count += 5)
cout count ' '';
}
Если вы откомпилируете эту программу, на вашем экране будут отображаться числа 0, 5,10 и т. д. до 100. Обратите внимание, что оператор цикла for использует для увеличения переменную count.
count += 5;
Если вы хотите добавить некоторое значение к текущему значению переменной, а затем присвоить результат той же переменной, C++ позволяет вам сделать это двумя способами. Первый: предположим, вашей программе необходимо добавить значение 5 к переменной count, это можно сделать, как показано ниже:
count = count + 5;
Второй: C++ позволяет вам использовать краткую запись, представленную ниже, для добавления значения 5 к переменной count.
count += 5;
Поскольку это легче записать, то данная краткая форма является общепринятой внутри циклов.
При использовании цикла for вы не обязаны продвигать счетчик в сторону увеличения. Следующая программа CNT_DOWN.CPP использует цикл for для вывода чисел в порядке уменьшения от 100 до 1:
#include iostream.h
void main(void)
{
int count ;
for (count = 100; count = 1; count--)
cout count ' ';
}
Как видите, цикл for инициализирует переменную count значением 100. На каждой итерации цикл уменьшает значение этой переменной на 1. Цикл завершается, когда переменная count содержит значение 0.
Остерегайтесь бесконечных циклов
Как вы уже знаете, цикл for предоставляет вашим программам способ повторять связанные операторы определенное количество раз. Используя переменную управления, цикл for по существу считает количество выполненных итераций. Когда цикл достигает своего конечного условия, ваша программа прекращает повторение операторов и продолжает свое выполнение с первого оператора, следующего за циклом for.
К сожалению, из-за ошибок в программах в некоторых случаях цикл никогда не достигает своего завершающего условия и, таким образом, зацикливается навсегда (или до тех пор, пока вы не прервете программу). Такие не завершающиеся циклы называются бесконечными циклами. Другими словами, это циклы, не имеющие способа для завершения. Например, следующий оператор for создает бесконечный цикл:
for (count = 0; count 100; wrong_variable++)
// операторы
Как видите, цикл for использует переменную count в качестве своей управляющей переменной. Однако в секции цикла увеличения программа увеличивает не ту переменную. В результате цикл никогда не увеличивает переменную count, и она никогда не будет иметь значение больше или равно 100. Таким образом, этот цикл превращается в никогда не завершающийся бесконечный цикл.
Важно обратить внимание, что циклы for не ограничиваются использованием переменных типа int в качестве их управляющих переменных. Например, следующая программа LOOPVAR.CPP использует переменную типа char для вывода букв алфавита внутри одного цикла и переменную типа float для вывода чисел с плавающей точкой внутри другого цикла:
#include iostream.h
void main(void)
{
char letter;
float value;
for (letter = 'A'; letter = 'Я'; letter++)
cout letter;
cout endl;
for (value = 0.0; value = 1.0; value += 0.1)
cout value ' ';
cout endl;
}
Если вы откомпилируете и запустите эту программу, на экране появится следующий вывод:
АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ
0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9
Повторение цикла определенное число раз
Одна из наиболее общих операций, которую будут выполнять ваши программы, состоит в повторении одного или нескольких операторов определенное количество раз. Оператор C++ for позволяет вашим программам сделать именно это. Такой оператор for использует управляющую переменную, хранящую количество выполнений цикла. Общий формат оператора for выглядит так:
for (инициализация; проверка; увеличение)
оператор;
При запуске этот цикл for присваивает начальное значение управляющей переменной цикла. Далее программа проверяет условие цикла. Если условие истинно, она выполняет операторы внутри цикла, затем увеличивает управляющую переменную цикла и повторяет проверку условия. Если условие истинно, процесс повторяется. Если же условие ложно, цикл for завершается и программа продолжает свое выполнение с первого оператора, следующего за циклом for.
Изменение значений параметров
Из урока 9 вы узнали, как разделить ваши программы на небольшие легко управляемые части, называемые функциями. Как вы уже знаете, программы могут передавать информацию (параметры) функциям. Представленные в уроке 9 программы использовали или выводили значения параметров, но не меняли их. Из этого урока вы узнаете, как изменить значение параметра в функции. Вы обнаружите, что для изменения параметров в функции фактически требуется больше шагов, чем можно предположить. Однако этот урок обучит вас всем шагам, которые необходимо знать. К концу данного урока вы освоите следующие основные концепции:
• Если функция не использует указатели или ссылки, она не может изменить значение параметра.
• Для изменения значения параметра функция должна знать адрес параметра в памяти.
• Оператор адреса C++ () позволяет вашей программе определить адрес переменной в памяти.
• Когда ваша программа узнает адрес памяти, она сможет использовать операцию разыменования C++ (*) для определения значения, хранимого по данному адресу.
• Если программе нужно изменить значение параметров функции, программа передает в функцию адрес параметра.
Изменение значения параметра функции представляет собой обычную операцию. Экспериментируйте с программами, представленными в этом уроке, чтобы убедиться, что вы полностью освоили этот процесс.
ИЗМЕНЕНИЕ ЗНАЧЕНИЯ ПАРАМЕТРА
Для изменения значения параметра функция должна знать адрес памяти параметра. Чтобы сообщить функции адрес параметра, ваши программы должны использовать оператор адреса C++ (). Следующий вызов функции иллюстрирует, как программа будет использовать оператор адреса для передачи адресов переменных big и small в функцию change_values:
change_values (big, small); --Передача параметров по адресу
Внутри функции вы должны сообщить C++ , что программа будет передавать параметры с помощью адреса. Для этого вы объявляете переменные-указатели, предваряя имя каждой переменной звездочкой, как показано ниже:
void сhange_values (int. *big, int. *small) --- Указатель на тип int
Переменная-указатель представляет собой переменную, которая содержит адрес памяти. Внутри функции вы должны сообщить C++ , что функция работает с адресом параметра. Для этого вы предваряете имя параметра звездочкой, как показано ниже:
*big = 1001;
*small = 1001;
Следующая программа CHGPARAM.CPP использует оператор адреса для передачи адресов параметров big и small в функцию change_values. Функция, в свою очередь, использует указатели участков памяти параметров. Следовательно, изменения параметров, сделанные функцией, остаются и после завершения функции:
#include iostream.h
void change_values (int *a, int *b)
{
*a = 1001;
*b = 1001;
cout "Значения в функции display_values" " равны " *а " и " *b endl;
}
void main(void)
{
int big = 2002, small = 0;
cout "Значения перед функцией " big " и " small endl;
change_values(big, small);
cout "Значения после функции " big " и " small endl;
}
Когда вы откомпилируете и запустите эту программу, на экране появится следующий вывод:
C:\ CHGPARAM ENTER
Значения перед функцией 2002 и 0
Значения в функции display_values равны 1001 и 1001
Значения после функции 1001 и 1001
Как видите, значения, которые функция change_values присваивает параметрам, остаются и после завершения функции. Чтобы понять, почему изменения, которые функция выполнила над переменными, остались после ее завершения, необходимо вспомнить, что функция имеет доступ к ячейке памяти каждой переменной. Если вы передаете параметры по адресу, C++ помещает адрес каждой переменной в стек, как показано на 10.2.
10.2. Передача параметров по адресу.
Используя указатели (адреса памяти) внутри функции, change_values может обратиться к памяти по адресу каждого параметра, изменяя значения параметров, что и требуется.
Изменение значений параметров в функциях
Для изменения значения параметра в функции, функция должна знать адрес параметра в памяти. Следовательно, ваша программа должна передать адрес параметра с помощью оператора адреса C++ :
some_function(some_variable);
Внутри функции вы должны сообщить C++ , что функция будет работать с адресом памяти (указателем). Для этого при объявлении вы предваряете имя параметра звездочкой:
void some_function(int *some_variable);
Далее внутри функции вы должны употреблять звездочку перед именем переменной:
*some_variable = 1001;
cout *some_variable;
Во избежание ошибок C++ не позволит вашей программе передать адрес переменной в функцию, которая не ожидает указатль в качестве параметра. Кроме того, C++ обычно генерирует предупреждение компилятора, когда ваша программа пытается передать значение в функцию, которая ожидает указатель в качестве параметра.
ИЗУЧЕНИЕ ФУНКЦИЙ БИБЛИОТЕКИ ЭТАПА ВЫПОЛНЕНИЯ
Ваш компилятор C++ обеспечивает сотни функций библиотеки этапа выполнения. Документация, поставляемая с вашим компилятором, должна содержать полное описание всех функций библиотеки этапа выполнения. Если вы просмотрите эту документацию, то найдете, что функции обычно используют простые прототипы. Например, для функции sqrt вы могли бы найти следующий прототип:
double sqrt(double);
В данном случае прототип функции сообщает вам, что функция возвращает значение типа double и ожидает параметр тоже типа double. Аналогично этому можно найти следующий прототип для функции time:
time_t time (time_t *);
И опять прототип сообщает вам, что функция возвращает значение типа time_t (этот тип определен в заголовочном файле time.h). Функция ожидает, что ее параметр должен быть указателем на переменную типа time_t. По мере чтения документации о функциях библиотеки этапа выполнения вы очень много узнаете о самих функциях и о C++ , при этом обращайте внимание на прототипы функций.
Другой путь изучения библиотечных функций вашего компилятора состоит в просмотре заголовочных файлов, содержащихся в подкаталоге INCLUDE. Выделите время, например, для того, чтобы сейчас распечатать заголовочные файлы math.h, time.h и stdlib.h, которые вы использовали в программах этого урока.
Использование функций API
В дополнение к стандартной библиотеке этапа выполнения многие компиляторы обеспечивают функции API или интерфейс прикладных программ. Например, если вы программируете в среде Windows, то вам потребуются функции графического API, телефонного API (TAPI), API для мультимедиа и т. д. Прежде чем создавать свои собственные функции, убедитесь, что вы не нашли таких функций в API, предоставляемом вашим компилятором.
Изучение основ
В этой части вы изучите основные понятия, необходимые для создания собственных программ на C++. Если вы никогда не создавали программ, не беспокойтесь, здесь описано все шаг за шагом с самого начала. К тому времени, когда вы освоите несложные уроки, представленные в этой части, вы будите готовы выбрать свой путь в программировании на C++! Уроки этой части:
Урок 1. Создание вашей первой программы.
Урок 2. Более внимательный взгляд на C++.
Урок 3. Вывод сообщений на экран.
Урок 4. Программы хранят информацию в переменных.
Урок 5. Выполнение простых операций.
Урок 6. Чтение ввода с клавиатуры.
Урок 7. Программа принимает решение.
Урок 8. Повторение одного или нескольких операторов.
Содержание | Следующая часть
ИЗУЧЕНИЕ СИНТАКСИЧЕСКИХ ОШИБОК
Каждый язык — английский, французский, немецкий и даже C++ — имеет набор правил, называемых синтаксисом, которым вы должны следовать, когда используете данный язык. В английском языке, например, предложения обычно заканчиваются точкой, восклицательным или вопросительным знаком. Вы также используете заглавные буквы в начале предложения. В синтаксисе C++ используется точка с запятой, круглые скобки, фигурные скобки и многие другие символы. Когда вы забываете или неправильно употребляете эти символы, компилятор C++ выводит на экран сообщение об ошибке, которое описывает ошибку и соответствующий ей номер строки в исходном файле.
Компилятор C++ не может создать выполнимую программу, пока не будут исправлены все синтаксические ошибки. Чтобы понять процесс обнаружения и исправления синтаксических ошибок, создайте следующую программу с именем SYNTAX. CPP:
#include iostream.h
void main(void)
{
cout Заключайте сообщение в кавычки;
}
Если посмотреть внимательно, можно заметить, что сообщения, выведенные двумя предыдущими программами, в вашем исходном файле взяты в кавычки. Синтаксис (правила) C++ требует кавычек. При компиляции программы компилятор выведет сообщения о синтаксических ошибках. В случае Borland C++ компилятор выведет следующие сообщения:
С:\ ВСС SYNTAX.CPP ENTER
Borland C++ Version 4.00 Copyright (с) 1993 Borland
International syntax.cpp:
Error syntax.cpp 5: Undefined symbol 'Заключайте' in function main()
Error syntax.cpp 5: Statement missing; in function main() *** 2 errors in Compile ***
В этом случае компилятор вывел две синтаксические ошибки. Обе ошибки относятся к 5 строке исходного файла. Отредактируйте файл и возьмите сообщение в кавычки, как показано ниже:
cout "Заключайте сообщение в кавычки";
Теперь можете успешно откомпилировать программу и получить выполнимый файл. Когда вы впервые начинаете использовать какой-либо язык программирования, можете рассчитывать на несколько синтаксических ошибок каждый раз при компиляции программы. После того как вы самостоятельно создадите несколько программ, вы будете быстро определять и исправлять подобные ошибки.
Изучение синтаксических ошибок
При создании программы на C++ вам следует придерживаться определенных правил, называемых правилами синтаксиса. Например, надо брать текстовые сообщения в кавычки и ставить точку с запятой после большинства операторов вашей программы (позже вы узнаете, для каких операторов требуется точка с запятой, а для каких нет). Если в программе нарушаются правила синтаксиса, компилятор C++ выводит сообщение об ошибке на экран. Вам следует исправить все синтаксические ошибки до того, как компилятор сможет создать выполнимую программу.
Как 'А' отличается от "А"
При рассмотрении программ на C++ вы можете встретить символы, заключенные в одинарные кавычки (например, 'А') и символы, заключенные в
17.3. Как компилятор C++ хранит символьную константу 'А' и строковую константу "А".
двойные кавычки ("А"). Символ внутри одинарных кавычек представляет собой символьную константу. Компилятор C++ выделяет только один байт памяти для хранения символьной константы. Однако символ в двойных кавычках представляет собой строковую константу — указанный символ и символ NULL (добавляемый компилятором). Таким образом, компилятор будет выделять два байта для символьной строки. 17.3 иллюстрирует, как компилятор C++ хранит символьную константу 'А' и строковую константу "А".
КАК C++ ХРАНИТ ОБЪЕДИНЕНИЯ
Внутри ваших программ объединения C++ очень похожи на структуры. Например, следующая структура определяет объединение с именем distance, содержащее два элемента:
union distance
{
int miles;
long meters;
};
Как и в случае со структурой, описание объединения не распределяет память. Вместо этого описание предоставляет шаблон для будущего объявления переменных. Чтобы объявить переменную объединения, вы можете использовать любой из следующих форматов:
union distance
{
union distance
{
int miles; int miles;
long meters; long meters;
} japan, germany, franee;
};
distance japan, germany, franee;
Как видите, данное объединение содержит два элемента: miles и meters. Эти объявления создают переменные, которые позволяют вам хранить расстояния до указанных стран. Как и для структуры, ваша программа может присвоить значение любому элементу. Однако в отличие от структуры значение может быть присвоено только одному элементу в каждый момент времени. Когда вы объявляете объединение, компилятор C++ распределяет память для хранения самого большого элемента объединения. В случае объединения distance компилятор распределяет достаточно памяти для хранения значения типа long, как показано на 19.
19. C++ распределяет память, достаточную для хранения только самого большого элемента объединения.
Предположим, что ваша программа присваивает значение элементу miles, как показано ниже:
japan.miles = 12123;
Если далее ваша программа присваивает значение элементу meters, значение, присвоенное элементу miles, теряется.
Следующая программа USEUNION.CPP иллюстрирует использование объединения distance. Сначала программа присваивает значение элементу
miles и выводит это значение. Затем программа присваивает значение элементу meters. При этом значение элемента miles теряется:
#include iostream.h
void main(void)
{
union distance
{
int miles;
long meters;
} walk;
walk.miles = 5;
cout "Пройденное расстояние в милях " walk.miles endl;
walk.meters = 10000;
cout "Пройденное расстояние в метрах " walk.meters endl;
}
Как видите, программа обращается к элементам объединения с помощью точки, аналогичная запись использовалась при обращении к элементам структуры в уроке 18.
Объединение хранит значение только одного элемента в каждый момент времени
Объединение представляет собой структуру данных, которая, подобно структуре C++, позволяет вашим программам хранить связанные части информации внутри одной переменной. Однако в отличие от структуры объединение хранит значение только одного элемента в каждый момент времени. Другими словами, когда вы присваиваете значение элементу объединения, вы перезаписываете любое предыдущее присваивание.
Объединение определяет шаблон, с помощью которого ваши программы могут позднее объявлять переменные. Когда компилятор C++ встречает определение объединения, он распределяет количество памяти, достаточное для хранения только самого большого элемента объединения.
КАК ЗАСТАВИТЬ C++ ПРОВЕРЯТЬ ИСКЛЮЧИТЕЛЬНЫЕ СИТУАЦИИ
Прежде чем ваши программы могут обнаружить и отреагировать на исключительную ситуацию, вам следует использовать оператор C++ try для разрешения обнаружения исключительной ситуации. Например, следующий оператор try разрешает обнаружение исключительной ситуации для вызова функции file_соpy:
try
{
file_copy("SOURCE.ТХТ", "TARGET.ТХТ") ;
};
Сразу же за оператором try ваша программа должна разместить один или несколько операторов catch, чтобы определить, какая исключительная ситуация имела место (если она вообще была):
try
{
file_copy("SOURCE.ТХТ", "TARGET.ТХТ") ;
};
catch (file_open_error)
{
cerr "Ошибка открытия исходного или целевого файла" endl;
exit(1);
}
catch (file_read_error)
{
cerr "Ошибка чтения исходного файла" endl;
exit(1);
}
catch (file_write_error)
{
cerr "Ошибка записи целевого файла" endl;
exit(1);
}
Как видите, приведенный код проверяет возникновение исключительных ситуаций работы с файлами, определенных ранее. В данном случае независимо от типа ошибки код просто выводит сообщение и завершает программу. В идеале ваш код мог бы отреагировать и не так — возможно, попытаться исключить причину ошибки и повторить операцию. Если вызов функции прошел успешно и исключительная ситуация не выявлена, C++ просто игнорирует операторы catch.
КОГДА НЕОБХОДИМА ПЕРЕГРУЗКА
Одним из наиболее общих случаев использования перегрузки является применение функции для получения определенного результата, исходя из различных параметров. Например, предположим, что в вашей программе есть функция с именем day_of_week, которая возвращает текущий день недели (0 для воскресенья, 1 для понедельника, ..., 6 для субботы). Ваша программа могла бы перегрузить эту функцию таким образом, чтобы она верно возвращала день недели, если ей передан юлианский день в качестве параметра, или если ей переданы день, месяц и год:
int day_of_week(int julian_day)
{
// Операторы
}
int day_of_week(int month, int day, int year)
{
// Операторы
}
По мере изучения объектно-ориентированного программирования в C++, представленного в следующих уроках, вы будете использовать перегрузку функций для расширения возможностей своих программ.
Перегрузка функций улучшает удобочитаемость программ
Перегрузка функций C++ позволяет вашим программам определять несколько функций с одним и тем же именем. Перегруженные функции должны возвращать значения одинакового типа*, но могут отличаться количеством и типом параметров. До появления перегрузки функций в C++ программисты языка С должны были создавать несколько функций с почти одинаковыми именами. К сожалению программисты, желающие использовать такие функции, должны были помнить, какая комбинация параметров соответствует какой функции. С другой стороны, перегрузка функций упрощает задачу программистов, требуя, чтобы они помнили только одно имя функции.
* Перегруженные функции не обязаны возвращать значения одинакового типа по той причине, что компилятор однозначно идентифицирует функцию по ее имени и набору ее аргументов. Для компилятора функции с одинаковыми именами, но различными типами аргументов — разные функции, поэтому тип возвращаемого значения — прерогатива каждой функции. — Прим.перев.
КОМПИЛЯЦИЯ ВАШЕЙ ПРОГРАММЫ
Компьютер работает с комбинациями единиц и нулей (называемых машинным языком), которые представляют наличие или отсутствие электрических сигналов. Если сигнал равен единице (наличие), компьютер может выполнить одну операцию, а если сигнал равен нулю (отсутствие), компьютер может выполнить другую операцию. Однако к счастью, нет необходимости писать программы в нулях и единицах (как это делали программисты в 1940 и 50 гг.). Вместо этого специальная программа — компилятор C++ — преобразует операторы программы (ваш исходный код) в машинный язык.
Другими словами, компилятор просматривает исходный файл, содержащий операторы программы на C++. Если ваши операторы не нарушают ни одно правило языка C++, компилятор преобразует их в машинный язык (единицы и нули), который компьютер может выполнить. Компилятор хранит машинный язык в выполняемом файле, имеющем, как правило, расширение ЕХЕ. Если файл ЕХЕ существует, вы можете запустить программу, вводя ее имя в ответ на командную подсказку.
В зависимости от используемого вами компилятора, команды, которые вы применяете для его вызова, будут различны. Например, при использовании Borland C++ вам следует компилировать программу FIRST.CPP с помощью команды ВСС:
C:\ BCC FIRST.CPP ENTER
Если вы применяете не Borland C++, обратитесь к документации, поставляемой с вашим компилятором, чтобы определить правильную команду для его запуска. По окончании работы компилятор создаст выполнимую программу и сохранит ее в файле на диске. В среде MS-DOS файл выполни-
мой программы будет иметь расширение ЕХЕ, например FIRST.EXE. Если при компиляции программы компилятор выдает сообщения об ошибках, отредактируйте свой исходный файл и сравните каждый символ исходного файла с символами, которые приведены в этой книге. Исправьте все ошибки, сохраните сделанные изменения, а затем откомпилируйте программу второй раз. После того как вы успешно откомпилировали свою программу, запустите ее, вводя имя программы в ответ на командную подсказку как это было показано выше.
Представление о компиляторе
При создании программы вы используете язык программирования (такой как C++), чтобы указать инструкции, выполняемые компьютером. Применяя текстовый редактор, вы вносите операторы программы в исходный файл. Далее используется специальная программа — компилятор, которая преобразует ваш исходный файл в машинный язык (единицы и нули, понимаемые компьютером). Если компиляция прошла успешно, результатом будет файл выполнимой программы. Однако если вы допустили одну или несколько ошибок или нарушили какое-либо правило C++, компилятор выдаст на экран сообщения об ошибках, и для их исправления вам следует заново отредактировать исходный файл.
Если вы работаете на большой машине или мини-ЭВМ, у вас должен быть компилятор, доступный для вас и других пользователей вашей системы. Если же вы используете ПК, вам следует приобрести и установить компилятор, такой как Borland C++ или Microsoft Visual C++.
Конструктор и деструктор
При создании объектов одной из наиболее широко используемых операций которую вы будете выполнять в ваших программах, является инициализация элементов данных объекта. Как вы узнали из урока 22, единственным способом, с помощью которого вы можете обратиться к частным элементам данных, является использование функций класса. Чтобы упростить процесс инициализации элементов данных класса, C++ использует специальную функцию, называемую конструктором, которая запускается для каждого создаваемого вами объекта. Подобным образом C++ обеспечивает функцию, называемую деструктором, которая запускается при уничтожении объекта. В данном уроке конструктор и деструктор рассматриваются более подробно. К концу этого урока вы освоите следующие основные концепции:
Конструктор представляет собой метод класса, который облегчает вашим программам инициализацию элементов данных класса.
Конструктор имеет такое же имя, как и класс.
Конструктор не имеет возвращаемого значения.
Каждый раз, когда ваша программа создает переменную класса, C++ вызывает конструктор класса, если конструктор существует.
Многие объекты могут распределять память для хранения информации; когда вы уничтожаете такой объект, C++ будет вызывать специальный деструктор, который может освобождать эту память, очищая ее после объекта.
Деструктор имеет такое же имя, как и класс, за исключением того, что вы должны предварять его имя символом тильды (~).
Деструктор не имеет возвращаемого значения.
Термины конструктор и деструктор не должны вас пугать. Вместо этого представьте конструктор как функцию, которая помогает вам строить (конструировать) объект. Подобно этому, деструктор представляет собой функцию, которая помогает вам уничтожать объект. Деструктор обычно используется, если при уничтожении объекта нужно освободить память, которую занимал объект.
Конструкторы и параметры по умолчанию
Как вы уже знаете из урока 15, C++ позволяет указывать значения по умолчанию для параметров функции. Если пользователь не указывает каких-либо параметров, функция будет использовать значения по умолчанию. Конструктор не является исключением; ваша программа может указать для него значения по умолчанию так же, как и для любой другой функции. Например, следующий конструктор employee использует по умолчанию значение оклада равным 10000.0, если программа не указывает оклад при создании объекта. Однако программа должна указать имя служащего и его номер:
employee::employee(char *name, long employee_id, float salary = 10000.00)
{
strcpy(employee::name, name);
employee::employee_id = employee_id;
if (salary 50000.0)
employee::salary = salary;
else // Недопустимый оклад
employee::salary = 0.0;
}
Локальные переменные и область видимости
Как вы уже знаете, функции позволяют разделить программу на небольшие легко управляемые части. Все функции, используемые вами до настоящего момента, были совершенно просты. Как только вашим функциям потребуется выполнить более сложную работу, они должны будут использовать переменные для реализации своих задач. Переменные, объявляемые внутри функции, называются локальными переменными. Их значения и даже сам факт, что локальные переменные существуют, известны только данной функции, Другими словами, если вы объявляете локальную переменную с именем salary в функции payroll, то другие функции не имеют доступа к значению переменной salary. Фактически, другие функции не имеют никакого представления о том, что переменная salary существует. Этот урок рассматривает область видимости переменной, или участок внутри вашей программы, в котором переменная известна. К концу этого урока вы освоите следующие основные концепции:
• Вы объявляете локальные переменные внутри функции точно так же, как и внутри main: указывая тип и имя переменной.
• Имена переменных, используемых внутри функций, должны быть уникальными только по отношению к данной функции.
• Область видимости переменной определяет участок программы, где переменная известна и доступна.
• Глобальные переменные в отличие от локальных переменных известны на протяжении всей программы и доступны внутри всех функций.
• Оператор глобальной области видимости C++ (::) позволяет вам управлять областью видимости переменной.
Объявление локальных переменных внутри функции достаточно просто. Фактически вы уже это делали каждый раз, когда объявляли переменные внутри main.
Множественное наследование
Из урока 26 вы узнали, что можно построить один класс из другого, наследуя его характеристики. Оказывается, C++ позволяет порождать класс из нескольких базовых классов. Когда ваш класс наследует характеристики нескольких классов, вы используете множественное наследование. Как вы узнаете из данного урока, C++ полностью поддерживает множественное наследование. К концу этого урока вы изучите следующие основные концепции:
Если вы порождаете класс из нескольких базовых классов, то получаете преимущества множественного наследования.
При множественном наследовании производный класс получает атрибуты двух или более классов.
При использовании множественного наследования для порождения класса конструктор производного класса должен вызвать конструкторы всех базовых классов.
При порождении класса из производного класса вы создаете иерархию наследования (иерархию классов).
Множественное наследование является мощным инструментом объектно-ориентированного программирования. Экспериментируйте с программами, представленными в этом уроке, и вы обнаружите, что построение класса из уже существующего значительно экономит усилия на программирование.
Наследование
Цель объектно-ориентированного программирования состоит в повторном использовании созданных вами классов, что экономит ваше время и силы. Если вы уже создали некоторый класс, то возможны ситуации, что новому классу нужны многие или даже все особенности уже существующего класса, и необходимо добавить один или несколько элементов данных или функций. В таких случаях C++ позволяет вам строить новый объект, используя характеристики уже существующего объекта. Другими словами, новый объект будет наследовать элементы существующего класса (называемого базовым классом). Когда вы строите новый класс из существующего, этот новый класс часто называется производным классом. В этом уроке впервые вводится наследование классов в C++ . К концу данного урока вы изучите следующие основные концепции:
Ели ваши программы используют наследование, то для порождения нового класса необходим базовый класс, т.е. новый класс наследует элементы базового класса.
Для инициализации элементов производного класса ваша программа должна вызвать конструкторы базового и производного классов.
Используя оператор точку, программы могут легко обращаться к элементам базового и производного классов.
В дополнение к общим (public) (доступным всем) и частным (private) (доступным методам класса) элементам C++ предоставляет защищенные (protected) элементы, которые доступны базовому и производному классам.
Для разрешения конфликта имен между элементами базового и производного классов ваша программа может использовать оператор глобального разрешения, указывая перед ним имя базового или производного класса.
Наследование является фундаментальной концепцией объектно-ориентированного программирования. Выберите время для экспериментов с программами, представленными в этом уроке. И вы обнаружите, что реально наследование реализуется очень просто и может сохранить огромные усилия, затрачиваемые на программирование.
Наследование и шаблоны
Главное преимущество объектно-ориентированного программирования заключается в том, что класс, который вы создаете для одной программы, часто может быть использован в другой. Как вы узнаете из этой части, C++ не только позволяет вашим программам повторно использовать классы, но также позволяет строить один класс из другого. Когда вы строите один класс из другого, новый класс наследует характеристики начального класса. В этой части вы освоите, как использовать возможности наследования C++ для экономии значительного объема программирования. К концу данной части вы очень много узнаете о концепциях объектно-ориентированного программирования.
Урок 26. Наследование.
Урок 27. Множественное наследование.
Урок 28. Частные элементы и друзья.
Урок 29. Использование шаблонов функций.
Урок 30. Использование шаблонов классов.
Предыдущая часть | Следующая часть
О конфликте имен
При объявлении локальных переменных внутри функции очень вероятно, что имя локальной переменной, объявляемой вами в одной функции, будет таким же, как и имя переменной, используемой в другой функции. Как уже упоминалось, локальные переменные известны только в текущей функции. Таким образом, если две функции используют одно и то же имя для своих локальных переменных, это не приводит к конфликту. C++ трактует имя каждой переменной как локальное по отношению к соответствующей функции. Следующая программа LCLNAME.CPP использует функцию add_values для сложения двух целочисленных значений. Эта функция присваивает свой результат локальной переменной value. Однако в main один из параметров, передаваемых в функцию, также носит имя value. Тем не менее, поскольку C++ трактует обе переменные как локальные для соответствующих функций, их имена не конфликтуют:
#include iostream.h
int add_values(int a, int b)
{
int value;
value =a + b;
return(value);
}
void main (void)
{
int value = 1001;
int other value = 2002;
cout value " + " other_value " = " add_values(value, other_value) endl;
}
О локальных переменных
Локальные переменные представляют собой переменные, объявляемые внутри функции. Имя и значение локальной переменной известны только функции, внутри которой переменная объявлена. Вы должны объявлять локальные переменные в начале вашей функции сразу же после первой открывающей фигурной скобки. Имена, назначаемые локальным переменным, должны быть уникальными только для функции, внутри которой эти переменные определены. При объявлении локальной переменной внутри функции ее можно инициализировать с помощью оператора присваивания.
О МАТЕМАТИКЕ УКАЗАТЕЛЯ
Как вы уже знаете, ваши программы могут использовать указатели на массивы любых типов. В предыдущей программе функция show_float увеличивала указатель для продвижения по массиву типа float. Указатель указывает на участок памяти, содержащий значение определенного типа, например char, int или float. Когда функция сканирует массив с помощью указателя, функция увеличивает указатель для продвижения от одного значения к следующему. Чтобы указатель указывал на следующий элемент массива, C++ должен знать размер каждого элемента (в байтах), чтобы определить, на сколько необходимо увеличить значение указателя. Например, для продвижения указа-
теля к следующему символу в массиве, C++ должен увеличить значение указателя на 1. Однако, чтобы указать следующее значение в массиве типа int C++ должен увеличить указатель на два байта (значение типа int занимает два байта памяти). Для значений типа. float C++ увеличивает указатель на 4 байта. Зная тип значения, на которое указывает указатель, C++ знает, на сколько необходимо увеличить значение этого указателя. В ваших программах вы просто используете оператор увеличения, например pointer++. Однако за кулисами C++ увеличивает реальное значение (адрес памяти), содержащееся в указателе, на корректную величину.
Объединения
Из урока 18 вы узнали, как группировать связанную информацию в одной переменной с помощью структур C++. По мере усложнения вашим программам могут потребоваться разные способы просмотра части информации. Кроме того, программе может потребоваться работать с двумя или несколькими значениями, используя при этом только одно значение в каждый момент времени. В таких случаях для хранения данных ваши программы могут использовать объединения. В данном уроке вы изучите, как создавать и использовать объединения для хранения информации. Как вы узнаете, объединения очень похожи на структуры, описанные в уроке 18. Прежде чем вы закончите этот урок, вы освоите следующие основные концепции:
Объединения C++ очень похожи на структуры, за исключением того, как C++ хранит их в памяти; кроме того, объединение может хранить значение только для одного элемента в каждый момент времени.
Объединение представляет собой структуру данных, подобную структуре C++, и состоит из частей, называемых элементами.
Объединение определяет шаблон, с помощью которого программы далее объявляют переменные.
Для обращения к определенному элементу объединения ваши программы используют оператор C++ точку.
Чтобы изменить значения элемента объединения внутри функции, ваша программа должна передать переменную объединения в функцию с помощью адреса.
Анонимное объединение представляет собой объединение, у которого нет имени (тэга).
Как вы узнаете, объединения очень похожи на структуры C++, однако способ, с помощью которого C++ хранит объединения, отличается от способа, с помощью которого C++ хранит структуры.
ОБЪЯВЛЕНИЕ ГЕНЕРИРУЕМЫХ ФУНКЦИЕЙ ИСКЛЮЧИТЕЛЬНЫХ СИТУАЦИЙ
Как вы уже знаете, прототип функции позволяет вам определить тип возвращаемого функцией значения и типы ее параметров. Если ваши программы используют исключительные ситуации, вы также можете использовать прототип функции, чтобы указать генерируемые данной функцией исключительные ситуации. Например, следующий прототип функции сообщает компилятору, что функция power_plant может генерировать исключительные ситуации melt_down и radiation_leak:
void power_plant(long power_needed) throw (melt_down, radiation_leak);
Включая подобным образом возможные исключительные ситуации в прототип функции, вы можете легко сообщить другому программисту, который будет читать ваш код, какие исключительные ситуации ему необходимо проверять при использовании данной функции.
ОБЪЯВЛЕНИЕ ЛОКАЛЬНЫХ ПЕРЕМЕННЫХ
Локальная переменная представляет собой переменную, определенную внутри функции. Такая переменная называется локальной, потому что ее известность ограничена данной функцией. Вы объявляете локальные переменные в начале функции после открывающей фигурной скобки:
void some_function(void)
{
int count;
float result;
}
Следующая программа USEBEEPS.CPP использует функцию sound_speaker, которая заставляет играть встроенный компьютерный динамик столько раз, сколько указано параметром beeps. Внутри функции sound_speaker локальная переменная counter хранит количество звуков, издаваемых динамиком:
#include iostream.h
void sound_beeps(int beeps)
{
for (int counter = 1; counter = beeps; counter++) cout '\a';
}
void main(void)
{
sound_beeps(2);
sound_beeps(3);
}
Как видите, функция sound_beeps объявляет переменную counter сразу же после открывающей фигурной скобки. Поскольку counter определяется внутри функции sound_beeps, эта переменная является локальной по отношению к sound_beeps, и это означает, что только sound_beeps знает об этой переменной и может к ней обращаться.
ОБЪЯВЛЕНИЕ ПЕРЕМЕННОЙ МАССИВА
Массив представляет собой переменную, способную хранить одно или несколько значений. Подобно переменным, используемым вашими программами до сих пор, массив должен иметь тип (например, inl, char или float) и уникальное имя. В дополнение к этому вам следует указать количество значений, которые массив будет хранить. Все сохраняемые в массиве значения должны быть одного и того же типа. Другими словами, ваша программа не может поместить значения типа float, char и long в один и тот же массив. Следующее объявление создает массив с именем test_scores, который может вмещать 100 целых значений для тестовых очков:
———————————————— Тип массива
int test_scores[100]; //------ Размер массива
Когда компилятор C++ встречает объявление этой переменной, он распределит достаточно памяти для хранения 100 значений типа int. Значения, хранящиеся в массиве, называются элементами массива.
Массивы хранят несколько значений одного и того же типа
По мере усложнения вашим программам потребуется работать с несколькими значениями одного и того же типа. Например, программы могут хранить возраст 100 служащих или стоимость 25 акций. Вместо того чтобы заставлять программу работать со 100 или с 25 переменными с уникальными именами, C++ позволяет вам определить одну переменную — массив —, которая может хранить несколько связанных значений.
Для объявления массива вы должны указать тип и уникальное имя массива, а также количество элементов, которые будет содержать массив. Например, следующие операторы объявляют три разных массива:
float part_cost[50];
int employee_age[100];
float stock_prices[25];
ОБЪЯВЛЕНИЕ ПЕРЕМЕННЫХ В ПРОГРАММАХ
Ваши программы используют переменные для хранения информации. В зависимости от типа хранимого значения, например, целое число, буква алфавита или число с плавающей точкой, тип вашей переменной будет разным.
Тип переменной указывает тип значения, хранимого в переменной, а также набор операций (таких как сложение, умножение и другие), которые программа может выполнять над значением переменной. Большинство программ на C++ будут использовать типы переменных, перечисленные в табл. 4.1.
Таблица 4.1. Типы переменных C++.
|
Тип
|
Хранимые значения
|
char
|
Значения в диапазоне от -128 до 127. Обычно используется для хранения букв алфавита
|
int
Значения в диапазоне от -32768 до 32767 |
unsigned |
Значения в диапазоне от 0 до 65535 |
long |
Значения в диапазоне от -2147483648 до 2147483647 |
float |
Значения в диапазоне от -3.4 x 10-38 до 3.4 x 1038 |
double |
Значения в диапазоне от 1.7х 10-308 до 1.7х 10308 |
Прежде чем вы сможете использовать переменную, ваша программа должна ее объявить. Другими словами, вам следует представить переменную компилятору C++. Чтобы объявить переменную в программе, вам следует указать тип переменной и ее имя, по которому программа будет обращаться к данной переменной. Указывайте тип и имя переменной после открывающей фигурной скобки главной программы, как показано ниже:
тип_переменной имя_переменной;
Как правило, тип переменной будет одним из типов, перечисленных в табл. 4.1. Выбираемое вами имя переменной должно нести смысловую нагрузку, которая описывает (для всех, кто читает вашу программу) использование переменной. Например, ваша программа могла бы использовать переменные, такие как employee_name, employee_age и т. д. Обратите внимание на точку с запятой, которая следует за именем переменной. В C++ объявление переменной считается оператором. Поэтому вы должны поставить после объявления точку с запятой.
Фрагмент следующей программы объявляет три переменные, используя типы int, float и long:
#include iostream.h
void main(void)
{
int test_score;
float salary;
long distance_to_mars;
}
Важно обратить внимание, что данная программа ничего не выполняет, а только объявляет переменные. Как видите, объявление каждой переменной заканчивается точкой с запятой. Если вы объявляете несколько переменных одного и того же типа, можно разделять их имена запятой. Следующий оператор, например, объявляет три переменных с плавающей точкой:
float salary, income_tax, retirement_fund;
Изучение переменных
Переменная представляет собой имя ячейки в памяти компьютера. Во время выполнения ваши программы хранят информацию в переменных. При создании программ вы должны объявлять переменные, сообщая компилятору C++ имя и тип переменной. Например, следующий оператор объявляет переменную с именем age типа int:
int age;
ОБЪЯВЛЕНИЕ СИМВОЛЬНЫХ СТРОК В ПРОГРАММАХ
Программисты на C++ широко используют символьные строки для хранения имен пользователей, имен файлов и другой символьной информации.
Для объявления символьной строки внутри программы просто объявите массив типа char с количеством элементов, достаточным для хранения требуемых символов. Например, следующее объявление создает переменную символьной строки с именем filename, способную хранить 64 символа (не забывайте, что символ NULL является одним из этих 64 символов):
char filename[64];
Как видно из 17.1, это объявление создает массив с элементами, индексируемыми от filename[0] до filename[63].
17.1. C++ трактует символьную строку как массив типа char.
Главное различие между символьными строками и другими типами массивов заключается в том, как C++ указывает последний элемент массива, Как вы уже знаете, программы на C++ представляют конец символьной строки с помощью символа NULL, который в C++ изображается как специальный символ '\0'. Когда вы присваиваете символы символьной строке, вы должны поместить символ NULL ('\0') после последнего символа в строке. Например, следующая программа ALPHABET. CPP присваивает буквы от А до Я переменной alphabet, используя цикл for. Затем программа добавляет символ NULL в эту переменную и выводит ее с помощью cout.
#include iostream.h
void main(void)
{
char alphabet [34]; // 33 буквы плюс NULL char letter;
int index;
for (letter = 'A', index = 0; letter = 'Я';
letter++, index++) alphabet[index] = letter;
alphabet[index] = NULL;
cout "Буквы " alphabet;
}
Как видите, программа присваивает строке символ NULL, чтобы указать последний символ строки:
alphabet[index] = NULL;
Когда выходной поток cout выводит символьную строку, он по одному выводит символы строки, пока не встретит символ NULL. Короче говоря, cимвол NULL указывает программе последний символ в строке.
Обратите внимание на цикл for, который появляется в предыдущей программе. Как видите, цикл инициализирует и увеличивает две переменные (letter и index). Когда цикл for инициализирует или увеличивает несколько переменных, разделяйте операции запятой (запятая тоже является оператором C++):
for (letter = 'A', index = 0; letter = 'Я'; letter++, index++)
ОБЪЯВЛЕНИЕ СТРУКТУРЫ
Структура определяет шаблон, с помощью которого ваша программа может позднее объявить одну или несколько переменных. Другими словами, ваша программа сначала определяет структуру, а затем объявляет переменные типа этой структуры. Для определения структуры ваши программы используют ключевое слово struct, за которым обычно следует имя и левая фигурная скобка. Следом за открывающей фигурной скобкой вы указываете тип и имя одного или нескольких элементов. За последним элементом вы размещаете правую закрывающую фигурную скобку. В этот момент вы можете (необязательно) объявить переменные данной структуры:
struct name
{
int member_name_l; |—————— Объявления элементов структуры
float member_name_2; } variable; |——————————————— Объявление переменной
}
Например, следующее определение создает структуру, содержащую информацию о служащем:
struct employee
{
char name [64] ;
long employee_id;
float salary;
char phone[10];
int office_number;
};
В данном случае определение не объявляет какие-либо переменные типа этой структуры. После того как вы определите структуру, ваша программа может объявить переменные типа этой структуры, используя имя структуры (иногда называемое структурным тэгом), как показано ниже:
—————————————————————————— Тэг
employee boss, worker, new_employee; Объявление переменных
В данном случае оператор создает три переменные структуры employee. В некоторых случаях вы можете увидеть объявление, в котором тэг структуры предваряется ключевым словом struct, как показано ниже:
struct employee boss, worker, new_employee;
Ключевое слово struct является обязательным при программировании на С, так что некоторые программисты могут включать его по привычке. Однако в C++ использовать ключевое слово struct необязательно.
ОБРАБОТКА НЕОЖИДАННЫХ ИСКЛЮЧИТЕЛЬНЫХ СИТУАЦИЙ
Из урока 11 вы узнали, что компиляторы C++ предоставляют функции библиотеки этапа выполнения, которые вы можете использовать в своих программах. Если вы читали документацию по этим функциям, то могли обратить внимание на функции, которые генерируют определенные исключительные ситуации. В таких случаях ваши программы должны проверять соответствующие исключительные ситуации. По умолчанию если ваша программа генерирует исключительную ситуацию, которая не улавливается (программа не имеет соответствующего обработчика исключительной ситуации), то запустится стандартный обработчик, предоставляемый языком C++. В большинстве случаев стандартный обработчик завершит вашу программу. Следующая программа UNCAUGHT.CPP иллюстрирует, как стандартный обработчик исключительной ситуации завершает выполнение вашей программы:
#include iostream.h
class some_exception { };
void main(void)
{
cout "Перед генерацией исключительной ситуации" endl;
throw some_exception();
cout "Исключительная ситуация сгенерирована" endl;
}
В данном случае, когда программа генерирует исключительную ситуацию (которая не улавливается программой), C++ вызывает стандартный обработчик исключительной ситуации, который завершает программу. Поэтому последний оператор программы, который выводит сообщение о генерации исключительной ситуации, никогда не выполняется. Вместо использования стандартного обработчика исключительной ситуации C++ ваши программы могут определить свой собственный стандартный обработчик (обработчик по умолчанию). Чтобы сообщить компилятору C++ о своем стандартном обработчике, ваши программы должны использовать функцию библиотеки этапа выполнения set_unexpected. Прототип функции set_unexpected определен в заголовочном файле except.h.
ОБРАБОТКА НЕСКОЛЬКИХ УСЛОВИЙ
Программы, представленные в этом уроке, использовали if и else, чтобы указать один набор операторов, который программе следует выполнить, если условие истинно, и другой набор операторов, выполняемых, если условие ложно. Однако в некоторых случаях программам потребуется проверить несколько разных условий. Предположим, например, что вашей программе необходимо определить тестовые очки студента. Для этого программа должна проверить, больше тестовые очки или равны 90, 80, 70, 60 и т. д. Следующая программа SHOWGRAD.CPP использует для этого серию операторов if-else:
#include iostream.h
void main(void)
{
int test_score;
cout "Введите тестовые очки и нажмите Enter: ";
cin test_score;
if (test_score = 90)
cout "Вы получили А!" endl;
else if (test_score = 80)
cout "Вы получили В!" endl;
else if (test_score = 70)
cout "Вы получили С" endl;
else if (test_score = 60)
cout "Ваша оценка была D" endl;
else
cout "Вы провалили тест" endl;
}
При выполнении первого оператора if программа проверяет, больше тестовые очки или равны 90. Если это так, программа выводит сообщение пользователю, что он получил А. В противном случае, если тестовые очки не больше или равны 90, программа выполняет следующие else if, чтобы проверить, больше ли тестовые очки или равны 80. Программа повторяет данный процесс до тех пор, пока не определит правильную оценку. Как и ранее, экспериментируйте с этой программой, вводя разные тестовые очки.
Обращение к элементам массива
Как вы уже знаете, массив позволяет вашим программам хранить несколько значений в одной и той же переменной. Для обращения к определенным значениям, хранящимся в массиве, используйте значение индекса, которое указывает на требуемый элемент. Например, для обращения к первому элементу массива test_scores вы должны использовать значение индекса 0. Для обращения ко второму элементу используйте индекс 1. Подобно этому, для обращения к третьему элементу используйте индекс 2. Как показано на 16.1, первый элемент массива всегда имеет индекс 0, а значение индекса последнего элемента на единицу меньше размера массива:
16.1. Как C++ индексирует элементы массива.
Важно помнить, что C++ всегда использует 0 для индекса первого элемента массива, а индекс последнего элемента на единицу меньше размера массива. Следующая программа ARRAY. CPP создает массив с именем values, который вмещает пять целочисленных значений. Далее программа присваивает элементам значения 100, 200, 300, 400 и 500:
#include iostream.h
void main(void)
{
int values[5]; // Объявление массива
values[0] = 100;
values[1] = 200;
values[2] = 300;
values[3] = 400;
values [4] = 500;
cout "Массив содержит следующие значения" endl;
cout values [0] ' ' values [1] ' ' values [2] ' ' values [3] ' ' values [4] endl;
}
Как видите, программа присваивает первое значение элементу 0 (values[0]). Она также присваивает последнее значение элементу 4 (размер Массива (5) минус 1).
Использование индекса для обращения к элементам массива
Массив позволяет вашим программам хранить несколько значений внутри одной и той же переменной. Для обращения к определенному значению внутри массива программы используют индекс. Говоря кратко, значение индекса указывает требуемый элемент массива. Все массивы C++ начинаются с элемента с индексом 0. Например, следующий оператор присваивает значение 100 первому элементу массива с именем scores:
scores[0] = 100;
Когда ваша программа объявляет массив, она указывает количество элементов, которые массив может хранить. Например, следующий оператор объявляет массив, способный хранить 100 значений типа int.
int scores[100];
В данном случае массив представляет собой элементы от scores[0] до scores[99].
ОГРАНИЧЕНИЕ КОЛИЧЕСТВА ДРУЗЕЙ
Как вы только что узнали, если вы объявляете один класс другом другого класса, вы обеспечиваете классу-другу доступ к частным элементам данных этого другого класса. Вы также знаете и то, что чем больше доступа к частным данным класса, тем больше шансов на внесение ошибок в программу. Следовательно, если доступ к частным данным другого класса необходим только нескольким функциям класса, C++ позволяет указать, что только определенные функции дружественного класса будут иметь доступ к частным элементам. Предположим, например, что класс librarian, представленный в предыдущей программе, содержит много разных функций. Однако предположим, что только функциям change_catalog и get_catalog необходим доступ к частным элементам класса book. Внутри определения класса book мы можем ограничить доступ к частным элементам только этими двумя функциями, как показано ниже:
class book
{
public:
book(char *, char *, char *);
void show_book(void);
friend char *librarian::get_catalog(book);
friend void librarian: :change_catalog( book *, char *);
private:
char title[64];
char author[ 64 ];
char catalog[64];
};
Как видите, операторы friend содержат полные прототипы всех дружественных функций, которые могут напрямую обращаться к частным элементам.
О функциях-друзьях
Если ваша программа использует друзей для доступа к частным данным класса, вы можете ограничить количество функций-элементов класса-друга, который может обращаться к частным данным, используя дружественные функции. Для объявления функции-друга укажите ключевое слово friend, за которым следует полный прототип, как показано ниже:
public:
friend class_name::function_name(parameter types);
Только функции-элементы, указанные как друзья, могут напрямую обращаться к частным элементам класса, используя оператор точку.
Если ваша программа начинает ссылаться на один класс из другого, вы можете получить синтаксические ошибки, если порядок определения классов неверен. В данном случае определение класса book использует прототипы функций, определенные в классе librarian. Следовательно, определение класса librarian должно предшествовать определению класса book. Однако если вы проанализируете класс librarian, то обнаружите, что он ссылается на класс book:
class librarian
{
public:
void change_catalog(book *, char *);
char *get_catalog(book);
};
Поскольку вы не можете поставить определение класса book перед определением класса librarian, C++ позволяет вам объявить класс book, тем самым сообщая компилятору, что такой класс есть, а позже определить его. Ниже показано, как это сделать:
class book; // объявление класса
Следующая программа LIMITFRI.CPP использует дружественные функции для ограничения доступа класса librarian к частным данным класса book. Обратите внимание на порядок определения классов:
#include iostream.h
#include string.h
class book;
class librarian
{
public:
void change_catalog(book *, char *);
char *get_catalog(book);
};
class book
{
public:
book(char *, char *, char *) ;
void show_book (void);
friend char *librarian::get_catalog(book);
friend void librarian::change_catalog( book *, char *);
private:
char title[64];
char author[64];
char catalog[64];
};
book::book(char *title, char *author, char *catalog)
{
strcpy(book::title, title);
strcpy(book::author, author);
strcpy(book::catalog, catalog);
}
void book::show_book(void)
{
cout "Название: " title endl;
cout "Автор: " author endl;
cout "Каталог: " catalog endl;
}
void librarian::change_catalog(book *this_book, char *new_catalog)
{
strcpy(this_book-catalog, new_catalog) ;
}
char *librarian::get_catalog(book this_book)
{
static char catalog[64];
strcpy(catalog, this_book.catalog);
return(catalog) ;
}
void main(void)
{
book programming( "Учимся программировать на C++", "Jamsa", "P101");
librarian library;
programming.show_book();
library.change_catalog(programming, "Легкий C++ 101");
programming.show_book();
}
Как видите, программа сначала использует объявление, чтобы сообщить компилятору, что класс book будет определен позже. Поскольку объявление извещает компилятор о классе book, определение класса librarian может ссылаться на класс book, который еще не определен в программе.
Что такое идентификатор класса
Идентификатор представляет собой имя, например имя переменной или класса. Если ваши программы используют дружественные классы, то может случиться, что определение одного класса ссылается на другой класс (его имя или идентификатор), о котором компилятор C++ еще ничего не знает. В таких случаях компилятор C++ будет сообщать о синтаксических ошибках. Чтобы избавиться от ошибок типа "что следует определять сначала", C++ позволяет вам включать в начало исходного текста программы объявление класса, тем самым вводя идентификатор класса:
class class_name;
Эта строка сообщает компилятору, что ваша программа позже определит указанный класс, а пока программе разрешается ссылаться на этот класс.