В C++ указатели и массивы тесно связаны. Обычно компилятор преобразует массив в указатели. С помощью указателей можно манипулировать элементами массива, как и с помощью индексов.
Имя массива по сути является адресом его первого элемента. Соответственно через операцию разыменования мы можем получить значение по этому адресу:
#include <iostream> int main() { int nums[] {1, 2, 3, 4, 5}; std::cout << "nums[0] address: " << nums << std::endl; std::cout << "nums[0] value: " << *nums << std::endl; }
Так, в моем случае я получу следующий консольный вывод:
nums[0] address: 0x1f1ebffe60 nums[0] value: 1
Прибавляя к адресу первого элемента некоторое число, мы можем получить определенный элемент массива.
#include <iostream> int main() { int nums[] {1, 2, 3, 4, 5}; int num2 = *(nums + 1); // второй элемент int num3 = *(nums + 2); // третий элемент std::cout << "num2 = " << num2 << std::endl; // num2 = 2 std::cout << "num3 = " << num3 << std::endl; // num3 = 3 }
То есть, например, адрес второго элемента будет представлять выражение nums+1
, а его значение - *(nums+1)
.
В отношении сложения и вычитания здесь действуют те же правила, что и в операциях с указателями. Добавление единицы означает прибавление к адресу значения, которое равно размеру типа массива. Так, в данном случае массив представляет тип int, размер которого, как правило, составляет 4 байта, поэтому прибавление единицы к адресу означает увеличение адреса на 4. Прибавляя к адресу 2, мы увеличиваем значение адреса на 4 * 2 = 8. И так далее.
Например, в цикле пробежимся по всем элементам:
#include <iostream> int main() { int nums[] {1, 2, 3, 4, 5}; for(unsigned i{}; i < std::size(nums); i++) { std::cout << "nums[" << i << "]: address=" << nums+i << "\tvalue=" << *(nums+i) << std::endl; } }
И в итоге программа выведет на консоль следующий результат:
nums[0]: address=0xd95adffc30 value=1 nums[1]: address=0xd95adffc34 value=2 nums[2]: address=0xd95adffc38 value=3 nums[3]: address=0xd95adffc3c value=4 nums[4]: address=0xd95adffc40 value=5
Но при этом имя массива это не стандартный указатель, и мы не можем изменить его адрес, например, так:
int nums[] {1, 2, 3, 4, 5}; nums++; // так сделать нельзя int b {8}; nums = &b; // так тоже сделать нельзя
Имя массива всегда хранит адрес самого первого элемента. И нередко для перемещения по элементам массива используются отдельные указатели:
int nums[] {1, 2, 3, 4, 5}; int *ptr {nums}; int num3 = *(ptr+2); std::cout << "num3: " << num3 << std::endl; // num3: 3
Здесь указатель ptr
изначально указывает на первый элемент массива. Увеличив указатель на 2, мы пропустим 2 элемента в массиве и
перейдем к элементу nums[2]
.
Можно сразу присвоить указателю адрес конкретного элемента массива:
int nums[] {1, 2, 3, 4, 5}; int *ptr {&nums[2]}; // адрес третьего элемента std::cout << "*ptr = " << *ptr << std::endl; //*ptr = 3
С помощью указателей легко перебрать массив:
#include <iostream> int main() { const int n = 5; int nums[n]{1, 2, 3, 4, 5}; for(int *ptr{nums}; ptr<=&nums[n-1]; ptr++) { std::cout << "address=" << ptr << "\tvalue=" << *ptr << std::endl; } }
Так как указатель хранит адрес, то мы можем продолжать цикл, пока адрес в указателе не станет равным адресу последнего элемента.
Аналогичным образом можно перебрать и многомерный массив:
#include <iostream> int main() { int nums[3][4] { {1, 2, 3, 4} , {5, 6, 7, 8}, {9, 10, 11, 12}}; unsigned int n { sizeof(nums)/sizeof(nums[0]) }; // число строк unsigned int m { sizeof(nums[0])/sizeof(nums[0][0]) }; // число столбцов int *end {nums[0] + n * m - 1}; // указатель на самый последний элемент 0 + 3 * 4 - 1 = 11 int *ptr {nums[0]}; // указатель на первый элемент for( unsigned i{1}; ptr <= end; ptr++, i++) { std::cout << *ptr << "\t"; // если остаток от целочисленного деления равен 0, // переходим на новую строку if(i%m == 0) { std::cout << std::endl; } } }
Поскольку в данном случае мы имеем дело с двухмерным массивом, то адресом первого элемента будет выражение a[0]
. Соответственно указатель указывает на
этот элемент. С каждой итерацией указатель увеличивается на единицу, пока его значение не станет равным адресу последнего элемента, который хранится в
указателе end.
Мы также могли бы обойтись и без указателя на последний элемент, проверяя значение счетчика:
#include <iostream> int main() { const unsigned n {3}; // число строк const unsigned m {4}; // число столбцов int nums[n][m] { {1, 2, 3, 4} , {5, 6, 7, 8}, {9, 10, 11, 12}}; const unsigned count {m * n}; // общее количество элементов int *ptr{nums[0]}; // указатель на первый элемент первого массива for(unsigned i{1}; i <= count; ptr++, i++) { std::cout << *ptr << "\t"; // если остаток от целочисленного деления равен 0, // переходим на новую строку if(i%m == 0) { std::cout << std::endl; } } }
Но в обоих случаях программа вывела бы следующий результат:
1 2 3 4 5 6 7 8 9 10 11 12
Поскольку массив символов может интерпретироваться как строка, то указатель на значения типа char тоже может интерпретироваться как строка:
#include <iostream> int main() { char hello[] {"hello"}; char *phello {hello}; std::cout << phello << std::endl; // hello }
При выводе на консоль значения указателя фактически будет выводиться строка.
Также можно применять операцию разыменовывания для получения отдельных символов, например, выведем первый символ:
std::cout << *phello << std::endl; // h
Если же необходимо вывести на консоль адрес указателя, то его надо преобразовать к типу void*:
std::cout << (void*)phello << std::endl; // 0x60fe8e
В остальном работа с указателем на массив символов производится также, как и с указателями на массивы других типов.
Также поскольку указатель типа char тоже может интерпретироваться как строка, то теоретически мы можем написать следующим образом:
char *phello {"hello"};
Однако следует учитывать, что строковые литералы в С++ рассматриваются как константы. Поэтому предыдущее определение указателя может при компиляции вызвать как минимум предупреждение, а попытка изменить элементы строки через указатель - к ошибке компиляции. Поэтому при определении указателя на строку, следует определять указатель как указатель на константу:
#include <iostream> int main() { const char *phello {"hello"}; // указатель на константу std::cout << phello << std::endl; // hello }
Также можно определять массивы указателей. В некотором смысле массив указателей будет похож на массив, который содержит другие массивы. Однако массив указателей имеет преимущества.
Например, возьмем обычный двухмерный символьный массив - массив, который хранит строки:
#include <iostream> int main() { char langs[][20] { "C++", "Python", "JavaScript"}; std::cout << langs[0] << ": " << std::size(langs[0]) << " bytes" << std::endl; // C++: 20 bytes }
Для определения двухмерного массива мы должны указать как минимум размер вложенных массивов, который будет достаточным, чтобы вместить каждую строку. В данном случае размер каждого вложенного массива - 20 символов. Однако зачем для первой строки - "C++", которая содержит 4 символа (включая концевой нулевой байт) выделять аж 20 байтов? Это - ограничение подобных массивов. Массивы указателей же позволяют обойти подобное ограничение:
#include <iostream> int main() { const char *langs[] { "C++", "Python", "JavaScript"}; // перебор массива for(unsigned i{}; i< std::size(langs); i++) { std::cout << langs[i] << std::endl; } }
В данном случае элементами массива langs являются указатели: 3 указателя, каждый из которых занимает 4 или 8 байт в зависимости от архитекутуры (размер адреса). Каждый из этих указателей указывает на адрес в памяти, где расположены соответствующие строки: "C++", "Python", "JavaScript". Однако каждая из этих строк будет занимать именно то пространство, которое ей непосредственно необходимо. То есть строка "С++" будет занимать 4 байта. С одной стороны, мы здесь сталкиваемся с дополнительными издержками: дополнительно выделяется память для хранения адресов в указателях. С другой стороны, когда строки в массиве сильно различаются по длине , то мы можем получить общий выигрыш в количестве потребляемой памяти.