Лекции по C++

Астраханский государственный технический университет

Кафедра «Информационных

технологий и коммуникаций»

Конспект лекций по дисциплине

«Основы алгоритмического языка С++»

для специальности 220200.

Астрахань 2000 г.

1. Переменные и операции языка С++ 4

ИЗУЧАЕМЫЕ ПОНЯТИЯ 5

Буквы и цифры 6

Пробельные символы 6

Знаки пунктуации и специальные символы 6

ESC- последовательности 8

Операции 9

Константы 11

Целые константы 11

Константы с плавающей точкой 13

Константа-символ 14

Строковые литералы 14

Идентификаторы 15

Ключевые слова 15

Комментарии 16

Лексемы 17

ИСХОДНЫЕ ТЕКСТЫ ПРИМЕРОВ 17

2. Конструкции принятия решений и циклы 23

ВОПРОСЫ И ОТВЕТЫ 23

СТРУКТУРА ПРОГРАММЫ 26

Исходная программа 26

ОБЪЯВЛЕНИЯ 27

Спецификаторы типов 28

Деклараторы 30

Деклараторы массивов, функций и указателей 30

Составные деклараторы 31

Об"явления переменной 33

Объявление простой переменной 34

Объявление перечисления 34

Объявления структур 36

Битовые поля 37

Об"явление совмещений 38

Об"явление массива 39

Об"явление функций 42

Классы памяти 45

Об"явления переменной на внешнем уровне 45

Об"явление переменной на внутреннем уровне 48

Об"явление функции на внешнем и внутреннем уровнях 49

Инициализация 50

Базовые типы и типы указателей 50

Составные типы 51

Строковые инициализаторы 53

Об"явления типов 54

Типы структур, совмещений и перечислений 54

Об"явления typedef 55

Имена типов 56

КОНТРОЛЬНЫЕ ВОПРОСЫ: 57

Функции 57

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

ТИПОВЫЕ ВОПРОСЫ С ОТВЕТАМИ 62

ПРАКТИКУМ 62

Контрольные вопросы 62

Массивы 65

СОРТИРОВКА массива - ПРИМЕР в файле list6_4cpp. 67

- ПОИСК в массиве 67

БИБЛИОТЕЧНЫЕ ФУНКЦИИ ПОИСКА и СОРТИРОВКИ в непрерывных массивах: 68

Строки и управление вводом/выводом 69

Форматированный потоковый вывод 70

Листинг 1. Исходный текст программы OUT1.CPP 70

Функция printf 71

Функция printf 72

Таблица 1. Еsс - последовательности 72

Таблица 7.2. Значения флагов строки формата функции printf 72

Таблица 3. Символы типов данных строки формата функции printf 73

Листинг 3. Исходный текст программы OUT2.CPP в файле List7-3.CPP 74

Таблица 4. Результат действия спецификаций форматирования в функции printf из строки 13 75

Ввод строк 76

Функция getline 76

Присвоение значений строкам 77

Инициализация строки 77

Функция strcpy 77

Функция strdup 77

Функция strncpy 78

Определение длины строки 78

Функция strlen 78

Функция strcat 78

Функция strncat 79

Сравнение строк 79

Функция strcmp 79

Пример 80

Функция stricmp 80

Пример 80

Функция strncmp 80

Пример 80

Пример 81

(см. List7_5.cpp - Исходный текст программы STRING2.CPP) 81

Преобразование строк 81

Функция strlwr 81

Пример 81

Функция strupr 81

Пример 81

Обращение строк 82

Функция strrev 82

Поиск символов 82

Функция strchr 82

Функция strrchr 82

Пример 83

Функция Strspn 83

Пример 83

Функция strcspn 83

Пример 83

Функция strpbrk 83

Пример 84

Поиск строк 84

Функция strstr 84

Пример 84

Функция strtok 84

Пример 84

Основы объектно-ориентированного программирования СИНТАКСИС ОСНОВНЫХ КОНСТРУКЦИЙ 85

Объявление базовых классов 85

Конструкторы 88

Деструкторы 90

Объявление иерархии классов 91

Виртуальные функции 92

Дружественные функции 95

Операции и дружественные операции 96

Виртуальные функции 97

Правило виртуальной функции 99

Операции и дружественные операции 101

ИСХОДНЫЕ ТЕКСТЫ ПРИМЕРОВ 103

ВОПРОСЫ И ОТВЕТЫ 103

Контрольные вопросы 104

ФАЙЛОВЫЕ ОПЕРАЦИИ ВВОДА/ВЫВОДА 105

Stream-библиотека C++ 105

ОБЩИЕ ФУНКЦИИ ПОТОКОВОГО ВВОДА/ВЫВОДА 106

Функция-компонент open 106

Функция-компонент close 107

ПОСЛЕДОВАТЕЛЬНЫЙ ТЕКСТОВЫЙ ПОТОК ВВОДА/ВЫВОДА 107

Функция-элемент getline 108

ПОСЛЕДОВАТЕЛЬНЫЙ ДВОИЧНЫЙ ФАЙЛОВЫЙ ВВОД/ВЫВОД 109

Функция-элемент write 110

Функция-элемент read 110

Файловый ввод/вывод с прямым доступом 113

Функция-элемент seekg 113

Заключение 115

Вопросы и ответы 115

Практикум 116

Контрольные вопросы 116

Упражнение 116

  1. Переменные и операции языка С++

Здесь представлены базовые компоненты программ на С++. В их число

входят типы данных, переменные, константы и выражения.

ИЗУЧАЕМЫЕ ПОНЯТИЯ

- Предопределенные типы данных в С++ включают в себя типы int, char,

float, double и void. В языке С++ гибкость типов данных увеличивается

благодаря применению модификаторов типов. Эти модификаторы изменяют

точность представления и диапазон значений переменных. Модификаторами

типа являются signed, unsigned, short и long.

- Идентификаторы в С++ могут иметь длину до 32 символов и

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

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

тификаторы С++ чувствительны к регистру. Ограничение на 32 символа

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

- Директива #include является специальной командой компилятора. Она

предписывает компилятору включить в программу содержимое опреде-

ленного файла, как если бы вы сами ввели его в текущий исходный

файл.

- Объявление констант предусматривает использование директивы #define

для объявления констант, определенных при помощи макросов, или ис-

пользование ключевого слова const для объявления формальных кон-

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

(значением по умолчанию является int), имени и ассоциированного с

ними значения.

- Объявление переменной требует, чтобы вы задали ее тип и имя, С++

дает вам возможность инициализировать переменную при ее объявлении.

Вы можете объявить несколько переменных в одном операторе объявле-

ния.

- Арифметическими операциями являются +, -, *, / и % (деление по

модулю).

- Арифметические выражения различаются по сложности. Самое простое

выражение содержит единственный элемент данных (литерал, константу

или переменную). Сложные выражения включают набор операций, функ-

ции, литералы, константы и переменные.

- Операции инкремента и декремента используются в префиксной и пост-

фиксной формах. Язык С++ дает вам возможность применять эти опе-

рации к переменным, в которых хранятся символы, целые числа и даже

числа с плавающей точкой.

- Арифметические операции присваивания дают вам возможность записы-

вать более короткие арифметические выражения, в которых первый опе-

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

- Оператор sizeof возвращает как для типов данных, так и для переменных

их размер в байтах.

- Механизм приведения типа дает вам возможность форсировать преобра-

зование типа выражения.

- Операции отношений и логические операции дают вам возможность стро-

ить логические выражения.

- Булевы выражения объединяют операции отношений и логические опе-

рации для формулирования нетривиальных условий. Эти выражения позволяют

программе принимать сложные решения.

- Условное выражение предлагает вам короткую форму для простого опе-

ратора if-else с двумя альтернативами.

- Операции манипулирования битами выполняют поразрядные операции

AND, OR, XOR и NOT. Кроме того, в С++ поддерживаются поразрядные

операции сдвига << и >>.

- Операции манипулирования битами с присваиванием предлагают корот-

кие формы для простых операций манипулирования битами.

Буквы и цифры

Множество символов Си включает большие и малые буквы из ан­глийского алфавита и 10 десятичных арабских цифр:

-большие английские буквы:

A B C D E F G H I J K L M N O P Q R T U V W X Y Z

-малые английские буквы:

a b c d e f g h i j k l m n o p q r t u v w x y z

-десятичные цифры:

0 1 2 3 4 5 6 7 8 9

Буквы и цифры используются при формировании констант, иден-

тификаторов и ключевых слов. Все эти конструкции описаны ниже. Компилятор Си рассматривает одну и ту же малую и большую

буквы как отличные символы. Если в данной записи использованы ма­лые буквы, то замена малой буквы "a" на большую букву "A" сделает отличной данную запись от предшествующей.

Пробельные символы

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

Символ CONTROL-Z рассматривается как индикатор конца файла. Компилятор игнорирует любой текст, следующий за символом

CONTROL-Z.

Компилятор Си игнорирует пробельные символы, если они не используются как разделители или как компоненты константы-символа или строковых литералов. Это нужно иметь в виду, чтобы дополни­тельно использовать пробельные символы для повышения наглядности программы (например,для просмотра редактором текстов).

Знаки пунктуации и специальные символы

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

-----------------------------------------------------------

Символ Наименование Символ Наименование

-----------------------------------------------------------

, Запятая ! Восклицатель-

ный знак

. Точка | Вертикальная

черта

; Точка с за- / Наклонная чер-

пятой та вправо

: Двоеточие \ Наклонная чер-

та влево

? Знак вопроса ~ Тильда

' Одиночная ка _ Подчеркивание

вычка

( Левая круглая # Знак номера

скобка

) Правая круглая % Знак процента

скобка

{ Левая фигурная & Амперсанд

скобка

} Правая фигурная ^ Caret

скобка

< Левая угловая - Знак минус

скобка

> Правая угловая = Знак равно

скобка

[ Левая квадратная + Знак плюс

скобка

] Правая квадратная

скобка -----------------------------------------------------------

Табл. 2.1. Знаки пунктуации и специальные символы

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

ESC- последовательности

ESC- последовательности- это специальные символьные комби­нации, которые представляют пробельные символы и неграфические символы в строках и символьных константах.

Их типичное использование связано со спецификацией таких действий, как возврат каретки и табуляция , а также для задания литеральных представлений символов, таких как символ двойная кавычка. ESC-последовательность состоит из наклон­ной черты влево, за которой следует буква, знаки пунктуации ' " \ или комбинация цифр. В таблице 2.2. приведен список ESC- последо­вательностей языка Си.

-------------------------------------------------

ESC- последовательность Наименование

-------------------------------------------------

\n Новая строка

\t Горизонтальная табу-

ляция

\v Вертикальная табуля-

ция

\b Пробел

\r Возврат каретки

\f Новая страница

\a Звонок(сигнал)

\' Одиночная кавычка

\" Двойная кавычка

\\ Наклонная черта влево

\ddd ASCII символ в восьми-

ричном представлении

\xdd ASCII символ в шестнад-

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

Табл. 2.2. ESC- последовательности

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

представляет символ "c" в литеральной строке или константе-симво­ле.

Последовательности \ddd и \xdd позволяют задать любой сим­вол в ASCII (Американский стандартный код информационного интер­фейса) как последовательность трех восьмеричных цифр или двух ше­стнадцатеричных цифр. Например, символ пробела может быть задан как \010 или \x08. Код ASCII "нуль" может быть задан как \0 или \x0 . В восьмеричной ESC- последовательности могут быть исполь­зованы от одной до трех восьмеричных цифр.

Например, символ пробела может быть задан как \10 . Точно так же в шестнадцатеричной ESC- последовательности могут быть ис­пользованы от одной до двух шестнадцатеричных цифр. Так, шестнад­цатеричная последовательность для символа пробела может быть за­дана как \x08 или \x8 .

Замечание:

Когда используется восьмеричная или шестнадцатеричная ESC­последовательность в строках, то нужно полностью задавать все цифры ESC- последовательности (три цифры для восьмеричной и две цифры для шестнадцатеричной ESC- последовательностей). Иначе, ес­ли символ непосредственно следующий за ESC- последовательностью, случайно окажется восьмеричной или шестнадцатеричной цифрой, то он проинтерпретируется как часть последовательности. Например, строка \x7Bell при выводе на печать будет выглядеть как {ell , поскольку \x7B проинтерпретируется как символ левой фигурной скобки({) . Строка \x07Bell будет правильным представлением сим-

вола "звонок" с последующим словом Bell.

ESC- последовательности позволяют посылать неграфические управляющие символы к внешним устройствам. Например, ESC- после­довательность\033 часто используется как первый символ команд уп­равления терминалом и принтером. Неграфические символы всегда должны представляться ESC-последовательностями, поскольку, непос­редственное использование в программах на Си неграфических симво­лов будет иметь непредсказуемый результат.

Наклонная черта влево (\) помимо определения ESC-последова­тельностей используется также, как символ продолжения строки в препроцессорных определениях.

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

Операции

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

В Табл. 2.3 представлен список операций. Операции должны использоваться точно так, как они представлены в таблице: без про­бельных символов между символами в тех операциях, которые предс­тавлены несколькими символами.

Операция sizeof не включена в эту таблицу. Она скорее пред­ставляет собой ключевое слово, чем символ.

-------------------------------------------------

Операция Наименование -------------------------------------------------

! Логическое НЕ

~ Побитовое дополнение

+ Сложение

- Вычитание, арифмети-

ческое отрицание

* Умножение

/ Деление

% Остаток

<< Сдвиг влево

>> Сдвиг вправо

< Меньше

<= Меньше или равно

> Больше

>= Больше или равно

== Равно

!= Не равно

& Побитовое И, адрес от

| Побитовое включающее ИЛИ

^ Побитовое исключающее ИЛИ

&& Логическое И

|| Логическое ИЛИ

' Последовательное выполне-

ние (запятая)

?: Операция условного вы-

ражения

++ Инкремент

-- Декремент

= Простое присваивание

+= Сложение с присваиванием

-= Вычитание с присваиванием

*= Умножение с присваиванием

/= Деление с присваиванием

%= Остаток с присваиванием

>>= Сдвиг вправо с присваива-

иванием

<<= Сдвиг влево с присваива-

нием

&= Побитовое И с присваива-

нием

|= Побитовое включающее ИЛИ

с присваиванием

^= Побитовое исключающее ИЛИ

с присваиванием

-------------------------------------------------------

Табл. 2.3. Операции

Замечание:

Операция условного выражения ?: -это тернарная, а не двух­символьная операция. Формат условного выражения следующий: <expression>?<expression>:<expression>

Константы

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

Целые константы

Целая константа- это десятичное, восьмеричное или шестнад­цатеричное число, которое представляет целую величину. Десятичная константа имеет следующий формат представления:

<digits>,

где <digits> - это одна или более десятичных цифр от 0 до 9.

Восьмеричная константа имеет следующий формат представле­ния:

0<odigits>,

где <odigits> - это одна или более восьмеричных цифр от 0 до 7. Запись ведущего нуля необходима.

Шестнадцатеричная константа имеет один из следующих форма­тов представления:

0x<hdigits>

0X<hdigits>,

где <hdigits> одна или более шестнадцатеричных цифр. Шестнадцатеричная цифра может быть цифрой от 0 до 9 или

буквой (большой или малой) от A до F. В представлении константы допускается "смесь" больших и малых букв. Запись ведущего нуля и следующего за ним символа x или X необходима.

Пробельные символы не допускаются между цифрами целой конс­танты. В Табл. 2.4 иллюстрируются примеры целых констант.

-----------------------------------------------------------

Десятичные Восьмеричные Шестнадцатеричные

константы константы константы

-----------------------------------------------------------

10 012 0xa или 0xA

132 0204 0x84

32179 076663 0x7dB3 или 0x7DB3

-----------------------------------------------------------

Табл. 2.4 Примеры констант

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

константы. Знак минус рассматривается как арифметическая опера­ция.

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

Восьмеричные и шестнадцатеричные константы в зависимости от размера могут быть типа int, unsigned int, long или unsigned long. Если константа может быть представлена как int, она специ­фицируется типом int. Если ее величина больше, чем максимальная положительная величина, которая может быть представлена типом int, но меньше величины, которая представляется в том же самом числе бит как и int, она задается типом unsigned int. Наконец, константа, величина которой больше чем максимальная величина, представляемая типом unsigned int, задется типом long или unsigned long, если это необходимо. В Табл. 2.5 показаны диапазо-

ны величин восьмеричных и шестнадцатеричных констант, представи­мых соответствующими типами на машине, где тип int имеет длину 16 бит.

-----------------------------------------------------------

Шестнадцатеричные Восьмеричные Тип

диапазоны диапазоны

-----------------------------------------------------------

0x0-0x7FFF 0-077777 int

0x8000-0xFFFF 0100000-0177777 unsigned int

0x10000-0x7FFFFFFF 0200000-017777777777 long

0x80000000-0xFFFFFFFF 020000000000-030000000000 unsigned long

-----------------------------------------------------------

Табл. 2.5 Диапазоны величин восьмеричных и

шестнадцатеричных констант

Важность рассмотренных выше правил состоит в том, что вось­меричные и шестнадцатеричные константы не содержат "знаковых" расширений, когда они преобразуются к более длинным типам (преоб­разование типов смотри в разделе 5 "Выражения и присваивания").

Программист может определить для любой целой константы тип

long, приписав букву "l" или "L" в конец константы. В Табл. 2.6 показаны примеры целых констант.

------------------------------------------------------------

Десятичные Восьмеричные Шестнадцатеричные

константы константы константы

------------------------------------------------------------

10L 012L 0xaL или 0xAL

79l 0115l 0x4fl или 0x4Fl

------------------------------------------------------------

Табл. 2.6 Примеры целых констант типа long

Константы с плавающей точкой

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

[<digits>][.<digits>][E[-]<digits>],

где <digits> - одна или более десятичных цифр (от 0 до 9),

а E или e -символ экспоненты. Целая или дробная части константы могут быть опушены, но не обе сразу. Десятичная точка может быть опущена только тогда, когда задана экспонента.

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

Пробельные символы не могут разделять цифры или символы

константы.

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

Примеры констант с плавающей точкой и константных выраже-

ний:

15.75

1.575E1

1575e-2

-0.0025

-2.5e-3

25e-4

Целая часть константы с плавающей точкой может быть опуще­на, например:

.75

.0075e2

-.125

-.175E-2

Все константы с плавающей точкой имеют тип double.

Константа-символ

Константа-символ- это буква, цифра, знак пунктуации или ESC- символ, заключенные в одиночные кавычки. Величина констан­ты-символа равна значению представляющего ее кода символа.

Константа-символ имеет следующую форму представления:

'<char>',

где <char> может быть любым символом иэ множества предста­вимых символов, включая любой ESC- символ, исключая одиночную ка­вычку ('), наклонную черту влево (\) и символ новой строки.

Чтобы использовать одиночную кавычку или наклонную черту влево в качестве константы-символа, необходимо вставить перед этими знаками наклонную черту влево. Чтобы представить символ но­вой строки, необходимо использовать запись '\n'.

----------------------------------------------

Константа Название величины

----------------------------------------------

'a' Малая буква а

'?' Знак вопроса

'\b' Знак пробела

'0x1B' ASCII ESC- символ

'\'' Одиночная кавычка

'\\' Наклонная черта влево

-------------------------------------------------

Табл. 2.7 Примеры констант-символов.

Константы-символы имеют тип int.

Строковые литералы

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

"<characters>" ,

где <characters> - это нуль или более символов из множества представимых символов, исключая двойную кавычку ("), наклонную черту влево (\) и символ новой строки. Чтобы использовать символ новой строки в строковом литерале, необходимо напечатать наклон­ную черту влево, а затем символ новой строки.

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

"Long strings can be bro\

cken into two pieces."

идентичен строке:

"Long strings can be brocken into two pieces."

Чтобы использовать двойные кавычки или наклонную черту вле­во внутри строкового литерала, нужно представить их с предшеству­ющей наклонной чертой влево, как показано в следующем примере:

"This is a string literal"

"First \\ Second"

"\"Yes, I do,\" she said."

"The following line shows a null string:"

""

Заметим, что ESC- символы (такие как \\ и \") могут появ­ляться в строковых литералах. Каждый ESC- символ считается одним отдельным символом.

Символы строки запоминаются в отдельных байтах памяти. Сим­вол null (\0) является отметкой конца строки. Каждая строка в программе рассматривается как отдельный об"ект. Если в программе содержатся две идентичные строки, то каждая из них будет хра­ниться в отдельном месте памяти.

Строчные литералы имеют тип char[]. Под этим подразумевает­ся, что строка- это массив, элементы которого имеют тип char. Число элементов в массиве равно числу символов в строчном литера­ле плюс один, поскольку символ null (отметка конца строки) тоже считается элементом массива.

Идентификаторы

Идентификаторы- это имена переменных, функций и меток, ис­пользуемых в программе. Идентификатор создается об"явлением соот-

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

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

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

Примеры идентификаторов:

temp1

toofpage

skip12

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

add

ADD

Add

aDD

Компилятор Си не допускает идентификаторов, которые имеют ту же самую орфографию, что и ключевые слова. Ключевые слова опи­саны в следующем раздела

Замечание:

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

Ключевые слова

Ключевые слова- это предопределенные идентификаторы, кото­рые имеют специальное значение для компилятора Си. Их можно ис­пользовать только так как они определены. Имена об"ектов програм­мы не могут совпадать с названиями ключевых слов.

Список ключевых слов:

auto double int struct

break else long switch

case enum register typedef

char extern return union

const float short unsigned

continue for signed void

default goto sizeof while

do if static volatile

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

Ключевые слова const и volatile зарезервированы для буду­щего использования.

Следующие идентификаторы могут быть ключевыми словами для некоторых приложений:

cdecl

far

fortran

huge

near

pascal

Комментарии

Комментарий- это последовательность символов, которая восп­ринимается компилятором как отдельный пробельный символ или, дру­гими словами, игнорируется.

Комментарий имеет следующую форму представления:

/*<characters>*/,

где <characters> может быть любой комбинацией символов из множества представимых символов, включая символы новой строки, но исключая комбинацию */. Это означает, что комментарии могут зани­мать более одной строки, но не могут быть вложенными.

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

Следующие примеры иллюстрируют некоторые комментарии:

/* Comments can separate and document

lines of a program. */

/* Comments can contain keywords such as for

and while */

/*******************************************

Comments can occupy several lines. *******************************************/

Так как комментарии не могут содержать вложенных коммента­риев, то следующий пример будет ошибочным:

/* You cannot/* nest */ comments */

Компилятор распознает первую комбинацию */ после слова nest как конец комментария. Затем, компилятор попытается обрабатывать оставшийся текст и выработает сообщение об ошибке. Чтобы обойти

компиляцию комментариев больших размеров, нужно использовать ди­рективу #if препроцессора.

Лексемы

Когда компилятор обрабатывает программу, он разбивает прог­рамму на группы символов, называемых лексемами. Лексема- это еди­ница текста программы, которая имеет определенный смысл для ком­пилятора и которая не может быть разбита в дальнейшем. Операции, константы, идентификаторы и ключевые слова, описанные в этом раз­деле,являются примерами лексем. Знаки пунктуации, такие как квад­ратные скобки ([]), фигурные скобки ({}), угловые скобки (<>), круглые скобки и запятые, также являются лексемами. Границы лек­сем определяются пробельными символами и другими лексемами, таки­ми как операции и знаки пунктуации. Чтобы предупредить неправиль­ную работу компилятора, запрещаются пробельные символы между сим­волами идентификаторов, операциями, состоящими из нескольких сим­волов и символами ключевых слов.

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

Например, рассмотрим следующее выражение:

i+++j

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

ИСХОДНЫЕ ТЕКСТЫ ПРИМЕРОВ

// Программа VAR.CPP, иллюстрирующая простые переменные

#include <iostream.h>

int main()

{

int i, j = 2;

double x, y = 355.0 / 113;

i = 3 * j;

cout << "i = " << i << endl

<< "j = " << j << endl;

x = 2 * y;

x = x * x;

cout << "y = " << y << endl

<< "x = " << x << endl;

return 0;

}

/*

Результаты:

i = 6

j = 2

y = 3.141593

x = 39.4784

*/

// Программа CONST1.CPP, иллюстрирующая константы

#include <iostream.h>

#define SEC_IN_MIN 60

#define MIN_IN_HOUR 60

int main()

{

long hours, minutes, seconds;

long totalSec;

cout << "Введите часы: ";

cin >> hours;

cout << "Введите минуты: ";

cin >> minutes;

cout << "Введите секунды: ";

cin >> seconds;

totalSec = ((hours * MIN_IN_HOUR + minutes) *

SEC_IN_MIN) + seconds;

cout << endl << totalSec << " секунд прошло с полуночи" << endl;

return 0;

}

/* Тест и результаты:

Введите часы: 10

Введите минуты: 0

Введите секунды: 0

36000 секунд прошло сполуночи

*/

// Программа CONST2.CPP, иллюстрирующая формальные константы

#include <iostream.h>

const int SEC_IN_MIN = 60; // глобальная константа

int main()

{

const int MIN_IN_HOUR = 60; // локальная константа

long hours, minutes, seconds;

long totalSec;

cout << "Введите часы: ";

cin >> hours;

cout << "Введите минуты: ";

cin >> minutes;

cout << "Введите секунды: ";

cin >> seconds;

totalSec = ((hours * MIN_IN_HOUR + minutes) *

SEC_IN_MIN) + seconds;

cout << endl << endl << totalSec << " секунд прошло с полуночи" << endl;

return 0;

}

/* Тест и результаты:

Введите часы: 1

Введите минуты: 10

Введите секунды: 20

4220 секунд прошло с полуночи

*/

// Программа OPER1.CPP, иллюстрирующая простые математические операции

#include <iostream.h>

int main()

{

int int1, int2;

long long1, long2, long3, long4, long5;

float x, y, real1, real2, real3, real4;

cout << endl << "Введите первое целое число: ";

cin >> int1;

cout << "Введите второе целое число: ";

cin >> int2;

cout << endl;

long1 = int1 + int2;

long2 = int1 - int2;

long3 = int1 * int2;

long4 = int1 / int2;

long5 = int1 % int2;

cout << int1 << " + " << int2 << " = " << long1 << endl;

cout << int1 << " - " << int2 << " = " << long2 << endl;

cout << int1 << " * " << int2 << " = " << long3 << endl;

cout << int1 << " / " << int2 << " = " << long4 << endl;

cout << int1 << " % " << int2 << " = " << long5 << endl;

cout << endl << endl;

cout << "Веедите первое вещественное число: ";

cin >> x;

cout << "Введите второе вещественное число: ";

cin >> y;

cout << endl;

real1 = x + y;

real2 = x - y;

real3 = x * y;

real4 = x / y;

cout << x << " + " << y << " = " << real1 << endl;

cout << x << " - " << y << " = " << real2 << endl;

cout << x << " * " << y << " = " << real3 << endl;

cout << x << " / " << y << " = " << real4 << endl;

cout << endl << endl;

return 0;

}

/* Тест и результаты:

Введите первое целое число: 10

Введите второе целое число: 5

10 + 5 = 15

10 - 5 = 5

10 * 5 = 50

10 / 5 = 2

10 % 5 = 0

Введите первое вещественное число: 1.25

Введите второе вещественное число: 2.58

1.25 + 2.58 = 3.83

1.25 - 2.58 = -1.33

1.25 * 2.58 = 3.225

1.25 / 2.58 = 0.484496

*/

//Демонстрация операций инкремента и декремента см. в программе OPER2.CPP

// Программа SIZEOF.CPP, которая возвращает размеры данных, используя

// для этого операцию sizeof() с переменными и типами данных.

#include <iostream.h>

int main()

{

short int aShort;

int anInt;

long aLong;

char aChar;

float aReal;

cout << "Таблица 1. Размеры памяти для переменных" << endl

<< endl;

cout << " Тип данных Используемая " << endl;

cout << " память (в байтах)" << endl;

cout << "------------------ -----------" << endl;

cout << " short int " << sizeof(aShort) << endl;

cout << " integer " << sizeof(anInt) << endl;

cout << " long integer " << sizeof(aLong) << endl;

cout << " character " << sizeof(aChar) << endl;

cout << " float " << sizeof(aReal) << endl;

cout << endl << endl << endl;

cout << "Таблица 2. Размеры памяти для типов данных" << endl

<< endl;

cout << " Тип данных Используемая" << endl;

cout << " память (в байтах)" << endl;

cout << "------------------ -----------" << endl;

cout << " short int " << sizeof(short int) << endl;

cout << " integer " << sizeof(int) << endl;

cout << " long integer " << sizeof(long) << endl;

cout << " character " << sizeof(char) << endl;

cout << " float " << sizeof(float) << endl;

cout << endl << endl << endl;

return 0;

}

/* Результаты:

Таблица 1. Размеры памяти для переменных"

Тип данных Используемая

память (в байтах)

------------------ -----------

short int 2

integer 2

long integer 4

character 1

float 4

Таблица 2. Размеры памяти для типов данных

Тип данных Используемая

память (в байтах)

------------------ -----------

short int 2

integer 2

long integer 4

character 1

float 4

*/

// Простая программа TYPECAST.CPP, демонстрирующая приведение типа

#include <iostream.h>

int main()

{

short shortInt1, shortInt2;

unsigned short aByte;

int anInt;

long aLong;

char aChar;

float aReal;

// присваиваются значения

shortInt1 = 10;

shortInt2 = 6;

// действия выполняются без приведения типа

aByte = shortInt1 + shortInt2;

anInt = shortInt1 - shortInt2;

aLong = shortInt1 * shortInt2;

aChar = aLong + 5; // автоматическое преобразование

// в символьный тип

aReal = shortInt1 * shortInt2 + 0.5;

cout << "shortInt1 = " << shortInt1 << endl

<< "shortInt2 = " << shortInt2 << endl

<< "aByte = " << aByte << endl

<< "anInt = " << anInt << endl

<< "aLong = " << aLong << endl

<< "aChar is " << aChar << endl

<< "aReal = " << aReal << endl << endl << endl;

// дейтсвия выполняются с приведением типа

aByte = (unsigned short) (shortInt1 + shortInt2);

anInt = (int) (shortInt1 - shortInt2);

aLong = (long) (shortInt1 * shortInt2);

aChar = (unsigned char) (aLong + 5);

aReal = (float) (shortInt1 * shortInt2 + 0.5);

cout << "shortInt1 = " << shortInt1 << endl

<< "shortInt2 = " << shortInt2 << endl

<< "aByte = " << aByte << endl

<< "anInt = " << anInt << endl

<< "aLong = " << aLong << endl

<< "aChar is " << aChar << endl

<< "aReal = " << aReal << endl << endl << endl;

return 0;

}

/* Результаты:

shortInt1 = 10

shortInt2 = 6

aByte = 16

anInt = 4

aLong = 60

aChar is A

aReal = 60.5

shortInt1 = 10

shortInt2 = 6

aByte = 16

anInt = 4

aLong = 60

aChar is A

aReal = 60.5

*/

/* *** ВОПРОСЫ И ОТВЕТЫ ***

Существуют ли особые соглашения о присвоении имен идентификаторам?

Существует несколько стилей, которые стали популярными в последние

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

имя переменной с символа, набранного в нижнем регистре. Если идентифи-

катор состоит из нескольких слов, как, например, numberOfElements,

набирайте первый символ каждого последующего слова в верхнем реги-

стре.

Как реагирует компилятор, если вы объявляете переменную, но никогда не

присваиваете ей значения?

Компилятор выдает предупреждение, что на переменную нет ссылок.

Каково булево выражение для проверки того, что значение переменной i

находится в заданном диапазоне значений (например, определяемом пере-

менными lowVal и hiVal)?

Выражением, которое определяет, находится ли значение переменной i

в некотором диапазоне, является (i >= lowVal && i <= hiVal).

  1. Конструкции принятия решений и циклы

ВОПРОСЫ И ОТВЕТЫ

Предъявляет ли С++ какие-либо требования на отступ операторов в предложениях оператора?

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

Вот пример оператора if с записью предложений без отступа:

if ( i > 0 )

j = i * 1;

else

j = 10 - i;

Сравните этот листинг и его вариант с отступами

if ( i > 0 )

j = i * i;

else

j = 10 - i;

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

Каковы правила написания условий в операторе if-else?

Здесь существуют два подхода. Первый рекомендует писать условия так, что true будет чаще, чем false. Второй подход рекомендует избегать отрицательных выражений (тех, которые используют операции сравнения != и булевы операции !).

Программисты из последнего лагеря преобразуют такой оператор if:

if ( i != 0 )

j = 100/i;

else

j = 1;

в следующую эквивалентную форму:

if ( i == 0 )

j = 1;

else

j = 100/i;

хотя вероятность равенства нулю переменной i достаточно низка.

Как обработать условие, подобное нижеследующему, где имеется деление на переменную, которая может оказаться равной нулю?

if ( i != 0 && 1/i > 1 )

j = i * i;

С++ не всегда оценивает проверяемые условия полностью. Эта частичная оценка происходит, когда член булева выражения превращает все выражение в false или true, независимо от значения других членов. В этом случае, если переменная i равна 0, исполняющая система не будет оценивать 1/i > 1, потому что член i != 0 есть false и обращает в false все выражение, независимо от значения второго члена. Это называется укороченной оценкой булевых выражений.

Действительно ли необходимо включать предложения else или default в многоальтернативные операторы if-else и switch?

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

Как смоделировать цикл while циклом for?

Рассмотрим простой пример.

int i; int i = 1;

for (i=1; i<=10; i+=2) { while ( i <= 10) {

cout << i << endl; cout << i << endl;

} i += 2;

}

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

Как смоделировать цикл while циклом do-while?

Рассмотрим простой пример.

i = 1; i = 1;

do { while (i <= 10) {

cout << i << endl; cout << i << endl;

i += 2; i += 2;

} while (i <= 10); }

Оба цикла имеют одинаковые условия в предложениях while.

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

Как открытый цикл for может эмулировать циклы while и do-while?

Открытый цикл for эмулирует другие циклы С++ установкой оператора if выхода из цикла в начале или конце цикла. Рассмотрим пример эмуляции цикла while открытым циклом for:

i = 1; i = 1;

while (i <= 10) { for (;;) {

if (i > 10) break;

cout << i << endl; cout << i << endl;

i += 2; i += 2;

} }

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

Рассмотрим простой пример, иллюстрирующий эмуляцию цикла do-while:

i = 1; i = 1;

do { for (;;) {

cout << i << endl; cout << i << endl;

if (i > 10) break;

i += 2; i += 2;

} while (i <= 10) }

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

Можно ли во вложенном цикле for использовать переменную управления внешним циклом в качестве границы диапазона значений для внутренних циклов?

Да. С++ не только не запрещает такое использование, на самом деле

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

for ( int i = 1; i <= 100; i += 5)

for ( int j = i; i <= 100; j++)

cout < i * j << endl;

Ограничивает ли С++ вложение циклов разных типов?

Нет. В программе на С++ вы можете вкладывать любые комбинации

циклов.

СТРУКТУРА ПРОГРАММЫ

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

Исходная программа

Исходная программа- это совокупность следующих об"ектов: директив, указаний компилятору, об"явлений и определений. Дирек­тивы задают действия препроцессора по преобразованию текста прог­раммы перед компиляцией. Указания компилятору- это команды, вы­полняемые компилятором во время процесса компиляции. Об"явления задают имена и атрибуты переменных, функций и типов, используемых в программе. Определения- это об"явления, определяющие переменные и функции.

Определение переменной в дополнении к ее имени и типу зада­ет начальное значение об"явленной переменной. Кроме того, опреде­ление предполагает распределение памяти для переменной.

Определение функции специфицирует ее структуру, которая

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

Исходная программа может содержать любое число директив,

указаний компилятору, об"явлений и определений. Любой из об"ектов программы имеет определенный синтаксис, описанный в этом руковод­стве,и каждая составляющая может появляться в любом порядке, хотя влияние порядка, в котором следуют переменные и функции может быть использовано в программе (см. раздел 3.5 "Время жизни и ви­димость").

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

В следующем примере иллюстрируется простая исходная прог­рамма на языке Си.

int x = 1;/* Variable definitions */

int y = 2;

extern int printf(char *,...);/* Function declaration */

main () /* Function definition for main function */

{

int z; /* Variable declarations */

int w;

z = y + x; /* Executable statements */

w = y - x;

printf("z = %d \nw = %d \n", z, x);

}

Эта исходная программа определяет функцию с именем main и об"являет функцию printf. Переменные x и y задаются своими опре­делениями. Переменные z и w только об"являются.

ОБЪЯВЛЕНИЯ

В этом разделе описываются форматы и составные части об"яв­лений переменных, функций и типов. Об"явления Си имеют следующий синтаксис:

[<sc-specifier>][<type-specifier>]<declarator>[=<initializer>] [,<declarator>[=<initializer>...],

где:

<sc-specifier>- спецификатор класса памяти; <type-specifier>- имя определяемого типа;

<declarator>- идентификатор, который может быть модифициро­ван при об"явлении указателя, массива или функции;

<initializer>- задает значение или последовательность зна­чений, присваиваемых переменной при об"явлении.

Все переменные Си должны быть явно об"явлены перед их ис­пользованием. Функции Си могут быть об"явлены явно или неявно в случае их вызова перед определением.

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

об"явлений на типах данных уже определенных.

Об"явление Си требует одного или более деклараторов. Декла­ратор- это идентификатор, который может быть определен с квадрат­ными скобками ([]), эвездочкой (*) или круглыми скобками () для об"явления массива, указателя или функции. Когда об'является простая переменная (такая как символ, целое или плавающее), структура или совмещение простых переменных, то декларатор- это идентификатор.

В Си определено четыре спецификатора класса памяти, а имен­но: auto, extern, register и static.

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

Об"явления функций описаны в разделе 4.4.

Спецификаторы типов

Язык Си поддерживает определения для множества базовых ти­пов данных, называемых "основными" типами. Названия этих типов перечислены в Табл. 4.1.

------------------------------------------------------------

Типы целых Типы плавающих Другие типы

------------------------------------------------------------

signed char float void

signed int double

signed short intsigned long int

unsigned char

unsigned int

unsignet short int unsigned long int

-----------------------------------------------------------

Табл. 4.1. Основные типы.

Перечислимые типы также рассматриваются как основные типы. Спецификаторы перечислимых типов рассмотрены в разделе 4.7.1. Ти­пы signed char, signed int, signed short int и signed long int

вместе с соответствующими двойниками unsigned называются типами целых.

Спецификаторы типов float и double относятся к типу "плава­ющих". В об"явлениях переменых и функций можно использовать любые спецификаторы "целый" и "плавающий".

Тип void может быть использован только для об"явления функ­ций, которые не возвращают значения. Типы функций рассмотрены в разделе 4.4.

Можно задать дополнительные спецификаторы типа путем об"яв­ления typedef, описанного в разделе 4.7.2.

При записи спецификаторов типов допустимы сокращения как показано в табл. 4.2. В целых типах ключевое слово signed может быть опущено. Так, если ключевое слово unsigned опускается в за­писи спецификатора типа, то тип целого будет знаковым, даже если опущено ключевое слово signed.

В некоторых реализациях могут быть использованы опции ком­пилятора, позволяющие изменить умолчание для типа char со знако­вого на беззнаковый. Когда задана такая опция, сокращение char имеет то же самое значение, что и unsigned char, и следовательно ключевое слово sidned должно быть записано при об"явлении сим­вольной величины со знаком.

-----------------------------------------------------------

Спецификатор типа Сокращение

-----------------------------------------------------------

signed char char

signed int signed, int

signed short int short, signed short

signed long int long, signed long

unsigned char -

unsigned int unsigned

unsigned short int unsignet short

unsignet long int unsignet long

float -

long float double

------------------------------------------------------------

Табл. 4.2. Спецификаторы и сокращения

Замечание: в этом руководстве в основном используются сок­ращенные формы, перечисленные в Табл. 4.2, при этом предполагает­ся, что char по умолчанию знаковый.

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

-----------------------------------------------------------

Тип Представление Область значений

в памяти величины

-----------------------------------------------------------

char 1 байт -128 до 127

int зависит от

реализации

short 2 байта -32768 до 32767

long 4 байта -2.147.483.648 до 2.147.483.647

unsigned char 1 байт 0 до 255

unsigned зависит от

реализации

unsigned short 2 байта 0 до 65535

unsigned long 4 байта 0 до 4.294.967.295

float 4 байта IEEE стандартное

соглашение

double 8 байт IEEE стандартное

соглашение ------------------------------------------------------------

Табл 4.3 Размер памяти и область значений типов

Тип char используется для запоминания буквы, цифры или сим­вола из множества представимых символов. Значением об"екта типа char является ASCII код, соответствующий данному символу. Так как тип char интерпретируется как однобайтовая целая величина с об­ластью значений от -128 до 127, то только величины от 0 до 127

имеют символьные эквиваленты. Аналогично, тип unsigned char может запоминать величины с областью значений от 0 до 255.

Заметим, что представление в памяти и область значений для типов int и unsigned int не определены в языке Си. По умолчанию размер int (со знаком и без знака) соответствует реальному разме­ру целого на данной машине. Например, на 16-ти разрядной машине тип int всегда 16 разрядов или 2 байта. На 32-ух разрядной машине тип int всегда 32 разряда или 4 байта. Таким образом, тип int эк­вивалентен типам short int или long int в зависимости от реализа­ции.

Аналогично, тип unsigned int эквивалентен типам unsigned short или unsigned long. Спецификаторы типов int и unsigned int широко используются в программах на Си, поскольку они позволяют наиболее эффективно манипулировать целыми величинами на данной машине.

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

Деклараторы

Синтаксис:

<identifier>

<declarator>[]

<declarator>[constant-expression>]

*<declarator>

<declarator>()

<declarator>(<arg-type-list>)

(<declarator>)

Си позволяет об"являть: массивы величин, указатели на вели­чины, величины возвратов функций. Чтобы об"явить эти об"екты, нужно использовать декларатор, возможно модифицированный квадрат­ными скобками ([]), круглыми скобками () и звездочкой (*), что соответствует типам массива, функции или указателя. Деклараторы появляются в об"явлениях указателей, массивов и функций.

Деклараторы массивов, функций и указателей

Когда декларатор состоит из немодифицируемого идентификато­ра, то об'ект, который об"является, имеет немодифицированный тип. Звездочка, которая может появиться слева от идентификатора, моди­фицирует его в тип указателя. Если за идентификатором следуют квадратные скобки ([]), то тип модифицируется на тип массива. Ес­ли за идентификатором следуют круглые скобки, то тип модифициру­ется на тип функции. Сам по себе декларатор не образует полного об"явления. Для этого в об"явление должен быть включен специфика­тор типа. Спецификатор типа задает тип элементов массива или тип адресуемых об"ектов и возвратов функции.

Следующие примеры иллюстрируют простейшие формы декларато­ров:

1. int list[20]

2. char *cp

3. double func(void),

где:

1. Массив list целых величин

2. Указатель cp на величину типа char

3. Функция func без аргументов, возвращающая величину double



Составные деклараторы

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

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

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

char *(*(*var) ()) [10];

^ ^ ^ ^ ^ ^ ^

7 6 4 2 1 3 5

1. Идентификатор var об'явлен как

2. Указатель на

3. Функцию, возвращающую

4. Указатель на

5. Массив из 10 элементов, который состоит 6. Из указателей на

7. Величины типа char.

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

1. int *var[5]; - массив указателей на величины типа int.

2. int (*var)[5]; - указатель на массив величин типа int.

3. long *var(long,long); - функция, возвращающая указатель на величину типа long.

4. long (*var) (long,long); - указатель на функцию, возвра­щающую величину типа long.

5. struct both {

int a;

char b;

} ( *var[5] ) ( struct both, struct both); массив указателей на функции, возвращающих структуры.

6. double ( *var( double (*) [3] ) ) [3];

функция, возвращающая указатель на массив из трех величин типа double.

7. union sign {

int x;

unsigned y;

} **var[5] [5];

массив массивов указателей на указатели совмещений.

8. union sign *(*var[5]) [5];

массив указателей на массив указателей на совмещения.

Описание примеров:

В первом примере, модификатор массива имеет высший приори­тет, чем модификатор указателя, так что var об"является массивом. Модификатор указателя определяет тип элементов массива; элемента­ми являются указатели на величины типа int.

Во втором примере скобки меняют значение об"явления первого примера. Теперь модификатор указателя имеет более высокий приори­тет, чем модификатор массива, и переменная var об"является как указатель на массив из пяти величин типа int.

В третьем примере модификатор функции имеет более высокий приоритет, чем модификатор указателя, так что переменная var об"является функцией, возвращающей указатель на величину типа long. Функция об"явлена с двумя аргументами типа long.

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

Элементы массива не могут быть функциями. Взамен этому в пятом примере показано, как об"явить массив указателей на функ­ции. В этом примере переменная var об"явлена как массив из пяти указателей на функции, возвращающие структуры с двумя элементами. Оба аргумента функции об"явлены как структуры типа both. Заметим, что круглые скобки, в которые заключено выражение *var[5], обяза­тельны. Без них об"явление будет неверным, поскольку будет об"яв­лен массив функций:

/* ILLEGAL */

struct both *var[5] ( struct both, struct both );

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

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

В восьмом примере показано, как круглые скобки изменили

смысл об"явления. В этом примере var- это массив из пяти указате­лей на массив из пяти указателей на совмещения.



Об"явления переменной

В этом разделе дано описание синтаксиса и семантики об"яв­лений переменной. В частности, здесь об"ясняется каким образом об"явить следующие переменные:

Тип переменной Описание

Простая переменная Переменная целого или плаваю-

щего типа.

Переменная перечис- Простая переменная целого типа

ления. которая принимает значения из

предопределенного набора зна-

чений поименованных констант. Структура Переменная, которой соответс-

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

Совмещение Переменная, которой соответс-

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

Массив Переменная, представляющая на-

бор элементов одного типа.

Указатель Переменная, которая указывает

на другую переменную (содержит местоположение другой перемен­ной в форме адреса).

Общий синтаксис об"явлений переменных следующий:

[<sc-spesifier>] <type-spesifier> <declarator> [,<declarator>...],

где <type- spesifier> - задает тип данных, представляемых переменной, а <declarator> - это имя переменной, возможно модифи­цированное для об"явления массива или указателя. В об"явлении мо жет быть задана более чем одна переменная путем задания множест­венного об"явления, в котором деклараторы разделены запятыми. <sc- spesifier> задает класс памяти переменной. В некоторых слу­чаях переменные могут быть инициализированы при их определении. Классы памяти и инициализация описаны в разделах 4.6 и 4.7 соот­ветственно.

Объявление простой переменной

Синтаксис:

<type-specifier><identifier>[,<identifier>...];

Об"явление простой переменной определяет имя переменной и ее тип; оно может также определять класс памяти переменной, как это описано в разделе 4.6. Имя переменной- это идентификатор, за­данный в об"явлении. Спецификатор типа <type-specifier> задает имя определяемого типа данных.

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

Примеры

int x; /* Example 1 */

unsigned long reply, flag /* Example 2 */ double order; /* Example 3 */

В первом примере об"является простая переменная x. Эта пе-

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

Во втором примере об"явлены две переменные: reply и flag. Обе переменные имеют тип unsigned long.

В третьем примере об"явлена переменная order, которая имеет тип double. Этой переменной могут быть присвоены величины с пла­вающей запятой.

Объявление перечисления

Синтаксис: enum[<tag>]{<enum-list>}<identifier>[,<identifier>...]; enum<tag><identifier>[,<identifier>...];

Об"явление перечисления задает имя переменной перечисления и определяет список именованных констант, называемый списком пе­речисления. Значением каждого имени списка является целое число. Переменная перечисления принимает значение одной из именованных констант списка. Именованные константы списка имеют тип int. Та-

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

Объявление перечисления начинается с ключевого слова enum и

имеет две формы представления. В первой форме представления имена перечисления задаются в списке перечисления <enum-list>.

Опция <tag>- это идентификатор, который именует тип пере­числения, определенного в <enum-list>.

Переменную перечисления именует <identifier>. В об"явлении может быть описана более чем одна переменная перечисления.

Во второй форме используется тег перечисления, который ссы-

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

<enum-list> имеет следующий синтаксис:

<identifier>[=<constant-expression>][,<identifier>

[=<constant-expression]]...

.

.

.

Каждый идентификатор именует элементы перечисления. По умолчанию первому идентификатору соответствует значение 0, следу­ющий идентификатор ассоциируется со значением 1 и т. д. Имя конс­танты перечисления эквивалентно ее значению.

Запись =<constant-expression> переопределяет последователь­ность значений, заданных по умолчанию. Идентификатор, следующий перед записью =<constant-expression> принимает значение, задавае­мое этим константным выражением. Константное выражение имеет тип int и может быть отрицательным. Следующий идентификатор в списке ассоциируется с величиной, равной <constant-expression>+1, если он явно не задается другой величиной.

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

Примеры:

/**************** Example 1 ***************/

enum day {

saturday,

sunday = 0,

monday,

tuesday,

wednesday,

thursday,

friday

} workday;

/***************** Example 2 ***************/

enum day today = wednesday;

В первом примере определяется тип перечисления, поименован­ный day и об"является переменная workday этого типа перечисления. С saturday по умолчанию ассоциируется значение 0. Идентификатор sunday явно устанавливается в 0. Оставшиеся идентификаторы по умолчанию принимают значение от 1 до 5.

Во втором примере переменной today типа enum day присваива­ется значение из перечисления. Заметим, что для присваивания ис­пользуется имя константы из перечисления. Так как тип перечисле­ния day был предварительно об"явлен, то достаточно сослаться только на тег перечисления.

Объявления структур

Синтаксис: struct[<tag>]{<member-declaration-list>}<declarator>[,<declarator>...]; struct<tag><declarator>[,<declarator>...];

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

Об"явление структуры начинается с ключевого слова struct и имеет две формы представления, как показано выше. В первой форме представления типы и имена элементов структуры специфицируются в списке об"явлений элементов <member-declaration-list>. <tag>- это идентификатор, который именует тип структуры, определенный в списке об"явлений элементов.

Каждый <declarator> задает имя переменной типа структуры. Тип переменной в деклараторе может быть модифицирован на указа­тель к структуре, на массив структур или на функцию, возвращающую структуру.

Вторая синтаксическая форма использует тег- <tag> структуры для ссылки на тип структуры. В этой форме об"явления отсутствует список об"явлений элементов, поскольку тип структуры определен в другом месте. Определение типа структуры должно быть видимым для тега, который используется в об"явлении и определение должно предшествовать об"явлению через тег, если тег не используется для об"явления указателя или структурного типа typedef. В последних случаях об"явления могут использовать тег структуры без предвари­тельного определения типа структуры, но все же определение должно находиться в пределах видимости об"явления.

Список об"явлений элементов <member-declaration-list>- это одно или более об"явлений переменных или битовых полей. Каждая

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

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

Битовые поля

Об"явления битовых полей имеют следующий синтаксис:

<type-specifier>[<identifier>]:<constant-expression>; Битовое поле состоит из некоторого числа бит, специфициро-

ванных константным выражением- <constant- expression>. Для бито-

вого поля спецификатор типа <type- specifier> должен специфициро-

вать беззнаковый целый тип, а константное выражение должно быть неотрицательной целой величиной. Массивы битовых полей, указатели на битовые поля и функции, возвращающие битовые поля не допуска­ются. Идентификатор- <identifier> именует битовое поле. Неимено­ванное битовое поле, чей размер специфицируется как нулевой, име­ет специальное назначение: оно гарантирует, что память для следу­ющей переменной об"явления будет начинаться на границе int.

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

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

Битовые поля не располагаются на пересечении границ, обяв­ленных для них типов. Например, битовое поле, об"явленое с типом unsigned int, упаковывается или в пространстве, оставшимся от предидущего unsigned int или начиная с нового unsigned int.

Примеры

/**************** Example 1 ****************/

struct {

float x,y;

} complex;

/**************** Example 2 *****************/

struct employee {

char name[20];

int id;

long class;

} temp;

/**************** Example 3 ******************/

struct employee student, faculty, staff;

/**************** Example 4 ******************/ struct sample {

char c;

float *pf;

struct sample *next;

} x;

/***************** Example 5 ******************/

struct {

unsigned icon : 8;

unsigned color : 4; unsigned underline : 1; unsigned blink : 1;

} screen[25][80];

В первом примере об"является переменная с именем complex типа структура. Эта структура состоит из двух элементов x и y ти­па float. Тип структуры не поименован.

Во втором примере об"является переменная с именем temp типа структура. Структура состоит из трех элементов с именами name, id и class. Элемент с именем name- это массив иэ 20- ти элементов типа char. элементы с именами id и class- это простые переменные типа int и long соответственно. Идентификатор employee является тегом структуры.

В третьем примере об"явлены три переменных типа структура с именами: student, faculty и staff. Каждая из структур состоит из трех элементов одной и той же конструкции. Элементы определены при об"явлении типа структуры с тегом employee в предыдущем примере.

В четвертом примере об"является переменная с именем x типа структура. Первые два элемента структуры представлены переменной c типа char и указателем pf на величину типа float. Третий эле­мент с именем next об"являются как указатель на описываемую структуру sample.

В пятом примере об"является двумерный массив поименованный screen, элементы которого имеют структурный тип. Массив состоит из 2000 элементов и каждый элементэто отдельная структура, состо­ящая из четырех элементов типа bit-fild с именами icon, color, underline и blink.

Об"явление совмещений

Синтаксис: union[<tag>]{<member-declaration-list>}<declarator>[,<declarator>...]; union<tag><declarator>[,<declarator>...];

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

Об"явление совмещения имеет тот же самый синтаксис, как и об"явление структуры, за исключением того, что она начинается с ключевого слова union вместо ключевого слова struct. Для об"явле­ния совмещения и структуры действуют одни и те же правила, за ис­ключением того, что в совмещении не допускаются элементы типа би­товых полей.

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

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

Примеры:

/************** Example 1 ********************/

union sign {

int svar;

unsigned uvar;

} number;

/************** Example 2 ********************/

union {

char *a, b;

float f[20];

} jack;

/*************** Example 2 *******************/

union {

struct {

char icon;

unsigned color : 4;

} window1, window2, window3, window4;

} screen[25][80];

В первом примере об"является переменная типа совмещения, поименованная number. Список элементов совмещения состоит из двух

об"явлений переменных: svar типа int и uvar типа unsigned. Это об"явление позволяет запоминать текущее значение number в знако­вом или беззнаковом виде. Тип совмещения поименован идентификато­ром sign.

Во втором примере об"является переменная типа совмещения с именем jack. Список элементов об"явления состоит из трех об"явле­ний: указателя a на величину типа char, переменной b типа char и массива f из 20 элементов типа float. Тип совмещения не поимено­ван.

Память, распределенная для переменной jack, равна памяти, распределенной под массив f, поскольку f самый большой элемент совмещения.

В третьем примере об"является двумерный массив совмещений с именем screen. Массив состоит из 2000 об"ектов. Каждый об"ект­это отдельное совмещение из четырех элементов: window1, window2, window3, window4, где каждый элемент- это структура. В любое за­данное время каждый об"ект совмещения поддерживается одним из че­тырех возможных элементов типа структура. Таким образом, перемен­ная screen- это композиция четырех возможных "windows".

Об"явление массива

Синтаксис: <type-specifier><declarator>[<constant-expression>]; <type-specifier><declarator>[];

Здесь квадратные скобки- это терминальные символы. Об"явле­ние массива определяет тип массива и тип каждого элемента. Оно может определять также число элементов в массиве. Переменная типа массив рассматривается как указатель на элементы массива. Об"яв­ление массива может представляться в двух синтаксических формах, указанных выше. Декларатор<declarator> задает имя переменной. Квадратные скобки, следующие за декларатором, модифицируют декла­ратор на тип массива. Константное выражение

<constant-expression>, заключенное в квадратные скобки, определя­ет число элементов в массиве. Каждый элемент имеет тип, задавае­мый спецификатором типа <type-specifier>, который может специфи­цировать любой тип, исключая void и тип функции.

Во второй синтаксической форме опущено константное выраже­ние в квадратных скобках. Эта форма может быть использована толь­ко тогда, когда массив инициализируется или об"явлен как формаль­ный параметр или об"явлен как ссылка на массив, явно определенный где-то в программе.

Массив массивов или многомерный массив определяется путем задания списка константных выражений в квадратных скобках, следу­щего за декларатором:

<type-specifier><declarator>[<constant-expression>]

[<constant-expression>]...

Каждое константное выражение- <constant-expression> в квад­ратных скобках определяет число элементов в даннном иэмерении

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

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

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

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

char A[2][3]

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

Примеры:

/*************** Example 1 ******************/

int scores[10], game;

/*************** Example 2 ******************/

float matrix[10][15];

/*************** Example 3 ******************/

struct {

float x,y;

} complex[100];

/*************** Example 4 *******************/

char *name[20];

В первом примере об"является переменная типа массив с име­нем scores из 10 элементов типа int. Переменная с именем game об"явлена как простая переменная целого типа.

Во втором примере об"является двумерный массив с именем matrix. Массив состоит из 150-ти элементов типа float.

В третьем примере об"является массив структур. Массив сос­тоит из 100 об"ектов. Каждый об"ект массива представляет собой структуру, состоящую из двух элементов.

В четвертом примере об"явлен массив указателей. Массив сос­тоит из 20-ти элементов, каждый из которых является указателем на величину типа char.

4.4.6. Об"явление указателей

Синтаксис:

<type-specifier> *<declarator>;

Об"явление указателя определяет имя переменной типа указа­тель и тип об"екта, на который указывает эта переменная. Деклара­тор- <declarator> определяет имя переменной с возможной модифика­цией ее типа. Спецификатор типа- <type- specifier> задает тип об"екта, который может быть базового типа, типа структуры или совмещения.

Переменная типа указатель может указывать также на функции, массивы и другие указатели. Более полная информация о типах ука­зателей дана в разделе 4.3.2. "Составные деклараторы".

Если указатель не используется до определения типа структу­ры или совмещения, то он может быть об"явлен ранее этого опреде­ления. Такие об"явления допускаются, поскольку компилятору не требуется знать размера структуры или совмещения, чтобы распреде­лить память под переменную типа указатель. Указатель может быть об"явлен посредством использования тега структуры или совмещения (смотри ниже пример 4).

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

Для некоторых реализаций используются специальные ключевые слова near, far и huge, чтобы модифицировать размер указателя. Об"явления, использующие специальные ключевые слова, были описаны в разделе 4.3.3. Информация о смысле ключевых слов дана в системной документации.

Примеры:

char *message; /* Example 1 */

int *pointers[10]; /* Example 2 */ int (*pointer)[10]; /* Example 3 */ struct list *next, *previous; /* Example 4 */

struct list { /* Example 5 */ char *token;

int count;

struct list *next;

} line;

struct id { /* Example 6 */ unsigned int id_no;

struct name *pname;

} record;

В первом примере об"является переменная- указатель поимено­ванная message. Она указывает на величину типа char.

Во втором примере об"явлен массив указателей, поименованный pointers. Массив состоит из 10 элементов. Каждый элемент- это указатель на переменную типа int.

В третьем примере об"явлена переменная- указатель, поимено­ванная pointer. Она указывает на массив из 10 элементов. Каждый элемент в этом массиве имеет тип int.

В четвертом примере об"явлены две переменныхуказателя, ко­торые ссылаются на величины структурного типа list (смотри следу­ющий пример). Определение типа с именем list должно находиться в пределах видимости об"явления.

В пятом примере об"является переменная с именем line, структурного типа, поименованного list. Тип структуры с именем list определяется тремя элементами. Первый элементэто указатель на величину типа char, второй- на величину типа int, а третий­это указатель на следующую структуру типа list.

В шестом примере об"является переменная с именем record, имеющая тип структуры с именем id. Заметим, что третий элемент с именем pname об"явлен как указатель на другой тип структуры с именем name. Это об"явление может появиться перед об"явление структуры с именем name.

Об"явление функций

Синтаксис:

[<type-specifier>]<declarator>([<arg-type-list>])[,<declarator>...];

Об"явление функции определяет имя, тип возврата функции и, возможно, типы и число ее аргументов. Об"явление функции также называется forward- об"явлением. Декларатор функции об"являет имя функции, а спецификатор типа задает тип возврата. Если специфика­тор типа опущен в об"явлении функции, то предполагается, что функция возвращает величину типа int.

Об"явление функции может включать спецификаторы класса па­мяти extern или static.

Список типов аргументов.

Список типов аргументов- <arg-type-list> определяет число и типы аргументов функции. Синтаксис списка аргументов следующий:

<type-name-list>[,...]

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

Если список типов аргументов- <arg-type-list> содержит

только многоточие (...), то число аргументов функции является пе-

ременным или равно нулю.

Замечание:

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

Имя типа- <type- name> для типов структуры, совмещения или базового типа состоит из спецификатора этого типа (такого как int ). Имена типов для указателей, массивов и функций формируются пу­тем комбинации спецификатора типа с "абстрактным декларатором". Абстрактный декларатор- это декларатор без идентификатора. В раз­деле 4.9 "Имена типов" об"ясняется, каким об"разом формировать и интерпретировать абстрактные деклараторы.

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

Еще одна специальная конструкция допускается в списке типов аргументов. Это фраза void *, которая специфицирует аргумент типа указатель. Эта фраза может быть использована в списке типов аргу­ментов вместо имени типа.

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

ся, то компилятор не проверяет соответствия между формальными и фактическими параметрами при вызове функции. Более подробная ин­формация дана в разделе 7.4 "Вызовы функций".

Тип возврата

Функции могут возвращать величины любого типа за исключени­ем массивов и функций. Для этого посредством спецификатора типа­"type-specifier" в об"явлении функции можно специфицировать любой тип: основной, структуру или совмещение. Идентификатор функции может быть модифицирован одной или несколькими звездочками (*), чтобы об"явить возвращаемую величину типа указателя.

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

Примеры:

int add(int, int); /* Example 1 */

double calc(); /* Example 2 */

char *strfind(char *,...); /* Example 3 */

void draf(void); /* Example 4 */

double (*sum(double, double)) [3]; /* Example 5 */ int (*select(void)) (int) ; /* Example 6 */

char *p; /* Example 7 */

short *q;

int prt(void *);

В первом примере об"является функция, поименованная add, которая требует два аргумента типа int и возвращает величину типа int.

Во втором примере об"является функция, поименованная calc, которая возвращает величину типа double. Список типов аргументов не задан. В третьем примере об"является функция, поименованная strfind, которая возвращает указатель на величину типа char. Фун­кция требует, по крайней мере один аргументуказатель на величину типа char. Список типов аргументов заканчивается запятой с много­точием, обозначающим, что функция может потребовать большее число аргументов.

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

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

В шестом примере функция, поименованная select, об"явлена без аргументов и возвращает указатель на функцию. Указатель возв­рата ссылается на функцию, требующую один аргумент типа int и возвращающую величину типа int.

В седьмом примере об"явлена функция prt, которая требует аргумент- указатель любого типа, и которая возвращает величину типа int. Любой указатель p или q могли бы быть использованы как аргументы функции без выдачи при этом предупреждающего сообщения.



Классы памяти

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

Переменные с локальным временем жизни захватывают новую па-

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

ет свое значение.

Хотя Си определяет два типа классов памяти, но, тем не ме­нее, имеется следующих четыре спецификатора классов памяти:

auto

register

static

extern

Об"екты классов auto и register имеют локальное время жиз­ни. Спецификаторы static и extern определяют об"екты с глобальным временем жизни. Каждый из спецификаторов класса памяти имеет оп­ределенный смысл, который влияет на видимость функций и перемен­ных в той же мере, как и сами классы памяти. Термин "видимость" относится к той части программы, в которой могут ссылаться друг на друга функции и переменные. Об"екты с глобальным временем жиз­ни существуют на протяжении выполнения исходной программы, но они могут быть видимы не во всех частях программы. Видимость и свя­занная с ней концепция времени жизни рассмотрена в разделе 3.5.

Месторасположение об"явления переменной или функции внутри

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

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

Об"явления переменной на внешнем уровне

Об"явления переменной на внешнем уровне используют специфи­каторы класса памяти static и extern или совсем опускают их. Спе­цификаторы класса памяти auto и register не допускаются на внеш­нем уровне.

Об"явления переменных на внешнем уровне- это определения переменных или ссылки на определения, сделанные в другом месте.

Об"явление внешней переменной, которое инициализирует эту

переменную (явно или неявно), называется определением этой пере­менной. Определение на внешнем уровне может задаваться в следую­щих различных формах:

-переменная на внешнем уровне может быть определена путем ее об"явления со спецификатором класса памяти static. Такая пере­менная может быть явно инициализирована константным выражением. Если инициализатор отсутствует, то переменная автоматически ини­циализируется нулем во время компиляции. Таким образом, об"явле­ния static int k = 16; и static int k; оба рассматриваются как определения;

-переменная определяется, когда она явно инициализируется

на внешнем уровне. Например, int j = 3; это определение перемен­ной.

Так как переменная определяется на внешнем уровне, то она видима в пределах остатка исходного файла, от места, где она оп­ределена. Переменная не видима выше своего определения в том же самом исходном файле ни в других исходных файлах программы, если не об"явлена ссылка, которая делает ее видимой.

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

Спецификатор класса памяти extern используется для об"явле­ния ссылки на переменную, определенную где-то в другом месте. Та­кие об"явления используются в случае, когда нужно сделать видимым определение переменной в других исходных файлах или выше места, где она определена в том же самом исходном файле. Так как ссылка на переменную об"явлена на внешнем уровне, то переменная видима в пределах остатка исходного файла от места об"явления ссылки.

В об"явлениях, которые используют спецификатор класса памя­ти extern, инициализация не допускается, так как они ссылаются на переменные, чьи величины уже определены.

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

Есть одно исключение из правил, описанных выше. Можно опус-

тить из об"явления переменной на внешнем уровне спецификатор класса памяти и инициализатор. Например, об"явление int n; будет правильным внешним об"явлением. Это об"явление имеет два различ­ных смысла в зависимости от контекста.

1. Если где-нибудь в программе будет определена на внешнем уровне переменная с тем же именем, то об"явление является ссылкой на эту переменную, как если бы был использован спецификатор клас­са памяти extern в об"явлении.

2. Если нет такого определения, то об"явленной переменной распределяется память во время линкования и переменная инициали­зируется нулем. Если в программе появится более чем одно такое об"явление, то память распределится для наибольшего размера из об"явленных переменных. Например, если программа содержит два не­инициализированных об"явления переменной i на внешнем уровне int i; и char i; то память во время линкования распределится под пе­ременную i типа int.

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

Пример:

/*****************************************************

SOURCE FILE ONE *****************************************************/

extern int i; /* reference to i

defined below */

main()

{

i++;

printf("%d\n", i); /* i equals 4 */

next();

}

int i = 3; /* definition of i */

next()

{

i++;

printf("%d\n", i); /* i equals 5 */

other();

}

/***************************************************** SOURCE FILE TWO

*****************************************************/

extern int i; /* reference to i in

first source file */

other()

{

i++;

printf("%d\n", i); /* i equals 6 */

}

Два исходных файла в совокупности содержат три внешних об"явления i. Одно об"явление содержит инициализацию- int i = 3; , где глобальная переменная i определена с начальным значением равным 3.

Самое первое об"явление extern в первом файле делает гло­бальную переменную видимой выше ее определения в файле.

Без об"явления extern функция main не смогла бы сослаться на глобальную переменную i. Об"явление extern переменной i во втором исходном файле делает глобальную переменную видимой в этом исходном файле.

Все три функции выполняют одну и ту же задачу: они увеличи­вают i на 1 и печатают получившееся значение. (Предполагается, что функция printf определена где-то еще в программе.). Печатают­ся величины равные 4, 5 и 6.

Если бы переменная i не была бы инициализирована,она бы бы­ла автоматически установлена в 0 при линковании. В этом случае напечатанные значения были бы равны 1, 2 и 3.

Об"явление переменной на внутреннем уровне

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

Спецификатор класса памяти auto об"являет переменную с ло-

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

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

ют ту же самую видимость, что и переменные auto.

Число регистров, которое может быть использовано под память переменных, зависит от машины. Когда компилятор встречает специ­фикатор класса памяти register в об"явлении, а свободного регист­ра не имеется, то для переменной распределяется память класса auto. Компилятор назначает переменным регистровую память в том порядке, в котором появляются об"явления в исходном файле. Регис­тровая память, если она имеется, гарантирована только для целого и адресного типов.

Переменная, об"явленная на внутреннем уровне со специфика­тором класса памяти static,имеет глобальное время жизни и имеет видимость только внутри блока, в котором она об"явлена. В отличие от переменных auto, переменные, об"явленные как static, сохраняют свое значение при завершении блока.

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

Переменная, об"явленная со спецификатором класса памяти extern, является ссылкой на переменную с тем же самым именем, оп­ределенную на внешнем уровне в любом исходном файле программы.

Цель внутреннего об"явления extern состоит в том, чтобы

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

Пример:

int i = 1;

main()

{ /* reference to i, defined above */

extern int i;

/* initial value is zero; a is

visible only within main */

static int a;

/* b is stored in a register, if possible */ register int b = 0;

/* default storage class is auto */

int c = 0;

/* values printed are 1, 0, 0, 0 */ printf("%d\n%d\n%d\n%d\n", i, a, b, c);

other();

}

other()

{

/* i is redefined */

int i = 16;

/* this a is visible only within other */

static int a = 2;

a += 2;

/* values printed are 16, 4 */

printf("%d\n%d\n", i, a);

}

Переменная i определяется на внешнем уровне с инициализаци­ей 1. В функции main об"явлена ссылка extern на переменную i внешнего уровня. Переменная класса памяти static автоматически

устанавливается в 0, так как инициализатор опущен. Вызов функции print (предполагается, что функция print определена в каком-то месте исходной программы.) печатает величины 1, 0, 0, 0.

В функции other, переменная i переопределяется как локаль­ная переменная с начальным значением 16. Это не влияет на значе­ние внешней переменной i. Переменная a об"является как переменная класса памяти static с начальным значением 2. Она не противоречит переменной a, об"явленной в функции main, так как видимость пере­менных класса памяти static на внутреннем уровне ограничена бло­ком, в котором она об"явлена.

Значение переменной увеличивается на 2 и становится равным 4. Если бы функция other была вызвана снова в той же самой прог­рамме, то начальное значение a стало бы равным 4. Внутренние пе­ременные класса памяти static сохраняют свои значения, когда за­канчивается выполнение блока, в котором они об"явлены.

Об"явление функции на внешнем и внутреннем уровнях

Функции могут быть об"явлены со спецификаторами класса па­мяти static или extern. Функции всегда имеют глобальное время жизни.

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

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

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

Об"явления функций, в которых опущен спецификатор класса памяти, считаются по умолчанию extern.

Инициализация

В об"явлении переменной может быть присвоено начальное зна­чение посредством инициализатора. Величина или величины инициали­затора присваиваются переменной.

Синтаксически, записи инициализатора предшествует знак рав­но (=)

=<initializer>

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

Переменные, об"явленные на внешнем уровне, могут быть ини-

циализированы. Если они явно не инициализированы, то они устанав­ливаются в нуль во время компиляции или линкования. Любая пере­менная, об"явленная со спецификатором класса памяти static, может быть инициализирована константным выражением. Инициализация пере­менных класса static выполняется один раз во время компиляции. Если отсутствует явная инициализация, то переменные класса памяти static автоматически устанавливаются в нуль.

Инициализация переменных auto и register выполняется каждый раз при входе в блок, в котором они об"явлены. Если инициализатор опущен в об"явлении переменной класса памяти auto или register, то начальное значение переменной не определено. Инициализация

составных типов auto (массив, структура, совмещение) запрещена. Любое составное об"явление класса памяти static может быть иници­ализировано на внешнем уровне.

Начальными значениями для внешних об"явлений переменной и для всех переменных static как внешних так и внутренних должно быть константное выражение. Автоматические и регистровые перемен­ные могут быть инициализированы константными или переменными ве­личинами.

Базовые типы и типы указателей

Синтаксис:

=<expression>

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

Примеры:

int x = 10; /* Example 1 */

register int *px = 0; /* Example 2 */ int c = (3 * 1024); /* Example 3 */ int *b = &x; /* Example 4 */

В первом примере x инициализируется константным выражением 10. Во втором примере, указатель px инициализирован нулем, в ре­зультате чего получился "null" указатель. В третьем примере ис­пользуется константное выражение для инициализации c. В четвертом примере инициализируется указатель b адресом другой переменной x.

Составные типы

Синтаксис:

={<initializer-list>}

Список инициализаторов <initializer-list> - это последова­тельность инициализаторов, разделенных запятыми. Каждый инициали­затор в последовательности- это либо константное выражение, либо список инициализаторов. Поэтому, заключенный в фигурные скобки список, может появиться внутри другого списка инициализации. Эта конструкция используется для инициализации элементов составных конструкций.

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

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

Эти правила применяются к каждому вложенному списку инициа­лизаторов, точно так же как и ко всей конструкции в целом.

Пример:

int p[4] [3] = {

{ 1, 1, 1 },

{ 2, 2, 2 }, { 3, 3, 3,}, { 4, 4, 4,},

};

В примере об"является массив p размерности 4 строки на 3 столбца. Элементы первой строки инициализируются 1, второй строки

2 и т. д. Заметим, что списки инициализаторов третьей и четвертой строк заканчиваются запятой. Последний список инициализаторов { 4, 4, 4,} также заканчивается запятой.

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

int p[4] [3] = {

1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4

};

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

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

typedef struct {

int n1, n2, n3;

} triplet;

triplet nlist[2] [3] = {

{ { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }, /* Line 1 */ { { 10,11,12}, { 13,14,15}, { 15,16,17} } /* Line 2 */

};

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

1. Первая левая фигурная скобка Line 1 информирует компиля­тор о том, что это начало инициализации первой строки массива nlist(nlist[0]).

2. Вторая левая фигурная скобка означает то, что начинается инициализация первого элемента первой строки массива ( nlist[0] [0] ).

3. Первая правая фигурная скобка сообщает об окончании ини­циализации первого элемента- структуры nlist[0] [0]. Следующая левая фигурная скобка сообщает о начале инициализации второго элемента первой строки nlist[0] [1].

4. Процесс продолжается до конца Line 1 и заканчивается по последней правой фигурной скобке.

Аналогично, Line 2 назначает величины второй строке массива nlist.

Заметим, что внешние фигурные скобки инициализаторов Line 1 и Line 2 требуются. Следующая конструкция, в которой внешние фи­гурные скобки опущены будет неверной.

/* THIS CAUSES AN ERROR */

triplet nlist[2] [3] = {

{ 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 }, /* Line 1 */

{ 10,11,12}, { 13,14,15}, {16,17,18 } /* Line 2 */

};

В этом примере первая левая фигурная скобка в Line 1 стар­тует инициализацию nlist[0], которая является массивом из трех структур. Величины 1, 2, 3 назначаются трем элементам первой структуры. Когда встретится правая фигурная скобка (после величи­ны 3), инициализация nlist[0] закончится и две оставшиеся струк­туры автоматически инициализируются нулем. Аналогично, { 4, 5, 6 } инициализирует первую структуру во второй строке nlist, а ос­тавшиеся две структуры nlist[1] установятся в нуль. Когда компи­лятор встретит следующий список инициализации { 7, 8, 9 }, то это приведет к попытке инициализировать nlist[2]. Так как nlist со­держит только две строки, то будет выдано сообщение об ошибке.

Примеры:

/******************* Example 1 *********************/

struct list {

int i, j, k;

float n[2] [3];

} x = {

1,

2,

3,

{4.0, 4.0, 4.0}

};

/******************* Example 2 *********************/

union {

char x[2] [3];

int i, j, k;

} y = {

{'1'},

{'4'}

};

В первом примере три элемента int структурной переменной x инициализированы 1, 2, и 3 соответственно. Три элемента первой строки массива m инициализированы как 4.0. Элементы второй строки инициализированы нулем по умолчанию.

Во втором примере инициализируется переменная y типа совме­щения. Первым элементом совмещения является массив, для которого требуется составной инициализатор. Список инициализации {'1'} за­дает величины для первой строки массива. Поскольку в списке всего одна величина, то только первый элемент строки массива инициали­зируется символом 1 , а оставшиеся два элемента в строке инициа­лизируются нулем (символом \0) по умолчанию. Аналогично, первый элемент второй строки массива x инициализируется символом 4, а оставшиеся два элемента в строке инициализируются нулем.

Строковые инициализаторы

Массив может быть инициализирован строчным литералом.

Например,

char code[ ] = "abc";

инициализирует code как массив символов из четырех элемен­тов. Четвертым элементом является символ \0, который завершает все строковые литералы.

Если специфицируется размер массива, а строка больше чем

специфицированный размер, то лишние символы отбрасываются. Следу­ющее об"явление инициализирует переменную code, как трехэлемент­ный массив символов:

char code[3] = "abcd"

В примере только три первые символа инициализатора назнача­ются для массива code. Символ d и сивол нуль отбрасываются.

Если строка короче, чем специфицированный размер массива, то оставшиеся элементы массива инициализируются нулем (символом \0).

Об"явления типов

Об"явление типа определяет имя и элементы структурного или совмещающего типов или имя и перечислимое множество перечислимого типа.

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

Об"явление typedef определяет спецификатор типа для типа. Это об"явление используется для того, чтобы создавать более ко­роткие или более осмысленные имена типов уже определенных в Си или об"явленных пользователем.

Типы структур, совмещений и перечислений

Об"явления типов структур, совмещений и перечислений имеют ту же самую общую синтаксическую форму, как и об"явления перемен­ных этих типов. В об"явлении типа идентификатор переменной опу­щен, так как нет переменной которая об"является. Именем структу­ры, совмещения или перечисления является тег.

В об"явлении типа может появиться список об"явлений элемен­тов- <member-declaration-list> или список перечисления-

<enum-list>, определяющие тип.

Сокращенная форма об"явления переменной, в котором tag ссы­лается на тип, определенный где-то еще, при об"явлении типа не используется.

Примеры:

/******************** Example 1 ********************/

enum status {

loss = -1,

bye,

tie = 0,

win,

};

/********************* Example 2 *******************/

struct student {

char name[20];

int id, claas;

};

В первом примере об"является тип перечисления, поименован­ный status. Имя типа может быть использовано в об'явлениях пер­менных типа перечисления. Идентификатор loss явно устанавливается в -1. Идентификаторы bye и tie ассоциируются со значением 0, а win принимает значение 1. Во втором примере об"является структур­ный тип, поименованный student. Теперь можно использовать такое об"явление, как struct student employee, чтобы об"явить структур-

ную переменную employee типа student.

Об"явления typedef

Синтаксис:

typedef <type-spesifier><declarator>[,<declarator>...]; Об"явления typedef являются аналогом об"явления переменной,

за исключением того, что ключевое слово typedef заменяет специфи­катор класса памяти.

Об"явление интерпретируется тем же самым путем, как об"яв­ления переменной или функции, но <declarator> вместо того, чтобы стать переменной типа, специфицированного об"явлением, становится синонимом имени типа. Об"явление typedef не создает типов. Оно создает синонимы для существующих имен типов, которые были специ­фицированы другим способом. Любой тип может быть об"явлен с typedef, включая типы указателя, функции и массива. Имя с ключе­вым словом typedef для типов указателя, структуры или совмещения может быть об"явлено прежде чем эти типы будут определены, но в пределах видимости об"явления.

Примеры:

/******************** Example 1 ********************/

typedef int WHOLE;

/******************** Example 2 ********************/

typedef struct club {

char name[30];

int sise, year;

} GROUP;

/******************** Example 3 ********************/

typedef GROUP *PG;

/******************** Example 4 ********************/

typedef void DRAWE(int, int);

В первом примере об"является WHOLE как синоним для int .

Во втором примере об" является GROUP как структурный тип с тремя элементами. Так как специфицирован также тег clab, то имя GROUP и тег club могу быть использованы в об"явлениях.

В третьем примере используется предидущее имя typedef для об"явления адресного типа. Тип PG об"является как указатель на тип GROUP, который в свою очередь определен как структурный тип.

В последнем примере представлен тип DRAWE для функции не

возвращающей значения и требующей два аргумента типа int. Это оз­начает, например, что об"явление DRAWE box; эквивалентно об"явле­нию void box(int, int);

Имена типов

Имя типа специфицирует особенности типа данных. Имена типов используются в трех контекстах: в списках типов аргументов, при об"явлении функций, в вычислениях cast (преобразованиях типов), и в sizeof операциях. Списки типов аргументов рассматривались в

разделе 4.5. "Об"явления функций". Преобразования cast и операция sizeof обсуждаются в разделах 5.7.2. и 5.3.4. соответственно.

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

<type-specifier><abstract-declarator>

Абстрактный декларатор <abstract-declarator>- это деклара­тор без идентификатора, состоящий из одного или более модификато­ров указателей, массивов и функций. Модификатор указателя (*) всегда появляется перед идентификатором в деклараторе, в то время как модификатор массива ([]) или функции ( () ) появляются после идентификатора. Таким образом, чтобы правильно интерпретировать абстрактный декларатор, нужно начинать интерпретацию с подразуме­ваемого идентификатора.

Абстрактные деклираторы могут быть составными. Скобки в составном абстрактном деклараторе специфицируют порядок интерпре­тации, подобно тому как это делается при интерпретации составных деклараторов об"явлений. Абстрактный декларатор, состоящий из пустых круглых скобок () не допускается, поскольку это двусмыс­ленно. В этом случае невозможно определить находится ли подразу-

меваемый идентификатор внутри скобок, и в таком случае- это немо­дифицированный тип, или перед скобками, тогда- это тип функции. Спецификаторы типа, установленные посредством об"явлений typedef, также рассматриваются как имена типов.

Примеры:

long * /* Example 1 */

int (*) [5] /* Example 2 */

int (*) (void) /* Example 3 */

В первом примере задано имя типа как указатель на тип long. Во втором и третьем примерах показано каким образом скобки

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

КОНТРОЛЬНЫЕ ВОПРОСЫ:



1. Какие ошибки содержат следующие операторы?



enum State { on, off };

enum YesNo { yes, no};

enum DiskDriveStatus { on, off };



2. Верно или нет, что объявление следующего перечислимого типа неправильно?



enum YesNo { no = 0, No = 0, yes = 1, Yes = 1 };



3. Что не так в следующей программе?



#include <iostream.h>

int main()

{

int *p = new int;

cout << "Enter а number";

cin >> *p;

cout << "The square of " << *p << " = " << (*p * *p);

return 0;

}



Функции



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



- Общая форма определения функции такова:

возвращаемыйТип имяФункции(<список параметров>)

// обязателен тип возвращаемого значения

{

< объявление данных >



< тело функции>

return возвращаемоеЗначение; // - если возвращаемыйТип не void

}



- Выход из функции осуществляется по оператору return. Void-функции

могут не возвращать значения.



Список параметров:

[const] тип1 параметр1, [const] тип2 параметр2, ...



- Ключевое слово const предохраняет передаваемые по ссылке аргументы от случайного изменения.



Программа USERINFO.CPP иллюстрирует использование модификатора

// const в списке параметров

*/

struct userInfo

{

int age;

char name[150];

};

void processUserInfo(/*const*/ userInfo &ui)

// при снятии комментария будет сообщение об ошибке,

// поскольку модификатор const запрещает изменение параметра

{

if ( ui.age < 18 ) {

cout << "Значение параметра меньше 18" << endl;

return;

}

if ( ui.age < 21 )

ui.age = 21;

}

/*

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

возврТип имяФункции(<список параметров>);

При объявлении функции имена параметров могут быть опущены.

- Передача аргумента по ссылке позволяет функции изменять значение переданного аргумента и экономит память, так как при этом не создается локальная копия аргумента:

[const] тип1& параметр1, [const] тип2& параметр2, ...

void foo(int &); // - объявление функции - это ее прототип

int main()

{

int value = 5;

foo(value);

cout << value << endl;

return 0;

}

void foo(int &parm) // - определение функции вызов параметра по ссылке

{

++parm;

}

/* Результаты:

6

*/



void foo(int *); // пердача указателя

int main()

{

int value = 5;

foo(&value); // передается адрес

cout << value << endl;

getch();

foo(&value);

cout << value << endl;

getch();

return 0;

}

void foo(int* parm)

{

++*parm; // параметр - указатель

}

/* Результаты:

6

7



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



Программа LOCAL.CPP знакомит с понятием локальной переменной

- Ключевое слово static позволяет объявить переменную как статическую.

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



Программа STATIC.CPP знакомит с понятием статической локальной переменной

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

#define min(n1, n2) (((n1) < (n2)) ? (n1) : (n2))

#define max(n1, n2) (((n1) > (n2)) ? (n1) : (n2))

double num1 = 50, num2 = 5, rslt;

rslt = min(num1 / 2, num2 * 2);



- При объявлении функции с модификатором inline компилятор заменяет вызов функции ее телом. В этом смысле эти функции похожи на макросы.

Отличие состоит в том, что встроенные функции выполняют проверку типов данных.

Программа INLINE.CPP, иллюстрирующая применение встроенной функции

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

Программа DEFARGS.CPP, иллюстрирующая применение аргументов по умолчанию

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

Пример программы FACTOR.CPP, использующей рекурсивную функцию

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

Программа OVERLOAD.CPP, иллюстрирующая перегрузку функции

ТИПОВЫЕ ВОПРОСЫ С ОТВЕТАМИ



Можно ли в С++ объявлять вложенные функции?

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

В каких случаях нужно использовать статические глобальные переменные?

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

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

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

ПРАКТИКУМ



Контрольные вопросы



1. Каков будет результат работы следующей программы? Что вы можете сказать по поводу функции swap?

*/



# include <iostream.h>

void swap(int i, int j)

{

int temp = i;

i = j;

j = temp;

}

int main()

{

int a = 10, b = 3;

swap (a, b);

cout << "а = " << a << " and b = " << b;

return 0;

}

/*

2. Каков будет результат работы следующей программы? Что вы можете сказать по поводу еще одной функции swap?

*/

#include <iostream.h>

void swap(int &i, int &j)

{

int temp = i;

i = j;

j = temp;

}

int main()

{

int a = 10, b = 3;

swap (a, b);

cout << "а = " << a << " and b = " << b;

return 0;

}

/*

3. Что за проблема возникнет со следующими перегруженными функциями?

*/

void inc(int &i)

{

i = i + 1;

}

void inc(int &i, int diff = 1)

{

i = + diff;

}

/*

4. Найдите ошибку в функции.

/*

double volume(double length, double width = 1, double height)

{

return length * width * height

}

/*

5. Найдите ошибку в функции.

*/

void inc (int &i, int diff = 1)

{

i = I + diff;

}

/*

6. В этой программе есть ошибка. Что это за ошибка и как ее исправить?

*/

# include<iostream.h>

int main()

{

double x = 5.2;

cout << x << " ^ 2 = " << sqr(x);

return 0;

}

double sqr( double х)

{ return x * x; }

/*

7. Попробуйте в функции вычисления факториала использовать операцию ?: .

Массивы



// Листинг 6.1. исходный текст программы AVERAGE1.CPP

// Программа иллюстрирует использование одномерных массивов

// при расчете среднего значения.

#include <iostream.h>

const int MAX = 0x1FFF; //64K/8 - максимальный размер массива типа double ***

int main()

{

double array[MAX]; // объявление одномерного массива ***

int num_elem;

// Ввод количества обрабатываемых данных

do

{

cout << "Введите размер массива данных [2 ... "

<< MAX << "]: ";

cin >> num_elem;

cout << endl;

} while (num_elem < 2 || num_elem > MAX);

// Ввод данных

for (int ix = 0; ix < num_elem; ix++)

{

cout << "массив[" << ix << "]: ";

cin >> array[ix];

}

// Расчет среднего значения

double sum = 0;

for (ix = 0; ix < num_elem; ++ix)

sum += array[ix];

cout << endl << "Среднее: " << sum / num_elem << endl;

return 0;



/*

- При объявлении одномерных массивов им можно присвоить начальные значения. Список ИНИЦИАЛИЗАЦИИ должен быть заключен в фигурные скобки, а элементы в нем должны быть разделены запятыми. Можно при инициализации задать данных МЕНЬШЕ, чем размер массива. В этом случае компилятор автоматически присвоит нулевые значения тем элементам, которые вы не инициализировали. И вдобавок, если вы не укажете размерность инициализируемого массива, она будет определена по количеству элементов в списке инициализации.

*/

// Листинг 6.2. исходный текст программы AVERAGE2.CPP

// Программа иллюстрирует использование одномерных массивов

// при расчете среднего значения.

// Данные задаются при инициализации массива.

#include <iostream.h>

const int MAX = 10; //50

int main()

{

double array[MAX] = { 12.2, 45.4, 67.2, 12.2, 34.6, 87.4,

83.6, 12.3, 14.8/*, 55.5*/ };

int num_elem = MAX;

//double array[] = { 12.2, 45.4, 67.2, 12.2, 34.6, 87.4,

// 83.6, 12.3, 14.8, 55.5 };

//int num_elem = sizeof(array) / sizeof(array[0]);

double sum = 0;

for (int ix = 0; ix < num_elem; ++ix)

{

sum += array[ix];

cout << "массив[" << ix << "]: " << array[ix] << endl;

}

cout << endl << "Среднее: " << sum / num_elem << endl;

return 0;

}

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

(Листинг 6.3а. исходный текст программы MINMAX.CPP)

(Листинг 6.3. исходный текст программы MINMAX.CPP)

СОРТИРОВКА массива - ПРИМЕР в файле list6_4cpp.

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

  • ПОИСК в массиве

означает нахождение в массиве элемента, совпадающего с заданным значением. Методы поиска делятся на две группы: для упорядоченных и неупорядоченных массивов. Метод линейного поиска применяется для неупорядоченных массивов, а метод двоичного поиска - для сортированных массивов. (Пример - list6_5.cpp)

Рассмотрим понятия ПАРАМЕТРОВ-ФУНКЦИЙ и УКАЗАТЕЛИ НА ФУНКЦИИ:

(Листинг 6.5. исходный текст программы SEARCH.CPP)

БИБЛИОТЕЧНЫЕ ФУНКЦИИ ПОИСКА и СОРТИРОВКИ в непрерывных массивах:

*/

void *bsearch(const void *key, const void *base, size_t nelem,

size_t width, int (*fcmp)(const void*, const void*));

// key - указатель на искомый элемент,

// возвращаемое значение - указатель на элемент (0 - не найден)

// base - базовый адрес массива

// num - число элементов в массиве

// width - размер элемента

// fcmp - указатель на функцию сравнения элементов массива

// Функция возвращает указатель на элемент, а не значение индекса элемента

// Если элемент не обнаружен, возвращается 0.

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

index = (searchRslt - arrayBase) / sizeof(arrayBase[0]);



void *lfind(const void *key, const void *base, size_t *num,

size_t width, int (*fcmp)(const void *, const void*));

void *lsearch(const void *key, void *base, size_t *num,

size_t width, int (*fcmp)(const void *, const void *));

// - если нет элемента, то он вставляется, поэтому возвращаемое значение

// всегда не ноль.

void qsort(void *base, size_t nelem,

size_t width, int (*fcmp)(const void *, const void *));

/*

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

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

Пример работы с двумерным массивом:

(Листинг 6.6. Исходный текст программы MATRIX1.CPP)

- При объявлении многомерных массивов им можно присвоить начальные значения. Список ИНИЦИАЛИЗАЦИИ должен быть заключен в фигурные скобки, а элементы в нем должны быть разделены запятыми. Можно при инициализации задать данных меньше, чем размер массива, В этом случае компилятор автоматически присвоит нулевые значения тем элементам, для которых вы не указали начальные значения:

(Листинг 6.7. Исходный текст программы MATRIX2.CPP.)

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

(Листинг 6.8. Исходный текст программы MATRIX3.CPP)

Строки и управление вводом/выводом

Здесь подробнее рассматриваются операции консольного ввода/вы­вода. C++, как и его предок — язык С — не определяет операции ввода/вывода как часть языка, а выносит операции консольного ввода/вывода в библиотеки ввода/вывода. Такие библиотеки в основном предназначены для работы в MS-DOS. Рассмотрим небольшую выборку функций ввода/вывода, объявляемых в заголовочных файлах STDIO.H и IOSTREAM.H.

Сегодня мы рассмотрим следующие темы:

    Форматированный потоковый вывод

    Потоковый ввод

    Функция printf

    Строки в C++

    Ввод строк

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

    Присвоение значений строкам

    Определение длины строки

    Конкатенация строк

    Сравнение строк

    Преобразование строк

    Перестановка символов в строке в обратном порядке

    Поиск символа

    Поиск подстроки

Форматированный потоковый вывод

C++ имеет целое семейство гибких библиотек функций ввода/вывода. Разработчикам языка было ясно, что функции ввода/вывода из STDIO.H, унаследованные из С, имеют ограничения при работе с классами (вы узнаете больше о классах в главе 8)., В результате в C++ было введено понятие потоков. Вспомним, что потоки, которые уже существовали в С, означают последовательность данных, передаваемых из одной части компьютера в дру­гую. В программах, рассматриваемых ранее, вы видели операцию помещения в поток «, например — в стандартный поток вывода, cout. Встречалась вам и операция извлечения из потока », применяемая к стандартному потоку ввода, cin. В этом разделе мы познакомимся с потоковыми функциями width и precision, используемыми при форматировании вывода. Библиотеки потоков C++ содержат большое количество таких функций, позволяющих настроить ваш вывод.

Функция width задает ширину поля вывода. Общая форма использования функции width с потоком cout:

cout.width (widthOf Output);

Функция precision определяет количество значащих цифр после точки для чисел с пла­вающей точкой. Общая форма использования функции precision с потоком cout:

cout.precision(numberOfDigits) ;

Обратимся к примеру, программе OUT1.CPP, исходный текст которой при­веден в листинге 1. Программа, в которую ничего не вводится, просто выво­дит форматированные целые числа, числа с плавающей точкой и символы с использованием функций width и precision.

Листинг 1. Исходный текст программы OUT1.CPP

01

// Программа иллюстрирует потоковый форматированный вывод в C++

02

// с использованием функций width и precision

03

#include <iostream.h>

04

05

int main()

06

{

07

int anInt = 67;

08

unsigned char aByte = 128;

09

char aChar = '@';

10

float aSingle = 355.1112;

11

double aDouble = 1.131112e+002;

12

13

// Вывод простых выражений

14

cout.width(3); cout << int(aByte) << " + ";

15

cout.width(2); cout << anInt << " = ";

16

cout.width(3); cout << (aByte + anInt) << endl;

17

18

cout.precision(3); cout << aSingle << " / ";

19

cout << aDouble << " =";

20

cout.width(7); cout.precision(4); cout << (aSingle / aDouble) << endl;

21

22

cout << "Символьная переменная aChar: "

23

<< aChar << endl;

24

return 0;

25

}

Пример программной сессии:

Введите три числа через пробел: 123

Сумма чисел = 6

Среднее этих чисел = 2

Введите три символа: ABC

Вы ввели символы 'A', 'B', 'C'

Введите число, символ, и число: 12A34.4

Вы ввели 12 A 34.4

Введите символ, число и символ: A3.14Z

Вы ввели A 3.14 Z

В программе из листинга 2 объявляется четыре переменных типа double и три переменных типа char. Оператор вывода в строке 10 предлагает вам ввести три числа. Оператор ввода в строке 11 помещает введенные вами числа в переменные х, у и z. He забывайте, что при вводе чисел их нужно разделять пробелами. Либо вводите каждое число с новой строки. Первое введенное вами число будет помещено в переменную х, второе — в у, а третье окажется в переменной z. Данные в переменные заносятся в том порядке, в котором пере­менные перечислены в операторе ввода в строке 11. Оператор в строке 12 вычисляет сумму значений переменных х, у и z. Оператор вывода в строках 13 и 14 выводит сумму и среднее значение введенных вами величин.

Оператор вывода в строке 15 предлагает вам ввести три символа. Оператор (ввода в строке 16 последовательно размещает введенные символы в перемен­ных с1, с2, с3. Использовать пробел для разделения вводимых символов не обязательно. Например, вы можете ввести данные и таким образом: 1А2, Bob и 1 D d. Оператор вывода в строках 17—19 выводит введенные вами символы, разделенные пробелами и заключенные в одинарные кавычки.

Оператор вывода в строке 20 предлагает вам ввести число, символ и число. Оператор ввода в строке 21 помещает ваши данные в переменные х, с1 и у. Пробел-разделитель здесь нужен только в том случае, если символ может быть интерпретирован как часть числа. Например, если вам нужно ввести число 12, символ «точка» и число 55, вам нужно набрать на клавиатуре 12 . 55. Вводимый символ «точка» лучше «заключить» в пробелы, чтобы быть уверенным, что поток вода не воспримет эту точку как точку, разде­ляющую в вещественном числе целую и дробную части. Оператор вывода в строке 22 выводит введенные вами данные разделенные пробелами.

Оператор вывода в строке 23 предлагает вам ввести символ, число и символ. Оператор ввода в строке 24 последовательно размещает введенные значения в переменных с1, х, с2. Пробел-разделитель здесь нужно исполь­зовать только в том случае, если символ может быть интерпретирован как часть числа. Например, если вам нужно ввести символ «-», число 12 и цифру 0, вам нужно набрать на клавиатуре 12 0. Оператор вывода в строке 25 выводит введенные вами данные, разделяя их пробелами.

Функция printf

Просматривая программы, написанные разными людьми, вы часто можете встретить функцию printf. Этот стандартный оператор вывода пришел из языка С. Так как C++ является расширением С, эта функция поддерживается и в этом языке. Многие программисты до сих пор предпочитают использовать старую функцию printf, а не потоки ввода/вывода C++. Вот почему вам эта функция наверняка уже знакома. Но, помимо этого, эта функция имеет не­сколько очень мощных возможностей, и в ряде случаев она оказывается удоб­нее функций потоков. Прототип функций можно найти в заголовочном файле STDIO.H.

Функция printf

Общая форма объявления функции printf:

int printf(const char *format[, argument,... ]);

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

Функция printf является очень мощной функцией с богатыми возмож­ностями форматирования вывода. В качестве первого шага в освоении ее возможностей рассмотрим Esc-последовательности, позволяющие представ­лять специальные символы. Esc-последовательность начинается с символа «\» — «обратная косая черта». Esc-коды представлены в таблице 1.

Таблица 1. Еsс - последовательности

Последовательность

Десятичное значение

Шестнадцатеричное значение

Название

7

0х07

Звонок

\b

8

0х08

Возврат назад

\f

12

0х0С

Перевод страницы

\n

10

0х0А

Новая строка

13

0x0D

Возврат каретки

\t

9

0х09

Табуляция

\v

11

0х0В

Вертикальная табуляция

\\

92

0х5С

Обратная черта

\'

44

0х2С

Апостроф

\"

34

0х22

Кавычка

\?

63

0х3 F

Знак вопроса

\0

Восьмеричное число, от 1 до 3 цифр

\XHHH и \xhhh

0xhhh

Шестнадцатеричное число

Функция printf имеет специальные форматирующие спецификации (сим­волы) для вывода переменных. Общий вид этих спецификаций таков:

% [flags] [width] [.precision] [F | N | h | l | L ] <символ типа>

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

Таблица 7.2. Значения флагов строки формата функции printf

Символ

Назначение

-

Выравнивать вывод по левому краю поля

+

Всегда выводить знак числа

Пробел

Выводить пробел перед положительным числом и знак минус — перед отрицательным

#

Не влияет на вывод десятичных целых, для шестнадцатеричных чисел выводит префикс 0х или 0Х, перед восьмеричными целыми выводит ноль, десятичную точку для вещественных чисел.

Спецификация width определяет минимальное количество выводимых символов. Если необходимо, используются заполнители — пробелы или нули. Когда значение для width начинается с нуля, printf использует в качестве заполнителей нули, а не пробелы. Если в качестве значения для width используется универсальный символ *, а не число, то printf подставляет на место этого символа значение, которое должно содержаться в списке аргументов. Это значение ширины поля должно предшествовать выводимому значению. Ниже приведен пример вывода числа 2, занимающего три позиции, согласно значе­нию второго аргумента printf:

printf("%*d", 3, 2);

Спецификатор precision определяет максимальное количество выводимых цифр. В случае целого числа он определяет минимальное количество выво­димых символов. Для precision также можно применить символ *, вместо которого будет подставлено значение из списка аргументов. Это значение точности представления должно предшествовать выводимому значению. Ниже приведен пример вывода числа с плавающей точкой 3.3244 с использованием десяти символов, как это задано вторым аргументом printf:

printf("%7.*f", 10, 3.3244);

Символы F, N, h, l и L являются символами размера, переопределяющими размер по умолчанию. Символы F и N применяются с указателями, far и near соответственно. Символы h, l, и L используются для указания соответ­ственно типов short int, long или long double.

Символам типа данных должен предшествовать форматирующий символ %. В таблице 7.2 мы показали возможные значения флагов форматирующей строки printf. Символы типов данных перечислены в таблице 7.3.

Таблица 3. Символы типов данных строки формата функции printf

Тип данных

символ типа

результат

Символ

c

Один символ

d

Десятичное целое со знаком

i

Десятичное целое со знаком

O

Восьмеричное целое без знака

N

Десятичное целое без знака

X

Шестнадцатеричное целое без знака; набор цифр - 0123456789abcdef

X

Шестнадцатеричное целое без знака; набор цифр - 0123456789ABCDEF

Указатель

P

Для указателей near выводит только смещение в виде: 0000. Указатели far отображаются в виде: SSSS:0000

Указатель на целое

N

Вещественное

F

Выводит величину со знаком в формате [-]dddd.dddd

E

Выводит вещественную величину со знаком в экспоненциальном формате [-]d.dddde[+|-]ddd

Е

Выводит вещественную величину со знаком в экспоненциальном формате [-]d.ddddE[+|-]ddd

G

Выводит вещественную величину со знаком в формате f или е в зависимости от ее значения и заданной точности

G

Выводит вещественную величину со знаком в формате F или Е в зависимости от ее значения и заданной точности

Указатель

S

Выводит строку символов, пока не встретит нуль-терминатор строки

Разберем небольшой пример. Программа OUT2.CPP, исходный код ко­торой приведен в листинге 3, создана на основе программы OUT1.CPP. В этой программе используется форматированный вывод с использованием функции printf. Программа выводит те же числа, что и OUT1.CPP, используя три различных набора спецификаций преобразования.

Листинг 3. Исходный текст программы OUT2.CPP в файле List7-3.CPP

01

// Программа, использующая printf для форматирования вывода

02

03

#include <stdio.h>

04

05

int main()

06

{

07

int anInt = 67;

08

Unsigned char aByte = 128;

09

char aChar = '@';

10

Float aSingle = 355.0;

11

Double aDouble = 1.130e+002;

12

13

Printf("%3d + %2d = %3d\n",

14

aByte, anInt, aByte + anInt );

15

16

Printf("Вывод использует спецификации преобразования %%lf :\n");

17

Printf(" %6.4f / %10.4lf = %7.5lf\n",

18

aSingle, aDouble, aSingle / aDouble );

19

20

Printf("Вывод использует спецификации преобразования %%le :\n");

21

printf(" %6.4e / %6.4le = %7.5le\n",

22

aSingle, aDouble, aSingle / aDouble );

23

24

printf("Вывод использует спецификации преобразования %%lg :\n");

25

printf(" %6.4g / %6.4lg = %7.5lg\n",

26

aSingle, aDouble, aSingle / aDouble );

27

28

printf("Символьная переменная aChar: %c\n", aChar);

29

printf("ASCII-код %c: %d\n", aChar, aChar);

30

return 0;

31

}

Пример вывода программы из листинга 3:

128 + 67 = 195

Вывод использует спецификации преобразования %lf :

355.0000 / 113.0000 = 3.14159

Вывод использует спецификации преобразования %le :

3.5500e+02 / 1.1300e+02 = 3.14159e+00

Вывод использует спецификации преобразования %lg :

355 / 113 = 3.1416

Символьная переменная aChar: @

ASCII-код @: 64

В программе из листинга 3 объявляется целый набор переменных раз­личных типов. Оператор вывода в строках 13 и 14 выводит целые, используя спецификацию формата %d. В таблице 4 приведены результаты действия спецификаций преобразования из строки 13. Обратите внимание на то, что первая переменная была преобразована из типа unsigned char в тип integer.

Таблица 4. Результат действия спецификаций форматирования в функции printf из строки 13

Спецификация формата

Переменная

Тип данных

Тип после преобразования

%3d

aByte

unsigned char

Int

%2d

anInt

int

Int

%3d

aByte + anInt

int

Int

Оператор вывода в строке 17 выводит переменные aSingle, aDouble и вы­ражение aSingle / aDouble, используя спецификации преобразования %6.4f, %6.41f и % 7.51f. Точность представления задается ими равной 4, 4 и 5 цифрам, а минимальная ширина поля 6, 6 и 7 цифрам соответственно. Две последних спецификации осуществляют преобразование величин двойной точности.

Оператор вывода в строке 21 подобен оператору из строки 17. Отличие состоит в том, что используется е-формат вместо f-формата. Соответственно три значения выводятся в экспоненциальном формате.

Оператор из строки 25 также похож на оператор из строки 17. Основное отличие состоит в том, что вместо f-формата используется g-формат. В ре­зультате первые два числа выводятся без дробной части, поскольку они яв­ляются целыми величинами.

Оператор вывода в строке 28 выводит содержимое переменной aChar по формату %с. Оператор вывода в строке 29 выводит ту же переменную aChar дважды, первый раз как символ, а второй раз как целое (или, если быть точным, выводится ASCII-код символа). Для этого используются специфика­ции преобразования %с и %d соответственно.

Массивы символов в C++

В C++ имеется специальный класс для работы со строками, которого, конечно, не было в языке С. В С строки вводились как массивы символов, ограниченные нуль-символом (ASCII-код которого равен нулю), поэтому боль­шое количество программ, написанных на С, используют символьные мас­сивы. Более того, и в C++, несмотря на то, что он имеет класс для работы со строками, находится применение массивам символов. Поэтому термин «строка» имеет два значения: строка в смысле C++ и строка как массив символов. Весь этот раздел будет посвящен тому, как нужно и не нужно использовать символьные массивы.

Символ '\0' также называют нуль-терминатором. Строки, оканчивающиеся нуль-терминатором, называют еще ASCIIZ-строками, где символ Z обозначает ноль — ASCII-код нуль-терминатора. Еще этот символ называют NUL-символом, поскольку этот термин является его именем в ASCII.

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

Ввод строк

В программах, которые мы рассматривали, операторы потокового вывода выводили строковые константы; C++ поддерживает потоковый вывод для строк как специального не-предопределенного типа данных. (Можно сказать, что это было сделано по требованию масс.) Операции и синтаксис для вывода строковых переменных остаются прежними. При вводе строк операция из­влечения из потока » не всегда будет работать так, как вы ожидаете, поскольку строки часто содержат пробелы, которые игнорируются оператором ввода; поэтому вместо оператора ввода вам нужно использовать функцию getline. Эта функция вводит заданное количество символов.

Функция getline

Перегруженная функция getline объявляется следующим образом:

istreams getline( signed char *buffer,

int size,

char delimiter = '\n') ;

istreams getline( unsigned char *buffer,

int size,

char delimiter = '\n') ;

istream& getline( char *buffer,

int size,

char delimiter = '\n') ;

Параметр buffer является указателем на строку, в которую поме­щаются вводимые символы. Параметр size задает максимальное коли­чество вводимых символов. Параметр delimeter определяет символ-ог­раничитель, при появлении которого ввод символов прекращается прежде, чем будут введены все size символов. Параметр delimeter имеет аргумент по умолчанию, равный '\n'. В случае ввода символов с клавиатуры этот символ появляется в потоке ввода при нажатии клавиши

Пример

#include <iostream.h> //см. файл Ex01.cpp

int main()

{

char name[80] ;

cout « "Enter your name: ";

cin.getline(name, sizeof(name) - 1);

cout « "Hello " « name « ", how are you?";

return 0;

}

Функции, объявленные в STRING. H

Стандартная библиотека для работы со строками содержит много полез­ных функций (объявляемых в STRING.H), разработанных коллективными усилиями многих программистов на С. В файлах заголовка STDIO.H и IOS-TREAM.H также имеются прототипы строковых функций. Комитетом ANSI/ISO C++ предложен класс для работы со строками. Строки этого класса больше похожи на строки в языках Pascal и BASIC. (Мы познакомимся с классами в День 8, а со строковым классом в День 11.) Этот раздел будет посвящен рассмотрению некоторых (ни в коей мере не всех) функций, объ­явленных в STRING.H.

Некоторые функции из STRING.H имеют несколько версий. Дополни­тельные версии этих функций, имеющих в имени префиксы _f, f или _ работают с указателями типа far. Этих версий вы не встретите в плоской, 32-битной модели памяти компилятора Borland.

Присвоение значений строкам

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

Инициализация строки

Общий метод инициализации строки:

char stringVar[stringSize] = stringLiteral;

Пример

char a3tring[81] = "Borland C++ 5 in 21 days";

char Named = "Rene Kinner";

Второй способ присвоить значение строке — это вызвать функцию, ко­торая копирует содержимое одной строки в другую, — не забывая при этом и нуль-символ. Эта функция называется strcpy. Она предполагает, что ко­пируемая строка оканчивается символом NUL и прекращает копирование, как только встретит этот символ.

Функция strcpy

Прототип функции strcpy таков:

char* strcpy(char *target, const char *source);

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

Пример

char name[41] ;

strcpy(name, "Borland C++ 5");

Переменная name содержит строку "Borland C++ 5".

Функция strdup

Функция strdup копирует одну строку в другую, при этом отводит не­обходимое количество памяти для целевой строки.

Прототип функции strdup таков:

char* strdup(const char *source);

Функция копирует строку source и возвращает указатель на стро­ку-копию.

Пример

char *string1 = "Монархия в Испании";

char *string2;

string2 = strdup(string1);

После того, как будет отведено необходимое количество памяти для строки string2, строка string1будет скопирована в строку string2.

Функция strncpy

Библиотека строковых функций предлагает также функцию strncpy, ко­пирующую заданное количество символов из одной строки в другую.

Прототип функции strncpy таков:

char * strncpy(char *target, const char *source, size_t num);

Функция копирует num символов из строки source в строку target. Функция не выполняет ни усечение, ни заполнение строки.

Пример

char str1[] = "Pascal";

char str2[] = "Hello there";

strcnpy(strl, str2, 5);

Переменная strl содержит строку "Hellol". Заметьте, что символ ‘l’ строки-приемника, следующий за скопированной частью строки, сохра­нился.

Определение длины строки

При работе со строками часто бывает нужно знать длину строки.

Функция strlen

Функция strlen возвращает количество символов в строке, в которое не включается нуль-терминатор.

Прототип функции strncpy таков:

size_t strlen (const char *string) ,

Функция strlen возвращает длину строки string. size_t — это имя, приписанное типу unsigned int оператором typedef.

Пример

char str[] = "1234567890";

size_t i;

i = strlen(str),

Переменной i будет присвоено значение 10.

Конкатенация строк

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

Присоединить одну строку к другой можно функцией strcat.

Функция strcat

Конкатенация строк означает их последовательное присоединение друг к другу.

Прототип функции strcat таков:

char *strcat(char *target, const char *source) ;

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

Пример

char string[81] ;

strcpy(string, "Turbo");

strcat (string, " C++");

Переменная string содержит строку "Turbo C++".

Функция strncat

Функция strncat добавляет к содержимому целевой строки указанное количество символов из строки-источника.

Прототип функции strcat :

char *strncat(char *target, const char *source, size_t num);

Функция добавляет к содержимому целевой строки num символов из строки-источника и возвращает указатель на целевую строку.

char strl[81] = "Hello I am ";

char str2[41] = "Keith Thompson";

strncat(strl, str2, 5);

Переменная strl теперь содержит строку "Hello I am Keith".

Пример использования функций getline, strlen и strcat в файле List7_4.cpp (исходный код программы STRING.CPP). Программа выполняет следующие задачи:

    Предлагает вам ввести строку; ввод не должен превышать 40 символов

    Предлагает вам ввести вторую строку; ввод не должен превышать 40 символов

    Выводит число символов, содержащихся в каждой строке

    Присоединяет вторую строку к первой

    Выводит результат конкатенации

    Выводит длину объединенной строки

    Предлагает вам ввести символ для поиска

    Предлагает вам ввести символ для замены

    Выводит содержимое объединенной строки после замены символа

Сравнение строк

Поскольку строки являются массивами символов, вы не можете приме­нить операцию сравнения для проверки равенства двух строк. Библиотека функций STRING.H предлагает набор функций для сравнения строк. Эти функции сравнивают символы двух строк, используя для этого ASCII-коды символов. Это функции strcmp, stricmp, strncmp и strnicmp.

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

Функция strcmp

Функция strcmp выполняет сравнение двух строк с учетом регистра сим­волов.

Прототип функции strcmp:

int strcmp(const char *strl, const char *str2);

Функция сравнивает строки strl и str2. Возвращает в качестве ре­зультата сравнения целую величину:

< 0 когда strl меньше, чем str2;

= 0 когда strl равна str2;

> 0 когда strl больше, чем str2.

Пример

char stringl[] = "Borland C++";

char string2[] = "BORLAND C++";

i = strcmp(string1, string2);

В последнем операторе переменной i присваивается положительное значение, так как string1 больше string2 (ASCII-коды символов в ниж­нем регистре больше ASCII-кодов символов в верхнем.)

Функция stricmp

Функция stricmp выполняет сравнение двух строк, не учитывая регистра символов.

Прототип функции stricmp:

int stricmp(const char *strl, const char *str2);

Функция сравнивает строки strl и str2, не делая различия между символами в нижнем и верхнем регистре. Возвращает в качестве ре­зультата сравнения целую величину:

< 0 когда strl меньше, чем str24

= 0 когда strl равна str24

> 0 когда strl больше, чем str2.

Пример

char string1[] = "Borland C++";

char string2[] = "BORLAND C++";

int i = strcmp(string1, string2);

В последнем операторе переменной i присваивается значение 0, так как string1 и string2 отличаются друг от друга только регистром сим­волов.

Функция strncmp выполняет сравнение заданного количества символов двух строк с учетом регистра символов.

Функция strncmp

Прототип функции strncmp:

int strncmp(const char *strl, const char *str2, size_t num);

Функция сравнивает первые num символов строк strl и str2. Воз­вращает в качестве результата сравнения целую величину:

< 0 когда strl меньше, чем str2;

= 0 когда strl равна str2;

> 0 когда strl больше, чем str2.

Пример

char string1[] = "Borland C++";

char string2[] = "Borland Pascal";

i = stricmp(string1, string2, 9);

В последнем операторе переменной i присваивается отрицательное значение, так как значение "Borland С" меньше, чем "Borland Р".

Функция strnicmp

Функция strnicmp выполняет сравнение заданного количества символов двух строк без учета регистра символов.

Прототип функции strnicmp :

int strnicmp(const char *strl, const char *str2, size_t num);

Функция сравнивает первые num символов строк strl и str2, не делая различия в регистре символов. Возвращает в качестве результата сравнения целую величину:

< 0 когда strl меньше, чем str2;

= 0 когда strl равна str2;

> 0 когда strl больше, чем str2.

Пример

char string1[] = "Borland C++";

char string2[] = "BORLAND Pascal";

i = strnicmp(string1, string2, 7);

В последнем операторе переменной i присваивается значение 0, так как подстрока "Borland" отличается в этих строках только регистром.

Рассмотрим пример программы, в которой применяются функции срав­нения строк. Программа из листинга 5 объявляет массив строк и присваивает им значения. Затем программа выводит исходный массив, сортирует его и выводит значения строк отсортированного массива. |

(см. List7_5.cpp - Исходный текст программы STRING2.CPP)

Преобразование строк

Функция strlwr

Прототип функции strlwr:

char* strlwr (char *source)

Функция преобразует символы верхнего регистра в символы ниж­него регистра в строке source. Другие символы не затрагиваются. Функ­ция возвращает указатель на строку source.

Пример

char str[] = "HELLO THERE";

strlwr(str);

Переменная str теперь содержит строку "hello there".

Функция strupr

Прототип функции strupr:

char* strupr(char *source)

Функция преобразует символы нижнего регистра в символы верх­него регистра в строке source. Другие символы не затрагиваются. Функ­ция возвращает указатель на строку source.

Пример

char str[] = "Borland C++";

strupr(str);

Переменная str теперь содержит строку "BORLAND С ++".

Обращение строк

Библиотека STRING.H предлагает функцию strrev для записи символов в строке в обратном порядке.

Функция strrev

Прототип функции strrev:

char* strrev(char *str)

Функция обращает порядок символов в строке str и возвращает указатель на строку str. char str[] = "Hello";

strrev(str) ;

cout « str;

Будет выведено "olleH".

Рассмотрим программу, которая манипулирует символами в строке. List7_6.cpp показывает исходный текст программы STRING3.CPP. Программа выполняет следующие задачи:

    Запрашивает у вас ввод строки

    Отображает ваш ввод

    Выводит вашу строку в нижнем регистре

    Выводит вашу строку в верхнем регистре

    Отображает символы, которые вы ввели, в обратном порядке

    Выводит сообщение, что ваш ввод не содержит символов верхнего реги­стра, если это так

    Выводит сообщение, что ваш ввод не содержит символов в нижнем ре­гистре, если это так

    Выводит сообщение, что ваша строка симметрична, если это так

Поиск символов

Библиотека STRING.H предлагает ряд функций для поиска символов в строках. Это функции strchr, strrchr, strspn, strcspn и strpbrk. Они осущест­вляют поиск в строках символов и простых символьных шаблонов.

Функция strchr

Функция strchr определяет первое вхождение символа в строку.

Прототип функции strchr:

char* strchr(const char *target, int c)

Функция находит первое вхождение символа с в строку target. Функ­ция возвращает указатель на символ в строке target, который соответст­вует заданному образцу с. Если символ с в строке не обнаруживается, функция возвращает 0.

Пример

char str[81] = "Borland C++";

char *strPtr;

strPtr = strchr(str, '+');

Указатель strPtr теперь содержит адрес подстроки "++" в строке str.

Функция strrchr

Функция strrchr определяет последнее вхождение символа в строке.

Прототип функции strrchr:

char* strrchr(const char *target, int c)

Функция находит последнее вхождение символа с в строку target. Функция возвращает указатель на символ в строке target, который соответствует заданному образцу с. Если символ с в строке не обнару­живается, функция возвращает 0.

Пример

char str[81] = "Borland C++ is here";

char* strPtr;

strPtr = strrchr(str, '+');

Указатель strPtr теперь указывает на подстроку "+ is here " в строке str.

Функция Strspn

Функция strspn возвращает число символов с начала строки, совпадаю­щих с любым символом из шаблона.

Прототип для функции strspn:

size_t strspn(const char *target, const char *pattern)

Функция strspn возвращает число символов от начала строки target, совпадающих с любым символом из шаблона pattern.

Пример

char str[] = "Borland C++ 5";

char sub>str[] = "narlBod";

int index;

index = strspn(str, sub>str);

Этот оператор присваивает 8 переменной index, потому что первые восемь символов из str содержатся в подстроке sub>str.

Функция strcspn

Функция strcspn просматривает строку и выдает число первых символов в строке, которые не содержатся в шаблоне.

Прототип функции strcspn:

size_t strcspn(const char* str1, const char* str2)

Функция strcspn просматривает строку str1 и выдает длину под­строки, отсчитываемой с начала строки, символы которой полностью отсутствуют в строке str2.

Пример

char strng[] = "The rain in Spain";

int i = strcspn(strng, " in");

Этот пример возвращает 3 (расположение первого пробела в строке strng) переменной i.

Функция strpbrk

Функция strpbrk просматривает строку и определяет первое вхождение любого символа из образца.

Прототип функции strpbrk:

char* strpbrk(const char* target, const char* pattern)

Функция strpbrk ищет в строке target первое вхождение любого сим­вола из образца pattern. Если символы из образца не содержатся в строке, функция возвращает 0.

Пример

char *str = "Hello there how are you";

char *sub>str = "hr";

char *ptr;

ptr = strpbrk(str, sub>str);

cout « ptr « endl;

Вы увидите на экране строку "here how are you", потому что 'h' встречается в строке str раньше, чем 'r'.

Поиск строк

Библиотека функций STRING.H предлагает для поиска подстроки в строке функцию strstr.

Функция strstr

Прототип функции strstr:

char* strstr(const char *str, const char *sub>str);

Функция ищет в строке str первое вхождение подстроки sub>str. Функ­ция возвращает указатель на первый символ найденной в строке str под­строки sub>str. Если строка sub>str не обнаружена в строке str, функция возвращает 0.

Пример

char str[] = "Hello there! how are you";

char sub>str[] = "how";

char *ptr;

ptr = strstr (str, sub>str);

cout « ptr « endl ;

Это приведет к выводу строки "how are you", поскольку в строке str , была обнаружена подстрока "how". Указатель ptr содержит адрес остатка первоначальной строки, начинающегося с подстроки "how".

Функция strtok

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

Подстроки иногда называются лексемами.

Прототип функции strtok:

char* strtok(char *target, const char * delimiters);

Функция разбивает строку на лексемы, согласно символам-ограни­чителям, заданным в параметре delimeters. В следующем примере по­казано, как работать с этой функцией и как получать лексемы, на которые была разбита строка. Функция strtok вводит символ '\0' после каждой лексемы. (Опять же не забудьте сохранить копию вашей строки в другой строковой переменной.)

Пример

#include <stdio.h> // см. файл Ex02.cpp

#include <string.h>

int main()

{

char *str = "(Base_Cost + Profit) * Margin";

char *tkn = "+*()";

char *ptr = str;

printf("%s\n", str);

// Первый вызов функции

ptr = strtok(str, tkn);

printf("Лексемы этой строки: %s", ptr);

while (ptr)

{

// Первый аргумент должен быть равен нулю

if ((ptr = strtok(0, tkn)) != 0)

printf (",%s", ptr);

}

printf("\n");

return 0;

}

В результате выполнения этой программы на экран выводятся сле­дующие строки:

(Base_Cost + Profit) * Margin

Лексемы этой строки: Base_Cost, Profit, Margin

Рассмотрим пример программы поиска символов и строк. Листинг 7 (List7_7.cpp) содержит исходный текст программы STRING4.CPP. Программа выполняет следующие задачи:

    Запрашивает у вас ввод основной строки

    Запрашивает строку поиска

    Предлагает вам ввести символ поиска

    Выводит линейку цифр и основную строку

    Выводит номер символа в основной строке, с которого начинается строка поиска *

    Выводит номер символа в основной строке, совпавшего с символом по­иска.

Основы объектно-ориентированного программирования СИНТАКСИС ОСНОВНЫХ КОНСТРУКЦИЙ

Объявление базовых классов

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

Базовый класс

Базовый класс определяется следующим образом (синтаксис):

class className

{

private:

<закрытые элементы-данные>

<закрытые конструкторы>

<закрытые функции-элементы>

protected:

<защищенные элементы-данные>

<защищенные конструкторы>

<защищенные функции-элементы>

public:

<открытые элементы-данные>

<открытые конструкторы>

<открытый деструктор>

<открытые функции-элементы>

};

Пример 1:

class point

{

protected:

double х;

double у;

public:

point(double xVal, double yVal);

double getX();

double getY();

void assign(double xVal, double yVal);

point& assign(point &pt);

};

Разделы класса

Классы С++ имеют три различных уровня доступа к своим элементам - как к данным, так и к функциям:

    Закрытые (частные) элементы

    Защищенные элементы

    Открытые элементы

К данным в закрытом разделе имеют доступ только функции-элементы класса.

Классам-потомкам запрещен доступ к закрытым данным своих 6азовых классов.

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

Существуют следующие правила для разделов класса:

    Разделы могут появляться в любом порядке.

    Один и тот же раздел можно определять несколько раз.

    Если не определен ни один раздел, компилятор (по умолчанию) объявляет все элементы закрытыми.

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

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

    Класс может иметь несколько конструкторов.

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

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

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

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

class point

{

protected:

double x;

double y;

public:

point(double xVal, double yVal);

double getX();

// другие функции-элементы

};

Определения конструктора и функций-элементов должны выглядеть так

point::point (double xVal, double yVal)

{

// операторы

}

double point::getX()

{

// операторы

}

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

В листинге 8.1 приведен исходный текст программы RECT.CPP. Программа предлагает вам ввести длину и ширину прямоугольника (в данном примере прямоугольник является объектом). Затем программа выводит значения длины, ширины и площади определенного вами прямоугольника.

Конструкторы

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

Общий вид (синтаксис) объявления конструктора:

class className

{

public:

className(); // конструктор по умолчанию

className(const className &c); // конструктор копии

className(<список параметров>); // остальные конструкторы

};

Пример 2:

// Замечание: Здесь только объявление класса без описания объявленных

// функций-параметров

class point

{

protected:

double x;

double y;

public:

point();

point(double xVal, double yVal);

point(const point &pt);

double getX();

double getY();

void assign(double xVal, double yVal);

point& assign(point &pt);

};

int main()

{

point p1;

point p2(10, 20);

point p3(p2);

p1.assign(p2);

cout << p1.getX() << " " << p1.getY() << endl;

cout << p2.getX() << " " << p2.getY() << endl;

cout << p3.getX() << " " << p3.getY() << endl;

return 0;

}

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

В С++ имеются следующие особенности и правила работы с конструкторами:

    Имя конструктора класса должно совпадать с именем класса.

    Нельзя определять тип возвращаемого значения для конструктора, даже тип void.

    Класс может иметь несколько конструкторов или не иметь их совсем.

    Конструктором по умолчанию является конструктор, не имеющий параметров, или конструктор, у которого все параметры имеют значения по умолчанию.

Рассмотрим два примера с фрагментами объявления конструкторов.

// класс с конструктором без параметров

class point1

{

protected:

double x;

double y;

public:

point1();

// другие функции-элементы

};

// конструктор класса имеет параметры со значениями по умолчанию

class point2

{

protected:

double x;

double y;

public:

point2(double xVal = 0, double yVal = 0);

// другие функции-элементы

};

    Конструктор копии создает объект класса на основе существующего объекта.

Например:

class point

{

protected:

double x;

double y;

public:

point();

point(double xVal = 0, double yVal = 0);

point(const point &pt);

// другие функции-элементы

};

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

point p1; // применяется конструктор по умолчанию

point p2(1.1, 1.3); // используется второй по счету конструктор

point p3(p2); // используется конструктор копии

Поскольку объект p1 объявляется без параметров, компилятор использует

конструктор по умолчанию. Объект p2 объявляется с двумя вещественными

аргументами, поэтому компилятор вызовет второй конструктор. Объект p3

при объявлении имеет параметром объект p2, поэтому компилятор вызовет

конструктор копии, чтобы создать новый объект из объекта p2.

ВНИМАНИЕ:

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

Не полагайтесь на поверхностный конструктор копии для классов имеющих

данные-указатели.

Деструкторы

Классы С++ могут содержать деструкторы, которые автоматически разрушают объекты класса.

Общий синтаксис объявления деструктора:

class className

{

public:

className(); // конструктор по умолчанию

// другие конструкторы

~className(); // объявление деструктора

// другие функции-элементы

};

Пример 3 на синтаксис обявления деструктора:

class String

{

protected:

char *str;

int len;

public:

String();

String(const String& s);

~String();

// другие функции-элементы

};

Деструкторы в С++ имеют следующие особенности и подчиняются следующим правилам:

    Имя деструктора должно начинаться со знака тильды (~), за которым должно следовать имя класса.

    Нельзя определять тип возвращаемого значения, даже тип void.

    Класс может иметь только один деструктор или ни одного. В последнем случае компилятор создаст деструктор по умолчанию.

    Деструктор не должен иметь параметров.

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

(см. LIST8-2.CPP)

Объявление иерархии классов

Производный класс

Общая форма (синтаксис) объявления производного класса:

class classname : [<спецификатор доступа>] parentClass

{

<дружественные классы>

private:

<закрытые элементы-данные>

<закрытые конструкторы>

<закрытые функции-элементы>

protected:

<защищенные элементы-данные>

<защищенные конструкторы>

<защищенные функции-элементы>

public:

<открытые элементы-данные>

<открытые конструкторы>

<открытый деструктор>

<открытые функции-элементы>

<дружественные функции и дружественные операции>

};

Пример 4 объявления класса Rectangle и класса-потомка Box:

class Rectangle

{

protected:

double length;

double width;

public:

Rectangle(double len, double wide);

double getLength() const;

double getWidth() const;

double assign(double len, double wide);

double calcArea();

};

class Вох : public Rectangle

{

protected:

double height;

public:

Box(double len, double wide, double height);

double getHeight () const;

assign(double len, double wide, double height);

double calcVolume();

};

(см. LIST8-3.CPP)

Виртуальные функции

Мы уже упоминали о полиморфизме - важной особенности объектно-

ориентированного программирования. Рассмотрим следующий пример (6):

#include <iostream.h>

class X

{

public:

double A(double x) { return x * x; }

double B(double x) { return A(x) / 2; }

};

class Y : public X

{

public:

double A(double x) { return x * x * x; }

};

int main ()

{

Y y;

cout << y.B(3) << endl;

return 0;

}

В классе X объявляются функции A и B, причем функция B вызывает функцию А. Класс Y, потомок класса X, наследует функцию B, но переопределяет функцию A. Цель этого примера - демонстрация полиморфного поведения класса Y. Мы должны получить следующий результат: вызов наследуемой функции X::B должен привести к вызову функции Y::A. Что же выдаст нам наша программа? Ответом будет 4.5, а не 13.5! В чем же дело? Почему компилятор разрешил выражение y.B(3) как вызов наследуемой функции X::B, которая, в свою очередь, вызывает X::A, а не функцию Y::A, что должно было бы произойти в случае полиморфной реакции класса?

Виртуальные функции объявляются следующим образом (синтаксис):

class className1

{

// функции-элементы

virtual returnType functionName(<список параметров>);

};

class className2 : public className1

{

// функции-элементы

virtual returnType functionName(<список параметров>);

};

Пример 7, показывающий, как при помощи виртуальных функций можно реализовать полиморфное поведение классов X и Y:

#include <iostream.h>

class X

{

public:

virtual double A(double x) { return x * x; }

double B (double x) { return A(x) / 2; }

};

class Y : public X

{

public:

virtual double A(double x) { return x * x * x; }

};

main()

{

Y y;

cout << y.B(3) << endl;

return 0;

}

Этот пример выведет вам правильное значение 13.5, потому что в результате вызова наследуемой функции X::B, вызывающей функцию A, в качестве функции A во время выполнения программы будет использована замещающая функция Y::A.

*** Правило виртуальной функции ***

Правило виртуальной функции гласит:

"Виртуальная однажды - виртуальна всегда".

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

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

#include <iostream.h>

class A

{

public:

A() {}

virtual void foo(char c)

{ cout << "virtual A::foo() returns " << c << endl; }

};

class B : public A

{

public:

B() {}

void foo(const char* s)

{ cout << "B::foo() returns " << s << endl; }

void foo(int i)

{ cout << "B::foo() retuzns " << i << endl; }

virtual void foo(char c)

{ cout << "virtual B::foo() returns " << c << endl; }

};

class C: public B

{

public:

C() {}

void foo(const char* s)

{ cout << "C::foo() returns " << s << endl; }

void foo(double x)

{ cout << "C::foo() returns " << x << endl; }

virtual void foo(char c)

{ cout << "virtual C::foo() returns " << c << endl; }

};

int main()

{

A Aobj;

B Bobj;

C Cobj;

Aobj.foo('A');

Bobj.foo('B');

Bobj.foo(10);

Bobj.foo("Bobj");

Cobj.foo('C');

Cobj.foo(144.123);

Cobj.foo("Cobj");

return 0;

}

В этом примере вводятся три класса - A, B и C - образующих линейную иерархию наследования. В классе A объявляется виртуальная функция foo(char).

Класс B объявляет свою версию виртуальной функции foo(char), но, кроме того, в классе B объявляются невиртуальные перегруженные функции foo(const char*) и foo(int). Класс C объявляет свою версию виртуальной функции foo(char) и невиртуальные перегруженные функции foo(const char*) и foo(double). Обратите внимание на то, что в классе C приходится заново объявлять функцию foo(const char*), поскольку в данном случае функция-элемент B::foo(const char*) не наследуется. Таким образом, в С++ схема наследования отличается от обычной для случая виртуальной и перегруженных функций с одинаковым именем. В функции main объявляются объекты для всех трех классов и вызываются различные версии функции-элемента foo.

Дружественные функции

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

Общий вид (синтаксис) объявления дружественной функции следующий:

class className

{

public:

className();

// другие конструкторы

friend returnType friendFunction(<список параметров>);

};

Пример 9:

class String

{

protected:

char *str;

int len;

public:

String();

~String();

// другие функции-элементы

friend String& append(String &str1, String &str2);

friend String& append(const char* str1, String &str2);

friend String& append(String &str1, const char* str2);

};

Дружественные функции могут решать задачи, которые при помощи

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

Рассмотрим простой пример использования дружественных функций. Текст программы FRIEND.CPP представлен в листинге 8.5. Программа следит за памятью, отведенной для хранения массива символов. Эта программа - первый шаг к созданию класса string.

Операции и дружественные операции

Последняя программа использовала функции-элементы и дружественную функцию, которые реализовали действия, выполняемые в стандартных типах с помощью операций вроде = и +. Подход типичен для языков C и Pascal, потому что эти языки не поддерживают определяемые пользователем операции. В отличии от них C++ позволяет вам объявлять операции и дружественные операции. Эти операции включают в себя: +, -, *, /, %, ==, !=, <=, <, >=, >, +=, -=, *=, /=, %=, [],

(), << и >>. Обратитесь к описанию языка C++, где обсуждаются детали определения этих операций. С++ трактует операции и дружественные операции как специальный тип функций-элементов и дружественных функций.

Общий синтаксис для объявления операций и дружественных операций:

class className

{

public:

// конструкторы и деструктор

// функции-элементы

// унарная операция

returnType operator operatorSymbol();

// бинарная операция

returnType operator operatorSymbol(operand);

// унарная дружественная операция

friend returnType operator operatorSymbol(operand);

// бинарная дружественная операция

friend returnType operator operatorSymbol(firstOperand, secondOperand);

};

Пример 10:

class String

{

protected:

char *str;

int num;

public:

String();

~String();

// другие функции-элементы

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

String& operator =(String& s);

String& operator +=(String& s);

// операции конкатенации

friend String& operator +(String& s1, String& s2);

friend String& operator +(const char* s1, String& s2);

friend String& operator +(String& s1, const char* s2);

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

friend int operator >(String& s1, String& s2);

friend int operator =>(String& s1, String& s2);

friend int operator <(String& sl, String& s2);

friend int operator <=(String& sl, String& s2);

friend int operator ==(String& s1, String& s2);

friend int operator !=(String& sl, String& s2);

};

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

Виртуальные функции

Мы уже упоминали о полиморфизме - важной особенности объектно-ориентированного программирования. Рассмотрим следующий пример (6):

#include <iostream.h>

class X

{

public:

double A(double x) { return x * x; }

double B(double x) { return A(x) / 2; }

};

class Y : public X

{

public:

double A(double x) { return x * x * x; }

};

int main ()

{

Y y;

cout << y.B(3) << endl;

return 0;

}

В классе X объявляются функции A и B, причем функция B вызывает функцию А. Класс Y, потомок класса X, наследует функцию B, но переопределяет функцию A. Цель этого примера - демонстрация полиморфного поведения класса Y. Мы должны получить следующий результат: вызов наследуемой функции X::B должен привести к вызову функции Y::A. Что же выдаст нам наша программа? Ответом будет 4.5, а не 13.5! В чем же дело? Почему компилятор разрешил выражение y.B(3) как вызов наследуемой функции X::B, которая, в свою очередь, вызывает X::A, а не функцию Y::A, что должно было бы произойти в случае полиморфной реакции класса?

Виртуальные функции объявляются следующим образом (синтаксис):

class className1

{

// функции-элементы

virtual returnType functionName(<список параметров>);

};

class className2 : public className1

{

// функции-элементы

virtual returnType functionName(<список параметров>);

};

Пример 7, показывающий, как при помощи виртуальных функций можно реализовать полиморфное поведение классов X и Y:

#include <iostream.h>

class X

{

public:

virtual double A(double x) { return x * x; }

double B (double x) { return A(x) / 2; }

};

class Y : public X

{

public:

virtual double A(double x) { return x * x * x; }

};

main()

{

Y y;

cout << y.B(3) << endl;

return 0;

}

Этот пример выведет вам правильное значение 13.5, потому что в результате вызова наследуемой функции X::B, вызывающей функцию A, в качестве функции A во время выполнения программы будет использована замещающая функция Y::A.

Правило виртуальной функции

Правило виртуальной функции гласит:

"Виртуальная однажды - виртуальна всегда".

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

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

Рассмотрим пример 8, иллюстрирующий сказанное.

#include <iostream.h>

class A

{

public:

A() {}

virtual void foo(char c)

{ cout << "virtual A::foo() returns " << c << endl; }

};

class B : public A

{

public:

B() {}

void foo(const char* s)

{ cout << "B::foo() returns " << s << endl; }

void foo(int i)

{ cout << "B::foo() retuzns " << i << endl; }

virtual void foo(char c)

{ cout << "virtual B::foo() returns " << c << endl; }

};

class C: public B

{

public:

C() {}

void foo(const char* s)

{ cout << "C::foo() returns " << s << endl; }

void foo(double x)

{ cout << "C::foo() returns " << x << endl; }

virtual void foo(char c)

{ cout << "virtual C::foo() returns " << c << endl; }

};

int main()

{

A Aobj;

B Bobj;

C Cobj;

Aobj.foo('A');

Bobj.foo('B');

Bobj.foo(10);

Bobj.foo("Bobj");

Cobj.foo('C');

Cobj.foo(144.123);

Cobj.foo("Cobj");

return 0;

}

В этом примере вводятся три класса - A, B и C - образующих линейную иерархию наследования. В классе A объявляется виртуальная функция foo(char).

Класс B объявляет свою версию виртуальной функции foo(char), но, кроме того, в классе B объявляются невиртуальные перегруженные функции foo(const char*) и foo(int). Класс C объявляет свою версию виртуальной функции foo(char) и невиртуальные перегруженные функции foo(const char*) и foo(double). Обратите внимание на то, что в классе C приходится заново объявлять функцию foo(const char*), поскольку в данном случае функция-элемент B::foo(const char*) не наследуется. Таким образом, в С++ схема наследования отличается от обычной для случая виртуальной и перегруженных функций с одинаковым именем. В функции main объявляются объекты для всех трех классов и вызываются различные версии функции-элемента foo.

Дружественные функции

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

Общий вид (синтаксис) объявления дружественной функции следующий:

class className

{

public:

className();

// другие конструкторы

friend returnType friendFunction(<список параметров>);

};

Пример 9:

class String

{

protected:

char *str;

int len;

public:

String();

~String();

// другие функции-элементы

friend String& append(String &str1, String &str2);

friend String& append(const char* str1, String &str2);

friend String& append(String &str1, const char* str2);

};

Дружественные функции могут решать задачи, которые при помощи

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

Рассмотрим простой пример использования дружественных функций.

Текст программы FRIEND.CPP представлен в листинге 8.5. Программа следит за памятью, отведенной для хранения массива символов. Эта программа - первый шаг к созданию класса string.

Операции и дружественные операции

Последняя программа использовала функции-элементы и дружественную функцию, которые реализовали действия, выполняемые в стандартных типах с помощью операций вроде = и +. Подход типичен для языков C и Pascal, потому что эти языки не поддерживают определяемые пользователем операции. В отличии от них C++ позволяет вам объявлять операции и дружественные операции. Эти операции включают в себя: +, -, *, /, %, ==, !=, <=, <, >=, >, +=, -=, *=, /=, %=, [], (), << и >>. Обратитесь к описанию языка C++, где обсуждаются детали определения этих операций. С++ трактует операции и дружественные операции как специальный тип функций-элементов и дружественных функций.

Общий синтаксис для объявления операций и дружественных операций:

class className

{

public:

// конструкторы и деструктор

// функции-элементы

// унарная операция

returnType operator operatorSymbol();

// бинарная операция

returnType operator operatorSymbol(operand);

// унарная дружественная операция

friend returnType operator operatorSymbol(operand);

// бинарная дружественная операция

friend returnType operator operatorSymbol(firstOperand, secondOperand);

};

Пример 10:

class String

{

protected:

char *str;

int num;

public:

String();

~String();

// другие функции-элементы

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

String& operator =(String& s);

String& operator +=(String& s);

// операции конкатенации

friend String& operator +(String& s1, String& s2);

friend String& operator +(const char* s1, String& s2);

friend String& operator +(String& s1, const char* s2);

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

friend int operator >(String& s1, String& s2);

friend int operator =>(String& s1, String& s2);

friend int operator <(String& sl, String& s2);

friend int operator <=(String& sl, String& s2);

friend int operator ==(String& s1, String& s2);

friend int operator !=(String& sl, String& s2);

};

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

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

ИСХОДНЫЕ ТЕКСТЫ ПРИМЕРОВ

(Листинг 8.1. исходный текст программы RECT.CPP

// Программа C++, иллюстрирующая использование класса.

// Программа моделирует прямоугольник.)

// Листинг 8.2. Исходный текст программы ARRAY.CPP

// Программа демонстрируюет использование конструкторов и деструкторов:

// - создает динамический массив (объект),

// - присваивает значения элементам динамического массива,

// - выводит значения элементов динамического массива,

// - удаляет динамический массив.

// Листинг 8.3. Исходный текст программы CIRCLE.CPP

// Простой пример иерархии классов.

// Листинг 8.4. Исходный текст программы VIRTUAL.CPP

// Программа демонстрирует использование виртуальных функций

// для моделирования квадратов и прямоугольников и вывода их

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

ВОПРОСЫ И ОТВЕТЫ

Что случится, если я объявлю конструктор по умолчанию, конструктор копии и другие конструкторы в защищенной области?

Программы, использующие ваш класс, не смогут создавать объекты этого класса. Однако они смогут объявлять классы-потомки с открытыми конструкторами.

Могу я задать цепочку вызовов функций-элементов ?

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

String& upperCase();

String& reverse();

Stringa mapChar(char find, char replace);

вы можете написать следующий оператор обработки объекта класса

String:

s.upperCase().reverse().mapChar(' ', '+');

Что может случиться, если класс полагается на конструктор копии, созданный компилятором, и при этом класс использует указатели в качестве элементов-данных?

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

Могу ли я создавать массив объектов?

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

Могу ли я использовать указатель при создании объекта класса?

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

Вот пример, использующий класс Complex. Не забудьте, что для обращения к элементам классов или структур используется операция ->, если вы ссылаетесь на них при помощи указателей.

Complex *pC;

pC = new Complex;

// операции с объектом, к которому обращаются по указателю pC

delete pC;

или

Complex *pC = new Complex;

// операции с объектом, к которому обращаются по указателю pC

delete pC;

Контрольные вопросы

1. Найдите ошибку в следующем объявлении класса:

class String {

char *str;

unsigned len;

String ();

String(const String& s);

String(unsigned size, char = ' ');

String(unsigned size);

String& assign(String& s);

~String();

unsigned getLen() const;

char* getString();

// другие функции-элементы

};

2. Найдите ошибку в следующем объявлении класса:

class String {

protected:

char *str;

unsigned len;

public:

String();

String(const char* s);

String(const String& s);

String(unsigned size, char = ' ');

String(unsigned size);

~String();

// другие функции-элементы

3. Верно или нет? Следующий оператор, который создает объект s класса String, объявленного ранее, является правильным:

s = String("Hello Borland C++");

4. Если в программе OPERATOR.CPP вы следующим образом измените объявления объектов, будет ли программа компилироваться без ошибок?

String s1 = String("Kevin");

String s2 = String(" Нау");

String s3 = s1;

ФАЙЛОВЫЕ ОПЕРАЦИИ ВВОДА/ВЫВОДА

Сегодняшний урок посвящен файловым операциям ввода/вывода с использованием библиотеки управления потоками C++. У вас есть две возможности: либо использовать функции файлового ввода/вывода, описанные в заголовочном файле STDIO.H, либо функции stream-библиотеки C++. Каждая из этих библиотек имеет множество мощных и удобных функций. Сегодня будут представлены основные операторы, которые позволят вам читать и записывать данные в файл. Вы изучите следующие темы:

Стандартные функции потоков ввода/вывода

    Последовательный ввод/вывод потока с текстовой информацией

    Последовательный ввод/вывод двоичных данных

    Прямой доступ к потоку двоичных данных

Stream-библиотека C++

Stream-библиотека (известная также как библиотека iostream) выполнена в виде иерархии классов, которые описаны в нескольких заголовочных файлах. Файл IOSTREAM.H, используемый до сих пор, - это только один из них. Другой, который будет интересен в этой главе, - FSTREAM.H. Файл IOSTREAM.H поддерживает основные классы для ввода/вывода потока. Файл FSTREAM.H содержит определения для основных классов файлового ввода/вывода.

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

ОБЩИЕ ФУНКЦИИ ПОТОКОВОГО ВВОДА/ВЫВОДА

В этом разделе представлены функции-элементы ввода/вывода, являющиеся общими как для последовательного, так и для прямого доступа. Эти функции включают open, close, good и fail в дополнение к операции !. Функция open открывает файловый поток для ввода, вывода, добавления, а также для ввода и вывода. Эта функция позволяет указывать тип данных, с которыми вы собираетесь работать: двоичные или текстовые.

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

Существуют некоторые особые тонкости, связанные с файлами текстового режима, на которые следует обратить особое внимание и запомнить. Первая из них - символ EOF (26 в коде ASCII или Ctrl+Z) - представляет собой метку (символ) конца файла. В текстовом режиме, где встречается символ EOF, система C++ низкого уровня автоматически продвигается к концу файла; вы ничего не можете прочитать после специального символа. Это может вызвать проблемы, если такой специальный символ окажется в середине файла.

Другая особенность текстового режима заключается в том, как интерпретируются строки текстового файла. Каждая строка заканчивается последовательностью конца строки (EOL). На компьютерах PC и совместимых с ними EOL-последовательность представлена двумя символами кода ASCII: CR (13 в коде ASCII или Ctrl+M) и LF (10 в коде ASCII или Ctrl+J). Эта CRLF-последовательность используется функциями чтения и записи текстовой строки, которые автоматически, вставляют ее в файл или удаляют из него. Заметьте, что на большинстве других, систем (UNIX и Macintosh) EOF просто является символом LF.

Функция-компонент open

Прототип функции open

void open (const char* filename, int mode, int m = filebuf::openprot);

Параметр filename задает имя открываемого файла. Параметр mode указывает режим ввода/вывода. Далее следует список аргументов для mode, описанных в заголовочном файле FSTREAM.H:

    in открыть поток для ввода,

    out открыть поток для вывода,

    ate установить указатель потока на конец файла,

    app открыть поток для добавления,

    trunk удалить содержимое файла, если он уже существует (bc++5),

    nocreate инициировать ошибку, если уже не существует,

    noreplace инициировать ошибку, если файл уже существует,

    binary открыть в двоичном режиме.

Пример 1.

// открыть поток для ввода

fstream f;

f.open("simple.txt", ios::in);

// открыть поток для вывода fstream f;

fstream f;

f.open ("simple.txt", ios::out);

// открыть поток ввода/вывода для двоичных данных fstream f;

fstream f;

f.open("simple.txt", ios::in | ios::out | ios::binary);

Внимание: Классы файловых потоков предусматривают конструкторы, которые выполняют действия (и имеют такие же параметры) функции-компонента open.

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

Функция-компонент close

Прототип для функции close:

void close();

Пример 2.

fstream f;

// открыть поток

f.open ( "simple.txt", ios:: in);

// работа с файлом

// закрыть поток

f.close ();

Stream-библиотека C++ включает в себя набор основных функций, которые контролируют состояние ошибки потоковой операции. Эти функции включают следующие:

    Функция good() возвращает ненулевое значение, если при выполнении потоковой операции не возникает ошибки. Объявление функции good: int good();

    Функция fail() возвращает ненулевое значение, если при выполнении потоковой операции возникает ошибка. Объявление функции fail: int fail();

    Перегруженная операция ! применяется к экземпляру потока для определения состояния ошибки.

Stream-библиотека C++ предоставляет дополнительные функции для установки и опроса других аспектов и типов ошибок потока.

ПОСЛЕДОВАТЕЛЬНЫЙ ТЕКСТОВЫЙ ПОТОК ВВОДА/ВЫВОДА

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

    Операция извлечения из потока << записывает строки или символы в поток.

    Операция помещения в поток >> читает символы потока.

    Функция getline читает строку из потока.

Функция-элемент getline

Прототипы функции-элемента getline:

istream& getline (char* buffer, int size, char delimiter = '\n');

istream& getline (signed char* buffer, int size, char delimiter = '\n');

istream& getline (unsigned char* buffer, int size, char delimiter = '\n');

Параметр buffer - это указатель на строку, принимающую символы из потока. Параметр size задает максимальное число символов для чтения. Параметр delimiter указывает разделяющий символ, который вызывает прекращение ввода строки до того, как будет введено количество символов, указанное в параметре size. По умолчанию параметру delimiter присваивается значение '\n'.

Пример 3.

fstream f;

char textLine[MAX];

f.open("sample.txt", ios::in);

while (!f.eof()) {

f.getline(textLine, MAX);

cout << textLine << endl;

}

f.close();

Рассмотрим пример. В листинге 10.1 приведен исходный код программы TRIM.CPP. Программа выполняет следующие задачи:

    Выдает запрос на ввод имени входного текстового файла.

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

    Читает строки из входного файла и удаляет из них <висящие> пробелы.

    Записывает эти строки в выходной файл и также в стандартное окно вывода.

Листинг 10.1. Исходный код программы TRIM.CPP

// C++ программа демонстрации последовательного файлового

// ввода/вывода

Программа в листинге 10.1 не объявляет никаких классов, вместо этого она фокусируется на использовании файловых потоков для ввода и вывода текста. Эта программа описывает функции trimStr, getInputFilename, getOutputFilename, processLines и обязательную функцию main.

Функция trimStr вычищает <висящие> пробелы в строках, передаваемых через параметр s. Эта функция объявляет переменную i и присваивает ей индекс символа, находящегося сразу за завершающим нулем. Функция использует цикл while, начинающийся в строке 14, чтобы выполнить обратное сканирование символов в строке s до первого символа, не являющегося пробелом. Оператор в строке 16 присваивает завершающий нуль символу, стоящему справа от последнего символа, не являющегося пробелом, в строке s.

Функция getInputFilename получает имя входного файла и открывает соответствующий файловый поток. Параметр inFile передает это имя вызывающей функции. Ссылочный параметр f передает открытый входной поток вызывающей функции. Функция getInputFilename объявляет локальный флажок ok и использует цикл do-while (строки с 23 по 34), чтобы открыть входной файл. Строка 25 содержит первый оператор тела цикла, в котором флажок ok инициализируется значением true. Оператор вывода в строке 26 запрашивает ввод имени входного файла; в строке 27 с помощью вызова функции getline это имя принимается и сохраняется в переменной inFile. Оператор в строке 28 пытается открыть входной файл, используя параметр потока f. Оператор open использует значение ios::in для указания на то, что входной текстовый файл был открыт. Если вызов возвращает ошибку, оператор if (строка 29) определит это, сообщит об ошибке открытия файла пользователю и присвоит переменной ok значение false. При значении ok, равном true, цикл do-while будет выполняться и сохранять ответы пользователя до тех, пока не произойдет успешное открытие файла.

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

Функция processLines читает строки их входного файлового потока, приводя их в порядок и записывая в выходной файловый поток. Параметры fin и font передают файловые указатели входного и выходного потоков, соответственно. Эта функция объявляет локальную строковую переменную line и использует (строки с 69 по 74) цикл while для обработки текстовых строк. Предложение while содержит вызов функции getline, которая читает следующую строку входного потока fin и присваивает переменной line содержимое этой строки. В теле этого цикла просто вызывается функция trimStr, а затем line передается в потоки fout и cout. Заметьте, что команды для получения строки текста из файла и передачи ее в файл в точности совпадают с теми, которые могли бы использоваться для получения текста с экрана и передачи его на экран. Это происходит потому, что используемые вами cout и cin - на самом деле только потоки, которые открываются автоматически и работают непосредственно с

экраном.

Функция main, как обычно со всеми уже описанными функциями, довольно простая. Она только объявляет переменные файловых потоков fin, fout и inFile, outFile для сохранения имен этих потоков. Далее входной и выходной файлы открываются в функциях getInputFilename и getOutputFilename. Наконец, функция processLine приводит в порядок и копирует содержимое файла, а функция-компонент close вызывается для каждого из потоков.

ПОСЛЕДОВАТЕЛЬНЫЙ ДВОИЧНЫЙ ФАЙЛОВЫЙ ВВОД/ВЫВОД

Stream-библиотека C++ имеет перегруженные потоковые функции-элементы write и read для последовательного двоичного файлового ввода/вывода. Функция write посылает ряд байт в выходной поток. Эта функция может записывать любую переменную или экземпляр в поток.

Функция-элемент write

Прототип перегруженной функции-элемента:

ostream& write(const char* buff, int num);

ostream& write(const signed char* buff, int num);

ostream& write(const unsigned char* buff, int num);

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

Пример 4.

const MAX = 80;

char buff[MAX+1] = "Hello World!";

int len = strlen (buff) + 1;

fstream f;

f.open("CALC.DAT", ios::out | ios::binary);

f.write((const unsigned char*) &len, sizeof(len));

f.write((const unsigned char*) buff, len);

f.close();

В этом примере открывается файл CALC.DAT, записывается целое, содержащее число байт в строке и записывается сама строка перед тем, как файл закрывается.

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

Функция-элемент read

Прототип перегруженной функции-элемента read:

ostream& read(char* buff, int num);

ostream& read(signed char* buff, int num);

ostream& read(unsigned char* buff, int num);

Параметр buff - это указатель на буфер, который принимает данные из входного потока. Параметр num указывает число считываемых из потока байт.

Пример 5.

const MAX = 80;

char buff [MAX+1];

int len;

fstream f;

f.open("CALC.DAT", ios::in | ios::binary);

f.read((unsigned char*) &len, sizeof(len));

f.read((unsigned char*) buff, len);

f.close();

В этом примере считывается информация, записанная в предыдущем примере.

Рассмотрим пример, выполняющий последовательный двоичный потоковый ввод/вывод. В листинге 10.2 представлен исходный код программы ARRAY.CPP. Эта программа объявляет класс, который моделирует численный динамический массив. Операции ввода/вывода позволяют программе читать и писать как отдельные элементы массива, так и целый массив в двоичный файл. Эта программа создает массивы arr1, arr2 и аrrЗ, а затем выполняет следующие задачи:

    Присваивает значения элементам массива arr1. (Этот массив имеет 10 элементов).

    Присваивает значения элементам массива аrrЗ. (Этот массив имеет 20 элементов).

    Отображает значения массива arr1.

    Записывает элементы массива arr1 в файл ARRAY1.DAT (по одному за операцию).

    Читает элементы массива arr1 из этого файла в массив arr2 (по одному за операцию). (Массив arr2 имеет 10 элементов, то есть он одного размера с массивом arr1).

    Отображает элементы массива arr2.

    Отображает элементы массива аrrЗ.

    Записывает элементы массива аrrЗ в файл ARRAY3.DAT, все сразу.

    Читает (все сразу) данные из файла ARRAY3.DAT и сохраняет их в массиве arr1.

    Отображает значения массива arr1. (Выход показывает, что массив arr1 имеет тот же размер, что и массив arr3).

Листинг 10.2. Исходный код программы ARRAY.CPP

// C++ демонстрация последовательного двоичного

// ввода/вывода

Программа листинга 10.2 объявляет версию класса Array, который похож на приводимый в главе 8 в листинге 8.2. Основное отличие в том, что здесь мы использовали operator[ ] для замены и функции store, и recall. Эта операция проверяет правильность указания индекса и возвращает значение в badIndex, если аргумент выходит за диапазон массива. В дополнение к operator[ ] мы добавили функции-элементы writeElem, readElem, writeArray и readArray для выполнения последовательного двоичного файлового ввода/вывода. Мы также добавили функции resize и getPrt как вспомогательные функции соответственно для изменения размера массива и для возвращения указателя на соответствующий элемент массива.

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

Функция writeElem, определенная в строках с 43 по 49, записывает одиночные элементы массива в выходной поток. Параметр os представляет выходной поток. Параметр index определяет элемент массива для записи. Функция writeElem возвращает true, если индекс правильный и если операция по выводу осуществляется без ошибок. После того, как writeElem записывает элемент массива, внутренний указатель потока продвигается в следующее положение.

Функция readElem, определяемая в строках с 51 по 57, считывает одиночный элемент массива из входного потока. Параметр Is представляет входной поток. Параметр index определяет индекс элемента массива для чтения. Эта функция возвращает true, если индекс массива правильный и если операция по вводу осуществляется без ошибок. После того, как readElem считывает элемент массива, внутренний указатель потока продвигается в следующее положение.

Функции writeElem и readElem позволяют экземпляру класса соответственно писать и читать элементы данных из различных потоков.

Функция writeArray, определенная в строках с 59 по 69, записывает все элементы массива в двоичный файл. Параметр filename определяет имя выходного файла. Функция открывает выходной поток и записывает значение элемента класса size, а затем и элементы динамического массива. Функция writeArray возвращает true, если массив в поток записан успешно. Иначе она возвращает false. Эта функция открывает локальный выходной поток, используя потоковую функцию open и передавая ей имя файла и режим ввода/вывода. Режим ввода/вывода представляет собой выражение ios::out|ios::binary, которое определяет, что поток открывается только для вывода двоичных записей. Эта функция дважды вызывает потоковую функцию write - первый раз для записи компонента класса size, второй - для записи элементов динамического массива.

Функция readArray, определенная в строках с 71 по 83, читает все элементы массива из двоичного файла. Параметр filename определяет имя входного файла. Функция открывает входной поток и считывает значение компонента класса size, а затем считывает элементы динамического массива. Функция readArray возвращает true, если она успешно считывает массив из потока. В противном случае, возвращается false. Функция открывает локальный входной поток, используя потоковую функцию open и передавая ей имя файла и аргументы режима ввода/вывода. Аргумент режима ввода/вывода - это выражение ios::in | ios::binary, которое определяет, что поток открыт только для двоичного ввода. Функция делает два вызова потоковой функции read, первый - для чтения элемента класса size, и второй - для чтения элементов динамического массива. Другим свойством функции readArray является то, что она изменяет размер экземпляра класса Array для настройки его в соответствии с данными двоичного файла, вызывая функцию-элемент resize. Это означает, что динамический массив, который доступен посредством экземпляра класса, может либо уменьшаться, либо расширяться в зависимости от размера массива, сохраняемого в файле.

Функция-элемент resize, которая начинается в строке 65, на самом деле очень простая. Она проверяет, является ли требуемый размер тем же, что и установленный ранее. Если нет, то память, зарезервированная функцией dataPtr, освобождается, а затем создается новая область памяти, соответствующая новому размеру. Этот новый размер присваивается компоненту класса size.

Функция dispArray чаще всего является функцией-элементом, но я решил сделать ее здесь обычной функцией, чтобы лучше показать, как использование функции operator[ ] позволяет тем, кто работает с классом Array, обращаться к нему таким же способом, какой они применяют к элементам стандартного массива. В этом случае есть простой цикл for, который выполняется для каждого элемента arr и отображает его содержимое.

Наконец, мы подходим к функции main (строка 104). Обычно она в основном

только запускает функции, которые уже были созданы, для выполнения

следующих задач:

    Объявляет (строка 108) три экземпляра класса Array с именами arr1, arr2 и аrr3. (Первые два экземпляра имеют тот же самый размер динамического массива, заданный константой SIZE1, в то время как аrr3 имеет больший размер, определенный константой SIZE2).

    Объявляет (строка 111) файловый поток f и открывает его (используя конструктор потока) для доступа к файлу ARRAY1.DAT в двоичном режиме.

    Использует циклы for (строки с 114 по 116), чтобы произвольно присвоить значения экземплярам arr1 и аrr3.

    Отображает элементы экземпляра arr1 (строка 119).

    Записывает элементы массива arr1 в выходной файловый поток f, используя цикл for (строка 122) для вызова функции-компонента writeElem с выходным файловым потоком f и переменной цикла i.

    Закрывает файловый поток f, вызывая функцию-элемент close этого потока.

    Открывает (строка 127) файловый поток f для доступа к файлу ARRAY1.DAT. (На это раз сообщение open определяет режим двоичного ввода)

    Считывает элементы в arr2 (которому до сих пор не присваивались никакие значения) из входного файлового потока f, используя цикл for (строка 128).

    Закрывает входной поток (строка 130). D Отображает элементы экземпляров arr2 и аrr3 (строки 132 и 133).

    Записывает все содержимое аrr3, вызывая функцию-компонент writeArray. (Функция writeArray имеет аргумент имени файла ARRAY3.DAT).

    Считывает массив файла ARRAY3.DAT в экземпляр arr1, вызывая функцию-компонент readArray и передавая ей в качестве аргумента имени файла ARRAY3.DAT.

    Отображает новые элементы экземпляра arr1.

Файловый ввод/вывод с прямым доступом

Файловые операции ввода/вывода прямого доступа также используют потоковые функции-элементы read и write, представленные в предыдущем разделе. Stream-библиотека имеет ряд функций, позволяющих вам передвигать указатель потока в любое необходимое положение. Функция-элемент seekg - одна из таких функций.

Функция-элемент seekg

Прототип для перегруженной функции-компонента seekg:

istream& seekg(long pos);

istream& seekg(long offset, seek_dir dir);

Параметр pos в первой версии определяет абсолютное положение байта в потоке. Во второй версии параметр offset определяет относительное смещение, в зависимости от аргумента dir. Аргументы для последнего параметра:

ios::beg С начала файла

ios::cur С текущей позиции файла

ios::end С конца файла

Пример

const BLOCK SIZE = 80

char buff[BLOCK_SIZE] = "Hello World!";

f.open("CALC.DAT", ios::in | ios::out | ios::binary);

f.seekg(3 * BLOCK_SIZE); // продвинутся к блоку 4

f.read((const unsigned char*)buff, BLOCK_SIZE);

cout < buff < endl;

fclose ();

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

Рассмотрим пример файлового ввода/вывода прямого доступа. В листинге 10.3 приведен исходный код программы VIRTUAL.CPP и реализует виртуальный массив. Программа выполняет следующие задачи:

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

    Отображает элементы неупорядоченного объекта виртуального массива.

    Сортирует элементы объекта виртуального массива.

    Отображает элементы сортированного объекта виртуального массива.

Листинг 10.3. Исходный код прогшраммы VIRTUAL.CPP

// C++ демонстрация файлового ввода/вывода прямого доступа

Программа листинга 10.3 объявляет класс VmArray. Этот класс моделирует динамический базирующийся на диске массив, который сохраняет все его элементы в двоичном файле прямого доступа. Заметьте, что в этом классе объявлен экземпляр класса fstream и что не существует указателя на динамический массив. Класс объявляет конструктор, деструктор и ряд функций-компонентов.

Конструктор класса имеет два параметра: Size и filename. Параметр Size задает размер виртуального массива. Параметр filename именует двоичный файл, который сохраняет элементы экземпляров класса. Конструктор открывает поток f, используя потоковую функцию open и передавая ей в качестве аргументов filename и выражение для режима работы с файлом ios::in | ios::out | ios::binary.

Это выражение указывает, что поток открывается для двоичного ввода/вывода (то есть режима прямого доступа). Если конструктор успешно открывает файловый поток, он заполняет файл пустыми строками. Деструктор класса выполняет простую задачу закрытия файлового потока f.

Функции setElem и getElem поддерживают прямой доступ к элементам массива. Эти функции используют потоковую функцию seekg, чтобы устанавливать указатель потока на соответствующий элемент массива. Затем функция setElem вызывает потоковую функцию write для сохранения элемента массива (передаваемый параметром str). Напротив, функция getElem называет потоковую функцию read, чтобы получить элемент массива (возвращаемый через аргумент str). Обе функции возвращают результат типа bad, который указывает на успешность операции ввода/вывода.

Класс VmArray также объявляет функцию BubbleSort для сортировки элементов виртуального массива. Эта функция использует функции-элементы getElem и setElem для доступа и свопинга элементов массива. Затем, наконец, запускается последняя функция-элемент display для элементов виртуального массива, которая посылает их на экран. Функция main выполняет следующие

задачи:

    Объявляет экземпляр arr класса VmArray. (Этот экземпляр сохраняет 10 строк в двоичном файле ARR.DAT)

    Присваивает случайное значение элементам экземпляра аот, используя цикл for (строки 97 и 98).

    Отображает несортированные элементы экземпляра arr, вызывая функцию-элемент display.

    Сортирует массив, вызывая функцию BubbleSort.

    Отображает сортированные элементы экземпляра arr.

Заключение

Сегодняшний урок представил краткое введение в библиотеку ввода/вывода C++ и вынес на обсуждение следующие вопросы:

    Общие функции ввода/вывода, включая open, close, good, fail и оператор !.

    Функция open открывает файловый поток ввода/вывода и поддерживает попеременный и множественный режимы ввода/вывода. Функция close закрывает файловый поток. Функции good и fail индицируют успешную или ошибочную, соответственно, потоковую операцию ввода/вывода.

    C++ позволяет выполнять последовательный потоковый ввод/вывод для текста с использованием операций < и >, так же как и при помощи потоковой функции getline. Операция < позволяет записать символы и строки (а также и другие предопределенные типы данных). Операция > применяется для

    получения символов. Функция getline позволяет вашему приложению считывать строки с клавиатуры или из текстового файла.

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

    Потоковый ввод/вывод прямого доступа для двоичных данных использует функцию seekg в объединении с функциями read и write. Функция seekg позволяет вам передвигать потоковый указатель либо в абсолютное, либо в относительное положение в потоке.

Вопросы и ответы

Как можно эмулировать прямой доступ к строкам в текстовом файле?

Сначала считывайте строки из файла как текст, получайте длину строк (плюс два символа для конца каждой строки) и сохраняйте накапливаемую длину в специальном массиве. (Назовите его, например, lineIndex) Этот массив сохраняет позицию байта, где начинается каждая строка. Последний элемент массива будет содержать размер файла. Для доступа к строке номер i, используйте функцию seek или seekg, чтобы найти смещение для lineIndex[i]. Размер строки номер i равен lineIndex[i+1] - lineIndex[i+1].

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

Вам необходимо использовать потоковую функцию gcount() для получения ряда байт фактически читаемых в последнем неформатированном потоковом вводе. Вот функция copyStream:

void copyStream(fstreamit fin, fstreamil fout,

unsigned char* buffer, int buffSize)

{

int n;

while (fin. read (buffer, buffaize))

{

n = fin.gcount();

fout.write (buffer, n);

}

}

Практикум

Контрольные вопросы

1. Верно или нет? Потоковые функции ввода/вывода read и write способны правильно считывать и записывать данные любого типа.

2. Верно или нет? Потоковые функции ввода/вывода read и write способны правильно считывать и записывать данные любого типа, не имеющих указателей.

3. Верно или нет? Функции seek и seekg расширяют файл, когда вы передаете индекс, который на один или более байт превышает текущий конец файла.

4. Верно или нет? Аргументы функций seek и seekg не требуют проверки диапазона.

Упражнение

Создайте программу VSEARCH.CPP, модифицируя программу VIRTUAL.CPP. Класс VmArray в VSEARCH.CPP должен иметь функцию binSearch, которая проводит двоичный поиск в элементах сортированного массива. Добавьте цикл в конец функции main для поиска в массиве arr, используя неупорядоченные данные инициализирующего списка. (Элементы этого списка доступны при использовании данных-указателей.)

1