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

  • Какая версия по-вашему лучше и почему

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


    Скачать 1.85 Mb.
    НазваниеЯзык программирования C Пятое издание
    Дата15.07.2019
    Размер1.85 Mb.
    Формат файлаpdf
    Имя файла620354-www.libfox.ru.pdf
    ТипДокументы
    #84130
    страница33 из 54
    1   ...   29   30   31   32   33   34   35   36   ...   54
    Упражнение 12.18. Почему класс указателя shared_ptr не имеет функции-члена release()?
    12.1.6. Класс weak_ptr
    Класс weak_ptr (табл. 12.5) представляет интеллектуальный указатель, который не контролирует продолжительность существования объекта, на который он указывает. Он только указывает на объект, который контролирует указатель shared_ptr. Привязка указателя weak_ptr к указателю shared_ptr не изменяет счетчик ссылок этого указателя shared_ptr. Как только последний указатель shared_ptr на этот объект будет удален, удаляется и сам объект.
    Page 591/1103

    Этот объект будет удален, даже если останется указатель weak_ptr на него. Имя weak_ptr отражает концепцию "слабого" совместного использования объекта.
    Создаваемый указатель weak_ptr инициализируется из указателя shared_ptr: auto p = make_shared<int>(42); weak_ptr<int> wp(p); // wp слабо связан с p; счетчик ссылок p неизменен
    Здесь указатели wp и p указывают на тот же объект. Поскольку совместное использование слабо, создание указателя wp не изменяет счетчик ссылок указателя p; это делает возможным удаление объекта, на который указывает указатель wp.
    Таблица 12.5. Функции указателя weak_ptr weak_ptr<T> w Обнуляет указатель weak_ptr,
    способный указывать на объект типа T weak_ptr<T> w(sp) Указатель weak_ptr на тот же объект, что и указатель sp типа shared_ptr. Тип Т должен быть приводим к типу, на который указывает sp w = p Указатель p может иметь тип shared_ptr или weak_ptr. После присвоения w разделяет собственность с указателем p w.reset() Обнуляет указатель w w.use_count()
    Возвращает количество указателей shared_ptr, разделяющих собственность с указателем w w.expired() Возвращает значение true, когда функция w.use_count() должна возвратить нуль, и значение false в противном случае w.lock() Возвращает нулевой указатель shared_ptr, если функция expired() должна возвратить значение true; в противном случае возвращает указатель shared_ptr на объект, на который указывает указатель w
    Поскольку объект может больше не существовать, нельзя использовать указатель weak_ptr для непосредственного доступа к его объекту. Для этого следует вызвать функцию lock(). Она проверяет существование объекта, на который указывает указатель weak_ptr. Если это так, то функция lock() возвращает указатель shared_ptr на совместно используемый объект. Такой указатель гарантирует существование объекта, на который он указывает, по крайней мере,
    пока существует этот указатель shared_ptr. Рассмотрим пример: if (shared_ptr<int> np = wp.lock()) { // true, если np не нулевой
    // в if, np совместно использует свой объект с p
    }
    Внутренняя часть оператора if доступна только в случае истинности вызова функции lock(). В
    операторе if использование указателя np для доступа к объекту вполне безопасно.
    Проверяемый класс указателя
    Для того чтобы проиллюстрировать, насколько полезен указатель weak_ptr, определим вспомогательный класс указателя для нашего класса StrBlob. Класс указателя, назовем его
    StrBlobPtr, будет хранить указатель weak_ptr на переменную-член data класса StrBlob,
    которым он был инициализирован. Использование указателя weak_ptr не влияет на продолжительность существования вектора, на который указывает данный объект класса
    StrBlob. Но можно воспрепятствовать попытке доступа к вектору, которого больше не существует.
    Page 592/1103

    Класс StrBlobPtr будет иметь две переменные-члена: указатель wptr, который может быть либо нулевым, либо указателем на вектор в объекте класса StrBlob; и переменную curr,
    хранящую индекс элемента, который в настоящее время обозначает этот объект. Подобно вспомогательному классу класса StrBlob, у класса указателя есть функция-член check(),
    проверяющая безопасность обращения к значению StrBlobPtr:
    //
    StrBlobPtr передает исключение при попытке доступа к
    // несуществующему элементу class StrBlobPtr { public:
    StrBlobPtr() : curr(0) { }
    StrBlobPtr(StrBlob &a, size_t sz = 0): wptr(a.data), curr(sz) { } std::string& deref() const;
    StrBlobPtr& incr(); // префиксная версия private:
    // check() возвращает shared_ptr на вектор, если проверка успешна std::shared_ptr<std::vector<std::string>> check(std::size_t, const std::string&) const;
    // хранит weak_ptr, означая возможность удаления основного вектора std::weak_ptr<std::vector<std::string>> wptr; std::size_t curr; // текущая позиция в пределах массива
    };
    Стандартный конструктор создает нулевой указатель StrBlobPtr. Список инициализации его конструктора (см. раздел 7.1.4) явно инициализирует переменную-член curr нулем и неявно инициализирует указатель-член wptr как нулевой указатель weak_ptr. Второй конструктор получает ссылку на StrBlob и (необязательно) значение индекса. Этот конструктор инициализирует wptr как указатель на вектор данного объекта класса StrBlob и инициализирует переменную curr значением sz. Используем аргумент по умолчанию (см.
    раздел 6.5.1) для инициализации переменной curr, чтобы обозначить первый элемент. Как
    Page 593/1103
    будет продемонстрировано, ниже параметр sz будет использован функцией-членом end()
    класса StrBlob.
    Следует заметить, что нельзя связать указатель StrBlobPtr с константным объектом класса
    StrBlob. Это ограничение следует из того факта, что конструктор получает ссылку на неконстантный объект типа StrBlob.
    Функция-член check() класса StrBlobPtr отличается от таковой у класса StrBlob, поскольку она должна проверять, существует ли еще вектор, на который он указывает: std::shared_ptr<std::vector<std::string>>
    StrBlobPtr::check(std::size_t i, const std::string &msg) const { auto ret = wptr.lock(); // существует ли еще вектор? if (!ret) throw std::runtime_error("unbound StrBlobPtr"); if (i >= ret->size()) throw std::out_of_range(msg); return ret; // в противном случае, возвратить shared_ptr на вектор
    }
    Так как указатель weak_ptr не влияет на счетчик ссылок соответствующего указателя shared_ptr, вектор, на который указывает StrBlobPtr, может быть удален. Если вектора нет,
    функция lock() возвратит нулевой указатель. В таком случае любое обращение к вектору потерпит неудачу и приведет к передаче исключения. В противном случае функция check()
    проверит переданный индекс. Если значение допустимо, функция check() возвратит указатель shared_ptr, полученный из функции lock(). Операции с указателями
    Определение собственных операторов рассматривается в главе 14, а пока определим функции deref() и incr() для обращения к значению и инкремента указателя класса StrBlobPtr соответственно.
    Функция-член deref() вызывает функцию check() для проверки безопасности использования вектора и принадлежности индекса curr его диапазону: std::string& StrBlobPtr::deref() const { auto p = check(curr, "dereference past end"); return (*p)[curr]; //
    (*p) - вектор, на который указывает этот объект
    }
    Если проверка прошла успешно, то p будет указателем типа shared_ptr на вектор, на который указывает данный указатель StrBlobPtr. Выражение (*p)[curr] обращается к значению данного
    Page 594/1103
    указателя shared_ptr, чтобы получить вектор, и использует оператор индексирования для доступа и возвращения элемента по индексу curr.
    Функция-член incr() также вызывает функцию check():
    // префикс: возвратить ссылку на объект после инкремента
    StrBlobPtr& StrBlobPtr::incr() {
    // если curr уже указывает на элемент после конца контейнера,
    // его инкремент не нужен check(curr, "increment past end of StrBlobPtr");
    ++curr; // инкремент текущего состояния return *this;
    }
    Безусловно, чтобы получить доступ к переменной-члену data, наш класс указателя должен быть дружественным классу StrBlob (см. раздел 7.3.4). Снабдим также класс StrBlob функциями begin() и end(), возвращающими указатель StrBlobPtr на себя:
    // предварительное объявление необходимо для объявления дружественным
    // классу StrBlob class StrBlobPtr; class StrBlob { friend class StrBlobPtr;
    // другие члены, как в разделе 12.1.1
    // возвратить указатель StrBlobPtr на первый и следующий
    // после последнего элементы
    Page 595/1103

    StrBlobPtr begin() { return StrBlobPtr(*this); }
    StrBlobPtr end()
    { auto ret = StrBlobPtr(*this, data->size()); return ret; }
    }; Упражнения раздела 12.1.6
    Упражнение 12.19. Определите собственную версию класса StrBlobPtr и модифицируйте класс StrBlob соответствующим объявлением дружественным, а также функциями-членами begin() и end().
    Упражнение 12.20. Напишите программу, которая построчно читает исходный файл в операционной системе класса StrBlob и использует указатель StrBlobPtr для вывода каждого его элемента.
    Упражнение 12.21. Функцию-член deref() класса StrBlobPtr можно написать следующим образом: std::string& deref() const
    { return (*check(curr, "dereference past end"))[curr]; }

    Какая версия по-вашему лучше и почему?
    Упражнение 12.22. Какие изменения следует внести в класс StrBlobPtr, чтобы получить класс,
    применимый с типом const StrBlob? Определите класс по имени ConstStrBlobPtr, способный указывать на const StrBlob.
    12.2. Динамические массивы
    Операторы new и delete резервируют объекты по одному. Некоторым приложениям нужен способ резервировать хранилище для многих объектов сразу. Например, векторы и строки хранят свои элементы в непрерывной памяти и должны резервировать несколько элементов сразу всякий раз, когда контейнеру нужно повторное резервирование (см. раздел 9.4).
    Для этого язык и библиотека предоставляют два способа резервирования всего массива объектов. Язык определяет второй вид оператора new, резервирующего и инициализирующего массив объектов. Библиотека предоставляет шаблон класса allocator,
    позволяющий отделять резервирование от инициализации. По причинам, описанным в разделе 12.2.2, применение класса allocator обычно обеспечивает лучшую производительность и более гибкое управление памятью.
    У многих (возможно, у большинства) приложений нет никакой непосредственной необходимости в динамических массивах. Когда приложение нуждается в переменном количестве объектов, практически всегда проще, быстрей и безопасней использовать вектор
    (или другой библиотечный контейнер), как было сделано в классе StrBlob. По причинам,
    описанным в разделе 13.6, преимущества использования библиотечного контейнера даже более явны по новому стандарту. Библиотеки, поддерживающие новый стандарт, работают существенно быстрее, чем предыдущие версии.
    Page 596/1103

    Большинство приложений должно использовать библиотечные контейнеры, а не динамически созданные массивы. Использовать контейнер проще, так как меньше вероятность допустить ошибку управления памятью, и , вероятно, он обеспечивает лучшую производительность.
    Как уже упоминалось, использующие контейнеры классы могут использовать заданные по умолчанию версии операторов копирования, присвоения и удаления (см. раздел 7.1.5).
    Классы, резервирующие динамические массивы, должны определить собственные версии этих операторов для управления памятью при копировании, присвоении и удалении объектов.
    Не резервируйте динамические массивы в классах, пока не прочитаете главу 13.
    12.2.1. Оператор new и массивы
    Чтобы запросить оператор new зарезервировать массив объектов, после имени типа следует указать в квадратных скобках количество резервируемых объектов. В данном случае оператор new резервирует требуемое количество объектов и (при успешном резервировании)
    возвращает указатель на первый из них:
    // вызов get_size() определит количество резервируемых целых чисел int *pia = new int[get_size()]; // pia указывает на первое из них
    Значение в скобках должно иметь целочисленный тип, но не обязано быть константой.
    Для представления типа массива при резервировании можно также использовать псевдоним типа (см. раздел 2.5.1). В данном случае скобки не нужны: typedef int arrT[42]; // arrT - имя типа массива из 42 целых чисел int *p = new arrT; // резервирует массив из 42 целых чисел;
    // p указывает на первый его элемент
    Здесь оператор new резервирует массив целых чисел и возвращает указатель на его первый элемент. Даже при том, что никаких скобок в коде нет, компилятор выполняет это выражение,
    используя оператор new[]. Таким образом, компилятор выполняет это выражение, как будто код был написан так: int *p = new int[42]; Резервирование массива возвращает указатель на тип элемента
    Хотя обычно память, зарезервированную оператором new T[], называют "динамическим
    Page 597/1103
    массивом", это несколько вводит в заблуждение. Когда мы используем оператор new для резервирования массива, объект типа массива получен не будет. Вместо этого будет получен указатель на тип элемента массива. Даже если для определения типа массива использовать псевдоним типа, оператор new не резервирует объект типа массива. И в данном случае резервируется массив, хотя часть [ число ] не видима. Даже в этом случае оператор new возвращает указатель на тип элемента.
    Поскольку зарезервированная память не имеет типа массива, для динамического массива нельзя вызвать функцию begin() или end() (см. раздел 3.5.3). Для возвращения указателей на первый и следующий после последнего элементы эти функции используют размерность массива (являющуюся частью типа массива). По тем же причинам для обработки элементов так называемого динамического массива нельзя также использовать серийный оператор for.
    Важно помнить, что у так называемого динамического массива нет типа массива.
    Инициализация массива динамически созданных объектов
    Зарезервированные оператором new объекты (будь то одиночные или их массивы)
    инициализируются по умолчанию. Для инициализации элементов массива по умолчанию (см.
    раздел 3.3.1) за размером следует расположить пару круглых скобок: int *pia = new int[10]; // блок из десяти неинициализированных
    // целых чисел int *pia2 = new int[10](); // блок из десяти целых чисел,
    // инициализированных по умолчанию
    // значением 0 string *psa = new string[10]; // блок из десяти пустых строк string *psa2 = new string[10](); // блок из десяти пустых строк
    По новому стандарту можно также предоставить в скобках список инициализаторов элементов:
    // блок из десяти целых чисел, инициализированных соответствующим
    //
    Page 598/1103
    инициализатором int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
    // блок из десяти строк; первые четыре инициализируются заданными
    // инициализаторами, остальные элементы инициализируются значением
    // по умолчанию string *psa3 = new string[10]{"a", "an", "the", string(3, 'x')};
    При списочной инициализации объекта типа встроенного массива (см. раздел 3.5.1)
    инициализаторы используются для инициализации первых элементов массива. Если инициализаторов меньше, чем элементов, остальные инициализируются значением по умолчанию. Если инициализаторов больше, чем элементов, оператор new потерпит неудачу,
    не зарезервировав ничего. В данном случае оператор new передает исключение типа bad_array_new_length. Подобно исключению bad_alloc, этот тип определен в заголовке new.
    Хотя для инициализации элементов массива по умолчанию можно использовать пустые круглые скобки, в них нельзя предоставить инициализаторы для элементов. Благодаря этому факту при резервировании массива нельзя использовать ключевое слово auto (см. раздел
    12.1.2). Динамическое резервирование пустого массива вполне допустимо
    Для определения количества резервируемых объектов можно использовать произвольное выражение: size_t n = get_size(); // get_size() возвращает количество необходимых
    // элементов int* p = new int[n]; // резервирует массив для содержания элементов for (int* q = p; q != p + n; ++q)
    /* обработка массива */ ;
    Возникает интересный вопрос: что будет, если функция get_size() возвратит значение 0? Этот код сработает прекрасно. Вызов функции new[n] при n равном 0 вполне допустим, даже при том, что нельзя создать переменную типа массива размером 0: char arr[0]; // ошибка: нельзя определить массив нулевой длины
    Page 599/1103
    char *cp = new char[0]; // ok: но обращение к значению cp невозможно
    При использовании оператора new для резервирования массива нулевого размера он возвращает допустимый, а не нулевой указатель. Этот указатель гарантированно будет отличен от любого другого указателя, возвращенного оператором new. Он будет подобен указателю на элемент после конца (см. раздел 3.5.3) для нулевого элемента массива. Этот указатель можно использовать теми способами, которыми используется итератор после конца. Его можно сравнивать в цикле, как выше. К нему можно добавить нуль (или вычесть нуль), такой указатель можно вычесть из себя, получив в результате нуль. К значению такого указателя нельзя обратиться, в конце концов, он не указывает на элемент.
    В гипотетическом цикле, если функция get_size() возвращает 0, то n также равно 0. Вызов оператора new зарезервирует нуль объектов. Условие оператора for будет ложно (p равно q +
    n, поскольку n равно 0). Таким образом, тело цикла не выполняется. Освобождение динамических массивов
    Для освобождения динамического массива используется специальная форма оператора delete, имеющая пустую пару квадратных скобок: delete p; // p должен указывать на динамически созданный объект или
    // быть нулевым delete [] pa; // pa должен указывать на динамически созданный
    // объект или быть нулевым
    Второй оператор удаляет элементы массива, на который указывает pa, и освобождает соответствующую память. Элементы массива удаляются в обратном порядке. Таким образом,
    последний элемент удаляется первым, затем предпоследний и т.д.
    При применении оператора delete к указателю на массив пустая пара квадратных скобок необходима: она указывает компилятору, что указатель содержит адрес первого элемента массива объектов. Если пропустить скобки оператора delete для указателя на массив (или предоставить их, передав оператору delete указатель на объект), то его поведение будет непредсказуемо.
    Напомним, что при использовании псевдонима типа, определяющего тип массива, можно зарезервировать массив без использования [] в операторе new. Но даже в этом случае нужно использовать скобки при удалении указателя на этот массив: typedef int arrT[42]; // arrT имя типа массив из 42 целых чисел int *p = new arrT; // резервирует массив из 42 целых чисел; p указывает
    Page 600/1103

    // на первый элемент delete [] p; // скобки необходимы, поскольку был
    // зарезервирован массив
    Несмотря на внешний вид, указатель p указывает на первый элемент массива объектов, а не на отдельный объект типа arrT. Таким образом, при удалении указателя p следует использовать [].
    Компилятор вряд ли предупредит нас, если забыть скобки при удалении указателя на массив или использовать их при удалении указателя на объект. Программа будет выполняться с ошибкой, не предупреждая о ее причине. Интеллектуальные указатели и динамические массивы
    Библиотека предоставляет версию указателя unique_ptr, способную контролировать массивы,
    зарезервированные оператором new. Чтобы использовать указатель unique_ptr для управления динамическим массивом, после типа объекта следует расположить пару пустых скобок:
    // up указывает на массив из десяти неинициализированных целых чисел unique_ptr<int[]> up(new int[10]); up.release(); // автоматически использует оператор delete[] для
    // удаления указателя
    Скобки в спецификаторе типа (<int[]>) указывают, что указатель up указывает не на тип int, а на массив целых чисел. Поскольку указатель up указывает на массив, при удалении его указателя автоматически используется оператор delete[].
    Указатели unique_ptr на массивы предоставляют несколько иные функции, чем те, которые использовались в разделе 12.1.5. Эти функции описаны в табл. 12.6. Когда указатель unique_ptr указывает на массив, нельзя использовать точечный и стрелочный операторы доступа к элементам. В конце концов, указатель unique_ptr указывает на массив, а не на объект, поэтому эти операторы были бы бессмысленны. С другой стороны, когда указатель unique_ptr указывает на массив, для доступа к его элементам можно использовать оператор индексирования: for (size_t i = 0; i != 10; ++i) up[i] = i; // присвоить новое значение каждому из элементов
    Page 601/1103

    Таблица 12.6. Функции указателя unique_ptr на массив Операторы доступа к элементам
    (точка и стрелка) не поддерживаются указателями unique_ptr на массивы. Другие его функции неизменны unique_ptr<T[]> u u может указывать на динамически созданный массив типа
    T unique ptr<T[]> u(p) u указывает на динамически созданный массив, на который указывает встроенный указатель p. Тип указателя p должен допускать приведение к типу T
    (см. раздел 4.11.2). Выражение u[i] возвратит объект в позиции i массива, которым владеет указатель u. u должен быть указателем на массив
    В отличие от указателя unique_ptr, указатель shared_ptr не оказывает прямой поддержки управлению динамическим массивом. Если необходимо использовать указатель shared_ptr для управления динамическим массивом, следует предоставить собственную функцию удаления:
    // чтобы использовать указатель shared_ptr, нужно предоставить
    // функцию удаления shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; }); sp.reset(); // использует предоставленное лямбда-выражение, которое в
    // свою очередь использует оператор delete[] для освобождения массива
    Здесь лямбда-выражение (см. раздел 10.3.2), использующее оператор delete[], передается как функция удаления.
    Если не предоставить функции удаления, результат выполнения этого кода непредсказуем.
    По умолчанию указатель shared_ptr использует оператор delete для удаления объекта, на который он указывает. Если объект является динамическим массивом, то при использовании оператора delete возникнут те же проблемы, что и при пропуске [], когда удаляется указатель на динамический массив (см. раздел 12.2.1).
    Поскольку указатель shared_ptr не поддерживает прямой доступ к массиву, для обращения к его элементам применяется следующий код:
    // shared_ptr не имеет оператора индексирования и не поддерживает
    // арифметических действий с указателями for (size_t i = 0; i != 10; ++i)
    *(sp.get() + i) = i; //
    Page 602/1103
    для доступа к встроенному указателю
    // используется функция get()
    Указатель shared_ptr не имеет оператора индексирования, а типы интеллектуальных указателей не поддерживают арифметических действий с указателями. В результате для доступа к элементам массива следует использовать функцию get(), возвращающую встроенный указатель, который можно затем использовать обычным способом. Упражнения раздела 12.2.1
    Упражнение 12.23. Напишите программу, конкатенирующую два строковых литерала и помещающую результат в динамически созданный массив символов. Напишите программу,
    конкатенирующую две строки библиотечного типа string, имеющих те же значения, что и строковые литералы, используемые в первой программе.
    Упражнение 12.24. Напишите программу, которая читает строку со стандартного устройства ввода в динамически созданный символьный массив. Объясните, как программа обеспечивает ввод данных переменного размера. Проверьте свою программу, введя строку,
    размер которой превышает длину зарезервированного массива.
    Упражнение 12.25. С учетом следующего оператора new, как будет удаляться указатель pa? int *pa = new int[10];
    12.2.2. Класс allocator
    Важный аспект, ограничивающий гибкость оператора new, заключается в том, что он объединяет резервирование памяти с созданием объекта (объектов) в этой памяти. Точно так же оператор delete объединяет удаление объекта с освобождением занимаемой им памяти.
    Обычно объединение инициализации с резервированием — это именно то, что и нужно при резервировании одиночного объекта. В этом случае почти наверняка известно значение,
    которое должен иметь объект.
    Когда резервируется блок памяти, обычно в нем планируется создавать объекты по мере необходимости. В таком случае желательно было бы отделить резервирование памяти от создания объектов. Это позволит резервировать память в б о льших объемах, а дополнительные затраты на создание объектов нести только тогда, когда это фактически необходимо.
    Зачастую объединение резервирования и создания оказывается расточительным. Например: string *const p = new string[n]; // создает n пустых строк string s; string *q = p; // q указывает на первую строку
    Page 603/1103
    while (cin >> s && q != p + n)
    *q++ = s; // присваивает новое значение *q const size_t size = q - p; // запомнить количество прочитанных строк
    // использовать массив delete[] p; // p указывает на массив; не забыть использовать delete[]
    Этот оператор new резервирует и инициализирует n строк. Но n строк может не понадобиться, — вполне может хватить меньшего количества строк. В результате, возможно,
    были созданы объекты, которые никогда не будут использованы. Кроме того, тем из объектов, которые действительно используются, новые значения присваиваются немедленно, поверх только что инициализированных строк. Используемые элементы записываются дважды: сначала, когда им присваивается значение по умолчанию, а затем,
    когда им присваивается значение.
    Еще важней то, что классы без стандартных конструкторов не могут быть динамически созданы как массив. Класс allocator и специальные алгоритмы
    Библиотечный класс allocator, определенный в заголовке memory, позволяет отделить резервирование от создания. Он обеспечивает не типизированное резервирование свободной области память. Операции, поддерживаемые классом allocator, приведены в табл.
    12.7. Операции с классом allocator описаны в этом разделе, а типичный пример его использования — в разделе 13.5.
    Подобно типу vector, тип allocator является шаблоном (см. раздел 3.3). Чтобы определить экземпляр класса allocator, следует указать тип объектов, которые он сможет резервировать.
    Когда объект allocator резервирует память, он обеспечивает непрерывное хранилище соответствующего размера для содержания объектов заданного типа: allocator<string> alloc; // объект, способный резервировать строки auto const p = alloc.allocate(n); // резервирует n незаполненных строк
    Этот вызов функции allocate() резервирует память для n строк.
    Таблица 12.7. Стандартный класс allocator и специальные алгоритмы allocator<T> a
    Определяет объект а класса allocator, способный резервировать память для объектов типа T
    a.allocate(n) Резервирует пустую область памяти для содержания n объектов типа T
    a.deallocate(p, n) Освобождает область памяти, содержавшую n объектов типа T, начиная с адреса в указателе p типа Т*. Указатель p должен быть ранее возвращен функцией allocate(),
    а размер n — соответствовать запрошенному при создании указателя p. Функцию destroy()
    Page 604/1103
    следует выполнить для всех объектов, созданных в этой памяти, прежде, чем вызвать функцию deallocate() a.construct(p, args) Указатель p на тип T должен указывать на незаполненную область памяти; аргументы args передаются конструктору типа Т,
    используемому для создания объекта в памяти, на которую указывает указатель p a.destroy(p)
    Выполняет деструктор (см. раздел 12.1.1) для объекта, на который указывает указатель p типа Т* Класс allocator резервирует незаполненную память
    Память, которую резервирует объект класса allocator, не заполнена. Эта область памяти используется при создании объектов. В новой библиотеке функция-член construct() получает указатель и любое количество дополнительных аргументов; она создает объекты в заданной области памяти. Для инициализации создаваемого объекта используются дополнительные аргументы. Подобно аргументам функции make_shared() (см. раздел 12.1.1), эти дополнительные аргументы должны быть допустимыми инициализаторами объекта создаваемого типа. В частности, если типом объекта является класс, эти аргументы должны соответствовать конструктору этого класса: auto q = p; // q указывает на следующий элемент после последнего
    // созданного alloc.construct(q++); //
    *q - пустая строка alloc.construct(q++, 10, 'c'); //
    *q - cccccccccc alloc.construct(q++, "hi"); //
    *q - hi!
    В прежних версиях библиотеки функция construct() получала только два аргумента: указатель для создаваемого объекта и значение его типа. В результате можно было только скопировать объект в незаполненную область, но никакой другой конструктор этого типа использовать было нельзя.
    Использование незаполненной области памяти, в которой еще не был создан объект,
    является ошибкой: cout << *p << endl; // ok: использует оператор вывода класса string cout << *q << endl; // ошибка: q указывает на незаполненную память!
    Чтобы использовать память, возвращенную функцией allocate(), в ней следует создать объекты. Результат использования незаполненной памяти другими способами непредсказуем.
    По завершении использования объектов следует удалить ранее созданные элементы. Для этого следует вызвать функцию destroy() каждого созданного элемента. Функция destroy()
    Page 605/1103
    получает указатель и запускает деструктор (см. раздел 12.1.1) указанного объекта: while (q != p) alloc.destroy(--q); // освободить фактически зарезервированные строки
    В начале цикла q указывает на следующий элемент после последнего заполненного. Перед вызовом функции destroy() осуществляется декремент указателя q. Таким образом, при первом вызове функции destroy() указатель q указывает на последний созданный элемент.
    Первый элемент удаляется на последней итерации, после которой q станет равен p и цикл закончится.
    Удалять можно только те элементы, которые были фактически созданы.
    Как только элементы удалены, память можно повторно использовать для содержания других строк или возвратить их операционной системе. Для освобождения памяти используется функция deallocate(): alloc.deallocate(p, n);
    Указатель, передаваемый функции deallocate(), не может быть нулевым; он должен указывать на область памяти, зарезервированной функцией allocate(). Кроме того, переданный ей аргумент размера должен совпадать с размером, использованным при вызове функции allocate(), зарезервировавшим область памяти, на которую указывает указатель. Алгоритмы копирования и заполнения неинициализированной памяти
    В дополнение к классу allocator библиотека предоставляет два алгоритма, способных создавать объекты в неинициализированной памяти. Эти функции описаны в табл. 12.8 и определены в заголовке memory.
    Таблица 12.8. Алгоритмы, связанные с классом allocator Эти функции создают элементы по назначению, а не присваивают их uninitialized_copy(b, е, b2) Копирует элементы из исходного диапазона, обозначенного итераторами b и е, в незаполненную память, обозначенную итератором b2. Память, обозначенная итератором b2, должна быть достаточно велика для содержания копии элементов из исходного диапазона uninitialized_copy_n(b, n, b2) Копирует n элементов, начиная с обозначенного итератором b в незаполненную память, начиная с позиции b2 uninitialized_fill(b, е, t) Создает объекты в диапазоне незаполненной памяти,
    обозначенной итераторами b и е как копию t uninitialized_fill_n(b, n, t) Создает n объектов,
    начиная с b. Итератор b должен обозначать незаполненную память достаточного размера для содержания заданного количества объектов
    Предположим, например, что имеется вектор целых чисел, который необходимо скопировать в динамическую память. Память будет резервироваться дважды для каждого целого числа в векторе. Первую половину вновь зарезервированной памяти заполним копиями элементов из исходного вектора. Элементы второй половины заполним заданным значением:
    // зарезервировать вдвое больше элементов, чем хранения в vi auto p = alloc.allocate(vi.size() * 2);
    //
    Page 606/1103
    создать элементы, начиная с p как копии элементов в vi auto q = uninitialized_copy(vi.begin(), vi.end(), p);
    // инициализировать остальные элементы значением 42 uninitialized_fill_n(q, vi.size(), 42);
    Подобно алгоритму copy() (см. раздел 10.2.2), алгоритм uninitialized_copy() получает три итератора. Первые два обозначают исходную последовательность, а третий обозначает получателя, в который будут скопированы эти элементы. Итератор назначения, переданный алгоритму uninitialized_copy(), должен обозначить незаполненную память. В отличие от алгоритма copy(), алгоритм uninitialized_copy() создает элементы в своем получателе.
    Подобно алгоритму copy(), алгоритм uninitialized_copy() возвращает (приращенный) итератор назначения. Таким образом, вызов функции uninitialized_copy() возвращает указатель на следующий элемент после последнего заполненного. В данном примере этот указатель сохраняется в переменной q, передаваемой функции uninitialized_fill_n(). Эта функция, как и функция fill_n() (см. раздел 10.2.2), получает указатель на получателя, количество и значение.
    Она создает заданное количество объектов из заданного значения в позиции, начиная с заданной получателем. Упражнения раздела 12.2.2
    Упражнение 12.26. Перепишите программу из начала раздела, используя класс allocator.
    12.3. Использование библиотеки: программа запроса текста
    Для завершения обсуждения библиотеки реализуем простую программу текстового запроса.
    Она позволит пользователю искать в заданном файле слова, которые могли встречаться в нем. Результатом запроса будет количество экземпляров слова и список строк, в которых оно присутствует. Если слово встречается несколько раз в той же строке, то она отображается только однажды. Строки отображаются в порядке возрастания, т.е. строка номер 7
    отображается перед строкой номер 9 и т.д.
    Например, прочитав файл, содержащий начало этой главы и запустив поиск слова element,
    программа должна создать следующий вывод: element occurs 112 times
    (line 36) A set element contains only a key;
    (line 158) operator creates a new element
    (line 160) Regardless of whether the element
    (line 168) When we fetch an element from a map, we
    (line 214) If the element is not found, find returns
    Далее следует примерно 100 строк, также содержащих слово element.
    Page 607/1103

    12.3.1. Проект программы
    Наилучший способ начать проект программы — это составить перечень ее функциональных возможностей. Зная, какие именно функции необходимо обеспечить, значительно легче разобраться, какие именно структуры данных понадобятся. Итак, начнем с требований,
    которым должна удовлетворять разрабатываемая программа.
    • Читая ввод, программа должна запоминать строку (строки), в которой присутствует искомое слово. Следовательно, программа должна читать ввод построчно и разделять прочитанные строки на отдельные слова
    • При создании вывода программа должна:
    • получать номера строк, содержащих искомое слово;
    • нумеровать строки в порядке возрастания без дубликатов;
    • отображать текст исходного файла по заданному номеру строки.
    Эти требования можно выполнить с помощью библиотечных средств.
    • Для хранения копии всего входного файла используем вектор vector<string>. Каждая строка входного файла станет элементом этого вектора. При необходимости вывода строку можно будет выбрать, используя ее номер как индекс.
    • Для разделения строки на слова используем строковый поток istringstream (см. раздел 8.3).
    • Для хранения номеров строк, в которых присутствует искомое слово, используем контейнер set. Это гарантирует, что каждая строка будет присутствовать только однажды, а номера строки будут храниться в порядке возрастания.
    • Для связи каждого слова с набором номеров строк, в которых присутствует искомое слово,
    используем контейнер map. Это позволит выбрать соответствующий набор для каждого заданного слова.
    По рассматриваемым вскоре причинам в решении будет также использован указатель shared_ptr. Структуры данных
    Несмотря на то что программу можно было бы написать, используя контейнеры vector, set и map непосредственно, полезней определить более абстрактное решение. Для начала разработаем класс для содержания входного файла, чтобы упростить запрос. Этот класс,
    TextQuery, будет содержать вектор и карту. Вектор будет содержать текст входного файла, а карта ассоциировать каждое слово в этом файле с набором номеров строк, в которых присутствует искомое слово. У этого класса будет конструктор, читающий заданный входной файл, и функция, обрабатывающая запросы.
    Работа функции обработки запроса довольно проста: она просматривает свою карту в поисках искомого слова. Трудней всего решить, что должна возвращать функция запроса.
    Если известно, что слово найдено, необходимо выяснить, сколько раз оно встретилось, в строках с какими номерами и каков текст каждой строки с этими номерами.
    Проще всего вернуть все эти данные, определив второй класс, назовем его QueryResult,
    который и будет содержать результаты запроса. У этого класса будет функция print(),
    выводящая результаты, хранимые в объекте класса QueryResult. Совместное использование данных классами
    Page 608/1103

    Класс QueryResult предназначен для представления результатов запроса. Эти результаты включают набор номеров строк, связанных с искомым словом, и текст соответствующих строк из входного файла. Эти данные хранятся в объектах типа TextQuery.
    Поскольку данные, необходимые объекту класса QueryResult, хранятся в объекте класса
    TextQuery, необходимо решить, как получить к ним доступ. Можно было бы скопировать набор номеров строк, но это может оказаться слишком дорого. Кроме того, копировать вектор не хотелось бы потому, что это повлечет за собой копирование всего файла для вывода лишь его части (как обычно и бывает).
    Избежать копирования можно, возвратив итераторы (или указатели) на содержимое объекта
    TextQuery. Но с этим подходом связана проблема: что если объект класса TextQuery будет удален до объекта класса QueryResult? В этом случае объект класса QueryResult ссылался бы на данные в больше не существующем объекте.
    Это требует синхронизации продолжительности существования объекта класса QueryResult с объектом класса TextQuery, результаты которого он представляет. Таким образом, эти два класса концептуально "совместно используют" данные. Для отражения совместного использования этих структур данных используем указатели shared_ptr (см. раздел 12.1.1).
    Применение класса TextQuery
    При разработке класса может быть полезно написать использующие его программы, прежде чем фактически реализовать его члены. Таким образом можно выяснить, какие функции ему необходимы. Например, следующая программа использует проектируемые классы TextQuery и QueryResult. Эта функция получает поток класса ifstream для подлежащего обработке файла и взаимодействует с пользователем, выводя результаты по заданному слову: void runQueries(ifstream &infile) {
    // infile - поток ifstream для входного файла
    TextQuery tq(infile); // хранит файл и строит карту запроса
    // цикл взаимодействия с пользователем: приглашение ввода искомого
    // слова и вывод результатов while (true) { cout << "enter word to look for, or q to quit: "; string s;
    // остановиться по достижении конца файла или при встрече
    // символа 'q' во вводе
    Page 609/1103
    if (!(cin >> s) || s == "q") break;
    // выполнить запрос и вывести результат print(cout, tq.query(s)) << endl;
    }
    }
    Начнем с инициализации объекта tq класса TextQuery данными из переданного потока ifstream. Конструктор TextQuery() читает файл в свой вектор и создает карту, связывающую слова из ввода с номерами строк, в которых они присутствуют.
    Цикл while (непрерывно) запрашивает у пользователя искомое слово и выводит полученные результаты. Условие цикла проверяет литерал true (см. раздел 2.1.3), поэтому оно всегда истинно. Для выхода из цикла используется оператор break (см. раздел 5.5.1) в операторе if.
    Он проверяет успешность чтения. Если оно успешно, проверяется также, не ввел ли пользователь символ "q", желая завершить работу. Получив искомое слово, запрашиваем его поиск у объекта tq, а затем вызываем функцию print() для вывода результата поиска.
    Упражнения раздела 12.3.1
    Упражнение 12.27. Классы TextQuery и QueryResult используют только те возможности,
    которые уже были описаны ранее. Не заглядывая вперед, напишите собственные версии этих классов.
    Упражнение 12.28. Напишите программу, реализующую текстовые запросы, не определяя классы управления данными. Программа должна получать файл и взаимодействовать с пользователем, запрашивая слова, искомые в этом файле. Используйте контейнеры vector,
    map и set для хранения данных из файла и создания результатов запросов.
    Упражнение 12.29. Перепишите цикл взаимодействия с пользователем, используя цикл do while (см. раздел 5.4.4). Объясните, какая версия предпочтительней и почему.
    12.3.2. Определение классов программы запросов
    Начнем с определения класса TextQuery. Пользователь создает объекты этого класса,
    предоставляя поток istream для чтения входного файла. Этот класс предоставляет также функцию query(), которая получает строку и возвращает объект класса QueryResult,
    представляющий строки, в которых присутствует искомое слово.
    Переменные-члены класса должны учитывать совместное использование с объектами класса
    QueryResult. Класс QueryResult совместно использует вектор, представляющий входной файл и наборы, содержащие номера строк, связанные с каждым словом во вводе. Следовательно,
    у нашего класса есть две переменные-члена: указатель shared_ptr на динамически созданный вектор, содержащий входной файл, а также карта строк и указателей shared_ptr<set>.
    Карта ассоциирует каждое слово в файле с динамически созданным набором, содержащим номера строк, в которых присутствует это слово.
    Чтобы сделать код немного понятней, определим также тип-член (см. раздел 7.3.1) для обращения к номерам строк, которые являются индексами вектора строк:
    Page 610/1103
    class QueryResult; // объявление необходимого типа возвращаемого
    // значения функции запроса class TextQuery { public: using line_no = std::vector<std::string>::size_type;
    TextQuery(std::ifstream&);
    QueryResult query(const std::string&) const; private: std::shared_ptr<std::vector<std::string>> file; // исходный файл
    // сопоставить каждое слово с набором строк, в которых присутствует
    // это слово std::map<std::string, std::shared_ptr<std::set<line_no>>> wm;
    };
    Самая трудная часть этого кода — разобраться в именах классов. Как обычно, для кода из файла заголовка применяется часть std::, указывающая имя библиотеки (см. раздел 3.1). Но в данном случае частое повторение имени std:: делает код немного менее понятным для чтения. Рассмотрим пример: std::map<std::string, std::shared_ptr<std::set<line_no>>> wm;
    Его будет проще понять, переписав так: map<string, shared_ptr<set<line_no>>> wm; Конструктор TextQuery()
    Конструктор TextQuery() получает поток ifstream, позволяющий читать строки по одной:
    // прочитать входной файл, создать карту строк и их номеров
    TextQuery::TextQuery(ifstream &is): file(new vector<string>) { string text;
    Page 611/1103
    while (getline(is, text)) { // для каждой строки в файле file->push_back(text); // запомнить эту строку текста int n = file->size() - 1; // номер текущий строки istringstream line(text); // разделить строку на слова string word; while (line >> word) { // для каждого слова в этой строке
    // если слова еще нет в wm, индексация добавляет новый
    // элемент auto &lines = wm[word]; // lines - это shared_ptr if (!lines) // этот указатель - вначале нулевой, когда
    // встречается слово lines.reset(new set<line_no>); // резервирует новый набор lines->insert(n); // вставить номер этой строки
    }
    }
    }
    Список инициализации конструктора резервирует новый вектор для содержания текста из входного файла. Функция getline() используется для чтения из файла по одной строке за раз и их помещения в вектор. Поскольку file — это указатель shared_ptr, используем оператор ->
    Page 612/1103
    для обращения к его значению, чтобы вызвать функцию push_back() для того элемента вектора, на который указывает указатель file.
    Затем поток istringstream (см. раздел 8.3) используется для обработки каждого слова только что прочитанной строки. Внутренний цикл while использует оператор ввода класса istringstream для чтения каждого слова текущей строки в строку word. В цикле while используется оператор индексирования карты для доступа к связанному со словом указателю shared_ptr<set> и связи ссылки lines с этим указателем. Обратите внимание, что lines —
    это ссылка, поэтому внесенные в нее изменения будут сделаны с элементом карты wm.
    Если слова еще нет в карте, оператор индексирования добавит строку word в карту wm (см.
    раздел 11.3.4). Ассоциируемый со строкой word элемент инициализирован значением по умолчанию. Это значит, что ссылка lines будет нулевой, если оператор индексирования добавит строку word в карту wm. Если ссылка lines будет нулевой, резервируем новый набор и вызываем функцию reset() для обновлении указателя shared_ptr, на который ссылается ссылка lines, чтобы он указывал на этот только что созданный набор.
    Независимо от того, был ли создан новый набор, происходит вызов функции insert(),
    добавляющей текущий номер строки. Поскольку lines — это ссылка, вызов функции insert()
    добавляет элемент в набор карты wm. Если данное слово встречается несколько раз в той же строке, вызов функции insert() не делает ничего. Класс QueryResult
    Класс QueryResult обладает тремя переменными-членами: строка, представляющая слово,
    указатель shared_ptr на вектор, содержащий входной файл; и указатель shared_ptr на набор номеров строк, в которых присутствует это слово. Его единственная функция-член —
    конструктор, инициализирующий эти три члена: class QueryResult { friend std::ostream& print(std::ostream&, const QueryResult&); public:
    QueryResult(std::string s, std::shared_ptr<std::set<line_no>> p, std::shared_ptr<std::vector<std::string>> f): sought(s), lines(p), file(f) { } private: std::string sought; // слово, представляющее запрос std::shared_ptr<std::set<line_no>> lines; // номера строк std::shared_ptr<std::vector<std::string>> file; // входной файл
    };
    Единственная задача конструктора — сохранить свои аргументы в соответствующих
    Page 613/1103
    переменных-членах, что он и делает в списке инициализации конструктора (см. раздел 7.1.4).
    Функция query()
    Функция query() получает строку, которую она использует для поиска соответствующего набора номеров строк в карте. Если строка найдена, функция query() создает объект класса
    QueryResult из заданной строки, переменной-члена file класса TextQuery и набора,
    извлеченного из карты wm.
    Единственный вопрос: что следует возвратить, если заданная строка не найдена? В данном случае никакого набора возвращено не будет. Решим эту проблему, определив локальный статический объект, являющийся указателем shared_ptr на пустой набор номеров строк.
    Когда слово не найдено, возвратим копию этого указателя shared_ptr:
    QueryResult
    TextQuery::query(const string &sought) const {
    // возвратить указатель на этот набор, если искомое слово не найдено static shared_ptr<set<line_no>> nodata(new set<line_no>);
    // использовать find() но не индексировать, чтобы избежать
    // добавления слова в карту wm! auto loc = wm.find(sought); if (loc == wm.end()) return QueryResult(sought, nodata, file); // не найдено else return QueryResult(sought, loc->second, file);
    } Вывод результатов
    Функция print() выводит заданный объект класса QueryResult в заданный поток: ostream &print(ostream &os, const QueryResult &qr) {
    // если слово найдено, вывести количество и все вхождения os << qr.sought << " occurs " << qr.lines->size() << " "
    << make_plural(qr.lines->size(), "time", "s") << endl;
    //
    Page 614/1103
    вывести каждую строку, в которой присутствует слово for (auto num : *qr.lines) // для каждого элемента в наборе
    // не путать пользователя с номерами строк, начинающимися с 0 os << "\t (line " << num + 1 << ") "
    << *(qr.file->begin() + num) << endl; return os;
    }
    Для отчета о количестве найденных соответствий используем функцию size() набора, на который ссылается qr.lines. Поскольку этот набор контролируется указателем shared_ptr,
    следует помнить об обращении к значению lines. Чтобы вывести слово time или times, в зависимости от того, равен ли размер 1, используем функцию make_plural() (см. раздел 6.3.2).
    Цикл for перебирает набор, на который ссылается lines. Тело цикла for выводит номер строки,
    откорректированный так, как привычно человеку. Числа в наборе являются индексами элементов в векторе, их нумерация начинается с нуля. Но большинство пользователей привыкли к тому, что первая строка имеет номер 1, поэтому будем систематически добавлять
    1 к номерам строк, чтобы отображать их в общепринятой форме.
    Используем номер строки для выбора строк из вектора, на который указывает указатель-член file. Помните, что при добавлении числа к итератору будет получен элемент на столько же элементов далее (см. раздел 3.4.2). Таким образом, часть file->begin() + num дает номер элемента от начала вектора, на который указывает file.
    Обратите внимание: эта функция правильно обрабатывает случай, когда слово не найдено. В
    данном случае набор будет пуст. Первый оператор вывода заметит, что слово встретилось нуль раз. Поскольку *res.lines пуст, цикл for не выполнится ни разу. Упражнения раздела
    12.3.2
    Упражнение 12.30. Определите собственные версии классов TextQuery и QueryResult, а также выполните функцию runQueries() из раздела 12.3.1.

    1   ...   29   30   31   32   33   34   35   36   ...   54


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