Пользовательские типы данных
В реальных задачах обрабатываемая информация может иметь достаточно сложную структуру. Для ее адекватного представления используются типы данных, построенные на основе простых типов данных, массивов и указателей. Язык C++ позволяет программисту определять свои типы данных и правила работы с ними.
Переименование типов (typedef)
Для того чтобы сделать программу более ясной, можно задать типу новое имя с помощью ключевого слова typedef:
typedef тип новое_имя [ размерность ];
В данном случае квадратные скобки являются элементом синтаксиса. Размерность может отсутствовать.
Примеры:
typedef unsigned int UINT;
typedef char Msg[100];
Введенное таким образом имя можно использовать таким же образом, как и имена стандартных типов:
UINT i, j; // две переменных типа unsigned int
Msg str[10]; // массив из 10 строк по 100 символов
Кроме задания типам с длинными описаниями более коротких псевдонимов, ключевое слово typedef используется для облегчения переносимости программ: если машинно-зависимые типы объявить с помощью операторов typedef, при переносе программы потребуется внести изменения только в эти операторы.
Перечисления (enum)
При написании программ часто возникает потребность определить несколько именованных констант, имеющих различные значения. Для этого удобно воспользоваться перечисляемым типом данных, все возможные значения которого задаются списком целочисленных констант.
Формат: enum [ имя_типа ] { список_констант };
Имя типа задается в том случае, если в программе требуется определять переменные этого типа. Компилятор обеспечивает, чтобы эти переменные принимали значения только из списка констант.
Константы должны быть целочисленными и могут инициализироваться обычным образом. При отсутствии инициализатора первая константа обнуляется, а каждой следующей присваивается на единицу большее значение, чем предыдущей:
enum Err {ERR_READ, ERR_WRITE, ERR_CONVERT};
Err error;
switch (error) {
case ERR_READ: /* операторы */ break;
case ERR_WRITE: /* операторы */ break;
case ERR_CONVERT: /* операторы */ break;
}
Константам ERR_READ, ERR_WRITE, ERR_CONVERT присваиваются значения 0, 1 и 2 соответственно.
Другой пример:
enum {two = 2, three, four, ten = 10, eleven, fifty = ten + 40};
Константам three и four присваиваются значения 3 и 4, константе eleven —11.
Имена перечисляемых констант должны быть уникальными, а значения могут совпадать.
Преимущество применения перечисления перед описанием именованных констант и директивой #define состоит в том, что связанные константы нагляднее; кроме того, компилятор при инициализации констант может выполнять проверку типов.
При выполнении арифметических операций перечисления преобразуются в целые. Поскольку перечисления являются типами, определяемыми пользователем, для них можно вводить собственные операции.
Структуры (struct)
В отличие от массива, все элементы которого однотипны, структура может содержать элементы разных типов.
В языке C++ структура является видом класса и обладает всеми его свойствами, но во многих случаях достаточно использовать структуры так, как они определены в языке С:
struct [ имя_типа ] {
тип_1 элемент_1;
тип_2 элемент_2;
тип_n элемент_n;
} [ список_описателей ];
Элементы структуры называются полями структурыи могут иметь любой тип, кроме типа этой же структуры, но могут быть указателями на него.
Если отсутствует имя типа, должен быть указан список описателей переменных, указателей или массивов. В этом случае описание структуры служит определением элементов этого списка:
// Определение массива структур и указателя на структуру:
struct {
char fio[30];
int date, code;
double salary;
} staff[100], *ps;
Если список отсутствует, описание структуры определяет новый тип, имя которого можно использовать в дальнейшем наряду со стандартными типами, например:
struct Worker{ // описание нового типа Worker
char fio[30];
int date, code;
double salary;
}; // описание заканчивается точкой с запятой
// определение массива типа Worker и указателя на тип Worker:
Worker staff[100], *ps;
Имя структуры можно использовать сразу после его объявления в тех случаях, когда компилятору не требуется знать размер структуры. Определить структуру можно дать позднее. Это позволяет создавать связные списки структур:
struct List;. // объявление структуры List
struct Link{
List *p; // указатель на структуру List
Link *prev, *succ; // указатели на структуру Link
}
struct List { /* определение структуры List */};
Для инициализации структурызначения ее элементов перечисляют в фигурных скобках в порядке их описания:
struct{
char fio[30];
int date, code;
double salary;
} worker = {"Страусенко", 31, 215, 3400.55};
При инициализации массивов структур следует заключать в фигурные скобки каждый элемент массива (учитывая, что многомерный массив — это массив массивов):
struct complex{
float real, im;
} compl [2][3] = {
{{1, 1}, {1, 1}, {1, 1}}, // строка 1, т.е. массив compl[0]
{{2, 2}, {2, 2}, {2, 2}} // строка 2, т.е. массив compl[1]
}
Для переменных одного и того же структурного типа определена операция присваивания,при этом происходит поэлементное копирование. Структуру можно передавать в функцию и возвращать в качестве значения функции. Другие операции со структурами могут быть определены пользователем.
Размер структуры не обязательно равен сумме размеров ее элементов, поскольку они могут быть выровнены по границам слова.
Доступ к полям структурывыполняется с помощью операций выбора . (точка) при обращении к полю через имя структуры и -> при обращении через указатель, например:
Worker worker, staff[100], *ps;
…
worker.fio = "Страусенко";
staff[8].code = 215;
ps->salary = 0.12;
Если элементом структуры является другая структура, то доступ к ее элементам выполняется через две операции выбора:
struct A {int a; double x;};
struct В {A a; double x;} х[2];
х[0].а.а = 1;
х[1].х = 0.1;
Как видно из примера, поля разных структур могут иметь одинаковые имена, поскольку у них разная область видимости. Более того, можно объявлять в одной области видимости структуру и другой объект (например, переменную или массив) с одинаковыми именами, если при определении структурной переменной использовать слово struct.
Битовые поля
Битовые поля — это особый вид полей структуры. Они используются для плотной упаковки данных, например, флажков типа «да/нет». Минимальная адресуемая ячейка памяти — 1 байт, а для хранения флажка достаточно одного бита.
При описании битового поля после имени через двоеточие указывается длина поля в битах (целая положительная константа):
struct Options{
bool centerX:l;
bool centerY:l;
unsigned int shadow:2;
unsigned int palette:4;
}
Битовые поля могут быть любого целого типа. Имя поля может отсутствовать, такие поля служат для выравнивания на аппаратную границу. Доступ к полю осуществляется обычным способом — по имени. Адрес поля получить нельзя, однако в остальном битовые поля можно использовать точно так же, как обычные поля структуры.
Следует учитывать, что операции с отдельными битами реализуются гораздо менее эффективно, чем с байтами и словами, так как компилятор должен генерировать специальные коды, и экономия памяти под переменные оборачивается увеличением объема кода программы. Размещение битовых полей в памяти зависит от компилятора и аппаратуры.
Объединения (union)
Объединение представляет собой частный случай структуры, все поля которой располагаются по одному и тому же адресу. Формат описания такой же, как у структуры, только вместо ключевого слова struct используется слово union.
Длина объединения равна наибольшей из длин его полей. В каждый момент времени в переменной типа объединение хранится только одно значение, и ответственность за его правильное использование лежит на программисте.
Объединения применяют для экономии памяти в тех случаях, когда известно, что больше одного поля одновременно не требуется:
#include <iostream.h>
int main( ) {
enum paytype {CARD, CHECK};
paytype ptype;
union payment{
char card[25];
long check;
} info;
…
/* присваивание значений info и ptype */
switch (ptype){
case CARD: cout << "Оплата по карте: " << info.card; break;
case CHECK: cout << "Оплата чеком: " << info.check, break;
}
return 0;
}
Объединение часто используют в качестве поля структуры, при этом в структуру удобно включить дополнительное поле, определяющее, какой именно элемент объединения используется в каждый момент. Имя объединения можно не указывать, что позволяет обращаться к его полям непосредственно:
#include <iostream.h>
int main( ) {
enum paytype {CARD, CHECK};
struct{
paytype ptype;
union{
char card[25];
long check;
}
} info;
... /* присваивание значения info */
switch (info.ptype){
case CARD: cout << "Оплата по карте: " << info.card; break;
case CHECK: cout << "Оплата чеком: " << info.check; break;
}
return 0;
}
Объединения применяются также для разной интерпретации одного и того же битового представления. Хотя в этом случае лучше использовать явные операции преобразования типов. В качестве примера рассмотрим работу со структурой, содержащей битовые поля:
struct Options{
bool centerX:l;
bool centerY:l;
unsigned int shadow:2;
unsigned int palette:4;
}
union{
unsigned char ch;
Options bit;
} option = {0xC4};
cout << option.bit.palette;
option.ch &= 0xF0; // наложение маски
По сравнению со структурами на объединения налагаются некоторые ограничения:
· объединение может инициализироваться только значением его первого элемента;
· объединение не может содержать битовые поля;
· объединение не может содержать виртуальные методы, конструкторы, деструкторы и операцию присваивания;
· объединение не может входить в иерархию классов.
Тема 1.22