1
/
Программирование СИ++
Учебное пособие
по дисциплине Программирование на СИ++
для студентов специальностей
Вычислительные машины, системы, комплексы и сети
Информационные системы
дневной и заочной форм обучения
г. Тольятти
2007
Учебное пособие предназначено для студентов дневной и заочной форм обучения специальностей «Вычислительные машины, системы, комплексы и сети», «Информационные системы» для изучения методов и средств программирования с помощью языка высокого уровня СИ++.
Составил: к.т.н., доц. Трубачева С.И.
Введение
Учебное пособие может быть использовано студентами специальностей «Вычислительные машины, комплексы, системы и сети», «Информационные системы» при изучении дисциплины «Программирование на СИ++.
Данная дисциплина читается в третьем семестре, необходима для овладения знаниями, навыками, умениями в программировании на СИ, СИ++, призвана обеспечить подготовку студентов к изучению последующих курсов, а именно, компьютерной графики, системного программного обеспечения и др.
Учебное пособие содержит теоретический материал, примеры программ, задания к выполнению лабораторных работ, контрольные вопросы для самопроверки.
Изучение средств и методов разработки приложений, как пользовательских, так и системных, поможет студенту получить знания, практические навыки в программировании, системном администрировании.
Учебное пособие состоит из трех основных разделов. В первом разделе представлен материал основных конструкций языка, приемов программирования на СИ. Во втором разделе рассмотрены основные подходы объектно-ориентированного программирования: понятия, свойства, приемы программирования с использованием классов, производных классов, виртуальных функций. Третий раздел содержит примеры программ с пояснениями.
Учебное пособие не претендует на полноту изложения материала, основной целью его является оказать помощь студентам в освоении учебной программы по дисциплине.
Раздел I. Язык программирования СИ
1.1 Переменные и операции языка СИ
1.1.1 Используемые символы
Множество символов используемых в языке СИ можно разделить на пять групп.
Символы, используемые для образования ключевых слов и идентификаторов. В эту группу входят прописные и строчные буквы английского алфавита, а также символ подчеркивания. Следует отметить, что одинаковые прописные и строчные буквы считаются различными символами, так как имеют различные коды.
2. Группа прописных и строчных букв русского алфавита и арабские цифры.
3. Знаки нумерации и специальные символы.
Эти символы используются, с одной стороны, для организации процесса вычислений, а с другой, - для передачи компилятору определенного набора инструкций (таблица 1.1).
Таблица 1.1 - Символы языка
Знак, пояснение |
Знак, пояснение |
Знак, пояснение |
|
, запятая |
) круглая скобка правая |
. точка |
|
( круглая скобка левая |
; точка с запятой |
} фигурная скобка правая |
|
: двоеточие |
{ фигурная скобка левая |
? вопросительный знак |
|
< меньше |
' апостроф |
> больше |
|
! восклицательный знак |
[ квадратная скобка |
| вертикальная черта |
|
] квадратная скобка |
/ дробная черта |
# номер |
|
обратная черта |
% процент |
& амперсанд |
|
^ логическое не |
+ плюс |
= равно |
|
- минус |
' кавычки |
* звездочка |
|
~ тильда |
4. Управляющие и разделительные символы. К той группе символов относятся: пробел, символы табуляции, перевода строки, возврата каретки, новая страница и новая строка. Эти символы отделяют друг от друга объекты, определяемые пользователем, к которым относятся константы и идентификаторы. Последовательность разделительных символов рассматривается компилятором как один символ (последовательность пробелов).
5. Кроме выделенных групп символов в языке СИ широко используются так называемые, управляющие последовательности (таблица 1.2), т.е. специальные символьные комбинации, используемые в функциях ввода и вывода информации. Управляющая последовательность строится на основе использования обратной дробной черты () (обязательный первый символ) и комбинацией латинских букв и цифр.
Таблица 1.2 - Управляющие последовательности
Управляющая последовательность |
Наименование |
Шеснадцатеричная замена |
|
a |
Звонок |
007 |
|
b |
Возврат на шаг |
008 |
|
t |
Горизонтальная табуляция |
009 |
|
n |
Переход на новую строку |
00A |
|
v |
Вертикальная табуляция |
00B |
|
r |
Возврат каретки |
00C |
|
f |
Перевод формата |
00 |
|
D' |
Кавычки |
022 |
|
' |
Апостроф |
027 |
|
|
Ноль- символ |
000 |
|
\ |
Обратная дробная черта |
05C |
|
ddd |
Символ набора кодов ПЭВМ в восьмеричном представлении |
Например, 15 - восьмеричная управляющая последовательность |
|
xddd |
Символ набора кодов ПЭВМ в шестнадцатеричном представлении |
Например, x00D - шестнадцатеричная управляющая последовательность |
Следует отметить, что в строковых константах всегда обязательно задавать все три цифры в управляющей последовательности. Например, отдельную управляющую последовательность n (переход на новую строку) можно представить как 10 или xA, но в строковых константах необходимо задавать все три цифры, в противном случае символ или символы, следующие за управляющей последовательностью, будут рассматриваться как ее недостающая часть.
Кроме определения управляющей последовательности, символ обратной дробной черты () используется также как символ продолжения. Если за () следует (n), то оба символа игнорируются, а следующая строка является продолжением предыдущей. Это свойство может быть использовано для записи длинных строк.
1.1.2 Константы
Константами называются неизменяемые переменные программы. В языке СИ разделяют четыре типа констант: целые константы, константы с плавающей запятой, символьные константы, строковыми литералы.
Целая константа: это десятичное, восьмеричное или шестнадцатеричное число, которое представляет целую величину в одной из следующих форм: десятичной, восьмеричной или шестнадцатеричной.
Десятичная константа состоит из одной или нескольких десятичных цифр, причем первая цифра не должна быть нулем (в противном случае число будет воспринято как восьмеричное). Восьмеричная константа состоит из обязательного нуля и одной или нескольких восьмеричных цифр (среди цифр должны отсутствовать восьмерка и девятка, так как эти цифры не входят в восьмеричную систему счисления). Шестнадцатеричная константа начинается с обязательной последовательности 0х или 0Х и содержит одну или несколько шестнадцатеричных цифр (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F). В таблице 1.3 представлены примеры целых констант.
Таблица 1.3 - Примеры целых констант
Десятичная константа |
Восьмеричная константа |
Шестнадцатеричная константа |
|
16 |
020 |
0x10 |
|
127 |
177 |
0x7F |
Если требуется сформировать отрицательную целую константу, то используют знак '-' перед записью константы (который будет называться унарным минусом). Например: -0x2A, -088, -16 .
Каждой целой константе присваивается тип, определяющий преобразования, которые должны быть выполнены, если константа используется в выражениях. Тип константы определяется следующим образом:
- десятичные константы рассматриваются как величины со знаком, им присваивается тип int (целая) или long (длинная целая) в соответствии со значением константы. Если константа меньше 32768, то ей присваивается тип int в противном случае long.
- восьмеричным и шестнадцатеричным константам присваивается тип int, unsigned int (беззнаковая целая), long или unsigned long в зависимости от значения константы.
Для того чтобы любую целую константу определить типом long, достаточно в конце константы поставить букву 'l' или 'L'.
Примеры: 5l, 6l, 128L, 0105L, OX2A11L.
Константа с плавающей точкой - десятичное число, представленное в виде действительной величины с десятичной точкой или экспонентой.
Формат имеет вид:
[ цифры ].[ цифры ] [ Е | e [ + | - ] цифры ] .
Число с плавающей точкой состоит из целой и дробные часте й и (или) экспоненты. Константы с плавающей точкой представляют положительные величины удвоенной точности (имеют тип double). Для определения отрицательной величины необходимо сформировать константное выражение, состоящее из знака минуса и положительной константы.
Примеры: 115.75, 1.5Е-2, -0.025, .075, -0.85Е2
Символьная константа - представляется символом, заключенным в апострофы. Управляющая последовательность рассматривается как одиночный символ, допустимо ее использовать в символьных константах. Значением символьной константы является числовой код символа.
Примеры:
' '- пробел , 'Q'- буква Q , 'n' - символ новой строки , '\' - обратная дробная черта, 'v' - вертикальная табуляция.
Символьные константы имеют тип int и при преобразовании типов дополняются знаком.
Строковая константа (литерал) - последовательность символов (включая строковые и прописные буквы русского и латинского алфавита, а также цифры, заключенные в кавычки (') . Например: 'Школа № 26', “город Тольятти”.
Отметим, что все управляющие символы, кавычка ('), обратная дробная черта () и символ новой строки в строковом литерале и в символьной константе представляются соответствующими управляющими последовательностями. Каждая управляющая последовательность представляется как один символ. Например, при печати литерала 'Школа n № 26' его часть 'Школа' будет напечатана на одной строке, а вторая часть '№ 26' на следующей строке. Символы строкового литерала сохраняются в области оперативной памяти. В конец каждого строкового литерала компилятором добавляется нулевой символ, представляемый управляющей последовательностью .
Строковый литерал имеет тип char[ ]. Это означает, что строка рассматривается как массив символов. Отметим важную особенность, число элементов массива равно числу символов в строке плюс 1, так как нулевой символ (символ конца строки) также является элементом массива. Все строковые литералы рассматриваются компилятором как различные объекты. Строковые литералы могут располагаться на нескольких строках. Такие литералы формируются на основе использования обратной дробной черты и клавиши ввод. Обратная черта с символом новой строки игнорируется компилятором, что приводит к тому, что следующая строка является продолжением предыдущей.
Например:
'строка неопределенной n
длины'
полностью идентична литералу 'строка неопределенной длины'.
Для сцепления строковых литералов можно использовать символ (или символы) пробела. Если в программе встречаются два или более строковых литерала, разделенные только пробелами, то они будут рассматриваться как одна символьная строка. Этот принцип можно использовать для формирования строковых литералов, занимающих более одной строки.
1.1.3 Идентификаторы
Идентификатором называется последовательность цифр и букв, а также специальных символов, при условии, что первой стоит буква или специальный символ. Для образования идентификаторов могут быть использованы строчные или прописные буквы латинского алфавита. В качестве специального символа может использоваться символ подчеркивание (_). Два идентификатора, для образования которых используются совпадающие строчные и прописные буквы, считаются различными.
Например: abc, ABC, A128B, a128b .
Важной особенностью является то, что компилятор допускает любое количество символов в идентификаторе, хотя значимыми являются первые 31 символ. Идентификатор создается на этапе объявления переменной, функции, структуры и т.п. после этого его можно использовать в последующих операторах разрабатываемой программы. Следует отметить важные особенности при выборе идентификатора.
Во-первых, идентификатор не должен совпадать с ключевыми словами, с зарезервированными словами и именами функций библиотеки компилятора языка СИ.
Во вторых, следует обратить особое внимание на использование символа (_) подчеркивание в качестве первого символа идентификатора, поскольку идентификаторы, построенные таким образом, что, с одной стороны, могут совпадать с именами системных функций и (или) переменных, а, с другой стороны, при использовании таких идентификаторов программы могут оказаться непереносимыми.
В третьих, на идентификаторы, используемые для определения внешних переменных, должны быть наложены ограничения, формируемые используемым редактором связей (отметим, что использование различных версий редактора связей или различных редакторов накладывает различные требования на имена внешних переменных).
1.1.4 Ключевые слова
Ключевые слова - это зарезервированные идентификаторы, которые наделены определенным смыслом. Их можно использовать только в соответствии со значением известным компилятору языка СИ.
Приведем список ключевых слов языка: auto, double, int, struct, break, else, long, switch, register, tupedef, char, extern, return, void, case, float, unsigned, default, for, signed, union, do, if, sizeof, volatile, continue, enum, short, while
Кроме того, в версях реализации языка СИ, зарезервированными словами являются: _asm, fortran, near, far, cdecl, huge, paskal, interrupt.
Ключевые слова не могут быть использованы в качестве идентификаторов.
1.1.5 Использование комментариев в тексте программы
Комментарий - это набор символов, которые игнорируются компилятором. На этот набор символов, однако, накладываются следующие ограничения. Внутри набора символов, который представляет комментарий, не может быть специальных символов, определяющих начало и конец комментариев, соответственно (/* и */). Комментарии в СИ обозначаются “//”, “/*…*/”. Отметим, что комментарии могут заменить как одну строку, так и несколько.
Например:
/* комментарии к программе */
/* начало алгоритма */
или
/* комментарии можно записать в следующем виде, однако, надо быть осторожным, чтобы внутри последовательности, которая игнорируется компилятором, не встретились операторы программы, которые также будут игнорироваться */
Неправильное определение комментариев:
/* комментарии к алгоритму /* решение краевой задачи */ */
1.2 Типы данных и их объявление
Важное отличие языка СИ от других языков является отсутствие принципа умолчания, что приводит к необходимости объявления всех переменных, используемых в программе явно вместе с указанием соответствующих им типов.
Объявления переменной имеет следующий формат:
[спецификатор-класса-памяти] спецификатор-типа
описатель [=инициатор] [,описатель [= инициатор] ]
Описатель - идентификатор простой переменной либо более сложная конструкция с квадратными скобками, круглыми скобками или звездочкой (набором звездочек).
Спецификатор типа - одно или несколько ключевых слов, определяющих тип объявляемой переменной. В языке СИ имеется стандартный набор типов данных, используя который можно сконструировать новые (уникальные) типы данных.
Инициатор - задает начальное значение или список начальных значений, которые (которое) присваивается переменной при объявлении.
Спецификатор класса памяти - определяется одним из четырех ключевых слов языка СИ: auto, register, static, extern и указывает, каким образом будет распределяться память под объявляемую переменную, с одной стороны, а, с другой, область видимости этой переменной, т.е. из каких частей программы можно к ней обратиться.
1.2.1 Категории типов данных
Ключевые слова для определения основных типов данных представлены в таблице 2.1.
Таблица 2.1 - Ключевые слова для определения основных типов данных
Целый тип |
Вещественный тип |
|
char |
float |
|
int |
double |
|
short |
long double |
|
long |
||
signed |
||
unsigned |
Переменная любого типа может быть объявлена как немодифицируемая. Это достигается добавлением ключевого слова const к спецификатору-типа. Объекты с типом const представляют собой данные, используемые только для чтения, т.е. этой переменной не может быть присвоено новое значение. Отметим, что если после слова const отсутствует спецификатор-типа, то подразумевается спецификатор типа int. Если ключевое слово const стоит перед объявлением составных типов (массив, структура, смесь, перечисление), то это приводит к тому, что каждый элемент также должен являться немодифицируемым, т.е. значение ему может быть присвоено только один раз.
Примеры:
const double A=1.23E-2;
const B=456; (подразумевается const int B=456)
Целый тип данных. Для определения данных целого типа используются различные ключевые слова, которые определяют диапазон значений и размер области памяти, выделяемой под переменные (таблица 2.2).
Таблица 2.2 - Диапазон значений и размер области памяти, выделяемой под переменные
Тип |
Размер памяти в байтах |
Диапазон значений |
|
char |
1 |
от -128 до 127 |
|
int |
для IBM XT,AT,SX,DX 2 |
от -32768 до 32767 |
|
short |
2 |
от -32768 до 32767 |
|
long |
4 |
от -2 147 483 648 до 2 147 483 647 |
|
unsigned char |
1 |
oт 0 до 255 |
|
unsigned int |
2 |
для IBM XT,AT,SX,DX |
|
unsigned short |
2 |
от 0 до 65535 |
|
unsigned long |
4 |
от 0 до 4 294 967 295 |
Отметим, что ключевые слова signed и unsigned необязательны. Они указывают, как интерпретируется нулевой бит объявляемой переменной, т.е., если указано ключевое слово unsigned, то нулевой бит интерпретируется как часть числа, в противном случае нулевой бит интерпретируется как знаковый. В случае отсутствия ключевого слова unsigned целая переменная считается знаковой. В том случае, если спецификатор типа состоит из ключевого типа signed или unsigned и далее следует идентификатор переменной, то она будет рассматриваться как переменная типа int.
Например:
unsigned int n;
int c; (подразумевается signed int c );
unsigned d; (подразумевается unsigned int d);
signed f; (подразумевается signed int f).
Отметим, что модификатор-типа char используется для представления символа (из массива представление символов) или для объявления строковых литералов. Значением объекта типа char является код (размером 1 байт), соответствующий представляемому символу. Для представления символов русского алфавита, модификатор типа идентификатора данных имеет вид unsigned char, так как коды русских букв превышают величину 127.
Следует сделать следующее замечание: в языке СИ не определено представление в памяти и диапазон значений для идентификаторов с модификаторами-типа int и unsigned int. Размер памяти для переменной с модификатором типа signed int определяется длиной машинного слова, которое имеет различный размер на разных машинах. Так, на 16-ти разрядных машинах размер слова равен 2-м байтам, на 32-х разрядных машинах, соответственно, 4-м байтам, т.е. тип int эквивалентен типам short int, или long int в зависимости от архитектуры используемой ПЭВМ.
Таким образом, одна и та же программа может правильно работать на одном компьютере и неправильно на другом. Для определения длины памяти, занимаемой переменной, можно использовать операцию sizeof языка СИ, возвращающую значение длины указанного модификатора-типа.
Формат операции: sizeof(выражение).
Например:
a = sizeof(int);
b = sizeof(long int);
c = sizeof(unsigned long);
d = sizeof(short);
В качестве выражения может быть использован любой идентификатор, либо имя типа, заключенное в скобки.
Отметим, что не может быть использовано имя типа void, а идентификатор не может относиться к полю битов или быть именем функции. Если в качестве выражения указанно имя массива, то результатом является размер всего массива (т.е. произведение числа элементов на длину типа), а не размер указателя, соответствующего идентификатору массива. Когда sizeof применяются к имени типа структуры или объединения или к идентификатору, имеющему тип структуры или объединения, то результатом является фактический размер структуры или объединения, который может включать участки памяти, используемые для выравнивания элементов структуры или объединения. Таким образом, этот результат может не соответствовать размеру, получаемому путем сложения размеров элементов структуры.
Отметим также, что восьмеричные и шестнадцатеричные константы также могут иметь модификатор unsigned. Это достигается указанием префикса u или U после константы, константа без этого префикса считается знаковой.
Например:
0xA8C (int signed);
01786l (long signed);
0xF7u (int unsigned);
Данные плавающего типа. Для переменных, представляющих число с плавающей точкой, используются следующие модификаторы-типа: float, double, long double (в некоторых реализациях языка объявление long double отсутствует).
Величина с модификатором-типа float занимает 4 байта. Из них 1 бит отводится для знака, 8 бит для избыточной экспоненты и 23 бита для мантиссы. Отметим, что старший бит мантиссы всегда равен 1, поэтому он не заполняется, в связи с этим диапазон значений переменной с плавающей точкой приблизительно равен от 3.14E-38 до 3.14E+38.
Величина типа double занимает 8 байт в памяти. Ее формат аналогичен формату float. Биты памяти распределяются следующим образом: 1 бит для знака, 11 бит для экспоненты и 52 бита для мантиссы. С учетом опущенного старшего бита мантиссы диапазон значений равен от 1.7E-308 до 1.7E+308.
Примеры объявления переменных вещественного типа:
float f, a, b;
double x, y;
1.2.2 Инициализация данных
При объявлении переменной ей можно присвоить начальное значение, присоединяя инициатор к описателю. Инициатор начинается со знака '=' и имеет следующие формы:
Формат 1: = инициатор;
Формат 2: = {список - инициаторов};
Формат 1 используется при инициализации переменных основных типов и указателей, а формат 2 - при инициализации составных объектов.
Примеры:
char n = 'N';
Переменная n инициализируется символом 'N'.
const long megabute = (1024 * 1024);
Немодифицируемая переменная megabute инициализируется константным выражением, после чего она не может быть изменена.
Инициализацию массива символов можно выполнить путем использования строкового литерала.
char stroka[ ] = 'Привет';
Инициализируется массив символов из 7 элементов, последним элементом (седьмым) будет символ ' ', которым завершаются все строковые литералы.
В том случае, если задается размер массива, а строковый литерал длиннее, чем размер массива, то лишние символы отбрасываются.
Следующее объявление инициализирует переменную stroka как массив, состоящий из семи элементов.
char stroka[5] = 'Привет';
В переменную stroka попадают первые пять элементов литерала, а символы 'т' и ' ' отбрасываются.
Если строка короче, чем размер массива, то оставшиеся элементы массива заполняются нулями.
1.3 Выражения и присваивания
1.3.1 Операнды и операции
Комбинация знаков операций и операндов, результатом которой является определенное значение, называется выражением. Знаки операций определяют действия, которые должны быть выполнены над операндами. Каждый операнд в выражении может быть выражением. Значение выражения зависит от расположения знаков операций и круглых скобок в выражении, а также от приоритета выполнения операций.
В языке СИ присваивание также является выражением, значением такого выражения является величина, которая присваивается.
При вычислении выражений тип каждого операнда может быть преобразован к другому типу. Преобразования типов могут быть неявными, при выполнении операций и вызовов функций; или явными, при выполнении операций приведения типов.
Операнд - это константа, литерал, идентификатор, вызов функции, индексное выражение, выражение выбора элемента или более сложное выражение, сформированное комбинацией операндов, знаков операций и круглых скобок. Любой операнд, который имеет константное значение, называется константным выражением. Каждый операнд имеет тип.
Если в качестве операнда используется константа, то ему соответствует значение и тип представляющей его константы. Целая константа может быть типа int, long, unsigned int, unsigned long, в зависимости от ее значения и от формы записи. Символьная константа имеет тип int. Константа с плавающей точкой всегда имеет тип double. Строковый литерал состоит из последовательности символов, заключенных в кавычки, и представляется в памяти как массив элементов типа char, инициализируемый указанной последовательностью символов. Значением строкового литерала является адрес первого элемента строки и синтаксически строковый литерал является немодифицируемым указателем на тип char. Строковые литералы могут быть использованы в качестве операндов в выражениях, допускающих величины типа указателей. Однако, так как строки не являются переменными, их нельзя использовать в левой части операции присваивания. Следует помнить, что последним символом строки всегда является нулевой символ, который автоматически добавляется при хранении строки в памяти.
В качестве операнда могут использоваться идентификаторы переменных и функций. Каждый идентификатор имеет тип, который устанавливается при его объявлении. Значение идентификатора зависит от типа следующим образом:
- идентификаторы объектов целых и плавающих типов представляют значения соответствующего типа;
- идентификатор объекта типа enum представлен значением одной константы из множества значений констант в перечислении. Значением идентификатора является константное значение. Тип значения есть int, что следует из определения перечисления;
Константное выражение - это выражение, результатом которого является константа. Операндом константного выражения могут быть целые константы, символьные константы, константы с плавающей точкой, константы перечисления, выражения приведения типов, выражения с операцией sizeof и другие константные выражения. Однако на использование знаков операций в константных выражениях налагаются следующие ограничения:
1. В константных выражениях нельзя использовать операции присваивания и последовательного вычисления (,).
2. Операция 'адрес' (&) может быть использована только при некоторых инициализациях.
Выражения со знаками операций могут участвовать в выражениях как операнды. Выражения со знаками операций могут быть унарными (с одним операндом), бинарными (с двумя операндами) и тернарными (с тремя операндами).
Унарное выражение состоит из операнда и предшествующего ему знаку унарной операции и имеет следующий формат:
знак-унарной-операции операнд
Бинарное выражения состоит из двух операндов, разделенных знаком бинарной операции:
операнд 1 знак-бинарной-операции операнд 2 .
Тернарное выражение состоит из трех операндов, разделенных знаками тернарной операции (?) и (:), и имеет формат:
операнд_1 ? операнд_2 : операнд_3 .
Операции. По количеству операндов, участвующих в операции, операции подразделяются на унарные, бинарные и тернарные.
В языке СИ имеются следующие унарные операции:
- арифметическое отрицание (отрицание и дополнение);
~ побитовое логическое отрицание (дополнение);
! логическое отрицание;
* разадресация (косвенная адресация);
& вычисление адреса;
+ унарный плюс;
++ увеличение (инкремент);
-- уменьшение (декремент);
sizeof размер.
Унарные операции выполняются справа налево.
В отличие от унарных, бинарные операции, выполняются слева направо. Бинарные операции можно классифицировать следующим образом:
Мультипликативные
* - Умножение ;
/ - Деление;
% - Остаток от деления.
Аддитивные
+ - Сложение;
- - Вычитание.
Операции сдвига
<< - Сдвиг влево;
>> - Сдвиг вправо.
Операции отношения
< - Меньше;
<= - Меньше или равно;
>= - Больше или равно;
== - Равно;
!= - Не равно.
Поразрядные операции
& - Поразрядное И ;
| - Поразрядное ИЛИ;
^ - Поразрядное исключающее ИЛИ.
Логические операции
&& - Логическое И ;
|| - Логическое ИЛИ.
Последовательного вычисления
, - Последовательное вычисление .
Операции присваивания
= - Присваивание;
*= - Умножение с присваиванием;
/= - Деление с присваиванием;
%= - Остаток от деления с присваиванием;
-= - Вычитание с присваиванием;
+= - Сложение с присваиванием;
<<= - Сдвиг влево с присваиванием;
>>= - Сдвиг вправо присваиванием;
&= - Поразрядное И с присваиванием;
|= - Поразрядное ИЛИ с присваиванием;
^= - Поразрядное исключающее ИЛИ с присваиванием.
Левый операнд операции присваивания должен быть выражением, ссылающимся на область памяти (но не объектом, объявленным с ключевым словом const), такие выражения называются леводопустимыми, к ним в первую очередь относятся:
- идентификаторы данных целого и плавающего типов, типов указателя, структуры, объединения;
При записи выражений следует помнить, что символы (*), (&), (!), (+) могут обозначать унарную или бинарную операцию.
Преобразования при вычислении выражений
При выполнении операций производится автоматическое преобразование типов, чтобы привести операнды выражений к общему типу или, чтобы расширить короткие величины до размера целых величин, используемых в машинных командах. Выполнение преобразования зависит от специфики операций и от типа операнда или операндов.
Рассмотрим общие арифметические преобразования.
1. Операнды типа float преобразуются к типу double.
2. Если один операнд long double, то второй преобразуется к этому же типу.
3. Если один операнд double, то второй также преобразуется к типу double.
4. Любые операнды типа char и short преобразуются к типу int.
5. Любые операнды unsigned char или unsigned short преобразуются к типу unsigned int.
6. Если один операнд типа unsigned long, то второй преобразуется к типу unsigned long.
7. Если один операнд типа long, то второй преобразуется к типу long.
8. Если один операнд типа unsigned int, то второй операнд преобразуется к этому же типу.
Таким образом, можно отметить, что при вычислении выражений операнды преобразуются к типу того операнда, который имеет наибольший размер.
Пример:
double ft, sd; unsigned char ch; unsigned long in; int i;
....
sd = ft * (i + ch / in);
При выполнении оператора присваивания правила преобразования будут использоваться следующим образом. Операнд ch преобразуется к unsigned int (правило 5). Затем он преобразуется к типу unsigned long (правило 6). По этому же правилу i преобразуется к unsigned long и результат операции, заключенной в круглые скобки, будет иметь тип unsigned long. Затем он преобразуется к типу double (правило 3) и результат всего выражения будет иметь тип double.
Явное преобразование типов может быть осуществлено посредством операции приведения типов, которая имеет формат:
(имя-типа) операнд
В приведенной записи имя-типа задает тип, к которому должен быть преобразован операнд.
Пример:
int i=2; long l=2; double d; float f;
d = (double)i * (double)l;
f = (float)d;
В данном примере величины i, l, d будут явно преобразовываться к указанным в круглых скобках типам.
Рассмотрим некоторые операции языка СИ.
Операция арифметического, логического отрицания и операция дополнения
Операция арифметического отрицания (-)
Пример: int i = 5, k; k = -i; // k =-5
Операция логического отрицания 'НЕ' (!) вырабатывает значение 0, если операнд есть истина (не нуль), и значение 1, если операнд равен нулю (0). Результат имеет тип int. Операнд должен быть целого или плавающего типа или типа указатель. Пример: int t, z=0; t=!z; Переменная t получит значение равное 1, так как переменная z имела значение равное 0 (ложно).
Операция двоичного дополнения (~) вырабатывает двоичное дополнение своего операнда. Операнд должен быть целого типа. Осуществляется обычное арифметическое преобразование, результат имеет тип операнда после преобразования.
Пример: int i = 5, k; k = ~i;
При выполнении операции дополнения десятичное число преобразуется в двоичное, инвертируется, добавляется 1 к младшему разряду. В данном случае, десятичное число 5 в двоичном коде есть 101, инверсия этого числа есть -010, в результате будет -011, в десятичном коде - -3. Дополнение - операция определения дополнения до 1.
Пример: char b = '9'; unsigned char f = b, c = ~f;
В результате операции ~f будет получено число, что соответствует символу 'Ж'.
Операции логического отрицания; разадресации, вычисления адреса будут рассмотрены ниже.
Операции увеличения и уменьшения. Операции увеличения (++) и уменьшения (--) являются унарными операциями присваивания. Они соответственно увеличивают или уменьшают значения операнда на единицу. Операнд может быть целого или плавающего типа или типа указатель и должен быть модифицируемым. Операнд целого или плавающего типа увеличиваются (уменьшаются) на единицу. Тип результата соответствует типу операнда. Операнд адресного типа увеличивается или уменьшается на размер объекта, который он адресует. В языке допускается префиксная или постфиксная формы операций увеличения (уменьшения), поэтому значения выражения, использующего операции увеличения (уменьшения) зависит от того, какая из форм указанных операций используется. Если знак операции стоит перед операндом (префиксная форма записи), то изменение операнда происходит до его использования в выражении и результатом операции является увеличенное или уменьшенное значение операнда. В том случае если знак операции стоит после операнда (постфиксная форма записи), то операнд вначале используется для вычисления выражения, а затем происходит изменение операнда.
Примеры:
int t = 1, s = 2, z, f; z = (t++) * 5;
Вначале происходит умножение t*5, а затем увеличение t. В результате получится z=5, t=2.
f = (++s) / 3;
Вначале значение s увеличивается, а затем используется в операции деления. В результате получим s=3, f=1.
В случае, если операции увеличения и уменьшения используются как самостоятельные операторы, префиксная и постфиксная формы записи становятся эквивалентными.
z++; /* эквивалентно */ ++z;
Мультипликативные операции. К этому классу операций относятся операции умножения (*), деления (/) и получение остатка от деления (%). Операндами операции (%) должны быть целые числа. Отметим, что типы операндов операций умножения и деления могут отличаться, и для них справедливы правила преобразования типов. Типом результата является тип операндов после преобразования.
Операция умножения (*) выполняет умножение операндов.
Пример:
int i = 5;
float f = 0.2;
double g, z;
g = f * i;
Тип произведения i и f преобразуется к типу double, затем результат присваивается переменной g.
Операция деления (/) выполняет деление первого операнда на второй. Если две целые величины не делятся нацело, то результат округляется в сторону нуля.
При попытке деления на ноль выдается сообщение во время выполнения.
Пример:
int i = 49, j = 10, n, m;
n = i / j; /* результат 4 */
m = i / (-j); /* результат -4 */
Операция остаток от деления (%) дает остаток от деления первого операнда на второй.
Знак результата зависит от конкретной реализации. Если второй операнд равен нулю, то выдается сообщение.
Пример:
int n = 49, m = 10, i, j, k, l;
i = n % m; /* 9 */
j = n % (-m); /* 9 */
k = (-n) % m; /* -9 */
Аддитивные операции. К аддитивным операциям относятся сложение (+) и вычитание (-). Операнды могут быть целого или плавающего типов. В некоторых случаях над операндами аддитивных операций выполняются общие арифметические преобразования. Однако преобразования, выполняемые при аддитивных операциях, не обеспечивают обработку ситуаций переполнения и потери значимости. Информация теряется, если результат аддитивной операции не может быть представлен типом операндов после преобразования. При этом сообщение об ошибке не выдается.
Пример:
int i=30000, j=30000, k; k = i + j;
В результате сложения k получит значение равное -5536.
Результатом выполнения операции сложения является сумма двух операндов. Операнды могут быть целого или плавающего типа или один операнд может быть указателем, а второй - целой величиной.
Когда целая величина складывается с указателем, то целая величина преобразуется путем умножения ее на размер памяти, занимаемой величиной, адресуемой указателем.
Когда преобразованная целая величина складывается с величиной указателя, то результатом является указатель, адресующий ячейку памяти, расположенную на целую величину дальше от исходного адреса. Новое значение указателя адресует тот же самый тип данных, что и исходный указатель.
Операция вычитания (-) вычитает второй операнд из первого. Возможна следующая комбинация операндов:
1. Оба операнда целого или плавающего типа.
2. Оба операнда являются указателями на один и тот же тип.
3. Первый операнд является указателем, а второй - целым.
Отметим, что операции сложения и вычитания над адресами в единицах, отличных от длины типа, могут привести к непредсказуемым результатам.
Операции сдвига. Операции сдвига осуществляют смещение операнда влево (<<) или вправо (>>) на число битов, задаваемое вторым операндом. Оба операнда должны быть целыми величинами. Выполняются обычные арифметические преобразования. При сдвиге влево правые освобождающиеся биты устанавливаются в нуль. При сдвиге вправо метод заполнения освобождающихся левых битов зависит от типа первого операнда. Если тип unsigned, то свободные левые биты устанавливаются в нуль. В противном случае, они заполняются копией знакового бита. Результат операции сдвига не определен, если второй операнд отрицательный.
Преобразования, выполненные операциями сдвига, не обеспечивают обработку ситуаций переполнения и потери значимости. Информация теряется, если результат операции сдвига не может быть представлен типом первого операнда, после преобразования.
Отметим, что сдвиг влево соответствует умножению первого операнда на степень числа 2, равную второму операнду, а сдвиг вправо соответствует делению первого операнда на 2 в степени, равной второму операнду.
Примеры:
long int i = 0x1234, j, k ;
k = i << 4 ; /* k = 0x2340 */
j = i << 8 ; /* j = 0x3400 */
Поразрядные операции. К поразрядным операциям относятся: операция поразрядного логического 'И' (&), операция поразрядного логического 'ИЛИ' (|), операция поразрядного 'исключающего ИЛИ' (^).
Операнды поразрядных операций могут быть любого целого типа. При необходимости над операндами выполняются преобразования по умолчанию, тип результата - это тип операндов после преобразования.
Операция поразрядного логического И (&) сравнивает каждый бит первого операнда с соответствующим битом второго операнда. Если оба сравниваемых бита единицы, то соответствующий бит результата устанавливается в 1, в противном случае в 0.
Операция поразрядного логического ИЛИ (|) сравнивает каждый бит первого операнда с соответствующим битом второго операнда. Если любой (или оба) из сравниваемых битов равен 1, то соответствующий бит результата устанавливается в 1, в противном случае результирующий бит равен 0.
Операция поразрядного исключающего ИЛИ (^) сравнивает каждый бит первого операнда с соответствующими битами второго операнда. Если один из сравниваемых битов равен 0, а второй бит равен 1, то соответствующий бит результата устанавливается в 1, в противном случае, т.е. когда оба бита равны 1 или 0, бит результата устанавливается в 0.
Примеры:
i = 0x45FF, /* i= 0100 0101 1111 1111 */
j = 0x00FF; /* j= 0000 0000 1111 1111 */
r = i ^ j; /* r=0x4500 = 0100 0101 0000 0000 */
r = i | j; /* r=0x45FF = 0100 0101 1111 1111 */
r = i & j /* r=0x00FF = 0000 0000 1111 1111 */
Логические операции. К логическим операциям относятся операция логического И (&&) и операция логического ИЛИ (||). Операнды логических операций могут быть целого типа, плавающего типа или типа указателя, при этом в каждой операции могут участвовать операнды различных типов. Операнды логических выражений вычисляются слева направо. Если значения первого операнда достаточно, чтобы определить результат операции, то второй операнд не вычисляется. Логические операции не вызывают стандартных арифметических преобразований. Они оценивают каждый операнд с точки зрения его эквивалентности нулю. Результатом логической операции является 0 или 1, тип результата int.
Операция логического И (&&) вырабатывает значение 1, если оба операнда имеют нулевые значения. Если один из операндов равен 0, то результат также равен 0. Если значение первого операнда равно 0, то второй операнд не вычисляется.
Операция логического ИЛИ (||) выполняет над операндами операцию включающего ИЛИ. Она вырабатывает значение 0, если оба операнда имеют значение 0, если какой-либо из операндов имеет ненулевое значение, то результат операции равен 1. Если первый операнд имеет ненулевое значение, то второй операнд не вычисляется.
Составное присваивание. Кроме простого присваивания, имеется целая группа операций присваивания, которые объединяют простое присваивание с одной из бинарных операций. Такие операции называются составными операциями присваивания и имеют вид:
(операнд_1) (бинарная операция) = (операнд_2) .
Составное присваивание по результату эквивалентно следующему простому присваиванию:
(операнд_1) = (операнд_1) (бинарная операция) (операнд_2) .
Отметим, что выражение составного присваивания, с точки зрения реализации, не эквивалентно простому присваиванию, так как в последнем случае операнд_1 вычисляется дважды.
Каждая операция составного присваивания выполняет преобразования, которые осуществляются соответствующей бинарной операцией. Левым операндом операций (+=) (-=) может быть указатель, в то время как правый операнд должен быть целым числом.
Примеры:
double a = 2.0;
double b=3.0;
b+=a; /* эквивалентно b=b+a */
Условная операция. В языке СИ имеется одна тернарная операция - условная операция, которая имеет следующий формат:
операнд_1 ? операнд_2 : операнд_3
Операнд_1 должен быть целого или плавающего типа или быть указателем. Он оценивается с точки зрения его эквивалентности 0. Если операнд_1 не равен 0, то вычисляется операнд_2 и его значение является результатом операции. Если операнд_1 равен 0, то вычисляется операнд_3 и его значение является результатом операции. Следует отметить, что вычисляется либо операнд_2, либо операнд_3, но не оба. Тип результата зависит от типов операнда_2 и операнда_3, следующим образом.
1. Если операнд_2 или операнд_3 имеет целый или плавающий тип (отметим, что их типы могут отличаться), то выполняются обычные арифметические преобразования. Типом результата является тип операнда после преобразования.
2. Если операнд_2 и операн_3 имеют один и тот же тип структуры, объединения или указателя, то тип результата будет тем же самым типом структуры, объединения или указателя.
3. Если оба операнда имеют тип void, то результат имеет тип void.
4. Если один операнд является указателем на объект любого типа, а другой операнд является указателем на vold, то указатель на объект преобразуется к указателю на vold, который и будет типом результата.
5. Если один из операндов является указателем, а другой константным выражением со значением 0, то типом результата будет тип указателя.
Пример:
max = (d <= b) ? b : d;
Переменной max присваивается максимальное значение переменных d и b.
1.3.2 Приоритеты операций и порядок вычислений
В языке СИ операции с высшими приоритетами вычисляются первыми. Наивысшим приоритетом является приоритет, равный 1 (табл. 1.3).
Таблица 1.3 - Приоритеты операций
Приоритет |
Знак операции |
|
1. |
~ ! * & ++ -- sizeof приведение типов |
|
2. |
() [] . -> |
|
3. |
* / % |
|
4. |
+ - |
|
5. |
<< >> |
|
6. |
< > <= >= |
|
7. |
== !=Отношение (равенство) |
|
8. |
& Поразрядное И |
|
9. |
^ Поразрядное исключающее ИЛИ |
|
10. |
| Поразрядное ИЛИ |
|
11. |
&& Логическое И |
|
12. |
|| Логическое ИЛИ |
|
13. |
? : Условная |
|
14. |
= * = / = % = + = - = & = | = >> = << = ^ = |
|
15. |
, Последовательное вычисление |
1.3.3 Побочные эффекты
Операции присваивания в сложных выражениях могут вызывать побочные эффекты, так как они изменяют значение переменной. Побочный эффект может возникать и при вызове функции, если он содержит прямое или косвенное присваивание (через указатель). Это связано с тем, что аргументы функции могут вычисляться в любом порядке. Например, побочный эффект имеет место в следующем вызове функции: prog (a, a=k*2).
В зависимости от того, какой аргумент вычисляется первым, в функцию могут быть переданы различные значения. Порядок вычисления операндов некоторых операций зависит от реализации и поэтому могут возникать разные побочные эффекты, если в одном из операндов используется операции увеличения или уменьшения, а также другие операции присваивания.
Например, выражение i*j+(j++)+(--i) может принимать различные значения при обработке разными компиляторами. Чтобы избежать недоразумений при выполнении побочных эффектов, необходимо придерживаться следующих правил.
1. Не использовать операции присваивания переменной в вызове функции, если эта переменная участвует в формировании других аргументов функции.
2. Не использовать операции присваивания переменной в выражении, если эта переменная используется в выражении более одного раза.
1.4 Операторы
Все операторы языка СИ могут быть условно разделены на следующие категории:
- оператор 'выражение', пустой оператор и др.;
- условные операторы, к которым относятся:
- оператор условия if и оператор выбора switch;
- операторы цикла (for,while,do while);
- операторы перехода (break, continue, return, goto);
Операторы в программе могут объединяться в составные операторы с помощью фигурных скобок. Любой оператор в программе может быть помечен меткой, состоящей из имени и следующего за ним двоеточия.
Все операторы языка СИ, кроме составных операторов, заканчиваются точкой с запятой ';'.
1.4.1 Оператор выражение
Любое выражение, которое заканчивается точкой с запятой, является оператором. Выполнение оператора выражение заключается в вычислении выражения. Полученное значение выражения никак не используется, поэтому, как правило, такие выражения вызывают побочные эффекты. Заметим, что вызвать функцию, невозвращающую значения можно только при помощи оператора выражения. Правила вычисления выражений были сформулированы выше.
Примеры:
++ i;
Этот оператор представляет выражение, которое увеличивает значение переменной i на единицу.
a=cos(b * 5);
Этот оператор представляет выражение, включающее в себя операции присваивания и вызова функции.
a(x,y);
Этот оператор представляет выражение, состоящее из вызова функции.
1.4.2 Пустой оператор
Пустой оператор состоит только из точки с запятой. При выполнении этого оператора ничего не происходит. Он обычно используется в следующих случаях:
- в операторах do, for, while, if в строках, когда оператор не требуется, но по синтаксису требуется хотя бы один оператор;
- при необходимости пометить фигурную скобку.
Синтаксис языка СИ требует, чтобы после метки обязательно следовал оператор. Фигурная же скобка оператором не является. Поэтому, если надо передать управление на фигурную скобку, необходимо использовать пустой оператор.
Пример: int main ( ) {
:
{
if (...) goto a; /* переход на скобку */
{ ... }
a:;
}
return 0; }
Отметим также, что выражение, стоящее после return, может быть заключено в круглые скобки, хотя наличие последних необязательно.
1.4.3 Составной оператор
Составной оператор представляет собой несколько операторов и объявлений, заключенных в фигурные скобки:
{ [oбъявление]
:
оператор; [оператор];
: }
Заметим, что в конце составного оператора точка с запятой не ставится.
Выполнение составного оператора заключается в последовательном выполнении составляющих его операторов.
1.4.4 Условные операторы
Оператор if
Формат оператора:
if (выражение) оператор_1; [else оператор_2;]
Выполнение оператора if начинается с вычисления выражения.
Далее выполнение осуществляется по следующей схеме:
- если выражение истинно (т.е. отлично от 0), то выполняется оператор_1.
- если выражение ложно (т.е. равно 0), то выполняется оператор_2.
- если выражение ложно и отсутствует оператор_2 (в квадратные скобки заключена необязательная конструкция), то выполняется следующий за if оператор.
После выполнения оператора if значение передается на следующий оператор программы, если последовательность выполнения операторов программы не будет принудительно нарушена использованием операторов перехода.
Пример:
if (i < j) i++;
else { j = i - 3; i++; }
Этот пример иллюстрирует также и тот факт, что на месте оператор_1, так же как и на месте оператор_2 могут находиться сложные конструкции.
Допускается использование вложенных операторов if. Оператор if может быть включен в конструкцию if или в конструкцию else другого оператора if. Чтобы сделать программу более читабельной, рекомендуется группировать операторы и конструкции во вложенных операторах if, используя фигурные скобки. Если же фигурные скобки опущены, то компилятор связывает каждое ключевое слово else с наиболее близким if, для которого нет else.
Примеры:
int main ( ) { int t = 2, b = 7, r = 3;
if (t > b)
{
if (b < r) r = b;
}
else r = t;
return (0); }
В результате выполнения этой программы r станет равным 2.
Если же в программе опустить фигурные скобки, стоящие после оператора if, то программа будет иметь следующий вид:
int main ( )
{
int t = 2, b = 7, r = 3;
if ( t > b )
if ( b < r ) r = b;
else r = t;
return (0);
}
В этом случае r получит значение равное 3, так как ключевое слово else относится ко второму оператору if, который не выполняется, поскольку не выполняется условие, проверяемое в первом операторе if.
Следующий фрагмент иллюстрирует вложенные операторы if:
char ZNAC;
int x, y, z;
:
if (ZNAC == '-') x = y - z;
else if (ZNAC == '+') x = y + z;
else if (ZNAC == '*') x = y * z;
else if (ZNAC == '/') x = y / z;
else ...
Из анализа этого примера можно сделать вывод, что конструкции, использующие вложенные операторы if, являются довольно громоздкими и не всегда достаточно надежными.
Другим способом организации выбора из множества различных вариантов является использование специального оператора выбора switch.
Оператор switch
Оператор switch предназначен для организации выбора из множества различных вариантов. Формат оператора следующий:
switch (выражение)
{ [объявление]
:
[case константное-выражение1]: [ список-операторов1]
[case константное-выражение2]: [ список-операторов2]
:
:
[default: [ список операторов ]]
}
Выражение, следующее за ключевым словом switch в круглых скобках, может быть любым выражением, допустимыми в языке СИ, значение которого должно быть целым. Отметим, что можно использовать явное приведение к целому типу, однако, необходимо помнить о тех ограничениях и рекомендациях, о которых говорилось выше.
Значение этого выражения является ключевым для выбора из нескольких вариантов. Тело оператора smitch состоит из нескольких операторов, помеченных ключевым словом case с последующим константным выражением. Следует отметить, что использование целого константного выражения является существенным недостатком, присущим рассмотренному оператору.
Так как константное выражение вычисляется во время трансляции, оно не может содержать переменные или вызовы функций. Обычно в качестве константного выражения используются целые или символьные константы.
Все константные выражения в операторе switch должны быть уникальны. Кроме операторов, помеченных ключевым словом case, может быть, но обязательно один, фрагмент, помеченный ключевым словом default.
Список операторов может быть пустым, либо содержать один или более операторов. Причем, в операторе switch не требуется заключать последовательность операторов в фигурные скобки.
Отметим также, что в операторе switch можно использовать свои локальные переменные, объявления которых находятся перед первым ключевым словом case, однако в объявлениях не должна использоваться инициализация.
Схема выполнения оператора switch следующая:
- вычисляется выражение в круглых скобках;
- вычисленные значения последовательно сравниваются с константными выражениями, следующими за ключевыми словами case;
- если одно из константных выражений совпадает со значением выражения, то управление передается на оператор, помеченный соответствующим ключевым словом case;
- если ни одно из константных выражений не равно выражению, то управление передается на оператор, помеченный ключевым словом default, а в случае его отсутствия управление передается на следующий после switch оператор.
Отметим интересную особенность использования оператора switch: конструкция со словом default может быть не последней в теле оператора switch. Ключевые слова case и default в теле оператора switch существенны только при начальной проверке, когда определяется начальная точка выполнения тела оператора switch. Все операторы, между начальным оператором и концом тела, выполняются вне зависимости от ключевых слов, если только какой-то из операторов не передаст управления из тела оператора switch. Таким образом, программист должен сам позаботиться о выходе из case, если это необходимо. Чаще всего для этого используется оператор break.
Для того, чтобы выполнить одни и те же действия для различных значений выражения, можно пометить один и тот же оператор несколькими ключевыми словами case.
Пример:
int i = 2;
switch (i)
{
case 1: i += 2;
case 2: i *= 3;
case 0: i /= 2;
case 4: i -= 5;
default: ;
}
Выполнение оператора switch начинается с оператора, помеченного case 2. Таким образом, переменная i получает значение, равное 6, далее выполняется оператор, помеченный ключевым словом case 0, а затем case 4, переменная i примет значение 3, а затем значение -2. Оператор, помеченный ключевым словом default, не изменяет значения переменной.
Рассмотрим ранее приведенный пример, в котором иллюстрировалось использование вложенных операторов if, переписанный теперь с использованием оператора switch.
char ZNAC;
int x,y,z;
switch (ZNAC)
{
case '+': x = y + z; break;
case '-': x = y - z; break;
case '*': x = y * z; break;
case '/': x = u / z; break;
default: ;
}
Использование оператора break позволяет в необходимый момент прервать последовательность выполняемых операторов в теле оператора switch, путем передачи управления оператору, следующему за switch.
Отметим, что в теле оператора switch можно использовать вложенные операторы switch, при этом в ключевых словах case можно использовать одинаковые константные выражения.
Пример:
:
switch (a)
{
case 1: b=c; break;
case 2:
switch (d)
{ case 0: f=s; break;
case 1: f=9; break;
case 2: f-=9; break;
}
case 3: b-=c; break;
:
}
Оператор break
Оператор break обеспечивает прекращение выполнения самого внутреннего из объединяющих его операторов switch, do, for, while. После выполнения оператора break управление передается оператору, следующему за прерванным.
Операция последовательного вычисления
Операция последовательного вычисления обозначается запятой (,) и используется для вычисления двух и более выражений там, где по синтаксису допустимо только одно выражение. Эта операция вычисляет два операнда слева направо. При выполнении операции последовательного вычисления, преобразование типов не производится. Операнды могут быть любых типов. Результат операции имеет значения и тип второго операнда. Отметим, что запятая может использоваться также как символ разделитель, поэтому необходимо по контексту различать, запятую, используемую в качестве разделителя или знака операции. Операция последовательного вычисления часто используется в операторе цикла for.
1.4.5 Операторы организации цикла
Оператор for
Оператор for - это наиболее общий способ организации цикла. Он имеет следующий формат:
for ( выражение_1; выражение_2 ; выражение_3) тело
Выражение_1 обычно используется для установления начального значения переменных, управляющих циклом. Выражение_2 - это выражение, определяющее условие, при котором тело цикла будет выполняться. Выражение_3 определяет изменение переменных, управляющих циклом после каждого выполнения тела цикла.
Схема выполнения оператора for:
1. Вычисляется выражение_1.
2. Вычисляется выражение_2.
3. Если значения выражения_2 отлично от нуля (истина), выполняется тело цикла, вычисляется выражение_3 и осуществляется переход к пункту 2, если выражение_2 равно нулю (ложь), то управление передается на оператор, следующий за оператором for.
Существенно то, что проверка условия всегда выполняется в начале цикла. Это значит, что тело цикла может ни разу не выполниться, если условие выполнения сразу будет ложным.
Пример:
int main()
{ int i, b;
for (i=1; i <b; i++) …….
Некоторые варианты использования оператора for повышают его гибкость за счет возможности использования нескольких переменных, управляющих циклом.
Пример:
int main()
{ int top, bot;
char string[100], temp;
for ( top=0, bot=100 ; top < bot ; top++, bot--)
{ temp=string[top];
string[bot]=temp;
}
return 0;
}
В этом примере, реализующем запись строки символов в обратном порядке, для управления циклом используются две переменные top и bot. Отметим, что на месте выражение_1 и выражение_3 здесь используются несколько выражений, записанных через запятую, и выполняемых последовательно.
Другим вариантом использования оператора for является бесконечный цикл. Для организации такого цикла можно использовать пустое условное выражение, а для выхода из цикла обычно используют дополнительное условие и оператор break.
Пример:
for (;;)
{ ...
... break;
...
}
Так как согласно синтаксису языка СИ оператор может быть пустым, тело оператора for также может быть пустым. Такая форма оператора может быть использована для организации поиска.
Оператор while
Оператор цикла while называется циклом с предусловием и имеет следующий формат:
while (выражение) тело
В качестве выражения допускается использовать любое выражение языка СИ, а в качестве тела любой оператор, в том числе пустой или составной. Схема выполнения оператора while следующая:
1. Вычисляется выражение.
2. Если выражение ложно, то выполнение оператора while заканчивается и выполняется следующий по порядку оператор. Если выражение истинно, то выполняется тело оператора while.
3. Процесс повторяется с пункта 1.
Оператор цикла вида
for (выражение_1; выражение_2; выражение_3) тело ;
Оператор for может быть заменен оператором while следующим образом:
выражение_1;
while (выражение_2)
{ тело
выражение_3; }
Так же, как и при выполнении оператора for, в операторе while вначале происходит проверка условия. Поэтому оператор while удобно использовать в ситуациях, когда тело оператора не всегда нужно выполнять.
Внутри операторов for и while можно использовать локальные переменные, которые должны быть объявлены с определением соответствующих типов.
Оператор do while
Оператор цикла do while называется оператором цикла с постусловием и используется в тех случаях, когда необходимо выполнить тело цикла хотя бы один раз. Формат оператора имеет следующий вид:
do тело while (выражение);
Схема выполнения оператора do while:
1. Выполняется тело цикла (которое может быть составным оператором).
2. Вычисляется выражение.
3. Если выражение ложно, то выполнение оператора do while заканчивается и выполняется следующий по порядку оператор. Если выражение истинно, то выполнение оператора продолжается с пункта 1.
Чтобы прервать выполнение цикла до того, как условие станет ложным, можно использовать оператор break.
Операторы while и do while могут быть вложенными.
Пример:
int i, j, k;
...
i=0; j=0; k=0;
do {
i++;
j--;
k++;
}
while (a[k] < i)
Оператор continue
Оператор continue, как и оператор break, используется только внутри операторов цикла, но в отличие от него выполнение программы продолжается не с оператора, следующего за прерванным оператором, а с начала прерванного оператора.
Формат оператора следующий:
continue;
Оператор continue, как и оператор break, прерывает самый внутренний из объемлющих его циклов.
Оператор goto
Использование оператора безусловного перехода goto в практике программирования на языке СИ настоятельно не рекомендуется, так как он затрудняет понимание программ и возможность их модификаций.
Формат этого оператора следующий:
goto имя-метки;
...
имя-метки: оператор;
Оператор goto передает управление на оператор, помеченный меткой имя-метки. Помеченный оператор должен находиться в той же функции, что и оператор goto, а используемая метка должна быть уникальной, т.е. одно имя-метки не может быть использовано для разных операторов программы. Имя метки - это идентификатор. Любой оператор в составном операторе может иметь свою метку.
Используя оператор goto, можно передавать управление внутрь составного оператора. Но нужно быть осторожным при входе в составной оператор, содержащий объявления переменных с инициализацией, так как объявления располагаются перед выполняемыми операторами и значения объявленных переменных при таком переходе будут не определены.
1.5 Типы данных, массивы, перечисляемые типы данных
1.5.1 Определение типов
Для объявления типов можно использовать ключевое слово typedef.
При объявлении с ключевым словом typedef идентификатор, стоящий на месте описываемого объекта, является именем вводимого в рассмотрение типа данных, и далее этот тип может быть использован для объявления переменных. Отметим, что любой тип может быть объявлен с использованием ключевого слова typedef, включая типы указателя, функции или массива.
Пример:
typedef char FIO[40] /* FIO - массив из сорока символов */
FIO person; /* Переменная person - массив из сорока символов */
/* Это эквивалентно объявлению */
char person[40];
1.5.2 Массивы
Массивы - это группа элементов одинакового типа (double, float, int и т.п.). Из объявления массива компилятор должен получить информацию о типе элементов массива и их количестве.
Объявление массива имеет два формата:
спецификатор-типа описатель [константное - выражение];
спецификатор-типа описатель [ ];
Описатель - это идентификатор массива.
Спецификатор-типа задает тип элементов объявляемого массива. Элементами массива не могут быть функции и элементы типа void.
Константное-выражение в квадратных скобках задает количество элементов массива. Константное-выражение при объявлении массива может быть опущено в следующих случаях:
- при объявлении массив инициализируется;
- массив объявлен как формальный параметр функции;
- массив объявлен как ссылка на массив, явно определенный в другом файле.
В языке СИ определены только одномерные массивы, но поскольку элементом массива может быть массив, можно определить и многомерные массивы. Они формализуются списком константных-выражений, следующих за идентификатором массива, причем каждое константное-выражение заключается в свои квадратные скобки.
Каждое константное-выражение в квадратных скобках определяет число элементов по данному измерению массива, так что объявление двухмерного массива содержит два константных-выражения, трехмерного - три и т.д. Отметим, что в языке СИ первый элемент массива имеет индекс равный 0.
Примеры:
int a[2][3]; /* представлено в виде матрицы
a[0][0] a[0][1] a[0][2]
a[1][0] a[1][1] a[1][2] */
double b[10]; /* вектор из 10 элементов имеющих тип double */
int w[3][3] = { { 2, 3, 4 },{ 3, 4, 8 }, 1, 0, 9 } };
В последнем примере объявлен массив w[3][3]. Списки, выделенные в фигурные скобки, соответствуют строкам массива, в случае отсутствия скобок инициализация будет выполнена неправильно.
В языке СИ можно использовать сечения массива, как и в других языках высокого уровня (PL1 и т.п.), однако на использование сечений накладывается ряд ограничений. Сечения формируются вследствие опускания одной или нескольких пар квадратных скобок. Пары квадратных скобок можно отбрасывать только справа налево и строго последовательно. Сечения массивов используются при организации вычислительного процесса в функциях языка СИ, разрабатываемых пользователем. Примеры:
int s[2][3];
Если при обращении к некоторой функции написать s[0], то будет передаваться нулевая строка массива s.
int b[2][3][4];
При обращении к массиву b можно написать, например, b[1][2] и будет передаваться вектор из четырех элементов, а обращение b[1] даст двухмерный массив размером 3 на 4. Нельзя написать b[2][4], подразумевая, что передаваться будет вектор, потому что это не соответствует ограничению наложенному на использование сечений массива.
Пример объявления символьного массива:
char str[] = 'объявление символьного массива';
Следует учитывать, что в символьном литерале находится на один элемент больше, так как последний из элементов является управляющей последовательностью ' '.
Примеры инициализации массивов:
static int b[2][2] = {1,2,3,4};
Инициализируется двухмерный массив b целых величин, элементам массива присваиваются значения из списка.
Эта же инициализация может быть выполнена следующим образом:
static int b[2][2] = { { 1,2 }, { 3,4 } };
Если при инициализации указано меньше значений для строк, то оставшиеся элементы инициализируются 0, т.е. при описании
static int b[2][2] = { { 1,2 }, { 3 } };
элементы первой строки получат значения 1 и 2, а второй 3 и 0.
При инициализации массива можно опустить одну или несколько размерностей
static int b[3][] = { { 1,2 }, { 3,4 } };
1.5.3 Перечислимые типы данных
Переменная, которая может принимать значение из некоторого списка значений, называется переменной перечислимого типа или перечислением.
Объявление перечисления начинается с ключевого слова enum и имеет два формата представления.
Формат 1:
enum [имя-тега-перечисления] {список-перечисления} описатель [, описатель...];
Формат 2:
enum имя-тега-перечисления описатель [, описатель..];
Объявление перечисления задает тип переменной перечисления и определяет список именованных констант, называемый списком-перечисления. Значением каждого имени списка является некоторое целое число.
Переменная типа перечисления может принимать значения одной из именованных констант списка. Именованные константы списка имеют тип int.
Таким образом, память, соответствующая переменной перечисления, это память необходимая для размещения значения типа int.
Переменная типа enum могут использоваться в индексных выражениях и как операнды в арифметических операциях и в операциях отношения.
В первом формате 1 имена и значения перечисления задаются в списке перечислений. Необязательное имя-тега-перечисления, это идентификатор, который именует тег перечисления, определенный списком перечисления. Описатель именует переменную перечисления. В объявлении может быть задана более чем одна переменная типа перечисления.
Список-перечисления содержит одну или несколько конструкций вида:
идентификатор [= константное выражение]
Каждый идентификатор именует элемент перечисления. Все идентификаторы в списке enum должны быть уникальными. В случае отсутствия константного выражения первому идентификатору соответствует значение 0, следующему идентификатору - значение 1 и т.д. Имя константы перечисления эквивалентно ее значению. Идентификатор, связанный с константным выражением, принимает значение, задаваемое этим константным выражением. Константное выражение должно иметь тип int и может быть как положительным, так и отрицательным. Следующему идентификатору в списке присваивается значение, равное константному выражению плюс 1, если этот идентификатор не имеет своего константного выражения.
Использование элементов перечисления должно подчиняться следующим правилам:
1. Переменная может содержать повторяющиеся значения.
2. Идентификаторы в списке перечисления должны быть отличны от всех других идентификаторов в той же области видимости, включая имена обычных переменных и идентификаторы из других списков перечислений.
3. Имена типов перечислений должны быть отличны от других имен типов перечислений, структур и смесей в этой же области видимости.
4. Значение может следовать за последним элементом списка перечисления.
Пример:
enum week { SUB = 0, /* 0 */
VOS = 0, /* 0 */
POND, /* 1 */
VTOR, /* 2 */
SRED, /* 3 */
HETV, /* 4 */
PJAT /* 5 */
} rab_ned;
В данном примере объявлен перечислимый тег week, с соответствующим множеством значений, и объявлена переменная rab_ned имеющая тип week.
Во втором формате используется имя тега перечисления для ссылки на тип перечисления, определяемый где-то в другом месте. Имя тега перечисления должно относиться к уже определенному тегу перечисления в пределах текущей области видимости. Так как тег перечисления объявлен где-то в другом месте, список перечисления не представлен в объявлении.
Пример: enum week rab1;
В объявлении указателя на тип данных перечисления и объявляемых typedef для типов перечисления можно использовать имя тега перечисления до того, как данный тег перечисления определен. Однако определение перечисления должно предшествовать любому действию используемого указателя на тип объявления typedef. Объявление без последующего списка описателей описывает тег, или, если так можно сказать, шаблон перечисления.
1.6 Структуры и объединения
Структуры
Cтруктуры - это составной объект, в который входят элементы любых типов, за исключением функций. В отличие от массива, который является однородным объектом, структура может быть неоднородной. Тип структуры определяется записью вида:
struct { список определений }
В структуре обязательно должен быть указан хотя бы один компонент.
Определение структур имеет следующий вид:
тип-данных описатель;
где тип-данных указывает тип структуры для объектов, определяемых в описателях. В простейшей форме описатели представляют собой идентификаторы или массивы.
Пример:
struct { double x, y; } s1, s2, sm[9];
struct { int year; char moth, day; } date1, date2;
Переменные s1, s2 определяются как структуры, каждая из которых состоит из двух компонент х и у. Переменная sm определяется как массив из девяти структур. Каждая из двух переменных date1, date2 состоит из трех компонентов year, moth, day.
Существует и другой способ ассоциирования имени с типом структуры, он основан на использовании тега структуры. Тег структуры аналогичен тегу перечислимого типа. Тег структуры определяется следующим образом:
struct тег { список описаний; };
где тег является идентификатором.
В приведенном ниже примере идентификатор student описывается как тег структуры:
struct student { char name[25];
int id, age;
char grp;
};
Тег структуры используется для последующего объявления структур данного вида в форме:
struct тег список-идентификаторов;
Пример:
struct student st1,st2;
Доступ к компонентам структуры осуществляется с помощью указания имени структуры и следующего через точку имени выделенного компонента, например:
st1.name = 'Иванов';
st2.id = st1.id;
Объединения (смеси)
Объединение подобно структуре, однако в каждый момент времени может использоваться (или другими словами быть ответным) только один из элементов объединения. Тип объединения может задаваться в следующем виде:
union { описание элемента 1;
...
описание элемента n;
};
Главной особенностью объединения является то, что для каждого из объявленных элементов выделяется одна и та же область памяти, т.е. они перекрываются. Хотя доступ к этой области памяти возможен с использованием любого из элементов, элемент для этой цели должен выбираться так, чтобы полученный результат не был бессмысленным. Доступ к элементам объединения осуществляется тем же способом, что и к структурам. Тег объединения может быть формализован точно так же, как и тег структуры.
Объединение применяется для следующих целей:
- инициализации используемого объекта памяти, если в каждый момент времени только один объект из многих является активным;
- интерпретации основного представления объекта одного типа, как если бы этому объекту был присвоен другой тип.
Память, которая соответствует переменной типа объединения, определяется величиной, необходимой для размещения наиболее длинного элемента объединения. Когда используется элемент меньшей длины, то переменная типа объединения может содержать неиспользуемую память. Все элементы объединения хранятся в одной и той же области памяти, начиная с одного адреса.
Примеры:
union {
char fio[30];
char adres[80];
int vozrast;
int telefon; } inform;
При использовании объекта inform типа union можно обрабатывать только тот элемент, который получил значение, т.е. после присвоения значения элементу inform.fio, не имеет смысла обращаться к другим элементам.
Поля битов
Элементом структуры может быть битовое поле, обеспечивающее доступ к отдельным битам памяти. Вне структур битовые поля объявлять нельзя. Нельзя также организовывать массивы битовых полей и нельзя применять к полям операцию определения адреса. В общем случае тип структуры с битовым полем задается в следующем виде:
struct { unsigned идентификатор 1 : длина-поля 1;
unsigned идентификатор 2 : длина-поля 2;
}
длина поля задается целым выражением или константой. Эта константа определяет число битов, отведенное соответствующему полю. Поле нулевой длины обозначает выравнивание на границу следующего слова.
Пример:
struct {
unsigned a1: 1;
unsigned a2: 2;
unsigned a3: 5;
unsigned a4: 2;
} prim;
Структуры битовых полей могут содержать и знаковые компоненты. Такие компоненты автоматически размещаются на соответствующих границах слов, при этом некоторые биты слов могут оставаться неиспользованными.
Ссылки на поле битов выполняются точно так же, как и компоненты общих структур. Само же битовое поле рассматривается как целое число, максимальное значение которого определяется длиной поля.
Переменные с изменяемой структурой
Очень часто некоторые объекты программы относятся к одному и тому же классу, отличаясь лишь некоторыми деталями. Рассмотрим, например, представление геометрических фигур. Общая информация о фигурах может включать такие элементы, как площадь, периметр. Однако соответствующая информация о геометрических размерах может оказаться различной в зависимости от их формы.
Рассмотрим пример, в котором информация о геометрических фигурах представляется на основе комбинированного использования структуры и объединения.
struct figure {
double area, perimetr; /* общие компоненты */
int type; /* признак компонента */
union /* перечисление компонент */
{ double radius; /* окружность */
double a[2]; /* прямоугольник */
double b[3]; /* треугольник */
} geom_fig;
} fig1, fig2;
В общем случае каждый объект типа figure будет состоять из трех компонентов: area, perimetr, type. Компонент type называется меткой активного компонента, так как он используется для указания, какой из компонентов объединения geom_fig является активным в данный момент. Такая структура называется переменной структурой, потому что ее компоненты меняются в зависимости от значения метки активного компонента (значение type).
Отметим, что вместо компоненты type типа int, целесообразно было бы использовать перечисляемый тип. Например, такой
enum figure_chess { CIRCLE,
BOX,
TRIANGLE
} ;
Константы CIRCLE, BOX, TRIANGLE получат значения соответственно равные 0, 1, 2. Переменная type может быть объявлена как имеющая перечислимый тип:
enum figure_chess type;
В этом случае компилятор СИ предупредит программиста о потенциально ошибочных присвоениях, таких, например, как
figure.type = 40;
В общем случае переменная структуры будет состоять из трех частей: набор общих компонент, метки активного компонента и части с меняющимися компонентами. Общая форма переменной структуры, имеет следующий вид:
struct { общие компоненты;
метка активного компонента;
union { описание компоненты 1;
описание компоненты 2;
:::
описание компоненты n;
} идентификатор-объединения;
} идентификатор-структуры;
Пример определения переменной структуры с именем helth_record
struct { // общая информация
char name [25]; // имя
int age; // возраст
char sex; // пол
// метка активного компонента (семейное положение)
enum merital_status ins;
// переменная часть
union { // холост
; // нет компонент
struct { // состоит в браке
char marrige_date[8];
char spouse_name[25];
int no_children;
} marriage_info;
// разведен
char date_divorced[8];
} marital_info;
} health_record;
enum marital_status { SINGLE, // холост
MARRIGO, // женат
DIVOREED // разведен
} ;
Обращаться к компонентам структуры можно при помощи ссылок:
helth_record.name,
helth_record.ins,
helth_record.marriage_info.marriage_date .
Рекурсивные структуры
Использование тегов структуры необходимо для описания рекурсивных структур, содержащих ссылки сами на себя и, как правило, используемых для образования связанных списков при работе с динамической памятью.
Пример:
struct node { int data;
struct node * next; } st1_node;
Тег структуры node действительно является рекурсивным, так как он используется в своем собственном описании, т.е. в формализации указателя next. Структуры не могут быть прямо рекурсивными, т.е. структура node не может содержать компоненту, являющуюся структурой node, но любая структура может иметь компоненту, являющуюся указателем на свой тип, как и сделано в приведенном примере.
Инициализация составных объектов
При инициализации составных объектов, нужно внимательно следить за использованием скобок и списков инициализаторов.
Примеры:
struct complex { double real;
double imag; }
comp [2][3] = { { {1,1}, {2,3}, {4,5} }, { {6,7}, {8,9}, {10,11} } };
В данном примере инициализируется массив структур comp из двух строк и трех столбцов, где каждая структура состоит из двух элементов real и imag.
struct complex comp2 [2][3] ={ {1,1},{2,3},{4,5}, {6,7},{8,9},{10,11} };
В этом примере компилятор интерпретирует рассматриваемые фигурные скобки следующим образом:
- первая левая фигурная скобка - начало составного инициатора для массива comp2;
- вторая левая фигурная скобка - начало инициализации первой строки массива comp2[0]. Значения 1,1 присваиваются двум элементам первой структуры;
- первая правая скобка (после 1) указывает компилятору, что список инициаторов для строки массива окончен, и элементы оставшихся структур в строке comp[0] автоматически инициализируются нулем;
- аналогично список {2,3} инициализирует первую структуру в строке comp[1], а оставшиеся структуры массива обращаются в нули;
- на следующий список инициализаторов {4,5} компилятор будет сообщать о возможной ошибке, так как строка 3 в массиве comp2 отсутствует.
При инициализации объединения задается значение первого элемента объединения в соответствии с его типом.
Пример:
union tab { unsigned char name[10];
int tab1;
} pers = {'A','H','T','O','H'};
Инициализируется переменная pers.name, и так как это массив, для его инициализации требуется список значений в фигурных скобках. Первые пять элементов массива инициализируются значениями из списка, остальные нулями.
Инициализацию массива символов можно выполнить путем использования строкового литерала.
char stroka[ ] = 'Привет';
Инициализируется массив символов из 7 элементов, последним элементом (седьмым) будет символ ' ', которым завершаются все строковые литералы.
В том случае, если задается размер массива, а строковый литерал длиннее, чем размер массива, то лишние символы отбрасываются.
Следующее объявление инициализирует переменную stroka как массив, состоящий из семи элементов.
char stroka[5] = 'Привет';
В переменную stroka попадают первые пять элементов литерала, а символы 'Т' и ' ' отбрасываются. Если строка короче, чем размер массива, то оставшиеся элементы массива заполняются нулями. Отметим, что инициализация переменной типа tab может иметь следующий вид:
union tab pers1 = 'Антон';
и, таким образом, в символьный массив попадут символы:
'А','Н','Т','О','Н',' ',
а остальные элементы будут инициализированы нулем.
1.7 Ссылочные переменные и указатели, массивы и указатели на массивы, указатели на структуры
Ссылочные переменные
C++ позволяет использовать ссылочные переменные. Применяя ссылки, можно обращаться к переменным, используя их псевдонимы.
Ссылочные переменные - это псевдонимы переменных, к которым они обращаются.
Объявление ссылочных переменных имеет следующий синтаксис:
тип& ссылПер (переменная);
тип& ссылПер = переменная;
СсылПер - это ссылочная переменная, которая инициализируется после того, как объявлена. Перед использованием ссылочной переменной необходимо удостовериться в том, что она инициализирована или ей присвоено значение.
Пример:
int x = 10, y = 3;
int& rx(x);
int& ry = y; // взять ссылку
Указатели
Указатель - это адрес памяти, распределяемой для размещения идентификатора (в качестве идентификатора может выступать имя переменной, массива, структуры, строкового литерала). В том случае, если переменная объявлена как указатель, то она содержит адрес памяти, по которому может находиться скалярная величина любого типа. При объявлении переменной типа указатель, необходимо определить тип объекта данных, адрес которых будет содержать переменная, и имя указателя с предшествующей звездочкой (или группой звездочек). Формат объявления указателя:
спецификатор-типа [ модификатор ] *описатель
Спецификатор-типа задает тип объекта и может быть любого основного типа, типа структуры, смеси. Задавая вместо спецификатора-типа ключевое слово void, можно своеобразным образом отсрочить спецификацию типа, на который ссылается указатель. Переменная, объявляемая как указатель на тип void, может быть использована для ссылки на объект любого типа. Однако для того, чтобы можно было выполнить арифметические и логические операции над указателями или над объектами, на которые они указывают, необходимо при выполнении каждой операции явно определить тип объектов. Такие определения типов может быть выполнено с помощью операции приведения типов.
В качестве модификаторов при объявлении указателя могут выступать ключевые слова const, near, far, huge. Ключевое слово const указывает, что указатель не может быть изменен в программе. Размер переменной объявленной как указатель, зависит от архитектуры компьютера и от используемой модели памяти, для которой будет компилироваться программа. Указатели на различные типы данных не обязательно должны иметь одинаковую длину.
Для модификации размера указателя можно использовать ключевые слова near, far, huge.
Примеры:
unsigned int * a; // переменная а представляет собой указатель
// на тип unsigned int (целые числа без знака)
double * x; // переменная х указывает на тип данных с
// плавающей точкой удвоенной точности */
char * fuffer ; // объявляется указатель с именем fuffer,
// который указывает на переменную типа char
double nomer;
void *addres;
addres = & nomer;
(double *)addres ++;
Переменная addres объявлена как указатель на объект любого типа. Поэтому ей можно присвоить адрес любого объекта (& - операция вычисления адреса). Однако, как было отмечено выше, ни одна арифмитическая операция не может быть выполнена над указателем, пока не будет явно определен тип данных, на которые он указывает. Это можно сделать, используя операцию приведения типа (double *) для преобразования addres к указателю на тип double, а затем увеличение адреса.
Пример:
const * dr;
Переменная dr объявлена как указатель на константное выражение, т.е. значение указателя может изменяться в процессе выполнения программы, а величина, на которую он указывает, нет.
Пример:
unsigned char * const w = &obj;
Переменная w объявлена как константный указатель на данные типа char unsigned. Это означает, что на протяжении всей программы w будет указывать на одну и ту же область памяти. Содержание же этой области может быть изменено.
Преобразование типов указателя
Указатель на величину одного типа может быть преобразован к указателю на величину другого типа. Однако результат может быть не определен из-за отличий в требованиях к выравниванию и размерах для различных типов.
Указатель на тип void может быть преобразован к указателю на любой тип, и указатель на любой тип может быть преобразован к указателю на тип void без ограничений. Значение указателя может быть преобразовано к целой величине. Метод преобразования зависит от размера указателя и размера целого типа следующим образом:
- если размер указателя меньше размера целого типа или равен ему, то указатель преобразуется точно так же, как целое без знака;
- если указатель больше, чем размер целого типа, то указатель сначала преобразуется к указателю с тем же размером, что и целый тип, и затем преобразуется к целому типу.
Целый тип может быть преобразован к адресному типу по следующим правилам:
- если целый тип того же размера, что и указатель, то целая величина просто рассматривается как указатель (целое без знака);
- если размер целого типа отличен от размера указателя, то целый тип сначала преобразуется к размеру указателя (используются способы преобразования, описанные выше), а затем полученное значение трактуется как указатель.
Массивы и указатели на массивы
С++ и С интерпретируют имя массива как адрес его первого элемента. Таким образом, если x - массив, то выражения &x[0] и x эквивалентны. В случае матрицы - назовем ее mat - выражения &mat[0][0] и mat также эквивалентны.
С++ позволяет использовать указатели для обращения к разным элементам массива. Когда Вы вызываете элемент x[i] массива x , то выполняются две задачи: получит базовый адрес массива, т.е. узнать, где находится первый элемент массива и использовать i для вычисления смещения базового адреса массива. Фактически, если есть указатель ptr на базовый адрес массива, т.е.
prt = x;
то можно записать:
адрес элемента x[i] = адрес x + i * sizeof(базовый тип)
или
адрес элемента x[i] = ptr + i * sizeof(базовый тип)
В С++ это записывается более коротким образом:
адрес элемента x[i] = ptr + i
При последовательном обращении к элементам массива можно использова метод инкремента/декремента указателя, т.е. операцию ++ и --, например:
ptr++;
a = * ptr++;
a = * (ptr--);
Указатели на структуры
С++ поддерживает объявление и использование указателей на структуры. Для присвоения адреса структурной переменной указателю того же типа используется тот же синтаксис, что и в случае простых переменных. После того, как указатель получит адрес структурной переменной, для обращения к элементам структуры нужно использовать операцию ->.
Общий синтаксис для доступа к элементам структуры с помощью указателя имеет вид
structPtr -> aMember
где structPtr -указатель на структуру, aMember - элемент структуры.
Пример:
struct point
{
double x;
double y;
};
point p;
point* ptr = & p;
ptr->x = 23.3;
ptr->y = ptr->x + 12.3;
1.8 Указатели и динамическая память, дальние указатели
Обработка особых ситуаций
Обработка особых ситуаций (исключений) была введена в стандарт ANSI/ISO C как способ обойти программным путем неожиданно возникающие проблемы, а так же, как средство, обеспечивающее универсальный метод обработки ошибок.
Операторы, которые могут генерировать ошибку во время исполнения, размещаются в специальном пробном блоке (try-блоке). За этим блоком следует один или более блоков захвата (catch -блоков), которые идентифицируют и обрабатывают ошибки, генерируемые в пробном блоке.
Пробный блок и блоки захвата имеют следующий синтаксис:
try
{
// Здесь размещается код, который может генерировать исключения
}
catch (T1 [X1])
{
// обрабатывает тип исключений Т1
}
[ catch (T2 [X2])
{
// обрабатывает тип исключений Т2
}]
[ другие блоки захвата ]
[catch (…)
{
// обрабатывает оставшиеся типы исключений
}]
Типы Т1 и Т2 - специальные типы, которые поддерживают исключения, определяемые пользователем. Параметр Х1 может иметь тип Т1, T1& , const T1 и const T1&. Параметр Х2 может иметь тип Т2, T2& , const T2 и const T2&. Последний блок захвата использует многоточие, чтобы показать, что это блок, захватывающий все.
Операции new и delete
Существует много приложений, в которых необходимо создавать новые переменные и динамически распределять для них память во время выполнения программы. В языке С для этого имеются функции динамической памяти, такие как malloc, calloc, free. В С++ введены новые операции - new и delete, которые лучше контролируют тип создаваемых динамических данных. К тому же эти операции работают с конструкторами и деструкторами (см. раздел объектно-ориентированное программирование).
Операции new и delete имеют следующий синтаксис:
указатель = new тип;
delete указатель;
Операция new возвращает адрес динамически распределенной переменной. Операция delete освобождает динамически распределенную память, на которую ссылается указатель.
Если динамическое распределение памяти с помощью операции new потерпело неудачу, оно выбрасывает исключение типа xalloc, объявленное в заголовочном файле except.h, поэтому часто динамическое распределение памяти осуществляется внутри блока try.
Пример:
try
{
int *pint;
pint = new int;
*pint = 33;
cout << “Указателю выделена память и в ней хранятся данные” <<
<< *pint << endl;
delete pint;
}
catch (xalloc&)
{
cout << “Ошибка при выделении памяти” << endl;}
Дальние указатели
Использование сегментирования памяти для процессоров, таких как Intel 80*86x, приводит к использованию двух типов указателей - ближних указателей и дальних указателей. Внутри сегмента можно использовать только ближние указатели, которые хранят только адрес смещения. Дальние указатели хранят также и адрес сегмента (для их объявления используется ключевое слово _far между типом и именем указателя), употребляются только для IBM PC, только в 16-битном режиме. Для Windows 95 и других 32-битных режимов ключевые слова _far запрещены. Использование ближних и дальних указателей различно также при использовании различных моделей памяти.
1.9 Функции, переменные, макроподстановки
1.9.1 Определения, прототипы функций и вызов функций
Мощность языка СИ во многом определяется легкостью и гибкостью в определении и использовании функций в СИ-программах. В отличие от других языков программирования высокого уровня в языке СИ нет деления на процедуры, подпрограммы и функции, здесь вся программа строится только из функций.
Функция - это совокупность объявлений и операторов, обычно предназначенная для решения определенной задачи. Каждая функция должна иметь имя, которое используется для ее объявления, определения и вызова. В любой программе на СИ должна быть функция с именем main (главная функция), именно с этой функции, в каком бы месте программы она не находилась, начинается выполнение программы.
При вызове функции ей при помощи аргументов (формальных параметров) могут быть переданы некоторые значения (фактические параметры), используемые во время выполнения функции. Функция может возвращать некоторое (одно !) значение. Это возвращаемое значение и есть результат выполнения функции, который при выполнении программы подставляется в точку вызова функции, где бы этот вызов ни встретился. Допускается также использовать функции, не имеющие аргументов и функции, не возвращающие никаких значений. Действие таких функций может состоять, например, в изменении значений некоторых переменных, выводе на печать некоторых текстов и т.п.
С использованием функций в языке СИ связаны три понятия:
определение функции (описание действий, выполняемых функцией);
объявление функции (задание формы обращения к функции);
вызов функции.
Определение функции задает тип возвращаемого значения, имя функции, типы и число формальных параметров, а также объявления переменных и операторы, называемые телом функции, и определяющие действие функции. В определении функции также может быть задан класс памяти.
Пример:
int rus (unsigned char r)
{ if (r>='А' && c<='Z')
return 1;
else
return 0;
}
В данном примере определена функция с именем rus, имеющая один параметр с именем r и типом unsigned char. Функция возвращает целое значение, равное 1, если параметр функции является заглавной буквой алфавита, или 0 в противном случае.
В языке СИ нет требования, чтобы определение функции обязательно предшествовало ее вызову. Определения используемых функций могут следовать за определением функции main, перед ним, или находится в другом файле.
Однако для того, чтобы компилятор мог осуществить проверку соответствия типов передаваемых фактических параметров типам формальных параметров до вызова функции нужно поместить объявление (прототип) функции.
Объявление функции имеет такой же вид, что и определение функции, с той лишь разницей, что тело функции отсутствует, и имена формальных параметров тоже могут быть опущены. Для функции, определенной в последнем примере, прототип может иметь вид
int rus (unsigned char r); или rus (unsigned char);
В программах на языке СИ широко используются, так называемые, библиотечные функции, т.е. функции предварительно разработанные и записанные в библиотеки. Прототипы библиотечных функций находятся в специальных заголовочных файлах, поставляемых вместе с библиотеками в составе систем программирования, и включаются в программу с помощью директивы #include.
Если объявление функции не задано, то по умолчанию строится прототип функции на основе анализа первой ссылки на функцию, будь то вызов функции или определение. Однако такой прототип не всегда согласуется с последующим определением или вызовом функции. Рекомендуется всегда задавать прототип функции. Это позволит компилятору либо выдавать диагностические сообщения, при неправильном использовании функции, либо корректным образом регулировать несоответствие аргументов, устанавливаемое при выполнении программы.
Объявление параметров функции при ее определении может быть выполнено в так называемом 'старом стиле', при котором в скобках после имени функции следуют только имена параметров, а после скобок объявления типов параметров. Например, функция rus из предыдущего примера может быть определена следующим образом:
int rus (r)
unsigned char r;
{ ... /* тело функции */ ... }
В соответствии с синтаксисом языка СИ определение функции имеет следующую форму:
[спецификатор-класса-памяти] [спецификатор-типа] имя-функции
([список-формальных-параметров])
{ тело-функции }
Необязательный спецификатор-класса-памяти задает класс памяти функции, который может быть static или extern. Подробно классы памяти будут рассмотрены далее.
Спецификатор-типа функции задает тип возвращаемого значения и может задавать любой тип. Если спецификатор-типа не задан, то предполагается, что функция возвращает значение типа int.
Функция не может возвращать массив или функцию, но может возвращать указатель на любой тип, в том числе и на массив и на функцию. Тип возвращаемого значения, задаваемый в определении функции, должен соответствовать типу в объявлении этой функции.
Функция возвращает значение, если ее выполнение заканчивается оператором return, содержащим некоторое выражение. Указанное выражение вычисляется, преобразуется, если необходимо, к типу возвращаемого значения и возвращается в точку вызова функции в качестве результата. Если оператор return не содержит выражения или выполнение функции завершается после выполнения последнего ее оператора (без выполнения оператора return), то возвращаемое значение не определено. Для функций, не использующих возвращаемое значение, должен быть использован тип void, указывающий на отсутствие возвращаемого значения. Если функция определена как функция, возвращающая некоторое значение, а в операторе return при выходе из нее отсутствует выражение, то поведение вызывающей функции после передачи ей управления может быть непредсказуемым.
Список-формальных-параметров - это последовательность объявлений формальных параметров, разделенная запятыми. Формальные параметры - это переменные, используемые внутри тела функции и получающие значение при вызове функции путем копирования в область памяти значений соответствующих фактических параметров. Список-формальных-параметров может заканчиваться запятой (,) или запятой с многоточием (,...), это означает, что число аргументов функции переменно. Однако предполагается, что функция имеет, по крайней мере, столько обязательных аргументов, сколько формальных параметров задано перед последней запятой в списке параметров. Такой функции может быть передано большее число аргументов, но над дополнительными аргументами не проводится контроль типов.
Если функция не использует параметров, то наличие круглых скобок обязательно, а вместо списка параметров рекомендуется указать слово void.
Порядок и типы формальных параметров должны быть одинаковыми в определении функции и во всех ее объявлениях. Типы фактических параметров при вызове функции должны быть совместимы с типами соответствующих формальных параметров. Тип формального параметра может быть любым основным типом, структурой, объединением, перечислением, указателем или массивом. Если тип формального параметра не указан, то этому параметру присваивается тип int.
Для формального параметра можно задавать класс памяти register, при этом, для величин типа int спецификатор типа можно опустить.
Идентификаторы формальных параметров используются в теле функции в качестве ссылок на переданные значения. Эти идентификаторы не могут быть переопределены в блоке, образующем тело функции, но могут быть переопределены во внутреннем блоке внутри тела функции.
При передаче параметров в функцию, если необходимо, выполняются обычные арифметические преобразования для каждого формального параметра и каждого фактического параметра независимо. После преобразования формальный параметр не может быть короче, чем int, т.е. объявление формального параметра с типом char равносильно его объявлению с типом int. Параметры, представляющие собой действительные числа, имеют тип double.
Преобразованный тип каждого формального параметра определяет, как интерпретируются аргументы, помещаемые при вызове функции в стек. Несоответствие типов фактических аргументов и формальных параметров может быть причиной неверной интерпретации.
Тело функции - это составной оператор, содержащий операторы, определяющие действие функции.
Все переменные, объявленные в теле функции без указания класса памяти, имеют класс памяти auto, т.е. они являются локальными. При вызове функции локальным переменным отводится память в стеке и производится их инициализация. Управление передается первому оператору тела функции и начинается выполнение функции, которое продолжается до тех пор, пока не встретится оператор return или последний оператор тела функции. Управление при этом возвращается в точку, следующую за точкой вызова, а локальные переменные становятся недоступными. При новом вызове функции для локальных переменных память распределяется вновь, и поэтому старые значения локальных переменных теряются.
Параметры функции передаются по значению и могут рассматриваться как локальные переменные, для которых выделяется память при вызове функции и производится инициализация значениями фактических параметров. При выходе из функции значения этих переменных теряются. Поскольку передача параметров происходит по значению, в теле функции нельзя изменить значения переменных в вызывающей функции, являющихся фактическими параметрами. Однако, если в качестве параметра передать указатель на некоторую переменную, то используя операцию разадресации можно изменить значение этой переменной.
Пример:
/* Неправильное использование параметров */
void change (int x, int y)
{ int k=x;
x=y;
y=k;
}
В данной функции значения переменных x и y, являющихся формальными параметрами, меняются местами, но поскольку эти переменные существуют только внутри функции change, значения фактических параметров, используемых при вызове функции, останутся неизменными. Для того чтобы менялись местами значения фактических аргументов можно использовать функцию, приведенную в следующем примере.
Пример:
/* Правильное использование параметров */
void change (int *x, int *y)
{ int k=*x;
*x=*y;
*y=k;
}
При вызове такой функции в качестве фактических параметров должны быть использованы не значения переменных, а их адреса
change (&a,&b);
Если требуется вызвать функцию до ее определения в рассматриваемом файле, или определение функции находится в другом исходном файле, то вызов функции следует предварять объявлением этой функции.
Объявление (прототип) функции имеет следующий формат:
[спецификатор-класса-памяти] [спецификатор-типа] имя-функции ([список-формальных-параметров]) [,список-имен-функций];
В отличие от определения функции, в прототипе за заголовком сразу же следует точка с запятой, а тело функции отсутствует. Если несколько разных функций возвращают значения одинакового типа и имеют одинаковые списки формальных параметров, то эти функции можно объявить в одном прототипе, указав имя одной из функций в качестве имени-функции, а все другие поместить в список-имен-функций, причем каждая функция должна сопровождаться списком формальных параметров. Правила использования остальных элементов формата такие же, как при определении функции. Имена формальных параметров при объявлении функции можно не указывать, а если они указаны, то их область действия распространяется только до конца объявления.
Прототип - это явное объявление функции, которое предшествует определению функции. Тип возвращаемого значения при объявлении функции должен соответствовать типу возвращаемого значения в определении функции.
Если прототип функции не задан, а встретился вызов функции, то строится неявный прототип из анализа формы вызова функции. Тип возвращаемого значения создаваемого прототипа int, а список типов и числа параметров функции формируется на основании типов и числа фактических параметров, используемых при данном вызове.
Таким образом, прототип функции необходимо задавать в следующих случаях:
1. Функция возвращает значение типа, отличного от int.
2. Требуется проинициализировать некоторый указатель на функцию до того, как эта функция будет определена.
Наличие в прототипе полного списка типов аргументов параметров позволяет выполнить проверку соответствия типов фактических параметров при вызове функции типам формальных параметров, и, если необходимо, выполнить соответствующие преобразования.
В прототипе можно указать, что число параметров функции переменно, или что функция не имеет параметров.
Если прототип задан с классом памяти static, то и определение функции должно иметь класс памяти static. Если спецификатор класса памяти не указан, то подразумевается класс памяти extern.
Вызов функции имеет следующий формат:
адресное-выражение ([список-выражений])
Поскольку синтаксически имя функции является адресом начала тела функции, в качестве обращения к функции может быть использовано адресное-выражение (в том числе и имя функции или разадресация указателя на функцию), имеющее значение адреса функции.
Список-выражений представляет собой список фактических параметров, передаваемых в функцию. Этот список может быть и пустым, но наличие круглых скобок обязательно.
Фактический параметр может быть величиной любого основного типа, структурой, объединением, перечислением или указателем на объект любого типа. Массив и функция не могут быть использованы в качестве фактических параметров, но можно использовать указатели на эти объекты.
Выполнение вызова функции происходит следующим образом:
1. Вычисляются выражения в списке выражений и подвергаются обычным арифметическим преобразованиям. Затем, если известен прототип функции, тип полученного фактического аргумента сравнивается с типом соответствующего формального параметра. Если они не совпадают, то либо производится преобразование типов, либо формируется сообщение об ошибке. Число выражений в списке выражений должно совпадать с числом формальных параметров, если только функция не имеет переменного числа параметров. В последнем случае, проверке подлежат только обязательные параметры. Если в прототипе функции указано, что ей не требуются параметры, а при вызове они указаны, формируется сообщение об ошибке.
2. Происходит присваивание значений фактических параметров соответствующим формальным параметрам.
3. Управление передается на первый оператор функции.
4. Выполнение оператора return в теле функции возвращает управление и возможно, значение в вызывающую функцию. При отсутствии оператора return управление возвращается после выполнения последнего оператора тела функции, а возвращаемое значение не определено.
Адресное выражение, стоящее перед скобками определяет адрес вызываемой функции. Это значит, что функция может быть вызвана через указатель на функцию.
Пример:
int (*fun)(int x, int *y);
Здесь объявлена переменная fun как указатель на функцию с двумя параметрами: типа int и указателем на int. Сама функция должна возвращать значение типа int. Круглые скобки, содержащие имя указателя fun и признак указателя *, обязательны, иначе запись
int *fun (intx,int *y);
будет интерпретироваться как объявление функции fun возвращающей указатель на int.
Вызов функции возможен только после инициализации значения указателя fun и имеет вид:
(*fun)(i,&j);
В этом выражении для получения адреса функции, на которую ссылается указатель fun, используется операция разадресации *.
Указатель на функцию может быть передан в качестве параметра функции. При этом разадресация происходит во время вызова функции, на которую ссылается указатель на функцию. Присвоить значение указателю на функцию можно в операторе присваивания, употребив имя функции без списка параметров.
Пример:
double (*fun1)(int x, int y);
double fun2(int k, int l);
fun1 = fun2; /* инициализация указателя на функцию */
(*fun1)(2,7); /* обращение к функции */
В рассмотренном примере указатель на функцию fun1 описан как указатель на функцию с двумя параметрами, возвращающую значение типа double, и также описана функция fun2. В противном случае, т.е. когда указателю на функцию присваивается функция, описанная иначе, чем указатель, произойдет ошибка.
Рассмотрим пример использования указателя на функцию в качестве параметра функции вычисляющей производную от функции cos(x).
Пример:
double proiz(double x, double dx, double (*f)(double x) );
double fun(double z);
int main()
{
double x; /* точка вычисления производной */
double dx; /* приращение */
double z; /* значение производной */
scanf('%f,%f',&x,&dx); /* ввод значений x и dx */
z=proiz(x,dx,fun); /* вызов функции */
printf('%f',z); /* печать значения производной */
return 0;
}
double proiz(double x,double dx, double (*f)(double z) )
{ /* функция, вычисляющая производную */
double xk, xk1, pr;
xk = fun(x);
xk1 = fun(x+dx);
pr = (xk1/xk-1)*xk/dx;
return pr;
}
double fun( double z)
{ /* функция, от которой вычисляется производная */
return (cos(z));
}
Для вычисления производной от какой-либо другой функции можно изменить тело функции fun или использовать при вызове функции proiz имя другой функции. В частности, для вычисления производной от функции cos(x) можно вызвать функцию proiz в форме
z=proiz(x,dx,cos);
а для вычисления производной от функции sin(x) в форме
z=proiz(x,dx,sin);
Любая функция в программе на языке СИ может быть вызвана рекурсивно, т.е. она может вызывать саму себя. Компилятор допускает любое число рекурсивных вызовов. При каждом вызове для формальных параметров и переменных с классом памяти auto и register выделяется новая область памяти, так что их значения из предыдущих вызовов не теряются, но в каждый момент времени доступны только значения текущего вызова.
Переменные, объявленные с классом памяти static, не требуют выделения новой области памяти при каждом рекурсивном вызове функции и их значения доступны в течение всего времени выполнения программы.
Классический пример рекурсии - это математическое определение факториала n!
n! = 1 при n=0;
n*(n-1)! при n>1 .
Функция, вычисляющая факториал, будет иметь следующий вид:
long fakt(int n)
{
return ( (n==1) ? 1 : n*fakt(n-1) );
}
Хотя компилятор языка СИ не ограничивает число рекурсивных вызовов функций, это число ограничивается ресурсом памяти компьютера и при слишком большом числе рекурсивных вызовов может произойти переполнение стека.
Вызов функции с переменным числом параметров
При вызове функции с переменным числом параметров в вызове этой функции задается любое требуемое число аргументов. В объявлении и определении такой функции переменное число аргументов задается многоточием в конце списка формальных параметров или списка типов аргументов.
Все аргументы, заданные в вызове функции, размещаются в стеке. Количество формальных параметров, объявленных для функции, определяется числом аргументов, которые берутся из стека и присваиваются формальным параметрам. Программист отвечает за правильность выбора дополнительных аргументов из стека и определение числа аргументов, находящихся в стеке.
Примерами функций с переменным числом параметров являются функции из библиотеки функций языка СИ, осуществляющие операции ввода-вывода информации (printf,scanf и т.п.). Подробно эти функции рассмотрены далее.
Программист может разрабатывать свои функции с переменным числом параметров. Для обеспечения удобного способа доступа к аргументам функции с переменным числом параметров имеются три макроопределения (макросы) va_start, va_arg, va_end, находящиеся в заголовочном файле stdarg.h. Эти макросы указывают на то, что функция, разработанная пользователем, имеет некоторое число обязательных аргументов, за которыми следует переменное число необязательных аргументов. Обязательные аргументы доступны через свои имена как при вызове обычной функции. Для извлечения необязательных аргументов используются макросы va_start, va_arg, va_end в следующем порядке:
Макрос va_start предназначен для установки аргумента arg_ptr на начало списка необязательных параметров и имеет вид функции с двумя параметрами:
void va_start(arg_ptr,prav_param);
Параметр prav_param должен быть последним обязательным параметром вызываемой функции, а указатель arg_prt должен быть объявлен с предопределением в списке переменных типа va_list в виде:
va_list arg_ptr;
Макрос va_start должен быть использован до первого использования макроса va_arg.
Макрокоманда va_arg обеспечивает доступ к текущему параметру вызываемой функции и тоже имеет вид функции с двумя параметрами
type_arg va_arg(arg_ptr,type);
Эта макрокоманда извлекает значение типа type по адресу, заданному указателем arg_ptr, увеличивает значение указателя arg_ptr на длину использованного параметра (длина type) и таким образом параметр arg_ptr будет указывать на следующий параметр вызываемой функции. Макрокоманда va_arg используется столько раз, сколько необходимо для извлечения всех параметров вызываемой функции.
Макрос va_end используется по окончании обработки всех параметров функции и устанавливает указатель списка необязательных параметров на ноль (NULL).
Рассмотрим применение этих макросов для обработки параметров функции, вычисляющей среднее значение произвольной последовательности целых чисел. Поскольку функция имеет переменное число параметров, будем считать концом списка значение равное -1. Поскольку в списке должен быть хотя бы один элемент, у функции будет один обязательный параметр.
Пример:
#include
int main()
{ int n;
int sred_znach(int,...);
n=sred_znach(2,3,4,-1);
/* вызов с четырьмя параметрами */
printf('n=%d',n);
n=sred_znach(5,6,7,8,9,-1);
/* вызов с шестью параметрами */
printf('n=%d',n);
return (0);
}
int sred_znach(int x,...);
{
int i=0, j=0, sum=0;
va_list uk_arg;
va_start(uk_arg,x); /* установка указателя uk_arg на */
/* первый необязятельный параметр */
if (x!=-1) sum=x; /* проверка на пустоту списка */
else return (0);
j++;
while ( (i=va_arg(uk_arg,int))!=-1)
/* выборка очередного */
{ /* параметра и проверка */
sum+=i; /* на конец списка */
j++;
}
va_end(uk_arg); /* закрытие списка параметров */
return (sum/j);
}
Передача параметров функции main
Функция main, с которой начинается выполнение СИ-программы, может быть определена с параметрами, которые передаются из внешнего окружения, например, из командной строки. Во внешнем окружении действуют свои правила представления данных, а точнее, все данные представляются в виде строк символов. Для передачи этих строк в функцию main используются два параметра, первый параметр служит для передачи числа передаваемых строк, второй для передачи самих строк. Общепринятые (но не обязательные) имена этих параметров argc и argv. Параметр argc имеет тип int, его значение формируется из анализа командной строки и равно количеству слов в командной строке, включая и имя вызываемой программы (под словом понимается любой текст, не содержащий символа пробел). Параметр argv это массив указателей на строки, каждая из которых содержит одно слово из командной строки. Если слово должно содержать символ пробел, то при записи его в командную строку оно должно быть заключено в кавычки.
Функция main может иметь и третий параметр, который принято называть argp. Он служит для передачи в функцию main параметров операционной системы (среды), в которой выполняется СИ-программа.
Заголовок функции main имеет вид:
int main (int argc, char *argv[], char *argp[])
Если, например, командная строка СИ-программы имеет вид:
A:>cprog working 'C program' 1
то аргументы argc, argv, argp представляются в памяти как показано на рис. 1.
argc [ 4 ]
argv [ ]--> [A:cprog.exe ]
[ ]--> [working ]
[ ]--> [C program ]
[ ]--> [1 ]
[NULL]
argp [ ]--> [path=A:;C: ]
[ ]--> [lib=D:LIB ]
[ ]--> [include=D:INCLUDE ]
[ ]--> [conspec=C:COMMAND.COM]
[NULL]
Операционная система поддерживает передачу значений для параметров argc, argv, argp, а на пользователе лежит ответственность за передачу и использование фактических аргументов функции main.
Следующий пример представляет программу печати фактических аргументов, передаваемых в функцию main из операционной системы и параметров операционной системы.
Пример:
int main ( int argc, char *argv[], char *argp[])
{ int i=0;
printf ('n Имя программы %s', argv[0]);
for (i=1; i>=argc; i++)
printf ('n аргумент %d равен %s', argv[i]);
printf ('n Параметры операционной системы:');
while (*argp)
{ printf ('n %s',*argp);
argp++;
}
return (0);
}
Доступ к параметрам операционной системы можно также получить при помощи библиотечной функции geteuv, ее прототип имеет следующий вид:
char *geteuv (const char *varname);
Аргумент этой функции задает имя параметра среды, указатель на значение которой выдаст функция geteuv. Если указанный параметр не определен в среде в данный момент, то возвращаемое значение NULL.
Используя указатель, полученный функцией geteuv, можно только прочитать значение параметра операционной системы, но нельзя его изменить. Для изменения значения параметра системы предназначена функция puteuv.
Компилятор языка СИ строит СИ-программу таким образом, что вначале работы программы выполняется некоторая инициализация, включающая, кроме всего прочего, обработку аргументов, передаваемых функции main, и передачу ей значений параметров среды. Эти действия выполняются библиотечными функциями _setargv и _seteuv, которые всегда помещаются компилятором перед функцией main.
Если СИ-программа не использует передачу аргументов и значений параметров операционной системы, то целесообразно запретить использование библиотечных функций _setargv и _seteuv, поместив в СИ-программу перед функцией main функции с такими же именами, но не выполняющие никаких действий (заглушки). Начало программы в этом случае будет иметь вид:
_setargv()
{ return; /* пустая функция */
}
-seteuv()
{ return ; /* пустая функция */
}
int main()
{ /* главная функция без аргументов */
...
...
renurn (0);
}
В приведенной программе при вызове библиотечных функций _setargv и _seteuv будут использованы функции, помещенные в программу пользователем и не выполняющие никаких действий. Это заметно снизит размер получаемого exe-файла.
Выход из функций. Оператор return
Оператор return завершает выполнение функции, в которой он задан, и возвращает управление в вызывающую функцию, в точку, непосредственно следующую за вызовом. Функция main передает управление операционной системе. Формат оператора:
return [выражение];
Значение выражения, если оно задано, возвращается в вызывающую функцию в качестве значения вызываемой функции. Если выражение опущено, то возвращаемое значение не определено. Выражение может быть заключено в круглые скобки, хотя их наличие не обязательно.
Если в какой-либо функции отсутствует оператор return, то передача управления в вызывающую функцию происходит после выполнения последнего оператора вызываемой функции. При этом возвращаемое значение не определено.
Если функция не должна иметь возвращаемого значения, то ее нужно объявлять с типом void.
Таким образом, использование оператора return необходимо либо для немедленного выхода из функции, либо для передачи возвращаемого значения.
Пример:
int sum (int a, int b)
{ renurn (a+b); }
Функция sum имеет два формальных параметра a и b типа int, и возвращает значение типа int, о чем говорит описатель, стоящий перед именем функции.
Возвращаемое оператором return значение равно сумме фактических параметров.
Пример:
void prov (int a, double b)
{ double c;
if (a==10) return;
else { c=a+b;
if ((2*c-b)==11) return;
}
}
В этом примере оператор return используется для выхода из функции в случае выполнения одного из проверяемых условий.
Преобразования при вызове функции
Преобразования, выполняемые над аргументами при вызове функции, зависят от того, был ли задан прототип функции (объявление 'вперед') со списком объявлений типов аргументов.
Если задан прототип функции, и он включает объявление типов аргументов, то над аргументами в вызове функции выполняются только обычные арифметические преобразования.
Эти преобразования выполняются независимо для каждого аргумента. Величины типа float преобразуются к double, величины типа char и short преобразуются к int, величины типов unsigned char и unsigned short преобразуются к unsigned int. Могут быть также выполнены неявные преобразования переменных типа указатель. Задавая прототипы функций, можно переопределить эти неявные преобразования и позволить компилятору выполнить контроль типов.
Встроенные функции
При использовании функции расходуется время и память, связанные с вызовом функции, передачей аргументов, возвращаемых значений. Можно использовать макросы, но в них отсутствуют локальные переменные и другие полезные элементы функции. В С++ есть встроенные функции, вызовы которых компилятор заменяет телом самой функции.
Синтаксис встроенной функции:
inline возврТип имяФункции (<список параметров>)
{<операторный блок>}
Примеры:
inline double cube (double x)
{ return x*x*x;}
inline char nextChar (char c)
{ return c+1;}
В отличие от макросов встроенные функции производят проверку типов, что делает их использование в ряде случаев предпочтительнее.
При отладке программы встроенные функции рекомендуется объявлять как обычные. Желательно использовать встроенные функции небольших размеров, не содержащих сложных действий.
Аргументы по умолчанию
В С++ можно использовать аргументы по умолчанию (т.е. этот аргумент можно опускать при вызове функции), но при этом необходимо соблюдать следующие правила:
- если одному параметру присвоено значение по умолчанию, то значения по умолчанию надо присваивать и всем последующим параметрам;
- если опущен аргумент параметра, используемого по умолчанию, то нужно опустить аргументы и для всех оставшихся в списке параметров.
Перегрузка функций
В С++ можно использовать перегрузку функций. Это позволяет определять функции с одним и тем же именем, но разными списками параметров. Список параметров часто называют сигнатурой функции. Тип возвращаемого значения не относится сигнатуре функции.
В перегруженных функциях не рекомендуется использовать аргументы по умолчанию.
1.9.2 Классы памяти переменных и функций
Обычная СИ-программа представляет собой определение функции main, которая для выполнения необходимых действий вызывает другие функции. Приведенные выше примеры программ представляли собой один исходный файл, содержащий все необходимые для выполнения программы функции. Связь между функциями осуществлялась по данным посредством передачи параметров и возврата значений функций. Но компилятор языка СИ позволяет также разбить программу на несколько отдельных частей (исходных файлов), оттранслировать каждую часть отдельно, и затем объединить все части в один выполняемый файл при помощи редактора связей.
При такой структуре исходной программы функции, находящиеся в разных исходных файлах могут использовать глобальные внешние переменные. Все функции в языке Си по определению внешние и всегда доступны из любых файлов. Например, если программа состоит из двух исходных файлов, как показано на рис. 2., то функция main может вызывать любую из трех функций fun1, fun2, fun3, а каждая из этих функций может вызывать любую другую.
fun1() { ... } main () { ... } |
... fun2() { ... } fun3() { ... } |
|
file1.c |
file2.c |
Рисунок 2 - Пример программы из двух файлов
Для того, чтобы определяемая функция могла выполнять какие либо действия, она должна использовать переменные. В языке СИ все переменные должны быть объявлены до их использования. Объявления устанавливают соответствие имени и атрибутов переменной, функции или типа. Определение переменной вызывает выделение памяти для хранения ее значения. Класс выделяемой памяти определяется спецификатором класса памяти, и определяет время жизни и область видимости переменной, связанные с понятием блока программы.
В языке СИ блоком считается последовательность объявлений, определений и операторов, заключенная в фигурные скобки. Существуют два вида блоков - составной оператор и определение функции, состоящее из составного оператора, являющегося телом функции, и предшествующего телу заголовка функции (в который входят имя функции, типы возвращаемого значения и формальных параметров). Блоки могут включать в себя составные операторы, но не определения функций. Внутренний блок называется вложенным, а внешний блок - объемлющим.
Время жизни - это интервал времени выполнения программы, в течение которого программный объект (переменная или функция) существует. Время жизни переменной может быть локальным или глобальным. Переменная с глобальным временем жизни имеет распределенную для нее память и определенное значение на протяжении всего времени выполнения программы, начиная с момента выполнения объявления этой переменной. Переменная с локальным временем жизни имеет распределенную для него память и определенное значение только во время выполнения блока, в котором эта переменная определена или объявлена. При каждом входе в блок для локальной переменной распределяется новая память, которая освобождается при выходе из блока.
Все функции в СИ имеют глобальное время жизни и существуют в течение всего времени выполнения программы.
Область видимости - это часть текста программы, в которой может быть использован данный объект. Объект считается видимым в блоке или в исходном файле, если в этом блоке или файле известны имя и тип объекта. Объект может быть видимым в пределах блока, исходного файла или во всех исходных файлах, образующих программу. Это зависит от того, на каком уровне объявлен объект: на внутреннем, т.е. внутри некоторого блока, или на внешнем, т.е. вне всех блоков.
Если объект объявлен внутри блока, то он видим в этом блоке, и во всех внутренних блоках. Если объект объявлен на внешнем уровне, то он видим от точки его объявления до конца данного исходного файла.
Объект может быть сделан глобально видимым с помощью соответствующих объявлений во всех исходных файлах, образующих программу.
Спецификатор класса памяти в объявлении переменной может быть auto, register, static или extern. Если класс памяти не указан, то он определяется по умолчанию из контекста объявления.
Объекты классов auto и register имеют локальное время жизни. Спецификаторы static и extern определяют объекты с глобальным временем жизни.
При объявлении переменной на внутреннем уровне может быть использован любой из четырех спецификаторов класса памяти, а если он не указан, то подразумевается класс памяти auto.
Переменная с классом памяти auto имеет локальное время жизни и видна только в блоке, в котором объявлена. Память для такой переменной выделяется при входе в блок и освобождается при выходе из блока. При повторном входе в блок этой переменной может быть выделен другой участок памяти. Переменная с классом памяти auto автоматически не инициализируется. Она должна быть проинициализирована явно при объявлении путем присвоения ей начального значения. Значение неинициализированной переменной с классом памяти auto считается неопределенным.
Спецификатор класса памяти register предписывает компилятору распределить память для переменной в регистре, если это представляется возможным. Использование регистровой памяти обычно приводит к сокращению времени доступа к переменной. Переменная, объявленная с классом памяти register, имеет ту же область видимости, что и переменная auto. Число регистров, которые можно использовать для значений переменных, ограничено возможностями компьютера, и в том случае, если компилятор не имеет в распоряжении свободных регистров, то переменной выделяется память как для класса auto. Класс памяти register может быть указан только для переменных с типом int или указателей с размером, равным размеру int.
Переменные, объявленные на внутреннем уровне со спецификатором класса памяти static, обеспечиваю возможность сохранить значение переменной при выходе из блока и использовать его при повторном входе в блок. Такая переменная имеет глобальное время жизни и область видимости внутри блока, в котором она объявлена. В отличие от переменных с классом auto, память для которых выделяется в стеке, для переменных с классом static память выделяется в сегменте данных, и поэтому их значение сохраняется при выходе из блока.
Пример:
/* объявления переменной i на внутреннем уровне
с классом памяти static. */
/* исходный файл file1.c */
main()
{ ...
}
fun1()
{
static int i=0; ...
}
/* исходный файл file2.c */
fun2()
{
static int i=0; ...
}
fun3()
{
static int i=0; ...
}
В приведенном примере объявлены три разные переменные с классом памяти static, имеющие одинаковые имена i. Каждая из этих переменных имеет глобальное время жизни, но видима только в том блоке (функции), в которой она объявлена. Эти переменные можно использовать для подсчета числа обращений к каждой из трех функций.
Переменные класса памяти static могут быть инициализированы константным выражением. Если явной инициализации нет, то такой переменной присваивается нулевое значение. При инициализации константным адресным выражением можно использовать адреса любых внешних объектов, кроме адресов объектов с классом памяти auto, так как адрес последних не является константой и изменяется при каждом входе в блок. Инициализация выполняется один раз при первом входе в блок.
Переменная, объявленная локально с классом памяти extern, является ссылкой на переменную с тем же самым именем, определенную глобально в одном из исходных файлов программы. Цель такого объявления состоит в том, чтобы сделать определение переменной глобального уровня видимым внутри блока.
Пример:
/* объявления переменной i, являющейся именем внешнего
массива длинных целых чисел, на локальном уровне */
/* исходный файл file1.c */
main()
{ ... }
fun1()
{
extern long i[]; ...
}
/* исходный файл file2.c */
long i[MAX]={0};
fun2()
{ ... }
fun3()
{ ... }
Объявление переменной i[] как extern в приведенном примере делает ее видимой внутри функции fun1. Определение этой переменной находится в файле file2.c на глобальном уровне и должно быть только одно, в то время как объявлений с классом памяти extern может быть несколько.
Объявление с классом памяти extern требуется при необходимости использовать переменную, описанную в текущем исходном файле, но ниже по тексту программы, т.е. до выполнения ее глобального определения. Следующий пример иллюстрирует такое использование переменной с именем st.
Пример:
main()
{
extern int st[]; ...
}
static int st[MAX]={0};
fun1()
{ ... }
Объявление переменной со спецификатором extern информирует компилятор о том, что память для переменной выделять не требуется, так как это выполнено где-то в другом месте программы.
При объявлении переменных на глобальном уровне может быть использован спецификатор класса памяти static или extern, а так же можно объявлять переменные без указания класса памяти. Классы памяти auto и register для глобального объявления недопустимы.
Объявление переменных на глобальном уровне - это или определение переменных, или ссылки на определения, сделанные в другом месте программы. Объявление глобальной переменной, которое инициализирует эту переменную (явно или неявно), является определением переменной. Определение на глобальном уровне может задаваться в следующих формах:
1. Переменная объявлена с классом памяти static. Такая переменная может быть инициализирована явно константным выражением, или по умолчанию нулевым значением. То есть обявления static int i=0 и static int i эквивалентны, и в обоих случаях переменной i будет присвоено значение 0.
2. Переменная объявлена без указания класса памяти, но с явной инициализацией. Такой переменной по умолчанию присваивается класс памяти static. То есть объявления int i=1 и static int i=1 будут эквивалентны.
Переменная, объявленная глобально, видима в пределах остатка исходного файла, в котором она определена. Выше своего описания и в других исходных файлах эта переменная невидима (если только она не объявлена с классом extern).
Глобальная переменная может быть определена только один раз в пределах своей области видимости. В другом исходном файле может быть объявлена другая глобальная переменная с таким же именем и с классом памяти static, конфликта при этом не возникает, так как каждая из этих переменных будет видимой только в своем исходном файле.
Спецификатор класса памяти extern для глобальных переменных используется, как и для локального объявления, в качестве ссылки на переменную, объявленную в другом месте программы, т.е. для расширения области видимости переменной. При таком объявлении область видимости переменной расширяется до конца исходного файла, в котором сделано объявление. В объявлениях с классом памяти extern не допускается инициализация, так как эти объявления ссылаются на уже существующие и определенные ранее переменные. Переменная, на которую делается ссылка с помощью спецификатора extern, может быть определена только один раз в одном из исходных файлов программы.
Классы памяти функций. Функции всегда определяются глобально. Они могут быть объявлены с классом памяти static или extern. Объявления функций на локальном и глобальном уровнях имеют одинаковый смысл. Правила определения области видимости для функций отличаются от правил видимости для переменных и состоят в следующем.
1. Функция, объявленная как static, видима в пределах того файла, в котором она определена. Каждая функция может вызвать другую функцию с классом памяти static из своего исходного файла, но не может вызвать функцию определенную с классом static в другом исходном файле. Разные функции с классом памяти static имеющие одинаковые имена могут быть определены в разных исходных файлах, и это не ведет к конфликту.
2. Функция, объявленная с классом памяти extern, видима в пределах всех исходных файлов программы. Любая функция может вызывать функции с классом памяти extern.
3. Если в объявлении функции отсутствует спецификатор класса памяти, то по умолчанию принимается класс extern.
Все объекты с классом памяти extern компилятор помещает в объектном файле в специальную таблицу внешних ссылок, которая используется редактором связей для разрешения внешних ссылок. Часть внешних ссылок порождается компилятором при обращениях к библиотечным функциям СИ, поэтому для разрешения этих ссылок редактору связей должны быть доступны соответствующие библиотеки функций.
Время жизни переменной (глобальной или локальной) определяется по следующим правилам.
1. Переменная, объявленная глобально (т.е. вне всех блоков), существует на протяжении всего времени выполнения программы.
2. Локальные переменные (т.е. объявленные внутри блока) с классом памяти register или auto, имеют время жизни только на период выполнения того блока, в котором они объявлены. Если локальная переменная объявлена с классом памяти static или extern, то она имеет время жизни на период выполнения всей программы.
Видимость переменных и функций в программе определяется следующими правилами.
1. Переменная, объявленная или определенная глобально, видима от точки объявления или определения до конца исходного файла. Можно сделать переменную видимой и в других исходных файлах, для чего в этих файлах следует ее объявить с классом памяти extern.
2. Переменная, объявленная или определенная локально, видима от точки объявления или определения до конца текущего блока. Такая переменная называется локальной.
3. Переменные из объемлющих блоков, включая переменные, объявленные на глобальном уровне, видимы во внутренних блоках. Эту видимость называют вложенной. Если переменная, объявленная внутри блока, имеет то же имя, что и переменная, объявленная в объемлющем блоке, то это разные переменные, и переменная из объемлющего блока во внутреннем блоке будет невидимой.
4. Функции с классом памяти static видимы только в исходном файле, в котором они определены. Всякие другие функции видимы во всей программе.
Метки в функциях видимы на протяжении всей функции.
Имена формальных параметров, объявленные в списке параметров прототипа функции, видимы только от точки объявления параметра до конца объявления функции.
При инициализации глобальных и локальных переменных необходимо придерживаться следующих правил:
1. Объявления, содержащие спецификатор класса памяти extern, не могут содержать инициаторов.
2. Глобальные переменные всегда инициализируются, и если это не сделано явно, то они инициализируются нулевым значением.
3. Переменная с классом памяти static может быть инициализирована константным выражением. Инициализация для них выполняется один раз перед началом программы. Если явная инициализация отсутствует, то переменная инициализируется нулевым значением.
4. Инициализация переменных с классом памяти auto или register выполняется всякий раз при входе в блок, в котором они объявлены. Если инициализация переменных в объявлении отсутствует, то их начальное значение не определено.
5. Начальными значениями для глобальных переменных и для переменных с классом памяти static должны быть константные выражения. Адреса таких переменных являются константами и эти константы можно использовать для инициализации объявленных глобально указателей. Адреса переменных с классом памяти auto или register не являются константами и их нельзя использовать в инициаторах.
Пример:
int global_var;
int func(void)
{
int local_var; /* по умолчанию auto */
static int *local_ptr=&local_var; /* так неправильно */
static int *global_ptr=&global_var; /* а так правильно */
register int *reg_ptr=&local_var; /* и так правильно */
}
В приведенном примере глобальная переменная global_var имеет глобальное время жизни и постоянный адрес в памяти, и этот адрес можно использовать для инициализации статического указателя global_ptr. Локальная переменная local_var, имеющая класс памяти auto размещается в памяти только на время работы функции func, адрес этой переменной не является константой и не может быть использован для инициализации статической переменной local_ptr. Для инициализации локальной регистровой переменной reg_ptr можно использовать неконстантные выражения, и, в частности, адрес переменной local_ptr.
1.9.3 Директивы препроцессора
Директивы препроцессора представляют собой инструкции, записанные в тексте программы на СИ, и выполняемые до трансляции программы. Директивы препроцессора позволяют изменить текст программы, например, заменить некоторые лексемы в тексте, вставить текст из другого файла, запретить трансляцию части текста и т.п. Все директивы препроцессора начинаются со знака #. После директив препроцессора точка с запятой не ставятся.
Примером может служить директива #include
Директива #include включает в текст программы содержимое указанного файла. Эта директива имеет две формы:
#include 'имя файла'
#include
Имя файла должно соответствовать соглашениям операционной системы и может состоять либо только из имени файла, либо из имени файла с предшествующим ему маршрутом. Если имя файла указано в кавычках, то поиск файла осуществляется в соответствии с заданным маршрутом, а при его отсутствии в текущем каталоге. Если имя файла задано в угловых скобках, то поиск файла производится в стандартных директориях операционной системы, задаваемых командой PATH.
Директива #include может быть вложенной, т.е. во включаемом файле тоже может содержаться директива #include, которая замещается после включения файла, содержащего эту директиву.
Директива #include широко используется для включения в программу так называемых заголовочных файлов, содержащих прототипы библиотечных функций, и поэтому большинство программ на СИ начинаются с этой директивы.
язык операция переменная программа
1.9.4 Макроподстановки
Директива #define служит для замены часто использующихся констант, ключевых слов, операторов или выражений некоторыми идентификаторами. Идентификаторы, заменяющие текстовые или числовые константы, называют именованными константами. Идентификаторы, заменяющие фрагменты программ, называют макроопределениями, причем макроопределения могут иметь аргументы.
Директива #define имеет две синтаксические формы:
#define идентификатор текст
#define идентификатор (список параметров) текст
Эта директива заменяет все последующие вхождения идентификатора на текст. Такой процесс называется макроподстановкой. Текст может представлять собой любой фрагмент программы на СИ, а также может и отсутствовать. В последнем случае все экземпляры идентификатора удаляются из программы.
Пример:
#define WIDTH 80
#define LENGTH (WIDTH+10)
Эти директивы изменят в тексте программы каждое слово WIDTH на число 80, а каждое слово LENGTH на выражение (80+10) вместе с окружающими его скобками.
Скобки, содержащиеся в макроопределении, позволяют избежать недоразумений, связанных с порядком вычисления операций. Например, при отсутствии скобок выражение t=LENGTH*7 будет преобразовано в выражение t=80+10*7, а не в выражение t=(80+10)*7, как это получается при наличии скобок, и в результате получится 780, а не 630.
Во второй синтаксической форме в директиве #define имеется список формальных параметров, который может содержать один или несколько идентификаторов, разделенных запятыми. Формальные параметры в тексте макроопределения отмечают позиции, на которые должны быть подставлены фактические аргументы макровызова. Каждый формальный параметр может появиться в тексте макроопределения несколько раз.
При макровызове вслед за идентификатором записывается список фактических аргументов, количество которых должно совпадать с количеством формальных параметров.
Пример:
#define MAX(x,y) ((x)>(y))?(x):(y)
Эта директива заменит фрагмент
t=MAX(i,s[i]);
на фрагмент
t=((i)>(s[i])?(i):(s[i]);
Как и в предыдущем примере, круглые скобки, в которые заключены формальные параметры макроопределения, позволяют избежать ошибок связанных с неправильным порядком выполнения операций, если фактические аргументы являются выражениями.
Например, при наличии скобок фрагмент
t=MAX(i&j,s[i]||j);
будет заменен на фрагмент
t=((i&j)>(s[i]||j)?(i&j):(s[i]||j);
а при отсутствии скобок - на фрагмент
t=(i&j>s[i]||j)?i&j:s[i]||j;
Условное выражение вычисляется в другом порядке.
Директива #undef используется для отмены действия директивы #define. Синтаксис этой директивы следующий:
#undef идентификатор
Директива отменяет действие текущего определения #define для указанного идентификатора. Не является ошибкой использование директивы #undef для идентификатора, который не был определен директивой #define.
Пример:
#undef WIDTH
#undef MAX
Эти директивы отменяют определение именованной константы WIDTH и макроопределения MAX.
Раздел II. Объектно-ориентированное программирование и классы C++
С введением классов в С++ появляются объектно-ориентированные программные конструкции.
2.1 Основы объектно-ориентированного программирования
Мы живем в мире объектов. Каждый объект имеет определенные атрибуты и набор действий, которые могут им (или над ними) производиться. Объекты могут существенно отличаться друг от друга, и вы можете ввести некоторую классификацию объектов по «классам. Например, апельсин можно причислить к классу фруктов, а наручные часы CASIO Data Bank - к классу наручных часов. Объектно - ориентированное программирование (ООП) использует при разработке программ понятия объектов.
На основе отдельно взятого класса вы можете построить целую иерархию классов. Например, класс часов CASIO Data Bank может являться частью иерархии классов, включающий в себя классы Swatch и Rolex. Кроме классов, основными понятиями ООП являются: объекты, методы, наследование и полиформизм. Термином класс определяется тип объектов. Каждый отдельный объект класса является представителем данного класса.
Классы. Объекты. Можно сказать, что объектам класса присущи общие свойства и функциональное назначение. Однако каждый объект всегда имеет свое, уникальное состояние, определяемое текущими значениями его свойств (атрибутов). Функциональное назначение класса определяется возможными действиями над представителями класса. В С++ свойства класса определяются понятием элементы данных, а действия - понятием функции - элементы. Говорят, что классы инкапсилируют элементы - данные и элементы - функции.
Возвращаясь назад, к нашему примеру класса часов CASIO, можно сказать, что имеющиеся на часах кнопки представляют собой функции-элементы, в то время как дисплей является представителем данных-элементов. Нажатием кнопки вы можете изменить дату и время. Говоря языком ООП, этим действием вы изменили состояние объекта, поскольку вы изменили значения его элементов-данных.
Сообщения и методы. В объектно-ориентированном программировании взаимодействие между объектами строится на основе событий, когда запросы посылаются от одного объекта к другому или происходит обмен запросами между объектами. Объект, получивший запрос, отвечает на него посредством соответствующего метода, а именно, соответствующей функции-элемента. В С++ не определяются строго понятия запроса и метода, как это делается в других языках ООП, таких, например, как SmallTalk. Тем не менее, некоторые находят удобным называть вызов (активацию) функции-элемента термином «запрос». Что касается терминов метод и функция- элемент - они являются синонимами. Запрос - это некоторое действие, совершаемое над объектом. А метод - это способ ответа объекта на пришедший запрос. Функция-элемент - это обычная функция, объявление которой внесено в определение класса.
Связь функции-элемента с включающим ее классом превращает функцию-элемент в специфический вид функции. Во время своего выполнения функция-элемент имеет доступ ко всем данным своего класса, и ей не требуется указания на имя класса. Это оказывается возможным благодаря скрытому параметру, который передается каждой функции-элементу. Этот скрытый параметр называется this-указателем, и является специфическим указателем на объект класса. Когда функции-элементу нужно получить доступ к элементу данных, компилятор генерирует код, использующий указатель this. Если вам нужно получить доступ к объему класса и использовать его данные, вы можете использовать этот указатель в явном виде.
Наследование. В объектно-ориентированном программировании на основе уже существующих классов вы можете строить производные классы. Термин наследование означает, что производный класс (называемый также классом-потомком) наследует элементы-данные и функции-элементы от своих родительских классов (классов-предков). Термин базовый класс используется как синоним родительскому классу в иерархии классов.
При создании производственного класса, родительский класс совершенствуется путем добавления новых свойств и новых функций. Обычно производный класс определяет новые элементы-данные и функции-элементы. Кроме того, в производном классе наследуемые функции могут быть переопределены, если они оказываются непригодными в новом классе.
Попробуем применить концепцию наследования к классу часов CASIO Data Bank. Предположим, что производитель часов задумал создать новую модель CASIO Data Comm, которая обладает теми же качествами, что и CASIO Data Bank, но еще имеет будильник. Вместо того, чтобы создавать новую модель заново (на языке ООП-новый класс), инженеры фирмы CASIO берут за основу модель CASIO Data Bank. В результате у новой модели могут появиться новые свойства и функции, могут быть пересмотрены старые функции. Таким образом, модель CASIO Data Comm наследует функции и свойства модели CASIO Data Bank. Можно сказать, что класс CASIO Data Comm является потомком CASIO Data Bank, или производным классом.
Полиморфизм. Понятие полиморфизма в ООП понимается как способность объектов различных классов реагировать на запросы функций по-разному. Например, в иерархии графических примитивов (точки, линии, квадраты, углы, окружности, эллипсы и т.д.) каждый примитив имеет свою функцию Draw, которая должна соответствующим образом прорисовывать нужный в данном запросе объект. Полиморфизм - это способность объектов различных классов отвечать на запрос функции сообразно типу своего класса.
2.2 Базовые классы
Итак, в С++ вы имеете возможность объявлять классы, которые инкапсулируют элементы-данные и функции-элементы. Эти функции изменяют и позволяют обращаться к значениям данных-элементов и выполняют другие задачи.
Базовый класс определяется следующим образом:
class className
private:
<закрытые элементы-данные
закрытые конструкторы
закрытые функции-элементы
protected:
защищенные элементы-данные
защищенные конструкторы
защищенные функции-элементы
public:
открытые элементы-данные
открытые конструкторы
открытый деструктор
открытые функции-элементы
Пример:
class point
protected
double x;
double y;
public:
point (double xVal, double yVal);
double getX ( );
double getY ( );
void assign (double xVal, double yVal) ;
point& assign (point &pt);
Разделы класса. Классы С++ имеют три различных уровня доступа к своим элементам - как к данным, так и к функциям:
Закрытые (частные) элементы;
Защищенные элементы;
Открытые элементы.
К данным в закрытом разделе имеют доступ только функции-элементы класса. Классам-потомкам запрещен доступ к закрытым данным своих базовых классов. К данным в защищенной секции имеют доступ функции-элементы класса и классов-потомков. Данные из открытой секции находятся в области видимости функций-элементов класса, функций-элементов классов-потомков, и вообще доступны кому угодно.
Существуют следующие правила для разделения класса.
Разделы могут появиться в любом порядке.
Один и тот же раздел можно определять несколько раз.
Если не определен ни один раздел, компилятор (по умолчанию) объявляет все элементы закрытыми.
Помещать данные-элементы в открытый раздел следует только в том случае, если в этом есть необходимость, например, если это упрощает вашу задачу. Обычно элементы-данные помещаются в защищенный раздел, чтобы к ним имели доступ функции-элементы классов-потомков.
Используйте для изменения значений данных и доступа к ним функции-элементы. При использовании функции вы можете осуществлять проверку данных и, если нужно, изменять другие данные.
Класс может иметь несколько конструкторов.
Класс может иметь только один деструктор, который должен объявляться в открытом разделе класса.
Функции-элементы (в том числе конструкторы и деструкторы), состоящие из- нескольких операторов, должны определяться вне объявления класса. Определение функции может содержаться в том же файле, в котором определяется класс. Это напоминает порядок работы с обычными функциями: задание прототипа и определение функции.
Конструкторы являются специфическим типом функций-элементов, тип возвращаемого значения, для которых не указывается, а имя должно совпадать с именем класса-хозяина. Вызываются они при создании нового представителя класса. Деструктор вызывается для разрушения представителя класс. Определения функций-элементов, упоминавшихся выше, принято собирать в отдельный файл (файл. Н или. ССР). При определении функции-элемента вы должны указать ее имя и имя ее класса (т. н. квалификатор), а затем, черед два двоеточия (::), имя функции. В качестве примера рассмотрим такой класс:
Пример:
class point
proteсted:
double x;
double y;
public:
point (double xVal, double yVal);
double getX( ) ;
/ / другие функции-элементы
Определения конструктора и функций - элементов должны выглядеть так
point : : point (double xVal, double yVal)
/ / операторы
double point : : getX ( )
/ / операторы
После того, как вы объявили класс, вы можете использовать имя класса в качестве спецификатора типа данных при объявлении представителей класса. Синтаксис объявления тот же, что и при объявлении переменной.
В листинге приведен исходный текст программы RECT. CPP. Программа предлагает вам ввести длину и ширину прямоугольника (в данном примере прямоугольник является объектом). Затем программа выводит значения длины, ширины и площади определенного вами прямоугольника. Конструкторы. Конструкторы и деструкторы в C++ вызываются автоматически, что гарантирует правильное создание и разрушение объектов класса. Общий вид объявления конструктора:
class className
{
public:
className () ; //конструктор по умолчанию
className(const className &c); // конструктор копии
className (<список параметров;); //остальные конструкторы
};
Конструктор копии создает объект класса, копируя при этом данные из существующего объекта класса.
В C++ имеются следующие особенности и правила работы с конструкторами:
Имя конструктора класса должно совпадать с именем класса.
Вы не должны определять тип возвращаемого значения для конструктора, даже тип void.
Класс может иметь несколько конструкторов или не иметь их совсем.
Конструктором по умолчанию является конструктор, не имеющий параметров, или конструктор, у которого все параметры имеют значения по умолчанию.
Примеры:
// класс с конструктором без параметров
class point1
{
protected:
double x;
double у;
public:
point1 () ;
// другие функции-элементы ;
};
// конструктор класса имеет параметры со значениями по умолчанию
class point2
{
protected:
double x;
double у;
public:
point2(double xVal = 0, double yVal = 0) ;
// другие функции-элементы
};
5. Конструктор копии создает объект класса на основе существующего объекта. Например:
class point
{
protected:
double x;
double у;
public:
point () ;
point(double xVal = 0, double yVal = 0) ;
point(const point &pt);
// другие функции-элементы
};
6. Объявление объекта класса, которое может содержать параметры и, в качестве параметра, имя уже существующего объекта, влечет за собой вызов конструктора. Но какой из конструкторов будет использоваться в каждом конкретном случае? Ответ зависит от того, как много конструкторов вы объявили и с какими аргументами вы объявляете объект класса.
Например, рассмотрим следующие объявления объектов.
point p1; //применяется конструктор по умолчанию
point p2(1.1, 1.3); // используется второй по счету конструктор
point p3 (p2); // используется конструктор копии
Поскольку объект р1 объявляется без параметров, компилятор использует конструктор по умолчанию. Объект p2 объявляется с двумя вещественными аргументами, поэтому компилятор вызовет второй конструктор. Объект p3 при объявлении имеет параметром объект p2, поэтому компилятор вызовет конструктор копии, чтобы создать новый объект из объекта p2.
Деструкторы. Классы C++ могут содержать деструкторы, которые автоматически разрушают объекты класса.
Общий синтаксис объявления деструктора:
class className
{
public:
className(); // конструктор по умолчанию
// другие конструкторы
~className() ;
// другие функции-элементы
};
Пример:
class String
{
protected:
char *str;
int len;
public:
String() ;
String(const String& s);
~String() ;
// другие функции-элементы
};
Деструкторы в C++ имеют следующие особенности и подчиняются следующим правилам:
1. Имя деструктора должно начинаться со знака тильды (~), за которым должно следовать имя класса.
2. Вы не должны определять тип возвращаемого значения, даже тип void.
3. Класс может иметь только один деструктор или ни одного. В последнем случае компилятор создаст деструктор по умолчанию.
4. Деструктор не должен иметь параметров.
5. Исполняющая система автоматически вызывает деструктор класса, когда объект класса выходит за пределы области действия и может быть удален, или удаляется явным образом.
2.3 Производные классы
Одно из мощных объектно-ориентированных свойств языка C++ состоит в том, что вы можете строить новые классы на основе уже существующих, Класс-потомок наследует элементы своих классов-предков (т.е. «отцовского» класса, «дедушкиного» класса и т.д.) и при этом может переопределять, если это необходимо, некоторые наследуемые функции. Таким образом, наследование позволяет вам в классе-потомке повторно использовать программный код класса-предка.
Общая форма объявления производного класса:
class classname : [<спецификатор доступа>] parentClass
{
дружественные классы>
private:
<закрытые элементы-данные>
<закрытые конструкторы>
<закрытые функции-элементы>
protected:
<защищенные элементы-данные>
защищенные конструкторы>
защищенные функции-элементы»
public:
открытые элементы-данные>
<открытые конструкторы>
< открытый деструктор>
<открытые функции-элементы>
<дружественные функции и дружественные операции>
}
Пример:
В следующем примере объявляются класс Rectangle и класс-потомок Box:
class Rectangle
{ protected:
double length;
double width;
public:
Rectangle(double len, double wide);
double getLength() const;
double getWidthO const;
double assign(double len, double wide);
double calcArea();
};
class Box : 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 ();
};
При объявлении производного класса после его имени ставится двоеточие и указывается родословная класса: список имен родительских классов, которым может предшествовать спецификатор доступа (которым может быть либо public, либо protected, либо private). Если вы указали спецификатор public, то объекты класса-потомка будут иметь доступ к открытым элементам родительского класса и других классов-предков. А если вы укажете спецификатор private или опустите спецификатор, то вы лишите объект класса-готомка доступа к элементам классов-предков.
Класс-потомок наследует данные-элементы своих классов-предков. В C++ нет возможности отказаться от наследования ненужных элементов-данных, но зато можно переопределять наследуемые функции-элементы.
Рассмотрим пример объявления простой иерархии классов. Текст программы CIRCLE.CPP содержится в листинге. Объявляется иерархия классов графических примитивов: окружностей и цилиндров. В программе отсутствует ввод данных. Все данные для создания объектов задаются самой программой, затем программа выводит характеристики объектов - размеры и площадь.
Исходный текст для программы C1RCLE.CPP
// Простой пример иерархии классов
#include <iostream.h>
#include <math.h>
const double pi = 4 * atan(l);
inline double sqr(double x)
{
return x * x;
}
class Circle
{
protected:
double radius;
public:
Circle (double radiusVal =0) : radius (radiusVal) )
void setRadius(double radiusVal)
{ radius = radiusVal; }
double Radius() const
{ return radius; }
double Area() const
{ return pi * sqr(radius); }
void showData();
}
class Cylinder : public Circle
{protected:
double height;
public:
Cylinder(double height Val = 0, double radiusVal = 0) : height(heightVal);
Circle(radiusVal);
void setHeight(double heightVal)
{ height = heightVal; }
double Height() const
{ return height; }
double Area() const
{ return 2 * Circle::Area() + 2 * pi * radius * height; }
void showData();
};
void Circle::showData ()
{
cout <<'Circle radius = '<< Radius()<<endl<<'Circle area = '<< <<Area()<<endl<<endl;
void Cylinder::showData()
{
cout << 'Cylinder radius = '<< Radius()<<endl<<
<<'Cylinder height ='<<Heigth()<<endl<<
<< 'Cylinder area = '<< Area()<<endl<<endl;
}
int main() {
Circle circle (1);
Cylinder cylinder (10, 1);
circle.showData();
cylinder.showData ();
return 0;
}
В программе объявляются классы Circle и Cylinder. Класс Circle является моделью окружностей, а класс Cylinder представляет цилиндры. Класс Circle содержит только один элемент данных, radius, предназначенный для хранения радиуса окружности. В классе объявляется конструктор и несколько функций-элементов. Конструктор присваивает значение элементу radius при создании объекта класса. Обратите внимание на новый синтаксис, используемый для присвоения конструктором значения элементу radius. Когда конструктор вызывается, он вызывает конструктор базового класса и, кроме того, инициализирует данные-элементы: все это должно быть указано после двоеточия. В нашем случае элемент radius создается» вызовом его с аргументом radiusVal, т.е. ему присваивается новое значение.
Функция setRadius присваивает значение, а функция Radius возвращает значение переменной radius. Функция Area возвращает значение площади круга. Функция showData выводит значения радиуса и площади объекта.
Класс Cylinder, потомок класса Circle, объявляет только один элемент данных height, в котором хранится высота цилиндра. Класс наследует элемент данных radius, предназначенный для хранения радиуса цилиндра. В классе объявляется конструктор и несколько функций-элементов. При создании объекта класса конструктор присваивает значения элементам radius и height. Обратите внимание на синтаксис определения конструктора - элемент height инициализируется, а конструктор базового класса Circle инициализирует элемент radius величиной radiusVal. Функция setHeight присваивает значение, а функция Height возвращает значение переменной height. Класс наследует функции setRadius и Radius для работы с унаследованным элементом данных radius. Функция Area, переопределяющая наследуемую функцию Circle::Агеа(), возвращает величину площади поверхности цилиндра. Заметьте, что эта переопределенная функция вызывает свою прародительницу - функцию Circle::Агеа(). Функция showData выводит значения радиуса, высоты и площади объекта.
В объявлениях функций Area, Height содержится ключевое слово const. Эта спецификация const сообщает компилятору о том, что функция-элемент не может изменять значения данных-элементов. Эта черта языка очень удобна при создании программ группой программистов, когда руководитель группы, объявляющий класс, может указать те элементы-функции, которые не должны изменять данные объекта класса.
В функции main объявляется объект circle класса Circle с радиусом, равным 1. Объявляется объект cylinder, экземпляр класса Cylinder, с высотой, равной 10 и радиусом равным 1. Затем функция main выводит на экран параметры объектов.
2.4 Изменяемые данные-элементы
Применение ключевого слова const при объявлении функции-элемента запрещает изменять значения элементов-данных. Однако при помощи спецификатора mutable это ограничение можно обойти.
Спецификатор mutable
Общий вид применения спецификатора mutable к объявлению переменной:
mutable <type> <variable>
Спецификатор mutable позволяет объявить данные-элементы изменяемыми теми функциями-элементами, которые были объявлены со спецификатором const.
Пример:
#include <iostream.h>
class A
{
mutable int count;
mutable const int* iptr;
int last;
public:
int func(int i=0) const // не должна изменять данные
{
count = i++; //но изменяет count
iptr = Si; // также изменяет указатель iptr
last = i; // но не элемент last: ОШИБКА!
return count;
}...
Спецификатор mutable может быть полезен для «технических» переменных, например, подсчитывающих количество внутренних обращений и т.п. Определив необходимые данные-элементы со спецификатором mutable, а некоторые функции-элементы со спецификатором const, вы сможете изменять такие изменчивые данные, защитив все остальные.
2.5 Виртуальные функции
Важной особенностью объектно-ориентированного программирования является полиморфизм.
Рассмотрим следующий пример:
#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;
}
В классе Х объявляются функции А и В, причем, функция В вызывает функцию А. Класс Y, потомок класса X, наследует функцию В, но переопределяет функцию А. Цель этого примера - демонстрация полиморфного поведения класса Y. Мы должны получить следующий результат: вызов наследуемой функции Х::В должен привести к вызову функции Y::A. Что же выдаст нам наша программа? Ответом будет 4.5, а не 13.5. В чем же дело? Почему компилятор разрешил выражение у.В(3) как вызов наследуемой функции Х::В, которая, в свою очередь, вызывает Х::А, а не функцию Y::A, что должно было бы произойти в случае полиморфной реакции класса. C++ решает эту проблему и обеспечивает полиморфизм при помощи механизма виртуальных функций.
Виртуальные функции объявляются при помощи спецификатора virtual, указываемого перед типом возвращаемого функцией значения. Выбор нужной виртуальной функции осуществляется во время выполнения программы.
После того как вы объявили функцию как виртуальную, вы можете переопределить (заместить) ее только в классах-потомках, замещающие функции должны иметь тот же список параметров. Виртуальные функции могут замещать невиртуальные функции базовых классов. Как только вы объявили функцию как виртуальную, во всех классах-потомках она будет трактоваться как виртуальная, даже если спецификатор virtual не будет применяться.
Виртуальные функции объявляются следующим образом:
class classNamel
{
// функции-элементы
virtual returnType functionName(<список_параметров>);
};
class className2 : public classNamel
{
// функции-элементы
virtual returnType functionName(<список_п параметров>);
Пример:
# include <iostream.h>
class X
{
public:
virtual double A (double x) {return x*x ; }
double В (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, потому что в результате вызова наследуемой функции Х::В, вызывающей функцию А, в качестве функции А во время выполнения программы будет использована замещающая функция Y::A.
Раздел III. Примеры программ
3.1 Вывод строки приветствия
#include <iostream.h>
int main()
{
cout << 'Hello';
return 0;
}
Программа использует директиву #include для включения заголовочного файла потокового ввода-вывода iostream.h (в нем определяется код, необходимый для вывода). Главная функция программы - функция main целого типа описана командой int main(). В качестве ее значения команда return 0 возвращает 0. Фигурные скобки ограничивают содержимое функции main. Основная команда - вывод текстового сообщения с помощью процедуры вывода cout. Символы `<<' служат для разделения элементов потокового вывода. Обратите внимание на использование разделителей операторов `;' в программе.
3.2 Работа с простыми переменными
#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 << 'x=' << x <<
<< 'y=' << y << endl;
return 0;
}
В программе объявляются переменные i,j типа int и переменных double x,y типа double. Переменные j,y инициализируются при объявлении. Переменная j умножается на 3 и результат сохраняется в переменной i . Значения переменных i,j отображаются на экран.
Элемент потокового вывода endl служит для перевода строки при выводе информации на экран. Переменная y умножается на 2 и результат сохраняется в переменной x. Переменная x возводится в квадрат (умножается сама на себя) и результат сохраняется в этой же переменной x. Значения переменных x,y отображаются на экран.
3.3 Работа с константами с помощью директивы компилятора #define
Программа запрашивает ввод количества часов, минут и секунд, прошедших с полуночи. Затем программа отображает на экране общее число прошедших с полуночи секунд.
#include <iostream.h>
#define sec_in_min 60
#define min_in_hour 60
int main()
{
long h,m,s;
long ts;
cout << 'Введите часы : ' ;
cin >> h;
cout << 'Введите минуты : ';
cin >> m;
cout << 'Введите секунды : ';
cin >> s;
ts=((h*min_in_hour +m )* sec_in_min)+s;
cout << endl << ts << ' секунд после полуночи';
return 0;
}
Заголовочный файл iostream.h содержит определения, необходимые и для ввода информации. Директива #define объявляет константы, определенные с помощью макросов. Объявляются четыре переменные типа long . Значения переменных h,m,s вводятся с помощью процедуры потокового ввода cin , использующей разделители `>>'. Для вывода подсказки и итоговой информации по-прежнему используется процедура cout. В переменной ts сохраняется общее число секунд.
3.4 Работа с константами с использованием формальных констант
Программа аналогична предыдущей. Для объявления констант используется формальный синтаксис языка С++. Константа sec_in_min объявлена вне функции main и является глобальной, константа min_in_hour объявлена внутри функции main и является локальной, то есть ее можно использовать только внутри этой функции.
#include <iostream.h>
const sec_in_min=60;
int main()
{
const int min_in_hour=60 ;
long h,m,s;
long ts;
cout << 'Введите часы : ' ;
cin >> h;
cout << 'Введите минуты : ';
cin >> m;
cout << 'Введите секунды : ';
cin >> s;
ts=((h*min_in_hour +m )* sec_in_min)+s;
cout << endl << ts << ' секунд после полуночи';
return 0;
}
3.5 Сокращенные операции присваивания и инкремента
#include <iostream.h>
int main()
{
int i,j;
double x,y;
// Действия с переменными типа int
cout << 'Введите i : ' ;
cin >> i;
cout << 'Введите j : ';
cin >> j;
i+=j;
j-=6;
i*=4;
j/=3;
i++;
j--;
cout << 'i=' << i << endl
<< 'j=' << j << endl;
// Действия с переменными типа double
cout << 'Введите x : ' ;
cin >> x;
cout << 'Введите y : ';
cin >> y;
x+=y;
y-=4.0;
x*=4.0;
y/=3.0;
x++;
y--;
cout << 'x=' << x << endl
<< 'y=' << y << endl;
return 0;
}
3.6 Программа, возвращающая размеры данных, использующая для этого операцию sizeof() с переменными и типами данных
#include <iostream.h>
int main()
{
short int aS;
int aI;
long aL;
char aC;
float aR;
cout << 'Тип даты Используемая память , байты ' << endl;
cout << 'short int '<< sizeof(aS) << endl;
cout << 'integer '<< sizeof(aI) << endl;
cout << 'long integer '<< sizeof(aL) << endl;
cout << 'character '<< sizeof(aC) << endl;
cout << 'float '<< sizeof(aR) << endl;
cout << endl << endl << 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;
}
3.7 Преобразование типов
#include <iostream.h>
int main()
{
short sI1,sI2;
unsigned short aB;
int aI;
long aL;
char aC;
float aR;
sI1=10;
sI2=6;
aB=sI1+sI2;
aI=sI1-sI2;
aL=sI1*sI2;
aC=aL+5;
aR=sI1*sI2+0.5;
cout <<' sI1= ' << sI1 << endl
<<' sI2= ' << sI2 << endl
<<' aB= ' << aB << endl
<<' aI= ' << aI << endl
<<' aL= ' << aL << endl
<<' aC is ' << aC << endl
<<' aR= ' << aR << endl
<< endl << endl << endl;
aB=(unsigned short) (sI1 + sI2);
aI=(int) (sI1-sI2);
aL=(long) (sI1*sI2);
aC=(unsigned char) (aL + 5);
aR=(float) (sI1*sI2 + 0.5);
cout <<' sI1= ' << sI1 << endl
<<' sI2= ' << sI2 << endl
<<' aB= ' << aB << endl
<<' aI= ' << aI << endl
<<' aL= ' << aL << endl
<<' aC is ' << aC << endl
<<' aR= ' << aR << endl
<< endl << endl << endl;
return 0;
}
3.8 Работа с логическими операциями и условной операцией
#include <iostream.h>
const int MIN_NUM = 30;
const int MAX_NUM = 199;
int main()
{
int i, j, k;
int xor_sum;
int flag1, flag2, in_range,
same_int, xor_flag;
cout << 'Первое число: '; cin >> i;
cout << 'Второе число: '; cin >> j;
cout << 'Третье число: '; cin >> k;
flag1 = i >= MIN_NUM;
flag2 = i <= MAX_NUM;
in_range = flag1 && flag2;
cout << endl << i << ' в диапазоне от '
<< MIN_NUM << ' до ' << MAX_NUM << ': '
<< (in_range ? 'ИСТИНА' : 'ЛОЖЬ');
same_int = i == j || i == k || j==k;
cout << endl << 'по крайней мере два числа из введенных совпадают: '
<< (same_int ? 'ИСТИНА' : 'ЛОЖЬ');
cout << endl << i << ' != ' << j << ': '
<< ((i != j) ? 'ИСТИНА' : 'ЛОЖЬ');
cout << endl << 'NOT (' << i << ' < ' << j << '): '
<< ((!(i < j)) ? 'ИСТИНА' : 'ЛОЖЬ');
cout << endl << i << ' <= ' << j << ': '
<<((i <= j) ? 'ИСТИНА' : 'ЛОЖЬ');
cout << endl << k << ' > ' << j << ': '
<< ((k > j) ? 'ИСТИНА' : 'ЛОЖЬ');
cout << endl << '(' << k << ' = ' << i << ') AND ('
<< j << ' != ' << k << ') : '
<< ((k == i && j != k) ? 'ИСТИНА' : 'ЛОЖЬ');
xor_sum = (k <= i) + (j >= k) ;
xor_flag = xor_sum == 1;
cout << endl << '(' << k << '<=' << i << ') XOR ('
<< j << '>=' << k <<') : '
<< (xor_flag ? 'ИСТИНА' : 'ЛОЖЬ');
cout << endl << '(' << k << '>' << i << ') AND ('
<< j << '<=' << k << ') : '
<< ((k>i && j<=k) ? 'ИСТИНА' : 'ЛОЖЬ');
cout << endl << endl;
return 0;
}
3.9 Краткая форма условного оператора if
#include <iostream.h>
int main()
{
double x;
cout << 'Введите Х ';
cin >> x;
if (x !=0)
cout << ' ФУНКЦИЯ ОТ ' << x
<< ' РАВНА '<<(1/x)<< endl;
return 0;
}
3.10 Полная форма условного оператора if
#include <iostream.h>
#include <ctype.h>
int main()
{
char c;
cout <<'Введите букву :';
cin >> c;
c = toupper (c);
if (c >='A' && c <='Z')
cout << 'Это буква' << endl;
else
cout << ' Это не буква' << endl;
return 0;
}
Файл ctype.h содержит оприсание функции перевода символа в верхний регистр toupper();
3.11 Использование вложенных операторов if
#include <iostream.h>
int main()
{
char c;
cout << 'Введите символ : ';
cin >> c;
if (c >='A' && c<='Z')
cout << 'Это большая буква' << endl;
else if (c >='a' && c<='z')
cout << 'Это строчная буква' << endl;
else if (c >='0' && c<='9')
cout << 'Это цифра' << endl;
else
cout << 'Это вообще чушь' << endl;
return 0;
}
3.12 Применение оператора switch
#include <iostream.h>
int main()
{
char c;
cout <<'Введите переменную : ';
cin >> c;
switch (c)
{
case 'A':
case 'B':
case 'C':
case 'D':
...
cout <<'Это буква прописная' << endl;
break;
case 'a':
case 'b':
case 'c':
case 'd':
...
cout <<'Это буква строчная' << endl;
break;
case '1':
case '2':
case '3':
case '4':
...
cout <<'Это цифра' << endl;
break;
default:
cout << 'Это вообще чушь' << endl;
break;
}
return 0;
}
3.13 Использование вложенных операторов if
#include <iostream.h>
int main()
{
char c;
cout << 'Введите символ : ';
cin >> c;
if ((c >='A' && c <='Z') || (c >='a' && c <='z'))
if (c >='A' && c <='Z')
cout <<'Это буква прописная' << endl;
else
cout <<'Это буква строчная' << endl;
else
cout << 'Это вообще чушь' << endl;
return 0;
}
3.14 Расчет суммы и среднего значения ряда целых чисел
#include <iostream.h>
int main()
{
int count = 0;
double sum = 0.0;
int f, l, t;
cout << 'Введите первое число : ';
cin >> f;
cout << 'Введите последнее число : ';
cin >> l;
if (f > l)
{
t = f;
f = l;
l = t;
}
for (int i = f; i <= l; i++)
{count++;
sum += (double)i;
}
cout << 'Сумма чисел от ' << f << ' до ' << l << ' = '
<< sum << endl;
cout << ' Cреднее значение = ' << sum / count << endl;
return 0;}
3.15 Расчет суммы и среднего значения ряда целых чисел
Программа аналогична предыдущей, но в операторе цикла выражение 1 опускается и оператор цикла работает без установления начального значения переменной, управляющей циклом. Фактически, в качестве переменной цикла используется переменная f.
#include <iostream.h>
int main()
{
int count = 0;
double sum = 0.0;
int f, l, t;
cout << 'Введите первое число : ';
cin >> f;
cout << 'Введите последнее число : ';
cin >> l;
if (f > l)
{
t = f;
f = l;
l = t;
}
cout << 'Сумма чисел от ' << f << ' до ' << l << ' = ' ;
for (; f <= l;)
{count++;
sum += (double)f++;
}
cout << sum << endl;
cout << ' Cреднее значение = ' << sum / count << endl;
return 0;}
3.16 Использование оператора for для создания неопределенного цикла
#include <iostream.h>
#include <ctype.h>
int main()
{
char ch;
double x,y;
for(;;)
{
cout << 'Введите число : ';
cin >> x;
if (x !=0)
{
y=1/x;
cout << ' 1/' << x << '='<< y << endl;
cout << 'Продолжать вычисления? (Y/N) ';
cin >> ch;
if (toupper(ch) != 'Y')
break;
}
else
cout << 'Нельзя вводить 0 ' << endl ;
}
return 0;
}
3.17 Использование оператора цикла do while
Программа аналогична предыдущей, но вместо цикла for используется оператор цикла do while . Программа содержит внутренний цикл для проверки на нулевое значение, что на самом деле не очень удобно, так как при данном решении задачи после каждого числа приходится еще вводить и ноль. Более удобно в этом случае использовать оператор цикла while.
#include <iostream.h>
#include <ctype.h>
int main()
{
char ch;
double x,y;
do
{
do
{
cout << 'Введите число : ';
cin >> x;
if (x ==0 )
cout << 'Нельзя вводить 0 ' << endl;
} while ( x == 0 );
y=1/x;
cout << ' 1/' << x << '='<< y << endl;
cout << 'Продолжать вычисления? (Y/N) ';
cin >> ch;
} while (toupper(ch) != 'Y')
return 0;
}
3.18 Расчет суммы и среднего значения ряда целых чисел. Использование оператора цикла while
Программа аналогична примеру 3.2, но вместо цикла for используется оператор цикла while
#include <iostream.h>
int main ()
{
int count = 0;
double sum = 0.0;
int f, l, t;
cout << 'Введите первое число : ';
cin >> f;
cout << 'Введите последнее число : ';
cin >> l;
if (f > l)
{
t = f;
f = l;
l = t;
}
cout << 'Сумма чисел от ' << f << ' до ' << l << ' = ' ;
while ( f <= l;)
{
count++;
sum += (double)f++;
}
cout << sum << endl;
cout << ' Cреднее значение = ' << sum / count << endl;
return 0;}
3.19 Использование оператора contunue для пропуска итераций
#include <iostream.h>
#include <match.h>
int main()
{
for (int i = 0; i < 10; ++i)
{
if (i >=4 && i <=6)
continue;
cout << ' Шаг номер ' << i << endl;
}
return 0;
}
3.20 Работа с перечисляемыми типами данных
Тип данных matherror, содержит возможные варианты ошибок математических операций
noErr - нет ошибки
badOp - плохой оператор
divZero - деление на нуль
В программе используется переменная этого типа err
#include <iostream.h>
enum matherror { noErr, badOp, divZero };
int main()
{
double x, y, z;
char op;
matherror err = noErr;
cout << ' Введите число ,знак, число : ';
cin >> x >> op >> y ;
switch (op)
{
case '+':
z = x + y;
break;
case '-':
z = x - y;
break;
case '*':
z = x*y;
break;
case '/':
if (y != 0)
z = x/y;
else
err = divZero;
break;
default:
err = badOp;
break;
}
if (err == noErr)
cout << x << ' ' << op << ' ' << y << ' = ' << z << endl;
else
switch (err)
{
case noErr:
cout << ' нет ошибки ' << endl;
break;
case badOp:
cout << 'ошибка : неправильный оператор ' << endl;
break;
case divZero:
cout << 'ошибка : деление на ноль ' << endl;
break;
}
return 0;
}
Обратите внимание, что сообщение «нет ошибки», хотя и предусмотрено в программе, на самом деле никогда не появляется.
3.21 Работа со структурами. Вычисление площади прямоугольника
Программа содержит две структуры - координаты точки (x,y), и структура - содержащую точку-верхний левый угол прямоугольника, точку - нижний правый угол прямоугольника и его площадь.
#include<iostream.h>
#include<math.h>
struct point
{
double x;
double y;
};
struct rect
{
point ulc;
point lrc;
double area;
};
int main()
{
rect r;
double length, width;
cout << 'Введите х у - координаты верхнего левого угла ';
cin >> r.ulc.x >> r.ulc.y;
cout << 'Введите х у - координаты верхнего правого угла ';
cin >> r.lrc.x >> r.lrc.y;
length = fabs (r.ulc.x - r.lrc.x);
width = fabs (r.ulc.x - r.lrc.y);
r.area = length * width;
cout << 'Площадь = ' << r.area << endl;
return 0;
}
3.22 Работа со ссылочными переменными
#include<iostream.h>
int main()
{
int x = 10;
int& rx = x;
cout << “ x содержит ” << x << endl;
cout << “ x содержит (используется ссылка)” << rx << endl;
x *= 2;
cout << “ x содержит (используется ссылка)” << rx << endl;
rx *= 2;
cout << “ x содержит ” << x << endl;
return 0;}
3.23 Работа с указателями на действительные переменные
#include <iostream.h>
int main()
{
int x = 10;
int* px = &x;
cout << “ x содержит ” << x << endl;
cout << “ x содержит (используется указатель)” << *px << endl;
x *= 2;
cout << “ x содержит (используется указатель)” << *px << endl;
*px *= 2;
cout << “ x содержит ” << x << endl;
return 0;
}
3.24 Работа с массивами с использованием указателей
Вводится количество элементов массива и их значения. Определяются сумма и среднее значение. Указатель *realPtr указывает на ячейку с нулевым элементом массива.
#include <iostream.h>
const int MAX =30;
int main()
{
double x[MAX];
double *realPtr = x; // можно &x[0]
double sum = 0.0, mean;
int n, count;
do
{
cout << 'Введите количество данных от 2 до ' << MAX << ': ';
cin >> n;
cout << endl;
} while (n < 2 || n > MAX);
for (int i = 0; i < n; i++)
{
cout << 'X[' << i << ']: ';
cin >> *(x+i); // Запись данных в элемент x[i]
}
count = n;
for (i = 0; i < n; i++)
sum += *(realPtr+i);
mean = sum / count;
cout << endl << 'Среднее = ' << mean << endl ;
return 0;
}
3.25 Работа с массивами с использованием указателей. Использование метода инкремента/декремента указателя
#include <iostream.h>
const int MAX =30;
int main()
{
double x[MAX];
double *realPtr = x;
double sum = 0.0, mean;
int n, count;
do
{
cout << 'Введите количество данных от 2 до ' << MAX << ': ';
cin >> n;
cout << endl;
} while (n < 2 || n > MAX);
for (int i = 0; i < n; i++)
{
cout << 'X[' << i << ']: ';
cin >> *realPtr++;
}
realPtr -= n;
count = n;
for (i = 0; i < n; i++)
sum += *(realPtr++);
mean = sum / count;
cout << endl << 'Среднее = ' << mean << endl ;
return 0;
}
3.26 Обращение к элементам массива непосредственно через указатель
Во многом совпадает с предыдущей программой, отличие - обращение к элементам массива непосредственно через указатель.
В структуру rect добавляется новый элемент - порядковый номер прямоугольника.
Добавляется также новый тип данных rectArr - массив из прямоугольников.
При обращении к координатам прямоугольника вместо названия переменной структуры используется указатель с операцией ->.
#include<iostream.h>
#include<stdio.h>
#include<math.h>
const int MAX_RECT = 4;
struct point
{
double x;
double y;
};
struct rect
{
point ulc;
point lrc;
double area;
int id;
};
typedef rect rectArr[MAX_RECT];
int main()
{
rectArr r;
rect temp;
rect* pr = r;
rect* pr2;
double length, width;
for (int i = 0; i < MAX_RECT; i++,pr++)
{
cout << 'Введите х у - координаты верхнего левого угла прямоугольника '
<< i+1 << “: “ ;
cin >> pr->ulc.x >> pr->ulc.y;
cout << 'Введите х у - координаты верхнего правого угла прямоугольника '
<< i+1 << “: “ ;
cin >> pr->lrc.x >> pr->lrc.y;
pr->id = i;
length = fabs (pr->ulc.x - pr->lrc.x);
width = fabs (pr->ulc.x - pr->lrc.y);
r.area = length * width;
cout << 'Площадь прямоугольника ' << i << '= ' << pr->area << endl;
}
return 0;
}
3.27 Управление динамической памятью с помощью указателей
В заголовочном файле except.h объявлено исключение xalloc(описана ошибочная ситуация), которое возникает при неудаче операции динамического распределения памяти new.
#include <except.h>
#include <iostream.h>
const int MAX =30;
int main()
{
double* x;
double sum = 0.0, mean;
int *n, count;
try { n = new int ;}
catch (xalloc&) {return 1;}
do
{
cout << 'Введите количество данных от 2 до ' << MAX << ': ';
cin >> *n;
cout << endl;
} while (*n < 2 || *n > MAX);
try { n = new double [*n];}
catch (xalloc&)
{
delete n;
return 1;
}
for (int i = 0; i < *n; i++)
{
cout << 'X[' << i << ']: ';
cin >> *x[i];
}
count = *n;
for (i = 0; i < *n; i++)
sum += *(x + i);
mean = sum / count;
cout << endl << 'Среднее = ' << mean << endl ;
delete n;
delete [] x;
return 0;
}
3.28 Использование локальной переменной
#include<iostream.h>
void fun1 (int start)
{
int ix;
for (ix=start; ix < start +5; ++ix)
cout << ix << endl;
}
int main()
{
fun1 (23);
return 0;
}
3.29 Использование статической локальной переменной
#include<iostream.h>
double aver (double x)
{
static double count = 0;
static double sum = 0;
++count;
sum += x;
return sum / cout;
}
int main()
{
cout << “Среднее значение равно” << aver (1) << endl;
cout << “Среднее значение равно” << aver (2) << endl;
cout << “Среднее значение равно” << aver (4) << endl;
cout << “Среднее значение равно” << aver (10) << endl;
cout << “Среднее значение равно” << aver (11) << endl;
return 0;
}
3.30 Встроенная функция
#include<iostream.h>
inline double sqr (double x)
{
return x * x;
}
inline double cube (double x)
{
return x * x * x;
}
int main()
{
double x;
cout << “Введите число : ”;
cin >> x;
cout << “Квадрат от” << x << “ = ” << sqr (x) << endl;
cout << “Куб от” << x << “ = ” << cube (x) << endl;
return 0;
}
3.31 Использование аргументов по умолчанию
#include<iostream.h>
void mess (const char *msg, const char *name = ' Вася')
{
cout << name << ' говорит ' ' << msg << ' ' ' << endl;
}
int main()
{
mess (“Привет ”);
mess (“Здравствуй ”, “Петя”);
return 0;
}
3.32 Рекурсивная функция. Вычисление факториала
#include<iostream.h>
const int MIN = 1
const int MAX = 30
double factorial ( double x)
{
if ( f > 1)
return f * factorial ( f - 1 );
else
return f ;
}
int main()
{
int x;
do
{
cout << “Введите число между ” << MIN << “ и ” << MAX “ : ”;
cin >> x;
} while ( x < MIN || x > MAX);
cout << x << “! = “ << factorial (x) << endl;
return 0;
}
3.33 Перегрузка функции
Программа выполняет следующие действия:
объявляет переменные типов char, int, double и инициализирует их;
выводит начальные значения этих величин;
вызывает перегруженные функции, которые увеличивают значения этих величин;
выводит вычисленные значения.
#include<iostream.h>
void pfun (int &i) // версия для аргумента целого типа
{ i = i+1; }
void pfun (double &x) // версия для аргумента вещественного типа
{ i = i+1; }
void pfun (char &c) // версия для символьного аргумента
{ i = i+1; }
int main()
{
char c = `A';
int i = 10;
double x = 10.2;
cout << “c = ” << c << endl
<< “i = ” << i << endl
<< “x = ” << x << endl ;
pfun (c);
pfun (i);
pfun (x);
cout << “c = ” << c << endl
<< “i = ” << i << endl
<< “x = ” << x << endl;
return 0;}
3.34 Программа, иллюстрирующая использование класса
#include <iostream.h>
class rectangle
{
private:
double length;
double width;
public:
rectangle () |
{assign (0, 0); } |
|
rectangle(double Len, double Wide) |
{assign (Len, Wide);} |
|
double Length() |
{ return length; } |
|
double Width() |
{ return width; } |
|
double Area() |
{return length * width;} |
|
void assign(double Len, double Wide); |
||
}; |
void rectangle::assign(double Len, double Wide)
{
length = Len;
width = Wide;
}
int main()
{
rectangle rect;
double len, wide;
cout << 'Enter length of rectangle: ' ;
cin >> len;
cout << 'Enter width of rectangle: ';
cin >>wide;
rect.assign (len, wide);
cout << ' Rectanglelength = << rect.Length()<<endl
<<” width=”<<rect.Width()<<endl
<<” area=”<< rect.Area()<<endl;
return 0;
}
В программе объявляется класс rectangle, моделирующий прямоугольник. Класс имеет два элемента данных типа double - length и width, определяющих размеры прямоугольника, длину и ширину соответственно. Кроме того, класс имеет два конструктора, один из которых является конструктором по умолчанию. В классе определяются функции-элементы: Length, Width, Area, assing.
Конструктор по умолчанию не имеет параметров (или все его параметры должны иметь значения по умолчанию) и вызывается при создании представителя класса, которому не заданы аргументы. Если при объявлении представителя класса указываются параметры, то в этом случае вызывается соответствующий конструктор, а не конструктор по умолчанию.
Функция Length, определенная в объявлении класса, просто возвращает значение элемента данных length. Функция Width, также определенная в объявлении класса, возвращает значение элемента width. Функция Area, шределенная в объявлении класса, просто возвращает значение произведения элементов length и width.
Функция assign, определенная вне объявления класса, присваивает значения своих параметров Len и Wide элементам-данным length и width соответственно.
В функции main определяются экземпляр rect класса rectangle и переменные len и wide типа double. Заметьте, что в этом примере при объявлении данных им не присваиваются никакие начальные значения, но вы могли бы присвоить значения длине и ширине прямоугольника, если бы объявили rect таким образом: rectangle rect(10, 20);
Такое объявление объекта класса приведет к вызову конструктора
rectangle::rectangle(double, double), а не конструктора rectangle::rectangle().
В результате, данные-элементы length и width получат соответствующие значения. Операторы ввода/вывода предлагают ввести значения и помещают ввод в переменные len и wide. Затем функция main использует эти переменные при вызове функции-элемента assign. Оператор вывода выводит значения Length, Width и Area для объекта rect.
Список рекомендуемой литературы
1. Иванова Г.С. Программирование на СИ++ /Учебник. Изд. МГТУ им. В.Н. Баумана, 2002 г.
2. Страуструп Б. Язык программирования СИ++/ Пер с англ.- М.:Радио и связь, 2004.
3. Теренс Чан Системное программирование на СИ++ для Unix./Под ред. Коломыцева, Киев, 2002.
Приложение
Математические библиотечные функции
Функция |
Краткое описание |
|
Abs |
Нахождение абсолютного значения выражения типа int |
|
Acos |
Вычисление арккосинуса |
|
Asin |
Вычисление арксинуса |
|
Atan |
Вычисление арктангенса x |
|
Atan2 |
Вычисление арктангенса y/x |
|
Cabs |
Нахождение абсолютного значения комплексного числа |
|
Ceil |
Нахождение наименьшего целого, большего или равного x |
|
Cos |
Вычисление косинуса |
|
Exp |
Вычисление функции экспоненты |
|
Fabs |
Нахождение абсолютного значения типа double |
|
Floop |
Нахождение наибольшего целого, меньшего или равного x |
|
Fmod |
Нахождение остатка от деления x/y |
|
Hypot |
Вычисление гипотенузы |
|
Labs |
Вычисление абсолютного значения типа long |
|
Log |
Вычисление натурального логарифма |
|
Log10 |
Вычисление логарифма по основанию 10 |
|
Modf |
Разложение x на дробную и целую часть |
|
Pow |
Вычисление x в степени y |
|
Sin |
Вычисление синуса |
|
Sgrt |
Вычисление квадратного корня |
|
Tan |
Вычисление тангенса |