Menu

C'de Başlık Dosyaları: .h ile .c, Include Korumaları ve Çok Dosyalı Programlar

Bir C programını dosyalara bölmek: neyin .h'ye, neyin .c'ye ait olduğu, çifte dahil etmeyi engelleyen include korumaları, birkaç dosyayı birlikte derlemek ve extern ile küresel değişken paylaşmak.

Bu sayfada çalıştırılabilir editörler var - düzenle, çalıştır ve sonucu anında gör.

Tek bir .c dosyası 2.000 satır olana dek sorunsuzdur. Bir programı dosyalara bölmek her parçanın ayrı derlenmesini, başka programlarda yeniden kullanılmasını ve tek başına okunmasını sağlar - ama C'nin bir içe aktarma sistemi yoktur. Sahip olduğu şey metin yapıştıran ön işlemci artı parçaları sonunda birleştiren bir bağlayıcıdır.

Bir başlık dosyası (.h), o parçalar arasındaki paylaşılan sözleşmedir: gerçekleştirimi içermeden her kaynak dosyaya başka yerde neyin var olduğunu söyler.

Bildirimler ile Tanımlar

Tüm tasarım tek bir ayrıma dayanır.

Bildirim, bu bir yerde var ve biçimi budur der. Hiç kod üretmez ve istediğiniz sayıda görünebilir:

int add(int a, int b);        /* fonksiyon bildirimi (prototip) */
extern int error_count;       /* değişken bildirimi */
struct Point { int x, y; };   /* tür tanımı - dosya başına tekrarlanması güvenli */

Tanım şeyi yaratır. Tüm programda tam olarak bir kez görünmelidir:

int add(int a, int b) { return a + b; }   /* fonksiyon tanımı */
int error_count = 0;                      /* değişken tanımı */

Başlıklar bildirim tutar. Kaynak dosyalar tanım tutar. Bunu tersine çevirin, bağlayıcı "multiple definition of ..." diye şikâyet eder - güvenilir biçimde bir tanımın bir başlığa sızdığı anlamına gelen tek hata mesajı.

İki Dosyalı Bir Program

İşte en küçük yararlı bölme. İki fonksiyon bildiren bir başlık:

/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int max_of(int a, int b);

#endif

Onları gerçekleştiren kaynak dosya - kendi başlığını dahil ettiğine dikkat edin:

/* math_utils.c */
#include "math_utils.h"

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

int max_of(int a, int b) {
    return (a > b) ? a : b;
}

Ve onları kullanan program:

/* main.c */
#include <stdio.h>
#include "math_utils.h"

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

Her iki kaynak dosyayı birlikte derleyin:

gcc main.c math_utils.c -o app
./app

Fark edilmeye değer iki ayrıntı. Birincisi, math_utils.c kendi başlığını dahil eder - bu gereksiz değildir. Derleyicinin her tanımın kendi bildirimiyle eşleştiğini denetlemesini sağlar, dolayısıyla başlığın prototipini değiştirip .c dosyasını unutursanız, bağlama zamanında bir uyumsuzluk yerine hemen bir hata alırsınız.

İkincisi, math_utils.h, gcc komut satırında değildir. Başlıklar asla derlenmez; #include ile .c dosyalarına yapıştırılırlar. Derleyiciye bir .h geçirmek başıboş bir ön derlenmiş başlık dosyası üretir ve bağlanabilir kod üretmez.

Aynı program, burada çalıştırabilmeniz için tek dosyaya sıkıştırılmış hâli:

main'den önceki bildirimler, gerçek sürümde başlığın sağladıklarıdır - fonksiyon prototipleri ile başlıkların iki ölçekte aynı fikir olmasının sebebi tam olarak budur.

Include Korumaları

#include metin yapıştırır ve iki kez yapıştırılan metin, yinelenmiş metindir. Bu bir fonksiyon prototipi için zararsız, bir struct için ölümcüldür:

/* KORUMASIZ shapes.h */
struct Point { int x, y; };

main.c hem shapes.h hem canvas.h'yi dahil ederse ve canvas.h de shapes.h'yi dahil ederse, derleyici tek bir çeviri biriminde struct Point'in iki kez tanımlandığını görür ve "redefinition of 'struct Point'" diyerek durur. Gerçek bir projede bu zincirler elle takip edemeyeceğiniz kadar derinleşir.

Çözüm bir include korumasıdır: "bu başlık zaten yapıştırıldı" bilgisini kaydeden bir makro.

/* shapes.h */
#ifndef SHAPES_H
#define SHAPES_H

struct Point { int x, y; };
struct Point origin_point(void);

#endif /* SHAPES_H */

İlk dahil etme SHAPES_H'yi tanımsız bulur, dolayısıyla gövde korunur - ve geçerken SHAPES_H'yi tanımlar. Aynı dosyadaki sonraki her dahil etme onu tanımlı bulur ve doğrudan #endif'e atlar. Makro adı proje genelinde benzersiz olmalıdır; yoldan türetilen DOSYAADI_H alışıldık gelenektir.

Tek satırlık alternatif her yaygın derleyici tarafından desteklenir:

/* shapes.h */
#pragma once

struct Point { int x, y; };

#pragma once isim çakışması yaşayamaz ve #endif'teki bir yazım hatasıyla bozulamaz. Tek dezavantajı C standardında olmamasıdır, dolayısıyla sıra dışı derleyicilerde derlenmesi gereken bir proje #ifndef biçimini tercih etmelidir. Her iki durumda da her başlık bir tane alır - hiçbir istisna olmadan, başka hiçbir şeyin dahil etmeyeceğini düşündüğünüz başlıklar dahil.

Bir Başlığa Ne Ait

.h'ye koyun:

  • Fonksiyon prototipleri
  • struct, union ve enum tanımları
  • typedef bildirimleri
  • Paylaşılması amaçlanan makrolar
  • Paylaşılan küresel değişkenlerin extern bildirimleri
  • O başlığın kendi kendine yeterli olması için ihtiyaç duyduğu #include'lar

.h'den uzak tutun:

  • Fonksiyon gövdeleri (bilerek static inline olmadıkça)
  • Değişken tanımları - bir başlıktaki int counter;, onu dahil eden her dosyada ayrı bir değişken tanımlar ya da derleyiciye göre bir bağlama hatası verir
  • Başlığın kendisinin ihtiyaç duymadığı başlıkların #include'u - bu bağımlılığı aşağı akıştaki herkese yükler

"Kendi kendine yeterli" bir kural yapmaya değer: bir başlık, her şeyden önce dahil edildiğinde derlenebilmelidir. shapes.h, size_t kullanıyorsa dahil eden dosyanın yapmış olmasını ummak yerine <stddef.h>'yi kendisi dahil eder.

Eksiksiz, düzgün biçimli bir başlık:

/* inventory.h */
#ifndef INVENTORY_H
#define INVENTORY_H

#include <stddef.h>   /* aşağıda kullanılan size_t için */

#define MAX_NAME 64

typedef struct {
    char   name[MAX_NAME];
    int    quantity;
    double price;
} Item;

/* program genelinde paylaşılır, inventory.c'de bir kez tanımlanır */
extern int item_count;

void   inventory_add(const Item *item);
double inventory_total(void);
size_t inventory_size(void);

#endif /* INVENTORY_H */

extern ile Küresel Paylaşmak

Bir küresel değişken tam olarak bir .c dosyasında tanımlanmalı ve diğer her yerde bildirilmelidir. Bildirimi yapan şey extern'dür:

/* inventory.h  - bildirim, depolama yok */
extern int item_count;
/* inventory.c  - tek tanım */
#include "inventory.h"
int item_count = 0;
/* main.c - başlık üzerinden kullanır */
#include <stdio.h>
#include "inventory.h"

int main(void) {
    printf("%d oge\n", item_count);
    return 0;
}

Başlıktan extern'ü çıkarın, dahil eden her dosya kendi item_count'unu tanımlar ki bu en iyi ihtimalle bir "multiple definition" bağlama hatası, en kötü ihtimalle iki bağımsız sayaçtır.

Tersi ihtiyaç da aynı derecede yaygındır: tek bir .c dosyasına özel kalması gereken bir değişken ya da yardımcı fonksiyon. Dosya kapsamındaki static bunu yapar - isme iç bağlantı verir; bağlayıcıya ve dolayısıyla diğer her dosyaya görünmez:

/* inventory.c */
static Item storage[256];                  /* bu dosyaya özel */
static int  find_slot(const char *name);   /* özel yardımcı */

İki dosya, çakışma olmadan birer static int counter;'a sahip olabilir. Bu, C'nin özel üye sürümüdür ve uzanmanız gereken varsayılandır - yalnızca diğer dosyaların gerçekten ihtiyaç duyduğu şey başlığa gider.

Daha Büyük Programları Derlemek

Her dosyayı listelemek çalışır ve yavaştır, çünkü her dosya her seferinde yeniden derlenir:

gcc main.c inventory.c report.c -o app

Ölçeklenebilir biçim, her kaynağı bir nesne dosyasına derleyip bağlamaktır:

gcc -c main.c        # main.o üretir
gcc -c inventory.c   # inventory.o üretir
gcc -c report.c      # report.o üretir
gcc main.o inventory.o report.o -o app

Artık report.c'yi değiştirmek yalnızca gcc -c report.c ve yeniden bağlama ister. make'in otomatikleştirdiği tam olarak bu kayıt tutmadır:

app: main.o inventory.o report.o
	gcc main.o inventory.o report.o -o app

%.o: %.c
	gcc -Wall -Wextra -c $< -o $@

Başlıklarınız bir alt dizinde yaşıyorsa -Iinclude onu açılı parantez arama yoluna ekler.

Hatalar ve Anlamları

Hangi aşamanın ürettiğini bildiğinizde iki başarısızlık biçimini ayırt etmek kolaydır.

"undefined reference to 'add'" - bir bağlayıcı hatası. Bildirim bulundu, tanım bulunamadı. Ya .c dosyasını komut satırında listelemeyi unuttunuz, ya fonksiyon static, ya da isim iki yerden birinde yanlış yazıldı.

"multiple definition of 'item_count'" - yine bir bağlayıcı hatası, ayna görüntüsü: bir tanım bir başlığa ya da iki kaynak dosyaya düştü. Onu tek bir .c'ye taşıyın ve başlıkta bir extern bildirimi bırakın.

"redefinition of 'struct Item'" - bir derleyici hatası; bir başlığın tek bir dosyaya iki kez yapıştırıldığı anlamına gelir. Include korumasını ekleyin.

"implicit declaration of function 'add'" - bir derleyici uyarısı (C99 ve sonraki kiplerde bir hata); prototipin hiç görülmediği anlamına gelir. #include'u unuttunuz ya da başlık onu bildirmiyor.

Bir program dosyalara yayıldığında genellikle isteyeceğiniz sonraki şey, platform ya da derleme türü başına neyin derleneceğini değiştirmektir - bu da koşullu derlemedir.

Sıkça Sorulan Sorular

.h dosyasına ne, .c dosyasına ne konur?

Başlık bildirimleri tutar - fonksiyon prototipleri, struct ve typedef tanımları, enum'lar, makrolar ve paylaşılan küresellerin extern bildirimleri. .c dosyası tanımları tutar - fonksiyon gövdeleri ve asıl değişkenler. Pratik kural: başlık neyin var olduğunu, kaynak dosya ne yaptığını söyler.

Include koruması nedir ve neden gerekir?

#include metin yapıştırır, dolayısıyla bir başlığı iki kez dahil etmek içeriğini iki kez yapıştırır - bu da içindeki her yapıyı ve typedef'i yeniden tanımlar ve derlenmez. Bir include koruması başlığı #ifndef MYHEADER_H / #define MYHEADER_H / #endif ile sarar, böylece ikinci dahil etme makroyu zaten tanımlı bulur ve gövdeyi atlar.

Birden fazla dosyalı bir C programını nasıl derlerim?

Her .c dosyasını komut satırında listeleyin: gcc main.c math_utils.c -o app. Oraya asla bir .h dosyası koymayın - başlıklar #include ile yapıştırılır, tek başlarına derlenmez. Daha büyük projeler için nesne dosyalarına derleyip (gcc -c main.c) bağlayın; bir Makefile bunu otomatikleştirir.

#pragma once mı yoksa #ifndef include koruması mı kullanmalıyım?

İkisi de çalışır. #pragma once tek satırdır, isim çakışması yaşayamaz ve her yaygın derleyici onu destekler - ama C standardında değildir. #ifndef/#define/#endif biçimi standarttır ve her yerde çalışır. Birini seçip bir projede tutarlı kullanın; en üst düzey taşınabilirlik için #ifndef biçimini seçin.

Coddy programming languages illustration

Coddy ile kodlamayı öğren

BAŞLA