Совершенство достигается только к моменту краха.
- С.Н. Паркинсон
В этой главе описаны основные типы (char, int, float и т.д.) и
основные способы построения из них новых типов (функций, векторов,
указателей и т.д.). Имя вводится в программе посредством описания,
которое задает его тип и, возможно, начальное значение. Даны
понятия описания, определения, области видимости имен, времени
жизни объектов и типов. Описываются способы записи констант в C++,
а также способы определения символических констант. Примеры просто
демонстрируют характерные черты языка. Более развернутый и
реалистичный пример приводится в следующей главе для знакомства с
выражениями и операторами языка C++. Механизмы задания типов,
определяемых пользователем, с присоединенными операциями
представлены в Главах 4, 5 и 6 и здесь не упоминаются.
2.1 Описания
2.1.1 Область Видимости | |
2.1.2 Объекты и Адреса (Lvalue) | |
2.1.3 Время Жизни |
Прежде чем имя (идентификатор) может быть использовано в C++ программе, он должно быть описано. Это значит, что надо задать его тип, чтобы сообщить компилятору, к какого вида объектам относится имя. Вот несколько примеров, иллюстрирующих разнообразие описаний:
char ch; int count = 1; char* name = "Bjarne"; struct complex { float re, im; }; complex cvar; extern complex sqrt(complex); extern int error_number; typedef complex point; float real(complex* p) { return p->re; }; const double pi = 3.1415926535897932385; struct user;
extern complex sqrt(complex); extern int error_number; struct user;
int count; int count; // ошибка: переопределение exnern int error_number; exnern int error_number; // ошибка: несоответствие типов
exnern int error_number; exnern int error_number;
struct complex { float re, im; }; typedef complex point; float real(complex* p) { return p->re }; const double pi = 3.1415926535897932385;
int count = 1; char* name = "bjarne"; //... count = 2; name = "marian";
Описание вводит имя в области видимости; то есть, имя может использоваться только в определенной части программы. Для имени, описанного в функции (такое имя часто называют локальным), эта область видимости простирается от точки описания до конца блока, в котором появилось описание; для имени не в функции и не в классе (называемого часто глобальным именем) область видимости простирается от точки описания до конца файла, в котором появилось описание. Описание имени в блоке может скрывать (прятать) описание во внутреннем блоке или глобальное имя. Это значит, что можно переопределять имя внутри блока для ссылки на другой объект. После выхода из блока имя вновь обретает свое прежнее значение. Например:
int x; // глобальное x f() { int x; // локальное x прячет глобальное x x = 1; // присвоить локальному x { int x; // прячет первое локальное x x = 2; // присвоить второму локальному x } x = 3; // присвоить первому локальному x } int* p = &x; // взять адрес глобального x
int x; f() { int x = 1; // скрывает глобальное x ::x = 2; // присваивает глобальному x }
int x; f() { int x = x; // извращение }
int x; f() // извращение { int y = x; // глобальное x int x = 22; y = x; // локальное x }
f(int x) { int x; // ошибка }
Можно назначать и использовать переменные, не имеющие имен, и
можно осуществлять присваивание выражениям странного вида
(например, *p[a+10]=7). Следовательно, есть потребность в имени
"нечто в памяти". Вот соответствующая цитата из справочного
руководства по c++: "Объект есть область памяти; lvalue есть
выражение, ссылающееся на объект"(#с.5). Слово "lvalue" первоначально было придумано для значения "нечто, что может стоять
в левой части присваивания". Однако не всякий адрес можно
использовать в левой части присваивания; бывают адреса, ссылающиеся
на константу (см. #2.4).
Если программист не указал иного, то объект создается, когда
встречается его описание, и уничтожается, когда его имя выходит из
области видимости, Объекты с глобальными именами создаются и
инициализируются один раз (только) и "живут" до завершения
программы. Объекты, определенные описанием с ключевым словом
static, ведут себя так же. Например *1:
Имя (идентификатор) состоит из последовательности букв и цифр.
Первый символ должен быть буквой. Символ подчерка _ считается
буквой. C++ не налагает ограничений на число символов в имени, но
некоторые части реализации находятся вне ведения автора компилятора
(в частности, загрузчик), и они, к сожалению, такие ограничения
налагают. Некоторые среды выполнения также делают необходимым
расширить или ограничить набор символов, допустимых в
идентификаторе; расширения (например, при допущении в именах
символа $) порождают непереносимые программы. В качестве имени не
могут использоваться ключевые слова C++ (см. #с.2.3). Примеры имен:
2.1.3 Время Жизни
int a = 1;
void f()
{
int b = 1; // инициализируется при каждом вызове f()
static int c = 1; // инициализируется только один раз
cout << " a = " << a++
<< " b = " << b++
<< " c = " << c++ << "\n";
}
main()
{
while (a < 4) f();
}
производит вывод
a = 1 b = 1 c = 1
a = 2 b = 1 c = 2
a = 3 b = 1 c = 3
Не инициализированная явно статическая (static) переменная неявно
инициализируется нулем.
С помощью операций new и delete программист может также создавать
объекты, время жизни которых управляется непосредственно; см.
#3.2.4.
2.2 Имена
hello this_is_a_most_unusially_long_name
DEFINED foO bAr u_name HorseSense
var0 var1 CLASS _class ___
Примеры последовательностей символов, которые не могут
использоваться как идентификаторы:
012 a fool $sys class 3var
pay.due foo~bar .name if
Буквы в верхнем и нижнем регистрах считаются различными, поэтому
Count и count - различные имена, но вводить имена, лишь
незначительно отличающиеся друг от друга, нежелательно. Имена,
начинающиеся с подчерка, по традиции используются для специальных
средств среды выполнения, поэтому использовать такие имена в
прикладных программах нежелательно.
Во время чтения программы компилятор всегда ищет наиболее длинную
строку, составляющую имя, поэтому var10 - это одно имя, а не имя
var, за которым следует число 10; и elseif - одно имя, а не
ключевое слово else, после которого стоит ключевое слово if.
2.3 Типы
Каждое имя (идентификатор) в C++ программе имеет ассоциированный с ним тип. Этот тип определяет, какие операции можно применять к имени (то есть к объекту, на который оно ссылается), и как эти операции интерпретируются. Например:
int error number; float real(complex* p);
main() { int* p = new int; cout << "sizeof(int) = " << sizeof(int) "\n"; }
float f; char* p; //... long ll = long(p); // преобразует p в long int i = int(f); // преобразует f в int
В C++ есть набор основных типов, которые соответствуют наиболее общим основным единицам памяти компьютера и наиболее общим основным способам их использования:
char short int int long int
float double
unsigned char unsigned short int unsigned int unsigned long int
const a = 1; static x;
1==sizeof(char)<=sizeof(short)<= sizeof(int)<=sizeof(long) sizeof(float)<=sizeof(double)
unsigned surprise = -1;
Основные типы можно свободно сочетать в присваиваниях и
выражениях. Везде, где это возможно, значения преобразуются так,
чтобы информация не терялась. Точные правила можно найти в #с.6.6.
Существуют случаи, в которых информация может теряться или
искажаться. Присваивание значения одного типа переменной другого
типа, представление которого содержит меньшее число бит, неизбежно
является источником неприятностей. Допустим, например, что
следующая часть программы выполняется на машине с двоичным
дополнительным представлением целых и 8-битовыми символами:
int i1 = 256+255; char ch = i1 // ch == 255 int i2 = ch; // i2 == ?
Другие типы модно выводить из основных типов (и типов, определенных пользователем) посредством операций описания:
* | указатель |
& | ссылка |
[] | вектор |
() | функция |
int* a; float v[10]; char* p[20]; // вектор из 20 указателей на символ void f(int); struct str { short length; char* p; };
int v[10]; // описывает вектор i = v[3]; // использует элемент вектора int* p; // описывает указатель i = *p; // использует указываемый объект
int* v[10]; // вектор указателей int (*p)[10]; // указатель на вектор
int x, y; // int x; int y;
int* p, y; // int* p; int y; НЕ int* y; int x, *p; // int x; int* p; int v[10], *p; // int v[10]; int* p;
Тип void (пустой) синтаксически ведет себя как основной тип. Однако использовать его можно только как часть производного типа, объектов типа void не существует. Он используется для того, чтобы указать, что функция не возвращает значения, или как базовый тип для указателей на объекты неизвестного типа.
void f() // f не возвращает значение void* pv; // указатель на объект неизвестного типа
void* allocate(int size); // выделить void deallocate(void*); // освободить f() { int* pi = (int*)allocate(10*sizeof(int)); char* pc = (char*)allocate(10); //... deallocate(pi); deallocate(pc); }
Для большинства типов T T* является типом указатель на T. То есть, в переменной типа T* может храниться адрес объекта типа T. Для указателей на вектора и указателей на функции вам, к сожалению, придется пользоваться более сложной записью:
int* pi; char** cpp; // указатель на указатель на char int (*vp)[10]; // указатель на вектор из 10 int'ов int (*fp)(char, char*); // указатель на функцию // получающую параметры (char, char*) // и возвращающую int
char c1 = 'a'; char* p = &c1; // в p хранится адрес c1 char c2 = *p; // c2 = 'a'
int strlen(char* p) { int i = 0; while (*p++) i++; return i; }
int strlen(char* p) { char* q = p; while (*q++) ; return q-p-1; }
Для типа T T[size] является типом "вектор из size элементов типа T". Элементы индексируются (нумеруются) от 0 до size-1. Например:
float v[3]; // вектор из трех float: v[0], v[1], v[2] int a[2][5]; // два вектора из пяти int char* vpc; // вектор из 32 указателей на символ
extern int strlen(char*); char alpha[] = "abcdefghijklmnoprstuvwxyz"; main() { int sz = strlen(alpha); for (int i=0; i.
'a' = 97 = 0141 = 0x61 'b' = 98 = 0142 = 0x62 'c' = 99 = 0143 = 0x63 ...
char v[9]; v = "строка"; // ошибка
int v1[] = { 1, 2, 3, 4 }; int v2[] = { 'a', 'b', 'c', 'd' }; char v3[] = { 1, 2, 3, 4 }; char v4[] = { 'a', 'b', 'c', 'd' };
int bad[5,2]; // ошибка
int v[5][2]; int bad = v[4,1]; // ошибка int good = v[4][1]; // ошибка Описание char v[2][5];
char v[2][5] = { 'a', 'b', 'c', 'd', 'e', '0', '1', '2', '3', '4' } main() { for (int i = 0; i<2; i++) { for (int j = 0; j<5; j++) cout << "v[" << i << "][" << j << "]=" << chr(v[i][j]) << " "; cout << "\n"; } }
v[0][0]=a v[0][1]=b v[0][2]=c v[0][3]=d v[0][4]=e v[1][0]=0 v[1][1]=1 v[1][2]=2 v[1][3]=3 v[1][4]=4
Указатели и вектора в C++ связаны очень тесно. Имя вектора можно использовать как указатель на его первый элемент, поэтому пример с алфавитом можно было написать так:
char alpha[] = "abcdefghijklmnopqrstuvwxyz"; char* p = alpha; char ch; while (ch = *p++) cout << chr(ch) << " = " << ch << " = 0" << oct(ch) << "\n";
char* p = &alpha[0];
extern int strlen(char*); char v[] = "annemarie"; char* p = v; strlen(p); strlen(v);
main() { char cv[10]; int iv[10]; char* pc = cv; int* pi = iv; cout << "char* " << long(pc+1)-long(pc) << "\n"; cout << "int* " << long(ic+1)-long(ic) << "\n"; }
char* 1 int* 4
Вектор есть совокупность элементов одного типа; struct является совокупностью элементов (практически) произвольных типов. Например:
struct address { // почтовый адрес char* name; // имя "Jim Dandy" long number; // номер дома 61 char* street; // улица "South Street" char* town; // город "New Providence" char* state[2]; // штат 'N' 'J' int zip; // индекс 7974 }
address jd; jd.name = "jim dandy"; jd.number = 61;
address jd = { "jim dandy", 61, "south street", "new providence", {'n','j'}, 7974 };
void print_addr(address* p) { cout << p->name << "\n" << p->number << " " << p->street << "\n" << p->town << "\n" << chr(p->state[0]) << chr(p->state[1]) << " " << p->zip << "\n"; }
address current; address set_current(address next) { address prev = current; current = next; return prev; }
struct link{ link* previous; link* successor; }
struct no_good { no_good member; };
struct list; // должна быть определена позднее struct link { link* pre; link* suc; link* member_of; }; struct list { link* head; }
Два структурных типа являются различными даже когда они имеют одни и те же члены. Например:
struct s1 { int a; }; struct s2 { int a; };
s1 x; s2 y = x; // ошибка: несоответствие типов
s1 x; int i = x; // ошибка: несоответствие типов
typedef char* Pchar; Pchar p1, p2; char* p3 = p1;
Ссылка является другим именем объекта. Главное применение ссылок состоит в спецификации операций для типов, определяемых пользователем; они обсуждаются в Главе 6. Они могут также быть полезны в качестве параметров функции. Запись x& означает ссылка на x. Например:
int i = 1; int& r = i; // r и i теперь ссылаются на один int int x = r // x = 1 r = 2; // i = 2;
int ii = 0; int& rr = ii; rr++; // ii увеличивается на 1
double& dr = 1;
double* drp; // ссылка, представленная как указатель double temp; temp = double(1); drp = &temp;
int x = 1; void incr(int& aa) { aa++; } incr(x) // x = 2
int x = 1; int next(int p) { return p+1; } x = next(x); // x = 2 void inc(int* p) { (*p)++; } inc(&x); // x = 3
struct pair { char* name; int val; };
const large = 1024; static pair vec[large+1}; pair* find(char* p) /* поддерживает множество пар "pair": ищет p, если находит, возвращает его "pair", иначе возвращает неиспользованную "pair" */ { for (int i=0; vec[i].name; i++) if (strcmp(p,vec[i].name)==0) return &vec[i]; if (i == large) return &vec[large-1]; return &vec[i]; }
int& value(char* p) { pair* res = find(p); if (res->name == 0) { // до сих пор не встречалось: res->name = new char[strlen(p)+1]; // инициализировать strcpy(res->name,p); res->val = 0; // начальное значение 0 } return res->val; }
const MAX = 256; // больше самого большого слова main() // подсчитывает число вхождений каждого слова во вводе { char buf[max]; while (cin>>buf) value(buf)++; for (int i=0; vec[i].name; i++) cout << vec[i].name << ": " << vec [i].val << "\n"; }
aa bb bb aa aa bb aa aa
aa: 5 bb: 3
Во многих машинных архитектурах можно обращаться к (небольшим) объектам заметно быстрее, когда они помещены в регистр. В идеальном случае компилятор будет сам определять оптимальную стратегию использования всех регистров, доступных на машине, для которой компилируется программа. Однако это нетривиальная задача, поэтому иногда программисту стоит дать подсказку компилятору. Это делается с помощью описания объекта как register. Например:
register int i; register point cursor; register char* p;
2.4.1 Целые Константы | |
2.4.2 Константы с Плавающей Точкой | |
2.4.3 Символьные Константы | |
2.4.4 Строки | |
2.4.5 Ноль | |
2.4.6 Const | |
2.4.7 Перечисления |
C++ дает возможность записи значений основных типов: символьных
констант, целых констант и констант с плавающей точкой. Кроме того,
ноль (0) может использоваться как константа любого указательного
типа, и символьные строки являются константами типа char[]. Можно
также задавать символические константы. Символическая константа -
это имя, значение которого не может быть изменено в его области
видимости. В C++ имеется три вида символических констант: (1)
любому значению любого типа можно дать имя и использовать его как
константу, добавив к его описанию ключевое слово const; (2)
множество целых констант может быть определено как перечисление; и
(3) любое имя вектора или функции является константой.
Целые константы предстают в четырех обличьях: десятичные,
восьмеричные, шестнадцатиричные и символьные константы. Десятичные
используются чаще всего и выглядят так, как можно было бы ожидать:
Константы с плавающей точкой имеют тип double. Как и в предыдущем
случае, компилятор должен предупреждать о константах с плавающей
точкой, которые слишком велики, чтобы их можно было представить.
Вот некоторые константы с плавающей точкой:
2.4.1 Целые Константы
0 1234 976 12345678901234567890
Десятичная константа имеет тип int, при условии, что она влезает в
int, в противном случае ее тип long. Компилятор должен
предупреждать о константах, которые слишком длинны для
представления в машине.
Константа, которая начинается нулем за которым идет x (0x),
является шестнадцатиричным числом (с основанием 16), а константа,
которая начинается нулем за которым идет цифра, является
восьмеричным числом (с основанием 8). Вот примеры восьмеричных
констант:
0 02 077 0123
их десятичные эквиваленты - это 0, 2, 63, 83. В шестнадцатиричной
записи эти константы выглядят так:
0x0 0x2 0x3f 0x53
Буквы a, b, c, d, e и f, или их эквиваленты в верхнем регистре,
используются для представления чисел 10, 11. 12, 13, 14 и 15,
соответственно. Восьмеричная и шестнадцатиричная записи наиболее
полезны для записи набора битов; применение этих записей для
выражения обычных чисел может привести к неожиданностям. Например,
на машине, где int представляется как двоичное дополнительное
шестнадцатеричное целое, 0xffff является отрицательным десятичным
числом -1; если бы для представления целого использовалось большее
число битов, то оно было бы числом 65535.
2.4.2 Константы с Плавающей Точкой
1.23 .23 0.23 1. 1.0 1.2e10 1.23e-15
Заметьте, что в середине константы с плавающей точкой не может
встречаться пробел. Например, 65.43 e-21 является не константой с
плавающей точкой, а четырьмя отдельными лексическими символами
(лексемами):
65.43 e - 21
и вызовет синтаксическую ошибку.
Если вы хотите иметь константу с плавающей точкой типа float, вы
можете определить ее так (#2.4.6):
const float pi = 3.14159265;
Хотя в C++ и нет отдельного символьного типа данных, точнее, символ может храниться в целом типе, в нем для символов имеется специальная и удобная запись. Символьная константа - это символ, заключенный в одинарные кавычки; например, 'a' или '0'. Такие символьные константы в действительности являются символическими константами для целого значения символов в наборе символов той машины, на которой будет выполняться программа (который не обязательно совпадает с набором символов, применяемом на том компьютере, где программа компилируется). Поэтому, если вы выполняетесь на машине, использующей набор символов ASCII, то значением '0' будет 48, но если ваша машина использует EBCDIC, то оно будет 240. Употребление символьных констант вместо десятичной записи делает программу более переносимой. Несколько символов также имеют стандартные имена, в которых обратная косая \ используется как escape-символ:
'\b' | возврат назад |
'\f' | перевод формата |
'\n' | новая строка |
'\r' | возврат каретки |
'\t' | горизонтальная табуляция |
'\v' | вертикальная табуляция |
'\\' | обратная косая (обратный слэш) |
'\'' | одинарная кавычка |
'\"' | двойная кавычка |
'\0' | null, пустой символ, целое значение 0 |
'\6' '\x6' 6 ASCII ack '\60' '\x30' 48 ASCII '0' '\137' '\x05f' 95 ASCII '_'
Строковая константа - это последовательность символов, заключенная в двойные кавычки:
"это строка"
sizeof("asdf")==5;
cout << "гудок в конце сообщения\007\n"
"это не строка, а синтаксическая ошибка"
cout << "здесь все \ ok"
здесь все ok
char v1[] = "a\x0fah\0129"; // 'a' '\xfa' 'h' '\12' '9' char v2[] = "a\xfah\129"; // 'a' '\xfa' 'h' '\12' '9' char v3[] = "a\xfad\127"; // 'a' '\xfad' '\127'
Ноль (0) можно употреблять как константу любого целого,
плавающего или указательного типа. Никакой объект не размещается по
адресу 0. Тип нуля определяется контекстом. Обычно (но не
обязательно) он представляется набором битов все-нули
соответствующей длины.
Ключевое слово const может добавляться к описанию объекта, чтобы
сделать этот объект константой, а не переменной. Например:
Есть другой метод определения целых констант, который иногда
более удобен, чем применение const. Например:
2.4.6 Const
const int model = 145;
const int v[] = { 1, 2, 3, 4 };
Поскольку константе ничего нельзя присвоить, она должна быть
инициализирована. Описание чего-нибудь как const гарантирует, что
его значение не изменится в области видимости:
model = 145; // ошибка
model++; // ошибка
Заметьте, что const изменяет тип, то есть ограничивает способ
использования объекта, вместо того, чтобы задавать способ
размещения константы. Поэтому например вполне разумно, а иногда и
полезно, описывать функцию как возвращающую const:
const char* peek(int i)
{
return private[i];
}
Функцию вроде этой можно было бы использовать для того, чтобы
давать кому-нибудь читать строку, которая не может быть затерта или
переписана (этим кем-то).
С другой стороны, компилятор может несколькими путями
воспользоваться тем, что объект является константой (конечно, в
зависимости от того, насколько он сообразителен). Самое очевидное -
это то, что для константы не требуется выделять память, поскольку
компилятор знает ее значение. Кроме того, инициализатор константы
часто (но не всегда) является константным выражением, то есть он
может быть вычислен на стадии компиляции. Однако для вектора
констант обычно приходится выделять память, поскольку компилятор в
общем случае не может вычислить, на какие элементы вектора сделаны
ссылки в выражениях. Однако на многих машинах даже в этом случае
может достигаться повышение эффективности путем размещения векторов
констант в память, доступную только для чтения.
Использование указателя вовлекает два объекта: сам указатель и
указываемый объект. Снабжение описания указателя "префиксом" const
делает объект, но не сам указатель, константой. Например:
const char* pc = "asdf"; // указатель на константу
pc[3] = 'a'; // ошибка
pc = "ghjk"; // ok
Чтобы описать сам указатель, а не указываемый объект, как
константный, используется операция const*. Например:
char *const cp = "asdf"; // константный указатель
cp[3] = 'a'; // ok
cp = "ghjk"; // ошибка
Чтобы сделать константами оба объекта, их оба нужно описать const.
Например:
const char *const cpc = "asdf"; // const указатель на const
cpc[3] = 'a'; // ошибка
cpc = "ghjk"; // ошибка
Объект, являющийся константой при доступе к нему через один
указатель, может быть переменной, когда доступ осуществляется
другими путями. Это в частности полезно для параметров функции.
Посредством описания параметра указателя как const функции
запрещается изменять объект, на который он указывает. Например:
char* strcpy(char* p, const char* q); // не может изменить q
Указателю на константу можно присваивать адрес переменной,
поскольку никакого вреда от этого быть не может. Однако нельзя
присвоить адрес константы указателю, на который не было наложено
ограничение, поскольку это позволило бы изменить значение объекта.
Например:
int a = 1;
const c = 2;
const* p1 = &c; // ok
const* p2 = &a; // ok
int* p3 = &c; // ошибка
*p3 = 7; // меняет значение c
Как обычно, если тип в описании опущен, то он предполагается int.
2.4.7 Перечисления
enum { ASM, AUTO, BREAK };
определяет три целых константы, называемы перечислителями, и
присваивает им значения. Поскольку значения перечислителей по
умолчанию присваиваются начиная с 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 Экономия Пространства
2.5.1 Поля | |
2.5.2 Объединения |
В ходе программирования нетривиальных разработок неизбежно
наступает время, когда хочется иметь больше пространства памяти,
чем имеется или отпущено. Есть два способа выжать побольше
пространства из того, что доступно:
Использование char для представления двоичной переменной,
например, переключателя включено/выключено, может показаться
экстравагантным, но char является наименьшим объектом, который в
C++ может выделяться независимо. Можно, однако, сгруппировать
несколько таких крошечных переменных вместе в виде полей struct.
Член определяется как поле путем указания после его имени числа
битов, которые он занимает. Допустимы неименованные поля; они не
влияют на смысл именованных полей, но неким машинно-зависимым
образом могут улучшить размещение:
Рассмотрим проектирование символьной таблицы, в которой каждый
элемент содержит имя и значение, и значение может быть либо
строкой, либо целым:
[1] Помещение в байт более одного небольшого объекта; и
[2] Использование одного и того же пространства для хранения
разных объектов в разное время.
Первого можно достичь с помощью использования полей, второго -
через использование объединений. Эти конструкции описываются в
следующих разделах. Поскольку обычное их применение состоит чисто в
оптимизации программы, и они в большинстве случаев непереносимы,
программисту следует дважды подумать, прежде чем использовать их.
Часто лучше изменить способ управления данными; например, больше
полагаться на динамически выделяемую память (#3.2.6) и меньше на заранее выделенную статическую память.
2.5.1 Поля
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;
}
Однако применение полей для упаковки нескольких переменных в один
байт не обязательно экономит пространство. Оно экономит
пространство, занимаемое данными, но объем кода, необходимого для
манипуляции этими переменными, на большинстве машин возрастает.
Известны программы, которые значительно сжимались, когда двоичные
переменные преобразовывались из полей бит в символы! Кроме того,
доступ к 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 имеют один и тот же адрес. Отсюда следует, что все члены
объединения вместе занимают лишь столько памяти, сколько занимает
наибольший член.
Использование объединений таким образом, чтобы при чтении
значения всегда применялся тот член, с применением которого оно
записывалось, совершенно оптимально. Но в больших программах
непросто гарантировать, что объединения используются только таким
образом, и из-за неправильного использования могут появляться
трудно уловимые ошибки. Можно капсулизировать объединение таким
образом, чтобы соответствие между полем типа и типами членов было
гарантированно правильным (#5.4.6).
Объединения иногда используют для "преобразования типов" (это
делают главным образом программисты, воспитанные на языках, не
обладающих средствами преобразования типов, где жульничество
является необходимым). Например, это "преобразует" на VAX'е int в
int*, просто предполагая побитовую эквивалентность:
struct fudge { union { int i; int* p; }; }; fudge a; a.i = 4096; int* p = a.p; // плохое использование
fudge.p = 0; int i = fudge.i; // i не обязательно должно быть 0
union fudge { int i; int* p; };
const one = 1;
const num[] = { 1, 2 };
char str[] = "a short string";
*1 Команда #include была выброшена из примеров в этой
главе для экономии места. Она необходима в примерах, производящих
ввывод, чтобы они были полными. (прим. автора)