
- •Часть 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. Полиморфизм.
Операторы преобразования.
Естественно, коль скоро мы говорим о преобразованиях, связанных с конструктором, когда у нас есть объект какого-то типа и компилятор находит соответствующие неявные преобразования, то имеет смысл говорить и об операторах преобразования, т.к. в некоторых случаях совершенно не нужно вызывать конструкторы.
Оба методы вполне допустимы. Что делает конструктор преобразования? Он создает временный объект ( в нашем примере – Complex(A) ), имея
C=A;
где C – комплексное, а A – целое, мы получим:
C=Complex(A);
то есть некоторый объект, который будет удален неизвестно когда (на усмотрение компилятора). Конечно, для комплексных чисел проблемы большой не видно. А если объект имеет достаточно сложную структуру? Тогда речь идет о том, что на базе одного объекта создается другой, и это может оказаться накладно. Поэтому есть так называемые операторы преобразования. Это некая функция-член, которая выглядит следующим образом:
operator T();
T – это тип, к которому происходит преобразование. Параметров нет. Явных. Есть один неявный – указатель на самого себя this.
Например:
class MyString {
MyString( char * );
MyString( const MyString *);
operator char*();
};
В данном случае оператор преобразования – char* (). Чем он удобнее в данном контексте? Пусть нам надо выдать строку (будем выдавать ее через безопасный вывод, через класс ostream с перекрытой операцией вывода):
ostream S;
ostream& operator << (const char *)
так как у нас используется ссылка, то мы можем сделать такой вывод:
S << “Hello!” << ‘\n’ <<”World?”;
Это альтернатива стандартного ввода вывода. И вот проблема: мы не можем изменить класс ostream, но очень хотим уметь выводить наши строки MyString (вполне невинное желание). Значит, нам нужно из класса ostream выводить новый класс (только для того, чтобы переопределить операцию вывода). Если у нас встречается конструкция:
MyString Str;
S << Str;
то можем ли мы воспользоваться конструктором преобразования, определенном в классе MyString? Нет. Ведь нам надо сделать преобразование из MyString в (char *), а конструктор делает наоборот. В данном случае помочь может только оператор преобразования. Если он есть, то мы получим то, что хотели:
(char *) Str;
То есть оператор и конструктор преобразования – обратные операции. Конструктор делает из «чужого» объекта «свой», а оператор – наоборот.
Уместно поговорить о константах. Так как свойство константности имеет место и при преобразованиях, то есть, когда компилятор вставляет код к неявному преобразованию константы, то получившийся объект продолжает оставаться константой. С этой точки зрения, если мы описываем функцию:
f(MyString & S);
а так как у нас есть операторы преобразования, то напишем:
f(“Hello!”);
в результате… сообщение об ошибке. В чем ошибка? В данной ситуации происходит преобразование (char *) => MyString, то есть работает конструктор. Что здесь получилось:
f( MyString(“Hello!”));
а так как свойство константности остается, то фактически мы создали константу типа MyString, компилятор смотрит на соответствующий профиль класса ( MyString( char *) ), но там стоит не константа. Получаем ошибку. Каким образом это лечится? Добавлением const в описание функции:
f(const MyString & S);
Дело в том, что прежнее описание f говорит о том, что мы собираемся изменять переменную S. А второе описание говорит, что мы передаем по ссылке, не собираясь ничего изменять, а только лишь для избежания накладных расходов, которые неизбежно возникают при передаче параметра по значению.
Теперь давайте поговорим о том – хорошая или плохая штука эти стандартные преобразования. Заметим, что в Modula-2 и Oberon неявные преобразования вообще запрещены. Разрешены только преобразования числовых типов, при которых не теряется точность (int -> longint ->real -> long real). Что же говорить о преобразованиях, определенных пользователем? Пусть у нас есть:
Stack(25);
у класс Stack есть конструктор, который оказывается конструктором преобразования:
Stack(int);
хотя сложно было бы подумать, что процесс порождения стека – перевод некоторого числа (длины стека) в некий объект. Но по правилам языка это можно трактовать именно так.
Посмотрим на следующее:
S=11;
Что хотелось сделать? Кажется – описка. Компилятор же преобразует данную строку в:
S = Stack(11);
Почему это произойдет? А дело в том, что при присваивании объекту типа Stack числа типа int произойдет автоматический вызов конструктора преобразования, который в нашем случае еще и создает стек заданной длины. Таким образом наш старый стек пойдет ко дну. С содержательной стороны, конечно же, хотелось бы видеть сообщение об ошибке. В последних версиях C++ эта проблема решается с помощью ключевого слова explicit:
explicit Stack( int );
В этом случае будет разрешено только явное использование данного конструктора при преобразованиях, и выражение:
S=11;
где подразумевается неявный вызов, повлечет за собой ошибку, что не найдет соответствующий оператор преобразования.
Отнюдь не случайно вопрос о неявных преобразованиях дискутируется в кругах разработчиков языков программирования. Заметим, что в языке Java, который можно рассматривать, как наследника C++. Однако его создатели ставили себе целью не просто создать язык, который был бы удобен некоему классу пользователей, а более жесткую задачу – ЯП, который можно было использовать в интернет с концепцией: «написал один раз, выполняешь – везде». К тому же создаетли Java не ориентировались на совместимость с C++, в то время как C++ был создан, как надстройка С и должен был быть совместим с ним.
В языке Java неявных преобразований нет, слишком уж это опасная штука.
Когда еще возникают неявные преобразования? Пусть у нас есть некий конструктор
X (int).
и функция:
f( x&);
Тогда
f(5) ~ f(X(5));
и будет произведена генерация некотрого временного объекта. Опять же – тут следует быть осторожным, если мы не хотим вызова X(), то надо использовать explicit. Однако и это не решает всех проблем. Вернемся к классу MyString:
class MyString {
char * body;
…
operator char* ( ) {return body; }
…
}
Естественно, написать функцию “+” для конкатенации двух строк:
MyString S(“Hello, ”);
S+”World!”
Если у нас есть функция “+” перекрытая с аргументами (string, string), то будет найден конструктор преобразования, который сгенерирует:
S+MyString(“World!”);
Пусть теперь у нас есть некая функция :
char * f( char *) {…; return t;}
где t – параметр, переданный функции (то есть функция возвращает то, что в нее передали).
Тогда будет выполнено следующее преобразование:
P = f(S+”Hello”) ~ f( (char*) tmp),
где
tmp=S+MyString(“Hello”); - временный объект типа MyString.
Таким образом мы в f передаем указатель на временный объект (char *) tmp и сама функция вернет нам значение именно этого указателя. То есть P будет указывать на (char *) tmp, временный объект в куче! В какой момент P прекратит существование? Никто не знает. Например, исходя из практики, компилятор Visual C сразу выполняет деструктор временного объекта по выходу из функции, компилятор Borland ждет конца блока.
Вобщем, не исключено, что после такого вызова P будет ссылаться черт знает куда. Хотя вроде бы мы не обязаны знать внутреннего устройства: у нас есть функция f, строки – а P ссылается неизвестно куда, и мы не можем им пользоваться. Эта беда случилась исключительно из-за неявных преобразований. Как только мы введем переменную tmp сами, то будем знать, что P живет столько же, сколько и P.
Какого рода еще есть проблемы?
Вернемся к нашему конструктору преобразования X:
X (int)
X (const X&)
Рассмотрим присваивание:
X x=5;
Здесь генерируется следующий код:
X tmp(5);
X x=tmp; - конструктор копирования
~tmp; - деструктор tmp
Что здесь можно оптимизировать? В данном случае оптимизация будет следующая:
X x(5);
то есть конструктор преобразования будет работать сразу для переменной x, минуя создание и удаление временной переменной tmp, что является хорошей экономией.
Эти все вещи специфичны только для языка C++. Заметим, что даже такая красивая концепция, как классы, способна испортить программисту жизнь.