Untuk membaca dan menulis teks baris-demi-baris, pasangan fgets/fputs adalah pilihan paling aman di C. fgets selalu menerima ukuran buffer sebagai argumen, sehingga tidak akan menulis melampaui kapasitas yang kita berikan—berbeda dengan gets legendaris yang sudah dihapus di C11 karena memicu begitu banyak eksploitasi buffer overflow. fputs menulis string apa adanya ke stream tanpa menambahkan apapun, memberi kita kendali penuh atas newline. Pada chapter ini kita akan melihat bagaimana fgets memotong baris panjang menjadi beberapa panggilan, dan kenapa ini justru menjadi fitur keamanan paling penting di library I/O C.
Kontrak fgets dan fputs #
ISO/IEC 9899 §7.21.7.2 dan §7.21.7.4 mendefinisikan:
char *fgets(char *restrict s, int n, FILE *restrict stream);
int fputs(const char *restrict s, FILE *restrict stream);Kontrak fgets:
- Membaca sampai salah satu dari tiga kondisi — menemui
\n(yang juga ikut disimpan), mengisin-1byte, atau menemui EOF. - Selalu menambahkan null terminator — total maksimum
nbyte (termasuk terminator). - Mengembalikan
sbila berhasil,NULLbila gagal — gagal terutama saat EOF langsung ditemui di awal panggilan. nadalahint, bukansize_t— nilain <= 0menyebabkan perilaku undefined pada beberapa implementasi; selalu pakain >= 1.
Kontrak fputs:
- Menulis string tanpa null terminator ke stream — karakter
\0di tengah string akan menghentikan penulisan (karenafputsmenggunakanstrleninternal). - Tidak menambahkan
\n— berbeda denganputsyang otomatis menambahkan newline. - Mengembalikan non-negatif bila berhasil,
EOFbila gagal.
Pola Aman Membaca Baris dengan fgets #
Pola yang selalu kita pakai adalah memanggil fgets di dalam loop while, dengan argumen kedua berupa sizeof(buffer) agar otomatis benar bila ukuran buffer berubah.
#include <stdio.h>
#include <string.h>
int main(void) {
/* Tulis beberapa baris dengan fputs */
FILE *f = fopen("/tmp/test_lines.txt", "w");
if (!f) { perror("fopen"); return 1; }
fputs("Baris pertama\n", f);
fputs("Baris kedua yang lebih panjang dari biasanya\n", f);
fputs("Baris ketiga tanpa newline di akhir", f); /* tanpa \n */
fclose(f);
/* Baca dengan fgets, batasi ukuran buffer */
f = fopen("/tmp/test_lines.txt", "r");
if (!f) { perror("fopen read"); return 1; }
char buf[16]; /* sengaja kecil untuk demo pemotongan */
int line_no = 0;
while (fgets(buf, sizeof(buf), f) != NULL) {
line_no++;
size_t len = strlen(buf);
int has_newline = (len > 0 && buf[len-1] == '\n');
printf("Baris %d (len=%zu, newline=%d): \"%s\"\n",
line_no, len, has_newline, buf);
}
fclose(f);
return 0;
}Output yang Dihasilkan:
Baris 1 (len=14, newline=1): "Baris pertama
"
Baris 2 (len=15, newline=0): "Baris kedua yan"
Baris 3 (len=15, newline=0): "g lebih panjang"
Baris 4 (len=15, newline=1): " dari biasanya
"
Baris 5 (len=15, newline=0): "Baris ketiga ta"
Baris 6 (len=15, newline=0): "npa newline di "
Baris 7 (len=5, newline=0): "akhir"Perhatikan apa yang terjadi pada baris 2 file asli ("Baris kedua yang lebih panjang dari biasanya\n"). Karena buffer buf hanya berisi 16 byte, fgets hanya membaca 15 karakter ("Baris kedua yan") per panggilan dan tidak sempat menemui \n. Pemanggilan kedua membaca sisa baris ("g lebih panjang"), dan baru pada pemanggilan ketiga (" dari biasanya\n") fgets menemui \n dan berhenti dengan newline di akhir. Inilah sebabnya fgets aman: tidak peduli seberapa panjang baris di file, ia tidak akan pernah menulis melebihi sizeof(buf).
Mari kita telaah baris-baris kode di atas:
- Baris 8 (
fputs("Baris pertama\n", f);):fputsmenulis string apa adanya. Kita sendiri yang menyertakan\nbila ingin baris baru. - Baris 12 (
fputs("Baris ketiga tanpa newline di akhir", f);): baris terakhir sengaja tanpa\nuntuk mendemonstrasikan bahwafgetstetap bisa membacanya—berhenti karena EOF, bukan karena\n. - Baris 18 (
char buf[16];): buffer sengaja kecil (16 byte) agar kita bisa melihatfgetsmemotong baris panjang. Pada produksi, gunakan buffer yang cukup besar (misalnya 4096 atau 8192) untuk mengurangi pemanggilan fungsi. - Baris 20 (
while (fgets(buf, sizeof(buf), f) != NULL)): argumen kedua adalahsizeof(buf)— otomatis benar bila ukuran buffer berubah.fgetsmengembalikanNULLbila langsung menemui EOF (tidak ada karakter yang dibaca). - Baris 23 (
int has_newline = (len > 0 && buf[len-1] == '\n');): kita bisa memeriksa apakah baris selesai dibaca dengan melihat apakah karakter terakhir adalah\n. Bila bukan, berarti baris dipotong karena buffer penuh atau EOF. - Baris 24-25 (
printf("Baris %d (len=%zu, newline=%d): \"%s\"\n", ...)): kita mencetak nomor baris (yang sebenarnya adalah nomor pemanggilan, bukan nomor baris file), panjang string, dan apakah ada newline.%zuadalah specifier untuksize_t.
Perbandingan: gets (Dihapus di C11) vs fgets #
Sebelum C11, library C menyediakan fungsi gets yang membaca baris dari stdin tanpa parameter ukuran:
/* Versi lama - BERBAHAYA, dihapus di C11 */
char *gets(char *s);Karena gets tidak menerima parameter ukuran buffer, ia akan terus membaca sampai menemui \n—tidak peduli seberapa besar buffer yang kita sediakan. Inilah penyebab eksploitasi klasik stack smashing: penyerang mengirim input yang sangat panjang ke program yang memakai gets, menimpa return address di stack, dan mengarahkan eksekusi ke kode pilihan penyerang. C11 (ISO/IEC 9899:2011) menghapus gets sepenuhnya dan menggantinya dengan gets_s opsional yang menerima parameter ukuran.
Mari kita bandingkan secara langsung: perhatikan bagaimana fgets membatasi penulisan, sementara gets (bila masih ada di compiler lama) tidak.
#include <stdio.h>
#include <string.h>
/* Demo: fgets dengan buffer kecil tidak akan overflow */
int main(void) {
/* Buat input simulasi dari file: kalimat panjang */
FILE *f = fopen("/tmp/test_long_input.txt", "w");
if (!f) return 1;
fputs("Kalimat ini sangat panjang dan akan melebihi buffer kecil\n", f);
fclose(f);
f = fopen("/tmp/test_long_input.txt", "r");
if (!f) return 1;
char kecil[8]; /* buffer 8 byte */
if (fgets(kecil, sizeof(kecil), f) != NULL) {
printf("Isi kecil (len=%zu): \"%s\"\n", strlen(kecil), kecil);
}
/* Lanjut baca sisa baris */
char lanjut[64];
if (fgets(lanjut, sizeof(lanjut), f) != NULL) {
printf("Sisa (len=%zu): \"%s\"\n", strlen(lanjut), lanjut);
}
fclose(f);
return 0;
}Output yang Dihasilkan:
Isi kecil (len=7): "Kalimat"
Sisa (len=51): " ini sangat panjang dan akan melebihi buffer kecil
"Pada output di atas, fgets(kecil, 8, f) hanya membaca 7 karakter pertama ("Kalimat") karena harus menyisakan 1 byte untuk null terminator. Sisa baris tetap ada di stream dan dibaca oleh fgets berikutnya. Bila kita memakai gets(kecil) pada kode lama, seluruh baris (60+ byte) akan ditulis ke kecil yang hanya berkapasitas 8 byte—buffer overflow yang menghancurkan stack.
fputs Tanpa newline Tambahan #
Berbeda dengan puts yang otomatis menambahkan \n, fputs menulis persis apa yang kita berikan. Ini berguna ketika kita ingin menulis string tanpa newline (misalnya menulis header CSV lalu data per baris).
#include <stdio.h>
int main(void) {
FILE *f = fopen("/tmp/test_fputs_demo.csv", "w");
if (!f) { perror("fopen"); return 1; }
/* Header CSV tanpa newline di tengah */
fputs("nama,umur,nilai\n", f);
/* Data per baris */
fputs("Budi,20,85.5\n", f);
fputs("Siti,21,92.3\n", f);
fputs("Andi,19,78.0\n", f);
fclose(f);
/* Baca dan tampilkan untuk verifikasi */
f = fopen("/tmp/test_fputs_demo.csv", "r");
if (!f) { perror("fopen read"); return 1; }
char line[64];
while (fgets(line, sizeof(line), f) != NULL) {
printf(">> %s", line);
}
fclose(f);
return 0;
}Output yang Dihasilkan:
>> nama,umur,nilai
>> Budi,20,85.5
>> Siti,21,92.3
>> Andi,19,78.0char buf[256];
if (fgets(buf, sizeof(buf), stdin) != NULL) {
buf[strcspn(buf, "\n")] = '\0'; /* hapus \n */
/* gunakan buf... */
}