5.4.5 Указатели на члены Можно брать адрес члена класса. Операция взятия адреса функции-члена часто оказывается полезной,
поскольку цели и способы применения указателей на функции, о которых мы говорили в $$4.6.9, в
равной степени относятся и к таким функциям. Указатель на член можно получить, применив операцию
взятия адреса & к полностью уточненному имени члена класса, например, &class_name::member_name.
Чтобы описать переменную типа "указатель на член класса X", надо использовать описатель вида X::*.
Например:
#include struct cl
{
char*
val;
void print(int x) { cout << val << x << '\n'; }
cl(char* v) { val = v; }
};
Указатель на член можно описать и использовать так:
typedef void (cl::*PMFI)(int);
int main()
{
cl z1("z1 ");
cl z2("z2 ");
cl* p = &z2;
PMFI pf = &cl::print;
z1.print(1);
(z1.*pf)(2);
z2.print(3);
(p->*pf)(4);
}
Использование typedef для замены трудно воспринимаемого описателя в С достаточно типичный
случай. Операции .* и ->* настраивают указатель на конкретный объект, выдавая в результате функцию,
которую можно вызывать. Приоритет операции () выше, чем у операций .* и ->*, поэтому нужны скобки.
Во многих случаях виртуальные функции ($$6.2.5) успешно заменяют указатели на функции.
5.4.6 Структуры и объединения По определению структура - это класс, все члены которого общие, т.е. описание
Бьерн Страуструп.
Язык программирования С++
140
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)
Бьерн Страуструп.
Язык программирования С++
141
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
t
1("короткая");
// присваивается v
tok_val t2("длинная строка");
// присваивается p
char
s[8];
strncpy(s,t1.id(),8);
//
нормально
strncpy(s,t2.id(),8);
//
check()
выдаст ошибку
}
Бьерн Страуструп.
Язык программирования С++
142
Описав тип 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);