Язык программирования C Пятое издание
![]()
|
Упражнение 12.31. Что будет, если для хранения номеров строк использовать вектор вместо набора? Какой подход лучше? Почему? Упражнение 12.32. Перепишите классы TextQuery и QueryResult так, чтобы для хранения входного файла вместо вектора vector<string> использовался класс StrBlob. Упражнение 12.33. В главе 15 программа запроса будет дополнена, и классу QueryResult понадобятся дополнительные члены. Добавьте функции-члены по имени begin() и end(), возвращающие итераторы для набора номеров строк, возвращенных данным запросом, и функцию-член get_file(), возвращающую указатель shared_ptr на файл в объекте QueryResult. Резюме Page 615/1103 В языке С++ для резервирования памяти используется оператор new, а для освобождения — оператор delete. Библиотека определяет также класс allocator, чтобы резервировать пустые блоки динамической памяти. Резервирующие динамическую память программы отвечают за ее освобождение. Освобождение динамической памяти — богатейший источник ошибок: память может быть не освобождена никогда или может быть освобождена, когда еще есть указатели на нее. Новая библиотека определяет классы интеллектуальных указателей (shared_ptr, unique_ptr и weak_ptr), делающие работу с динамической памятью намного более безопасной. Интеллектуальный указатель автоматически освобождает память, как только удаляется последний указатель на нее. В современных программах С++ следует использовать именно интеллектуальные указатели. Термины Деструктор (destructor). Специальная функция-член, которая освобождает занятую объектом память, когда он выходит из области видимости или удаляется. Динамическая память (free store). Пул памяти, доступный программе для хранения объектов, создаваемых динамически. Динамически созданный объект (dynamically allocated object). Объект, который создан в динамической памяти. Такие объекты существуют до тех пор, пока не будут удалены из динамической памяти явно или пока программа не завершит работу. Интеллектуальный указатель shared ptr. Интеллектуальный указатель, обеспечивающий совместную собственность: объект освобождается, когда удаляется последний указатель shared_ptr на тот объект. Интеллектуальный указатель unique_ptr. Интеллектуальный указатель, обеспечивающий единоличную собственность: объект освобождается, когда удаляется указатель unique_ptr на этот объект. Указатель unique_ptr не может быть скопирован или присвоен непосредственно. Интеллектуальный указатель weak_ptr. Интеллектуальный указатель на объект, управляемый указателем shared_ptr. Указатель shared_ptr не учитывает указатель weak_ptr, принимая решение об освобождении своего объекта. Интеллектуальный указатель (smart pointer). Библиотечный тип, действует как указатель, но допускающий проверку на безопасность использования. Тип сам заботится об освобождении памяти, когда это нужно. Класс allocator. Библиотечный класс, резервирующий области памяти. Оператор delete. Освобождает участок памяти, зарезервированный оператором new. Оператор delete p освобождает объект, a delete [] p — массив, на который указывает указатель p. Указатель p может быть пуст или указывать на область память, зарезервированную оператором new. Оператор new. Резервирует область в динамической памяти. Оператор new T резервирует область памяти, создает в ней объект типа T и возвращает указатель на этот объект. Если T — тип массива, оператор new возвращает указатель на его первый элемент. Аналогично Page 616/1103 оператор new [n] Т резервирует n объектов типа T и возвращает указатель на первый элемент массива. По умолчанию вновь созданный объект инициализируется значением по умолчанию. Но можно также предоставить инициализаторы. Потерянный указатель (dangling pointer). Указатель, содержащий адрес области памяти, в которой уже нет объекта. Потерянный указатель является весьма распространенным источником ошибок в программе, причем такие ошибки крайне трудно обнаружить. Размещающий оператор new (placement new). Форма оператора new, получающая в круглых скобках дополнительные аргументы после ключевого слова new; например, синтаксис new (nothrow) int устанавливает, что оператор new не должен передавать исключения. Распределяемая память (heap). То же, что и динамическая память. Счетчик ссылок (reference count). Счетчик, отслеживающий количество пользователей, совместно использующих общий объект. Используется интеллектуальными указателями, чтобы узнать, когда безопасно освобождать память, на которую они указывают. Функция удаления (deleter). Функция, передаваемая интеллектуальному указателю, для использования вместо оператора delete при освобождении объекта, на который указывает указатель. Часть III Инструменты для разработчиков классов Классы — основная концепция языка С++. Подробное рассмотрение определения классов начато в главе 7. Она затрагивает такие фундаментальные для классов темы, как область видимости класса, сокрытие данных и конструкторы. Она познакомила также с важнейшими средствами класса: функциями-членами, неявным указателем this, дружественными отношениями, а также членами const, static и mutable. В этой части дополним тему классов, рассмотрев управление копированием, перегрузку операторов, наследование и шаблоны. Как уже упоминалось, в классах С++ определяются конструкторы, контролирующие происходящее при инициализации объектов класса. Классы контролируют также то, что происходит при копировании, присвоении, перемещении и удалении объектов. В этом отношении язык С++ отличается от других языков, большинство из которых не позволяет разработчикам классов контролировать эти операции. В главе 13 будут рассмотрены эти темы, а также две важные концепции, введенные новым стандартом: ссылки на r-значения и операции перемещения. Глава 14 посвящена перегрузке операторов, позволяющей использовать операнды типа классов со встроенными операторами. Перегрузка оператора — это один из способов, которым язык С++ позволяет создавать новые типы, столь же интуитивно понятные в использовании, как и встроенные типы. Среди доступных для перегрузки операторов класса есть оператор вызова функции. Объекты таких классов можно "вызвать" точно так же, как если бы они были функциями. Рассмотрим также новые библиотечные средства, облегчающие использование различных типов вызываемых объектов единообразным способом. Эта глава завершается рассмотрением еще одного специального вида функций-членов класса — операторов преобразования. Эти операторы определяют неявные преобразования Page 617/1103 из объектов типа класса. Компилятор применяет эти преобразования в тех же контекстах (и по тем же причинам), что и преобразования встроенных типов. Последние две главы этой части посвящены поддержке языком С++ объектно-ориентированного и обобщенного программирования. Глава 15 рассматривает наследование и динамическое связывание. Наряду с абстракцией данных наследование и динамическое связывание — это основы объектно-ориентированного программирования. Наследование облегчает определение связанных типов, а динамическое связывание позволяет писать независимый от типов код, способный игнорировать различия между типами, которые связаны наследованием. Глава 16 посвящена шаблонам классов и функций. Шаблоны позволяют писать обобщенные классы и функции, которые не зависят от типов. Новый стандарт ввел множество новых средств, связанных с шаблонами: шаблоны с переменным количеством аргументов, псевдонимы типов шаблона и новые способы контроля создания экземпляра. Создание собственных объектно-ориентированных или обобщенных типов требует довольно хорошего понимания языка С++. К счастью, для их использования это не обязательно. Например, стандартная библиотека интенсивно использует средства, которые рассматриваются только в главах 15 и 16, но библиотечные типы и алгоритмы использовались уже с самого начала книги, даже без объяснения их реализации. Поэтому читатели должны понимать, что часть III посвящена довольно сложным средствам. Написание шаблонов и объектно-ориентированных классов требует хорошего понимания основ языка С++ и глубокого знания того, как определяют базовые классы. Глава 13 Управление копированием Как упоминалось в главе 7, каждый класс является определением нового типа и операций, которые можно выполнять с объектами этого типа. В этой главе упоминалось также о том, что классы могут определять конструкторы, которые контролируют происходящее при создании объектов данного типа. В этой главе мы изучим то, как классы могут контролировать происходящее при копировании, присвоении, перемещении и удалении объектов данного типа. Для этого классы имеют специальные функции-члены: конструктор копий, конструктор перемещения, оператор присвоения копии, оператор присваивания при перемещении и деструктор. При определении класса разработчик (явно или неявно) определяет происходящее при копировании, перемещении, присвоении и удалении объектов данного класса. Класс контролирует эти операции, определяя пять специальных функций-членов: конструктор копий (copy constructor), оператор присвоения копии (copy-assignment operator), конструктор перемещения (move constructor), оператор присваивания при перемещении (move-assignment operator) и Page 618/1103 деструктор (destructor). Конструкторы копирования и перемещения определяют происходящее при инициализации объекта данными из другого объекта того же типа. Операторы копирования и присваивания при перемещении определяют происходящее при присвоении объекта данного класса другому объекту того же класса. Деструктор определяет происходящее в момент, когда объект данного типа прекращает существование. Все эти операции вместе мы будем называть управлением копированием (copy control). Если класс определяет не все функции-члены управления копированием, компилятор сам определит недостающие. В результате многие классы могут не определять управление копированием (см. раздел 7.1.5). Но некоторые классы не могут полагаться на заданные по умолчанию определения. Зачастую наиболее трудная часть реализации операций управления копированием — это принятие решения об их необходимости. Управление копированием — это важнейшая часть определения любого класса С++. У начинающих программистов С++ зачастую возникают затруднения при необходимости определения действий, происходящих при копировании, перемещении, присвоении и удалении объектов. Это затруднение обусловлено тем, что задавать их не обязательно, компилятор вполне может создать их сам, хотя результат этих действий может быть не совсем таким, как хотелось бы. 13.1. Копирование, присвоение и удаление Начнем с наиболее простых операций: конструктора копий, оператора присвоения копии и деструктора. Операции перемещения (введенные новым стандартом) рассматриваются в разделе 13.6. 13.1.1. Конструктор копий Если первый параметр конструктора — ссылка на тип класса, а все дополнительные параметры имеют значения по умолчанию, то это конструктор копий: class Foo { public: Foo(); // стандартный конструктор Foo(const Foo&); // конструктор копий // ... } По причинам, которые будут описаны ниже, первый параметр должен иметь ссылочный тип. Page 619/1103 Он почти всегда является ссылкой на константу, хотя вполне можно определить конструктор копий, получающий ссылку на не константу. При некоторых обстоятельствах конструктор копий используется неявно. Следовательно, конструктор копий обычно не следует объявлять как explicit (см. раздел 7.5.4). Синтезируемый конструктор копий Если конструктор копий не определен для класса явно, компилятор синтезирует его сам. В отличие от синтезируемого стандартного конструктора (см. раздел 7.1.4), конструктор копий синтезируется, даже если определены другие конструкторы. Как будет продемонстрировано в разделе 13.1.6, синтезируемый конструктор копий (synthesized copy constructor) некоторых классов препятствует копированию объектов этого типа. В противном случае синтезируемый конструктор копий осуществляет почленное копирование (memberwise copy) членов своего аргумента в создаваемый объект (см. раздел 7.1.5). Компилятор по очереди копирует каждую нестатическую переменную-член заданного объекта в создаваемый. Способ копирования каждой переменной-члена определяет ее тип: для типов класса применяется конструктор копий этого класса, а члены встроенного типа копируются непосредственно. Хотя нельзя непосредственно скопировать массив (см. раздел 3.5.1), синтезируемый конструктор копий копирует члены типа массива поэлементно. Элементы типа класса копируются с использованием конструкторов копий элементов. Например, синтезируемый конструктор копий для класса Sales_data эквивалентен следующему: class Sales_data { public: // другие члены и конструкторы как прежде // объявление, эквивалентное синтезируемому конструктору копий Sales_data(const Sales_data&); private: std::string bookNo; int units_sold = 0; double revenue = 0.0; }; // эквивалент конструктора копий, синтезированный для класса Sales_data Sales_data::Sales_data(const Sales_data &orig): Page 620/1103 bookNo(orig.bookNo), // использование конструктора копий string units_sold(orig.units_sold), // копирует orig.units_sold revenue(orig.revenue) // копирует orig.revenue { } // пустое тело Инициализация копией Теперь можно полностью рассмотреть различия между прямой инициализацией и инициализацией копией (см. раздел 3.2.1): string dots(10, '.'); // прямая инициализация string s(dots); // прямая инициализация strings2 = dots; // инициализация копией string null_book = "9-999-99999-9"; // инициализация копией string nines = string(100, '9'); // инициализация копией При прямой инициализации от компилятора требуется использовать обычный выбор функции (см. раздел 6.4) для подбора конструктора, наилучшим образом соответствующего предоставленным аргументам. Когда используется инициализация копией (copy initialization), от компилятора требуется скопировать правый операнд в создаваемый объект, осуществляя преобразования в случае необходимости (см. раздел 7.5.4). Инициализация копией обычно использует конструктор копий. Но, как будет продемонстрировано в разделе 13.6.2, если у класса есть конструктор перемещения, то инициализация копией иногда использует конструктор перемещения вместо конструктора копий, а пока достаточно знать, что при инициализации копией требуется либо конструктор копий, либо конструктор перемещения. Инициализация копией осуществляется не только при определении переменных с использованием оператора =, но и при: • передаче объекта как аргумента параметру не ссылочного типа; • возвращении объекта из функции с не ссылочным типом возвращаемого значения; Page 621/1103 • инициализации списком в скобках элементов массива или членов агрегатного класса (см. раздел 7.5.5) Некоторые классы используют также инициализацию копией для резервируемых объектов. Например, библиотечные контейнеры инициализируют копией свои элементы при инициализации контейнера, либо при вызове функции insert() или функции push() элемента (см. раздел 9.3.1). Элементы, созданные функцией emplace(), напротив, отличаются прямой инициализацией (см. раздел 9.3.1). Параметры и возвращаемые значения Во время вызова функции с параметрами не ссылочного типа осуществляется инициализация копией (см. раздел 6.2.1). Точно так же, когда у функции не ссылочный тип возвращаемого значения, возвращаемое значение используется в точке вызова для инициализации копией результата оператора вызова (см. раздел 6.3.2). Тот факт, что конструктор копий используется для инициализации не ссылочных параметров типа класса, объясняет, почему собственный параметр конструктора копий должен быть ссылкой. Если бы этот параметр не был ссылкой, то вызов не был бы успешным — при вызове конструктора копий должен быть использован конструктор копий для копирования аргумента, но для копирования аргумента следует вызвать конструктор копий и так далее до бесконечности. Ограничения на инициализацию копией Как уже упоминалось, используется ли инициализация копией или прямая инициализация, если используется инициализатор, то потребуется преобразование в явный конструктор (см. раздел 7.5.4): vector<int> v1(10); // ok: прямая инициализация vector<int> v2 = 10; // ошибка: конструктор, получающий размер, // является явным void f(vector<int>); // параметр f() инициализируется копией f(10); // ошибка: нельзя использовать явный конструктор для // копирования аргумента f(vector<int>(10)); // ok: непосредственно создать временный вектор // из int Прямая инициализация вектора v1 корректна, но на первый взгляд эквивалентная Page 622/1103 инициализация копией вектора v2 ошибочна, поскольку конструктор вектора, получающий один параметр размера, является явным. По тем же причинам недопустима инициализация копией вектора v2 — нельзя неявно использовать явный конструктор при передаче аргумента или возвращении значения из функции. Если нужно использовать явный конструктор, то сделать это следует явно, как в последней строке примера, приведенного выше. Компилятор может обойти конструктор копий Во время инициализации копией компилятору можно (но не обязательно) пропустить конструктор копий или перемещения и создать объект непосредственно. Таким образом, код string null_book = "9-999-99999-9"; // инициализация копией компилятор может выполнить так: string null_book("9-999-99999-9"); // компилятор пропускает конструктор // копий Но даже если компилятор обойдет вызов конструктора копий или перемещения, то он все равно должен существовать и быть доступен (не должен быть закрытым, например) в этой точке программы. Упражнения раздела 13.1.1 Упражнение 13.1. Что такое конструктор копий? Когда он используется? Упражнение 13.2. Объясните, почему следующее объявление недопустимо: Sales_data::Sales_data(Sales_data rhs); Упражнение 13.3. Объясните, что происходит при копировании объектов классов StrBlob и StrBlobPtr? Упражнение 13.4. Предположим, класс Point имеет открытый конструктор копий. Укажите каждый случай использования конструктора копий в этом фрагменте кода: Point global; Point foo_bar(Point arg) { Point local = arg, *heap = new Point(global); *heap = local; Point pa[4] = { local, *heap }; return *heap; } Упражнение 13.5. Напишите с учетом следующего эскиза класса конструктор копий, копирующий все переменные-члены. Конструктор должен динамически резервировать новую строку (см. раздел 12.1.2) и копировать объект, на который указывает ps, а не сам указатель ps. Page 623/1103 class HasPtr { public: HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) { } private: std::string *ps; int i; }; 13.1.2. Оператор присвоения копии Подобно тому, как класс контролирует инициализацию своих объектов, он контролирует также присваивание своих объектов: Sales_data trans, accum; trans = accum; // использует оператор присвоения копии // класса Sales_data Компилятор сам синтезирует оператор присвоения копии, если он не определен в классе явно. Перегруженный оператор присвоения Прежде чем перейти к синтезируемому оператору присвоения, необходимо ознакомиться с перегрузкой операторов (overloaded operator), подробно рассматриваемой в главе 14. Перегруженные операторы — это функции, имена которых состоят из слова operator и символа определяемого оператора. Следовательно, оператор присвоения — это функция operator=. Подобно любой другой функции, у функции оператора есть тип возвращаемого значения и список параметров. Параметрами перегруженного оператора являются его операнды. Некоторые операторы, например присвоение, должны быть определены, как функции-члены. Когда оператор является функцией-членом, левый операнд связан с неявным параметром this (см. раздел 7.1.2). Правый операнд бинарного оператора, такого как присвоение, передается как явный параметр. Оператор присвоения копии получает аргумент того же типа, что и класс: class Foo { public: Foo& operator=(const Foo&); // Page 624/1103 оператор присвоения // ... }; Для совместимости с оператором присвоения встроенных типов (см. раздел 4.4) операторы присвоения обычно возвращают ссылку на свой левый операнд. Следует также заметить, что библиотека обычно требует от типов, хранимых в контейнере, наличия операторов присвоения, возвращающих ссылку на левый операнд. Операторы присвоения обычно должны возвращать ссылку на свой левый операнд. Синтезируемый оператор присвоения копии Подобно конструктору копий, компилятор создает синтезируемый оператор присвоения копии (synthesized assignment operator) для класса, если в нем не определен собственный. Аналогично конструктору копий, у некоторых классов синтезируемый оператор присвоения копии не подразумевает присвоения (раздел 13.1.6). В противном случае он присваивает значение каждой нестатической переменной-члена правого объекта соответствующей переменной-члену левого объекта с использованием оператора присвоения копии типа этой переменной. Массивы присваиваются поэлементно. Синтезируемый оператор присвоения копии возвращает ссылку на свой левый операнд. Например, следующий код эквивалентен синтезируемому оператору присвоения копии класса Sales_data: // эквивалент синтезируемого оператора присвоения копии Sales_data& Sales_data::operator=(const Sales_data &rhs) { bookNo = rhs.bookNo; // вызов string::operator= units_sold = rhs.units_sold; // использует встроенное присвоение int revenue = rhs.revenue; // использует встроенное // присвоение double return *this; // возвратить этот объект } Упражнения раздела 13.1.2 Упражнение 13.6. Что такое оператор присвоения копии? Когда он используется? Что делает Page 625/1103 |