Аддитивные операции
Аддитивные операции + и - группируют слева направо. Выполняются обычные арифметические преобразования. Каждая операция имеет некоторые дополнительные возможности, связанные с типами.
аддитивное_выражение:
выражение + выражение выражение - выражение
Результатом операции + является сумма операндов. Можно суммировать указатель на объект массива и значение целого типа. Последнее во всех случаях преобразуется к смещению адреса с помощью умножения его на длину объекта, на который указывает указатель. Результатом является указатель того же типа, что и исходный указатель, указывающий на другой объект того же массива и соответствующим образом смещенный от первоначального объекта. Так, если P есть указатель на объект массива, то выражение P+1 есть указатель на следующий объект массива.
Никакие другие комбинации типов для указателей не допустимы.
Операция + ассоциативна и выражение с несколькими умножениями на одном уровне может быть реорганизовано компилятором.
Результатом операции - является разность операндов. Выполняются обычные арифметические преобразования. Кроме того, значение любого целого типа может вычитаться из указателя, в этом случае применяются те же преобразования, что и к сложению.
Если вычитаются указатели на объекты одного типа, то результат преобразуется (посредством деления на длину объекта) к целому, представляющему собой число объектов, разделяющих объекты, указанные указателями. В зависимости от машины результирующее целое может быть или типа int, или типа long; см. #2.6. Вообще говоря, это преобразование будет давать неопределенный результат кроме тех случаев, когда указатели указывают на объекты одного массива, поскольку указатели, даже на объекты одинакового типа, не обязательно различаются на величину, кратную длине объекта.
Администратор Экрана
Вначале было намерение написать администратор экрана на C (а не на C++), чтобы подчеркнуть разделение уровней реализации. Это оказалось слишком утомительным, поэтому пришлось пойти на компромисс: используется стиль C (нет функций членов, виртуальных функций, определяемых пользователем операций и т.п.), однако применяются конструкторы, надлежащим образом описываются и проверяются параметры функций и т.д. Оглядываясь назад, можно сказать, что администратор экрана очень похож на C программу, которую потом модифицировали, чтобы воспользоваться средствами C++ не переписывая все полностью.
Экран представляется как двумерный массив символов, работу с которым осуществляют функции put_point() и put_line(), использующие при ссылке на экран структуру point:
// файл screen.h
const XMAX=40, YMAX=24;
struct point { int x,y; point() {} point(int a, int b) { x=a; y=b; } };
overload put_point; extern void put_point(int a, int b); inline void put_point(point p) { put_point(p.x,p.y); }
overload put_line; extern void put_line(int, int, int, int); inline void put_line(point a, point b) { put_line(a.x,a.y,b.x,b.y); }
extern void screen_init(); extern void screen_refresh(); extern void screen_clear();
#include
Перед первым использованием функции put экран надо инициализировать с помощью screen_init(), а изменения в структуре данных экрана отображаются на экране только после вызова screen_refresh(). Как увидит пользователь, это "обновление" ("refresh") осуществляется просто посредством печати новой копии экрана под его предыдущим вариантом. Вот функции и определения данных для экрана:
#include "screen.h" #include
enum color { black='*', white=' ' };
char screen[XMAX][YNAX];
void screen_init() { for (int y=0; y=a a
Предоставляются функции для очистки экрана и его обновления:
void screen_clear() { screen_init(); } // очистка
void screen_refresh() // обновление { for (int y=YMAX-1; 0
Альтернативные Интерфейсы
После того, как описаны средства языка, которые относятся к производным классам, обсуждение снова может вернуться к стоящим задачам. В классах, которые описываются в этом разделе, основополагающая идея состоит в том, что они однажды написаны, а потом их используют программисты, которые не могут изменить их определение. Физически классы состоят из одного или более заголовочных файлов, определяющих интерфейс, и одного или более файлов, определяющих реализацию. Заголовочные файлы будут помещены куда-то туда, откуда пользователь может взять их копии с помощью директивы #include. Файлы, определяющие реализацию, обычно компилируют и помещают в библиотеку.
Альтернативные Реализации
Пока описание открытой части класса и описание функций членов остаются неизменными, реализацию класса можно модифицировать не влияя на ее пользователей. Как пример этого рассмотрим таблицу имен, которая использовалась в настольном калькуляторе в . Это таблица имен:
struct name { char* string; char* next; double value; };
Вот вариант класса table:
// файл table.h
class table { name* tbl; public: table() { tbl = 0; }
name* look(char*, int = 0); name* insert(char* s) { return look(s,1); } };
Эта таблица отличается от той, которая определена в Главе 3 тем, что это настоящий тип. Можно описать более чем одну table, можно иметь указатель на table и т.д. Например:
#include "table.h"
table globals; table keywords; table* locals;
main() { locals = new table; // ... }
Вот реализация table::look(), которая использует линейный поиск в связанном списке имен name в таблице:
#include
name* table::look(char* p, int ins) { for (name* n = tbl; n; n=n-next) 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 = tbl; tbl = nn; return nn; }
Теперь рассмотрим класс table, усовершенствованный таким образом, чтобы использовать хэшированный просмотр, как это делалось в примере с настольным калькулятором. Сделать это труднее из-за того ограничения, что уже написанные программы, в которых использовалась только что определенная версия класса table, должны оставаться верными без изменений:
class table { name** tbl; int size; public: table(int sz = 15); ~table();
name* look(char*, int = 0); name* insert(char* s) { return look(s,1); } };
В структуру данных и конструктор внесены изменения, отражающие необходимость того, что при использовании хэширования таблица должна иметь определенный размер. Задание конструктора с параметром по умолчанию обеспечивает, что старая программа, в которой не указывался размер таблицы, останется правильной. Параметры по умолчанию очень полезны в ситуации, когда нужно изменить класс не повлияв на старые программы. Теперь конструктор и деструктор создают и уничтожают хэш-таблицы:
table::table(int sz) { if (sz string; delete n; } delete tbl; }
Описав деструктор для класса name можно получить более простой и ясный вариант table::~table(). Функция просмотра практически идентична той, которая использовалась в примере настольного калькулятора ():
#include
name* table::look(char* p, int ins) { 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 = tbl[ii]; tbl[ii] = nn; return nn;
}
Очевидно, что функции члены класса должны заново компилироваться всегда, когда вносится какое-либо изменение в описание класса. В идеале такое изменение никак не должно отражаться на пользователях класса. К сожалению, это не так. Для размещения переменной классового типа компилятор должен знать размер объекта класса. Если размер этих объектов меняется, то файлы, в которых класс используется, нужно компилировать заново. Можно написать такую программу (и она уже написана), которая определяет множество (минимальное) файлов, которое необходимо компилировать заново после изменения описания класса, но пока что широкого распространения она не получила.
Почему, можете вы спросить, C++ разработан так, что после изменения закрытой части необходима новая компиляция пользователей класса? И действительно, почему вообще закрытая часть должна быть представлена в описании класса? Другими словами, раз пользователям класса не разрешается обращаться к закрытым членам, почему их описания должны приводиться в заголовочных файлах, которые, как предполагается, пользователь читает? Ответ - эффективность. Во многих системах и процесс компиляции, и последовательность операций, реализующих вызов функции, проще, когда размер автоматических объектов (объектов в стеке) известен во время компиляции.
Этой сложности можно избежать, представив каждый объект класса как указатель на "настоящий" объект. Так как все эти указатели будут иметь одинаковый размер, а размещение "настоящих" объектов можно определить в файле, где доступна закрытая часть, то это может решить проблему. Однако решение подразумевает дополнительные ссылки по памяти при обращении к членам класса, а также, что еще хуже, каждый вызов функции с автоматическим объектом класса включает по меньшей мере один вызов программ выделения и освобождения свободной памяти. Это сделало бы также невозможным реализацию inline-функций членов, которые обращаются к данным закрытой части. Более того, такое изменение сделает невозможным совместную компоновку C и C++ программ (поскольку C компилятор обрабатывает struct не так, как это будет делать C++ компилятор). Для C++ это было сочтено неприемлемым.
Арифметические преобразования
Большое количество операций вызывают преобразования и дают тип результата одинаковым образом. Этот стереотип будет называться "обычным арифметическим преобразованием".
Во-первых, любые операнды типа char, unsigned char или short преобразуются к типу int. | |
Далее, если один из операндов имеет тип double, то другой преобразуется к типу double и тот же тип имеет результат. | |
Иначе, если один из операндов имеет тип unsigned long, то другой преобразуется к типу unsigned long и таков же тип результата. | |
Иначе, если один из операндов имеет тип long, то другой преобразуется к типу long и таков же тип результата. | |
Иначе, если один из операндов имеет тип unsigned, то другой преобразуется к типу unsigned и таков же тип результата. | |
Иначе оба операнда должны иметь тип int и таков же тип результата. |
Библиотека Фигур
Нам нужно определить общее понятие фигуры (shape). Это надо сделать таким образом, чтобы оно использовалось (как базовый класс) всеми конкретными фигурами (например, кругами и квадратами), и так, чтобы любой фигурой можно было манипулировать исключительно через интерфейс, предоставляемый классом shape:
struct shape { shape() { shape_list.append(this); }
virtual point north() { return point(0,0); } // север virtual point south() { return point(0,0); } // юг virtual point east() { return point(0,0); } // восток virtual point neast() { return point(0,0); } // северо-восток virtual point seast() { return point(0,0); } // юго-восток
virtual void draw() {}; // нарисовать virtual void move(int, int) {}; // переместить };
Идея состоит в том, что расположение фигуры задается с помощью move(), и фигура помещается на экран с помощью draw(). Фигуры можно располагать относительно друг друга, используя понятие точки соприкосновения, и эти точки перечисляются после точек на компасе (сторон света). Каждая конкретная фигура определяет свой смысл этих точек, и каждая определяет способ, которым она рисуется. Для экономии места здесь на самом деле определяются только необходимые в этом примере стороны света. Конструктор shape::shape() добавляет фигуру в список фигур shape_list. Этот список является gslist, то есть, одним из вариантов обобщенного односвязанного списка, определенного в Он и соответствующий итератор были сделаны так:
typedef shape* sp; declare(gslist,sp);
typedef gslist(sp) shape_lst; typedef gslist_iterator(sp) sp_iterator;
поэтому shape_list можно описать так:
shape_lst shape_list;
Линию можно построить либо по двум точкам, либо по точке и целому. В последнем случае создается горизонтальная линия, длину которой определяет целое. Знак целого указывает, каким концом является точка: левым или правым. Вот определение:
class line : public shape { /* линия из 'w' в 'e' north() определяется как ``выше центра и на север как до самой северной точки'' */ point w,e; public: point north() { return point((w.x+e.x)/2,e.ydraw(); screen_refresh(); }
И вот, наконец, настоящая сервисная функция (утилита). Она кладет одну фигуру на верх другой, задавая, что south() одной должен быть сразу над north() другой:
void stack(shape* q, shape* p) // ставит p на верх q { point n = p-north(); point s = q-south(); q-move(n.x-s.x,n.y-s.y+1); }
Теперь представим себе, что эта библиотека считается собственностью некоей компании, которая продает программное обеспечение, и что они продают вам только заголовочный файл, содержащий определения фигур, и откомпилированный вариант определений функций. И у вас все равно остается возможность определять новые фигуры и использовать для ваших собственных фигур сервисные функции.
Бинарные и Унарные Операции
Бинарная операция может быть определена или как функция член, получающая один параметр, или как функция друг, получающая два параметра. Таким образом, для любой бинарной операции @ aa@bb может интерпретироваться или как aa.operator@(bb), или как operator@(aa,bb). Если определены обе, то aa@bb является ошибкой. Унарная операция, префиксная или постфиксная, может быть определена или как функция член, не получающая параметров, или как функция друг, получающая один параметр. Таким образом, для любой унарной операции @ aa@ или @aa может интерпретироваться или как aa.operator@(), или как operator@(aa). Если определена и то, и другое, то и aa@ и @aa являются ошибками. Рассмотрим следующие примеры:
class X { // друзья
friend X operator-(X); // унарный минус friend X operator-(X,X); // бинарный минус friend X operator-(); // ошибка: нет операндов friend X operator-(X,X,X); // ошибка: тернарная
// члены (с неявным первым параметром: this)
X* operator(); // унарное (взятие адреса) X operator(X); // бинарное (операция И) X operator(X,X); // ошибка: тернарное
};
Когда операции ++ и -- перегружены, префиксное использование и постфиксное различить невозможно.
Бинарные операции
Бинарная операция может быть определена или с помощью функции члена (см. #8.5.4), получающей один параметр, или с помощью функции друга (см. #8.5.9), получающей два параметра, но не двумя способами одновременно. Так, для любой бинарной операции @, x@y может быть проинтерпретировано как x.операция@(y) или операция@(x,y).
Благодарности
C++ никогда бы не созрел без постоянного использования, предложений и конструктивной критики со стороны многих друзей и коллег. Том Карджил, Джим Коплин, Сту Фельдман, Сэнди Фрэзер, Стив Джонсон, Брайэн Керниган, Барт Локанти, Дуг МакИлрой, Дэннис Риччи, Лэрри Рослер, Джерри Шварц и Джон Шопиро подали важные для развития языка идеи. Дэйв Пресотто написал текущую реализацию библиотеки потоков ввода/вывода.
Кроме того, в развитие C++ внесли свой вклад сотни людей, которые присылали мне предложения по усовершенствованию, описания трудностей, с которыми они сталкивались, и ошибки компилятора. Здесь я могу упомянуть лишь немногих из них: Гэри Бишоп, Эндрю Хьюм, Том Карцес, Виктор Миленкович, Роб Мюррэй, Леони Росс, Брайэн Шмальт и Гарри Уокер.
В издании этой книги мне помогли многие люди, в частности, Джон Бентли, Лаура Ивс, Брайэн Керниган, Тэд Ковальски, Стив Махани, Джон Шопиро и участники семинара по C++, который проводился в Bell Labs, Колумбия, Огайо, 26-27 июня 1985 года. Мюррэй Хилл, Нью Джерси Бьярн Страустрап
Блоки
Блок - это возможно пустой список операторов, заключенный в фигурные скобки:
{ a=b+2; b++; }
Блок позволяет рассматривать несколько операторов как один. Область видимости имени, описанного в блоке, простирается до конца блока. Имя можно сделать невидимым с помощью описаний такого же имени во внутренних блоках.
Большие Объекты
При каждом применении для comlpex бинарных операций, описанных выше, в функцию, которая реализует операцию, как параметр передается копия каждого операнда. Расходы на копирование каждого double заметны, но с ними вполне можно примириться. К сожалению, не все классы имеют небольшое и удобное представление. Чтобы избежать ненужного копирования, можно описать функции таким образом, чтобы они получали ссылочные параметры. Например:
class matrix { double m[4][4]; public: matrix(); friend matrix operator+(matrix, matrix); friend matrix operator*(matrix, matrix); };
Ссылки позволяют использовать выражения, содержащие обычные арифметические операции над большими объектами, без ненужного копирования. Указатели применять нельзя, потому что невозможно для применения к указателю смысл операции переопределить невозможно. Операцию плюс можно определить так:
matrix operator+(matrix, matrix); { matrix sum; for (int i=0; i
Эта operator+() обращается к операндам + через ссылки, но возвращает значение объекта. Возврат ссылки может оказаться более эффективным:
class matrix { // ... friend matrix operator+(matrix, matrix); friend matrix operator*(matrix, matrix); };
Это является допустимым, но приводит к сложности с выделением памяти. Поскольку ссылка на результат будет передаваться из функции как ссылка на возвращаемое значение, оно не может быть автоматической переменной. Поскольку часто операция используется в выражении больше одного раза, результат не может быть и статической переменной. Как правило, его размещают в свободной памяти. Часто копирование возвращаемого значения оказывается дешевле (по времени выполнения, объему кода и объему данных) и проще программируется.
Буферизация
При задании операций ввода/вывода мы никак не касались типов файлов, но ведь не все устройства можно рассматривать одинаково с точки зрения стратегии буферизации. Например, для ostream, подключенного к символьной строке, требуется буферизация другого вида, нежели для ostream, подключенного к файлу. С этими проблемами можно справиться, задавая различные буферные типы для разных потоков в момент инициализации (обратите внимание на три конструктора класса ostream). Есть только один набор операций над этими буферными типами, поэтому в функциях ostream нет кода, их различающего. Однако функции, которые обрабатывают переполнение сверху и снизу, виртуальные. Этого достаточно, чтобы справляться с необходимой в данное время стратегией буферизации. Это также служит хорошим примером применения виртуальных функций для того, чтобы сделать возможной однородную обработку логически эквивалентных средств с различной реализацией. Описание буфера потока в выглядит так:
struct streambuf { // управление буфером потока
char* base; // начало буфера char* pptr; // следующий свободный char char* qptr; // следующий заполненный char char* eptr; // один из концов буфера char alloc; // буфер, выделенный с помощью new
// Опустошает буфер: // Возвращает EOF при ошибке и 0 в случае успеха virtual int overflow(int c =EOF);
// Заполняет буфер // Возвращет EOF при ошибке или конце ввода, // иначе следующий char virtual int underflow();
int snextc() // берет следующий char { return (++qptr==pptr) ? underflow() : *qptr0377 }
// ...
int allocate() // выделяет некоторое пространство буфера
streambuf() { /* ... */} streambuf(char* p, int l) { /* ... */} ~streambuf() { /* ... */} };
Обратите внимание, что здесь определяются указатели, необходимые для работы с буфером, поэтому обычные посимвольные действия можно определить (только один раз) в виде максимально эффективных inline- функций. Для каждой конкретной стратегии буферизации необходимо определять только функции переполнения overflow() и underflow(). Например:
struct filebuf : public streambuf {
int fd; // дескриптор файла char opened; // файл открыт
int overflow(int c =EOF); int underflow();
// ...
// Открывает файл: // если не срабатывает, то возвращает 0, // в случае успеха возвращает "this" filebuf* open(char *name, open_mode om); int close() { /* ... */ }
filebuf() { opened = 0; } filebuf(int nfd) { /* ... */ } filebuf(int nfd, char* p, int l) : (p,l) { /* ... */ } ~filebuf() { close(); } };
int filebuf::underflow() // заполняет буфер из fd { if (!opened allocate()==EOF) return EOF;
int count = read(fd, base, eptr-base); if (count
Целые Константы
Целые константы предстают в четырех обличьях: десятичные, восьмеричные, шестнадцатиричные и символьные константы. Десятичные используются чаще всего и выглядят так, как можно было бы ожидать:
12345678901234567890
Десятичная константа имеет тип int, при условии, что она влезает в int, в противном случае ее тип long. Компилятор должен предупреждать о константах, которые слишком длинны для представления в машине.
Константа, которая начинается нулем за которым идет x (0x), является шестнадцатиричным числом (с основанием 16), а константа, которая начинается нулем за которым идет цифра, является восьмеричным числом (с основанием 8). Вот примеры восьмеричных констант:
23
их десятичные эквиваленты - это 0, 2, 63, 83. В шестнадцатиричной записи эти константы выглядят так:
0x3f 0x53
Буквы a, b, c, d, e и f, или их эквиваленты в верхнем регистре, используются для представления чисел 10, 11. 12, 13, 14 и 15, соответственно. Восьмеричная и шестнадцатиричная записи наиболее полезны для записи набора битов; применение этих записей для выражения обычных чисел может привести к неожиданностям. Например, на машине, где int представляется как двоичное дополнительное шестнадцатеричное целое, 0xffff является отрицательным десятичным числом -1; если бы для представления целого использовалось большее число битов, то оно было бы числом 65535.
Целая константа, состоящая из последовательности цифр, считается восьмиричной, если она начинается с 0 (цифры ноль), и десятичной в противном случае. Цифры 8 и 9 не являются восьмиричными цифрами. Последовательность цифр, которой предшествует 0х или 0Х, воспринимается как шестнадцатеричное целое. В шестнадцатеричные цифры входят буквы от а или А до f или F, имеющие значения от 10 до 15. Десятичная константа, значение которой превышает наибольшее машинное целое со знаком, считается длинной (long); восьмеричная и шестнадцатеричная константа, значение которой превышает наибольшее машинное целое со знаком, считается long; в остальных случаях целые константы считаются int.
Const
Ключевое слово const может добавляться к описанию объекта, чтобы сделать этот объект константой, а не переменной. Например:
const int model = 145; const int v[] = { 1, 2, 3, 4 };
Поскольку константе ничего нельзя присвоить, она должна быть инициализирована. Описание чего-нибудь как const гарантирует, что его значение не изменится в области видимости:
model = 145; // ошибка model++; // ошибка
Заметьте, что const изменяет тип, то есть ограничивает способ использования объекта, вместо того, чтобы задавать способ размещения константы. Поэтому например вполне разумно, а иногда и полезно, описывать функцию как возвращающую const:
const char* peek(int i) { return private[i]; }
Функцию вроде этой можно было бы использовать для того, чтобы давать кому-нибудь читать строку, которая не может быть затерта или переписана (этим кем-то).
С другой стороны, компилятор может несколькими путями воспользоваться тем, что объект является константой (конечно, в зависимости от того, насколько он сообразителен). Самое очевидное - это то, что для константы не требуется выделять память, поскольку компилятор знает ее значение. Кроме того, инициализатор константы часто (но не всегда) является константным выражением, то есть он может быть вычислен на стадии компиляции. Однако для вектора констант обычно приходится выделять память, поскольку компилятор в общем случае не может вычислить, на какие элементы вектора сделаны ссылки в выражениях. Однако на многих машинах даже в этом случае может достигаться повышение эффективности путем размещения векторов констант в память, доступную только для чтения.
Использование указателя вовлекает два объекта: сам указатель и указываемый объект. Снабжение описания указателя "префиксом" const делает объект, но не сам указатель, константой. Например:
const char* pc = "asdf"; // указатель на константу pc[3] = 'a'; // ошибка pc = "ghjk"; // ok
Чтобы описать сам указатель, а не указываемый объект, как константный, используется операция const*. Например:
char *const cp = "asdf"; // константный указатель cp[3] = 'a'; // ok cp = "ghjk"; // ошибка
Чтобы сделать константами оба объекта, их оба нужно описать const. Например:
const char *const cpc = "asdf"; // const указатель на const cpc[3] = 'a'; // ошибка cpc = "ghjk"; // ошибка
Объект, являющийся константой при доступе к нему через один указатель, может быть переменной, когда доступ осуществляется другими путями. Это в частности полезно для параметров функции. Посредством описания параметра указателя как const функции запрещается изменять объект, на который он указывает. Например:
char* strcpy(char* p, const char* q); // не может изменить q
Указателю на константу можно присваивать адрес переменной, поскольку никакого вреда от этого быть не может. Однако нельзя присвоить адрес константы указателю, на который не было наложено ограничение, поскольку это позволило бы изменить значение объекта. Например:
int a = 1; const c = 2; const* p1 = c // ok const* p2 = a // ok int* p3 = c // ошибка *p3 = 7; // меняет значение c
Как обычно, если тип в описании опущен, то он предполагается int.
Деструкторы
Функция член класса cl с именем ~cl называется деструктором. Деструктор не возвращает никакого значения и не получает никаких параметров; он используется для уничтожения значений типа cl непосредственно перед уничтожением содержащего их объекта. Деструктор не может быть overload, virtual или friend.
Деструктор для базового класса выполняется после деструктора производного от него класса. Как деструкторы используются для управления свободной памятью, см. объяснение в #17.
Добавление к Классу
В предыдущих примерах производный класс ничего не добавлял к базовому классу. Для производного класса функции определялись только чтобы обеспечить преобразование типа. Каждый производный класс просто задавал альтернативный интерфейс к общему множеству программ. Этот специальный случай важен, но наиболее обычная причина определения новых классов как производных классов в том, что кто-то хочет иметь то, что предоставляет базовый класс, плюс еще чуть-чуть.
Для производного класса можно определить данные и функции дополнительно к тем, которые наследуются из его базового класса. Это дает альтернативную стратегию обеспечить средства связанного списка. Заметьте, когда в тот slist, который определялся выше, помещается элемент, то создается slink, содержащий два указателя. На их создание тратится время, а ведь без одного из указателей можно обойтись, при условии, что нужно только чтобы объект мог находиться в одном списке. Так что указатель next на следующий можно поместить в сам объект, вместо того, чтобы помещать его в отдельный объект slink. Идея состоит в том, чтобы создать класс olink с единственным полем next, и класс olist, который может обрабатывать указателями на такие звенья olink. Тогда olist сможет манипулировать объектами любого класса, производного от olink. Буква "o" в названиях стоит для того, чтобы напоминать вам, что объект может находиться одновременно только в одном списке olist:
struct olink { olink* next; };
Класс olist очень напоминает класс slist. Отличие состоит в том, что пользователь класса olist манипулирует объектами класса olink непосредственно:
class olist { olink* last; public: void insert(olink* p); void append(olink* p); olink* get(); // ... };
Мы можем вывести из класса olink класс name:
class name : public olink { // ... };
Теперь легко сделать список, который можно использовать без накладных расходов времени на размещение или памяти.
Объекты, помещаемы в olist, теряют свой тип. Это означает, что компилятор знает только то, что они olink'и. Правильный тип можно восстановить с помощью явного преобразования типа объектов, вынутых из olist. Например:
void f() { olist ll; name nn; ll.insert(nn); // тип nn потерян name* pn = (name*)ll.get(); // и восстановлен }
Другой способ: тип можно восстановить, выведя еще один класс из olist для обработки преобразования типа:
class olist : public olist { // ... name* get() { return (name*)olist::get(); } };
Имя name может одновременно находиться только в одном olist. Для имен это может быть и не подходит, но в классах, для которых это подойдет полностью, недостатка нет. Например, класс фигур shape использует для поддержки списка всех фигур именно этот метод. Обратите внимание, что можно было бы определить slist как производный от olist, объединяя таким образом оба понятия. Однако использование базовых и производных классов на таком микроскопическом уровне может очень сильно исказить код.
Договоренности о Лексике
Есть шесть классов лексем: идентификаторы, ключевые слова, константы, строки, операторы и прочие разделители. Символы пробела, табуляции и новой строки, а также комментарии (собирательно - "белые места"), как описано ниже, игнорируются, за исключением тех случаев, когда они служат разделителями лексем. Некое пустое место необходимо для разделения идентификаторов, ключевых слов и констант, которые в противном случае окажутся соприкасающимися.
Если входной поток разобран на лексемы до данного символа, принимается, что следующая лексема содержит наиболее длинную строку символов из тех, что могут составить лексему.
Драйвер
Когда все части программы на месте, нам нужен только драйвер для инициализации и всего того, что связано с запуском. В этом простом примере main() может работать так:
int main() { // вставить предопределенные имена: insert("pi")-value = 3.1415926535897932385; insert("e")-value = 2.7182818284590452354;
while (cin) { get_token(); if (curr_tok == END) break; if (curr_tok == PRINT) continue; cout
Принято обычно, что main() возвращает ноль при нормальном завершении программы и не ноль в противном случае, поэтому это прекрасно может сделать возвращение числа ошибок. В данном случае оказывается, что инициализация нужна только для введения предопределенных имен в таблицу имен.
Основная работа цикла - читать выражения и писать ответ. Это делает строка:
cout
Проверка cin на каждом проходе цикла обеспечивает завершение программы в случае, если с потоком ввода что-то не так, а проверка на END обеспечивает корректный выход из цикла, когда get_token() встречает конец файла. Оператор break осуществляет выход из ближайшего содержащего его оператора switch или цикла (то есть, оператора for, оператора while или оператора do). Проверка на PRINT (то есть, на '\n' или ';') освобождает expr() от обязанности обрабатывать пустые выражения. Оператор continue равносилен переходу к самому концу цикла, поэтому в данном случае
while (cin) { // ... if (curr_tok == PRINT) continue; cout
эквивалентно
while (cin) { // ... if (curr_tok == PRINT) goto end_of_loop; cout
Более подробно циклы описываются в #с.9.
Друзья
Предположим, вы определили два класса, vector и matrix (вектор и матрица). Каждый скрывает свое представление и предоставляет полный набор действий для манипуляции объектами его типа. Теперь определим функцию, умножающую матрицу на вектор. Для простоты допустим, что в векторе четыре элемента, которые индексируются 0...3, и что матрица состоит из четырех векторов, индексированных 0...3. Допустим также, что доступ к элементам вектора осуществляется через функцию elem(), которая осуществляет проверку индекса, и что в matrix имеется аналогичная функция. Один подход состоит в определении глобальной функции multiply() (перемножить) примерно следующим образом:
vector multiply(matrix m, vector v); { vector r; for (int i = 0; i
Это своего рода "естественный" способ, но он очень неэффективен. При каждом обращении к multiply() elem() будет вызываться 4*(1+4*3) раза.
Теперь, если мы сделаем multiply() членом класса vector, мы сможем обойтись без проверки индексов при обращении к элементу вектора, а если мы сделаем multiply() членом класса matrix, то мы сможем обойтись без проверки индексов при обращении к элементу матрицы. Однако членом двух классов функция быть не может. Нам нужно средство языка, предоставляющее функции право доступа к закрытой части класса. Функция не член, получившая право доступа к закрытой части класса, называется другом класса (friend). Функция становится другом класса после описания как friend. Например:
class matrix;
class vector { float v[4]; // ... friend vector multiply(matrix, vector); };
class matrix { vector v[4]; // ... friend vector multiply(matrix, vector); };
Функция друг не имеет никаких особенностей, помимо права доступа к закрытой части класса. В частности, friend функция не имеет указателя this (если только она не является полноправным членом функцией). Описание friend - настоящее описание. Оно вводит имя функции в самой внешней области видимости программы и сопоставляется с другими описаниями этого имени. Описание друга может располагаться или в закрытой, или в открытой части описания класса; где именно, значения не имеет.
Теперь можно написать функцию умножения, которая использует элементы векторов и матрицы непосредственно:
vector multiply(matrix m, vector v); { vector r; for (int i = 0; i
Есть способы преодолеть эту конкретную проблему эффективности не используя аппарат friend (можно было бы определить операцию векторного умножения и определить multiply() с ее помощью). Однако существует много задач, которые проще всего решаются, если есть возможность предоставить доступ к закрытой части класса функции, которая не является членом этого класса. В Главе 6 есть много примеров применения friend. Достоинства функций друзей и членов будут обсуждаться позже.
Функция член одного класса может быть другом другого. Например:
class x { // ... void f(); };
class y { // ... friend void x::f(); };
Нет ничего необычного в том, что все функции члены одного класса являются друзьями другого. Для этого есть даже более краткая запись:
class x { friend class y; // ... };
Такое описание friend делает все функции члены класса y друзьями x.
Друзья (friends)
Функция operator+() не воздействует непосредственно на представление вектора. Действительно, она не может этого делать, поскольку не является членом. Однако иногда желательно дать функциям не членам возможность доступа к закрытой части класса. Например, если бы не было функции "доступа без проверки" vector::elem(), вам пришлось бы проверять индекс i на соответствие границам три раза за каждый проход цикла. Здесь мы избежали этой сложности, но она довольно типична, поэтому у класса есть механизм предоставления права доступа к своей закрытой части функциям не членам. Просто в описание класса помещается описание функции, перед которым стоит ключевое слово friend. Например, если имеется
class Vec; // Vec - имя класса
class vector { friend Vec operator+(Vec, Vec); //... };
То вы можете написать
Vec operator+(Vec a, Vec b) { int s = a.size(); if (s != b.size()) error("плохой размер вектора для +"); Vec sum = *new Vec(s); int* sp = sum.v; int* ap = a.v; int* bp = b.v; while (s--) *sp++ = *ap++ + *bp++; return sum; }
Одним из особенно полезных аспектов механизма friend является то, что функция может быть другом двух и более классов. Чтобы увидеть это, рассмотрим определение vector и matrix, а затем определение функции умножения (см. #с.8.8).
Другом класса является функция не-член, которая может использовать имена закрытых членов. Следующий пример иллюстрирует различия между членами и друзьями:
class private { int a; friend void friend_set (private*,int); public: void member_set (int); };
void friend_set (private* p,int i) { p-a=i; }
void private.member_set (int i) { a = i; }
private obj;
friend_set (obj,10);
obj.member_set (10);
Если описание friend относится к перегруженному имени или операции, то другом становится только функция с описанными типами параметров. Все функции класса cl1 могут быть сделаны друзьями класса cl2 с помощью одного описания
class cl2 { friend cl1; . . . };
Друзья и Члены
Теперь, наконец, можно обсудить, в каких случаях для доступа к закрытой части определяемого пользователем типа использовать члены, а в каких - друзей. Некоторые операции должны быть членами: конструкторы, деструкторы и виртуальные функции (см. ), но обычно это зависит от выбора.
Рассмотрим простой класс X:
class X { // ... X(int); int m(); friend int f(X); };
Внешне не видно никаких причин делать f(X) другом дополнительно к члену X::m() (или наоборот), чтобы реализовать действия над классом X. Однако член X::m() можно вызывать только для "настоящего объекта", в то время как друг f() может вызываться для объекта, созданного с помощью неявного преобразования типа. Например:
void g() { 1.m(); // ошибка f(1); // f(x(1)); }
Поэтому операция, изменяющее состояние объекта, должно быть членом, а не другом. Для определяемых пользователем типов операции, требующие в случае фундаментальных типов операнд lvalue (=, *=, ++ и т.д.), наиболее естественно определяются как члены.
И наоборот, если нужно иметь неявное преобразование для всех операндов операции, то реализующая ее функция должна быть другом, а не членом. Это часто имеет место для функций, которые реализуют операции, не требующие при применении к фундаментальным типам lvalue в качестве операндов (+, -, и т.д.).
Если никакие преобразования типа не определены, то оказывается, что нет никаких существенных оснований в пользу члена, если есть друг, который получает ссылочный параметр, и наоборот. В некоторых случаях программист может предпочитать один синтаксис вызова другому. Например, оказывается, что большинство предпочитает для обращения матрицы m запись m.inv(). Конечно, если inv() действительно обращает матрицу m, а не просто возвращает новую матрицу, обратную m, ей следует быть другом.
При прочих равных условиях выбирайте, чтобы функция была членом: никто не знает, вдруг когда-нибудь кто-то определит операцию преобразования. Невозможно предсказать, потребуют ли будущие изменения изменить статус объекта. Синтаксис вызова функции члена ясно указывает пользователю, что объект можно изменить; ссылочный параметр является далеко не столь очевидным. Кроме того, выражения в члене могут быть заметно короче выражений в друге. В функции друге надо использовать явный параметр, тогда как в члене можно использовать неявный this. Если только не применяется перегрузка, имена членов обычно короче имен друзей.
Друзья и Объединения
В это разделе описываются еще некоторые особенности, касающиеся классов. Показано, как предоставить функции не члену доступ к закрытым членам. Описывается, как разрешать конфликты имен членов, как можно делать вложенные описания классов, и как избежать нежелательной вложенности. Обсуждается также, как объекты класса могут совместно использовать члены данные, и как использовать указатели на члены. Наконец, приводится пример, показывающий, как построить дискриминирующее (экономное) объединение.
Еще об операциях
Другое направление развития - снабдить вектора операциями:
class Vec : public vector { public: Vec(int s) : (s) {} Vec(Vec); ~Vec() {} void operator=(Vec); void operator*=(Vec); void operator*=(int); //... };
Обратите внимание на способ определения конструктора производного класса, Vec::Vec(), когда он передает свой параметр конструктору базового класса vector::vector() и больше не делает ничего. Это полезная парадигма. Операция присваивания перегружена, ее можно определить так:
void Vec::operator=(Vec a) { int s = size(); if (s!=a.size()) error("плохой размер вектора для ="); for (int i = 0; i
void error(char* p) { cerr
Файлы и Потоки
Потоки обычно связаны с файлами. Библиотека потоков создает стандартный поток ввода cin, стандартный поток вывода cout и стандартный поток ошибок cerr. Программист может открывать другие файлы и создавать для них потоки.
Файлы как Модули
В предыдущем разделе .c и .h файлы вместе определяли часть программы. Файл .h является интерфейсом, который используют другие части программы; .c файл задает реализацию. Такой объект часто называют модулем. Доступными делаются только те имена, которые необходимо знать пользователю, остальные скрыты. Это качество часто называют скрытием данных, хотя данные - лишь часть того, что может быть скрыто. Модули такого вида обеспечивают большую гибкость. Например, реализация может состоять из одного или более .c файлов, и в виде .h файлов может быть предоставлено несколько интерфейсов. Информация, которую пользователю знать не обязательно, искусно скрыта в .c файлах. Если важно, что пользователь не должен точно знать, что содержится в .c файлах, не надо делать их доступными в исходом виде. Достаточно эквивалентных им выходных файлов компилятора (.o файлов).
Иногда возникает сложность, состоящая в том, что подобная гибкость достигается без формальной структуры. Сам язык не распознает такой модуль как объект, и у компилятора нет возможности отличить .h файлы, определяющие имена, которые должны использовать другие модули (экспортируемые), от .h файлов, которые описывают имена из других модулей (импортируемые).
В других случаях может возникнуть та проблема, что модуль определяет множество объектов, а не новый тип. Например, модуль table определяет одну таблицу, и если вам нужно две таблицы, то нет простого способа задать вторую таблицу с помощью понятия модуля. Решение этой проблемы приводится в .
Каждый статически размещенный объект по умолчанию инициализируется нулем, программист может задать другие (константные) значения. Это только самый примитивный вид инициализации. К счастью, с помощью классов можно задать код, который выполняется для инициализации перед тем, как модуль каким- либо образом используется, и/или код, который запускается для очистки после последнего использования модуля; см..
Философские замечания
Язык программирования служит двум связанным между собой целям: он дает программисту аппарат для задания действий, которые должны быть выполнены, и формирует концепции, которыми пользуется программист, размышляя о том, что делать. Первой цели идеально отвечает язык, который настолько "близок к машине", что всеми основными машинными аспектами можно легко и просто оперировать достаточно очевидным для программиста образом. С таким умыслом первоначально задумывался C. Второй цели идеально отвечает язык, который настолько "близок к решаемой задаче", чтобы концепции ее решения можно было выражать прямо и коротко. С таким умыслом предварительно задумывались средства, добавленные к C для создания C++.
Связь между языком, на котором мы думаем/программируем, и задачами и решениями, которые мы можем представлять в своем воображении, очень близка. По этой причине ограничивать свойства языка только целями исключения ошибок программиста в лучшем случае опасно. Как и в случае с естественными языками, есть огромная польза быть по крайней мере двуязычным. Язык предоставляет программисту набор концептуальных инструментов; если они не отвечают задаче, то их просто игнорируют. Например, серьезные ограничения концепции указателя заставляют программиста применять вектора и целую арифметику, чтобы реализовать структуры, указатели и т.п. Хорошее проектирование и отсутствие ошибок не может гарантироваться чисто за счет языковых средств.
Система типов должна быть особенно полезна в нетривиальных задачах. Действительно, концепция классов в C++ показала себя мощным концептуальным средством.
Float и double
Для выражений float могут выполняться действия арифметики с плавающей точкой одинарной точности. Преобразования между числами одинарной и двойной точности выполняются настолько математически корректно, насколько позволяет аппаратура.
Форматированный Вывод
Пока char* oct(long, int =0); // восьмеричное представление char* dec(long, int =0); // десятичное представление char* hex(long, int =0); // шестнадцатиричное представление char* chr(int, int =0); // символ char* str(char*, int =0); // строка
Если не задано поле нулевой длины, то будет производиться усечение или дополнение; иначе будет использоваться столько символов (ровно), сколько нужно. Например:
cout
Если x==15, то в результате получится:
dec(15) = oct( 17) = hex( f);
Можно также использовать строку в общем формате:
char* form(char* format ...); cout
Функции
Функция - это именованная часть программы, к которой можно обращаться из других частей программы столько раз, сколько потребуется. Рассмотрим программу, печатающую степени числа 2:
extern float pow(float, int); //pow() определена в другом месте
main() { for (int i=0; i
Первая строка функции - описание, указывающее, что pow - функция, получающая параметры типа float и int и возвращающая float. Описание функции используется для того, чтобы сделать определенными обращения к функции в других местах.
При вызове тип каждого параметра функции сопоставляется с ожидаемым типом точно так же, как если бы инициализировалась переменная описанного типа. Это гарантирует надлежащую проверку и преобразование типов. Например, обращение pow(12.3,"abcd") вызовет недовольство компилятора, поскольку "abcd" является строкой, а не int. При вызове pow(2,i) компилятор преобразует 2 к типу float, как того требует функция. Функция pow может быть определена например так:
float pow(float x, int n) { if (n
Первая часть определения функции задает имя функции, тип возвращаемого ею значения (если таковое имеется) и типы и имена ее параметров (если они есть). Значение возвращается из функции с помощью оператора return.
Разные функции обычно имеют разные имена, но функциям, выполняющим сходные действия над объектами различных типов, иногда лучше дать возможность иметь одинаковые имена. Если типы их параметров различны, то компилятор всегда может различить их и выбрать для вызова нужную функцию. Может, например, иметься одна функция возведения в степень для целых переменных и другая для переменных с плавающей точкой:
overload pow; int pow(int, int); double pow(double, double); //... x=pow(2,10); y=pow(2.0,10.0);
Описание
overload pow;
сообщает компилятору, что использование имени pow более чем для одной функции является умышленным.
Если функция не возвращает значения, то ее следует описать как void:
void swap(int* p, int* q) // поменять местами { int t = *p; *p = *q; *q = t; }
Обычный способ сделать что-либо в C++ программе - это вызвать функцию, которая это делает. Определение функции является способом задать то, как должно делаться некоторое действие. Функция не может быть вызвана, пока она не описана.
Есть только две вещи, которые можно проделывать с функцией: вызывать ее и брать ее адрес. Если в выражении имя функции возникает не в положении имени функции в вызове, то генерируется указатель на функцию. Так, для передачи одной функции другой можно написать
typedef int (*PF) (); extern g (PF); extern f (); ... g (f);
Тогда определение g может иметь следующий вид:
g (PF funcp) { ... (*funcp) (); ... }
Заметьте, что f должна быть описана явно в вызывающей программе, поскольку ее появление в g(f) не сопровождалось (.
Функции Члены
Рассмотрим реализацию понятия даты с использованием struct для того, чтобы определить представление даты date и множества функций для работы с переменными этого типа:
struct date { int month, day, year; }; // дата: месяц, день, год } date today; void set_date(date*, int, int, int); void next_date(date*); void print_date(date*); // ...
Никакой явной связи между функциями и типом данных нет. Такую связь можно установить, описав функции как члены:
struct date { int month, day, year;
void set(int, int, int); void get(int*, int*, int*); void next(); void print(); };
Функции, описанные таким образом, называются функциями членами и могут вызываться только для специальной переменной соответствующего типа с использованием стандартного синтаксиса для доступа к членам структуры. Например:
date today; // сегодня date my_burthday; // мой день рождения
void f() { my_burthday.set(30,12,1950); today.set(18,1,1985);
my_burthday.print(); today.next(); }
Поскольку разные структуры могут иметь функции члены с одинаковыми именами, при определении функции члена необходимо указывать имя структуры:
void date::next() { if ( ++day 28 ) { // делает сложную часть работы } }
В функции члене имена членов могут использоваться без явной ссылки на объект. В этом случае имя относится к члену того объекта, для которого функция была вызвана.
Просто структуры данных вроде employee и manager на самом деле не столь интересны и часто не особенно полезны, поэтому рассмотрим, как добавить к ним функции. Например:
class employee { char* name; // ... public: employee* next; void print(); // ... };
class manager : public employee { // ... public: void print(); // ... };
Надо ответить на некоторые вопросы. Как может функция член производного класса manager использовать члены его базового класса employee? Как члены базового класса employee могут использовать функции члены производного класса manager? Какие члены базового класса employee может использовать функция не член на объекте типа manager? Каким образом программист может повлиять на ответы на эти вопросы, чтобы удовлетворить требованиям приложения?
Рассмотрим:
void manager::print() { cout
Член производного класса может использовать открытое имя из своего базового класса так же, как это могут делать другие члены последнего, то есть без указания объекта. Предполагается, что на объект указывает this, поэтому (корректной) ссылкой на имя name является this-name. Однако функция manager::print компилироваться не будет, член производного класса не имеет никакого особого права доступа к закрытым членам его базового класса, поэтому для нее name недоступно.
Это многим покажется удивительным, но представьте себе другой вариант: что функция член могла бы обращаться к закрытым членам своего базового класса. Возможность, позволяющая программисту получать доступ к закрытой части класса просто с помощью вывода из него другого класса, лишила бы понятие закрытого члена всякого смысла. Более того, нельзя было бы узнать все использования закрытого имени посмотрев на функции, описанные как члены и друзья этого класса. Пришлось бы проверять каждый исходный файл во всей программе на наличие в нем производных классов, потом исследовать каждую функцию этих классов, потом искать все классы, производные от этих классов, и т.д. Это по меньшей мере утомительно и скорее всего нереально.
Функция, описанная как член, (без спецификатора friend (#8.5.9)) называется функцией членом и вызывается с помощью синтаксиса члена класса (#7.1). Например:
struct tnode { char tword[20]; int count; tnode *left; tnode *right; void set (char* w,tnode* l,tnode* r); };
tnode n1, n2;
n1.set ("asdf",n2,0); n2.set ("ghjk",0,0);
Определение функции члена рассматривается как находящееся в области видимости ее класса. Это значит, что она может непосредственно использовать имена ее класса. Если определение функции члена находится вне описания класса, то имя функции члена должно быть уточнено именем класса с помощью записи
typedef-имя . простое_оп_имя
см. 3.3. Определения функций обсуждаются в
).
В члене функции ключевое слово this указывает на объект, для которого вызвана функция. Типом this в функции, которая является членом класса cl, является cl*. Если mem - член класса cl,то mem и this-mem - синонимы в функции члене класса cl (если mem не был использован в качестве имени локальной переменной в промежуточной области видимости).
Функция член может быть определена (#10.1) в описании класса. Помещение определения функции члена в описание класса является кратким видом записи описания ее в описании класса и затем определения ее как inline () сразу после описания класса. Например:
int b; struct x { int f () { return b; } int f () { return b; } int b; };
означает
int b; struct x { int f (); int b; }; inline x.f () { return b; }
Для функций членов не нужно использование спецификатора overload (): если имя описывается как означающее несколько имен в классе, то оно перегружено (см. #8.9).
Применение операции получения адреса к функциям членам допустимо. Тип параметра результирующей функции указатель на есть (...), то есть, неизвестен (). Любое использование его является зависимым от реализации, поскольку способ инициализации указателя для вызова функции члена не определен.
Функции и Файлы
Итерация свойственна человеку, рекурсия божественна. - Л. Питер Дойч
Все нетривиальные программы собираются из нескольких раздельно компилируемых единиц (их принято называть просто файлами). В этой главе описано, как раздельно откомпилированные функции могут обращаться друг к другу, как такие функции могут совместно пользоваться данными (разделять данные), и как можно обеспечить согласованность типов, которые используются в разных файлах программы. Функции обсуждаются довольно подробно. Сюда входят передача параметров, параметры по умолчанию, перегрузка имен функций, и, конечно же, описание и определение функций. В конце описываются макросы.
Функции Операции
Можно описывать функции, определяющие значения следующих операций:
+ - * / % ^ | ~ ! = += -= *= /= %= ^= = |= = = ++ -- [] () new delete
Последние четыре - это индексирование (), вызов функции (), выделение свободной памяти и освобождение свободной памяти (#3.2.6). Изменить приоритеты перечисленных операций невозможно, как невозможно изменить и синтаксис выражений. Нельзя, например, определить унарную операцию % или бинарную !. Невозможно определить новые лексические символы операций, но в тех случаях, когда множество операций недостаточно, вы можете использовать запись вызова функции. Используйте например, не **, а pow(). Эти ограничения могут показаться драконовскими, но более гибкие правила могут очень легко привести к неоднозначностям. Например, на первый взгляд определение операции **, означающей возведение в степень, может показаться очевидной и простой задачей, но подумайте еще раз. Должна ли ** связываться влево (как в Фортране) или вправо (как в Алголе)? Выражение a**p должно интерпретироваться как a*(*p) или как (a)**(p)?
Имя функции операции есть ключевое слово operator (то есть, операция), за которым следует сама операция, например, operator void f(complex a, complex b) { complex c = a + b; // сокращенная запись complex d = operator+(a,b); // явный вызов }
При наличии предыдущего описания complex оба инициализатора являются синонимами.
Функция операция
Большинство операций могут быть перегружены с тем, чтобы они могли получать в качестве операндов объекты класса.
имя_функции_операции: operator op op: + - * / % ^ | ~ ! = += -= *= /= %= ^= = |= = == != = ++ -- () []
Последние две операции - это вызов функции и индексирование. Функция операция может или быть функцией членом, или получать по меньшей мере один параметр класса. См. также #7.16.
Функция ввода
Чтение ввода - часто самая запутанная часть программы. Причина в том, что если программа должна общаться с человеком, то она должна справляться с его причудами, условностями и внешне случайными ошибками. Попытки заставить человека вести себя более удобным для машины образом часто (и справедливо) рассматриваются как оскорбительные. Задача низкоуровневой программы ввода состоит в том, чтобы читать символы по одному и составлять из них лексические символы более высокого уровня. Далее эти лексемы служат вводом для программ более высокого уровня. У нас ввод низкого уровня осуществляется get_token(). Обнадеживает то, что написание программ ввода низкого уровня не является ежедневной работой; в хорошей системе для этого будут стандартные функции.
Для калькулятора правила ввода сознательно были выбраны такими, чтобы функциям по работе с потоками было неудобно эти правила обрабатывать; незначительные изменения в определении лексем сделали бы get_token() обманчиво простой.
Первая сложность состоит в том, что символ новой строки '\n' является для калькулятора существенным, а функции работы с потоками считают его символом пропуска. То есть, для этих функций '\n' значим только как ограничитель лексемы. Чтобы преодолеть это, надо проверять пропуски (пробел, символы табуляции и т.п.):
char ch
do { // пропускает пропуски за исключением '\n' if(!cin.get(ch)) return curr_tok = END; } while (ch!='\n' isspace(ch));
Вызов cin.get(ch) считывает один символ из стандартного потока ввода в ch. Проверка if(!cin.get(ch)) не проходит в случае, если из cin нельзя считать ни одного символа; в этом случае возвращается END, чтобы завершить сеанс работы калькулятора. Используется операция ! (НЕ), поскольку get() возвращает в случае успеха ненулевое значение.
Функция (inline) isspace() из обеспечивает стандартную проверку на то, является ли символ пропуском (#8.4.1); isspace(c) возвращает ненулевое значение, если c является символом пропуска, и ноль в противном случае. Проверка реализуется в виде поиска в таблице, поэтому использование isspace() намного быстрее, чем проверка на отдельные символы пропуска; это же относится и к функциям isalpha(), isdigit() и isalnum(), которые используются в get_token().
После того, как пустое место пропущено, следующий символ используется для определения того, какого вида какого вида лексема приходит. Давайте сначала рассмотрим некоторые случаи отдельно, прежде чем приводить всю функцию. Ограничители лексем '\n' и ';' обрабатываются так:
switch (ch) { case ';': case '\n': cin WS; // пропустить пропуск return curr_tok=PRINT;
Пропуск пустого места делать необязательно, но он позволяет избежать повторных обращений к get_token(). WS - это стандартный пропусковый объект, описанный в ; он используется только для сброса пропуска. Ошибка во вводе или конец ввода не будут обнаружены до следующего обращения к get_token(). Обратите внимание на то, как можно использовать несколько меток case (случаев) для одной и той же последовательности операторов, обрабатывающих эти случаи. В обоих случаях возвращается лексема PRINT и помещается в curr_tok.
Числа обрабатываются так:
case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': cin.putback(ch); cin number_value; return curr_tok=NUMBER;
Располагать метки случаев case горизонтально, а не вертикально, не очень хорошая мысль, поскольку читать это гораздо труднее, но отводить по одной строке на каждую цифру нудно.
Поскольку операция определена также и для чтения констант с плавающей точкой в double, программирование этого не составляет труда: сперва начальный символ (цифра или точка) помещается обратно в cin, а затем можно считывать константу в number_value. Имя, то есть лексема NAME, определяется как буква, за которой возможно следует несколько букв или цифр:
if (isalpha(ch)) { char* p = name_string; *p++ = ch; while (cin.get(ch) isalnum(ch)) *p++ = ch; cin.putback(ch); *p = 0; return curr_tok=NAME; }
Эта часть строит в name_string строку, заканчивающуюся нулем. Функции isalpha() и isalnum() заданы в ; isalnum(c) не ноль, если c буква или цифра, ноль в противном случае.
Вот, наконец, функция ввода полностью:
token_value get_token() { char ch;
do { // пропускает пропуски за исключением '\n' if(!cin.get(ch)) return curr_tok = END; } while (ch!='\n' isspace(ch));
switch (ch) { case ';': case '\n': cin WS; // пропустить пропуск return curr_tok=PRINT; case '*': case '/': case '+': case '-': case '(': case ')': case '=': return curr_tok=ch; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': cin.putback(ch); cin number_value; return curr_tok=NUMBER; default: // NAME, NAME= или ошибка if (isalpha(ch)) { char* p = name_string; *p++ = ch; while (cin.get(ch) isalnum(ch)) *p++ = ch; cin.putback(ch); *p = 0; return curr_tok=NAME; } error("плохая лексема"); return curr_tok=PRINT; } }
Поскольку token_value ( значение лексемы) операции было определено как целое значение этой операции , обработка всех операций тривиальна.
Введение в язык Си++
C++ снабжен имеющим дурную репутацию оператором goto.
goto идентификатор; идентификатор : оператор
В общем, в программировании высокого уровня он имеет очень мало применений, но он может быть очень полезен, когда C++ программа генерируется программой, а не пишется непосредственно человеком. Например, операторы goto можно использовать в синтаксическом анализаторе, порождаемом генератором синтаксических анализаторов. Оператор goto может быть также важен в тех редких случаях, когда важна наилучшая эффективность, например, во внутреннем цикле какой- нибудь программы, работающей в реальном времени.
Одно из немногих разумных применений состоит в выходе из вложенного цикла или переключателя (break лишь прекращает выполнение самого внутреннего охватывающего его цикла или переключателя). Например:
for (int i = 0; i
Характеристики аппаратного обеспечения
В нижеследующей таблице собраны некоторые характеристики аппаратного обеспечения, различающиеся от машины к машине.
_____________________________________________________________ | DEC VAX-11 Motorola 68000 IBM 370 ATT 3B | | ASCII ASCII EBCDIC ASCII | |___________________________________________________________| | char | 8 бит | 8 бит | 8 бит | 8 бит | | int | 32 бит | 16 бит | 32 бит | 16 бит | | short | 16 бит | 16 бит | 16 бит | 16 бит | | long | 32 бит | 32 бит | 32 бит | 32 бит | | float | 32 бит | 32 бит | 32 бит | 32 бит | | double | 64 бит | 64 бит | 64 бит | 64 бит | | указатель| 32 бит | 32 бит | 24 бит | 32 бит | | диапазон | | | | | | float | +_10E+_38 | +_10E+_38 | +_10E+_76 | +_10E+_38 | | диапазон | | | | | | double | +_10E+_38 | +_10E+_38 | +_10E+_76 | +_10E+_308 | | тип char | знаковый | без знака | без знака | без знака | | тип поля | знаковый | без знака | без знака | без знака | | порядок | справа | слева | слева | слева | | полей | налево | направо | направо | направо | |__________|___________|___________|___________|____________|
Идентификаторы (имена)
Идентификатор - последовательность букв и цифр произвольной длины; первый символ обязан быть буквой; подчерк '_' считается за букву; буквы в верхнем и нижнем регистрах являются различными.
Иерархия Типов
Производный класс сам может быть базовым классом. Например:
class employee { ... }; class secretary : employee { ... }; class manager : employee { ... }; class temporary : employee { ... }; class consultant : temporary { ... }; class director : manager { ... }; class vice_president : manager { ... }; class president : vice_president { ... };
Такое множество родственных классов принято называть иерархией классов. Поскольку можно выводить класс только из одного базового класса, такая иерархия является деревом и не может быть графом более общей структуры. Например:
class temporary { ... }; class employee { ... }; class secretary : employee { ... };
// не C++: class temporary_secretary : temporary : secretary { ... }; class consultant : temporary : employee { ... };
И этот факт вызывает сожаление, потому что направленный ациклический граф производных классов был бы очень полезен. Такие структуры описать нельзя, но можно смоделировать с помощью членов соответствующий типов. Например:
class temporary { ... }; class employee { ... }; class secretary : employee { ... };
// Альтернатива: class temporary_secretary : secretary { temporary temp; ... }; class consultant : employee { temporary temp; ... };
Это выглядит неэлегантно и страдает как раз от тех проблем, для преодоления которых были изобретены производные классы. Например, поскольку consultant не является производным от temporary, consultant'а нельзя помещать с список временных служащих (temporary employee), не написав специальной программы. Однако во многих полезных программах этот метод успешно используется.
Имена
Имя (идентификатор) состоит из последовательности букв и цифр. Первый символ должен быть буквой. Символ подчерка _ считается буквой. C++ не налагает ограничений на число символов в имени, но некоторые части реализации находятся вне ведения автора компилятора (в частности, загрузчик), и они, к сожалению, такие ограничения налагают. Некоторые среды выполнения также делают необходимым расширить или ограничить набор символов, допустимых в идентификаторе; расширения (например, при допущении в именах символа $) порождают непереносимые программы. В качестве имени не могут использоваться ключевые слова C++ (см. ). Примеры имен:
hello this_is_a_most_unusially_long_name DEFINED foO bAr u_name HorseSense var0 var1 CLASS _class ___
Примеры последовательностей символов, которые не могут использоваться как идентификаторы:
a fool $sys class 3var pay.due foo~bar .name if
Буквы в верхнем и нижнем регистрах считаются различными, поэтому Count и count - различные имена, но вводить имена, лишь незначительно отличающиеся друг от друга, нежелательно. Имена, начинающиеся с подчерка, по традиции используются для специальных средств среды выполнения, поэтому использовать такие имена в прикладных программах нежелательно.
Во время чтения программы компилятор всегда ищет наиболее длинную строку, составляющую имя, поэтому var10 - это одно имя, а не имя var, за которым следует число 10; и elseif - одно имя, а не ключевое слово else, после которого стоит ключевое слово if.
Имена и Типы
Имя обозначает (денотирует) объект, функцию, тип, значение или метку. Имя вводится в программе описанием (). Имя может использоваться только внутри области текста программы, называемой его областью видимости. Имя имеет тип, определяющий его использование. Объект - это область памяти. Объект имеет класс памяти, определяющий его время жизни. Смысл значения, обнаруженного в объекте, определяется типом имени, использованного для доступа к нему.
Имена типов
Иногда (для неявного задания преобразования типов и в качестве параметра sizeof или new) нужно использовать имя типа данных. Это выполняется при помощи "имени типа" которое по сути является описанием для объекта этого типа, в котором опущено имя объекта.
имя_типа: спецификатор_типа абстрактный_описатель абстрактный_описатель : пустой * абстрактный_описатель абстрактный_описатель ( списоко_писателей_параметров) абстрактный_описатель [ константное_выражение opt ] ( абстрактный_описатель )
Является возможным идентифицировать положение в абстрактном_описателе, где должен был бы появляться идентификатор в случае, если бы конструкция была описателем в описании. Тогда именованный тип является тем же, что и тип предполагаемого идентификатора. Например:
int int * int *[3] int *() int (*)()
именует, соответственно, типы "целое", "указатель на целое", "указатель на массив из трех целых", "функция, возвращающая указатель на функцию, возвращающую целое" и "указатель на целое".
Простое имя типа есть имя типа, состоящее из одного идентификатора или ключевого слова.
простое_имя_типа: typedef-имя char short int long unsigned float double
Они используются в альтернативном синтаксисе для преобразования типов. Например:
(double) a
может быть также записано как
double (a)
Индексирование
Чтобы задать смысл индексов для объектов класса используется функция operator[]. Второй параметр (индекс) функции operator[] может быть любого типа. Это позволяет определять ассоциативные массивы и т.п. В качестве примера давайте перепишем пример из , где при написании небольшой программы для подсчета числа вхождений слов в файле применялся ассоциативный массив. Там использовалась функция. Здесь определяется надлежащий тип ассоциативного массива:
struct pair { char* name; int val; };
class assoc { pair* vec; int max; int free; public: assoc(int); int operator[](char*); void print_all(); };
В assoc хранится вектор пар pair длины max. Индекс первого неиспользованного элемента вектора находится в free. Конструктор выглядит так:
assoc::assoc(int s) { max = (s
При реализации применяется все тот же простой и неэффективный метод поиска, что использовался в #2.3.10. Однако при переполнении assoc увеличивается:
#include
int assoc::operator[](char* p) /* работа с множеством пар "pair": поиск p, возврат ссылки на целую часть его "pair" делает новую "pair", если p не встречалось */ { register pair* pp;
for (pp=vec[free-1]; vecname)==0) return pp-val;
if (free==max) { // переполнение: вектор увеличивается pair* nvec = new pair[max*2]; for ( int i=0; iname = new char[strlen(p)+1]; strcpy(pp-name,p); pp-val = 0; // начальное значение: 0 return pp-val; }
Поскольку представление assoc скрыто, нам нужен способ его печати. В следующем разделе будет показано, как определить подходящий итератор, а здесь мы используем простую функцию печати:
vouid assoc::print_all() { for (int i = 0; ibuf) vec[buf]++; vec.print_all(); }
Инициализация
Использование для обеспечения инициализации объекта класса функций вроде set_date() (установить дату) неэлегантно и чревато ошибками. Поскольку нигде не утверждается, что объект должен быть инициализирован, то программист может забыть это сделать, или (что приводит, как правило, к столь же разрушительным последствиям) сделать это дважды. Есть более хороший подход: дать возможность программисту описать функцию, явно предназначенную для инициализации объектов. Поскольку такая функция конструирует значения данного типа, она называется конструктором. Конструктор распознается по тому, что имеет то же имя, что и сам класс. Например:
class date { // ... date(int, int, int); };
Когда класс имеет конструктор, все объекты этого класса будут инициализироваться. Если для конструктора нужны параметры, они должны даваться:
date today = date(23,6,1983); date xmas(25,12,0); // сокращенная форма // (xmas - рождество) date my_burthday; // недопустимо, опущена инициализация
Часто бывает хорошо обеспечить несколько способов инициализации объекта класса. Это можно сделать, задав несколько конструкторов. Например:
class date { int month, day, year; public: // ... date(int, int, int); // день месяц год date(char*); // дата в строковом представлении date(int); // день, месяц и год сегодняшние date(); // дата по умолчанию: сегодня };
Конструкторы подчиняются тем же правилам относительно типов параметров, что и перегруженные функции (#4.6.7). Если конструкторы существенно различаются по типам своих параметров, то компилятор при каждом использовании может выбрать правильный:
date today(4); date july4("Июль 4, 1983"); date guy("5 Ноя"); date now; // инициализируется по умолчанию
Заметьте, что функции члены могут быть перегружены без явного использования ключевого слова overload. Поскольку полный список функций членов находится в описании класса и как правило короткий, то нет никакой серьезной причины требовать использования слова overload для предотвращения случайного повторного использования имени.
Инициализация Потоков Ввода
Естественно, тип istream, так же как и ostream, снабжен конструктором:
class istream { // ... istream(streambuf* s, int sk =1, ostream* t =0); istream(int size, char* p, int sk =1); istream(int fd, int sk =1, ostream* t =0); };
Параметр sk задает, должны пропускаться пропуски или нет. Параметр t (необязательный) задает указатель на ostream, к которому прикреплен istream. Например, cin прикреплен к cout; это значит, что перед тем, как попытаться читать символы из своего файла, cin выполняет
cout.flush(); // пишет буфер вывода
С помощью функции istream::tie() можно прикрепить (или открепить, с помощью tie(0)) любой ostream к любому istream. Например:
int y_or_n(ostream to, istream from) /* "to", получает отклик из "from" */ { ostream* old = from.tie(to); for (;;) { cout
Когда используется буферизованный ввод (как это происходит по умолчанию), пользователь не может набрав только одну букву ожидать отклика. Система ждет появления символа новой строки. y_or_n() смотрит на первый символ строки, а остальные игнорирует.
Символ можно вернуть в поток с помощью функции istream::putback(char). Это позволяет программе "заглядывать вперед" в поток ввода.
Инициализация Потоков Вывода
ostream имеет конструкторы:
class ostream { // ... ostream(streambuf* s); // связывает с буфером потока ostream(int fd); // связывание для файла ostream(int size, char* p); // связывет с вектором };
Главная работа этих конструкторов - связывать с потоком буфер. streambuf - класс, управляющий буферами; он описывается в #8.6, как и класс filebuf, управляющий streambuf для файла. Класс filebuf является производным от класса streambuf.
Описание стандартных потоков вывода cout и cerr, которое находится в исходных кодах библиотеки потоков ввода/вывода, выглядит так:
// описать подходящее пространство буфера char cout_buf[BUFSIZE]
// сделать "filebuf" для управления этим пространством // связать его с UNIX'овским потоком вывода 1 (уже открытым) filebuf cout_file(1,cout_buf,BUFSIZE);
// сделать ostream, обеспечивая пользовательский интерфейс ostream cout(cout_file);
char cerr_buf[1];
// длина 0, то есть, небуферизованный // UNIX'овский поток вывода 2 (уже открытый) filebuf cerr_file()2,cerr_buf,0;
ostream cerr(cerr_file);
Примеры двух других конструкторов ostream можно найти в #8.3.3 и .
Inline
При программировании с использованием классов очень часто используется много маленьких функций. По сути, везде, где в программе традиционной структуры стояло бы просто какое-нибудь обычное использование структуры данных, дается функция. То, что было соглашением, стало стандартом, который распознает компилятор. Это может страшно понизить эффективность, потому что стоимость вызова функции (хотя и вовсе не высокая по сравнению с другими языками) все равно намного выше, чем пара ссылок по памяти, необходимая для тела функции.
Чтобы справиться с этой проблемой, был разработан аппарат inline- функций. Функция член, определенная (а не просто описанная) в описании класса, считается inline. Это значит, например, что в функциях, которые используют приведенные выше char_stack, нет никаких вызовов функций кроме тех, которые используются для реализации операций вывода! Другими словами, нет никаких затрат времени выполнения, которые стоит принимать во внимание при разработке класса. Любое, даже самое маленькое действие, можно задать эффективно. Это утверждение снимает аргумент, который чаще всего приводят чаще всего в пользу открытых членов данных.
Функцию член можно также описать как inline вне описания класса. Например:
char char_stack { int size; char* top; char* s; public: char pop(); // ... };
inline char char_stack::pop() { return *--top; }
Inline-подстановка
Если часто повторяется обращение к очень маленькой функции, то вы можете начать беспокоиться о стоимости вызова функции. Обращение к функции члену не дороже обращения к функции не члену с тем же числом параметров (надо помнить, что функция член всегда имеет хотя бы один параметр), и вызовы в функций в C++ примерно столь же эффективны, сколь и в любом языке. Однако для слишком маленьких функций может встать вопрос о накладных расходах на обращение. В этом случае можно рассмотреть возможность спецификации функции как inline-подставляемой. Если вы поступите таким образом, то компилятор сгенерирует для функции соответствующий код в месте ее вызова. Семантика вызова не изменяется. Если, например, size и elem inline-подставляемые, то
vector s(100); //... i = s.size(); x = elem(i-1);
порождает код, эквивалентный
//... i = 100; x = s.v[i-1];
C++ компилятор обычно достаточно разумен, чтобы генерировать настолько хороший код, насколько вы можете получить в результате прямого макрорасширения. Разумеется, компилятор иногда вынужден использовать временные переменные и другие уловки, чтобы сохранить семантику.
Вы можете указать, что вы хотите, чтобы функция была inline- подставляемой, поставив ключевое слово inline, или, для функции члена, просто включив определение функции в описание класса, как это сделано в предыдущем примере для size() и elem().
При хорошем использовании inline-функции резко повышают скорость выполнения и уменьшают размер объектного кода. Однако, inline- функции запутывают описания и могут замедлить компиляцию, поэтому, если они не необходимы, то их желательно избегать. Чтобы inline- функция давала существенный выигрыш по сравнению с обычной функцией, она должна быть очень маленькой.
Интерфейс
Рассмотрим такое написание класса slist для однократно связанного списка, с помощью которого можно создавать как однородные, так и неоднородные списки объектов тех типов, которые еще должны быть определены. Сначала мы определим тип ent:
typedef void* ent;
Точная сущность типа ent несущественна, но нужно, чтобы в нем мог храниться указатель. Тогда мы определим тип slink:
class slink { friend class slist; friend class slist_iterator; slink* next; ent e; slink(ent a, slink* p) { e=a; next=p;} };
В одном звене может храниться один ent, и с помощью него реализуется класс slist:
class slist { friend class slist_iterator; slink* last; // last-next - голова списка public: int insert(ent a); // добавить в голову списка int append(ent a); // добавить в хвост списка ent get(); // вернуться и убрать голову списка void clear(); // убрать все звенья
slist() { last=0; } slist(ent a) { last=new slink(a,0); last-next=last; } ~slist() { clear(); } };
Хотя список очевидным образом реализуется как связанный список, реализацию можно изменить так, чтобы использовался вектор из ent'ов, не повлияв при этом на пользователей. То есть, применение slink'ов никак не видно в описаниях открытых функций slist'ов, а видно только в закрытой части и определениях функций.
Интерфейсы и Реализации
Что представляет собой хороший класс? Нечто, имеющее небольшое и хорошо определенное множество действий. Нечто, что можно рассматривать как "черный ящик", которым манипулируют только посредством этого множества действий. Нечто, чье фактическое представление можно любым мыслимым способом изменить, не повлияв на способ использования множества действий. Нечто, чего можно хотеть иметь больше одного.
Для всех видов контейнеров существуют очевидные примеры: таблицы, множества, списки, вектора, словари и т.д. Такой класс имеет операцию "вставить", обычно он также имеет операции для проверки того, был ли вставлен данный элемент. В нем могут быть действия для осуществления проверки всех элементов в определенном порядке, и кроме всего прочего, в нем может иметься операция для удаления элемента. Обычно контейнерные (то есть, вмещающие) классы имеют конструкторы и деструкторы.
Скрытие данных и продуманный интерфейс может дать концепция модуля (см. например : файлы как модули). Класс, однако, является типом. Чтобы использовать его, необходимо создать объекты этого класса, и таких объектов можно создавать столько, сколько нужно. Модуль же сам является объектом. Чтобы использовать его, его надо только инициализировать, и таких объектов ровно один.
Исторические замечания
Безусловно, C++ восходит главным образом к C [7]. C сохранено как подмножество, поэтому сделанного в C акцента на средствах низкого уровня достаточно, чтобы справляться с самыми насущными задачами системного программирования. C, в свою очередь, многим обязано своему предшественнику BCPL [9]; на самом деле, комментарии // (заново) введены в C++ из BCPL. Если вы знаете BCPL, то вы заметите, что в C++ по-прежнему нет VALOF блока. Еще одним источником вдохновения послужил язык Simula67 [2,3]; из него была позаимствована концепция класса (вместе с производными классами и функциями членами). Это было сделано, чтобы способствовать модульности через использование виртуальных функций. Возможности C++ по перегрузке операций и свобода в расположении описаний везде, где может встречаться оператор, похожи на Алгол68 [14].
Название C++ - изобретение совсем недавнее (лета 1983его). Более ранние версии языка использовались начиная с 1980ого и были известны как "C с Классами". Первоначально язык был придуман потому, что автор хотел написать модели, управляемые прерываниями, для чего был бы идеален Simula67, если не принимать во внимание эффективность. "C с Классами" использовался для крупных проектов моделирования, в которых строго тестировались возможности написания программ, требующих минимального (только) пространства памяти и времени на выполнение. В "C с Классами" не хватало перегрузки операций, ссылок, виртуальных функций и многих деталей. C++ был впервые введен за пределами исследовательской группы автора в июле 1983его; однако тогда многие особенности C++ были еще не придуманы.
Название C++ выдумал Рик Масситти. Название указывает на эволюционную природу перехода к нему от C. "++" - это операция приращения в C. Чуть более короткое имя C+ является синтаксической ошибкой; кроме того, оно уже было использовано как совсем другого языка. Знатоки семантики C находят, что C++ хуже, чем ++C. Названия D язык не получил, поскольку он является расширением C и в нем не делается попыток исцеляться от проблем путем выбрасывания различных особенностей. Еще одну интерпретацию названия C++ можно найти в приложении к Оруэллу [8].
Изначально C++ был разработан, чтобы автору и его друзьям не приходилось программировать на ассемблере, C или других современных языках высокого уровня. Основным его предназначением было сделать написание хороших программ более простым и приятным для отдельного программиста. Плана разработки C++ на бумаге никогда не было; проект, документация и реализация двигались одновременно. Разумеется, внешний интерфейс C++ был написан на C++. Никогда не существовало "Проекта C++" и "Комитета по разработке C++". Поэтому C++ развивался и продолжает развиваться во всех направлениях чтобы справляться со сложностями, с которыми сталкиваются пользователи, а также в процессе дискуссий автора с его друзьями и коллегами.
В качестве базового языка для C++ был выбран C, потому что он (1) многоцелевой, лаконичный и относительно низкого уровня; (2) отвечает большинству задач системного программирования; (3) идет везде и на всем; и (4) пригоден в среде программирования UNIX. В C есть свои сложности, но в наспех спроектированном языке тоже были бы свои, а сложности C нам известны. Самое главное, работа с C позволила "C с Классами" быть полезным (правда, неудобным) инструментом в ходе первых месяцев раздумий о добавлении к C Simula-образных классов.
C++ стал использоваться шире, и по мере того, как возможности, предоставляемые им помимо возможностей C, становились все более существенными, вновь и вновь поднимался вопрос о том, сохранять ли совместимость с C. Ясно, что отказавшись от определенной части наследия C можно было бы избежать ряда проблем (см., например, Сети [12]). Это не было сделано, потому что (1) есть миллионы строк на C, которые могли бы принести пользу в C++ при условии, что их не нужно было бы полностью переписывать с C на C++; (2) есть сотни тысяч строк библиотечных функций и сервисных программ, написанных на C, которые можно было бы использовать из или на C++ при условии, что C++ полностью совместим с C по загрузке и синтаксически очень похож на C; (3) есть десятки тысяч программистов, которые знают C, и которым, поэтому, нужно только научиться использовать новые особенности C++, а не заново изучать его основы; и (4), поскольку C++ и C будут использоваться на одних и тех же системах одними и теми же людьми, отличия должны быть либо очень большими, либо очень маленькими, чтобы свести к минимуму ошибки и недоразумения. Позднее была проведена проверка определения C++, чтобы удостовериться в том, что любая конструкция, допустимая и в C и в C++, действительно означает в обоих языках одно и то же.
Язык C сам эволюционировал за последние несколько лет, частично под влиянием развития C++ (см. Ростлер [11]). Предварительный грубый ANSI стандарт C [10] содержит синтаксис описаний функций, заимствованный из "C с Классами". Заимствование идей идет в обе стороны; например, указатель void* был придуман для ANSI C и впервые реализован в C++. Когда ANSI стандарт разовьется несколько дальше, придет время пересмотреть C++, чтобы удалить необоснованную несовместимость. Будет, например, модернизирован препроцессор (#с.11), и нужно будет, вероятно, отрегулировать правила осуществления плавающей арифметики. Это не должно оказаться болезненным, и C и ANSI C очень близки к тому, чтобы стать подмножествами C++ (см. ).
Явно заданные длинные константы
Десятичная, восьмиричная или шестнадцатиричная константа, за которой непосредственно стоит l (латинская буква "эль") или L, считается длинной константой.
Явное Преобразование Типа
Простое_имя_типа (#8.2), возможно, заключенное в скобки, за которым идет заключенное в скобки выражение (или список_выражений, если тип является классом с соответствующим образом описанным конструктором ) влечет преобразование значения выражения в названный тип. Чтобы записать преобразование в тип, не имеющий простого имени, имя_типа (#8.7) должно быть заключено в скобки. Если имя типа заключено в скобки, выражение заключать в скобки необязательно. Такая запись называется приведением к типу.
Указатель может быть явно преобразован к любому из интегральных типов, достаточно по величине для его хранения. То, какой из int и long требуется, является машинно зависимым. Отобразующая функция также является машинно зависимой, но предполагается, что она не содержит сюрпризов для того, кто знает структуру адресации в машине. Подробности для некоторых конкретных машин были приведены в #2.6.
Объект интегрального типа может быть явно преобразован в указатель. Отображающая функция всегда превращает целое, полученное из указателя, обратно в тот же указатель, но в остальных случаях является машинно зависимой.
Указатель на один тип может быть явно преобразован в указатель на другой тип. Использование полученного в результате указателя может привести к исключительной ситуации адресации, если исходный указатель не указывает на объект, соответствующим образом выравненный в памяти. Гарантируется, что указатель на объект данного размера может быть преобразован в указатель на объект меньшего размера и обратно без изменений. Различные машины могут различаться по числу бит в указателях и требованиям к выравниванию объектов. Составные объекты выравниваются по самой строгой границе, требуемой каким-либо из его составляющих.
Объект может преобразовываться в объект класса только если был описан соответствующий конструктор или операция преобразования ().
Объект может явно преобразовываться в ссылочный тип X, если указатель на этот объект может явно преобразовываться в X*.
Явные преобразования указателей
Определенные преобразования, включающие массивы, выполняются, но имеют зависящие от реализации аспекты. Все они задаются с помощью явной операции преобразования типов, см. ##7.2 и 8.7.
Указатель может быть преобразован к любому из целых типов, достаточно больших для его хранения. То, какой из int и long требуется, является машинно зависимым. Преобразующая функция также является машинно зависимой, но предполагается, что она не содержит сюрпризов для того, кто знает структуру адресации в машине. Подробности для некоторых конкретных машин были даны в #2.6.
Объект целого типа может быть явно преобразован в указатель. Преобразующая функция всегда превращает целое, полученное из указателя, обратно в тот же указатель, но в остальных случаях является машинно зависимой.
Указатель на один тип может быть преобразован в указатель на другой тип. Использование результирующего указателя может вызывать особые ситуации, если исходный указатель не указывает на объект, соответствующим образом выравненный в памяти. Гарантируется, что указатель на объект данного размера может быть преобразован в указатель на объект меньшего размера и обратно без изменений.
Например, программа, выделяющая память, может получать размер (в байтах) размещаемого объекта и возвращать указатель на char; это можно использовать следующим образом.
extern void* alloc (); double* dp;
dp = (double*) alloc (sizeof (double)); *dp= 22.0 / 7.0;
alloc должна обеспечивать (машинно зависимым образом) то, что возвращаемое ею значение подходит для преобразования в указатель на double; в этом случае использование функции мобильно. Различные машины различаются по числу бит в указателях и требованиям к выравниванию объектов. Составные объекты выравниваются по самой строгой границе, требуемой каким-либо из его составляющих.