Официальный сайт студ.городка НГТУ
Список блогов » C++

#110.06.08 20:50Поля, структуры

C@pricorN
Сообщений: 0
Email Профиль Приват 

Поля, структуры

Пользовательские типы данных в С.

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

В С совсем немного возможностей по построению пользовательских типов данных. Точнее, сами возможности практически безграничны, но вот "базовых кирпичиков", из которых все строится, всего 4. Это структуры (structures), объединения, или союзы (unions), битовые поля (bit fields) и перечисления (enumerations). Причем последний тип, перечисление, в С не очень популярен - никаких дополнительных преимуществ, кроме улучшения читаемости программы, он не дает. А наиболее распространен первый тип - структуры. Давайте с него и начнем.

Структуры.

Представьте, что вам надо написать библиотеку для работы с комплексными числами. Ясно, что и результаты функций, и их параметры будут состоять из реальной и мнимой частей. Можно, конечно, для представления комплексного числа пользоваться массивом из двух double:
/* Complex number as array */
double cmplx[2]; /* [0] - real part, [1] - img */
Можно, но не очень удобно - хотя бы потому, что вместо массива комплексных чисел вам придется работать с двумерным массивом вещественных
/* Array of complex numbers-arrays */
double cmplxarr[10][2]; /* [][0] - real part. [][1] - img */
Поэтому в таких случаях гораздо удобнее построить свой тип данных, чтобы переменная такого типа вела себя как одно целое. Для этого надо определить структуру:
/* complex.h */

struct COMPLEX {
  double re;
  double im;
};
или то же самое, но короче
/* complex.h */

struct COMPLEX {
  double re, im;
};
Как видите, для определения структуры сначала ставят ключевое слово struct, затем название типа (это не название переменной, а именно название пользовательского типа), а затем в фигурных скобках определяют так называемые поля - таким же образом, как вне структуры вы бы определяли переменные. Так, в нашей структуре для комплексных чисел есть два поля типа double с именами re и im. Обычно, если программа состоит из нескольких файлов, то такое описание типа выносят в файл-заголовок (поэтому я и написал в комментарии complex.h). А потом включают его в те файлы, где таким типом хотят пользоваться.

Написав подобное определение, мы теперь можем пользоваться нашим новым типом данных - создавать переменные, указатели на него, массивы, и так далее. Только название нашего нового типа будет 'struct COMPLEX', а не COMPLEX. Вот примеры использования нового типа для создания различных объектов:
#include "complex.h"

struct COMPLEX number1, number2; /* два числа */
struct COMPLEX arr[10]; /* массив */
struct COMPLEX *ptr; /* указатель */
Кстати, если структура понадобилась только в одном файле, то можно даже совместить описание типа и создание переменных, вот так:
struct COMPLEX {
  double re, im;
} number1, *ptr;

struct COMPLEX number2;
Разумеется, проделывать такое в файле-заголовке не следует, иначе транслятор попытаться создать переменные с одинаковыми именами везде, куда вы этот заголовок включите.

Ограничений на типы используемых полей очень немного - это могут быть встроенные типы, массивы, указатели, другие структуры. Нельзя, правда, включить в структуру ее саму в качестве поля. А вот указатель на эту же структуру можно, и этим часто пользуются при описании элемента дерева, связного списка и т.п. Вот пример структуры с разными полями:
/* Структура - элемент односвязного списка */
struct LIST_ELM {

  char info[100]; /* поле-массив char */

  /* Поле-указатель на предыдущий элемент */
  struct LIST_ELM *prev;
};
Прямо при создании структур (имеются в виду переменные, а не само описание типа) их можно инициализировать - задавать полям начальные значения. Форма записи при этом следующая (на примере LIST_ELM):
struct LIST_ELM first = { "First item", NULL };

struct LIST_ELM second = { "Second item", &first };
Как видите, в фигурных скобках мы просто перечисляем инициализирующие значения подходящего типа. Разумеется, если бы поле само было структурой или массивом, то для его инициализации нам пришлось бы поставить вложенный список значений в фигурных скобках:
struct USELESS {
  char *p;
  int a[2];
  double d;
};

struct USELESS useless = { NULL, { 1,2 }, 3.14 };
Я показал вам, как определять структуры, создавать и инициализировать переменные такого типа. Осталось еще научиться с ними работать. Операторы для работы с создаваемыми пользователем типами - один из признаков объектно-ориентированного языка, и, когда мы доберемся до С++, мы научимся проделывать подобные вещи. Но пока мы стараемся держаться в рамках С, а в нем не предусмотрено операций для работы с пользовательскими типами данных, и относится это не только к структурам. Поэтому с каждым полем вам придется работать, как с самостоятельной переменной. Но прежде всего надо научиться получать доступ к нужному полю. Для этого в С есть два специальных оператора - . (точка) и -> (стрелочка, составленная из минуса и знака "больше"). Точка используется, когда у вас есть сама структура, а стрелочка - если вы работаете с указателем на структуру. Вот как это выглядит в программе (на примере определенного раньше типа COMPLEX):
#include "complex.h"

struct COMPLEX number, *ptr;

/* Работа со полями структуры - точка */
number.re = 1.0;
number.im = number.re;

/* Работа с полями через указатель - стрелочка */
ptr = &number;
ptr->re=1.0;
ptr->im=ptr->re;
Разумеется, во втором случае можно было бы использовать комбинацию звездочки (доступ через указатель) и точки
(*ptr).re=1.0;
(*ptr).im=(*ptr).re;
но в варианте со стрелочкой транслятору приходится разбираться не с двумя операторами, а с одним. А уж о читаемости программы и говорить не приходится, особенно если поля у вас сами являются указателями на структуры. Попробуйте переписать без стрелочки строку
p->next->q1->i=0;
и вы сразу это оцените.

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

Но все-таки структуры придуманы прежде всего для того, чтобы работать с разнородной информацией, как с одним целым. Пока все преимущества, которые мы видели - поля переменной сгруппированы в одном объекте и доступны через имя этой переменной. Что еще можно делать со структурой? Разумеется, передавать в функцию в качестве параметра и возвращать результат. Однако тут есть одна особенность - нельзя передавать в функцию и возвращать из нее саму структуру, нужно использовать ее адрес. Связана эта особенность с тем, что структура может нести в себе большой объем данных, так что при передаче по значению приходилось бы копировать все данные из оригинала в локальную копию (или из локальной копии в возвращаемое значение). И, чтобы избежать подобных накладных расходов и связанной с ними потери эффективности, создатели языка потребовали, чтобы в качестве параметров и возвращаемых значений использовались только адреса подобных объектов. Вот как это выглядит на практике:
#include <stdio.h>
#include "complex.h"

/* Заносим комплексное значение по указанному адресу */
void set_somplex(struct COMPLEX *n, double re, double im) {
  n->re = re;
  n->im = im;
}

/* Складываем два числа, возвращаем результат */
struct COMPLEX *add_complex(
    struct COMPLEX *n1,
    struct COMPLEX *n2
)
{
  static struct COMPLEX result;
  result.re = n1->re + n2->re;
  result.im = n1->im + n2->im;
  return &result;
}

main() {
  struct COMPLEX number1, number2, *ptr;

  set_complex( &number1, 1.0, 1.0);
  set_complex( &number2, 1,0, 0.0);

  ptr=add_complex(&number1, &number2);

  printf("%g %g\n", ptr->re, ptr->im);
}
Между прочим, попробуйте догадаться, почему я в функции add_complex для хранения результата использовал статическую структуру, а не автоматическую.

Битовые поля.

Вот, собственно, и все, что я собирался сказать о структурах. Перейдем теперь к битовым полям. Прежде, чем показать, как они выглядят, приведу характерный пример, где они требуются. Представьте себе, что вы пишете программу для работы с контроллером КАМАК. И в документации на контроллер написано что-нибудь в таком духе: "CSR (cтатусный регистр) доступен по адресу 0xD8000. Для генерации цикла КАМАК номер функции (0..31) нужно занести в биты 1-5 статусного регистра, а в бит 0 занести 1. После завершения цикла бит 0 будет очищен контроллером.". И вам надо выполнить 8 функцию КАМАК, а затем дождаться завершения цикла. Разумеется, все это можно проделать с помощью операций битовой арифметики:
volatile char *csr = (char*)0xD8000;

/* Заслать в биты 1..5 функцию 8, взвести бит 0 */
*csr = (8<<1)|1

/* Дождаться, пока контроллер очистит бит 0 */
while ( (*csr & 1) != 0) ;
Такое, конечно, сработает. Но писать долго, читать непонятно. Неплохо бы иметь возможность работать с группами битов как с переменными. Вот именно для этого и предусмотрены битовые поля.

Само по себе определение битового поля очень похоже на определение структуры, только после каждого поля указывается его размер в битах. И в качестве типа поля указывать можно только int, signed или unsigned. Вот как мы могли бы описать статусный регистр из примера выше:
struct CSR {
  unsigned busy : 1; /* Бит, запускающий цикл */
  unsigned f : 5 ; /* 5 битов под функцию */
  int unused: 2; /* Три неиспользуемых старших бита */
};
Теперь, создав переменную такого типа, мы будем иметь в ней три "маленьких" целых числа - unused с длиной 3 бита и диапазоном -4..3, беззнаковое f длиной 5 бит и диапазоном 0..31, и беззнаковое busy из одного бита, то есть, с диапазоном 0..1. Причем все эти переменные окажутся упакованными в один байт. И теперь мы можем проделать со статусным регистром требуемую работу в гораздо более понятной форме:
volatile struct CSR *mycsr = (struct CSR *)0xD8000;

/* Заслать в биты 1..5 функцию 8 */
csr->f = 8;

/* взвести бит 0 */
csr->busy = 1;

/* Дождаться, пока контроллер очистит бит 0 */
while ( csr->busy ) ;
Можно еще более усовершенствовать определение нашего битового поля - не давать имя первой, неиспользуемой, группе битов, благо С это позволяет:
struct CSR {
  unsigned busy : 1; /* Бит, запускающий цикл */
  unsigned f : 5 ; /* 5 битов под функцию */
  int : 2; /* Три неиспользуемых старших бита */
};
При этом 3 бита под безымянное поле все равно будут выделены, так что нужные нам поля f и busy окажутся на прежних местах.

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

Объединения.

Третий из определяемых пользователем типов данных - это объединение, или союз. Определяется этот тип с помощью другого ключевого слова - union, например, так.
union DOUBLE_UCHAR8 {
  double d;
  unsigned char uc[8];
};
Как видите у союза тоже есть поля. Вот только ведут себя эти поля совсем иначе, не так, как в структуре. Дело в том, что все поля союза располагаются по одному адресу. Помните, я рассказывал на одном из занятий о том, как неприятно бывает, когда вы подсовываете функции 4 байта int, а транслятор по незнанию берет 8 - четыре ваших, и еще 4 - мусора, и использует их как double? Так вот, союз - это законный способ проделать то же самое, когда вы этого действительно хотите. Во многих книгах пишут, что союз может держать значение только в одном поле, На мой взгляд, правильнее и понятнее говорить, что союз через разные поля дает доступ к одному и тому же содержимому. Просто программа интерпретирует это содержимое по разному. Например, используя тот союз, что мы написали выше, можно посмотреть на внутреннее представление числа типа double (на байтовую последовательность, которую представляет собой данное число):
int i;
union DOUBLE_UCHAR8 v;

/* Засылаем значение в поле типа double */
v.d = 2.71828;

/* Смотрим, как оно выглядит в виде цепочки байтов */
for (i=0; i<8; i++)
  printf("uc[%d]= %d\n", i, v.uc[i] );
Союз тоже можно инициализировать прямо при создании. Правда, только значением для первого поля:
union DOUBLE_UCHAR8 u = { 3.14 };
Это, впрочем, вполне естественно - память под полями одна и та же, так что инициализатор для второго поля испортил бы нам первое значение.

Союзы прежде всего полезны там, где у вас по смыслу в одной переменной могут храниться разные типы данных. Например, в качестве карточки библиотечного каталога - либо номер (ISBN), либо структура с такими полями, как автор, название книги и т.п. И в отличие от битовых полей союзы можно и нужно использовать в подобных случаях для экономии памяти.

Перечисления.

И, наконец, последний тип - перечисление (enumeration). По-моему, наименее популярный - это можно понять даже по соотношению частоты определения перечислений и структур в той же стандартной библиотеке С. Этот тип позволяет создавать синонимы для последовательности целых чисел, а затем эти синонимы использовать в качестве символических констант. Или использовать сам этот тип в качестве параметра функции. Например, мы можем написать
enum BOOLEAN {
FALSE,
TRUE
};
А потом использовать вместо 0 и 1 имена FALSE и TRUE. Или использовать имя нашего типа для задания параметра:
void f(enum BOOLEAN flag) {

  if (flag==FALSE)
    printf("flag==FALSE\n");
  else
    printf("flag==TRUE\n");
}

main() {
  f(FALSE);
  f(TRUE);
}
Удобно, конечно, и программа лучше читается. Вот только С не любит напрягаться на предмет проверки типов. Так что с таким же успехом мы могли бы в main написать f(25), и даже предупреждения от транслятора не увидели бы. Однако все-таки немного расскажу вам про него.

Если вы просто перечисляете имена элементов в enum, то первое имя он сделает синонимом нуля, второе - единицы, и так далее в порядке возрастания. Именно поэтому с таким enum, как мы написали выше, FALSE стало синонимом 0, а TRUE - синонимом 1. Однако можно заставить транслятор начать нумерацию с любого числа:
enum COLOR {
  RED = 1, /* Просим начать нумерацию с 1 */
  GREEN, /* GREEN == 2 */
  BLUE /* BLUE == 3 */
};
Проделывать такое можно не один раз. При этом можно создавать и по несколько синонимов для одного и того же числа. И можно оставлять "пустые места" в последовательности чисел:
enum PIXELCOLOR {

  black, /* black == 0 */
  background = 0, /* Тоже 0 */
  red, /* red == 1 */
  green, /* green == 2 */
      /* пропускаем значение 3 */
  blue = 4,
      /* пропускаем 5,6 */
  white=7
};
Вот, собственно, и все, что я хотел сказать про перечисления.

Оператор typedef.

Думаю, что пришло время рассказать вам про оператор typedef. Этот оператор служит для создания синонимов для имени типа. Применять его можно с любыми типами, но особенно он помогает при работе со структурными типами и со сложными в понимании типами указателей. Пользоваться им очень просто. Представьте, что вы создаете переменную или несколько переменных в одном операторе. А теперь добавьте вначале ключевое слово typedef, и вы получите не переменные, а имена-синонимы для соответсвующих типов:
typedef int MY_INT;

struct COMPLEX { int re, im };
typedef struct COMPLEX COMPLEX_t, *COMPLEX_ptr;
Теперь вы можете создавать переменные с использованием этих новых имен:
MY_INT i = 1;

COMPLEX_t n = { 1.0, 0.0};
COMPLEX_ptr ptr = &n;
Специально подчеркну - это не новые типы данных, это синонимы уже имеющихся. Для транслятора исходные, "настоящие" имена типов
int
struct COMPLEX
struct COMPLEX *
теперь ничем не отличаются от
MY_INT
COMPLEX_t
COMPLEX_ptr
Ну вот, а теперь давайте разбираться с динамической памятью.

Offline

ФутЕр:)

© Hostel Web Group, 2002-2025.   Сообщить об ошибке

Сгенерировано за 0.272 сек.
Выполнено 10 запросов.