Sebelum mengimplementasikan operasi CRUD, kita perlu mendefinisikan model data yang menjadi pondasi seluruh aplikasi. Pada bagian ini kita membuat contact.h yang berisi dua struct (Contact dan ContactList), satu enum (ContactStatus), beberapa konstanta ukuran field, dan prototipe fungsi yang akan diisi di bagian-bagian berikutnya. Tujuan bagian ini adalah memastikan kontrak data jelas sebelum logika ditulis — pendekatan “design first” yang menghindari refactor mahal di kemudian hari.
Konsep #
struct Contact merepresentasikan satu kontak dengan empat field: id (integer unik), nama, telp, dan email (string). Field id penting karena memungkinkan pengubahan dan penghapusan kontak tanpa ambiguitas — dua orang boleh punya nama yang sama, tetapi tidak boleh punya ID yang sama. Kita menetapkan id 0 sebagai “kosong” sehingga ID sah dimulai dari 1.
struct ContactList membungkus array Contact data[CONTACT_MAKS] bersama dua metadata: jumlah (berapa kontak yang aktif) dan id_berikutnya (ID berikutnya yang akan di-generate saat contact_tambah dipanggil). Tanpa id_berikutnya, kita harus memindai seluruh kontak untuk mencari ID maksimum setiap kali menambah — operasi O(n) yang bisa dihindari.
Ukuran field seperti CONTACT_NAMA_MAKS = 50 ditetapkan sebagai #define agar mudah diubah. Batas ini juga dipakai di strncpy untuk mencegah buffer overflow bila pengguna mengetik nama yang sangat panjang.
Implementasi #
Definisi struct di contact.h:
#define CONTACT_NAMA_MAKS 50
#define CONTACT_TELP_MAKS 20
#define CONTACT_EMAIL_MAKS 60
#define CONTACT_MAKS 100
typedef struct {
int id;
char nama[CONTACT_NAMA_MAKS];
char telp[CONTACT_TELP_MAKS];
char email[CONTACT_EMAIL_MAKS];
} Contact;
typedef struct {
Contact data[CONTACT_MAKS];
int jumlah;
int id_berikutnya;
} ContactList;Enum ContactStatus untuk melaporkan hasil operasi:
typedef enum {
CONTACT_OK = 0,
CONTACT_ERR_PENUH = 1,
CONTACT_ERR_TIDAK_DITEMUKAN = 2,
CONTACT_ERR_INPUT_INVALID = 3
} ContactStatus;Kode Lengkap #
Berkas contact.h lengkap untuk Bagian 1:
#ifndef CONTACT_H
#define CONTACT_H
#include <stdio.h>
#define CONTACT_NAMA_MAKS 50
#define CONTACT_TELP_MAKS 20
#define CONTACT_EMAIL_MAKS 60
#define CONTACT_MAKS 100
typedef struct {
int id;
char nama[CONTACT_NAMA_MAKS];
char telp[CONTACT_TELP_MAKS];
char email[CONTACT_EMAIL_MAKS];
} Contact;
typedef struct {
Contact data[CONTACT_MAKS];
int jumlah;
int id_berikutnya;
} ContactList;
typedef enum {
CONTACT_OK = 0,
CONTACT_ERR_PENUH = 1,
CONTACT_ERR_TIDAK_DITEMUKAN = 2,
CONTACT_ERR_INPUT_INVALID = 3
} ContactStatus;
void contact_list_init(ContactList *list);
ContactStatus contact_tambah(ContactList *list,
const char *nama,
const char *telp,
const char *email);
void contact_tampilkan(const ContactList *list, FILE *out);
int contact_cari(const ContactList *list, const char *nama);
ContactStatus contact_ubah(ContactList *list, int id,
const char *nama_baru,
const char *telp_baru,
const char *email_baru);
ContactStatus contact_hapus(ContactList *list, int id);
int contact_valid_nama(const char *nama);
#endif /* CONTACT_H */Stub minimal di contact.c agar build berhasil:
#include "contact.h"
void contact_list_init(ContactList *list) {
if (list == 0) return;
list->jumlah = 0;
list->id_berikutnya = 1;
}Pengujian #
Kompilasi stub untuk memverifikasi header syntactically valid:
cat > /tmp/p3b1_demo.c << 'EOF'
#include "contact.h"
#include <stdio.h>
int main(void) {
ContactList list;
contact_list_init(&list);
printf("jumlah = %d, id_berikutnya = %d\n",
list.jumlah, list.id_berikutnya);
return 0;
}
EOF
gcc -std=c11 -Wall -Wextra -I/home/z/my-project/content/c/proyek-c/proyek-03-manajemen-kontak/ -o /tmp/p3b1_demo /tmp/p3b1_demo.c /home/z/my-project/content/c/proyek-c/proyek-03-manajemen-kontak/contact.c && /tmp/p3b1_demoOutput yang Dihasilkan:
jumlah = 0, id_berikutnya = 1contact_list_init mengatur jumlah = 0 dan id_berikutnya = 1, persis seperti yang diharapkan. Mari kita telaah baris-baris kunci pada contact.h:
- Baris
int id;diContact: field pertama adalah ID integer unik. ID disimpan terpisah dari nama sehingga dua kontak dengan nama “Budi” tetap bisa dibedakan. - Baris
char nama[CONTACT_NAMA_MAKS];: array karakter fixed-size.CONTACT_NAMA_MAKS = 50berarti nama maksimal 49 karakter + 1 null terminator. - Baris
Contact data[CONTACT_MAKS];diContactList: array statis 100 elemen. Alokasi statis ini sederhana tetapi membatasi kapasitas; untuk aplikasi nyata kita akan memakai dynamic allocation (malloc/realloc). - Baris
int id_berikutnya;: counter ID yang naik setiap kalicontact_tambahdipanggil. Field ini menghindari pemindaian O(n) untuk mencari ID maksimum. - Baris
void contact_list_init(ContactList *list);: fungsi inisialisasi menerima pointer ke struct yang sudah dialokasikan pemanggil. Pola “init by pointer” umum di C — caller memiliki memori, callee mengisi field.
Bagian 2 akan mengisi contact_tambah dan contact_tampilkan.
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;
}