Конструкторы и деструкторы

Конструктором называется функция-член класса, которая выделяет память под поля данных класса и производит их инициализацию, т.е. задает начальные значения в месте объявления переменных.

Имя конструктора совпадает с именем класса. Например, в классе Location конструктор имеет следующий вид: Location (int _x, int _y).

Конструктор не возвращает никакого значения, даже void.

Одним из важных свойств конструктора является его автоматический вызов при описании любого объекта какого-либо класса, использующего конструктор, что снимает с программиста задачу своевременного отслеживания инициализации вновь вводимых объектов.

В общем случае конструкторы классов могут иметь списки параметров, которые могут потребоваться при инициализации. При этом программист будет обязан задать список инициализации при описании каждого нового объекта.

Конструкторов в классе может быть много. В этом случае реализуется механизм перегрузки функции.

Если конструкторы не объявлены, компилятор сам создает конструктор без параметров по умолчанию.

Объявление объектов можно проиллюстрировать следующим образом:

 

void main (void)

{Location NK(0,0), KK(10,10), *PL;

cout<<KK.Getx(); //возвращаемое значение : 10

PL=&NK;

cout<<PL->Gety(); //возвращаемое значение : 0

}

 

Здесь при объявлении NK(0,0) и KK(10,10) неявно вызываются конструкторы.

cout<<KK.Getx() обращение идет через переменную.

cout<<PL->Gety() обращение идет через указатель.

 

Конструктор копий

{Location A(1,1),B,D=A;

… }

Сначала создается объект D и он инициализируется значением объекта A. Для инициализации нужно явно определить конструктор.

В конструкторе копий в качестве параметра используется простая или константная ссылка на объект.

Location::Location([const]Location &S)

{x=S.x; y=S.y}

 

Для каждого из объектов класса при очистке памяти компилятором создается деструктор по умолчанию. Определяется деструктор следующим образом: ~ имя. Имя деструктора совпадает с именем класса, но с символом ~ (тильда) в начале.

Деструктор решает обратную конструктором задачу, т.е. очищает память.

Если в конструкторе объекта запрашивается динамическая память или открывается файл, то при уничтожении объекта необходимо предусмотреть действия по очистке памяти и закрытию файла. В этом случае пользователю необходимо определять деструктор. Этот деструктор будет вызываться при выходе объекта из области видимости.

Локальные объекты удаляются тогда, когда они выходят из области видимости. Глобальные объекты удаляются при завершении программы.

 

Наследование

Применительно к C++ наследование – это механизм, посредством которого один класс может наследовать свойства другого. Наследование позволяет строить иерархию классов, переходя от более общих к более специальным.

Класс, свойства и поведение которого наследуются, называется базовым классом.

Класс, который наследует называет, называется производным классом.

Обычно процесс наследования начинается с задания базового класса. Базовый класс определяет все те качества, которые будут общими для всех производных от него классов. В сущности, базовый класс представляет собой наиболее общее описание ряда характерных черт. Производный класс наследует эти общие черты и добавляет свойства, характерные только для него.

Наследование, при котором указывается один базовый класс, называется простым.

Если указываются несколько классов, то наследование называется множественным.

 

Объявление выглядит следующим образом:

class имя класса : public имя базового класса

Например, class D: public A

{ … }

После имени класса D имеется двоеточие, за которым следует ключевое слово public и имя класса A. Для компилятора это указание на то, что класс D будет наследовать все компоненты класса A. Само ключевое слово public информирует компилятор о том, что, поскольку класс A будет наследоваться, значит, все открытые элементы базового класса будут также открытыми элементами производного класса. Однако все закрытые элементы базового класса останутся закрытыми и к ним не будет прямого доступа из производного класса. Причина, по которой закрытые члены класса становятся недоступными для производных классов – поддержка инкапсуляции. Если бы закрытые члены класса становились открытыми просто посредством наследования этого класса, инкапсуляция была бы совершенно несостоятельна.

При множественном наследовании объявление выглядит так:

class D: public A [, public C]

{ тело класса D}

 

Рассмотрим пример:

enum Bool

{false, true}; //константы сводятся к int. Они изменяются с шагом равным единице.

class Point: public Location

{protected:

Bool vis;

public:

Point (int _x, int _y);

void Show();

void Hide();

};

Point::Point (int_x, int_y) : Location(_x, _y)

{vis=false;}

Здесь класс Point наследует свойства базового класса Location.

 

Наследование и контроль доступа

Спецификатор доступа определяет то, как элементы базового класса наследуются производным классом. Если спецификатором доступа наследуемого базового класса является ключевое слово public, то все открытые члены базового класса остаются открытыми и в производном. Если спецификатором доступа наследуемого базового класса является ключевое слово private, то все открытые члены базового в производном классе становятся закрытыми. В обоих случаях все закрытые члены базового класса в производном классе остаются закрытыми и недоступными.

Важно понимать, что если спецификатором доступа является ключевое слово private, то хотя открытые члены базового класса становятся закрытыми в производном, они остаются доступными для функций – членов производного класса.

Доступ к полям базового класса в производном классе может быть сохранен или ужесточен, но никогда не может быть облегчен. Чтобы нагляднее представить себе этот принцип, обратимся к таблице:

 

Доступ наследования Доступ компонентов в базовом классе Доступность компонентов базового класса в производном классе
  public private protected public Нет доступа protected public  
  protected private protected public Нет доступа protected protected  
  private private protected public Нет доступа private private

 

 

Указатель this

Когда функция, принадлежащая классу, вызывается для обработки данных конкретного объекта, этой функции автоматически и неявно передается указатель на тот объект, для которого функция вызвана. Этот указатель имеет фиксированное имя this и незаметно для программиста (“тайно”) определен в каждой функции класса следующим образом:

имя_класса * const this=адрес обрабатываемого объекта;

 

Имя this является служебным (ключевым) словом. Явно описать или определить указатель this нельзя и не нужно. В соответствии с неявным определением this является константным указателем, т.е. изменить его нельзя, однако в каждой принадлежащей классу функции он указывает именно на тот объект, для которого функция вызывается. Говорят, что указатель this является дополнительным (скрытым) параметром каждой нестатической компонентной функции. Другими словами, при входе в тело принадлежащей классу функции указатель this инициализируется значением адреса того объекта, для которого вызвана функция. Объект, который адресуется указателем this, становится доступным внутри принадлежащей классу функции именно с помощью указателя this. При работе с компонентами класса внутри принадлежащей классу функции можно было бы везде использовать этот указатель.

Таким образом, каждая нестатическая функция – элемент класса имеет доступ к объекту, для которого она вызвана через указатель this.

Рассмотрим пример:

comp &operator+(comp)

{real=real+x.real;

im=im+x.im;

return *this;}

В примере реализована для класса comp перегрузка операции сложения. Здесь последовательно складываются действительные и мнимые части. Возвращение результата происходит через указатель this. Если нужно вернуть адрес объекта, то пишется return this.

 

Друзья

Дружественной функцией класса называется функция, которая, не являясь его компонентом, имеет доступ к его защищенным и собственным компонентам.

Функция не может стать другом класса “без его согласия”. Для получения прав друга функция должна быть описана в теле класса со спецификатором friend. Именно при наличии такого описания класс предоставляет функции права доступа к защищенным и собственным компонентам.

 

class C

{ …

friend class A; }

Все функции класса A имеют доступ к закрытым полям класса C.

Дружба не носит ”сквозного” характера (не обладает свойством транзитивности): если класс A друг класса B, а класс B друг класса C, то это не означает, что A друг C.

 

Отметим особенности дружественных функций. Дружественная функция при вызове не получает указателя this. Объекты классов должны передаваться дружественной функции только явно через аппарат параметров. При вызове дружественной функции нельзя использовать операции выбора:

имя_объекта.имя_функции и указатель_на_объект->имя_функции

 

Перегрузка операций

На все операции языка C++, кроме операций объявления, new, delete, и других операций, связанных с определением производных типов данных, распространяется свойство полиморфизма, т.е. возможности использования в различных случаях для одной и той же операции операндов различных типов. Так, например, операция сложения позволяет “смешивать” типы int, double, float и другие в одном выражении. Такой полиморфизм обеспечен внутренними механизмами языка C++.

Таким образом, нельзя перегружать такие операции: . :: * ?:

Чтобы появилась возможность использовать стандартную для языка C++ операцию с необычными для нее данными, необходимо специальным образом определить ее новое поведение. Это возможно, если хотя бы один из операндов является объектом некоторого класса, т.е. введенного пользователем типа. В этом случае применяется механизм, во многом схожий с механизмом определения функций. Для распространения действия операции на новые пользовательские типы данных программист определяет специальную функцию, называемую “операция-функция” (operator function). Формат определения операции-функции:

 

тип_возвращаемого_значения operator знак_операции (спецификация параметров операции-функции)

{операторы тела операции-функции }

При необходимости может добавляться и прототип операции-функции с таким форматом:

 

тип_возвращаемого_значения operator знак_операции (спецификация параметров операции-функции);

И в прототипе, и в заголовке определения операции-функции используется ключевое слово operator, вслед за которым помещен знак операции. Если принять, что

конструкция operator знак_операцииесть имя некоторой функции,то определение и прототип операции-функции подобны определению и прототипу обычной функции языка C++. Например, для распространения действия бинарной операции * на объекты класса T может быть введена функция с заголовком T operator *(T x, T y).

Определенная таким образом операция (в нашем примере операция “ звездочка”) называется перегруженной (по-английски - overload), а сам механизм – перегрузкой или расширением действия стандартных операций языка C++.

Количество параметров у операции-функции зависит от арности операции и от способа определения функции. Операция-функция определяет алгоритм выполнения перегруженной операции, когда эта операция применяется к объектам класса, для которого операция-функция введена. Чтобы явная связь с классом была обеспечена, операция-функция должна быть либо компонентом класса, либо она должна быть определена в классе как дружественная, либо у нее должен быть хотя бы один параметр типа класс (или ссылка на класс).

Если для класса T введена операция-функция с приведенным выше заголовком и определены два объекта A и B класса T,то выражение A*B интерпретируется как вызов функции operator * (A,B).

 

Рассмотрим пример. Реализуем перегрузку операции сложения для класса комплексных чисел.

class comp

{float im; float real;

public:

comp(float i, float r)

{real=r;

im=i;}

 

comp operator +(comp X)

{return comp(im+X.im, real+X.real);}

}

void main()

{

comp C1(1,1), C2(5,5),C3;

C3=C1.operator+(C2) // Прямой вызов операции-функции. Не используется.

C3=C1+C2 // Косвенный вызов операции-функции.

}

Компилятор по типам объектов С1 и С2 определяет, что необходимо реализовать не просто сложение двух скаляров, как это бывает в обычном использовании операции +, а вызвать перегруженную функцию operator +.Так как при определении класса поля im и real доступны функциям класса, есть необходимость определять только второй объект (X в нашем примере).

В языке C++ требуется, чтобы операции присваивания, индексации и косвенного обращения к полям класса (->) обязательно определялись как методы, т.е. как функции-члены класса.

Когда левый операнд операции является представителем класса, перегруженную операцию нужно определять как метод этого класса.

Для многих операций C++ существуют свои особенности при перегрузке (доопределении). Так, унарные операции переопределяются с описанием операции-функции без аргумента, например:

class A

{

A operator --() {текст функции}

}

Соответственно доопределение бинарной операции использует описание операции-функции с одним аргументом, т.к. вторым является объект, для которого вызвана операция. Следует также помнить, что операция присваивания “=” может перегружаться только объявлением метода без описателя static. То же относится к операциям “()” и ”[]”.

Посмотрим, как будет выглядеть перегрузка операции присваивания для примера с комплексными числами.

comp & operator =([const] comp & X)

{real=X.real;

im=X.im;

return *this;}

Если указываем const, то это показывает, что параметр не должен изменяться внутри функции, а кроме того, позволяет обрабатывать константные объекты.

Операция присваивания не наследуется.