C Dersi

Bu derste C programlama dersi anlatılacaktır. Bu dersin düzgünce anlaşılabilmesi için temel düzey gnu/linux bilmeniz gerekmektedir.

Derleme işlemi

C derlemeli bir programlama dilidir. Yani yazılan kodun derleneek bilgisayarın anlayacağı hale getirilmesi gerekmektedir. Derleme işlemini gcc veya clang kullanarak yapabiliriz.

# koddan .o dosyası üretelim
$ gcc -c main.c
# .o dosyasından derlenmiş dosya üretelim.
$ gcc -o main main.o
# kodu çalıştıralım
$ ./main
-> Hello World

Yukarıdaki örnekte öncelikle .o uzantılı object dosyamızı ürettik. Bu dosya kodun derlenmiş fakat henüz kullanıma hazır hale getirilmemiş halidir. Bu sebeple .o dosyalarını linkleme işleminden geçirerek son halini almasını sağlamalıyız.

Not: derleyicimiz .o üretmeden de doğrudan derleme yapabilir.

$ gcc -o main main.c

Açıklama satırı

C kodlarında 3 farklı yolla girintileme yapılabilir.

  1. // kullanarak satırın geri kalanını açıklama satırı yapabiliriz.

// bu bir açıklama satırıdır.
  1. /* ile başlayıp */ ile biten alanlar açıklamadır.

/* Bu
   bir
   açıklama
   satırıdır */
  1. #if 0 ile başlayan satırdan #endif satırına kadar olan kısım açıklama satırıdır.

#if 0
bu bir
açıklama
satırıdır
#endif

Girintileme

C programlama dilinde blocklar {} karakterleri ile belirtilir. Kodun okunalı olması için girintilenmesi gereklidir fakat şart değildir. Girintileme için 4 boşluk veya 1 tab kullanabilirsiniz.

Bir block aşağdaki gibi bir yapıya sahiptir.

aaaa (bbbb) {
        cccc;
  ddd;
}

Not: Her satırın sonunda ; işareti bulunmalıdır.

İlk program

C programları çalıştırıldığında main fonsiyonu çalıştırılır. Aşağıda örnek main fonksiyonu bulunmaktadır.

int main(int argc, char** argv) {
    return 0;
}
  • int main kısmında int döndürülecek değer türü main adıdır.

  • int argc parametre sayısını belirtir.

  • char **argv parametre listesini belirtir.

  • return 0 komutu 0 ile çıkış yapar.

Burada main fonksiyonunu türünün bir önemi yoktur. void olarak da tanımlanabilir. Ayrıca kullanmayacaksak arguman tanımlamaya da gerek yoktur. Kısaca Şu şekilde de yazabilirdik.

void main(){}

Ekrana yazı yazma

Öncelikle stdio.h kütüphanesine ihtiyacimiz olduğu için onu eklemeliyiz. Ardından printf fonksiyonu ile ekrana yazı yazabiliriz.

printf fonksiyonunun 1. parametresi yazdirma şablonunu diğerleri ise yazdırılacak verileri belirtir.

#include <stdio.h>

int main(int argc, char** argv) {
    printf("%s\n", "Merhaba Dünya!");
    return 0;
}
  • include ile belirttiğimiz dosyalar sistemde /usr/include içerisinde bulunur.

  • printf fonksiyonundaki %s yazılar için, %c karakterler için, %d sayılar için kullanılır.

Değişkenler

C dilinde değişkenler aşağıdaki gibi tanımlanır.

...
int sayi = 12;
char* yazi = "test";
char karakter = 'c';
float sayi2 = 12.42;
...

Bununla birlikte #define kullanarak derlemeden önce koddaki alanların karşılığı ile değiştirilmesini sağlayabilirsiniz. Bu şekilde tanımlanan değerler derlemeden önce yerine yazıldığı için değişken olarak işlem görmezler.

#define sayi 12
...
printf("%d\n",sayi);
...

Diziler

Diziler iki şekilde tanımlanabilir.

1. Pointer kullanarak tanımlanabilir. Bu konunun detaylarına ilerleyen kısımda değinilecektir. Bu şekilde tanımlanan dizilerde başta uzunluk belirtilmek zorunda değildir.

int *dizi = {12, 22, 31};
  1. Uzunluk belirterek tanımlanabilir. Bu şekilde tanımlanan dizilerin uzunluğu sabittir.

int dizi[3] = {12, 22, 31};

C dilinde string kavramı bulunmaz. Onun yerine karakter dizileri kullanılır.

char *txt = "deneme123";

Dizinin bir elemanına erişmek için aşağıdaki gibi bir yol kullanılır.

int *dizi = {12, 22, 31};
int c = dizi[1]; // dizinin 2. elemanı

Not: Dizi indisleri 0dan başlar.

Bir dizinin uzunluğunu dizinin bellekteki boyutunu birim boyutuna bölerek buluruz. Bunun için sizeof fonksiyonu kullanılır.

int *dizi = {11, 22, 31};
int l = sizeof(dizi) / sizeof(int);

Klavyeden değer alma

Klavyeden değer almak için scanf kullanılır. İlk parameter şablonu diğerleri ise değişkenlerin bellek adresini belirtir.

int sayi;
scanf("%d\n", &sayi);

Not: Bu şekilde değer alma yaptığımızda formata uygun olmayan şekilde değer girilebilir. Eğer böyle bir durum oluşursa değişken NULL olarak atanır. yani değeri bulunmaz. Buda kodun işleyişinde soruna yol açabilir. Bu yüzden değişkeni kullanmadan ince NULL olup olmadığını kontrol etmelisiniz.

Koşullar

Koşullar için if bloğu kullanılır. Block içindeki ifade 0 veya NULL olursa koşul sağlanmaz. Bu durumda varse else bloğu çalıştırılır.

if (koşul1) {
    block 1
} else if (koşul2) {
  block 2
} else {
  block 3
}

Örnek olarak girilen sayının çift olup olmadığını yazan uygulama yazalım.

#include <stdio.h>

int main(int argc, char** argv) {
    int sayi;
    scanf("%d",&sayi);
    if (sayi == NULL) {
        printf("%s\n", "Geçersiz sayı girdiniz.");
    } else if(sayi % 2) {
        printf("%d tektir.\n", sayi);
    } else {
        printf("%d çifttir.\n", sayi);
    }
    return 0;
}

Burada % operatörü 2 ile bölümden kalanı bulmaya yarar. Sayı tek ise 1 değilse 0 sonucu elde edilir. Bu sayede tek sayılar için koşul sağlanır çift sayılar için sağlanmaz.

Tek satırdan oluşan koşullarda {} kullanmaya gerek yoktur.

if (i < 32)
  printf("%s\n","32den küçüktür");

Koşul ifadeleri aşağıdaki gibi listelenebilir.

Koşul işleyicileri

ifade

anlamı

örnek

>

büyüktür

121 > 12

<

küçüktür

12 < 121

==

birbirine eşittir

121 == 121

!

karşıtlık bildirir.

!(12 > 121)

&&

logic and

"fg" == "aa" && 121 > 12

||

logic or

"fg" == "aa" || 121 > 12

!=

eşit değildir

"fg" != "aa"

>=

büyük eşittir

121 >= 121

<=

küçük eşittir

12 <= 12

Switch - Case

Bir sayıya karşılık bir işlem yapmak için switch - case yapısı kullanılır.

      switch(sayi) {
        1:
          // sayı 1se burası çalışır.
          // break olmadığı için alttan devam eder.
        2:
          // sayı 1 veya 2 ise burası çalışır.
          break;
        3:
          // sayı 3 ise burası çalışır.
        default:
          // sayı eşleşmezse burası çalışır.
}

Döngüler

Döngüler koşullara benzer fakat döngülerde koşul sağlanmayana kadar block içi tekrarlanır. Döngü oluşturmak için while kullanılır.

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

Yukarıdaki örnekte 10dan 0a kadar geri sayan örnek verilmiştir. En son i değişkeni 0 olduğunda koşul sağlanmadığı için döngü sonlanır.

Aynı işlemi for ifadesi ile de yapabiliriz.

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

Burada for içerisinde 3 bölüm bulunur. İlkinde değer atanır. İkincinde koşul yer alır. Üçüncüsünde değişkene yapılacak işlem belirtilir.

Döngülerde continue kullanarak döngünün tamamlanması beklenmeden başa dönülür. break kullanarak döngüden çıkılır.

      int sayi = 10
      while(1) {
          printf("%d\n",sayi);
    if(sayi < 0) {
              break;
          }
          sayi--;
          continue;
          printf("%s\n","Bu satıra gelinmez.");
}

Yukarıdaki örnekte döngü koşulu sürekli olarak devam etmeye neden olur. Sayımız 0dan küçükse döngü break kullanarak sonlandırılır. Döngü içinde continue kısmına gelindiğinde başa dönüldüğü için bir alttaki satır çalıştırılmaz.

goto

C dilinde kodun içerisindeki bir yere etiket tanımlanıp goto ile bu etikete gidilebilir.

yaz:
printf("%s\n", "Hello World");
goto yaz;

Yukarıdaki örnekte sürekli olarak yazı yazdırılır. Bunun sebebi her seferinde yaz etiketine gidilmesidir.

Bundan faydalanarak döngü oluşturulabilir.

int i = 10;
islem:
if(i < 0){
    printf("%d\n",i);
    i--;
    goto islem;
}

Burada koşul bloğunun en sonunda tekrar başa dönmesi için goto kullandık.

Fonksiyonlar

C dilinde bir fonksiyon aşağıdaki gibi tanımlanır.

int yazdir(char* yazi){
    if(yazi != NULL){
        printf("%s\n",yazi);
        return 0;
    }
return 1;
}

Yukarıdaki fonksiyon verilen değişken değere sahipse ekrana yazdırıp 0 döndürür. Eğer değeri yoksa 1 döndürür.

Basit işlemler için #define ile de fonksiyon tanımlanabilir. Bu şekilde tanımlanan fonksiyonlar derleme öncesi yerine yazılarak çalışır.

#define topla(A,B) A+B

int main(int argc, char** argv){
    int sayi = topla(3, 5);
    return 0;
}

Fonksiyonlar yazılma sırasına göre kullanılabilirler. Bu yüzden fonksiyonlar henüz tanımlı değilse kullanılamazlar. Bu durumun üstesinden gelmek için header tanımlaması yapılır.

void yaz();
int main(){
    yaz();
    return 0;
}
void yaz(){
    printf("%s\n","Hello World");
}

Header tanımlamaları kütüphane yazarken de kullanılır. Bunun için bu tanımlamaları .h uzantılı dosyalara yazmanız gereklidir. Bu dosyayı include kullanarak eklemeliyiz.

yaz.h dosyası

void yaz();

main.c dosyası

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

int main(){
    yaz();
    return 0;
}

void yaz(){
    printf("%s\n","Hello World");
}

Not: include ifadesinde <> içine aldığımız dosyalar /usr/include "" içine aldığımız ise mevcut dizinde aranır.

Pointer ve Address kavramı

Pointerlar bir değişkenin bellekte bulunduğu yeri belirtir. ve * işareti ile belirtir. Örneğin aşağıda bir metin pointer olarak tanımlansın ve 2 birim kaydırılsın.

char* msg = "abcde";
printf("%s\n", msg + sizeof(char)*2 );

Bura 2 char uzunluğu kadar pointer kaydırıldığı için ekrana ilk iki karakteri silinerek yazdırılmıştır.

Adres ise bir değişkenin bellek adresini ifade eder. & işareti ile belirtilir. Örneğin rastgele bir değişken oluşturup adresini ekrana yazalım.

int i = 0;
printf("%p\n" &i);

Konunun daha iyi anlaşılması için bir değişken oluşturup adresini bir pointera kopyalayalım. ve sonra değişkenimizi değiştirelim.

int i = 0; // değişken tanımladık.
int *k = &i; // adresini kopyaladık.
int l = i; // değeri kopyaladık.
i = 1; // değişkeni değiştirdik.
printf("%d %d\n", i, *k, l);

Bu örnekte ilk iki değer de değişir fakat üçüncüsü değişmez. Bunun sebebi ikinci be birinci değişkenlerin adresi aynıyken üçüncü değişkenin adresi farklıdır.

Bir fonksiyon tanımlarken pointer olarak arguman aldırıp bu değerde değişiklik yapabilir. Buna örnek kullanım olarak scanf fonksiyonu verilebilir.

#include <stdio.h>
void topla(int* sonuc, int sayi1, int sayi2){
    *sonuc = sayi1 + sayi2;
}
void main(){
    int i;
    topla(&i, 12, 22);
    printf("%d\n",i);
}

Burada fonksiyona değişkenin adresi girilir. Fonksiyon bu adrese toplamı yazar. Daha sonra değişkenimizi kullanabilirsiniz.

Fonksiyonun kendisini de pointer olarak kullanmak mümkündür. Bunun için aşağıdaki gibi bir yapı kullanılabilir.

int topla(int i, int j){
    return i + j;
}

void main(){
    int (*topla_func)(int, int) = topla;
    topla_func(3, 5);
}

Ayrıca typedef yapısı ile de fonksiyon pointerları oluşturulabilir. Bu konunun detaylarına ilerleyen kısımlarda yer verilmiştir.

typedef int (*topla_func)(int, int);
int topla(int i, int j){
    return i + j;
}

void main(){
    topla_func topla_fn = topla;
    topla_fn(3, 5);
}

Dinamik bellek yönetimi

Dinamik bellek yönetimi için malloc, realloc, calloc, free fonksiyonları kullanılır. Bu fonksiyonlar stdlib.h ile sağlanmaktadır.

malloc fonksiyonu belirtilen boyut kadar boş alanı void* olarak tahsis eder.

// 10 elemanlı sayı dizisi oluşturmak için.
int *sayilar = (int*) malloc(10 * sizeof(int));
// şununla aynı anlama gelir.
int sayilar[10];

calloc fonksiyonu malloc ile benzerdir fakat istenen block boyutunu da belirterek kullanılır.

// 10 elemanlı sayı dizisi oluşturmak için
int *sayilar = (int*) calloc(10, sizeof(int));
// şununla aynı anlama gelir
int *sayilar = (int*) malloc(10 * sizeof(int));

realloc bir değişkenin yeniden boyutlandırılmasını sağlar.

// 5 elemanlı dizi tanımlayalım.
int sayilar[5];
// boyutu 10 yapalım
sayilar = (int*) realloc(sayilar, 10*sizeof(int));

free fonksiyonu değişkeni bellekten siler.

// malloc ile bir alan tanımlayalım.
void* alan = malloc(100);
// bu alanı silelim.
free(alan);

Konunun daha iyi anlaşılması için 2 stringi toplayan fonksiyon yazalım.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
char* add(char *s1, char *s2){
    int ss = strlen(s1); // ilk arguman uzunluğu
    int sx = strlen(s2); // ikinci arguman uzunluğu
    char* s3 = (char*)malloc(ss+sx*sizeof(char)); // uzunluklar toplamı kadar alan ayır.
    for(int i=0;s1[i];i++) // ilkinin tüm elemanlarını kopyala
        s3[i] = s1[i];
    for(int i=0;s2[i];i++) // ikincinin tüm elemanlarını kopyala
        s3[i+ss] = s2[i];
    s3[ss+sx]='\0'; // stringler '\0' ile sonlanır
    return s3;
}

void main(){
    char *new_str = add( "hello", "world");
    printf("%s\n", new_str);
}

Struct

Structure yapıları bellekte belli bir değişken topluluğu oluşturup kullanabilmek için kullanılır. Bu yapılar sayesinde kendi veri türlerinizi tanımlayabilirsiniz.

struct test {
    int num;
    char* name;
};

void main(){
    struct test t1;
    t1.num = 12;
    t1.name = "hello";
}

Veri türü adına alias tanımlamak için typedef kullanılabilir. Bu sayede değişken tanımlar gibi tanımlama yapmak mümkündür.

typedef struct Test {
    int num;
    char* name;
} test;

void main(){
    test t1;
    t1.num = 12;
    t1.name = "hello";
}

typedef kullanarak struct dışında değişken türü tanımlamak da mümkündür.

typedef char* my_string;

void main(){
    my_string str = "Hello World";
}

C programlama dili nesne yönelimli bir dil değildir. Bu yüzden sınıf kavramı bulunmaz. Fakat struct kullanarak benzer işler yapılabilir. Bunun için fonksiyon pointeri tanımlayıp struct yapımıza ekleyelim. Bir init fonksiyonu kullanarak nesnemizi oluşturalım.

// nesne struct yapısı tanımladık
typedef struct Test {
    // nesne fonksiyonunu tanımladık.
    void (*yazdir)(char*);
    int num;
} test;

// nesne fonksiyon işlevin tanımladık
void test_yazdir(char* msg){
    printf("%s\n",msg);
}

// nesneyi oluşturan fonsiyonu tanımladık.
test test_init(){
    test t1;
    t1.num = 12;
    t1.yazdir = test_yazdir;
    return t1;
}

void main(){
    test obj = test_init();
    obj.yazdir("Hello World");
}

Kütüphane dosyası oluşturma

Kütüphaneler ana kaynak kodun kullandığı yardımcı kodları barındırır. Bu sayede her uygulama için tek tek aynı şeyleri yazmak yerine tek bir kütüphaneden yararlanılabilir.

GNU/Linux ortamında kütüphaneler .so uzantılıdır ve /lib ve /usr/lib dizinlerinde bulunur.

Not: Ek kütüphane dizinlerini /etc/ld.so.conf ve /etc/lo.so.conf.d/* dosyalarında belirlenir. Bunula birlikte LD_LIBRARY_PATH çevresel değişkeni ile kütüphane dizini tanımı yapılabilir.

Bir dosyanın bağımlı olduğu kütüphaneleri ldd komutu ile görüntüleyebiliriz.

$ ldd /bin/bash
    /lib/ld-musl-x86_64.so.1 (0x7fd299f6d000)
    libreadline.so.8 => /usr/lib/libreadline.so.8 (0x7fd299e5e000)
    libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7fd299f6d000)
    libncursesw.so.6 => /usr/lib/libncursesw.so.6 (0x7fd299e0a000)

Kendi kütüphanemizi olşturmak için kaynak kodumuzu -shared parametresi ile derlememiz gerekmektedir. Bunu için örneğin aşağıdaki gibi bir kaynak kodumuz olsun.

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

Bu kodu derleyelim.

$ gcc -c test.c
$ gcc -o libtest.so test.o -shared

Şimdi de bu kütüphaneyi kullanabilmek için test.h dosyamızı oluşturalım.

int topla (int a, int b);

Son olarak kütüphaneyi kullanan kodumuzu yazalım.

#include <test.h>
void main(){
    int sayi = topla(3, 5);
}

Dikkat ettiyseniz include kullanırken "" işareti yerine <> kullandık. Bunun sebebi kütüphanelerin kaynak koddan bağımsız olacak şekilde tasarlanmasıdır. Header dosyamızın /usr/include içinde ve kutuphanemizin de /usr/lib içinde olduğunu varsayarak kodladık.

Kütüphanemizin kutuphane adındaki bir dizinde bulunduğunu düşünelim ve aşağıdaki gibi derlemeyi tamamlayalım.

$ gcc -c main.c -I ./kutuphane
$ gcc -o main main.o -L ./kutuphane -ltest

Kodu kütüphaneyi sisteme yüklemeden derleyebilmemiz için derleyicimize -I parametresi eklenir. Bu parametre header aradığı dizinlere belirtilen dizini de ekler. Benzer şekilde derlemenin linkleme aşamasında -l parametresi ile hangi kütüphanelere ihtiyaç duyulduğu belirtilir. -L parametresi ile kütüphanenin aranacağı dizinler listesine belirtilen dizin eklenir.

Gördüğünüz gibi bu parametreler sisteme göre değişiklik gösterebilmektedir. Bu karmaşanın önüne geçebilmek için pkg-config kullanılır. Bu dosyada belirtilen değerler kütüphane ile beraber gelmekte olup derlemeye nelerin ekleneceğini belirtir.

Örnek olarak aşağıdaki gibi kullanabiliriz.

# derleme parametreleri
$ pkg-config --cflags readline
  -DNCURSES_WIDECHAR
# linkleme parametreleri
$ pkg-config --libs readline
  -lreadline

Kaynak kodu derlerken aşağıdaki gibi kullanılabilir.

$ gcc -c main.c `pkg-config --cflags readline`
$ gcc -o main main.o `pkg-config --libs readline`

pkg-config dosyaları .pc uzantılıdır ve /usr/lib/pkgconfig içinde bulunur. pkg-config dosyaları aşağıdaki formata benzer şekilde yazılır.

prefix=/usr
includedir=${prefix}/include

Name: Test
Description: Test library
Version: 1.0
Requires: readline
Cflags: -I{includedir}/test
Libs: -ltest -L{libdir}/test

Yukarıdaki örnekte /usr/include/test/ içerisindeki header dosyamızı ve /usr/lib/test/ içindeki kütüphane dosyamızı sorunsuzca kullanarak derleme yapabilik.