C_Kurs_Lekt / C_I_семестр / 06_Массивы
.pdfМАССИВЫ |
1 |
МАССИВЫ
Массив - это один из структурированных типов языка. Массивом называется конечная именованная последовательность однотипных величин. При описании массива, в квадратных скобках задается количество элементов массива (размерность): Определение одномерного массива типа type выглядит следующим образом:
type имя_массива[êонстантêое_выражение];
float а [10]; // описание массива из 10 вещественных чисел
При описание внешнего массива, который определен в другой части программы, где ему выделена память и (возможно) присвоены начальные значения его элементам. допустимо описание массива без указания количества его элементов: extern double rtt[];
Элементы массива нумеруются с нуля. При описании массива используются те же модификаторы (класс памяти, const и инициализатор), что и для простых переменных.
При определении массива может выполняться его инициализация. Инициализация выполняется по умолчанию, если массив статический или внешний. В этих случаях всем элементам массива присваиваются нулевые значения:
Явная инициализация элементов массива разрешена только при его определении и возможна двумя способами: либо с указанием размера массива в квадратных скобках, либо без явного указания (без конкретного выражения) в квадратных скобках. Количество элементов массива в этом случае компилятор определяет по числу начальных зна- чений в списке инициализации, помещенном в фигурных скобках при определении массива.
char СН[] = {'А', 'В', 'С'}; int IN[6] = { 10, 20, 30, 40};
При отсутствии константного выражения в квадратных скобках список начальных значений в определении массива обязателен. Если размер массива явно задан, то количество элементов в списке начальных значений не должно превышать размера массива.
В тех случаях, когда массив не определяется, а описывается, список начальных значений задавать нельзя Имя массива является указателем-константой, значением которой служит адрес первого элемента массива (с
индексом 0). Таким образом, для любого массива соблюдается равенство:
имя_массива = &имя_массмва = имя_массива[0]
К имени массива (указателю) применимы все правила адресной арифметики. Более того запись имя_массива[индеêс] является выражением с двумя операндами. Первый из них, т.е. имя_массива, - это константный указатель - адрес начала массива в основной памяти. Индекс - это выражение целого типа, определяющее смещение от начала массива. Используя операцию обращения по адресу * (раскрытие ссылки, разыменование), действие бинарной операции [ ] можно объяснить так: * (имя_массива + индеêс).
Пример массив int z[3] из трех элементов включает индексированные элементы z [0], z [1], z [2]. Это соглашение языка становится очевидным, если учесть, что индекс определяет не номер элемента, а его смещение относительно начала массива. Таким образом, *z - обращение к первому элементу z[0], * (z + 1) - обращение ко второму элементу z[l] и т.д.
Для доступа к элементу массива после его имени указывается номер элемента (индекс) в квадратных скобках. Обращение к элементу массива в языке Си относят к постфиксному выражению вида ÐÅ [IE]. Постфиксное выражение ÐÅ должно быть указателем на нужный тип, выражение IE в квадратных скобках должно быть целочисленного типа. Таким образом, если ÐÅ - указатель на массив, то ÐÅ [IE] - индексированный элемент этого массива. * (ÐÅ + IE) - другой путь доступа к тому же элементу массива. Поскольку сложение коммутативно, то возможна такая эквивалентная запись* (IE + ÐÅ) и, следовательно, IE [ÐÅ] именует тот же элемент массива, что и РЕ [IE]
Размерность массивов предпочтительнее задавать с помощью именованных констант.
При обращении к элементам массива автоматический контроль выхода индекса за границу массива не производится, что может привести к ошибкам.
Многомерные массивы
Многомерный массив в соответствии с синтаксисом языка есть массив массивов, т.е. массив, элементами которого служат массивы. Определение общем случае должно содержать сведения о типе, размерности и количествах элементов каждой размерности:
type имя_массива[К1][К2]...[KN];
Здесь type - допустимый тип (основной или производный), имя_массива - идентификатор, N - размерность массива, К1 - количество в массиве элементов размерности N-1 каждый и т.д. Например:
int ARRAY[4][3][6];
Трехмерный массив ARRAY состоит из четырех элементов, каждый из которых - двухмерный массив с размерами 3 на 6. В памяти массив ARRAY, как и все многомерные массивы, размещается в порядке возрастания самого правого(последнего) индекса , т.е. самый младший адрес имеет элемент ARRAY [0] [0] [0], затем идет ARRAY [0] [0] [1] è ò.ä.
I Схема размещения в памяти трехмерного массива
Ïðè инициализации многомерного массива он представляется либо как массив из массивов, при этом каждый массив заключается в свои фигурные скобки, либо задается общий список элементов в том порядке, в котором элементы располагаются в памяти:
МАССИВЫ |
2 |
int ARRAY[4][3][6] = {0, 1, 2, 3, 4, 5, 6, 7}; -Â данном определении начальные значения получи-
ли только "первые" 8 элементов трехмерного массива.т.е.: ARRAY[0][0][0] |
= |
0, АRRAY[0][0][1] |
= 1, |
||
ARRAY[0][0][2] |
= 2, ARRAY[0][0][3] |
= 3, ARRAY[0][0][4] = |
4, |
ARRAY[0][0][5] = |
5, |
ARRAY[0][1][0] |
= 6, ARRAY[0][1][1] = |
7 |
|
|
|
Если необходимо инициализировать только часть элементов многомерного массива, но они размещены не в его начале или не подряд, то можно вводить дополнительные фигурные скобки, каждая пара которых выделяет последовательность значений, относящихся к одной размерности. (Нельзя использовать скобки без информации внутри
íèõ.) |
|
|
Следующее определение с инициализацией трехмерного массива |
||
int А[4][5][6] = { { |
{0} } , |
|
{ |
(100), |
{110, 111) }, |
{ |
{200}, |
{210}, {220, 221, 222} } ; |
задает только некоторые значения |
его элементов: А[0][0][0]=0, А[1][0][0]=100, |
А[1][1][0]=110, |
||
А[1][1][1]=111, |
А[2][0][0]=200, |
А[2][1][0]=210, |
А[2][2][0]=220, |
А[2][2][1]=221, |
А[2][2][2]=222. |
Остальные элементы массива явно не инициализируются. |
|
Если многомерный массив при определении инициализируется, то его самая левая размерность может в скобках не указываться. Количество элементов компилятор определяет по числу членов в инициализирующем списке. Например, определение
float matrix [][5] ={ {1}, (2}, {3} };
формирует массив matrix с размерами 3 на 5, но не определяет явно начальных значений всех его элементов. Как и в случае одномерных массивов, доступ к элементам многомерных массивов возможен с помощью индек-
сированных переменных и с помощью указателей. Возможно объединение обоих способов в одном выражении. Чтобы не допускать ошибок при обращении к элементам многомерных массивов с помощью указателей, нужно помнить, что при добавлении целой величины к указателю его внутреннее значение изменяется на "длину" элемента соответствующего типа. Имя массива всегда константа-указатель. Для массива, определенного как type AR [N][M][L], AR - указатель, поставленный в соответствие элементам типа type [M][L] . Добавление 1 к указателю AR приводит к изменению значения адреса на величину sizeof(type) * М * L.
Именно поэтому выражение *(AR + 1) есть адрес элемента AR[1], т.е. указатель на массив меньшей размерности, отстоящий от начала массива, т.е. от &AR[0], на размер одного элемента type[M][L]. Пример: задан массив int b[3][2][4], адрес массива b[][][] = b, адрес массива b[0][][] = *b, адрес массива b[0][0][] = **b, элемент b[0][0][0] = ***b,
адрес массива b[1][][] = *(b+1), адрес массива b[2][][] = *(b+2), адрес массива b[0][1][] = *(*b+1)
В общем случае для трехмерного массива индексированный элемент b[i] [j] [k] соответствует выражению
*(*(*(b + i) + j) + k
Допустимо в одном выражении комбинировать обе формы доступа к элементам многомерного массива: Пример: доступ к элементу b[1][1][1] можно осуществить разыменованием *(*(*(b+1)+1)+1) или *(b[1][1]+1) или *(b[1]+1)[1]
Как бы ни был указан путь доступа к элементу многомерного массива, внутренняя адресная арифметика, используемая компилятором, всегда предусматривает действия с конкретными числовыми значениями адресов. Компилятор всегда реализует доступ к элементам массива с помощью указателей и операции разыменования. Если в программе использована, например, такая индексированная переменная: AR[i] [j] [k], принадлежащая массиву type AR[N] [M] [L], где N, M, L - целые положительные константы, то последовательность действий компилятора такова:
выбирается адрес начала массива, т.е. целочисленное значение указателя AR, равное (unsigned long) AR;
добавляется смещение i * (M * L) * sizeof(type) для вычисления начального адреса i-ro массива с размерами M на L, входящего в исходный трехмерный массив;
добавляется смещение для вычисления начального адреса j-й строки (одномерный массив), включающей L элементов. Теперь смещение равно (i * (M * L) + j * L) * sizeof (type);
добавляется смещение для получения адреса k-ro элемента в строке, т.е. получается адрес (unsigned long)(i*(M * L) + j * L + k) * sizeof(type);
применяется разыменование, т.е. обеспечивается доступ к содержимому элемента по его адресу: * ((unsigned long) (i * (M * L) + j * L + k)).
Строки
Строка представляет собой массив символов, заканчивающийся нуль-символом. Нуль-символ — это символ с кодом, равным 0, что записывается в виде управляющей последовательности '\0'. По положению нуль-символа определяется фактическая длина строки. Строку можно инициализировать строковым литералом:
char str[10] = "Vasia";
// выделено 10 элементов с номерами от 0 до 9
// первые элементы- 'V'. 'a', 's', 'i', 'a', '\0'-
В этом примере под строку выделяется 10 байт, 5 из которых занято под символы строки, а шестой — под нуль-символ. Если строка при определении инициализируется, ее размерность можно опускать (компилятор сам выделит соответствующее количество байт):
char str[] - "Vasia": // выделено и заполнено 6 байт
Оператор
char *str = "Vasia"
создает не строковую переменную, а указатель на строковую константу, изменить которую невозможно (к примеру, оператор str[l]='o' не допускается). Знак равенства перед строковым литералом означает инициализацию, а не присваивание. Операция присваивания одной строки другой не определена (поскольку строка является массивом) и может выполняться с помощью цикла или функций стандартной библиотеки. Библиотека предоставляет возможности копирования, сравнения, объединения строк, поиска подстроки, определения длины строки и т. д.
Массивы указателей.
Следующее определение - int *array[6]; - вводит массив указателей на объекты типа int. Имя массива array, он состоит из шести элементов, тип каждого int *.
МАССИВЫ |
3 |
Определение int (*ptr)[6]; вводит указатель ptr на массив из шести элементов, каждый из которых имеет тип int. Таким образом, выражение (array + 1) соответствует перемещению в памяти на sizeof (int *) байтов от нача- ла массива (т.е. на длину указателя типа int *). Если прибавить 1 к ptr, то адрес изменится на величину sizeof (int [6]), т.е. на 12 байт при двухбайтовом представлении данных типа int.
По определению массива, его элементы должны быть однотипными и одного "размера". Предположим, что мы хотим определить массив для представления списка фамилий (учеников класса, сотрудников фирмы и т.п.). Если определять его как двухмерный массив элементов типа char, то в определении для элементов массива необходимо задать предельные размеры каждого из двух индексов.
Таким образом, "прямолинейное" определение массива для хранения списка фамилий может быть таким: char
spisok[25][20];
Для примера здесь предполагается, что количество фамилий в списке не более 25 и что длина каждой фамилии не превышает 19 символов (букв). После такого определения или с помощью инициализа ции в самом определении в элементы spisok[0], spisok[l], ... можно занести конкретные фамилии, представленные в виде строк. Размеры так определенного массива всегда фиксированы.
При определении массива один из его предельных размеров (самого левого индекса) можно не указывать. В этом случае количество элементов массива определяется, например, инициализацией:
char spisok[][20] = { "Иванов",
"Петров", "Сидоров" };
Теперь в массиве spisok только 3 элемента, каждый из них длиной 20 элементов типа char .
Двухмерный массив char spisok[3] [20] и одномерный массив указателей char *pointer [ 3 ], инициализированные одинаковыми строками
Нерациональное использование памяти и в этом случае налицо -даже для коротких строк всегда выделяется одно и то же количество байтов, заранее указанное в качестве предельного значения второго индекса массива spisok.
В противоположность этому при определении и инициализации теми же символьными строками одномерного массива указателей типа char * память распределяется гораздо рациональнее:
char *pointer[] = { "Иванов",
"Петров", "Сидоров" };
Для указателей массива pointer, в котором при таком определении 3 элемента и каждый является указателемпеременной типа char *, выделяется всего 3*sizeof (char *) байтов. Кроме того, компилятор размещает в памяти три строковые константы "Иванов" (7 байт), "Петров" (7 байт), "Сидоров" (8 байт), а их адреса становятся значениями элементов pointer[0], pointer[l], pointer[2].
Динамические массивы создают с помощью функции malloc библиотеки С, например:
int n = 100;
float *p = (float *) malloc(n * sizeof(float));
В этой строке создается переменная-указатель на float, в динамической памяти отводится непрерывная область, достаточная для размещения 100 элементов вещественного типа, и адрес ее начала записывается в указатель р. Динамические массивы нельзя при создании инициализировать, и они не обнуляются.
Преимущество динамических массивов состоит в том, что размерность может быть переменной, то есть объем памяти, выделяемой под массив, определяется на этапе выполнения программы. Доступ к элементам динамического массива осуществляется точно так же, как к статическим, например, к 5-му элементу приведенного выше массива можно обратиться как р[5] или *(р+5).
Операция преобразования типа, записанная перед обращением к функции та malloc, требуется потому, что функция возвращает значение указателя типа void*, а инициализируется указатель на float.
Память, зарезервированная под динамический массив с помощью функции malloc, должна освобождаться функции free, например: free (p);
ПРИМЕЧАНИЕ: Для правильной интерпретации объявлений полезно запомнить мнемоническое правило: «суффикс прежде префикса». Если при описании переменной используются одновременно префикс * (указатель) и суффикс [] (массив), то переменная интерпретируется как массив указателей, а не указатель на массив: int *p[10]: — массив из 10 указателей на int.