Wszystkie klasy, które do tej pory napisaliśmy, były na tyle proste, że udało nam się w stanie zaimplementować funkcje składowe bezpośrednio w samej definicji klasy. Oto na przykład prosta Date klasa, w której wszystkie funkcje składowe są zdefiniowane w Date definicji klasy:
#include <iostream>
class Date
{
private:
int m_year{};
int m_month{};
int m_day{};
public:
Date(int year, int month, int day)
: m_year { year }
, m_month { month }
, m_day { day}
{
}
void print() const { std::cout << "Date(" << m_year << ", " << m_month << ", " << m_day << ")\n"; }
int getYear() const { return m_year; }
int getMonth() const { return m_month; }
int getDay() const { return m_day; }
};
int main()
{
Date d { 2015, 10, 14 };
d.print();
return 0;
}Jednak w miarę jak klasy stają się dłuższe i bardziej skomplikowane, posiadanie wszystkich definicji funkcji składowych w klasie może utrudnić zarządzanie klasą i pracę z nią. Korzystanie z już napisanej klasy wymaga jedynie zrozumienia jej publicznego interfejsu (funkcji elementu publicznego), a nie tego, jak klasa działa pod maską. Implementacje funkcji składowych zaśmiecają interfejs publiczny szczegółami, które nie są istotne dla faktycznego korzystania z klasy.
Aby rozwiązać ten problem, C++ pozwala nam oddzielić część klasy zawierającą „deklarację” od części „implementacyjnej” poprzez zdefiniowanie funkcji składowych poza definicją klasy.
Oto ta sama Date klasa co powyżej, z konstruktorem i print() funkcjami składowymi zdefiniowanymi poza definicją klasy. definicja klasy. Należy zauważyć, że prototypy tych funkcji składowych nadal istnieją w definicji klasy (ponieważ funkcje te muszą być zadeklarowane jako część definicji typu klasy), ale faktyczna implementacja została przeniesiona na zewnątrz:
#include <iostream>
class Date
{
private:
int m_year{};
int m_month{};
int m_day{};
public:
Date(int year, int month, int day); // constructor declaration
void print() const; // print function declaration
int getYear() const { return m_year; }
int getMonth() const { return m_month; }
int getDay() const { return m_day; }
};
Date::Date(int year, int month, int day) // constructor definition
: m_year{ year }
, m_month{ month }
, m_day{ day }
{
}
void Date::print() const // print function definition
{
std::cout << "Date(" << m_year << ", " << m_month << ", " << m_day << ")\n";
};
int main()
{
const Date d{ 2015, 10, 14 };
d.print();
return 0;
}Funkcje składowe można definiować poza definicją klasy, tak samo jak funkcje niebędące składowymi. Jedyna różnica polega na tym, że nazwy funkcji składowych musimy poprzedzać nazwą typu klasy (w tym przypadku Date::), aby kompilator wiedział, że definiujemy element tego typu klasy, a nie osobę niebędącą członkiem.
Zauważ, że funkcje dostępu pozostawiliśmy zdefiniowane w definicji klasy. Ponieważ funkcje dostępu to zazwyczaj tylko jedna linia, zdefiniowanie tych funkcji w definicji klasy powoduje minimalny bałagan, podczas gdy przeniesienie ich poza definicję klasy spowodowałoby wiele dodatkowych linii kodu. Z tego powodu definicje funkcji dostępu (i innych trywialnych, jednowierszowych funkcji) często pozostawia się w definicji klasy.
Umieszczanie definicji klas w pliku nagłówkowym
Jeśli zdefiniujesz klasę w pliku źródłowym (.cpp), tej klasy można używać tylko w tym konkretnym pliku źródłowym. W większych programach często zdarza się, że będziemy chcieli używać klas, które piszemy w wielu plikach źródłowych.
W lekcji 2.11 — Pliki nagłówkowe, dowiedziałeś się, że możesz umieścić deklaracje funkcji w plikach nagłówkowych. Następnie możesz #include te deklaracje funkcji do wielu plików kodu (lub nawet wielu projektów). Zajęcia nie różnią się od siebie. Definicje klas można umieścić w plikach nagłówkowych, a następnie #załączyć do innych plików, które chcą używać danego typu klasy.
W przeciwieństwie do funkcji, które wymagają jedynie deklaracji forward, do użycia, kompilator zazwyczaj musi zobaczyć pełną definicję klasy (lub dowolnego typu zdefiniowanego przez program), aby typ mógł zostać użyty. Dzieje się tak dlatego, że kompilator musi zrozumieć, w jaki sposób deklarowane są elementy członkowskie, aby mieć pewność, że są one używane prawidłowo, i musi być w stanie obliczyć, jak duże są obiekty tego typu, aby utworzyć ich instancję. Dlatego nasze pliki nagłówkowe zwykle zawierają pełną definicję klasy, a nie tylko deklarację klasy w przód.
Nazywanie nagłówka klasy i plików z kodem
Najczęściej klasy są definiowane w plikach nagłówkowych o tej samej nazwie co klasa, a wszelkie funkcje składowe zdefiniowane poza klasą są umieszczane w pliku .cpp o tej samej nazwie co klasa.
Oto nasza klasa Date, ponownie podzielona na Plik .cpp i .h:
Date.h:
#ifndef DATE_H
#define DATE_H
class Date
{
private:
int m_year{};
int m_month{};
int m_day{};
public:
Date(int year, int month, int day);
void print() const;
int getYear() const { return m_year; }
int getMonth() const { return m_month; }
int getDay() const { return m_day; }
};
#endifDate.cpp:
#include "Date.h"
Date::Date(int year, int month, int day) // constructor definition
: m_year{ year }
, m_month{ month }
, m_day{ day }
{
}
void Date::print() const // print function definition
{
std::cout << "Date(" << m_year << ", " << m_month << ", " << m_day << ")\n";
};Teraz dowolny inny plik nagłówkowy lub kod, który chce użyć Date klasa może po prostu #include "Date.h". Zauważ, że Date.cpp należy także wkompilować do dowolnego projektu używającego Date.h , aby linker mógł połączyć wywołania funkcji składowych z ich definicjami.
Najlepsza praktyka
Wolę umieścić definicje klas w pliku nagłówkowym o tej samej nazwie co klasa. Trywialne funkcje składowe (takie jak funkcje dostępu, konstruktory z pustymi treściami itp.) można zdefiniować w definicji klasy.
Wolę definiować nietrywialne funkcje składowe w pliku źródłowym o tej samej nazwie co klasa.
Czy zdefiniowanie klasy w pliku nagłówkowym nie narusza reguły jednej definicji, jeśli nagłówek jest #included więcej niż raz?
Typy są wyłączone z części reguły jednej definicji (ODR), która mówi, że w każdym programie można mieć tylko jedną definicję. Dlatego nie ma problemu #włączenia definicji klas do wielu jednostek tłumaczeniowych. Gdyby tak było, klasy nie byłyby zbyt przydatne.
Włączanie definicji klasy więcej niż raz do pojedynczej jednostki tłumaczeniowej nadal stanowi naruszenie ODR. Jednakże osłony nagłówka (lub #pragma once) zapobiegną temu zjawisku.
Wbudowane funkcje członkowskie
Funkcje członkowskie nie są wyłączone z ODR, więc możesz się zastanawiać, w jaki sposób unikamy naruszeń ODR, gdy funkcje składowe są zdefiniowane w pliku nagłówkowym (który może następnie zostać włączony do więcej niż jednej jednostki tłumaczeniowej).
Funkcje składowe zdefiniowane wewnątrz definicja klasy to domyślnie wbudowane. Funkcje wbudowane są zwolnione z jednej definicji na część programu w regule jednej definicji.
Funkcje składowe zdefiniowane poza w definicji klasy nie są domyślnie wbudowane (i dlatego podlegają jednej definicji na część programu w regule jednej definicji). Dlatego takie funkcje są zwykle definiowane w pliku z kodem (gdzie będą miały tylko jedną definicję w całym programie).
Alternatywnie, funkcje składowe zdefiniowane poza definicją klasy można pozostawić w pliku nagłówkowym, jeśli są wbudowane (przy użyciu słowa kluczowego inline ). Oto nasz nagłówek Date.h z funkcjami składowymi zdefiniowanymi poza klasą oznaczonymi jako inline:
Date.h:
#ifndef DATE_H
#define DATE_H
#include <iostream>
class Date
{
private:
int m_year{};
int m_month{};
int m_day{};
public:
Date(int year, int month, int day);
void print() const;
int getYear() const { return m_year; }
int getMonth() const { return m_month; }
int getDay() const { return m_day; }
};
inline Date::Date(int year, int month, int day) // now inline
: m_year{ year }
, m_month{ month }
, m_day{ day }
{
}
inline void Date::print() const // now inline
{
std::cout << "Date(" << m_year << ", " << m_month << ", " << m_day << ")\n";
};
#endifThis Date.h można bez problemu włączyć do wielu jednostek tłumaczeniowych.
Kluczowa informacja
Funkcje zdefiniowane w definicji klasy są domyślnie inline, co umożliwia #włączenie ich do wielu plików kodu bez naruszania ODR.
Funkcje zdefiniowane poza definicją klasy nie są domyślnie wbudowane. Można je utworzyć inline za pomocą inline słowo kluczowe.
wbudowanego rozwinięcia funkcji składowych
Kompilator musi widzieć pełną definicję funkcji, aby móc przeprowadzić rozwinięcie wbudowane. Najczęściej takie funkcje (np. funkcje dostępu) definiowane są wewnątrz definicji klasy. Jeśli jednak chcesz zdefiniować funkcję składową poza definicją klasy, ale nadal chcesz, aby kwalifikowała się do rozwijania wbudowanego, możesz zdefiniować ją jako funkcję wbudowaną tuż pod definicją klasy (w tym samym pliku nagłówkowym). W ten sposób definicja funkcji będzie dostępna dla każdego, kto #uwzględni nagłówek.
Dlaczego więc nie umieścić wszystkiego w pliku nagłówkowym?
Możesz ulec pokusie, aby umieścić wszystkie definicje funkcji składowych w pliku nagłówkowym, albo wewnątrz definicji klasy, albo jako funkcje wbudowane poniżej definicji klasy. Chociaż to się skompiluje, ma to kilka wad.
Po pierwsze, jak wspomniano powyżej, zdefiniowanie elementów wewnątrz definicji klasy zaśmieca definicję klasy.
Po drugie, jeśli zmienisz jakikolwiek kod w nagłówku, będziesz musiał ponownie skompilować każdy plik zawierający ten nagłówek. Może to wywołać efekt domina, w którym jedna drobna zmiana powoduje konieczność ponownej kompilacji całego programu. Koszt ponownej kompilacji może się znacznie różnić: zbudowanie małego projektu może zająć tylko minutę lub mniej, podczas gdy duży projekt komercyjny może zająć wiele godzin.
I odwrotnie, jeśli zmienisz kod w pliku .cpp, wystarczy ponownie skompilować tylko ten plik .cpp. Dlatego też, mając wybór, ogólnie rzecz biorąc, jeśli to możliwe, lepiej jest umieścić nietrywialny kod w pliku .cpp.
Istnieje kilka przypadków, w których sensowne może być naruszenie najlepszej praktyki umieszczania definicji klasy w nagłówku, a nietrywialnych funkcji składowych w pliku kodu.
Po pierwsze, w przypadku małej klasy, która jest używana tylko w jednym pliku kodu i nie jest przeznaczona do ogólnego ponownego użycia, możesz preferować zdefiniowanie klasy (i wszystkich funkcje członkowskie) bezpośrednio w pojedynczym pliku .cpp, w którym jest używana. Pomaga to wyjaśnić, że klasa jest używana tylko w tym pojedynczym pliku i nie jest przeznaczona do szerszego użycia. Zawsze możesz później przenieść klasę do osobnego pliku nagłówka/kodu, jeśli później okaże się, że chcesz użyć jej w więcej niż jednym pliku lub stwierdzisz, że definicje klas i funkcji składowych zaśmiecają plik źródłowy.
Po drugie, jeśli klasa ma tylko niewielką liczbę nietrywialnych funkcji składowych, które prawdopodobnie nie ulegną zmianie, utworzenie pliku .cpp zawierającego tylko jedną lub dwie definicje może nie być warte wysiłku (ponieważ zaśmieca projekt). W takich przypadkach korzystne może być utworzenie funkcji inline i umieszczenie ich pod definicją klasy w nagłówku.
Po trzecie, we współczesnym C++ klasy lub biblioteki są coraz częściej dystrybuowane jako „tylko nagłówek”, co oznacza, że cały kod klasy lub biblioteki jest umieszczany w pliku nagłówkowym. Robi się to przede wszystkim po to, aby ułatwić dystrybucję i używanie takich plików, ponieważ wystarczy #included nagłówek, podczas gdy plik z kodem musi zostać jawnie dodany do każdego projektu, który go używa, aby można go było skompilować. Jeśli celowo tworzysz klasę lub bibliotekę zawierającą tylko nagłówek do dystrybucji, wszystkie nietrywialne funkcje składowe można inline umieścić w pliku nagłówkowym pod definicją klasy.
Wreszcie, w przypadku klas szablonowych, szablonowe funkcje składowe zdefiniowane poza klasą są prawie zawsze zdefiniowane w pliku nagłówkowym, pod definicją klasy. Podobnie jak funkcje szablonów niebędące członkami, kompilator musi zobaczyć pełną definicję szablonu, aby go utworzyć. Funkcje składowe szablonów omówimy w lekcji 15.5 — Szablony klas z funkcjami składowymi.
Nota autora
W przyszłych lekcjach większość naszych klas będzie zdefiniowana w pojedynczym pliku .cpp, a wszystkie funkcje zostaną zaimplementowane bezpośrednio w definicji klasy. Dzieje się tak, aby przykłady były zwięzłe i łatwe do samodzielnego skompilowania. W prawdziwych projektach znacznie częściej klasy są umieszczane w ich własnym kodzie i plikach nagłówkowych i należy się do tego przyzwyczaić.
Domyślne argumenty dla funkcji składowych
W lekcji 11.5 -- Domyślne argumenty, omówiliśmy najlepsze praktyki dotyczące domyślnych argumentów funkcji nieczłonkowskich: „Jeśli funkcja ma deklarację forward (szczególnie w pliku nagłówkowym), umieść argument domyślny W przeciwnym razie umieść domyślny argument w definicji funkcji.”
Ponieważ funkcje składowe są zawsze deklarowane (lub definiowane) jako część definicji klasy, najlepsza praktyka w przypadku funkcji składowych jest w rzeczywistości prostsza: zawsze umieszczaj domyślny argument w definicji klasy.
Najlepsza praktyka
Umieszczaj wszelkie domyślne argumenty funkcji składowych w definicji klasy.
Biblioteki
W swoich programach używałeś klas, które są część biblioteki standardowej, taka jak std::string. Aby skorzystać z tych klas, wystarczy #include odpowiedni nagłówek (taki jak #include <string>). Pamiętaj, że nie musisz dodawać żadnych plików kodu (takich jak string.cpp lub iostream.cpp) do swoich projektów.
Pliki nagłówkowe zawierają deklaracje wymagane przez kompilator w celu sprawdzenia, czy pisane programy są poprawne składniowo. Jednakże implementacje klas należących do standardowej biblioteki C++ są zawarte w prekompilowanym pliku, który jest dołączany automatycznie na etapie łączenia. Kodu nigdy nie widzisz.
Wiele pakietów oprogramowania typu open source udostępnia pliki .h i .cpp, które możesz wkompilować w swój program. Jednak większość bibliotek komercyjnych udostępnia tylko pliki .h i prekompilowany plik biblioteki. Jest ku temu kilka powodów: 1) Szybciej jest połączyć prekompilowaną bibliotekę niż ją ponownie kompilować za każdym razem, gdy jej potrzebujesz, 2) Pojedyncza kopia prekompilowanej biblioteki może być współużytkowana przez wiele aplikacji, podczas gdy skompilowany kod jest kompilowany do każdego pliku wykonywalnego, który go używa (zwiększając rozmiar pliku) oraz 3) Względy związane z własnością intelektualną (nie chcesz, aby ludzie kradli Twój kod).
Omawiamy, jak uwzględnić strony trzecie prekompilowane biblioteki do projektów w załączniku.
Chociaż prawdopodobnie nie będziesz tworzyć i dystrybuować własnych bibliotek przez jakiś czas, rozdzielenie klas na pliki nagłówkowe i pliki źródłowe jest nie tylko dobrą formą, ale także ułatwia tworzenie własnych niestandardowych bibliotek. Tworzenie własnych bibliotek wykracza poza zakres tych samouczków, ale oddzielenie deklaracji od implementacji jest warunkiem wstępnym, jeśli chcesz dystrybuować prekompilowane pliki binarne.
Czas quizu
Pytanie nr 1
h/t do czytelnika „recenzera lekcji uczenia się” w przypadku tych pytań quizowych.
Jaki jest cel definiowania funkcji składowych poza definicją klasy?
a) Aby definicja klasy była krótsza i łatwiejsza do zrozumienia zarządzaj.
b) Aby oddzielić interfejs publiczny od szczegółów implementacji.
c) Gdy zdefiniowano w pliku źródłowym, aby zminimalizować czas rekompilacji w przypadku zmiany szczegółów implementacji.
d) Wszystkie powyższe.
Jak zdefiniować funkcję składową poza definicją klasy?
a) Po prostu zdefiniuj funkcję składową pełnić funkcję normalnej funkcji bez przedrostka klasy.
b) Zdefiniuj funkcję z nazwą klasy poprzedzoną operatorem rozpoznawania zakresu (::).
c) Zadeklaruj funkcję wewnątrz definicji klasy i zdefiniuj ją na zewnątrz, używając słowa kluczowego znajomego.
d) Żadne z powyższych.
Kiedy w definicji klasy należy zdefiniować trywialne funkcje składowe?
a) Zawsze, aby poprawić wydajność.
b) Gdy funkcje mają pojedynczą linię kodu.
c) Kiedy funkcje są często wywoływane.
d) Nie zaleca się definiowania żadnych funkcji składowych wewnątrz klasy definicja.
Gdzie należy umieścić definicję klasy, aby ułatwić jej ponowne użycie w wielu plikach lub projektach?
a) W pliku .cpp o tej samej nazwie co klasa.
b) W oddzielnym pliku nagłówkowym o tej samej nazwie co klasa.
c) W pliku .cpp który zawiera plik nagłówkowy.
d) Gdziekolwiek w kodzie, o ile funkcje są zdefiniowane poza klasą.
Które z poniższych stwierdzeń jest prawdziwe w odniesieniu do reguły jednej definicji klas i funkcji składowych?
a) Zabrania definiowania klasy w nagłówku plik.
b) Umożliwia wielokrotne włączenie definicji klasy do tego samego pliku.
c) Funkcje składowe zdefiniowane w definicji klasy są wyłączone z zasady jednej definicji.
d) Nietrywialne funkcje składowe powinny być zawsze definiowane w pliku nagłówkowym.

