Konwersja typów przy operacjach arytmetycznych w C

Aby wykonać operację arytmetyczną w  C, kompilator musi dysponować dwoma operandami tego samego typu. Jeżeli programista o to wcześniej nie zadbał, konwersja jest wykonywana niejawnie. Na przykład:

  • Zmienna o mniejszym rozmiarze jest konwertowana do większego np. uint8 na uint32, czy float na double.
  • Zmienna bez znaku jest konwertowana na zmienną ze znakiem.
  • Zmienna int jest konwertowana na float.

Konwersje typów są również wykonywane przy operacjach przypisania. Możemy na przykład do zmiennej uint przypisać liczbę ujemną. Możemy również do niej przypisać wynik operacji arytmetycznej, gdzie czasem wartość jest dodatnia, a czasem ujemna. Takie konwersje są dobrym miejscem do powstawania różnego rodzaju błędów i nieoczywistych, czy wręcz niezdefiniowanych zachowań.

Zwiększanie rozmiaru zmiennych całkowitych

W C kiedy chcemy przypisać do zmiennej unsigned wartość z końca zakresu, często zamiast MAX_UINT przypisujemy po prostu -1. Co więcej taką implementację stałej MAX_UINT można czasem znaleźć nawet w bibliotece standardowej (np. w xc-32 dla procesorów PIC32).

Popatrzmy na poniższy kod:

#include <stdio.h>
#include <stdint.h>

int main(void)
{
    uint32_t a;
    int32_t b;

    a = -5;
    b = -1;

    printf("%d\n", a + b);

    return 0;
}
Output: -6

Do zmiennej unsigned przypisujemy liczbę ujemną, a dalej wykonujemy dodawanie liczby signed i unsigned. Wynik jest zgodny z oczekiwaniami. Wprowadźmy teraz małą modyfikację i zmniejszmy rozmiar zmiennej a:

#include <stdio.h>
#include <stdint.h>

int main(void)
{
    uint16_t a;
    int16_t b;

    a = -5;
    b = -1;

    printf("%d\n", a + b);

    return 0;
}
Output 65530

Teraz wynik zmienił się diametralnie. Przed wykonaniem operacji a + b, typy zmiennych zostały zmienione na 32-bitowe i a przyjęło wartość 0x0000FFFB, a nie 0xFFFFFFFB.

A co jeśli zmniejszymy rozmiar zmiennej ze znakiem?

#include <stdio.h>
#include <stdint.h>

int main(void)
{
    uint32_t a;
    int16_t b;
    
    a = -5;
    b = -1;
    
    printf("%d\n", a + b);
    
    return 0;
}
Output: -6

Wynik nie uległ zmianie. Podczas promocji zmienna b zmieniła wartość z 0xFFFF na 0xFFFFFFFF. Kompilator podczas konwersji dopełnił liczbę bitami znaku.

Przypisanie wyniku do zmiennej o mniejszym rozmiarze

Może się zdarzyć, że zmienna, do której chcemy zapisać wynik jest zbyt mała.

#include <stdio.h>
#include <stdint.h>

int main(void)
{
uint16_t a;
uint32_t b;
int8_t c;

a = 12345;
b = 6789012;
c = a + b;

printf("%d\n", c);

return 0;
}

Output: -51

Zmienna 8-bitowa przechowuje jedynie najmłodsze bity wyniku.

MISRA C

Pozwalając na niejawne konwersje programista traci panowanie nad typami zmiennych. W systemach embedded kontrolujących jakieś ważne procesy taka sytuacja często jest niedopuszczalna. Dlatego na przykład standard MISRA C, mający za zadanie zwiększyć niezawodność aplikacji pisanych w C reguluje temat niejawnych konwersji. Pozwala on jedynie na niejawne zwiększenie rozmiaru zmiennej bez zmiany typu. Należy wykonywać jawne rzutowania i rozbijać obliczenia na mniejsze części tak, aby w każdym momencie typy zmiennych były jasno określone.

Wiąże się z tym jedno dosyć upierdliwe obostrzenie. Otóż stałe również mają określony typ. Domyślnie stała jest liczbą ze znakiem. Aby wymusić, aby była bez znaku, należy dodać do niej suffix U. Czyli na przykład jeśli zdefiniujemy stałą o wartości 0 to możemy ją używać tylko ze zmiennymi typu int. Dla zmiennych uint potrzebujemy osobnej stałej 0U.

 

 

 

 

 

 

1 Comment

  1. Przed drugim listingiem kodu mowa jest o zmniejszeniu rozmiaru zmiennej 'a’, a obie się zmniejszają:

    uint16_t a;
    int16_t b;

    Chyba nie zaszłaby wtedy promocja do 32bitów?

Dodaj komentarz

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