Бьерн Страуструп - Язык программирования С++. Главы 5-8
Страница 18. Структуры и объединения



5.4.6 Структуры и объединения

 По определению структура - это класс, все члены которого общие,
 т.е. описание

         struct s { ...

 это просто краткая форма описания

         class s { public: ...

 Поименованное объединение определяется как структура, все члены
 которой имеют один и тот же адрес ($$R.9.5). Если известно, что
 в каждый момент времени используется значение только одного члена
 структуры, то объявив ее объединением, можно сэкономить память.
 Например, можно использовать объединение для хранения лексем
 транслятора С:

          union tok_val {
             char* p;      // строка
             char v[8];    // идентификатор (не более 8 символов)
             long i;       // значения целых
             double d;     // значения чисел с плавающей точкой
          };

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

           void strange(int i)
           {
             tok_val x;
             if (i)
                x.p = "2";
             else
                x.d = 2;
             sqrt(x.d);     // ошибка, если i != 0
           }

 Кроме того, определенное таким образом объединение нельзя
 инициализировать таким кажущимся вполне естественным способом:

           tok_val val1 = 12;   // ошибка: int присваивается  tok_val
           tok_val val2 = "12"; // ошибка: char* присваивается tok_val

 Для правильной инициализации надо использовать конструкторы:

           union tok_val {
             char* p;    // строка
             char v[8];  // идентификатор (не более 8 символов)
             long i;     // значения целых
             double d;   // значения чисел с плавающей точкой

             tok_val(const char*);  // нужно выбирать между p и v
             tok_val(int ii)    { i = ii; }
             tok_val(double dd) { d = dd; }
           };

 Эти описания позволяют разрешить с помощью типа членов неоднозначность
 при перегрузке имени функции (см. $$4.6.6 и $$7.3). Например:

           void f()
           {
             tok_val a = 10;    // a.i = 10
             tok_val b = 10.0;  // b.d = 10.0
           }

 Если это невозможно (например, для типов char* и char[8] или int
 и char и т.д.), то определить, какой член инициализируется, можно,
 изучив инициализатор при выполнении программы, или введя
 дополнительный параметр. Например:

           tok_val::tok_val(const char* pp)
           {
             if (strlen(pp) <= 8)
                strncpy(v,pp,8);    // короткая строка
             else
                p = pp;            // длинная строка
           }

 Но лучше подобной неоднозначности избегать.
     Стандартная функция strncpy() подобно strcpy() копирует
 строки, но у нее есть дополнительный параметр, задающий
 максимальное число копируемых символов.
     То, что для инициализации объединения используются конструкторы,
 еще не гарантирует от случайных ошибок при работе с объединением, когда
 присваивается значение одного типа, а выбирается значение другого
 типа. Такую гарантию можно получить, если заключить объединение
 в класс, в котором будет отслеживаться тип заносимого значения :

          class tok_val {
          public:
             enum Tag { I, D, S, N };

          private:
             union {
               const char* p;
               char v[8];
               long i;
               double d;
             };

             Tag tag;

             void check(Tag t) { if (tag != t) error(); }
           public:
             Tag get_tag() { return tag; }

             tok_val(const char* pp);
             tok_val(long ii)   { i = ii; tag = I; }
             tok_val(double dd) { d = dd; tag = D; }

             long& ival()        { check(I); return i; }
             double& fval()      { check(D); return d; }
             const char*& sval() { check(S); return p; }
             char* id()          { check(N); return v; }
          };

          tok_val::tok_val(const char* pp)
          {
            if (strlen(pp) <= 8)  { // короткая строка
               tag = N;
               strncpy(v,pp,8);
            }
            else {                  // длинная строка
               tag = S;
               p = pp;              // записывается только указатель
            }
          }

 Использовать класс tok_val можно так:

          void f()
          {
            tok_val t1("короткая");       // присваивается v
            tok_val t2("длинная строка"); // присваивается p
            char s[8];
            strncpy(s,t1.id(),8);         // нормально
            strncpy(s,t2.id(),8);         // check() выдаст ошибку
          }

     Описав тип Tag и функцию get_tag() в общей части, мы гарантируем,
 что тип tok_val можно использовать как тип параметра. Таким образом,
 появляется надежная в смысле типов альтернатива описанию параметров
 с эллипсисом. Вот, например, описание функции обработки ошибок,
 которая может иметь один, два, или три параметра с типами char*,
 int или double:

           extern tok_val no_arg;

           void error(
               const char* format,
               tok_val a1 = no_arg,
               tok_val a2 = no_arg,
               tok_val a3 = no_arg);

 
« Предыдущая статья   Следующая статья »