PDA

Tam Sürümünü Görmek İçin : int[0] sorusu


Darkstar
09/12/2004, 17:37
Geçen gün karşılaştığım ve ilginç gelen bir konuda fikrinizi alayım dedim.

Soru şu Aşağıdaki kod parçası çalıştığında ekrana ne yazar?

int* p;
int N = 0;
p = NULL;
p = new int[N];
if (!p)
cout<<"null\n";
else
cout<<"not null!";


Sizin de tahmin ettiğiniz gibi ekrana "not null" yazıyor...

Esas soru ise N'in run time sırasında belirlendiğini varsayarsak bu memory'i silmek için
destructor'umuza ne yazacağımız..

destructorda Şöyle yazsak olurmu :

if (p!=0)
delete[] p;


acehreli
09/12/2004, 18:52
Darkstar, oncelikle new'un (ve new[]'un) cok onemli bir davranisini gozden kacir$igini soylemek isterim: bu islecler hicbir zaman 0 (veya NULL) dondurmezler.

Bu, ozellikle C++'i malloc'tan sonra ogrenen programcilarin cok yaptiklari bir hatadir. Uzerine de alinma lutfen, ayni hataya dusen bir suru programciyla calistim. Hatta bunlardan birisine new'un 0 dondurmedigini anlatamadim bile. (Acayip sinirlenmistim; sanki benim ona inandirmam gerekiyormus gibi... :) Neyse...)

Benim bildigim su: new'un dondurdugu gosterge degeri her zaman icin gecerli bir nesneyi gosterir. Kurulmakta olan nesnenin kendi atabilecegi hatalara karsi, new de bellek ayiramadigi durumda std::bad_alloc hatasini atar.

Bu bildiklerime dayanarak, verdigin kodun yasal oldugunu dusunuyorum. Icinde sifir tane nesne olan bir diziye eristigin zaman baska turlu bir hata yapacak oldugun icin, bence yazdigin kadariyla kod gecerli.

Pek bir sey ifade etmese de, programini g++ 3.4.2 ile derleyip calistirdim ve valgrind'la sinadim; ama hicbir uyari almadim...

Ali

Darkstar
09/12/2004, 21:46
yani destructorda :

if (p!=0)
delete[] p;

yazabiliriz?

acehreli
09/12/2004, 21:50
delete[] p; yazabilirsin ama (p!=0)'a gerek yoktur. free'de oldugu gibi, delete'e de guvenle 0 gonderilebilir.

Ama new'un hicbir zaman 0 dondurmedigine dikkat edersen, zaten genelde delete'e gonderecegin sey de 0 olamaz. Yani o denetimi kaldirabilirsin...

Ali

Darkstar
09/12/2004, 23:36
Ok. denetim yapmamın zararı yok ama gereksiz dediğin gibi. Zaten p'yi sildikten sonra NULL'a çekiyorum artık. Bu da ilerde karşılaşılacak problemleri önlüyor.

delete[] p;
p = 0;

acehreli
10/12/2004, 00:14
Ben artik hicbir zaman gostergeleri nesneleri silindikten sonra sifira esitlemiyorum, cunku artik hicbir kaynagi kod icinde acikca geri vermiyorum. Dinamik bellekte olusturulan nesneler her zaman icin bir akilli gostergeye emanet edildikleri icin zaten onlari ben kendim acikca silmiyorum.

Tek bir dinamik nesneyi sahiplenen bir sinifim oldugunda ise, o nesne bozucu islevde geri verildigi icin, zaten sifirlamaya gerek kalmiyor. Cunku nesne gittikten sonra artik icindeki gostergenin yanlislikla kullanilmasi da olanaksiz oluyor.

Ozetle, ben o sifirlama islemini bile yapmiyorum. :) Ama tabii onun da bir zarari yok.

Ali

Darkstar
10/12/2004, 18:51
Maalesef aşağıdaki şekilde kullandığımızda ilerde problemlere yolaçtığını öğrenmiş bulunuyorum. Bunun sebebi "new int[0]" şeklindeki [0] allocation'ının C++ ta tanımlı olmaması. Yani undefined! Aşağıdaki gibi N ne değerini check etmeden kullanırsanız ilerde problemlere yolaçıyor ve olmadık yerde segmentation fault alıp hata nerde diye arıyorsunuz.

int* p;
int N = 0;
p = NULL;
p = new int[N];

Yukardaki kullanım yerine alloc. yapmadan önce N'i check etmeniz gerekli:

int* p;
int N = 0;
p = NULL;
if (N)
p = new int[N];


Bu konuyla ilgili olarak effective c++ adlı kitaptan bir alıntı aktarmak istiyorum:

When you take it upon yourself to write operator new (Item 10 explains why you might want to), it's important that your function(s) offer behavior that is consistent with the default operator new. In practical terms, this means having the right return value, calling an error-handling function when insufficient memory is available (see Item 7), and being prepared to cope with requests for no memory. You also need to avoid inadvertently hiding the "normal" form of new, but that's a topic for Item 9. ¤ Item E8, P2
The return value part is easy. If you can supply the requested memory, you just return a pointer to it. If you can't, you follow the rule described in Item 7 and throw an exception of type std::bad_alloc. ¤ Item E8, P3
It's not quite that simple, however, because operator new actually tries to allocate memory more than once, calling the error-handling function after each failure, the assumption being that the error-handling function might be able to do something to free up some memory. Only when the pointer to the error-handling function is null does operator new throw an exception. ¤ Item E8, P4
In addition, the °C++ standard requires that operator new return a legitimate pointer even when 0 bytes are requested. (Believe it or not, requiring this odd-sounding behavior actually simplifies things elsewhere in the language.) ¤ Item E8, P5
That being the case, pseudocode for a non-member operator new looks like this: ¤ Item E8, P6
void * operator new(size_t size) // your operator new might
{ // take additional params
if (size == 0) { // handle 0-byte requests
size = 1; // by treating them as
} // 1-byte requests
while (1) {
attempt to allocate size bytes;
if (the allocation was successful)
return (a pointer to the memory);
// allocation was unsuccessful; find out what the
// current error-handling function is (see Item 7)
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if (globalHandler) (*globalHandler)();
elsethrow std::bad_alloc();
}
}
The trick of treating requests for zero bytes as if they were really requests for one byte looks slimy, but it's simple, it's legal, it works, and how often do you expect to be asked for zero bytes, anyway?

acehreli
10/12/2004, 21:15
Darkstar, alinti yaptigin madde bu konuyla ancak uzaktan ilgili.

Scott Meyers orada operator new islecini kendimiz tanimlamaya karar verdigimiz zaman nasil normal (default) operator new gibi davranmasina dikkat etmemiz gerektigini anlatiyor.

Normal operator new'un davranislari arasinda da buyukluk olarak 0 bile gonderilse, yine de yasal bir gosterge dondurmesi gerektigi sayiliyor. Bu da C++ standardinin sart kostugu bir kuralmis: "C++ standard requires that operator new return a legitimate pointer even when 0 bytes are requested." (legitimate: yasal, mesru)

Evet, Scott Meyers orada new'den bahsediyor ama ben yine de new[] icin de ayni oldugunu tahmin ediyorum. Standarda bakip bu durumun new[] icin farkli olup olmadigina bakacagim. (Bakiyorum...) Evet, new[]'un 0 nesne icin cagrilmasi yasal... Yaptigi sey, boyu 0 olan bir dizi olusturmakmis. Standardin 5.3.4/7 numarali paragrafi soyle diyor:

"When the value of the expression in a directnewdeclarator is zero, the allocation function is called to allocate an array with no elements. The pointer returned by the newexpression is nonnull. [Note: If the library allocation function is called, the pointer returned is distinct from the pointer to any other object. ]"

Turkcesi kabaca soyle: "new'e gonderilen ifadenin degeri sifir oldugunda; bellegi ayiran asil islev, bos bir dizi ayirmak icin cagrilir. new ifadesinin dondurdugu gosterge degili sifirdan farklidir. [Not: Eger kutuphanenin bellek ayirma islevi cagrilmissa; donen gostergenin degeri, butun diger nesnelerin gosterge degerlerinden farkli olur.]

Ben g++'a ve valgrind'a da cok guveniyorum. Bu konuda g++'in standarda uygun davrandigini goruyroum.

O yuzden, bence senin kodunda baska turlu bir hata var. 'Segmentation fault'u hangi noktada aliyorsun? Acaba daha sonradan o 0 buyukluklu diziye sanki icinde nesne varmis gibi erisiyor musun?

Ornegin, su program senin ortaminda gocuyor mu? Hatasiz olarak derlenip calismasi gerekiyor:


#include <iostream>

using namespace std;

int main()
{
int* p;
int N = 0;
p = 0;
p = new int[N];
if (!p)
cout<<"null\n";
else
cout<<"not null!" << p << '\n';

delete[] p;
}


Eger bu program senin icin hata veriyorsa bence senin derleyicin standardi desteklemiyor. Eger bu program calisiyorsa, senin programinda baska yerde bir hata var.

Ali

Darkstar
10/12/2004, 22:33
Peki o halde neden new'i yeniden tanımlarken size==0 ise size=1 yapıyor. Madem hiçbir problem yok, o halde dokunmasına da gerek yok ?

acehreli
11/12/2004, 01:28
Standart, sifir uzunluklu dizilerin bile kendilerine ait bir adresleri olmalarini sart kosuyormus. Aslinda ben bu kuralin genel halini biliyorum: Hicbir nesne barindirmayan siniflarin nesnelerinin de kendilerine ait adreslerinin olmasi gerekir. Bunun icin de bos siniflarin bile boyu 1'dir:


class Sinif {}; // Icinde hicbir sey yok.

int foo()
{
// Ama boyu 1'dir
assert(sizeof(Sinif) == 1);
}


C++ her nesnenin kendisine ait bir adresi olmasini ister. Onun icin bos sinif nesnelerinin de boylari 1'dir. Eger boylari 0 olsaydi onlari bir diziye koydugumuzda hepsinin adresi ayni olurdu. Cunku dizi[i] dedigimizde aslinda su formul isletilir ya:

*(dizi + i * sizeof(Tur))

Eger o turun boyu 0 olsaydi butun nesneler dizi'nin baslangic adresinde gibi gorunurlerdi. Halbuki standart her nesnenin ayri bir adreste oturmasini ister.

Herhalde new[]'un sifir ile cagrildigindaki davranisi da bununla ilgili olmali...

size=1 yapilmasinin tek nedeni, standardin istedigini yerine getirebilmek icin belki de en uygun yontem olmasidir... Birlikte dusunelim: Sifir uzunluklu diziler istendiginde o dizilerin her birisi icin kendilerine has adresleri nereden edinebiliriz? En uygun cozum, bellekten bir tane nesne icin yer ayirmak ve onun adresini gondermektir. Scott Meyers de boyle yapiyor iste. Evet, gereksiz yere bir nesnelik bellek harcamis oluruz ama Scott Meyers'in da dedigi gibi, "sifir bayt uzunlugunda bellek ne sIklIkla istenir ki zaten..."

Tabii bu kural gerceklenirken 1 tane nesne ayrildigini biliyoruz diye o adrese erismeye calismamaliyiz. Bu tanimsiz davranis olur, cunku orada hic bir nesne yoktur. En azindan o bellek ustunde kurucu islev isletilmemistir. Standardin gozunde orada ici bos olan bir dizi vardir. Bos bir dizinin nesnelerine de erisemeyiz.

Ali