Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Бьерн Страуструп C++.doc
Скачиваний:
12
Добавлен:
07.11.2018
Размер:
2.45 Mб
Скачать

5.4.5 Указатели на члены

Можно брать адрес члена класса. Операция взятия адреса функции-члена часто оказывается полезной, поскольку цели и способы применения указателей на функции, о которых мы говорили в $$4.6.9, в равной степени относятся и к таким функциям. Указатель на член можно получить, применив операцию взятия адреса & к полностью уточненному имени члена класса, например, &class_name::member_name. Чтобы описать переменную типа "указатель на член класса X", надо использовать описатель вида X::*. Например:

#include <iostream.h>

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 Структуры и объединения

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

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