C++.Бархатный путь

         

Перечень операций


Этот раздел содержит краткую предварительную информацию об операциях C++. Детальное описание большинства операций на этом этапе ещё невозможно. Графическое представление, название и назначение операции - сейчас этого вполне достаточно. Всё ещё впереди…

, , , , , , , , , , , , , , ,

Унарные операции

Адресные операции

Операция получения адреса операнда.

Операндом может быть любое l-выражение. Операция возвращает адрес объекта или функции, на который ссылается операнд. Операция невыполнима по отношению к объектам, определённым со спецификатором register, поскольку существует вероятность того, что они не располагаются в памяти и не имеют определённого адреса.

* Операция обращения по адресу или операция косвенного обращения.

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

Операции преобразования знака

- Операция унарный минус.

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

+ Операция унарный плюс.

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

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

В формальном языке каждая лексема имеет своё название и назначение. И этот самый плюс-украшение также является операцией. А дальше - рушится иллюзия симметрии унарных операций. Унарный минус работает. Он эквивалентен умножению значения операнда на -1. Унарный плюс эквивалентен умножению значения операнда на +1. Он ничего не делает.


Побитовые операции

~ Операция инвертирования или побитового отрицания.

Операндом может быть любое выражение интегрального типа. Операция обеспечивает побитовое инвертирование двоичного кода.

! Операция логического отрицания.

Операндом может быть любое выражение со значением арифметического типа. Для непосредственного обозначения логических значений в C++ используются целочисленные значения 0 - ложь и 1 - истина. Кроме того, в логических операциях любое ненулевое значение операнда ассоциируется с единицей. Поэтому отрицанием нулевого значения является 1, т.е. истина, а отрицанием любого ненулевого значения оказывается 0, т.е. ложь.

Операция определения размера



sizeof Операция определения размера объекта или типа.

В C++ различают два варианта этой операции. В первом случае операндом может быть любое l-выражение. Это выражение записывается справа от символа операции. Значением выражения является размер конкретного объекта в байтах. Во втором случае операндом является имя типа. Это выражение записывается в скобках непосредственно за символом операции. Значением выражения является размер конкретного типа данных в байтах. Результатом этой операции является константа типа size_t. Этот производный целочисленный беззнаковый тип определяется конкретной реализацией.

Операции увеличения и уменьшения значения

++ Инкремент, или операция увеличения на единицу.

Точнее, на величину, кратную единице, поскольку всё зависит от типа операнда. Операция имеет дополнительный эффект - она изменяет значение операнда. Поэтому операндом здесь может быть только леводопустимое выражение арифметического типа, либо типа указателя. В C++ различают префиксную и постфиксную операции инкремента.

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

В постфиксной форме увеличение значения операнда производится после определения значения выражения. Поэтому значение выражения оказывается меньше значенния операнда.

В выражении с префиксной операцией увеличения знак ++ записывается слева от операнда, в выражении с постфиксной операцией - справа.

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



-- Операция уменьшения значения операнда на величину, кратную единице (декремент).

Эта операция в буквальном смысле симметрична операции инкремента. Имеет аналогичный дополнительный эффект, соответствующие ограничения для операнда (свойство леводопустимости, арифметический тип, либо тип указателя, префиксную и постфиксную формы, изменение значения адреса).

В выражении с префиксной операцией увеличения знак -- записывается слева от операнда, в выражении с постфиксной операцией - справа.

Операции динамического распределения памяти

new Операция выделения памяти.

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

delete Операция освобождения памяти.

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

Операция доступа

:: Операция доступа.

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

int m; // Глобальная переменная. ::::: int mmm() { int m; // Локальная переменная. m = 100; // Присвоение значения локальной переменной. ::m = 125; // Присвоение значения глобальной // переменной m, находящейся вне области видимости // имени. }

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

Бинарные операции

Аддитивные операции

+ Операция сложения.



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

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

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

- Операция вычитания.

Симметричная по отношению к операции сложения бинарная операция.

Мультипликативные операции

* Операция умножения.

Операндами могут быть выражения арифметического типа. Значением выражения является произведение значений. Тип результата выражения любой бинарной операции определяется в зависимости от принятых в C++ процедур преобразования типов данных.

/ Операция деления.

Операндами могут быть выражения арифметического типа. Значением выражения является частное от деления значения первого операнда на второй операнд. Тип результата выражения любой бинарной операции определяется в зависимости от принятых в C++ процедур преобразования типов данных.

% Операция получения остатка от деления целочисленных операндов (деление по модулю).

Операндами могут быть выражения арифметического типа. В процессе выполнения операции операнды приводятся к целому типу. При неотрицательных операндах остаток положительный. В противном случае знак остатка определяется в зависимости от реализации. Известно, что для Borland C++ 15%6=3, (-15)%6=-3, 15%(-6)=3, (-15)%(-6)=-3.

При ненулевом делителе для целочисленных операндов выполняется соотношение (a/b)*b+a%b=a

Операции сдвига

Эти операции определены только для целочисленных операндов.

Операция левого сдвига.

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



Операция правого сдвига.

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

Поразрядные операции

Поразрядные операции определены только для целочисленных операндов.

Поразрядная конъюнкция битовых представлений значений целочисленных операндов.

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

Бит левого операнда Бит правого операнда Результат операции
/td> /td> /td>
/td> /td> /td>
/td> /td> /td>
/td> /td> /td>
| Поразрядная дизъюнкция битовых представлений значений целочисленных операндов.

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

Бит левого операнда Бит правого операнда Результат операции |
/td> /td> /td>
/td> /td> /td>
/td> /td> /td>
/td> /td> /td>
^ Поразрядная исключающая дизъюнкция битовых представлений значений целочисленных операндов.

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

Бит левого операнда Бит правого операнда Результат операции ^
/td> /td> /td>
/td> /td> /td>
/td> /td> /td>
/td> /td> /td>
Операции сравнения

,=,,=,==,!= Меньше, меньше равно, больше, больше равно, равно, не равно.



Операции сравнения определены на множестве операндов арифметического типа. Допускается также сравнение значений адресов в памяти ЭВМ. Следующая таблица демонстрирует зависимость результата сравнения от значений операндов Val1 и Val2. Результат сравнения всегда целочисленный и может принимать одно из двух значений: 0 и 1. При этом 0 означает ложь, а 1 - истину.

Операция если если
Val1 меньше Val2 Val1 больше или равно Val2
= Val1 меньше или равно Val2 Val1 больше Val2
Val1 больше Val2 Val1 меньше или равно Val2
= Val1 больше или равно Val2 Val1 меньше Val2
== Val1 равно Val2 Val1 не равно Val2
!= Val1 не равно Val2 Val1 равно Val2
Логические бинарные операции

, И, ИЛИ.

Логические бинарные операции объединяют выражения сравнения со значениями истина (!=0) и ложь (==0). Результат операций приведён в следующей таблице

Первый операнд Второй операнд
Истина истина /td> /td>
Истина ложь /td> /td>
Ложь истина /td> /td>
Ложь ложь /td> /td>
Операция присваивания

= Простая форма операции присваивания.

Левый операнд операции присваивания является леводопустимым выражением.

В качестве правого операнда операции присваивания может выступать любое выражение. Значение правого операнда присваивается левому операнду. Значение выражения оказывается равным значению правого операнда. Не существует никаких ограничений на структуру этого операнда. Правый операнд может состоять из множества выражений, соединенных операциями присвоения: An=…=A3=A2=A1;

где A1, A2, A3, …, An являются выражениями. Для определения значений выражений подобной структуры в C++ существуют правила группирования операндов выражений сложной структуры (эти правила подробно будут описаны ниже). В соответствии с одним из этих правил операнды операции присвоения группируются справа налево: An=(An-1=…=(A3=(A2=A1))…);

Очевидно, что в таком выражении все операнды, кроме самого правого, должны быть модифицируемыми l-выражениями. В результате выполнения этого выражения операндам An, An-1, … A3, A2 будет присвоено значение операнда A1.

Специальные формы операций присваивания



В процессе трансляции выражений на этапе генерации кода транслятор строит последовательности машинных кодов, реализующие закодированные в выражениях действия. Например, при трансляции выражения 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-выражением.

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

Операции выбора компонентов структурированного объекта

К операциям выбора компонентов структурированного объекта относятся:

. Операция прямого выбора - точка. - Операция косвенного выбора.

Об этих операциях будет сказано позже, после определения понятия класса и объекта-представителя класса.

Операции обращения к компонентам класса

К операциям обращения к компонентам класса относятся:

.* Операция обращения к компоненте класса по имени объекта или ссылки на объект (левый операнд операции) и указателю на компоненту класса (правый операнд операции). -* Операция обращения к компоненте класса по указателю на объект (левый операнд операции) и указателю на компоненту класса (правый операнд операции). :: Операция доступа к компоненте класса по имени класса и имени компоненты.

Операция управления процессом вычисления значений

, Операция запятая.

Группирует выражения слева направо. Разделённые запятыми выражения вычисляются последовательно слева направо, в качестве результата сохраняются тип и значение самого правого выражения. A = B, A * B, -A

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

Операция вызова функции



() Операция вызова.

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

Операция явного преобразования типа

() Операция преобразования (или приведения) типа.

Эта бинарная операция в контексте так называемого постфиксного выражения и в контексте выражения приведения обеспечивает изменение типа значения выражения, представляемого вторым операндом. Информация о типе, к которому преобразуется значение второго операнда, кодируется первым выражением, которое является спецификатором типа. Существуют две формы операции преобразования типа: каноническая, при которой в скобки заключается первый операнд (в выражениях приведения), и функциональная (в постфиксных выражениях), при которой в скобки заключается второй операнд. При функциональной форме операции преобразования типа спецификатор типа представляется одним идентификатором. Для приввведениия значения к типу unsigned long следует использовать лишь каноническую форму операции преобразования. Механизм преобразования типа рассматривается ниже

Операция индексации

[] Операция индексации.

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

Операция с тремя операндами

?: Условная операция.

Единственная в C++ операция с тремя операндами. Первое выражение-операнд располагается слева от знака ?, второе выражение-операнд располагается между знаками ? и :, третье выражение-операнд - справа от знака :. Выполнение условной операции начинается с вычисления значения самого левого операнда. Если его значение оказывается отличным от 0, то вычисляется значение второго операнда, которое и становится значением выражения. Если значение первого операнда оказывается равным 0, то вычисляется значение третьего операнда, и тогда это значение становится значением выражения. (x 10)? x = 25: x++

Операция typeid

Операция typeid обеспечивает динамическую идентификацию типов. Пока лишь упомянем о её существовании, поскольку её описание требует углублённых познаний в области объектно-ориентированного программирования.


Переопределение конструктора копирования


Упомянутая нами в предыдущем разделе аксиома о конструкторе копирования имеет одно интересное следствие.

В классе X в принципе не может быть объявлено конструктора с ЕДИНСТВЕННЫМ параметром типа X. Это происходит из-за того, что выражение вызова такого конструктора просто невозможно будет отличить от выражения вызова конструктора копирования. Не бывает совместно используемых функций с неразличимыми выражениями вызова.

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

Итак, конструктор копирования предназначается для копирования объектов. Он также участвует в реализации механизма передачи параметров при вызове функций.

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

ComplexType(const ComplexType ctVal) { cout "Здесь конструктор копирования" endl; } ; //^ В теле класса ComplexType мы имеем право на эту точку с запятой…

Несмотря на пустое тело, перед нами настоящий конструктор копирования. Всякий конструктор, параметром которого является ссылка на объект-константу, представляющий данный класс, называется конструктором копирования. Даже если этот конструктор ничего не копирует.

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

С момента появления переопределённой версии конструктора копирования, вся работа по реализации алгоритмов копирования возлагается на программиста. Переопределённый конструктор копирования может вообще ничего не копировать (как и наш новый конструктор). Впрочем, заставить конструктор копирования копировать объекты совсем несложно:

ComplexType(const ComplexType ctVal) { cout "Здесь конструктор копирования" endl; real = ctVal.real; imag = ctVal.imag; CTcharVal = ctVal.CTcharVal; x = ctVal.x; }

Но конструктор, создающий подобные копии объектов, скорее всего, окажется непригодным для работы с объектами, содержащими указатели или ссылки. Не самым удачным решением является ситуация, при которой данные-члены типа char*, их нескольких объектов, возможно расположенных в различных сегментах памяти, в результате деятельности конструктора копирования настраиваются на один и тот же символьный массив.

В переопределяемом конструкторе копирования (а в классе он может быть только один) можно реализовывать разнообразные алгоритмы распределения памяти. Здесь всё зависит от программиста.



Первичное выражение


Выражение строится на основе операций, объединяющих операнды. Основным элементом выражения является первичное выражение. Первичное выражение - это фактически элементарный строительный блок любого выражения. Следующее множество БНФ определяет синтаксис первичного выражения:

ПервичноеВыражение ::= Литерал ::= Имя

::= (Выражение) ::= this ::= ::ИмяОператорнойФункции

::= ::КвалифицированноеИмя

::= ::Идентификатор

Понятие литерала ранее уже обсуждалось.

Нетерминальный символ Имя также определяется с помощью соответствующего множества БНФ:

Имя ::= Идентификатор ::= ИмяОператорнойФункции

::= ИмяФункцииПриведения

::= КвалифицированноеИмя

::= ~ИмяКласса

Таким образом, квалифицированное имя является одним из вариантов имени. Оба нетерминальных символа, в свою очередь, представляют первичные выражения.

В C++ не существует выражений без типа и значения. Даже в том случае, когда говорят, что значение выражения не определено, выражение всё же имеет значение соответствующего типа. Это случайное значение.

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

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

Сложность первичного выражения ничем не ограничивается. Заключённое в круглые скобки выражение рассматривается как первичное выражение.

Первичное выражение this связано с понятием класса. Оно также имеет собственный тип и значение, в качестве которого выступает указатель на объект.

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

Ничего нельзя сказать о том, что находится вне области видимости. Для транслятора C++ это потусторонний мир. И поэтому не случайно в соответствующей форме Бэкуса-Наура после операции разрешения области видимости используется терминальный символ Идентификатор, а не Имя. Идентификатор становится именем лишь после соответствующего объявления. В момент выполнения операции разрешения области видимости нельзя утверждать, что её операнд является именем.

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



Побитовые выражения


ВыражениеИлиВключающее ::= ВыражениеИлиИсключающее

::= ВыражениеИлиВключающее | ВыражениеИлиИсключающее

ВыражениеИлиИсключающее ::= ВыражениеИ

::= ВыражениеИлиИсключающее ^ ВыражениеИ

ВыражениеИ ::= ВыражениеРавенства

::= ВыражениеИ ВыражениеРавенства



Постфиксное выражение


Постфиксное выражение определяется на основе первичного выражения. Соответствующее множество БНФ включает множество разнообразных альтернатив.

ПостфиксноеВыражение ::= ПервичноеВыражение

::= ПостфиксноеВыражение [Выражение] ::= ПостфиксноеВыражение ([СписокВыражений]) ::= ПостфиксноеВыражение.Имя

::= ПостфиксноеВыражение-Имя

::= ПостфиксноеВыражение++ ::= ПостфиксноеВыражение- СписокВыражений ::= ВыражениеПрисваивания

::= СписокВыражений, ВыражениеПрисваивания

Первичное выражение является частным случаем постфиксного выражения. Вторым в списке возможных альтернатив постфиксных выражений является БНФ, представляющая различные варианты выражений индексации. Это выражение строится из двух выражений - постфиксного (первичного) выражения, за которым следует ещё одно выражение (второй операнд операции индексации), заключённое в квадратные скобки. Обычно первое выражение представляет указатель на объект типа X (пока неважно, какого типа объект), второе выражение является выражением интегрального типа. Это выражение называется индексом.

Следующей альтернативой является БНФ, представляющая выражения вызова. В нём также участвуют два выражения. Первое выражение может быть представлено именем функции, указателем или ссылкой (частный случай указателя). Список выражений в круглых скобках (второй операнд операции вызова) определяет значения множества параметров, которые используются при означивании соответствующих параметров вызываемой функции.

Выражения явного преобразования типа (в функциональной форме) являются ещё одним вариантом постфиксного выражения. Это выражение начинается с имени простого типа (простой тип - не обязательно основной). В круглых скобках заключается список выражений (второй операнд операции преобразования), на основе которого формируется значение типа, заданного первым элементом выражения. Выражение явного преобразования может содержать пустой список значений. В этом случае результатом выполнения подобной операции также оказывается значение (неважно какое) заданного простого типа. Здесь важен именно тип значения. Само же значение зависит от разных обстоятельств. Оно вообще может оказаться неопределённым, а может определяться в ходе выполнения программы.

Следующие две БНФ представляют схемы выражений доступа к члену класса. Они будут рассмотрены позже.

Наконец, последняя пара БНФ представляет постфиксные выражения увеличения и уменьшения. Эти выражения представляют собой сочетания символов (именно символов!) операций с выражениями-операндами. Операнды выражений инкремента и декремента обязаны быть модифицируемым l-выражениями.



Правила образования идентификаторов


Рассмотрим правила построения идентификаторов из букв алфавита (в C++ три):

Первым символом идентификатора C++ может быть только буква. Следующими символами идентификатора могут быть буквы, буквы-цифры и буквы-подчерки. Длина идентификатора неограниченна (фактически же длина зависит от реализации системы программирования).

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



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


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

Это означает, что выражение для вычисления суммы двух слагаемых уже известного нам типа ComplexType по своей структуре не должно отличаться от соответствующих выражений для слагаемых типа int или float. Но большинство операций языка C++ определены лишь для основных типов данных. Использование в качестве операндов операций выражений производных типов вызывает ошибки трансляции. Поэтому в классе ComplexType и были определены специальные функции-члены, реализующие арифметические операции над множеством комплексных чисел.

И всё же возможность сохранения привычной структуры выражений для производных типов в C++ существует.

Вернёмся к известному классу ComplexType. Мы определим два объекта класса ComplexType, после чего воспользуемся операцией присвоения.

ComplexType ctVal1(3.14, 0.712); ComplexType ctVal2, ctVal3; /* Комплексные числа со случайными значениями данных-членов.*/ ::::: ctVal2 = ctVal1; ctVal3 = ctVal2 = ctVal1; /* Операция присвоения коммутативна.*/

Если теперь вывести значения данных-членов объектов ctVal2 и ctVal3, то окажется, что они полностью совпадают со значениями данных-членов объекта ctVal1. Операция присваивания изначально определена для объектов класса ComplexType. Её можно рассматривать как предопределённую операцию, которая обеспечивает фактически побитовое копирование объекта, стоящего справа от символа = в объект, расположенный слева от этого знака.

Подобно любому другому выражению, выражение присваивания имеет собственное значение. Это значение равно выражению, стоящему справа от операции =. В ходе выполнения программы изменяется значение l-выражения в выражении присваивания, после чего значение этого выражения оказывается равным изменённому значению этого самого l-выражения.

Строго говоря, операцию присваивания для объектов производных типов нельзя называть операцией. Как и ранее рассмотренные нами "операции" приведения, она является операторной функцией. Это значит, что при её объявлении используется специальное имя, состоящее из ключевого слова operator с символом операции, а для её вызова можно использовать полную и сокращённую форму.

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

Работу по объявлению этой функции мы начнём с того, что попытаемся представить её общий вид:


функция объявляется как нестатический член класса и вызывается для объекта, которому надо присвоить соответствующее значение; её имя состоит из ключевого слова operator, за которым, очевидно, следует символ = ; основное назначение функции состоит в присваивании значения (множества значений) одного объекта другому. Следовательно, операторная функция operator=() должна иметь, по крайней мере, один параметр, который должен представлять присваиваемое значение; и последнее, очень важное обстоятельство. Операторная функция operator=() должна возвращать новое значение объекта и по форме вызова должна создавать видимость коммутативности, поскольку этим свойством обладает операция присвоения.

class ComplexType { ::::: ComplexType operator = (const ComplexType ); /* Ссылка на константу при объявлении параметра не является обязательным условием для объявления операторной функции. Но это гарантия того, что присваиваемое значение не будет изменено в результате обращения к данным-членам. Операторная функция operator=() возвращает значение (именно значение!) объект класса ComplexType. Это не самый оптимальный способ обеспечения коммутативности операторной функции. Но при этом обеспечивается подобие операторной функции операции присваивания. */ ::::: } ::::: ComplexType ComplexType::operator = (const ComplexType ctKey) { cout "This is operator = (ComplexType ctKey)..." endl; /* Подтверждение о том, что выполняется именно эта функция. */ this-real = ctKey.real; this-imag = ctKey.imag; this-CTcharVal = ctKey.CTcharVal; this-x = ctKey.x; /* Теперь вся ответственность за корректность процесса копирования целиком и полностью возлагается на программиста. */ return *this; /* Мы возвращаем значение объекта, представленного this указателем. */ } ::::: /* Будем считать, что объекты ctVal1 и ctVal2 уже определены. Осталось рассмотреть варианты вызовов этой функции. */ ctVal2.operator = (ctVal1); /* Вариант полной формы вызова функции.*/ ctVal2 = ctVal1; /* Вариант сокращённой формы вызова функции. Операция обращения, ключевое слово operator в составном имени операторной функции и скобки, заключающие выражение, представляющее значение параметра опускаются. Создаётся иллюзия использования обычной операции присваивания. */ /* Демонстрация коммутативности операторной функции присваивания. */ ctVal3.operator = (ctVal2.operator = (ctVal1)); /* Операторная функция operator=() вызывается непосредственно из объекта ctVal3 со значением атрибута (ссылкой на объект), который сам в свою очередь является результатом применения операторной функции operator=() к объекту ctVal2 с параметром-ссылкой на объект ctVal1. Всё очень просто и красиво! */ ctVal3 = ctVal2 = ctVal1; /* Сокращённая форма коммутативного вызова операторной функции присваивания. */



При объявлении и определении операторных функций (в том числе и operator=() ), используется синтаксическая конструкция, обозначаемая в терминах формальной грамматики нетерминальным символом ИмяФункцииОперации. Несколько форм Бэкуса-Наура позволяют однозначно определить это понятие:

Имя ::= ИмяФункцииОперации

::= *****

ИмяФункцииОперации ::= operator СимволОперации

СимволОперации ::= +|-|*|?|%|^||~|!|,|=|||=|=|++|--|||==|!=|| |+=|-=|*=|=|=|[]|()|-|-*|new|delete|

Как следует из приведённых БНФ, большинство символов операций языка C++ могут участвовать в создании так называемых имён функций операций или операторных функций. То есть на основе этих символов можно объявлять операторные функции, сокращённая форма вызова которых позволяет создавать видимость применения операций к объектам производных типов.

C++ не накладывает никаких ограничений на семантику этих самых операторных функций. Наша операторная функция operator=() могла бы вообще не заниматься присвоением значений данных-членов. Она могла бы не возвращать никаких значений. Само собой, что тогда выражение вызова этой функции не могло бы быть коммутативным. А единственный параметр можно было бы передавать по значению. Но тогда всякий раз при вызове функции неизбежно должен был бы вызываться конструктор копирования, который бы создавал в области активации функции копию объекта, которую впоследствии должен был бы разрушать деструктор.

Операторная функция operator=(), как и любая другая функция, может быть перегружена. Например, объявление параметра типа int, позволило бы присваивать комплексным числам целочисленные значения. Здесь нет пределов совершенствования. В принципе, механизм операторных функций регламентирует лишь внешний вид заголовка функции (его "операторное" имя, количество параметров, в ряде случаев - возвращаемое значение). Информация о заголовке принципиальна, поскольку от этого зависит форма сокращённого вызова операторной функции.

Ещё несколько замечаний по поводу спецификации возвращаемого значения операторной функции.

Операторная функция operator=() может вообще не возвращать никаких значений. Сокращённая форма вызова ctVal2 = ctVal1;



с точки зрения транслятора абсолютно корректна и полностью соответствует следующим прототипам:

void ComplexType::operator = (const ComplexType ctKey); void ComplexType::operator = (ComplexType ctKey); void ComplexType::operator = (ComplexType ctKey);

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

С другой стороны, уже существующий вариант нашей операторной функции также может быть оптимизирован. Функция может возвращать не ОБЪЕКТ (ЗНАЧЕНИЕ), а ССЫЛКУ на объект.

В этом случае при возвращении значения не будет создано временного объекта. Также не будет вызываться деструктор для его уничтожения. Модификация операторной функции operator=() минимальна - всего лишь дополнительная ptrОперация в спецификации возвращаемого значения (мы приводим здесь только прототип новой версии функции): ComplexType operator = (const ComplexType );

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

Следующий пример является подтверждением того факта, что при объявлении операторных функций полностью отсутствуют чёткие правила. Это подтверждает следующий пример, посвящённый объявлению и вызову различных вариантов операторных функций operator():

ComplexType operator () (const ComplexType); /* Первый вариант совместно используемой функции operator().*/ void operator () (int); /* Второй вариант совместно используемой функции operator().*/ ::::: /* Определения этих функций. Как всегда, они не делают ничего полезного… */ ComplexType ComplexType::operator () (const ComplexType ctKey) { cout "This is operator (ComplexType ctKey)..." endl; return *this; } void ComplexType::operator () (int iKey) { cout "This is operator ( " iKey " )..." endl; } ::::: /* Полные и сокращённые формы вызова этих функций. Первая операторная функция коммутативна. */ CDw2.operator()(CDw1); CDw2(CDw1); CDw3.operator()(CDw2.operator()(CDw1)); CDw3(CDw2(CDw1)); CDw2.operator()(25); CDw2(50);



И это ещё не всё! Ещё не рассматривались варианты операторной функции operator() с несколькими параметрами. И здесь следует вспомнить о функциях с переменным количеством параметров. Это не единственный, но наиболее оптимальный подход к объявлению операторной функции operator() с несколькими параметрами. Здесь мы не будем вдаваться в детали алгоритма извлечения информации из списка параметров (мы их уже обсуждали раньше), а ограничимся лишь общей схемой объявления и вариантами выражения вызова. В нашей версии (всего лишь одной из возможных!), первым параметром функции всегда будет целое число:

ComplexType operator () (int, ...);// Прототип. ::::: ComplexType ComplexType::operator () (int iKey, ...) { cout "This is operator ( " iKey ", ...)" endl; return *this; } ::::: CDw2(50); CDw2(50, 100); CDw2(50, "Это тоже вызов операторной функции", 3.14, 0,123456789);

В C++ может быть объявлено более трёх десятков различных вариантов операторных функций. К этому выводу приводит анализ списка символов операций, которые потенциально могут входить в качестве элемента имени операции.

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

Как известно, операция косвенного обращения - является бинарной операцией. Её первым операндом является указатель на объект, вторым - имя члена класса.

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

Рассмотрим различные варианты объявления, определения и вызова этой операторной функции.

Первый вариант тривиален:

::::: ComplexType* operator - (); ::::: ComplexType* ComplexType::operator - () { cout "This is operator - ()..." endl; return this; } :::::



Таково, в общих чертах, объявление и определение функции. Функция без параметров.

::::: if (CDw2.operator-() == NULL) cout "!!!" endl; :::::

Это полная форма вызова в выражении равенства в составе условного оператора.

::::: CDw3-real = 125.07; (CDw3.operator-())-real = 125.07; :::::

Сокращённая и полная формы вызова операторной функции в составе оператора присвоения. Функция возвращает адрес, к которому применяется обычная двухместная операция косвенного обращения.

А вот более простого варианта сокращённой формы вызова функции operator-(), наподобие того, который ранее использовался в составе условного оператора, в C++ не существует. Правильно построенных выражений вида (xObject-) с единственным операндом, где - является символом операции, в C++ нет, поскольку - бинарная операция.

Из-за того, что не всегда удаётся различить по контексту выражение вызова функции и операцию косвенного обращения, сокращённый вызов операторной функции operator-() используется исключительно для имитации выражений с операцией косвенного обращения.

Операторная функция operator-() возвращает указатель на объект, и как любая нестатическая функция-член класса должна вызываться непосредственно "из объекта". Эта прописная истина не представляла бы никакого интереса, если бы в C++ существовали жёсткие ограничения на тип возвращаемого значения функции-члена класса. Но таких ограничений для операторных функций в C++ не существует, а потому возможны и такие экзотические варианты операторных функций:

::::: class ComplexType { ::::: }; ::::: class rrr // Объявляется новый класс. { public: ComplexType* pComplexVal; // Собственные версии конструкторов и деструкторов. rrr () { pComplexVal = new ComplexType; // Порождение собственного экземпляра объекта ComplexType. } ~rrr () { if (pComplexVal) = delete pComplexVal; } // Наконец, встроенная операторная функция. ComplexType* operator - () { cout "This is operator - ()..." endl; return pComplexVal; } }; ::::: // А это уже собственно фрагмент программы… rrr rrrVal; // Определяем объект - представитель класса rrr. cout rrrVal -real " real." endl; ::::: Сокращённая форма вызова операторной функции operator-() имеет вид rrrVal-real и интерпретируется транслятором как (rrrVal.operator-())-real, о чём и свидетельствует оператор, содержащий полную форму вызова этой операторной функции. ::::: cout (rrrVal.operator-())-imag " imag." endl; :::::



В этом случае из объекта- представителя класса rrr вызывается операторная функция, в обязательном порядке возвращающая адрес объекта-представителя класса ComplexType, к которому сразу же (!) применяется операция косвенного обращения.

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

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

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


Предварительная инициализация параметров функции


Список параметров в определении и прототипе функции, кроме согласования типов параметров, имеет ещё одно назначение.

Объявление параметра может содержать инициализатор, то есть выражение, которое должно обеспечить параметру присвоение начального значения. Инициализатор параметра не является константным выражением. Начальная инициализация параметров происходит не на стадии компиляции (как, например, выделение памяти под массивы), а непосредственно в ходе выполнения программы.

Следующие строки демонстрируют пример объявления функции с инициализацией параметров. Для инициализации параметра 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++ обеспечивается выполнением операций и вызовом функций. Операции используют операнды, функции требуют параметров. Операнды и параметры характеризуются типом. В C++ не существует операций, которые, например, обеспечивали бы сложение или умножение операндов различных типов. Выражения вызова функций также требуют соответствия типа параметров типу параметров определения и прототипа.

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

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

Присвоение "большему типу" значения "меньшего типа". Безопасное присвоение, гарантирует сохранение значения. unsigned int UnsignedIntVal; unsigned char UnsignedCharVal; UnsignedIntVal = UnsignedCharVal;

Присвоение "меньшему типу" значения "большего типа". Потенциально опасное присвоение, грозит потерей информации. int IntVal; char CharVal; CharVal = IntVal;

Преобразование значения из "меньшего типа" в "больший тип". Называется расширением типа. (unsigned int)UnsignedCharVal;

Преобразование значения из "большего типа" в "меньший тип". Называется сужением типа. Является опасным преобразованием. (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.


Программный модуль


Программа строится на основе программных модулей. Модуль состоит из элементов программного модуля. В модуле нет ничего, кроме инструкций препроцессора и (или) списков операторов.

Как сказано в справочном руководстве по C++, файл состоит из последовательности объявлений.

Здесь нет ничего странного: определение является частным случаем объявления (например, объявление, содержащее инициализацию).

Сложность оператора практически ничем не регламентируется, к ним, в частности, относятся объявления и определения объектов, объявления (или прототипы) и определения функций.

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

Тело функции представляет собой блок операторов - список операторов (опять!), заключаемый в фигурные скобки.



Пространство имён


С понятием области действия имени связано понятие пространства имени.

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

Пространство имён глобальных объектов. Это пространство образуется множеством образующих программу программных модулей. Имена глобальных объектов должны быть уникальны среди множества имён глобальных объектов во всех модулях, образующих программу. Пространство имен поименованных операторов (или операторов с меткой) - функция. Имя оператора должно быть уникально в теле функции, в которой метка была введена в программу. Пространство имён структур, классов, объединений и перечислимых типов зависит от контекста, в котором были объявлены структуры, классы, объединения. Если они были объявлены в блоке - это пространство будет составлять блок, если они были объявлены в модуле, таковой областью является программа. C++ помещает эти имена в общее пространство имён. Имена элементов структур, классов, объединений и перечислимых данных должны быть уникальны в пределах определения структуры, класса, объединения и перечислимых данных. При этом в разных структурах, классах, объединениях и перечислимых данных допустимы элементы с одинаковыми именами. Пространством имён для элементов структур, классов, объединений и перечислимых данных элементов являются сами структуры, классы, объединения и перечисления. Имена переменных и функций, имена пользовательских типов (типов, определённых пользователем - о них также немного позже) должны быть уникальны в области определения: глобальные объекты должны иметь уникальное имя среди всех глобальных объектов и т.д.

По крайней мере в реализациях C++, для процессоров использующих сегментированную модель памяти, существует определённая связь между пространством имени и расположением поименованного объекта в конкретном сегменте памяти. В пределах определённого сегмента может находиться лишь один объект с уникальным именем. В противном случае возникли бы проблемы с организацией ссылок на располагаемые в сегменте памяти объекты.

Вместе с тем, одни и те же имена могут использоваться при организации ссылок на объекты, располагаемые в разных сегментах памяти. Например, в теле функции можно обращаться как к глобальному объекту, так и к одноимённому локальному объекту, определённому в теле функции.

Правда, обращение к одноимённым объектам, расположенным в различных пространствах имён, ограничено специальными правилами. В связи с необходимостью организации специального протокола обращения к одноимённым объектам, располагаемым в различных сегментах памяти, в C++ возникло понятие области видимости.



Работа системы управления исключением


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

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

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

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

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

И если исключительная ситуация возникает в цикле - пусть её перехватчик остановит цикл. А вопросы эффективности и корректной работы со стеком - это вопросы к транслятору.

#include iostream.h #include string.h /* "Рабочее тело" одного из исключений. На его основе создаётся объект исключения. */ class MyException { public: int CopyKey; char *ExcMessage; // Конструктор умолчания. MyException(): ExcMessage("Стандартное сообщение от MyException...") { CopyKey = 0; } // Конструктор копирования. MyException(const MyException MyExcKey) { cout "Работает конструктор копии..." endl; ExcMessage = strdup(MyExcKey.ExcMessage); CopyKey = 1; // Признак копии для деструктора. } // Деструктор освобождает динамическую память. ~MyException() { if (CopyKey ExcMessage) delete(ExcMessage); } }; int MyFun() throw (int, char *); int Fun2() throw (int); void main() throw (MyException) { int RetMainVal; for (RetMainVal = 0; RetMainVal = 0; ) { try { RetMainVal = MyFun(); cout "RetMainVal == " RetMainVal endl; if (RetMainVal == 9) throw MyException(); /* Вызов конструктора для создания безымянного объекта - представителя класса MyException в точке возбуждения исключения (с использованием выражения явного преобразования типа). После этого код, расположенный ниже точки генерации исключения уже не выполняется. */ cout "Последний RetMainVal не был равен 9!" " Иначе были бы мы здесь..." endl; } // Место расположения перехватчиков исключений. catch (int ExcVal) { cout "(int) ExcVal == " ExcVal endl; } catch (char *ExcMessage) { cout "(char *) ExcMessage " ExcMessage endl; } catch (MyException ExcObj) /*


Безымянный объект, созданный в точке возбуждения исключения, инициализирует параметр обработчика исключения. С этой целью нами был определен специальный конструктор копирования. */ { cout ExcObj.ExcMessage "... Такое вот сообщение пришло" endl; /* После завершения выполнения блока обработки исключения, параметр обработчика уничтожается. Для этого мы определили собственную версию деструктора. */ } cout "За пределами tryБлока: RetMainVal == " RetMainVal endl; // cout ExcMessage "!!!" endl; // Обработчик исключений определяет собственную область действия. // ExcMessage оказывается за пределами области действия имени. } cout "Это конец работы программы." " И чтобы больше никаких перехватов..." endl; } int MyFun() throw (int, char *) { int Answer, RetMyFunVal; cout "MyFun "; cin Answer; cout Answer endl; switch (Answer) { case 1: throw 1; cout "Когда рак на горе свистнет, тогда это сообщение появится."; break; case 2: throw "XXX"; case 3: RetMyFunVal = Fun2(); cout "Вернулись из Fun2(). RetMyFunVal = " RetMyFunVal endl; break; } cout "Привет из MyFun..." endl; return Answer; } int Fun2() throw (int) { int Answer; cout "Fun2 "; cin Answer; cout Answer endl; switch (Answer) { case 0: throw 1; /* После возбуждения исключения, процесс нормального выполнения программы прерывается. Мы уже не попадаем в точку возврата функции. Используя стек, минуем функцию MyFun и оказываемся непосредственно в catch-блоке функции main, связанном с исключением типа int. */ default: Answer *= 2; } cout "Конец работы в Fun2." endl; return Answer; }

Перед нами программа-полигон для демонстрации взаимодействия генераторов исключений и перехватчиков. Функция main содержит контролируемый блок операторов. Наряду с другими операторами, он составляет тело оператора цикла for.

Функция возвращает значение определённого типа. Тип возвращаемого значения является важной характеристикой функции. Спецификация возвращаемого значения явным образом указывается при объявлении и определении функции. В различных ситуациях та же функция может возбуждать исключения совершенно разных типов и классов. Средством контроля над типами возбуждаемых исключений как раз является спецификация исключений. Этот необязательный элемент в заголовке обеспечивает дополнительный контроль над функцией со стороны транслятора. Хотя функция и может без предварительной спецификации возбуждать любые исключения, им не следует пренебрегать.

Транслятор следит за тем, чтобы не нарушались области действия имён объектов. Областью действия переменной, объявленной непосредственно в try-блоке, является данный try-блок. Соответственно, областью действия переменной, объявленной в одном из catch-блоков, этот самый catch-блок.

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

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

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

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

И вот, наконец, свершилось! В ходе выполнения контролируемого кода, непосредственно в try-блоке или в теле одной из вызываемых из этого блока функций возникает ситуация, которая может быть квалифицирована как исключительная. Реакцией на неё является возбуждение с помощью throw-оператора соответствующего исключения. С этого момента весь ход выполнения программы меняется.

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

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

Тем более отменяется выполнение каких-либо операторов вызова. Точка генерации исключения в определённом смысле оказывается действительно точкой. В этой самой точке принципиально меняется весь дальнейший ход выполнения программы. Сразу после возбуждения исключения начинается поиск соответствующего блока перехвата исключения.

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

Существуют чёткие критерии соответствия блока перехвата и возбуждённого исключения. Перечислим их:



блок перехвата исключения соответствует возбуждённому исключению, если в их объявлении и генерации использован один и тот же тип; если возбуждаемое исключение может быть преобразовано к типу исключения, объявленного в блоке перехвата путём неявного преобразования типа, исключение считается соответствующим данному блоку перехвата; если возбуждаемое исключение преобразуется к типу исключения, объявленного в блоке перехвата путём явного преобразования типа, оно считается соответствующим данному блоку перехвата; исключение, которое является объектом-представителем производного класса, соответствует блоку перехвата, в котором объявлено исключение-представитель базового класса. Таким образом, исключение производного класса может быть перехвачено в блоке перехвата, в котором объявлено исключение-представитель базового класса. Это обстоятельство следует учитывать при расположении в программе блоков, определяющих списки реакций. В списке реакций контролируемого блока операторов перехватчики исключений, порождённых базовыми классами, должны располагаться в списке исключений ниже перехватчиков исключений, представляющих производные классы; блок перехвата, содержащий вместо объявления исключения многоточие catch (...) {/*...*/}, соответствует любому исключению. Это своего рода универсальный блок перехвата. Он должен завершать список перехватчиков, поскольку ни один блок перехвата после него не сможет быть выполнен для обработки данного исключения, поскольку все возможные исключения будут перехвачены этим блоком.

Как известно, конструкторы и деструкторы не возвращают значений. Но в них могут быть размещены операторы генерации исключений. Если теперь программный код, обеспечивающий вызов конструкторов или деструкторов разместить в try-операторе, то можно будет организовать перехват исключения от конструкторов и деструкторов. Возбуждение исключения в конструкторе должно сопровождаться, если это необходимо, автоматическим вызовом деструкторов для уничтожения образующих этот объект составных элементов (если таковые существуют). Если исключительная ситуация возникла в ходе создания массива объектов, вызываемый в результате генерации исключения деструктор уничтожит лишь созданные на момент возникновения исключительной ситуации объекты.

Если соответствующий блок перехвата был обнаружен и содержит именованный параметр, временный объект, созданный throw операцией, его инициализирует. Здесь всё происходит примерно также, как и при вызове функции. Для инициализации параметра исключения, являющегося представителем какого-либо класса, может потребоваться собственная версия конструктора копирования и деструктора. Проинициализированный именованный параметр получает доступ к информации, заложенной в исключение в момент его генерации. И здесь уместна аналогия с вызовом функции. Существует проинициализированный и поименованный параметр - будет и доступ к передаваемой информации. В ряде случаев, как и при вызове функции, без конкретного значения параметра можно и обойтись - лишь бы вовремя активизировался соответствующий обработчик и принял бы соответствующие меры по ликвидации последствий исключительной ситуации. А меры в этой связи могут быть приняты самые разнообразные. Здесь всё определяется конкретной задачей.

Стартовав из try-блока, в результате возникновения исключительной ситуации, при благоприятном стечении обстоятельств, мы оказались в одном из связанных с ним блоков перехвата исключения. По сигналу тревоги, благодаря системе программирования C++, в нужное время мы прибыли в нужное место. Теперь всё зависит от программиста. Наши действия в catch-блоке практически ничем не ограничены. Выведем ли мы предупредительное сообщение на экран, исправим ли значение индекса массива, запросим ли новое значение для делителя - это транслятор не волнует. Формально мы совершили действие, в результате которого исключительная ситуация перехвачена, а её причина, возможно, что и ликвидирована. Что бы мы ни сделали catch-блоке (в конце концов, исправляя ошибку, мы можем сделать новую ошибку), будет воспринято без возражений.

Находясь в catch-блоке, мы можем вообще отказаться от каких-либо неотложных мероприятий. С помощью оператора throw; можно повторно возбудить последнее исключение. Этот оператор обязательно должен быть расположен в catch-блоке. В результате повторно запускается всё тот же механизм поиска нового подходящего catch-блока. Стек при этом продолжает разматываться, и если при этом в ходе выполнения программы имела место ситуация "вложенных" контролируемых блоков (из try-блока одной функции прямо или косвенно была вызвана функция, содержащая собственный контролируемый блок), то повторно возбуждённое исключение может быть перехвачено уровнем ниже. Таким образом, можно поручить перехват исключения функции, которая была вызвана ранее и, возможно, не несёт ответственности за возникшую исключительную ситуацию. Если соответствующего перехватчика исключения не окажется, выполнение программы будет остановлено.

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

Может так случиться, что исключение окажется неперехваченным. Не во всех же программах прописывается универсальный блок перехвата… Безуспешный просмотр всех записей стека в поисках соответствующего перехватчика является признаком неперехваченного исключения. Оно оказывается за пределами контролируемого блока операторов, таким же независимым и свободным, как исключение, возбуждённое в "автономном" режиме. И последней преградой на пути неперехваченного исключения встаёт функция unexpected.

Эту функцию невозможно переопределить, а из-за жёстких ограничений на её список параметров (он непременно должен быть пустым), нельзя определить соответствующие совместно используемые функции. Функция unexpected - "вещь в себе", заглушка. Известно лишь, что она вызывает функцию terminate, но может вызвать и ещё какую-либо другую функцию. Изменить ситуацию на этом "последнем рубеже" можно лишь одним единственным способом - определив собственную функцию, которая должна заместить функцию unexpected в результате выполнения уже известной функции set_unexpected. Здесь ещё существует возможность исправить положение. Дальше такой возможности уже не будет.

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

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



#include iostream.h #include string.h #define YESMESS "Мы продолжаем." #define NOMESS "Мы завершаем." class MyDivideByZeroError { char *MyErrorMessage; public: char ContinueKey; MyDivideByZeroError(): MyErrorMessage(NULL) { char YesKey; cout "Зафиксировано деление на нуль." endl; cout "Принимать экстренные меры? (Y/N) "; cin YesKey; if ( YesKey == 'Y' YesKey == 'y' ) { ContinueKey = 1; MyErrorMessage = strdup(YESMESS); } else { ContinueKey = 0; MyErrorMessage = strdup(NOMESS); } } MyDivideByZeroError(const MyDivideByZeroError CopyVal) { ContinueKey = CopyVal.ContinueKey; MyErrorMessage = strdup(CopyVal.MyErrorMessage); } ~MyDivideByZeroError() { if (MyErrorMessage) delete(MyErrorMessage); } void PrintMessage() { cout MyErrorMessage endl; } }; float Dividor(float, float) throw(MyDivideByZeroError); void main() { float MyVal1, MyVal2; for (;;) { // __ Начало контролируемого блока __________________________________. try { cout "========================================" endl; cout "MyVal1 "; cin MyVal1; cout "MyVal2 "; cin MyVal2; cout "Считаем... " Dividor(MyVal1, MyVal2) endl; cout "Получилось! "; } catch (MyDivideByZeroError MyExcept) { MyExcept.PrintMessage(); if (MyExcept.ContinueKey == 0) { cout "Надоело воевать с ошибками! Уходим." endl; break; } } //__ За пределами контролируемого блока ____________________________. cout "Уже за пределами блока. Мы продолжаем..." endl; } } float Dividor(float Val1, float Val2) throw(MyDivideByZeroError) { if (Val2 == 0.0) throw MyDivideByZeroError(); return Val1/Val2; }

И, наконец, пример замещения функций unexpected и terminate. Последняя программа в этой книге.

#include iostream.h #include except.h #define MAXERR 5 class MaxError; class MyError { public: MyError() { CounterError++; if (CounterError MAXERR) { cout " Здесь MyError()... throw MaxError()!" endl; throw MaxError(); } else { cout " Здесь MyError()... CounterError++!" endl; } } void ErrSay() { cout " Здесь ErrSay(): " CounterError endl; } static int CounterError; }; int MyError::CounterError = 0; class MaxError { public: MaxError() { if (CounterMaxError == 0) { /* MaxError один раз может подправить значение счётчика MyError::CounterError. */ CounterMaxError++; MyError::CounterError -= 2; cout "Здесь MaxError().. MyError::CounterError-= 2;" endl; } else { cout " Здесь MaxError()... ###" endl; } } static int CounterMaxError; }; int MaxError::CounterMaxError = 0; void RunnerProcessor(); void Run() throw(MyError); void MyUnex(); void MyTerm(); void main() { unexpected_function OldUnex; terminate_function OldTerm; OldUnex = set_unexpected(MyUnex); OldTerm = set_terminate(MyTerm); /* Мы замещаем функции unexpected() и terminate(). Адресные переменные нужны для того, чтобы запомнить адреса старых функций. В случае необходимости, их можно восстановить: set_unexpected(OldUnex); set_terminate(OldTerm); */ RunnerProcessor(); } void RunnerProcessor() { for (;;) { try { Run(); } catch (MyError err) { err.ErrSay(); } } } void Run() throw(MyError) { cout "Работает Run()..." endl; throw MyError(); } void MyUnex() { /* Мы всё ещё находимся в пределах try-блока. */ cout "Это MyUnex()..." endl; throw MyError(); } void MyTerm() { int MyTermKey = 0; /* Вышли из try-блока. Включилась система автоматического торможения. */ for ( ; MyTermKey 5; ) { cout "Это MyTerm()........................" MyTermKey endl; MyError::CounterError = 0; MaxError::CounterMaxError = 0; RunnerProcessor(); MyTermKey += 1; /* Цикл здесь уже не циклится! */ } MaxError::CounterMaxError = 0; throw MyError(); /* Исключения не работают! */ }

Всё. Приехали. Можно расслабиться. Можно постоять на берегу океана. Послушать шум ветра в соснах. Посмотреть на касаток в холодной прозрачной воде. Только недолго. Впереди ждут великие дела.


Рекомендации по наименованию объектов


Имена - это идентификаторы. Любая случайным образом составленная последовательность букв, цифр и знаков подчёркивания с точки зрения грамматики языка идеально подходит на роль имени любого объекта, если только она начинающаяся с буквы. Фрагмент программы, содержащий подобную переменную, будет синтаксически безупречен.

И всё же имеет смысл воспользоваться дополнительной возможностью облегчить восприятие и понимание последовательностей операторов. Для этого достаточно закодировать с помощью имён содержательную информацию.

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



Семантика


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



Шаблоны функций и шаблонные функции


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

int min (int iVal_1, int iVal_2) { return iVal_1 iVal_2 ? iVal_1 : iVal_2; /* Возвращается значение iVal_1, если это значение меньше iVal_2. В противном случае возвращается значение iVal_2. */ }

Для каждого типа сравниваемых величин должен быть определён собственный вариант функции min(). Вот как эта функция выглядит для float:

float min (float fVal_1, float fVal_2) { return fVal_1 fVal_2 ? fVal_1 : fVal_2; }

А для double… А для…

И так для всех используемых в программе типов сравниваемых величин. Мы можем бесконечно упражняться в создании совместно используемых функций, хотя можно воспользоваться средствами препроцессирования: #define min(a,b) ((a)(b)?(a):(b))

Это определение правильно работает в простых случаях:

min(10, 20); min(10.0, 20.0);

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

C++ предоставляет ещё одно средство для решения этой задачи. При этом сохраняется присущая макроопределениям краткость и строгость контроля типов языка. Этим средством является шаблон функции.

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

Синтаксис объявления шаблона определяется следующим множеством предложений Бэкуса-Наура:

Объявление ::= ОбъявлениеШаблона

ОбъявлениеШаблона ::= template СписокПараметровШаблона Объявление

СписокПараметровШаблона ::= ПараметрШаблона

::= СписокПараметровШаблона, ПараметрШаблона


ПараметрШаблона ::= ТиповыйПараметр

::= *****

ТиповыйПараметр ::= class Идентификатор

Итак, объявление и определение шаблона функции начинается ключевым словом template, за которым следует заключённый в угловые скобки и разделённый запятыми непустой список параметров шаблона. Эта часть объявления или определения обычно называется заголовком шаблона.

Каждый параметр шаблона состоит из служебного слова class, за которым следует идентификатор. В контексте объявления шаблона функции служебное слово class не несёт никакой особой смысловой нагрузки. Дело в том, что аналогичная конструкция используется также и для объявления шаблона класса, где, как скоро увидим, ключевое слово class играет свою особую роль. В заголовке шаблона имена параметров шаблона должны быть уникальны.

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

Шаблон функции служит инструкцией для транслятора. По этой инструкции транслятор может самостоятельно построить определение новой функции.

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

В качестве примера рассмотрим программу, в которой для определения минимального значения используется шаблон функции min().



template class Type Type min (Type a, Type b); /* Прототип шаблона функции. Ключевое слово template обозначает начало списка параметров шаблона. Этот список содержит единственный идентификатор Type. Сама функция содержит два объявления шаблонных параметра, специфицированных шаблоном параметра Type. Спецификация возвращаемого значения также представлена шаблоном параметра Type. */ int main (void) { min(10,20);// int min (int, int); min(10.0,20.0);// float min (float, float); /* Вызовы шаблонной функции. Тип значений параметров определён. На основе выражения вызова (транслятор должен распознать тип параметров) и определения шаблона транслятор самостоятельно строит различные определения шаблонных функций. И только после этого обеспечивает передачу управления новорождённой шаблонной функции. */ return 1; } template class Type Type min (Type a, Type b) { return a b ? a : b; } /* По аналогии с определением функции, эту конструкцию будем называть определением шаблона функции. */

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

В результате конкретизации шаблона функции min() транслятор строится следующий вариант программы с двумя шаблонными функциями. По выражению вызова на основе шаблона строится шаблонная функция. Почувствуйте прелесть употребления однокоренных слов! Шаблон функции и шаблонная функция - два разных понятия.

int min (int a, int b); float min (float a, float b); int main (void) { min(10,20); min(10.0,20.0); return 1; } int min (int a, int b) { return a b ? a : b; } float min (float a, float b) { return a b ? a : b; }

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

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



template class Type Type min (Type *a, Type *b) { return a b ? a : b; } и при этом вызов функции имеет вид: int a = 10, b = 20; int *pa = a, *pb = b; min(pa,pb);

то в процессе конкретизации идентификатор типа Type будет замещён именем производного типа int:

int min (int *a, int *b) { return a b ? a : b; }

В процессе конкретизации недопустимы расширения типов и другие преобразования типов параметров:

template class Type Type min (Type a, Type b) { return a b ? a : b; } unsigned int a = 10; ::::: min(1024,a); /* Здесь транслятор сообщит об ошибке. В вызове функции тип второго фактического параметра модифицирован по сравнению с типом первого параметра - int и unsigned int. Это недопустимо. В процессе построения новой функции транслятор не распознаёт модификации типов. В вызове функции типы параметров должны совпадать. Исправить ошибку можно с помощью явного приведения первого параметра. */ min((unsigned int)1024,a); :::::

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

И опять пример с излюбленным классом ComplexType. На множестве комплексных чисел определены лишь два отношения: равенства (предполагает одновременное равенство действительной и мнимой частей) и неравенства (предполагает все остальные случаи). В нашей новой программе мы объявим и определим шаблон функции neq(), которая будет проверять на неравенство значения различных типов.

Для того, чтобы построить шаблонную функцию neq() для комплексных чисел, нам придётся дополнительно определить операторную функцию-имитатор операции != для объектов-представителей множества комплексных чисел. Это важно, поскольку операция != явным образом задействована в шаблоне neq(). Транслятор не поймёт, как трактовать символ != , а, значит, и как строить шаблонную функцию neq(ComplexType, ComplexType), если эта операторная функция не будет определена для класса ComplexType.



#include iostream. h template class Type int neq (Type, Type); /*Прототип шаблона функции.*/ class ComplexType { public: double real; double imag; // Конструктор умолчания. ComplexType(double re = 0.0, double im = 0.0) {real = re; imag = im;} /* Операторная функция != . Без неё невозможно построение шаблонной функции neq() для комплексных чисел. */ int operator != (ComplexType KeyVal) { if (real == KeyVal.real imag == KeyVal.imag) return 0; else return 1; } }; void main () { // Определены и проинициализированы переменные трёх типов. int i = 1, j = 2; float k = 1.0, l = 2.0; ComplexType CTw1(1.0,1.0), CTw2(2.0,2.0); //На основе выражений вызова транслятор строит три шаблонных функции. cout "neq() for int:" neq(i,j) endl; cout "neq() for float:" neq(k,l) endl; cout "neq() for ComplexType:" neq(CTw2,CTw3) endl; } /*Определение шаблона функции.*/ template class Type int neq (Type a, Type b) { return a != b ? 1 : 0; // return a != b; /* На самом деле, можно и так… */ }

И ещё один пример. Этот пример подтверждает обязательность включения всех параметров шаблона в список параметров шаблона определяемой функции. Независимо от того, какая роль предназначается шаблонному параметру (он вообще может не использоваться в шаблонной функции), его присутствие в списке параметров обязательно. В процессе построения шаблонной функции транслятор модифицирует весь шаблон полностью - его заголовок и его тело. Так что в теле шаблона можно объявлять переменные, специфицированные параметрами шаблона.

#include iostream.h #include typeinfo.h /* В программе используется объект класса Type_info, позволяющий получать информацию о типе. Здесь подключается заголовочный файл, содержащий объявление этого класса */ template class YYY, class ZZZ YYY Tf (ZZZ, YYY, int); /* Шаблон прототипа функции. Функция Tf возвращает значение пока ещё неопределённого типа, обозначенного параметром шаблона YYY. Список её параметров представлен двумя (всеми!) параметрами шаблона и одним параметром типа int. */ void main() { cout Tf((int) 0, '1', 10) endl; /* Собственно эти вызовы и управляют работой транслятора. Тип передаваемых значений параметров предопределяет структуру шаблонной функции. В первом случае шаблону параметра ZZZ присваивается значение "int", шаблону параметра YYY присваивается значение "char", после чего прототип шаблонной функции принимает вид char Tf (int, char, int); */ cout Tf((float) 0, "This is the string...", 10) endl; /* Во втором случае шаблону параметра ZZZ присваивается значение "float", шаблону параметра YYY присваивается значение "char *", после чего прототип шаблонной функции принимает вид char* Tf (float, char *, int); В результате, используя один общий шаблон, мы получаем две совершенно различных совместно используемых функции. */ } /* Шаблон функции. Первый параметр не используется, поэтому в списке параметров он представлен спецификатором объявления. Второй шаблонный параметр определён и также зависит от шаблона, третий параметр от шаблона не зависит. */ template class YYY, class ZZZ YYY Tf (ZZZ, YYY yyyVal, int x) { ZZZ zzzVal; int i; for (i = 0; i x; i++) { cout "Tf() for " typeid(zzzVal).name() endl; } return yyyVal; }


Символы операций и разделителей


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

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

, ! != | |= % %=
= () * *= + ++ +=
- -- -= - -* . .* /
/= :: = =
= = == ?: [] ^ ^= ~
# ## sizeof new delete typeid throw

Кроме того, к числу разделителей относятся следующие последовательности специальных символов:

...;{}



Совместно используемые функции


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

При объявлении различных функций в 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, или использованием ссылки (эти спецификаторы не используются при модификации имён).

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

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

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

Точное сопоставление.

Точное сопоставление предполагает однозначное соответствие количества, типа и порядка значений параметров выражения вызова и параметров в определении функции. // Произвольная функция, которая возвращает целое значение. int iFunction(float, char *); //Объявление пары совместно используемых функций... extern void FF(char *); //Вариант 1... extern void FF(int); //Вариант 2... //Вызов функции. FF(0); FF(iFunction(3.14, "QWERTY"));



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

Сопоставление с помощью расширения типа.

При таком сопоставлении производится приведение типа значения параметра в выражении вызова к типу параметра в определении функции. Для этого используется расширение типа.

Если ни для одного из вызовов точного сопоставления не произошло, то применяются следующие расширения типа:

Параметр типа 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 и значение, допускающее целочисленное расширение. Вызов сопоставляется со вторым вариантом совместно используемой функции.

Сопоставление со стандартным преобразованием. Применяется в случае неудачи сопоставления по двум предыдущим критериям сопоставления. Фактический параметр преобразуется в соответствии с правилами стандартных преобразований. Стандартное преобразование типа реализует следующие варианты сопоставления значений параметров в выражениях вызова и параметров объявления:

любой целочисленный тип параметра выражения вызова сопоставляется с любым целочисленным типом параметра, включая 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); /* При явном указании имени функции в операторе вызова транслятор однозначно идентифицирует функцию. */


Список литературы


М. Эллис, Б. Строуструп. Справочное руководство по языку C++ с комментариями: Пер. с англ. - Москва: Мир, 1992. 445с. Стенли Б. Липпман. C++ для начинающих: Пер. с англ. 2тт. - Москва: Унитех; Рязань: Гэлион, 1992, 304-345сс. Бруно Бабэ. Просто и ясно о Borland C++: Пер. с англ. - Москва: БИНОМ, 1994. 400с. В.В. Подбельский. Язык C++: Учебное пособие. - Москва: Финансы и статистика, 1995. 560с. Ирэ Пол. Объектно-ориентированное программирование с использованием C++: Пер. с англ. - Киев: НИИПФ ДиаСофт Лтд, 1995. 480с. Т. Фейсон. Объектно-ориентированное программирование на Borland C++ 4.5: Пер. с англ. - Киев: Диалектика, 1996. 544с. Т. Сван. Освоение Borland C++ 4.5: Пер. с англ. - Киев: Диалектика, 1996. 544с. Г. Шилдт. Самоучитель C++: Пер. с англ. - Санкт-Петербург: BHV-Санкт-Петербург, 1998. 620с. У. Сэвитч. C++ в примерах: Пер. с англ. - Москва: ЭКОМ, 1997. 736с. К. Джамса. Учимся программировать на языке C++: Пер. с англ. - Москва: Мир, 1997. 320с. В.А. Скляров. Язык C++ и объектно-ориентированное программирование: Справочное издание. - Минск: Вышэйшая школа, 1997. 480с. Х. Дейтел, П. Дейтел. Как программировать на C++: Пер. с англ. - Москва: ЗАО "Издательство БИНОМ", 1998. 1024с.

|



Структура предложения C++


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



Тип функции


Основными характеристиками функции является тип возвращаемого значения и список типов формальных параметров. Подобно тому, как имена переменных никаким образом не влияют на их тип, имена функций не является частью их типа. Тип функции определяется типом возвращаемого значения и списком типов её формальных параметров. Например, пара функций 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);

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


Тип связывания или тип компоновки


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

Статическое связывание бывает внешним или внутренним. Оно обеспечивается на стадии формирования исполнительного модуля, ещё до этапа выполнения программы.

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

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

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



Типы


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

Поскольку конкретное значение может быть зафиксировано в области памяти, которая соответствует объекту определённого типа, можно также говорить о типе значения. Значения представляются выражениями. Поэтому имеет смысл также говорить и о типе выражения. Таким образом, тип оказывается важнейшей характеристикой языка.

Можно предположить существование языка с единственным типом объекта. Такой язык можно считать нетипизированным языком. Для нетипизированного языка характерен фиксированный размер объекта, единый формат хранения данных, унифицированные способы интерпретации значений.

Как ни странно, нетипизированный язык одинаково неудобен для решения задач в любой конкретной предметной области. К обработке символьной информации или решению сложных вычислительных задач транслятор нетипизированного языка относится одинаково. Для него все объекты одинаковые. Так что реализация алгоритмов сравнения символьных строк, вычисление значений тригонометрических функций, корректное прочтение и запись значений переменных и констант, способы интерпретации информации, применение разнообразных операций к данным (при анализе символьной информации бессмысленны операции умножения и деления) и многие другие проблемы оказываются исключительно проблемами программиста. Больше проблем - больше ошибок.

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



Транслятор и компоновщик


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

Процесс создания программы предполагает несколько этапов. За этапом разработки проекта программы следует этап программирования. На этом этапе пишется программа. Программистами этот текст воспринимается легче двоичного кода, поскольку различные мнемонические сокращения и имена заключают дополнительную информацию.

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

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

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

После синтаксического анализа наступает этап поэтапной генерации кода. На этом этапе происходит замена операторов языка высокого уровня инструкциями ассемблера, а затем последовательностями машинных команд. Результат преобразования исходного текста программы записывается в виде двоичного файла (его называют объектным модулем) с расширением ".obj".

Системы программирования, реализующие язык программирования C++, предусматривают стандартные приёмы и средства, которые делают процесс программирования более технологичным, а саму программу более лёгкой для восприятия.

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

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

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

Объектный модуль можно выполнять лишь после специальной дополнительной обработки (компоновки), которая осуществляется специальной программой-компоновщиком.

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

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

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

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



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 подчиняется следующим правилам (ничто не даётся даром):

Спецификатор typedef может переопределять имя как имя типа, даже если это имя само уже было ранее введено typedef спецификатором: typedef int I; typedef I I;

Спецификатор typedef не может переопределять имя типа, объявленное в одной и той же области действия, и замещающее имя другого типа. typedef int I; typedef float I; // Ошибка: повторное описание…

На имена, введённые в программу с помощью спецификатора typedef, распространяются правила области действия, за исключением разрешения на многократное использование имени (правило 1.).


Указатель this


Продолжаем определение класса ComplexType. Теперь объявим и определим функцию-член PrintVal, которая будет выводить значение чисел-объектов.

Прототип функции разместим в классе: void PrintVal();

При определении функции используется квалифицированное имя:

void ComplexType::PrintVal() { cout "(" real ", " imag "i)" endl; cout (int)CTcharVal ", " x "…" endl; }

Значения данных-членов объекта выводятся при выполнении выражения вызова функции PrintVal: CDw1.PrintVal();

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

Среди операторов функции-члена PrintVal() нет ни одного оператора, который позволял бы определить, какому объекту принадлежат данные-члены. И, тем не менее, вызов этой функции для каждого из определённых и различным образом проинициализированных объектов, в том числе и для безымянного объекта, который создаётся в результате непосредственного вызова конструктора: ComplexType(0.0,0.0, 1).PrintVal(); ,

а также вызов функции для объекта, адресуемого указателем: pCD-PrintVal();

сопровождается сообщением о значениях собственных данных-членов. Заметим, что "собственные" данные-члены объектов, как и те функции-члены класса, с которыми мы уже успели познакомиться, считаются нестатическими данными и функциями-членами класса. Существуют также и статические члены класса, к изучению свойств которых мы обратимся в недалёком будущем.

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

Вряд ли алгоритм распознавания хозяина данных очень сложен. Здесь проблема заключается совсем в другом: этот алгоритм должен быть реализован практически для каждой нестатической функции-члена класса. Он используется везде, где производится обращение к данным-членам объектов, а это означает, что на программиста может быть возложена дополнительная обязанность по кодированию. Несколько обязательных строк для каждой функции-члена? Да никогда…

К счастью, C++ освобождает программистов от утомительной и однообразной работы кодирования стандартного алгоритма распознавания. В C++ вообще многое делается без их участия. Функции-члены определяются как обычные функции. Транслятор переопределяет эти функции, обеспечивая при этом стандартными средствами связь между объектами и их данными. Эта связь реализуется благодаря специальному преобразованию исходного кода программы. Мы опишем это преобразование, условно разделив его на два этапа.

На первом этапе каждая нестатическая функция-член преобразуется в функцию с уникальным именем и дополнительным параметром - константным указателем на объект класса. Затем преобразуются обращения к нестатическим данным-членам в операторах функции-члена. Они переопределяются с учётом нового параметра. В C++ при подобном преобразовании для обозначения дополнительного параметра-указателя (константного указателя) и постфиксного выражения с операциями обращения для обращения к нестатическим данным-членам используется одно и то же имя this. Вот как могла бы выглядеть функция-член PrintVal после её переопределения:


void ComplexType::ComplexType_PrintVal(ComplexType const *this) { cout "(" this-real "," this-imag "i)" endl; cout int(this-CTcharVal) "," x "…" endl; }

На втором этапе преобразуются вызовы функций-членов. К списку значений параметров выражения вызова добавляется выражение, значением которого является адрес данного объекта. Это вполне корректное преобразование. Дело в том, что нестатические функции-члены всегда вызываются для конкретного объекта. И потому не составляет особого труда определить адрес объекта. Например, вызов функции-члена PrintVal() для объекта CDw1, который имеет вид CDw1.PrintVal();

после преобразования принимает вид: ComplexType_PrintVal(CDw1);

А вызов функции-члена безымянного объекта, адресуемого указателем pCD pCD-PrintVal();

преобразуется к виду ComplexType_PrintVal((*pCD));

что эквивалентно следующему оператору: ComplexType_PrintVal(pCD);

Первый (и в нашем случае единственный) параметр в вызове новой функции является адресом конкретного объекта.

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

Причина изменения имени для функций-членов класса очевидна. В разных классах могут быть объявлены одноименные функции-члены. В этих условиях обращение к функции-члену класса непосредственно по имени может вызвать конфликт имён: в одной области действия имени одним и тем же именем будут обозначаться различные объекты - одноименные функции-члены разных классов. Стандартное преобразование имён позволяет решить эту проблему.

Указатель this можно использовать в теле функции-члена без его дополнительного объявления. В частности, операторы функции ComplexType::PrintVal() могут быть переписаны с использованием указателя this:



void ComplexType::PrintVal() { cout "(" this-real "," this-imag "i)" endl; cout int(this-CTcharVal) "," x "…" endl; }

Явное употребление this указателя не вызывает у транслятора никаких возражений, что свидетельствует об эквивалентности старого и нового вариантов функции. В этом случае указатель this считается не именем (имя вводится объявлением), а первичным выражением. Напомним, что имя, как и первичное выражение this являются частными случаями выражения.

В ряде случаев при написании программы оправдано явное использование указателя this. При этом выражение this

представляет адрес объекта, а выражение *this

представляет сам объект:

this-ВЫРАЖЕНИЕ

(*this).ВЫРАЖЕНИЕ

(здесь нетерминальный символ ВЫРАЖЕНИЕ обозначает член класса). Эти выражения обеспечивают доступ к членам уникального объекта, представленного указателем this с целью изменения значения данного, входящего в этот объект или вызова функции-члена.

Следует помнить о том, что this указатель является константным указателем. Это означает, что непосредственное изменение его значение (перенастройка указателя, например, this++) недопустимо. Указатель this с самого начала настраивается на определённый объект.

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

В объявлении нестатической функции-члена this указателю можно задавать дополнительные свойства. В частности, возможно объявление константного this указателя на константу. Синтаксис языка C++ позволяет сделать это. Среди БНФ, посвящённых синтаксису описателей, есть и такая форма:

Описатель ::= Описатель (СписокОбъявленийПараметров) [СписокCVОписателей] ::= *****



CVОписатель ::= const ::= *****

Так что небольшая модификация функции-члена PrintVal, связанная с добавлением cvОписателя const: void PrintVal() const;

в прототипе и

void ComplexType::PrintVal() const { ::::: }

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

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

this-CTcharVal = 125; real = imag*25; imag++;

cvОписатель const в заголовке функции не допускает непосредственной модификации значений принадлежащих объекту данных.

Заметим также, что this указатель включается также в виде дополнительного параметра в список параметров конструктора. И в этом нет ничего удивительного, поскольку его значением является всего лишь область памяти, занимаемая объектом.


Указатель 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; // Здесь будет выдано предупреждение об // опасном преобразовании.

Это обстоятельство имеет определённые последствия, которые связаны с тем, что все преобразования над значениями указателей будут производиться без учёта особенностей структуры тех объектов, на которые указатель в самом начале был настроен.

При этом ответственность за результаты подобных преобразований возлагается на программиста.


Указатели на компоненты класса. Доступ по указателю


Прежде всего, рассмотрим объявление класса XXX.

class XXX { public: long x1; int x2; /*Данные-члены класса.*/ long getVal1() {return x1;} long getVal2() {return x2*x1;} /*Функции-члены класса без параметров.*/ int getVal3(int param) {return x2*param;} char* getVal4(char *str) {return str;} /*Функции-члены класса с параметрами.*/ static int f1() {return 100;} static int f2() {return 10;} static int f3(int param) {return param;} /* Определение различных статических функций*/ XXX(long val1, int val2){x1 = val1; x2 = val2;} /*Конструктор.*/ };

Поскольку нестатические функции-члены формально, а нестатические данные-члены фактически не существуют без объекта-представителя класса, определение указателя на компонент класса (член класса или функцию-член) отличается от определения указателя на объект или обычную функцию.

Для объявления указателя на нестатическую функцию используется специальная синтаксическая конструкция, состоящая из спецификатора объявления и заключённого в скобки квалифицированного имени указателя, состоящего из имени класса, операции доступа к члену класса ::, разделителя * , собственно имени указателя, закрывающей скобки и списка параметров: int (XXX::*fp_3) (int);

Подобный указатель может быть проинициализирован инициализатором, состоящим из операции присвоения, операции взятия адреса и квалифицированного имени соответствующей функции-члена: int (XXX::*fp_3) (int) = XXX::getVal1;

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

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

Класс - это не объект! И не совсем понятно, какое значение имеет адрес нестатичесного члена класса. Значение проинициализированного указателя на нестатическую компоненту остаётся неопределённым.

Оно определяется лишь в результате выполнения операций обращения к членам класса .* и -* .

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


int val = (q.*fp)(6); char val = (pq-*fp4)("new string");

Аналогичным образом осуществляется объявление и инициализация указателей на данные- члены класса. При этом структура объявления указателя на член класса проще (нет спецификации возвращаемого значения, не нужно указывать список параметров). Это не функция, здесь дело обходится спецификацией объявления и квалифицированными именами указателей:

long (XXX::*px1) = XXX::x1; // Определение и инициализация указателя на член класса XXX типа long q.*px11 = 10; // p - объект-представитель класса XXX. pq-*px11 = 10; // pq - указатель на объект-представитель класса XXX.

Основные приёмы работы с указателями на функции-члены демонстрируются на следующих примерах:

class XXX { public: long x1; int x2; /*Данные-члены класса.*/ long getVal1() {return x1;} long getVal2() {return x2*x1;} /*Функции-члены класса без параметров.*/ int getVal3(int param) {return x2*param;} char* getVal4(char *str) {return str;} /*Функции-члены класса с параметрами.*/ static int f1() {return 100;} static int f2() {return 10;} static int f3(int param) {return param;} /* Определение различных статических функций*/ XXX(long val1, int val2){x1 = val1; x2 = val2;} /*Конструктор.*/ }; void main() { XXX q(1,2);/* Определение объекта.*/ XXX* pq = new (XXX); pq-x1 = 100; pq-x2 = 100; /*Определение и инициализация объекта по указателю.*/ long (XXX::*fp_0) (); /*Указатель на функцию-член класса.*/ long (XXX::*fp_1) () = XXX::getVal1; /* Проинициализированный указатель на функцию-член класса. Его значение является относительной величиной и равняется значению смещения функции-члена относительно первого члена класса. */ fp_0 = XXX::getVal1; /* Инициализация первого указателя. Один и тот же указатель можно настраивать на различные функции-члены класса. Главное, чтобы у всех этих функций-членов совпадали списки параметров и возвращаемые значения функций. */ long val_1 = (q.*fp1)(); /*Вызов функции-члена класса по указателю из объекта.*/



long val_2 = (pq-*fp0)(); /* Вызов функции-члена класса по указателю с помощью указателя на объект. */ int (XXX::*fp_3) (int) = XXX::getVal3; /* Проинициализированный указатель на функцию-член класса. С параметрами типа int. */ int val_3 = (q.*fp_3)(6); /* Вызов функции-члена класса по указателю из объекта с передачей параметров. */ char* (XXX::*fp_4) (char) = XXX::getVal3; /* Проинициализированный указатель на функцию-член класса с параметрами типа int. */ char val_4 = (pq-*fp4)("new string"); /* Вызов функции-члена класса по указателю с помощью указателя на объект. */ int (*fp_5) () = XXX::f1; /* Указатель на статическую функцию объявляется без спецификации класса. Явная спецификация класса необходима лишь при инициализации указателя. */ int retval = (*fp_5)(); /*Вызов статической функции по указателю.*/ fp_5 = XXX::f2; /* Перенастройка статического указателя. Главное требование - совпадение списков параметров и типа возвращаемого значения. */ int (*fp_6) (int) = XXX::f3; /*Указатель на статическую функцию с параметрами.*/ int retval = (*fp_6)(255); /*Вызов статической функции с параметрами по указателю.*/ long (XXX::*px1) = XXX::x1; /*Определили и проинициализировали указатель на член класса long*/ q.*px11 = 10; /*Используя указатель на компоненту класса, изменили значение переменной x1 объекта q, представляющего класс XXX. */ pq-*px11 = 10; /*Используя указатель на компоненту класса, изменили значение переменной x1 объекта, представляющего класс XXX и расположенного по адресу pq. */ }

Вызов статических функций-членов класса не требует никаких объектов и указателей на объекты. От обычных функций их отличает лишь специфическая область видимости.


Указатели на объекты


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

#include iostream.h class A { }; class AB: public A { }; class AC: public A { }; void main () { A *pObj; A MyA; pObj = MyA; cout "OK A" endl; AB MyAB; AC MyAC; pObj = MyAB; cout "OK AB" endl; pObj = MyAC; cout "OK AC" endl; }

Это очень простой пример. Пустые классы, простое наследование… Единственно, что важно в объявлении этих классов - спецификаторы доступа в описании баз производных классов. Базовый класс (его будущие члены) должен быть абсолютно доступен в производном классе. Первый оператор функции main() - объявление указателя на объект класса A. Затем следует определение объекта-представителя класса A, следом - настройка указателя на этот объект. Естественно, при этом используется операция взятия адреса. Всё это давно известно и очень просто. Следующие две строки являются определениями пары объектов, которые являются представителями двух разных производных классов…

За объявлениями объектов в программе располагаются строки, которые позволяют настроить указатель на базовый класс на объект производного класса. Для настройки указателя на объект производного класса нам не потребовалось никаких дополнительных преобразований. Здесь важно только одно обстоятельство. Между классами должно существовать отношение наследования. Таким образом, проявляется важное свойство объектно-ориентированного программирования: УКАЗАТЕЛЬ НА БАЗОВЫЙ КЛАСС МОЖЕТ ССЫЛАТЬСЯ НА ОБЪЕКТЫ - ПРОИЗВОДНЫХ КЛАССОВ. Подобное, на первый взгляд, странное обстоятельство имеет своё объяснение.

Рассмотрим схемы объектов MyA, MyAB, MyAC:

MyA::= A

MyAB::= A AB

MyAC::= A AC

Все три объекта имеют общий элемент (объекты производных классов - фрагмент) - представитель базового класса A. Исключительно благодаря этому общему элементу указатель на объект класса A можно настроить на объекты производных классов. Указателю просто присваивается адрес базового фрагмента объекта производного типа. В этом и состоит секрет подобной настройки. Как мы увидим, для указателя pObj, настроенного на объект производного класса, вообще не существует фрагмента объекта, представленного производным классом.


pObj

A AC

Ниже пунктирной линии - пустота. Для того чтобы убедиться в этом, мы усложним структуру класса A, определив в нём функцию Fun1. Конечно же, эта функция ничего не будет делать. Но у неё будет спецификация возвращаемого значения и непустой список параметров. Нам от неё большего и не требуется. Лишь бы сообщала о собственном вызове…

class A { public: int Fun1(int); }; int A::Fun1(int key) { cout " Fun1( " key " ) from A " endl; return 0; }

Аналогичной модификации подвергнем производные классы AB и AC (здесь предполагаются вызовы функций-членов непосредственно из функции main(), а потому надо помнить о спецификаторе public), а затем продолжим опыты.

class AB: public A { public: int Fun1(int key); }; int AB::Fun1(int key) { cout " Fun1( " key " ) from AB " endl; return 0; } class AC: public A { public: int Fun1(int key); int Fun2(int key);// В этом классе мы объявим вторую функцию. }; int AC::Fun1(int key) { cout " Fun1( " key " ) from AC " endl; return 0; } int AC::Fun2(int key) { cout " Fun2( " key " ) from AC " endl; return 0; }

Теперь мы займёмся функцией main(). Первая пара операторов последовательно из объекта запускает функцию-член производного класса, а затем - подобную функцию базового класса. С этой целью используется квалифицированное имя функции-члена.

MyAC.Fun2(2); //Вызвана AC::Fun2()… MyAC.Fun1(2); //Вызвана AC::Fun1()… MyAC.A::Fun1(2); //Вызвана A::Fun1()…

Следующие строки посвящены попытке вызова функций-членов по указателю на объект базового типа. Предполагается, что в данный момент он настроен на объект MyAC.

pObj-Fun1(2); //Вызвана A::Fun1()…

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



//pObj-Fun2(2); //pObj-AC::Fun1(2);

То ли дело указатель на объект производного типа! И опять здесь нет ничего нового и неожиданного. С "нижнего этажа бункера" видны все "этажи"!

AC* pObjAC = MyAC; pObjAC-Fun1(2); pObjAC-Fun2(2); pObjAC-AC::Fun1(2); pObjAC-Fun1(2); pObjAC-A::Fun1(2);

И, разумеется, указатель на объект производного класса не настраивается на объект базового. //pObjAC = MyA;

Основной итог этого раздела заключается в следующем: указатель на объект базового класса можно настроить на объект производного типа. Через этот указатель можно "увидеть" лишь фрагмент объекта производного класса - его "базовую" часть - то, что объект получает в наследство от своих предков. Решение о том, какая функция должна быть вызвана, принимается транслятором. В момент выполнения программы всё уже давно решено. Какая функция будет вызвана из объекта производного типа - зависит от типа указателя, настроенного на данный объект. В этом случае мы наблюдаем классический пример статического связывания.


Унарное выражение


УнарноеВыражение ::= ПостфиксноеВыражение

::= ++ УнарноеВыражение

::= -- УнарноеВыражение

::= УнарнаяОперация ВыражениеПриведения

::= sizeof УнарноеВыражение

::= sizeof (ИмяТипа) ::= ВыражениеРазмещения

::= ВыражениеОсвобождения

УнарнаяОперация ::= * | | + | - | ! | ~

Унарные выражения группируются справа налево.

Вторая и третья БНФ являются основой для построения префиксных выражений увеличения и уменьшения (инкремента и декремента). Символ операции в выражении инкремента и декремента вовсе не означает, что в ходе вычисления значения выражения к операндам будут применяться операции уменьшения и увеличения. В сочетании с операндами производных типов определение значений этих выражений сопровождается вызовами специальных (операторных) функций.

В выражениях, представленных четвёртой БНФ, унарная операция * является операцией разыменования. Типом выражения приведения является указатель на объект типа X, а это указывает на то, что описываемое значение является l-выражением. Значением выражения является значение размещённого в памяти объекта. Если типом операнда является тип указатель на объект типа X, то типом выражения является непосредственно тип X.

Результатом операции является адрес объекта, представленного операндом. При этом операнд операции может оказаться либо l-выражением, либо квалифицированным именем. Но об этом позже.

Далее приводится множество БНФ, определяющих синтаксис выражений размещения и освобождения. У этих выражений достаточно сложная семантика. Детально разобрать их в данный момент мы пока просто не сможем. На этом этапе придётся ограничиться лишь самыми необходимыми сведениями.



Управление исключением - блоки try и catch, операция throw


Предлагаемое в C++ решение проблемы реакции на синхронные исключительные ситуации связано с использованием так называемых контролируемых блоков операторов.

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

Синтаксис контролируемых блоков описывается следующим множеством формул Бэкуса-Наура:

Оператор ::= КонтролируемыйБлокОператоров

КонтролируемыйБлокОператоров ::= try СоставнойОператор СписокРеакций

СписокРеакций ::= Реакция [СписокРеакций]

Реакция ::= catch (ОбъявлениеИсключения) СоставнойОператор

ОбъявлениеИсключения ::= СписокСпецификаторовТипа Описатель

::= СписокСпецификаторовТипа АбстрактныйОписатель

::= СписокСпецификаторовТипа

::= ...

Это одно из последних множеств БНФ на нашем пути. Всё те же знакомые описатели, любимые абстрактные описатели, и даже хорошо известное многоточие.

На основе данных БНФ строим контролируемый блок операторов. Как всегда, пока важен лишь внешний вид оператора.

КонтролируемыйБлокОператоров

try СоставнойОператор СписокРеакций

try { Оператор Оператор Оператор } СписокРеакций try { int retVal; retVal = MyFun(255); cout "retVal == " retVal "…" endl } catch (ОбъявлениеИсключения) СоставнойОператор

СписокРеакций try { int retVal; retVal = MyFun(255); cout "retVal == " retVal "..." endl } catch (char *) { x = x * 25; } catch (MyException MyProblem1) { cout "Неполадки типа MyException: " MyProblem1.text endl; } catch (...) { cout "Нераспознанные исключения..." endl; }

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

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

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

catch-блок содержит код, предназначенный для перехвата исключений. Однако без генератора исключений он абсолютно бесполезен.

Возбуждение (или генерация) исключения обеспечивается операцией throw. Это весьма странная операция. Даже с точки зрения синтаксиса:


Выражение ::= ГенерацияИсключения

ГенерацияИсключения ::= throw [Выражение]

Выражение, состоящее из одного символа операции (с пустым множеством операндов) уже является выражением. Выражением с неопределённым значением. Однако оператор на основе этого выражения построить можно! throw;

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

Его выполнение в автономном режиме или за пределами контролируемого блока приводит к завершению процесса выполнения программы. Точнее, сначала может быть вызвана функция unexpected, следом за которой по умолчанию запускается функция terminate. Она вызывает функцию abort для аварийного завершения работы программы.

Функции unexpected и terminate в ходе выполнения программы можно заменить какие-либо другими функциями, для чего следует воспользоваться функциями set_unexpected и set_terminate. Прототипы функций set_unexpected и set_terminate обычно располагаются в заголовочном файле except.h. В качестве параметров они получают указатели на функции и возвращают значения адресов замещённых функций. Так что по ходу дела всё можно будет с помощью этих же функций вернуть назад.

Пользовательские функции, замещающие функции unexpected и terminate, всё равно оказываются самыми последними функциями завершаемой программы. Именно поэтому они должны иметь тип возвращаемого значения void, а также не должны иметь параметров.

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

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

Операция throw может применяться в сочетании с операндом, каковым может оказаться выражение произвольного типа и значения.

Оператор, построенный на основе такого выражения, можно называть генератором исключения. А его место расположения обычно называют точкой генерации. Вот примеры разнообразных генераторов исключений:



throw 1; throw "Это сообщение об исключении…"; throw 2*2*fVal; throw (int)5.5; throw (ComplexType)125.96; /* Разумеется, если определён соответствующий конструктор преобразования или функция приведения. */

Для генератора важны как значение выражения-исключения, так и его тип. Иногда даже конкретное значение исключения не так важно, как его тип.

В качестве исключения может быть использовано значение указателя. Допускаются исключения и такого вида:

throw NULL; throw (void *) iVal;

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

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

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

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

ОбъявлениеФункции ::= [СписокСпецификаторовОбъявления] Описатель

[СпецификацияИсключения];

ОпределениеФункции ::= [СписокСпецификаторовОбъявления] Описатель

[ctorИнициализатор] [СпецификацияИсключения] ТелоФункции

СпецификацияИсключения ::= throw ([СписокТипов])

СписокТипов ::= [СписокТипов ,] ИмяТипа

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

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


Условные и логические выражения


УсловноеВыражение ::= ВыражениеИлиЛогическое

::= ВыражениеИлиВключающее ? Выражение : УсловноеВыражение

ВыражениеИЛогическое ::= ВыражениеИлиВключающее

::= ВыражениеИЛогическое ВыражениеИлиВключающее

ВыражениеИлиЛогическое ::= ВыражениеИЛогическое

::= ВыражениеИлиЛогическое ВыражениеИЛогическое



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


Очередная модификация базового класса приводит к неожиданным последствиям. Эта модификация состоит в изменении спецификатора функции-члена базового класса. Мы (впервые!) используем спецификатор virtual в объявлении функции. Функции, объявленные со спецификатором virtual, называются виртуальными функциями. Введение виртуальных функций в объявление базового класса (всего лишь один спецификатор) имеет столь значительные последствия для методологии объектно-ориентированного программирования, что мы лишний раз приведём модифицированное объявление класса A:

class A { public: virtual int Fun1(int); };

Один дополнительный спецификатор в объявлении функции и больше никаких (пока никаких) изменений в объявлениях производных классов. Как всегда, очень простая функция main(). В ней мы определяем указатель на объект базового класса, настраиваем его на объект производного типа, после чего по указателю мы вызываем функцию Fun1():

void main () { A *pObj; A MyA; AB MyAB; pObj = MyA; pObj-Fun1(1); AC MyAC; pObj = MyAC; pObj-Fun1(1); }

Если бы не спецификатор virtual, результат выполнения выражения вызова pObj-Fun1(1);

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

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

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


//pObj-Fun2(2); //pObj-AC::Fun1(2);

Результат отрицательный. Указатель, как и раньше, настроен лишь на базовый фрагмент объекта производного класса. И всё же вызов функций производного класса возможен. Когда-то, в разделах, посвящённых описанию конструкторов, нами был рассмотрен перечень регламентных действий, которые выполняются конструктором в ходе преобразования выделенного фрагмента памяти в объект класса. Среди этих мероприятий упоминалась инициализация таблиц виртуальных функций.

Наличие этих самых таблиц виртуальных функций можно попытаться обнаружить с помощью операции sizeof. Конечно, здесь всё зависит от конкретной реализации, но, по крайней мере, в версии Borland C++ объект-представитель класса, содержащего объявления виртуальных функций, занимает больше памяти, нежели объект аналогичного класса, в котором те же самые функции объявлены без спецификатора virtual. cout "Размеры объекта: " sizeof(MyAC) "…" endl;

Так что объект производного класса приобретает дополнительный элемент - указатель на таблицу виртуальных функций. Схему такого объекта можно представить следующим образом (указатель на таблицу мы обозначим идентификатором vptr, таблицу виртуальных функций - идентификатором vtbl):

MyAC::= vptr A AC

vtbl::= AC::Fun1

На нашей новой схеме объекта указатель на таблицу (массив из одного элемента) виртуальных функций не случайно отделён от фрагмента объекта, представляющего базовый класс лишь пунктирной линией. Он находится в поле зрения этого фрагмента объекта. Благодаря доступности этого указателя оператор вызова виртуальной функции Fun1 pObj-Fun1(1);

можно представить следующим образом: (*(pObj-vptr[0])) (pObj,1);

Здесь только на первый взгляд всё запутано и непонятно. На самом деле, в этом операторе нет ни одного не известного нам выражения.

Здесь буквально сказано следующее:

ВЫЗВАТЬ ФУНКЦИЮ, РАСПОЛОЖЕННУЮ ПО НУЛЕВОМУ ИНДЕКСУ ТАБЛИЦЫ ВИРТУАЛЬНЫХ ФУНКЦИЙ vtbl (в этой таблице у нас всего один элемент), АДРЕС НАЧАЛА КОТОРОЙ МОЖНО НАЙТИ ПО УКАЗАТЕЛЮ vptr.

В СВОЮ ОЧЕРЕДЬ, ЭТОТ УКАЗАТЕЛЬ ДОСТУПЕН ПО УКАЗАТЕЛЮ pObj, НАСТРОЕННОМУ НА ОБЪЕКТ MyAC. ФУНКЦИИ ПЕРЕДАЁТСЯ ДВА (!) ПАРАМЕТРА, ПЕРВЫЙ ИЗ КОТОРЫХ ЯВЛЯЕТСЯ АДРЕСОМ ОБЪЕКТА MyAC (значение для this указателя!), ВТОРОЙ - ЦЕЛОЧИСЛЕННЫМ ЗНАЧЕНИЕМ, РАВНЫМ 1.

Вызов функции-члена базового класса обеспечивается посредством квалифицированного имени. pObj-A::Fun1(1);



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

Мы в очередной раз модифицируем объявление классов A, AB и объявляем новый класс ABC.

Модификация классов A и AB сводится к объявлению в них новых функций-членов:

class A { public: virtual int Fun1(int key); virtual int Fun2(int key); }; ::::: int A::Fun2(int key) { cout " Fun2( " key " ) from A " endl; return 0; } class AB: public A { public: int Fun1(int key); int Fun2(int key); }; ::::: int AB::Fun2(int key) { cout " Fun2( " key " ) from AB " endl; return 0; } Класс ABC является производным от класса AB: class ABC: public AB { public: int Fun1(int key); }; int ABC::Fun1(int key) { cout " Fun1( " key " ) from ABC " endl; return 0; }

В этот класс входит объявление функции-члена Fun1, которая объявляется в косвенном базовом классе A как виртуальная функция. Кроме того, этот класс наследует от непосредственной базы функцию-член Fun2. Эта функция также объявляется в базовом классе A как виртуальная. Мы объявляем объект-представитель класса ABC: ABC MyABC;

Его схему можно представить следующим образом:

MyABC::= vptr A AB ABC

vtbl::= AB::Fun2 ABC::Fun1

Таблица виртуальных функций сейчас содержит два элемента. Мы настраиваем указатель на объект базового класса на объект MyABC, затем вызываем функции-члены:

pObj = MyABC; pObj-Fun1(1); pObj-Fun2(2);

В этом случае невозможно вызвать функцию-член AB::Fun1(), поскольку её адрес не содержится в списке виртуальных функций, а с верхнего уровня объекта MyABC, на который настроен указатель pObj, она просто не видна. Таблица виртуальных функций строится конструктором в момент создания объекта соответствующего объекта. Безусловно, транслятор обеспечивает соответствующее кодирование конструктора. Но транслятор не в состоянии определить содержание таблицы виртуальных функций для конкретного объекта. Это задача времени исполнения. Пока таблица виртуальных функций не будет построена для конкретного объекта, соответствующая функция-член производного класса не сможет быть вызвана. В этом легко убедиться, после очередной модификации объявления классов.

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



#include iostream.h class A { public: virtual int Fun1(int key); }; int A::Fun1(int key) { cout " Fun1( " key " ) from A." endl; return 0; } class AB: public A { public: AB() {Fun1(125);}; int Fun2(int key); }; int AB::Fun2(int key) { Fun1(key * 5); cout " Fun2( " key " ) from AB." endl; return 0; } class ABC: public AB { public: int Fun1(int key); }; int ABC::Fun1(int key) { cout " Fun1( " key " ) from ABC." endl; return 0; } void main () { ABC MyABC; // Вызывается A::Fun1(). MyABC.Fun1(1); // Вызывается ABC::Fun1(). MyABC.Fun2(1); // Вызываются AB::Fun2() и ABC::Fun1(). MyABC.A::Fun1(1); // Вызывается A::Fun1(). A *pObj = MyABC; // Определяем и настраиваем указатель. cout "==========" endl; pObj-Fun1(2); // Вызывается ABC::Fun1(). //pObj-Fun2(2); // Эта функция через указатель недоступна !!! pObj-A::Fun1(2); // Вызывается A::Fun1(). }

Теперь в момент создания объекта MyABC

ABC MyABC;

из конструктора класса AB (а он вызывается раньше конструктора класса ABC), будет вызвана функция A::Fun1(). Эта функция является членом класса A. Объект MyABC ещё до конца не сформирован, таблица виртуальных функций ещё не заполнена, о существовании функции ABC::Fun1() ещё ничего не известно. После того, как объект MyABC будет окончательно сформирован, таблица виртуальных функций заполнится, а указатель pObj будет настроен на объект MyABC, вызов функции A::Fun1() через указатель pObj будет возможен лишь с использованием полного квалифицированного имени этой функции:

pObj-Fun1(1); // Это вызов функции ABC::Fun1()! pObj-A::Fun1(1); // Очевидно, что это вызов функции A::Fun1()!

Заметим, что вызов функции-члена Fun1 непосредственно из объекта MyABC приводит к аналогичному результату: MyABC.Fun1(1); // Вызов функции ABC::Fun1().

А попытка вызова невиртуальной функции AB::Fun2() через указатель на объект базового класса заканчивается неудачей. В таблице виртуальных функций адреса этой функции нет, а с верхнего уровня объекта "посмотреть вниз" невозможно. //pObj-Fun2(2); // Так нельзя!



Результат выполнения этой программки наглядно демонстрирует специфику использования виртуальных функций. Всего несколько строк…

Fun1(125) from A. Fun1(1) from ABC. Fun1(5) from ABC. Fun2(1) from AB. Fun1(1) from A. ========== Fun1(2) from ABC. Fun1(2) from A.

Один и тот же указатель в ходе выполнения программы может настраиваться на объекты-представители различных производных классов. В результате в буквальном смысле одно и то выражение вызова функции-члена обеспечивает выполнение совершенно разных функций. Впервые мы сталкиваемся с так называемым ПОЗДНИМ или ОТЛОЖЕННЫМ СВЯЗЫВАНИЕМ.

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

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

И ещё один маленький пример, демонстрирующий изменение поведение объекта-представителя производного класса после того, как одна из функция базового класса становится виртуальной.

#include iostream.h class A { public: void funA () {xFun();}; /*virtual*/void xFun () {cout "this is void A::xFun();" endl;}; }; class B: public A { public: void xFun () {cout "this is void B::xFun ();"endl;}; }; void main() { B objB; objB.funA(); }

В начале спецификатор virtual а определении функции A::xFun() закомментирован. Процесс выполнения программы состоит в определении объекта-представителя objB производного класса B и вызова для этого объекта функции-члена funA(). Эта функция наследуется из базового класса, она одна и очевидно, что её идентификация не вызывает у транслятора никаких проблем. Эта функция принадлежит базовому классу, а это означает, что в момент её вызова, управление передаётся "на верхний уровень" объекта objB. На этом же уровне располагается одна из функций с именем xFun(), и именно этой функции передаётся управление в ходе выполнения выражения вызова в теле функции funA(). Мало того, из функции funA() просто невозможно вызвать другую одноименную функцию. В момент разбора структуры класса A транслятор вообще не имеет никакого представления о структуре класса B. Функция xFun() - член класса B оказывается недостижима из функции funA().

Но если раскомментировать спецификатор virtual в определении функции A::xFun(), между двумя одноименными функциями установится отношение замещения, а порождение объекта objB будет сопровождаться созданием таблицы виртуальных функций, в соответствии с которой будет вызываться замещающая функция член класса B. Теперь для вызова замещаемой функции необходимо использовать её квалифицированное имя:

void A::funA () { xFun(); A::xFun(); }


Виртуальные классы


Мы продолжаем модификацию последнего варианта нашей программы, добавляя к прототипу функции int A::Fun1(int); спецификатор virtual.

class A { public: int x0; virtual int Fun1(int key); };

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

Как бы мы ни старались, вызвать функцию-член класса A из любого фрагмента объекта-представителя производного класса D невозможно. Сначала конструкторы строят объект, настраивают таблицы виртуальных функций, а потом уже мы сами начинаем его "перекраивать", создавая на основе базового фрагмента видимость самостоятельного объекта. Напрасно. Объект построен, таблицы виртуальных функций также настроены. До конца жизни объекта виртуальные функции остаются недоступны.

Следует обратить особое внимание на то обстоятельство, что независимо от места вызова виртуальной функции (а мы её вызываем непосредственно из базовых фрагментов объекта), замещающей функции в качестве параметра передаётся корректное значение this указателя.

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

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

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


ОписательБазы ::= ПолноеИмяКласса

::= [virtual] [СпецификаторДоступа] ПолноеИмяКласса

::= [СпецификаторДоступа] [virtual] ПолноеИмяКласса

Модифицируем нашу программу. Мы добавим в описатели баз производных классов B и C спецификатор virtual:

class A { public: int x0; int Fun1(int key); }; class B: virtual public A { public: int x0; int Fun1(int key); int Fun2(int key); }; class C: public virtual A { public: int x0; int Fun2(int key); }; class D: public B, public C { public: int x0; int Fun1(int key); };

Вот как выглядит после модификации граф производного класса D:

A B C D

А вот как меняется структура класса D, представляемая в виде неполной схемы. Спецификатор virtual способствует минимизации структуры производного класса. Виртуальные базовые классы не тиражируются.

A B C D

А вот и схема объекта-представителя класса D.

D MyD; MyD ::= A (int)x0; (int)xA; B (int)xB; C (int)x0; D (int)x0; (int)xD;

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

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

В C++ допускаются такие варианты объявления производных классов, при которых одни и те же классы одновременно выступают в роли виртуальных и невиртуальных базовых классов. Например, сам класс D может быть использован в качестве базового класса:

class F: public A { ::::: } class G: public A, public D { ::::: } ::::: G MyG;

Множество одноименных виртуальных и невиртуальных базовых фрагментов, данных-членов, простых и виртуальных функций. Можно до бесконечности рисовать направленные ациклические графы, диаграммы классов и объектов… Искусство объявления классов и навигации по фрагментам объектов совершенствуется в результате напряжённой длительной практики.

Введём новое понятие, связанное с доступом к данным и функциям-членам производных классов.

Имя x (имя переменной, класса, или функции), объявленное в классе X, обозначается как X::x. Имя B::f доминирует над именем A::f, если объявление класса B содержит в списке баз имя класса A.

На понятии доминирования имён основывается правило доминирования, определяющее корректность доступа к членам производного класса, объявленного на основе виртуальных базовых классов.

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


Выбирающий оператор


ВыбирающийОператор ::= 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 (предварительная стадия); это значение последовательно сравнивается со значениями константных выражений за метками 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 блока условных операторов.

Однако каждый оператор хорош на своём месте.


Выражение


Выражение ::= ВыражениеПрисваивания

::= Выражение , ВыражениеПрисваивания

В контексте, где запятая выступает в роли разделителя, например, списке параметров вызова функции или в списке инициализации, запятая как знак операции может появиться только в круглых скобках:

MyFun(a, (w = 5, w + 9), c) /* Выражение вызова функции с тремя параметрами. Значение второго параметра задаётся выражением, значение которого равно 14. */

Большая часть выражений представляет собой сочетание символов операций и операндов. Однако это вовсе не означает, что в ходе вычисления значения подобных выражений непременно будут применяться соответствующие операции. Выражение - это видимость. В каждом конкретном случае всё зависит от типа операндов. Если операнды оказываются операндами основного типа, либо указателями, то можно предположить, что при вычислении его значения будет выполняться конкретная операция C++. Если же операнды выражения оказываются операндами производного типа, символ операции может оказаться эквивалентным вызову операторной функции. И кто знает, что делает эта самая операторная функция.



Выражение и l-выражение


Доступ к объектам и функциям обеспечивается выражениями, которые в этом случае ссылаются на объекты.

Выражение, которое обеспечивает ссылку на константу, переменную или функцию, называется l-выражением. Имя объекта в C++ является частным случаем l-выражения.

В C++ допускается изменение значений переменных. Значения констант и функций в C++ изменению не подлежат. l-выражение называется модифицируемым l-выражением, либо леводопустимым выражением, если только оно не ссылается на функцию, массив или константу. Таким образом, леводопустимыми выражениями называют l-выражения, которые ссылаются на переменные.



Выражение освобождения


ВыражениеОсвобождения ::= [::] delete ВыражениеПриведения

::= [::] delete [] ВыражениеПриведения

Это выражение не имеет определённого значения. А значит и о типе выражения мало что можно сказать определённого. Возможно, что оно является выражением типа void. Именно так обозначается специальный тип, который называется также пустым типом. Операция delete работает с динамической памятью. Она способна прочитать скрытую дополнительную информацию о ранее размещённом в динамической памяти с помощью операции new объекте. Поэтому операция delete требует всего одного операнда указателя на объект.

Последствия выполнения операции delete над указателем непредсказуемы и ужасны, если он ссылается на объект, который ранее не был размещён с помощью операции new. Гарантируется безопасность действия операции delete над нулевым указателем. Для удаления массивов используется специальный вариант операции с квадратными скобками. Удаление констант считается ошибкой. Она фиксируется на стадии трансляции. Позже мы обсудим назначение разделителя в выражениях освобождения и размещения '::'.



Выражение приведения


Для преобразования данного значения к определённому типу используется выражение явного преобразования (одна из разновидностей постфиксного выражения). Оно имеет вид функциональной формы записи: имя типа, за которым в скобочках записывается список выражений.

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

ВыражениеПриведения ::= УнарноеВыражение

::= (ИмяТипа) ВыражениеПриведения

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



Выражение размещения


ВыражениеРазмещения

::= [::] 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]…



Выражения отношения


ВыражениеОтношения ::= ВыражениеСдвига

::= ВыражениеОтношения ВыражениеСдвига

::= ВыражениеОтношения ВыражениеСдвига

::= ВыражениеОтношения = ВыражениеСдвига

::= ВыражениеОтношения = ВыражениеСдвига



Выражения присваивания


ВыражениеПрисваивания ::= УсловноеВыражение

::= УнарноеВыражение ОперацияПрисваивания ВыражениеПрисваивания

ОперацияПрисваивания ::= = | *= | /= | %= | += | -= | = | = | = | ^= | |=



Выражения равенства


ВыражениеРавенства ::= ВыражениеОтношения

::= ВыражениеРавенства == ВыражениеОтношения

::= ВыражениеРавенства != ВыражениеОтношения



Выражения с указателями


pmВыражение ::= ВыражениеПриведения

::= pmВыражение .* ВыражениеПриведения

::= pmВыражение -* ВыражениеПриведения



Выражения сдвига


ВыражениеСдвига ::= АддитивноеВыражение

::= ВыражениеСдвига АддитивноеВыражение

::= ВыражениеСдвига АддитивноеВыражение



Вызов операторной функции operator ~() против вызова деструктора


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

На основе символа операции '~' может быть определена операторная функция. Нам сейчас абсолютно безразлично её назначение и устройство. Мы определим её лишь в самых общих чертах.

ComplexType ComplexType::operator ~ () { cout "Это ComplexType ComplexType::operator ~ ()" endl; return ComplexType(); }

Кроме того, предположим существование ещё одной функции-члена класса ComplexType, в теле которой и расположим интересующие нас выражения и операторы.

void ComplexType::xFun() { ::::: ComplexType CTw = ComplexType(); /* В результате выполнения выражения преобразования типа вызывается конструктор умолчания, который создаёт временный объект, значение которого копируется в переменную CTw. */ ~CTw; CTw.operator ~(); /* Сокращённая и полная формы вызова операторной функции ComplexType ComplexType::operator ~ () */ ~ComplexType(); /* Создаётся временный безымянный объект, для которого вызывается операторная функция ComplexType ComplexType::operator ~ (). Используется сокращённая форма вызова. */ ComplexType().operator ~(); /* Создаётся временный безымянный объект, для которого вызывается операторная функция ComplexType ComplexType::operator ~ (). Используется полная форма вызова. */ CTw.~ComplexType(); /*Наконец, явный вызов деструктора для объекта CTw */ this-~ComplexType(); /* Явный вызов деструктора для объекта, расположенного по адресу *this */ ::::: }