Опубликован: 10.10.2006 | Уровень: специалист | Доступ: свободно
Лекция 5:

Классы

5.5.4 Объекты класса как члены

Рассмотрим пример:

class classdef {
  table members;
  int no_of_members;
  // ...
  classdef(int size);
  ~classdef();
};

Цель этого определения, очевидно, в том, чтобы classdef содержал член, являющийся таблицей размером size, но есть сложность: надо обеспечить вызов конструктора table::table() с параметром size. Это можно сделать, например, так:

classdef::classdef(int size)
 :members(size)
         {
 no_of_members = size;
 // ...
}

Параметр для конструктора члена (т.е. для table::table() ) указывается в определении (но не в описании) конструктора класса, содержащего член (т.е. в определении classdef::classdef() ). Конструктор для члена будет вызываться до выполнения тела того конструктора, который задает для него список параметров.

Аналогично можно задать параметры для конструкторов других членов (если есть еще другие члены):

class classdef {
  table members;
  table friends;
  int no_of_members;
  // ...
  classdef(int size);
  ~classdef();
};

Списки параметров для членов отделяются друг от друга запятыми (а не двоеточиями), а список инициализаторов для членов можно задавать в произвольном порядке:

classdef::classdef(int size)
: friends(size), members(size), no_of_members(size)
{
  // ...
}

Конструкторы вызываются в том порядке, в котором они заданы в описании класса.

Подобные описания конструкторов существенны для типов, инициализация и присваивание которых отличны друг от друга, иными словами, для объектов, являющихся членами класса с конструктором, для постоянных членов или для членов типа ссылки. Однако, как показывает член no_of_members из приведенного примера, такие описания конструкторов можно использовать для членов любого типа.

Если конструктору члена не требуется параметров, то и не нужно задавать никаких списков параметров. Так, поскольку конструктор table::table() был определен со стандартным значением параметра, равным 15, достаточно такого определения:

classdef::classdef(int size)
   : members(size), no_of_members(size)
          {
  // ...
}

Тогда размер таблицы friends будет равен 15.

Если уничтожается объект класса, который сам содержит объекты класса (например, classdef ), то вначале выполняется тело деструктора объемлющего класса, а затем деструкторы членов в порядке, обратном их описанию.

Рассмотрим вместо вхождения объектов класса в качестве членов традиционное альтернативное ему решение: иметь в классе указатели на члены и инициализировать члены в конструкторе:

class classdef {
   table* members;
   table* friends;
   int no_of_members;
   // ...
};

          classdef::classdef(int size)
          {
  members = new table(size);
  friends = new table;  // используется стандартный
     // размер table
  no_of_members = size;
  // ...
}

Поскольку таблицы создавались с помощью операции new, они должны уничтожаться операцией delete:

classdef::~classdef()
{
 // ...
 delete members;
 delete friends;
}

Такие отдельно создаваемые объекты могут оказаться полезными, но учтите, что members и friends указывают на независимые от них объекты, каждый из которых надо явно размещать и удалять. Кроме того, указатель и объект в свободной памяти суммарно занимают больше места, чем объект-член.

5.5.5 Массивы объектов класса

Чтобы можно было описать массив объектов класса с конструктором, этот класс должен иметь стандартный конструктор, т.е. конструктор, вызываемый без параметров. Например, в соответствии с определением

table tbl[10];

будет создан массив из 10 таблиц, каждая из которых инициализируется вызовом table::table(15), поскольку вызов table::table() будет происходить с фактическим параметром 15.

В описании массива объектов не предусмотрено возможности указать параметры для конструктора. Если члены массива обязательно надо инициализировать разными значениями, то начинаются трюки с глобальными или статическими членами.

Когда уничтожается массив, деструктор должен вызываться для каждого элемента массива. Для массивов, которые размещаются не с помощью new, это делается неявно. Однако для размещенных в свободной памяти массивов неявно вызывать деструктор нельзя, поскольку транслятор не отличит указатель на отдельный объект массива от указателя на начало массива, например:

void f()
{
 table* t1 = new table;
 table* t2 = new table[10];
 delete t1;  // удаляется одна таблица
 delete t2;  // неприятность:
         // на самом деле удаляется 10 таблиц
}

В данном случае программист должен указать, что t2 - указатель на массив:

void g(int sz)
{
   table* t1 = new table;
   table* t2 = new table[sz];
   delete t1;
   delete[] t2;
}

Функция размещения хранит число элементов для каждого размещаемого массива. Требование использовать для удаления массивов только операцию delete[] освобождает функцию размещения от обязанности хранить счетчики числа элементов для каждого массива. Исполнение такой обязанности в реализациях С++ вызывало бы существенные потери времени и памяти и нарушило совместимость с С.

5.5.6 Небольшие объекты

Если в вашей программе много небольших объектов, размещаемых в свободной памяти, то может оказаться, что много времени тратится на размещение и удаление таких объектов. Для выхода из этой ситуации можно определить более оптимальный распределитель памяти общего назначения, а можно передать обязанность распределения свободной памяти создателю класса, который должен будет определить соответствующие функции размещения и удаления.

Вернемся к классу name, который использовался в примерах с table. Он мог бы определяться так:

struct name {
  char* string;
  name* next;
  double value;

  name(char*, double, name*);
  ~name();

  void* operator new(size_t);
  void operator delete(void*, size_t);
private:
  enum { NALL = 128 };
  static name* nfree;
};

Функции name::operator new() и name::operator delete() будут использоваться (неявно) вместо глобальных функций operator new() и operator delete(). Программист может для конкретного типа написать более эффективные по времени и памяти функции размещения и удаления, чем универсальные функции operator new() и operator delete(). Можно, например, разместить заранее "куски" памяти, достаточной для объектов типа name, и связать их в список; тогда операции размещения и удаления сводятся к простым операциям со списком. Переменная nfree используется как начало списка неиспользованных кусков памяти:

void* name::operator new(size_t)
 {
   register name* p = nfree;  // сначала выделить

   if (p)
  nfree = p->next;
   else { // выделить и связать в список
  name* q = (name*) new char[NALL*sizeof(name) ];
  for (p=nfree=&q[NALL-1]; q<p; p--) p->next = p-1;
  (p+1)->next = 0;
   }

   return p;
}

Распределитель памяти, вызываемый new, хранит вместе с объектом его размер, чтобы операция delete выполнялась правильно. Этого дополнительного расхода памяти можно легко избежать, если использовать распределитель, рассчитанный на конкретный тип. Так, на машине автора функция name::operator new() для хранения объекта name использует 16 байтов, тогда как стандартная глобальная функция operator new() использует 20 байтов.

Отметим, что в самой функции name::operator new() память нельзя выделять таким простым способом:

name* q= new name[NALL];

Это вызовет бесконечную рекурсию, т.к. new будет вызывать name::name().

Освобождение памяти обычно тривиально:

void name::operator delete(void* p, size_t)
{
  ((name*)p)->next = nfree;
  nfree = (name*) p;
}

Приведение параметра типа void* к типу name* необходимо, поскольку функция освобождения вызывается после уничтожения объекта, так что больше нет реального объекта типа name, а есть только кусок памяти размером sizeof(name). Параметры типа size_t в приведенных функциях name::operator new() и name::operator delete() не использовались. Отметим, что наши функции размещения и удаления используются только для объектов типа name, но не для массивов names.

Равиль Ярупов
Равиль Ярупов
Привет !
Федор Антонов
Федор Антонов
Оплата и обучение
Роман Островский
Роман Островский
Украина
Оксана Пагина
Оксана Пагина
Россия, Москва