В языке Си массивы и указатели тесно связаны. С помощью указателей мы также легко можем манипулировать элементами массива, как и с помощью индексов.
Имя массива без индексов в Си является адресом его первого элемента. Соответственно, используя имя массива и индекс его элемента, мы можем получить адрес элемента массива:
int numbers[] = {11, 12, 13, 14};
for(size_t i=0; i< sizeof(numbers) / sizeof(numbers[0]); ++i){
printf("numbers[%d]: %p\n", i, numbers+i);
}
В моем случае вывод следующий:
numbers[0]: 0x7fffb4d10140 numbers[1]: 0x7fffb4d10144 numbers[2]: 0x7fffb4d10148 numbers[3]: 0x7fffb4d1014c
В каждом конкретном случае адреса могут быть иными, но в любом случае адрес последующего элемента будет отличаться от адреса предыдущего на размер элемента. То есть по консольному выводу мы увидим, что элементы массива идут в памяти подряд. Графически мы могли бы это представить следующим образом:
Адрес | 0xd10140 | 0xd10144 | 0xd10148 | 0xd1014c |
Значение | 11 | 12 | 13 | 14 |
Поскольку имя массива - это указатель на первый элемент, то через операцию разыменования мы можем получить значение адресу, который представляет имя указателя:
#include <stdio.h>
int main(void)
{
int array[] = {1, 2, 3, 4, 5};
printf("array[0] = %d", *array); // array[0] = 1
return 0;
}
Более того связь между массивом и указателем четко обзначена стандартом языка С (раздел 6.3.2.1, параграф 3): «За исключением случаев, когда это операнд оператора sizeof или унарный оператор &... выражение, имеющее тип «массив типа», преобразуется в выражение с типом «указатель на тип», которое указывает на начальный элемент объекта массива и не является lvalue». То есть фактически массив поддерживает только два оператора: sizeof и &, в остальных случаях он неявно преобразуется к соответствующему типу указателя.
Прибавляя определенное число к имени массива, мы можем получить указатель на соответствующий элемент массива:
#include <stdio.h>
int main(void)
{
int array[] = {1, 2, 3, 4, 5};
int second = *(array + 1); // получим второй элемент
printf("array[1] = %d", second); // array[1] = 2
return 0;
}
Более того, когда мы в обращаемся к определенному элементу массива, используя квадратные скобки, например:
array[2]
компилятор рассмотривает эту запись как прибавление индекса к указателю на начальный элемент:
array+2
Поэтому мы даже можем написать 2[array], что также будет валидным обращением к элементу массива:
#include <stdio.h>
int main(void)
{
int array[] = {1, 2, 3, 4, 5};
int third = 2[array];
printf("array[2] = %d", third); // array[2] = 3
return 0;
}
Соответственно мы можем пробежаться по всем элементом массива, прибавляя к адресу определенное число:
#include <stdio.h>
int main(void)
{
int array[5] = {1, 2, 3, 4, 5};
for(int i = 0; i < 5; i++)
{
void* address = array + i; // получаем адрес i-го элемента массива
int value = *(array + i); // получаем значение i-го элемента массива
printf("array[%d]: address=%p \t value=%d \n", i, address, value);
}
return 0;
}
То есть, например, адрес второго элемента будет представлять выражение a+1, а его значение - *(a+1).
Со сложением и вычитанием здесь действуют те же правила, что и в операциях с указателями. Добавление единицы означает прибавление к адресу значения, которое равно размеру типа массива. Так, в данном случае массив представляет тип int, размер которого, как правило, составляет 4 байта, поэтому прибавление единицы к адресу означает увеличение адреса на 4. Прибавляя к адресу 2, мы увеличиваем значение адреса на 4 * 2 =8. И так далее.
В итоге в моем случае я получу следующий результат работы программы:
array[0]: address=0060FE98 value=1 array[1]: address=0060FE9C value=2 array[2]: address=0060FEA0 value=3 array[3]: address=0060FEA4 value=4 array[4]: address=0060FEA8 value=5
В то же время имя массива это не стандартный указатель, мы не можем изменить его адрес, например, так:
int array[5] = {1, 2, 3, 4, 5};
array++; // так сделать нельзя
int b = 8;
array = &b; // так тоже сделать нельзя
Имя массива всегда хранит адрес самого первого элемента, соответственно его можно присвоить другому указателю и затем через указатель обращаться к элеиментам массива:
#include <stdio.h>
int main(void)
{
int array[5] = {1, 2, 3, 4, 5};
int *ptr = array; // указатель ptr хранит адрес первого элемента массива array
printf("value: %d \n", *ptr); // 1
return 0;
}
Прибавляя (или вычитая) определенное число от адреса указателя, можно переходить по элементам массива. Например, перейдем к третьему элементу:
#include <stdio.h>
int main(void)
{
int array[5] = {1, 2, 3, 4, 5};
int *ptr = array; // указатель ptr хранит адрес первого элемента массива array
ptr = ptr + 2; // перемезаем указатель на 2 элемента вперед
printf("value: %d \n", *ptr); // value: 3
return 0;
}
Здесь указатель ptr изначально указывает на первый элемент массива. Увеличив указатель на 2, мы пропустим 2 элемента в массиве и
перейдем к элементу array[2].
И как и другие данные, можно по указателю изменить значение элемента массива:
#include <stdio.h>
int main(void)
{
int array[5] = {1, 2, 3, 4, 5};
int *ptr = array; // указатель ptr хранит адрес первого элемента массива array
ptr = ptr + 2; // переходим к третьему элементу
*ptr = 8; // меняем значение элемента, на который указывает указатель
printf("array[2]: %d \n", array[2]); // array[2] : 8
return 0;
}
Стоит отметить, что указатель также может использовать индексы, как и массивы:
#include <stdio.h>
int main(void)
{
int array[5] = {1, 2, 3, 4, 5};
int *ptr = array; // указатель ptr хранит адрес первого элемента массива array
int value = ptr[2]; // используем индексы - получаем 3-й элемент (элемент с индексом 2)
printf("value: %d \n", value); // value: 3
return 0;
}
С помощью указателей легко перебрать массив:
int array[5] = {1, 2, 3, 4, 5};
for(int *ptr=array; ptr<=&array[4]; ptr++)
{
printf("address=%p \t value=%d \n", (void*)ptr, *ptr);
}
Так как указатель хранит адрес, то мы можем продолжать цикл, пока адрес в указателе не станет равным адресу последнего элемента (ptr<=&array[4]).
Аналогичным образом можно перебрать и многомерный массив:
#include <stdio.h>
int main(void)
{
int array[3][4] = { {1, 2, 3, 4} , {5, 6, 7, 8}, {9, 10, 11, 12}};
int n = sizeof(array)/sizeof(array[0]); // число строк
int m = sizeof(array[0])/sizeof(array[0][0]); // число столбцов
int *final = array[0] + n * m - 1; // указатель на самый последний элемент
for(int *ptr=array[0], i = 1; ptr <= final; ptr++, i++)
{
printf("%d \t", *ptr);
// если остаток от целочисленного деления равен 0,
// переходим на новую строку
if(i%m==0)
{
printf("\n");
}
}
return 0;
}
Так как в данном случае мы имеем дело с двухмерным массивом, то адресом первого элемента будет выражение array[0]. Соответственно указатель указывает на
этот элемент. С каждой итерацией указатель увеличивается на единицу, пока его значение не станет равным адресу последнего элемента, который хранится в
указателе final.
Мы также могли бы обойтись и без указателя на последний элемент, проверяя значение счетчика, пока оно не станет равно общему количеству элементов (m * n):
for(int *ptr = array[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