Jika program kita bertambah besar, maka akan lebih baik jika kita memisah-misah program kita menjadi beberapa file sumber, agar pengelolaannya menjadi lebih mudah, dan agar kita lebih mudah jika ingin memakai sebagian saja dari program kita di waktu yang akan datang (code reusability, nama kerennya).
Misalnkan kita pisahkan program sederhana kita menjadi tiga file:
File hello.h
, yang berisi deklarasi dari sebuah fungsi print_hello()
:
/* file hello.h */
#ifndef __HELLO_H__
#define __HELLO_H__
void print_hello(void);
#endif
File hello.c
, yang berisi definisi dari fungsi print_hello()
:
/* file hello.c */
#include <stdio.h>
#include "hello.h"
void print_hello(void)
puts("Hello world");
};
Dan ini adalah file main.c
yang menggunakan fungsi print_hello()
di atas:
/* file main.c */
#include "hello.h"
int main(void)
{
print_hello();
return 0;
};
Catatan:
print_hello()
di header file di atas disebut dideklarasikan (karena body fungsinya belum ada), sedangkan fungsi print_hello()
di file hello.c
disebut didefinisikan (karena di sini body fungsinya ada).
print_hello
secara eksplisit dengan parameter (void)
. Bedanya dengan C++, kalau di C, jika suatu fungsi didefinisikan dengan parameter kosong (misalnya void print_hello()
>), maka artinya fungsi ini bisa menerima parameter apa saja, sehingga kita bisa memanggil fungsi itu dengan print_hello(1)
, print_hello("error loe!")
, dan lain-lain. Kompiler maupun linker tidak akan menganggap pemanggilan ini sebagai error; argumen yang diberikan pada fungsi akan secara otomatis diabaikan. Namun meskipun tidak error, tentunya ini merupakan praktik yang tidak 'bertanggung jawab'.
Kelakuan seperti ini merupakan standar ANSI, jadi bukannya si kompiler yang bodoh.
hello.h
, kita menggunakan preprocesor #ifndef
, #define
, dan #endif
untuk mengatur kapan file hello.h
ini akan diproses oleh kompiler. Dengan preprosesor #ifndef __HELLO_H__
(artinya 'if __HELLO_H__ is not defined'), kita memberitahu kompiler bahwa blok di bawah #ifndef
sampai #endif
hendaknya hanya diproses jika __HELLO_H__
tidak terdefinisi (tidak diproses artinya dianggap tidak ada, atau seolah-olah blok itu blank). Di lain pihak, setelah preprosesor #ifndef
kita segera mendefinisikan simbol __HELLO_H__
itu (dengan preprosesor #define
).
Ketika kompiler memproses hello.h
untuk pertama kalinya (yaitu ketika pertama kali menemukan preprosesor #include "hello.h"
di file main.c
atau hello.c
, tergantung file mana yang disebutkan terlebih dahulu ketika memanggil gcc
), maka kompiler tahu bahwa simbol __HELLO_H__
belum terdefinisi, sehingga blok setelah #ifndef
akan diproses. Setelah itu, kompiler menemukan preprosesor #define __HELLO_H__
yang memerintahkan agar kompiler mendefinisikan simbol __HELLO_H__
tersebut. Lalu kompiler memproses baris-baris di bawahnya seperti biasa.
Ketika kompiler memproses file hello.h
untuk kedua kalinya, blok di bawah #ifndef
akan di-skip, karena simbol __HELLO_H__
sudah terdefinisi.
Praktik ini biasanya kita lakukan untuk menghindari error 'suatu variabel/tipe/simbol dideklarasikan lebih dari satu kali' (karena kita mendeklarasikan variabel/tipe/simbol itu di header file, sedangkan header file bisa diproses lebih dari satu kali, setiap kompiler menemukan preprosesor #include
).
Praktek ini lazim kita jumpai di hampir semua header file, dan memang sebaiknya kita lakukan di header file kita. Namun hati-hati dalam memilih nama simbol untuk preprosesor, jangan sampai bentrok dengan simbol lain yang ada di header-header lain.
Untuk membuat program executable dari file-file sumber di atas, kita ketik perintah berikut:
$ gcc -o hello hello.c main.c
Parameter -o hello
pada perintah di atas memberitahu gcc
agar memberi nama output filenya hello
, sedangkan parameter-parameter berikutnya menunjukkan file-file sumber yang harus di-compile.
Catatan:
perhatikan bahwa kita tidak pernah memberikan header file (dalam hal ini hello.h
) sebagai file sumber yang harus di-compile oleh gcc
(ataupun kompiler yang lain). File .h
akan otomatis diproses jika ada file .c
yang meng-#include
-nya.
Meskipun contoh kita kali ini sangat sederhana, namun ada sebuah aspek penting untuk diperhatikan, dan yang berguna untuk membangun program yang lebih besar dan kompleks.
Dalam program kita di atas, kedua file sumber (yaitu main.c
dan hello.c
) sama-sama bergantung pada deklarasi yang terdapat pada header file hello.h
. Artinya jika header file ini berubah (misalnya kita merubah fungsi void print_hello(void)
menjadi void print_hello(const char *from)
), maka kita harus meng-compile ulang kedua file sumber tersebut.
Namun kedua file .c
tidak tergantung satu dengan yang lain. Jadi misalnya kita merubah isi dari fungsi print_hello()
di file hello.c
(misalnya mengganti puts
dengan printf
), kita tidak perlu meng-compile ulang file main.c
. Demikian pula sebaliknya, jika kita merubah-rubah file main.c
, kita tidak perlu meng-compile ulang file hello.c
.
Keterhubungan antar file-file sumber ini disebut dependency.
Jika program kita kecil seperti contoh kita ini, kita masih bisa ingat dependency antar file, dan bisa meng-compile dengan benar. Namun ada cara lain yang lebih baik, yaitu dengan menggunakan Makefile.
Memang inilah tujuan utama dari make
dan Makefile
, yaitu mengeksekusi perintah-perintah sesuai dengan ketergantungan/dependency dari perintah-perintah yang lain.
Sesuai dengan dependency-nya, Makefile
untuk program kita:
all: hello
hello: hello.o main.o
gcc -o hello hello.o main.o
hello.o: hello.c hello.h
gcc -c hello.c
main.o: main.c hello.h
gcc -c main.c
Pada Makefile di atas, terlihat bahwa hello.o
tergantung dari hello.c
dan hello.h
, sehingga jika file-file ini berubah, maka hello.o
akan di-compile lagi.
Sedangkan file main.o
tergantung dari main.c
dan hello.h
, sehingga bila file-file ini berubah, maka main.o
akan di-recompile lagi.
Dan jika main.o
atau hello.o
ada yang berubah, maka file hello
akan di-recompile lagi.