1 Структурные типы данных

advertisement
1
Структурные типы данных
Одним из центральных понятий современных языков программирования является понятие абстракции данных. Оно
позволяет программисту решать поставленную перед ним задачу в терминах самой задачи, а не в терминах компьютера, на котором задача решается. Например, если нам нужно написать программу для учета автомобилей на складе
автомагазина, мы можем захотеть иметь описание всех тех автомобилей, которые у нас имеются в наличии. Если
мы решаем задачу в терминах компьютера, нам необходимо завести несколько одинаковых по количеству элементов
массивов, каждый из которых хранит один из аспектов наших автомобилей — один массив хранит цены (целые числа), другой — цвета (строки), третий — мощность двигателя (вещественные числа), и так далее. Если таких массивов
потребуется достаточно много, неудобство этого подхода очевидно.
На практике тот факт, что язык программирования поддерживает абстракцию данных, выражается в возможности создания новых типов данных, значения которых способны более-менее точно (с точки зрения задачи) описывать
объекты реального мира. Как правило, это структурные типы данных, т. е. наборы переменных с заданными программистом именами и типами из числа базовых типов языка или уже созданных к данному моменту пользовательских
типов.
Вообще, как правило, в большинстве языков программирования переменная структурного типа представляет собой
набор вложенных переменных, каждая из которых имеет имя и тип, структурный или базовый (такой, например, как
целое или вещественное число). Каждая переменная структурного типа имеет свои значения всех вложенных переменных, т. е. одна и та же вложенная переменная в составе разных переменных структурного типа может иметь разные
значения. Из этого правила могут быть исключения (так называемые статические поля), которые будут обсуждаться
позже в отдельном разделе.
Сам структурный тип данных определяет, какие именно вложенные переменные содержатся в каждой переменной
этого типа. Это значит, что любые две переменные одного структурного типа имеют один и тот же состав, т. е.
состоят из одного и того же набора вложенных переменных (с теми же именами и типами), возможно, имеющих
разные значения.
Сами вложенные переменные обычно называются полями большой структурной переменной, их содержащей. Поля
могут иметь любые типы, в том числе структурные; наличие у одной структурной переменной полей другого структурного типа называется композицией. В языке C++ не допускается, чтобы структурный тип данных имел поле того
же типа; более того, запрещены даже циклические зависимости такого рода, т. е. например, два структурных типа,
каждый из которых имеет поле другого типа из них.
Продолжая пример с автомобилями, теперь мы можем определить структурный тип данных «автомобиль», содержащий поля, описывающие разные аспекты автомобиля, которые могут интересовать потенциальных клиентов:
например, поле price (целого типа) может содержать цену, поле color (строка) — цвет, и т. д.
Поддержка языком программирования абстракции данных обычно не ограничивается возможностью создавать
новые типы данных, но простирается дальше и обеспечивает возможность работать с этими новыми типами так же,
как и с любыми другими, например встроенными. В частности, это означает возможность создавать в программе
переменные таких типов, возможность передавать их в качестве параметров подпрограммам и возвращать в качестве
результатов, возможность присваивать значение одной переменной структурного типа другой переменной того же
типа и т. д.
Двигаясь далее в примере с автомобилями, мы можем теперь завести массив автомобилей, т. е. переменных, каждая
из которых имеет тип «автомобиль». Кроме того, мы можем передавать автомобили в функции в качестве параметров,
возвращать их в качестве результатов, присваивать одной переменной типа автомобиль значение другой переменной
такого же типа. Это все и позволяет нам наконец писать нашу программу в терминах автомобилей, а не в терминах
встроенных типов конкретного языка программирования — символьных, целых, вещественных, логических переменных и иногда строк.
Как и в большинстве императивных языков программирования, в C++ имеются структурные типы данных; обычно, для обозначения этого понятия в языке C++ используется термин «структуры». Структуры похожи на массивы
тем, что позволяют объединять под одним именем переменной несколько переменных. На этом, однако, сходство с
массивами и заканчивается. В отличие от массивов, объединяемые в одну структуру переменные могут иметь разные типы, и доступ к ним, в отличие от массивов, происходит с использованием специальных идентификаторов, а не
номеров.
Как и для переменных или функций, существуют объявления и определения структур. Объявление структурного
типа данных выглядит следующим образом:
struct имя;
Этого достаточно только для объявления или определения указателей на данный структурный тип. Для того, чтобы
определять переменные данного структурного типа, нужно его определение, которое выглядит так:
struct имя { тело } переменные;
Тело структурного типа данных, в частности, может содержать определения полей, в том числе битовых. Под полями (термин из теории баз данных) структуры понимаются те переменные, которые объединяются в структурную
переменную.Определение поля выглядит как обычная декларация переменной. Старый стандарт 1998 года (как и 2003
года) запрещает инициализировать поля структурных типов данных непосредственно в теле такого типа, предписывая
любую инициализацию производить в определении конструкторов — см. дальше. Новый стандарт 2011 года позволяет
инициализировать поля прямо в теле структурного типа. Такая инициализация используется, если в конструкторе
пропущен инициализатор (что это такое, см. дальше) для данного поля.
Если у нас есть переменная типа структуры, для доступа к полям используется символ «.», за которым следует
имя интересующего нас поля. Кроме того, в C++ имеется удобная операция для доступа к полю той структуры,
на которую указывает указатель. Если p — указатель на структуру, а f — поле этой структуры, то выражение p->f
означает поле f той структурной переменной, на которую указывает p.
Например, следующая структура описывает комплексное число:
struct Complex { double re, im; };
Каждая переменная только что описанного типа состоит из двух вещественных переменных с именами re и im.
Если мы заведем теперь переменную z типа Complex, написав
Complex z;
мы на самом деле получим две вещественные переменные с именами z.re и z.im. Работать с этими переменными можно
вполне также, как и с обычными вещественными переменными, т. е. мы можем присваивать им вещественные числа,
считывать их значения при помощи операторов ввода, или использовать их значения в выражениях или операторах
вывода.
Теперь уместен вопрос: где же удобство? На первый взгляд, ситуация только ухудшилась: мы написали несколько
лишних строк кода и в результате получили те же переменные, но с длинными, составными именами. Ответ на этот
вопрос состоит в следующем: удобство такого подхода начинается, когда мы начнем передавать наши переменные
в функции в качестве параметров. Дело в том, что переменные структурных типов данных можно передавать в
функции, равно как и возвращать их из функций, ровно также, как и обычные, не структурные (или, как еще говорят,
скалярные) переменные, скажем, целого или вещественного типа. В итоге, полученный код резко упрощается. Для
сравнения можно привести два варианта организации суммы трех комплексных чисел:
// --- без структур
void add1(double r1,
double &r,
{
r = r1 + r2;
i = i1 + i2;
}
...
/* нужно сложить три
a + b i, c + d i
записывается в g
--double i1, double r2, double i2,
double &i)
комплексных числа:
и e + f i; результат
+ h i
понадобятся две вспомогательные вещественные
переменные x и y */
add1(a, b, c, d, x, y);
add1(x, y, e, f, g, h);
// --- со структурами --Complex add2(Complex z1, Complex z2)
{
Complex res;
res.re = z1.re + z2.re;
res.im = z1.im + z2.im;
return res;
}
/* нужно сложить три комплексных числа:
z1, z2 и z3; результат записывается в z4
вспомогательные переменные не понадобятся */
z4 = add2( add2(z1,z2), z3 );
Второй вариант незначительно труднее в реализации, но зато много проще в использовании. Конечно, можно было
бы возразить, что трудности в первом варианте надуманные, для решения задачи достаточно написать
g = a+c+e;
h = b+d+f;
вообще без всяких функций, однако трудности такого подхода резко возрастают при усложнении той операции, которую нужно вычислять (например, во что превратится этот подход, если нужно вычислить не сумму, а, скажем,
произведение трех комплексных чисел? А четырех?).
Кроме того, язык C++ позволяет еще упростить использование структурных переменных. Здесь имеются в виду
временные безымянные объекты, а также перегрузка операций. Забегая вперед, скажем, что, используя перегрузку
операций, можно вообще складывать комплексные числа при помощи «+», а умножать — «*».
Задача.
Написать структурный тип данных, описывающий автомобиль (в нем удобно будет, кроме полей, описывающих
разные свойства автомобиля, иметь одно дополнительное логическое поле — зачем оно, будет ясно дальше). Написать также функции, принимающие в качестве параметра массив автомобилей и число его элементов и делающие
следующее:
а) Инициализация. У каждого автомобиля из массива дополнительное логическое поле устанавливается в false, что
означает, что массив пока пуст (ни одна из его ячеек пока не используется).
б) Заполнение склада. Пробежаться по массиву и для каждой ячейки, логическое поле которой имеет значение
false, сделать следующее: ввести с клавиатуры значенияя полей, и логическое поле установить в true.
в) Печать информации. Пробежаться по массиву и для каждой ячейки, логическое поле которой имеет значение
true, напечатать номер в массиве и информацию о соответствующем автомобиле.
г) Продать автомобиль. Ввести с клавиатуры его номер в массиве и проверить, что автомобиль с указанным
номером имеется на складе, т. е. у соответствующего элемента массива логическое поле имеет значение true. Если это
так, то продать автомобиль, т. е. установить его логическое поле в false. Иначе выдать сообщение об ошибке.
В функции main завести массив из 10 автомобилей, и организовать цикл, на каждой итерации которого пользователю выдается меню из пяти пунктов (четыре вышеописанных функции и выход из программы), вводится выбор
пользователя и вызывается выбранная пользователем функция, до тех пор, пока пользователь не выберет выход.
Кроме только что рассмотренных обычных полей, существуют также так называемые битовые поля.
Битовые поля — это поля, хранящие целое число (знаковое или нет) и занимающие указанное число битов. Например, следующая структура имеет три битовых поля, каждое из которых занимает два бита; поскольку они беззнаковые
(для знаковых надо писать int), диапазон значений каждого из них — от 0 до 3:
struct bitfields { unsigned a:2, b:2, c:2; };
Битовые поля, чаще всего, используются для экономии памяти. Используя битовые поля, в одном байте можно хранить
8 логических переменных, тогда как если каждую из них определять с типом bool, они в совокупности займут 8 байтов. Также битовые поля используются для программирования аппаратуры на низком уровне (на уровне регистров),
поскольку один регистр, хранящий небольшое целое число, может делиться на некоторое число полей, управляющих
независимыми функциями аппаратуры. В этом случае может возникнуть потребность пропустить некоторое число
битов между соседними битовыми полями; для этого можно воспользоваться безымянным битовым полем, определяемым также, как и обычное, но без имени:
struct bitfields { unsigned a:2, :3, c:2; };
Здесь между a и c будут пропущены 3 бита.
Битовым полям можно присваивать значения или использовать их значения в выражениях. Однако, применять к
ним операцию взятия адреса & нельзя, поскольку у битового поля, вообще говоря, нет своего адреса: адрес имеется
только у всей ячейки целиком, а не у ее частей.
Всякое поле может быть обычным или статическим. Статическое поле, в отличие от обычного, копия которого
имеется в каждой структурной переменной данного типа, существует в единственном экземпляре для всех переменных
данного типа, и соответствующие выражения доступа к этому полю для всех переменных данного типа — синонимы.
Статические поля, кроме своего определения в структуре, должны еще быть объявлены как обычные глобальные
переменные; при таком объявлении это поле может быть инициализировано. Кроме того, доступ к ним возможен не
только через переменную структурного типа, но и через сам тип. Используются такие поля в основном для того,
чтобы хранить информацию обобщающего характера обо всех переменных данного типа. Например, в таком поле
можно хранить число всех существующих в программе переменных данного типа и использовать его для того, чтобы
снабдить каждую такую переменную «серийным номером».
В качестве примера рассмотрим структурный тип данных, в котором используется статическое поле.
struct stfields
{
int a;
static int b;
} x, y;
int stfields::b = 7;
Здесь мы определяем структурный тип данных, в котором два поля. Одно поле, a, — обычное, а другое, b, — статическое. Здесь же определяются две переменные такого структурного типа. Так вот, поле a у каждой из этих переменных
свое, и x.a и y.a — две разных целых переменных; а поле b у всех переменных типа stfields общее, и кроме x.b и
y.b у него есть еще один синоним — stfields::b, который и используется для определения этого поля как глобальной
переменной.
Абстракция данных позволяет сильно упростить процесс программирования, однако в этом направлении можно
пойти дальше. А именно, можно ввести понятие методов — функций, связанных с конкретным структурным типом
данных. Такие функции похожи на обычные, но имеют дополнительный неявный параметр, называющийся this и
представляющий собой указатель на тот объект (переменную данного структурного типа), для которого вызывается
метод.
Кроме описания полей, в теле структурного типа данных могут встречаться объявления и определения методов.
Синтаксис у этих элементов структурного типа данных такой же, как и у объявлений и определений обычных функций. Однако, тот факт, что эти объявления или определения функций находятся внутри структуры, автоматически
означает, что это методы. Например, следующая структура имеет метод, выводящий единственное поле переменной
данного типа на экран:
struct sout
{
int a;
void print() { cout<<a; }
} x, y;
Тела методов могут быть написаны внутри описания структуры; в этом случае такие методы автоматически считаются подставляемыми функциями (как если бы было указано ключевое слово inline — см. файл про функции). Однако,
можно поступать и по-другому — внутри структуры размещаются объявления методов, а определения — снаружи. В
таком стиле определение предыдущей структуры будет выглядеть так:
struct sout
{
int a;
void print();
} x, y;
void sout::print() { cout<<a; }
В этом случае, при использовании раздельной компиляции, определение метода, как и любой другой функции, должно
встречаться только в одном исходном файле.
Теперь добавим в структурный тип данных, описывающий комплексные числа, метод, вычисляющий модуль комплексного числа. После этого его описание будет выглядеть так:
struct Complex { double re, im; double abs(); };
Таким образом мы заявляем, что в классе Complex имеется метод abs без параметров, возвращающий вещественное
число. Но чтобы пользоваться этим методом, его надо реализовать, т. е. написать
double Complex::abs()
{
return sqrt(re*re+im*im);
}
Эта запись показывает, что должен делать наш метод, определяя его тело, т. е. из каких операторов он состоит.
Здесь надо отметить ряд моментов. Во-первых, в качестве имени функции указано не просто abs, а Complex::abs. Это
и означает, что это не простая функция abs, а метод abs класса Complex. Далее, в теле этого метода мы пользуемся
полями класса Complex, не указывая объекта, к которому они относятся. Действительно, каждый метод вызывается
не просто, как функция, но для конкретного объекта своего класса, и поля класса внутри метода, для которых объект
не указан, означают поля того объекта, для которого вызван метод.
Вызывается метод следующим образом: объект.метод(параметры). В нашем случае это может выглядеть примерно
так:
int main()
{
Complex z;
z.re = 1; z.im = 2;
cout<<"|z|="<<z.abs()<<endl;
return EXIT_SUCCESS;
}
Так что при вызове метода abs для объекта z внутри тела метода переменные re и im означают соответствующие
поля переменной z. Реализовано это при помощи дополнительного неявного параметра this, который передается в
метод и содержит указатель на тот объект, для которого метод был вызван. Этим указателем можно пользоваться
внутри метода и явно, так что в данном случае запись im внутри метода abs означает то же, что и более длинная
запись this->im.
Задача.
Переделать решение предыдущей задачи следующим образом. Написать новый структурный тип данных «склад
автомобилей», каждый объект которого содержит массив из 10 автомобилей, и переписать функции из предыдущей
задачи как методы нового структурного типа, соответствующим образом изменив функцию main.
Одной из весьма привлекательных черт языка программирования С++ является возможность перегружать методы, как и обычные функции. Эта возможность позволяет иметь несколько разных методов с одним и тем же именем,
но с разными наборами типов параметров. Различие в типах возвращаемых значений или именах параметров достаточным для перегрузки не является; если имеются два метода, отличающиеся только типами возвращаемых значений
или именами параметров, такая ситуация рассматривается компилятором как ошибка.
Как и поля, методы тоже могут быть статическими. Такие методы, хотя и могут быть вызваны для конкретного
объекта, не имеют неявного параметра this и поэтому не могут иметь доступа к нестатическим полям и методам структурного типа. Их также можно вызвать не через объект, а через структурный тип, написав тип::метод(параметры).
Среди всех методов выделяются специальные, которые называются конструкторами и деструкторами. Конструкторы предназначены для создания объектов. Эти методы обеспечивают правильное заполнение полей объектов начальными значениями. Кроме того, они же могут использоваться для выделения определенных ресурсов, необходимых
объекту, например, открытия файлов. Иногда конструкторы в процессе своей работы создают другие объекты, если
они так или иначе входят в состав создаваемого объекта.
Конструкторы вызываются автоматически при создании новых объектов, например, при определении переменных
структурного типа или создании динамических переменных. Для запуска определенного конструктора при создании
переменной параметры конструктора указываются в скобках после ее имени:
тип имя(параметры конструктора);
При создании динамической переменной — после типа:
указатель = new тип(параметры конструктора);
Если планируется вызвать конструктор без параметров, пустые скобки лучше не ставить — запись
тип имя();
понимается как прототип функции без параметров с указанным именем, возвращающей объект указанного типа.
По смыслу своей работы конструктор отличается от всех остальных методов тем, что он вызывается для создания
объекта, и в момент его вызова память под поля объекта уже выделена, но эти поля еще не заполнены правильным
образом.
Конструкторы описываются с именем, совпадающим с именем структуры, и без типа возвращаемого значения —
вместо него не пишется ничего, даже void.
Правило перегрузки методов касается и конструкторов. Это означает, что можно иметь несколько разных конструкторов, но любые два из них должны отличаться набором типов параметров.
Среди всех конструкторов выделяется ряд особых случаев. Первый из них — конструктор по умолчанию, т. е. конструктор без параметров. Он может быть создан компилятором автоматически из конструкторов по умолчанию для
отдельных полей, если в структуре нет никакого конструктора. Его наличие было критическим в старом стандарте
С++, поскольку при создании массива из элементов структурного типа (неважно, обычный массив создавался или
динамический) его элементы могли быть инициализированы только конструктором по умолчанию. В новом стандарте массив можно инициализировать при помощи так называемых списков инициализации, так что необходимость в
конструкторе по умолчанию несколько ослабла.
Следующий особый вид конструктора — конструктор копирования. У этого конструктора имеется один параметр
типа ссылки на тот структурный тип, к которому он относится (иногда — ссылки на константу этого типа). Конструктор копирования может быть создан компилятором автоматически, исходя из конструкторов копирования для
отдельных полей, однако часто встречаются случаи, когда этот конструктор нужно писать вручную.
Чтобы рассказать об этих случаях, нужно обсудить понятия больших и маленьких объектов. Начнем с маленьких.
Маленький объект — это такой объект, который полностью умещается в отведенной под его поля памяти. Типичным
примером является комплексное число — вся его информация содержится в двух его полях, и он не имеет никаких
внешних частей. В противоположность ему, большой объект обычно имеет в своем составе указатели или ссылки на
внешние части — другие объекты, относящиеся к данному, хозяином которых он является. Термин «хозяин» в данном
случае означает, что при уничтожении большого объекта все те другие объекты, хозяином которых он является, тоже
должны быть уничтожены.
Забегая вперед, в качестве типичного примера большого объекта приведем связный список. Сам объект связного
списка содержит обычно всего лишь указатель на первый элемент списка — другой объект, имеющий специальный
вспомогательный тип «узел списка» с двумя полями — данные в узле и указатель на следующий узел. Итак, сам объект списка содержит лишь один указатель, но список состоит не только из этого указателя, но из некоторого (иногда
довольно большого) числа узлов, раскиданных по памяти компьютера в разных местах. Объект списка является хозяином этих объектов, поскольку при уничтожении списка все его узлы тоже должны быть уничтожены во избежание
утечки памяти.
Теперь все готово к тому, чтобы объяснить, зачем связному списку самодельный конструктор копирования. Тот
конструктор копирования, который строится компилятором, просто будет копировать указатели. В итоге получится
несколько (возможно, больше, чем один) объектов списка, состоящих из одних и тех же узлов (не одних и тех же
данных в узлах, а именно тех же узлов — тех же объектов, сидящих по одному и тому же адресу!). И если теперь
уничтожить один из этих списков, эти узлы тоже будут уничтожены, а все остальные списки будут указывать на
освобожденную память — такие указатели называются висячими.
Например, такая ситуация может возникнуть при попытке передать связный список в функцию в качестве параметра по значению. При передаче параметра по значению для инициализации формального параметра используется
как раз конструктор копирования, так что если пользоваться автоматически созданным конструктором копирования,
фактический параметр и формальный будут иметь одни и те же узлы. Но формальный параметр существует по тем
же законам, что и обычная локальная переменная — при завершении работы функции он уничтожается, и вместе с
ним должны быть уничтожены его узлы. В итоге указатель у фактического параметра становится висячим, что плохо.
Так что для больших объектов, скорее всего, должен быть написан самодельный конструктор копирования. Для
связного списка он должен создавать новый список из такого же числа узлов, как и тот, который копируется, и
переписывать данные из узлов старого списка в узлы нового.
Новый стандарт вводит еще один вид специального конструктора — конструктор перемещения (у него единственный параметр — ссылка на значение, а не на переменную — используется && вместо &). Этот вид конструктора
позволяет сэкономить время на копировании больших объектов в тех случаях, когда нужно копировать объекты,
которые не будут использованы после этого копирования. Например, у меня есть функция, возвращающая связный
список, и я делаю в конце return l; где l — локальная переменная типа связный список. По правде, при возврате
результата из функции (по значению) используется конструктор копирования, так что этот список будет скопирован
во временную переменную, и почти сразу же уничтожен, поскольку локальные переменные подлежат уничтожению
по выходе из той функции, где они определены. Однако, эти действия (копирование списка и сразу же за тем уничтожение того списка, который копировался) выглядят лишней работой. Для избежания ее и был придуман конструктор
перемещения. Смысл его в том, что он берет в свой объект узлы того связного списка, который был его параметром,
а у него сбрасывает указатель на первый элемент в nullptr, чтобы эти узлы не были уничтожены тем кодом, который
будет заниматься уничтожением локальных переменных (забегая вперед, скажем, что этим занимается другой вид
специальных методов — деструкторы).
Чтобы эта механика работала, нужно сделать две вещи — во-первых, написать грамотный конструктор перемещения, и во-вторых, указать кмпилятору на возможность его использования, написав return move(l);
Наконец, любой другой конструктор с одним параметром называется конструктором преобразования. Это название
объясняется тем, что такие конструкторы могут использоваться для преобразования типов аргументов при вызове
функций.
Например, пусть у нас есть два типа A и B, причем у второго есть конструктор преобразования с аргументом типа
первого:
struct A {};
struct B { B(A); }
Пусть также у нас есть функция с параметром типа B и переменная типа A:
void f(B);
A x;
Тогда мы можем вызвать функцию f с параметром x, и вместо f(x) (написанного в нашей программе) компилятор
будет понимать f(y), где у — временная переменная, созданная при помощи конструктора преобразования B y(x);
Однако не всегда бывает удобно иметь много конструкторов преобразования. Дело в том, что по правилам вызова
функций в С++ при вызове функции рассматривается не более одного преобразования каждого параметра, и если из
полученных таким образом наборов аргументов подходит для вызова имеющихся функций только один, он и берется
в качестве действующего; но если подходят два или более вариантов, выдается сообщение об ошибке.
Чтобы избежать чрезмерного количества возможных преобразований, некоторые конструкторы преобразования
можно пометить ключевым словом explicit — такой конструктор преобразования уже нельзя использовать для автоматического преобразования типов, хотя его по-прежнему можно вызвать явно.
В языке С++ конструктор может быть использован и для создания так называемых временных безымянных
объектов. Например, если добавить в структуру комплексных чисел конструктор по значениям вещественной и мнимой
частей:
struct Complex { double re, im; double abs();
Complex(double r, double i): re(r), im(i) {} };
то функцию, складывающую комплексные числа, можно сильно упростить:
Complex add2(Complex z1, Complex z2)
{
return Complex(z1.re + z2.re, z1.im + z2.im);
}
Здесь для инициализации полей структуры Complex применены так называемые инициализаторы (выражения
между заголовком конструктора и его телом, выглядящие как имя поля и за ним в скобках выражение для инициализации). Они позволяют выполнить произвольный конструктор для каждого конкретного поля. Если для какого-то
поля инициализатора нет, используется конструктор по умолчанию (по старому стандарту; по новому, используется
инициализация, указанная в определении соответствующего поля в теле структурного типа данных, если она есть).
Вообще, перед запуском конструктора объекта поля этого объекта уже должны быть инициализированы при помощи
конструкторов их типов. Именно эти конструкторы и могут быть вызваны при помощи инициализаторов. Кстати,
порядок вызова инициализаторов всегда таков, в котором поля следуют в теле структурного типа. При этом порядок
следования инициализаторов в конструкторе не имеет никакого значения.
Наконец, можно сказать здесь и о таком явлении нового стандарта C++, как списки инициализации, а заодно и
об универсальном виде инициализации.
В новом стандарте C++ имеется специальный тип для списков инициализации, который называется initializer_list<тип
элемента>. Это тип, к которому относятся списки из соответствующего типа элементов, в фигурных скобках и через
запятую. Например, выражение {1,2,3} теперь (по новому стандарту C++) имеет тип initializer_list<int>.
Если этот тип используется в конструкторе в качестве единственного параметра, то объект такого типа может
быть инициализирован при помощи соответствующего списка инициализации. Например, объект библиотечного класса
вектор может быть инициализирован так:
vector<int> v { 1, 2, 3 };
Теперь и для старого способа инициализации вводится новая форма с фигурными скобками вместо круглых для указания параметров конструктора. Этот способ имеет то преимущество перед старым, что теперь объявление переменной
не путается с объявлением функции. Правда, у нового способа указания параметров конструктора есть и недостатки: если все параметры конструктора имеют один и тот же тип, то вызов такого конструктора можно перепутать со
списком инициализации из элементов соответствующего типа. Поскольку списки инициализации имеют предпочтение
перед другими параметрами конструктора, в этом случае для указания параметров конструктора нужно пользоваться
старым вариантом с круглыми скобками.
Тот факт, что теперь списки инициализации имеют свой тип, приводит к тому, что теперь можно использовать
этот тип и в качестве типа параметра функции. При вызове такой функции на месте соответствующего параметра
должен стоять подходящий по типу элементов список инициализации.
Деструкторы имеют назначение, противоположное назначению конструкторов — они используются для грамотного
уничтожения объектов, которые больше не нужны. Имя деструктора всегда совпадает с операцией ˜, за которой
следует имя структурного типа данных. Как и у конструкторов, тип результата не пишется. Кроме того, деструкторы,
в отличие от конструкторов, не могут иметь параметров. Согласно правилам перегрузки методов, отсюда следует, что
любой структурный тип данных может иметь не более одного деструктора.
Деструктор, как правило, вызывается автоматически для уничтожения объектов данного структурного типа, например, локальных переменных при выходе из функции или при уничтожении динамических переменных при помощи
операции delete. Однако, можно вызвать деструктор и явно.
Деструкторы для каждого конкретного объекта всегда вызываются в порядке, обратном к тому, в котором вызывались конструкторы для его создания. Практически это означает, что сначала вызывается деструктор всего объекта,
определенный в его типе, а затем — деструкторы полей, в порядке, обратном к тому, в котором поля перечислены в
теле структурного типа. Именно необходимость всегда вызывать деструкторы в порядке, обратном порядку вызова
конструкторов, была причиной того, что порядок вызова конструкторов полей строго фиксирован.
Задачи.
0. В задаче про склад автомобилей преобразовать метод инициализации в конструктор.
1. Написать структуру Drug, описывающую лекарство в аптеке. В нем должны быть следующие поля: название,
действующее вещество (строки), доза, форма выпуска (таблетки/ампулы/мазь), количество в упаковке, цена упаковки,
вспомогательное поле логического типа (понадобится для дальнейшего; оно будет означать, используется ли данная
запись или пустует).
2. Написать функцию, принимающую один параметр типа Drug по ссылке, заполняющую его поля значениями,
введенными с клавиатуры.
3. Написать функцию, проверяющую действующее вещество (ее параметры — структура типа Drug и строка, и она
сравнивает эту строку с действующим веществом лекарства; если они совпадают, возвращается true, иначе — false).
4. Написать функцию, определяющую стоимость единицы действующего вещества (ее параметр — запись типа
Drug, возвращает вещественное число).
5. Написать структуру Store, описывающую склад лекарств. Она должна содержать поля типа динамический
массив записей типа Drug, число его элементов и еще одно целое число — количество собранных денег.
6. Написать конструктор, инициализирующий переменную типа Store. Он должен иметь один параметр — целое
число. В нем нужно завести динамический массив, размер которого равен целому параметру, у каждого элемента
этого массива установить логическое поле в false (изначально на сладе нет лекарств), а также установить количество
собранных денег в ноль.
7. Написать метод, принимающий лекарство на склад. Он должен иметь один параметр типа Drug и искать в
динамическом массиве объекта первую свободную ячейку, т. е. такую, у которой вспомогательное логическое поле
имеет значение false, и записывать в нее значение своего параметра типа Drug, после чего присваивать логическому
полю этой ячейки значение true.
8. Написать метод print с параметром — строкой, печатающий названия всех лекарств на складе с указанным
действующим веществом.
9. Написать метод search с параметром — строкой, печатающий название лекарства с самой большой дозой указанного действующего вещества.
10. Написать метод expensive с параметром — вещественным числом, печатающий все действующие вещества с
ценой за единицу большей, чем значение вещественного параметра.
11. Разработать структурный тип данных, описывающий вагоны и тепловозы в депо. Он должен иметь поля тип
(платформа, вагонетка, грузовой вагон без крыши, теплушка, холодильник, цистерна, пассажирский, тепловоз), грузоподъемность для вагона/максимальное число вагонов для тепловоза, пробег в км и максимальный пробег, идентификационный номер, станция приписки, вспомогательное логическое поле «данная запись активна». Пользуясь первой
записью, разработать структуру «депо». Она должна содержать динамический массив вагонов и иметь конструктор
по целому числу (число записей), устанавливающий размер динамического массива и присваивающий всем полям «запись активна» всех элементов этого массива значение false (изначально депо пусто). Также должны быть реализованы
методы «принять вагон или тепловоз в депо», ищущий неактивные записи и на место первой из них вводящий информацию о новом вагоне; «выдать вагон», принимающий в качестве параметра номер вагона и делающий его запись
неактивной; «списать вагоны», пробег которых превысил максимально допустимую величину; «напечатать информацию о всех вагонах, приписанных к определенной станции и не меньше указанной грузоподъемности», принимающий
в качестве параметров название станции и минимальную грузоподъемность.
12. Написать класс многочленов степени не выше 20 с вещественными коэффициентами. Должны быть реализованы конструктор, устанавливающий значения всех коэффициентов равными 0, а также следующие методы: eval с
единственным вещественным параметром, возвращающий значение многочлена в указанной точке, der, заменяющий
многочлен на его производную (не возвращает значения), integr c параметрами-концами отрезка, возвращающий значение определенного интеграла от многочлена на указанном отрезке, sum с параметром того же типа (многочлен),
добавляющий свой параметр к многочлену, mul, умножающий многочлен на свой параметр (тоже многочлен того же
типа; все слагаемые степени больше 20 игнорируются), print, печатающий многочлен.
13. Написать класс матриц 3 × 3 c вещественными элементами. Должны быть реализованы конструктор, устанавливающий значения всех элементов равными 0, а также следующие методы: trans, транспонирующий матрицу,
inv, заменяющий матрицу на обратную к ней, det, возвращающий определитель матрицы, tr — след матрицы (сумма
диагональных элементов), rk — ранг матрицы (сравнение вещественных чисел с 0 с точностью 1e-10), add и mul с
параметром-матрицей, добавляющий и умножающий нашу матрицу на параметр, print, печатающий матрицу.
2
Права доступа
В C++ существует механизм ограничения доступа к полям и методам структурных типов данных. В связи с этим
существуют два (на самом деле три, но об этом позже — когда речь пойдет о наследовании) уровня доступа к полям
и методам. Первый уровень доступа называется public. Принадлежащие ему поля и методы открыты, т. е. доступ к
ним возможен из любой функции программы. Другой уровень доступа называется private; принадлежащие ему поля
и методы доступны только из методов данного класса.
В структуре по умолчанию все поля и методы являются открытыми; если нужно, чтобы какие-то из них были закрытыми, нужно перед ними написать private: (двоеточие тоже нужно писать; этот уровень доступа будет действовать
на все последующие объявления и определения полей и методов, до тех пор, пока явно не встретится public:).
Имеется и другое ключевое слово для определения структурных типов данных, class. Отличие его от struct только
в том, что оно означает по умолчанию режим доступа private.
Иногда по соображениям эффективности требуется открыть доступ даже к закрытым полям и методам класса для
некоторой функции или даже для всех методов некоторого другого класса. Для этого существует понятие дружбы.
Функция является другом класса, если она имеет доступ ко всем его полям и методам, даже к закрытым. Все дружественные классу функции должны быть явно указаны внутри его определения, т. е. внутри класса должны быть
перечислены прототипы дружественных ему функций, перед каждым из которых должно стоять ключевое слово
friend. Также обстоит дело и с классами: каждый дружественный класс для класса A, т. е. такой, все методы которого
имеют доступ ко всем полям и методам класса A, должен быть объявлен внутри A с ключевым словом friend спереди.
Важное замечание: в C++ нет никакой неявной дружбы, т. е. отношение дружбы между классами не обязано быть
ни симметричным, ни транзитивным. В частности, если класс A — друг класса B, это отнюдь не означает, что B —
друг A.
Более того, вложенные классы, определенные внутри других классов, совершенно не обязаны быть друзьями последних.
Напоследок в этом разделе скажем пару слов о том, зачем нужно ограничение прав доступа. Этому есть две
главных причины.
а) Во-первых, это позволяет гарантировать целостность данных. Если любая функция может записывать что угодно в любые поля структурной переменной, очевидно, нельзя ничего гарантировать относительно ее состояния. Если
же записывать информацию в поля могут только методы самого класса, они вполне могут проверять правильность
этой информации, и принимать меры к исправлению ситуации, если информация, которую мы собираемся записать,
правильной не является. Типичный пример — класс, содержащий дату. Если в этом классе есть три целых поля — год,
день и месяц, то далеко не любое сочетание из трех целых чисел в этих полях можно рассматривать как правильную
дату. Если кто угодно может записывать в эти поля что угодно, ничего гарантировать нельзя, но если поля закрыты
и единственным методом, при помощи которого можно записать дату в переменную, будет, скажем, метод setDate
с тремя целыми параметрами, то он уже имеет полную возможность проверить записываемую в переменную дату
и принять меры, если она неправильная (реакция на неправильную дату может быть самой разной: от игнорирования проблемы, т. е. при попытке записать неправильную дату просто записывается какая-то заранее фиксированная
правильная дата и процесс продолжается как ни в чем не бывало, до выдачи сообщения об ошибке и аварийного
завершения программы).
б) Во-вторых, мы можем впоследствии захотеть изменить реализацию, т. е. внутреннее устройство нашего класса.
Если все поля и методы нашего класса открыты и любая функция нашей программы может пользоваться любыми
из них, очень трудно отследить все те места в программе, в которых используется, например, определенное поле,
и как оно там используется. Если же поля закрыты и пользователям видно лишь небольшое число методов, мы
вольны менять закрытую часть класса как хотим (все равно она не видна никому, кроме нашего класса), лишь бы
открытые методы при новой реализации вели себя по смыслу также, как и раньше. Продолжая пример с датой, мы
можем захотеть изменить внутреннее представление даты с трех целых чисел на одно: номер дня, начиная с какой-то
определенной даты. Если поля даты открыты, нам нужно отслеживать и корректировать все места в программе, где
эти поля используются, что представляет собой очень сложную задачу для больших программ. Если же они закрыты,
и клиенты могут пользоваться только методами, скажем, setDate, getYear, getMonth, getDay, то все, что нам нужно
сделать — переписать наш класс так, чтобы эти методы выдавали (при новой реализации) правильные результаты.
3
Перегрузка операций
В C++ есть еще одна очень удобная черта, которая называется перегрузкой операций. Мы уже встречались с ней,
когда говорили, что для целых чисел операция << означает битовый сдвиг влево, но если левый операнд — поток
вывода, то та же самая операция означает вывод информации в поток.
Оказывается, в C++ можно перегружать не только функции, но и почти все операции, такие, как сложение, вычитание и т. д. Правда, определять новые версии этих операций можно только для пользовательских типов. Например,
переопределить сложение для целых или вещественных чисел нельзя.
Большинство операций можно перегружать двумя способами:
а) Как метод. Пример для сложения комплексных чисел приводится ниже:
class Complex
{
double re, im;
public:
Complex(double r = 0, double i = 0):
re(r), im(i) {}
Complex operator+(Complex c);
};
Complex Complex::operator+(Complex c)
{
return Complex(x+c.x, y+c.y);
}
Здесь у сложения всего один параметр, потому что при таком переопределении сложения (как метод) первым
параметром всегда будет тот объект, для которого этот метод вызван, т. е. если переменные z1 и z2 определены как
Complex, то выражение z1+z2 понимается компилятором как z1.operator+(z2). В итоге, при таком переопределении
операций нужно в скобках указывать на один параметр меньше.
б) Как внешнюю функцию. Тот же пример в этом случае будет выглядеть так:
class Complex
{
double x, y;
public:
Complex(double r = 0, double i = 0):
re(r), im(i) {}
friend Complex operator+(Complex c1, Complex c2);
};
Complex operator+(Complex c1, Complex c2)
{
return Complex(c1.x+c2.x, c1.y+c2.y);
}
В этом случае то же выражение z1+z2 трактуется уже как operator+(z1, z2). Очевидно, что нельзя переопределять
одну и ту же по смыслу операцию обоими способами сразу — тогда компилятор не будет знать, как ее нужно вызывать.
Скажем теперь пару слов о том, как выбрать конкретный способ для перегрузки той или иной операции. Сначала
остановимся на аргументах в пользу перегрузки как метод:
а) Существуют операции, которые можно перегружать только как методы. В их число входят операция вызова
функции (operator()), операция индексации (operator[]), операции присваивания.
б) Если операция использует диспетчеризацию (о том, что это такое — см. следующий файл), ее можно перегружать
только как метод (в C++ нет диспетчеризации для внешних функций).
в) Нет необходимости объявлять операцию другом — как метод, она автоматически имеет доступ ко всем полям и
методам своего класса.
Теперь — аргументы в пользу внешней функции:
а) Если нужно иметь в качестве первого операнда объект чужого класса (например, библиотечного), куда мы
не можем добавить новый метод, такую операцию можно перегрузить только как внешнюю функцию. Пример —
операции ввода из потока или вывода в поток (классы потоков — библиотечные, и добавить в них свой метод нельзя).
б) Сохранение симметрии ситуации — чего ради коммутативная операция сложения отдаст предпочтение первому
параметру, став методом?
В связи с перегрузкой операций можно сделать еще ряд замечаний. Во-первых, язык C++ позволяет перегружать
операции инкремента (++) и декремента (−−). Однако, каждая из этих операций имеет два варианта — префиксный
и постфиксный. Как их различать при помощи типов параметров? Решение здесь было принято такое: у постфиксного варианта добавляется дополнительный фиктивный параметр (используется только при объявлении) типа int. У
постфиксного потому, что префиксных операций в C++ гораздо больше, и префиксная операция рассматривается в
C++ как горазо более типичная.
Также, язык C++ позволяет перегружать операцию ->. Она перегружается только как метод без параметров, и
смысл ее перегрузки следующий. Если компилятор видит выражение нечто->поле, возможны три случая:
а) Нечто — обычный указатель. Тогда это выражение трактуется как поле той структуры, на которую указывает
данный указатель, т. е. вполне обычным образом.
б) Нечто — переменная структурного типа. Тогда в данном структурном типе ищется перегруженная операция
->, и она применяется для данной переменной, после чего результат подставляется на место этой переменной, т. е.
исходное выражение заменяется на (нечто.operator->())->поле. После этого анализ этого выражения повторяется еще
раз.
в) Ни то, ни другое, например нечто — переменная типа int или double. В этом случае выдается сообщение об
ошибке.
Такой способ перегрузки этой операции позволяет писать структурные типы данных, поведение которых очень
похоже на поведение указателей.
Далее, в этом разделе скажем и о перегрузке операции преобразования типов. Мы уже говорили о конструкторах
преобразования, но они находятся в том классе, к которому нужно привести значение того или иного типа. Оказывается, преобразование типов можно определить и в том типе, значение которого подлежит преобразованию, для чего и
используется перегрузка операции преобразования типов. Тип результата у этой операции не пишется, а имя выглядит
как operator тип. Например, если мы хотим иметь возможность преобразовать наш тип в int, мы должны в нашем
классе определить операцию operator int() тело
Наконец, скажем пару слов о перегрузке операций выделения памяти new и освобождения ее delete.
Эти операции можно перегружать двумя способами. Первый — глобально, в этом случае их прототипы следующие:
void
void
void
void
*operator new(size_t size);
*operator new[](size_t size);
operator delete(void *ptr);
operator delete[](void *ptr);
Как видно из прототипов, отдельная версия для массивов есть не только у delete, но и у new (какую версию
вызвать, компилятор решает на основании того типа, который используется в операции new).
Однако, перегружать глобальные версии new и delete крайне не рекомендуется. Вместо этого гораздо лучше перегружать их для каждого класса отдельно; для этого вышеупомянутые прототипы нужно поместить внутрь определения класса, и затем определить их как методы. Эти методы в любом случае будут статическими. Забегая вперед,
скажем, что они наследуются, т. е. если определить их в базовом классе, они же будут использованы и для производного класса.
Кроме того, в C++ имеется еще такое понятие, как new с размещением. Если мы уже выделили под объект память
(и на нее указывает указатель ptr) и хотим только вызвать конструктор объекта на данной области памяти, это
делается так:
new(ptr) тип(параметры конструктора)
Эта версия new не может быть перегружена.
Задачи.
1. Написать структурный тип данных, представляющий комплексное число. Должны быть конструктор по вещественной и мнимой части, арифметические операции, операция комплексного сопряжения, метод, печатающий комплексное число (в стандартном виде, т. е. вещественная часть + мнимая часть i).
2. Написать структурный тип данных, представляющий дробь (поля — числитель и знаменатель — целые числа).
Должны быть конструктор дроби по двум целым числам, метод сокращения дроби, арифметические операции и
операции сравнения.
4
Литералы пользовательских типов
В старом стандарте С++ можно было пользоваться явно выписанными константами только встроенных типов, и
то не всех. Новый стандарт исправляет эту ситуацию, и в нем можно определять суффиксы пользователя, которые
добавляются к явно выписанным константам для получения значения пользовательского типа.
Такие суффиксы бывают двух разновидностей. Первая из них применяется к строковому представлению соответствующей лексемы, даже если эта лексема — целое или вещественное число. Это удобно, например, тем, что так
можно получить числа любой длины — не нужно заботиться о том, чтобы число, к которому мы применяем суффикс,
влезло в разрядную сетку компьютера. Правда, за это удобство приходится платить необходимостью самостоятельно
превращать строку в число, т. е. в тот тип данных, который это число будет хранить.
Вторая разновидность суффиксов применяется к уже сформированным целым или вещественным числам. Эти
числа должны влезать в разрядную сетку, но мы получаем их уже готовыми в качестве параметров соответствующей
функции.
Суффиксом может быть любой допустимый идентификатоор, начинающийся с символа подчеркивания. Синтаксис определения функции, отвечающей за превращение явно выписанной константы с нашим суффиксом в объект
интересующего нас типа, таков:
а) суффиксы первого типа (для целых и вещественных констант) определяются так:
тип operator"" суффикс (const char *имя) { тело }
Здесь имя — имя параметра, в котором передается строка, содержащая текст лексемы. Как и принято в С, эта строка
завершается символом с кодом 0.
б) суффиксы второго типа (для любых констант, в том числе и для строковых) можно определить почти так же,
только типы параметров будут отличаться: unsigned long long для целых чисел, long double для вещественных, char
для символов, а для строковых констант аж два параметра: const char * для содержимого и size_t для количества
символов (таким образом, символ с кодом 0 тоже может входить в состав строковой константы).
Задачи.
1. Добавить к реализации класса комплексных чисел возможность задавать мнимые числа вида число_i.
2. Добавить к реализации класса дробей возможность задавать дроби вида "числитель/знаменатель"_f.
5
Константные объекты
C++ позволяет использовать ключевое слово const не только со встроенными типами, но и с пользовательскими.
Смысл этого в том, чтобы еще на этапе компиляции отлавливать некоторые ошибки.
Добавление ключевого слова const в декларацию переменной структурного типа приводит к тому, что у такого
объекта:
а) нельзя присваивать открытым полям значения;
б) можно пользоваться только константными, т. е. не меняющими своего объекта, методами. Такие методы, вопервых, в заголовке после списка параметров, т. е. между ) и ; или телом должны иметь ключевое слово const,
и во-вторых, не должны пытаться присваивать полям значения или вызывать для того же объекта неконстантные
методы. Такие методы просто рассматриваются как принимающие неявный параметр this типа не просто указатель
на свой класс, а указатель на константу типа этого класса. Это соображение позволяет в рамках обычных правил
перегрузки методов, и более общо, функций, иметь два метода, одинаковых по имени и типам параметров, один из
которых константный, а другой — нет. Это достаточно удобно при реализации библиотечных классов.
Хотя и редко, но иногда возникают ситуации, когда требуется иметь в классе поля, которые можно менять даже
у константных объектов. Язык C++ позволяет определять такие поля, для чего существует ключевое слово mutable.
Помеченные таким образом поля можно менять даже у константных объектов.
Задачи.
1. Написать структурный тип данных, представляющий момент времени. При этом желательно гарантировать
правильность записанной в нем информации (при помощи закрытия полей).
2. Написать структурный тип данных, представляющий остаток по модулю NUMBER (константа, определенная в
программе). Должен быть реализован конструктор по целому числу и операции сложения, вычитания и умножения.
3. Написать структурный тип данных, представляющий многочлен с вещественными коэффициентами (данные
— степень и динамический массив коэффициентов). Должен быть конструктор по вещественному числу (константа),
конструктор по степени и массиву коэффициентов, конструктор копирования, деструктор, перегруженные операции:
присваивание, арифметические операции, операция индексации (возвращает ссылку на коэффициент по степени одночлена), операция () (параметр — вещественное число, результат — значение многочлена в указанной точке).
4. Написать структурный тип данных, представляющий матрицу из вещественных чисел. Должен быть конструктор по паре целых чисел и массиву вещественных (число строк, столбцов и элементы), конструктор копирования,
деструктор, перегруженные операции: присваивание, арифметические операции (кроме деления), операция индексации (возвращает ссылку на коэффициент по номерам строки и столбца, круглые скобки, потому что в квадратных
допускается только один параметр), операция ! (транспонирование), ˜ (обратная матрица), метод det (определитель).
6
Указатели на поля и методы
Ровно так же, как нам может потребоваться выбрать переменную или функцию в одном месте, а использовать в
другом, то же самое может потребоваться и в отношении полей и методов классов. Решение этой задачи называется
«указатели на поля и методы» или обобщенно «указатели на члены». Например, пусть есть класс X с восемью полями
целого типа:
struct X { int a, b, c, d, e, f, g, h; };
Тогда указатель p на одно из целых полей класса X определяется так:
int X::*p;
Теперь выбор одного из полей класса X осуществляется так:
p = &X::d;
А использование выбранного поля, скажем, переменной y, так:
cout<<y.*p;
Здесь y — переменная класса X.
Здесь уместно сделать два замечания. Во-первых, .* — это одна операция, так что пробелы между . и * недопустимы.
Во-вторых, ее приоритет ниже, чем у обычного выбора поля или вызова функции, так что иногда может понадобиться
ставить скобки.
Ровно также может быть определен указатель на метод:
struct X { int a(int); int b(int); int c(int); int d(int);
int e(int); int f(int); int g(int); int h(int); };
int (X::*p)(int);
p = &X::f;
X y;
cout<<(y.*p)(7);
Забегая вперед (см. следующий файл), скажем, что указатели на методы вполне согласуются с диспетчеризацией.
Как и для обычных полей и методов, существуют версии операции доступа для указателей на структуры ->* .
7
Объединения
Языку С++ в наследство от C досталось еще одно средство организации структурных типов данных, называемое
объединением. Объявление и определение объединения похоже на обычные структурные типы данных, но вместо
struct или class используется ключевое слово union. Отличие объединения от структуры в том, что все его поля
разделяют одну и ту же область памяти, и следовательно, в каждый момент времени использоваться может только
одно поле.
Объединения обычно используются для следующих целей:
а) Экономия памяти. Если одновременно используется не более одного поля структуры, то такую структуру можно
заменить объединением. Для этих целей, как правило, используются так называемые безымянные объединения —
объединения, у которых не указано имя (между ключевым словом union и телом). Такие объединения, в отличие от
обычных, определяют не новый тип, а набор переменных (полей безымянного объединения), занимающих одну область
памяти. Безымянные объединения могут встречаться везде, где может стоять определение переменной — глобальной,
локальной или поля структуры.
б) Преобразование типов данных, сохраняющее битовое представление. Обычное преобразование типов, например,
с помощью операции (тип), старается по возможности сохранить значение преобразуемого выражения. Например, значением выражения (double)5 будет вещественное число 5.0, хотя битовое представление целого числа 5 и вещественного
5.0 сильно отличаются одно от другого. Однако, иногда бывает нужно преобразовать тип переменной, сохранив неизменным именно битовое представление. Например, иногда бывает удобно трактовать вещественное число как восемь
подряд идущих байтов.
Для этого можно воспользоваться объединением с двумя полями, первое из которых имеет тип исходного выражения, а второе — тип, к которому его надо привести. Далее мы присваиваем первому полю интересующее нас значение,
а из второго поля считываем результат преобразования.
Того же эффекта можно добиться при помощи преобразования типов указателей. Нужно просто взять указатель
на интересующее нас значение и явно преобразовать его в указатель на интересующий нас тип. Можно, конечно,
преобразовать тип указателя и при помощи операции (тип *), но для этой ситуации в языке C++ предусмотрено
специальное средство reinterpret_cast<тип *>(исходный указатель).
в) Полиморфизм. Полиморфизм — это возможность производить одни и те же по смыслу действия над операндами
разных типов при помощи одних и тех же выражений. Объединения можно использовать для того, чтобы один и тот
же структурный тип данных мог представлять различные по смыслу объекты. Обычно для этого в структурном
типе данных используются поля, определяющие, что представляет собой объект в данный момент, и безымянное
объединение, поля которого представляют разные возможные варианты объекта.
Задача.
Написать структурный тип данных, который может представлять вещественное или целое число. В нем должны
быть конструкторы по целому и по вещественному числу, операции считывания из потока и записи в поток, а также
перегруженные арифметические операции и операции сравнения. При этом, арифметические операции + − * / для
целых операндов должны давать целый результат (последняя операция — в том случае, если возможно деление
нацело), остальные — вещественный.
Download