O wskaźnikach i rzutowaniu w C

Zmienne programu są w pamięci reprezentowane jako pewien ciąg bitów. Dla komputera nie ma znaczenia, czy ten ciąg to liczba, string, struktura, czy cokolwiek innego. Pisząc programy definiujemy typy zmiennych i to na te typy rzutowane są wartości bitowe.

Języki wyższego poziomu jak np. C#, czy Python oddzielają tą implementację za pewną warstwą abstrakcji. Twórcy tych języków doszli do wniosku, że program nie powinien polegać na konkretnej reprezentacji bitowej. Możemy oczywiście za pomocą odpowiednich funkcji uzyskać dostęp do takiej bitowej reprezentacji, ale czynność ta celowo została utrudniona.

C jest jednak starszym językiem wyznającym zasadę, że programista wie co robi. Dlatego w C mamy łatwy dostęp do reprezentacji bitowej. Możemy używać wskaźników, czy rzutować typy na inne. Efektem jest większa wydajność wynikowych programów za cenę wolniejszego developmentu. Sprawdzanie błędów w C jest bardzo ubogie i nawet doświadczony programista może łatwo strzelić sobie w stopę.

W tym wpisie pokażę kilka potencjalnie niebezpiecznych fragmentów kodu oraz omówię jak radzi sobie z nimi standard MISRA C oraz jak to rozwiązano w C++.

Typy i wskaźniki w C

Wskaźnik w C przechowuje informacje o adresie w pamięci i o typie (ta informacja jest wykorzystywana np. przy inkrementacji adresu – dodawany jest wtedy sizeof(type), a nie 1). Nie ma żadnego problemu, żeby wskazywany adres tak naprawdę przechowywał zmienną innego typu.

#include <stdio.h>

int main(void)
{
    float f;
    int *ptr_int;

    f = 1.0f;
    ptr_int = &f;

    printf("%d\n", *ptr_int);

    return 0;
}

Output: 1065353216

Kompilator zgłosi warning o podejrzanym rzutowaniu, ale nam tego nie zabroni.

main.c: In function 'main':
 main.c:7:13: warning: assignment from incompatible pointer type [-Wincompatible-pointer-types]
 ptr_int = &f;

Warning oczywiście możemy obejść rzutując &f:

ptr_int = (int *)&f;

Otwiera nam to wiele ciekawych możliwości. Możemy na przykład wykorzystać rzutowanie do optymalizacji obliczeń, jak w słynnym kodzie z Quake 3:

float Q_rsqrt(float number)
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;
    
    x2 = number * 0.5F;
    y = number;
    i = *(long *)&y;                         // evil floating point bit level hacking
    i = 0x5f3759df - (i >> 1);               // what the fuck?
    y = *(float *) &i;
    y = y * (threehalfs - (x2 * y * y));     // 1st iteration
//    y = y * (threehalfs - (x2 * y * y));     // 2nd iteration, this can be removed

    return y;
}

Jednak równie dobrze możemy zrobić coś naprawdę dziwnego np:

typedef void (*fun_ptr)(int a);

int main() {
   float f = 123.4f;
   ((fun_ptr)&f)(1);
}

W powyższym kodzie rzutujemy liczbę float na wskaźnik na funkcję i go wywołujemy. Kod najprawdopodobniej zakończy się runtime errorem, jednak sam kompilator nam tego nie zabroni. W zdecydowanej większości przypadków nie będzie to pożądane działanie, jednak ten jeden raz na milion może się przydać. Ten konkretny trick (co prawda rzutujemy uint32_t a nie float) stosuje się na przykład pisząc bootloader, kiedy chcemy skoczyć do właściwego programu.

Jeśli decydujemy się na zastosowanie tego typu hacków, na pewno musimy opatrzyć je odpowiednim komentarzem. Po jakimś czasie prawdopodobnie sami nie będziemy pamiętać, czy pisząc ten kod zdawaliśmy sobie sprawę, że rzutujemy na niekompatybilny typ i czy na pewno tak to miało wyglądać.

A co na to MISRA C?

Jak nietrudno się domyślić standard MISRA C uznaje rzutowanie wskaźników za potencjalnie niebezpiecznie i go zakazuje. Możliwe jest jedynie rzutowanie do void pointera. Prawdopodobnie po to, aby  można było korzystać np. z memcpy. Nie jest to wielkie zaskoczenie – jak chcemy, aby aplikacja była bezpieczna, nie należy stosować takich tricków.

Rozwiązanie z C++

W C++ problem rzutowania został fajnie rozwiązany. Wprowadzono różne rodzaje rzutowań:

  • static_cast – używamy do konwersji między kompatybilnymi typami np. enum -> int, float -> int, ale float * -> int * już nie. Ogólna zasada mówi, że static cast używamy jeżeli w odwrotnym kierunku kompilator wykonałby rzutowanie niejawnie.
  • const_cast – używamy, aby dodać lub usunąć modyfikator const z pointera.
  • dynamic_cast – używamy do rzutowania między klasami, które po sobie dziedziczą.
  • reinterpret_cast – używamy do rzutowania między jakimikolwiek typami. Kompilator nie sprawdza, czy rzutowanie ma sens. Można powiedzieć, że jest to odpowiednik rzutowania z C – typy nie są ze sobą powiązane, ale programista wie co robi.

Więcej informacji o rodzajach rzutowań w C++ można znaleźć tutaj.

Dzięki zastosowaniu takiego podziału programista może łatwo zasygnalizować, że np. rzutuje na kompatybilny typ i stosuje wtedy static_cast. Jeżeli na skutek późniejszych modyfikacji typ się zmieni na niekompatybilny, kompilator wyrzuci błąd. Natomiast stosując reinterpret_cast dajemy jasno do zrozumienia, że z premedytacją rzutujemy na zupełnie inny typ.

Podobne wpisy

  • |

    Nowa zabawka

    Nad kupnem drukarki 3D zastanawiałem się już od dłuższego czasu, ale do tej pory na zastanawianiu się kończyło. Moim zdaniem jest to technologia przyszłości i za kilkanaście lat będzie tak popularna jak zwykłe drukarki. Takiego przekonania nabrałem oglądając reklamówki, na których gospodynie domowe drukowały sobie foremki do ciast. Kiedy więc ostatnio pojawiła się okazja kupienia…

  • Podsumowanie roku 2023

    Witam w Nowym Roku. Jak nakazuje tradycja – pora na podsumowanie starego roku i plany na nowy. Ostatni rok upłynął mi pod znakiem delegowania zadań, testowania narzędzi i procesów oraz sprawdzania różnych sposobów tworzenia treści w internecie. Był też nowy kurs online, szkolenie stacjonarne, prezentacja na konferencji, a Gdańsk Embedded Meetup doczekał się nagrań z…

  • Estymowanie czasu

    W dzisiejszym artykule omawiam tajemną sztukę estymowania czasu. Wiele osób ma do siebie pretensje, że nie potrafi poprawnie przewidzieć wymaganego czasu na zadanie i projekt. Prawdopodobnie zapominają oni jakie jest znaczenie słowa estymata. Aby rozjaśnić temat wychodzę od statystyki i pewnych faktów o estymatorach, a następnie formułuję wnioski dotyczące estymowania czasu.  Nie należy tego traktować…

  • C dla zaawansowanych

    Od jakiegoś czasu pracuję nad szkoleniem online C dla zaawansowanych. Ostatnio uruchomiłem stronę szkolenia: https://cdlazaawansowanych.pl/ Na razie możecie na niej przeczytać trochę jak będzie wyglądać ten program. Możecie także zapisać na mój newsletter i mieć pewność, że nie przegapicie żadnych informacji dotyczących szkolenia i otrzymacie najlepszą ofertę dołączenia. Przy okazji na listę mailową wrzucam różne…

  • Miary jakości unit testów

    Wprowadzenie do Test Driven Development – wszystkie wpisy Piramida testów – do czego służą poszczególne poziomy Miary jakości unit testów Mocki – radzenie sobie z zależnościami w testach Antywzorce unit testów Jak pisać dobre unit testy Kiedy nie stosować TDD Wymówki, aby nie pisać unit testów Zalety TDD Na czym polega TDD Dlaczego zainteresowałem się…

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *