Setiap tipe integer di C bisa dipakai dalam dua varian: signed (bisa negatif) dan unsigned (selalu positif). Modifier signed dan unsigned tidak mengubah ukuran tipe, tetapi mengubah cara byte-byte tersebut diinterpretasikan. Pilihan ini memiliki implikasi besar: rentang nilai berbeda, perilaku overflow berbeda, dan ada jebakan saat mencampur kedua varian dalam ekspresi yang sama. Pada Chapter ini kita akan memetakan perbedaan kedua varian, perilaku wrap-around pada unsigned, serta peringatan krusial saat membandingkan kedua varian.

Apa Perbedaan signed dan unsigned #

Tipe integer disimpan sebagai deretan byte. Interpretasi byte-byte tersebut bergantung pada modifier:

  • signed: satu bit (bit tertinggi) dipakai sebagai tanda. Sisanya merepresentasikan magnitudo dengan teknik two’s complement.
  • unsigned: seluruh bit dipakai untuk magnitudo; nilai selalu non-negatif.

Karena jumlah bit yang sama dipakai untuk magnitudo, unsigned bisa menampung nilai positif sekitar dua kali lipat dari signed dengan ukuran byte yang sama. Sebagai contoh, int 4 byte memiliki rentang -2.147.483.648 hingga 2.147.483.647, sedangkan unsigned int 4 byte memiliki rentang 0 hingga 4.294.967.295.

c
#include <stdio.h>
#include <limits.h>

int main(void) {
    signed int suhu = -15;
    unsigned int populasi = 4300000u;

    printf("suhu      = %d\n", suhu);
    printf("populasi  = %u\n", populasi);

    printf("INT_MIN  = %d\n", INT_MIN);
    printf("INT_MAX  = %d\n", INT_MAX);
    printf("UINT_MAX = %u\n", UINT_MAX);
    return 0;
}

Output yang Dihasilkan:

text
suhu      = -15
populasi  = 4300000
INT_MIN  = -2147483648
INT_MAX  = 2147483647
UINT_MAX = 4294967295

Mari kita telaah baris-baris kode di atas:

  1. Baris 6 (signed int suhu = -15;): kata kunci signed boleh ditulis eksplisit; tanpa menuliskannya, int sudah bersifat signed secara default.
  2. Baris 7 (unsigned int populasi = 4300000u;): suffix u menandakan literal bertipe unsigned int; nilai 4300000 tidak mungkin negatif sehingga cocok untuk populasi.
  3. Baris 9 (%d): specifier untuk mencetak int bertanda; menghasilkan -15.
  4. Baris 10 (%u): specifier untuk mencetak unsigned int sebagai desimal tak bertanda; menghasilkan 4300000.
  5. Baris 13 (UINT_MAX): konstanta dari limits.h yang menunjukkan batas atas unsigned int, yaitu 2^32 - 1 = 4.294.967.295.

Wrap-Around pada unsigned #

Tipe unsigned tidak pernah menghasilkan undefined behavior saat nilai meluap. Sebagai gantinya, nilainya dibungkus (wrap-around) secara modular sesuai 2^N di mana N adalah jumlah bit. Artinya, 0u - 1u menghasilkan UINT_MAX, dan UINT_MAX + 1u menghasilkan 0. Perilaku ini dijamin oleh standar C dan bisa dimanfaatkan untuk algoritma tertentu, tetapi juga sumber bug klasik ketika programmer lupa bahwa pengurangan yang menghasilkan negatif tetap menghasilkan bilangan besar pada unsigned.

c
#include <stdio.h>

int main(void) {
    unsigned int a = 5;
    unsigned int b = 9;

    /* Pengurangan unsigned: hasilnya selalu non-negatif (wrap-around) */
    unsigned int hasil = a - b;
    printf("5u - 9u = %u (wrap-around)\n", hasil);

    /* Bandingkan dengan signed */
    int sa = 5;
    int sb = 9;
    printf("5 - 9   = %d\n", sa - sb);
    return 0;
}

Output yang Dihasilkan:

text
5u - 9u = 4294967292 (wrap-around)
5 - 9   = -4

Pada baris 8, ekspresi a - b secara matematis adalah -4, tetapi karena kedua operan bertipe unsigned int, hasilnya dihitung modulo 2^32. Nilai -4 dipetakan menjadi 4.294.967.292 (= 2^32 - 4). Pada baris 13, perhitungan yang sama dilakukan pada int dan menghasilkan -4 seperti yang kita harapkan. Inilah mengapa tipe unsigned tidak boleh dipakai ketika pengurangan bisa menghasilkan nilai negatif.

Jebakan Perbandingan signed dan unsigned #

Saat kita membandingkan signed dengan unsigned, compiler mengikuti aturan usual arithmetic conversion: signed akan dipromosikan ke unsigned. Akibatnya, bilangan negatif berubah menjadi bilangan positif besar dan perbandingan menjadi tidak intuitif. Flag -Wsign-compare (termasuk dalam -Wextra) akan memperingatkan hal ini.

c
#include <stdio.h>

int main(void) {
    /* Membandingkan signed dengan unsigned: bahaya tersembunyi */
    int negatif = -1;
    unsigned int positif = 5;

    if (negatif < positif) {
        printf("-1 < 5  (logika seharusnya)\n");
    } else {
        printf("-1 >= 5 (yang terjadi saat unsigned promotion!)\n");
    }

    printf("(unsigned)(-1) = %u\n", (unsigned int)negatif);
    return 0;
}

Output yang Dihasilkan:

text
-1 >= 5 (yang terjadi saat unsigned promotion!)
(unsigned)(-1) = 4294967295

Pada baris 8, ekspresi negatif < positif seharusnya true secara matematis. Tetapi karena positif bertipe unsigned int, variabel negatif juga dipromosikan ke unsigned int. Nilai -1 dalam representasi two’s complement adalah 0xFFFFFFFF, yang setara dengan 4.294.967.295 ketika diinterpretasikan sebagai unsigned. Maka 4.294.967.295 < 5 bernilai false, dan cabang else yang dieksekusi. Baris 15 membuktikan hal ini: cast eksplisit (unsigned int)negatif menghasilkan 4.294.967.295.

Kapan Memakai unsigned #

Tipe unsigned tepat dipakai pada beberapa konteks:

  • Bitmap dan mask bit: karena urutan bit digeser dan di-OR tanpa melibatkan tanda.
  • Ukuran dan jumlah: size_t adalah unsigned karena ukuran tidak pernah negatif.
  • Hash dan checksum: hasil operasi bit sering overflow yang dimanfaatkan sebagai wrap-around.
  • API sistem operasi: banyak API POSIX memakai unsigned untuk ID proses, panjang buffer, dll.

Untuk perhitungan biasa yang bisa melibatkan nilai negatif (suhu, selisih waktu, koordinat relatif), gunakan tipe signed. Aturan praktis: pakai unsigned hanya jika nilai negatif tidak masuk akal secara domain, dan tidak ada operasi pengurangan yang bisa menghasilkan nilai negatif.

Dengan kedua varian ini, kita siap mempelajari modifier short, long, dan long long yang menyesuaikan ukuran tipe integer dengan kebutuhan kita.