несколько таких крошечных переменных вместе в виде полей 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
Выражения и операторы
С другой стороны,
мы не можем игнорировать эффективность
- Джон Бентли
C++ имеет небольшой, но гибкий набор различных видов операторов
для контроля потока управления в программе и богатый набор операций
для манипуляции данными. С наиболее общепринятыми средствами вас
познакомит один законченный пример. После него приводится
резюмирующий обзор выражений и с довольно подробно описываются
явное описание типа и работа со свободной памятью. Потом
представлена краткая сводка операций, а в конце обсуждаются стиль
выравнивания* и комментарии.
3.1 Настольный калькулятор
С операторами и выражениями вас познакомит приведенная здесь
программа настольного калькулятора, предоставляющего четыре
стандартные арифметические опреации над числами с плавающей точкой.
Пользователь может также определять переменные. Например, если
вводится
r=2.5
area=pi*r*r
(pi определено заранее), то программа калькулятора напишет:
2.5
19.635
где 2.5 - результат первой введенной строки, а 19.635 - результат
второй.
Калькулятор состоит из четырех основных частей: программы
синтаксического разбора (parser'а), функции ввода, таблицы имен и
управляющей программы (драйвера). Фактически, это миниатюрный
компилятор, в котором программа синтаксического разбора производит
синтаксический анализ, функция ввода осуществляет ввод и
лексический анализ, в таблице имен хранится долговременная
информация, а драйвер распоряжается инициализцией, выводом и
обработкой ошибок. Можно было бы многое добавить в этот
калькулятор, чтобы сделать его более полезным, но в существующем
виде эта программа и так достаточно длинна (200 строк), и большая
часть дополнительных возможностей просто увеличит текст программы
не давая дополнительного понимания применения C++.
____________________
* Нам неизвестен русскоязычный термин, эквивалентный английскому
indentation. Иногда это называется отступами. (прим. перев.)
- стр 78 -
3.1.1 Программа синтаксического разбора
Вот грамматика языка, допускаемого калькулятором:
program:
END // END - это конец ввода
expr_list END
expr_list:
expression PRINT // PRINT - это или '\n' или ';'
expression PRINT expr_list
expression:
expression + term
expression - term
term
term:
term / primary
term * primary
primary
primary:
NUMBER // число с плавающей точкой в C++
NAME // имя C++ за исключением '_'
NAME = expression
- primary
( expression )
Другими словами, программа есть последовательность строк. Каждая
строка состоит из одного или более выражений, разделенных запятой.
Основными элементами выражения являются числа, имена и операции *,
/, +, - (унарный и бинарный) и =. Имена не обязательно должны
описываться до использования.
Используемый метод синтаксического анализа обычно называется