Указатели на структуры

Язык С
Учебное введение Оператор FOR Копирование файла Подсчет строк Массивы Аргументы - вызов по значению Область действия: внешние переменные Резюме Константы Арифметические операции Затем, если один из операндов имеет тип DOUBLE, то другой преобразуется в DOUBLE, и результат имеет тип DOUBLE Побитовые логические операции Условные выражения Поток управления Переключатель Цикл DO - WHILE Оператор GOTO и метки Функции, возвращающие нецелые значения Внешние переменные Правила, определяющие область действия Статические переменные Инициализация Препроцессор языка “C” Указатели и адреса Адресная арифметика Указатели символов и функции Многомерные массивы Инициализация массивов указателей Структуры Массивы сруктур Указатели на структуры Поиск в таблице Объединения Ввод и вывод Средства ввода/вывода не являются составной частью языка “с”, так что мы не выделяли их в нашем предыдущем изложении Форматный вывод - функция PRINTF Обычные символы (не %), которые предполагаются совпадающими со следующими отличными от символов пустых промежутков символами входного потока Форматное преобразование в памяти Низкоуровневый ввод/вывод - операторы READ и WRITE Произвольный доступ - SEEK и LSEEK Пример - распределитель памяти Лексические соглашения Имеется шесть классов лексем: идентификаторы, ключевые слова, константы, строки, операции и другие разделители Характеристики аппаратных средств Следующая ниже таблица суммирует некоторые свойства аппаратного оборудования, которые меняются от машины к машине Первичные выражения Первичные выражения, включающие ., ->, индексацию и обращения к функциям, группируются слева направо Унарные операции Выражение с унарными операциями группируется справо налево Операции равенства Выражение-равенства: выражение == выражение выражение != выражение Операция запятая Выражение-с-запятой: выражение , выражение Внешние определения данных Снова о типах В этом разделе обобщаются сведения об операциях, которые можно применять только к объектам определенных типов Анахронизмы Так как язык “C” является развивающимся языком, в старых программах можно встретить некоторые устаревшие конструкции
424070
знаков
0
таблиц
0
изображений

6.4. Указатели на структуры.

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

Внешнее описание массива KEYTAB не нужно изменять, но функции MAIN и BINARY требуют модификации.

MAIN() /* COUNT C KEYWORD; POINTER VERSION */

( INT T;

CHAR WORD[MAXWORD];

STRUCT KEY *BINARY(), *P;

WHILE ((T = GETWORD(WORD, MAXWORD;) !=EOF) IF (T==LETTER) IF ((P=BINARY(WORD,KEYTAB,NKEYS)) !=NULL) P->KEYCOUNT++;

FOR (P=KEYTAB; P>KEYTAB + NKEYS; P++) IF (P->KEYCOUNT > 0) PRINTF(“%4D %S/N”, P->KEYCOUNT, P->KEYWORD);

) STRUCT KEY BINARY(WORD, TAB, N) / FIND WORD */ CHAR WORD / IN TAB[0]...TAB[N-1] */ STRUCT KEY TAB [];

INT N;

( INT COND;

STRUCT KEY *LOW = &TAB[0];

STRUCT KEY *HIGH = &TAB[N-1];

STRUCT KEY *MID;

WHILE (LOW <= HIGH) ( MID = LOW + (HIGH-LOW) / 2;

IF ((COND = STRCMP(WORD, MID->KEYWORD)) < 0) HIGH = MID - 1;

ELSE IF (COND > 0) LOW = MID + 1;

ELSE RETURN(MID);

) RETURN(NULL);

)

Здесь имеется несколько моментов, которые стоит отметить. Во-первых, описание функции BINARI должно указывать, что она возвращает указатель на структуру типа KEY, а не на целое; это объявляется как в функции MAIN, так и в BINARY.

Если функция BINARI находит слово, то она возвращает указатель на него; если же нет, она возвращает NULL.

Во-вторых, все обращения к элементам массива KEYTAB осуществляются через указатели. Это влечет за собой одно существенное изменение в функции BINARY: средний элемент больше нельзя вычислять просто по формуле

MID = (LOW + HIGH) / 2 потому что сложение двух указателей не дает какого-нибудь полезного результата (даже после деления на 2) и в действительности является незаконным. эту формулу надо заменить на

MID = LOW + (HIGH-LOW) / 2 в результате которой MID становится указателем на элемент, расположенный посередине между LOW и HIGH.

Вам также следует разобраться в инициализации LOW и HIGH. указатель можно инициализировать адресом ранее определенного объекта; именно как мы здесь и поступили.

В функции MAIN мы написали FOR (P=KEYTAB; P < KEYTAB + NKEYS; P++) Если P является указателем структуры, то любая арифметика с P учитывает фактический размер данной структуры, так что P++ увеличивает P на нужную величину, в результате чего P указывает на следующий элемент массива структур. Но не считайте, что размер структуры равен сумме размеров ее членов, - из-за требований выравнивания для различных объектов в структуре могут возникать “дыры”.

И, наконец, несколько второстепенный вопрос о форме записи программы. Если возвращаемая функцией величина имеет тип, как, например, в

STRUCT KEY *BINARY(WORD, TAB, N) Tо может оказаться, что имя функции трудно выделить среди текста. В связи с этим иногда используется другой стиль записи:

STRUCT KEY * BINARY(WORD, TAB, N) Это главным образом дело вкуса; выберите ту форму, которая вам нравится, и придерживайтесь ее.

6.5. Структуры, ссылающиеся на себя.

Предположим, что нам надо справиться с более общей задачей, состоящей в подсчете числа появлений всех слов в некотором файле ввода. Так как список слов заранее не известен, мы не можем их упорядочить удобным образом и использовать бинарный поиск. Мы даже не можем осуществлять последовательный просмотр при поступлении каждого слова, с тем чтобы установить, не встречалось ли оно ранее; такая программа будет работать вечно. (Более точно, ожидаемое время работы растет как квадрат числа вводимых слов). Как же нам организовать программу, чтобы справиться со списком произвольных слов?

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

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

Каждому новому слову соответствует один “узел” дерева;

каждый узел содержит: указатель текста слова счетчик числа появлений указатель узла левого потомка указатель узла правого потомка Никакой узел не может иметь более двух детей; возможно отсутсвие детей или наличие только одного потомка.

Узлы создаются таким образом, что левое поддерево каждого узла содержит только те слова, которые меньше слова в этом узле, а правое поддерево только те слова, которые больше. Чтобы определить, находится ли новое слово уже в дереве, начинают с корня и сравнивают новое слово со словом, хранящимся в этом узле. Если слова совпадают, то вопрос решается утвердительно. Если новое слово меньше слова в дереве, то переходят к рассмотрению левого потомка; в противном случае исследуется правый потомок. Если в нужном направлении потомок отсутствует, то значит новое слово не находится в дереве и место этого недостающего потомка как раз и является местом, куда следует поместить новое слово. Поскольку поиск из любого узла приводит к поиску одного из его потомков, то сам процесс поиска по существу является рекурсивным. В соответствии с этим наиболее естественно использовать рекурсивные процедуры ввода и вывода.

Возвращаясь назад к описанию узла, ясно, что это будет структура с четырьмя компонентами: STRUCT TNODE ( /* THE BASIC NODE */ CHAR WORD; / POINTS TO THE TEXT */ INT COUNT; /* NUMBER OF OCCURRENCES */ STRUCT TNODE LEFT; / LEFT CHILD */ STRUCT TNODE RIGHT; / RIGHT CHILD */

);

Это “рекурсивное” описание узла может показаться рискованным, но на самом деле оно вполне корректно. Структура не имеет права содержать ссылку на саму себя, но

STRUCT TNODE *LEFT;

описывает LEFT как указатель на узел, а не как сам узел.

140

Текст самой программы оказывается удивительно маленьким, если, конечно, иметь в распоряжении набор написанных нами ранее процедур, обеспечивающих нужные действия. Мы имеем в виду функцию GETWORD для извлечения каждого слова из файла ввода и функцию ALLOC для выделения места для хранения слов.

Ведущая программа просто считывает слова с помощью функции GETWORD и помещает их в дерево, используя функцию TREE.

#DEFINE MAXWORD 20

MAIN() /* WORD FREGUENCY COUNT */

( STRUCT TNODE *ROOT, *TREE();

CHAR WORD[MAXWORD];

INT T;

ROOT = NULL;

WHILE ((T = GETWORD(WORD, MAXWORD)) ! = EOF) IF (T == LETTER) ROOT = TREE(ROOT, WORD);

TREEPRINT(ROOT);

)

Функция TREE сама по себе проста. Слово передается функцией MAIN к верхнему уровню (корню) дерева. На каждом этапе это слово сравнивается со словом, уже хранящимся в этом узле, и с помощью рекурсивного обращения к TREE просачивается вниз либо к левому, либо к правому поддереву. В конце концов это слово либо совпадает с каким-то словом, уже находящимся в дереве (в этом случае счетчик увеличивается на единицу), либо программа натолкнется на нулевой указатель, свидетельствующий о необходимости создания и добавления к дереву нового узла. В случае создания нового узла функция TREE возвращает указатель этого узла, который помещается в родительский узел.

STRUCT TNODE *TREE(P, W) /* INSTALL W AT OR BELOW P */ STRUCT TNODE *P;

CHAR *W;

( STRUCT TNODE *TALLOC();

CHAR *STRSAVE();

INT COND;

IF (P == NULL) ( /* A NEW WORD HAS ARRIVED */ P == TALLOC(); /* MAKE A NEW NODE */ P->WORD = STRSAVE(W);

P->COUNT = 1;

P->LEFT = P->RIGHT = NULL;

) ELSE IF ((COND = STRCMP(W, P->WORD)) == 0) P->COUNT++; /* REPEATED WORD */ ELSE IF (COND < 0)/* LOWER GOES INTO LEFT SUBTREE */ P->LEFT = TREE(P->LEFT, W);

ELSE /* GREATER INTO RIGHT SUBTREE */ P->RIGHT = TREE(P->RIGHT, W);

RETURN(P);

)

Память для нового узла выделяется функцией TALLOC, являющейся адаптацией для данного случая функции ALLOC, написанной нами ранее. Она возвращает указатель свободного пространства, пригодного для хранения нового узла дерева. (Мы вскоре обсудим это подробнее). Новое слово копируется функцией STRSAVE в скрытое место, счетчик инициализируется единицей, и указатели обоих потомков полагаются равными нулю.

Эта часть программы выполняется только при добавлении нового узла к ребру дерева. Мы здесь опустили проверку на ошибки возвращаемых функций STRSAVE и TALLOC значений (что неразумно для практически работающей программы).

Функция TREEPRINT печатает дерево, начиная с левого поддерева; в каждом узле сначала печатается левое поддерево (все слова, которые младше этого слова), затем само слово, а затем правое поддерево (все слова, которые старше). Если вы неуверенно оперируете с рекурсией, нарисуйте дерево сами и напечатайте его с помощью функции TREEPRINT ; это одна из наиболее ясных рекурсивных процедур, которую можно найти.

TREEPRINT (P) /* PRINT TREE P RECURSIVELY */ STRUCT TNODE *P;

( IF (P != NULL) ( TREEPRINT (P->LEFT);

PRINTF(“%4D %SN”, P->COUNT, P->WORD);

TREEPRINT (P->RIGHT);

)

)

Практическое замечание: если дерево становится “несбалансированным” из-за того, что слова поступают не в случайном порядке, то время работы программы может расти слишком быстро. В худшем случае, когда поступающие слова уже упорядочены, настоящая программа осуществляет дорогостоящую имитацию линейного поиска. Существуют различные обобщения двоичного дерева, особенно 2-3 деревья и AVL деревья, которые не ведут себя так “в худших случаях”, но мы не будем здесь на них останавливаться.

Прежде чем расстаться с этим примером, уместно сделать небольшое отступление в связи с вопросом о распределении памяти. Ясно, что в программе желательно иметь только один распределитель памяти, даже если ему приходится размещать различные виды объектов. Но если мы хотим использовать один распределитель памяти для обработки запросов на выделение памяти для указателей на переменные типа CHAR и для указателей на STRUCT TNODE, то при этом возникают два вопроса. Первый: как выполнить то существующее на большинстве реальных машин ограничение, что объекты определенных типов должны удовлетворять требованиям выравнивания (например, часто целые должны размещаться в четных адресах)? Второй: как организовать описания, чтобы справиться с тем, что функция ALLOC должна возвращать различные виды указателей ?

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

Например, на PDP-11 достаточно, чтобы функция ALLOC всегда возвращала четный указатель, поскольку в четный адрес можно поместить любой тип объекта. единственный расход при этом лишний символ при запросе на нечетную длину. Аналогичные действия предпринимаются на других машинах. Таким образом, реализация ALLOC может не оказаться переносимой, но ее использование будет переносимым. Функция ALLOC из главы 5 не предусматривает никакого определенного выравнивания; в главе 8 мы продемонстрируем, как правильно выполнить эту задачу.

Вопрос описания типа функции ALLOC является мучительным для любого языка, который серьезно относится к проверке типов. Лучший способ в языке “C” - объявить, что ALLOC возвращает указатель на переменную типа CHAR, а затем явно преобразовать этот указатель к желаемому типу с помощью операции перевода типов. Таким образом, если описать P в виде

CHAR *P;

то (STRUCT TNODE *) P преобразует его в выражениях в указатель на структуру типа TNODE . Следовательно, функцию TALLOC можно записать в виде: STRUCT TNODE *TALLOC()

( CHAR *ALLOC();

RETURN ((STRUCT TNODE *) ALLOC(SIZEOF(STRUCT TNODE)));

)

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

Упражнение 6-4.

Напишите программу, которая читает “C”-программу и печатает в алфавитном порядке каждую группу имен переменных, которые совпадают в первых семи символах, но отличаются где-то дальше. (Сделайте так, чтобы 7 было параметром).

Упражнение 6-5.

Напишите программу выдачи перекрестных ссылок, т.е.

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

Упражнение 6-6.

Напишите программу, которая печатает слова из своего файла ввода, расположенные в порядке убывания частоты их появления. Перед каждым словом напечатайте число его появлений.


Информация о работе «Язык С»
Раздел: Информатика, программирование
Количество знаков с пробелами: 424070
Количество таблиц: 0
Количество изображений: 0

Похожие работы

Скачать
48443
0
0

... основаниям. При этом философская абстракция языка оказывается неразрывно связана с основными темами и движениями философии в целом. Более конкретно, на ранние стадии традиционно рассматриваемого в рамках АФ анализа обыденного языка глубокое влияние оказала философия Дж. Э. Мура, особенно его учение о здравом смысле, согласно которому такие понятия, как «человек», «мир», «я», «внешний мир», « ...

Скачать
43709
0
0

... и других странах СНГ, а также облегчение доступа к русской и мировой культуре и науке. Таким образом, судя по данным наших исследований, востребованность русского языка осталась в республике достаточно высокой. Многие представители современной молдавской молодежи продолжают, как их отцы и деды, тянуться к русской культуре, научным и техническим достижениям России. Русский язык остается языком ...

Скачать
39778
0
1

... рисуночное словесно-слоговое письмо). Памятники среднеэламского периода (14—12 вв. до н.э.) выполнены аккадской клинописью. Памятники новоэламского периода относятся к 8—6 вв. до н.э. Был официальным языком в персидском государстве Ахеменидов в 6—4 вв. предполагается, что он, подвергшись влиянию древнеперсидского, сохранился до раннего средневековья. 7. Бурушаски язык Язык бурушаски ( ...

Скачать
64931
0
0

... /диалект), скифский, согдийский, среднеперсидский, таджикский, таджриши (язык/диалект), талышский, татский, хорезмийский, хотаносакский, шугнано-рушанская группа языков, ягнобский, язгулямский и др. Они относятся к индоиранской ветви индоевропейских языков. Области распространения: Иран, Афганистан, Таджикистан, некоторые районы Ирака, Турции, Пакистана, Индии, Грузии, Российской Федерации. Общее ...

0 комментариев


Наверх