Указатель на указатель — что это?
Указатель в C — не семантика, а механизм. Он сам по себе не несёт смысла, но может использоваться для выражения того или иного смысла. То же относится и к двойному указателю: он может использоваться для разных вещей. Вот несколько примеров.
-
В C параметры передаются по значению, то есть из коробки нету передачи параметров «по ссылке» (они же & -параметры C++, они же ref -/ out -параметры C#). Для того, чтобы объявить такой параметр, пользуются указателем на фактический параметр (то есть, передают в функцию адрес параметра). Если тип самого параметра — указатель, получается двойной указатель. Пример:
void split_half(char* input, char** first_half, char** second_half) < var len = strlen(input); var halflen = len / 2; *first_half = malloc(halflen + 1); strncpy(*first_half, input, halflen); (*firsthalf)[halflen] = 0; *second_half = strdup(&input[halflen]); >
int main(int argc, char** argv)
struct matrix < int** data; int width; int height; >void init_matrix(int width, int height, struct matrix* matrix) < matrix->width = width; matrix->height = height; matrix->data = malloc(height * sizeof(int*)); for (int y = 0; y < height; y++) matrix->data[y] = malloc(width * sizeof(int)); >
В C++ обычно ручное управление памятью не приветствуется, поэтому там кратные указатели встречаются куда реже.
Отслеживать
ответ дан 25 ноя 2014 в 14:36
206k 28 28 золотых знаков 291 291 серебряный знак 526 526 бронзовых знаков
Ни в языке С++, ни в языке С, нет такого понятия, как «указатель на указатель» в виде самостоятельной сущности с какими-то новыми качественными свойствами. Поэтому в строгом смысле слова, вопрос о том «зачем нужен указатель на указатель» не имеет никакого смысла.
В языках С и С++ есть такое понятие, как указатель. Просто указатель. Т.е. если у вас есть некий тип T , то вы можете объявить указатель p на этот тип
и заставить этот указатель указывать на объект t типа T
T t; p = &t;
После этого выражения t и *p будут обозначать один и тот же объект. Т.е. если вы, например, поменяете значение объекта *p , то тем самым вы поменяете и объект t (и наоборот). Вы можете также завести еще сколько угодно указателей на один и от же объект.
Это — элементарные основы идеи указателя.
Ну так а далее можно просто заметить, что тип T сам по себе может быть указательным типом. Но это совершенно ничего не меняет. Нет ничего принципиально разного между ситуацией, когда T — это int , и ситуацией, когда T — это double * . Все вышесказанное относится к обоим случаям в одинаковой мере.
Вот, собственно и все. Т.е. нет никакого смысла вводить в рассмотрение такую сущность, как «указатель на указатель», и устраивать вокруг нее какие-то обсуждения. Все, что нам нужно — это обычный указатель, который может просто-напросто указывать и на другой указатель. Но эти два уровня указательности (три, четыре, пять уровней. ) совершенно отдельны, ничего о друг друге не знают и знать не хотят.
И рассматривать такие указатели надо как обычные указатели. То же самое в полной мере справедливо и об «указателях на указатели на указатели», «указателях на указатели на указатели на указатели» и т.д. до бесконечности.
Указатель на указатель?
В современном С++ вообще указателей стараются избегать. Но вот несколько примеров:
Псевдо2D массив — массив массивов
Массив указателей. Например вектор хранит внутри указатель на буфер. .data() возвращает этот указатель. Если там хранятся указатели, получается указатель на указатель.
Когда нужно изменить указатель и используется АПИ С: указатель на объект используется если нужно изменить объект так, чтобы ето было видно вне изменяющей функции. В данном случае объект — другой указатель.
Ответ написан более трёх лет назад
Нравится 1 4 комментария
Что значит псевдо2d? Как тогда настоящие nd массивы делаются по-вашему?
Хасан Хафизов: > «Что значит псевдо2d?»
Значит что их тип это не «массив массивов» и все гарантии к 2D массивам на них не распространяются.
int a[5][5]; int** b = new int *[5]< new int [5], new int [5], new int [5], new int [5], new int [5], >; assert(&(a[2][4]) + 1 == &(a[3][0])); //Всегда true assert(&(b[2][4]) + 1 == &(b[3][0])); //Скорее всего false
Настоящие делаются либо динамическим выделением массива массивов (придётся знать при компиляции все размеры кроме первого), либо выделением плоского массива и доступа специальными аксессорами.
В первой строке вы выделяете прямоугольный массив (подходит для таблиц). А при помощи указателей можно создавать массивы, каждый элемент которого будет массив различной длинны (подходит для строк). И создавать их надо, конечно, динамически, но изначально некоторого небольшого размера, а при надобности вызывать realloc. Так не придется знать всех размеров.
Хасан Хафизов: Проблема с указателями на указатели в невозможности обеспечить cache locality и data prefetch. В результате скорость обработки ощутимо падает.
Указатели — весьма низкоуровневая вещь, но без них нельзя представить современного программирования.
Подумайте, что есть переменная? Ведь память — это, на деле, просто лента, на которую можно написать 0 и 1. Так вот переменная — лишь небольшой участок этой памяти. Если мы выделяем под переменную 4 байта, то она будет занимать 32 ячейки, в каждой из которых будут записаны 0 или 1. Ну хорошо, записали мы ее, а как же нам теперь ее считать? Тут и требуются указатели! Указатель указывает на первый бит участка, в котором и хранится переменная. Дальше, действуя подобным образом можно получить массивы, а дальше и все современное программирование.
Понимать эту абстракцию — совершенно необходимо. Научитесь писать на C. Советую Д. Ритчи «язык программирования си». Язык очень прост, содержательная часть книги всего 100 страниц. Но пару неделек придется потратить.
Указатели на указатели — многочисленное перенаправление
Концепция массивов указателей открыта и проста, поскольку индексы имеют вполне определенное значение. Тем не менее, в случае, когда один указатель указывает на другой, могут возникнуть проблемы. Указатель на указатель является формой многочисленного перенаправления или цепочки указателей. Рассмотрим рис.
Здесь p объявляется как указатель на целое, a q — это указатель на указатель на целое. Вызов printf() выводит число 10 на экран.
Указатели
Указатели представляют собой объекты, значением которых служат адреса других объектов (переменных, констант, указателей) или функций. Как и ссылки, указатели применяются для косвенного доступа к объекту. Однако в отличие от ссылок указатели обладают большими возможностями.
Определение указателя
Для определения указателя надо указать тип объекта, на который указывает указатель, и символ звездочки *:
тип_данных* название_указателя;
Сначала идет тип данных, на который указывает указатель, и символ звездочки *. Затем имя указателя.
Например, определим указатель на объект типа int:
int* p;
Такой указатель может хранить только адрес переменной типа int , но пока данный указатель не ссылается ни на какой объект и хранит случайное значение. Мы его даже можем попробовать вывести на консоль:
#include int main()
Например, в моем случае консоль вывела «0x8» — некоторый адрес в шестнадцатеричном формате (обычно для представления адресов в памяти применяется шестнадцатеричная форма). Но также можно инициализировать указатель некоторым значением:
int* p<>;
Поскольку конкрентное значение не указано, указатель в качестве значения получает число 0. Это значение представляет специальный адрес, который не указывает не на что. Также можно явным образом инициализировать нулем, например, используя специальную константу nullptr :
int* p;
Хотя никто не запрещает не инициализировать указатели. Однако в общем случае рекомендуется все таки инициализировать, либо каким-то конкретным значением, либо нулем, как выше. Так, к примеру, нулевое значение в будущем позволит определить, что указатель не указывает ни на какой объект.
Cтоит отметить что положение звездочки не влияет на определение указателя: ее можно помещать ближе к типу данных, либо к имени переменной — оба определения будут равноценны:
int* p1<>; int *p2<>;
Также стоит отметить, что размер значения указателя (хранимый адрес) не зависит от типа указателя. Он зависит от конкретной платформы. На 32-разрядных платформах размер адресов равен 4 байтам, а на 64-разрядных — 8 байтам. Например:
#include int main() < int *pint<>; double *pdouble<>; std::cout
В данном случае определены два указателя на разные типы — int и double. Переменные этих типов имеют разные размеры — 4 и 8 байт соответственно. Но размеры значений указателей будут одинаковы. В моем случае на 64-разрядной платформе размер обоих указателей равен 8 байтам.
Получение адреса и оператор &
С помощью операция & можно получить адрес некоторого объекта, например, адрес переменной. Затем этот адрес можно присвоить указателю::
int number ; int *pnumber ; // указатель pnumber хранит адрес переменной number
Выражение &number возвращает адрес переменной number . Поэтому переменная pnumber будет хранить адрес переменной number . Что важно, переменная number имеет тип int, и указатель, который указывает на ее адрес, тоже имеет тип int. То есть должно быть соответствие по типу. Однако также можно использовать ключевое слово auto :
int number ; auto *pnumber ; // указатель pnumber хранит адрес переменной number
Если мы попробуем вывести адрес переменной на консоль, то увидим, что он представляет шестнадцатиричное значение:
#include int main() < int number ; int *pnumber ; // указатель pnumber хранит адрес переменной number std::cout
Консольный вывод программы в моем случае:
number addr: 0x1543bffc74
В каждом отдельном случае адрес может отличаться и при разных запусках программы может меняться. К примеру, в моем случае машинный адрес переменной number — 0x1543bffc74 . То есть в памяти компьютера есть адрес 0x1543bffc74, по которому располагается переменная number. Так как переменная x представляет тип int , то на большинстве архитектур она будет занимать следующие 4 байта (на конкретных архитектурах размер памяти для типа int может отличаться). Таким образом, переменная типа int последовательно займет ячейки памяти с адресами 0x1543bffc74, 0x1543bffc75, 0x1543bffc76, 0x1543bffc77.
И указатель pnumber будет ссылаться на адрес, по которому располагается переменная number, то есть на адрес 0x1543bffc74.
Итак, указатель pnumber хранит адрес переменной number, а где хранится сам указатель pnumber? Чтобы узнать это, мы также можем применить к переменной pnumber операцию &:
#include int main() < int number ; int *pnumber ; // указатель pnumber хранит адрес переменной number std::cout
Консольный вывод программы в моем случае:
number addr: 0xe1f99ff7cc pnumber addr: 0xe1f99ff7c0
Здесь мы видим, что переменная number располагается по адресу 0xe1f99ff7cc , а указатель, который хранит этот адрес, — по адресу 0xe1f99ff7c0 . Из вывода видно, что обе переменные хранятся совсем рядом в памяти
Получение значения по адресу
Но так как указатель хранит адрес, то мы можем по этому адресу получить хранящееся там значение, то есть значение переменной number. Для этого применяется операция * или операция разыменования («indirection operator» / «dereference operator»). Результатом этой операции всегда является объект, на который указывает указатель. Применим данную операцию и получим значение переменной number:
#include int main() < int number ; int *pnumber ; std::cout Address = 0x44305ffd4c Value = 25
Значение, которое получено в результате операции разыменования, можно присвоить другой переменной:
int n1 ; int *pn1 ; // указатель pn1 хранит адрес переменной n1 int n2 < *pn1>; // n2 получает значение, которое хранится по адресу в pn1 std::cout int x = 10; int *px = &x; *px = 45; std::cout