Setelah dua proyek pertama yang berfokus pada input numerik, proyek ketiga ini menampilkan tipe data baru: struct Contact yang menyimpan tiga field sekaligus (nama, telepon, email). Aplikasi manajemen kontak memberi kita kesempatan untuk berlatih mengelola koleksi data dalam array statis, melakukan operasi CRUD (Create, Read, Update, Delete) dengan pencarian linier, dan mempertahankan data ke file teks agar tetap ada setelah program ditutup.
Program yang akan kita bangun punya enam menu: tambah, tampilkan, cari, ubah, hapus, dan keluar. Saat keluar, seluruh kontak disimpan ke file kontak.txt dengan format pipa-separated (id|nama|telp|email). Saat program dibuka kembali, file dimuat otomatis sehingga pengguna tidak kehilangan data.
Tampilan Program Akhir #
Sesi pemakaian berikut menunjukkan alur lengkap: tambah dua kontak, tampilkan daftar, lalu keluar (yang menyimpan ke file):
Belum ada file kontak.txt. Memulai dengan daftar kosong.
=== Manajemen Kontak ===
1. Tambah
2. Tampilkan
3. Cari
4. Ubah
5. Hapus
6. Keluar
Pilih: Nama : Telp : Email : Kontak ditambahkan (id=1).
=== Manajemen Kontak ===
1. Tambah
2. Tampilkan
3. Cari
4. Ubah
5. Hapus
6. Keluar
Pilih: Nama : Telp : Email : Kontak ditambahkan (id=2).
=== Manajemen Kontak ===
1. Tambah
2. Tampilkan
3. Cari
4. Ubah
5. Hapus
6. Keluar
Pilih: ID Nama Telp Email
1 Budi Santoso 0812345678 [email protected]
2 Siti Rahma 0898765432 [email protected]
Total: 2 kontak
Menyimpan 2 kontak ke kontak.txt.
Terima kasih.Saat program dibuka kembali, daftar kontak dimuat otomatis dari file sehingga data sebelumnya tetap tersedia.
Tujuan Pembelajaran #
Setelah menyelesaikan proyek ini, kita mampu:
- mendefinisikan
structdengan field string dan integer, lalu mengelola array dari struct tersebut, - memakai
strncpydengan batasan panjang untuk mencegah buffer overflow, - mengimplementasikan operasi CRUD (tambah, tampilkan, cari, ubah, hapus) pada array statis,
- mempertahankan data ke file teks dengan format pipa-separated,
- memuat data dari file dan memvalidasi format saat parsing,
- menulis test yang memverifikasi persistensi (simpan lalu muat kembali).
Prasyarat #
Proyek ini memakai materi bab sebelumnya:
- Struct —
ContactdanContactList, - Array — penyimpanan
data[CONTACT_MAKS], - String —
strncpy,strcmp,strlen, - Fungsi dan Header Files — modularisasi
contact.h/contact.c/storage.h/storage.c, - File I/O Dasar —
fopen,fprintf,fscanf,fgets,fclose, - Enum —
ContactStatusdanStorageStatus.
Tahapan Pengerjaan #
Proyek dikerjakan dalam enam bagian berurutan:
- Bagian 1: Model Data — mendefinisikan
struct Contact,ContactList, dan enum status. - Bagian 2: Menambah dan Menampilkan Kontak — operasi
contact_tambahdancontact_tampilkan. - Bagian 3: Pencarian dan Pengubahan —
contact_cariberbasis nama,contact_ubahberbasis ID. - Bagian 4: Penghapusan Kontak —
contact_hapusdengan pergeseran elemen array. - Bagian 5: Penyimpanan ke File —
storage_simpandanstorage_muatdengan format pipa-separated. - Bagian 6: Testing dan Build —
tests.cdengan 19 test case danMakefileakhir.
Mulai dari Bagian 1 dan ikuti urutannya karena setiap bagian dibangun di atas berkas bagian sebelumnya. Berkas sumber lengkap tersedia pada bagian bawah halaman masing-masing part.
Definisikan struct Contact, ContactList, dan enum status.
Buka → # Bagian 2: Menambah dan Menampilkan KontakImplementasi contact_tambah dengan strncpy dan contact_tampilkan.
Buka → # Bagian 3: Pencarian dan PengubahanImplementasi contact_cari dengan strcmp dan contact_ubah berbasis ID.
Buka → # Bagian 4: Penghapusan KontakImplementasi contact_hapus dengan pergeseran elemen array.
Buka → # Bagian 5: Penyimpanan ke FileImplementasi storage_simpan dan storage_muat dengan format pipa-separated.
Buka → # Bagian 6: Testing dan BuildTulis tests.c dengan 19 test case dan susun Makefile akhir.
Buka → # Buka →Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
contact.c
Unduh berkas/* contact.c -- implementasi model dan operasi kontak. */
#include "contact.h"
#include <string.h>
void contact_list_init(ContactList *list) {
if (list == 0) return;
list->jumlah = 0;
list->id_berikutnya = 1;
}
int contact_valid_nama(const char *nama) {
if (nama == 0 || nama[0] == '\0') return 0;
/* Cek panjang maksimum. */
if (strlen(nama) >= CONTACT_NAMA_MAKS) return 0;
return 1;
}
ContactStatus contact_tambah(ContactList *list,
const char *nama,
const char *telp,
const char *email) {
if (list == 0) return CONTACT_ERR_INPUT_INVALID;
if (!contact_valid_nama(nama)) return CONTACT_ERR_INPUT_INVALID;
if (list->jumlah >= CONTACT_MAKS) return CONTACT_ERR_PENUH;
Contact *c = &list->data[list->jumlah];
c->id = list->id_berikutnya++;
strncpy(c->nama, nama, CONTACT_NAMA_MAKS - 1);
c->nama[CONTACT_NAMA_MAKS - 1] = '\0';
strncpy(c->telp, telp ? telp : "", CONTACT_TELP_MAKS - 1);
c->telp[CONTACT_TELP_MAKS - 1] = '\0';
strncpy(c->email, email ? email : "", CONTACT_EMAIL_MAKS - 1);
c->email[CONTACT_EMAIL_MAKS - 1] = '\0';
list->jumlah++;
return CONTACT_OK;
}
void contact_tampilkan(const ContactList *list, FILE *out) {
if (list == 0 || out == 0) return;
fprintf(out, "%-4s %-20s %-15s %-25s\n",
"ID", "Nama", "Telp", "Email");
for (int i = 0; i < list->jumlah; i++) {
const Contact *c = &list->data[i];
fprintf(out, "%-4d %-20s %-15s %-25s\n",
c->id, c->nama, c->telp, c->email);
}
fprintf(out, "Total: %d kontak\n", list->jumlah);
}
int contact_cari(const ContactList *list, const char *nama) {
if (list == 0 || nama == 0) return -1;
for (int i = 0; i < list->jumlah; i++) {
if (strcmp(list->data[i].nama, nama) == 0) {
return i;
}
}
return -1;
}
ContactStatus contact_ubah(ContactList *list, int id,
const char *nama_baru,
const char *telp_baru,
const char *email_baru) {
if (list == 0) return CONTACT_ERR_INPUT_INVALID;
for (int i = 0; i < list->jumlah; i++) {
if (list->data[i].id == id) {
if (nama_baru && !contact_valid_nama(nama_baru)) {
return CONTACT_ERR_INPUT_INVALID;
}
if (nama_baru) {
strncpy(list->data[i].nama, nama_baru, CONTACT_NAMA_MAKS - 1);
list->data[i].nama[CONTACT_NAMA_MAKS - 1] = '\0';
}
if (telp_baru) {
strncpy(list->data[i].telp, telp_baru, CONTACT_TELP_MAKS - 1);
list->data[i].telp[CONTACT_TELP_MAKS - 1] = '\0';
}
if (email_baru) {
strncpy(list->data[i].email, email_baru, CONTACT_EMAIL_MAKS - 1);
list->data[i].email[CONTACT_EMAIL_MAKS - 1] = '\0';
}
return CONTACT_OK;
}
}
return CONTACT_ERR_TIDAK_DITEMUKAN;
}
ContactStatus contact_hapus(ContactList *list, int id) {
if (list == 0) return CONTACT_ERR_INPUT_INVALID;
for (int i = 0; i < list->jumlah; i++) {
if (list->data[i].id == id) {
/* Geser elemen setelahnya ke depan. */
for (int j = i; j < list->jumlah - 1; j++) {
list->data[j] = list->data[j + 1];
}
list->jumlah--;
return CONTACT_OK;
}
}
return CONTACT_ERR_TIDAK_DITEMUKAN;
}
main.c
Unduh berkas/* main.c -- titik masuk aplikasi manajemen kontak. */
#include "contact.h"
#include "storage.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define DB_PATH "kontak.txt"
/* Membaca satu baris dari stdin, memotong newline. */
static void baca_baris(const char *prompt, char *buf, size_t ukuran) {
printf("%s", prompt);
if (fgets(buf, (int)ukuran, stdin) == 0) {
buf[0] = '\0';
return;
}
size_t n = strlen(buf);
while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r')) {
buf[--n] = '\0';
}
}
static int baca_int(const char *prompt, int *out) {
char buf[32];
baca_baris(prompt, buf, sizeof(buf));
char *sisa = 0;
long v = strtol(buf, &sisa, 10);
if (sisa == buf) return 0;
*out = (int)v;
return 1;
}
static void menu_tambah(ContactList *list) {
char nama[CONTACT_NAMA_MAKS];
char telp[CONTACT_TELP_MAKS];
char email[CONTACT_EMAIL_MAKS];
baca_baris("Nama : ", nama, sizeof(nama));
baca_baris("Telp : ", telp, sizeof(telp));
baca_baris("Email : ", email, sizeof(email));
ContactStatus s = contact_tambah(list, nama, telp, email);
switch (s) {
case CONTACT_OK:
printf("Kontak ditambahkan (id=%d).\n", list->data[list->jumlah - 1].id);
break;
case CONTACT_ERR_PENUH:
printf("Daftar penuh.\n"); break;
case CONTACT_ERR_INPUT_INVALID:
printf("Input tidak valid (nama wajib diisi).\n"); break;
default:
printf("Error tidak dikenal.\n"); break;
}
}
static void menu_ubah(ContactList *list) {
int id;
if (!baca_int("ID kontak yang diubah: ", &id)) {
printf("ID tidak valid.\n"); return;
}
char nama[CONTACT_NAMA_MAKS];
char telp[CONTACT_TELP_MAKS];
char email[CONTACT_EMAIL_MAKS];
baca_baris("Nama baru (kosongkan jika tidak diubah) : ", nama, sizeof(nama));
baca_baris("Telp baru (kosongkan jika tidak diubah) : ", telp, sizeof(telp));
baca_baris("Email baru (kosongkan jika tidak diubah): ", email, sizeof(email));
ContactStatus s = contact_ubah(list, id,
nama[0] ? nama : 0,
telp[0] ? telp : 0,
email[0] ? email : 0);
if (s == CONTACT_OK) printf("Kontak diubah.\n");
else if (s == CONTACT_ERR_TIDAK_DITEMUKAN) printf("Kontak tidak ditemukan.\n");
else printf("Input tidak valid.\n");
}
static void menu_hapus(ContactList *list) {
int id;
if (!baca_int("ID kontak yang dihapus: ", &id)) {
printf("ID tidak valid.\n"); return;
}
ContactStatus s = contact_hapus(list, id);
if (s == CONTACT_OK) printf("Kontak dihapus.\n");
else if (s == CONTACT_ERR_TIDAK_DITEMUKAN) printf("Kontak tidak ditemukan.\n");
}
static void menu_cari(const ContactList *list) {
char nama[CONTACT_NAMA_MAKS];
baca_baris("Nama yang dicari: ", nama, sizeof(nama));
int idx = contact_cari(list, nama);
if (idx < 0) {
printf("Kontak dengan nama '%s' tidak ditemukan.\n", nama);
} else {
const Contact *c = &list->data[idx];
printf("Ditemukan: [%d] %s | %s | %s\n",
c->id, c->nama, c->telp, c->email);
}
}
int main(void) {
ContactList list;
contact_list_init(&list);
StorageStatus sm = storage_muat(&list, DB_PATH);
if (sm == STORAGE_OK) {
printf("Memuat %d kontak dari %s.\n", list.jumlah, DB_PATH);
} else if (sm == STORAGE_ERR_BUKA_FILE) {
printf("Belum ada file %s. Memulai dengan daftar kosong.\n", DB_PATH);
} else {
printf("Peringatan: format file rusak (kode %d).\n", sm);
}
int lanjut = 1;
while (lanjut) {
printf("\n=== Manajemen Kontak ===\n");
printf("1. Tambah\n2. Tampilkan\n3. Cari\n4. Ubah\n5. Hapus\n6. Keluar\n");
int pilihan = 0;
if (!baca_int("Pilih: ", &pilihan)) { printf("Pilihan tidak valid.\n"); continue; }
switch (pilihan) {
case 1: menu_tambah(&list); break;
case 2: contact_tampilkan(&list, stdout); break;
case 3: menu_cari(&list); break;
case 4: menu_ubah(&list); break;
case 5: menu_hapus(&list); break;
case 6: lanjut = 0; break;
default: printf("Pilihan tidak dikenal.\n"); break;
}
}
StorageStatus ss = storage_simpan(&list, DB_PATH);
if (ss == STORAGE_OK) {
printf("Menyimpan %d kontak ke %s.\n", list.jumlah, DB_PATH);
} else {
printf("Gagal menyimpan (kode %d).\n", ss);
}
printf("Terima kasih.\n");
return 0;
}
storage.c
Unduh berkas/* storage.c -- persistensi kontak ke file teks. */
#include "storage.h"
#include <stdio.h>
#include <string.h>
/* Memotong newline di akhir string (kalau ada). */
static void potong_newline(char *s) {
size_t n = strlen(s);
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) {
s[--n] = '\0';
}
}
StorageStatus storage_simpan(const ContactList *list, const char *path) {
if (list == 0 || path == 0) return STORAGE_ERR_BUKA_FILE;
FILE *f = fopen(path, "w");
if (f == 0) return STORAGE_ERR_BUKA_FILE;
fprintf(f, "%d\n", list->jumlah);
for (int i = 0; i < list->jumlah; i++) {
const Contact *c = &list->data[i];
fprintf(f, "%d|%s|%s|%s\n",
c->id, c->nama, c->telp, c->email);
}
if (fclose(f) != 0) return STORAGE_ERR_TULIS;
return STORAGE_OK;
}
StorageStatus storage_muat(ContactList *list, const char *path) {
if (list == 0 || path == 0) return STORAGE_ERR_BUKA_FILE;
FILE *f = fopen(path, "r");
if (f == 0) return STORAGE_ERR_BUKA_FILE;
int jumlah = 0;
if (fscanf(f, "%d\n", &jumlah) != 1) {
fclose(f);
return STORAGE_ERR_FORMAT;
}
if (jumlah < 0 || jumlah > CONTACT_MAKS) {
fclose(f);
return STORAGE_ERR_FORMAT;
}
char baris[256];
int id_terbesar = list->id_berikutnya - 1;
for (int i = 0; i < jumlah; i++) {
if (fgets(baris, sizeof(baris), f) == 0) {
fclose(f);
return STORAGE_ERR_FORMAT;
}
potong_newline(baris);
int id;
char nama[CONTACT_NAMA_MAKS];
char telp[CONTACT_TELP_MAKS];
char email[CONTACT_EMAIL_MAKS];
/* sscanf dengan %[^\|] untuk membaca sampai '|'. */
int parsed = sscanf(baris, "%d|%49[^|]|%19[^|]|%59[^\n]",
&id, nama, telp, email);
if (parsed < 4) {
fclose(f);
return STORAGE_ERR_FORMAT;
}
if (list->jumlah >= CONTACT_MAKS) break;
Contact *c = &list->data[list->jumlah];
c->id = id;
strncpy(c->nama, nama, CONTACT_NAMA_MAKS - 1);
c->nama[CONTACT_NAMA_MAKS - 1] = '\0';
strncpy(c->telp, telp, CONTACT_TELP_MAKS - 1);
c->telp[CONTACT_TELP_MAKS - 1] = '\0';
strncpy(c->email, email, CONTACT_EMAIL_MAKS - 1);
c->email[CONTACT_EMAIL_MAKS - 1] = '\0';
list->jumlah++;
if (id > id_terbesar) id_terbesar = id;
}
list->id_berikutnya = id_terbesar + 1;
fclose(f);
return STORAGE_OK;
}
tests.c
Unduh berkas/* tests.c -- rangkaian test untuk modul contact dan storage. */
#include "contact.h"
#include "storage.h"
#include <stdio.h>
#include <string.h>
static int lulus = 0;
static int gagal = 0;
#define PERIKSA(kondisi, nama) \
do { \
if (kondisi) { lulus++; printf("[LULUS] %s\n", nama); } \
else { gagal++; printf("[GAGAL] %s\n", nama); } \
} while (0)
static void test_tambah_dan_tampilkan(void) {
ContactList list;
contact_list_init(&list);
ContactStatus s1 = contact_tambah(&list, "Budi", "0811", "[email protected]");
ContactStatus s2 = contact_tambah(&list, "Siti", "0812", "[email protected]");
PERIKSA(s1 == CONTACT_OK && s2 == CONTACT_OK, "Tambah 2 kontak berhasil");
PERIKSA(list.jumlah == 2, "Jumlah = 2 setelah tambah");
PERIKSA(list.data[0].id == 1 && list.data[1].id == 2,
"ID auto-increment 1, 2");
}
static void test_tambah_nama_kosong(void) {
ContactList list;
contact_list_init(&list);
ContactStatus s = contact_tambah(&list, "", "0811", "[email protected]");
PERIKSA(s == CONTACT_ERR_INPUT_INVALID, "Nama kosong ditolak");
PERIKSA(list.jumlah == 0, "Tidak ada kontak ditambahkan");
}
static void test_cari(void) {
ContactList list;
contact_list_init(&list);
contact_tambah(&list, "Budi", "0811", "[email protected]");
contact_tambah(&list, "Siti", "0812", "[email protected]");
int idx = contact_cari(&list, "Siti");
PERIKSA(idx == 1, "Cari 'Siti' -> indeks 1");
PERIKSA(contact_cari(&list, "Tidak Ada") == -1, "Cari nama tidak ada -> -1");
}
static void test_ubah(void) {
ContactList list;
contact_list_init(&list);
contact_tambah(&list, "Budi", "0811", "[email protected]");
ContactStatus s = contact_ubah(&list, 1, "Budi Santoso", 0, 0);
PERIKSA(s == CONTACT_OK, "Ubah kontak id=1 berhasil");
PERIKSA(strcmp(list.data[0].nama, "Budi Santoso") == 0,
"Nama berubah menjadi 'Budi Santoso'");
PERIKSA(strcmp(list.data[0].telp, "0811") == 0,
"Telp tetap '0811' (tidak diubah)");
}
static void test_hapus(void) {
ContactList list;
contact_list_init(&list);
contact_tambah(&list, "Budi", "0811", "[email protected]");
contact_tambah(&list, "Siti", "0812", "[email protected]");
contact_tambah(&list, "Andi", "0813", "[email protected]");
ContactStatus s = contact_hapus(&list, 2);
PERIKSA(s == CONTACT_OK, "Hapus id=2 berhasil");
PERIKSA(list.jumlah == 2, "Jumlah = 2 setelah hapus");
PERIKSA(strcmp(list.data[1].nama, "Andi") == 0,
"Setelah hapus, elemen 1 = 'Andi'");
PERIKSA(contact_hapus(&list, 999) == CONTACT_ERR_TIDAK_DITEMUKAN,
"Hapus id tidak ada -> error");
}
static void test_penyimpanan(void) {
ContactList list;
contact_list_init(&list);
contact_tambah(&list, "Budi", "0811", "[email protected]");
contact_tambah(&list, "Siti", "0812", "[email protected]");
StorageStatus ss = storage_simpan(&list, "/tmp/test_kontak.txt");
PERIKSA(ss == STORAGE_OK, "Simpan ke /tmp/test_kontak.txt");
ContactList dimuat;
contact_list_init(&dimuat);
ss = storage_muat(&dimuat, "/tmp/test_kontak.txt");
PERIKSA(ss == STORAGE_OK, "Muat dari /tmp/test_kontak.txt");
PERIKSA(dimuat.jumlah == 2, "Jumlah dimuat = 2");
PERIKSA(strcmp(dimuat.data[0].nama, "Budi") == 0,
"Kontak pertama dimuat: 'Budi'");
PERIKSA(dimuat.id_berikutnya == 3,
"id_berikutnya = 3 setelah muat");
}
int main(void) {
test_tambah_dan_tampilkan();
test_tambah_nama_kosong();
test_cari();
test_ubah();
test_hapus();
test_penyimpanan();
printf("\nRingkasan: %d lulus, %d gagal\n", lulus, gagal);
return gagal == 0 ? 0 : 1;
}