Жанр: Учеба
Язык С++ с комментариями
...ого от 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:
- стр 220 -
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, объединяя таким образом оба понятия. Однако
использование бызовых и производных классов на таком
микроскопическом уровне может очень сильно исказить код.
7.5 Неоднородные Списки
Предыдущие списки были однородными. То есть, в список помещались
только объекты одного типа. Это обеспечивалось аппаратом
производных классов. Списки не обязательно должны быть
одднородными. Список, заденный в виде указаетелей на класс, может
содержать объекты любого класса, производного от этого класса. То
есть, список может быть неоднородным. Вероятно, это единственный
наиболее важный и полезный аспект производных классов, и он весьма
существенно используется в стиле программирования, который
демонстрируется приведенным выше примером. Этот стиль
программирования часто называют объектно-основанным или
объектно-ориентированным. Он опирается на то, что действия над
объектами неоднородных списков выполняются одинаковым образом.
Смысл этих действий зависит от фактического типа объектов,
находящихся в списке (что становится известно только на стадии
выполнения), а не просто от типа элементов списка (который
компилятору известен).
- стр 221 -
7.6 Законченна Программа
Разберем процесс написания программы для рисования на экране
геметрических фигур. Она естественным образом разделяется на три
части:
[1] Администратор экрана: подпрограммы низкого уровня и структуры
данных, определяющие экран; он ведает только точками и прямыми
линиями;
[2] Библиотека фигур: набор определений оосновных фигур вроде
прямоугольника и круга и стандартные программы для работы с
ними; и
[3] Прикладная программа: множество определений,
специализированных для данного приложения, и код, в котором
они используются.
Эти три части скорее всего будут писать разные люди (в разных
организациях и в разное время). При этом части будут скорее всего
писать именно в указанном порядке с тем осложняющим
обстоятельством, что у разработчиков нижнего уровня не будет
точного представления, для чего их код в конечном счете будет
использоваться. Это отражено в приводимом примере. Чтобы пример был
короче, графическая библиотека предоставляет только весьма
ограниченный сервис, а сама прикладная программа очень проста.
Чтобы читатель смог испытать программу, даже если у него нет совсем
никаких графических средств, используется чрезвычайно простая
концепция экрана. Не должно составить труда заменить эту экранную
часть программы чем-нибудь подходящим, не изменяя код библиотеки
фигур и прикладной программы.
7.6.1 Администратор Экрана
Вначале было намерени написать администратор экрана на C (а не на
C++), чтобы подчеркнуть разделение уровней реализации. Это
оказалось слишком утомительным, поэтому пришлось пойти на
компромисс: используется стиль C (нет функций членов, виртуальных
функций, определяемых пользователем операций и т.п.), однако
применяются конструкторы, надлежащим образом описываются и
проверяются параметры функций и т.д. Оглядываясь назад, можно
сказать, что администратор экрана очень похож на C программу,
которую потом модифицировали, чтобы воспользоваться средствами C++
не переписывая все полностью.
Экран представляется как двумерный массив символов, работу с
которым осуществляют функции put_point() и put_line(), использующие
при ссылке на экран структуру point:
- стр 222 -
// файл 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"=b) y0 += dy, eps -= two_a;
}
}
Предоставляются функции для очистки экрана и его обновления:
void screen_clear() { screen_init(); } // очистка
void screen_refresh() // обновление
{
for (int y=YMAX-1; 0"=y; y--) { // сверху вниз
for (int x=0; x
7.6.2 Библиотека Фигур
Нам нужно определить общее понятие фигуры (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, то
есть, одним из вариантов обобщенного односвязанного списка,
определенного в #7.3.5. Он и соответствующий итератор были сделаны
так:
typedef shape* sp;
declare(gslist,sp);
typedef gslist(sp) shape_lst;
typedef gslist_iterator(sp) sp_iterator;
поэтому shape_list можно описать так:
shape_lst shape_list;
Линию можно построить либо по двум точкам, либо по точке и целому.
В последнем случае создается горизонтальная линия, длину которой
определяет целое. Знак целого указывает, каким концом является
точка: левым или правым. Вот определение:
- стр 225 -
class line : public shape {
/*
линия из 'w' в 'e'
north() определяется как ``выше центра
и на север как до самой северной точки''
*/
point w,e;
public:
point north()
{ return point((w.x+e.x)/2,e.ydraw();
screen_refresh();
}
- стр 227 -
И вот, наконец, настоящая сервисная функция (утилита). Она кладет
одну фигуру на верх другой, задавая, что 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);
}
Теперь представим себе, что эта библиотека считается
собственностью некоей компании, которая продает программное
обеспечение, и что они продают вам только заголовочный файл,
содержаций определения фигур, и откомпилированный вариант
определений функций. И у вас все равно остается возможность
определять новые фигуры и использовать для ваших собственных фигур
сервисные функции.
7.6.3 Прикладная Программа
Прикладная программа чрезвычайно проста. Определяется новая
фигура my_shape (на печати она немного похожа на рожицу), а потом
пишется главная программа, которая надевает на нее шляпу. Вначале
описание my_shape:
#include "shape.h"
class myshape : public rectangle {
line* l_eye; // левый глаз
line* r_eye; // правый глаз
line* mouth; // рот
public:
myshape(point, point);
void draw();
void move(int, int);
};
Глаза и рот - отдельные и независимые объекты, которые создает
конструктор my_shape:
myshape::myshape(point a, point b) : (a,b)
{
int ll = neast().x-swest().x+1;
int hh = neast().y-swest().y+1;
l_eye = new line(
point(swest().x+2,swest().y+hh*3/4),2);
r_eye = new line(
point(swest().x+ll-4,swest().y+hh*3/4),2);
mouth = new line(
point(swest().x+2,swest().y+hh/4),ll-4);
}
Объекты глаза и рот порознь рисуются заново функцией
shape_refresh(), и в принципе могут обрабатываться независимо из
- стр 228 -
объекта my_shape, которому они принадлежат. Это один способ
определять средства для иерархически построенных объектов вроде
my_shape. Другой способ демонстрируется на примере носа. Никакой
нос не определяется, его просто добавляет к картинке функция
draw():
void myshape::draw()
{
rectangle::draw();
put_point(point(
(swest().x+neast().x)/2,(swest().y+neast().y)/2));
}
my_shape передвигается посредством перемещения базового
прямоугольника rectangle и вторичных объектов l_eye, r_eye и mouth
(левого глаза, правого глаза и рта):
void myshape::move()
{
rectangle::move();
l_eye-"move(a,b);
r_eye-"move(a,b);
mouth-"move(a,b);
}
Мы можем, наконец, построить несколько фигур и немного их
подвигать:
main()
{
shape* p1 = new rectangle(point(0,0),point(10,10));
shape* p2 = new line(point(0,15),17);
shape* p3 = new myshape(point(15,10),point(27,18));
shape_refresh();
p3-"move(-10,-10);
stack(p2,p3);
stack(p1,p2);
shape_refresh();
return 0;
}
Еще раз обратите внимание, как функции вроде shape_refresh() и
stack() манипулируют объектами типов, определяемых гораздо позже,
чем были написаны (и, может быть, откомпилированы) сами эти
функции.
- стр 229 -
***********
* *
* *
* *
* *
* *
* *
* *
* *
* *
***********
*****************
*************
* *
* ** ** *
* *
* *
* ********* *
* *
*************
7.7 Свободная Память
Если вы пользоались классом slist, вы могли обнаружить, что ваша
программа тратит на заметное время на размещение и освобождение
объектов класса slink. Класс slink - это превосходный пример
класса, который может значительно выиграть от того, что программист
возьмет под контроль управление свободной памятью. Для этого вида
объектов идеально подходит оптимизирующий метод, который описан в
#5.5.6. Поскольку каждый slink создается с помощью new и
уничтожается с помощью delete членами класса slist, другой способ
выделения памяти не представляет никаких проблемм.
Если производный класс осуществляет присваивание указателю this,
то конструктор его базового класса будет вызываться только после
этого присваивания, и значение указателя this в конструкторе
базового класса будет тем, которое присвоено конструктором
производного класса. Если базовый класс присваивает указателю this,
то будет присвоено то значение, которое использует конструктор
производного класса. Например:
- стр 230 -
#include
struct base { base(); };
struct derived : base { derived(); }
base::base()
{
cout "" "\tbase 1: this=" "" int(this) "" "\n";
if (this == 0) this = (base*)27;
cout "" "\tbase 2: this=" "" int(this) "" "\n";
}
derived::derived()
{
cout "" "\tderived 1: this=" "" int(this) "" "\n";
this = (this == 0) ? (derived*)43 : this;
cout "" "\tderived 2: this=" "" int(this) "" "\n";
}
main()
{
cout "" "base b;\n";
base b;
cout "" "new base b;\n";
new base;
cout "" "derived d;\n";
derived d;
cout "" "new derived d;\n";
new derived;
cout "" "at the end\n";
}
порождает вывод
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, то будет присвоено то значение, которое всретил
- стр 231 -
деструктор его базового класса. Когда кто-либо делает в
конструкторе присваивание указателю this, важно, чтобы присваивание
указателю this всречалось на всех путях в конструкторе*.
7.8 Упражнения
1. (*1) Определите
class base {
public:
virtual void iam() { cout "" "base\n"; }
};
Выведите из base два класса и для каждого определите iam() ("я
есть"), которая выводит имя класса на печать. Создайте объекты
этих классов и вызовите для них iam(). Присвойте адреса
объектов производных классов указателям base* и вызовите iam()
через эти указатели.
2. (*2) Реализуйте примитивы экрана (#7.6.1) подходящим для вашей
системы образом.
3. (*2) Определите класс triangle (треугольник) и класс circle
(круг).
4. (*2) Определите функцию, которая рисует линию, соединяющую две
фигуры, отыскивая две ближайшие "точки соприкосновения" и
соединяя их.
5. (*2) Модифицируйте пример с фигурами так, чтобы line была
rectangle и наоборот.
6. (*2) Придумайте и реализуйте дважды связанный список, который
можно использовать без итератора.
7. (*2) Придумайте и реализуйте дважды связанный список, которым
можно пользоваться только посредством итератора. Итератор
должен иметь действия для движения вперед и назад, действия
для вставления и удаления элементов списка, и способ доступа к
текущему элементу.
8. (*2) Постройте обобщенный вариант дважды связанного списка.
9. (*4) Сделайте список, в котором вставляются и удаляются сами
объекты (а не просто указатели на объекты). Проделайте это для
класса X, для которого определены X::X(X&), X::~X()
X::operator=(X&).
10. (*5) Придумайте и реализуйте библиотеку для написания
моделей, управляемых прерываниями. Подсказка: . Только
это - старая программа, а вы могли бы написать лучше. Должен
быть класс task (- задача). Объект класса task должен мочь
сохранять свое состояние и восстанавливаться в это состояние
(вы можете определить task::save() и task::restore()), чтобы
____________________
* К сожалению, об этом присваивании легко забыть. Например, в
первом издании этой книги (английском - перев.) вторая строка
конструктор derived::derived() читалась так:
if (this == 0) this = (derived*)43;
И следовательно, для d конструктор базового класса base::base() не
вызывался. Программа была допустимой и корректно выполнялась, но
очевидно делала не то, что подразумевал автор. (прим. автора)
- стр 232 -
он мог действовать как сопрограмма. Отдельные задачи можно
определять как объекты классов, производных от класса task.
Программа, которую должна исполнять задача, модет задаваться
как виртуальная функция. Должна быть возможность передавать
новой задаче ее параметры как параметры ее конструктора(ов).
Там должен быть планировщик, реализующий концепцию
виртуального времени. Обеспечьте функцию задержки
task::delay(), которая "тратит" виртуальное время. Будет ли
планировщик отдельным или частью класса task - это один из
основных вопросов, которые надо решить при проектировании.
Задача должна передавать данные. Для этого разработайте класс
queue (очередь). Придумайте способ, чтобы задача ожидала ввода
из нескольких очередей. Ошибки в ходе выполнения обрабатывайте
единообразно. Как бы вы отлаживали программы, написанные с
помощью такой библиотеки?
Глава 8
Потоки
Язык C++ не обеспечивает средств для ввода/вывода. Ему это и не
нужно; такие средства легко и элегантно можно создать с помощью
самого языка. Описанная зжесь стандартная библиотека потокового
ввода/вывода обеспечивает гибкий и эффективный с гарантией типа
метод обработки символьного ввода целых чисел, чисел с плавающей
точкой и символьных строк, а также простую модель ее расширения для
обработки типов, определяемых пользователем. Ее пользовательский
интерфейс находится в . В этой главе описывается сама
библиотека, некоторые способы ее применения и методы, которые
использовались при ее реализации.
8.1 Введение
Разработка и реализация стандартных средств ввода/вывода для
языка программирования зарекомдовала себя как заведомо трудная
работа. Традиционно средства ввода/вывода разрабатывались
исключительно для небольшого числа встроенных типов данных. Однако
в C++ программах обычно используется много типов, определенных
пользователем, и нужно обрабатывать ввод и вывод также и значений
этих типов. Очевидно, средство ввода/вывода должно быть простым,
удобным, надежным в употребелении, эффективным и гибким, и ко всему
прочему полным. Ничье решение еще не смогло угодить всем, поэтому у
пользователя должна быть возможность задавать альтернативные
средства ввода/вывода и расширять стандартные средства ввода/вывода
применительно к требованиям приложения.
C++ разработан так, чтобы у пользователя была возможность
определять новые типы столь же эффективные и удобные, сколь и
встроенные типы. Поэтому обоснованным является требование того, что
средства ввода/вывода для C++ должны обеспечиваться в C++ с
применением только тех средств, которые доступны каждому
программисту. Описываемые здесь средства ввода/вывода предсавляют
собой попытку ответить на этот вызов.
Средства ввода/вывода связаны исключительно с
обработкой преобразования типизированнных объектов в
последовательности символов и обратно. Есть и другие схемы
ввода/вывода, но эта является основополагающей в системе UNIX, и
большая часть видов бинарного ввода/вывода обрабатывается через
рассмотрение символа просто как набор бит, при этом его
общепринятая связь с алфавитом игнорируется. Тогда для программиста
ключевая проблема заключается в задании соответствия между
типизированным объектом и принципиально нетипизированной строкой.
Обработка и встроенных и определенных пользователем типов
однородным образом и с гарантией типа достигается с помощью одного
перегруженного имени функции для набора функций вывода. Например:
- стр 234 -
put(cerr,"x = "); // cerr - поток вывода обшибок
put(cerr,x);
put(cerr,"\n");
Тип параметра определяет то, какая из функций put будет вызываться
для каждого параметра. Это решение применялось в нескольких языках.
Однако ему недостает лаконичности. Перегрузка операции "" значением
"поместить в" дает более хорошую запись и позволяет программисту
выводить ряд объектов одним оператором. Например:
cerr "" "x = " "" x "" "\n";
где cerr - стандартный поток вывода ошибок. Поэтому, если x
является int со значением 123, то этот оператор напечатает в
стандартный поток вывода ошибок
x = 123
и символ новой строки. Аналогично, если X принадлежит определенному
пользователем типу complex и имеет значение (1,2.4), то приведенный
выше оператор напечатает в cerr
x = 1,2.4)
Этот метод можно применять всегда, когда для x определена
операция "", и пользователь может определять операцию "" для нового
типа.
8.2 Вывод
В этом разделе сначала обсуждаются средства форматного и
бесформатного вывода встроенных типов, потом приводится стандартный
способ спецификации действий вывода для определяемых пользователем
типов.
8.2.1 Вывод Встроенных Типов
Класс ostream определяется вместе с операцией "" ("поместить в")
для обработки вывода встроенных типов:
class ostream {
// ...
public:
ostream& operator""(char*);
ostream& operator""(int i) { return *this"
8.2.3 Некоторые Подробности Разработки
Операция вывода используется, чтобы избежать той многословности,
которую дало бы использование функции вывода. Но почему ""?
Возможности изобрести новый лексический символ нет (#6.2).
Операция присваивания была кандидатом одновременно и на ввод, и на
вывод, но оказывается, большинство людей предпочитают, чтобы
операция ввода отличалась от операции вывода. Кроме того, = не в ту
сторону связывается (ассоциируется), то есть cout=a=b означает
cout=(a=b).
Делались попытки оспользовать операции " и ", но значения
"меньше" и "больше" настолько прочно вросли в сознание людей, что
новые операции ввода/вывода во всех реальных случаях оказались
нечитаемыми. Помимо этого, """ находится на большинстве клавиатур
как раз на ",", и у людей получаются операторы вроде такого:
cout " x , y , z;
Для таких операторов непросто выдать хорошие сообщения об ошибках.
Операции "" и "" к такого рода проблемам не приводят, они
асимметричны в том смысле, что их можно проассоциировать с "в" и
"из", а приоритет "" достаточно низок, чтобы можно было не
использовать скобки для арифметических выражений в роли операндов.
Например:
cout "" "a*b+c=" "" a*b+c "" "\n";
Естественно, при написании выражений, которые содержат операции с
более низкими приоритетами, скобки использовать надо. Например:
cout "" "a^b|c=" "" (a^b|c) "" "\n";
Операцию левого сдвига тоже можно применять в операторе вывода:
cout "" "a"
8.2.4 Форматированный Вывод
Пока "" применялась только для неформатированного вывода, и на
самом деле в реальных программах она именно для этого главным
образом и применяется. Помимо этого существует также несколько
форматирующих функций, создающих представление своего параметра в
виде строки, которая используется для вывода. Их второй
(необязательный) параметр указывает, сколько символьных позиций
должно использоваться.
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 "" "dec(" "" x
"" ") = oct(" "" oct(x,6)
"" ") = hex(" "" hex(x,4)
"" ")";
Если x==15, то в результате получится:
dec(15) = oct( 17) = hex( f);
Можно также использовать строку в общем формате:
- стр 238 -
char* form(char* format ...);
cout"
8.2.5 Виртуальная Функция Вывода
Иногда функция вывода должна быть virtual. Рассмотрим пример
класса shape, который дает понятие фигуры (#1.18):
class shape {
// ...
public:
// ...
virtual void draw(ostream& s); // рисует "this" на "s"
};
class circle : public shape {
int radius;
public:
// ...
void draw(ostream&);
};
То есть, круг имеет все признаки фигуры и может обрабатываться
как фигура, но имеет также и некоторые специальные свойства,
которые должны учитываться при его обработке.
Чтобы поддерживать для таких классов стандартную парадигму
вывода, операция "" определяется так:
ostream& operator""(ostream& s, shape* p)
{
p-"draw(s);
return s;
}
- с
...Закладка в соц.сетях