Введение в язык Си++

         

Свободная память


Именованный объект является либо статическим, либо автоматическим см. ). Статический объект размещается во время запуска программы и существует в течение всего выполнения программы. Автоматический объект размещается каждый раз при входе в его блок и существует только до тех пор, пока из этого блока не вышли. Однако часто бывает полезно создать новый объект, существующий до тех пор, пока он не станет больше не нужен. В частности, часто полезно создать объект, который можно использовать после возврата из функции, где он создается. Такие объекты создает операция new, а в последствие уничтожать их можно операцией delete. Про объекты, выделенные с помощью операции new, говорят, что они в свободной памяти. Такими объектами обычно являются вершины деревьев или элементы связанных списков, являющиеся частью большей структуры данных, размер которой не может быть известен на стадии компиляции. Рассмотрим, как можно было бы написать компилятор в духе написанного настольного калькулятора. Функции синтаксического анализа могут строить древовидное представление выражений, которое будет использоваться при генерации кода. Например:

struct enode { token_value oper; enode* left; enode* right; };

enode* expr() { enode* left = term();

for(;;) switch(curr_tok) { case PLUS: case MINUS: get_token(); enode* n = new enode; n-oper = curr_tok; n-left = left; n-right = term(); left = n; break; default: return left; } }

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

void generate(enode* n) { switch (n-oper) { case PLUS: // делает нечто соответствующее delete n; } }

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


Рассмотрим:

main() { table* p = new table(100); table* q = new table(200); delete p; delete p; // возможно, ошибка }

Конструктор table::table() будет вызван дважды, как и деструктор table::~table(). То, что C++ не дает никаких гарантий, что для объекта, созданного с помощью new, когда-либо будет вызван деструктор, ничего не значит. В предыдущей программе q не уничтожается, а p уничтожается дважды! Программист может счесть это ошибкой, а может и не счесть, в зависимости от типа p и q. Обычно то, что объект не уничтожается, является не ошибкой, а просто лишней тратой памяти. Уничтожение p дважды будет , как правило, серьезной ошибкой. Обычно результатом применения delete дважды к одному указателю приводит к бесконечному циклу в подпрограмме управления свободной памятью, но определение языка не задает поведение в таком случае, и оно зависит от реализации.

Пользователь может определить новую реализацию операций new и delete (см. ). Можно также определить способ взаимодействия конструктора или деструктора с операциями new и delete (см. #5.5.6)




Операция new создает объект типа имя_типа (см. #8.7), к которому он применен. Время жизни объекта, созданного с помощью new, не ограничено областью видимости, в которой он создан. Операция new возвращает указатель на созданный ей объект. Когда объект является массивом, возвращается указатель на его первый элемент. Например, и new int и new int[10] возвращают int*. Для объектов некоторых классов надо предоставлять инициализатор (#8.6.2). Операция new (#7.2) для получения памяти вызывает функцию

void* operator new (long);

Параметр задает требуемое число байтов. Память будет инициализирована. Если operator new() не может найти требуемое количество памяти, то она возвращает ноль.

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

Чтобы освободить указанную память, операция delete вызывает функцию

void operator delete (void*);



В форме

delete [ выражение ] выражение

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




Если вы пользовались классом slist, вы могли обнаружить, что ваша программа тратит на заметное время на размещение и освобождение объектов класса slink. Класс slink - это превосходный пример класса, который может значительно выиграть от того, что программист возьмет под контроль управление свободной памятью. Для этого вида объектов идеально подходит оптимизирующий метод, который описан в Поскольку каждый slink создается с помощью new и уничтожается с помощью delete членами класса slist, другой способ выделения памяти не представляет никаких проблем.

Если производный класс осуществляет присваивание указателю this, то конструктор его базового класса будет вызываться только после этого присваивания, и значение указателя this в конструкторе базового класса будет тем, которое присвоено конструктором производного класса. Если базовый класс присваивает указателю this, то будет присвоено то значение, которое использует конструктор производного класса. Например:

#include

struct base { base(); };

struct derived : base { derived(); }

base::base() { cout

порождает вывод

base b; base 1: this=2147478307 base 2: this=2147478307 new base; base 1: this=0 base 2: this=27 derived d; derived 1: this=2147478306 base 1: this=2147478306 base 2: this=2147478306 derived 1: this=2147478306 new derived; derived 1: this=0 base 1: this=43 base 2: this=43 derived 1: this=43 at the end

Если деструктор производного класса осуществляет присваивание указателю this, то будет присвоено то значение, которое встретил деструктор его базового класса. Когда кто-либо делает в конструкторе присваивание указателю this, важно, чтобы присваивание указателю this встречалось на всех путях в конструкторе*1.




Операция new (#7.2) вызывает функцию

extern void* _new (long);

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

Операция delete вызывает функцию

extern void _delete (void*);

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

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

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

class cl { int v[10]; cl () { this = my_own_allocator (sizeof (cl)); } ~cl () { my_own_deallocator (this); this = 0; } }

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

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



Сводка операторов


Операторы C++ систематически и полностью изложены в #с.9, прочитайте, пожалуйста, этот раздел. А здесь приводится краткая сводка и некоторые примеры.



Таблица имен


К таблице имен доступ осуществляется с помощью одной функции

name* look(char* p, int ins =0);

Ее второй параметр указывает, нужно ли сначала поместить строку символов в таблицу. Инициализатор =0 задает параметр, который надлежит использовать по умолчанию, когда look() вызывается с одним параметром. Это дает удобство записи, когда look("sqrt2") означает look("sqrt2",0), то есть просмотр, без помещения в таблицу. Чтобы получить такое же удобство записи для помещения в таблицу, определяется вторая функция:

inline name* insert(char* s) { return look(s,1);}

Как уже отмечалось раньше, элементы этой таблицы имеют тип:

srtuct name { char* string; char* next; double value; }

Член next используется только для сцепления вместе имен в таблице. Сама таблица - это просто вектор указателей на объекты типа name:

const TBLSZ = 23; name* table[TBLSZ];

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

Для нахождения элемента в таблице в look() принимается простой алгоритм хэширования (имена с одним и тем же хэш-кодом зацепляются вместе):

int ii = 0; // хэширование char* pp = p; while (*pp) ii = ii

То есть, с помощью исключающего ИЛИ каждый символ во входной строке "добавляется" к ii ("сумме" предыдущих символов). Бит в x^y устанавливается единичным тогда и только тогда, когда соответствующие биты в x и y различны. Перед применением в символе исключающего ИЛИ, ii сдвигается на один бит влево, чтобы не использовать в слове только один байт. Это можно было написать и так:

ii

Кстати, применение ^ лучше и быстрее, чем +. Сдвиг важен для получения приемлемого хэш-кода в обоих случаях. Операторы

if (ii

обеспечивают, что ii будет лежать в диапазоне 0...TBLSZ-1; % - это операция взятия по модулю (еще называемая получением остатка).

Вот функция полностью:

extern int strlen(const char*); extern int strcmp(const char*, const char*); extern int strcpy(const char*, const char*);


name* look(char* p, int ins =0) { int ii = 0; // хэширование char* pp = p; while (*pp) ii = iinext) // поиск if (strcmp(p,n-string) == 0) return n;
if (ins == 0) error("имя не найдено");
name* nn = new name; // вставка nn-string = new char[strlen(p)+1]; strcpy(nn-string,p); nn-value = 1; nn-next = table[ii]; table[ii] = nn; return nn; }
После вычисления хэш- кода ii имя находится простым просмотром через поля next. Проверка каждого name осуществляется с помощью стандартной функции strcmp(). Если строка найдена, возвращается ее name, иначе добавляется новое name.
Добавление нового name включает в себя создание нового объекта в свободной памяти с помощью операции new (см. #3.2.6), его инициализацию, и добавление его к списку имен. Последнее осуществляется просто путем помещения нового имени в голову списка, поскольку это можно делать даже не проверяя, имеется список, или нет. Символьную строку для имени тоже нужно сохранить в свободной памяти. Функция strlen() используется для определения того, сколько памяти нужно, new - для выделения этой памяти, и strcpy() - для копирования строки в память.

Тип void


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

void f() // f не возвращает значение void* pv; // указатель на объект неизвестного типа

Переменной типа void* можно присваивать указатель любого типа. На первый взгляд это может показаться не особенно полезным, поскольку void* нельзя разыменовать, но именно это ограничение и делает тип void* полезным. Главным образом, он применяется для передачи указателей функциям, которые не позволяют сделать предположение о типе объекта, и для возврата из функций нетипизированных объектов. Чтобы использовать такой объект, необходимо применить явное преобразование типа. Подобные функции обычно находятся на самом нижнем уровне системы, там, где осуществляется работа с основными аппаратными ресурсами. Например:

void* allocate(int size); // выделить void deallocate(void*); // освободить

f() { int* pi = (int*)allocate(10*sizeof(int)); char* pc = (char*)allocate(10); //... deallocate(pi); deallocate(pc); }



Типы


Каждое имя (идентификатор) в C++ программе имеет ассоциированный с ним тип. Этот тип определяет, какие операции можно применять к имени (то есть к объекту, на который оно ссылается), и как эти операции интерпретируются. Например:

int error number; float real(complex* p);

Поскольку error_number описано как int, его можно присваивать, использовать в арифметических выражениях и т.д. Тогда как функция real может вызываться с адресом complex в качестве параметра. Можно взять адрес любого из них. Некоторые имена, вроде int и complex, являются именами типов. Обычно имя типа используется в описании для спецификации другого имени. Единственные отличные от этого действия над именем типа - это sizeof (для определения количества памяти, которая требуется для хранения объекта типа) и new (для размещения объекта типа в свободной памяти). Например:

main() { int* p = new int; cout

Имя типа можно также использовать для задания явного преобразования одного типа в другой, например:

float f; char* p; //... long ll = long(p); // преобразует p в long int i = int(f); // преобразует f в int



Типы и Описания


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

int inch;

определяет, что inch имеет тип int, то есть, inch является целой переменной.

Описание - это оператор, который вводит имя в программе. Описание задает тип этого имени. Тип определяет правильное использование имени или выражения. Для целых определены такие операции, как +, -, * и /. После того, как включен файл stream.h, объект типа int может также быть вторым операндом Тип объекта определяет не только то, какие операции могут к нему применяться, но и смысл этих операций. Например, оператор

cout

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

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



Турне по C++


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

- Брайэн Керниган

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



Указатель на Функцию


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

void error(char* p) { /* ... */ }

void (*efct)(char*); // указатель на функцию

void f() { efct = error // efct указывает на error (*efct)("error"); // вызов error через efct }

Чтобы вызвать функцию через указатель, например, efct, надо сначала этот указатель разыменовать, *efct. Поскольку операция вызова функции () имеет более высокий приоритет, чем операция разыменования *, то нельзя писать просто *efct("error"). Это означает *efct("error"), а это ошибка в типе. То же относится и к синтаксису описаний (см. также #7.3.4).

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

void (*pf)(char*); // указатель на void(char*) void f1(char*); // void(char*) int f2(char*); // int(char*) void f3(int*); // void(int*)

void f() { pf = f1 // ok pf = f2 // ошибка: не подходит возвращаемый тип pf = f3 // ошибка: не подходит тип параметра

(*pf)("asdf"); // ok (*pf)(1); // ошибка: не подходит тип параметра

int i = (*pf)("qwer"); // ошибка: void присваивается int'у }

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

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

typedef int (*SIG_TYP)(); // из typedef void (*SIG_ARG_TYP); SIG_TYP signal(int,SIG_ARG_TYP);

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

typedef void (*PF)();

PF edit_ops[] = { // операции редактирования cut, paste, snarf, search };


PF file_ops[] = { // управление файлом open, reshape, close, write };

Затем определяем и инициализируем указатели, определяющие действия, выбранные в меню, которое связано с кнопками (button) мыши:

PF* button2 = edit_ops; PF* button3 = file_ops;

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

(button2[3])();

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

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

typedef int (*CFT)(char*,char*);

int sort(char* base, unsigned n, int sz, CFT cmp) /* Сортирует "n" элементов вектора "base" в возрастающем порядке с помощью функции сравнения, указываемой "cmp". Размер элементов "sz".

Очень неэффективный алгоритм: пузырьковая сортировка */ { for (int i=0; iname, Puser(q)-name); }

int cmp2(char*p, char* q) // Сравнивает числа dept { return Puser(p)-dept-Puser(q)-dept; }

Эта программа сортирует и печатает:

main () { sort((char*)heads,6,sizeof(user),cmp1); print_id(heads,6); // в алфавитном порядке cout

Можно взять адрес inline-функции, как, впрочем, и адрес перегруженной функции(#с.8.9).


Указатели


Для большинства типов T T* является типом указатель на T. То есть, в переменной типа T* может храниться адрес объекта типа T. Для указателей на вектора и указателей на функции вам, к сожалению, придется пользоваться более сложной записью:

int* pi; char** cpp; // указатель на указатель на char int (*vp)[10]; // указатель на вектор из 10 int'ов int (*fp)(char, char*); // указатель на функцию // получающую параметры (char, char*) // и возвращающую int

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

char c1 = 'a'; char* p = c1 // в p хранится адрес c1 char c2 = *p; // c2 = 'a'

Переменная, на которую указывает p,- это c1, а значение, которое хранится в c1, это 'a', поэтому присваиваемое c2 значение *p есть 'a'.

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

int strlen(char* p) { int i = 0; while (*p++) i++; return i; }

Другой способ найти длину состоит в том, чтобы сначала найти конец строки, а затем вычесть адрес начала строки из адреса ее конца:

int strlen(char* p) { char* q = p; while (*q++) ; return q-p-1; }

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


Если производный класс derived имеет открытый базовый класс base, то указатель на derived можно присваивать переменной типа указатель на base не используя явное преобразование типа. Обратное преобразование, указателя на base в указатель на derived, должно быть явным. Например:

class base { /* ... */ }; class derived : public base { /* ... */ };

derived m; base* pb = m // неявное преобразование derived* pd = pb; // ошибка: base* не является derived* pd = (derived*)pb; // явное преобразование

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

Будь base закрытым базовым классом класса derived, неявное преобразование derived* в base* не делалось бы. Неявное преобразование не может в этом случае быть выполнено, потому что к открытому члкну класса base можно обращаться через указатель на base, но нельзя через указатель на derived:

class base { int m1; public: int m2; // m2 - открытый член base };

class derived : base { // m2 НЕ открытый член derived };

derived d; d.m2 = 2; // ошибка: m2 из закрытой части класса base* pb = d // ошибка: (закрытый base) pb-m2 = 2; // ok pb = (base*)d // ok: явное преобразование pb-m2 = 2; // ok

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



Указатели и целые


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

Можно производить вычитание над двумя указателями на объекты одного типа; в этом случае результат преобразуется к типу int или long в зависимости от машины; см. #7.4.



Указатели и Вектора


Указатели и вектора в C++ связаны очень тесно. Имя вектора можно использовать как указатель на его первый элемент, поэтому пример с алфавитом можно было написать так:

char alpha[] = "abcdefghijklmnopqrstuvwxyz"; char* p = alpha; char ch;

while (ch = *p++) cout

Описание p можно было также записать как

char* p = alpha[0];

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

extern int strlen(char*); char v[] = "Annemarie"; char* p = v; strlen(p); strlen(v);

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

Результат применения к указателям арифметических операций +, -, ++ или -- зависит от типа объекта, на который они указывают. Когда к указателю p типа T* применяется арифметическая операция, предполагается, что p указывает на элемент вектора объектов типа T; p+1 означает следующий элемент этого вектора, а p-1 - предыдущий элемент. Отсюда следует, что значение p+1 будет на sizeof(T) больше значения p. Например, выполнение

main() { char cv[10]; int iv[10];

char* pc = cv; int* pi = iv;

cout

дает

char* 1 int* 4

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



Указатели на Члены


Можно брать адрес члена класса. Получение адреса функции члена часто бывает полезно, поскольку те цели и причины, которые приводились в #4.6.9 относительно указателей на функции, в равной степени применимы и к функциям членам. Однако, на настоящее время в языке имеется дефект: невозможно описать выражением тип указателя, который получается в результате этой операции. Поэтому в текущей реализации приходится жульничать, используя трюки. Что касается примера, который приводится ниже, то не гарантируется, что он будет работать. Используемый трюк надо локализовать, чтобы программу можно было преобразовать с использованием соответствующей языковой конструкции, когда появится такая возможность. Этот трюк использует тот факт, что в текущей реализации this реализуется как первый (скрытый) параметр функции члена:

#include

struct cl { char* val; void print(int x) { cout

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



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


Выражения с унарными операциями группируют справа налево:

унарное_выражение: унарная_операция выражение выражение ++ выражение -- sizeof выражение sizeof ( имя_типа ) ( имя_типа ) выражение простое_имя_типа ( список_выражений ) new имя_типа инициализатор opt new ( имя_типа ) delete выражение delete [ выражение ] выражение унарная_операция: одна из * - ! ~ ++ --

Унарная операция * означает косвенное обращение: выражение должно быть указателем и результатом будет lvalue, ссылающееся на объект, на который указывает выражение. Если выражение имеет тип "указатель на ...", то тип результата есть "...".

Результатом унарной операции является указатель на объект, на который ссылается операнд. Операнд должен быть lvalue. Если выражение имеет тип "...", то тип результата есть "указатель на ...".

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

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

Результатом операции логического отрицания ! является 1, если значение операнда 0, и 0, если значение операнда не 0. Результат имеет тип int. Применима к любому арифметическому типу или к указателям.

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


Унарная операция, префиксная или постфиксная, может быть определена или с помощью функции члена (см. #8.5.4), не получающей параметров, или с помощью функции друга (см. #8.5.10), получающей один параметр, но не двумя способами одновременно. Так, для любой унарной операции @, x@ и @x могут интерпретироваться как x.операция@() или операция@(x). При перегрузке операций ++ и -- невозможно различить префиксное и постфиксное использование.



Unsigned


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

При преобразовании целого без знака в длинное значение результата численно совпадает со значением целого без знака. Таким образом, преобразование сводится к дополнению нулями слева.



Управление строкой


Для помощи другим препроцессорам, генерирующим программы на C, строка вида

#line константа "имя_файла"

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



в конце глав. Все упражнения


Упражнения находятся в конце глав. Все упражнения главным образом типа напишите программу. Для решения всегда пишите такую программу, которая будет компилироваться и работать по меньшей мере на нескольких тестовых случаях. Упражнения различаются в основном по сложности, поэтому они помечены оценкой степени сложности. Шкала экспоненциальная, так что если на упражнение (*1) вам потребовалось пять минут, то упражнение (*2) вам может потребоваться час, а на (*3) - день. Время, которое требуется на то, чтобы написать и оттестировать программу, зависит больше от опыта читателя, нежели от самого упражнения. Упражнение (*1) может отнять день, если для того, чтобы запустить ее, читателю сначала придется знакомиться с новой вычислительной системой. С другой стороны, тот, у кого под рукой окажется нужный набор программ, может сделать упражнение (*5) за час. В качестве источника упражнений к Главам 2-4 можно использовать любую книгу по C. У Ахо и др. [1] приведено большое количество общих структур данных и алгоритмов в терминах абстрактных типов данных. Эту книгу также может служить источником упражнений к Главам 5-7. Однако языку, который в этой книге использовался, недостает как функций членов, так и производных классов. Поэтому определенные пользователем типы часто можно выражать в C++ более элегантно.


(*1) Заставьте работать программу с "Hello, world" (1.1.1).
(*1) Для каждого описания в сделайте следующее: Если описание не является определением, напишите для него определение. Если описание является определением, напишите для него описание, которое при этом не является определением.
(*1) Напишите описания для: указателя на символ; вектора из 10 целых; ссылки на вектор из 10 целых; указателя на вектор из символьных строк; указателя на указатель на символ; константного целого; указателя на константное целое; и константного указателя на целое. Каждый из них инициализируйте.
(*1.5) Напишите программу, которая печатает размеры основных и указательных типов. Используйте операцию sizeof.
(*1.5) Напишите программу, которая печатает буквы 'a'...'z' и цифры '0'...'9' и их числовые значения. Сделайте то же для остальных печатаемых символов. Сделайте то же, но используя шестнадцатиричную запись.
(*1) Напечатайте набор битов, которым представляется указатель 0 на вашей системе. Подсказка:
(*1.5) Напишите функцию, печатающую порядок и мантиссу параметра типа double.
(*2) Каковы наибольшие и наименьшие значения, на вашей системе, следующих типов: char, short, int, long, float, double, unsigned, char*, int* и void*? Имеются ли дополнительные ограничения на принимаемые ими значения? Может ли, например, int* принимать нечетное значение? Как выравниваются в памяти объекты этих типов? Может ли, например, int иметь нечетный адрес?
(*1) Какое самое длинное локальное имя можно использовать в C++ программе в вашей системе? Какое самое длинное внешнее имя можно использовать в C++ программе в вашей системе? Есть ли какие-нибудь ограничения на символы, которые можно употреблять в имени?
(*2) Определите one следующим образом:
const one = 1;
Попытайтесь поменять значение one на 2. Определите num следующим образом:
const num[] = { 1, 2 };
Попытайтесь поменять значение num[1] на 2.
(*1) Напишите функцию, переставляющую два целых (меняющую значения). Используйте в качесте типа параметра int*. Напишите другую переставляющую функцию, использующую в качесте типа параметра int.


(*1) Напишите следующие описания: функция, получающая параметр типа указатель на символ и ссылку на целое и не возвращающая значения; указатель на такую функцию; функция, получающая такой указатель в качестве параметра; и функция, возвращающая такой указатель. Напишите определение функции, которая получает такой указатель как параметр и возвращает свой параметр как возвращаемое значение. Подсказка: используйте typedef.
(*1) Что это значит? Для чего это может использоваться?
typedef int (rifii) (int, int);
(*1.5) Напишите программу вроде "Hello, world", которая получает имя как параметр командной строки и печатает "Hello, имя". Модифицируйте эту программу так, чтобы она получала получала любое количество имен и говорила hello каждому из них.
(*1.5) Напишите программу, которая читает произвольное число файлов, имена которых задаются как аргументы командной стоки, и пишет их один за другим в cout. Поскольку эта программа при выдаче конкатенирует свои параметры, вы можете назвать ее cat (кошка).
(*2) Преобразуйте небольшую C программу в C++. Измените заголовочные файлы так, чтобы описывать все вызываемые функции и описывать тип каждого параметра. Замените, где возможно, директивы #define на enum и const или inline. Уберите из .c файлов описания extern и преобразуйте определения функций к синтаксису C++. Замените вызовы malloc() и free() на new и delete. Уберите необязательные приведения типа.
(*2) Реализуйте sort() (#4.6.7) используя эффективный алгоритм сортировки.
(*2) Посмотрите на определение struct tnode в Напишите функцию для введения новых слов в дерево узлов tnode. Напишите функцию для вывода дерева узлов tnode. Напишите функцию для вывода дерева узлов tnode со словами в алфавитном порядке. Модифицируйте tnode так, чтобы в нем хранился (только) указатель на слово произвольной длины, помещенное с помощью new в свободную память. Модифицируйте функции для использования нового определения tnode.
(*2) Напишите "модуль", реализующий стек. Файл .h должен описывать функции push(), pop() и любые другие удобные функции (только). Файл .c определяет функции и данные, необходимые для хранения стека.


(*1) Модифицируйте настольный калькулятор из Главы 3, чтобы использовать класс table.
(*1) Разработайте tnode (#с.8.5) как класс с конструкторами, деструкторами и т.п. Определите дерево из tnode'ов как класс с конструкторами, деструкторами и т.п.
(*1) Преобразуйте класс intset () в множество строк.
(*1) Преобразуйте класс intset в множество узлов node, где node - определяемая вами структура.
(*3) Определите класс для анализа. хранения, вычисления и печати простых арифметических выражений, состоящих из целых констант и операций +, -, * и /. Открытый интерфейс должен выгляедть примерно так:
class expr { // ... public: expr(char*); int eval(); void print(); }
Параметр строка конструктора expr::expr() является выражением. Функция expr::eval() возвращает значение выражение, а expr::print() печатает представление выражения в cout. Программа может выглядеть, например, так:
expr x("123/4+123*4-3"); cout
Определите класс expr два раза: один раз используя в качестве представления связанный список узлов, а другой раз - символьную строку. Поэкспериментируйте с разными способами печати выражения: с полностью расставленными скобками, в постфиксной записи, в ассемблерном коде и т.д.
(*1) Определите класс char_queue (символьная очередь) таким образом, чтобы открытый интерфейс не зависел от представления. Реализуйте char_queue как (1) связанный список и как (2) вектор. О согласованности не заботьтесь.
(*2) Определите класс histogram (гистограмма), в котором ведется подсчет чисел в определенных интервалах, которые задаются как параметры конструктора histogram. Обеспечьте функцию вывода гистограммы на печать. Сделайте обработку значений, выходящих за границы. Подсказка: .
(*2) Определите несколько классов, предоставляющих случайные числа с определенными распределениями. Каждый класс имеет конструктор, задающий параметры распределения, и функцию draw, которая возвращает "следующее" значение. Подсказка: . Посмотрите также класс intset.
(*2) Перепишите пример date (#5.8.2), пример char_stack () и пример intset () не используя функций членов (даже конструкторов и деструкторов). Используйте только class и friend. Сравните с версиями, в которых использовались функции члены.


(*2) Определите итератор для класса string. Определите операцию конкатенации + и операцию "добавить в конец" +=. Какие еще операции над string вы хотели бы осуществлять?
(*1.5) Задайте с помощью перегрузки () операцию выделения подстроки для класса строк.
(*3) Постройте класс string так, чтобы операция выделения подстроки могла использоваться в левой части присваивания. Напишите сначала версию, в которой строка может присваиваться подстроке той же длины, а потом версию, где эти длины могут быть разными.
(*2) Постройте класс string так, чтобы для присваивания, передачи параметров и т.п. он имел семантику по значению, то есть в тех случаях, когда копируется строковое представление, а не просто управляющая структура данных класса sring.
(*3) Модифицируйте класс string из предыдущего примера таким образом, чтобы строка копировалась только когда это необходимо. То есть, храните совместно используемое представление двух строк, пока одна из этих строк не будет изменена. Не пытайтесь одновременно с этим иметь операцию выделения подстроки, которая может использоваться в левой части.
(*4) Разработайте класс string с семантикой по значению, копированием с задержкой и операцией подстроки, которая может стоять в левой части.
(*2) Какие преобразования используются в каждом выражении следующей программы:
struct X { int i; X(int); operator+(int); };
struct Y { int i; Y(X); operator+(X); operator int(); };
X operator* (X,Y); int f(X);
X x = 1; Y y = x; int i = 2;
main() { i + 10; y + 10; y + 10 * y; x + y + i; x * x + i; f(7); f(y); y + y; 106 + y; }
Определите X и Y так, чтобы они оба были целыми типами. Измените программу так, чтобы она работала и печатала значения всех допустимых выражений.
(*2) Определите класс INT, который ведет себя в точности как int. Подсказка: определите INT::operator int().
(*1) Определите класс RINT, который ведет себя в точности как int за исключением того, что единственные возможные операции - это + (унарный и бинарный), - (унарный и бинарный), *, /, %. Подсказка: не определяйте $ (R?)INT::operator int().


(*1) Определите
class base { public: virtual void iam() { cout
Выведите из base два класса и для каждого определите iam() ("я есть"), которая выводит имя класса на печать. Создайте объекты этих классов и вызовите для них iam(). Присвойте адреса объектов производных классов указателям base* и вызовите iam() через эти указатели.
(*2) Реализуйте примитивы экрана () подходящим для вашей системы образом.
(*2) Определите класс triangle (треугольник) и класс circle (круг).
(*2) Определите функцию, которая рисует линию, соединяющую две фигуры, отыскивая две ближайшие "точки соприкосновения" и соединяя их.
(*2) Модифицируйте пример с фигурами так, чтобы line была rectangle и наоборот.
(*2) Придумайте и реализуйте дважды связанный список, который можно использовать без итератора.
(*2) Придумайте и реализуйте дважды связанный список, которым можно пользоваться только посредством итератора. Итератор должен иметь действия для движения вперед и назад, действия для вставления и удаления элементов списка, и способ доступа к текущему элементу.
(*2) Постройте обобщенный вариант дважды связанного списка.
(*4) Сделайте список, в котором вставляются и удаляются сами объекты (а не просто указатели на объекты). Проделайте это для класса X, для которого определены X::X(X), X::~X() X::operator=(X).
(*5) Придумайте и реализуйте библиотеку для написания моделей, управляемых прерываниями. Подсказка: . Только это - старая программа, а вы могли бы написать лучше. Должен быть класс task (- задача). Объект класса task должен мочь сохранять свое состояние и восстанавливаться в это состояние (вы можете определить task::save() и task::restore()), чтобы он мог действовать как сопрограмма. Отдельные задачи можно определять как объекты классов, производных от класса task. Программа, которую должна исполнять задача, модет задаваться как виртуальная функция. Должна быть возможность передавать новой задаче ее параметры как параметры ее конструктора(ов). Там должен быть планировщик, реализующий концепцию виртуального времени. Обеспечьте функцию задержки task::delay(), которая "тратит" виртуальное время. Будет ли планировщик отдельным или частью класса task - это один из основных вопросов, которые надо решить при проектировании. Задача должна передавать данные. Для этого разработайте класс queue (очередь). Придумайте способ, чтобы задача ожидала ввода из нескольких очередей. Ошибки в ходе выполнения обрабатывайте единообразно. Как бы вы отлаживали программы, написанные с помощью такой библиотеки?
*1 К сожалению, об этом присваивании легко забыть. Например, в первом издании этой книги (английском - перев.) вторая строка конструктор derived::derived() читалась так:
if (this == 0) this = (derived*)43;
И следовательно, для d конструктор базового класса base::base() не вызывался. Программа была допустимой и корректно выполнялась, но очевидно делала не то, что подразумевал автор. (прим. автора)
[] [] []
Copyright © CIT


(*1.5) Считайте файл чисел с плавающей точкой, составьте из пар считанных чисел комплексные числа и выведите комплексные числа.
(*1.5) Определите тип name_and_address (имя_и_адрес). Определите для него . Скопируйте поток объектов name_and_address.
(*2) Постройте несколько функций для запроса и чтения различного вида информации. Простейший пример - функция y_or_n() в Идеи: целое, число с плавающей точкой, имя файла, почтовый адрес, дата, личные данные и т.д. Постарайтесь сделать их защищенными от дурака.
(*1.5) Напишите программу, которая печатает (1) все буквы в нижнем регистре, (2) все буквы, (3) все буквы и цифры, (4) все символы, которые могут встречаться в идентификаторах C++ на вашей системе, (5) все символы пунктуации, (6) целые значения всех управляющих символов, (7) все символы пропуска, (8) целые значения всех символов пропуска, и (9) все печатаемые символы.
(*4) Реализуйте стандартную библиотеку ввода/вывода C () с помощью стандартной библиотеки ввода/вывода C++ ().
(*4) Реализуйте стандартную библиотеку ввода/вывода C++ () с помощью стандартной библиотеки ввода/вывода C ().
(*4) Реализуйте стандартные библиотеки C и C++ так, чтобы они могли использоваться одновременно.
(*2) Реализуйте класс, для которого [] перегружено для реализации случайного чтения символов из файла.
(*3) Как Упражнение 8, только сделайте, чтобы [] работало и для чтения, и для записи. Подсказка: сделайте, чтобы [] возвращало объект "дескрипторного типа", для которого присваивание означало бы присвоить файлу через дескриптор, а неявное преобразование в char означало бы чтение из файла через дескриптор.
(*2) Как Упражнение 9, только разрешите [] индексировать записи некоторого вида, а не символы.
(*3) Сделайте обобщенный вариант класса, определенного в Упражнении 10.
(*3.5) Разработайте и реализуйте операцию ввода по сопоставлению с образцом. Для спецификации образца используйте строки формата в духе printf. Должна быть возможность попробовать сопоставить со вводом несколько образцов для нахождения фактического формата. Можно было бы вывести класс ввода по образцу из istream.
(*4) Придумайте (и реализуйте) вид образцов, которые намного лучше.
[] [] []

Условная компиляция


Командная строка компилятора вида

#if выражение

проверяет, является ли результатом вычисления выражения не-ноль. Выражение должно быть константным выражением, которые обсуждаются в #15; применительно к использованию данной директивы есть дополнительные ограничения: константное выражение не может содержать sizeof или перечислимые константы. Кроме обычных операций C может использоваться унарная операция defined. В случае применения к идентификатору она дает значение не-ноль, если этот идентификатор был ранее определен с помощью #define и после этого не было отмены определения с помощью #undef; иначе ее значение 0.

Командная строка вида

#ifdef идент

проверяет, определен ли идентификатор в препроцессоре в данный момент; то есть, был ли он объектом командной строки #define.

Командная строка вида

#ifndef идент

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

После строки каждого из трех видов может стоять произвольное количество строк, возможно, содержащих командную строку

#else

и далее до командной строки

#endif

Если проверенное условие истинно, то все строки между #else и #endif игнорируются. Если проверенное условие ложно, то все строки между проверкой и #else или, в случае отсутствия #else, #endif, игнорируются.

Эти конструкции могут быть вложенными.



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


условное_выражение:

выражение ? выражение : выражение


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



Условный оператор


Есть два вида условных операторов

if ( выражение ) оператор if ( выражение ) оператор else оператор

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



Уточнение*1 Имени Члена


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

class x { int m; public: int readm() { return x::m; } void setm(int m) { x::m = m; } };

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

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

class my_file { // ... public: int open(char*, char*); };

int my_file::open(char* name, char* spec) { // ... if (::open(name,flag)) { // использовать open() из UNIX(2) // ... } // ... }



Увеличение и уменьшение


Операция ++ используется для явного выражения приращения вместо его неявного выражения с помощью комбинации сложения и присваивания. По определению ++lvalue означает lvalue+=1, что в свою очередь означает lvalue=lvalue+1 при условии, что lvalue не вызывает никаких побочных эффектов. Выражение, обозначающее (денотирующее) объект, который должен быть увеличен, вычисляется один раз (только). Аналогично, уменьшение выражается операцией --. Операции ++ и -- могут применяться и как префиксные, и как постфиксные. Значением ++x является новое (то есть увеличенное) значение x. Например, y=++x эквивалентно y=(x+=1). Значение x++, напротив, есть старое значение x. Например, y=x++ эквивалентно y=(t=x,x+=1,t), где t - переменная того же типа, что и x.

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

inline void cpy(char* p, const char* q) { while (*p++ = *q++) ; }

Напомню, что увеличение и уменьшение указателей, так же как сложение и вычитание указателей, осуществляется в терминах элементов вектора, на которые указывает указатель; p++ приводит к тому, что p указывает на следующий элемент. Для указателя p типа T* по определению выполняется следующее:

long(p+1) == long(p)+sizeof(T);


Операнд префиксного ++ получает приращение. Операнд должен быть адресным . Значением является новое значение операнда, но оно не адресное. Выражение ++x эквивалентно x+=1. По поводу данных о преобразованиях см. обсуждение операций сложения () и присваивания ().

Операнд префиксного -- уменьшается аналогично действию префиксной операции ++.

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

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



Вектора


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

class vector { int* v; int sz; public: vector(int); // конструктор ~vector(); // деструктор int size() { return sz; } void set_size(int); int operator[](int); int elem(int i) { return v[i]; } };

Функция size возвращает число элементов вектора, таким образом индексы должны лежать в диапазоне 0 ... size()-1. Функция set_size сделана для изменения этого размера, elem обеспечивает доступ к элементам без проверки индекса, а operator[] дает доступ с проверкой границ.

Идея состоит в том, чтобы класс сам был структурой фиксированного размера, управляющей доступом к фактической памяти вектора, которая выделяется конструктором вектора с помощью распределителя свободной памяти new:

vector::vector(int s) { if (s

Теперь вы можете описывать вектора типа vector почти столь же элегантно, как и вектора, встроенные в сам язык:

vector v1(100); vector v2(nelem*2-4);

Операцию доступа можно определить как

int vector::operator[](int i) { if(i

Операция (ИЛИИЛИ) - это логическая операция ИЛИ. Ее правый операнд вычисляется только тогда, когда это необходимо, то есть если вычисление левого операнда дало ноль. Возвращение ссылки обеспечивает то, что запись [] может использоваться с любой стороны операции присваивания:

v1[x] = v2[y];

Функция со странным именем ~vector - это деструктор, то есть функция, описанная для того, чтобы она неявно вызывалась, когда объект класса выходит из области видимости. Деструктор класса C имеет имя ~C. Если его определить как

vector::~vector() { delete v; }

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


Для типа T T[size] является типом "вектор из size элементов типа T". Элементы индексируются (нумеруются) от 0 до size-1. Например:

float v[3]; // вектор из трех float: v[0], v[1], v[2] int a[2][5]; // два вектора из пяти int char* vpc; // вектор из 32 указателей на символ

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

extern int strlen(char*);

char alpha[] = "abcdefghijklmnoprstuvwxyz";

main() { int sz = strlen(alpha);

for (int i=0; i.

Функция strlen() использовалась для подсчета числа символов в alpha; вместо этого можно было использовать значение размера alpha ( #2.4.4). Если применяется набор символов ASCII, то выдача выглядит так:

'a' = 97 = 0141 = 0x61 'b' = 98 = 0142 = 0x62 'c' = 99 = 0143 = 0x63 ...

Заметим, что задавать размер вектора alpha необязательно; компилятор считает число символов в символьной строке, указанной в качестве инициализатора. Использование строки как инициализатора для вектора символов - удобное, но к сожалению и единственное применение строк. Аналогичное этому присваивание строки вектору отсутствует. Например:

char v[9]; v = "строка"; // ошибка

ошибочно, поскольку присваивание не определено для векторов.

Конечно, для инициализации символьных массивов подходят не только строки. Для остальных типов нужно применять более сложную запись. Эту запись можно использовать и для символьных векторов. Например:

int v1[] = { 1, 2, 3, 4 }; int v2[] = { 'a', 'b', 'c', 'd' };

char v3[] = { 1, 2, 3, 4 }; char v4[] = { 'a', 'b', 'c', 'd' };

Заметьте, что v4 - вектор из четырех (а не пяти) символов; он не оканчивается нулем, как того требуют соглашение и библиотечные подпрограммы. Обычно применение такой записи ограничивается статическими объектами.

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

int bad[5,2]; // ошибка

и так:

int v[5][2]; int bad = v[4,1]; // ошибка int good = v[4][1]; // ошибка

Описание

char v[2][5];

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

char v[2][5] = { 'a', 'b', 'c', 'd', 'e', '0', '1', '2', '3', '4' }

main() { for (int i = 0; i

это дает в результате

v[0][0]=a v[0][1]=b v[0][2]=c v[0][3]=d v[0][4]=e v[1][0]=0 v[1][1]=1 v[1][2]=2 v[1][3]=3 v[1][4]=4



Вектора Объектов Класса


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

table tblvec[10];

будет ошибкой, так как для table::table() требуется целый параметр. Нет способа задать параметры конструктора в описании вектора. Чтобы можно было описывать вектор таблиц table, можно модифицировать описание table () например так:

class table { // ... void init(int sz); // как старый конструктор public: table(int sz) // как раньше, но без по умолчанию { init(sz); } table() // по умолчанию { init(15); } }

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

void f() { table* t1 = new table; table* t2 = new table[10]; delete t1; // одна таблица delete t2; // неприятность: 10 таблиц }

В этом случае длину вектора должен задавать программист:

void g(int sz) { table* t1 = new table; table* t2 = new table[sz]; delete t1; delete[] t2; }

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



Векторные Параметры


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

int strlen(const char*);

void f() { char v[] = "a vector" strlen(v); strlen("Nicholas"); };

Иначе говоря, при передаче как параметр типа T[] преобразуется к T*. Следовательно, присваивание элементу векторного параметра изменяет значение элемента вектора, который является параметром. Другими словами, вектор отличается от всех остальных типов тем, что вектор не передается (и не может передаваться) по значению.

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

void compute1(int* vec_ptr, int vec_size); // один способ

struct vec { // другой способ int* ptr; int size; };

void compute2(vec v);

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

char* day[] = { "mon", "tue", "wed", "thu", "fri", "sat", "sun" };

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

void print_m34(int m[3][4]) { for (int i = 0; i

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

Первая размерность массива не имеет отношения к задаче отыскания положения элемента (#2.3.6). Поэтому ее можно передавать как параметр:

void print_mi4(int m[][4], int dim1) { for (int i = 0; i



Видимость


Класс employee стал открытым (public) базовым классом класса manager в результате описания:

class manager : public employee { // ... };

Это означает, что открытый член класса employee является также и открытым членом класса manager. Например:

void clear(manager* p) { p-next = 0; }

будет компилироваться, так как next - открытый член и employee и manager'а. Альтернатива - можно определить закрытый (private) класс, просто опустив в описании класса слово public:

class manager : employee { // ... };

Это означает, что открытый член класса employee является закрытым членом класса manager. То есть, функции члены класса manager могут как и раньше использовать открытые члены класса employee, но для пользователей класса manager эти члены недоступны. В частности, при таком описании класса manager функция clear() компилироваться не будет. Друзья производного класса имеют к членам базового класса такой же доступ, как и функции члены.

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

Когда описывается производная struct, ее базовый класс по умолчанию является public базовым классом. То есть,

struct D : B { ...

означает

class D : public B { public: ...

Отсюда следует, что если вы не сочли полезным то скрытие данных, которое дают class, public и friend, вы можете просто не использовать эти ключевые слова и придерживаться struct. Такие средства языка, как функции члены, конструкторы и перегрузка операций, не зависят от механизма скрытия данных.

Можно также объявить некоторые, но не все, открытые $ члены базового класса открытыми членами производного класса. Например:

class manager : employee { // ... public: // ... employee::name; employee::department; };

Запись

имя_класса :: имя_члена ;

не вводит новый член, а просто делает открытый член базового класса открытым для производного класса. Теперь name и department могут использоваться для manager'а, а salary и age - нет. Естественно, сделать сделать закрытый член базового класса открытым членом производного класса невозможно. Невозможно с помощью этой записи также сделать открытыми перегруженные имена.

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



Видимость имен членов


Члены класса, описанные с ключевым словом class, являются закрытыми, это значит, что их имена могут использоваться только функциями членами () и друзьями (см. ), пока они не появятся после метки public: . В этом случае они являются общими. Общий член может использоваться любой функцией. Структура является классом, все члены которого общие; см. #8.5.11.

Если перед именем базового класса в описании производного класса стоит ключевое слово public, то общие члены базового класса являются общими для производного класса; если нет, то они являются закрытыми. Общий член mem закрытого базового класса base может быть описан как общий для производного класса с помощью описания вида

typedef-имя . идентификатор;

в котором typedef-имя означает базовый класс, а идентификатор есть имя члена базового класса. Такое описание может появляться в общей части производного класса.

Рассмотрим

class base { int a; public: int b,c; int bf (); };

class derived : base { int d; public: base.c; int e; int df (); };

int ef (derived);

Внешняя функция ef может использовать только имена c, e и df. Являясь членом derived, функция df может использовать имена b, c, bf, d, e и df, но не a. Являясь членом base, функция bf может использовать члены a, b, c и bf.



Виртуальная Функция Вывода


Иногда функция вывода должна быть virtual. Рассмотрим пример класса shape, который дает понятие фигуры ():

class shape { // ... public: // ... virtual void draw(ostream s); // рисует "this" на "s" };

class circle : public shape { int radius; public: // ... void draw(ostream); };

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

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

ostream operatordraw(s); return s; }

Если next - итератор типа определенного в #7.3.3, то список фигур распечатывается например так:

while ( p = next() ) cout



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


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

class shape { point center; color col; //... public: void move(point to) { center=to; draw(); } point where() { return center; } virual void draw(); virtual void rotate(int); //... };

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

class circle: public shape { int radius; public: void draw(); void rotatte(int i) {} //... };

Теперь, если shape_vec - вектор фигур, то можно написать:

for (int i = 0; i

*1

Программирующим на C

*2 одномерный массив. Это принятый термин (например, вектора прерываний), и мы сочли, что стандартный перевод его как "массив" затуманит изложение. (прим. перев.)

*3 англ. dereference - получить значение объекта, на который указывает данный указатель. (прим. перев.)

*4 в оригинале expression-oriented (expression - выразительность и выражение). (прим. перев.)

[] [] []


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

struct employee { employee* next; char* name; short department; // ... virtual void print(); };

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

void employee::print() { cout name department

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

struct manager : employee { employee* group; short level; // ... void print(); };

void manager::print() { employee::print(); cout

Функция print_employee() теперь не нужна, поскольку ее место заняли функции члены print(), и теперь со списком служащих можно работать так:

void f(employee* ll) { for (; ll; ll=ll-next) ll-print(); }

Каждый служащий будет печататься в соответствии с его типом. Например:

main() { employee e; e.name = "Дж.Браун"; e.department = 1234; e.next = 0; manager m; m.name = "Дж.Смит"; e.department = 1234; m.level = 2; m.next = e f(m); }

выдаст

Дж.Смит 1234 уровень 2 Дж.Браун 1234

Заметьте, что это будет работать даже в том случае, если f() была написана и откомпилирована еще до того, как производный класс manager был задуман! Очевидно, при реализации этого в каждом объекте класса employee сохраняется некоторая информация о типе. Занимаемого для этого пространства (в текущей реализации) как раз хватает для хранения указателя. Это пространство занимается только в объектах классов с виртуальными функциями, а не во всех объектах классов и даже не во всех объектах производных классов. Вы платите эту пошлину только за те классы, для которых описали виртуальные функции.

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




Если базовый класс base содержит (виртуальную) virtual (#8.1) функцию vf, а производный класс derived также содержит функцию vf, то вызов vf для объекта класса derived вызывает derived::vf. Например:

struct base { virtual void vf (); void f (); };

struct derived : public base { void vf (); void f (); };

derived d; base* bp = d

bp-vf (); bp-f ();

Вызовы вызывают, соответственно, derived::vf и base::f для объекта класса derived, именованного d. Так что интерпретация вызова виртуальной функции зависит от типа объекта, для которого она вызвана, в то время как интерпретация вызова невиртуальной функции зависит только от типа указателя, обозначающего объект.

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

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



Включение файлов


Командная строка компилятора вида

#include "имя_файла"

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

Альтернативный вариант, командная строка вида

#include

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

Включения с помощью #include могут быть вложенными.



Вложенные Классы


Описание класса может быть вложенным. Например:

class set { struct setmem { int mem; setmem* next; setmem(int m, setmem* n) { mem=m; next=n; } }; setmem* first; public: set() { first=0; } insert(int m) { first = new setmem(m,first);} // ... };

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

class set { struct setmem { int mem; setmem* next; setmem(int m, setmem* n) }; // ... };

setmem::setmem(int m, setmem* n) { mem=m, next=n} setmem m1(1,0);

Такая запись, как set::setmem::setmem(), не является ни необходимой, ни допустимой. Единственный способ скрыть имя класса - это сделать это с помощью метода файлы-как-модули. Большую часть нетривиальных классов лучше описывать раздельно:

class setmem { friend class set; // доступ только с помощью членов set int mem; setmem* next; setmem(int m, setmem* n) { mem=m; next=n; } };

class set { setmem* first; public: set() { first=0; } insert(int m) { first = new setmem(m,first);} // ... };


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

int x;

class enclose /* охватывающий */ { int x; class inner { int y; f () { x=1 } ... }; g (inner*); ... };

int inner; /* вложенный */

enclose.g (inner* p) { ... }

В этом примере x в f ссылается на x, описанный перед классом enclose. Поскольку y является закрытым членом inner, g не может его использовать. Поскольку g является членом enclose, имена, использованные в g, считаются находящимися в области видимости класса enclose. Поэтому inner в описании параметров g относится к охваченному типу inner, а не к int.



Внешние Определения


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


программа: внешнее_определение внешнее_определение программа внешнее_определение: определение_функции описание определение_функции: спецификаторы_описания opt описатель_функции инициализатор_базового_класса opt тело_функции описатель_функции: описатель ( список_описаний_параметров) тело_функции: составной_оператор инициализатор_базового_класса: : ( список_параметров opt )



Возврат Значения


Из функции, которая не описана как void, можно (и должно) возвращать значение. Возвращаемое значение задается оператором return. Например:

int fac(int n) {return (n1) ? n*fac(n-1) : 1; }

В функции может быть больше одного оператора return:

int fac(int n) { if (n 1) return n*fac(n-1); else return 1; }

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

double f() { // ... return 1; // неявно преобразуется к double(1) }

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

int* f() { int local = 1; // ... return local // так не делайте }

Эта ошибка менее обычна, чем эквивалентная ошибка при использовании ссылок:

int f() { int local = 1; // ... return local; // так не делайте }

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

int f() { return 1;} // так не делайте



Время Жизни


Если программист не указал иного, то объект создается, когда встречается его описание, и уничтожается, когда его имя выходит из области видимости, Объекты с глобальными именами создаются и инициализируются один раз (только) и "живут" до завершения программы. Объекты, определенные описанием с ключевым словом static, ведут себя так же. Например *1:

int a = 1;

void f() { int b = 1; // инициализируется при каждом вызове f() static int c = 1; // инициализируется только один раз cout

производит вывод

a = 1 b = 1 c = 1 a = 2 b = 1 c = 2 a = 3 b = 1 c = 3

Не инициализированная явно статическая (static) переменная неявно инициализируется нулем.

С помощью операций new и delete программист может также создавать объекты, время жизни которых управляется непосредственно; см.

#3.2.4.



Это турне проведет вас через




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


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

Рассмотрим пример с калькулятором. Он был представлен в виде одного исходного файла. Если вы его набили, то у вас наверняка были небольшие трудности с расположением описаний в правильном порядке, и пришлось использовать по меньшей мере одно "фальшивое" описание, чтобы компилятор смог обработать взаимно рекурсивные функции expr(), term() и prim(). В тексте уже отмечалось, что программа состоит из четырех частей (лексического анализатора, программы синтаксического разбора, таблицы имен и драйвера), но это никак не было отражено в тексте самой программы. По сути дела, калькулятор был написан по-другому. Так это не делается; даже если в этой программе "на выброс" пренебречь всеми соображениями методологии программирования, эксплуатации и эффективности компиляции, автор все равно разобьет эту программу в 200 строк на несколько файлов, чтобы программировать было приятнее.

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

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


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

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

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


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

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

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

Обработка и встроенных и определенных пользователем типов однородным образом и с гарантией типа достигается с помощью одного перегруженного имени функции для набора функций вывода. Например:
put(cerr,"x = "); // cerr - поток вывода ошибок put(cerr,x); put(cerr,"\n");
Тип параметра определяет то, какая из функций put будет вызываться для каждого параметра. Это решение применялось в нескольких языках. Однако ему недостает лаконичности. Перегрузка операции cerr

где cerr - стандартный поток вывода ошибок. Поэтому, если x является int со значением 123, то этот оператор напечатает в стандартный поток вывода ошибок
x = 123
и символ новой строки. Аналогично, если X принадлежит определенному пользователем типу complex и имеет значение (1,2.4), то приведенный выше оператор напечатает в cerr
x = 1,2.4)
Этот метод можно применять всегда, когда для x определена операция


Язык программирования C++ - это C, расширенный введением классов, inline-функций, перегруженных операций, перегруженных имен функций, константных типов, ссылок, операций управления свободной памятью, проверки параметров функций. Коротко различия между С++ и "старым С" приведены в #15. В этом руководстве описывается язык по состоянию на Июнь 1985.

Ввод


Следующая (довольно многословная) программа предлагает вам ввести число дюймов. После того, как вы это сделаете, она напечатает соответствующее число сантиметров.

#include

main() { int inch = 0; // inch - дюйм cout inch; cout

Первая строка функции main() описывает целую переменную inch. Ее значение считывается с помощью операции ("взять из") над стандартным потоком ввода cin. Описания cin и , конечно же, находятся в . После ее запуска ваш терминал может выглядеть примерно так:

$ a.out inches=12 12 in = 30.48 cm $

В этом примере на каждую команду вывода приходится один оператор; это слишком длинно. Операцию вывода

cout

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


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



Ввод Типов, Определяемых Пользователем


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

istream operator(istream s, complex a) /* форматы ввода для complex; "f" обозначает float: f ( f ) ( f , f ) */ { double re = 0, im = 0; char c = 0;

s c; if (c == '(') { s re c; if (c == ',') s im c; if (c != ')') s.clear(_bad); // установить state } else { s.putback(c); s re; }

if (s) a = complex(re,im); return s; }

Несмотря на то, что не хватает кода обработки ошибок, большую часть видов ошибок это на самом деле обрабатывать будет. Локальная переменная c инициализируется, чтобы ее значение не оказалось случайно '(' после того, как операция окончится неудачно. Завершающая проверка состояния потока гарантирует, что значение параметра a будет изменяться только в том случае, если все идет хорошо.

Операция установки состояния названа clear() (очистить), потому что она чаще всего используется для установки состояния потока заново как _good. _good является значением параметра по умолчанию и для istream::clear(), и для ostream::clear().

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



Ввод Встроенных Типов


Класс istream определяется так:

class istream { // ... public: istream operator(char*); // строка istream operator(char); // символ istream operator(short); istream operator(int); istream operator(long); istream operator(float); istream operator(double); // ... };

Функции ввода определяются в таком духе:

istream istream::operator(char c); { // пропускает пропуски int a; // неким образом читает символ в "a" c = a; }

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

В качестве альтернативы можно использовать функции get():

class istream { // ... istream get(char c); // char istream get(char* p, int n, int ='\n'); // строка };

Они обрабатывают символы пропуска так же, как остальные символы. Функция istream::get(char) читает один и тот же символ в свой параметр; другая istream::get читает не более n символов в вектор символов, начинающийся в p. Необязательный третий параметр используется для задания символа остановки (иначе, терминатора или ограничителя), то есть этот символ читаться не будет. Если будет встречен символ ограничитель, он останется как первый символ потока. По умолчанию вторая функция get будет читать самое большее n символов, но не больше чем одну строку, '\n' является ограничителем по умолчанию. Необязательный третий параметр задает символ, который читаться не будет. Например:

cin.get(buf,256,'\t');

будет читать в buf не более 256 символов, а если встретится табуляция ('\t'), то это приведет к возврату из get. В этом случае следующим символом, который будет считан из cin, будет '\t'.

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

int isalpha(char) // 'a'..'z' 'A'..'Z' int isupper(char) // 'A'..'Z' int islower(char) // 'a'..'z' int isdigit(char) // '0'..'9' int isxdigit(char) // '0'..'9' 'a'..'f' 'A'..'F' int isspase(char) // ' ' '\t' возврат новая строка // перевод формата int iscntrl(char) // управляющий символ // (ASCII 0..31 и 127) int ispunct(char) // пунктуация: ниодин из вышеперечисленных int isalnum(char) // isalpha() | isdigit() int isprint(char) // печатаемый: ascii ' '..'-' int isgraph(char) // isalpha() | isdigit() | ispunct() int isascii(char c) { return 0

Все кроме isascii() реализуются внешне одинаково, с применением символа в качестве индекса в таблице атрибутов символов. Поэтому такие выражения, как

(('a'

не только утомительно пишутся и чреваты ошибками (на машине с набором символов EBCDIC оно будет принимать неалфавитные символы), они также и менее эффективны, чем применение стандартной функции:

isalpha(c)



Выражения


В C++ имеется большое число операций, и они будут объясняться там, где (и если) это потребуется. Следует учесть, что операции

~ (дополнение)
(И)
^ (исключающее ИЛИ)
| (включающее ИЛИ)
(логический сдвиг влево)
(логический сдвиг вправо)

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

Смысл операции зависит от числа операндов; унарное является операцией взятия адреса, а бинарное - это операция логического И. Смысл операции зависит также от типа ее операндов: + в выражении a+b означает сложение с плавающей точкой, если операнды имеют тип float, но целое сложение, если они типа int. В #1.8 объясняется, как можно определить операцию для типа, определяемого пользователем, без потери ее значения, предопределенного для основных и производных типов.

В C++ есть операция присваивания =, а не оператор присваивания, как в некоторых языках. Таким образом, присваивание может встречаться в неожиданном контексте; например, x=sqrt(a=3*x). Это бывает полезно. a=b=c означает присвоение c объекту b, а затем объекту a. Другим свойством операции присваивания является то, что она может совмещаться с большинством бинарных операций. Например, x[i+3]*=4 означает x[i+3]=x[i+3]*4, за исключением того факта, что выражение x[i+3] вычисляется только один раз. Это дает привлекательную степень эффективности без необходимости обращения к оптимизирующим компиляторам. К тому же это более кратко.

В большинстве программ на C++ широко применяются указатели. Унарная операция * разыменовывает*3 указатель, т.е. *p есть объект, на который указывает p. Эта операция также называется косвенной адресацией. Например, если имеется char* p, то *p есть символ, на который указывает p. Часто при работе с указателями бывают полезны операция увеличения ++ и операция уменьшения --. Предположим, p указывает на элемент вектора v, тогда p++ делает p указывающим на следующий элемент.


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

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

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

Кроме стандартного значения, описанного в , операции могут быть перегружены*3, то есть, могут быть заданы их значения для случая их применения к типам, определяемым пользователем; см.




выражение: терм выражение бинарная_операция выражение выражение ? выражение : выражение список_выражений терм: первичный * терм терм - терм ! терм ~ терм ++терм --терм терм++ терм-- ( имя_типа) выражение имя_простого_типа ( список_выражений) sizeof выражение sizeof ( имя_типа ) new имя_типа new ( имя_типа ) первичный: id :: идентификатор константа строка this ( выражение ) первичный[ выражение ] первичный ( список_выражений opt ) первичный.id первичный-id id: идентификатор typedef-имя :: идентификатор список_выражений: выражение список_выражений, выражение операция: унарная_операция бинарная_операция специальная_операция Бинарные операции имеют приоритет, убывающий в указанном порядке: бинарная_операция: * / % + -

== != ^ |

= += -= *= /= %= ^= = |= =



Выражения и Операторы


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


С другой стороны, мы не можем игнорировать эффективность

- Джон Бентли

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



Прежде всего, давайте напишем программу,


Прежде всего, давайте напишем программу, выводящую строку выдачи:
#include
main() { cout
Строка #include сообщает компилятору, чтобы он включил стандартные возможности потока ввода и вывода, находящиеся в файле stream.h. Без этих описаний выражение cout
main() { ... }
определяет функцию, названную main. Каждая программа должна содержать функцию с именем main, и работа программы начинается с выполнения этой функции.





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

Вывод Встроенных Типов


Класс ostream определяется вместе с операцией class ostream { // ... public: ostream operator



Вызов Функции


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

Для типа ассоциативного массива assoc мы не определили итератор. Это можно сделать, определив класс assoc_iterator, работа которого состоит в том, чтобы в определенном порядке поставлять элементы из assoc. Итератору нужен доступ к данным, которые хранятся в assoc, поэтому он сделан другом:

class assoc { friend class assoc_iterator; pair* vec; int max; int free; public: assoc(int); int operator[](char*); };

Итератор определяется как

class assoc_iterator{ assoc* cs; // текущий массив assoc int i; // текущий индекс public: assoc_iterator(assoc s) { cs = s i = 0; } pair* operator()() { return (ifree)? cs-vec[i++] : 0; } };

Надо инициализировать assoc_iterator для массива assoc, после чего он будет возвращать указатель на новую pair из этого массива всякий раз, когда его будут активизировать операцией (). По достижении конца массива он возвращает 0:

main() // считает вхождения каждого слова во вводе { const MAX = 256; // больше самого большого слова char buf[MAX]; assoc vec(512); while (cinbuf) vec[buf]++; assoc_iterator next(vec); pair* p; while ( p = next() ) cout name val

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

Конечно, такое применение объектов для представления итераторов никак особенно с перегрузкой операций не связано. Многие любят использовать итераторы с такими операциями, как first(), next() и last() (первый, следующий и последний).



Заголовочные Файлы


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

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

#include "to_be_included"

замещает строку, в которой встретилось #include, содержимым файла "to_be_included". Его содержимым должен быть исходный текст на C++, поскольку дальше его будет читать компилятор. Часто включение обрабатывается отдельной программой, называемой C препроцессором, которую CC вызывает для преобразования исходного файла, который дал программист, в файл без директив включения перед тем, как начать собственно компиляцию. В другом варианте эти директивы обрабатывает интерфейсная система компилятора по мере того, как они встречаются в исходном тексте. Если программист хочет посмотреть на результат директив включения, можно воспользоваться командой

CC -E file.c

для препроцессирования файла file.c точно также, как это сделала бы CC перед запуском собственно компилятора. Для включения файлов из стандартной директории включения вместо кавычек используются угловые скобки . Например:

#include // из стандартной директории включения #define "myheader.h" // из текущей директории

Использование имеет то преимущество, что в программу фактическое имя директории включения не встраивается (как правило, сначала просматривается /usr/include/CC, а потом usr/include). К сожалению, пробелы в директиве include существенны:


#include // не найдет

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

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

В заголовочном файле могут содержаться:

Определения типов struct point { int x, y; }
Описания функций extern int strlen(const char*);
Определения inline-функций inline char get() { return *p++; }
Описания данных extern int a;
Определения констант const float pi = 3.141593
Перечисления enum bool { false, true };
Директивы include #include
Определения макросов #define Case break;case
Комментарии /* проверка на конец файла */
но никогда

Определения обычных функций char get() { return *p++; }
Определения данных int a;
Определения сложных константных объектов const tbl[] = { /* ... */ }
В системе UNIX принято, что заголовочные файлы имеют суффикс (расширение) .h. Файлы, содержащие определение данных или функций, должны иметь суффикс .c. Такие файлы часто называют, соответственно, ".h файлы" и ".c файлы". В #4.7 описываются макросы. Следует заметить, что в C++ макросы гораздо менее полезны, чем в C, поскольку C++ имеет такие языковые конструкции, как const для определения констант и inline для исключения расходов на вызов функции.

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


Законченная Программа


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

[1] Администратор экрана: подпрограммы низкого уровня и структуры данных, определяющие экран; он ведает только точками и прямыми линиями;

[2] Библиотека фигур: набор определений основных фигур вроде прямоугольника и круга и стандартные программы для работы с ними; и

[3] Прикладная программа: множество определений, специализированных для данного приложения, и код, в котором они используются.

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



Законченный Класс


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

Вот пример законченного типа intset, который реализует понятие "множество целых":

class intset { int cursize, maxsize; int *x; public: intset(int m, int n); // самое большее, m int'ов в 1..n ~intset();

int member(int t); // является ли t элементом? void insert(int t); // добавить "t" в множество

void iterate(int i) { i = 0; } int ok(int i) { return i

void error(char* s) { cerr

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

main(int argc, char* argv[]) { if (argc != 3) error("ожидается два параметра"); int count = 0; int m = atoi(argv[1]); // число элементов множества int n = atoi(argv[2]); // в диапазоне 1..n intset s(m,n);

while (count maxsize) error("слищком много элементов"); int i = cursize-1; x[i] = t;

while (i0 x[i-1]x[i]) { int t = x[i]; // переставить x[i] и [i-1] x[i] = x[i-1]; x[i-1] = t; i--; } }

Для нахождения членов используется просто двоичный поиск:

int intset::member(int t) // двоичный поиск { int l = 0; int u = cursize-1;

while (l x[m]) l = m+1; else return 1; // найдено } return 0; // не найдено }

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

Дается три функции: iterate() для инициализации итерации, ok() для проверки, есть ли следующий элемент, и next() для того, чтобы взять следующий элемент:

class intset { // ... void iterate(int i) { i = 0; } int ok(int i) { return iiterate(var); while (set-ok(var)) cout next(var)

Другой способ задать итератор приводится в #6.8.

[] [] []



Закрытие Потоков Вывода


Деструктор для ostream сбрасывает буфер с помощью открытого члена функции ostream::flush():

ostream::~ostream() { flush(); // сброс }

Сбросить буфер можно также и явно. Например:

cout.flush();



Замечания для программистов на C


Чем лучше кто-нибудь знает C, тем труднее окажется избежать писания на C++ в стиле C, теряя, тем самым, некоторые возможные выгоды C++. Поэтому проглядите, пожалуйста, раздел "Отличия от C" в справочном руководстве (#с.15). Там указывается на области, в которых C++ позволяет делать что-то лучше, чем C. Макросы (#define) в C++ почти никогда не бывают необходимы; чтобы определять провозглашаемые константы, используйте const (#2.4.6) или enum (#2.4.7), и inline (#1.12) - чтобы избежать лишних расходов на вызов функции. Старайтесь описывать все функции и типы всех параметров - есть очень мало веских причин этого не делать. Аналогично, практически нет причин описывать локальную переменную не инициализируя ее, поскольку описание может появляться везде, где может стоять оператор, - не описывайте переменную, пока она вам не нужна. Не используйте malloc() - операция new () делает ту же работу лучше. Многие объединения не нуждаются в имени - используйте безымянные объединения ().

# C++ можно купить в ATT, Software Sales and Marketing, PO Box 25000, Greensboro, NC 27420, USA (телефон 800-828-UNIX) или в ваших местных организациях, осуществляющих продажу Системы UNIX. (прим. автора)

(*1) Некоторые легко запоминаемые эмпирические правила, "Правила- помощники." (прим. перев.)

[] []



Замечания по проекту языка


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

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

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

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

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



Замечания по реализации


Во время написания этой книги все реализации C++ использовали версии единственного интерфейсного компилятора #. Он используется на многих архитектурах, включая действующие версии системы операционной системы UNIX на ATT 3B, DEC VAX, IBM 370 и Motorolla 68000. Фрагменты программ, которые приводятся в этой книге, взяты непосредственно из исходных файлов, которые компилировались на 3B в UNIX System V версии 2 [15], VAX11/750 под 8-ой Редакцией UNIX [16] и CCI Power 6/32 под BSD4.2 UNIX [17]. Язык, описанный в этой книге, - это "чистый C++", но имеющиеся на текущий момент компиляторы реализуют большое число "анахронизмов" (описанных в #с.15.3), которые должны способствовать переходу от C к C++.



Замена идентификаторов


Командная строка компилятора имеет вид

#define идент строка_символов

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

Строка вида

#define идент( идент , ..., идент ) строка_символов

где отсутствует пробел между первым идентификатором и (, является макроопределением с параметрами. Последующие вхождения первого идентификатора с идущими за ним (, последовательностью символов, разграниченной запятыми, и ), заменяются строкой символов, заданной в определении. Каждое местоположение идентификатора, замеченного в списке параметров определения, заменяется соответствующей строкой из вызова. Фактическими параметрами вызова являются строки символов, разделенные запятыми; однако запятые в строке, заключенной в кавычки, или в круглых скобках не являются разделителями параметров. Число формальных и фактических параметров должно совпадать. Строки и символьные константы в символьной строке сканируются в поисках формальных параметров, но строки и символьные константы в остальной программе не сканируются в поисках определенных (с помощью define) идентификаторов.

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

Командная строка вида

#undef идент

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



Заметки для читателя


"О многом," -молвил Морж, - "Пришла пора поговорить." Л. Кэррол

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



Запись Синтаксиса


По используемым в данном руководстве синтаксическим правилам записи синтаксические категории выделяются курсивом а литеральные слова и символы шрифтом постоянной ширины*2 . Альтернативные категории записываются на разных строках. Необязательный терминальный или нетерминальный символ обозначается нижним индексом "opt", так что

{ выражение opt }

указывает на необязательность выражения в фигурных скобках. Синтаксис кратко изложен в #14.



Знакомство и краткий обзор


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

Тип есть конкретное представление некоторой концепции (понятия). Например, имеющийся в C++ тип float с его операциями +, -, * и т.д. обеспечивает ограниченную, но конкретную версию математического понятия действительного числа. Новый тип создается для того, чтобы дать специальное и конкретное определение понятия, которому ничто прямо и очевидно среди встроенных типов не отвечает. Например, в программе, которая работает с телефоном, можно было бы создать тип trunk_module (элемент линии), а в программе обработки текстов - тип list_of_paragraphs (список параграфов). Как правило, программу, в которой создаются типы, хорошо отвечающие понятиям приложения, понять легче, чем программу, в которой это не делается. Хорошо выбранные типы, определяемые пользователем, делают программу более четкой и короткой. Это также позволяет компилятору обнаруживать недопустимые использования объектов, которые в противном случае останутся необнаруженными до тестирования программы.

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

Эта глава состоит из четырех практически отдельных частей:

Классы и Члены. Этот раздел знакомит с основным понятием типа, определяемого пользователем, который называется класс (class). Доступ к объектам класса может ограничиваться набором функций, которые описаны как часть этого класса. Такие функции называются функциями членами. Объекты класса создаются и инициализируются функциями членами, специально для этой цели описанными. Эти функции называются конструкторами. Функция член может быть специальным образом описана для "очистки" каждого классового объекта при его уничтожении. Такая функция называется деструктором.


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

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

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