Урок 11. Особенности массивов при работе с указателями

Особенности языка С. Учебное пособие

К указателям можно прибавлять целые числа

Рассмотрим программу:

#include <stdio.h>
 
#define N 5
 
main () {
	int arrI[N], i;
 
	for (i=0; i<N; i++)
		printf("%p\n", &arrI[i]);
}

Создается массив arrI, далее в цикле for выводятся значения адресов ячеек памяти каждого элемента массива. Результат выполнения программы будет выглядеть примерно так:

0x7ffffbff4050 
0x7ffffbff4054 
0x7ffffbff4058 
0x7ffffbff405c 
0x7ffffbff4060 

Обратите внимание на то, что значение адреса каждого последующего элемента массива больше значения адреса предыдущего элемента на 4 единицы. В вашей системе эта разница может составлять 2 единицы. Такой результат вполне очевиден, если вспомнить, сколько байтов отводится на одно данное типа int, и что элементы массива сохраняются в памяти друг за другом.

Теперь объявим указатель на целый тип и присвоим ему адрес первого элемента массива:

	int *pI;
 
	pI = &arrI[0];

Цикл for изменим таким образом:

	for (i=0; i<N; i++)
		printf("%p\n", pI + i);

Здесь к значению pI, которое является адресом ячейки памяти, прибавляется сначала 0, затем 1, 2, 3 и 4. Можно было бы предположить, что прибавление к pI единицы в результате дает адрес следующего байта за тем, на который указывает pI. А прибавление двойки вернет адрес байта, через один от исходного. Однако подобное предположение не верно.

Вспомним, что тип указателя сообщает, на сколько байт простирается значение по адресу, на который он указывает. Таким образом, хотя pI указывает только на один байт (первый), но "знает", что его "собственность" простирается на все четыре (или два). Когда мы прибавляем к указателю единицу, то получаем указатель на следующее значение, но никак не на следующий байт. А следующее значение начинается только через 4 байта (в данном случае). Поэтому результат выполнения приведенного цикла с указателем правильно отобразит адреса элементов массива.

Задание
Убедитесь в этом сами.

Прибавляя к указателям (или вычитая из них) целые значения, мы имеем дело с так называемой адресной арифметикой.

Задание
Напишите программу, в которой объявлен массив вещественных чисел из десяти элементов. Присвойте указателю адрес четвертого элемента, затем, используя цикл, выведите на экран адреса 4, 5 и 6-ого элементов массива.

Имя массива содержит адрес его первого элемента

Да, это именно так, данный факт следует принять как аксиому. Вы можете убедиться в этом выполнив такое выражение:

	printf("%p = %p\n", arrI, &arrI[0]);

Отсюда следует, что имя массива – это ничто иное, как указатель. (Хотя это немного особенный указатель, о чем будет упомянуто ниже.) Поэтому выражения pI = &arrI[N] и pI = arrI дают одинаковый результат: присваивают указателю pI адрес первого элемента массива.

Раз имя массива — это указатель, ничего не мешает получать адреса элементов вот так:

	for (i=0; i<N; i++)
		printf("%p\n", arrI + i);

Соответственно значения элементов массива можно получить так:

	for (i=0; i<N; i++)
		printf("%d\n", *(arrI + i));

Примечание. Если массив был объявлен как автоматическая переменная (т.е. не глобальная и не статическая) и при этом не был инициализирован (не присваивались значения), то в нем будет содержаться "мусор" (случайные числа).

Получается, что запись вида arrI[3] является сокращенным (более удобным) вариантом выражения *(arr+3).

Взаимозаменяемость имени массива и указателя

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

	int arrI[N], i;
	int *pI;
 
	pI = arrI;
 
	for (i=0; i<N; i++)
		printf("%d\n", pI[i]);

Отсюда следуют выводы. Если arrI — массив, а pI — указатель на его первый элемент, то пары следующих выражений дают один и тот же результат:

  • arrI[i] и pI[i];
  • &arrI[i] и &pI[i];
  • arrI+i и pI+i;
  • *(arrI+i) и *(pI+i).

Задание
Что получается в результате выполнения данных пар выражений: адреса или значения элементов массива? Если вы испытываете трудности при ответе на этот вопрос, перечитайте урок 7, этот урок, изучите другие источники.

Указателю pI можно присвоить адрес любого из элементов массива. Например, так pI = &arrI[2] или так pI = arr+2. В таком случае результат приведенных выше пар выражений совпадать не будет. Например, когда будет выполняться выражение arrI[i], то будет возвращаться i-ый элемент массива. А вот выражение pI[i] уже вернет не i-ый элемент от начала массива, а i-ый элемент от того, адрес которого был присвоен pI. Например, если pI был присвоен адрес третьего элемента массива (pI = arr+2), то выражение arrI[1] вернет значение второго элемента массива, а pI[1] — четвертого.

Задание
Присвойте указателю (pI) ссылку не на первый элемент массива (arrI). В одном и том же цикле выводите результат выражений arrI[i] и pI[i], где на каждой итерации цикла i для обоих выражений имеет одинаковое значение. Объясните результат выполнения такой программы.

Имя массива — это указатель-константа

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

Это значит, что выражение pI = arrI допустимо, а arrI = pI нет. Имя массива является константой. При этом не надо путать имя массива (адрес) и значения элементов массива. Последние константами не являются. Действительно, ведь для всех переменных мы не можем менять их адрес в процессе выполнения программы, можем менять лишь их значения. В этом смысле имя массива — это обычная переменная, хотя и содержащая адрес.

Как следствие в программном коде выражения присваивания, инкрементирования и декрементирования допустимы для указателей, а для имени массива — запрещены.

Задание
Посмотрите на программу ниже. Что она делает? Почему? Проверьте ваши рассуждения опытным путем.

#include <stdio.h>
 
main () {
	char str[20], *ps = str, n=0;
 
	printf("Enter word: ");
	scanf("%s", str);
 
	while(*ps++ != '\0') n++;
 
	printf("%d\n", n);
}

#include <stdio.h>   int

#include <stdio.h>
 
int main(int argc, char *argv[]) {
    char name[] = "Hello World";
    printf("%p\t", &name);
    printf("%p\t", name);
    printf("%s\t", name);
}

Почему в данном случае значения одинаковы, во втором принте по сути должен быть адрес первой ячейки массива, а в первом адрес самого указателя на массив, если массивы являются указателями?