Главная страница
Навигация по странице:

  • Упражнение 13.9. Что такое деструктор Что делает синтезируемый деструктор Когда деструктор синтезируется

  • Если да, то почему Какой вывод получится

  • Язык программирования C Пятое издание


    Скачать 1.85 Mb.
    НазваниеЯзык программирования C Пятое издание
    Дата15.07.2019
    Размер1.85 Mb.
    Формат файлаpdf
    Имя файла620354-www.libfox.ru.pdf
    ТипДокументы
    #84130
    страница35 из 54
    1   ...   31   32   33   34   35   36   37   38   ...   54

    Упражнение 13.7. Что произойдет при присвоении одного объекта класса StrBlob другому?
    Что произойдет при присвоении объектов класса StrBlobPtr?
    Упражнение 13.8. Напишите оператор присвоения для класса HasPtr из упражнения 13.5
    раздела 13.1.1. Подобно конструктору копий, данный оператор присвоения должен копировать объект, на который указывает указатель рs.
    13.1.3. Деструктор
    Действие деструктора противоположно действию конструктора: конструкторы инициализируют нестатические переменные-члены объекта, а также могут выполнять другие действия; деструкторы осуществляют все действия, необходимые для освобождения использованных объектом ресурсов и удаления нестатических переменных-членов объекта.
    Деструктор — это функция-член с именем класса, предваряемым тильдой (

    ). У нее нет ни параметров, ни возвращаемого значения: class Foo { public:
    Foo(); // деструктор
    // ...
    };
    Поскольку деструктор не получает никаких параметров, он не может быть перегружен. Для каждого класса возможен только один деструктор. Что делает деструктор
    Подобно тому, как конструктор имеет часть инициализации и тело (см. раздел 7.5.1),
    деструктор имеет тело и часть удаления. В конструкторе переменные-члены инициализируются перед выполнением тела, а инициализация членов осуществляется в порядке их объявления в классе. В деструкторе сначала выполняется тело, а затем происходит удаление членов. Переменные- члены удаляются в порядке, обратном их инициализации.
    Тело деструктора осуществляет все операции, которые разработчик класса считает необходимыми выполнить после использования объекта. Как правило, деструктор освобождает ресурсы объекта, зарезервированные на протяжении его существования.
    У деструктора нет ничего похожего на список инициализации конструктора для контроля удаления переменных-членов; часть удаления неявна. Происходящее при удалении переменной-члена зависит от его типа. Члены типа класса удаляются за счет выполнения его собственного деструктора. У встроенных типов нет деструкторов, поэтому для удаления членов встроенного типа не делается ничего.
    Неявное удаление члена-указателя встроенного типа не удаляет объект, на который он указывает.
    Page 626/1103

    В отличие от обычных указателей, интеллектуальные указатели (см. раздел 12.1.1) являются классами и имеют деструкторы. Поэтому, в отличие от обычных указателей, члены,
    являющиеся интеллектуальными указателями, автоматически удаляются на фазе удаления.
    Когда происходит вызов деструктора
    Деструктор автоматически используется всякий раз, когда удаляется объект его типа.
    • Переменные удаляются, когда выходят из области видимости.
    • Переменные-члены объекта удаляются при удалении объекта, которому они принадлежат.
    • Элементы в контейнере (будь то библиотечный контейнер или массив) удаляются при удалении контейнера.
    • Динамически созданные объекты удаляются при применении оператора delete к указателю на объект (см. раздел 12.1.2).
    • Временные объекты удаляются в конце выражения, в котором они были созданы.
    Поскольку деструкторы выполняются автоматически, программы могут резервировать ресурсы и (обычно) не заботиться о том, когда они освобождаются.
    Например, следующий фрагмент кода определяет четыре объекта класса Sales_data:
    { // новая область видимости
    // p и p2 указывают на динамически созданные объекты Sales_data
    *p = new Sales_data; // p - встроенный указатель auto p2 = make_shared<Sales_data>(); // p2 - shared_ptr
    Sales_data item(*p); // конструктор копий копирует *p в item vector<Sales_data> vec; // локальный объект vec.push_back(*p2); // копирует объект, на который указывает p2 delete p; // деструктор вызывается для объекта, на
    // который указывает p
    Page 627/1103

    } // выход из локальной области видимости; деструктор вызывается
    // для item, p2 и vec
    // удаление p2 уменьшает его счетчик пользователей; если значение
    // счетчика дойдет до 0, объект освобождается
    // удаление вектора vec удалит и его элементы
    Каждый из этих объектов содержит член типа string, который резервирует динамическую память для содержания символов переменной-члена bookNo. Но единственная память,
    которой код должен управлять непосредственно, — это самостоятельно зарезервированный объект. Код непосредственно освобождает только динамически созданный объект, связанный с указателем p.
    Другие объекты класса Sales_data автоматически удаляются при выходе из области видимости. По завершении блока vec, p2 и item выходят из области видимости, это означает вызов деструкторов классов vector, shared_ptr и Sales_data для соответствующих объектов.
    Деструктор класса vector удалит элемент, помещенный в вектор vec. Деструктор класса shared_ptr осуществит декремент счетчика ссылок объекта, на который указывает указатель p2. В данном примере этот счетчик достигнет нуля, поэтому деструктор класса shared_ptr удалит объект класса Sales_data, зарезервированный с использованием указателя p2.
    Во всех случаях деструктор класса Sales_data неявно удаляет переменную-член bookNo.
    Удаление переменной-члена bookNo запускает деструктор класса string, который освобождает память, используемую для хранения ISBN.
    Когда из области видимости выходит ссылка или указатель на объект, деструктор не выполняется .Синтезируемый деструктор
    Компилятор определяет синтезируемый деструктор (synthesized destructor) для любого класса, который не определяет собственный деструктор. Подобно конструкторам копий и операторам присвоения копии,
    определение для некоторых классов синтезируемого деструктора предотвращает удаление объектов этого типа (раздел 13.1.6). В противном случае у синтезируемого деструктора будет пустое тело.
    Например, синтезируемый деструктор класса Sales_data эквивалентен следующему: class Sales_data { public:
    //
    Page 628/1103
    не делать ничего, кроме удаления переменных-членов,
    // осуществляемого автоматически
    Sales_data() { }
    // другие члены как прежде
    };
    Переменные-члены автоматически удаляются после выполнения (пустого) тела деструктора.
    В частности, деструктор класса string будет выполнен для освобождения памяти,
    используемой переменной-членом bookNo.
    Важно понять, что само тело деструктора не удаляет переменные-члены непосредственно.
    Они удаляются в ходе неявной фазы удаления, которая следует за телом деструктора. Тело деструктора выполняется в дополнение к удалению членов, осуществляемому в ходе удаления объекта.Упражнения раздела 13.1.3

    Упражнение 13.9. Что такое деструктор? Что делает синтезируемый деструктор? Когда деструктор синтезируется?
    Упражнение 13.10. Что произойдет при удалении объекта класса StrBlob? А класса StrBlobPtr?
    Упражнение 13.11. Добавьте деструктор в класс HasPtr из предыдущих упражнений.
    Упражнение 13.12. Сколько вызовов деструктора происходит в следующем фрагменте кода? bool fcn(const Sales_data *trans, Sales_data accum) {
    Sales_data item1(*trans), item2(accum); return item1.isbn() != item2.isbn();
    }
    Упражнение 13.13. Наилучший способ изучения функций-членов управления копированием и конструкторов — это определить простой класс с этими функциями-членами, каждая из которых выводит свое имя: struct X {
    X() {std::cout << "X()" << std::endl;}
    X(const X&) {std::cout << "X(const X&)" << std::endl;}
    };
    Добавьте в структуру X оператор присвоения копии и деструктор, а затем напишите программу, использующую объекты класса X различными способами: передайте их как ссылочный и не ссылочный параметры; динамически зарезервируйте их; поместите в
    Page 629/1103
    контейнеры и т.д. Изучайте вывод, пока не начнете хорошо понимать, когда и почему используется каждая функция-член управления копированием. По мере чтения вывода помните, что компилятор может обойти вызовы конструктора копий.
    13.1.4. Правило три/пять
    Как уже упоминалось, существуют три базовых функции, контролирующих копирование объектов класса: конструктор копий, оператор присвоения копии и деструктор. Кроме того, как будет продемонстрировано в разделе 13.6, по новому стандарту класс может также определить конструктор перемещения и оператор присваивания при перемещении.
    Определять все эти функции не обязательно: вполне можно определить один или два из них,
    не определяя все. Эти функции можно считать модулями. Если нужен один, не обязательно определять их все. Классы, нуждающиеся в деструкторах, нуждаются в копировании и присвоении
    Вот эмпирическое правило, используемое при принятии решения о необходимости определения в классе собственных версий функций-членов управления копированием:
    сначала следует решить, нужен ли классу деструктор. Зачастую потребность в деструкторе более очевидна, чем потребность в операторе присвоения или конструкторе копий. Если класс нуждается в деструкторе, он почти наверняка нуждается также в конструкторе копий и операторе присвоения копии.
    Используемый в упражнениях класс HasPtr отлично подойдет для примера (см. раздел
    13.1.1). Этот класс резервирует динамическую память в конструкторе. Синтезируемый деструктор не будет удалять указатель-член. Поэтому данный класс должен определить деструктор для освобождения памяти, зарезервированной конструктором.
    Хоть это и не очевидно, но согласно эмпирическому правилу класс HasPtr нуждается также в конструкторе копий и операторе присвоения копии.
    Давайте посмотрим, что было бы, если бы у класса HasPtr был деструктор и синтезируемые версии конструктора копий и оператора присвоения копии: class HasPtr { public:
    HasPtr(const std::string &s = std::string()): ps(new std::string(s)), i(0) { }
    HasPtr() { delete ps; }
    // ошибка: HasPtr нуждается в конструкторе копий и операторе
    // присвоения копии
    //
    Page 630/1103
    другие члены, как прежде
    };
    В этой версии класса зарезервированная в конструкторе память будет освобождена при удалении объекта класса HasPtr. К сожалению, здесь есть серьезная ошибка! Данная версия класса использует синтезируемые версии операторов копирования и присвоения. Эти функции копируют указатели-члены, а значит, несколько объектов класса HasPtr смогут указывать на ту же область памяти:
    HasPtr f(HasPtr hp) //
    HasPtr передан по значению, поэтому он
    // копируется
    {
    HasPtr ret = hp; // копирует данный HasPtr
    // обработка ret return ret; // ret и hp удаляются
    }
    Когда функция f() завершает работу, объекты hp и ret удаляются и деструктор класса HasPtr выполняется для каждого из них. Этот деструктор удалит указатель-член и в объекте ret, и в объекте hp. Но эти объекты содержат одинаковое значение указателя. Код удалит тот же указатель дважды, что является серьезной ошибкой (см. раздел 12.1.2) с непредсказуемыми результатами.
    Кроме того, вызывающая сторона функции f() может все еще использовать переданный ей объект:
    HasPtr p("some values"); f(p); // по завершении f() память, на которую указывает p.ps,
    // освобождается
    HasPtr q(p); // теперь и p, и q указывают на недопустимую память!
    Память, на которую указывает указатель p (и q), больше недопустима. Она была возвращена операционной системе, когда был удален объект hp (или ret)!
    Page 631/1103

    Если класс нуждается в деструкторе, он почти наверняка нуждается также в операторе присвоения копии и конструкторе копий. Классы, нуждающиеся в копировании, нуждаются также в присвоении, и наоборот
    Хотя большинству классов требуется определить все функции-члены управления копированием (или ни один из них), у некоторых классов есть необходимость только в копировании или присвоении объектов, но нет никакой необходимости в деструкторе.
    В качестве примера рассмотрим класс, присваивающий каждому своему объекту уникальный последовательный номер. Такому классу нужен конструктор копий для создания нового уникального последовательного номера для создаваемого объекта. Этот конструктор копировал бы все остальные переменные-члены заданного объекта. Класс нуждался бы также в собственном операторе присвоения копии, чтобы избежать присвоения объекту слева последовательного номера. Однако у этого класса не было бы никакой потребности в деструкторе.
    Этот пример иллюстрирует второе эмпирическому правило: если класс нуждается в конструкторе копий, то он почти наверняка нуждается в операторе присвоения копии, и наоборот, — если класс нуждается в операторе присвоения, то он почти наверняка нуждается также в конструкторе копий. Однако нужда в конструкторе копий или операторе присвоения копии не означает потребности в деструкторе. Упражнения раздела 13.1.4
    Упражнение 13.14. Предположим, что класс numbered имеет стандартный конструктор,
    создающий уникальный последовательный номер для каждого объекта, который хранится в переменной-члене mysn. Класс numbered использует синтезируемые функции-члены управления копированием и имеет следующую функцию: void f(numbered s) { cout << s.mysn << endl; }
    Какой вывод создаст следующий код? numbered a, b = a, с = b; f(a); f(b); f(c);
    Упражнение 13.15. Предположим, что у класса numbered есть конструктор копий, создающий новый последовательный номер. Изменит ли это вывод вызовов в предыдущем упражнении?

    Если да, то почему? Какой вывод получится?
    Упражнение 13.16. Что если параметром функции f() будет const numbered&? Это изменяет вывод? Если да, то почему? Какой вывод получится?
    Упражнение 13.17. Напишите версии класса numbered и функции f(), соответствующие трем предыдущим упражнениям, и проверьте правильность предсказания вывода.
    13.1.5. Использование спецификатора = default
    Используя спецификатор = default, можно явно указать компилятору на необходимость создать синтезируемые версии функций-членов управления копированием (см. раздел 7.1.4): class Sales_data { public:
    Page 632/1103

    // управление копированием; версии по умолчанию
    Sales_data() = default;
    Sales_data(const Sales_data&) = default;
    Sales_data& operator=(const Sales_data &);
    Sales_data() = default;
    // другие члены как прежде
    };
    Sales_data& Sales_data::operator=(const Sales_data&) = default;
    Когда в объявлении функции-члена в теле класса использован спецификатор = default,
    синтезируемая функция неявно становится встраиваемой (как и любая другая функция-член,
    определенная в теле класса). Если синтезируемая функция-член класса не должна быть встраиваемой функцией, можно добавить часть = default в ее определение, как это было сделано в определении оператора присвоения копии.
    Спецификатор = default можно использовать только для тех функций-членов, у которых есть синтезируемая версия (т.е. стандартный конструктор или функция-член управления копированием).
    13.1.6. Предотвращение копирования
    Большинство классов должно определить (явно или неявно) стандартный конструктор,
    конструктор копий и оператор присвоения копии.
    Хотя большинство классов должно определять (и, как правило, определяет) конструктор копий и оператор присвоения копии, у некоторых классов нет реальной необходимости в этих функциях. В таких случаях класс должен быть определен так, чтобы предотвращать копирование и присвоение. Например, классы iostream предотвращают копирование, чтобы не позволять нескольким объектам писать или читать из того же буфера ввода-вывода.
    Казалось бы, предотвратить копирование можно, и не определяя функции-члены управления копированием. Но эта стратегия не сработает: если класс не определит эти функции, то компилятор синтезирует их сам. Определение функции как удаленной
    По новому стандарту можно предотвратить копирование, определив конструктор копий и оператор присвоения копии как удаленные функции (deleted function). Удаленной называется функция, которая была объявлена, но не может использована никаким другим способом. Чтобы определить функцию как удаленную, за списком ее параметров следует расположить часть = delete: struct NoCopy {
    NoCopy() = default; //
    Page 633/1103
    использовать синтезируемый стандартный
    // конструктор
    NoCopy(const NoCopy&) = delete; // без копирования
    NoCopy &operator=(const NoCopy&) = delete; // без присвоения
    NoCopy() = default; // используйте синтезируемый деструктор
    // другие члены
    };
    Часть = delete указывает компилятору (и читателям кода), что эти функции-члены не определяются преднамеренно .
    В отличие от части = default, часть = delete должна присутствовать в первом объявлении удаленной функции. Это различие согласуется со смыслом данных объявлений. Часть =
    default влияет только на то, какой код создает компилятор; следовательно, она необходима,
    только пока компилятор не создаст код. С другой стороны, компилятор должен знать, что функция удалена, и запретить ее использование другими функциями.
    Также в отличие от части = default, часть = delete можно применить для любой функции (=
    default применима только к стандартному конструктору или функции-члену управления копированием, которую компилятор может синтезировать). Хотя изначально удаленные функции предназначались для подавления функций-членов управления копированием, они иногда применимы также для воздействия на процесс подбора функции. Деструктор не должен быть удаленной функцией-членом
    Следует заметить, что удалять деструктор нельзя. Если его удалить, то не будет никакого способа освободить объект этого типа. Компилятор не позволит определять переменные или создавать временные объекты типа, у которого удален деструктор. Кроме того, нельзя определять переменные или временные объекты класса, обладающего членом, у типа которого удален деструктор. Если у переменной-члена класса удален деструктор, то она не может быть освобождена. Если не может быть удалена переменная-член, не может быть удален и весь объект в целом.
    Хотя определить переменные или переменные-члены таких типов нельзя, вполне можно динамически резервировать объекты с удаленным деструктором. Однако впоследствии их нельзя будет освободить: struct NoDtor {
    NoDtor() = default; //
    Page 634/1103
    использовать синтезируемый стандартный
    // конструктор
    NoDtor() = delete; // нельзя удалять объекты типа NoDtor
    };
    NoDtor nd; // ошибка: у NoDtor удаленный деструктор
    NoDtor *p = new NoDtor(); // ok: но нельзя удалить p delete p; // ошибка: у NoDtor удаленный деструктор
    Невозможно определить объект или удалить указатель на динамически созданный объект типа с удаленным деструктором. Функции-члены управления копированием могут быть синтезированы как удаленные
    Как уже упоминалось, если не определены функции-члены управления копированием,
    компилятор определит их сам. Аналогично, если класс не определяет конструктор,
    компилятор синтезирует стандартный конструктор для этого класса сам (см. раздел 7.1.4).
    Для некоторых классов компилятор определяет эти синтезируемые функции-члены как удаленные.
    • Синтезируемый деструктор определяется как удаленный, если у класса есть переменная-член, собственный деструктор которой удален или недоступен (например,
    private).
    • Синтезируемый конструктор копий определяется как удаленный, если у класса есть переменная-член, собственный деструктор которой удален или недоступен. Он также будет удаленным, если у класса есть переменная-член с удаленным или недоступным деструктором.
    • Синтезируемый оператор присвоения копии определяется как удаленный, если у класса есть переменная-член с удаленным или недоступным оператором присвоения копии, либо если у класса есть константный или ссылочный член.
    • Синтезируемый стандартный конструктор определяется как удаленный, если у класса есть переменная-член с удаленным или недоступным деструктором; или имеется ссылочный член без внутриклассового инициализатора (см. раздел 2.6.1); или есть константная переменная-член, тип которой не определяет стандартный конструктор явно и не имеет внутриклассового инициализатора.
    Короче говоря, эти правила означают, что если у класса есть переменная-член, которая не может быть стандартно создана, скопирована, присвоена или удалена, то соответствующая функция-член класса будет удаленной функцией.
    Как ни удивительно, но переменная-член, класс которой имеет удаленный или недоступный
    Page 635/1103
    деструктор, приводит к определению синтезируемого стандартного конструктора копий удаленным. Основание для этого правила в том, что без него возможно создание объектов,
    которые невозможно удалить.
    Однако в том, что компилятор не будет синтезировать стандартный конструктор для класса со ссылочным или с константным членом, который не может быть создан стандартно, ничего удивительного нет. Нет ничего удивительного и в том, что класс с константным членом не может использовать синтезируемый оператор присвоения копии: в конце концов, этот оператор пытается присвоить значения всем членам классов. Однако присвоить новое значение константному объекту невозможно.
    Хотя вполне возможно присвоить новое значение ссылке, это изменит значение объекта, на который она ссылается. Если бы оператор присвоения копии синтезировался для таких классов, то левый операнд продолжил бы ссылаться на тот же объект, что и перед присвоением. Он не ссылался бы на тот же объект, что и правый операнд. Поскольку это поведение вряд ли будет желательно, синтезируемый оператор присвоения копии определяется как удаленный, если у класса есть ссылочный член.
    Как будет продемонстрировано в разделах 13.6.2, 15.7.2 и 19.6, есть и другие аспекты, в связи с которыми функции-члены копирования могут быть определены как удаленные.
    Как правило, функции-члены управления копированием синтезируются как удаленные, когда невозможно скопировать, присвоить или удалить член класса. Закрытые функции управления копированием
    До появления нового стандарта классы предотвращали копирование, объявляя свой конструктор копий и оператор присвоения копии как закрытые (private): class PrivateCopy {
    // нет спецификатора доступа; следующие члены являются закрытыми
    // по умолчанию; см. p. 7.2
    // функции управления копированием закрыты, а потому недоступны
    // обычному пользовательскому коду
    PrivateCopy(const PrivateCopy&);
    PrivateCopy &operator=(const PrivateCopy&);
    // другие члены public:
    PrivateCopy() = default; // использовать синтезируемый стандартный
    Page 636/1103

    // конструктор
    PrivateCopy(); // пользователи могут определять объекты этого типа,
    // но не копировать их
    };
    Поскольку деструктор является открытым (public), пользователи смогут определять объекты класса PrivateCopy. Но так как конструктор копий и оператор присвоения копии являются закрытыми (private), пользовательский код не сможет копировать такие объекты. Но дружественные классы и члены класса вполне могут создавать копии. Чтобы предотвратить копирование и друзьями, и членами класса, эти функции-члены объявляют закрытыми и не определяют их.
    За одним исключением, рассматриваемым в разделе 15.2.1, вполне допустимо объявлять, но не определять функции-члены (см. раздел 6.1.2). Попытка использования неопределенной функции-члена приведет к отказу во время компоновки. При объявлении (без определения)
    закрытого конструктора копий можно предотвратить любую попытку скопировать объект класса: пользовательский код, пытающийся сделать копию, будет помечен как ошибочный во время компиляции; попытки копирования в функциях-членах или дружественных классах будут отмечены как ошибка во время редактирования.
    Для классов, которые должны предотвратить копирование, следует определить собственный конструктор копий и оператор присвоения копии, используя часть = delete вместо объявления их закрытыми. Упражнения раздела 13.1.6
    Упражнение 13.18. Определите класс Employee, содержащий имя сотрудника и его уникальный идентификатор. Снабдите класс стандартным конструктором и конструктором,
    получающим строку, представляющую имя сотрудника. Каждый конструктор должен создавать уникальный идентификатор за счет приращения статической переменной-члена.
    Упражнение 13.19. Должен ли класс Employee определить собственные версии функций-членов управления копированием? Если да, то почему? Если нет, то тоже почему?
    Реализуйте все члены управления копированием, в которых, на ваш взгляд, нуждается класс
    Employee.
    Упражнение 13.20. Объясните, что происходит при копировании, присвоении и удалении объектов классов TextQuery и QueryResult из раздела 12.3.
    Упражнение 13.21. Должны ли классы TextQuery и QueryResult определять собственные версии функций-членов управления копированием? Если да, то почему? Если нет, то почему?
    Реализуйте функции управления копированием, необходимые, по-вашему, в этих классах.
    13.2. Управление копированием и ресурсами
    Обычно классы, управляющие ресурсами, расположенными вне его, должны определять
    Page 637/1103
    функции-члены управления копированием. Как упоминалось в разделе 13.6, такие классы нуждаются в деструкторах, освобождающих зарезервированные объектом ресурсы. Если класс нуждается в деструкторе, он почти наверняка нуждается также в конструкторе копий и операторе присвоения копии.
    Чтобы определить эти функции-члены, сначала следует решить, что будет означать копирование объекта данного типа. Вообще, есть два способа: операцию копирования можно определить так, чтобы класс вел себя, как значение или как указатель.
    У классов, которые ведут себя, как значения, есть собственное состояние. При копировании объекта как значения копия и оригинал независимы друг от друга. Внесенные в копию изменения никак не влияют на оригинал, и наоборот.
    Классы, действующие как указатели, используют состояние совместно. При копировании объектов таких классов копии и оригиналы используют те же данные. Изменения, внесенные в копии, изменяют также оригинал, и наоборот.
    Из использованных ранее библиотечных классов поведением, подобным значениям,
    обладали классы библиотечных контейнеров и класс string. Ничего удивительного, что класс shared_ptr демонстрирует поведение, подобное указателю, как и класс StrBlob (см. раздел
    12.1.1). Типы ввода-вывода и класс unique_ptr не допускают ни копирования, ни присвоения,
    поэтому их поведение не похоже ни на значение, ни на указатель.
    Чтобы проиллюстрировать эти два подхода, определим для используемого в упражнениях класса HasPtr функции-члены управления копированием. Сначала заставим класс действовать, как значение, а затем повторно реализуем его в версии, ведущей себя, как указатель.
    У класса HasPtr есть два члена типа int и указатель на тип string. Обычно классы непосредственно копируют переменные-члены встроенного типа (кроме указателей); такие члены являются значениями, а следовательно, ведут себя обычно, как значения.
    Происходящее при копировании указателя-члена определяет то, должно ли у такого класса,
    как HasPtr, быть поведение, подобное значению или указателю. Упражнения раздела 13.2
    Упражнение 13.22. Предположим, класс HasPtr должен вести себя, как значение. Таким образом, у каждого его объекта должна быть собственная копия строки, на которую указывает объект. Определения функций-членов управления копированием рассматривается в следующем разделе, но уже сейчас известно все необходимое для их реализации. Напишите конструктор копий класса HasPtr и оператор присвоения копии прежде, чем продолжите чтение.
    13.2.1. Классы, действующие как значения
    Для обеспечения поведения, подобного значению, у каждого объекта должна быть собственная копия ресурса, которым управляет класс. Это значит, что у каждого объекта класса HasPtr должна быть собственная копия строки, на которую указывает указатель ps.
    Для реализации поведения, подобного значению, классу HasPtr нужно следующее.
    • Конструктор копий, который копирует строку, а не только указатель.
    • Деструктор, освобождающий строку.
    • Оператор присвоения копии, освобождающий строку существующего объекта и копирующий
    Page 638/1103
    ее значение в строку правого операнда.
    Вот подобная значению версия класса HasPtr: class HasPtr { public:
    HasPtr(const std::string &s = std::string()): ps(new std::string(s)), i(0) { }
    // у каждого объекта класса HasPtr есть собственный экземпляр строки,
    // на которую указывает указатель ps
    HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) { }
    HasPtr& operator=(const HasPtr &);
    HasPtr() { delete ps; } private: std::string *ps; int i;
    };
    Класс достаточно прост, все, кроме оператора присвоения, определено в теле класса.
    Первый конструктор получает (необязательный) аргумент типа string. Он динамически резервирует собственную копию этой строки и сохраняет ее адрес в указателе ps.
    Конструктор копий также резервирует собственный экземпляр строки. Деструктор освобождает память, зарезервированную ее конструкторами, выполняя оператор delete для указателя-члена ps. Подобный значению оператор присвоения копии
    Обычно операторы присвоения объединяют действия деструктора и конструктора копий.
    Подобно деструктору, оператор присвоения освобождает ресурсы левого операнда. Подобно конструктору копий, оператор присвоения копирует данные из правого операнда. Однако критически важно, чтобы эти действия осуществлялись в правильной последовательности,
    даже если объект присваивается сам себе. Кроме того, по возможности следует писать собственные операторы присвоения так, чтобы они оставляли левый операнд в корректном состоянии, иначе произойдет исключение (см. раздел 5.6.2).
    В данном случае можно отработать случай присвоения самому себе (и сделать код устойчивым к исключению), осуществляя сначала копирование правого операнда. После копирования освобождается левый операнд и указатель модифицируется так, чтобы он указывал на вновь зарезервированную строку:
    HasPtr& HasPtr::operator=(const HasPtr &rhs) {
    Page 639/1103
    auto newp = new string(*rhs.ps); // скопировать строку delete ps; // освободить прежнюю память ps = newp; // копировать данные из rhs в этот объект i = rhs.i; return *this; // возвратить этот объект
    };
    В этом операторе присвоения, безусловно, сначала выполняется работа конструктора:
    инициализатор newp идентичен инициализатору ps в конструкторе копий класса HasPtr.
    Затем, как в деструкторе, удаляется строка, на которую в настоящее время указывает указатель ps. Остается только скопировать указатель на недавно созданную строку и значение типа int из rhs в этот объект. Ключевая концепция. Операторы присвоения
    Создавая оператор присвоения, следует учитывать два момента.
    • Операторы присвоения должны работать правильно, если объект присваивается сам себе.
    • Большинство операторов присвоения делят работу с деструктором и конструктором копий.
    Шаблон разработки оператора присвоения подразумевает сначала копирование правого операнда в локальный временный объект.
    После копирования вполне безопасно удалить существующие члены левого операнда. Как только левый операнд будет освобожден, копировать данные из временного объекта в переменные-члены левого операнда.
    Для иллюстрации важности принятия мер против присвоения самому себе рассмотрим, что случилось бы, выгляди оператор присвоения так:
    //
    НЕПРАВИЛЬНЫЙ способ написания оператора присвоения!
    HasPtr&
    HasPtr::operator=(const HasPtr &rhs) { delete ps; // освобождает строку, на которую указывает этот объект
    // если rhs и *this - тот же объект, произойдет копирование удаленной
    //
    Page 640/1103
    памяти! ps = new string(*(rhs.ps)); i = rhs.i; return *this;
    }
    Если rhs и этот объект совпадают, удаление ps освободит строку, на которую указывают и
    *this, и rhs. При попытке копирования *(rhs.ps) в операторе new этот указатель указывает уже на недопустимую область памяти. Результат непредсказуем.
    Для операторов присвоения критически важно работать правильно, даже если объект присваивается сам себе. Проще всего обеспечить это, скопировав правый операнд перед удалением левого. Упражнения раздела 13.2.1
    Упражнение 13.23. Сравните функции-члены управления копированием, написанные для решения упражнений предыдущего раздела, с кодом, представленным здесь. Убедитесь, что понимаете различия, если таковые вообще есть, между вашим кодом и приведенным в книге.
    Упражнение 13.24. Что будет, если в версии класса HasPtr данного раздела не определен деструктор? Что если не определен конструктор копий?
    Упражнение 13.25. Предположим, необходимо определить версию класса StrBlob,
    действующего как значение. Предположим также, что необходимо продолжить использовать указатель shared_ptr, чтобы класс StrBlobPtr все еще мог использовать указатель weak_ptr для вектора. Переделанный класс будет нуждаться в конструкторе копий и операторе присвоения копии, но не в деструкторе. Объясните, что должны делать конструктор копий и оператор присвоения копий. Объясните, почему класс не нуждается в деструкторе.
    Упражнение 13.26. Напишите собственную версию класса StrBlob, описанного в предыдущем упражнении.
    13.2.2. Определение классов, действующих как указатели
    Чтобы класс HasPtr действовал как указатель, конструктор копий и оператор присвоения копии должны копировать указатель-член, а не строку, на которую он указывает. Класс все еще будет нуждаться в собственном деструкторе, чтобы освободить память,
    зарезервированную получающим строку конструктором (см. раздел 13.6). Тем не менее в данном случае деструктор не может односторонне освободить связанную с ним строку. Это можно сделать только тогда, когда исчезнет последний указатель на строку.
    Простейший способ заставить класс действовать как указатель — это использовать указатель shared_ptr для управления ресурсами в классе. При копировании (или присвоении)
    копируется (или присваивается) указатель shared_ptr. Класс shared_ptr сам отслеживает количество пользователей, совместно использующих объект, на который он указывает. Когда пользователей больше нет, класс shared_ptr освобождает ресурс.
    Но иногда управлять ресурсом следует непосредственно. В таких случаях может пригодиться счетчик ссылок (reference count) (см. раздел 12.1.1). Для демонстрации работы счетчика
    Page 641/1103
    ссылок переопределим класс HasPtr так, чтобы обеспечить поведение, подобное указателю,
    но с использованием собственного счетчика ссылок.Счетчики ссылок
    Счетчик ссылок работает следующим образом.
    • В дополнение к инициализации объекта каждый конструктор (кроме конструктора копий)
    создает счетчик. Этот счетчик отслеживает количество объектов, совместно использующих создаваемые данные. Сразу после создания объект только один, поэтому счетчик инициализируется значением 1.
    • Конструктор копий не создает новый счетчик; он копирует переменные-члены переданного ему объекта, включая счетчик. Конструктор копий увеличивает значение этого совместно используемого счетчика, указывая на наличие еще одного пользователя данных этого объекта.
    • Деструктор уменьшает значение счетчика, указывая, что стало на одного пользователя совместно используемых данных меньше. Если значение счетчика достигает нуля,
    деструктор удаляет данные.
    • Оператор присвоения копии увеличивает счетчик правого операнда и уменьшает счетчик левого. Если счетчик левого операнда достигает нуля, значит, пользователей больше нет. В
    данном случае оператор присвоения копии должен удалить данные левого операнда.
    Единственное затруднение — это решить, где разместить счетчик ссылок. Счетчик не может быть членом непосредственно класса объекта HasPtr. Чтобы убедиться почему, рассмотрим происходящее в следующем примере:
    HasPtr p1("Hiya!");
    HasPtr p2(p1); // p1 и p2 указывают на ту же строку
    HasPtr p3(p1); // p1, p2 и p3 указывают на ту же строку
    Если счетчик ссылок будет храниться в каждом объекте, то как модифицировать его правильно при создании объекта p3? Можно увеличить счетчик в объекте p1 и скопировать счет в p3, но как модифицировать счетчик в p2?
    Один из способов решения этой проблемы в том, чтобы хранить счетчик в динамической памяти. При создании объекта резервируется также и новый счетчик. При копировании или присвоении объекта копируется и указатель на счетчик. Таким образом, и копия, и оригинал укажут на тот же счетчик. Определение класса счетчика ссылок
    Используя счетчик ссылок, можно написать подобную указателю версию класса HasPtr следующим образом: class HasPtr { public:
    // конструктор резервирует новую строку и новый счетчик,
    //
    Page 642/1103
    устанавливаемый в 1
    HasPtr(const std::string &s = std::string()): ps(new std::string(s)), i(0), use(new std::size_t(1)) {}
    // конструктор копий копирует все три переменные-члена и увеличивает
    // счетчик
    HasPtr(const HasPtr &p): ps(p.ps), i(p.i), use(p.use) { ++*use; }
    HasPtr& operator=(const HasPtr&);
    HasPtr(); private: std::string *ps; int i; std::size_t *use; // член, отслеживающий количество объектов,
    // совместно использующих *ps
    };
    Здесь была добавлена новая переменная-член use, отслеживающая количество объектов,
    совместно использующих ту же строку. Получающий строку конструктор резервирует счетчик и инициализирует его значением 1, означающим наличие одного пользователя строкового члена класса этого объекта. Функции-члены копирования подобного указателю класса используют счетчик ссылок
    При копировании или присвоении объектов класса HasPtr необходимо, чтобы копия и оригинал указывали на ту же строку. Таким образом, когда копируется объект класса HasPtr,
    копируется сам указатель ps, а не строка, на которую он указывает. При копировании увеличивается также счетчик, связанный с этой строкой.
    Конструктор копий (определенный в классе) копирует все три члена переданного ему объекта класса HasPtr. Этот конструктор увеличивает также значение указателя-члена use, означая,
    что у строки, на которую указывают указатели ps и p.ps, появился другой пользователь.
    Деструктор не может безоговорочно удалить указатель ps, поскольку могли бы быть и другие объекты, указывающие на ту же область памяти. Вместо этого деструктор осуществляет декремент счетчика ссылок, означая, что строку совместно используют на один объект меньше. Если счетчик достигает нуля, деструктор освобождает память, на которую указывают указатели ps и use:
    Page 643/1103

    HasPtr::HasPtr() { if (--*use == 0) { // если счетчик ссылок достиг 0, delete ps; // удалить строку delete use; // и счетчик
    }
    }
    Оператор присвоения копии, как обычно, выполняет действия, общие для конструктора копий и деструктора. Таким образом, оператор присвоения должен увеличить счетчик правого операнда (действие конструктора копий) и декремент счетчика левого операнда, освобождая по мере необходимости используемую память (действие деструктора).
    Кроме того, как обычно, оператор должен учитывать присвоение себя самому. Для этого инкремент счетчика rhs осуществляется прежде декремента счетчика в левом операнде.
    Таким образом, если оба операнда являются тем же объектом, значение счетчика будет увеличено прежде проверки необходимости удаления указателей ps и use:
    HasPtr& HasPtr::operator=(const HasPtr &rhs) {
    ++*rhs.use; // инкремент счетчика пользователей правого операнда if (--*use == 0) { // затем декремент счетчика этого объекта delete ps; // если никаких других пользователей нет delete use; // освободить резервированные члены этого объекта
    } ps = rhs.ps; // копировать данные из rhs в этот объект i = rhs.i; use = rhs.use; return *this; //
    Page 644/1103
    возвратить этот объект
    } Упражнения раздела 13.2.2
    Упражнение 13.27. Определите собственную версию класса HasPtr со счетчиком ссылок.
    Упражнение 13.28. С учетом следующих классов реализуйте стандартный конструктор и необходимые функции-члены управления копированием.
    (a) class TreeNode { (b) class BinStrTree { private: private: std::string value; TreeNode *root; int count; };
    TreeNode *left;
    TreeNode *right;
    };
    13.3. Функция swap()
    Кроме функций-членов управления копированием, управляющие ресурсами классы зачастую определяют также функцию swap() (см. раздел 9.2.5). Определение функции swap() особенно важно для классов, которые планируется использовать с алгоритмами переупорядочивания элементов (см. раздел 10.2.3). Такие алгоритмы вызывают функцию swap() всякий раз, когда им нужен обмен двух элементов.
    Если класс определяет собственную функцию swap(), алгоритм использует именно ее. В
    противном случае используется функция swap(), определенная библиотекой. Как обычно,
    хоть мы пока и не знаем, как реализуется функция swap(), концептуально несложно заметить,
    что обмен двух объектов задействует копирование и два присвоения. Например, код обмена двух объектов подобного значению класса HasPtr (см. раздел 13.2.1) мог бы выглядеть так:
    HasPtr temp = v1; // сделать временную копию значения v1 v1 = v2; // присвоить значение v2 объекту v1 v2 = temp; // присвоить сохраненное значение v1 объекту v2
    Этот код дважды копирует строку, которая первоначально принадлежала объекту v1: один раз, когда конструктор копий класса HasPtr копирует объект v1 в объект temp, и второй раз,
    когда оператор присвоения присваивает объект temp объекту v2. Он также копирует строку,
    которая первоначально принадлежала объекту v2, когда объект v2 присваивается объекту v1.
    Как уже упоминалось, копирование объекта, подобного значению класса HasPtr, резервирует
    Page 645/1103
    новую строку и копирует строку, на которую указывает объект класса HasPtr.
    В принципе ни одно из этих резервирований памяти не обязательно. Вместо того чтобы резервировать новые копии строки, можно было бы обменять указатели. Таким образом,
    имело бы смысл обменять два объект класса HasPtr так, чтобы выполнить следующее: string *temp = v1.ps; // создать временную копию указателя в v1.ps v1.ps = v2.ps; // присвоить указатель v2.ps указателю v1.ps v2.ps = temp; // присвоить сохраненный указатель v1.ps
    // указателю v2.ps Написание собственной функции swap()
    Переопределить стандартное поведение функции swap() можно, определив в классе ее собственную версию. Вот типичная реализация функции swap(): class HasPtr { friend void swap(HasPtr&, HasPtr&);
    // другие члены, как в разделе 13.2.1
    }; inline void swap(HasPtr &lhs, HasPtr &rhs) { using std::swap; swap(lhs.ps, rhs.ps); // обмен указателями, а не строковыми данными swap(lhs.i, rhs.i); // обмен целочисленными членами
    }
    Все начинается с объявления функции swap(), дружественной, чтобы предоставить ей доступ к закрытым переменным-членам класса HasPtr. Поскольку функция swap() предназначена для оптимизации кода, определим ее как встраиваемую (см. раздел 6.5.2). Тело функции swap()
    вызывает функции swap() каждой из переменных-членов заданного объекта. В данном случае сначала обмениваются указатели, а затем целочисленные члены объектов, связанных с параметрами rhs и lhs.
    В отличие от функций-членов управления копированием, функция swap() никогда не бывает
    Page 646/1103
    обязательной. Однако ее определение может быть важно для оптимизации классов,
    резервирующих ресурсы. Функции swap() должны вызвать функции swap(), а не std::swap()
    В этом коде есть один важный нюанс: хотя в данном случае это не имеет значения, важно,
    чтобы функция swap() вызвала именно функцию swap(), а не std::swap(). В классе HasPtr переменные-члены имеют встроенные типы. Для встроенных типов нет специализированных версий функции swap(). В данном случае она вызывает библиотечную функцию std::swap().
    Но если класс имеет член, тип которого обладает собственной специализированной функцией swap(), то вызов функции std::swap() был бы ошибкой. Предположим, например, что есть другой класс по имени Foo, переменная-член h которого имеет тип HasPtr. Если не написать для класса Foo собственную версию функции swap(), то будет использована ее библиотечная версия. Как уже упоминалось, библиотечная функция swap() осуществляет ненужное копирование строк, управляемых объектами класса HasPtr.
    Ненужного копирования можно избежать, написав функцию swap() для класса Foo. Но версию функции swap() для класса Foo можно написать так: void swap(Foo &lhs, Foo &rhs) {
    //
    Ошибка: эта функция использует библиотечную версию
    // функции swap(), а не версию класса HasPtr std::swap(lhs.h, rhs.h); // обменять другие члены класса Foo
    }
    Этот код нормально компилируется и выполняется. Однако никакого различия в производительности между этим кодом и просто использующим стандартную версию функции swap() не будет. Проблема в том, что здесь явно запрошен вызов библиотечной версии функции swap(). Однако нужна версия функции не из пространства имен std, а определенная в классе HasPtr.
    Правильный способ написания функции swap() приведен ниже. void swap(Foo &lhs, Foo &rhs) { using std::swap; swap(lhs.h, rhs.h); // использует функцию swap() класса HasPtr
    // обменять другие члены класса Foo
    }
    Все вызовы функции swap() обходятся без квалификаторов. Таким образом, каждый вызов должен выглядеть как swap(), а не std::swap(). По причинам, рассматриваемым в разделе
    Page 647/1103

    16.3, если есть специфическая для типа версия функции swap(), она будет лучшим соответствием, чем таковая из пространства имен std. В результате, если у типа есть специфическая версия функции swap(), вызов swap() будет распознан как относящийся к специфической версии. Если специфической для типа версии нет, то (с учетом объявления using для функции swap() в области видимости) при вызове swap() будет использована версия из пространства имен std.
    У очень осторожных читателей может возникнуть вопрос: почему объявление using функции swap() не скрывает объявление функции swap() класса HasPtr (см. раздел 6.4.1). Причины, по которым работает этот код, объясняются в разделе 18.2.3. Использование функции swap() в операторах присвоения
    Классы, определяющие функцию swap(), зачастую используют ее в определении собственного оператора присвоения. Эти операторы используют технологию, известную как копия и обмен (copy and swap)). Она подразумевает обмен левого операнда с копией правого:
    // обратите внимание: параметр rhs передается по значению. Это значит,
    // что конструктор копий класса HasPtr копирует строку в правый
    // операнд rhs
    HasPtr& HasPtr::operator=(HasPtr rhs) {
    // обменивает содержимое левого операнда с локальной переменной rhs swap(*this, rhs); // теперь rhs указывает на память, которую
    // использовал этот объект return *this; // удаление rhs приводит к удалению указателя в rhs
    }
    В этой версии оператора присвоения параметр не является ссылкой. Вместо этого правый операнд передается по значению. Таким образом, rhs — это копия правого операнда.
    Копирование объекта класса HasPtr приводит к резервированию новой копии строки данного объекта.
    Page 648/1103

    В теле оператора присвоения вызывается функция swap(), обменивающая переменные-члены rhs с таковыми в *this. Этот вызов помещает указатель, который был в левом операнде, в rhs, и указатель, который был в rhs,— в *this. Таким образом, после вызова функции swap() указатель-член в *this указывает на недавно зарезервированную строку,
    являющуюся копией правого операнда.
    По завершении оператора присвоения параметр rhs удаляется и выполняется деструктор класса HasPtr. Этот деструктор освобождает память, на которую теперь указывает rhs,
    освобождая таким образом память, на которую указывал левый операнд.
    В этой технологии интересен тот момент, что она автоматически отрабатывает присвоение себя себе и изначально устойчива к исключениям. Копирование правого операнда до изменения левого отрабатывает присвоение себя себе аналогично примененному в нашем первоначальном операторе присвоения (см. раздел 13.2.1). Это обеспечивает устойчивость к исключениям таким же образом, как и в оригинальном определении. Единственный код,
    способный передать исключение, — это оператор new в конструкторе копий. Если исключение произойдет, то это случится прежде, чем изменится левый операнд.
    Операторы присвоения, использующие копию и обмен, автоматически устойчивы к исключениям и правильно отрабатывают присвоение себя себе. Упражнения раздела 13.3
    Упражнение 13.29. Объясните, почему вызов функции swap() в вызове swap(HasPtr&,
    HasPtr&) не приводит к бесконечной рекурсии.
    Упражнение 13.30. Напишите и проверьте функцию swap() для подобной значению версии класса HasPtr. Снабдите свою функцию swap() оператором вывода примечания о ее выполнении.
    Упражнение 13.31. Снабдите свой класс оператором < и определите вектор объектов класса HasPtr. Вставьте в вектор несколько элементов, а затем отсортируйте его (sort()).
    Обратите внимание на то, когда вызывается функция swap().
    Упражнение 13.32. Получит ли преимущества подобная указателю версия класса HasPtr от определения собственной функции swap()? Если да, то в чем это преимущество? Если нет, то почему?
    13.4. Пример управления копированием
    Несмотря на то что управление копированием обычно необходимо для классов,
    резервирующих ресурсы, управление ресурсами не единственная причина определения этих функций-членов. У некоторых классов может быть необходимость в учете или других действиях, выполняемых функциями управления копированием.
    В качестве примера, нуждающегося в управлении копированием класса для учета,
    рассмотрим два класса, которые могли бы использоваться в приложении обработки почты.
    Эти классы, Message и Folder, представляют соответственно сообщение электронной (или другой) почты и каталог, в котором могло бы находиться это сообщение. Каждое сообщение может находиться в нескольких папках. Но может существовать только одна копия содержимого любого сообщения. Таким образом, если содержимое сообщения изменится,
    эти изменения отображаются при просмотре данного сообщения в любой из папок.
    Для отслеживания того, какие сообщения в каких папках находятся, каждый объект класса
    Page 649/1103

    Message будет хранить набор указателей на объекты класса Folder, в которых они присутствуют, а каждый объект класса Folder будет содержать набор указателей на его объекты класса Message. Эту конструкцию иллюстрирует рис. 13.1.
    Рис. 13.1. Проект классов Message и Folder
    Класс Message будет предоставлять функции save() и remove() для добавления и удаления сообщений из папки. Для создания нового объекта класса Message следует определить содержимое сообщения, но не папку. Чтобы поместить сообщение в определенную папку,
    следует вызвать функцию save().
    После копирования сообщения копия и оригинал будут разными объектами класса Message,
    но оба сообщения должны присутствовать в том же самом наборе папок. Таким образом,
    копирование сообщения скопирует содержимое и набор указателей на папку. Он должен также добавить указатель на недавно созданный объект класса Message к каждому из этих объектов класса Folder.
    После удаления сообщения объект класса Message больше не существует. Поэтому его удаление должно удалять указатели на этот объект класса Message из всех объектов класса
    Folder, которые содержали это сообщение.
    Когда один объект класса Message присваивается другому, содержимое (contents) левого сообщения заменяется таковым правого. Следует также модифицировать набор папок,
    удалив левый объект класса Message из предыдущих объектов класса Folder и добавив в них правый.
    Глядя на этот список операций, можно заметить, что и деструктор, и оператор присвоения копии должны удалять заданное сообщение из папок, которые указывают на него. Точно так же и конструктор копий, и оператор присвоения копии добавляют объект класса Message в заданный список объекта класса Folder. Для решения этих задач определим пару закрытых вспомогательных функций.
    Оператор присвоения копии зачастую осуществляет ту же работу, которая необходима в конструкторе копий и деструкторе. В таких случаях эти действия обычно помещают в закрытые вспомогательные функции.
    Класс Folder будет нуждаться в аналогичных функциях-членах управления копированием для добавления и удаления себя из хранящих их объектов класса Message.
    Проектирование и реализацию класса Folder оставим читателю в качестве самостоятельного упражнения, но будем подразумевать, что у него есть функции-члены addMsg() и remMsg(),
    выполняющие все действия по добавлению и удалению заданного сообщения из набора сообщений указанной папки. Класс Message
    С учетом проекта выше можно написать класс Message следующим образом: class Message { friend class Folder; public:
    // папки неявно инициализируются пустым набором explicit Message(const std::string &str = ""):
    Page 650/1103
    contents(str) { }
    // функции управления копированием, контролирующие указатели на
    // это сообщение
    Message(const Message&); // конструктор копий
    Message& operator=(const Message&); // присвоение копии
    Message(); // деструктор
    // добавить/удалить это сообщение из набора сообщений папки void save(Folder&); void remove(Folder&); private: std::string contents; // фактический текст сообщения std::set<Folder*> folders; // папки, содержащие это сообщение
    // вспомогательные функции, используемые конструктором копий,
    // оператором присвоения и деструктором
    // добавить это сообщение в папки, на которые указывает параметр void add_to_Folders(const Message&);
    // удалить это сообщение из каждой папки в folders void remove_from_Folders();
    };
    Page 651/1103

    Класс определяет две переменные-члена: contents — для хранения текста сообщения и folders — для хранения указателей на объекты класса Folder, в которых присутствует данное сообщение. Получающий строку конструктор копирует ее в переменную contents и (неявно)
    инициализирует переменную folders пустым набором. Поскольку у этого конструктора есть аргумент по умолчанию, он также является стандартным конструктором класса Message (см.
    раздел 7.5.1). Функции-члены save() и remove()
    Кроме функций управления копированием, у класса Message есть только две открытых функции-члена: save(), помещающая сообщение в данную папку, и remove(), извлекающая его: void Message::save(Folder &f) { folders.insert(&f); // добавить данную папку в список папок f.addMsg(this); // добавить данное сообщение в набор сообщений
    } void Message::remove(Folder &f) { folders.erase(&f); // удалить данную папку из списка папок f.remMsg(this); // удалить данное сообщение из набора сообщений
    }
    Чтобы сохранить (или удалить) сообщение, требуется модифицировать член folders класса
    Message. При сохранении сообщения сохраняется указатель на данный объект класса Folder;
    при удалении сообщения этот указатель удаляется.
    Эти функции должны также модифицировать заданный объект класса Folder. Модификация этого объекта является задачей, контролируемой классом Folder при помощи функций-членов addMsg() и remMsg(), которые добавляют или удаляют указатель на данный объект класса
    Message соответственно. Управление копированием класса Message
    При копировании сообщения копия должна появляться в тех же папках, что и оригинальное сообщение. В результате необходимо перебрать набор указателей класса Folder, добавляя указатель на новое сообщение в каждую папку, на которую указывал оригинал сообщения.
    Для этого и конструктор копий, и оператор присвоения копии должны будут выполнять те же действия, поэтому определим функцию для этой общей работы:
    // добавить это сообщение в папки, на которые указывает m void Message::add_to_Folders(const Message &m) { for (auto f : m.folders) //
    Page 652/1103
    для каждой папки, содержащей m, f->addMsg(this); // добавить указатель на это сообщение
    // в данную папку
    }
    Здесь происходит вызов функции addMsg() для каждого объекта класса Folder в m.folders.
    Функция addMsg() добавит указатель на этот объект класса Message в данный объект класса
    Folder.
    Конструктор копий класса Message копирует переменные-члены данного объекта:
    Message::Message(const Message &m): contents(m.contents), folders(m.folders) { add_to_Folders(m); // добавить это сообщение в папки, на которые
    // указывает m
    }
    А также вызывает функцию add_to_Folders(), чтобы добавить указатель на недавно созданный объект класса Message каждому объекту класса Folder, который содержит оригинал сообщения. Деструктор класса Message
    При удалении объекта класса Message следует удалить это сообщение из папок, которые указывают на него. Это общее действие с оператором присвоения копии, поэтому определим для этого общую функцию:
    // удалить это сообщение из соответствующих папок void Message::remove_from_Folders() { for (auto f : folders) // для каждого указателя в folders f->remMsg(this); // удалить это сообщение из данной папки
    }
    Реализация функции remove_from_Folders() подобна таковой у функции add_to_Folders(), за исключением того, что она использует функцию remMsg() для удаления текущего сообщения.
    Page 653/1103

    При наличии функции remove_from_Folders() написать деструктор несложно:
    Message::Message() { remove_from_Folders();
    }
    Вызов функции remove_from_Folders() гарантирует отсутствие у объектов класса Folder указателей на удаленный объект класса Message. Компилятор автоматически вызывает деструктор класса string для освобождения объекта contents, а деструктор класса set освобождает память, используемую элементами набора. Оператор присвоения копии класса
    Message
    Как обычно, оператор присвоения и оператор присвоения копии класса Folder должны выполнять действия конструктора копий и деструктора. Как всегда, крайне важно структурировать свой код так, чтобы он выполнялся правильно, даже если операнды слева и справа — тот же объект.
    В данном случае защита против присвоения самому себе осуществляется за счет удаления указателей на это сообщение из папок левого операнда прежде, чем вставить указатели в папки правого операнда:
    Messages Message::operator=(const Message &rhs) {
    // отработать присвоение себе самому, удаляя указатели прежде вставки remove_from_Folders(); // обновить существующие папки contents = rhs.contents; // копировать содержимое сообщения из rhs folders = rhs.folders; // копировать указатели Folder из rhs add_to_Folders(rhs); // добавить это сообщение к данным папкам return *this;
    }
    Если левый и правый операнды — тот же объект, то у них тот же адрес. Если вызвать функцию remove_from_Folders() после вызова функции add_to_Folders(), это сообщение будет удалено изо всех соответствующих ему папок. Функция swap() класса Message
    Библиотека определяет версии функции swap() для классов string и set (см. раздел 9.2.5). В
    результате класс Message извлечет пользу из определения собственной версии функции swap(). При определении специфической для класса Message версии функции swap() можно избежать лишних копирований членов contents и folders.
    Page 654/1103

    Но наша функция swap() должна также управлять указателями Folder, которые указывают на обмениваемые сообщения. После такого вызова, как swap(m1, m2), указатели Folder,
    указывающие на объект m1, должны теперь указать на объект m2, и наоборот.
    Для управления указателями Folder осуществляются два прохода по всем элементам folders.
    Первый проход удалит сообщения из соответствующих папок. Затем вызов функции swap()
    совершит обмен переменных-членов. Второй проход по элементам folders добавляет указатели на обмениваемые сообщения: void swap(Message &lhs, Message &rhs) { using std::swap; // в данном случае не обязательно, но привычка
    // хорошая
    // удалить указатели на каждое сообщение из их (оригинальных) папок for (auto f: lhs.folders) f->remMsg(&lhs); for (auto f: rhs.folders) f->remMsg(&rhs); // обмен наборов указателей contents и folders swap(lhs.folders, rhs.folders); // использует swap(set&, set&) swap(lhs.contents, rhs.contents); // swap(string&, string&)
    // добавляет указатели на каждое сообщение в их (новые) папки for (auto f: lhs.folders) f->addMsg(&lhs); for (auto f: rhs.folders) f->addMsg(&rhs);
    } Упражнения раздела 13.4
    Упражнение 13.33. Почему параметр функций-членов save() и remove() класса Message имеет тип Folder&? Почему этот параметр не определен как Folder или const Folder&?
    Упражнение 13.34. Напишите класс Message, как описано в этом разделе.
    Page 655/1103


    1   ...   31   32   33   34   35   36   37   38   ...   54


    написать администратору сайта