Способы создания и разрушения объектов




Пространство имён.

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

Пространство имен namespace это новый элемент языка и для работы с STL мы обязаны принять его во внимание. Этот элемент создан для программ созданных из многих файлов, в которых есть опасность конфликта имен.

Объявляется пространство имен командой namespace:

C++ Спецификацияnamespace [идентификатор]{ описание для этой рабочей области }

Для использования рабочей области применяется команда using namespace

#include "stdafx.h"#include "iostream.h" namespace spaceA{ int MyVal=10;} namespace spaceB{ int MyVal=20;} namespace spaceC{ int MyVal=30;} void Test(){ using namespace spaceB; cout << MyVal << " " << "spaceB" << endl;} void main(){ using namespace spaceA; cout << MyVal << " " << "spaceA" << endl; Test(); cout << spaceC::MyVal << " " << "spaceC" << endl;}

 


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

Класс может наследовать функциональность от нескольких классов. Это называется множественным наследованием. Множественное наследование создаёт известную проблему (в C++), когда класс наследуется от нескольких классов-посредников, которые в свою очередь наследуются от одного класса (так называемая «Проблема ромба»): если метод общего предка был переопределён в посредниках, неизвестно, какую реализацию метода должен наследовать общий потомок. Решается эта проблема путём отказа от множественного наследования для классов и разрешением множественного наследования для полностью абстрактных классов (то есть интерфейсов) (C#, Delphi, Java), либо через виртуальное наследование (C++).
37. Статические члены класса

Члены класса могут быть объявлены с использованием модификатора класса памяти static

. Такие члены данных разделяются всеми экземплярами данного класса и хранятся в одном месте. Нестатические члены данных создаются для каждой переменной-объекта класса.

Отсутствие возможности объявлять статически члены класса привело бы к необходимости объявлять эти данные на глобальном уровне программы. Это разорвало бы отношения между данными и их классом, а также не согласуется с основной парадигмой ООП – объединение в классе данных и методов для их обработки. Статический член позволяет данным класса, которые не специфичны для отдельного экземпляра, существовать в области видимости класса.

Так как статический член класса не зависит от конкретного экземпляра, то обращение к нему выглядит следующим образом:

class_name::variable

где class_name – это имя класса, а variable означает имя члена класса.

Как видите, для обращения к статическому члену класса используется оператор разрешения контекста::. При обращении к статическому члену внутри методов класса оператор контекста необязателен.

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

Статический член класса можно явно проинициализировать нужным значением, для этого он должен быть определен и проинициализирован на глобальном уровне. Например, у нас есть класс CParser, предназначенный для синтаксического разбора текстов, и нам необходимо считать общее количество обработанных слов и символов. Достаточно объявить нужные члены класса статическими и инициализировать их на глобальном уровне. Тогда все экземпляры класса при работе будут использовать общие счетчики слов и символов.

//+------------------------------------------------------------------+ //| Класс "Анализатор текстов" | //+------------------------------------------------------------------+ class СParser { public: static int s_words; static int s_symbols; //--- конструктор и деструктор Parser(void); ~Parser(void){}; };... //--- инициализация статических членов класса Parser на глобальном уровне int CParser::s_words=0; int CParser::s_symbols=0;

Способы создания и разрушения объектов

Особой разновидностью методов являются конструкторы и деструкторы. Создание объекта включает выделение памяти под экземпляр и инициализацию его полей, а разрушение - очистку полей и освобождение памяти. Действия по инициализации и очистке полей специфичны для каждого конкретного класса объектов. По этой причине язык Delphi позволяет переопределить стандартный конструктор Create и стандартный деструктор Destroy для выполнения любых полезных действий. Можно даже определить несколько конструкторов и деструкторов (имена им назначает сам программист), чтобы обеспечить различные процедуры создания и разрушения объектов.
Объявление конструкторов и деструкторов похоже на объявление обычных методов с той лишь разницей, что вместо зарезервированных слов function и procedure используются слова constructor и destructor. Пример:
type TPeople = class
Name: string;
Family: string;
procedure GetName;
procedure GetFamily;
construcor Create;
destrucot Destroy;
end;
Возможная реализация:

procedure TPeople.Create;
begin
TPeople.Name:= ' ';
TPeople.Family:= ' ';
end;
procedure TPeople.Destroy;
begin
//Пока ничего не делаем
end;
Если объект содержит встроенные объекты или другие динамические данные, то конструктор - это как раз то место, где их нужно создавать. Конструктор применяется к классу или к объекту. Конструктор создаёт новый объект только в том случае, если перед его именем указано имя класса. Если указать имя уже существующего объекта, он поведёт себя по-другому: не создаст новый объект, а только выполнит код, содержащийся в теле конструктора. Если он применяется к классу,

People:= TPeople.Create;

то выполняется следующая последовательность действий:
1. В динамической памяти выделяется место для нового объекта.
2. Выделенная память заполняется нулями. В результате все числовые поля и поля порядкового типа приобретают нулевые значения, строковые поля становятся пустыми, а поля, содержащие указатели и объекты получают значение nil.
3. Затем выполняются заданные программистом действия конструктора.
4. Ссылка на созданный объект возвращается в качестве значения конструктора. Тип возвращаемого значения совпадает с типом класса, использованного при вызове (в нашем примере это тип TPeople).

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

People.Create;

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

Деструктор уничтожает объект к которому применяется:

People.Destroy;

В результате выполняются:
1. Заданный программистом код завершения.
2. Освобождается занимаемая объектом динамическая память.
В теле деструктора обычно должны уничтожаться встроенные объекты и динамические данные, как правило, созданные конструктором. Как и обычные методы, деструктор может иметь параметры, но эта возможность используется крайне редко.
23.Иерархия классов

Иерархию классов в ООП можно построить по-разному. Фактически иерархия классов является классификатором объектов. В данном случае при построении системы классов разработчик пытается принять во внимание следующие соображения «Столь ли существенна разница между рублевыми и валютными вкладами, что их следует разделить на различные классы?», «Разные виды Депозитов — это разные характеристики одного и того же класса или же разные классы?» и т.п.

Как видно из рисунка, представленного выше, разница между рублевым и валютным счетом настолько существенна, что они выделены в разные классы. Разные виды Депозитов также представлены разными классами. Если бы решили, что денежная единица, в которой выражается сумма на счете, — лишь дополнительны атрибут счета, и разные типы депозитов различаются дополнительной характеристикой класса «Депозит», то иерархия классов преобразовалась бы к виду, изображенному на рисунке:


25,26,27.

Инкапсуляция. Виды доступа к методам и атрибутам, видимость. Примеры (эл. конспект лекций);

Полиморфизм. Определение, использование полиморфизма в ООП. Примеры (эл. конспект лекций);

Наследование. Определение, использование наследования в ООП. Примеры (эл. конспект лекций);

 

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

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

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

Второй определяющей характеристикой объектно-ориентированных технологий является полиморфизм. Полиморфизм, попросту говоря, означает, что клиент может рассматривать разные объекты как одинаковые и каждый из объектов будет вести себя соответствующим образом. Возможность рассматривать разные вещи единообразно – суть полиморфизма. Простой пример: сущности классов "стол", "стул" и "шкаф" - это представители разных классов, но на определенном уровне абстракции они единообразны и все являются экземплярами класса "мебель".

Последняя определяющая характеристика традиционных объектно-ориентированных технологий – наследование. Идея проста: имея один объект, можно создать новый, автоматически поддерживающий все или некоторые «способности» старого. Следует отметить различия наследования реализации и наследование интерфейса. В первом случае объект наследует от своего родителя код. В противоположенность этому наследование интерфейса в действительности означает повторное использование спецификации – определений методов, поддерживаемых объектом. Наследование интерфейса облегчает к тому же реализацию полиморфизма.

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

Наследование позволяет повторно использовать существующие исходные тексты программ, подправлять их и перекомпилировать. Эта способность готового к компиляции исходного текста названа расширяемостью.

Для дополнения класса shape (фигура) классом circle (круг), достаточно лишь объявления его в классе shape (без изменения функций элементов класса) в модуле shape.h и скомпилировать исходный текст в shape.obj. Таким образом нет необходимости изменять исходный текст shape.c. Успешное создание потомков позволяет увеличивать программу за счет накопления уже готовых текстов программ.

 

Порядок вызова конструкторов и деструкторов по цепи наследования строго регламентирован. Вначале вызываются конструкторы базовых классов. Следом вызываются конструкторы производных классов. Поскольку конструкторы не наследуются, при создании производного класса наследуемые им данные-члены должны инициализироваться конструктором базового класса. Конструктор базового класса вызывается автоматически и выполняется до конструктора производного класса. Параметры конструктора базового класса указываются в определении конструктора производного класса. Таким образом происходит передача аргументов от конструктора производного класса конструктору базового класса.

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

Пример.

class Basiс {

int i; // сокрытие переменной

public:

Basis(int x){ i=x; }

int GetI(void) {return I;} // получить значение скрытой переменной

void SetI(int k) {i=k;} // установить значение скрытой переменной

};

 

class Inherit: public Basis { // класс-наследник

int count;

public:

Inherit(int x,int c): Basis(x){ count=c; }

};

 

 


 

31,32,33.

Перегрузка унарных операций (УМК); Перегрузка бинарных операций (УМК);

Перегрузка и переопределение операторов и функций. Пример реализации нескольких методов с одинаковым именем в одном классе. (конспект лекций);

Одним из подходов реализации принципа полиморфизма в языке С++ является использование переопределения функций. В С++ две и более функций могут иметь одно и то же имя. Компилятор С++ оперирует не исходными именами функций, а их внутренними представлениями, которые существенно отличаются от используемых в программе. Эти имена содержат в себе скрытое описание типов аргументов. С этими же именами работают программы компоновщика и библиотекаря. По этой причине мы можем использовать функции с одинаковыми именами, только типы аргументов у них должны быть разными. Именно на этом и основана реализация одной из особенностей полиморфизма. Заметим, что компилятор не различает функции по типу возвращаемого значения. Поэтому для компилятора функции с различным списком аргументов – это разные функции, а с одинаковым списком аргументов, но с разными типами возвращаемого значения - одинаковые. Для корректной работы программ последнего следует избегать. Рассмотрим простой пример переопределения функции sum, выполняющей сложение нескольких чисел различного типа.

#include "iostream.h"

Class cls

{ int n;

double f;

public:

cls(int N,float F): n(N),f(F) {}

int sum(int); // функция sum с целочисленнным аргументом

double sum(double); // функция sum с дробным аргументом

void see(); // вывод содержимого объекта

};

int cls:: sum(int k)

{ n+=k;

return n;

}

double cls:: sum(double k)

{ f+=k;

return f;

}

void cls:: see()

{cout <<n<<' '<<f<<endl;}

 

void main()

{ cls obj(1,2.3);

obj.see(); // вывод содержимого объекта

cout <<obj.sum(1)<<endl; // вызов функции sum с целочисл. аргументом

cout <<obj.sum(1.)<<endl; // вызов функции sum с дробным аргументом

}

Результат работы программы:

1 2.3

3.3

В тоже время переопределение функций может создавать проблему двусмысленности. Например:

Class A

{ ...

public:

void fun(int i,long j) {cout<<i+j<<endl;}

void fun(long i,int j) { cout<<i+j<<endl;}

};

 

main()

{ A a;

a.fun(2,2); // ошибка неизвестно какая из 2 функций вызывается

}

В этом случае возникает неоднозначность вызова функции fun объекта a.

 

В ООП программист может определять смысл операций при их применении к объектам определенного класса. Кроме арифметических, можно определять еще и логические операции, операции сравнения, вызова () и индексирования [], а также можно переопределять присваивание и инициализацию. Можно определить явное и неявное преобразование между определяемыми пользователем и основными типами.

Возможность использовать знаки стандартных операций для записи выражений как для встроенных, так и для абстрактных типов данных. В языке С++ для перегрузки операций используется ключевое слово operator, с помощью которого определяется специальная операция-функция (operator function). Ее формат:

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

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

Перегрузка унарных операций. Любая унарная операция Å может быть определена двумя способами: либо как компонентная функция без параметров, либо как глобальная функция с одним параметром. В первом случае выражение ÅZ означает вызов Z.operatorÅ (), во втором - вызов operatorÅ(Z). Вызываемый объект класса автоматически воспринимается как операнд.

Пример.

class audio_player {

int records_count;

int current_record;

...

public:

play(int i);

void init(void){ current_record = 0; }

void operator++ (){ //++a

current_record++;

if(current_record>records_count) current_record = 0;}

void operator++ (int i){ //a++

current_record++;

if(current_record>records_count) current_record = 0;}

void operator—(){ //--a

current_record--;

if(current_record<0) current_record = records_count - 1;}

};

void main(){

audio_plajer a;

a.init();

++a;

a++;

}

Перегрузка бинарных операций. Любая бинарная операция Å может быть определена двумя способами: либо как функция-член класса с одним параметром, либо как глобальная функция с двумя параметрами. В первом случае xÅy означает вызов x.operatorÅ (y), во втором – вызов operatorÅ (x,y).

class Car{

int model; int year; int color;

public:

bool __fastcall operator== (Car& c){

return (model==c.model) && (year==c.year) && (color==c.color);}

};


 

28.Виртуальные функции. Раскройте смысл понятий «раннего» и «позднего» связывания в ООП (УМК, конспект лекций);

Ранее связывание имеет место в случае переопределяемых методов, когда компилятор умеет отличать один вызов от другого по типу их аргументов. Используя эту информацию, он "жестко" связывает коды программы с соответствующими методами. Особым случаем раннего связывания являются вызовы статических методов. К статическим данным и коду можно обращаться и тогда, когда объект класса еще не существует. Доступ к статическим членам возможен не только через имя объекта, но и через имя класса:

имя_класса:: имя_компонента

Однако так можно обращаться только к public членам. Для обращения к private статической компоненте извне можно с помощью статических методов. Пример.

#include <iostream.h>

class Car {

int speed;

static int CarCount; // статический атрибут – количество машин

public:

Car(int s) {

speed = s;

CarCount++;

}

static int& count() {

return CarCount;

}

};

 

int Car::CarCount = 0; //инициализация статического атрибута

 

void main(void) {

Car c1(20); Car c2(30);

cout << ” Количество машин = “ << Car::count();

}

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

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

virtual тип имя_функции (список_формальных_параметров);

Виртуальность наследуется. После того как функция определена как виртуальная, ее повторное определение в производном классе (с тем же самым прототипом) создает в этом классе новую виртуальную функцию, причем спецификатор virtual может не использоваться.

Конструкторы не могут быть виртуальными, в отличие от деструкторов. Практически каждый класс, имеющий виртуальную функцию, должен иметь виртуальный деструктор. Так как вызов невиртуальной функции-элемента выполняется несколько быстрее, чем виртуального, то в общем случае рекомендуется, когда не встает вопрос о расширяемости, но существенное значение имеет производительность, пользоваться обычными функциями элементами. В противном случае, нужно пользоваться виртуальными. Пример описания виртуальной функции:

virtual int funct1(void); // определение виртуальной функции

Важно понимать, что виртуальные функции не могут быть статическими. Виртуальные функции могут не переопределяться в производных классах. В случае переопределения виртуальных функций, количество и типы аргументов должны (с необходимостью) совпадать с определением базовой виртуальной функции. Переопределение здесь равносильно замене базовой (именно базовой) функции на другую. Можно описать фукцию int Base::Fun(int) и int Derived::Fun(int) даже не виртуальную. В этом случае int Derived::Fun(int) говорит, что все другие версии функции Fun(int), описанные в базовых класса, скрыты.




Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2016-08-20 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: