Бьерн Страуструп (947334), страница 30
Текст из файла (страница 30)
};
class set {
setmem* first;
public:
set() { first=0; }
insert(int m) { first = new setmem(m,first); }
// ...
};
Полезное свойство вложенности - это сокращение числа глобальных имен,
а недостаток его в том, что оно нарушает свободу использования
вложенных типов (см. $$12.3).
Имя класса-члена (вложенного класса) можно использовать вне
описания объемлющего его класса так же, как имя любого другого
члена:
class X {
struct M1 { int m; };
public:
struct M2 { int m; };
M1 f(M2);
};
void f()
{ M1 a; // ошибка: имя `M1' вне области видимости
M2 b; // ошибка: имя `M1' вне области видимости
X::M1 c; // ошибка: X::M1 частный член
X::M2 d; // нормально
}
Отметим, что контроль доступа происходит и для имен вложенных
классов.
В функции-члене область видимости класса начинается после
уточнения X:: и простирается до конца описания функции. Например:
M1 X::f(M2 a) // ошибка: имя `M1' вне области видимости
{ /* ... */ }
X::M1 X::f(M2 a) // нормально
{ /* ... */ }
X::M1 X::f(X::M2 a) // нормально, но третье уточнение X:: излишне
{ /* ... */ }
5.4.4 Статические члены
Класс - это тип, а не некоторое данное, и для каждого объекта
класса создается своя копия членов, представляющих данные. Однако,
наиболее удачная реализация некоторых типов требует, чтобы все
объекты этого типа имели некоторые общие данные. Лучше, если эти
данные можно описать как часть класса. Например, в операционных
системах или при моделировании управления задачами часто нужен
список задач:
class task {
// ...
static task* chain;
// ...
};
Описав член chain как статический, мы получаем гарантию, что
он будет создан в единственном числе, т.е. не будет создаваться
для каждого объекта task. Но он находится в области видимости
класса task, и может быть доступен вне этой области, если только
описан в общей части. В этом случае имя члена должно уточняться
именем класса:
if (task::chain == 0) // какие-то операторы
В функции-члене его можно обозначать просто chain. Использование
статических членов класса может заметно сократить потребность в
глобальных переменных.
Описывая член как статический, мы ограничиваем его область
видимости и делаем его независимым от отдельных объектов его
класса. Это свойство полезно как для функций-членов, так и для
членов, представляющих данные:
class task {
// ...
static task* task_chain;
static void shedule(int);
// ...
};
Но описание статического члена - это только описание, и где-то
в программе должно быть единственное определение для описываемого
объекта или функции, например, такое:
task* task::task_chain = 0;
void task::shedule(int p) { /* ... */ }
Естественно, что и частные члены могут определяться подобным образом.
Отметим, что служебное слово static не нужно и даже нельзя
использовать в определении статического члена класса. Если бы оно
присутствовало, возникла бы неоднозначность: указывает ли оно на то,
что член класса является статическим, или используется для
описания глобального объекта или функции?
Слово static одно из самых перегруженных служебных слов в С
и С++. К статическому члену, представляющему данные, относятся
оба основных его значения: "статически размещаемый" , т.е.
противоположный объектам, размещаемым в стеке или свободной памяти,
и "статический" в смысле с ограниченной областью видимости, т.е.
противоположный объектам, подлежащим внешнему связыванию. К
функциям-членам относится только второе значение static.
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);
5.5 Конструкторы и деструкторы
Если у класса есть конструктор, он вызывается всякий раз при
создании объекта этого класса. Если у класса есть деструктор,
он вызывается всякий раз, когда уничтожается объект этого класса.
Объект может создаваться как:
[1] автоматический, который создается каждый раз, когда его
описание встречается при выполнении программы, и уничтожается
по выходе из блока, в котором он описан;
[2] статический, который создается один раз при запуске программы
и уничтожается при ее завершении;
[3] объект в свободной памяти, который создается операцией new
и уничтожается операцией delete;
[4] объект-член, который создается в процессе создания другого
класса или при создании массива, элементом которого он
является.
Кроме этого объект может создаваться, если в выражении явно
используется его конструктор ($$7.3) или как временный объект
($$R.12.2). В обоих случаях такой объект не имеет имени. В следующих
подразделах предполагается, что объекты относятся к классу с
конструктором и деструктором. В качестве примера используется
класс table из $$5.3.1.
5.5.1 Локальные переменные
Конструктор локальной переменной вызывается каждый раз, когда при
выполнении программы встречается ее описание. Деструктор локальной
переменной вызывается всякий раз по выходе из блока, где она
была описана. Деструкторы для локальных переменных вызываются в
порядке, обратном вызову конструкторов при их создании:
void f(int i)
{
table aa;
table bb;
if (i>0) {
table cc;
// ...
}
// ...
}
Здесь aa и bb создаются (именно в таком порядке) при каждом вызове
f(), а уничтожаются они при возврате из f() в обратном порядке -
bb, затем aa. Если в текущем вызове f() i больше нуля, то cc
создается после bb и уничтожается прежде него.
Поскольку aa и bb - объекты класса table, присваивание aa=bb
означает копирование по членам bb в aa (см. $$2.3.8). Такая
интерпретация присваивания может привести к неожиданному (и обычно
нежелательному) результату, если присваиваются объекты класса,
в котором определен конструктор:
void h()
{
table t1(100);
table t2 = t1; // неприятность
table t3(200);
t3 = t2; // неприятность
}
В этом примере конструктор table вызывается дважды: для t1 и t3.