Koło informatyczne

Pamięć komputera

Pamięć komputerowa (ang. computer memory) jest urządzeniem cyfrowym służącym do przechowywania informacji w postaci bitów. Dzielimy ją na:
  • pamięć operacyjną (ang. operating memory) służy do przechowywania uruchomionych programów oraz przetwarzanych danych. Jest pamięcią szybką, o krótkim czasie dostępu do przechowywanej informacji. Pamięć operacyjna realizowana jest z układów półprzewodnikowych. Posiada relatywnie małą pojemność.
  • pamięć masową (ang. mass storage) służy do składowania programów oraz dużej ilości informacji. Pamięć masowa posiada dłuższy czas dostępu do przechowywanych danych w porównaniu z pamięcią operacyjną, lecz ma dużą pojemność (setki gigabajtów). Realizowana jest w postaci dysków twardych, stacji CD/DVD, dysków sieciowych (dostępnych poprzez sieć teleinformatyczną).

Pamięć RAM (ang. Random Access Memory pamięć o dostępie swobodnym) jest podstawowym składnikiem pamięci operacyjnej komputera. Termin RAM oznacza pamięć, z której informacja może być odczytywana w dowolnej kolejności bez względu na poprzednie odczyty czy zapisy. Termin RAM wprowadzono w celu odróżnienia pamięci o dostępie swobodnym od pamięci o dostępie sekwencyjnym (np. taśmowej, dyskowej itp.), popularnej na początku ery komputerowej.

Informacja przechowywana jest w pamięci RAM w postaci bitów umieszczanych w komórkach (ang memory cell), których mogą być miliardy. Aby komputer mógł uzyskiwać w prosty sposób dostęp do każdej komórki pamięci, zostały one ponumerowane. Numery komórek nazywamy adresami komórek pamięci (ang. memory cell address). Poniżej przedstawiamy fragment logicznej struktury pamięci (czyli tak, jak widzi swoją pamięć komputer):

 

Pamięć
Adres Zawartość komórki
0 11000110
1 00001111
2 11000011
3 11111110
4 00000001
5 11100111
... ...

 

Ze względów ekonomicznych poszczególne komórki pamięci przechowują grupę kilku bitów (najczęściej jest ich 8 czyli 1 bajt, ale rozmiar bitowy komórki pamięci zależy od architektury systemu komputerowego). Na przykład komórka o adresie 3 przechowuje 8 bitów o zawartości 11111110. Treść tej informacji uzależniona jest od interpretacji stanów bitów.

 

obrazek

 

Komputer steruje pamięcią przy pomocy trzech magistral (ang. bus). Magistrale zbudowane są z linii, którymi transmituje się sygnały. We współczesnych komputerach magistrale są cyfrowe, co oznacza, iż poszczególne linie przesyłają tylko sygnały dwustanowe, czyli bity. Widać z tego wyraźnie, iż komputery są maszynami binarnymi nie tylko ze względu na rodzaj przetwarzanych informacji, lecz również z powodu swojej wewnętrznej budowy mówimy, iż posiadają architekturę binarną (ang. binary architecture).

Magistrala adresowa (ang. address bus) przekazuje pamięci adres komórki, do której komputer chce uzyskać dostęp odczytać zawartość lub umieścić nowe dane. Ponieważ adres przekazywany jest magistralą cyfrową, to sam również występuje jako liczba binarna. Ilość linii na magistrali adresowej określa zakres dostępnych adresów, a zatem maksymalny rozmiar pamięci komputera. Do obliczeń stosujemy prosty wzór:

 

rozmiar pamięci = 2liczba linii na magistrali adresowej

 

Na przykład w starych komputerach magistrala adresowa mogła zawierać maksymalnie 16 linii. Zatem rozmiar możliwej do zaadresowania pamięci wynosił 216 = 65536 komórek (sławne 64KB kilo bajty). Jeśli magistrala adresowa składa się z 32 linii, to komputer jest w stanie wykorzystać 232 = 4294967296 = 4GB pamięci (GB gigabajt). Oczywiście w systemie może być mniej pamięci (np. tylko 1GB = 1073741824 komórek), w takim przypadku część adresów nie jest wykorzystywana, gdyż nie stoją za nimi żadne komórki. Ilość możliwych do zaadresowania komórek nosi nazwę przestrzeni adresowej (ang. address space). Natomiast pamięć fizyczna (ang. physical memory, physical storage) określa ilość pamięci rzeczywiście zainstalowanej w systemie komputerowym.

Magistrala danych (ang. data bus) umożliwia komputerowi przekazywanie danych do pamięci oraz odczyt przechowywanych przez pamięć informacji z komórek. Magistrala danych zbudowana jest z linii sygnałowych, po których przekazywane są bity. Ilość linii na magistrali danych zależy od architektury komputera. Na przykład w systemach 32-bitowych magistrala danych zawiera 32 linie, co pozwala w jednym cyklu dostępu do pamięci przesłać porcję 32 bitów.

 

Jeśli dokładnie czytałeś podane wyżej informacje, to zapewne zauważyłeś, iż pisaliśmy o pamięci zawierającej komórki 8 bitowe. Tutaj z kolei piszemy, że magistrala danych jest 32-bitowa. Jak pogodzić ze sobą te dwa fakty. Prześledźmy krótką historię rozwoju magistral danych.

 

Popularne w latach 80-tych ubiegłego wieku komputery 8-bitowe

obrazek obrazek obrazek
Sinclair ZX-Spectrum Commodore 64 Atari 800XL

 

Magistrala danych pierwszych popularnych komputerów domowych była 8 bitowa i odpowiadała dokładnie rozmiarowi komórki pamięci. Dane umieszczane na 8-bitowej magistrali trafiały bezpośrednio do zaadresowanej komórki. Również odczyt danych z dowolnej komórki był realizowany przy pomocy 8 bitowej magistrali. Stąd systemy takie często określa się dzisiaj mianem komputerów 8 bitowych. Magistrale 8 bitowe wciąż są w użyciu w świecie mikrokontrolerów małych komputerków, które w całości mieszczą się w pojedynczym układzie scalonym i sterują różnymi urządzeniami monitorami, radiami, telewizorami, aparatami fotograficznymi, pralkami, zegarkami, grami elektronicznymi itp.

 

Komputery 16-bitowe, rozpowszechnione pod koniec lat 80-tych ubiegłego wieku.

obrazek obrazek obrazek obrazek
Commodore Amiga 500 Atari 520ST IBM PC-AT Apple Macintosh

 

Kolejna generacja komputerów osobistych to maszyny z 16 bitową magistralą danych. Komórki pamięci zostały dalej 8-bitowe. Pamięć podzielono na dwa banki, które współpracowały z jedną połówką magistrali danych.

 

Połączenie banków pamięci z 16-bitową magistralą danych
Bank 1 Bank 0
d15 d14 d13 d12 d11 d10 d9 d8 d7 d6 d5 d4 d3 d2 d1 d0

 

Na przykład bank 0 podłączony był do linii d7...d0, czyli do młodszych 8 bitów magistrali danych. Poprzez te linie komputer komunikował się z komórkami pamięci zawartymi w banku 0. Z kolei drugi bank, bank 1, podłączony był do pozostałych 8 linii danych d15...d8.

 

Bank 1   Bank 0
Adres Zawartość   Adres Zawartość
1 01111110   0 11101000
3 11111111   2 11110000
5 00000000   4 11110001
7 10000000   6 00000001
...     ...  

 

Z punktu widzenia komputera komórki w banku 0 posiadały adresy parzyste 0, 2, 4, 6, ... Komórki w banku 1 posiadały adresy nieparzyste. Oba banki pamięci połączone były z tą samą magistralą adresową bez linii A0, która służyła do wyboru banku pamięci w przypadku danych 8-bitowych. Dzięki takiemu rozwiązaniu komputer mógł przesłać do lub pobrać z pamięci porcję 16 bitów (naraz dwie komórki), gdyż magistrala adresowa wybierała z obu pamięci komórki leżące w tym samym wierszu. Istnieje też pewna niedogodność. Jeśli dane 16-bitowe zostaną umieszczone pod nieparzystym adresem (tutaj w komórkach 5 i 6), to nie można ich pobrać w jednym cyklu odczytu pamięci, ponieważ znajdują się w dwóch różnych wierszach. Powoduje to spowolnienie działania programu przetwarzającego te dane komputer musi czytać pamięć dwa razy po 8 bitów, pomimo że jest maszyną 16-bitową!. Dlatego kompilatory języków programowania posiadają wbudowane odpowiednie mechanizmy umieszczania danych wielobajtowych pod właściwymi adresami, nawet jeśli prowadziłoby to do powstania dziur (niewykorzystanych komórek) w obszarze pamięci.

 

 32 bitowe komputery lat 90-tych ubiegłego wieku.

obrazek obrazek obrazek
Commodore Amiga 4000 Apple Macintosh LC-475 IBM PC 486

 

Rozwój komputeryzacji wymusił pojawienie się maszyn 32-bitowych. Pamięć komputera 32-bitowego wciąż zbudowana jest z komórek 8-bitowych. Zastosowano podobne rozwiązanie jak w systemach 16 bitowych podzielono pamięć na cztery banki 0, 1, 2 i 3. Każdy bank współpracuje z 8 liniami magistrali danych. Banki są podłączone do wspólnej magistrali adresowej z wyjątkiem linii A1 i A0, które sterują wybieraniem odpowiedniego banku (lub pary banków) w przypadku danych 8-bitowych (lub 16 bitowych).

 

Połączenie banków pamięci z 32-bitową magistralą danych
Bank 3 Bank 2 Bank 1 Bank 0
d31 d30 d29 d28 d27 d26 d25 d24 d23 d22 d21 d20 d19 d18 d17 d16 d15 d14 d13 d12 d11 d10 d9 d8 d7 d6 d5 d4 d3 d2 d1 d0

 

Poniżej przedstawiamy rozłożenie adresów komórek w poszczególnych bankach pamięci z punktu widzenia komputera.  Magistrala adresowa wybiera zawsze rząd 4 komórek, leżących pod tym samym adresem w każdym z banków. Dwa najmłodsze bity A1 i A0 adresują odpowiedni bank, a komputer odczytuje lub zapisuje dane wykorzystując linie magistrali danych połączone z wybranym bankiem (lub z wybranymi bankami).

 

Bank 3   Bank 2   Bank 1   Bank 0
Adres Zawartość   Adres Zawartość   Adres Zawartość   Adres Zawartość
3 00000000   2 11111111   1 11110000   0 00001111
7 11001100   6 10101010   5 01010101   4 11000011
11 11100111   10 10000001   9 01111110   8 11010011
15 11010110   14 00101100   13 00111010   12 11010100
19 11010010   18 00010100   17 00100100   16 11011110
      ...     ...     ...  

 

Aby wykorzystać maksymalnie potencjał systemu 32-bitowego dane 16 bitowe należy umieszczać pod adresami parzystymi (np. komórki 6-7 i 8-9), a dane 32 bitowe należy umieszczać pod adresami podzielnymi przez 4 (np. komórki 16-17-18-19). Wtedy komputer będzie miał do nich dostęp w jednym cyklu odczytu lub zapisu pamięci.

Zwróć uwagę na sposób przechowywania danych wielobajtowych w komórkach pamięci. Możliwe są dwa rozwiązania tzw. little-endian i big-endian. Wszystkie procesory Intel i kompatybilne stosują system little-endian, który polega na tym, iż w niższych adresach przechowuje się mniej znaczące bajty danych. Zatem dana 16-bitowa w little-endian zostanie umieszczona  w kolejnych dwóch komórkach jako b7...b0 w pierwszej komórce (o niższym adresie) i b15...b8 w drugiej komórce o adresie wyższym. Z danymi 32-bitowymi jest identycznie : najmłodszy bajt trafi do pierwszej komórki, a najstarszy do ostatniej. Porządek ten odzwierciedla nasz schemat rozmieszczenia bloków pamięci. W systemie big-endian (stosowanym w starszych komputerach Amiga, Macintosh oraz w niektórych systemach mainframe) jest na odwrót: pierwszy adres przechowuje starsze bity, następne adresy przechowują coraz młodsze bity danej.

 

Zmienne w pamięci

Zmienne są obiektami przechowywanymi w pamięci komputera. Posiadają zatem adresy. Adres zmiennej uzyskujemy za pomocą operatora adresu &. Dodatkowo zmienne zajmują określoną liczbę komórek pamięci. Rozmiar zmiennej podaje nam operator sizeof. Przekopiuj lub przepisz poniższy program do edytora kodu w pakiecie Code::Blocks (druga opcja jest bardziej wskazana, gdyż, pisząc, więcej zapamiętasz).

 

// Koło Informatyczne I LO w Tarnowie
// (C)2015 mgr Jerzy Wałaszek
// Adresy i rozmiary zmiennych
//-----------------------------------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
  char a;
  bool b;
  short int c;
  int d;
  long long e;
  float f;
  double g;
  long double h;

  cout << "a" << setw(3) << sizeof(a) << setw(10) << (void *)&a << endl;
  cout << "b" << setw(3) << sizeof(b) << setw(10) << &b << endl;
  cout << "c" << setw(3) << sizeof(c) << setw(10) << &c << endl;
  cout << "d" << setw(3) << sizeof(d) << setw(10) << &d << endl;
  cout << "e" << setw(3) << sizeof(e) << setw(10) << &e << endl;
  cout << "f" << setw(3) << sizeof(f) << setw(10) << &f << endl;
  cout << "g" << setw(3) << sizeof(g) << setw(10) << &g << endl;
  cout << "h" << setw(3) << sizeof(h) << setw(10) << &h << endl;

  return 0;
}

 

Uwagi: strumień cout traktuje adres zmiennej znakowej jako ciąg znaków. Musimy stosować jawne rzutowanie na typ np. void (spróbuj usunąć to rzutowanie i zobacz, co się stanie).

Kolejna ciekawa rzecz, to sposób rozmieszczenia zmiennych w pamięci (otrzymane adresy zależą od komputera i kompilatora, na którym uruchomiono program):

 

a  1  0x7ffc03514364
b  1  0x7ffc03514365
c  2  0x7ffc03514366
d  4  0x7ffc03514368
e  8  0x7ffc03514370
f  4  0x7ffc0351436c
g  8  0x7ffc03514378
h 16  0x7ffc03514380

 

Zmienne umieszczane są kolejno w kierunku końca pamięci – ostatnia zmienna h  znajduje się pod najwyższym  adresem.

Struktury

Struktura definiuje złożony typ danych, który może w sobie zawierać inne typy. Strukturę w języku C++ definiujemy przed pierwszym użyciem w programie w sposób następujący:

 

struct nazwa
{
  typ pole_1;
  typ pole_2;
  ...
  typ pole_n;
};

 

Nazwa struktury budowana jest wg tych samych zasad co nazwy zmiennych. Wewnątrz struktury definiujemy tzw. pola, które określają rodzaj danych przechowywanych przez strukturę. Nazwy pól również tworzymy wg zasad podanych dla nazw zmiennych. Po zdefiniowaniu struktury jej nazwa staje się nowym typem danych, który służy do tworzenia zmiennych o typie struktury.

Na przykład, poniższa definicja tworzy nowy typ danych point_xy:

 

struct point_xy
{
  double x,y;
};

 

Struktura typu point_xy zawiera dwa pola o nazwach x i y. W każdym z tych pól można przechowywać daną typu double, czyli np. współrzędną punktu.

Na podstawie typu point_xy (który jest jakby planem budowy zmiennej) tworzymy nowe zmienne:

 

point_xy a,b;

 

W każdej z nich znajdują się dwa pola x i y. Dostęp do zawartości tych pól uzyskujemy za pomocą operatora kropki:

 

a.x = 1;   // Do pola x zmiennej a trafi 1
a.y = 3.7; // Do pola y zmiennej a trafi 3,7
b.x = a.x + 2.2; // Do pola x zmiennej b trafi 3,2
b.y = b.x - a.y; // Do pola y zmiennej b trafi -0,5

 

Poniższy program oblicza odległość dwóch punktów na płaszczyźnie. Korzystamy ze wzoru:

 

obrazek

 

// Koło Informatyczne I LO w Tarnowie
// (C)2015 mgr Jerzy Wałaszek
// Struktury
//-----------------------------------

#include <iostream>
#include <cmath>

using namespace std;

// Definiujemy strukturę
struct point_xy
{
    double x,y;
};

int main()
{
  point_xy a,b;  // Dwa punkty
  double L,x2,y2;

  cout << "Obliczanie odległości dwóch punktów\n"
          "-----------------------------------\n\n";
  cout << "PUNKT A:\n";
  cout << "x = "; cin >> a.x;
  cout << "y = "; cin >> a.y;
  cout << endl;

  cout << "PUNKT B:\n";
  cout << "x = "; cin >> b.x;
  cout << "y = "; cin >> b.y;
  cout << endl;
  
  x2 = a.x - b.x;
  y2 = a.y - b.y;
  x2 *= x2;
  y2 *= y2;
  L = sqrt(x2 + y2);
  
  cout << "L = " << L << endl;
  
  return 0;
}

 

Deklarowanie tablic

Tablica (ang. array) lub wektor (ang. vector) jest złożoną strukturą danych (ang. compound data structure) zbudowaną z ciągu elementów tego samego typu. W pamięci komputera elementy tablicy są ułożone kolejno jeden obok drugiego. Dostęp do elementu odbywa się poprzez numer zwany indeksem. Na podstawie indeksu, rozmiaru elementu oraz adresu początku tablicy komputer oblicza adres elementu i w ten sposób uzyskujemy do niego dostęp.

We współczesnych językach programowania tablice są stosowane powszechnie do przechowywania danych podobnego rodzaju. Przy ich pomocy można zapisywać ciągi liczbowe, wyniki pomiarów różnych wielkości oraz tworzyć złożone bazy danych. Liczba zastosowań tablic jest w zasadzie ograniczona naszą wyobraźnią. Podstawową zaletą tablic jest prostota przetwarzania ich elementów. Dzięki dostępowi poprzez indeksy, elementy tablic daje się łatwo przetwarzać w pętlach iteracyjnych.

Przed pierwszym użyciem każda tablica musi być zadeklarowana tak jak wszystkie zmienne używane w programie tablica jest zmienną złożoną. Poniżej podajemy sposoby deklaracji tablicy w wybranych przez nas językach programowania:

 

Deklarację tablicy umieszczamy w języku C++ na liście deklaracji zmiennych. Składnia jest następująca:

 

typ_danych nazwa_tablicy[liczba_elementów];
typ_danych  –  określa rodzaj informacji przechowywanych przez deklarowane zmienne
nazwa_tablicy   tworzona jest wg zwykłych reguł tworzenia nazw zmiennych w języku C++
Liczba_elementów   określa, ile elementów danego typu przechowuje tablica

 

Poniżej podajemy kilka przykładów deklaracji tablic w C++:

  ...
  int    a[3];  // tablica zawierająca 3 elementy typu int
  double x[10]; // tablica przechowująca 10 liczb typu double
  char   c[6];  // tablica przechowująca 6 wartości znakowych
  ...

W języku C++ indeksy tablic rozpoczynają się od 0. Ma to sens, ponieważ nazwa tablicy jest traktowana zawsze jak adres początku obszaru pamięci, w którym tablica przechowuje swoje elementy. Naturalne zatem jest, iż pierwszy element leży właśnie pod adresem tablicy. Stąd jego indeks wynosi 0, czyli nic nie musimy dodawać do adresu początku tablicy, aby uzyskać dostęp do jej pierwszego elementu.

W powyższym przykładzie zadeklarowano trzy tablice a, x  oraz c. Posiadają one elementy o następujących indeksach:

 

Tablica a  : a[0] a[1] a[2] 3 elementy typu integer
Tablica x  : x[0] x[1] x[2] x[3] x[4] x[5] x[6] x[7] x[8] x[9] 10 elementów typu double
Tablica c  : c[0] c[1] c[2] c[3] c[4] c[5] 6 elementów typu char

 

Zwróć uwagę, iż tablica nie posiada elementu o indeksie równym ilości elementów. Zatem jeśli zadeklarujemy np. tablicę:

 

double Tlk[168];

 

to jej ostatnim elementem jest Tlk[167], a nie Tlk[168]. Odwołanie się w programie do Tlk[168] jest błędem, którego kompilator zwykle nie zgłosi, zakładając, iż programista wie co robi. Niestety, język C++ nie był tworzony z myślą o początkujących.

 

Inicjalizacja tablic

Często zdarza się, iż chcemy utworzyć tablicę z zadaną z góry zawartością (np. tablica zawierająca początkowe liczby pierwsze). Postępujemy wtedy w sposób następujący:

 

typ_elementów nazwa_tablicy[ ] = {lista_wartości_dla_kolejnych_elementów};

 

Zwróć uwagę, iż nie musimy podawać liczby elementów. Kompilator utworzy tyle elementów, ile podamy dla nich wartości na liście inicjalizacyjnej.  Poniższy przykład tworzy tablicę 10 liczb całkowitych i wypełnia ją kolejnymi liczbami Fibonacciego.

 

...
int fib[ ] = (0,1,1,2,3,5,8,13,21,34);
...
 

Przykład

Elementami tablicy nie muszą być typy proste. Poniższy przykład tworzy tablicę struktur i wypełnia ją przykładowymi wartościami.

 

// Koło Informatyczne I LO w Tarnowie
// (C)2015 mgr Jerzy Wałaszek
// Struktury
//-----------------------------------

#include <iostream>
#include <cmath>

using namespace std;

// Definiujemy strukturę
struct sqrt_n
{
    unsigned n;
    double   sqrt_of_n;
};

const unsigned N = 100;  // Liczba elementów
int main()
{
  sqrt_n a[N];   // Tablica N struktur typu sqrt_n
  int i;

  for(i = 0; i < N; i++)
  {
    a[i].n = 2 * (i+1);
    a[i].sqrt_of_n = sqrt(a[i].n);
  }

  for(i = 0; i < N; i++)
    cout << a[i].n << " ---> " << a[i].sqrt_of_n << endl;

  return 0;
}

 

Tablice dynamiczne

Zdarza się, iż w trakcie pisania programu nie wiemy, ile dokładnie elementów będzie zawierała używana w tym programie tablica. W takim przypadku problem tworzenia tablicy możemy rozwiązać na dwa sposoby:

  1. Utworzyć tablicę o maksymalnej, przewidywanej liczbie elementów. Rozwiązanie nieefektywne ze względu na wykorzystanie pamięci. Jeśli w typowych przypadkach wykorzystujemy małą liczbę elementów tablicy, to i tak musimy rezerwować założoną ilość komórek dla przypadku pesymistycznego, który pojawia się bardzo rzadko, ale jest prawdopodobny.
  2. Utworzyć tablicę dynamicznie o tylu komórkach, ile w danej chwili jest nam potrzebne. Po wykorzystaniu, tablicę dynamiczną usuwamy, zwalniając w ten sposób zajmowany przez nią obszar pamięci, który teraz można wykorzystać do innych celów np. dla nowej tablicy dynamicznej.

W celu utworzenia w języku C++ tablicy dynamicznej, tworzymy zmienną wskaźnikową na typ danych, które mają być przechowywane w tablicy:

 

typ_elementów * nazwa_tablicy_dynamicznej;

 

Zmienna wskaźnikowa (ang. pointer variable) nie przechowuje danych tylko adres obszaru pamięci komputera, w którym te dane się znajdują. Deklarację zmiennej wskaźnikowej zawsze poprzedzamy znakiem gwiazdki. W poniższym przykładzie tworzymy trzy wskaźniki a, b i c do danych typu double (czyli do obszaru pamięci, w którym będą przechowywane liczby zmiennoprzecinkowe o podwójnej precyzji):

 

...
double * a, * b, * c;
...
 

Pamięć rezerwujemy operatorem new i adres zarezerwowanego obszaru umieszczamy w zmiennej wskaźnikowej:

 

nazwa_tablicy_dynamicznej  = new typ_elementów[liczba_elementów];

 

Poniższy przykład tworzy trzy tablice dynamiczne, w których będzie można przechowywać odpowiednio 10, 100 i 1000 elementów typu double:

 

...
a = new double[10];    // elementy od a[0] do a[9]
b = new double[100];   // elementy od b[0] do b[99]
c = new double[1000];  // elementy od c[0] do c[999]
...
 

Po tej operacji do elementów tablic a, b  i c  odwołujemy się w zwykły sposób za pomocą indeksów. Istnieje również alternatywna metoda, wykorzystująca fakt, iż zmienne a, b  i c  są wskaźnikami. W języku C++ dodanie do wskaźnika liczby całkowitej powoduje obliczenie adresu elementu o indeksie równym dodawanej liczbie. Zatem wynik takiej operacji jest również wskaźnikiem:

 

Tablica Wskaźnik
a[2] = 10.54;
cout << a[2] << endl;
* (a + 2) = 10.54;
cout << * (a + 2) << endl;

 

W rzeczywistości zapis a[i] kompilator i tak przekształca sobie na zapis * (a  + i). Forma tablicowa jest tylko uproszczeniem zapisu wskaźnikowego.

Tablice dynamiczne nie są automatycznie usuwane z pamięci, jeśli utworzono je w funkcji. Dlatego po zakończeniu korzystania z tablicy program powinien zwolnić zajmowaną przez tablicę pamięć. Dokonujemy tego poleceniem delete w sposób następujący:

 

delete [ ] nazwa_tablicy_dynamicznej;

 

W poniższym przykładzie zwalniamy pamięć zarezerwowaną wcześniej na elementy tablic b i c.

 

...
delete [ ] b;  // usuwamy obszar wskazywany przez b
delete [ ] c;  // usuwamy obszar wskazywany przez c
...
 

Należy również wspomnieć, iż Code::Blocks dopuszcza konstrukcję:

 

typ_elementów  nazwa_tablicy[zmienna];

 

co pozwala na tworzenie statycznych tablic o liczbie elementów podanej w zmiennej. Na przykład poniższa konstrukcja programowa tworzy statyczną tablicę a  o liczbie elementów odczytanej ze strumienia wejściowego konsoli znakowej:

 

...
int n;
cin >> n;
double a[n];
...
 

Jednakże nie jest to zbyt standardowe rozwiązanie i może nie być przenośne na inne kompilatory C++, dlatego odradzam używania go lepiej zastosować tablicę dynamiczną.

 

Wprowadzanie/wyprowadzanie danych

Dane dla programu zwykle muszą być odczytywane ze zewnętrznego źródła konsoli lub pliku. W takim przypadku nie wiemy z góry (tzn. w trakcie pisania programu) ile ich będzie. Narzucającym się rozwiązaniem jest zastosowanie tablic dynamicznych. Ze źródła danych odczytujemy rozmiar tablicy, tworzymy tablicę dynamiczną o odpowiednim rozmiarze, a następnie wczytujemy do jej komórek poszczególne dane.

Poniżej podajemy sposoby odczytu zawartości tablicy z konsoli. Sposób ten jest bardzo ogólny. Wykorzystanie standardowego wejścia jako źródła danych daje nam kilka możliwości wprowadzania danych:

  1. Dane podajemy bezpośrednio z klawiatury. Sposób skuteczny i prosty dla niedużego zbioru danych. Jednakże przy większej ich liczbie staje się bardzo uciążliwy.
  2. Skopiowanie danych poprzez schowek. Procedura postępowania jest następująca:

    tworzymy w notatniku Leafpad (aplikacja zawsze pod ręką) odpowiedni zbiór danych
    zbiór kopiujemy do schowka (zaznaczamy całość Ctrl-A i naciskamy Ctrl-C)
    uruchamiamy program
    klikamy prawym przyciskiem myszki w pasek tytułowy okna konsoli
    z menu kontekstowego wybieramy polecenie EdytujWklej
    gotowe

  3. Przekierowanie standardowego wejścia z konsoli na plik na dysku. W tym przypadku program będzie pobierał dane z pliku, a nie z klawiatury. Aby to uzyskać uruchamiamy program w oknie konsoli następująco:
    ./nazwa_programu  < nazwa_pliku_wejściowego

    Na przykład nasz program nazywa się szukaj, a plik nosi nazwę dane.txt. Odpowiednie polecenie odczytu danych z pliku przez nasz program wygląda następująco:

    ./szukaj < dane.txt

    To rozwiązanie umożliwia również zapis danych wynikowych nie na ekran konsoli, lecz do pliku na dysku. W tym celu wystarczy wydać polecenie:

    ./nazwa_programu > nazwa_pliku_wynikowego


    Wejście i wyjście można przekierować w jednym poleceniu. Np. nasz program szukaj może odczytać dane wejściowe z pliku dane.txt, a wyniki swojej pracy umieścić w pliku wyniki.txt. W tym celu wydajemy takie oto polecenie:

    ./szukaj < dane.txt > wyniki.txt

 

Program

Program z pierwszego wiersza odczytuje liczbę n  określającą ilość danych. Z następnych n  wierszy odczytywane są dane i umieszczane w tablicy dynamicznej. Odczytane dane zostają następnie wyświetlone jedna obok drugiej. Wypróbuj z tym programem podane powyżej trzy opcje dostarczania danych i wyprowadzania wyników.

 

// Odczyt danych
// (C)2015 mgr Jerzy Wałaszek
//---------------------------

#include <iostream>

using namespace std;

int main()
{
  int * T,n,i;

  cin >> n;
  
  T = new int[n];
  
  for(i = 0; i < n; i++) cin >> T[i];

  cout << endl;

  for(i = 0; i < n; i++) cout << T[i] << " ";

  cout << endl << endl;

  delete [] T;

  return 0;
}
Wynik
6
12
34
28
65
121
83

12 34 28 65 121 83

 

Wypełnianie tablicy

Zdarza się, że algorytm musi wstępnie wypełnić tablicę określoną zawartością. Operację taką przeprowadza się w pętli iteracyjnej, której zmienna licznikowa przebiega przez wszystkie kolejne indeksy elementów. Następnie wykorzystuje się zmienną licznikową jako indeks elementu tablicy, w którym umieszczamy określoną zawartość.

W poniższych przykładach zakładamy, iż w programie zadeklarowano tablicę T o 100 elementach typu integer. Indeksy elementów tablicy T są w zakresie od 0 do 99.


Wypełnianie stałą zawartością x

...
for(i = 0; i < 100; i++) T[i] = x;
...

 

Wypełnianie liczbami parzystymi począwszy od 2

...
for(i = 0; i < 100; i++)
  T[i] = (i + 1) << 1;
...

 

Wypełnianie liczbami nieparzystymi począwszy od 1

...
for(i = 0; i < 100; i++)
  T[i] = 1 + (i << 1);
...

 

Wypełnianie liczbami pseudolosowymi z przedziału <a,b>

#include <cstdlib>
#include <time.h>

...
srand((unsigned)time(NULL));
...
for(i = 0; i < 100; i++)
  T[i] = a + rand() % (b - a + 1);
...

 


   I Liceum Ogólnokształcące   
im. Kazimierza Brodzińskiego
w Tarnowie

©2024 mgr Jerzy Wałaszek

Dokument ten rozpowszechniany jest zgodnie z zasadami licencji
GNU Free Documentation License.

Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl

W artykułach serwisu są używane cookies. Jeśli nie chcesz ich otrzymywać,
zablokuj je w swojej przeglądarce.
Informacje dodatkowe