8.1. Одномерные массивы
В Си имеются как обычные одномерные, так и многомерные массивы. Начнем мы, конечно, с рассмотрения одномерных массивов.
При описании массива обычно указываются тип и количество элементов. Вот так: int а [10 ] ;. Нумеруются элементы массива всегда с нуля, т.е. в массиве а имеются элементы с а[0] по а [9] . Массивы, как и переменные, можно инициализировать при описании. Пример:
int а[10]={0,1,2,3,4,5,6,7,8,9};
Замечание. Таким образом можно инициализировать массивы только при описании Просто присвоить значения всем элементам массива сразу (есть даже термин такой — "агрегатно") нельзя.
Почему мы сказали, что при описании обычно указываются тип и количество...? Потому что тип элементов указывается всегда, а вот количество может быть опущено при наличии списка инициализации, в этом случае компилятор сам вычислит количество элементов в массиве в зависимости от количества элементов в списке. Таким образом, приведенный выше пример эквивалентен следующему:
int а[]={0,1,2,3,4,5,6,7,8,9};
Замечание. Использовать эту возможность на практике, на наш взгляд, не следует, это ведь делает программу менее понятной. Мы упомянули об этом потому, что авторы популярных книг по Си в ряде случаев используют именно такую запись, развивая, видимо, арифметические способности читателей.
Доступ к элементам массива производится обычным образом — с указанием номера элемента в квадратных скобках: а [5] (собственно, выше мы уже это продемонстрировали).
Самое важное свойство массивов в Си, которое лежит в основе всех особенностей их использования, следующее: имя массива является константой-указателем на нулевой элемент этого массива. Это очень важно! Рассмотрим сначала простой пример, иллюстрирующий сказанное:
#include
<stdio.h>
#define N 10
void main(void)
{ int a[N]={0,l,2,3,4,5,6,7,8,9},
*p,i;
p-a;
for (i=0;i<N;i++)
printf("\na[%d]=%d",i,p[i]);
}
Таким образом, когда мы описываем массив, мы фактически описываем константу-указатель на соответствующий тип. В отличие от Паскаля в Си нельзя присвоить "массив массиву", т.е. следующий пример не пройдет компиляцию:
int a[N],b[N] ;
<„.>
a=b;
Ошибка в том, что и b, и, что для
данного примера более важно, а являются
константами-указателями и менять их
значения нельзя.
Теперь, пожалуй, надо разобраться с тем, почему мы так вольно написали р [ i ] , хотя вроде бы переменная p описана не как массив, а как обычный указатель. Дело в том, что для компилятора Си разницы на самом деле никакой нет. В следующем разделе мы попробуем разобраться с этим вопросом подробнее.
8.2. Операции над указателями. Массивы и указатели
В разделе 7.1 мы рассмотрели две основные операции, связанные с указателями, — & (взятие адреса) и * (разыменование). Эти операции имели аналоги в Паскале, и вопросов, связанных с ними, обычно бывает не много. Помимо этих операций, в Си имеются специфические для этого языка арифметические операции над указателями. Кратко рассмотрим их.
Пусть имеются следующие описания: int *р1,*р2,i;
Операция |
Пример |
Описание |
<указатель>+<целое> |
p2=pl+i; |
В результате прибавления к указателю целого числа (давайте считать, что положительного, иначе фактически получится операция вычитания) получается указатель, отстоящий от данного на количество единиц типа, на данные которого указывает указатель, в сторону возрастания адресов памяти. Пусть, к примеру, указатель р1 имел значение 100 (указывал на ячейку памяти с адресом 100). Тогда после выполнения операции р2=р1+5 указатель р2 будет иметь значение 110 (ведь данные типа int занимают в памяти 2 байта). Если бы мы описали указатели на char, считать было бы чуть проще, но и пример был бы менее показательным. |
<указатель>—<целое> |
p2=pl-i; |
Совершенно аналогичная операция, только результат отсчитывается в сторону убывания адресов. |
<указатель>++, <указатель>-- |
pl++; р1--; |
Эти операции тоже не должны вызывать вопросов. Надо только все время помнить, что действие (сдвиг указателя) производится "в единицах типа. на который он указывает". |
<указатель>--<указатель> |
i=pl-p2; |
Разность указателей (они должны указывать на данные одного типа' дает целое число, абсолютная величина которого показывает, сколько единиц типа, на данные которого указывает указатель, целиком помещается между ними, а знак разности говорит о том, какой из указателей "больше". |
Кроме того, в отличие от Паскаля к указателям можно применять операции отношения (смысл их очень простой — больше тот указатель, который указывает на ячейку с большим адресом). Рассмотрим несколько примеров.
#include <stdio.h>
#define N 10
void main(void)
{ int a[N]={0,l,2,3,4,5,6,7,8,9}, *p,i;
p=a;
for (i=0;i<N;i++,p++) printf("\na[%d]=%d",i,p);
}
А вот такой пример:
#include <stdio.h>
#define N 10
void main (void)
{ int a[N]={0,l,2,3,4,5,6,7,8,9}, i;
for (i=0;i<N;i++,a++) printf ("\na [%d.] =%d", i,
a) ;
}
не пройдет компиляцию, ибо а — константа и менять ее значение нельзя.
#include <stdio.h>
#define N 10
void main(void)
{ int a[N>(0,l,2,3,4,5, 6,7, 8, 9}, *p,i;
p=a;
for (i=0;i<N;i++) printf("\na[%d]=%d",i,*(p+i));
}
Этот пример демонстрирует, что записи p[i] и * (p+i) эквивалентны. Помните, именно этим вопросом мы закончили предыдущий раздел? Так вот, для компилятора действительно нет никакой разницы между этими записями, можно даже сказать, что вторая для него "более естественна". Запись р [ i ] он понимает именно как * (p+i).
После всего сказанного так же должно стать понятно, почему массивы всегда передаются в функции "по ссылке". Потому что всегда в действительности передаются именно указатели. Следующие заголовки функций для компилятора эквивалентны:
void
sort(int a[10])
void sort (int a[])
void sort(int *a)
Можно считать, что два первых описания он все равно понимает как третье.
8.3. Многомерные пассивы
Как уже было сказано выше, в Си имеется возможность использовать массивы больших размерностей. В практических задачах обычно используются двухмерные и трехмерные массивы.
Описание двухмерного массива выглядит примерно так: int а[3] [5] , трехмерного — int b[2] [3] [5]. Доступ к элементам массива производится обычным образом — а [ i ] [ j ] и a[i][j][k].
Замечание. Те, кто привык к записи а [ i, j ] в стиле Паскаля, должны отвыкнуть. Компилятор Си такой записи не понимает.
В памяти элементы многомерных массивов располагаются согласно правилу: быстрее всего меняется "старший" индекс. Для двухмерных массивов это означает, что в памяти они располагаются "по строкам". То есть элементы двухмерного массива а [ 3 ] [ 5 ] будут расположены в порядке
а[0][0], a[0][l], a[0][2], а[0][3], а[0][4], а[1,0], а[1][1], а[1][2], ..., а[2][2], а[2][3], а[2][4].
Элементы трехмерного массива b [ 2 ] [ 3 ] [ 5 ] будут расположены в памяти в следующем порядке:
b[0][0][0], b[0][0][1], b[0][0][2], b[0][0] [З], b[0][0][4], b[0][1][0], ..., b[1] [2] [З], b[1] [2] [4].
Так же, как и одномерные массивы, двухмерные имеют непосредственное отношение к указателям (имя массива также является константой-указателем, только в случае двухмерного массива — константой-указателем на указатель, в случае трехмерного... ух, произнесите это сами). Приведем соответствующий пример (в нем показана также инициализация двухмерного массива, которая обсуждается чуть ниже):
#include <stdio.h>
void main(void)
{ int a[2][3]={{1,2,3},{4,5,6}},i,j;
int (*p) [3];
p=a;
for (i=0;i<2,i++)
{ printf("\n") ;
for (j:=0;j<3;j++)
printf("%d=%d",*(*(p+i)+]),a[i][j]);
}
}
Как видно из приведенного примера, операция индексации a [i] [j ] в случае двухмерного массива, так же, как и в случае одномерного, сводится к операциям над указателями. Запись а [ i ] [ j ] понимается компилятором как серия операций над указателями; * (* (a+i) +j) . Интересно, что именно с этим связано правило передачи многомерных массивов в функции, которое гласит, что у массива, являющегося формальным параметром функции, необходимо явно указывать все размерности, кроме, возможно, первой (т.е. первую тоже можно указать, но это не обязательно, а. вот все остальные — обязательно). Например, при передаче двухмерного массива следующие заголовки функций понимаются компилятором одинаково:
void
sort(int m[4][5])
void sort (int (*m)[5])
void sort(int m[][5])
Как связаны между собой эти два факта: правило передачи многомерных массивов и преобразование операции индексации в операции над указателями? Кратко рассмотрим этот вопрос, нам он представляется достаточно важным для понимания сути операций над указателями в Си.
Как мы уже обсуждали выше, операция индексации одномерного массива а [ i ] понимается компилятором как запись *(a+i) . Для того чтобы выполнить операцию a+i, компилятору необходимо знать, сколько байт памяти занимает каждый элемент массива а. Эту информацию он имеет, поскольку знает тип элементов массива, В случае двухмерного массива при выполнении операции индексации выполняются две операции над указателями (запись а [i] [j] понимается как * (* (a+i) +j)). Для выполнения первой операции "+" (a+i) необходимо знать длину строки массива (т.е. вторую размерность), а для выполнения второй операции "+" * (a+i) +j знать первую размерность уже не надо, ведь величина сдвига здесь определяется типом элементов массива.
В случае трехмерного массива ситуация аналогичная: запись b[i] [j] [k] понимается компилятором как *(*(*(b+i)+j)+k).H при передаче трехмерных массивов в функции так же, как и в случае двухмерных, можно опускать только первую размерность. Так, записи
void
sort(int m[4][5][6])
void sort(int (*m)[5][6])
void sort-tint m[][5][6])
понимаются компилятором одинаково.
Замечание. Наблюдательные ребята, конечно, обратят внимание, что в описании int (*р)[3] (и в других аналогичных описаниях) мы использовали скобки. Дело в том, что записи int *р[3] и int (*р) [3] понимаются компилятором по-разному. В первом случае описывается массив из трех указателей на целое, Во втором — указатель на массив из трех целых.
8.4. Структуры (в терминах Паскаля - записи)
Структуры (struct) в Си являются аналогом записей (record) в Паскале. Типичный пример описания структуры:
struct point {int x,y;}
/* точка на плоскости с
целочисленными координатами; х и у являются
полями структуры point*/
Приведенный пример является именно описанием самой структуры, описание соответствующей переменной выглядит следующим образом: struct point a;
Обратите внимание, что переменная описывается не как просто point а, а именно как struct point а. В C++ таких (равно как и многих других) "сложностей" нет.
Переменные можно описывать и при описании самой структуры:
struct point {int x,y;} a,b,c;
/* описание структуры point и трех переменных a, b и с*/
Можно также описать переменные, не давая имя самой структуре. Это неудобно (ведь имя структуры не задается, следовательно, использовать его в дальнейшем нельзя), но мы все же приведем соответствующий пример "для полноты картины":
struct {int х,у;} a,b,c; /* описание трех "структурных" переменных a, b и с*/
Доступ к элементам структуры производится так же, как и в Паскале, с указанием имени переменной и имени поля. Пример: а.х — поле х переменной а.
При описании указателя на структуру (такие указатели часто используются для организации списков, деревьев и других динамических структур данных, о которых будет рассказано далее) доступ к полю можно осуществлять посредством операции —>. Приведем соответствующий пример. Пусть у нас уже имеется описание структуры point (см, выше), и мы описываем указатель на эту структуру: struct point *р. Для доступа к полю х в этом случае необходимо использовать запись (*р).х,но то же самое можно записать и короче: р—>х.
Структуры могут "вкладываться" друг в друга, это обычно делается для более точного описания объекта. Так, можно описать окружность посредством одной структуры struct circl {int x,y,r; }, но запись struct circl {struct point centre; int r; }, на наш взгляд, изящнее. Хотя за это придется "платить" усложнением доступа к полям структуры. В первом случае при описании переменной struct circl о для доступа к полю х достаточно написать просто о.х, а во втором требуется запись о.centre.х. Впрочем, в случае более сложных примеров (а мы приводим совсем простые) такие "усложнения" описания типов обычно оправданны, ибо улучшают читабельность программы. В заключение этого раздела приведем пример описания массива структур. Ничего особенного и нового в этом описании нет, но массивы структур приходится использовать часто, и мы хотим, чтобы соответствующий пример был у вас перед глазами. Итак, если у нас уже имеется описание структуры point, то описание массива из десяти точек выглядит следующим образом: struct point m[10]. Доступ к элементам массива и их полям производится следующим образом: m [ i ].х.
8.5. Объединения
В Паскале нет аналогов объединениям (union), да и в Си объединения используются не слишком часто (как написано в одной из популярных книг по Си, объединения чаще всего используются для "сомнительного преобразования типов"). Мы не будем много заниматься объединениями, но кратко поясним, что это такое.
Описание объединения похоже на описание структуры: union point { int х, у;}, но само объединение принципиально отличается от структуры тем, что все элементы объединения располагаются в памяти, начиная с одного и того же адреса. То есть переменные описанного нами объединения point будут занимать два байта памяти (элементы структуры point занимали четыре байта), и поля х и у данного объединения практически неразличимы. Приведем пример короткой программы, демонстрирующий сказанное:
#include
<stdio.h>
void main(void)
{ union point
{ int x,y;} a;
a.x=10;
printf("\nРазмер
объединения:%d
a.x=%d a.y=%d",sizeof(a),
a.x, a.y);
}
На печать будет выведено 2 10 10.
Замечание. В Турбо Паскале подобный механизм можно реализовать с использованием absolute. Но этот вопрос мы здесь не рассматриваем.
8.6. Описания типов (typedef)
Выше мы фактически уже продемонстрировали описание типов (перечислимого типа, структур и объединений). В этом разделе речь пойдет о специальном операторе typedef, который предназначен для объявления типов, но сначала приведем пример еще одного способа "псевдоописания" типа посредством директивы #define. Для "псевдоописания" типа можно использовать следующую конструкцию:
/* Описание "типа"
POINT */ #define
POINT struct point
POINT { int x,y;};
/* Описание переменных типа
POINT */
POINT a,b,c;
В приведенном примере тип point, конечно, не определяется, все построено на контекстных заменах. Но такой прием иногда используют, часто он встречается и в различных пособиях по языку Си,
Теперь о том, как в Си описываются именно типы. Рассмотрим сначала совсем простые примеры.
Вот описание обычной вещественной переменной с именем real: float real;. А вот описание типа real: typedef float real. После описания типа real можно описывать переменные данного типа: real а,Ь, с;
Вот описание целочисленного массива из десяти элементов: int array[10];..A вот описание типа array: typedef int array [10] . После описания типа array можно описывать переменные array ml,m2,m3; —все это описания "целочисленных массивов ml, m2, mЗ из 10 элементов.
Наверное, приведенных примеров достаточно, чтобы понять суть: описание переменной превращается в описание типа посредством добавления слова typedef перед описанием. Все очень изящно, никакого отдельного раздела описания типов, как в Паскале, не требуется.