класса, который может значительно выиграть от того, что программист
возьмет под контроль управление свободной памятью. Для этого вида
объектов идеально подходит оптимизирующий метод, который описан в
#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 Форматированный Вывод
Пока << применялась только для неформатированного вывода, и на
самом деле в реальных программах она именно для этого главным