C’de tanımladığımız ve ismi olan değişken, fonksiyon vb. her şeyin görünür olduğu bir alan vardır. Yani her ismi her yerde kullanmamız mümkün değildir. İşte C’de bildirilen bir ismin görünür olduğu ve diğer nesneler tarafından doğrudan erişilebildiği bu alana Faaliyet Alanı (Scope) adı verilmektedir. Bu kavram Türkçe’ye Bilinirlik Alanı veya Kapsam olarak da çevrilebilir. Faaliyet alanı kavramı sadece C diline özel bir kavram değildir. Diğer programlama dillerinde de bulunan bir kavramdır. C dilinde bu kavram derleme zamanına ilişkin bir kavramdır. Yani aslında faaliyet alanı derleme esnasında değişkenin ulaşılabilirliğini belirlemektedir.
Çoğu durumda bir değişkene veya fonksiyona her yerden erişebilmek programcı açısından oldukça risklidir ve birçok hataya neden olabilir. Örneğin aynı ismi programın başka bir alanında başka bir amaç için bildirdiğinizde ya bunu yapamayacaksınız ya da yaptığınızda ilginç sonuçlar ile karşılaşacaksınızdır. İşte bu gibi durumlardan kaçınmak için programlama dillerinde faaliyet alanı kavramı vardır. Bu kavram iki sınıfa ayrılabilir. Bunlar:
- Statik Faaliyet Alanı (Static Scoping)
- Dinamik Faaliyet Alanı (Dynamic Scoping)
Statik Faaliyet Alanı (Static Scoping) veya diğer bir adıyla Sözcüksel Faaliyet Alanı (Lexical Scoping) kavramında, her bir blok yeni bir faaliyet alanı oluşturur ve bu faaliyet alanında bildirilen isimlere dış faaliyet alanlarından erişim olamaz. Ancak bu faaliyet alanında yer alan isimler onu kapsayan bir diğer dış faaliyet alanındaki isimlere erişebilir. C, C++, Java ve günümüzdeki pek çok programlama dili bu yaklaşımı kullanır.
Dinamik Faaliyet Alanı (Dynamic Scoping) ise bir isme herhangi bir faaliyet alanında en son bildirildiği yere göre değer atanır. Örneğin 3 farklı fonksiyonun içinde değer atanan aynı isimli değişkenleri ele aldığımızda, bu değişkenin değeri en son çağrılan fonksiyona göre belirlenecektir. Dinamik faaliyet alanı çok fazla yerde karşımıza çıkan bir kavram olmamakla birlikte, kodun yazılmasını ve hatalarının ayıklanmasını oldukça zorlaştırır. Logo, Emacs Lisp, LaTex ve Bash gibi diller bu faaliyet alanı türünü kullanmaktadırlar. Eğer faaliyet alanı türleri ile ilgili açıklamalar karmaşık geldiyse aşağıdaki görseli inceleyebilirsiniz:
C dilinin statik faaliyet alanını kullandığını ve bunun tam olarak ne olduğunu açıkladıktan sonra, artık C dilindeki faaliyet alanı kurallarına geçebiliriz. C dilinde faaliyet alanlarını 4 kategori altında inceleyebiliriz. Bunları bir tabloda özetleyecek olursak:
| Faaliyet Alanı Kategorisi | Açıklama |
|---|---|
| Blok Faaliyet Alanı (Block Scope) | Bir ismin, onun bir blok içinde bildirildiği yerden bloğun sonlandığı “}” blok parantezinin bulunduğu yere kadar bilinmesidir. “{” ve “}” blok parantezleri arasında bildirilen isimler için geçerlidir. Fonksiyon tanımında yer alan parametre değişkenleri de bu tür faaliyet alanına sahiptirler. |
| Dosya Faaliyet Alanı (File Scope) | Bir ismin, onun kaynak kodda bildirildiği yerden kaynak dosyanın sonuna kadar olan kısımda bilinmesidir. Yani o isimden sonra bildirilen veya tanımlanan tüm fonksiyonlar bu ismi görebilir. Sadece bütün fonksiyonların dışında tanımlanan isimler için geçerlidir. |
| Fonksiyon Prototipi Faaliyet Alanı (Function Prototype Scope) | Fonksiyon prototipinde yer alan isimler sadece o prototip tarafından bilinmesini ifade eder. |
| Fonksiyon Faaliyet Alanı (Function Scope) | Fonksiyon faaliyet alanları fonksiyonun başladığı blok parantezinden itibaren başlayıp fonksiyonun bittiği blok parantezine kadarki alanı kapsarlar. Bu faaliyet alanı türü sadece etiketler (labels) için geçerlidir. |
Öncelikle blok faaliyet alanı ile başlayalım. Blok Faaliyet Alanı (Block Scope), aslında “{” ve “}” parantezleri arasında var olan faaliyet alanıdır. C’de bloklar iç içe olabilirler. Bir bloğun içinde bildirilen bir isme içteki bloklar erişim sağlayabilirler. Ancak içteki blokta bildirilen bir isme dıştaki bloklardan erişim sağlanamaz. Şimdi bu faaliyet alanı kategorisini ve iç içe bloklardaki durumunu bir örnekle anlatmaya çalışalım:
void func(int b)
{
int a = 5;
b = 4;
{
int b = 9;
printf("%d\n", b); // Output: 9
{
int b = 10;
int c = 50;
printf("%d\n", a); // Output: 5
printf("%d\n", b); // Output: 10
}
printf("%d\n", b); // Output: 9
a = 3;
printf("%d\n", c); // ERROR!
}
printf("%d\n", a); // Output: 3
}
Yukarıdaki fonksiyonun içini incelediğimizde öncelikle a isimli bir değişkenin bildirimini ve ilk değer almasını görüyoruz. Bu değişkenin faaliyet alanı, onun bildirildiği yerden (3. satır) func fonksiyonunun bittiği blok parantezine (19. satır) kadar olan kısımdadır. Aynı şekilde parametre değişkeni olan b değişkeninin de faaliyet alanı, o fonksiyon bloğunun başladığı yerden bittiği yere kadardır. Bu fonksiyon içinde 5. satırdan itibaren yeni bir blok açılmış ve onun içinde de bir b değişkeni tanımlanmıştır. Bu değişkenin faaliyet alanı ise 6. ve 17. satırlar arasında olacaktır. Farklı faaliyet alanlarında aynı ismi tekrar kullanabileceğimizden, artık bu blok parantezleri arasında b isminin kullanımı bu değişkeni belirtecektir. Dıştaki parametre değişkeni ile bir işimiz kalmayacaktır.
8. ve 13. satırlar arasında ise yeni bir blok görüyoruz. Burada b ve c isimli değişkenler tanımlanmıştır. Bu değişkenlerin faaliyet alanı yine onların tanımlandığı yerden blok parantezinin sonuna kadar olmakla beraber, dıştaki parametre değişkeni olan b değişkeninin artık burada sözü geçmeyecektir. Bu nedenle 12. satırda 10 çıktısını alırız. Ancak 11. satırdaki çıktıda 2 seviye yukarda olan a değişkeninin değerini görürüz. Çünkü o bloğa kadar başka bir a ismi bildirilmemiştir ve içteki bloklar dıştaki bloklardaki isimleri kullanabilirler. Bu bloktan çıkıp 14. satıra geldiğimizde ise b değişkeninin değerinin 9 olduğunu görürüz. Çünkü artık iç blokla işimiz bittiğinden oradaki değişkeni de göremiyor olacağız.
15. satırda a değişkenine bir atama yapıyoruz. Bu atama elbetteki en dıştaki a değişkenini etkileyecektir. 16. satırdaki gibi bir kod derleme zamanında hataya yol açar. Çünkü artık c diye bir değişken yoktur. O değişken iç blokta bildirilmiştir ve dış bloktan iç bloğa erişim sağlanamaz. Ancak bu satırı görmezden gelip 18. satıra gelirsek a değişkeninin değerinin 3 olduğunu görürüz. Çünkü iç blokta a ile ilgili bir atama yapılmıştır ve a değişkeninin faaliyet alanı hala devam etmektedir. Bu nedenle böyle bir sonuç ile karşılaşırız. C99 öncesi for ve while gibi yapıların parantezlerinde değişken bildirimine izin verilmediği için, onlara ait bir blok faaliyet alanı söz konusu değildi. C99 ve sonrasında bu durum değiştirilmiştir:
int n = 0;
for(int n=1; n<2; n++)
{
printf("%d\n", n); // Output: 1
}
printf("%d\n", n); // Output: 0
Yukarıdaki kodda C99 öncesinde hata alırsınız. Çünkü C99 öncesinde for parantezi içinde değişken bildirimine izin verilmemekteydi. Ancak C99 ve sonrasında bu parantezlerin içinde yapılan bildirimlere izin verilmiş ve burada bildirilen isimlerin faaliyet alanları for bloğunun blok parantezlerinin arası olarak belirlenmiştir. Bu nedenle for bloğu içinde onun parantezleri arasındaki n değişkeni, dışında ise dıştaki n değişkeni işleme alınmaktadır. Yani burada yine blok faaliyet alanı kuralı söz konusudur.
Dosya Faaliyet Alanı (File Scope) ise bütün fonksiyon ve onların iç bloklarının dışında yer alan isim bildirimlerini kapsamaktadır. Genellikle bu isimler kaynak kodun üstünde bildirilirler ve kaynak kodun herhangi bir yerinden bu isimlere erişim sağlanabilir. Aşağıdaki örneğe bakalım:
int g = 0;
void change1()
{
g = 10;
}
void change2()
{
g = 20;
}
int main()
{
g = 3;
change1();
printf("%d\n", g); // Output: 10
change2();
printf("%d\n", g); // Output: 20
return 0;
}
Yukarıdaki g değişkeni tüm fonksiyonlar tarafından erişilebilir ve değiştirilebilir yapıdadır. Bu nedenle onun dosya faaliyet alanında olduğunu söylemek mümkündür. Bir de çok önemsizmiş gibi görünen ancak bazı durumlarda bilinmesi oldukça önemli olan Fonksiyon Prototipi Faaliyet Alanı (Function Prototype Scope) bulunmaktadır. Fonksiyon prototiplerinde yer alan isimler (genellikle parametrelere verilen isimler) bu kategoriye girmektedirler. Bu isimlerin faaliyet alanı sadece o bildirim için geçerlidir. Fonksiyonun tanımında veya kaynak kodun herhangi bir yerinde geçerli değildir. Hemen şu örneğe bakalım:
int a(int a, int b);
int main()
{
int res = a(3, 4);
printf("%d\n", res); // Output: 7
return 0;
}
int a(int a2, int b2)
{
return a2 + b2;
}
Burada fonksiyon prototipinde kullanılan a ismi sadece o prototipe özeldir. Bu isim fonksiyon tanımında veya kaynak kodun başka bir yerinde kullanılamaz. Zaten fonksiyonun isminin prototipteki parametre ismi ile aynı olabilmesi de bunu kanıtlar niteliktedir. Fonksiyonun ismi dosya faaliyet alanında bulunurken, prototipte yer alan a ismi fonksiyon prototipi faaliyet alanında yer almaktadır. Bu nedenle yukarıdaki kodda herhangi bir hata çıkmaz. Şimdi günümüz C kodlarında çok az karşımıza çıkan son faaliyet alanı kategorisine bakalım. Fonksiyon Faaliyet Alanı (Function Scope) sadece goto deyimi ile kullanılan etiket (label) isimleri için geçerli bir faaliyet alanı kavramıdır. Örnek:
void func1()
{
{
goto label;
label:;
}
goto label;
}
void func2()
{
goto label; // Compile error!
}
label isimli etiketin faaliyet alanı yazıldığı fonksiyonun blok parantezleri arasında yer alır. goto deyiminin ondan önce, sonra, bir bloğun içinde veya dışında olup olmadığına bakılmaksızın, eğer goto deyimi aynı fonksiyonun içinde kullanıldıysa kod geçerli olur. Ancak farklı fonksiyonlardan bu etikete ulaşmaya çalışan goto deyimlerinde derleyici hata verir. goto günümüzde çok sık kullanılan bir deyim olmadığından (ve hatta bence çoğu durumda kullanılmaması gerektiğinden) şimdilik bu konuyu genel kültür bilgisi olarak aklınızda bulundurup geçebilirsiniz.
Son bahsettiğim faaliyet alanı kategorisi dışında tüm kategoriler bildirimin yapılma yerine dikkat ederler. Yani aynı kapsamda olsanız bile bildirimi daha önce yapılmamış bir ismi kullanmak derleme zamanında hataya yol açar. Daha önce söylediğim gibi kodun derlenmesi yukarıdan aşağıya doğru gerçekleştiğinden, derleyicinin önce bildirimi görmesi gerekir. Yoksa ismin ne olduğunu çözümleyemez. Yine daha önce söylediğim gibi farklı faaliyet alanlarında aynı ismi tekrar kullanabiliriz, ancak aynı faaliyet alanında birden fazla aynı isim yer alamaz, bu derleme zamanında hataya yol açar. Şu örneğe bakalım:
#include <stdio.h>
float a; // a: Global faaliyet alanı
int b(int a); // a: Fonksiyon prototipi faaliyet alanı
int main()
{
int a = 5; // a: Blok faaliyet alanı (dışta)
printf("%d\n", a); // Output: 5
{
int a = 3; // a: Blok faaliyet alanı (içte)
printf("%d\n", a); // Output: 3
}
printf("%d\n", b(2)); // Output: 2
return 0;
}
int b(int a) // a: Blok faaliyet alanı
{
return a;
}
Gördüğünüz gibi bu kodlarda kullanılan a isimlerinin hepsinin faaliyet alanı farklı olduğundan, bu kullanımların hepsine izin verilir ve derleme zamanında bir hata ortaya çıkmaz. Ancak örneğin int a = 5; satırının altına float a = 8.9; gibi bir satır yerleştirirsek, bu iki isim aynı faaliyet alanında yer aldığından derleme zamanında hata ortaya çıkardı. Artık bu noktadan sonra değişkenleri 3 kategoriye ayırabiliriz. Bunlar:
| Değişken Türü | Açıklama |
|---|---|
| Yerel Değişken (Local Variable) | Bir fonksiyonun veya bir bloğun içerisinde bildirilen ve blok faaliyet alanında yer alan değişkenlere verilen isimdir. |
| Global Değişken (Global Variable) | Bütün blokların dışında bildirilen ve dosya faaliyet alanında yer alan değişkenlere verilen isimdir. |
| Parametre Değişkeni (Formal Parameter) | Fonksiyon tanımında yer alan parametre değişkenlerine verilen isimdir. Yerel değişkenler ile aynı özelliklere sahiptirler. |
Değişkenleri bu şekilde kategorilere ayırmak, onları bir sonraki konumuz olan belirleyiciler konusunda incelerken oldukça kolaylık sağlayacaktır. Aslında yerel ve global değişken kavramı size çok yabancı gelmeyecektir. Parametre değişkeni kavramı ise fonksiyonların tanımında yer alan parametre isimlerini belirtmektedir. Bu değişkenler yine yerel değişkenler gibi blok faaliyet alanı kuralına uyarlar. Yine yerel değişkenler gibi onlar da bildirildikleri fonksiyona özeldirler. Ancak yerel değişkenlerin fonksiyon içindeki bildirilme noktası önemliyken, parametre değişkenleri bütün fonksiyonu kapsadığından, parametredeki bildirim sırasının (fonksiyon içinde kullanılması açısından) bir önemi yoktur. Şu örneğe bakarak konumuzu pekiştirelim:
int g = 5; // g: Global Değişken
void func(int a) // a: Parametre Değişkeni
{
int res; // res: Yerel Değişken
{
int temp = 6; // temp: Yerel Değişken
res = temp + g + a;
}
}
C’de ilk değer verilmeyen yerel değişkenler tanımsız davranış (undefined behavior) durumundadırlar. Yani kullandığınız derleyiciye göre bu gibi durumlarda ilginç sonuçlar alabilirsiniz. Bazen 0 (tamsayı değerleri için), bazen çöp değer (garbage value), bazen de hata alabilmeniz olasıdır. Ancak global değişkenlerin durumu daha farklıdır. Global değişkenler bildirildikleri an ilk değer alırlar. Yani sizin onlara bir ilk değer vermenize ihtiyaç duymazlar. Hemen bir örnek verelim:
int g;
void print()
{
printf("%d\n", g); // Output: 0
}
print fonksiyonunu çağırdığınızda ekrana 0 değerinin basılacağı garanti altındadır. Elbette bildirilen global değişkenin türüne göre aldığı ilk değer de değişebilir. Genel olarak bildirilen global değişkenin türü ve aldığı ilk değerleri şöyle bir tabloda toparlayabiliriz:
| Veri Türü | Alacağı İlk Değer |
|---|---|
| int | 0 |
| char | ‘\0’ |
| float | 0 |
| double | 0 |
| Gösterici Türü | NULL |
Gösterici türüne şimdilik takılmayın, ondan daha sonra bahsedeceğiz. Durum her ne olursa olsun değişkenlere ilk değer vermek pratikte her zaman iyidir. Bu nedenle yerel veya global olup olmadıklarına bakılmaksızın değişkenlere ilk değer vermenizi tavsiye ederim. Burada artık yazıyı noktalıyorum. Yer belirleyiciler konusunu bu yazıda anlatmayı planlıyordum, ancak buraya sığmayacak kadar geniş bir konu olduğundan başka bir yazıya taşımaya karar verdim. Bir sonraki yazıda değişkenlerin ömürlerinden ve yer belirleyiciler ile değişkenlerin ömürlerini nasıl etkileyebileceğimizden bahsedeceğim.



