BAZA WIEDZY
KURSY
Bazy danych w PHP
Kurs AdvancedAJAX
Kurs ASP
Kurs ASP.NET
Kurs C++
Kurs CSS
Kurs HTML
Kurs HTML drugi
Kurs JavaScript
Kurs MySQL
Kurs PHP
Kurs RSS
Kurs XHTML
Obiekty DOM
MANUALE
CSS1 - W3C
DOM - w budowie
PHP 2005
PHP 2006
Wyrażenia regularne
SHOUTBOX
STAT
Online: 5 | UU: 523

Ku pamięci

Wskaźniki są ściśle związane z pamięcią komputera - a więc miejscem, w którym przechowuje on dane. Przydatne będzie zatem przypomnienie sobie (a może dopiero poznanie?) kilku podstawowych informacji na ten temat.

Rodzaje pamięci

Można wyróżnić wiele rodzajów pamięci, jakimi dysponuje pecet, kierując się różnymi przesłankami. Najczęściej stosuje się kryteria szybkości i pojemności ; są one ważne nie tylko dla nas, programistów, ale praktycznie dla każdego użytkownika komputera.

Nietrudno przy tym zauważyć, że są one ze sobą wzajemnie powiązane: im większa jest szybkość danego typu pamięci, tym mniej danych można w niej przechowywać, i na odwrót. Nie ma niestety pamięci zarówno wydajnej, jak i pojemnej - zawsze potrzebny jest jakiś kompromis.

Zjawisko to obrazuje poniższy wykres:


Wykres 2. Szybkość oraz pojemność kilku typów pamięci komputera

Zostały na nim umieszczone wszystkie rodzaje pamięci komputera, jakimi się zaraz dokładnie przyjrzymy.

Rejestry procesora

Procesor jest jednostką obliczeniową w komputerze. Nieszczególnie zatem kojarzy się z przechowywaniem danych w jakiejś formie pamięci. A jednak posiada on własne jej zasoby, które są kluczowe dla prawidłowego funkcjonowania całego systemu. Nazywamy je rejestrami .

Każdy rejestr ma postać pojedynczej komórki pamięci, zaś ich liczba zależy głównie od modelu procesora (generacji). Wielkość rejestru jest natomiast potocznie znana jako "bitowość" procesora: najpopularniejsze obecnie jednostki 32-bitowe mają więc rejestry o wielkości 32 bitów, czyli 4 bajtów.

Ten sam rozmiar mają też w C++ zmienne typu int , i nie jest to bynajmniej przypadek :)

Większość rejestrów ma ściśle określone znaczenie i zadania do wykonania. Nie są one więc przeznaczone do reprezentowania dowolnych danych, które by się weń zmieściły. Zamiast tego pełnią różne ważne funkcje w obrębie całego systemu.

Ze względu na wykonywane przez siebie role, wśród rejestrów procesora możemy wyróżnić:

  • cztery rejestry uniwersalne (EAX, EBX, ECX i EDX 1 ). Przy ich pomocy procesor wykonuje operacje arytmetyczne (dodawanie, odejmowanie, mnożenie i dzielenie). Niektóre wspomagają też wykonywanie programów, np. EAX jest używany do zwracania wyników funkcji, zaś ECX jako licznik w pętlach.
    Rejestry uniwersalne mają więc największe znaczenie dla programistów (głównie asemblera), gdyż często są wykorzystywane na potrzeby ich aplikacji. Z pozostałych natomiast korzysta prawie wyłącznie sam procesor.

Każdy z rejestrów uniwersalnych zawiera w sobie mniejsze, 16-bitowe, a te z kolei po dwa rejestry ośmiobitowe. Mogą one być modyfikowane niezależnie do innych, ale trzeba oczywiście pamiętać, że zmiana kilku bitów pociąga za sobą pewną zmianę całej wartości.

  • rejestry segmentowe pomagają organizować pamięć operacyjną. Dzięki nim procesor "wie", w której części RAMu znajduje się kod aktualnie działającego programu, jego dane itp.

  • rejestry wskaźnikowe pokazują na ważne obszary pamięci, jak choćby aktualnie wykonywana instrukcja programu.

  • dwa rejestry indeksowe są używane przy kopiowaniu jednego fragmentu pamięci do drugiego.

Ten podstawowy zestaw może być oczywiście uzupełniony o inne rejestry, jednak powyższe są absolutnie niezbędne do pracy procesora.

Najważniejszą cechą wszystkich rejestrów jest błyskawiczny czas dostępu . Ponieważ ulokowane są w samym procesorze, skorzystanie z nich nie zmusza do odbycia "wycieczki" wgłąb pamięci operacyjnej i dlatego odbywa się wręcz ekspresowo. Jest to w zasadzie najszybszy rodzaj pamięci, jakim dysponuje komputer.

Ceną za tę szybkość jest oczywiście znikoma objętość rejestrów - na pewno nie można w nich przechowywać złożonych danych. Co więcej, ich panem i władcą jest tylko i wyłącznie sam procesor, zatem nigdy nie można mieć pewności, czy zapisane w nich informacje nie zostaną zastąpione innymi. Trzeba też pamiętać, że nieumiejętne manipulowanie innymi rejestrami niż uniwersalne może doprowadzić nawet do zawieszenia komputera; na tak niskim poziomie nie ma już bowiem żadnych komunikatów o błędach.

Zmienne przechowywane w rejestrach

Możemy jednak odnieść pewne korzyści z istnienia rejestrów procesora i sprawić, by zaczęły działać po naszej stronie. Jako niezwykle szybkie porcje pamięci są idealne do przechowywania małych, ale często i intensywnie używanych zmiennych.

Na dodatek nie musimy wcale martwić się o to, w którym dokładnie rejestrze możemy w danej chwili zapisać dane oraz czy pozostaną one tam nienaruszone. Czynności te można bowiem zlecić kompilatorowi: wystarczy jedynie użyć słowa kluczowego register - na przykład:

register int nZmiennaRejestrowa;

Gdy opatrzymy deklarację zmiennej tym modyfikatorem, to będzie ona w miarę możliwości przechowywana w którymś z rejestrów uniwersalnych procesora. Powinno to rzecz jasna przyspieszyć działanie całego programu.

1 Wszystkie nazwy rejestrów odnoszą się do procesorów 32-bitowych.

Dostęp do rejestrów

Rejestry procesora, jako związane ścisle ze sprzętem, są rzeczą niskopoziomową . C++ jest zaś językiem wysokiego poziomu i szczyci się niezależnością od platformy sprzętowej.

Powoduje to, iż nie posiada on żadnych specjalnych mechanizmów, pozwalających odczytać lub zapisywać dane do rejestrów procesora. Zdecydowała o tym nie tylko przenośność, ale i bezpieczeństwo - "mieszanie" w tak zaawansowanych obszarach systemu może bowiem przynieść sporo szkody.

Jedynym sposobem na uzyskanie dostępu do rejestrów jest skorzystanie z wstawek asemblerowych, ujmowanych w bloki __asm . Można o nich przeczytać w MSDN ; używając ich trzeba jednak mieć świadomość, w co się pakujemy :)

Pamięć operacyjna

Do sensownego funkcjonowania komputera potrzebne jest miejsce, w którym mógłby on składować kod wykonywanych przez siebie programów (obejmuje to także system operacyjny) oraz przetwarzane przez nie dane. Jest to stosunkowo spora ilość informacji, więc wymaga znacznie więcej miejsca niż to oferują rejestry procesora. Każdy komputer posiada więc osobną pamięć operacyjną , przeznaczoną na ten właśnie cel. Nazywamy ją często angielskim skrótem RAM (ang. random access memory - pamięć o dostępie bezpośrednim).

Skąd się bierze pamięć operacyjna?

Pamięć tego rodzaju utożsamiamy zwykle z jedną lub kilkoma elektronicznymi układami scalonymi (tzw. kośćmi), włożonymi w odpowiednie miejsca płyty głównej peceta.


Fotografia 2. Kilka kości RAM typu DIMM
(zdjęcie pochodzi z serwisu Tom's Hardware Guide )

Rzeczywiście jest to najważniejsza część tej pamięci (sama zwana jest czasem pamięcią fizyczną ), ale na pewno nie jedyna. Obecnie wiele podzespołów komputerowych posiada własne zasoby pamięci operacyjnej, przystosowane do wykonywania bardziej specyficznych zadań.

W szczególności dotyczy to kart graficznych i dźwiękowych, zoptymalizowanych do pracy z właściwymi im typami danych. Ilość pamięci, w jaką są wyposażane, systematycznie rośnie.

Pamięć wirtualna

Istnieje jeszcze jedno, przebogate źródło dodatkowej pamięci operacyjnej: jest nim dysk twardy komputera, a ściślej jego część zwana plikiem wymiany (ang. swap file ) lub plikiem stronnicowania (ang. paging file ).

Obszar ten służy systemowi operacyjnemu do "udawania", iż ma pokaźnie więcej pamięci niż posiada w rzeczywistości. Właśnie dlatego taką symulowaną pamięć nazywamy wirtualną .

Podobny zabieg jest niewątpliwie konieczny w środowisku wielozadaniowym, gdzie naraz może być uruchomionych wiele programów. Chociaż w danej chwili pracujemy tylko z jednym, to pozostałe mogą nadal działać w tle - nawet wówczas, gdy łączna ilość potrzebnej im pamięci znacznie przekracza fizyczne możliwości komputera.

Ceną za ponadplanowe miejsce jest naturalnie wydajność. Dysk twardy charakteryzuje się dłuższym czasem dostępu niż układy RAM, zatem wykorzystanie go jako pamięci operacyjnej musi pociągnąć za sobą spowolnienie działania systemu. Dzieje się jednak tylko wtedy, gdy uruchamiamy wiele aplikacji naraz.

Mechanizm pamięci wirtualnej, jako niemal niezbędny do działania każdego nowoczesnego systemu operacyjnego, funkcjonuje zazwyczaj bardzo dobrze. Można jednak poprawić jego osiągi, odpowiednio ustawiając pewne opcje pliku wymiany. Przede wszystkim warto umieścić go na nieużywanej zwykle partycji (Linux tworzy nawet sam odpowiednią partycję) i ustalić stały rozmiar na mniej więcej dwukrotność ilości posiadanej pamięci fizycznej.

Pamięć trwała

Przydatność komputerów nie wykraczałaby wiele poza zastosowania kalkulatorów, gdyby swego czasu nie wynaleziono sposobu na trwałe zachowywanie informacji między kolejnymi uruchomieniami maszyny. Tak narodziły się dyskietki, dyski twarde, zapisywalne płyty CD, przenośne nośniki "długopisowe" i inne media, służące do długotrwałego magazynowania danych.

Spośród nich na najwięcej uwagi zasługują dyski twarde, jako że obecnie są niezbędnym elementem każdego komputera. Zwane są czasem pamięcią trwałą (z wyjaśnionych wyżej względów) albo masową (z powodu ich dużej pojemności).

Możliwość zapisania dużego zbioru informacji jest aczkolwiek okupiona ślamazarnością działania. Odczytywanie i zapisywanie danych na dyskach magnetycznych trwa bowiem zdecydowanie dłużej niż odwołanie do komórki pamięci operacyjnej. Ich wykorzystanie ogranicza się więc z reguły do jednorazowego wczytywania dużych zestawów danych (na przykład całych plików) do pamięci operacyjnej, poczynienia dowolnej ilości zmian oraz powtórnego, trwałego zapisania. Wszelkie operacje np. na otwartych dokumentach są więc w zasadzie dokonywane na ich kopiach, rezydujących wewnątrz pamięci operacyjnej.

Nie zajmowaliśmy się jeszcze odczytem i zapisem informacji z plików na dysku przy pomocy kodu C++. Nie martw się jednak, gdyż ostatecznie poznamy nawet więcej niż jeden sposób na dokonanie tego. Pierwszy zdarzy się przy okazji omawiania strumieni, będących częścią Biblioteki Standardowej C++.

Organizacja pamięci operacyjnej

Spośród wszystkich trzech rodzajów pamięci, dla nas w kontekście wskaźników najważniejsza będzie pamięć operacyjna. Poznamy teraz jej budowę widzianą z koderskiego punktu widzenia.

Adresowanie pamięci

Wygodnie jest wyobrażać sobie pamięć operacyjną jako coś w rodzaju wielkiej tablicy bajtów. W takiej strukturze każdy element (zmiemy go komórką ) powinien dać się jednoznacznie identyfikować poprzez swój indeks. I tutaj rzeczywiście tak jest - numer danego bajta w pamięci nazywamy jego adresem .

W ten sposób dochodzimy też do pojęcia wskaźnika:

Wskaźnik (ang. pointer ) jest adresem pojedynczej komórki pamięci operacyjnej.

Jest to więc w istocie liczba, interpretowana jako unikalny indeks danego miejsca w pamięci. Specjalne znaczenie ma tu jedynie wartość zero, interpretowana jako wskaźnik pusty (ang. null pointer ), czyli nieodnoszący się do żadnej konkretnej komórki pamięci.

Wskaźniki służą więc jako łączą do określonych miejsc w pamięci operacyjnej; poprzez nie możemy odwoływać się do tychże miejsc. Będziemy również potrafili pobierać wskaźniki na zmienne oraz funkcje, zdefiniowane we własnych aplikacjach, i wykonywać przy ich pomocy różne wspaniałe rzeczy :)

Zanim jednak zajmiemy się bliżej samymi wskaźnikami w języku C++, poświęćmy nieco uwagi na to, w jaki sposób systemy operacyjne zajmują się organizacją i systematyzacją pamięci operacyjnej - czyli jej adresowaniem. Pomoże nam to lepiej zrozumieć działanie wskaźników.

Epoka niewygodnych segmentów

Dawno, dawno temu (co oznacza przełom lat 80. i 90. ubiegłego stulecia) większość programistów nie mogła być zbytnio zadowolona z metod, jakich musieli używać, by obsługiwać większe ilości pamięci operacyjnej. Była ona bowiem podzielona na tzw. segmenty , każdy o wielkości 64 kilobajtów.

Aby zidentyfikować konkretną komórkę należało więc podać aż dwie opisujące jej liczby: oczywiście numer segmentu, a także offset , czyli konkretny już indeks w ramach danego segmentu.


Schemat 31. Segmentowe adresowanie pamięci. Adres zaznaczonej komórki zapisywano zwykle jako 012A:0007, a więc oddzielając dwukropkiem numer segmentu i offset (oba zapisane w systemie szesnastkowym). Do ich przechowywania potrzebne były dwie liczby 16-bitowe.

Może nie wydaje się to wielką niedogodnością, ale naprawdę nią było. Przede wszystkim niemożliwe było operowanie na danych o rozmiarze większym niż owe 64 kB (a więc chociażby na długich napisach). Chodzi też o fakt, iż to programista musiał martwić się o rozmieszczenie kodu oraz danych pisanego programu w pamięci operacyjnej. Czas pokazał, że obowiązek ten z powodzeniem można przerzucić na kompilator - co zresztą wkrótce stało się możliwe.

Płaski model pamięci

Dzisiejsze systemy operacyjne mają znacznie wygodniejszy sposób organizacji pamięci RAM. Jest nim właśnie ów płaski model (ang. flat memory model ), likwidujący wiele mankamentów swego segmentowego poprzednika.

32-bitowe procesory pozwalają mianowicie, by cała pamięć była jednym segmentem . Taki segment może mieć rozmiar nawet 4 gigabajtów, więc z łatwością zmieszczą się w nim wszystkie fizyczne i wirtualne zasoby RAMu.

To jednakże nie wszystko. Otóż płaski model umożliwia zgrupowanie wszystkich dostępnych rodzajów pamięci operacyjnej (kości RAM, plik wymiany, pamięć karty graficznej, itp.) w jeden ciągły obszar, zwany przestrzenią adresową . Programista nie musi się przy tym martwić, do jakiego szczególnego typu pamięci odnosi się dany wskaźnik! Na poziomie języka programowania znikają bowiem wszelkie praktyczne różnice między nimi: oto mamy jeden, wielki segment całej pamięci operacyjnej i basta!


Schemat 32. Idea płaskiego modelu pamięci. Adresy składają się tu tylko z offsetów, przechowywanych jako liczby 32-bitowe. Mogą one odnosić się do jakiegokolwiek rzeczywistego rodzaju pamięci, na przykład do takich jak na ilustracji.

W Windows dodatkowo każdy proces (program) posiada swoją własną przestrzeń adresową, niedostępną dla innych. Wymiana danych może więc zachodzi jedynie poprzez dedykowane do tego mechanizmy. Będziemy o nich mówić, gdy już przejdziemy do programowania aplikacji okienkowych.

Przy takim modelu pamięci porównanie jej do ogromnej, jednowymiarowej tablicy staje się najzupełniej słuszne. Wskaźniki można sobie wtedy całkiem dobrze wyobrażać jako indeksy tej tablicy.

Stos i sterta

Na koniec wspomnimy sobie o dwóch ważnych dla programistów rejonach pamięci operacyjnych, a więc właśnie o stosie oraz stercie.

Czym jest stos?

Stos (ang. stack ) jest obszarem pamięci, który zostaje automatycznie przydzielony do wykorzystania dla programu.

Na stosie egzystują wszystkie zmienne zadeklarowane jawnie w kodzie (szczególne te lokalne w funkcjach), jest on także używany do przekazywania parametrów do funkcji.

Faktycznie więc można by w ogóle nie wiedzieć o jego istnieniu. Czasem jednak objawia się ono w dość nieprzyjemny sposób: poprzez błąd przepełnienia stosu (ang. stack overflow ). Występuje on zwykle wtedy, gdy nastąpi zbyt wiele wywołań funkcji.

O stercie

Reszta pamięci operacyjnej nosi oryginalną nazwę sterty .

Sterta (ang. heap ) to cała pamięć dostępna dla programu i mogąca być mu przydzielona do wykorzystania.

Czytając oba opisy (stosu i sterty) pewnie trudno jest wychwycić między nimi jakieś różnice, jednak w rzeczywistości są one całkiem spore.

Przede wszystkim, rozmiar stosu jest ustalany raz na zawsze podczas kompilacji programu i nie zmienia się w trakcie jego działania. Wszelkie dane, jakie są na nim przechowywane, muszą więc mieć stały rozmiar - jak na przykład skalarne zmienne, struktury czy też statyczne tablice.

Kontrolą pamięci sterty zajmuje się natomiast sam programista i dlatego może przyznać swojej aplikacji odpowiednią jej ilość w danej chwili, podczas działania programu . Jest to bardzo dobre rozwiązanie, kiedy konieczne jest przetwarzanie zbiorów informacji o zmiennym rozmiarze .

Terminy 'stos' i 'sterta' mają w programowaniu jeszcze jedno znaczenie. Tak mianowicie nazywają się dwie często wykorzystywane struktury danych. Omówimy je przy okazji poznawania Biblioteki Standardowej C++.

***

Na tym zakończymy ten krótki wykład o samej pamięci operacyjnej. Część tych wiadomości była niektórym pewnie doskonale znana, ale chyba każdy miał okazję dowiedzieć się czegoś nowego :)

Wiedza ta będzie nam teraz szczególnie przydatna, gdyż rozpoczynamy wreszcie zasadniczą część tego rozdziału, czyli omówienie wskaźników w języku C++: najpierw na zmienne, a potem wskaźników na funkcje.

| | | |
Copyright © 2006-2013 egrafik.pl | Kontakt | Reklama | Projekty domów
jocker