C Programlama Temelleri

C Programlama Temelleri

Algoritma kurmadan bellek yönetimine, işaretçilerden dosya işlemlerine ve önişlemciye kadar C programlamanın temelini oluşturan ayrıntılı ders notları.

C ile ilgili bu notları programlamaya giriş ve sistem programlama temellerini çalışırken oluşturdum. İlk sürüm 2013-2014 döneminin ders akışını izliyordu. Daha sonraki düzenlemelerde dil standardındaki değişiklikleri yalnız gerekli yerlerde güncelleme notu olarak ekledim; C'nin temel semantiğini güncel kolaylıklarla geriye doğru yeniden yazmadım.

Ünite 1: Programlamaya Giriş

Problem çözme

Programlama, bir problemi bilgisayarın yürütebileceği kesin adımlara dönüştürme sürecidir. Kod yazmak bu sürecin son aşamalarından biridir. Önce problem anlaşılır, girdiler ve çıktılar belirlenir, çözüm yöntemi oluşturulur ve ancak bundan sonra programlama diline geçilir.

Temel süreç:

Problemi tanımla
    ↓
Girdi ve çıktıları belirle
    ↓
Algoritmayı oluştur
    ↓
Veri yapılarını seç
    ↓
Programı yaz
    ↓
Derle ve çalıştır
    ↓
Test et
    ↓
Sonucu değerlendir

Bir programın doğru çalışması yalnız sözdiziminin doğru olmasına bağlı değildir. Program:

  • doğru problemi çözmeli,
  • bütün gerekli durumları ele almalı,
  • hatalı girdilere karşı belirlenen davranışı göstermeli,
  • kaynakları uygun kullanmalı,
  • bakım yapılabilecek kadar açık olmalıdır.

Algoritma

Algoritma, belirli girdilerden istenen çıktıyı elde etmek için tanımlanmış, açık ve sonlu işlem dizisidir.

Bir algoritmanın temel özellikleri:

  • adımları belirsiz olmamalıdır,
  • uygulanabilir işlemlerden oluşmalıdır,
  • sonlu bir noktada tamamlanmalıdır,
  • aynı koşullarda tanımlı bir davranış göstermelidir,
  • problemin gerekli bütün durumlarını kapsamalıdır.

Örneğin üç sayıdan en büyüğünü bulmak için:

enBuyuk <- a

eğer b > enBuyuk ise
    enBuyuk <- b

eğer c > enBuyuk ise
    enBuyuk <- c

enBuyuk değerini döndür

Bu algoritma C veya C++'tan bağımsızdır. Programlama dili yalnız algoritmanın gerçeklenme aracıdır.

Sözde kod

Sözde kod, algoritmayı belirli bir dilin ayrıntılarına bağlamadan ifade eder.

Örnek:

ortalama(liste):
    toplam <- 0

    listedeki her eleman için:
        toplam <- toplam + eleman

    döndür toplam / eleman_sayısı

Sözde kod:

  • kontrol akışını,
  • veri ilişkilerini,
  • işlem sırasını

göstermelidir. Derlenebilir olmak zorunda değildir.

Atama

Atama:

x <- 5

veya programlama dilinde:

x = 5;

biçiminde ifade edilir.

Buradaki = matematikteki eşitlik işareti değildir. Sağ taraftaki ifade değerlendirilir ve sonuç sol taraftaki nesneye atanır.

Bu nedenle:

x = x + 1;

programlama açısından anlamlıdır. Önce eski x okunur, bir artırılır ve yeni değer tekrar x içine yazılır.

Koşul

Bir işlemin yalnız belirli koşulda yapılması seçim yapısıdır.

eğer sıcaklık < 0 ise
    "donma riski" yaz
değilse
    "donma riski yok" yaz

C ve C++ karşılığı:

if (temperature < 0) {
    /* işlem */
} else {
    /* diğer işlem */
}

Döngü

Belirli işlemlerin tekrarlanması döngüyle ifade edilir.

Koşul başta denetleniyorsa:

while koşul:
    işlem

Belirli sayıda tekrar için:

for başlangıç; koşul; güncelleme:
    işlem

Koşulun döngü sonunda denetlendiği yapı:

do:
    işlem
while koşul

C ve C++ bu üç temel modeli de destekler.

Fonksiyon

Fonksiyon belirli bir görevi kapsülleyen, adlandırılmış program parçasıdır.

Örnek:

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

Fonksiyon kullanımı:

  • tekrar eden kodu azaltır,
  • programı parçalara ayırır,
  • test edilebilirliği artırır,
  • arabirim ile gerçekleştirim ayrımını destekler.

Akış şemaları

Akış şeması algoritmayı grafiksel olarak gösterir.

Temel semboller:

  • başlangıç ve bitiş,
  • işlem,
  • karar,
  • giriş ve çıkış,
  • akış oku.

Küçük algoritmalarda öğretici olabilir. Büyük yazılımlarda sözde kod, durum diyagramları, UML ve doğrudan kaynak kod daha kullanışlı hale gelir.

Nassi-Shneiderman diyagramları yapısal programlamanın:

  • sıra,
  • seçim,
  • tekrar

yapılarını kutular içinde gösterir. Tarihsel ve öğretici değeri vardır, ancak modern yazılım geliştirmede zorunlu bir araç değildir.

Programlama dilleri

Programlama dilleri farklı soyutlama düzeyleri sunar.

C:

  • düşük düzey donanım ayrıntılarına erişebilir,
  • doğrudan bellek adresleriyle çalışabilir,
  • küçük çalışma zamanı maliyetine sahiptir,
  • sistem programlamada güçlüdür.

C++:

  • C benzeri düşük düzey olanakları,
  • sınıflar,
  • şablonlar,
  • RAII,
  • jenerik programlama,
  • standart kütüphane,
  • değer semantiği,
  • modern tür sistemi

ile genişletir.

C++ tarihsel olarak C'den gelişmiş olsa da günümüzde "C'nin üzerine birkaç nesne yönelimli özellik eklenmiş dil" olarak düşünülmemelidir. Kendi dil kuralları, tür sistemi ve standart kütüphanesi olan bağımsız bir programlama dilidir.

Ünite 2: C Programlarının Yapısı

En küçük C programı

Standart bir C programı:

int main(void)
{
    return 0;
}

ile başlayabilir.

main, barındırılan standart C ortamında programın giriş noktasıdır.

Yaygın iki biçim:

int main(void)

ve:

int main(int argc, char *argv[])

şeklindedir.

void main() standart C programının taşınabilir giriş biçimi değildir.

Derleme süreci

Basitleştirilmiş süreç:

Kaynak kod
   ↓
Önişleme
   ↓
Derleme
   ↓
Assembly
   ↓
Nesne kodu
   ↓
Bağlama
   ↓
Yürütülebilir program

Gerçek araç zincirinde bazı aşamalar birleştirilebilir.

Örneğin:

cc program.c -o program

komutu önişleme, derleme, assembly ve bağlama adımlarını tek komutla yürütebilir.

Başlık dosyaları

Standart kütüphane bildirimleri başlık dosyalarıyla görünür hale getirilir.

Örnek:

#include <stdio.h>

<stdio.h>:

  • printf,
  • fopen,
  • fclose,
  • fread,
  • fwrite

gibi standart giriş çıkış işlevlerinin bildirimlerini içerir.

Standart başlıklar için:

#include <stdio.h>

biçimi tercih edilir.

Projeye ait başlıklar için:

#include "my_header.h"

kullanımı yaygındır.

Açıklamalar

C'de çok satırlı açıklama:

/* açıklama */

biçimindedir.

C99'dan itibaren tek satırlı açıklama da standarttır:

// açıklama

Açıklama kodun ne yaptığını tekrar etmek yerine, gerektiğinde neden o şekilde yazıldığını açıklamalıdır.

Temel veri türleri

C'nin temel türlerinden bazıları:

char
short
int
long
long long
float
double
long double
_Bool / bool

C23 ile bool, true ve false doğrudan dil düzeyindeki adlar arasında yer alır.

Tamsayı türlerinin kesin bit genişliği her platformda aynı olmak zorunda değildir.

Örneğin:

sizeof(int)

değeri çoğu güncel sistemde 4 bayttır, ancak C standardı int türünün daima 32 bit olmasını şart koşmaz.

Kesin genişliğin önemli olduğu durumlarda:

#include <stdint.h>

int32_t
uint32_t
int64_t
uint64_t

gibi türler, gerçekleştirim tarafından sağlanıyorsa kullanılabilir.

sizeof

Bir türün veya nesnenin bayt cinsinden boyutu:

sizeof(int)
sizeof variable

ile alınır.

sizeof(char) tanım gereği 1'dir. Ancak bir C baytının bit sayısı:

CHAR_BIT

ile belirlenir ve standart yalnız bunun en az 8 olmasını garanti eder.

Değişken bildirimi

int count;
double average;
char grade;

Başlangıç değeri:

int count = 0;
double pi = 3.141592653589793;

Yerel otomatik değişken başlangıç değeri verilmeden okunmamalıdır.

Sabitler

Literal:

42
3.14
'A'
"metin"

gibi kaynak kodda doğrudan yazılan değerlerdir.

Salt okunur nesne:

const int max_count = 100;

ile tanımlanabilir.

C'de const ile tanımlanan nesne değiştirilemez olarak kullanılır, ancak her bağlamda derleme zamanı sabiti anlamına gelmez.

Makro sabit:

#define MAX_COUNT 100

şeklinde de tanımlanabilir.

Tür güvenliği ve hata ayıklama açısından uygun olduğunda const, enum veya C23'teki sabit ifade olanakları makro yerine tercih edilebilir.

Yerel değişken

Blok içinde tanımlanan değişkenin kapsamı ilgili blokla sınırlıdır:

void f(void)
{
    int x = 10;
}

x bu blok dışından adıyla erişilemez.

Küresel değişken

Dosya kapsamında tanımlanan nesne:

int counter;

dosya kapsamına sahiptir.

Küresel durum program parçaları arasında gizli bağımlılık oluşturabileceğinden gerektiğinde kullanılmalıdır.

static

Blok içindeki:

static int count;

nesnesinin saklama süresi programın tamamıdır, fakat adı ilgili blokla sınırlıdır.

Dosya kapsamındaki:

static int value;

iç bağlantıya sahiptir ve başka çeviri birimlerinden aynı adla erişilemez.

static sözcüğünün etkisi bulunduğu bağlama göre değerlendirilmelidir.

extern

Başka bir yerde tanımlanmış dış bağlantılı nesneyi bildirmek için:

extern int global_counter;

kullanılabilir.

Büyük programlarda ortak değişkenleri başlık dosyalarında extern ile bildirmek, tek bir .c dosyasında tanımlamak tipik yaklaşımdır.

register

Tarihsel C'de:

register int i;

derleyiciye değişkenin hızlı erişim için yazmaçta tutulabileceğine ilişkin bir ipucu verirdi.

Modern derleyiciler yazmaç atamasını optimizasyon sırasında kendileri yaptığı için bu kullanım pratik önemini büyük ölçüde kaybetmiştir. Güncel C'de kaynak uyumluluğu açısından görülebilir ancak performans yönetimi için güvenilir bir araç değildir.

Ünite 3: C'de İşleçler ve Program Denetimi

Aritmetik işleçler

+   toplama
-   çıkarma
*   çarpma
/   bölme
%   kalan

Tamsayı bölmesi kesirli kısmı atar:

7 / 2

sonucu:

3

olur.

Kayan noktalı sonuç için operandlardan en az biri kayan nokta olmalıdır:

7.0 / 2.0

sonucu:

3.5

olur.

Artırma ve azaltma

++i;
--i;
i++;
i--;

Ön ve son biçimlerin ifade içindeki değer üretme zamanı farklıdır.

Basit sayaç güncellemelerinde:

++i;

ve:

i++;

aynı son değeri oluşturur.

Bir ifadede aynı nesneyi birden fazla kez, sıralaması güvence altına alınmadan değiştiren karmaşık kullanımlardan kaçınılmalıdır.

Bileşik atama

x += 5;
x -= 2;
x *= 3;
x /= 4;

genellikle:

x = x + 5;

gibi ifadelerin kısa biçimidir.

Karşılaştırma işleçleri

==  eşit
!=  eşit değil
<   küçük
>   büyük
<=  küçük veya eşit
>=  büyük veya eşit

Atama ile karşılaştırma farklıdır:

x = 5;

atamadır.

x == 5

karşılaştırmadır.

Mantıksal işleçler

&&  VE
||  VEYA
!   DEĞİL

C'de && ve || kısa devre değerlendirmesi yapar.

Örneğin:

if (p != NULL && *p > 0) {
    ...
}

ifadesinde p == NULL ise *p değerlendirilmez.

Bit işleçleri

&   bit VE
|   bit VEYA
^   bit XOR
~   bit tümleme
<<  sola kaydırma
>>  sağa kaydırma

Mantıksal && ile bit düzeyindeki & aynı değildir.

Bit işleçleri:

  • bayraklar,
  • protokoller,
  • register erişimi,
  • bit maskeleri,
  • sıkıştırılmış veri

üzerinde önemlidir.

if

if (score >= 50) {
    passed = 1;
}

else ile:

if (score >= 50) {
    passed = 1;
} else {
    passed = 0;
}

İç içe if

if (x > 0) {
    if (x % 2 == 0) {
        ...
    }
}

Derin iç içe koşullar okunabilirliği azaltabilir. Gerektiğinde koşullar sadeleştirilmeli veya işlem fonksiyonlara ayrılmalıdır.

Koşul işleci

condition ? value_if_true : value_if_false

Örnek:

int max = a > b ? a : b;

Küçük değer seçimlerinde kullanışlıdır. Büyük iş bloklarını ?: içine taşımak okunabilirliği azaltır.

switch

switch (command) {
case 1:
    start();
    break;
case 2:
    stop();
    break;
default:
    handle_unknown();
    break;
}

break kullanılmazsa yürütme sonraki case bölümüne düşebilir. Bilinçli fall-through açık biçimde belirtilmelidir.

for

for (int i = 0; i < 10; ++i) {
    printf("%d\n", i);
}

Üç bölüm:

başlangıç
koşul
güncelleme

işlevini görür.

while

while (condition) {
    ...
}

Koşul ilk yinelemeden önce sınanır.

do-while

do {
    ...
} while (condition);

Gövde en az bir kez yürütülür.

break

En yakın döngü veya switch yapısından çıkar:

if (found) {
    break;
}

continue

Mevcut yinelemenin kalanını atlar ve döngünün sonraki adımına geçer.

goto

C dilinde goto vardır:

goto cleanup;

Genel kontrol akışında gereksiz kullanımı okunabilirliği bozar. Bununla birlikte C'de birden fazla kaynak edinilmişse tek bir hata temizleme yoluna gitmek için kontrollü kullanım bazen pratiktir:

FILE *f = fopen(path, "rb");
if (f == NULL) {
    return -1;
}

void *buffer = malloc(size);
if (buffer == NULL) {
    goto close_file;
}

/* işlem */

free(buffer);
fclose(f);
return 0;

close_file:
fclose(f);
return -1;

Bu tür kullanım rastgele dallanma değil, yapılandırılmış hata temizleme tekniğidir.

Ünite 4: Diziler ve Karakter Dizileri

Tek boyutlu diziler

int values[5];

beş int elemanı için ardışık saklama alanı oluşturur.

İndisler:

0, 1, 2, 3, 4

aralığındadır.

C sınır denetimi yapmaz. Dizinin dışına erişmek tanımsız davranıştır.

Başlangıç değeri

int values[5] = {10, 20, 30, 40, 50};

Boyut çıkarılabilir:

int values[] = {10, 20, 30};

Kısmi başlangıç:

int values[5] = {1, 2};

kalan elemanları sıfırlar.

Eleman sayısı

Aynı kapsamda gerçek dizi için:

size_t count = sizeof values / sizeof values[0];

kullanılabilir.

Ancak dizi fonksiyon parametresi olarak geldiğinde çoğunlukla işaretçiye dönüşür. Bu yöntem fonksiyon parametresi üzerinde dizi uzunluğunu vermez.

Çok boyutlu diziler

int matrix[3][4];

üç satır ve dört sütundan oluşan gerçek bir dizi yapısıdır.

C'de satır-major yerleşim kullanılır. Son indis bellekte ardışık değişir.

Eleman:

matrix[i][j]

ile alınır.

Matris toplama

Aynı boyutlu matrislerde:

for (size_t i = 0; i < rows; ++i) {
    for (size_t j = 0; j < cols; ++j) {
        c[i][j] = a[i][j] + b[i][j];
    }
}

Matris çarpımı

A matrisi m x n, B matrisi n x p ise sonuç m x p olur:

C[i][j] = sum A[i][k] B[k][j]

Klasik algoritma:

for (size_t i = 0; i < m; ++i) {
    for (size_t j = 0; j < p; ++j) {
        c[i][j] = 0;

        for (size_t k = 0; k < n; ++k) {
            c[i][j] += a[i][k] * b[k][j];
        }
    }
}

C karakter dizileri

C'de ayrı bir yerleşik string türü yoktur. Klasik metin temsili '\0' ile sonlanan char dizisidir.

char name[] = "Ali";

bellekte:

'A' 'l' 'i' '\0'

olarak tutulur.

Burada NULL ile '\0' aynı kavram değildir.

  • '\0': değeri sıfır olan karakterdir.
  • NULL: null pointer constant için kullanılan makrodur.

Güvenli metin okuma

Tarihsel C kaynaklarında sık görülen:

gets(buffer);

işlevi güvenli değildir ve C11 ile standarttan çıkarılmıştır. Alınacak verinin uzunluğunu sınırlayamadığı için kullanılmamalıdır.

Yerine:

fgets(buffer, sizeof buffer, stdin);

kullanılabilir.

Karakter dizisi uzunluğu

#include <string.h>

size_t n = strlen(text);

strlen, sonlandırıcı '\0' karakterini saymaz.

Çalışma süresi metin uzunluğu kadar ilerleme gerektirdiği için:

O(n)

düzeyindedir.

Kopyalama

Standart:

strcpy(destination, source);

işlevi hedef tamponun yeterli büyüklükte olduğunu varsayar. Kapasite denetlenmeden kullanımı taşmaya yol açabilir.

Boyutu açık olarak yönetmek çoğu durumda daha güvenlidir.

Örneğin:

if (strlen(source) + 1 <= destination_size) {
    memcpy(destination, source, strlen(source) + 1);
}

Güvenli C kodunda tampon kapasitesi veriyle birlikte düşünülmelidir.

Birleştirme

strcat(destination, source);

hedefte yeterli boş alan olmasını gerektirir.

Metin birleştirme kodunda kapasite hesabı yapılmadan strcat kullanmak güvenli değildir.

Karşılaştırma

strcmp(a, b)

sonucu:

  • negatif: a < b,
  • sıfır: eşit,
  • pozitif: a > b

anlamındadır.

Metinler:

a == b

ile içerik bakımından karşılaştırılmaz. Bu ifade işaretçi veya dizi bağlamına göre adres davranışıyla ilgilidir.

Ünite 5: İşaretçiler ve Bellek

İşaretçi kavramı

İşaretçi, başka bir nesnenin veya fonksiyonun adresini temsil edebilen türdür.

int value = 10;
int *p = &value;

Burada:

  • value: int,
  • &value: int *,
  • p: int *,
  • *p: int

türündedir.

Adres alma

&value

nesnenin adresini verir.

Dolaylı erişim

*p

işaret edilen nesneye erişir.

*p = 20;

sonrasında:

value == 20

olur.

Null işaretçi

Bir işaretçinin geçerli bir nesneyi göstermediğini belirtmek için:

int *p = NULL;

kullanılabilir.

Null işaretçiyi dereference etmek:

*p

tanımsız davranıştır.

İşaretçi aritmetiği

Dizi elemanını gösteren işaretçide:

p + 1

adres değerini bir bayt değil, işaret edilen türün bir elemanı kadar ilerletir.

Örnek:

int a[4] = {10, 20, 30, 40};
int *p = a;

printf("%d\n", *(p + 2));

çıktı:

30

olur.

İşaretçi aritmetiği yalnız aynı dizi nesnesi ve bir-past-the-end sınırları içinde tanımlı davranışa sahiptir.

Dizi ve işaretçi ilişkisi

Birçok ifadede:

array

ilk elemanın işaretçisine dönüşür:

&array[0]

Ancak dizi ve işaretçi aynı tür değildir.

Örneğin:

sizeof array

dizinin tamamının boyutunu verebilirken:

sizeof pointer

yalnız işaretçinin boyutunu verir.

İşaretçi ile dizi dolaşma

for (int *p = a; p != a + count; ++p) {
    printf("%d\n", *p);
}

indisli sürümle aynı temel belleği dolaşır:

for (size_t i = 0; i < count; ++i) {
    printf("%d\n", a[i]);
}

İşaretçi dizisi

const char *names[] = {
    "Ali",
    "Ayse",
    "Mehmet"
};

Burada names karakter dizilerinin kendisini değil, karakter dizilerine işaret eden işaretçileri tutar.

İşaretçiye işaret eden işaretçi

int value = 5;
int *p = &value;
int **pp = &p;

**pp ile value değerine ulaşılabilir.

Bu yapı:

  • dinamik iki boyutlu yapılar,
  • çıktı parametreleri,
  • argv,
  • işaretçi değişkeninin değiştirilmesi

gibi durumlarda görülebilir.

Dinamik bellek ayırma

C'de:

#include <stdlib.h>

int *p = malloc(count * sizeof *p);

ile dinamik alan ayrılabilir.

Başarı denetlenmelidir:

if (p == NULL) {
    /* hata */
}

Kullanım bitince:

free(p);
p = NULL;

yapılabilir.

calloc

int *p = calloc(count, sizeof *p);

ayrılan bütün baytları sıfır bit değerleriyle başlatır.

Bu durum her tür için semantik olarak "sıfır değerli nesne dizisi" anlamına otomatik olarak indirgenmemelidir. Tamsayı türlerinde pratik ve standart uyumlu kullanım yaygındır.

realloc

int *temp = realloc(p, new_count * sizeof *p);

başarısız olduğunda eski p hâlâ geçerli olabileceği için sonucu doğrudan p üzerine yazmak risklidir.

Güvenli desen:

int *temp = realloc(p, new_count * sizeof *p);

if (temp != NULL) {
    p = temp;
}

Bellek hataları

C'de önemli hata sınıfları:

  • null dereference,
  • buffer overflow,
  • out-of-bounds erişim,
  • use-after-free,
  • double free,
  • dangling pointer,
  • memory leak,
  • uninitialized read.

Bu hataların çoğu derleme sırasında yakalanmayabilir.

Derleyici uyarıları ve çalışma zamanı araçları önemlidir:

-Wall
-Wextra
-Wpedantic
-fsanitize=address
-fsanitize=undefined

gibi seçenekler geliştirme sırasında yararlı olabilir.

Ünite 6: C Fonksiyonları

Fonksiyon bildirimi

Prototip:

int sum(int a, int b);

Tanım:

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

Çağrı:

int result = sum(3, 4);

void fonksiyon

Değer döndürmeyen fonksiyon:

void print_message(void)
{
    puts("Merhaba");
}

void parametre listesi C'de fonksiyonun parametre almadığını açıkça belirtir.

Parametre aktarımı

C'de parametreler değer ile aktarılır.

void f(int x)
{
    x = 10;
}

çağıranın değişkenini değiştirmez.

Kaynaklarda bazen "referans ile çağırma" denilen C tekniği gerçekte işaretçi değerinin fonksiyona geçirilmesidir.

void set_value(int *p)
{
    *p = 10;
}

çağrı:

int x = 0;
set_value(&x);

Burada yine p parametresinin kendisi değer ile aktarılmıştır. Ancak bu değer x nesnesinin adresidir.

C'de C++ anlamındaki reference türü yoktur.

Çıktı parametresi

Bir fonksiyon birden çok sonuç üretecekse:

int divide(int a, int b, int *quotient, int *remainder)
{
    if (b == 0) {
        return 0;
    }

    *quotient = a / b;
    *remainder = a % b;
    return 1;
}

kullanılabilir.

Diziyi fonksiyona aktarma

double average(const int values[], size_t count);

parametre bağlamında:

const int values[]

işlevsel olarak işaretçi parametresidir.

Bu nedenle eleman sayısı ayrıca taşınmalıdır.

const parametre

Fonksiyon işaret edilen veriyi değiştirmeyecekse:

void print_array(const int *values, size_t count);

bildirimi niyeti açıklar ve yanlış değişikliği engeller.

Küresel değişkenlerle veri paylaşımı

Küresel değişken fonksiyonlar arasında veri paylaşabilir, ancak:

  • bağımlılıkları gizler,
  • testleri zorlaştırır,
  • eşzamanlılık sorunları oluşturabilir.

Fonksiyon parametreleri ve açık veri yapıları çoğu zaman daha iyi tasarım sağlar.

static yerel değişken

int next_id(void)
{
    static int id = 0;
    return ++id;
}

id yalnız fonksiyon kapsamında görünür fakat program boyunca varlığını sürdürür.

Bu tür gizli durum thread-safety ve yeniden giriş açısından ayrıca değerlendirilmelidir.

Özyineleme

Fonksiyon kendisini çağırabilir.

Faktöriyel:

unsigned long long factorial(unsigned n)
{
    if (n <= 1) {
        return 1;
    }

    return n * factorial(n - 1);
}

Her özyinelemeli algoritmada:

  • taban durum,
  • taban duruma ilerleme

bulunmalıdır.

Özyineleme çağrı yığını kullanır. Çok derin özyineleme stack overflow oluşturabilir.

Ünite 7: C Yapıları, Birlikler ve Bit Alanları

struct

struct, farklı türde alanları tek veri yapısında birleştirir.

struct Student {
    int id;
    char name[64];
    double grade;
};

Nesne:

struct Student s;

Alan erişimi:

s.id = 10;

typedef

typedef struct {
    int x;
    int y;
} Point;

sonrasında:

Point p;

kullanılabilir.

C23 ile etiket ve typedef ihtiyaçları bazı yeni dil olanaklarıyla sadeleşmiş olsa da geleneksel biçim çok yaygındır.

Başlangıç değeri

struct Point {
    int x;
    int y;
};

struct Point p = {10, 20};

Belirlenmiş başlatıcı:

struct Point p = {
    .x = 10,
    .y = 20
};

özellikle çok alanlı yapılarda okunabilirliği artırır.

Yapı işaretçisi

struct Student *p = &s;

alan:

p->id

ile alınır.

Bu:

(*p).id

ifadesinin kısa biçimidir.

Yapının fonksiyona aktarılması

Değer ile:

void print_point(struct Point p);

veya işaretçi ile:

void print_point(const struct Point *p);

aktarılabilir.

Büyük yapılarda gereksiz kopyayı önlemek ve salt okunur erişimi belirtmek için const işaretçi uygun olabilir.

Dizi içinde struct

struct Student students[100];

yapısında her eleman bir Student kaydıdır.

union

Birlikte bütün üyeler aynı depolama alanını paylaşır:

union Value {
    int i;
    double d;
};

Aynı anda alanın yalnız uygun aktif yorumunun kullanılması gerekir.

union:

  • protokol çözümleme,
  • donanım temsili,
  • tagged union

gibi yapılarda kullanılabilir, ancak tür yorumlama kuralları dikkatle ele alınmalıdır.

Etiketli birlik

Birliğin hangi üyesinin geçerli olduğunu ayrıca tutmak daha güvenlidir:

enum ValueType {
    VALUE_INT,
    VALUE_DOUBLE
};

struct Value {
    enum ValueType type;
    union {
        int i;
        double d;
    } data;
};

Bit alanları

struct Flags {
    unsigned ready : 1;
    unsigned mode  : 3;
};

belirli bit genişlikli alanlar tanımlar.

Ancak:

  • alan yerleşimi,
  • sıralama,
  • padding

gibi ayrıntılar gerçekleştirim tanımlı olabilir.

Ağ protokolü veya kalıcı dosya biçimi gibi tam bit düzeninin zorunlu olduğu yapılarda açık maskeler ve kaydırmalar daha taşınabilir olabilir.

Ünite 8: C Dosya İşlemleri ve Standart Kütüphane

Dosya açma

FILE *f = fopen("data.txt", "r");

Başarı denetlenmelidir:

if (f == NULL) {
    perror("fopen");
    return 1;
}

Açma kipleri

Yaygın kipler:

r   oku
w   yaz, varsa içeriği sıfırla
a   sona ekle
r+  oku/yaz
w+  oku/yaz, içeriği sıfırla
a+  oku ve sona ekle

İkili kip için:

rb
wb
ab

gibi biçimler kullanılır.

Özellikle Windows gibi metin ve ikili kip ayrımı bulunan sistemlerde doğru kip önemlidir.

Dosya kapatma

if (fclose(f) != 0) {
    /* hata */
}

Yazma hatalarının bir bölümü tampon boşaltılırken ortaya çıkabileceği için fclose sonucu önemli olabilir.

Karakter okuma ve yazma

int ch = fgetc(f);

fgetc dönüş türü inttir; çünkü bütün unsigned char değerlerinin yanında EOF değerini de temsil etmesi gerekir.

Yazma:

fputc('A', f);

Satır okuma

char line[256];

while (fgets(line, sizeof line, f) != NULL) {
    ...
}

tampon sınırı belirli olduğundan gets kullanımından daha güvenlidir.

Biçimlendirilmiş G/Ç

Yazma:

fprintf(f, "%d %.2f\n", id, value);

Okuma:

int n = fscanf(f, "%d %lf", &id, &value);

scanf ailesinde dönüş değeri okunabilen alan sayısını gösterir ve mutlaka denetlenmelidir.

İkili yazma

size_t written = fwrite(buffer, element_size, count, f);

İkili okuma:

size_t read_count = fread(buffer, element_size, count, f);

Sonuçlar beklenen eleman sayısıyla karşılaştırılmalıdır.

Struct'ı doğrudan dosyaya yazmak

fwrite(&record, sizeof record, 1, f);

aynı program içinde geçici veri için çalışabilir, ancak taşınabilir kalıcı dosya biçimi değildir.

Nedenleri:

  • padding,
  • endian,
  • tür genişliği,
  • ABI,
  • derleyici,
  • sürüm değişiklikleri.

Kalıcı veya ağ üzerinden taşınan veri için açık serileştirme biçimi kullanılmalıdır.

Dosya konumu

fseek(f, offset, SEEK_SET);
long pos = ftell(f);
rewind(f);

rastgele erişim için kullanılabilir.

Büyük dosyalarda platformun büyük dosya API'leri ve off_t benzeri türleri gerekebilir.

Dosya silme ve yeniden adlandırma

remove("file.txt");
rename("old.txt", "new.txt");

standart C kütüphanesinde bulunur.

Dosya sistemi izinleri ve yarış koşulları işletim sistemine bağlıdır.

Karakter sınıflandırma

#include <ctype.h>

isdigit(c)
isalpha(c)
isspace(c)
toupper(c)
tolower(c)

Bu işlevlere EOF veya unsigned char olarak temsil edilebilen değerler verilmelidir. Negatif char değerini doğrudan geçirmek tanımsız davranış oluşturabilir.

Güvenli kalıp:

toupper((unsigned char)c)

Matematik kütüphanesi

#include <math.h>

sqrt
pow
sin
cos
tan
log
log10
exp
fabs

işlevleri yaygındır.

Tarih ve zaman

#include <time.h>

başlığı:

  • time,
  • difftime,
  • mktime,
  • strftime

gibi olanaklar sağlar.

Yerel saat, zaman dilimi ve takvim işlemleri platform ayrıntıları içerdiğinden salt saniye değerinden daha karmaşıktır.

Ünite 9: C Önişlemcisi

Önişleme

# ile başlayan yönergeler derlemenin sözdizim çözümünden önce önişlemci tarafından işlenir.

Başlıca kullanım:

  • dosya dahil etme,
  • makro,
  • koşullu derleme,
  • derleme zamanı yapılandırması.

Nesne benzeri makro

#define BUFFER_SIZE 4096

Makro metinsel yer değiştirmedir. Türü yoktur.

Bir sabit nesne veya enum daha uygun olabiliyorsa makro kullanmak zorunlu değildir.

Fonksiyon benzeri makro

#define SQUARE(x) ((x) * (x))

Parantez kullanımı öncelik sorunlarını azaltır.

Buna rağmen:

SQUARE(i++)

tehlikelidir, çünkü i++ birden fazla değerlendirilir.

C'de uygun olduğunda inline fonksiyon daha güvenlidir:

static inline int square_int(int x)
{
    return x * x;
}

# ve ##

Stringification:

#define TO_STRING(x) #x

Token birleştirme:

#define JOIN(a, b) a##b

İleri makro metaprogramlamada kullanılır ancak gereksiz karmaşıklık oluşturabileceğinden ölçülü kullanılmalıdır.

#undef

#undef NAME

daha önce tanımlanmış makroyu kaldırır.

Koşullu derleme

#ifdef DEBUG
    ...
#endif

ve:

#if FEATURE_LEVEL >= 2
    ...
#elif FEATURE_LEVEL == 1
    ...
#else
    ...
#endif

kullanılabilir.

Header guard

#ifndef MY_HEADER_H
#define MY_HEADER_H

/* bildirimler */

#endif

aynı başlığın bir çeviri biriminde birden fazla kez etkili olmasını önler.

#pragma once birçok derleyicide desteklenir, ancak ISO C standardının zorunlu bir yönergesi değildir.

Bu sayfanın QR kodu