Компьютерные науки - Учебники на русском языке - Скачать бесплатно
можно было бы написать так:
extern int strlen(char*);
char alpha[] = "abcdefghijklmnoprstuvwxyz";
main()
- 45 -
(*
int sz = strlen(alpha);
for (int i=0; i
char ch = alpha[i];
cout << "'" << chr(ch) << "'"
<< " = " << ch
<< " = 0" << oct(ch)
<< " = 0x" << hex(ch) << "\n";
*)
*)
Функция chr() возвращает представление небольшого целого
в виде строки; например, chr(80) это "P" на машине, на кото-
рой используется набор символов ASCII. Функция oct() строит
восьмеричное представление своего целого аргумента, а hex()
строит шестнадцатиричное представление своего целого аргумен-
та; chr() oct() и hex() описаны в . Функция
strlen() использовалась для подсчета числа символов в alpha;
вместо этого можно было использовать значение размера alpha
(#2.4.4). Если применяется набор символов ASCII, то выдача
выглядит так:
'a' = 97 = 0141 = 0x61
'b' = 98 = 0142 = 0x62
'c' = 99 = 0143 = 0x63
...
Заметим, что задавать размер вектора alpha необязатель-
но. Компилятор считает число символов в символьной строке,
указанной в качестве инициализатора. Использование строки как
инициализатора для вектора символов - удобное, но к сожалению
и единственное применение строк. Аналогичное этому присваива-
ние строки вектору отсутствует. Например:
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' *);
Заметьте, что v4 - вектор из четырех (а не пяти) симво-
лов; он не оканчивается нулем, как того требуют соглашение и
библиотечные подпрограммы. Обычно применение такой записи ог-
раничивается статическими объектами.
Многомерные массивы представляются как вектора векторов,
и применение записи через запятую, как это делается в некото-
рых других языках, дает ошибку при компиляции, так как запя-
тая (,) является операцией следования (см. #3.2.2). Попробуй-
те, например, сделать так:
int bad[5,2]; // ошибка
и так:
int v[5][2];
- 46 -
int bad = v[4,1]; // ошибка
int good = v[4][1]; // ошибка
Описание
char v[2][5];
описывает вектор из двух элементов, каждый из которых
является вектором типа char[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
2.3.7 Указатели и Вектора
Указатели и вектора в С++ связаны очень тесно. Имя век-
тора можно использовать как указатель на его первый элемент,
поэтому пример с алфавитом можно было написать так:
char alpha[] = "abcdefghijklmnopqrstuvwxyz";
char* p = alpha;
char ch;
while (ch = *p++)
cout << chr(ch) << " = " << ch
<< " = 0" << oct(ch) << "\n";
Описание p можно было также записать как
char* p = &alpha[0];
Эта эквивалентность широко используется в вызовах функ-
ций, в которых векторный параметр всегда передается как ука-
затель на первый элемент вектора. Так, в примере
extern int strlen(char*);
char v[] = "Annemarie";
char* p = v;
strlen(p);
strlen(v);
функции strlen в обоих вызовах передается одно и то же
значение. Вся штука в том, что этого невозможно избежать; то
есть не существует способа описать функцию так, чтобы вектор
v в вызове функции копировался (#4.6.3). Результат применения
к указателям арифметических операций +, -, ++ или -- зависит
от типа объекта, на который они указывают. Когда к указателю
p типа T* применяется арифметическая операция, предполагает-
ся, что p указывает на элемент вектора объектов типа T; p+1
- 47 -
означает следующий элемент этого вектора, а p- 1 - предыдущий
элемент. Отсюда следует, что значение p+1 будет на sizeof(T)
больше значения p. Например, выполнение
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
поскольку на моей машине каждый символ занимает один
байт, а каждое целое занимает четыре байта. Перед вычитанием
значения указателей преобразовывались к типу long с помощью
явного преобразования типа (#3.2.5). Они преобразовывались к
long, а не к "очевидному" int, поскольку есть машины, на ко-
торых указатель не влезет в int (то есть,
sizeof(int)
Вычитание указателей определено только тогда, когда оба
указателя указывают на элементы одного и того же вектора (хо-
тя в языке нет способа удостовериться, что это так). Когда из
одного указателя вычитается другой, результатом является чис-
ло элементов вектора между этими указателями (целое число).
Можно добавлять целое к указателю или вычитать целое из ука-
зателя; в обоих случаях результатом будет значение типа ука-
зателя. Если это значение не указывает на элемент того же
вектора, на который указывал исходный указатель, то результат
использования этого значения неопределен. Например:
int v1[10];
int v2[10];
int i = &v1[5]-&v1[3]; // 2
i = &v1[5]-&v2[3]; // результат неопределен
int* p = v2+2; // p == &v2[2]
p = v2-2; // p неопределено
2.3.8 Структуры
Вектор есть совокупность элементов одного типа, 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 (почтовый ад-
рес), состоящий из пунктов, требующихся для того, чтобы пос-
лать кому-нибудь корреспонденцию (вообще говоря, address не
- 48 -
является достаточным для работы с полным почтовым адресом, но
в качестве примера достаточен). Обратите внимание на точку с
запятой в конце; это одно из очень немногих мест в С++, где
необходимо ставить точку с запятой после фигурной скобки, по-
этому люди склонны забывать об этом.
Переменные типа address могут описываться точно также,
как другие переменные, а доступ к отдельным членам получается
с помощью операции . (точка). Например:
address jd;
jd.name = "Jim Dandy";
jd.number = 61;
Запись, которая использовалась для инициализации векто-
ров, можно применять и к переменным структурных типов. Напри-
мер:
address jd = (*
"Jim Dandy",
61, "South Street",
"New Providence", (*'N','J'*), 7974
*);
Однако обычно лучше использовать конструктор (#5.2.4).
Заметьте, что нельзя было бы инициализировать jd.state стро-
кой "NJ". Строки оканчиваются символом '\0', поэтому в "NJ"
три символа, то есть на один больше, чем влезет в jd.state.
К структурным объектам часто обращаются посредством ука-
зателей используя операцию ->. Например:
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;
*)
Остальные осмысленные операции, такие как сравнение (==
и !=) не определены. Однако пользователь может определить эти
операции, см. Главу 6. Размер объекта структурного типа нель-
зя вычислить просто как сумму его членов. Причина этого сос-
тоит в том, что многие машины требуют, чтобы объекты опреде-
ленных типов выравнивались в памяти только по некоторым
зависящим от архитектуры границам (типичный пример: целое
должно быть выравнено по границе слова), или просто гораздо
более эффективно обрабатывают такие объекты, если они вырав-
нены в машине. Это приводит к "дырам" в структуре. Например,
(на моей машине) sizeof(address) равен 24, а не 22, как можно
было ожидать.
- 49 -
Заметьте, что имя типа становится доступным сразу после
того, как оно встретилось, а не только после того, как пол-
ностью просмотрено все описание. Например:
struct link(*
link* previous;
link* successor;
*)
Новые объекты структурного типа не могут быть описывать-
ся, пока все описание не просмотрено, поэтому
struct no_good (*
no_good member;
*);
является ошибочным (компилятор не может установить раз-
мер no_good). Чтобы дать возможность двум (или более)
структурным типам ссылаться друг на друга, можно просто опи-
сать имя как имя структурного типа. Например:
struct list; // должна быть определена позднее
struct link (*
link* pre;
link* suc;
link* member_of;
*);
struct list (*
link* head;
*)
Без первого описания list описание link вызвало бы к
синтаксическую ошибку.
2.3.9 Эквивалентность типов
Два структурных типа являются различными даже когда они
имеют одни и те же члены. Например:
struct s1 (* int a; *);
struct s2 (* int a; *);
есть два разных типа, поэтому
s1 x;
s2 y = x; // ошибка: несоответствие типов
Структурные типы отличны также от основных типов, поэто-
му
s1 x;
int i = x; // ошибка: несоответствие типов
Однако, существует механизм для описания нового имени
для типа без введения нового типа. Описание с префиксом
typedef описывает не новую переменную данного типа, а новое
имя этого типа. Например:
typedef char* Pchar;
Pchar p1, p2;
char* p3 = p1;
Это может служить удобной сокращенной записью.
2.3.10 Ссылки
- 50 -
Ссылка является другим именем объекта. Главное примене-
ние ссылок состоит в спецификации операций для типов, опреде-
ляемых пользователем; они обсуждаются в Главе 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
допустимо, но rr++ не увеличивает ссылку; вместо этого +
+ применяется к int, которым оказывается ii. Следовательно,
после инициализации значение ссылки не может быть изменено;
она всегда ссылается на объект, который ей было дано обозна-
чать (денотировать) при инициализации. Чтобы получить указа-
тель на объект, денотируемый ссылкой rr, можно написать &rr.
Очевидным способом реализации ссылки является констант-
ный указатель, который разыменовывается при каждом использо-
вании. Это делает инициализацию ссылки тривиальной, когда
инициализатор является lvalue (объектом, адрес которого вы
можете взять, см. #с.5). Однако инициализатор для &T не обя-
зательно должен быть lvalue, и даже не должен быть типа T. В
таких случаях:
[1] Во-первых, если необходимо, применяется преобразова-
ние типа (#с.6.6-8, #с.8.5.6),
[2] Затем полученное значение помещается во временную
переменную и
[3] Наконец, ее адрес используется в качестве значения
инициализатора.
Рассмотрим описание
double& dr = 1;
Это интерпретируется так:
double* drp; // ссылка, представленная как указатель
double temp;
temp = double(1);
drp = &temp;
int x = 1;
void incr(int& aa) (* aa++; *)
incr(x) // x = 2
По определению семантика передачи параметра та же, что
семантика инициализации, поэтому параметр aa функции incr
становится другим именем для x. Однако, чтобы сделать прог-
рамму читаемой, в большинстве случаев лучше всего избегать
функций, которые изменяют значение своих параметров. Часто
- 51 -
предпочтительно явно возвращать значение из функции или тре-
бовать в качестве параметра указатель:
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;
*);
Основная идея состоит в том, что строка имеет ассоцииро-
ванное с ней целое значение. Легко определить функцию поиска
find(), которая поддерживает структуру данных, состоящую из
одного pair для каждой отличной отличной от других строки,
которая была ей представлена. Для краткости представления ис-
пользуется очень простая (и неэффективная) реализация:
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];
*)
Эту функцию может использовать функция value(), реализу-
ющая массив целых, индексированный символьными строками
(вместо обычного способа):
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;
*)
Для данной в качестве параметра строки value() находит
целый объект (а не значение соответствующего целого); после
чего она возвращает ссылку на него. Ее можно использовать,
- 52 -
например, так:
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";
*)
На каждом проходе цикл считывает одно слово из стандарт-
ной строки ввода cin в buf (см. Главу 8), а затем обновляет
связанный с ней счетчик с помощью find(). И, наконец, печата-
ется полученная таблица различных слов во введенном тексте,
каждое с числом его встречаемости. Например, если вводится
aa bb bb aa aa bb aa aa
то программа выдаст:
aa: 5
bb: 3
Легко усовершенствовать это в плане собственного типа
ассоциированного массива с помощью класса с перегруженной
операцией (#6.7) выбора [].
2.3.11 Регистры
Во многих машинных архитектурах можно обращаться к (не-
большим) объектам заметно быстрее, когда они помещены в ре-
гистр. В идеальном случае компилятор будет сам определять оп-
тимальную стратегию использования всех регистров, доступных
на машине, для которой компилируется программа. Однако это
нетривиальная задача, поэтому иногда программисту стоит дать
подсказку компилятору. Это делается с помощью описания объек-
та как register. Например:
register int i;
register point cursor;
register char* p;
Описание register следует использовать только в тех слу-
чаях, когда эффективность действительно важна. Описание каж-
дой переменной как register засорит текст программы и может
даже увеличить время выполнения (обычно воспринимаются все
инструкции по помещению объекта в регистр или удалению его
оттуда).
Невозможно получить адрес имени, описанного как
register, регистр не может также быть глобальным.
2.4 Константы
С++ дает возможность записи значений основных типов:
символьных констант, целых констант и констант с плавающей
точкой. Кроме того, ноль (0) может использоваться как конс-
танта любого указательного типа, и символьные строки являются
константами типа char[]. Можно также задавать символические
константы. Символическая константа - это имя, значение кото-
рого не может быть изменено в его области видимости. В С++
имеется три вида символических констант: (1) любому значению
- 53 -
любого типа можно дать имя и использовать его как константу,
добавив к его описанию ключевое слово const; (2) множество
целых констант может быть определено как перечисление; и (3)
любое имя вектора или функции является константой.
2.4.1 Целые Константы
Целые константы предстают в четырех обличьях: десятич-
ные, восьмеричные, шестнадцатиричные константа и символьные
константы. Десятичные используются чаще всего и выглядят так,
как можно было бы ожидать:
0 1234 976 12345678901234567890
Десятичная константа имеет тип int, при условии, что она
влезает в int, в противном случае ее тип long. Компилятор
должен предупреждать о константах, которые слишком длинны для
представления в машине.
Константа, которая начинается нулем за которым идет x (0
x), является шестнадцатиричным числом (с основанием 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 Константы с Плавающей Точкой
Константы с плавающей точкой имеют тип double. Как и в
предыдущем случае, компилятор должен предупреждать о констан-
тах с плавающей точкой, которые слишком велики, чтобы их мож-
но было представить. Вот некоторые константы с плавающей точ-
кой:
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;
- 54 -
2.4.3 Символьные Константы
Хотя в С++ и нет отдельного символьного типа данных,
точнее, символ может храниться в целом типе, в нем для симво-
лов имеется специальная и удобная запись. Символьная констан-
та - это символ, заключенный в одинарные кавычки; например,
'a' или '0'. Такие символьные константы в действительности
являются символическими константами для целого значения сим-
волов в наборе символов той машины, на которой будет выпол-
няться программа (который не обязательно совпадает с набором
символов, применяемом на том компьютере, где программа компи-
лируется). Поэтому, если вы выполняетесь на машине, использу-
ющей набор символов ASCII, то значением '0' будет 48, но если
ваша машина использует EBCDIC набор символов, то оно будет
240. Употребление символьных констант вместо десятичной запи-
си делает программу более переносимой. Несколько символов
также имеют стандартные имена, в которых обратная косая \ ис-
пользуется как escape-символ:
'\b', возврат назад
'\f', перевод формата
'\n', новая строка
'\r', возврат каретки
'\t', горизонтальная табуляция
'\v', вертикальная табуляция
'\\', \ обратная косая (обратный слэш)
'\'', одинарная кавычка '
'\"', двойная кавычка "
'\0', null, пустой символ, целое значение 0
Вопреки их внешнему виду каждое является одним символом.
Можно также представлять символ одно-, дву- или трехзначным
восьмеричным числом (символ \, за которым идут восьмеричные
цифры), или одно-, дву- или трехзначным шестнадцатиричным
числом (\x, за которым идут шестнадцатиричные цифры). Напри-
мер:
'\6' '\x6' 6 ASCII ack
'\60' '\x30' 48 ASCII '0'
'\137' '\x05f' 95 ASCII '_'
Это позволяет представлять каждый символ из машинного
набора символов, и в частности вставлять такие символы в сим-
вольные строки (см. следующий раздел). Применение числовой
записи для символов делает программу непереносимой между ма-
шинами с различными наборами символов.
2.4.4 Строки
Строковая константа - это последовательность символов,
заключенная в двойные кавычки "
"это строка"
Каждая строковая константа содержит на один символ боль-
ше, чем кажется; все они заканчиваются пустым символом '\0'
со значением 0. Например:
sizeof("asdf")==5;
Строка имеет тип "вектор из соответствующего числа сим-
волов", поэтому "asdf" имеет тип char[5]. Пустая строка запи-
сывается "" (и имеет тип char[1]). Заметьте, что для каждой
строки s strlen(s)==sizeof(s)-1, поскольку strlen() не учиты-
вает завершающий 0.
Соглашение о представлении неграфических символов с об-
- 55 -
ратной косой можно использовать также и внутри строки. Это
дает возможность представлять двойные кавычки и escape-сим-
вол. Самым обычным символом этого рода является, безусловно,
символ новой строки '\n'. Например:
cout << "гудок в конце сообщения\007\n"
где 7 - значение ASKII символа bel (звонок).
В строке невозможно иметь "настоящую" новую строку:
"это не строка,
а синтаксическая ошибка"
Однако в строке может стоять обратная косая, сразу после
которой идет новая строка; и то, и другое будет проигнориро-
вано. Например:
cout << "здесь все \
ok"
напечатает
здесь все ok
Новая строка, перед которой идет escape (обратная ко-
сая), не приводит к появлению в строке новой строки, это
просто договоренность о записи.
В строке можно иметь пустой символ, но большинство прог-
рамм не будет предполагать, что есть символы после него. Нап-
ример, строка "asdf\000hjkl" будет рассматриваться стандарт-
ными функциями, вроде strcpy() и strlen(), как "asdf".
Вставляя численную константу в строку с помощью
восьмеричной или шестнадцатиричной записи благоразумно всегда
использовать число из трех цифр. Читать запись достаточно
трудно и без необходимости беспокоиться о том, является ли
символ после константы цифрой или нет. Разберите эти примеры:
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'
Имейте в виду, что двухзначной шестнадцатиричной записи
на машинах с 9-битовым байтом будет недостаточно.
2.4.5 Ноль
Ноль можно употреблять как константу любого целого, пла-
вающего или указательного типа. Никакой объект не размещается
по адресу 0. Тип нуля определяется контекстом. Обычно (но не
обязательно) он представляется набором битов все-нули соот-
ветствующей длины.
2.4.6 Const
Ключевое слово const может добавляться к описанию объек-
та, чтобы сделать этот объект константой, а не переменной.
Например:
const int model = 145;
const int v[] = (* 1, 2, 3, 4 *);
Поскольку константе ничего нельзя присвоить, она должна
быть инициализирована. Описание чего-нибудь как const гаран-
тирует, что его значение не изменится в области видимости:
- 56 -
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 указатель, а не указываемый объ-
ект, как константный, используется операция 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
Указателю на константу можно присваивать адрес перемен-
ной, поскольку никакого вреда от этого быть не может. Однако
нельзя присвоить адрес константы указателю, на который не бы-
- 57 -
ло наложено ограничение, поскольку это позволило бы изменить
значение объекта. Например:
int a = 1;
const c = 2;
const* p1 = &c; // ok
const* p2 = &a; // ok
int* p3 = &c; // ошибка
*p3 = 7; // меняет значение c
Как обычно, если тип в описании опущен, то он предпола-
гается int.
2.4.7 Перечисления
Есть другой метод определения целых констант, который
иногда более удобен, чем применение const. Например:
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 Экономия Пространства
В ходе программирования нетривиальных разработок неиз-
- 58 -
бежно наступает время, когда хочется иметь больше пространс-
тва памяти, чем имеется или отпущено. Есть два способа выжать
побольше пространства из того, что доступно:
[1] Помещение в байт более одного небольшого объекта и
[2] Использование одного и того же пространства для хра-
нения разных объектов в разное время.
Первого можно достичь с помощью использования полей,
второго - через использование объединений. Эти конструкции
описываются в следующих разделах. Поскольку обычное их приме-
нение состоит чисто в оптимизации программы, и они в боль-
шинстве случаев непереносимы, программисту следует дважды по-
думать, прежде чем использовать их. Часто лучше изменить
способ управления данными; например, больше полагаться на ди-
намически выделяемую память (#3.2.6) и меньше на заранее вы-
деленную статическую память.
2.5.1 Поля
Использование char для представления двоичной перемен-
ной, например, переключателя включено/выключено, может
показаться экстравагантным, но char является наименьшим объ-
ектом, который в С++ может выделяться независимо. Можно, од-
нако, сгруппировать несколько таких крошечных переменных
вместе в виде полей 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;
*)
Однако применение полей для упаковки нескольких перемен-
ных в один байт не обязательно экономит пространство. Оно
экономит пространство, занимаемое данными, но объем кода, не-
обходимого для манипуляции этими переменными, на большинстве
машин возрастает. Известны программы, которые значительно
сжимались, когда двоичные переменные преобразовывались из по-
лей бит в символы! Кроме того, доступ к char или int обычно
намного быстрее, чем доступ к полю. Поля - это просто удобная
- 59 -
и краткая запись для применения логических операций с целью
извлечения информации из части слова или введения информации
в нее.
2.5.2 Объединения
Рассмотрим проектирование символьной таблицы, в которой
каждый элемент содержит имя и значение, и значение может быть
либо строкой, либо целым:
struct entry (*
char* name;
char type;
char* string_value; // используется если type == 's'
int int_value; // используется если type == 'i'
*);
void print_entry(entry* p)
(*
|