В этой статье будет рассказано:
- Изменение реализации
- Обзор ситуации на текущий момент
- конструкторы и деструкторы классов
- Объявление и определение конструкторов
- Имена членов и имена параметров
- Использование конструкторов
- Конструкторы по умолчанию
С выводом программы связан один момент, который может не устраивать — неподходящее форматирование чисел. Имеется возможность улучшить реализацию, не затрагивая интерфейс. Класс ostream содержит функции-члены, которые управляют форматированием. Не особо вдаваясь в детали, скажем, что с помощью метода setf() можно избавиться от экспоненциальной нотации:
std::cout.setf(std::ios_base::fixed, std::ios_base::floatfield);
Этот вызов устанавливает флаг, который заставляет объект cout использовать нотацию с фиксированной точкой. Подобным же образом следующий оператор заставляет cout выводить три десятичных знака после точки:
std::cout.precision(3) ;
Эти средства можно использовать в методе show () для управления
форматированием, но следует учесть еще один момент. В случае изменения реализации метода внесенные модификации не должны влиять на другие части клиентской программы. Изменения в формате будут оставаться активными вплоть до следующих изменений, поэтому они могут повлиять на последующий вывод в клиентской программе. Следовательно, в show () должен быть предусмотрен возврат к состоянию форматирования, которое было до вызова этого метода. Это можно сделать, с применением возвращаемых значений операторов установки формата:
std::streamsize prec =
std::cout.precision(3); // сохранение предыдущего значения точности
std::cout.precision(prec); // восстановление предыдущего значения
// Сохранение исходных флагов
std::ios_base::fmtflags orig = std::cout.setf(std::ios_base::fixed);
// Восстановление сохраненных значений
std::cout.setf(orig, std::ios_base::floatfield);
Во-первых, вспомните, что fmt flags — это тип, определенный в классе iosbase, который находится в пространстве имен std, отсюда и такое довольно длинное имя типа для orig. Во-вторых, orig хранит все флаги, и оператор сброса использует эту информацию для восстановления установок в разделе float field, который включает флаги для
нотации с фиксированной точкой и экспоненциальной нотации. В-третьих, давайте не будем здесь сильно беспокоиться о деталях. Главный момент в том, что изменения ограничиваются файлом реализации и не влияют на программу, использующую этот класс. Итак, изменим определение метода в файле реализации следующим образом:
void Stock::show()
{
using std::cout;
using std::ios_base; ^
// Установка формата в #.###
ios_base::fmtflags orig =
cout.setf(ios_base::fixed, ios_base::floatfield);
std::streamsize prec = cout.precision (3);
cout « "Company: " « company
« " Shares: " « shares « ' \n• ;
cout « " Share Price: $" « share_val;
// Установка формата в #.##
cout.precision (2);
cout « " Total Worth: $" « total_val « '\n';
// Восстановление исходного формата
cout.setf(orig, ios_base::floatfield);
cout.precision(prec);
}
После этой замены программу можно перекомпилировать. Теперь вывод будет выглядеть так:
Company: NanoSmart Shares: 20
Share Price: $12.500 Total Worth: $250.00
Company: NanoSmart Shares: 35
Share Price: $18.125 Total Worth: $634.38
You can't sell more than you have! Transaction is aborted.
Company: NanoSmart Shares: 35
Share Price: $18.125 Total Worth: $634.38
Company: NanoSmart Shares: 300035
Share Price: $40.125 Total Worth: $12038904.38
Company: NanoSmart Shares: 35
Share Price: $0.125 Total Worth: $4.38
Обзор ситуации на текущий момент
Первый шаг в проектировании класса заключается в предоставлении объявления класса. Объявление класса смоделировано на основе объявления структуры и может включать в себя данные-члены и функции-члены. Объявление имеет раздел private, и члены, объявленные в этом разделе, могут быть доступны только через функции- члены. Объявление также содержит раздел public, и объявленные в нем члены могут быть непосредственно доступны программе, использующей объекты класса. Как правило, данные-члены попадают в закрытый раздел, а функции-члены — в открытый, поэтому типичное объявление класса имеет следующую форму:
class имяКласса
{
private:
объявления данных-членов
public:
прототипы функций-членов
Содержимое открытого раздела включает абстрактную часть проектного
решения — открытый интерфейс. Инкапсуляция данных в закрытом разделе защищает их целостность и называется сокрытием данных. Таким образом, использование классов — это способ, который предлагает C++ для облегчения реализации абстракций, сокрытия данных и инкапсуляции ООП.
Второй шаг в спецификации класса — это реализация функций-членов класса. Вместо прототипов в объявление можно включать полное определение функций, однако общепринятая практика состоит в том, чтобы определять функции отдельно, за исключением наиболее простых. В этом случае вам понадобится операция разрешения контекста для индикации того, к какому классу данная функция-член принадлежит. Например, предположим, что класс Bozo имеет функцию-член Retort (), которая возвращает указатель на тип char. Заголовок функции должен выглядеть примерно так:
char * Bozo::Retort ()
Другими словами, Retort () — не только функция типа char *, это функция типа char *, принадлежащая классу Bozo. Полное, или уточненное, имя функции будет выглядеть как Bozo: :Retort(). Имя Retort (), с другой стороны, является сокращением уточненного имени, и оно должно использоваться только в определенных случаях, таких как в коде методов класса.
Другой способ описания этой ситуации — это говорить о том, что Retort имеет область видимости класса, поэтому необходима операция разрешения контекста для уточнения имени, когда оно встречается вне объявления и вне методов класса.
Для создания объекта, который является частным примером класса, применяется имя класса, как если бы оно было именем типа:
Bozo bozetta;
Это работает потому, что класс является типом, определенным пользователем. Функция-член класса, или метод, вызывается с использованием объекта класса. Это делается с помощью операции членства (точки):
cout << bozetta.Retort();
Код вызывает функцию-член Retort (), и всякий раз, когда код этой функции
обращается к определенным данным-членам, используются значения членов объекта bozetta.
Конструкторы и деструкторы классов
Теперь нам нужно сделать с классом Stock нечто большее. Существует ряд
определенных стандартных функций, называемых конструкторами и деструкторами, которыми обычно должен быть снабжен класс. Давайте поговорим о том, почему они необходимы и как их создавать.
Одна из целей C++ состоит в том, чтобы сделать использование объектов классов подобным применению стандартных типов. Однако код, который был приведен до сих пор в настоящий статье, не позволяет инициализировать объект Stock таким же способом, как это можно сделать с int или struct. To есть обычный синтаксис инициализации не применим к типу Stock:
int year =2001; // допустимая инициализация
struct thing
{
char * pn;
int m;
};
thing amabob = {"wodget", -23}; // допустимая инициализация
Stock hot = {"Sukie's Autos, Inc.", 200, 50.25}; // ошибка компиляции
Причина, по которой нельзя таким способом инициализировать объект Stock, связана с тем, что к данным класса разрешен только закрытый доступ, а это означает, что единственный способ, с помощью которого программа может получить доступ к ним — через функции-члены. Следовательно, для успешной инициализации объекта
понадобится придумать соответствующую функцию-член. (Чтобы инициализировать объект класса так, как показано выше, нужно объявить данные-члены как public, a не private, но в этом случае нарушается один из базовых принципов использования классов — сокрытие данных.)
В общем случае лучше, чтобы все объекты инициализировались при их создании. Например, рассмотрим следующий код:
Stock gift;
gift.buy(10, 24.75);
При существующей реализации класса Stock объект gift не имеет установленного значения для члена company. В проектном решении класса предполагается, что пользователь вызовет acquire () раньше любых других функций-членов, однако нет какого-либо средства, чтобы гарантировать это. Единственным способом обойти эту трудность является автоматическая инициализация объектов при их создании. Для этого в C++ предлагаются специальные функции-члены, называемые конструкторами класса, которые предназначены для создания новых объектов и присваивания значений их членам-данным. Если говорить точнее, то C++ регламентирует имя для таких функций- членов, а также синтаксис их вызова, тогда как ваша задача — написать определение этого метода. Имя метода конструктора совпадает с именем класса. Например, возможный конструктор для класса Stock — это функция-член Stock (). Прототип и заголовок конструктора обладают интересным свойством: несмотря на то, что конструкторы не имеют возвращаемого значения, они не объявляются с типом void. Фактически конструкторы не имеют объявленного типа.
Объявление и определение конструкторов
Теперь потребуется написать конструктор Stock. Поскольку объект Stock имеет три значения, которые ему нужно получить из внешнего мира, вы должны передать конструктору три аргумента. (Четвертое значение — это член totalval; он вычисляется на основе shares и shareval, поэтому передавать его конструктору не понадобится.) Возможно, вы решите передать только значение члена company и установить
остальные члены в нули; это можно сделать с использованием аргументов по умолчанию. Таким образом, прототип будет выглядеть следующим образом:
// Прототип конструктора с несколькими аргументами по умолчанию
Stock (const string & со, long n = 0, double pr = 0.0) ;
Первый аргумент представляет собой указатель на строку, используемую для инициализации члена класса company типа string. Аргументы п и рг предоставляют значения для членов shares и shareval. Обратите внимание, что тип возвращаемого значения не указан. Прототип размещен в открытом разделе объявления класса. Ниже приведен один из вариантов определения конструктора:
// Определение конструктора
Stock::Stock(const string & со, long n, double pr)
{
company = со;
if (n < 0)
{
std::cerr << "Number of shares can't be negative; "
<< company « " shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot () ;
}
Это тот же код, который использовался ранее в предыдущих статьях для функции acquire (). Разница в том, что в данном случае программа автоматически вызовет конструктор при объявлении объекта.
Имена членов и имена параметров
Новички часто пытаются использовать имена переменных-членов класса в качестве имен аргументов в конструкторе, как показано в следующем примере:
// Так поступать нельзя!
Stock::Stock(const string & company, long shares, double share_val)
}
Это неверно. Аргументы конструктора не являются переменными-членами; они представляют значения, которые присваиваются членам класса. Таким образом, они должны иметь отличающиеся имена, иначе вы столкнетесь с непонятным кодом вроде такого:
shares = shares;
Одним из часто используемых способов, призванных помочь избежать этого, является
использование префикса т_ для идентификации данных-членов:
class Stock
{
private:
string m_company;
long m_chares;
Другой также часто применяемый способ заключается в применении суффикса в виде подчеркивания для имен членов:
class Stock
{
private:
string company_;
long shares_;
Воспользовавшись одним из соглашений, в качестве имен параметров в открытом интерфейсе можно использовать company и shares.
Использование конструкторов
Язык C++ предлагает два способа инициализации объектов с помощью
конструктора. Первый — вызвать конструктор явно:
Stock food = Stock ("World Cabbage", 250, 1.25);
Это устанавливает значение члена company объекта food равным строке "World
Cabbage", значение shares равным 250 и т.д.
Второй способ — вызвать конструктор неявно:
Stock garment("Furry Mason", 50, 2.5);
Эта более компактная форма эквивалентна следующему явному вызову:
Stock garment = Stock("Furry Mason", 50, 2.5);
C++ использует конструктор класса всякий раз, когда вы создаете объект класса, даже если применяется операция new для динамического выделения памяти. Ниже показано, как использовать конструктор вместе с new:
Stock *pstock = new Stock("Electroshock Games",18,19.0);
Оператор, создающий объект Stock, инициализирует его значениями,
переданными в аргументах, и присваивает адрес нового объекта указателю pstock. В этом случае объект не имеет имени, но для управления объектом можно применять указатель.
Конструкторы используются способом, отличным от всех остальных методов класса. Обычно объект применяется для вызова метода:
stock1.show(); // объект stock вызывает метод show()
Однако нельзя использовать объект для вызова конструктора, поскольку до тех пор, пока конструктор не завершит создание объекта, его не существует. Вместо того чтобы вызываться объектом, конструктор служит для создания объекта.
Конструкторы по умолчанию
Конструктор по умолчанию — это конструктор, который используется для создания объекта, когда не предоставлены явные инициализирующие значения. То есть это конструктор, который применяется для объявлений, подобных показанному ниже:
Stock fluffy_the_cat; // используется конструктор по умолчанию
Причина, по которой этот оператор
работает, состоит в том, что если вы забудете о написании конструкторов, то C++ автоматически создаст конструктор по умолчанию. Это — неявная версия конструктора по умолчанию, который ничего не делает. Для класса Stock конструктор по умолчанию будет таким:
Stock::Stock() { }
В результате создается объект fluffy_the_cat с инициализированными членами, как в следующем операторе создается х без указания его значения:
int x;
То, что конструктор по умолчанию не имеет аргументов, отражает факт отсутствия значений в объявлении.
Любопытным моментом, имеющим отношение к конструктору но умолчанию, является то, что компилятор создает его, только если вы не определите ни одного собственного конструктора. После того, как вы определите хотя бы один конструктор класса, компилятор перестанет создавать конструктор по умолчанию. Если вы предоставите конструктор не по умолчанию вроде Stock (const string & со, long n, double
pr), но не предложите собственную версию конструктора по умолчанию, то
следующее объявление вызовет ошибку:
Stock stockl; // невозможно с существующим конструктором
Причина такого поведения в том, что может понадобиться запрет создания
неинициализированных объектов. Если же, однако, вы предпочитаете создавать объекты без явной инициализации, то должны будете определить собственный конструктор* Этот конструктор не имеет аргументов. Конструктор по умолчанию можно создать двумя способами. Один из них предусматривает указание значений по умолчанию для всех аргументов в существующем конструкторе:
Stock (const string & со = "Error", long n = 0, double pr = 0.0)
Второй способ — использование возможности перегрузки функций для
определения второго конструктора без аргументов:
Stock();
Допускается наличие только одного конструктора по умолчанию, поэтому
удостоверьтесь, что не создали их два. На самом деле обычно вы должны
инициализировать объекты для гарантии того, что при создании объекта все члены получают
известные и подходящие значения. Таким образом, пользовательский конструктор по
умолчанию, как правило, обеспечивает явную инициализацию всех
переменных-членов. Например, ниже показано, как можно определить конструктор по умолчанию для
класса Stock:
Stock:: Stock () // конструктор по умолчанию
{
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
Совет
При проектировании класса обычно должен быть предусмотрен конструктор по умолчанию, который неявно инициализирует все переменные-члены класса.
После создания конструктора по умолчанию любым из двух способов (без
аргументов или с предоставлением значений по умолчанию для всех аргументов) можно объявлять объектные переменные без явной инициализации:
Stock first; // вызывает конструктор по умолчанию неявно
Stock first = Stock (); // вызывает конструктор по умолчанию явно
Stock *prelief = new Stock; // вызывает конструктор по умолчанию неявно
Однако вас не должна сбивать с толку неявная форма конструктора не по
умолчанию:
Stock first("Concrete Conglomerate");// вызывает конструктор
Stock secondO; // объявляет функцию ,.
Stock third; // вызывает конструктор по умолчанию
Первое объявление из приведенных выше вызывает конструктор не по
умолчанию — т.е. такой, который принимает аргументы. Второе объявление устанавливает, что second () — это функция, возвращающая объект Stock. При неявном вызове конструктора по умолчанию круглые скобки указываться не должны.
Деструкторы
В случае использования конструктора для создания объекта программа
отслеживает этот объект до момента его исчезновения. В этот момент программа автоматически вызывает специальную функцию-член с несколько пугающим названием деструктор. Деструктор призван очищать всяческий "мусор", поэтому он служит весьма полезной цели. Например, если в конструкторе используется операция new для выделения памяти, то деструктор должен обратиться к delete для ее освобождения. Конструктор нашего класса Stock не делает никаких причудливых действий наподобие вызова new, поэтому деструктору класса Stock делать нечего. В таком случае вы можете просто позволить компилятору сгенерировать неявный деструктор, который ничего не делает, что и имеет место в первой версии класса Stock. С другой стороны, полезно посмотреть, как объявляются и определяются деструкторы, поэтому давайте предусмотрим
это в классе Stock.
Как и конструктор, деструктор имеет специальное имя. Оно формируется из имени класса и предваряющего его символа тильды (~). То есть деструктор для класса Stock называется -Stock (). Подобно конструктору, деструктор не имеет ни возвращаемого значения, ни объявляемого типа. Однако в отличие от конструктора, деструктор не должен иметь аргументы. Таким образом, прототип деструктора класса Stock выглядит следующим образом:
-Stock ();
Поскольку деструктор Stock не имеет никаких обязанностей, его можно
кодировать как функцию, которая ничего не делает:
Stock::-Stock()
{
}
Но просто для того, чтобы увидеть, когда вызывается конструктор, определим его следующим образом:
Stock::-Stock()
{
cout « "Bye, " « company « "!\n";
}
Когда должен вызываться деструктор? Этим управляет компилятор. Обычно деструктор не должен явно вызываться в коде. (Исключение из этого правила описано в главе 12.) Если вы создаете статический объект класса, то его деструктор вызывается автоматически при завершении работы программы. Если вы создаете автоматический (локальный) объект класса, как в приведенном примере, то его деструктор вызывается автоматически, когда выполнение программы покидает блок кода, в котором определен объект. Если объект создается с использованием операции new, он размещается в свободной памяти, и его деструктор вызывается автоматически, когда вызывается delete для ее освобождения. И, наконец, программа может создавать временные объекты для обслуживания некоторых операций; в этом случае деструктор вызывается тогда, когда программа завершает пользование объектом.
Поскольку деструктор вызывается автоматически при уничтожении объекта класса, деструктор должен существовать. Если вы его не предусмотрите, компилятор неявно создаст конструктор по умолчанию и, если обнаружит код, который ведет к уничтожению объекта, также неявно создаст деструктор.
Конец
Спасибо, если вы прочитали эту статью. Надеюсь вы что-то новое узнали для себя, и конечно же поняли. Подпишитесь, поставьте лайк, напишите комментарий, поддержите меня. Хотелось бы увидеть как улучшать статьи и чего не хватает. Буду анализировать и улучшать контент. Еще раз спасибо, до свидания! :)
#ооп #c++ #языкипрограммирования #классы #структура #компилятор #разработка #программирование #программа #интерфейс