Страница 18 из 88
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);
|