Главная страница
Навигация по странице:

  • 4.5.1 Дружественная функция

  • 4.5.2 Дружественная класс

  • 5.2 Перегрузка унарных операций

  • 5.3 Перегрузка бинарных операций

  • 5.4 Перегрузка операции присваивания

  • 5.5 Перегрузка операции приведения типа

  • 5.6 Особенности работы операторов new и delete

  • 5.7 Перегрузка операторов new и delete для отдельных классов

  • 5.8 Переопределение глобальных операторов new и delete

  • 6.2 Простое наследование

  • ттттт. Объектноориентированное программирование


    Скачать 1.73 Mb.
    НазваниеОбъектноориентированное программирование
    Анкорттттт
    Дата30.10.2021
    Размер1.73 Mb.
    Формат файлаpdf
    Имя файлаOOP-PrePrint.pdf
    ТипКонспект
    #259341
    страница4 из 15
    1   2   3   4   5   6   7   8   9   ...   15

    4.5 Дружественные функции и классы
    Иногда желательно иметь непосредственный доступ извне к скрытым полям класса, то есть расширить интерфейс класса. Для этого служат дру- жественные функции и дружественные классы.
    4.5.1 Дружественная функция
    Дружественная функция объявляется внутри класса, к элементам кото- рого ей нужен доступ, с ключевым словом friend
    . В качестве параметра ей должен передаваться объект или ссылка на объект класса, поскольку указа- тель this ей не передается. Одна функция может «дружить» сразу с не- сколькими классами.
    Дружественная функция может быть обычной функцией или методом другого ранее определенного класса. На нее не распространяется действие

    36 спецификаторов доступа, место размещения ее объявления в классе безраз- лично.
    В качестве примера ниже приведено описание двух функций, друже- ственных классу monster
    . Функция kill является методом класса hero
    , а функция steal_ammo не принадлежит ни одному классу. Обеим функциям в качестве параметра передается ссылка на объект класса monster class monster;
    // Предварительное объявление класса class hero
    { void kill(monster &);
    }; class monster
    { friend int steal_ammo(monster &);
    /* Класс hero должен быть определен ранее */ friend void hero::kill(monster &);
    }; int steal_ammo(monster &M){return --M.ammo;} void hero::kill(monster &M){M.health = 0; M.ammo = 0;}
    4.5.2 Дружественная класс
    Если все методы какого-либо класса должны иметь доступ к скрытым полям другого, весь класс объявляется дружественным с помощью ключе- вого слова friend
    . В приведенном ниже примере класс mistress объявля- ется дружественным классу hero
    : class hero {
    ... friend class mistress;
    }; class mistress{
    ... void f1(); void f1();
    };
    Функции f1
    и f2
    являются дружественными по отношению к классу hero
    (хотя и описаны без ключевого слова friend
    ) и имеют доступ ко всем его полям.
    Объявление friend не является спецификатором доступа и не наследу- ется. Обратите внимание на то, что класс сам определяет, какие функции и классы являются дружественными, а какие нет.

    37
    4.6 Деструкторы
    Деструктор – это особый вид метода, применяющийся для освобождения памяти, занимаемой объектом. Деструктор вызывается автоматически, ко- гда объект выходит из области видимости:

    для локальных переменных – при выходе из блока, в котором они объ- явлены;

    для глобальных – как часть процедуры выхода из main
    ;

    для объектов, заданных через указатели, деструктор вызывается не- явно при использовании операции delete
    (автоматический вызов деструк- тора при выходе указателя из области действия не производится).
    При уничтожении массива деструктор вызывается для каждого элемента удаляемого массива. Для динамических объектов деструктор вызывается при уничтожении объекта операцией delete
    . При выполнении операции delete[]
    деструктор вызывается для каждого элемента удаляемого мас- сива.
    Имя деструктора начинается с тильды (


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

    не имеет аргументов и возвращаемого значения;

    не может быть объявлен как const или static
    ;

    не наследуется;

    может быть виртуальным;

    может вызываться явным образом путем указания полностью уточ- ненного имени; это необходимо для объектов, которым с помощью new вы- делялся конкретный адрес.
    Если деструктор явным образом не определен, компилятор создает его автоматически.
    Описывать в классе деструктор явным образом требуется в случае, когда объект содержит указатели на память, выделяемую динамически – иначе при уничтожении объекта память, на которую ссылались его поля-указа- тели, не будет помечена как свободная. Указатель на деструктор определить нельзя.
    Деструктор для рассматриваемого примера должен выглядеть так: monster::monster() {delete [] name;}

    38
    5.
    ПЕРЕГРУЗКА ОПЕРАЦИЙ В ООП
    5.1 Перегрузка операций
    С++ позволяет переопределить действие большинства операций так, чтобы при использовании с объектами конкретного класса они выполняли заданные функции. Эта дает возможность использовать собственные типы данных точно так же, как стандартные. Обозначения собственных операций вводить нельзя. Можно перегружать любые операции, существующие в
    С++, за исключением:
    .*
    ?:
    ::
    #
    ##
    sizeof
    Перегрузка операций осуществляется с помощью функций специального вида (функций-операций) и подчиняется следующим правилам:

    сохраняются количество аргументов, приоритеты операций и правила ассоциации (справа налево или слева направо) по сравнению с использова- нием в стандартных типах данных;

    нельзя переопределить операцию по отношению к стандартным типам данных;

    функция-операция не может иметь аргументов по умолчанию;

    функции-операции наследуются (за исключением =).
    Функцию-операцию можно определить тремя способами: она должна быть либо методом класса, либо дружественной функцией класса, либо обычной функцией. В двух последних случаях функция должна принимать хотя бы один аргумент, имеющий тип класса, указателя или ссылки на класс
    (особый случай: функция-операция, первый параметр которой – стандарт- ного типа, не может определяться как метод класса).
    Функция-операция содержит ключевое слово operator
    , за которым сле- дует знак переопределяемой операции: тип operator операция ( список параметров) { тело функции }
    5.2 Перегрузка унарных операций
    Унарная функция-операция, определяемая внутри класса, должна быть представлена с помощью нестатического метода без параметров, при этом операндом является вызвавший ее объект, например: class monster
    {... monster & operator ++() {++health; return *this;}} monster Vasia; cout << (++Vasia).get_health();

    39
    Если функция определяется вне класса, она должна иметь один параметр типа класса: class monster
    {... friend monster & operator ++( monster &M);}; monster& operator ++(monster &M) {++M.health; return M;}
    Если не описывать функцию внутри класса как дружественную, нужно учитывать доступность изменяемых полей (в данном случае поле health недоступно извне, так как описано со спецификатором private
    , поэтому для его изменения требуется использование соответствующего метода, не описанного в приведенном примере).
    Операции постфиксного инкремента и декремента должны иметь пер- вый параметр типа int
    . Он используется только для того, чтобы отличить их от префиксной формы: class monster
    {... monster operator ++(int){monster M(*this); health++; return M;}}; monster Vasia; cout << (Vasia++).get_health();
    5.3 Перегрузка бинарных операций
    Бинарная функция-операция, определяемая внутри класса, должна быть представлена с помощью нестатического метода с параметрами, при этом вызвавший ее объект считается первым операндом: class monster
    { bool operator >( const monster &M)
    { if( health > M.get_health()) return true; return false;
    }
    };
    Если функция определяется вне класса, она должна иметь два параметра типа класса: bool operator >(const monster &M1, const monster &M2)
    { if( M1.get_health() > M2.get_health()) return true; return false;
    }
    Бинарные арифметические операции, такие как
    +
    ,
    - и
    *
    , возвращают но- вый экземпляр класса, помеченный ключевым словом const
    . Например:

    40 const MyClass MyClass::operator+(const MyClass &other) const {
    MyClass result = *this; // Make a copy of myself result.value += other.value; // Use += to add other to the copy. return result; // All done!
    }
    Использование ключевого слова const необходимо для того, что бы было не возможно написать следующий код:
    MyClass a, b, c;
    (a + b) = c;
    В случае отсутствия ключевого слова const, данный код будет успешно скомпилирован.
    5.4 Перегрузка операции присваивания
    Операция присваивания определена в любом классе по умолчанию как поэлементное копирование. Эта операция вызывается каждый раз, когда од- ному существующему объекту присваивается значение другого. Если класс содержит поля ссылок на динамически выделяемую память, необходимо определить собственную операцию присваивания. Чтобы сохранить семан- тику операции, операция-функция должна возвращать ссылку на объект, для которого она вызвана, и принимать в качестве параметра единственный аргумент – ссылку на присваиваемый объект: monster& operator = (const monster &M)
    { if (&M == this)
    // Проверка на самоприсваивание return *this; if (name) delete [] name; if (M.name) { name = new char [strlen(M.name) + 1]; strcpy(name, M.name);
    } else name = 0; health = M.health; ammo = M.ammo; skin = M.skin; return *this;
    }
    Операцию присваивания можно определять только в теле класса. Она не наследуется. Можно заметить, что операция присваивания возвращает ссылку, что позволяет совершать «цепочки присваивания»: int a, b, c, d; a = b = c = d = 23;
    В данной цепочке присваивания первой выполняется операция d = 23
    , возвращающая ссылку на переменную d
    , значение которой, в свою очередь, присваивается переменной с
    и т.д.

    41
    5.5 Перегрузка операции приведения типа
    Можно определить функции-операции, которые будут осуществлять преобразование класса к другому типу. Формат: operator имя_нового_типа ();
    Тип возвращаемого значения и параметры указывать не требуется.
    Можно определять виртуальные функции преобразования типа.
    Пример: monster::operator int(){ return health; } monster Vasia; cout << int(Vasia);
    5.6 Особенности работы операторов new и delete
    Переменная объектного типа в динамической памяти создаётся в два этапа:
    1. Выделяется память с помощью оператора new
    2. Вызывается конструктор класса.
    Удаляется такая переменная тоже в два этапа:
    1. Вызывается деструктор класса.
    2. Освобождается память с помощью оператора delete
    5.7 Перегрузка операторов new и delete для отдельных классов
    Операторы new и delete можно перегрузить. Для этого есть несколько причин:

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

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

    можно реализовать собственный «сборщик мусора» (
    garbage collector
    ).

    можно вести лог выделения/освобождения памяти.
    Операторы new и delete имеют следующие сигнатуры: void *operator new(size_t size); void operator delete(void *p);
    Оператор new принимает размер памяти, которую необходимо выделить, и возвращает указатель на выделенную память.

    42
    Оператор delete принимает указатель на память, которую нужно осво- бодить. class A { public: void *operator new(size_t size); void operator delete(void *p);
    }; void *A::operator new(size_t size) { printf("Allocated %d bytes\n", size); return malloc(size);
    } void A::operator delete(void *p) { free(p);
    }
    Вместо функций malloc и free можно использовать глобальные опера- торы
    ::new и
    ::delete
    Рекомендуется не производить в операторе new
    (особенно в глобальном) какие-либо операции с объектами, которые могут вызвать оператор new
    Например, для вывода текста используется функция printf
    , а не объект std::cout
    Операторы new и delete
    , объявленные внутри класса, функционируют подобно статическим функциям и вызываются для данного класса и его наследников, для которых эти операторы не переопределены.
    5.8 Переопределение глобальных операторов new и delete
    В некоторых случаях может потребоваться перегрузить глобальные опе- раторы new и delete
    . Они находятся не в пространстве имен std
    , а в гло- бальном пространстве имён.
    Глобальные операторы new и delete вызываются для примитивных ти- пов и для классов, в которых они не переопределены. Они имеют такие же сигнатуры, что и рассмотренные выше операторы new и delete
    // Для примитивных типов вызываются глобальные ::new и ::delete int *i = new int; delete i;
    // Для класса A вызываются переопределённые A::new и A::delete
    A *a = new A; delete a;

    43
    // Для класса C операторы new и delete не переопределены,
    // поэтому вызываются глобальные ::new и ::delete
    C *c = new C; delete c;

    44
    6.
    НАСЛЕДОВАНИЕ В ООП
    При большом количестве никак не связанных классов управлять ими ста- новится невозможным. Наследование позволяет справиться с этой пробле- мой путем упорядочивания и ранжирования классов, то есть объединения общих для нескольких классов свойств в одном классе и использования его в качестве базового.
    Механизм наследования классов позволяет строить иерархии, в которых производные классы получают элементы родительских, или базовых, клас- сов и могут дополнять их или изменять их свойства.
    Классы, находящиеся ближе к началу иерархии, объединяют в себе наиболее общие черты для всех нижележащих классов. По мере продвиже- ния вниз по иерархии классы приобретают все больше конкретных черт.
    Множественное наследование позволяет одному классу обладать свой- ствами двух и более родительских классов.
    6.1 Виды наследования
    При описании класса в его заголовке перечисляются все классы, являю- щиеся для него базовыми. Возможность обращения к элементам этих клас- сов регулируется с помощью модификаторов наследования private
    , protected и public
    Если базовых классов несколько, то они перечисляются через запятую.
    Перед каждым может стоять свой модификатор наследования. По умолча- нию для классов установлен модификатор private
    , а для структур – public
    Если задан модификатор наследования public
    , оно называется откры- тым. Использование модификатора protected делает наследование защи- щенным, а модификатора private
    – закрытым. В зависимости от вида наследования классы ведут себя по-разному. Класс может наследовать от структуры, и наоборот.
    Для любого элемента класса может также использоваться спецификатор protected
    , который для одиночных классов, не входящих в иерархию, рав- носилен private
    . Разница между ними проявляется при наследовании. Воз- можные сочетания модификаторов и спецификаторов доступа приведены в таблице ниже.
    Как видно из таблицы ниже, private элементы базового класса в произ- водном классе недоступны вне зависимости от ключа. Обращение к ним мо- жет осуществляться только через методы базового класса.

    45
    Модификатор наследо- вания
    Спецификатор базового класса
    Доступ в производном классе private private нет protected private public private protected private нет protected protected public protected public private нет protected protected public public
    Элементы protected при наследовании с ключом private становятся в производном классе private
    , в остальных случаях права доступа к ним не изменяются.
    Доступ к элементам public при наследовании становится соответству- ющим ключу доступа.
    Если базовый класс наследуется с ключом private
    , можно выборочно сделать некоторые его элементы доступными в производном классе, объ- явив их в секции public производного класса с помощью операции доступа к области видимости: class Base {... public: void f();
    }; class Derived : private Base {... public: using Base::f;
    };
    6.2 Простое наследование
    Простым называется наследование, при котором производный класс имеет одного родителя. Для различных элементов класса существуют раз- ные правила наследования. Рассмотрим наследование классов на примере.
    Создадим производный от класса monster класс daemon
    , добавив «де- мону» способность думать: enum color {red, green, blue};
    // ------------- Класс monster ------------- class monster
    {
    // --------- Скрытые поля класса: ------------ int health, ammo; color skin; char *name; public:
    // ------------- Конструкторы: monster(int he = 100, int am = 10); monster(color sk);

    46 monster(char * nam); monster(monster &M);
    // ------------- Деструктор:
    monster() {delete [] name;}
    // ------------- Операции: monster& operator ++(){++health; return *this;} monster operator ++(int)
    {monster M(*this); health++; return M;} operator int(){return health;} bool operator >(monster &M)
    { if( health > M.get_health()) return true; return false;
    } monster& operator = (monster &M)
    { if (&M == this) return *this; if (name) delete [] name; if (M.name) { name = new char [strlen(M.name) + 1]; strcpy(name, M.name);
    } else name = 0; health = M.health; ammo = M.ammo; skin = M.skin; return *this;
    }
    // ------------- Методы доступа к полям: int get_health() const { return health; } int get_ammo() const { return ammo; }
    // ------------- Методы, изменяющие значения полей: void set_health(int he){ health = he;} void draw(int x, int y, int scale, int position);
    };
    // ------------- Реализация класса monster ------------- monster::monster(int he, int am): health (he), ammo (am), skin (red), name (0){} monster::monster(monster &M)
    { if (M.name)
    { name = new char [strlen(M.name) + 1]; strcpy(name, M.name);
    } else name = 0; health = M.health; ammo = M.ammo; skin = M.skin;
    } monster::monster(color sk)
    { switch (sk)
    { case red: health = 100; ammo = 10; skin = red; name = 0; break; case green: health = 100;ammo = 20;skin = green; name = 0; break; case blue: health = 100; ammo = 40; skin = blue; name = 0;break;
    }
    } monster::monster(char * nam)
    { name = new char [strlen(nam)+1]; strcpy(name, nam); health = 100; ammo = 10; skin = red;
    }

    47 void monster::draw(int x, int y, int scale, int position)
    { /* ... Отрисовка monster */ }
    // ------------- Класс daemon ------------- class daemon : public monster
    { int brain; public:
    // ------------- Конструкторы: daemon(int br = 10){brain = br;}; daemon(color sk) : monster (sk) {brain = 10;} daemon(char * nam) : monster (nam) {brain = 10;} daemon(daemon &M) : monster (M) {brain = M.brain;}
    // ------------- Операции: daemon& operator = (daemon &M)
    { if (&M == this) return *this; brain = M.brain; monster::operator = (M); return *this;
    }
    // ------------- Методы, изменяющие значения полей: void draw(int x, int y, int scale, int position); void think();
    };
    // ------------- Реализация класса daemon ------------- void daemon::draw(int x, int y, int scale, int position)
    { /* ... Отрисовка daemon */ } void daemon:: think(){ /* ... */ }
    В классе daemon введено поле brain и метод think
    , определены соб- ственные конструкторы и операция присваивания, а также переопределен метод отрисовки draw
    . Все поля класса monster
    , операции (кроме присваи- вания) и методы get_health
    , get_ammo и set_health наследуются в классе daemon
    , а деструктор формируется по умолчанию.
    1   2   3   4   5   6   7   8   9   ...   15


    написать администратору сайта