
- •Часть I. Традиционные Языки Программирования
- •Глава 1. Управляющие структуры. Процедурные абстракции.
- •Базисные свойства языков программирования
- •Процедурные абстракции
- •Передача управления
- •Передача данных
- •Глава II. Основные понятия и проблемы, связанные с типами данных
- •Глава III. Базисные типы данных
- •3.1 Простые типы данных
- •3.1.2. Другие базисные типы данных.
- •Определение новых типов данных
- •Конструкторы.
- •Конструктор умолчания.
- •Конструктор копирования.
- •Конструктор преобразования.
- •Операторы преобразования.
- •Деструкторы
- •Глава 5. Инкапсуляция. Абстрактные типы данных (атд).
- •Модула–2.
- •Оберон.
- •Инициализация статических членов в Java (отступление)
- •Глава 5. Инкапсуляция. Абстрактные типы данных (атд) (продолжение).
- •Язык Java.
- •Глава 6. Раздельная трансляция.
- •Раздельная независимая трансляция
- •Именование
- •Include-файлы
- •Раздельная зависимая трансляция
- •Глава 7. Статическая параметризация.
- •Язык Ада.
- •Глава 7. Исключительные ситуации в языках программирования.
- •1. Объявление исключений.
- •2. Определение исключения.
- •3. Возникновение.
- •4. Распространение и обработка.
- •Часть II. Объектно-ориентированные яп.
- •Глава I. Наследование в яп.
- •1. Каждый объект данных имеет ровно один тип.
- •2. Типы эквивалентны тогда и только тогда, когда их имена совпадают.
- •3. Каждый тип данных характеризуется набором данных и множеством операций.
- •4. Различные типы не совместимы по присваиванию и передаче параметров.
- •Множественное наследование.
- •Глава 2. Динамическое связывание методов.
- •Динамическое связывание методов
- •Снятие механизма виртуального вызова
- •Абстрактные методы. Абстрактные классы.
- •Динамическая идентификация типа.
- •3. Полиморфизм.
Конструктор преобразования.
Почему этот тип конструкторов выделен в отдельный класс, а не отнесен к прочим конструкторам? У конструктора преобразования есть особая семантика, которая тоже иногда настигает программиста и бьет по голове. Дело в том, что в языке С++ есть неявные преобразования, которые были оставлены для совместимости с языком Си. Неявные преобразования, это когда компилятор, вместо присваивания x=y (T1 x; T2 y;) вставляет код x=T1(y) (в Си преобразование типа выглядит иначе: x=(T1)y). В языке Си всегда были следующие неявные преобразования:
char => short => int => float => double
T* => void*
Т.к. более сложных типов в Си не было, то все было нормально. Одной из идей введения классов, было то, чтоб можно было расширять возможности языка без явных добавлений новых возможностей в базис. Например, программу на языке Fortran трудно переписать на язык Си, потому что в Си нет типа Complex. Для работы с комплексными числами, приходится писать специальные функции, и в результате выражения сильно усложняются:
A = B*C + D*(0,1); // Выражение на языке Fortran
A = Plus( Mult(B,C) , Mult(D,Im1) ); // То же самое выражение на языке Си
Разумеется, любой физик откажется писать такие выражения на Си, когда есть более удобный язык Fortran. В С++ концепция класса позволяет смоделировать комплексный тип (и не только комплексный), причем на С++ можно написать выражение, почти эквивалентное соответствующему выражению в языке Fortran. Для этого используется понятие класса, понятие функций-членов, и понятие перекрытия операций. Функции члены можно тоже перекрывать, и мы уже с этим сталкивались – это наличие нескольких конструкторов с одним и тем же именем, но с разным набором параметров. Страуструп разрешил перекрытие любых операций, за исключением трех: "?:" (условная операция), "." (операция точка), ".*" (операция взятия указателя элемента структуры). Остальные знаки операций перекрывать можно (в т.ч. операции умножения, сложения, вычитания, и даже скобки). Для структуры Complex перекрытие оператора сложения можно сделать следующим образом:
Complex operator + (Complex C1&, Complex C2&) { return Complex(C1.Re+C2.Re, C1.Im+C2.Im); };
Такие же перекрытия надо написать для других операций. Возникает проблема: если переменная С в выражении будет вещественной, то как поступить в этом случае? Можно написать, конечно, конструктор преобразования Complex(double,double), но тогда его придется вписывать явно в выражение. Можно для каждого из базисных типов данных написать свой оператор "+" и все прочие операторы, но это приводит к значительному "раздуванию" библиотек.
Лекция 12
Обсудим подробнее конструкторы преобразования. Почему они появились? Изначальной идеей Страуструпа было создать классы так, чтобы с их помощью можно было определять любые типы с произвольной семантикой. При этом новые типы ничем не отличались от базисных с точки зрения эксплуатации. Хорошим примером в данном случае будет тип комплексных чисел
A=B*C+D*(0,1)
Это выражение на языке Fortran, в нем подразумевается, что все переменные имеют тип комплексного числа. Написать подобное выражение на языке, не обладающем гибкостью C++, вообще говоря, сложно. С другой стороны можно добавлять типы данных в базис и интегрировать их с языком, но это не самое лучшее решение.
С этой точки зрения Страуструп обеспечил следующие средства развития. Во-первых, сам механизм классов, во-вторых, перекрытие операций. То есть можно перекрывать произвольные функции и знаки операций, причем, функции операции могут быть, как функциями членами, так и глобальными.
Мы писали уже перекрытие оператора «+» для комплексных чисел, он у нас выглядел, как функция-член. Это не совсем хорошо, но об этом мы еще поговорим.
С точки зрения языка Fortran, такое решение выглядит далеко не самым лучшим, так как в этом случае, если A,B,C,D - комплексные
Complex A,B,C,D;
(для определения константы следует воспользоваться конструктором)
Выражение на C++ примет вид:
A=B*C+D*Complex(0,1);
Вообще говоря, тут можно было бы схитрить и перекрыть операцию «,», но это ящик Пандоры, так как запятые используются не только для связывания комплексных констант. Можно поступить по-другому и описать константу следующим образом:
const Complex Im1(0,1);
Однако, что еще есть в Fortran? Там есть еще неявное преобразование типов (которое есть практически в любом языке). Естественно, в случае, если A и D – комплексные, а B и C – float, то наше выражение будет компилироваться с ошибкой. Получается, что нам следует определять операции сложения, умножения и т.д. для всех базисных типов данных. Библиотеки, таким образом будут неимоверно разрастаться. В результате Страуструп разрешил пользователю управлять неявными преобразованиями типов, так как если вспомнить философию языка C++, то основной ее тезис – сделать язык удобным для программирования, а удобство означает прежде всего – большие мощность и гибкость, чтобы человек мог сделать все то, что он хочет.
Итак, неявные преобразования программист может разрешать и составлять сам. Для этого ему и служит конструктор преобразования.
Пусть у нас есть A+B, где A и B – double числа.
+(Complex, Complex)
Если наш конструктор находит Complex(double) (а он его, конечно, найдет, правда, по хитрым правилам), то в этом случае компилятор проведет замену:
A+B => Complex(A)+Complex(B)
проблема решена. Конечно, недостаточно написать Complex(double), следует еще написать Complex(float), Complex(int) – и т.д. Тогда у нас полностью обеспечивается гибкость Fortran.
Возникает вопрос, почему бы не разрешить
1+C ~ Complex(double(1)) + C?
Дело в том, что здесь мы имеем цепочку преобразований, сначала базисное, затем определяемое пользователем, а в больших проектах может быть не одно преобразование, а в общем случае некоторый граф преобразований, возможно, с циклами, что, конечно же, позволяет совершить массу ошибок. Поэтому разрешаются только одношаговые преобразования.