Адресная арифметика

Рассмотрим следующую программу:

#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 указывает лишь на один байт, он "знает", что его значение располагается на все четыре (или два). Потому прибавление единицы приводит нас к следующему значению, а не байту. Поэтому приведенный цикл с указателем будет корректно отображать адреса элементов массива.

Когда мы добавляем или вычитаем целые значения из указателей, мы используем так называемую адресную арифметику.

Задание:

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

Имя массива - это указатель на адрес его первого элемента

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

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

Таким образом, имя массива — это ровно указатель. Несмотря на особенность, выражения pI = &arrI[N] и pI = arrI  дают одинаковый результат: адрес первого элемента массива.

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

    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).
Задание:

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

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

Задание:

Присвойте указателю (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);
}

Вопросы для самопроверки:

  1. Чем является имя массива в языке C?
  2. Чем запрещено присваивание имени массива?
  3. Как можно получить адреса элементов массива с помощью имени массива?
  4. Как ведет себя имя массива в контексте присваивания и арифметики?

Программа курса:

  1. Описание курса
  2. Введение в язык программирования C
  3. Типы данных в C и форматированный вывод
  4. Символьные типы и управляющие символы в C
  5. Операторы ветвления и switch в C
  6. Циклы и операторы в языке C
  7. Битовые операции в языке C
  8. Посимвольный ввод и вывод в C - буферизация
  9. Переменные, адреса и указатели в C
  10. Передача аргументов по ссылке и значению в C
  11. Форматированный ввод данных с использованием scanf
  12. Генерация псевдослучайных чисел на C
  13. Адресная арифметика в массивах C
  14. Передача массивов в функции и указатели
  15. Строки в языке C - особенности и функции работы
  16. Функции работы со строками в C
  17. Работа со структурами в C - создание и применение
  18. Динамические структуры данных в C
  19. Ввод и вывод данных из файлов в языке C
  20. Передача аргументов в C и работа с файлами
  21. Препроцессор в языке C - директивы и макросы
  22. Создание и компиляция многофайловых программ в C
  23. Использование статических и динамических библиотек в C