Podstawy programowania 8

Jak to się robiło/robi w czystym C?

Ponieważ język C++ co do zasady jest zgodny z C – dobrze napisany program w C powinien dać się skompilować kompilatorem C++, więc wielu programistów nadal używa w C++ sposobów, które powstały w czasach języka C. Dobry programista C++ powinien je także znać, jeśli nawet nie po to, by samemu używać w pisanych programach, to aby rozumieć co napisali inni.

Dynamiczna alokacja pamięci

Widzieliśmy już jak w C++ zarządzać pamięcią – używając operatorów new i delete. Biblioteka języka C też dostarcza swoich sposobów zarządzania pamięcią. Dopóki używamy jedynie prostych typów wbudowanych, to są to sposoby zupełnie równoważne (funkcje C nie wiedzą nic o klasach, w przeciwieństwie do new i delete).

Odpowiednikiem new jest funkcja malloc, która jako argument przyjmuje wielkość pamięci do zarezerwowania. Tak zarezerwowaną pamięć należy zwolnić używając funkcji free. Obie funkcje dostępne są po dołączeniu nagłówka cstdlib. Dla przykładu, stworzenie dynamicznej tablicy int-ów za pomocą funkcji malloc wygląda następująco:

#include <cstdlib>

int main() {
    int n;
    cin >> n;
    int *t = static_cast<int*>(malloc(n*sizeof(int)));
    ...
    free(t);
    return 0;
}

Funkcja malloc zwraca wskaźnik do przydzielonej pamięci. Ponieważ jednak tak naprawdę malloc nie ma pojęcia o przechowywanym tam typie, zwracany wskaźnik jest typu void*. W C nie czyni to problemów, ale w C++ kontrola typów jest dokładniejsza, dlatego wynik musimy zrzutować na właściwy typ int*.

Funkcja malloc (podobnie jak new) nie zeruje przydzielonej pamięci. Do tego można użyć funkcji memset.

Pamięć przydzieloną z użyciem malloc należy zwalaniać tylko z użyciem free, nie delete. Analogiczne zasady dotyczą new. Co prawda pod g++ prawdopodobnie nie dostaniemy błędu, jednak nasz program będzie niezgodny ze standardem języka.

Wejście i wyjście

W C++ do obsługi wejścia/wyjścia z reguły używamy biblioteki IOStreams poprzez zmienne cout i cin oraz operatory >> i <<. Jednak można też używać procedur z C, które co prawda są mniej wygodne, za to czasem szybsze (albo inaczej – łatwo sprawić by cout i cin były wolniejsze). Mimo wszystko, w większości programów obsługa wejścia/wyjścia nie zajmuje wystarczająco dużo czasu żeby się tym przejmować, liczy się łatwość programowania.

Funkcje obsługujące wejście/wyjście w C są zadeklarowane w nagłówku cstdio. Do obsługi standardowego wejścia służy funkcja scanf a do obsługi standardowego wyjścia funkcja printf.

printf

Funkcja printf przyjmuje zmienną liczbę parametrów. Pierwszy parametr jest obowiązkowy, jest to łańcuch określający format wypisywanego łańcucha. Łańcuch ten jest po prostu wypisywany, a znajdujące się wewnątrz sekwencje kontrolne (poprzedzone znakiem %) odpowiednio interpretowane jako instrukcje formatowania – dla każdej instrukcji formatowania funkcja printf oczekuje jednego dodatkowego parametru.

Najważniejszą częścią instrukcji formatowania jest typ. Najważniejsze typy to:

  • %d – liczba całkowita ze znakiem
  • %f – liczba zmiennoprzecinkowa ze znakiem
  • %s – łańcuch (char*)
  • %c – znak
  • %p – wskaźnik

Oprócz typu instrukcje mogą wskazywać też na wielkość zmiennej (np. int czy long) oraz na pożądane formatowanie (np. dwie cyfry po przecinku). Wszystkie opcje są w dokumentacji.

Dla przykładu, aby wypisać liczbę π z dokładnością do 10 miejsc po przecinku wraz z odpowiednim komentarzem i znakiem końca linii, można napisać:

#include <cmath>
#include <cstdio>

int main() {
    printf("PI to w przybliżeniu %.10f\n", M_PI);
    return 0;
}

scanf

Funkcja scanf działa bardzo podobnie, jednak wczytuje dane zamiast je wypisywać. Używa sekwencji kontrolnych bardzo podobnych do tych używanych przez printf. Dla każdej sekwencji kontrolnej scanf oczekuje argumentu będącego wskaźnikiem na miejsce w pamięci gdzie ma przechować odpowiednio zinterpretowaną wartość.

Funkcja scanf z reguły nie zwraca uwagi na spacje, tabulatory i znaki końca linii, jednak wszystkie inne znaki, które nie mają zostać sczytane muszą pojawić się w łańcuchu formatującym. Dokładne działanie używanej implementacji należy poszukać w dostarczanej wraz z kompilatorem dokumentacji.

#include <cstdio>

int main() {
    double pi;
    printf("Jaką wartość π chcesz przyjąć do obliczeń: ",);
    //program oczekuje, że użytkownik wpisze np.
    //pi = 3.14
    //i naciśnie ENTER
    scanf("pi = %lf\n", &pi);
    return 0;
}

Należy zwracać szczególną uwagę na instrukcję sterującą %s, która sczytuje dowolnie dużą liczbę znaków podaną przez użytkownika. Przeznaczony bufor musi być w stanie pomieścić te znaki. Ponieważ nie ma żadnego mechanizmu ograniczającego ilość znaków, takie wywołanie jest niebezpieczne.

Biblioteka glibc używana z reguły jako biblioteka języka C pod Linuksem na komputerach osobistych pozwala napisać %ms, co spowoduje, że wywołanie scanf przydzieli wystarczająco dużo pamięci i sczyta dane. Jako parametr należy przekazać wskaźnik na wskaźnik (char**) i zaalokowaną pamięć zwolnić później poprzez wywołanie free na tym wskaźniku.

Łańcuchy znaków

Wygodna klasa string jest także dodatkiem dostępnym tylko w C++. W języku C łańcuchy znaków są tak naprawdę tablicami znaków zakończonymi bajtem zerowym. W szczególności, aby wiedzieć jak długi jest taki łańcuch trzeba przeszukać go aż do znalezienia tego bajtu. Łańcuchy języka C są więc obiektami typu char* lub char[].

Biblioteka języka C dostarcza w nagłówku cstring wielu funkcji przydatnych przy pracy z takimi łańcuchami. Funkcje te pozwalają na kopiowanie, łączenie, porównywanie łańcuchów czy wyszukiwanie znaków w łańcuchach. Pełna lista znajduje się w dokumentacji biblioteki.

Mimo dostępności tych funkcji, obsługa łańcuchów char* jest bardziej kłopotliwa niż łańcuchów string ponieważ programista musi samodzielnie zarządzać pamięcią. Popełnione w zarządzaniu pamięcią błędy z reguły prowadzą do bardzo groźnych luk bezpieczeństwa.