§ 7. Массивы и указатели

§ 7.1 Указатели

Эти два понятия — массивы и указатели — неразрывно связаны между собой.

Указатель — это переменная для хранения адреса другой переменной. Его объявление содержит тип и знак *. Чтобы узнать адрес переменной, нужно указать знак амперсанд и её имя: &foo. Чтобы работать со значением, записанным по адресу в указателе, воспользуемся операцией, которая называется разыменование указателя: *ptr.

int foo = 1;
int *ptr = &foo;            // Адрес переменной сохраняем в указатель.
printf("%d %d", foo, *ptr); // Прямое и косвенное обращение.

Как будет выглядеть ввод чисел при прямом и косвенном обращении?

scanf("%d", &foo);
scanf("%d", ptr); // ptr уже адрес, знак & не нужен.

Не имеет значения, записываете вы int* ptr, int *ptr или int * ptr. А при объявлении в одну строку звёздочка распространяется только на первое имя.

int* ptr1, ptr2;

Предыдущая запись эквивалентна следующей.

int* ptr1;
int  ptr2;

Функция обмена

Давайте напишем простую функцию, которая меняет местами значения двух переменных.

void swap(int* a, int* b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

В функцию передаются не значения переменных, а их адреса (указатели). Операция разыменования *a позволяет обращаться к значению, которое скрывается за указателем. Вызов функции будет выглядеть так (если аргумент является указателем, то операция взятия адреса & будет лишней).

swap(&foo, &bar);

Сейчас вам должно быть понятно, когда знак & в функции scanf() требуется, а когда нет. Если вы передаёте указатель, то этот знак не требуется.

§ 7.2 Нулевой указатель

Если нужно инициализировать указатель, значение которого пока не известно, нужно воспользоваться NULL (может быть определено как 0, 0l или (void*)0).

int* ptr = NULL;

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

if (ptr != NULL)
{
    // ...
}

NULL можно привести к любому типу-указателю, это значит, что ptr в этом выражении может быть указателем на любой тип.

§ 7.3 Одномерные массивы

Теперь перейдём к массивам. Массив — набор переменных, расположенных в памяти непосредственно друг за другом. К элементам массива обращаются по индексу (номеру). Начальный элемент имеет индекс 0. Размер массива должен быть константой, хотя некоторые компиляторы поддерживают массивы переменной длины.

Где-то в начале программы, ещё до функции main(), нужно определить размер массива с помощью директивы препроцессора (он подробнее рассматривается позднее).

#define SIZE 10u

Тогда в функции main() или любой другой можно написать объявление массива.

int data[SIZE]; // Массив из десяти целых.

Примечание. В языке C константы не являются константами в полном смысле этого слова, поэтому недостаточно написать const size_t size = 10u;, хотя это возможно в языке C++. (Тип size_t является псевдонимом для типа unsigned. Можете вместо него для простоты писать unsigned или даже int.)

За объявлением массива может следовать список инициализации.

int data[SIZE] = {5, 1, 3, 34, 0, 21, 2, 1, 8, 13};

Если указать не все элементы, то остальные будут нулями.

int data[SIZE] = {5, 1, 3, 34}; // {5, 1, 3, 34, 0, 0, 0, 0, 0, 0}

Можно воспользоваться выделенным инициализатором.

int data[SIZE] = {5, [5] = 21, 2, [1] = 1}; // {5, 1, 0, 0, 0, 21, 2, 0, 0, 0} 

Повторно инициализировать массив нельзя.

int data[SIZE] = {5, 1, 3, 34, 0, 21, 2, 1, 8, 13};
data = {0, 1, 1, 2, 3, 5, 8, 13, 21, 34}; // Ошибка.

Зато можно не указывать размер, он будет вычислен автоматически.

int data[] = {5, 1, 3, 34, 0, 21, 2, 8, 1, 13};

Вот небольшая программа, которая перебирает все элементы массива, ищет в нём самый большой элемент и его индекс (INT_MIN определено в заголовочном файле <limits.h>).

int max = INT_MIN;
size_t index = 0;
for (size_t i = 0; i < size; ++i)
{
    if (data[i] > max)
    {
        max = data[i];
        index = i;
    }
}
printf("Max: %d, index: %zu", max, index); // Max: 34, index: 3

Имя массива, использованное без квадратных скобок, означает адрес нулевого элемента (константный указатель на нулевой элемент). Следующие вызовы приведут к одинаковому результату.

printf("0x%p\n", data);
printf("0x%p\n", &data[0]);

Теперь загадка: что будет выведено?

printf("%d", 7[data]);

А будет выведено 8. Запись data[7] на самом деле является синтаксическим сахаром для двух операций: *(data + 7). Она расшифровывается следующим образом: к адресу нулевого элемента прибавляется семь элементов — получается адрес седьмого элемента; разыменовываем, и получается сам седьмой элемент.

Запись 7[data] тогда означает *(7 + data). От перестановки мест слагаемых сумма не меняется.

§ 7.4 Передача массива в функцию

Для этого будем передавать указатель на начало массива и его размер. (К слову, первый аргумент может выглядеть как int data[].)

void max(const int * data, size_t size) // Указатель на массив и его размер.
{
    int max = INT_MIN;
    size_t index = 0;
    for (size_t i = 0; i < size; ++i)
    {
        if (data[i] > max)
        {
            max = data[i];
            index = i;
        }
    }
    printf("Max: %d, index: %zu", max, index); // Max: 34, index: 3
}

Вызов будет иметь следующий вид.

max(arr, size);

Мы не просто написали в аргументе int*, хотя и могли бы. А написали мы const int*, тем самым давая компилятору понять, что мы не собираемся менять элементы массива (это избавляет от случайных ошибок).

Аргумент

Можно ли изменять
элементы

Можно ли изменять
сам указатель

int *

const int *, int const *

int * const

const int * const, int const * const

Поэкспериментируйте с разными типами аргументов.

§ 7.5 Динамические массивы

Если размер массива на момент компиляции ещё не известен либо массив должен быть очень большим, то память можно выделить прямо во время выполнения программы.

size_t size = 10u;
int* data = malloc(size * sizeof(int)); // Количество запрашиваемых байтов.
if (data == NULL)
{
    // Памяти недостаточно, принимаем меры.
}
// Работа как с обычным массивом.
free(arr2);

Примечание. В языке C++ неявного приведения типа void*, возвращаемого malloc(), к типу int* не произойдёт. Поэтому запись будет выглядеть как int* data = (int*)malloc(size * sizeof(int));.

Можно воспользоваться и функцией calloc(). Её первый аргумент — количество элементов, второй — их размер.

int* data = calloc(size, sizeof(int));

Несмотря на то, что работа с обоими видами массивов выглядит одинаково, различия всё же есть.

#define SIZE 10u
int data1[SIZE];

size_t size = 10u;
int* data2 = malloc(size * sizeof(int));

Если мы попытаемся узнать размер динамического массива, то у нас ничего не получится: data2 — это указатель, а его размер на моей машине равен 4 байтам.

printf("%zu %zu\n", sizeof(data1), sizeof(data2)); // 40 4

Как мы уже сказали, адрес «настоящего» массива, использованный без квадратных скобок, означает адрес нулевого элемента. И сколько мы не будем пытаться брать адрес, будет получаться то же самое. А вот в случае динамического массива «имя массива» всего лишь указатель, то есть адрес, а у него в свою очередь тоже есть адрес.

printf("%p %p\n", data1, &data1); // Одинаковые адреса.
printf("%p %p\n", data2, &data2); // Разные адреса.

§ 7.6 Двумерные массивы

Двумерные (трёхмерные и так далее) массивы определяются следующим образом.

#define ROWS 4u
#define COLUMNS 5u
int matrix[ROWS][COLUMNS] =
{
    {0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0},
};

Мы уже говорили, что квадратные скобки в одномерном массиве (data[i]) лишь синтаксический сахар (*(data + i)). За двойными квадратными скобками (matrix[i][j]) скрывается то же самое: *(*(matrix + i) + j).

А вот способов организовать двумерный (или даже трёхмерный) динамический массив множество. У каждого свои достоинства и недостатки. Вот один из них (вытягивание массива в змейку).

size_t rows = 4u, columns = 5u;
int* matrix = malloc(rows * columns * sizeof(int));
if (matrix == NULL)
{
    // Памяти недостаточно, принимаем меры.
}
printf("%d", matrix[3 * columns + 2]); // Обращение к элементу [3][2].
free(matrix);

§ 7.7 Указатели на функции

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

void foo(int);

Тогда указатель на такую функцию будет иметь следующий вид. Сперва указывается тип возвращаемого значения, затем в скобках после знака * произвольное имя указателя, дальше в отдельных скобках через запятую перечисляются типы аргументов. Это значит, что конкретно в этот указатель нельзя записать адрес функции, которая, скажем, не имеет аргументов.

int (*ptr)(int) = foo; // Можно писать &foo.

Прямой и косвенный вызов функции приводит к одинаковому результату.

foo(7); // Прямой вызов.
ptr(7); // Косвенный вызов.

§ 7.8 Правило «право-лево»

В этот момент вы могли начать паниковать из-за увеличивающейся сложности объявлений — все эти указатели, константные указатели, указатели на функцию... Первое, что нужно найти, это имя (идентификатор). Далее будем двигаться вправо и влево, отталкиваясь от скобок и замечая, что * — это указатель, [] — массив, а () — функция.

int (*(*(*ptr)())[7])(); //  ptr - это...

«ptr — это...». Однако дальше ), потому идём влево. Символ * означает указатель.

int (*(*(*ptr)())[7])(); //  ptr - это указатель...

Наткнулись на (, движемся вправо. Скобки () означают функцию.

int (*(*(*ptr)())[7])(); //  ptr - это указатель на функцию...

Дальше снова ), движемся влево. Там *.

int (*(*(*ptr)())[7])(); //  ptr - это указатель на функцию, возвращающую
                             указатель...

Двигаясь вправо, встречаем []. Это массив.

int (*(*(*ptr)())[7])(); //  ptr - это указатель на функцию, возвращающую
                             указатель на массив из 7...

И так далее.

int (*(*(*ptr)())[7])(); //  ptr - это указатель на функцию, возвращающую
                             указатель на массив из 7 указателей...
int (*(*(*ptr)())[7])(); //  ptr - это указатель на функцию, возвращающую
                             указатель на массив из 7 указателей на функции...
int (*(*(*ptr)())[7])(); //  ptr - это указатель на функцию, возвращающую
                             указатель на массив из 7 указателей на функции,
                             возвращающие int.

Есть хороший сервис, с помощью которого можно переводить объявления языка C на английский, и наоборот.

14 июня