“Будьте осторожными со своими желаниями —
они могут сбыться”.
Пословица
В этом приложении кратко изложены основные сведения о ключевых элементах языка С++. Оно имеет очень избирательный характер и предназначено для новичков, желающих узнать немного больше, чем написано в книге. Цель этого приложения — краткость, а не полнота.
Это приложение является справочником. Его не обязательно читать с начала до конца, как обычную главу. В нем (более или менее) систематично описаны ключевые элементы языка С++. Впрочем, это не полный справочник, а всего лишь его конспект. Приложение посвящено тем вопросам, которые чаще всего задают студенты. Как правило, для того чтобы получить более полный ответ, читателям придется прочитать соответствующие главы. Настоящее приложение нельзя считать эквивалентом стандарта по точности изложения и терминологии. Вместо этого мы сделали упор на доступность изложения. Более полную информацию читатели смогут найти в книге Stroustrup, The C++ Programming Language. Определение языка C++ изложено в стандарте ISO C++, но этот документ не подходит для новичков. Впрочем, он для них и не был предназначен. Не забудьте о возможности использовать документацию, имеющуюся в сети. Если вы будете заглядывать в приложение, читая первые главы, то многое вам покажется непонятным. Читая остальные главы, вы постепенно во всем разберетесь.
Возможности стандартной библиотеки описаны в приложении Б.
Стандарт языка C++ определен комитетом, работающим под эгидой ISO (International Organization for Standardization — Международная организация по стандартизации) в сотрудничестве с национальными стандартными комитетами, такими как INCITS (США), BSI (Великобритания) и AFNOR (Франция). Действующим стандартом считается документ ISO/IEC 14882:2003 Standard for Programming Language C++. Он доступен как в электронном виде, так и в виде обычной книги: The C++ Standard, опубликованной издательством Wiley (ISBN 2870846747).
В стандарте языка C++ даны следующие определения программы на языке C++ и разных его конструкций.
• Соответствие стандарту. Программа, написанная на языке C++ в соответствии со стандартом, называется соответствующей стандарту (conforming), или легальной (legal), или корректной (valid).
• Зависимость от реализации. Программа может зависеть (и обычно зависит) от свойств (таких как размер типа
int
или числовое значение символа 'a'
), которые точно определены только для заданного компилятора, операционной системы, машинной архитектуры и т.д. Свойства языка, зависящие от реализации, перечислены в стандарте и должны быть указаны в сопроводительной документации компилятора, а также в стандартных заголовках, таких как
(см. раздел Б.1.1). Таким образом, соответствие стандарту не эквивалентно переносимости программы на разные реализации языка C++ .
• Неопределенность. Смысл некоторых конструкций является неустановленным точно (unspecified), неопределенным (undefined) или не соответствующим стандарту, но не диагностируемым (not conforming but not requiring a diagnostic). Очевидно, что такие свойства лучше не использовать. В этой книге их нет. Перечислим неопределенные свойства, которых следует избегать.
• Несогласованные определения в разных исходных файлах (используйте заголовочные файлы согласованно; см. раздел 8.3).
• Повторное чтение и запись одной и той же переменной в выражении (основным примером является инструкция
a[i]=++i;
).
• Многочисленные явные преобразования типов (приведения), особенно
reinterpret_cast
.
В программе на языке С++ должна быть отдельная глобальная функция с именем
main()
. Программа начинается с выполнения именно этой функции. Значение, возвращаемое функцией main()
, имеет тип int
(альтернативный тип void
не соответствует стандарту). Значение, возвращаемое функцией main()
, передается системе. Некоторые системы игнорируют это значение, но признаком успешного завершения программы является нуль, а признаком ошибки — ненулевое значение или исключение, оставшееся не перехваченным (правда, такие исключения считаются признаком плохого стиля).
Аргументы функции
main()
могут зависеть от реализации, но любая реализация должна допускать два варианта (но только одну для конкретной программы).
int main(); // без аргументов
int main(int argc, char* argv[]); // массив argv[] содержит
// argc C-строк
В определении функции
main()
явно указывать тип возвращаемого значения не обязательно. В таком случае программа, дойдя до конца, вернет нуль. Вот как выглядит минимальная программа на языке C++:
int main() { }
Если вы определили глобальный (в пространстве имен) объект, имеющий конструктор и деструктор, то вполне логично, чтобы конструктор выполнялся до функции
main()
, а деструктор — после функции main()
. Формально говоря, выполнение таких конструкторов является частью вызова функции main()
, а выполнение деструкторов — частью возвращения из функции main()
. При малейшей возможности постарайтесь избегать глобальных объектов, особенно если они требуют нетривиального создания и уничтожения.
Все, что можно сказать в программе, должно быть сказано. Однако в языке C++ есть два стиля комментариев, позволяющие программистам сказать то, что невозможно выразить с помощью кода.
// это однострочный комментарий
/*
это многострочный
блок комментариев
*/
Очевидно, что блоки комментариев чаще всего оформляются как многострочные комментарии, хотя некоторые люди предпочитают разделять их на несколько однострочных.
// Это многострочный
// комментарий,
// представленный в виде трех однострочных комментариев,
/* а это однострочный комментарий, представленный как блочный
комментарий */
Комментарии играют важную роль для документирования предназначения кода; см. также раздел 7.6.4.
Литералы представляют значения разных типов. Например, литерал 12 представляет целое число двенадцать, литерал "
Morning
" — символьную строку Morning, а литерал true
— булево значение true.
Целочисленные литералы (integer literals) имеют три разновидности.
• Десятичные: последовательности десятичных цифр.
Десятичные цифры: 0, 1, 2, 3, 4, 5, 6, 7, 8 и 9.
• Восьмеричные: последовательности восьмеричных цифр, начинающиеся с нуля.
Восьмеричные цифры: 0, 1, 2, 3, 4, 5, 6 и 7.
• Шестнадцатеричные: последовательности шестнадцатеричных цифр, начинающихся с 0x или 0X.
Шестнадцатеричные цифры: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f, A, B, C, D, E и F.
Суффикс u или U означает, что целочисленный литерал не имеет знака, т.е. имеет спецификатор unsigned (см. раздел 25.5.3), а суффикс l или L относит их к типу
long
, например 10u
или 123456UL
.
Обычно мы записываем числа в десятичной системе. Число
123
означает 1
сотню плюс 2
десятки плюс 3
единицы, или 1*100+2*10+3*1
, или (используя символ ^
для обозначения степени) 1*10^2+2*10^1+3*10^0
. Иногда вместо слова десятичный говорят: “База счисления равна десяти” (base-10). В данном случае число 10 означает, что в выражении 1*base^2+2*base^1+3*base^0
выполняется условие base==10
. Существует много теорий, объясняющих, почему мы используем десятичную систему счисления. Одна из них апеллирует к естественным языкам: у нас на руках десять пальцев, а каждый символ, такой как 0, 1 и 2, представляющий собой цифру в позиционной системе счисления, в английском языке называется digit. Слово Digit в латинском языке означает палец.
Впрочем, иногда используются и другие системы счисления. Как правило, положительные целые числа в памяти компьютера представляются в двоичной системе счисления, т.е. база счисления равна 2 (значения 0 и 1 относительно легко представить с помощью физических состояний). Люди, сталкивающиеся с необходимостью решать задачи на низком уровне аппаратного обеспечения, иногда используют восьмеричную систему счисления (база равна 8), а при адресации памяти чаще используется шестнадцатеричная система (база равна 16).
Рассмотрим шестнадцатеричную систему счисления. Мы должны назвать шестнадцать значений от 0 до 15. Обычно для этого используются следующие символы:
0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
, A
, B
, C
, D
, E
, F
, где A
имеет десятичное значение 10
, B
— десятичное значение 11
и так далее:
A==10
, B==11
, C==12
, D==13
, E==14
, F==15
Теперь можем записать десятичное число
123
как 7B
в шестнадцатеричной системе счисления. Для того чтобы убедиться в этом, обратите внимание на то, что в шестнадцатеричной системе счисления число 7B
равно 7*16+11
, что в десятичной системе счисления равно 123
. И наоборот, шестнадцатеричное число 123
означает 1*16^2+2*16+3
, т.е. 1*256+2*16+3
, что в десятичной системе счисления равно 291
. Если вы никогда не сталкивались с недесятичными представлениями целых чисел, то мы настоятельно рекомендуем вам поупражняться в преобразовании чисел из десятичной системы в шестнадцатеричную, и наоборот. Обратите внимание на то, что шестнадцатеричная цифра имеет очень простое соответствие со своим двоичным значением.
Это объясняет популярность шестнадцатеричной системы. В частности, значение байта просто выражается двумя шестнадцатеричными цифрами.
В языке C++ (к счастью) числа являются десятичными, если иное не указано явно. Для того чтобы сказать, что число является шестнадцатеричным, следует поставить префикс
0X
(символ X
происходит от слова hex), так что 123==0X7B
и 0X123==291
. Точно так же можно использовать символ x
в нижнем регистре, поэтому 123==0x7B
и 0x123==291
. Аналогично мы можем использовать шестнадцатеричные цифры a
, b
, c
, d
, e
и f
в нижнем регистре. Например, 123==0x7b
.
Восьмеричная система основана на базе счисления, равной восьми. В этом случае мы можем использовать только восемь восьмеричных цифр:
0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
. В языке C++ числа в восьмеричной системе счисления начинаются с символа 0
, так что число 0123
— это не десятичное число 123
, а 1*8^2+2*8+3
, т.е. 1*64+2*8+3
или (в десятичном виде) 83
. И наоборот, восьмеричное число 83
, т.е. 083
, равно 8*8+3
, т.е. десятичному числу 67
. Используя систему обозначений языка C++, получаем равенства 0123==83
и 083==67
.
Двоичная система основана на базе счисления, равной двум. В этой системе есть только две цифры:
0
и 1
. В языке С++ невозможно непосредственно представить двоичные числа как литералы. В качестве литералов и формата ввода-вывода в языке С++ непосредственно поддерживаются только восьмеричные, десятичные и шестнадцатеричные числа. Однако двоичные числа полезно знать, даже если мы не можем явно представить их в тексте программы. Например, десятичное число 123
равно 1*2^6+1*2^5+1*2^4+1*2^3+0*2^2+1*2+1
, т.е. 1*64+1*32+1*16+1*8+0*4+1*2+1
, т.е. (в двоичном виде) 1111011
.
Литералы с плавающей точкой (floating-point-literal) содержат десятичную точку (
.
), показатель степени (например, e3
) или суффикс, обозначающий число с плавающей точкой (d
или f
). Рассмотрим примеры.
123 // int (нет десятичной точки, суффикса или показателя степени)
123. // double: 123.0
123.0 // double
.123 // double: 0.123
0.123 // double
1.23e3 // double: 1230.0
1.23e–3 // double: 0.00123
1.23e+3 // double: 1230.0
Литералы с плавающей точкой имеют тип
double
, если суффикс не означает иное. Рассмотрим примеры.
1.23 // double
1.23f // float
1.23L // long double
Литералами типа
bool
являются литералы true
и false
. Целочисленное значение литерала true
равно 1
, а литерала false
— 0
.
Символьный литерал (character literal) — это символ, заключенный в одинарные кавычки, например
'a'
или '@'
. Кроме того, существует несколько специальных символов.
Специальный символ представляется с помощью имени в языке C++, заключенного в одинарные кавычки, например
'\n'
(новая строка) и '\t'
(табуляция).
Набор символов содержит следующие видимые символы:
abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
!@#$%^&*()_+|~`{}[]:";'<>?,./
В переносимом коде нельзя рассчитывать на дополнительные видимые символы. Значение символа, например
'a'
для буквы a
, зависит от реализации (но его легко выяснить, например, выполнив инструкцию, cout << int('a')
).
Строковый литерал (string literal) — это последовательность символов, заключенных в двойные кавычки, например
"Knuth"
и "King Canute"
. Строковый литерал нельзя произвольно разбивать на несколько строк; для перехода на новую строку используется специальный символ \n
.
"King
Canute" // ошибка: переход на новую строку в строковом литерале
"King\nCanute" // OK: правильный переход на новую строку
Два строковых литерала, разделенных только одним пробелом, считаются одним строковым литералом. Рассмотрим пример.
"
King" "Canute" // эквивалентно "KingCanute" (без пробела)
Обратите внимание на то, что специальные символы, такие как
\n
, могут входить в строковые литералы.
Существует только один указательный литерал (pointer literal): нулевой указатель (
0
). В качестве нулевого указателя можно использовать любое константное выражение, равное 0
.
t* p1 = 0; // OK: нулевой указатель
int* p2 = 2–2; // OK: нулевой указатель
int* p3 = 1; // ошибка: 1 — int, а не указатель
int z = 0;
int* p4 = z; // ошибка: z — не константа
В данном случае значение
0
неявно превращается в нулевой указатель. Как правило (но не всегда), нулевой указатель представляется в виде битовой маски, состоящей из одних нулей, как и число 0
.
В языке C++ (но не в языке C, поэтому будьте осторожны с заголовками языка C) литерал
NULL
по определению равен 0
, поэтому можно написать следующий код:
int* p4 = NULL; // ( при правильном определении литерала NULL)
// нулевой указатель
В языке C++0x нулевой указатель будет обозначаться ключевым словом
nullptr
. А пока рекомендуем использовать для этого число 0
.
Идентификатор (identifier) — это последовательность символов, начинающаяся с буквы или знака подчеркивания, за которыми следуют (или не следуют) буквы, цифры или знаки подчеркивания (в верхнем или нижнем регистре).
int foo_bar; // OK
int FooBar; // OK
int foo bar; // ошибка: пробел не может использоваться
// в идентификаторе
int foo$bar; // ошибка: символ $ не может использоваться
// в идентификаторе
Идентификаторы, начинающиеся со знака подчеркивания или содержащие двойной символ подчеркивания, резервируются для использования компилятором; не используйте их. Рассмотрим пример.
int _foo; // не рекомендуем
int foo_bar; // OK
int foo__bar; // не рекомендуем
int foo_; // OK
Ключевые слова (keywords) — это идентификаторы, используемые самим языком для выражения языковых конструкций.
Каждое имя в языке C++ (за исключением имен препроцессора; см. раздел A.17) имеет определенную область видимости (scope); иначе говоря, существует область текста, в которой его можно использовать. Данные (объекты) хранятся в памяти; вид памяти, используемой для хранения объекта, называется классом памяти (storage class). Время жизни (lifetime) объекта отсчитывается от момента его инициализации до момента окончательного уничтожения.
Существует пять видов областей видимости (см. раздел 8.4).
• Глобальная область видимости (global scope). Имя находится в глобальной области видимости, если оно объявлено вне языковой конструкции (например, вне класса или функции).
• Область видимости пространства имен (namespace scope). Имя находится в области видимости пространства имен, если оно определено в пространстве имен и вне какой-либо языковой конструкции (например, вне класса и функции). Формально говоря, глобальная область видимости — это область видимости пространства имен с “пустым именем”.
• Локальная область видимости (local scope). Имя находится в локальной области видимости, если она объявлена в функции (включая параметры функции).
• Область видимости класса (class scope). Имя находится в области видимости класса, если оно является именем члена этого класса.
• Область видимости инструкции (statement scope). Имя находится в области видимости инструкции, если оно объявлено в части (
...
) инструкции for
, while
, switch
или if
.
Область видимости переменной распространяется (исключительно) до конца инструкции, в которой она объявлена. Рассмотрим пример.
for (int i = 0; i
// переменная i может быть использована здесь
}
if (i < 27) // переменная i из инструкции for вышла из области
// видимости
Области видимости класса и пространства имен имеют свои имена, поэтому можем ссылаться на их членов извне. Рассмотрим пример.
void f(); // в глобальной области видимости
namespace N {
void f() // в пространстве области видимости N
{
int v; // в локальной области видимости
::f(); // вызов глобальной функции f()
}
}
void f()
{
N::f(); // вызов функции f(x) из области видимости N
}
Что произойдет, если мы вызовем функции
N::f()
или ::f()
? См. раздел A.15.
Существуют три класса памяти (раздел 17.4).
• Автоматическая память (automatic storage). Переменные, определенные в функциях (включая параметры функции), размещаются в автоматической памяти (т.е. в стеке), если они явно не объявлены с помощью ключевого слова
static
. Автоматическая память выделяется, когда функция вызывается, и освобождается при возвращении управления в вызывающий модуль. Таким образом, если функция (явно или неявно) вызывает сама себя, может существовать несколько копий автоматических данных: по одной копии на каждый вызов (см. раздел 8.5.8).
• Статическая память (static storage). Переменные, объявленные в глобальной области видимости и в области видимости пространства имен, хранятся в статической памяти, как и переменные, явно объявленные с помощью ключевого слова
static
в функциях и классах. Редактор связей выделяет статическую память до запуска программы.
• Свободная память (куча) (free store (heap)). Объекты, созданные с помощью оператора
new
, размещаются в свободной памяти.
Рассмотрим пример.
vector vg(10); // создается один раз при старте программы
// ("до функции main()")
vector* f(int x)
{
static vector vs(x); // создается только при первом
// вызове f()
vector vf(x+x); // создается при каждом вызове f()
for (int i=1; i<10; ++i) {
vector vl(i); // создается на каждой итерации
// ...
} // переменная v1 уничтожается здесь (на каждой итерации)
return new vector(vf); // создается в свободной памяти
// как копия переменной vf
} // переменная vf уничтожается здесь
void ff()
{
vector* p = f(10); // получает вектор от функции f()
// .. .
delete p; // удаляет вектор, полученный от
// функции f
}
Переменные
vg
и vs
, размещенные в статической памяти, уничтожаются по завершении программы (после функции main()
), при условии, что они были созданы.
Память для членов класса отдельно не выделяется. Когда вы размещаете объект где-то, то нестатические члены размещаются там же (в том же классе памяти, что и сам объект, которому они принадлежат).
Код хранится отдельно от данных. Например, функция-член не хранится в каждом объекте своего класса; одна ее копия хранится вместе с остальной частью кода программы.
См. также разделы 14.3 и 17.4.
Перед тем как объект будет (легально) использован, он должен быть проинициализирован. Эту инициализацию можно осуществить явно, с помощью инициализатора, или неявно, используя конструктор или правило инициализации объектов встроенных типов по умолчанию. Время жизни объекта заканчивается в точке, определенной его областью видимости и классом памяти (например, см. разделы 17.4 и Б.4.2).
• Локальные (автоматические) объекты создаются, когда поток выполнения достигает их определения, и уничтожаются при выходе из области видимости.
• Временные объекты создаются конкретным подвыражением и уничтожаются по завершении полного выражения. Полное выражение — это выражение, которое не является подвыражением другого выражения.
• Объекты в пространстве имен и статические члены классов создаются в начале программы (до функции
main()
) и уничтожаются в конце программы (после функции main()
”).
• Локальные статические объекты создаются, когда поток выполнения достигает их определения и (если они были созданы) уничтожаются в конце программы.
• Объекты в свободной памяти создаются оператором
new
и (необязательно) уничтожаются с помощью оператора delete
.
Временная переменная, связанная с локальной ссылкой, существует столько же, сколько и сама ссылка. Рассмотрим пример.
const char* string_tbl[] = { "Mozart", "Grieg", "Haydn", "Chopin" };
const char* f(int i) { return string_tbl[i]; }
void g(string s){}
void h()
{
const string& r = f(0); // связываем временную строку
// с ссылкой r
g(f(1)); // создаем временную строку
// и передаем ее
string s = f(2); // инициализируем s временной строкой
cout << "f(3): " << f(3) // создаем временную строку
// и передаем ее
<< "s: " << s
<< "r: " << r << '\n';
}
Результат выглядит следующим образом:
f(3): Chopin s: Haydn r: Mozart
Временные строки, сгенерированные при вызовах
f(1)
, f(2)
и f(3)
, уничтожаются в конце выражения, в котором они были созданы. Однако временная строка, сгенерированная при вызове f(0)
, связана с переменной r
и “живет” до конца функции h()
.
В этом разделе описываются операторы языка C++. Мы используем обозначения, которые считаем мнемоническими, например:
m
— для имени члена; T
— для имени типа; p
— для выражения, создающего указатель; x
— для выражения; v
— для выражения lvalue
; lst
— для списка аргументов. Типы результатов арифметических операций определяются обычными арифметическими преобразованиями (раздел A.5.2.2). Описания, приведенные в этом разделе, касаются только встроенных операторов, а не операторов, которые программист может определить самостоятельно, хотя, определяя свои собственные операторы, следует придерживаться семантических правил, установленных для встроенных операторов (см. раздел 9.6).
Обратите внимание на то, что члены могут быть сами вложенными, поэтому можем получить такие выражения, как
N::C::m
(см. также раздел 8.7).
Оператор
typeid
и его применения не описаны в этой книге; его детали можно найти в более сложных учебниках. Обратите внимание на то, что операторы приведения не модифицируют свой аргумент. Вместо этого они создают результат своего типа, который каким-то образом соответствует значению аргумента (раздел A.5.7).
Объекты, на которые ссылается указатель
p
в инструкциях delete p
и delete[] p
, должны быть размещены в памяти с помощью оператора new
(раздел A.5.6). Следует подчеркнуть, что выражение (T)x
является менее конкретным и, следовательно, более уязвимым для ошибок, чем более конкретные операторы приведения (раздел A.5.7).
Эти инструкции в книге не рассматриваются; обратитесь к более сложным учебникам.
Если
y==0
, то результат выражений x/y
и x%y
не определен. Если переменная x
или y
является отрицательной, то результат выражения x%y
является отрицательным.
Для встроенных типов операторы
>>
и <<
означают сдвиг битов (см. раздел 25.5.4). Если левым операндом является объект класса iostream
, то эти операторы используются для ввода и вывода (см. главы 10-11).
Результатом оператора сравнения является значение типа
bool
.
Обратите внимание на то, что
x!=y
эквивалентно !(x==y)
. Результат оператора равенства имеет тип bool
.
Оператор
&
(как и операторы ^
, |
, ~
, >>
и <<
) возвращает комбинацию битов. Например, если переменные a
и b
имеют тип unsigned char
, то результат выражения a&b
имеет тип unsigned char
, в котором каждый бит является результатом применения оператора &
к соответствующим битам переменных a
и b
(раздел A.5.5).
См. раздел А.5.5.
Рассмотрим пример.
template T& max(T& a, T& b) { return (a>b)?a:b; }
Оператор “знак вопроса” описан в разделе 8.4.
Фраза “аналог
v=v*(x)
” означает, что значение выражения v*=x
совпадает со значением выражения v=v*(x)
, за исключением того, что значение v вычисляется только один раз. Например, выражение v[++i]*=7+3
означает (++i, v[i]=v[i]*(7+3)
), а не (v[++i]=v[++i]*(7+3)
) (которое может быть неопределенным; см. раздел 8.6.1).
Результат выражения
throw
имеет тип void
.
Каждая таблица содержит операторы, имеющие одинаковый приоритет. Операторы в более высоко расположенных таблицах имеют более высокий приоритет по сравнению с операторами, расположенными ниже. Например, выражение
a+b*c
означает a+(b*c)
, а не (a+b)*c
, поскольку оператор *
имеет более высокий приоритет по сравнению с оператором +
. Аналогично, выражение *p++
означает *(p++)
, а не (*p)++
. Унарные операторы и операторы присваивания являются правоассоциативными (right-associative); все остальные — левоассоциативными. Например, выражение a=b=c
означает a=(b=c)
, а выражение a+b+c
означает (a+b)+c
. Lvalue
— это объект, допускающий модификацию. Очевидно, что объект lvalue
, имеющий спецификатор const
, защищен от модификации системой типов и имеет адрес. Противоположностью выражения lvalue
является выражение rvalue
, т.е. выражение, идентифицирующее нечто, что не может быть модифицировано или не имеет адреса, например, значение, возвращаемое функцией (&f(x)
— ошибка, поскольку значение, возвращаемое функцией f(x)
, является значением rvalue
).
Правила, перечисленные выше, установлены для встроенных типов. Если же используется оператор, определенный пользователем, то выражение просто преобразовывается в вызов соответствующей операторной функции, определенной пользователем, и порядок действий определяется правилами, установленными для вызова функций. Рассмотрим пример.
class Mine { /* .. . */ };
bool operator==(Mine, Mine);
void f(Mine a, Mine b)
{
if (a==b) { // a==b означает operator==(a,b)
// ...
}
}
Тип, определенный пользователем, — это класс (см. главу 9, раздел A.12) или перечисление (см. разделы 9.5, A.11).
Целочисленные типы или типы с плавающей точкой (раздел A.8) могут свободно смешиваться в операторах присваивания и в выражениях. При первой же возможности значения преобразовываются так, чтобы не потерять информацию. К сожалению, преобразования, уничтожающие значение, выполняются также неявно.
Неявные преобразования, сохраняющие значения, обычно называют продвижениями (promotions). Например, перед выполнением арифметической операции для создания типа int из более коротких целочисленных типов выполняется целочисленное продвижение (integral promotion). Это отражает исходную цель продвижений: привести операнды арифметических операций к “естественным” размерам. Кроме того, преобразование значения типа
float
в значение типа double
также считается продвижением.
Продвижения используются как часть обычных арифметических преобразований (раздел A.5.2.2).
Значения фундаментальных типов можно преобразовывать друг в друга самыми разными способами. При написании программы следует избегать неопределенного поведения и непредсказуемых преобразований, которые незаметно искажают информацию (см. разделы 3.9 и 25.5.3). Компиляторы обычно способны предупредить о многих сомнительных преобразованиях.
• Целочисленные преобразования. Целое число может быть преобразовано в другой целый тип. Значение перечисления может быть преобразовано в целый тип. Если результирующим типом является тип без знака (
unsigned
), то результирующее значение будет иметь столько же битов, сколько и источник, при условии, что оно может поместиться в целевой области памяти (старшие биты при необходимости могут быть отброшены). Если целевой тип имеет знак, то значение останется без изменения, при условии, что его можно представить с помощью целевого типа; в противном случае значение определяется реализацией языка. Обратите внимание на то, что типы bool
и char
являются целочисленными.
• Преобразования значений с плавающей точкой. Значение с плавающей точкой можно преобразовать в значение с плавающей точкой другого типа. Если исходное значение можно точно представить с помощью целевого типа, то результатом будет исходное числовое значение. Если же исходное значение лежит между двумя целевыми значениями, то результатом будет одно из этих значений. Иначе говоря, результат непредсказуем. Обратите внимание на то, что преобразование значения типа float в значение типа
double
считается продвижением.
• Преобразование указателей и ссылок. Любой указатель на тип объекта можно преобразовать в указатель типа
void*
(см. разделы 17.8 и 27.3.5). Указатель (ссылка) на производный класс можно неявно преобразовать в указатель (ссылку) на доступный и однозначно определенный базовый класс (см. раздел 14.3). Константное выражение (см. разделы A.5 и 4.3.1), равное нулю, можно неявно преобразовать в любой другой тип указателя. Указатель типа T*
можно неявно преобразовать в указатель const T*
. Аналогично ссылку T&
можно неявно преобразовать в ссылку типа const T&
.
• Булевы преобразования. Указатели, целые числа и числа с плавающей точкой можно неявно преобразовать в значение типа
bool
. Ненулевое значение преобразовывается в значение true
, а нуль — в значение false
.
• Преобразования чисел с плавающей точкой в целые числа. Если число с плавающей точкой преобразуется в целое число, то его дробная часть отбрасывается. Иначе говоря, преобразование из типа с плавающей точкой в целый тип является усечением. Если усеченное значение невозможно представить с помощью целевого типа, то результат становится непредсказуемым. Преобразования целых чисел в числа с плавающей точкой являются математически корректными только в той степени, в которой это допускается аппаратным обеспечением. Если целое число невозможно точно представить как число с плавающей точкой, происходит потеря точности.
• Обычные арифметические преобразования. Эти преобразования выполняются над операндами бинарных операторов, чтобы привести их к общему типу, а затем использовать этот тип для представления результата.
1. Если один из операндов имеет тип
long double
, то другой преобразовывается в тип long double
. В противном случае, если один из операндов имеет тип double
, другой преобразовывается в тип double
. В противном случае, если один из операндов имеет тип float
, другой преобразовывается в тип float
. В противном случае над обоими операндами целочисленного типа выполняется продвижение.
2. Если один из операндов имеет тип
unsigned long
, то другой преобразовывается в тип unsigned long
. В противном случае, если один из операндов имеет тип long int
, а другой — unsigned int
, значение типа unsigned int
преобразуется в значение типа long int
, при условии, что тип long int
может представить все значения типа unsigned int
. В противном случае оба операнда преобразовываются в тип unsigned long int
. В противном случае, если один из операндов имеет тип long
, другой преобразовывается в тип long
. В противном случае, если другой операнд имеет тип unsigned
, другой преобразовывается в тип unsigned
. В противном случае оба операнда имеют тип int
.
Очевидно, что лучше не полагаться на слишком запутанные сочетания типов и минимизировать необходимость неявных преобразований.
Кроме стандартных преобразований и продвижений, программист может определить преобразования типов, определенных пользователем. Конструктор, принимающий один аргумент, определяет преобразование этого аргумента в значение своего типа. Если конструктор имеет спецификатор
explicit
(см. раздел 18.3.1), то преобразование происходит, только если программист явно потребует его выполнить. В противном случае преобразование может быть неявным.
Константное выражение (constant expression) — это выражение, которое может быть вычислено на этапе компиляции и содержит только операнды типа
int
. (Это немного упрощенное определение, но для большинства целей оно вполне подходит.) Рассмотрим пример.
const int a = 2*3;
const int b = a+3;
Константные выражения требуются в немногих случаях, например, при вычислении границ массивов, меток разделов
case
, инициализаторов перечислений и шаблонных аргументов типа int
. Рассмотрим пример.
int var = 7;
switch (x) {
case 77: // OK
case a+2: // OK
case var: // ошибка (var — не константное выражение)
// ...
};
В выражении
sizeof(x)
аргумент x
может быть типом или выражением. Если x
— выражение, то значением sizeof(x)
является размер результирующего объекта. Если x
— тип, то значением sizeof(x)
является размер объекта типа x
. Размеры измеряются в байтах. По определению sizeof(char)==1
.
В языке C++ предусмотрены логические операторы для целочисленных типов.
Эти операторы применяются к каждому биту своих операндов, в то время как логические операторы (
&&
и ||
) трактуют число 0
как значение false
, а все — как true
. Определения этих операторов приведены ниже.
Свободная память (динамическая память, или куча) выделяется с помощью оператора
new
, а освобождается — с помощью оператора delete
(для индивидуальных объектов) или delete[]
(для массива).
Если память исчерпана, то оператор
new
генерирует исключение bad_alloc
. В случае успеха операция new
выделяет как минимум один байт и возвращает указатель на объект, размещенный в памяти. Тип этого объекта определяется после выполнения оператора new
. Рассмотрим пример.
int* p1 = new int; // размещает (неинициализированное) число
// типа int
int* p2 = new int(7); // размещает число типа int,
// инициализированное
// числом 7
int* p3 = new int[100]; // размещает 100 (неинициализированных)
// чисел int
// ...
delete p1; // удаляет индивидуальный объект
delete p2;
delete[] p3; // удаляет массив
Если с помощью оператора
new
вы размещаете в памяти объекты встроенного типа, они не будут инициализированы, если не указан инициализатор. Если с помощью оператора new
вы размещаете в памяти объекты класса, имеющего конструктор, то, если не указан инициализатор, будет вызван этот конструктор (см. раздел 17.4.4).
Оператор
delete
вызывает деструкторы каждого операнда, если они есть. Обратите внимание на то, что деструктор может быть виртуальным (раздел A.12.3.1).
Существуют четыре оператора приведения к типу.
Динамическое приведение обычно используется для навигации по иерархии классов, если указатель
p
— указатель на базовый класс, а класс D
— производный от базового класса. Если операнд v
не относится к типу D*
, то эта операция возвращает число 0
. Если необходимо, чтобы операция dynamic_cast
в случае неудачи не возвращала 0
, а генерировала исключение bad_cast
, то ее следует применять к ссылкам, а не к указателям. Динамическое приведение — единственное приведение, опирающееся на проверку типов во время выполнения программы.
Статическое приведение используется для “разумных преобразований,” т.е. если операнд v может быть результатом неявного преобразования типа
T
(см. раздел 17.8).
Оператор
reinterpret_cast
используется для реинтерпретации комбинации битов. Его переносимость не гарантируется. Фактически лучше считать, что он является вообще не переносимым. Типичным примером реинтерпретации является преобразование целого числа в указатель, чтобы получить машинный адрес в программе (см. разделы 17.8 и 25.4.1).
Приведения в стиле языка С и функциональные приведения могут выполнить любое преобразование типа, которое можно осуществить с помощью оператора
static_cast
или reinterpret_cast
в сочетании с оператором const_cast
.
Приведений лучше избегать. Во многих ситуациях их использование свидетельствует о плохом стиле программирования. Исключения из этого правила представлены в разделах 17.8 и 25.4.1. Приведение в стиле языка С и функциональные приведения имеют ужасное свойство: они позволяют вам не вникать в то, что именно они делают (см. раздел 27.3.4). Если вы не можете избежать явного преобразования типа, лучше используйте именованные приведения.
Грамматическое определение инструкций языка C++ приведено ниже (opt означает “необязательный”).
инструкция:
объявление
{ список_инструкции opt }
{ список_инструкции opt } список_обработчиковtry
выражение opt;
инструкция_выбора
инструкция_итерации
инструкция_с_метками
управляющая_инструкция
инструкция_выбора:
(условие) инструкцияif
(условие) инструкция if
else
инструкция
(условие) инструкцияswitch
инструкция_итерации:
while
(условие) инструкция
do
инструкция while
(выражение);
for
(инструкция_инициализации_for условие opt; выражение opt) инструкция
инструкция_с_метками:
case
константное_выражение: инструкция
default:
инструкция
identifier:
инструкция
управляющая_инструкция:
break;
continue;
return
выражение opt;
goto
идентификатор;
список_инструкции:
инструкция список_инструкции opt
условие:
выражение
спецификатор_типа объявляемый_объект = выражение
инструкция_инициализации_for:
выражение opt;
спецификатор_типа объявляемый_объект = выражение;
список_обработчиков:
catch
(объявление_исключения) { список_инструкции opt }
список_обработчиков список_обработчиков opt
Обратите внимание на то, что объявление — это инструкция, а присваивание и вызов функции являются выражениями. К этому определению следует добавить следующий список.
• Итерация (
for
и while
); см. раздел 4.4.2.
• Ветвление (
if
, switch
, case
и break
); см. раздел 4.4.1. Инструкция break
прекращает выполнение ближайшей вложенной инструкции switch
, while
, do
или for
. Иначе говоря, следующей будет выполнена инструкция, следующая за последней в теле одной из перечисленных выше инструкций.
• Выражения; см. разделы A.5 и 4.3.
• Объявления; см. разделы A.6 и 8.2.
• Исключения (
try
и catch
); см. разделы 5.6 и 19.4.
Рассмотрим пример, созданный просто для того, чтобы продемонстрировать разнообразие инструкций (какую задачу они решают?).
int* f(int p[],int n)
{
if (p==0) throw Bad_p(n);
vector v;
int x;
while (cin>>x) {
if (x==terminator) break; // выход из цикла while
v.push_back(x);
}
for (int i = 0; i
if (v[i]==*p)
return p;
else
++p;
}
return 0;
}
Объявление (declaration) состоит из трех частей:
• имя объявляемой сущности;
• тип объявляемой сущности;
• начальное значение объявляемой сущности (во многих случаях необязательное).
Мы можем объявлять следующие сущности:
• объекты встроенных типов и типов, определенных пользователем (раздел A.8);
• типы, определенные пользователем (классы и перечисления) (разделы A.10–А.11, глава 9);
• шаблоны (шаблонные классы и функции) (раздел A.13);
• альтернативные имена (раздел A.16);
• пространства имен (разделы A.15 и 8.7);
• функции (включая функции-члены и операторы) (раздел A.9, глава 8);
• перечисления (значения перечислений) (разделы A.11 и 9.5);
• макросы (разделы A.17.2 и 27.8).
Определение с инициализацией, резервирующее область памяти или как-то иначе поставляющую компилятору всю информацию, необходимую для использования имени в программе, называется определением (definition). Каждый тип, объект и функция в программе должны иметь только одно определение. Рассмотрим примеры.
double f(); // объявление
double f() { /* ... */ }; // также определение
extern const int x; // объявление
int y; // также определение
int z = 10; // определение с явной инициализацией
Константы должны быть инициализированы. Для этого используется инициализатор, если константа не объявлена с помощью ключевого слова extern (в таком случае инициализатор вместе с определением должны быть расположены в другом месте) или если константа не имеет тип, имеющий конструктор по умолчанию (раздел A.12.3). Константные члены класса должны инициализироваться в каждом конструкторе с помощью инициализатора (раздел A.12.3).
Язык C++ имеет много фундаментальных типов и типов, составленных из фундаментальных типов с помощью модификаторов.
Здесь
T
означает “некий тип”, поэтому существуют варианты long unsigned int
, long double
, unsigned char
и const char*
(указатель на константный символ char
). Однако эта система не совсем полная; например, в ней нет типа short double
(его роль играет тип float
); типа signed bool
(совершенно бессмысленного); типа short long int
(это было бы лишним) и типа long long long long int
. Некоторые компиляторы в ожидании стандарта C++0x допускают тип long long int
(читается как “очень длинный целый тип ”). Гарантируется, что тип long long
содержит не менее 64 бит.
Типы с плавающей точкой (floating-point types) — это типы
float
, double
и long double
. Они являются приближением действительных чисел в языке С++.
Целочисленные типы (integer types), иногда называемые интегральными (integral), — это типы
bool
, char
, short
, int
, long
и (в языке C++0x) long long
, а также их варианты без знака. Обратите внимание на то, что тип или значения перечислений часто можно использовать вместо целочисленного типа или значения.
Размеры встроенных типов обсуждались в разделах 3.8, 17.3.1 и 25.5.1; указатели и массивы — в главах 17 и 18; ссылки — в разделах 8.5.4–8.5.6.
Указатель (pointer) — это адрес объекта или функции. Указатели хранятся в переменных указательных типов. Корректный указатель на объект содержит адрес этого объекта.
int x = 7;
int* pi = &x; // указатель pi ссылается на объект x
int xx = *pi; // *pi — это значение объекта,
// на который ссылается указатель pi, т.е. 7
Некорректный указатель — это указатель, не содержащий указателя ни на один объект.
int* pi2; // неинициализированный
*pi2 = 7; // неопределенное поведение
pi2 = 0; // нулевой указатель (указатель pi2 остается некорректным)
*pi2 = 7; // неопределенное поведение
pi2 = new int(7); // теперь указатель pi2 становится корректным
int xxx = *pi2; // отлично: переменная xxx становится равной 7
Мы хотим, чтобы все некорректные указатели были нулевыми (
0
), поэтому можем провести проверку.
if (p2 == 0) { // "если указатель некорректный"
// не используйте значение *p2
}
Или еще проще:
if (p2) { // "если указатель корректный"
// используйте значение *p2
}
См. разделы 17.4 и 18.5.4.
Перечислим операции над указателями на объекты (не
void
). Операции сравнения <
, <=
, >
, >+
можно применять только к указателям одного и того же типа внутри одного и того же объекта или массива.
Подчеркнем, что операции арифметики указателей (например,
++p
и p+=7
) могут применяться только к указателям, ссылающимся на элементы массива, а эффект разыменования указателя, ссылающегося на область памяти за пределами массива, не определен (и, скорее всего, не сможет быть проверен компилятором или системой выполнения программ).
Только операции над указателем типа
void*
являются копированием (присваиванием или инициализацией) и приведением (преобразованием типа).
Указатель на функцию (см. раздел 27.2.5) можно только копировать и вызывать. Рассмотрим пример.
typedef void (*Handle_type)(int);
void my_handler(int);
Handle_type handle = my_handler;
handle(10); // эквивалент my_handler(10)
Массив (array) — это неразрывная последовательность объектов (элементов) одинакового типа, имеющая фиксированную длину.
int a[10]; // 10 целых чисел
Если массив является глобальным, то его элементы могут быть инициализированы соответствующим значением, принятым для данного типа по умолчанию. Например, значение
a[7]
равно 0
. Если массив является локальным (переменная объявлена в функции) или создан с помощью оператора new
, то элементы встроенных типов останутся неинициализированными, а элементы, имеющие пользовательский тип, будут инициализированы его конструкторами.
Имя массива неявно преобразуется в указатель на его первый элемент. Рассмотрим пример.
int* p = a; // указатель p ссылается на элемент a[0]
Массив или указатель на элемент массива может индексироваться с помощью оператора
[]
. Рассмотрим пример.
a[7] = 9;
int xx = p[6];
Элементы массива нумеруются начиная с нуля (разделы 18.5).
Диапазон индексов массива не проверяется. Кроме того, поскольку они часто передаются с помощью указателей, информация, необходимая для проверки диапазона, передается пользователям ненадежным способом. Рекомендуем использовать класс
vector
. Размер массива — это сумма размеров его элементов. Рассмотрим пример.
int a[max]; // sizeof(a) == sizeof(int)*max
Можно определить и использовать массив массивов (двумерный массив), массив массивов массивов (многомерный массив) и т.д. Рассмотрим пример.
double da[100][200][300]; // 300 элементов типа, состоящего из
da[7][9][11] = 0;
Нетривиальное использование многомерных массивов — тонкое и уязвимое для ошибок дело (см. раздел 24.4). Если у вас есть выбор, следует предпочесть класс
Matrix
(как в главе 24).
Ссылка (reference) — это синоним (alias), т.е. альтернативное имя объекта.
int a = 7;
int& r = a;
r = 8; // переменная a становится равной 8
Ссылки часто используются в качестве параметров функций, чтобы предотвратить копирование.
void f(const string& s);
// ...
f("эту строку слишком дорого копировать, \\
поэтому используется ссылка");
См. разделы 8.5.4–8.5.6.
Функция (function) — это именованный фрагмент кода, получающий (возможно, пустой) набор аргументов и (необязательно) возвращающий значение. Функция объявляется с помощью указания типа возвращаемого значения, за которым следует ее имя и список параметров.
char f(string, int);
Итак,
f
— это функция, принимающая объекты типа string
и int
и возвращающая объект типа char
. Если функция должна быть просто объявлена, но не определена, то ее объявление завершается точкой с запятой. Если функция должна быть определена, то за объявлением аргументов следует тело функции.
char f(string s, int i) { return s[i]; }
Телом функции должен быть блок (см. раздел 8.2) или блок
try
(см. раздел 5.6.3).
Функция, в объявлении которой указано, что она возвращает какое-то значение, должна его возвращать (используя оператор
return
).
char f(string s, int i) { char c = s[i]; } // ошибка: ничего
// не возвращается
Функция
main()
представляет собой странное исключение из этого правила (см. раздел A.1.2). За исключением функции main()
, если не хотите возвращать значение, то поставьте перед именем функции ключевое слово void
. Другими словами, используйте слово void
как тип возвращаемого значения.
void increment(int& x) { ++x; } // OK: возвращать значение
// не требуется
Функция вызывается с помощью оператора вызова
()
с соответствующим списком аргументов.
char x1 = f(1,2); // ошибка: первый аргумент функции f() должен
// быть строкой
string s = "Battle of Hastings";
char x2 = f(s); // ошибка: функция f() требует двух аргументов
char x3 = f(s,2); // OK
Более подробную информацию о функциях см. в главе 8.
Разрешение перегрузки (overload resolution) — это процесс выбора функции для вызова на основе набора аргументов. Рассмотрим пример.
void print(int);
void print(double);
void print(const std::string&);
print(123); // вызывается print(int)
print(1.23); // вызывается print(double)
print("123"); // вызывается print(const string&)
Компилятор, руководствуясь правилами языка, может самостоятельно выбрать правильную функцию. К сожалению, эти правила довольно сложные, поскольку они пытаются учесть максимально сложные примеры. Здесь мы приведем их упрощенный вариант.
Выбор правильного варианта перегруженной функции осуществляется на основе поиска наилучшего соответствия между типами аргументов функции и типами ее параметров (формальных аргументов).
Для конкретизации нашего представления о выборе наилучшего соответствия сформулируем несколько критериев.
1. Точное совпадение, т.е. совпадение при полном отсутствии преобразований типов или при наличии только самых простых преобразований (например, преобразование имени массива в указатель, имени функции — в указатель на функцию и типа
T
— в тип const T
).
2. Совпадение после продвижения, т.е. целочисленные продвижения (
bool
— в int
, char
— в int
, short
— в int
и их аналоги без знака; см. раздел A.8), а также преобразование типа float
в double
.
3. Совпадение после стандартных преобразований, например,
int
— в double
, double
— в int
, double
— в long double
, Derived*
— в Base*
(см. раздел 14.3), T*
— в void*
(см. раздел 17.8), int
— в unsigned int
(см. раздел 25.5.3).
4. Совпадение после преобразований, определенных пользователем (см. раздел A.5.2.3).
5. Совпадение на основе эллипсиса ... в объявлении функции (раздел A.9.3). Если найдено два совпадения, то вызов отменяется как неоднозначный. Правила разрешения перегрузки ориентированы в основном на встроенные числовые типы (см. раздел A.5.3).
Для разрешения перегрузки на основе нескольких аргументов мы сначала должны найти наилучшее совпадение для каждого аргумента. Выбирается та из функций, которая по каждому аргументу подходит так же хорошо, как и остальные функции, но лучше всех остальных соответствует вызову по одному из аргументов; в противном случае вызов считается неоднозначным. Рассмотрим пример.
void f(int, const string&, double);
void f(int, const char*, int);
f(1,"hello",1); // OK: call f(int, const char*, int)
f(1,string("hello"),1.0); // OK: call f(int, const string&, double)
f(1, "hello",1.0); // ошибка: неоднозначность
В последнем вызове строка "
hello
" соответствует типу const char*
без преобразования, а типу const string&
— только после преобразования. С другой стороны, число 1.0
соответствует типу double
без преобразования, а число типа int
— только после преобразования, поэтому ни один из вариантов функции f()
не соответствует правилам лучше других.
Если эти упрощенные правила не соответствуют правилам вашего компилятора и вашим представлениям, в первую очередь следует предположить, что ваша программа сложнее, чем требуется. Постарайтесь упростить код, в противном случае проконсультируйтесь с экспертами.
Иногда функции имеют больше аргументов, чем это требуется в наиболее часто встречающихся распространенных ситуациях. Для того чтобы учесть это обстоятельство, программист может предусмотреть аргументы по умолчанию, которые будут использоваться, если при вызове соответствующие аргументы не будут заданы. Рассмотрим пример.
void f(int, int=0, int=0);
f(1,2,3);
f(1,2); // вызовы f(1,2,0)
f(1); // вызовы f(1,0,0)
Задавать по умолчанию можно только замыкающие аргументы. Рассмотрим пример.
void g(int, int =7, int); // ошибка: по умолчанию задан
// не замыкающий аргумент
f(1,,1); // ошибка: пропущен второй аргумент
Альтернативой аргументам, заданным по умолчанию, может быть перегрузка (и наоборот).
Можно задать функцию, не указав ни количество аргументов, ни их тип. Для этого используется эллипсис (...), означающий “и, возможно, другие аргументы”. Например, вот как выглядит объявление и некоторые вызовы, вероятно, самой известной функции в языке C:
printf()
(см. разделы 27.6.1 и Б.10.2):
void printf(const char* format ...); // получает форматную строку и,
// может быть, что-то еще
int x = 'x';
printf("hello, world!");
printf("print a char '%c'\n",x); // печатает целое число x как
// символ
printf("print a string \"%s\"",x); // "выстрел себе в ногу"
Спецификаторы формата в форматной строке, такие как
%c
и %s
, определяют способ использования аргументов. Как показано выше, это может привести к ужасным последствиям. В языке C++ неопределенных аргументов лучше избегать.
Код на языке С++ часто используется наряду с кодом на языке С в одной и той же программе; иначе говоря, одни части бывают написаны на языке С++ (и скомпилированы с помощью компилятора языка С++), а другие — на языке С (и скомпилированы с помощью компилятора языка С). Для того чтобы воспользоваться этой возможностью, язык С++ предлагает программистам спецификации связей (linkage specifications), указывающие, что та или иная функция может быть вызвана из модуля, написанного на языке С. Спецификацию связи с языком С можно поместить перед объявлением функции.
extern "C" void callable_from_C(int);
В качестве альтернативы ее можно применить ко всем объявлениям в блоке.
extern "C" {
void callable_from_C(int);
int and_this_one_also(double, int*);
/* ... */
}
Детали можно найти в разделе 27.2.3.
В языке С нет возможности перегружать функции, поэтому можете поместить спецификацию связи с языком С только в одной версии перегруженной функции.
Есть два способа определить новый (пользовательский) тип: в виде класса (
class
, struct
и union
; см. раздел A.12) и в виде перечисления (enum
; см. раздел A.11).
Программист может определить смысл большинства операторов, принимающих операнды пользовательского типа. Изменить стандартный смысл операторов для встроенных типов или ввести новый оператор невозможно. Имя оператора, определенного пользователем (перегруженного оператора), состоит из символа оператора, которому предшествует ключевое слово
operator
; например, имя функции, определяющей оператор +
, выглядит как operator +
.
Matrix operator+(const Matrix&, const Matrix&);
Примеры можно найти в определениях классов
std::ostream
(см. главы 10-11), std::vector
(см. главы 17–19 и раздел Б.4), std::complex
(см. раздел Б.9.3) и Matrix
(см. главу 24).
Перегрузить можно все операторы за исключением следующих:
?: . .* :: sizeof typeid
Функции, определяющие следующие операторы, должны быть членами класса:
= [ ] ( ) –>
Все остальные операторы можно определить и как члены-функции, и как самостоятельные функции.
Обратите внимание на то, что каждый пользовательский тип имеет оператор
=
(присваивание и инициализация), &
(взятие адреса) и ,
(запятая), определенные по умолчанию.
При перегрузке операторов следует проявлять умеренность и придерживаться общепринятых соглашений.
Перечисление (enumeration) определяет тип, содержащий набор именованных значения (перечислителей).
enum Color { green, yellow, red };
По умолчанию первый перечислитель равен нулю
0
, так что green==0
, а остальные значения увеличиваются на единицу, так что yellow==1
и red==2
. Кроме того, можно явно определить значение перечислителя.
enum Day { Monday=1,Tuesday,Wednesday };
Итак,
Monday==1
, Tuesday==2
и Wednesday==3
.
Отметим, что перечислители принадлежат не области видимости своего перечисления, а охватывающей области видимости.
int x = green; // OK
int y = Color::green; // ошибка
Перечислители и значения перечислений неявно преобразовываются в целые числа, но целые числа не преобразовываются в типы перечислений неявно.
int x = green; // OK: неявное преобразование Color в int
Color c = green; // OK
c = 2; // ошибка: нет неявного преобразования
// int в Color
c = Color(2); // OK: (непроверяемое) явное преобразование
int y = c; // OK: неявное преобразование Color в int
Использование перечислений обсуждается в разделе 9.5.
Класс (class) — это тип, для которого пользователь определил представление его объектов и операции, допустимые для этих объектов.
class X {
public:
// пользовательский интерфейс
private:
// реализация
};
Переменные, функции и типы, определенные в объявлении класса, называются членами этого класса. Технические детали изложены в главе 9.
Открытый член класса доступен для пользователей; закрытый член класса доступен только членам класса.
class Date {
public:
// ...
int next_day();
private:
int y, m, d;
};
void Date::next_day() { return d+1; } // OK
void f(Date d)
{
int nd = d.d+1; // ошибка: Date::d — закрытый член класса
// ...
}
Структура — это класс, члены которого по умолчанию являются открытыми.
struct S {
// члены (открытые, если явно не объявлены закрытыми)
};
Более подробная информация о доступе к членам класса, включая обсуждение защищенных членов, приведена в разделе 14.3.4.
К членам объекта можно обращаться с помощью оператора
.
(точка), примененного к его имени, или оператора –>
(стрелка), примененного к указателю на него.
struct Date {
int d, m, y;
int day() const { return d; } // определенный в классе
int month() const; // просто объявленный; определен
// в другом месте
int year() const; // просто объявленный; определен
// в другом месте
};
Date x;
x.d = 15; // доступ через переменную
int y = x.day(); // вызов через переменную
Date* p = &x;
p–>m = 7; // доступ через указатель
int z = p–>month(); // вызов через указатель
На члены класса можно ссылаться с помощью оператора
::
(разрешение области видимости).
int Date::year() const { return y; } // определение за пределами
// класса
В функциях-членах класса можно ссылаться на другие члены класса, не указывая имя класса.
struct Date {
int d, m, y;
int day() const { return d; }
// ...
};
Такие имена относятся к объекту, из которого вызвана функция:
void f(Date d1, Date d2)
{
d1.day(); // обращается к члену d1.d
d2.day(); // обращается к члену d2.d
// ...
}
Если хотите явно сослаться на объект, из которого вызвана функция-член, то можете использовать зарезервированный указатель
this
.
struct Date {
int d, m, y;
int month() const { return this–>m; }
// ...
};
Функция-член, объявленная с помощью спецификатора
const
(константная функция-член), не может изменять значение члена объекта, из которого она вызвана.
struct Date {
int d, m, y;
int month() const { ++m; } // ошибка: month() — константная
// функция
// ...
};
Более подробная информация о константных функциях-членах изложена в разделе 9.7.4.
Функция, не являющаяся членом класса, может получить доступ ко всем членам класса, если ее объявить с помощью ключевого слова
friend
. Рассмотрим пример.
// требует доступа к членам классов Matrix и Vector members:
Vector operator*(const Matrix&, const Vector&);
class Vector {
friend
Vector operator*(const Matrix&, const Vector&); // есть доступ
// ...
};
class Matrix {
friend
Vector operator*(const Matrix&, const Vector&); // есть доступ
// ...
};
Как показано выше, обычно это относится к функциям, которым нужен доступ к двум классам. Другое предназначение ключевого слова
friend
— обеспечивать функцию доступа, которую нельзя вызывать как функцию-член.
class Iter {
public:
int distance_to(const iter& a) const;
friend int difference(const Iter& a, const Iter& b);
// ...
};
void f(Iter& p, Iter& q)
{
int x = p.distance_to(q); // вызов функции-члена
int y = difference(p,q); // вызов с помощью математического
// синтаксиса
// ...
}
Отметим, что функцию, объявленную с помощью ключевого слова
friend
, нельзя объявлять виртуальной.
Члены класса, являющиеся целочисленными константами, функциями или типами, могут быть определены как в классе, так и вне его.
struct S {
static const int c = 1;
static const int c2;
void f() { }
void f2();
struct SS { int a; };
struct SS2;
};
Члены, которые не были определены в классе, должны быть определены “где-то”.
const int S::c2 = 7;
void S::f2() { }
struct S::SS2 { int m; };
Статические константные целочисленные члены класса (
static const int
) представляют собой особый случай. Они просто определяют символические целочисленные константы и не находятся в памяти, занимаемой объектом. Нестатические данные-члены не требуют отдельного определения, не могут быть определены отдельно и инициализироваться в классе.
struct X {
int x;
int y = 7; // ошибка: нестатические данные-члены
// не могут инициализироваться внутри класса
static int z = 7; // ошибка: данные-члены, не являющиеся
// константами, не могут инициализироваться
// внутри класса
static const string ae = "7"; // ошибка: нецелочисленный тип
// нельзя инициализировать
// внутри класса
static const int oe = 7; // OK: статический константный
// целочисленный тип
};
int X::x = 7; // ошибка: нестатические члены класса нельзя
// определять вне класса
Если вам необходимо инициализировать не статические и не константные данные-члены, используйте конструкторы.
Функции-члены не занимают память, выделенную для объекта.
struct S {
int m;
void f();
};
Здесь
sizeof(S)==sizeof(int)
. На самом деле стандартом это условие не регламентировано, но во всех известных реализациях языка оно выполняется. Следует подчеркнуть, что класс с виртуальной функцией имеет один скрытый член, обеспечивающий виртуальные вызовы (см. раздел 14.3.1).
Определить смысл инициализации объекта класса можно, определив один или несколько конструкторов (constructors). Конструктор — это функция-член, не имеющая возвращаемого значения, имя которой совпадает с именем класса.
class Date {
public:
Date(int yy,int mm,int dd):y(yy),m(mm),d(dd) { }
// ...
private:
int y,m,d;
};
Date d1(2006,11,15); // OK: инициализация с помощью конструктора
Date d2; // ошибка: нет инициализации
Date d3(11,15); // ошибка: неправильная инициализация
// (требуются три инициализатора)
Обратите внимание на то, что данные-члены могут быть инициализированы с помощью списка инициализации в конструкторе. Члены класса инициализируются в порядке их определения в классе.
Конструкторы обычно используются для установления инвариантов класса и получения ресурсов (см. разделы 9.4.2 и 9.4.3).
Объекты класса создаются снизу вверх, начиная с объектов базового класса (см. раздел 14.3.1) в порядке их объявления. Затем в порядке объявления создаются члены класса, после чего следует код самого конструктора. Если программист не сделает чего-нибудь очень странного, это гарантирует, что каждый объект класса будет создан до своего использования.
Если конструктор с одним аргументом не объявлен с помощью ключевого слова
explicit
, то он определяет неявное преобразование типа своего аргумента в свой класс.
class Date {
public:
Date(string);
explicit Date(long); // используется целочисленное
// представление даты
// ...
};
void f(Date);
Date d1 = "June 5, 1848"; // OK
f("June 5, 1848"); // OK
Date d2 = 2007*12*31+6*31+5; // ошибка: Date(long) — явный
// конструктор
f(2007*12*31+6*31+5); // ошибка: Date(long) — явный конструктор
Date d3(2007*12*31+6*31+5); // OK
Date d4 = Date(2007*12*31+6*31+5); // OK
f(Date(2007*12*31+6*31+5)); // OK
Если базовые классы или члены производного класса не требуют явных аргументов и в классе нет других конструкторов, то автоматически генерируется конструктор по умолчанию (default constructor). Этот конструктор инициализирует каждый объект базового класса и каждый член, имеющий конструктор по умолчанию (оставляя члены, не имеющие конструкторы по умолчанию, неинициализированными). Рассмотрим пример.
struct S {
string name, address;
int x;
};
Этот класс
S
имеет неявный конструктор S()
, инициализирующий члены name и address
, но не x
.
Смысл операции удаления объекта (т.е. что произойдет, когда объект выйдет за пределы области видимости) можно определить с помощью деструктора (destructor). Имя деструктора состоит из символа
~
(оператор дополнения), за которым следует имя класса.
class Vector { // вектор чисел типа double
public:
explicit Vector(int s):sz(s),p(new double[s]) { }
// конструктор
~Vector() { delete[] p; }
// деструктор
// ...
private:
int sz;
double* p;
};
void f(int ss)
{
Vector v(s);
// ...
} // при выходе из функции f() объект v будет уничтожен;
// для этого будет вызван деструктор класса Vector
Деструкторы, вызывающие деструкторы членов класса, могут генерироваться компилятором. Если класс используется как базовый, он обычно должен иметь виртуальный деструктор (см. раздел 17.5.2).
Деструкторы, как правило, используются для “очистки” и освобождения ресурсов. Объекты класса уничтожаются сверху вниз, начиная с кода самого деструктора, за которым следуют члены в порядке их объявления, а затем — объекты базового класса в порядке их объявления, т.е. в порядке, обратном их созданию (см. раздел A.12.3.1).
Можно определить суть копирования объекта класса.
class Vector { // вектор чисел типа double
public:
explicit Vector(int s):sz(s), p(new double[s]) { }
// конструктор
~Vector() { delete[] p; } // деструктор
Vector(const Vector&); // копирующий конструктор
Vector& operator=(const Vector&); // копирующее присваивание
// ...
private:
int sz;
double* p;
};
void f(int ss)
{
Vector v(s);
Vector v2 = v; // используем копирующий конструктор
// ...
v = v2; // используем копирующее присваивание
// ...
}
По умолчанию (т.е. если вы не определили копирующий конструктор и копирующее присваивание) компилятор сам генерирует копирующие операции. По умолчанию копирование производится почленно (см. также разделы 14.2.4 и 18.2).
Класс можно определить производным от других классов. В этом случае он наследует члены классов, от которых происходит (своих базовых классов).
struct B {
int mb;
void fb() { };
};
class D:B {
int md;
void fd();
};
В данном случае класс
B
имеет два члена: mb
и fb()
, а класс D
— четыре члена: mb
, fb()
, md
и fd()
.
Как и члены класса, базовые классы могут быть открытыми и закрытыми (
public
или private
).
Class DD:public B1,private B2 {
// ...
};
В таком случае открытые члены класса
B1
становятся открытыми членами класса DD
, а открытые члены класса B2
— закрытыми членами класса DD
. Производный класс не имеет особых привилегий доступа к членам базового класса, поэтому члены класса DD
не имеют доступа к закрытым членам классов B1
и B2
.
Если класс имеет несколько непосредственных базовых классов (как, например, класс
DD
), то говорят, что он использует множественное наследование (multiple inheritance).
Указатель на производный класс
D
можно неявно преобразовать в указатель на его базовый класс B
при условии, что класс B
является доступным и однозначным по отношению к классу D
. Рассмотрим пример.
struct B { };
struct B1: B { }; // B — открытый базовый класс по отношению
// к классу B1
struct B2: B { }; // B — открытый базовый класс по отношению
// к классу B1
struct C { };
struct DD : B1, B2, private C { };
DD* p = new DD;
B1* pb1 = p; // OK
B* pb = p; // ошибка: неоднозначность: B1::B или B2::B
C* pc = p; // ошибка: DD::C — закрытый класс
Аналогично, ссылку на производный класс можно неявно преобразовать в ссылку на однозначный и доступный базовый класс.
Более подробную информацию о производных классах можно найти в разделе 14.3. Описание защищенного наследования (
protected
) изложено во многих учебниках повышенной сложности и в справочниках.
Виртуальная функция (virtual function) — это функция-член, определяющая интерфейс вызова функций, имеющих одинаковые имена и одинаковые типы аргументов в производных классах. При вызове виртуальной функции она должна быть определена хотя бы в одном из производных классов. В этом случае говорят, что производный класс замещает (override) виртуальную функцию-член базового класса.
class Shape {
public:
virtual void draw(); // "virtual" означает "может быть
// замещена"
virtual ~Shape() { } // виртуальный деструктор
// ...
};
class Circle:public Shape {
public:
void draw(); // замещает функцию Shape::draw
~Circle(); // замещает функцию Shape::~Shape()
// ...
};
По существу, виртуальные функции базового класса (в данном случае класса
Shape
) определяют интерфейс вызова функций производного класса (в данном случае класса Circle
).
void f(Shape& s)
{
// ...
s.draw();
}
void g()
{
Circle c(Point(0,0), 4);
f(c); // вызов функции draw из класса Circle
}
Обратите внимание на то, что функция
f()
ничего не знает о классе Circle
: ей известен только класс Shape
. Объект класса, содержащего виртуальную функцию, содержит один дополнительный указатель, позволяющий найти набор виртуальных функций (см. раздел 14.3).
Подчеркнем, что класс, содержащий виртуальные функции, как правило, должен содержать виртуальный деструктор (как, например, класс
Shape
); см. раздел 17.5.2.
Абстрактный класс (abstract class) — это класс, который можно использовать только в качестве базового класса. Объект абстрактного класса создать невозможно.
Shape s; // ошибка: класс Shape является абстрактным
class Circle:public Shape {
public:
void draw(); // замещает override Shape::draw
// ...
};
Circle c(p,20); // OK: класс Circle не является абстрактным
Наиболее распространенным способом создания абстрактного класса является определение как минимум одной чисто виртуальной функции (pure virtual function), т.е. функции, требующей замещения.
class Shape {
public:
virtual void draw() = 0; // =0 означает "чисто виртуальная"
// ...
};
См. раздел 14.3.5.
Реже, но не менее эффективно абстрактные классы создаются путем объявления всех их конструкторов защищенными (
protected
); см раздел. 14.2.1.
При определении классов некоторые операции над их объектами будут определены по умолчанию.
• Конструктор по умолчанию.
• Копирующие операции (копирующее присваивание и копирующая инициализация).
• Деструктор.
Каждый из них (также по умолчанию) может рекурсивно применяться к каждому из своих базовых классов и членов. Создание производится снизу вверх, т.е. объект базового класса создается до создания членов производного класса. Члены производного класса и объекты базовых классов создаются в порядке их объявления и уничтожаются в обратном порядке. Таким образом, конструктор и деструктор всегда работают с точно определенными объектами базовых классов и членов производного класса. Рассмотрим пример.
struct D:B1, B2 {
M1 m1;
M2 m2;
};
Предполагая, что классы
B1
, B2
, M1
и M2
определены, можем написать следующий код:
void f()
{
D d; // инициализация по умолчанию
D d2 = d; // копирующая инициализация
d = D(); // инициализация по умолчанию,
// за которой следует копирующее присваивание
} // объекты d и d2 уничтожаются здесь
Например, инициализация объекта
d
по умолчанию выполняется путем вызова четырех конструкторов по умолчанию (в указанном порядке): B1::B1()
, B2::B2()
, M1::M1()
и M2::M2()
. Если один из этих конструкторов не определен или не может быть вызван, то создание объекта d
невозможно. Уничтожение объекта d
выполняется путем вызова четырех деструкторов (в указанном порядке): M2::~M2()
, M1::~M1()
, B2::~B2()
и B1::~B1()
. Если один из этих деструкторов не определен или не может быть вызван, то уничтожение объекта d
невозможно. Каждый из этих конструкторов и деструкторов может быть либо определен пользователем, либо сгенерирован автоматически.
Если класс имеет конструктор, определенный пользователем, то неявный (сгенерированный компилятором) конструктор по умолчанию остается неопределенным (не генерируется).
Битовое поле (bitfield) — это механизм упаковки многих маленьких значений в виде слова или в соответствии с установленным извне битовым форматом (например, форматом регистра какого-нибудь устройства). Рассмотрим пример.
struct PPN {
unsigned int PFN: 22;
int: 3; // не используется
unsigned int CCA;
bool nonreacheable;
bool dirty;
bool valid;
bool global;
};
Упаковка битовых полей в виде слова слева направо приводит к следующему формату (см. раздел 25.5.5).
Битовое поле не обязано иметь имя, но если его нет, то к нему невозможно обратиться. Как это ни удивительно, но упаковка многих небольших значений в отдельное слово не всегда экономит память. На самом деле использование одного из таких значений приводит к излишнему расходу памяти по сравнению с использованием типа
char
или int
даже для представления одного бита. Причина заключается в том, что для извлечения бита из слова и для записи бита в слово без изменения других битов необходимо выполнить несколько инструкций (которые также хранятся где-то в памяти). Не пытайтесь создавать битовые поля для экономии памяти, если у вас нет большого количества объектов с очень маленькими полями данных.
Объединение (union) — это класс, в котором все члены расположены в одной и той же области памяти. В каждый момент времени объединение может содержать только один элемент, причем считывается только тот элемент объединения, который был записан последним. Рассмотрим пример.
union U {
int x;
double d;
}
U a;
a.x = 7;
int x1 = a.x; // OK
a.d = 7.7;
int x2 = a.x; // Ой!
Правила согласованного чтения и записи членов объединения компилятором не проверяются. Мы вас предупредили.
Шаблон (template) — это класс или функция, параметризованные набором типов и/или целыми числами.
template
class vector {
public:
// ...
int size() const;
private:
int sz;
T* p;
};
template
int vector::size() const
{
return sz;
}
В списке шаблонных аргументов ключевое слово
class
означает тип; его эквивалентной альтернативой является ключевое слово typename
. Функция-член шаблонного класса по умолчанию является шаблонной функцией с тем же списком шаблонных аргументов, что и у класса.
Целочисленные шаблонные аргументы должны быть константными выражениями.
template
class Fixed_array {
public:
T a[sz];
// ...
int size() const { return sz; };
};
Fixed_array x1; // OK
int var = 226;
Fixed_array x2; // ошибка: неконстантный шаблонный аргумент
Аргументы шаблонного класса указываются каждый раз, когда используется его имя.
vector v1; // OK
vector v2; // ошибка: пропущен шаблонный аргумент
vector v3; // ошибка: слишком много шаблонных аргументов
vector<2> v4; // ошибка: ожидается тип шаблонного аргумента
Аргументы шаблонной функции обычно выводятся из ее аргументов.
template
T find(vector& v, int i)
{
return v[i];
}
vector v1;
vector v2;
// ...
int x1 = find(v1,2); // здесь тип T — это int
int x2 = find(v2,2); // здесь тип T — это double
Можно объявить шаблонную функцию, для которой невозможно вывести ее шаблонные аргументы. В этом случае мы должны конкретизировать шаблонные аргументы явно (точно так же, как для шаблонных классов). Рассмотрим пример.
template T* make(const U& u) { return new T(u); }
int* pi = make(2);
Node* pn = make(make_pair("hello",17));
Этот код работает, только если объект класса
Node
можно инициализировать объектом класса pair
(раздел Б.6.3). Из механизма явной конкретизации шаблонной функции можно исключать только замыкающие шаблонные аргументы (которые будут выведены).
Вариант шаблона для конкретного набора шаблонных аргументов называется специализацией (specialization). Процесс генерации специализаций на основе шаблона и набора аргументов называется конкретизацией шаблона (template instantiation). Как правило, эту задачу решает компилятор, но программист также может самостоятельно определить отдельную специализацию. Обычно это делается, когда общий шаблон для конкретного набора аргументов неприемлем. Рассмотрим пример.
template struct Compare { // общее сравнение
bool operator()(const T& a,const T& b) const
{
return a
}
};
template<> struct Compare { // сравнение C-строк
bool operator()(const char* a,const char* b) const
{
return strcmp(a,b)==0;
}
};
Compare c2; // общее сравнение
Compare c; // сравнение С-строк
bool b1 = c2(1,2); // общее сравнение
bool b2 = c("asd","dfg"); // сравнение С-строк
Аналогом специализации для функций является перегрузка.
template bool compare(const T& a,const T& b)
{
return a
}
bool compare (const char* a, const char* b) // сравнение С-строк
{
return strcmp(a,b)==0;
}
bool b3 = compare(2,3); // общее сравнение
bool b4 = compare("asd","dfg"); // сравнение С-строк
Отдельная компиляция шаблонов (когда в заголовочных файлах содержатся только объявления, а в исходных файлах — однозначные определения) не гарантирует переносимость программы, поэтому, если шаблон необходимо использовать в разных исходных файлах, в заголовочном файле следует дать его полное определение.
Шаблон может иметь как члены, являющиеся типами, так и члены, не являющиеся типами (как данные-члены и функции-члены). Это значит, что в принципе трудно сказать, относится ли имя члена к типу или нет. По техническим причинам, связанным с особенностями языка программирования, компилятор должен знать это, поэтому мы ему должны каким-то образом передать эту информацию. Для этого используется ключевое слово
typename
. Рассмотрим пример.
template struct Vec {
typedef T value_type; // имя члена
static int count; // данное-член
// ...
};
template void my_fct(Vec& v)
{
int x = Vec::count; // имена членов по умолчанию
// считаются относящимися не к типу
v.count = 7; // более простой способ сослаться
// на член, не являющийся типом
typename Vec::value_type xx = x; // здесь нужно слово
// "typename"
// ...
}
Более подробная информация о шаблонах приведена в главе 19.
Исключения используются (посредством инструкции
throw
) для того, чтобы сообщить вызывающей функции об ошибке, которую невозможно обработать на месте. Например, спровоцируем исключение Bad_size
в классе Vector
.
struct Bad_size {
int sz;
Bad_size(int s):ss(s) { }
};
class Vector {
Vector(int s) { if (s<0 || maxsize
// ...
};
Как правило, мы генерируем тип, определенный специально для представления конкретной ошибки. Вызывающая функция может перехватить исключение.
void f(int x)
{
try {
Vector v(x); // может генерировать исключения
// ...
}
catch (Bad_size bs) {
cerr << "Вектор неправильного размера (" << bs.sz << ")\n";
// ...
}
}
Для перехвата всех исключений можно использовать инструкцию
catch (...)
.
try {
// ...
} catch (...) { // перехват всех исключений
// ...
}
Как правило, лучше (проще, легче, надежнее) применять технологию RAII (“Resource Acquisition Is Initialization” — “выделение ресурсов — это инициализация”), чем использовать множество явных инструкций
try
и catch
(см. раздел 19.5).
Инструкция
throw
без аргументов (т.е. throw;
) повторно генерирует текущее исключение. Рассмотрим пример.
try {
// ...
} catch (Some_exception& e) {
// локальная очистка
throw; // остальное сделает вызывающая функция
}
В качестве исключений можно использовать типы, определенные пользователем. В стандартной библиотеке определено несколько типов исключений, которые также можно использовать (раздел Б.2.1). Никогда не используйте в качестве исключений встроенные типы (это может сделать кто-то еще, и ваши исключения могут внести путаницу).
Когда генерируется исключение, система поддержки выполнения программ на языке С++ ищет вверх по стеку раздел
catch
, тип которого соответствует типу генерируемого объекта. Другими словами, она ищет инструкции try
в функции, генерирующей исключение, затем в функции, вызвавшей функцию, генерирующую исключение, затем в функции, вызвавшей функцию, вызвавшей функцию, которая генерирует исключение, пока не найдет соответствие. Если соответствие найдено не будет, программа прекратит работу. В каждой функции, обнаруженной на этом пути, и в каждой области видимости, в которой проходит поиск, вызывается деструктор. Этот процесс называется раскруткой стека (stack unwinding).
Объект считается созданным в тот момент, когда заканчивает работу его конструктор. Он уничтожается либо в процессе раскрутки стека, либо при каком-либо ином выходе из своей области видимости. Это подразумевает, что частично созданные объекты (у которых некоторые члены или базовые объекты созданы, а некоторые — нет), массивы и переменные, находящиеся в области видимости, обрабатываются корректно. Подобъекты уничтожаются, если и только если они ранее были созданы. Не генерируйте исключение, передающееся из деструктора в вызывающий модуль. Иначе говоря, деструктор не должен давать сбой. Рассмотрим пример.
X::~X() { if (in_a_real_mess()) throw Mess(); } // никогда так
// не делайте!
Основная причина этого “драконовского” правила заключается в том, что если деструктор сгенерирует исключение (или сам не перехватит исключение) в процессе раскрутки стека, то мы не сможем узнать, какое исключение следует обработать. Целесообразно всеми силами избегать ситуаций, в которых выход из деструктора происходит с помощью генерирования исключения, поскольку не существует систематического способа создания правильного кода, в котором это может произойти. В частности, если это произойдет, не гарантируется правильная работа ни одной функции или класса из стандартной библиотеки.
Пространство имен (namespace) объединяет связанные друг с другом объявления и предотвращает коллизию имен.
int a;
namespace Foo {
int a;
void f(int i)
{
a+= i; // это переменная a из пространства имен Foo
// (Foo::a)
}
}
void f(int);
int main()
{
a = 7; // это глобальная переменная a (::a)
f(2); // это глобальная функция f (::f)
Foo::f(3); // это функция f из пространства имен Foo
::f(4); // это глобальная функция f (::f)
}
Имена можно явно уточнять именами их пространств имен (например,
Foo::f(3)
) или оператором разрешения области видимости ::
(например, ::f(2)
), который относится к глобальному пространству имен.
Все имена в пространстве имен (например, в стандартном пространстве
std
) можно сделать доступными с помощью директивы using namespace std
;
Будьте осторожны с директивой
using
. Удобство, которое она предоставляет, достигается за счет потенциальной коллизии имен. В частности, старайтесь избегать директив using
в заголовочных файлах. Отдельное имя из пространства имен можно сделать доступным с помощью объявления пространства имен.
using Foo::g;
g(2); // это функция g из пространства имен Foo (Foo::g)
Более подробная информация о пространствах имен содержится в разделе 8.7.
Для имени можно определить альтернативное имя (alias); иначе говоря, можно определить символическое имя, которое будет означать то же самое, что и имя, с которым оно связано (для большинства случаев употребления этого имени).
typedef int* Pint; // Pint — это указатель на int
namespace Long_library_name { /* ... */ }
namespace Lib = Long_library_name; // Lib — это Long_library_name
int x = 7;
int& r = x; // r — это x
Ссылки (см. разделы 8.5.5 и A.8.3) — это механизм указания на объекты, работающий на этапе выполнения программы. Ключевые слова
typedef
(см. разделы 20.5 и 27.3.1) и namespace
относятся к механизмам ссылок на имена, работающим на этапе компиляции. В частности, инструкция typedef
не вводит новый тип, а просто задает новое имя существующего типа. Рассмотрим пример.
typedef char* Pchar; // Pchar — это имя типа char*
Pchar p = "Idefix"; // OK: p — это указатель типа char*
char* q = p; // OK: p и q — указатели типа char
int x = strlen(p); // OK: p — указатель типа char*
Каждая реализация языка C++ содержит препроцессор (preprocessor). В принципе препроцессор работает до компилятора и преобразовывает исходный код, написанный нами, в то, что видит компилятор. В действительности это действие интегрировано в компиляторе и не представляет интереса, за исключением того, что оно может вызывать проблемы. Каждая строка, начинающаяся символом
#
, представляет собой директиву препроцессора.
Мы широко использовали препроцессор для включения заголовочных файлов. Рассмотрим пример.
#include "file.h"
Эта директива приказывает препроцессору включить содержимое файла
file.h
в точку исходного текста, где стоит сама директива. Для стандартных заголовков используются угловые скобки (<...>
), а не кавычки ("..."
). Например:
#include
Это рекомендованная система обозначений для включения стандартных заголовков.
Препроцессор выполняет также определенные манипуляции с символами, которые называются макроподстановками (macro substitution). Например, определим имя символьной строки.
#define FOO bar
Теперь везде, где препроцессор увидит символы
FOO
, они будут заменены символами bar
.
int FOO = 7;
int FOOL = 9;
В таком случае компилятор увидит следующий текст:
int bar = 7;
int FOOL = 9;
Обратите внимание на то, что препроцессор знает об именах языка С++ достаточно много, чтобы не заменить символы
FOO
, являющиеся частью слова FOOL
.
С помощью директивы
define
можно также определить макросы, принимающие параметры.
#define MAX(x,y) (((x)>(y))?(x) : (y))
Их можно использовать следующим образом:
int xx = MAX(FOO+1,7);
int yy = MAX(++xx,9);
Эти выражения будут развернуты так:
int xx = (((bar+1)>( 7))?(bar+1) : (7));
int yy = (((++xx)>( 9))?(++xx) : (9));
Подчеркнем, что скобки необходимы для того, чтобы получить правильный результат при вычислении выражения
FOO+1
. Кроме того, переменная xx
была инкрементирована дважды совершенно неочевидным образом. Макросы чрезвычайно популярны, в основном потому, что программисты на языке С имели мало альтернатив. Обычные заголовочные файлы содержат определения тысяч макросов. Будьте осторожны!
Если уж вам приходится использовать макросы, то называйте их, используя только прописные буквы, например
ALL_CAPITAL_LETTERS
, а обычные имена не должны состоять только из прописных букв. Прислушайтесь к хорошему совету. Например, в одном из вполне авторитетных заголовочных файлов мы нашли макрос max
.
См. также раздел 27.8.
“По возможности, вся сложность должна быть скрыта
от постороннего взгляда”.
Дэвид Дж. Уилер (David J. Wheeler)
Это приложение содержит краткий обзор основных возможностей стандартной библиотеки языка С++. Изложенная в нем информация носит выборочный характер и предназначена для новичков, желающих получить общее представление о возможностях стандартной библиотеки и узнать немного больше, чем написано в основном тексте книги.
Это приложение является справочником и не предназначено для последовательного чтения от начала до конца, как обычная глава. В нем более или менее систематично описываются основные элементы стандартной библиотеки языка С++. Впрочем, этот справочник не полон; он представляет собой краткий обзор с немногочисленными примерами, иллюстрирующими ключевые возможности. За более подробным объяснением читателям часто придется обращаться к соответствующим главам данной книги. Кроме того, следует подчеркнуть, что мы не стремились к точности стандарта и не придерживались его терминологии. Более подробную информацию читатели найдут в книге Stroustrup, The C++ Programming Language[13]. Полным определением языка является стандарт ISO C++, но этот документ не предназначен для новичков и не подходит для первоначального изучения языка. Не забудьте также об использовании документации, доступной в Интернете.
Какая польза от выборочного (а значит, неполного) обзора? Вы можете быстро найти известную операцию или бегло просмотреть раздел в поисках доступных операций. Вы можете найти очень подробную информацию в других источниках: но что конкретно искать, вам подскажет именно этот краткий обзор. В этом приложении содержатся перекрестные ссылки на учебный материал из других глав, а также кратко изложены возможности стандартной библиотеки. Пожалуйста, не старайтесь запомнить изложенные в нем сведения; они предназначены не для этого. Наоборот, это приложение позволит вам избавиться от необходимости запоминать лишнее.
Здесь вы можете найти готовые средства, вместо того, чтобы изобретать их самостоятельно. Все, что есть в стандартной библиотеке (и особенно все, что перечислено в приложении), оказалось весьма полезным для многих людей. Стандартные возможности библиотеки практически всегда разработаны, реализованы и документированы намного лучше, чем это можете сделать вы, находясь в цейтноте. Кроме того, их переносимость из одной системы в другую обеспечена намного лучше. Итак, по возможности всегда следует отдавать предпочтение стандартным библиотечным средства, а не “самогону” (“home brew”). В таком случае ваш код будет намного понятнее.
Если вы чувствительная натура, то огромное количество возможностей может вас напугать. Не бойтесь, просто игнорируйте то, что вам не нужно. Если же вы дотошный человек, то обнаружите, что о многом мы не сказали. Полнота нужна лишь для справочников, предназначенных для экспертов, и онлайн-документации. В любом случае многое покажется вам загадочным и, возможно, интересным. Постигайте эти тайны!
Интерфейсы средств из стандартной библиотеки определены в заголовках. Некоторые из заголовков, упомянутых в следующей таблице, не входят в стандарт языка C++, принятый ISO в 1998 году. Тем не менее они станут частью следующего стандарта и в настоящее время являются широкодоступными. Такие заголовки обозначены “C++0x”. Для их использования может потребоваться отдельная инсталляция и/или пространство имен, отличающееся от
std
(например, tr1
или boost
). В этом разделе вы узнаете, какие средства могут стать доступными в вашей программе, а также можете угадать, где они определены и описаны.
Для каждого заголовка стандартной библиотеки языка С существует аналогичный заголовочный файл без первой буквы c в имени и с расширением
.h
, например заголовочный файл
для заголовка
. Версии заголовков с окончанием .h
определяют глобальные имена, а не имена в пространстве имен std
.
Некоторые, но не все средства, определенные в этих заголовках, описаны в следующих разделах и главах основного текста книги. Если вам необходима более полная информация, обратитесь к онлайн-документации или к книге по языку С++ экспертного уровня.
Средства стандартной библиотеки определены в пространстве имен
std
, поэтому, чтобы использовать их, необходимо указать их явную квалификацию, выполнить объявление using
или директиву using
.
std::string s; // явная квалификация
using std::vector; // объявление using
vectorv(7);
using namespace std; // директива using
map m;
В этой книге для доступа к пространству имен
std
мы использовали директиву using
. Будьте осторожны с директивами using
(см. раздел A.15).
Полное описание даже простой операции из стандартной библиотеки, например конструктора или алгоритма, может занять несколько страниц. По этой причине мы используем чрезвычайно лаконичный стиль представления. Рассмотрим пример.
Мы старались выбирать мнемонические идентификаторы, поэтому символы
b,e
будут обозначать итераторы, задающие начало и конец диапазона; p
— указатель или итератор; x
— некое значение, полностью зависящее от контекста. В этой системе обозначений отличить функцию, не возвращающую никакого результата, от функции, возвращающей переменную булевого типа, без дополнительных комментариев невозможно, поэтому, если не приложить дополнительных усилий, их можно перепутать. Для операций, возвращающих переменную типа bool
, в объяснении обычно стоит знак вопроса.
Если алгоритмы следуют общепринятым соглашениям, возвращая конец входной последовательности для обозначения событий “отказ”, “не найден” и т.п. (раздел Б.3.1), то мы это явно не указываем.
Стандартная библиотека состоит из компонентов, которые разрабатывались в течение сорока лет. По этой причине ее стиль и принципы обработки ошибок являются несогласованными.
• Библиотека в стиле языка С состоит из функций, многие из которых для индикации ошибок устанавливают флаг
errno
(см. раздел 24.8).
• Многие алгоритмы для последовательностей элементов возвращают итератор, установленный на элемент, следующий за последним, отмечая тем самым, что произошла ошибка или искомый элемент не найден.
• Библиотека потоков ввода-вывода для сообщений об ошибках использует состояние каждого потока и может (если пользователь этого потребует) генерировать исключения (см. разделы 10.6 и Б.7.2).
• Некоторые компоненты стандартной библиотеки, такие как
vector
, string
и bitset
, при обнаружении ошибок генерируют исключения.
Стандартная библиотека разработана так, чтобы все ее средства удовлетворяли базовым условиям (см. раздел 19.5.3). Иначе говоря, даже если исключение сгенерировано, ни один ресурс (например, память) не будет потерян и ни один инвариант класса из стандартной библиотеки не будет нарушен.
Некоторые средства стандартной библиотеки сообщают об ошибках, генерируя исключения.
Эти исключения могут возникнуть в любом коде, явно или неявно использующем указанные средства библиотеки. Если вы уверены, что все использованные средства были использованы правильно и поэтому не могли сгенерировать исключение, то целесообразно всегда в каком-то месте (например, в функции
main()
) перехватывать объекты одного из корневых классов иерархии исключений из стандартной библиотеки (например, exception
).
Мы настоятельно рекомендуем не генерировать исключения встроенных типов, например числа типа
int
или строки в стиле языка C. Вместо этого следует генерировать объекты типов, специально разработанных для использования в качестве исключения. Для этого можно использовать класс, производный от стандартного библиотечного класса exception
.
class exception {
public:
exception();
exception(const exception&);
exception& operator=(const exception&);
virtual ~exception();
virtual const char* what() const;
};
Функцию
what()
можно использовать для того, чтобы получить строку, предназначенную для представления информации об ошибки, вызвавшей исключение.
Приведенная ниже иерархия стандартных исключений может помочь вам классифицировать исключения.
Можете определить исключение, выведя его из стандартного библиотечного исключения следующим образом:
struct My_error:runtime_error {
My_error(int x):interesting_value(x) { }
int interesting_value;
const char* what() const { return "My_error"; }
};
Итераторы — это клей, скрепляющий алгоритмы стандартной библиотеки с их данными. Итераторы можно также назвать механизмом, минимизирующим зависимость алгоритмов от структуры данных, которыми они оперируют (см. раздел 20.3).
Итератор — это аналог указателя, в котором реализованы операции косвенного доступа (например, оператор
*
для разыменования) и перехода к новому элементу (например, оператор ++
для перехода к следующему элементу). Последовательность элементов определяется парой итераторов, задающих полуоткрытый диапазон [begin:end]
.
Иначе говоря, итератор
begin
указывает на первый элемент последовательности, а итератор end
— на элемент, следующий за последним элементом последовательности. Никогда не считывайте и не записывайте значение *end
. Для пустой последовательности всегда выполняется условие begin==end
. Другими словами, для любого итератора p последовательность [p:p]
является пустой.
Для того чтобы считать последовательность, алгоритм обычно получает пару итераторов (
b, e
) и перемещается по элементам с помощью оператора ++
, пока не достигнет конца.
while (b!=e) { // используйте !=, а не <
// какие-то операции
++b; // переходим к последнему элементу
}
Алгоритмы, выполняющие поиск элемента в последовательности, в случае неудачи обычно возвращают итератор, установленный на конец последовательности. Рассмотрим пример.
p = find(v.begin(),v.end(),x); // ищем x в последовательности v
if (p!=v.end()) {
// x найден в ячейке p
}
else {
// x не найден в диапазоне [v.begin():v.end())
}
См. раздел 20.3.
Алгоритмы, записывающие элементы последовательности, часто получают только итератор, установленный на ее первый элемент. В данном случае программист должен сам предотвратить выход за пределы этой последовательности. Рассмотрим пример.
template void f(Iter p, int n)
{
while (n>0) *p++ = ––n;
vector v(10);
f(v.begin(),v.size()); // OK
f(v.begin(),1000); // большая проблема
Некоторые реализации стандартной библиотеки проверяют выход за пределы допустимого диапазона, т.е. генерируют исключение, при последнем вызове функции
f()
, но этот код нельзя считать переносимым; многие реализации эту проверку не проводят.
Перечислим операции над итераторами.
Обратите внимание на то, что не каждый вид итераторов (раздел Б.3.2) поддерживает все операции над итераторами.
В стандартной библиотеке предусмотрены пять видов итераторов.
С логической точки зрения итераторы образуют иерархию (см. раздел 20.10).
Поскольку категории итераторов не являются классами, эту иерархию нельзя считать иерархией классов, реализованной с помощью наследования. Если вам требуется выполнить над итераторами нетривиальное действие, поищите класс
iterator_traits
в профессиональном справочнике.
Каждый контейнер имеет собственные итераторы конкретной категории:
•
vector
— итераторы произвольного доступа;
•
list
— двунаправленные итераторы;
•
deque
— итераторы произвольного доступа;
•
bitset
— итераторов нет;
•
set
— двунаправленные итераторы;
•
multiset
— двунаправленные итераторы;
•
map
— двунаправленные итераторы;
•
multimap
— двунаправленные итераторы;
•
unordered_set
— однонаправленные итераторы;
•
unordered_multiset
— однонаправленные итераторы;
•
unordered_map
— однонаправленные итераторы;
•
unordered_multimap
— однонаправленные итераторы.
Контейнер содержит последовательность элементов. Элементы этой последовательности имеют тип
value_type
. Наиболее полезными контейнерами являются следующие.
Эти контейнеры определены в классах
,
и др. (см. раздел Б.1.1). Последовательные контейнеры занимают непрерывную область памяти или представляют собой связанные списки, содержащие элементы соответствующего типа value_type
(выше мы обозначали его буквой T
). Ассоциативные контейнеры представляют собой связанные структуры (деревья) с узлами соответствующего типа value_type (выше мы обозначали его как pair(K,V)
). Последовательность элементов в контейнерах set
, map
или multimap
упорядочена по ключу (K). Последовательность в контейнерах, название которых начинается со слова unordered
, не имеет гарантированного порядка. Контейнер multimap
отличается от контейнера map
тем, что в первом случае значение ключа может повторяться много раз. Адаптеры контейнеров — это контейнеры со специальными операциями, созданные из других контейнеров.
Если сомневаетесь, используйте класс
vector
. Если у вас нет весомой причины использовать другой контейнер, используйте класс vector
.
Для выделения и освобождения памяти (см. раздел 19.3.6) контейнеры используют распределители памяти. Мы не описываем их здесь; при необходимости читатели найдут информацию о них в профессиональных справочниках. По умолчанию распределитель памяти использует операторы
new
и delete
, для того чтобы занять или освободить память, необходимую для элементов контейнера.
Там, где это целесообразно, операция доступа реализована в двух вариантах: один — для константных объектов, другой — для неконстантных (см. раздел 18.4).
В этом разделе перечислены общие и “почти общие” члены стандартных контейнеров (более подробную информацию см. в главе 20). Члены, характерные для какого-то конкретного контейнера, такие как функция
splice()
из класса list
, не указаны; их описание можно найти в профессиональных справочниках.
Некоторые типы данных обеспечивают большинство операций, требующихся от стандартного контейнера, но все-таки не все. Иногда такие типы называют “почти контейнерами”. Перечислим наиболее интересные из них.
Операции, предусмотренные в стандартных контейнерах, можно проиллюстрировать следующим образом:
Контейнер определяет множество типов его членов.
Контейнеры имеют много разнообразных конструкторов и операторов присваивания. Перечислим конструкторы, деструкторы и операторы присваивания для контейнера C (например, типа
vector
или map
).
Для некоторых контейнеров и типов элементов конструктор или операция копирования может генерировать исключения.
Контейнер можно интерпретировать как последовательность, порядок следования элементов в которой определен либо итератором контейнера, либо является обратным к нему. Для ассоциативного контейнера порядок определяется критерием сравнения (по умолчанию оператором
<
).
К некоторым элементам можно обратиться непосредственно.
Некоторые реализации — особенно их тестовые версии — всегда выполняют проверку диапазонов, но рассчитывать на корректность или наличие такой проверки на разных компьютерах нельзя. Если этот вопрос важен, просмотрите документацию.
Стандартные контейнеры
vector
и deque
обеспечивают эффективные операции над концами (back
) последовательности элементов. Кроме того, контейнеры list
и deque
обеспечивают аналогичные операции над началом (front
) своей последовательности.
Обратите внимание на то, что функции
push_front()
и push_back()
копируют элемент в контейнер. Это значит, что размер контейнера увеличивается (на единицу). Если копирующий конструктор элемента может генерировать исключения, то вставка может завершиться отказом.
Отметим, что операции удаления элементов не возвращают значений. Если бы они это делали, то копирующие конструкторы, генерирующие исключения, могли бы серьезно усложнить реализацию. Для доступа к элементам стека и очереди рекомендуем использовать функции
front()
и back()
(см. раздел Б.4.5). Мы не ставили себе задачу перечислить все ограничения; попробуйте догадаться об остальных (как правило, компиляторы сообщают пользователям об их неверных догадках) или обратитесь к более подробной документации.
Ниже приведены операции над списком.
Результат
q
функции insert()
ссылается на последний вставленный элемент. Результат q
функции erase()
ссылается на элемент, следующий за последним удаленным элементом.
Размер — это количество элементов в контейнере; емкость — это количество элементов, которое контейнер может содержать до того, как потребуется дополнительно увеличить память
Изменяя размер или емкость, можно переместить элементы в новое место. Из этого следует, что итераторы (а также указатели и ссылки) на элементы могут стать некорректными (т.е. относиться к старым адресам).
Контейнеры можно копировать (см. раздел Б.4.3), сравнивать и обменивать.
Если сравнение контейнеров производится с помощью соответствующего оператора (например,
<
), то их элементы сравниваются с помощью эквивалентного оператора для сравнения элементов (например, <
).
Ассоциативные контейнеры обеспечивают поиск на основе ключей.
Упорядоченные ассоциативные контейнеры (
map
, set
и др.) имеют необязательный шаблонный аргумент, указывающий тип предиката сравнения, например, set
использует предикат C
для сравнения значений типа K
.
Первый итератор пары, возвращенной функцией
equal_range
, равен lower_bound
, а второй — upper_bound
. Вы можете вывести на печать значения всех элементов, имеющих ключ "Marian
" в контейнере multimap
, написав следующий код:
string k = "Marian";
typedef multimap::iterator MI;
pair pp = m.equal_range(k);
if (pp.first!=pp.second)
cout << "elements with value ' " << k << " ':\n";
else
cout << "no element with value ' " << k << " '\n";
for (MI p = pp.first; p!=pp.second; ++p) cout << p–>second << '\n';
В качестве альтернативы можно выполнить следующую эквивалентную инструкцию:
pair pp = make_pair(m.lower_bound(k),m.upper_bound(k));
Однако эта инструкция выполняется вдвое дольше. Алгоритмы
equal_range
, lower_bound
и upper_bound
можно выполнять также для упорядоченных последовательностей (раздел Б.5.4). Определение класса pair
приведено в разделе Б.6.3.
В заголовке
определено около 60 алгоритмов. Все они относятся к последовательностям, определенным парами итераторов (для ввода) или одним итератором (для вывода).
При копировании, сравнении и выполнении других операций над двумя последовательностями первая из них задается парой итераторов
[b:e]
, а вторая — только одним итератором b2
, который считается началом последовательности, содержащей элементы, количество которых достаточно для выполнения алгоритма, например, столько же, сколько элементов в первой последовательности: [b2:b2+(e–b)]
.
Некоторые алгоритмы, такие как
sort
, используют итераторы произвольного доступа, а многие другие, такие как find
, только считывают элементы с помощью однонаправленного итератора.
Многие алгоритмы придерживаются обычного соглашения и возвращают конец последовательности в качестве признака события “не найден”. Мы больше не будем упоминать об этом каждый раз, описывая очередной алгоритм.
Немодифицирующий алгоритм просто считывает элементы последовательности; он не изменяет порядок следования элементов последовательности и не изменяет их значения.
Предотвратить модификацию элементов операцией, передаваемой алгоритму
for_each
, невозможно; это считается приемлемым. Передача операции, изменяющей проверяемые ею элементы, другим алгоритмам (например, count или ==
) недопустима.
Рассмотрим пример правильного использования алгоритма.
bool odd(int x) { return x&1; }
int n_even(const vector& v) // подсчитывает количество четных
// чисел в v
{
return v.size()–count_if(v.begin(),v.end(),odd);
}
Модифицирующие алгоритмы могут изменять элементы последовательностей, являющихся их аргументами.
Алгоритм
shuffle
перетасовывает последовательность точно так же, как перетасовывается колода карт; иначе говоря, после перетасовки элементы следуют в случайном порядке, причем смысл слова “случайно” определяется распределением, порожденным датчиком случайных чисел.
Следует подчеркнуть, что эти алгоритмы не знают, являются ли их аргументы контейнерами, поэтому не могут добавлять или удалять элементы. Таким образом, такой алгоритм, как
remove
, не может уменьшить длину входной последовательности, удалив (стерев) ее элементы; вместо этого он передвигает эти элементы к началу последовательности.
typedef vector::iterator VII;
void print_digits(const string& s, VII b, VII e)
{
cout << s;
while (b!=e) { cout << *b; ++b; }
cout << '\n';
}
void ff()
{
int a[] = { 1,1,1,2,2,3,4,4,4,3,3,3,5,5,5,5,1,1,1 };
vector v(a,a+sizeof(a)/sizeof(int));
print_digits("all: ",v.begin(), v.end());
vector::iterator pp = unique(v.begin(),v.end());
print_digits("head: ",v.begin(),pp);
print_digits("tail: ",pp,v.end());
pp=remove(v.begin(),pp,4);
print_digits("head: ",v.begin(),pp);
print_digits("tail: ",pp,v.end());
}
Результат приведен ниже.
all: 1112234443335555111
head: 1234351
tail: 443335555111
head: 123351
tail: 1443335555111
С формальной точки зрения вспомогательные алгоритмы также могут модифицировать последовательности, но мы считаем, что лучше их перечислить отдельно, чтобы они не затерялись в длинном списке.
Обратите внимание на то, что неинициализированные последовательности должны использоваться только на самых нижних уровнях программирования, как правило, в реализации контейнеров. Элементы, представляющие собой цели алгоритмов
uninitialized_fill
и uninitialized_copy
, должны иметь встроенный тип или быть неинициализированными.
Сортировка и поиск относятся к категории фундаментальных алгоритмов. В то же время потребности программистов довольно разнообразны. Сравнение по умолчанию выполняется с помощью оператора
<
, а эквивалентность пар значений a
и b
определяется условием !(a
, а не оператором ==
.
Рассмотрим следующий пример:
vector v;
list lst;
v.push_back(3); v.push_back(1);
v.push_back(4); v.push_back(2);
lst.push_back(0.5); lst.push_back(1.5);
lst.push_back(2); lst.push_back(2.5); // список lst упорядочен
sort(v.begin(),v.end()); // сортировка вектора v
vector v2;
merge(v.begin(),v.end(),lst.begin(),lst.end(),back_inserter(v2));
for (int i = 0; i
Алгоритмы вставки описаны в разделе Б.6.1. В итоге получается следующий результат:
0.5, 1, 1.5, 2, 2, 2.5, 3, 4,
Алгоритмы
equal_range
, lower_bound
и upper_bound
используются точно так же, как и их эквиваленты для ассоциативных контейнеров (раздел Б.4.10).
Эти алгоритмы интерпретируют последовательность как множество элементов и выполняют основные операции над множествами. Входные и выходные последовательности предполагаются упорядоченными.
Куча — это структура данных, в вершине которой находится элемент с наибольшим значением. Алгоритмы над кучами позволяют программистам работать с последовательностями произвольного доступа.
Куча позволяет быстро добавлять элементы и обеспечивает быстрый доступ к элементу с наибольшим значением. В основном кучи используются при реализации очередей с приоритетами.
Перестановки используются для генерирования комбинаций элементов последовательности. Например, перестановками последовательности
abc
являются последовательности abc
, acb
, bac
, bca
, cab
и cba
.
Если последовательность
[b:e]
уже содержит последнюю перестановку (в данном примере это перестановка cba
), то алгоритм next_permutation
возвращает значение x
, равное false
; в таком случае алгоритм создает первую перестановку (в данном примере это перестановка abc
). Если последовательность [b:e]
уже содержит первую перестановку (в данном примере это перестановка abc
), то алгоритм prev_permutation
возвращает значение x
, равное false
; в таком случае алгоритм создает последнюю перестановку (в данном примере это перестановка cba
).
Сравнение значений полезно во многих случаях.
В стандартной библиотеке есть несколько инструментов для облегчения использования стандартных библиотечных алгоритмов.
Запись результатов в контейнер с помощью итератора подразумевает, что элементы, на которые указывает итератор, можно перезаписать. Это открывает возможность для переполнения и последующего повреждения памяти. Рассмотрим следующий пример:
void f(vector& vi)
{
fill_n(vi.begin(),200,7); // присваиваем 7 элементам
// vi[0]..[199]
}
Если вектор
vi
содержит меньше 200 элементов, то возникает опасность. В заголовке
стандартная библиотека предусматривает три итератора, позволяющих решить эту проблему с помощью добавления (вставки) элементов в контейнер, а не перезаписи его старых элементов. Для генерирования этих трех итераторов вставки используются три функции.
Для правильной работы алгоритма
inserter(c,p)
необходимо, чтобы итератор p был корректным итератором для контейнера c
. Естественно, каждый раз при записи очередного элемента с помощью итератора вставки контейнер увеличивается на один элемент. При записи алгоритм вставки добавляет новый элемент в последовательность с помощью функции push_back(x)
, c.push_front()
или insert()
, а не перезаписывает существующий элемент. Рассмотрим следующий пример:
void g(vector& vi)
{
fill_n(back_inserter(vi),200,7); // добавляет 200 семерок
// в конец vi
}
Многие стандартные алгоритмы принимают в качестве аргументов объекты-функции (или функции), чтобы уточнить способ решения задачи. Обычно эти функции используются в качестве критериев сравнения, предикатов (функций, возвращающих значения типа
bool
) и арифметических операций. Несколько самых общих объектов-функций описано в заголовке
стандартной библиотеки.
Рассмотрим следующий пример:
vector v;
// ...
sort(v.begin(),v.end(),greater()); // сортировка v в убывающем
// порядке
Обратите внимание на то, что предикаты
logical_and
и logical_or
всегда вычисляют оба свои аргумента (в то время как операторы &&
и ||
— нет).
В заголовке
стандартная библиотека содержит несколько вспомогательных компонентов, включая класс pair
.
template
struct pair {
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair(); // конструктор по умолчанию
pair(const T1& x,const T2& y);
// копирующие операции:
template pair(const pair& p
);
};
template
pair make_pair(T1 x, T2 y) { return pair(x,y); }
Функция
make_pair()
упрощает использование пар. Например, рассмотрим схему функции, возвращающей значение и индикатор ошибки.
pair my_fct(double d)
{
errno = 0; // очищаем индикатор ошибок в стиле языка C
// выполняем много вычислений, связанных с переменной d,
// и вычисляем x
error_indicator ee = errno;
errno = 0; // очищаем индикатор ошибок в стиле языка C
return make_pair(x,ee);
}
Этот пример является полезной идиомой. Его можно использовать следующим образом:
pair res = my_fct(123.456);
if (res.second==0) {
// используем res.first
}
else {
// Ой: ошибка
}
Библиотека потоков ввода-вывода содержит средства форматированного и неформатированного буферизованного ввода-вывода текста и числовых значений.
Определения потоков ввода-вывода находятся в заголовках
,
и т.п. (см. раздел Б.1.1).
Объект класса
ostream
преобразовывает объекты, имеющие тип, в поток символов (байтов).
Объект класса
istream
преобразовывает поток символов (байтов) в объекты, имеющие тип.
Объект класса
iostream
— это поток, который может действовать и как объект класса istream
, и как объект класса ostream
. Буфера, изображенные на диаграмме, являются потоковыми буферами (streambuf
). Если читателям потребуется перейти от потоков класса iostream
к новым видам устройств, файлов или памяти, они смогут найти их описание в профессиональных учебниках.
Существуют три стандартных потока.
Поток
istream
можно связать с устройством ввода (например, клавиатурой), файлом или объектом класса string
. Аналогично поток ostream
можно связать с устройством вывода (например, текстовым окном), файлом или объектом класса string
. Потоки ввода-вывода образуют иерархию классов.
Поток можно открыть либо с помощью конструктора, либо вызова функции
open()
.
Для файловых потоков имя файлов представляет собой строку в стиле языка С.
Открыть файл можно в одном из режимов, приведенных ниже.
В каждом из этих режимов открытие файла может зависеть от операционной системы и ее возможностей учесть требование программиста открыть файл именно так, а не иначе. В результате поток может не оказаться в состоянии
good()
. Рассмотрим пример.
void my_code(ostream& os); // функция my_code может использовать
// любой поток вывода
ostringstream os; // буква "o" означает "для вывода"
ofstream of("my_file");
if (!of) error("невозможно открыть 'my_file' для записи");
my_code(os); // используется объект класса string
my_code(of); // используется файл
См. раздел 11.3.
Поток
iostream
может пребывать в одном из четырех состояний.
Используя функцию
s.exceptions()
, программист может потребовать, чтобы поток iostream
сгенерировал исключение, если из состояния good()
он перешел в другое состояние (см. раздел 10.6).
Любая операция, в результате которой поток не находится в состоянии
good()
, не имеет никакого эффекта; такая ситуация называется “no op”.
Объект класса
iostream
можно использовать как условие. В данном случае условие является истинным (успех), если поток iostream
находится в состоянии good()
. Это обстоятельство стало основой для распространенной идиомы, предназначенной для считывания потока значений.
X x; // "буфер ввода" для хранения одного значения типа X
while (cin>>x) {
// какие-то действия с объектом x
}
// мы окажемся в этой точке, если оператор >> не сможет прочитать
// очередной объект класса X из потока cin
Почти все операции ввода описаны в заголовке
, за исключением операций ввода в объект класса string
; эти операции описаны в заголовке
:
Если не указано иное, операция ввода возвращает ссылку на объект класса
istream
, поэтому можно создавать цепочки таких операций, например cin>>x>>y
;.
Функции
get()
и getline()
помещают после символов, записанных в ячейки p[0]
и т.д., число 0
(если символы были введены); функция getline()
удаляет признак конца ввода (t
из потока ввода, если он обнаружен, а функция get()
этого не делает. Функция read(p,n)
не записывает число 0
в массив после считанных символов. Очевидно, что операторы форматированного ввода проще в использовании и менее уязвимы для ошибок, чем операции неформатированного ввода.
Почти все операции вывода описаны в заголовке
, за исключением операции записи в объекты класса string
; такие операции описаны в заголовке
.
Если не указано иное, операции вставки в поток
ostream
возвращают ссылку на его объекты, поэтому можно создавать цепочки операций вывода, например cout << x<
;.
Формат потока ввода-вывода управляется комбинацией типа объекта, состояния потока, информацией о локализации (см. раздел
) и явными операциями. Большая часть информации об этом изложена в главах 10-11. Здесь мы просто перечислим стандартные манипуляторы (операции, модифицирующие поток), поскольку они обеспечивают наиболее простой способ изменения формата.
Вопросы локализации выходят за рамки рассмотрения настоящей книги.
В стандартной библиотеке предусмотрены манипуляторы, соответствующие разнообразным изменениям формата. Стандартные манипуляторы определены в заголовках
,
,
,
и
(для манипуляторов, получающих аргументы).
Каждая из этих операций возвращает ссылку на свой первый операнд потока
s
.
Рассмотрим пример.
cout << 1234 << ',' << hex << 1234 << ',' << oct << 1234 << endl;
Этот код выводит на экран следующую строку:
1234,4d2,2322
В свою очередь, код
cout << '(' << setw(4) << setfill('#') << 12 << ") (" << 12 << ")\n";
выводит на экран такую строку:
(##12) (12)
Для того чтобы явно установить общий формат вывода чисел с плавающей точкой, используйте следующую инструкцию:
b.setf(ios_base::fmtflags(0),ios_base::floatfield)
См. главу 11.
В стандартной библиотеке предусмотрены операции классификации символов в заголовке
, строки с соответствующими операциями в заголовке
, регулярные выражения в заголовке
(C++0x) и поддержка С-строк в заголовке
.
Символы из основного набора могут быть классифицированы так, как показано ниже.
Кроме того, в стандартной библиотеке описаны две полезные функции для изменения регистра символа.
Расширенные наборы символов, такие как Unicode, также поддерживаются стандартной библиотекой, но эта тема выходит за рамки рассмотрения настоящей книги.
Класс
string
из стандартной библиотеки представляет собой специализацию общего шаблонного класса basic_string
для символьного типа char
; иначе говоря, объект string
— это последовательность переменных типа char
.
Библиотека регулярных выражений еще не является частью стандартной библиотеки, но вскоре станет ею и будет широко доступной, поэтому мы решили привести ее в этом разделе. Более подробные объяснения изложены в главе 23. Ниже перечислены основные функции из заголовка
.
• Поиск (searching) строки, соответствующей регулярному выражению в (произвольно длинном) потоке данных, — обеспечивается функцией
regex_search()
.
• Сопоставление (matching) регулярного выражения со строкой (известного размера) — обеспечивается функцией
regex_match()
.
• Замена соответствий (replacement of matches) — обеспечивается функцией
regex_replace()
; в данной книге не описывается; см. профессиональные учебники или справочники.
Результатом работы функций
regex_search()
и regex_match()
является коллекция соответствий, как правило, представленных в виде объекта класса smatch
.
regex row("^[\\w ]+(\\d+)(\\d+)(\\d+)$"); // строка данных
while (getline(in,line)) { // проверка строки данных
smatch matches;
if (!regex_match(line, matches, row))
error("bad line", lineno);
// проверка строки:
int field1 = from_string(matches[1]);
int field2 = from_string(matches[2]);
int field3 = from_string(matches[3]);
// ...
}
Синтаксис регулярных выражений основан на символах, имеющих особый смысл (см. главу 23).
Некоторые классы символов поддерживаются аббревиатурами.
В стандартной библиотеке языка C++ содержатся основные строительные конструкции для математических (научных, инженерных и т.д.) вычислений.
Каждая реализация языка C++ определяет свойства встроенных типов, чтобы программисты могли использовать эти средства для проверки предельных значений, установки предохранителей и т.д.
В заголовке
определен класс numeric_limits
для каждого встроенного или библиотечного типа T
. Кроме того, программист может определить класс numeric_limits
для пользовательского числового типа X
. Рассмотрим пример.
class numeric_limits {
public:
static const bool is_specialized = true;
static const int radix = 2; // основание системы счисления
// (в данном случае двоичная)
static const int digits = 24; // количество цифр в мантиссе
// в текущей системе счисления
static const int digits10 = 6; // количество десятичных цифр
// в мантиссе
static const bool is_signed = true;
static const bool is_integer = false;
static const bool is_exact = false;
static float min() { return 1.17549435E–38F; } // пример
static float max() { return 3.40282347E+38F; } // пример
static float epsilon() { return 1.19209290E–07F; } // пример
static float round_error() { return 0.5F; } // пример
static float infinity() { return /* какое-то значение */; }
static float quiet_NaN() { return /* какое-то значение */; }
static float signaling_NaN() { return /* какое-то значение */; }
static float denorm_min() { return min(); }
static const int min_exponent = –125; // пример
static const int min_exponent10 = –37; // пример
static const int max_exponent = +128; // пример
static const int max_exponent10 = +38; // пример
static const bool has_infinity = true;
static const bool has_quiet_NaN = true;
static const bool has_signaling_NaN = true;
static const float_denorm_style has_denorm = denorm_absent;
static const bool has_denorm_loss = false;
static const bool is_iec559 = true; // соответствует системе
IEC-559
static const bool is_bounded = true;
static const bool is_modulo = false;
static const bool traps = true;
static const bool tinyness_before = true;
static const float_round_style round_style =
round_to_nearest;
};
В заголовках
и
определены макросы, определяющие основные свойства целых чисел и чисел с плавающей точкой.
В стандартной библиотеке определены основные математические функции (в заголовках
и
).
Существуют версии этих функций, принимающие аргументы типа
float
, double
, long double
и complex
. У каждой из этих функций тип возвращаемого значения совпадает с типом аргумента.
Если стандартная математическая функция не может выдать корректный с математической точки зрения результат, она устанавливает переменную
errno
.
В стандартной библиотеке определены типы для комплексных чисел
complex
, complex
и complex
. Класс complex
, где Scalar
— некий другой тип, поддерживающий обычные арифметические операции, как правило, работоспособен, но не гарантирует переносимости программ.
template class complex {
// комплексное число — это пара скалярных значений,
// по существу — пара координат
Scalar re, im;
public:
complex(const Scalar & r, const Scalar & i):re(r), im(i) { }
complex(const Scalar & r):re(r),im(Scalar ()) { }
complex():re(Scalar ()), im(Scalar ()) { }
Scalar real() { return re; } // действительная часть
Scalar imag() { return im; } // мнимая часть
// операторы : = += –= *= /=
};
Кроме этих членов, в классе
предусмотрено много полезных операций.
Кроме того, к комплексным числам можно применять стандартные математические функции (см. раздел Б.9.2). Примечание: в классе
complex
нет операций <
или %
(см. также раздел 24.9).
Объект стандартного класса
valarray
— это одномерный массив чисел; иначе говоря, он предусматривает арифметические операции для массивов (аналогично классу Matrix
из главы 24), а также срезы (slices) и шаги по индексу (strides).
Эти алгоритмы из раздела
обеспечивают общие варианты типичных операций над последовательностями числовых значений.
Стандартная библиотека языка С включена в стандартную библиотеку языка С++ с минимальными изменениями. В ней предусмотрено относительно небольшое количество функций, полезность которых подтверждена многолетним опытом использования в разнообразных предметных областях, особенно в низкоуровневом программировании. Библиотека языка С разделена на несколько категорий.
• Ввод-вывод в стиле языка C.
• Строки в стиле языка C.
• Управление памятью.
• Дата и время.
• Остальное.
Библиотека языка С содержит намного больше функций, чем описано в этой книге; рекомендуем читателям обратиться к хорошим учебникам по языку С, например, к книге Kernighan, Ritchie, The C Programming Language (K&R).
Система ввода-вывода, описанная в заголовке
, основана на файлах. Указатель на файл (FILE*
) может относиться как к файлу, так и к стандартным потокам ввода и вывода, stdin
, stdout
и stderr
. Стандартные потоки доступны по умолчанию; остальные файлы должны быть открыты явным образом.
Режим — это строка, содержащая одну или несколько директив, определяющих, как именно должен быть открыт файл.
В конкретной операционной системе может быть (и, как правило, так и есть) больше возможностей. Некоторые режимы могут комбинироваться, например, инструкция
fopen("foo","rb")
пытается открыть файл foo
для чтения в бинарном режиме. Режимы ввода-вывода для потоков из библиотек stdio
и iostream
должны быть одинаковыми (см. раздел Б.7.1)
Наиболее популярными функциями в стандартной библиотеке языка С являются функции ввода-вывода. Тем не менее рекомендуем использовать библиотеку
iostream
, потому что она безопасна с точки зрения типов и допускает расширение. Функция форматированного вывода printf()
используется очень широко (в том числе и в программах на языке C++) и часто имитируется в других языках программирования.
В каждой версии число
n
— это количество записанных символов, а в случае неудачи — отрицательное число. На самом деле значение, возвращаемое функцией printf()
, практически всегда игнорируется.
Объявление функции
printf()
имеет следующий вид:
int printf(const char* format ...);
Иначе говоря, эта функция получает строку в стиле языка С (как правило, строковый литерал), за которой следует список, состоящий из произвольного количества аргументов произвольного типа. Смысл этих дополнительных аргументов задается спецификаторами преобразования в форматной строке, например
%c
(вывести символ) и %d
(вывести целое число). Рассмотрим пример.
int x = 5;
const char* p = "asdf";
printf("Значение x равно '%d', а значение p равно '%s'\n",x,s);
Символ, следующий за знаком
%
управляет обработкой аргументов. Первый знак %
применяется к первому дополнительному аргументу (в данном примере спецификатор %d
применяется к переменной x
), второй знак %
относится ко второму дополнительному аргументу (в данном примере спецификатор %s
применяется к переменной p
) и т.д. В частности, рассмотренный выше вызов функции printf()
приводит к следующему результату:
Значение x равно '5', а значение p равно 'asdf'
Затем происходит переход на новую строку.
В принципе соответствие между директивой преобразования
%
и типом, к которому она применяется, проверить невозможно. Рассмотрим пример.
printf("Значение x равно '%s', а значение p равно '%d'\n",x,p); // ой!
Набор спецификаторов преобразования довольно велик и обеспечивает большую гибкость (а также много возможностей сделать ошибку). За символом
%
могут следовать спецификаторы, описанные ниже.
Нулевая или слишком маленькая ширина поля никогда не приводит к усечению вывода; дополнение вывода нулями или пробелами производится только тогда, когда заданная ширина поля превышает реальную.
Поскольку в языке C нет пользовательских типов в смысле языка C++, в нем нет возможностей для определения форматов вывода для таких классов, как
complex
, vector
или string
.
Стандартный поток вывода
stdout
в языке C соответствует потоку cout
. Стандартный поток ввода stdin
в языке С соответствует потоку cin
. Стандартный поток сообщений об ошибках stderr
в языке С соответствует потоку cerr
. Эти соответствия между стандартными потоками ввода-вывода в языке C и C++ настолько близки, что потоки ввода-вывода как в стиле языка С, так и стиле языка С++ могут использовать один и тот ж буфер. Например, для создания одного и того же потока вывода можно использовать комбинацию операций над объектами cout
и stdout
(такая ситуация часто встречается в смешанном коде, написанном на языка С и С++). Эта гибкость требует затрат. Для того чтобы получить более высокую производительность, не смешивайте операции с потоками из библиотек stdio
и iostream
при работе с одним и тем же потоком, вместо этого вызывайте функцию ios_base::sync_with_stdio(false)
перед выполнением первой операции ввода-вывода. В библиотеке stdio
определена функция scanf()
, т.е. операция ввода, похожая на функцию printf()
. Рассмотрим пример.
int x;
char s[buf_size];
int i = scanf("Значение x равно '%d', а значение s равно '%s'\n",&x,s);
Здесь функция
scanf()
пытается считать целое число в переменную x
и последовательность символов, не являющихся разделителями, в массив s
. Неформатные символы указывают, что они должны содержаться в строке ввода. Рассмотрим пример.
"Значение x равно '123', а значение s равно 'string '\n"
Программа введет число
123
в переменную x
и строку "string
", за которой следует 0
, в массив s
. Если вызов функции scanf()
завершает работу успешно, результирующее значение (i
в предыдущем вызове) будет равно количеству присвоенных аргументов-указателей (в данном примере это число равно 2
); в противном случае оно равно EOF
. Этот способ индикации ввода уязвим для ошибок (например, что произойдет, если вы забудете вставить пробел после строки "string
" в строке ввода?). Все аргументы функции scanf()
должны быть указателями. Мы настоятельно рекомендуем не использовать эту функцию.
Как же ввести данные, если мы вынуждены использовать библиотеку
stdio
? Один и из распространенных ответов гласит: “Используйте стандартную библиотечную функцию gets()
”.
// очень опасный код:
char s[buf_size];
char* p = gets(s); // считывает строку в массив s
Вызов
p=gets(s)
будет вводить символы в массив s
, пока не обнаружится символ перехода на новую строку или не будет достигнут конец файла. В этом случае в конец строки s
после последнего символа будет вставлен 0
. Если обнаружен конец файла или возникла ошибка, то указатель p устанавливается равным NULL
(т.е. 0
); в противном случае он устанавливается равным s
. Никогда не используйте функцию gets(s)
или ее эквивалент scanf("%s",s))!
За прошедшие годы создатели вирусов облюбовали их слабые места: генерируя вводную строку, переполняющую буфер ввода (в данном примере строку s
), они научились взламывать программы и атаковать компьютеры. Функция sprintf()
страдает от таких же проблем, связанных с переполнением буфера.
Библиотека
stdio
содержит также простые и полезные функции чтения и записи символов.
Обратите внимание на то, что результатом этих функций является число типа
int
(а не переменная типа char
или макрос EOF
). Рассмотрим типичный цикл ввода в программе на языке С.
int ch; /* но не char ch; */
while ((ch=getchar())!=EOF) { /* какие-то действия */ }
Не применяйте к потоку два последовательных вызова
ungetc()
. Результат такого действия может оказаться непредсказуемым, а значит, программа не будет переносимой.
Мы описали не все функции из библиотеки
stdio
, более полную информацию можно найти в хороших учебниках по языку С, например в книге K&R.
Строки в стиле языка C представляют собой массивы элементов типа
char
, завершающиеся нулем. Эти строки обрабатываются функциями, описанными в заголовках
(или
; примечание: но не
) и
.
Эти функции оперируют строками в стиле языка С с помощью указателей
char*
(указатели const char*
ссылаются на ячейки памяти, предназначенные исключительно для чтения).
Обратите внимание на то, что в языке C++ функции
strchr()
и strstr()
дублируются, чтобы обеспечить безопасность типов (они не могут преобразовать тип const char*
в тип char*
, как их аналоги в языке C); см. также раздел 27.5.
Функции извлечения символов просматривают строку в стиле языка С в поисках соответственно форматированного представления числа, например "
124
" и "1.4
". Если такое представление не найдено, функция извлечения возвращает 0
. Рассмотрим пример.
int x = atoi("fortytwo"); /* x становится равным 0 */
Функции управления памятью действуют в “голой памяти” (без известного типа) с помощью указателей типа
void*
(указатели const void*
ссылаются на ячейки памяти, предназначенные только для чтения).
Функции
malloc()
и ей подобные не вызывают конструкторы, а функция free()
не вызывает деструкторы. Не применяйте эти функции к типам, имеющим конструкторы или деструкторы. Кроме того, функция memset()
также никогда не должна применяться к типам, имеющим конструктор.
Функции, начинающиеся с приставки mem, описаны в заголовке
, а функции выделения памяти — в заголовке
.
См. также раздел 27.5.2.
В заголовке
можно найти несколько типов и функций, связанных с датами и временем.
Структура
tm
определяется примерно так:
struct tm {
int tm_sec; // секунда минуты [0:61]; 60 и 61
//"високосные" секунды
int tm_min; // минута часа [0,59]
int tm_hour; // час дня [0,23]
int tm_mday; // день месяца [1,31]
int tm_mon; // месяц года [0,11]; 0 — январь (примечание: не [1:12])
int tm_year; // год с 1900- го года ; 0 — 1900-й год,
// 102 — 2002-й год
int tm_wday; // дни, начиная с воскресенья [0,6]; 0 — воскресенье
int tm_yday; // дней после 1 января [0,365]; 0 — 1 января
int tm_isdst; // часы летнего времени
};
Функции для работы с датами и временем
clock_t clock(); // количество тактов таймера после старта программы
time_t time(time_t* pt); // текущее календарное
// время
double difftime(time_t t2, time_t t1); // t2–t1 в секундах
tm* localtime(const time_t* pt); // локальное время для *pt
tm* gmtime(const time_t* pt); // время по Гринвичу (GMT) tm для
// *pt или 0
time_t mktime(tm* ptm); // time_t для *ptm или time_t(–1)
char* asctime(const tm* ptm); // представление *ptm в виде
// C-строки
char* ctime(const time_t* t) { return asctime(localtime(t)); }
Пример результата вызова функции
asctime()
: "Sun Sep 16 01:03:52 1973\n"
.
Рассмотрим пример использования функции
clock
для измерения времени работы функции (do_something()
).
int main(int argc, char* argv[])
{
int n = atoi(argv[1]);
clock_t t1 = clock(); // начало отсчета
if (t1 == clock_t(–1)) { // clock_t(–1) означает "clock()
// не работает "
cerr << "Извините, таймер не работает \n";
exit(1);
}
for (int i = 0; i
clock_t t2 = clock(); // конец отсчета
if (t2 == clock_t(–1)) {
cerr << "Извините, таймер переполнен \n";
exit(2);
}
cout << "do_something() " << n << " работала "
<< double(t2–t1)/CLOCKS_PER_SEC << " секунд "
<< " (точность измерения: " << CLOCKS_PER_SEC
<< " секунд )\n";
}
Явное преобразование
double(t2–t1)
перед делением является необходимым, потому что число clock_t
может быть целым. Для значений t1
и t2
, возвращаемых функцией clock()
, величина double(t2–t1)/CLOCKS_PER_SEC
является наилучшим системным приближением времени в секундах, прошедшего между двумя вызовами.
Если функция
clock()
не поддерживается процессором или временной интервал слишком длинный, то функция clock()
возвращает значение clock_t(–1)
.
В заголовке
определены следующие функции.
Функция для сравнения (
cmp
), используемая функциями qsort()
и bsearch()
, должна иметь следующий тип:
int (*cmp)(const void* p,const void* q);
Иначе говоря, функции сортировки не известен тип упорядочиваемых элементов: она просто интерпретирует массив как последовательность байтов и возвращает целое число, удовлетворяющее следующим условиям:
• оно является отрицательным, если
*p
меньше, чем *q
;
• оно равно нулю, если
*p
равно *q
;
• оно больше нуля, если
*p
больше, чем *q
.
Подчеркнем, что функции
exit()
и abort()
не вызывают деструкторы. Если хотите вызывать деструкторы для статических объектов и объектов, созданных автоматически (см. раздел A.4.2), генерируйте исключение.
Более полную информацию о функциях из стандартной библиотеки можно найти в книге K&R или другом авторитетном справочнике по языку С++.
Исследуя возможности стандартной библиотеки, вы, конечно, не найдете чего-то, что могло бы быть полезным для вас. По сравнению с задачами, стоящими перед программистами, и огромным количеством доступных библиотек, стандартная библиотека языка C++ является довольно скромной. Существует много библиотек, предназначенных для решения следующих задач.
• Графические пользовательские интерфейсы.
• Сложные математические вычисления.
• Доступ к базам данных.
• Работа в сети.
• XML.
• Дата и время.
• Система манипуляции файлами.
• Трехмерная графика.
• Анимация.
• Прочее
Тем не менее эти библиотеки не являются частью стандарта. Вы можете найти их в Интернете или спросить у своих друзей и коллег. Не следует думать, что полезными являются только библиотеки, представляющие собой часть стандартной библиотеки.
“Вселенная не только страннее,
чем мы себе представляем,
но и страннее, чем мы можем представить”.
Дж. Б.С. Холдейн (J.B.S. Haldane)
В этом приложении описаны шаги, которые необходимо сделать до того, как вы войдете в программу, скомпилируете ее и запустите на выполнение с помощью среды разработки Microsoft Visual Studio.
Для того чтобы запустить программу, вам необходимо как-то собрать файлы вместе (чтобы, если ссылаются друг на друга — например, исходный файл на заголовочный — они могли найти друг друга). Затем необходимо вызвать компилятор и редактор связей (если не потребуется сделать что-то еще, он позволит по крайней мере связать программу со стандартной библиотекой языка С++) и запустить (выполнить) программу. Существует несколько способов решения это задачи, причем в разных операционных системах (например, Windows и Linux) приняты разные соглашения и предусмотрены разные наборы инструментов. Тем не менее, все примеры, приведенные в книге, можно выполнить во всех основных системах, используя один из распространенных наборов инструментов. В этом приложении показано, как это сделать в одной из наиболее широко распространенных систем — Microsoft Visual Studio.
Лично мы, реализуя некоторые примеры, испытали то же чувство разочарования, которое испытывает любой программист, приступая к работе с новой и странной системой. В этом случае стоит обратиться за помощью. Однако, обращаясь за помощью, постарайтесь, чтобы ваш советчик научил вас тому, как решить задачу, а не решил ее за вас.
Visual Studio — это интерактивная среда разработки программ (IDE — interactive development environment) для операционной системы Windows. Если она не установлена на вашем компьютере, можете купить ее и следовать приложенным инструкциям или загрузить и инсталлировать свободно распространяемую версию Visual C++ Express с веб-страницы www.microsoft.com/express/download. Описание, приведенное здесь, следует версии Visual Studio 2005. Остальные версии могут немного отличаться от нее.
Создание и запуск программы состоит из следующих шагов.
1. Создание нового проекта.
2. Добавление в проект исходного файла на языке С++.
3. Ввод исходного кода.
4. Создание исполняемого файла.
5. Выполнение программы.
6. Сохранение программы.
В среде Visual Studio “проектом” считается совокупность файлов, участвующих в создании и выполнении программы (называемой также приложением) в операционной системе Windows.
1. Откройте среду Visual C++, щелкнув на пиктограмме Microsoft Visual Studio 2005 или выполнив команду Start⇒Programs⇒Microsoft Visual Studio 2005⇒Microsoft Visual Studio 2005.
2. Откройте меню File, выберите команду New и щелкните на опции Visual C++.
3. На вкладке Project Types включите переключатель Visual C++.
4. В разделе Templates включите переключатель Win32 Console Application.
5. В окне редактирования Name наберите имя вашего проекта, например Hello, World!
6. Выберите каталог для вашего проекта. По умолчанию предлагается путь C:\Documents and Settings\Your name\My Documents\Visual Studio 2005\Projects.
7. Щелкните на кнопке OK.
8. После этого должно открыться окно мастера приложения Win32 Application Wizard.
9. В левой части диалогового окна выберите пункт Application Settings.
10. Находясь в разделе Additional Options включите переключатель Empty Project.
11. Щелкните на кнопке Finish. Теперь для вашего консольного проекта будут инициализированы все установки компилятора.
Для вашей первой программы мы настоятельно рекомендуем использовать заголовочный файл
std_lib_facilities.h
, который можно загрузить с веб-страницы www.stroustrup.com/Programming/std_lib_facilities.h. Скопируйте его в каталог, выбранный в разделе С.3.1 на шаге 6. (Примечание. Сохраните этот файл как текстовый, а не как HTML-файл.) Для того чтобы использовать этот файл, вставьте в вашу программу строку
#include "../../std_lib_facilities.h"
Символы “
../../
” сообщат компилятору, что вы разместили это файл в каталоге C:\Documents and Settings\Your name\My Documents\Visual Studio 2005\Projects, где его смогут найти другие проекты, а не просто рядом с вашим исходным файлом, так как в этом случае вам придется повторно копировать его в каталог каждого нового проекта.
Ваша программа должна состоять как минимум из одного исходного файла (хотя часто программы состоят из нескольких файлов).
1. Щелкните на пиктограмме Add New Item в строке меню (обычно вторая слева). В результате откроется диалоговое окно Add New Item. Выберите в категории Visual C++ пункт Code.
2. Выберите в окне шаблонов пункт С++ File (.cpp). Наберите в окне редактирования имя вашей программы (Hello, World!) и щелкните на кнопке Add.
Итак, вы создали пустой исходный файл. Теперь вы готовы набирать текст вашей программы.
В этом пункте вы можете либо ввести исходный код, набрав его непосредственно в среде разработки, либо скопировать и вставить его из другого источника.
Если вы уверены, что правильно набрали текст исходного кода вашей программы, зайдите в меню Build и выберите команду Build Selection или щелкните на треугольной пиктограмме, щелкнув справа на списке пиктограмм верхней части окна среды разработки. Среда разработки попытается скомпилировать код и отредактировать связи вашей программы. Если этот процесс завершится успешно, вы получите в окне Output сообщение
Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped
В противном случае в окне Output появится много сообщений об ошибках. Отладьте программу, чтобы исправить ошибки, и выполните команду Build Solution.
Если вы использовали треугольную пиктограмму, то программа автоматически начнет выполняться, если в ней нет ошибок. Если вы использовали пункт меню Build Solution, вы можете явно запустить программу, как описано в разделе С.3.6.
После устранения всех ошибок, выполните программу, выбрав в меню Debug и выбрав пункт Start Without Debugging.
Находясь в меню File, щелкните на пункте Save All. Если вы забудете об этом и попытаетесь закрыть среду разработки, то она напомнит вам об этом.
Среда разработки имеет бесконечное множество свойств и возможностей. Не беспокойтесь об этом, иначе вы полностью запутаетесь. Если ваш проект станет вести себя странно, попросите опытного друга помочь вам или создайте новый проект с нуля. Со временем потихоньку начинайте экспериментировать со свойствами и возможностями.
“Если код и комментарии противоречат друг другу,
то, вероятно, оба неверны”.
Норм Шрайер (Norm Schreyer)
В этом приложении показано, как загрузить, инсталлировать и отредактировать связи графической библиотеки FLTK.
Мы выбрали библиотеку FLTK (Fast Light Tool Kit) (читается как “фултик”) как основу для нашего представления графики и решения задач, связанных с созданием графического пользовательского интерфейса, потому что она является переносимой, относительно простой, относительно широко распространенной и легко инсталлируемой. Мы покажем, как инсталлировать библиотеку FLTK в среде Microsoft Visual Studio, потому что именно это интересует большинство наших студентов и вызывает у них наибольшие затруднения. Если вы используете какую-то другую систему (как некоторые из наших студентов), просто поищите в главном каталоге загружаемых файлов (раздел Г.3) инструкции, касающиеся вашей системы.
Если вы используете библиотеку, не являющуюся частью стандарта ISO C++, то вам придется загрузить ее, инсталлировать и правильно использовать в своем коде. Эта задача редко бывает тривиальной, так что инсталлирование библиотеки FLTK — неплохая задача, потому что загрузка и инсталлирование даже самой хорошей библиотеки часто вызывают трудности, если вы никогда не делали этого раньше. Не стесняйтесь спрашивать совета у людей, делавших это раньше, но не перепоручайте им свои проблемы, а учитесь у них.
Отметим, что в реальности файлы и процедуры могут немного отличаться от того, что мы описываем. Например, может появиться новая версия библиотеки FLTK или вы можете изменить версию Visual Studio, или вообще перейти в совершенно другую среду.
Перед тем как делать что-нибудь, сначала проверьте, не установлена ли библиотека FLTK на вашем компьютере (см. раздел Г.5). Если нет, то загрузите файлы библиотеки.
1. Зайдите на веб-страницу http://fltk.org. (Если не получится, то можете скопировать эти файлы с веб-сайта, посвященного этой книге (www.stroustrup.com/Programming/FLTK).
2. Щелкните на кнопке Download в навигационном меню.
3. Выберите в выпадающем меню пункт FLTK 1.1.x и щелкните на кнопке Show Download Locations.
4. Выберите место, откуда вы будете загружать файл, и загрузите файл с расширением .zip.
Полученный вами файл записан в формате zip. Это формат архивации, удобный для передачи файлов по сети. Для того чтобы разархивировать файлы и привести их к обычному виду, вам нужна специальная программа, например, в системе Windows для этого подходят программы WinZip и 7-Zip.
При выполнении инструкций может возникнуть одна из двух проблем: за время, прошедшее с момента выхода нашей книги, что-то изменилось (это случается), или вы не понимаете терминологию (в этом случае мы ничем не можем вам помочь; извините). В последнем случае позовите друга, который вам все объяснит.
1. Распакуйте загруженный файл и откройте основной каталог,
fltk-1.1.
? В каталоге системы C++ (например, vc2005
или vcnet
) откройте файл fltk.dsw
. Если вас попросят подтвердить обновление всех старых проектов, отвечайте Yes to All.
2. В меню Build выберите команду Build Solution. Это может занять несколько минут. Исходный код компилируется в статические библиотеки (static link libraries), поэтому вам не придется компилировать исходный код библиотеки FLTK каждый раз при создании нового проекта. После завершения процесса закройте среду Visual Studio.
3. В основном каталоге библиотеки FLTK откройте подкаталог
lib
. Скопируйте (а не просто переместите или перетащите) все файлы с расширением .lib
, за исключением файла README.lib
(их должно быть семь) в каталог C:\Program Files\Microsoft Visual Studio\Vc\lib.
4. Вернитесь в основной каталог библиотеки FLTK и скопируйте подкаталог FL в каталог C:\Program Files\Microsoft Visual Studio\Vc\include.
Эксперты скажут вам, что было бы лучше инсталлировать библиотеку, а не копировать файлы в каталоги C:\Program Files\Microsoft Visual Studio\Vc\lib и C:\Program Files\Microsoft Visual Studio\Vc\include. Они правы, но мы не стремимся быть экспертами по среде Visual Studio. Если эксперты будут настаивать, попросите их продемонстрировать лучшую альтернативу.
1. Создайте новый проект в среде Visual Studio, внеся одно изменение в обычной процедуре: выбирая тип проекта, выберите опцию “Win32 project”, а не “Console application”. Убедитесь, что вы создаете “Empty project”; в противном случае мастер добавит в ваш проект много лишнего кода, который вы не поймете и вряд ли будете использовать.
2. Находясь в среде Visual Studio, выберите команду Project в главном меню, а в выпадающем меню выполните команду Properties.
3. В левом меню окна Properties щелкните на пиктограмме Linker. В открывающемся подменю выберите команду Input. В поле редактирования Dependencies, находящемся справа, введите следующий текст:
fltkd.lib wsock32.lib comctl32.lib fltkjpegd.lib fltkimagesd.lib
(Следующий шаг может оказаться ненужным, поскольку в настоящее время он выполняется по умолчанию.)
В поле редактирования Ignore Specific Library введите следующий текст:
libcd.lib
4. Этот шаг может оказаться ненужным, так как в настоящее время опция /MDd включается по умолчанию. В левом меню того же самого окна Properties выберите команду C/C++, чтобы открыть другое подменю. Открыв подменю, выберите команду Code Generation. В правом меню измените опцию Runtime Library на Multi-threaded Debug DLL (/MDd). Щелкните на кнопке OK, чтобы закрыть окно Properties.
Создайте новый файл с расширением
.cpp
в новом проекте и введите следующий код. Он должен скомпилироваться без проблем.
#include
#include
#include
int main()
{
Fl_Window window(200, 200, "Window title");
Fl_Box box(0,0,200,200,"Hey, I mean, Hello, World!");
window.show();
return Fl::run();
}
Если что-то не работает, выполните следующее.
• Если вы получили сообщение компилятора, утверждающее, что файл с расширением
.lib
невозможно найти, то, возможно, вы сделали что-то не так при инсталлировании библиотеки. Внимательно проверьте п. 3, в котором указан путь для сохранения библиотечных файлов (.lib
) на вашем компьютере.
• Если вы получили сообщение компилятора, утверждающее, что файл с расширением
.h
невозможно открыть, значит, скорее всего, вы ошиблись при инсталлировании. Внимательно проверьте п. 3, в котором указан путь для сохранения заголовочных файлов (.h
) на вашем компьютере.
• Если вы получили сообщение редактора связей, упоминающее о неразрешенных внешних ссылках, то проблема таится в свойствах проекта.
Если наши советы вам не помогли, зовите друга.
“Когда вы наконец поймете, что делаете,
то все пойдет правильно”
Билл Фэйрбэнк (Bill Fairbank)
В этом приложении представлена реализация обратных вызовов, а также классов
Window
, Widget
и Vector_ref
. В главе 16 мы не требовали от читателей знать об указателях и операторах приведения типа, поэтому вынесли подробные объяснения в приложение.
Обратные вызовы реализованы следующим образом:
void Simple_window::cb_next(Address, Address addr)
// вызов функции Simple_window::next() для окна,
// расположенного по адресу addr
{
reference_to(addr).next();
}
Поскольку вы уже прочитали главу 17, то вам должно быть очевидно, что аргумент
Address
должен иметь тип void*
. И, разумеется, функция reference_to(addr)
должна каким-то образом создавать ссылку на объект класса Simple_window
из указателя addr
, имеющего тип void*
. Однако, если у вас нет опыта программирования, то ничто для вас не “очевидно” и не “разумеется”, пока вы не прочтете главу 17, поэтому рассмотрим и использование адресов подробнее.
Как описано в разделе A.17, язык C++ предлагает способ для указания имени типа. Рассмотрим пример.
typedef void* Address; // Address — это синоним типа void*
Это значит, что мы можем использовать имя
Address
вместо void*
. В данном случае, используя имя Address
, мы хотим подчеркнуть, что передаем адрес, и скрыть тот факт, что void*
— это имя типа указателя на объект, тип которого неизвестен.
Итак, функция
cb_next()
получает указатель типа void*
с именем addr
в качестве аргумента и — каким-то образом — немедленно преобразовывает его в ссылку Simple_window&
:
reference_to(addr)
Функция
reference_to
является шаблонной (раздел A.13).
templateW& reference_to(Address pw)
// интерпретирует адрес как ссылку на объект класса W
{
return *static_cast(pw);
}
Здесь мы использовали шаблонную функцию, для того чтобы самостоятельно написать операции, действующие как приведение типа
void*
к типу Simple_window&
. Это приведение типа static_cast
описано в разделе 17.8.
Компилятор не имеет возможности проверить наши предположения о том, что аргумент
addr
ссылается на объект класса Simple_window
, но правила языка требуют, чтобы компилятор в этом вопросе доверял программисту. К счастью, мы оказались правы. Об этом свидетельствует от факт, что система FLTK возвращает нам обратно указатель, который мы ей передавали. Поскольку, передавая указатель системе FLTK, мы знали его тип, можно использовать функцию reference_to
, чтобы “получить его обратно”. Все это немного запутанно, не проходит проверку и не больше характерно для низкоуровневого программирования.
Получив ссылку на объект класса
Simple_window
, мы можем использовать ее для вызова функции-члена класса Simple_window
. Рассмотрим пример (раздел 16.3).
void Simple_window::cb_next(Address, Address pw)
// вызов функции Simple_window::next() для окна,
// расположенного по адресу pw
{
reference_to(pw).next();
}
Мы использовали довольно сложную функцию обратного вызова
cb_next()
, просто чтобы согласовать типы, необходимые для вызова совершенно обычной функции-члена next()
.
Наш интерфейсный класс
Widget
выглядит следующим образом.
class Widget {
// Класс Widget — это дескриптор класса Fl_widget,
// а не сам класс Fl_widget;
// мы пытаемся не смешивать наши интерфейсные классы с FLTK
public:
Widget(Point xy, int w, int h, const string& s, Callback cb)
:loc(xy), width(w), height(h), label(s), do_it(cb)
{ }
virtual ~Widget() { } // деструктор
virtual void move(int dx,int dy)
{ hide(); pw–>position(loc.x+=dx, loc.y+=dy); show(); }
virtual void hide() { pw–>hide(); }
virtual void show() { pw–>show(); }
virtual void attach(Window&) = 0; // каждый объект класса
// Widget определяет хотя бы
// одно действие над окном
Point loc;
int width;
int height;
string label;
Callback do_it;
protected:
Window* own; // каждый объект класса Widget
// принадлежит объекту классу Window
Fl_Widget* pw; // каждый объект класса Widget о "своем"
// классе Fl_Widget
};
Обратите внимание на то, что наш класс
Widget
следит за “своим” компонентом библиотеки FLTK и классом Window
, с которыми он связан. Кроме того, отметьте, что для этого нам необходимы указатели, поскольку объект класса Widget
на протяжении времени своего существования может быть связан с разными объектами класса Window
. Ссылки или именованного объекта для этого недостаточно. (Объясните почему?)
Объект класса
Widget
имеет местоположение (loc
), прямоугольную форму (width
и height
), а также сметку (label
. Интересно, что он также имеет функцию обратного вызова (do_it
), т.е. связывает образ объекта класса Widget
на экране с фрагментом своего кода. Смысл операций move()
, show()
, hide()
и attach()
должен быть очевидным.
Класс
Widget
выглядит незаконченным. Он спроектирован как класс реализации, который пользователи не должны видеть слишком часто. Его стоит переделать. Мы подозреваем, что все эти открытые члены и “очевидные” операции содержат подводные камни.
Класс
Widget
имеет виртуальную функцию и может быть использован как базовый класс, поэтому в нем предусмотрен виртуальный деструктор (см. раздел 17.5.2).
Когда следует использовать указатели, а когда ссылки? Мы обсудили этот общий вопрос в разделе 8.5.6. Здесь мы лишь отметим, что некоторые программисты любят указатели и что нам нужны указатели, когда мы хотим сослаться на разные объекты в разные моменты времени.
До сих пор мы скрывали главный класс в нашей графической библиотеке — класс
Window
. Основная причина этого заключалась в том, что он использует указатели, а его реализация с помощью библиотеки FLTK опирается на использование свободной памяти. Вот как описан этот класса в заголовочном файле Window.h
.
class Window : public Fl_Window {
public:
// позволяет системе выбрать место в памяти:
Window(int w, int h, const string& title);
// верхний левый угол в точке xy:
Window(Point xy, int w, int h, const string& title);
virtual ~Window() { }
int x_max() const { return w; }
int y_max() const { return h; }
void resize(int ww, int hh) { w=ww, h=hh; size(ww,hh); }
void set_label(const string& s) { label(s.c_str()); }
void attach(Shape& s) { shapes.push_back(&s); }
void attach(Widget&);
void detach(Shape& s); // удаляет элемент w из фигур
void detach(Widget& w); // удаляет элемент w из окна
// (отключает обратные вызовы)
void put_on_top(Shape& p); // помещает объект p поверх
// всех других фигур
protected:
void draw();
private:
vector shapes; // фигуры связываются с окном
int w,h; // размер окна
void init();
};
Итак, когда мы связываем фигуру с окном, используя функцию
attach()
, мы храним указатель в объектах класса Shape
, поэтому объект класса Window
может рисовать соответствующую фигуру. Поскольку впоследствии мы можем отсоединить фигуру от окна с помощью функции detach()
, поэтому нам нужен указатель. По существу, присоединенная фигура принадлежит своему коду; мы просто передаем объекту класса Window
ссылку на нее. Функция Window::attach()
преобразовывает свой аргумент в указатель, чтобы его можно было сохранить. Как показано выше, функция attach()
является тривиальной; функция detach()
немного сложнее. Открыв файл Window.cpp
, мы видим следующее.
void Window::detach(Shape& s)
// определяет, что первой должна быть удалена
// последняя присоединенная фигура
{
for (unsigned int i = shapes.size(); 0
if (shapes[i–1]==&s) shapes.erase(&shapes[i–1]);
}
Функция-член
erase()
удаляет (стирает) значение из вектора, уменьшая его размер на единицу (раздел 20.7.1). Класс Window
используется как базовый, поэтому он содержит виртуальный деструктор (раздел 17.5.2).
По существу, класс
Vector_ref
имитирует вектор ссылок. Мы можем инициализировать его ссылками или указателями.
• Если объект передается объекту класса
Vector_ref
с помощью ссылки, то предполагается, что он принадлежит вызывающей функции, которая управляет его временем жизни (например, объект — это переменная, находящаяся в определенной области видимости).
• Если объект передается объекту класса
Vector_ref
с помощью указателя, то предполагается, что он размещен в памяти с помощью оператора new
, а ответственность за его удаление несет класс Vector_ref
.
Элемент хранится в объекте класса
Vector_ref
в виде указателя, а не как копия объекта, и имеет семантику ссылок. Например, можно поместить в вектор класса Vector_ref
объект класса Circle
, не подвергаясь опасности срезки.
template class Vector_ref {
vector v;
vector owned;
public:
Vector_ref() {}
Vector_ref(T* a, T* b = 0, T* c = 0, T* d = 0);
~Vector_ref() { for (int i=0; i
delete owned[i]; }
void push_back(T& s) { v.push_back(&s); }
void push_back(T* p) { v.push_back(p); owned.push_back(p); }
T& operator[](int i) { return *v[i]; }
const T& operator[](int i) const { return *v[i]; }
int size() const { return v.size(); }
};
Деструктор класса
Vector_ref
удаляет каждый объект, переданный ему как указатель.
Это законченная программа. Она демонстрирует многие из свойств классов
Widget/Window
. Мы поместили в нее минимальное количество комментариев. К сожалению, такое недостаточное комментирование программ — довольно распространенное явление. Попытайтесь выполнить эту программу и объяснить, как она работает.
#include "../GUI.h"
using namespace Graph_lib;
class W7 : public Window {
// четыре способа продемонстрировать, что кнопка может
// передвигаться:
// показать/скрыть, изменить местоположение, создать новую
// и присоединить/отсоединить
public:
W7(int n, int n, const string& t);
Button* p1; // показать/скрыть
Button* p2;
bool sh_left;
Button* mvp; // переместить
bool mv_left;
Button* cdp; // создать/уничтожить
bool cd_left;
Button* adp1; // активировать/деактивировать
Button* adp2;
bool ad_left;
void sh(); // действия
void mv();
void cd();
void ad();
static void cb_sh(Address, Address addr) // обратные вызовы
{ reference_to(addr).sh(); }
static void cb_mv(Address, Address addr)
{ reference_to(addr).mv(); }
static void cb_cd(Address, Address addr)
{ reference_to(addr).cd(); }
static void cb_ad(Address, Address addr)
{ reference_to(addr).ad(); }
};
Однако объект класса
W7
(эксперимент с объектом класса Window
номер 7
) на самом деле содержит шесть кнопок: просто две из них он скрывает.
W7::W7(int w, int h, const string& t)
:Window(w,h,t),
sh_left(true),mv_left(true),cd_left(true),ad_left(true)
{
p1 = new Button(Point(100,100),50,20,"show",cb_sh);
p2 = new Button(Point(200,100),50,20,"hide",cb_sh);
mvp = new Button(Point(100,200),50,20,"move",cb_mv);
cdp = new Button(Point(100,300),50,20,"create",cb_cd);
adp1 = new Button(Point(100,400),50,20,"activate",cb_ad);
adp2 = new Button(Point(200,400),80,20,"deactivate",cb_ad);
attach(*p1);
attach(*p2);
attach(*mvp);
attach(*cdp);
p2–>hide();
attach(*adp1);
}
В этом классе существуют четыре обратных вызова. Каждый из них проявляется в том, что нажатая кнопка исчезает и вместо нее появляется новая. Однако это достигается четырьмя разными способами.
voidW7::sh() // скрывает кнопку, показывает следующую
{
if (sh_left) {
p1–>hide();
p2–>show();
}
else {
p1–>show();
p2–>hide();
}
sh_left = !sh_left;
}
void W7::mv() // перемещает кнопку
{
if (mv_left) {
mvp–>move(100,0);
}
else {
mvp–>move(–100,0);
}
mv_left = !mv_left;
}
void W7::cd() // удаляет кнопку и создает новую
{
cdp–>hide();
delete cdp;
string lab = "create";
int x = 100;
if (cd_left) {
lab = "delete";
x = 200;
}
cdp = new Button(Point(x,300), 50, 20, lab, cb_cd);
attach(*cdp);
cd_left = !cd_left;
}
void W7::ad() // отсоединяет кнопку от окна и
// устанавливает связь с ее заменой
{
if (ad_left) {
detach(*adp1);
attach(*adp2);
}
else {
detach(*adp2);
attach(*adp1);
}
ad_left = !ad_left;
}
int main()
{
W7 w(400,500,"move");
return gui_main();
}
Эта программа демонстрирует основные способы добавления и удаления элементов окна, которые проявляются в их исчезновении и появлении.