умолчанию присваиваются начиная с 0 в порядке возрастания, это
эквивалентно записи:
const ASM = 0;
const AUTO = 1;
const BREAK = 2;
Перечисление может быть именованным. Например:
enum keyword { ASM, AUTO, BREAK };
Имя перечисления становится синонимом int, а не новым типом.
Описание переменной keyword, а не просто int, может дать как
программисту, так и компилятору подсказку о том, что использование
преднамеренное. Например:
keyword key;
switch (key) {
case ASM:
// что-то делает
break;
case BREAK:
// что-то делает
break;
}
побуждает компилятор выдать предупреждение, поскольку только два
значения keyword из трех используются.
Можно также задавать значения перечислителей явно. Например:
enum int16 {
sign=0100000, // знак
most_significant=040000, // самый значимый
least_significant=1 // наименее значимый
};
Такие значения не обязательно должны быть различными, возрастающими
или положительными.
2.5 Экономия Пространства
В ходе программирования нетривиальных разработок неизбежно
наступает время, когда хочется иметь больше пространства памяти,
чем имеется или отпущено. Есть два способа выжать побольше
пространства из того, что доступно:
- стр 73 -
[1] Помещение в байт более одного небольшого объекта; и
[2] Использование одного и того же пространства для хранения
разных объектов в разное время.
Первого можно достичь с помощью использования полей, второго -
через использование объединений. Эти конструкции описываются в
следующих разделах. Поскольку обычное их применение состоит чисто в
оптимизации программы, и они в большинстве случаев непереносимы,
программисту следует дважды подумать, прежде чем использовать их.
Часто лучше изменить способ управления данными; например, больше
полагаться на динамически выделяемую память (#3.2.6) и меньше на
заранее выделенную статическую память.
2.5.1 Поля
Использование char для представления двоичной переменной,
например, переключателя включено/выключено, может показаться
экстравагантным, но char является наименьшим объектом, который в
C++ может выделяться независимо. Можно, однако, сгруппировать
несколько таких крошечных переменных вместе в виде полей struct.
Член определяется как поле путем указания после его имени числа
битов, которые он занимает. Допустимы неименованные поля; они не
влияют на смысл именованных полей, но неким машинно-зависимым
образом могут улучшить размещение:
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;
}
Получилось размещение регистра 0 сосояния DEC PDP11/45 (в
предположении, что поля в слове размещаются слева направо). Этот
пример также иллюстрирует другое основное применение полей:
именовать части внешне предписанного размещения. Поле должно быть
целого типа и используется как другие целые, за исключением того,
что невозможно взять адрес поля. В ядре операционной системы или в
отладчике тип sreg можно было бы использовать так:
sreg* sr0 = (sreg*)0777572;
//...
if (sr->access) { // нарушение доступа
// чистит массив
sr->access = 0;
}
Однако применение полей для упаковки нескольких переменных в один
байт не обязательно экономит пространство. Оно экономит
пространство, занимаемое данными, но объем кода, необходимого для
манипуляции этими переменными, на большинстве машин возрастает.
Известны программы, которые значительно сжимались, когда двоичные
- стр 74 -
переменные преобразовывались из полей бит в символы! Кроме того,
доступ к char или int обычно намного быстрее, чем доступ к полю.
Поля - это просто удобная и краткая запись для применения
логических операций с целью извлечения информации из части слова
или введения информации в нее.
2.5.2 Объединения
Рассмотрим проектирование символьной таблицы, в которой каждый
элемент содержит имя и значение, и значение может быть либо
строкой, либо целым:
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\n";
break;
}
}
Поскольку string_value и int_value никогда не могут
использоваться одновременно, ясно, что пространство пропадает
впустую. Это можно легко исправить, указав, что оба они должны быть
членами union (объединения); например, так:
struct entry {
char* name;
char type;
union {
char* string_value; // используется если type == 's'
int int_value; // используется если type == 'i'
};
};
Это оставляет всю часть программы, использующую entry, без
изменений, но обеспечивает, что при размещении entry string_value и
int_value имеют один и тот же адрес. Отсюда следует, что все члены
объединения вместе занимают лишь столько памяти, сколько занимает
наибольший член.
Использование объединений таким образом, чтобы при чтении
значения всегда применялся тот член, с применением которого оно
- стр 75 -
записывалось, совершенно оптимально. Но в больших программах
непросто гарантировать, что объединения используются только таким
образом, и из-за неправильного использования могут появляться
трудно уловимые ошибки. Можно капсулизировать объединение таким
образом, чтобы соответствие между полем типа и типами членов было
гарантированно правильным (#5.4.6).
Объединения иногда испольуют для "преобразования типов" (это
делают главным образом программисты, воспитанные на языках, не
обладающих средствами преобразования типов, где жульничество
является необходимым). Например, это "преобразует" на VAX'е int в
int*, просто предполагая побитовую эквивалентность:
struct fudge {
union {
int i;
int* p;
};
};
fudge a;
a.i = 4096;
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.
2.6 Упражнения
1. (*1) Заставьте работать программу с "Hello, world" (1.1.1).
2. (*1) Для каждого описания в #2.1 сделайте следующее: Если
описание не является определением, напишите для него
определение. Если описание является определением, напишите для
него описание, которое при этом не является определением.
3. (*1) Напишите описания для: указателя на символ; вектора из 10
целых; ссылки на вектор из 10 целых; указателя на вектор из
- стр 76 -
символьных строк; указателя на указатель на символ;
константного целого; указателя на константное целое; и
константного указателя на целое. Каждый из них
инициализируйте.
4. (*1.5) Напишите программу, которая печатает размеры основных и
указательных типов. Используйте операцию sizeof.
5. (*1.5) Напишите программу, которая печатает буквы 'a'...'z' и
цифры '0'...'9' и их числовые значения. Сделайте то же для
остальных печатаемых символов. Сделайте то же, но используя
шестнадцатиричную запись.
6. (*1) Напечатайте набор битов, которым представляется указатель
0 на вашей системе. Подсказка: #2.5.2.
7. (*1.5) Напишите функцию, печатающую порядок и мантиссу
параметра типа double.
8. (*2) Каковы наибольшие и наименьшие значения, на вашей
системе, следующих типов: char, short, int, long, float,
double, unsigned, char*, int* и void*? Имеются ли
дополнительные ограничения на принимаемые ими значения? Может
ли, например, int* принимать нечетное значение? Как
выравниваются в памяти объекты этих типов? Может ли, например,
int иметь нечетный адрес?
9. (*1) Какое самое длинное локальное имя можно использовать в
C++ программе в вашей системе? Какое самое длинное внешнее имя
можно использовать в C++ программе в вашей системе? Есть ли
какие-нибудь ограничения на символы, которые можно употреблять
в имени?
10. (*2) Определите one следующим образом:
const one = 1;
Попытайтесь поменять значение one на 2. Определите num
следующим образом:
const num[] = { 1, 2 };
Попытайтесь поменять значение num[1] на 2.
11. (*1) Напишите функцию, переставляющую два целых (меняющую
значения). Используйте в качесте типа параметра int*. Напишите
другую переставляющую функцию, использующую в качесте типа
параметра int&.
12. (*1) Каков размер вектора str в следующем примере:
char str[] = "a short string";
Какова длина строки "a short string"?
13. (*1.5) Определите таблицу названий месяцев года и числа дней
в них. Выведите ее. Сделайте это два раза: один раз используя
вектор для названий и вектор для числа дней, и один раз
используя вектор структур, в каждой из которых хранится
название месяца и число дней в нем.
14. (*1) С помощью typedef определите типы: беззнаковый char;
константный беззнаковый char; указатель на целое; указатель на
указатель на char; указатель на вектора символов; вектор из 7
целых указателей; указатель на вектор из 7 целых указателей;
и вектор из 8 векторов из 7 целых указателей.
* Глава 3 *
Выражения и операторы
С другой стороны,
мы не можем игнорировать эффективность
- Джон Бентли