Лекции по СП, страница 3
Описание файла
Документ из архива "Лекции по СП", который расположен в категории "". Всё это находится в предмете "практикум (прикладное программное обеспечение и системы программирования)" из 4 семестр, которые можно найти в файловом архиве МГУ им. Ломоносова. Не смотря на прямую связь этого архива с МГУ им. Ломоносова, его также можно найти и в других разделах. .
Онлайн просмотр документа "Лекции по СП"
Текст 3 страницы из документа "Лекции по СП"
Complex x (1.5); // x = 1.5
Complex y; // y = 0
const Complex t (5); // t – константа 5
Complex z1 = z; // конструктор копирования
Complex z2 = Complex (1,2); // создаётся временный об’ект
//Complex (1,2), потом вызывается конструктор копирования
// компиляторы часто оптимизируют и не создают вр. об’ект
// (стандартом это допускается; в то же время доступность
// конструктора копирования все равно проверяется)
z2 = z; // оператор присваивания
z = t; // для этого пишем const в параметре констр. копир-я
z2.operator= (t); // то же самое
Complex *p;
p = new Complex (1.5); // отводится память в куче
// и вызывается конструктор
/* работа с p */
delete p; // вызывается деструктор, память возвр. в кучу
Complex p[10]; // по адресу p выделяется память для 10
//об’ектов типа Complex. Работает конструктор умолчания
// если он не определён, ошибка!
//инициализировать массив конструкторами с параметрами,
// к сожалению, нельзя
return 0;
}
Особенности работы с неплоскими классами.
Пример. Описание класса MFC-строк.
class String { // С-шная строка, с которой хранится её длина.
int size; // длина строки без ‘\0’
char *p; // указатель на строку
public:
String (const char *str){ // конструктор
p = new char [(size = strlen(str)) + 1];
strcpy (p, str);
} // теперь можно создавать строки: String a (“Hello!”);
~String () { delete[] p;}
Пусть теперь с классом работает некоторая функция:
void f(){
String s1 (“Hello!”);
String s2 = s1;
String s3 = String (“2007”);
s3 = s1;
}
Тут происходит поверхностное копирование конструкторами по умолчанию. Это значит, что копируются указатели, но не содержание строк. В этой ситуации s1.p и s2.p ссылаются на одну и ту же область памяти, а при присваивании в последней строчке мы оставляем в памяти мусор. Но самое интересное начнётся при выходе из функции, когда начнёт работать описанный нами деструктор. Он захочет три раза удалить память, занятую строчкой “Hello!”. Но двум смертям не бывать, получим runtime error.
Напишем специальные функции, поддерживающие глубинное копирование.
String (const String &a){
p = new char [(size = a.size) + 1];
strcpy (p, a.p);}
String& operator= (const String &a){
if (this == &a) return *this; // если s1 == s2
delete[] p; // освобождаем память для старой строки
p = new char [(size = a.size) + 1];
strcpy (p, a.p);
return *this;}
Абстрактные типы данных. Перегрузка операций.
Абстрактный тип данных (АТД) — это такой тип, который предоставляет для работы с элементами этого типа определённый набор функций, а также возможность создавать элементы этого типа при помощи специальных функций. Вся внутренняя структура такого типа спрятана от разработчика программного обеспечения — в этом и заключается суть абстракции. Абстрактный тип данных определяет набор независимых от конкретной реализации типа функций для оперирования его значениями. Например, можно определить абстрактные типы для стека, очереди, списка.
Перегрузка операций – описание своих операций (+, -, new и т.д.) вместо уже определённых.
Нельзя перегружать операции: ., ?:,::, .* , # , ## , sizeof, typeid.
Если мы перегружаем какой-нибудь значок, надо стараться, чтобы он определял похожую на обычный вкладываемый в него смысл сущность. Например, разумно использовать * для перемножения матриц или многочленов или для кон’юнкции.
К сожалению, мы не можем изменить арность операций, то есть бинарная операция всегда будет бинарной, а унарная – унарной. Также операции будут подчиняться законам старшинства, определённым раньше. Часто программисты сталкиваются с проблемой при перегрузке оператора ^ в качестве возведения в степень. От побитового сложения по модулю 2 нам достался приоритет, меньший, чем у сложения или умножения. Очевидно, это противоречит вкладываемому в него смыслу, и приходится использовать скобки.
Перегрузка бинарных операций.
Существует три возможности перегрузки операций:
1) написать функцию-член класса с одним параметром;
2) с помощью глобальной функции с двумя параметрами (в этом случае нам нужен доступ к скрытым полям класса с помощью сеттеров/геттеров, но так получается медленнее и вообще не используется);
3) функция-друг класса с двумя параметрами (если класс об’являет функцию своим другом, она может использовать все поля класса).
Пример. Продолжение описания класса комплексных чисел.
const Complex operator+ (const Complex &a) const{
Complex t (re + a.re, im + a.im);
return t;}
При использовании z = x + y, x – вызывающий об’ект, а y – параметр. Можно опустить const у параметра, но тогда нельзя будет складывать об’екты нашего класса с константами, то есть z = y + 5.
Почему мы возвращаем значение не по ссылке? Если бы мы действительно сделали так, мы вернули бы адрес локальной переменной, которая лежит в стеке, и её память освобождается после выхода из функции. Конечно, можно рассчитывать на то, что компилятор не заругается (зависит от реализации), и значение t не успеет затереться, просто переместится указатель стека. Но тогда, если мы напишем цепочку сложений, то при следующем вызове функции об’ект t скорее всего будет создан на том же месте, и тогда точно программа полетит. Не возвращайте локальные переменные по ссылке!
lvalue – переменная, которой возможно присваивание (она обладает адресом).
rvalue – кусок памяти, которому нельзя присваивать, например, константа или временный результат вычисления.
Самый первый const превращает полученный результат из lvalue в rvalue. Иначе можно будет вызвать, например (a+b).change(1), а это не по-C++ному.
int main (){
double t = 7.5;
Complex x(1,2), y(5), w;
const Complex z(1,3);
w = x + y; // равносильно w = x.operator+(y);
// или w.operator= (x.operator+(y));
w = x + z; // можно из-за const у параметра
w = z + x; // можно из-за const в конце
w = x + t; // преобразование типа double к const Complex
// w = x.operator+ (Complex(t)); т.к. есть Complex(double)
w = t + x; // ашыпка
return 0;}
Теперь рассмотрим перегрузку с помощью дружественной функции. Для этого в классе нужно написать:
friend const Complex operator+ (const Complex &a, const Complex &b);
Зона видимости функции совпадает с областью видимости класса. private: на неё не действует! Описание в классе или вне класса (тот же профиль без friend) приводит к одинаковому результату.
const Complex operator+ (const Complex &a, const Complex &b){
Complex t (a.re + b.re, a.im + b.im);
return t;}
int main(){
double t = 7.5;
Complex x(1,2), y(3), z;
z = x + y; // z = operator+ (x, y);
z = x + t; // z = operator+ (x, Complex(t));
z = t + x; // z = operator+ (t, Complex(x));
return 0;}
Так что, если мы хотим смешивать данные, лучше заводить друзей. ;)
Формально можно перегрузить оператор и в классе, и с помощью друга.
z = x + y; // неоднозначность - ошибка
z = x + t; // неоднозначность - ошибка
z = t + x; // друг работает
Перегрузка унарных операций.
Есть те же три возможности перегрузить унарный оператор (функция-член класса без параметров, обычная глобальная функция или функция-друг с 1 параметром).
const Complex operator- () const{
Complex t (-re,-im); return t;}
int main (){
double t = 7.5;
Complex x(1,2), y;
const Complex z (1);
y = -x; // y = x.operator-();
y = -z; // y = z.operator-();
-x = y; // низя, т.к. –x не l-value
x = -t; // x = Complex (-t); - здесь нет нашего отрицания
return 0;}
Друга пишут аналогично прошлому разу.
Особенности перегрузки операций ++ и --.
Как обычно, есть 3 варианта.
Чтобы отличать описание префиксной формы от описания постфиксной, договорились, что функция для префиксной формы описывается без формальных параметров, а для постфиксной – с одним параметром типа int.
const Complex& operator++ () // ++x;
{ ++re; ++im; return *this;}
const Complex operator++ (int notused) // x++;
{Complex t = *this;
++re; ++im;
return t;}
int main () {
Complex x(1,2); y;
y = ++x; // y = x.operator++();
y = x++; //y = x.operator++(0); -обычно там реально ноль
++ ++x; // нельзя, т.к. ++x не l-value
y = (x + y)++; // нельзя, так мы определили сложение
return 0;}
Еще одно глобальное замечание: совершенно не обязательно при перегрузке, скажем, бинарной операции функцией-членом класса делать тип операнда совпадающим с типом неявного.
Виды отношений между классами.
Выделяют следующие темы межклассовых отношений:
-
ассоциация
-
наследование
-
агрегация
-
использование
-
инстанцирование (выделяют не всегда)
Ассоциация – тот факт, что пара об’ектов связана между собой. Связь не конкретизируется, обычно такая связь появляется в начале планирования, а потом уточняется и переходит в другие формы.
Далее будем приводить UML-подобные примеры, точнее, мы будем рисовать ER-диаграммы (Entity Relations), которые пришли из области баз данных.
Степень связи (мощность) показывает, сколько об’ектов с каждой стороны могут участвовать в связи. Обозначается спецификацией около карточки.
Возможные виды связей:
-
один к одному
-
один ко многим
-
многие ко многим
Наследование – взаимодействие, выражающее связь ‘частное - общее’ (‘is a‘), например, любое млекопитающее есть животное.
Общее считается базовым классом, а частное – производным. В другой терминологии, принятой, например, в Java, это суперкласс и подкласс. На диаграммах стрелочки рисуются от производного класса к базовому. Производный класс может расширить базовый класс, а может переопределить некоторые его члены.
Агрегация – соотношение ‘часть - целое’ (‘has a’) – ситуация, при которой об’ект одного класса внутри себя содержит об’екты другого класса.
Различают строгую и нестрогую агрегацию. Строгая агрегация (композиция) – агрегация, при которой вложенный об’ект не может исчезнуть, пока существует его хозяин. Тогда ромбик на диаграмме закрашивают.
class Triangle { ... Point v1, v2, v3; ... }
Нестрогая агрегация (часто просто агрегация) – часть может исчезнуть, пока существует целое, например, у треугольника можно отрезать вершину. Тогда ромбик не закрашивают.
class Shareholder { ... Share *asserts; ... }
Возможно, share == NULL. Вот такой у нас безNULL.
Использование (зависимость) возникает в трёх случаях:
-
ситуация, при которой имя класса используется в профиле функции-члена другого класса (тип параметров или возвращаемого значения);
-
функция-член одного класса для реализации использует локальную переменную типа – другого класса;
-
в теле функции-члена одного класса вызывается функция-член другого класса, то есть, либо у нас есть статическая функция, либо мы поступаем, как в предыдущем пункте.
При этом используемый класс называется сервером, использующий – клиентом.
Инстанцирование – получение из некоторого абстрактного типа конкретного, из которого потом генерируются об’екты, то есть это связь шаблона класса и того, что генерируется по шаблону.
Единичное наследование в C++
Теперь можно рассказать о модификаторе доступа protected. Если имена попали в эту секцию, это значит, что вне класса они не видимы, так же, как private, однако они доступны в производных классах.
Пример. Сотрудники.
class Person { // человек
protected:
char *name;
int age;