В процессе создания нетривиальной программы рано или поздно наступает
момент, когда требуется больше памяти, чем можно выделить или
запросить. Есть два способа выжать еще некоторое количество памяти:
[1] паковать в байты переменные с малыми значениями;
[2] использовать одну и ту же память для хранения разных объектов
в разное время.
Первый способ реализуется с помощью полей, а второй - с помощью
объединений. И те, и другие описываются ниже. Поскольку назначение
этих конструкций связано в основном с оптимизацией программы, и
поскольку, как правило, они непереносимы, программисту следует
хорошенько подумать, прежде чем использовать их. Часто лучше изменить
алгоритм работы с данными, например, больше использовать динамически
выделяемую память, чем заранее отведенную статическую память.
2.6.1 Поля
Кажется расточительным использовать для признака, принимающего
только два значения ( например: да, нет) тип char, но объект типа
char является в С++ наименьшим объектом, который может независимо
размещаться в памяти. Однако, есть возможность собрать переменные
с малым диапазоном значений воедино, определив их как поля структуры.
Член структуры является полем, если в его определении после имени
указано число разрядов, которое он должен занимать. Допустимы
безымянные поля. Они не влияют на работу с поименованными полями,
но могут улучшить размещение полей в памяти для конкретной машины:
struct sreg {
unsigned enable : 1;
unsigned page : 3;
unsigned : 1; // не используется
unsigned mode : 2;
unsigned : 4; // не используется
unsigned access : 1;
unsigned length : 1;
unsigned non_resident : 1;
};
Приведенная структура описывает разряды нулевого
регистра состояния DEC PDP11/45 (предполагается, что поля в слове
размещаются слева направо). Этот пример показывает также другое
возможное применение полей: давать имена тем частям
объекта, размещение которых определено извне. Поле должно иметь
целый тип ($$R.3.6.1 и $$R.9.6), и оно используется аналогично другим
объектам целого типа. Но есть исключение: нельзя брать адрес поля.
В ядре операционной системы или в отладчике тип sreg мог бы
использоваться следующим образом:
sreg* sr0 = (sreg*)0777572;
//...
if (sr0->access) { // нарушение прав доступа
// разобраться в ситуации
sr0->access = 0;
}
Тем не менее,
применяя поля для упаковки нескольких переменных в один байт, мы
необязательно сэкономим память. Экономится память для данных, но
на большинстве машин одновременно возрастает объем команд,
нужных для работы с упакованными данными.
Известны даже такие программы, которые значительно сокращались в объеме,
если двоичные переменные, задаваемые полями, преобразовывались в
переменные типа char! Кроме того, доступ к char или int обычно
происходит намного быстрее, чем доступ к полю. Поля - это просто
удобная краткая форма задания логических операций для извлечения
или занесения информации в части слова.
Рассмотрим таблицу имен, в которой каждый элемент содержит имя и
его значение. Значение может задаваться либо строкой, либо целым числом:
struct entry {
char* name;
char type;
char* string_value; // используется если type == 's'
int int_value; // используется если type == 'i'
};
void print_entry(entry* p)
{
switch(p->type) {
case 's':
cout << p->string_value;
break;
case 'i':
cout << p->int_value;
break;
default:
cerr << "type corrupted\n";
break;
}
}
Поскольку переменные
string_value и int_value никогда не могут использоваться одновременно,
очевидно, что часть памяти пропадает впустую. Это можно легко исправить,
описав обе переменные как члены объединения, например, так:
struct entry {
char* name;
char type;
union {
char* string_value; // используется если type == 's'
int int_value; // используется если type == 'i'
};
};
Теперь гарантируется, что при выделении памяти для entry члены
string_value и int_value будут размещаться с одного адреса, и
при этом не нужно менять все части программы, работающие с entry.
Из этого следует, что все члены объединения вместе занимают такой же
объем памяти, какой занимает наибольший член объединения.
Надежный способ работы с объединением заключается в том, чтобы
выбирать значение с помощью того же самого члена, который его записывал.
Однако, в больших программах трудно гарантировать, что объединение
используется только таким способом, а в результате использования
не того члена обЪединения могут возникать трудно обнаруживаемые ошибки.
Но можно встроить объединение в такую структуру, которая обеспечит
правильную связь между значением поля типа и текущим типом члена
объединения ($$5.4.6).
Иногда объединения используют для "псевдопреобразований" типа
(в основном на это идут программисты, привыкшие к языкам, в которых
нет средств преобразования типов, и в результате приходится обманывать
транслятор). Приведем пример такого "преобразования" int в int*
на машине VAX, которое достигается простым совпадением разрядов:
struct fudge {
union {
int i;
int* p;
};
};
fudge a;
a.i = 4095;
int* p = a.p; // некорректное использование
В действительности это вовсе не преобразование типа, т.к. на одних
машинах int и int* занимают разный объем памяти, а на других целое
не может размещаться по адресу, задаваемому нечетным числом. Такое
использование объединений не является переносимым, тогда как
существует переносимый способ задания явного преобразования
типа ($$3.2.5).
Иногда объединения используют специально, чтобы избежать
преобразования типов. Например, можно использовать fudge, чтобы
узнать, как представляется указатель 0:
fudge.p = 0;
int i = fudge.i; // i необязательно должно быть 0
Объединению можно дать имя, то есть можно сделать его
полноправным типом. Например, fudge можно описать так:
union fudge {
int i;
int* p;
};
и использовать (некорректно) точно так же, как и раньше. Вместе с тем,
поименованные объединения можно использовать и вполне корректным
и оправданным способом (см. $$5.4.6).
Достарыңызбен бөлісу: |