“Değişkenleri her zaman başlatır”, önemli hataların gizlenmesine neden olmaz mı?


35

C ++ Çekirdek Kuralları ES.20 kuralına sahiptir: Her zaman bir nesneyi başlat .

Kullanılan önceden ayarlanmış hatalardan ve bunların tanımsız davranışlarından kaçının. Karmaşık başlatmanın anlaşılmasıyla ilgili sorunlardan kaçının. Yeniden düzenlemeyi basitleştirin.

Fakat bu kural böcek bulmaya yardımcı olmaz, sadece onları gizler.
Bir programın başlatılmamış bir değişken kullandığı bir yürütme yoluna sahip olduğunu varsayalım. Bu bir hatadır. Tanımlanmamış davranış bir yana, bu aynı zamanda bir şeyin yanlış gittiği anlamına geliyor ve program muhtemelen ürün gereksinimlerini karşılamıyor. Üretime dağıtılacağı zaman, bir para kaybı olabilir, hatta daha da kötüsü olabilir.

Hataları nasıl tararız? Testler yazıyoruz. Ancak, testler yürütme yollarının% 100'ünü kapsamaz ve testler program girişlerinin% 100'ünü asla kapsamaz. Bundan daha fazlası, bir test bile hatalı bir uygulama yolunu kapsıyor - yine de geçebiliyor. Sonuçta tanımsız davranış, başlatılmamış bir değişkenin biraz geçerli bir değeri olabilir.

Ancak testlerimize ek olarak, başlatılmamış değişkenlere 0xCDCDCDCD gibi bir şey yazabilen derleyicilerimiz var. Bu, testlerin tespit oranını biraz artırır.
Daha da iyisi - başlatılmamış bellek baytlarının tüm okurlarını yakalayacak Adres Sanitizer gibi araçlar var.

Ve son olarak, programa bakıp bu yürütme yolunda önceden ayarlanmış bir okuma olduğunu söyleyebilecek statik analizörler var.

Bu yüzden birçok güçlü aracımız var, ancak değişkeni başlatırsak - sterilizatörler hiçbir şey bulamazlar .

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

Başka bir kural var - program yürütme bir hatayla karşılaşırsa, program en kısa sürede ölmelidir. Onu hayatta tutmaya gerek yok, sadece kaza, kaza dökümü yazma, mühendislere soruşturması için verme.
Değişkenleri sıfırlamak gereksiz yere tersi yapar - aksi halde zaten bir segmentasyon hatası alacağı zaman, program canlı tutulur.


10
Bunun iyi bir soru olduğunu düşünmeme rağmen, örneğinizi anlamıyorum. Bir okuma hatası meydana gelirse ve bytes_readdeğişmediyse (bu yüzden sıfır tutulursa), bunun neden bir hata olması gerekiyor? Program, dolaylı olarak beklenmediği sürece, aklı başında bir şekilde devam edebilir bytes_read!=0. Bu yüzden dezenfektanlar şikayetçi değil. Öte yandan, ne zaman bytes_readönceden başlatılmadı, program böylece başlatılıyor değil, aklı başında bir şekilde devam etmek mümkün olmayacaktır bytes_readaslında tanıtır önceden yoktu bir hata.
Doktor Brown

2
@Abyx: üçüncü taraf olsa bile, onunla başlayan bir arabellekle ilgilenmezse \0buggy. Bununla ilgilenmemesi belgelenirse, arama kodunuz buggy'dir. Kullanmadan bytes_read==0önce arama yapmak için arama kodunu düzeltirseniz, başladığınız yere geri dönersiniz: başlatmazsanız kodunuz buggy, eğer yaparsanız bytes_readgüvenlidir. ( Genelde fonksiyonların bir hata durumunda bile parametrelerini doldurması gerekir : tam olarak değil. Oldukça sık sık çıktılar yalnız bırakılmış veya tanımsız kalmıştır.)
Mat

1
Bu kodun err_tdöndürdüğü görmezden gelmesinin bir nedeni var mı my_read()? Örnekte herhangi bir yerde bir böcek varsa, o kadar.
Blrfl

1
Kolay: Değişkenleri yalnızca anlamlı olması durumunda başlat. Eğer değilse o zaman yapma. Bunu yapmak için "kukla" verileri kullanmanın kötü olduğu için aynı fikirdeyim, çünkü hataları gizliyor.
Pieter B

1
"Başka bir kural var - program yürütme bir hatayla karşılaşırsa, program mümkün olan en kısa sürede ölmelidir. Onu hayatta tutmaya gerek yok, sadece çarpış, çarpışma dökümü yaz, incelemesi için mühendislere ver.": Bunu bir uçuşta dene kontrol yazılımı. Uçağın enkazından çıkan çarpışmayı kurtarmada iyi şanslar.
Giorgio

Yanıtlar:


44

Muhakemeniz birkaç hesapta yanlış gidiyor:

  1. Segmentasyon hataları, gerçekleşmekten çok uzak. Başlatılmamış bir değişkenin kullanılması tanımsız davranışa neden olur . Segmentasyon hataları, bu tür davranışların kendini gösterebilmesinin bir yoludur, ancak normal şekilde görünmek de aynı derecede muhtemeldir.
  2. Derleyiciler, başlatılmamış belleği hiçbir zaman tanımlanmış bir desenle doldurmaz (0xCD gibi). Bu, bazı hata ayıklayıcıların başlatılmamış değişkenlerin kullanıldığı yerleri bulmada size yardımcı olmak için yaptığı bir şeydir. Böyle bir programı hata ayıklayıcı dışında çalıştırırsanız, değişken tamamen rasgele çöp içerecektir. Bu eşit olasılıkla gibi bir karşı olmasıdır bytes_readdeğerine sahiptir 10bu değere sahip olduğu şekilde 0xcdcdcdcd.
  3. Başlatılmamış belleği sabit bir desene ayarlayan bir hata ayıklayıcıda çalışıyor olsanız bile, bunu yalnızca başlangıçta yaparlar. Bu, bu mekanizmanın yalnızca statik (ve muhtemelen yığınla ayrılmış) değişkenler için güvenilir şekilde çalıştığı anlamına gelir. Yığına tahsis edilen veya yalnızca bir kayıt defterinde yaşayan otomatik değişkenler için, değişkenin daha önce kullanılan bir konumda depolanma olasılığı yüksektir, bu nedenle anlatılan hafıza modelinin üzerine yazılmıştır.

Her zaman değişkenleri başlatmak için rehberliğin arkasındaki fikir, bu iki durumu mümkün kılmaktır.

  1. Değişken, varlığının başlangıcından itibaren faydalı bir değer içerir. Bunu sadece bir ihtiyacınız olduğunda bir değişkeni bildirmek için bir rehberle birleştirirseniz, gelecekteki bakım programcılarının değişkenin var olacağı ancak başlatılmamış olduğu bir değişken kullanmaya başlaması tuzağına düşmesini önleyebilirsiniz.

  2. Değişken, benzeri bir fonksiyonun my_readdeğeri güncelleyip güncellemediğini anlamak için daha sonra test edebileceğiniz tanımlanmış bir değer içerir . Başlatma olmadan, bytes_readgerçekten geçerli bir değer olup olmadığını söyleyemezsiniz , çünkü hangi değerle başladığını bilemezsiniz.


8
1)% 1 ile% 99 gibi olasılıklar hakkında. 2 ve 3) VC ++, yerel değişkenler için de böyle bir başlatma kodu oluşturur. 3) statik (global) değişkenler her zaman 0 ile başlatılır.
Abyx

5
@Abyx: 1) Benim tecrübeme göre, olasılık ~% 80 "hemen hemen hiçbir davranışsal davranış farkı yok",% 10 "yanlış olanı yapıyor",% 10 "segfault". (2) ve (3) için: VC ++ bunu yalnızca hata ayıklama oluşturmalarında yapar. Buna güvenmek oldukça kötü bir fikir çünkü seçici olarak piyasaya sürülen yapıları kırıyor ve birçok testinizde görünmüyor.
Christian Aichinger

8
Bence "rehberliğin arkasındaki fikir" bu cevabın en önemli kısmı. Kılavuz, kesinlikle her değişken bildirimini izlemenizi söylemez = 0;. Tavsiyenin amacı değişkeni, bunun için yararlı bir değere sahip olacağınız bir noktada beyan etmek ve hemen bu değeri atamaktır. Bu, ES21 ve ES22 kurallarını hemen takip ederek açıkça anlaşılmıştır. Bu üçünün birlikte çalıştığı anlaşılmalıdır; bireysel ilgisiz kurallar olarak değil.
GrandOpener,

1
@GrandOpener Kesinlikle. Değişkenin bildirildiği noktada atanacak anlamlı bir değer yoksa, değişkenin kapsamı muhtemelen yanlıştır.
Kevin Krumwiede

5
"Derleyiciler asla doldurmaz" her zaman olmamalı mıydı ?
CodesInChaos

25

"Bu kural böcek bulmaya yardımcı olmaz, sadece onları gizler" - yazıyorsunuz, kuralın amacı böcek bulmaya yardım etmek değil, onlardan kaçınmaktır . Ve bir hatadan kaçınıldığında, gizli hiçbir şey yoktur.

Sorunu sizin örneğinize göre tartışalım: my_readFonksiyonun bytes_readher koşulda başlaması için yazılı sözleşmesi olduğunu varsayalım , ancak bir hata durumunda yapmaz, bu nedenle en azından bu durumda hatalı. Amacınız, bytes_readilk önce parametreyi ilklendirmeyerek bu hatayı göstermek için çalışma zamanı ortamını kullanmaktır . Kesin olarak bir adres temizleyici bulunduğunu bildiğiniz sürece, böyle bir hatayı tespit etmek gerçekten de mümkün. Bir hatayı düzeltmek için, kişi my_readdahili olarak işlevini değiştirmek zorundadır .

Ancak, en azından eşit derecede geçerli olan farklı bir bakış açısı vardır: hatalı davranış, yalnızca önceden başlatılmaması ve daha sonra çağrılması ( bundan sonra başlatılması beklentisi ile) kombinasyonundan ortaya çıkar . Bu, bir işleve ilişkin yazılı özelliğin % 100 net olmadığı veya bir hata durumunda davranış hakkında yanlış olduğu durumlarda gerçek dünya bileşenlerinde sıkça yaşanacak bir durumdur . Ancak, çağrıdan önce sıfıra başlatıldığı sürece , program başlatma içinde yapıldığı gibi davranır, bu nedenle doğru davranır, bu kombinasyonda programda hata yoktur.bytes_readmy_readbytes_readmy_readbytes_readmy_read

Bundan dolayı benim tavsiyem şu: başlangıç ​​niteliğinde olmayan yaklaşımı sadece

  • Bir işlev veya kod bloğunun belirli bir parametre başlatıp başlatmadığını test etmek istiyorsanız
  • % 100 emrinizde, söz konusu işlevin, söz konusu parametreye değer atamamak kesinlikle yanlış olan bir sözleşmesi olduğundan emin olabilirsiniz.
  • % 100 ortamın bunu yakalayabileceğinden emin

Bunlar, belirli bir takım ortamı için , tipik olarak test kodunda düzenleyebileceğiniz şartlardır.

Bununla birlikte, üretim kodunda önceden böyle bir değişkeni daha iyi başlatmak, sözleşmenin eksik veya yanlış olması durumunda veya adres temizleyici veya benzeri güvenlik önlemlerinin etkinleştirilmemesi durumunda hataları önleyen daha savunucu bir yaklaşımdır. Programın yürütülmesi bir hatayla karşılaştığında, doğru şekilde yazdığınız gibi "erken çökme" kuralı uygulanır. Ancak bir değişkeni önceden başlatırken yanlış bir şey olmadığı anlamına gelir, daha sonra yürütmeyi durdurmaya gerek yoktur.


4
Bu okuduğumda tam olarak düşündüğüm şeydi. Halının altındaki şeyleri süpürmek değil, onları çöp kutusuna süpürüyor!
corsiKa

22

Değişkenlerinizi daima sıfırlayın

Düşündüğünüz durumlar arasındaki fark, başlatılmamış durumun tanımsız davranışa yol açmasıdır ; başlangıçta zaman ayırdığınız durum iyi tanımlanmış ve belirleyici bir hata yaratır . Bu iki vakanın ne kadar farklı olduğunu vurgulayamıyorum.

Varsayımsal bir simülasyon programında varsayımsal bir çalışanın başına gelebilecek varsayımsal bir örnek düşünün. Bu varsayımsal takım, varsayımsal olarak, varsayımsal olarak sattıkları ürünün ihtiyaçları karşıladığını göstermek için deterministik bir simülasyon yapmaya çalışıyordu.

Tamam, enjeksiyon kelimesini bırakacağım. Bence sen anladın ;-)

Bu simülasyonda, başlatılmamış yüzlerce değişken vardı. Bir geliştirici simülasyonda hataya neden oldu ve birkaç "başlatılmamış değerde dallanma" hatası olduğunu fark etti. “Hmm, bu belirsizliğe neden olabilir gibi görünüyor, en çok ihtiyacımız olduğunda test çalışmalarını tekrarlamayı zorlaştırıyor.” Geliştirici yönetime gitti, ancak yönetim çok sıkı bir programdaydı ve bu sorunu bulmak için kaynakları ayıramadı. “Kullanmadan önce tüm değişkenlerimizi başlattık. Sonunda iyi kodlama uygulamalarımız var.”

Nihai teslimattan birkaç ay önce, simülasyon tam karışıklık modundayken ve tüm ekip, şimdiye kadar finanse edilen her projede olduğu gibi çok küçük bir bütçeyle vaat edilen tüm işleri yönetmek için koşuyor. Birisi, temel bir özelliği test edemediklerini fark etti çünkü bazı nedenlerden dolayı deterministik sim hata ayıklamak için belirleyici davranmıyordu.

Tüm ekip durdurulmuş ve özellikleri uygulamak ve test etmek yerine başlatılmamış değer hatalarını düzeltmek için tüm simülasyon kod temeli tümünü birleştirerek 2 ayın daha iyi bir bölümünü harcadı. Söylemeye gerek yok, çalışan "size söylemiştim" i atladı ve doğrudan diğer geliştiricilerin başlatılmamış değerlerin ne olduğunu anlamalarına yardımcı oldu. Garip bir şekilde, kodlama standartları bu olaydan kısa bir süre sonra değiştirildi ve bu da geliştiricilerin değişkenlerini her zaman başlatmaya teşvik etti.

Ve bu uyarı atışı. Bu burnundan sıyrılan mermi. Asıl mesele tahmin ettiğinizden çok daha uzak .

Başlatılmamış bir değer kullanmak "tanımsız davranış" dır (gibi birkaç köşe durumu hariç char). Tanımsız davranış (veya kısaca UB) sizin için o kadar delice ve tamamen kötü ki, asla alternatifinden daha iyi olduğuna inanmamalısınız. Bazen, belirli derleyicinizin UB'yi tanımladığını ve sonra kullanımının güvenli olduğunu tanımlayabilirsiniz, ancak aksi takdirde tanımsız davranış "derleyicinin hissettiği herhangi bir davranış" dır. Belirsiz bir değeri olan "aklı başında" olarak adlandırdığınız bir şey yapabilir. Muhtemelen programınızın kendini bozmasına neden olarak geçersiz kodlar yayabilir. Derleme zamanında bir uyarı tetikleyebilir veya derleyici dürüst bir hata olduğunu bile düşünebilir.

Ya da hiçbir şey yapmaz

UB'deki kömür madenindeki kanaryam, hakkında okuduğum bir SQL motorundan. Bağlamadığım için beni bağışlayın, makaleyi tekrar bulamadım. Bir işleve daha büyük arabellek boyutu ilettiğinizde SQL motorunda arabellek taşması sorunu vardı, ancak yalnızca belirli bir Debian sürümünde. Hata usulca kaydedildi ve araştırıldı. Komik kısım şuydu: tampon taşması kontrol edildi . Arabellek taşması yerine işlemek için kod vardı. Böyle bir şeye benziyordu:

// move the pointers properly to copy data into a ring buffer.
char* putIntoRingBuffer(char* begin, char* end, char* get, char*put, char* newData, unsigned int dataLength)
{
    // If dataLength is very large, we might overflow the pointer
    // arithmetic, and end up with some very small pointer number,
    // causing us to fail to realize we were trying to write past the
    // end.  Check this before we continue
    if (put + dataLength < put)
    {
        RaiseError("Buffer overflow risk detected");
        return 0;
    }
    ...
    // typical ring-buffer pointer manipulation followed...
}

Yorumuma daha fazla yorum ekledim, ancak fikir aynı. Eğer put + dataLengthsarar etrafında, bu daha küçük olacaktır putpointer (onlar emin imzasız int meraklı bir işaretçi boyutu oldu yapmak için derleme zamanı kontrolleri vardı). Bu olursa, standart halka tamponu algoritmalarının bu taşma ile karışabileceğini biliyoruz, bu yüzden 0 döndürüyoruz.

Görünüşe göre, işaretçilerdeki taşma C ++ 'da tanımsızdır. Çoğu derleyici işaretçilere tamsayı olarak davrandığından, istediğimiz davranış olan tipik tamsayı taşma davranışlarıyla sonuçlanır. Ancak, bu ise tanımsız davranış derleyici yapmak için izin verilir, yani bir şey o istiyor.

Bu hata durumunda, Debian oldu diğer büyük Linux tatlar hiçbiri üretim sürümlerde güncellendi ettiğini gcc yeni bir sürümünü kullanmayı tercih etmek. Gcc'nin bu yeni sürümü daha agresif bir ölü kod düzenleyicisine sahipti. Derleyici tanımsız davranışı gördü ve ififadenin sonucunun “kod optimizasyonunu en iyi yapan ne olursa olsun” olduğuna karar verdi, bu UB'nin tamamen hukuki bir çevirisiydi. Buna göre, bir UB işaretçi taşması olmadan ptr+dataLengthhiçbir zaman altında olamayacağından ptr, ififadenin hiçbir zaman tetiklemeyeceği ve arabellek taşma denetimini en iyi duruma getireceği varsayımı yaptı .

"Aklı başında" UB kullanımı, aslında büyük bir SQL ürününün , önlenmesi için kod yazıldığından yararlanan bir arabellek taşmasına neden oldu!

Asla tanımsız davranışa güvenmeyin. Hiç.


Undefined davranışıyla ilgili çok eğlenceli bir okuma için, software.intel.com/en-us/blogs/2013/01/06/… ne kadar kötü olabileceği konusunda inanılmaz derecede iyi yazılmış bir yazıdır. Bununla birlikte, bu özel görev çoğu için oldukça kafa karıştırıcı olan atomik işlemler üzerinedir, bu yüzden UB için bir primer olarak önerilmesini ve nasıl yanlış gidebileceğini önlüyorum.
Cort Ammon

1
C, tanımlanmamış değerleri tek başına bırakırken belirsiz değerlere, belirtilmemiş değerlere veya belirtilmemiş değerlere, tanımlanmamış değerlere, belirsiz değerlere veya belirsiz değerlere çevirmek için belirsiz değerlere veya belirsiz değerlere çevirmek için içsel değerlere sahip olsaydı. Derleyiciler, yararlı optimizasyonlara yardımcı olmak için bu tür direktifleri kullanabilir ve programcılar, seyrek-matris teknikleri gibi şeyler kullanırken "optimizasyonları" engellerken yararsız kod yazmak zorunda kalmamak için bunları kullanabilirler.
Supercat

@supercat Bunun geçerli bir çözüm olduğu platformları hedeflediğinizi varsaymak hoş bir özellik olacaktır. Bilinen sorunların örneklerinden biri, yalnızca bellek türü için geçersiz olan ancak sıradan yollarla elde edilmesi mümkün olmayan bellek kalıpları oluşturma yeteneğidir. boolbariz problemlerin olduğu mükemmel bir örnektir, ancak x86 veya ARM veya MIPS gibi çok yararlı bir platformda çalıştığınızı ve bu problemlerin tümünün opcode zamanda çözüldüğünü varsaymadığınız sürece başka bir yerde ortaya çıkarlar.
Cort Ammon

Bir optimize edicinin, tamsayı aritmetiğinin boyutlarından dolayı, a için kullanılan bir değerin 8'den az olduğunu kanıtlayabildiğini göz önünde bulundurun switch, bu nedenle, “büyük” bir değerin gelme riski olmadığı varsayılan hızlı komutları kullanabilirler. belirtilmemiş değer (asla derleyicinin kuralları kullanılarak oluşturulamadı) belirir, beklenmeyen bir şey yapar ve aniden bir atlama tablasının sonuna doğru büyük bir sıçrama yaparsınız. Burada belirtilmemiş sonuçlara izin verilmesi, programdaki her anahtar ifadesinin "asla gerçekleşemeyecek" durumları desteklemek için ekstra tuzaklara sahip olması gerektiği anlamına gelir.
Cort Ammon

Eğer gerçek standartlaştırılmış olsaydı, semantikleri onurlandırmak için ne gerekiyorsa onu yapmak için derleyiciler gerekebilirdi; örneğin, bazı kod yolları bir değişken koyarsa, bazıları ise yapmaz ve içsel ise, "başlatılmamış veya belirsizse belirtilmemiş değere dönüştür; başka bir yere bırakma" diyorsa, "değeri olmayan" kayıtlara sahip platformlar için bir derleyicinin herhangi bir kod yolundan önce değişkeni başlatmak için ya da herhangi bir kod yolunda başlangıç ​​durumuna getirme işlemi için kod eklenirse, aksi halde kaçırılır, ancak bunun için gereken anlamsal analiz oldukça basittir.
Supercat

5

Ben çoğunlukla değişkenleri yeniden atamanıza izin verilmeyen işlevsel bir programlama dilinde çalışıyorum. Hiç. Bu tamamen bu böcek sınıfını ortadan kaldırır. Bu, ilk başta büyük bir kısıtlama gibi gözüküyordu, ancak kodunuzu basitleştirmeye ve bakımını daha kolay hale getirme eğiliminde olan yeni verileri öğrendiğiniz sırada tutarlı olacak şekilde yapılandırmanız için zorluyor.

Bu alışkanlıklar da zorunlu dillere aktarılabilir. Bir değişkeni kukla değerle başlatmaktan kaçınmak için kodunuzu yeniden gözden geçirmek neredeyse her zaman mümkündür. Bu kuralların size yapmanı söylediği şey. İçine anlamlı bir şey koymanı istiyorlar, sadece otomatik araçları mutlu edecek bir şey değil.

C tarzı bir API ile olan örneğiniz biraz daha zor. Bu durumlarda, derleyicinin şikayet etmesini önlemek için işlevi kullandığımda sıfıra başlayacağım, ancak my_readbirim testlerinde bir kez hata durumunun düzgün çalıştığından emin olmak için başka bir şey başlatacağım. Her kullanımın olası her hata durumunu test etmeniz gerekmez.


5

Hayır, böcek saklamıyor. Bunun yerine, davranışları bir kullanıcı bir hatayla karşılaştığında bir geliştirici tarafından yeniden üretilebilecek şekilde deterministik yapar.


1
Ve -1 ile başlatılması aslında anlamlı olabilir. "İnt bytes_read = 0" ifadesinin kötü olduğu için, aslında 0 bayt okuyabilir, -1 ile başlatmanız, bayt okuma girişiminin başarılı olmadığını açıkça gösterir ve bunun için test edebilirsiniz.
Pieter B

4

TL; DR: Bu programı düzeltmenin, değişkenlerinizi ilklendirmenin ve dua etmenin iki yolu vardır. Sadece bir tanesi tutarlı sonuçlar veriyor.


Sorunuza cevap vermeden önce, öncelikle Tanımsız Davranış'ın ne anlama geldiğini açıklamam gerekecek . Aslında, derleyici bir yazarın eserin büyük bölümünü yapmasına izin vereceğim:

Bu makaleleri okumak istemiyorsanız, bir TL; DR:

Tanımsız Davranış , geliştirici ve derleyici arasındaki sosyal bir sözleşmedir; derleyici, kullanıcısının Undefined Behavior'a asla, asla, asla güvenmeyeceğine inanmadığını varsayar.

"Burnunuzdan uçan iblisler" arketipi, maalesef bu gerçeğin sonuçlarını tam olarak ifade edemedi. Bir şeyin olabileceğini ispatlamak istese de, çoğunlukla omuz silktiğinden o kadar inanılmazdı ki.

Bununla birlikte gerçek şu ki, Tanımsız Davranış , programı kullanmaya çalışmadan bile önce (bir hata ayıklayıcı içinde olsun ya da olmasın) ve davranışını tamamen değiştirmeden çok önce derlemenin kendisini etkiler .

Örnek 2'nin üstündeki grevdeki örneği buldum:

void contains_null_check(int *P) {
  int dead = *P;
  if (P == 0)
    return;
  *P = 4;
}

dönüştürülür:

void contains_null_check(int *P) {
  *P = 4;
}

çünkü kontrol edilmeden önce onaylandığından beri Polamayacağı açık 0.


Bu sizin örneğiniz için nasıl geçerlidir?

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

Tanımsız Davranışın bir çalışma zamanı hatasına neden olacağı varsayımını yaygın bir hata yaptınız . Olmayabilir.

Bunun tanımının olduğunu düşünelim my_read:

err_t my_read(buffer_t buffer, int* bytes_read) {
    err_t result = {};
    int blocks_read = 0;
    if (!(result = low_level_read(buffer, &blocks_read))) { return result; }
    *bytes_read = blocks_read * BLOCK_SIZE;
    return result;
}

ve satır içi ile iyi bir derleyiciden beklendiği gibi devam edin:

int bytes_read; // UNINITIALIZED

// start inlining my_read

err_t result = {};
int blocks_read = 0;
if (!(result = low_level_read(buffer, &blocks_read))) {
    // nothing
} else {
    bytes_read = blocks_reads * BLOCK_SIZE;
}

// end of inlining my_read

buffer.shrink(bytes_read);

Daha sonra, iyi bir derleyiciden beklendiği gibi, işe yaramaz dalları optimize ediyoruz:

  1. Başlatılmamış değişken kullanılmamalıdır
  2. bytes_readeğer başlatılmamış kullanılan olacağını resultdeğildi0
  3. Geliştirici bu resultasla olmayacak 0!

Yani resultasla 0:

int bytes_read; // UNINITIALIZED
err_t result = {};
int blocks_read = 0;
result = low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Oh, resulthiç kullanılmamış:

int bytes_read; // UNINITIALIZED
int blocks_read = 0;
low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Ah, ilanını erteleyebiliriz bytes_read:

int blocks_read = 0;
low_level_read(buffer, &blocks_read);

int bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Ve işte biz, orijinalin kesin olarak onaylayan bir dönüşümünü yapıyoruz ve hiçbir hata ayıklayıcı başlatılmamış bir değişkeni yakalayamaz, çünkü hiçbiri yoktur.

O yolda oldum, beklenen davranış ve montajın uyuşmadığı meseleyi anlamak hiç de eğlenceli değil.


Bazen, derleyicilerin bir UB yolunu yürüttüklerinde kaynak dosyaları silmek için programı alması gerektiğini düşünüyorum. Programcılar
UB'nin

1

Örnek kodunuza daha yakından bakalım:

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

Bu iyi bir örnektir. Böyle bir hatayı tahmin edersek, çizgiyi ekleyebilir assert(bytes_read > 0);ve bu hatayı çalışma zamanı sırasında yakalayabiliriz, bu başlatılmamış bir değişkenle mümkün değildir.

Fakat varsayalım ve fonksiyonun içinde bir hata bulduk use(buffer). Programı hata ayıklayıcısına yüklüyor, geri izlemeyi kontrol ediyor ve bu koddan çağrıldığını öğreniyoruz. Bu yüzden, bu snippet'in en üstüne bir kesme noktası koyduk, tekrar çalıştırın ve hatayı yeniden oluşturduk. Yakalamaya çalışırken tek bir adım atıyoruz.

Başlatmadıysanız bytes_read, çöp içerir. Her seferinde aynı çöpleri içermesi gerekmez. Çizgiyi geçiyoruz my_read(buffer, &bytes_read);. Şimdi, öncekinden farklı bir değer ise, böceklerimizi hiç çoğaltamayabiliriz! Bir dahaki sefere, aynı girdiyle, tamamen kazayla işe yarayabilir. Eğer sürekli sıfırsa, tutarlı davranışlar elde ederiz.

Değeri kontrol ediyoruz, belki de aynı dönemde bir geri adımda. Eğer sıfırsa, bir şeylerin yanlış olduğunu görebiliriz ; bytes_readbaşarıda sıfır olmamalıdır. (Ya da olabilirse, -1 olarak başlatmak isteyebiliriz.) Muhtemelen hatayı burada yakalayabiliriz. Eğer bytes_readmakul bir değer olsa da, bu yanlış olur, bir bakışta farkeder miyiz?

Bu, özellikle işaretçiler için geçerlidir: bir NULL imleci bir hata ayıklayıcıda her zaman açıkça görülebilir, çok kolay bir şekilde test edilebilir ve kurallara aykırı davranmaya çalışırsak modern donanıma yönelmeli. Çöp işaretçisi daha sonra yeniden üretilemeyen bellek bozulmalarına neden olabilir ve bunların hata ayıklaması neredeyse imkansızdır.


1

OP tanımsız davranışa dayanmıyor ya da en azından tam olarak değil. Aslında tanımsız davranışa güvenmek kötüdür. Aynı zamanda, bir programın beklenmeyen bir durumda davranışı da tanımsızdır, ancak farklı bir tanımsızdır. Sıfıra bir değişkeni ayarlamak, ama sen bir hata var ve ne zaman kullandığı ilk sıfır, program davranır sanely olacağı o bir yürütme yoluna sahip niyetinde olmadıysa yapmak böyle bir yol var mı? Şimdi yabani otların içindesin; Bu değeri kullanmayı düşünmediniz, ama yine de kullanıyorsunuz. Belki zararsız olabilir veya belki programın çökmesine neden olabilir veya programın sessizce veri bozulmasına neden olabilir. Bilmiyorsun

OP'nin söylediği, eğer izin verirseniz, bu hatayı bulmanıza yardımcı olacak araçlar vardır. Değeri başlatmazsanız, ancak yine de kullanırsanız, size bir hata olduğunu söyleyen statik ve dinamik analizörler vardır. Statik bir analizör, programı test etmeye başlamadan önce size söyleyecektir. Öte yandan, değeri kör bir şekilde başlatırsanız, analizörler bu başlangıç ​​değerini kullanmayı planlamadığınızı söyleyemez ve böylelikle hatalarınız tespit edilemez. Şanslıysanız, zararsızdır veya yalnızca programı çökertir; şanssızsanız, verileri sessizce bozar.

OP ile aynı fikirde olmadığım tek yer, en sonunda, "ne zaman zaten bir segmentasyon hatası alacağı" dediği yerde. Aslında, başlatılmamış bir değişken güvenilir bir segmentasyon hatası vermeyecektir. Bunun yerine, programı yürütmeye bile çalışmanıza izin vermeyecek statik analiz araçları kullanmanız gerektiğini söyleyebilirim.


0

Sorunuzun cevabının, bir programda görünen farklı değişken türlerine bölünmesi gerekir:


Yerel değişkenler

Genellikle bildirim, değişkenin ilk değerini aldığı noktada doğru olmalıdır. Eski C stilinde olduğu gibi değişkenleri önceden bildirmeyin:

//Bad: predeclared variables
int foo = 0;
double bar = 0.0;
long* baz = NULL;

bar = getBar();
foo = (int)bar;
baz = malloc(foo);


//Correct: declaration and initialization at the same place
double bar = getBar();
int foo = (int)bar;
long* baz = malloc(foo);

Bu, başlatma ihtiyacının% 99'unu ortadan kaldırır, değişkenler nihai değerlerini doğrudan kapalı tutar. Bazı istisnalar, başlatmanın bazı koşullara bağlı olduğu yerlerdir:

Base* ptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}

Bu davaları böyle yazmanın iyi bir fikir olduğuna inanıyorum:

Base* ptr = nullptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}
assert(ptr);

I. e. Değişkeninizin bazı hassas başlatma işlemlerinin yapıldığını açıkça belirtiniz.


Üye değişkenleri

Burada diğer cevap verenlerin söylediklerine katılıyorum: Bunlar her zaman yapıcılar / başlatıcı listeleri tarafından başlatılmalıdır. Aksi takdirde, üyeleriniz arasında tutarlılığı sağlamakta zorlanırsınız. Ve her durumda başlatılmaya ihtiyacı olmayan bir üyeniz varsa, sınıfınızı yeniden düzenleyin, bu üyeleri her zaman ihtiyaç duydukları türetilmiş bir sınıfa ekleyin.


tamponlar

Burası diğer cevaplara katılmıyorum. İnsanlar değişkenleri başlatma konusunda dindar olduklarında, sıklıkla bunun gibi tamponları başlatırlar:

char buffer[30];
memset(buffer, 0, sizeof(buffer));

char* buffer2 = calloc(30);

Bunun neredeyse her zaman zararlı olduğuna inanıyorum: Bu ilklendirmelerin tek etkisi, valgrindgüçsüz gibi araçlar ürettikleridir . İlklendirilen arabelleklerden daha fazla okuyan herhangi bir kod olması muhtemeldir. Ancak ilklendirme ile bu hataya maruz kalamazsınız valgrind. Bu nedenle, onları sıfırlarla dolu belleğe gerçekten güvenmiyorsanız, onları kullanmayın (ve bu durumda, sıfıra neden ihtiyaç duyduğunuza dair bir yorum bırakın).

Ayrıca, valgrindbaşlatma öncesi kullanım hatalarını ve bellek sızıntılarını ortaya çıkarmak için tüm test ünitesini veya benzer bir aracı çalıştıran derleme sisteminize bir hedef eklemenizi şiddetle tavsiye ederim . Bu, değişkenlerin tüm ön başlatmalarından daha değerlidir. Bu valgrindhedef, herhangi bir kod açıklanmadan önce, en önemlisi düzenli olarak gerçekleştirilmelidir.


Global Değişkenler

Başlatılmamış global değişkenlere sahip olamazsınız (en azından C / C ++ vb.), Bu başlatmanın istediğiniz şey olduğundan emin olun.


Üçlü operatörle koşullu ilklendirmeler yazabildiğinizi gözlemleyin, örn. Base& b = foo() ? new Derived1 : new Derived2;
Davislor

@Lorehead Bu basit durumlar için işe yarayabilir, ancak daha karmaşık olanlar için işe yaramaz: Üç veya daha fazla vakanız varsa bunu yapmak istemezsiniz ve kurucularınız sadece okunabilirlik için üç veya daha fazla argüman alırlar nedenler. Ve bu, bir döngüdeki ilklendirme dalı için bir argüman aramak gibi yapılması gereken herhangi bir hesaplamayı düşünmüyor bile.
cmaster

Daha komplike durumlarda, fabrika işlevinde başlatma kodu sarabilirdiniz: Base &b = base_factory(which);. Bu, kodu bir kereden fazla çağırmanız gerekirse veya sonucu sabitleştirmenize izin veriyorsa, en kullanışlıdır.
Davislor

@Lorehead Bu doğru ve istenen mantık basit değilse kesinlikle gitmek için bir yol. Yine de, ilk kullanıma hazırlama ?:işleminin bir PITA olduğu ve bir fabrika işlevinin hala üstesinden geldiği küçük bir gri alan olduğuna inanıyorum . Bu davalar arasında çok az ve çok var, ancak var.
cmaster

-2

Doğru derleyici seçenekleri ayarlanmış olan iyi bir C, C ++ veya Objective-C derleyicisi, değeri ayarlanmadan önce bir değişken kullanılırsa derleme zamanında size söyleyecektir. Başlatılmamış bir değişkenin değerini kullanan bu dillerde tanımsız davranış olduğundan, "kullanmadan önce bir değer belirleme" bir ipucu veya bir kılavuz veya iyi bir uygulama olmadığından,% 100 gereksinimidir; Aksi takdirde programınız kesinlikle bozulur. Java ve Swift gibi diğer dillerde, derleyici başlatılmadan önce bir değişken kullanmanıza asla izin vermez.

"İnitialize" ve "set a value" arasında mantıksal bir fark var. Dolar ve euro arasında dönüşüm oranını bulmak ve "çifte oran = 0.0;" yazmak istersem sonra değişken bir değer kümesine sahiptir, ancak başlatılmadı. Burada depolanan 0.0, doğru sonuçla hiçbir ilgisi yoktur. Bu durumda, bir hata nedeniyle hiçbir zaman doğru dönüşüm oranını kaydetmezseniz, derleyicinin size söyleme şansı yoktur. Az önce "çifte oran" yazdıysanız; ve asla anlamlı bir dönüşüm oranı kaydetmedi, derleyici size söyleyecektir.

Öyleyse: Derleyici başlatılmadan kullanıldığını söylediği için bir değişkeni başlatmayın. Bu bir hatayı gizliyor. Asıl sorun, kullanmamanız gereken bir değişken kullanıyor olmanız veya bir kod yolunda bir değer belirlememiş olmanızdır. Sorunu çöz, saklanma.

Bir değişkeni başlatmayın, çünkü derleyici size başlatılmadan kullanıldığını söyleyebilir. Yine, problemleri gizliyorsun.

Kullanıma yakın değişkenleri bildirin. Bu, bildirim noktasında anlamlı bir değerle başlatabileceğiniz olasılığını artırır.

Değişkenleri tekrar kullanmaktan kaçının. Bir değişkeni tekrar kullandığınızda, ikinci amaç için kullandığınızda büyük olasılıkla işe yaramaz bir değere ilklendirilir.

Bazı derleyicilerin yanlış negatifleri olduğu ve başlangıç ​​kontrolünün durma problemine eşdeğer olduğu yorumlanmıştır. Her ikisi de pratikte alakasız. Belirtildiği gibi bir derleyici, hata bildirildikten on yıl sonra başlatılmamış bir değişkenin kullanımını bulamazsa, alternatif bir derleyici aramanın zamanı gelmiştir. Java bunu iki kez uygular; derleyicide bir kez, denetleyicide bir kez, sorunsuz olarak. Durma probleminin üstesinden gelmenin kolay yolu, bir değişkenin kullanılmadan önce başlatılması değil, basit ve hızlı bir algoritma ile kontrol edilebilecek şekilde kullanılmadan önce başlatılmasıdır.


Bu, yüzeysel olarak iyi geliyor, ancak başlatılmamış değer uyarılarının doğruluğuna çok güveniyor. Bu alınıyor mükemmel doğru Durma Problemi eşdeğerdir ve üretim derleyiciler ve (yani onlar yanlış negatif acı yapabilirim yok onlar gerekirken başlatılmamış değişkeni teşhis); örneğin , on yıldan fazla bir süredir sabitlenmemiş olan GCC böcek 18501'e bakınız .
zwol

Gcc hakkında söylediklerin sadece söyleniyor. Gerisi alakasız.
gnasher729

Gcc için üzücü, ancak geri kalanının neden alakalı olduğunu anlamıyorsanız, kendinizi eğitmeniz gerekir.
zwol
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.