Pomoce:

Instalacja Code::Blocks
Języki wysokiego poziomu
Struktura programu w języku C++
Zmienne i strumienie
Sterowanie wykonaniem programu

Tablice w C++

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.

 

Deklarowanie tablic

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,33);
...

 

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 Windows (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.exe, 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


    Jeśli często korzystasz z takich opcji uruchamiania programu, to zamiast wpisywać polecenie z klawiatury, można stworzyć sobie prosty plik wsadowy (ang. batch file), w którym umieszczamy niezbędne polecenia. Plikowi można nadać prostą nazwę, np. !.cmd. Wtedy w celu uruchomienia zawartych w nim poleceń wystarczy wpisać !. Oczywiście plik wsadowy należy umieścić w katalogu projektowym, ale to chyba już wiesz. Poniżej podaję przykładową zawartość takiego pliku wsadowego:

    @echo off
    cls
    echo DANE WEJSCIOWE:
    echo.
    type dane.txt
    echo.
    prg < dane.txt > wyniki.txt
    echo WYNIKI:
    echo.
    type wyniki.txt
    echo.

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
// Data:   25.04.2008
// (C)2012 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);
...

 

Klasa vector

Język C++ wyposażony jest w bibliotekę STL (ang. Standard Template Library - Biblioteka Standardowych Szablonów). Zawiera ona wiele pożytecznych klas, które możemy wykorzystywać w swoich programach. Jedną z nich jest klasa vector definiująca tablicę dynamiczną. Jednakże w przeciwieństwie do zwykłych tablic dynamicznych klasa vector pozwala tablicę dynamicznie powiększać w czasie działania programu - nie musimy zatem rezerwować wyliczonej wcześniej liczby komórek.

Aby użyć klasy vector w programie , należy dołączyć plik nagłówkowy:

 

#include <vector>

 

Tablicę typu vector możemy w programie utworzyć na kilka różnych sposobów. Oto trzy z nich:

 

  1. vector<typ_elementów> nazwa_tablicy;
    Tworzona jest tablica bez elementów - zostaną one dodane później w trakcie wykonywania programu.
  2. vector<typ_elementów> nazwa_tablicy(liczba_elementów, wartość_elementu);
    Tworzona jest tablica o zadanej liczbie elementów. Każdy element otrzymuje zadaną wartość.
  3. vector<typ_elementów> nazwa_tablicy(inna_tablica);
    Tworzona jest tablica będąca kopią innej tablicy.

 

Dostęp do elementów tablicy odbywa się na dwa sposoby za pomocą indeksu:

 

#include <iostream>
#include <vector>

using namespace std;

int main()
{
  vector<int> T(10,5);  // tworzymy tablicę 10 liczb całkowitych
  int i;

  for(i = 0; i < 10; i++) cout << T[i] << endl;

  return 0;
}


#include <iostream>
#include <vector>

using namespace std;

int main()
{
  vector<int> T(10,5);  // tworzymy tablicę 10 liczb całkowitych
  int i;

  for(i = 0; i < 10; i++) cout << T[i] << endl;

  return 0;
}

Różnica pomiędzy T[i] a T.at(i) jest taka, iż funkcja składowa at() sprawdza, czy element o indeksie i  leży wewnątrz tablicy. Jeśli nie, to zostaje zgłoszony wyjątek.

Dane w tablicy możemy umieszczać na kilka sposobów:

 

#include <iostream>
#include <vector>

using namespace std;

int main()
{
  vector<int> T(10,5);  // tworzymy tablicę 10 liczb całkowitych
  unsigned int i;

  T[5]    = 25;
  T.at(6) = 315;
  T.push_back(1999);

  for(i = 0; i < T.size(); i++) cout << T.at(i) << endl;

  return 0;
}

 

T[indeks] = wartość;    komórka o podanym indeksie przyjmuje nową wartość. Indeks nie jest sprawdzany.

T.at(indeks) = wartość; jak wyżej, jednakże indeks jest sprawdzany, czy wskazuje komórkę w tablicy. Jeśli nie, powstanie wyjątek.

T.push_back(wartość);   na końcu tablicy zostaje dopisany nowy element o podanej wartości. Liczba komórek w tablicy zostaje zwiększona.

 

Ponieważ rozmiar tablicy T może się dynamicznie zmieniać, to aktualną liczbę komórek poda nam funkcja składowa T.size().

 

Kolejny przykład pokazuje, jak można stworzyć prosty stos:

 

#include <iostream>
#include <vector>

using namespace std;

int main()
{
  vector<int> T;  // tworzymy pustą tablicę
  int i;

  for(i = 1; i < 16; i++) T.push_back(i);

  while(!T.empty())
  {
     cout << T.back() << endl;
     T.pop_back();
  }

  return 0;
}

 

W pętli dopisujemy do końca tablicy kolejne liczby naturalne od 1 do 15. W drugiej pętli sprawdzamy za pomocą funkcji składowej T.empty(), czy tablica jest pusta. Jeśli nie, to to wyświetlamy w oknie konsoli ostatni element - funkcja składowa T.back(), a następnie element ten usuwamy z tablicy za pomocą funkcji składowej T.pop_back().

 

Z biblioteką STL wiąże się pojęcie iteratora. Iterator jest klasą wskaźnika, który wskazuje elementy np. tablicy. Zmienną typu iterator dla tablic vector tworzymy następująco:

 

vector<typ_elementów>::iterator nazwa iteratora;

 

Do obsługi iteratorów w klasie vector mamy dwie funkcje składowe:

 

begin() - zwraca iterator na pierwszy element tablicy
end()   - zwraca iterator wskazujący poza ostatni element tablicy

 

Poniższy przykład pokazuje wykorzystanie iteratorów do przeglądania tablicy.

 

#include <iostream>
#include <vector>

using namespace std;

int main()
{
  vector<int> T;            // tworzymy pustą tablicę
  vector<int>::iterator it; // tworzymy iterator
  int i;

  for(i = 1; i < 10; i++) T.push_back(i*i); // wypełniamy tablicę

  for(it = T.begin(); it != T.end(); it++)  // przeglądamy tablicę
    cout << *it << endl;                    // *it jest elementem tablicy

  return 0;
}

 

Iteratory wykorzystuje się w operacjach usuwania elementów z tablicy.

 

#include <iostream>
#include <vector>

using namespace std;

int main()
{
  vector<int> T;            // tworzymy pustą tablicę
  vector<int>::iterator it; // tworzymy iterator
  unsigned int i;

  for(i = 0; i < 10; i++) T.push_back(i*10); // wypełniamy tablicę liczbami

  it = T.begin();     // ustawiamy iterator na pierwszy element tablicy
  T.erase(it+5);      // usuwamy z tablicy element o indeksie 5
  for(i = 0; i < T.size(); i++) cout << T[i] << endl;

  return 0;
}

 

Drugi przykład pokazuje sposób usuwania ciągu kolejnych elementów:

 

#include <iostream>
#include <vector>

using namespace std;

int main()
{
  vector<int> T;            // tworzymy pustą tablicę
  vector<int>::iterator it; // tworzymy iterator
  unsigned int i;

  for(i = 0; i < 10; i++) T.push_back(i*10); // wypełniamy tablicę liczbami

  it = T.begin();     // ustawiamy iterator na pierwszy element tablicy
  T.erase(it+2,it+5); // usuwamy z tablicy elementy o indeksach od 2 do 4
  for(i = 0; i < T.size(); i++) cout << T[i] << endl;

  return 0;
}

 

Iteratory można również wykorzystywać do wstawiania nowych elementów do tablicy. Wstawianie polega na tym, iż elementy tablicy od pozycji iteratora zostają przesunięte w górę, a w powstałe w ten sposób miejsce jest wstawiany nowy element.

 

#include <iostream>
#include <vector>

using namespace std;

int main()
{
  vector<int> T;            // tworzymy pustą tablicę
  vector<int>::iterator it; // tworzymy iterator
  unsigned int i;

  for(i = 0; i < 10; i++) T.push_back(i); // wypełniamy tablicę liczbami

  it = T.begin()+5;      // ustawiamy iterator na element o indeksie 5
  it = T.insert(it,100); // do tablicy wstawiamy 100
  for(i = 0; i < T.size(); i++) cout << T[i] << endl;

  return 0;
}

 

Zwróć uwagę na instrukcję:

 

it = T.insert(it,100);

 

W naszym programie nie ma znaczenia, co się stanie dalej z iteratorem it, ponieważ po wstawieniu nowego elementu już z niego nie korzystamy. Funkcja składowa insert() w przypadku wstawiania jednego elementu zwraca iterator do pozycji, na której ten element został wstawiony. Jest to bardzo istotne, gdy chcesz dalej korzystać z iteratora, np. do wstawiania kolejnych elementów, ponieważ adres tablicy może ulec zmianie w pamięci. Wstawienie elementu powoduje wzrost obszaru pamięci zajmowanego przez tablicę. Jeśli przekroczy on zarezerwowaną wartość, to zostaje utworzony nowy, większy obszar i cała zawartość tablicy jest kopiowana do tego nowego obszaru. Stary obszar jest zwracany do puli systemowej. Powoduje to zmianę adresów wszystkich komórek tablicy i unieważnienie twojego iteratora - przestaje on wskazywać cokolwiek. Jeśli jednak odświeżymy jego zawartość wynikiem zwracanym przez insert(), to iterator pozostanie aktualny nawet po zmianie adresu tablicy. Pamiętaj o tym, aby uniknąć w przyszłości niespodzianek!

 

Kolejny przykład pokazuje wstawianie do tablicy wielu komórek o zadanej wartości. W tym przypadku iterator staje się nieaktualny i należy go bezwzględnie odświeżyć za pomocą funkcji składowej begin() - insert() dla wielu wstawień nic nie zwraca!

 

#include <iostream>
#include <vector>

using namespace std;

int main()
{
  vector<int> T;            // tworzymy pustą tablicę
  vector<int>::iterator it; // tworzymy iterator
  unsigned int i;

  for(i = 0; i < 10; i++) T.push_back(i); // wypełniamy tablicę liczbami

  it = T.begin()+5;   // ustawiamy iterator na element o indeksie 5
  T.insert(it,5,100); // do tablicy wstawiamy 5 wartości 100
  for(it = T.begin(); it != T.end(); it++) cout << *it << endl;

  return 0;
}

 

Podsumowanie

#include <vector>  -  należy wstawić na początku programu, aby uzyskać dostęp do klasy vector.
vector<typ> nazwa;  - tworzy pustą tablicę
vector<typ> nazwa(n,m);  - tworzy tablicę n  komórek o wartości m
vector<typ> nazwa(nazwa);  - tworzy tablicę, która jest kopią innej tablicy
nazwa.size()  - podaje liczbę komórek w tablicy
nazwa.empty()  - zwraca true, jeśli tablica jest pusta
nazwa[i]  - element o indeksie i
nazwa.at(i)  - element o indeksie i  ze sprawdzaniem przynależności do tablicy
nazwa.push_back(m)  - dodaje na końcu tablicy element m
nazwa.front()  - pierwszy element tablicy
nazwa.back()  - ostatni element tablicy
nazwa.pop_back()  - usuwa ostatni element tablicy
vector<typ>::iterator it;  - tworzy iterator do elementów tablicy
nazwa.begin()  - zwraca iterator do pierwszego elementu tablicy
nazwa.end()  - zwraca iterator wskazujący poza ostatni element tablicy
nazwa.erase(it)  - usuwa element na pozycji iteratora it
nazwa.erase(it1,it2)  - usuwa wszystkie elementy od pozycji iteratora it1 do pozycji tuż przed iteratorem it2 - element na pozycji iteratora it2 pozostaje w tablicy
nazwa.insert(it,m)  - wstawia m  na pozycji wskazywanej przez iterator it. Zwraca iterator do wstawionego elementu
nazwa.insert(it,n,m)  - wstawia od pozycji iteratora it n  elementów o wartości m. Iterator traci ważność.
nazwa.clear()  - usuwa wszystkie elementy z tablicy

 


   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