sp-dd1-part2 (1119737), страница 2
Текст из файла (страница 2)
Проектирование (декомпозиция) – разбиение системы на подсистемы.
Нас интересует об’ектная декомпозиция, то есть статическая структура системы – об’екты и связи между ними, а динамическое поведение системы – обмен сообщениями между об’ектами. В C++ обмен сообщениями представлен набором методов об’екта, которые можно вызывать из других об’ектов.
Основные принципы об’ектной модели:
-
абстракция и инкапсуляция
-
наследование
-
полиморфизм
Абстракция – приём борьбы со сложностью. В предметной области необходимо выявить об’екты, выделить нужные нам свойства, то есть создать модель предметной области.
Инкапсуляция – средство, позволяющее спрятать реализацию и показывать пользователю только интерфейс абстракции (возможности взаимодействия с другими абстракциями).
Наследование – возможность выявить иерархию об’ектов, в рамках которой мы можем строить абстракции от общего к частному и использовать для частностей общие методы.
Полиморфизм различают статический, динамический и типовой.
Статический полиморфизм – возможность иметь несколько функций с одинаковыми именами, но разными профилями (выбор осуществляется по списку параметров) – overloading (перегрузка).
Динамический полиморфизм – перегрузка функций в рамках выстроенной иерархии и механизм виртуальных функций. Выбор быть осуществлён на этапе компиляции.
Типовой полиморфизм (шаблоны функций/классов) – возможность настройки абстракции на конкретный тип данных. Обработка состоит из двух этапов: настройка на типовой параметр и вызов этой функции, уже настроенной на тип. Например, при описании класса, описывающего стек, мы не знаем, какой тип элементов захочет использовать юзер.
Следует различать понятия об’ектного языка и об’ектно-ориентированного языка.
Об’ектный язык – язык, который поддерживает принципы ООП за исключением наследования. Например, таким были языки Simula ’67, ADA’85.
Об’ектно-ориентированный язык – язык, поддерживающий все принципы ООП. Например, C++, Java, Eiffel, Object Pascal и другие.
Об’ектно-ориентированное проектирование.
Мы должны научиться выделять об’екты и конкретизировать их состояние и поведение. При анализе существительные переходят в об’екты. Например, при описании банкомата об’ектами будут сам банкомат, счёт, банк и т.д. При этом следует следить за отсутствием избыточности (грубо говоря, в списке об’ектов не должно быть синонимов). Свойства об’ектов не должны быть об’ектами, например, деньги – не самостоятельный об’ект, а свойство счёта. Операции с об’ектом также не являются об’ектами. Возникает проблема, на каком этапе надо задумываться об об’ектах, которых нет в предметной области, но которые нужны для реализации? Существуют разные мнения на эту тему: либо на этапе проектирования, либо отложить их до кодирования. Например, таким об’ектом является очередь клиентов при доступе к счетам.
Пример. Моделирование предметной области для банковской сети.
Выделяем следующие абстракции – об’екты предметной области:
-
баксомёт
-
клиент
-
карта
-
счёт
Д
иаграмма нарисована в стиле UML-карточки. Вверху пишется имя об’екта, во второй части его свойства – атрибуты, в третьей – методы – характеристики его поведения.
Далее нужно проанализировать инкапсуляцию. Обычно прячут всё, что возможно, так как другие об’екты не должны знать, как функционирует об’ект, а только его интерфейс. В большинстве случаев, свойства прячут, а методы открывают. В диаграмме ‘-‘ означает права доступа private, ’+’ – public, а ‘#’ – protected.
Если об’екты получаются из существительных, то их методы возникают из глаголов. Также, у любого об’екта есть конструктор или конструкторы и деструктор. Для скрытых свойств иногда нужны специальные методы – setters и getters, которые читают и записывают скрытые поля.
Некоторые особенности языка C++.
В C++, в отличие от C, в функцию можно передавать параметры по ссылке, примерно как в Pascal. Например, в C мы бы написали
void swap (int *x, int *y){
int t; t = *x; *x = *y; *y = t;
}
А в теле программы вызывали бы int a, b; ... swap (&a,&b);
А на плюсах можно написать так:
void swap (int &x, int &y){
int t; t = x; x = y; y = t;
}
И тогда вызывать swap (&a,&b);
Для работы с динамической памятью в C использовались функции malloc, calloc, realloc и free. В C++ есть операции new и delete, которые призваны заменить их, хотя по-старому писать, конечно, тоже можно. Используют их примерно так:
int *p;
p = new int; // вместо p = (int*) malloc (sizeof(int))
*p = 5;
// работа с p
delete p;
В случае если надо запросить память под массив, действуют так:
int *p = new int[10]; //вместо p = (int*)malloc(10*sizeof(int))
// работа с p
delete[] p;
Классы, специальные функции-члены класса.
Классы бывают плоскими и не плоскими. Плоским принято считать класс, который не содержит указателей.
Конструктор класса – специальная функция-член класса, с помощью которой создаётся об’ект, имя которой совпадает с именем класса, и не возвращающая никакого значения. Любой класс содержит конструкторы, по дефолту есть два конструктора – умолчания и копирования.
Конструктор умолчания – конструктор вида X (); Впрочем, параметры могут присутствовать, но для них всех должны быть заданы значения по умолчанию. Если в классе явно определён хотя бы один конструктор, конструктор умолчания автоматически не создаётся!
Конструктор копирования – конструктор вида X (const X&); который служит для инициализации об’екта другим об’ектом. Дополнительно могут быть параметры кроме первого, но для них должны быть заданы умолчания. При использовании const в определении типа аргумента могут возникнуть проблемы при инициализации об’екта определяемого класса неконстантным об’ектом.
Обычные конструкторы – конструкторы, у которых есть параметры, не все из них имеют значения по умолчанию, и первый из них не const X&. Такой конструктор вызывается при создании об’екта (что-нить типа X a (3,5);) или при создании об’екта в динамической памяти (X *p; p = new X (3,5); - тут происходит сначала заказ памяти по new, а потом вызывается конструктор об’екта p*), а также в некоторых других случаях, которые будут рассмотрены позднее.
Деструктор класса – специальная функция-член класса, с помощью которой удаляется об’ект, имя которой отличается от имени класса на тильду вначале, не имеющая параметров и не возвращающая никакого значения. Любой класс содержит один деструктор. Если он не описан явно, то действует дефолтный деструктор:
~X () {}
Пример. Описание класса комплексных чисел.
Вывод на консоль обычно осуществляется в C++ через поток cout. Для его использования надо подключить заголовочный файл iostream.
class Complex{
double re, im;
public:
void print() const{ // печать числа в алгебраической форме
cout<<re<<” + i*”<<im<<endl;
}
}
В секцию private попадают имена, которые не видны извне, соответственно, в секцию public – интерфейс класса. По умолчанию, сначала идут скрытые члены класса. Модификатор устанавливает права доступа вниз полям и методам до следующего модификатора или конца класса. В структурах, наоборот – по умолчанию идут общедоступные члены. В остальном, конструкции class и struct совпадают.
Создание об’екта:
Complex a;
Компилятор отведёт статическую память под об’ект, вызывается конструктор умолчания.
В об’явлении функции print() содержится слово const. Это значит, что функция не должна менять об’ект, который её вызвал. Только такие функции могут использовать члены класса, об’явленные как константы.
void change(const double a) { re += a;}
Эту функцию мы не можем об’явить константной, так как она меняет поле класса.
Функции можно определять также и вне класса. Например, в классе мы напишем:
void print() const;
А вне его:
void Complex::print() const{ cout<<re<<” + i*”<<im<<endl;}
Четвероточие – оператор разрешения контекста. Об’явление функций должно в обоих случаях совпадать, то есть тип параметров, тип возвращаемого значения и спецификаторы функции должны быть идентичны.
Зачастую определение функции выносится из класса для удобства. Но есть различия и для компилятора. Если функция описана в классе, то её вызов будет компилироваться не как вызов функции, а просто на место вызова функции подставится её код, настроенный на фактические параметры. В этом случае мы проигрываем по памяти, но выигрываем по скорости. Разумно использовать эту возможность для маленьких функций. Результат будет аналогичным, если дописать к определению функции слово inline, а её тело вынести за класс. Чего-то вроде outline, к сожалению, не существует
Описанный класс содержит требуемые данные, но косяк в том, что мы не можем задать или изменить значения полей re и im. Выход – описать в public-части конструкторы или сеттеры/геттеры. Опишем конструктор:
Complex (double r, double i) {re = r; im = i;}
Теперь мы можем написать:
Complex a (3.5,2.7);
Можно описать ещё один конструктор для задания чисел вещественной оси:
Complex (double r) {re = r; im = 0;}
Он будет вызван, если вызвать конструктор с одним параметром.
Создание хотя бы одного конструктора отменяет действие дефолтного. То есть, xs не можем сейчас написать Complex a ();
Но проще вместо всего этого гемора из трёх конструкторов написать
Complex (double r = 0, double i = 0) {re = r; im = i;}
При вызове конструктора с одним параметром он считается значением первого аргумента, второй определяется по умолчанию.
Конструктор копирования. Оператор присваивания.
1) Complex d = a;
2) Complex e = Complex (1,2);
В описанных примерах работает конструктор копирования. По умолчанию он будет описан так:
Complex (const Complex &a) {re = a.re; im = a.im;}
В первом случае произойдёт копирование полей об’екта a в поля d. Во втором случае сначала создаётся безымянный временный об’ект, проинициализированный требуемыми значениями, а потом его поля копируются в поля e.
В C для структур допустим оператор присваивания: b = c;
Присваивание определено и для классов по умолчанию как простое копирование полей:
Complex& operator= (const Complex &a)
{re = a.re; im = a.im; return *this;}
Разумеется, оператор присваивания может быть перегружен.
Функция возвращает ссылку на об’ект, чтобы пользователь мог писать цепочки присваиваний типа, как принято в C/C++:
x = y = z;
Возникает логичный вопрос, почему мы возвращаем параметр по ссылке? Рассмотрим, что будет происходить, если об’явить оператор как
Complex operator= (const Complex &a);
Тип возвращаемого результата Complex. Значит, вернуть мы должны об’ект, которому мы что-то присвоили. this – определённая в любой функции класса переменная, которая всегда имеет значение ссылки на об’ект, от которого вызвана функция. Поэтому мы возвращаем *this. Допустим, что пользователь написал цепочку x = y = z; Тогда первое присваивание (правое) вернёт копию y. Потом работает конструктор копирования, получаем временный об’ект, который потом присваивается иксу (левое присваивание). Если же написать амперсанд, то мы вернём ссылку на игрек, который присвоится иксу без всяких временных об’ектов. Зачем платить больше?
Однако на этом отличия не заканчиваются. Подумаем, что произойдёт, если написать:
(x = y).change(2);
Компилятор это одобрит. Но в последнем случае функция вызовется от временного об’екта, который потом умрёт, и эффекта никакого мы не увидим. В первом случае мы возвращаем ссылку на об’ект x, который и ‘изменится’.
Рассмотрим применение описанных функций.
int main (){
Complex z (1.2,3.5); // y = 1.2 + i*3.5
Complex x (1.5); // x = 1.5
Complex y; // y = 0















