25 luty 2009

Istotna kolejność

Dzięki operatorowi sizeof() możemy dowiedzieć się jaki jest rozmiar danego typu w bajtach. Standard świadomie nie określa odgórnych rozmiarów typów podstawowych, zostawiając to zależnym od platformy sprzętowej (opisane są tylko relacje wielkości pomiędzy typami). Mimo, że wszystkie produkowane dziś procesory operują na słowach 64 bitowych, to jednak znaczna większość użytkowników posiada 32 bitowe systemy operacyjne, dla których tworzony jest kod aplikacji.

Przypatrzmy się więc dwóm z pozoru identycznym strukturom:

struct A{
    short a1;
    int b1;
    short a2;
};
struct B{
    short a1
    short a2;
    int b1;
};

Mają służyć tylko jako przykład, więc nie są to wyjątkowo praktyczne typy. Jak widzimy obydwie struktury mają po dwa pola typu short i jedno typu int. Zróbmy teraz mały eksperyment i wyświetlmy rozmiary obu tych typów:

printf("%d %d\n",sizeof(A), sizeof(B));

Naszym oczom ukaże się dość zadziwiający widok, bo rozmiary typów A i B są różne, i wynoszą odpowiednio 12 i 8 bajtów (na procesorze z rodziny IA-32). Dość zaskakujące, nieprawdaż? Tylko z pozoru.
Zacznijmy więc od początku. Z prostego dodawania wynika, że obie te struktury powinny zajmować 8 bajtów (2+2+4). Pierwsza z nich jednak, zajmuje o 4 bajty więcej. W tym momencie przyda się podstawowa wiedza na temat działania procesora. Danymi, na których procesory 32 bitowe, pracują z największą wydajnością to właśnie 32 bity. Tyle wynosi rozmiar słowa i rejestrów ogólnego przeznaczenia. Wysłanie większej, lub mniejszej, ilości danych, jest realizowane wolniej, dlatego nie zawsze zaleca się ich używanie. Spójrzmy teraz na strukturę A. Aby efektywnie móc przesłać wartość jej pól do rejestrów, najlepiej zrobić to 32 bitowymi paczkami. Napotykamy jednak na problem. Wedle tej idei, zmienną b1 trzeba wysyłać 2 razy (po 16 bitów wraz z a1 i a2). Nie jest bezpieczne i poprawne rozwiązanie. W takim przypadku kompilatory stosują bardzo prostą sztuczkę: wyrównują zmienne do 32 bitów, co w tym przypadku oznacza, że a1 oraz a2 będą zajmować po 4 bajty. Na każdej z tych zmiennych zyskujemy dodatkowo po 2 bajty, więc zagadka została wyjaśniona. Dlaczego więc struktura B, zajmuje 8 bajtów? Decyduje o tym właśnie ułożenie zmiennych. W tym przypadku zmienne a1 i a2 można wysłać za pomocą jednego wywołania instrukcji mov, więc wyrównanie ich nie jest potrzebne.
Obrazowo można przedstawić to tak, że struktura (a zarazem i klasa) to pojemnik z 32 bitowymi (lub 64, w przypadku procesorów i systemów operacyjnych 64 bitowych) pudełkami, do których są kolejno dodawane zmienne. Jeśli dana zmienna nie zajmuje całego pudełka, a kolejna zmienna już nie mieści się do niego, wolne miejsce zostaje zapełnione – natura nie lubi próżni. :) 

02 luty 2009

Kodowanie znaków, WinApi i Visual Studio

Wielu początkujących programistów, po używaniu środowisk typu Dev-C++, przesiada się na Visual Studio. Mimo, że na początku może lekko przytłaczać, pakiet Microsoftu jest dość intuicyjnym narzędziem. Stworzenie aplikacji okienkowej w WinApi jest pewnie naszym pierwszym celem. Rozpoczynamy więc lekturę odpowiednich kursów, kopiujemy przykładowy kod i… Jak to zwykle bywa natrafiamy na problem. Okazuje się, że jego przyczyną jest błąd podobny do tego:

error C2664: 'CreateWindowExW' : cannot convert parameter 2 from 'char [12]' to 'LPCWSTR'

Błąd informuje nas o tym, że kompilator nie może zrzutować drugiego parametru funkcji CreateWindowEx z typu char na typ LPCWSTR. Przyjrzyjmy sie może czym jest LPCWSTR. Zaglądając do pliku WinNT.h (skąd wiem, że akurat tam? Wystarczy kliknąć prawym przyciskiem myszy na typ i wybrać: “Go to definition”) możemy odczytać:

typedef __nullterminated CONST WCHAR *LPCWSTR, *PCWSTR;

Jest to więc stała (#define CONST const) typu wchar_t (typedef wchar_t WCHAR) zakończona zerem (Microsoft wprowadził adnotacje do C++, jedną z nich jest właśnie __nullterminated – więcej informacji pod tym adresem).
Teraz przyjrzyjmy się bliżej naszej funkcji. Analiza błędu, przedstawionego nam przez IDE odkryje przed nami pewną ciekawostkę. Okazuje się bowiem, że nie wywoływaliśmy funkcji CreateWindowExW
, zaprezentowanej w błędzie, tylko CreateWindowEx. W czym tkwi haczyk? Zobaczmy jak zadeklarowana jest owa funkcja:

#ifdef UNICODE
#define CreateWindowEx CreateWindowExW
#else
#define CreateWindowEx CreateWindowExA
#endif // !UNICODE

Tak. Oto całość. Jak na zwykłą deklaracje, kod wydaje się dość dziwny, jednak jego znaczenie jest dość proste. Na początku sprawdzane jest, czy została zdefiniowana stała preprocesora o nazwie UNICODE. Jeśli tak, każde wywołanie funkcji CreateWindowEx będzie zastępowane przez CreateWindowExW. W wypadku gdy stała nie jest zdefiniowana wywoływana będzie CreateWindowExA*.  Tylko czym różnią się te funkcje z przyrostkami “A” i “W” ? Otóż jeśli funkcja ma postfix “W” oznacza to, że operuje na znakach Unicode, czyli wchar_t (LPCWSTR), zaś “A” oznacza zwykłe kodowanie ASCII (char).
Pozostaje jeszcze jedno pytanie. Skąd wiadomo, że stała UNICODE jest zdefiniowana i jak, w razie potrzeby, ją zdefiniować? Aby zapoznać się z pełną listą stałych wystarczy wejść we właściwości projektu, tam w element C++ (znajdujący się w Configuration Properties) a następnie w Command Line. Dla standardowego projektu Win32 możemy przeczytać następujące stałe: WIN32, DEBUG lub NDEBUG dla projektu Release, UNICODE oraz WINDOWS. Zmianę lub dodanie własnej stałej, wykonujemy poprzez Preprocesor Definitions elementu Preprocesor (dalej jesteśmy w C++). Tylko, że nie ma tam UNICODE, nawet jeśli stała ta jest zdefiniowana. Co należy zrobić by korzystać ze standardowego kodowania? Cóż, znów przyjdzie nam z pomocą okienko z właściwościami projektu. Klikamy na zakładkę General w Configuration Properties i tam Character Set ustawiamy na Not Set
Klikamy na OK, rekompilujemy nasz projekt i możemy korzystać ze starego dobrego typu char.

*Warto wspomnieć o tym, że tak naprawdę kompilator nie wie, czym jest funkcja CreateWindowEx. Każde jej wywołanie, zostanie mechanicznie zastąpione jedną z dwóch wyżej wymienionych funkcji, jeszcze przed kompilacją – na tym ogólnie polega idea preprocesora tekstu w C++.