e = *((struct employee**)vp); /* Получить хранящиеся в дереве данные */
/* использование данных в *е ... */
}
Как
можно указатель на вершину использовать как указатель на указатель данных? Рассмотрим, как была бы реализована вершина двоичного дерева. В каждой вершине хранится по крайней мере указатель на элемент данных пользователя и указатели на потенциальные порожденные вершины справа и слева. Поэтому она должна выглядеть примерно так.
struct binary_tree {
void *user_data; /* Указатель на данные пользователя */
struct binary_tree *left; /* Порожденная вершина слева или NULL */
struct binary_tree *right; /* Порожденная вершина справа или NULL */
/* ...здесь возможны другие поля... */
} node;
С и C++ гарантируют, что поля внутри структуры располагаются в порядке возрастания адресов. Таким образом, выражение '
&node.left < &node.right
' истинно. Более того, адрес структуры является также адресом ее первого поля (другими словами, игнорируя проблемы типов, '
&node == &node.user_data
').
Следовательно, концептуально '
е = *((struct employee**)vp);
' означает:
1.
vp
является
void*
, то есть общим указателем. Это адрес внутренней вершины дерева, но это также адрес части вершины (скорее всего, другого
void*
), которая указывает на данные пользователя.
2. '
(struct employee**)vp
' приводит адрес внутреннего указателя к нужному типу; он остается указателем на указатель, но в этот раз на
struct employee
. Помните, что приведение одного типа указателя к другому не изменяют значения (паттерна битов); оно меняет лишь способ интерпретации компилятором значения для анализа типов.
3. '
*((struct employee**)vp)
' разыменовывает вновь созданный
struct employee**
, возвращая годный к употреблению указатель
struct employee*
.
4. '
е = *((struct employee**)vp)'
сохраняет это значение в
е
для непосредственного использования позже.
Идея проиллюстрирована на рис. 14.2.
Рис. 14.2. Вершины дерева и их указатели
Для упрощения использования возвращенного указателя вы могли бы рассмотреть определение макроса:
Первый параметр является корнем дерева (не указателем на корень). Второй является указателем на функцию обратного вызова, которая вызывается с тремя аргументами, указателем на исследуемую вершину дерева; типом перечисления, указывающим, как осуществляется обход данной вершины; и целого, обозначающего глубину текущей вершины (корень находится на глубине 0, как объяснялось ранее).
Использование функции обратного вызова здесь такое же, как для
nftw
(см. раздел 8.4.3.2 «Функция обратного вызова
nftw
»). Там функция обратного вызова вызывается для каждого объекта в файловой системе. Здесь функция обратного вызова вызывается для каждого объекта, хранящегося в дереве.
Есть несколько способов прохождения, или «обхода», двоичного дерева:
использует второй способ: сначала родительская вершина, затем левая, затем правая. Каждый раз при встрече с вершиной говорят, что она посещается. [159] В ходе посещения порожденной вершины функция должна посетить и родительскую. Соответственно, значения типа
VISIT
указывают, на какой стадии произошла встреча с этой вершиной:
preorder
До посещения порожденных.
159
В голову приходят образы, как маленькие двоичные структуры данных сидят друг рядом с другом за чаем и пирожными. По крайней мере, такое бывает, если вы проводите слишком много времени перед своим компьютером. — Примеч. автора.
postorder
После посещения первой, но до посещения второй порожденной вершины.
endorder
После посещения обеих порожденных.
leaf
Эта вершина является концевой, не имеющей порожденных вершин.
ЗАМЕЧАНИЕ. Использованная здесь терминология не соответствует точно той, которая используется в формальных руководствах по структурированию данных. Там используются термины inorder, preorder и postorder для обозначения соответствующих трех перечисленных ранее способов прохождения дерева. Таким образом,
twalk
использует прохождение по типу
preorder
, но использует именованные константы preorder и т.д. для обозначения того, на какой стадии была посещена вершина. Это может сбивать с толку.