C++. Бархатный путь. Часть 1
ОГЛАВЛЕНИЕ
При работе над книгой, как и при подготовке учебного курса, автор предполагал у читателей и слушателей наличие элементарных понятий и представлений из области информатики, а также навыков программирования. Это предположение позволило сосредоточиться на основной задаче книги: формировании чётких знаний о языке программирования C++, его синтаксисе и семантике. С этой целью при описании грамматики языка в книге широко используются формы Бэкуса-Наура, и уделяется большое внимание обсуждению причин и следствий введения в язык тех или иных языковых конструкций.
Одной из особенностей книги является практически полное отсутствие примеров, связанных с реализацией конкретных содержательных алгоритмов. Изучение языка программирования и обучение программированию на языке различаются примерно так же, как изучение грамматики иностранного языка и овладение навыками разговорной речи на этом языке.
Изучение иностранного языка не требует детального анализа произведений художественной литературы. Как правило, в книгах, посвящённых описанию языков программирования, в качестве иллюстраций приводятся примеры не сложнее, чем реализация алгоритма пузырьковой сортировки. Более сложные примеры отвлекают от основного предмета изучения. К тому же, даже самые простые содержательные примеры в книге о языке программирования неизбежно ограничивают "поле зрения" читателей, оставляя за пределами их внимания множество безупречных, с точки зрения синтаксиса языка программирования, предложений. К таким предложениям можно отнести, например, не содержащие помеченных операторов или содержащие всего один простой оператор операторы выбора:
switch (i) ; // Синтаксически правильный оператор выбора…
switch (j) {} // Ещё один правильный оператор…
switch (val1) default: x++;
объявления пустых безымянных классов:
class {};
class {};
полную форму вызова операторной функции:
ctVal2.operator = (ctVal1); /* Вариант полной формы вызова операторной функции присваивания для двух комплексных чисел. */
Все эти операторы C++, несмотря на весьма странный вид, остаются абсолютно корректными предложениями. Они позволяют рельефно и наглядно выявлять принципиальные особенности языка. Такие операторы часто приводятся в качестве примеров в этой книге.
Профессиональное программирование, как и грамотная речь, невозможны без досконального знания языка программирования. Недоумение по поводу "непривычных для пользователей" операторов языка, а также вопросы типа "Что это такое?" или "Зачем это нужно?", которые обычно возникают по причине незнания, либо непонимания особенностей языка, исчезают сами собой по мере погружения в этот язык. Обеспечение перехода от навыков использования фиксированного подмножества языка к знанию и сознательному применению всех выразительных возможностей языка C++ является основной задачей предлагаемой книги. В методах, которые использует автор для решения этой задачи, заключается основное отличие этой книги от других книг, посвящённых описанию языков программирования.
В настоящее время объём рукописи составляет 472 страницы формата A4. Степень готовности рукописи составляет 95% (возможны незначительные дополнения и исправления).
Книга "C++. Бархатный путь" может быть использована в качестве учебного пособия для учащихся старших классов и студентов младших курсов ВУЗов, однако это чисто условная адресация, т. к. учиться никогда не поздно. Смысл названия книги раскрывается в предисловии. Простая структура книги определяется её содержанием, поскольку при описании основ языка прежде всего важна логическая последовательность изложения материала.
Сведения об авторе:
Марченко Антон Леонардович, 1956 г.р.
Образование высшее:
1980г. - Философский факультет МГУ им. М.В. Ломоносова (кафедра логики),
1986г. - факультет прикладной математики Московского Института Электронного Машиностроения,
До 1993 года - программист в НИИ,
с 1993 года - преподаватель МГУ им М.В. Ломоносова и Лицее Информационных Технологий, кандидат физ.-мат. наук.
Введение
В одном энциклопедическом словаре по поводу того, что такое язык, сказано буквально следующее:
"ЯЗЫК, 1) естественный язык, важнейшее средство человеческого общения. Я. неразрывно связан с мышлением; является социальным средством хранения и передачи информации, одним из средств управления человеческим поведением. Я. возник одновременно с возникновением общества в процессе совместной трудовой деятельности первобытных людей. Возникновение членораздельной речи явилось мощным средством дальнейшего развития человека, общества и сознания. Реализуется и существует в речи. Я. мира различаются строением, словарным составом и др., однако всем Я. присущи некоторые общие закономерности, системная организация единиц языка (например, парадигматические и синтагматические отношения между ними) и др. Я. изменяется во времени (см. Диахрония), может перестать использоваться в сфере общения (мёртвые Я.). Разновидности Я. (нац. Я., лит. Я., диалекты, Я. культа и др.) играют различную роль в жизни общества. 2) Любая знаковая система, напр. Я. математики, кино, Я. жестов. См. также Искусственные языки, Язык программирования. 3)…"
C++ также является языком. Его так и называют "язык программирования C++". Это формальный язык. Он служит для описания данных и алгоритмов их обработки на ЭВМ.
Несмотря на огромную разницу между естественными и формальными языками, у них есть много общего. Их общие черты неожиданно проявляются в самых разных областях.
Например, изучение естественного языка является сложным процессом, включающим как обретение элементарных автоматических навыков, так и восприятие сложных абстрактных понятий. При этом возможность относительно свободного использования языка как средства общения появляется уже на ранних стадиях этого процесса, когда вообще ещё не имеет смысла говорить о знании языка. Так, подавляющее большинство населения любого крупного города общается между собой, используя разговорный язык той страны или той местности, в которой расположен этот город. Практически все, кто проживает в городе, свободно владеет разговорным языком, а вернее, навыками разговорной речи. При этом лишь незначительная часть жителей действительно знает этот язык.
Аналогичная ситуация наблюдается и с языками программирования. Первые опыты программирования, подобно использованию навыков разговорной речи, не требуют особых познаний в области формальных языков. Для составления работающих программ достаточно иметь интуитивные представления об алгоритмах и устройстве компьютера. Часто бывает достаточно ознакомиться с несколькими работающими программами или даже с фрагментами таких программ, чтобы, в буквальном смысле используя образцы, успешно описывать собственные алгоритмы.
Однако грамотная речь невозможна без знания языка, а профессиональное программирование требует глубоких знаний в области языков программирования.
Книга "C++. Бархатный путь" адресована, прежде всего, тем, кто уже имеет навыки в области программирования, кто уже знает, что такое алгоритм, компьютер, редактор и транслятор, кто, возможно, уже наблюдал появление на экране дисплея волнующей надписи "Hello, world!" - учащимся старших классов и студентам младших курсов. Подобная адресация чисто условна, поскольку учиться, как известно, никогда не бывает поздно.
Эта книга о языке программирования C++. В ней на основе базовых языковых конструкций и элементарных понятий описываются всё более сложные элементы языка и связанные с ними концепции:
- алфавит и идентификаторы
- элементарные типы, объявления и пределения
- операции, выражения и операторы
- функции и их характеристики
- производные типы и средства их построения
- функции-члены, конструкторы и операторные функции
- механизмы наследования, инкапсуляции и полиморфизма
- шаблоны
- потоки ввода-вывода
- средства обработки исключительных ситуаций.
В книгу также включено несколько приложений:
- грамматика языка C++;
- информация о контроле типов, типизации и прочих важных характеристиках языков программирования;
- сведения об алгоритмах преобразования, дополнительном коде и о преобразовании дробной части вещественного числа. Эти приложения включены в книгу с единственной целью: чтобы при обсуждении того, ЧТО ДЕЛАЕТСЯ, не возникало вопросов по поводу того, КАК ЭТО ДЕЛАЕТСЯ;
- элементы теории комплексных чисел. Комплексные числа - наш полигон. Многие конструкции языка C++ мы в дальнейшем будем отрабатывать применительно к множеству комплексных чисел;
- элементарные сведения об устройстве ЭВМ.
"Бархатный путь" не является учебником по программированию и поэтому в этой книге нет полезных советов, которые позволяли бы с помощью обсуждаемых языковых конструкций решать конкретные прикладные задачи. Большинство приводимых здесь примеров, на первый взгляд, кажутся бесполезными и даже бессмысленными. Однако выбор этих примеров далеко не случаен.
Дело в том, что C++ является языком "общения" человека с компьютером. Основным "читателем" текстов на языке C++ является транслятор. Это особая программа, в обязанности которой входит проверка правильности текста программы и его последующий перевод на язык процессора - основного устройства ЭВМ, который и обеспечивает выполнение программы. У процессора свой взгляд на программу. Он не имеет никакого представления о содержательной стороне описываемых алгоритмов. Процессору важны адреса, регистры, прерывания.
Язык программирования позволяет описывать алгоритмы и данные. Однако его выразительные возможности не исчерпываются множеством содержательных алгоритмов и связанных с ними структур данных. Даже самые абсурдные с точки зрения программиста, реализующего сколько-нибудь значимый алгоритм, предложения языка остаются абсолютно правильными и корректными для транслятора. Примеры, основанные на содержательных алгоритмах, неизбежно оставляют за рамками изложения множества предложений, на которых, порой, и выявляются характерные черты языка.
Несколько слов о названии книги. Понятие бархатного пути связано с железной дорогой. Бархатный путь - это высокое качество железнодорожного полотна и мастерство вождения локомотива. Бархатный путь предполагает мягкое взятие состава с места, его плавный разгон и ведение поезда без толчков, качки, лязга и скрежета. Путешествие по железной дороге - не самый лучший способ знакомства с местностью, по которой проложена эта самая дорога. Из окна вагона можно не заметить даже очень крутого поворота пути, а узкая полоска защитных насаждений вдоль железнодорожных путей порой кажется густым лесом. Проезжая через населённые пункты, часто удаётся разглядеть лишь заборы и привокзальные постройки. Так и страницы книги о языке программирования - не самое лучшее место для описания алгоритмов.
Однако поездка по железной дороге на дрезине, вне расписания, с остановками у переездов и мостов, стрелок, семафоров, поворотных кругов, горок и замедлителей является лучшим способом ознакомиться с устройством этой самой дороги. Такое путешествие - прекрасная возможность всё рассмотреть, потрогать и покрутить.
При работе над книгой использовался компилятор, входящий в состав интегрированной среды разработки приложений Borland C++ 4.5. Его следует воспринимать исключительно как простое средство передвижения по "бархатному пути" - своего рода hand car. Выбор транслятора для этой книги абсолютно не принципиален. Следует лишь иметь в виду, что выполнение примеров, которые приводятся в этой книге, в других инструментальных средах в ряде случаев может привести к иным результатам. Это связано с тем, что многие свойства языка C++ (размеры данных основных типов, способы их размещения в различных сегментах памяти и т.д.) зависят от конкретной реализации языка. Однако это нисколько не противоречит тому, о чём говорится в книге. Впрочем, везде, где это необходимо, в тексте содержатся соответствующие предупреждения.
C++ - это сложный, логически стройный и красивый язык. Его хорошее знание приводит к мастерскому владению этим языком. И здесь уже будет по силам решение любой задачи.
Язык и грамматика
Формальный язык является объединением нескольких множеств:
- множества исходных символов, называемых литерами (алфавит),
- множества правил, которые позволяют строить из букв алфавита новые слова (правила порождения слов или идентификаторов),
- множества предопределённых идентификаторов или словаря ключевых слов (прочие идентификаторы называются именами),
- множества правил, которые позволяют собирать из имён и ключевых слов выражения, на основе которых строятся простые и сложные предложения (правила порождения операторов или предложений).
Множество правил порождения слов, выражений и предложений называют грамматикой формального языка или формальной грамматикой.
У формального языка много общего с естественным языком, предложения которого также строятся в соответствии с грамматическими правилами. Однако грамматика естественного языка, подобно наукам о природе с известной степенью достоверности описывает и обобщает результаты наблюдений за естественным языком как за явлением окружающего мира. Характерные для грамматики естественных языков исключения из правил свидетельствуют о том, что зафиксированная в грамматике языка система правил не может в точности описать все закономерности развития языка.
Формальные языки проще естественных языков. Они создаются одновременно с системой правил построения слов и предложений. Исключения из правил в формальном языке могут свидетельствовать лишь о противоречивости и некорректности системы грамматических правил.
Однако и здесь не всё так просто. В языке программирования C++ существуют так называемые дополнительные специальные правила соотнесения (соотнесения имени и его области действия - скоро мы встретимся с этими правилами). Так вот эти правила (а, может быть, соглашения?) вполне можно рассматривать как аналоги исключений, поскольку они директивно (по соглашению) отдают предпочтение одной из возможных альтернатив.
Грамматические правила можно записывать различными способами. Грамматика естественного языка традиционно описывается в виде грамматических правил на естественном языке.
Грамматика формального языка также может быть описана в виде множества правил на естественном языке. Но обычно для этого используют специальные средства записи: формулы и схемы. В качестве примера рассмотрим простой формальный язык.
Алфавит этого языка состоит из 17 букв:
А Б Е З И Й К Н О П Р С Т У Ч Ш Ы
и одного знака пунктуации - '.' (точки).
Рассмотрим систему правил, составляющих грамматику языка.
- Правила словообразования (мы не будем вдаваться в их подробное описание) позволяют сформировать из букв языка 5 различных идентификаторов (имён и ключевых слов):
КУБ
ШАР
ПРОЗРАЧНЫЙ
СИНИЙ
УКРАШАЕТ
и ни одним идентификатором больше. - Идентификаторы КУБ и ШАР считаются именами, прочие идентификаторы считаются ключевыми словами.
По весьма отдалённой аналогии с естественным языком, ключевые слова будут играть роли членов предложения и частей речи.
- Определение сказуемого (это член предложения): ключевое слово УКРАШАЕТ будем считать сказуемым.
- Определение прилагательного (это часть речи): ключевые слова ПРОЗРАЧНЫЙ и СИНИЙ будем считать прилагательными.
- Имена играют роль существительных.
По аналогии с естественным языком, где предложения строятся из членов предложений, предложения-операторы языка состоят из членов предложений-выражений. Часть выражений считается подлежащими, часть - дополнениями.
- Определение подлежащего: выражения-подлежащие состоят из ключевого слова-прилагательного и имени.
- Определение дополнения: выражения-дополнения состоят из ключевого слова-прилагательного и имени (одного из двух).
- Определение оператора (это последнее правило грамматики): предложение состоит из тройки выражений, самым первым из которых является подлежащее, затем сказуемое и дополнение. Предложение заканчивается точкой.
Только что нами была определена грамматика формального языка. Она была описана привычным способом, с помощью нескольких предложений русского языка.
Рассмотрим ещё один способ записи этой грамматики - с помощью формул. Запишем сначала в виде формулы определение оператора:
оператор ::= подлежащее сказуемое дополнение . (1)
В этой формуле символ ::= следует читать как "является" или "заменить".
Затем определим в виде формул подлежащее и дополнение:
подлежащее ::= прилагательное существительное (2)
дополнение ::= прилагательное существительное (3)
Следующая формула отражает тот факт, что сказуемым является ключевое слово УКРАШАЕТ.
сказуемое ::= УКРАШАЕТ (4)
Следующее правило определяет прилагательное:
прилагательное ::= ПРОЗРАЧНЫЙ | СИНИЙ (5)
Здесь вертикальная черта между двумя ключевыми словами означает, альтернативу (прилагательным в выражении может быть либо ключевое слово ПРОЗРАЧНЫЙ, либо ключевое слово СИНИЙ). Существует еще, по крайней мере, один способ описания альтернативы. Воспользуемся им при определении существительного. Это правило задаёт множество имён:
существительное ::= ШАР (6)
::= КУБ
Правила построения предложений в нашем языке оказались записаны с помощью шести коротких формул. Слова, стоящие справа и слева от знака "заменить" принято называть символами формальной грамматики, а сами формулы - грамматическими правилами.
Заметим, что символы в формулах грамматики не являются словами в обычном смысле этого слова. Символ в формуле является лишь своеобразным иероглифом, по внешнему виду напоминающим слово. При изменении внешнего вида символов суть формул грамматики нисколько бы не изменилась. Мы всего лишь используем возможность кодирования дополнительной информации с помощью внешнего вида символа. В надежде, что это поможет лучше понять происходящее.
Символы, которые встречаются только в левой части правил, называются начальными нетерминальными символами или начальными нетерминалами.
Символы, которые встречаются как в левой, так и в правой части грамматических правил называются нетерминальными символами.
Символы, которые встречаются только в правой части правил, называются терминальными символами.
Воспользуемся этой грамматикой и построим несколько предложений.
Алгоритм порождения операторов-предложений и отдельных выражений с помощью правил формальной грамматики очень прост:
- Выбрать начальный нетерминал (оператор) или отдельный нетерминальный символ, найти правило, содержащее этот символ в левой части и заменить его на символ или на последовательность символов из правой части правила.
- Процесс замены продолжать до тех пор, пока в предложении будут встречаться нетерминальные символы.
Выбор нетерминального символа обеспечивает порождение выражения, выбор начального нетерминала обеспечивает вывод оператора:
оператор (1)
подлежащее сказуемое дополнение . (2)
прилагательное существительное сказуемое дополнение . (3)
прилагательное существительное сказуемое прилагательное
существительное. (4)
прилагательное существительное УКРАШАЕТ прилагательное
существительное. (5)
ПРОЗРАЧНЫЙ существительное УКРАШАЕТ СИНИЙ
существительное. (6)
ПРОЗРАЧНЫЙ ШАР УКРАШАЕТ СИНИЙ КУБ.
Больше терминальных символов нет. По правилам формальной грамматики мы построили первое предложение языка.
СИНИЙ КУБ УКРАШАЕТ ПРОЗРАЧНЫЙ КУБ.
Это ещё одно предложение нашего языка.
Формальная грамматика может использоваться не только для порождения предложений, но и для проверки, является ли какая-либо последовательность символов выражением языка. Для этого среди символов исследуемой последовательности надо сначала отыскать терминальные символы и применяя правила формальной грамматики, справа налево заменять терминальные символы нетерминальными, а затем "сворачивать" последовательности нетерминальных символов до тех пор, пока не будет получен начальный нетерминал, или окажется единственный нетерминальный символ.
Так последовательность символов
СИНИЙ КУБ ВЕНЧАЕТ ПРОЗРАЧНЫЙ КУБ.
не является оператором языка, поскольку символ ВЕНЧАЕТ не встречается среди нетерминальных символов. В свою очередь, пара терминальных символов СИНИЙ ШАР является выражением нашего языка и может быть как подлежащим, так и дополнением, поскольку может быть преобразована как в нетерминальный символ подлежащее, так и в нетерминальный символ дополнение.
Рассмотренный нами способ записи правил грамматики языка называется формой Бэкуса-Наура (сокращенно БНФ). Зачем они и что, собственно, с ними делать?
Не бояться их. Смотреть на них. Читать их. Символы формальной грамматики складываются в основном из букв родного алфавита. Формулы кратки и информативны. Правила, для изложения которых обычно требуется несколько фраз естественного языка, часто описываются одной формой Бэкуса-Наура. После небольшой тренировки чтение этих форм становится лёгким и приятным занятием.
Впервые БНФ были использованы при описании языка программирования Алгол более 30 лет назад и до сих пор БНФ применяются для описания грамматики при разработке новых языков программирования. Это очень эффективное и мощное средство. Без лишних слов, просто, лаконично, наглядно.
Мы часто будем использовать эти формы. При этом нетерминальные символы в БНФ будут выделяться подчёркиванием.
Подобно предложениям естественного языка, которые обычно служат основой связного повествования (сказки, романа, научного исследования), предложения формального языка также могут быть использованы для описания всевозможных явлений и процессов. Множества операторов языка программирования служат для создания программ - основного жанра произведений, для которых и используются эти языки. Программы пишут для различных программируемых устройств. К их числу относятся и электронно-вычислительные машины, которые в настоящее время являются наиболее универсальными вычислительными устройствами и основными потребителями программ.
Устройство современных ЭВМ основано на принципах двоичной арифметики, где для представления чисел используются всего две цифры - 0 и 1. В двоичной арифметике любое число кодируется битовыми последовательностями. Вся необходимая для работы ЭВМ информация также хранится в памяти ЭВМ в битовом представлении.
Особенности устройства ЭВМ определяют способы её управления. Командами для управления ЭВМ служат всё те же битовые последовательности. Поэтому наиболее естественным способом управления ЭВМ является кодирование информации для ЭВМ в виде всё тех же битовых последовательностей. Для первых ЭВМ альтернативных способов управления просто не существовало. Алфавит языка непосредственного кодирования содержал всего две буквы (а, может быть, цифры?). Можно представить правила словообразования и внешний вид словаря этого языка. Программирование в кодах ЭВМ требует досконального знания системы команд машины и большого внимания. Кроме того, процесс программирования в кодах малоэффективен. Проблема повышения эффективности программирования возникла одновременно (а может и раньше) с появлением первых действующих вычислительных машин.
Первая попытка оптимизации программирования в двоичных кодах заключалась в разработке специальной системы кодирования двоичных машинных команд многобуквенными мнемоническими сокращениями.
Программирование в мнемонических командах удобнее для программиста, поскольку мнемонические коды содержат для программиста дополнительную информацию по сравнению с трудно различимыми последовательностями нулей и единиц. Вместе с тем текст подобной программы становится абсолютно непонятным вычислительной машине и требует специальной программы-переводчика (или транслятора), которая бы заменяла мнемонический код исходной двоичной командой. С момента реализации этой идеи кодирование становится программированием.
Языки, которые требуют предварительного перевода, называются языками высокого уровня. Считается, что эти языки в определённом смысле более близки к естественному языку. С последним утверждением можно не согласится, но одно очевидно: многолетний опыт показал, что использование языков высокого уровня значительно повышает эффективность программирования по сравнению с обычным кодированием.
Следующим шагом в развитии языков программирования явилась реализация возможности построения большой программы из отдельных фрагментов программного кода. С этой целью используются подпрограммы. Это последовательности команд, предназначенные для многократного использования в одной программе. Программирование с использованием подпрограмм требует ещё одной специальной программы, которая обеспечивает сборку единой программы из отдельных фрагментов-подпрограмм и её размещение в памяти ЭВМ. Эта программа называется компоновщиком.
Транслятор и компоновщик
Программа - это последовательность инструкций, предназначенных для выполнения компьютером. В настоящее время программы оформляются в виде текста, который записывается в файлы. Этот текст является результатом деятельности программиста и, несмотря на специфику формального языка, остаётся программой для программиста.
Процесс создания программы предполагает несколько этапов. За этапом разработки проекта программы следует этап программирования. На этом этапе пишется программа. Программистами этот текст воспринимается легче двоичного кода, поскольку различные мнемонические сокращения и имена заключают дополнительную информацию.
Файл с исходным текстом программы (его также называют исходным модулем) обрабатывается транслятором, который осуществляет перевод программы с языка программирования в понятную машине последовательность кодов. Процесс трансляции разделяется на несколько этапов.
На первом этапе исходный текст (он обычно хранится в виде текстового файла) подвергается лексической обработке. Программа разделяется на предложения, предложение делится на элементарные составляющие (лексемы). Каждая лексема распознаётся (имя, ключевое слово, литерал, символ операции или разделитель) и преобразуется в соответствующее двоичное представление. Этот этап работы транслятора называют лексическим анализом.
Затем наступает этап синтаксического анализа. На этом этапе из лексем собираются выражения, а из выражений - операторы. В ходе трансляции последовательности терминальных символов преобразуются в нетерминалы. Невозможность достижения очередного нетерминала является признаком синтаксической ошибки в тексте исходной программы.
После синтаксического анализа наступает этап поэтапной генерации кода. На этом этапе происходит замена операторов языка высокого уровня инструкциями ассемблера, а затем последовательностями машинных команд. Результат преобразования исходного текста программы записывается в виде двоичного файла (его называют объектным модулем) с расширением ".obj".
Системы программирования, реализующие язык программирования C++, предусматривают стандартные приёмы и средства, которые делают процесс программирования более технологичным, а саму программу более лёгкой для восприятия.
К числу таких средств относится система поддержки многомодульных программ, которые строятся из отдельных фрагментов. Модули располагаются в различных файлах, часть из которых может быть независимо от других обработана транслятором. На этапе сборки часть модулей может быть собрана в так называемые загрузочные модули, которые и выполняются процессором.
Процесс разработки многомодульных программ эффективнее, особенно если разрабатывается программа большого размера, когда над реализацией проекта может работать несколько программистов, каждый из которых имеет возможность модифицировать фрагменты программы, не мешая работе остальных.
В C++ не существует специальных языковых конструкций, которые непосредственно в программе описывали бы общую структуру многомодульной программы. Обычно структура программы описывается специальными неязыковыми средствами и зависит от конкретной реализации системы программирования. Межмодульные связи поддерживаются специальными файлами проектов, в которых и фиксируется вся необходимая для создания многомодульной программы информация.
Объектный модуль можно выполнять лишь после специальной дополнительной обработки (компоновки), которая осуществляется специальной программой-компоновщиком.
Рассмотрим в общих чертах процесс компоновки. Программа строится из инструкций и операторов. В свою очередь, операторы включают выражения, которые состоят из операций и операндов. По крайней мере, части операндов в выражениях должны соответствовать отдельные "участки" оперативной памяти, предназначаемые, например, для сохранения результатов вычислений.
В ходе трансляции устанавливается соответствие между операндами и адресами областей памяти вычислительной машины. Так вот задача компоновщика состоит в согласовании адресов во всех фрагментах кода, из которых собирается готовая к выполнению программа. Компоновщик отвечает за то, чтобы конкретному операнду выражения соответствовала определённая область памяти.
Компоновщик также добавляет к компонуемой программе коды так называемых библиотечных функций (они обеспечивают выполнение конкретных действий - вычисления, вывод информации на экран дисплея и т.д.), а также код, обеспечивающий размещение программы в памяти, её корректное начало и завершение.
Преобразованная компоновщиком программа называется загрузочным или выполнимым модулем. Файлы, содержащие загрузочные модули, называют загрузочными или выполнимыми файлами.
Библиотеки
Языки программирования предназначены для написания программ. Однако было бы странно писать всякий раз одни и те же программы или даже одни и те же подпрограммы (например, подпрограмму вывода информации на дисплей или на принтер - эта подпрограмма требуется практически в каждой программе).
К счастью, проблема многократного использования программного кода уже очень давно и успешно решена.
Практически каждая система, реализующая тот или иной язык программирования (транслятор, компоновщик и прочее программное окружение) имеет набор готовых к использованию фрагментов программного кода. Этот код может находиться в разной степени готовности. Это могут быть фрагменты текстов программ, но, как правило, это объектный код, располагаемый в особых файлах. Такие файлы называются библиотечными файлами.
Для использования библиотечного кода программисту бывает достаточно указать в программе требуемый файл и обеспечить вызов соответствующих функций. Для использования библиотечного кода бывает достаточно стандартного набора языковых средств. Решение всех остальных проблем транслятор и компоновщик берут на себя. Разумеется, программисту должно быть известно о существовании подобных библиотек и о содержании библиотечных файлов.
Алфавит C++
Алфавит (или множество литер) языка программирования C++ основывается на множестве символов таблицы кодов ASCII. Алфавит C++ включает:
- строчные и прописные буквы латинского алфавита (мы их будем называть буквами),
- цифры от 0 до 9 (назовём их буквами-цифрами),
- символ '_' (подчерк - также считается буквой),
- набор специальных символов:
" { } , | [ ] + - % / \ ; ' : ? < > = ! & # ~ ^ . * - прочие символы.
Алфавит C++ служит для построения слов, которые в C++ называются лексемами. Различают пять типов лексем:
- идентификаторы,
- ключевые слова,
- знаки (символы) операций,
- литералы,
- разделители.
Почти все типы лексем (кроме ключевых слов и идентификаторов) имеют собственные правила словообразования, включая собственные подмножества алфавита.
Лексемы разделяются разделителями. Этой же цели служит множество пробельных символов, к числу которых относятся пробел, символы горизонтальной и вертикальной табуляции, символ новой строки, перевода формата и комментарии.
Правила образования идентификаторов
Рассмотрим правила построения идентификаторов из букв алфавита (в C++ три):
- Первым символом идентификатора C++ может быть только буква.
- Следующими символами идентификатора могут быть буквы, буквы-цифры и буквы-подчерки.
- Длина идентификатора неограниченна (фактически же длина зависит от реализации системы программирования).
Вопреки правилам словообразования в C++ существуют ограничения относительно использования подчерка в качестве самой первой буквы в идентификаторах. Особенности реализации делают нежелательными для использования идентификаторы, которые начинаются с этого символа.
Ключевые слова и имена
Часть идентификаторов C++ входит в фиксированный словарь ключевых слов. Эти идентификаторы образуют подмножество ключевых слов (они так и называются ключевыми словами). Прочие идентификаторы после специального объявления становятся именами. Имена служат для обозначения переменных, типов данных, функций и меток. Обо всём этом позже.
Ниже приводится список ключевых слов:
asm auto break case catch char class const continue default do double else enum extern float for friend goto if inline int long new operator private protected public register return short signed sizeof static struct switch template this throw try typedef typeid union unsigned virtual void volatile while.
Символы операций и разделителей
Множество лексем, соответствующее множеству символов операций и разделителей строится на основе набора специальных символов и букв(!) алфавита. Единственное правило словообразования для этих категорий лексем заключается в задании фиксированного множества символов операций и разделителей.
Слеующие последовательности специальных символов и букв алфавита образуют множество символов операций (часть из них в зависимости от контекста может быть использована в качестве разделителей):
, | ! | != | | | |= | % | %= | & |
&& | &= | () | * | *= | + | ++ | += |
- | -- | -= | -> | ->* | . | .* | / |
/= | :: | < | << | <= | <<= | > | >> |
>= | >>= | == | ?: | [] | ^ | ^= | ~ |
|| | # | ## | sizeof | new | delete | typeid | throw |
Кроме того, к числу разделителей относятся следующие последовательности специальных символов:
... | ; | {} |
Литералы
В C++ существует четыре типа литералов:
- целочисленный литерал,
- вещественный литерал,
- символьный литерал,
- строковый литерал.
Это особая категория слов языка. Для каждого подмножества литералов испольльзуются собственные правила словообразования. Мы не будем приводить здесь эти правила. Ограничимся лишь общим описанием структуры и назначения каждого подмножества литералов. После этого правила станут более-менее понятны.
Целочисленный литерал служит для записи целочисленных значений и является соответствующей последовательностью цифр (возможно со знаком '-'). Целочисленный литерал, начинающийся с 0, воспринимается как восьмеричное целое. В этом случае цифры 8 и 9 не должны встречаться среди составляющих литерал символов. Целочисленный литерал, начинающийся с 0x или 0X, воспринимается как шестнадцатеричное целое. В этом случае целочисленный литерал может включать символы от A или a, до F или f, которые в шестнадцатеричной системе эквивалентны десятичным значениям от 10 до 15. Непосредственно за литералом может располагаться в произвольном сочетании один или два специальных суффикса: U (или u) и L (или l).
Вещественный литерал служит для отображения вещественных значений. Он фиксирует запись соответствующего значения в обычной десятичной или научной нотациях. В научной нотации мантисса отделяется от порядка литерой E или e). Непосредственно за литералом могут располагаться один из двух специальных суффиксов: F (или f) и L или l).
Значением символьного литерала является соответствующее значения ASCII кода (это, разумеется, не только буквы, буквы-цифры или специальные символы алфавита C++). Символьный литерал представляет собой последовательность из одной или нескольких литер, заключённых в одинарные кавычки. Символьный литерал служит для представления литер в одном из форматов представления. Например, литера Z может быть представлена литералом 'Z', а также литералами '\132' и '\x5A'. Любая литера может быть представлена в нескольких форматах представления: обычном, восьмеричном и шестнадцатеричном. Допустимый диапазон для обозначения символьных литералов в восьмеричном представлении ограничен восьмеричными числами от 0 до 377. Допустимый диапазон для обозначения символьных литералов в шестнадцатеричном представлении ограничен шестнадцатеричными числами от 0x0 до 0xFF. Литеры, которые используются в качестве служебных символов при организации формата представления или не имеют графического представления, могут быть представлены с помощью ещё одного специального формата. Ниже приводится список литер, которые представляются в этом формате. К их числу относятся литеры, не имеющие графического представления, а также литеры, которые используются при организации структуры форматов.
Список литер организован по следующему принципу: сначала приводится представление литеры в специальном формате, затем - эквивалентное представление в шестнадцатеричном формате, далее - обозначение или название литеры, за которым приводится краткое описание реакции на литеру (смысл литеры).
\0 \x00 null пустая литера
\a \x07 bel сигнал
\b \x08 bs возврат на шаг
\f \x0C ff перевод страницы
\n \x0A lf перевод строки
\r \x0D cr возврат каретки
\t \x09 ht горизонтальная табуляция
\v \x0B vt вертикальная табуляция
\\ \x5C \ обратная косая черта
\' \x27 '
\" \x22 "
\? \x3F ?
Структура предложения C++
Предложения в C++ называются операторами. Подобно тому, как в естественном языке предложение строится из различных частей предложения и даже отдельных предложений (сложные предложения), оператор C++ состоит из выражений и может содержать вложенные операторы. Выражение является частью оператора и строится на основе множества символов операций, ключевых слов и операндов. Операндами являются литералы и имена. Одной из характеристик выражения является его значение, которое вычисляется на основе значений операндов по правилам, задаваемым операндами.
Программный модуль
Программа строится на основе программных модулей. Модуль состоит из элементов программного модуля. В модуле нет ничего, кроме инструкций препроцессора и (или) списков операторов.
Как сказано в справочном руководстве по C++, файл состоит из последовательности объявлений.
Здесь нет ничего странного: определение является частным случаем объявления (например, объявление, содержащее инициализацию).
Сложность оператора практически ничем не регламентируется, к ним, в частности, относятся объявления и определения объектов, объявления (или прототипы) и определения функций.
В свою очередь, функция состоит из заголовка, который включает спецификаторы объявления, описатели и инициализаторы и тела.
Тело функции представляет собой блок операторов - список операторов (опять!), заключаемый в фигурные скобки.
Объекты и функции
Объектом называют область памяти, выделяемую для сохранения какой-либо информации. Эта информация в данной области памяти кодируется двоичной последовательностью. Такие последовательности составляют множество значений объекта.
Резервирование области памяти предполагает обязательную возможность доступа к ней. Обращение к объекту обеспечивается выражениями. Выражение в языке программирования является единственным средством взаимодействия с объектами. Частным случаем выражения является имя объекта.
Объекты, которые используются исключительно для сохранения информации, называются константами. Обычно константе присваивается значение в момент создания объекта. Дальнейшие изменения значения константы не допускаются.
Объекты, которые допускают изменение зафиксированных в них значений, называются переменными. Инициализация переменной (присваивание ей начального значения) может быть не связана с оределением этой переменной. Переменная открыта для изменения значений, а потому присвоение значения может быть произведено в любом месте программы, где только существует возможность доступа к переменной.
Основными характеристиками объекта являются: тип, класс памяти, область действия связанного с объектом имени, видимость имени объекта, время жизни, тип компоновки (или тип связывания).
Все атрибуты объектов в программе взаимосвязаны. Они могут быть явным образом специфицированы в программе, а могут быть заданы по умолчанию в зависимости от контекста, в котором имя объекта встречается в тексте программы.
Область памяти, выделяемая для сохранения программного кода, называется функцией. Между объектами и функциями много общего. Обращение к функциям также обеспечивается выражениями. Эти выражения называются выражениями вызова функций. Значения выражений вызова вычисляются в результате выполнения соответствующего программного кода. Функция характеризуется типом, область действия связанного с функцией имени, видимостью имени функции, типом связывания.
Семантика
Семантика языка устанавливает соответствие между составляющими программу языковыми конструкциями и конкретными действиями, которые выполняет вычислительная машина в ходе выполнения программы. Фактически семантика определяет смысл предложений языка. При этом синтаксис и семантика являются независимыми языковыми характеристиками. Синтаксически правильное предложение может оказаться в принципе невыполнимым и потому лишённым всякого смысла.
Типы
Тип является основной характеристикой объекта и функции. Тип определяет, что и как следует делать со значениями объектов и функций. Значение функции выполняется, значение константы читается, константой переменной модифицируется. Тип определяет структуру и размеры объекта, диапазон и способы интерпретации его значения, множество допустимых операций.
Поскольку конкретное значение может быть зафиксировано в области памяти, которая соответствует объекту определённого типа, можно также говорить о типе значения. Значения представляются выражениями. Поэтому имеет смысл также говорить и о типе выражения. Таким образом, тип оказывается важнейшей характеристикой языка.
Можно предположить существование языка с единственным типом объекта. Такой язык можно считать нетипизированным языком. Для нетипизированного языка характерен фиксированный размер объекта, единый формат хранения данных, унифицированные способы интерпретации значений.
Как ни странно, нетипизированный язык одинаково неудобен для решения задач в любой конкретной предметной области. К обработке символьной информации или решению сложных вычислительных задач транслятор нетипизированного языка относится одинаково. Для него все объекты одинаковые. Так что реализация алгоритмов сравнения символьных строк, вычисление значений тригонометрических функций, корректное прочтение и запись значений переменных и констант, способы интерпретации информации, применение разнообразных операций к данным (при анализе символьной информации бессмысленны операции умножения и деления) и многие другие проблемы оказываются исключительно проблемами программиста. Больше проблем - больше ошибок.
Здесь имеет смысл обратиться к приложениям, связанным с типизацией и контролем типов. В следующих разделах мы будем говорить о типах объектов. Типы функций будут рассмотрены позже.
Основные типы C++
Основные типы в C++ подразделяются на две группы: целочисленные типы и типы с плавающей точкой (для краткости их будем называть плавающими типами). Это арифметические типы.
В C++ нет жёсткого стандарта на диапазоны значений арифметических типов (в стандарте языка оговариваются лишь минимально допустимые значения). В принципе, эти диапазоны определяются конкретной реализацией. Обычно выбор этих характеристик диктуется эффективностью использования вычислительных возможностей компьютера. Зависимость языка от реализации создаёт определённые проблемы переносимости. C++ остаётся машинно-зависимым языком.
К целочисленным типам относятся типы, представленные следующими именами основных типов:
char
short
int
long
Имена целочисленных типов могут использоваться в сочетании с парой модификаторов типа:
signed
unsigned
Эти модификаторы изменяют формат представления данных, но не влияют на размеры выделяемых областей памяти.
Модификатор типа signed указывает, что переменная может принимать как положительные, так и отрицательные значения. Возможно, что при этом самый левый бит области памяти, выделяемой для хранения значения, используется для представления знака. Если этот бит установлен в 0, то значение переменной считается положительным. Если бит установлен в 1, то значение переменной считается отрицательным.
Модификатор типа unsigned указывает, что переменная принимает неотрицательные значения. При этом самый левый бит области памяти, выделяемой для хранения значения, используется так же, как и все остальные биты области памяти - для представления значения.
В ряде случаев модификаторы типа можно рассматривать как имена основных типов.
Здесь также многое определяется конкретной реализацией. В версиях Borland C++ данные типов, обозначаемых как signed, short и int в памяти занимают одно и то же количество байтов.
Особое место среди множества основных целочисленных типов занимают перечисления, которые обозначаются ключевым словом enum. Перечисления представляют собой упорядоченные наборы целых значений. Они имеют своеобразный синтаксис и достаточно специфическую область использования. Их изучению будет посвящён специальный раздел.
Здесь также многое зависит от реализации. По крайней мере, для Borland C++ 4.5, основные характеристики целочисленных типов выглядят следующим образом:
Тип данных | Байты | Биты | Min | Max |
signed char | 1 | 8 | - 128 | 127 |
unsigned char | 1 | 8 | 0 | 255 |
signed short | 2 | 16 | -32768 | 32767 |
enum | 2 | 16 | -32768 | 32767 |
unsigned short | 2 | 16 | 0 | 65535 |
signed int | 2 | 16 | -32768 | 32767 |
unsigned int | 2 | 16 | 0 | 65535 |
signed long | 4 | 32 | -2147483648 | 2147483647 |
unsigned long | 4 | 32 | 0 | 4294967295 |
К плавающим типам относятся три типа, представленные следующими именами типов, модификаторов и их сочетаний:
float
double
long double
Как и ранее, модификатор типа входит в число имён основных типов.
Плавающие типы используются для работы с вещественными числами, которые представляются в форме записи с десятичной точкой, так и в "научной нотации". Разница между нотациями становится очевидной из простого примера, который демонстрирует запись одного и того же вещественного числа в различных нотациях.
297.7
2.977*10**2
2.977E2
и ещё один пример…
0.002355
2.355*10**-3
2.355E-3
В научной нотации слева от символа E записывается мантисса, справа - значение экспоненты, которая всегда равняется показателю степени 10.
Для хранения значений плавающих типов в памяти используется специальный формат представления вещественных чисел. Этот формат называется IEEE форматом.
Ниже представлены основные характеристики типов данных с плавающей точкой (опять же для Borland C++ 4.5):
Тип данных | Байты | Биты | Min | Max |
float | 4 | 32 | 3.4E-38 | 3.4E+38 |
double | 8 | 64 | 1.7E-308 | 1.7E+308 |
long double | 10 | 80 | 3.4E-4932 | 3.4E+4932 |
Подведём итог.
Имена типов данных и их сочетания с модификаторами типов используются для представления данных различных размеров в знаковом и беззнаковом представлении:
char
signed char
unsigned char
short
signed short
unsigned short
signed
unsigned
short int
signed short int
unsigned short int
int
signed int
unsigned int
long
signed long
unsigned long
long int
signed long int
unsigned long int
Все эти типы образуют множество целочисленных типов. К этому множеству также относятся перечисления.
А вот сочетания имён типов и модификаторов для представления чисел с плавающей точкой:
float
double
long double
Вот и всё об основных типах. Помимо основных типов в C++ существуют специальные языковые средства, которые позволяют из элементов основных типов создавать новые, так называемые производные типы.
Объявление переменных
Мы приступаем к изучению синтаксиса операторов C++. В языке различают несколько типов операторов. Каждый из них выполняет в программе строго определённые функции.
Так, операторы объявления служат для ввода имён в программу. Процедура ввода имени переменной предполагает не только создание отличного от любого ключевого слова идентификатора, но и кодирование дополнительной информации о характеристиках объекта, с которым будет связано объявляемое имя.
К характеристикам объекта относятся тип объекта, класс памяти, время жизни объекта, множество других свойств, представляемых различными модификаторами.
Прежде чем приступить к описанию грамматики объявления переменных, введём для употребления в БНФ пару новых символов: [ и ]. Эти символы мы будем называть синтаксическими скобками. Заключение какого либо символа БНФ в синтаксические скобки означает, что этот символ в данной БНФ, а значит и в описываемом выражении является необязательным элементом. Он может входить в выражение, а может и не появляться вовсе.
Договоримся также об использовании в БНФ ещё одного символа. Этот символ будет иметь вид последовательности из пяти звёздочек, стоящих непосредственно за символом ::= в левой части формулы. Таким образом, содержащая этот символ БНФ будет выглядеть так:
Описатель ::= *****
или даже так:
::= *****
Этот символ мы будем называть прерывателем БНФ. Он будет означать, что определение нетерминального символа прерывается и будет продолжено позже.
Оператор ::= ОператорОбъявления
::= *****
ОператорОбъявления ::= Объявление
Объявление ::= ОбъявлениеПеременной
::= *****
ОбъявлениеПеременной ::= ОбъявлениеПеременнойОсновногоТипа
::= *****
ОбъявлениеПеременнойОсновногоТипа ::=
[СписокСпецификаторовОбъявления] [СписокОписателей];
СписокСпецификаторовОбъявления ::=
[СписокСпецификаторовОбъявления] СпецификаторОбъявления
СпецификаторОбъявления ::= СпецификаторКлассаПамяти
::= СпецификаторТипа
::= cvСпецификатор
::= fctСпецификатор
::= *****
СпецификаторКлассаПамяти ::= auto
::= register
::= static
::= extern
СпецификаторТипа ::= ИмяПростогоТипа
::= СпецификаторПеречисления
::= СпецификаторКласса
::= УточнённыйСпецификаторТипа
УточнённыйСпецификаторТипа ::= КлючевоеСловоКласса Идентификатор
::= КлючевоеСловоКласса ИмяКласса
::= enum ИмяПеречисления
ИмяПростогоТипа ::= char
::= short
::= int
::= long
::= signed
::= unsigned
::= float
::= double
::= void
::= ******
cvСпецификатор ::= const
::= volatile
СписокОписателей ::= ОписательИнициализатор
::= СписокОписателей , ОписательИнициализатор
ОписательИнициализатор ::= Описатель [Инициализатор]
Описатель ::= dИмя
::= (Описатель)
::= *****
Инициализатор ::= = Выражение
::= (СписокВыражений)
::= *****
Выражение ::= Литерал
::= Имя
::= *****
СписокВыражений ::= ВыражениеПрисваивания
::= СписокВыражений , ВыражениеПрисваивания
dИмя ::= Имя
::= ИмяКласса
::= ~ ИмяКласса
::= ОписанноеИмяТипа
::= КвалифицированноеИмяТипа
ВыражениеПрисваивания - этот нетерминальный символ используется в Справочном руководстве по C++ для обозначения элементов списка выражений. Не следует особо смущаться по поводу этого нового обозначения. Это всего лишь частный случай выражения.
dИмя - это имя того, что описывается описателем в данном объявлении. В "Справочном руководстве по языку программирования C++" английский эквивалент понятия описатель - declarator. Обилие нетерминальных символов, производных от символа Имя не должно вызывать никаких затруднений. В конечном счёте, нетерминальные символы ИмяКласса , ОписанноеИмяТипа , ИмяПеречисления (об этом позже) - являются обыкновенными идентификаторами. Всё зависит от контекста объявления. Что объявляется, так и называется. Именующее класс ОписанноеИмяТипа одновременно является и ИменемКласса .
ИмяКласса ::= Идентификатор
ОписанноеИмяТипа ::= Идентификатор
ИмяПеречисления::= Идентификатор
Мы располагаем достаточно большим (хотя пока и неполным) множеством БНФ, которые задают правила построения синтаксически безупречных операторов объявления переменных в C++.
Согласно приведённым правилам, оператором объявления переменных будет считаться пустой оператор
;
он состоит из точки с запятой. Между прочим, точкой с запятой заканчиваются все операторы C++. Оператором объявления будет также считаться и такая последовательность спецификаторов объявления:
auto register static extern char short int const;
и здесь также важно не забыть поставить точку с запятой. С точки зрения синтаксиса это правильное предложение.
Язык программировани C++ позволяет описывать данные и алгоритмы их обработки. Вместе с тем, правильно построенная цепочка слов языка может быть абсолютно бессмысленной, то есть не нести никакой информации ни о данных, ни о шагах конкретного алгоритма. Большая часть порождаемых с помощью грамматических правил предложений оказывается семантически некорректными и лишёнными всякого смысла.
Грамматика не отвечает за семантику и тем более за смысл предложений. Она всего лишь описывает правила построения операторов. Тем не менее, транслятор обеспечивает частичный семантический контроль предложений. Поэтому ранее рассмотренные объявления и воспринимаются как ошибочные. Также ошибочным оказывается объявление, состоящее из одного спецификатора объявления:
int ;
Можно было бы усовершенствовать систему обозначений, которая применяется в БНФ, а заодно и сделать более строгими правила синтаксиса. Например, можно было бы добиться того, чтобы пустые операторы воспринимались как синтаксически некорректные предложения. Однако это не может привести к кардинальному решению проблемы семантического контроля.
Рекомендации по наименованию объектов
Имена - это идентификаторы. Любая случайным образом составленная последовательность букв, цифр и знаков подчёркивания с точки зрения грамматики языка идеально подходит на роль имени любого объекта, если только она начинающаяся с буквы. Фрагмент программы, содержащий подобную переменную, будет синтаксически безупречен.
И всё же имеет смысл воспользоваться дополнительной возможностью облегчить восприятие и понимание последовательностей операторов. Для этого достаточно закодировать с помощью имён содержательную информацию.
Желательно создавать составные осмысленные имена. При создании подобных имён в одно слово можно "втиснуть" предложение, которое в доступной форме представит информацию о типе объекта, его назначении и особенностях использования.
Комментарии: возможность выразиться неформально
Язык программирования C++, как и любой формальный язык непривычен для восприятия и в силу этого в ряде случаев может быть тяжёл для понимания. В C++ предусмотрены дополнительные возможности для облегчения восприятия текстов программ. Для этого используются комментарии.
Комментарии - это любые последовательности символов алфавита C++, заключённые в специальные символы. Эти символы называются символами - комментариями. Существуют две группы символов - комментариев. К первой группе относятся парные двухбуквенные символы /* и */.
Ко второй группе символов - комментариев относится пара, состоящая из двухбуквенного символа // и не имеющего графического представления пробельного символа новой строки.
Последовательность символов, ограниченная символами комментариев, исчезает из поля зрения транслятора. В этой "мёртвой зоне" программист может подробно описывать особенности создаваемого алгоритма, а может просто "спрятать" от транслятора целые предложения на C++.
Элементы программного модуля
Мы переходим к описанию синтаксиса элементов программного модуля, но, прежде всего, определим ещё одну категорию спецификаторов объявления.
СпецификаторОбъявления ::= fctСпецификатор
::= *****
fctСпецификатор ::= inline
::= virtual
fctСпецификатор используется при объявлении и определении функций. Их назначение ещё будет обсуждаться в дальнейшем.
ЭлементПрограммногоМодуля ::= СписокИнструкцийПрепроцессора
::= СписокОператоров
СписокОператоров ::= [СписокОператоров] Оператор
Оператор ::= ОператорОбъявления
::= *****
ОператорОбъявления ::= Объявление
Объявление ::= ОбъявлениеФункции
::= ОпределениеФункции
::= *****
ОбъявлениеФункции ::=
[СписокСпецификаторовОбъявления]
Описатель
[СпецификацияИсключения];
ОпределениеФункции ::=
[СписокСпецификаторовОбъявления]
Описатель
[ctorИнициализатор]
[СпецификацияИсключения]
ТелоФункции
Описатель ::= ИмяОписатель
::= ptrОперация Описатель
::= Описатель (СписокОбъявленийПараметров)
::= Описатель [[КонстантноеВыражение]]
::= (Описатель)
ИмяОписатель ::= Имя
::= *****
ptrОперация ::= * [СписокCVОписателей]
::= & [СписокCVОписателей]
СписокCVОписателей ::= CVОписатель [СписокCVОписателей]
CVОписатель ::= const | volatile
ctorИнициализатор ::= *****
СпецификацияИсключения ::= *****
О последних двух нетерминалах позже.
КонстантноеВыражение ::= УсловноеВыражение
Свойства константного выражения мы также обсудим позже.
УсловноеВыражение ::= *****СписокОбъявленийПараметров ::= [СписокОбъявленийПарам] [...] ::= СписокОбъявленийПарам, ...
Следует обратить особое внимание на последнюю БНФ. В ней зафиксировано различие между двумя нетерминалами. Так что СписокОбъявленийПараметров - совсем не то, что СписокОбъявленийПарам. Здесь нет никаких опечаток или ошибок. Первый нетерминал по смыслу шире второго. Три точечки, заключённые в круглые скобочки (...) уже в определённом контексте можно рассматривать как СписокОбъявленийПараметров, но это никак не СписокОбъявленийПарам. Это как раз тот самый случай, когда к нетерминалам имеет смысл относится как к СИМВОЛАМ, а не как к последовательностям подчёркнутых слов.
СписокОбъявленийПарам ::= ОбъявлениеПараметра
::= [СписокОбъявленийПарам,] ОбъявлениеПараметра
ОбъявлениеПараметра ::=
СписокСпецификаторовОбъявления Описатель
::= СписокСпецификаторовОбъявления
Описатель
Инициализатор
::=
СписокСпецификаторовОбъявления
[АбстрактныйОписатель]
[Инициализатор]
АбстрактныйОписатель ::= ptrОперация [АбстрактныйОписатель]
::= [АбстрактныйОписатель]
(СписокОбъявленийПараметров)
[СписокCVОписателей]
::= [АбстрактныйОписатель] [[КонстантноеВыражение]]
::= (АбстрактныйОписатель)
БНФ, раскрывающая смысл нетерминала АбстрактныйОписатель, также проста, как и все прочие БНФ. Достаточно беглого взгляда, чтобы понять, что в роли этого самого абстрактного описателя могут выступать операции *, &, даже пара символов [], между которыми может располагаться константное выражение. Абстрактный описатель можно также поместить в круглые скобки.
Если обычный описатель предполагает какое-либо имя, то абстрактный описатель предназначается для обозначения неименованных (безымянных) сущностей.
ТелоФункции ::= СоставнойОператор
СоставнойОператор ::= {[СписокОператоров]}
Фигурные скобочки - характерный признак составного оператора.
СписокОператоров ::= Оператор
::= СписокОператоров Оператор
Оператор ::= ОператорОбъявления
::= *****
СписокИнструкцийПрепроцессора ::=
[СписокИнструкцийПрепроцессора]
ИнструкцияПрепроцессора
ИнструкцияПрепроцессора ::= #
::= Макроопределение
::= ФункциональноеМакроопределение
::= *****
Макроопределение ::= #define Идентификатор СтрокаЛексем
ФункциональноеМакроопределение ::=
#define Идентификатор (СписокИдентификаторов) СтрокаЛексем
СписокИдентификаторов ::= Идентификатор
::= СписокИдентификаторов, Идентификатор
СтрокаЛексем ::= Лексема
::= СтрокаЛексем Лексема
Составной оператор также называют блоком операторов (или просто блоком).
Несмотря на значительное количество пропусков в приведённых выше БНФ, содержащейся в них информации о синтаксисе программного модуля вполне достаточно для реконструкции его общей структуры.
Структура модуля
Сейчас мы рассмотрим структуру модуля. На содержательную часть этой "программы" можно не обращать никакого внимания. Сейчас важен лишь синтаксис.
СписокИнструкцийПрепроцессора
СписокОператоров
Макроопределение
Оператор
Оператор
Оператор
Оператор
#define Идентификатор СтрокаЛексем
ОбъявлениеПеременной
ОбъявлениеФункции
ОпределениеФункции
ОпределениеФункции
#define IdHello "Hello…"
int *pIntVal[5];
/*
Объявлена переменная типа массив указателей размерности 5 на объекты типа int с именем pIntVal.
*/
СпецификаторОбъявления Описатель;
СпецификаторОбъявления Описатель ТелоФункции
СпецификаторОбъявления Описатель ТелоФункции
#define IdHello "Hello…"
int *pIntVal[5];
int Описатель (СписокОбъявленийПараметров);
float Описатель (СпецификаторОбъявления Имя ) ТелоФункции
unsigned int MyFun2 (int Param1, ...) СоставнойОператор
#define IdHello "Hello…"
int *pIntVal[5];
int MyFun1 (
СпецификаторОбъявления ,
СпецификаторОбъявления АбстрактныйОписатель Инициализатор,
);
float MyFun2 (СпецификаторОбъявления ИмяОписатель)
ТелоФункции
unsigned int MyFun3 (int Param1, ...) {СписокОператоров}
#define IdHello "Hello…"
int *pIntVal[5];
int MyFun1 (float, int *[5] = pIntVal);
/*
Объявление функции. В объявлении второго параметра используется
абстрактный описатель - он описывает нечто абстрактное, а, главное,
безымянное, вида *[5]. Судя по спецификатору объявления int,
расположенному перед описателем, "нечто" подобно массиву указателей
на объекты типа int из пяти элементов (подробнее о массивах после).
И эта безымянная сущность инициализируется с помощью инициализатора.
Сейчас нам важно проследить формальные принципы построения программного
модуля. Прочие детали будут подробно обсуждены ниже.
*/
float MyFun2 (char chParam1)
{
СписокОператоров
}
unsigned int MyFun3 (int Param1, …)
{СписокОператоров}
#define IdHello "Hello…"
int *pIntVal[5];
int MyFun1 (float, int *[5] = pIntVal); // Объявление функции.
// Определены две функции…
float MyFun2 (char chParam1)
{
extern int ExtIntVal;
char *charVal;
}
unsigned int MyFun3 (int Param1, …)
{
const float MMM = 233.25;
int MyLocalVal;
}
Только что на основе БНФ было построено множество предложений, образующих программный модуль. Фактически, наша первая программа ничего не делает. Всего лишь несколько примеров бесполезных объявлений и никаких алгоритмов. Тем не менее, этот пример показывает, что в программе нет случайных элементов. Каждый символ, каждый идентификатор программы играет строго определённую роль, имеет собственное название и место в программе. И в этом и состоит основная ценность этого примера.
Итак, наш первый программный модуль представляет собой множество инструкций препроцессора и операторов. Часть операторов играет роль объявлений. С их помощью кодируется необходимая для транслятора информация о свойствах объектов. Другая часть операторов является определениями и предполагает в ходе выполнения программы совершение разнообразных действий (например, создание объектов в различных сегментах памяти).
После трансляции модуля предложения языка преобразуются во множество команд процессора. При всём различии операторов языка и команд процессора, трансляция правильно написанной программы обеспечивает точную передачу заложенного в исходный текст программы смысла (или семантики операторов). Программист может следить за ходом выполнения программы по операторам программы на C++, не обращая внимания на то, что процессор в это время выполняет собственные последовательности команд.
С процессом выполнения программы связана своеобразная система понятий. Когда говорят, что в программе управление передаётся какому-либо оператору, то имеют в виду, что в исполнительном модуле процессор приступил к выполнению множества команд, соответствующих данному оператору.
Класс памяти
Класс памяти определяет порядок размещения объекта в памяти. Различают автоматический и статический классы памяти. C++ располагает четырьмя спецификаторами класса памяти:
auto
register
static
extern
по два для обозначения принадлежности к автоматическому и статическому классам памяти.
В свою очередь, статический класс памяти может быть локальным (внутренним) или глобальным (внешним).
Следующая таблица иллюстрирует иерархию классов памяти.
Динамический класс памяти | Статический класс памяти | ||
Автоматический | Регистровый | Локальный | Глобальный |
auto | register | static | Extern |
Спецификаторы позволяют определить класс памяти определяемого объекта:
- auto. Этот спецификатор автоматического класса памяти указывает на то, что объект располагается в локальной (или автоматически распределяемой) памяти. Он используется в операторах объявления в теле функций, а также внутри блоков операторов. Объекты, имена которых объявляются со спецификатором auto, размещаются в локальной памяти непосредственно перед началом выполнения функции или блока операторов. При выходе из блока или при возвращении из функции (о механизмах вызова функций и возвращения из них речь ещё впереди), соответствующая область локальной памяти освобождается и все ранее размещённые в ней объекты уничтожаются. Таким образом спецификатор влияет на время жизни объекта (это время локально). Спецификатор auto используется редко, поскольку все объекты, определяемые непосредственно в теле функции или в блоке операторов и так по умолчанию располагаются в локальной памяти. Вне блоков и функций этот спецификатор не используется.
- register. Ещё один спецификатор автоматического класса памяти. Применяется к объектам, по умолчанию располагаемым в локальной памяти. Представляет из себя "ненавязчивую просьбу" к транслятору (если это возможно) о размещении значений объектов, объявленных со спецификатором register в одном из доступных регистров, а не в локальной памяти. Если по какой-либо причине в момент начала выполнения кода в данном блоке операторов регистры оказываются занятыми, транслятор обеспечивает с этими объектами обращение, как с объектами класса auto. Очевидно, что в этом случае объект располагается в локальной области памяти.
- static. Спецификатор внутреннего статического класса памяти. Применяется только(!) к именам объектов и функций. В C++ этот спецификатор имеет два значения. Первое означает, что определяемый объект располагается по фиксированному адресу. Тем самым обеспечивается существование объекта с момента его определения до конца выполнения программы. Второе значение означает локальность. Объявленный со спецификатором static локален в одном программном модуле (то есть, недоступен из других модулей многомодульной программы) или в классе (о классах - позже). Может использоваться в объявлениях вне блоков и функций. Также используется в объявлениях, расположенных в теле функций и в блоках операторов.
- extern. Спецификатор внешнего статического класса памяти. Обеспечивает существование объекта с момента его определения до конца выполнения программы. Объект, объявленный со спецификатором extern доступен во всех модулях программы, то есть глобален.
Выбор класса памяти, помимо явных спецификаторов, зависит от размещения определения или объявления в тексте программы. Модуль, функция, блок могут включать соответствующие операторы объявления или определения, причём всякий раз определяемый объект будет размещаться в строго определённых областях памяти.
Пространство имен
С понятием области действия имени связано понятие пространства имени.
Пространством имени называется область программы, в пределах которой это имя должно быть уникальным. Различные категории имён имеют различные пространства имён. К их числу относятся:
- Пространство имён глобальных объектов. Это пространство образуется множеством образующих программу программных модулей. Имена глобальных объектов должны быть уникальны среди множества имён глобальных объектов во всех модулях, образующих программу.
- Пространство имен поименованных операторов (или операторов с меткой) - функция. Имя оператора должно быть уникально в теле функции, в которой метка была введена в программу.
- Пространство имён структур, классов, объединений и перечислимых типов зависит от контекста, в котором были объявлены структуры, классы, объединения. Если они были объявлены в блоке - это пространство будет составлять блок, если они были объявлены в модуле, таковой областью является программа. C++ помещает эти имена в общее пространство имён.
- Имена элементов структур, классов, объединений и перечислимых данных должны быть уникальны в пределах определения структуры, класса, объединения и перечислимых данных. При этом в разных структурах, классах, объединениях и перечислимых данных допустимы элементы с одинаковыми именами. Пространством имён для элементов структур, классов, объединений и перечислимых данных элементов являются сами структуры, классы, объединения и перечисления.
- Имена переменных и функций, имена пользовательских типов (типов, определённых пользователем - о них также немного позже) должны быть уникальны в области определения: глобальные объекты должны иметь уникальное имя среди всех глобальных объектов и т.д.
По крайней мере в реализациях C++, для процессоров использующих сегментированную модель памяти, существует определённая связь между пространством имени и расположением поименованного объекта в конкретном сегменте памяти. В пределах определённого сегмента может находиться лишь один объект с уникальным именем. В противном случае возникли бы проблемы с организацией ссылок на располагаемые в сегменте памяти объекты.
Вместе с тем, одни и те же имена могут использоваться при организации ссылок на объекты, располагаемые в разных сегментах памяти. Например, в теле функции можно обращаться как к глобальному объекту, так и к одноимённому локальному объекту, определённому в теле функции.
Правда, обращение к одноимённым объектам, расположенным в различных пространствах имён, ограничено специальными правилами. В связи с необходимостью организации специального протокола обращения к одноимённым объектам, располагаемым в различных сегментах памяти, в C++ возникло понятие области видимости.
Тип связывания или тип компоновки
Тип связывания или тип компоновки определяет соответствие имени объекту или функции в программе, исходный текст которой располагается в нескольких модулях. Различают статическое и динамическое связывание.
Статическое связывание бывает внешним или внутренним. Оно обеспечивается на стадии формирования исполнительного модуля, ещё до этапа выполнения программы.
Если объект локализован в одном модуле, то используется внутреннее связывание. Тип компоновки специальным образом не обозначается, а определяется компилятором по контексту, местоположению объявлений и использованию спецификаторов класса памяти.
Внешнее связывание выполняется компоновщиком, который на этапе сборки многомодульной программы устанавливает связь между уникальным объектом и обращениями к объекту из разных модулей программы.
При динамическом связывании компоновщик не имеют никакого представления о том, какой конкретно объект будет соответствовать данному обращению. Динамическое связывание обеспечивается транслятором в результате подстановки специального кода, который выполняется непосредственно в ходе выполнения программы.
Выражение и l-выражение
Доступ к объектам и функциям обеспечивается выражениями, которые в этом случае ссылаются на объекты.
Выражение, которое обеспечивает ссылку на константу, переменную или функцию, называется l-выражением. Имя объекта в C++ является частным случаем l-выражения.
В C++ допускается изменение значений переменных. Значения констант и функций в C++ изменению не подлежат. l-выражение называется модифицируемым l-выражением, либо леводопустимым выражением, если только оно не ссылается на функцию, массив или константу. Таким образом, леводопустимыми выражениями называют l-выражения, которые ссылаются на переменные.
Перечень операций
Этот раздел содержит краткую предварительную информацию об операциях C++. Детальное описание большинства операций на этом этапе ещё невозможно. Графическое представление, название и назначение операции - сейчас этого вполне достаточно. Всё ещё впереди…
1. Унарные операции
1.1. Адресные операции
& Операция получения адреса операнда.
Операндом может быть любое l-выражение. Операция возвращает адрес объекта или функции, на который ссылается операнд. Операция невыполнима по отношению к объектам, определённым со спецификатором register, поскольку существует вероятность того, что они не располагаются в памяти и не имеют определённого адреса.
* Операция обращения по адресу или операция косвенного обращения.
Операндом может быть выражение, значением которого является адрес. Операция косвенного обращения называется также операцией разыменования, поскольку позволяет обращаться к объекту не употребляя при этом имени объекта.
1.2. Операции преобразования знака
- Операция унарный минус.
Операндом может быть любое выражение со значением арифметического типа. Операция преобразует положительное значение в отрицательное значение и наоборот.
+ Операция унарный плюс.
Операндом может быть любое выражение со значением арифметического типа. Операция в буквальном смысле ничего не делает. В некоторых источниках её существование объясняется тем, что она ведена для симметрии с унарным минусом. Однако, не совсем понятно, что имеется в виду под понятием симметрии в формальном языке.
В C++ существует возможность присвоения (мы ещё уделим внимание этой интуитивно понятной операции) переменной отрицательного значения. Старательные и аккуратные программисты могут особо подчеркнуть и тот факт, что переменной присвоено положительное значение. Для этого в C++ и была реализована специальная операция унарный плюс.
В формальном языке каждая лексема имеет своё название и назначение. И этот самый плюс-украшение также является операцией. А дальше - рушится иллюзия симметрии унарных операций. Унарный минус работает. Он эквивалентен умножению значения операнда на -1. Унарный плюс эквивалентен умножению значения операнда на +1. Он ничего не делает.
1.3. Побитовые операции
~ Операция инвертирования или побитового отрицания.
Операндом может быть любое выражение интегрального типа. Операция обеспечивает побитовое инвертирование двоичного кода.
! Операция логического отрицания.
Операндом может быть любое выражение со значением арифметического типа. Для непосредственного обозначения логических значений в C++ используются целочисленные значения 0 - ложь и 1 - истина. Кроме того, в логических операциях любое ненулевое значение операнда ассоциируется с единицей. Поэтому отрицанием нулевого значения является 1, т.е. истина, а отрицанием любого ненулевого значения оказывается 0, т.е. ложь.
1.4. Операция определения размера
sizeof Операция определения размера объекта или типа.
В C++ различают два варианта этой операции. В первом случае операндом может быть любое l-выражение. Это выражение записывается справа от символа операции. Значением выражения является размер конкретного объекта в байтах. Во втором случае операндом является имя типа. Это выражение записывается в скобках непосредственно за символом операции. Значением выражения является размер конкретного типа данных в байтах. Результатом этой операции является константа типа size_t. Этот производный целочисленный беззнаковый тип определяется конкретной реализацией.
1.5. Операции увеличения и уменьшения значения
++ Инкремент, или операция увеличения на единицу.
Точнее, на величину, кратную единице, поскольку всё зависит от типа операнда. Операция имеет дополнительный эффект - она изменяет значение операнда. Поэтому операндом здесь может быть только леводопустимое выражение арифметического типа, либо типа указателя. В C++ различают префиксную и постфиксную операции инкремента.
В префиксной форме увеличение значения операнда производится до определения значения выражения. В результате значение выражения и значение операнда совпадают.
В постфиксной форме увеличение значения операнда производится после определения значения выражения. Поэтому значение выражения оказывается меньше значенния операнда.
В выражении с префиксной операцией увеличения знак ++ записывается слева от операнда, в выражении с постфиксной операцией - справа.
Операция инкремента по отношению к указателю увеличивает значение операнда на количество байт, равное длине одного объекта этого типа, то есть действительно на величину, кратную единице.
-- Операция уменьшения значения операнда
на величину, кратную единице (декремент).
Эта операция в буквальном смысле симметрична операции инкремента. Имеет аналогичный дополнительный эффект, соответствующие ограничения для операнда (свойство леводопустимости, арифметический тип, либо тип указателя, префиксную и постфиксную формы, изменение значения адреса).
В выражении с префиксной операцией увеличения знак -- записывается слева от операнда, в выражении с постфиксной операцией - справа.
1.6. Операции динамического распределения памяти
new Операция выделения памяти.
Позволяет выделить и сделать доступным участок в динамической памяти. В качестве операнда используется имя типа и, возможно, выражение инициализатор. Операция возвращает адрес размещённого в памяти объекта.
delete Операция освобождения памяти.
Освобождает ранее выделенную с помощью операции new область динамической памяти. В качестве операнда используется адрес освобождаемой области памяти.
1.7. Операция доступа
:: Операция доступа.
Обеспечивает обращение к именованной глобальной области памяти, находящейся вне области видимости. Эта операция применяется при работе с одноимёнными объектами, расположенными во вложенных областях действия имён. Когда объект во внутренней области действия имени скрывает одноименный объект, областью действия которого является файл. Например:
int m; // Глобальная переменная.
:::::
int mmm()
{
int m; // Локальная переменная.
m = 100; // Присвоение значения локальной переменной.
::m = 125; // Присвоение значения глобальной
// переменной m, находящейся вне области видимости
// имени.
}
Не следует испытывать никаких иллюзий относительно возможностей этой операции. Операция обеспечивает доступ лишь к глобальным, естественно, ранее объявленным объектам, независимо от степени вложенности области действия. Поэтому она не обладает свойством транзитивности. Выражения вида ::(::(::m)) воспринимаются транслятором как ошибочные.
2. Бинарные операции
2.1. Аддитивные операции
+ Операция сложения.
Операция используется с операндами арифметического типа. Один из операндов может иметь тип указателя. В любом случае значением выражения является либо сумма значений, либо сумма адреса и целочисленного значения, кратного размерам данного типа.
Результат сложения указателя с целым числом эквивалентен результату соответствующего количества операций инкремента, пррименённых к укаазателю.
Тип и значение результата выражения любой бинарной операции определяется в зависимости от принятых в C++ соглашений о преобразовании типов, о которых будет сказано ниже.
- Операция вычитания.
Симметричная по отношению к операции сложения бинарная операция.
2.2. Мультипликативные операции
* Операция умножения.
Операндами могут быть выражения арифметического типа. Значением выражения является произведение значений. Тип результата выражения любой бинарной операции определяется в зависимости от принятых в C++ процедур преобразования типов данных.
/ Операция деления.
Операндами могут быть выражения арифметического типа. Значением выражения является частное от деления значения первого операнда на второй операнд. Тип результата выражения любой бинарной операции определяется в зависимости от принятых в C++ процедур преобразования типов данных.
% Операция получения остатка от деления
целочисленных операндов (деление по модулю).
Операндами могут быть выражения арифметического типа. В процессе выполнения операции операнды приводятся к целому типу. При неотрицательных операндах остаток положительный. В противном случае знак остатка определяется в зависимости от реализации. Известно, что для Borland C++
15%6=3, (-15)%6=-3, 15%(-6)=3, (-15)%(-6)=-3.
При ненулевом делителе для целочисленных операндов выполняется соотношение
(a/b)*b+a%b=a
2.3. Операции сдвига
Эти операции определены только для целочисленных операндов.
<< Операция левого сдвига.
Операндами могут быть выражения интегрального типа. Значением выражения является битовое представление левого операнда, сдвинутое влево на количество разрядов, равное значению правого операнда. При левом сдвиге на i разрядов первые i разрядов левого операнда теряются, последние i разрядов левого операнда заполняются нулями.
>> Операция правого сдвига.
Операндами могут быть выражения интегрального типа. Значением выражения является битовое представление левого операнда, сдвинутое вправо на количество разрядов, равное значению правого целочисленного операнда. При правом сдвиге на i разрядов первые i разрядов левого операнда заполняются нулями, если левый операнд имеет беззнаковый тип или имеет неотрицательное значение, в противном случае значение определяется реализацией. Последние i разрядов левого операнда теряются.
2.4. Поразрядные операции
Поразрядные операции определены только для целочисленных операндов.
& Поразрядная конъюнкция битовых представлений
значений целочисленных операндов.
Операндами могут быть выражения интегрального типа. Значение выражения вычисляется путём побитовых преобразований и зависит от значений соответствующих битов левого и правого операнда. Следующая таблица однозначно определяет операцию поразрядной конъюнкции.
Бит левого операнда | Бит правого операнда | Результат операции & |
1 | 1 | 1 |
1 | 0 | 0 |
0 | 1 | 0 |
0 | 0 | 0 |
| Поразрядная дизъюнкция битовых представлений
значений целочисленных операндов.
Операндами могут быть выражения интегрального типа. Значение выражения вычисляется путём побитовых преобразований и зависит от значений соответствующих битов левого и правого операнда. Следующая таблица определяет операцию поразрядной дизъюнкции.
Бит левого операнда | Бит правого операнда | Результат операции | |
1 | 1 | 1 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 0 |
^ Поразрядная исключающая дизъюнкция битовых
представлений значений целочисленных операндов.
Операндами могут быть выражения интегрального типа. Значение выражения вычисляется путём побитовых преобразований и зависит от значений соответствующих битов левого и правого операнда. Следующая таблица определяет операцию поразрядной исключающей дизъюнкции.
Бит левого операнда | Бит правого операнда | Результат операции ^ |
1 | 1 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 0 |
2.5. Операции сравнения
<,<=,>,>=,==,!= Меньше, меньше равно, больше,
больше равно, равно, не равно.
Операции сравнения определены на множестве операндов арифметического типа. Допускается также сравнение значений адресов в памяти ЭВМ. Следующая таблица демонстрирует зависимость результата сравнения от значений операндов Val1 и Val2. Результат сравнения всегда целочисленный и может принимать одно из двух значений: 0 и 1. При этом 0 означает ложь, а 1 - истину.
Операция | 1, если | 0, если |
< | Val1 меньше Val2 | Val1 больше или равно Val2 |
<= | Val1 меньше или равно Val2 | Val1 больше Val2 |
> | Val1 больше Val2 | Val1 меньше или равно Val2 |
>= | Val1 больше или равно Val2 | Val1 меньше Val2 |
== | Val1 равно Val2 | Val1 не равно Val2 |
!= | Val1 не равно Val2 | Val1 равно Val2 |
2.6. Логические бинарные операции
&&,|| И, ИЛИ.
Логические бинарные операции объединяют выражения сравнения со значениями истина (!=0) и ложь (==0). Результат операций приведён в следующей таблице
Первый операнд | Второй операнд | && | || |
Истина | истина | 1 | 1 |
Истина | ложь | 0 | 1 |
Ложь | истина | 0 | 1 |
Ложь | ложь | 0 | 0 |
2.7. Операция присваивания
= Простая форма операции присваивания.
Левый операнд операции присваивания является леводопустимым выражением.
В качестве правого операнда операции присваивания может выступать любое выражение. Значение правого операнда присваивается левому операнду. Значение выражения оказывается равным значению правого операнда. Не существует никаких ограничений на структуру этого операнда. Правый операнд может состоять из множества выражений, соединенных операциями присвоения:
An=…=A3=A2=A1;
где A1, A2, A3, …, An являются выражениями. Для определения значений выражений подобной структуры в C++ существуют правила группирования операндов выражений сложной структуры (эти правила подробно будут описаны ниже). В соответствии с одним из этих правил операнды операции присвоения группируются справа налево:
An=(An-1=…=(A3=(A2=A1))…);
Очевидно, что в таком выражении все операнды, кроме самого правого, должны быть модифицируемыми l-выражениями. В результате выполнения этого выражения операндам An, An-1, … A3, A2 будет присвоено значение операнда A1.
2.8. Специальные формы операций присваивания
В процессе трансляции выражений на этапе генерации кода транслятор строит последовательности машинных кодов, реализующие закодированные в выражениях действия. Например, при трансляции выражения
A = A + 125
транслятор, прежде всего, генерирует код для вычисления значения выражения A + 125 и присвоения результата переменной A. При этом фрагмент кода, вычисляющий адрес переменной A дважды войдёт во множество команд процессора, реализующих это выражение.
В целях упрощения структуры подобных операторов в C++ применяются комбинированные (или сокращённые) формы операторов присваивания.
*= Операция присвоения произведения.
A *= B
Присвоение левому операнду произведение значений левого и правого операндов. Операция по своему результату эквивалентна простой форме операции присвоения, у которой правый операнд имеет вид произведения A * B, левый операнд имеет вид A. При этом A является модифицируемым l-выражением:
A = A * B
/= Операция присвоения частного от деления.
A /= B + 254
Присвоение левому операнду частного от деления значения левого операнда на значение выражения правого операнда. Операция по своему результату эквивалентна простой форме операции присвоения, у которой правый операнд имеет вид
A / (B + 254)
левый операнд прелставляется выражением A. Очевидно, что при этом A должно быть модифицируемым l-выражением:
A = A / (B + 254)
%= Операция присвоения остатка от деления.
A %= B
Левый операнд должен быть модифицируемым l-выражением.
+= Операция присвоения суммы.
A += B
Левый операнд должен быть модифицируемым l-выражением.
-= Операция присвоения разности.
A -= B
Левый операнд должен быть модифицируемым l-выражением.
<<= Операция присвоения результата операции побитового сдвига влево на
количество бит, равное значению правого целочисленного операнда.
A <<= B
Левый операнд должен быть модифицируемым l-выражением.
>>= Операция присвоения результата операции побитового сдвига вправо
на количество бит, равное значению правого целочисленного операнда.
A <<= B
Левый операнд должен быть модифицируемым l-выражением.
&= Операция присвоения результата поразрядной конъюнкции битовых
представлений значений целочисленных операндов.
A &= B
Левый операнд должен быть модифицируемым l-выражением.
|= Операция присвоения результата поразрядной дизъюнкции битовых
представлений значений целочисленных операндов.
A |= B
Левый операнд должен быть модифицируемым l-выражением.
^= Операция присвоения результата поразрядной исключающей дизъюнкции
битовых представлений значений целочисленных операндов.
A ^= B
Левый операнд должен быть модифицируемым l-выражением.
Специальные формы операций присвоения позволяют не только изменять структуру выражений, но и оптимизировать создаваемый транслятором программный код. Фрагмент кода, определяющий адрес левого операнда выражения встречается в соответствующем множестве команд процессора лишь один раз.
2.9. Операции выбора компонентов структурированного объекта
К операциям выбора компонентов структурированного объекта относятся:
. Операция прямого выбора - точка.
-> Операция косвенного выбора.
Об этих операциях будет сказано позже, после определения понятия класса и объекта-представителя класса.
2.10. Операции обращения к компонентам класса
К операциям обращения к компонентам класса относятся:
.* Операция обращения к компоненте класса по имени объекта
или ссылки на объект (левый операнд операции) и указателю
на компоненту класса (правый операнд операции).
->* Операция обращения к компоненте класса по указателю на
объект (левый операнд операции) и указателю на компоненту
класса (правый операнд операции).
:: Операция доступа к компоненте класса по имени класса и имени компоненты.
2.11. Операция управления процессом вычисления значений
, Операция запятая.
Группирует выражения слева направо. Разделённые запятыми выражения вычисляются последовательно слева направо, в качестве результата сохраняются тип и значение самого правого выражения.
A &= B, A * B, -A
Эта операция формально также является бинарной операцией, хотя операнды этой операции абсолютно не связаны между собой
2.12. Операция вызова функции
() Операция вызова.
Играет роль бинарной операции при вызове функции. Левый операнд представляет собой выражение, значением которого является адрес функции. Правый операнд является разделённым запятыми списком выражений, определяющих значения параметров.
2.13. Операция явного преобразования типа
() Операция преобразования (или приведения) типа.
Эта бинарная операция в контексте так называемого постфиксного выражения и в контексте выражения приведения обеспечивает изменение типа значения выражения, представляемого вторым операндом. Информация о типе, к которому преобразуется значение второго операнда, кодируется первым выражением, которое является спецификатором типа. Существуют две формы операции преобразования типа: каноническая, при которой в скобки заключается первый операнд (в выражениях приведения), и функциональная (в постфиксных выражениях), при которой в скобки заключается второй операнд. При функциональной форме операции преобразования типа спецификатор типа представляется одним идентификатором. Для приввведениия значения к типу unsigned long следует использовать лишь каноническую форму операции преобразования. Механизм преобразования типа рассматривается ниже.
2.14. Операция индексации
[] Операция индексации.
Играет роль бинарной операции при индексации элементов массива (определение массива приводится ниже). Левый операнд представляет собой выражение, значением которого является адрес первого элемента массива. Правый операнд является выражением, определяющим значение индекса, т.е. смещения относительно первого элемента массива.
2. Операция с тремя операндами
?: Условная операция.
Единственная в C++ операция с тремя операндами. Первое выражение-операнд располагается слева от знака ?, второе выражение-операнд располагается между знаками ? и :, третье выражение-операнд - справа от знака :. Выполнение условной операции начинается с вычисления значения самого левого операнда. Если его значение оказывается отличным от 0, то вычисляется значение второго операнда, которое и становится значением выражения. Если значение первого операнда оказывается равным 0, то вычисляется значение третьего операнда, и тогда это значение становится значением выражения.
(x < 10)? x = 25: x++
3. Операция typeid
Операция typeid обеспечивает динамическую идентификацию типов. Пока лишь упомянем о её существовании, поскольку её описание требует углублённых познаний в области объектно-ориентированного программирования.
Выражение
Выражение ::= ВыражениеПрисваивания
::= Выражение , ВыражениеПрисваивания
В контексте, где запятая выступает в роли разделителя, например, списке параметров вызова функции или в списке инициализации, запятая как знак операции может появиться только в круглых скобках:
MyFun(a, (w = 5, w + 9), c)
/* Выражение вызова функции с тремя параметрами. Значение второго параметра
задаётся выражением, значение которого равно 14. */
Большая часть выражений представляет собой сочетание символов операций и операндов. Однако это вовсе не означает, что в ходе вычисления значения подобных выражений непременно будут применяться соответствующие операции. Выражение - это видимость. В каждом конкретном случае всё зависит от типа операндов. Если операнды оказываются операндами основного типа, либо указателями, то можно предположить, что при вычислении его значения будет выполняться конкретная операция C++. Если же операнды выражения оказываются операндами производного типа, символ операции может оказаться эквивалентным вызову операторной функции. И кто знает, что делает эта самая операторная функция.
Выражения присваивания
ВыражениеПрисваивания ::= УсловноеВыражение
::= УнарноеВыражение ОперацияПрисваивания ВыражениеПрисваивания
ОперацияПрисваивания ::= = | *= | /= | %= | += | -= |
>>= | <<= | &= | ^= | |=
Условные и логические выражения
УсловноеВыражение ::= ВыражениеИлиЛогическое
::= ВыражениеИлиВключающее ? Выражение : УсловноеВыражение
ВыражениеИЛогическое ::= ВыражениеИлиВключающее
::= ВыражениеИЛогическое && ВыражениеИлиВключающее
ВыражениеИлиЛогическое ::= ВыражениеИЛогическое
::= ВыражениеИлиЛогическое || ВыражениеИЛогическое
Побитовые выражения
ВыражениеИлиВключающее ::= ВыражениеИлиИсключающее ::= ВыражениеИлиВключающее | ВыражениеИлиИсключающее ВыражениеИлиИсключающее ::= ВыражениеИ ::= ВыражениеИлиИсключающее ^ ВыражениеИ ВыражениеИ ::= ВыражениеРавенства ::= ВыражениеИ & ВыражениеРавенстваВыражения равенства
ВыражениеРавенства ::= ВыражениеОтношения ::= ВыражениеРавенства == ВыражениеОтношения ::= ВыражениеРавенства != ВыражениеОтношенияВыражения отношения
ВыражениеОтношения ::= ВыражениеСдвига ::= ВыражениеОтношения < ВыражениеСдвига ::= ВыражениеОтношения > ВыражениеСдвига ::= ВыражениеОтношения <= ВыражениеСдвига ::= ВыражениеОтношения >= ВыражениеСдвигаВыражения сдвига
ВыражениеСдвига ::= АддитивноеВыражение ::= ВыражениеСдвига << АддитивноеВыражение ::= ВыражениеСдвига >> АддитивноеВыражениеАддитивные выражения
АддитивноеВыражение ::= МультипликативноеВыражение ::= АддитивноеВыражение + МультипликативноеВыражение ::= АддитивноеВыражение - МультипликативноеВыражениеМультипликативные выражения
МультипликативноеВыражение ::= pmВыражение ::= МультипликативноеВыражение * pmВыражение ::= МультипликативноеВыражение / pmВыражение ::= МультипликативноеВыражение % pmВыражениеВыражения с указателями
pmВыражение ::= ВыражениеПриведения ::= pmВыражение .* ВыражениеПриведения ::= pmВыражение ->* ВыражениеПриведенияВыражение приведения
Для преобразования данного значения к определённому типу используется выражение явного преобразования (одна из разновидностей постфиксного выражения). Оно имеет вид функциональной формы записи: имя типа, за которым в скобочках записывается список выражений.
Кроме того, в C++ существует каноническая форма записи выражения приведения.
ВыражениеПриведения ::= УнарноеВыражение ::= (ИмяТипа) ВыражениеПриведенияОсновные ограничения на типы операндов и особенности выполнения соответствующих операций также ранее уже обсуждались.
Унарное выражение
УнарноеВыражение ::= ПостфиксноеВыражение
::= ++ УнарноеВыражение
::= -- УнарноеВыражение
::= УнарнаяОперация ВыражениеПриведения
::= sizeof УнарноеВыражение
::= sizeof (ИмяТипа)
::= ВыражениеРазмещения
::= ВыражениеОсвобождения
УнарнаяОперация ::= * | & | + | - | ! | ~
Унарные выражения группируются справа налево.
Вторая и третья БНФ являются основой для построения префиксных выражений увеличения и уменьшения (инкремента и декремента). Символ операции в выражении инкремента и декремента вовсе не означает, что в ходе вычисления значения выражения к операндам будут применяться операции уменьшения и увеличения. В сочетании с операндами производных типов определение значений этих выражений сопровождается вызовами специальных (операторных) функций.
В выражениях, представленных четвёртой БНФ, унарная операция * является операцией разыменования. Типом выражения приведения является указатель на объект типа X, а это указывает на то, что описываемое значение является l-выражением. Значением выражения является значение размещённого в памяти объекта. Если типом операнда является тип указатель на объект типа X, то типом выражения является непосредственно тип X.
Результатом операции & является адрес объекта, представленного операндом. При этом операнд операции может оказаться либо l-выражением, либо квалифицированным именем. Но об этом позже.
Далее приводится множество БНФ, определяющих синтаксис выражений размещения и освобождения. У этих выражений достаточно сложная семантика. Детально разобрать их в данный момент мы пока просто не сможем. На этом этапе придётся ограничиться лишь самыми необходимыми сведениями.
Выражение размещения
ВыражениеРазмещения
::= [::] new [Размещение] ИмяТипаNew [ИнициализаторNew]
::= [::] new [Размещение] (ИмяТипа) [ИнициализаторNew]
Размещение ::= (СписокВыражений)
ИмяТипаNew ::= СписокСпецификаторовТипа [ОписательNew]
ОписательNew ::= * [СписокCVОписателей] [ОписательNew]
::= [ОписательNew] [Выражение]
ИмяТипа ::= СписокСпецификаторовТипа [АбстрактныйОписатель]
СписокСпецификаторовТипа ::=
СпецификаторТипа [СписокСпецификаторовТипа]
СпецификаторТипа ::= ИмяПростогоТипа
::= const
::= volatile
::= *****
Существуют также спецификаторы типа, обозначаемые нетерминальными символами СпецификаторКласса, СпецификаторПеречисления и УточнённыйСпецификаторТипа:
СпецификаторТипа ::= СпецификаторКласса
::= СпецификаторПеречисления
::= УточнённыйСпецификаторТипа
Об этих спецификаторах позже. Нетерминальный символ ИмяПростогоТипа представляет все известные в C++ имена основных типов. Кроме того, именами простого типа также считаются синтаксические конструкции, обозначаемые нетерминальными символами ПолноеИмяКласса и КвалифицированноеИмяТипа. Все эти имена строятся на основе идентификаторов, возможно, в сочетании с операцией ::.
ИмяПростогоТипа ::= ПолноеИмяКласса
::= КвалифицированноеИмяТипа
::= *****
ПолноеИмяКласса ::= КвалифицированноеИмяКласса
::= :: КвалифицированноеИмяКласса
Наконец мы можем описать, что собой представляет квалифицированное имя. Это система имён, разделённых операцией :: (обозначает класс, объявленный внутри другого класса).
КвалифицированноеИмя ::= КвалифицированноеИмяКласса :: Имя
КвалифицированноеИмяКласса ::= ИмяКласса
::= ИмяКласса::КвалифицированноеИмяКласса
КвалифицированноеИмяТипа ::= ОписанноеИмяТипа
::= ИмяКласса :: КвалифицированноеИмяТипа
ИмяКласса ::= Идентификатор
ОписанноеИмяТипа ::= Идентификатор
ИнициализаторNew ::= ([СписокИнициализаторов])
СписокИнициализаторов ::= [СписокИнициализаторов,] Инициализатор
Нетерминал АбстрактныйОписатель нам известен. Он используется для описания общей структуры объекта в тех случаях, когда имя объекта не играет никакой роли и может быть опущено. Например, в объявлениях.
Выражение размещения обеспечивает выполнение действий, в результате которых в динамической памяти создаётся объект определённого типа.
При этом отводится память, необходимая для размещения объекта. Сам объект, возможно, инициализируется. После чего возвращается указатель на размещённый в динамической памяти объект.
При этом время жизни объекта, созданного в результате выполнения выражения размещения, не ограничивается областью действия, в которой он был создан. Значением выражения является указатель на созданный объект.
При создании динамического массива (множества объектов одного типа, расположенных друг за другом в одной области динамической памяти), значением выражения размещения оказывается значение указатель на первый элемент массива. При этом соответствующий ОписательNew в квадратных скобках должен содержать информацию о размерах выделяемой области памяти. Естественно, выражение в квадратных скобках должно быть выражением целого типа. Никаких других ограничений на это выражение не накладывается.
…new int[25]…
…new int* [val1 + val2]…
…new float** [x]…
Выражение освобождения
ВыражениеОсвобождения ::= [::] delete ВыражениеПриведения ::= [::] delete [] ВыражениеПриведенияЭто выражение не имеет определённого значения. А значит и о типе выражения мало что можно сказать определённого. Возможно, что оно является выражением типа void. Именно так обозначается специальный тип, который называется также пустым типом. Операция delete работает с динамической памятью. Она способна прочитать скрытую дополнительную информацию о ранее размещённом в динамической памяти с помощью операции new объекте. Поэтому операция delete требует всего одного операнда указателя на объект.
Последствия выполнения операции delete над указателем непредсказуемы и ужасны, если он ссылается на объект, который ранее не был размещён с помощью операции new. Гарантируется безопасность действия операции delete над нулевым указателем. Для удаления массивов используется специальный вариант операции с квадратными скобками. Удаление констант считается ошибкой. Она фиксируется на стадии трансляции. Позже мы обсудим назначение разделителя в выражениях освобождения и размещения '::'.
Постфиксное выражение
Постфиксное выражение определяется на основе первичного выражения. Соответствующее множество БНФ включает множество разнообразных альтернатив.
ПостфиксноеВыражение ::= ПервичноеВыражение ::= ПостфиксноеВыражение [Выражение] ::= ПостфиксноеВыражение ([СписокВыражений]) ::= ПостфиксноеВыражение.Имя ::= ПостфиксноеВыражение->Имя ::= ПостфиксноеВыражение++ ::= ПостфиксноеВыражение- СписокВыражений ::= ВыражениеПрисваивания ::= СписокВыражений, ВыражениеПрисваиванияПервичное выражение является частным случаем постфиксного выражения. Вторым в списке возможных альтернатив постфиксных выражений является БНФ, представляющая различные варианты выражений индексации. Это выражение строится из двух выражений - постфиксного (первичного) выражения, за которым следует ещё одно выражение (второй операнд операции индексации), заключённое в квадратные скобки. Обычно первое выражение представляет указатель на объект типа X (пока неважно, какого типа объект), второе выражение является выражением интегрального типа. Это выражение называется индексом.
Следующей альтернативой является БНФ, представляющая выражения вызова. В нём также участвуют два выражения. Первое выражение может быть представлено именем функции, указателем или ссылкой (частный случай указателя). Список выражений в круглых скобках (второй операнд операции вызова) определяет значения множества параметров, которые используются при означивании соответствующих параметров вызываемой функции.
Выражения явного преобразования типа (в функциональной форме) являются ещё одним вариантом постфиксного выражения. Это выражение начинается с имени простого типа (простой тип - не обязательно основной). В круглых скобках заключается список выражений (второй операнд операции преобразования), на основе которого формируется значение типа, заданного первым элементом выражения. Выражение явного преобразования может содержать пустой список значений. В этом случае результатом выполнения подобной операции также оказывается значение (неважно какое) заданного простого типа. Здесь важен именно тип значения. Само же значение зависит от разных обстоятельств. Оно вообще может оказаться неопределённым, а может определяться в ходе выполнения программы.
Следующие две БНФ представляют схемы выражений доступа к члену класса. Они будут рассмотрены позже.
Наконец, последняя пара БНФ представляет постфиксные выражения увеличения и уменьшения. Эти выражения представляют собой сочетания символов (именно символов!) операций с выражениями-операндами. Операнды выражений инкремента и декремента обязаны быть модифицируемым l-выражениями.
Первичное выражение
Выражение строится на основе операций, объединяющих операнды. Основным элементом выражения является первичное выражение. Первичное выражение - это фактически элементарный строительный блок любого выражения. Следующее множество БНФ определяет синтаксис первичного выражения:
ПервичноеВыражение ::= Литерал
::= Имя
::= (Выражение)
::= this
::= ::ИмяОператорнойФункции
::= ::КвалифицированноеИмя
::= ::Идентификатор
Понятие литерала ранее уже обсуждалось.
Нетерминальный символ Имя также определяется с помощью соответствующего множества БНФ:
Имя ::= Идентификатор
::= ИмяОператорнойФункции
::= ИмяФункцииПриведения
::= КвалифицированноеИмя
::= ~ИмяКласса
Таким образом, квалифицированное имя является одним из вариантов имени. Оба нетерминальных символа, в свою очередь, представляют первичные выражения.
В C++ не существует выражений без типа и значения. Даже в том случае, когда говорят, что значение выражения не определено, выражение всё же имеет значение соответствующего типа. Это случайное значение.
Понятие имени операторной функции связано с так называемым совместным использованием операций (разные типы данных совместно используют одни и те же символы операций). Совместно используемые операции в C++ служат для обозначения особой категории функций, предназначенных для имитации операций C++.
Имя функции приведения и имя класса, которому предшествует специальный символ ~, а также квалифицированное имя непосредственно связаны с понятием класса.
Сложность первичного выражения ничем не ограничивается. Заключённое в круглые скобки выражение рассматривается как первичное выражение.
Первичное выражение this связано с понятием класса. Оно также имеет собственный тип и значение, в качестве которого выступает указатель на объект.
Операция разрешения области видимости ::, за которой следует идентификатор, квалифицированное имя или имя операторной функции, также образуют первичное выражение.
Ничего нельзя сказать о том, что находится вне области видимости. Для транслятора C++ это потусторонний мир. И поэтому не случайно в соответствующей форме Бэкуса-Наура после операции разрешения области видимости используется терминальный символ Идентификатор, а не Имя. Идентификатор становится именем лишь после соответствующего объявления. В момент выполнения операции разрешения области видимости нельзя утверждать, что её операнд является именем.
Уже из определения первичного выражения видно, что в C++ сложность выражения ничем не ограничивается. Вместе с тем любое правильно построенное выражение может быть успешно распознано и систематизировано. Здесь всё зависит от контекста, а фактически от символа "соединяющей" операнды операции.
Константные выражения
КонстантноеВыражение ::= УсловноеВыражение
В ряде случаев C++ требует, чтобы вычисляемое значение выражения было целочисленной константой. Это относится к границам массивов, размерам битовых полей, значениям инициализаторов элементов перечисления. Константные выражения представляют собой неизменяемые целочисленные значения. Они строятся на основе литералов, элементов перечисления (о них речь впереди), проинициализированных целочисленных констант, выражений, построенных на основе операции sizeof.
Константное выражение не меняет своего значения. Поэтому константное выражение не может быть именем переменной или выражением, которое включает имя переменной.
Константные выражения вычисляются на стадии трансляции, а потому в константном выражении не могут использоваться функции, объекты классов, указатели, ссылки, операция запятая и операция присваивания.
Константное выражение может состоять из литералов, имён констант, элементов перечисления (о них позже), может содержать символы арифметических операций, которые связывают константные выражения.
Основное назначение константного выражения в C++ - фиксация значений ограниченного множества значений, предназначенных для организации управленния процессом выполнения программы, задание предопределённых характеристик объектов (например, размер массива). Управление выполнением и характеристика размерности не требует особой точности. Органы управленния должны быть максимально простыми, количество элементов и длина в байтах задаются целочисленными значениями. Здесь нет проблем, связанных с точностью вычисления, здесь достаточно значений интегрального типа.
Значение константного выражения определяется уже на стадии трансляции, поскольку размерность массива и метка помеченного оператора в операторе выбора должны быть известны до начала выполнения программы. А это ещё один аргумент в пользу запрещения включения в константное выражение вызовов функций (на стадии трансляции нет возвращаемых значений).
По этой же причине константное выражение не может быть указателем или ссылкой (о ссылках - позже), поскольку всё, что связано с адресами, определяется лишь на этапе выполнения программы.
Константное выражение не может содержать операцию присваивания, операции инкрементации и декрементации.
А вот операции сравнения, арифметические операции, операция sizeof и, как ни странно, операция запятая не вызывают возражений транслятора (транслятор и считать умеет, и сравнивать, он и размеры определяет, а также понимает, какое значение следует присвоить выражению, содержащему символ операции запятая).
Операторы C++
Согласно принятой нами терминологии, любое законченное предложение на языке C++ называется оператором. Рассмотрим множество БНФ, определяющих синтаксис операторов.
Оператор ::= ОператорВыражение
::= Объявление
::= СоставнойОператор
::= ПомеченныйОператор
::= ОператорПерехода
::= ВыбирающийОператор
::= ОператорЦикла
ОператорВыражение ::= [Выражение];
Судя по последней форме Бэкуса-Наура, любое правильно построенное выражение (построенное по правилам грамматики), заканчивающееся точкой с запятой, является оператором C++.
Мы уже второй раз сталкиваемся с пустым оператором (достаточно внимательно посмотреть на последнюю форму Бэкуса-Наура). Первый раз мы встретили пустой оператор при анализе объявления. Ничего удивительного. Объявление - это тоже оператор.
Пустой оператор имеет особое назначение. Он используется везде, где по правилам синтаксиса обязательно требуются операторы, а по алгоритму раазрабатываемой программы не нужно ни одного. Эти ситуации подробно будут рассмотрены ниже.
Оператор объявления мы уже рассмотрели ранее. На очереди - составной оператор.
СоставнойОператор ::= {[СписокОператоров]}
Что такое список операторов - также уже известно. Судя по последней БНФ, составной оператор (даже пустоой) всегда начинается разделителем { и завершается разделителем }. Кроме того, составной оператор может быть абсолютно пустым (между двумя фигурными скобками может вообще ничего не стоять).
Так что конструкция
{};
однозначно воспринимается транслятором как последовательность, состоящая из двух операторов - пустого составного оператора и простого пустого.
Оператор return. Точка вызова и точка возврата
Нам уже известна операция вызова функции и синтаксис постфиксного выражения, обеспечивающего ввызов. Можно довольно просто представить внешний вид оператора вызова функции. Это оператор-выражение произвольной сложности, в состав которого входит выражение вызова функции. Любое выражение имеет значение и тип. Значение выражения вычисляется в ходе выполнения соответствующего программного кода.
Для каждого выражения существует момент начала вычисления значения. Этот момент характеризуется соответствующими значениями регистров процессора и состоянием памяти компьютера. Это обстоятельство позволяет определить гипотетическую точку начала выполнения выражения. На листинге программы эта точка располагается обычно слева, но она может быть расположена и справа от соответствующего выражения. Расположение этой точки зависит от многих обстоятельств. В том числе, от приоритета выполняемых операций и от порядка вычисления выражения, который зависит от входящей в выражение операции.
Мы можем указать эту точку на листинге программы лишь благодаря тому обстоятельству, что транслятор обеспечивает строгое функциональное соответствие множества команд ассемблера и программного кода.
Точка завершения выполнения выражения соответствует моменту завершения вычисления значения и на листинге программы располагается справа или соответственно слева от вычисляемого выражения. В точке завершения становится известно значение выражения.
Если выражение является выражением вызова функции, точка завершения выполнения выражения называется точкой возврата из функции.
Так вот оператор return немедленно прекращает выполнение операторов в теле функции и передаёт управление в точку возврата. Поскольку вызов функции является выражением, точка возврата имеет значение. Это значение определяется значением выражения, которое обычно располагается непосредственно за оператором возврата return. Тип возвращаемого значения должен соответствовать типу, который указывается спецификатором определения в объявлении и определении функции.
Если в качестве спецификатора объявления в определении и объявлении функции используется ключевое слово void, оператор return в теле этой функции используется без выражения. В этом случае выражение вызова функции оказывается выражением типа void, а значение выражения вызова в точке возврата оказывается неопределённым. Такое выражение не может входить в состав выражений более сложной структуры в качестве операнда выражения, поскольку значение всего выражения оказывается неопределённым.
Выражение с неопределённым значением (выражение вызова функции типа void) может выступать лишь в качестве выражения-оператора. Главное - это не забыть поставить в конце этого выражения разделитель ';', который и превращает это выражение в оператор.
Выбирающий оператор
ВыбирающийОператор ::= if (Выражение) Оператор [else Оператор]
::= switch (Выражение) Оператор
Определение понятия оператора выбора начнём с важного ограничения. Выражение в скобках после ключевых слов if и switch являются обязательными выражениями. От их значения зависит выполнение тела оператора выбора. Так что в этом месте нельзя использовать выражения с неопределённым значением - выражения вызова функции, возвращающей неопределённое значение.
Операторы выбора определяют один из возможных путей выполнения программы.
Выбирающий оператор if имеет собственное название. Его называют условным оператором.
В ходе выполнения условного оператора if вычисляется значение выражения, стоящего в скобках после ключевого слова if. В том случае, если это выражение оказывается не равным нулю, выполняется первый стоящий за условием оператор. Если же значение условия оказывается равным нулю, то управление передаётся оператору, стоящему после ключевого слова else, либо следующему за условным оператором оператору.
if (i)
{int k = 1;}
else
{int l = 10;}
Этот пример условного оператора интересен тем, что операторы, выполняемые после проверки условия (значение переменной i), являются операторами объявления. В ходе выполнения одного из этих операторов объявления в памяти создаётся объект типа int с именем k и значением 1, либо объект типа int с именем l и значением 10. Областью действия этих имён являются блоки операторов, заключающих данные операторы объявления. Эти объекты имеют очень короткое время жизни. Сразу после передачи управления за пределы блока эти объекты уничтожаются. Ситуация не меняется, если условный оператор переписывается следующим образом:
if (i)
int k = 1;
else
int l = 10;
При этом область действия имён и время жизни объектов остаются прежними. Это позволяет несколько расширить первоначальное определение блока: операторы, входящие в выбирающий оператор также считаются блоком операторов.
Подобное обстоятельство являлось причиной стремления запретить использование операторов объявлений в теле условного оператора. В справочном руководстве по C++ Б.Строуструпа по этому поводу сказано, что в случае, если объявление является единственным оператором, то в случае его выполнения возникает имя "с непонятной областью действия".
Однако запрещение использования оператора объявления в условном операторе влечёт за собой очень много ещё более непонятных последствий. Именно по этой причине в последних реализациях C++ это ограничение не выполняется. Проблема области действия является проблемой из области семантики языка и не должна оказывать влияния на синтаксис оператора.
Выбирающий оператор switch или оператор выбора предназначается для организации выбора из множества различных вариантов.
Выражение, стоящее за ключевым словом switch обязательно должно быть выражением целого типа. Транслятор строго следит за этим. Это связано с тем, что в теле оператора могут встречаться помеченные операторы с метками, состоящими из ключевого слова case и представленного константным выражением значения. Так вот тип switch-выражения должен совпадать с типом константных выражений меток.
Синтаксис выбирающего оператора допускает пустой составной оператор и пустой оператор в качестве операторов, следующих за условием выбирающего оператора:
switch (i) ; // Синтаксически правильный оператор выбора…
switch (j) {} // Ещё один… Такой же бесполезный и правильный…
switch (r) i++;// Этот правильный оператор также не работает.
В теле условного оператора в качестве оператора может быть использовано определение:
switch (k) {
int q, w, e;
}
Этот оператор выбора содержит определения объектов с именами q, w, e.
Туда могут также входить операторы произвольной сложности и конфигурации:
switch (k) {
int q, w, e;
q = 10; e = 15;
w = q + e;
}
Входить-то они могут, а вот выполняться в процессе выполнения условного оператора не будут!
А вот включение в оператор выбора операторов определений с одновременной инициализацией создаваемого объекта недопустимо. И об этом мы уже говорили. Оно вызывает сообщение об ошибке независимо от того, в каком месте оператора выбора оно располагается:
switch (k) {
int q = 100, w = 255, e = 1024; // Ошибка…
default: int r = 100; // Опять ошибка…
}
Дело в том, что в ходе выполнения оператора объявления с одновременной инициализацией создаваемого объекта происходят два события:
во-первых, производится определение переменной, при котором выделяется память под объект соответствующего типа:
int q; int w; int e;
во-вторых, выполняется дополнительное днйствие - нечто эквивалентное оператору присвоения:
q = 100; w = 255; e = 1024;
а вот этого в данном контексте и не разрешается делать! Просто так операторы в теле условного оператора не выполняются.
При этом возникает странная ситуация: создание объекта в памяти со случайным значением оказывает на процесс выполнения программы меньшее влияние, нежели создание того же самого объекта с присвоением ему конкретного значения.
Казалось, логичнее было бы не делать никаких различий между операторами объявления и прочими операторами. Но дело в том, что оператор выбора состоит из одного единственного блока. И нет иного пути создания объекта с именем, область действия которого распространялась бы на всё тело оператора выбора, как разрешение объявления переменных в любой точке оператора выбора. Судя по всему, переменная создаётся до того момента, как начинают выполняться операторы в блоке. Объявление превыше всего!
И всё же, какие операторы выполняются в теле оператора выбора (разумеется, за исключением объявления без инициализации)? Ответ: все подряд, начиная с одного из помеченных.
Возможно, что помеченного меткой "default:". При этом в теле оператора выбора может быть не более одной такой метки.
switch (val1) default: x++;
А возможно, помеченного меткой "case КонстантноеВыражение :". В теле оператора выбора таких операторов может быть сколь угодно много. Главное, чтобы они различались значениями константных выражений.
Нам уже известно, что является константным выражением и как вычисляется его значение.
Небольшой тест подтверждает факт вычисления значения константного выражения транслятором:
switch (x)
{
int t;// Об этом операторе уже говорили…
case 1+2: y = 10;
case 3: y = 4; t = 100; // На этом месте транслятор
//сообщит об ошибке. А откуда он узнал, что 1+2 == 3 ?
// Сам сосчитал…
default: cout << y << endl;
}
А вот пример, который показывает, каким оразом вычисляется выражение, содержащее операцию запятая:
int XXX = 2;
switch (XXX)
{
case 1,2: cout << "1,2"; break;
case 2,1: cout << "2,1"; break;
}
Константное выражение принимает значение правого операнда, на экран дисплея выводится первое сообщение.
И ещё один вопрос. Почему множество значений выражения, располагаемого после switch, ограничивается целыми числами. Можно было бы разрешить использование выражения без ограничения на область значений. Это ограничение связано с использованием константных выражений. Каждый оператор в теле оператора выбора выполняется только при строго определённых неизменных условиях. А это означает, что выражения должны представлять константные выражения. Константные выражения в C++ являются выражениями целочисленного типа (константных выражений плавающего типа в C++ просто не существует).
Схема выполнения оператора switch
Рассмотрим, наконец, схему выполнения оператора switch:
- вычисляется выражение в круглых скобках после оператора switch (предварительная стадия);
- это значение последовательно сравнивается со значениями константных выражений за метками case (стадия определения начальной точки выполнения оператора);
- если значения совпадают, управление передаётся соответствующему помеченному оператору (стадия выполнения);
- если ни одно значение не совпадает и в теле оператора case есть оператор, помеченный меткой default, управление передаётся этому оператору (но даже в этом случае сочетание объявления с инициализацией недопустимо!) (стадия выполнения);
- если ни одно значение не совпадает, и в теле оператора case нет оператора, помеченного меткой default, управление передаётся оператору, следующему за оператором switch (стадия выполнения).
Метки case и default в теле оператора switch используются лишь при начальной проверке, на стадии определения начальной точки выполнения тела оператора. На стадии выполнения все операторы от точки выполнения и до конца тела оператора выполняются независимо от меток, если только какой-нибудь из операторов не передаст управление за пределы оператора выбора. Таким образом, программист сам должен заботиться о выходе из оператора выбора, если это необходимо. Чаще всего для этой цели используется оператор break.
В этом разделе нам остаётся обсудить ещё один вопрос. Это вопрос о соответствии оператора выбора и условного оператора. На первый взгляд, оператор выбора легко может быть переписан в виде условного оператора. Рассмотрим в качестве примера следующий оператор выбора:
int intXXX;
:::::
switch (intXXX)
{
case 1:
int intYYY;
/* Здесь инициализация переменной запрещена, однако определение
переменной должно выполняться. */
break;
case 2:
case 3:
intYYY = 0;
break;
}
Казалось бы, этот оператор выбора может быть переписан в виде условного оператора:
int intXXX;
:::::
if (intXXX == 1)
{
int intYYY = 0; // Здесь допускается инициализация!
}
else if (intXXX == 2 || intXXX == 3)
{
intYYY = 0;
/*
Здесь ошибка! Переменная intYYY не объявлялась в этом блоке операторов.
*/
}
Если в операторе выбора используется локальная переменная, то для всего множества помеченных операторов из блока оператора выбора требуется единственное объявление этой переменной (лишь бы она не инициализировалась).
В условном операторе переменная должна объявляться в каждом блоке.
Ситуация с необъявленной в одном из блоков условного оператора переменной может быть решена путём создания внешнего блока, в который можно перенести объявления переменных, которые должны использоваться в блоках условного оператора.
int intXXX;
:::::
if (1)
/*
Этот условный оператор определяет внешний блок операторов, в котором
располагается объявление переменной intYYY.
*/
{
int intYYY = 0;
if (intXXX == 1)
{
intYYY = 0;
}
else if (intXXX == 2 || intXXX == 3)
{
intYYY = 0;
}
}
Нам удалось преодолеть проблемы, связанные с областями действия, пространствами и областями видимости имён путём построения сложной системы вложенных блоков операторов. Простой одноблочный оператор выбора, содержащий N помеченных операторов, моделируется с помощью N+1 блока условных операторов.
Однако каждый оператор хорош на своём месте.
Операторы цикла
Операторы цикла задают многократное исполнение.
ОператорЦикла ::= while (Выражение) Оператор
::=
for (ОператорИнициализацииFor [Выражение] ; [Выражение] )Оператор
::= do Оператор while (Выражение);
ОператорИнициализацииFor ::= ОператорВыражение
::= Объявление
Прежде всего, отметим эквивалентные формы операторов цикла.
Оператор
for (ОператорИнициализацииFor [ВыражениеA] ;[ВыражениеB]) Оператор
эквивалентен оператору
ОператорИнициализацииFor while (ВыражениеA)
{
Оператор
ВыражениеB ;
}
Эти операторы называются операторами с предусловием.
Здесь следует обратить внимание на точку с запятой после выражения в теле оператора цикла while. Здесь выражение становится оператором.
А вот условие продолжения цикла в операторе цикла while опускать нельзя. В крайнем случае, это условие может быть представлено целочисленным ненулевым литералом.
Следует также обратить внимание на точку с запятой между двумя выражениями цикла for. В последнем примере они представлены символами ВыражениеA и ВыражениеB. Перед нами классический пример разделителя.
ОператорИнициализацииFor является обязательным элементом заголовка цикла. Обязательный оператор вполне может быть пустым.
Оператор цикла for
Рассмотрим пример оператора цикла for:
for ( ; ; ) ;
Его заголовок состоит из пустого оператора (ему соответствует первая точка с запятой) и разделителя, который разделяет два пустых выражения. Тело цикла - пустой оператор.
Пустое выражение, определяющее условие выполнения цикла for интерпретируется как всегда истинное условие. Отсутствие условия выполнения предполагает безусловное выполнение.
Синтаксис C++ накладывает на структуру нетерминального символа ОператорИнициализацииFor жёсткие ограничения:
- это всегда единственный оператор,
- он не может быть блоком операторов,
- единственным средством усложнения его структуры служит операция запятая.
Эта операция управляет последовательностью выполнения образующих оператор выражений.
Рассмотрим принципы работы этого оператора. Цикл состоит из четырёх этапов.
- Прежде всего, выполняется оператор инициализации цикла. Если он не пустой, выражение за выражением, слева направо. Этот этап можно назвать этапом инициализации цикла. Он выполняется один раз, в самом начале работы цикла.
- Затем вычисляется значение выражения, которое располагается слева от оператора инициализации. Это выражение называется выражением условия продолжения цикла. Сам этап можно назвать этапом определения условий выполнимости.
- Если значение этого выражения отлично от нуля (т.е. истинно), выполняется оператор цикла. Этот этап можно назвать этапом выполнения тела цикла.
- После этого вычисляются значения выражений, которые располагаются слева от выражения условия продолжения цикла. Этот этап можно назвать этапом вычисления шага цикла.
- На последних двух этапах могут измениться значения ранее определённых переменных. А потому следующий цикл повторяется с этапа определения условий выполнимости.
Оператор инициализации цикла - это всего лишь название оператора, который располагается в заголовке цикла. Этот оператор может инициализировать переменные, если того требует алгоритм, в этот оператор могут входить любые выражения, в конце концов, он может быть пустым. Транслятору важен синтаксис оператора, а не то, как будет выполняться данный оператор цикла.
int qwe;
for (qwe < 10; ; ) {}
// Оператор инициализатор построен на основе выражения сравнения.
for (this; ; ) {}
// Оператор инициализатор образован первичным выражением this.
for (qwe; ; ) {}
// Оператор инициализатор образован первичным выражением qwe.
Ещё пример:
int i = 0;
int j;
int val1 = 0;
int val2;
:::::
i = 25;
j = i*2;
:::::
for ( ; i < 100; i++, j--)
{
val1 = i;
val2 - j;
}
Мы имеем оператор цикла for, оператор инициализации которого пуст, а условие выполнения цикла основывается на значении переменной, которая была ранее объявлена и проинициализирована. Заголовок цикла является центром управления цикла. Управление циклом основывается на внешней по отношению к телу цикла информации.
Ещё пример:
for ( int i = 25, int j = i*2; i < 100; i++, j--)
{
val1 = i;
val2 - j;
}
Заголовок нового оператора содержит пару выражений, связанных операцией запятая. Тело оператора представляет всё тот же блок операторов. Что может содержать тело оператора? Любые операторы. Всё, что может называться операторами. От самого простого пустого оператора, до блоков операторов произвольной сложности! Этот блок живёт по своим законам. В нём можно объявлять переменные и константы, а поскольку в нём определена собственная область действия имён, то объявленные в блоке переменные и константы могут скрывать одноимённые объекты с более широкой областью действия имён.
А вот использование блока в операторе инициализации привело бы к дополнительным трудноразрешимым проблемам с новыми областями действия и видимости имён, вводимых в операторе инициализации. Часть переменных могла бы оказаться невидимой в теле оператора цикла.
Операция запятая позволяет в единственном операторе сделать всё то, для чего обычно используется блок операторов. В качестве составных элементов (в буквальном смысле выражений-операторов) этого оператора могут использоваться даже объявления. Таким образом, в заголовке оператора цикла for можно объявлять и определять переменные.
Примеры использования оператора цикла for
Рассмотрим несколько примеров. Так, в ходе выполнения оператора цикла
int i;
for (i = 0; i < 10; i++)
{
int j = 0; j += i;
}
десять раз будет выполняться оператор определения переменной j. Каждый раз это будут новые объекты. Каждый раз новой переменной заново будет присваиваться новое значение одной и той же переменной i, объявленной непосредственно перед оператором цикла for.
Объявление переменной i можно расположить непосредственно в теле оператора-инициализатора цикла:
for (int i = 0; i < 10; i++)
{
int j = 0; j += i;
}
И здесь возникает одна проблема. Дело в том, что тело оператора цикла for (оператор или блок операторов) имеет ограниченную область действия имён. А область действия имени, объявленного в операторе-инициализаторе, оказывается шире этой области.
Заголовок цикла for в C++ - центр управления циклом. Здесь следят за внешним миром, за тем, что происходит вне цикла. И потому все обращения к переменным и даже их новые объявления в заголовке цикла относятся к "внешней" области видимости. Следствием такого допущения (его преимущества далеко не очевидны) является правило соотнесения имени, объявленного в заголовке и области его действия.
По отношению к объявлению переменной в заголовке оператора цикла for, правило соотнесения гласит, что область действия имени, объявленного в операторе инициализации цикла for, располагается в блоке, содержащем данный оператор цикла for.
А вот область действия имени переменной j при этом остаётся прежней.
В теле оператора for может быть определена одноимённая переменная:
for (int i = 0; i < 10; i++)
{
int i = 0; i += i;
}
Пространство имени переменной в операторе цикла ограничено блоком из двух операторов. В этом пространстве переменная, объявленная в заголовке, оказывается скрытой одноимённой переменной.
Десять раз переменная i из оператора-инициализатора цикла будет заслоняться одноимённой переменной из оператора тела цикла. И всякий раз к нулю будет прибавляться нуль.
Ещё один пример. Два расположенных друг за другом оператора цикла for содержат ошибку
for (int i = 0, int j = 0; i < 100; i++, j--)
{
// Операторы первого цикла.
}
for (int i = 0, int k = 250; i < 100; i++, k--)
{
// Операторы второго цикла.
}
Всё дело в том, что, согласно правилу соотнесения имён и областей действия имён в операторе цикла for, объявления переменных в заголовке цикла оказываются в общем пространстве имён. А почему, собственно, не приписать переменные, объявленные в заголовке цикла блоку, составляющему тело цикла? У каждого из альтернативных вариантов соотнесения имеются свои достоинства и недостатки. Однако выбор сделан, что неизбежно ведёт к конфликту имён и воспринимается как попытка переобъявления ранее объявленной переменной.
Эту самую пару операторов for можно переписать, например, следующим образом:
for (int i = 0, int j = 0; i < 100; i++, j--)
{
// Здесь располагаются операторы первого цикла.
}
for (i = 0, int k = 250; i < 100; i++, k--)
{
// Здесь располагаются операторы второго цикла.
}
Здесь нет ошибок, но при чтении программы может потребоваться дополнительное время для того, чтобы понять, откуда берётся имя для выражения присвоения i = 0 во втором операторе цикла. Кроме того, если предположить, что операторы цикла в данном контексте реализуют независимые шаги какого-либо алгоритма, то почему попытка перемены мест пары абсолютно независимых операторов сопровождается сообщением об ошибке:
for (i = 0, int k = 250; i < 100; i++, k--)
{
// Здесь располагаются операторы второго цикла.
}
for (int i = 0, int j = 0; i < 100; i++, j--)
{
// Здесь располагаются операторы первого цикла.
}
Очевидно, что в первом операторе оказывается необъявленной переменная i. Возможно, что не очень удобно, однако, в противном случае, в центре управления циклом трудно буден следить за внешними событиями. В конце концов, никто не заставляет программиста располагать в операторе инициализации объявления переменных. Исходная пара операторов может быть с успехом переписана следующим образом:
int i, j, k;
:::::
for (i = 0, k = 250; i < 100; i++, k--)
{
// Здесь располагаются операторы второго цикла.
}
for (i = 0, j = 0; i < 100; i++, j--)
{
// Здесь располагаются операторы первого цикла.
}
А вот ещё один довольно странный оператор цикла, в котором, тем не менее, абсолютно корректно соблюдены принципы областей действия имён, областей видимости имён, а также соглашения о соотнесении имён и областей их действия:
for (int x; x < 10; x++) {int x = 0; x++;}
Так что не забываем о том, что область действия имён в заголовке цикла шире от области действия имён в теле цикла. И вообще, если можно, избавляемся от объявлений в заголовке оператора цикла.
Оператор цикла do … while называется оператором цикла с постусловием. От циклов с предусловием он отличается тем, что сначала выполняется оператор (возможно, составной), а затем проверяется условие выполнения цикла, представленное выражением, которое располагается в скобках после ключевого слова while. В зависимости от значения этого выражения возобновляется выполнение оператора. Таким образом, всегда, по крайней мере один раз, гарантируется выполнение оператора цикла.
int XXX = 0;
do {cout << XXX << endl; XXX++;} while (XXX < 0);
Указатель void *
В C++ существует специальный тип указателя, который называется указателем на неопределённый тип. Для определения такого указателя вместо имени типа используется ключевое слово void в сочетании с описателем, перед которым располагается символ ptrОперации *.
void *UndefPoint;С одной стороны, объявленная подобным образом переменная также является объектом определённого типа - типа указатель на объект неопределённого типа. В Borland C++ 4.5 имя UndefPoint действительно ссылается на объект размером в 32 бита со структурой, которая позволяет сохранять адреса.
Но, с другой стороны, для объекта типа указатель на объект неопределённого типа отсутствует информация о размерах и внутренней структуре адресуемого участка памяти. Из-за этого не могут быть определены какие-либо операции для преобразования значений.
Поэтому переменной UndefPoint невозможно присвоить никаких значений без явного преобразования этих значений к определённому типу указателя.
UndefPoint = 0xb8000000; // Такое присвоение недопустимо.Подобный запрет является вынужденной мерой предосторожности. Если разрешить такое присвоение, то неизвестно, как поступать в случае, когда потребуется изменить значение переменной UndefPoint, например, с помощью операции инкрементации.
UndefPoint++; // Для типа void * нет такой операции…Эта операция (как и любая другая для типа указатель на объект неопределённого типа) не определена. И для того, чтобы не разбираться со всеми операциями по отдельности, лучше пресечь подобные недоразумения "в корне", то есть на стадии присвоения значения.
Объектам типа указатель на объект неопределённого типа в качестве значений разрешается присваивать значения лишь в сочетании с операцией явного преобразования типа.
В этом случае указатель на объект неопределённого типа становится обычным указателем на объект какого-либо конкретного типа. Со всеми вытекающими отсюда последствиями.
Но и тогда надо постоянно напоминать транслятору о том типе данных, который в данный момент представляется указателем на объект неопределённого типа:
int mmm = 10; pUndefPointer = (int *)&mmm; pUndefPointer выступает в роли указателя на объект типа int. (*(int *)pUndefPointer)++;Для указателя на объект неопределённого типа не существует способа непосредственной перенастройки указателя на следующий объект с помощью операции инкрементации. В операторе, реализующем операции инкрементации и декрементации, только с помощью операций явного преобразования типа можно сообщить транслятору величину, на которую требуется изменить первоначальное значение указателя.
pUndefPointer++; // Это неверно, инкрементация не определена… (int *)pUndefPointer++; // И так тоже ничего не получается… ((int *)pUndefPointer)++; // А так хорошо… Сколько скобок! ++(int *)pUndefPointer; // И вот так тоже хорошо…С помощью операции разыменования и с дополнительной операцией явного преобразования типа изменили значение переменной mmm.
pUndefPointer = (int *)pUndefPointer + sizeof(int); Теперь перенастроили указатель на следующий объект типа int. pUndefPointer = (int *)pUndefPointer + 1;И получаем тот же самый результат.
Специфика указателя на объект неопределённого типа позволяет выполнять достаточно нетривиальные преобразования:
(*(char *)pUndefPointer)++;А как изменится значение переменной mmm в этом случае?
pUndefPointer = (char *)pUndefPointer + 1;Указатель перенастроился на объект типа char. То есть просто сдвинулся на 1байт.
Работа с указателями на объекты определённого типа не требует такого педантичного напоминания о типе объектов, на которые настроен указатель. Транслятор об этом не забывает.
int * pInt; int mmm = 10; pInt = &mmm; // Настроили указатель. pInt++; // Перешли к очередному объекту. *pInt++; // Изменили значение объекта, идущего следом за // переменной mmm.Напомним, что происходит в ходе выполнения этого оператора.
- после выполнения операции разыменования вычисляется значение (адрес объекта mmm),
- это значение становится значением выражения,
- после чего это значение увеличивается на величину, кратную размеру того типа данного, для которого был объявлен указатель.
Операции явного преобразования типов позволяют присваивать указателям в качестве значений адреса объектов типов, отличных от того типа объектов, для которого был объявлен указатель:
int mmm = 10; char ccc = 'X'; float fff = 123.45; pInt = &mmm; pNullInt = (int *)&ccc; pNullInt = (int *)&fff; // Здесь будет выдано предупреждение об // опасном преобразовании.Это обстоятельство имеет определённые последствия, которые связаны с тем, что все преобразования над значениями указателей будут производиться без учёта особенностей структуры тех объектов, на которые указатель в самом начале был настроен.
При этом ответственность за результаты подобных преобразований возлагается на программиста.
Массивы. Синтаксис объявления
Рассмотрим новые формы Бэкуса-Наура, которые дополняют уже известные понятия описателя и инициализатора.
Объявление ::= [СписокСпецификаторовОбъявления] [СписокОписателей]; Описатель ::= Описатель [Инициализатор] Описатель ::= Описатель[[КонстантноеВыражение]] ::= ***** Инициализатор ::= = {СписокИнициализаторов [,]} СписокИнициализаторов ::= Выражение ::= СписокИнициализаторов, Выражение ::= {СписокИнициализаторов [,]}Теперь мы располагаем набором синтаксических средств для объявления массивов. Массивы представляют собой производные типы (указатели также относятся к производным типам).
Объект типа "массив элементов заданного типа" представляет последовательность объектов этого самого типа, объединённых одним общим именем. Количество элементов массива является важной характеристикой самого массива, но не самого типа. Эта характеристика называется размерностью массива.
Приведём примеры объявления и определения массивов.
extern int intArray_1[];Объявлен (именно объявлен - об этом говорит спецификатор extern) массив типа int, имя массива - intArray_1, разделители [] указывают на то, что перед нами объявление массива.
int intArray_2[10];А это уже определение массива. Всё тот же тип int, имя массива - intArray, между разделителями [ и ] находится константное выражение, значение которого определяет размерность массива.
Требование синтаксиса по поводу константного выражения между разделителями в определении массива может быть объяснено лишь тем, что информация о количестве элементов массива требуется до момента начала выполнения программы.
int intArray_3[] = {1,2,3}; // Это также определение массива.Количество элементов массива становится известным транслятору при анализе инициализатора. Элементам массива присваиваются соответствующие значения из списка инициализаторов.
Ещё одна форма определения массива:
int intArray_4[3] = {1,2,3};В этом определении массива важно, чтобы количество элементов в инициализаторе массива не превышало значение константного выражения в описателе массива.
В результате выполнения этого оператора в памяти выделяется область, достаточная для размещения трёх объектов-представителей типа int. Участку присваивается имя intArray_4. Элементы инициализируются значениями, входящими в состав инициализатора.
Возможна частичная инициализация массива. При этом значения получают первые элементы массива:
int intArray_5[3] = {1,2};В этом определении массива означены лишь первые два элемента массива. Значение последнего элемента массива в общем случае не определено.
Здесь нужно отметить одну интересную особенность синтаксиса инициализатора массива. Речь идёт о необязательной запятой в конце списка инициализаторов. По-видимому, её назначение заключается в том, чтобы указывать на факт частичной инициализации массива.
Действительно, последний вариант (частично) инициализирующего оператора определения массива выглядит нагляднее:
int intArray_5[3] = {1,2,};Последняя запятая предупреждает о факте частичной инициализации массива. Затраты на связывание запятой в конце списка инициализаторов со строго определённым контекстом частичной инициализации оказываются столь значительными, что последняя запятая традиционно (по крайней мере со времени выхода "Справочного руководства по языку программирования C++") оказывается всего лишь необязательным элементом любой (в том числе и полной) инициализации.
int intArray_6[3] = {1,2,3}; int intArray_6[3] = {1,2,3,};// Полная инициализация с запятой… int intArray_6[] = {1,2,3}; int intArray_6[] = {1,2,3,};Между этими операторами не существует никакой разницы.
А вот в таком контексте
int intArray_6[3] = {1,2,}; // Частичная инициализация массива из трёх элементов…Последняя запятая в фигурных скобках - не более как полезное украшение. Что-то недосказанное таится в таком операторе присвоения…
int intArray_7[];А вот это некорректное объявление. Без спецификатора extern транслятор воспринимает это как ошибку. В скором времени мы обсудим причину этого явления.
Основные свойства массивов
Всё, что здесь обсуждается, имеет, прежде всего, отношение к версии языка Borland C++ 4.5. Однако маловероятно, что в других версиях языка массив обладает принципиально другими свойствами.
Первое специфическое свойство массивов заключается в том, что определение массива предполагает обязательное указание его размеров. Зафиксировать размер массива можно различными способами (о них мы уже говорили), однако это необходимо сделать непосредственно в момент его объявления, в соответствующем операторе объявления.
В модулях многомодульной программы массив определяется в одном из модулей (в главном модуле) программы. В остальных модулях при объявлении этого массива используется спецификатор extern. Подобное объявление может быть включено и в главный модуль. Главное, чтобы транслятор мог различить объявления и собственно определение.
В объявлениях со спецификатором extern можно указывать произвольные размеры объявляемого массива (лишь бы они были описаны в виде константного выражения), а можно их и не указывать вовсе - транслятор всё равно их не читает.
int intArray1[10] = {0,1,2,3,4,5,6,7,8,9};
extern intArray1[];
extern intArray1[1000];
/*Казалось бы, если транслятор всё равно не читает значение
константного выражения в объявлении, то почему бы там не
записать выражение, содержащее переменные?*/
int ArrVal = 99;
extern intArray1[ArrVal + 1];
/*Однако этого сделать нельзя. ArrVal не константное выражение.*/
Но зато он очень строго следит за попытками повторной инициализации.
extern intArray1[10] = {9,9,9,};
/*Здесь будет зафиксирована ошибка. Хотя, если в объявлении не проверяется
размерность массива, то какой смысл реагировать на инициализацию…*/
Второе свойство массивов заключается в том, что объекту типа массив невозможно присвоить никакого другого значения, даже если это значение является массивом аналогичного типа и размерности:
char chArray_1[6];
char chArray_2[] = {'q', 'w', 'e', 'r', 't', 'y'};
Попытка использовать оператор присвоения вида
chArray_1 = chArray_2;
вызывает сообщение об ошибке, суть которой сводится к уведомлению, что выражение chArray_1 не является леводопустимым выражением.
Следует заметить, что подобным образом ведёт себя и константный указатель, с которым мы познакомились раньше. Он также требует немедленной инициализации (это его единственный шанс получить определённое значение) и не допускает последующего изменения собственного значения.
Часто указатель один "знает" место расположения участка памяти, выделенного операциями или функциями распределения памяти. Изменение значения этого указателя приводит к потере ссылки на расположенный в динамической памяти объект. Это означает, что соответствующая область памяти на всё оставшееся время выполнения программы оказывается недоступной.
По аналогичной причине невозможна и операция присвоения, операндами которой являются имена массивов.
Операторы
intArray1 = intArray2;
intArray1[] = intArray2[];
не допускаются транслятором исключительно по той причине, что имя массива аналогично константному указателю. Оно является неизменяемым l-выражением, следовательно, не является леводопустимым выражением и не может располагаться слева от операции присвоения.
Заметим, что при создании в динамической памяти с помощью выражения размещения безымянных массивов объектов (при инициализации указателей на массивы) инициализаторы не допускаются. Инициализатор в выражении размещения может проинициализировать только один объект. И дело здесь не в особых свойствах выражения размещения, а в особенностях языка и самого процесса трансляции.
Рассмотрим процессы, происходящие при выполнении оператора определения массива. Они во многом аналогичны процессам, происходящим при определении константного указателя:
- по константному выражению в описателе или на основе информации в инициализаторе определяется размер необходимой области памяти. Здесь сразу уже необходима полная информация о размерности массива. Размер области памяти составляет равняется произведению размера элемента массива на размерность массива,
- выделяется память,
- адрес выделенной области памяти присваивается объекту, который по своим характеристикам близок константному указателю (хотя это объект совершенно особого типа).
Теперь можно вспомнить объявление, которое было рассмотрено нами в одном из прошлых разделов. Объявление массива
int intArray_7[];
воспринимается транслятором как ошибочное объявление исключительно по причине функционального сходства между объявлением массива и объявлением константного указателя. Массив, как и константный указатель должен быть проинициализирован в момент объявления.
Массив и константный указатель
Несмотря на некоторое сходство с константным указателем, массив является особым типом данных. В этом разделе мы рассмотрим основные отличия массива и константного указателя.
Прежде всего, рассмотрим варианты инициализации указателя:
char * const pcchVal_1 = chArray_2;
char * const pcchVal_2 = new char[5];
char * const pcchVal_3 = (char *) malloc(5*sizeof(char));
Для инициализации последнего константного указателя был использован вызов функции malloc().
Каждый из этих операторов демонстрирует один из трёх возможных способов инициализации константного указателя: непосредственное присвоение значения, использование операции new, вызов функции. Операция new и функции распределения памяти, выделяют соответствующие участки памяти и возвращают начальный адрес выделенной области памяти. Ни один из этих способов не подходит для инициализации массива.
В свою очередь, при определении константных указателей не используются уже известные инициализаторы массивов с явным указанием размерности и списком инициализаторов.
Определим массив и константный указатель на область памяти:
int intArray[5]= {11,22,33,44,55};
int * const pciVal = new int[5];
К константным указателям и массивам применимы одни и те же методы навигации, связанные с использованием операции индексации:
intArray[-25] = 10;
*(intArray + 25) = 10;
pciVal[2] = 100;
*(pciVal + 5) = 100;
А теперь применим операцию sizeof по отношению к проинициализированным указателям:
cout << "pciVal:"<< sizeof(pciVal)<<
" intArray:"<< sizeof(intArray);
Для Borland C++ 4.5, операция sizeof покажет размер области памяти, занимаемой указателем (4 байта) и размер массива (размер элемента * размерность массива)==(10 байт). Операция sizeof различает указатели и имена массивов.
Кроме того, следующий тест также даёт различные результаты.
if (intArray == &intArray)
cout << "Yes, массив." << endl;
else
cout << "No, массив." << endl;
if (pciVal == &pciVal)
cout << "Yes, указатель. " << endl;
else
cout << "No, указатель." << endl;
Результат выполнения:
Yes, массив.
No, указатель.
Значение указателя, представляющего массив, совпадает с адресом первого элемента массива.
Значением указателя, проинициализированного с помощью выражения размещения, является адрес начала этой области. Сам указатель как объект обладает своим собственным адресом.
Интересно, что сравнение значения указателя с результатом выполнения операции взятия адреса не является абсолютно корректным с точки зрения соответствия типов. Операция взятия адреса возвращает лишь определённое значение адреса. И при этом после выполнения этой операции как бы ничего не известно о типе операнда, чей адрес определяли с помощью этой самой операции взятия адреса. Транслятор отслеживает это нарушение принципа соответствия типов и выдаёт предупреждение "Nonportable pointer comparison".
Поскольку это всего лишь предупреждение, выполнение процесса трансляции не прерывается и загрузочный модуль, построенный на основе этого программного кода, корректно выполняется. "Успокоить" транслятор можно с помощью операции явного преобразования типа, которая отключает контроль над типами:
if (intArray == (int *)&intArray)
cout << "Yes";
else
cout << "No";
Массив констант
Как уже известно, имя массива является константным указателем. Именно поэтому и невозможно копирование массивов с помощью простого оператора присвоения. Константный указатель "охраняет" область памяти, выделенную для размещения данного массива. При этом значения элементов массива можно изменять в ходе выполнения программы. Защитить их от изменения можно с помощью дополнительного спецификатора типа const. При этом массив должен быть проинициализирован непосредственно в момент определения:
const int cIntArray[] = {0,1,2,3,4,5,6,7,8,9};Это аналог константного указателя на массив констант. Попытки изменения значения элементов массива пресекаются на этапе компиляции.
cIntArray[5] = 111; // Ошибка.А вот от скрытого изменения значения элементы массива констант уберечь не удаётся.
const char cCH[] = "0123456789"; char CH[] = "0123456789"; CH[15] = 'X'; /* Выполнение этого оператора ведёт к изменению строки cCH. */ cout << cCH << endl;Транслятор не занимается проверкой корректности выполняемых операций. На этапе выполнения программы язык C++ не предоставляет никаких средств защиты данных.
Многомерный массив
Многомерные массивы в C++ рассматриваются как массивы, элементами которых являются массивы.
Определение многомерного массива должно содержать информацию о типе, размерности и количестве элементов каждой размерности.
int MyArray1[10]; // Одномерный массив размерности 10.
int MyArray2[20][10]; // 20 одномерных массивов размерности 10.
int MyArray3[30][20][10]; // 30 двумерных массивов размерности 20*10.
По крайней мере, для Borland C++ 4.5, элементы многомерного массива располагаются в памяти в порядке возрастания самого правого индекса, т.е. самый младший адрес имеют элементы
MyArray1[0],
MyArray2[0][0],
MyArray3[0][0][0],
затем элементы
MyArray1[1],
MyArray2[0][1],
MyArray3[0][0][1]
и т.д.
Многомерный массив подобно одномерному массиву может быть проинициализирован с помощью списка инициализаторов. Первыми инициализируются элементы с самыми маленькими индексами:
int MyArray[3][3][3] = {0,1,2,3,4,5,6,7,8,9,10,11};
Начальные значения получают следующие элементы трёхмерного массива:
MyArray[0][0][0] == 0
MyArray[0][0][1] == 1
MyArray[0][0][2] == 2
MyArray[0][1][0] == 3
MyArray[0][1][1] == 4
MyArray[0][1][2] == 5
MyArray[0][2][0] == 6
MyArray[0][2][1] == 7
MyArray[0][2][2] == 8
MyArray[1][0][0] == 9
MyArray[1][0][1] == 10
MyArray[1][0][2] == 11
Остальные элементы массива получают начальные значения в соответствии со статусом массива (в глобальном массиве значения остальных элементов равны 0, в локальном массиве элементам присваиваются неопределённые значения).
Дополнительные фигурные скобки в инициализаторе позволяют инициализировать отдельные фрагменты многомерного массива. Каждая пара фигурных скобок специфицирует значения, относящиеся к одной определённой размерности. Пустые фигурные скобки не допускаются (и это означает, что в C++ реализован жёсткий алгоритм инициализации массивов):
int MyArray[3][3][3] = {
{{0,1}},
{{100},{200,210},{300}},
{{1000},{2000,2100},{3000,3100,3200}}
};
В результате выполнения этого оператора определения будут означены следующие элементы массива MyArray:
MyArray[0][0][0] == 0
MyArray[0][0][1] == 1
MyArray[1][0][0] == 100
MyArray[1][1][0] == 200
MyArray[1][1][1] == 210
MyArray[1][2][0] == 300
MyArray[2][0][0] == 1000
MyArray[2][1][0] == 2000
MyArray[2][1][1] == 2100
MyArray[2][2][0] == 3000
MyArray[2][2][1] == 3100
MyArray[2][2][2] == 3200
По аналогии с одномерным массивом, при явной инициализации массива входящего в состав многомерного массива его самая левая размерность может не указываться. Она определяется на основе инициализатора.
int MyArray[ ][3][3] = {
{{0,1}},
{{100},{200,210},{300}},
{{1000},{2000,2100}}
};
Транслятор понимает, что речь идёт об определении массива размерности 3*3*3.
А в таком случае
int MyArray[ ][3][3] = {
{{0,1}},
{{100},{200,210},{300}},
{{1000},{2000,2100}},
{{10000}}
};
предполагается размерность 4*3*3. В результате MyArray оказывается массивом из четырёх частично проинициализированных двумерных массивов. Следует помнить, что в C++ нет принципиальной разницы между массивом массивов произвольной размерности и обычным одномерным массивом. Потому и простор для творчества в деле инициализации многомерных массивов ограничивается левым индексом.
Директива препроцессора define
Директива define позволяет связать идентификатор (мы будем называть этот идентификатор замещаемой частью) с лексемой (возможно, что пустой!) или последовательностью лексем (строка символов является лексемой, заключённой в двойные кавычки), которую называют строкой замещения или замещающей частью директивы define.
Например,
#define PI 3.14159
Идентификаторы, которые используют для представления констант, называют объявленными или символическими константами. Например, последовательность символов, располагаемая после объявленной константы PI, объявляет константу 3.14159. Препроцессор заменит в оставшейся части программы все отдельно стоящие вхождения идентификатора PI на лексему, которую транслятор будет воспринимать как плавающий литерал 3.14159.
Препроцессор выполняет грубую предварительную работу по замене замещаемых идентификаторов замещающими строками. В этот момент ещё ничего не известно об именах, поскольку транслятор фактически ещё не начинал своей работы. А потому следует следить за тем, чтобы замещаемые идентификаторы входили в состав объявлений лишь как элементы инициализаторов.
Рассмотрим несколько примеров. Директива препроцессора
#define PI 3.14159
Превращает корректное объявление
float PI;
в синтаксически некорректную конструкцию
float 3.14159;
А следующее определение правильное.
float pi = PI;
После препроцессирования оно принимает такой вид:
float pi = 3.14159;
Сначала препроцессор замещает, затем транслятор транслирует. И потому здесь будет зафиксирована ошибка:
#define PI 3.14 0.00159
float pi = PI;
После препроцессирования объявление принимает такой вид:
float pi = 3.14 0.00159;
А здесь - всё корректно:
#define PI 3.14 + 0.00159
float pi = PI;
После препроцессирования получается правильное объявление с инициализацией:
float pi = 3.14 + 0.00159;
Строка замещения может оказаться пустой.
#define ZZZ
В этом случае оператор-выражение
ZZZ;
и ещё более странные конструкции
ZZZ ZZZ ZZZ ZZZ ZZZ ZZZ ZZZ ZZZ ZZZ ZZZ ZZZ;
превращаются препроцессором в пустой оператор. Это лишь побочный эффект работы препроцессора. У макроопределений с пустой строкой замещения имеется собственная область пременения.
Строка замещения может располагаться на нескольких строках. При этом символ '\' уведомляет препроцессор о необходимости включения в состав строки замещения текста, располагаемого на следующей стоке. Признаком завершения многострочного определения является символ конца строки:
#define TEXT "1234567890-=\
йцукенгшщзхъ\"
В ходе препроцессорной обработки вхождения идентификатора TEXT заменяются на строку замещения:
1234567890-= йцукенгшщзхъ\
Макроопределения define могут быть вложенными:
#include <iostream.h>
#define WHISKEY "ВИСКИ с содовой."
#define MARTINI "МАРТИНИ со льдом и " WHISKEY
void main() {cout << MARTINI;}
В результате выполнения последнего оператора выводится строка
МАРТИНИ со льдом и ВИСКИ с содовой.
После каждого расширения препроцессор возвращается к началу файла, переходит к очередному макроопределению и повторяет процесс преобразования макроопределений. Препроцессорные замены не выполняются внутри строк, символьных констант и комментариев. При этом в замещающей части не должно быть вхождений замещаемой части макроопределения.
Так что макроопределение
#define WHISKEY "стаканчик ВИСКИ " WHISKEY
обречено на неудачу.
В макроопределениях может встречаться несколько макроопределений с одной и той же замещаемой частью. При этом следует использовать в тексте программы директиву препроцессора
#undef ИмяЗамещаемойЧасти
Эта инструкция прекращает действие препроцессора по замене соответствующего идентификатора.
#define PI 3.14 + 0.00159
float pi1 = PI;
#undef PI
#define PI 3.14159
float pi2 = PI;
Операция ##. Конкатенация в макроопределениях
В следующем примере мы используем ещё одну специальную операцию для упревления препроцессором - операцию конкатенации ##. Обычно эта операция используется в контексте функциональных макроопределений.
Если в замещающей последовательности лексем между двумя лексемами встречается операция препоцессирования ## и, возможно, что одна или обе лексемы являются параметрами, то препроцессор вначале обеспечивает подстановку фактических значений в параметры, после чего сама операция конкатенации и окружающие её пробельные символы убираются:
#include <iostream.h> #define XXX(YYY) "МАРТИНИ со льдом и" ## YYY void main() { cout << XXX("ВИСКИ с содовой") << endl; cout << XXX("на сегодня достаточно…") << endl; // Для препроцессора тип параметра не имеет значения. // Важно, как посмотрит на это транслятор… cout << XXX(255); }Перед нами ещё одно мощное и изящное средство управления препроцессором.
Функция. Прототип
Функция в C++ объявляется, определяется, вызывается. В разделе, посвящённом структуре программного модуля, в качестве примера мы уже рассматривали синтаксис определения функции. Определение функции состоит из заголовка и тела. Заголовок функции состоит из спецификаторов объявления, имени функции и списка параметров. Тело функции образуется блоком операторов.
Синтаксис выражений вызова функции ранее был рассмотрен достаточно подробно. Это постфиксное выражение со списком (возможно пустым) выражений в круглых скобках. При разборе выражения вызова, транслятору C++ требуется информация об основных характеристиках вызываемой функции. К таковым, прежде всего, относятся типы параметров, а также тип возвращаемого значения функции. При этом тип возвращаемого значения оказывается актуален лишь в том случае, если выражение вызова оказывается частью более сложного выражения.
Если определение функции встречается транслятору до выражения вызова, никаких проблем не возникает. Вся необходимая к этому моменту информация о функции оказывается доступной из её определения:
#include <iostream.h> void ZZ(int param) // Определение функции. { cout << "This is ZZ >> " << param << endl; } void main (void) { ZZ(10); // Вызов функции. Транслятор уже знает о функции всё. }При этом не принципиально фактическое расположение определения функции и выражения её вызова. Главное, чтобы в момент разбора выражения вызова в транслятор знал бы всё необходимое об этой функции. Например, в таком случае:
#include <iostream.h> #include "zz.cpp" /*Препроцессор к моменту трансляции "подключает" определение функции ZZ() из файла zz.cpp.
*/ void main (void) { ZZ(125); } Файл zz.cpp: void ZZ(int par1) { cout << "This is ZZ " << par1 << endl; }Но как только в исходном файле возникает ситуация, при которой вызов функции появляются в тексте программы до определения функции, разбор выражения вызова завершается ошибкой:
#include <iostream.h> void main (void) { ZZ(10); /* Здесь транслятор сообщит об ошибке. */ } void ZZ(int param) { cout << "This is ZZ " << param << endl; }Каждая функция, перед тем, как она будет вызвана, по крайней мере, должна быть объявлена. Это обязательное условие успешной трансляции и вольный перевод соответствующего сообщения об ошибке (Call to undefined function 'ИмяФункции'), выдаваемого транслятором в случае вызова необъявленной функции.
Напомним, что объявление и определение - разные вещи. Объект может быть много раз объявлен, но только один раз определён. Прототип функции при этом играет роль объявления функции. В объявлении функции сосредоточена вся необходимая транслятору информация о функции - о списке её параметров и типе возвращаемого значения. И это всё, что в момент трансляции вызова необходимо транслятору для осуществления контроля над типами. Несоответствия типов параметров в прототипе и определении функции выявляются на стадии окончательной сборки программы. Несоответствие спецификации возвращаемого значения в объявлении прототипа и определении функции также является ошибкой.
#include <iostream.h> void ZZ(int ppp); /* Эта строка требуется для нормальной компиляции программы. Это и есть прототип функции. Имя параметра в объявлении может не совпадать с именем параметра в определении. */ void main (void) { ZZ(125); } void ZZ(int par1) { cout << "This is ZZ " << par1 << endl; }Самое интересное, что и такое объявление не вызывает возражений транслятора.
#include <iostream.h> void ZZ(int); /* Отсутствует имя параметра. Можно предположить, что имя параметра не является обязательным условием правильной компиляции. */ void main (void) { ZZ(125); } void ZZ(int par1) { cout << "This is ZZ " << par1 << endl; }Правила грамматики подтверждают это предположение. Ранее соответствующее множество БНФ уже рассматривалось:
ОбъявлениеПараметра ::= СписокСпецификаторовОбъявления Описатель ::= СписокСпецификаторовОбъявления Описатель Инициализатор ::= СписокСпецификаторовОбъявления [АбстрактныйОписатель] [Инициализатор]Из этой формы Бэкуса-Наура следует, что объявление параметра может состоять из одного спецификатора объявления (частный случай списка спецификаторов). Так что имени параметра в списке объявления параметров в прототипе функции отводится в букальном смысле роль украшения. Его основное назначение в прототипе - обеспечение легкочитаемости текста программы. Принципиальное значение имеет соответствие типов параметров в определении и объявлении функции.
Попытка трансляции следующего примера программы оказывается неудачной.
#include <iostream.h> void ZZ(float);// Другой тип параметра. void main (void) { ZZ(125); } void ZZ(int par1) { cout << "This is ZZ " << par1 << endl; }Если функция не возвращает значения, в объявлении и определении обязательно используется спецификатор объявления void.
Функция также может не иметь параметров. В этом случае объявление параметров в определении и прототипе может быть либо пустым, либо может состоять из одного ключевого слова void. В контексте объявления параметров слово void и пустой список спецификаторов параметров эквивалентны.
Предварительная инициализация параметров функции
Список параметров в определении и прототипе функции, кроме согласования типов параметров, имеет ещё одно назначение.
Объявление параметра может содержать инициализатор, то есть выражение, которое должно обеспечить параметру присвоение начального значения. Инициализатор параметра не является константным выражением. Начальная инициализация параметров происходит не на стадии компиляции (как, например, выделение памяти под массивы), а непосредственно в ходе выполнения программы.
Следующие строки демонстрируют пример объявления функции с инициализацией параметров. Для инициализации параметра ww используется функция XX.
int BigVal;
int XX(int);
int ZZ(int tt, int ww = XX(BigVal));
Второй параметр можно проинициализировать и таким способом, вовсе не указывая его имени. Синтаксис объявления позволяет сделать и такое!
int ZZ(int tt, int = XX(BigVal));
Единственное условие подобной инициализации - соответствие типа параметра и типа выражения, значение которого используется при начальной инициализации.
Прототипы функции могут располагаться в различных областях видимости. Его можно даже разместить в теле определяемой функции. Каждое объявление функции может содержать собственные варианты объявления и инициализации параметров. Но во множестве объявлений одной и той же функции в пределах одной области видимости не допускается повторная инициализация параметров. Всему должен быть положен разумный предел.
Кроме того, в C++ действует ещё одно ограничение, связанное с порядком инициализации параметров в пределах области видимости. Инициализация проводится непременно с самого последнего (самого правого) параметра в списке объявлений параметров. Инициализация параметров не допускает пропусков: инициализированные параметры не могут чередоваться с параметрами неинициализированными.
int MyF1 (int par1, int par2, int par3, int par4 = 10);
int MyF1 (int par1, int par2 = 20, int par3 = 20, int par4);
int MyF1 (int par1 = 100, int, int, int);
Список параметров в определении функции строится по аналогичным правилам. В списке параметров определения функции также допускаются инициализаторы, в ряде случаев также могут быть опущены имена параметров. Разумеется, включение в заголовок определения функции безымянного параметра затрудняет возможность использования этого параметра в определяемой функции. К безымянному параметру невозможно обращаться по имени.
И всё же отказ от использования параметра может быть оправдан. Такие параметры, вернее их спецификаторы, позволяют сократить затраты на модификацию сложных многомодульных программ, когда в результате изменения функции меняется число параметров этой функции. Ненужные параметры могут быть отключены без изменения многочисленных вызовов этой функции. В этом случае имеет смысл сохранить общее количество параметров функции, а имя ненужного параметра из списка параметров удалить.
Массивы и параметры
В C++ возможно лишь поэлементное копирование массивов. Этим объясняется то обстоятельство, что в списке объявлений параметров не объявляются параметры-массивы. В Borland С++ 4.5 транслятор спокойно реагирует на объявление одномерного массива в заголовке функции, проверяет корректность его объявления (размеры массива должны быть представлены константными выражениями), однако сразу же игнорирует эту информацию. Объявление одномерного массива-параметра преобразуется к объявлению указателя. Подтверждением этому служит тот факт, что "массив"-параметр невозможно проинициализировать списком значений, что совершенно нормально для обычных массивов:
void ff(int keyArr[ ] = {0,1,2,3,4,5,6,7,8,9});// Ошибка объявления.
void ff(int keyArr[10] = {0,1,2,3,4,5,6,7,8,9});// Ошибка объявления.
Оба варианта прототипа функции будут отвергнуты. При этом транслятор утверждает, что указателю (и это несмотря на явное указание размеров массива!) можно присваивать значение адреса, либо NULL.
int keyArr[100]; // Глобальный массив.
int xArr[5]; // Ещё один глобальный массив.
int XXX; // Простая переменная.
void ff(int keyArr[ 1] = keyArr, //Объявление одноименного параметра.
int pArr1 [10] = xArr,
int pArr2 [ ] = &XXX, // Адрес глобальной переменной.
int pArr3 [ ] = &xArr[10], //Адрес несуществующего элемента.
int pArr4 [50] = NULL);
/* Допустимые способы инициализации массивов в прототипе функции
свидетельствуют о том, что здесь мы имеем дело с указателями. */
Следующий пример подтверждает тот факт, что объявление одномерного массива в списке параметров оказывается на самом деле объявлением указателя.
#include <iostream.h>
void fun(int *, int[], int qwe[10] = NULL);
/* Все три объявления параметров на самом деле являются объявлениями указателей. */
void main()
{
int Arr[10] = {0,1,2,3,4,5,6,7,8,9};
int *pArr = Arr;
/* В функции main определены массив и указатель.*/
cout << Arr << " " << &Arr << " " << &Arr[0] << endl;
cout << pArr << " " << &pArr << " " << &pArr[0] << endl;
/* Разница между массивом и указателем очевидна: значение выражения,
представленного именем массива, собственный адрес массива и адрес
первого элемента массива совпадают. */
fun(Arr, Arr, Arr);
}
void fun(int* pArr1, int pArr2[], int pArr3[100])
{
cout << sizeof(pArr1) << endl;
cout << sizeof(pArr2) << endl;
cout << sizeof(pArr3) << endl;
cout << pArr1 << " " << &pArr1 << " " << &pArr1[0] << endl;
cout << pArr2 << " " << &pArr2 << " " << &pArr2[0] << endl;
cout << pArr3 << " " << &pArr3 << " " << &pArr3[0] << endl;
/* Все параметры проявляют свойства указателей. */
}
Так что размеры массива в объявлении параметра, подобно имени параметра в прототипе, являются лишь украшением, которое предназначается для напоминания программисту о назначении параметра.
При вызове функции передаются либо отдельные элементы массива и тогда мы имеем тривиальный список параметров, либо адреса, которые воспринимаются как адреса начальных элементов массивов. В последнем случае неизвестными оказываются размеры массива, однако, эта проблема решается благодаря введению дополнительного целочисленного параметра, задающего размеры массива, представленного указателем.
Следующий пример демонстрирует возможный вариант решения проблемы передачи в вызываемую функцию переменного количества однотипных значений. Подлежащие обработке данные в вызывающей функции располагаются в непрерывной области памяти (в нашем примере это целочисленный массив Arr). При этом обрабатывающая функция имеет два параметра, один из которых является указателем на объект обрабатываемого типа (в определении функции закомментированы альтернативные варианты объявления этого параметра), второй - целочисленного типа. В выражении вызова значением первого параметра оказывается адрес первого элемента массива, значением второго параметра - количество обрабатываемых элементов массива. Таким образом, функция с постоянным количеством параметров позволяет обрабатывать заранее неизвестное количество значений.
#include <iostream.h>
void fun(int * = NULL, int = 0);
void main()
{
int Arr[10] = {0,1,2,3,4,5,6,7,8,9};
fun(Arr, 10);
fun(Arr, sizeof(Arr)/sizeof(Arr[0]));
}
void fun(int* pArr /* int pArr[] */ /* int pArr[150] */, int key)
{
for ( key--; key >= 0; key--) cout << pArr[key] << endl;
}
Фактическое тождество одномерного массива и указателя при объявлении параметров определяет специфику объявления многомерных массивов-параметров. В C++ многомерный массив - понятие условное. Как известно, массив размерности n является одномерным массивом множества объектов производного типа - массивов размерности n-1. Размерность массива является важной характеристикой производного типа. Отсюда - особенности объявления многомерных массивов как параметров функций.
В следующем примере определена функция fun с трёхмерным параметром размерности 5*5*25. Транслятор спокойно реагирует на различные варианты прототипов функции fun в начале программы. Если последовательно комментировать варианты объявлений функции, ошибка будет зафиксирована лишь тогда, когда будут закомментированы все объявления, у которых характеристика второй и третьей размерности совпадает с аналогичной характеристикой многомерного параметра-массива в определении функции.
#include <iostream.h>
#define DIM1 3
#define DIM2 5
// void fun(int rrr[][][]);
/* Такой прототип неверен! Квадратные скобки в объявлении параметра,
начиная со второй, обязательно должны содержать константные выражения,
значения которых должны соответствовать значениям в квадратных скобках
(начиная со второй!) в объявлении параметра в определении функции. Эти
значения в контексте объявления параметров являются элементами
спецификации ТИПА параметра, а не характеристиками его РАЗМЕРОВ. Типы
составляющих одномерные массивы элементов в прототипе и заголовке
определения функции должны совпадать. */
//void fun(int rrr[5][DIM1][DIM2]);
void fun(int rrr[][3][5]);
void fun(int rrr[15][DIM1][5]);
void fun(int *rrr[3][DIM2]);
/* Во всех этих случаях параметр rrr является указателем на двумерный
массив из 3*5 элементов типа int. "Массив из трёх по пять элементов типа
int" - такова спецификация типа объекта. */
/* Следующие два прототипа, несмотря на одно и то же имя функции,
объявляют ещё пока неопределённые фунции. Одноимённые функции с
различными списками параметров называются перегруженными функциями. */
void fun(int *rrr[25][250]);
void fun(int rrr[50][100][DIM1]);
void main()
{
int Arr[2][DIM1][DIM2] = {
{
{1 ,2 ,3 ,4 ,5 },
{10,20,30,40,50},
{11,12,13,14,15},
},
{
{1,},
{2,},
{3,},
}
};
fun(Arr); // Вызов fun. Значение параметра - адрес начала массива.
}
void fun(int pArr[75][DIM1][DIM2])
{
cout << sizeof(pArr) << endl;
cout << pArr << " " << &pArr << " " << &pArr[0][0] << endl;
/* Параметр проявляет свойства указателей. */
cout << sizeof(*pArr) << endl;
cout << *pArr << " " << &*pArr << " " << &*pArr[0][0] << endl;
/* Если применить к указателю операцию разыменования, можно убедиться в том,
что параметр указывает на массив. При этом о топологии многомерного массива
можно судить исключительно по косвенной информации (в данном случае - по
значениям константных выражений DIM1 и DIM2) или по значениям дополнительных
параметров. */
}
При работе с параметрами-массивами мы имеем дело с указателями. Это немаловажное обстоятельство позволяет непосредственно из вызываемой функции изменять значения объектов, определённых в вызывающей функции.
Многомерные динамические массивы
Многомерный массив в C++ по своей сути одномерен. Операции new[] и delete[] позволяют создавать и удалять динамические массивы, поддерживая при этом иллюзию произвольной размерности. Деятельность по организации динамического массива требует дополнительного внимания, которое окупается важным преимуществом: характеристики массива (операнды операции new) могут не быть константными выражениями. Это позволяет создавать многомерные динамические массивы произвольной конфигурации. Следующий пример иллюстрирует работу с динамическими массивами.
#include <iostream.h>
int fdArr(int **, int, int);
int fdArr(int ***, int, int, int);
// Одноимённые функции. Различаются списками списками параметров.
// Это так называемые перегруженные функции. О них позже.
void main()
{
int i, j;
/* Переменные (!) для описания характеристик массивов.*/
int dim1 = 5, dim2 = 5, dim3 = 10, wDim = dim2;
/*
Организация двумерного динамического массива производится в два этапа.
Сначала создаётся одномерный массив указателей, а затем каждому элементу
этого массива присваивается адрес одномерного массива. Для характеристик
размеров массивов не требуется константных выражений.
*/
int **pArr = new int*[dim1];
for (i = 0; i < dim1; i++) pArr[i] = new int[dim2];
pArr[3][3] = 100;
cout << pArr[3][3] << endl;
fdArr(pArr,3,3);
/*
Последовательное уничтожение двумерного массива…
*/
for (i = 0; i < dim1; i++) delete[]pArr[i];
delete[]pArr;
/*
Организация двумерного "треугольного" динамического массива. Сначала
создаётся одномерный массив указателей, а затем каждому элементу этого
массива присваивается адрес одномерного массива. При этом размер
(количество элементов) каждого нового массива на единицу меньше
размера предыдущего. Заключённая в квадратные скобки переменная в
описателе массива, которая, в данном контексте, является операндом
операции new, позволяет легко сделать это.
*/
int **pXArr = new int*[dim1];
for (i = 0; i < dim1; i++, wDim--) pXArr[i] = new int[wDim];
pXArr[3][3] = 100;
cout << pArr[3][3] << endl;
fdArr(pXArr,3,3);
/*
Последовательное уничтожение двумерного массива треугольной конфигурации…
*/
for (i = 0; i < dim1; i++) delete[]pXArr[i];
delete[]pXArr;
/*
Создание и уничтожение трёхмерного массива требует дополнительной итерации.
Однако здесь также нет ничего принципиально нового.
*/
int ***ppArr;
ppArr = new int**[dim1];
for (i = 0; i < dim1; i++) ppArr[i] = new int*[dim2];
for (i = 0; i < dim1; i++)
{
for (j = 0; j < dim2; j++) ppArr[i][j] = new int[dim3];
}
ppArr[1][2][3] = 750; cout << ppArr[1][2][3] << endl; fdArr(ppArr,1,2,3);
for (i = 0; i < dim1; i++)
{
for (j = 0; j < dim2; j++) delete[]ppArr[i][j];
}
for (i = 0; i < dim1; i++) delete[]ppArr[i];
delete[] ppArr;
}
int fdArr(int **pKey, int index1, int index2)
{
cout << pKey[index1][index2] << endl;
}
int fdArr(int ***pKey, int index1, int index2, int index3)
{
cout << pKey[index1][index2][index3] << endl;
Функции с изменяемым списком параметров
Для решения задачи передачи неопределённого количества параметров C++ располагает также средствами объявления переменных списков параметров.
Вспомним несколько форм Бэкуса-Наура, определяющих синтаксис списка параметров в определении и прототипе функции.
СписокОбъявленийПараметров ::= [СписокОбъявленийПарам] [...]
::= СписокОбъявленийПарам, ...
СписокОбъявленийПарам ::= ОбъявлениеПараметра
::= [СписокОбъявленийПарам,] ОбъявлениеПараметра
Таким образом, список объявлений параметров может завершаться многоточием, отделённым запятой от списка объявлений параметров, этого многоточия в списке параметров может не быть, а возможно также, что кроме многоточия в списке параметров вовсе ничего нет.
Так вот это многоточие предупреждает транслятор о том, что определяемая или объявляемая функция может вызываться с произвольным списком параметров.
В этом случае количество и тип параметров становятся известны из списка выражений, определяющих значения параметров в выражении вызова функции.
Рассмотрим прототип и определение функции с переменным количеством параметров.
int PP(…);
int PP(…)
{
return 100;
}
Трансляция этого фрагмента кода не вызывает у транслятора никаких возражений. Многоточием в списке параметров он предупреждён о возможных неожиданностях.
Следующий фрагмент кода демонстрирует варианты выражений вызова функции PP().
int retVal;
retVal = PP();
retVal = PP(1,2 + retVal,3,4,5,25*2);
PP('z',25,17);
В ходе выполнения выражений вызова функций с переменным количеством параметров изменяется алгоритм формирования записи активации. Теперь он выглядит примерно так:
- в стековом сегменте выделяется запись активации. Теперь размер записи активации зависит от количества и типа параметров в выражении вызова, (а не прототипа). Так что сначала нужно определить и запомнить общее количество и тип выражений, которые образуют список параметров в выражении вызова функции. Как и раньше, вычисление значений производится в процессе выполнения программы. Как и раньше, значение параметра может быть представлено любыми выражениями;
- на основе анализа прототипов вызываемой функции, расположенных в области видимости вызывающей функции, определяются начальные значения параметров (если они имеются), которые и записываются в соответствующие области записи активации. Как мы увидим дальше, в функциях с переменным количеством параметров слева от многоточия всё же может находиться хотя бы один явным образом определённый параметр. В противном случае просто не будет возможности воспользоваться значениями параметров;
- вычисляются значения выражений, которые образуют список параметров в выражении вызова. Поскольку вычисление значений производится в ходе выполнения программы, здесь также нет никаких ограничений на процесс определения значения выражений. Можно использовать любые значения, а также вызовы ранее объявленных функций;
- элементам записи активации присваиваются вычисленные значения. При этом возможно, часть параметров, которым были присвоены значения умолчания (это всегда ближайшие к многоточию параметры), получит новые значения. В этом процессе не допускается неопределённых ситуаций. Либо элемент записи активации получает значение умолчания, либо ему присваивается значение при вызове. Нарушение порядка означивания, как и раньше, выявляется ещё на стадии трансляции программы;
- в вызываемой функции всем параметрам, которые были указаны в списке параметров, присваиваются значения из записи активации. Для остальных (непостоянных и, естественно, безымянных) параметров выделяется дополнительная память. Эти параметры также получают свои значения из записи активации;
- управление передаётся первому оператору функции. Означенные параметры используются как переменные с определёнными значениями. Доступ к безымянным параметрам, в силу того, что к ним невозможно обращение по имени, обеспечивается специальными алгоритмами.
Итак, параметрам вызываемой функции присвоены соответствующие значения, представленные в выражении вызова. Возникает вопрос, как воспользоваться этими значениями в теле вызываемой функции. Если у параметра существует собственное имя, то всё очевидно.
Если же параметр был определён как параметр без имени, то существует единственный способ доступа к таким параметрам - доступ с помощью указателей.
Дело в том, что все означенные параметры, с именами и безмянные, занимают одну непрерывную область памяти. Поэтому для доступа к элементам этого списка достаточно знать имя и тип хотя бы одного параметра. Для этого в функции определяется указатель, которому с помощью операции взятия адреса присваивается значение, которое соответствует адресу именованного параметра. Переход от параметра к параметру при этом обеспечивается с помощью операций адресной арифметики над значением этого указателя.
С точки зрения реализации всё очень просто. Если бы не одно обстоятельство, которое заметно ограничивает свободу применения подобных функций.
Дело в том, что всякий раз при создании функций с неопределённым количеством параметров, мы вынуждены разрабатывать алгоритм доступа к списку этих самых параметров. А для этого необходимо, по крайней мере, представлять закономерность расположения параметров в списке. Так что список необъявленных параметров не может состоять из подобранных случайным образом элементов, поскольку не существует универсальных средств распознавания элементов этого списка. На практике дело обычно ограничивается несколькими тривиальными вариантами.
При этом либо известен тип и количество передаваемых параметров, и процедура доступа к параметрам сводится к примитивному алгоритму, который воспроизводится в следующем примере:
#include <iostream.h>
long PP(int n, ...);
void main (void)
{
long RR;
RR = PP(5, 1, 2, 3, 4, 5 );
/*
Вызвали функцию с 6 параметрами. Единственный обязательный параметр
определяет количество передаваемых параметров.
*/
cout << RR << endl;
}
long PP(int n ...)
{
int *pPointer = &n;
// Настроились на область памяти с параметрами...
int Sum = 0;
for ( ; n; n--) Sum += *(++pPointer);
return Sum;
}
Либо известен тип элементов списка и признак завершения списка передаваемых параметров. Процедура доступа к параметрам также проста, как и в первом случае:
#include <iostream.h>
long PP(int par1 ...);
void main (void)
{
long RR;
RR = PP( 1, 2, 0, 4, 0 );
/*
Вызвали функцию с 5 параметрами. Единственный обязательный параметр -
первый параметр в списке параметров.
*/
cout << RRR << endl;
}
long PP(int par1 ...)
{
int *pPointer = &par1;
/*
Настроились на область памяти с параметрами. Признак конца списка -
параметр с нулевым значением.
*/
int Sum = 0;
for ( ; *pPointer != 0; pPointer++) Sum += *pPointer;
// Что-то здесь не так… Мы так и не обработали до конца весь список.
return Sum;
}
Преобразование основных типов
Вычисление значений выражений в операторах C++ обеспечивается выполнением операций и вызовом функций. Операции используют операнды, функции требуют параметров. Операнды и параметры характеризуются типом. В C++ не существует операций, которые, например, обеспечивали бы сложение или умножение операндов различных типов. Выражения вызова функций также требуют соответствия типа параметров типу параметров определения и прототипа.
Однако не всегда в программе удаётся легко согласовать типы операндов и параметров. И здесь проблем, связанных с согласованием типов операндов и параметров транслятор берёт на себя. Фактически это означает введение ещё одной системы правил, которая называется правилами стандартного преобразования типов.
В общем случае, при определении значения выражения могут возникать следующие ситуации:
1.Присвоение "большему типу" значения "меньшего типа". Безопасное присвоение, гарантирует сохранение значения.
unsigned int UnsignedIntVal; unsigned char UnsignedCharVal; UnsignedIntVal = UnsignedCharVal;2.Присвоение "меньшему типу" значения "большего типа". Потенциально опасное присвоение, грозит потерей информации.
int IntVal; char CharVal; CharVal = IntVal;3.Преобразование значения из "меньшего типа" в "больший тип". Называется расширением типа.
(unsigned int)UnsignedCharVal;4.Преобразование значения из "большего типа" в "меньший тип". Называется сужением типа. Является опасным преобразованием.
(char)IntVal;Корректное выполнение действий со значениями различных типов в безопасных случаях и в ряде опасных случаев обеспечивается благодаря реализованной в C++ системе преобразования типов.
При трансляции выражений с различными типами операндов транслятор использует механизмы неявных преобразований, которые основываются на следующих правилах стандартных преобразований:Присваивание значения объекту преобразует это значение к типу объекта.
unsigned int MyIntU;Передача значения при вызове функции преобразует это значение в тип параметра функции. Он становится известен благодаря прототипу вызываемой функции.
MyIntU = 3.14159;
Эквивалентно
MyIntU = (unsigned int)3.14159;
void ff(int); // Прототип функции.
:::::
ff(3.14159);Эквивалентно
ff((int)3.14159);
При этом на стадии трансляции возможно появление предупреждения о сужении типа.
В арифметическом выражении тип результата выражения определяется самым "широким" типом среди всех образующих выражение операндов. Этот тип называют результирующим типом выражения. К этому типу преобразуются все остальные операнды.unsigned int MyIntU = 5;
…(MyIntU + 3.14159)…
Результирующим типом выражения здесь оказывается тип double, представленный в выражении литералом 3.14159. В процессе вычисления выражения значение переменной MyIntU преобразуется в 5.0, к которому прибавляется 3.14159.
Преобразование типа при вычислениях арифметических выражений применяется к копиям значений образующих выражение подвыражений. В процессе преобразования типов результаты преобразований подвыражениям не присваиваются.unsigned int MyIntU = 5;
MyIntU = MyIntU + 3.14159;
Здесь имеют место два последовательных преобразования:
По ходу вычисления выражения значение переменной MyIntU расширяется до double и к расширенной копии значения 5.0 прибавляется 3.14159. После этого результирующее значение 8.14159, в соответствии с первым правилом, сужается до типа unsigned int. В результате чего получается значение 8, которое и присваивается переменной MyIntU.
Указатель на любой не являющийся константой тип можно присваивать указателю типа void*. Этот указатель способен адресовать объекты любого типа данных. Он используется всякий раз, когда неизвестен тип объекта.int iVal;
int *p_iVal = 0;
char *p_chVal = 0;
void *p_Val;
const int *pc_iVal = &iVal;
p_Val = p_iVal;
p_Val = p_chVal;
// ПРАВИЛО 5 выполняется...
p_Val = pc_iVal;
//Ошибка: pc_iVal - указатель на константу.
const void *pcVal = pc_iVal;
/*
А здесь всё хорошо! Указателю на константу присвоен указатель на константу.
*/
Перед операцией разыменования указатель типа void* нужно явно преобразовать в указатель на конкретный тип, поскольку в этом случае отсутствует информация о типе, подсказывающая транслятору способ интерпретации битовой последовательности, представляемой указателем:
char *p_chValName = "Marina";Механизм неявных преобразований может быть отключён посредством явного указания в тексте программы требуемого преобразования типов.
p_Val = p_chValName;
p_chVal = (char*)p_Val; /*Явное приведение.*/
Так, модификация ранее рассмотренного примера
MyIntU = MyIntU + (int)3.14159;отключает механизм неявных преобразований и при вычислении значения переменной производится лишь одно преобразование типа, которое заключается в сужении типа значения литерала 3.14159.
typedef-объявление
На стадии компиляции производится полная идентификация типов всех входящих в программу выражений. Даже отсутствие имени типа в объявлении как, например,
unsigned long MMM; // Вместо имени типа - комбинация модификаторов unsigned long.восстанавливается транслятором в соответствии с принятыми в C++ правилами умолчания.
Помимо явного объявления типа в C++ предусмотрены дополнительные средства описания имён типов. Таким средством является typedef-объявление. С его помощью в программу можно ввести новые имена, которые затем используются для обозначения производных и основных типов.
typedef-объявление - это инструмент объявления. Средство ввода новых имён в программу, средство замены громоздких последовательностей имён в объявлениях (но не определениях!) новыми именами.
Синтаксис typedef-объявления как подмножества объявления представляется внушительным списком форм Бэкуса-Наура. Но при известной степени концентрации это нагромождение БНФ всё же можно разобрать:
Объявление ::= [СписокСпецификаторовОбъявления][СписокОписателей]; СписокСпецификаторовОбъявления ::= СпецификаторОбъявления [СписокСпецификаторовОбъявления] СпецификаторОбъявления ::= typedef ::= ***** СписокОписателей ::= [СписокОписателей,] ОписательИнициализатор ОписательИнициализатор ::= Описатель [Инициализатор] Описатель ::= dИмя ::= ***** dИмя ::= Имя ::= ОписанноеИмяТипа ::= ***** ОписанноеИмяТипа ::= Идентификатор СписокСпецификаторовТипа ::= СпецификаторТипа [СписокСпецификаторовТипа] СпецификаторТипа ::= ИмяПростогоТипа ::= СпецификаторКласса ::= *****Таким образом, typedef-объявление является объявлением, которое начинается спецификатором typedef и состоит из последовательностей разнообразных спецификаторов объявления и описателей. Список описателей (элементы списка разделяются запятыми) может содержать языковые конструкции разнообразной конфигурации. В него могут входить описатели (в конце концов, это всего лишь разнообразные имена) с символами ptrОпераций (* и &), описатели, заключённые в круглые скобки, описатели в сопровождении заключённых в скобки списков объявлений параметров, описателей const и volatile, а также заключённых в квадратные скобки константных выражений (последние, надо полагать, предназначены для спецификации массивов).
В качестве примера рассмотрим, следующее typedef-объявление:
typedef int Step, *pInteger;Это объявление начинается спецификатором typedef, содержит спецификатор объявления int и список описателей, в который входит два элемента: имя Step и имя pInteger, перед которым стоит символ ptrОперации *.
Объявление эквивалентно паре typedef-объявлений следующего вида:
typedef int Step; typedef int *pInteger;В соответствии с typedef-объявлениями, транслятор производит серию подстановок, суть которых становится понятной из анализа примера, в котором пара операторов объявления
Step StepVal; extern pInteger pVal;заменяется следующими объявлениями:
int StepVal; extern int * pVal;На основе этого примера можно попытаться воспроизвести алгоритм подстановки:
- после возможного этапа декомпозиции списка описателей typedef-объявления, в результате которого может появиться новая серия typedef-объявлений, транслятор переходит к анализу операторов объявлений;
- в очередном операторе объявления выделяется идентификатор, стоящий на месте спецификатора объявления;
- среди typedef-объявлений производится поиск соответствующего объявления, содержащего вхождение этого идентификатора в список описателей. Таким образом, транслятор находит соответствующий контекст для подстановки. Мы будем называть этот контекст контекстом замены. Контекст замены оказывается в поле зрения транслятора вместе с оператором объявления, в котором транслятор различает спецификатор объявления и описатель;
- оператор объявления заменяется контекстом замены, в котором совпадающий со спецификатором объявления идентификатор заменяется соответствующим описателем.
Если в программе присутствует typedef-объявление
typedef char* (*PPFF) (int,int,int*,float);то компактное объявление функции
PPFF ReturnerF(int, int);преобразуется при трансляции в сложное, но как мы далее увидим, абсолютно корректное объявление:
char* (*ReturnerF(int, int))(int,int,int*,float);При этом по идентификатору PPFF в прототипе функции находится контекст замены char* (*PPFF) (int,int,int*,float), в котором замещаемый описатель PPFF заменяется замещающим описателем ReturnerF(int, int).
Цель достигнута. Простое становится сложным. И как хорошо, что всё это происходит без нашего участия! Перед нами очередное средство для "облегчения" труда программиста.
Заметим, что подстановка возможна и в том случае, когда замещаемый описатель заменяется пустым замещающим описателем.
То же самое typedef-объявление позволяет построить следующее объявление функции:
void MyFun (int, int, int*, float, PPFF);Рассмотрим ещё один пример.
typedef long double NewType; /* Используем спецификатор для ввода в программу нового имени типа. */ ::::: NewType MyFloatVal;Новое имя для обозначения типа введено…
Новое имя ранее уже поименованного типа называют ОПИСАННЫМ ИМЕНЕМ ТИПА. Именно таким образом и назывался (так выглядел) соответствующий нетерминальный символ во множестве БНФ, связанных с typedef-объявлением.
Описанное имя типа может заменять прежнее имя типа везде, где это возможно, поскольку объявления с описанным именем при трансляции заменяется первоначальным объявлением:
long double MyFloatVal;В ряде случаев описанное имя типа может оказаться единственным именем для обозначения безымянного типа (об этом позже).
В области действия объявления имени типа (typedef-объявления), идентификатор NewType (он является спецификатором типа) становится синонимом другого спецификатора типа - конструкции long double. Иногда подобным образом вводимый синоним называют замещающим идентификатором.
Использование спецификатора typedef подчиняется следующим правилам (ничто не даётся даром):
1. Спецификатор typedef может переопределять имя как имя типа, даже если это имя само уже было ранее введено typedef спецификатором:
typedef int I; typedef I I;2. Спецификатор typedef не может переопределять имя типа, объявленное в одной и той же области действия, и замещающее имя другого типа.
typedef int I; typedef float I; // Ошибка: повторное описание…3. На имена, введённые в программу с помощью спецификатора typedef, распространяются правила области действия, за исключением разрешения на многократное использование имени (правило 1.).
Тип функции
Основными характеристиками функции является тип возвращаемого значения и список типов формальных параметров. Подобно тому, как имена переменных никаким образом не влияют на их тип, имена функций не является частью их типа. Тип функции определяется типом возвращаемого значения и списком типов её формальных параметров. Например, пара функций
char MyF1 (int, int, int*, float);
char MyNew (int MyP1, int MyP2, int* MyP3, float MyP3);
имеют один и тот же тип:
char (int, int, int*, float)
Подобную конструкцию мы назовём описанием типа функции.
А вот как выглядит описание типа функции, которая возвращает указатель на объект типа char:
char * (int, int, int*, float)
Описанию этого типа соответствует, например, функция
char *MyFp (int MyP1, int MyP2, int* MyP3, float MyP3);
Комбинируя знак ptr-операции * с именем функции мы получаем новую языковую конструкцию:
char (*MyPt1) (int MyP1, int MyP2, int* MyP3, float MyP3);
Это уже не объявление функции. Это определение указателя на функцию! Это объект со следующими характеристиками:
- его имя MyPt1,
- это указатель на функцию,
- эта функция должна возвращать значения типа char,
- список её формальных параметров имеет вид (int,int,int*, float).
Так что это должны быть функции со строго определёнными характеристиками. В нашем случае - это функции типа
char (int, int, int*, float)
Описание типа указателя на функцию, возвращающую указатель на объект типа char с параметрами (int, int, int*, float)
char * (int, int, int*, float)
отличается от описания типа этой функции дополнительным элементом (*):
char * (*) (int, int, int*, float).
Пример определения подобного указателя:
char* (*MyPt2) (int MyP1, int MyP2, int* MyP3, float MyP3);
И опять новый объект:
- его имя MyPt2,
- это указатель на функцию,
- эта функция должна возвращать указатель на объекты типа char,
- список её формальных параметров имеет вид (int,int,int*, float).
Также можно определить функцию, которая будет возвращать указатель на объект типа void (то есть просто указатель). Это совсем просто:
void * (int)
Описанию этого типа соответствует, например, функция
void *malloc (int size);
Эта функция пытается выделить блок памяти размера size и в случае, если это удалось сделать, возвращает указатель на выделенную область памяти. В противном случае возвращается специальное значение NULL. Как распорядиться выделенной памятью - личное дело программиста. Единственное ограничение заключается в том, что при этом необходимо использовать явное преобразование типа:
#include <stdlib.h>
char *p = NULL;
void NewMemory ()
{
p = malloc(sizeof(char)*1024);// Этот оператор не пройдёт!
p = (char*) malloc(sizeof(char)*1024);
// Требуется явное преобразование типа.
}
Имя массива, если к нему не применяется операция индексации, оказывается указателем на первый элемент массива. Аналогично, имя функции, если к нему не применяется операция вызова, является указателем на функцию. В нашем случае ранее объявленная функция под именем MyFp приводится к безымянному указателю типа
char * (*) (int, int, int*, float)
К имени функции может быть применена операция взятия адреса. Её применение также порождает указатель на эту функцию. Таким образом, MyFp и &MyFp имеют один и тот же тип. А вот как инициируется указатель на функцию:
char* (*MyPt2) (int, int, int*, float) = MyFp;
Очевидно, что функция MyFp() должна быть к этому моменту не только объявлена, но и определена.
Новому указателю на функцию
char* (*MyPt3) (int, int, int*, float);
можно также присвоить новое значение.
Для этого достаточно использовать ранее определённый и проинициализированный указатель:
MyPt3 = MyPt2;
Или адрес ранее определённой функции:
MyPt3 = MyFp;
При этом инициализация и присваивание оказываются корректными лишь при условии, что имеет место точное сопоставление списков формальных параметров и списков формальных значений в объявлениях указателей и функций.
Вызов функции
Для вызова функции с помощью указателя использование операции разыменования не обязательно. Полная форма вызова
char* MyPointChar = (*MyPT3)(7,7,NULL,7.7);
имеет краткую эквивалентную форму
char* MyPointChar = MyPT3(7,7,NULL,7.7);
Значением выражения MyPT3 является адрес функции.
А вот каким образом описывается массив указателей на функцию:
char* (*MyPtArray[3]) (int, int, int*, float);
Здесь описан массив указателей из 3 элементов. Инициализация массива указателей возможна лишь после объявления
трёх однотипных функций:
extern char* MyFF1 (int, int, int*, float);
extern char* MyFF2 (int, int, int*, float);
extern char* MyFF3 (int, int, int*, float);
char* (*MyPtArray[3]) (int, int, int*, float) =
{
MyFF1,
MyFF2,
MyFF3
}; // Инициализация массива указателей.
Вызов функции (например, MyFF3()) с помощью элемента массива указателей можно осуществить следующим образом:
char* MyPointChar = MyPtArray[2](7,7,NULL,7.7);
Указатель на функцию может быть описан как параметр функции:
void MonitorF(int,int,int*,float,char*(*)(int,int,int*,float));
// Торжество абстрактного описателя!
И этому параметру можно присвоить значение (значение по умолчанию):
void MonitorF(int,int,int*,float,char*(*)(int,int,int*,float)=MyFF1);
Функция, что используемая для инициализации последнего параметра функция должна быть к моменту инициализации, по крайней мере, объявлена.
А вот как может выглядеть определение функции MonitorF:
#include <assert.h>
/*
Заголовочный файл, содержащий макроопределение assert.
Это макроопределение преобразуется в условный оператор if.
Если в ходе проверки значение условного выражения оказывается
равным нулю, то происходит прерывание выполнения программы.
*/
void MonitorF (
int val1,
int val2,
int* pVal,
float fVal,
char*(*pParF)(int,int,int*,float)
)
{
char* pChar;
assert(pVal != NULL);
assert(pParF != NULL);
//Это всего лишь проверка того, не являются ли указатели пустыми...
pChar = pParF(val1, val2, pVal, fVal);
}
Возможные варианты вызова этой функции:
int MMM;
int* pIval = &MMM;
/*
Указатель pIval используется для инициализации третьего параметра.
*/
MMM = 100;
/*
А значение объекту, на который настроен указатель pIval, может быть
изменено в любой момент.
*/
MonitorF(9,9,pIval,9.9);
/*
При вызове используем значение указателя на функцию, присвоенное последнему
параметру по умолчанию.
*/
MonitorF(11,11,pIval,11.11,MyFF3);
/* А теперь передаём адрес новой функции.*/
Указатель на функцию может также быть типом возвращаемого значения. Объявление подобной функции требует определённого навыка. Начнём с той части объявления, которая содержит имя функции и список её формальных параметров.
ReturnerF(int, int)
Определим теперь тип указателя на функцию, который будет возвращаться функцией ReturnerF(int, int).
char* (*)(int,int,int*,float)
Теперь остаётся правильно соединить обе части объявления.
char* (*ReturnerF(int, int))(int,int,int*,float);
Получилась такая вот матрёшка. Функция о двух целочисленных параметрах, возвращающая указатель на функцию, которая возвращает указатель на объект типа char и имеет собственный список формальных параметров вида: (int,int,int*,float). Нет предела совершенству!
Самое сложное - это объявить прототип подобной функции. Всё остальное очень просто. При определении функции нужно помнить, что она (всего лишь) возвращает указатель на функцию, то есть просто имя функции. Разумеется, эта функция должна быть предварительно объявлена и определена, а её описание должно соответствовать характеристикам функции ReturnerF.
Есть такие функции! Здесь их целых три: MyFF1, MyFF2, MyFF3.
Приступаем к реализации и параллельно обыгрываем параметры.
char* (*ReturnerF(int param1, int param2))(int,int,int*,float)
{
char* (*PointF) (int,int,int*,float);
/*
Это всего лишь указатель на функцию. Мы можем себе позволить этот пустяк.
*/
if (!param1) return NULL;
switch param2
{
case 1: PointF = MyFF1; break;
case 2: PointF = MyFF2; break;
case 3: PointF = MyFF3; break;
default: PointF = NULL; break;
}
return PointF;
}
Теперь только вызов! Наша функция возвращает адрес функции. И поэтому самое простое - это вызов функции непосредственно из точки возврата функции ReturnerF:
int val1, val2;
:::::
MyPointChar = (ReturnerF(val1,val2))(7,7,NULL,7.7);
Всё было бы хорошо, если бы только не существовала вероятность возвращения пустого указателя.
Так что придётся воспользоваться ранее объявленным указателем на функцию, проверять возвращаемое значение и
только потом вызывать функцию по означенному указателю. Это не намного сложнее:
MyPtArray[3] = ReturnerF(val1,val2);
if (MyPtArray[3])
{MyPointChar = (MyPtArray[3])(7,7,NULL,7.7);}
/* Вот и элемент массива указателей пригодился.*/
Настало время вспомнить о typedef-спецификаторе. С его помощью запись указателя на функцию можно сделать компактнее:
typedef char* (*PPFF) (int,int,int*,float);
Здесь надо представлять всё ту же матрёшку. Замещающий идентификатор PPFF располагается внутри определяемого выражения. И вот новое объявление старой функции.
PPFF ReturnerF(int, int);
В процессе трансляции будет восстановлен исходный вид объявления.
Совместно используемые функции
Имя функции в заголовке объявления можно рассматривать как индивидуальную характеристику функции. Однако имени функции для её однозначной идентификации недостаточно. Здесь важен комплекс характеристик функции. При этом спецификация возвращаемого функцией значения актуальна лишь в случае, когда выражение вызова функции является частью более сложного выражения. В пределах области действия данного имени функция однозначно идентифицируется именем в сочетании со списком её параметров. Это обстоятельство позволяет реализовывать механизм совместного использования функций.
При объявлении различных функций в C++ можно использовать одни и те же имена. При этом одноимённые функции различаются по спискам параметров. Отсюда становится понятным смысл понятия совместно используемых функций: одни и те же имена функций совместно используются различными списками параметров.
У совместно используемых функций имеется ещё одно название. Такие функции называются перегруженными. Смысл этого названия становится понятным из следующей аналогии. В естественном языке одни и те же глаголы могут обозначать различные действия. Например, можно "ходить по комнате", "ходить под парусом", "ходить конём". В каждом из этих контекстов глагол "ходить" употребляется в новом смысле и в буквальном смысле перегружается разными смыслами.
Механизм совместного использования заключается в том, в ходе трансляции исходного кода переименовываются все функции. Новые имена создаются транслятором на основе старых имен и списков типов параметров. Никакие другие характеристики функция при создании новых имён транслятором не учитываются.
Приведём пример объявления совместно используемых функций. Предположим, что требуется объявить и определить несколько функций, выполняющих практически одну и ту же работу - выбор максимального значения. При этом каждая функция имеет свои собственные особенности реализации, которые связаны с количеством и типом передаваемых параметров. Очевидно, что каждой функции можно присвоить своё собственное имя, но это (будто бы) затрудняет чтение и понимание текста программы.
Попытка объединения нескольких функций в одну функцию, которая в зависимости от значений параметров реализовывала бы один из алгоритмов сравнения, неоправданно усложняет структуру программы и затруднит модификацию этой самой функции.
C++ предлагает компромиссное решение, в основе которого лежит так называемый алгоритм декодирования имени. В программе можно объявить несколько одноименных функций:
int max(int,int);
int max(int*,int);
int max(int,int*);
int max(int*,int*);
и при этом в процессе трансляции, к имени каждой из объявленных функций будет прибавлена специальная цепочка символов, зависящая от типа и порядка параметров функции. Конкретный алгоритм декодирования зависит от транслятора. В соответствии с представленной в книге Б.Бабэ схемой декодирования имён в Borland C++, декодированные имена четвёрки функций будут выглядеть следующим образом:
@max$qii
@max$qpii
@max$qipi
@max$qpipi
Заметим, что при кодировании имён транслятор не использует информацию о типе возвращаемых значений и поэтому пара функций
int max(int*,int*);
int * max(int*,int*);
должна была бы получить одно и то же декодированное имя @max$qpipi, что неизбежно вызвало бы сообщение об ошибке.
Причина, по которой при кодировании имён не используется информация о типе возвращаемых значений, заключается в том, что транслятор не всегда способен установить соответствие между выражениями вызова функций и их новыми именами, которые присваиваются определениям и объявлениям функций в ходе трансляции.
Функция, которая возвращает целочисленное значение, в программе может быть вызвана без учёта её возвращаемого значения. Если бы транслятор ориентировался на информацию о типе возвращаемого значения, то в этом случае он бы не смог установить соответствие между вызовом и определением (транслятор должен знать, он не должен угадывать).
Так что не являются совместно используемыми функции, различающиеся лишь типом возвращающего значения.
Также не являются совместно используемыми функции, списки параметров которых различаются лишь применением модификаторов const или volatile, или использованием ссылки (эти спецификаторы не используются при модификации имён).
Кроме того, множество вариантов совместно используемых функций объявляется и определяется внутри одной и той же области видимости имени функции. Объявляемые в различных областях видимости функции совместно не используются. Такие функции скрывают друг друга.
Решение относительно вызова совместно используемой функции принимается транслятором и сводится к выбору конкретного варианта функции. Выбор производится в соответствии со специально разработанным алгоритмом, который называется алгоритмом сопоставления параметров.
Этот алгоритм обеспечивает сопоставление типа значений параметров в выражениях вызова с параметрами каждого из объявленных вариантов функции. В процессе сопоставления параметров используются, по крайней мере, три различных критерия сопоставления.
1.Точное сопоставление.
Точное сопоставление предполагает однозначное соответствие количества, типа и порядка значений параметров выражения вызова и параметров в определении функции.
// Произвольная функция, которая возвращает целое значение.
int iFunction(float, char *);
//Объявление пары совместно используемых функций...
extern void FF(char *); //Вариант 1...
extern void FF(int); //Вариант 2...
//Вызов функции.
FF(0);
FF(iFunction(3.14, "QWERTY"));
Поскольку нуль имеет тип int, оба вызова сопоставляется со вторым вариантом совместно используемой функции.
2.Сопоставление с помощью расширения типа.
При таком сопоставлении производится приведение типа значения параметра в выражении вызова к типу параметра в определении функции. Для этого используется расширение типа.
Если ни для одного из вызовов точного сопоставления не произошло, то применяются следующие расширения типа:
- Параметр типа char, unsigned char или short расширяются до типа int. Параметр типа unsigned short расширяется до типа int, если размер объекта типа int больше размера объекта типа short (это зависит от реализации). Иначе он расширяется до типа unsigned int.
- Параметр типа float расширяется до типа double.
//Объявление пары совместно используемых функций...
extern void FF(char *); //Вариант 1...
extern void FF(int); //Вариант 2...
//Вызов функции.
FF('a');
Литера 'a' имеет тип char и значение, допускающее целочисленное расширение. Вызов сопоставляется со вторым вариантом совместно используемой функции.
3.Сопоставление со стандартным преобразованием. Применяется в случае неудачи сопоставления по двум предыдущим критериям сопоставления. Фактический параметр преобразуется в соответствии с правилами стандартных преобразований. Стандартное преобразование типа реализует следующие варианты сопоставления значений параметров в выражениях вызова и параметров объявления:
- любой целочисленный тип параметра выражения вызова сопоставляется с любым целочисленным типом параметра, включая unsigned,
- значение параметра, равное нулю, сопоставляется с параметром любого числового типа, а также с параметром типа указатель, а значение параметра типа указатель на объект (любого типа) будет сопоставляться с формальным параметром типа void*.
//Объявление пары совместно используемых функций...
extern void FF(char *); //Вариант 1...
extern void FF(float); //Вариант 2...
//Вызов функции.
FF(0);
В соответствии со вторым правилом стандартных преобразований, которое утверждает, что передача значения при вызове функции преобразует это значение в тип параметра функции, значение фактического параметра может быть преобразовано к значению указателя, т.е. к NULL. Вызов сопоставляется с первым вариантом функции.
Можно представить шкалу соответствия типа параметров в выражениях вызова параметрам множества совместно используемых функций. При этом:
- точное сопоставление формального и фактического параметров оценивается максимальным баллом по шкале соответствия параметров,
- сопоставление с расширением типа оценивается средним баллом,
- сопоставление со стандартным преобразованием оценивается низшим баллом по шкале соответствия,
- несоответствие фактического и формального параметров является абсолютным нулём нашей замечательной шкалы.
В качестве примера рассмотрим следующие ситуации сопоставления:
Объявляются четыре варианта совместно используемых функций.
extern void FF(unsgned int); //Вариант 1...
extern void FF(char*); //Вариант 2...
extern void FF(char); //Вариант 3...
extern void FF(int); //Вариант 4...
И ещё несколько переменных различных типов...
unsigned int iVal;
int *p_iVal;
unsigned long ulVal;
Рассмотрим вызовы функций.
- Успешные:
FF('a');
//Успешное сопоставление с вариантом 3.
FF("iVal");
//Успешное сопоставление с вариантом 2.
FF(iVal);
//Успешное сопоставление с вариантом 1.
- Неудачные:
FF(p_iVal);
//Сопоставления нет.
FF(ulVal);
/*
Поскольку по правилам стандартного преобразования тип unsigned long,
пусть с потерей информации, но всё же может быть преобразован в
любой целочисленный тип, сопоставление окажется неуспешным по причине
своей неоднозначности. Сопоставление происходит со всеми вариантами
функции за исключением функции, имеющей тип char*.
*/
Решение относительно вызова совместно используемой функции с несколькими параметрами принимается на основе алгоритма сопоставления параметров к каждому из параметров вызова функции. При этом применяется так называемое правило пересечения. Согласно этому правилу, из множества совместно используемых функций выбирается функция, для которой разрешение каждого параметра будет НЕ ХУЖЕ (баллы по шкале соответствия), чем для всего множества совместно используемых функций, и ЛУЧШЕ (баллы по шкале соответствия), чем для всех остальных функций, хотя бы для одного параметра. Например:
extern MyFFF(char*, int);
extern MyFFF(int, int);
MyFFF(0, 'a');
По правилу пересечения выбирается второй вариант функции. И происходит это по двум причинам:
- Сопоставление первого фактического параметра вызова функции и первого параметра второй функции оценивается высшим баллом по шкале соответствия параметров, поскольку константа 0 точно сопоставляется с формальным параметром типа int.
- Второй параметр вызова сопоставляется со вторым формальным параметром обеих функций. При этом литера 'a' имеет тип char и значение, допускающее целочисленное расширение. Таким образом, имеет место сопоставление с помощью расширения типа.
Вызов считается неоднозначным, если ни один из вариантов совместно используемых функций не даёт наилучшего сопоставления. Вызов также считается неоднозначным, если несколько вариантов функции дают лучшее сопоставление.
Известно, что значением выражения, состоящего из имени функции, является адрес данной функции. Подобные выражения для перегруженных функций недопустимы в силу своей неоднозначности. Транслятор просто не представляет, адрес какой из функций следует определять. Однако всё же определение адреса совместно используемых функций возможно. Это можно осуществить в контексте определения и инициализации указателя на функцию. Необходимую для выбора соответствующей перегруженной функции информацию транслятор получает из спецификации соответствующего указателя.
char* MyFF1 (int,int,int*,float);
char* MyFF1 (int,int*,float);
/* Прототипы перегруженных функций. */
:::::
char* MyFF1 (int key1, int key2, int* pVal, float fVal) {/* ... */}
char* MyFF1 (int XX, int* pXX, float FF) {/* ... */}
/* Определения перегруженных функций. */
:::::
char* (*fPointer1) (int,int,int*,float) = MyFF1;
/* Определение и инициализация указателя на первую функцию.
Транслятор делает правильный выбор. */
char* (*fPointer2) (int,int*,float);
/* Определение указателя на вторую функцию. */
fPointer2 = MyFF1;
/* И опять транслятор правильно выбирает соответствующую функцию. */
fPointer1(1,2,NULL,3.14);
fPointer2(1,NULL,3.14);
/* Вызовы функций по указателю. */
По крайней мере, в Borland C++ 4.5, аналогичная инициализация параметров-указателей на функции адресами совместно используемых функций недопустима. Можно предположить, что на этом этапе у транслятора нет ещё полной и достоверной информации обо всех совместно используемых функциях программы.
В разделе, посвящённом указателям на функции, в качестве примера была приведена функция, у которой в качестве параметра был указатель на функцию. Так вот попытка предварительной инициализации параметра-указателя адресом совместно используемой функции недопустимо. Соответствующие ограничания накладываются и на использование значения по умолчанию этого параметра при вызове функции.
void MonitorF(int,int,int*,float,char*(*)(int,int,int*,float)=MyFF1);
/*
Транслятор утверждает, что имя этих функций двусмысленно в
контексте инициализации.
*/
MonitorF(9,9,pIval,9.9);
/*
Использование значения параметра по умолчанию также невозможно.
*/
void MonitorF(int,int,int*,float,char*(*)(int,int,int*,float));
MonitorF(11,11,pIval,11.11,MyFF1);
/*
При явном указании имени функции в операторе вызова транслятор
однозначно идентифицирует функцию.
*/