Pers.narod.ru. Обучение. Лекции по Си. Глава 9

9. Составные типы данных

 

В этом разделе рассматриваются модифицируемые, перечислимые и структурные типы данных языка Си.

 

9.1. Имена с модификаторами

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

Модификаторы cdecl, pascal, interrupt воздействуют на идентификатор и могут  быть записаны непосредственно рядом c ним:

<модификатор> тип список;

тип <модификатор> список;

В ряде случаев допускаются оба варианта.

Модификаторы const, volatile, near, far, huge воздействуют либо на идентификатор, либо на указатель, расположенный непосредственно справа. Если справа расположен идентификатор, модифицируется тип именуемого объекта. Если справа расположен признак указателя "*", то преобразуется тип объекта, адресуемого указателем на модифицированный тип. Таким образом, конструкция "модификатор *" обозначает указатель на модифицированный тип.

Например, int const *t; - это указатель на const int, который можно переназначать, тогда как int * const t; - это const указатель на int и переназначить его нельзя.

Указание const не допускает действий по изменению значения переменной. Приведем пример.

char *const str = "Строка текста";

str = "Другая строка";

Последний оператор недопустим, т. к. пытается переназначить const указатель. Тем не менее, допустим оператор

strcpy(str,"Строка");

Последнее было бы невозможно для указателя вида

char const *str = "Строка текста";

т. к. он указывает на неизменяемую строку.

Модификатор volatile несет противоположный смысл. Он подчеркивает, что значение переменной может быть изменено не только исполняемой программой, но и некоторым внешним воздействием, например, программой обработки прерываний или обменом с внешним устройством. Фактически, объявление volatile "предупреждает" компилятор, что модифицируемая переменная может измениться в любой момент и не следует делать предположений относительно стабильности значения соответствующего объекта в памяти. Для выражений, содержащих объекты volatile, не будут применяться методы оптимизации кода, такие объекты не будет загружаться в регистры процессора.

Модификатор pascal в применении к идентификатору означает, что он преобразуется к верхнему регистру и к нему не добавляется символ подчеркивания. В применении к имени функции модификатор оказывает влияние также на передачу аргументов. В этом случае аргументы пересылаются в стек  в прямом порядке , т. е., первым отправляется первый аргумент. По умолчанию в Си принято пересылать аргументы  в обратном порядке. Существует также  настройка компиляции, которая присваивает всем функциям и указателям на функции тип pascal. Такие функции могут вызываться из программы на Паскале. При этом может потребоваться, чтобы некоторые функции и указатели на функции применяли вызывающую последовательность, принятую в Си, а их имена имели принятый для идентификаторов Си вид. В этом случае соответствующие объявления делаются с модификатором cdecl. Все функции в стандартных включаемых файлах объявлены с модификатором cdecl.

Модификатор interrupt предназначен для объявления функций‑обработчиков прерываний. Для такой функции генерируется дополнительный код для сохранения и восстановления состояния регистров процессора. Модификатор не может использоваться совместно с near, far или huge. Приведем пример использования volatile и interrupt.

volatile int t;

void interrupt time() { t++; }

wait(int i) {

 t=0;

 while ( t<i);

}

Без объявления t как volatile оптимизирующий компилятор мог бы вынести за пределы цикла сравнение t и i, поскольку обе они не меняются в теле цикла. Это привело бы к зацикливанию.

Модификаторы near, far, huge оказывают действие на работу с адресами объектов. Формат указателей, принятый в данный момент по умолчанию,  определяется используемой моделью памяти (см. п. 11). Однако можно объявить указатель с форматом, отличным от действующего по умолчанию. Это делается с помощью явного указания ключевого слова near, far или huge:

char far *p = 0xB8000000ul;

 //"длинный" указатель сегмент+смещение

Подробнее действие этих модификаторов раскрыто в п. 11.

Допускается более одного модификатора для одного объекта, например

int far* pascal function();

 

9.2. Перечислимый тип данных

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

Практически в любом контексте перечислимый тип интерпретируется как int; первый элемент списка перечисления по умолчанию равен 0.

В примере ниже описывается тип day для перечисления дней недели. При описании переменной типа сразу же создается переменная workday.

enum day { SATURDAY, SUNDAY=0, MONDAY,

TUESDAY, WEDNESDAY, THURSDAY, FRIDAY }

workday;

workday = WEDNESDAY; //SATURDAY также =0!

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

typedef enum season {

 spring, summer, autumn, winter };

season now=spring;

for (int i=0; i<4; i++)

printf ("\n%d", now++); //0,1,2,3

В последнем примере значение item3 установлено равным 0, однако при увеличении переменной myitem в цикле она последовательно примет значения от 0 до 3.

#include <stdio.h>

typedef enum item {

 item1, item2, item3=0, item4 };

void main () {

 item myitem=item1;

 for (int i=0; i<4; i++)

  printf ("\n%d",myitem++); //0,1,2,3

 myitem=item1; printf ("\n%d",myitem); //0

 myitem=item2; printf ("\n%d",myitem); //1

 myitem=item3; printf ("\n%d",myitem); //0

 myitem=item4; printf ("\n%d",myitem); //1

}

 

9.3. Структуры

Структура - основной составной тип данных. В отличие от массива, она объединяет в одном объекте совокупность значений, которые могут иметь различные типы.

В Си реализован ограниченный набор операций над структурами как единым целым: передача структуры в качестве аргумента функции, возврат структуры из функции, получение ее адреса и создание указателя на структуру. Можно присваивать одну структуру другой, если они имеют одинаковый тег (структурный тип).

Общий синтаксис описания структуры следующий:

struct тег {

 список объявлений элементов;

} список описателей;

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

struct book {

 char title [80];

 char author [40];

 float cost;

};

struct book mybook;

Рекомендуется в описании структуры использовать ключевое слово typedef:

typedef struct point {

 double x,y;

};

В этом случае можно не писать ключевое слово struct в описании структур вновь созданного типа или при передаче их функциям. Обращение к полю структуры выполняется с помощью первичной операции "." (точка).

void main () {

 point a,b;

 a.x=a.y=0;

 b=a;

 printf ("\na=(%lf,%lf)",a.x,a.y);

 printf ("\nb=(%lf,%lf)",b.x,b.y);

}

Альтернативный вид программы мог бы быть следующим:

struct point {

 //'typedef' здесь был бы ошибкой!

 double x,y;

} a,b;

void main () {

 a.x=a.y=0;

 b=a;

 //...

}

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

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

Элементы структуры запоминаются в памяти последовательно в том порядке, в котором они объявляются: первому элементу соответствует меньший адрес памяти, а последнему - больший.

Каждый элемент в памяти может быть выровнен на границу слова, соответствующую его типу. Выравниванием управляет  опция Word Alignment в компиляторах. Для процессоров на базе Intel 8086/8088 выравнивание означает, что любой тип выравнивается на четную границу адресации при размере машинного слова 2 байта.

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

#include <stdio.h>

typedef struct texel_struct {

unsigned char ch;

unsigned char attr;

} texel;

typedef texel screen_array [25][80];

screen_array far *screen_ptr =

 (screen_array far *)0xB8000000L;

#define screen (*screen_ptr)

void fillscr

 (int l,int t,int r,int b,int c,int a) {

int i,j;

for (i=t; i<=b; i++)

 for (j=l; j<=r; j++) {

  screen[i][j].ch=c; screen[i][j].attr=a;

}

}

void main () {

fillscr(0,0,79,24,'*',128);

getchar();

}

Для доступа к полям структуры через указатель вместо конструкции

(*ptr).field

необходимой из‑за того, что приоритет операции "." выше, чем у унарной "*", используется сокращенная запись

ptr->field

Оба способа обращения компилируются одинаково, но второй более компактен и удобен.

Работу с указателем на структуру проиллюстрируем следующим примером, в котором формируется и выводится массив из 3 переменных структурного типа.

#include <stdio.h>

#include <string.h>

#include <alloc.h>

#include <stdlib.h>

struct test {

char *name; //Фамилия или имя

int ball; //Средний балл

};

struct test *tests;

 //Указатель на массив структур

void check (struct test *);

void main () {

const int n=3;

tests = (struct test *) malloc

  (n * sizeof (struct test));

 //Выделение памяти под массив структур

char buf[80];

for (int i=0; i<n; i++) {

  puts ("Имя? "); fgets (buf,78,stdin);

  tests[i].name =

   (char *) malloc (strlen(buf));

   //Выделение памяти под одну фамилию

  buf[strlen(buf)-1]='\0';

   //Удаляем '\n', который fgets

   //оставит в конце строк

  strcpy (tests[i].name, buf);

  puts ("Балл? "); fgets (buf,5,stdin);

  tests[i].ball = atoi (buf);

  check (&tests[i]);

}

for (i=0; i<n; i++)

  printf ("\n%s,%d",

   tests[i].name,tests[i].ball);

}

void check (struct test *p) {

if (p->ball < 0 ) p->ball = 0;

if (strlen (p->name)<1) {

  p->name = (char *) malloc (6);

  strcpy (p->name,"NoName");

}

}

Использование указателей на структурные типы в сочетании с указателями на функции обеспечивает мощь и гибкость языка Си в инкапсуляции данных даже без применения появившихся в Си++ классов (см. п. 13). Следующий пример представляет собой "заготовку" для несложной системы динамических меню.

#include <conio.h>

#include <stdlib.h>

typedef unsigned char byte;

typedef void (*FUN)(void);

 //Указатель на функцию обработки

 //пункта меню

struct MENU {

int x,y; // Позиция на экране пункта меню

byte *str; // Строка текста меню

FUN sf; // Указатель на функцию

         // обработки пункта

};

void Exit () { exit (0); }

void Start () { /* код функции

 обработки пункта меню */ }

void DrawMenu (MENU *m) {

gotoxy(m->x,m->y);

cprintf ("%s",m->str);

}

#define ITEMS 2

void main () {

MENU Menu[ITEMS]={

  { 1, 1, "Начать", Start },

  {10, 1, "Выход", Exit }

};

clrscr ();

for (int i=0; i<ITEMS; i++)

  DrawMenu (&Menu[i]);

}

 

9.4. Битовые поля структур используются обычно в двух целях:

·       для экономии памяти, поскольку позволяют плотно упаковать значения не по границам байтов;

·       для организации удобного доступа к регистрам внешних устройств, в которых различные биты могут иметь самостоятельное функциональное назначение, например, при доступе к элементам файловых таблиц FAT.

Объявление битового поля имеет следующий синтаксис:

тип идентификатор: константное_выражение;

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

Идентификатор необязателен. Неименованное битовое поле означает пропуск указанного числа битов перед размещением следующего элемента структуры. Неименованное битовое поле, для которого указан размер 0, имеет специальное назначение: оно гарантирует, что память для следующей переменной в этой структуре будет начинаться на границе машинного слова (int). Это относится и к следующему битовому полю.

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

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

struct {

 unsigned background: 8;

 unsigned color: 4;

 unsigned underline: 1;

 unsigned blink: 1;

} screen [25][80];

 

9.5. Объединение позволяет в разные моменты времени хранить в одном объекте значения различного типа. При объявлении объединения для него описывается набор типов значений, которые могут с ним ассоциироваться. В каждый момент времени объединение интерпретируется как значение только одного типа из набора. Контроль над тем, какого типа значение хранится в данный момент в объединении, возлагается на программиста. Синтаксис объявления объединения имеет следующий вид:

union тег {

 список_объявлений_элементов;

} список_описателей;

Память, которая выделяется для объединения, определяется размером наиболее длинного из его элементов. Все элементы объединения размещаются с одного и того же адреса памяти. Значение текущего элемента объединения теряется, когда другому элементу присваивается значение. В качестве примера приведем объединение, которое можно интерпретировать как знаковое или беззнаковое целое число.

union sign {

 int svar;

 unsigned uvar;

} number;

sign number2;

Во втором примере показано объединение, которое можно использовать для получения либо полного двухбайтового кода нажатой клавиши, либо отдельно скан‑ и ASCII‑кодов.

union key {

 char k[2];

 unsigned kod;

};

Элементами объединений могут быть и указатели. Например, в приведенном далее коде с помощью 4‑байтового указателя "сегмент‑смещение" отслеживается нажатие клавиш Ctrl, Shift или Alt через BIOS.

#include <stdio.h>

#include <conio.h>

struct FAR_PTR {

unsigned off, seg;

};

union MK_FAR {

unsigned char far *ptr;

struct FAR_PTR mk;

} work;

void main () {

work.mk.off = 0x17;

work.mk.seg = 0x40;

  //сегмент и смещение в BIOS,

  //где хранится флаг нажатия клавиш

while (!kbhit ()) {

  int shift = *(work.ptr);

  cprintf ("\r%02d",shift);

}

}

 

 

Рейтинг@Mail.ru
вверх гостевая; E-mail
Hosted by uCoz