Урок №93. указатели на указатели

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

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

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

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

int a[] = {1, 2, 3, 4, 5};
std::cout << "a = " << *a << std::endl;	// a = 1

Прибавляя к адресу первого элемента некоторое число, мы можем получить определенны элемент массив. Например, в цикле
пробежимся по всем элементам:

#include <iostream>

int main()
{
	const int n = 5;
	int a = {1, 2, 3, 4, 5};
	
	for(int i=0; i < n; i++)
    {
        std::cout << "a: address=" << a+i << "\tvalue=" << *(a+i) << std::endl;
    }
 	
	return 0;
}

То есть, например, адрес второго элемента будет представлять выражение , а его значение — .

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

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

a: address=0x60fe84	value=1
a: address=0x60fe88	value=2
a: address=0x60fe8c	value=3
a: address=0x60fe90	value=4
a: address=0x60fe94	value=5

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

int a = {1, 2, 3, 4, 5};
a++;			// так сделать нельзя
int b = 8;
a = &b;			// так тоже сделать нельзя

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

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

int a = {1, 2, 3, 4, 5};
int *ptr = a;
int a2 = *(ptr+2);
std::cout <<  "value: " << a2 << std::endl;  // value: 3

Здесь указатель изначально указывает на первый элемент массива. Увеличив указатель на 2, мы пропустим 2 элемента в массиве и
перейдем к элементу a.

С помощью указателей легко перебрать массив:

int a = {1, 2, 3, 4, 5};

for(int *ptr=a; ptr<=&a; ptr++)
{
	std::cout << "address=" << ptr << "\tvalue=" << *ptr << std::endl;
}

Так как указатель хранит адрес, то мы можем продолжать цикл, пока адрес в указателе не станет равным адресу последнего элемента.

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

#include <iostream>

int main()
{
	int a = { {1, 2, 3, 4} , {5, 6, 7, 8}, {9, 10, 11, 12}};
	int n = sizeof(a)/sizeof(a);         // число строк
    int m = sizeof(a)/sizeof(a);   // число столбцов
     
    int *end = a + n * m - 1;    // указатель на самый последний элемент 0 + 3 * 4 - 1 = 11
    for(int *ptr=a, i=1; ptr <= end; ptr++, i++)
    {
        std::cout << *ptr << "\t";
        // если остаток от целочисленного деления равен 0,
        // переходим на новую строку
        if(i%m == 0)
        {
            std::cout << std::endl;
        }
    }
	
	return 0;
}

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

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

#include <iostream>

int main()
{
	int a = { {1, 2, 3, 4} , {5, 6, 7, 8}, {9, 10, 11, 12}};
	int n = sizeof(a)/sizeof(a);         // число строк
    int m = sizeof(a)/sizeof(a);   // число столбцов
     
    int *end = a + n * m - 1;    // указатель на самый последний элемент 0 + 3 * 4 - 1 = 11
    for(int *ptr=a, 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 тоже может интерпретироваться как строка:

#include <iostream>
 
int main()
{
    char letters[] = "hello";
	char *p = letters;
	std::cout << p << std::endl;		// hello
    return 0;
}

Если же необходимо вывести на консоль адрес указателя, то его надо переобразовать к типу void*:

std::cout << (void*)p << std::endl;	// 0x60fe8e

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

НазадВперед

Оператор адреса &

При выполнении инициализации переменной, ей автоматически присваивается свободный адрес памяти, и, любое значение, которое мы присваиваем переменной, сохраняется по этому адресу в памяти. Например:

int b;

1 intb;

При выполнении этого стейтмента процессором, выделяется часть оперативной памяти. В качестве примера предположим, что переменной присваивается ячейка памяти под номером 150. Всякий раз, когда программа встречает переменную в выражении или в стейтменте, она понимает, что для того, чтобы получить значение — ей нужно заглянуть в ячейку памяти под номером 150.

Хорошая новость: нам не нужно беспокоиться о том, какие конкретно адреса памяти выделены для определенных переменных. Мы просто ссылаемся на переменную через присвоенный ей идентификатор, а компилятор конвертирует это имя в соответствующий адрес памяти. Однако этот подход имеет некоторые ограничения, которые мы обсудим на этом и следующих уроках.

Оператор адреса позволяет узнать, какой адрес памяти присвоен определенной переменной. Всё довольно просто:

#include <iostream>

int main()
{
int a = 7;
std::cout << a << ‘\n’; // выводим значение переменной a
std::cout << &a << ‘\n’; // выводим адрес памяти переменной a

return 0;
}

1
2
3
4
5
6
7
8
9
10

#include <iostream>

intmain()

{

inta=7;

std::cout<<a<<‘\n’;// выводим значение переменной a

std::cout<<&a<<‘\n’;// выводим адрес памяти переменной a

return;

}

Результат на моем компьютере:

Примечание: Хотя оператор адреса выглядит так же, как оператор побитового И, отличить их можно по тому, что оператор адреса является унарным оператором, а оператор побитового И — бинарным оператором.

Двумерные динамически выделенные массивы

Другим распространенным применением указателей на указатели является динамическое выделение многомерных массивов. В отличие от двумерного фиксированного массива, который можно легко объявить следующим образом:

int array;

1 intarray157;

Динамическое выделение двумерного массива немного отличается. У вас может возникнуть соблазн написать что-то вроде следующего:

int **array = new int; // не будет работать!

1 int**array=newint157;// не будет работать!

Здесь вы получите ошибку. Есть два возможных решения. Если правый индекс является константой типа compile-time, то вы можете сделать следующее:

int (*array) = new int;

1 int(*array)7=newint157;

Скобки здесь потребуются для соблюдения приоритета. В C++11 хорошей идеей будет использовать ключевое слово auto для автоматического определения типа данных:

auto array = new int; // намного проще!

1 auto array=newint157;// намного проще!

К сожалению, это относительно простое решение не работает, если правый индекс не является константой типа compile-time. В таком случае всё немного усложняется. Сначала мы выделяем массив указателей (как в примере, приведенном выше), а затем перебираем каждый элемент массива указателей и выделяем динамический массив для каждого элемента этого массива. Итого, наш динамический двумерный массив — это динамический одномерный массив динамических одномерных массивов!

int **array = new int*; // выделяем массив из 15 указателей типа int — это наши строки
for (int count = 0; count < 15; ++count)
array = new int; // а это наши столбцы

1
2
3

int**array=newint*15;// выделяем массив из 15 указателей типа int — это наши строки

for(intcount=;count<15;++count)

arraycount=newint7;// а это наши столбцы

Доступ к элементам массива выполняется как обычно:

array = 4; // это то же самое, что и (array) = 4;

1 array83=4;// это то же самое, что и (array) = 4;

Этим методом, поскольку каждый столбец массива динамически выделяется независимо, можно сделать динамически выделенные двумерные массивы, которые не являются прямоугольными. Например, мы можем создать массив треугольной формы:

int **array = new int*; // выделяем массив из 15 указателей типа int — это наши строки
for (int count = 0; count < 15; ++count)
array = new int; // а это наши столбцы

1
2
3

int**array=newint*15;// выделяем массив из 15 указателей типа int — это наши строки

for(intcount=;count<15;++count)

arraycount=newintcount+1;// а это наши столбцы

В примере, приведенном выше, — это массив длиной 1, а — массив длиной 2 и т.д.

Для освобождения памяти динамически выделенного двумерного массива (который создавался с помощью этого способа) также потребуется цикл:

for (int count = 0; count < 15; ++count)
delete[] array;
delete[] array; // это следует выполнять в конце

1
2
3

for(intcount=;count<15;++count)

deletearraycount;

deletearray;// это следует выполнять в конце

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

А это, в свою очередь, приведет к неожиданным результатам.

Поскольку процесс выделения и освобождения двумерных массивов является несколько запутанным (можно легко наделать ошибок), то часто проще «сплющить» двумерный массив в одномерный массив:

// Вместо следующего:
int **array = new int*; // выделяем массив из 15 указателей типа int — это наши строки
for (int count = 0; count < 15; ++count)
array = new int; // а это наши столбцы

// Делаем следующее:
int *array = new int; // двумерный массив 15×7 «сплющенный» в одномерный массив

1
2
3
4
5
6
7

// Вместо следующего:

int**array=newint*15;// выделяем массив из 15 указателей типа int — это наши строки

for(intcount=;count<15;++count)

arraycount=newint7;// а это наши столбцы

// Делаем следующее:

int*array=newint105;// двумерный массив 15×7 «сплющенный» в одномерный массив

Простая математика используется для конвертации индексов строки и столбца прямоугольного двумерного массива в один индекс одномерного массива:

int getSingleIndex(int row, int col, int numberOfColumnsInArray)
{
return (row * numberOfColumnsInArray) + col;
}

// Присваиваем array значение 3, используя наш «сплющенный» массив
array = 3;

1
2
3
4
5
6
7

intgetSingleIndex(introw,intcol,intnumberOfColumnsInArray)

{

return(row *numberOfColumnsInArray)+col;

}

// Присваиваем array значение 3, используя наш «сплющенный» массив

arraygetSingleIndex(9,4,5)=3;

Другие типы данных

 
Программа, написанная на языке Си, оперирует с данными различных типов. Все данные имеют имя и тип. Обращение к данным в программе осуществляется по их именам (идентификаторам).Идентификатор — это последовательность, содержащая не более 32 символов, среди которых могут быть любые буквы латинского алфавита a — z, A — Z, цифры 0 — 9 и знак подчеркивания (_). Первый символ идентификатора не должен быть цифрой.
Несмотря на то, что допускается имя, имеющее до 32 символов, определяющее значение имеют только первые 8 символов. Помимо имени, все данные имеют тип. Указание типа необходимо для того, чтобы было известно, сколько места в оперативной памяти будет занимать данный объект.
Компилятор языка Си придерживается строгого соответствия прописных и строчных букв в именах идентификаторов и лексем.

Верно Неверно
int a = 2, b;
b = a+3;
Int a=2;  // правильно int
INT a=2;
int a = 2, b;
b = A + 3; // идентификатор А не объявлен
int a = 2;
b = a + 3; // идентификатор b не объявлен
Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector