Начнем с объяснения структуры программ на языке Си++, мало обращая внимания на существование языка Си, точнее, не всегда явно отмечая степень преемственности.
Пример 1. Следуя классикам [1,3], приведем программу, выводя их на экран дисплея фразу Hello, Woгd! (Здравствуй, Вселенная!):
//HELLO.СРР - имя файла с программой.
include < iostream >
main()
{
cout " "\nHellо, Wогd!\n";
}
Результат выполнения программы:
Hello, World!
В первой строке текста программы - однострочный комментарий, начинающийся парой символов '// ', заканчивающийся неизображаемым символом "конец строки". Между этими разделителями может быть помещен произвольный текст. В данном примере указано имя файла hello.cpp, текст программы.
Во второй строке помещена команда (директива) препроцессора, обеспечивающая включение в программу средств связи со стандартными потоками ввода и вывода данных. Указанные средства находятся в файле с именем iostream.h (мнемоника: "i" (input) - ввод; (оutput) - вывод; stream - поток; "h" (head) - заголовок). Стандартным потоком вывода по умолчанию считается вывод на экран дисплея. (Стандартный поток ввода обеспечивает чтение данных от клавиатуры.) Третья строка является заголовком функции с именем.Любая программа на языке Си++ должна включать одну и только одну функцию с этим именем. Именно с нее начинается выполнение программы. Перед именем main помещено служебное слово void - спецификатор типа, указывающий, что функция main в данной программе не возвращает никакого значения. Круглые скобки после main требуются в соответствии с форматом (синтаксисом) заголовка любой функции. В них помещается список параметров. В нашем примере параметры не нужны и список пуст.
Тело любой функции - это заключенная в фигурные скобки последовательность описаний, определений и операторов. Каждое описание, определение или оператор заканчивается символом "точка с запятой" В теле функции main явных описаний и определений нет и есть только один оператор
cout << "\nHello, World"\n";
Имя cout в соответствии с информацией, содержащейся в файле iostream.h, является именем объекта, который обеспечивает вывод информации на экран дисплея (в стандартный поток вывода). Информация для вывода передается объекту cout с помощью операции << ("поместить в"). То, что нужно вывести, помещается справа от знака операции <<. В данном случае это строка (строковая константа) "\nНе11о, Worldl\n". Строка в языке Си++ определена как заключенная в кавычки почти любая последовательность символов. Среди них могут встречаться обозначения не изображаемых на экране дисплея управляющих символов. Например, '\n' - обозначение управляющего символа перехода к началу следующей строки экрана. Таким образом, программа выведет на новой строке экрана фразу Hello, world! и переведет курсор в начало следующей строки экрана.
Уже сейчас следует отметить одну из принципиальных особенностей языка Си++, называемую перегрузкой или расширением действия стандартных операций. Лексема << означает операцию вставки ("поместить в") только в том случае, если слева от нее находится имя объекта cout. В противном случае пара символов означает бинарную операцию сдвига влево. Итак, в единственном операторе этой программы использована операция вставки в выходной поток cout значения, помещенного справа от лексемы
До выполнения программы необходимо подготовить ее текст в файле с расширением. СРР; передать этот файл на компиляцию и устранить синтаксические ошибки, выявленные компилятором; безошибочно откомпилировать (получится объектный файл с расширением .OBJ); дополнить объектный файл нужными библиотечными функциями (компоновка) и получить исполняемый модуль программы в файле с расширением. ЕХЕ. Схема подготовки исполняемой программы приведена на рис. 1.1, где перед шагом компиляции показан шаг препроцессорной обработки текста программы. В нашем примере препроцессор обрабатывает директиву #include < iostream.h > и подключает к исходному тексту программы средства ясна с дисплеем (для поддержки операции <<).
Если исходный текст программы подготовлен в файле HELLO.СРР, препроцессор, выполнив директивы препроцессора, сформирует полный текст программы, компилятор создаст объектный файл HELLO.OBJ, выбрав (по умолчанию) для него указанное имя, а компоновщик (редактор связей. Linker) дополнит программу библиотечными функциями, например, для работы с объектом cout и построит модуль HELLO. ЕХЕ. Запустив на выполнение файл HЕLLO.EXE, получим на экране желаемую фразу Hеllo, World!
Особенности выполнения перечисленных действий зависят от конкретного компилятора языка Си++ и той операционной системы, в которой он работает. Технические подробности следует изучить по документации для конкретного программного продукта. Например, при работе с интегрированными средами фирмы Borland необходимая информация может быть получена из руководств пользователя [4, 10]. Независимо от использованного компилятора при работе в MS-DOS исполняемый модуль программы записывается в некоторый каталог (директорию). Если исполняемый модуль создан в каталоге BOOK на диске С:, то для запуска нашей программы нужна команда MS-DOS:
>С:\BOOK\HELLO.EXE < Enter >
Здесь и далее < Enter > обозначает нажатие клавиши Enter.
Пример 2. Для иллюстрации некоторых особенностей определения и инициализации переменных и массивов, а также ввода и вывода данных напишем программу вычисления должностного оклада в соответствии с заданным разрядам единой тарифной сетки (ETC) оплаты труда для работников бюджетных отраслей в Российской Федерации. (Установлена постановлением Правительства Российской Федерации №785 от 14 октября 1992 г. Отменена 27 февраля 1995 г.)
Исходные данные для расчета: минимальная ставка 1-го разряда (smin), массив тарифных коэффициентов, т.е. коэффициентов перерасчета a[], и номер категории (разряда) г. Массив a[] инициализируется в тексте программы, и его нельзя изменить без изменения и трансляции программы. Минимальная ставка определена в программе, но может заменяться другим значением с помощью аргумента командной строки при запуске программы на исполнение. Номер разряда всегда должен вводиться пользователем явно с клавиатуры при выполнении программы. Таким, образом, программа иллюстрирует три способа задания исходных данных для расчета.
//Р1-02.СРР - имя файла с программой //1
#include < iostream.h > //2
#include < stretrea.h > //3
int mai(int narg, char **arg) //4
{ float -in = 2250;// Ставка 1-го разряда (1992 r.) //5
// а[] - массив значений тарифных коэффициентов: //6
float a[] - ( 1.0, 1.3, 1.69, 1.91. 2.16, 2.44, //7
2.76, 3.12, 3.53, 3.99, 4.51, 5.10, 5.76, //8
6.61, 7.36, 8.17, 9.07, 10.07 ) //9
int r; // r - разряд тарифной сетки оплаты труда //10
cout " "\n"; //11
cout << "Программа вычисляет оклад в соответствии" ; //12
cout << "\nс единой тарифной сеткой оплаты "труда" ; //13
cout << "\n для работников бюджетных отраслей"; //14
сout << "в России." ; //15
if (narg == 1) //16
cout << "\nПо умолчанию минимальный оклад "; //17
cout << smin " " руб."; //18
cout << "\nПри необходимости нацепить значение"; //19
cout << " минимального"; //20
cout << "\nоклада его нужно указать в": //21
cout << " командной строке.\n"; //22
} //23
else //24
{// Чтение из безымянного строкового потока: //25
intrstream(arg[l]) " sain; //26
cout << "\n0пределен минимальный оклад в " ; //27
cout << sain " " pуб.\n" ; //28
} //29
cout << "\nВведите номер разряда тарифной сетки: "; //30
in >> r;; // Вводится с клавиатуры нoмер разряда //31
if(r < 1 II r > 18) //32
{cout " "Ошибка в выборе разряда! " ; //33
return 1; // Аварийный выход из программы //34
} //35
cout " "Введенному разряду соответствует ставка "; //36
cout " (long) (a[r-l]*sain) " " руб."; //37
return 0 // Безошибочное завершение программы //38
} //39
Строки программы справа пронумерованы. Номера входят в комментарии и нужны только для ссылок на строки программы при выяснении.
В строке 2 - директива (команда) препроцессора, подключающая к программе из файла iostream.h средства связи с библиотечными функциями потокового ввода-вывода. Именно там определены стандартные потоки ввода данных от клавиатуры cin и вывода данных на экран дисплея cout, соответствующие операции чтения из потока >> и записи в поток <<.
Препроцессорная директива из строки 3 включает в текст программы средства для обмена со строковыми потоками. Мнемоника значения файла: str - string (строка), strea - stream (поток), h- head (заголовок). Именно эти средства позволили в строке 26 создать безымянный строковый поток и связать его со строкой arg[l].
Обратите внимание, что в отличие от первой программы здесь функция main () возвращает целочисленное значение int, что явно указано в строке 4. Это целочисленное значение после завершения программы передается в операционную систему и может быть проанализировано. Принято соглашение, что любая программа при аварийном завершении должна возвращать ненулевое значение. При правильном выполнении программы в операционную систему передается нулевой результат. Вопрос об анализе результата в операционной системе в этой книге, посвященной языку Си++, не рассматривается.
В списке аргументов функции main (строка 4) первый аргумент int narg служит для передачи в программу количества параметров, использованных в командной строке при запуске программы на выполнение. Значение narg всегда больше или равно 1. Если narg равно 1 (это условие проверяется в строке 16), то никакого параметра в командной строке явно не указано. В этом случае единственный параметр, передаваемый как значение символьного массива, адресуемого указателем arg[0] - это полное имя файла с исполняемым модулем программы. Например, если программа, находящаяся в каталоге BOOK, начинает выполняться после ввода командной строки.
>С: \ВООК\Р1-02.ЕХЕ
то значением, связанным с arg[0], будет строка "с:\воок\р1-02. ЕХЕ". После имени программы в команде операционной системы могут быть указаны через пробелы параметры. В этом случае значением narg будет (k + 1), где k - количество явно указанных параметров. В программе параметры доступны с помощью указателей arg [i], где i=1,.... k. Например, используя команду
>С: \ВООК\Р1-02.ЕХЁ 1800
мы передадим в программу параметр "1800" как значение символьного массива, адресуемого arg[l]. При этом значением narg будет 2, а массив arg[0] , как и раньше, будет указывать на полное имя файла с программой.
В строке 5 определена и инициализирована значением 2250 переменная smin. Спецификатор типа float указывает, что это вещественная арифметическая переменная с внутренним представлением в формате с плавающей точкой. Далее (строки 7-9) определен и инициализирован массив из 18 элементов, каждый из которых является временной вещественного типа со внутренним представлением в формате с плавающей точкой. Количество элементов массива и их значения определяются списком начальных значений, помещенным в фигурных скобках справа, от знака операции присваивания ' = '.
В строке 10 определена, но явно не инициализирована целая (int) переменная r. Ее значение с помощью операции >> ("взять из") вводится при выполнении программы из стандартного входного потока (строка 31). В соответствии с принципом перегрузки (расширения действия) стандартных операций в языке Си++ лексема >> означает операцию извлечения данных из входного потока только в том случае, когда слева от >> находится имя потока. (В противном случае потоки символов >> означает бинарную операцию сдвига вправо, о которой речь пойдет позже.) Итак, в строке 31 используется операция извлечения данных из стандартного входного потока cin. Оператор >> r; преобразует набираемую пользователем на клавиатуре последовательность символов в целочисленное значение и присваивает это значение переменной r. При этом недопустимо появление набранных символов чего-либо отличного от цифр, знаков ' + ', пробелов. Символы, набираемые на клавиатуре и отображаемые на экране дисплея, становятся доступными программе после нажатия клавиши Enter, что одновременно переводит курсор к началу следующей строки. Таким образом, после каждого считывания данных курсор на экране дисплея размещается в начале следующей строки. Обратите внимание на отсутствие символа перевода строки ' \n' в строках 33 и 36.
В строке 16 начинается условный оператор, проверяющий значение narg. Если значение narg равно 1, то по умолчанию используется начальное значение smin, о чем выдается сообщение (Строки 17 - 22) на экран дисплея, и начинает выполняться оператор из строки 30. В противном случае (строки 25 - 28) значение минимального оклада smin выбирается из параметра командной строки операционной системы с помощью указателя arg[l]. Для преобразования этого параметрa (например, строки "1800") в числовое значение используется <<безымянный строковый поток istrstream (arg[1]). Чтение из этого строкового потока выполняет операция извлечения данных из потока >>. Так как справа от этой операции помещена переменная smin типа float, то в параметре можно использовать только символы для изображения числовых значений. Правильность введенного значения для простоты программы не проверяется, и это может быть источником ошибок.Полученное значение smin вместе с пояснительным текстом выводится на экран дисплея (строки 27 - 28), и выполнение условного оператора (строки 16 - 29) завершается.
В строке 30 формируется на экране подсказка пользователю. Затем (строка 31) вводится значение переменной r.
В строках 32 + 35- условный оператор. Вслед за служебным словом if в круглых скобках записано проверяемое логическое выражение - дизъюнкция двух отношений r < 1 и r > 18. Выражение истинно, если значение r меньше 1 или больше 18. В этом случае выполняются заключенные в фигурные скобки { } операторы из строк 33, 34, т.е. печатается сообщение об ошибке, оператор return 1; завершает выполнение программы и передает управление операционной системе, возвращая ей ненулевое значение.
Если номер разряда введен правильно, то выполняются операторы вывода из строк 36,37,38, и программа в строке 38 завершает работу, возвращая операционной системе нулевое значение из оператора return. Отметим, что этот оператор не является обязательным. При его отсутствии возвращаемое программой значение всегда будет нулевым. Если пользователь, находясь в MS-DOS, введет команду (расширение .ZXE по правилам MS-DOS можно опускать)
>С:\ВООК\Р1-02
программа выведет на экран:
Программа вычисляет оклад,в соответствии
с единой тарифной сеткой оплаты труда
для работников бюджетных отраслей в России.
По умолчанию минимальный оклад 2250 руб.
При необходимости изменить значение минимального
оклада его нужно указать в командной строке.
Введите номер разряда тарифной сетки: 11
Введенному разряду соответствует ставка 10147 руб.
Здесь в ответ на приглашение (подсказку) программы, пользователь ввел в качестве номера разряда значение 11 и нажал клавишу Enter. Запуск программы из MS-DOS директивой
>С:\ВООК\Р1-02 20400
приведет к такому результату:
Программа вычисляет оклад в соответствии с единой тарифной сеткой оплаты труда
для работников бюджетных отраслей в России.
Определен минимальный оклад в 20400 руб.
Введите номер разряда тарифной сетки: 11
Введенному разряду соответствует ставка 92004 руб.
Следует остановиться на некоторых особенностях вывода на эк-числовых значений. В операторах из строк 18, 28 в стандартный одной поток cout пересылается преобразованное в символьный ряд вещественное значение переменной smin. В операторе из строки 37 выводится значение выражения (long) (a[r-l]*smin); символом '*' здесь обозначается операция умножения. a[r-l] - индексированная переменная, обеспечивающая доступ к 1-му по порядку элементу массива а []. Унаследованная от своего предшественника - языка Си особенность языка Си++ - нумерация элементов массива, начиная с 0. Таким образом, в нашем примере а[0] имеет значение 1.0; а[1]-1.3; ...; a[16]-9.07;а [17]-10. 07. (Лексемой '--' вне текста программ будем обозначать равенство значений справа и слева от знака '-'. Такое соглашение соответствует синтаксису языка Си++ и позволяет отличать присваивание '-'от равенства'--'.) Вводя номер разряда как значение целочисленной переменной r, мы обращаемся в массив за соответствующим ему коэффициентом с помощью индексированного элемента a[r-1]. Полученное значение тарифного коэффициента умножается на значение минимальной ставки smin. Соответствующее выражение заключено в скобки. Перед скобками помещена операция (long) явного приведения типа, преобразующая вычисляемое вещественное значение к целому "длинному" типу. Это сделано для получения значения ставки в рублях без дробной части (дробная часть отбрасывается при приведении типа).
В операторах из строк 18, 28, 37 операция " применяется по несколько раз. Любой из этих операторов можно заменить несколькими. Например, вместо строки 37 можно записать два оператора:
cout << (long)(a(r-1]*smin);
cout << " руб.";
Результат при выполнении программы будет тем же. Интересно отметить, что в начале 1993 г. оператор из строки 37 имел такой вид:
cout << (int) (a[r-l]*smin) << " руб.";
Однако положительные целые числа типа int в используемом компиляторе Си++ не могут превышать значения 32767. Поэтому при повышении минимальной ставки пришлось перейти к целым числам с большим количеством значащих цифр, т.е. использовать преобразование (long). He потребуются ли нам значения еще большие, например unsigned long?
В качестве упражнения читатель может осовременить программу, учтя постановление Правительства Российской Федерации №189 от 27 февраля 1995 г. В соответствии с этим постановлением:
(С 1-го мая 1995 г. постановление №189 отменено новым постановлением Правительства Российской Федерации и т.д.)
Основная программная единица на языке Си++ - это текстовый файл с названием <имя>. СРР, где СРР - принятое расширение для программ Си++, а имя выбирается достаточно произвольно. Для удобства ссылок и сопоставления программ с их внешними именами целесообразно помещать в начале текста каждой программы строку комментария с именем файла, в котором она находится. Это уже сделано в программах предыдущего параграфа. Текстовый файл с программой Си++ вначале обрабатывает препроцессор (см. рис. 1.1), который распознает команды (директивы) препроцессора (каждая такая команда начинается с символа ' # ') и выполняет их. В приведенных выше программах использованы препроцессорные команды
#include <имя_включаемого_файла>
Выполняя препроцессорные директивы, препроцессор изменяет исходный текст программы. Команда #include вставляет в программу заранее подготовленные тексты из включаемых файлов. Сформированный таким образом измененный текст программы поступает на компиляцию. Компилятор, во-первых, выделяет из поступившего к нему текста программы лексические элементы, т.е. лексемы, а затем на основе грамматики языка распознает смысловые конструкции языка выражения, определения, описания, операторы и т.д.), построенные из этих лексем. Фазы работы компилятора здесь рассматривать нет необходимости. Важно только отметить, что в результате работы компилятора формируется объектный модуль программы.
Компилятор, выполняя лексический анализ текста программы на языке Си++, для распознавания начала и (или) конца отдельных лексем использует пробельные разделители. К пробельным разделителям относятся собственно символы пробелов, символы табуляции, симвoлы перехода на новую строку. Кроме того, к пробельным разделителям относятся комментарии.
В языке Си++ есть два способа задания комментариев. Традиционный способ (ведущий свое происхождение от многих предшествующих языков, например, ПЛ/1, Си и т.д.) определяет комментарий как последовательность символов, ограниченную слева парой символов /*, а справа - парой символов */. Между этими граничными парами может размещаться почти любой текст, в котором разрешено использовать не только символы из алфавита языка Си++, но и другие символы (например, русские буквы):
/* Это комментарий, допустимый и в Си, и в Си++ */
ANSI-стандартом запрещено вкладывать комментарии друг в друга, однако многие компиляторы предусматривают режим, допускающий вложение комментариев.
Второй способ (введенный в Си++) определяет комментарий как последовательность символов, началом которой служат символы //, а концом - код перехода на новую строку. Таким образом, однострочный комментарий имеет вид:
// Это однострочный комментарий, специфичный для языка Си++
Алфавит и лексемы языка СИ++. В алфавит языка Си++ входят:
Из символов алфавита формируются лексемы языка:
Рассмотрим эти лексические элементы языка подробнее.
Идентификатор - последовательность из букв латинского алфавита, десятичных цифр и символов подчеркивания, начинающаяся не с
RUN run hard_RAM_disk сору_54
Прописные и строчные буквы различаются. Таким образом, в том примере два первых идентификатора различны. На длину различаемой части идентификатора конкретные реализации накладывают ограничение. Компиляторы фирмы Borland различают не более 32-х символов любого идентификатора. Некоторые реализации Си++ на ЭВМ типа VAX допускают идентификаторы длиной до 8.
Ключевые (служебные) слова - это идентификаторы, зарезервированные в языке для специального использования. Ключевые слова Си++:
asm double new switch auto else operator template break enum private this case extern protected throw catch float public try char for registe typedef class friend return typeid const goto short: union continue if signed unsigned default inline sixeof virtual delete int static void do long struct volatile while
Ранее [1] в языке Си++ был зарезервирован в качестве ключевого слова идентификатор overload. Для компиляторов фирмы Borland Си++ и ТС++) дополнительно введены ключевые слова:
cdecl _export _loadds _saveregs _cs far near _seg _ds huge pascal _ss _es interrupt _regparam
Там же введены как служебные слова регистровые переменные:
_CH _ВH _СH _DH _SI _SP _SS _AL _BL _CL _DL _DI _CS _ES _AX _ВХ _СХ _DX _BP _DS _fIAGS
Отметим, что ранние версии ВС++ и ТС++ не включали в качестве ключевых слов идентификаторы throw, try, typeid, catch.
He все из перечисленных служебных слов сразу же необходимы программисту, однако их запрещено использовать в качестве произвольно выбираемых имен, и список служебных слов нужно иметь уже на начальном этапе знакомства с языком Си++. Кроме того, идентификаторы, включающие два подряд символа подчеркивания (- -), резервируются для реализации Си++ и стандартных библиотек. Идентификаторы, начинающиеся с символа подчеркивания (_), используются в реализациях языка Си. В связи с этим начинать выбираемые пользователем идентификаторы с символа подчеркивания и использовать в них два подряд символа подчеркивания не рекомендуется.
Константа (литерал) - это лексема, представляющая изображение фиксированного числового, строкового или символьного (литерного)значения.
Константы делятся на пять групп: целые, вещественные (с плавающей точкой), перечислимые, символьные (литерные) и строковые (строки или литерные строки). Перечислимые константы проект стандарта языка Си++ [2] относит к одному из целочисленных типов.
Компилятор, выделив константу в качестве лексемы, относит её к той или другой группе, а внутри группы - к тому или иному типу данных по ее "внешнему виду" (по форме записи) в исходном тексте и по числовому значению.
Целые константы могут быть десятичными, восьмеричными и шестнадцатеричными.
Десятичная целая константа определена, как последовательность десятичных цифр, начинающаяся не с нуля, если это не число нуль: 16, 484216, 0, 4. Для реализации ТС++ и ВС++ диапазон допустимых целых положительных значений от 0 до 4294967295. Константы, превышающие указанное максимальное значение, вызывают ошибку на этапе компиляции. Отрицательные константы - это константы без знака, к которым применена операция изменения знака. Абсолютные значения отрицательных десятичных констант для ТС++ и ВС++ не должны превышать 2147483648.
Таблица 2.1
Целые константы и выбираемые для них типы
Тип данных | |||
j десятичные | восьмеричные | шестнадцатеричные | |
от 0 до 32767 |
от 00 до 077777 |
от 0х0000 до Ox7FFF |
int |
- | от 0100000 до 0177777 |
от 0х8000 до ОхFFFF |
unsigned int |
от 32768 до 2147483647 |
от 0200000 до 017777777777 |
от 0х10000 до Ox7FFFFFFFF |
long |
от 2147483648 до 4294967295 |
от 020000000000 до 037777777777 |
от 0х80000000 до OxFFFFFFFF |
unsigned long |
> 4294967295 | > 037777777777 | > OxFFFFFFFF | ошибка |
Восьмеричные целые константы начинаются всегда с нуля: 016 имеет десятичное значение 14. Если в записи восьмеричной константы встретится недопустимая цифра 8 или 9, то это воспринимается как ошибка. В реализациях ТС++ и ВС++ диапазон допустимых значений для положительных восьмеричных констант от 00 до 037777777777. Для отрицательной восьмеричной константы абсолютное значение не должно превышать 020000000000.
Последовательность шестнадцатеричных цифр, которой предшествует 0х, считается шестнадцатеричной константой. В шестнадцатеричные цифры кроме десятичных входят латинские буквы от а (или А) до f (или F). Таким образом, 0х16 имеет десятичное значение 22, a OxF - десятичное значение 15. Диапазон допустимых значений для положительных шестнадцатеричных констант в реализациях ТС++ и ВС++ от 0х0 до OxFFFFFFFF. Для отрицательных шестнадцатеричных констант абсолютные значения не должны превышать 0х80000000.
В зависимости от значения целой константы компилятор по-разному представляет её в памяти ЭВМ. О форме представления данных в памяти ЭВМ говорят, используя термин тип данных. Соответствие между значениями целых констант и автоматически выбираемыми для них компилятором типами данных отображает табл. 2.1, удовлетворяющая требованиям ANSI языка Си, отнесенным ко внутреннему представлению данных для компиляторов семейства IBM PC/XT/AT.
Если программиста по каким-либо причинам не устраивает тот тип, который компилятор приписывает константе, то он может явным образом повлиять на его выбор. Для этого служат суффиксы L, 1 (long) и U, u (unsigned). Например, константа 64L будет иметь тип long,значению 64 должен быть приписан тип int, как это видно из табл. 2.1. Для одной константы можно использовать два суффикса U(и) и L(1), причем в произвольном порядке. Например, константы 0х22U1, OxlILu, ОхЗЗООООUL, Ox551u будут иметь тип unsigned long. При использовании одного суффикса выбирается тот тип данных, который ближе всего соответствует типу, выбираемому для константы по умолчанию (т.е. без суффикса в соответствии с табл. 2.1). Например, 04L есть константа типа long, 04U имеет тип unsigned int и т.д.
Чтобы проиллюстрировать влияние абсолютного значения константы и использованных в ее изображении суффиксов L, U на тип данных, который ей присваивается на этапе компиляции, приведем следующую программу:
//Р2-01.СРР - имя файла с текстом программы //1
#include < iostream.h > //2
void main () //3
{ cout << " \n sizeof 111 = " << sizeof 111; //4
cout << " \n sizeof 111u = " << sizeof 111u; //5
cout << " \n sizeof 111L = " << sizeof 111L; //6
cout << " \n sizeof 111ul = " << sizeof 111ul; //7
cout << " \n\t sizeof 40000 = " << sizeof 40000; //8
cout << " \n\t sizeof 40000u = " << sizeof 40000u; //9
cout << " \n\t sizeof 40000L = " << sizeof 40000L; //10
cout << " \n\t sizeof 40000LU = " << sizeof 40000LU; //11
} //12
Здесь использована унарная операция языка Си++ sizeof, позволяющая определить размер в байтах области памяти, выделенной для стоящего справа операнда.
Результат выполнения программы:
sizeof 111 = 2
sizeof 111u = 2
sizeof 111L = 4
sizeof 111uL = 4
sizeof 40000 = 4
sizeof 40000u = 2
sizeof 40000L = 4
sizeof 40000LU = 4
Заслуживает внимания длина десятичной константы 40000u, соответствующая типу unsigned int. По умолчанию (см. табл. 2.1) такой тип не приписывается никакой десятичной константе.
Вещественные константы, т.е. константы с плавающей точкой, даже не отличаясь от целых констант по значению, имеют другую форму внутреннего представления в ЭВМ. Эта форма требует использования арифметики с плавающей точкой при операциях с такими константами. Поэтому компилятор должен уметь распознавать вещественные константы. Распознает он их по внешним признакам. Константа с плавающей точкой может включать следующие шесть частей: целая часть (десятичная целая константа); десятичная точка; дробная часть (десятичная целая константа); признак (символ) экспоненты е или Е; показатель десятичной степени (десятичная целая константа, возможно со знаком); суффикс F (или f) либо L (или l). В записях вещественных констант могут опускаться: целая или дробная часть (но не одновременно); десятичная точка или признак экспоненты с показателем степени (но не одновременно); суффикс. Примеры:
66. .0 .12 3.14159F 1.12e-2 2E+6L 2.71
При отсутствии суффиксов F (f) или L ( l ) вещественные константы имеют форму внутреннего представления, которой в языке Си++ соответствует тип данных double. Добавив суффикс f или F, константе придают тип float. Константа имеет тип long double, если в ее представлении используется суффикс L или l. Диапазоны возможных значений и длины внутреннего представления (размер в битах) данных вещественного типа указаны в табл. 2.2 [4, 9, 21, 29].
Таблица 2.2
Тип данных | Размер, бит | Диапазон значений |
float | 32 | от 3.4E-38 до 3.4Е+38 |
double | 64 | от 1.7Е-308 до 1.7Е+308 |
long double | 80 | от 3.4Е-4932 до 1.IE+4932 |
Следующая программа показывает, какие участки памяти выделяются вещественным константам разного типа в реализациях ТС++ и ВС++.
//Р2-02.СРР - размеры памяти для вещественных констант
#include
void main ()
{ cout << "\n sizeof 3.141592653589793 =";
cout << sizeof 3.141592653589793;
cout << "\n sizeof 3.14159 =" << sizeof 3.14159;
cout << "\n sizeof 3.l4159f =" << sizeof 3.14l59f;
cout << "\n sizeof 3.14159L =" << sizeof 3.14159L;
}
Результаты выполнения программы - размеры в байтах областей памяти, выделенных для вещественных констант:
sizeof 3.141592653589793 = 8
sizeof 3.14159 = 8
sizeof 3.14159f = 4
sizeof 3.14159L = 10
Перечислимые константы (или константы перечисления [ 3 ], иначе константы перечислимого типа) вводятся с помощью служебного слова еnum. По существу это обычные целочисленные константы (типа int), которым приписаны уникальные и удобные для использования обозначения. В качестве обозначений выбираются произвольные идентификаторы, не совпадающие со служебными словами и именами других объектов программы. Обозначения присваиваются константам с помощью определения, например, такого вида:
enum ( one = 1, two = 2, three = 3 );
Здесь enum - служебное слово, определяющее тип данных "перечисление", one, two, three - условные имена, введенные программистом для обозначения констант 1, 2, 3. После такого определения в программе вместо константы 2 (и наряду с ней) можно использовать ее обозначение two и т.д.
Если в определении перечислимых констант опускать знаки "=" и не указывать числовых значений, то они будут приписываться идентификаторам (именам) по умолчанию. При этом самый левый в фигурных скобках идентификатор получит значение 0, а каждый последующий увеличивается на 1. Например, в соответствии с определением
enum { zero, one, two, three } ;
перечислимые константы примут значения:
zero==0, one==1, two==2, three==3
Правило о последовательном увеличении на 1 значений перечислимых констант действует и в том случае, когда первым из них (слева в списке) явно присвоены значения. Например, определение
enum { ten = 10, three = 3, four, five, six };
вводит следующие константы:
tеn==10, three==3, four==4, five==5, six==6
Имена перечислимых констант должны быть уникальными, однако к значениям констант это не относится. Одно значение могут иметь разные константы. Например, определение
enum { zero, nought = 0, one, two, paiz = 2, three };
вводит следующие константы:
zero==0, nought==0, one==1, two==2, paiz==2, three==3
Значения, принимаемые перечислимыми константами, могут быть заданы не только в виде целочисленных констант, но и в виде выражений. Например,
enum { two = 2, four = two * 2 };
определит константы
two==2 и four==4
Так как отрицательная целая константа - это константа без знака, к которой применена унарная операция " - " (минус), то перечислимые константы могут иметь и отрицательные значения.
Для перечислимых констант может быть введено имя типа, соответствующего приведенному списку констант. Имя типа - это произвольно выбираемый уникальный идентификатор, помещаемый между служебным словом enum и открывающейся фигурной скобкой ' } '.
Например, определение
enum week { sunday, monday, tuesday, wednesday,
thursday, friday, saturday };
только определяет константы sunday==0, monday==l, ..., но и вводит перечислимый тип с именем week, который может в дальнейшем использоваться в определениях и описаниях других объектов.
Символьные (литерные) константы - это один или два символа, заключенные в апострофы. Односимвольные константы имеют стандартный тип char. Для представления их значений могут вводится переменные символьного типа, т.е. типа char. Примеры констант: ' z ' , '*', '\012', '\0' , ' \n' - односимвольные константы. 'db' , '\х07\x07 ', '\n\t' - двух символьные константы. В этих примерах заслуживают внимания последовательности, начинающиеся со знака '\ '. Символ обратной косой черты ' \ ' используется, во-первых, при записи кодов, не имеющих графического изображения, и, во-вторых, символов апостроф ( ' ), обратная косая черта (\), знак вопроса (?) и кавычки ( " ). Кроме того, обратная косая черта позволяет вводить символьные константы, явно задавая их коды в восьмеричном или шестнадцатеричном виде.
Последовательности литер, начинающиеся со знака ' \ ', называют эскейп-последовательностями. В табл. 2.3 приведены их допустимые значения.
В табл. 2.3 000 - строка от 1 до 3 восьмеричных цифр; hh - строка из 1 или 2 шестнадцатеричных цифр. Строка восьмеричных цифр может содержать любое целое восьмеричное число в диапазоне от 0 до 377. Превышение этого верхнего значения приводит к ошибке. Наиболее часто в программах используется последовательность '\0', обозначающая пустую (null) литеру. В эскейп-последовательности вслед за \х может быть записано любое количество шестнадцатеричных цифр. Таким образом, допустимы, например, константа \x0004F и ее аналог \х4F. Однако числовое значение не должно выходить за диапазон от 0х0 до OXFF. Большее значение вызывает ошибку при компиляции. Если непосредственно за символом ' \ ' поместить символ, не предусмотренный таблицей 2.3, то результат будет неопределенным. Если среди восьмеричных цифр последовательности \ооо или шестнадцатеричных в \xhh встретится неподходящий символ, то это считается концом восьмеричного или соответственно шестнадцатеричного кода.
Таблица2.3
Допустимые ESC-последовательности в языке Си++
Изображение | Внутренний код | Обозначаемый символ (название) | Реакция или смысл |
\а | 0х07 | bel (audible bell) | Звуковой сигнал |
\b | 0х08 | bs (backspace) | Возврат на шаг (забой) |
\f | ОхОС | ff (form feed) | Перевод страницы (формата) |
\n | ОхОА | lf (line feed) | Перевод строки (новая строка) |
\r | 0х0D | cr (carriage return) | Возврат каретки |
\t | 0х09 | ht (horizontal tab) | Табуляция горизонтальная |
\v | ОхОВ | vt (vertical tab) | Табуляция вертикальная |
\\ | Ох5С | \ (backslash) | Обратная косая черта |
\' | 0х27 | ' (single quote) | Апостроф (одинарная кавычка) |
\" | 0х22 | " (double quote) | Двойная кавычка |
\? | Ox3F | ? (question mark) | Вопросительный знак |
\000 | 000 | Любой (octal number) | Восьмеричный код символа |
\xhh | Oxhh | Любой (hex number) | Шестнадцатеричный код символа |
Для использования внутренних кодов символов при работе, например, с экраном дисплея нужна таблица, в которой каждому изображаемому на экране символу соответствует числовое значение его кода в десятичном, восьмеричном, шестнадцатеричном представлении. На IBM-совместимых ПЭВМ применяется таблица кодов ASCII (см. Приложение 1). Выбирая из кодовой таблицы подходящее значение, можно (если это полезно по каким-либо причинам) использовать их в программе вместо явных изображений символов. Например:
//Р2-ОЗ.СРР - использование кодов символьных констант
#include < iostream.h >
void main()
{ cout << ' \хОA' << ' \х48' << '\х65' << '\х6C' << '\х6С';
cout << '\x6F' << '\х2С';
cout << '\40'<< '\127'<< '\157';
cout << '\162' << '\154' << '\144' << '\41';
}
Программа с новой строки выведет на экран:
Hello, World!
В первом и втором из операторов вывода использованы шестнадцатеричные, а в третьем и четвертом - восьмеричные коды символов: '\хОА' - '\n'; '\х48' - 'Н'; ...; '\40' -"пробел"; '\127' - 'W'; ...; '\41'-' ! '.
Значением символьной константы является числовое значение ее внутреннего кода. Как упоминалось, в Си++ односимвольная константа имеет тип char, т.е. занимает в памяти 1 байт. Двух символьные константы вида '\t\n' или '\r\07' представляются двухбайтовыми значениями типа int, причем первый символ ( ' \t или ' \г ' в примерах) размещается в младшем байте (с меньшим адресом), а второй (в примерах ' \n ' или ' \07 ' ) - в старшем байте.
Перечисленные соглашения иллюстрирует следующая программа:
//Р2-04.СРР - длины внутренних представлений символьных
// констант
#include < iostream.h >
void main ()
{ cout << " \n Длины символьных (литерных) констант ";
cout << " (в байтах) : ";
cout << "\nsireof\'z\ ' = " << sizeof ' z ';
cout << "\nsizeof\'\\n\' = " << sizeof ' \n';
cout << " \nsizeof\'\\n\\t\" = " << sizeof '\n\f';
cout << "\nsizeof\'\\x07\\x07\' = " << sizeof '\x07\x07';
cout << "\nsizeof\'\\x0004F\' = " << sizeof '\x0004F';
cout << "\nsizeof\'\\x4F\' = " << sizeof '\x4F';
cout << "\nsizeof\'\\111\' = " << sizeof '\111' ;
cout << "\nДесятичное значение ";
cout << "кода символа \'\\x0004F\ ' = " << (int) ' \x0004E';
cout << "\nДесятичное значение ";
cout << "кода символа \'\\x4F\ ' = " << (int) ' \x4F';
cout << "\nДесятичное значение хода пробела = ";
cout << (int) ' ';
}
В результате выполнения программы на экран будет выведено:
Длины символьных (литерных) констант (в байтах):
sizeof ' z ' = 1
sizeof '\n' = 1
sizeof '\n\t = 2
sizeof '\x07\x07' = 2
sizeof '\x0004F' = 1
sizeof '\x4F' = 1
sizeof '\111' = 1
Десятичное значение кода символа '\x0004F' = 79
Десятичноe значение кода символа '\x4F' = 79
Десятичное значение кода пробела = 32
Строка, или строковая константа, иногда называемая литерной строкой, определяется как последовательность символов, заключенная в кавычки (не в апострофы):
"Это строка, навиваемая также строковой константой"
Говоря о строках, иногда используют термины "строковый литерал" [5], "стринговый литерал", "стринговая константа", "стринг" [3], однако мы будем придерживаться традиционной терминологии, так как опыт показывает, что возможное неоднозначное толкование термина "строка" легко устраняется контекстом. (В английском языке этой проблемы не возникает, так как используются два различных слова: line - линия, строка текста и string - серия, ряд, последовательность, гирлянда...) Среди символов строки могут быть эскейп-последовательности, т.е. сочетания, соответствующие не изображаемым символьным константам или символам, задаваемым значениями их внутренних кодов. В этом случае, как и в представлениях отдельных символов, они начинаются с обратной косой черты ' \ ';
//Р2-05.СРР - строки c эскейп-последовательностями
#include < iostream.h >
void main ()
{ cout << " \nЭто строка, \nиначе - \"стринг\",\nиначе - ";
cout << " \ "строковый литeрал\" . ";
}
При выводе на экран дисплея этих строк эскейп-последовательности '\n' и '\" ' обеспечат такое размещение информации:
Это строка,
иначе - "стринг",
иначе - "строковый литерал".
Обратите внимание на наличие символа ' \ ' перед двойной кавычкой внутри строки. Именно по наличию этого символа компилятор отличает внутреннюю кавычку от кавычки, ограничивающей строку.
Строки, записанные в программе подряд или через пробельные разделители, при компиляции конкатенируются (склеиваются). Таким образом, в тексте программы последовательность из двух строк:
"Строка - это массив символов."
"Строка имеет тип char[] . "
эквивалентна одной строке:
"Строка - это массив символов. Строка имеет тип char[] . "
Длинную строковую константу можно еще одним способом разместить в нескольких строках текста программы, используя специальное обозначение переноса - ' \ '. Пример одной строковой константы, размещенной на трех строках в тексте программы:
"Обычно транслятор отводит \
каждой строковой константе \
отдельное место в памяти ЭВМ."
Следующая программа иллюстрирует разные формы "склеивания" строк в одну:
//Р2-06.СРР - конкатенация строк
#include < iostream.h >
void main ()
{ cout << "\n1" "9" "93"
" год" ; // При выводe пробелы будут удалены
cout << " начался c\
пятницы. " ; // Пробелы из этой строки сохранятся
}
Результат выполнения программы:
1993 год начался с пятницы.
Обратите внимание на количество пробелов в результате перед словом "пятницы". Продолжением перенесенной с помощью символа ' \ ' строки считается любая информация на следующей строке, в том числе и пробелы.
Размещая строку в памяти, транслятор автоматически добавляет в ее конец символ ' \0 ' , т.е. нулевой байт. Таким образом, количество символов во внутреннем представлении строки на 1 больше числа символов в ее записи. Пустая строка хранится как один символ " \0 ".
Кроме непосредственного использования строк в выражениях, строку можно поместить в символьный (типа char) массив с выбранным именем и в дальнейшем обращаться к ней по имени массива. Чаще всего для размещения строковой константы в массиве используется его инициализация. Следующая программа выполняет указанные действия:
//Р2-07.СРР - инициализация массива строковой константой
#include < iostream.h >
void main ()
{ char stroka [] = "REPETITIO EST MATER STUDIORUM";
cout << "\nsizeof stroka = " << sizeof stroka;
cout << "\nstroka = " << stroka;
}
Результат выполнения программы:
sizeof stroka = 30
stroka = REPETITIO EST MATER STUDIORUM
Обратите внимание, что при определении массива char после его имени stroka в скобках [] не указано количество элементов. Размер массива подсчитывается автоматически во время инициализации и равен количеству символов в строковой инициализирующей константе (в нашем случае 29) плюс один элемент для завершающего символа ' \0 '.
Кавычки не входят в строку, а служат ее ограничителями при записи в программе. В строке может быть один символ, например, "А" -строка из одного символа. Однако в отличие от символьной константы 'А' длина внутреннего представления строки "А" равна 2. Строка может быть пустой " " , при этом ее длина равна 1. Однако символьная константа не может быть пустой, т.е. запись ' ' в большинстве реализаций недопустима.
//Р2-08.СРР - длины строк и символьных констант (литер)
#include < iostream.h >
void main ()
{ cout << "\nsizeof\"\" = " << sizeof " ";
cout << "\tsizeof\'A\' = " << sizeof 'A';
cout << "\tsizeof\"A\" = " << sizeof "A";
cout << "\nsizeof\'\\n\' = " << sizeof '\n';
cout << "\tsizeof\"\\n\" = " << sizeof "\n ";
cout << "\nsizeof\'\\xFF\" = " << sizeof '\xFF';
cout << "\tsizeof\"\\xFF\" = " << sizeof "\xFF";
}
Результат выполнения программы:
sizeof** = 1 sizeof'A' = 1 sizeof"A" = 2
sizeof'\n' = 1 sizeof"\n" = 2 sizeof'\xFF' = 1
sizeof"\xFF" = 2
Знаки операций обеспечивают формирование и последующее вычисление выражений. Выражение есть правило для получения значения. Один и тот же знак операции может употребляться в различных выражениях и по-разному интерпретироваться в зависимости от контекста. Для изображения операций в большинстве случаев используется несколько символов. В ANSI-стандарте языка Си определены следующие знаки операций [3,5 ]:
[ ] ( ) . -> ++ - -
& * + - ~ !
sizeof / % << >> <
> <= >= == != ^
| && || ?: = *=
\= %= += -= <<= >>=
&= ^= |= , # ##
Дополнительно к перечисленным в Си++ введены:
: : .* ->* new delete typeid
За исключением операций [], () и ? : все знаки операций распознаются компилятором как отдельные лексемы. В зависимости от контекста одна и та же лексема может обозначать разные операции. Например, бинарная операция & - это поразрядная конъюнкция, а унарная операция & - это операция получения адреса.
Одним из принципиальных отличий языка Си++ от предшествующего ему языка Си является возможность расширения действия, иначе перегрузки (overload) стандартных операций, т.е. распространения их действия на нестандартные для них операнды. Материал, относящийся к расширению действия (перегрузке) операций, будет рассматриваться в следующих главах. Сейчас опишем кратко стандартные возможности отдельных операций.
& операция получения адреса операнда;
* операция обращения по адресу, т.е. раскрытия ссылки, иначе операция разыменования (доступа по адресу к значению того объекта, на который указывает операнд). Операндом должен быть адрес;
- унарный минус - изменяет знак арифметического операнда;
+ унарный плюс (введен для симметрии с унарным минусом); поразрядное инвертирование внутреннего двоичного кода целочисленного аргумента (побитовое отрицание);
~ логическое отрицание (НЕ) значения операнда; применяется к скалярным операндам; целочисленный результат 0 (если операнд ненулевой, т.е. истинный) или 1 (если операнд нулевой, т.е. ложный). В качестве логических значений в языке Си++ используют целые числа: о - ложь и не нуль (! о) - истина. Отрицанием любого ненулевого числа будет 0, а отрицанием нуля будет 1. Таким образом: ! l равно 0; ! 2 равно 0; ! (-5) равно 0; ! 0 равно 1;
++ увеличение на единицу (инкремент или авто увеличение): префиксная операция - увеличение значения операнда на 1 до его использования; постфиксная операция - увеличение значения операнда на 1 после его использования. Операнд не может быть константой либо другим праводопустимым выражением. Записи ++5 или 84++ будут неверными. Операндом не может быть и произвольное выражение. Например, ++( j+k ) также неверная запись. Операндом унарных операций ++ и - - должны быть всегда лево допустимые выражения, например, переменные (разных типов); уменьшение на единицу (декремент или авто уменьшение) -операция, операндом которой не может быть константа и право допустимое выражение: префиксная операция - уменьшение значения операнда на 1 до его использования; постфиксная операция - уменьшение значения операнда на 1 после его использования;
sizeof операция вычисления размера (в байтах) для объекта того типа, который имеет операнд. Разрешены два формата операции: sizeof унарное_выражвие и sizeof(тип).
Примеры использования операций с простейшими унарными выражениями, к которым относятся константы, приводились в связи с изложением материала о константах (см. Р2-04.СРР). Проиллюстрируем применение этой операции со стандартными типами:
//Р2-09.СРР - размеры разных типов данных
#include < iostream.h >
void main ()
{ cout << " \nsizeof(int) = << " sizeof (int);
cout << "\tsizeof(short) = << " sizeof (short) ;
cout << " \tsizeof(long) = << " sizeof (long) ;
cout << "\nsizeof(float) = << " sizeof (float) ;
cout << "\tsizeof(double) = << " sizeof (double) ;
cout << "\tsizeof(char) = << " sizeof(char) ;
}
Результат выполнения программы:
sizeof(int) = 2 sizeof(short) = 2 sizeof(long) = 4
sizeof(float) = 4 sizeof(double) = 8 sizeof(char) = 1
Бинарные операции. Эти операции делятся на следующие группы:
Аддитивные операции:
+ бинарный плюс (сложение арифметических операндов или сложение указателя с целочисленным операндом);
- бинарный минус (вычитание арифметических операндов или указателей).
Мультипликативные операции:
* умножение операндов арифметического типа;
/ деление операндов арифметического типа. Операция стандартна. При целочисленных операндах абсолютное значение результата округляется до целого. Например, 20/3 равно 6, -20/3 равняется -6, (-20) / 3 равно -6, 20/(-3) равно -6;
% получение остатка от деления целочисленных операндов (деление по модулю). При неотрицательных операндах остаток положительный. В противном случае остаток определяется реализацией. В компиляторах ТС++ и ВС++:
13%4 равняется 1, (-13) %4 равняется -1,
13 % (-4) равно + 1, а (-13) % (-4) равняется-1
При ненулевом делителе для целочисленных операндов всегда выполняется соотношение: (a/b)*b + а%b равно а.
Операции сдвига (определены только для целочисленных операндов). Формат выражения с операцией сдвига:
операнд_левый операция_сдвига операнд_правый
" сдвиг влево битового представления значения левого целочисленного операнда на количество разрядов, равное значению правого целочисленного операнда;
" сдвиг вправо битового представления значения левого целочисленного операнда на количество разрядов, равное значению правого целочисленного операнда.
Поразрядные операции:
& поразрядная конъюнкция (И) битовых представлений значений целочисленных операндов;
| поразрядная дизъюнкция (ИЛИ) битовых представлений значений целочисленных операндов;
^ поразрядное исключающее ИЛИ битовых представлений значений целочисленных операндов.
Следующая программа иллюстрирует особенности операций сдвига и поразрядных операций.
//Р2-10.СРР - операции сдвига и поразрядные операции
#include < iostream.h >
void main ()
{ cout << "\n4<< 2 равняется" << (4<<2);
cout << "\t5>>1 равняется " << (5>>1);
cout << "\n6&5 равняется " << (6&5);
cout << "\t6|5 равняется " << (6 | 5);
cout << "\t6^5 равняется " << (6^5);
}
Результат выполнения программы:
4 <<2 равняется 16
5 >>1 равняется 2
6&5 равняется 4
6|5 равняется 7
6^5 равняется 3
Тем, кто давно не обращал внимания на битовое представление целых чисел, напоминаем, что двоичный код для 4 равен 100, для 5 -это 101, для 6 - 110 и т.д. При сдвиге влево на 2 позиции код 100 становится равным 10000 (десятичное значение равно 16). Остальные результаты операций сдвига и поразрядных операций могут быть прослежены аналогично.
Обратите внимание, что сдвиг влево на n позиций эквивалентен умножению значения на 2n, а сдвиг вправо кода уменьшает соответствующее значение в 2n раз с отбрасыванием дробной части результата. (Поэтому 5>>l равно 2.)
Операции отношения (сравнения):
< меньше, чем;
> больше, чем;
<= меньше или равно;
>= больше или равно;
== равно;
!= не равно;
Операнды в операциях отношения арифметического типа или указатели. Результат целочисленный: 0 (ложь) или 1 (истина). Последние две операции (операции сравнения на равенство) имеют более низкий приоритет по сравнению с остальными операциями отношения. Таким образом, выражение (х < B == А < х) есть 1 тогда и только тогда, когда значение х находится в интервале от А до в. (Вначале вычисляются х < B и A < х,а к результатам применяется операция сравнения на равенство ==.)
Логические бинарные операции:
&& конъюнкция (И) арифметических операндов или отношений. Целочисленный результат 0 (ложь) или 1 (истина);
|| дизъюнкция (ИЛИ) арифметических операндов или отношений. Целочисленный результат 0 (ложь) или 1 (истина).
(Вспомните о существовании унарной операции отрицания ' f '.)
Следующая программа иллюстрирует некоторые особенности операций отношения и логических операций:
//Р2-11.СРР - операции отношения и логические операции
#include < iostream.h >
void main ()
{ cout << "\n3<5 равняется " << (3<5);
cout << "\t3>5 равняется " << (3>5);
cout << "\3==5 равняется " << (3==5);
cout << " \t3!=5 равняется " << (3!=5);
cout << "\n!=5 || 3==5 равняется " << (3!=5| |3==5);
cout << " \n+4>5 && 3+5>4 && 4+5>3 равняется " << (3+4>5 && 3+5>4 && 4+5>3);
}
Результат выполнения программы:
3<5 равняется 1
3>5 равняется О
3==5 равняется О
3!=5 равняется 1
3!=5 | | 3==5 равняется 1
3+4>5 && 3+5>4 && 4+5>3 равняется 1
Операции присваивания.
В качестве левого операнда в операциях присваивания может использоваться только модифицируемое l-значение - ссылка на некоторую именованную область памяти, значение которой доступно изменениям. Термин l-значение (left value), иначе - лево допустимое выражение, происходит от объяснения действия операции присваивания Е = D, в которой операнд Е слева от знака операции присваивания может быть только модифицируемым l-значением. Примером модифицируемого l-значения служит имя переменной, которой выделена память и соответствует некоторый класс памяти. Итак, перечислим операции присваивания:
= присвоить значение выражения-операнда из правой части операнду левой части: Р = 10.3 - 2*х;
*= присвоить операнду левой части произведение значений обоих операндов:
P *= 2 эквивалентно Р = Р * 2;
/= присвоить операнду левой части частное от деления значения левого операнда на значение правого:
р /= 2.2 - d эквивалентно Р = Р / (2.2 - d);
%= присвоить операнду левой части остаток от деления целочисленного значения левого операнда на целочисленное значение правого операнда:
N %= 3 эквивалентно N = N % 3;
+= присвоить операнду левой части сумму значений обоих операндов:
А += в эквивалентно А = А + B;
-= присвоить операнду левой части разность значений левого и правого операндов:
х ~= 4.3 - z эквивалентно х = х - (4.3 - z ) ;
<<= присвоить целочисленному операнду левой части значение, полученное сдвигом влево его битового представления на количество разрядов, равное значению правого целочисленного операнда:
а <<= 4 эквивалентно а = а << 4;
>>= присвоить целочисленному операнду левой части значение, полученное сдвигом вправо его битового представления на количество разрядов, равное значению правого целочисленного операнда:
а >>= 4 эквивалентно а = а >> 4;
&= присвоить целочисленному операнду левой части значение, полученное поразрядной конъюнкцией (И) его битового представления с битовым представлением целочисленного операнда правой части:
e &= 44 эквивалентно e = е & 44;
| = присвоить целочисленному операнду левой части значение, полученное поразрядной дизъюнкцией (ИЛИ) его битового представления с битовым представлением целочисленного операнда правой части:
а | = b эквивалентно а = а | b;
^= присвоить целочисленному операнду левой части значение, полученное применением поразрядной операции исключающего ИЛИ к битовым представлениям значений обоих операндов:
z ^= х + у эквивалентно z = z ^ (x + у).
Обратите внимание, что для всех операций сокращенная форма присваивания EI ор= Е2 эквивалентна El = E1 ор (Е2),где ор-обозначение операции.
Для иллюстрации некоторых особенностей выполнения операций присваивания рассмотрим следующую программу:
//Р2-12.СРР - операции присваивания
#include < iostream.h >
void main ()
{ int k ;
cout << "\n\n k = 35/4 равняется " << (k=35/4);
cout << "\t k /= 1 + 1 + 2 равняется " << (k/=1+1+2);
cout << "\n k *= 5 - 2 равняется " << (k*=5-2);
cout << "\t k %= 3 + 2 равняется " << (k%=3+2);
cout << "\n k += 21/3 равняется " << (k+=21/3);
cout << "\t k -= 6 - 6/2 равняется " << (k-=6-6/2);
cout << "\n k <<= 2 равняется " << (k<<= 2);
cout << "\t k "= 6-5 равняется " << (k"=6-5);
cout << "\n k &= 9 + 4 равняется " << (k&=9+4);
cout << "\t k |= 8 - 2 равняется " << (k|=8-2);
cout << "\n k ^= 10 равняется " << (k^=1O);
}
В первом присваивании обратите внимание на выполнение деления целочисленных операндов, при котором выполняется округление за счет отбрасывания дробной части результата.
Результаты выполнения:
k = 35/4 равняется 8 k /= 1 + 1 + 2 равняется 2
k *= 5 - 2 равняется 6 k %= 3 + 2 равняется 1
k += 21/3 равняется 8 k -= 6 - 6/2 равняется 5
k <<= 2 равняется 20 k <<= 6-5 равняется 10
k &= 9 + 4 равняется 8 k |= 8 - 2 равняется 14
k ^= 10 равняется 4
Полученные числовые значения, во-первых, подтверждают эквивалентность записей El ор= Е2 иЕ1 = E1 ор (Е2). Кроме того, анализируя результаты, можно еще раз рассмотреть особенности поразрядных операций. Двоичный код для k, равного 5, будет 101. Сдвиг влево на 2 дает 10100 (десятичное 20). Затем сдвиг на 1 вправо формирует код 1010 (десятичное 10). Поразрядная конъюнкция 1010&1101 дает 1000 (десятичное 8). Затем 1000 | 110 дает значение 1110 (десятичное 14). Результатом 1110^1010 будет 0100 (десятичное 4).
Операции выбора компонентов структурированного объекта:
(точка) прямой выбор (выделение) компонента структурированного объекта, например объединения. Формат применения операции:
имя структурированного объекта.имя компонента
-> косвенный выбор (выделение) компонента структурированного объекта, адресуемого указателем. При использовании операции требуется, чтобы с объектом был связан указатель. В этом случае формат применения операции имеет вид:
указатель на структурированный объект -> имя компонента
Так как операции выбора компонентов структурированных объектов используются со структурами, объединениями, классами, то необходимые пояснения и примеры мы приведем позже, введя перечисленные понятия и, кроме того, определив указатели.
Операции с компонентами классов:
* прямое обращение к компоненту класса по имени объекта и указателю на компонент;
->* косвенное обращение к компоненту класса через указатель на объект и указатель на компонент.Комментировать эти операции затруднительно, не введя понятие класса, что будет сделано позже.
:: операция указания области видимости имеет две формы: бинарную и унарную. Бинарная форма применяется для доступа к компоненту класса. Унарная операция ':: ' позволяет получить доступ к внешней для некоторой функции именованной области памяти. Следующая программа поясняет возможности унарной операции:
/ / Р2-13.СРР - изменение видимости внешней переменной
#include < iostream.h >
int k = 15; // Глобальная переменная с начальным значением
void main ()
{ int k = 10; // Локальная переменная с начальным значением
cout << "\nВнешняя переменная k = " << : : k;
cout << "\nВнутренняя переменная k = " << k;
::k = О;
cout << "\nВнешняя переменная k = " << : :k;
cout << "\nВнутренняя переменная k = " << k;
}
Результат выполнения программы:
Внешняя переменная k = 15
Внутренняя переменная k = 10
Внешняя переменная k = О
Внутренняя переменная k = 10
Как видно из примера, с помощью унарной операции ": : " можно организовать доступ из тела функции к внешней переменной, если переменная с тем же именем определена внутри функции. Несколько другие возможности обеспечивает та же операция при работе с классами. Об этих ее особенностях речь пойдет позже.
Запятая в качестве операции:
, несколько выражений, разделенных запятыми, вычисляются последовательно слева направо. В качестве результата сохраняются тип и значение самого правого выражения. Таким образом, операция "запятая" группирует вычисления слева направо. Тип и значение результата определяются самым правым из разделенных запятыми операндов (выражений). Значения всех левых операндов игнорируются. Например:
/ / Р2-14.СРР - запятая в качестве знака операции
#include < iostream.h >
void main ()
{ int d;
cout << "\nВыражение d = 4, d*2 равно " << (d=4, d*2);
cout << ", d равно " << d;
}
Программа выведет на экран:
Выражение d = 4, d*2 равно 8, d равно 4
Круглые ' () ' и квадратные ' [] ' скобки играют роль бинарных операций при вызове функций и индексировании элементов массивов. Для программиста, мало знакомого с техникой использования указателей, мысль о том, что скобки в ряде случаев являются бинарными операциями, часто даже не приходит в голову. И это тогда, когда он практически в каждой программе обращается к функциям или применяет индексированные переменные. Итак, отметим, что скобки могут служить бинарными операциями, особенности и возможности которых достойны внимания.
Круглые скобки обязательны в обращении к функции:
имя_функции (список_аргументов)
Здесь операндами служат имя_функции и список_аргументов. Результат вызова вычисляется в теле функции, структуру которого задаст ее определение.
В выражении имя_массива [индекс] операндами для операции ' [] ' служат имя_массива и индекс. В языках Си и Си++ принято, что индексы массивов начинаются с нуля, т.е. массив int z[3] из трех элементов включает индексированные элементы z [0], z [ l ] , z [ 2 ]. Это соглашение языка становится очевидным, если учесть, что индекс определяет не номер элемента, а его смещение относительно начала массива. Таким образом, z[0] - обращение к первому элементу, z[l] - обращение ко второму элементу и т.д. В следующей программе показано, как обычно используют квадратные скобки при работе с элементами массива:
/ / Р2-15.СРР - работа с элементами массива
#include < iostream.h >
void main ()
{ char x[ ] = "DIXI" ; / / "Я СКАЗАЛ" (высказался)
int i = 0;
while (x(i] ! = '\0' )
cout << "\n" << x [i++] ;
}
Результат - слово "DIXI", написанное в столбик (сверху вниз):
D
I
х
I
Оператор цикла с заголовком while выполняется, пока верно выражение в скобках, т.е. пока очередной символ массива не равен ' \0 '. При каждом вычислении выражения x[i++] используется текущее значение i , которое затем увеличивается на 1. В данном случае квадратные скобки играют роль бинарной операции, а операндами служат имя массива х и индекс i++.
В Си++ действие операций ' [ ] ' и ' ( ) ' расширено (они перегружены - overloaded), но об этом нужно говорить в связи с классами, что будет сделано позже.
Именно из-за появления в языке Си++ механизма расширения действия (перегрузки - overload) стандартных операций и в связи с необходимостью расширять действие скобок-операций на необычные для них операнды скобки ' [ ] ', ' ( ) ' отнесены к стандартным операциям языка Си++.
Условная операция. В отличие от унарных и бинарных операций условная операция используется с тремя операндами. В изображении условной операции два размещенных не подряд символа ' ? ', и ' : ' и три операнда-выражения:
выражениие_1 ? выражение_ 2 : выражение_ 3
Первым вычисляется значение выражения_1. Если оно истинно, т.е. не равно нулю, то вычисляется значение выражения_2, которое становится результатом. Если при вычислении выражения_1 получится 0, то в качестве результата берется значение выражения_3. Классический пример:
х < о ? -х : х ;
Выражение возвращает абсолютное значение переменной х.
Операция явного преобразования (приведения) типа в языке Си++ имеет две различные формы. Каноническая, унаследованная от языка Си, имеет следующий формат:
( имя_типа)операнд
и позволяет преобразовывать значение операнда к нужному типу. В качестве операнда используется унарное выражение, которое в простейшем случае может быть переменной, константой или любым выражением, заключенным в круглые скобки. Например, следующие преобразования изменяют длину внутреннего представления целых констант, не меняя их значений:
(long) l - внутреннее представление имеет длину 4 байта;
(char) l - внутреннее представление имеет длину 1 байт.
В этих преобразованиях константа не меняла значения и оставалась целочисленной. Однако возможны более глубокие преобразования, например, (long double)1 или (float)1 не только изменяют длину константы, но и структуру ее внутреннего представления. В ней будут выделены порядок и мантисса. При преобразовании длинных целочисленных констант к вещественному типу (например, к типу float) возможна потеря значащих цифр (потеря точности). Если вещественное значение преобразуется к целому, то возможна ошибка при выходе полученного значения за диапазон допустимых значений для целых. В этом случае результат преобразования непредсказуем и целиком зависит от реализации.
Кроме рассмотренной канонической операции явного приведения типа, которая унаследована языком Си++ от языка Си, в языке Си++ введена еще одна возможность приведения типов, которую обеспечивает функциональная форма преобразования типа:
имя типа(операнд)
Она может использоваться только в тех случаях, когда тип имеет простое (несоставное) наименование (обозначение):
long (2) - внутреннее представление имеет длину 4 байта;
double (2) - внутреннее представление имеет длину 8 байтов.
Однако будет недопустимым выражение:
unsigned long(2) / / Ошибка!
Операции new и delete для динамического распределения памяти.
Это еще две особые унарные операции, появившиеся в языке Си++. Они связаны с одной из задач управления памятью, а именно с ее динамическим распределением. Операция
new имя_ типа либо
new имя_типа инициализатор
позволяет выделить и сделать доступным свободный участок в основной памяти, размеры которого соответствуют типу данных, определяемому именем типа. В выделенный участок заносится значение, определяемое инициализатором, который не является обязательным элементом. В случае успешного выполнения операция new возвращает адрес начала выделенного участка памяти. Если участок нужных размеров не может быть выделен (нет памяти), то операция new возвращает нулевое значение адреса (NULL). Синтаксис применения операции:
указатель = new имя_типа инициализатор
Здесь необязательный инициализатор - это выражение в круглых скобках. Указатель, которому присваивается получаемое значение адреса, должен относиться к тому же типу данных, что и имя_типа в операции new. О размерах участков памяти уже упоминалось в связи с константами разных типов. Поэтому, не вводя пока других понятий, относящихся к типам данных, приведем несложные примеры.
Операция new float выделяет участок памяти размером 4 байта. Операция new int (l5) выделяет участок памяти в 2 байта и инициализирует этот участок целым значением 15. Синтаксис использования операций new и delete предполагает применение указателей. Предварительно каждый указатель должен быть определен.
Определение указателя имеет вид:
тип *имя_указателя ;
Имя_указателя - это идентификатор. В качестве типа можно использовать, например, уже упомянутые стандартные типы int, long, float, double, char. Таким образом, int *h; - определение указателя h, который может быть связан с участком памяти, выделенным для величины целого типа. Введя с помощью определения указатель, можно присвоить ему возвращаемое операцией new значение:
h = new int(15);
В дальнейшем доступ к выделенному участку памяти обеспечивает выражение *h.
В случае отсутствия в операции new инициализатора значение, которое заносится в выделенный участок памяти, не определено и не следует рассчитывать, что там будет, например, нуль.
Если в качестве имени типа в операции new используется массив, то для массива должны быть полностью определены все размерности. Но и при этом инициализация участка памяти, выделяемого для массива, запрещена. Подробнее о выделении памяти для массивов речь пойдет в главе 5.
Продолжительность существования выделенного с помощью операции new участка памяти - от точки создания до конца программы или до явного его освобождения.
Для явного освобождения выделенного операцией new участка памяти используется оператор delete указатель; где указатель адресует освобождаемый участок памяти, ранее выделенный с помощью операции new.
Например, delete h; - освободит участок памяти, связанный с указателем h. Повторное применение операции delete к тому же указателю дает неопределенный результат. Также непредсказуем результат применения этой операции к указателю, получившему значение без использования операции new. Однако применение delete к указателю с нулевым значением не запрещено, хотя и не имеет особого смысла.
Иллюстрацией к сказанному служит следующая программа, в которой с одним указателем целого типа последовательно связываются четыре динамически выделяемых участка памяти:
/ / Р2-16.СРР - динамическое распределение памяти
#include < iostream.h >
void main ()
{ int *i;
i = new int(1);
cout << "\n*i=" << *i << " \t i=" << i;
i = new int(5);
cout << "\t*i=" << *i << " \t\t i=" << i;
i = new int (2**i);
cout << "\n*i="<<*i<<"\t i=" << i;
i = new int (2**i);
cout << "\t*i=" << *i << "\t\t i" << i;
delete i;
cout << "\n После освобождения памяти: ";
cout << "\n*i=" << *i << "\t i=" <
delete i; / / Некорректное применение операции
cout << "\t*i=" << *i << "\t i=" << i;
}
Результат выполнения программы:
*i=l i=0x91790004 *i=5 i=0x917a0004
*i-10 i=0x917b0004 *i=20 i=0ac917c0004
После освобождения памяти:
*i=20 i=0x917c0004 *i=-28292 i=0x917c0004
Обратите внимание, что после выполнения первого оператора delete i; значение указателя i и содержимое связанного с ним участка памяти *i еще сохранились. После вторичного применения операции delete значение указателя не изменилось, а содержимое связанного с ним участка памяти "испортилось". Указанные изменения и (или) сохранения значения *і не обязательны и полностью зависят от реализации и от конкретного исполнения программы. В ряде случаев при работе с интегрированной средой ВС++ версии 3.1 выдается сообщение об ошибке:
Null pointer assignment
Для освобождения памяти, выделенной для массива, используется следующая модификация того же оператора:
delete [] указатель;
где указатель связан с выделенным для массива участком памяти. Подробнее об этой форме оператора освобождения памяти будем говорить в связи с массивами и указателями в главе 5.
Ранги операций. Завершая краткий обзор операций языка Си++, приведем таблицу приоритетов, или рангов операций [4,9].
Таблица 2.4
Приоритеты операций
Ранг | Операции | Ассоциативность |
1 | ( ) [ ] -> : : . | -> |
2 | ! ~ + - ++ -- & * (тип) sizeof new delete тип ( ) (функциональное преобразование типа) | <- |
3 | . * -> * | -> |
4 | * / % (мультипликативные бинарные операции) | -> |
5 | + - (аддитивные бинарные операции) | -> |
6 | << >> | -> |
7 | < <= >= > | -> |
8 | == ! = | -> |
9 | & | -> |
10 | ^ | -> |
11 | | | -> |
12 | && | -> |
13 | | | | -> |
14 | ?: (условная операция) | <- |
15 | = *= /= %= += -= & = ^= |= <<= >>= | <- |
16 | , (операция "запятая") | -> |
Грамматика языка Си++ определяет 16 категорий приоритетов операций. В табл. 2.4 категории приоритетов названы рангами. Операции ранга 1 имеют наивысший приоритет. Операции одного ранга имеют одинаковый приоритет, и если их в выражении несколько, то они выполняются в соответствии с правилом ассоциативности либо слева направо (->), либо справа налево (<-). Если один и тот же знак операции приведен в таблице дважды, то первое появление (с меньшим по номеру, т.е. старшим по приоритету, рангом) соответствует унарной операции, а второе - бинарной.
Отметим, что кроме стандартных режимов использования операций язык Си++ допускает расширение их действия на объекты классов, вводимых пользователем или уже определенных в конкретной реализации языка.Примером такого расширения (перегрузки) является операция извлечения данных из потока << и операция передачи данных в выходной поток >>,применяемые к потокам ввода cin и вывода cout.
Разделители,или знаки пунктуации, входят в число лексем языка:
[ ] ( ) { } , ; : ... * = # &
Квадратные скобки ' [ ] ' ограничивают индексы одно- и многомерных массивов и индексированных элементов.
// Одномерный массив из пяти элементов:
int А [] = (О, 2, 4, 6, 8 );
// е - двумерный массив - матрица размерности 3х2:
int х, е[3] [2];
// Начальному элементу массива е и переменном х
// присваивается значение 4 (третий элемент массива А):
е[0] [0] = х = А[2];
Круглые скобки ' ( ) ' :
1) выделяют условные выражения (в операторе "если" ):
if (х < 0) х = -х; // Модуль значения
/ / арифметической переменной
2) входят как обязательные элементы в определение, описание (в прототип) и вызов любой функции, где выделяют соответственно список формальных параметров, список спецификаций параметров и список фактических параметров (аргументов):
float F(float x, int k) // Определение функции
{ тело_функции }
float F(float, int); // Описание функции - ее прототип
. . .
F(Z,n) ; // Вызов функции
3) обязательны в определении указателя на функцию:
int (*func)(void) ; / / Определение указателя на функцию
4) группируют выражения, изменяя естественную последовательность выполнения операций:
у = (а + b) / с; / / Изменение приоритета операций
5) входят как обязательные элементы в операторы циклов:
for (i = О, j = 1; i < j ; i += 2, j++) тело_цикла;
while (i < j) тело цикла;
do тело цикла while (k > 0);
6) необходимы при явном преобразовании типа. Как показано выше при описании операций, в Си++ существуют две формы задания явного преобразования типов: приведение типа (имя_типа) операнд и функциональное приведение - имя_типа ( операнд), где операнд - это любое скалярное выражение. В обеих формах явного приведения типа круглые скобки обязательны. Примеры:
long i = 12L; int j; // Определения переменных
j = int(i) ; // Функциональная запись преобразования
float В; // Определение переменной
В = (float) j; // Явное приведение типа
j получает значение 12L, преобразованное к типу int. Затем B получает значение 12, преобразованное к типу float.
7) применение круглых скобок настоятельно рекомендуется в макроопределениях, обрабатываемых препроцессором:
#define R(x,y) sqrt ((x)*(x) + (у)*(у))
Это позволяет использовать в качестве параметров макровызовов арифметические выражения любой сложности и не сталкиваться с нарушениями приоритетов операций (см. гл. 8).
Фигурные скобки ' { } ' обозначают соответственно начало и конец составного оператора или блока. Пример использования составного оператора в условном операторе:
if (d > x) { d - -; x++; }
Пример блока, являющегося телом функции:
float exponent (float x, int n)
{ float d = 1.0 ;
int i = 0
;
if (x==0) return 0.0;
for( ; i < abs(n) ; i++, d*=x);
return n > 0 ? d : 1.0/d;
}
Обратите внимание на отсутствие точки с запятой после закрывающейся скобки ' } ', обозначающей конец составного оператора или блока.
Фигурные скобки используются для выделения списка компонентов в определениях типов структур, объединений, классов:
struct cell { char *b; // Определение типа
int cc; / / структуры
double U[6];
};
union smes
{ unsigned int ii; // Определение типа
char cc[2]; / / объединения };
class sir
{ int В; / / Определение класса public:
int X, D;
sir(int);
};
Обратите внимание на необходимость точки с запятой после описания (определения) каждого типа.
Фигурные скобки используются при инициализации массивов и структур при их определении:
// Инициализация массива:
int month[ ] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
// Инициализация структуры stock типа mixture:
struct mixture { int ii; double dd; char cc; }
stock = { 666, 3.67, '\f' };
В примере mixture - имя типа структуры с тремя компонентами разных типов, stock - имя конкретной структуры типа mixture. Компоненты ii, dd, cc структуры stock получают значения при инициализации.
В следующей программе описана структура с именем constant и выполнена ее инициализация, т.е. ее компонентам типа long double присвоены значения знаменитых констант:
//Р2-17.СРР - фигурные скобки в структуре и при ее
// инициализации
#include < iostream.h >
void main ()
{ struct
{ long double pi;
long double c;
}
constant = { 3.1415926S35897932385,
2.7182818284590452354 }
cout << "\n" << constant.pі << "\t" << constant.c;
}
На печать (на экран дисплея) выводятся:
3.141593 2.718282
Обратите внимание на точность (только 7 значащих цифр) выбираемую по умолчанию при работе со стандартным потоком вывода.
Запятая ' , ' разделяет элементы списков. Во-первых, это списки начальных значений, присваиваемых индексированным элементам массивов и компонентам структур при их инициализации:
char name [ ] = { 'С', 'и' , 'р', 'a', 'н','о'}; // Это не строка!
struct A {int x; float у; char x;}
F= {3, 18.4, 'с'};
Другой пример списков - списки формальных и фактических параметров и их спецификаций в функциях.
Третье использование запятой как разделителя - в заголовке оператора цикла for:
for ( х = pl, у = р2, i = 2 ; i < n;
z = x + у, x = у, у = z, i++);
(В данном примере вычисляется n-й член ряда Фибоначчи z по значениям первых двух pl и р2.)
Запятая как разделитель используется также в описаниях и определениях объектов (переменных) одного типа:
int i, n; float х, у, z, р1, р2;
Запятая в качестве операции уже рассматривалась. Следует обратить внимание на необходимость с помощью круглых скобок отделять запятую-операцию от запятой-разделителя. Например, в следующей программе для элементов массива m используется список с тремя начальными значениями:
/ / Р2-18.СРР - запятая как разделитель и как знак операции
#include < iostream.h >
void main ()
{ int i = 1, m[ ] = { i, (i=2,i*i), i };
cout << "\ni = " << i << "\
}
Результат на экране:
i = 2 m[0] = 1 m[1] = 4 m[2] = 2
В данном примере запятая в круглых скобках выступает в роли знака операции. Операция присваивания "=" имеет более высокий приоритет, чем операция "запятая". Поэтому вначале i получает значение 2, затем вычисляется произведение i*i, и этот результат служит значением выражения в скобках. Однако значением переменной i остается 2.
В качестве еще одной области применения запятой как разделителя нужно отметить описание производного класса, где используются список базовых классов и список вызываемых конструкторов. В каждом из них могут понадобиться запятые. Кроме того, в списке однотипных компонентов класса они отделяются друг от друга запятыми.
Перечисленные применения запятой будут понятны после рассмотрения классов.
Точка с запятой ' ; ' завершает каждый оператор, каждое определение (кроме определения функции) и каждое описание.
Любое допустимое выражение, за которым следует ' ; ' , воспринимается как оператор. Это справедливо и для пустого выражения, т.е. отдельный символ "точка с запятой" считается пустым оператором. Пустой оператор часто используется как тело цикла. Примером может служить цикл for, приведенный выше для иллюстрации особенностей использования запятой в качестве разделителя. (Вычисляется n-й член ряда Фибоначчи.)
Примеры операторов-выражений:
// Результат выполнения - только изменение значения i F(z,4);
// Результат определяется телом функции с именем F
Двоеточие ' : ' служит для отделения (соединения) метки и помечаeмого ею оператора:
метка: оператор;
Метка - это идентификатор. Таким образом, допустимы, например, такие помеченные операторы:
xyz: а = (Ь - с) * (d - с); сc: z *= 1;
Второе применение двоеточия - описание производного класса, где имя класса отделяется от списка базовых классов двоеточием:
ключ_класса имя_класса: базовый_список {список _компонентов}
ключ_класса имя_класса: базовый_список {список _компонентов} ключ_класса - это одно из трех служебных слов: struct, union, class. Имя_класса - произвольно выбираемый идентификатор. Базовый_список - это список имен порождающих (базовых) классов. Не определяя списка компонентов (к чему вернемся, рассматривая классы), приведем пример определения производного класса:
class x: А, В {список_компонентов};
Многоточие - это три точки '... ' без пробелов между ними. Оно используется для обозначения переменного числа параметров у функции при ее определении и описании (при задании ее прототипа). При работе на языке Си программист очень часто использует библиотечные функции со списком параметров переменной длины для форматных ввода и вывода. Их прототипы выглядят следующим образом:
int printf(char *format, ...);
int acanf (char *format, ...);
Здесь с помощью многоточия указана возможность при обращении к функциям использовать разное количество параметров (не меньше одного, так как параметр format должен быть указан всегда и не может опускаться).
Подготовка своих функций с переменным количеством параметров на языке Си++ требует применения средств адресной арифметики, например, предоставляемых головным файлом stdarg.h. Описание макросов va_arg, va_end, va_start для организации доступа из тела такой функции к спискам ее параметров приведено в главе 6.
Звездочка '*', как уже упоминалось, используется в качестве знака операции умножения и знака операции разыменования (получения значения через указатель). В описаниях и определениях звездочка означает, что описывается указатель на значение использованного в объявлении типа:
int *point; // Указатель на величину типа int
char **refer; // Указатель на указатель
// на величину типа char
Знак '=', как уже упоминалось, является обозначением операции присваивания. Кроме того, в определении он отделяет описание объекта от списка его инициализации:
struct {char x, int у} А = {'z', 1918};
int F = 66;
В списке формальных параметров функции знак ' = ' указывает на выбираемое по умолчанию значение аргумента (фактического параметра):
char CC(int Z = 12, char L = '\0' ) {....}
По умолчанию параметр z равен 12, параметр L равен ' \о '.
Символ '#' (знак номера или диеза в музыке) используется для обозначения директив (команд) препроцессора. Если этот символ является первым отличным от пробела символом в строке программы, то строка воспринимается как директива препроцессора.
Символ '&' играет роль разделителя при определении переменных типа ссылки:
int В; // Описание переменной
int &A = В; // А - ссылка на В
Отметив использование символа '&' в качестве разделителя при описании ссылок, отложим подробное рассмотрение ссылок.
Разговор о типах начнем с переменных. В пособиях по языкам программирования переменную чаще всего определяют как пару "имя" - "значение". Имени соответствует адрес (ссылка) на участок памяти, выделенный переменной, а значением является содержимое этого участка. Именем служит идентификатор, а значение соответствует типу переменной, определяющему множество допустимых значений и набор операций, для которых переменная может служить операндом. Множество допустимых значений переменной обычно совпадает со множеством допустимых констант того же типа (см. табл. 2.1 - 2.2). Таким образом, вводятся вещественные, целые и символьные переменные, причем символьные (char) иногда относят к целым.
Целочисленные и вещественные считаются арифметическими типами. Арифметический (включая символьный) тип является частным случаем скалярных типов. К скалярным типам кроме арифметических относятся указатели, ссылки и перечисления. Перечисления (enum) уже введены при рассмотрении целых констант. К указателям и ссылкам вернемся немного позже. Переменные типизируются с помощью определений и описаний. Сразу же введем терминологические соглашения. В отличие от описания определение не только вводит объект (например, переменную), но и предполагает, что на основании этого определения компилятор выделит память для объекта (переменной). Для определения и описания переменных основных типов используются следующие ключевые слова, каждое из которых в отдельности может выступать в качестве имени типа:
При определении переменных им можно приписывать начальные значения, которые заносятся в выделяемую для них память в процессе инициализации. Примеры определений (описания с инициализацией):
char newsimbol = ' \n ';
long filebagin = OL;
double pi = 3.1415926535897932385;
В обозначении типа может использоваться одновременно несколько служебных слов. Например, определение
long double zebra, stop;
вводит переменные с именами zebra и stop вещественного типа повышенной точности, но явно не присваивает этим переменным никаких начальных значений.
Употребляемые как отдельно, так и вместе с другими именами типов служебные слова unsigned (беззнаковый) и signed (знаковый) позволяют для арифметического или символьного типа выбирать способ учета знакового разряда:
unsigned int i, j, k; // Значения от 0 до 65535
unsigned long L, M ,N; // Значения от 0 до 4294967295
unsigned char с, s; // Значения от 0 до 255
При таком определении переменные i, j, k могут принимать только целые положительные значения в диапазоне от 0 до 65535 и т.д.
Применение в определениях типов отдельных служебных слов fait, char, short, long эквивалентно signed int, signed char, signed short, signed long. Именно поэтому служебное слова signed обычно опускается в определениях и описаниях. Использование при задании типа только одного unsigned эквивалентно unsigned int.
При операциях с беззнаковыми (unsigned) целыми не возникает переполнений, так как используется арифметика по модулю 2 в степени n, где n - количество битовых разрядов, выделяемых для представления соответствующих значений.
Переменные одного типа занимают в памяти одно и то же количество единиц (байтов), и это количество единиц может быть всегда вычислено с помощью операции sizeof, как мы это делали в описании ее возможностей. Вот еще несколько примеров:
// РЗ-01.СРР - размеры разных типов данных
#include < iostream.h >
void main ()
{ int i;
Таблица 3.1
Основные типы данных
Тип данных | Размер, бит | Диапазон значений | Примечание-назначение типа |
unsigned char | 8 | 0...255 | Небольшие целью числа и коды символов |
char | 8 | -128...127 | Очень малью целью числа и ASCII-коды |
еnum | 16 | -32768...32767 | Упорядоченные наборы целых значений |
unsigned int | 16 | 0...65535 | Большие целые и счетчики циклов |
short int | 16 | -32768...32767 | Небольшие целые, управление циклами |
int | 16 | -32768...32767 | Небольшие целые, управление циклами |
unsigned long | 32 | 0...4294967295 | Астрономические расстояния |
long | 32 | -2147483648... ...2147483647 | Большие числа, популяции |
float | 32 | 3.4Е-38...3.4Е+38 | Научные расчеты (7 значащих цифр) |
double | 64 | 1.7Е-308...1.7Е+308 | Научные расчеты (15 значащих цифр) |
long double | 80 | 3.4Е-4932... ...1.1Е+4932 | Финансовые расчеты (19 значащих цифр) |
unsigned int ui;
long 1;
unsigned long ul;
double d;
long double ld;
cout << "\n sizeof (int) = " << sizeof(i);
cout << "\t sizeof (unsigned int) = " << sizeof(ui);
cout << "\n sizeof (long) = " << sizeof(1);
cout << "\t sizeof (unsigned long) = " << sizeof(ul);
cout << "\n sizeof (double) = " << sizeof(d);
cout << "\t sizeof (long double) = " << sizeof(ld);
}
Результаты выполнения:
sizeof (int) = 2 sizeof (unsigned int) = 2
sizeof (long) = 4 sizeof (unsigned long) = 4
sizeof (double) = 8 sizeof (long double) = 10
В табл. 3.1 приведены типы данных, их размеры в памяти и диапазоны допустимых значений для компиляторов, ориентированных на ПЭВМ семейства IBM PC/XT/AT (см., например, [9], с. 19). В таблицу не включены указатели, так как они будут подробно рассмотрены позже.
Используя, спецификатор typedef, можно в своей программе вводить удобные обозначения для сложных описаний типов. В следующем примере
typedef unsigned char COD;
COD simbol;
введен новый тип COD - сокращенное обозначение для unsigned char и переменная этого типа simbol, значениями которой являются беззнаковые числа в диапазоне от 0 до 255.
Рассматривая переменные, мы пока использовали базовые (предопределенные целиком или фундаментальные) типы, для обозначения которых употребляются по отдельности и в допустимых сочетаниях служебные слова char, int, signed, double, long, unsigned, float, short, void.
Из этих базовых типов с помощью операций ' * ', ' & ', ' [ ] ', ' ( ) ' и механизмов определения типов структурированных данных (классов, структур, объединений) можно конструировать множество производных типов. Обозначив именем type допустимый тип, приведем форматы некоторых производных типов:
type имя []
массив объектов заданного типа type. Например:
long int м[5]; - пять объектов типа long int, доступ к которым обеспечивают индексированные переменные M[0], М[1], М[2], М[3], М[4].
type1 имя (tуре2);
функция, принимающая аргумент типа type2 и возвращающая значение типа typel. Например:
int fl (void); - функция, не требующая аргументов и возвращающая значение типа int; void f2 (double); - функция, принимающая аргумент типа double и не возвращающая значений.
type *имя;
указатель на объекты типа type. Например, char *ptr ; определяет указатель ptr на объекты типа char.
type *имя[];
массив указателей на объекты типа type.
type (*имя) [];
указатель на массив объектов типа type.
typel *имя(type2);
функция, принимающая аргумент типа type2 и возвращающая указатель на объект типа typel.
typel (*имя) (type2);
указатель на функцию, принимающую параметр типа type2 и возвращающую значение типа typel. Например, описание int (*ptr) (char) ; определяет указатель ptr на функцию, принимающую параметр типа char и возвращающую целое значение типа int.
typel *(*имя) (type2);
указатель на функцию, принимающую параметр типа type2 и возвращающую указатель на объект типа typel.
type & имя = имя _объекта _типа _type;
инициализированная ссылка на объект типа type. Например, unsigned char &cc = simbol; определяет ссылку cc на объект типа unsigned char. Предполагается, что ранее в программе присутствует определение unsigned char simbol;
type1(&имя)(type2);
ссылка на функцию, возвращающую значение заданного типа type1 и принимающую параметр типа type2.
struct имя { type1 имя1; type2 имя2; };
тип структура в данном случае с двумя компонентами, которые имеют типы typel и type2. Например, struct ST{int x; char у; float z; ) ; -определяет структурный тип ST структуры с тремя компонентами разных типов: целая x, символьная у, вещественная z. (Количество компонентов в определении структуры может быть произвольным.)
union имя {type1 имя1; type2 имя2;};
тип объединение (в данном случае двух компонентов с типами typel, type2). Например, union UN{int m; char с [ 2 ];}; - объединяющий тип ОМ -объединение целой переменной m и двух элементов символьного массива с [0] и с[1] (Количество компонентов объединения может быть любым.)
class имя {type1 имя1; type2 имя2 (type3);};
класс, включающий в данном случае два компонента - объект типа type1 и функцию типа type2 с аргументом типа type3. Например: class А (int N; float F(char);}; -тип А - класс, компонентами которого служат целая переменная N и вещественная функция F с символьным аргументом. (Количество компонентов класса может быть произвольным.)
Еще один производный тип языка Си++ - это указатели на компоненты классов. Так как это понятие нужно вводить одновременно с определениями механизмов классов, то отложим рассмотрение этих указателей.
Все возможные производные типы принято разделять на скалярные (scalar), агрегатные (agregate) и функции (function). К скалярным типам относят арифметические типы, перечислимые типы, указатели и ссылки (ссылки введены только в Си++, но не в языке Си). Агрегатные типы, которые также называют структурированными, включают массивы, структуры, объединения и классы (последние только в Си++).
Одним из основных понятий языка Си++ является унаследованное из языка Си и предшествующих языков понятие объекта как некоторой области памяти. Переменная - это частный случай объекта как именованной области памяти. Отличительной чертой переменной является возможность связывать с ее именем различные значения, совокупность Которых определяется типом переменной. При определении значения переменной в соответствующую ей область памяти помещается некоторый код. Это может происходить либо во время компиляции, либо во время исполнения программы. В первом случае говорят об инициализации, во втором случае - о присваивании. Операция присваивания Е = B содержит имя переменной (E) и некоторое выражение (B). Имя переменной есть частный случай более общего понятия - "лево допустимое выражение" (left value expression), или l-значение. Название "лево допустимое выражение" произошло как раз от изображения операции присваивания, так как только l-значение может быть использовано в качестве ее левого операнда. Лево допустимое выражение определяет в общем случае ссылку на некоторый объект. Частным случаем такой ссылки является имя переменной.
Итак, объект определяется как некоторая область памяти. Это понятие вводится как понятие времени исполнения программы, а не понятие языка Си++. В языке Си++ термин объект зарезервирован как термин объектно-ориентированного подхода к программированию. При этом объект всегда принадлежит некоторому классу, и такие объекты мы будем рассматривать в главах, посвященных классам.
Вернемся к объектам как к участкам памяти.
Так как с объектом связано значение, то кроме l-выражения для объекта задается тип, который:
Имя объекта как частный случай лево допустимого выражения обеспечивает как получение значения объекта, так и изменение этого значения. Не любые выражения являются лево допустимыми. К лево допустимым относятся:
имя_структуры.имя_компонента;
указатвль_на_объект -> имя_компонвнта;
Кроме лево допустимых выражений, определены право допустимые выражения, которые невозможно использовать в левой части оператора присваивания. Например:
Кроме типов, для объектов явно либо по умолчанию определяются:
Все перечисленные атрибуты взаимосвязаны и должны быть явно указаны, в противном случае они выбираются по контексту неявно при определении и (или) описании конкретного объекта. Рассмотрим подробнее их возможности и особенности.
Тип определяет размер памяти, выделенной для значения объекта, правила интерпретации двоичных кодов значений объекта и набор допустимых операций. Типы рассмотрены в связи с переменными. Табл. 3.1 содержит основные типы и их свойства.
Класс памяти определяет размещение объекта в памяти и продолжительность его существования. Для явного задания класса памяти при определении (описании) объекта используются или подразумеваются по умолчанию следующие спецификаторы:
auto (автоматически выделяемая, локальная память)
Спецификатор auto может быть задан только при определении объектов блока, например, в теле функции. Этим объектам память выделяется при входе в блок и освобождается при выходе из него. Вне блока объекты класса auto не существуют.
register (автоматически выделяемая, по возможности регистровая память)
Спецификатор register аналогичен auto, но для размещения значений объектов используются регистры, а не участки основной памяти. Такая возможность имеется не всегда, и в случае отсутствия регистровой памяти (если регистры заняты другими данными) объекты класса register компилятор обрабатывает как объекты автоматической памяти.
static (внутренний тип компоновки и статическая продолжительность существования)
Объект, описанный со спецификатором static, будет существовать в пределах того файла с исходным текстом программы (модуля), где он определен. Класс памяти static может приписываться переменным и функциям.
extern (внешний тип компоновки и статическая продолжительность существования)
Объект класса extern глобален, т.е. доступен во всех модулях (файлах) программы. Класс extern может быть приписан переменной или функции.
Кроме явных спецификаторов, на выбор класса памяти существенное влияние оказывают размещение определения и описаний объекта в тексте программы. Такими определяющими частями программы являются блок, функция, файл с текстом кода программы (модуль) и т.д. Таким образом, класс памяти, т.е. размещение объекта (в регистре, стеке, в динамически распределяемой памяти, в сегменте) зависит как от синтаксиса определения, так и от размещения определения в программе.
Область (сфера) действия идентификатора (имени) - это часть программы, в которой идентификатор может быть использован для доступа к связанному с ним объекту [5,12], Область действия зависит от того, где и как определены объекты и описаны идентификаторы. Здесь имеются следующие возможности: блок, функция, прототип функции, файл (модуль) и класс.
Если идентификатор описан (определен) в блоке, то область его действия - от точки описания до конца блока. Когда блок является телом функции, то в нем определены не только описанные в нем объекты, но и указанные в заголовке функции формальные параметры. Таким образом, сфера действия формальных параметров в определении функции есть тело функции. Например, следующая функция вычисляет факториал значения своего аргумента. (Сфера действия для параметра z и переменных, описанных в теле функции, есть блок - тело функции):
long fact (int z) //1
{ long m = 1; //2
if (z < 0) return 0; //3
for (int i = 1; i < z; m = ++i * m); //4
return m; //5
} //6
Область действия: для переменной m - строки 2-6; для переменной i - строки 4-6; для параметра z - блок в целом.
Метки операторов в тексте определения функции имеют в качестве сферы действия функцию. В пределах тела функции они должны быть уникальны, а вне функции они недоступны. Никакие другие идентификаторы, кроме меток, не могут иметь в качестве сферы действия функции.
Прототип функции является сферой действия идентификаторов, указанных в списке формальных параметров. Конец этой сферы действия совпадает с концом прототипа функции. Например, в прототипе
float expon (float d, int m);
определены идентификаторы d, в, не существующие в других частях программы, где помещен данный прототип. Для примера рассмотрим программу, в которой используется только что определенная функция fact () для вычисления факториала:
//РЗ-02.СРР - сфера действия формальных параметров
// прототипа
#include < iostream.h >
long fact (int z)
{ long m = 1;
if (z < 0) return 0;
for (int i = 1; i < z; m = ++i * m);
return m;
}
main ()
{ int j = l, К = 3;
long fact(int К = 0);// Прототип функции
for ( ; j <= К; j++)
cout << "\n arg = " << j <<
" arg ! = " << fact(j);
}
Результат выполнения программы:
arg = 1 arg != 1
arg = 2 arg != 2
arg = 3 arg != 6
Программа иллюстрирует независимость идентификаторов списка параметров прототипа функции от других идентификаторов программы. Имя к в прототипе функции и имя переменной K, определенной в тексте основной программы (используется в цикле), имеют разные сферы действия и полностью независимы.
Файл с текстом программы (модуль) является сферой действия для всех глобальных имен, т.е. для имен объектов, описанных вне любых функций и классов. Каждое глобальное имя определено от точки описания до конца файла. С помощью глобальных имен удобно связывать функции по данным, т.е. создавать общее "поле данных" для всех функций файла с текстом программы. Простейший пример:
//РЗ-ОЗ.СРР - область действия глобальных имен
#include < iostream.h >
int LC;
char C[] = "Фраза";
void WW(void)
{ LC = sizeof(C);}
void Prin (void)
{ cout << "\n Длина строки С = " << LC; }
void main (void)
{ WW ();
Prin ();
}
Результат выполнения программы:
Длина строки С = 6
В программе три функции и два глобальных объекта - массив C и целая LC, через которые реализуется связь функций по данным.
Определение класса задает множество его компонентов, включающее данные и функции их обработки.
Имеются специальные правила доступа и определения сферы действия, когда речь идет о классах. Подробным рассмотрением этих вопросов мы займемся позже в связи с классами и принадлежащими им объектами.
С понятием области (сферы) действия связано пространство имен - область, в пределах которой идентификатор должен быть "уникальным" [5,12]. С учетом пространства имен используемые в программе идентификаторы делятся на четыре группы [5,9]:
Понятие видимость объекта понадобилось в связи с возможностью повторных определений идентификатора внутри вложенных блоков (или функций). В этом случае разрывается исходная связь имени с объектом, который становится "невидимым" из блока, хотя сфера действия имени сохраняется. Следующая программа иллюстрирует эту ситуацию:
//РЗ-04.СРР - переопределение внешнего имени внутри блока
#include < iostream.h >
void main ()
{ char cc[] = "Число"; // Массив автоматической памяти
float pi = 3.1415926; // Переменная типа float
cout << "\n Обращение к внешнему имени: pi = " << pi;
{ // Переменная типа double переопределяет pi:
double pi = 3.1415926535897932385;
// Видимы double pi и массив сс[]:
cout << '\n' << сс " double pi = " << pi;
}
// Видимы float pi и массив cc[]:
cout << '\n' << cc " float pi = " << pi;
}
Результат выполнения программы:
Обращение к внешнему имени: pi = 3.1415926
Число double pi = 3.1415926535897932385
Число float pi = 3.1415926
Достаточно часто сфера (область) действия идентификатора и видимость связанного с ним объекта совпадают. Область действия может превышать видимость, но обратное невозможно, что иллюстрирует данный пример. За описанием переменной double pi внутри блока внешнее имя переменной float pi становится невидимым. Массив char сс [] определен и видим во всей программе, а переменная float pi видима только вне вложенного блока, внутри которого действует описание double pi. Таким образом, float pi невидима во внутреннем блоке, хотя сферой действия для имени pi является вся программа. Для переменной double pi и сферой действия, и сферой видимости служит внутренний блок, вне которого она недоступна и не существует.
Второй пример изменения видимости объектов при входе в блок:
//РЗ-05.СРР - переопределение внешнего имени внутри блока
#include < iostream.h >
void main ()
{int k = О, j = 15;
{ cout << "\nВнешняя для блока переменная k = " << k;
char k = 'А'; / / Определена внутренняя переменная
cout << "\nВнутренняя переменная k = " << k;
cout << "\nВидимая в блоке переменная j = " << j;
j = 30; // Изменили значение внешней переменной
} // Конец блока
cout << "\nВне блока: k = " << k << ", j = " << j;
}
Результат выполнения программы:
Внешняя для блока переменная k = 0
Внутренняя переменная k = А
Видимая в блоке переменная j = 15
Вне блока: k = 0, j = 30
Как видно из примера, внутри блока сохраняется сфера действия внешних для блока имен до их повторного описания (переменная 1с). Определение объекта внутри блока действует от точки размещения определения до конца блока (до выхода из блока). Внутреннее определение изменяет видимость внешнего объекта с тем же именем (объект невидим). Внутри блока видимы и доступны определенные во внешних блоках объекты (переменная j). После выхода из блока восстанавливается видимость внешних объектов, переопределенных внутри блока. (Вывод значения переменной k после выхода из блока.)
Язык Си++ позволяет изменить видимость объектов с помощью операции '::'. Программа Р2-13.срр, иллюстрирующая возможность такого изменения видимости, приведена при описании операции указания области действия '::'. Рассмотрим еще один пример обращения к "невидимой" внутри функции внешней строке с помощью операции указания области действия:
//РЗ-06.СРР - доступ из функции к внешнему объекту,
// имя которого переопределено в теле функции
#include < iostream.h >
char cc[] - "Внешний массив";
void func(void) // Определение функции
{ char cc[] = "Внутренний массив";
// Обращение к локальному объекту сс:
cout << '\n' << сс;
// Обращение к глобальному объекту cc:
cout << '\n' <<::сс;
}
void main (void)
{ void func(void); // Прототип функции
func (); // Вызов функции
}
Результат выполнения программы:
Внутренний массив
Внешний массив
Следующая программа и соответствующая ей схема (рис. 3.1) обобщают соглашения о сферах действия идентификаторов и о видимости объектов. Программа готовится в виде одного текстового файла. В программе три функции, из которых одна главная (main):
// РЗ-07.СРР - файл с текстом программы (модуль)
#include < iostream.h >
char dc [] = "Объект 1"; // Глобальный для модуля объект 1
void func1(void)
{ cout << "\nf1.dc =" << dc; // Виден глобальный объект 1
char dc[] = "Объект 2"; // Локальный для func1() объект 2
cout << "\nf1.dc =" << dc; // Виден локальный объект 2
{ // Внутренний блок для func1()
// Виден локальный для func1() объект 2:
cout << "\nf1.block.dc =" << dc;
// Локализованный в блоке объект 3:
char dc [] = "Объект З";
// Виден локальный объект 3:
cout << "\nf1.block.dc =" << dc;
// Виден глобальный объект 1:
cout << "\nf1.block . ::dc = " << ::dc;
}
// Конец блока
// Виден локальный для func1() объект 2:
cout << "\nf1.dc = " << dc;
// Виден глобальный объект 1:
cout << "\nf1. : :dc = " << ::dc;
} // Конец функции func1()
void func2(char *dc) // dc - параметр функции
{ cout << "\nf 2. параметр. dc =" << dc; // Виден параметр
// Виден глобальный объект 1:
cout << "\nf2. : :dc = " << ::dc;
{ // Внутренний блок для func2()
// Локализованный в блоке объект 4:
char dc[] = "Объект 4";
// Виден локальный для func2 () объект 4:
cout << "\nf2.dc = " << dc;
} // Конец блока
} // Конец функции func2 ()
void main(void)
{ // Виден глобальный объект 1:
cout << "\nfmain.dc = " << dc;
char dc[] = "Объект 5"; // Локальный для main ()объект 5
func1();
func2(dc);
Рис. 3.1. Видимость объектов, связанных с одним идентификатором (именем), в однофайловой программе
// Виден локальный для main () объект 5:
cout << "\nfmain.dc = " << dc;
}
Результат выполнения программы:
fmain.dc = Объект 1
f1.dc = Объект 1
f1.dc = Объект 2
f1.block.dc = Объект 2
f1.block.dc = Объект З
f1.block. : :dc = Объект 1
fl.dc = Объект 2
fl.::dc= Объект 1
f2.параметр.dc = Объект 5
f2.::dc = Объект 1
f2.dc = Объект 4
fmain.dc = Объект 5
На рис.3.1 для имени объекта с типом type1 областью действия служит файл в целом; однако видимость этого объекта различна внутри блоков и функций. Если определение typel имя; (char dc[] = "объект1";) поместить в конце файла, то ничего хорошего не получится - действие определения не распространяется вверх по тексту программы. Все попытки обратиться к глобальному "Объекту1" в этом случае приведут к синтаксическим ошибкам, выявленным компилятором.
Объект, определенный в тексте программы ниже (позже) своего первого использования, должен быть описан в той функции, где он используется, с атрибутом extern:
void fl(void)
{ extern int ex; // Описание внешней переменной
cout << "\nfl : ex = " << ex;
}
void main(void)
{ f1();}
int ex = 33; // Определение с инициализацией
Результат выполнения программы:
fl:ex = 33
Продолжительность существования объектов определяет период, в течение которого идентификаторам в программе соответствуют конкретные объекты в памяти. Определены три вида продолжительности: статическая, локальная и динамическая.
Объектам со статической продолжительностью существования память выделяется в начале выполнения программы и сохраняется до окончания ее обработки. Статическую продолжительность имеют все функции и все файлы. Остальным объектам статическая продолжительность существования может быть задана с помощью явных спецификаторов класса памяти static и extern. При статической продолжительности существования объект не обязан быть глобальным.
При отсутствии явной инициализации объекты со статической продолжительностью существования по умолчанию инициализируются нулевыми, или пустыми значениями. Следующий пример содержит переменную к со статической продолжительностью существования, сферой действия для которой является только тело функции (блок), т.е. переменная к локализована в теле функции counter:
//РЗ-08.СРР - инициализация и существование локальных
// статических объектов
#include < iostream.h >
int counter (void) // Определение функции
{static int К; // Статическая переменная,
return ++K; // локализованная в теле функции
}
void main (void)
{int counter(void); // Прототип функции
int К = 3; // Локальная переменная функции main
for( ; К != 0; К- -)
{ cout << "\nАвтоматическая К = " << К;
cout << "\tСчетчик=" << counter();
}
}
Результат выполнения программы:
Автоматическая К = 3 Счетчик = 1
Автоматическая К = 2 Счетчик = 2
Автоматическая К = 1 Счетчик = 3
В данном примере статическая переменная K, локализованная в теле функции counter, по умолчанию инициализируется нулевым значением, а затем сохраняет значение после каждого выхода из функции, т.к. продолжительность существования переменной к статическая, и выделенная ей память будет уничтожена только при выходе из программы.
В основной функции примера определена целая переменная к с локальной продолжительностью существования. Такие переменные называются автоматическими. Они создаются при каждом входе в блок или функцию, где они описаны, и уничтожаются при выходе. Как образно выразились авторы руководств [5, 12], объекты с локальной продолжительностью жизни "ведут более случайное существование", чем объекты со статической продолжительностью. Объекты с локальной продолжительностью должны быть инициализированы только явным образом, иначе их начальное значение непредсказуемо. Память для них при входе в блок или функцию выделяется в регистрах или в стеке. Область действия для объекта с локальной продолжительностью существования всегда локальна - это блок или функция. Для задания локальной продолжительности при описании объекта можно использовать спецификатор класса памяти auto, однако он всегда избыточен, т.к. этот класс памяти по умолчанию приписывается всем объектам, определенным в блоке или функции.
Следует обратить внимание, что не для всех объектов с локальной областью действия определена локальная продолжительность существования. Например, в функции counter ( ) переменная к имеет локальную область действия (тело функции), однако для нее спецификатором static определена статическая продолжительность существования.
Объекты с динамической продолжительностью существования создаются (получают память) и уничтожаются с помощью явных операторов в процессе выполнения программы. Для создания используется операция new или функция malloc (), а для уничтожения - операция delete или функция free (). Пример программы с динамическим распределением памяти для объектов приведен при описании возможностей операций new и delete. Приведем программу, в которой для тех же целей используются библиотечные функции malloc() и free (). Указанные функции находятся в стандартной библиотеке языка Си и его наследника - языка Си++, а их прототипы включены в заголовочный файл alloc .h. Прототип функции для выделения памяти:
void *malloc(int size);
Исходной информацией для функции malloc () служит целое значение size, определяющее в байтах требуемый размер выделяемой памяти. Функция возвращает адрес выделенного участка памяти. Возвращаемый адрес равен 0, если выделить память не удается. В следующей программе при выделении памяти проверка для упрощения не выполняется:
//РЗ-09.СРР - динамическое выделение памяти для объектов
#include < alloc.h > // Прототипы malloc() и free ()
#include < iostream.h > // Для cout
void main (void)
{int *t; // Память выделена только для t, но не для *t
// Память выделена для m и *m;
int *m = (int *)
malloc(sizeof(int));
*m = 10; t = m; // Запомнили значение указателя
// Память выделена для *m:
m = (int *)
malloc(sizeof(int));
*m = 20;
cout << "\n Второе значение *m = " << *m;
free(m); // Освободить память, выделенную для *m
cout << "\n Первое значение *m = " << *t;
free(t); // Освободить память, выделенную для *m
}
При определении указатель t получил память, но память для целого *t не выделена. При определении указателя в функцией malloc() выделена память для целого значения *m. В этот участок памяти заносится значение 10.
При втором использовании malloc() указатель m устанавливается на новый участок памяти. В *m заносится значение 20 и следует печать. Остальное поясняют комментарии.
Результат выполнения программы:
Второе значение *m = 20
Первое значение *m = 10
Тип компоновки, или тип связывания, определяет соответствие идентификатора конкретному объекту или функции в программе, исходный текст которой размещен в нескольких файлах (модулях). В этом случае каждому имени, используемому в нескольких файлах, может соответствовать либо один объект (функция), общий для всех файлов, либо по одному и более объектов в каждом файле. Файлы программы (модули) могут транслироваться отдельно, и в этом случае возникает проблема установления связи между одним и тем же идентификатором и единственным объектом, которому он соответствует. Такие объекты (функции) и их имена нуждаются во внешнем связывании, которое выполняет компоновщик (редактор связей) при объединении отдельных объектных модулей программы. Для остальных объектов (функций), которые локализованы в файлах, используется внутреннее связывание.
Тип компоновки (связывания) никак специальным образом не обозначается при определении объектов и описании имен, а устанавливается компилятором по контексту, местоположению этих объявлений и использованию спецификаторов класса памяти static и extern.
Например, имена статических объектов (static) локализованы в своем файле и могут быть использованы как имена других объектов и функций в других независимо транслируемых файлах. Такие имена имеют внутренний тип компоновки (связывания). Если имена объектов или функций определены со спецификатором extern, то имя будет иметь внешний тип компоновки (связывания). Рассмотрим следующую программу, функции которой и определения переменных рассредоточены по трем текстовым файлам:
// ---------------------------------------- модуль 1 --------------------------------------
//РЗ-10-1.СРР - первый файл многомодульной программы
int К = 0; // Для К - внешнее связывание
void counter(void) // Для counter - внешнее связывание
{static int K_IN =0; // Для К_IN - внутреннее связывание К += ++K_IN;
}
// ----------------------------------------- Модуль 2 -------------------------------------
//РЗ-10-2.СРР - второй (основной) файл программы
#include < iostream.h >
void main(void)
{ void counter(void); // Прототип - внешнее связывание
void display(void); // Прототип - внешнее связывание
// К - локальный объект - внутреннее связывание:
for (int К = О; К < 3; К++)
{ cout << "\nПараметр цикла К =" <<К;
counter (); // Изменяет свою К_ IN и внешнюю К
display ();
}
}
//----------------------------------------- Модуль 3 ---------------------------------------
//РЗ-10-З.СРР - третий файл программы
#include < iostream.h >
void display(void) // Для display - внешнее связывание
{ extern int К; // Для К - внешнее связывание static
int К _IN = 0; // Для К_IN - внутреннее связывание
cout << "\nВнешнее К = " << К++ <<
" Внутреннее К_IN из функции display = " <<
К_IN++;
}
Результат выполнения:
Параметр цикла К = О
Внешнее К = 1
Внутреннее K_IN из функции display = 0
Параметр цикла К = 1
Внешнее К = 4
Внутреннее K_IN из функции display = 1
Параметр цикла К = 2
Внешнее К = 8
Внутреннее K_IN из функции display = 2
Анализируя текст и результаты программы, обратите внимание на следующее:
Для некоторых имен тип компоновки не существует. К ним относятся параметры функций, имена объектов, локализованных в блоке (без спецификаторов extern), идентификаторы, не являющиеся именами объектов и функций (например, имя введенного пользователем типа).
Различия между определениями и описаниями. Все взаимосвязанные атрибуты объектов (тип, класс памяти, область (сфера) действия имени, видимость, продолжительность существования, тип компоновки) приписываются объекту с помощью определений и описаний, а также контекстом, в котором эти определения и описания появляются. Определения устанавливают атрибуты объектов, резервируют для них память и связывают эти объекты с именами (идентификаторами). Кроме определений, в тексте программы могут присутствовать описания, каждое из которых делает указанные в них идентификаторы известными компилятору. В переводной литературе и особенно в документации наряду с терминами "описание" и "определение" используют "объявление" и "декларация". Не втягиваясь в терминологическую дискуссию, остановимся на варианте, примененном в переводах работ [1] и [25].
Итак, появление двух терминов "определение" и "описание" объясняется тем фактом, что каждый конкретный объект может быть многократно описан, однако в программе должно быть только единственное определение этого объекта. Определение обычно выделяет объекту память и связывает с этим объектом идентификатор - имя объекта, а описания только сообщают свойства того объекта, к которому относится имя. Говорят, что описание ассоциирует тип с именем, а определение, кроме того, задает все другие свойства (объем памяти, внутреннее представление и т.д.) объекта и выполняет его инициализацию. В большинстве случаев описание одновременно является и определением.
Однако это не всегда возможно и не всегда требуется.
Определять и описывать можно следующие объекты:
Определения и описания переменных. Из всего перечисленного разнообразия объектов только переменным соответствуют основные типы. Определение переменных заданного типа имеет следующий формат:
s m тип имя1 иниц. 1, имя2 иниц. 2, . . .;
где
s - спецификатор класса памяти (auto, static, extern, register, typedef) - подробно описан в п. 3.2;
m - модификатор const или volatile;
тип- один из основных типов (табл. 3.1);
имя - идентификатор;
иниц. - необязательный инициализатор, определяющий начальное значение соответствующего объекта.
Синтаксис инициализатора (иниц.) переменной:
= инициализирующее _выражение
либо
(инициализирующее_выражение)
Наиболее часто в качестве инициализирующего выражения используется константа. Вторая "скобочная" форма инициализации разрешена только внутри функций, т.е. для локальных объектов. Напомним, что описание является определением, если:
Описание не может быть определением, если:
Приведем примеры описаний:
extern int g; // Внешняя переменная
float fn(int, double); // Прототип функции
extern const float pi; // Внешняя константа
struct st; // Имя структуры (класса)
typedef unsigned char simbol; // Новый тип simbol.
Примеры определений:
char sm; // Переменная автоматической или внешней памяти
float dim = 10.0; // Инициализированная переменная
double Euler(2.718282); // Инициализированная переменная
// автоматической памяти
const float pi = 3.14159; // Константа
float x2(float x) (return x*x;);// Функция
struct (char a; int b;) st; // Структура
enum (zero, one, two); // Перечисление
Некоторая неоднозначность есть в определении переменной char. Если такое описание находится в блоке, то это определение неинициализированной переменной автоматической памяти. Если такое описание появляется вне блоков и классов, то это определение статической внешней переменной, которой по умолчанию присваивается нулевое значение, а для символьной переменной char - пробел. В тех файлах, где нужен доступ к этой глобальной переменной, должно быть помещено ее описание вида extern char sm;. Подобное описание необходимо для доступа из функций к внешним объектам, определения которых размещены в том же файле, но ниже текста определения функции. Пример:
// РЗ-11.СРР - определения и описания переменных
#include < iostream.h >
float pi = 3.141593; // Определение с явной инициализацией
int s0; // Определение s0 (инициализация по умолчанию)
int s2 = 5; // Определение s2 с явной инициализацией
void main()
{ extern int s0; // Описание s0
extern char s1; // Описание s1
int s2(4); // Описание s2 с явной инициализацией
cout << "\n Инициализация по умолчанию: s0 = " << s0;
cout << "\n Явная инициализация: s1 = " << s1;
cout << "\n Внутренняя переменная: s2 = " << s2;
cout << "\n Внешняя переменная: pi = " << pi;
}
char s1='S'; // Определение s1 с явной инициализацией
//Конец файла с текстом программы
Результат выполнения программы:
Инициализация по умолчанию: s0 = 0
Явная инициализация: s1 = S
Внутренняя переменная: s2 = 4
Внешняя переменная: pi = 3.141593
Для инициализации переменной s2, относящейся к автоматической памяти, использована "скобочная" форма задания начального значения. Внешние переменные таким образом инициализировать нельзя - компилятор воспринимает их как неверные определения функций. В программе обратите внимание на переменную pi, которая определена (и инициализирована) вне функции main(), а внутри нее не описана. Так как программа оформлена в виде одного файла, то все внешние переменные, определенные до текста функции, доступны в ней без дополнительных описаний. Таким образом, описание extern int s0; в данной однофайловой программе излишнее, а описание extern char s1; необходимо.
Следующая программа еще раз иллюстрирует доступ к внешним переменным из разных функций однофайловой (одномодульной) программы:
//РЗ-12.СРР - обмен между функциями через внешние
// переменные
#include < iostream.h >
int x; // Определение глобальной переменной
void main ()
{ void func(void); // Необходимый прототип функции
extern int x; // Излишнее описание
х = 4;
func ();
cout << "\n х = " << х;
}
void func (void)
{ extern int x; // Излишее описание х = 2;
}
Результат выполнения:
х = 2
Отметим некоторые особенности спецификаторов класса памяти. Спецификатор auto редко появляется в программах. Действительно, его запрещено использовать во внешних описаниях, а применение внутри блока или функции излишне - локальные объекты блока по умолчанию (без спецификаторов static или extern) являются объектами автоматической памяти. Спецификатор register также запрещен во внешних описаниях, однако его применение внутри блоков или функций может быть вполне обосновано.
Появление typedef среди спецификаторов класса памяти (auto, static,...) объясняется не семантикой, а синтаксическими аналогиями. Служебное слово typedef специфицирует новый тип данных, который в дальнейшем можно использовать в описаниях и определениях. Однако не следует считать, что typedef вводит новый тип. Вводится только новое название (имя, синоним) типа, который программист хочет иметь возможность называть по-другому. Сравним два описания:
static unsigned int ui;
typedef unsigned int NAME;
В первом определена статическая целая беззнаковая переменная ui, a во втором никакой объект не определен, а описано новое имя типа NАМЕ для еще не существующих без знаковых целых объектов. В дальнейшем NAME можно использовать в описаниях и определениях. Например, запись
register NAME rn = 44; // Допустим спецификатор
// класса памяти
эквивалентна определению
register unsigned int rn = 44;
Заметим, что register не имя типа, а спецификатор класса памяти. Имена типов, введенные с помощью typedef, нельзя употреблять в одном описании (определении) с другими спецификаторами типов. Например, будет ошибочной запись
long NAME start; // Ошибочное сочетание
// спецификаторов типов
Однако определение const NАМЕ cn = о; вполне допустимо. const - не имя типа, а модификатор.
Спецификатор typedef нельзя употреблять в определениях функций, однако, можно в их описаниях (прототипах).
Имя типа, введенное с помощью typedef, входит в то же пространство имен, что и прочие идентификаторы программ (за исключением меток), и подчиняется правилам области (сферы) действия имен.
Например:
typedef long NL;
unsigned int NL = 0; // Ошибка - повторное определение
ML void func()
{ int NL = 1; // Верно - новый объект определен
}
Модификаторы const и volatile. Эти модификаторы позволяют сообщить компилятору об изменчивости или постоянстве определяемого объекта. Если переменная описана как const, то она недоступна в других модулях программы, ее нельзя изменять во время выполнения программы. Единственная возможность присвоить ей значение - это инициализация при определении. Объекту с модификатором const не только нельзя присваивать значения, но для него запрещены операции инкремента (++) и декремента (- -). Указатель, определенный с модификатором const, нельзя изменять, однако может быть изменен объект, который им адресуется. Примеры с константами:
const zero = 0; //По умолчанию добавляется тип
int const char *ptrconst = "variable"; // Указатель
const // на строку
char *point = "строка"; // Обычный указатель на строку
char const *ptr = "константа"; // Указатель на
// строку-константу
char *varptr = ptr; // Запрещено
zero += 4; // Ошибка - нельзя изменить константу
ptrconst = point; // Ошибка - указатель должен быть
// постоянным
strcpy(ptrconst,point); // Допустимо - меняется адресуемая
// строка
strcpy(ptrconst,ptr); // Допустимо, значения ptrconst
// и ptr не изменяются
Отметим ошибочную попытку присвоить указателю (не константному) varptr значение указателя на константу. Это запрещено, так как в противном случае можно было бы через указатель varptr изменить константу.
Модификатор volatile отмечает, что в процессе выполнения программы значение объекта может изменяться в промежутке между явными обращениями к нему. Например, на объект может повлиять внешнее событие. Поэтому компилятор не должен помещать его в регистровую память и не должен делать никаких предположений о постоянстве объекта в те моменты, когда в программе нет явных операции, изменяющих значение объекта.
Модификаторы const и volatile имеют особое значение при работе с классами, и мы к ним еще обратимся.
Кроме спецификаторов класса памяти и модификаторов const, volatile, диалекты языка Си++, реализованные в компиляторах для ПЭВМ типа IBM PC, включают модификаторы [9]:
Эти модификаторы предназначены для влияния на распределение памяти при размещении объектов и учета особенностей сегментной организации и адресации памяти в процессорах семейства 80х86.
Выражение - это последовательность операндов, разделителей и знаков операций, задающая вычисление.
Порядок применения операций к операндам определяется рангами (приоритетами) операций (см. табл. 2.4) и правилами группирования операций (их ассоциативностью). Для изменения порядка выполнения операций и их группирования используют разделители (круглые скобки). В общем случае унарные операции (ранг 2), условная операция (ранг 14) и операции присваивания (ранг 15) правоассоциативны, а остальные операции левоассоциативны (см. табл. 2.4). Таким образом, х = у = z означает х= (у = z),ax + у - z означает (х + у) - z.
Кроме формирования результирующего значения, вычисление выражения может вызвать побочные эффекты. Например, значением выражения z = 3, z + 2 будет 5, а в качестве побочного эффекта z примет значение 3. В результате вычисления х > 0? x - -: х будет получено значение х, а в качестве побочного эффекта положительное значение х будет уменьшено на 1.
В языке Си++ программист может расширить действие стандартных операций (overload - перегрузка), т.е. придавать им новый смысл при работе с нестандартными для них операндами. Отметим, что операции могут быть распространены на вводимые пользователем типы, однако у программиста нет возможности изменить действие операций на операнды стандартных типов. Эту связанную с классами возможность языка Си++ рассмотрим позже, а сейчас остановимся на некоторых свойствах операций, стандартно определенных для тех типов, для которых эти операции введены.
Формальный синтаксис языка Си++ [2,5] предусматривает рекурсивное определение выражений. Рекурсивность синтаксических определений (не только для выражений) и широкие возможности конструирования новых типов делают попытки "однопроходного" изложения семантики сразу всего языка Си++ практически безнадежными. Поясним это, рассмотрев выражения.
Основным исходным элементом любого выражения является первичное выражение. К ним относятся:
К константам относятся:
Все типы констант - это лексемы, и они уже в предыдущих главах рассмотрены. Приведены форматы констант, предельные значения для реализации Си++ и даны примеры.
имя
К именам относятся:
идентификатор
может использоваться в качестве имени только в том случае, если он введен с помощью подходящего определения. Самый распространенный представитель - идентификатор как имя переменной.
имя_функции-операции
вводится только в связи с расширением действия (с перегрузкой) операций. Механизм перегрузки, возможно, объяснить только после определения понятия класс.
имя_функции_приведения
функции приведения, или преобразующие функции являются компонентами классов, и для объяснения их семантики требуется ввести соответствующие понятия, относящиеся к классам.
имя_ класса
обозначает обращение к специальному компоненту класса - к деструктору.
квалифицированное_имя (уточненное_имя)
имеет рекурсивное определение следующего формата:
квалифицированное_имя_класса:: имя
Таким образом, чтобы определить понятие квалифицированиое_имя. Требуется ввести понятие квалифицированное_имя_класса и уже иметь определение имени. Следовательно, не вводя понятие "класс", можно в качестве имен из всего перечисленного использовать только идентификаторы.
(выражение)
Третий вариант первичного выражения содержит рекурсию, так как это произвольное выражение, заключенное в круглые скобки.
::идентификатор
Четвертый вариант первичного выражения:: идентификатор включает операцию изменения области действия, смысл которой объяснялся.
Все остальные представители первичных выражений (за исключением псевдопеременных) невозможно объяснять и иллюстрировать примерами, не вводя понятие класса. Таким образом, следуя логике нашего изложения (алфавит - лексемы - базовые типы - скалярные типы - выражения) и не вводя структурированных типов, к которым относятся классы, придется рассматривать не все варианты первичных выражений и тем более не все варианты выражений. Учитывая это ограничение, продолжим "конструирование" выражений. На основе первичных выражений вводится постфиксное выражение, которым может быть:
Индексация. Интуитивный смысл постфиксного выражения (обозначим его РЕ), за которым в квадратных скобках следует выражение (IE), есть индексация:
int j = 5, d[100]; // Определение целой переменной j
// и массива d
...
...d[j]... // Постфиксное выражение
...
В записи PE[IE] постфиксное выражение (РЕ) должно быть, например, именем массива нужного типа, выражение IE в квадратных скобках должно быть целочисленного типа. Таким образом, если РЕ -указатель на массив, то РЕ [IE] - индексированный элемент этого массива. Примеры работы с индексированными переменными (с элементами массивов) уже несколько раз приводились. Например, индексирование анализировалось в связи с рассмотрением квадратных скобок в качестве бинарной операции в п.2.4.
Обращение к функции. Постфиксное выражение РЕ(список_выражений)
представляет обращение к функции. В этом случае РЕ - имя функции, или указатель на функцию, или ссылка на функцию. Список выражений - это список фактических параметров. Значения фактических параметров вычисляются до выполнения функции, поэтому и побочные эффекты проявляются до входа в нее и могут сказываться во время выполнения операторов тела функции. Порядок вычисления значений фактических параметров синтаксисом языка Си++ не определен. Например [2], функция f (int, int) при таком обращении
int m = 2;
f(m - -, m - -);
может быть вызвана в зависимости от реализации со следующими значениями параметров: f(2,2), или f(l,2), или f(2,l). (Компилятор ВС++ вызывает эту функцию в виде f(l,2).) В то же время за счет побочного эффекта значением переменной, а станет 0. Для проверки конкретных правил вычисления значений фактических параметров можете выполнить с помощью доступного компилятора такую программу:
//РЗ-14.СРР - порядок вычисления фактических параметров
#include < iostream.h >
int m = 5; // Глобальная переменная
void p(int i,int j,int k) // Определение функции
{ cout << "\ni = "<< i << " j = " << j << " k = " << k;
cout << "\nВнутри функции p(...) m=" << m;
}
void main ()
{ void p(int, int, int); // Прототип функции
р(m++, (m = m * 5, m * 3), m - -);
cout << "\nВ главной программе после вызова р(...)";
cout << " m = " << m;
}
Результат выполнения программы для компилятора в С++:
i = 20 j = 60 k=5
Внутри функции р (. . .)
m = 21
В главной программе после вызова p(...)
m = 21
Как видно из результатов, параметры вычисляются справа налево. После вычисления очередного параметра выполняются постфиксные операции для операндов, входящих в выражение фактического параметра.
Еще раз отметим, что при обращении к функции перед скобками в качестве постфиксного выражения может использоваться не только имя функции, но и указатель на функцию и ссылка на функцию. Подробнее возможности этих обращении будут рассмотрены позже.
Явное преобразование типа. Постфиксное выражение type(список_выражений) служит для формирования значений типа type на основе списка_выражений, помещенного в круглых скобках. Если выражений больше одного, то тип должен быть классом, и данное постфиксное выражение вызывает конструктор класса. Если в списке выражений всего одно выражение, a type - имя простого типа, то имеет место уже рассмотренное в разделе 2.4 непосредственное функциональное приведение типа. Общая форма такой функциональной записи явного Преобразования типа имеет вид: имя простого типа (выражение). Примеры: int (3.14), float (2/5), int (' А ').
Функциональная запись не может применяться для типов, не имеющих простого имени. Например, попытка трактовать конструкции
unsigned long (x/3+2)
или
char *(0777)
как функциональные преобразования вызовет ошибку компиляции. Напомним (см. п. 2.4), что кроме функциональной записи для явного преобразования типа можно использовать каноническую операцию приведения (cast) к требуемому типу. Для ее изображения используется обозначение типа в круглых скобках. Те же примеры можно записать с помощью операции приведения типа так: (int) 3.14, (float) 2/5, (int) 'А'. Каноническая операция приведения к типу может применяться и для типов, имеющих сложные обозначения. Например, можно записать
(unsigned long)(x/3+2)
или
(char *)0777
и тем самым выполнить нужные преобразования.
Другую возможность явного преобразования для типов со сложным наименованием обеспечивает введение собственных обозначении типов с помощью typedef.
Например:
typedef unsigned long int ULI;
typedef char *PCH;
После введения пользователем таких простых имен типов можно применять функциональную запись преобразования типа: ULI (x/3+2) или PSH(0777). При преобразовании типов существуют некоторые ограничения. Но прежде чем остановиться на них, рассмотрим стандартные преобразования типов, выполняемые при необходимости по умолчанию.
Стандартные преобразования типов. При вычислении выражений некоторые операции требуют, чтобы операнды имели соответствующий тип, а если требования к типу не выполнены, принудительно вызывают выполнение нужных преобразований. Та же ситуация возникает при инициализации, когда тип инициализирующего выражения приводится к типу определяемого объекта. Напомним, что в языках Си и Си++ присваивание является бинарной операцией, поэтому сказанное относительно преобразования типов относится и ко всем формам присваивания. Правила преобразования в языке Си++ для основных типов полностью совпадают с правилами преобразований, стандартизованными в языке Си. Эти стандартные преобразования включают перевод "низших" типов в "высшие" в интересах точности представления и непротиворечивости данных [5, 12]. Среди преобразования типов в языке Си++ выделяют:
При преобразовании типов нужно различать преобразования, изменяющие внутреннее представление данных, и преобразования, изменяющие только интерпретацию внутреннего представления. Например, когда данные типа unsigned int переводятся в тип int, менять их внутреннее представление не требуется - изменяется только интерпретация. При преобразовании типа float в тип int недостаточно изменить только интерпретацию, необходимо изменить длину участка памяти для внутреннего представления и кодировку. При таком преобразовании из float в int возможен выход за диапазон допустимых значений типа int, и реакция на эту ситуацию существенно зависит от конкретной реализации. Именно поэтому для сохранения мобильности программ в них рекомендуется с осторожностью применять преобразование типов.
Рассмотрим этапы (последовательность выполнения) преобразования операндов в арифметических выражениях.
Таблица 3.2
Правила стандартных арифметических преобразований
Исходный тип | Преобразуется в | Правила преобразований |
Char | Int | Расширение нулем или знаком в зависимости от умолчания для char |
unsigned char | Int | Старший байт заполняется нулем |
Signed char | Int | Расширение знаком |
Short | Int | Сохраняется то же значение |
unsigned short | unsigned int | Сохраняется то же значение |
Enum | Int | Сохраняется то же значение |
битовое поле | Int | Сохраняется то же значение |
Используя арифметические выражения, следует учитывать приведенные правила и не попадать в "ловушки" преобразования типов, так как некоторые из них приводят к потерям информации, а другие изменяют интерпретацию битового (внутреннего) представления данных.
На рис. 3.2, взятом с некоторыми сокращениями из проекта стандарта языка Си++ [2], стрелками отмечены арифметические преобразования, гарантирующие сохранение точности и неизменность численного значения.
Рис.3.2. Последовательности арифметических преобразований типов, гарантирующие сохранение значимости
При преобразованиях, которые не отнесены схемой (рис.3.2) к безопасным, возможны существенные информационные потери. Для оценки значимости таких потерь рекомендуется проверить обратимость преобразования типов. При арифметических преобразованиях необратимость вполне объяснима и естественна. Преобразование целочисленных значений к вещественному типу осуществляется настолько точно, насколько это предусмотрено аппаратурой. Если целочисленное значение не может быть точно представлено как вещественное, то младшие значащие цифры теряются.
Преобразование вещественного значения к целому типу выполняется за счет отбрасывания дробной части. Обратное преобразование целой величины к вещественному значению может привести к потере точности.
Следующая программа иллюстрирует сказанное:
//РЗ-16.СРР - потери информации при преобразованиях типов
#include < iostream.h >
void main ()
{ long k = 123456789;
float g = (float) k;
cout << "\n k = " << k // Печатает: k = 123456789
cout << "\n g = " << g // Печатает: g = 1.234567e+08
k = (long)g;
cout << "\n k = " << k // Печатает: k = 123456792
g = (float)2.222222e+2
int m = (int)g;
cout << "\n g = " << g // Печатает: g = 222.222198
cout << "\n m = " << m // Печатает: m = 222
g = (float)m;
cout << "\n g = " << g // Печатает: g = 222
}
К менее предсказуемым результатам может привести необратимость преобразования типов для указателей, ссылок и указателей на компоненты классов.
Материал, относящийся к операторам, по-видимому, наиболее традиционный. Здесь язык Си++ почти полностью соответствует языку Си, который, в свою очередь, наследует конструкции классических алгоритмических языков лишь с небольшими усовершенствованиями. Операторы, как обычно, определяют действия и логику (порядок) выполнения этих действий в программе. Среди операторов выделяют операторы, выполняемые последовательно, и управляющие операторы.
Каждый оператор языка Си++ заканчивается и идентифицируется разделителем "точка с запятой". Любое выражение, после которого поставлен символ "точка с запятой", воспринимается компилятором как отдельный оператор. (Исключения составляют выражения, входящие в заголовок цикла for.)
Часто оператор-выражение служит для вызова функции, не возвращающей никакого значения. Например:
//Р4-01.СРР - обращение к функции как оператор-выражение
#include < iostream.h >
void cod_char(char с)
{cout << "\n " << с << " = " << (unsigned int)c;
}
void main ()
{ void cod_char(char); // Прототип функции
cod char( 'A' ); // Оператор-выражение
cod__char('x'); // Оператор-выражение
}
Результат выполнения программы:
А = 65
х = 120
Еще чаще оператор-выражение - это не вызов функции, а выражение присваивания. Именно в связи с тем, что присваивание относится к операциям и используется для формирования бинарных выражении, в языке Си++ (и в Си) отсутствует отдельный оператор присваивания. Оператор присваивания всего-навсего является частным случаем оператора-выражения.
Специальным случаем оператора служит пустой оператор. Он представляется символом "точка с запятой", перед которым нет никакого выражения или не завершенного разделителем оператора. Пустой оператор не предусматривает выполнения никаких действий. Он используется там, где синтаксис языка требует присутствия оператора, а по смыслу программы никакие действия не должны выполняться. Пустой оператор чаще всего используется в качестве тела цикла, когда все циклически выполняемые действия определены в его заголовке:
// Вычисляется факториал: 5! for (int i = 0, p=1; i < 5; i++, p * = i);
Перед каждым оператором может быть помещена метка, отделяемая от оператора двоеточием. В качестве метки используется произвольно выбранный программистом уникальный идентификатор:
ABC: x = 4 + х * 3;
Метки локализуются в сфере действия функции. Описания и определения, после которых помещен символ "точка с запятой", считаются операторами. Поэтому перед ними также могут помещаться метки:
metka: int z = 0, d = 4; // Метка перед определением
С помощью пустого оператора, перед которым имеет право стоять метка, метки можно размещать во всех точках программы, где синтаксис разрешает использовать операторы. Прежде чем привести пример, определим составной оператор как заключенную в фигурные скобки последовательность операторов. Если среди операторов, находящихся в фигурных скобках, имеются определения и описания, то составной оператор превращается в блок, где локализованы все определенные в нем объекты. Синтаксически и блок, и составной оператор являются отдельными операторами. Однако ни блок, ни составной оператор не должны заканчиваться точкой с запятой. Для них ограничителем служит закрывающая фигурная скобка. Внутри блока (и составного оператора) любой оператор должен оканчиваться точкой с запятой:
{ int a;
char b = '0';
a = (int)b;
}
// Это блок
{ func (z + 1.0, 22); е=4 * х-1;}// Составной оператор
// Составной оператор с условным переходом к его окончанию:
{ i - -; if (i > k) go to MET; k++; MET:;} // Помечен пустой
// оператор
Говоря о блоках, нужно помнить правила определения сферы действия имен и видимости объектов. Так как и блок, и составной оператор пользуются правами операторов, то разрешено их вложение, причем на глубину вложения синтаксис не накладывает ограничений. О вложении составных операторов и блоков удобнее говорить в связи с циклами, функциями и операторами выбора, к которым мы и перейдем. О входе в блок и выходе из блока речь пойдет в связи с операторами передачи управления (п. 4.4).
К операторам выбора, называемым операторами управления потоком выполнения программы, относят: условный оператор (if...else) и переключатель (switch). Каждый из них служит для выбора пути выполнения программы.
Синтаксис условного оператора:
if (выражение) оператор_1
else оператор_2
Выражение должно быть скалярным и может иметь арифметический тип или тип указателя. Если оно не равно нулю (или не есть пустой указатель), то условие считается истинным и выполняется оператор_1. В противном случае выполняется оператор_2. В качестве операторов нельзя использовать описания и определения. Однако здесь могут быть составные операторы и блоки:
if (х > 0)
{ х = -х; f(x * 2);
}
else
{ int i = 2; х * = i; f(x);
}
При использовании блоков (т.е. составных операторов с определениями и описаниями) нельзя забывать о локализации определяемых в блоке объектов. Например, ошибочна будет последовательность:
if (j > 0)
( int i; i = 2 * j; )
else i = -j ;
так как переменная i локализована в блоке и не существует вне его.
Допустима сокращенная форма условного оператора, в которой отсутствует else и оператор_2. В этом случае при ложности (равенстве нулю) проверяемого условия никакие действия не выполняются:
if (а < 0) а = -а;
В свою очередь, оператор_1 и оператор_2 могут быть условными, что позволяет организовать цепочку проверок условии любой глубины вложенности. В этих цепочках каждый из условных операторов (после проверяемого условия и после else) может быть как полным условным, так и иметь сокращенную форму записи.
При этом могут возникать ошибки неоднозначного сопоставления if и else. Синтаксис языка предполагает, что при вложениях условных операторов каждое else соответствует ближайшему к нему предшествующему if. В качестве примера неверного толкования этого правила в документации [5] приводится такой пример:
if (х == 1)
if (у == 1)
cout << "х равно 1 и у равно 1";
else cout << "х не равно 1";
При х, равном 1, и у, равном 1, совершенно справедливо печатается фраза "х равно 1 и у равно 1". Однако фраза "х не равно 1" может быть напечатана только при х, равном 1, и при у, не равном 1, т.к. else относится к ближайшему if. Внешний условный оператор, где проверяется х==1, является сокращенным и в качестве оператора_1 включает полный условный оператор, в котором проверяется условие у==1. Таким образом, проверка этого условия выполняется только при х, равном 1! Простейшее правильное решение этой микро задачи можно получить, применив фигурные скобки, т.е. построив, составной оператор. Нужно фигурными скобками ограничить область действия внутреннего условного оператора, сделав его неполным. Тем самым внешний оператор превратится в полный условный:
if (х == 1)
{ if (у == 1)
cout << "х равно 1 и у равно 1";
}
else
cout << "х не равно 1";
Теперь else относится к первому if, и выбор выполняется верно. В качестве второго примера вложения условных операторов рассмотрим функцию, возвращающую максимальное из значений трех аргументов:
int max3(int х, int у, int z)
{ if (х < у)
if (y < z)
return z;
else
return y;
else
if (y < x)
return z;
else return x;
}
В тексте соответствие if и else показано с помощью отступов.
Переключатель является наиболее удобным средством для организации мультиветвления. Синтаксис переключателя таков:
switch (переключающее_выражение)
{ case константное_выражение_1: операторы_1;
case константное_выражение_2: операторы_2;
. . .
case константное_выражение_n: операторы_n;
default: операторы;
}
Управляющая конструкция switch передает управление к тому из помеченных с помощью case операторов, для которого значение константного выражения совпадает со значением переключающего выражения. Переключающее выражение должно быть целочисленным или его значение приводится к целому. Значения константных выражений, помещаемых за служебными словами case, приводятся к типу переключающего выражения. В одном переключателе все константные выражения должны иметь различные значения, но быть одного типа. Любой из операторов, помещенных в фигурных скобках после конструкции switch (...), может быть помечен одной или несколькими метками вида
case константное_выражение:
Если значение переключающего выражения не совпадает ни с одним из константных выражений, то выполняется переход к оператору, отмеченному меткой default:. В каждом переключателе должно быть не больше одной метки default, однако эта метка может и отсутствовать. В случае отсутствия метки default при несовпадении переключающего выражения ни с одним из константных выражений, помещаемых вслед за case, в переключателе не выполняется ни один из операторов.
Сами по себе метки case константное_выражение_j: и default:
не изменяют последовательности выполнения операторов. Если не предусмотрены переходы или выход из переключателя, то в нем последовательно выполняются все операторы, начиная с той метки, на которую передано управление. Пример программы с переключателем:
//P4-02.CPP - названия нечетных целых цифр, не меньших заданной
#include < iostream.h >
void main ()
( int ic;
cout << "\nВведите любую десятичную цифру:";
cin << ic;
cout << '\n';
switch (ic)
{ case 0: case 1 cout << "один,";
case 2: case 3 cout << "три,";
case 4: case 5 cout << "пять,";
case 6: case 7 cout << "семь,";
case 8: case 9 cout << "девять.";
break; // Выход из переключателя
default cout << "Ошибка! Это не цифра!";
}
// Конец переключателя
}
// Конец программы
Результаты двух выполнении программы:
Введите любую десятичную цифру:
4 < Enter > пять, семь, девять
Введите любую десятичную цифру:
z < Enter > Ошибка! Это не цифра!
Кроме сказанного о возможностях переключателя, приведенная программа иллюстрирует действие оператора break. С его помощью выполняется выход из переключателя. Если поместить операторы break после вывода каждой цифры, то программа будет печатать название только одной нечетной цифры.
Несмотря на то, что в формате переключателя после конструкции switch () приведен составной оператор, это не обязательно. После switch () может находиться любой оператор, помеченный с использованием служебного слова case. Однако без фигурных скобок такой оператор может быть только один, и смысл переключателя теряется: превращается в разновидность сокращенного условного оператора.
В переключателе могут находиться описания и определения объектов, т.е. составной оператор, входящий в переключатель, может быть блоком. В этом случае нужно избегать ошибок "перескакивания" через определения [2]:
switch (n) // Переключатель с ошибками
{ char d = ' D '; // Никогда не обрабатывается
case 1: float f = 3.14; // Обрабатывается только
// для n == 1
case 2:
...
if (int(d)! = int(f))
...
// Ошибка:
// d и (или) f не определены
...
}
Операторы цикла задают многократное исполнение операторов тела цикла. Определены три разных оператора цикла:
тело цикла не может быть описанием или определением. Это либо отдельный (в том числе пустой) оператор, который всегда завершается точкой с запятой, либо составной оператор, либо блок (заключаются в фигурные скобки). Выражение-условие - это во всех операторах скалярное выражение (чаще всего отношение или арифметическое выражение), определяющее условие продолжения выполнения операций (если его значение не равно нулю). Инициализация_цикла в цикле for всегда завершается точкой с запятой, т.е. отделяется этим разделителем от последующего выражения-условия, которое также завершается точкой с запятой. Даже при отсутствии инициализации_цикла и выражения-условия в цикле for символы "точка с запятой" всегда присутствуют. Список_выражений (в цикле for) - последовательность скалярных выражений, разделенных запятыми. Прекращение выполнения цикла возможно в следующих случаях:
Последнюю из указанных возможностей проиллюстрируем позже, рассматривая особенности операторов передачи управления.
Оператор while (оператор "повторять пока (истинно условие)") называется оператором цикла с предусловием. При входе в цикл вычисляется выражение-условие. Если его значение отлично от нуля, то выполняется тело_цикла. Затем вычисление выражения-условия и выполнение операторов тела_цикла повторяются последовательно, пока значение выражения-условия не станет равным 0. Оператором while удобно пользоваться для просмотра всевозможных последовательностей, если в конце каждой из них находится заранее известный признак. Например, по определению, строка есть последовательность символов типа char, в конце которой находится нулевой символ. Следующая функция подсчитывает длину строки, заданной в качестве параметра:
int length (char *stroka)
{ int len = 0;
while (*stroka++) len++;
return len;
}
Здесь выход из цикла - равенство нулю того элемента строки, который адресуется указателем stroka. (Обратите внимание на порядок вычисления проверяемого выражения. Вначале будет выбрано значение указателя stroka, затем оно будет использовано для доступа по адресу, выбранное значение будет значением выражения в скобках и затем значение указателя будет увеличено на 1.) В качестве проверяемого выражения-условия часто используются отношения. Например, следующая последовательность операторов вычисляет сумму квадратов первых к натуральных чисел (членов натурального ряда):
int i = О; // Счетчик
int s = 0; // Будущая сумма
while (i < К)
s += ++i * i; // Цикл вычисления суммы
Если в выражении-условии нужно сравнивать указатель с нулевым значением (с пустым указателем), то следующие три проверки эквивалентны:
while (point != NULL) ... while (point) ... while (point != 0) ...
Используя, оператор цикла с предусловием, необходимо следить за тем, чтобы операторы тела_цикла воздействовали на выражение-условие, либо оно еще каким-то образом должно изменяться во время вычислений. (Например, за счет побочных эффектов могут изменяться операнды выражения-условия. Часто для этих целей используют унарные операции ++ и --.) Только при изменении выражения-условия можно избежать зацикливания. Например, следующий оператор обеспечивает бесконечное выполнение пустого оператора в теле цикла:
while (1); // Бесконечный цикл с пустым
// оператором в качестве тела
Такой цикл может быть прекращен только за счет событий, происходящих вне потока операций, явно предусмотренных в программе. Самый жесткий вариант такого события - отключение питания ЭВМ. Обычно в конкретных реализациях языка возможности выхода из бесконечного цикла обеспечивают средства доступа к механизму прерываний.
Оператор do (оператор "повторять") называется оператором цикла с постусловием. Он имеет следующий вид:
do
тело_цикла while (выражение-условие);
При входе в цикл do обязательно выполняется тело_цикла. Затем вычисляется выражение-условие и, если его выражение не равно 0, вновь выполняется тело_цикла. При обработке некоторых последовательностей применение цикла с постусловием оказывается удобнее, чем цикла с предусловием. Это бывает в тех случаях, когда обработку нужно заканчивать не до, а после появления концевого признака. Например, следующая функция переписывает заданную строку (указатель star) в другую, заранее подготовленную строку (nov):
void copy_str(char *star, char *nov)
{ do *nov = *star++;
while (*nov++);
}
Еще один вариант того же цикла с пустым телом_цикла:
do
;
while(*nov ++= *star++);
Даже если строка пустая, в ней (по определению строки) в конце присутствует признак '\0'. Именно его наличие проверяется после записи по адресу nov каждого очередного символа.
К выражению-условию требования те же, что и для цикла while с предусловием - оно должно изменяться при итерациях либо за счет операторов тела цикла, либо при вычислениях. Бесконечный цикл:
do
;
while (1);
Оператор итерационного цикла for имеет формат:
for (инициализация_цикла;
выражение-условие;
список выражений)
тело цикла
Здесь инициализация_цикла - последовательность определений (описаний) и выражений, разделяемых запятыми. Все выражения, входящие в инициализацию цикла, вычисляются только один раз при входе в цикл. Чаще всего здесь устанавливаются начальные значения счетчиков и параметров цикла. Выражение-условие такое же, как и в циклах while и do: если оно равно 0, то выполнение цикла прекращается. В случае отсутствия выражения-условия следующий за ним разделитель "точка с запятой" сохраняется. При отсутствии выражения-условия предполагается, что его значение всегда истинно. При отсутствии инициализации цикла соответствующая ему точка с запятой сохраняется. Выражения из списка_выражений вычисляются на каждой итерации после выполнения операторов тела цикла и до следующей проверки выражения-условия. Тело_цикла может быть блоком, отдельным оператором, составным оператором и пустым оператором.
Следующие операторы for иллюстрируют разные решения задачи суммирования квадратов первых к членов натурального ряда:
for (int i = 1, s = 0; i <= K; i++) s += i * i;
for (int i = 0, s = 0; i <= K; s += ++i * i);
for (int i = 0, s=0; i <= К; ) s += ++i * i;
for (int i = 0, s=0; i <= К; ) { int j; j = ++i; s += j * j;
}
Во втором операторе тело_цикла - пустой оператор. В третьем отсутствует список_выражений. Во всех операторах в инициализации_циклов определены (и инициализированы) целые переменные.
Итак, еще раз проследим последовательность выполнения итерационного цикла for. Определяются объекты и вычисляются выражения, включенные в инициализацию_цикла. Вычисляется выражение-условие. Если оно отлично от нуля, выполняются операторы тела_цикла. Затем вычисляются выражения из списка выражений, вновь вычисляется выражение-условие и проверяется его значение. Далее цепочка действий повторяется.
При выполнении итерационного цикла for выражение-условие может изменяться либо при вычислении его значений, либо под действием операторов тела цикла, либо под действием выражений из списка заголовка. Если выражение-условие не изменяется либо отсутствует, то цикл бесконечен. Следующие операторы обеспечивают бесконечное выполнение пустых операторов:
for( ; ;); // Бесконечный цикл
for( ; 1; ); // Бесконечный цикл
В проекте стандарта языка Си++ нет специальных соглашений относительно области действия имен, определенных в инициализирующей части оператора цикла. В конкретных реализациях принято, что область действия таких имен - от места размещения цикла for до конца блока, в котором этот цикл используется.
Например, следующая последовательность операторов
for (int i = 0; i < 3; i++)
cout << "\t" << i;
for ( ; i > 0; i- -)
cout << "\t" << i;
выводит на печать такие значения переменной i:
0 1 2 3 2 1
Если во второй цикл поместить еще одно определение той же переменной i, т.е.
for (int i = 0; i < 3; i++)
cout << "\t" << i;
for (int i = 3; i > 0; i - -)
// Ошибка!!
cout << "\t" << i;
то получим сообщение компилятора об ошибке: "многократное определение переменной i".
Разрешено и широко используется вложение любых циклов в любые циклы. В этом случае в инициализации внутреннего цикла for может быть определена (описана с инициализацией) переменная с таким же именем, что и переменная, определенная во внешнем цикле.
//Р4-ОЗ.СРР - вложение циклов
#include < iostream.h >
void main(void)
{ for (int i = 0; i < 3; i++)
{ cout << "\nДо цикла: i = " << i;
cout << ", вложенный цикл: ";
for (int i = 6; i > 3; i - -)
cout << " i = " << i;
cout << ".\n После: i = " << i << ".";
}
}
Результат выполнения этой программы несколько неожиданный:
До цикла: i = 0, вложенный цикл: i = 6 i = 5 i = 4.
После: i=3
До цикла: i = 1, вложенный цикл: i = 6 i = 5 i = 4.
После: i = 3
До цикла: i = 2, вложенный цикл: i = 6 i = 5 i = 4.
После: i = 3
До внутреннего цикла действует определение переменной i в инициализации внешнего цикла for. Инициализация внутреннего цикла определяет другую переменную с тем же именем, и это определение остается действительным до конца тела внешнего цикла.
К операторам передачи управления относят оператор безусловного перехода, иначе - оператор безусловной передачи управления (go to), оператор возврата из функции (return), оператор выхода из цикла или переключателя (break) и оператор перехода к следующей итерации цикла (continue).
Оператор безусловного перехода имеет вид:
Go to идентификатор;
где идентификатор - имя метки оператора, расположенного в той же функции, где используется оператор безусловного перехода.
Передача управления разрешена на любой помеченный оператор в теле функции. Однако существует одно важное ограничение: запрещено "перескакивать" через описания, содержащие инициализацию объектов. Это ограничение не распространяется на вложенные блоки, которые можно обходить целиком. Следующий фрагмент иллюстрирует сказанное:
gо to B; // Ошибочный переход, минуя описание
float х = 0.0; // Инициализация не будет выполнена
go to B; // Допустимый переход, минуя блок
{ int n = 10; // Внутри блока определена переменная
х = n * х + х;
}
В: соut << "\tx = " << х;
Все операторы блока достижимы для перехода к ним из внешних блоков. Однако при таких переходах необходимо соблюдать то же самое правило: нельзя передавать управление в блок, обходя инициализацию. Следовательно, будет ошибочным переход к операторам блока, перед которыми помещены описания с явной или неявной инициализацией. Это же требование обязательного выполнения инициализации справедливо и при внутренних переходах в блоке. Следующий фрагмент содержит обе указанные ошибки:
{ ... // Внешний блок
gо tо AВС; // Во внутренний блок, минуя описание ii
...
{ int ii = 15; // Внутренний блок
... AВС:
...
gо tо ХУZ; // Обход описания СС сhаг СС = ' ';
...
ХУZ:
...
}
...
}
Принятая в настоящее время дисциплина программирования рекомендует либо вовсе отказаться от оператора gо tо, либо свести его применение к минимуму и строго придерживаться следующих рекомендаций [16]:
Следование перечисленным рекомендациям позволяет исключить возможные нежелательные последствия бессистемного использования оператора безусловного перехода. Полностью отказываться от оператора go tо вряд ли стоит. Есть случаи, когда этот оператор обеспечивает наиболее простые и понятные решения. Один из них - это ситуация, когда в рамках текста одной функции необходимо из разных мест переходить к одному участку программы. Если по каким-либо причинам эту часть программы нельзя оформить в виде функции, то наиболее простое решение - применение безусловного перехода с помощью оператора go to. Второй случай возникает, когда нужно выйти из нескольких вложенных друг в друга циклов или переключателей. Оператор brеаk прерывания цикла и выхода из переключателя здесь не поможет, так как он обеспечивает выход только из самого внутреннего вложенного цикла или переключателя. Например, в задаче поиска в матрице хотя бы одного элемента с заданным значением для перебора элементов матрицы обычно используют числа вложенных цикла. Как только элемент с заданным значением будет найден, нужно выйти сразу из двух циклов, что удобно сделать с помощью
Оператор возврата из функции имеет вид:
return выражение;
или просто
return;
Выражение, если оно присутствует, может быть только скалярным. Например, следующая функция вычисляет и возвращает куб значения своего аргумента:
float cube(float х) {return z * z * z;}
Выражение в операторе rеturn не может присутствовать в том случае, если возвращаемое функцией значение имеет тип void. Например, следующая функция выводит на экран дисплея, связанный с потоком cout, значение третьей степени своего аргумента и не возвращает в точку вызова никакого значения:
void cube_print (float z)
}
cout << "\t сubе = " << z * z * z;
return;
}
В данном примере оператор возврата из функции не содержит выражения.
Оператор brеаk служит для принудительного выхода из цикла или переключателя. Определение "принудительный" подчеркивает безусловность перехода. Например, в случае цикла не проверяются и не учитываются условия дальнейшего продолжения итераций. Оператор break. прекращает выполнение оператора цикла или переключателя и осуществляет передачу управления (переход) к следующему за циклом или переключателем оператору. При этом в отличие от перехода с помощью goto оператор, к которому выполняется передача управления, не должен быть помечен. Оператор break нельзя использовать нигде, кроме циклов и переключателей.
Необходимость в использовании оператора brеаk в теле цикла возникает, когда условия продолжения итераций нужно проверять не в начале итерации (циклы fог, whi1е), не в конце итерации (цикл dо), а в середине тела цикла. В этом случае тело цикла может иметь такую структуру:
{операторы
if (условие) brеаk;
операторы}
Например, если начальные значения целых переменных i, j таковы, что i < j, то следующий цикл определяет наименьшее целое, не меньшее их среднего арифметического:
while (i < j)
{i++;
if (i == j) brеаk;
j - -;
}
Для i == 0, j == 3 результат i == j == 2 достигается при выходе из цикла с помощью оператора brеаk. (Запись i == j == 2 не в тексте программы означает равенство значений переменных х, j и константы 2.) Для i == о, j == 2 результат i == j == 1 будет получен при естественном завершении цикла.
Оператор brеаk практически незаменим в переключателях, когда с их помощью надо организовать разветвление. Например, следующая программа печатает название любой, но только одной, восьмеричной цифры:
//Р4-04.СРР - оператор brеаk в переключателе
#include < iostream. h >
void main ()
{ int iс;
cout << "\n Введите восьмеричную цифру: ";
cin >> iс;
соut << "\n" << iс;
switch (iс)
{ саsе 0: соut << " - нуль"; brеаk;
case 1: соut << " - один"; brеаk;
саsе 2: соut << " - два"; brеаk;
саsе 3: соut << " - три"; brеаk;
саsе 4: соut << " - четыре"; brеаk;
саsе 5: соut << " - пять"; brеаk;
саsе 6: соut << " - шесть"; brеаk;
саsе 7: соut << " - семь"; brеаk;
defaul t: соut << " - это не восьмеричная цифра!";
}
соut << "\nКонец выполнения программы.";
}
Программа напечатает название только одной введенной цифры и прекратит работу. Если в ней удалить операторы brеаk, то в переключателе будут последовательно выполнены все операторы, начиная с помеченного нужным (введенным) значением.
Циклы и переключатели могут быть многократно вложенными. Однако следует помнить, что оператор brеаk позволяет выйти только из самого внутреннего цикла или переключателя. Например, в следующей программе, которая в символьном массиве подсчитывает количество нулей (lс0) и единиц (0с1), в цикл вложен переключатель:
//Р4-05.СРР - brеаk при вложении переключателя в цикл
#include < iostream.h >
void main(void)
{ сhar с[] - "АВС100111";
int k0 = 0, k1 = 0;
fоr (int i = 0; с[i]!= '\0'; i++)
switch (с [i])
{ саsе '0': k0++; brеаk;
case '1': k1++; brеаk;
default: brеаk;
}
соut << "\nВ строкe " << k0 << "нуля," <<
k1 << "единицы";
}