Лабораторная работа № 9 СТРУКТУРА ПРОГРАММЫ. СКАЛЯРНЫЕ ТИПЫ

advertisement
3
Лабораторная работа № 9
СТРУКТУРА ПРОГРАММЫ. СКАЛЯРНЫЕ ТИПЫ
ДАННЫХ. ВЫРАЖЕНИЯ И ПРИСВАИВАНИЯ
Цель: Изучить категории типов данных, виды выражений и
операций и работу с ними на языке Си.
Общие сведения
Разговор о типах начнем с переменных. В пособиях по языкам
программирования переменную чаще всего определяют как пару "имя" –
"значение". Имени соответствует адрес (ссылка) на участок памяти,
выделенный переменной, а значением является содержимое этого участка.
Именем служит идентификатор, а значение соответствует типу переменной,
определяющему множество допустимых значений и набор операций, для
которых переменная может служить операндом. Множество допустимых
значений переменной обычно совпадает со множеством допустимых констант
того же типа. Таким образом, вводятся вещественные, целые и символьные
переменные, причем символьные (char) иногда относят к целым.
Целочисленные и вещественные считаются арифметическими типами.
Арифметический (включая символьный) тип является частным случаем
скалярных типов. К скалярным типам, кроме арифметических, относятся
указатели, ссылки и перечисления. Перечисления (enum) уже введены при
рассмотрении целых констант. К указателям и ссылкам вернемся немного
позже. Переменные типизируются с помощью определений и описаний. Сразу
же введем терминологические соглашения. В отличие от описания определение
не только вводит объект (например, переменную), но и предполагает, что на
основании этого определения компилятор выделит память для объекта
(переменной). Для определения и описания переменных основных типов
используются следующие ключевые слова, каждое из которых в отдельности
может выступать в качестве имени типа:
– char (символьный);
– short (короткий целый);
– int (целый);
– long (длинный целый);
– float (вещественный);
– double (вещественный с удвоенной точностью);
– void (отсутствие значения).
Применение в определениях типов отдельных служебных слов int, char,
short, long эквивалентно signed int, signed char, signed short, signed long. Именно
поэтому служебное слово signed обычно опускается в определениях и
описаниях. Использование при задании типа только одного unsigned
4
эквивалентно unsigned int. При операциях с беззнаковыми (unsigned) целыми не
возникает переполнение, так как используется арифметика по модулю 2 в
степени п, где п – количество битовых разрядов, выделяемых для
представления соответствующих значений.
Переменные одного типа занимают в памяти одно и то же количество
единиц (байтов), и это количество единиц может быть всегда вычислено с
помощью операции sizeof, как мы это делали в описании ее возможностей. Вот
еще несколько примеров:
//Размеры разных типов данных
#include <iostream.h>
void main()
{ int i;
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 << "\t sizeof (double) = " << sizeof(d) ;
cout << "\n sizeof (long double) = " << sizeof(ld);
}
Результаты выполнения:
sizeof (int) = 2
sizeof (long) = 4
sizeof (double) = 8
sizeof (unsigned int) = 2
sizeof (unsigned long) = 4
sizeof (long double) = 10
В таблице приведены типы данных, их размеры в памяти и диапазоны
допустимых значений для компиляторов, ориентированных на ПЭВМ
семейства IBM PC/XT/AT.
Выражение – это последовательность операндов, разделителей и знаков
операций, задающая вычисление. Порядок применения операций к операндам
определяется рангами (приоритетами) операций и правилами группирования
операций (их ассоциативностью). Для изменения порядка выполнения
операций и их группирования используют разделители (круглые скобки). В
общем случае унарные операции (ранг 2), условная операция (ранг 14) и
операции присваивания (ранг 15) правоассоциативны, а остальные операции
левоассоциативны. Таким образом, х = у = z означает х = (у = z), a x + у - z
означает (х + у) - z.
5
Основные типы данных
Тип
данных
Unsigned
char
Char
Enum
Unsigned
int
Short int
Int
Unsigned
long
Long
Float
Double
Long
double
Размер,
бит
8
Диапазон значений
0...255
Небольшие целые числа и коды символов
8
16
16
-128...127
-32768,..32767
0...65535
Очень малые целые числа и ASCII-коды
Упорядоченные наборы целых значении
Большие целые и счетчики циклов
16
16
32
-32768...32767
-32768...32767
0. . .4294967295
Небольшие целые
Небольшие целые, управление циклами
Астрономические расстояния
32
-2147483648...
…2147483647
3.4Е-38...3.4Е+38
1.7Е-308...1.7Е+308
3.4Е-4932...
…1.1Е+4932
32
64
80
Примечание – назначение типа
Большие числа, популяции
Научные расчеты (7 значащих цифр)
Научные расчеты (15 значащих цифр)
Финансовые расчеты (19 значащих цифр)
Kpoмe формирования результирующего значения, вычисление
выражения может вызвать побочные эффекты. Например, значением
выражения 2=3, z + 2 будет 5, а в качестве побочного эффекта z примет
значение 3. В результате вычисления х > 0 ? х-- : х будет получено значение х, а
в качестве побочного эффекта положительное значение х будет уменьшено на
1. В языке Си++ программист может расширить действие стандартных
операций (overload – перегрузка), т. е. придать им новый смысл при работе с
нестандартными для них операндами. Отметим, что операции могут быть
распространены на вводимые пользователем типы, однако у программиста нет
возможности изменить действие операций на операнды стандартных типов. Эту
связанную с классами возможность языка Си++ рассмотрим позже, а сейчас
остановимся на некоторых свойствах операций, стандартно определенных для
тех типов, для которых эти операции введены.
Формальный синтаксис языка Си++ предусматривает рекурсивное
определение выражений. Рекурсивность синтаксических определений (не
только для выражений) и широкие возможности конструирования новых типов
делают попытки "однопроходного" изложения семантики сразу всего языка
Си++ практически безнадежными. Поясним это, рассмотрев выражения.
Основным исходным элементом любого выражения является первичное
выражение. К ним относятся:
• константа;
• имя;
• выражение;
• ::идентификатор;
• ::имя функции-операции;
6
• ::квалифицированное имя;
• this;
• псевдопеременная (реализация Си++ для ПЭВМ).
Константа. К константам относятся:
• целая константа;
• символьная константа;
• перечислимая константа;
• вещественная константа;
• строковая константа (строка).
Имя. К именам относятся идентификатор. Он может использоваться в
качестве имени только в том случае, если он введен с помощью подходящего
определения. Самый распространенный представитель-идентификатор как имя
переменной.
Имя функции-операции вводится только в связи с расширением действия (с
перегрузкой) операций. Механизм перегрузки возможно объяснить только
после определения понятия «класс».
Имя функции приведения
Функции приведения или преобразующие функции являются
компонентами классов, и для объяснения их семантики требуется ввести
соответствующие понятия, относящиеся к классам.
Например, имя_класса обозначает обращение к специальному компоненту
класса – деструктору.
Квалифицированное_имя
(уточненное_имя)
имеет
рекурсивное
определение следующего формата:
квалифицированное_имя_класса: :имя
Таким образом, чтобы определить понятие квалифицированное_имя,
требуется ввести понятие квалифицированное_имя__класса и уже иметь
определение имени. Следовательно, не вводя понятие "класс", можно в
качестве имен из всего перечисленного использовать только идентификаторы.
Третий вариант первичного выражения содержит рекурсию, т. к., это
произвольное выражение, заключенное в круглые скобки.
::идентификатор.
Четвертый вариант первичного выражения :: идентификатор включает
операцию изменения области действия, смысл которой объяснялся.
Все остальные представители первичных выражений (за исключением
псевдопеременных) невозможно объяснять и иллюстрировать примерами, не
вводя понятие класса. Таким образом, следуя логике нашего изложения
(алфавит – лексемы – базовые типы – скалярные типы – выражения) и не вводя
структурированных типов, к которым относятся классы, придется
рассматривать не все варианты первичных выражений и тем более не все
варианты выражений.
Учитывая это ограничение, продолжим "конструирование" выражений.
На основе первичных выражений вводится постфиксное выражение, которым
может быть:
7
– первичное выражение;
– постфиксное выражение [выражение];
– постфиксное_выражение (список_выражений);
– имя простого типа (списки выражений);
Если приведенную синтаксическую конструкцию записать без первых
круглых скобок, т. е. в виде int *fun(char), то компилятор воспримет ее как
указатель на функцию.
Практические задания
Составить программу
произвольном значении х.
вычисления
значения
функции
y(x)
при
1. y=(1+cos)/(3^x+x^(1/2)-x^(x)).
2. y=(x+1)/(e*cos x).
3. y=2x*ln(2x+x).
4. y=(cos x )/(1+tg x).
5. y=(sin x)/(1+ln(x+1)).
6. y=(e/(ln(x) +1))*(x-1).
7. y=(x+3*cos x)/(x+tg x).
8. y=sin(x)+e-1.
9. y=((1+2*e)^(0.5))-cos(x).
10. y=ln(sin(x))+x+2.
11. y=x/(e+tg(x/2)).
12. y=0.5*ln(e*x)+cos(x).
13. y=1/(cos(x)*sin(x)).
14.y=(4x*e+((3x)^(x)))/(1+x).
15. y=x+(1+2x)^(0.5).
16. y=ln(cos(x+1)).
17. y=e/(x+sin(2+x)).
18. y=(3sin(x))/(7x-sin(x)+e).
19. y=(tg(x))/(2+sin(x)).
20. y=(1-cos(x))/arcsin(x).
8
Лабораторная работа № 10
ПРАВИЛА ИСПОЛЬЗОВАНИЯ ДИРЕКТИВ
ПРЕПРОЦЕССОРА
Цель: Изучение основных правил использования директив
препроцессора Си.
Общие сведения
В интегрированную среду подготовки программ на Си++ или в
компилятор языка как обязательный компонент входит
препроцессор.
Назначение препроцессора - обработка исходного текста программы при ее
компиляции. Препроцессорная обработка в соответствии с требованиями
стандарта языка Си++ включает несколько стадий, выполняемых
последовательно. Конкретная реализация транслятора может разъединять
несколько стадий, но результат должен быть таким, как если бы они
выполнялись последовательно:
– Все системно зависимые обозначения (например, системно зависимый
индикатор конца строки) перекодируются в стандартные коды;
– Каждая пара из символов '\' и "конец строки" убираются, и тем самым
следующая строка исходного файла присоединяется к строке, в
которой находилась эта пара символов;
– В тексте распознаются директивы препроцессора, а каждый
комментарий заменяется одним символом пустого промежутка.
– Выполняются
директивы
препроцессора
и
производятся
макроподстановки;
– ЕSC-последовательности в символьных константах и символьных
строках, например, '\n' заменяются на их эквиваленты (на
соответствующие числовые коды);
– Смежные символьные строки конкатенируются, т. е. соединяются в
одну строку.
Знакомство с перечисленными задачами препроцессорной обработки
объясняет некоторые соглашения синтаксиса языка. Например, становится
понятным смысл утверждений: каждая символьная строка может быть
перенесена в файле на следующую строку, если использовать символ '\’, или
две символьные строки, записанные рядом, воспринимаются как одна строка.
Рассмотрим подробно стадию обработки директив препроцессора. При ее
выполнении возможны следующие действия:
– замена идентификаторов (обозначений) заранее подготовленными
последовательностями символов;
– включение в программу текстов из указанных файлов;
– исключение из программы отдельных частей ее текста (условная
9
компиляция);
– макроподстановка, т. е. замена обозначения параметризованным
текстом, формируемым препроцессором с учетом конкретных
параметров (аргументов).
Для управления препроцессором, т. е. для задания нужных действий,
используются команды (директивы) препроцессора,
каждая из которых
помещается на отдельной строке и начинается с символа #. Определены
следующие препроцессорные директивы: #defune, #include, #undef, #if, #ifdef,
#ifndef, #else, #endif, #elif, #line, #error, #pragma, #.
Директива #define имеет несколько модификаций. Они предусматривают
определение макросов или препроцессорных идентификаторов, каждому из
которых ставится в соответствие некоторая символьная последовательность. В
последующем
тексте
программы
препроцессорные
идентификаторы
заменяются на заранее запланированные последовательности символов.
Директива #include позволяет включать в текст программы текст из
выбранного файла.
Директива #undef отменяет действие команды #define, которая
определила до этого имя препроцессорного идентификатора.
Директива #if и ее модификации #ifdef, #ifndef совместно с директивами
#else, #endif, #elif позволяют организовать условную обработку текста
программы. Условность состоит в том, что компилируется не весь текст, а
только те его части, которые так или иначе выделены с помощью
перечисленных директив.
Директива #line позволяет управлять нумерацией строк в файле с
программой. Имя файла и начальный номер строки указываются
непосредственно в директиве #line.
Директива #еrror позволяет задать текст диагностического сообщения,
которое выводится при возникновении ошибок.
Директива #pragma вызывает действия, зависящие от реализации.
Директива # ничего не вызывает, т. к. является пустой директивой, т. е. не
дает никакого эффекта и всегда игнорируется.
Рассмотрим возможности перечисленных команд при решении типичных
задач, поручаемых препроцессору.
Для
замены
идентификатора
заранее
подготовленной
последовательностью символов используется следующая директива (обратите
внимание на пробелы): #define идентификатор строка замещения.
Директива может размещаться в любом месте обрабатываемого текста, а
ее действие в обычном случае распространяется от точки замещения до конца
текста. Директива, во-первых, определяет идентификатор как процессорный. В
результате обработки все вхождения определенного командой #define
идентификатора в текст программы вменяются строкой замещения, окончанием
которой обычно служит признак конца той строки, где размещена команда
#define. Символы пробелов, помещенные в начале и в конце строки замещения,
в подстановке не используются. Например:
10
Исходный текст
Результат препроцессорной обработки
#define begin {
#define end }
void main()
void main()
begin
{
операторы
операторы
end
}
В данном случае программист решил использовать в качестве
операторных скобок идентификаторы begin, end.
Компилятор языка Си++ не может обрабатывать таких скобок, и поэтому
до компиляции препроцессор заменяет все вхождения этих идентификаторов
стандартными скобками { и }. Соответствующие указания программист дал
препроцессору с помощью директив #define.
Директиву #undef удобно использовать при разработке больших
программ, когда они собираются из отдельных "кусков текста", написанных в
разное время или разными программистами. В этом случае могут встретиться
одинаковые обозначения разных объектов. Чтобы не изменять исходных
файлов, включаемый текст можно "обрамлять" подходящими директивами
#define - #undef и тем самым устранять возможные ошибки. Приведем пример:
…
А = 10; // Основной текст
…
#define A X
…
А = 5; // Включенный текст
…
#undef A
…
В = А; // Основной текст
…
При выполнении программы В примет значение 10, несмотря на наличие
оператора присваивания А = 5 во включенном тексте.
Для включения текста из файла используется команда #include, имеющая
две формы записи:
#include <имя файла> // Имя в угловых скобках
#include "имя файла" // Имя в кавычках.
Если имя_файла – в угловых скобках, то препроцессор разыскивает файл
в стандартных системных каталогах. Если имя_файла заключено в кавычки, то
вначале препроцессор просматривает текущий каталог пользователя и только
затем обращается к просмотру стандартных системных каталогов.
Начиная работать с языком Си++, пользователь сразу же сталкивается с
необходимостью использования в программах средств ввода-вывода. Для этого
11
в начале текста программы помещают директиву #include <iostream.h>
Выполняя эту директиву, препроцессор включает в программу средства
связи с библиотекой ввода-вывода. Поиск файла iostream.h ведется в
стандартных системных каталогах. По принятому соглашению суффикс .h
приписывается тем файлам, которые нужно помещать в заголовке программы,
т. е. до исполняемых операторов.
Кроме такого, в некоторой степени стандартного файла, каким является
iostream.h, в заголовок программы могут быть включены любые другие файлы
(стандартные или подготовленные специально).
Заголовочные файлы оказываются весьма эффективным средством при
модульной разработке крупных программ, когда связь между модулями,
размещаемыми в разных файлах, реализуется не только с помощью параметров,
но и через внешние объекты, глобальные для нескольких или всех модулей.
Описания таких внешних объектов (переменных, массивов, структур и т. п.)
помещаются в одном файле, который с помощью директив #include включается
во все модули, где необходимы внешние объекты. В тот же файл можно
включить и директиву подключения библиотеки функций ввода-вывода.
Заголовочный файл может быть, например, таким:
#include <iostream.h> // Включение средств обмена
extern int ii, jj, 11; // Целые внешние переменные
extern float AA, BB; // Вещественные внешние переменные
В практике программирования на Си++ обычна и обратная ситуация.
Если в программе используется несколько функций, то иногда удобно текст
каждой из них хранить в отдельном файле. При подготовке программы
пользователь включает в нее тексты используемых функций с помощью команд
#include.
Практические задания:
1.
Разработать функцию зануления столбца двумерного массива.
Занулить 3 столбца. Результат вывести на печать.
2.
Разработать функцию зануления строки двумерного массива.
Занулить 3 произвольные строки. Результат вывести на печать.
3.
Разработать функцию очистки (зануления) двумерного массива.
Занулить два массива. Результат вывести на печать.
4.
Разработать функцию, позволяющую инвертировать каждый
столбец двумерного массива целых чисел. Обработать два массива. Результат
вывести на печать.
5.
Разработать функцию, которая меняет местами содержимое
старшего и младшего байтов каждого элемента двумерного массива. Ввести два
двумерных массива разной длины. Результаты вывести на печать.
6.
Заполнить 2 двумерных массива разной длины символами.
Написать функцию, которая ставит символ пробела в старшем байте каждого
слова массива. Обработать 2 массива. Результаты вывести на печать.
12
7.
Разработать функцию поэлементного суммирования двух строк
массива в одну. Ввести массив. Сложить два раза по две строки. Вывести на
печать.
8.
Разработать функцию поэлементного суммирования двух столбцов
двумерного массива. Просуммировать два раза по два столбца. Результат
вывести на печать.
9.
Разработать функцию, позволяющую поменять местами две
произвольные столбца двумерного массива. Ввести массив. Поменять два раза
по два столбца. Результат вывести на печать.
10. Разработать функцию, позволяющую поменять местами две
произвольные строки двухмерного массива. Ввести массив. Поменять два раза
по две строки. Результат вывести на печать.
11. Разработать функцию выделения произвольного столбца в
двумерном массиве. Ввести массив и выделить в нем два произвольных
столбца. Результат вывести на печать.
12. Разработать функцию поэлементного умножения массива на
константу. Результат вывести на печать.
13. Разработать функцию поэлементного сложения двух двумерных
массивов одинаковой длины в третий. Ввести шесть двумерных массивов.
Результат вычислений вывести на печать.
14. Разработать функцию выделения произвольной строки из
двумерного массива. Ввести двумерный массив. Выделить и вывести две
строки. Результат вывести на печать.
15. Разработать функцию суммирования элементов двумерного
массива произвольной длины. Ввести два массива разной длины. Результат
вычислений вывести на печать.
16. Разработать функцию, которая переводит десятичное число в
двоичное. Ввести пять чисел. Результат вывести на печать.
17. Разработать
функцию
перевода
десятичного
числа
в
шестнадцатеричное представление. Ввести пять чисел. Результат вывести на
печать.
18. Разработать
функцию
перевода
двоичного
числа
в
шестнадцатеричное. Ввести десять чисел. Результат вывести на печать.
19. Разработать функцию перевода двоичного числа в восьмеричное
представление. Ввести восемь чисел. Результат вывести на печать.
20. Разработать функцию перевода десятичного числа в восьмеричное
представление. Ввести десять чисел. Результат вывести на печать.
13
Лабораторная работа № 11
ЧАСТЬ 1
ОПЕРАТОРЫ ЯЗЫКА С++: ОПЕРАТОРЫ УСЛОВИЯ
Цель: Ознакомиться с операторами условия и методами
реализации алгоритма программы с их помощью.
Общие сведения
К операторам выбора, называемым операторами управления потоком
выполнения программы, относят: условный оператор (if...else) и переключатель
(switch). Каждый из них служит для выбора пути выполнения программы.
Синтаксис условного оператора:
if (выражение) оператор 1; else оператор 2;
Выражение должно быть скалярным и может иметь арифметический тип
или тип указателя. Если оно не равно нулю (или не есть пустой указатель), то
условие считается истинным и выполняется оператор_1. В противном случае
выполняется оператор_2. В качестве операторов нельзя использовать описания
и определения. Однако здесь могут быть составные операторы и блоки:
if (х > 0) {х = -x; f(x * 2); }
else { int i = 2; х *= i; t(x) ; }
При использовании блоков (т. е. составных операторов с определениями
и описаниями) нельзя забывать о локализации определяемых в блоке объектов.
Например, ошибочна будет последовательность:
if (j > 0) { int i; i = 2 * j; ) else i = -j;
т. к. переменная i локализована в блоке и не существует вне его.
Допустима сокращенная форма условного оператора, в которой
отсутствует else и оператор_2. В этом случае при ложности (равенстве нулю)
проверяемого условия никакие действия не выполняются:
if (а < 0) а = -а;
В свою очередь, оператор_1 и оператор_2 могут быть условными, что
позволяет организовать цепочку проверок условий любой глубины
вложенности. В этих цепочках каждый из условных операторов (после
проверяемого условия и после else) может быть полным условным, или иметь
сокращенную форму записи.
При этом могут возникать ошибки неоднозначного сопоставления if и
else. Синтаксис языка предполагает, что при вложениях условных операторов
каждое else соответствует ближайшему к нему предшествующему if. Приведём
пример неверного толкования этого правила:
14
if (х == 1)
if (у == 1) cout « "х равно 1 и у равно 1";
else cout « "х не равно 1";
При х, равном 1, и у, равном 1, совершенно справедливо печатается фраза
"х равно 1 и у равно I". Однако фраза "х не равно 1" может быть напечатана
только при х, равном 1, и при у, не равном 1, т. к. else относится к ближайшему
if. Внешний условный оператор, где проверяется х==1, является сокращенным
и в качестве оператора_1 включает полный условный оператор, в котором
проверяется условие у==1. Таким образом, проверка этого условия
выполняется только при х, равном 1. Простейшее правильное решение этой
микрозадачи можно получить, применив фигурные скобки, т. е. построив
составной оператор. Нужно фигурными скобками ограничить область действия
внутреннего условного оператора, сделав его неполным. Тем самым внешний
оператор превратится в полный условный:
if (х == 1)
{ if (у == 1) cout « "х равно 1 и у равно 1"; }
else cout « "х не равно I";
Теперь else относится к первому if, и выбор выполняется верно. В
качестве второго примера вложения условных операторов рассмотрим
функцию, возвращающую максимальное из значений трех аргументов:
int max3(int х, int у, int z)
{ if (х < у)
if (у < z) return z
else return y;
else
if (ac < z) return z
else return x;
В тексте соответствие if и else показано с помощью отступов.
Переключатель является наиболее удобным средством для организации
мультиветвления. Синтаксис переключателя таков:
switch (переключающее_выражение)
{ case константное_выражение 1: операторы 1 ;
case константное выражение 2: операторы 2;
…
case константное выражение п: операторы п;
default: операторы;
}
Управляющая конструкция switch передает управление к тому из
помеченных с помощью case операторов, для которого значение константного
выражения
совпадает
со
значением
переключающего
выражения.
Переключающее выражение должно быть целочисленным или его значение
приводится к целому. Значения константных выражений, помещаемых за
15
служебными словами case, приводятся к типу переключающего выражения. В
одном переключателе все константные выражения должны иметь различные
значения, но быть одного типа. Любой из операторов, помещенных в фигурных
скобках после конструкции switch (...), может быть помечен одной или
несколькими метками вида case константное_выражение:
Если значение переключающего выражения не совпадает ни с одним из
константных выражений, то выполняется переход к оператору, отмеченному
меткой default:. В каждом переключателе должно быть не больше одной метки
default, однако эта метка может и отсутствовать. В случае отсутствия метки
default при несовпадении переключающего выражения ни с одним из
константных выражений, помещаемых вслед за case, в переключателе не
выполняется ни один из операторов.
Сами по себе метки case константное выражение j : и default: не изменяют
последовательности выполнения операторов. Если не предусмотрены переходы
или выход из переключателя, то в нем последовательно выполняются все
операторы, начиная с той метки, на которую передано управление. Пример
программы с переключателем:
//названия нечетных целых цифр, не меньших заданной #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;
// Выход из переключателя
cout « "Ошибка! Это не цифра'";
}
//Конец переключателя
}
// Конец программы
Результаты двух выполнений программы:
Введите любую десятичную цифру: 4 <Enter>
пять, семь, девять
Введите любую десятичную цифру: z <Enter>
Ошибка! Это не цифра!
Кроме сказанного о возможностях переключателя, приведенная
программа иллюстрирует действие оператора break. С его помощью
выполняется выход из переключателя. Если поместить операторы break после
16
вывода каждой цифры, то программа будет печатать название только одной
нечетной цифры.
Несмотря на то, что в формате переключателя после конструкции switch()
приведен составной оператор, это не обязательно. После switch() может
находиться любой оператор, помеченный с использованием служебного слова
case. Однако без фигурных скобок такой оператор может быть только один, и
смысл переключателя теряется, он превращается в разновидность
сокращенного условного оператора.
В переключателе могут находиться описания и определения объектов, т.
е. составной оператор, входящий в переключатель, может быть блоком. В этом
случае нужно избегать ошибок "перескакивания" через определения:
Switch (n)
// Переключатель с ошибками
{ char d = ‘D’
// Никогда не обрабатывается
case 1: float f= 3.14;
// Обрабатывается только для n = 1
case 2: … if (int (d) != int(f))...
// Ошибка:
Практические задания
Написать программу вычисления следующих функций:
1. y=x, x=1.5
2. y= ln(s+1)+t, |s|<1
y=e+1, x1.5
y=ln|s|+e, |s|>1
3. y= bx+cx,b<c
y=sin x +b, b=c
y= c+bc, b>c
4. y= sin(t-1)+2,t<a
y=e+4,t>a
5. y= 0.7ln(cx),c>x
y= x,c<x
y=1,c=x
6. y= |q|,q<1
y= 0,q=-1
y=q+2q,q<-1
7. y= e,b<a
y= e,a<b
y=1,b=a
8. y= 2z+x,z>x
y= z-x,z<x
y=z,z=x
9. Найти сумму положительных элементов вектора.
10. Вычислить y= na+(n-1)a+…+a.
11. Задать число и вывести все нечетные числа до него. Результат вывести на
печать.
12. Задать число и вывести числа, делящиеся на него. Выводить не менее 20
чисел.
17
13. Вывести случайным образом ряд из 15 цифр. Задать число и подсчитать
количество таких цифр в выведенном ряду.
14. Задать число и вывести все числа до него с заданным интервалом. Результат
вывести на печать.
15. y= ln(s+1)+t, |s|<1
y=ln|s|, |s|>1
16. y= e+1,b<a
y= e^2,a<b
y=1,b=a
17. y= 2z+5x,z>x
y= z-2x,z<x
y=z+1,z=x
18. y= bx+c,b<c
y=arcsin x +b, b=c
y= cx+bc, b>c
19. Вычислить y= a+2a+…+na.
20. Вычислить y= a+…+a^n.
Лабораторная работа №11
ЧАСТЬ 2
ОПЕРАТОРЫ ЯЗЫКА С++: ОПЕРАТОРЫ ЦИКЛА
Цель: Ознакомиться с операторами цикла их видами и
правилами их использования.
Общие сведения
Операторы цикла задают многократное исполнение операторов тела
цикла. Определены три разных оператора цикла:
1) цикл с предусловием:
while (выражение-условие)
тело цикла;
2) цикл с постусловием:
do
тело цикла
while (выражение-условие);
3) итерационный цикл:
for (инициализация_цикла;
выражение-условие;
список выражений)
тело цикла.
18
Тело_цикла не может быть описанием или определением. Это либо
отдельный (в том числе пустой) оператор, который всегда завершается точкой с
запятой, либо составной оператор, либо блок (заключается в фигурные скобки
). Выражение-условие – это во всех операторах скалярное выражение (чаще
всего отношение или арифметическое выражение), определяющее условие
продолжения выполнения итераций (если его значение не равно нулю).
Инициализация_цикла в цикле for всегда завершается точкой с запятой, т.
е. отделяется этим разделителем от последующего выражения-условия, которое
также
завершается
точкой
с
запятой.
Даже
при
отсутствии
инициализации_цикла и выражения-условия в цикле for символы "точка с
запятой" всегда присутствуют.
Список_выражений (в цикле for) – последовательность скалярных
выражений, разделенных запятыми.
Прекращение выполнения цикла возможно в следующих случаях:
– при нулевом значеним проверяемого выражения-условия;
– выполненим в теле цикла оператора передачи управления (break, goto,
return) за пределы цикла.
Последнюю из указанных возможностей проиллюстрируем позже,
рассматривая особенности операторов передачи управления.
Оператор while (оператор "повторять пока (истинно условие)")
называется оператором цикла с предусловием. При входе в цикл вычисляется
выражение-условие. Если его значение отлично от нуля, то выполняется
тело_цикла. Затем вычисление выражения-условия и выполнение операторов
тела_цикла повторяются последовательно, пока значение выражения-условия
не станет равным 0.
Оператором while удобно пользоваться для просмотра всевозможных
последовательностей, если в конце каждой из них находится заранее известный
признак. Например, по определению строка есть последовательность символов
типа char, в конце которой находится нулевой символ. Следующая функция
подсчитывает длину строки, заданной в качестве параметра:
int length (char *stroka)
{ int len = 0;
while (*stroka++) len++;
return len;
}
Здесь выход из цикла – равенство нулю того элемента строки, который
адресуется указателем stroka. Обратите внимание на порядок вычисления
проверяемого выражения. Вначале будет выбрано значение указателя stroka,
затем оно будет использовано для доступа по адресу, выбранное значение будет
значением выражения в скобках, и затем значение указателя будет увеличено
на 1.
В качестве проверяемого выражения-условия часто используются
отношения. Например, следующая последовательность операторов вычисляет
19
сумму квадратов первых К натуральных чисел (членов натурального ряда):
int i = 0;
// Счетчик
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++) ;
20
}
Еще один вариант того же цикла с пустым телом_цикла:
do;
while(*nov ++= *star++);
Даже если строка пустая, в ней (по определению строки) в конце
присутствует признак '\0'. Именно его наличие проверяется после записи по
адресу nov каждого очередного символа.
К выражению-условию требования те же, что и к циклу while с
предусловием – оно должно изменяться при итерациях либо за счет операторов
тела цикла, либо при вычислениях. Бесконечный цикл:
do; while(1);
Оператор итерационного цикла for имеет формат:
for (инициализация_цикла;
выражение-условие;
список выражений)
тело_цикла
Здесь инициализация_цикла – последовательность определений
(описаний) и выражений, разделяемых запятыми. Все выражения, входящие в
инициализацию цикла, вычисляются только один раз при входе в цикл. Чаще
всего здесь устанавливаются начальные значения счетчиков и параметров
цикла. Выражение-условие такое же, как и в циклах while и do: если оно равно
0, то выполнение цикла прекращается. В случае отсутствия выражения-условия
следующий за ним разделитель "точка с запятой" сохраняется. При отсутствии
выражения-условия предполагается, что его значение всегда истинно. При
отсутствии инициализации цикла соответствующая ему точка с запятой
сохраняется. Выражения из списка_выражений вычисляются на каждой
итерации после выполнения операторов тела цикла и до следующей проверки
выражения-условия. Тело_цикла может быть отдельным оператором,
составным оператором и пустым оператором.
Следующие операторы for иллюстрируют разные решения уже
упомянутой простой задачи суммирования квадратов первых К членов
натурального ряда:
for (int i =1, s = 0; i <= К; i++) s += i * i;
for (int i = 0, s = 0; i <= К; 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 ; }
Во втором операторе тело_цикла – пустой оператор. В третьем
отсутствует список_выражений. Во всех операторах в инициализации_циклов
определены (и инициализированы) целые переменные.
Итак, еще раз проследим последовательность выполнения итерационного
21
цикла 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 может быть
определена (описана с инициализацией) переменная с таким же именем, что и
переменная, определенная во внешнем цикле.
//вложение циклов
#include <ioatream.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;
22
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. Инициализация внутреннего цикла
определяет другую переменную с тем же именем, и это определение остается
действительным до конца тела внешнего цикла.
Практические задания:
1. Вычислить сумму квадратов первых К натуральных чисел. Результат
вывести на печать.
2. Вычислить квадраты чисел от 0 до 9. Результат вывести на печать.
3. Задать строку из произвольных символов и переписать строку в
обратном порядке. Результат вывести на печать.
4. Вычислить квадраты четных чисел от 0 до 9. Результат вывести на
печать.
5. Вычислить квадраты первых К натуральных чисел. Результат вывести
на печать.
6. Вычислить сумму первых К натуральных чисел. Результат вывести на
печать.
7. Ввести слово и отцентровать его
8. y(x)=(a+ln|x-b|)/(e+d), x=1.7(0.3)3.2
9. y(x)=(sin(x+c)-d)/(e+a), x=1.5(0.1)2
10. y(x)=(ln|x-a|+d)/(cx+bx), x=1(0.3)2.5
11. y(x)=(cos(d+x)+cx)/(ax+b), x=-1(0.2)1
12. y(x)=(cos(a-x)+sin(d))/(ax+bx), x=1(1)5
13. y(x)=(ln(x)+a)/(bx+c), x=1(2)11
14. y(x)=(ex+a)/(x^2), x=1(0.2)3
15. y(x)=(ln(x-a)+b)/(cos(x)), x=2(1)8
16. y(x)=(tg(ln|a-x|))/(a), x=1(0.1)2
17. y(x)=((e+x)^3)/(x^(0.5)), x=1(0.5)6
18. y(x)=(ex+a)/(bx), x=1.5(0.25)5
19. y(x)=(ln|e+x|-ax)/(bx-a), x=3.9(0.2)4.7
23
Лабораторная работа № 11
ЧАСТЬ 3
ОПЕРАТОРЫ ЯЗЫКА С++: ОПЕРАТОРЫ ПЕРЕДАЧИ
УПРАВЛЕНИЯ
Цель: Ознакомиться с операторами передачи управления и
их возможностями.
Общие сведения
К операторам передачи управления относят оператор безусловного
перехода (иначе – оператор безусловной передачи управления (goto)), оператор
возврата из функции (return), оператор выхода из цикла или переключателя
(break) и оператор перехода к следующей итерации цикла (continue).
Оператор безусловного перехода имеет вид
goto идентификатор;
где идентификатор – имя метки оператора, расположенного в той же функции,
где используется оператор безусловного перехода.
Передача управления разрешена на любой помеченный оператор в теле
функции. Однако существует одно важное ограничение: запрещено
"перескакивать" через описания, содержащие инициализацию объектов. Это
ограничение не распространяется на вложенные блоки, которые можно
обходить целиком. Следующий фрагмент иллюстрирует сказанное:
…
goto b;
// Ошибочный переход, минуя описание
float x = 0.0;
// Инициализация не будет выполнена
goto В;
// Допустимый переход, минуя блок
{ int n = 10; // Внутри блока определена переменная
х = n * х + х;
}
B: cout « "\tx = ” « x;
…
Все операторы блока достижимы для перехода к ним из внешних блоков.
Однако при таких переходах необходимо соблюдать то же самое правило:
нельзя передавать управление в блок, обходя инициализацию. Следовательно,
будет ошибочным переход к операторам блока, перед которыми помещены
описания с явной* или неявной инициализацией. Это же требование
обязательного выполнения инициализации справедливо и при внутренних
переходах в блоке. Следующий фрагмент содержит обе указанные ошибки:
{ ...
// Внешний блок
24
goto АВС; // Во внутренний блок, минуя описание ii
…
{ int ii = 15; // Внутренний блок
…
ABC:
…
goto XYZ; // Обход описания CC
char CC = ‘’;
…
XYZ:
…
}
…
}
Принятая в настоящее время дисциплина программирования рекомендует
либо вовсе отказаться от оператора goto, либо свести его применение к
минимуму и строго придерживаться следующих рекомендаций:
– не входить внутрь блока извне;
– не входить внутрь условного оператора, т. е. не передавать управление
операторам, размещенным после служебных слов if или else;
– не входить извне внутрь переключателя (switch);
– не передавать управление внутрь цикла.
Следование перечисленным рекомендациям позволяет исключить
возможные нежелательные последствия бессистемного использования
оператора безусловного перехода. Полностью отказываться от оператора goto
вряд ли стоит. Есть случаи, когда этот оператор обеспечивает наиболее простые
и понятные решения. Один из них – это ситуация, когда в рамках текста одной
функции необходимо из разных мест переходить к одному участку программы.
Если по каким-либо причинам эту часть программы нельзя оформить в виде
функции (например, это может быть текст на ассемблере), то наиболее простое
решение – применение безусловного перехода с помощью оператора goto.
Такое положение возникает, например, при необходимости обрабатывать
ошибки, выявляемые в процессе выполнения программы.
Второй случай возникает, когда нужно выйти из нескольких вложенных
друг в друга циклов или переключателей. Оператор break прерывания цикла и
выхода из переключателя здесь не поможет, так как он обеспечивает выход
только из самого внутреннего вложенного цикла или переключателя.
Например, в задаче поиска в матрице хотя бы одного элемента с заданным
значением для перебора элементов матрицы обычно используют два
вложенных цикла. Как только элемент с заданным значением будет найден,
нужно выйти сразу из двух циклов, что удобно сделать с помощью goto.
Оператор возврата из функции имеет вид:
return выражение;
25
или просто return;
Выражение (если оно присутствует) может быть только скалярным.
Например, следующая функция вычисляет и возвращает куб значения своего
аргумента:
float cube(float z) { return z * z * z; }
Выражение в операторе return не может присутствовать в том случае,
если возвращаемое функцией значение имеет тип void. Например, следующая
функция выводит на экран дисплея, связанный с потоком cout, значение
третьей степени своего аргумента и не возвращает в точку вызова никакого
значения:
void cube_print(float z)
{ cout « "\t cube = " « z * z * z;
return;
}
В данном примере оператор возврата из функции не содержит
выражения.
Оператор break служит для принудительного выхода из цикла или
переключателя. Определение "принудительный" подчеркивает безусловность
перехода. Например, в случае цикла не проверяются и не учитываются условия
дальнейшего продолжения итераций. Оператор break прекращает выполнение
оператора цикла или переключателя и осуществляет передачу управления
(переход) к следующему за циклом или переключателем оператору.
При этом, в отличие от перехода с помощью goto, оператор, к которому
выполняется передача управления, не должен быть помечен. Оператор break
нельзя использовать нигде, кроме циклов и переключателей.
Необходимость в использовании оператора break в теле цикла возникает
тогда, когда условия продолжения итераций нужно проверять не в начале
итерации (циклы for, while), не в конце итерации (цикл do), а в середине тела
цикла. В этом случае тело цикла может иметь такую структуру:
{ операторы
if (условие) break;
операторы}
Например, если начальные значения целых переменных i, j таковы, что i
< j, то следующий цикл определяет наименьшее целое, не меньшее их среднего
арифметического:
while (i < j)
{i++;
if (i == j) break;
j--;
}
Для i == 0, j == 3 результат i == j === 2 достигается при выходе из цикла с
26
помощью оператора break. Запись i == j == 2 не в тексте программы означает
равенство значений переменных i, j и константы 2. Для i == 0, j == 2 результат i
== j == 1 будет получен при естественном завершении цикла.
Оператор break практически незаменим в переключателях, когда с их
помощью надо организовать разветвление. Например, следующая программа
печатает название любой, но только одной, восьмеричной цифры:
//Оператор break в переключателе
#include <iostream.h>
void.main()
{ int ic;
cout « "\n Введите восьмеричную цифру: ";
cin » ic;
cout « "\n" « ic;
switch (ic)
{ case 0 Cout « " - нуль"; break ;
case 1 Cout « " - один"; break;
case 2 Cout « " - два"; break;
case 3 Cout « " - три"; break;
case 4 Cout « " - четыре"; break;
case 5 Cout « " - пять"; break;
case 6 Cout « " - шесть"; break;
case 7 Cout « " - семь"; break;
default Cout « " - это не восьмеричная цифра!'
}
cout « "\nКонец выполнения программы.
}
Программа напечатает название только одной введенной цифры и
прекратит работу. Если в ней удалить операторы break, то в переключателе
будут последовательно выполнены все операторы, начиная с помеченного
нужным (введенным) значением.
Циклы и переключатели могут быть многократно вложенными. Однако
следует помнить, что оператор break позволяет выйти только из самого
внутреннего цикла или переключателя. Например, в следующей программе,
которая в символьном массиве подсчитывает количество нулей (k0) и единиц
(kl), в цикл вложен переключатель:
//break при вложении переключателя в цикл
#include <iostream.h>
void main(void)
{ char c[l = "АВС100111";
int k0 = 0, kl = 0;
for (int i = 0; c[i] = ‘\0’; i++)
switch (c[i])
{ case ‘0’: k0++ ; break ;
27
case ‘1’: kl++; break;
default: break;
}
cout « "\nB строке " « kO « " нуля, " «
k1 « " единицы";
}
Результат выполнения программы:
В строке 2 нуля, 4 единицы.
Оператор break в данном примере передает управление из переключателя,
но не за пределы цикла. Цикл продолжается до естественного завершения. При
многократном вложении циклов и переключателей оператор break не может
вызвать передачу управления из самого внутреннего уровня непосредственно
на самый внешний. Например, при решении задачи поиска в матрице хотя бы
одного элемента с заданным значением удобнее всего пользоваться не
оператором break, а оператором безусловной передачи управления (goto):
for (int i = 0; i < n; i++)
for (int j = 0; j < и; j++)
{ if (A[i][j] ==» x) goto success;
// Действия при отсутствии элемента в матрице
…
} // Конец цикла success:
cout « "\nЭлемент х найден. Строка i = " « i;
cout « ", столбец j = " « j;
В качестве примера, когда при вложении циклов целесообразно
применение оператора break, рассмотрим задачу вычисления произведения
элементов строки матрицы. В данном случае вычисление произведения
элементов можно прервать, если один из сомножителей окажется равным 0.
Возможный вариант реализации может быть таким:
…
for (i = 0; i < n; i++) // Перебор строк матрицы
// Перебор элементов строки:
for (j = 0, p[i] =1; j < a; j++)
if (A[i][jl == 0.0) // Обнаружен нулевой элемент
{ p[i] = 0.0; break; }
else p[i] *= A[i][j];
…
При появлении в строке нулевого элемента оператор break прерывает
выполнение только внутреннего цикла, однако внешний цикл перебора строк
всегда выполняется для всех значений i от 0 до n-1.
Оператор continue употребляется только в операторах цикла. С его
помощью завершается текущая итерация и начинается проверка условия
дальнейшего продолжения цикла, т. е. условий начала следующей итерации.
28
Для объяснений действия оператора continue рекомендуется рассматривать
следующие три формы основных операторов цикла:
while (foo)
do
for (;foo;)
{…
{…
{…
contin:
contin:
contin:
}
} while (foo);
}
В каждой из форм многоточием обозначены операторы тела цикла. Вслед
за ними размещен пустой оператор с меткой contin. Если среди операторов тела
цикла есть оператор continue и он выполняется, то его действие эквивалентно
оператору безусловного перехода на метку contin.
Типичный пример использования оператора continue: подсчитать среднее
значение только положительных элементов одномерного массива:
for (s = 0.0, k = 0, i - 0; i < n; i++)
{ if (x[i] <= 0.0) continue;
k++;
// Количество положительных
s += x[i]; // Сумма положительных
}
if (k > О) э = s/k; // Среднее значение
Практические задания
1. Задать матрицу, найти необходимое число.
2. Задать строку из 0 и 1 и подсчитать количество 0 и 1.
3. Написать программу для выполнения следующих действий. При вводе
цифры от 0 до 9 будет выводиться ее название. Использовать оператор break.
4. Задать одномерный массив и посчитать среднее значение
положительных элементов.
5. Вычислить произведение элементов строки матрицы. При обнаружении
0 должно выдаваться сообщение «Обнаружен 0».
6. Вывести четные числа от 0 до 100 используя оператор continue.
7. Задать строку из произвольного количества цифр и посчитать
количество одинаковых введенных цифр.
8. Вывести числа, делящиеся на 3 в интервале от 0 до 100, используя
оператор continue.
9. Вывести числа от 0 до 100, удовлетворяющие условию х1=х^2, x2=x1^2
и т. д. используя оператор continue.
10. Вычислить произведение положительных элементов строки матрицы.
При обнаружении 0 должно выдаваться сообщение «Обнаружен 0».
11. Вычислить произведение отрицательных элементов строки матрицы.
При обнаружении 0 должно выдаваться сообщение «Обнаружен 0».
12. Написать программу для выполнения следующих действий. При вводе
в строку цифр буквы выдается сообщение «Это не цифра».
29
13. Вывести числа, делящиеся на К в интервале от 0 до 100, используя
оператор continue.
14. Вывести нечетные числа от 0 до 100, используя оператор continue.
15. Написать программу для выполнения следующих действий. При вводе
в строку цифры 0 выдается сообщение «Это 0, введите другую цифру». Ввод
продолжается.
16. Написать программу для выполнения следующих действий. При вводе
цифры от 0 до 9 будет выводиться ее название. Использовать оператор go to.
17. Задать строку из произвольного количества цифр и посчитать
количество четных и нечетных введенных цифр.
18. Написать программу для выполнения следующих действий. При вводе
в строку цифр нечетной цифры выдается сообщение «Это нечетная цифра,
введите другую цифру», ввод продолжается.
19. y(x)=(ln|e+x|-ax)/(bx-a), x=3.9(0.2)4.7.
20. y(x)=(sin(x^2)+ax)/(ex-b), x=2.9(0.3)4.1.
Лабораторная работа № 12
УКАЗАТЕЛИ И АДРЕСА ОБЪЕКТОВ
Цель: Изучить свойства указателей и методы адресации.
Общие сведения
Специальными объектами в программах на языках Си и Си++ являются
указатели. О них уже кратко говорилось, например, в связи с операциями new и
delete для динамического управления памятью. Различают указателипеременные (именно их мы будем называть указателями) и указателиконстанты. Значениями указателей служат адреса участков памяти, выделенных
для объектов конкретных типов. Именно поэтому в определении и описании
указателя всегда присутствует обозначение соответствующего ему типа. Эта
информация позволяет в последующем с помощью указателя получить доступ
ко всему сохраняемому объекту в целом.
Указатели делятся на две категории – указатели на объекты и указатели
на функции. Выделение этих двух категорий связано с отличиями в свойствах и
правилах использования. Например, указатели функций не допускают
применения к ним арифметических операций, а указатели объектов разрешено
использовать в некоторых арифметических выражениях. Начнем с указателей
объектов.
В простейшем случае определение и описание указателя-переменной на
некоторый объект имеют вид:
type *имя_указателя;
30
где type – обозначение типа; имя_указателя – это идентификатор; * –
унарная операция раскрытия ссылки (операция разыменования; операция
обращения по адресу; операция доступа по адресу), операндом которой должен
быть указатель (именно в соответствии с этим правилом вслед за ней следует
имя указателя).
Признаком указателя при лексическом разборе определения или описания
служит символ '*', помещенный перед именем.
Таким образом, при необходимости определить несколько указателей на
объекты одного и того же типа этот символ '*' помещают перед каждым
именем. Например, определение int *ilp, *i2p, *i3p, i; вводит три указателя на
объекты целого типа ilp, i2p, i3p и одну переменную i целого типа. Переменной
i будет отведено в памяти 2 байта (ТС++ или ВС++), а указатели ilp, i2p, i3p
разместятся в участках памяти, размер которых также зависит от реализации,
но которые только иногда имеют длину 2 байта.
В совокупности имя типа и символ '*' перед именем воспринимаются как
обозначение особого типа данных – "указатель на объект данного типа".
При определении указателя в большинстве случаев целесообразно
выполнить его инициализацию. Формат определения станет таким:
type *имя_указателя инициализатор;
Как упоминалось, инициализатор имеет две формы записи, поэтому
допустимы следующие две формы определения указателей:
type *имя_указателя = инициализирующее_выражение;
type *имя_указателя (инициализирующее_выражение);
В качестве инициализирующего выражения должно использоваться
константное выражение, частными случаями которого являются:
– явно заданный адрес участка памяти;
– указатель, уже имеющий значение;
– выражение, позволяющее получить адрес объекта с помощью
операции '&'.
Если значение константного выражения равно нулю, то это нулевое
значение преобразуется к пустому (иначе нулевому) указателю. Синтаксис
языка гарантирует, что этот указатель отличен от указателя на любой объект.
Кроме того, внутреннее (битовое) представление пустого указателя может
отличаться от битового представления целого значения 0. В компиляторах
ТС++ и ВС++ условное нулевое значение адреса, соответствующее значению
пустого указателя, имеет специальное обозначение NULL. Примеры
определений указателей:
char сс = 'd';
// Символьная переменная (типа char)
char *рс = &cс; // Инициализированный указатель на объект
// типа char
char *ptr(NULL); // Нулевой указатель на объект типа char
char *p;
// Неинициализированный указатель на
31
// объект типа char
Переменная инициализирована значением символьной константы 'd'.
После определения (с инициализацией) указателя рс доступ к значению
переменной возможен как с помощью имени, так и с помощью адреса,
являющегося значением указателя-переменной рс. В последнем случае должна
применяться операция разыменования '*' (получение значения через указатель).
Таким образом, при выполнении оператора
cout « "\n cc равно "« cc «" и *рс = "« *рс;
будет выведено:
cc равно d и *рс = d.
Указатели ptr и р, определенные в нашем примере, пользуются
различными "правами". Указатель ptr получил нулевое начальное значение
(пустой указатель), и попытка его разыменования будет бесперспективной.
Не нужно надеяться, что пустой указатель связан с участком памяти,
имеющим нулевой адрес или хранящим нулевое значение. Синтаксис языка
Си++ этого не гарантирует. Однако, присвоив затем ptr значение адреса уже
существующего объекта, можно осмысленно применять операцию
разыменования.
Например, любой из операторов присваивания ptr =&cc; или ptr =pc;
свяжет ptr с участком памяти, выделенным для переменной ее, т. е. после их
выполнения значением *ptr будет 'd'.
Присвоив указателю адрес конкретного участка памяти, можно с
помощью операции разыменования не только получать, но и изменять
содержимое этого участка памяти. Например, операторы присваивания *pc =
‘+’; или ptr = pс; *ptr = ‘+’; сделают значением переменной ее символ ' +'.
Унарное выражение *указатель обладает в некотором смысле правами имени
переменной, т. е. *рс и *ptr служат синонимами (псевдонимами, другими
именами) имени ее. Выражение *указатель может использоваться практически
везде, где допустимо использование имен объектов того типа, к которому
относится указатель. Однако это утверждение справедливо лишь в том случае,
если указатель инициализирован при определении явным способом. В нашем
примере не инициализирован указатель р. Поэтому попытки использовать
выражение *р в левой части оператора присваивания или в операторе ввода
неправомерны. Значение указателя р неизвестно, а результат занесения
значения в неопределенный участок памяти непредсказуем и иногда может
привести к аварийному событию.
*р = ‘%’; // Ошибочное применение неинициализированного р
Если присвоить указателю адрес конкретного объекта (р = *бес;) или
значение уже инициализированного указателя (р = рс;), то это превратит *р в
синоним (псевдоним) уже имеющегося имени объекта.
Чтобы связать неинициализированный указатель с новым участком
памяти, еще не занятым никаким объектом программы, используется оператор
32
new или присваивается указателю явный адрес:
р = new char;
// Выделили память для переменной типа char
// и связали указатель р с этим участком
// памяти
р = (char *)Oxb8OOOOOO; // Начальный адрес видеопамяти
// ПЭВМ для цветного дисплея
// в текстовом режиме
Обратите внимание на необходимость преобразования числового
значения к типу указателя (char *).
После любого из таких операторов можно использовать *р для записи в
память нужных символьных значений. Например, станут допустимы
операторы:
*р = '&';
или
cin » *р;
Числовое значение адреса может быть использовано не только во время
присваивания указателю значения в ходе выполнения программы, но и при
инициализации указателя при его определении. Нужно только не забывать о
необходимости явного преобразования типов. Например, в следующем
определении указатель с именем Computer при инициализации получает
значение адреса того байта, в котором содержатся сведения о типе компьютера,
на котором выполняется программа (справедливо только для ЭВМ,
совместимых с IBM PC):
char *Computer = (char *)OxFOOOFFFE;
Работая с указателями и применяя к ним операцию '*' (разыменования),
стоит употреблять словесное описание ее действия. Операцию разыменования
'*' вместе с указателем при их использовании в выражении можно объяснить
как получение значения, размещенного по адресу, равному значению указателя.
Если та же конструкция находится слева от знака операции присваивания или в
операторе ввода данных, то действие таково: разместить значение по адресу,
равному значению указателя.
В соответствии с соглашениями, принятыми в операционной системе MSDOS, байт основной памяти, имеющий шестнадцатеричный адрес
OxFOOOFFFE, может содержать следующие коды:
FF (ДЛЯ IBM PC);
FE (ДЛЯ IBM PC XT);
FD (для IBM PCjr);
FA (для IBM PC AT).
С помощью введенного выше указателя Computer несложно получить
доступ к содержимому этого байта идентификации типа ПЭВМ. Следующая
программа решает эту задачу:
33
//проверка типа компьютера (обращение к байту памяти)
#include <iostream.h>
void main(void)
{ char *Con>puter = (char *)OxFOOOFFFE;
cout « "\nПрограмма выполняется на ";
switch (*Computer)
{ case (char)OxFF: cout « "ПЭВМ типа IBM PC.";
break ;
case (char)OxFE: cout « "ПЭВМ типа IBM PC XT.";
break; case (char)OxFD: cout « "ПЭВМ типа IBM PCjr.";
break ;
case (char)OxFC: cout « "ПЭВМ типа IBM PC AT.";
break ;
default: cout « "ПЭВМ неизвестного типа ”;
}
}
Результат выполнения на ПЭВМ с процессором 80386 при использовании
модели памяти Large:
программа выполняется на ПЭВМ типа IBM PC AT.
В тексте программы обратите внимание на явные преобразования типов.
Во-первых, целочисленный шестнадцатеричный код адреса преобразуется к
типу char * определяемого указателя computer. Значением computer служит
величина типа char, поэтому в метках переключателя после case также должны
быть значения типа char. Явные преобразования типов (char) помещены перед
шестнадцатеричными кодами.
При определении указателя как сам он, так и его значение могут быть
объявлены константами. Для этого используется модификатор const:
type const * const имя_указателя инициализатор;
Модификаторы const – это необязательные элементы определения.
Ближайший к имени указателя модификатор const относится собственно к
указателю, а const перед символом '*' определяет "константность" начального
значения, связанного с указателем. Мнемоника очевидна, так как выражение
*имя_указателя есть обращение к содержимому соответствующего указателю
участка памяти. Таким образом, определение неизменяемого (константного)
указателя имеет следующий формат:
type * const имя_указателя инициализатор;
Для примера определим указатель-константу key_byte и свяжем его с
байтом, отображающим текущее состояние клавиатуры ПЭВМ IBM PC:
char * const key byte = (char *)1047; case break ;
case (char)OxFC: cout « "ПЭВМ типа IBM PC AT.";
break;
34
default: cout « "ПЭВМ неизвестного типа.
}}
Унарное выражение *указатель обладает в некотором смысле правами
имени переменной, т. е. *рс и *ptr служат синонимами (псевдонимами, другими
именами) имени ее. Выражение *указатель может использоваться практически
(char)OxFD: cout « "ПЭВМ типа IBM PCjr."; не допустит компилятор и выдаст
сообщение об ошибке:
Error...: Cannot modify a const object.
Формат определения указателя на константу:
type const * имя указателя инициализатор;
Например, введем указатель на константу целого типа со значением 0:
const int zero =0; // Определение константы
int const *point_to_const= &zero; // Указатель на константу 0
Операторы вида *point_to_const = 1; cin » *point to const; недопустимы, т.
к. каждый из них – это попытка изменить значение константы 0. Однако
операторы point_to_const = &CC; point to const = NULL; вполне допустимы.
Они разрывают связь указателя point_to_const с константой 0, однако не
меняют значения этой константы, т. е. не изменяют ее изображение в
фиксированном участке основной памяти.
Можно определить неизменяемый (постоянный) указатель на константу.
Например, иногда полезен так называемый определенный указатель-константа
на константное значение:
const float pi = 3.141593;
float const *const pointpi = π
Здесь невозможно изменить значение константы, обращаясь к ней с
помощью выражения *pointpi. Нельзя изменить и значение указателя pointpi, т.
е. он всегда "смотрит" на константу 3.141593.
Работая с указателями, постоянно используют операцию & – получение
адреса объекта. Для нее существуют естественные ограничения:
– Нельзя определять адрес неименованной константы, т. е. недопустимы
выражения &3.141593 или &'?';
– Нельзя определять адрес значения, получаемого при вычислении
скалярных выражений, т. е. недопустимы конструкции
&(44 * х - z) или &(а + b) != 12;
– Нельзя определить адрес переменной, относящейся к классу памяти
register. Следовательно, ошибочной будет последовательность
операторов:
int register Numb = 1;
int *prt Numb = &Numb;
Цитируя проект стандарта языка и обобщая сказанное, можно сделать
вывод, что операция & применима к объектам, имеющим имя и размещенным в
35
памяти. Ее нельзя применять к выражениям, неименованным константам,
битовым полям структур и объединений, к регистровым переменным и
внешним объектам (файлам).
Однако допустимо получать адрес именованной константы, т. е.
правомерна, например, такая последовательность определений:
const float Euler = 2.718282;
float *pEuler = (float *)&Euler;
Обратите внимание на необходимость явного приведения типов, так как
&Euler имеет тип const float *, а не float *.
Практические задания:
1. Присвоить указателю адрес переменной.
2. Определить количество байтов, извлекаемых из памяти при
выполнении операции разадресации.
3. Присвоить указателю адрес переменной типа float.
4. Создать указатель, который сможет указывать объект любого типа.
Присвоить указателю типы данных (float, int).
5. Присвоить один указатель другому.
6. Сравнить два указателя. Выполнить сравнение два раза, когда
указатели равны и когда указатели не равны.
7. Выбрать данные из памяти с помощью разных типов указателей (float,
char).
8. Записать в память данные и просмотреть содержимое памяти.
9. Вычислить контрольную сумму последовательности байтов,
ограниченной двумя указателями.
10. Увеличить указатель на размер адресуемой им структуры.
11. Преобразовать указатель одного типа в указатель другого.
12. Выбрать данные из памяти с помощью разных типов указателей (int,
long).
13. Уменьшить указатель на размер адресуемой им структуры.
14. Создать указатель, который сможет указывать объект любого типа.
Присвоить указателю типы данных (long, char).
15. Произвести вычитание указателей.
16. Сложить два указателя.
17. Изменить указатель на произвольную величину.
18. Отобразить адреса и длины указателей разных типов.
19. Спрограммировать цепочку из трех указателей.
20. Произвести вычитание адресов и указателей разных длин.
36
Лабораторная работа № 13
МАССИВЫ И УКАЗАТЕЛИ
Цель: Изучить способы доступа к произвольному элементу
массива с помощью индексированных переменных
(указателей).
Общие сведения
Теперь необходимо тщательно разобрать соотношение между массивами
и указателями.
Самое загадочное в массивах языков Си и Си++ – это их различное
"поведение" на этапах определения и использования.
При определении массива ему выделяется память так же, как и массивам
других алгоритмических языков (например, ПЛ/1 или Паскаль). Но как только
память для массива выделена, имя массива воспринимается как константный
указатель того типа, к которому отнесены элементы массива. Существуют
исключения, например, применение имени массива в операции sizeof. В этой
операции массив "вспоминает" о своем отличии от обычного указателя, и
результатом является размер в байтах участка памяти, выделенного не для
указателя, а для массива в целом. Исключением является и применение
операции & (получения адреса) к имени массива. Результат – адрес начального
(с нулевым индексом) элемента массива. В остальных случаях значением имени
массива является адрес первого элемента массива, и это значение невозможно
изменить. Таким образом, для любого массива соблюдается равенство:
имя массива == &имя_массива == &имя_массива[0]
Итак, массив – это один из структурированных типов языка Си++. От
других структурированных данных массив отличается тем, что все его
элементы имеют один и тот же тип и что элементы массива расположены в
памяти подряд. Определение одномерного массива типа type:
type имя массива[константное_выражение];
Здесь имя массива – идентификатор; константное_выражение, если оно
присутствует, определяет размер массива, т. е. количество элементов в массиве.
В некоторых случаях допустимо описание массива без указания количества его
элементов, т. е. без константного выражения в квадратных скобках.
Например: extern unsigned long UL[]; суть описание внешнего массива,
который определен в другой части программы, где ему выделена память и
(возможно) присвоены начальные значения его элементам.
При определении массива может выполняться его инициализация, т. е.
элементы массива получают конкретные значения. Инициализация
выполняется по умолчанию (без вмешательства программиста), если массив
37
статический или внешний. В этих случаях всем элементам массива компилятор
автоматически присваивает нулевые значения:
void f(void)
( static float F[4];
// Внутренний статический массив
long double A[10];
// Массив автоматической памяти
}
void main()
{ extern int D[] ;
//Описание массива
...
f();
...
int D[8];
// Внешний массив (определение)
Массивы D[8] и F[4] инициализированы нулевыми значениями. В
основной программе main () массив D описан без указания количества его
элементов. Массив А[10] не получает конкретных значений своих элементов
при определении.
Явная инициализация элементов массива разрешена только при его
определении и возможна двумя способами: либо с указанием размера массива в
квадратных скобках, либо без явного указания (без конкретного выражения) в
квадратных скобках:
char СН[] = { 'А', 'В', 'С', 'D'}; // Массив из 4 элементов
int IN[6] = { 10, 20, 30, 40 }; // Массив на 6 элементов
char STR[] = "ABCD";
// Массив из 5 элементов
Количество элементов массива СН компилятор определяет по числу
начальных значений в списке инициализации, помещенном в фигурных скобках
при определении массива. В массиве IN шесть элементов, но только первые
четыре из них явно получают начальные значения. Элементы IN[4], IN[S] либо
не определены, либо имеют нулевые значения, когда массив внешний или
статический. В массиве STR элемент STR[4] равен ' \о', а всего в этом массиве 5
элементов.
При отсутствии константного выражения в квадратных скобках список
начальных значений в определении массива обязателен. Если размер массива
явно задан, то количество элементов в списке начальных значений не должно
превышать размера массива. Ошибочные определения:
float А[]; // Ошибка в определении массива - нет размера
double В[4] = ( 1, 2, 3, 4, 5, 6 ); // Ошибка инициализации
В тех случаях, когда массив не определяется, а описывается, список
начальных значений задавать нельзя. В описании массива может отсутствовать
и его размер:
extern float E[]; // Правильное описание внешнего массива
Предполагается, что в месте определения массива Е для него выделена
38
память и выполнена инициализация. Описание массива (без указания размера и
без списка начальных значений) может использоваться в списке формальных
параметров определения функции и в спецификации параметров прототипа
функции. Примеры:
float MULTY(float G[], float F[]) // Определение функции MOLTY ( .. .
тело_функции
void print array(int I[]);
// Прототип функции print_array
Доступ к элементам массива с помощью индексированных переменных
мы уже несколько раз демонстрировали на примерах. Приведем еще один, но
предварительно обратим внимание на полезный прием, позволяющий
контролировать диапазон изменения индекса массива при его "просмотре",
например, в цикле. С помощью операции sizeof (имя_массива) можно
определить размер массива в байтах, т. е. размеры участка памяти, выделенного
для массива. Так как все элементы массива имеют одинаковые размеры, то
частное sizeof (имя_массива) / sizeof (имя_массива[0]) определяет количество
элементов в массиве. Следующий фрагмент программы печатает значения всех
элементов массива:
for (int i = 0; i < sizeof(m)/sizeof(m[0]); i++)
cout « "m[« i « "] = " « mi[i] « " ";
Результат на экране дисплея:
m[0] = 10 m[l] = 20 m[2] = 30 m[3] = 40
Еще раз отметим, что для первого элемента массива индекс равен 0. Цикл
завершается при достижении i значения 4.
По определению имя массива является указателем-константой, значением
которой служит адрес первого элемента массива (с индексом 0). Таким
образом, в нашем примере &m == m. Раз имя массива есть указатель, то к нему
применимы все правила адресной арифметики, связанной с указателями. Более
того, запись имя_массива[индекс] является выражением с двумя операндами.
Первый из них, т. е. имя массива, – это константный указатель (адрес начала
массива в основной памяти), индекс – это выражение целого типа,
определяющее смещение от начала массива.
Используя операцию обращения по адресу * (раскрытие ссылки,
разыменование), действие бинарной операции [] можно объяснить так:
*(имя_массива + индекс)
Таким образом, операндами для операции [] служат имя массива и
индекс. В языках Си и Си++ принято, что индексы массивов начинаются с
нуля, т.е. массив int z[3] из трех элементов включает индексированные
элементы z[0], z[l], z[2]. Это соглашение языка становится очевидным, если
учесть, что индекс определяет не номер элемента, а его смещение относительно
начала массива. Таким образом, *z – обращение к первому элементу z[0],*(z +
1) – обращение ко второму элементу z[l] и т.д. В следующей программе
39
показано, как можно не использовать квадратные скобки при работе с
элементами массива:
//работа с элементами массива без скобок []
#include <iostream.h>
void main()
{ char x[] " "DIXI";
// "Я сказал (высказался)"
int i = 0;
while (*(x + i) ! = '\0') cout « "\n" « * (х + i++);
Результат выполнения программы: слово "DIXI", написанное в столбик.
В данном примере оператор цикла с заголовком while выполняется, пока
верно выражение в скобках, т. е. пока очередной символ массива не равен '\о'.
Это же условие можно проверять и при таком заголовке цикла:
while (*(x + i))
В цикле при каждом вычислении выражения х + i++ используется
текущее значение i, которое затем увеличивается на 1. Тот же результат будет
получен, если для вывода в цикле поместить оператор cout « '\n' « x[i++];
(квадратные скобки играют роль бинарной операции, а операндами служат имя
массива х и индекс i++.). Индексированный элемент можно употребить и в
заголовке цикла:
while(x[i]).
Обращение к элементу массива в языке Си++ относят к постфиксному
выражению вида PE[IE]. Постфиксное выражение PE должно быть указателем
на нужный тип, выражение IE в квадратных скобках должно быть
целочисленного типа. Таким образом, если PE – указатель на массив, то pe[ie] –
индексированный элемент этого массива. Другой путь доступа к тому же
элементу массива * (ре + ie). Поскольку сложение коммутативно, то возможна
эквивалентная запись *(ie + ре) и, следовательно, ie[pe] именует тот же элемент
массива, что и ре [ie] .
Сказанное иллюстрирует следующая программа:
//коммутативность операции []
#include <iostream.h>
void main()
{int m[] = {10, 20, 30, 40 );
int j=1;
cout << “ \ nm[j] = ” << m[j];
cout << “ *(m + j++) = ” << (m + j++);
cout << “\n*(++j + m) = ” << * (++j + m);
cout << “ j[m] = ” << j[m]
cout << “\n*( j-- + m) = ” << *(j-- + m);
cout << “ j-- [m] = ” << j-- [m]
cout << “\n*( --j + m) = ” << *(--j + m);
cout << “--j [m] = ” *--j [m];
40
cout << “\n3[m] = ” << 3[m] << “ 2 [m]m = ” << 2[m] <<
“ 1[m] = “ << 1[m] << “ 0[m] = “ << 0[m];
}
Впечатляющий результат на экране:
m[j] = 20
*(m + j++)=20
*(++j + m) =40 j[m] = 40
*( j-- + m) = 40 j--[m] = 30
*( --j + m) = 10 --j[m] = 9
3[m] = 40 2[m] = 30 1[m] = 20 0[m] = 9
Обратите внимание на порядок вычислений. В выражении j—[m]
вычисляется j[m], а затем j--. В выражении –j[m] вычисляется j[m] и результат
уменьшается на 1, т. е. --(j[m]).
В некоторых не совсем обычных конструкциях можно использовать
постфиксное выражение PE[IE] с отрицательным значением индекса. В этом
случае PE должен указывать не на начало массива, т. е. не на его нулевой
элемент. Например, последовательность операторов:
char А[] = "СОН";
char *U = &A[2] ;
cout « "\n" « U[0] « U[-1] « U[-2];
приведет к выводу на экран слова НОС. Toт же результат будет получен при
использовании оператора cout « "\n" « *U « *U-- « *U--;
То же самое слово будет выведено на экран при таком использовании
вспомогательной переменной индекса:
int i = 2;
cout « "\n" « i[A] « i[A - 1] « i[A - 2];
Как видно из приведенных примеров, перемещение указателя от одного
элемента к другому выполняется в естественном порядке, т. е. при увеличении
индекса или указателя на 1 переходим к элементу с большим номером. Внутри
массива нет проблемы "обратного" размещения в памяти последовательно
определенных в программе объектов.
Так как имя массива есть не просто указатель, а указатель-константа, то
значение имени массива невозможно изменить. Попытка получить доступ ко
второму элементу массива int z[4] с помощью выражения *(++z) будет
ошибочной. А выражение *(z+1) вполне допустимо.
Следующая программа иллюстрирует естественный порядок размещения
в памяти элементов массива и обратный порядок расположения массивов,
последовательно определенных в программе.
//адреса массивов и использование указателей для доступа
#include <iostream.h>
void main(void)
{ int A[] = { 1, 2, 3, 4, 5, 6 );
int B[] = ( 1, 2, 3, 4, 5, 6 );
41
int *pA = А, *рВ = &В[5];
cout « "\nАдреса массивов: &А = "« &А «
" &В = " « &В « "\n";
while (*pA < *рВ)
cout « " *рА++ + *рВ-- = " « *рА++ + *рВ--;
cout « "\n Значения указателей после цикла:";
cout « "\n рА = " « рА « " рВ = " « рВ;
}
Результат выполнения программы:
Адреса массивов: &А = 0x8d8e0fec &B = 0x8d8e0fe0
*рА++ + *рВ-- = 7 *рА++ + рВ-- = 7 *рА++ + *рВ-- = 7
Значения указателей после цикла:
рА = 0x8d8e0ff2 рВ = 0x8d8e0fe4
Обратите внимание, что тот же результат будет получен, если определить
указатели таким образом: int *рА = &А[0], *рВ = (В + 5); .
Как видно по значениям адресов &А, &в, массивы А и В размещены в
памяти в обратном порядке по сравнению с их определением в программе.
Внутри массивов элементы размещены в естественном порядке.
Инициализация символьных массивов может быть выполнена не только с
помощью строк, но и с помощью списка инициализации, где последовательно
указаны значения каждого отдельного элемента:
char stroka[] = { 'S', ‘I', 'С', '\0' ); .
При такой инициализации списком в конце символьного массива можно
явно записать символ ' \0'. Только при этом одномерный массив (в данном
случае stroka) получает свойства строки, которую можно использовать,
например, в библиотечных функциях для работы со строками или при выводе
строки на экран дисплея с помощью оператора cout << stroka;.
Продолжая изучать массивы и указатели, рассмотрим конструкцию:
type *имя;
В зависимости от контекста она описывает или определяет различные
объекты типа type *. Если она размещена вне любой функции, то объект есть
внешний указатель, инициализированный по умолчанию нулевым значением.
Внутри функции это тоже указатель, но не имеющий определенного
значения. В обоих случаях его можно связать с массивом элементов типа type
несколькими способами как во время определения, так и в процессе
выполнения программы. В определениях существуют следующие возможности:
type *имя » имя_уже_определенного_массива_типа_tуре;
type *имя = new tуре[размер_массива];
type *имя = (type *)mаllос(размер * sizeof(type));
Например:
long arlong[] = ( 100, 200, 300, 400); // Определили массив
42
// Определили указатель,
// связали его с массивом
int *arint = new int[4];
// Определили указатель
// и выделили участок памяти float *arfloat =
new float[4];
// Определили указатель
// и выделили участок памяти
double *ardouble =
// Определили указатель и
(double *)malloc(4 * sizeof(double));// выделили участок
//памяти
В примерах определены четыре массива из четырех элементов в каждом.
Массив arlong инициализирован списком начальных значений в
фигурных скобках. Массив, связанный с указателем arfloat, с помощью
операции new получил участок памяти нужных размеров (16 байт), однако эта
память явно не инициализирована. Без инициализации остается и массив,
связанный с указателем arint. Память для элементов массива, связанного с
указателем ardouble, выделена с помощью библиотечной функции ша11ос()
языка Си. В ее параметре приходится указывать количество выделяемой памяти
(в байтах). Так как эта функция возвращает значение указателя типа void *, то
потребовалось явное преобразование типа (double *). Выделенная память явно
не инициализирована.
В отличие от имени массива указатель, связанный с массивом, никогда не
"вспоминает" об этом факте. Операция sizeof, применяемая к такому указателю,
вернет количество байтов, занятых именно этим указателем, а вовсе не размер
массива, связанного с указателем. Операция & указатель возвращает адрес
указателя в основной памяти, а никак не адрес начала массива, на который
настроен указатель. Таким образом, для наших примеров:
sizeof arint ==4 - длина указателя int *
sizeof *arint ==2- длина элемента arint [ 0 ]
sizeof arlong ==16
sizeof arlo ==4
Как и при обычном определении массивов типа char, указатели char*
могут инициализироваться с помощью символьных строк:
char *имя_указателя = "символьная строка";
char *имя_указателя = { "символьная строка" } ;
char *имя_указателя ("символьная строка");
В этом случае количество элементов в символьном массиве, связанном с
указателем, как обычно, на 1 больше, чем количество символов в
инициализирующей строке. Примеры определения массивов типа char:
char *car1 = "строка-1";
char *car2 = { "строка-2" );
char *car3("строка-3");
Длины массивов, связанных с указателями сar1, car2, саr3, одинаковы. В
long *arlo = arlong;
43
последнем элементе каждого из этих массивов находится символ ' \0’.
Операция sizeof, примененная к указателю на символьный массив,
возвращает длину не массива, а самого указателя, например, sizeof(carl) == 4.
Как и при обычном определении массивов, к элементам массивов,
связанных с указателями, существует несколько путей доступа. Принципиально
различных путей два: с помощью операции [] и с помощью операции
разыменования. В качестве иллюстрации приведем пример программы,
использующей оба способа доступа:
//Копирование массивов-строк
#include <iostream.h>
#include <string.h>
void main() // Для функции sir ten()
char * arch = "0123456789";// Массив из 11 элементов
int k = strlen(arch) + 1; // k - размер массива
char * newar = new char[k];
for (int i = 0; i < k;)
{ newar[i++] = *arch++;
if (!(i%3)) cout « "\narch = " « arch;
{
cout « "\nk = " « k « " newar =" « newar;
cout « "\nsizeof(arch) = " « sizeof(arch);
Результат выполнения программы:
arch = 3456789
arch = 6789
arch = 9
k = 11 newar = 0123456789
sizeof(arch) = 4
Для определения длины массива, не имеющего фиксированного имени,
нельзя использовать операцию sizeof. Поэтому в заголовке программы включен
файл string.h с прототипами функций для работы со строками. Одна из них, а
именно функция strlen(), определяющая длину строки-параметра, использована
для определения количества элементов в массиве, связанном с указателем arch.
Функция strlen () возвращает количество "значащих" символов в строке без
учета конечного нулевого символа.
Именно поэтому при определении значения k к результату strlen(arch)
прибавляется 1.
В программе определен и инициализирован символьный массив-строка,
связанный с указателем arch, и выделена память операцией new для такого же
по типу и размерам, но динамического и неинициализированного массива,
связанного с указателем newar. Длина каждого из массивов с учетом
"невидимого" в строке инициализации символа '\0' равна 11.
"Перебор" элементов массивов в программе выполняется по-разному.
Доступ к компонентам массива, связанного с указателем newar,
44
реализован с помощью операции [], к элементам второго массива – с помощью
разыменования *. У массива, связанного с указателем newar, изменяется
индекс. Указатель arch изменяется под действием операции ++. Такой
возможности не существует для обычных массивов.
В программе использована еще одна возможность вывода с помощью
операции « в стандартный поток cout – ему передается имя (указатель) массива,
содержащего строку, а на экран выводятся значения всех элементов массива в
естественном порядке, за исключением последнего символа ' \0'. При этом
необязательно, чтобы указатель адресовал начало массива. Указатель arch
"перемещается" по элементам массива, поэтому в цикле выводятся в поток cout
разные "отрезки" исходной строки. Чтобы сократить количество печати, в цикл
добавлен условный оператор, в котором проверяется значение модуля i%3.
Обратите внимание, что здесь выполнен вывод массива-строки. Если бы
указатель newar был связан не со строкой, а с массивом произвольного типа, то
вывод содержимого на экран дисплея с помощью cout « был бы невозможен.
Итак, в случае определения массива с использованием указателя этот
указатель является переменной и доступен изменениям. Такими свойствами
обладают arch и newar в нашей программе. Вот еще варианты циклов
копирования:
for (; *newar'='\0'; *newar++ = *arch++);
while (*newar++ = *arch++);
Результат будет тем же самым. Однако указатель newar в обоих случаях
сместится с начала массива, и его нельзя в дальнейшем использовать,
например, для печати строки.
При определении указателя ему может быть присвоено значение другого
указателя, уже связанного с массивом того же типа:
int pi1 [] = ( 1, 2, 3, 4 );
int *pi2 = pi1;
// pi2 - другое имя" для pi1
double pd1 [] = { 10, 20, 30, 40, 50 );
double *pd2 = pd1;
// pd2 - "другое имя" для pd1
После таких определений к элементам каждого из массивов возможен
доступ с помощью двух разных имен. Например:
cout « pi2[0]; // Выводится 1
*pil =0;
// Изменяется pi1[0]
cout « *pi2; // Выводится 0
cout « pd1[3]; // Выводится 40
*(pd2 + 3) = 77; // Изменяется pd2[3]
cout « pdl[3] // Выводится 77.
Такие же присваивания указателям допустимы и в процессе исполнения
программы, т. е. последовательность операторов int *pi3; pi3 = pil; свяжет еще
один указатель pi3 с тем же самым массивом int из четырех элементов.
Возможность доступа к элементам массива с помощью нескольких
45
указателей не следует путать с продемонстрированной в программе схемой
присваивания одному массиву значений элементов другого массива.
Рассмотрим такой пример:
char str[] = "массив”
// Определили массив с именем str
char *pstr = str;
// Определили указатель patr и
// "настроили" его на массив str
pstr = "строка";
// Изменили значение указателя,
// но никак не изменили массив str.
Присваивание указателю pstr не переписывает символьную строку
"строка" в массив str, вместо этого изменится значение самого указателя pstr.
Если при определении он указывал на начало массива с именем str, то после
присваивания его значением станет адрес того участка памяти, в котором
размещена строковая константа "строка". Чтобы в процессе выполнения
программы изменить значения элементов массива, необходимо, явно или
опосредованно (с помощью указателей или средств ввода данных), выполнить
присваивания. Например, заменить содержимое массива-строки str таким
дополнительным оператором while (atr++ = pstr++); или его аналог с
индексированными переменными for (int i = 0; str[i] = pstr[i]; i++);
При переписывании одного массива в другой длина заполняемого
массива должна быть не меньше длины копируемого массива, т. к. никаких
проверок предельных значений индексов язык Си++ не предусматривает, а
выход за границу индекса часто приводит к аварийной ситуации. В обоих
операторах учтено, что длины строк "массив" и "строка" одинаковы, а в конце
строки всегда размещается нулевой символ, по достижении которого цикл
завершается.
Примечание. Для копирования строк в стандартной библиотеке языков Си и Си++
имеется функция strcpy(), прототип которой находится в заголовочном файле string. h.
Возможно "настроить" на массив указатели других типов, однако при этом
потребуются явные приведения типов:
char *pch = (char *) pil;
float *pfl = (float *) pil;
Так, определенные указатели позволят по-другому "перебирать"
элементы массива. Выражения * (pch + 2) или рсh[2] обеспечивают доступ к
байту с младшим адресом элемента pi1[l]. Индексированный элемент pfl[l] и
выражение * (pfl + 1) соответствуют четырем байтам, входящим в элементы pil
[2], pil [3]. Например, присваивание значения индексированному элементу pfl[l]
изменит в общем случае как pil [2] = 3, так и pil [3] = 4. После выполнения
операторов pfl[l] = 1.0/3.0; cout « "\npil[2].« " « pil[2] « " pil[3] = " « Pil [3]; На
экране появится такой результат: pil[2] = -21845 pil[3] = 16042; что совсем не
похоже на исходные значения pil[2] = 3 pil[3] – 4;
Итак, допустимо присваивать указателю адрес начала массива. Однако
имя массива, являясь указателем, не обладает этим свойством, т. к. имя массива
есть указатель-константа. Рассмотрим пример:
46
long arl[] = { 10, 20, 30, 40 };
long *pl = new long[4];
Определены два массива по 16 байт каждый. Операторы присваивания
для имен этих массивов обладают разными правами:
arl = pi;
// Недопустимый оператор
pi = arl;
// Опасный оператор
Первый оператор недопустим, т. к. имя массива arl соответствует
указателю-константе. Второй оператор синтаксически верен, однако приводит к
опасным последствиям – участок памяти, выделенный операцией new long[4],
становится недоступным. Его нельзя теперь не только использовать, но и
освободить, т. к. в операции delete нужен адрес начала освобождаемой' памяти,
а его значение потеряно.
Мы неоднократно отмечали особую роль символьных строковых
констант в языках Си и Си++. В языке Си++ нет специального типа данных
"строка". Вместо этого каждая символьная строка в памяти ЭВМ
представляется в виде одномерного массива типа char, последним элементом
которого является символ '\0'. Изображение строковой константы
(последовательность символов, заключенная в двойные кавычки) может
использоваться по-разному. Если строка используется для инициализации
массива типа char, например, так: то адрес первого элемента строки становится
значением указателя-константы (имени массива) array.
Если строка используется для инициализации указателя типа char* : char
* pointer = "инициализирующая строка"; то адрес первого элемента строки
становится значением указателя-переменной (pointer).
И, наконец, если использовать строку в выражении, где разрешено
применять указатель, то используется адрес первого элемента строки:
char * string;
string = "строковый литерал";
В данном примере значением указателя string будет не вся строка
"строковый литерал", а только адрес ее первого элемента.
Практические задания:
1. Ввести строку латинских и русских букв вперемешку. Заменить в ней
все русские буквы латинскими.
2. Найти сумму положительных элементов массива J(10).
3. Ввести две символьные строки одинаковой длины. Сравнить их символ
за символом до первого отличного. Напечатать номер первого несравнившегося
символа.
4. Найти сумму отрицательных элементов массива J(10).
47
5. Ввести строку латинских и русских букв вперемешку. Заменить в ней
все русские буквы пробелами. Вывести полученную строку.
6. Ввести строку символов. Подсчитать, сколько раз в ней встречается
символ «А» .
7. Найти наибольший элемент вектора J(20).
8. Найти наименьший элемент вектора J(20).
9. Ввести строку символов. Сформировать строку вдвое большей длины,
где каждый символ предворяется пробелом. Вывести полученную строку.
10. Передать массив I(10) в массив J(10) в обратной последовательности.
11. Ввести строку символов. Сформировать строку вдвое большей длины,
где каждый символ повторяется дважды. Вывести полученную строку.
12. Передать в массив I(5) вторую половину массива J(10).
13. Ввести четное количество символов. Вывести вторую половину их в
обратной последовательности.
14. Передать массив I(5) в первую половину массива J(10) в обратной
последовательности.
15. Ввести четное количество символов. Поменять местами каждую пару
символов. Вывести новую строку.
16. Сформировать массив J(10) из массива I(10) по следующему
принципу: J(N)=I(N)+N.
17. Ввести четное количество символов. Раздвинуть строку в середине
десятью пробелами. Вывести новую строку.
18. Найти сумму четных элементов массива J(10).
19. Ввести строку латинских и русских букв вперемешку в одинаковом
количестве. Сформировать из нее отдельно две строки – латинских и русских
букв.
20. Сложить поэлементно два массива I(10) и J(10). Результат в массиве I.
48
Лабораторная работа № 14
СОСТАВНЫЕ
ОБЪЯВЛЕНИЯ,
ТИПЫ
ДАННЫХ.
ИНИЦИАЛИЗАЦИИ
И
ПРАВИЛА
РАБОТЫ
С
СОСТАВНЫМИ ТИПАМИ ДАННЫХ
Цель:
Изучить
основные
общие
правила
работы
с
составными типами данных.
Общие сведения
Массив – это совокупность однородных объектов, имеющая общее имя Iидентификатор массива. Другими словами, все элементы массива являются
объектами одного и того же типа. Это не всегда удобно. Пусть, например,
библиотечная (библиографическая) карточка каталога должна включать
сведения, которые приведены для книг в списке литературы, помещенном в
конце нашей книги. Таким образом, для каждой книги будет указываться
следующая информация:
• фамилия и инициалы автора;
• заглавие книги;
• место издания;
• издательство;
• год издания;
• количество страниц.
Если к библиографической карточке каталога нужно обращаться как к
единому целому, то воспользоваться массивом для представления всех ее
данных весьма сложно. Все данные имеют разные длины и разные типы.
Объединить такие разнородные данные удобно с помощью структуры. Каждая
структура включает в себя один или несколько объектов (переменных,
массивов, указателей, структур и т. д.), называемых элементами структуры.
Сведения о данных, входящих в библиографическую карточку, с помощью
структуры можно представить таким структурным типом:
struct card { char * author;
// Ф.И.О. автора книги
char * title;
// Заголовок книги
char *city;
// Место издания
char *finn;
// Издательство
int year;
// Год издания
int pages;
// Количество страниц.
Если структура определяется однократно, т. е. нет необходимости в разных
частях программы определять или описывать одинаковые по внутреннему
составу структурированные объекты, то можно не вводить именованный
49
структурный тип, а непосредственно определять структуры одновременно с
определением их компонентного состава. Следующий оператор определяет две
структуры с именами XX, YY, массив структур с именем ЕЕ и указатель pst на
структуру:
struct { char N[12]; int value; } XX, YY, EE[8], *pst;
В XX,YY и в каждый элемент массива EE[0],..., EE[7] входят в качестве
элементов массив char N[12] и целая переменная value. Имени у
соответствующего структурного типа нет. Для обращения к объектам,
входящим в качестве элементов в контурную структуру, чаще всего
используются уточненные имена. Общей формой уточненного имени элемента
структуры является следующая конструкция: имя структуры, имя элемента
структуры. Например, для определенной выше структуры YY оператор YY.
value =86;
присвоит переменной value значение 86.
Для ввода значения переменной value структуры EE[4] можно
использовать оператор cin » ЕЕ[4].value;
Точно так можно вывести в выходной поток cout значение переменной из
любой структуры. Другими словами, элемент структуры обладает правами
объекта того типа, который указан в конструкции (в определении) структурного
типа.
Например, для переменных с именем value из структур EE[0],..., EE[7],
XX, YY определен тип int. При определении структур возможна их
инициализация, т. е. задание начальных значений их элементов. Например,
введя структурный тип card, можно определить и инициализировать
конкретную структуру:
card dictionary = ( "Hornby A.S.", "Oxford students\
dictionary of Current English", "Oxford",
"Oxford University", 1984, 769);
Такое определение эквивалентно следующей последовательности
операторов:
card dictionary;
dictionary.author = "Hornby A.S.";
dictionary.title =
"Oxford students dictionary of Current English" ;
dictionary.city = "Oxford";
dictionary.firm = "Oxford University";
dictionary.year = 1984;
dictionary.pages = 769;
Нужно еще раз обратить внимание на отличие имени конкретной
структуры (в наших примерах dictionary, ree1, rac2, rec3, XX, YY, EE [0],..., EE
[7]) от имени структурного типа (в нашем случае card).
Как обычно, определяемый указатель может быть инициализирован.
Значением каждого указателя на структуру может быть адрес структуры того
50
же типа, т.е., грубо говоря, номер байта, начиная с которого структура
размещается в памяти.
Структурный тип задает ее размеры и тем самым определяет, на какую
величину (на сколько байтов) изменится значение указателя на структуру, если
к нему прибавить 1 (или из него вычесть 1).
Например, после наших определений структурного типа card и структуры
rec2 можно так записать определение указателя на структуру типа card: card
*ptrcard = &rec2;
Здесь определен указатель ptrcard и ему с помощью инициализации
присвоено значение адреса одной из конкретных структур типа card.
Практические задания:
Составить макет данных и написать программу для ввода и реализации печати
введенных данных для следующих документов:
1. График выполнения проекта
- -----------------------------------------------| № п/п | Ф.И.О. | Дата | % выполнения |
- -----------------------------------------------2. Список литературы
- ---------------------------------------------------------| № п/п | Уч. дисциплина | Литература | Автор |
- ---------------------------------------------------------3. Платежная ведомость за электроэнергию
------------------------------------------------------------------------------------------| Ф.И.О.| Расход электроэнергии | Стоимость | 1кВт/ч | Сумма к оплате |
------------------------------------------------------------------------------------------4. Зачетная ведомость
- ---------------------------------------------------------------------------| № п/п | Ф.И.О. | Отметка о зачете | Подпись преподавателя |
- ---------------------------------------------------------------------------5. Ведомость защиты лабораторных работ
- -------------------------------------------------------------------------------------------| № п/п | Группа | Ф.И.О. | № лаб. работы | Дата | Подпись преподавателя |
- -------------------------------------------------------------------------------------------6. График защиты дипломных проектов
- ---------------------------------------------------------| № п/п | Ф.И.О. | Тема проекта | № ГЭК | Дата |
51
- ---------------------------------------------------------7. Карта трудовой дисциплины ФЭТ
- ----------------------------------------------------------------|
| Количество пропущенных часов |
| Номер группы |---------------------------------------------|
|
| Всего | По н/у | Лекций |
8. График защиты курсовых работ
- -----------------------------------------------------------------| № п/п | Ф.И.О. | Тема проекта | Руководитель | Дата |
- -----------------------------------------------------------------9. График дежурства по вахте общежития
- -------------------------------------------| № п/п | № комнаты | Ф.И.О. | Дата |
- -------------------------------------------10. Платежная ведомость
- ------------------------------------------------------| № п/п | Ф.И.О. | Сумма к выдаче | Подпись |
- ------------------------------------------------------11. График отпусков
- --------------------------------------------------------------------------| № п/п | Ф.И.О. | Начало отпуска | Конец отпуска | Подпись |
- --------------------------------------------------------------------------12. Требования к материалам
- -----------------------------------------------------------------------| № п/п | Наименование материала | Кол-во | Дата выдачи |
- -----------------------------------------------------------------------13. График сдачи ГТО
- --------------------------------------------------| № п/п | Отдел | Вид спорта | Дата сдачи |
- --------------------------------------------------14. Список путевок на курортное лечение
- ----------------------------------------------------------------------------------------------| № п/п | Назв. курорта | Показания для лечения | Срок путевки | Стоимость |
- ----------------------------------------------------------------------------------------------15. План выпуска учебно-методической документации
52
-----------------------------------------------------------------------------------------------| № п/п | Наим. мероприятия | Объем в листах | Тираж в экземплярах | Дата |
-----------------------------------------------------------------------------------------------16. План мероприятий НИР
- -----------------------------------------------------------------------------| № п/п | Наим. мероприятия | Исполнитель | Срок исполнения |
- -----------------------------------------------------------------------------17. Численность работников отдела
- -------------------------------------------------------------------------| № п/п | Должность | План | Факт | % к предыдущему году |
- -------------------------------------------------------------------------18. Список абитуриентов, зачисленных в ХГТУ
- --------------------------------------------------------------------------------------------| № п/п | Факультет | Специальность | Общий балл | Категория зачисления |
- --------------------------------------------------------------------------------------------19. Расход материалов за сутки
- ----------------------------------------------------------------------------------------| № п/п | Наименование материала | Единица измерения | Расход | Дата |
- ----------------------------------------------------------------------------------------20. Список студентов групп
- ------------------------------------------------------------------------------------------| № п/п | Ф.И.О. | Домашний адрес | Общественное поручение | Возраст |
---------------------------------------------------------------------------------------------
Лабораторная работа № 15
ЛЕКСИЧЕСКИЕ ОСНОВЫ ЯЗЫКА С++
Цель: Получить общие сведения о лексемах, алфавите,
идентификаторах, служебных словах, константах и знаках
операций, используемых в языке С++.
Общие сведения
Основная программная единица на языке Си++ – это текстовый файл с
названием <имя>.срр, где срр – принятое расширение для программ на Си++, а
имя выбирается достаточно произвольно. Для удобства ссылок и сопоставления
программ с их внешними именами целесообразно помещать в начале текста
53
каждой программы строку комментария с именем файла, в котором она
находится. Текстовый файл с программой на Си++ вначале обрабатывает
препроцессор, который распознает команды (директивы) препроцессора
(каждая такая команда начинается с символа ‘#’) и выполняет их. В
приведенных выше программах использованы препроцессорные команды
#include <имя включаемого файла>
Выполняя препроцессорные директивы, препроцессор изменяет
исходный текст программы. Команда #include вставляет в программу заранее
подготовленные тексты из включаемых файлов. Сформированный таким
образом измененный текст программы поступает на компиляцию. Компилятор,
во-первых, выделяет из поступившего к нему текста программы лексические
элементы, т. е. лексемы, а затем на основе грамматики языка распознает
смысловые конструкции языка (выражения, определения, описания, операторы
и т. д.), построенные из этих лексем. Фазы работы компилятора здесь
рассматривать нет необходимости. Важно только отметить, что в результате
работы компилятора формируется объектный модуль программы.
Алфавит и лексемы языка СИ++.
В алфавит языка Си++ входят:
· прописные и строчные буквы латинского алфавита;
· цифры 0,1, 2, 3, 4, 5, 6, 7, 8, 9;
· специальные знаки:
“ { } , | [ ] ( ) + – / % | ; ‘ : ? < = > _ ! & # ~ ^ . *
Из символов алфавита формируются лексемы языка:
· идентификаторы;
· ключевые (служебные, или зарезервированные) слова;
· константы;
· знаки операций;
· разделители (знаки пунктуации).
Рассмотрим эти лексические элементы языка подробнее.
Прописные и строчные буквы различаются. Таким образом, в этом
примере два первых идентификатора различны. На длину различаемой части
идентификатора
конкретные
реализации
накладывают
ограничение.
Компиляторы фирмы Borland различают не более 32 первых символов любого
идентификатора. Некоторые реализации Си++ на ЭВМ типа VAX допускают
идентификаторы длиной до 8 символов.
Ключевые (служебные) слова – это идентификаторы, зарезервированные
в языке для специального использования.
Ключевые слова Си++:
Asm
Double
New
Switch
Auto
Else
Operator
Template
Break
Enua
Private
This
Case
Extern
Protected Throw
Catch
Float
Public
Try
Char
For
Register
Typedef
54
Class
Friend
Return
Typeid
Const
Goto
Short
Union
Continue If
Signed
Unsigned
Default
Inline
Sizeof
Virtual
Delete
Int
Static
Void
Do
Long
Struct
Volatile
While
Ранее в языке Си++ был зарезервирован в качестве ключевого слова
идентификатор overload. Для компиляторов фирмы Borland (ВС++ и ТС++)
дополнительно введены ключевые слова:
cdecl
_export
_loadds
saveregs
_cs
far
near
_seg
_ds
huge
pascal
_ss
_es
interrupt
_regparam
Там же введены как служебные слова регистровые переменные:
_АН _ВН _СН
_DH _SI _SP
_SS _AL _BI
_CL _DL _DI
_CS _ES _AX
_BX _CX _DX
_BP _DS _FLAGS
Отметим, что ранние версии ВС++ и ТС++ не включали в качестве
ключевых слов идентификаторы throw, try, typeid, catch.
Не все из перечисленных служебных слов сразу же необходимы
программисту, однако их запрещено использовать в качестве произвольно
выбираемых имен, и список служебных слов нужно иметь уже на начальном
этапе знакомства с языком Си++. Кроме того, идентификаторы, включающие
два подряд символа подчеркивания (_), резервируются для реализации Си++ и
стандартных библиотек. Идентификаторы, начинающиеся с символа
подчеркивания (_), используются в реализациях языка Си. В связи с этим
начинать
выбираемые
пользователем
идентификаторы
с
символа
подчеркивания и использовать в них два подряд символа подчеркивания не
рекомендуется.
Константа (литерал) – это лексема, представляющая изображение
фиксированного числового, строкового или символьного (литерного) значения.
Константы делятся на пять групп: целые, вещественные (с плавающей
точкой), перечислимые, символьные (литерные) и строковые (строки или
55
литерные строки). Перечислимые константы проект стандарта языка Си++
относит к одному из целочисленных типов.
Компилятор, выделив константу в качестве лексемы, относит её к той или
другой группе, а внутри группы – к тому или иному типу данных по ее
"внешнему виду" (по форме записи) в исходном тексте и по числовому
значению.
Целые константы могут быть десятичными, восьмеричными и
шестнадцатеричными.
Вещественные константы, т. е. константы с плавающей точкой, даже не
отличаясь от целых констант по значению, имеют другую форму внутреннего
представления в ЭВМ. Эта форма требует использования арифметики с
плавающей точкой при операциях с такими константами. Поэтому компилятор
должен уметь распознавать вещественные константы. Распознает он их по
внешним признакам. Константа с плавающей точкой может включать
следующие семь частей: целая часть (десятичная целая константа); десятичную
точку; дробную часть (десятичная целая константа); признак
(символ)
экспоненты е или в; показатель десятичной степени (десятичная целая
константа, возможно со знаком); суффикс F (или f) либо L (или l).
В записях вещественных констант могут опускаться целая или дробная
часть (но не одновременно), десятичная точка или признак экспоненты с
показателем степени (но не одновременно), суффикс.
Например: 66. .0 .12 3.14159F 1.12е-2 2E+6L 2.71
Перечисляемые константы (или константы перечисления, иначе
константы перечисляемого типа) вводятся с помощью служебного слова enum.
По существу это обычные целочисленные константы (типа int), которым
приписаны уникальные и удобные для использования обозначения. В качестве
обозначений выбираются произвольные идентификаторы, не совпадающие со
служебными словами и именами других объектов программы. Обозначения
присваиваются константам с помощью определения, например, такого вида:
enum { one=1, two=2, three=3 };
Символьные (литерные) константы – это один или два символа,
заключенные в апострофы. Односимвольные константы имеют стандартный
тип char. Для представления их значений могут вводиться переменные
символьного типа, т. е. типа char. Примеры констант: ‘z’, ‘*’, ‘\12’, ‘\0’, ‘\n’ –
односимвольные константы, ‘db’,‘\х07\х07’, ‘\n\t’ – двухсимвольные константы.
В этих примерах заслуживают внимания последовательности, начинающиеся со
знака ‘\’. Символ обратной косой черты ‘\’ используется, во-первых, при записи
кодов, не имеющих графического изображения, и, во-вторых, символов:
апостроф (‘), обратная косая черта (\), знак вопроса (?) и кавычки (“). Кроме
того, обратная косая черта позволяет вводить символьные константы, явно
задавая их коды в восьмеричном или шестнадцатеричном виде.
Последовательности литер, начинающиеся со знака ‘\’, называют эскейппоследовательностями. Знаки операций обеспечивают формирование и
56
последующее вычисление выражений. Выражение – есть правило для
получения значения. Один и тот же знак операции может употребляться в
различных выражениях и по-разному интерпретироваться в зависимости от
контекста. Для изображения операций в большинстве случаев используется
несколько символов. В ANSI-стандарте языка Си++ определены следующие
знаки операций:
[]
()
.
->
++ -&
*
+
~
sizeof
!
/
%
<< >> <
>
<= >= ++ !=
^
|
&& ||
?:
=
*=
/=
%= += –= <<= >>=
&= ^=
|=
,
#
##
Дополнительно к перечисленным в Си++ введены:
::
.*
–>* new delete
typeid
За исключением операций [], () и ?: все знаки операций распознаются
компилятором как отдельные лексемы. В зависимости от контекста одна и та
же лексема может обозначать разные операции. Например, бинарная операция
& – это поразрядная конъюнкция, а унарная операция & – это операция
получения адреса.
Практические задания
1. Проиллюстрировать влияние абсолютного значения константы и
использованных в ее изображении суффиксов L, U на тип данных, который и
присваивается на этапе компиляции.
2. Показать участки памяти, выделяемые для вещественных констант
разного типа.
3. Определить размер памяти в байтах, выделяемый для операнда.
4. Вывести слово с помощью символьных констант.
5. Вывести 3 строки с использованием эскейп-последовательностей.
6. Склеить несколько строк в одну.
7. Разместить
инициализацию.
строковые
константы
в
массиве,
8. Определить размер в байтах для разных типов данных.
используя
его
57
9. Проиллюстрировать возможности унарных операций.
10. Проиллюстрировать возможности аддитивных операций.
11. Проиллюстрировать возможности мультипликативных операций.
12. Проиллюстрировать возможности операций сдвига.
13. Проиллюстрировать возможности поразрядных операций.
14. Проиллюстрировать возможности операций отношения (сравнения).
15. Проиллюстрировать возможности логических бинарных операций.
16. Проиллюстрировать возможности операций присваивания.
17. Проиллюстрировать возможности операций выбора компонентов
структурного объекта.
18. Проиллюстрировать возможности условных операций.
19. Проиллюстрировать возможности операций явного преобразования
(приведения) типа.
20. Проиллюстрировать возможности операций new и delete для
динамического распределения памяти.
Лабораторная работа № 16
ФУНКЦИИ, УКАЗАТЕЛИ, ССЫЛКИ
Цель: Изучить определения, описания и вызовы функций,
указателей и ссылок на функции.
Общие сведения
Если в таких языках как Алгол, Фортран, ПЛ/1, Паскаль и др. делается
различие между программами, подпрограммами, процедурами, функциями, то в
языке Си++ и в его предшественнике – языке Си – используются только
функции.
При программировании на языке Си++ функция – это основное понятие,
без которого невозможно обойтись. Во-первых, каждая программа обязательно
58
должна включать единственную функцию с именем main (главная функция).
Именно функция main обеспечивает создание точки входа в
откомпилированную программу. Кроме функции с именем main, в программу
может входить произвольное количество неглавных функций, выполнение
которых инициируется прямо или опосредованно вызовами из функции main.
Всем именам функций программы по умолчанию присваивается класс памяти
extern, т. е. каждая функция имеет внешний тип компоновки и статическую
продолжительность существования. Как объект с классом памяти extern, каждая
функция глобальна, т. е. при определенных условиях доступна в модуле и даже
во всех модулях программы. Для доступности в модуле функция должна быть в
нем определена или описана до первого вызова.
Итак, каждая программа на языке Си++ – это совокупность функций,
каждая из которых должна быть определена или по крайней мере описана до ее
использования в конкретном модуле программы. В определении функции
указываются последовательность действий, выполняемых при ее вызове, имя
функции, тип функции (тип возвращаемого ею значения, т. е. тип результата) и
совокупность формальных параметров (аргументов). Каждый формальный
параметр не только перечисляется, но и специфицируется, т. е. для него
задается тип. Совокупность формальных параметров определяет сигнатуру
функции. Этот термин активно используется в связи с перегрузкой функций.
Сигнатура функции зависит от количества параметров, от их типов и от
порядка их размещения в спецификации формальных параметров.
В языках Си и Си++ допустимы функции, количество параметров у
которых при компиляции определения функции не определено. Кроме того,
могут быть неизвестными и типы параметров. Количество и типы параметров
становятся известными только в момент вызова функции, когда явно задан
список фактических параметров. При определении и описании таких функций,
имеющих списки параметров неопределенной длины, спецификация
формальных параметров заканчивается многоточием. Формат прототипа
функции с переменным списком параметров:
тип имя (спецификация явных параметров, ...);
Здесь тип – тип возвращаемого функцией значения; имя – имя функции;
спецификация_явных_параметров – список спецификаций отдельных
параметров, количество и типы которых фиксированы и известны в момент
компиляции. Эти параметры можно назвать обязательными. После списка
явных (обязательных) параметров ставится необязательная запятая, а затем
многоточие, извещающее компилятор, что дальнейший контроль соответствия
количества и типов параметров при обработке вызова функции проводить не
нужно. Сложность в том, что у переменного списка параметров нет даже
имени, поэтому непонятно, как найти его начало и где этот список
заканчивается.
Для доступа к списку параметров используется указатель pik типа int *.
Вначале ему присваивается адрес явно заданного параметра k, т. е. он
59
устанавливается на начало списка параметров в памяти (в стеке). Затем в цикле
указатель pik перемещается по адресам следующих фактических параметров,
соответствующих неявным формальным параметрам. С помощью
разыменования *pik выполняется выборка их значений. Параметром цикла
суммирования служит аргумент k, значение которого уменьшается на 1 после
каждой итерации и, наконец, становится нулевым. Особенность функции –
возможность работы только с целочисленными фактическими параметрами, т.
к. указатель pik после обработки значения очередного параметра
"перемещается вперед" на величину sizeof(int) и должен быть всегда установлен
на начало следующего параметра.
Следующий пример содержит функцию для вычисления произведения
переменного количества параметров. Признаком окончания списка
фактических параметров служит параметр с нулевым значением.
//индексация конца переменного списка параметров
#include <iostream.h>
// Функция вычисляет произведение параметров:
double prod(double arg, ...)
{ double aa = 1.0;
// Формируемое произведение
double *prt = farg;
// Настроили указатель
// на первый параметр
if (*prt =я 0.0) return 0.0;
for (; *prt; prt++) aa *= *prt; void main()
{ long minimum(char г, int k, ...); // Прототип функции
cout « "\n\tminimum('l', 3, 10L, 20L, 30L) = " «
minimum('1',3,10L,20L,30L);
cout «"\n\tminimum('i', 4, 11, 2, 3, 4) = " «
minimumCi',4,11,2,3,4) ;
cout « "\n\tminimum(4c1, 2, 0, 64) = " «
minimum('k',2,0,64);
}
// Функция с переменным списком параметров
long minimum(char z, int k, ...) 'i')
{ int *pi = bk + 1;
// Настроились на первый
// необязательный параметр
int min = *рi;
// Значение первого
// необязательного параметра
for(; k; k--, pi++)
min = min > *pi ? *pi : min;
return (long)min;
}
if (z == ‘l’)
{ long *pl - (long*) (tlc+1) ;
long min = *pl; // Значение первого параметра
for (;k; k--, pi++) Bin »« ain > *pl ? *pl : min;
60
return (long)min;
}
cout « "\пОшибка! Неверно задан 1-й параметр:";
return 2222L;
}
Результат выполнения программы:
minimum('1', 310L, 20L, 30L) = 10
minimum(‘i’, 4, 11, 2, 3, 4) = 2
Ошибка! Неверно задан 1-й параметр:
minimum('k',2,0,64) = 2222
Функция называется косвенно рекурсивной в том случае, если она
содержит обращение к другой функции, содержащей прямой или косвенный
вызов определяемой (первой) функции. В данном случае исходя из определения
рекурсии, используемая функция, по определению, рекурсивная иначе –
самовызываемая или самовызывающая: self-calling. Классический пример –
функция для вычисления факториала неотрицательного целого числа.
long fact(int k)
{ if (k < 0) return 0;
if (k == 0) return 1;
return k * fact(k-l); }
Для отрицательного аргумента результат по определению факториала не
существует. В этом случае функция возвратит нулевое значение. Для нулевого
параметра функция возвращает значение 1, т. к. по определению 0! равен 1. В
противном случае вызывается та же функция с уменьшенным на 1 значением
параметра и результат умножается на текущее значение параметра. Тем самым
для положительного значения параметра k организуется вычисление
произведения k * (k-l) * (k-2) *…*3*2*1*1.
Прежде чем вводить указатель на функцию, напомним, что каждая
функция характеризуется типом возвращаемого значения, именем и
сигнатурой. Напомним, что сигнатура определяется количеством, порядком
следования и типами параметров. Иногда говорят, что сигнатурой функции
называется список типов ее параметров. При использовании имени функции без
последующих скобок и параметров имя функции выступает в качестве
указателя на эту функцию, и его значением служит адрес размещения функции
в памяти. Это значение адреса может быть присвоено другому указателю, и
затем уже этот новый указатель можно применять для вызова функции. Однако
в определении нового указателя должен быть тот же тип, что и возвращаемое
функцией значение, и та же сигнатура. Указатель на функцию определяется
следующим образом:
тип функции (*имя указателя)(спецификация параметров);
Например: int (*func1Ptr)(char); – определение указателя funclptr на функцию с
параметром типа char, возвращающую значение типа int.
61
– постфиксное выражение.имя;
– постфиксное выражение->имя;
– постфиксное_выражение++;
Постфиксное выражение – прототип некой функции с именем fun и параметром
типа char, возвращающей значение указателя типа int *.
Второй пример: char * (*func2Ptr) (char *,int); – определение указателя
func2Ptr на функцию с параметрами типа указатель на char i и типа int,
возвращающую значение типа указатель на char.
В определении указателя на функцию тип возвращаемого значения и
сигнатура (типы, количество и последовательность параметров) должны
совпадать с соответствующими типами и сигнатурами тех функций, адреса
которых предполагается присваивать вводимому указателю при инициализации
или с помощью оператора присваивания.
В качестве простейшей иллюстрации сказанного приведем программу с
указателем на функцию.
//определение и использование указателей на функции
#include <iostream.h>
// Для ввода-вывода
void fl(void)
// Определение fl
{cout « "\nВыполняется f1()"; }
void f2(void)
// Определение f2
{ cout « "\пВыполняется f2()"; }
void main()
{ void (*ptr) (void) ;
// ptr – указатель на функции
ptr = f2;
// Присваивается адрес f2()
(*ptr)();
// Вызов f2() по ее адресу
ptr = fl;
// Присваивается адрес fl()
(*ptr)();
// Вызов fl0 no ее адресу
ptr();
// Вызов эквивалентен (*ptr)() ;
Результат выполнения программы:
Выполняется f2()
Выполняется f1()
Выполняется f1()
При присваивании указателей на функции также необходимо соблюдать
соответствие типов возвращаемых значений функций и сигнатур для
указателей правой и левой частей оператора присваивания. То же справедливо
и при последующем вызове функций с помощью указателей, т. е. типы и
количество фактических параметров, используемых при обращении к функции
по адресу, должны соответствовать формальным параметрам вызываемой
функции.
Массивы указателей на функции удобно использовать при разработке
всевозможных меню, точнее программ, управление которыми выполняется с
помощью меню. Для этого действия предлагаемые на выбор будущему
пользователю программы оформляются в виде функций, адреса которых
62
помещаются в массив указателей на функции. Пользователю предлагается
выбрать из меню нужный ему пункт (в простейшем случае он вводит номер
выбираемого пункта) и по номеру пункта, как по индексу, из массива
выбирается соответствующий адрес функции. Обращение к функции по этому
адресу обеспечивает выполнение требуемых действий. Самую общую схему
реализации такого подхода иллюстрирует следующая программа для обработки
файлов:
//массив указателей на функции
#include <stdlib.h>
// Для exit()
#include <iostream.h> // Для cout, cin
// Определение функций для
//обработки меню:
void act1(char* name)
{ cout « "Действия no созданию файла " « name; )
void act2(char* name)
( cout « "Действия по уничтожению файла " « name; )
void act3(char* name)
{cout « "Действия по чтению файла " « name; )
void act4(char* name)
{cout « "Действия по модификации файла " « name; }
void act5(char* name)
{ cout « "Действия по закрытию файла.";
exit(0);
// Завершить программу
}
// Тип MENU указателей на
//функции типа void (char *):
typedef void(*MENU)(char *) ;
// Инициализация таблицы
//адресов функций меню:
MENU MenuAct[5] - ( acti, act2, act3, act4, act5 );
void main()
{ int number;
// Номер выбранного пункта меню
char FileName[30];
// Строка для имени файла
cout « "\n 1 – создание файла"; cout « "\n 2 – уничтожение файла";
cout « "\n 3 – чтение файла";
cout « "\n 4 – модификация файла";
cout « "\n 5 – выход из программы";
while (1)
// Бесконечный цикл
{ while (I)
{
// Цикл продолжается до
//ввода правильного номера
cout « "\n\nВведите номер пункта меню: ";
cin » number;
if (number >= 1 && number <= 5) break;
63
cout « "\nОшибка в номере пункта меню!";
} if (number != 5)
{ cout « "Введите имя файла: ";
cin » FileName; // Читать имя файла
}
// Вызов функции по указателю на нее:
(*ManuAct[number-l])(FileName) ;
}// Конец бесконечного цикла
}
При выполнении программы возможен, например, такой диалог:
1 – создание файла;
2 – уничтожение файла;
3 – чтение файла;
4 – модификация файла;
5 – выход из программы.
Введите номер пункта меню: 3 <Enter>
Введите имя файла: PROBA.TXT <Enter>
Действия по чтению файла PROBA.TXT
...
Введите номер пункта меню: 5 <Enter>
Действия по закрытию файла.
В языке Си++ ссылка определена как другое имя уже существующего
объекта. Основные достоинства ссылок проявляются при работе с функциями,
однако ссылки могут использоваться и безотносительно к функциям. Для
определения ссылки используется символ &, если он употребляется в types
имя_ссылки инициализатор.
В соответствии с синтаксисом инициализатора, наличие которого
обязательно, определение ссылки может быть таким:
type С имя_ссылки » выражение; или
types_имя ссылки (выражение);
Раз ссылка есть другое имя уже существующего объекта, то в качестве
инициализирующего выражения должно выступать имеющее значение
леводопустимое выражение, т. е. имя некоторого объекта, имеющего место в
памяти.
Значением ссылки после определения с инициализацией становится адрес
этого объекта. Примеры определений ссылок:
int L = 777;
// Определена и инициализирована переменная L
int& RL = L;
// Значением ссылки RL является адрес
// переменной L
int&6 RI(0);
// Опасная инициализация – значением ссылки RI
// становится адрес об'ъекта, в котором
// временно размещено нулевое целое значение
64
В определении ссылки символ '&' не является частью типа, т. е. rl или ri
имеют тип int и именно так должны восприниматься в программе.
Практические задания
Решить задачу с помощью функций. Ввод исходных данных осуществить
в функции Main. Обмен информацей между Main и функциией, решающей
задачу, осуществлять через аппарат формальных и фактических параметров.
1.
Написать функцию нахождения МАХ среди массива целых чисел.
Найти МАХ в двух массивах разной длины.
2.
Написать функцию поэлементного сложения двух одномерных
массивов в третий. Сложить 2 массива, еще 2 массива другой длины в третий.
3.
Написать функцию, позволяющую в строке символов раздвинуть
пару символов пробелом. Обработать две строки символов разной длины.
4.
Написать функцию нахождения MIN среди двумерного массива
целых чисел. Обработать два массива разной длины.
5.
Написать функцию инвертирования строки символов.
Инвертировать две строки.
6.
Написать функцию нахождения суммы элементов двумерного
массива. Найти суммы двух массивов разной длины.
7.
Написать функцию перевода целого числа в двоичное. Обработать
три числа.
8.
Написать функцию перевода целого числа в восьмеричное.
Обработать 3 числа.
9.
Написать функцию перевода целого числа в шестнадцатеричное.
Обработать 3 числа.
10. Написать функцию замены всех русских символов в строке
пробелами.
Ввести и обработать две смешанные строки.
11. Написать функцию разделения русских и латинских букв в
отдельные строки. Ввести и обработать две смешанные строки.
65
12. Написать функцию объединения двух строк в одну (вторая
присваивается к концу первой). Объединить две строки, затем еще две.
13. Написать функцию, которая заменяет латинский символ его
порядковым номером в латинском алфавите. Обработать две строки символов.
14. Написать функцию обмена содержимым двух одномерных
массивов целых чисел. Обработать два массива, затем еще два другой длины
15. Написать функцию, которая заключает каждую пару символов в
круглые скобки. Обработать две строки.
16. Написать функцию выделения произвольной строки двумерного
массива в одномерный. Выделить две произвольные строки.
17. Написать функцию умножения произвольного столбца двумерного
массива на const. Умножить два столбца массива на разные константы.
18. Написать функцию повторения символа n раз. Ввести строку
символов. Повторить символ трижды, вывести новую строку.
19. Написать функцию, заменяющую
латинскими. Обработать две строки.
символы
русских
букв
Библиографический список
1. Подбельский В. В. Язык Си++. – М.: Финансы и статистика, 1995. –
560 с.
2. Романовская Л.М. Программирование в среде Си для ПЭВМ ЕС. – М.:
Финансы и статистика, 1992. – 352 с.
3. Информатика: Методические указания к лабораторным работам № 1–8
для студентов специальности 210100 «Управление и информатика в
технических системах» / Сост. И. В. Кочетова – Издательство
Хабаровского государственного технического университета, 1996. – 28
с.
66
Оглавление
Лабораторная работа № 9
Структура программы. Скалярные типы данных.
Выражения и присваивания ………………….…………………………………..3
Лабораторная работа № 10
Правила использования директив препроцессора ……………………………..8
Лабораторная работа № 11.Часть 1
Операторы языка С++: операторы деления…………………..………………..13
Лабораторная работа № 11. Часть 2
Операторы языка С++: операторы цикла……………………..………………..17
Лабораторная работа № 11. Часть 3
Операторы языка С++: операторы передачи управления..…..………………..23
Лабораторная работа № 12
Указатели и адреса объектов….…………………………………..…………….29
Лабораторная работа № 13
Массивы и указатели…………………………………………………………….36
Лабораторная работа № 14
Составные типы данных. Правила объявления, инициализации и
работы с составными типами данных.…………………………………………48
Лабораторная работа № 15
Лексические основы языка С++………..……………………….………………52
Лабораторная работа № 16
Функции, указатели, ссылки……………………………………………………57
Библиографический список .…………………………………………………...65
Download