1. Булеви функции. Теорема на Пост-Яблонски за пълнота. Нека J2 = { 0, 1}. Всяка функция f : J2n  J



страница14/29
Дата11.01.2018
Размер5.91 Mb.
#44141
1   ...   10   11   12   13   14   15   16   17   ...   29

Указателите към обекти се дефинират по същия начин както указател към стандартен тип данни. Например, ако в програмата е дефиниран класът MyClass, то са валидни следните дефиниции:

MyClass obj;

MyClass *ref = &obj;

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

Елементите на един масив могат да са обекти, но разбира се от един и същи клас. Нека например е дефиниран класът MyClass, който има открита член-данна x и открита член-функция f без параметри.

Следната дефиниция е валидна:

MyClass obj[20];

Достъпът до елементите на масива е пряк и се осъществява по стандартния начин чрез индексиране: obj[0], obj[1], …, obj[19].

Тъй като obj[i], i = 0, 1, …, 19, е обект, към него са възможни обръщенията obj[i].x и obj[i].f().

Член-данна на един клас може да е масив. Например, да предположим, че MyClass има допълнителна открита член-данна y[5]. Нека obj е обект от клас MyClass. Тогава достъпът до елементите на масива y в обекта obj ще се осъществи по следния начин: obj.y[0], obj.y[1], …, obj.y[4]. Конструкторът по подразбиране играе важна роля при дефинирането и инициализирането на масиви от обекти. Масив от обекти, дефиниран в програмата се инициализира по два начина: явно (чрез инициализиращ списък) или неявно (чрез извикване на конструктора по подразбиране за всеки обект – елемент на масива).

Всяка програма има няколко места за съхраняване на данни, едно от тях е областта за динамични данни (heap). Динамичните данни са такива, че техният брой и размер не е известен в момента на проектиране на програмата. Те се създават и разрушават по време на изпълнение на програмата. След разрушаването им, заетата от тях памет се освобождава и може да се използва наново. Така паметта се използва по-ефективно.

Създаването и разрушаването на динамични данни в езика C++ се извършва чрез операцията new и оператора delete. Извикването на new заделя необходимата памет в heap-а и връща указател към нея. Извикването на delete освобождава паметта в heap-а, сочена от указател. На всяко извикване на new трябва да съответства извикване на delete, тъй като heap-ът не се чисти автоматично и в противен случай паметта рано или късно ще свърши. Синтаксисът на операцията new е следния:

new <име_на_тип> [[<размер>]] или

new <име_на_тип>(<инициализация>). Тук <име_на_тип> е име на някой от стандартните типове или е име на клас, <размер> е произволен израз, който може да се преобразува до цял и показва броя на компонентите от тип <име_на_тип>, за които да се задели памет в heap-а, <инициализация> е израз от тип <име_на_тип> или инициализация на обект според синтактиса на конструктор на класа, ако <име_на_тип> е име на клас. При това, <инициализация> не може да присъства, ако е зададен <размер>. Семантиката на операцията new е следната: в heap-а се заделят

sizeof (<име_на_тип>) или sizeof (<име_на­_тип>) * <размер> байта в зависимост от това дали не е указан или е указан <размер> и се инициализират чрез <инициализация>, ако такава има. Операцията връща като резултат указател към (първия елемент от) заделената памет. Ако в heap-а няма достатъчно памет, new връща NULL. Синтаксисът на оператора delete е следния:

delete <указател_към_динамичен_обект>. Тук <указател_към_динамичен_обект> е указател към динамична данна, създадена чрез new. Семантиката на delete е следната: разрушава данната, адресирана от указателя и паметта, заемана от тази данна се освобождава. Ако данната, адресирана от указателя е обект на клас преди да се разруши се извиква деструктора на класа. Ако динамичната данна е масив

(в заделянето на памет за нея с new е бил указан <размер>

по-голям от 1), то delete трябва да се използва в следната форма: delete [] <указател>.

Разрушаването на обекти на класовете в някои случаи е свързано с извършването на определени действия, които се наричат заключителни. Най-често тези действия са свързани с освобождаване на заделената преди това динамична памет, възстановяване на състояние на програмата и др. Ефектът от заключителните действия е противоположен на ефекта на инициализацията. Естествено е да се даде възможност тези действия да се извършат автоматично при разрушаване на обекта. Това се осъществява чрез деструкторите на класовете. Деструкторът е член-функция, която се извиква при разрушаване на динамичен обект с оператора delete или при излизане от блока, в който е бил създаден обекта на класа. Един клас може да има явно дефиниран точно един деструктор. Името му съвпада с името на класа, предшествано от символа ‘~’. Типът му е void и той явно не се указва. Освен това, деструкторът не може да има формални параметри. Ако конструкторът или някоя член-функция на един клас реализира динамично заделяне на памет за някоя член-данна, използването на деструктор е задължително, тъй като трябва да се освободи заетата динамична памет.

Съществуват два начина за създаване на обекти: чрез дефиниция или чрез функциите за динамично управление на паметта.

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

Във втория случай създаването и разрушаването на обекта се управлява от програмиста. Създаването става чрез операцията new, а разрушаването чрез оператора delete. Операцията new използва конструкторите на класа, а оператора delete деструктора на класа.

Създаването на масиви от обекти става по два начина.

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

Вторият начин е чрез функциите за динамично управление на паметта. Отново създаването става чрез new, при това се указва размерът на масива и унищожаването става чрез delete []. При това, при изпълнението на операцията new за всеки обект от новосъздадения масив се извиква конструктора по премълчаване на класа, а при изпълнението на оператора delete[] се извиква деструктора на класа за всеки един от елементите на масива.

Както вече споменахме, за масивите, реализирани в динамичната памет не може явно да се задава инициализация.

Всеки оператор се характеризира с позиция на оператора, спрямо аргументите му, приоритет и асоциативност. Позицията на оператора спрямо аргументите му го определя като префиксен (поставя се пред аргументите), инфиксен (поставя се между аргументите) или постфиксен (поставя се след аргументите). Приоритетът определя реда на изпълнение на операторите в операторен терм. Операторите с по-висок приоритет се изпълняват преди тези с по-нисък. Асоциативността определя реда на изпълнение на оператори с еднакъв приоритет в операторен терм. В C++ има ляво и дясно асоциативни оператори. Лявоасоциативните оператори се изпълняват отляво надясно, а дясноасоциативните се изпълняват отдясно наляво.

В C++ не могат да се дефинират нови оператори, но всеки съществуващ оператор, с изключение на ::, ?:, ., .*, sizeof, може да бъде предефиниран от програмиста, стига поне един операнд на оператора да е обект на някакъв клас. Предефинирането се извършва чрез дефиниране на специален вид функции, наречени операторни функции. Последните имат синтаксиса на обикновени функции, но името им се състои от запазената дума operator, последвана от мнемоничното означение на предефинираната операция. Когато предефинираната операция изисква достъп до компонентите на класове, декларирани като private или protected, операторната дефиниция трябва да е член-функция или функция-приятел на тези класове. В противен случай, т.е. когато операторната дефиниция е външна функция неприятел на тези класове тя няма достъп до въпросните компоненти. Позицията спрямо аргументите, приоритета и асоциативността на една операция не може да се променят при нейното предефиниране. Също така, броят на нейните аргументи не може да бъде променен и операторните функции не могат да имат аргументи по премълчаване.

В различни случаи операторните функции е най-добре да бъдат приятелски функции или функции-елементи.

Ако левият операнд на предефинираната операция трябва задължително да бъде обект на клас или псевдоним на обект на клас, тогава тя се предефинира с функция елемент. Например операциите за извикване на функция ‘()’, за достъп до елемент на масив ‘[]’, указателната операция ‘->’ и операцията за присвояване ‘=’ винаги се предефинират с функция елемент на клас.

Ако левият операнд трябва да бъде обект от друг клас или от вграден тип, тогава операцията не може да бъде предефинирана с функция-елемент и тя се предефинира с външна функция, която е приятел на класа.

Унарна операция може да се предефинира с помощта на нестатична функция елемент без аргументи или с външна функция-приятел с един аргумент. Този аргумент трябва да бъде или обект на класа или псевдоним на обект на класа.

Бинарна операция може да се предефинира с помощта на нестатична функция елемент с един аргумент или с външна функция-приятел с два аргумента. Единият от тези аргументи трябва да бъде или обект от класа или псевдоним на обект от класа.

Операциите = и унарната & могат да се използват с обекти от всеки клас без те да са предефинирани. По премълчаване, унарната операция &, приложена към обект от кой да е клас, връща адреса на обекта в паметта. По премълчаване, присвояване (=) може да се извършва между обекти от един и същ клас и то се свежда до побитово копиране на данните-елементи на класа. Такова копиране е опасно за класове с данни-елементи, които сочат към динамично разпределена памет. След извършване на такова присвояване за два различни обекта от такъв клас, тези два обекта ще сочат към една и съща област от паметта. Изпълнението на деструктора на кой да е от тези обекти ще доведе до освобождаването на тази памет. Ако после, обаче, чрез другия обект се извърши обръщение към сочената от него вече освободена памет, резултатът ще бъде неопределен.

Поради тази причина, операцията = за такива класове задължително се предефинира.

Възниква необходимостта от средства, които реализират функции и класове, зависещи от параметри, които задават типове данни и при конкретни приложения параметрите да се конкретизират. Такива средства са шаблоните. Те позволяват създаването на функции и класове, използващи неопределени типове данни за своите аргументи. Така шаблоните на класове позволяват да бъдат описвани обобщени типове данни. Шаблоните на класове

най-често се използват за изграждане на общоцелеви класове-контейнери (стекове, опашки, списъци и др.).

Шаблон на функция се дефинира като обикновена функция, но със следните разлики: заглавието на функцията се предшества от запазената дума template, последвана от списъка от формалните параметри на шаблона, заграден в ‘<’, ‘>’ – списък от идентификатори, предшествани от запазената дума class и разделени със запетаи. Формалните параметри на шаблона означават всеки вграден или потребителски тип и могат да се използват при указването на типове на параметрите на функцията, при указването на типа на резултата на функцията или при указването на тип вътре в тялото на функцията (например, при дефинирани на локални променливи). Използването на дефиниран шаблон на функция става чрез обръщение към обобщената функция, която шаблонът дефинира, с параметри от конкретен тип. При такова обръщение, компилаторът генерира шаблонна функция, като замества параметрите на шаблона с типовете на съответните фактически параметри.

Декларацията на шаблон на клас изглежда като традиционно описание на клас със следната разлика: предшества се от заглавие, което започва с ключовата дума template, последвана от списъка на формалните параметри на шаблона, ограден в ‘<’ и ‘>’. Този списък има аналогичен вид както при шаблоните на функции.

Формалните параметри на шаблона на класа означават типове (вградени или потребителски) и могат да се използват навсякъде в декларацията на класа – като типове за член-данни, като типове на параметри или тип на резултат на член-функции, като типове на локални променливи на член-функции и т.н. Всяка дефиниция на функция-елемент извън декларацията на класа трябва да се предшества от заглавието на шаблона на класа. Дефиницията е стандартна с тази разлика, че винаги когато в нея се използва името на класа (с изключение на случая, когато то е име на конструктор), то трябва да се задава след него списъка от имената на формалните параметри на шаблона, ограден в ‘<’ и ‘>’.

За разлика от шаблоните на функции, при създаване на обект на шаблонен клас изрично трябва да се зададат конкретните типове, за които ще се създава този обект, като имената на конкретните типове са подредени в списък, ограден с ‘<’ и ‘>’ след името на класа в дефиницията на обекта. Например, нека имаме следната декларация на шаблон на клас:

template

class Stack {

};



Тогава са валидни следните дефиниции на обекти:

Stack obj1;

Stack obj2;

и т.н.


В шаблоните на функции и класове има възможност да се използват и нетипови параметри – те се задават заедно с типовите параметри в списъка на формалните параметри на шаблона.

Фактическите стойности на нетиповите параметри трябва да са константни изрази, тъй като те се обработват по време на компилация.


12. Обектно-ориентирано програмиране (C++): Наследяване и полиморфизъм.
Производните класове и наследяването са една от най-важните характеристики на обектно-ориентираното програмиране. Като се използва механизмът на наследяването от съществуващ клас може да се създаде нов клас. Класът, от който се създава, се нарича базов клас, класът, който се създава се нарича производен клас. Производният клас може да наследи компонентите на един или на няколко базови класа. В първия случай наследяването се нарича просто, във втория случай се нарича множествено. Дефинирането на производни класове е еквивалентно на конструирането на йерархии от класове. Всеки производен клас може от своя страна да е базов при създаване на нови производни класове. Ако множество от класове имат общи данни и методи, тези общи части могат да се обособят като базови класове, а всяка от останалите части да се дефинира като производен клас на тези базови класове. Така се прави икономия на памет, тъй като се избягва многократното описание на едни и същи програмни фрагменти.

Производните класове се дефинират като обикновени класове с единствената разлика, че след името на производния клас в заглавието се поставя символът ‘:’, последван от списък от двойки

[<атрибут_за_област>] <име_на_базов_клас>, разделени със запетаи. Този списък определя кои са базовите класове. Атрибутът за област е незадължителен и може да бъде public, private или protected. Той определя областта на наследените компоненти в производния клас. Ако атрибутът за област е пропуснат, подразбира се private. Множеството от компонентите на производния клас се състои от компонентите на всички негови базови класове, заедно със собствените компоненти на производния клас.

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

При атрибут за област public, елементите от тип public на базовия клас се наследяват като елементи от тип public на производния клас и елементите от тип protected на базовия клас се наследяват като елементи от тип protected на производния клас.

При атрибут за област protected, елементите от тип public и protected на базовия клас се наследяват като елементи от тип protected на производния клас.

При атрибут за област private, елементите от тип public и protected на базовия клас се наследяват като елементи от тип private на производния клас.

И при трите вида наследявания, производният клас няма достъп до елементите от тип private на базовия клас.

Външна функция, която не е приятел на производния клас чрез обект от производния клас има достъп само до компонентите от тип public на производния клас – това означава, че такава функция чрез обект на производния клас може да има достъп до наследени компоненти от базовия клас само ако те са от тип public в базовия клас и атрибутът за област е public.

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

Функциите приятели на производния клас имат същите права за достъп както член-функциите на производния клас – това са достъп до всички собствени компоненти на производния клас и достъп до наследените компоненти от тип public и protected на базовия клас. Декларацията за приятелство не се наследява – функция приятел на базов клас не е автоматично приятел на производния клас (освен ако не е изрично декларирана като такава в производния клас).

Базовият и производният клас могат да притежават компоненти с еднакви имена. В този случай, производният клас ще притежава компоненти с еднакви имена. Обръщението към такава компонента чрез обект от производния клас или от член-функция на производния клас извиква декларираната в производния клас компонента, т.е. името на собствената компонента скрива името на наследената компонента. За да се използва покритата компонента, трябва да се укаже нейното пълно име:



<име_на_клас>::<име_на_компонента>, където <име_на_клас> е името на базовия клас.

По-нататък под обикновен конструктор ще разбираме конструктор, различен от конструктора за присвояване.

Обикновените конструктори, конструкторът за присвояване, операторната функция за присвояване и деструкторът са методи за които не важат общите правила за наследяване. Тези методи

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

Основен въпрос е как да се реализира инициализирането на наследената част на производния клас. Най-естествено е това да се направи от конструктора на производния клас, но от друга страна този конструктор няма достъп до private компонентите на базовия клас. Затова конструкторите на производния клас инициализират само собствената част на този клас, а наследената част се инициализира от конструктор на базовия клас – това се осъществява като в дефиницията на конструктора на производния клас се укаже обръщение към съответен конструктор на базовия клас. Основен момент е, че обръщенията към конструктор на базови класове се осъществява посредством инициализиращ списък. Този списък се записва в дефиницията на конструктора на производния клас след името на този конструктор и знак ‘:’. Списъкът се състои от обръщения към конструктори на базовите класове, разделени със запетаи. При просто наследяване в този списък присъства най-много едно обръщение към конструктор на единствения базов клас. При множествено наследяване в списъка са указани няколко обръщения към конструктори на базови класове, най-много по едно обръщение към конструктор за даден базов клас. Имената на параметрите на конструктора на производния клас могат да се използват като фактически параметри в обръщенията към конструкторите на базовите класове в инициализиращия списък.

Дефинирането на обект от производен клас предизвиква създаване на неявни обекти от базовите класове и добавяне към тях на декларираните в производния клас компоненти. Това означава, че първо се извикват конструкторите на базовите класове, а след това се извиква конструкторът на производния клас. Редът, в който се извикват конструкторите на базовите класове, е редът, в който тези класове са посочени в заглавието на производния клас. На този ред не влияе последователността, в която са посочени конструкторите на базовите класове в инициализиращия списък в дефиницията на конструктора на производния клас. Ако производният клас има член-данни, които са обекти, техните конструктори се извикват след изпълнението на конструкторите на базовите класове и преди изпълнението на тялото на конструктора на производния клас. Редът, в който се изпълняват конструкторите на обектите-елементи е редът, в който те са декларирани в тялото на производния клас.

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

Ако за базовия клас е дефиниран поне един конструктор с параметри и за този клас не е дефиниран конструктор по премълчаване, то за производния клас задължително трябва да е дефиниран конструктор, който в своя инициализиращ списък извършва обръщение към един от дефинираните конструктори на въпросния базов клас. Ако това не е изпълнено, компилаторът

съобщава за грешка.

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

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

Деструкторите на един производен клас и на неговите базови класове се изпълняват в ред, обратен на реда на изпълнение на съответните конструктори. Най-напред се изпълнява деструкторът на производния клас, след това деструкторите на обектите-елементи на производния клас и най-накрая деструкторите на базовите класове.

С някои малки изключения, производният клас не наследява от базовия клас конструктора за присвояване и операторната функция за присвояване.

При конструкторите за присвояване се спазва същият принцип както при обикновените конструктори на производния и базовия клас. Конструкторът за присвояване на производния клас инициализира собствените член-данни на производния клас, а конструкторът за присвояване на базовия клас инициализира наследените член-данни.

Нека в производния клас не е дефиниран конструктор за присвояване, но в базовия клас има такъв. Тогава, за производния клас се създава служебен конструктор за присвояване, който от своя страна извиква конструктора за присвояване на базовия клас. Да отбележим, че при обикновените конструктори този случай ще предизвика грешка, ако за базовия клас не е дефиниран конструктор по премълчаване. Затова в случая се казва, че производният клас наследява конструкторът за присвояване от базовия клас.

Нека в производния клас и в базовия клас няма дефинирани конструктори за присвояване. Тогава и за двата класа се създават служебни конструктори за присвояване, като този на производния клас извиква този на базовия клас.

Нека в производния клас е дефиниран конструктор за присвояване. В неговия инициализиращ списък може да има, но може и да няма обръщение към конструктор (обикновен или за присвояване) на базовия клас. Препоръчва се в инициализиращия списък на конструктора за присвояване на производния клас да има обръщение към конструктора за присвояване на базовия клас, ако такъв е дефиниран. При това, фактическият параметър на това обръщение може да съвпада с фактическия параметър на обръщението към конструктора за присвояване на производния клас. Това е позволено, тъй като в случая обекта на производния клас може да се разглежда като обект на базовия клас (при работа с обекти във външни функции, това може да се счита само при наследяване от тип public). Ако в инициализиращия списък на конструктора за присвояване на производния клас не е указано обръщение към конструктор на базовия клас, то се извиква конструкторът по премълчаване на базовия клас. Ако базовия клас няма такъв конструктор, компилаторът ще съобщи за грешка.

Операторната функция за присвояване на производен клас трябва да указва как да става присвояването както на собствените, така и на наследените си член-данни. За разлика от конструкторите на производни класове, тя прави това в своето тяло (не поддържа инициализиращ списък).

Нека в производния клас не е дефинирана операторна функция за присвояване. Тогава компилаторът създава служебна такава. Тя се обръща към операторната функция за присвояване на базовия клас, чрез която инициализира наследената част, след това инициализира чрез обикновено присвояване и собствената част на производния клас. Затова в този случай се казва, че операторът за присвояване на базовия клас се наследява, т.е. за наследените член-данни се използва служебният или предефинираният оператор за присвояване на базовия клас.

Нека в производния клас е дефинирана операторна функция за присвояване. Тогава тази функция трябва да се погрижи за присвояването на наследените член-данни. Налага се в нейното тяло да има обръщение към операторната функция за присвояване на базовия клас, ако има такава. Ако това обръщение не е направено явно, то стандартът на езика не уточнява как ще стане присвояването на наследените компоненти.

В случаите когато производният клас наследява повече от един базов клас се казва, че класът е с множествено наследяване. Този вид наследяване е мощен инструмент на обектно-ориентираното програмиране, тъй като чрез него се изграждат графовидни йерархични структури. Имената на базовите класове се задават в заглавието на производния клас след името му и символът ‘:’, разделят се със запетаи, при това всеки от тях се предшества или не от съответен атрибут за област. За член-функциите от голямата четворка на производен клас с множествено наследяване са в сила същите правила, както при производен клас с просто наследяване – тези правила се прилагат независимо към всеки един базов клас. Изпълнението на конструктор на производен клас с множествено наследяване става така: първо се извикват конструкторите на всички базови класове, в реда, указан в заглавието на производния клас – кой конструктор ще се извика зависи от това дали присъства или не обръщение в инициализиращия списък на конструктора на производния клас, второ се извикват конструкторите на собствените обекти-елементи на производния клас, в реда, в който те са описани в тялото на класа и най-накрая се изпълнява тялото на конструктора на производния клас.

Извикването на деструкторите става в ред, обратен на реда на съответните конструктори. Препоръчва се конструкторът за присвояване (ако е дефиниран) на производния клас да извършва обръщения чрез инициализиращия си списък към конструкторите за присвояване на всички базови класове. Операторната функция за присвояване на производен клас с множествено наследяване обикновено има следния вид:


<производен_клас>& <производен_клас>::operator= (const <производен_клас>&r) {

if (this != &r)

{ <базов_клас_1>::operator= (r);

<базов_клас_2>::operator= (r);

<базов_клас_N>::operator= (r);



Del();

Copy (r);

}

return *this;



}

Тук функцията Del изтрива собствените компоненти на подразбиращия се обект, а функцията Copy (r) копира в подразбиращия се обект компонентите на обекта r, съответни на собствените член-данни на производния клас. Извършва се проверка за самоприсвояване и функцията връща псевдоним на обекта от лявата страна на присвояването, което позволява слепване на няколко присвоявания.





Сподели с приятели:
1   ...   10   11   12   13   14   15   16   17   ...   29




©obuch.info 2024
отнасят до администрацията

    Начална страница