Главная страница

Курс на Си. Подбельский. Курс программирования на Си. В., Фомин С. С. Курс программирования на языке Си Учебник


Скачать 1.57 Mb.
НазваниеВ., Фомин С. С. Курс программирования на языке Си Учебник
АнкорКурс на Си
Дата18.02.2023
Размер1.57 Mb.
Формат файлаdocx
Имя файлаПодбельский. Курс программирования на Си.docx
ТипУчебник
#943863
страница21 из 42
1   ...   17   18   19   20   21   22   23   24   ...   42

Указатели на структуры. Эти указатели определяются, как и ука­затели на данные других типов. Пример:

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.

    1. Структуры и функции

Для «взаимоотношения» структур и функций имеются две основ­ные возможности: структура может быть возвращаемым функцией значением и структура может использоваться в параметрах функ­

ции. Кроме того, в обоих случаях могут использоваться указатели на объекты структурных типов. Итак, рассмотрим все перечисленные варианты.

/* Определение структурного типа: */ 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 структурный тип для представления рацио­нальных дробей.

Определим функции, реализующие операции над рациональными дробями:

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

Оформим определение структурного типа «рациональная дробь» и набор прототипов перечисленных выше функций для операций над дробями в виде следующего файла:

/* Файл 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 /* Для exit() и malloc() */ void input (fraction * pd) /* Ввод дроби */ {

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 (с); /* Вывод дроби */

}

Результаты выполнения программы:

Введите дроби:

Числитель:

5



Знаменатель:

4



Числитель:

2



Знаменатель:

3



Рациональная

дробь:

23/12

Рациональная

дробь:

10/12

Рациональная

дробь:

15/8

Обратите внимание на необходимость явного освобождения па­мяти после выполнения функции mult( ), внутри тела которой для результата (для структуры типа fraction) явно выделяется память, и указатель на этот участок памяти передается в вызывающую про­грамму. После печати результата out(*p) библиотечная функция free(p) освобождает память.

    1. Динамические информационные структуры

Статическое и динамическое представление данных. В язы­ках программирования типы данных и определения переменных (как объектов заданных типов) нужны для того, чтобы опреде­лить требования к памяти для каждого из этих объектов и фик­сировать диапазон (пределы) значений, которые могут принимать эти объекты (переменные). Для базовых типов (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 данные I указатель 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( ), помещенный в приведенной ниже программе, еще раз сформулируем использо­ванные в ней конструктивные решения.

  1. При каждом обращении к этой функции в основной памяти формируется новый список, указатель на начало которого воз­вращает функция.

  2. Если для очередного звена списка вводится нулевое значение элемента weight, то звено не подключается к списку, процесс формирования списка заканчивается. Такой набор данных, введенных для элемента очередного звена, считается терми­нальным.

  3. Если нулевое значение элемента weight введено для первого звена списка, то функция input( ) возвращает значение NULL.

  4. Список определяется рекурсивно как первое (головное) звено, за которым следует присоединяемый к нему список.

Текст функции 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);

}

Возможный результат выполнения программы:

Введите данные структур:

Sign=Zoro



weight=1938



Sign=Zodiac



weight=1812



Sign=0



weight=0



Содержимое

списка:

Sign=Zoro

weight=1938

Sign=Zodiac

weight=1812

Список исчерпан!



    1. Объединения и битовые поля

Объединения. Со структурами в «близком родстве» находятся объединения, которые вводятся (описываются, определяются) с по­мощью служебного слова union.

Объединение можно рассматривать как структуру, все элементы которой имеют нулевое смещение от ее начала. При таком размеще­нии разные элементы занимают в памяти один и тот же участок. Тем самым объединения обеспечивают возможность доступа к одному и тому же участку памяти с помощью переменных (и/или масси­вов и структур) разного типа. Необходимость в такой возможности возникает, например, при выделении из внутреннего представления целого числа его отдельных байтов. Для иллюстрации сказанного введем такое объединение:

union {

char hh[2];

int ii;

} CC;

Здесь:

  • union - служебное слово, вводящее тип данных «объединение» или объединяющий тип данных;

  • CC - имя конкретного объединения;

  • символьный массив char hh[2] и целая переменная int ii - эле­менты (компоненты) объединения.

Схема размещения объединения CC в памяти ЭВМ приведена на рис. 6.4.

Определение объединения:

union {charhh[2]; int ii; } ее;

(Массив символов)

Участок памяти, выделенный объединению СС:

*— hh[0]—►

—hh[l]—*-

<— Байт—►

■*— Байт —►

< 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 void main (void)

{

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=1



n=3

cod=49

Номера



битов:

7

6

5

4

3

2

1

0

Значения битов:

0

0

1

1

0

0

0

1

□ второй вариант:

m=0 n=1 cod=16

Номера битов: 7 6 5 4 3 2 1 0

Значения битов: 0 0 0 1 0 0 0 0

Контрольные вопросы

  1. Дайте определение структуры.

  2. Что является спецификатором структурного типа?

  3. Вспомните формат определения конкретных структур.

  4. Назовите все схемы определения структур.

  5. Каким образом производится инициализация структур?

  6. Разрешается ли присваивание структур?

  7. Каким образом можно сравнить структуры?

  8. Как осуществляется доступ к элементам структуры?

  9. Что такое «уточненное имя»?

  10. Поясните смысл операции «точка».

  11. Можно ли операцию получения адреса & применить к уточнен­ному имени?

  12. Допускается ли вложение структур?

  13. Как определить массив структур?

  14. Каким образом осуществляется доступ к компонентам структур, входящим в массив структур?

  15. Могут ли вводиться указатели на структуры для безымянных структурных типов?

  16. Поясните способы доступа к элементам структуры с помощью указателя.

  17. Какие операции допустимы над указателями на структуры?

18. Объясните применение в качестве элемента структуры указате­ля на структуру того же типа, что и определяемый структурный тип.

19. Перечислите варианты использования структур при вызове

функции.

  1. Каким образом в языке Си можно вводить данных?

  2. Поясните способ создания динамических структур.

абстрактные типы информационных

  1. Дайте определение объединения.

  2. Приведите определение для именованного объединяющего типа.

  3. Что такое битовые поля и в объектах каких типов их можно использовать?

  4. Каким образом можно получить доступ к любым разрядам внут­реннего представления переменной?

  5. Можно ли ввести в битовое поле значение из входного потока?

1   ...   17   18   19   20   21   22   23   24   ...   42


написать администратору сайта