Dlaczego nie powinienem dołączać plików cpp i zamiast tego używać nagłówka?


147

Skończyłem więc pierwsze zadanie z programowania w C ++ i otrzymałem ocenę. Ale zgodnie z oceną straciłem oceny including cpp files instead of compiling and linking them. Nie bardzo wiem, co to oznacza.

Patrząc wstecz na mój kod, zdecydowałem się nie tworzyć plików nagłówkowych dla moich klas, ale zrobiłem wszystko w plikach cpp (wydawało się, że działa dobrze bez plików nagłówkowych ...). Domyślam się, że oceniający miał na myśli, że napisałem '#include "mycppfile.cpp";' w niektórych moich plikach.

Moje uzasadnienie dla #includeplików cpp było następujące: - Wszystko, co miało trafić do pliku nagłówkowego, znajdowało się w moim pliku cpp, więc udawałem, że jest jak plik nagłówkowy - W stylu monkey-see-monkey do, widziałem to drugie pliki nagłówkowe były #includew plikach, więc zrobiłem to samo dla mojego pliku cpp.

Więc co dokładnie zrobiłem źle i dlaczego jest to złe?


36
To naprawdę dobre pytanie. Spodziewam się, że pomoże to wielu początkującym w c ++.
Mia Clarke

Odpowiedzi:


174

O ile wiem, standard C ++ nie zna różnicy między plikami nagłówkowymi a plikami źródłowymi. Jeśli chodzi o język, każdy plik tekstowy z kodem prawnym jest taki sam jak każdy inny. Jednak, chociaż nie jest to nielegalne, włączenie plików źródłowych do programu prawie całkowicie wyeliminuje wszelkie korzyści, które miałbyś z oddzielenia plików źródłowych w pierwszej kolejności.

Zasadniczo #includemówi preprocesorowi, aby wziął cały określony plik i skopiował go do aktywnego pliku, zanim kompilator dostanie go w swoje ręce. Kiedy więc włączysz razem wszystkie pliki źródłowe do projektu, zasadniczo nie ma różnicy między tym, co zrobiłeś, a po prostu utworzeniem jednego ogromnego pliku źródłowego bez jakiejkolwiek separacji.

„Och, to nic wielkiego. Jeśli działa, to w porządku” , słyszę twój płacz. I w pewnym sensie miałbyś rację. Ale teraz masz do czynienia z malutkim, małym programem i ładnym i stosunkowo nieobciążonym procesorem, który skompiluje go za Ciebie. Nie zawsze będziesz miał tyle szczęścia.

Jeśli kiedykolwiek zagłębisz się w sferę poważnego programowania komputerowego, zobaczysz projekty z liczbą linii, która może sięgać milionów, a nie dziesiątek. To dużo linii. A jeśli spróbujesz skompilować jeden z nich na nowoczesnym komputerze stacjonarnym, może to zająć kilka godzin zamiast sekund.

„O nie! To brzmi okropnie! Jednak czy mogę zapobiec temu tragicznemu losowi ?!” Niestety niewiele możesz z tym zrobić. Jeśli kompilacja trwa godzinami, kompilacja zajmuje wiele godzin. Ale to naprawdę ma znaczenie tylko za pierwszym razem - po raz skompilowany nie ma powodu, aby kompilować go ponownie.

Chyba że coś zmienisz.

Teraz, jeśli masz dwa miliony linii kodu połączonych w jeden gigantyczny behemot i musisz zrobić prostą naprawę błędu, na przykład, x = y + 1oznacza to, że musisz ponownie skompilować wszystkie dwa miliony linii, aby to przetestować. A jeśli dowiesz się, że zamierzałeś zrobić x = y - 1zamiast tego, to znowu czekają na Ciebie dwa miliony wierszy kompilacji. To wiele zmarnowanych godzin, które lepiej byłoby spędzić na czymkolwiek innym.

„Ale ja nienawidzę być nieproduktywny! Gdyby tylko był jakiś sposób na skompilowanie osobnych części mojego kodu i późniejsze połączenie ich w jakiś sposób !” W teorii doskonały pomysł. Ale co, jeśli Twój program musi wiedzieć, co się dzieje w innym pliku? Niemożliwe jest całkowite oddzielenie bazy kodu, chyba że chcesz zamiast tego uruchomić kilka malutkich plików .exe.

„Ale na pewno musi być możliwe! W przeciwnym razie programowanie brzmi jak czysta tortura! A co, jeśli znajdę sposób na oddzielenie interfejsu od implementacji ? zamiast tego w jakimś pliku nagłówkowym ? W ten sposób mogę użyć #include dyrektywy preprocesora, aby wprowadzić tylko informacje niezbędne do kompilacji! ”

Hmm. Może coś tam jest. Daj mi znać, jak to działa.


13
Dobra odpowiedź, sir. To była przyjemna lektura i łatwa do zrozumienia. Chciałabym, żeby mój podręcznik był napisany w ten sposób.
ialm

@veol Wyszukaj serię książek Head First - nie wiem jednak, czy mają wersję C ++. headfirstlabs.com
Amarghosh

2
Jest to (zdecydowanie) najlepsze sformułowanie, jakie do tej pory słyszałem lub rozważałem. Justin Case, utalentowany początkujący, osiągnął projekt osiągający milion naciśnięć klawiszy, jeszcze nie wysłany, a godny pochwały „pierwszy projekt”, który widzi światło aplikacji w prawdziwej bazie użytkowników, rozpoznał problem rozwiązany przez zamknięcia. Brzmi zadziwiająco podobnie do zaawansowanych stanów pierwotnej definicji problemu OP bez „zakodowania tego prawie sto razy i nie mogę określić, co zrobić dla null (jako brak obiektu) w porównaniu z null (jako siostrzeniec) bez użycia programowania przez wyjątki”.
Nicholas Jordan

Oczywiście to wszystko rozpada się w przypadku szablonów, ponieważ większość kompilatorów nie obsługuje / nie implementuje słowa kluczowego „eksport”.
KitsuneYMG

1
Inną kwestią jest to, że masz wiele najnowocześniejszych bibliotek (jeśli pomyśleć o BOOST), które używają tylko nagłówków klas ... Ho, czekaj? Dlaczego doświadczony programista nie oddziela interfejsu od implementacji? Częścią odpowiedzi może być to, co powiedział Blindly, inną częścią może być to, że jeden plik jest lepszy niż dwa, kiedy jest to możliwe, a inna część to to, że linkowanie ma koszt, który może być dość wysoki. Widziałem programy działające dziesięć razy szybciej z bezpośrednim włączeniem optymalizacji źródła i kompilatora. Ponieważ łączenie w większości blokuje optymalizację.
kriss

45

To prawdopodobnie bardziej szczegółowa odpowiedź, niż chciałeś, ale myślę, że przyzwoite wyjaśnienie jest uzasadnione.

W językach C i C ++ jeden plik źródłowy jest definiowany jako jedna jednostka tłumaczenia . Zgodnie z konwencją, pliki nagłówkowe zawierają deklaracje funkcji, definicje typów i definicje klas. Rzeczywiste implementacje funkcji znajdują się w jednostkach tłumaczeniowych, tj. Plikach .cpp.

Pomysł polega na tym, że funkcje i funkcje składowe klasy / struktury są kompilowane i składane raz, a następnie inne funkcje mogą wywoływać ten kod z jednego miejsca bez tworzenia duplikatów. Twoje funkcje są domyślnie deklarowane jako „extern”.

/* Function declaration, usually found in headers. */
/* Implicitly 'extern', i.e the symbol is visible everywhere, not just locally.*/
int add(int, int);

/* function body, or function definition. */
int add(int a, int b) 
{
   return a + b;
}

Jeśli chcesz, aby funkcja była lokalna dla jednostki tłumaczeniowej, definiujesz ją jako „statyczną”. Co to znaczy? Oznacza to, że jeśli włączysz pliki źródłowe z funkcjami extern, otrzymasz błędy redefinicji, ponieważ kompilator napotka tę samą implementację więcej niż raz. Dlatego chcesz, aby wszystkie jednostki tłumaczeniowe widziały deklarację funkcji, ale nie jej treść .

Jak więc to wszystko się na końcu zlewa? To jest praca linkera. Linker czyta wszystkie pliki obiektowe, które są generowane przez etap asemblera i rozwiązuje symbole. Jak powiedziałem wcześniej, symbol to tylko nazwa. Na przykład nazwa zmiennej lub funkcji. Gdy jednostki tłumaczeniowe, które wywołują funkcje lub deklarują typy, nie znają implementacji tych funkcji lub typów, mówi się, że te symbole są nierozwiązane. Linker rozwiązuje nierozwiązany symbol, łącząc jednostkę translacyjną, która zawiera niezdefiniowany symbol, z tym, który zawiera implementację. Uff. Dotyczy to wszystkich symboli widocznych na zewnątrz, niezależnie od tego, czy są one zaimplementowane w kodzie, czy dostarczane przez dodatkową bibliotekę. Biblioteka to tak naprawdę tylko archiwum z kodem wielokrotnego użytku.

Istnieją dwa godne uwagi wyjątki. Po pierwsze, jeśli masz małą funkcję, możesz ją wstawić. Oznacza to, że wygenerowany kod maszynowy nie generuje wywołania funkcji extern, ale jest dosłownie konkatenowany w miejscu. Ponieważ zwykle są małe, rozmiar narzutu nie ma znaczenia. Możesz sobie wyobrazić, że działają statycznie. Dlatego bezpieczne jest implementowanie funkcji inline w nagłówkach. Implementacje funkcji wewnątrz definicji klasy lub struktury są również często wstawiane automatycznie przez kompilator.

Innym wyjątkiem są szablony. Ponieważ kompilator musi widzieć całą definicję typu szablonu podczas ich tworzenia, nie jest możliwe oddzielenie implementacji od definicji, tak jak w przypadku funkcji autonomicznych lub normalnych klas. Cóż, być może jest to teraz możliwe, ale uzyskanie szerokiego wsparcia kompilatora dla słowa kluczowego „eksport” zajęło dużo czasu. Tak więc bez obsługi „eksportu” jednostki tłumaczeniowe otrzymują swoje własne lokalne kopie typów i funkcji z szablonami, na których działają instancje, podobnie jak działają funkcje wbudowane. W przypadku obsługi „eksportu” tak nie jest.

Z dwóch wyjątków niektórzy uważają, że „przyjemniej” jest umieścić implementacje funkcji wbudowanych, funkcji opartych na szablonach i typów opartych na szablonach w plikach .cpp, a następnie # uwzględniać plik .cpp. Nie ma znaczenia, czy jest to nagłówek, czy plik źródłowy; preprocesor nie dba o to i jest tylko konwencją.

Krótkie podsumowanie całego procesu od kodu C ++ (kilka plików) do końcowego pliku wykonywalnego:

  • Uruchomiony zostaje preprocesor , który analizuje wszystkie dyrektywy zaczynające się od znaku „#”. Dyrektywa #include łączy dołączony plik z gorszym, na przykład. Wykonuje również zastępowanie makr i wklejanie tokenów.
  • Rzeczywisty kompilator działa na pośrednim pliku tekstowym po etapie preprocesora i emituje kod asemblera.
  • W asemblera działa na pliku montaż i emituje kodu maszynowego, jest to zwykle nazywa się plik obiektu i następuje binarny format wykonywalny systemu operacyjnego, o którym mowa. Na przykład Windows używa PE (przenośny format wykonywalny), podczas gdy Linux używa formatu ELF Unix System V z rozszerzeniami GNU. Na tym etapie symbole są nadal oznaczone jako niezdefiniowane.
  • Na koniec uruchamiany jest konsolidator . Wszystkie poprzednie etapy zostały przeprowadzone na każdej jednostce tłumaczeniowej po kolei. Jednak etap konsolidatora działa na wszystkich wygenerowanych plikach obiektów, które zostały wygenerowane przez asembler. Linker rozwiązuje symbole i wykonuje wiele magicznych czynności, takich jak tworzenie sekcji i segmentów, co jest zależne od platformy docelowej i formatu binarnego. Generalnie programiści nie muszą o tym wiedzieć, ale w niektórych przypadkach z pewnością pomaga.

Ponownie, było to zdecydowanie więcej, niż prosiłeś, ale mam nadzieję, że drobiazgowe szczegóły pomogą ci zobaczyć większy obraz.


2
Dziękuję za dokładne wyjaśnienie. Przyznaję, że to jeszcze nie ma dla mnie sensu i myślę, że będę musiał ponownie (i jeszcze raz) przeczytać Twoją odpowiedź.
ialm

1
+1 za doskonałe wyjaśnienie. szkoda, że ​​prawdopodobnie odstraszy to wszystkich początkujących C ++. :)
goldPseudo

1
Heh, nie czuj się źle. W przypadku przepełnienia stosu najdłuższa odpowiedź rzadko jest najlepszą odpowiedzią.

int add(int, int);jest deklaracją funkcji . Prototyp część jest po prostu int, int. Jednak wszystkie funkcje w C ++ mają prototyp, więc termin naprawdę ma sens tylko w C. Zredagowałem twoją odpowiedź dotyczącą tego efektu.
melpomene

exportfor templates został usunięty z języka w 2011 roku. Kompilatory nigdy nie były tak naprawdę obsługiwane.
melpomene

10

Typowym rozwiązaniem jest użycie .hplików tylko do deklaracji i .cppplików do implementacji. Jeśli chcesz ponownie użyć implementacji, dołączasz odpowiedni .hplik do .cpppliku, w którym jest używana niezbędna klasa / funkcja / cokolwiek, i odsyłasz do już skompilowanego .cpppliku (albo .objplik - zwykle używany w jednym projekcie - albo plik .lib - zwykle używany do ponownego wykorzystania z wielu projektów). W ten sposób nie musisz ponownie kompilować wszystkiego, jeśli tylko zmieni się implementacja.


6

Pomyśl o plikach cpp jako o czarnej skrzynce, a pliki .h jako o przewodnikach, jak korzystać z tych czarnych skrzynek.

Pliki cpp można skompilować z wyprzedzeniem. To nie działa w tobie, # Uwzględnij je, ponieważ musi faktycznie „dołączać” kod do programu za każdym razem, gdy go kompiluje. Jeśli dołączysz tylko nagłówek, może po prostu użyć pliku nagłówkowego, aby określić, jak używać wstępnie skompilowanego pliku cpp.

Chociaż nie zrobi to dużej różnicy w przypadku twojego pierwszego projektu, jeśli zaczniesz pisać duże programy cpp, ludzie będą cię nienawidzić, ponieważ czasy kompilacji eksplodują.

Przeczytaj również: Wzorce dołączania plików nagłówkowych


Dziękuję za konkretniejszy przykład. Próbowałem przeczytać Twój link, ale teraz jestem zdezorientowany ... Jaka jest różnica między jawnym dołączeniem nagłówka a deklaracją do przodu?
ialm

to jest świetny artykuł. Veol, tutaj zawierają nagłówki, w których kompilator potrzebuje informacji dotyczących rozmiaru klasy. Deklaracja Forward jest używana, gdy używasz tylko wskaźników.
pankajt

deklaracja do przodu: int someFunction (int requiredValue); zwróć uwagę na użycie informacji o typie i (zwykle) bez nawiasów klamrowych. To, jak podano, mówi kompilatorowi, że w pewnym momencie będziesz potrzebować funkcji, która pobiera int i zwraca int, kompilator może zarezerwować dla niej wywołanie, korzystając z tych informacji. Można by to nazwać deklaracją do przodu. Bardziej zaawansowane kompilatory powinny być w stanie znaleźć funkcję bez potrzeby tego, w tym nagłówek może być wygodnym sposobem na zadeklarowanie zestawu deklaracji do przodu.
Nicholas Jordan

6

Pliki nagłówkowe zwykle zawierają deklaracje funkcji / klas, podczas gdy pliki .cpp zawierają rzeczywiste implementacje. W czasie kompilacji każdy plik .cpp jest kompilowany do pliku obiektowego (zwykle z rozszerzeniem .o), a konsolidator łączy różne pliki obiektowe w ostateczny plik wykonywalny. Proces łączenia jest na ogół znacznie szybszy niż kompilacja.

Korzyści z tego oddzielenia: Jeśli rekompilujesz jeden z plików .cpp w swoim projekcie, nie musisz ponownie kompilować wszystkich pozostałych. Po prostu utwórz nowy plik obiektu dla tego konkretnego pliku .cpp. Kompilator nie musi przeglądać innych plików .cpp. Jeśli jednak chcesz wywołać funkcje z bieżącego pliku .cpp, które zostały zaimplementowane w innych plikach .cpp, musisz poinformować kompilator, jakie argumenty przyjmują; to jest cel dołączania plików nagłówkowych.

Wady: Podczas kompilowania danego pliku .cpp, kompilator nie „widzi”, co jest w innych plikach .cpp. Dlatego nie wie, jak zaimplementowano tam funkcje, w wyniku czego nie może optymalizować tak agresywnie. Ale myślę, że nie musisz się tym jeszcze przejmować (:


5

Podstawowa idea, że ​​nagłówki są dołączane tylko, a pliki cpp są tylko kompilowane. Stanie się to bardziej przydatne, gdy będziesz mieć wiele plików cpp, a ponowna kompilacja całej aplikacji, gdy zmodyfikujesz tylko jeden z nich, będzie zbyt wolna. Lub kiedy funkcje w plikach zaczną się w zależności od siebie. Dlatego należy oddzielić deklaracje klas do plików nagłówkowych, pozostawić implementację w plikach cpp i napisać plik Makefile (lub coś innego, w zależności od używanych narzędzi), aby skompilować pliki cpp i połączyć wynikowe pliki obiektowe z programem.


3

Jeśli # uwzględnisz plik cpp w kilku innych plikach w programie, kompilator będzie próbował wielokrotnie skompilować plik cpp i wygeneruje błąd, ponieważ będzie wiele implementacji tych samych metod.

Kompilacja potrwa dłużej (co staje się problemem w przypadku dużych projektów), jeśli wprowadzisz zmiany w plikach #included cpp, które następnie wymuszą ponowną kompilację wszystkich plików, # zawierających je.

Po prostu umieść deklaracje w plikach nagłówkowych i dołącz je (ponieważ w rzeczywistości nie generują one kodu jako takiego), a linker połączy deklaracje z odpowiednim kodem cpp (który następnie zostanie skompilowany tylko raz).


Więc oprócz dłuższego czasu kompilacji zacznę mieć problemy, gdy # dołączę mój plik cpp do wielu różnych plików, które używają funkcji zawartych w dołączonych plikach cpp?
ialm

Tak, nazywa się to kolizją przestrzeni nazw. Interesujące jest tutaj to, czy linkowanie z bibliotekami wprowadza problemy z przestrzenią nazw. Ogólnie uważam, że kompilatory zapewniają lepsze czasy kompilacji dla zakresu jednostek tłumaczeniowych (wszystko w jednym pliku), co wprowadza problemy z przestrzenią nazw - co prowadzi do ponownego oddzielenia ... możesz dołączyć plik dołączany do każdej jednostki tłumaczeniowej (przypuszczalnie) istnieje nawet pragma (raz #pragma), która ma to wymusić, ale jest to przypuszczenie czopków. Uważaj, aby nie polegać ślepo na libs (plikach .O) z dowolnego miejsca, ponieważ łącza 32-bitowe nie są wymuszane.
Nicholas Jordan

2

Chociaż jest to z pewnością możliwe, aby to zrobić, standardową praktyką jest umieszczanie wspólnych deklaracji w plikach nagłówkowych (.h), a definicje funkcji i zmiennych - implementacja - w plikach źródłowych (.cpp).

Zgodnie z konwencją pomaga to jasno określić, gdzie wszystko się znajduje, i jasno rozróżnić interfejs i implementację modułów. Oznacza to również, że nigdy nie musisz sprawdzać, czy plik .cpp znajduje się w innym, przed dodaniem do niego czegoś, co mogłoby się zepsuć, gdyby został zdefiniowany w kilku różnych jednostkach.


2

możliwość ponownego użycia, architektura i enkapsulacja danych

oto przykład:

powiedzmy, że tworzysz plik cpp, który zawiera prostą formę procedur łańcuchowych, wszystkie w klasie mystring, umieszczasz w tym celu deklarację klasy w mystring.h kompilując mystring.cpp do pliku .obj

teraz w swoim głównym programie (np. main.cpp) dołączasz nagłówek i link do mystring.obj. do użytku mystring w swoim programie, że nie dbają o szczegóły , jak mystring jest realizowany od nagłówku mówi , co można zrobić

teraz, jeśli kumpel chce użyć twojej mystring class, daj mu mystring.h i mystring.obj, on również niekoniecznie musi wiedzieć, jak to działa, dopóki działa.

później, jeśli masz więcej takich plików .obj, możesz połączyć je w plik .lib i zamiast tego utworzyć łącze do niego.

możesz również zdecydować o zmianie pliku mystring.cpp i zaimplementowaniu go bardziej efektywnie, nie wpłynie to na twój main.cpp ani program twoich znajomych.


2

Jeśli to działa, nie ma w tym nic złego - poza tym, że będzie potrząsać piórami ludzi, którzy myślą, że jest tylko jeden sposób na zrobienie czegoś.

Wiele odpowiedzi udzielonych tutaj dotyczy optymalizacji dla projektów oprogramowania na dużą skalę. Warto o tym wiedzieć, ale nie ma sensu optymalizować małego projektu tak, jakby to był duży projekt - jest to tak zwane „przedwczesna optymalizacja”. W zależności od środowiska programistycznego konfigurowanie konfiguracji kompilacji w celu obsługi wielu plików źródłowych na program może być znacznie bardziej skomplikowane.

Jeśli z czasem ewoluuje Twoich projektów i można zauważyć, że proces budowania trwa zbyt długo, wtedy możesz byłaby kodu do korzystania z wielu plików źródłowych dla przyrostowego szybciej buduje.

Kilka odpowiedzi dotyczy oddzielenia interfejsu od implementacji. Jednak nie jest to nieodłączna cecha plików nagłówkowych i dość często zdarza się, że pliki nagłówkowe #include bezpośrednio zawierają ich implementację (nawet biblioteka standardowa C ++ robi to w znacznym stopniu).

Jedyną naprawdę „niekonwencjonalną” rzeczą w tym, co zrobiłeś, było nazwanie dołączonych plików „.cpp” zamiast „.h” lub „.hpp”.


1

Kiedy kompilujesz i łączysz program, kompilator najpierw kompiluje poszczególne pliki CPP, a następnie łączy je (łączy). Nagłówki nigdy nie zostaną skompilowane, chyba że zostaną najpierw dołączone do pliku cpp.

Zwykle nagłówki to deklaracje, a cpp to pliki implementacyjne. W nagłówkach definiujesz interfejs dla klasy lub funkcji, ale pomijasz sposób implementacji szczegółów. W ten sposób nie musisz ponownie kompilować każdego pliku cpp, jeśli wprowadzisz zmianę w jednym.


jeśli zostawisz implementację poza plikiem nagłówkowym, przepraszam, ale to brzmi jak interfejs Java, prawda?
gansub

1

Zasugeruję ci przejście przez projekt oprogramowania C ++ na dużą skalę autorstwa Johna Lakosa . Na uczelni zazwyczaj piszemy małe projekty, w których nie napotykamy takich problemów. Książka podkreśla znaczenie oddzielenia interfejsów i implementacji.

Pliki nagłówkowe zwykle mają interfejsy, które nie powinny być tak często zmieniane. Podobnie spojrzenie na wzorce, takie jak idiom Virtual Constructor, pomoże ci lepiej zrozumieć koncepcję.

Wciąż się uczę jak Ty :)


Dzięki za sugestię dotyczącą książki. Nie wiem, czy kiedykolwiek
dojdę

programowanie programów na dużą skalę i dla wielu wyzwań to zabawa.
Zaczynam

1

To jak pisanie książki, chcesz wydrukować ukończone rozdziały tylko raz

Powiedz, że piszesz książkę. Jeśli umieścisz rozdziały w oddzielnych plikach, będziesz musiał wydrukować rozdział tylko wtedy, gdy go zmieniłeś. Praca nad jednym rozdziałem nie zmienia żadnego z pozostałych.

Ale włączenie plików cpp jest, z punktu widzenia kompilatora, jak edycja wszystkich rozdziałów książki w jednym pliku. Następnie, jeśli to zmienisz, musisz wydrukować wszystkie strony całej książki, aby wydrukować poprawiony rozdział. W generowaniu kodu obiektowego nie ma opcji „drukuj wybrane strony”.

Wracając do oprogramowania: mam Linux i Ruby src w pobliżu. Zgrubna miara linii kodu ...

     Linux       Ruby
   100,000    100,000   core functionality (just kernel/*, ruby top level dir)
10,000,000    200,000   everything 

Każda z tych czterech kategorii zawiera dużo kodu, stąd potrzeba modułowości. Ten rodzaj bazy kodu jest zaskakująco typowy dla systemów w świecie rzeczywistym.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.