Tarihçe

  • 1972, AT&T Bell laboratuvarları

  • Dennis Ritchie (Ken Thompson, Brian Kernighan)

  • Unix araçlarının ve işletim sisteminin gerçeklenmesi


Yaygınlık

  • 2022 itibarıyla 50 yaşında bir dil

  • TIOBE indeksine göre 2022 Şubat ayı itibarıyla 2'nci sırada

  • Bir yıl önce (2021) 1'nci sırada


Neden C Öğrenmelisiniz?

  • Ustalaşmanız gerekmiyor, ama en azından temel okur/yazarlık seviyesinde öğrenmeniz gerekiyor

  • "Lingua Franca"


C Programlama dili günümüzde kullanılan bilgisayar mimarisini en "gerçekçi" modelleyen dil

  • Mimari: CPU, bellek, girdi/çıktı

  • Gerçekçi? Minimal soyutlama, donanıma ("metal"e) yakınlık


  • Geleceği bilemiyoruz (Kuantum Hesaplama vs)

  • Mimaride köklü bir değişiklik olursa bu argümanın geçerliliği azalır


Sonuç: C'yi öğrendiğinizde günümüzün bilgisayar mimarisine daha fazla hakim olacaksınız

  • Donanım kısıtları nedir?

  • Başarımı iyileştirmek için neler yapılabilir?

  • (Veya) Başarımı iyileştiren bir düzenleme bunu nasıl başarıyor?

  • Falanca sorun neden bir güvenlik sorunu?


  • İşletim sistemleri nasıl gerçekleniyor?

  • Dinamik programlama dilleri arka planda nasıl gerçekleniyor?


Tüm bu soruları cevaplayabilmek sizi daha iyi bir programcı yapar


Merhaba Dünya

01    #include <stdio.h>
02
03    int main(void)
04    {
05            printf("Hello, World!\n");
06
07            return 0;
08    }

Derle ve çalıştır

cc -o helloworld helloworld.c
./helloworld
Hello, World!
  • cc: Sisteminizde kurulu C derleyicisini (C Compiler) gösteren bir sembolik link

  • Linux'ta genel olarak gcc, clang vb seçenekler de var

  • -o: Derleme sonucunda üretilen çalıştırılabilir dosyanın adı


cc helloworld.c
./a.out
Hello, World!
  • -o ile çıktı program verilmezse öntanımlı olarak a.out adında bir dosya üretilir

  • Tarihsel olarak seçilen bir isim

  • a.out? "Assembler Output", ama çıktı "Assembler" formatında değil

  • Çalıştırılabilir dosya formatı işletim sistemine bağlı olarak değişir

  • Linux'ta ELF (Executable and Linkable Format)

  • Windows'da PE (Portable Executable)


Kaçış karakteriAçıklama
\nYeni satır (newline)
\tSekme (tab)
\rSatır başı (carriage return)
\0NUL karakter
\\\ karakterinin kendisi

The C Programming Language

Brian Kernighan ve Dennis Ritchie tarafından yazılmış "efsane" kitap

  • Teknik literatürde altın referans

  • "Hello, World!" bu kitapla başlayan bir gelenek

  • Her programcının okuması gereken 3 kaynak istense biri bu

  • Şimdi bu kitaptan örneklerle C programlama dilinde gezineceğiz


Fahrenhayt-Santigrad dönüşümü

0, 20, 40, ..., 300 fahrenhayt sıcaklıklık değerlerinin santigrad karşılıklarını görüntüle


while

#include <stdio.h>

/* print Fahrenheit-Celsius table for fahr = 0, 20, ..., 300 */
int main(void)
{
        int fahr, celsius;
        int lower, upper, step;

        lower = 0;   /* lower limit of temperature scale */
        upper = 300; /* upper limit */
        step = 20;   /* step size */

        fahr = lower;
        while (fahr <= upper) {
                celsius = 5 * (fahr-32) / 9;
                printf("%d\t%d\n", fahr, celsius);
                fahr = fahr + step;
        }

        return 0;
}

TipAçıklama
charKarakter
shortKısa tamsayı, int
longUzun tamsayı, long int
floatGerçel sayı
doubleÇift hassasiyetli gerçel sayı

Bakın bu örnekte sık karşılaşılan bir patern var

  • önce "bir şey"i ilkle

  • döngü başında "bir şey"i sına: yanlışsa döngüyü sonlandır, doğruysa dön

  • her döngü sonunda "bir şey"i değiştir


        i'yi ilkle
        i falan koşulu sağladığı sürece
                ...
                i'yi değiştir

yani

        i = lower;
        while (i <= upper) {
                ...
                i = i + step;
        }

for

#include <stdio.h>

/* print Fahrenheit-Celsius table */
int main(void)
{
        int fahr;

        for (fahr = 0; fahr <= 300; fahr = fahr + 20)
                printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));

        return 0;
}

Sabitler

Bu programda 3 sabit var: bu sabitleri koda gömmek doğru bir pratik değil

  • Başlangıç değeri (alt sınır): 0

  • Bitiş değeri (üst sınır): 300

  • Artış miktarı (adım): 20


#include <stdio.h>

#define LOWER 0   /* lower limit of table */
#define UPPER 300 /* upper limit */
#define STEP  20  /* step size */

/* print Fahrenheit-Celsius table */
int main(void)
{
        int fahr;

        for (fahr = LOWER; fahr <= UPPER; fahr = fahr + STEP)
                printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));

        return 0;
}

BelirtimAçıklama
%dDesimal tamsayı olarak görüntüle
%6dEn az 6 hane uzunluğunda desimal tamsayı görüntüle
%fGerçel sayı olarak görüntüle
%6fEn az 6 hane uzunluğunda gerçel sayı görüntüle
%.2fOndalık noktasından sonra iki hane ile görüntüle
%6.2fEn az 6 hane, ondalık noktasından sonra 2 hane ile görüntüle

BelirtimAçıklama
%cKarakter görüntüle
%sDizgi (string) görüntüle
%xOnaltılı tabanda görüntüle
%oSekizli tabanda görüntüle
%%% karakterinin kendisi

char: Karakter veri tipi

  • Bilgisayar için her şey bir sayı

  • Ya bir matematiksel işleme giren olağan bir sayı, örneğin bir tam sayı veya gerçel sayı

  • Ya da bir şeyi temsil eden (onu kodlayan bir) "kod", örneğin bir renk, bir komut

  • Bilgisayarın dünyasındaki (tek bildiği şey olan) sayıların büyük bir kısmı bu ikinci grupta


  • O halde "karakter" de bir sayı, bir "kod" sayısı

  • En basit kodlama: ASCII

  • Her karakter bu tabloda bir doğal sayı (pozitif tam sayı)


ASCII Tablo

Dec  Char                           Dec  Char     Dec  Char     Dec  Char
---------                           ---------     ---------     ----------
  0  NUL (null)                      32  SPACE     64  @         96  `
  1  SOH (start of heading)          33  !         65  A         97  a
  2  STX (start of text)             34  "         66  B         98  b
  3  ETX (end of text)               35  #         67  C         99  c
  4  EOT (end of transmission)       36  $         68  D        100  d
  5  ENQ (enquiry)                   37  %         69  E        101  e
  6  ACK (acknowledge)               38  &         70  F        102  f
  7  BEL (bell)                      39  '         71  G        103  g
  8  BS  (backspace)                 40  (         72  H        104  h
  9  TAB (horizontal tab)            41  )         73  I        105  i
 10  LF  (NL line feed, new line)    42  *         74  J        106  j
 11  VT  (vertical tab)              43  +         75  K        107  k
 12  FF  (NP form feed, new page)    44  ,         76  L        108  l
 13  CR  (carriage return)           45  -         77  M        109  m
 14  SO  (shift out)                 46  .         78  N        110  n
 15  SI  (shift in)                  47  /         79  O        111  o
 16  DLE (data link escape)          48  0         80  P        112  p
 17  DC1 (device control 1)          49  1         81  Q        113  q
 18  DC2 (device control 2)          50  2         82  R        114  r
 19  DC3 (device control 3)          51  3         83  S        115  s
 20  DC4 (device control 4)          52  4         84  T        116  t
 21  NAK (negative acknowledge)      53  5         85  U        117  u
 22  SYN (synchronous idle)          54  6         86  V        118  v
 23  ETB (end of trans. block)       55  7         87  W        119  w
 24  CAN (cancel)                    56  8         88  X        120  x
 25  EM  (end of medium)             57  9         89  Y        121  y
 26  SUB (substitute)                58  :         90  Z        122  z
 27  ESC (escape)                    59  ;         91  [        123  {
 28  FS  (file separator)            60  <         92  \        124  |
 29  GS  (group separator)           61  =         93  ]        125  }
 30  RS  (record separator)          62  >         94  ^        126  ~
 31  US  (unit separator)            63  ?         95  _        127  DEL

Gözlemler

  • Bir metin dosyasında klavyeden girdiğiniz hemen hemen tüm karakterler 32-126 aralığında

  • Sekme (\t) ve satır sonu (\n) ilk 32'lik blokta

  • Rakamlar, büyük harfler ve küçük harfler peşpeşe sıralı

  • Örneğin 9'a ait kod 5'e ait koddan büyük

  • Örneğin Z harfine ait kod z harfine ait koddan küçük


Tabloya bakarak yorumlayın

#include <stdio.h>

int main(void)
{
        char c;

        printf("%d\n", sizeof(char));

        c = 'a';
        printf("'%c' -> %d\n", c, c);

        c = ' ';
        printf("'%c' -> %d\n", c, c);

        return 0;
}
cc -o main main.c
./main
1
'a' -> 97
' ' -> 32

Ayrıntılar

C standardına göre

  • char bir karakteri temsil eden en az (ve çoğunlukla tam olarak) 8 bit uzunluğunda bir veri tipi

  • Bir char'ın bir bayt olduğunu ve 1 bayt'ın en az 8 bit olduğunu garanti edilir

Fakat

  • 1 bayt'ın tam olarak 8 bit olduğu söylenmez (tarihsel olarak 1 bayt = 9 bit gibi örnekler de var)

  • Tipik ve yaygın olarak 1 bayt = 1 karakter = 8 bit

  • Karakter bir tam sayıya dönüştürüldüğünde daima pozitif bir tam sayı elde edileceği söylenmez

  • Ama tipik ve yaygın olarak 0-255 aralığında (uçlar dahil) pozitif bir tam sayıdır


Girdiyi Çıktıya kopyala

        bir karakter oku
        okunan karakter dosya sonunu belirtmediği sürece
                karakteri yaz
                yeni karakter oku

#include <stdio.h>

int main(void)
{
        int c;

        c = getchar();
        while (c != EOF) {
                putchar(c);
                c = getchar();
        }

        return 0;
}

Standart Girdi/Çıktı modeli üzerinde ayrıntılı duracağız

  • getchar: standart girdiden bir karakter oku

  • putchar: standart çıktıya bir karakter yaz


Daha kısa nasıl yazabiliriz?

#include <stdio.h>

int main(void)
{
        int c;

        while ((c = getchar()) != EOF)
                putchar(c);

        return 0;
}

  • Bakın bu örnekte c bir char değil bir int, neden?

  • Çünkü getchar daima bir tam sayı dönüyor (negatif değer de alabilen)

  • İsmiyle döndüğü değer arasında büyük uyumsuzluk olan standart kitaplık fonksiyonlarının başında geliyor

  • getchar bir char dönmüyor, int dönüyor, neden?


Çünkü:

  • getchar dosya sonuna geldiği bilgisini çağıran tarafa "bir şekilde" iletmeli, nasıl? dönüş değeriyle

  • EOF stdio.h içinde tanımlı bir sabit, değeri "çoğunlukla" -1

  • Tipik olarak geçerli bir char daima bir doğal sayı, bk. ASCII tablo

  • Yani EOF geçerli bir karakter değil (bu konuyu daha ayrıntılı anlatacağız)

  • Bu sayede dosya sonuna gelindiği bilgisi geçerli bir karakter kodu kullanmadan iletebiliyor


  • getchar'ın girdiyi klavyeden okuduğuna dikkat edin

  • Aslında stdin adı verilen standart girdiyi okuyor

  • Girdiyi böyle hep karakter karakter mi alacağız? Bir sayı veya dizgi okumak istesek?


scanf

printf'in kardeşi

  • Standart girdiden (stdin) bir biçim şablonuna uygun şekilde veri okumakta kullanılıyor

  • "Scan formatted" → scanf

#include <stdio.h>

int main(void)
{
        int x, y;

        scanf("%d %d", &x, &y);
        printf("x: %d y: %d\n", x, y);

        return 0;
}

&x, &y?

  • Değişken adının başındaki & karakterinin anlamı

  • &: "adresi" operatörü, &x → x değişkeninin adresi

  • Bu ileri bir konu, "işaretçiler"i anlatırken ayrıntılı değineceğiz


        int x;

        scanf("%d", &x);

Şimdilik şöyle bir metaforla anlayalım:

  • Bir tamsayı hacimli x kovasını, kovanın & ile gösterilen sapından tutup scanf fonksiyonunun içine sarkıtıyoruz

  • scanf dış dünyadan bir tamsayı okuyor ve kovayı dolduruyor

  • Kovayı çektiğimizde içinde artık bir tamsayı var → x


Zorunlu Uyarı

scanf ailesindeki fonksiyonları sadece eğitsel amaçlarla kullanın

  • Bu fonksiyonların gerçek programlarda bir yeri yok

  • printf ailesi bu önermenin dışında

Neden?

  • Bu fonksiyonlar "güvenli" değil, duruma göre taşma "overflow" yaşanabilir, engellemek zor

  • Veri girişi komplekstir, scanf gibi basit bir fonksiyonla bu içsel kompleksiteyi karşılayamazsınız


Veri Girişi/Çıkışı

  • Programın dış dünyayla kontak noktaları

  • C programlarında UNIX'in girdi/çıktı modelini iyi kavramamız lazım

  • Bu model stdio.h içinde tanımlanan fonksiyonlarla gerçekleniyor

  • Standart C çalışma ortamına sahip her işletim sisteminde bu modeli bulabilirsiniz


Standart Girdi/Çıktı modeli

  • Program komut satırında adını yazarak (veya Grafik ortamda tıklayarak/taplayarak) "çalıştırılıyor"

  • İşletim sistemi program imajını diskten belleğe yüklüyor

  • Program işletim sistemi denetiminde bir "proses"e dönüştürülüyor


  • Standart modelde her proses için 3 adet "dosya" veya "akım" ("stream") sunuluyor (ata prosesten miras yoluyla)

  • Bunlardan ikisi çıktı: sırasıyla 1 ve 2 tamsayıları ile veya stdout ve stderr isimleriyle gösteriliyor

  • İlki ise girdi: 0 tamsayısı ile veya stdin ismiyle gösteriliyor


Etkileşimli ("interactive") bir terminalin başındasınız

  • Prosesin girdisi klavye ile sizin tarafınızdan sağlanıyor

  • Proses çıktı olarak ekranı kullanıyor; işlem sonuçları ekranda görüntüleniyor


Özetle "etkileşimli" bir terminalde:

  • klavye → stdin

  • stdout → ekran

  • stderr → ekran (okumaya devam edin)


stdin

Proses, örneğin verileri stdin'den okuyor, nasıl? Pek çok yolu var, bir örnek:

int x;

scanf("%d", &x);

Bir başka örnek:

int c;
c = getchar();

  • Programlama dillerinde klavyeden veri okuyan neredeyse tüm standart fonksiyonlar aslında stdin'den veri okur

  • Standart fonksiyonlar: getchar, scanf vb

  • klavye → stdin


stdout

Proses, örneğin yararlı işlem sonuçlarını stdout'a yazıyor, nasıl? Özel bir şey yapmasına gerek yok:

printf("Result, %d\n", result);

Bir başka örnek:

putchar(c);
  • Programlama dillerinde ekranda görüntüleme yapan neredeyse tüm standart fonksiyonlar aslında stdout'a yazar

  • Standart fonksiyonlar: putchar, printf vb

  • stdout → ekran


stderr?

  • Prosese başlangıçta her ikisi de ekrana "yönlendirilmiş" olarak verilen iki çıktı dosyasından biri

  • Programın hata iletilerini stderr'e yazacak şekilde gerçeklendiğini düşünelim, nasıl?

fprintf(stderr, "Bu bir hata\n");

Yönlendirme

  • Her iki çıktı dosyası da ekrana yönlendirildiğinden sonuçlar ve hata iletileri hepsi birlikte ekranda

  • Standart girdi/çıktı dosyaları farklı yerlere yönlendirilebilir

  • stderr'i ekran yerine bir başka yere yönlendirirsek ne olur?

  • Tüm hata iletilerini "günlük" niteliğinde kaydetmiş oluruz


Kabuk

  • Programları "adlarını yazarak" çalıştırdığımız yönetici bir program var: "kabuk"

  • Kabuk, yönlendirme yapmak için olanaklar sunuyor, bk. önceki örnekte >


ls olmayan_bir_dosyanın_listenmesi_bir_hatadır 2>error.txt
cat error.txt
ls: cannot access 'olmayan_bir_dosyanın_listenmesi_bir_hatadır': No such file or directory
  • Neden 2>error.txt? 2?

  • Hatırlayın stderr'e 2 tamsayısı atanmıştı

  • Bu kabukta kullanılan özel bir notasyon: 2>«dosya»

  • Dikkat! 2 ile > arasında boşluk yok, boşluk olursa sonuç ne oluyor?


Bu şekilde standart girdiyi de (stdin) ön tanımlı klavye yerine bir başka yerden, örneğin bir dosyadan alabiliriz:

#include <stdio.h>

int main(void)
{
        int x, y;

        scanf("%d %d", &x, &y);
        printf("x: %d y: %d", x, y);

        return 0;
}

Klavyeden:

cc -o main main.c
./main

Dosyadan

./main <veri.txt

Bunun başka yolları da var, "heredoc notasyonu":

echo 9 | ./main
./main <<IN
9
IN
  • "heredoc" << yönlendirme karakterlerinden sonra hangi tanımlayıcı yazılmışsa onunla kapatın

  • Örnekte IN


Tüm bu örneklerde veri klavyeden değil dosyadan geliyor: zarif ve basit bir model

Bu sayede prosesin nereden veri alıp nereye veri yazacağını, program kodunda bir düzenleme yapmadan ayarlayabiliyoruz


Örnekler

Düzenleyelim (main.c)

#include <stdio.h>

int main(void)
{
        int a;

        scanf("%d", &a);

        if (a <= 0) {
                fprintf(stderr, "Error: Number must be a positive integer\n");
                return 1;
        }

        printf("%d\n", a);

        return 0;
}

Derleyelim

cc -o main main.c

./main

./main >out.txt

./main >out.txt <<IN
19
IN

./main <<IN
-1
IN

./main 2>err.txt <<IN
-1
IN

./main >/dev/null <<IN
19
IN

./main 2>/dev/null <<IN
-1
IN

main fonksiyonunun dönüş değeri

#include <stdio.h>

int main(void)
{
        ...
        if (a <= 0) {
                fprintf(stderr, "Error: Number must be a positive integer\n");
                return 1;
        }
        ...
        return 0;
}
  • Programın başarılı olup olmadığını çağıran tarafa (kabuk) iletmemizde yarar var

  • Bu sayede programın başarılı veya başarısız olması durumuna göre farklı işlemler yapabiliriz

  • İşte bu nedenle main bir tamsayı döner

  • 0 → başarı (çünkü başarı tek)

  • != 0 (tipik olarak 1) → başarısız (pek çok şekilde başarısız olabilirsiniz, her birine farklı sayı atayarak)


cat

  • concatenation yapıyor; ismini bu kelimedeki cat'ten almış

  • concatenation birleştirme

  • cat terimini gördüğünüzde aklınıza daima bir tür "birleştirme" anlamı gelsin


cat /etc/passwd /etc/fstab
  • İki dosyanın içeriği "birleştirilerek" ekran görüntüleniyor

  • İstediğiniz sayıda dosya verebilirsiniz

  • cat argüman olarak verilen dosyaları birleştirerek stdout'ta görüntüler


UNIX sistemlerde dosya içeriğini görüntülemenin en basit ve kanonik yolu cat kullanmak

cat /etc/passwd
  • Neden? Tek dosya argümanı verirseniz (birleştirecek bir şey olmadığından) sadece bu dosyayı görüntüler

  • Peki, cat'e hiç bir argüman verilmezse?


cat programı argüman yoksa veriyi stdin'den, yani ön tanımlı olarak klavyeden okumaya çalışır

cat
  • Bilgisayar kilitlendi mi?

  • Hayır, sizden veri girmenizi bekliyor


  • Veri girdik, her yeni satırda cat satırı görüntüler

  • Peki, nasıl çıkacağız? Cevap Ctrl-C değil, bu sadece işlemi iptal eder

  • cat'e dosya sonunda olduğu bilgisini iletmemiz lazım


Etkileşimli terminallerde girdi sonunu "sinyallemek" için Ctrl-D tuş kombinasyonunu kullanıyoruz

  • Bu tuş kombinasyonuna bastığımızda girdiye (Ctrl-D'ye karşı düşeceğini varsaydığımız) bir karakter eklenmiyor

  • Ctrl-D bir karakter değil

  • Ctrl-D işletim sisteminin klavye sürücüsü tarafından yakalanarak programa "sözde" EOF değeri ileten bir tuştur


EOF

"End Of File"

  • Negatif bir tamsayı: genel olarak -1, ilgili platformdaki standart C kitaplığında tanımlı

  • İşletim sisteminin girdi/çıktı fonksiyonlarına "akımın sonundayız" durumunu iletmek için kullanılan bir değer

  • EOF dosyaların sonunda bulunan "özel bir karakter" değil

  • Negatif bir değer olması nedeniyle EOF'nin (pozitif tamsayı değerli) geçerli bir karakter olamayacağını not edin


  • Standart girdinden veri okuyoruz

  • Verinin sonuna geldiğimizi nasıl anlayacağız?

  • Standart C kitaplığında dosyadan veri okuyan hemen hemen tüm fonksiyonlar dosya sonunda EOF değeri döner

  • Neden?

  • İşletim sisteminin girdi/çıktı fonksiyonlarına "akımın sonundayız" durumunu iletmek için kullandığı değer EOF


getchar ve girdi sonu

Örneği hatırlayalım:

#include <stdio.h>

int main(void)
{
        int c;

        while ((c = getchar()) != EOF)
                putchar(c);

        return 0;
}

Bu örnekte stdin'den sürekli bir karakter okuyoruz; çalıştırıp deneyin

  • Girdiyi nasıl sonlandıracağız?

  • cat'teki gibi: Ctrl-D

  • Ama unutmayın ki girdi klavye yerine dosyadan alınıyorsa dosya sonunda doğal olarak sonlanacak


cat chars.txt
a
b
c

./main <chars.txt
a
b
c

Dosyalarla çalışırken kullandığımız notasyonla ilgili önemli bilgilendirme

  • "Bakın chars.txt dosyasının içeriği budur" anlamında "cat chars.txt" komutunu kullanıyoruz

  • Bu sayede ayrıca dosya adı, dosya içeriği yazmamız gerekmiyor

  • Kodu denerken de kolaylık sağlıyor


scanf ve girdi sonu

#include <stdio.h>

int main(void)
{
        int num;

        while (scanf("%d", &num) != EOF)
                printf("Read: %d\n", num);

        return 0;
}

Bu örnekte stdin'den dosya sonuna gelinceye kadar bir sayı okuyoruz; çalıştırıp deneyin

  • Girdiyi nasıl sonlandıracağız?

  • cat'teki gibi: Ctrl-D

  • Ama unutmayın ki girdi klavye yerine dosyadan alınıyorsa dosya sonunda doğal olarak sonlanacak


scanf dönüş değeri

scanf fonksiyonunun standart kitaplık dokümanlarında açıklanmış olan dönüş değerlerine dikkat edin

  • getchar gibi dönüş değeri bir tamsayı

  • getchar okunan karakteri dönerken, scanf okuma durumuyla ilgili bir durum bilgisi dönüyor, okunan veriyi değil

  • scanf okunan veriyi adresini aldığı değişkene yazar

  • "Akım" sonuna gelindiğinde EOF

  • Başarılı bir okumada (eşleştirmeler hatasız, dönüştürme hatası yok) okunan değişken sayısı

  • Dönüşüm hatasında 0


cat numbers.txt
3
5
8

./main <numbers.txt
Read: 3
Read: 5
Read: 8

Bunu başka nasıl çalıştırabiliriz?

cat numbers.txt | ./main
Read: 3
Read: 5
Read: 8

veya

./main <<EOF
2
4
6
EOF
Read: 2
Read: 4
Read: 6

Hatta şöyle:

echo -e "2\n4\n8" | ./main
Read: 2
Read: 4
Read: 6

getchar/scanf ve girdi tamponu

Standart girdiden okuma yapan girdi fonksiyonları doğrudan klavyeyi değil bir girdi tamponunu ("buffer") okur

  • Klavyeden gelen karakterler sırayla bir tampona giriyor

  • Tampona '\n' satır sonu karakteri girdiğinde girdi fonksiyonları çağrılıyor

  • Girdi fonksiyonları her seferinde bir karakteri çıkartarak tamponu tüketiyor

  • Buna tamponlanmış girdi ("buffered input") diyoruz


Sürprizlere dikkat!

  • Unutmayın stdio.h'ta tanımlı getchar/scanf vs daima tampon okuyor, tamponda o sırada ne varsa

  • Tamponda beklentinizin dışında karakterler bulunabilir


|: Borulama

Bir prosesin stdout'a yazdığı veriyi bir başka prosesin stdin'ine yazar

  • İki proses arasında bir tür boru var (|: "pipe" karakteri)

  • Örnekte echo'nun çıktısı main'in girdisi oluyor

  • Bu işlemi "kabuk" (doğal olarak işletim sistemi yoluyla) yapar

  • Kabuğun buradaki işlevi? Gerekli sözdizimini sağlamak


getchar tekrar

Standart girdi/çıktı hakkında kısa bir bilgilendirme yaptıktan sonra örneklerimize devam edelim


Karakter sayısı

#include <stdio.h>

int main(void)
{
        long nc;

        nc = 0;
        while (getchar() != EOF)
                ++nc;
        printf("%ld\n", nc);

        return 0;
}

Peki ya çok büyük bir dosya ise?

#include <stdio.h>

int main(void)
{
        unsigned long long nc;

        for (nc = 0; getchar() != EOF; ++nc)
                ; /* nop */

        printf("%ld\n", nc);

        return 0;
}

unsigned long long

#include <stdio.h>

int main(void)
{
        unsigned long long nc;

        printf("%d\n", sizeof(nc));

        return 0;
}
cc -o main main.c
./main
8

  • Sonuç platforma göre değişir, burada 8 bayt

  • unsigned işaretsiz, bu sayede pozitif sayı limiti artıyor, neden? negatif sayılar kodlanmıyor

  • C99 standardında tanımlı, en az 64 bit garanti ediliyor (ki bu örnekte de 64 bit)

  • Yani? 0'dan başlayıp +18,446,744,073,709,551,615'a kadar çıkabiliyoruz


Satır sayısı

#include <stdio.h>

int main(void)
{
        int c, nl;

        nl = 0;
        while ((c = getchar()) != EOF)
                if (c == '\n')
                        ++nl;
        printf("%d\n", nl);

        return 0;
}

if

  • Bir anahtar kelime

  • Koşul deyimi

  • ==: Eşitlik denetimi yapan operatör


Kelime istatistiği

#include <stdio.h>

#define IN      1
#define OUT     0

int main(void)
{
        int c, nl, nw, nc, state;

        state = OUT;
        nl = nw = nc = 0;
        while ((c = getchar()) != EOF) {
                ++nc;
                if (c == '\n')
                        ++nl;
                if (c == ' ' || c == '\n' || c == '\t')
                        state = OUT;
                else if (state == OUT) {
                        state = IN;
                        ++nw;
                }
        }

        printf("%d %d %d\n", nl, nw, nc);

        return 0;
}

Bu örnekte yeni olarak karşımıza çıkanlar:

  • else if / else

  • ||: Mantıksal VEYA bağlacı


Karakter istatistiği

#include <stdio.h>

int main(void)
{
        int c, i, nwhite, nother;
        int ndigit[10];

        nwhite = nother = 0;
        for (i = 0; i < 10; ++i)
                ndigit[i] = 0;

        while ((c = getchar()) != EOF)
                if (c >= '0' && c <= '9')
                        ++ndigit[c - '0'];
                else if (c == ' ' || c == '\n' || c == '\t')
                        ++nwhite;
                else
                        ++nother;

        printf("digits = ");
        for (i = 0; i < 10; ++i)
                printf(" %d", ndigit[i]);
        printf(", white space = %d, other = %d\n",
                nwhite, nother);


        return 0;
}

Bu örnekte yeni olarak karşımıza çıkanlar:

  • &&: Mantıksal VE bağlacı

  • En önemlisi int ndigit[10] bu bir dizi


Diziler

int ndigit[10];
  • 10 tam sayı sığasında bir dizi → dizi uzunluğu 10

  • Dizide ilk tam sayı ndigit[0], son tamsayı ndigit[9]

  • ndigit[10] geçerli bir dizi erişimi değil değil mi?

  • Dizi uzunluğu neden 10? Çünkü 10 tane rakam var: 0, 1, ..., 9


ndigit[c - '0'] ???

  • c - '0' işleminin 0-9 aralığında bir sayı ürettiğini not edin

  • Bir rakam okunduğunda ndigit dizisinde nereye (hangi indise) karşı geldiğini belirlemenin zarif yolu

  • ASCII tabloda karakterlerin yerleştirilme düzeninden yararlanan bir yöntem

  • Yöntem neden çok zarif? Aksi halde uzun bir if/else if merdiveni kullanacaktık


Fonksiyonlar

#include <stdio.h>

int power(int m, int n);

int main(void)
{
        int i;

        for (i = 0; i < 10; ++i)
                printf("%d %d %d\n", i, power(2, i), power(-3, i));

        return 0;
}

int power(int base, int n)
{
        int i, p;

        p = 1;
        for (i = 1; i <= n; ++i)
                p = p * base;
        return p;
}

dönüş-tipi function-adı(argüman bildirimleri...)
{
        yerel bildirimler

        deyimler

        dönüş deyimi
}

  • return <ifade>; dönüş deyiminde <ifade> bildirilen dönüş-tipinde bir değer üretir

  • Dönüş değeri yoksa dönüş tipi void

  • void fonksiyonlarda gerekirse return; ile erken dönüş yapabilirsiniz

  • Fonksiyon argüman kabul etmiyorsa argüman bildirimleri alanında void


  • Fonksiyon bildirimiyle fonksiyon tanımının aynı şeyler olmadığına dikkat edin

  • Fonksiyon kullanılmadan önce bildirilmeli

  • Fonksiyon tanımı herhangi bir yerde olabilir (ama mutlaka tanımlanmalı)


Fonksiyon tanımlandığında takip eden satırlar için de bildirilmiş olur (ayrıca bildirim yapmak gerekmez)

#include <stdio.h>

int power(int base, int n)
{
        int i, p;

        p = 1;
        for (i = 1; i <= n; ++i)
                p = p * base;
        return p;
}

int main(void)
{
        int i;

        for (i = 0; i < 10; ++i)
                printf("%d %d %d\n", i, power(2, i), power(-3, i));

        return 0;
}

Değerle çağırmak

"Call by value"

  • Fonksiyon çağrılırken argümanların kendisi değil birer kopyası fonksiyona geçirilir

  • Bu özellikten yararlanılabilirsiniz


int power(int base, int n)
{
        int p;
        for (p = 1; n > 0; --n)
                p = p * base;
        return p;
}

Öte yandan "değerle çağırma"nın belirli kısıtlamalar sunduğunu da not edin

#include <stdio.h>

void swap(int a, int b)
{
        int temp = a;

        a = b;
        b = temp;
}

int main(void)
{
        int a = 1, b = 2;

        printf("before: a = %d, b = %d\n", a, b);

        swap(a, b);

        printf("after: a = %d, b = %d\n", a, b);

        return 0;
}

Dizgiler

Standart girdiden okunan satırlar arasında en uzun olanını görüntüleyin


girdide satır olduğu sürece
        eğer satır bir önceki en uzun satırdan daha uzunsa
                en uzun satır olarak kaydet
                uzunluğunu kaydet
en uzun satırı görüntüle

#include <stdio.h>

#define MAXLINE 1000

int readline(char line[], int maxline);
void copy(char to[], char from[]);

int main(void)
{
        int len;
        int max;
        char line[MAXLINE];
        char longest[MAXLINE];

        max = 0;
        while ((len = readline(line, MAXLINE)) > 0)
                if (len > max) {
                        max = len;
                        copy(longest, line);
                }

        if (max > 0)
                printf("%s", longest);

        return 0;
}

  • Satır bir "karakter dizisi"

  • Buna dizgi ("string") diyeceğiz

  • Okunan her satırı bir karakter dizisinde saklıyoruz

  • Dizi için önceden yer ayrılmalı

  • Ayrılan yerin kapasitesi → MAXLINE


int readline(char s[], int lim)
{
        int c, i;

        for (i = 0; i < lim - 1 && (c = getchar()) != EOF && c != '\n'; ++i)
                s[i] = c;
        if (c == '\n') {
                s[i] = c;
                ++i;
        }
        s[i] = '\0';

        return i;
}

void copy(char to[], char from[])
{
        int i;

        i = 0;
        while ((to[i] = from[i]) != '\0')
                ++i;
}

  • C'de dizgi için özel bir veri tipi yok

  • Dizgiler birer dizi, karakter dizisi

  • Dizgi sonu? \0 karakteri


C'de dizgiler \0 "null karakteri" ile sonlanır


Bir dizide dolaşırken sona geldiğinizi nasıl anlarsınız?

  • Ancak bu sayede kuracağınız bir "dolaşma" döngüsünün sonlanma koşulunu belirleyebilirsiniz

  1. Dizinin uzunluğunu bilirim

  2. Dizi sonuna ulaştığımı gösteren özel bir değere bakarım


  • Pascal programlama dilinde örneğin ilk yöntem kullanılıyor

  • Pascal'da her dizginin bir başlık bölümü var; dizgi uzunluğu bu başlık bölümünde kayıtlı


C'de birer karakter dizisi olan dizgilerde dolaşırken sonlanma koşulu olarak 2'nci yöntemi kullanıyoruz

  • Bu "özel değer"e sentinel diyoruz

  • Özel değer de char tipiyle temsil edilebilir bir şey olmalı, fakat...

  • Özel değer geçerli bir karakter olmamalı değil mi? (aksi halde ondan sonraki geçerli karakterler dikkate alınmaz)

  • C'de seçilen bu "özel değer" → "null karakter" \0

  • Dikkat! Bunu daha sonra göreceğiniz NULL işaretçi ile karıştırmayın (her ne kadar sayı değeri 0 olsa da)


Standart girdiden okunan bir hello satırı

+----+----+----+----+----+----+----+
| h  | e  | l  | l  | o  | \n | \0 |
+----+----+----+----+----+----+----+
  • Satır girdisi satır sonuyla alındığından sonda \n var

  • Dizgi sonunda \0

  • Dizgi uzunluğu nedir?

  • İpucu: dizgi ve dizi farkına dikkat!

  • \0 dizgi uzunluğuna katkıda bulunmaz, ama dizi uzunluğunda hesaba katılır


Dizgi veri tipi

C'de dizgi (String) veri tipi yok

  • C'de "string" bir karakter dizisi (array)

  • Ama çoğunlukla dizi yerine işaretçilerle çalışacağız (daha sonra anlatılacak)


strlen

/* strlen: return length of s */
int strlen(char s[])
{
        int i = 0;
        while (s[i] != '\0')
                ++i;
        return i;
}

Veri tipleri

TipAçıklama
charKarakter
shortKısa tamsayı, int
intTamsayı
longUzun tamsayı, long int
floatGerçel sayı
doubleÇift hassasiyetli gerçel sayı
long doubleÇift hassasiyetli gerçel sayı

TipAçıklama
unsigned charİşaretsiz char
unsigned intİşaretsiz int
unsigned longİşaretsiz long
unsigned long longİşaretsiz long long

Mantıksal veri tipi

Takip ettiğiniz C standardına göre mantıksal veri tipi olmayabilir

  • Örneğin C99 öncesindeki C standartlarında özel bir bool veri tipi yok (ama C++'da her zaman var)

  • C99 ve daha yeni standartlarda #include <stdbool.h> ile kullanılabilir

  • Halen yaygın olan ANSI C'de (C89) mantıksal veri tipi yok


Mantıksal doğruluk/yanlışlık

  • C'de bir ifadenin sonucu 0 olarak değerlendirilmişse bu mantıksal olarak yanlıştır

  • Yanlış olmayan her şey doğru


Örnekler

  • Doğru → -1, "", "0", EOF

Gelecekte daha ayrıntılı göreceğimiz konular:

  • Yanlış → \0 (çünkü değerlendirme sonucu 0)

  • Yanlış → NULL (çünkü değerlendirme sonucu 0)


Sayı sabitleri

  • char: 'a'

  • int: 1234

  • float: 123.4f veya 123.4F, 1e-2f veya 1e-2F

  • double: 123.4 veya 1e-2 (f kullanılmıyorsa double)


Dizgi sabitleri

"hello, world" veya "hello, " "world"


Ön ekler

Sekizli tabanda tamsayı: 0 (sıfır) ön eki, 0h

  • h: 0 ... 7

Onaltılı tabanda tamsayı: \x veya 0x veya 0X ön ekleri, ör. \xhh

  • hh: 0 ... 9, a ... f veya A ... F

Son ekler

  • Uzun sayı (tamsayı veya gerçel sayı): l veya L son eki

  • İşaretsiz sayı: u veya U son eki

  • İşaretsiz uzun sayı: ul veya UL

  • Gerçel sayı: f veya F


Enum sabitler

        enum boolean { NO, YES };

        enum days { SUN = 1, MON, TUE, WED, THU, FRI, SAT };
  • Bunlar birer tamsayı listesi, ör. TUE → 3`)

  • Bu tür sabitler #define ile de tanımlanabilir, enum'ın avantajı derleme zamanı tip denetimi


Tanımlayıcılar

Sürpriz yok


Artık yıl (Leap year)

Bir yıl 4 ile tam bölünebiliyor, fakat 100 ile bölünemiyorsa artık yıldır. Öte yandan 400 ile tam bölünenler daima artık yıldır.

if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
        printf("%d is a leap year\n", year);
else
        printf("%d is not a leap year\n", year);

Short circuit

İfadeyi ele alalım:

  • E1: year % 4 == 0

  • E2: year % 100 != 0

  • E3: year % 400 == 0

if ((E1 && E2) || E3)
        printf("%d is a leap year\n", year);
else
        printf("%d is not a leap year\n", year);

  • E1'in yanlış olduğu anlaşıldığında E2 değerlendirilmez

  • Fakat E1 && E2 yanlış olsa bile E3 değerlendirilir

  • Öte yandan E1 && E2 doğru ise E3 değerlendirmez çünkü gerek yok

Buna "short circuit evaluation" deniliyor

  • Bir ifade grubunun değerlendirme sonucu diğerlerinin değerlendirilmesini gereksiz kılıyor

  • "İfade grubu kısa devre yaptığında diğer ifadelerin akımını hesaplamaya gerek yok!"


Tip dönüşümleri

İşaretli sayılarda dönüşüm kuralları

  • Bir işlenen long double ise diğerini de long double yap

  • Aksi halde, bir işlenen double ise diğerini de double yap

  • Aksi halde, char veya short tipleri int yap

  • Bir işlenen long ise diğerini de long yap


İşaretsizlerde dönüşüm kuralları basit değil


"Casting"

Değer tipinin olağan anlamın dışında bir tipte yorumlanmasını sağlamak için "casting" yapıyoruz

  • (tip) ifade

Örnek:

a = sqrt((double) n);

Fakat bu örnekte sqrt fonksiyonu zaten double argüman alacak şekilde tanımlanmışsa "casting" gerekmiyor

double sqrt(double);

...

a = sqrt(2);

atoi

/* atoi: convert s to integer */
int atoi(char s[])
{
        int i, n;
        n = 0;
        for (i = 0; s[i] >= '0' && s[i] <= '9'; ++i)
                n = 10 * n + (s[i] - '0');
        return n;
}

lower

/* lower: convert c to lower case; ASCII only */
int lower(int c)
{
        if (c >= 'A' && c <= 'Z')
                return c + 'a' - 'A';
        else
                return c;
}

#include <ctype.h>

/* lower: convert c to lower case; ASCII only */
int lower(int c)
{
        if (isdigit(c))
                return c + 'a' - 'A';
        else
                return c;
}

Arttırma/Azaltma operatörleri

Ön ek ve son ek formunda: ++i veya i++

  • Semantik farka dikkat, ön ek formunda önce arttır sona değer dön, son ek formunda önce değer dön sonra arttır
n = 5;
k = ++n;
k = n++;

squeeze

/* squeeze: delete all c from s */
void squeeze(char s[], int c)
{
        int i, j;

        for (i = j = 0; s[i] != '\0'; i++)
                if (s[i] != c)
                        s[j++] = s[i];
        s[j] = '\0';
}

strcat

/* strcat: concatenate t to end of s; s must be big enough */
void strcat(char s[], char t[])
{
        int i, j;

        i = j = 0;
        while (s[i] != '\0') /* find end of s */
                i++;
        while ((s[i++] = t[j++]) != '\0') /* copy t */
                ;
}

Üçlü (ternary) ifade işleci

if (a > b)
        z = a;
else
        z = b;

yerine

z = (a > b) ? a : b;

  • Üçlü operatörlü ifadeyi şöyle yazsak?

    z = a > b ? a : b;
    
  • Önce hangi işlem yapılır? Bu ifadenin anlamı hangisi?

    z = a > (b ? a : b)
    z = (a > b) ? a : b
    
  • Evet beklendildiği gibi

    z = a > b ? a : b;
    

Buna "operatör önceliği" (operator precedence) deniliyor

  • Parantezler her zaman beklenildiği gibi çalışır, "parantezler arasındaki ifadeyi grup olarak işle"

  • Parantezler olmadığında "operatör önceliği" kuralları devreye giriyor


Operatör Önceliği

  • Bu kurallara uzun uzun girmek istemiyoruz

  • Genel olarak pek çok işlem "ilk bakışta umulan (makul)" bir öncelik sırasıyla yapılır

  • Şüphe halinde parantezlere başvurabilirsiniz

  • Ama lütfen çok açık durumlar için bunu yapmayın

  • İlave (gereksiz) parantezler "malumu ilam" oluyor ve bilakis okunurluğu zorlaştırıyor


Akış denetimi


Askıda else

"Dangling else"

if (n > 0)
        if (a > b)
                z = a;
        else
                z = b;

  • Örnekteki else hangi if'e ait?

  • Girintiden de anlaşılacağı gibi en yakın if'e ait


Bu bazen istediğiniz bir şey olmayabilir!

if (n > 0)
        if (a > b)
                z = a;
else
        z = b;

Girintiye göre niyetimiz bu, ama sonuç böyle değil (bir önceki slayta bakın)!

if (n > 0) {
        if (a > b)
                z = a;
} else
        z = b;

switch/case deyimi

if/else "merdivenleri"ne bir alternatif


#include <stdio.h>

int main(void)
{
        int c, i, nwhite, nother;
        int ndigit[10];

        nwhite = nother = 0;
        for (i = 0; i < 10; ++i)
                ndigit[i] = 0;

        while ((c = getchar()) != EOF)
                if (c >= '0' && c <= '9')
                        ++ndigit[c - '0'];
                else if (c == ' ' || c == '\n' || c == '\t')
                        ++nwhite;
                else
                        ++nother;

        printf("digits = ");
        for (i = 0; i < 10; ++i)
                printf(" %d", ndigit[i]);
        printf(", white space = %d, other = %d\n",
                nwhite, nother);


        return 0;
}

#include <stdio.h>

int main(void)
{
        int c, i, nwhite, nother;
        int ndigit[10];

        nwhite = nother = 0;
        for (i = 0; i < 10; ++i)
                ndigit[i] = 0;

        while ((c = getchar()) != EOF)
                switch (c) {
                case '0': case '1': case '2': case '3': case '4':
                case '5': case '6': case '7': case '8': case '9':
                        ndigit[c - '0']++;
                        break;
                case ' ': case '\n': case '\t':
                        nwhite++;
                        break
                default:
                        nother++;
                        break;
                }

        printf("digits = ");
        for (i = 0; i < 10; ++i)
                printf(" %d", ndigit[i]);
        printf(", white space = %d, other = %d\n",
                nwhite, nother);


        return 0;
}

  • case'lerde "fall through" yapılır, yani case işleminden sonra bir sonraki case'e düşülür

  • Bu istediğimiz bir davranış değil, bu nedenle break koyuyoruz

  • Bu örnekteki break switch/case deyimini kırıyor, dıştaki while döngüsünü değil

  • Duruma göre bazen break yerine return tercih edilebilir


do/while döngüsü

  • while döngülerinde döngü koşulu döngüye girerken mutlaka denetlenir

  • Ve duruma göre döngüye hiç girilmeyebilir!

  • Fakat bazı durumlarda döngünün en az bir kere çalışması gerektiğini biliriz

  • Bu tür durumlarda do/while kullanıyoruz

do {
        ...
} while (ifade);

itoa

/* itoa: convert n to characters in s */
void itoa(int n, char s[])
{
        int i, sign;
        if ((sign = n) < 0) /* record sign */
                n = -n;     /* make n positive */
        i = 0;
        do {
                /* generate digits in reverse order */
                s[i++] = n % 10 + '0'; /* get next digit */
        } while ((n /= 10) > 0);
        /* delete it */
        if (sign < 0)
                s[i++] = '-';
        s[i] = '\0';

        reverse(s);
}

break ve continue

break: döngüyü bu noktada koşulsuz olarak sonlandır

  • Akış döngü deyiminden sonraki ilk deyimle devam eder

continue: bu noktada dur ve döngü koşuluyla devam et

  • Akış döngü koşulunun sınanmasıyla devam eder (ve buna göre döngü sonlanır veya sonlanmaz)

trim

break örneği

/* trim: remove trailing blanks, tabs, newlines */
int trim(char s[])
{
        int n;
        for (n = strlen(s)-1; n >= 0; n--)
                if (s[n] != ' ' && s[n] != '\t' && s[n] != '\n')
                        break;
        s[n+1] = '\0';
        return n;
}

continue örneği

for (i = 0; i < n; i++)
        if (a[i] < 0) /* negatif değerleri atla */
                continue;
        ... /* pozitif değerleri işle */


goto

  • Kodda bir yerden (etiketle belirtilmiş) bir başka yere atlama yapıyor

  • Yapısal olmayan programlama dillerinde çok yaygın

  • Kod okunurluğunu genel olarak çok azaltıyor → "spagetti programlama"

  • Fakat C'de bunun için yine de olanak sunulmuş, neden?

  • Bazen iç içe döngülerden çıkmanın en kestirme ve basit yolu


a ve b dizilerinde ortak bir eleman var mı?

        for (i = 0; i < n; i++)
                for (j = 0; j < m; j++)
                        if (a[i] == b[j])
                                goto found;
        /* ortak bir eleman bulunamadı */
        ...
found:
        /* a[i] == b[j] olan bir eleman bulundu */

Alternatif olarak şöyle yazılabilirdi:

found = 0;
for (i = 0; i < n && !found; i++)
        for (j = 0; j < m && !found; j++)
                if (a[i] == b[j])
                        found = 1;
if (found)
        /* a[i] == b[j] olan bir eleman bulundu */
        ...
else
        /* ortak bir eleman bulunamadı */
        ...

  • Fakat bu alternatif formun bir parça daha karışık olduğu görülüyor

  • İlave bir kaç denetim ve bir değişken pahasına goto kullanmaktan kaçındık

  • "Asla goto kullanmayın" tutuculuğunda olmayalım


Fonksiyonlar ve Program organizasyonu


dönüş-tipi function-adı(argüman bildirimleri)
{
        yerel bildirimler ve deyimler
}

Minimal fonksiyon

dummy() { }

veya

void dummy(void)
{
}

return

return ifade;
  • Dönüş değerinin literal bir değer olması gerekmiyor

  • İfade değerlendirilerek sonucu dönülüyor

  • İfade yoksa bu bir void fonksiyondur

  • Yani dönüş değeri olmayan fonksiyonlarda erken dönüşler için kullanılabilir


#include <ctype.h>

/* atof: convert string s to double */
double atof(char s[])
{
        double val, power;
        int i, sign;

        for (i = 0; isspace(s[i]); i++) /* skip white space */
                ;
        sign = (s[i] == '-') ? -1 : 1;
        if (s[i] == '+' || s[i] == '-')
                i++;
        for (val = 0.0; isdigit(s[i]); i++)
                val = 10.0 * val + (s[i] - '0');
        if (s[i] == '.')
                i++;
        for (power = 1.0; isdigit(s[i]); i++) {
                val = 10.0 * val + (s[i] - '0');
                power *= 10;
        }

        return sign * val / power;
}

/* atoi: convert string s to integer using atof */
int atoi(char s[])
{
        double atof(char s[]);

        return (int) atof(s);
}

Her iki fonksiyon da standart C kitaplığında stdlib.h'da tanımlı


Program organizasyonu

Gerçel sayı okuyan bir fonksiyon yazalım, öyle ki:

  • Fonksiyon kendi başına ayrı bir dosyada bulunsun

  • Diğer kaynak kod dosyalarından buna erişilebilsin

Özetle programın gerçeklemesini birden fazla dosyaya yayalım


getnum.c

#include <stdio.h>  /* BUFSIZ, getchar */
#include <stdlib.h> /* atof */

char line[BUFSIZ];

/* getnum: convert line to a double */
double getnum(void)
{
        int c, i;
        i = 0;

        int lim = BUFSIZ;

        while (--lim > 0 && (c = getchar()) != EOF && c != '\n')
                line[i++] = c;
        if (c == '\n')
                line[i++] = c;

        line[i] = '\0';

        return atof(line);
}

BUFSIZ

  • Gerçel sayıya çevrilecek dizgi için bellekte yer ayırmalıyız

  • Şimdilik bunu statik olarak yapacağız

  • Gelecekte dinamik olarak malloc yoluyla da yapılabileceğini göreceksiniz

  • Statik olarak ayrılacak bellek alanının boyutunu rastgele büyük bir değer seçmek yerine BUFSIZ olarak seçtik

  • BUFSIZ değeri kullandığınız platforma göre farklı değerler alabilir

  • C standardına göre en az 256 olmalı, tipik olarak 4096 değeri alıyor

  • Bu değer nerede tanımlanmış? stdio.h dosyasında


Kapsam


C'de {} blokları sözdizimsel kapsam oluşturur


#include <stdio.h>

int main(void)
{
        int n = 19;

        {
                int n = 42;
                printf("%d\n", n);
        }

        printf("%d\n", n);

        return 0;
}

#include <stdio.h>

int main(void)
{
        for (int i = 0; i < 2; i++) {
                printf("i: %d\n", i);
        }

        printf("%d\n", i);

        return 0;
}

main.c

#include <stdio.h>

extern double getnum(void);

int main(void)
{
        double num = 0.0;

        while (1) {
                printf("Sayı? ");

                if ((num = getnum()) == 0.0)
                        break;

                printf("Sayı: %f\n", num);
        }

        return 0;
}

extern

extern double getnum(void);
  • Nitelendirilmiş değişkenin bu dosyada değil "dışarıda" bir dosyada tanımlandığını belirtir

  • Kullanılmaması halinde derleyici bu dosyada hata verecektir; çünkü bildirilmemiş bir değişken var


Dosyaları bir arada derlememiz gerekiyor, sadece main.c dosyasını derleyemezsiniz

cc -o getnum getnum.c main.c
./getnum

static

char line[BUFSIZ];
  • Bu değişken getnum kitaplığının "iç işi"

  • Fakat istenirse diğer dosyalardan extern char line[]; bildirimiyle erişilebilir

  • Bunu engellemeliyiz, nasıl?

static char line[BUFSIZ];

Statik olarak bildirilmiş değişkenler:

  • Sadece o dosyaya özeldir, bir başka dosyadan (ör. extern ile) erişilemez

  • Kapsamı o dosya olan "genel değişken" ("global variable") olarak davranır

  • Genel değişkenlerden mümkün olduğunca kaçınıyoruz

  • Kaçınılamazsa en azından static ile sadece dosya geneli yapın


getnum.c

#include <stdio.h>
#include <stdlib.h>

static char line[BUFSIZ];

/* getnum: convert line to a double */
double getnum(void)
{
        int c, i;
        i = 0;

        int lim = BUFSIZ;

        while (--lim > 0 && (c = getchar()) != EOF && c != '\n')
                line[i++] = c;
        if (c == '\n')
                line[i++] = c;

        line[i] = '\0';

        return atof(line);
}

getnum fonksiyonunu kullanan dosyalar her seferinde extern yoluyla bunu bildirmek zorunda mı?

  • Gerçeklemeyi birden fazla dosyaya yaydığımızda bakım sorunları da başlıyor

  • Bu süreci kolaylaştıracak bir şey olmalı


getnum.h

double getnum(void);

main.c

#include <stdio.h>

#include "getnum.h"

int main(void)
{
        double num = 0.0;

        while (1) {
                printf("Sayı? ");

                if ((num = getnum()) == 0.0)
                        break;

                printf("Sayı: %f\n", num);
        }

        return 0;
}

Başlık dosyaları

"Header files"

  • C derlenen bir dil

  • Derleme sırasında tip denetimi dahil statik denetimlerin yapılması için derleyiciyi önden bilgilendirmelisiniz

  • Fonksiyon ne tip değer dönüyor, ne tip değerler alıyor?

  • Gerçeklemeyi birden fazla dosyaya yaydığımızda bu tanımları ortak bir yerde toplamalıyız

  • Başlık dosyaları birbiriyle ilişkili tüm bildirimleri toplayan dosyalar

  • Geleneksel olarak .h uzantısı kullanılıyor


Başlık dosyalarını #include yoluyla tüketiyoruz

  • #include kullanıldığı noktada ilgili dosyanın içeriğini satır satır ekliyor

  • Bu işlem derlemenin ilk fazındaki "ön işleme"de ("preprocessing") gerçekleşiyor


Eklenen dosya nerede bulunuyor? <>, "" farkı

#include <stdio.h>

#include "getnum.h"
  • Üçgen parantezler içinde yazılan dosyalar derleyicinin öntanımlı "include" PATH'inde aranır

  • Örnekte stdio.h /usr/include dizinindedir ve bu dizin öntanımlı dizin listesinde

  • Çift tırnaklar içinde yazılan dosyalar göreceli olarak bulunduğunuz dizinde aranır

  • Örnekte "getnum.h" derleme yaptığınız dizinde

  • Öntanımlı arama yollarını değiştirmek için cc komut satırına girilen seçenekleri veya ortam değişkenlerini kullanın


Başlık dosyalarında genel olarak sadece bildirimler ve sabitler (ör. BUFSIZ) olmalı

  • Fonksiyon bildirimi .h başlık dosyasında

  • Fakat fonksiyon tanımı .c dosyasında


cc -o getnum getnum.c main.c
./getnum
  • Başlık dosyalarının derlemeyi yapan komut satırına girmediğine dikkat edin, neden?

  • Çünkü ön işlemede bu dosyalar zaten kullanılıyor, derleme komutuna özel bir ekleme yapmanız gerekmez


Şimdi bir başka örnek üzerinde çalışalım

  • Basit bir "Stack" ("Yığın") gerçeklemesi

  • "Stack"? Bir tür dizi, sadece iki anlamlı işlem push, pop

  • Yeni ögeler diziye sadece bir taraftan, sondan veya baştan giriyor → push

  • Mevcut ögelerden sadece yığın tepesindeki ögeyi çıkarabiliyoruz → pop


stack.c

#include <stdio.h>

#define MAXITEM 100 /* maximum depth of stack */

static int sp;                /* next free stack position */
static double stack[MAXITEM]; /* stack (of double number values) */

/* push: push f onto value stack */
void push(double f)
{
        if (sp < MAXITEM)
                stack[sp++] = f;
        else
                printf("Error: Stack full, can't push %g\n", f);
}

/* pop: pop and return top value from stack */
double pop(void)
{
        if (sp > 0)
                return stack[--sp];
        else {
                printf("Error: Stack empty\n");
                return 0.0;
        }
}

static int sp = 0;
  • Statik bildirilmiş veriler C standartı gereği daima sıfırlanır

  • Bu ilklemeye gerek yok (ama açık olmak adına böyle yapabilirsiniz)


stack.c (devam)

/* dump: print stack items */
void dump(void)
{
        if (sp > 0) {
                int i = sp;

                while (i-- > 0)
                        printf("%d: %f\n", i, stack[i]);
        } else {
                printf("Stack empty!\n");
                return;
        }
}

/* flush: reset stack while printing all the items */
void flush(void)
{
        while (--sp >= 0)
                printf("%d: %f\n", sp, stack[sp]);
}

"Off-by-one" hatalarına dikkat! --sp >= 0 yerine:

  • sp-- >= 0 yazılırsa?

  • --sp > 0 yazılırsa?

  • Çok sinsi hatalar


stack.h

void push(double);
void pop(void);
void dump(void);
void flush(void);

main.c

#include <stdio.h>

#include "getnum.h"
#include "stack.h"

int main(void)
{
        double num = 0.0;

        while (1) {
                printf("Number [ENTER to stop]? ");

                if ((num = getnum()) == 0.0)
                        break;

                push(num);
        }

        dump();
        flush();
        dump();

        return 0;
}

cc -o stack getnum.c stack.c main.c
./stack

İşaretçiler


Takip eden slaytlardaki anlatım Richard Buckland'ın UNSW'deki derslerinden neredeyse bire bir alıntılanmıştır


+---------+---------+---------+---------+
|0        |1        |2        |3        |
|         |         |         |         |
|    7    |    2    |   4     |    8    |
|         |         |         |         |
+---------+---------+---------+---------+
|4        |5        |6        |7        |
|         |         |         |         |
|    11   |    10   |   3     |    5    |
|         |         |         |         |
+---------+---------+---------+---------+
|8        |9        |10       |11       |
|         |         |         |         |
|    10   |    8    |   2     |    6    |
|         |         |         |         |
+---------+---------+---------+---------+
|12       |13       |14       |15       |
|         |         |         |         |
|    1    |    0    |   0     |    5    |
|         |         |         |         |
+---------+---------+---------+---------+

+---------+---------+---------+---------+
|0        |1        |2        |3        |
|         |         |         |         |
|    7    |    2    |   4     |    8    |
|         |         |         |         |
+---------+---------+---------+---------+
|4        |5        |6        |7        |
|         |         |         |         |
|    11   |    10   |   3     |    5    |
|         |         |         |         |
+---------+---------+---------+---------+
|8        |9        |10       |11       |
|         |         |         |         |
|    10   |    8    |   2     |    6    |
|         |         |     X   |         |
+---------+---------+---------+---------+
|12       |13       |14       |15       |
|         |         |         |         |
|    1    |    0    |   0     |    5    |
|         |         |         |         |
+---------+---------+---------+---------+
printf("%d", x);

  • Değişken → bellek hücrelerine iliştirilen etiket

  • x hücresi → 10 hücre adresi, 2 hücre değeri

  • x denildiğinde ne anlıyoruz? 2 değeri mi, 10 adresi mi?


Bu örneğe göre

  • x denildiğinde "doğal olarak" 2 değerini anlıyoruz

  • Değişken üzerinden 10 adresini nasıl ifade ederiz?

  • & operatörü


+---------+---------+---------+---------+
|0        |1        |2        |3        |
|         |         |         |         |
|    7    |    2    |   4     |    8    |
|         |         |         |         |
+---------+---------+---------+---------+
|4        |5        |6        |7        |
|         |         |         |         |
|    11   |    10   |   3     |    5    |
|         |         |         |         |
+---------+---------+---------+---------+
|8        |9        |10       |11       |
|         |         |         |         |
|    10   |    8    |   2     |    6    |
|         |         |     X   |         |
+---------+---------+---------+---------+
|12       |13       |14       |15       |
|         |         |         |         |
|    1    |    0    |   0     |    5    |
|         |         |         |         |
+---------+---------+---------+---------+
printf("%d", &x);

+---------+---------+---------+---------+
|0        |1        |2        |3        |
|         |         |         |         |
|    7    |    2    |   4     |    8    |
|         |         |         |         |
+---------+---------+---------+---------+
|4        |5        |6        |7        |
|         |         |         |         |
|    11   |    10   |   3     |    5    |
|         |         |         |      Y  |
+---------+---------+---------+---------+
|8        |9        |10       |11       |
|         |         |         |         |
|    10   |    8    |   2     |    6    |
|         |         |     X   |         |
+---------+---------+---------+---------+
|12       |13       |14       |15       |
|         |         |         |         |
|    1    |    0    |   0     |    5    |
|         |         |         |         |
+---------+---------+---------+---------+

  • 7 adresindeki hücrede 5 değeri var

  • Elimizde y adında bir değişken olmadığını varsayalım

  • Adres üzerinden değere nasıl erişiriz?

  • * operatörü ("pointer indirection operator")


+---------+---------+---------+---------+
|0        |1        |2        |3        |
|         |         |         |         |
|    7    |    2    |   4     |    8    |
|         |         |         |         |
+---------+---------+---------+---------+
|4        |5        |6        |7        |
|         |         |         |         |
|    11   |    10   |   3     |    5    |
|         |         |         |      Y  |
+---------+---------+---------+---------+
|8        |9        |10       |11       |
|         |         |         |         |
|    10   |    8    |   2     |    6    |
|         |         |     X   |         |
+---------+---------+---------+---------+
|12       |13       |14       |15       |
|         |         |         |         |
|    1    |    0    |   0     |    5    |
|         |         |         |         |
+---------+---------+---------+---------+
printf("%d", *7); // y

7 adresli hücrenin değeri → 5


+---------+---------+---------+---------+
|0        |1        |2        |3        |
|         |         |         |         |
|    7    |    2    |   4     |    8    |
|         |         |         |         |
+---------+---------+---------+---------+
|4        |5        |6        |7        |
|         |         |         |         |
|    11   |    10   |   3     |    5    |
|         |         |         |         |
+---------+---------+---------+---------+
|8        |9        |10       |11       |
|         |         |         |         |
|    10   |    8    |   2     |    6    |
|         |         |     X   |         |
+---------+---------+---------+---------+
|12       |13       |14       |15       |
|         |         |         |         |
|    1    |    0    |   0     |    5    |
|         |         |         |         |
+---------+---------+---------+---------+
printf("%d", *x);

  • *x operatörünün anlamı x değerli adresin içeriği

  • *24

  • x değeri olan 2 bir adres olarak yorumlanıyor

  • 2 adresli hücrenin içeriği (değer) → 4


???

printf("%d", *&&**x);

* ve & operatörleriyle kurulan bu dünyanın yararı ne?


Fonksiyonlar temel yapıtaşı

  • Karmaşıklığı yönetiyoruz

  • "Code reuse" için en temel birim


Fonksiyonlar arasındaki veri akışını göz önüne alalım

  • Fonksiyona nasıl veri aktarıyoruz?

  • Çağrılan bir fonksiyon çağıran tarafa nasıl veri (örneğin bir hesabın sonucu) aktarıyor?


void task(int x, int y)
{
        x++;
        y++;
}

int main(void)
{
        int x = 19, y = 42;

        task(x, y);

        return 0;
}

  • Function call frame

  • Call stack


  • Fonksiyon çağrılarında argümanlar çağıran tarafından fonksiyonun yığınına kopyalanıyor

  • Bu semantiğe "değerle çağırma" ("pass by value") diyoruz

  • Fonksiyon değerlerin kopyası üzerinde çalışıyor, değerlerin kendisi üzerinde değil

  • Özgün değerler fonksiyon içinde yapılan değişikliklerden etkilenmiyor


"Değerle çağırma" semantiği çok yararlı

  • Fonksiyon çağrılarındaki "yan etki"leri azaltıyor

  • Veri akışını daha anlaşılır kılıyor

  • Örneği hatırlayalım

int power(int base, int n)
{
        int p;
        for (p = 1; n > 0; --n)
                p = p * base;
        return p;
}

Fakat ...

  • Bu örnekte void bir fonksiyon kullandık, fonksiyonun çağıran tarafa veri aktarabilmesi olanaksız

  • Argüman dizi olsaydı ne olacaktı?


void task(int a[], size_t len)
{
        for (int i = 0; i < len; i++)
                a[i]++;
}

int main(void)
{
        size_t len = 1000000;
        int a[len];

        for (int i = 0; i < len; i++)
                a[i] = i;

        task(a, len);

        return 0;
}

Fonksiyonlara dizi argümanları geçirildiğinde ne olur?

  • ilk örnekte olduğu gibi tüm dizi baştan sona yığına kopyalanacak mı?

  • Yani "pass by value" semantiği dizi argümanlarında da kullanılıyor mu?

Bu iki sorunun da yanıtı evet ise ciddi problemlerimiz var

  • İlk örnekle benzer şekilde, çağrılan fonksiyon çağıran tarafa veri aktaramıyor

  • Neden? Çünkü dizinin kopyası üzerinde çalışıyor, kendisi üzerinde değil

  • Tüm bir dizi kopyalanıyorsa bu fonksiyon çağrısının maliyeti ne olur?


İşte işaretçiler bu aşamada sahne alıyor

  • Çağırma sırasında fonksiyona değerin kopyasını değil de kendisini versek?

  • Kendisi? Değerin bulunduğu bellek hücresinin adresi

  • Buna değer referansı diyoruz

  • Bu şekilde gerçekleşen veri aktarımı da "referansla çağırma" ("pass by reference") olarak adlandırılıyor


void task(int *px, int *py)
{
        *px = *px + 1;
        *py = *py + 1;
}

int main(void)
{
        int x = 19, y = 42;

        task(&x, &y);

        return 0;
}

... Veya daha kısa olarak

void task(int *px, int *py)
{
        (*px)++;
        (*py)++;
}

int main(void)
{
        int x = 19, y = 42;

        task(&x, &y);

        return 0;
}

  • int *px → "*px bir tamsayıdır" olarak okuyun

  • px bir işaretçi ("pointer"), bu örnekte bir tamsayı değere işaret eden bir işaretçi

  • Çağırma sırasında px'in &x ile değer aldığına dikkat edin

  • px değeri "adres" olan bir değişken (buna tekrar değineceğiz)


  • *px++ ifadesinde iki operatör var: * ve ++

  • Hangisi önce çalışıyor? "Operator Precedence"

  • *px++*(px++)

  • O halde (*px)++: "px adresindeki değeri 1 arttır"


Takas örneğini hatırlayalım

#include <stdio.h>

void swap(int a, int b)
{
        int temp = a;

        a = b;
        b = temp;
}

int main(void)
{
        int a = 1, b = 2;

        printf("before: a = %d, b = %d\n", a, b);

        swap(a, b);

        printf("after: a = %d, b = %d\n", a, b);

        return 0;
}

Bakın artık böyle çalışır

#include <stdio.h>

void swap(int *pa, int *pb)
{
        int temp = *pa;

        *pa = *pb;
        *pb = temp;
}

int main(void)
{
        int a = 1, b = 2;

        printf("before: a = %d, b = %d\n", a, b);

        swap(&a, &b);

        printf("after: a = %d, b = %d\n", a, b);

        return 0;
}

İşaretçiler sayesinde çağıran ve çağrılan arasındaki veri aktarımındaki sorunu çözmüş olduk

  • Hata yapmaya daha açık kodlar yazma pahasına

  • Çağırana hatalı bir adres aktarılırsa ne olur?

  • Şimdi diğer soruna, "dizinin kopyalanması" sorununa bakalım...


Bir deneme yapalım

void task(int a[], size_t len)
{
        for (int i = 0; i < len; i++)
                a[i]++;
}

int main(void)
{
        size_t len = 1000000;
        int a[len];

        for (int i = 0; i < len; i++)
                a[i] = i;

        task(a, len);

        for (int i = 0; i < 10; i++)
                printf("%d\n", a[i]);

        return 0;
}

Bakın bu kod doğru çalıştı

  • Dizinin her elemanı 1 arttı

  • Demek ki çağırma sırasında fonksiyona dizi kopyası değil, dizinin kendisi iletildi

  • Nasıl?


C'de fonksiyon çağrılarında diziler daima "referansla aktarılır"

  • Fonksiyona geçirilen dizi adı dizinin ilk elemanına ait adresi temsil eder

  • Yani aslında fonksiyona bir "pointer" geçirilir

  • Dizi adı "bir radyoaktif maddenin dönüşümü" gibi bir işaretçiye dönüşür

  • "Array to pointer decay"


void task(int a[], size_t len)
{
        for (int i = 0; i < len; i++)
                a[i]++;
}

int main(void)
{
        size_t len = 1000000;
        int a[len];

        for (int i = 0; i < len; i++)
                a[i] = i;

        task(&a[0], len);

        for (int i = 0; i < 10; i++)
                printf("%d\n", a[i]);

        return 0;
}

void task(int *a, size_t len)
{
        for (int i = 0; i < len; i++)
                a[i]++;
}

int main(void)
{
        size_t len = 1000000;
        int a[len];

        for (int i = 0; i < len; i++)
                a[i] = i;

        task(&a[0], len);

        for (int i = 0; i < 10; i++)
                printf("%d\n", a[i]);

        return 0;
}

void task(int *a, size_t len)
{
        for (int i = 0; i < len; i++)
                a[i]++;
}

veya

void task(int *a, size_t len)
{
        for (int i = 0; i < len; i++)
                *(a + i)++;
}

p bir işaretçi ise

  • p[i] ile *(p + i) eşdeğerdir

  • * operatörü yerine indis parantezleri kullanabilirsiniz

  • p[0]*(p + 0)*p


strlen


/* strlen: return length of string s */
int strlen(char *s)
{
        int n;

        for (n = 0; *s != '\0'; s++)
                n++;

        return n;
}

/* strlen: return length of string s */
int strlen(char *s)
{
        char *p = s;

        while (*p != '\0')
                p++;

        return p - s;
}

Dizgi ilkleme

char amessage[] = "hello"; /* an array */
char *pmessage = "hello"; /* a pointer */
                      +---+---+---+---+---+---+
amessage   ---------> | h | e | l | l | o |\0 |
                      +---+---+---+---+---+---+

           +---+      +---+---+---+---+---+---+
pmessage   |   | ---> | h | e | l | l | o |\0 |
           +---+      +---+---+---+---+---+---+

strcpy

/* strcpy: copy t to s; array subscript version */
void strcpy(char *s, char *t)
{
        int i;

        i = 0;
        while ((s[i] = t[i]) != '\0')
                i++;
}

/* strcpy: copy t to s; pointer version */
void strcpy(char *s, char *t)
{
        while ((*s = *t) != '\0') {
                s++;
                t++;
        }
}

/* strcpy: copy t to s; pointer version 3 */
void strcpy(char *s, char *t)
{
        while (*s++ = *t++)
                ;
}

strcmp

/* strcmp: return <0 if s<t, 0 if s==t, >0 if s>t */
int strcmp(char *s, char *t)
{
        int i;

        for (i = 0; s[i] == t[i]; i++)
                if (s[i] == '\0')
                        return 0;

        return s[i] - t[i];
}

/* strcmp: return <0 if s<t, 0 if s==t, >0 if s>t */
int strcmp(char *s, char *t)
{
        for ( ; *s == *t; s++, t++)
                if (*s == '\0')
                        return 0;

        return *s - *t;
}

Çok boyutlu diziler

static char daytab[2][13] = {
        {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
        {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};

/* day_of_year: set day of year from month & day */
int day_of_year(int year, int month, int day)
{
        int i, leap;

        leap = year%4 == 0 && year%100 != 0 || year%400 == 0;
        for (i = 1; i < month; i++)
                day += daytab[leap][i];

        return day;
}

/* month_day: set month, day from day of year */
void month_day(int year, int yearday, int *pmonth, int *pday)
{
        int i, leap;

        leap = year%4 == 0 && year%100 != 0 || year%400 == 0;
        for (i = 1; yearday > daytab[leap][i]; i++)
                yearday -= daytab[leap][i];

        *pmonth = i;
        *pday = yearday;
}

/* month_name: return name of n-th month */
char *month_name(int n)
{
        static char *name[] = {
                "Illegal month",
                "January", "February", "March",
                "April", "May", "June",
                "July", "August", "September",
                "October", "November", "December"
        };

        return (n < 1 || n > 12) ? name[0] : name[n];
}

echo

#include <stdio.h>

/* echo command-line arguments; 1st version */
int main(int argc, char *argv[])
{
        int i;

        for (i = 1; i < argc; i++)
                printf("%s%s", argv[i], (i < argc-1) ? " " : "");
        printf("\n");

        return 0;
}

#include <stdio.h>

/* echo command-line arguments; 2nd version */
int main(int argc, char *argv[])
{
        while (--argc > 0)
                printf("%s%s", *++argv, (argc > 1) ? " " : "");
        printf("\n");

        return 0;
}

Matrisler

static int a[2][2] = {
        {1, 2},
        {3, 4},
};

static int b[2][2] = {
        {3, 5},
        {7, 9},
};

void display(int a[][], int m, int n)
{
        int i, j;

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

void multiply(int a[][], int b[][], int c[][], int m, int n)
{
        int i, j, k;

        for (i = 0; i < m; i++)
                for (j = 0; j < n; j++)
                        c[i][j] = 0;

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

Dizilerin işaretçilere dönüşmesi

#include <stdio.h>

int main(void)
{
        int numbers[] = { 3, 5, 8 };

        printf("Size of integer array: %d\n", sizeof(numbers));

        return 0;
}
  • Örnekte ekranda 12 görüntüleniyor (bu makinede)

  • 3 elemanlı bir dizide neden 12?


#include <stdio.h>

int main(void)
{
        int numbers[] = { 3, 5, 8 };

        printf("Size of integer: %d\n", sizeof(int));
        printf("Size of integer array: %d\n", sizeof(numbers));

        return 0;
}
  • Tamsayı boyutu 4 bayt, dizi boyutu 12 bayt

  • Şimdi anlamlı, 12/4 = 3 dizi boyutunu elde ettik

  • Gelin bunu bir makro yapalım


#include <stdio.h>

#define COUNT_OF(array) (sizeof(array)/sizeof(array[0]))

int main(void)
{
        int numbers[] = { 3, 5, 8 };

        printf("Count of array elements: %d\n", COUNT_OF(numbers));

        return 0;
}
  • COUNT_OF makrosu dizinin elemanlarının hangi tipte olduğuna bakmıyor

  • Dizi elemanlarının boyutunu örnekleme yaparak, örneğin ilk elemanın boytuna bakarak belirliyoruz


Güzel, fakat bu makro her kullanım koşulunda geçerli mi? Deneyelim...


#include <stdio.h>

#define COUNT_OF(array) (sizeof(array)/sizeof(array[0]))

void function(int numbers[])
{
        for (int i = 0; i < COUNT_OF(numbers); i++)
                printf("%d: %d\n", i, numbers[i]);
}

int main(void)
{
        int numbers[] = { 3, 5, 8 };

        function(numbers);

        return 0;
}

Bir sorun var, dizinin sadece ilk iki elemanı görüntülendi, neden?

  • Sorunu görebiliyor musunuz?

  • Sanki COUNT_OF fonknsiyon içindeyken 3 değil de 2 sonucunu verdi gibi, neden?


#include <stdio.h>

#define COUNT_OF(array) (sizeof(array)/sizeof(array[0]))

void function(int numbers[])
{
        printf("function: size of integer array: %d\n", sizeof(numbers));
        printf("function: count of array elements: %d\n", COUNT_OF(numbers));

        for (int i = 0; i < COUNT_OF(numbers); i++)
                printf("%d: %d\n", i, numbers[i]);
}

int main(void)
{
        int numbers[] = { 3, 5, 8 };

        printf("main: size of integer: %d\n", sizeof(int));
        printf("main: size of integer pointer: %d\n", sizeof(int *));
        printf("main: size of integer array: %d\n", sizeof(numbers));
        printf("main: count of array elements: %d\n", COUNT_OF(numbers));

        function(numbers);

        return 0;
}

  • Dizi değişkenleri fonksiyonlara geçirildiğinde birer işaretçiye dönüşür

  • Yani? Parametre aktarımı sırasında dizinin boyut bilgisi kaybolur

  • Dizinin tanımlandığı çağıran tarafta dizi boyutunu COUNT_OF ile öğrenebiliriz

  • Fakat çağrılan tarafta bu bilgi yok

  • Ne yapmalıyız? C'de fonksiyonlara dizi aktarırken çoğunlukla dizinin eleman sayısını da açıkça aktarırız


struct

  • C programlama dilinde özel veri tipleri (!) oluşturmanın yolu

  • Birbiriyle ilişkili bir dizi niteliği bir yapı ("structure") içinde bir araya getiriyoruz

  • Kompozit veri yapıları oluşturuyoruz

  • Dilin çok önemli bir özelliği

  • Nesne yönelimli programlamada bunun bir adım ileri götürülerek class'a dönüşeceğini de göreceksiniz



#include <stdio.h>

struct point {
        int x;
        int y;
};

int main(void)
{
        struct point a = { .x = 3, .y = 5 };

        printf("(%d,%d)\n", a.x, a.y);

        return 0;
}

(İşaretçi olmayan) struct yapılarındaki alanlara . operatörü ile erişiyoruz


#include <stdio.h>

struct point {
        int x;
        int y;
};

void dump(struct point a)
{
        printf("(%d,%d)\n", a.x, a.y);
}

int main(void)
{
        struct point a = { .x = 3, .y = 5 };

        dump(a);

        return 0;
}

Bu örnekte "değerle çağırma"nın maliyetini görebiliyor musunuz?

  • Her fonksiyon çağrısında iki tamsayıdan oluşan bir yapı kopyalanıyor

  • Belki bu örnek özelinde küçük, ama ya daha büyük struct'larda?

  • "Referansla çağırma" kullanalım, yani işaretçiler


#include <stdio.h>

struct point {
        int x;
        int y;
};

void dump(const struct point *p)
{
        printf("(%d,%d)\n", p->x, p->y);
}

int main(void)
{
        struct point a = { .x = 3, .y = 5 };

        dump(&a);

        return 0;
}

struct işaretçilerinde yapı içindeki alanlara -> operatörü ile erişiyoruz


Dinamik bellek yönetimi

        struct point a = { .x = 3, .y = 5 };
  • Bu örnekte statik olarak yer ayırıyoruz

  • main fonksiyonunun yığıtında (stack)

  • Bu alan çok dar ve ayrıca her fonksiyon çağrısında ayırma yapılıyor (optimizasyonlar bir yana)


  • Dinamik bellek yönetimi: belleği çalışma zamanında yönetmek

  • Program çalışırken işletim sisteminden bellek talep ediyoruz: malloc

  • "Dürüst vatandaşlık" gereği ayrılan kaynakla işimiz bittiğinde geri iade ediyoruz: free


#include <stdio.h>
#include <stdlib.h>

struct point {
        int x;
        int y;
};

struct point *point_create(int x, int y)
{
        struct point *this =
                (struct point *)malloc(sizeof(struct point));

        this->x = x;
        this->y = y;

        return this;
}

void point_destroy(struct point *this)
{
        free(this);
}

void point_print(const struct point *this)
{
        printf("(%d,%d)\n", this->x, this->y);
}

int main(void)
{
        struct point *p = point_create(3, 5);
        struct point *q = point_create(9, 7);

        point_print(p);
        point_print(q);

        point_destroy(p);
        point_destroy(q);

        return 0;
}

struct dizileri

#define COUNT_OF(array) (sizeof(array)/sizeof(array[0]))

void points_dump(const struct point points[], int nelem)
{
        for (int i = 0; i < nelem; i++)
                point_print(&points[i]);
}

int main(void)
{
        struct point points[] = {
                { .x = 13, .y = 5  },
                { .x = -3, .y = 9  },
                { .x = 9,  .y = 1  },
                { .x = 3,  .y = -2 },
                { .x = 0,  .y = 19 },
        };

        points_dump(points, COUNT_OF(points));

        return 0;
}

Sıralama

Gelin bu noktaları x koordinatlarına göre sıralayalım


int main(void)
{
        struct point points[] = {
                { .x = 13, .y = 5  },
                { .x = -3, .y = 9  },
                { .x = 9,  .y = 1  },
                { .x = 3,  .y = -2 },
                { .x = 0,  .y = 19 },
        };

        printf("Unsorted points\n---------------------\n");
        points_dump(points, COUNT_OF(points));

        points_sort(points, COUNT_OF(points));

        printf("\nSorted points\n---------------------\n");
        points_dump(points, COUNT_OF(points));

        return 0;
}

qsort

void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));
  • Standart C kitaplığında bulunan bir fonksiyon

  • "Quick Sort" sıralama algoritmasını gerçekliyor

  • (Gerekli kılavuz sayfaları kurulmuşsa) man qsort ile dokümanlara erişebilirsiniz


İlk bakışta anlamakta güçlük çekeceğiniz iki husus var

  • void *? void tipinde bir işaretçi ne demek?

  • int (*compar)(const void *, const void *)? Bu bir fonksiyon işaretçisi?

  • Fonksiyona işaret eden bir işaretçi, yani fonksiyon da bir tür veri?

  • İkisini birlikte anlatmaya çalışalım


int compare_by_x(const void *a, const void *b)
{
        struct point *p = (struct point *)a;
        struct point *q = (struct point *)b;

        if (p->x > q->x)
                return 1;
        else if (p->x < q->x)
                return -1;
        else
                return 0;
}

void points_sort(struct point points[], int nelem)
{
        qsort(points, nelem, sizeof(struct point), compare_by_x);
}

void *

void * işaretçilerinin yokluğunda qsort nasıl yazılırdı, düşünelim?

  • C programlama dilinde "jenerik programlama" yapmakta kullanılan en önemli özellik

  • Bir süreliğine işaret edilen verinin tipiyle ilgilenmediğimiz işaretçiler için kullanılıyor

  • void * işaretçileri bir noktada "cast" etmek zorundayız

  • void * işaretçilerle "aritmetik" yapamayız, neden? İşaret edilen verinin tipi bilinmiyor

  • Genel olarak derleyicinin karşımıza çıkaracağı "tip denetimi" kısıtlarını aşmak için kullanıyoruz