Бьярн Страустрап. Введение в язык Си++, Классы
Программирование на языке Си++

Google
  Главная   Новости   Статьи   Книги   Ссылки  
 

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

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

5.4.1 Друзья

Предположим, вы определили два класса, vector и matrix (вектор и матрица). Каждый скрывает свое представление и предоставляет полный набор действий для манипуляции объектами его типа. Теперь определим функцию, умножающую матрицу на вектор. Для простоты допустим, что в векторе четыре элемента, которые индексируются 0...3, и что матрица состоит из четырех векторов, индексированных 0...3. Допустим также, что доступ к элементам вектора осуществляется через функцию elem(), которая осуществляет проверку индекса, и что в matrix имеется аналогичная функция. Один подход состоит в определении глобальной функции multiply() (перемножить) примерно следующим образом:

  vector multiply(matrix& m, vector& 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&, vector&);
  };

  class matrix {
      vector v[4];
      // ...
      friend vector multiply(matrix&, vector&);
  };

Функция друг не имеет никаких особенностей, помимо права доступа к закрытой части класса. В частности, friend функция не имеет указателя this (если только она не является полноправным членом функцией). Описание friend - настоящее описание. Оно вводит имя функции в самой внешней области видимости программы и сопоставляется с другими описаниями этого имени. Описание друга может располагаться или в закрытой, или в открытой части описания класса; где именно, значения не имеет.

Теперь можно написать функцию умножения, которая использует элементы векторов и матрицы непосредственно:

  vector multiply(matrix& m, vector& 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 Уточнение*1 Имени Члена

Иногда полезно делать явное различие между именами членов класса и прочими именами. Для этого используется операция :: разрешения области видимости:

  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(), не является ни необходимой, ни допустимой. Единственный способ скрыть имя класса - это сделать это с помощью метода файлы-как-модули. Большую часть нетривиальных классов лучше описывать раздельно:

  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 реализуется как первый (скрытый) параметр функции члена:

  #include <iostream.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(&z1.print);
	  PROC pf2=PROC(&z2.print);
	  z1.print(1);
	  (*pf1)(&z1,2);
	  z2.print(3);
	  (*pf2)(&z2,4);
	}

Во многих случаях можно воспользоваться виртуальными функциями (см. Главу 7) там, где иначе пришлось бы использовать указатели на функции*2.

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& ival()       { check('I',"ival"); return i; }
      double& fval()     { check('D',"fval"); return d; }
      char*& 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().

Вызов конструкторов и деструкторов для статических объектов играет в C++ чрезвычайно важную роль. Это способ обеспечить надлежащую инициализацию и очистку структур данных в библиотеках. Рассмотрим . Откуда берутся cin, cout и cerr? Где они получают инициализацию? И, что самое главное, поскольку потоки вывода имеют внутренние буферы символов, как же эти буферы становятся заполненными? Простой и очевидный ответ, что эта работа осуществляется соответствующими конструкторами и деструкторами до и после выполнения main(). Для инициализации и очистки библиотечных средств есть возможности, альтернативные использованию конструкторов и деструкторов. Все они или очень специальные, или очень уродливые.

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

Иногда, когда вы разрабатываете библиотеку, необходимо или просто удобно создать тип с конструктором и деструктором, предназначенными только для одного: инициализировать и очистить. Такой тип обычно используется только с одной целью, для размещения статического объекта так, чтобы вызывались конструктор и деструктор.

5.5.3 Свободная Память

Рассмотрим:

  main() {
      table* p = new table(100);
      table* q = new table(200);
      delete p;
      delete p;             // возможно, ошибка
  }

Конструктор table::table() будет вызван дважды, как и деструктор table::~table(). То, что C++ не дает никаких гарантий, что для объекта, созданного с помощью new, когда-либо будет вызван деструктор, ничего не значит. В предыдущей программе q не уничтожается, а p уничтожается дважды! Программист может счесть это ошибкой, а может и не счесть, в зависимости от типа p и q. Обычно то, что объект не уничтожается, является не ошибкой, а просто лишней тратой памяти. Уничтожение p дважды будет , как правило, серьезной ошибкой. Обычно результатом применения delete дважды к одному указателю приводит к бесконечному циклу в подпрограмме управления свободной памятью, но определение языка не задает поведение в таком случае, и оно зависит от реализации.

Пользователь может определить новую реализацию операций new и delete (см. #3.2.6). Можно также определить способ взаимодействия конструктора или деструктора с операциями new и delete (см. #5.5.6)

5.5.4 Объекты Класса и Члены

Рассмотрим

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

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

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

Параметры для конструктора члена member (здесь это 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;
      // ...
  }

Порядок, в котором вызываются конструкторы, не определен, поэтому не рекомендуется делать списки параметров с побочными эффектами:

  classdef::classdef(int size)
  : friends(size=size/2), members(size);      // дурной стиль
  {
      no_of_members = size;
      // ...
  }

Если конструктору для члена не нужно ни одного параметра, то никакого списка параметров задавать не надо. Например, поскольку table::table был определен с параметром по умолчанию 15, следующая запись является правильной:

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

и размер size таблицы friend'ов будет равен 15.>

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

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

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

  classdef::classdef(int size)
  {
      members = new table(size);
      friends = new table;          // размер таблицы по умолчанию
      no_of_members = size;
      // ...
  }

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

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

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

5.5.5 Вектора Объектов Класса

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

  table tblvec[10];

будет ошибкой, так как для table::table() требуется целый параметр. Нет способа задать параметры конструктора в описании вектора. Чтобы можно было описывать вектор таблиц table, можно модифицировать описание table (#5.3.1) например так:

  class table {
      // ...
      void init(int sz);    // как старый конструктор
  public:
      table(int sz)         // как раньше, но без по умолчанию
          { init(sz); }
      table()               // по умолчанию
          { init(15); }
  }

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

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

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

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

Но почему же компилятор не может найти число элементов вектора из объема выделенной памяти? Потому, что распределитель свободной памяти не является частью языка и может быть задан программистом.

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

Когда вы используете много небольших объектов, размещаемых в свободной памяти, то вы можете обнаружить, что ваша программа тратит много времени выделяя и освобождая память под эти объекты. Первое решение - это обеспечить более хороший распределитель памяти общего назначения, второе для разработчика классов состоит в том, чтобы взять под контроль управление свободной памятью для объектов некоторого класса с помощью подходящих конструкторов и деструкторов.
Рассмотрим класс name, который использовался в примерах table. Его можно было бы определить так:

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

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

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

  const NALL = 128;
  name* nfree;

Распределитель, используемый операцией new, хранит размер объекта вместе с объектом, чтобы обеспечить правильную работу операции delete. С помощью распределителя, специализированного для типа, можно избежать этих накладных расходов. Например, на моей машине следующий распределитель использует для хранения name 16 байт, тогда как для стандартного распределителя свободной памяти нужно 20 байт. Вот как это можно сделать:

  name::name(char* s, double v, name* n)
  {
      register name* p = nfree;       // сначала выделить

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

      this = p;                       // затем инициализировать
      string = s;
      value = v;
      next = n;
  }

Присвоение указателю this информирует компилятор о том, что программист взял себе управление, и что не надо использовать стандартный механизм распределения памяти. Конструктор name::name() обрабатывает только тот случай, когда name размещается посредством new, но для большей части типов это всегда так. В #5.5.8 объясняется, как написать конструктор для обработки как размещения в свободной памяти, так и других видов размещения.

Заметьте, что просто как

  name* q = new name[NALL];

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

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

  name::~name()
  {
      next = nfree;
      nfree = this;
      this = 0;
  }

Присваивание указателю this 0 в деструкторе обеспечивает, что стандартный распределитель памяти не используется.

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

Когда в конструкторе производится присваивание указателю this, значение this до этого присваивания не определено. Таким образом, ссылка на член до этого присваивания не определена и скорее всего приведет к катастрофе. Имеющийся компилятор не пытается убедиться в том, что присваивание указателю this происходит на всех траекториях выполнения:

  mytype::mytype(int i)
  {
      if (i) this = mytype_alloc();
      // присваивание членам
  };

откомпилируется, и при i==0 никакой объект размещен не будет.

Конструктор может определить, был ли он вызван операцией new, или нет. Если он вызван new, то указатель this на входе имеет нулевое значение, в противном случае this указывает на пространство, уже выделенное для объекта (например, на стек). Поэтому можно просто написать конструктор, который выделяет память, если (и только если) он был вызван через new. Например:

  mytype::mytype(int i)
  {
      if (this == 0) this = mytype_alloc();
      // присваивание членам
  };

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

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

5.5.8 Объекты Переменного Размера

Когда пользователь берет управление распределением и освобождением памяти, он может конструировать объекты, размер которых во время компиляции недетерминирован. В предыдущих примерах вмещающие (или контейнерные - перев.) классы vector, stack, intset и table реализовывались как структуры доступа фиксированного размера, содержание указатели на реальную память. Это подразумевает, что для создания таких объектов в свободной памяти необходимо две операции по выделению памяти, и что любое обращение к хранимой информации будет содержать дополнительную косвенную адресацию. Например:

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

Если каждый объект класса размещается в свободной памяти, это делать не нужно. Вот другой вариант:

  class char_stack {
      int size;
      char* top;
      char s[1];
  public:
      char_stack(int sz);
      void push(char c)  { *top++ = c; }
      char pop()         { return *--top; }
  };

  char_stack::char_stack(int sz)
  {
      if (this) error("стек не в свободной памяти");
      if (sz <1) error("размер стека < 1");
	  this=(char_stack*) new char[sizeof(char_stack)+sz-1]; 
	  size=sz;
	  top=s;
   } 

Заметьте, что деструктор больше не нужен, поскольку память, которую использует char_stack, может освободить delete без всякого содействия со стороны программиста.

5.6 Упражнения

  1. (*1) Модифицируйте настольный калькулятор из Главы 3, чтобы использовать класс table.
  2. (*1) Разработайте tnode (#с.8.5) как класс с конструкторами, деструкторами и т.п. Определите дерево из tnode'ов как класс с конструкторами, деструкторами и т.п.
  3. (*1) Преобразуйте класс intset (#5.3.2) в множество строк.
  4. (*1) Преобразуйте класс intset в множество узлов node, где node - определяемая вами структура.
  5. (*3) Определите класс для анализа. хранения, вычисления и печати простых арифметических выражений, состоящих из целых констант и операций +, -, * и /. Открытый интерфейс должен выгляедть примерно так:
           class expr {
               // ...
           public:
               expr(char*);
               int eval();
               void print();
           }
    


    Параметр строка конструктора expr::expr() является выражением. Функция expr::eval() возвращает значение выражение, а expr::print() печатает представление выражения в cout. Программа может выглядеть, например, так:

           expr x("123/4+123*4-3");
           cout << "x=" << x.eval() << " \n"; 
    	   x.print();
    	   

    Определите класс expr два раза: один раз используя в качестве представления связанный список узлов, а другой раз - символьную строку. Поэкспериментируйте с разными способами печати выражения: с полностью расставленными скобками, в постфиксной записи, в ассемблерном коде и т.д.

  6. (*1) Определите класс char_queue (символьная очередь) таким образом, чтобы открытый интерфейс не зависел от представления. Реализуйте char_queue как (1) связанный список и как (2) вектор. О согласованности не заботьтесь.
  7. (*2) Определите класс histogram (гистограмма), в котором ведется подсчет чисел в определенных интервалах, которые задаются как параметры конструктора histogram. Обеспечьте функцию вывода гистограммы на печать. Сделайте обработку значений, выходящих за границы. Подсказка: .
  8. (*2) Определите несколько классов, предоставляющих случайные числа с определенными распределениями. Каждый класс имеет конструктор, задающий параметры распределения, и функцию draw, которая возвращает "следующее" значение. Подсказка: . Посмотрите также класс intset.
  9. (*2) Перепишите пример date (#5.8.2), пример char_stack (#5.2.5) и пример intset (#5.3.2) не используя функций членов (даже конструкторов и деструкторов). Используйте только class и friend. Сравните с версиями, в которых использовались функции члены.
  10. (*3) Для какого-нибудь языка спроектируйте класс таблица имен и класс вхождение в таблицу имен. Чтобы посмотреть, как на самом деле выглядит таблица имен, посмотрите на компилятор этого языка.
  11. (*2) Модифицируйте класс выражение из Упражнения 5 так, чтобы обрабатывать переменные и операцию присваивания =. Используйте класс таблица имен из Упражнения 10.
  12. (*1) Дана программа:
           #include <iostream.h>
    
           main()
           {
               cout << "Hello, world\n"; 
    	   } 

    модифицируйте ее, чтобы получить выдачу

           Initialize
           Hello, world
           Clean up
    

    Не делайте никаких изменений в main().

*1 Иногда называется также квалификацией. (прим. перев.)
*2 Более поздние версии C++ поддерживают понятие указатель на член: cl::* означает "указатель на член класса cl". Например:


  typedef void (cl::*PROC)(int);
  PROC pf1 = &cl::print;    // приведение к типу ненужно
  PROC pf2 = &cl::print;
  

Для вызовов через указатель на функцию член используются операции . и ->. Например:


  (z1.*pf1)(2);
  ((&z2)->*pf2)(4);

(прим. автора)

Назад ] [Содержание ] [Вперед]

 

Используются технологии uCoz