int* a;
float v[10];
char* p[20]; // вектор из 20 указателей на символ
void f(int);
struct str { short length; char* p; };
Правила построения типов с помощью этих операций подробно
объясняются в #с.8.3-4. Основная идея состоит в том, что описание
производного типа отражает его использование. Например:
- стр 55 -
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;
Мнение автора таково, что подобные конструкции делают программу
менее удобочитаемой, и их следует избегать.
2.3.4 Тип void
Тип void (пустой) синтаксически ведет себя как основной тип.
Однако использовать его можно только как часть производного типа,
объектов типа void не существует. Он используется для того, чтобы
указать, что функция не возвращает значения, или как базовый тип
для указателей на объекты неизвестного типа.
void f() // f не возвращает значение
void* pv; // указатель на объект неизвестного типа
Переменной тиа void* можно присваивать указатель любого типа. На
первый взгляд это может показаться не особенно полезным, поскольку
void* нельзя разыменовать, но именно это ограничение и делает тип
void* полезным. Главным образом, он применяется для передачи
указателей функциям, которые не позволяют сделать предположение о
типе объекта, и для возврата из функций нетипизированных объектов.
Чтобы использовать такой объект, необходимо применить явное
- стр 56 -
преобразование типа. Подобные функции обычно находятся на самом
нижнем уровне системы, там, где осуществляется работа с основными
аппаратными ресурсами. Например:
void* allocate(int size); // выделить
void deallocate(void*); // освободить
f() {
int* pi = (int*)allocate(10*sizeof(int));
char* pc = (char*)allocate(10);
//...
deallocate(pi);
deallocate(pc);
}
2.3.5 Указатели
Для большинства типов 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'
Переменная, на которую указывает p,- это c1, а значение, которое
хранится в c1, это 'a', поэтому присваиваемое c2 значение *p есть
'a'.
Над указателями можно осуществлять некоторые арифметические
действия. Вот, например, функция, подсчитывающая число символов в
строке (не считая завершающего 0):
int strlen(char* p)
{
int i = 0;
while (*p++) i++;
return i;
}
Другой способ найти длину состоит в том, чтобы сначала найти конец
строки, а затем вычесть адрес начала строки из адреса ее конца:
- стр 57 -
int strlen(char* p)
{
char* q = p;
while (*q++) ;
return q-p-1;
}
Очень полезными могут оказаться указатели на функции; они
обсуждаются в #4.6.7.
2.3.6 Вектора
Для типа 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. Функция strlen() использовалась
для подсчета числа символов в alpha; вместо этого можно было
использовать значение размера alpha (#2.4.4). Если применяется
набор символов ASCII, то выдача выглядит так:
'a' = 97 = 0141 = 0x61
'b' = 98 = 0142 = 0x62
'c' = 99 = 0143 = 0x63
...
- стр 58 -
Заметим, что задавать размер вектора 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];
int bad = v[4,1]; // ошибка
int good = v[4][1]; // ошибка
Описание
char v[2][5];
описывает вектор из двух элементов, каждый из которых является
вектором типа char[5]. В следующем примере первый из этих векторов
инициализируется первыми пятью буквами, а второй - первыми пятью
цифрами.
- стр 59 -
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 Указатели и Вектора
Указатели и вектора в C++ связаны очень тесно. Имя вектора можно
использовать как указатель на его первый элемент, поэтому пример с
алфавитом можно было написать так:
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* применяется арифметическая операция,
- стр 60 -
предполагается, что p указывает на элемент вектора объектов типа T;
p+1 означает следующий элемент этого вектора, а 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)
2.3.8 Структуры
Вектор есть совокупность элементов одного типа; struct является
совокупностью элементов (практически) произвольных типов. Например:
- стр 61 -
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 не является достаточным для
работы с полным почтовым адресом, но в качетве примера
достаточен). Обратите внимание на точку с запятой в конце; это одно
из очень немногих мест в C++, где необходимо ставить точку с
запятой после фигурной скобки, поэтому люди склонны забывать об
этом.
Переменные типа 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.
К структурным объектам часто обращаются посредством указателей
используя операцию ->. Например: