Главная->Інформатика та програмування->Содержание->Эффективное использование памяти классом String

ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ В C++ (4-Е ИЗДАНИЕ) (часть 11) онлайн

Эффективное использование памяти классом String

Программы ASSIGN и XOFXREF на самом деле вовсе не нуждаются в перегрузке при-

сваиваний и конструкторов копирования. Ведь они используют крайне простые

 классы с единственным элементом данных, поэтому присваивание и копирова-

ние по умолчанию работали бы вполне нормально. Но сейчас мы рассмотрим

пример, в котором крайне важно использовать перегрузку этих операторов.

Недостатки класса String

Мы уже насмотрелись на разные версии нашего кустарного класса String в пре-

дыдущих главах. И эти версии не являются на самом деле оптимальными. Было

бы здорово, наверное, перегрузить оператор присваивания, тогда мы смогли бы

присваивать значение одного объекта класса String другому с помощью выражения

s2 = s1;

Но, если мы и вправду перегрузим оператор присваивания, встанет вопрос,

как работать с реальными строками (то есть массивами символьного типа char),

которые являются принципиальными элементами данных для класса String.

Можно сделать так, чтобы каждый объект имел специально отведенное «место»

для хранения строки. Тогда, присваивая один объект класса String другому, мы

просто копируем строку из исходного объекта в объект назначения. Но если вы

все же хотите экономить память, то необходимо обеспокоиться тем, чтобы одна

и та же строка не дублировалась попусту в двух и более местах памяти. Поверьте,

это не слишком эффективный подход, особенно учитывая то, что строчки могут

быть довольно длинными. На рис. 11.4 показано, как это странно выглядит:

Рис. 11.4. Объектная диаграмма UML: дублирование строк

Вместо того чтобы держать в каждом объекте класса String символьную стро-

ку, можно занести в объект только лишь указатель на строку! Теперь, присва-

ивая значения одного объекта другому, мы копируем только указатель из одно-

го объекта в другой. Оба указателя, разумеется, будут указывать на одну и ту же

строку. Да, это действительно намного лучше, ведь мы храним в памяти только

один экземпляр каждой строки. На рис. 11.5 это показано.

Конечно, используя такую систему, необходимо внимательно следить за уда-

лением объектов класса String. Если деструктор этого класса использует delete

для освобождения памяти, занятой символьной строкой, и если имеются несколь-

ко указателей на эту строку, объекты так и останутся с висящими указателями,

указывающими туда, где строки уже давно нет.

 

Рис. 11.5. Объектная диаграмма UML: дублирование указателей на строку

Выходит, что для того, чтобы использовать указатели на строки в объектах

класса String, необходимо следить за тем, сколько именно объектов указывают

на конкретную строку. Тем самым мы можем избежать использования delete по

отношению к строкам до тех пор, пока не будет удален последний объект, ука-

зывающий на данную строку. Наш следующий пример, STRIMEM, предназначен

как раз для демонстрации этого.

Класс-счетчик строк

Допустим, имеется несколько объектов класса String, указывающих на опреде-

ленную строку, и мы хотим научить программу считать, сколько именно объек-

тов на нее указывают. Где нам хранить этот счетчик?

Было бы слишком обременительно для каждого объекта класса String счи-

тать, сколько его коллег указывают на данную строку, поэтому мы не будем

вводить счетчик в состав переменных класса String. Может быть, использовать

статическую переменную? А ведь это мысль! Мы могли бы создать статический

массив, который хранил бы список адресов строк и их порядковые номера. Да,

но, с другой стороны, накладные расходы слишком велики. Рациональней будет

создать новый особый класс для подсчета строк и указателей. Каждый объект

такого класса, назовем его strCount, будет содержать счетчик и сам указатель на

строчку. В каждый объект класса String поместим указатель на соответствующий

объект класса strCount. Схема такой системы показана на рис. 11.6.

Для того чтобы убедиться в нормальном доступе объектов String к объектам

strCount, сделаем String дружественным по отношению к strCount. Кроме того, нам

хотелось бы достоверно знать, что класс strCount используется только классом

String. Чтобы запретить несанкционированный доступ к каким-либо его функци-

ям, сделаем все методы strCount скрытыми. Поскольку String является дружест-

венным, на него это ограничение не распространяется. Приводим листинг про-

граммы STRIMEM.

Листинг 11.18. Программа STRIMEM

// strimem.cpp

// Класс String с экономией памяти

// Перегружаемая операция присваивания и конструктор копирования

#include <iostream>

#include <cstring>              // для strcpy() и т. д.

using namespace std;

 

 

///////////////////////////////////////////////////////////

class strCount                  // Класс-счетчик уникальных строк

  {                    private:

    int count;                  // собственно счетчик

    char* str;                  // указатель на строку

    friend class String;        // сделаем себя доступными

    //методы скрыты         

//---------------------------------------------------------

    strCount(char* s)           // конструктор с одним аргументом

      {

      int length = strlen(s);   // длина строкового

                                // аргумента

      str = new char[length+1]; // занять память

                                // под строку

      strcpy(str, s);           // копировать в нее аргументы

      count=1;                  // считать с единицы

      }

//---------------------------------------------------------

    ~strCount()                 // деструктор

      { delete[] str; }         // удалить строку

  };

///////////////////////////////////////////////////////////

class String                    // класс String

  {

  private:

    strCount* psc;              // указатель на strCount

  public:

    String()                    // конструктор без аргументов

      { psc = new strCount("NULL"); }

//---------------------------------------------------------

    String(char* s)             // конструктор с одним аргументом

      { psc = new strCount(s); }

//---------------------------------------------------------

    String(String& S)           // конструктор копирования

      {   

      psc = S.psc;

      (psc->count)++;

      }

//---------------------------------------------------------

    ~String()                   // деструктор

      {

      if(psc->count==1)         // если последний

                                // пользователь,

        delete psc;             // удалить strCount

      else                      // иначе 

        (psc->count)--;         // уменьшить счетчик

      }

//---------------------------------------------------------

    void display()              // вывод String

      {

      cout << psc->str;         // вывести строку

      cout << " (addr=" << psc << ")";  // вывести адрес

      }

//---------------------------------------------------------

    void operator = (String& S) // присвоение String

      {

      if(psc->count==1)         // если последний

                                // пользователь,

 

 

Листинг 11.18 (продолжение)

        delete psc;             // удалить strCount

      else                      // иначе

        (psc->count)--;         // уменьшить счетчик

      psc = S.psc;              //использовать strCount

                                //аргумента

      (psc->count)++;           //увеличить счетчик

      }

  };

///////////////////////////////////////////////////////////

int main()

{

  String s3 = "Муха по полю пошла, муха денежку нашла";

  cout << "\ns3="; s3.display(); //вывести s3

 

  String s1;                     //определить объект String

  s1 = s3;                       //присвоить его другому объекту

  cout << "\ns1="; s1.display(); //вывести его

 

  String s2(s3);                 //инициализация

  cout << "\ns2="; s2.display(); //вывести

                                 //инициализированное

  cout << endl;

  return 0;

}

Рис. 11.6. Объекты классов String и strCount

 

В части main() данной программы мы определяем объект класса String, s3, со-

держащий строку из известного детского стишка: «Муха по полю пошла, муха

денежку нашла». Затем определяем еще один объект, s1, и присваиваем ему зна-

чение s3. Определяем s2 и инициализируем его с помощью s3. Полагание s1 рав-

ным s3 запускает перегружаемую операцию присваивания, а инициализация s2

с помощью s3 запускает перегружаемую операцию копирования. Выводим все

три строки, а также адрес объекта класса strCount, на который ссылается указа-

тель каждого объекта. Мы это делаем для того, чтобы показать, что все строки

в действительности представлены одной и той же строкой. Ниже — результат

работы программы STRIMEM:

s3= Муха по полю пошла, муха денежку нашла (addr=0x8f5410e00)

s2= Муха по полю пошла, муха денежку нашла (addr=0x8f5410e00)

s1= Муха по полю пошла, муха денежку нашла (addr=0x8f5410e00)

Остальные обязанности класса String мы поделили между классами String

и strCount. Посмотрим, что каждый из них делает.

Класс strCount

Этот класс содержит указатель на реальную строку и считает, сколько объектов

класса String на нее указывают. Его единственный конструктор рассматривает

указатель на строку в качестве аргумента и выделяет для нее область памяти. Он

копирует строку в эту область и устанавливает счетчик в единицу, так как толь-

ко один объект String указывает на строку сразу после ее создания. Деструктор

класса strCount освобождает память, занятую строкой. (Мы используем delete[]

с квадратными скобками, так как строка — это массив.)

Класс String

У класса String есть три конструктора. При создании новой строки генерируется

новый объект strCount для ее хранения, а указатель psc хранит ссылку на этот

объект. Если копируется уже существующий объект String, указатель psc про-

должает ссылаться на старый объект strCount, а счетчик в этом объекте увели-

чивается.

Перегружаемая операция присваивания должна наравне с деструктором

удалять старый объект strCount, на который указывает psc, если счетчик равен

единице. (Здесь нам квадратные скобки после delete уже не требуются, так

как мы удаляем всего лишь единственный объект strCount.) Но почему вдруг

оператор присваивания должен заботиться об удалениях? Все дело в том, что

объект String, стоящий в выражении слева от знака равенства (назовем его s1),

указывал на некоторый объект strCount (назовем его oldStrCnt). После присваи-

вания s1 будет указывать на объект, находящийся справа от знака равенства.

А если больше не существует объектов String, указывающих на oldStrCnt, он

должен быть удален. Если же еще остались объекты, ссылающиеся на него,

его счетчик просто должен быть уменьшен. Рисунок 11.7 показывает действия

перегружаемой операции присваивания, а рис. 11.8 показывает конструктор

копирования.

 

 

 

До выполнения s1 = s2

 

После выполнения s1 = s2

Рис. 11.7. Оператор присваивания в STRIMEM

 

До выполнения String s2 (s3)

 

После выполнения String s2 (s3)

Рис. 11.8. Конструктор копирования в STRIMEM

 

24