Praktyka Programowania projekt instrukcja cz. 1 K.M. Ocetkiewicz 28 IX 2011 Wprowadzenie do środowiska Visual Studio Tworzenie nowego projektu Aby móc skompilować jakikolwiek program, należy najpierw utworzyć projekt, wybierając polecenie New->Project... z menu File: Rysunek 1 Menu File Następnie należy wybrać szablon typ projektu Visual C++ / Win32 oraz szablon Win32 Console Application (UWAGA: w zależności od wersji lub sposobu instalacji Visual Studio drzewo typów projektów może wyglądać inaczej, np. nasz szablon może znajdować się w Other Languages / C++; ważne, by typ projektu zawierał C++ oraz Win32). Należy także wypełnić pola Name (nazwa projektu), Location (miejsce na dysku, gdzie pliki projektu będą zapisywane) oraz Solution (Solution to pewien pojemnik grupujący różne projekty; my w solution będziemy przechowywali tylko jeden projekt, ale, niestety, jakieś solution zawsze musi być): Rysunek 2 Nowy projekt okno dialogowe Następnie klikamy OK, co przenosi nas do następującego okna dialogowego:
Rysunek 3 Nowy projekt pierwsza strona Klikamy Next >, w poniższym oknie włączamy opcję Empty project i naciskamy Finish. Rysunek 4 Nowy projekt strona druga W ten sposób utworzyliśmy nowy projekt. Aby móc cokolwiek napisać, musimy dodać do naszego projektu nowy plik. Klikamy prawym przyciskiem myszy na Source files w poddrzewie naszego projektu i z menu kontekstowego wybieramy Add -> New Item...
Rysunek 5 Dodawanie nowego pliku menu kontekstowe Po czym wybieramy C++ File (.cpp), wpisując odpowiednią nazwę w polu Name. Rysunek 6 Dodawanie nowego pliku okno dialogowe Po naciśnięciu Add plik zostanie utworzony i będziemy mogli wypełnić go treścią. Budowanie, kompilacja, uruchamianie Po dwukrotnym kliknięciu nazwy pliku w drzewie projektów możemy edytować wybrany plik. drzewo projektów Rysunek 7 Drzewo projektów
Wprowadźmy następującą treść pliku: #include<stdio.h> int main() { return 0; }; Będzie to treść naszego programu. Aby uruchomić program, musimy go najpierw skompilować. Polecenia służące do kompilacji znajdują się w menu Build: Rysunek 8 Menu Build Polecenie Build służy do kompilacji. Proszę zauważyć, że po pomyślnej kompilacji druga nie zostanie wykonana, jeżeli zawartość naszych plików się nie zmieniła. Do wymuszenia kompilacji służy polecenie Rebuild polecenie to wykona kompilację nawet gdy nie jest ona konieczna. Polecenie Clean służy do usunięcia wyniku kompilacji i wszystkich plików tymczasowych (polecenie Rebuild jest w praktyce równoważne wykonaniu polecenia Clean a następnie Build). Pierwsze trzy polecenia dotyczą całego solution wybrana operacja zostanie wykonana na wszystkich projektach należących do solution. Kolejne trzy polecenia dotyczą pojedynczego, aktywnego projektu. Jeżeli solution zawiera kilka projektów, aktywny projekt możemy ustawić klikając w drzewie projektów na wybranym projekcie i wybierając opcję Set as StartUp Project: Rysunek 9 Ustawianie aktywnego projektu Gdy kompilacja zakończy się sukcesem, możemy uruchomić nasz program. Możemy to zrobić ręcznie, odszukując na dysku utworzony plik wykonywalny (będzie on w katalogu <katalog_solution>\debug, gdzie katalog_solution to katalog, który podaliśmy w polu Location tworząc nowy projekt (zob. Rysunek 2). Drugi sposób to skorzystanie z menu Debug:
Rysunek 10 Menu Debug Mamy tu polecenie Start Debugging, które powoduje uruchomienie naszego programu z debuggerem (więcej o debuggerze poniżej) oraz Start Without Debugging które uruchamia nasz program bez debuggera, ale po jego zakończeniu nie zamknie natychmiast okna naszego programu, lecz poczeka na naciśnięcie klawisza. Jeżeli uruchamiamy nasz program by go sprawdzić i znaleźć ewentualne błędy, lepiej wybrać polecenie Start Debugging. Jeżeli chcemy obejrzeć rezultaty wykonania programu, lepszym pomysłem jest opcja Start Without Debugging. Oba polecenia możemy wydać nawet wtedy, gdy program nie jest skompilowany. W takiej sytuacji program zostanie automatycznie skompilowany i dopiero wtedy uruchomiony. W takiej sytuacji należy jednak zachować szczególną ostrożność, gdyż w przypadku wystąpienia błędu kompilacji pojawi się okno: Rysunek 11 Okno w którym NIE należy klikać Yes Jest to informacja, że wystąpiły błędy kompilacji i pytanie, czy w tej sytuacji uruchomić POPRZEDNIĄ wersję programu (tę, która kompilowała się bez błędów). W oknie tym należy wybrać No i poprawić błędy. Błędy Tworząc program będziemy popełniać błędy. Część z nich zostanie wykryta w trakcie kompilacji (będziemy je nazywać błędami kompilacji, compilation errors), pozostałe będziemy musieli znaleźć samodzielnie uruchamiając program i testując jego działanie (takie błędy będziemy nazywali błędami uruchomienia, runtime errors). W tym rozdziale omówimy błędy kompilacji, w następnym debugger narzędzie usprawniające samodzielne poprawianie błędów. Wydanie polecenia budowania (Build) powoduje skompilowanie projektu. Jeżeli kompilator znajdzie jakieś błędy, przedstawi je nam w postaci komunikatów w oknie Output lub Error List. Jeżeli jedno z tych okien nie jest widoczne, można je włączyć poleceniem Output z menu View lub poleceniem Other Windows -> Error List z menu View. Dwukrotne kliknięcie na komunikacie błędu w jednym z tych okien przeniesie nas do konkretnego miejsca w pliku, w którym błąd ten wystąpił. Okno Error List umożliwia także wybór rodzaju komunikatów, które będą widoczne.
Rysunek 12 Polecenia Output i Other Windows -> Error List okno Output Rysunek 13 Okno Output
okno Error List Rysunek 14 Okno Error List Komunikaty błędów zazwyczaj są jasno sformułowane i wyjaśniają co jest przyczyną błędu. Opis często występujących lecz mniej oczywistych błędów można znaleźć na końcu tego dokumentu. Oprócz błędów kompilator może zgłosić również ostrzerzenie (Warning). Jest to swego rodzaju uwaga, stwierdzenie przez kompilator, że z formalnego punktu widzenia program można skompilować, ale wskazana rzecz wygląda podejrzanie. Mimo, że kompilacja się powiedzie, poniższe ostrzeżenia trzeba potraktować poważnie i bezwzględnie należy poprawić kod, by nie wystąpiły (ich zignorowanie prowadzi do bardzo trudnych to poprawienia błędów uruchomienia np. program uruchomiony na jednym komputerze działa, na innym nie): 'nazwa_funkcji' : not all control paths return a value Ostrzeżenie to mówi, że w podanej funkcji istnieje ścieżka przez funkcję, która nie prowadzi do zwrócenia wyniku. Przykładem takiej funkcji jest: int fn(int x) { if(x == 3) return 2; }; Wartość zwrócona przez tę funkcję po wywołaniu z parametrem różnym od 3 jest nieokreślona. uninitialized local variable 'nazwa_zmiennej' used Podana zmienna jest użyta bez uprzedniego przypisania jej wartości. Zawartość takiej zmiennej jest nieokreślona a więc także wynik wszystkich obliczeń, które korzystają z tej wartości. Przykładowy fragment kodu, który powoduje to ostrzeżenie to: int x; int z; z = 3 * x; Wartość x w tym przypadku jest nieokreślona, zatem wartość z też będzie nieokreślona. Pliki tworzone przez Visual Studio Środowisko Visual Studio tworzy wiele pomocniczych plików. Katalog ze skompilowanym projektem, nawet jeżeli program jest praktycznie pusty, może zajmować ponad jeden megabajt a czasem nawet ponad 20MB. Większość z tego zajmują pliki pomocnicze. Nie są one konieczne do skompilowania programu ich usunięcie nie zniszczy nam projektu, nie spowoduje nawet wyświetlenia komunikatu informacyjnego (podczas kompilacji zostaną utworzone na nowo). Ich obecność usprawnia jednak pracę np. program kompiluje się
szybciej. W czasie pracy nad projektem dobrze jest pliki te zostawić w spokoju, jednak gdy chcemy przenieść gdzieś projekt (np. wysłać prowadzącemu na maila czy przenieść projekt na inny komputer) pliki te tylko przeszkadzają. Załóżmy, że w naszym projekcie, o nazwie Zadanie1, jest jeden plik x.cpp o treści: int main() { return 0; }; Oto przykładowa lista plików powstała po kompilacji tego programu (KP to katalog projektu): <KP>\Zadanie1.sln <KP>\Zadanie1.suo <KP>\Zadanie1.opensdf <KP>\Zadanie1.sdf <KP>\Debug\Zadanie1.exe <KP>\Debug\Zadanie1.ilk <KP>\Debug\Zadanie1.pdb <KP>\Zadanie1\Debug\cl.command.1.tlog <KP>\Zadanie1\Debug\CL.read.1.tlog <KP>\Zadanie1\Debug\CL.write.1.tlog <KP>\Zadanie1\Debug\link-cvtres.read.1.tlog <KP>\Zadanie1\Debug\link-cvtres.write.1.tlog <KP>\Zadanie1\Debug\link.3384-cvtres.read.1.tlog <KP>\Zadanie1\Debug\link.3384-cvtres.write.1.tlog <KP>\Zadanie1\Debug\link.3384.read.1.tlog <KP>\Zadanie1\Debug\link.3384.write.1.tlog <KP>\Zadanie1\Debug\link.command.1.tlog <KP>\Zadanie1\Debug\link.read.1.tlog <KP>\Zadanie1\Debug\link.write.1.tlog <KP>\Zadanie1\Debug\mt.command.1.tlog <KP>\Zadanie1\Debug\mt.read.1.tlog <KP>\Zadanie1\Debug\mt.write.1.tlog <KP>\Zadanie1\Debug\rc.command.1.tlog <KP>\Zadanie1\Debug\rc.read.1.tlog <KP>\Zadanie1\Debug\rc.write.1.tlog <KP>\Zadanie1\Debug\vc100.idb <KP>\Zadanie1\Debug\vc100.pdb <KP>\Zadanie1\Debug\x.obj <KP>\Zadanie1\Debug\Zadanie1.exe.embed.manifest <KP>\Zadanie1\Debug\Zadanie1.exe.embed.manifest.res <KP>\Zadanie1\Debug\Zadanie1.exe.intermediate.manifest <KP>\Zadanie1\Debug\Zadanie1.lastbuildstate <KP>\Zadanie1\Debug\Zadanie1.log <KP>\Zadanie1\Debug\Zadanie1_manifest.rc <KP>\Zadanie1\x.cpp <KP>\Zadanie1\Zadanie1.vcxproj <KP>\Zadanie1\Zadanie1.vcxproj.filters <KP>\Zadanie1\Zadanie1.vcxproj.user Proszę zwrócić uwagę na lokalizację pliku wykonywalnego (Zadanie1.exe). Wykonanie polecenia Clean redukuje tę listę do: <KP>\Zadanie1.sln <KP>\Zadanie1.suo <KP>\Zadanie1.opensdf <KP>\Zadanie1.sdf <KP>\Zadanie1\Debug\Zadanie1.Build.CppClean.log <KP>\Zadanie1\Debug\Zadanie1.log <KP>\Zadanie1\x.cpp <KP>\Zadanie1\Zadanie1.vcxproj <KP>\Zadanie1\Zadanie1.vcxproj.filters <KP>\Zadanie1\Zadanie1.vcxproj.user Wszystkie pliki usunięte przez polecenie Clean były plikami pomocniczymi. Do poprawnego skompilowania naszego programu wystarczą jednak tylko pliki: <KP>\Zadanie1\x.cpp <KP>\Zadanie1\Zadanie1.vcxproj
Plik projektu (Zadanie1.vcxproj 1 ) potrzebny jest, aby zapamiętać dodatkowe ustawienia (te, które zmodyfikowaliśmy poleceniem Project -> Properties. Jeżeli chcemy zachować podział plików na zakładki (Header Files, Source Files w drzewie projektu) potrzebujemy jeszcze pliku <KP>\Zadanie1\Zadanie1.vcxproj.filters Proszę zwrócić uwagę, że nie potrzebujemy plików opisujących solution (Zadanie1.sln i Zadanie1.suo). Gdy otworzymy projekt (plik Zadanie1.vcxproj), tuż przed kompilacją środowisko automatycznie utworzy solution dla tego projektu. Debugger Debugger jest narzędziem usprawniającym poszukiwanie i poprawianie błędów w naszym programie. Debugger umożliwia nam obserwowanie programu w trakcie jego działania. Podstawowymi narzędziami udostępnianymi przez debugger są pułapki i praca krokowa. Praca krokowa umożliwia wykonanie małego fragmentu programu na raz (wykonanie jednego kroku). Zazwyczaj jednym krokiem jest jedna linia programu. Pracę krokową umożliwiają polecenia Step Into i Step Over z menu Debug. Rysunek 15 Polecenia Step Into i Step Over Oba te polecenia wykonują jeden krok programu, lecz różnią się w rozumieniu tego kroku. Jeżeli kolejna linia do wykonania zawiera wywołanie funkcji, polecenie Step Over potraktuje to wywołanie jako jeden krok. Step Into zwróci uwagę, że funkcja ta także ma jakąś treść i wykonanie tego polecenia przeniesie nas do pierwszej linii wywoływanej funkcji. W trakcie pracy krokowej, strzałka na lewym marginesie kodu przedstawia bieżące miejsce wykonania programu. wskaźnik bieżącej pozycji w wykonywanym programie Rysunek 16 Praca krokowa 1 W starszych wersjach Visual Studio pliki projektów mają rozszerzenie vcproj
Step Over Step Into Rysunek 17 Rezultat wykonania polecenia Step Over i Step Into Jeżeli nasz program jest duży, a my chcemy zaobserwować działanie konkretnej funkcji, przechodzenie do niej kolejnymi krokami może być uciążliwe. Lepiej zastosować pułapkę. Pułapka jest miejscem, w którym wykonanie programu ma się zatrzymać. Pułapkę ustawiamy, klikając lewym przyciskiem myszy na lewym marginesie kodu. Czerwona kropka która pojawi się w efekcie kliknięcia symbolizuje pułapkę. Rysunek 18 Pułapka Ponowne kliknięcie w tym samym miejscu usuwa pułapkę. Kliknięcie na niej prawym przyciskiem myszy otwiera menu kontekstowe, które pozwala na usunięcie i tymczasowe dezaktywowanie pułapki, ustawienie dodatkowego warunku który musi być spełniony, by pułapka zadziałała, czy licznika trafień (pułapka zatrzyma program dopiero po n-tym przejściu przez pułapkę). Gdy ustawimy pułapkę, możemy uruchomić program (Debug -> Start Debugging) i zaobserwujemy, że wykonanie zatrzyma się właśnie w miejscu pułapki. Rysunek 19 Program zatrzymany przez pułapkę Zatrzymany program możemy uruchomić dalej (do następnej pułapki lub do zakończenia) poleceniem Debug -> Continue. Możemy także rozpocząć pracę krokową (Step Into/Over) by po kilku krokach dokończyć program (Debug -> Continue). Jeżeli zamierzamy przerwać debugowanie, musimy wydać polecenie Debug -> Stop Debugging. Oczywiście w trakcie pracy krokowej możemy dodawać, usuwać i zmieniać parametry pułapek.
Oprócz wykonania programu często chcemy także podejrzeć zawartość zmiennych. Pierwszą metodą, jest najechanie kursorem na zmienną, której zawartość nas interesuje. Po chwili powinno pojawić się okienko z jej zawartością. Rysunek 20 Podgląd zawartości zmiennej Drugą metodą jest skorzystanie z okien Locals i Watch. Powinny one pojawiać się automatycznie podczas pracy krokowej, lecz jeżeli nie są widoczne, możemy je włączyć poleceniami Debug -> Windows -> Locals i Debug -> Windows -> Watch -> Watch 1...4. Okno Locals i Watch Zakładka Watch Zakładka Call Stack Zakładka Immediate Zakładka Locals Rysunek 21 Okna Locals, Watch, Call Stack I Immediate Okno Locals zawiera wszystkie zmienne lokalne z bieżącej funkcji. W oknie Watch możemy wprowadzać dowolne zmienne (a nawet wyrażenia) których zawartość chcemy obejrzeć. Wystarczy dwukrotnie kliknąć na pustym polu w kolumnie Name i wpisać żądane wyrażenie. Rysunek 22 Wprowadzanie wyrażeń do okna Watch Oba okna umożliwiają nam także modyfikację zawartości zmiennych. Wystarczy dwukrotnie kliknąć na jej wartości (kolumna Value) i wpisać żądaną wartość. Oczywiście w przypadku okna Watch jest to możliwe tylko
gdy dany wiersz przedstawia zawartość pojedynczej zmiennej (nie jest możliwe zmodyfikowanie wartości wyrażenia np. a+b). Dwa kolejne, użyteczne okna to Call Stack i Immediate (zob. Rysunek 21). W przypadku ich nieobecności można je włączyć poleceniami Debug -> Call Stack i Debug -> Immediate Window. Pierwsze z nich przedstawia drogę którą doszliśmy do danego miejsca w programie. Pierwszy wpis przedstawia funkcję (wraz z parametrami wywołania) w której jesteśmy. Wpis poniżej mówi, w którym miejscu wywołano tę funkcję (z jakiej funkcji, jakie były parametry i w której linii). Kolejny wpis mówi z kolei, skąd wywołaliśmy funkcję na drugiej pozycji itp.. Rysunek 23 Okno Call Stack: funkcja Funkcja została wywołana przez funkcję main w linii 7 Okno Immediate jest pewnym brudnopisem. Możemy w nim wpisywać wyrażenia, a środowisko obliczy ich wartość. Jeżeli chcemy dowiedzieć się, jaką wartość ma wyrażenie a+b*99 (gdzie a i b to zmienne z naszego programu) możemy w tym oknie wpisać a+b*99 i nacisnąć <Enter>. Wynik pojawi się w następnej linii. Możemy skorzystać z tego okna także do nadawania wartości zmiennym (pisząc np. a = 8) czy obliczania wartości funkcji z naszego programu, np. Funkcja(a, 12). Rysunek 24 Okno Immediate Projekty składające się z wielu plików Nic nie stoi na przeszkodzie, by kod naszego programu był umieszczony w więcej niż jednym pliku. Zdarza się tak, gdy program robi się duży i trzymanie całego kodu w jednym pliku przestaje być wygodne. Możemy także korzystać z kodu dostarczonego przez inne osoby (np. kodu dostarczonego do zadania projektowego). Kolejne pliki dodajemy do projektu tak samo jak pierwszy (zob. Rysunek 5 i Rysunek 6), przy czym możemy dodać albo nowy plik i przenieść do niego zawartość z dostarczonego pliku albo istniejący plik (Add -> Existing Item...). W przypadku dodania istniejącego pliku, w projekcie zostanie tylko zapisana informacja o ścieżce do zewnętrznego pliku. W takiej sytuacji należy zachować ostrożność przenosząc czy kopiując projekt jeżeli tak dodane pliki nie znajdowały się w katalogu projektu, to kopia katalogu projektu nie będzie zawierała kompletnych źródeł. Zalety dodawania istniejących plików ujawniają się, gdy pracujemy na kilku projektach, które współdzielą pliki dzięki dodaniu zewnętrznego mogą one korzystać z tej samej kopii pliku. Jeżeli musimy zmodyfikować taki plik, robimy to tylko raz (gdybyśmy dodali go jako nowy do każdego projektu to musielibyśmy wprowadzić zmiany w każdej kopii we wszystkich projektach). Podział na pliki nagłówkowe (Header Files, zob. Rysunek 7) i źródłowe (Source Files) ma znaczenie czysto estetyczne. Projekt wygląda porządniej, gdy pliki.h znajdują się w zakładce Header Files a.cpp w Source Files, ale z punktu widzenia kompilacji ten podział nie ma znaczenia (pliki.cpp zostaną kompilowane nawet gdy zostaną dodane w zakładce Header Files).
Często występujące błędy Błąd kompilacji unexpected end of file while looking for precompiled header Jeżeli w trakcie kompilacji otrzymujemy błąd: fatal error C1010: unexpected end of file while looking for precompiled header... to znaczy że włączone są prekompilowane nagłówki a nasz kod nie jest z nimi zgodny. Prekompilowanie nagłówków może skrócić czas kompilacji naszego programu, jednak jest to technika specyficzna dla danego środowiska i kodu wykorzystującego prekompilowane nagłówki nie będzie można skompilować innym kompilatorem dlatego bezpieczniej jest nie korzystać z tego mechanizmu. Wyłączyć go można albo w trakcie tworzenia projektu (opcja Precompiled header, zob. Rysunek 4) lub wchodząc w opcje projektu (Project -> Properties...), w zakładkę Configuration Properties / C/C++ / Precompiled Headers i przełączając opcję Create/Use Precompiled Header na Not Using Precompiled Header. Rysunek 25 Wyłączanie prekompilowanych nagłówków Błąd kompilacji Failed to save the updated manifest to the file "...embed.manifest" Wystąpienie błędu kompilacji: mt.exe : general error c101008a: Failed to save the updated manifest to the file "...embed.manifest". mówi nam o problemach z zapisem lub modyfikacją pliku manifestu. Plik manifestu jest to plik zawierający pewne dodatkowe informacje dotyczące naszego programu 2. Nie zawsze jest on potrzebny (i jeżeli nie wiemy czy go potrzebujemy, to go nie potrzebujemy). Błąd ten możemy poprawić wykonując polecenie Clean na naszym projekcie. Jeżeli to nie pomaga, lub błąd ten pojawia się co pewien czas, możemy całkowicie wyłączyć korzystanie z pliku manifestu. Należy wejść w opcje projektu (Project -> Properties...) i w zakładce Configuration Properties / Linker / Manifest File przełączyć opcję Generate Manifest na No. 2 więcej informacji o tych plikach można znaleźć na stronie http://en.wikipedia.org/wiki/side-by-side_assembly
Rysunek 26 Wyłączanie generowania pliku manifestu Nie można znaleźć składnika MSVCR90D.DLL (MSVCR100.DLL i podobne) Rysunek 27 Komunikat o braku zewnętrznej biblioteki Komunikat ten pojawia się, gdy nasza aplikacja nie może znaleźć podanej biblioteki (zawiera ona implementację biblioteki standardowej języka C/C++). Problem ten można rozwiązać umieszczając tę bibliotekę w miejscu, w którym nasza aplikacja ją znajdzie (np. w WINDOWS\SYSTEM32 lub w katalogu bieżącym naszej aplikacji) lub włączyć statyczne linkowanie. Statyczne linkowanie spowoduje, że kod biblioteki standardowej zostanie włączony do pliku wykonywalnego i biblioteka zewnętrzna nie będzie potrzebna. Wadą tego rozwiązania jest zwiększenie rozmiaru pliku wykonywalnego. Statyczne linkowanie włączamy wchodząc w opcje projektu (Project -> Properties...), przechodząc do zakładki Configuration Properties / C/C++ / Code Generation i zmieniając parametr Runtime Library na Multi-Threaded Debug (/MTd).
Rysunek 28 Włączanie statycznego linkowania Program się uruchamia, lecz nie działa jak powinien nie uwzględnia wprowadzonych ostatnio zmian Wystąpiły błędy kompilacji i polecono środowisku, by w takiej sytuacji uruchamiał poprzednią działającą wersję (zob. Rysunek 11). Należy przebudować projekt (polecenie Rebuild) i poprawić błędy.