stdint.h dan inttypes.h

Bilangan bulat lebar tetap dan penentu printf portabel untuk kode lintas platform.

Daftar isi

Tipe int pada C tidak menjamin lebar yang sama di semua platform. Ketidakpastian itu menjadi masalah serius ketika program berkomunikasi lewat format berkas, protokol jaringan, atau register perangkat keras yang lebar bidangnya sudah ditetapkan. Topik ini membahas stdint.h yang menyediakan tipe bilangan bulat berlebar tetap beserta batas nilainya, lalu inttypes.h yang menyediakan penentu format printf agar pencetakan tetap benar di platform mana pun. Setelah topik ini dipahami, kode yang menuntut lebar byte tertentu tidak lagi bergantung pada kebetulan ukuran tipe.

Mengapa lebar int tidak dijamin#

Standar C hanya menetapkan batas minimum lebar untuk tipe dasar, yaitu int paling sedikit 16 bit dan long paling sedikit 32 bit, tanpa menetapkan nilai pastinya. Pada platform 64 bit modern, int umumnya berukuran 4 byte. Long berukuran 8 byte di Linux dan 4 byte di Windows, sehingga kode yang mengasumsikan samanya akan mengalami error halus ketika dipindahkan. Kelalaian seperti ini sulit ditemukan karena programnya berjalan benar di platform asalnya.

Tipe dasar juga menyimpan perbedaan penting pada operasi berlebih. Apabila variabel int dengan nilai terbesarnya ditambah satu, hasilnya merupakan perilaku tak terdefinisi. Pada tipe tak bertanda, aritmetikanya melingkar (wrap around) dan diatur ketat oleh standar. Program berikut menunjukkan lebar, batas, dan perilaku melingkar tersebut dengan tipe lebar tetap:

c lebar-tetap.c
#include <stdint.h>
#include <stdio.h>

int main(void)
{
    printf("sizeof(int32_t) = %zu byte\n", sizeof(int32_t));
    printf("sizeof(int64_t) = %zu byte\n", sizeof(int64_t));
    printf("INT32_MIN       = %d\n", INT32_MIN);
    printf("INT32_MAX       = %d\n", INT32_MAX);
    printf("UINT8_MAX       = %u\n", UINT8_MAX);

    uint32_t penghitung = UINT32_MAX;
    penghitung = penghitung + 1;   /* aritmetika tak bertanda melingkar */
    printf("UINT32_MAX + 1  = %u\n", penghitung);
    return 0;
}
text Keluaran
sizeof(int32_t) = 4 byte
sizeof(int64_t) = 8 byte
INT32_MIN       = -2147483648
INT32_MAX       = 2147483647
UINT8_MAX       = 255
UINT32_MAX + 1  = 0

Penambahan satu pada nilai terbesar uint32_t menghasilkan nol secara pasti, karena aturan melingkarnya didefinisikan oleh standar. Hal yang sama tidak boleh diharapkan dari tipe bertanda, karena pelampauan (overflow) pada int32_t adalah perilaku tak terdefinisi dan kompiler bebas menganggap kasus itu tidak pernah terjadi. Pemisahan tanggung jawab ini menjelaskan mengapa data protokol dan checksum lebih aman ditampung tipe tak bertanda.

Keluarga tipe lebar tetap pada stdint.h#

Header stdint.h menyediakan tipe bertanda dan tak bertanda pada lebar 8, 16, 32, dan 64 bit, yaitu int8_t, int16_t, int32_t, dan int64_t beserta pasangan tak bertandanya yang diawali huruf u. Tipe ini hanya tersedia apabila platform benar-benar memiliki tipe dasar dengan lebar yang persis sama, dan pada praktiknya seluruh platform modern menyediakannya. Masing-masing tipe punya makro batas, seperti INT32_MIN, INT32_MAX, dan UINT64_MAX, yang nilainya menyesuaikan tipe dasar di balik typedef-nya.

Selain lebar persis, stdint.h menyediakan dua keluarga pendamping untuk kasus yang tidak menuntut lebar tepat. Keluarga int_leastN_t menjamin lebar paling sedikit N bit dan berguna ketika hemat memori lebih penting daripada lebar pasti. Keluarga int_fastN_t memilih tipe tercepat yang lewarnya paling sedikit N bit dan cocok untuk variabel perulangan. Dengan memilih keluarga yang sesuai maksud, kode menyatakan kebutuhannya secara eksplisit alih-alih menyandarkannya pada tipe int.

Nama tipe pada stdint.h sengaja tidak menyebut tipe dasar di baliknya, karena pemetaan itu berbeda antarplatform, misalnya int64_t menjadi long di Linux 64 bit dan long long di Windows. Kode yang menulis int64_t tetap benar di keduanya, sedangkan kode yang menulis long secara langsung membawa asumsi platform ke dalam logikanya. Prinsip yang sama berlaku pada makro batasnya, yaitu INT64_MAX selalu menyesuaikan tipe dasar yang dipilih typedef-nya.

Penentu format portabel dari inttypes.h#

Masalah muncul ketika nilai bertipe int64_t dicetak dengan printf, karena int64_t tidak selalu bertipe long atau long long pada platform yang sama. Penentu yang benar di satu platform bisa menjadi tidak cocok di platform lain, dan ketidakcocokan penentu printf terhadap tipe argumennya merupakan perilaku tak terdefinisi:

c Contoh: penentu yang tidak portabel
// tidak-dijalankan
printf("%ld", nilai_64);   /* benar di Linux 64 bit, keliru di Windows */
printf("%lld", nilai_64);  /* benar di Windows, keliru di Linux 32 bit */

Header inttypes.h menyelesaikan masalah ini dengan makro penentu format yang meluas menjadi teks yang cocok dengan tipe di platform masing-masing. Keluarganya sistematis, yaitu PRIdN untuk desimal bertanda, PRIuN untuk desimal tak bertanda, dan PRIXN atau PRIxN untuk heksadesimal besar dan kecil, dengan N berupa lebar 8 sampai 64. Makro ini dipakai bersama penggabungan literal string, sehingga penentunya tetap bisa diberi lebar dan awalan:

c makro-format.c
#include <inttypes.h>
#include <stdio.h>

int main(void)
{
    int16_t kecil = -320;
    uint32_t ceksum = 3735928559u;
    uint64_t byte_arsip = 68719476736;

    printf("kecil      = %" PRId16 "\n", kecil);
    printf("ceksum     = %" PRIu32 " (%08" PRIX32 ")\n", ceksum, ceksum);
    printf("byte_arsip = %" PRIu64 "\n", byte_arsip);
    return 0;
}
text Keluaran
kecil      = -320
ceksum     = 3735928559 (DEADBEEF)
byte_arsip = 68719476736

Pada setiap pemanggilan printf, makro PRId16 dan kawan-kawannya meluas menjadi literal penentu yang tepat saat kompilasi, sehingga biayanya nol saat program berjalan. Pola %" PRIu64 " terlihat asing pada pandangan pertama, tetapi strukturnya sederhana. Tanda persen ditutup kutip, makro menyambung penentu formatnya, lalu kutip berikutnya melanjutkan teks. Untuk scanf tersedia keluarga serupa yang diawali SCN, seperti SCNu32, dan makronya juga mengembalikan penentu yang cocok dengan tipe argumen penampungnya.

Tipe penunjuk bilangan dan nilai maksimum#

Header inttypes.h juga menyediakan intptr_t dan uintptr_t, yaitu tipe bilangan bulat yang cukup lebar untuk menampung alamat pointer tanpa kehilangan informasi. Tipe ini berguna ketika alamat perlu disimpan sebagai angka sementara, misalnya untuk penghitungan hash atau pencatatan, tetapi bukan pengganti aritmetika pointer biasa. Program berikut menghitung selisih dua alamat dalam byte lewat tipe tersebut:

c selisih-pointer.c
#include <inttypes.h>
#include <stdio.h>

int main(void)
{
    int data[4] = {10, 20, 30, 40};

    intptr_t alamat_awal   = (intptr_t)&data[0];
    intptr_t alamat_tengah = (intptr_t)&data[2];

    printf("selisih byte data[2] - data[0] = %" PRIdPTR "\n",
           alamat_tengah - alamat_awal);
    return 0;
}
text Keluaran
selisih byte data[2] - data[0] = 8

Selisih dua elemen int adalah dua kali empat byte, yaitu 8, dan penentu PRIdPTR memastikan pencetakannya tetap benar pada platform dengan lebar pointer berapa pun. Stdint.h juga menyediakan intmax_t dan uintmax_t yang mampu menampung bilangan bulat terlebar di platformnya, dengan penentu PRIdMAX pada inttypes.h. Ketika perhitungan menengah berisiko melampaui lebar 64 bit, menampung nilai pada intmax_t adalah pilihan paling aman.

Pemakaian tipe ini sebaiknya dibatasi pada kasus yang benar-benar membutuhkan angka dari alamat, seperti kunci hash sederhana atau pencatatan diagnostik. Konversi bolak-balik antara pointer dan bilangan bulat tidak dijamin oleh standar C, kecuali nilai yang kembali persis sama dengan yang dikirim. Karena itu, aritmetika alamat lebih baik tetap dikerjakan dengan pointer biasa. Pada platform dengan pointer lebih lebar daripada tipe bilangan terbesarnya, intptr_t bahkan bisa tidak tersedia.

Kesalahan umum#

Empat kesalahan berikut paling sering muncul ketika tipe lebar tetap mulai dipakai.

  • Mencetak tipe lebar tetap dengan penentu yang ditebak. %d untuk uint64_t atau %ld untuk int64_t dapat berjalan benar di satu platform dan gagal diam-diam di platform lain. Gunakan makro PRI dari inttypes.h sejak baris pertama kode ditulis.
  • Mencetak uint8_t dengan %u. Nilai uint8_t dipromosikan menjadi int sebelum dikirim ke printf, sehingga pencocokan tipe menjadi tidak cocok. Pakai PRIu8 atau konversikan nilainya ke unsigned int lebih dahulu.
  • Mengharapkan pelampauan bertanda melingkar. Pelampauan pada int32_t adalah perilaku tak terdefinisi, berbeda dengan uint32_t yang melingkar dengan pasti. Tipe tak bertanda adalah pilihan untuk penghitung yang sengaja dibiarkan melingkar.
  • Lupa menyertakan inttypes.h. Makro PRI hanya ada di inttypes.h, sementara tipenya di stdint.h, sehingga kode yang hanya menyertakan stdint.h gagal dikompilasi saat makro dipakai. Sertakan keduanya bersamaan.

Ringkasan#

  • Standar C hanya menjamin lebar minimum tipe dasar, sedangkan stdint.h menyediakan intN_t dan uintN_t dengan lebar 8 sampai 64 bit yang persis.
  • Aritmetika tipe tak bertanda melingkar secara terdefinisi, sedangkan pelampauan tipe bertanda adalah perilaku tak terdefinisi.
  • Makro PRI pada inttypes.h meluas menjadi penentu printf yang cocok dengan tipe di setiap platform, dengan keluarga SCN untuk scanf dan PRIdPTR untuk intptr_t.
  • intptr_t menampung alamat sebagai bilangan bulat untuk kasus tertentu, dan intmax_t menampung bilangan terlebar yang tersedia di platformnya.

Referensi#