c-pp - СО РАН

advertisement
С++
1. Введение
1.1 Обзор языка
Язык С++ был создан Американским ученым Бьярном Строуструпом в первой
половине 80-х годов. Задачи:
- улучшить С
- поддержать абстракцию данных
- поддержать объектно-ориентированное программирование.
Улучшение С выразилось в:
- контроль типов параметров
- операции работы с памятью (динамич. создание и уничтожение объектов)
- совмещение (overloading) имен функций и операций
- встраивание функций в код программы.
С был взят в качестве базового языка, потому что он:
- многоцелевой, лаконичный и относительно низкого уровня,
- отвечает большинству задач системного программирования,
- идет везде и на всем,
- может быть использован в среде программирования UNIX.
Абстракция данных есть метод разработки программ с представлением в ней
понятий из прикладной области как пользовательских (user-defined) типов данных.
Интерфейс типа данных (спецификация) отделяется от его реализации, что
- облегчает понимание программ, позволяя прямо описывать понятие в
естественных для него терминах,
- изменять реализацию типа данных, не задевая пользователей.
С++ обеспечивает абстракцию данных поддерживая следующие понятия и
возможности:
- конструктор пользовательских типов class
- средства управления доступом (public, private)
- абстрактные классы
- гарантированная инициализация и очистка объектов
- пользовательские преобразования типов объектов
- параметризованные (родовые) функции и типы данных
- обработка исключительных ситуаций.
Ключевым понятием в С++ является класс. Классы обеспечивают перечисленные
выше возможности.
ООП основывается на иерархической организации классов подобно тому, как
устанавливается иерархия понятий в реальной жизни. В С++ оно поддерживается
понятиями
- производный класс (подкласс)
- виртуальная функция (подмена функций).
Концепция класса вместе с производными классами и виртуальными функциями
была взята из языка Симула 67. Возможности совмещения имен и расположения
описаний везде, где может встретиться оператор, взяты из языка Алгол 68. Родовые
конструкции (generics) появились под влиянием языков Клу и Ада. Из Ады взят
также механизм обработки исключительных ситуаций.
1.2. Нотация для синтаксиса
Синтаксические понятия выделяются курсивом, а литеральные слова и символы
(терминальные понятия) - жирным шрифтом. Альтернативы перечисляются на
отдельных строчках, кроме немногих случаев, в которых большое количество
альтернатив представлено на одной строчке, помеченной фразой “одно из”.
Факультативное понятие помечается индексом opt, так что
{выражение opt}
означает возможное выражение, заключенное в фигурные скобки.
2
2. Лексические соглашения
Программа на С++ состоит из одного или нескольких файлов (3.3). Файл
представлят собой фрагмент текста, содержащий исходную программу на С++ с
командами прероцессора. Концептуально каждый файл транслируется в несколько
проходов. На первом проходе выполняется препроцессирование (16): в частности,
на этом этапе производятся сборка файлов и макроподстановки.
Препроцессирование управляется директивами препроцессора. Ими считаются
строки, начинающиеся с литеры #. Результатом препроцессирования является
последовательность лексем, называемая единицей трансляции (translation unit).
2.1. Лексемы
Имеется 5 видов лексем: идентификаторы, ключевые слова, литералы, знаки
операций и прочие разделители. Пробел, символы табуляции, новой строки и
протяжки страницы, а также комментарии (обобщенно - символы пустых
промежутков) служат для разделения лексем. Каждая очередная лексема
выбирается как наиболее длинная строка, начиная с данной литеры, которая может
образовать лексему.
2.2. Комментарии
Сочетание литер /* начинает, а сочетание литер */ заканчивает комментарий.
Комментарии нельзя вкладывать друг в друга. Литеры // открывают комментарий,
который который заканчивается в конце той же строки.
2.3. Идентификаторы
Идентификатор (имя) - это последовательность букв, цифр и символа “_”,
начинающаяся с буквы (символ “_” считается буквой). Большие и малые буквы
считаются различными. Имена, начинающиеся с подчерка, по традиции
зарезервированы и используются специальным образом в системах,
обеспечивающих выполнение программ.
2.4. Ключевые слова
Следующие идентификаторы могут употребляться только как ключевые слова:
asm
auto
break
case
catch
char
class
continue
default
delete
do
double
else
enum
float
for
friend
goto
if
inline
int
new
operator
private
protected
public
register
return
3
signed
sizeof
static
struct
switch
templete
this
try
typedef
union
unsigned
virtual
void
volatile
const
extern
long
short
throw
while
Для обозначения операций и в качестве знаков препинания используются
следующие символы и их комбинации:
!
[
->
||
% ^
]
\
++
*=
&
;
-/=
*
(
)
‘
:
“
.*
->*
%=
+=
<
<<
-=
+
=
{
>
?
,
>>
<=
<<= >>=
}
.
>=
&=
|
/
==
^=
~
!=
\=
&&
::
Две лексемы обрабатывает препроцессор: # ##
2.5. Литералы
Существует несколько видов литералов (часто называемые константами).
литерал:
целая-константа
литерная-константа
плавающая-константа
литеральная-строка
5.2.1. Целые константы
Целая константа состоит из последовательности десятичных цифр, начинающейся
не с нуля. Последовательность цифр без 8 и 9, начинающаяся с нуля,
рассматривается как восьмеричное число. Последовательность цифр,
начинающаяся с 0х или 0Х и возможно включающая буквы от А (а) до F (f),
рассматривается как шестнадцатиричное число. Пример:
двенадцать: 12
014
0XC
Тип целой константы зависит от ее формы, значения и суффикса. Если она
является десятичной и не содержит суффикса, то имеет первый из следующих
типов, в котором может быть представлено ее значение:
int, long int, unsigned long int.
Восьмеричная или шестнадцатиричная константа без суффикса имеет первый из
следующих типов, в котором может быть представлено ее значение:
int, unsigned int, long int, unsigned long int.
Если константа содержит суффикс u или U, она имеет первый из следующих типов,
в котором может быть представлено ее значение:
unsigned int, unsigned long int.
Если константа содержит суффикс l или L, она имеет первый из следующих типов,
в котором может быть представлено ее значение:
long int, unsigned long int.
4
Если константа имеет один из суффиксов ul, lu, uL, Lu, Ul, lU, UL, LU, ее типом
является unsigned long int.
2.5.2. Литерные константы
Литерная константа представляет собой последовательность из одной или
нескольких литер, заключенных в одинарные кавычки, как, например, ‘x’.
Литерная константа, содержащая одну литеру, имеет тип char. Многолитерные
константы имеют тип int. Значение многолитерной константы зависит от
реализации и потому их лучше избегать.
Специальный символ \ служит для представления неграфических литер:
новая строка (new line)
горизонтальная табуляция (horisontal tab)
вертикальная табуляция (vertical tab)
возврат на символ (backspace)
возврат каретки (carriage return)
протяжка страницы (form feed)
внимание (alert)
обратная косая черта (backslash)
вопросительный знак (question mark)
одинарная кавычка (single quote)
двойные кавычки (double qoute)
восьмеричное число (octal number)
шестнадцатиричное число (hex number)
NL (LF)
HT
VT
BS
CR
FF
BEL
\
?
‘
“
ooo
hhh
\n
\t
\v
\b
\r
\f
\a
\\
\?
\’
\”
\ooo
\xhhh
Последовательности типа \ооо или \xhhh используется для спецификации кода
литеры. В случае превышения допустимого значения типа char значение литерной
константы определяется реализацией.
2.5.3. Константы с плавающей точкой
Константа с плавающей точкой состоит из целой части, десятичной точки и
дробной части, литеры e или E, возможного целого показателя степени с
возможным знаком и возможного суффикса. Либо целая часть, либо дробная часть
(но не обе сразу) может быть опущена. Плавающая константа имеет тип double,
если только он не задан явно суффиксом. Суффиксы f и F задают тип float,
суффиксы l и L задают тип long double. Примеры:
double: 2.0, 2., 0.2e1, .2E1
float: 2.0F, 20e-1F
long double: 2.0L
В середине константы с плавающей точкой не может быть пробела. Например,
65.43 е-21 рассматривается как четыре отдельные лексемы:
65.43 е - 21
и считается синтаксической ошибкой.
5
2.5.4. Литерные строки
Литерная строка представляет собой последовательность из одной или нескольких
литер, заключенных в двойные кавычки, например, “...”. Строка имеет тип “массив
литер” (array of char) и статический класс памяти (3.5). В реализации конец строки
помечается литерой \’0’. Размер строки равен числу ее литер, включая
завершающий нуль. Например,
sizeof("asdf") == 5;
Строка "asdf" имеет тип char[5]. Пустая строка записывается как "" и имеет тип
char[1].
Длинные строки могут быть разорваны пропуском (т.е. ' ', '\t', '\v', '\f', '\n' или
комментарием) для того, чтобы сделать текст более четким. Пример:
char alfa[] = "abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
Такие смежные строки сцепляются, поэтому alfa могла бы быть эквивалентно
инициализирована одной строкой
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
2.5.5. Нуль
Нуль имеет тип int. Благодаря стандартным преобразованиям (4), его можно
употреблять как константу любого целого, плвающего или указательного типа. Тип
нуля определяется контекстом.
6
3. Основные понятия
Имя может обозначать объект, функцию, множество функций, перечислитель, тип,
член класса, шаблон, значение или метку. Имя вводится в программу посредством
объявления. Имя может быть употреблено только внутри ее области действия
(scope). Каждое имя имеет тип, который определяет способ его употребления.
Объект есть область памяти (3.7). Именованный объект имеет класс памяти (3.5),
задающий его время жизни. Смысл значения объекта определяется типом
выражения, использованного для доступа к нему.
3.1. Объявления и описания
Оъявление (7) вводит в программу одно или несколько имен. Объявление является
описанием за исключением следующих случаев:
1) оно объявляет функцию без спецификации тела (8.3),
2) содержит спецификатор extern (7.1.1) при отсутствии инициализатора и
тела функции,
3) является объявлением статического компонента в объявлениии класса (9.4),
4) является объявлением имени класса (9.1),
5) является объявлением имени типа (typedef) (7.1.3).
Примеры описаний:
int a;
extern const c = 1;
int f(int x) {return x+a;};
struct S {int a; int b;};
enum {up, down};
Примеры чисто объявлений:
extern int a;
extern const c;
int f(int);
struct S;
typedef int Int;
В программе должно быть ровно одно описание каждого объекта, функции, класса
и перечислителя, в ней использованных (объявления могут повторяться).
3.2. Области действия
Существует 4 разновидности областей действия: локальная, функция, файл и класс.
Локальная: Имя, объявленное в блоке (6.3), локализовано в этом блоке и может
быть использовано только в нем и в блоках, вложенных в него, начиная с места
7
объявления. Имена формальных параметров функции считаются объявленными в
самом внешнем блоке тела этой функции.
Функция: Метки (6.1), объявленные в теле функции, могут быть использованы
повсюду в теле функции.
Файл: Имя, объявленное вне всех блоков (6.3) и классов (9), можно использовать в
транслируем файле, содержащем это объявление, начиная с места объявления.
Такое имя называется глобальным (global).
Класс: Имя компонента класса локализуется в этом классе и может быть
использовано только:
1) в компонентной функции этого класса (9.3),
2) после операции “.” (точка), примененной к объекту этого класса (5.2.4),
или его производного класса (10),
3) после операции ->, примененной к указателю на объект этого класса
(5.2.4) или производного от него,
4) после операции :: (5.1), примененной к имени этого класса или
производного от него.
Имя, объявленное как дружественное (friend, 11.4), считается глобальным; то же
самое справедливо для имени класса, впервые объявленного в качестве типа
результата функции или типа параметра.
При повторном объявлении имени во вложенном блоке или классе предыдущее
объявление оказывается скрытым; после выхода из блока имя вновь обретает
прежний смысл. Скрытое имя компонента класса все же можно употреблять, если
его уточнять именем класса при помощи операции :: (5.1, 9.4, 10). Скрытое имя
объекта, функции, типа и перечислителя, у которого областью действия является
файл, можно использовать посредством унарной операции :: (5.1). Пример:
int g = 99;
f(int g) {return g ? g : ::g} //в последнем случае используется глобальное g
3.3. Программа и сборка
Пограмма состоит из одного или нескольких файлов, собранных вместе. Файл
состоит из последовательности объявлений. Глобальное имя, объявленное как
статическое (static), локализовано в своей единице трансляции и может быть
использовано в других файлах для именования других объектов и т.п. Такое имя
называется внутренним (internal linkage). Имя, объявленное как встраиваемое
(inline) или как const но без extern, также локализовано в своей единице
трансляции.
Каждое объявление глобального имени, не являющегося внутренним, приводит в
многофайловой программе к одному и тому же объекту (3.7), функции (8.2.5) или
8
классу (9). Такое имя называется внешним (external linkage). Внешним является
также имя, явно объявленное как extern.
Типы во всех объявлениях внешнего имени должны быть идентичны. Программа
должна содержать ровно одно описание каждой функции, объекта, класса или
перечислителя.
3.3.1. Компоновка
Иметь всю программу в одном файле обычно невозможно, поскольку
подпрограммы стандартных библиотек и операционной системы находятся в
другом месте. Кроме того, хранить большую программу в одном файле
непрактично и неудобно: поскольку файл является единицей трансляции, во всех
случаях, когда в него вносятся изменения, весь файл надо транслировать заново.
Чтобы обеспечить раздельную компиляцию, программист должен сделать
объявления, дающие информацию о типах, необходимую для анализа файла
независимо от остальной программы. Для этого объявления всех имен должны
быть согласованы. Рассмотрим два файла:
// file1.c:
int a = 1;
int f() { /* что-то делает */}
// file2.c:
extern int a;
int f();
void g() { a = f(); }
Переменная а и функция f(), которые использует функция g() в файле file2.c, - такие
же, как и те, что определены в файле file1.c. Ключевое слово extern указывает, что
объявление а в file2.c является только объявлением, а не описанием. Объект в
программе должен описываться только один раз, объявляться он может много раз,
но все объявления должны быть согласованы. Пример:
// file1.c:
int a = 1;
int b = 1;
extern int c;
// file2.c:
int a;
extern double b;
extern int c;
9
Здесь три ошибки: а описано дважды (int a; является описанием, которое означает
int a = 0;), b объявлено дважды с разными типами, а с объявлено дважды, но не
описано.
Другая картина при использовании описателя static:
// file1.c:
static int a = 6;
static int f() { /* . . . */}
// file2.c:
static int a = 7;
static int f() { /* . . . */}
Поскольку каждое а и f объявлено как static, программа правильна.
3.3.2. Заголовочные файлы
Обним из простых способов достижения согласованности определений различных
файлов является включение в исходные файлы программы заголовочных файлов с
интерфейсной информацией. Директива
#include "to-be-included"
замещает себя содержимым файла "to-be-included", которым должен быть
исходный текст на С++. Такое включение обычно делается препроцессором С++.
Для включения файлов из стандартного каталога вместо кавычек используются
угловые скобки "<" и ">". Пример:
#include <stream.h> // из стандартного каталога
#include "myheader.h" // из текущего каталога
В заголовочном файле могут содержаться:
Определения типов
Параметризованные типы
Объявления функций
Описания встраиваемых
функций
Одъявления данных
Описания констант
Перечисления
Объявления имен
Директивы включения
Определения макросов
Комментарии
struct point {int x, y;};
templete <class T> class V;
extern int strlen(const char*);
inline char get() {return *p++};
extern int a;
const float pi = 3.14.1593;
enum bool {false, true};
class Matrix;
#include <signal/h>
#define Case break; case
/* проверка на конец файла */
10
Заголовочные файлы обычно имеют суффикс .h, а файлы, содержащие функции
или определения данных - суффикс .с.
3.4. Старт и завершение
Пограмма должна содержать функцию с именем main(). Эта функция
предназначается для входа в программу. Она не может быть вызвана из программы.
Нельзя также брать ее адрес и объявлять ее как inline или static.
Вызов функции void exit(int), объявленной в <stdlib.h>, завершает программу.
Значение параметра возвращается окружению как результат работы программы.
Оператор return в main() приводит к вызову exit() с возвращаемым значением как
фактическим параметром.
Обычно программа осуществляет какой-либо вывод. Пример:
#include <iostream.h>
int main()
{
cout << "Hello, world!\n";
}
Здесь строка #include <iostream.h> сообщает компилятору о необходимости
использования стандартных средств ввода-вывода, находящихся в файле
iostream.h. Операция "<<" ("поместить в") переписывает второй аргумент в
первый (в данном случае, строку "Hello, world!\n" в стандартный поток cout.
Значение типа int, возвращаемое функцией main(), программа передает
окружающей среде. Если ничего не случается, среда получает случайное значение.
3.5. Классы памяти
В языке имеется два объявляемых класса памяти: автоматический и статический.
Автоматические (или локальные) объекты локализованы относительно
каждого входа в блок.
Статические объекты существуют и сохраняют свои значения в течение всего
времени выполнения программы.
Локальные объекты инициализируются (12.1) всякий раз при исполнении их
описаний и разрушаются (12.4) при выходе из содержащих их блоков (6.7).
Статический глобальный объект (любой объект, описанный на уровне файла)
инициализируется в единице трансляции до первого использования любой
функции или объекта, определенного в ней. Инициализация локальных
статических объектов (со спецификатором static) будет объяснена в 8.4.
11
3.6. Типы данных
Каждое имя (идентификатор ) в С++ программе имеет ассоциированный с ним тип,
который определяет: какие операции можно применить к имени (т.е. к объекту, на
который он ссылается) и как эти операции интерпретируются. Примеры:
int error-number;
float real(complex* h);
Поскольку error-number объявлено как int, его можно присваивать, использовать в
арифметических выражениях и т.д. Что касается функции real, то она может
вызываться с адресом complex в качестве параметра.
Имена вроде int и float являются именами типа. Обычно имя типа используется в
объявлениях для спецификации других имен. Допускается еще два действия над
именами типов: sizeof (для определения объема памяти, требуемой для размещения
объекта данного типа) и new (для размещения объекта данного типа в свободной
памяти). Пример:
int main()
{
int* p = new int;
cout << "sizeof(int) = " << sizeof(int) '\n';
}
Различаются два вида типов данных в С++: основные, или базовые (fundemental), и
производные (derived).
3.6.1. Основные типы
Имеется несколько основных типов и несколько их разновидностей. Они
соответствуют наиболее общим и широко используемым основным аппаратным
единицам памяти компьютера.
А) Литерные типы
Объект, объявленный как литера (char) может содержать любую литеру из набора
литер реализации. Литерные объекты могут быть явно обозначены как unsigned
(без знака) или signed (со знаком). Представление всех трех типов определяется
реализацией.
Б) Целые типы
12
Существует три вида целых типов: short int, int и long int. Представление зависит от
реализации, но обычно длинные целые занимают не меньше памяти, чем короткие
целые. Целые, объявленные как unsigned служат для представления натуральных
чисел.
Поскольку int - это по умолчанию тип со знаком, знаковые целые есть просто
сининимы обычных целых. Для компактности можно опускать int в комбинациях:
что не меняет смысла, так как long означает long int, и тип unsigned означает
unsigned int. Вообще, когда в объявлении опущен тип, предполагается int.
В) Плавающие типы
Существует три вида плавающих типов: float, double и long double. Тип double
обеспечивает не меньшую точность представления, чем float, а тип long double - не
меньшую, чем double.
Типы char, int всех размеров, а также перечисления (7.2) называются
целочисленными (integral) типами. Целочисленные и плавающие вместе составляют
арифметические типы. Типичные размеры в реализации (в байтах):
char
short int
int
long int
float
double
long double
1
2
2 или 4
4 или 8
4
8
12 или 16
Тип void определяет пустое множество значений. Он задается в качестве типа
результата функции, не возвращающей значения, или как базовый тип для
указателей на объекты неизвестного типа. Примеры:
void f();
void* pv;
// f не возвращает значения
// указатель на объект неизвестного типа
Переменной типа void* можно присвоить указательное значение любого типа. Это
применяется главным образом для передачи указателей функциям, которые не
позволяют сделать предположение о типе объекта, и для возврата из функций
нетипизированных обектов. Чтобы использовать такой объект, необходимо
применить явное преобразование типа. Подобные функции находятся на самом
нижнем уровне системы, там где осуществляется работа с основными аппаратными
ресурсами. Пример:
void* malloc(unsigned size);
void free(void*);
13
void f()
// выделение памяти в стиле C
{
int* pi = (int*)malloc(10*sizeof(int));
char* pc = (char*)malloc(10);
// . . .
free(pi);
free(pc);
}
Здесь запись (type)exprssion задает преобразование expression к типу type.
3.6.2. Производные типы
Существует потенциально бесконечное число производных типов,
сконструированных из основных типов следующими способами:
массивы (arrays) объектов заданного типа (8.2.4);
функции (functions) с параметрами заданных типов, возвращающие значение
указанного типа (8.2.5);
указатели (pointers) на объекты или функции заданного типа (8.2.1);
ссылки (references) на объекты или функции заданного типа (8.2.2);
константы (constants), являющиеся значениями заданного типа (7.1.6);
классы (classes), определяющие объекты, возможно составленные из значений
разных типов (9), набор функций для работы с этими объектами (9.3) и
набор ограничений на доступ к объектам и функциям (11);
структуры (structures), которые являются классами без ограничений на доступ
к объктам и функциям-членам структуры (11);
объединения (unions), являющиеся структурами, способными содержать
объекты разных типов в разные моменты времени (9.5);
указатели на компоненты класса (pointers to class members), которые (8.2.3)
идентифицируют компоненты заданного типа среди объектов данного
класса.
Объекты типа void* (указатель на void), const void* и volatile void* могут
указывать на объекты неизвестного типа. Представление типа void* должно
содержать достаточное число бит, чтобы хранит указатель на любой объект.
3.6.3. Именование типов
Основные и производные типы могут получать новые имена посредством
механизма typedef (7.1.3), а семейство типов или функций может быть задано или
поименовано посредством шаблонов (14). Пример:
typedef complex point;
14
3.7. L-значения
Объект есть область памяти; l-значением (l-value) называется выражение,
ссылающееся на объект или функцию. Очевидным примером l-выражения является
имя объекта. Некоторые операции требуют l-значение. Например, если Е есть
выражение, вырабатывающее указатель, то E* есть l-выражение, ссылающееся на
объект, на который указывает Е. Название “l-значение” возникло из оператора
присваивания Е1 := Е2, в котором левый операнд должен быть l-значением.
Пример сложного l-выражения:
p[a+10] = 7;
15
4. Стандартные преобразования
Некоторые операции могут в зависимости от своих операндов приводить значение
операнда от одного типа к другому.
4.1. Целочисленные расширения
Значения типов char и short int, элементы перечисления, объекты типа
перечисления (7.2) и целые битовые поля (9.6) могут употребляться везде, где
употребляются целые. Если int может представить все значения исходного типа,
значение приводится к int, в противном случае оно преобразуется в unsigned int.
Этот процесс называется целочисленным расширением (integral promotion).
4.2. Целочисленные преобразования
Когда целое преобразуется к беззнаковому типу, получатся значение наименьшего
целого без знака, конгруэнтного целому со знаком. Когда значение преобразуется к
знаковому типу, оно остается неизменным, если оно может быть представлено в
новом типе, в противном случае результат зависит от реализации.
4.3. Плавающие типы одинарной и двойной точности
Для выражений типа float используется вещественная арифметика одинарной
точности. Когда значение менее точного плавающего типа преобразуется к равному
или более точному плавающему типу, значение не меняется. Когда значение более
точного плавающего типа преобразуется к менее точному плавающему типу и оно
находится в границах представления, результат может быть следующим большим
или предшествующим меньшим представимым значением.
4.4. Плавающие и целочисленные типы.
Преобразование значения плавающего типа к целочисленному типу сводится к
обрубанию, т.е. отбрасыванию дробной части. Результат не определен, если
значение не может быть представлено этим целочисленным типом.
Преобразование значения целочисленного типа к плавающему типу математически
настолько точно, насколько позволяет аппаратура.
4.5. Арифметические преобразования
1. Если один операнд имеет тип long double, другой операнд приводится к long
double.
2. В противном случае, если один операнд имеет тип double, другой операнд
приводится к double.
16
3. В противном случае, если один операнд имеет тип float, другой операнд
приводится к float.
4. В противном случае, над обоими операндами производятся целочисленные
расширения.
4а. В этом случае, если один из операндов - unsigned long, другой преобразуется к
unsigned long.
4б. В противном случае, если один операнд имеет тип long int, а другой - unsigned
int, то, если long int может представить все значения unsigned int, последний
преобразуется к long int; иначе оба операнда преобразуются к unsigned long int.
4в. В противном случае, если один операнд имеет тип long, другой операнд
приводится к long.
4г. В противном случае, если один операнд имеет тип unsigned, другой операнд
приводится к unsigned.
4д. В противном случае оба операнда принадлежат к типу int.
Система безопасных преобразований может быть представлена следующими
цепочками:
signed char -> short -> int -> long
unsigned char -> unsigned shrt -> unsigned int -> unsigned long
float -> double -> long double
Любая реализация обычно имеет большее число безопасных преобразований.
4.6. Преобразования указателей
Над указателями при присваивании, инициализации, сравнении и другом
использовании (8.2.1) могут быть выполнены следующие преобразования:
1. Константное выражение (5.19), равное нулю, преобразуется к пустому указателю
(null pointer). Гарантируется, что этот указатель отличается от указателя на любой
объект.
2. Указатель на переменный объект может быть преобразован к void*.
3. Укаазатель на функцию можно преобразовать к void*, если void* имеет
достаточный размер, чтобы представить его.
17
4. Указатель на объект некоторого класса можно преобразовать к указателю на
объект доступного базового класса (10), если преобразование однозначно (10.1);
базовый класс считается доступным, если доступны его открытые (public)
компоненты (11.1). Пустой указатель преобразуется в себя.
5. Выражение типа “массив элементов типа Т” можно преобразовать к указателю
на начальный элемент этого массива.
6. Выражение типа “функция с результатом типа Т” можно преобразовать к
указателю на “функцию с результатом типа Т”, за исключением случая, когда оно
является операндом операции взятия адреса или операции вызова функции.
4.7. Преобразования ссылок
Следующие преобразования могут быть выполнены всякий раз, когда
инициализируются ссылки (8.2.2):
Ссылку на класс можно преобразовать в ссылку на доступный базовый класс,
если это преобразование однозначно (10.1.1). Результатом является ссылка на
подобъект объекта производного класса.
4.8. Преобразования указателей на компоненты
Следующие преобразования могут быть выполнены над указателями на
компоненты класса (8.2.3) при их использовании:
1. Константное выражение, (5.19), вычисленное равным 0, преобразуется в
указатель на компонент, отличный от любого кругого указателя на компонент.
2. Указатель на компонент класса можно преобразовать в указатель на компонент
класса, производного от данного, если при этом доступно (11.1) и однозначно
обратное преобразование от указателя производного класса к улазателю базового
класса (10.1.1).
Правило преобразования указателя на компонент (от указателя на компонент
базового класса к указателю на компонент производного класса) является
обратным по отношению к правилу для указателей на объекты (от указателя на
производный к указателю на базовый (6.4, 10). Эта инверсия необходима для
поддержки системы типов данных.
18
5. Выражения
Выражение есть последовательность знаков операций и операндов, задающая
вычисление. Выражение может вырабатывать результирующее значение и
вызывать побочные эффекты.
Порядок вычисления подвыражений определяется правилами старшинства и
группирования операций. Обычные математические правила для ассоциативных и
коммутативных операций можно применять только к действительно
ассоциативным и коммутативным операциям. Исключая то, что оговорено особо,
порядок вычисления операндов в операции не определен. В частности, если объект
модифицируется в выражении дважды, результат не определен, кроме случаев,
когда порядок гарантируется составляющими выражение операциями.
5.1 Первичные выражения
Первичными выражениями являются литералы, имена, а также имена уточненные
знаком операции указания области действия “::” (scope resolution operator).
первичное-выражение:
литерал
this
:: идентификатор
:: имя-функции-операции
:: уточненное-имя
(выражение)
имя
Литерал (literal) есть первичное выражение, тип которого определяется видом
литерала (2.5).
В теле компонентной функции класса (9.3) ключевое слово this обозначает
указатель на объект, для которого эта функция вызвана. Вне тела компонентной
функции класса это ключевое слово употребляться не может.
Операция “::” вместе с сопровождающим идентификатором, именем-функцииоперации или уточненным-именем образуют первичное выражение, тип которого
определяется типом операнда. Результат операции “::” - идентификатор, имя или
имя-функции-операции. Область действия операнда должен быть файл. Операция
дает возможность обращаться к операнду, даже если его идентификатор скрыт.
Пример:
int x;
void f()
{
int x = 1;
::x = 2;
//скрывает глобальное x
// присваивает глобальному x
19
}
Выражение в круглых скобках есть первичное выражение, чей тип и значение
идентичны такому же выражению без скобок.
Имя есть та форма выражения, в которой оно может поямляться после операций “.”
и “->“(5.2.4).
имя:
идентификатор
имя-функции-операции
имя-функции-приведения
~ имя-класса
уточненное-имя
Идентификатор есть имя, если он объявлен соответствующим образом (7). Имяфункции-операции и имя-функции-приведения объясняютя в 13.4 и 12.3.2,
соответственно. Имя-класса с предшествующей тильдой обозначает деструктор
(12.4).
уточненное-имя:
уточненное-имя-класса :: имя
Уточненное-имя-класса (9.1) со следующим за ним “::” и далее именем компонена
этого класса (9.2) или компонента базового класса (10) составляют уточненноеимя; его тип есть тип компонента класса. Результатом операции является этот
компонент класса. Когда встречается имя-класса :: имя-класса или имя-класса :: ~
имя-класса, оба имени-класса должны быть именем одного и того же класса; эти
конструкции обозначают соответственно имена конструкторов (12.1) и
деструкторов (12.4).
5.2. Постфиксные выражения
Постфиксные выражения группируются слева направо.
постфиксное-выражение:
первичное-выражение
постфиксное-выражение [выражение]
постфиксное-выражение (список-выражений opt)
имя-простого-типа (список-выражений opt)
постфиксное-выражение . имя
постфиксное-выражение -> имя
постфиксное-выражение ++
постфиксное-выражение -список-выражений:
выражение-присваивания
список-выражений , выражение-присваивания
5.2.1. Индексация
20
Постфиксное выражение, за которым следует выражение в квадратных скобках,
называется индексацией (subscription). Первое выражение должно быть типа
“указатель на Т”, а второе - целочисленного типа. Тип результата есть Т.
Выражение Е1[Е2] идентично по определению *((E1)+(E2)). См. 5.3 и 5.7
относительно операций * и + и 8.2.4 - относительно массивов.
5.2.2. Обращение к функции
Вызов функции состоит из постфиксного выражения, за которым в круглых
скобках идет список выражений (фактических параметров), разделенных запятыми.
Постфиксное выражение должно иметь один из следующих типов: “функция с
результатом типа Т”, “указатель на функцию с результатом типа Т” или “ссылка на
функцию с результатом типа Т”. Результат обращения к функции имеет тип Т.
При вызове функции каждый формальный параметр инициализируется своим
фактическим параметром. Выполняютя стандартные преобразования (4) и
преобразования, определенные пользователем (12.3). Функция может изменять
значения своих неконстантных формальных параметров, но эти значения не
влияют на значения фактических параметров вне области действия фукции,
исключая случай, когда неформальный параметр является неконстантной ссылкой
(8.2.2). Значение неконстантных объектов можно также модифицировать через
параметры-указатели.
Функция может быть также вызвана с меньшим числом фактических параметров
(умолчания для параметров) или с большим (если в объявлении задано многоточие,
8.2.5), чем задано в описании функции (8.3).
Порядок вычисления параметров не определен. Порядок вычисления постфиксного
выражения и списка выражений параметров также не определен.
Подробнее механизм подстановки параметров объяснен в разделе 8.3.
5.2.3. Явное преобразование типа
Иногда бывает необходимо явно преобразовать значение одного типа в значение
другого. Имя-простого-типа (7.1.6), за которым следует список-выражений
(возможно пустой) в круглых скобках, конструирует значение указанного типа по
списку выражений. Если список содержит больше одного значения, тип должен
быть классом с соответственно объявленным конструктором (8.4, 12.1). Если тип
является классом с соответственно объявленным конструктором, будет вызван этот
конструктор, в противном случае результатом является неопределенное значение
данного типа. Пример:
class complex {
21
// ...
public:
complex(double, double);
}
f()
{
complex z = complex(1,2);
int i = int();
// i получает неопределенное значение
float r = float(1); // 1 преобразуется в 1.0
}
Таким образом, в С++ сохраняется традиционная в С запись приведения, например,
(double)x и добавляется функциональная запись, например double(x).
Функциональная запись не может применяться для типов, не имеющих простого
имени. Например, чтобы преобразовать значение к указательному типу, надо или
использовать приведение типа
char* p = (char*)0777;
или определить новое имя типа:
typedef char* Pchar;
char* p = Pchar(0777);
Когда преобразование типа не является необходимым, его лучше избегать.
Программы, в которых используется много явных преобразований типов, труднее
понимать, чем те, в которых этого нет. Явные преобразования типа, однако,
позволяют избежать многих ошибок, возможных при использовании бестипового
языка.
5.2.4. Доступ к компоненте объекта
Постфиксное выражение, сопровождаемое точкой и далее именем, образует
постфиксное выражение. Выражение должно быть объектом класса, а имя - именем
компонента этого класса. Результатом является названный компонент объекта.
Постфиксное выражение, сопровождаемое знаком “->“ и далее именем, также
образует постфиксное выражение. Выражение должно быть указателем на объект
класса, а имя - именем компонента этого класса. Результатом является названный
компонент объекта.
Понятие “объект класса” включает объекты структур (9.2) и объединений (9.5).
5.2.5. Увеличение и уменьшение
22
Операция “++”, употребленная после операнда, поставляет исходное значение
операнда и затем увеличивает его на единицу; операнд должен быть l-значением
арифметического или указательного типа.
Операция “--”, употребленная после операнда, поставляет исходное значение
операнда и затем уменьшает его на единицу; операнд должен быть l-значением
арифметического или указательного типа.
Основное назначение операций “++” и “--” заключается в пошаговом продвижении
по массиву. В следующем примере строка по указателю p, завершающяяся нулем,
копируется в область по указателю q.
while (*q++ = *p++);
5.3. Унарные операции
Выражение с унарными (префиксными) операциями группируется справа налево.
унарное-выражение:
постфиксное-выражение
++ унарное-выражение
-- унарное-выражение
унарная-операция выражение-приведения
sizeof унарное-выражение
sizeof (имя-типа)
выражение-размещения
выражение-освобождения
унарная-операция: одно из * & + - ! ~
Унарная операция “*” означает разыменование (indirection): операнд должен
вырабатывать указатель, а результат есть l-значение, именующее объект (адрес
объекта), на который указывал операнд. Если тип операнда есть “указатель на Т”,
тип результата будет Т. Пример:
char c1 = 'a';
char* p = &c1;
char c2 = *p;
// в p хранится адрес c1
// c2 = 'a'
Здесь переменная, на которую указывает р, это - с1, а значение, которое хранится в
с1, это р, поэтому присваиваемое с2 значение *р есть 'a'.
Результат унарной операции “&” (“взять адрес”) есть указатель на операнд.
Операнд должен быть именем функции, l-значением или уточненным-именем. В
первых двух случаях, если тип операнда есть Т, результат будет иметь тип
“указатель на Т”. В случае уточненного-имени, если компонент нестатический и
имеет тип Т в классе С, у результата будет тип “указатель на компонент типа Т в
23
классе С”. Для статического компонента типа Т тип результата есть просто
“указатель на Т”.
Операнд унарной операции “+” должет иметь арифметический или указательный
тип, а результат будет равен значению аргумента. Операция принадлежит истории
и, вообще говоря, бесполезна.
Операнд унарного минуса “-”должет иметь арифметический тип, а результат будет
равным операнду с обратным знаком.
Операнд операции логического отрицания “!”должет иметь арифметический или
указательный тип, а результат будет равен 1 если значение операнда равно 0, и 0,
если оно отлично от нуля. Тип результата есть int.
Операнд операции “~”должет иметь целочисленный тип; результат операции есть
побитовое дополнение операнда.
5.3.1. Операции увеличения и уменьшения
Операция “++”, употребленная перед операндом, увеличивает его на единицу и
поставляет это значение; операнд должен быть l-значением арифметического или
указательного типа. Выражение ++x эквивалентно выражению x+=1.
Пример (функция, подсчитывающая число символов в строке, не считая
завершающего нуля):
int strlen(char* p)
{
int i = 0;
while (*p++) i++;
return i;
}
Операция “--”, употребленная перед операндом, уменьшает его на единицу и
поставляет это значение; операнд должен быть l-значением арифметического или
указательного типа.
5.3.2. Операция sizeof (размер)
Результатом операции является размер ее операнда в байтах. Операнд должен быть
либо унарным выражением (которое не вычисляется), либо именем типа в скобках.
Результатом применения операции к ссылке является размер именуемого ею
объекта.
24
В применении к классу операция дает число байтов в представлении этого класса.
Результат операции имеет тип size_t - целочисленный беззнаковый тип,
определенный в стандартном файле-заголовке <stddef.h> и зависящий от
реализации.
5.3.3. Выражение размещения (операция new)
Операция new создает объект типа имя_типа (8.1), к которому она применена.
Этот тип должен быть типом объекта, т.е. функция не может быть размещена при
помощи этой операции, хотя допустимо применение операции к типу указателя на
функцию. В упрощенном виде выражение-размещения имеет следующий
синтаксис:
выражение-размещения:
:: opt new имя-типа инициализация-new opt
инициализация-new:
(список-инициализаторов opt)
Выражение-размещения обозначает следующее:
1. Отведение памяти под объект указанного типа.
2. Инициализация объекта.
3. Возвращение указателя на объект.
Время жизни объекта, созданного операцией new, не ограничено областью
действия, в которой он был создан. Когда создается массив, возвращается указатель
на его начальный объект. Например, new int и new int[10] возвращают int*.
Вызов операции new T приводит к вызову функции operator new() с аргументом
sizeof(T) для получения области памяти (12.5) достаточной для размещения объекта
типа Т.
Когда операция new создает объект, не являющийся классом, используется
глобальная функция-операция ::operator new(). Когда операция new создает объект
класса Т, используется Т::operator new(), если она существует; в противном случае
используется глобальная ::operator new(). Употребление ::new гарантирует, что
будет вызвана глобальная ::operator new(), даже если Т::operator new() существует.
В выражении-размещения может быть указана инициализация-new. Для объектов
классов с конструктором (12.1) этот список параметров будет использован в вызове
конструктора; в противном случае инициализация должна иметь вид (выражение)
или (). В первом случае выражение будет использовано для инициализации
объекта, во втором случае значение объекта остается неопределенным. Пример:
class complex {
// ...
25
public:
complex(double, double);
// ...
}
void f()
{
complex* pc = new complex(1,2);
int* pi = new int(7);
double* pd1 = new double(); // неопределенное значение
double* pd2 = new double; // неопределенное значение
}
Операция new служит для создания объектов на определенное время, пока они
нужны. В частности, во многих случаях полезно создать объект, который можно
использовать после возврата из функции, в которой он создается. Об объектах,
созданных операцией new, говорят, что они располагаются в свободной
(динамической) памяти. Такими объектами обычно бывают вершины деревьев или
элементы связанных списков, являющиеся частью большой структуры данных,
размер которой не известен на стадии компиляции.
5.3.4. Выражение-освобождения (операция delete)
Операция delete уничтожает объект, созданный операцией new.
выражение-освобождения:
:: opt delete выражение-приведения
:: opt delete [] выражение-приведения
Результат имеет тип void. Операндом должен быть указатель, ранее поставленный
операцией new. Эффект применения операции delete к указателю, не являющемуся
результатом операции new, не определен и обычно ужасен. Гарантируется, однако,
безапасность выполнения операции над нулевым указателем. Пример:
void f()
{
int I;
int* p = &I;
delete p; // ошибка
p = new int[10];
p++;
delete p; // ошибка
p = 0;
delete p; // допустимо
}
26
Чтобы освободить память, операция delete будет вызывать функцию operator
delete() (12.5). Для объекта, не являющегося объектом класса, вызывается
глобальная operator delete(). Для объекта класса Т вызывается Т::operator delete(),
если она существует; в противном случае используется глобальная ::operator
delete(). Употребление операции ::delete гарантирует вызов глобальной ::operator
delete(), даже если в классе имеется Т::operator delete().
Для освобождения массивов используется форма
delete [] выражение-приведения
где выражение-приведения указывает на массив. Последствия выполнения над
массивом операции delete без [] не определены, также как не определены
последствия уничтожения отдельного объекта операцией delete [].
Память, освобожденная операцией delete, снова может исрользоваться операцией
new. Никакого "сборщика мусора", который ищет объекты, на которые нет ссылок,
и предоставляет их в распоряжение new для повторного использования не
существует.
5.4. Явное преобразование типа
Явное преобразование типа можно записать либо в функциональном виде (5.2.3),
либо как приведение (cast):
выражение-приведения:
унарное-выражение
(имя-типа) выражение-приведения
Явное преобразование типа необходимо для указания нестандартного
преобразования значения одного типа в значение другого типа.
Любой тип, который можеь быть преобразован к другому посредством
стандартного преобразования (4), может быть преобразован явно и в том же
смысле. Примеры:
float f;
char* p;
// . . .
long ll = long(p);
int i = int(f);
// преобразует p в long
// преобразует f в int
Указатель можно явно преобразовать к любому целочисленному типу, достаточно
вместительному для этого. Значение целочисленного типа может быть
преобразовано в указатель. Указатель, преобразованный в целое и обратно должен
иметь первоначальное значение. Пример:
char* f(char* p)
27
{
int i = (int)p;
return (char*)i; // f(arg) == arg?
}
Указатель на объект одного типа может быть явно преобразован в указатель на
объект другого типа.
Указатель на объект класса B может быть явно преобразован в указатель на объект
класса D, для которого В является прямым или косвенным базовым классом, если
существует однозначное преобразование от D к В (10.1.1). Такое приведение
базового класса к производному предполагает, что объект базового класса является
подобъектом объекта производного класса; результирующий указатель будет
нацелен на объемлющий объект производного класса. Если объект базового класса
не является подобъектом объекта производного класса,
приведение может вызвать исключительную ситуацию.
5.5. Операции над указателями на компоненты класса
Операции над указателями на компонент группируются слева направо.
рт-выражение:
выражение-приведения
рт-выражение .* выражение-приведения
рт-выражение ->* выражение-приведения
Бинарная операция “.*” связывает второй операнд, который должен иметь тип
“указатель на компонент класса Т”, с первым операндом, который должен быть
объектом класса Т или класса, для которого Т есть однозначно доступный базовый
класс. Результатом является объект или функция типа, заданного вторым
операндом.
Бинарная операция “->*” связывает второй операнд, который должен иметь тип
“указатель на компонент класса Т”, с первым операндом, который должен быть
указателем на объект класса Т или класса, для которого Т есть однозначно
доступный базовый класс. Результатом является объект или функция типа,
заданного вторым операндом. Пример:
(ptr_to_obj ->* ptr_to_mfct)(10)
вызывает компонентную функцию, указываемую ptr_to_mfct, из объекта,
указываемого ptr_to_obj.
5.6. Мультипликативные операции
Мультипликативные операции группируются слева направо.
28
мультипликативное-выражение:
рт-выражение
мультипликативное-выражение * рт-выражение
мультипликативное-выражение / рт-выражение
мультипликативное-выражение % рт-выражение
Операнды операций “*” (умножение) и “/” (деление) должны иметь
арифметический тип; операнды операции “%” (остаток от деления) должны иметь
целочисленный тип. Обычные арифметические преобразования (4.5) выполняются
над операндами и определяют тип результата. При делении двух целых результат целое число, зависящее от реализации.
5.6. Аддитивные операции
Аддитивные операции группируются слева направо. Обычные арифметические
преобразования (4.5) выполняются над операндами и определяют тип результата.
аддитивное-выражение:
мультипликативное-выражение
аддитивное-выражение + мультипликативное-выражение:
аддитивное-выражение - мультипликативное-выражение:
Операнды должны быть указателями или иметь арифметический тип. Результатом
операции “+” является сумма операндов. Указатель на элемент массива можно
сложить с целым; результатом является указатель того же типа, что и исходный,
указывающий на другой элемент массива.
Результатом операции “-” является разность операндов. Значение любого
целочисленного типа можно вычесть из указателя на элемент массива. Можно
взять разность двух указателей на объекты одного и того же типа. Результатом
является целое со знаком, определяющее количество объектов, разделяющее
указываемые объекты (указатели на смежные элементы массива различаются на 1).
5.8. Операции сдвига
Операции сдвига группируются слева направо
выражение-сдвига:
аддитивное-выражение
выражение-сдвига << аддитивное-выражение
выражение-сдвига >> аддитивное-выражение
Операции проводятся над целочисленными аргументами, над которыми проводятся
целочисленные расширения. Тип результата определяется типом левого операнда.
Значением Е1 << Е2 является Е1, сдвинутое влево на Е2 битов; освобождаемые
биты заполняются нулями. Значением Е1 >> Е2 является Е1, сдвинутое вправо на
Е2 битов; освобождаемые биты заполняются нулями. Правый сдвиг будет
29
логическим, если Е1 имеет беззнаковый тип или дает неотрицательное значение; в
противном случае результат зависит от реализации.
5.9. Операции отношения
Операции отношения группируются слева направо, но надо понимать, что a<b<c
означает (a<b)<c, а не (a<b)&&(b<c).
выражение-отношения:
выражение-сдвига
выражение-отношения < выражение-сдвига
выражение-отношения > выражение-сдвига
выражение-отношения <= выражение-сдвига
выражение-отношения >= выражение-сдвига
Операнды должны иметь арифметический тип или тип указателя. Операции “<”
(меньше, чем), “>” (больше, чем), “<=” (меньше или равно) и “>=” (больше или
равно) дают результат 0, если отношение ложно, и 1, если оно истинно. Тип
результата есть int.
5.10. Операции равенства
выражение-равенства:
выражение-отношения
выражение-равенства == выражение-отношения
выражение-равенства != выражение-отношения
Операции “==“ (равно) и “!=“ (не равно) аналогичны операциям отношения, но
имеют меньший приоритет (поэтому a<b == c<d истинно всякий раз, когда a<b и
c<d оба истинны или ложны).
5.11. Побитовая операция “И”
выражение-И:
выражение-равенства
выражение-И & выражение-равенства
Операция "&" применима только к целочисленным аргументам, над которыми
может выполняться обычные арифметические преобразования; результат есть
логическое произведение операндов.
Одно из стандартных применений этой и следующих двух операций - реализация
маленького множества (вектора битов). В этом случае каждый бит беззнакового
целого представляет один член множества, а число членов ограничено числом
битов. Для именования элементов такого множества можно использовать
перечисление.
30
5.12. Побитовая операция “исключающее ИЛИ”
выражение-исключающее-ИЛИ:
выражение-И
выражение-исключающее-ИЛИ ^ выражение-И
Операция “^”применима только
к целочисленным аргументам, над которыми может
выполняться обычные арифметические преобразования; результат есть логическая
симметричная разность (exclusive OR) операндов.
5.13. Побитовая операция “включающее ИЛИ”
выражение-включающее-ИЛИ:
выражение-исключающее-ИЛИ
выражение-включающее-ИЛИ | выражение-исключающее-ИЛИ
Операция “|”применима только
к целочисленным аргументам, над которыми может
выполняться обычные арифметические преобразования; результат есть логическая
сумма (inclusive OR) операндов.
5.14. Логическая операция “И”
логическое-выражение-И:
выражение-включающее-ИЛИ
логическое-выражение-И && выражение-включающее-ИЛИ
Операция “&&” группирует операнды слева направо. Результат равен 1, если оба
операнда отличны от нуля, и 0 - в противном случае. В отличае от операции “&”,
операция “&&” гарантирует порядок вычисления операндов слева направо; более
того, второй операнд не вычисляется, если первый операнд равен 0.
Операнды не приводятся к одному типу, но должны быть указателями или
числами. Результатом является целое типа int.
5.15. Логическая операция “ИЛИ”
логическое-выражение-ИЛИ:
логическое-выражение-И
логическое-выражение-ИЛИ || логическое-выражение-И
Операция “||” группирует операнды слева направо. Результат равен 1, если один из
операндов отличен от нуля, и 0 - в противном случае. В отличае от операции “|”,
операция “||” гарантирует порядок вычисления операндов слева направо; более
того, второй операнд не вычисляется, если первый операнд отличен от нуля.
Операнды не приводятся к одному типу, но должны быть указателями или
числами. Результатом является целое типа int.
31
5.16. Условная операция
условное-выражение:
логическое-выражение-ИЛИ
логическое-выражение-ИЛИ ? выражение : условное-выражение
Условные выражения группируются слева направо. Первое выражение (логическоевыражение-ИЛИ) должно быть числом или указателем. Если оно не нуль, результатом
становится значение второго выражения, в противном случае - третьего выражения.
В случае, когда оба второе и третье выражение - числа, если они имеют один тип,
то и результат имеет тот же тип, иначе для приведения к общему типу над одним из
них выполняются обычные арифметические преобразования.
В противном случае, когда оба второе и третье выражение - указатели либо равные
нулю константные выражения, выполняются преобразования указателей для их
приведения к общему типу.
В противном случае, когда оба второе и третье выражение - ссылки, выполняются
преобразования ссылок для их приведения к общему типу.
В противном случае, когда оба второе и третье выражение имеют тип void,
результат имеет тип void.
В противном случае, когда оба второе и третье выражение имеют типом класс Т,
результат имеет типом класс Т.
В противном случае, условное выражение ошибочно.
5.17. Операция присваивания
В С++ имеется несколько операций присваивания; все они группируются справа
налево и требуют l-значения в качестве левого операнда. Типом результата
является тип левого операнда. Результатом является значение, запомненное в левом
операнде сразу после выполнения операции и рассматриваемое далее как lзначение.
выражение-присваивания:
условное-выражение
унарное-выражение операция-присваивания выражение-присваивания
операция-присваивания: ОДНА ИЗ = *= /= %= += -= >>= <<= &= ~= |=
В случае простого присваивания (=) значение правого операнда заменяет значение
объекта, именуемого левым операндом. Если оба операнда - числа, правый операнд
до присваивания преобразуется к типу левого операнда. Если левый операнд 32
указатель, правый операнд тоже должен быть указателем или константным
выражением, равным нулю; правый операнд до присваивания преобразуется к типу
левого операнда.
Выражение вида Е1 op= Е2 эквивалентно выражению Е1 = Е1 op Е2, за
исключением того, что Е1 вычисляется только один раз.
5.18. Операция запятая
Операция группирует вычисления слева направо.
выражение:
выражение-присваивания
выражение , выражение-присваивания
Пара выражений, разделенных запятой, вычисляется слева направо и значение
левого операнда игнорируется. Все побочные эффекты левого выражения
проявляются до вычисления правого. Результат является l-значением, если таковым
является правое выражение.
В контексте, где запятая имеет определенный смысл, например, в списке
фактических параметров вызова функции (5.2.2) или в списке инициализации (8.4),
запятая как знак операции может появиться только в круглых скобках; например:
f(a, (t=3, t+2), c);
представляет вызов функции с тремя параметрами, второй из которых имеет
значение 5.
5.19. Константные выражения
В следующих случаях С++ требует, чтобы значение выражения было
целочисленной константой: граница массива (8.2.4), выражение в метке case (6.4.2),
длина битового поля (9.6) и значение, инициализирующее элемент перечисления
(7.2).
константное-выражение:
условное-выражение
константное-выражение может включать только литералы (2.5), элементы
перечислеия, константные значения целочисленного типа, инициализированные
константным выражением (8.4).
5.20. Сводка операций
В следующей сводке после каждой операции приведено ее название и пример
использования. В этих примерах class_name это имя класса, member - имя
компонента, object - выражение, дающее в результате объект класса, pointer -
33
выражение, дающее в результате указатель, expr - выражение, а lvalue - выражение,
дающее неконстантный объект. Type может быть произвольным именем типа,
только когда он стоит с скобках, во всех остальных случаях существуют
ограничения. Унарные операции и операции присваивания правоассоциативны, все
остальные - левоассоциативны. Это значит, что a = b = c означает a = (b = c), a+b+c
означает (a+b)+c, и *p++ означает *(p++), а не (*p)++.
------------------------------------------------------------------------------------------------::
разрешение области видимости
class_name::member
::
глобальное
::name
------------------------------------------------------------------------------------------------.
выбор компонента
object.member
->
выбор компонента
pointer->member
[]
индексация
pointer[expr]
()
вызов функции
expr(expr-list)
()
построение значения
type(expr-list)
sizeof
размер объекта
sizeof expr
sizeof
размер типа
sizeof(type)
-----------------------------------------------------------------------------------------++
приращение после
lvalue++
++
приращение до
++lvalue
-уменьшение после
lvalue--уменьшение до
--lvalue
~
дополнение
~expr
!
не
!expr
унарный минус
-expr
+
унарный плюс
+expr
&
адрес объекта
&lvalue
*
разыменование
*expr
new
создание (размещение)
new type
delete
уничтожение (освобождение)
delete pointer
()
приведение (преобразование типа)
(type)expr
-----------------------------------------------------------------------------------------.*
выбор компонента
object.*pointer-to-member
->*
выбор компонента
pointer.->*pointer-to-member
------------------------------------------------------------------------------------------*
умножение
expr*expr
/
деление
expr/expr
%
взятие по модулю
expr%expr
------------------------------------------------------------------------------------------+
сложение (плюс)
expr+expr
вычитание (минус)
expr-expr
-------------------------------------------------------------------------------------------
34
В каждой очерченной части находятся операции с одинаковым приоритетом.
Операция, расположенная в более высокой части, имеет приоритет больший, чем
операция, расположенная в более низкой части.
-------------------------------------------------------------------------------------------<<
сдвиг влево
expr << expr
>>
сдвиг вправо
expr >> expr
-------------------------------------------------------------------------------------------<
меньше
expr < expr
>
больше
expr > expr
<=
меньше или равно
expr <= expr
>=
больше или равно
expr >= expr
-------------------------------------------------------------------------------------------==
равно
expr == expr
!=
не равно
expr != expr
--------------------------------------------------------------------------------------------&
побитовое И
expr & expr
--------------------------------------------------------------------------------------------^
побитовое исключающее ИЛИ
expr ^ expr
--------------------------------------------------------------------------------------------|
побитовое включающее ИЛИ
expr | expr
--------------------------------------------------------------------------------------------&&
логическое И
expr && expr
--------------------------------------------------------------------------------------------||
логическое включающее ИЛИ
expr || expr
--------------------------------------------------------------------------------------------?:
арифметический IF
expr ? expr : expr
--------------------------------------------------------------------------------------------=
простое присваивание
lvalue = expr
*=
умножить и присвоить
lvalue *= expr
/=
разделить и присвоить
lvalue /= expr
%=
взять по модулю и присвоить
lvalue %= expr
+=
сложить и присвоить
lvalue += expr
-=
вычесть и присвоить
lvalue -= expr
<<=
сдвинуть влево и присвоить
lvalue <<= expr
>>=
сдвинуть вправо и присвоить
lvalue >>= expr
&=
И и присвоить
lvalue &= expr
|=
включающее ИЛИ и присвоить
lvalue |= expr
^=
исключающее ИЛИ и присвоить
lvalue |= expr
--------------------------------------------------------------------------------------------,
запятая
expr, expr
---------------------------------------------------------------------------------------------
35
6. Операторы
Операторы выполняются последовательно, если явно не указано обратное.
оператор:
помеченный-оператор
оператор-выражение
составной-оператор
выбирающий-оператор
оператор-цикла
оператор-перехода
оператор-объявление
6.1. Помеченный оператор
помеченный-оператор:
идентификатор : оператор
case константное-выражение : оператор
default : оператор
Идентификатор объявляет идентификатор метки, который может употребляться в
операторе перехода. Областью действия метки является функция, в которой она
объявлена. Метка может быть использована до ее объявления. Метки образуют
свое собственное пространство имен и не конфликтуют с другими
идентификаторами. Метки case и default могут появляться только в операторе
выбора.
6.2. Оператор-выражение
Большинство операторов в программе обычно являются операторамивыражениями, которые имееют вид:
оператор-выражение:
выражение opt;
Чаще всего операторы-выражения представляют собой присваивания и вызовы
функций. Все побочные выражения проявляются до выполнения следующего
оператора. Оператор-выражение с опущенным выражением называется пустым
оператором.
6.3. Составной оператор или блок
Составной оператор используется для задания нескольких операторов там, где по
синтаксису ожидается один.
составной- оператор:
{ список-операторов opt }
список-операторов:
36
оператор
список-операторов оператор
Заметим, что объявление тоже считается оператором (6.7).
6.4. Выбирающие операторы
Выбирающие ператоры выбирают один из нескольких путей исполнения
программы.
выбирающий-оператор:
if (выражение) оператор
if (выражение) оператор else оператор
switch (выражение) оператор
Оператор в выбирающем операторе не может быть объявлением.
6.4.1. Условный оператор (оператор if)
Значение выражения должно быть числом, указателем или объектом класса, для
которого существует однозначное преобразование к числу или указателю (12.3).
В С++ нет отдельного булевского типа. Операции сравнения
"==", "!=", "<", ">", "<=", ">="
возвращают целое 1, если сравнение истинно, иначе возвращают 0. Поэтому
выражение вычисляется и, если результат не равен нулю, выполняется первый
оператор. В противном случае, если есть часть else, выполняется ее оператор. Из
этого следует, что if (a) эквивалентно if (a != 0).
Логические операции "&&" и "||" наиболее часто исрользуются в условиях. Они не
вычисляют второй операнд, если этого не требуется. Например,
if (p && 1 < p->count)
сначала проверит, является ли р нулем, и только если это не так, проверит
1 < p->count.
Некоторые условные операторы могут быть с удобством заменены условными
выражениями, например:
if (a <= b)
max = b
else max = a:
лучше выражается так:
37
max = (a <= b) ? b : a;
6.4.2. Оператор выбора (оператор switch)
Оператор выбора передает управление одному из нескольких операторов в
зависимости от значения выражения, которое должно быть числом или объектом
класса, для которого существует однозначное преобразование к числу (12.3).
Любой оператор может быть помечен меткой case:
case константное-выражение:
где константное-выражение (5.19) приводится к типу выражения из оператора
выбора.
В одном операторе выбора может быть одна метка вида default
Выполнение оператора заключается в вычислении выражения и сравнении его
значения с метками выбора. Если одна из меток равна значению выражения,
управление передается оператору, следующему за меткой. Если ни одна из меток
выбора не равна значению выражения, управление передается оператору,
помеченному меткой default, если есть таковой. Пример:
switch (val) {
case 1: f(); break;
case 2: g(); break;
default: h(); break;
}
Необходимо заботиться о завершении каждой ветви, чтобы избежать выполнения
следующей. Например,
switch (val) {
// осторожно!
case 1: cout << "case1 \n";
case 2: cout << "case2 \n";
default: cout << "default: case не найден \n;
}
при val = 1 напечатает
case1
case2
default: case не найден
6.5. Операторы цикла
Операторы цикла задают многократное исполнение.
38
оператор-цикла:
while (выражение) оператор
do оператор while (выражение)
for (оператор-иниц-for выражение opt; выражение opt) оператор
оператор-иниц-for:
оператор-выражение
оператор-объявление
Оператор в операторе-цикла не может быть объявлением.
6.5.1. Оператор ПОКА (while)
В операторе ПОКА входящий в него оператор повторно выполняется до тех пор,
пока значение выражения не становится равным нулю. Проверка производится
перед каждым исполнением оператора. Значение выражения должно быть числом,
указателем или объектом класса, для которого существует однозначное
преобразование к числу или указателю (12.3).
6.5.2. Оператор ПОВТОРИТЬ (do)
В операторе ПОВТОРИТЬ входящий в него оператор повторно выполняется до
тех пор, пока значение выражения не становится равным нулю. Проверка
производится после каждого исполнения оператора. Значение выражения должно
быть числом, указателем или объектом класса, для которого существует
однозначное преобразование к числу или указателю (12.3).
6.5.3. Оператор итерации (for)
Оператор итерации
for (оператор-иниц-for выражение-1 opt; выражение-2 opt) оператор
эквивалентен оператору
оператор-иниц-for
while (выражение-1) {
оператор
выражение-2;
}
за исключением того, что оператор ПРОДОЛЖИТЬ (continue) в операторе
приводит к выполнению выражения-2 и далее очередному вычислению
выражения-1. Таким образом:
первый оператор задает инициализацию, необходимую для выполнения цикла,
39
первое выражение определяет проверку, выполняемую перед каждым
повторением, так что цикл завершается, когда выражение-1 становится
равным нулю,
второе выражение обычно осуществляет приращение переменных цикла после
каждого повторения.
Значение выражения-1 должно быть числом, указателем или объектом класса, для
которого существует однозначное преобразование к числу или указателю (12.3).
Пропуск выражения-1 подразумевает, что фраза while в эквивалентном
представлении сводится к while(1). Пример:
const int sz = 24;
int ia[sz];
for (int i = 0; i<sz; ++i) ia[i] = i;
Если оператор-иниц-for является объявлением, область действия объявленных
имен простирается до конца блока, содержащего оператор for.
6.6. Операторы перехода
Оператор перехода передает управление безусловно.
оператор-перехода:
break
continue
return выражение opt
goto идентификатор
При выходе из области действия вызываются деструкторы (12.4) для всех
сконструируемых в области действия объектов, которые еще не были уничтожены
к этому моменту. Это относится как к явно объявленным, так и временным
объектам (12.2).
6.6.1. Оператор завешения (break)
Оператор может употребляться только в операторе цикла или операторе выбора и
приводит к завершению наименьшего объемлющего оператора цикла или
оператора выбора; управление передается оператору, следующему за завершенным.
Пример:
switch (val) {
case 1: f(); break;
case 2: g(); break;
default: h(); break;
40
}
6.6.2. Оператор продолжения (continue)
Оператор может употребляться только в операторе цикла и приводит к передаче
управления на продолжающуюся часть цикла, т.е. на конец наименьшего
повторяемого оператора. Более точно, в каждом из операторов
while (foo) {
// ...
contin: ;
}
do {
// ...
contin: ;
} while (foo)
for (;;) {
// ...
contin: ;]
}
оператор continue, если он не содержится во вложенном операторе цикла,
эквивалентен goto contin. Пример:
while (cin) {
// . . .
if (cur_tok == PRINT) continue;
cout << expr() << '\n';
}
эквивалентно
while (cin) {
// . . .
if (cur_tok == PRINT) goto end_of-loop;
cout << expr() << '\n';
end_of-loop: ;
}
6.6.3. Оператор возврата (return)
Функция возвращает управление в точку вызова посредством оператора return.
Оператор return без выражения можно употреблять только в функциях,
возвращающих значение типа void, в конструкторах (12.1) и деструкторах (12.4).
Оператор return с выражением можно употреблять только в функциях,
возвращающих значение не типа void; значение передается в точку вызова; если
необходимо, оно преобразуется к требуемому функцией типу.
6.6.4. Оператор перехода (goto)
Оператор передает управление оператору, помеченному указанным
идентификатором. В программировании высокого уровня оператор имеет мало
применений и его следует избегать. Одно из немногих разумных его применений
состоит в выходе из вложенного цикла или переключателя (break прекращает
41
выполнение только самого внутреннего охватывающего его цикла или
переключателя). Пример:
void f()
{
int i;
int j;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (nm[i][j] == a) goto found; // найдено
// не найдено
// . . .
found: // найдено
// nm[i][j] == a
}
6.7. Оператор-объявление
Оператор-объявление вводит в текущем блоке новый идентификатор.
оператор-объявление:
объявление
Если идентификатор, введенный объявлением, был ранее объявлен во внешнем
блоке, внешнее объявление скрывается в оставшейся части блока и
восстанавливается при выходе из него.
6.8. Разрешение неоднозначностей
Существует неоднозначность грамматики при выборе между операторомвыражением и объявлением. Оператор-выражение с явным приведением типа в
функциональной записи (5.2.3) как самым левым подвыражением может оказаться
неотличимым от объявления, первый описатель которого начинается с “(“. В этом
случае оператор считается объявлением.
Чтобы снять неоднозначность, приходится иногда анализировать весь оператор.
Это решает проблему во многих случаях. Предположим, например, что Т является
именем-простого-типа (7.1.6), тогда:
T(a) -> m = 7;
T(a)++
T(a, 5) << c;
// оператор-выражение
// оператор-выражение
// оператор-выражение
T(*e) (int);
T(f)[]
T(g) = {1, 2};
// объявление
// объявление
// объявление
42
T(*d)(double(3));
// объявление
Прочие случаи являются объявлениями, например:
T(a);
T(*b)();
T(c) = 7;
T(d), e, f = 3;
T(g)(h, 2);
// объявление
// объявление
// объявление
// объявление
// объявление
43
7. Объявления
Объявления определяют смысл каждого идентификатора, но не всегда отводят
память под него.
объявление:
decl-спецификаторы opt список-описателей opt;
asm-объявление
описание-функции
объявление-шаблона
спецификация-сборки
Описатели, составляющие список-описателей (8), содержат объявляемые
идентификаторы. Decl-спецификаторы могут быть опущены только в описании
(8.3) и объявлениии функции. Список-описателей может быть пуст только в
объявлении класса (9) или перечисления (7.2), т.е. когда decl-спецификатор
является спецификатором-класса или спецификатором-перечисления. Объявление
имеет силу только в своей области действия (3.2). Примеры объявлений:
char ch;
int count = 1;
char* name = "Alex"
struct complex {float re, im};
complex cvar;
extern complex sqrt(complex);
extern int error-number;
typedef complex point;
float real (complex* p) {return p->re};
const double pi = 3.14159256358979932385;
struct user;
templete<class T> abs(T a) {return a<0 ? -a : a;};
enum beer {Carlsberg, Tuborg, Thor};
Как можно видеть из примеров, объявление может делать больше, чем
ассоциировать тип с именем. Большинство определений являются также
описаниями (определениями), т.е. они описывают сущность объявляемого имени.
Для имен ch, count и cvar этой сущностью является соответсвующий объем памяти,
используемый для хранения значения. Для имени real такой сущностью будет
заданная функция. Для константы pi - это значение 3.14159256358979932385. Для
имени complex - это новый тип. Наконец, для имени point - это тип complex, поэтому
point становится синонимом complex. В рассматриваемом списке только объявления
extern complex sqrt(complex);
extern int error-number;
struct user;
44
не являются одновременно описаниями. Это означает, что объекты, которые они
объявляют, будут описаны где-то еще. Тело функции sqrt должно задаваться неким
другим объявлением, память для переменной error-number типа int должна
выделяться неким другим объявлением, и некоторое другое объявление типа user
должно описывать, что он из себя представляет.
Некоторые объявления задают "значения" для тех сущностей, которые они вводят:
int count = 1;
char* name = "Alex"
struct complex {float re, im};
typedef complex point;
float real (complex* p) {return p->re};
const double pi = 3.14159256358979932385;
Для типов, функций и констант "значение" неизменно, значение переменной может
быть впоследствии изменено. Всякое объявление, задающее значение, является
описанием.
7.1. Спецификаторы
decl-спецификаторы:
decl-спецификаторы opt decl-спецификатор
decl-спецификатор:
спецификатор-класса-памяти
спецификатор-типа
fct-спецификатор
template
friend
typedef
7.1.1. Спецификаторы класса памяти
спецификатор-класса-памяти:
auto
register
static
extern
Спецификаторы auto и register применимы только к именам объектов, объявленным
в блоке (6.3) и формальным параметрам (8.3). Оба объявления вводят
автоматический объект, который размещается каждый раз при входе в его блок и
существует только до тех пор, пока из этого блока не вышли. Описатель auto почти
всегда избыточен, одно из его полезных применений - задать явное отличие
оператора-объявления от оператора-выражения (6.2). Пример: Компилятор знает,
что
int (*p)[5]
45
является объявлением указателя на массив из пяти целых элементов, а украшение
этого объявления спецификатором auto может лишь помочь восприятию читателя:
auto int (*p)[5]
Объявление register есть объявление auto, подсказывающее компилятору, что
объявляемая переменная будет интенсивно использоваться.
Объявление объекта является описанием, если оно не содержит спецификатор
extern или содержит инициализацию (3.1).
Описание вызывает отведение соответствующего количества памяти и
соответствующую инициализацию (8.4).
Спецификаторы static и extern применимы только к именам объектов и функций.
Имя, описанное как static имеет внутреннее связывание и живет только до конца
программы, оно инициализируется только один раз, когда поток управления
первый раз проходит через ее обявление. Пример:
int a = 1;
void f()
{
int b = 1;
static int c = a;
...
};
// инициализируется при каждом вызове f()
// инициализируется только один раз
int main()
{
while (a < 4) f();
}
Static-переменная без указанной инициализации неявно инициализируется нулем.
Когда переменные и функции явно объявлены как static на уровне файла,
соответствующий фрагмент программы легче понимать (некуда больше
заглядывать). Использование static для функций может, помимо этого, выгодно
влиять на накладные расходы по вызову функции, поскольку задает компилятору
более простую работу.
Имя, описанное как extern имеет внешнее связывание, кроме случая, когда оно
ранее получило внутренне связывание. Примеры объявлений внешних имен:
extern complex sqrt(complex);
extern int error-number;
46
Имя, объявленное на уровне файла без спецификатора-класса-памяти, имеет
внешнее связывание, если ему ранее не было придано внутреннее связывание или
оно не объявлено как const.
Все объявления одного имени должны быть согласованы с типом объекта, к
которому они относятся. Примеры:
static char* f();
char*f()
{ /* ... */}
// f() имеет внутреннее связывание
// f() все равно имеет внутреннее связывание
char* g();
static char* g()
{ /* ... */}
// g() имеет внешнее связывание
// ошибка: не то связывание
static int a;
int a;
// “а” имеет внутреннее связывание
// ошибка: два описания
static int b;
extern int b
// “b” имеет внутреннее связывание
// все равно внутреннее связывание
int c;
static int c;
// “с” имеет внешнее связывание
// ошибка: несоответствие связывания
extern int d;
static int d;
// “d” имеет внешнее связывание
// ошибка: несоответствие связывания
7.1.2. Спецификаторы функции
Некоторые спецификаторы можно употреблять только в объявлении функции.
fct-спецификатор:
inline
virtual
Спецификатор inline подсказывает компилятору, что открытая подстановка тела
функции предпочтительнее обычной реализации вызова функции. Идентификатору
функции при этом придается внутреннее связывание.
Спецификатор virtual можно употреблять только в объявлениях нестатических
компонентных функций внутри объявления класса (10.2).
7.1.3. Описание имени типа
47
Объявления, содержащие typedef, вводят идентификаторы, используемые для
именования основных или порожденных типов. Спецификатор typedef не может
использоваться в описании-функции (8.3).
описанное-имя-типа:
идентификатор
Пример:
typedef complex point;
Здесь complex и point - описанные имена типов.
Внутри области действия объявления имени типа каждый идентификатор,
являющийся частью описателя, становится синтаксически эквивалентным
ключевому слову и именует тип, ассоциированный с этим идентификатором,
способом, описанным в 8. Описанное-имя-типа становится таким образом
синонимом другого типа. Оно не вводит новый тип в том смысле, в каком это
делает объявление класса (9.1). Например, после объявления
typedef int MILES, *KLICKSP;
конструкции
MILES distance;
KLICKSP metricp;
верны; при этом distance имеет тип int, а metricp является указателем на int.
Описание имени типа не может переопределять имя типа, объявленное в этой же
области действия, но именующее другой тип, например:
class complex { /* . . . */ }
typedef int complex;
// ошибка: повторное описание
Описанное-имя-типа, именующее класс, является именем класса.
7.1.4. Спецификатор template
Спецификатор template применяется для определения семейств типов или функций
(14).
7.1.5. Спецификатор friend
Спецификатор friend применяется дя спецификации доступа к компонентам класса
(11.4).
7.1.6. Спецификаторы типа
48
спецификатор-типа:
имя-простого-типа
спецификатор-класса
спецификатор-перечисления
уточненный-спецификатор-типа
const
volatile
Ключевые слова const и volatile можно добавлять к любому правильному
спецификатору-типа в объявлении объекта. Из числа других в объявление можно
включать не более одного спецификатора типа.
Константный объект не может быть впоследствии изменен. Если он не объявлен
явно как extern, он должен быть инициализирован и не имеет внешнего
связывания. Примеры:
const int model = 90;
const int v[] = {1, 2, 3, 4};
Каждый элемент константного массива является константой. Каждый
нефункциональный нестатический компонент константного класса также является
константой (9.3.1).
Спецификатор const изменяет тип, т.е. ограничивает способ использования объекта.
Поэтому, например, разумно и полезно описывать функцию, как возвращающую
const. Пример:
const char* peek(int i)
{
return v[i];
}
// возврат указателя на константу
У подвижных (volatile) объектов нет семантики, не зависящей от реализации;
спецификатор volatile подсказывает компилятору, что тот должен избегать
оптимизации действий над этим объектом, поскольку его значение может быть
изменено скрытно от компилятора. Каждый элемент подвижного массива является
подвижным. Каждый нефункциональный нестатический компонент подвижного
класса также является подвижным (9.3.1).
Если спецификатор-типа в объявлении отсутствует, берется int.
имя-простого-типа:
полное-имя-класса
уточненое-имя-типа
char
short
int
49
long
signed
unsigned
float
double
void
Только один из спецификаторов short и long может быть скомбинирован с int.
Каждый из них может появиться и сам по себе, в этом случае int подразумевается.
Слово long может сочетаться с double. Одно из слов signed и unsigned можно
комбинировать с char, short, int или long. Опять, каждый из них может появиться и
сам по себе, в этом случае int подразумевается.
Спецификаторы-класса и спецификаторы-перечисления обсуждаются
соответственно в 9 и 7.2.
уточненный-спецификатор-типа:
ключевое-слово-класса имя-класса
ключевое-слово-класса идентификатор
enum имя-перечисления
ключевое-слово-класса:
сlass
struct
union
Если задан идентификатор, уточненный-спецификатор-типа объявляет его
именем-класса (9.1).
Имя, объявленное со спецификатором union, должно быть и описано как
объединение. Имя, объявленное со спецификатором сlass, должно быть описано
также со спецификатором сlass или struct. Имя, объявленное со спецификатором
struct, должно быть описано также со спецификатором сlass или struct.
Имена вложенных типов (9.7) могут быть уточнены именем объемлещего класса:
уточненое-имя-типа:
описанное-имя-типа
имя-класса :: уточненое-имя-типа
полное-имя-класса:
уточненное-имя-класса
:: уточненное-имя-класса
уточненное-имя-класса:
имя-класса
имя-класса :: уточненное-имя-класса
50
Имя, уточненное именем-класса, должно быть типом, описанным в этом классе или
в его базовом классе. Как обычно, имя, объявленное в производном классе,
скрывает одноименные компоненты, описанные в базовых классах (3.2).
7.2. Объявление перечисления
Перечисление рассматривается как отдельный целочисленный тип (3.6.1) с
поименованными константами. Его имя становится именем-перечисления, т.е.
зарезервированным словом в своей области действия.
имя-перечисления:
идентификатор
спецификатор-перечисления:
enum идентификатор opt {список-перечисления opt}
список-перечисления:
элемент-перечисления
список-перечисления , элемент-перечисления
элемент-перечисления:
идентификатор
идентификатор = константное-выражение
Идентификаторы в списке-перечисления рассматриваются как константы и могут
употребляться везде, где требуются константы. Если нет ни одного элементаперечисления со знаком “=“, то значения соответствующих констант растут на
единицу, начиная с нуля, при движении слева направо. Элемент-перечисления со
знаком “=“ придает идентификатору указанное значение; последующие
идентификаторы без “=“ продолжают прогрессию от присвоенного значения. Это
значение должно быть целым или приводимым к целому посредством
целочисленного расширения (4.1).
Имена элементов перечисления должны быть отличны от имен обычных
переменных и других элементов перечисления с той же областью действия.
Примеры:
enum color {red, orange, yellow, green, blue};
enum fruit {
apple,
pear,
orange,
kiwi };
// ошибка: повторное описание orange”
enum bird {
emu,
dodo,
51
ostrich,
kiwi
};
int emu;
// ошибка: повторное описание kiwi
// ошибка: повторное описание emu
Значения элементов перечисления не обязаны быть различными. Значение
элемента перечисления считается определенным сразу за его инициализатором.
Например:
enum { a, b, c = 0 };
enum { d, e, f = e+2 };
определяет, что a, c и d равны 0, b и t равны 1, f равно 3.
Фактически перечисление определяет несколько целочисленных констант, которые
могли бы быть определены и явным образом. Например,
enum { a, b, c = 0 };
эквивалентно
const int a = 0;
const int b = 1;
const int c = 0;
Каждое описание перечисления определяет новый целочисленный тип, отличный
от всех других целочисленных типов. Значение элемента перечисления может быть
преобразовано в целое при целочисленном расширении (4.1). Например:
enum color { red, yellow, green = 20, blue};
color col = red;
color* cp = &col;
if (*cp == blue) // . . .
Здесь возможными значениями объекта типа color являются red, yellow, green, blue;
эти значения могут быть преобразованы в целые значения 0, 1, 20, 21. Однако,
поскольку перечисления являются отдельными типами, объекту типа color может
быть присвоено значение только типа color. Например:
color c = 1
int i = yellow;
// ошибка: несоответствие типов,
// нет преобразования от int к color
// все верно: yellow преобразуется в целое 1
// посредством целочисленного расширения
52
Для преобразования целого в элемент перечисления необходимо указывать явное
преобразование, например:
color c2 = color(1).
7.3. ASM объявления
Ассемблерное объявление имеет вид:
asm-объявление:
asm (литеральная-строка);
Семантика такого объявления определяется реализацией. Обычно оно служит для
передачи информации через компилятор ассемблеру.
7.4. Спецификация сборки
Сборка (компоновка, редакция связей - linkage) фрагментов кода С++ с
фрагментами на других языках программирования осуществляется посредством
спецификации-сборки:
спецификация-сборки:
extern литеральная-строка { список-объявлений opt }
extern литеральная-строка объявление
список-объявлений:
объявление
список-объявлений объявление
Литеральная-строка задает нужный способ сборки; ее смысл определяется
реализацией.
53
8. Описатели
Описатель объявляет объект, функцию или тип внутри некоторого объявления.
Список-описателей, употребляемый в объявлении - это разделенная запятыми
последовательность описателей, каждый из которых может включать
инициализатор.
список-описателей:
описатель-с-инициализацией
список-описателей, описатель-с-инициализацией
описатель-с-инициализацией:
описатель инициализатор opt
Двумя составляющими объявления (7.1) являются спецификаторы и описатели.
Спецификаторы задают основной тип, класс памяти и другие характеристики
объявляемых объектов и функций. Описатели определяют имена этих объектов и
функций и, возможно, модифицируют их тип посредством таких операций как
“*”(указатель на) и () (функция). Описатель может также задавать начальные
значения (8.4, 12.6).
описатель:
d-имя
prt-операция описатель
описатель (список-объявлений-параметров) список-cv-описателей opt
описатель [константное-выражение opt]
(описатель)
prt-операция:
* список-cv-описателей opt
& список-cv-описателей opt
полное-имя-класса :: * список-cv-описателей opt
список-cv-описателей:
cv-описатель список-cv-описателей opt
cv-описатель:
const
volatile
d-имя:
имя
имя-класса
~ имя-класса
описанное-имя-типа
уточненное-имя-типа
Имя класса имеет особый смысл при объявлении класса с этим именем: а также при
уточнении этим именем, т.е. в позиции левого операнда операции разрешения
области действия “::” (12.1, 12.4).
54
8.1. Имена типов
Чтобы явно задать приведение значения или аргумент операций sizeof и new,
нужно уметь назвать тип. Эту задачу выполняет имя-типа, которое синтаксически
представляет собой как бы объявление объекта или функции этого типа без
указания имени этого объекта или функции.
имя-типа:
список-спецификаторов-типа абстрактный-описатель opt
список-спецификаторов-типа:
спецификатор-типа список-спецификаторов-типа opt
абстрактный-описатель:
prt-операция абстрактный-описатель opt
абстрактный-описатель opt (список-объявлений-параметров) список-cv-описателей opt
абстрактный-описатель opt [константное-выражение opt]
(абстрактный-описатель)
Можно однозначно определить то место в абстрактном-описателе, где появился
бы идентификатор, если бы конструкция была описателем в объявлении. Имя-типа
именует тот самый тип, который был бы у гипотетического идентификатора.
Например:
int
int *
int *[3]
int (*)[3]
int*()
int (*)(double)
// int i
// int *pi
// int *p[3]
// int (*p3i)[3]
// int *f()
// int (*pf)(double)
имеют соответственно типы “целое”, “указатель на целое”, “массив из трех
указателей на целое”, “указатель на массив из трех целых”, “функция без
параметров, возвращающая указатель на целое” и “указатель на функцию с
параметром типа double и результатом целого типа”.
8.1.1. Разрешение неоднозначности
Упомянутая в 6.8 неоднозначность, которая возникает из-за синтаксической
схожести явного приведения типа в функциональной записи и объявления, может
появляться и в контексте объявления. В этом контексте она возникает как выбор
между объявлением функции с лишней парой скобок при имени параметра и
объявлением объекта с функциональным стилем записи приведения в
инициализации. Так же как и для оператора, разрешение неоднозначности
закдючается в том, чтбы считать объявлением любую конструкцию, которая может
им быть.
55
8.2. Смысл описателей
Список описателей появляется после списка (возможно, пустого) declспецификаторов (7.1). Каждый описатель содержит ровно одно d-имя; оно
определяет объявляемый идентификатор. За исключением некоторах специальных
функций (12.3, 12.4) d-имя является просто идентификатором. Спецификаторы
auto, static, extern, register, friend, inline, virtual, typedef применяются к каждому dимени в списке-описателей; тип каждого d-имени зависит как от declспецификаторов (7.1), так и от его описателя.
Таким образом, объявление конкретного идентификатора имеет вид:
ТD
где Т - тип, а D - описатель.
В объявлении, где D - просто идентификатор, T является его типом.
В объвлении, где D имеет вид (D1), тип D1 тот же самый, что и у D. Скобки не
изменяют тип заключенного в них d-имени, но они могут изменить порядок
конструирования типа в сложных описателях.
По всеобщему мнению, синтаксис описателей в С и С++ очень сложен для их
записи и чтения. Поэтому рекомендуется использовать описание типа посредством
typedef. Например:
typedef char* F(int);
typedef F* A[10];
A* p;
//F есть тип функции с параметром типа int
// и результатом типа указатель на char
// А - тип массива из 10 указателей на F
// p - указатель на ужасно сложный тип,
// описанный выше
8.2.1. Указатели
В объявлении Т D, где D имеет вид
* список-cv-описателей opt D1,
тип содержащегося в объявлении идентификатора есть “ ... список-cv-описателей
указатель на Т”. То есть, cv-описатели относятся к указателю, а не к объекту, на
которай он указывает. В переменной типа Т* хранится адрес объекта типа Т.
Например, объявления
const ci = 10, *pc = &ci, *const cpc = pc;
int i, *p, *const cp = &i;
объявляют:
56
1) ci как целую константу,
2) pc как указатель на целую константу,
3) cpc как константный указатель на целую константу,
4) i как целую переменную,
5) p как указатель на целую переменную,
6) cp как константный указатель на целое.
Значения ci, cp, cpc не могут быть изьенены после инициализации. Значение pc
может быть изменено, как и значение объекта, указываемого cp. Примеры
допустимых действий:
i = ci;
*cp = ci;
pc++;
pc = cpc;
pc = p;
Примеры недопустимых действий:
ci = 1;
ci++;
*pc = 2;
cp = &ci;
cpc++;
p = pc;
Наличие типа константы const позволяет описать четыре варианта типов
указателей. Примеры:
char c, c1;
const char d = 'x';
char* pv;
pv = &c;
*pv = 'z'
// переменный указатель на переменную
// OK
// OK
const char* pc;
pc = &d
*pc = 'z'
// переменный указатель на константу
// OK
// ошибка: присваивание константе
char *const cp = &c;
cp = &c1;
*cp = 'y';
// константный указатель на переменную
// ошибка: присваивание константе
// OK
const char *const cpc = 'b'; // константный указатель на константу
57
cpc = &d;
*cpc = 'a'
// ошибка: присваивание константе
// ошибка: присваивание константе
Указателю на константу можно присваивать адрес переменной, потому что вреда
от этого быть не может. Например:
pc = &c;
// OK
Это, в частности, полезно при описании параметров функции для запрещения
модификации аргумента, например:
void strcopy(char* p, const char* q) // не может изменять q
Нельзя, однако, присвоить адрес константы указателю на переменную ибо это
позволило бы изменить значение константы, например:
pv = &d
// ошибка
Для указателей на массивы и функции приходится пользоваться более сложной
записью, например:
int (*vp)[10];
int (*fp)(char, char*);
// указатель на массив из 10 целых
// указатель на функцию типа int(char, char*)
8.2.2. Ссылки
В объявлении T D, где D имеет вид
& список-cv-описателей opt D1
тип идентификатора в объявлении есть “... список-cv-описателей ссылка на Т”. Тип
void запрещен.
Ссылка является другим именем объекта. Главное применение ссылок состоит в
описании параметров и возврате значений для функций вообще и для одинаково
названных операций в частности. Примеры:
int i = 1;
int& r = i;
// r и i ссылются теперь на один int
int x = r;
// x = 1
r = 2;
// i =2
void f(double& a) {a += 3.14;}
// параметр - ссылка
// ...
double d = 0;
f(d);
// прибавление 3.14 к d
58
Ссылка обязательно должна быть инициализирована (должно быть что-то, для чего
она является именем - 8.4.3). Ни одна операция ссылку не меняет. Пример:
int ii = 0;
int& rr = ii;
rr++;
// ii увеличивается на 1
Чтобы получить указатель на объект помеченный ссылкой rr, можно написать rr&,
что эквивалентно ii&.
Не существует ссылки на ссылки, ссылок на битовые поля (9.6), массивов ссылок и
указателей на ссылки.
Ссылки на константы играют важную роль в качестве параметров функций (5.2.2).
8.2.3. Указатели на компоненты класса
В объявлении T D, где D имеет вид
имя класса :: * список-cv-описателей opt D1
тип идентификатора в объявлении есть “... список-cv-описателей указатель на
компонент класса имя-класса типа Т”. Например,
class X {
public:
void f(int);
int a;
};
int X ::* pmi = &X::a;
void (X::* pmf)(int) = &X::f;
объявляет pmi и pmf указателями на компоненты класса Х типа int и void(int),
соответственно. Их можно использовать следующим образом:
X obj;
// ...
obj.*pmi = 7;
(obj.*pmf)(7);
// присваивает 7 компоненту класса типа int
// вызов компонентной функции с параметром 7
Указатель на компонент класса не может указывать на статический компонент
класса (9.4).
Указатели на компоненты класса могут быть полезны в качестве компонентов
массивов.
8.2.4. Массивы
59
В объявлении T D, где D имеет вид
D1 [константное-выражение]
тип идентификатора в объявлении есть “...массив элементов типа Т”. Если
константное-выражение присутствует, оно должно быть целочисленным больше
нуля. Оно задает число элементов массива. Если оно равно N, массив имеет N
элементов, пронумерованных от 0 до N-1.
Массив может состоять из элементов любого основного типа кроме void, из
указателей некоторого типа, из указателей на компоненты класса, из объектов
класса, из элементов типа перечисления или из массивов какого-либо типа.
Когда несколько спецификаций вида “массив из” являются смежными, создается
многомерный массив; константные выражения, задающие границы массива, могут
быть опущены только для первого элемента последователтности.
Возможность опускать первую размерность в объявлении массива полезна для
параметра функции, имеющей тип массива, а также в случае, когда массив является
внешним, а описание, распределяющее память, дается где-либо еще. Первое
константное-выражение может быть также опущено, если за описателем следует
список-инициализаторов (8.4). В этом случае размер массива равен количеству
элементов в инициализации (8.4.1). Примеры:
float fa[17], *afp[17];
объявляют массив чисел типа float и массив указателей на числа типа float.
static int x3d[3][5][7];
объявляет статический трехмерный массив целых чисел размером 3*5*7.
Когда идентификатор массива используется в выражении, он обычно
преобразуется в указатель на первый элемент массива. Операция индексации
обычно интерпретируется таким образом, что Е1[Е2] равносильно *((E1)+(E2)).
В С++ невозможно описание размерностей многомерных массивов через запятую,
как это делается в большинстве языков программирования. Поэтому ошибочно
объявление
int bad[5,2];
Указатели и массивы в С++ связаны очень тесно. Имя массива можно использовать
как указатель на его первый элемент. Например, программа печати целых значений
букв нижнего регистра может выглядеть следующим образом:
int main()
60
{
char alfa[] = "abcdefghijklmnopqrstuvwxyz";
char p* = alfa;
char ch;
while (ch = *p++)
cout << ch << " = " << int(ch) << " = 0" << oct(ch) << '\n';
}
Когда к указателю р типа *Т применяется арифметическая операция,
предполагается, что он указывает на элемент массива из Т; р+1 означает
следующий элемент массива, а р-1 - предыдущий элемент. Отсюда следует, что
значение р+1 будет на sizeof(T) больше значения р.
Имя массива является константой, потому присваивание массиву невозможно.
8.2.5. Функции
В объявлении T D, где D имеет вид
D1 (список-объявлений-параметров) список-cv-описателей opt D1
тип идентификатора в объявлении есть “... список-cv-описателей функция с
параметрами типа список-объявлений-параметров и результатом типа Т”.
список-объявлений-параметров:
список-объявлений-пар opt ... opt
список-объявлений-пар , ...
список-объявлений-пар:
объявление-параметра
список-объявлений-пар, объявление-параметра
объявление-параметра:
decl-спецификаторы описатель
decl-спецификаторы описатель = выражение
decl-спецификаторы абстрактный-описатель opt
decl-спецификаторы абстрактный-описатель opt = выражение
Если список-объявлений-параметров заканчивается многоточием, это означает, что
число аргументов будет равно или больше указанного числа параметров; если
список пуст, функция не имеет параметров.
Одно и то же имя может быть использовано для нескольких различных функций в
одной и той же области действия; это называется совмещением имен (overloading).
Типы параметров и тип результата являются частями типа функции. Список-cvописателей может быть частью объявления или описания нестатической
компонентной функции или указателя на компонентную функцию класса (9.3.1).
Список-cv-описателей является в таком случае частью типа функции.
61
Функции не могут возвращать функции или массивы, но могут возвращать ссылки
на них. Не разрешены массивы функций, но возможны массивы указателей на
функции.
Список-объявлений-параметров используется для проверки и приведения
фактических параметров в вызовах.
В объявлении функции можно давать имена ее параметрам (компилятор их
игнорирует); идентификатор, именующий параметр в объявлении функции не
может, однако, использоваться, так как немедленно заканчивается его область
действия. Имена параметров в разных объявлении и описании функции не
обязательно должны быть одинаковы. Пример. Объявление
f(),
*fpi(int),
(*pif)(const char*, const char*);
описывает функцию f без параметра с целым результатом, функцию fpi с целым
параметром и указателем на целое как результатом и указатель pif на функцию,
которая имеет два параметра - указатели на константные литеры - и целый
результат.
Семантика подстановки параметров идентична семантике инициализации. Сначала
проверяются типы параметров, а затем, если необходимо, производится неявное
преобразование типа. Например, для функции, объявленной как
double sqrt(double);
законен вызов
double sr2 = sqrt(2);
Имя функции является константой, потому присваивание функции невозможно.
8.2.6. Параметры по умолчанием
Если в объявлении параметра задано выражение, оно воспринимается как значение
параметра по умолчанию. При этом все следующие параметры также должны
иметь значения по умолчанию, заданные в этом или предыдущих объявлениях
функции. Значения по умолчанию подставляютя в вызов функции при отсутствии в
нем соответствующих фактических параметров. Пример. Функция, объявленная
как
point(int = 3, int = 4);
может быть вызвана следующими способами:
62
point(1, 2); point(1); point();
8.3. Описания функций
Каждая функция, используемая в программе, должна быть где-то описана (только
один раз). Описание функции имеет вид:
описание-функции:
decl-спецификаторы opt описатель ctor-инициализатор opt тело-функции
тело-функции:
составной оператор
Описатель в описании-функции согласно 8.2.5 должен иметь вид
D1 (список-объявлений параметров) список-cv-описателей opt
Пример:
int max(int a, int b, int c)
{
int m = (a > b) ? a : b;
return (m > c) ? m : c;
}
Здесь int - decl-спецификаторы, max(int a, int b, int c) - описатель, остальное - телофункции.
Нетерминал ctor-инициализатор употребляется только в конструкторе (12.1, 12.6).
Ключевые слова из список-cv-описателей могут появляться только в объявлении
компонентной функции, в описании нестатической компонентной функции и в
объявлении указателя на компонентную функцию класса (8.3.1). Этот список
является частью типа функции.
Неиспользуемые формальные параметры не обязаны иметь имена. Пример:
void print (int a, int)
{ printf(“a = %d\n”, a) }
Такие функции обычно возникают после упрощения текста программы и в
результате предварительного планирования на будущие расширения. В обоих
случаях, оставляя аргумунт на месте (хотя и неиспользуемым), мы гарантируем, что
вызывающие функцию программы не подвергнутся изменению.
8.3.1. Подстановка параметров
Когда вызывается функция, под ее формальные параметры выделяется память и
каждый из них инициализируется значением соответствующего фактического
параметра. Семантика подстановки параметров идентична семантике
63
инициализации. Есть особые правила для подстановки массивов (8.3.3), средства
передавать параметр без проверки типа, и средства для задания параметров по
умолчанию (8.2.6). Рассмотрим
void f(int val, int& ref)
{
val++;
ref++;
}
Когда вызывается f(), выражение val++ увеличивает локальную копию первого
фактического параметра, тогда как ref++ увеличивает второй фактический
параметр. Использование функций, которые изменяют фактические параметры,
может сделать программу трудно понимаемой и потому таких параметров лучше
избегать. Однако передача большого объекта по ссылке может быть эффективнее,
чем его передача по значению. В этом случае можно объявить такой параметр как
const, чтобы не позволить функции изменить значение подставляемого объекта.
Пример:
void f(const large& arg)
{
// значение "arg" не может быть изменено
}
Аналогично, если параметр, являющийся указателем, описывается как const,
подразумевается, что функция не меняет значение указываемого объекта. Примеры:
extern int strlen(const char*);
extern char* strcopy(char* to, const char* from);
Заметим, что семантика подстановки параметров отлична от семантики
присваивания. Это важно для параметров, специфицированных как const,
параметров-ссылок и параметров некоторых типов, определяемых пользователем.
8.3.2. Возврат значения
Функция, объявленная не как void, должна возвращать значение. Например:
int f() {} // ошибка
void g() {} // все в порядке
Возвращаемое значение указывается в операторе return. Например:
int fac(int n) { return (n >1) ? n*fac(n-1) : 1 }
64
В функции может быть больше одного оператора return, например:
int fac(int n)
{
if (n >1) return n*fac(n-1)
else return 1
}
Как и семантика подстановки параметров, семантика возврата значения идентична
семантике инициализации: тип возвращаемого значения проверяется на
согласованность с объявленным типом результата и выполняютя необходимые
преобразования типа. Пример:
double f()
{
// . . .
return 1; // неявно преобразуется к double(1)
}
Возвращать указатель на локальную переменную блока функции нельзя, так как эта
переменная после выхода из функции уничтожается. Пример:
int* f() {
int local = 1;
// . . .
return &local; // ошибка
}
Аналогичная ситуация при использовании ссылок:
int& f() {
int local = 1;
// . . .
return local; // ошибка
}
8.3.3. Массивы в качестве параметров
Если в качестве параметра функции используется массив, подставляется указатель
на его первый элемент. Пример:
int strlen(const char*);
void f()
{
65
char v[] = "an array";
strlen(v);
strlen("Nicolas");
};
Иначе говоря, Т[] преобразуется к Т*, когда он передается функции.
Следовательно, присваивание элементу параметра-массива меняет значение
фактического параметра-массива. Таким образом, массив не подставляется (и не
может подставляться) значением.
Размер массива недоступен функции. Это неудобство можно обойти несколькими
способами: строки заканчиваются нулем, который можно проверить, а для других
массивов можно задать второй параметр, содержащий размер массива, например:
void compute(int* vec_prt, int vec_size);
С многомерными массивами все хитрее. Рассмотрим описание функции, которая
работает с двумерными матрицами. Если размерность известна на стадии
компиляции, проблем нет:
void print_m34(int m[3][4])
{
for (int i =0; i<3; i++) {
for (int j = 0; j<4; j++)
cout << " " << m[i][j];
cout << '\n';
}
}
Хотя матрица все равно передается как указатель, объявленные размерности дают
данные для циклов.
Первую размерность можно заменить передачей параметра:
void print_mi4(int m[][4], int dim1)
{
for (int i =0; i<dim1; i++) {
for (int j = 0; j<4; j++)
cout << " " << m[i][j];
cout << '\n';
}
}
Сложный случай возникает, когда нужно передать обе размерности, так как
описание м[][] недопустимо. Вот правильное решение:
66
void print_mij(int** m, int dim1, int dim2)
{
for (int i =0; i<dim1; i++) {
for (int j = 0; j<dim2; j++)
cout << " " << ((int*)m)[i*dim2+j];
cout << '\n';
}
}
Здесь выражение, используемое для доступа к элементам, эквивалентно тому,
которое генерирует компилятор, когда он знает последнюю размерность.
8.3.4. Совмещение имен функций
Когда несколько функций выполняют сходную работу над объектами разных
типов, может быть удобно дать им одно и то же имя. Этот механизм называется
совмещением имен (overloading). Пример:
void print(int)
void print(const char*)
Когда вызывается функция с совмещенным именем f, компилятор должен понять, к
какой из функций с именем f следует обратиться. Для этого типа всех фактических
параметров сравниваются с типами формальных параметров всех функций с
именем f. Задача заключается в том, чтобы вызвать функцию, которая лучше всех
(по типам параметров) соответствует вызову или выдать сообщение об ошибке,
если подходящей функции нет. Пример:
void print(double);
void print(long);
void f()
{
print(1L);
print(1.0);
print(1);
// print(long)
// print(double)
// ошибка: неясно, print(long) или print(double)
Подробные правила согласования объясняются в 13.2. В упрощенном виде они
применяются в следующем порядке:
1) Точные совпадения, т.е. согласования, которые совсем не используют или
используют преобразования, без которых нельзя обойтись (например,
67
преобразование имени массива к указателю, имени функции к указателю на
функцию и Т к const T).
2) Согласования, использующие приведение к арифметическому типу (4.1),
например, char в int, short в int и их беззнаковых аналогов unsigned, а также
float в double, int во float и т.д..
3) Согласования, которые используют преобразования, определенные
пользователем (12.3).
4) Согласования, использующие многоточия ... в объявлении функции.
Правила отождествления имеют такую сложнею форму в основном из-за того, что
необходимо использовать уже имеющиеся в С правила преобразования числовых
типов. Примеры:
void print(int)
void print(const char*)
void print(double);
void print(long);
void print(char);
void h(char c, int i, short s, float f)
{
print(c);
// точное согласование, вызывается print(char)
print(i)
// точное согласование, вызывается print(int)
print(s)
// приведение к числовому типу, вызывается print(int)
print(f)
// приведение к числовому типу, вызывается print(double)
print('a')
// точное согласование, вызывается print(char)
print(49)
// точное согласование, вызывается print(int)
print("a")
// точное согласование, вызывается print(const char*)
При указанных правилах можно гарантировать, что, когда эффективность или
точность вычислений для используемых типов различаются, будет использоваться
простейший алгоритм.
8.3.5. Произвольное число параметров
Для некоторых функций невозможно заранее знать число и типы всех параметров,
которые могут оказаться в вызове. Пример:
Объявление
printf(const char* ...);
обявляет функцию с переменным числом параметров различных типов. Первым
параметром функции printf, однако, всегда должно быть значение, преобразуемое в
const char*. Примеры вызова:
printf(“hello world\n”);
printf("Мое имя %s %s\n", first_name, second_name);
68
printf(“%d+%d=%d\n”, 2, 3, 5);
Такая функция полагается на информацию, не доступную компилятору при
интерпретации ее списка параметров. В случае printf() первым параметром является
строка формата, содержащая специальные последовательности символов,
позволяющие printf() правильно обрабатывать остальные параметры. %s означает
"жди параметра типа *char, а %d означает "жди параметра типа int".
Очевидно, что если параметр не был описан, у компилятора нет информации для
проверки и преобразования типа фактического параметра. При необходимости
проверки типов, следует использовать функции с совмещенными именами или
функции с параметрами по умолчанию. Многоточие обычно применяют для того,
стобы установить интерфейс с библиотечными С-функциями, которые были
определены, когда альтернативы не было:
extern "C" int fprintf(FILE*, const char* ...);
extern "C" int execl(const char* ...);
Стандартный набор макросов для доступа к неспецифицированным параметрам
таких функций можно найти в <stdarg.h>. Ниже следует описание функции
ошибок, которая получает один целый параметр, указывющий серьезность ошибки,
после которого идет произвольное число строк. Идея состоит в том, чтобы
генгерировать сообщение об ошибке с помощью передачи каждого слова как
отдельного строкового параметра.
extern void error(int ...);
extern char* itoa(int);
main(int argc, char* argv[])
{
switch(argc) {
case1: error(0, argv[0], (char*)0); break;
case2: error(0, argv[0], argv[1], (char*)0); break;
default: error(1, argv[0], 'c', itoa(argc-1), "параметрами", (char*)0);
}
// . . .
}
Функцию ошибок можно определить так:
#include <stdarg.h.
void error(int severity ...)
// "severity" с последующем списком char*, оканчивающимся нулем
{
va_list ap;
69
va_start(ap, severity); // раскрутка аргумунтов
for (;;) {
char* p = va_arg(ap, char*);
if (p == 0) break;
cerr << p << ' ';
}
va_end(ap) // очистка стека
cerr << '\n';
if ceverity exit severity;
}
Сначала описывается и нициализируется вызовом va_start() объект типа va_list.
Макрос va_start получает в качестве параметров имя этого объекта и имя первого
формального параметра. Макрос va_arg() используется для выбора неименованных
параметров по порядку. При каждом обращении к нему программист должен задать
тип. Перед возвратом из функции, в которой был использован va_start(), должен
быть вызван va_end(). Причина в том, что va_start() может изменить стек так, что
нельзя будет успешно осуществить возврат; va_end() аннулирует эти изменения.
Преобразовывать 0 к (char*)0 необходимо потому, что sizeof(int) не обязательно
совпадает с sizeof(char*). Это иллюстрирует те сложности, скоторыми
программисту приходится сталкиваться из-за того, что при использовании
многоточия теряется контроль типов.
8.3.6. Указатель на функцию
С функцией в С++ можно делать только две вещи: вызывать ее и брать ее адрес.
Указатель, полученный взятием адреса функции, можно затем использовать для ее
вызова. Пример:
void error(char* p) { /* . . . */};
void (*efct)(char*); // указатель на функцию
void f()
{
efct = &error; // efct указывает на error
(*efct)("error"); // вызов error через efct
}
Чтобы вызвать функцию через указатель, его надо сначала разыменовать.
Поскольку операция вызова функции () имеет более высокий приоритет, чем
операция разыменования "*", нельзя написать просто *efct("error"), что означает
*(efct("error")). Можно, однако, написать efct("error"), и компилятор обнаружит,
что efct - это указатель и правильно вызовет функцию.
Часто бывает полезен массив указателей на функции.
70
8.4. Инициализаторы
Описатель может задавать начальное значение для объявляемого идентификатора.
инициализатор:
= выражение-присваивания
= {список-инициализаторов , opt}
(список-выражений)
список-инициализаторов:
выражение
список-инициализаторов, выражение
{список-инициализаторов , opt}
Вариант {список-инициализаторов , opt} взят из С и хорошо служит при инициализации
массивов и структур данных, например:
struct Conf {
char* month;
int year;
char* location;
} cpp[] = {{"November", 1987, "Santa Fe"},
{"October", 1988, "Denver"},
{"November", 1989, "San Francisco}}:
Вариант = выражение-присваивания также взят из С и наиболее естественен при
инициализации простых переменных, например:
int i = 1;
complex z = complex(2.3, 4.0);
Вариант (список-выражений) взят из Симулы и служит для создания сложного
объекта, например:
Task ring-monitor = Task("monitor", 1024, SHARED);
Указатель типа const T*, т.е. указатель на константу можно инициализировать
указателем типа Т*, но обратная инициализация запрещена. Объекты типа Т всегда
можно инициализировать объектами типа Т, независимо от модификаторов const и
volatile как у инициализируемой переменной, так и у инициализатора, например:
int a;
const int b = a;
int c = b;
const int* p0 = &a;
const int* p1 = &b;
71
int* p2 = &b;
int *const p3 = &a;
int *const p4 = p1;
//ошибка: указатель не на константу будет
// указывать на константу
//ошибка: указатель не на константу будет
// указывать на константу
const int* p5 = p1;
В обоих случаях допущение инициализаций позволило бы изменять через
указатель значение объекта, объявленного константой.
8.4.1. Агрегаты
Агрегат - это массив или объект некоторого класса. При инициализировании
агрегата инициализатор может быть списком-инициализаторов, состоящим из
заключенных в фигурные скобки и разделенных запятыми инициализаторов для
компонентов агрегата, записанных по возрастанию индекса или в порядке
объявления компонентов. Если в списке меньше инициализаторов, чем в агрегате компонентов, оставшиеся компоненты инициализируются нулями
соответствующих типов. Например,
struct S { int a; char b; int c};
S ss = {1, "asdf"};
инициализирует с нулем.
Вложенные фигурные скобки можно опускать:
float y[4][3] = {
{1, 3, 5},
{2, 4, 6},
{3, 5, 7},
};
эквивалентно
float y[4][3] = {1, 3, 5, 2, 4, 6, 3, 5, 7};
В обих случаях y[3] инициализируется нулями.
Инициализаторов должно быть не больше, чем элементов, требующих
инициализации.
8.4.2. Массивы литер
Массив литер может инициализироваться литеральной-строкой:
72
char msg[] = "Syntax error on line %s\n";
В данном примере: поскольку "\n" - это одна литера и добавляется завершающий 0,
длина строки - sizeof(msg) - равна 25.
Инициализация литерных массивов - это единственное применение строк в С++
(присваивание строк массиву невозможно).
8.4.3. Ссылки
Переменная, обявленная как T&, т.е. типа "ссылка типа Т", должна быть
инициализирована объектом типа Т или приводимым к нему. Пример:
void f()
{
int i;
int& r = i;
r = 1;
int* p = &r
int& rr = r
//r ссылается на i
// значение i становится равным 1
// p указывает на i
// rr ссылается на то же, что и r, т.е. на i
После своей инициализации ссылка не может быть изменена на ссылку на другой
объект.
Инициализатором для "обычного" Т должно быть l-значение (т.е. объект, адрес
которого можно взять). Однако инициализатор для const T& (ссылка на константу)
не обязательно должен быть l-значением, и даже может быть не типа Т. В таком
случае:
1) если необходимо, применяется преобразование типа;
2) полученное значение помещается во временную переменную;
3) ее адрес используется в качестве инициализатора.
Пример. Объявление
double& dr = 1;
const double& cdr = 1
// ошибка: необходимо l-значение
// OK
интерпретируется так:
double* cdr
double temp;
temp = double(1);
cdr = &temp;
// ссылка, представленная как указатель
73
Инициализатор может быть опущен для ссылки только в случае объявления
параметра функции (8.2.5) или типа ее результата, в объявлении компонента класса
внутри объявления самого класса (9.2), а также при наличии спецификатора extern.
74
9. Классы
Тип данных - это конкретное представление некоторой идеи или пониятия.
Например, тип float с его операциями обеспечивает ограниченную, но конкретную
версию математического понятия действительного (вещественного) числа. При
определении нового типа основная идея - отделить несущественные подробности
реализации (например, форматы данных, используемые для представления
объектов данного типа) от тех качеств, которые существенны для его правильного
использования (например, список операций, применимых к объектам данного
типа).
Класс - это определенный пользователем тип. Его имя становится именем-класса
(9.1), т.е. зарезервированным словом внутри его области действия.
имя-класса:
идентификатор
Для объявления имен-классов служат спецификаторы-класса и уточненныеспецификаторы-типа (7.1.6). Объект некоторого класса состоит из (возможно,
пустой) последовательности компонентов.
спецификатор-класса:
заголовок-класса {список-компонентов opt}
заголовок-класса:
ключевое-слово-класса идентификатор opt спецификация-базы opt
ключевое-слово-класса имя-класса спецификация-базы opt
ключевое-слово-класса:
class
struct
union
Имя класса можно употреблять как имя-класса уже внутри списка-компонентов
этого самого класса. Пример:
class link { link* next};
Cпецификатор-класса обычно называется объявлением класса. Класс полагается
описанным, когда известен его спецификатор-класса, даже если еще не описаны
его компонентные функции.
Объекты класса можно присваивать, передавать в качестве параметров функции и
возвращать как ее результат. Программистом могут быть определены и другие
естественные операции вроде проверки на равенство (13.4).
Структура (structure) - это класс, объявленный с ключевым-словом-класса struct; ее
компоненты и базовые классы (10) являются открытыми (public) по определению
75
(11). Объединение (union) - класс, объявленный с ключевым-словом-класса union,
его компоненты и базовые классы также являются открытыми по определению;
объединение всежда содержит лишь один компонент класса (9.5).
9.1. Имена классов
Объявление класса вводит новый тип. Например,
struct X {int a;};
struct Y {int a;};
X a1;
Y a2;
int a3;
объявляет три переменные трех разных типов. Из этого следует, что
а1 = а2;
а1 = а3;
// ошибка: Y присваивается X
// ошибка: int присваивается X
дает несоответствие типов, а
int f(X);
int f(Y);
объявляет две функции с совмещенным именем. Таким образом, С++ использует
принцип эквивалентности имен для контроля типов.
Если имя класса объявляется в области действия, где уже объявлен объект,
функция или перечислитель с тем же именем, этот класс можно именовать лишь
уточненным-спецификатором-типа (7.1.6). Пример:
struct stat {
// . . .
}
stat gstat;
// описыывается переменная типа stat
int stat(struct stat*)
void f()
{
struct stat* ps;
// . . .
stat(ps);
// . . .
// переопределяем stat как функцию
// префикс struct для именования структуры stat
// вызов stat()
76
}
Уточненный-спецификатор-типа с ключевым-словом-класса, за которым нет
объявляемого объекта или функции, явно вводит имя класса без его описания:
struct s { int a; };
void g()
{
struct s;
// скрывает глобальную структуру "s"
s* p;
// ссылается на локальную структуру "s"
struct s {char* p;};
// описание локальной структуры "s"
}
Уточненный-спецификатор-типа (7.1.6) может также употребляться в
объявлениях объектов и функций. Он отличается от объявления класса в том, что
если в области действия виден класс с этим уточненным именем, оно будет
относиться именно к этому классу. Пример:
struct s { int a; };
void g()
{
struct s* p = new s;
p->a = 1;
}
// s здесь именует глобальную структуру "s"
9.2. Компоненты класса
список-компонентов:
объявление-компонента список-компонентов opt
спецификатор доступа: список-компонентов opt
объявление-компонента:
decl-спецификаторы opt список-описателей-компонентов opt ;
описание-функции ; opt
уточненное-имя ;
список-описателей-компонентов:
описатель-компонента
список-описателей-компонентов, описатель-компонента
описатель-компонента:
описатель чистый-спецификатор opt
идентификатор opt: константное-выражение
чистый-спецификатор:
=0
77
В списке-компонентов можно объявлять данные, функции, классы, перечисления
(7.2), битовые поля (9.6), дружественные функции и классы (11.4) и имена типов
(7.1.3, 9.1). Список-компонентов может также содержать объявления,
корректирующие доступ к компонентам класса (11.3). Компонент класса не может
объявляться в списке-компонентов дважды.
Описатель-компонента не может содержать инициализатора (8.4): компонент
можно инициализировать посредством конструктора (12.1).
Компонент класса не может быть автоматическим, внешним или регистровым
(auto, extern, register).
Decl-спецификаторы могут быть опущены только в объявлении функций. Списокописателей-компонентов может быть опущен только после спецификатора-класса,
спецификатора-перечисления или decl-спецификатора в форме friend уточненныйспецификатор-типа. Чистый-спецификатор может появляться только в
объявлении виртуальной функции (10.2).
Компоненты класса, являющиеся объектами классов, должны быть объектами
ранее объявленных классов. В частности, класс не может содержать свой
собственный объект, но он может содержать ссылку на него.
Если нестатический компонент класса является массивом, у него должны быть
заданы все размерности.
Простым примером объявления класса является структура:
struct tnode {
char tword[20];
int count;
tnode *left;
tnode *right;
}
которая содержит массив из 20 чисел, целое число и два указателя на такие же
структуры. Объявление
tnode s, *sp;
объявляет s структурой типа tnode, а sp - указателем на структуру типа tnode.
Нестатические компонентные данные, между которыми нет спецификаторадоступа, размещаются в памяти объекта по возрастанию адресов в порядке их
объявления. Порядок размещени нестатических компонентных данных,
разделенных спецификатором-доступа, зависит от реализации.
78
Компонентная функция класса (9.3), имя которой совпадает с именем класса,
называется конструктором (12.1). Остальные компоненты не могут иметь имя,
совпадающее с именем класса.
9.3. Компонентные функции класса
Функция, объявленная в классе без спецификатора friend, называется
компонентной функцией класса. Ее вызов имеет соответствующий синтаксис
(5.2.4). Пример:
struct tnode {
char tword[20];
int count;
tnode *left;
tnode *right;
void set(char*, tnode* l, tnode* r);
}
Здесь set является компонентной функцией класса и может быть вызвана
следующим образом:
void f(tnode n1, tnode n2)
{
n1.set("abc", &n2, 0);
n2.set("def", 0, 0);
}
Таким образом, имя компонентной функции при ее вызове уточняется именем
объекта ее класса. Другой пример:
struct date {
int day, month, year;
void set(int, int, int);
void get(int*, int*, int*);
void next();
void print();
};
date today;
date my_birthday;
//
//
void f()
{
my_birthday.set(12, 2, 1943);
79
today.set(28, 8, 1995);
today.print();
Описание компонентной функции класса относится к области действия этого
класса. Это означает, что компонентная функция класса может непосредственно
использовать имена компонентов своего класса. Статическая компонентная
функция может непосредственно употреблять только имена статических
компонентов, перечислителей и вложенных типов. Если компонентная функция
описывается вне текста объявления класса, ее имя должно уточняться его именем,
так как разные классы могут иметь функции с одним и тем же именем, например:
void tnode::set(char* w, tnode* l, tnode* r);
{
count = strlen(w)+1;
if (sizeof(tword) <= count)
error("строка tnode слишком длинна");
strcopy(tword, w);
left = l;
right = r;
}
Здесь tnode::set означает, что функция set есть компонент класса tnode и лежит в
области его действия. Имена компонентов класса tword, count, left и right относятся
к компонентам того объекта, для которого вызывается функция. В вызове
n1.set("abc", &n2, 0) имя tword относится к n1.tword, а в вызове n2.set("def", 0, 0) - к
n2.tword.
Каждая компонентная функция класса должна иметь ровно одно описание в
программе.
Иногда полезно определить компонентную функцию так, чтобы она могла
считывать значение объекта, к которому она применяется, но не менять его. Такая
функция объявляется с суффиксом const и называется константной функцией.
Константная компонентная функция класса может быть вызвана и для константных
и для переменных объектов, в то время как неконстантная компонентная функция
класса может быть вызвана только для переменных объектов. Пример:
struct s {
int a;
int f() const;
int g();
}
void k(s& x, const s& y)
{
80
x.f();
x.g();
y.f();
y.g();
// OK
// OK
// OK
// ошибка
{
Конструкторы (12.1) и деструкторы (12.4) могут быть вызваны для константных и
подвижных объектов, но не могут быть объявлены таковыми.
9.3.1. Указатель this
В нестатической компонентной функции класса ключевое слово this обозначает
указатель на объект, для которого вызвана эта функция. Этот указатель в
компонентной функции класса Х имеет тип X *const, если только функция не
объявлена как const или volatile; в этих случаях тип указателя this есть,
соответственно, const X *const и volatile X *const. В функции, объявленной
одновременно как const и volatile, тип указателя this есть const volatile X *const.
Пример:
struct s {
int a;
int f() const;
int g() {return a++;};
int h const {return a++}
}
// ошибка
int s::f() const {return a}
Выражение а++ в теле функции s::h недопустимо, поскольку оно изменяет часть
"а" объекта, для которого вызывается s::h(). Это как раз делать нельзя в
константной компонентной функции класса, где this (а - короткая запись this->a)
есть указатель на const, т.е. *this - константа.
Как видно из предыдущего примера, при ссылке на компонент использование this
излишне. Главным образом, он используется при написании компонентных
функций, работающих непосредственно с указателями. При этом указатель на
объект, для которого вызвана компонентная функция, является ее скрытым
параметром. Типичный пример - функция, вставляющая звено в двусвязный
список:
struct dlink {
dlink* pre;
dlink* suc;
void append(dlink* p);
};
81
void dlink::append(dlink* p)
{
p->suc = suc;
// то есть, p->suc = this->suc
p->pre = this;
// явное использование this
suc = p;
// то есть, this->suc = p
};
dlink* list_head;
void f(dlink* a, dlink*b)
{
list_head->append(a);
list_head->append(b);
};
9.3.2. Встраиваемые компонентные функции класса
Компонентная функция класса может быть описана внутри объявления класса; в
этом случае она считается встраиваемой функцией. Описание функции в
объявлении класса эквивалентно явному ее объявлению как встраиваемой и ее
описанию сразу после объявления класса. Так,
struct x {
char* f() {return b};
char* b;
}
равносильно
struct x {
char* f();
char* b;
};
inline char* x::f() {return b}
9.4. Статические компоненты класса
Класс - это тип, а не объект данных, и в каждом его объекте имеется своя копия его
компонентных данных. Однако некоторые типы наиболее элегантно реализуются,
когда все объекты данного типа могут совместно использовать некоторые данные.
Предпочтительно, чтобы такие данные были объявлены как часть класса. Поэтому
при объявлении класса компонентные данные и функции можно объявлять
статическими (static). Существует лишь один экземпляр статических
82
компонентных данных класса, используемый всеми объектами этого класса в
программе. Статические компоненты глобального класса имеют внешнее
связывание (3.3). Примером может служить список задач, используемый для
управления задачами в операционной системе:
class task {
// . . .
public: static task* chain;
// . . .
};
Статическая компонентная функция класса не получает параметром указатель this и
потому может обращаться к нестатическим компонентам своего класса только
посредством операций "." или "->". Статическая компонентная функция класса не
может быть виртуальной.
Статические компоненты локального класса (9.8) не имеют связывания и не могут
быть описаны вне объявления класса.
Статический компонент mem класса с1 можно обозначать как c1::mem (5.1), т.е.
независимо от объекта. Пример:
if (task::chain = 0) // что-то делаем
На статический компонент можно также ссылаться посредством оперaций доступа
к компонентам класса "." и "->" (5.2.4), при этом выражение слева от операции не
вычисляется (нужен только его тип). Статический компонент класса будет
существовать, даже если не создан ни один объект данного класса.
Статические компоненты глобального класса подчиняются обычным правилам
доступа к компонентам класса (11), не считая того, что они должны быть
инициализированы в глобальной области, например:
task* task::chain = 0;
Использование статических компонентов класса может заметно снизить
потребность в глобальных переменных. Объявление компонента как статического
ограничивает его видимость и делает его независимым от индивидуальных
объектов класса.
9.5. Объединения
Объединение является структурой, у которой в любой момент времени существует
значение только одного из компонентов. По этой причине при хранении объектаобъединения достаточно выделить память под максимальный компонент.
83
Объединение может иметь компонентные функции (в том числе конструкторы и
деструкторы), но не виртуальные функции (10.2). Объединение не может иметь
базовых классов и само не может быть базовым классом; оно не может также иметь
статических компонентных данных. Пример:
union tok_val {
char* p;
char v[8];
long i;
double d;
};
// строка
// массив из 8 символов
// целое
// вещественное двойной точности
Cложность в использовании объединений заключается в том, что компилятор не
может знать, какой компонент используется в данный момент и проконтролировать
его, например:
void strange (int i)
{
tok_val x:
if (i)
x.p = "2";
else
x.d = 2;
sqrt(x.d);
// ошибка, если i != 0
};
По этой причине при использовании объединений необходимо отслеживать
текущий тип значения (например, посредством дополнительного параметра).
Объединение вида
union {список-компонентов}
называется безымянным (anonymous); оно определяет неименованный объект (а не
тип). Имена компонентов безымянного объединения должны отличаться от других
имен из его области действия и используются непосредственно. т.е. без обычного
для доступа к компонентам объединения (5.2.4). Пример:
void f()
{
union {int a; char* p};
a = 1;
// . . .
p = "Jenny";
// . . .
84
}
Здесь а и р употребляются как обычные переменные, но, поскольку они являются
компонентами объединения, имеют один и тот же адрес.
Глобальное безымянное объединение должно быть объявлено как статическое
(static), чтобы не иметь внешнего связывания и сопутствующих проблем
согласования типов в разных файлах.
Объединение, при котором объявлены объекты или указатели, не считается
безымянным. Пример:
union {int aa; char* p} obj, *ptr = &obj;
aa =1;
// ошибка
ptr->aa = 1
// нормально
Присваивание просто компоненте "аа" неверно, поскольку имя компонента
объединения не ассоциируется ни с каким конкретным объектом.
9.6. Битовые поля
Описатель-компонента вида
идентификатор opt : константное выражение
определяет битовое поле длиною, определяемому константным выражением.
Битовые поля пакуются в некоторую адресуемую единицу памяти. Неименованное
битовое поле полезно для разметки в соответствии с установленными извне
схемами. Оно не считается компонентом и не может быть инициализировано.
Битовое поле должно иметь целочисленный тип. Операция взятия адреса
неприменима к нему, поэтому указателей на битовые поля нет.
9.7. Вложенные объявления классов
Класс, объявленный внутри другого класса, является вложенным (nested). Имя
вложенного класса является локальным в его объемлющем классе. Не считая
обычной работы через явные указатели, ссылки и имена объектов, объявления во
вложенном классе могут использовать из объемлющего класса только имена типов,
статических компонентов и перечислителей. Пример:
int x;
int y;
85
class enclose {
public:
int x;
static int s;
class inter {
void f(int i)
{
x = i;
// ошибка: присваивание enclose::x
s = 1;
// верно: присваивание enclose::s
::x = 1; // верно: присваивание глобальной x
y = i;
// верно: присваивание глобальной y
};
void g(enclose* p, int i)
{
p->x = i; // верно: присваивание enclose::x
};
};
};
inner* = 0;
// ошибка: "inner' вне области действия
Компонентные функции как объемлющего класса, так и вложенного класса
подчиняются обычным правилам доступа. Пример:
class E {
int x;
class I {
int y;
void f(E* p, int i)
{
p->x = i; // ошибка: E::x скрытый компонент класса
};
};
int g(I* p)
{
return p->y; // ошибка: I::y скрытый компонент класса
};
};
Вложенность имеет то преимущество, что сводит к минимуму количество
глобальных имен, и тот недостаток, что препятствует использование вложенных
классов в других частях программы. Доступ к имени вложенного класса извне
объемлющего класса осуществляется так же, как и к имени любого другого
компонента. Пример:
86
class X {
struct M1 {int m};
public:
struct M2 {int m};
M1 f(M2);
};
void f()
{
M1 a;
M2 b;
X::M1 c;
X::M2 d;
}
// ошибка: имя "М1" невидимо
// ошибка: имя "М2" невидимо
// ошибка: "Х::М1" скрытый компонент класса
// все в порядке
9.8. Локальные объявления классов
Класс, описанный внутри функции, называется локальным. Объявления в
локальном классе могут использовать из объемлющей области действия только
имена типов, статические переменные, внешние переменные и элементы
перечисления (разрешение использования автоматических переменных
объемлющей функции предполагало бы допустимость вложенных функций).
Пример:
int x;
void f()
{
static int s;
int x;
extern int g();
struct local {
int g() {return x;} // ошибка: "x" автоматическая переменная
int h() {return s;} // верно
int k() {return ::x;} // верно
int l() {return g();}
// верно
};
};
local* p = 0;
// ошибка: "local" вне области действия
Компонентные функции локального класса должны быть описаны в нем.
Локальный класс не может иметь статических компонентных данных. Цель этих
ограничений - избежание сложных локальных классов.
9.9. Локальные имена типов
87
Имена типов подчиняются тем же правилам области действия, что и другие имена.
Тем самым, имена типов, определенных внутри объявления класса, нельзя
употреблять без уточнения "::" вне этого класса. Пример:
class X {
public:
typedef int I;
class Y { ... };
I a;
};
I b;
// ошибка
Y c;
// ошибка
X::Y d;
// нормально
Имя-класса, описанное-имя-типа или имя константы, использованное в имени
типа, не может переопределяться в объявлении класса после его использования в
нем. Пример:
typedef int c;
enum { i =1};
class X {
char v[i];
int f() {return sizeof(c)};
char c; //ошибка: имя типа переопределяется после использования
enum {i =2}; // ошибка: константа "i" переопределяется после исп-ния
};
typedef char* T;
struct Y {
T a;
typedef long T; // ошибка: Т уже использован
T b;
};
88
10. Производные классы
Для того чтобы отразить иерархические связи, существующие между понятиями и
выражающие общность между классами, вводится понятие производного класса и
связанные с ним понития языка. Например, понятие треугольника и окружности
связаны друг с другом, потому что оба являются фигурами. Для того чтобы ввести
треугольники и окружности в программу, не упустив при этом поняте фигуры,
следует явно определить классы circle и triangle как включенные в общий класс
shape. Данная глава содержит выводы из этой простой идеи, которая лежит в
основе того, что называется объектно-ориентированным программированием.
При определении производного класса список его базовых классов задается при
помощи следующей нотации:
спецификация-базы:
: список-баз
список-баз:
описатель-базы
список-баз, описатель-базы
описатель-базы:
полное-имя-класса
virtual спецификатор доступа opt полное-имя-класса
спецификатор доступа virtual opt полное-имя-класса
спецификатор доступа:
private
protected
public
Полное-имя-класса в описателе-базы должно обозначать ранее объявленный класс,
который называется в таком случае базовым классом. Если компоненты базового
класса не переопределены в производном классе, они обозначаются и трактуются
так же как и компоненты производного класса. В таком случае говорят, что
компоненты базового класса наследуются производным классом. Производный
класс может сам, в свою очередь, служить базовым классом с соответствующим
контролем доступа.
В качестве примера рассмотрим построение программы, которая имеет дело с
людьми, служащими в некоторой фирме. Структура данных в этой программе
может быть следующей:
struct employee {
char* name;
short age, department;
int salary;
employee* next; // список однотипных служащих
89
// . . .
};
Определим тип менеджера:
struct manager {
employee emp;
// сведения о менеджере как о служащем
employee* group // подчиненные люди
short level;
// уровень
// . . .
};
Менеджер является также служащим; относящиеся к служащему данные хранятся в
компоненте emp класса manager. Здесь нет, однако, ничего, выделяющего
компонент emp для компилятора. Указатель на менеджера (manager*) не является
указателем на служащего (employee*), поэтому использовать один просто там, где
требуется другой, нельзя. В частности, нельзя поместить менеджера в список
служащих, не написав для этого специальный фрагмент программы. Корректный
подход заключается в том, чтобы установить, что менеджер является служащим с
некоторой дополнительной информацией:
struct manager: employee {
employee* group // подчиненные люди
short level;
// уровень
// . . .
};
Здесь manager является производным для employee и, обратно, employee есть
базовый класс для manager. Указатель на объект производного класса может быть
преобразован в указатель на объект однозначно доступного базового класса (4.6).
Точно также ссылка на объект производного класса может быть преобразован в
ссылку на объект однозначно доступного базового класса (4.7). Имея объявления
employee и manager, мы можем теперь создать список служащих, некоторые из
которых являются менеджерами:
void f()
{
manager m1, m2;
employee e1, e2;
employee* elist;
elist = &m1;
m1.next = &e1;
e1.next = &m2;
m2.next = &e2;
e2.next = 0;
// поместить m1 в elist
// поместить e1 в elist
// поместить m2 в elist
// поместить e2 в elist
// завершить elist
90
};
Здесь было использовано неявное преобразования указателя на объект
производного класса в указатель на объект базового класса. Обратное
преобразование должно быть явным, чтобы удостовериться, что указатель
действительно направлен на объект данного производного класса; например:
void g()
{
manager mm;
employee* pe = &mm; // OK, каждый менеджер является служащим
employee ee;
manager* pm = ⅇ // ошибка: не каждый служащий является менеджером
pm = (manager*) pe; // OK, поскольку 'ре' на самом деле ссылается на 'mm'
pm->level = 2;
// OK, у менеджера есть место для 'level'
};
Заметим, что в типичной реализации С++ отсутствуют проверки на стадии
выполнения программы, которые гарантируют, что указатель на объект базового
класса действительно ссылается на объект производного класса (см. варианты
решения проблемы в 10.1.1 и 10.2)
Просто структуры данных вроде employee и manager не особенно полезны, поэтому
рассмотрим, как добавить в них функции. Пример:
class employee {
char* name;
// . . .
public:
employee* next;
void print() const;
// . . .
};
class manager: public employee {
// . . .
public:
void print() const;
// . . .
};
Здесь возникают несколько вопросов:
1) Как может компонентная функция производного класса использовать
компоненты базового класса?
91
2) Как компоненты базового класса могут использовать компонентные функции
производного класса?
Рассмотрим:
void manger::print() const
{
cout << " имя " << name << '\n';
};
Компонентная функция производного класса может использовать открытое имя из
своего базового класса так же как это могут делать компоненты базового класса,
т.е. без указания объекта (this предполагается неявно). Однако функция
manger::print компилироваться не будет, так как компонентная функция
производного класса не имеет особого права доступа к скрытым компонентам
своего базового класса. Обычно самое простое решение - использовать открытые
компоненты своего базового класса, например:
void manger::print() const
{
employee::print(); // печатает информацию о служащем
// печатает информацию о менеджере
};
Класс называется непосредственным базовым классом, если он упоминается в
списке-баз, и косвенным базовым классом, если он является базовым классом для
одного из классов, упомянутых в списке-баз. Имя в нотации имя-класса::имя может
быть именем компонента косвенного базового класса; эта запись просто
определяет класс, в котором начинается поиск имени. Пример:
class A {public: void f();};
class B : public A {};
class C : public B {public: void f();};
void C::f()
{
f();
//вызов f() из С
A::f(); //вызов f() из A
B::f() //вызов f() из A
};
Здесь A::f() вызывается оба раза, поскольку она - единственная f() в В.
10.1 Множественные базовые классы
92
Класс может быть порожден из любого числа базовых классов. Пример:
class A {/* . . . */};
class B {/* . . . */};
class C {/* . . . */};
class D: public A, public B, public C {/* . . . */};
Наличие нескольких непосредственных базовых классов называется
множественным наследованием (multiple inheritance).
Класс не может быть задан в качестве непосредственного базового класса более
одного раза, но он может быть более одного раза косвенным базовым классом.
Пример:
class B {/* . . . */};
class D: public B, public B {/* . . . */};
// неверно
class L {/* . . . */};
class A: public L {/* . . . */};
class E: public L {/* . . . */};
class C: public A, public Е {/* . . . */};
// верно
В последнем случае объект класса С будет иметь два подобъекта класса L. Класс не
может появляться дважды в списке базовых классов по той простой причине, что
каждая ссылка на него была бы неоднозначна.
10.1.1. Неоднозначности
Два базовых класса могут иметь компонентные функции с одним и тем же именем,
например:
class task {
// . . .
virtual debug_info* get_debug();
};
class displayed {
// . . .
virtual debug_info* get_debug();
};
При использовании обоих классов для порождения класса satellite:
class satellite : public task, public displayed
{
93
// . . .
};
неоднозначность в этих функциях должна быть устранена:
void f(satellite* sp)
{
debug_info* dip = sp->get_debug(); // ошибка: неоднозначность
dip1 = sp->task::get_debug();
// OK
dip2 = sp->displayed::get_debug();
// OK
Это решение не очень удачно, лучше описать новую функцию в производном
классе:
class satellite : public task, public displayed
{
// . . .
debug_info* get_debug()
{
debug_info* dip1 = task::get_debug();
debug_info* dip2 = displayed::get_debug();
return cond? dip1 : dip2;
};
};
Таким образом информация о базовых классах локализована, всякий раз при
вызове get_debug() для объекта класса satellite будет гарантированно вызываться
satellite::get_debug().
10.1.2. Поле типа
Чтобы использовать производные классы не только как удобную сокращенную
запись в описаниях, надо разрешить следующую простую проблему: если задан
указатель вида base*, какому производному типу в действительности принадлежит
указываемый объект? Есть три основных способа решения этой проблемы:
1) Обеспечить, чтобы всегда указывались только объекты одного типа.
2) Поместить в базовый класс поле типа, просматриваемое функциями.
3) Использовать виртуальные функции (10.2).
Обычно указатели на базовые классы используются при разработке контейнерных
классов: множество, вектор, список и т.п. В этом случае решение 1 дает
однородные списки. Решения 2 и 3 можно использовать для построения
неоднородных списков, т.е. списков указателей на объекты разных типов.
94
Исследуем снячала решение 2, исследующее поля типа. Пример со служащими и
менеджерами можно переопределить так:
struct employee {
enum employee_type {m, e};
employee_type type;
char* name;
short department;
employee* next; // список однотипных служащих
// . . .
};
struct manager : employee {
employee* group // подчиненные люди
short level;
// уровень
// . . .
};
Теперь мы можем написать функцию, печатающую информацию о каждом
служащем:
void print_employee (const employee* e)
{
switch (e->type) {
case e:
cout << e->name << '\t' << e->department << '\n'
// . . .
break;
case m:
cout << e->name << '\t' << e->department << '\n';
// . . .
manager p = (manager*)e;
cout "уровень" << p->level << '\n';
// . . .
break;
};
};
и воспользоваться ею, чтобы напечатать список служащих:
void f(const employee* elist)
{
for (; elist; elist = elist->next) print_employee(elist);
}
95
Такой вариант хорошо работает в одной программе, написанной одним человеком,
но имеет большой недостаток необходимости явной проверки типа. В больших
программах это обычно приводит к ошибкам двух сортов:
1) невыполнение проверки поля типа
2) учет не всех случаев в переключателе.
10.2. Виртуальные функции
Если класс base содержит виртуальную (virtual, 7.1.2) функцию vf, а класс derived,
порожденный из класса base, также содержит функцию vf того же типа, то
обращение к vf для объекта класса derived вызывает derived::vf (даже при доступе
через указатель или ссылку на base). В таком случае говорят, что функция
производного класса подменяет (override) функцию базового класса. Если, однако,
типы этих функций различны, то функции считаются различными и механизм
виртуальности не включается.
Виртуальные функции преодолевают те сложности, которое несет решение с
использованием полей типа, позволяя программисту описывать в базовом классе
функции, которые можно переопределять в производном классе. Пример:
class employee {
char* name;
short department;
// . . .
employee* next;
static employee* list;
public:
employee(char* n, int d);
virtual void print() const;
};
Здесь ключевое слово virtual указывает, что могут быть разные варианты функции
print() для разных производных классов, и что поиск подходящей из них является
задачей компилятора. Виртуальная функция базового класса должна быть описана
либо объявлена "чистой" (pure, 10.3). Пример:
void employee::print() const
{
cout << this->name << '\t' << this->department << '\n';
}
Виртуальная функция может, таким образом использоваться даже в том случае,
когда нет производных классов от ее класса. В производном классе, в котором не
96
нужен специальный ее вариант, ее задавать нет необходимости. Но если нужен
специальный вариант, он задается, например:
class manager : public employee {
employee* group // подчиненные люди
short level;
// уровень
// . . .
public:
manager(char*, int l, int d);
// . .
void print() const;
};
void manager::print() const
{
cout << this->name << '\t' << this->department << '\n';
cout "уровень" << this->level << '\n';
}
Функция print_employee() теперь не нужна, поскольку ее место заняли функции
print(). Список служащих теперь может быть распечатан следующим образом:
void employee::print_list()
{
for (employee* p = list; p; p = p->next) p->print();
}
Каждый служащий будет теперь печататься в соответствии со своим типом.
Например,
int main()
{
employee e("J. Brown", 1234);
manager m("J. Smith", 2, 1234);
employee::print_list();
}
выдаст
J. Brown 1234
уровень 2
J. Smith 1234
Явное уточнение операцией области действия подавляет механизм виртуальных
функций. Пример:
97
class B {public: virtual void f();};
class D public B {public: virtual void f();};
void D::f() { /* . . . */ B::f();}
Здесь при исполнении f из D реально произойдет вызов B::f(), а не D::f(), чтобы
избежать рекурсии.
10.2.1. Виртуальные базовые классы
Иногда возникает необходимость в тесной связи "братских" (т.е. потомков одного
прародителя) базовых классов для распределении информации между ними. Для
этого служит механизм виртуальных (virtual) базовых классов. Добавление к
описателю базового класса ключевого слова virtual приводит к тому, что
единственный объект виртуального базового класса используется каждым его
производным классом. Рассмотрим пример построения окон. Определим сначала
базовый класс:
class window {
// начальное наполнение
virtual void draw();
};
Для простоты мы рассматриваем только один компонент класса window - функцию
draw(). Другие окна могут быть построены как производные классы от window.
Каждый из них определяет свою собственную (детально разработанную) версию
этой функции, например:
class window_w_border :
public virtual window{
// наполнение границы
void draw();
};
class window_w_menu :
public virtual window{
// наполнение меню
void draw();
};
// окно с границами
// окно с меню
Теперь мы можем определить окно с границами и меню:
class window_w_border_and_menu :
public window_w_border,
98
public window_w_menu {
void draw();
};
Каждый такой производный класс дополняет окно некоторыми новыми
свойствами. Чтобы использовать их в сочетании друг с другом, мы должны
гарантировать, что в обоих случаях вхождения класса window в его производные
классы используется один и тот же объект класса window. Именно этого мы и
достигли, когда специфицировали window как виртуальный базовый класс для
производных окон: объект класса window_w_border_and_menu содержит только
один объект класса window
Далее мы должны запрограммировать различные функции draw(). Попробуем
сделать это самым простым способом, что неизбежно приведет к проблемам:
void window_w_border::draw()
{
window::draw();
// нарисовать границы
};
void window_w_menu::draw()
{
window::draw();
// нарисовать меню
};
До сих пор все нормально, поскольку все идет по образцу простого наследования.
Однако на следующем уровне обнаруживаются проблемы:
void window_w_border_and_menu::draw()
{
window_w_border::draw();
window_w_menu::draw();
// действия, характерные для window_w_border_and_menu
};
Данная функция будет вызывать window::draw() дважды, что может вызвать
искажения на экране. Чтобы избежать этого надо вернуться на шаг назад и
отделить работу, выполняемую базовым классом, от работы, выполняемой
производными классами. Для этого снабдим каждый класс двумя функциями:
1) функцией _draw(), делающей только то, что требует данный класс и
2) функцией draw(), делающей и то, что требуется в ее классе, и то, что требуется
в базовых классах.
99
class window {
// начальное наполнение
void _draw();
virtual void draw() { _draw();};
};
class window_w_border :
public virtual window{
// окно с границами
// наполнение границы
void _draw();
void draw() { window::_draw(); _draw(); };
};
class window_w_menu :
public virtual window{
// окно с меню
// наполнение меню
void _draw();
void draw() { window::_draw(); _draw(); };
};
Различие с предыдущем вариантом становится очевидным на следующем этапе:
class window_w_border_and_menu :
public window_w_border,
public window_w_menu {
void _draw();
void draw();
};
;
void window_w_border_and_menu::draw()
{
window::_draw()
window_w_border::_draw();
window_w_menu::_draw();
_draw(); // действия, характерные для window_w_border_and_menu
};
Теперь функция window::_draw() вызывается только один раз. Заметим, что класс
window используется как хранилище информации, используемой и
window_w_border, и window_w_menu.
10.3. Абстрактные классы
100
Механизм абстрактных классов служит для представления общих понятий, как,
например, фигура, которые фактически используются лишь для порождения более
конкретных понятий; например из фигуры порождается окружность или квадрат.
Абстрактный класс можно также употреблять как определение интерфейса, для
которого производные классы обеспечивают разнообразие реализаций.
Абстрактный класс - это класс, который может использоваться только лишь в
качестве базового для некоторого другого класса; никакие объекты абстрактного
класса не могут создаваться иначе как подобъекты объектов его порожденного
класса. Это видно хотя бы из того, что для виртуальных функций абстрактного
класса нельзя дать разумного определения. Пример:
class shape {
// . . .
public:
virtual void rotate(int) {error(shape::rotate); };
virtual void draw() {error(shape::draw); };
};
Наивная попытка создать неопределенную фигуру:
shape s;
// глупо: бесформенная фигура
бессмысленна.
Класс является абстрактным, если он содержит хотя бы одну чисто виртуальную
функцию (pure virtual function), т.е. функцию, в объявлении которой задан чистый
спецификатор "=0" (9.2). Пример:
class shape {
// . . .
public:
virtual void rotate(int) = 0; // чисто виртуальная функция
virtual void draw()= 0; // чисто виртуальная функция
};
Попытка создания объекта такого класса, например:
shape s;
// ошибка: переменная абстрактного класса
ведет к ошибке. Пример использования абстрактного класса:
class circle: public shape {
int radius;
public:
101
void rotate(int) {};
void draw();
circle(point p, int r);
// все в порядке: замещает shape::rotate
// все в порядке: замещает shape::draw
};
Чисто виртуальная функция, не определенная в производном классе, остается чисто
виртуальной функцией, так что производный класс будет абстрактным. Это
позволяет строить реализацию по шагам:
class X {
public:
virtual void f() = 0;
virtual void g() = 0;
};
X b;
// ошибка: объявление объекта абстрактного класса Х
class Y: public X {
void f(); // замещает X::f
};
Y b;
// ошибка: объявление объекта абстрактного класса Y
class Z: public Y {
void g();
};
Z c;
// замещает X::g
// OK
102
11. Управление доступом к компонентам классов
Компонент класса может быть
1) скрытым (private); это означает, что его имя может употребляться только внутри
его класса и друзьями его класса;
2) защищенным (protected); это означает, что его имя может употребляться только
внутри его класса, друзьями его класса и в производных классах;
3) открытым (public); это означает, что его имя может употребляться везде.
При таком подходе компоненты класса (функции, данные и т.д.) по их
возможностям использования делятся на три категории:
1) компоненты, реализующие интерфейс самого класса;
2) компоненты, реализующие интерфейс производного класса, и
3) другие компоненты.
Компоненты класса, объявленного с ключевым словом class, являются по
умолчанию скрытыми. Компоненты класса, объявленного с ключевым словом
struct или union, являются по умолчанию открытыми. Пример:
class X {
int a; // "а" скрытый компонент класса
};
struct S {
int a; // "а" открытый компонент класса
};
11.1 Спецификаторы доступа
Спецификатор-доступа определяет права доступа к компонентам класса,
следующим за ним до конца объявления класса или до следующего
спецификатора-доступа, например:
class X {
int a; // "а" скрытый компонент класса по умолчанию
public:
int b; // X::b - открытый
int c;; // X::с - открытый
};
Допускается любое число спецификаторов доступа в произвольном порядке:
struct S {
103
int a; // "а" открытый компонент по умолчанию
protected: int b;
private: int c;
public int d;
};
11.2. Спецификаторы доступа для базовых классов
Если класс объявляется базовым для некоторого другого класса посредством
спецификатора доступа public, то открытые компоненты базового класса
становятся открытыми компонентами и производного класса, а защищенные защищенными. Если же класс объявляется базовым для некоторого другого класса
посредством спецификатора доступа protected, то открытые и защищенные
компоненты базового класса становятся защищенными компонентами
производного класса. Скрытые же компоненты базового класса становятся
недоступными для производного класса, если только они не объявлены в базовом
классе как friend. Если же класс объявляется базовым для некоторого другого
класса посредством спецификатора доступа private, то открытые и защищенные
компоненты базового класса становятся скрытыми компонентами производного
класса. Скрытые же компоненты базового класса становятся недоступными для
производного класса, если только они не объявлены в базовом классе как friend.
При отсутствии спецификатора-доступа при базовом классе подразумевается
public, если производный класс объявлен со struct, и подразумевается private, если
производный класс объявлен с class. Примеры:
class B { /* . . . */};
class D1 : private B { /* . . . */};
class D2 : public B { /* . . . */};
class D3 : B { /* . . . */}; // "В" скрыт по умолчанию
struct D4 : B { /* . . . */}; // "В" открыт по умолчанию
Рассмотрим теперь следующие классы:
class X { public int a; // . . .
};
class Y1: public X {};
class Y2: protected X {};
class Y3: private X {};
Так как Х - это открытый базовый класс для Y1, то любая функция может (неявно)
преобразовывать указатель типа Y1* к Х* в точности так же, как она может
получить доступ к открытым компонентам класса Х.
104
Так как Х - это защищенный базовый класс для Y2, то только компоненты и друзья
класса Y2, а также компоненты и друзья классов, производных от Y2, могут
(неявно) преобразовывать указатель типа Y2* к Х* в точности так же, как они могут
получить доступ к открытым и защищенным компонентам класса Х.
Так как Х - это скрытый базовый класс для Y3, то только компоненты и друзья
класса Y3 могут (неявно) преобразовывать указатель типа Y2* к Х* в точности так
же, как они могут получить доступ к открытым и защищенным компонентам класса
Х.
Объявление базового класса скрытым не влияет на доступ к статическим
компонентам базового класса.
11.3. Объявления доступа
Доступ к компоненту базового класса в производном классе можно
скорректировать, упомянув его уточненное-имя в открытой или защищенной части
объявления производного класса. Это называется объявлением доступа (access
declaration). Пример:
class B {
int a;
public:
int b, c;
int bf();
};
class D : private B {
int d;
public:
B::c; // доступ к 'B::c'
int e;
int df();
};
int ef(D&);
Здесь функция ef может использовать лишь имена с, е и df. Являясь компонентом
класса D, функция df может использовать имена b, c, bf, d, e и df, но не а. Являясь
компонентом класса, функция bf может использовать имена а, b, c и bf.
Объявление доступа не может изменять права доступа к компоненту базового
класса по отношению к объявленным в нем. Пример:
class B {
105
public:
int a;
private:
int b;
protected:
int c;
};
class D : private B {
public:
B::a; // делает "а" открытым компонентом класса D
B::b; // ошибка: попытка усилить доступ
protected:
B::c; // делает "с" защищенным компонентом класса D
B::a; // ошибка: попытка ослабить доступ
};
Доступ к компоненту базового класса не может быть скорректирован в
производном классе если в нем объявляется компонент с тем же именем. Пример:
class X {
public void f();
};
class Y : private X {
public:
void f(int);
X::f; // ошибка: два объявления f
};
11.4. Друзья класса
Друг (friend) класса - это функция, которая не является компонентом класса, но
которой разрешается использовать его защищенные и скрытые компоненты. Друга
класса нельзя вызвать посредством операции доступа к компоненту класса, кроме
случая, когда друг является компонентом другого класса. Пример:
class X {
int a;
friend void friend_set(X*, int);
public void member_set(int);
};
void friend_set(X* p, int i) {p->a = 1;};
void X::member_set(int i) {a = i};
106
void f
{
X obj;
friend_set(&obj, 10);
obj.member_set(10);
};
Механизм друзей важен потому, что функция может быть другом двух классов и
потому - гораздо эффективнее обычной функции, оперирующей объектами обоих
классов. Пример:
class matrix;
class vector {
float v[4];
// . . .
friend vector multiply(const matrix&, const vector&);
};
class matrics {
vector v[4];
// . . .
friend vector multiply(const matrix&, const vector&);
};
Мы можем теперь написать функцию умножения, которая использует элементы
векторов и матриц непосредственно:
vector multiply(const matrix& m, const vector& v)
{
vector r;
for (int i = 0; i < 3; i++) { // r[i] = m[i] * v
r.v[i] = 0;
for (int j = 0; j < 3; j++) r.v[i] += m.v[i][j] * v.v[j];
};
return r;
};
Компонентная функция одного класса может быть другом другого:
class X {
// . . .
void f();
};
107
class Y {
// . . .
friend void X::f();
};
Сразу все компонентные функции класса Х могут быть объявлены друзьями класса
Y одним объявлением с уточненным-спецификатором-типа (9.1):
class Y {
// . . .
friend class X;
};
Функция, первое объявление которой содержит спецификатор friend, считается
также внешней (3.3, 7.1.1). Из сказанного следует:
static void f() { /* . . . */};
class X { friend g(); }; // подразумевает и extern g()
class Y {
friend void f();
};
// верно: f() имеет теперь внутренне связывание
static g() { /* . . . */}; // ошибка: несовместимое связывание
11.5. Доступ к защищенным компонентам класса
Дружественная функция или компонентная функция производного класса может
иметь доступ к защищенному статическому компоненту базового класса.
Дружественная функция или компонентная функция производного класса D может
иметь доступ к защищенному нестатическому компоненту базового класса только
по указателю или ссылке на D либо через объект класса D (или любого
производного от него). Пример:
class B {
protected int i;
};
class D1: public B {
};
class D2: public B {
friend void fr(B*, D1*, D2*);
108
void mem(B*, D1*);
};
void fr(B* pb, D1* p1, D2* p2)
{
pb->i = 1;
// запрещено
p1->i = 2;
// запрещено
p2->i = 3;
// верно: доступ через D2
};
void D2::mem(B* pb, D1* p1)
{
pb->i = 1;
// запрещено
p1->i = 2;
// запрещено
i = 1; // верно: доступ через "this"
};
void g(B* pb, D1* p1, D2* p2)
{
pb->i = 1;
// запрещено
p1->i = 2;
// запрещено
p2->i = 3;
// запрещено
};
11.6. Доступ к виртуальным функциям
Права доступа к виртуальной функции определяются ее объявлением и не
заменяются на права доступа к функциям, которые позднее подменяют ее. Пример:
class B {
public virtual f();
};
class D : public B {
private f();
};
void f()
{
D d;
B* pb = &d;
D* pd = &d;
pb->f();
// верно: B::f() - открытая функция
pd->f();
// ошибка: D::f() - скрытая функция
};
109
11.7. Множественный доступ
Если некоторое имя может быть достигнуто в графе множественного наследования
по нескольким путям, выбирается наибольший из возможных доступов. Пример:
class W {public: void f();};
class A: private virtual W {};
class B: public virtual W {};
class C: publicA, public B{
void f() {W::f();}
// верно
};
Так как W::f() доступна из С::f() вдоль открытого пути через В, такой доступ
возможен.
110
12. Специальные компонентные функции
Некоторые компонентные функции являются специальными в том смысле, что
влияют на создание, копирование и уничтожение объектов класса или задают
способ приведения значений к значениям других типов. Часто такие функции
вызываются неявно.
Специальные компонентные функции подчиняются стандартным правилам доступа
(11). Например, обявление конструктора объектов защищенным гарантирует, что
создание объектов с его участием возможно только в производных классах и
друзьях.
12.1. Конструкторы
Компонентная функция с тем же именем, что и у класса, называется
конструктором; она используется для создания значений объектов этого класса.
Пример:
class date {
// . . .
date(int, int, int);
};
Если у класса есть конструктор, каждый его объект будет инициализирован до
какого бы ни было его использования. Это защищает программиста от работы с
неинициализированными или многократно инициализированными объектами.
Конструктор вызываеся неявно с возможными аргументами при создании объекта
данного класса. Пример:
date xmas(25, 12, 0);
date today; // ошибка, пропущена инициализация
Можно задать несколько способов инициализации объектов, описав несколько
конструкторов, например:
class date {
int month, day, year;
public:
// . . .
date(int, int, int); // день, месяц, год
date(int, int);
// день и месяц текущего года
date(int);
// день, месяц и год текущие
date();
// дата по умолчанию: сегодня
date(const char*); // дата в строковом представлении
};
111
Конструкторы подчиняются тем же правилам относительно типов параметров, что
и функции с совмещенными именами, поэтому компилятор может выбрать нужный
конструктор при каждом его использовании, например:
date today(4);
date july4("July 4, 1983");
date now;
// инициализируется по умолчанию
Конструктор не может быть объявлен как константный или подвижный, не может
быть виртуальным и статическим. Конструкторы не наследуются.
Конструктор копирования класса Х - это конструктор, который может быть вызван
для копирования объекта класса Х, т.е. такой конструктор, у которого один из
параметров имеет тип X&. Пример:
class X {
// . . .
public
X(int);
X(const X&, int = 1);
};
X a(1);
X b(a, 0);
X c = b;
// вызывается X(int)
// вызывается X(const X&, int)
// вызывается X(const X&, int)
Конструктор не должен возвращать значение, поэтому оператор return в теле
конструктора не может сопровождаться значением. Нельзя брать адрес
конструктора.
Конструктор может быть вызван и явным образом в соответствии со следующим
синтаксисом:
имя-класса (список-выражений opt)
Пример:
complex zz = complex(1, 2.3);
cprint(complex(7.8, 1.2));
12.3. Преобразования
Преобразования (изменения типа) объектов классов выполняются конструкторами
и преобразующими функциями. Такие преобразования, называемые
112
пользовательскими (user-defined), часто применяются в дополнение к стандартным
преобразованиям (4).
12.3.1. Преобразование посредством конструктора
Конструктор с одним параметром задает преобразоание типа своего параметра к
типу своего класса. Пример:
class X {
// . . .
public
X(int);
X(const char*, int = 0);
};
void f(X arg) {
X a = 1;
X b = "Jessie";
a = 2;
f(3);
};
// a =X(1)
// b = X("Jessie", 0)
// a = X(2)
// f(X(3))
12.3.2. Преобразующие функции
Использование конструктора для задания преобразования типа удобно, но имеет
нежелательные следствия:
1) неявное преобразование из пользовательского типа в базовый тип невозможно;
2) нельзя задать преобразование из нового типа в существующий;
3) невозможно, определив конструктор с одним параметром, не получить при этом
преобразования.
Эти проблемы решаются посредством использования преобразующих функций.
Компонентная функция класса Х, имя которой имеет вид
имя-функции-приведения:
operator имя-приведенного-типа
имя-приведенного-типа:
список-спецификаторов-типа ptr-операция opt
определяет преобразование из Х в тип, заданный именем-приведенного-типа.
Пример:
class X {
// . . .
113
public:
operator int();
};
void f(X a)
{
int i = int(a);
i = (int)a;
i = a;
};
Во всех трех случаях присваиваемое значение будет преобразовано посредством
функции X::operator int(). Операции преобразования наследуются. Пример:
struct S {
operator int();
};
struct SS: public S {};
void f()
{
SS a;
int i = a;
};
// i = a.S::operator int()
Пользовательские преобразования неявно применяются лишь в случае, когда они
однозначны (неоднозначность может возникнуть как из-за выбора между
пользовательскими преобразованиями, так и между полбзовательским и
стандартным преобразованием).
12.4. Деструкторы
Деструктор служит для уничтожения объекта в памяти, когда исчезает
необходимость в нем. Если Х - имя класса, тогда ~X - имя деструктора этого
класса. Деструкторы полезны для освобождения памяти, выделяемой
конструктором для объекта в свободной памати. Вот пример стека (без обработки
ошибок):
class char_stack {
int size;
char* top;
char* s;
public:
char_stack(int sz) { top = s = new char[size = sz]; };
114
~char_stack() { delete[] s; } // деструктор
void push(char c) {*top++ = c; }
char pop() {return *--top;}
};
Когда объект типа char_stack исчезает из области видимости, вызывается
деструктор:
void f()
{
char_stack s1(100);
char_stack s2(200);
s1.push('a');
s2.push(s1.pop());
char ch = s2.pop();
cout << ch << '\n';
};
При вызове функции f() вызывается конструктор char_stack, чтобы выделить
массив из 100 символов для объекта s1 и массив из 200 символов для объекта s2.
При возврате из f() оба этих массива будут освобождены.
Деструктор не может быть объявлен как константный или подвижный и не может
быть статическим. Деструкторы не наследуются. Однако деструктор может быть
виртуальным.
12.5. Конструкторы и деструкторы
Конструкторы и деструкторы применяются к объектам, создаваемым следующим
образом.
1. Автоматический объект: создается каждый раз, когда его объявление встречается
при выполнении программы, и уничтожается каждый раз при выходе из блока, в
котором оно появилось.
2. Статический объект: создается один раз при запуске программы и уничтожается
один раз при ее завершении.
3. Объект в свободной памяти: создается операцией new и уничтожается операцией
delete.
4. Компонентный объект: компонент объекта другого класса или элемент массива.
В следующих подразделах предполагается, что объекты являются элементами
класса, имеющего конструктор и деструктор, например:
115
class table {
name* tb1;
int size;
public:
table(int sz = 15);
~table();
name* look(char*, int = 0);
name* insert(char* s) { return look(s, 1) }
};
12.5.1. Локальные переменные
Деструкторы локальных переменных выполняются в порядке, обратном порядку их
создания. Пример:
void f(int i)
{
table aa;
table bb:
if (i > 0) { table cc; /* . . . */ };
// . . .
};
В этом примере каждый раз при вызове функции f() создаются переменные:
сначала аа, затем - bb, которые каждый раз уничтожаются по выходе из f() в таком
порядке: сначала bb, затем - аа. Если i > 0, то, кроме того, последней создается и
первой уничтожается переменная сс.
12.5.2. Статическая память
Пример:
table tb1(100);
void f(int i)
{
static table tb2(200);
};
int main()
{
f(200);
// . . .
};
116
Здесь конструктор table() будет вызываться дважды: для tb1 и tb2. Деструктор
~table() также будет вызываться дважды: для уничтожения tb1 и tb2 после выхода
из main(). Конструкторы для глобальных статических объектов в файле
выполняются в порядке объявления переменных; деструкторы вызываются в
обратном порядке. Конструктор локального статического объекта вызывается, как
только поток управления достигнет описания объекта.
Вызов конструкторов и деструкторов для статических объектов играет в С++ очень
важную роль, так как это очень удобный способ обеспечить надлежащую
инициализацию и очистку данных в библиотеках.
12.5.3. Свободная память
Рассмотрим:
main()
{
table p* = new table(100);
table q* = new table(200);
delete p;
delete p; // возможна ошибка при исполнении
};
Конструктор и деструктор вызываются дважды, при этом q не уничтожается, а p
уничтожается дважды. Обычно, то что объект не уничтожается, не является
ошибкой, а лишь бесполезной тратой памяти. Двойное уничтожение p будет, как
правило, серьезной ошибкой (обычно это приводит к бесконечному циклу в
подпрограмме управления свободной паматью).
12.5.4. Компонентные объекты класса
Рассмотрим:
class classdef {
table members;
int no_of_members;
// . . .
classdef(int size);
~classdef();
};
Цель, очевидно, состоит в том, что объект класса classdef должен содержать
таблицу элементов длиною size, а проблема состоит в вызове конструктора table() с
параметром size. Сделать это можно так:
117
classdef::classdef(int size) : members(size)
{
no_of_memebrs = size;
// . . .
};
Параметры для конструктора компонента (здесь это table::table()) помещаются в
описание конструктора объемлющего класса (здесь это classdef::classdef()). При
этом конструктор компонента вызывается перед телом основного конструктора
согласно следующему синтаксису:
ctor-инициализатор:
: список-инициализаторов-компонентов
список-инициализаторов-компонентов:
инициализатор-компонента
инициализатор-компонента, список-инициализаторов-компонентов
инициализатор-компонента:
полное-имя-класса (список-выражений opt)
идентификатор (список-выражений opt)
Идентификатор в инициализаторе-компонента - это идентификатор
инициализируемого компонента. Если тип этого компонента не класс, выражение в
скобках поставляет инициализирующее значение для компонента, в противном
случае вызывается конструктор соответствующего класса. Если в классе есть
несколько компонентов, которым нужны списки параметров для конструкторов,
для них вызываются соответствующие конструкторы. Пример:
class classdef {
table members;
table friends;
int no_of_members;
// . . .
classdef(int size);
~classdef();
};
classdef::classdef(int size) : members(size), friends(size), no_of_memebrs(size)
{ //
};
Оба конструктора members и friends вызываются с параметром size.
Если конструктору компонента не нужно параметров, соответствующий
инициализатор-компонента может опускаться:
118
classdef::classdef(int size) : members(size)
{
no_of_memebrs = size;
// . . .
};
Поскольку table::table() был определен с параметром 15, размер таблицы friends
будет равен 15.
Когда объект, содержащий другие объекты, уничтожается, первым исполняется его
деструктор, а затем - деструкторы компонентов в порядке, обратном порядку их
объявления. Рассмотрим альтернативу компонентным объектам класса: указатели
на объекты, инициализируемые в конструкторе.
class classdef {
table* members;
table* friends;
int no_of_members;
// . . .
classdef(int size);
~classdef();
};
classdef::classdef(int size)
{
memebrs = new table(size);
friends = new table; // размер таблицы по умолчанию
no_of_memebrs = size;
// . . .
};
Так как таблицы созданы посредством new, они должны уничтожаться посредством
delete:
classdef::~classdef()
{
// . . .
delete members;
delete friends;
};
12.5.5. Массивы объектов класса
119
Чтобы объявить массив объектов класса, имеющего конструктор, этот класс
должен иметь конструктор по умолчанию, который может вызываться без списка
параметров, так как нет способа задания параметра конструктора в объявлении
массива. Например,
table tb1[10]
создает массив из 10 объектов типа table, каждый из которых инициализируется
вызовом конструктора table::table(15).
Когда массив уничтожается, деструктор должен вызываться для каждого его
элемента. Для автоматических массивов это делается неявно при выходе из блока.
Для массивов в свободной памяти уничтожение должно быть указано явным
образом. Пример:
void f(int sz)
{
table* t1 = new table;
table* t2 = new table[sz];
// . . .
delete t1;
delete[] t2;
};
12.5.6. Конструкторы и деструкторы производных классов
В конструкторе производного класса вызываются конструкторы его базовых
классов с соответствующими параметрами. Пример:
class employee {
char* name;
int department;
// . . .
public:
employee(char* n, int d);
// . . .
};
class manager: public employee {
int level, group;
// . . .
public:
manager(char* n, int d, l, g);
// . . .
};
120
Параметры конструктора базового класса указываются в описании конструктора
производного класса. В этом отношении базовый класс рассматривается как
компонент производного класса:
manager :: manager((char* n, int d, l, g) : employee(n, d), level(l), group(g) {};
Объекты производного класса конструируются снизу вверх: сначала объекты
базовых классов, затем компоненты, а потом сам производный класс;
уничтожаются они в обратном порядке.
121
13. Совмещение имен
Если в некоторой области действия имеется несколько различных объявлений
функций с одним именем, это имя называется совмещенным (overloaded). Когда
употребляется такое имя, нужная функция выбирается путем сопоставления типов
фактических и формальных параметров. Пример:
double abs(double);
int abs(int);
abs(1);
abs(1.0);
// вызывается abs(int)
// вызывается abs(double)
Функции, типы которых различаются только типом результата, не могут иметь
совмещенного имени.
Тип, описанный посредством typedef, является лишь синонимом указанного типа и
потому функции, типы которых различаются только "типами по typedef", не могут
иметь совмещенного имени. Пример:
typedef int Int;
void f(int i) { /* . . . */ }
void f(Int i) { /* . . . */ }
// ошибка: повторное описание f
13.1. Отождествление объявлений
Два объявления функции с одним и тем же именем относятся к одной функции,
если они лежат в одной области действия и имеют идентичные типы параметров.
Компонентная функция производного класса лежит в иной области действия,
нежели одноименная функция базового класса. Пример:
class B {
public int f(int);
};
class D : public B {
public int f(char*);
};
Здесь D::f(char*) скорее закрывает доступ к B::f(int), чем использует совмещенное
имя.
13.2. Отождествление параметров
122
При вызове функции с данным именем необходимо выбрать ту, для которой
фактические параметры подходят наилучшим образом. Для этого часто приходится
делать преобразования типов параметров.
При отождествлении фактического параметра не рассматриваются
последовательности преобразований, содержащие более одного пользовательского
преобразования, как и те, что могут быть сокращены удалением одного или
нескольких преобразований. Такая последовательность называется
последовательностью лучшего отождествления (best matching sequence). Например,
int->float->double не последовательность лучшего отождествления, так как она
содержит более короткую последовательность int->double
Следующие тривиальные преобразования над типом Т не влияют на то, какая из
двух последовательностей преобразований лучше:
ИЗ
T
T&
T[]
T(параметры)
T
T
T*
T*
T&
T&
В
T&
T
T*
T*(параметры)
const T
volatile T
const T*
volatile T*
const T&
volatile T&
Последовательности преобразований, различающиеся только порядком
преобразований, не различаются. Используются следующие правила:
1. Точное отождествление: последовательности из нуля или более тривиальных
преобразований лучше, чем любые другие последовательности. Из них те, которые
не преобразуют Т* к const T*, T* к volatile T*, T& к const T& и T& к volatile T&,
лучше. Примеры:
void f(char);
void f(int);
void f(unsigned);
void f(long);
void g()
{
f('c');
f(1u);
f(1);
// вызов f(char)
// вызов f(unsigned)
// вызов f(int)
123
f(1L);
// вызов f(long)
}
Можно описывать функции, различающиеся лишь словом const в указателе или
ссылке:
void f(char*);
void f(const char*);
void g(char* pc, const char* pcc)
{
f(pc);
// вызов f(char*)
f(pcc);
// вызов f(const char*)
}
2. Отождествление при помощи расширений: из последовательностей, не
названных в [1], те, которые содержат лишь целочисленные расширения (4.1).
void f(int);
void f(double);
void g()
{
short aa = 1;
float ff = 1.0;
f(aa);
// вызов f(int)
f(ff); // вызов f(double)
}
Без этого правила вызовы были бы неоднозначными, так как short может быть
преобразовано и в int и в double, как и float может быть преобразовано и в int и в
double.
3. Отождествление при помощи стандартных преобразований: из
последовательностей, не названных в [2], те, которые содержат стандартные (4.14.8) и тривиальные преобразования, лучше других. Из них, если В - открытый
прямой или косвенный производный от А, то преобразование B* в A* лучше, чем в
void* или const void*; кроме того, если С - открытый прямой или косвенный
производный от В, то преобразование С* в В* лучше, чем в A*, и преобразование
С& в В& лучше, чем в A&.
Это правило гарантирует, что все неоднозначности между стандартными
преобразованиями обнаруживаются и все стандартные преобразования считаются
эквивалентными. Пример:
124
void f(char);
void f(float);
void g()
{
f(1);
f(1L);
}
// неоднозначность: f(char) или f(float)
// неоднозначность: f(char) или f(float)
4. Отождествление посредством преобразований, определенных пользователем: из
последовательностей, не названных в [3], те, которые включают только
пользовательские преобразования (12.3), стандартные преобразования (4) и
тривиальные преобразования, лучше других.
5. Отождествление по многоточию: последовательности, содержащие
отождествление с многоточием, хуже всех других.
13.3. Адрес функции с совмещенным именем
Употребление имени функции без параметров приводит к выбору той
(единственной) среди функций с данным именем из текущей области действия,
которая точно соответствует цели. Целью может быть:
инициализируемый объект (8.4);
левая часть присваивания (5.17);
формальный параметр функции (5.2.2);
тип возвращаемого функцией значения (8.2.5).
Примеры:
int f(double);
int f(int);
int (*pdf)(double) = &f;
int (*pfi)(int) = &f;
13.4. Совмещенные знаки операций
Большинство операций могут иметь совмещенные знаки операций.
имя-функции-операции:
operator операция
операция: одна из
new delete
+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> <<= >>=
!= <= >= && || ++ -- , ->* -> () []
125
Следующие знаки операций не могут быть совмещены: . .* :: ?:
Функции-операции (operator functions) обычно вызываются не прямо, а для
выполнения операций. К ним, однако, можно обратиться и явно, например:
complex z = a.operator+(b);
// complex z = a+b;
Вызов функции
первичное-выражение (список-выражений opt)
рассматривается как бинарная операция с первичным-выражением в качестве
первого операнда и возможным списком-выражений - в качестве второго. Таким
образом, фраза х(пар1, пар2) понимается как вызов x.operator()(пар1, пар2).
Индексация
первичное-выражение [выражение]
также рассматривается как бинарная операция. Поэтому выражение x[y]
интерпретируется как x.operator[](y).
126
14. Шаблоны
Шаблон класса (class template) определяет данные и операции потенциально
неограниченного множества родственных типов. Шаблон функции (function
template) определяет потенциально неограниченное множество родственных
функций.
14.1. Шаблоны
объявление-шаблона:
template <список-параметров-шаблона> объявление
список-параметров-шаблона:
параметр-шаблона
список-параметров-шаблона, параметр-шаблона
параметр-шаблона:
типовый-параметр
объявление-параметра
типовый-параметр:
class идентификатор
Объявление в объявлении шаблона должно объявлять или описывать класс или
функцию.
Типовый-параметр объявляет свой идентификатор в области действия описания
шаблона как имя-типа.
Имена шаблонов подчиняются обычным правилам области действия и управления
доступом.
Объявление-шаблона является объявлением и может быть только глобальным.
14.2. Шаблоны классов
Шаблон класса задает способ построения отдельных классов. Например, шаблоны
класса vector можно объявить примерно так:
template <class T> class vector {
T* v;
int sz;
public
vector(int);
T& operator [](int);
T& elem(int i) {return v[i]; };
// . . .
};
127
Префикс template <class T> указывает, что объявляется шаблон с типовым
параметром Т. Теперь посредством имени-класса-по-шаблону можно образовать
конкретный класс:
имя-класса-по-шаблону:
имя-шаблона <список-факт-пар-шаблона>
список-факт-пар-шаблона:
фактический-параметр-шаблона
список-факт-пар-шаблона, фактический-параметр-шаблона
фактический-параметр-шаблона:
выражение
имя-типа
Имя-класса-по-шаблону является именем-класса (9). Типы фактическихпараметров-шаблона в имени-класса-по-шаблону отождествляются с
соответствующими (по порядку) типами из списка-параметров-шаблона и должны
соответствовать им по количеству.
Другие фактические-параметры-шаблона должны быть константнымивыражениями, адресами объектов или функций с внешним связыванием или
статических компонентов класса (т.е. допускать статический контроль типов).
Пример:
vector<int> v1(20);
vector<complex> v2(30);
typedef vector<complex> cvec; // заведение синонима
cvec v3(40);
Класс, сгенерированный из шаблона (например vector<int>), называется
шаблонным классом (template class).
14.3. Эквивалентность типов
Два имени-класса-по-шаблону обозначают один класс, если совпадают их шаблоны
и их фактические параметры имеют совпадающие значения. Пример:
template <class E, int size> class buffer;
buffer<char, 2*512> x;
buffer<char, 1024> y;
buffer<char, 512> z;
объявляет x и y, в отличае от z, как переменные одного и того же типа, а
template <class T, void(*err_fct)()>
class list { /* . . . */};
128
list<int, &error_handler1> x1;
list<int, &error_handler2> x2;
list<int, &error_handler2> x3;
list<char, &error_handler1> x4;
объявляет x2 и x3 как переменные одного типа, отличного от типа переменных x1 и
x4.
14.4. Шаблоны функций
Шаблон функций определяет потенциально неограниченное множество функций,
генерируемых из шаблона. Семейство функций сортировки можно объявить,
например, следующим образом:
template <class T> void sort(vector<T>);
Параметры шаблона при генерации конкретной функции явным образом не
задаются, а выводятся из типов аргументов, поэтому шаблонная функция
рассматривается как множество функций с совмещенным именем. Пример:
viod f(vector<complex>& cv, vector<int>& ci)
{
sort(cv); // вызывается sort(vector<complex>)
sort(ci);
// вызывается sort(vector,int>)
}
Функция, сгенерированная из шаблона, называется шаблонной функцией (template
function). Шаблонная функция может совмещать имя с другими шаблонными и
обыкновенными функциями. Распознование функции в таком случае выполняется в
три шага:
1. Поиск обычной функции, параметры которой в точности соответствуют
требуемым; если таковая находится, обращение производится к ней.
2. Поиск шаблона функции, из которого может быть сгенерирована функция при
точном соответствии параметров; если она найдена, она вызывается.
3. Поиск обычной функции, типы формальных параметров которой лучше всего
соответствуют фактическим параметрам вызова; если она найдена, она вызывается.
Если отождествление не найдено, фиксируется ошибка. Пример:
template <class T> T max(T a, T b) {return a>b? a : b;};
129
void f(int a, int b, char c, char d)
{
int m1 = max(a, b); // max(int, int)
char m2 = max(c, d); // max(char, char)
int m3 = max(a, c); // ошибка: нельзя сгенерировать max(int, char)
Заметим, что при отождествлении с шаблоном не применяются даже тривиальные
преобразования и потому в последнем примере вызов не сводится к вызову
max(a, int(c)). Однако добавление
int max(int, int);
приведет к разрешению третьего вызова, поскольку теперь вызов max(a, c) будет
отождествлен с этой функцией после применения к параметру с стандартного
преобразования char в int.
Если на каком-либо этапе будет найдено больше одного подходящего определения,
также фиксируется ошибка.
Для генерации отдельных версий шаблона необходимо описание шаблона; для
порождения обращений к этим отдельным версиям достаточно объявления
шаблона.
Все параметры шаблона функции должны быть типовыми параметрами и каждый
из них должен быть использован в типах параметров. Пример:
template <class T> T* create(); // ошибка
14.5. Объявления и описания
Использование имени-класса-по-шаблону составляет объявление шаблонного
класса. Вызов шаблона функции или взятие его адреса является объявлением
шаблонной функции.
Никаких операций, требующих описания шаблона класса, не может быть
выполнено над шаблонным классом, пока компилятору не доступен сам шаблон
класса. После этого отдельный шаблонный класс считается описанным
непосредственно перед первым объявлением, его именующим.
14.6. Шаблоны компонентных функций
Компонентная функция шаблонного класса неявно оказывается шаблонной
функцией, параметрами шаблона которой являются параметры шаблона класса. На
пример,
130
template <class T> class vector {
T* v;
int sz;
public
vector(int);
T& operator[](int);
T& elem(int i) {return v[i]; };
// . . .
};
неявно объявляет три шаблона функции. Функция индексации могла бы быть
описана следующим образом:
template <class T> T& vector<T>::operator[](int i);
{
if (i < 0 || sz <= i) error("vector: выход за границу");
return v[i];
}
Параметр шаблона для vector<T>::operator[]() определяется вектором, к которому
применяется индексация. Пример:
vector<int> v1(20);
vector<complex> v2(30);
v1[3] = 7;
// vector<int>::operator[]()
v2[3] = complex(7,8);// vector<complex>::operator[]()
14.7. Друзья
Функция-друг шаблона не становится неявно шаблонной функцией. Пример:
template <class T> class task {
// . . .
friend void next_time();
friend task<T>* preemt(task<T>*);
friend task* prmt(task*);
// ошибка
};
Здесь next_time становится другом всех task классов, и каждый task имеет
соответствующего типа функцию preemt() в качестве друга. Функции preemt()
должны быть описаны как шаблон:
template <class T> task<T>* preemt(task<T>* t) { /* . . . */}
131
Объявление prmt(task*) ошибочно, потому что не существует типа task, а лишь
отдельные шаблонные типы task(int), task(compleх) и т.д.
14.8. Статические компоненты и переменные
Каждый шаблонный класс или функция, сгенерированные по шаблону, обладают
своими экземплярями всех статических переменных или компонентов. Пример:
template <class T> class X {
static T s;
// . . .
};
X<int> aa;
X<char*> bb;
Здесь X<int> имеет статический компонент s типа int, а X<char*> имеет
статический компонент s типа char*. В том же духе:
template <class T> f(T* p)
{
static T s;
// . . .
};
void g(int a, char* b)
{
f(&a);
f(&b)
}
Здесь f(int*) имеет статическую локальную переменную s типа int, а f(char**) имеет
статическую локальную переменную s типа char*.
132
15. Обработка исключительных ситуаций
15.1. Обработка ситуаций
Механизм обработки ситуаций дает способ передачи управления из точки
выполнения программы в расположенную выше по управлению точку, в которой
определена реакция на ситуацию (exception handler). Главная идея состоит в том,
что функция, сталкивающаяся с неразрешимой проблемой, объявит
исключительную ситуацию, в надежде на то, что вызвавшая ее (прямо или
косвенно) функция может решить проблему. Реакция будет вызвана только в
случае исполнения выражения-возбуждения-ситуации внутри так называемого
блока-с-контролем или в функциях, вызванных из этого блока.
блок-с-контролем:
try составной-оператор список-реакций
список-реакций:
реакция список-реакций opt
реакция:
catch (объявление-ситуации) составной-оператор
объявление-ситуации:
список-спецификаторов-типа описатель opt
список-спецификаторов-типа абстрактный-описатель
...
выражение-возбуждения-ситуации:
throw выражение opt
Блок-с-контролем является оператором (6). Выражение-возбуждения-ситуации
является унарным-выражением типа void (5). Выражение-возбуждения-ситуации
иногда еще называют точкой возникновения (возбуждения) ситуации (throw-point).
О части программы, в которой исполнилось выражение-возбуждения-ситуации,
говорят, что в ней возникла ситуация (она возбудила ситуацию); часть программы,
на которую вследствие этого передается управление, называется реакцией на
ситуацию (handler).
Рассмотрим, каким образом можно определить и обработать ошибки диапазона,
возникающие в классе Vector.
class Vector {
int* p;
int* sz;
public:
class Range { };
// класс ситуаций
int& operator[] (int i);
// . . .
};
133
Объекты класса Range предназначены для использования в качестве исключений и
возбуждать последние следующим образом:
int& Vector::operator[] (int i)
{
if (0<=i && i<sz) return p[i];
throw Range();
}
Функция, нуждающаяся в обнаружении выхода за диапазоны индекса, должна
поместить вызов функции в блок-с-контролем с реакцией на ситуацию. Например:
void f(Vector& v)
{
// . . .
try {
do_something(v);
}
catch (Vector::Range) {
// реакция на исключение типа Vector::Range
// так как do_something() вызвало ситуацию, делаем что-то другое,
// попадаем сюда только, если обращение к do_something() приводит
// к вызову Vector::operator[] (i) с плохим индексом
}
// . . .
}
Конструкция
catch ( /* . . . */) { }
представляет собой реакцию на ситуацию. Она может использоваться только сразу
за блоком-с-контролем или непосредственно за другой реакцией (если есть
несколько реакций разных типов).
Процесс возбуждения и перехвата исключения требует просмотра цепочки вызова
функций вверх от точки возникновения исключения, пока не будет найден try-блок
с соответствующей реакцией. Если таким образом не будет найдено подходящая
реакция, программа завершается аварийно.
15.2. Возбуждение ситуации
При возбуждении ситуации управление передается на реакцию. При этом подается
объект, и тип этого объекта определяет, какие реакции могут перехватить данную
ситуацию. Например,
134
throw "Help!";
может быть перехвачена некоторой реакцией на тип char*:
try {
// . . .
}
catch (const char* p) {
// здесь реакция на ситуацию типа литерной строки
}
а ситуация
class Overflow {
// . . .
public: Ovrflow(char, double, double);
};
void f(double x)
{
// . . .
throw Overflow('+', x, 3.45e107);
}
может быть перехвачена реакцией
try {
// . . .
f(1.2);
// . . .
}
catch (Overflow& oo) {
// здесь реакция на ситуацию типа Overflow
}
При возникновении ситуации управление передается на ближайшую реакцию
соответствующего типа; "ближайшая" - та, в чей блок-с-контролем управление
попало в последний раз; "соответствующий тип" определяется в 15.4.
Выражение-возбуждения-ситуации создает временный объект, тип которого
статически определяется операндом throw, и затем инициализирует им
переменную соответствующего типа, названную в реакции.
135
Операнд throw трактуется как параметр в вызове функции (5.2.2) или операнд
оператора return, за исключением ограничений отождествления типов, описанных
в 15.4.
Выражение-возбуждения-ситуации без операнда повторно возбуждает
обрабатываемую ситуаци. Выражение-возбуждения-ситуации без операнда может
появиться только в реакции или в функции, явно или неявно вызванной из реакции.
Например, код, который нужно выполнить при возникновении какой-либо
ситуации, не обрабатывая ее полностью, мог бы быть написан так:
try {
// . . .
}
catch (...) {
// перехват ситуации
// частичная обработка ситуации
throw;
// передача ситуации некоторой другой реакции
15.3. Конструкторы и деструкторы
Объект не считается созданным, пока не закончит работу его конструктор. Объект,
состоящий из подъобектов, считается созданным после создания всех его
подобъектов. Хорошо написанный конструктор должен убедиться в полноте и
правильности созданного объекта и в противном слугае восстановить состояние
системы, существовавшее до его вызова.
Исключения дают решение проблемы, как сообщать об ошибках в конструкторе.
Поскольку конструктор не возвращает отдельное значение, которое могло бы быть
проверено его вызывющим, можно предложить:
1) возвратить объект в плохом состоянии и доверить пользователю проверку
состояния;
2) присвоить "ошибочное" значение некоторой нелокальной переменной.
Исключения позволяют передать из констрктора информацию о неудаче при
создании объекта. Пример:
class Vector {
int* p;
int* sz;
public:
class Range { }; // класс ситуаций "ошибка индексации"
class Size { };
// класс ситуаций "неприемлемый размер"
Vector (int sz);
int& operator[] (int i);
// . . .
136
};
Vector::Vector(int sz)
{
if (sz<0 || max<sz) throw Size();
// . . .
}
Участок программы, создающий векторы, может теперь обрабатывать ошибки типа
Size, например:
Vector* f(int i)
{
Vector* p;
try { p = new Vector v(i); }
catch(Vector::Size) {
// обработка ошибок, вызванных плохим размером вектора
}
// . . .
return p;
}
При передаче управления из точки возникновения ситуации на реакцию для всех
автоматических объектов, построенных после входа в блок-с-контролем,
вызываются деструкторы.
Процесс вызова деструкторов для автоматических объектов, построенных на пути
от блока-с-контролем до выражения-возбуждения-ситуации, называется сверткой
стека.
15.4. Распознование ситуации
Реакция с типом T, const T, T& или const T& отождествляется для выражениявозбуждения-ситуации с объектом типа Е, если:
а) Т и Е один и тот же тип, или
б) Т - доступный (4.6) в точке возбуждения ситуации базовый класс для Е, или
в) Т - тип указателя и Е - тип указателя, который можно преобразовать в Т
стандартным преобразованием указателя (4.6) в точке возбуждения ситуации.
Пример:
class Matherr {/* . . . */ virtual vf(); };
class Overflow: public Matherr {/* . . . */ };
class Underflow: public Matherr {/* . . . */ };
class Zerodivide: public Matherr {/* . . . */ };
137
void f()
{
try { g() }
catch (Overflow oo) {
// . . .
}
catch (Matherr mm) {
// . . .
}
}
Здесь реакция на Overflow будет перехватывать ситуации типа Overflow, а реакция
на Matherr будет перехватывать ситуации типа Matherr, а также все открытые
производные типы от Matherr, включаяя Underflow и Zerodivide (кроме, конечно
Overflow).
Реакции блока-с-контролем проверяются на применимость в порядке их
написания. Ошибкой является расположение реакции на базовый класс перед
реакцией на его производный класс, потому что в таком случае реакция на
производный класс никогда не получит управления.
В объявлении-ситуации многоточие (. . .) играет примерно ту же роль, что и в
параметрах объявления функции: оно дает отождествление для любой ситуации.
Реакция с многоточием, если она есть, должна быть последней в ее блоке-сконтролем.
Если среди реакций блока-с-контролем подходящей не найдено, поиск реакции
прододжается в блоке-с-контролем, динамически охватывающем данный блок-сконтролем.
Если нигде не нашлось подходящей реакции, вызывается функция terminate (15.6).
Если возникает ситуация в реакции, она обрабатывается тем участком программы,
который вызвал данный блок-с-контролем.
15.5. Спецификация ситуаций
Возбуждение и перехват ситуации влияют на взаимодействие функции с другими
функциями. Язык предоставляет возможность перечислить множество ситуаций,
которые функция может прямо или косвенно возбудить, как часть объявления
функции. Спецификации ситуаций следует за описателем функции:
спецификация-ситуаций:
throw (список-типов opt)
список-типов:
138
имя-типа
список-типов, имя-типа
Пример: void f() throw (X, Y) { /* . . . */ }.
Здесь указывается, что f() может возбуждать исключения типов Х и У, а также
производных от них, но никакие другие.
Закон Мерфи для обработки ситуаций читается так: "Любая ситуация, которая
может возникнуть, возникнет"; так что подобное объявление функции следует
читать как "f() возбуждает Х и У".
Когда функция сообщат что-нибудь о ее искдючениях, это позволяет дать гарантию
вызывающей ее функции; если при исполнении функции делается попытка вызвать
ситуацию, не названную в списке ее ситуаций (т.е. нарушить грантию), вызывается
функция unexpected (15.6).
Функция без спецификации-ситуаций может возбудить любую ситуацию.
Функция с пустой спецификацией-ситуаций не должна возбуждать ситуаций.
Спецификации-ситуаций не является часть тела функции.
15.6. Специальные функции
Функция terminate вызывается, когда не находитя реакция на ситуацию. Она в свою
очередь вызывает функцию abort(), которая прекращает выполнение программы.
Функция unexpected вызывается, когда функция со спецификацией-ситуаций
возбуждает непредусмотренную ситуацию. Эта функция также в конечном итоге
вызывает функцию abort().
15.7. Ситуации и доступ
Семантика обработки ситуации идентична семантике получения аргумента
функции. Поэтому формальные параметры в объявлении-ситуации подчиняются
тем же правилам доступа, что и формальные параметры функции, в которой
находитя это объявление.
Объект может быть послан выражением-возбуждения-ситуации, если его можно
копировать и уничтожать в контексте, в котором находится это выражение.
139
16. Потоки
16.1. Ввведение
С++ не обеспечивает встроенных средств ввода/вывода, но они создаются
посредством самого языка. Описываемая стандартная библиотека потоков
ввода/вывода обеспечивает гибкий и эффективный метод символьного
ввода/вывода целых чисел, чисел с плавающей запятой и символьных строк: а
также простую модель ее расширения для ввода/ данных пользовательских типов.
Ее пользовательский интерфейс находится в файле <iostream.h>.
Традиционно средства ввода/вывода разрабатывались исключительно для
небольшого числа встроенных типов данных. Однако в программах на С++ обычно
используется много типов, определенных пользователем, и требуется
вводить/выводить данные этих типов. Ничье решение этой задачи не может
угодить всем, поэтому у пользователя должна быть возможность задавать
альтернативные средства ввода/вывода и расширять стандартные средства
ввода/вывода применительно к требованиям приложения. Обоснованным является
также требование, что средства ввода/вывода для С++ должны обеспечиваться в
С++ посредством только тех возможностей, которые доступны программирующему
на этом языке.
Существует много независимых реализаций библиотеки потоков ввода/вывода, и
описываемый ниже набор средств - это только часть возможностей, которые можно
найти в библиотеке. В файле <iostream.h> определяется интерфейс с библиотекой
потоков. Более ранняя версия этой библиотеки использует файл <stream.h>. Там,
где есть обе версии, в файле <iostream.h> определяется полный набор
возможностей, тогда как <stream.h> - это только подмножество, которое образует
интерфейс с прежней, менее обширной библиотекой потоков.
10.2. Вывод
Единообразный с гарантией типового контроля вывод данных как встроенных, так
и пользовательских типов обеспечивается использованием одного совмещенного
имени для множества операций вывода. Пример:
put(cerr, "x = ");
put(cerr, x);
put(cerr, '\n');
// cerr - поток вывода ошибок
В каждом случае тип параметра определяет, какая из put должна вызываться. Более
лаконичный вариант вывода - использование совмещенного знака операции "<<",
обозначающего операцию "поместить в", что позволяет выводить одним
оператором сразу несколько объектов, например, если х есть int со значением 123,
то оператор
140
cerr << "x =' << x << '\n';
выведет в стандартный поток ошибок cerr строку
x = 123
и символ новой строки. Аналогично, если х имеет тип complex и значение
(1, 2.4), оператор выведет
х = (1, 2.4)
Этот метод можно использовать в тех случаях, когда для х определена операция
"<<", а определить ее можно в любом типе. Операция "<<" имет достаточно низкий
приоритет для того, чтобы использовать в качестве операндов арифметические
выражения без скобок, например:
cout << "a*b+c" << a*b+c << '\n';
Когда необходимо выводить значения выражений, содержащих менее
приоритетные операции, следует использовать скобки, например:
cout << "a^b|c" << (a ^ b | c) << '\n';
Операция сдвига влево также может быть использована в выводимом выражении,
но, разумеется, она должна быть заключена в скобки:
cout << "a<<b=" << (a << b) << '\n';
10.2.1. Вывод данных встроенных типов
Для вывода данных встроенных типов определен следующий класс ostream:
class ostream : public virtual ios {
// . . .
public:
ostream& operator << (const char*);
// строки
ostream& operator << (char);
ostream& operator << (short i) {return *this << int(i);};
ostream& operator << (int);
ostream& operator << (long);
ostream& operator << (double);
ostream& operator << (const void);
// указатели
// . . .
};
141
Естественно, что в потоке ostream содержится также ряд операций "<<" для работы
с беззнаковыми типами.
Согласно определению каждая операция "<<" возвращает ссылку на поток, для
котороко она вызвана и потому к нему можно применить другой оператор "<<" как
показано выше. Отсюда, в частности, следует, что когда один оператор выводит
несколько значений, они будут выводится в ожидаемом порядке.
Операция ostream::operator << (int) печатает целые числа, операция
ostream::operator << (char) - символы и т.д.; операция
ostream::operator << (const void) печатает значение указателя в той форме, которая
присуща адресам на данной машине. Пример:
main()
{
int i = 0;
int* p = new int(1);
cout << "локальная " << &i << ", динамическая " << p << '\n';
может напечатать
локальная 0x7fffead0, динамическая 0x500c.
10.2.2. Вывод значений пользовательских типов
Рассмотрим пользовательский тип
class complex {
double re, im;
public:
complex(double r = 0, double i = 0) { re = r; im = i;}
double real(complex& a) {return a.re;}
double imag(complex& a) {return a.im;}
complex operator + (complex, complex);
complex operator - (complex, complex);
complex operator * (complex, complex);
complex operator / (complex, complex);
// . . .
};
Операция "<<" для нового типа complex может быть определена следующим
образом:
ostream& operator << (ostream& s, complex z)
142
{
return s << '(' << real(z) << ',' << imag(z) << ')';
}
и использована стандартным образом:
main()
{
complex x(1, 2);
cout << "x = " << x << '\n';
}
со следующим результатом:
x = (1, 2)
10.3. Ввод
Ввод аналогичен выводу. Имеется класс istream, который предоставляет операцию
">>" (взять из) для стандартных типов. Эта операция может затем
переопределяться для пользовательских типов.
10.3.1. Ввод значений встроенных типов
Класс istream определяется следующим образом:
class istream : public virtual ios {
// . . .
public:
istream& operator >> (char*);
istream& operator >> (char&);
istream& operator >> (short&);
istream& operator >> (int&);
istream& operator >> (long&);
istream& operator >> (float&);
istream& operator >> (double&);
// . . .
};
// строки
// символы
Каждая из операций описывается в следующем духе:
istream& istream::operator >> (T& tvar);
{
// игнорировать пропуски
// неким образом считать значение типа Т в 'tvar'
143
return *this
}
Таким образом можно ввести последовательность целых чисел, разделенных
пропусками, в некоторый вектор целых чисел:
int readints(Vector<int> v)
// возвращает количество введенных чисел
{
for (int i = 0; i < v.size(); i++)
{
if (cin >> v[i]) continue;
return i;
};
// слишком много чисел для данного размера вектора
}
Первое встреченное нецелое значение на входе вызовет неудачное завершение
операции ввода и тем самым завершит цикл ввода. Например, при вводе
последовательности
1 2 3 4 5. 6 7 8
функция readints считала бы 5 первых значений, оставив точку как следующий
вводимый символ. Пропуском являются символы пробела, табуляции, перевода
строки, возврата каретки и конца страницы.
Кроме операции ">>", можно пользоваться также следующими функциями ввода:
class istream : public virtual ios {
// . . .
public:
istream& get(char*); // символ
istream& get(char*, int, char = '\n');
}
// строка
Эти функции не выделяют пропуски из других символов. Функция get(char*)
вводит в свой аргумент очередной символ, а функция get(char* p, int n, char = '\n') строку длиною максимум n-1 символ в вектор, начинающийся с p (n-ым символом
будет завершающий 0); третий аргумент задает символ останова, которым по
умолчанию является '\n'.
Стандартный заголовочный файл <ctype.h> содержит описания нескольких
функций, полезных при обработке ввода:
144
int isalfa(char)
int isupper(char)
int islower(char)
int isdigit(char)
int isxdigit(char)
int isspace(char)
// 'a' .. 'z' 'A' .. 'Z'
// 'A' .. 'Z'
// 'a' .. 'z'
// '0' .. '9'
// '0' .. '9' 'a' .. 'f' 'A' .. 'F'
// пробел, табуляция, перевод строки, возврат каретки,
// конец страницы
int iscntr(char)
// управляющий символ (ASCII 0 .. 31, 127)
int ispunct(char)
// символ пунктуации; ни один из перечисленных выше
int isalnum(char)
// isalfa(char) | isdigit(char)
int isprint(char)
// печатаемый: ASCII ' ' .. '~'
int isgraph(char)
// isalfa(char) | isdigit(char) | ispunct(char)
int isascii(char) {return 0 <= c && c <= 127; }
Функция eatwhite, считывающая подряд идущие пропуски из потока, может теперь
быть описана следующим образом:
istream& eatwhite(istream& is)
{
char c;
while is.get(c)) {
if (isspace(c) == 0) {
is.putback(c);
breack;
}
}
return is;
}
Здесь использована функция putback(c), которая возвращает символ назад в поток
для последующего считывания.
10.3.2. Состояния потока
Каждый поток обладает состоянием, и обработка ошибок и нештатных ситуаций
осуществляется путем установки и проверки этого состояния.
Состояние потока можно проверить посредством следующих функций класса ios.
class ios {
// базовый класс для istream и ostream
// . . .
public:
int eof()const
// конец файла
int fail()const
// следующая операция потерпит неудачу
int bad()const
// поток испорчен
145
int good()const
// следующая операция может пройти успешно
// . . .
enum io_state {
goodbit = 0;
eofbit = 1;
failbit = 2;
badbit = 4;
};
int rdstate() const;
// совокупность битов, задающих состояние потока
void clear(int i = 0); // установить состояние потока
}
Состояние good() или eof() означает, что последняя операция ввода прошла
успешно. В состоянии good() следующая операция ввода может пройти успешно, в
противном случае она закончится неудачей.
Если делается попытка читать в переменную v и операция заканчивается неудачей,
значение v не должно измениться (оно и не изменится, если v имеет тип, для
которого есть компонентная операция ввода в потоке istream).
Состояние fail() предполагает, что поток не испорчен и никакие символы в нем не
потеряны; в состоянии bad() может быть все, что угодно. Две последних функции
обычно используются только разработчиками операций ввода.
Константы перечисления io_state определяют целые значения, сопоставленные
этим состояниям.
Состояния потока могут быть проверены, например, следующим образом:
int s = cin.rdstate(); // совокупность io_state битов потока cin
if (s & ios::goodbit) {
// последняя операция над cin прошла успешно
}
else if (s & ios::badbit) {
// возможно, символы cin потеряны
}
else if (s & ios::failbit) {
// возможн была ошибка, но не слишком опасная
}
else if (s & ios::eofbit) {
// конец файла
}
146
Делать подобную проверку после каждой операции ввода в программе не очень
удобно, но предполагается, что приведенные средства могут быть использованы в
механизме обработки ситуаций.
10.3.3. Ввод данных пользовательских типов
Операция ввода для пользовательского типа определяется точно так же, как и
операция вывода, но при этом важно, чтобы второй параметр имел тип ссылки.
Пример:
istream operator >> (istream& s, complex a)
/* форматы для ввода комплексных чисел; f обозначает float:
f
(f)
(f, f)
*/
{
double re = 0, im = 0;
char c = 0;
c >> c;
if (c == '(' {
s >> re >> c;
if (c == ',') s >> im >> c;
if (c != ')') s.clear(ios::badbit); // установить состояние непригодности
}
else {
s.putback(c);
s >> re;
};
if (s) a = complex(re, im);
return s;
}
Эта функция будет обрабатывать большую часть возможных ошибок. Локальная
переменная с инициализируется, чтобы ее значение случайно не оказалось "(" после
провала операции ввода. Завершающая проверка состояния потока нужна для
гарантии, что все прошло хорошо.
147
Download