Урок №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 |
#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 |
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 |
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 |
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 |
// Вместо следующего: 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 |
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 не объявлен |