Курс на Си. Подбельский. Курс программирования на Си. В., Фомин С. С. Курс программирования на языке Си Учебник
Скачать 1.57 Mb.
|
Указатели на структуры. Эти указатели определяются, как и указатели на данные других типов. Пример: struct goods *p_goods; struct studest *p_stu, *p_stu2; Указатели на структуры могут вводиться и для безымянных структурных типов: struct { /* Безымянный структурный тип */ char processor [10]; /* Тип процессора */ int frequency; /* Частота в ГГц */ int memory; /* Память в Гбайт */ int disk; /* Емкость диска в Гбайт */ } *point_IBM, *point_2; Если название структурного типа введено с помощью typedef, то при определении указателей название этого типа используется без предшествующего служебного слова struct: complex 9cc, *ss, comp; При определении указателя на структуру он может быть инициализирован. Наиболее корректно в качестве инициализирующего значения применять адрес структурного объекта того же типа, что и тип определяемого указателя: struct particle { double mass; float coord [3]; } dot [3], point, *pinega; /* Инициализация указателей: */ struct particle *p_d=&dot [1], *pinta=&point; Значение указателя на структуру может быть определено и с помощью обычного присваивания: pinega=&dot [0]; Указатели как средство доступа к компонентам структур. Указатель на структуру, настроенный на конкретную структуру (конкретный объект) того же типа, обеспечивает доступ к ее элементам двумя способами: ( * указатель_на_структуру ) . имя_элемента или указатель_на_структуру -> имя_элемента Первый способ традиционный. Он основан на обратимости операций разыменования '*' и получения адреса '&'. Обозначив знак равенства последовательностью '==', можно таким образом формально напомнить эти правила на конкретном примере: если pinega = = &dot [0], то Доступ к элементам структуры dot[0] с помощью разыменования адресующего его указателя pinega можно в соответствии с приведенными соотношениями реализовать с помощью таких конструкций: (*pinega).mass = = dot[0].mass (*pinega).coord [0] = = dot[0].coord[0] (*pinega).coord [1] = = dot[0].coord[1] (*pinega).coord [2] = = dot[0].coord[2] Важным является наличие скобок, ограничивающих действие операции разыменования (*pinega). Скобки необходимы, так как бинарная операция «точка» имеет более высокий ранг, чем унарная операция разыменования (см. табл. 1.4 приоритетов операций в главе 1). Присвоить элементам структуры dot[0] значения можно, например, с помощью таких операторов присваивания: (*pinega).mass = 33.3; (*pinega).coord[1] = 12.3; Второй способ доступа к элементам структуры с помощью «настроенного» на нее указателя предусматривает применение специальной операции «стрелка» (->). Операция «стрелка» имеет самый высший ранг (см. табл. 1.4) наряду со скобками и операцией «точка». Операция «стрелка» обеспечивает доступ к элементу структуры через адресующий ее указатель того же структурного типа. Формат применения операции в простейшем случае: указатель_на_структуру -> имя_элемента Операция «стрелка» двуместная. Применяется для доступа к элементу (компоненту), задаваемому правым операндом, той структуры, которую адресует левый операнд. В качестве левого операнда должен быть указатель на структуру: в качестве правого - обозначение (имя) компонента этой структуры. Операция «стрелка» (->) иногда называется операцией косвенного выбора компонента (элемента) структурированного объекта, адресуемого указателем. Примеры: Если в программе (см. выше) определена структура point типа struct particle и на нее настроен указатель: struct particle *pinta = &point; то будут справедливы следующие равенства: pinta -> mass == (*pinta).mass pinta -> coord == (*pinta).coord Изменить значения элементов структуры point в этом случае можно, например, такими операторами: pinta -> mass = 18.4; for (i=0; i<3; i++) pinta -> coord[i] = 0.1*i; Тип результата выражения с операцией «стрелка» (->) совпадает с типом правого операнда, то есть того элемента структуры, на который «нацелена» стрелка. Операции над указателями на структуры. Эти операции не отличаются от операций над другими указателями на данные. (Исключение составляет операция «стрелка» (->), но ее мы уже рассмотрели). Если присвоить указателю на структуру конкретного структурного типа значение адреса одного из элементов массива структур того же типа, то, изменяя значение указателя (например, с помощью операций ++ или —), можно равномерно «перемещаться» по массиву структур. В качестве иллюстрации рассмотрим следующую задачу. Вычислить сумму заданного количества комплексных чисел, представленных в программе массивом array[ ] структур. struct complex * point = & array [0]; int k, i; k = sizeof (array) / sizeof (array [0]); for (i=0; i { summa.x += point -> x; summa.y += point -> y; point++; } printf(" \n Сумма: real=%f,\t imag=%f", summa.x, summa.y); } Результат выполнения программы: Сумма: real= -8.000000, imag=-16.000000 В программе введен тип struct complex. Определены: массив структур array[ ], структура summa, где формируется результат, и указатель point, в начале программы настроенный на первый (нулевой) элемент массива структур, а затем в цикле «пробегающий» по всему массиву. Доступ к структуре summa реализован с помощью уточненных имен (операция «точка»). Доступ к элементам структур, входящих в массив, осуществляется через указатель point и с помощью операции «стрелка» (->). Указатели на структуры как компоненты структур. В соответствии с синтаксисом языка компонентами структур могут быть данные любых типов, за исключением структур того же типа, что и определяемый структурный тип. Например, элементом структуры, как уже говорилось, может быть структура, тип которой уже определен. Обычна конструкция: struct mix {int N; double * d;}; struct hole { struct mix exit; float b; }; struct element { /* Структурный тип «химический элемент» */ int number /* Порядковый номер * double mass; /* Атомный вес */ char name[16] /* Название элемента */ struct element * next; }; В структурном типе «химический элемент» есть компонент next - указатель на структуру того же типа. С его помощью можно формировать списки (цепочки) интересующих нас элементов. Например, можно связать все элементы одной группы периодической таблицы химических элементов либо представить в виде единого списка всю таблицу Д. И. Менделеева. При определении структурных типов может потребоваться организация взаимных перекрестных связей между структурами двух или более разных типов. В этом случае невозможно выполнить требование об определенности всех связываемых структурных типов. Выходом из тупиковой ситуации служит применение указателей на структуры. Например: struct part { /* Структурный тип */ double modul; struct cell * element_cell; struct part * element_part; }; struct cell { /* Структурный тип */ long summa; struct cell * one; struct part * two; } В данном примере для организации перекрестных ссылок в структурах типа struct part использованы в качестве элементов указатели на объекты типа struct cell. В структуры типа struct cell входят указатели на структуры типа struct part. Структуры и функции Для «взаимоотношения» структур и функций имеются две основные возможности: структура может быть возвращаемым функцией значением и структура может использоваться в параметрах функ ции. Кроме того, в обоих случаях могут использоваться указатели на объекты структурных типов. Итак, рассмотрим все перечисленные варианты. /* Определение структурного типа: */ struct person {char * name; int age; }; Функция может возвращать структуру как результат: /* Прототип функции: */ struct person f (int N) Функция может возвращать указатель на структуру: /* Прототип функции: */ struct person * ff (void); Параметром функции может быть структура: /* Прототип функции: */ void fff (struct person str); Параметром функции может быть указатель на объект структурного типа: /* Прототип функции: */ void ffff (struct person * pst); При вызове функции fff( ) выделяется память для параметра, то есть для вспомогательного объекта типа struct person. В этот объект переносится значение аргумента, заменяющего параметр - структуру str. Далее выполняются действия, предусмотренные операторами тела функции fff( ). Эти действия не могут изменять структуру, использованную в качестве аргумента. В случае, когда параметром служит указатель на объект структурного типа, действиями внутри тела функции ffff( ) можно изменить ту структуру вызывающей функции, которая адресуется аргументом pst. Имитация абстрактных типов данных. При решении конкретных прикладных задач бывает удобным выделить набор типов данных, наиболее полно соответствующих понятиям и объектам предметной области. Такие типы данных обычно отсутствуют в языке программирования, но их можно сконструировать и ввести как производные типы. Одновременно с такими специализированными данными (точнее, с производными типами для их представления) для каждого типа вводится набор операций, удобный для обработки этих данных. Тип данных и набор операций над объектами этого типа совместно определяют абстрактный тип данных, ориентированный на представление понятий предметной области решаемой задачи. В языке Си программист имеет возможность вводить абстрактные типы данных с помощью структур, отображающих собственно данные, и функций, определяющих операции над данными. В качестве примера введем абстрактный тип данных для рациональных дробей. Выше в §6.1 введен и поименован с помощью typedef как fraction структурный тип для представления рациональных дробей. Определим функции, реализующие операции над рациональными дробями: input( ) - ввод дроби; out( ) - вывод дроби на дисплей; add( ) - сложение; sub( ) - вычитание; mult( ) - умножение; divide( ) - деление. Этот набор операций можно легко расширить, введя, например, функции для приведения дробей к одному знаменателю, для сокращения дроби, для выделения целой части неправильной дроби и т. д. Но для демонстрации особенностей применения структур в функциях при определении абстрактного типа данных перечисленного набора операций вполне достаточно. Оформим определение структурного типа «рациональная дробь» и набор прототипов перечисленных выше функций для операций над дробями в виде следующего файла: /* Файл fract.h - абстрактный тип "рациональная дробь" */ typedef struct rational_fraction { int numerator; /* Числитель */ int denominator; /* Знаменатель */ } fraction; /* Обозначение структурного типа */ /* Прототипы функций для работы с дробями: */ void input (fraction * pd); void out (fraction dr); fraction add (fraction dr1, fraction dr2); void sub (fraction dr1, fraction dr2, fraction * pdr); fraction * mult (fraction dr1, fraction dr2); fraction divide (fraction *pd1, fraction *pd2); Обратите внимание, что применение typedef позволило упростить название структурного типа. Вместо конструкции struct ratio- nal_fraction в прототипах функций и далее в программе используется более короткое обозначение fraction. Определения функций, реализующих перечисленные операции над рациональными дробями, объединим в один файл следующего вида: /*Файл fract.c - определения функций для работы с дробями */ #include #include int N; printf("\n Числитель:"); scanf("%d", pd -> numerator); рг^ТС’Знаменатель:"); scanf("%d", &N); if (N == 0) { printf("\n Ошибка! Нулевой знаменатель!"); exit (0); /* Завершение программы */ } pd -> denominator=N; } /* Изобразить дробь на экране: */ void out (fraction dr) { printf("Рациональная дробь:"); printf("%d/%d", dr.numerator, dr.denominator); } /* Сложение дробей: */ fraction add (fraction dr1, fraction dr2) { fraction dr; dr.numerator = dr1.numerator * dr2.denominator + dr1.denominator * dr2.numerator; dr.denominator = dr1.denominator * dr2.denominator; return dr; } /* Вычитание дробей: */ void sub (fraction dr1, fraction dr2, fraction * pdr) { pdr -> numerator=dr1.numerator * dr2.numerator dr2.numerator * dr1.denominator; pdr -> denominator=dr1.denominator * dr2.denominator; } /* Умножение дробей: */ fraction * mult (fraction dr1, fraction dr2) { fraction * mul; mul=(fraction *) malloc (sizeof (fraction)); mul -> numerator=dr1.numerator * dr2.numerator; mul -> denominator=dr1.denominator * dr2.denominator; return mul; } /* Деление дробей: */ fraction divide (fraction * pd1, fraction * pd2) { fraction d; d.numerator = pd1 -> numerator * pd2 -> denominator; d.denominator = pd1 -> denominator * pd2 -> numerator; return d; } Приведенный набор функций демонстрирует все возможные сочетания возвращаемых значений и параметров функций при работе со структурами. Обратите внимание, что функции add( ), divide( ) возвращают структуру типа fraction, которую нужно «разместить» в соответствующей структуре вызывающей программы. Функция sub( ) предусматривает передачу результата через параметр-указатель fraction * pdr. Функция mult( ) формирует динамический объект типа fraction и возвращает его адрес. В основной программе после обращения к mult( ) корректно будет освободить динамическую память с помощью функции free( ). Следующая программа демонстрирует работу с рациональными дробями с помощью введенных функций: #include "fract.h" /* Файл прототипов */ #include "fract.c" /* Файл определений функций */ void main ( ) { fraction a, b, c; /* Определили три дроби-структуры */ fraction *p; /* Указатель на дробь */ printf("\n Введите дроби:"); input(&a); input(&b); c = add(a, b); /* Сложение дробей */ out(c); /* Вывод дроби */ p = mult(a, b); /* Умножение дробей */ out(*p); /* Вывод дроби, адресуемой указателем */ free(p); /* Освободить память */ c = divide(&a, &b); /* Деление дробей */ out (с); /* Вывод дроби */ } Результаты выполнения программы:
Обратите внимание на необходимость явного освобождения памяти после выполнения функции mult( ), внутри тела которой для результата (для структуры типа fraction) явно выделяется память, и указатель на этот участок памяти передается в вызывающую программу. После печати результата out(*p) библиотечная функция free(p) освобождает память. Динамические информационные структуры Статическое и динамическое представление данных. В языках программирования типы данных и определения переменных (как объектов заданных типов) нужны для того, чтобы определить требования к памяти для каждого из этих объектов и фиксировать диапазон (пределы) значений, которые могут принимать эти объекты (переменные). Для базовых типов (int, long, double и т. д.) требования к памяти и диапазоны возможных значений определяются реализацией языка (компилятора). Из базовых типов формируются производные, такие как массивы и структурные типы. Для производных типов требования к памяти заложены в их определениях. Таким образом, определив массив или структурный тип, программист зафиксировал требования к памяти в самом определении. Например, на основании определения double array[18] в памяти для массива array[ ] будет выделено не менее 18* sizeof (double) байт. С помощью определения struct mixture { int ii; long ll; char cc [8]; }; не только задается состав структурного типа с названием struct mixture, но и утверждается, что каждый объект структурного типа struct mixture потребует для размещения не менее sizeof (int) + sizeof (long) + 8* sizeof (char) байт. Точный объем позволяет определить операция sizeof (struct mixture). Вводимые таким образом объекты позволяют представить только статические данные. Однако во многих задачах требуется использовать более сложные данные, представление которых (конфигурация, размеры, состав) может изменяться в процессе выполнения программы. О таких изменяемых данных говорят, используя принятый в информатике термин динамические информационные структуры. Односвязный список. Наиболее простая динамическая информационная структура - это односвязный список, элементами (звеньями) которого служат объекты, например такого структурного типа: struct имя_структурного_типа { элементы_структуры; /* данные */ struct имя_структурного_типа * указатель; }; В каждую структуру такого типа входит указатель на объект того же типа, что и определяемая структура. Чтобы продемонстрировать некоторые особенности обработки простых динамических информационных структур, разработаем программу, в которой определим структурный тип для представления звеньев односвязного списка и решим такую простейшую задачу: «Ввести с клавиатуры произвольное количество структур, объединяя их в односвязный список, а затем вывести на экран дисплея содержимое введенного списка в порядке формирования его звеньев». Анализ динамических информационных структур удобнее всего выполнять с помощью их графического изображения. На рис. 6.3 приведены схема односвязного списка и обозначения, которые будут использованы в программе. Для работы со списком понадобятся три указателя: beg - на начало списка; end - на последний элемент, уже включенный в список; rex - указатель для «перебора» элементов списка от его начала. Начало списка beg ► | данные | указатель | I rex ► ... ... | данные | указатель | end »| данные | указатель | (последний элемент списка) Рис. 6.3. Односвязный динамический список Следующая программа решает сформулированную задачу. #include #include /* Определение структурного типа "Звено списка":*/ struct cell { char sign [10]; int weight; struct cell * pc; void main() { /* Указатель для перебора звеньев списка: */ struct cell * rex; struct cell * beg=NULL; /* Начало списка */ struct cell * end=NULL; /* Конец списка */ printf("\nBeegume значения структур:\n"); /* Цикл ввода и формирования списка */ do { /* Выделить память для очередного звена списка: */ rex=(struct cell *)malloc(sizeof(struct cell)); /* Ввести значения элементов звена: */ printf("sign="); scanf("%s",& rex->sign); printf("weight="); scanf("%d",& rex->weight); if (rex->weight == 0) { free(rex); break; /* Выход из цикла ввода списка */ } /* Включить звено в список: */ if (beg==NULL && end==NULL)/* Список пуст*/ beg=rex; else /* Включить звено в уже существующий список */ end->pc=rex; end=rex; end->pc=NULL; } while(1); /* Конец ввода списка */ /* Напечатать список */ printf( "^Содержимое списка:"); rex=beg; while (rex!=NULL) { printf("\nsign=%s\tweight=%d",rex->sign,rex->weight); rex=rex->pc; } } Пример выполнения программы: Введите данные o структурах: Sign=sigma weight=16 Sign=omega weight=44 Sign=alfa weight=0 Содержимое Sign=sigma Sign=omega списка: weight=16 weight=44 В программе ввод данных, то есть заполнение односвязного списка структур, выполняется в цикле. Условием окончания цикла служит нулевое значение, введенное для элемента int weight, очередной структуры. Формирование списка структур происходит динамически. Указатели beg, end инициализированы нулевыми значениями - вначале список пуст. Обратите внимание на преобразование типов (struct cell *) при выделении памяти для структуры типа struct cell. Функция mal- loc( ) независимо от типа параметров всегда возвращает указатель типа void *, а слева от знака присваивания находится указатель rex типа struct cell *. Используется явное приведение типов, хотя это не обязательно - тип void * совместим по операции присваивания с указателем на объект любого типа. Остальные особенности программы поясняются комментариями в ее тексте. Рекурсия при обработке списка. Даже такая простая структура, как динамический односвязный список, представляет собой конструкцию, которую удобно обрабатывать с помощью рекурсивных процедур. Рассмотрим ту же задачу об односвязном списке, но оформим формирование списка и вывод списка на экран дисплея в виде рекурсивных функций. Сделаем это с целью показать на этом простом примере особенности рекурсивной обработки динамических информационных структур. В каждом звене списка содержатся полезная информация (данные) и ссылка (адрес) на следующее звено списка. Если такая ссылка нулевая, то список пройден до конца. Для начала просмотра списка нужно знать только адрес его первого элемента. Рассмотрим, какие действия должны выполнять функция рекурсивного формирования списка и функция его рекурсивного просмотра с выводом данных о звеньях списка на экран дисплея. Функция рекурсивного формирования (заполнения) списка ниже в программе имеет прототип: struct cell * input (void); Она возвращает указатель на сформированный и заполненный данными с клавиатуры динамический список. Предполагается, что при каждом вызове этой функции формируется новый список. Функция заканчивает работу, если для очередного звена списка введено нулевое значение переменной weight. В противном случае заполненная с клавиатуры структура подключается к списку, а ее элементу struct cell * pc присваивается значение, которое вернет функция input( ), рекурсивно вызванная из тела функции. После этого функция возвращает адрес очередного звена списка, то есть значение указателя struct cell * pc. Прежде чем рассматривать текст функции input( ), помещенный в приведенной ниже программе, еще раз сформулируем использованные в ней конструктивные решения. При каждом обращении к этой функции в основной памяти формируется новый список, указатель на начало которого возвращает функция. Если для очередного звена списка вводится нулевое значение элемента weight, то звено не подключается к списку, процесс формирования списка заканчивается. Такой набор данных, введенных для элемента очередного звена, считается терминальным. Если нулевое значение элемента weight введено для первого звена списка, то функция input( ) возвращает значение NULL. Список определяется рекурсивно как первое (головное) звено, за которым следует присоединяемый к нему список. Текст функции input( ) на языке Си учитывает перечисленные конструктивные решения. Для структуры типа struct cell выделяется память. После того как пользователь введет значения данных, выполняется их анализ. В случае терминальных значений библиотечная функция free(p) освобождает память от ненужного звена списка, и выполняется оператор return NULL;. В противном случае элементу связи (указатель struct cell * p) присваивается результат рекурсивного вызова функции input( ). Далее все повторяется для нового экземпляра этой функции. Функция рекурсивного просмотра и печати списка имеет такой прототип: void output (struct cell * p); Исходными данными для ее работы служит адрес начала списка (или адрес любого его звена), передаваемый как значение указателя - параметра struct cell * p. Если параметр имеет значение NULL - список исчерпан, исполнение функции прекращается. В противном случае выводятся на экран значения элементов той структуры, которую адресует параметр, и функция output( ) рекурсивно вызывается с параметром p -> pc. Тем самым выполняется продвижение к следующему элементу списка. Конец известен - функция печатает «Список исчерпан!» и больше не вызывает сама себя, а завершает работу. Текст программы с рекурсивными функциями: /* Определение структурного типа "Звено списка":*/ struct cell { char sign[10]; int weight; struct cell * pc; }; #include #include /* Функция ввода и формирования списка: */ struct cell * input(void) { struct cell * p; p=(struct cell *)malloc(sizeof(struct cell)); printf("sign="); scanf("%s",& p->sign); printf("weight="); scanf("%d",& p->weight); if (p -> weight == 0) { free (p); return NULL; } p -> pc = input(); /* Рекурсивный вызов */ return p; } /* Функция "печати" списка: */ void output(struct cell *p) { if (p == NULL) { printf("\пСписок исчерпан!"); return; } printf("\nsign=%s\tweight=%d",p->sign,p->weight); output(p -> pc); /* Рекурсивный вызов */ } void main() { struct cell * beg=NULL; /* Начало списка */ printf("\nBeegume данные структур:\n"); beg=input(); /* Ввод списка. */ /*Напечатать список: */ printf ("^Содержимое списка:"); output(beg); }
Объединения и битовые поля Объединения. Со структурами в «близком родстве» находятся объединения, которые вводятся (описываются, определяются) с помощью служебного слова union. Объединение можно рассматривать как структуру, все элементы которой имеют нулевое смещение от ее начала. При таком размещении разные элементы занимают в памяти один и тот же участок. Тем самым объединения обеспечивают возможность доступа к одному и тому же участку памяти с помощью переменных (и/или массивов и структур) разного типа. Необходимость в такой возможности возникает, например, при выделении из внутреннего представления целого числа его отдельных байтов. Для иллюстрации сказанного введем такое объединение: union { char hh[2]; int ii; } CC; Здесь: union - служебное слово, вводящее тип данных «объединение» или объединяющий тип данных; CC - имя конкретного объединения; символьный массив char hh[2] и целая переменная int ii - элементы (компоненты) объединения. Схема размещения объединения CC в памяти ЭВМ приведена на рис. 6.4. Определение объединения: union {charhh[2]; int ii; } ее; (Массив символов)
(Целая переменная) Рис. 6.4. Схема размещения объединения в памяти Для обращения к элементу объединения используются те же конструкции, что и для обращения к элементу структуры: имя_объединения . имя_элемента ( * указатель_на_объединение ) . имя_элемента указатель_на_объединение -> имя_элемента Смысловое отличие объединения от структуры состоит в том, что записать информацию в объединение можно с помощью одного из его элементов, а выбрать данные из того же участка памяти можно с помощью другого элемента того же объединения. Например, оператор CC.ii = 15; записывает значение 15 в объединение, а с помощью конструкций CC.hh[0], CC.hh[1] можно получить отдельные байты внутреннего представления целого числа 15. Как и данные других типов, объединение - это конкретный объект, которому выделено место в памяти. Размеры участка памяти, выделяемого для объединения, должны быть достаточны для размещения самого большого элемента объединения. В нашем примере элементы int ii и char hh[2] имеют одинаковую длину, но это не обязательно. Основное свойство объединения состоит в том, что все его элементы размещаются от начала одного и того же участка памяти. А размеры участка памяти, отводимого для объединения, определяются размером самого большого из элементов. Например, для объединения union { double dd; float aa; int jj; } uu; размеры объекта-объединения uu равны размерам самого большого из элементов, то есть: sizeof(uu) == sizeof(double) Объединяющий тип. В рассмотренных примерах определены объединения, но явно не обозначены объединяющие типы. Именованный объединяющий тип вводится с помощью определения такого вида: union имя_объединяющего_типа { определения_элементов }; где union - спецификатор типа; имя_объединяющего_типа - выбираемый программистом идентификатор; определение_элементов - совокупность описаний объектов, каждый из которых служит прототипом одного из элементов объединений вводимого объединяющего типа. Конструкция union имя_объединяющего_типа играет роль имени типа, то есть с ее помощью можно вводить объединения-объекты. Как и для структурных типов, с помощью typedef можно вводить обозначения объединяющих типов, не требующие применения union: typedef union имя_объединяющего_типа { определения_элементов } обозначение_объединяющего_типа ; Пример: typedef union uni { double dou; int in[4]; char ch[8]; } uni_name; Имея такое определение, можно вводить конкретные объединения двумя способами: union uni snow, ray; uni_name yet, zz4; Объединения не относятся ни к скалярным данным, ни к данным агрегирующих типов. Битовые поля. Битовое поле может быть только элементом структуры или объединения и вне объектов этих типов не встречается. Битовые поля не имеют адресов и не могут объединяться в массивы. Назначение битовых полей - обеспечить удобный доступ к отдельным битам данных. Кроме того, с помощью битовых полей можно формировать объекты с длиной внутреннего представления, не кратной байту. Это позволяет плотно «упаковывать» информацию, что экономит память. Описание структуры с битовыми полями должно иметь такой формат: struct { тип_1 имя_поля_1 : ширина_поля_1; тип_2 имя_поля_2 : ширина_поля_2; ... } имя_структуры ; где mun_i - тип поля, который может быть только int, возможно, со спецификатором unsigned или signed; ширина_поля - целое неотрицательное десятичное число, значение которого зависит от реализации компилятора. Разрешается поле без имени (для чего указываются только двоеточие и ширина), с помощью которого в структуру вводятся неиспользуемые биты (промежуток между значимыми полями). Для обращения к полям используются те же конструкции, что и для обращения к обычным элементам структур: имя_структуры . имя_поля_ (* указатель_на_структуру) . имя_поля_ указатель_на_структуру -> имя_поля_ Например, для структуры xx с битовыми полями: struct { int a:10; int b:14; } xx; правомочны такие операторы: xx.a=1; xx.b=48; или xx.a=xx.b=0; От реализации зависит порядок размещения в памяти полей одной структуры. Поля могут размещаться как слева направо, так и справа налево. Для архитектуры Intel поля, которые размещены в начале описания структуры, имеют младшие адреса. Таким образом, для примера: struct { int x:5; int y:3; } hh; hh.x=4; hh.y=2; в одном байте формируется последовательность битов, приведенная на рис. 6.5. 76543210 Номера битов: Битовые поля: Рис. 6.5. Размещение битовых полей в байте Вместо служебного слова struct может употребляться union. В этом случае определяется объединение с битовыми полями. В качестве иллюстрации возможностей объединений и структур с битовыми полями рассмотрим следующую программу. В ней вводятся значения двух целых переменных m и n, и остатки от их деления на 16 заносятся соответственно в четыре младших и четыре старших разряда одного байта. Таким образом выполняется некоторая кодировка введенных значений переменных m и n. Затем печатается изображение содержимого сформированного байта. Особенности программы объяснены в комментариях и поясняются результатами выполнения. Обратите внимание на использование объединений. В функции cod( ) запись данных производится в элементы (битовые поля) структуры hh, входящей в объединение un, а результат выбирается из того же объединения un в виде одного байта. В функции binar( ) происходит обратное преобразование - в нее как значение параметра передается байт, содержимое которого побитово «расшифровывается» за счет обращения к отдельным полям структуры byte, входящей в объединение cod. Текст программы: /* Структуры, объединения и битовые поля */ #include { unsigned char k; int m,n; void binar(unsigned char); /* Прототип функции */ unsigned char cod(int,int); /* Прототип функции */ printf("\n m="); scanf("%d",&m); printf(" n="); scanf("%d",&n); k=cod(m,n); printf(" cod=%u",k); binar(k); } /* Упаковка в один байт остатков от деления на 16 двух целых чисел */ unsigned char cod(int a, int b) { union { unsigned char z; struct { unsigned int x :4; /* Младшие биты */ unsigned int y :4; /* Старшие биты */ } hh; } un; un.hh.x=a%16; un.hh.y=b%16; return un.z; } /* binar - двоичное представление байта */ void binar(unsigned char ch) { union { unsigned char ss; struct Содержание 3 ПРЕДИСЛОВИЕ 12 Глава 1 16 БАЗОВЫЕ ПОНЯТИЯ ЯЗЫКА 16 1.1.Алфавит, идентификаторы, служебные слова 17 1.2.Литералы 20 1.3.Переменные и именованные 27 константы 27 1.4. Операции 35 1.5. Разделители 44 1.6.Выражения 49 Контрольные вопросы 59 Глава 2 61 ВВЕДЕНИЕ 61 В ПРОГРАММИРОВАНИЕ НА СИ 61 2.1.Структура и компоненты простой программы 61 2.2.Элементарные средства 71 программирования 71 2.3. Операторы цикла 92 2.4. Массивы и вложение 108 операторов цикла 108 2.5.Функции 123 2.6.Переключатели 135 Контрольные вопросы 139 Глава 3 140 ПРЕПРОЦЕССОРНЫЕ СРЕДСТВА 140 3.1.Стадии и директивы препроцессорной обработки 141 HUB 143 3.2.Замены в тексте 145 3.3.Включение текстов из файлов 150 3.4.Условная компиляция 152 3.5.Макроподстановки средствами 157 препроцессора 157 3.6.Вспомогательные директивы 163 3.7.Встроенные макроимена 165 Контрольные вопросы 167 Глава 4 169 УКАЗАТЕЛИ, МАССИВЫ, СТРОКИ 169 4.1.Указатели на объекты 169 4.2.Указатели и массивы 178 4.3. Символьная информация и строки 193 Контрольные вопросы 201 Глава 5 204 ФУНКЦИИ 204 5.1. Общие сведения о функциях 204 5.2.Указатели в параметрах функций 209 5.3.Массивы и строки 214 как параметры функций 214 5.4.Указатели на функции 223 5.5.Функции с переменным 237 количеством аргументов 237 5.6.Рекурсивные функции 249 5.7.Классы памяти 252 и организация программ 252 5.8. Параметры функции main( ) 259 Контрольные вопросы 262 Глава 6 264 СТРУКТУРЫ И ОБЪЕДИНЕНИЯ 264 6.1.Структурные типы и структуры 264 6.2.Структуры, массивы и указатели 278 6.3.Структуры и функции 289 6.4.Динамические информационные структуры 293 6.5.Объединения и битовые поля 300 Контрольные вопросы 309 Глава 7 312 ВВОД И ВЫВОД 312 7.1. Потоковый ввод-вывод 312 7.2. Ввод-вывод нижнего уровня 349 Контрольные вопросы 359 Глава 8 360 ПОДГОТОВКА И ВЫПОЛНЕНИЕ 360 ПРОГРАММ 360 8.1.Схема подготовки программ 360 8.2.Подготовка программ 362 в операционной системе UNIX 362 8.3. Утилита make 364 8.4. Библиотеки объектных модулей 368 Контрольные вопросы 375 Приложение 1 376 ТАБЛИЦЫ КОДОВ ASCII 376 HUB 381 Приложение 2 384 Константы предельных значений 384 Приложение 3 386 Стандартная библиотека функций языка Си 386 Приложение 4 397 МОДЕЛИ ПРЕДСТАВЛЕНИЯ 397 ЧИСЕЛ НА РАЗЛИЧНЫХ 397 КОМПЬЮТЕРНЫХ ПЛАТФОРМАХ 397 Литература 400 Предметный указатель 401 printf("\n Значения битов: %d %d " "%d %d " %d %d %d %d", cod.byte.a7, cod.byte.a6, cod.byte.a5, cod.byte.a4, cod.byte.a3, cod.byte.a2, cod.byte.a1, cod.byte.a0); } Результаты выполнения программы на современном ПК: □ первый вариант:
□ второй вариант: m=0 Номера битов: 7 6 5 4 3 2 1 0 Значения битов: 0 0 0 1 0 0 0 0 Контрольные вопросы Дайте определение структуры. Что является спецификатором структурного типа? Вспомните формат определения конкретных структур. Назовите все схемы определения структур. Каким образом производится инициализация структур? Разрешается ли присваивание структур? Каким образом можно сравнить структуры? Как осуществляется доступ к элементам структуры? Что такое «уточненное имя»? Поясните смысл операции «точка». Можно ли операцию получения адреса & применить к уточненному имени? Допускается ли вложение структур? Как определить массив структур? Каким образом осуществляется доступ к компонентам структур, входящим в массив структур? Могут ли вводиться указатели на структуры для безымянных структурных типов? Поясните способы доступа к элементам структуры с помощью указателя. Какие операции допустимы над указателями на структуры? 18. Объясните применение в качестве элемента структуры указателя на структуру того же типа, что и определяемый структурный тип. 19. Перечислите варианты использования структур при вызове
Дайте определение объединения. Приведите определение для именованного объединяющего типа. Что такое битовые поля и в объектах каких типов их можно использовать? Каким образом можно получить доступ к любым разрядам внутреннего представления переменной? Можно ли ввести в битовое поле значение из входного потока? |