Я искал и искал ответ на этот вопрос, но не могу найти ничего, что я на самом деле "получаю".
Я очень новичок в c++ и не могу понять, как использовать двойные, тройные указатели и т.д. В чем их смысл?
Может кто-нибудь просветить меня
Я искал и искал ответ на этот вопрос, но не могу найти ничего, что я на самом деле "получаю".
Я очень новичок в c++ и не могу понять, как использовать двойные, тройные указатели и т.д. В чем их смысл?
Может кто-нибудь просветить меня
Честно говоря, в хорошо написанном C++ вы должны очень редко видеть T**
вне библиотечного кода. На самом деле, чем больше у вас звезд, тем ближе вы получаете награду определенной природы.
Это не означает, что указатель на указатель никогда не требуется; вам может потребоваться построить указатель на указатель по той же причине, что вам когда-либо понадобится для создания указателя на любой другой тип объекта.
В частности, я могу ожидать увидеть такую вещь внутри структуры данных или реализации алгоритма, если вы перетасовываете динамически выделенные узлы, возможно?
Вообще, хотя, вне этого контекста, если вам нужно передать ссылку на указатель, вы сделаете именно это (т.е. T*&
), а не удвоитесь по указателям, и даже это должно быть довольно редко.
В Qaru вы увидите, как люди делают ужасные вещи с указателями на массивы динамически выделенных указателей на данные, пытаясь реализовать наименее эффективный "2D-вектор", о котором они могут думать. Пожалуйста, не вдохновляйтесь ими.
Таким образом, ваша интуиция не лишена заслуг.
Важной причиной того, почему вы должны/должны знать о pointer-to-pointer -..., является то, что вам иногда приходится взаимодействовать с другими языками (например, с C) через некоторый API (например, Windows API).
Эти API часто имеют функции, которые имеют выходной параметр, который возвращает указатель. Однако эти другие языки часто не имеют ссылок или совместимы (с C++) ссылками. Это ситуация, когда требуется указатель на указатель.
Я очень новичок в c++ и не могу понять, как использовать двойные, тройные указатели и т.д. В чем их смысл?
Трюк для понимания указателей на C - это просто вернуться к основам, которых вы, вероятно, никогда не учили. Они есть:
x
- переменная типа T
то &x
- значение типа T*
.x
оценивает значение типа T*
то *x
является переменной типа T
Более конкретно...x
оценивает значение типа T*
, равное &a
для некоторой переменной a
типа T
, то *x
является псевдонимом для a
.Теперь все следующее:
int x = 123;
x
- переменная типа int
. Его значение равно 123
.
int* y = &x;
y
- переменная типа int*
. x
- переменная типа int
. Итак, &x
- значение типа int*
. Поэтому мы можем хранить &x
в y
.
*y = 456;
y
оценивает содержимое переменной y
. Это значение типа int*
. Применение *
к значению типа int*
дает переменную типа int
. Поэтому мы можем назначить ему 456. Что такое *y
? Это псевдоним для x
. Поэтому мы просто назначили 456 на x
.
int** z = &y;
Что такое z
? Это переменная типа int**
. Что такое &y
? Поскольку y
является переменной типа int*
, &y
должно быть значением типа int**
. Поэтому мы можем назначить его z
.
**z = 789;
Что такое **z
? Работайте изнутри. z
оценивает значение int**
. Поэтому *z
- переменная типа int*
. Это псевдоним для y
. Поэтому это то же самое, что и *y
, и мы уже знаем, что это такое; это псевдоним для x
.
На самом деле, какой смысл?
Здесь у меня есть лист бумаги. Он говорит, что в 1600 Pennsylvania Avenue Washington DC
. Это дом? Нет, это лист бумаги с адресом дома, написанным на нем. Но мы можем использовать этот лист бумаги, чтобы найти дом.
Здесь у меня десять миллионов листов бумаги, все пронумерованы. Бумага номер 123456 говорит 1600 Pennsylvania Avenue
. Есть 123456 дом? Нет. Это листок бумаги? Нет. Но мне все еще достаточно информации, чтобы найти дом.
То, что точка: часто нам нужно обращаться к объектам через несколько уровней косвенности для удобства.
Тем не менее, двойные указатели сбивают с толку и признак того, что ваш алгоритм недостаточно абстрактен. Старайтесь избегать их, используя хорошие методы проектирования.
Он менее используется в c++. Однако в C это может быть очень полезно. Скажите, что у вас есть функция, которая будет вызывать некоторый случайный объем памяти и заполнять память некоторыми вещами. Было бы больно вызвать функцию, чтобы получить размер, который вам нужно выделить, а затем вызвать другую функцию, которая будет заполнять память. Вместо этого вы можете использовать двойной указатель. Двойной указатель позволяет функции установить указатель на ячейку памяти. Есть и другие вещи, которые можно использовать, но это лучшее, что я могу придумать.
int func(char** mem){
*mem = malloc(50);
return 50;
}
int main(){
char* mem = NULL;
int size = func(&mem);
free(mem);
}
Двойной указатель - это просто указатель на указатель. Общее использование - для массивов символов. Представьте первую функцию почти в каждой программе C/C++:
int main(int argc, char *argv[])
{
...
}
Что также можно записать
int main(int argc, char **argv)
{
...
}
Переменная argv является указателем на массив указателей на char. Это стандартный способ прохождения вокруг массивов C "строк". Зачем это? Я видел, как он использовался для поддержки нескольких языков, блоков ошибок и т.д.
Не забывайте, что указатель - это просто номер - индекс слота памяти внутри компьютера. Что это, не более того. Таким образом, двойной указатель является индексом части памяти, которая просто содержит другой индекс в другом месте. Если вам нравится математическое соединение-точки.
Вот как я объяснил своим детям:
Представьте, что компьютерная память - это серия ящиков. В каждом ящике записано число, начинающееся с нуля, поднимающееся на 1, на сколько бы байтов памяти не было. Скажем, у вас есть указатель на какое-то место в памяти. Этот указатель - это только номер окна. Мой указатель, скажем 4. Я смотрю в поле №4. Внутри есть еще один номер, на этот раз он 6. Итак, теперь мы смотрим в поле №6 и получаем последнее, что хотели. Мой исходный указатель (который сказал "4") был двойным указателем, потому что содержимое его поля было индексом другого поля, а не конечным результатом.
Кажется, в последнее время сами указатели стали парией программирования. Вернувшись в не слишком отдаленное прошлое, было совершенно нормально проходить вокруг указателей на указатели. Но с распространением Java и увеличением использования pass-by-reference в C++ основное понимание указателей уменьшилось - особенно вокруг, когда Java стала установлена в качестве первокурсника языка начинающих, скажем, Pascal и C.
Я думаю, что много яда о указателях состоит в том, что люди просто не понимают их должным образом. Вещи, которых люди не понимают, высмеиваются. Поэтому они стали "слишком жесткими" и "слишком опасными". Я думаю, что даже с предполагаемыми учеными людьми, пропагандирующими Smart Pointers и т.д., Эти идеи следует ожидать. Но на самом деле есть очень мощный инструмент программирования. Честно говоря, указатели - это волшебство программирования, и, в конце концов, это всего лишь число.
Во многих ситуациях Foo*&
заменяет Foo**
. В обоих случаях у вас есть указатель, адрес которого может быть изменен.
Предположим, у вас есть абстрактный тип без значения, и вам нужно его вернуть, но возвращаемое значение учитывается кодом ошибки:
error_code get_foo( Foo** ppfoo )
или же
error_code get_foo( Foo*& pfoo_out )
Теперь аргумент функции, являющийся изменяемым, редко бывает полезен, поэтому возможность изменения, где указывает крайний указатель ppFoo
, редко используется. Однако указатель имеет значение NULL - поэтому, если аргумент get_foo
является обязательным, указатель действует как необязательная ссылка.
В этом случае возвращаемое значение является необработанным указателем. Если он возвращает принадлежащий ему ресурс, он обычно должен быть вместо std::unique_ptr<Foo>*
- умный указатель на этом уровне косвенности.
Если вместо этого он возвращает указатель на то, на что он не разделяет права собственности, тогда исходный указатель имеет больше смысла.
Существуют и другие применения для Foo**
помимо этих "сырых параметров". Если у вас есть полиморфный тип нецензурного характера, не владеющие дескрипторами Foo*
, и по той же причине, почему вы хотите иметь int*
вы хотели бы иметь Foo**
.
Что затем заставляет вас спросить: "Почему вы хотите int*
?" В современном C++ int*
является не имеющей прав на nullable изменяемой ссылкой на int
. Это ведет себя лучше, когда хранится в struct
чем ссылка (ссылки в структурах порождают запутанную семантику вокруг назначения и копирования, особенно если они смешаны с не-ссылками).
Иногда вы могли бы заменить int*
на std::reference_wrapper<int>
, well std::optional<std::reference_wrapper<int>>
, но обратите внимание, что это будет 2x размером до простого int*
.
Таким образом, есть законные основания использовать int*
. После этого вы можете законно использовать Foo**
если хотите, чтобы указатель на тип, отличный от значения. Вы даже можете попасть в int**
, имея непрерывный массив int*
, над которым хотите работать.
Законно добраться до трехзвездочного программиста становится все сложнее. Теперь вам нужна законная причина (скажем) хотеть передать Foo**
по косвенности. Обычно задолго до того, как вы достигнете этого момента, вам следует рассмотреть возможность абстрагирования и/или упрощения структуры кода.
Все это игнорирует наиболее распространенную причину; взаимодействуя с C API. C не имеет unique_ptr
, он не имеет span
. Он обычно использует примитивные типы вместо structs, потому что structs требует неудобного доступа на основе функций (без перегрузки оператора).
Поэтому, когда C++ взаимодействует с C, вы иногда получаете 0-3 больше *
чем эквивалентный код C++.
Использование должно иметь указатель на указатель, например, если вы хотите передать указатель на метод по ссылке.
В C++, если вы хотите передать указатель как параметр out или in/out, вы передаете его по ссылке:
int x;
void f(int *&p) { p = &x; }
Но ссылка не может ("юридически") быть nullptr
, поэтому, если указатель необязателен, вам нужен указатель на указатель:
void g(int **p) { if (p) *p = &x; }
Конечно, поскольку C++ 17 у вас есть std::optional
, но "двойной указатель" уже много десятилетий является идиоматическим кодом C/C++, так что все должно быть в порядке. Кроме того, использование не так хорошо, вы либо:
void h(std::optional<int*> &p) { if (p) *p = &x) }
который является уродливым на сайте вызова, если у вас уже нет std::optional
или:
void u(std::optional<std::reference_wrapper<int*>> p) { if (p) p->get() = &x; }
что не очень приятно само по себе.
Кроме того, некоторые могут утверждать, что g
лучше читать на сайте вызова:
f(p);
g(&p); // '&' indicates that 'p' might change, to some folks
Какое реальное использование имеет двойной указатель?
Вот практический пример. Скажем, у вас есть функция, и вы хотите отправить массив строковых параметров (возможно, у вас есть DLL, в которую вы хотите передать параметры). Это может выглядеть так:
#include <iostream>
void printParams(const char **params, int size)
{
for (int i = 0; i < size; ++i)
{
std::cout << params[i] << std::endl;
}
}
int main()
{
const char *params[] = { "param1", "param2", "param3" };
printParams(params, 3);
return 0;
}
Вы будете отправлять массив указателей const char
, каждый указатель указывает на начало строки C с нулевым завершением. Компилятор распадет ваш массив на указатель на аргумент функции, поэтому вы получите const char **
указатель на первый указатель массива указателей const char
. Поскольку в этот момент размер массива будет потерян, вам нужно передать его в качестве второго аргумента.
Один случай, когда я использовал его, - это функция, управляющая связанным списком, в C.
Есть
struct node { struct node *next; ... };
для узлов списка и
struct node *first;
чтобы указать на первый элемент. Все функции манипуляции берут struct node **
, потому что я могу гарантировать, что этот указатель non- NULL
даже если список пуст, и мне не нужны специальные случаи для вставки и удаления:
void link(struct node *new_node, struct node **list)
{
new_node->next = *list;
*list = new_node;
}
void unlink(struct node **prev_ptr)
{
*prev_ptr = (*prev_ptr)->next;
}
Чтобы вставить в начале списка, просто передайте указатель на first
указатель, и он будет делать то же самое, даже если значение first
равно NULL
.
struct node *new_node = (struct node *)malloc(sizeof *new_node);
link(new_node, &first);
Множественная косвенность в значительной степени является удержанием от C (у которого нет ни ссылочных, ни контейнерных типов). Вы не должны видеть многократное косвенное отношение к хорошо написанному C++, если только вы не имеете дело с библиотекой наследия C или что-то в этом роде.
Сказав это, множественная косвенность выпадает из некоторых довольно распространенных случаев использования.
В C и C++ выражения массива будут "распадаться" от типа "N-элементный массив T
" до "указателя на T
" в большинстве случаев 1. Итак, предположим, что определение массива подобно
T *a[N]; // for any type T
Когда вы передаете a
a, например:
foo( a );
выражение a
будет преобразовано из "N-элементного массива T *
" в "указатель на T *
" или T **
, поэтому то, что фактически получает функция,
void foo( T **a ) { ... }
Второе место, которое они всплывают, - это когда вы хотите, чтобы функция изменяла параметр типа указателя, что-то вроде
void foo( T **ptr )
{
*ptr = new_value();
}
void bar( void )
{
T *val;
foo( &val );
}
Поскольку C++ представил ссылки, вы, вероятно, не увидите этого так часто. Обычно вы увидите это только при работе с C-based API.
Вы также можете использовать множественную косвенность для настройки "зубчатых" массивов, но вы можете добиться того же самого с контейнерами C++ для гораздо меньшей боли. Но если вы чувствуете мазохистство:
T **arr;
try
{
arr = new T *[rows];
for ( size_t i = 0; i < rows; i++ )
arr[i] = new T [size_for_row(i)];
}
catch ( std::bad_alloc& e )
{
...
}
Но большую часть времени в C++, единственный раз, когда вы должны увидеть множественную косвенность, когда массив указателей "распадается" на само выражение указателя.
sizeof
или одинарного &
оператора, или является строка символов используются для инициализации другого массива в объявлении.