String literal adalah teks yang diapit tanda kutip ganda, seperti "Halo" atau "Belajar C". Saat kita menulis literal di kode C, compiler menempatkan byte-byte literal tersebut di read-only data segment—area memori yang dipetasi sebagai hanya-baca oleh sistem operasi. Konsekuensinya, meskipun secara sintaksis kita bisa mendeklarasikan char *s = "literal"; (tanpa const), mencoba memodifikasi byte di dalam literal (s[0] = 'L';) adalah undefined behavior (UB): program biasanya crash dengan segmentation fault pada sistem modern. Pada chapter ini kita akan menelusuri bagaimana compiler menyimpan literal, bagaimana literal yang sama dapat di-sharing, dan apa yang terjadi bila kita melanggar aturan kebolehbacaan.

Literal dan Read-Only Memory #

Saat compiler memproses string literal, compiler menempatkan byte-byte literal tersebut ke dalam bagian read-only data segment dari executable. Pada saat runtime, sistem operasi memetasi area ini sebagai memori yang tidak dapat ditulis. Mari kita lihat bagaimana literal dipakai dalam berbagai bentuk escape sequence dan concatenation.

c
#include <stdio.h>

/* String literal di-decode saat compile */
int main(void) {
    /* Escape sequence dalam literal */
    char s1[] = "baris satu\nbaris dua";
    printf("%s\n", s1);

    /* Literal berdampingan otomatis digabung compiler */
    char s2[] = "satu " "dua " "tiga";
    printf("gabungan: %s\n", s2);

    /* Literal panjang boleh dipotong per baris dengan \ */
    char s3[] = "Ini teks yang sangat panjang \
yang dipotong menjadi beberapa baris.";
    printf("%s\n", s3);
    return 0;
}

Output yang Dihasilkan:

text
baris satu
baris dua
gabungan: satu dua tiga
Ini teks yang sangat panjang yang dipotong menjadi beberapa baris.

Pada s1, escape sequence \n di-decode oleh compiler menjadi byte newline (ASCII 10). Saat printf mencetak string, byte newline tersebut membuat kursor pindah ke baris baru. Pada s2, tiga literal berdampingan "satu ", "dua ", "tiga" otomatis digabung oleh compiler menjadi satu literal "satu dua tiga"—fitur ini bermanfaat untuk memecah literal panjang yang melebihi lebar layar editor. Pada s3, karakter \ di akhir baris memerintahkan compiler untuk melanjutkan literal ke baris berikutnya; backslash dan newline dihapus selama preprocessing sehingga hasil akhirnya satu literal tanpa jeda.

Compiler Sharing: Literal Identik Boleh Berbagi Alamat #

Compiler bebas menggabungkan dua literal identik menjadi satu region memori yang sama. Fitur ini disebut string pooling dan diaktifkan secara default pada banyak compiler modern. Mari kita uji apakah dua literal "halo" yang ditulis terpisah berbagi alamat.

c
#include <stdio.h>

/* Bandingkan address dua literal identik */
int main(void) {
    const char *a = "halo";
    const char *b = "halo";
    const char *c = "hai";

    printf("alamat a = %p\n", (const void *)a);
    printf("alamat b = %p\n", (const void *)b);
    printf("alamat c = %p\n", (const void *)c);

    printf("a == b ? %s\n", (a == b) ? "YA (compiler sharing)" : "TIDAK");
    printf("a == c ? %s\n", (a == c) ? "YA" : "TIDAK");

    /* Karakter di dalam literal bisa dibaca */
    printf("a[0] = %c, a[1] = %c\n", a[0], a[1]);
    return 0;
}

Output yang Dihasilkan:

text
alamat a = 0x563186952004
alamat b = 0x563186952004
alamat c = 0x563186952009
a == b ? YA (compiler sharing)
a == c ? TIDAK
a[0] = h, a[1] = a

Pada output di atas, kedua pointer a dan b menunjuk ke alamat yang sama (0x563186952004). Ini berarti compiler menggabungkan dua literal "halo" menjadi satu region di memori. Pointer c yang menunjuk ke literal berbeda "hai" memiliki alamat yang berbeda (0x563186952009, terpisah 5 byte—panjang "halo" + null terminator). Alamat aktual akan berbeda setiap kali program dijalankan karena ASLR (Address Space Layout Randomization), tetapi pola bahwa a == b tetap konsisten. Konsekuensi praktisnya: bila kita membandingkan dua pointer literal yang sama isinya dengan ==, hasilnya bisa true atau false tergantung implementasi compiler—selalu gunakan strcmp bila ingin membandingkan isi, bukan alamat.

Modifikasi String Literal adalah Undefined Behavior #

Inilah momen krusial: mencoba memutasi byte di dalam string literal adalah UB. Pada sistem modern dengan proteksi memori, program biasanya crash dengan segmentation fault. Mari kita lihat demonstrasinya.

c
/* Demonstrasi UB: memodifikasi string literal */
#include <stdio.h>

int main(void) {
    char *s = "literal";
    s[0] = 'L';   /* UB: literal di read-only memory */
    printf("s = %s\n", s);
    return 0;
}

Output yang Dihasilkan (program berakhir dengan segmentation fault):

text
Segmentation fault (exit code 139)

Mari kita telaah baris-baris kode di atas:

  1. Baris 6 (char *s = "literal";): variabel s bertipe char * (pointer ke char) dan menunjuk ke byte pertama dari literal "literal" di read-only data segment. Catatan: seharusnya kita menulis const char *s agar compiler memberi peringatan sejak compile time; tanpa const, kode ter-compile tanpa peringatan tetapi tetap menimbulkan UB saat runtime.
  2. Baris 7 (s[0] = 'L';): kita mencoba menulis karakter 'L' ke alamat yang ditunjuk s. Karena alamat tersebut berada di halaman memori yang dipetasi read-only, CPU memicu page fault dan kernel mengirim sinyal SIGSEGV ke proses. Program berakhir dengan exit code 139 (128 + 11, di mana 11 adalah nomor sinyal SIGSEGV).
  3. Baris 8 (printf(...)): tidak pernah dieksekusi karena program sudah crash di baris sebelumnya.

Ringkasan Konsep #

String literal ditempatkan compiler di read-only data segment—memori yang dipetasi hanya-baca oleh sistem operasi. Dua literal identik boleh berbagi alamat yang sama (string pooling), tetapi perilaku ini implementation-defined. Karena literal berada di memori hanya-baca, mencoba memutasi byte literal (s[0] = 'L'; di mana s menunjuk ke literal) adalah undefined behavior: program biasanya crash dengan segmentation fault. Aturan praktis: selalu deklarasikan pointer ke literal dengan const char * agar compiler mendeteksi upaya mutasi sejak compile time. Bila Anda butuh string yang dapat diubah, gunakan array char s[] = "..."; yang menyalin byte literal ke stack sehingga mutasi menjadi operasi yang sah. Pada chapter berikutnya kita akan membandingkan secara langsung kedua pola deklarasi ini dan implikasi mutabilitasnya.