1. Въведение в Windows


Създаване на собствени класове, поддърж. сериализация



страница3/4
Дата21.07.2017
Размер0.77 Mb.
1   2   3   4

21 Създаване на собствени класове, поддърж. сериализация.

Ако създаваме собствени класове и ако искаме те да поддържат променливи от този клас и ако направим този клас наследник на CObject, тогава не е неоход. да предеф. командите, компилатора автом. го прави.

- Новосъздаденият клас наследява CObject;

- В декларацията на класа DECLARE_SERIAL( име на класа)

- Предефинирвате Serialize() на базовия клас и сериализирате данновите членове на производния клас (ще поясним по-късно).

- Подразбиращ се конструктор на класа.

- В частта имплементация на класа IMPLEMENT_SERIAL

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

>>оператор за сесериализиране, <<оператор за сериализиране.

Кога се използва Serialize():

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

- Ако обектът е сериализиран със Serialize() трябва да се десериализира със Serialize().

- Ако създаваният клас има даннов елемент проявление или наследник на класа CОbject също се използва Serialize(), защото операциите << >> са дефинирани в CОbject да работят с указатели към обекти, а не с обекти.

- Ако сериализираният клас има даннов елемент указател към Cоbject или негов наследник, тогава имаме 2 възможности

1. Първо трябва да се конструира данновият обект във функцията за сериализация и след това да се използва указателя. Използването става по 2 начина:

PObject // указател

- PОbject ->Serialize();

- ar>>m.mydata>>p.Object

IMPLEMENT_SERIAL – този макрос иска три параметъра.

пример:

class CLine : public CObject



{DECLARE_SERIAL

(CLine)


protected: CPointm_ptFrom; CPointm_ptTo;

public: CLine(){}

CLine( CPoint from, CPoint to)

{m_ptFrom = from; m_ptTo = to;}

void Serialize (Carchive&ar );};

Kато дефинираме ф-ията Serialize така:

void CLine::Serialize( CArchive& ar)

{CObject::Serialize( ar);

if ( ar.IsStoring())

ar << m_ptFrom << m_ptTo;

else ar >> m_ptFrom >> m_ptTo;}

Добавяме и някъде в имплементацията:

IMPLEMENT_SERIAL( CLine, CObject, 1)

IMPLEMENT_SERIAL( CLine, CObject, 1 | VERSIONABLE_SCHEMA)

В примера са спазени всички правила за сериализация описани по-горе.
22 Версии при сериализация.

Ако след време искаме да променим нашия клас, тогава ! да преопишем Serialize, но пък написните преди това програми ! да се информират че версията е различна. Това става като се сравнят номерата на проектите: IMPLEMENT_SERIAL(cLine, CObject, 1| VERSIONABLE_SHEMA)

Ако открием разлика тогава се генерира exeption. Но ако искаме да подтиснем се използва VERSIONABLE_SHEMA и се приема че може да се работи с различни версии. Когато работим с различни версии ни трябва да предвидим код в Serialize(). Предефинирането работи за указатели към такива обекти-наследници, а не за самите обекти. Така че:

CLine* pLine = new CLine( CPoint( 0,0), CPoint( 40,40));

ar << pLine; //работи добре

CLine line( CPoint( 0,0), CPoint( 40,40));

ar << line; // няма да работи

Aко искаме да сериализираме по стойност, а не по указател, е добре така:

CLine line( CPoint( 0,0), CPoint( 40,40));

ar << &line; //ще работи за сериализация

при десериализация:

CLine* pLine;

ar >> pLine; //всичко е добре

CLine line = *pLine;

// операция “=” е предеф. в CLine като копиране;

delete pLine; // вече ненужен

Когато се обръщаме директно към Serialize се отказваме от проверка за проекти(само една версия) Операторите за сериал. преоб-ват CSrting в UniCode.
23. Управление на паметта в Win16.

1. Когато имаме много instance на едно приложение всички използват общ код и ресурси, само данните са различни.

2. Елементите в паметта са преместваеми по подразбиране

3. Сегментите както и ресурсите най-често се зареждат при повикване.

4. Работи механизма за отстраняване на блоковете от паметта.

Сегментна организация на Windows приложение:

В рамките на един сегмент указателите са near, а в различните сегменти са far (далечни). Ако работим с указатели трябва сегмента да го фиксираме и след като свършим работа да го освободим. Приложенията, като са сегментно организирани имат поне следните сегменти:

- един кодов и един даннов(поне)

- поне един служебен сегмент с някакви служебни таблици – този сегмент е общ за всички копия

- по един за всеки ресурс

Имаме 5 модела памет: small, medium, compact, large, huge.

Small – с 1 кодов и 1 даннов сегмент- модел за малки програми.

Medium – бързи програми с много кодови и един даннов сегмент,работи се с близки указатели към данни.

Compact – 1 кодов и много даннови сегменти, но той не е много добър

Large – модел с много кодови и с много даннови сегменти

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

1. Ако използваме обем неинициализирани данни, добре е те да са заделят динамично от глобалния Help.

2. Ако програмата се нуждае от голям брой инициализирани данни нищо не пречи да ги заделим като ресурс.

Правила:

Препоръчва се small и medium модела за 16 битова среда. Да се използват файл-указатели към данни. Да не се използват далечни указатели към функции, освен такива, които Windows си задава и тези , които се съставят в Windows програма. Разместване на сегментите в Windows програма.

Dos програмите съдържат в началото си код от вида:

mov ax, DGROUP

mov ds, ax

Данновият сегмент сочи данните на програмата и понеже DOS не размества, тогава този сегментен регистър позволява да се намират данните.

Освен това компилатора вмъква “пролог” и “епилог” части:

push bp


mov bp, sp

sub sp,x //име за локални променливи

Прологът и епилогът на всяка Windows far функция:

push ds //дата сегмента е единствения който сочи данните

pop ax

nop //празна операция



inc bp //basepointer

push bp


mov bp, sp

push ds


mov ds, ax

sub sp, x



Всяка far функция в паметта има един от следните видове пролози:

1.mov ax, ds

nop

За ф-ии викани от същата програма, но не от Windows



2. nop

nop


nop

За експлоатирани функции, които ще се викат само от Windows.

За пролога:

Когато линкера направи exe за всяка ф-ция стартовата точка се определя като преместваема. За всяка такава точка попълва служебна таблица, няколко байта служебна инф., к. се запазва с exe-то. Когато стартираме exe-то за изпълнението тази служебна инфо. се прочита и се създава шлюз на зареждане за всяка входна точка. Ако ф-ята е качена или не в паметта , а в този шлюз и на зареждане са намира, винаги има актуален адрес на ф-ята в паметта. Windows сам го обновява. Ако не е в паметта вместо jump, следва прекъсване на процесора и функцията се качва в паметта. Който вика само far във Windows, трябва да зареди в “ах” данните.MakeProcInstance cъздава втори шлюз Instance Thunk. В него ах се зарежда с данните и се реализира преход към Reload thunk.

reload thunk.

Instance thunk mov ax,..

DLL ф-те имат само 1Instance и тогава първите 3 байта се заменят с една 3 байтова ф-я. DLL нямат собствен стек, а ползват този на викащата страна

inc bp – целта е да можем чрез обратно прохождане на стека да се намери цяла поредица от викащи far ф-ии. Щом са far =>са различни сегменти=>може някоя от викащите ф-ии вече да не е в стека. За да се определи дали е в стека или не се инкрементира bp четен =>far нечетен, near – при обратно прохождане.

3. mov ax, xxx

За функциите от библиотечните модули на Windows (DLL)(USER, KERNEL, GDI,както и драйверите).


24. Виртуална памет

Осн. х-ка е изплзването и въвеждането на на вирт. памет. Всеки просец използва собствено адресно пространство 2(32)=4GB. Те се разделят поравно по default на 2 части- потребителски и системна. В рамките на това адр. Простр. Се отказваме от сегментите. Това променя работата с у-тели.Виртуалното адр. Пространство се образува като част от HDD. За да се ускори адр.простр се разделя на страници с фикс. Дължина 4К. Определя се от архитектурата на процесора. Страница е базовата ед. Памет ,к. се обменя м/у процесора и вирт. памет. В тези страници има флагове за различни нужди(дали стр. е в паметта). Една стр. може да е protected или shared. Динамичното заделяне също става на 3 стъпки:1. Резервиране 2. Ангажира. З.Заделя

Целта- ускоряване: 1. Информираме mem manager-a na win, че резервираме дадена памет и др.процеси не може да я ползват. 2. Съвместяваме табл.с адреси. 3. Обръщаме се към нея.

Резервирането става по страници. Има guard page и exeption: EXEPTION GUARD PAGE,к. се генерира при първият опит за достъп до охранявана станица.. Memory се алокира с shared( блока физически е на едно място, но е в адр. Пространство на 2 процеса)

Copy-on-write- при общ блок в адр.простр. на 2 процеса, за времето когато четат този блок е единствен, ако единия пробва да пише win managera прави копие на блока и насочва промените към копието.

HEAP: В 16Bit среда има локален и глобален heap. В 32 Heap-a e общ.

2 подхода:

1. Използване С С++ класове за работа с heap-a.

2. Използване API ф-ии на средата. Пример:

HANDLE hfile=::CreateFile(..)

HANDLE hMAP=::CreateFileMapping(hfile,…)

LPVOID lpvFile=::MapViewofFile(hMap,..)

DWORD dwfilesize=::GetFileSize(hfile,..)

::UnmapViewofFile(lpvFile);

::CloseHandle(hMap)

::CloseHandle(hFile)

Можем да създадем heap като укажем синхронен достъп на повече от една thread..

Във всеко проявление на класа ни да се структурира собствен heap. Удачно е щото при него се изолираме от влияние на други threat-ове. Може да предефинираме new() delete() ако заделяме памет за специфични обекти. Собствения heap авт. се унищожава като се унищож. общата за класа.(така гарантираме че няма да останат неизползвани хип-ове)

New()- проверява създаден ли е до момента private heap. Ако не-създава и инициализира брояч. Заделя памет в хип-а. Инкрементира брояча.

Delete()- декрементира. Освобождава памет от хип-а.



25. Работа с динамични блокове памет в многозадачна среда.

В 16 битова среда имаме локално и глобално адресно пространство.

При 32 битова среда поради линейното пространство няма локално и глобално адресно п-во, а то е едно (2GB потребителско адресно п-о). Има 2 функции за работа с хипове:

- С++ функции MFC- по – универсални

- с API функции на ОС – специфични и когато се гони бързо действие и големи обеми.

Има 2 вида хип:

- автоматично зареждане на приложението(по подразбиране)

- динамично зареждане – формиране на собствен потребителски хип.

Има API ф-ии heapCreate, с к. може да се съдаде heap. След което може да се заделя дин. област.



Препоръки за работа със собствения хип:

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

- безопасност – другата нишка да се намеси в хипа.

- до голяма степен се избягва фрагментацията.

- в С++ може да се предефинира New и Delete - чрез предефинирането на New може да се направи оптимално заделяне на памет. Схемата реализираща предефинирането на New е следната:

1) проверява дали е заделен локален хип с New ако не е го заделяме.

2) заедно със създаването се задава и брояч на New и Delete.

3) заделя се необходимия брой байтове, които се заделени

4) инкрементираме брояча

Схема реализираща предефинирането на Delete е следната:

1) освобождава блока

2) декремнтира брояча

3) проверява ако е 0 унищожава handler към файла

Има функция – heapwin за създаден вече (от нас) хип. Големите блокове алокират собствен хип и връщат указател към тях. За малки блокове използват памет от вече алокиран хип.

За големи блокове след интензивно използване на Delete трябва ни чести интервали за викане – heapwin, който всъшност освобождава тези блокове от хипа.

На етапа Commit паметта е заделена в Paging файла, а в паметта се заделя в ОП, едва когато се опитаме да запишем.



Права на достъп: само за четене, само за запис или др. Една страница в паметта може да е охраняема:

EXCEPTION_GUARD_PAGE

Ако имаме опция guard то трябва четене от паметта. MemoryManager отделя последната част от ОП като guard. Ако се опитаме да четем охраняемата граница, това става , когато е свършило останалото място и генериме EXEPTION_GUARD_PAGE, която може да се прихване и обработи, т.е. ако ни трябва още памет, то може за да заделим и да преместим охраняемата страница назад.

Една страница (част от паметта) може е shared, т.е да се използва от повече от 1 процес:

П-с1 Физ. памет П-с2

Share блок се помества в адресното пространство на 2 или повече процеса, но фактически е задлено само едно физическо адресно пространство.

Друг механизъм е Copy_On_Write

П-с1 Физ. памет П-с2

Едно .exe стартирано става процес. Ако 2 процеса само четат паметта то тя е една. Ако единият процес запише данни в този блок: то за другият процес се появяват нови данни. В този момент Copy_On_Write заделя нова ОП и копира там блока, тогава вече и двата процеса работят с различни блокове.

Memory Manager постоянно размества страници в паметта, но може да заключим страници за разместване и те тогава не могат да се разместват. Адреса е 32 – битов,отказваме се от 16 – битовата сегментна организация, (в 16 – битовата сегмента е 2.., а тук е 2..=4GB). Имаме по–големи сегменти.

Отделните процеси са в отделни сегменти. Друга новост тук е виртуалната организация на паметта. Тя се реализира чрез мощен SWAP механизъм. Трета новост е страничната организация на адресното пространство, т.е. работи се с блокчета, а не с цели сегмнети.

4GB адресно пространство може да се счита за разделено на 2 по 2GB.

- за потребителски програми

- за системни програми



Организация на адресното пространство:

Виртуален адрес

съдържание

0-64КВ

Служебни

Над 64КВ

За модули на изпълнимия файл

Над горния

за heaps и threads stacks

Над тях

За DLL

Над тях

С-мни DLL: Kernel32, User32, GDI32 и ОС

25В-45В

За нуждите на ОС

Резервирането заделя блок с определена големина:

pMem = VirtalAlloc(<начален адрес на блока NULL>, <бр.страници на резервиране>,<права на достъп>):

За виртуалната памет се прави файл върху диска, който съдържа страници. Обмена м/у вирт. и ОП памет става по страници.Един процес може да има най – много 4КВ обем страници. Една страница е валидна ако е в ОП. Ако страницата я няма в ОП , тя се намира в swap файла и се качва в паметта. Ако не достига памет, то страницата се сваля от ОП, но всъщност тя се припокрива. Има доста механизми и особености при припокриване. Съществува страница, маркирана за shared стр. с ограничено ниво на достъп и т. н. и всичко това ! да се провери.



Заделянето на памет за нуждите на процес е двустъпков процес:

1.Резервиране – Reserved

2.Ангажиране - Commit

Смисълът е да се отложи за колкото се може по – късен момент заделянето на памет, но възможно непосредствено преди записване. Резервирането е указание от MemoryManager за нуждата от такъв обем памет за дад. п-с.



26 Memory mapped file – средство за оптимално управление на паметта. Състой се от асоцииране на файла или част от файл с ОП. Файла или част от файл се асоциира с виртуалното адресно пространство и тогава над него могат да се правят определени операции без да се качва.

HANDLE hFile=::CreateFile(...)//създаваме Handle

HANDLE hMap=::CreateFileMapping(hFile,...);

LPVOID IpvFile=::MapViewoffile(hMap,...);//мапва целия файл

DWORD dwFilesize=::GetFileSize(hFile,...)//използваме файл

::UnmapViewofFile(IpvFile);

::CloseHandle(hMap);

::CloseHandle(hFile);

Два процеса могат да ползват общ hMap т.е. те имат обща памет:



GlobalAlloc(...,GMEM_SHARED...);

В Win32 не прави shared блок, както в Win16. Обща памет, но не общ файл както по – горе без CreateFile(...) и с подаване на параметри 0хFFFFFFFF вместо hFile.



GlobalAlloc е наследена в Win16. Тук в 32 битовата среда всъщност не действа , но може да се запише.

Съвет при работа с динамична памет:

- хипът се фрагментира при продължителна работа. Решението е по – често да се унищожава и да се създава нов или да се прекомпонира.

- викайте heapmin в NT за освобождаване на големи блокове

- не викайте HeapFree за малки блокове заделени с new.

- стекът вече не е ограничен до 64К и става толкова голям, колкото е необходимо.

Как да пестим пространство от swap файла?

- EXE и DLL файлове не са в swap file – те се включват във виртуалното адресно пространство на всеки процес. Добре е и константните данни по някакъв начин да се прикачат към тях, за да не попадат в swap file.

Низове :

Ако те са непроменяеми за цялото изпълнение , декларирайте const charmystr[]=”my string”;

Низът се съхранява заедно с кода – в секцията за инициализирани константни данни . тя се съхранява заедно с EXE файла. В тази секция не можем да слагаме само С++ обекти, създадени чрез конструктор. Например:

const Crect my_rect(0,0,100,100); се поставя в .bss, който се вкарва в swap file и всеки процес има копие на този обект. По – лошо е:

const CString my_str(“new instance”); което води до :


  1. поставяне на CString обект в .bss секцията

  2. масивът от символите в .data (иниц. неконст. данни )

  3. заемане памет за копие на символите за всеки стартиран процес.

Нищо не попада в EXE и всичко харчи памет.
27.Стратегии на управление на памет и събиране на боклук в .net платформа

* бъгове


* управляван heep в Common Language Runtime средата. Последоват. заемане, първо в кеш.

* heep на С runtime библ.:поддържа свързан списък на блокове памет.



Алгоритъм за събиране на боклук

OutOfMemoryException

корени (roots)- пример

граф на достижимете обекти от корените

# няма неизползвани обекти; # освободен  недостижим обект.

пр: class My {

static void XX() {

ArrayList x=new ArrayList(); //а е корен

for(Int32 x=0;x<1000;x++)

{

a.Add(new Object());



} //а реферира 1000 обекта

Console.WriteLine( a.Lenght); // използваме а

//липсва друго използване на а#### започва почистване на боклука

// 1001 обекта могат да се почистят

……}}

28.Финализиране на обекти в .NET

Типове, които обвиват ресурси, освен памет (файл, mutex, сокет..)изискват явно финализиране – Finalize() за почистването си. Пример:

protected override void Finalize()

{

try { CloseHandle(handle); }



finally { base.Finalize(); }

В C#, Managed C++ (всички __gc класове наследяват Finalize() от Object). Това става в синтаксиса на деструктора:

Ако имаме: ~MyClass()

{ Console::WriteLine(S”Finalizing..”);}

компилаторът го разширява до: MyClass::Finalize()

{Console::WriteLine(S”Finalizing..”); MyBaseClase::Finalize();}

virtual ~MyClass()

{System::GC::SuppressFinalize(this); A::Finalize();}

//”А” обединява всички обръщения към обекти, реферирани в конструктора

недостатъци на Finalize():

*заделяне на финализирани обекти е по-бавно

*осводождаването се забавя, особено при масиви от такива обекти

*финализиран обект, рефериращ други обекти забавя финализирането им

*няма контрол кога се изпълнява Finalize()

*няма гаранция за реда на изпълнения на Finalize().  във Finalize() да не се викат вътрешни обекти.

Кога се вика Finalize():


  1. поколение 0 се е запълнило;

  2. явно обръщение към System.GC.Collect()

  3. процес завършва – вика се Finalize() за всички обекти в него.

29. Унищожение – КОГА (модел на ликвидиране – експлицитно ). Интегриране на Finalize() и Dispose()

__gc class Tester : public IDisposable

{ bool bDisposed;

public:


Tester()

{ Console::WriteLine(S"Tester constructor"); bDisposed = false; }

~Tester()

{ if (bDisposed == false)

{ Console::WriteLine(S"Tester finalizer"); } }

void Dispose()

{ Console::WriteLine(S"Tester dispose"); bDisposed = true; }

void aMethod()

{ if (bDisposed) throw new ObjectDisposedException(S"Tester");

Console::WriteLine(S"Tester aMethod"); }};

// This is the entry point for this application

int main(void)

{Console::WriteLine(S"Finalization Test"); Tester* pt = new Tester();

/* Call Dispose */ pt->Dispose();

/* Try calling a method on the object...*/ pt->aMethod();

Console::WriteLine(S"End of Test"); return 0;}

// ресурс трябва да бъде освободен (Dispose) и след това изтрит (напр.

// File.Delete(..)). Иначе се счита че той се използва от друг процес.

// Почистване с Dispose форсира финализацията, но не унищожава обекта от

//паметта – това прави събирането на боклука.Т.е. могат да се викат

// методи на обекта – стига да не ползват невалидни указатели.

30.Поколения

*нов обект  по-кратък живот

*събиране нач аст от heap е по-бързо от събиране на целия heap.

*неизследвани обекти в управлявания heap са от поколение 0.

А B C D E

поколение 0

ABD

пок1 поколение 0



ABDFGHI JK

пок1 пок 0

ABDFGIK

пок 1пок 0



ABDFGIK LMNO

пок1 пок 0

ABDFGIJNO

поколение 0 пок 0

ABDFGIJNOPQRS

поколение 1 пок 0

DFINOQS

пок2 п1 поколение 0



активиране системата за събиране на боклука:

void GC.Collect(Int32 Generation);

void Gc.Collect();

31. Документ – Изображение

В MFC e залегнала концепцията програмата да се създаде с най - малко 2 слоя:

- документен – занимаващ се с функционалното обработ. на данните, възстановяване, сериализация, комуникация и др. Възможно е едно изображение да сменя множество документи или един документ да има много изображения.

- SDI - приложения определена архитектура (класове); В SDI в дадне момент има един документ и в общия случай едно изображение.

В МDI в даден момент се работи с няколко документа от различен тип.

В началото още се указва какво приложение SDI или МDI ще се прави. При двуслойна архитектура се обособяват няколко класове групи:

1)документен клас- обикновено името и класовете включват DOC

2)view class – обикновено името и класовете включват view частицата. Класът View е дъщерен на рамката (Frame) и се явява клиентската част на приложението. То трябва да има производен на СView класа и всички извеждания отиват към методите на тези класове.

3) Frame class – класове на рамката. Това е най-големия прозорец, в който се разполагат изображението. Някои съобщения могат да се пренасочат за обработка от Frame, а не от View.

4)Application class – включва стартова точка на програмата, връзката между Frame/Veiw/Document

5)Document class – отговаря за обработка на данни, сериализация, документация. Документния клас притежава специфичните интерфейси за обработка на данните. Има методи за обхойдане на всички обекти създадени от него или за UpdateAllView …

View class

Изобразява данните. Комуникира с документа. Всеки View се свързва с документ посредством GetDocument().

CViewCountDoc* pDoc = dynamic_cast(GetDocument());



// Добре е в прил. да има собствена реал. на GetDocument():

ViewCountDoc* CViewCountView::GetDocumet() //връзка между Doc и View

{CViewCountDoc* pDoc = dynamic_cast(GetDocument());

ASSERT_VALID( pDoc);

Return pDoc; }

void CMyView::OnDraw(CDC* pDC)

{ CMyDoc* pDoc = GetDocument(); //прави връзка с документа откъдето трябва да се вземе стринга

CString string = pDocGetString();

CRect rect;

GetClientRect(&rect);

pDCDrawText( string, rect,……………..); }

Има два Frame:

- MainFrame

- ChildFrame

Main Frame Windows

Document Template(документен шаблон)

Такъв има всяко приложение. Той е различен в MDI и SDI. Прави връзка между ( при това само по 1) document class, view, frame.

Има CSingleDocTemplate и CMultipleDocTemplate. Съдържа:



  • документен низ (7 полета),

\nMy_app\My_app\n\n\nMy_app.Document\nMy_app Document

  • Иконата на изображението

  • Менюто при активно изображение.

SDI документния шаблон се създава още в InitInstanse():

CSingleDocTemplate * pDocTemplate;

PDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS( CMyDoc), RUNTIME_CLASS( CMainFrame), RUNTIME_CLASS( CMyView));

AddDocTemplate(pDocTemplate);

// следват редове за създаване обектите от шаблона, показване рамката и изгледа. Включва се цикълът за обр. съобщения, като съобщенията се разделят към обектите - приложение, рамка, изглед, документ, прозорец (command routing).





Поделитесь с Вашими друзьями:
1   2   3   4




База данных защищена авторским правом ©obuch.info 2020
отнасят до администрацията

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