Язык программирования C++ от Страуструпа

       

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


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

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);


Содержание раздела