Главная страница

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


Скачать 1.85 Mb.
НазваниеЯзык программирования C Пятое издание
Дата15.07.2019
Размер1.85 Mb.
Формат файлаpdf
Имя файла620354-www.libfox.ru.pdf
ТипДокументы
#84130
страница19 из 54
1   ...   15   16   17   18   19   20   21   22   ...   54
Естественно, возникает вопрос: как инициализируются объекты total и trans?
Настолько известно, инициализатор для этих объектов не предоставлялся, поэтому они инициализируются значением по умолчанию (см. раздел 2.2.1). Классы сами контролируют инициализацию по умолчанию, определяя специальный конструктор, известный как стандартный конструктор (default constructor). Стандартным считается конструктор, не получающий никаких аргументов.
Как будет продемонстрировано, стандартный конструктор является особенным во многом,
например, если класс не определяет конструкторы явно, компилятор сам определит стандартный конструктор неявно .
Созданный компилятором конструктор известен как синтезируемый стандартный конструктор (synthesized default constructor). У большинства классов этот синтезируемый конструктор инициализирует каждую переменную-член класса следующим образом:
• Если есть внутриклассовый инициализатор (см. раздел 2.6.1), он и используется для инициализации члена класса.
• В противном случае член класса инициализируется значением по умолчанию (см. раздел
2.2.1).
Поскольку класс Sales_data предоставляет инициализаторы для переменных units_sold и revenue, синтезируемый стандартный конструктор использует данные значения для инициализации этих членов. Переменная bookNo инициализируется значением по умолчанию, т.е. пустой строкой. Некоторые классы не могут полагаться на синтезируемый стандартный конструктор
Только довольно простые классы, такие как текущий класс Sales_data, могут полагаются на синтезируемый стандартный конструктор. Как правило, собственный стандартный конструктор для класса определяют потому, что компилятор создает его, только если для класса не определено никаких других конструкторов . Если определен хоть один конструктор, то у класса не будет стандартного конструктора, если не определить его самостоятельно. Основание для этого правила таково: если класс требует контроля инициализации объекта в одном случае, то он, вероятно, потребует его во всех случаях.
Компилятор создает стандартный конструктор автоматически, только если в классе не объявлено никаких конструкторов.
Вторая причина для определения стандартного конструктора в том, что у некоторых классов синтезируемый стандартный конструктор работает неправильно. Помните, что определенные в блоке объекты встроенного или составного типа (такого как массивы и указатели) без
Page 338/1103
инициализации имеют неопределенное значение (см. раздел 2.2.1). Это же относится к не инициализированным членам встроенного типа. Поэтому классы, у которых есть члены встроенного или составного типа, должны либо инициализировать их в классе, либо определять собственную версию стандартного конструктора. В противном случае пользователи могли бы создать объекты с членами, значения которых не определены.
Классы, члены которых имеют встроенный или составной тип, могут полагаться на синтезируемый стандартный конструктор, только если у всех таких членов есть внутриклассовые инициализаторы.
Третья причина определения некоторыми классами собственного стандартного конструктора в том, что иногда компилятор неспособен создать его. Например, если у класса есть член типа класса и у этого класса нет стандартного конструктора, то компилятор не сможет инициализировать этот член. Для таких классов следует определить собственную версию стандартного конструктора. В противном случае у класса не будет пригодного для использования стандартного конструктора. Дополнительные обстоятельства,
препятствующие компилятору создать соответствующий стандартный конструктор,
приведены в разделе 13.1.6. Определение конструкторов класса Sales_data
Определим для нашего класса Sales_data четыре конструктора со следующими параметрами:
• Типа istream&, для чтения транзакции.
• Типа const string& для ISBN; типа unsigned для количества проданных книг; типа double для цены проданной книги.
• Типа const string& для ISBN. Для других членов этот конструктор будет использовать значения по умолчанию.
• Без параметров (т.е. стандартный конструктор). Этот конструктор придется определить,
поскольку определены другие конструкторы.
Добавим эти члены в класс так: struct Sales_data {
// добавленные конструкторы
Sales_data() = default;
Sales_data(const std::string &s): bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) { }
Sales_data(std::istream &);
// другие члены, как прежде std::string isbn() const { return bookNo; }
Page 339/1103

Sales_data& combine(const Sales_data&); double avg_price() const; std::string bookNo; unsigned units_sold = 0; double revenue = 0.0;
}; Что значит = default
Начнем с объяснения стандартного конструктора:
Sales_data() = default;
В первую очередь обратите внимание на то, что это определение стандартного конструктора,
поскольку он не получает никаких аргументов. Мы определяем этот конструктор только потому , что хотим предоставить другие конструкторы, но и стандартный конструктор тоже нужен. Этот конструктор должен делать то же, что и синтезируемая версия.
По новому стандарту, если необходимо стандартное поведение, можно попросить компилятор создать конструктор автоматически, указав после списка параметров часть =
default. Синтаксис = default может присутствовать как в объявлении в теле класса, так и в определении вне его. Подобно любой другой функции, если часть = default присутствует в теле класса, стандартный конструктор будет встраиваемым; если она присутствует в определении вне класса, то по умолчанию этот член не будет встраиваемым.
Стандартный конструктор работает в классе Sales_data только потому, что предоставлены инициализаторы для переменных-членов встроенного типа. Если ваш компилятор не поддерживает внутриклассовые инициализаторы, для инициализации каждого члена класса стандартный конструктор должен использовать список инициализации конструктора
(описанный непосредственно ниже). Список инициализации конструктора
Теперь рассмотрим два других конструктора, которые были определены в классе:
Sales_data(const std::string &s) : bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) { }
Новой частью этих определений являются двоеточие и код между ним и фигурными скобками, обозначающими пустые тела функции. Эта новая часть — список инициализации конструктора (constructor initializer list), определяющий исходные значения для одной или нескольких переменных-членов создаваемого объекта.
Инициализатор конструктора — это список имен переменных-членов класса, каждое из которых сопровождается исходным значением в круглых (или фигурных) скобках. Если инициализаций несколько, они отделяются запятыми.
Конструктор с тремя параметрами использует первые два параметра для инициализации переменных-членов bookNo и units_sold. Инициализатор для переменной revenue вычисляется при умножении количества проданных книг на их цену.
Конструктор с одним параметром типа string использует ее для инициализации переменной-члена bookNo, но переменные units_sold и revenue не инициализируются явно.
Page 340/1103

Когда член класса отсутствует в списке инициализации конструктора, он инициализируется неявно, с использованием того же процесса, что и у синтезируемого стандартного конструктора. В данном случае эти члены инициализируются внутриклассовыми инициализаторами. Таким образом, получающий строку конструктор эквивалентен следующему.
// то же поведение, что и у исходного конструктора выше
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.
Page 341/1103

Чтобы стало понятней, напомним, что второй параметр функции 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. Некоторые классы не могут полагаться на синтезируемые версии
Page 342/1103

Хотя компилятор и создает сам операторы копирования, присвоения и удаления, важно понимать, что у некоторых классов их стандартные версии ведут себя неправильно. В
частности, синтезируемые версии вряд ли будут правильно работать с классами, которые резервируют ресурсы, располагающиеся вне самих объектов класса. Пример резервирования и управления динамической памятью приведен в главе 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;
Page 343/1103

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.
Класс может содержать любое количество спецификаторов доступа; нет никаких ограничений на то, как часто используется спецификатор. Каждый спецификатор определяет уровень доступа последующих членов. Заданный уровень доступа остается в силе до следующего спецификатора доступа или до конца тела класса. Использование ключевых слов class и struct
Было также внесено еще одно изменение: в начале определения класса использовано ключевое слово class, а не struct. Это изменение является чисто стилистическим; тип класса можно определить при помощи любого из этих ключевых слов. Единственное различие между ключевыми словами struct и class в заданном по умолчанию уровне доступа.
Члены класса могут быть определены перед первым спецификатором доступа. Уровень доступа к таким членам будет зависеть от того, как определяется класс. Если используется ключевое слово struct, то члены, определенные до первого спецификатора доступа, будут открытыми; если используется ключевое слово class, то они будут закрытыми.
Общепринятым стилем считается определение классов, все члены которого предположительно будут открытыми, с использованием ключевого слова struct. Если члены класса должны быть закрытыми, используется ключевое слово class.
Единственное различие между ключевыми словами class и struct в задаваемом по умолчанию уровне доступа.Ключевая концепция. Преимущества инкапсуляции
Инкапсуляция предоставляет два важных преимущества.
• Пользовательский код не может по неосторожности повредить состояние
Page 344/1103
инкапсулированного объекта.
• Реализация инкапсулированного класса может со временем измениться, это не потребует изменений в коде на пользовательском уровне.
Определив переменные-члены закрытыми, автор класса получает возможность вносить изменения в данные. Если реализация изменится, то вызванные этим последствия можно исследовать только в коде класса. Пользовательский код придется изменять только при изменении интерфейса. Если данные являются открытыми, то любой использовавший их код может быть нарушен. Пришлось бы найти и переписать любой код, который полагался на прежнюю реализацию, и только затем использовать программу.
Еще одно преимущество объявления переменных-членов закрытыми в том, что данные защищены от ошибок, которые могли бы внести пользователи. Если есть ошибка,
повреждающая состояние объекта, места ее поиска ограничены только тем кодом, который является частью реализации. Это существенно облегчает поиск проблем и обслуживание программы. Упражнения раздела 7.2
Упражнение 7.16. Каковы ограничения (если они есть) на количество спецификаторов доступа в определении класса? Какие виды членов должны быть определены после спецификатора public? Какие после спецификатора private?

1   ...   15   16   17   18   19   20   21   22   ...   54


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