Działanie kompilacji w C++

Działanie kompilacji w C++
AN
  • Rejestracja: dni
  • Ostatnio: dni
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?

overcq
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 402
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. ;)

Azarien
  • Rejestracja: dni
  • Ostatnio: dni
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: dni
  • Ostatnio: dni
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: dni
  • Ostatnio: dni
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?

overcq
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 402
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.

elwis
  • Rejestracja: dni
  • Ostatnio: 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.

AN
  • Rejestracja: dni
  • Ostatnio: dni
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.

KS
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 708
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.

AN
  • Rejestracja: dni
  • Ostatnio: dni
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?

Azarien
  • Rejestracja: dni
  • Ostatnio: dni
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.

enedil
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 1028
0

Możesz zrobić też

Kopiuj
inline void SomeGlobalMethod()
{
}

i zadziała tak jak chciałeś.

RO
  • Rejestracja: dni
  • Ostatnio: dni
  • 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.

Marius.Maximus
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 2201
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 ?

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.