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


В языке Си массивы и указатели тесно связаны. С помощью указателей мы также легко можем манипулировать элементами массива, как и с помощью индексов.

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

1
2
int a[] = {1, 2, 3, 4, 5};
printf("a[0] = %d", *a); // a[0] = 1
Мы можем пробежаться по всем элементом массива, прибавляя к адресу определенное число:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(void)
{
int a[5] = {1, 2, 3, 4, 5};

for(int i=0;i<5;i++)
{
printf("a[%d]: address=%p \t value=%d \n", i, a+i, *(a+i));
}
return 0;
}
То есть, например, адрес второго элемента будет представлять выражение a+1, а его значение - *(a+1).

Со сложением и вычитанием здесь действуют те же правила, что и в операциях с указателями. Добавление единицы означает прибавление к адресу значения, которое равно размеру типа массива. Так, в данном случае массив представляет тип int, размер которого, как правило, составляет 4 байта, поэтому прибавление единицы к адресу означает увеличение адреса на 4. Прибавляя к адресу 2, мы увеличиваем значение адреса на 4 * 2 =8. И так далее.

В итоге в моем случае я получу следующий результат работы программы:

a[0]: address=0060FE98 value=1
a[1]: address=0060FE9C value=2
a[2]: address=0060FEA0 value=3
a[3]: address=0060FEA4 value=4
a[4]: address=0060FEA8 value=5
В то же время имя массива это не стандартный указатель, мы не можем изменить его адрес, например, так:

1
2
3
4
int a[5] = {1, 2, 3, 4, 5};
a++; // так сделать нельзя
int b = 8;
a = &b; // так тоже сделать нельзя
Имя массива всегда хранит адрес самого первого элемента. И нередко для перемещения по элементам массива используются отдельные указатели:

1
2
3
4
int a[5] = {1, 2, 3, 4, 5};
int *ptr = a;
int a2 = *(ptr+2);
printf("value: %d \n", a2); // 3
Здесь указатель ptr изначально указывает на первый элемент массива. Увеличив указатель на 2, мы пропустим 2 элемента в массиве и перейдем к элементу a[2].

С помощью указателей легко перебрать массив:

1
2
3
4
5
6
int a[5] = {1, 2, 3, 4, 5};

for(int *ptr=a; ptr<=&a[4]; ptr++)
{
printf("address=%p \t value=%d \n", ptr, *ptr);
}
Так как указатель хранит адрес, то мы можем продолжать цикл, пока адрес в указателе не станет равным адресу последнего элемента.

Аналогичным образом можно перебрать и многомерный массив:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int a[3][4] = { {1, 2, 3, 4} , {5, 6, 7, 8}, {9, 10, 11, 12}};
int n = sizeof(a)/sizeof(a[0]); // число строк
int m = sizeof(a[0])/sizeof(a[0][0]); // число столбцов

int *final = a[0] + n*m - 1; // указатель на самый последний элемент
for(int *ptr=a[0], i=1; ptr<=final; ptr++, i++)
{
printf("%d \t", *ptr);
// если остаток от целочисленного деления равен 0,
// переходим на новую строку
if(i%m==0)
{
printf("\n");
}
}
return 0;
}
Так как в данном случае мы имеем дело с двухмерным массивом, то адресом первого элемента будет выражение a[0]. Соответственно указатель указывает на этот элемент. С каждой итерацией указатель увеличивается на единицу, пока его значение не станет равным адресу последнего элемента, который хранится в указателе final.

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

1
2
3
4
5
6
7
8
for(int *ptr=a[0], i=0; i<m*n;)
{
printf("%d \t", *ptr++);
if(++i%m==0)
{
printf("\n");
}
}
Но в любом случае программа вывела бы следующий результат:

1 2 3 4
5 6 7 8
9 10 11 12