Вы на НеОфициальном сайте факультета ЭиП

На нашем портале ежедневно выкладываются материалы способные помочь студентам. Курсовые, шпаргалки, ответы и еще куча всего что может понадобиться в учебе!
Главная Контакты Карта сайта
 
Где мы?
» » » РЕАЛИЗАЦИЯ ОБЪЕКТНОЙ МОДЕЛИ В СИ ++. ИНКАПСУЛЯЦИЯ

Реклама


РЕАЛИЗАЦИЯ ОБЪЕКТНОЙ МОДЕЛИ В СИ ++. ИНКАПСУЛЯЦИЯ

Просмотров: 1759 Автор: admin

Данное пособие предназначено для тех, кто достаточно хорошо знаком с языком программирования Си, можно сказать, состоялся как программист на Си. Язык Си++ построен на твердом фундаменте Си, содержит все его полезные инструменты, оставаясь мощным и элегантным одновременно. Однако главное отличие Си++ от его предшественника заключается в том, что он поддерживает объектно-ориентированное программирование. Подробный разговор об объектно-ориентрованном анализе, проектировании и программировании невозможен в рамках небольшого пособия. Объектный подход это не только и не столько новая технология программирования, сколько новый взгляд на мир. Желающих можно отослать к соответствующей литературе. Наилучшим изданием на эту тему является книга Гради Буча «Объектно-ориентированный анализ и проектирование с примерами приложений на С++».

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

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

Материал излагается в определенном порядке, от простого к сложному. Изучение материала необходимо последовательное. Для того, чтобы самому научиться программировать на Си++, необходимо выполнять предложенные в пособии примеры или решать похожие задачи, варианты которых приведены в приложении.

Все примеры отлажены и протестированы в среде программирования BorlandC++

РЕАЛИЗАЦИЯ ОБЪЕКТНОЙ МОДЕЛИ В СИ ++. ИНКАПСУЛЯЦИЯ

Определение 1. Класс – это абстрактный тип данных. Представляет собой производный структурированный тип на базе существующих типов, задающий набор данных и определяющий набор операций над этими данными.

Определение 2. Объект (экземпляр класса) – это переменная, тип которой определен как «класс».

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

1)    диапазон значений;

2)    набор операций для переменных объявленного типа;

3)    имя для идентификации объекта.

Определение 3. Клиент – это программа, объявляющая объекты класса и манипулирующая ими. Взаимодействие выполняется на уровне запросов, в ответ на которые объект выполняет действие или изменяет состояние.

Использование классов в Си ++

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

Определение (спецификация) класса

Чтобы ввести в употребление абстрактный тип данных, необходимо определить класс следующим образом:


class имя
{
private:
         закрытые элементы;
protected:
         защищенные элементы;
publuc:
         открытые элементы;
};

Определение класса начинается с ключевого слова class, за которым следует имя класса (произвольный идентификатор), а далее в фигурных скобках определяется тело класса. Определение класса заканчивается знаком «точка с запятой», который указывает компилятору, что определение класса завершено.

Элементами класса могут быть как данные любых типов (определяют свойства или состояние объекта класса), так и функции (определяют методы или поведение объекта класса). В терминологии Borland C++ они называются data members и member functions, где member означает «любой элемент, часть». Используются также общие для данных и методов синонимы «поля класса», «атрибуты класса», и другие.

Метки прав доступа private, protectedи publicопределяют режим доступа к элементам класса. Могут следовать в любом порядке и количестве. Действуют до следующей метки или до конца описания. По умолчанию способ доступа назначается private.

К закрытым (private) элементам имеют доступ только методы самого класса и друзья класса (об этом позже). К защищенным (protected) элементам имеют доступ методы самого класса и методы наследников класса (об этом позже). К открытым (public) элементам имеют доступ все.

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

Рассмотрим пример определения простого класса. Имя класса One. Данными класса являются две переменные (aи b). Для работы с данными используются обычные для классов функции, позволяющие присвоить значение данным и вернуть значения.


class One {
private:
         int      a;                                            // Закрытые данные класса:
         float   b;                                            // числовые элементы a и b.
public:
         voidInit (inta1, floatb1) {                  // Открытые функции (методы) класса:
                   a = a1;                                     // присвоить значения,
b = b1;
}                
         intget_a (){                                       // вернуть значение данного а,
                   returna;
}                          
         floatget_b (){                                    // вернуть значение данного b.
                   returnb;
}
}; //EndofOne

Требования к именам классов нельзя сформулировать строго. Желательно, чтобы имя отражало смысл объекта, например, Shape(фигура) или Window (окно). Рекомендуется особым образом именовать абстрактные типы данных, чтобы по внешнему виду они отличались от просто данных. Обычно имя класса начинается с большой буквы. 

Создание объектов класса

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

Имя_типа имя_объекта;

Например, запись One    c1,c2; объявляет две переменные с именами c1 и c2, каждая из которых содержит по два данных (поля а и b).


Механизм хранения переменных типа «класс» имеет особенности. В объявляемые объекты (экземпляры) класса входят и хранятся только элементы данные класса. Компонентные функции, предназначенные для работы с данными конкретных объектов класса, в отличие от данных не тиражируются. Иными словами, на уровне реализации место в памяти выделяется именно для элементов данных каждого объекта класса. Так, для класса, объявленного в примере, распределение памяти под объекты c1, c2 приведено на рисунке 2.

Обращение к элементам класса

Обращение к элементам класса, как и к элементам структуры, выполняется с использованием операции прямого доступа (разыменования) «.» (точка). Синтаксически обращение выглядит так:

Имя_объекта_класса . имя_элемента_класса

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

Пример клиентской программы для работы с объектами класса One.


void main (void) {
// Объявление объектов класса.
One   c1, c2;
int      k;
// Обращение к методам класса.
c1.Init (1, 2.5);                         // Присвоить значения данным объекта С1.
c2.Init (3, 4.0};                         // Присвоить значения данным объекта С2.
        
k = c1.get_a ();                        // Получить значение поля а объекта С1.
k = c2.get_b ();                        // Получить значение поля b объекта С2.
} // main

Независимо от типа элемента класса (данное или функция), обращение к ним выполняется одинаково, а именно:

имя_класса.имя_данного

имя_класса.обращение_к_функции

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

k = c1.a;                             // Прямое обращение к элементу данному

                                          // (только для открытых полей). Здесь ошибка.

k = c1.get_a();                   // Обращение к закрытому данному с помощью

                                               // открытого метода.

Из объектов класса, как и из обычных переменных, можно строить более сложные структуры, например, массивы, структуры и прочие. Для класса One покажем, как ввести в употребление массив объектов класса.


#include <stdio.h>
void main(void) {
// Объявление массива из трех элементов типа One
One   c[3];
// Обращение к элементам массива выполняется через операцию
// разыменования [ ], а к полям класса через операцию разыменования «.»
for (int i=0; i<3; i++)                 // Присвоить значения данным объекта c
c.Init (i, i*2.);     
for (i=0; i<3; i++)                     // Получить значения
printf ("nЦелое=%3d Вещественное=%4.1f", c.get_a(), c.get_b());
} // main

Динамическое создание объектов класса

Объект класса можно создать динамически с использованием указателя на объект класса. Необходимо динамически выделить память под размещение объекта операцией new. Доступ к элементам динамического объекта выполняется через операцию доступа (разыменования), которая называется косвенной и записывается как лексема «->»:

Покажем, как ввести в употребление динамическую переменную класса One и обратиться к ее данным.


#include <stdio.h>
void main (void) {
One   *c3;                                         // Объявить указатель на объект с3.
c3 = new (One);                       // Создать объект с3, выделяя память.
c3 -> Init (1, 2.0);                     // Присвоить значения данным объекта с3.
                                                        // Получить значения.
printf ("nЦелое=%3d Вещественное=%4.1f", c3->get_a(), c3->get_b());
} // main

Интерфейс классов. Сокрытие информации

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

Для защиты своих данных от изменения класс использует метки прав доступа к элементам класса private. Они запрещают доступ к закрытым данным класса из клиентской программы.

Для обмена информацией с клиентом класс имеет общедоступный интерфейс, который обеспечивают методы типа public, благодаря которым клиент может манипулировать содержимым класса. Этот доступ является косвенным, т.к. определен по правилам интерфейса, отраженным в спецификации (объявлении) класса.

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

 

Область действия классов. Спецификация класса и реализация

Областью действия элементов класса (данных и методов) является тело класса, то есть имена элементов классов являются локальными для класса.

Областью действия имени класса является файл, в котором он определен.

При работе с большим многофайловым проектом с использованием классов существуют требования:

1)    класс должен быть виден отовсюду, где он используется:

2)    абстрактная структура класса должна быть прозрачна;

3)    интерфейсная часть класса должна быть удобна для использования.

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

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

Определение класса должно быть видно отовсюду, где предполагается использовать объекты данного класса, поэтому спецификация выносится в отдельный файл, и это файл заголовков (.h или .hpp). В нем открыта внешняя (интерфейсная) сторона методов. Файл заголовка инклудируется (#include) в тот файл, которому нужно определение классов, и потому доступен всем файлам проекта, которым он необходим.

Файлы реализации содержат код конкретных методов, скрывая их реализацию. Здесь записываются описания (объявление и тело) всех функций методов класса.

Файлы реализации имеют расширение .c или .cpp и могут компилироваться совместно с клиентской программой. В этом случае необходимо объединение файла реализации и клиентской программы в единый проект.

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

Пример организации многофайлового проекта для нашего простого класса приведен на рисунке 3.

 

файла спецификации для определения класса и файла реализации

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

Операция разрешения области видимости имен ( :: )

Среди новых операций в Си ++ введена операция разрешения области видимости имен :: . Она предназначена для разрешения коллизии имен, когда глобальная переменная скрыта локальной переменной с тем же именем. Операция :: позволяет «присоединить» к имени переменной имя глобального объекта, тем самым выполняя обращение изнутри к внешней переменной.

Самый простой пример – это доступ к глобальной переменной из функции.


#include <stdio.h>
int      i = 99;                            // Объявлена глобальная переменная.
void main (void) {
int      i = 10;                            // Объявлена локальная переменная с тем же именем.
printf ("nВнутренняя переменная=%d",     i );                        //Равно 10.
printf ("nВнешняя переменная=%d",         :: i );                     //Равно 99.
} // main

В ООП операция :: используется для тех же целей, а именно, чтобы «привязать» имя метода или имя данного к тому классу, где они объявлены. Этот механизм необходимо использовать в файлах реализации, когда описания методов выносятся из тела класса.

Функции, описываемые вне классов, должны быть привязаны к конкретному классу синтаксической конструкцией:

Имя_класса :: Имя_метода() {

// Тело функции

}

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

Подставляемые функции.

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

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

Например, возьмем метод get_a () класса One:


class One {

public:
int get_a ()                              // вернуть значение данного а,
                   { return a; }
…
}; //EndofOne
При обычном вызове метода должно происходить обращение к функции:
void main (void) {                                        
One   c1;                                                    int One::get_a() const {
int      k;                                                               return a;
k = c1.get_a ();                                 }
…
} // main

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

One       c1;

int                   k = c1.a;

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

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


inline int One :: get_a () {
         return a;
};

Итак, функции, описанные внутри класса, автоматически делаются подставляемыми (inline). Рекомендуется делать подставляемыми лишь небольшие простые функции из одного–двух операторов. 

Организация файлов спецификации и реализации.

Теперь покажем, как должен выглядеть наш проект. Заголовочный файл «primer.hpp» содержит абстрактное определение класса.


class One {
private:
         int      a;                                   // Необходимы пояснения к содержанию.
         float   b;
public:
         void Init (int a1, float b1);          // Объявления методов класса
         int      get_a();                          // Как видим, в описании присутствуют
         float   get_b();                          // только прототипы функций.
}; //End of One
Файл реализации «primer.сpp» содержит описание реализации методов класса.
#include "primer.hpp"                         // Обязательно везде, где
// используются объекты класса.
void One :: Init (int a1, float b1) {       // Оператор :: показывает, чей метод.
a = a1;
b = b1;
}
int One :: get_a () {
return a;
};
float One :: get_b () {
return b;
}
Простые методы get_a() и get_b(), скорее всего, будут подставляемыми. Рекомендация inlineдля явной подстановки может быть записана так:
inlineint One :: get_a () {
return a;
};
Файл клиентской программы «main.cpp» объявляет и манипулирует объектами классов. Может компилироваться совместно с файлами реализации, для чего они объединяются в проект.
#include "primer.hpp"
#include <stdio.h>
void main (void) {
One   С;
// Инициализировать объект С.
С1.Init (1, 9.9);
// Вывести данные объекта.
printf (“nОбъект: a=%4db=%6.1fn”, c1.get_a (), c1.get_b ());
} // main

 

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

Определение 1. Конструктор – функция, которая вызывается всегда при создании объекта класса (фактически при выделении памяти), в том числе операцией new.

Определение 2. Деструктор – функция, которая вызывается при уничтожении объекта класса (освобождении памяти).

Конструктор предназначен для инициализации создаваемых объектов. Деструктор предназначен для высвобождения памяти.

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

Существенной особенностью конструктора и деструктора как функций является то, что они не возвращают никакого значения, даже void.

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

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

Перегруженные функции

В Си ++ функция может перегруженной. Цель перегрузки состоит в том, чтобы функция с одним и тем же именем могла выполняться различными способами в зависимости от способа обращения к ней (это пример реализации идеи полиморфизма). Разрешается перегрузка по типу аргументов и по числу аргументов. Будем называть совокупность параметров функции (количество, порядок и типы) сигнатурой параметров. В любом случае для каждого перегружаемого имени можно определить, сколько различных функций с ним связано, или сколько вариантов сигнатур допустимо при обращении. Функция описывается столько раз, сколько необходимо, а при обращении к ней компилятор сам выбирает нужную реализацию функции в зависимости от фактических параметров обращения.

Пример 1. Перегрузка по типу параметров. Пусть необходимо выводить на печать данные типов int и float. Хочется, чтобы это делала функция с именем output (параметр), причем независимо от типа выводимого данного. Перегрузим output. Необходимо два описания функции.


#include      <stdio.h>
void   output (int);                    // Тип аргумента int
void   output (float);                  // Тип аргумента float
void main (void) {
int      a = 1;
float   b = 1.99;
output (a);                      // Обращение к функциям выглядит одинаково.
output (b);
}// main
// Перегруженные функции имеют различную реализацию.
void output (int x) {
printf ("n%d", x);
}
void output (float x) {
printf ("n%f", x);
}

При обращении компилятор анализирует тип фактических параметров и сам выбирает нужную реализацию функции. Это и называется перегрузкой.

Пример 2. Перегрузка по числу параметров. Покажем на примере поиска наибольшего значения из нескольких чисел (двух или трех). Прототипов функции max два. Обращение выполняется дважды (с двумя и тремя аргументами соответственно).


#include      <stdio.h>
int      max (int, int);                                    // Число аргументов 2
int      max (int, int, int);                               // Число аргументов 3
void main(void) {
int      a = 1, b = 2, c = 3, d;
d = max (a, b);                         // Обращение к функциям выглядит одинаково.
printf ("n%d", d);
d = max (a, b, c);
printf ("n%d", d);
}// main
// Реализация перегруженных функций различна.
int      max (int x, int y) {
return x>y ? x : y;
}
int      max (int x, int y, int z) {
return x>y ? (x>z ? x : z) : (y>z ? y : z);
}


Аргументы по умолчанию.

В Си ++ есть также возможность присвоить значения аргументам функции, которые будут приняты по умолчанию. Эти фактические значения присваиваются параметрам функции при ее объявлении (в прототипе функции). При обращении к функции умолчания могут быть заменены фактическими значениями или нет.

Пример. Функция рисования окружности требует задать значения трем атрибутам, полностью определяющим объект «окружность» на плоскости, а именно, координатам центра и радиусу. Функция рисования может быть определена так:

void DrawCircle (int x = 100, int y = 100, int r = 100);

Или так:

DrawCircle (int = 100, int = 100, int = 100);

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


void main(void) {
…
DrawCircle(500, 300, 50);            // Координаты 500, 300, радиус 50.
DrawCircle(530, 370);                  // Радиус 100 по умолчанию.
DrawCircle(530);                         // Координата y=100, радиус 100.
DrawCircle();                               // Все аргументы по умолчанию.
…
} // main

Замечание: Значения аргументов можно опускать как при описании, так и при обращении. При описании аргументы можно опускать слева направо, при обращении аргументы можно опускать справа налево. 

Использование и перегрузка конструкторов

Теперь перепишем определение класса One с использованием конструкторов и перегрузкой функций.

Пример. Класс такой же, как One, но вместо инициализирующей функции Init () используются конструкторы. Это позволяет инициализировать значения объектов класса при их создании.


#include <stdio.h>
class Two {
private:
int      a;
float   b;
public:
//Конструкторы c параметрами.
Two (int a1, float b1) {                       //Инициализирует оба параметра.
         a = a1;
b = b1;
}
Two (int a1) {                                    // Инициализирует первый параметр.
         float tmp;
         a = a1;
printf ("nВведите bn");
scanf ("%f", &tmp);b=tmp;
}
Two (float b1) {                                 //Второй инициализировать нельзя.
         b = b1;
printf ("nВведите an");
scanf ("%d", &a);
}
// Конструктор без параметров (по умолчанию).
Two() {
         a = 12;
b = 9.99;
}
// Конструктор без параметров может быть и таким (не оба вместе):
/*
Two() {
printf ("nВведите a,b n");
scanf ("%d%f", &a, &b);
}        */
//Деструктор.
~Two() {
         printf ("nУничтожение объекта типа Twon");
}
};// EndofTwo
// Пример обращения к конструкторам и деструктору.
void main (void)
{ //Создание объектов с использованием конструкторов.
Two   t1 (1,2.);               // Вызывается конструктор с двумя параметрами.
Two   t2 (3);                    // Вызывается конструктор с одним (первым) параметром.
//       Two   t3(,4.);         // Здесь будет ошибка.
Two   t4;                        //Конструктор без параметров вызывается без скобок.
~Two;                            //Обращение к деструктору, здесь оно лишнее.
} // main
Для конструкторов с параметрами удобно использовать механизм умолчаний. Значения параметров будут проинициализированы умолчаниями, если их опустить, или фактическими параметрами, если их использовать.
// В этом случае при обращении к конструктору параметры можно опускать.
Two (int a1=1, float b1=1.5) {
         a = a1;
b = b1;
}

 

Особенности конструктора

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

Определять значения полей можно двумя способами.

1. Инициализация по умолчанию. Например:

Two () {

a = 12;b = 9.99;

}

Тогда при объявлении объекта используется неявный вызов конструктора:

Two t4;                              //Поля принимают значения 12 и 9.99 .

2. Инициализация через параметры конструктора. Например:

Two (int a1, float b1) {

a = a1; b = b1;

}

Тогда при объявлении объекта используется явный вызов конструктора:

Two t1 (1, 1.11);                 //Поля принимают значения 1 и 1.11 .

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

По умолчанию, когда явного присутствия конструктора нет, формируются конструктор без параметров и конструктор копирования вида:

Type::Type (const Type &)

где Type – имя класса.

Благодаря этому возможно присваивание для объектов классов, например:

Two       t5 (1,1.11);

Two       t6 = t5;                           //Благодаря конструктору копирования

Two       t7;

t7 = t6;

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

Конструктор нельзя вызывать как обычный метод класса. Для вызова конструктора существует две формы:

1) Имя_класса имя_объекта (фактические_параметры);

Например,

         Two   t5 (1,1.11);

2) Имя_класса (фактические_параметры);

Например,

         Two t6 = Two (5,6.88);

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

Например,

         Two t7 = Two;

Ошибкой будет обращение вида:

Two t8();

Примечание: конструктор и деструктор не могут возвращать значения, поэтому им не приписывается никакой тип, даже void.

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

Динамическое создание и уничтожение объектов класса. Операции new и delete

В обычном Си для динамического создания и уничтожения объектов используются функции библиотеки alloc.h: calloc, malloc, realloc, free. Они имеют недостатки, например, необходимость определения размера выделяемой памяти и преобразования ее типа из void. В Си++, в связи с необходимостью простого выделения памяти под объекты класса и разрушения последних, введены две новые операции new и delete, которые заменяют собой всю библиотеку alloc.h.

Синтаксис операции new:

new имя_типа                           //Любое имя типа.

Или:

new имя_типа инициализатор //Любое имя типа и инициализирующее значение.

Семантика: new возвращает адрес, выделенный в динамической памяти, или NULL, если память не может быть выделена.

Здесь «Имя_типа» любое, в том числе имя объекта класса.

Механизм динамического выделения требует последовательного выполнения двух действий.

1). Определения указателя требуемого типа.

int *IP;

2). Выделения памяти и связывания ее с указателем.

IP = new int;

IP = new int (12);

При необходимости нужно выполнить высвобождение памяти операцией delete, синтаксис которой:

delete Имя_разрушаемого_объекта;

Например, delete IP;

Пример работы с операциями new и delete.


#include <stdio.h>
int main(void) {
int      *i_p;                      // Объявлен указатель на переменную целого типа.
double*d_p;                   // Объявлен указатель на переменную типа double.
int      *mas;                   // Массив.
i_p = new int;                  // Зарезервировано место под переменную целого типа,
                                      // переменная i_p принимает значение ее адреса.
                                      // Живет до конца программы или delete.
d_p = new double (3.1415);      //Резервирует место под вещественную переменную
                                     // приваивает d_p ее адрес,
                                      // *d_p инициализируется значением 3.14
mas = new int[5];           //Динамический массив из пяти значений.
// Места в памяти может не быть, тогда new вернет значение NULL
// необходим анализ ситуации следующим образом.
if (!(i_p && d_p && mas)) {
printf ("Нет памяти для динамических переменных. n");
         return 1;
}
for (int i = 0; i <= 4; i++)
         mas =i*10;
for (i = 0; i <= 4; i++)
         printf ("%dn",mas);
printf ("Целая: адрес=%p; значение=%dn",  i_p, *i_p);
         delete i_p;
printf ("Вещественная: адрес=%p; значение=%fn", d_p, *d_p);
         delete d_p;
printf ("Массив: адрес=%p; значение=%fn", mas, mas[0]);
         delete mas;
return 0;
} // main

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

Роль деструктора в уничтожении объектов класса.

Для статически создаваемых объектов использование деструктора не необходимо. Статические объекты имеют определенное время жизни и разрушаются при завершении работы функции, создавшей объект класса. Выполнив предыдущий пример в отладчике, можно увидеть момент вызова деструктора. Для всех статических объектов t1, t2, t4 деструктор будет вызван при завершении программы. Для динамически созданных объектов деструктор нужен, так как позволит уничтожить объект, высвобождая занятую им память.

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


#include <stdio.h>
class Three {
int      a;
float   b;
public:
Three () {
a = 5; b = 9.88;
}
Three (int a1,float b1) {
a = a1; b = b1;
}
~Three () {
printf ("nПока!n");         // Разрушаясь, объект прощается с нами.
}
}; //End of Three
void main (void) {
Three *c1, *c2, *c3;                           //Создаем объект, и тут же его разрушаем.
c1 = new Three (1,2.);
delete c1;
c2 = new Three;
delete c2;
c3 = new Three;
delete c3;
} // main

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

Пример.


#include <stdio.h>
class Three {
int      a;
float   b;
public:
void set_a (int x) {                     // Методы set введены для того, чтобы присвоить
a = x; }                            // значения полям данных.
void set_b (float y) {
b = y; }
void Out () { 
printf ("a=%d b=%fn",a,b);
}
Three () {                                 //Конструктор по умолчанию.
a = 1; b = 9.99;
}
Three (inta1, intb1) {              //Конструктор cпараметрами.
a = a1; b = b1;
}
~Three () {
printf ("nПока!n");
}
}; //End of Three
void main(void) {
int      i;
Three c[4];                               // Четырежды вызван конструктор по умолчанию.
for (i = 0; i < 4; i++)
c.Out();                       // Все элементы массива одинаковы.
// Конструктор с параметрами не может быть вызван.
// Можно инициализировать элементы массива так:
for (i = 0; i < 4; i++) {
c.set_a (i*10);
c.set_b (i*0.1);
}
for (i = 0; i < 4; i++)
c.Out();                       // Все элементы массива различны.
}
// С использованием динамических данных можно создать массив так:
Tree *d;
for (i = 0; i < 4; i++) {
         d = new Three (i*10, (float) i*10+5));
} // main
Настоящая инициализация данных массива выглядит как инициализация массивов размерности, большей чем 1, например:
ThreeQ[2] = {       {1, 9.9},
                            {2, 11.1}
                   };

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

Статические элементы класса

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

static тип имя;

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

 

Правила доступа распространяются и на статические элементы класса. Если они public, то обращение обычное, а если они private или protected, что чаще всего случается, доступ можно получить только посредством методов класса.

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

Вызов статического метода может быть как обычным:

имя_объекта.имя_метода(),

так и необычным, использующим квалифицированное имя вида:

имя_класса :: имя_статической_функции.

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


class Point {
int      x, y;
static int cou;                           //Статическое данное номер точки (количество).
public:
Point (int x1,int y1) {                //Конструктор (точки нумеруются при создании).
++cou;                           //Первоначально должен быть 0.
x = x1; y = y1;
}
static int counter () {                // Статическая функция количество точек,
return cou;                     // возвращает значение закрытого данного.
}
}; EndofPoint

Программа клиент должна инициализировать нулем значение статического данного, когда еще нет ни одного объекта. Используется прямое обращение к имени статического данного через имя класса (int Point :: cou=0). Это действие глобально. Статическая функция counter возвращает значение статического данного. Обратиться к ней можно двумя способами: через имя объекта (A.counter()); через имя класса (Point::counter()), что показано в примере.


#include <stdio.h>
int Point :: cou=0;                    // Инициализация статического данного
void main(void) {
printf ("Пока никого нет, счетчик=%dn", Point::counter());
Point A (4,5);
printf ("Теперь есть точка с номером %dn", A.counter());
printf ("Статическое данное хранится отдельно, размер =%dn",sizeof(Point));
Point B (9,10);
Point C (2,4);
printf ("Теперь их стало%dn", Point::counter());  //Обращение первого типа.
printf ("%d", A.counter());                                     //Обращение второго типа.
}

Передача параметров по ссылке

Передача параметров в функцию в Си выполняется по значению. При обращении к функции создается локальная копия данного внутри функции. Чтобы вернуть значение из функции, используется return. Значение переменной можно вернуть через список параметров только с использованием механизма указателей.

В Си++ определен новый способ «глобализации» передаваемого параметра – это передача в функцию ссылки. В этом случае синтаксис определения функции:

тип_функции имя_функции (тип &параметр, ...) {

// Далее в теле функции используется имя параметра.

}

Операция & у имени параметра означает использование ссылки на переменную.

Приведем пример, иллюстрирующий отличие этих механизмов.

1. В классическом Си используется передача параметров по значению.


void swap1 (int x, int y){
int      buf = y;                                    // Переменные x, y копии подлинников.
         y = x;
         x = buf;
}
2. Передача параметров по ссылке в стиле Си.
void swap2 (int *x, int *y) {                // Небходимо разыменование указателей.
int      buf = *y;
         *y = *x;
         *x = buf;                                  // Переменные x, y подлинники.
}
2. Передача параметров по ссылке в силе Си++.
void swap3 (int &x, int &y) {
int      buf = y;
         y = x;
         x = buf;                                    // Переменные x, y подлинники.
}

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

Ссылку можно вернуть из функции. Это полезно, например, при перегрузке операций определенных типов, когда необходима ссылка на созданный в теле функции объект. Синтаксис такого объявления:

int & f () {

    return Имя_объекта;

}

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

Указатель this

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

Этот указатель называется this, и он неявно определен в каждой функции класса следующим образом:

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

Имя this является ключевым словом. Явно его определить нельзя. Благодаря слову const изменить его нельзя, однако, в каждой функции класса он указывает именно на тот объект, для которого функция вызвана. Иллюстрирует смысл указателя this рисунок 5.

Пусть объявлены объекты:

OneC1, C2;

Они размещены по адресам «адрес1» и «адрес2». При вызове метода С1.get_a() он получает адрес С1, при вызове метода С2.get_a() он получает адрес С2. Обобществленное имя адреса и есть this.

 

Можно сделать вывод, что указатель this, как адрес объекта, можно использовать для работы с данными класса внутри метода класса.

Пример.


#include <stdio.h>
class Four {
int      a;
float   b;
public:
Four (int a1, float b1) {             //Конструктор работает с полями a и b.
this -> a = a1; 
this -> b = b1;
}
void Out (void) {                                //Вывод полей a и b.
printf ("a=%d, b=%f ", this->a, this->b);
}
}; // End of Four
void main(void) {
Four A (12,9.99);
A.Out();
} // main

Если обсуждать преимущества использования указателя this, то в этом примере их нет. Можно показать пример использования this для разрешения коллизии имен, когда имена параметров совпадают с именами полей данных.

Four (int a, float b) {

this -> a = a;

this -> b = b;

}

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


Информация

Комментировать статьи на нашем сайте возможно только в течении 60 дней со дня публикации.

Популярные новости

Статистика сайта



Rambler's Top100



 
Copyright © НеОфициальный сайт факультета ЭиП