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


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

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

1
2
int a[] = {1, 2, 3, 4, 5};
std::cout << "a[0] = " << *a << std::endl; // a[0] = 1
Прибавляя к адресу первого элемента некоторое число, мы можем получить определенны элемент массив. Например, в цикле пробежимся по всем элементам:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

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

for(int i=0; i < n; i++)
{
std::cout << "a[" << i << "]: address=" << a+i << "\tvalue=" << *(a+i) << std::endl;
}

return 0;
}
То есть, например, адрес второго элемента будет представлять выражение a+1, а его значение - *(a+1).

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

И в итоге программа выведет на консоль следующий результат:

a[0]: address=0x60fe84 value=1
a[1]: address=0x60fe88 value=2
a[2]: address=0x60fe8c value=3
a[3]: address=0x60fe90 value=4
a[4]: address=0x60fe94 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);
std::cout << "value: " << a2 << std::endl; // value: 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++)
{
std::cout << "address=" << ptr << "\tvalue=" << *ptr << std::endl;
}
Так как указатель хранит адрес, то мы можем продолжать цикл, пока адрес в указателе не станет равным адресу последнего элемента.

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

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

int main()
{
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 *end = a[0] + n * m - 1; // указатель на самый последний элемент 0 + 3 * 4 - 1 = 11
for(int *ptr=a[0], i=1; ptr <= end; ptr++, i++)
{
std::cout << *ptr << "\t";
// если остаток от целочисленного деления равен 0,
// переходим на новую строку
if(i%m == 0)
{
std::cout << std::endl;
}
}

return 0;
}
Поскольку в данном случае мы имеем дело с двухмерным массивом, то адресом первого элемента будет выражение a[0]. Соответственно указатель указывает на этот элемент. С каждой итерацией указатель увеличивается на единицу, пока его значение не станет равным адресу последнего элемента, который хранится в указателе end.

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

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

int main()
{
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 *end = a[0] + n * m - 1; // указатель на самый последний элемент 0 + 3 * 4 - 1 = 11
for(int *ptr=a[0], i=0; i<m*n;)
{
std::cout << *ptr++ << "\t";
// если остаток от целочисленного деления равен 0,
// переходим на новую строку
if(++i%m == 0)
{
std::cout << std::endl;
}
}

return 0;
}
Но в обоих случаях программа вывела бы следующий результат:

1 2 3 4
5 6 7 8
9 10 11 12
Указатель на массив символов
Поскольку массив символов может интерпретироваться как строка, то указатель на значения типа char тоже может интерпретироваться как строка:

1
2
3
4
5
6
7
8
9
#include <iostream>

int main()
{
char letters[] = "hello";
char *p = letters;
std::cout << p << std::endl; // hello
return 0;
}
Если же необходимо вывести на консоль адрес указателя, то его надо переобразовать к типу void*:

1
std::cout << (void*)p << std::endl; // 0x60fe8e
В остальном работа с указателем на массив символов производится также, как и с указателями на массивы других типов.