Poznajemy dyrektywę #include na nowo
Wraz z rozwojem każdego programu kodu przybywa, a poruszanie się po nim staje się coraz bardziej uciążliwe ze względu na jego długość. Z pewnością starasz się grupować tematycznie większość funkcji w programie, jednak i to niewiele daje, gdy przychodzi pracować z kodem, który ma co najmniej 1000 wierszy. Z pomocą przychodzi tu dyrektywa #include, którą już miałeś okazję niejednokrotnie poznać, stosując ją nawet do najprostszego programu.
Język C++ to zbiór logicznych zasad umożliwiających programowanie. Same zasady umożliwiają zarządzać danymi, jednak nie są one wystarczające do tego, aby wykorzystywać w łatwy sposób możliwości sprzętowe komputera. Ponieważ język C++ umożliwia łatwe organizowanie danych, to oczywistym też jest, że równie potrzebnym elementem jest interfejs, umożliwiający prezentację danych oraz interfejs reagujący na wszystkie urządzenia, jakie posiadamy w komputerze (np. mysz, klawiaturę, kartę graficzną, kartę sieciową itd.).
Zadaniem dyrektywy #include jest umożliwienie łatwego wykorzystywania zasobów sprzętowych komputera, poprzez dołączanie plików nagłówkowych bibliotek, które są odpowiedzialne za komunikację z różnymi urządzeniami. Innymi słowy za każdym razem, gdy korzystałeś z dyrektywy #include, dołączałeś do swojego programu interfejs umożliwiający łatwy dostęp do wybranych zasobów komputera. Za pomocą tego polecenia będziesz mógł w łatwy sposób pisać serwery TCP/UDP, używać OpenGL'a, czy też innych modułów umożliwiających łatwy dostęp do sprzętowych zasobów komputera.
Niniejsza dyrektywa pomimo, iż jest tak prosta w użyciu jest potężnym narzędziem w ręku programisty. Oprócz dołączania istniejących bibliotek, pozwala Ci ona dołączać własne biblioteki, które wkrótce sam zaczniesz pisać. Dzięki tej własności będziesz mógł zorganizować swój kod źródłowy lepiej, a wszelkie modyfikacje kodu staną się szybsze i łatwiejsze.
Ustawianie ścieżki do pliku nagłówkowego
Do tej pory gdy chciałeś dołączyć bibliotekę do programu, stosowałeś tylko i wyłącznie zapis
#include <ścieżka do pliku>. Użycie ostrych nawiasów
<...> informuje kompilator, aby przeszukiwał domyślne ścieżki, w których znajdują się pliki nagłówkowe. Ścieżki te są ustawione w środowisku Code::Blocks, które wskazują na katalogi w których znajdują się wszystkie standardowe biblioteki. Jeśli chcesz sprawdzić jakie ścieżki są domyślnie dołączane podczas kompilacji programów wejdź w następujące ustawienia:
Lista którą widzisz, to zbiór katalogów, które są przeszukiwane w celu odnalezienia odpowiedniego pliku nagłówkowego. Jeśli kompilator nie odnajdzie żądanego pliku nagłówkowego, kompilator zwróci błąd informując Cię, że pliku o podanej nazwie nie znaleziono.
Drugą metodą na dołączanie plików nagłówkowych jest wykorzystanie zapisu
#include "ścieżka do pliku". Zapis z użyciem podwójnych apostrofów informuje kompilator, że plik nagłówkowy ma być poszukiwany tylko i wyłącznie względem aktualnego katalogu, w którym znajduje się nasz projekt.
Rozszerzenia plików i ich znaczenie
Na przestrzeni lat język C, a od jakiegoś czasu również i C++ wypracowały sobie nazwy rozszerzeń dla plików, które automatycznie sugerują co się w nich znajduje i w jakim standardzie były pisane.
Pliki *.h *.hpp
Pliki *.h i *.hpp, są nazywane
plikami nagłówkowymi (ang. header files). Pierwszy z nich, tj. *.h oznacza, że był on pisany zgodnie ze standardami języka C. Rozszerzenie *.hpp natomiast mówi nam, że program pisany był zgodnie ze standardami języka C++. Jeśli masz kompilator C++ to nie musisz obawiać się o problemy z wykorzystywaniem bibliotek pisanych zarówno w C jak i C++, ponieważ standard C++ powstał w oparciu o C. Problemy możesz mieć natomiast w sytuacji odwrotnej, ponieważ mogą być zastosowane polecenia których język C po prostu nie zna.
Każdy plik nagłówkowy powinien zawierać tylko i wyłącznie interfejs. Przez słowo interfejs rozumiemy:
Dodatkowo dołączamy do niego niezbędne pliki nagłówkowe, jakie będą wykorzystywane przez daną bibliotekę. W pliku nagłówkowym nie umieszczamy natomiast bloków funkcji. Można powiedzieć po prostu, że w pliku nagłówkowym umieszczamy wszystko oprócz bloków funkcji.
Pliki *.c *.cpp
Pliki *.c i *.cpp nazywamy
plikami źródłowymi. Jak nietrudno domyślić się, *.c oznacza standard użytego języka C, natomiast *.cpp standard użytego języka C++. W plikach z takim rozszerzeniem umieszczamy tylko i wyłącznie definicje funkcji, czyli nazwę funkcji razem z jej ciałem (czyli blokiem funkcji).
Inne rozszerzenia plików
Język C++ nie narzuca nazw dla rozszerzeń plików. Zalecane jest jednak stosowanie się do wymienionych standardów, ponieważ są one jasne i zrozumiałe przez wszystkich programistów C i C++. Dodatkowo edytory często rozpoznają typ pliku po rozszerzeniach i w zależności od nich kolorują Tobie składnię.
Budowa pliku nagłówkowego *.h *.hpp
Istnieje co najmniej kilka wersji budowy plików nagłówkowych dla języka C i C++. Niektóre z nich nie działają jednak pod wszystkimi kompilatorami, dlatego też skupię się tylko i wyłącznie na jednej, która jest akceptowana przez wszystkie kompilatory.
#ifndef nazwaPliku_hpp
#define nazwaPliku_hpp
#endif
Opis użytych instrukcji preprocesora:
Zaprezentowany przykład czytasz następująco:
Wykorzystując instrukcje preprocesora zabezpieczasz bibliotekę przed wielokrotnym dołączaniem tego samego kodu do własnego programu. Jeśli go nie użyjesz, a dołączysz tą samą bibliotekę co najmniej dwukrotnie w programie (nawet w różnych plikach), otrzymasz błąd kompilacji nawet jeśli wszystko będzie poprawnie napisane. Nazwy zmiennych, które definiujesz za pomocą preprocesora muszą być unikatowe podczas kompilacji projektu dla każdego używanego pliku, tak więc w każdym pliku musi się znajdować inna nazwa zmiennej preprocesora. Najpopularniejszą metodą zapewnienia sobie unikatowych nazw zmiennych, jest używanie nazwy pliku dla zmiennej preprocesora. Nie jest to jednak obowiązek i masz tu praktycznie pełną dowolność. Nazwy zmiennych preprocesora mają takie same kryteria dla nazewnictwa jak zmienne języka C++.
Budowa pliku źródłowego *.c *.cpp
Plik źródłowy ma bardzo prostą budowę. Jedyne co musisz zrobić to dołączyć plik nagłówkowy pliku, który opisuje interfejs jaki znajduje się w tym pliku.
#include "nazwaPliku.hpp"
Błędy kompilacji
Jeśli będziesz próbował skompilować plik *.cpp i nie będzie w nim żadnych błędów, otrzymasz następujący błąd kompilacji:
[Linker error] undefined reference to `WinMain@16'
ld returned 1 exit status
Komunikat ten informuje Cię, że w programie nie ma 'ciała' programu, czyli głównej funkcji programu. Ponieważ pliki, które utworzyłeś nie mają być programem, tylko źródłem dołączanym do programu, który będzie zawierał funkcję main(), wskazane jest aby ten plik nie zawierał funkcji o którą 'doczepił się' kompilator.
Dołączając natomiast plik nagłówkowy (*.hpp) do programu głównego za pomocą dyrektywy #include, uzyskasz w pełni sprawny kod, który się kompiluje (pod warunkiem, że nie ma w nim innych błędów).
Utworzenie pliku źródłowego
By stworzyć plik nagłówkowy w Code::Blocks wystarczy utworzyć nowy pusty plik(
New ? Empty File), możemy też skorzystać ze skrótu klawiszowego
SHIFT+CTRL+N. Ukaże nam się komunikat,
czy dodać ten plik do aktywnego projektu, oczywiście potwierdzamy.
Następnie nadajemy mu odpowiednią nazwę dla pliku nagłówkowego
nazwaPliku.hpp i analogicznie dla pliku cpp
nazwaPliku.cpp. Ukaże nam się jeszcze informacja o tym czy pliki mają być obsługiwane podczas
Debuger czy
Release W tym wypadku można zaznaczyć obie opcje i potwierdzić
OK. W ten sposób do projektu dodaliśmy dwa puste pliki. Jeśli wszystko wykonałeś to powinieneś otrzymać taki układ dla plików w projekcie.
Przykład - Pliki źródłowe
#include <iostream>
#include <conio.h>
#include "nazwaPliku.hpp"
using namespace std;
int main()
{
cout << "Wynik dodawania to: " << dodajLiczby( 10, 15 ) << endl;
getch();
return( 0 );
}
#ifndef nazwaPliku_hpp
#define nazwaPliku_hpp
int dodajLiczby( int a, int b );
#endif
#include "nazwaPliku.hpp"
int dodajLiczby( int a, int b )
{
return( a + b );
}
Przykład 2 - Lekcja 18 Funkcje z strukturami
#include "nazwaPliku.hpp"
int main()
{
using std::cout;
using std::cin;
BokiTrojkata wprowadzDane;
WynikiTrojkata wyswietl;
cout << "Wprowadz dana bokow a, b i c: ";
while( cin >> wprowadzDane.bokA >> wprowadzDane.bokB >> wprowadzDane.bokC )
{
wyswietl = TwierdzeniePitagorasa( wprowadzDane );
WyswietlWyniki( wyswietl );
cout << "\nPodaj ponownie boki lub 'k' by wyjsc.\n";
}
return 0;
}
#ifndef nazwaPliku_hpp
#define nazwaPliku_hpp
#include <iostream>
struct BokiTrojkata
{
double bokA;
double bokB;
double bokC;
bool czy_prawda;
};
struct WynikiTrojkata
{
double Wynik1;
double Wynik2;
double Wynik3;
bool czy_prawda;
};
WynikiTrojkata TwierdzeniePitagorasa( BokiTrojkata pobiezboki );
void WyswietlWyniki( const WynikiTrojkata wynik );
#endif
#include "nazwaPliku.hpp"
#include <cmath>
WynikiTrojkata TwierdzeniePitagorasa( BokiTrojkata pobierzboki )
{
using std::pow;
WynikiTrojkata odpowiedz;
if(( pow( pobierzboki.bokA, 2 ) + pow( pobierzboki.bokB, 2 ) ) == pow( pobierzboki.bokC, 2 ) )
{
odpowiedz.Wynik1 =( pow( pobierzboki.bokA, 2 ) );
odpowiedz.Wynik2 =( pow( pobierzboki.bokB, 2 ) );
odpowiedz.Wynik3 =( pow( pobierzboki.bokC, 2 ) );
odpowiedz.czy_prawda = true;
} else {
odpowiedz.czy_prawda = false;
}
return odpowiedz;
}
void WyswietlWyniki( const WynikiTrojkata wynik )
{
using std::cout;
if( wynik.czy_prawda )
{
cout << "\nTo sa boki trojkata, a dodatkowo trojkata prostokatnego!\n"
<< "Udalo sie to ustalic dzieki twierdzeniu pitagorasa.\n"
<< "Bok a * a = " << wynik.Wynik1
<< "\tBok b * b = " << wynik.Wynik2
<< "\tBok c * c = " << wynik.Wynik3
<< "\nTwierdzenie pitagorasa to: (a*a) + (b*b) = (c*c)\n"
<< "\n";
} else
cout << "\nPodane boki nie tworza trojkata prostokatnego(lub zadnego innego)\n\n";
}
Przykład 3 - Lekcja 17 Budowa aplikacji opartej na funkcjach
#include "nazwaPliku.hpp"
int main()
{
using std::string;
const int LINIE = 6;
string LotyKlient[ LINIE ][ BILETY ];
int max_wierszy = 0;
int tab_wiersze[ 6 ] = { - 1, - 1, - 1, - 1, - 1, - 1 };
Menu();
while(( max_wierszy = OperacjeKasjera() ) != 27 ) {
if( max_wierszy != 27 && max_wierszy != - 1 && max_wierszy != 6 ) {
tab_wiersze[ max_wierszy ] = BazaLotow( LotyKlient, max_wierszy );
} else if( max_wierszy == 6 ) {
Wyswietl_Dane( LotyKlient, LINIE, tab_wiersze, LINIE );
}
Menu();
}
return 0;
}
#ifndef nazwaPliku_hpp
#define nazwaPliku_hpp
#include <iostream>
#include <conio.h>
#include <string>
const int BILETY = 10;
void Kursor( int, int );
void Menu();
void Wyswietl_Dane( const std::string[][ BILETY ], int, const int[], int );
int BazaLotow( std::string tabela1[][ BILETY ], int indeks1 );
int OperacjeKasjera();
#endif
#include "nazwaPliku.hpp"
#include "ddtconsole.h"
void Wyswietl_Dane( std::string const tabela1[][ BILETY ], int indeks1, const int tabela2[], int indeks2 )
{
using namespace ddt::console;
using std::cout;
std::string Miasta[ 6 ] = { "Barcelona", "Londyn", "Paryz",
"New York", "Moskwa", "Tokyo" };
clrscr();
gotoxy( 5, 5 );
textcolor( 10 );
cout << "Oto Dane dla linii lotniczych\n";
textcolor( 11 );
for( int i = 0; i < indeks2; i++ ) {
if( tabela2[ i ] != - 1 ) {
cout << "-----------------------------\n";
textcolor( 10 );
cout << Miasta[ i ] << "\n";
textcolor( 11 );
for( int j = 0; j <= tabela2[ i ]; j++ ) {
cout << tabela1[ i ][ j ] << "\n";
}
cout << "-----------------------------\n";
}
}
system( "pause" );
}
void Kursor( int pion, int poziom )
{
using namespace ddt::console;
using std::cout;
using std::endl;
gotoxy( pion, poziom );
textcolor( 7 );
cout << "->" << endl;
}
int BazaLotow( std::string tabela1[][ BILETY ], int indeks1 )
{
using namespace ddt::console;
using std::cin;
using std::cout;
std::string bufor;
int bilety;
int straznik = BILETY;
int licz = 0;
for( licz; licz < straznik; licz++ ) {
clrscr();
gotoxy( 20, 8 );
textcolor( 15 );
cout << "Literki 'q' i 'Q' - koncza wprowadzenie!\n\n";
cout << "Imie i nazwisko klienta: ";
getline( cin, tabela1[ indeks1 ][ licz ] );
if( tabela1[ indeks1 ][ licz ] == "q" || tabela1[ indeks1 ][ licz ] == "Q" ) {
tabela1[ indeks1 ][ licz ] = "\0";
return licz - 1;
}
gotoxy( 20, 12 );
cout << "Liczba ujemna przerywa wprowadzanie\n";
cout << "Ile biletow: ";
( cin >> bilety ).get();
if( !cin )
{
cin.clear();
gotoxy( 20, 19 );
cout << "Nie podano liczby!!! Restart!";
Sleep( 4000 );
return licz - 1;
}
else if( bilety <= 0 ) {
if( bilety == 0 ) {
bilety = 1;
} else {
tabela1[ indeks1 ][ licz ] = "\0";
return licz - 1;
}
} if( straznik >= bilety ) {
straznik -= bilety;
bufor = ", ilosc biletow: ";
tabela1[ indeks1 ][ licz ] += bufor;
bufor = 'a';
std::sprintf(( char * ) bufor.c_str(), "%d", bilety );
if( bilety == 10 ) {
tabela1[ indeks1 ][ licz ] += bufor;
tabela1[ indeks1 ][ licz ] += '0';
} else
tabela1[ indeks1 ][ licz ] += bufor;
} else {
cout << "\nPrzekroczono liczbe biletow!!!\n"
<< "Zostalo ich tylko - " << straznik;
Sleep( 5000 );
licz -= 1;
}
}
return licz - 1;
}
int OperacjeKasjera() {
using namespace ddt::console;
using std::cout;
using std::cin;
using std::endl;
int znak;
int pion = 17;
int poziom = 12;
do
{
Kursor( pion, poziom );
znak = getch();
if( znak == 224 || znak == 0 )
znak = getch();
switch( znak )
{
case 80:
{
gotoxy( pion, poziom );
cout << " " << endl;
if( poziom == 14 )
poziom = 11;
else
poziom++;
} break;
case 72:
{
gotoxy( pion, poziom );
cout << " " << endl;
if( poziom == 11 )
poziom = 14;
else
poziom--;
} break;
case 77:
{
gotoxy( pion, poziom );
cout << " " << endl;
if( pion == 17 ) {
pion = 37;
} else
pion -= 20;
} break;
case 75:
{
gotoxy( pion, poziom );
cout << " " << endl;
if( pion == 37 )
pion = 17;
else
pion += 20;
} break;
case 13:
{
if( poziom == 11 && pion == 17 )
return 0;
else if( poziom == 12 && pion == 17 )
return 1;
else if( poziom == 13 && pion == 17 )
return 2;
else if( poziom == 14 && pion == 17 )
return 3;
else if( poziom == 11 && pion == 37 )
return 4;
else if( poziom == 12 && pion == 37 )
return 5;
else if( poziom == 13 && pion == 37 )
return 6;
else if( poziom == 14 && pion == 37 )
return 27;
} break;
}
} while( znak != 27 );
return znak;
}
void Menu() {
using namespace ddt::console;
using std::cout;
using std::endl;
clrscr();
gotoxy( 30, 8 );
textcolor( 10 );
cout << "Linie AirDDT" << endl;
gotoxy( 20, 11 );
textcolor( 11 );
cout << "1 - Barcelona" << endl;
gotoxy( 20, 12 );
textcolor( 12 );
cout << "2 - Londyn" << endl;
gotoxy( 20, 13 );
textcolor( 13 );
cout << "3 - Paryz" << endl;
gotoxy( 20, 14 );
textcolor( 14 );
cout << "4 - New York" << endl;
gotoxy( 40, 11 );
textcolor( 15 );
cout << "5 - Moskwa" << endl;
gotoxy( 40, 12 );
textcolor( 13 );
cout << "6 - Tokyo" << endl;
gotoxy( 40, 13 );
textcolor( 11 );
cout << "7 - Wszystkie linie" << endl;
gotoxy( 40, 14 );
textcolor( 10 );
cout << "8 - Przerwa" << endl;
gotoxy( 30, 20 );
textcolor( 11 );
cout << "Esc - Zakoncz program" << endl;
gotoxy( 30, 21 );
textcolor( 11 );
cout << "Enter - Zatwierdzanie" << endl;
}
Pliki nagłówkowe a źródłowe
Tworzenie projektów kilku plikowych jak widać nie jest trudne. W plikach nagłówkowych
nazwaPliku.hpp powinnyśmy zawierać następujące elementy języka C++:
Więc jak pewnie się domyślasz w pliku
nazwaPliku.cpp znajdują się funkcje zadeklarowane w pliku nagłówkowym. W ten sposób tworzy się spójność pomiędzy tymi plikami a plikiem głównym
main.cpp.
Dzięki takiemu postępowaniu możemy użyć funkcji i mechanizmów zawartych w tych plikach w innych aplikacjach, bez konieczności kopiowania kodu wystarczy podpiąć odpowiednie pliki. Więc warto poświęcić trochę czasu tej tematyce, ponieważ dobrze napisane mechanizmy w plikach nagłówkowych mogą Wam się przydać w przyszłości, bez konieczności poświęcania kolejnego czasu na ich budowę.
Ćwiczenia
Znajdź wybrane zadanie z kursów i podziel je na kilka plików. Sam zdecyduj jakie to mają być zadania. Uwzględnij przy tym by nie dzielić programów, które tego nie wymagają.