Язык программирования C++. Пятое издание - Стенли Липпман
Шрифт:
Интервал:
Закладка:
// то же поведение, что и у исходного конструктора выше
Sales_data(const std::string &s):
bookNo(s), units_sold(0), revenue(0) { }
Обычно для конструктора лучше использовать внутриклассовый инициализатор, если он есть и присваивает члену класса правильное значение. С другой стороны, если ваш компилятор еще не поддерживает внутриклассовые инициализаторы, то каждый конструктор должен явно инициализировать каждый член встроенного типа.
Конструкторы не должны переопределять внутриклассовые инициализаторы, кроме как при использовании иного исходного значения. Если вы не можете использовать внутриклассовые инициализаторы, каждый конструктор должен явно инициализировать каждый член встроенного типа.
Следует заметить, что у обоих этих конструкторов тела пусты. Единственное, что должны сделать эти конструкторы, — присвоить значения переменным-членам. Если ничего другого делать не нужно, то тело функции пусто.
Определение конструктора вне тела классаВ отличие от наших других конструкторов, конструктору, получающему поток istream, действительно есть что делать. В своем теле этот конструктор вызывает функцию read(), чтобы присвоить переменным-членам новые значения:
Sales_data::Sales_data(std::istream &is) {
read(is, *this); // read читает транзакцию из is в текущий объект
}
У конструкторов нет типа возвращаемого значения, поэтому определение начинается с имени функции. Подобно любой другой функции-члену, при определении конструктора за пределами тела класса необходимо указать класс, которому принадлежит конструктор. Таким образом, синтаксис Sales data::Sales_data указывает, что мы определяем член класса Sales_data по имени Sales_data. Этот член класса является конструктором, поскольку его имя совпадает с именем класса.
В этом конструкторе нет списка инициализации конструктора, хотя с технической точки зрения было бы правильней сказать, что список инициализации конструктора пуст. Даже при том, что список инициализации конструктора пуст, члены этого объекта инициализируются прежде, чем выполняется тело конструктора.
Члены, отсутствующие в списке инициализации конструктора, инициализируются соответствующим внутриклассовым инициализатором (если он есть) или значением по умолчанию. Для класса Sales_data это означает, что при запуске тела функции на выполнение переменная bookNo будет содержать пустую строку, а переменные units_sold и revenue — значение 0.
Чтобы стало понятней, напомним, что второй параметр функции read() является ссылкой на объект класса Sales_data. В разделе 7.1.2 мы обращали внимание на то, что указатель this используется для доступа к объекту в целом, а не к его отдельному члену. В данном случае для передачи "этого" объекта в качестве аргумента функции read() используется синтаксис *this.
Упражнения раздела 7.1.4Упражнение 7.11. Добавьте в класс Sales_data конструкторы и напишите программу, использующую каждый из них.
Упражнение 7.12. Переместите определение конструктора Sales_data(), получающего объект istream, в тело класса Sales_data.
Упражнение 7.13. Перепишите программу из раздела 7.1.1 так, чтобы использовать конструктор с параметром istream.
Упражнение 7.14. Напишите версию стандартного конструктора, явно инициализирующую переменные-члены значениями, предоставленными внутриклассовыми инициализаторами.
Упражнение 7.15. Добавьте соответствующие конструкторы в класс Person.
7.1.5. Копирование, присвоение и удаление
Кроме определения способа инициализации своих объектов, классы контролируют также то, что происходит при копировании, присвоении и удалении объектов класса. Объекты копируются во многих случаях: при инициализации переменной, при передаче или возвращении объекта по значению (см. раздел 6.2.1 и раздел 6.3.2). Объекты присваиваются при использовании оператора присвоения (см. раздел 4.4). Объекты удаляются, когда они прекращают существование, например, при выходе локального объекта из блока, в котором он был создан (см. раздел 6.1.1). Объекты, хранимые в векторе (или массиве), удаляются при удалении вектора (или массива).
Если мы не определим эти операции, компилятор создаст их сам. Обычно создаваемые компилятором версии выполняются, копируя, присваивая или удаляя каждую переменную-член объекта. Например, когда в приложении книжного магазина (см. раздел 7.1.1) компилятор выполняет следующее присвоение:
total = trans; // обработать следующую книгу
оно выполняется, как будто было написано так:
// присвоение по умолчанию для Sales_data эквивалентно следующему:
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold;
total.revenue = trans.revenue;
Более подробная информация об определении собственных версий этих операторов приведена в главе 13.
Некоторые классы не могут полагаться на синтезируемые версииХотя компилятор и создает сам операторы копирования, присвоения и удаления, важно понимать, что у некоторых классов их стандартные версии ведут себя неправильно. В частности, синтезируемые версии вряд ли будут правильно работать с классами, которые резервируют ресурсы, располагающиеся вне самих объектов класса. Пример резервирования и управления динамической памятью приведен в главе 12. Как будет продемонстрировано в разделе 13.6, классы, которые управляют динамической памятью, вообще не могут полагаться на синтезируемые версии этих операций.
Однако следует заметить, что большинство классов, нуждающихся в динамической памяти, способны (и должны) использовать классы vector или string, если им нужно управляемое хранение. Классы, использующие векторы и строки, избегают сложностей, связанных с резервированием и освобождением памяти.
Кроме того, синтезируемые версии операторов копирования, присвоения и удаления правильно работают для классов, у которых есть переменные-члены класса vector или string. При копировании или присвоении объекта, обладающего переменной-членом класса vector, этот класс сам позаботится о копировании и присвоении своих элементов. Когда объект удаляется, переменная-член класса vector тоже удаляется, что в свою очередь удаляет элементы вектора. Класс string работает аналогично.
Пока вы еще не знаете, как определить операторы, описанные в главе 13, ресурсы, резервируемые вашими классами, должны храниться непосредственно как переменные-члены класса.
7.2. Управление доступом и инкапсуляция
На настоящий момент для нашего класса определен интерфейс; однако ничто не вынуждает пользователей использовать его. Наш класс еще не использует инкапсуляцию — пользователи вполне могут обратиться к объекту Sales_data и воспользоваться его реализацией. Для обеспечения инкапсуляции в языке С++ используют спецификаторы доступа (access specifier).
• Члены класса, определенные после спецификатора public, доступны для всех частей программы. Открытые члены (public member) определяют интерфейс к классу.
• Члены, определенные после спецификатора private, являются закрытыми (private member), они доступны для функций-членов класса, но не доступны для кода, который использует класс. Разделы private инкапсулируют (т.е. скрывают) реализацию.
Переопределив класс Sales_data еще раз, получаем следующее:
class Sales_data {
public: // добавлен спецификатор доступа
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) { }
Sales_data(const std::string &s): bookNo(s) { }
Sales_data(std::istream&);
std::string isbn() const { return bookNo; }
Sales_data &combine(const Sales_data&);
private: // добавлен спецификатор доступа
double avg_price() const
{ return units_sold ? revenue/units_sold : 0; }
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
Конструкторы и функции-члены, являющиеся частью интерфейса (например, isbn() и combine()), должны располагаться за спецификатором public; переменные-члены и функции, являющиеся частью реализации, располагаются за спецификатором private.
Класс может содержать любое количество спецификаторов доступа; нет никаких ограничений на то, как часто используется спецификатор. Каждый спецификатор определяет уровень доступа последующих членов. Заданный уровень доступа остается в силе до следующего спецификатора доступа или до конца тела класса.