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 #include
plikó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 #include
w 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?
źródło
Odpowiedzi:
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
#include
mó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 + 1
oznacza to, że musisz ponownie skompilować wszystkie dwa miliony linii, aby to przetestować. A jeśli dowiesz się, że zamierzałeś zrobićx = y - 1
zamiast 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.
źródło
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”.
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:
Ponownie, było to zdecydowanie więcej, niż prosiłeś, ale mam nadzieję, że drobiazgowe szczegóły pomogą ci zobaczyć większy obraz.
źródło
int add(int, int);
jest deklaracją funkcji . Prototyp część jest po prostuint, 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.export
for templates został usunięty z języka w 2011 roku. Kompilatory nigdy nie były tak naprawdę obsługiwane.Typowym rozwiązaniem jest użycie
.h
plików tylko do deklaracji i.cpp
plików do implementacji. Jeśli chcesz ponownie użyć implementacji, dołączasz odpowiedni.h
plik do.cpp
pliku, w którym jest używana niezbędna klasa / funkcja / cokolwiek, i odsyłasz do już skompilowanego.cpp
pliku (albo.obj
plik - 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.źródło
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
źródło
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ć (:
źródło
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.
źródło
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).
źródło
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.
źródło
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.
źródło
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”.
źródło
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.
źródło
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 :)
źródło
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 ...
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.
źródło