Değişken Parametre Alan Fonksiyonlar
Derleyicinin parametre sayısı üzerinde kontrol yapmadığı printf ve scanf gibi özel
fonksiyonlara değişken sayıda parametre alan fonksiyonlar denir. Değişken sayıda parametre
alan fonksiyonların parametrelerinde bu durum ... ile belirtilir. Değişken sayıda parametre
alan fonksiyonlar en az bir parametreye sahip olmak zorundadır. Fonksiyon çağırılırken
zorunlu olarak yazılması gereken parametreler birden fazla olabilir. Şüphesiz ... parametre
listesinin sonunda olmak zorundadır. Örneğin,
void Func(int a, int b, ...); geçerli
void falan(...); geçersiz
void Foo(int a, ..., int b); geçersiz
Değişken sayıda parametre alan fonksiyonlar stdarg.h dosyasındaki makrolar kullanılarak
yazılırlar. Bu makrolar;
VA_START
VA_ARG
VA_END
makrolarıdır. Değişken sayıda parametre alan fonksiyonları anlamak için fonksiyon parametre
aktarımına ilişkin temel bilgiler gerekir.
C'de parametrelerin aktarım sırası standart olarak belirlenmemiştir. PC'lerde cdecl denilen
biçim kullanılır ve bu biçime göre parameteler sağdan sola fonksiyona aktarılır. Bu sisteme
göre parametreler stack'e kopyalandığında düşük adreste en soldaki parametre olacak biçimde
ve bir dizi gibi yapı söz konusu olur. Yani, en soldaki parametre değişkeninin adresini ve
parametrelerin türlerini bilirsek bütün parametrelere erişebiliriz. Örneğin aşağıdaki kodda bir
assert hatası oluşmayacaktır:
void Func(int a, int b)
{
assert(&a + 1 == &b);
}
Ancak parametrelerin sağdan sola fonksiyona geçirileceği her sistemde garanti değildir, zaten
özel makroların kullanılması taşınabilirliği sağlamak için düşünülmüştür.
Değişken sayıda parametre alan fonksiyonlarda fonksiyonun kaç parametreyle çağırıldığı ve
parametrelerin türlerinin neler olduğu bilinmelidir. Bu yüzden birinci parametreden bu
bilgilerin anlaşılması gerekir. Örneğin birinci parametrede belirtilen sayıda int türden
parametreleri toplayan bir fonksiyon yazacak olalım.
void Add(int n, ...)
{
int total = 0;
int *p = &n + 1;
for (i = 0; i < n; i++) {
total += *p;
++p;
}
return total;
}
int main(void)
{
printf("%d\n", Add(5, 10, 20, 30, 40, 50));
return 0;
}
C'de derleyici bir fonksiyonun prototipini ya da tanımlamasını görmemişse ya da değişken
sayıda parametre alan bir fonksiyon çağırılıyorsa default argüman dönüştürmesi uygulanır.
Default argüman dönüştürmesinde char ve short türleri int türüne, float türü ise double türüne
dönüştürülür.
Yukarıdaki Add fonksiyonunun kodu taşınabilir değildir, taşınabilir hale getirmek için
stdarg.h içerisindeki makrolar kullanılmalıdır.
va_list Türü:
va_list parametre çekme işlemlerinde kullanılan bir türdür. Genellikle bu tür char türden bir
gösterici olarak ele alınır.
va_start Makrosu:
İşlemlere başlamadan önce bu makro bir kez çağırılmalıdır.
void va_start(va_list ar, lastParam);
Bu makro ilk parametrenin adresinden faydalanarak ilk parametreden sonraki ilk
belirtilmeyen argümanın adresini hesaplar ve ar'nin içerisine yerleştirir. Burada lastParam
...'dan bir önceki parametreyi temsil etmektedir.
va_arg Makrosu:
Bu makro her çağırıldığında geri dönüş değeri olarak bir sonraki parametreyi elde eder.
type va_arg(va_list ar, type);
Burada type programcının belirttiği türdür. Bu makro ilk kez çağırıldığında yazılmayan ilk
parametrenin bilgisi alınır ve ar'yi günceller. va_arg makrosunda elde edilen değer sağ taraf
değeridir.
va_end Makrosu:
Bu makro aslında pekçok sistem için gereksizdir, ancak taşınabilirliği sağlamak için işlem
sonunda kullanılmalıdır.
void va_end(va_list ar);
Pekçok derleyicide bu makro koda hiçbir şey açmamaktadır.
void Add(int n, ...)
{
int i;
total = 0;
va_start(ar, n);
for (i = 0; i < n; ++i)
total += va_arg(ar, int);
C ve Sistem Programcıları Derneği 63
va_end(ar);
}
Sınıf Çalışması: %d, %c ve %f işlemlerine duyarlı myprintf() fonksiyonunu yazınız. Bu
fonksiyonu yazarken stdarg.h makrolarını kullanınız.
void myprintf(const char *format, ...);
Açıklamalar:
Format karakterinde % karakteri görünene kadar karakterler yazdırılarak ilerlenir. % karakteri
görüldüğünde yanındaki karaktere bakılır ve işlemler yapılır. int ve double sayıları yazdırmak
için printf kullanılabilir.
#include <stdarg.h>
#include <stdio.h>
void myprintf(const char *format, ...)
{
va_list ar;
va_start(ar, format);
while(*format != '\0') {
if (*format == '%') {
switch (*(format + 1)) {
case 'd':
printf("%d", va_arg(ar, int));
break;
case 'c':
putchar(va_arg(ar, int));
break;
case 'f':
printf("%f", va_arg(ar, double));
break;
default:
putchar(*(format + 1));
}
format += 2;
}
else {
putchar(*format);
format++;
}
}
}
void main(void)
{
myprintf("float = %f int = %d char = %c", 2.34, 5, 'g');
}
vprintf, vsprintf ve vfprintf Fonksiyonları
Bu fonksiyonlar printf, sprintf ve fprintf fonksiyonlarının değişken parametre alan
biçimleridir.
int vprintf(const char *format, va_list ar);
int vsprintf(char *buf, const char *format, va_list ar);
int vprintf(FILE *f, const char *format, va_list ar);
Özellikle vsprintf, printf fonksiyonunun işlevini yaratmak için yaygın olarak kullanılmaktadır.
Bu fonksiyonlar programcıdan format stringini alırlar, ancak diğer parametreleri almazlar.
Diğer parametreler yerine onların va_list türünden adreslerini alırlar. Böylece yalnızca yazı
yazdıran bir fonksiyon vsprintf kullanılarak printf haline getirilmiş olur.
int MessagePrintf(const char *format, ...)
{
char buf[100];
va_list ar;
va_start(ar, format);
vsprintf(buf, format, ar);
MessageBox(NULL, buf, "Message", MB_OK);
va_end(ar);
}
MessagePrintf("a = %d b = %d\n", a, b);
Not: Standart C fonksiyonlarında bir tampon güvenliği problemi vardır ve bu durum çok
eleştirilmektedir. Örneğin gets fonksiyonu güvensizdir, çünkü klavyeden ne kadar karakter
girileceği belirli değildir. gets fonksiyonu tüm karakterleri belirtilen adrese yerleştirir, '\n'
karakterini tampondan siler, ama diziye yerleştirmez. Halbuki fgets en fazla SIZE - 1
karakteri diziye yerleştirir, ancak bu fonksiyon da '\n' karakterini dizinin sonuna
yerleştirmektedir. fgets fonksiyonunu gets gibi kullanabilmek için aşağıdaki gibi bir algoritma
önerilebilir.
char *p;
if (fgets(buf, SIZE, stdin) != NULL)
if ((p = strchr(buf, '\n')) != NULL)
*p = '\0';
Burada fgets fonksiyonu:
1) abc Ctrl+Z girişi yapıldığında tampona abc karakterlerini ve null karakterini yerleştirir ve
buf adresine geri döner.
2) Ctrl+Z girişi yapıldığında tampona bir şey yerleştirmez ve NULL değerine geri döner.
3) abc Enter girişi yapıldığında tampona abc\n ve null karakterlerini yerleştirir ve buf
adresiyle geri döner.
Tabii bunun yerine aşağıdaki gibi daha güvenli bir gets yazılabilir:
char *mygets(char *buf, size_t size)
{
size_t i;
int ch;
for (i = 0; (ch = getchar()) != EOF && ch != '\n'; i++) {
if (i >= size - 1)
break;
buf[i] = ch;
}
buf[i] = '\0';
C ve Sistem Programcıları Derneği 65
return buf;
}
exec Fonksiyonları
exec fonksiyonları başka bir programı çalıştırmak için kullanılan genel fonksiyonlardır.
Bilindiği gibi bir programın .text, .data, .bss ve .stack gibi bölümleri, yani process’in bellek
alanı process handle alanında tutulmaktadır. Böylece işletim sistemi başka bir programı
çalıştırmak üzere process’ler arası geçiş yaptığında geçiş yapılan process’in bellek alanı
process handle alanından hareketle elde edilmektedir. exec fonksiyonları o anda çalışmakta
olan process’in bellek alanını boşaltır, çalıştırılacak programı belleğe yükler, process’in yeni
bellek alanını çalıştırılacak programın belek alanı yapar. Yani exec fonksiyonları
uygulandığında artık programın bellek alanı ortadan kaldırılır. Yani program başka bir
program olarak çalışmaya devam eder. Aslında exec isimli bir fonksiyon yoktur. İsimleri
benzer bir grup fonksiyon vardır. Ancak exec sözcüğü bütün bu grubu anlatan bir fonksiyon
kavramı olarak kullanılmaktadır. Programda exec fonksiyonu uygulandıktan sonra artık o
programa özgü hiç bir kod çalışmayacaktır. Örneğin aşağıdaki kodda hiç bir zaman
unreachable code yazısı gözükmeyecektir.
{
exec(...);
printf(“unreachable code..\n”);
}
Görüldüğü gibi exec fonksiyonu sonrasında artık bu fonksiyonu uygulayan programın hiç bir
varlığı kalmaz. Bir process’in iki önemli bilgisi process handle alanı ve process’in bellek
alanıdır. fork işleminde hem handle alanının hem de bellek alanının birer kopyası çıkarılır.
exec işleminde ise başka bir program yüklenerek bellek alanı silinerek başka bir process
yüklenmektedir. Ancak process’in bellek alanı aynen kalmaktadır. UNIX/Linux sistemlerinde
başka bir biçimde program çalıştırmanın yolu yoktur.
Win32 Sistem Programcıları İçin Not: Win32 sistemlerinde exec benzeri bir işlem yoktur. Bu
sistemlerde başka bir programın çalıştırılması CreateProcess API fonksiyonu ile yapılır. Bu
fonksiyon yeni bir handle alanı oluşturup bir programı çalıştırır.
UNIX/Linux programcıları eski programın devam etmesini sağlayarak yeni bir program
çalıştırmak için önce bir kez fork yaparlar, böylece process’in bir kopyası yaratılır. Yaratılan
yeni alt process’te exec uygularlar. POSIX sistemlerinde kullanılan exec fonksiyonları
şunlardır:
execl
execv
execle
execve
execlp
execvp
Görüldüğü gibi exec sözcüğünden sonra bunu ‘l’ ya da ‘v’ izlemektedir. Bu harfleri de ‘e’ ya
da ‘p’ izler. ‘l’ harfi list sözcüğünden ‘v’ harfi ise vector sözcüğünden kısaltmadır. ‘e’
environment ‘p’ ise path sözcüklerinden gelir. Bu fonksiyonların hepsi birinci parametre
C ve Sistem Programcıları Derneği 66
olarak çalıştırılacak programın diskteki ismini alırlar. l’li versiyonlar programın komut satırı
argümanlarını ayrık parametre olarak, v’li versiyonları da komut satırı argümanlarını bir
gösterici dizisi olarak alır. l’li versiyonlar değişken sayıda parametre alan fonksiyon
biçiminde yazılmışlardır. Normal olarak bir process’in çevre değişkenleri fork işlemi ile alt
process’e tamamen aktarılır. Eğer exec işlemi sırasında exec fonksiyonlarının e’li versiyonları
kullanılırsa exec işlemi sırasında process’in çevre değişkenleri kümesi de tamamen
değiştirilebilmektedir.
’li ve p’siz versiyonları PATH çevre değişkenine bakılıp
bakılmayacağını belirtir. p’siz versiyonlarda çalıştırılacak dosya ismi yalnızca dosya ismi
yazılırken belirtilen path’te aranır. Bilindiği gibi path ifadesi / ile başlatılmışsa bu ifadeye
mutlak path ifadesi denilir ve yol root dizininden itibaren belirtilir. Eğer path ifadesi / ile
başlatılmamışsa buna göreli path ifadesi denir. Göreli path ifadesi bulunulan dizinden itibaren
yer belirtir. Path ifadesi . ile başlatılırsa POSIX sistemlerinde mutlak path ifadesi belirtir. exec
fonksiyonlarının p’li versiyonlarında dosya yalnızca PATH ile belirtilen dizinde aranır. DOS
ve Windows sistemlerinde olduğu gibi bulunulan dizinde aranmaz. Dosya isminin bulunulan
dizinde de aranması için bulunulan dizine ilişkin bir PATH ifadesinin eklenmiş olması
gerekir.
PATH = /home/kaan
p’li versiyonlarda eğer dosya ismi / ile başlatılmamışsa arama verilen göreli path ifadesinin
tek tek PATH’te belirtilen dizinlerin sonuna eklenmesi ile yapılır. Örneğin p’li versiyonlarla
dosya ismi “a/b” biçiminde belirtilmişse PATH’te belirtilen tüm dizinlerin altındaki a
dizininde b aranır. p’li versiyonlarda dosya ismi mutlak path ifadesi ile belirtilirse PATH
çevre değişkenine baş vurulmaz. Böylece p’li veriyonun bir anlamı kalmaz.
execl Fonksiyonu
int execl(const char *path, const char *argv, ...);
Fonksiyonun birinci parametresi çalıştırılacak programın path ifadesi, ikinci parametresi ise
ilk komut satırı argümanıdır. Bundan sonra istenildiği kadar komut satırı argümanı yazılabilir,
ancak NULL ile bitirilmesi gerekir. exec fonksiyonlarının geri dönüş değerleri başarısız ise –1
değeridir. unistd.h başlık dosyası içerisindedir. Son parametre olan NULL gösterici düz sıfır
olarak girilirse derleyici bunu null gösterici sabiti olarak yorumlamaz, int olarak yorumlar.
Göstericilerle int türünün farklı olduğu sistemlerde bu durum probleme yol açabilir. Bu
durumda sıfır’ın (char *) türüne dönüştürülmesi uygundur.
Sıfır Sabitine İlişkin Not: Standartlara göre null gösterici sabiti 0 ya da (void *) 0’dır. 0
sayısı bir göstericiye atandığında ya da bir gösterici ile != ve == operatörü ile
karşılaştırıldığında derleyici tarafından null gösterici olarak değerlendirilir. Ancak 0 sayısı
normal işlemlerde int türünden sabit olarak değerlendirilir. Örneğin
Func(x, 0);
Derleyici burada 0’ı nasıl yorumlayacaktır? İşte derleyici sıfır’a karşı gelen parametrenin
gösterici olup olmadığına bakar. Gösterici ise 0’ın null gösterici olduğu anlamını çıkartır, bu
parametreye karşı gelen parametre değişkenleri doğal türlere ilişkinse derleyici 0’ı int olarak
değerlendirir. Ancak 0 parametresine karşılık gelen parametre değişkeni prototip
yokluğundan bilinemiyorsa ya da değişken sayıda parametre alan bir fonksiyon söz konusu
ise derleyici 0’ı yine int türden kabul eder.
Görüldüğü gibi aslında C’de kullandığımız argv[0] program ismini içermek zorunda değildir.
Yani çalışabilen dosyanın ismini alabilmek için argv[0]’a bakmak taşınabilir bir yöntem
değildir. Çünkü exec fonksiyonlarında çalıştırılacak dosyanın ismi ile argv[0] istenirse farklı
olarak verilebilir. Tabii geleneksel olarak argv[0] program ismine ilişkin bir parametredir.
Benzer durum Win32 sistemlerinde de bu biçimdedir. Bu sistemlerde çalışabilen dosyanın
path ifadesini GetModuleFileName fonksiyonu ile almak gerekir.
int main(void)
{
printf(“Başla\n”);
if (execl(“/bin/ls”, “ls”, “-l”, (char *) NULL) == -1) {
fprintf(stderr, “Cannot exec...\n”);
exit(1);
}
printf(“Son..\n”);
}
execlp Fonksiyonu
Bu fonksiyon execl fonksiyonunun PATH çevre değişkenine bakan biçimidir.
int execlp(const char *file, const char *argv0, ...);
Bu fonksiyon execl fonksiyonundan farklı olarak eğer path ifadesi / ile başlatılmamışsa tek
tek path çevre değişkeniyle belirtilen dizinlerde arama yapar. Eğer path / ile başlatılmışsa bu
fonksiyonun execl'den bir farkı kalmaz.
execle Fonksiyonu
Normal olarak fork işlemi ile çevre değişkenlerinin hepsi yeni yaratılan process'e
aktarılmaktadır (çevre değişkenlerinin process handle alanında tutulduğu varsayılabilir). Yine
normal olarak exec işlemi sonrasında aynı çevre değişkenleri etkinliğini sürdürür. Ancak
execle ile exec işlemi sırasında yeni çalıştırılacak program çalıştırılmadan önce eski çevre
değişkenleri atılıp yeni çevre değişken takımı set edilebilir.
int execle(const char *path, const char *argv0, ...,
/* (char *)0, char *const envp[] */);
Görüldüğü gibi komut satırı argümanlarını sonlandırmak için NULL gösterici parametresi
girilir, NULL göstericisinden sonra da çevre değişkenleri dizisi girilmektedir. Çevre
değişkenlerinin bulunduğu gösterici dizisinin sonu NULL gösterici ile bitirilmelidir. Örneğin;
char *myenv[] = {"ali = 100", "veli = 200", NULL};
//...
execle("myprog", "myprog", (char *)0, myenv);
execle fonksiyonu PATH çevre değişkenine bakmaz.
Anahtar Notlar: Göstericiyi gösteren göstericilerde const olma durumu biraz karmaşıktır.
Göstericiyi gösteren göstericilerde const anahtar sözcüğü üç yere getirilebilir:
1) const char **ppc;
2) char *const *ppc;
3) char **const ppc;
Bu ifadelerde sırasıyla üç değişik nesne const yapılmıştır.
char c = 'a';
char *pc = &c;
char **ppc = &pc;
Bilindiği gibi const nesnenin adresi const bir adrestir ve gösterdiği yer const olan bir
göstericiye atanabilir. Aşağıdaki gibi bir atama güvensizdir:
char *pc;
const char **ppc;
ppc = &pc; /* güvensiz */
Buradaki atamanın güvenli olduğu sanılabilir, çünkü const olmayan bir nesnenin const bir
nesneye atanması normal izlenimi vermektedir. const char **ppc ifadesinde **ppc ifadesi
const'tur, ancak *ppc ifadesi kendisi const olmayan ama gösterdiği yer const olan bir
göstericidir. Bu durum *ppc kullanılarak const olmayan bir göstericiye const olan bir adresi
gizlice atamaya olanak sağlar. Örneğin:
const char c = 'a';
char *pc;
const char **ppc;
ppc = &pc;
*ppc = &c;
*pc = 'b';
Burada *ppc = &c işlemiyle aslında pc'ye c'nin adresi atanmaktadır, yani artık *pc = 'b' gibi
bir işlemle c bozulabilir. Ancak aşağıdaki atama güvenlidir:
const char c;
char *pc;
char *const *ppc;
ppc = &pc; /* güvenli */
Çünkü artık aşağıdaki işlem error'a yol açacaktır:
*ppc = &c; /* error */
Çünkü artık *ppc kendisi const olan bir göstericidir. Aşağıdaki atama da güvensizdir:
const char c;
const char *pc;
char *const *ppc;
ppc = &pc; /* güvensiz */
Bu durumun güvenli olabilmesi için const char *const *ppc; bildirimi yapılmalıdır.
ppc pc c
'a'
Şimdi bir fonksiyonun parametresinin aşağıdaki biçimde olduğunu düşünelim:
void Func(char *const *ppc);
Bu fonksiyona geçirilen hangi parametreler güvenlidir?
1) char *abc[10];
Func(abc); /* güvenli */
2) const char *abc[10];
Func(abc); /* güvensiz */
Aşağıdaki fonksiyon için inceleme yapalım:
void Func(const char **ppc);
1) char *abc[10];
Func(abc); /* güvensiz */
2) const char *abc[10];
Func(abc); /* güvenli */
Gösterici dizilerinde de const anahtar sözcüğü iki yere yerleştirilebilir:
1) const char *p[10];
Bu tanımlama p'nin bir gösterici dizisi olduğu, bu diziye gösterdiği yer const olan adreslerin
yerleştirileceği anlamına gelir.
2) char *const p[10] = {.....};
Burada dizinin kendisi const biçimindedir, yani diziye ilk değer verdikten sonra bir daha
elemanlara değer atayamayız. İlk değer vermekte kullandığımız adreslerin gösterdiği yerlerin
const olması gerekmez. Benzer biçimde const anahtar sözcüğü hem başa hem de dizi isminin
önüne getirilebilir.
execv Fonksiyonu
Bu versiyon da PATH çevre değişkenine bakmaz. execl fonksiyonundan farkı komut satırı
argümanları gösterici dizisi olarak girilir. Gösterici dizisinin sonu NULL ile bitirilmelidir.
int execv(const char *path, char *const argv[]);
Fonksiyonun ikinci parametresindeki const yerleşimi kendisi const olan bir gösterici dizisinin
adresini geçebilmemize olanak sağlar. Bu fonksiyon tipik olarak aşağıdaki gibi kullanılabilir:
int main(int argc, char *argv[])
{
// ...
if (execv(argv1, &argv[1]) == -1) {
perror("exec");
exit(1);
}
return 0;
}
Burada başka bir programı çalıştıran program söz konusudur. Buradaki program execute.c
olsun, myprog ise başka bir program olsun, execute programını şöyle çalıştırmış olalım:
$ execute myprog arg1 arg2
Burada myprog programının argv[0] parametresi myprog, argv[1] parametresi arg1'dir.
execvp Fonksiyonu
Bu fonksiyonun execv fonksiyonundan tek farkı PATH çevre değişkenine bakmasıdır.
int execvp(const char *file, char *const argv[]);
execve Fonksiyonu
Bu versiyon PATH çevre değişkenine bakmaz, ama çevre değişken takımını değiştirir.
int execve(const char *file, char *const argv[], char *const env[]);
fork ve exec Fonksiyonlarının Birarada Kullanılması
Bilindiği gibi normal olarak exec fonksiyonları process'i başka bir program olarak devam
ettirmektedir. Halbuki pek çok uygulamada başka bir programı çalıştıran programın da devam
etmesi istenir. Bunun için önce bir kere fork işlemi yapılır, alt process'te exec fonksiyonu
uygulanır. Bu işlem tipik olarak aşağıdaki gibi yapılabilir:
pid_t pid;
if ((pid = fork()) == -1) {
perror("fork");
exit(1);
}
if (pid == 0) {
if (execl(.....) < 0) {
perror("exec");
exit(1);
}
}
wait(NULL);
// ...
Buradaki işlem Win32'de CreateProcess uygulayıp WaitForSingleObject fonksiyonuyla
beklemeye karşılık gelir.
Sınıf Çalışması: gcc dereyicisiyle komut satırı argümanı olarak verilen bir dosyayı
derleyiniz, sonra çalışabilen dosyayı ls -l ile görüntüleyiniz.
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
C ve Sistem Programcıları Derneği 71
int main(int argc, char *argv[])
{
pid_t pid;
if (argc != 2) {
fprintf(stderr, "Wrong number of argument!..\n");
exit(1);
}
if ((pid = fork()) == -1) {
perror("Fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
printf(argv[1]);
printf("\n");
if (execlp("gcc", "gcc", "-g", argv[1], (char *) 0) < 0) {
perror("child1 exec");
exit(EXIT_FAILURE);
}
}
wait(NULL);
if ((pid = fork()) == -1) {
perror("Fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
if (execlp("ls", "ls", "-l", (char *) NULL) < 0) {
perror("child2 exec");
exit(EXIT_FAILURE);
}
}
wait(NULL);
return 0;
}