Книга: Linux программирование в примерах

15.4.1.5. По возможности избегайте объединений

15.4.1.5. По возможности избегайте объединений

«Не бывает бесплатных обедов»

- Lazarus Long -

union С относительно эзотерическая возможность. Она помогает экономить память, сохраняя различные элементы в одном и том же физическом пространстве; как программа интерпретирует его, зависит от способа доступа:

/* ch15-union.c --- краткая демонстрация использования union. */
#include <stdio.h>
int main(void) {
 union i_f {
  int i;
  float f;
 } u;
 u.f = 12.34; /* Присвоить значение с плавающей точкой */
 printf("%f also looks like %#xn", u.f, u.i};
 exit(0);
}

Вот что происходит, когда программа запускается на системе Intel x86 GNU/Linux:

$ ch15-union
12.340000 also looks like 0x414570a4

Программа выводит битовый паттерн, который представляет число с плавающей точкой в виде шестнадцатеричного целого. Оба поля занимают одно и то же место в памяти; разница в том, как этот участок памяти интерпретируется: u.f действует, как число с плавающей точкой, тогда как эти же биты в u.i действуют, как целое число.

Объединения особенно полезны в компиляторах и интерпретаторах, которые часто создают древовидные структуры, представляющие структуру файла с исходным кодом (которая называется деревом грамматического разбора (parse tree)). Это моделирует то, как формально описаны языки программирования: операторы if, операторы while, операторы присваивания и так далее для всех экземпляров более общего типа «оператора». Таким образом, в компиляторе могло бы быть нечто подобное этому:

struct if_stmt { ... }; /* Структура для оператора IF */
struct while_stmt { ... }; /* Структура для оператора WHILE */
struct for_stmt { ... }; /* Структура для оператора */
/* ...структуры для других типов операторов... */
typedef enum stmt_type {
 IF, WHILE, FOR, ...
} TYPE; /* Что у нас есть в действительности */
/* Здесь содержатся тип и объединения отдельных видов операторов. */
struct statement {
 TYPE type;
 union stmt {
  struct if_stmt if_st;
  struct while_stmt while_st;
  struct for_stmt for_st;
  ...
 } u;
};

Вместе с объединением удобно использовать макрос, который представляет компоненты объединения, как если бы они были полями структуры. Например:

#define if_s u.if_st /* Так что можно использовать s->if_s вместо s->u.if_st */
#define while_s u.while_st /* И так далее... */
#define for_s u.for_st
...

На только что представленном уровне это кажется разумным и выглядит осуществимым. В действительности, однако, все сложнее, и в реальных компиляторах и интерпретаторах часто есть несколько уровней вложенных структур и объединений. Сюда относится и gawk, в котором определение NODE, значение его флагов и макросов для доступа к компонентам объединения занимают свыше 120 строк![171] Здесь достаточно определений, чтобы дать вам представление о том, что происходит:

typedef struct exp_node {
 union {
  struct {
   union {
    struct exp_node *lptr;
    char *param_name;
    long ll;
   } l;
   union {
    ...
   } r;
   union {
    ...
   } x;
   char *name;
   short number;
   unsigned long reflags;
   ...
  } nodep;
  struct {
   AWKNUM fltnum;
   char *sp;
   size_t slen;
   long sref;
   int idx;
  } val;
  struct {
   struct exp_node *next;
   char *name;
   size_t length;
   struct exp_node *value;
   long ref;
  } hash;
#define hnext sub.hash.next
#define hname sub.hash.name
#define hlength sub.hash.length
#define hvalue sub.hash.value
  ...
 } sub;
 NODETYPE type;
 unsigned short flags;
 ...
} NODE;
#define vname sub.nodep.name
#define exec_count sub.nodep.reflags
#define lnode sub.nodep.l.lptr
#define nextp sub.nodep.l.lptr
#define source_file sub.nodep.name
#define source_line sub.nodep.number
#define param_cnt sub.nodep.number
#define param sub.nodep.l.param_name
#define stptr sub.val.sp
#define stlen sub.val.slen
#define stref sub.val.sref
#define stfmt sub.val.idx
#define var_value lnode
...

В NODE есть объединение внутри структуры внутри объединения внутри структуры! (Ой.) Поверх всего этого многочисленные «поля» макросов соответствуют одним и тем же компонентам struct/union в зависимости от того, что на самом деле хранится в NODE! (Снова ой.)

Преимуществом такой сложности является то, что код С сравнительно ясный. Нечто вроде 'NF_node->var_value->slen' читать просто.

У такой гибкости, которую предоставляют объединения, конечно, есть своя цена. Когда отладчик находится глубоко во внутренностях вашего кода, вы не можете использовать симпатичные макросы, которые имеются в исходном коде. Вы должны использовать развернутое значение.[172] (А для этого придется найти в заголовочном файле соответствующее определение.)

Например, сравните 'NF_node->var_value->slen' с развернутой формой: 'NF_node->sub.nodep.l.lptr->sub.val.slen'! Чтобы увидеть значение данных, вы должны набрать последнее в GDB. Взгляните снова на это извлечение из приведенного ранее сеанса отладки GDB:

(gdb) print *tree /* Вывести NODE */
$1 = {sub = {nodep =
 {1 = {lptr = 0x8095598, param_name = 0x8095598 "xUtb",
 ll = 134829464}, r = {rptr = 0x0, pptr = 0, preg = 0x0,
 hd = 0x0, av = 0x0, r_ent =0), x = {extra = 0x0, xl = 0,
 param_list = 0x0}, name = 0x0, number = 1, reflags = 0},
 val = { fltnum = 6.6614606209589101e-316, sp = 0x0,
 slen = 0, sref = 1, idx = 0),
 hash = {next = 0x8095598, name = 0x0, length = 0,
 value = 0x0, ref = 1}}, type = Node_K_print, flags = 1}

Это куча вязкой массы. Однако, GDB все же несколько упрощает ее обработку. Вы можете использовать выражения вроде '($1).sub.val.slen', чтобы пройти через дерево и перечислить структуры данных.

Есть другие причины для избегания объединений. Прежде всего, объединения не проверяются. Ничто, кроме внимания программиста, не гарантирует, что когда вы получаете доступ к одной части объединения, вы получаете доступ к той части, которая была сохранена последней. Мы видели это в ch15-union.c, в котором доступ к обоим «элементам» объединения осуществлялся одновременно.

Вторая причина, связанная с первой, заключается в осторожности с перекрытиями вложенных комбинаций struct/union. Например, в предыдущей версии gawk[173] был такой код.

/* n->lnode перекрывает размер массива, не вызывайте unref, если это массив */
if (n->type != Node_var_array && n->type != Node_array_ref)
unref(n->lnode);

Первоначально if не было, был только вызов unref(), которая освобождает NODE, на которую указывает n->lnode. Однако, в этот момент gawk могла создать аварийную ситуацию. Можете себе представить, сколько времени потребовало отслеживание в отладчике того факта, что то, что рассматривалось как указатель, на самом деле было размером массива!

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

Рекомендация: по возможности избегайте объединений (union). Если это невозможно, тщательно проектируйте и программируйте их!

Оглавление книги


Генерация: 1.691. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз