Menu

Prototipe Fungsi di C: Deklarasi vs Definisi

Kenapa C perlu melihat bentuk sebuah fungsi sebelum kamu memanggilnya: menulis prototipe, memperbaiki error implicit declaration, menaruh prototipe di header file, dan perbedaan sebenarnya antara f(void) dan f().

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Compiler Membaca dari Atas ke Bawah

Compiler C memproses file sumber dalam satu kali lintasan, dari baris pertama sampai terakhir. Saat sampai di sebuah pemanggilan fungsi, compiler harus sudah tahu tiga hal: apa yang dikembalikan fungsi itu, berapa argumen yang diterimanya, dan apa tipe-tipenya. Tanpa itu, compiler tidak bisa menghasilkan kode yang benar atau memeriksa pemanggilannya.

Jadi ini bermasalah:

#include <stdio.h>

int main(void) {
    printf("%d\n", add(2, 3));   /* add has not been seen yet */
    return 0;
}

int add(int a, int b) {
    return a + b;
}

Solusinya adalah prototipe: signature fungsi yang ditulis sebelum pemanggilan, dengan titik koma di tempat body seharusnya berada.

Sekarang main bisa ditaruh di depan, dan beginilah sebagian besar file C disusun: prototipe di bagian atas menyatakan apa yang disediakan file itu, main terbaca sebagai garis besarnya, dan detailnya menyusul di bawah.

Deklarasi vs Definisi

Dua kata yang mudah tertukar dan layak dibedakan:

  • Deklarasi menyatakan bahwa sebuah fungsi ada dan memberikan tipenya. Deklarasi diakhiri titik koma dan tidak punya body. Kamu boleh menulisnya sesering yang kamu mau.
  • Definisi memberikan body-nya. Definisi harus muncul tepat sekali di seluruh program: dua definisi untuk fungsi yang sama adalah error linker ("multiple definition of").
int add(int a, int b);                        /* declaration (prototype) */
int add(int a, int b) { return a + b; }       /* definition - also a declaration */

Nama parameter bersifat opsional di prototipe; hanya tipenya yang penting bagi compiler:

int add(int, int);              /* legal, and equivalent */
int add(int a, int b);          /* better: the names document the order */

Tetap tulis namanya. void drawRect(int, int, int, int); tidak memberi tahu pembaca apa pun, sedangkan void drawRect(int x, int y, int width, int height); memberi tahu segalanya.

Apa yang Didapat dari Prototipe

Bukan hanya kemampuan mengubah urutan definisi: prototipe adalah yang membuat compiler memeriksa pemanggilanmu.

Dengan prototipe di scope, 3 dan 2 dikonversi menjadi double sebelum pemanggilan. Tanpa prototipe, keduanya akan dikirim sebagai int dan scale akan membacanya sebagai double: hasilnya nilai sampah, tanpa diagnosis apa pun di dialek C lama.

Prototipe juga menangkap jumlah argumen yang salah dan tipe yang tidak cocok saat kompilasi:

scale(3.0);              /* error: too few arguments */
scale(3.0, "two");       /* error: passing char * where double is expected */

Itulah error yang kamu inginkan: keras, saat kompilasi, dan menunjuk ke barisnya.

Error Implicit Declaration

Panggil fungsi yang belum pernah dikenal compiler dan kamu akan mendapat salah satu pesan paling umum di C:

warning: implicit declaration of function 'add' [-Wimplicit-function-declaration]

Di C89, compiler akan menebak: menganggap fungsi itu mengembalikan int dan menerima argumen apa pun yang kamu berikan. Tebakan itu lebih sering salah daripada benar, dan saat fungsi sebenarnya mengembalikan double atau pointer, hasilnya ngawur. C99 menghapus implicit declaration dari bahasa ini, jadi ini adalah error di C99 dan sesudahnya: GCC dan clang modern versi terbaru menolaknya secara default.

Dua penyebab, dua solusi:

Fungsimu sendiri yang belum dideklarasikan. Tambahkan prototipe di atas pemanggilan, atau pindahkan definisinya ke bagian lebih awal.

Fungsi library yang header-nya terlupa. Prototipe library ada di header-nya, jadi kamu butuh #include:

printf, scanf, fopen        ->  #include <stdio.h>
malloc, free, exit, atoi    ->  #include <stdlib.h>
strlen, strcpy, strcmp      ->  #include <string.h>
sqrt, pow, sin, fabs        ->  #include <math.h>
isdigit, toupper            ->  #include <ctype.h>
bool, true, false           ->  #include <stdbool.h>

Pesan yang terkait, conflicting types for 'add', berarti prototipe dan definisinya tidak sepakat: tipe sebuah parameter berbeda, atau tipe kembaliannya. Perbaiki yang salah; keduanya harus cocok persis.

Rekursi Timbal Balik Butuh Prototipe

Kadang mengubah urutan tidak bisa menyelesaikannya: dua fungsi yang saling memanggil tidak bisa sama-sama ditaruh di depan.

Satu prototipe memutus lingkaran itu. Ini mekanisme yang sama dengan yang diandalkan fungsi rekursif biasa: nama fungsi itu sendiri sudah ada di scope di dalam body-nya, jadi rekursi langsung tidak butuh prototipe, tetapi rekursi timbal balik butuh.

Prototipe di Header File

Begitu sebuah program terdiri dari beberapa file .c, prototipe dipindahkan ke header agar setiap file melihat deklarasi yang sama dari satu tempat.

mathutils.h, antarmukanya:

#ifndef MATHUTILS_H
#define MATHUTILS_H

int add(int a, int b);
int multiply(int a, int b);
double average(const int values[], int count);

#endif

mathutils.c, implementasinya:

#include "mathutils.h"

int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}

double average(const int values[], int count) {
    if (count == 0) return 0.0;
    int total = 0;
    for (int i = 0; i < count; i++) total += values[i];
    return (double) total / count;
}

main.c, salah satu pemakainya:

#include <stdio.h>
#include "mathutils.h"

int main(void) {
    printf("%d\n", add(2, 3));
    return 0;
}

Lalu:

gcc main.c mathutils.c -o program

Ada dua kebiasaan yang layak diterapkan di sini. Pembungkus #ifndef / #define / #endif adalah include guard: ia mencegah deklarasinya diproses dua kali kalau header itu ter-include lewat dua jalur. Lalu mathutils.c meng-include header-nya sendiri. Kelihatannya berlebihan padahal tidak, karena dengan begitu compiler memeriksa setiap definisi terhadap deklarasi yang akan dipakai file lain. Kalau keduanya suatu saat tidak lagi selaras, kamu langsung tahu, bukan baru saat linking. Halaman header file membahas sisanya.

Kurung sudut (<stdio.h>) mencari di path include sistem; tanda kutip ("mathutils.h") mencari dulu di direktori proyekmu. Gunakan tanda kutip untuk header buatanmu sendiri.

f(void) vs f()

Yang satu ini benar-benar mengejutkan, dan inilah alasan setiap contoh di dokumentasi ini menulis int main(void).

void ping(void);      /* takes NO arguments - calls with arguments are rejected */
void pong();          /* says NOTHING about the parameters */

void ping(void); adalah prototipe: ia menyatakan bahwa fungsi itu tidak menerima parameter, jadi ping(1, 2, 3) adalah error kompilasi.

void pong(); adalah deklarasi gaya lama yang diwarisi dari C sebelum distandarkan. Deklarasi ini menyatakan tipe kembaliannya dan sama sekali tidak menyatakan apa pun tentang parameternya, sehingga compiler tidak bisa memeriksa pemanggilannya: pong(1, 2, 3) dikompilasi diam-diam dan melakukan sesuatu yang tidak terdefinisi.

int main(void) {      /* right: main takes no arguments */
int main() {          /* legal, but argument checking is off */

Selalu tulis (void) untuk fungsi tanpa parameter. C23 mengubah () agar berarti sama dengan (void), yang akhirnya menghilangkan jebakan ini. Tetapi banyak kode dan banyak compiler belum sampai ke sana, dan (void) benar di setiap standar C.

Perbedaan yang sama berlaku untuk definisinya. void ping(void) { } adalah definisi bergaya prototipe; void ping() { } bukan, dan tidak mengaktifkan pemeriksaan untuk pemanggilan yang muncul sebelumnya.

Kesalahan Umum

  • Titik koma pada definisi. int add(int a, int b); { return a + b; } mendeklarasikan add lalu meninggalkan blok nyasar. Pesan error-nya membingungkan; penyebabnya hanya satu karakter.
  • Titik koma yang hilang pada prototipe. Compiler terus membaca ke apa pun yang ada sesudahnya dan melaporkan sesuatu yang membingungkan beberapa baris di bawahnya.
  • Prototipe dan definisi yang tidak sepakat. conflicting types for .... Buat keduanya identik, atau lebih baik lagi, include header di file implementasi agar pemeriksaannya otomatis.
  • Mendeklarasikan fungsi di dalam fungsi lain. Sah (int add(int, int); di dalam main), tetapi deklarasinya lalu hanya berlaku di dalam fungsi itu. Taruh prototipe di file scope.
  • Mendefinisikan fungsi di header. Include header itu dari dua file .c dan linker melaporkan definisi ganda. Header berisi deklarasi; definisi ditaruh di file .c.
  • Mengandalkan () untuk pemeriksaan. Bentuk itu tidak memeriksa apa pun. Tulis (void).

Pertanyaan yang sering diajukan

Apa itu prototipe fungsi di C?

Deklarasi signature sebuah fungsi, yaitu tipe kembalian, nama, dan tipe parameternya, yang diakhiri titik koma alih-alih body: int add(int a, int b);. Prototipe memberi tahu compiler cara fungsi itu dipanggil agar pemanggilannya bisa diperiksa, tanpa menyatakan apa yang dilakukannya.

Apa perbedaan deklarasi dan definisi di C?

Deklarasi memperkenalkan nama dan tipe (int add(int, int);) dan boleh muncul berkali-kali. Definisi memberikan body (int add(int a, int b) { return a + b; }) dan harus muncul tepat sekali di seluruh program. Setiap definisi juga merupakan deklarasi.

Bagaimana cara memperbaiki "implicit declaration of function" di C?

Tambahkan prototipe sebelum pemanggilan. Untuk fungsimu sendiri, taruh returnType name(paramTypes); di dekat bagian atas file atau di header; untuk fungsi library, include header yang tepat: #include <stdio.h> untuk printf, <stdlib.h> untuk malloc, <string.h> untuk strlen, <math.h> untuk sqrt.

Apa perbedaan f(void) dan f() di C?

void f(void); mendeklarasikan fungsi yang tidak menerima argumen, dan compiler menolak setiap pemanggilan yang memberikan argumen. void f(); adalah deklarasi gaya lama yang tidak menyatakan apa pun tentang parameternya, sehingga pemeriksaan argumen dimatikan. Selalu tulis (void); C23 akhirnya membuat keduanya berarti sama, tetapi kode dan compiler yang lebih lama masih membedakannya.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai