Массивы указателей, строки и многоуровневая адресация


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

Массив указателей определяется одним из трех способов:

1
2
3
тип *имя_массива [размер];
тип *имя_массива [] = инициализатор;
тип *имя_массива [размер] = инициализатор;
Используем все эти способы:

1
2
3
4
int a[] = {1, 2, 3, 4};
int *p1[3];
int *p2[] = { &a[1], &a[2], &a[0] };
int *p3[3] = { &a[3], &a[1], &a[2] };
Массив указателей p1 состоит из трех элементов, но он не инициализирован и является пустым.

Массивы p2 и p3 в качестве элементов хранят адреса на элементы массива a.

Строки и указатели
Ранее мы рассмотрели, что строка по сути является набором символов, окончанием которого служит нулевой символ '\0'. И фактически строку можно представить в виде массива:

1
char[] hello = "Hello World";
Но в языке Си также для представления строк можно использовать указатели на тип char:

1
2
char *hello = "Hello World!";
printf("%s", hello);
Оба определения строки - с помощью массива и указателя будут равнозначны.

Соответственно массив указателей типа char представляет собой набор строк:

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

int main(void)
{
char *fruit[] = {"apricot", "apple", "banana", "lemon", "pear", "plum"};
int n = sizeof(fruit)/sizeof(fruit[0]);
for(int i=0; i<n; i++)
{
printf("%s \n", fruit[i]);
}
return 0;
}
Результат работы программы:

apricot
apple
banana
lemon
pear
plum
Указатели на указатели
Кроме обычных указателей в языке Си мы можем создавать указатели на другие указатели. Такие ситуации еще называются многоуровневой адресацией.

Например:

1
int **ptr;
Переменная ptr представляет указатель на указатель на объект типа int. Две звездочки в определении указателя говорят о том, что мы имеем дело с двухуровневой адресацией. Например:

1
2
3
4
5
6
7
8
9
int x = 22;
int *ptr1;
int **ptr2;
ptr1 = &x;
ptr2 = &ptr1;

printf("Address of ptr1: %p \n", ptr2);
printf("Address of x: %p \n", *ptr2);
printf("Value of x: %d \n", **ptr2);
Здесь указатель ptr2 хранит адрес указателя ptr1. Поэтому через выражение *ptr2 можно получить значение, которое хранится в указателе ptr - адрес переменной x. А через выражение **ptr2 можно получить значение по адресу из ptr1, то есть значение переменной x.