Указатели на массивы

Практический курс по C/C++: https://stepik.org/course/193691

На этом занятии посмотрим, как указатели можно использовать совместно с массивами. Давайте предположим, что в программе объявляется следующий массив с именем ar:

#include <stdio.h>
 
int main(void) 
{
         short ar[] = {4, 3, 2, 1, 5, 6, 7};
 
         return 0;
}

Так вот, имя массива ar в языке Си является указателем на массив и содержит адрес первой ячейки, начиная с которой хранятся элементы этого массива:

А раз это так, то значение первого элемента можно получить с помощью операции разыменования:

short a_1 = *ar;
printf("a_1 = %d\n", a_1);

И, действительно, после запуска программы увидим на экране значение 4 первого элемента. Мало того, используя адресную арифметику, мы можем переходить к произвольному элементу и работать с ним. Например:

short a_4 = *(ar+3);
printf("a_4 = %d\n", a_4);

В результате, переменная a_4 будет содержать значение 1 четвертого элемента. Наконец, мы можем через цикл вывести все значения элементов этого массива следующим образом:

for(int i = 0; i < sizeof(ar) / sizeof(*ar); ++i)
         printf("%d ", *(ar+i));

Давайте внимательнее посмотрим на эту конструкцию. В цикле for вычисляется длина массива (число его элементов) с помощью операции sizeof. Вначале определяется общее число байт, занимаемое массивом sizeof(ar), а затем, оно делится на число байт, отведенное под один элемент sizeof(*ar). Получаем общее число элементов в массиве. Однако если ar – это указатель на массив, то почему операция sizeof(ar) возвращает количество байт массива, а не указателя? В действительности, здесь ar является именно указателем на массив, а не просто указателем на ячейки памяти. В языке Си хоть и отсутствует массив, как структура данных, но указатель такого вида все же присутствует. Он формируется автоматически, как только мы объявляем какой-либо массив в программе. Его имя становится указателем на массив.

Чем же отличается указатель на массив от обычного указателя? В целом, только двумя моментами:

  • операция sizeof для указателя на массив возвращает число байт, занимаемых массивом в памяти устройства;
  • адрес указателя на массив формируется в момент его объявления (размещения в памяти устройства) и остается неизменным на протяжении работы программы.

Давайте я на конкретных примерах продемонстрирую эти особенности. Итак, пусть у нас имеется массив из целых чисел и указатель того же типа:

#include <stdio.h>
 
int main(void) 
{
         short ar[] = {4, 3, 2, 1, 5, 6, 7};
         short *ptr_ar;
 
         return 0;
}

Так как ar – это указатель, то мы можем присвоить указателю ptr_ar адрес массива ar следующим образом:

ptr_ar = ar;

или так:

ptr_ar = &ar[0];

Но первый вариант лучше и визуально и требует меньшего числа операций.

Далее проверим, можно ли работать с массивом ar через указатель ptr_ar:

short x = *ptr_ar;
*(ptr_ar+1) = -3;

Действительно, при отладке видим, что переменная x содержит значение 4, а массив ar изменил второй элемент на -3.

Теперь проверим, как будет работать операция sizeof с указателями ar и ptr_ar:

size_t len_1 = sizeof(ar);         // len_1 = 14
size_t len_2 = sizeof(ptr_ar);     // len_2 = 4

После запуска в режиме отладки видим значения переменных len_1 = 14 и len_2 = 4. То есть, эти указатели все же немного различаются. Указатель на массив выдает общее число байт, отведенных под массив, а ptr_ar – размер указателя в памяти устройства (4 байта для 32-х разрядных систем).

Попробуем теперь изменить адрес указателя на массив:

ar++;

При компиляции увидим сообщение об ошибке. Так делать нельзя. И, вообще, менять адрес указателя на массив запрещено. А вот с обычным указателем такая операция вполне допустима:

ptr_ar++;

Никаких ошибок и программа успешно выполняется. Мало того, мы с помощью указателя ptr_ar можем обращаться к произвольным элементам массива, используя оператор квадратные скобки. Например:

short a_3 = ptr_ar[2];       // a_3 = 2 (при ptr_ar = ar)

На самом деле, оператор [] – это обычная синтаксическая конструкция, подменяющая собой адресную арифметику вида:

ar[indx] = *(ar+indx)

То есть, операция индексирования – это не операция над массивами, а над адресами и не более того. Мало того, в общем виде ее можно представить так:

a[b] = *(a+b)

При этом совершенно не важно, что здесь выступает в роли a, а что в роли b. То есть, формально, обратиться к элементу массива ar с индексом 2 можно и так:

short a_3 = 2[ar];     // *(2+ar)

так как это эквивалентно операции *(2+ar), а она вполне допустима. Конечно, в реальных проектах так использовать операцию индексирования не нужно. Да и, вообще, никогда. Помните, текст программы пишется для человека, а потому не стоит прописывать различные корявые конструкции, дабы показать глубину своих познаний. Рабочий коллектив такое не оценит.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме