C Programlama Temelleri


Algoritma kurmadan bellek yönetimine; işaretçiler, dosya işlemleri ve önişlemciden translation unit, linkage, callback, hizalama, aliasing ve ABI sınırına uzanan kapsamlı C programlama ders notları.
Konu akışı 2013-2014 dönemindeki temel programlama ve sistem programlama ders çizgisini izler. Dil standardındaki sonraki değişiklikler yalnız konuyu açıklığa kavuşturduğu yerlerde belirtilir; C'nin temel semantiği daha yeni kolaylıklarla geriye dönük olarak yeniden tanımlanmaz.
Ünite 1: Programlamaya Giriş
1.1. 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ğerlendirBir 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.
1.2. 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ürBu algoritma C veya C++'tan bağımsızdır. Programlama dili yalnız algoritmanın gerçeklenme aracıdır.
1.3. 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.
1.4. Atama
Atama:
x <- 5veya 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.
1.5. 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" yazC ve C++ karşılığı:
if (temperature < 0) {
/* işlem */
} else {
/* diğer işlem */
}1.6. Döngü
Belirli işlemlerin tekrarlanması döngüyle ifade edilir.
Koşul başta denetleniyorsa:
while koşul:
işlemBelirli sayıda tekrar için:
for başlangıç; koşul; güncelleme:
işlemKoşulun döngü sonunda denetlendiği yapı:
do:
işlem
while koşulC ve C++ bu üç temel modeli de destekler.
1.7. 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.
1.8. 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.
1.9. 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ı
2.1. 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.
2.2. Derleme Süreci
Basitleştirilmiş süreç:
Kaynak kod
↓
Önişleme
↓
Derleme
↓
Assembly
↓
Nesne kodu
↓
Bağlama
↓
Yürütülebilir programGerçek araç zincirinde bazı aşamalar birleştirilebilir.
Örneğin:
cc program.c -o programkomutu önişleme, derleme, assembly ve bağlama adımlarını tek komutla yürütebilir.
2.3. 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.
2.4. 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çıklamaAçıklama kodun ne yaptığını tekrar etmek yerine, gerektiğinde neden o şekilde yazıldığını açıklamalıdır.
2.5. Temel Veri Türleri
C'nin temel türlerinden bazıları:
char
short
int
long
long long
float
double
long double
_Bool / boolC23 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_tgibi türler, gerçekleştirim tarafından sağlanıyorsa kullanılabilir.
2.6. Sizeof
Bir türün veya nesnenin bayt cinsinden boyutu:
sizeof(int)
sizeof variableile alınır.
sizeof(char) tanım gereği 1'dir. Ancak bir C baytının bit sayısı:
CHAR_BITile belirlenir ve standart yalnız bunun en az 8 olmasını garanti eder.
2.7. 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.
2.8. 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.
2.9. 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.
2.10. 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.
2.11. 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.
2.12. 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.
2.13. 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
Bir koşul veya döngü tasarlanırken önce giriş uzayı, döngü değişmezi ve sonlanma koşulu yazılmalıdır. Döngü gövdesinin değişmezi koruduğu ve sonlanmayı sağlayan bir ilerleme ölçüsünü değiştirdiği gösterilirse algoritmanın doğruluğu daha güçlü temellendirilir. Boş dizi, tek öğe ve sınır değerleri gibi durumlar ayrı test edilir. C dilinde indis aralıkları ve tamsayı taşması denetlenmeden yalnız örnek çıktıya dayanarak doğruluk kararı verilmemelidir.
3.1. Aritmetik İşleçler
+ toplama
- çıkarma
* çarpma
/ bölme
% kalanTamsayı bölmesi kesirli kısmı atar:
7 / 2sonucu:
3olur.
Kayan noktalı sonuç için operandlardan en az biri kayan nokta olmalıdır:
7.0 / 2.0sonucu:
3.5olur.
3.2. 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.
3.3. Bileşik Atama
x += 5;
x -= 2;
x *= 3;
x /= 4;genellikle:
x = x + 5;gibi ifadelerin kısa biçimidir.
3.4. Karşılaştırma İşleçleri
== eşit
!= eşit değil
< küçük
> büyük
<= küçük veya eşit
>= büyük veya eşitAtama ile karşılaştırma farklıdır:
x = 5;atamadır.
x == 5karşılaştırmadır.
3.5. Mantıksal İşleçler
&& VE
|| VEYA
! DEĞİLC'de && ve || kısa devre değerlendirmesi yapar.
Örneğin:
if (p != NULL && *p > 0) {
...
}ifadesinde p == NULL ise *p değerlendirilmez.
3.6. Bit İşleçleri
& bit VE
| bit VEYA
^ bit XOR
~ bit tümleme
<< sola kaydırma
>> sağa kaydırmaMantı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.
3.7. İf
if (score >= 50) {
passed = 1;
}else ile:
if (score >= 50) {
passed = 1;
} else {
passed = 0;
}3.8. İç İçe İf
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.
3.9. Koşul İş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.
3.10. 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.
3.11. For
for (int i = 0; i < 10; ++i) {
printf("%d\n", i);
}Üç bölüm:
başlangıç
koşul
güncellemeişlevini görür.
3.12. While
while (condition) {
...
}Koşul ilk yinelemeden önce sınanır.
3.13. do-while
do {
...
} while (condition);Gövde en az bir kez yürütülür.
3.14. Break
En yakın döngü veya switch yapısından çıkar:
if (found) {
break;
}3.15. Continue
Mevcut yinelemenin kalanını atlar ve döngünün sonraki adımına geçer.
3.16. 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
4.1. Tek Boyutlu Diziler
int values[5];beş int elemanı için ardışık saklama alanı oluşturur.
İndisler:
0, 1, 2, 3, 4aralığındadır.
C sınır denetimi yapmaz. Dizinin dışına erişmek tanımsız davranıştır.
4.2. 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.
4.3. 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.
4.4. Ç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.
4.5. 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];
}
}4.6. 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];
}
}
}4.7. 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.
4.8. 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.
4.9. 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.
4.10. 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.
4.11. 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.
4.12. Karşılaştırma
strcmp(a, b)sonucu:
- negatif:
a < b, - sıfır: eşit,
- pozitif:
a > b
anlamındadır.
Metinler:
a == bile 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
Bir işaretçi işlemi incelenirken adresin hangi nesneye ait olduğu, nesnenin yaşam süresi ve erişilebilir eleman aralığı belirlenir. Gösterici aritmetiğinin yalnız geçerli nesne sınırları içinde kullanılması gerekir; tahsis edilmiş belleğin serbest bırakılmasından sonra aynı adresin taşınması geçerli erişim hakkı sağlamaz. Bellek hatasını ararken önce sahiplik ve yaşam süresi, ardından taşma ve hizalama koşulları denetlenmelidir. Bu sıra, yalnız çökmeye yol açan örnekleri değil, sessiz veri bozulmasını da hedefler.
5.1. İş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.
5.2. Adres Alma
&valuenesnenin adresini verir.
5.3. Dolaylı Erişim
*pişaret edilen nesneye erişir.
*p = 20;sonrasında:
value == 20olur.
5.4. Null İş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:
*ptanımsız davranıştır.
5.5. İşaretçi Aritmetiği
Dizi elemanını gösteren işaretçide:
p + 1adres 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ı:
30olur.
İşaretçi aritmetiği yalnız aynı dizi nesnesi ve bir-past-the-end sınırları içinde tanımlı davranışa sahiptir.
5.6. Dizi ve İşaretçi İlişkisi
Birçok ifadede:
arrayilk elemanın işaretçisine dönüşür:
&array[0]Ancak dizi ve işaretçi aynı tür değildir.
Örneğin:
sizeof arraydizinin tamamının boyutunu verebilirken:
sizeof pointeryalnız işaretçinin boyutunu verir.
5.7. İş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]);
}5.8. İş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.
5.9. İşaretçiye İşaret Eden İş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.
5.10. 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.
5.11. 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.
5.12. 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;
}5.13. 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=undefinedgibi seçenekler geliştirme sırasında yararlı olabilir.
5.14. C'de Tanımlı Davranış, Ömür ve Sınırların Birlikte Yönetimi
C'nin düşük seviye gücü, programcıya bellek yerleşimi ve veri hareketi üzerinde geniş denetim verir. Aynı özellik, dilin tanımlı davranış sınırlarının bilinmesini zorunlu kılar. Doğru çalışan bir C programında yalnız algoritma değil; nesne ömrü, sınırlar, hizalama, tamsayı aralığı ve pointer geçerliliği de kontratın parçasıdır.
5.15. Dizi ve Pointer Aynı Şey Değildir
Bir dizi nesnesi bellekte art arda elemanlar içerir. Birçok ifade bağlamında dizi adı ilk elemana pointer'a dönüşür; fakat dizinin kendisi pointer nesnesi değildir. Bu ayrım özellikle sizeof, &array, fonksiyon parametreleri ve çok boyutlu dizilerde önemlidir.
int a[10];
size_t n = sizeof a / sizeof a[0];Bu hesap gerçek dizi tanımının bulunduğu kapsamda çalışır. Dizi bir fonksiyon parametresi olarak yazıldığında parametre pointer olarak ayarlanır; aynı sizeof ifadesi artık eleman sayısını vermez. Güvenli arayüz, pointer ile uzunluğu birlikte taşır.
5.16. Nesne Ömrü ve Dangling Pointer
Otomatik ömürlü yerel değişken fonksiyon/blok ömrü sona erdiğinde artık geçerli bir nesne değildir. Ona işaret eden adresi döndürmek pointer değerini saklasa bile işaret edilen nesnenin ömrünü uzatmaz. Benzer biçimde free() sonrasında aynı adrese erişmek use-after-free davranışıdır.
Dinamik bellek için sahiplik kuralı açık olmalıdır: belleği kim ayırır, kim serbest bırakır, hata yolunda serbest bırakma nerede yapılır? Aynı alanın iki kez serbest bırakılması ve hiç serbest bırakılmaması farklı hata sınıflarıdır.
5.17. Tamsayı ve Boyut Hesapları
size_t bellek boyutları ve nesne uzunlukları için kullanılan işaretsiz türdür. Harici girdiden gelen iki uzunluğun count * element_size biçiminde çarpılması taşabilir; malloc() küçük bir alan ayırsa bile sonraki döngü mantığı büyük alan varmış gibi davranabilir. Bellek ayırmadan önce aritmetik taşma kontrol edilmelidir.
İşaretli tamsayı taşması C'de genel olarak tanımsız davranıştır. İşaretsiz tamsayı aritmetiği modüler davranış gösterir; ancak bu, güvenlik açısından istenen sonucu otomatik olarak sağlamaz.
5.18. İfade Yan Etkileri ve Değerlendirme
Bir nesnenin aynı ifade içinde birden fazla kez değiştirilmesine dayanan kod, dil kurallarına bağlı olarak belirsiz/tanımsız davranış üretebilir. Kısa fakat yan etkisi yoğun ifadeler yerine sıra bağımlılığını açık statement'lara ayırmak daha güvenilir ve taşınabilirdir.
5.19. const, volatile Ve Atomiklik
const erişim yolundan değişiklik yapılmamasını ifade eder; veriyi otomatik olarak ROM'a taşımaz veya başka alias'ların değiştirmesini engellemez. volatile, belirli bellek erişimlerinin optimizer tarafından göz ardı edilmemesi gereken donanım/ortam etkileşimlerinde kullanılır; iş parçacıkları arasında atomiklik veya synchronization sağlamaz. Çok iş parçacıklı eşzamanlı erişimde C'nin atomik ve synchronization mekanizmaları gerekir.
5.20. Güvenli C Arayüzü için Kısa Ölçüt
Bir fonksiyon pointer alıyorsa şu sorular açık olmalıdır: NULL kabul edilir mi, kaç eleman geçerlidir, yazma yetkisi var mı, buffer kime aittir, fonksiyon pointer'ı saklıyor mu, hata durumunda kısmi çıktı oluşur mu? Bu soruların imzada veya dokümantasyonda cevapsız kalması bellek hatalarının temel nedenlerinden biridir.
5.21. Tanımsız Davranış, Taşınabilirlik ve Sınır Kontrolleri
C'de derleyicinin kabul ettiği her programın davranışı tanımlı değildir. Dizi sınırının aşılması, yaşamı bitmiş nesneye erişim, bazı signed integer taşmaları ve geçersiz pointer kullanımları tanımsız davranış üretebilir. Böyle bir programın "benim makinemde çalışması" doğruluk kanıtı değildir.
Taşınabilir kod, tür genişlikleri ve hizalama hakkında gereksiz varsayım kurmaz. Dosya/protokol biçiminde kesin genişlik gerektiğinde stdint.h türleri, sınır kontrolünde sizeof, dönüş değeri denetiminde ise API sözleşmesi esas alınır. Derleyici uyarılarını yükseltmek ve sanitizers gibi dinamik denetleyiciler kullanmak, bellek hatalarını erken bulmada etkilidir.
C'de güvenli yaklaşım, pointer aritmetiğini "normal programlama stili" değil kontrollü bir düşük seviye araç olarak görmektir. Bir işlevin hangi tamponu okuyabildiği, hangi uzunluğu beklediği ve sahipliği değiştirip değiştirmediği imzada veya açık sözleşmede görülebilmelidir.
5.22. Dizi, İşaretçi, Ömür ve Boyut Ayrımlarını Birlikte Okumak
C dilinde dizi ve işaretçi birçok ifadede benzer görünür, ancak aynı kavram değildir. int a[10] gerçek bir dizi nesnesi tanımlar ve sizeof(a) aynı kapsamda dizinin toplam byte sayısını verir. Dizi adı çoğu ifadede ilk elemana işaret eden bir değere dönüşür; bir fonksiyon parametresindeki int a[] da gerçekte işaretçi parametresi olarak ele alınır. Bu nedenle aynı sizeof ifadesinin fonksiyon içinde farklı sonuç vermesi dilin çelişkisi değil, türün artık dizi olmamasının sonucudur.
İşaretçinin geçerli olması yalnız adresin sıfırdan farklı olmasına bağlı değildir. İşaret edilen nesnenin ömrü sürmeli, adres uygun hizalamaya sahip olmalı ve erişim nesnenin sınırları içinde kalmalıdır. Yerel bir değişkenin adresini fonksiyondan döndürmek, fonksiyon tamamlandığında ömrü biten bir nesneye işaretçi bırakır. malloc ile alınan alan ise free edilene kadar ayrı bir ömre sahiptir; serbest bırakıldıktan sonra aynı adres değerinin değişmemiş görünmesi onu tekrar geçerli yapmaz.
C'de fonksiyon argümanları değer yoluyla aktarılır. Bir değişkeni fonksiyon içinde değiştirmek için onun adresi ayrıca aktarılır:
void increment(int *p) {
if (p != NULL) {
++*p;
}
}Burada işaretçinin kendisi yine değer olarak kopyalanır; ancak kopyalanan değer aynı nesnenin adresini gösterdiği için nesne değiştirilebilir.
Bu ayrımlar sınav sorularındaki sözdizimi tuzaklarından daha geniş bir anlama sahiptir. Dizi sınırı, nesne ömrü, sahiplik ve tanımsız davranış aynı bellek modelinin parçalarıdır. C kodunu güvenilir okumak, yalnız “bu ifade ne yapar?” değil, “bu erişimin dayandığı nesne hâlâ var mı ve erişim hangi sınır içinde tanımlı?” sorularını da sormayı gerektirir.
5.23. Tampon Sınırı ve Anahtar Malzemesi Bulunan Bellek
C'de bir karakter tamponunun ayrılan boyutu ile kopyalanan verinin uzunluğu ayrı değerlerdir. Veri alma fonksiyonu okunabilecek en fazla uzunluğu bilmiyorsa, sınır aşımı komşu belleği değiştirebilir; bu durum kriptografik anahtarlar dahil hassas veriyi riske sokar. Güvenli işleyişte önce sınır size_t düzeyinde doğrulanır, taşmaya açık toplama/çarpma kontrol edilir, çıktı alanının sonlandırma baytı gereksinimi belirlenir ve yalnız tanımlı uzunluk kadar kopyalama yapılır.
Bütünlük etiketi bulunan şifreli veri çözülmeden önce boyut ve biçim denetimine tabi olmalıdır; geçersiz etiket sonrası açık metin kullanılmaz. Üzerine sıfır yazmanın derleyici tarafından elenebileceği veya kopyaların bellekte kalabileceği dikkate alınır. Bu ünitenin temel amacı sınır kanıtı ve bellek güvenliğidir; anahtar türetme ve AEAD ayrıntıları Kriptoloji dersinde ele alınır.
5.24. İşaretçi Aritmetiğinde Nesne Sınırı ve Tanımsız Davranış
C dilinde bir işaretçinin sayısal adres taşıması, bu adres üzerinde istenen aritmetiğin tanımlı olduğu anlamına gelmez. Bir dizi elemanına işaret eden p üzerinde aynı dizinin elemanları ve son elemandan bir sonraki konum için izin verilen işaretçi aritmetiği, farklı nesneler arasında keyfî atlama yapma hakkı vermez. Son konumu gösteren işaretçi oluşturulabilir fakat bu konumdan değer okunamaz. Bu ayrım, özellikle tampon sınırları ve adres karşılaştırmaları için kritiktir.
sizeof ile hesaplanan bayt uzunluğu ve dizinin öğe sayısı farklı niceliklerdir. int a[8] ifadesinin tanımlandığı kapsamda öğe sayısı sizeof a / sizeof a[0] ile elde edilebilir; ancak fonksiyon parametresine aktarılan aynı ifade çoğu bağlamda işaretçiye dönüşür ve sizeof artık dizinin toplam boyutunu vermez. API, uzunluğu bağımsız bir parametreyle taşımalı ve size_t türündeki değerlerle taşma koşullarını kontrol etmelidir. n * sizeof(T) çarpımı tahsisattan önce SIZE_MAX / sizeof(T) sınırıyla denetlenebilir.
Taşmış indis, serbest bırakılmış nesneye erişim ve tanımsız işaretçi farkı farklı hata sınıflarıdır. Dinamik analiz araçları bazı yürütmelerde bu hataları yakalayabilir, fakat testin başarılı bitmesi C soyut makinesindeki davranışı tanımlı hâle getirmez. Doğru API, tampon sahipliği, uzunluk ve yaşam süresini açıkça belirtir; C'nin hız avantajı bu sözleşmelerin ihmal edilmesini haklı çıkarmaz.
5.25. Tanımsız Davranış, Sahiplik ve Statik Analiz Sınırları
C dilinde nesnenin yaşam süresi sona erdiğinde işaretçinin adres değeri görünürde aynı kalsa bile bu nesne üzerinden erişim tanımlı değildir. Dizi sınırı, işaretçi aritmetiği, işaretli tamsayı taşması ve başlatılmamış değer kullanımı farklı tanımsız davranış sınıflarıdır. Derleyicinin optimizasyonu, programın dil standardına uygun çalıştığı varsayımıyla gerçekleştirildiğinden, hatalı kodun gözlenen davranışı debug ve optimize derlemelerde farklılaşabilir.
Kaynak sahipliği için her tahsisin serbest bırakılma sorumlusu, başarısız dönüşte kaynakların temizlenme sırası ve alias oluşturan referansların geçerlilik alanı belirlenir. Statik analiz ve MISRA C rehberliği hata olasılıklarını azaltır, ancak tek araçla tam güvence elde edilmez. Farklı optimizasyon seviyelerinde uyarılar, runtime sanitizers ve sınır değer testleri birlikte yürütülür.
Ünite 6: C Fonksiyonları
6.1. 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);6.2. 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.
6.3. 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.
6.4. Çı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.
6.5. 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.
6.6. 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.
6.7. 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.
6.8. 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 iş parçacığı-safety ve yeniden giriş açısından ayrıca değerlendirilmelidir.
6.9. Ö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ı
7.1. 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;7.2. 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.
7.3. 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.
7.4. Yapı İşaretçisi
struct Student *p = &s;alan:
p->idile alınır.
Bu:
(*p).idifadesinin kısa biçimidir.
7.5. 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.
7.6. Dizi İçinde Struct
struct Student students[100];yapısında her eleman bir Student kaydıdır.
7.7. 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.
7.8. 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;
};7.9. Bit Alanları
struct Flags {
unsigned ready : 1;
unsigned mode : 3;
};belirli bit genişlikli alanlar tanımlar.
Ancak:
- alan yerleşimi,
- sıralama,
- dolgulama
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
8.1. Dosya Açma
FILE *f = fopen("data.txt", "r");Başarı denetlenmelidir:
if (f == NULL) {
perror("fopen");
return 1;
}8.2. 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
abgibi biçimler kullanılır.
Özellikle Windows gibi metin ve ikili kip ayrımı bulunan sistemlerde doğru kip önemlidir.
8.3. 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.
8.4. 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);8.5. 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.
8.6. 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.
8.7. İ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.
8.8. 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:
- dolgulama,
- 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.
8.9. 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.
8.10. 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.
8.11. 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)8.12. Matematik Kütüphanesi
#include <math.h>
sqrt
pow
sin
cos
tan
log
log10
exp
fabsişlevleri yaygındır.
8.13. 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
9.1. Ö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ı.
9.2. Nesne Benzeri Makro
#define BUFFER_SIZE 4096Makro metinsel yer değiştirmedir. Türü yoktur.
Bir sabit nesne veya enum daha uygun olabiliyorsa makro kullanmak zorunlu değildir.
9.3. 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;
}9.4. # Ve ##
Stringification:
#define TO_STRING(x) #xToken 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.
9.5. #Undef
#undef NAMEdaha önce tanımlanmış makroyu kaldırır.
9.6. Koşullu Derleme
#ifdef DEBUG
...
#endifve:
#if FEATURE_LEVEL >= 2
...
#elif FEATURE_LEVEL == 1
...
#else
...
#endifkullanılabilir.
9.7. Başlık Guard
#ifndef MY_HEADER_H
#define MY_HEADER_H
/* bildirimler */
#endifaynı 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.
Ünite 10: Derleme Birimleri, Bağlantı ve Çağrı Sınırı
Bir C programı yalnız kaynak dosyalarının toplamı değildir. Her .c dosyası önişlemden sonra ayrı bir translation unit olarak derlenir; daha sonra nesne dosyaları bağlayıcı tarafından tek program veya kitaplık hâline getirilir. Bu ayrım, görünürlük ile ömrü birbirine karıştırmamak açısından önemlidir.
10.1. İç ve Dış Bağlantı
Dosya kapsamındaki bir adın başka translation unit'lerden görülüp görülemeyeceği linkage ile belirlenir. static dosya kapsamındaki bir nesne veya işlevi ilgili translation unit ile sınırlar. extern ise başka bir yerde tanımlanan bir nesne veya işlev için bildirim yapar.
static int local_counter;
extern int shared_counter;Bu iki anahtar sözcük değişkenin yalnız ömrünü değil, programın bağlantı sözleşmesini de etkiler. Başlık dosyasına tanım koymak ile bildirim koymak arasındaki fark özellikle çok dosyalı projelerde önemlidir.
C dilinde ayrı derleme birimleri üzerinden yapılan bağlantı, kaynak dosyaların birbirinin iç ayrıntılarını görmesinden farklıdır. static ile dosya düzeyinde sınırlanan bir simgenin bağlantı görünürlüğü, başlıkta dışarıya sunulan işlev bildiriminden ayrılır. Aynı genel simge adının iki derleme biriminde farklı nesneleri temsil etmesi bağlantı hatasına veya beklenmeyen davranışa yol açabilir. Bir bileşenin dış arayüzünü başlıkta, özel durumunu kaynak dosyada tutmak; sembol tablosu ve bağlantı sonucunu kontrol etmek; taşınabilir derlemeyi güçlendirir.
10.2. İşlev İşaretçileri ve Geri Çağrılar
İşlev işaretçisi, yürütülebilir davranışın veri gibi seçilmesine imkân verir:
int apply(int x, int (*op)(int)) {
return op(x);
}Bu yapı karşılaştırıcı, callback, durum makinesi ve donanım soyutlama katmanı gibi düşük maliyetli esneklik gereken yerlerde kullanılır. Ancak işlev işaretçisinin türü çağrı sözleşmesiyle uyumlu olmalıdır; yanlış tür üzerinden çağrı tanımsız davranışa yol açabilir.
10.3. Hizalama, Takma Adlama ve Nesne Temsili
Bir nesnenin adresi, türünün gerektirdiği hizalamayı sağlamalıdır. Yanlış hizalanmış erişim bazı mimarilerde yavaş çalışabilir, bazılarında ise hata üretebilir. Benzer biçimde derleyici, farklı türdeki işaretçilerin aynı nesneyi göstermediğini belirli kurallar altında varsayabilir. Strict aliasing kurallarını ihlal eden kod kaynak düzeyinde makul görünse bile optimizasyon sonrasında beklenmeyen davranış gösterebilir.
Ham byte görünümüne ihtiyaç duyulduğunda unsigned char tabanlı erişim C'nin nesne gösterimini incelemek için tanımlı yollardan biridir. Bellekteki byte sırası ile programdaki sayısal değer aynı kavram değildir; endianness ve hizalama ayrı özelliklerdir.
10.4. ABI Sınırı
C standardı kaynak dilin davranışını tanımlar; ikili uyumluluğun tamamını tanımlamaz. Parametrelerin hangi yazmaçlardan geçirileceği, stack frame düzeni, sembol adlandırma ve veri hizalama ayrıntıları platformun Application Binary Interface (ABI) sözleşmesine bağlıdır.
Bu ayrım şu zinciri görünür kılar:
C kaynak kodu
↓
translation unit
↓
nesne dosyası
↓
ABI ve semboller
↓
bağlama
↓
çalıştırılabilir programC kodunu düşük seviyede güvenilir biçimde değerlendirmek için kaynak dili, derleyici varsayımları ve platform ABI'si aynı problem içinde fakat ayrı katmanlar olarak ele alınmalıdır.
10.5. C Kodunda Doğrulanabilirlik
C'de “çalışıyor” ile “davranışı tanımlı” aynı şey değildir. Bir örneğin doğruluğu derleyicinin kodu kabul etmesiyle değil; C standardındaki nesne ömrü, tür, sınır ve değerlendirme kurallarıyla uyumuyla değerlendirilir.
Taşınabilirlik için en az iki farklı derleyici ve yüksek uyarı düzeyi yararlıdır. Bellek hataları AddressSanitizer/UndefinedBehaviorSanitizer benzeri araçlarla aranabilir; ancak bu araçların hata bulmaması programın tüm girdiler için doğru olduğunu kanıtlamaz. Sınır koşulları ve sahiplik sözleşmesi ayrıca incelenmelidir.
Düşük seviyeli örneklerde platform varsayımları açık tutulmalıdır: tamsayı genişliği, endian düzeni, hizalama, işletim sistemi API'si ve derleyici uzantıları. Böylece standart C davranışı ile platforma özgü teknik ayrıntı birbirinden ayrılır.
10.6. C Kodunda Ölçülebilir Performans
C kodunun performansını ölçerken derleyicinin ürettiği makine kodu ile programın gözlenen davranışı birlikte değerlendirilir. volatile, sıradan bellek erişimlerini senkronizasyon işlemi yapmaz; döngüden kaldırılan sonuçlar ise bir mikrobenchmarkın gerçekte iş yapmamasına yol açabilir. Optimize edilmiş ve edilmemiş derlemelerde disassembly, branch miss, cache miss ve tahsis davranışı karşılaştırılmalı; C standardının tanımlamadığı davranışlara dayanan hızlanmalar geçerli sonuç sayılmamalıdır.
- Mikrobenchmark sırasında dead-code elimination ve ısınma etkilerinin denetlenmesi.
- Bellek yerleşimi ve erişim örüntüsünün CPU zamanından ayrı ölçülmesi.
- Undefined behavior içeren bir programın performans sonucunun güvenilir kabul edilmemesi.
10.7. IEEE 754 Davranışı ve Hızlı Matematik Optimizasyonları
Kayan noktalı işlemlerde birleşme özelliği genel olarak geçerli değildir: (a+b)+c ile a+(b+c) farklı yuvarlama sonuçları üretebilir. -ffast-math gibi derleyici seçenekleri NaN, sonsuzluk, signed zero ve işlem sırası hakkındaki standart varsayımları gevşetebilir. Bu nedenle belirli bir girdide daha hızlı çalışan sayısal kod, algoritmanın kabul ettiği hata sınırına göre ayrıca doğrulanmalıdır.
Bilimsel hesaplama, DSP veya güvenlik kararları için testler yalnız rastgele normal sayıları değil, sıfır çevresi, alt-normal değerler, taşmalar ve özel IEEE 754 değerlerini de kapsar. Hızlanma iddiası hedef işlemci, derleyici sürümü, derleme bayrakları ve toleransla birlikte raporlanır; bit düzeyinde determinizm gereken işlem yollarında yeniden sıralamaya izin veren optimizasyonlar ayrıca sınırlandırılır.
Kaynakça
- Brendan Gregg. Systems Performance: Enterprise and the Cloud. Pearson Education, 2014.
- Brian W. Kernighan; Dennis M. Ritchie. The C Programming Language. Prentice Hall, 1988.
- ISO/IEC. ISO/IEC 9899:2011 Programming Languages - C. International Organization for Standardization, 2011. Kaynak
- ISO/IEC. ISO/IEC 9899:2024 Information technology — Programming languages — C. International Organization for Standardization, 2024. Kaynak
- K. N. King. C Programming: A Modern Approach. W. W. Norton, 2008.
- Mor Harchol-Balter. Performance Modeling and Design of Computer Systems: Queueing Theory in Action. Cambridge University Press, 2013.
- NIST. SP 800-38D: GCM and GMAC, 2007. https://csrc.nist.gov/pubs/sp/800/38/d/final