Dlaczego możesz mieć definicję metody w pliku nagłówkowym w C ++, skoro w C nie możesz?

23

W C nie można mieć definicji / implementacji funkcji w pliku nagłówkowym. Jednak w C ++ możesz mieć pełną implementację metody w pliku nagłówkowym. Dlaczego zachowanie jest inne?

Joshua Partogi
źródło

Odpowiedzi:

28

W C, jeśli zdefiniujesz funkcję w pliku nagłówka, wówczas funkcja ta pojawi się w każdym skompilowanym module zawierającym ten plik nagłówka, a dla funkcji zostanie wyeksportowany symbol publiczny. Jeśli więc dodatek funkcji jest zdefiniowany w nagłówku.h, a zarówno foo.c, jak i bar.c zawierają nagłówek.h, wówczas zarówno foo.o, jak i bar.o będą zawierać kopie dodatku.

Kiedy przejdziesz do połączenia tych dwóch plików obiektowych razem, linker zobaczy, że sumowanie symboli jest zdefiniowane więcej niż raz i nie pozwoli na to.

Jeśli zadeklarujesz funkcję jako statyczną, żaden symbol nie zostanie wyeksportowany. Pliki obiektowe foo.o i bar.o nadal będą zawierać osobne kopie kodu dla funkcji i będą mogły z nich korzystać, ale linker nie będzie w stanie zobaczyć żadnej kopii funkcji, więc nie będę narzekać. Oczywiście żaden inny moduł również nie będzie w stanie zobaczyć tej funkcji. A twój program będzie rozdęty dwiema identycznymi kopiami tej samej funkcji.

Jeśli zadeklarujesz funkcję tylko w pliku nagłówkowym, ale nie zdefiniujesz jej, a następnie zdefiniujesz tylko w jednym module, linker zobaczy jedną kopię funkcji, a każdy moduł w twoim programie będzie mógł ją zobaczyć i Użyj tego. Twój skompilowany program będzie zawierał tylko jedną kopię funkcji.

Tak więc możesz mieć definicję funkcji w pliku nagłówkowym w C, jest to po prostu zły styl, zła forma i ogólnie zły pomysł.

(Przez „zadeklaruj” mam na myśli zapewnienie prototypu funkcji bez ciała; przez „zdefiniowanie” mam na myśli faktyczny kod ciała funkcji; jest to standardowa terminologia w języku C.)

David Conrad
źródło
2
To nie jest taki zły pomysł - tego rodzaju rzeczy można znaleźć nawet w nagłówkach GNU Libc.
SK-logic
ale co z idiomatycznym zawijaniem pliku nagłówkowego w warunkowej dyrektywie kompilacji? Następnie, nawet z funkcją zadeklarowaną AND w nagłówku, zostanie załadowana tylko raz. Jestem nowy w C, więc mogę się nieporozumieć.
user305964,
2
@papiro Problem polega na tym, że owijanie chroni tylko podczas jednego uruchomienia kompilatora. Więc jeśli foo.c jest skompilowany do foo.o w jednym uruchomieniu, bar.c do bar.o w innym, a foo.o i bar.o są połączone w a.out w trzecim (jak to typowe), to opakowanie nie zapobiega wielu jego wystąpieniom, po jednym w każdym pliku obiektowym.
David Conrad
Czy opisany tutaj problem nie #ifndef HEADER_Hpowinien zapobiec?
Robert Harvey,
27

C i C ++ zachowują się bardzo podobnie pod tym względem - możesz mieć inlinefunkcje w nagłówkach. W C ++ każda metoda, której treść znajduje się w definicji klasy, jest niejawna inline. Jeśli chcesz zrobić to samo w C, zadeklaruj funkcje static inline.

Simon Richter
źródło
zadeklaruj funkcjestatic inline ” ... a nadal będziesz mieć wiele kopii funkcji w każdej jednostce tłumaczeniowej, która z niej korzysta. W C ++ z static inlineniefunkcją będziesz miał tylko jedną kopię. Aby faktycznie mieć implementację w nagłówku w C, musisz 1) oznaczyć implementację jako inline(np. inline void func(){do_something();}) I 2) faktycznie powiedzieć, że ta funkcja będzie w określonej jednostce tłumaczeniowej (np void func();.).
Ruslan
6

Pojęcie pliku nagłówkowego wymaga małego wyjaśnienia:

Albo podasz plik w wierszu poleceń kompilatora, albo zrobisz „#include”. Większość kompilatorów akceptuje plik poleceń z rozszerzeniem c, C, cpp, c ++ itp. Jako plik źródłowy. Zazwyczaj zawierają one jednak opcję wiersza polecenia, aby umożliwić użycie dowolnego dowolnego rozszerzenia do pliku źródłowego.

Zasadniczo plik podany w wierszu poleceń nosi nazwę „Źródło”, a dołączony plik nazywa się „Nagłówek”.

Krok preprocesora faktycznie bierze je wszystkie i sprawia, że ​​wszystko wydaje się kompilatorowi jak jeden duży plik. To, co było w nagłówku lub w źródle, nie jest w tym momencie istotne. Zwykle istnieje opcja kompilatora, który może wyświetlić dane wyjściowe tego etapu.

Tak więc dla każdego pliku, który został podany w linii poleceń kompilatora, kompilator otrzymuje ogromny plik. Może zawierać kod / dane, które zajmują pamięć i / lub tworzą symbol, do którego można się odwoływać z innych plików. Teraz każdy z nich wygeneruje obraz „obiektu”. Linker może nadać „duplikatowi symbol”, jeśli ten sam symbol zostanie znaleziony w więcej niż dwóch plikach obiektowych, które są ze sobą połączone. Być może to jest powód; nie zaleca się umieszczania kodu w pliku nagłówkowym, który może tworzyć symbole w pliku obiektowym.

„Inline” są zwykle wstawiane. Ale podczas debugowania mogą nie być wstawiane. Dlaczego więc linker nie podaje wielokrotnie zdefiniowanych błędów? Proste ... Są to „słabe” symbole i tak długo, jak wszystkie dane / kod słabego symbolu ze wszystkich obiektów mają ten sam rozmiar i treść, połączony zachowa jedną kopię i upuści kopię z innych obiektów. To działa.

Vrdhn
źródło
3

Możesz to zrobić w C99: inlinefunkcje są zapewnione gdzie indziej, więc jeśli funkcja nie została wstawiona, jej definicja jest tłumaczona na deklarację (tzn. Implementacja jest odrzucana). I oczywiście możesz użyć static.

Logika SK
źródło
1

C ++ standardowe cytaty

C ++ 17 N4659 standardowy projekt 10.1.6 „specyfikatorem inline” mówi, że metody są niejawnie inline:

4 Funkcja zdefiniowana w definicji klasy jest funkcją wbudowaną.

a następnie w dalszej części widzimy, że metody wbudowane nie tylko mogą, ale muszą być zdefiniowane we wszystkich jednostkach tłumaczeniowych:

6 Funkcja wbudowana lub zmienna powinna być zdefiniowana w każdej jednostce tłumaczenia, w której jest używana odr i powinna mieć dokładnie taką samą definicję w każdym przypadku (6.2).

Jest to również wyraźnie wspomniane w nocie 12.2.1 „Funkcje członkowskie”:

1 Funkcja składowa może być zdefiniowana (11.4) w definicji klasy, w którym to przypadku jest to wbudowana funkcja składowa (10.1.6) [...]

3 [Uwaga: W programie może istnieć co najwyżej jedna definicja nieliniowej funkcji składowej. W programie może znajdować się więcej niż jedna definicja wbudowanej funkcji składowej. Patrz 6.2 i 10.1.6. - uwaga końcowa]

Implementacja GCC 8.3

main.cpp

struct MyClass {
    void myMethod() {}
};

int main() {
    MyClass().myMethod();
}

Kompiluj i przeglądaj symbole:

g++ -c main.cpp
nm -C main.o

wynik:

                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 W MyClass::myMethod()
                 U __stack_chk_fail
0000000000000000 T main

następnie widzimy, man nmże MyClass::myMethodsymbol jest oznaczony jako słaby w plikach obiektowych ELF, co oznacza, że ​​może pojawić się w plikach wielu obiektów:

„W” „w” Symbol jest słabym symbolem, który nie został specjalnie oznaczony jako symbol słabego obiektu. Kiedy słaby zdefiniowany symbol jest połączony z normalnie zdefiniowanym symbolem, normalnie zdefiniowany symbol jest używany bez błędu. Kiedy słaby niezdefiniowany symbol jest połączony, a symbol nie jest zdefiniowany, wartość symbolu jest określana w sposób specyficzny dla systemu bez błędu. W niektórych systemach wielkie litery wskazują, że określono wartość domyślną.

Ciro Santilli
źródło
-4

Prawdopodobnie z tego samego powodu, dla którego musisz umieścić pełną implementację metody w definicji klasy w Javie.

Mogą wyglądać podobnie, z nawiasami kwadratowymi i wieloma takimi samymi słowami kluczowymi, ale są to różne języki.

Paul Butcher
źródło
1
Nie, naprawdę jest prawdziwa odpowiedź, a jednym z głównych celów C ++ była zgodność wsteczna z C.
Ed S.
4
Nie, został zaprojektowany tak, aby miał „wysoki stopień kompatybilności z C” i „brak darmowych niezgodności z C”. (oba z Stroustrup). Zgadzam się, że można udzielić bardziej szczegółowej odpowiedzi, aby podkreślić, dlaczego ta szczególna niezgodność nie jest nieuzasadniona. Zapraszam do dostarczenia jednego.
Paul Butcher,
Zrobiłbym to, ale Simon Richter już to zrobił, kiedy to opublikowałem. Możemy spierać się o różnicę między „kompatybilnością wsteczną” a „zgodnością wysokiego stopnia C”, ale faktem jest, że ta odpowiedź jest nieprawidłowa. Ostatnia instrukcja byłaby poprawna, gdybyśmy porównali C # i C ++, ale nie tak bardzo z C i C ++.
Ed S.