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

Последнее обновление: 24.02.2023

В 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 байта. С одной стороны, мы здесь сталкиваемся с дополнительными издержками: дополнительно выделяется память для хранения адресов в указателях. С другой стороны, когда строки в массиве сильно различаются по длине , то мы можем получить общий выигрыш в количестве потребляемой памяти.

Дополнительные материалы
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850