Działanie kompilacji w C++

Działanie kompilacji w C++
AN
  • Rejestracja:około 19 lat
  • Ostatnio:około 2 godziny
0

Jakiś czas temu pobrałem źródła do VTTEST https://invisible-island.net/vttest/#download i zacząłem je przerabiać na swoje potrzeby

Niedawno potrzebowałem zaemulować system z Z80 bazując na https://github.com/Jean-MarcHarvengt/MCUME/tree/master/MCUME_pico/pico81 i pracujący na płytce Picomputer.

Teraz nie jestem w stanie "na poczekaniu" umieścić kawałka kodu obrazującego problem, ale spróbuję go przedstawić własnymi słowami. Temat nie jest już aktualny, bo w tych przypadkach udało mi się zrobić to co chciałem inaczej i większym nakładem pracy, jednak aktualne pozostaje pytanie, o to, jak działa kompilacja i z czego wynika problem.

Oryginalne źródła bez problemu kompilują się w oryginalnej postaci. VTTest kompiluje się za pomoca standardowego 'configure' i 'make', a Pico81 kompiluje się w sposób opisany w readme projektu.

Jednak nie o tym mowa. W obu projektach chciałem dorobić strukturę i funkcje bardzo globalne, dostępne z każdego miejsca, czy z wielu miejsc. Niekoniecznie jest to najlepsze rozwiązanie, ale jest to najprostsze do zrobienia. Próbowałem w następujący sposób: Tworzę nowy plik, powiedzmy additional.h, w tym pliku umieszczam taką treść (najprościej, jak można):

Kopiuj
#ifndef ADDITIONAL_H
#define ADDITIONAL_H

int SomeGlobalVariable = 0;

void SomeGlobalMethod()
{
}

#endif

Potem we wszystkich plikach źródła, w których chce mieć dostęp do globalnej zmiennej, umieszczam najzwyczajniej w świecie:

Kopiuj
#include "additional.h"

Ja rozumiem to tak, że preprocesor w miejsce każdego #include wstawia treść wskazanego pliku, a jak są podane kilka plików źródłowych, jak przykład poniżej, to następuje konkatenacja tekstów z podanych plików

Kopiuj
gcc file1.cpp file2.cpp file3.cpp -o binaryfile

Z tego, co rozumiem, pierwsza, druga i ostatnia linia przykładowego pliku additional.h sprawia, że przy pierwszym wystąpieniu #include "additional.h" zostanie wstawiony tekst z tego pliku, a za każdym następnym wystąpieniem, nic nie zostanie wstawione, bo zdefiniowanie ADDITIONAL_H zablokuje przetworzenie treści objętej dyrektywami #ifndef ADDITIONAL_H i #endif. Z tego, co czytałem, ta dyrektywa to jest popularna technika sprawiania, że dany plik zostanie wstawiony tylko jeden raz, w miejsce pierwszego wystąpienia #include "additional.h". Niektóre IDE same generują te dyrektywy przy tworzeniu nowych plików klas.

W ten oto sposób, przed samą kompilacją jest jeden wielki tekst kodu źródłowego, który już nie zawiera dyrektyw. Jak sama nazwa mówi, preprocesor, to jest wstępne przetwarzanie przygotowawcze niebędące kompilowaniem, mające miejsce przed zasadniczym przetwarzaniem, jakim jest kompilacją.

Czy to wszystko, co powyżej napisałem odnośnie działania dyrektyw jest prawdą i czy dobrze rozumiem?

Jeżeli takie rzeczy robię w swoich projektach, które robię od zera, to nie ma żadnego problemu, kompilacja programu przebiega bez problemu, a program działa zgodnie z moimi wyobrażeniami.

Natomiast, jak coś robię w cudzym projekcie, co miało miejsce w dwóch wymienionych, to pojawia się problem. Problem jest taki, że kompilator podaje komunikat, że zmienna SomeGlobalVariable jest już zadeklarowana, podobnie, jak metoda SomeGlobalMethod jest już wcześniej zaimplementowana. Wygląda, jakby dyrektywy nie działały, lub jakby kompilator przy dalszym przetwarzaniu zapominał, że wystąpił #define ADDITIONAL_H i później mimo tego, ponownie wstawia zawartość pliku, co oczywiście skutkuje błędem kompilacji.

Co do "mojego" C++, to jeżeli cos robię dla okienek w Qt creator, to dodaję kolejne pliki, nie zawracając sobie głowy konfiguracją kompilowania. Jeżeli coś robię w WASM, to kompiluję poleceniem emcc file1.cpp file2.cpp file3.cpp -o outscript.js

Czym różni się kompilowanie cudzych projektów od mojego, że przy moim projekcie, jak chce dorobić coś globalnego to nie ma żadnego problemu (a to, czy to jest rozsądne posunięcie to już inna sprawa, ale to nie o tym jest ten wątek), a jak w cudzym projekcie, to jest problem z działaniem dyrektyw i wielokrotnym dołączeniem tego samego pliku?

Wygląda na to, że kompilowanie tych projektów według dokumentacji przebiega inaczej i przez to kompilator gubi informację o dyrektywie, a i tak wychodzi jeden plik binarny. Z czego to wynika?

edytowany 4x, ostatnio: andrzejlisek
GO
Możesz podejrzeć wynik preprocesora dodając parametr -E, ja bym najpierw skompilował bez modyfikacji i potem dodawał po trochu, aż się wywali i analizował od tego miejsca. Też może być tak, że każdy plik jest oddzielnie kompilowany i potem linker je łączy, to dopiero wtedy wyjdzie, że kilka plików ma te same symbole w tablicy, nie wiem czy to może być zależne od wersji kompilatora.
overcq
  • Rejestracja:około 7 lat
  • Ostatnio:około 5 godzin
  • Postów:394
3

Nie wiem, dlaczego w Twoich projektach to działało. To nie może działać.
To prawda, że w danym pliku źródłowym plik nagłówkowy jest włączany tylko raz. Ale podczas konsolidacji wszystkie pliki źródłowe przetworzone na pliki *.o zawierają definicję tak samo nazwanej zmiennej globalnej i procedury.
Włączać w pliku nagłówkowym powinieneś tylko deklaracje extern, w innych plikach niż ten, w którym zdefiniowałeś zmienną i procedurę.
Ale masz tag C++, więc raczej nie traktuj tego, co piszę, jako pewne. ;)


Nie znam się, ale się wypowiem.
Wizytówka
joh­nny_Be_go­od jest mistrzem ‘eskejpowania’ i osadzania.
Azarien
  • Rejestracja:ponad 21 lat
  • Ostatnio:około 6 godzin
7

a jak są podane kilka plików źródłowych, jak przykład poniżej, to następuje konkatenacja tekstów z podanych plików
gcc file1.cpp file2.cpp file3.cpp -o binaryfile

Nie. Następuje połączenie tych plików, ale na poziomie linkera, czyli już po skompilowaniu każdego pliku .cpp osobno do postaci binarnej.
Równie dobrze powyższą komendę można zastąpić sekwencją komend

Kopiuj
gcc -c file1.cpp
gcc -c file2.cpp
gcc -c file3.cpp
gcc file1.o file2.o file3.o -o binaryfile
AN
  • Rejestracja:około 19 lat
  • Ostatnio:około 2 godziny
0

Nie wiem, dlaczego w Twoich projektach to działało. To nie może działać.

Przyznaję się, że nie miałem przypadku, żeby nagle dorabiać plik do skończonego projektu. Miałem w głowie całą strukturę i wiedziałem, gdzie dopisać, żeby nie zepsuć kompilacji i nie zaśmiecić źródła.

To prawda, że w danym pliku źródłowym plik nagłówkowy jest włączany tylko raz. Ale podczas konsolidacji wszystkie pliki źródłowe przetworzone na pliki *.o zawierają definicję tak samo nazwanej zmiennej globalnej i procedury.
Włączać w pliku nagłówkowym powinieneś tylko deklaracje extern, w innych plikach niż ten, w którym zdefiniowałeś zmienną i procedurę.

Przechodząc do meritum, załóżmy, że mam program w trzech plikach file1.cpp, file2.cpp, file3.cpp, które oczywiście mogą odnosić się do innych plików. W pliku file3.cpp jest int main() {} z jakąś treścią.

Wykonam trzy warianty kompilacji.

Wariant pierwszy:

  1. Polecenie gcc file1.cpp file2.cpp file3.cpp -o binaryfile.

Wariant drugi:

  1. Tworzę taki plik o nazwie fileall.cpp i z taką zawartością:
Kopiuj
#include "file1.cpp"
#include "file2.cpp"
#include "file3.cpp"
  1. Kompiluję takim poleceniem gcc fileall.cpp -o binaryfile.

Wariant trzeci:

  1. Robię konkatenację plików cat file1.cpp file2.cpp file3.cpp > fileall.cpp.
  2. Kompiluję otrzymany plik gcc fileall.cpp -o binaryfile.

Czy dobrze rozumiem, że wariant drugi i trzeci są równoważne, ale wariant pierwszy nie jest równoważny z drugim ani z trzecim? Czy dobrze rozumiem, że jeżeli w pliku file1.cpp będzie jakiś #define SomeDef, to w wariancie pierwszym będzie obowiązywać tylko w obrębie pierwszego pliku, a w wariantach drugim i trzecim będzie obowiązywac we wszystkich plikach do końca ostatniego, o ile nigdzie nie ma #undef SomeDef?

Ale masz tag C++, więc raczej nie traktuj tego, co piszę, jako pewne. ;)

Rozumiem, że piszesz bardziej o C niż C++, ale to też chętnie przyjmę do wiadomości. Co prawda to dwa różne języki, ale pewne elementy są wspólne dla obu. Dopisałem tag C, choć bardziej interesuje mnie C++, aczkolwiek te projekty są w czystym C i czasem, jak widać, mam z kontakt z C, a jak ja coś piszę na desktop lub WASM, to piszę w C++.

AN
  • Rejestracja:około 19 lat
  • Ostatnio:około 2 godziny
0

Nie. Następuje połączenie tych plików, ale na poziomie linkera, czyli już po skompilowaniu każdego pliku .cpp osobno do postaci binarnej.

To chyba wyjaśnia całą sprawę. Miałem błędne wyobrażenie o działaniu kompilatora i linkera w przypadku podania wielu plików źródłowych.

Nie wiem, dlaczego w Twoich projektach to działało. To nie może działać.

Zrobiłem szybkie testy i faktycznie, tak to nie działa.

Jak dołączę powiedzmy #include "additional.h" w dwóch plikach, to nie kompiluje się w przypadku gcc file1.c file2.c -o binaryfile, ale teraz już wyjaśniło się, co jest tego powodem, czyli jest konflikt nazw na etapie łączenia linkerem.

Natomiast, jak dołączę plik #include <stdio.h> w dwóch plikach źródłowych, to kompilator nie czepia się, tylko kompiluje i program działa. To samo dotyczy każdego nagłówka z biblioteki standardowej.

Czym różni się #include "additional.h" od #include <stdio.h> oprócz tego, że jest to inny plik, poszukiwany w innym miejscu, zawierający inne funkcje i zmienne?

edytowany 2x, ostatnio: cerrato
overcq
  • Rejestracja:około 7 lat
  • Ostatnio:około 5 godzin
  • Postów:394
1
andrzejlisek napisał(a):

Czym różni się #include "additional.h" od #include <stdio.h> oprócz tego, że jest to inny plik, poszukiwany w innym miejscu, zawierający inne funkcje i zmienne?

Ten drugi plik — jak możesz zobaczyć w źródle — zawiera deklaracje extern do rzeczy zdefiniowanych w implicite dołączanej bibliotece języka C.


Nie znam się, ale się wypowiem.
Wizytówka
joh­nny_Be_go­od jest mistrzem ‘eskejpowania’ i osadzania.
elwis
  • Rejestracja:ponad 18 lat
  • Ostatnio:25 dni
2
andrzejlisek napisał(a):
Kopiuj
#ifndef ADDITIONAL_H
#define ADDITIONAL_H

int SomeGlobalVariable = 0;

void SomeGlobalMethod()
{
}

#endif

Jeśli deklarujesz zmienną w nagłówku to tylko jako extern. Dyrektywa #pragma once lub wersja #ifndef... z C, której użyłeś, sprawia, ze kod będzie wstawiony raz dla każdego modułu, który kompilujesz (a każdy plik cpp to oddzielny moduł). W związku z tym, w takiej formie jak to zrobiłeś, każdy moduł ma zmienną globalną SomeGlobalVariable i przy linkowaniu następuje konflikt nazw zmiennych. Zmienną globalną (skoro już musisz jej używać…) należy zadeklarować w module (plik .c/.cpp) tak jak to zrobiłeś powyżej, a w nagłówku dać tylko extern int SomeGlobalVariable;. Tak samo z definicją funkcji. Definicja idzie do modułu, a w nagłówku jest tylko deklaracja.


edytowany 3x, ostatnio: elwis
LE
Raczej inline
elwis
Co raczej inline? No, funkcję można zrobić inline, jeśli akurat nam to pasuje. Jeśli na przykład chcemy mieć wskaźnik na tę funkcję to mogą się pojawić problemy.
LE
Nie rozumiesz jak działa inline, to słowo kluczowe służy właśnie do oznaczania zmiennych i funkcji w plikach nagłówkowych, żeby nie było konfliktów.
elwis
A może po prostu ty mówisz o C++, a ja o C? Jak patrzę w dokumentację, w C++ rzeczywiście ma trochę inne znaczenie.
LE
Temat dotyczy języka C++ i napisałeś, że "zmienną w nagłówku to tylko jako extern", więc to prostuję. Równie dobrze mogłeś napisać o Javie, ale po co?
AN
  • Rejestracja:około 19 lat
  • Ostatnio:około 2 godziny
0

Nigdy nie stosowałem extern w tym znaczeniu, znam tylko extern "C" { } służące do zupełnie czegoś innego. Z powyższych odpowiedzi i moich testów wynika, że extern int SomeGlobalVariable znaczy "informuję, że gdzieś w całym programie, niekoniecznie w niniejszym module, istnieje zmienna SomeGlobalVariable, a jej deklaracja ma zasięg na cały program".

Jeżeli np. w pliku file1.cpp jest funkcja void Func1(), a w pliku file2.cpp jest void Func2(), ale też funkcja int main() wywołująca Func1() i Func2(), to nie skompiluje się, bo w chwili kompilacji pliku file2.cpp kompilator nic nie wie o funckji Func1(), ale jak się w file2.c dopisze extern void Func1();, to się kompiluje i działa. Ja to rozumiem, że zapis extern void Func1(); znaczy "informuję, że gdzieś w programie, w innym module istnieje implementacja funkcji Func1() będąca funkcją bez parametrów i zwracająca pusty typ" W tym przypadku, sama implementacja jest w file1.cpp.

edytowany 1x, ostatnio: andrzejlisek
GO
extern C robi tyle, żę mówi, nie korzystaj z demanglingu tylko tak jak jest napisane taka jest nazwa symbolu, który deklarujesz, bo C ma nazwę funkcji = nazwie symbola, a w C++ jest nazwa funkcji gdzie poprzedza ją ilość liter w nazwie, potem nazwa i kończy typy parametrów jakie przyjmuje dana funkcja, czyli C++ ma zupełnie inny sposób kodowania nazw symboli niż C dlatego dodaje się te extern C
Azarien
w C też jest prymitywny mangling, najczęściej polegający na dodaniu underscore'a foo -> _foo.
KS
  • Rejestracja:prawie 4 lata
  • Ostatnio:około 3 godziny
  • Postów:624
1

Odpowiedź dotyczy tylko języka C ( patrz tagi tematu ).

Plik zostanie wstawiony ZA KAŻDYM RAZEM jak pojawi się #include "plik"
potem dopiero preprocesor będzie sobie na tym działał.

Można zrobić taką trolerkę w kodzie, że pewne makra załączą się dopiero po którymś powtórzonym includzie tego samego pliku.

edytowany 1x, ostatnio: ksh
AN
  • Rejestracja:około 19 lat
  • Ostatnio:około 2 godziny
0

Wobec tego, co uzgodniliśmy, zrobiłem jeszcze jedną próbę, która wydaje się być "wyjątkiem od reguły":

someclass.h - celowo nie ma zabezpieczenia przed wielokrotnym wklejaniem kodu z tego pliku

Kopiuj
#include <stdio.h>

class SomeClass
{
public:
    void Func1();
    void Func2();
};

someclass1.cpp

Kopiuj
#include "someclass.h"

void SomeClass::Func1()
{
    printf("%s", "Function 1 from someclass1.cpp\n");
}

someclass2.cpp

Kopiuj
#include "someclass.h"

void SomeClass::Func2()
{
    printf("%s", "Function 2 from someclass2.cpp\n");
}

prog.cpp

Kopiuj
#include <stdio.h>
#include "someclass.h"

int main()
{
    printf("%s", "Test begin\n");
    SomeClass Obj;
    Obj.Func1();
    Obj.Func2();
    printf("%s", "Test end\n");
    return 0;
}

Kompiluje to takim poleceniem:

Kopiuj
gcc prog.cpp someclass1.cpp someclass2.cpp -o binaryfile

W związku z tym, co uzgodniliśmy, rozumiem, że kompilator najpierw wytwarza trzy osobne moduły, po jednym na podstawie każdego pliku z include, a później łączy je w jeden program.

Mamy tu identyczną sytuację, czyli dla każdego modułu z osobna jest podłączany plik someclass.h i na końcu okazuje się, że w dwóch modułach (pliki *.o) jest identyczna deklaracja klasy, ale tutaj kompilator sobie poradzi. Tak naprawdę, ten plik jest użyty trzykrotnie w całym programie, po jednym razie dla każdego modułu.

Wyjaśniliśmy sobie, że wielokrotna deklaracja tego samego nawet pomiędzy różnymi modułami (pliki *.o) całkowicie uniemożliwia kompilację, a okazuje się, że w tym przypadku to nie jest problemem.

Jak dodam zmienną globalną, to słusznie się nie kompiluje i to potwierdza wielokrotność deklaracji tego samego na etapie łączenia modułów.

Kopiuj
#include <stdio.h>

int SomeGlobalVar;

class SomeClass
{
public:
    void Func1();
    void Func2();
};

Czym różni się wielokrotna deklaracja SomeGlobalVar od wielokrotnej deklaracji SomeClass, że to pierwsze blokuje kompilację, a to drugie już się kompiluje?

Ale, jeżeli zmodyfikuję plik na taki kod, czyli ręczne powórzenie deklaracji klasy, to już się nie kompiluje.

Kopiuj
#include <stdio.h>

class SomeClass
{
public:
    void Func1();
    void Func2();
};

class SomeClass
{
public:
    void Func1();
    void Func2();
};

Czym powyższe się różni wobec załączenia tego pliku do kilku modułów, skoro w obu przypadkach dochodzi do wielokrotnej deklaracji SomeClass?

edytowany 4x, ostatnio: cerrato
Azarien
  • Rejestracja:ponad 21 lat
  • Ostatnio:około 6 godzin
2

Czym różni się wielokrotna deklaracja SomeGlobalVar od wielokrotnej deklaracji SomeClass, że to pierwsze blokuje kompilację, a to drugie już się kompiluje?

Tym że int SomeGlobalVar; jest zmienną. Nie jest tylko informacją że taka zmienna istnieje, ale jest faktycznie utworzeniem tej zmiennej w miejscu, w którym w kodzie występuje. Następuje zarezerwowanie pamięci na tę zmienną (zapewne 4 bajtów).
Taką „informacją że zmienna istnieje bez tworzenia tej zmiennej” byłoby extern int SomeGlobalVar;.

Deklaracja klasy class SomeClass { public: void Func1(); void Func2(); }; nie powołuje niczego do istnienia. Jest tylko informacją dla kompilatora, że istnieje sobie taki typ. To coś jak typedef. Żadna pamięć nie jest alokowana ani rezerwowana. Póki wszystkie deklaracje są takie same, nie ma żadnego problemu - tak jak nie ma problemu z takim samym typedefem w każdym pliku .cpp.

edytowany 2x, ostatnio: Azarien
enedil
eh, no ale w środku możesz sobie zdefiniować ciało metod. Dlatego, że są by default z inline linkage
enedil
  • Rejestracja:prawie 12 lat
  • Ostatnio:3 dni
  • Postów:1027
0

Możesz zrobić też

Kopiuj
inline void SomeGlobalMethod()
{
}

i zadziała tak jak chciałeś.

edytowany 1x, ostatnio: enedil
RO
  • Rejestracja:9 miesięcy
  • Ostatnio:9 miesięcy
  • Postów:6
0

Mam pytanie dlaczego ludzie na produkcji używają cmake skoro pokazuje takie brzydkie rozwlekłe komunikaty o błędach? Przy gcc i clang wygląda to prawie tak samo, ale gdy korzystasz z samego clang bez cmake wygląda to trochę lepiej.
Jest jeszcze meson ale to nie ma nawet polotu do komunikatów rusta.

edytowany 1x, ostatnio: Robotics
GO
CMake generuje skrypt budujący, czyli na linuxie może ci wygenerować ninja czy makefile, możesz backend sobie wybrać i potem tym kompiluje, w zależności od systemu to inny backend wykorzystuje i dzięki temu jest przenośny bo na każdym systemie inny plik budujący ci zbuduje. Trochę to nieintuicyjne jest skoro możesz Makefile ręcznie napisać i zbudować to po co specjalnie CMake robić, który i tak ci na końcu tego makefile wygeneruje, ale jak będziesz chciał inny backend jak ninja to możesz wygenerować. Żeby debugować CMake można patrzeć np. na Makefile, który jest prostszy.
Marius.Maximus
  • Rejestracja:ponad 14 lat
  • Ostatnio:około 8 godzin
  • Postów:2105
0

@Robotics jeżeli nie rozumiesz cmake to pewnie nie jest on dla Ciebie i jakie komunikaty masz na mysli ?
Jak budujesz aplikację to masz komunikaty narzędzia którym budujesz np. ninja
ewentualnie może masz ustawione CMAKE_VERBOSE_MAKEFILE ?


--
Nie przyjmuję reklamacji za moje rady, używasz na własną odpowiedzialność.
Programowanie bez formatowania to jak chodzenie ze spodniami spuszczonymi na kostki. Owszem da się ale po pierwsze nie wygodne, po drugie nieprzyzwoicie wygląda.
Przed zaczęciem nowego wątku przeczytam problem XY
RO
Raczej nie zrozumiałeś o czym piszę, chodzi mi o to że jak kompilujesz program w języku rust i jak program się wykrzaczy to wyskakuje mały konkretny komunikat pokazujący co dokładnie zrobiłem źle. W cmake komunikat jest na całą kartkę a4, chaotycznie pokazany błąd z całą masą niepotrzebnego tekstu. Żeby w c++ mieć mniejsze bardziej konkretne komunikaty o błędach trzeba wpierw kompilować program za pomocą samego clanga (clang wyłapuje więcej błędów od gcc i w bardziej przejrzysty sposób je pokazuje), a dopiero potem dodać cmake czy inny meson.
KS
@Robotics: opowiadasz głupoty. Cmake niczego nie kompiluje. Jeżeli, nie rozumiesz tej jak to nazywasz chaotycznej ściany tekstu to po prostu nie rozumiesz co się dzieje. A warny się często sprawdza i w gcc, i clangu bo to nie tak, że jeden pokazuje wszystko lepiej ( sam też wolę clanga ).
RO
Gdyby tak było to cmake pokazałby taki sam mały komunikat co clang, a jednak pokazuje mnóstwo zbędnych wpisów. Jak mam rozszerzenie cmake tools i kompiluje wszystko klikając w build. Nie gadam głupot bo sam wybieram w cmake czy mój projekt ma skorzystać z clanga, gcc-13 czy gcc-14. Powinieneś wiedzieć że cmake wykorzystuje do kompilacji dany kompilator i tylko o to mi chodziło jakim sposobem kompiluje programy c++ i w jaki sposób wyświetla mi długie komunikaty błędów. Ty już z góry założyłeś że uważam cmake za kompilator.
KS
Odpalasz nakładkę na cmake ( jakieś windowsowe badziewie ) i warczysz na cmake. ogarnij się.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.