Реферат: Основы алгоритмического языка С++
Основы алгоритмического языка С++
Переменные и операции языка С++
Здесь представлены базовые компоненты программ на С++. В их число входят типы данных, переменные, константы и выражения.
ИЗУЧАЕМЫЕ ПОНЯТИЯ
- Предопределенные типы данных в С++ включают в себя типы 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.
Компилятор Си игнорирует пробельные символы, если они не используются как разделители или как компоненты константы-символа или строковых литералов. Это нужно иметь в виду, чтобы дополнительно использовать пробельные символы для повышения наглядности программы (например,для просмотра редактором текстов).
Знаки пунктуации и специальные символы
Эти символы имеют специальный смысл для компилятора Си. Их использование в языке Си описывается в дальнейшем содержании руководства. Знаки пунктуации из множества представимых символов, которые не представлены в данном списке, могут быть использованы только в строковых литералах, константах-символах и комментариях.
ESC- последовательности
ESC- последовательности- это специальные символьные комбинации, которые представляют пробельные символы и неграфические символы в строках и символьных константах.
Их типичное использование связано со спецификацией таких действий, как возврат каретки и табуляция , а также для задания литеральных представлений символов, таких как символ двойная кавычка. ESC-последовательность состоит из наклонной черты влево, за которой следует буква, знаки пунктуации ' " \ или комбинация цифр. В таблице 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).
Операция условного выражения ?: -это тернарная, а не двухсимвольная операция. Формат условного выражения следующий: <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 иллюстрируются примеры целых констант.
Целые константы всегда специфицируют положительные величины. Если требуется отрицательные величины, то необходимо сформировать константное выражение из знака минус и следующей за ним константы. Знак минус рассматривается как арифметическая операция.
Каждая целая константа специфицируется типом, определяющим ее представление в памяти и область значений. Десятичные константы могут быть типа int или long.
Восьмеричные и шестнадцатеричные константы в зависимости от размера могут быть типа int, unsigned int, long или unsigned long. Если константа может быть представлена как int, она специфицируется типом int. Если ее величина больше, чем максимальная положительная величина, которая может быть представлена типом int, но меньше величины, которая представляется в том же самом числе бит как и int, она задается типом unsigned int. Наконец, константа, величина которой больше чем максимальная величина, представляемая типом unsigned int, задется типом long или unsigned long, если это необходимо.
Важность рассмотренных выше правил состоит в том, что восьмеричные и шестнадцатеричные константы не содержат "знаковых" расширений, когда они преобразуются к более длинным типам (преобразование типов смотри в разделе 5 "Выражения и присваивания").
Программист может определить для любой целой константы тип long, приписав букву "l" или "L" в конец константы. В Табл. 2.6 показаны примеры целых констант.
Константы с плавающей точкой
Константа с плавающей точкой- это действительное десятичное положительное число. Величина действительного числа включает целую, дробную части и экспоненту. Константы с плавающей точкой имеют следующий формат представления:
[<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'.
Константы-символы имеют тип 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).
Конструкции принятия решений и циклы
ВОПРОСЫ И ОТВЕТЫ
Предъявляет ли С++ какие-либо требования на отступ операторов в предложениях оператора?
Нет. Отступ определяется только Вами. Типовые размеры отступа составляют два или четыре пробела. Использование отступов делает ваш листинг намного более удобочитаемым.
Вот пример оператора 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.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 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, при этом предполагается, что char по умолчанию знаковый.
В табл. 4.3 для каждого типа приведены: размер распределяемой памяти и области значений переменных для данного типа. Поскольку тип void не представляет переменных, он не включен в эту таблицу.
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 стандартное соглашение
Тип 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 :/P>
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 substr[] = "narlBod";
int index;
index = strspn(str, substr);
Этот оператор присваивает 8 переменной index, потому что первые восемь символов из str содержатся в подстроке substr.
Функция 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 *substr = "hr";
char *ptr;
ptr = strpbrk(str, substr);
cout “ ptr “ endl;
Вы увидите на экране строку "here how are you", потому что 'h' встречается в строке str раньше, чем 'r'.
Поиск строк
Библиотека функций STRING.H предлагает для поиска подстроки в строке функцию strstr.
Функция strstr
Прототип функции strstr:
char* strstr(const char *str, const char *substr);
Функция ищет в строке str первое вхождение подстроки substr. Функция возвращает указатель на первый символ найденной в строке str подстроки substr. Если строка substr не обнаружена в строке str, функция возвращает 0.
Пример
char str[] = "Hello there! how are you";
char substr[] = "how";
char *ptr;
ptr = strstr (str, substr);
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:P>
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, используя неупорядоченные данные инициализирующего списка. (Элементы этого списка доступны при использовании данных-указателей.)
|