5.3.1 Альтернативные Реализации

   Пока описание открытой части класса и описание функций членов остаются неизменными, реализацию класса можно модифцировать не влияя на ее пользователей. Как пример этого расмотрим таблицу имен, которая использовалась в настольном калькуляторе в Главе 3. Это таблица имен:
   struct name (* char* string; char* next; double value; *);
   Вот вариант класса table:
   // файл table.h
   class table (* name* tbl; public: table() (* tbl = 0; *)
   name* look(char*, int = 0); name* insert(char* s) (* return look(s,1); *) *);
   Эта таблица отличается от той, которая определена в Глве 3 тем, что это настоящий тип. Можно описать более чем одну
   table, можно иметь указатель на table и т.д. Например:
   #include «table.h»
   table globals; table keywords; table* locals;
   main() (* locals = new table; // ... *)
   Вот реализация table::look(), которая использует линеный поиск в связанном списке имен name в таблице:
   #include «string.h»
   name* table::look(char* p, int ins) (* for (name* n = tbl; n; n=n-»next) if (strcmp(p,n-»string) == 0) return n;
   if (ins == 0) error(«имя не найдено»);
   name* nn = new name; nn-»string = new char[strlen(p)+1]; strcpy(nn-»string,p); nn-»value = 1; nn-»next = tbl; tbl = nn; return nn; *)
   Теперь рассмотрим класс table, усовершенствованный таким образом, чтобы использовать хэшированный просмотр, как это делалось в примере с настольным калькулятором. Сделать это труднее из-за того ограничения, что уже написанные программы, в которых использовалась только что определенная версия класа table, должны оставаться верными без изменений:
   class table (* name** tbl; int size; public: table(int sz = 15); ~table();
   name* look(char*, int = 0); name* insert(char* s) (* return look(s,1); *) *);
   В структуру данных и конструктор внесены изменения, оражающие необходимость того, что при использовании хэшировния таблица должна иметь определенный размер. Задание конструктора с параметром по умолчанию обеспечивает, что страя программа, в которой не указывался размер таблицы, остнется правильной. Параметры по умолчанию очень полезны в стуации, когда нужно изменить класс не повлияв на старые программы. Теперь конструктор и деструктор создают и уничтжают хэш-таблицы:
   table::table(int sz) (* if (sz « 0) error(„отрицательный размер таблицы“); tbl = new name*[size=sz];
   for (int i = 0; i«sz; i++) tbl[i] = 0; *)
   table::~table() (* for (int i = 0; i«size; i++) for (name* n = tbl[i]; n; n=n-»next) (* delete n-»string; delete n; *) delete tbl; *)
   Описав деструктор для класса name можно получить более простой и ясный вариант table::~table(). Функция просмотра практически идентична той, которая использовалась в примере настольного калькулятора (#3.1.3):
   #include «string.h»
   name* table::look(char* p, int ins) (* int ii = 0; char* pp = p; while (*pp) ii = ii««1 ^ *pp++; if (ii « 0) ii = -ii; ii %= size;
   for (name* n=tbl[ii]; n; n=n-»next) if (strcmp(p,n-»string) == 0) return n;
   if (ins == 0) error(«имя не найдено»);
   name* nn = new name; nn-»string = new char[strlen(p)+1]; strcpy(nn-»string,p); nn-»value = 1; nn-»next = tbl[ii]; tbl[ii] = nn; return nn;
   *)
   Очевидно, что функции члены класса должны заново комплироваться всегда, когда вносится какое-либо изменение в опсание класса. В идеале такое изменение никак не должно отржаться на пользователях класса. К сожалению, это не так. Для размещения переменной классового типа компилятор должен знать размер объекта класса. Если размер этих объектов меняется, то файлы, в которых класс используется, нужно компилировать зново. Можно написать такую программу (и она уже написана), которая определяет множество (минимальное) файлов, которое необходимо компилировать заново после изменения описания класса, но пока что широкого распространения она не получила.
   Почему, можете вы спросить, С++ разработан так, что поле изменения закрытой части необходима новая компиляция ползователей класса? И действительно, почему вообще закрытая часть должна быть представлена в описании класса? Другими словами, раз пользователям класса не разрешается обращаться к закрытым членам, почему их описания должны приводиться в зголовочных файлах, которые, как предполагается, пользователь читает? Ответ – эффективность. Во многих системах и процесс компиляции, и последовательность операций, реализующих вызов функции, проще, когда размер автоматических объектов (объетов в стеке) известен во время компиляции.
 
   Этой сложности можно избежать, представив каждый объект класса как указатель на «настоящий» объект. Так как все эти указатели будут иметь одинаковый размер, а размещение «настящих» объектов можно определить в файле, где доступна закртая часть, то это может решить проблему. Однако решение поразумевает дополнительные ссылки по памяти при обращении к членам класса, а также, что еще хуже, каждый вызов функции с автоматическим объектом класса включает по меньшей мере один вызов программ выделения и освобождения свободной памяти. Это сделало бы также невозможным реализацию inline-функций члнов, которые обращаются к данным закрытой части. Более того, такое изменение сделает невозможным совместную компоновку C и С++ программ (поскольку C компилятор обрабатывает struct не так, как это будет делать С++ компилятор). Для С++ это было сочтено неприемлемым.

5.3.2 Законченный Класс

   Программирование без сокрытия данных (с применением структур) требует меньшей продуманности, чем программирование со сокрытием данных (с использованием классов). Структуру можно определить не слишком задумываясь о том, как ее предплагается использовать. А когда определяется класс, все внимние сосредотачивается на обеспечении нового типа полным мнжеством операций; это важное смещение акцента. Время, потраченное на разработку нового типа, обычно многократно окупается при разработке и тестировании программы.
   Вот пример законченного типа intset, который реализует понятие «множество целых»:
   class intset (* int cursize, maxsize; int *x; public: intset(int m, int n); // самое большее, m int'ов в 1..n ~intset();
   int member(int t); // является ли t элементом? void insert(int t); // добавить "t" в множество
   void iterate(int amp; i) (* i = 0; *) int ok(int amp; i) (* return i«cursize; *) int next(int amp; i) (* return x[i++]; *) *);
   Чтобы протестировать этот класс, можно создать и распчатать множество случайных целых чисел. Такое множество могло бы быть результатом розыгрыша лотереи. Это простое множество можно также использовать для проверки последовательности цлых на повторы. Но для большинства приложений тип множество должен быть немного более проработанным. Как всегда, возможны ошибки:
   #include «stream.h»
   void error(char* s) (* cerr «„ "set: " «« s «« «\n“; exit(1); *)
   Класс intset используется в main(), которая предполагает два целых параметра. Первый параметр задает число случайных чисел, которые нужно сгенерировать. Второй параметр указывает диапазон, в котором должны лежать случайные целые:
 
   main(int argc, char* argv[]) (* if (argc != 3) error(«ожидается два параметра»); int count = 0; int m = atoi(argv[1]); // число элементов множества int n = atoi(argv[2]); // в диапазоне 1..n intset s(m,n);
   while (count«m) (* int t = randint(n); if (s.member(t)==0) (* s.insert(t); count++; *) *)
   print_in_order( amp;s); *)
   В программе, для которой требуется два параметра, счечик числа параметров, argc, должен равняться трем, потому что имя программы всегда передается как argv[0]. Функция
   extern int atoi(char*);
   функция atoi() это стандартная библиотечная функция для преобразования представления целого в виде строки в его внуреннюю (двоичную) форму. Случайные числа генерируются с пмощью стандартной функции rand():
   extern int rand(); // Не очень случайные, будьте осторожны
   int randint(int u) // в диапазоне 1..u (* int r = rand(); if (r « 0) r = -r; return 1 + r%u ; *)
   Подробности реализации класса должны представлять для пользователя весьма незначительный интерес, но здесь в любом случае будут функции члены. Конструктор выделяет целый вектор заданного максимального размера множества, а деструктор освбождает его:
   intset::intset(int m, int n)//самое большее,m int'ов в 1..n (* if (m«1 !! n„m) error(«недопустимый размер intset“); cursize = 0; maxsize = m; x = new int[maxsize]; *)
   intset::~intset() (* delete x; *)
   Целые числа вставляются, поэтому они хранятся в возратающем порядке:
   void intset::insert(int t) (* if (++cursize » maxsize) error(«слишком много элементов»); int i = cursize-1; x[i] = t;
 
   while (i»0 amp; amp; x[i-1]»x[i]) (* int t = x[i]; // переставить x[i] и [i-1] x[i] = x[i-1]; x[i-1] = t; i–; *) *)
   Для нахождения членов используется просто двоичный писк:
   int intset::member(int t) // двоичный поиск (* int l = 0; int u = cursize-1;
   while (l «= u) (* int m = (l+u)/2; if (t „ x[m]) u = m-1; else if (t “ x[m]) l = m+1; else return 1; // найдено *) return 0; // не найдено *)
   И, наконец, нам нужно обеспечить множество операций, чтобы пользователь мог осуществлять цикл по множеству в нектором порядке, поскольку представление intset от пользователя скрыто. Множество внутренней упорядоченности не имеет, поэтму мы не можем просто дать возможность обращаться к вектору (завтра я, наверное, реализую intset по-другому, в виде свзанного списка).
   Дается три функции: iterate() для инициализации итерции, ok() для проверки, есть ли следующий элемент, и next() для того, чтобы взять следующий элемент:
   class intset (* // ... void iterate(int amp; i) (* i = 0; *) int ok(int amp; i) (* return i«cursize; *) int next(int amp; i) (* return x[i++]; *) *);
   Чтобы дать возможность этим трем операциям работать соместно и чтобы запомнить, куда дошел цикл, пользователь дожен дать целый параметр. Поскольку элементы хранятся в отсотированном списке, их реализация тривиальна. Теперь можно определить функцию печати по порядку print_in_order:
   void print_in_order(intset* set) (* int var; set-»iterate(var); while (set-»ok(var)) cout «„ set-“next(var) „« «\n“; *)
   Другой способ задать итератор приводится в #6.8.

5.4 Друзья и Объединения

   В это разделе описываются еще некоторые особенности, ксающиеся классов. Показано, как предоставить функции не члену доступ к закрытым членам. Описывается, как разрешать конфликты имен членов, как можно делать вложенные описания классов, и как избежать нежелательной вложенности. Обсуждается также, как объекты класса могут совместно использовать члены данные, и как использовать указатели на члены. Наконец, приводится пример, показывающий, как построить дискриминирующее (экононое) объединение.

5.4.1 Друзья

   Предположим, вы определили два класса, vector и matrix (вектор и матрица). Каждый скрывает свое представление и прдоставляет полный набор действий для манипуляции объектами его типа. Теперь определим функцию, умножающую матрицу на вектор. Для простоты допустим, что в векторе четыре элемента, которые индексируются 0...3, и что матрица состоит из четырех векторов, индексированных 0...3. Допустим также, что доступ к элементам вектора осуществляется через функцию elem(), котрая осуществляет проверку индекса, и что в matrix имеется аналогичная функция. Один подход состоит в определении глбальной функции multiply() (перемножить) примерно следующим образом:
   vector multiply(matrix amp; m, vector amp; v); (* vector r; for (int i = 0; i«3; i++) (* // r[i] = m[i] * v; r.elem(i) = 0; for (int j = 0; j«3; j++) r.elem(i) += m.elem(i,j) * v.elem(j); *) return r; *)
   Это своего рода «естественный» способ, но он очень неэфективен. При каждом обращении к multiply() elem() будет взываться 4*(1+4*3) раза.
   Теперь, если мы сделаем multiply() членом класса vector, мы сможем обойтись без проверки индексов при обращении к элменту вектора, а если мы сделаем multiply() членом класса matrix, то мы сможем обойтись без проверки индексов при обрщении к элементу матрицы. Однако членом двух классов функция быть не может. Нам нужно средство языка, предоставляющее функции право доступа к закрытой части класса. Функция не член, получившая право доступа к закрытой части класса, назвается другом класса (friend). Функция становится другом класса после описания как friend. Например:
   class matrix;
   class vector (* float v[4]; // ... friend vector multiply(matrix amp;, vector amp;); *);
   class matrix (* vector v[4]; // ... friend vector multiply(matrix amp;, vector amp;); *);
   Функция друг не имеет никаких особенностей, помимо права доступа к закрытой части класса. В частности, friend функция не имеет указателя this (если только она не является полноравным членом функцией). Описание friend – настоящее описние. Оно вводит имя функции в самой внешней области видимости
   программы и сопоставляется с другими описаниями этого имени. Описание друга может располагаться или в закрытой, или в отрытой части описания класса. Где именно, значения не имеет.
   Теперь можно написать функцию умножения, которая исползует элементы векторов и матрицы непосредственно:
   vector multiply(matrix amp; m, vector amp; v); (* vector r; for (int i = 0; i«3; i++) (* // r[i] = m[i] * v; r.v[i] = 0; for (int j = 0; j«3; j++) r.v[i] += m.v[i][j] * v.v[j]; *) return r; *)
   Есть способы преодолеть эту конкретную проблему эффетивности не используя аппарат friend (можно было бы определить операцию векторного умножения и определить multiply() с ее помощью). Однако существует много задач, кторые проще всего решаются, если есть возможность предоствить доступ к закрытой части класса функции, которая не явлется членом этого класса. В Главе 6 есть много примеров применения friend. Достоинства функций друзей и членов будут обсуждаться позже.
   Функция член одного класса может быть другом другого. Например:
   class x (* // ... void f(); *);
   class y (* // ... friend void x::f(); *);
   Нет ничего необычного в том, что все функции члены однго класса являются друзьями другого. Для этого есть даже блее краткая запись:
   class x (* friend class y; // ... *);
   Такое описание friend делает все функции члены класса y друзьями x.

5.4.2 Уточнение* Имени Члена

   – * Иногда называется также квалификацией. (прим. перев.)
   Иногда полезно делать явное различие между именами члнов класса и прочими именами. Для этого используется операция ::, «разрешения области видимости»:
   class x (* int m; public: int readm() (* return x::m; *) void setm(int m) (* x::m = m; *)
   *);
   В x::setm() имя параметра m прячет член m, поэтому единственный способ сослаться на член – это использовать его уточненное имя x::m. Операнд в левой части :: должен быть именем класса.
   Имя с префиксом :: (просто) должно быть глобальным имнем. Это особенно полезно для того, чтобы можно было исползовать часто употребимые имена вроде read, put и open как имена функций членов, не теряя при этом возможности обращатся к той версии функции, которая не является членом. Например:
   class my_file (* // ... public: int open(char*, char*); *);
   int my_file::open(char* name, char* spec) (* // ... if (::open(name,flag))(*//использовать open() из UNIX(2) // ... *) // ... *)

5.4.3 Вложенные Классы

   Описание класса может быть вложенным. Например:
   class set (* struct setmem (* int mem; setmem* next; setmem(int m, setmem* n) (* mem=m; next=n; *) *); setmem* first; public: set() (* first=0; *) insert(int m) (* first = new setmem(m,first);*) // ... *);
   Если только вложенный класс не является очень простым, в таком описании трудно разобраться. Кроме того, вложение класов – это не более чем соглашение о записи, поскольку вложеный класс не является скрытым в области видимости лексически охватывающего класса:
   class set (* struct setmem (* int mem; setmem* next; setmem(int m, setmem* n) *); // ... *);
   setmem::setmem(int m, setmem* n) (* mem=m, next=n*) setmem m1(1,0);
   Такая запись, как set::setmem::setmem(), не является ни необходимой, ни допустимой. Единственный способ скрыть имя класса – это сделать это с помощью метода файлы-как-модули (#
   4.4). Большую часть нетривиальных классов лучше описывать раздельно:
   class setmem (* friend class set; // доступ только с помощью членов set int mem; setmem* next; setmem(int m, setmem* n) (* mem=m; next=n; *) *);
   class set (* setmem* first; public: set() (* first=0; *) insert(int m) (* first = new setmem(m,first);*) // ... *);

5.4.4 Статические Члены

   Класс – это тип, а не объект данных, и в каждом объекте класса имеется своя собственная копия данных, членов этого класса. Однако некоторые типы наиболее элегантно реализуются, если все объекты этого типа могут совместно использовать (разделять) некоторые данные. Предпочтительно, чтобы такие разделяемые данные были описаны как часть класса. Например, для управления задачами в операционной системе или в ее модли часто бывает полезен список всех задач:
   class task (* // ... task* next; static task* task_chain; void shedule(int); void wait(event); // ... *);
   Описание члена task_chain (цепочка задач) как static обеспечивает, что он будет всего лишь один, а не по одной кпии на каждый объект task. Он все равно остается в области видимости класса task, и «извне» доступ к нему можно полчить, только если он был описан как public. В этом случае его имя должно уточняться именем его класса:
   task::task_chain
   В функции члене на него можно ссылаться просто task_chain. Использование статических членов класса может зметно снизить потребность в глобальных переменных.

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

   Можно брать адрес члена класса. Получение адреса функции члена часто бывает полезно, поскольку те цели и причины, кторые приводились в #4.6.9 относительно указателей на фунции, в равной степени применимы и к функциям членам. Однако, на настоящее время в языке имеется дефект: невозможно описать выражением тип указателя, который получается в результате этой операции. Поэтому в текущей реализации приходится жулничать, используя трюки. Что касается примера, который привдится ниже, то не гарантируется, что он будет работать. Ипользуемый трюк надо локализовать, чтобы программу можно было преобразовать с использованием соответствующей языковой контрукции, когда появится такая возможность. Этот трюк исползует тот факт, что в текущей реализации this реализуется как первый (скрытый) параметр функции члена*:
 
   – * Более поздние версии С++ поддерживают понятие указтель на член: cl::* означает «указатель на член класса cl». Например:
   typedef void (cl::*PROC)(int); PROC pf1 = amp;cl::print; // приведение к типу ненужно PROC pf2 = amp;cl::print;
   Для вызовов через указатель на функцию член используются операции . и -». Например:
   (z1.*pf1)(2); (( amp;z2)-»*pf2)(4);
   (прим. автора)
   #include «stream.h»
   struct cl (* char* val; void print(int x) (* cout «„ val «« x «« «\n“; *); cl(char* v) (* val = v; *) *);
   // ``фальшивый'' тип для функций членов: typedef void (*PROC)(void*, int);
   main() (* cl z1("z1 "); cl z2("z2 "); PROC pf1 = PROC( amp;z1.print); PROC pf2 = PROC( amp;z2.print); z1.print(1); (*pf1)( amp;z1,2); z2.print(3); (*pf2)( amp;z2,4); *)
   Во многих случаях можно воспользоваться виртуальными функциями (см. Главу 7) там, где иначе пришлось бы использвать указатели на функции.

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

   По определению struct – это просто класс, все члены кторого открытые, то есть
   struct s (* ...
   есть просто сокращенная запись
   class s (* public: ...
   Структуры используются в тех случаях, когда сокрытие данных неуместно.
   Именованное объединение определяется как struct, в котрой все члены имеют один и тот же адрес (см. #с.8.5.13). Если известно, что в каждый момент времени нужно только одно знчение из структуры, то объединение может сэкономить просранство. Например, можно определить объединение для хранения лексических символов C компилятора:
 
   union tok_val (* char* p; // строка char v[8]; // идентификатор (максимум 8 char) 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 curr_val = 12; //ошибка:int присваивается tok_val'у
   является недопустимым. Для того, чтобы это преодолеть, можно воспользоваться конструкторами:
   union tok_val (* char* p; // строка char v[8]; // идентификатор (максимум 8 char) long i; // целые значения double d; // значения с плавающей точкой
   tok_val(char*); // должна выбрать между p и v tok_val(int ii) (* i = ii; *) tok_val() (* d = dd; *) *);
   Это позволяет справляться с теми ситуациями, когда типы членов могут быть разрешены по правилам для перегрузки имени функции (см. #4.6.7 и #6.3.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(char* pp) (* if (strlen(pp) «= 8) strncpy(v,pp,8); // короткая строка else p = pp; // длинная строка *)
   Таких ситуаций вообще-то лучше избегать.
   Использование конструкторов не предохраняет от такого случайного неправильного употребления tok_val, когда сначала
   присваивается значение одного типа, а потом рассматривается как другой тип. Эта проблема решается встраиванием объединния в класс, который отслеживает, какого типа значение помщается:
   class tok_val (* char tag; union (* char* p; char v[8]; long i; double d; *); int check(char t, char* s) (* if (tag!=t) (* error(s); return 0; *) return 1; *) public: tok_val(char* pp); tok_val(long ii) (* i=ii; tag='I'; *) tok_val(double dd) (* d=dd; tag='D'; *)
   long amp; ival() (* check('I',"ival"); return i; *) double amp; fval() (* check('D',"fval"); return d; *) char* amp; sval() (* check('S',"sval"); return p; *) char* id() (* check('N',"id"); return v; *) *);
   Конструктор, получающий строковый параметр, использует для копирования коротких строк strncpy(). strncpy() похожа на strcpy(), но получает третий параметр, который указывает, сколько символов должно копироваться:
   tok_val::tok_val(char* pp) (* if (strlen(pp) «= 8) (* // короткая строка tag = 'N' strncpy(v,pp,8); // скопировать 8 символов *) else (* // длинная строка tag = 'S'; p = pp; // просто сохранить указатель *) *)
   Тип tok_val можно использовать так:
   void f() (* tok_val t1(«short»); // короткая, присвоить v tok_val t2(«long string»); //длинная строка,присвоить p char s[8]; strncpy(s,t1.id(),8); // ok strncpy(s,t2.id(),8); // проверка check() не пройдет *)

5.5 Конструкторы и Деструкторы

   Если у класса есть конструктор, то он вызывается всегда, когда создается объект класса. Если у класса есть деструктор, то он вызывается всегда, когда объект класса уничтожается. Объекты могут создаваться как:
   1. Автоматический объект: создается каждый раз, когда его описание встречается при выполнении программы, и уничтжается каждый раз при выходе из блока, в котором оно появлось;
   2. Статический объект: создается один раз, при запуске программы, и уничтожается один раз, при ее завершении;
   3. Объект в свободной памяти: создается с помощью опрации new и уничтожается с помощью операции delete;
   4. Объект член: как объект другого класса или как элмент вектора.
   Объект также может быть построен с помощью явного примнения конструктора в выражении (см. #6.4), в этом случае он является автоматическим объектом. В следующих подразделах предполагается, что объекты принадлежат классу, имеющему конструктор и деструктор. Примером может служить класс table из #5.3.

5.5.1 Предостережение

   Если x и y – объекты класса cl, то x=y в стандартном случае означает побитовое копирование y в x (см. #2.3.8). Ткая интерпретация присваивания может привести к изумляющему (и обычно нежелательному) результату, если оно применяется к объектам класса, для которого определены конструктор и десруктор. Например:
   class char_stack (* int size; char* top; char* s; public: char_stack(int sz) (* top=s=new char[size=sz]; *) ~char_stack() (* delete s; *) // деструктор void push(char c) (* *top++ = c; *) char pop() (* return *–top; *) *);
   void h() (* char_stack s1(100); char_stack s2 = s1; // неприятность char_stack s3(99); s3 = s2; // неприятность *)
   Здесь char_stack::char_stack() вызывается дважды: для s1 и для s3. Для s2 он не вызывается, поскольку эта переменная инициализируется присваиванием. Однако деструктор char_stack::~char_stack() вызывается трижды: для s1, s2 и s3! Кроме того, по умолчанию действует интерпретация присваивания как побитовое копирование, поэтому в конце h() каждый из s1, s2 и s3 будет содержать указатель на вектор символов, размщенный в свободной памяти при создании s1. Не останется никкого указателя на вектор символов, выделенный при создании s3. Таких отклонений можно избежать: см. Главу 6.

5.5.2 Статическая Память

   Рассмотрим следующее:
   table tbl1(100);
   void f() (* static table tbl2(200); *)
   main() (*
   f(); *)
   Здесь конструктор table::table(), определенный в #5.3.1, будет вызываться дважды: один раз для tbl1 и один раз для tbl2. Деструктор table::~table() также будет вызван дважды: для уничтожения tbl1 и tbl2 после выхода из main(). Конструторы для глобальных статических объектов в файле выполняются в том порядке, в котором встречаются описания; деструкторы вызываются в обратном порядке. Неопределено, вызывается ли конструктор для локального статического объекта, если фунция, в которой этот объект описан, не вызывается. Если контруктор для локального статического объекта вызывается, то он вызывается после того, как вызваны конструкторы для лексичеки предшествующих ему глобальных статических объектов.
   Параметры конструкторов для статических объектов должны быть константными выражениями:
   void g(int a) (* static table t(a); // ошибка *)
   Традиционно выполнением программы считалось выполнение main(). Так никогда не было, даже в C, но только размещение статических объектов класса с конструктором и/или деструктром дают программисту простой и очевидный способ задания тго, что будет выполняться до и/или после вызова main().
   Вызов конструкторов и деструкторов для статических обектов играет в С++ чрезвычайно важную роль. Это способ обеспечить надлежащую инициализацию и очистку структур данных в библиотеках. Рассмотрим «stream.h». Откуда берутся cin, cout и cerr? Где они получают инициализацию? И, что самое главное, поскольку потоки вывода имеют внутренние буферы сиволов, как же эти буферы заполняются? Простой и очевидный овет таков, что эта работа осуществляется соответствующими конструкторами и деструкторами до и после выполнения main(). Для инициализации и очистки библиотечных средств есть возмоности, альтернативные использованию конструкторов и деструторов. Все они или очень специальные, или очень уродливые.
   Если программа завершается с помощью функции exit(), то деструкторы для статических объектов будут вызваны, а если она завершается с помощью abort(), то не будут. Заметьте, что это подразумевает, что exit() не завершает программу мгновено. Вызов exit() в деструкторе может привести к бесконечной рекурсии.
   Иногда, когда вы разрабатываете библиотеку, необходимо или просто удобно создать тип с конструктором и деструктором, предназначенными только для одного: инициализировать и очитить. Такой тип обычно используется только с одной целью, для размещения статического объекта так, чтобы вызывались контруктор и деструктор.