C işaretçileri ne zaman NULL için kontrol edilmelidir?


18

Özet :

C'deki bir işlev her zaman bir NULLişaretçiyi kaydı silip etmediğinden emin olmalıdır mı? Değilse, bu kontrolleri atlamak ne zaman uygundur?

Ayrıntılar :

Röportajları programlama hakkında bazı kitaplar okudum ve C'deki işlev argümanları için uygun giriş doğrulama derecesinin ne olduğunu merak ediyorum? Açıkçası, bir kullanıcıdan girdi alan herhangi bir fonksiyonun, NULLkayıttan çıkarılmadan önce bir işaretçi kontrolü de dahil olmak üzere, doğrulama gerçekleştirmesi gerekir. Ancak, aynı dosyadaki API'nız aracılığıyla göstermeyi beklemediğiniz bir işlev durumunda ne olacak?

Örneğin git kaynak kodunda aşağıdakiler görünür:

static unsigned short graph_get_current_column_color(const struct git_graph *graph)
{
    if (!want_color(graph->revs->diffopt.use_color))
        return column_colors_max;
    return graph->default_column_color;
}

Eğer *graphbir NULLsonra bir boş gösterici muhtemelen programı kilitleniyor, ama muhtemelen diğer bazı öngörülemeyen davranışlara sonuçlanan indirgenmedikleri edilecektir. Öte yandan işlev staticve böylece programcı girişi zaten doğrulamıştır. Bilmiyorum, sadece rastgele seçtim çünkü C ile yazılmış bir uygulama programında kısa bir örnekti. İşaretçilerin NULL kontrol edilmeden kullanıldığı birçok başka yer gördüm. Sorum genel bu kod segmentine özgü değil.

İstisna teslimi bağlamında da benzer bir soru sordum . Ancak, C veya C ++ gibi güvenli olmayan bir dil için, işlenmeyen istisnaların otomatik hata yayılımı yoktur.

Öte yandan, açık kaynak projelerinde (yukarıdaki örnek gibi) bunları kullanmadan önce işaretçi kontrolü yapmayan birçok kod gördüm. Herkes bir işleve ne zaman kontrol koymak için işlevin doğru argümanlar ile çağrıldığını varsayarak yönergeleri hakkında düşünceleri olup olmadığını merak ediyorum.

Bu soruyla genel olarak üretim kodu yazmakla ilgileniyorum. Ama aynı zamanda programlama görüşmeleri bağlamıyla da ilgileniyorum. Örneğin, birçok algoritma ders kitabı (CLR gibi) algoritmaları hata denetimi olmaksızın sahte kodda sunma eğilimindedir. Bununla birlikte, bu bir algoritmanın çekirdeğini anlamak için iyi olsa da, açıkçası iyi bir programlama uygulaması değildir. Bu yüzden bir röportajcıya kod örneklerimi basitleştirmek için hata kontrolünü atladığımı söylemek istemezdim (ders kitabının yapabileceği gibi). Ama aynı zamanda aşırı hata kontrolü ile verimsiz kod üretmek gibi görünmek istemem. Örneğin , null olup olmadığını graph_get_current_column_colorkontrol etmek *graphiçin değiştirilmiş olabilirdi, ancak *graphnull olması gerekmemesi dışında, null olursa ne yapacağını açık değil.


7
Arayanların doğuştan gelenleri anlamadığı bir API için bir işlev yazıyorsanız, bu, dokümantasyonun önemli olduğu yerlerden biridir. Bir bağımsız değişkenin geçerli, NULL olmayan bir işaretçi olması gerektiğini belgelerseniz, aramayı denetlemek arayanın sorumluluğu haline gelir.
Blrfl


2017 yılının gündeminde, soruyu akılda tutarak ve çoğu cevap 2013'te yazılmıştır, cevaplardan herhangi biri derleyicileri optimize etmek nedeniyle zaman yolculuğu tanımlanmamış davranışlar sorununu ele alıyor mu?
rwong

Geçerli işaretçi argümanları bekleyen API çağrıları durumunda, sadece NULL için test değerinin ne olduğunu merak ediyorum? Silinmeyen tüm geçersiz işaretçiler NULL ve segfault kadar kötü olur.
PaulHK

Yanıtlar:


15

Geçersiz boş göstergelere programcı hatası veya çalışma zamanı hatası neden olabilir. Çalışma zamanı hataları, mallocdüşük bellek veya ağın bir paketi düşürmesi veya kullanıcı aptalca bir şey girmesi nedeniyle başarısız olması gibi bir programcının düzeltemediği bir şeydir. Programcı hatalarına, işlevi yanlış kullanan bir programcı neden olur.

Gördüğüm genel kural, çalışma zamanı hatalarının her zaman kontrol edilmesi gerektiğidir, ancak programcı hatalarının her seferinde kontrol edilmesi gerekmez. Diyelim ki doğrudan denilen bazı aptal programcı graph_get_current_column_color(0). İlk çağrıldığında segfault olur, ancak bir kez düzelttiğinizde, düzeltme kalıcı olarak derlenir. Her çalıştırıldığında kontrol etmeye gerek yok.

Bazen, özellikle üçüncü taraf kitaplıklarında, assertbir ififade yerine programcı hatalarını kontrol etmek için bir işaret görürsünüz . Bu, geliştirme sırasında kontrollerde derlemenizi ve üretim kodunda bırakmanızı sağlar. Ayrıca, zaman zaman potansiyel programcı hatasının kaynağının semptomdan çok uzak olduğu, nedensiz kontroller gördüm.

Açıkçası, her zaman daha bilgiç bulabilirsiniz, ama bildiğim çoğu C programcısı, marjinal olarak daha güvenli olan kod üzerinde daha az karmaşık kod lehine. Ve "daha güvenli" öznel bir terimdir. Geliştirme sırasında açık bir segfault, tarladaki küçük bir yolsuzluk hatasına tercih edilir.


Soru biraz öznel ama bu şimdilik en iyi cevap gibi görünüyordu. Bu soru hakkında düşüncelerini veren herkese teşekkürler.
Gabriel Southern

1
İOS'ta malloc asla NULL döndürmez. Bellek bulamazsa, uygulamanızdan önce belleği serbest bırakmasını ister, ardından işletim sisteminden (diğer uygulamalardan belleği serbest bırakmasını ve muhtemelen öldürmesini ister) sorar ve hala bellek yoksa uygulamanızı öldürür . Kontrol gerekmez.
gnasher729

11

Kernighan & Plauger, "Yazılım Araçları" nda, her şeyi kontrol edeceklerini yazdılar ve aslında asla gerçekleşmeyeceğine inandıkları koşullar için "Olmaz" hata mesajıyla iptal edeceklerdi.

Terminallerinde "Olamam" ın kaç kez ortaya çıktığını gördükleri için hızla aşağılandıklarını bildiriyorlar.

Kullanmadan önce (denemeden) DAİMA işaretçisini NULL için kontrol etmelisiniz. HER ZAMAN . Gerçekleşmeyen NULL'ları denetlemek için çoğalttığınız kod miktarı ve "boşa harcadığınız" işlemci döngüleri, bir çökme dökümünden başka bir şeyden hata ayıklamanız gerekmeyen çökme sayısıyla ödenir. eğer bu şanslıysan.

İşaretçi bir döngü içinde değişmezse, onu döngü dışında kontrol etmek yeterlidir, ancak daha sonra uygun const süslemelerini ekleyen döngü tarafından kullanılmak üzere kapsam sınırlı bir yerel değişkene "kopyalamanız" gerekir. Bu durumda, döngü gövdesinden çağrılan her işlevin, TÜM YOL AŞAĞI prototipleri üzerinde gerekli sabit süslemeleri içerdiğinden emin olmalısınız. Yapmazsanız veya yapamazsanız (örn. Bir satıcı paketi veya inatçı bir iş arkadaşı nedeniyle), o zaman DEĞİŞTİRİLEN HER ZAMAN BOŞ DEĞİL olup olmadığını kontrol etmelisiniz , çünkü COL Murphy'nin tedavi edilemez bir iyimser olduğundan biri gidiyor bakmadığın zaman zap.

Bir işlevin içindeyseniz ve işaretçinin NULL olmayan olarak gelmesi gerekiyorsa, bunu doğrulamanız gerekir.

Bir işlevden alıyorsanız ve NULL olmayan bir çıktı olması gerekiyorsa, doğrulamanız gerekir. malloc () bunun için özellikle kötü şöhretlidir. (Nortel Networks, şimdi geçersiz kılındı, bu konuda sert ve hızlı bir yazılı kodlama standardı vardı. Bir noktada bir çökme hata ayıklaması yaptım, malloc () işaretini geri döndüm ve bir NULL işaretçisi döndürdüm ve idiot kodlayıcı kontrol etmek için uğraşmadı ona yazmadan önce, çünkü sadece hafızası olduğunu biliyordu ... Sonunda bulduğumda çok kötü şeyler söyledim.)


8
NULL olmayan bir işaretçi gerektiren bir işlevdeyseniz, ancak yine de kontrol edersiniz ve NULL ... sonra ne olacak?
detly

1
@detly ne yaptığınızı durdurun ve bir hata kodu döndürün ya da bir iddiaya yol açın
James

1
@James - hiç düşünmedim assert. NULLKontrolleri dahil etmek için mevcut kodu değiştirme hakkında konuşuyorsanız hata kodu fikrini sevmiyorum .
detly

10
@detly hata kodlarından hoşlanmıyorsanız C dev kadar ilerlemeyeceksiniz
James

5
@ JohnR.Strohm - bu C, iddialar ya da hiçbir şey: P
detly

5

İşaretçiyi bir şekilde boş bırakamayacağınız konusunda kendinizi ikna edebildiğinizde bu denetimi atlayabilirsiniz.

Genellikle null işaretçi denetimleri, null değerinin bir nesnenin şu anda kullanılabilir olmadığının bir göstergesi olarak görünmesi beklenen kodda uygulanır. Null, bağlantılı listeleri, hatta işaretçi dizilerini sonlandırmak için bir sentinel değeri olarak kullanılır. argvGeçirilen dizeleri vektör mainolması gereklidir benzer bir dize boş karakterle sonlandırılır nasıl bir işaretçi tarafından boş sonlandırılmış: argv[argc]bir boş gösterici ve komut satırını ayrıştırılırken bu güvenebilirsiniz.

while (*argv) {
   /* process argument string *argv */
   argv++; /* increment to next one */
}

Bu nedenle, null değerini kontrol etmek için durumlar, beklenen bir değerdir. Boş kontroller, bağlı listenin aramasını durdurmak gibi boş göstericinin anlamını uygular. Kodun işaretçiyi kaydından çıkarmasını önlerler.

Tasarım tarafından boş bir işaretçi değerinin beklenmediği bir durumda, bunun kontrol edilmesinin bir anlamı yoktur. Geçersiz bir işaretçi değeri ortaya çıkarsa, büyük olasılıkla null olmayan görünür ve taşınabilir değerlerden herhangi bir taşınabilir şekilde ayırt edilemez. Örneğin, bir işaretçi türü olarak yorumlanan başlatılmamış depolamanın okunmasıyla elde edilen bir işaretçi değeri, bazı gölgeli dönüşümlerle elde edilen bir işaretçi veya sınırların dışına doğru artırılmış bir işaretçi.

Gibi bir veri türü hakkında graph *: bu, boş bir değer geçerli bir grafik olacak şekilde tasarlanabilir: kenarları ve düğümleri olmayan bir şey. Bu durumda, graph *işaretçi alan tüm işlevlerin , grafiklerin gösterilmesinde doğru bir etki alanı değeri olduğundan, bu değerle uğraşması gerekir. Öte yandan a, graph *bir grafiğe sahip olduğumuzda asla boş olmayan kap benzeri bir nesneye işaretçi olabilir; boş bir işaretçi bize "grafik nesnesinin mevcut olmadığını; henüz ayırmadık ya da serbest bıraktık; ya da bunun şu anda ilişkili bir grafiği olmadığını" söyleyebilir. Bu ikinci işaretçi kullanımı birleşik bir boolean / uydudur: boş olmayan işaretçi "Ben bu kardeş nesneye sahibim" anlamına gelir ve bu nesneyi sağlar.

Bir nesneyi serbest bırakmasak bile bir işaretçiyi null değerine ayarlayabiliriz, sadece bir nesneyi diğerinden ayırmak için:

tty_driver->tty = NULL; /* detach low level driver from the tty device */

Bir işaretçinin belirli bir noktada boş olamayacağını bildiğim en ikna edici argüman, o noktayı "if (ptr! = NULL) {" ve karşılık gelen "}" içine sarmaktır. Bunun ötesinde, resmi doğrulama alanındasınız.
John R. Strohm

4

Füglere bir ses daha ekleyeyim.

Diğer cevapların birçoğu gibi, diyorum ki - bu noktada kontrol etme zahmetine girmeyin; arayanın sorumluluğu. Ama basit bir uygunluktan (ve C programlama kibirinden) ziyade üzerine bir temelim var.

Donald Knuth'un programları olabildiğince kırılgan yapma ilkesini takip etmeye çalışıyorum. Bir şeyler ters giderse, büyük çökmesini sağlayın ve boş bir işaretçiye başvurmak genellikle bunu yapmanın iyi bir yoludur. Genel fikir bir çökme veya sonsuz bir döngü yanlış veri oluşturmaktan çok daha iyidir. Ve programcıların dikkatini çekiyor!

Ancak boş göstergelere (özellikle büyük veri yapıları için) başvurmak her zaman bir çökmeye neden olmaz. İç çekmek. Bu doğru. Ve bu, Asserts'ın devreye girdiği yerdir. Basittirler, programınızı anında kilitleyebilirler (bu, "yöntem bir boş değerle karşılaşırsa ne yapmalıdır?" Sorusunu yanıtlar) ve çeşitli durumlar için açılıp kapatılabilir (önerilir Müşterilerin çökmesi ve kötü verilere göre şifreli bir mesaj görmesi daha iyi olduğu için bunları kapatmayın.

Bu benim iki sentim.


1

Genellikle sadece bir işaretçi atandığında kontrol ederim, genellikle bu konuda bir şeyler yapabileceğim ve geçersizse kurtarabileceğim tek zamandır.

Örneğin, bir pencereye bir tutamacı alırsam, önce ve sonra orada boş olup olmadığını kontrol edeceğim ve boş koşul hakkında bir şey yapacağım, ama her seferinde boş olup olmadığını kontrol etmeyeceğim. İşaretçiyi kullanıyorum, işaretçinin geçirildiği her işlevde, aksi halde yinelenen hata işleme kodunun dağları olurdu.

graph_get_current_column_colorKötü bir işaretçi ile karşılaşırsa, durumunuz için yararlı bir şey yapamayacak gibi büyük olasılıkla işlevler , bu yüzden arayanlara NULL olup olmadığını kontrol ediyorum.


1

Aşağıdakilere bağlı olduğunu söyleyebilirim:

  1. CPU kullanımı kritik midir? NULL için yapılan her denetim biraz zaman alır.
  2. İşaretçinin NULL olma olasılığı nedir? Daha önceki bir işlevde kullanılmış mıydı. İşaretçinin değeri değiştirilmiş olabilir.
  3. Sistem önleyici mi? Bir görev değişikliği olabilir ve değeri değiştirebilir mi? Bir ISR girip değeri değiştirebilir mi?
  4. Kod ne kadar sıkı bağlanmış?
  5. BOŞ işaretçileri otomatik olarak kontrol edecek bir tür otomatik mekanizma var mı?

CPU Kullanımı / Oran İşaretçisi NULL (NULL) seçeneğini her kontrol edişinizde zaman alır. Bu nedenle kontrollerimi işaretçinin değerini değiştirebileceği yerle sınırlandırmaya çalışıyorum.

Önleyici Sistem Kodunuz çalışıyorsa ve başka bir görev kodu kesintiye uğratabilir ve değeri potansiyel olarak değiştirebilirse, bir kontrolün olması iyi olur.

Sıkı Bağlantılı Modüller Sistem sıkıca bağlanmışsa, daha fazla kontrole sahip olmanız mantıklı olacaktır. Bununla demek istediğim, birden fazla modül arasında paylaşılan veri yapıları varsa, bir modül başka bir modülün altından bir şeyi değiştirebilir. Bu durumlarda daha sık kontrol etmek mantıklıdır.

Otomatik Kontroller / Donanım Yardımı Dikkate alınması gereken son şey, üzerinde çalışmakta olduğunuz donanımın NULL olup olmadığını kontrol edebilecek bir çeşit mekanizmaya sahip olmasıdır. Özellikle Sayfa Arızası tespitine atıfta bulunuyorum. Sisteminizde sayfa hatası algılaması varsa, CPU'nun kendisi NULL erişimi kontrol edebilir. Şahsen bunu her zaman çalıştığı ve açık kontroller yapmak için programcıya güvenmediği için en iyi mekanizma olarak görüyorum. Aynı zamanda pratik olarak sıfır ek yükü avantajına sahiptir. Bu kullanılabilir varsa ben bunu tavsiye, hata ayıklama biraz daha zor ama aşırı öyle değil.

Kullanılabilir olup olmadığını test etmek için işaretçi içeren bir program oluşturun. İşaretçiyi 0 olarak ayarlayın ve ardından okuma / yazmaya çalışın.


Bir segfault'u otomatik bir NULL kontrolü yapmak olarak sınıflandırıp sınıflandırmayacağımı bilmiyorum. CPU bellek korumasına sahip olmanın, bir işlemin sistemin geri kalanına çok fazla zarar vermemesi için yardımcı olduğunu kabul ediyorum, ancak otomatik koruma olarak adlandırmam.
Gabriel Southern

1

Bence girişleri onaylama (ön / post-koşullar, yani) programlama hatalarını tespit etmek için iyi bir şeydir, ancak sadece göz ardı edilemeyecek türden yüksek ve iğrenç, gösteri durdurma hatalarıyla sonuçlanır. asserttipik olarak bu etkiye sahiptir.

Bunun altında kalan her şey, çok dikkatli bir şekilde koordine edilmiş takımlar olmadan bir kabusa dönüşebilir. Ve elbette ideal olarak tüm takımlar sıkı standartlar altında çok dikkatli bir şekilde koordine edilir ve birleştirilir, ancak çalıştığım ortamların çoğu bunun çok altında kaldı.

Bir örnek olarak, birinin boş göstergelerin varlığını dini olarak kontrol etmesi gerektiğine inanan bazı meslektaşları ile çalıştım, bu yüzden böyle bir çok kod serptiler:

void vertex_move(Vertex* v)
{
     if (!v)
          return;
     ...
}

... ve bazen bir hata kodu bile döndürmeden / ayarlamadan böyle. Ve bu, satın alınan birçok üçüncü taraf eklentisi ile birkaç on yıl eski bir kod tabanıydı. Ayrıca birçok hata ile boğulmuş bir kod tabanıydı ve genellikle sorunun nedeninden çok uzaktaki sitelerde çökme eğilimi gösterdikleri için kök nedenleri izlemesi çok zor olan hatalardı.

Ve bu uygulama bunun nedenlerinden biriydi. Yukarıdaki move_vertexişlevin yerleşik bir önkoşulun kendisine boş bir tepe noktası geçirmesi ihlalidir , ancak böyle bir işlev sessizce kabul etti ve yanıt olarak hiçbir şey yapmadı. Bu yüzden, bir eklentinin, söz konusu işleve boş kalmasına neden olan, sadece onu algılamaması, daha sonra sadece birçok şey yapmasına neden olan bir programcı hatası olabileceği ve nihayetinde sistem dökülmeye veya çökmeye başlamasıydı.

Ancak buradaki asıl mesele, bu sorunu kolayca tespit edememekti. Bu yüzden bir zamanlar yukarıdaki analog kodu bir şeye çevirirsem ne olacağını görmeye çalıştım, şöyle assert:

void vertex_move(Vertex* v)
{
     assert(v && "Vertex should never be null!");
     ...
}

... ve dehşetime göre, başvuruyu başlattıktan sonra bile bu iddianın sola ve sağa başarısız olduğunu gördüm. İlk birkaç arama sitesini düzelttikten sonra, birkaç şey daha yaptım ve daha sonra bir kayık yükleme iddiası başarısızlıkları aldım. Çok fazla kod değiştirene kadar devam ettim, çünkü değişikliklerimi geri aldım, çünkü çok müdahaleci oldular ve begrudgingly bu boş işaretçi kontrolünü tuttular, bunun yerine işlevin boş bir tepe noktasını kabul etmesine izin verdiğini belgelediler.

Ancak, en kötü durum senaryosu da olsa, ön / son koşul ihlallerini kolayca tespit edilememe tehlikesi budur. Daha sonra, yıllar geçtikçe, test radarı altında uçarken bu tür ön / son koşulları ihlal eden bir tekne yükü sessizce biriktirebilirsiniz. Kanımca böylesine boş gösterici kontrolleri, açık ve iğrenç bir iddia başarısızlığının dışında aslında iyiden çok, çok daha fazla zarar verebilir.

Boş göstergeleri ne zaman kontrol etmeniz gerektiğinin asıl sorusuna gelince, bir programcı hatasını tespit etmek için tasarlanmış olup olmadığını ve bunun sessiz ve algılanmasının zor olmasına izin vermeyerek liberal olarak iddia etmeye inanıyorum. Bir programlama hatası ve programlayıcının kontrolü dışında bir bellek yetersizliği gibi bir şey değilse, boş olup olmadığını kontrol etmek ve hata işleme kullanmak mantıklıdır. Bunun ötesinde bu bir tasarım sorusudur ve işlevlerinizin geçerli öncesi / sonrası koşulları olarak kabul ettiklerine dayanır.


0

Uygulamalardan biri, daha önce işaretlemediyseniz, her zaman sıfır denetimini gerçekleştirmektir; yani giriş A () işlevinden B () işlevine geçiriliyorsa ve A () işaretçiyi zaten doğruladıysa ve B () 'nin başka bir yere çağrılmadığından eminseniz, B () A () verileri temizledi.


1
... 6 ay içinde birisi gelip B () 'yi çağıran bir kod daha ekleyene kadar (muhtemelen B ()' yi yazan kişinin NULL'ları mutlaka kontrol ettiğini varsayarak ). O zaman mahvoldun, değil mi? Temel kural - bir işleve giriş için geçersiz bir koşul varsa, giriş işlevin denetiminin dışında olduğundan denetleyin.
Maximus Minimus

@ mh01 Sadece rastgele kod parçalamak (yani varsayımlarda bulunmak ve belgeleri okuma değil), o zaman ekstra NULLkontroller çok şey yapacağını sanmıyorum. Bir düşünün: şimdi B()kontrol ediyor NULLve ... ne yapıyor? Dönüş -1? Arayan kişi kontrol etmezse, telefonla NULLilgileneceklerine ne kadar güvenebilirsiniz?-1 dönüş değeri davasıyla ?
detly

1
Arayanların sorumluluğu budur. Verdiğiniz herhangi bir keyfi / bilinmeyen / potansiyel olarak doğrulanmamış girdilere güvenmemek de dahil olmak üzere kendi sorumluluğunuzla ilgilenirsiniz. Aksi takdirde şehir dışına çıkıyorsunuz. Arayan kişi kontrol etmezse, arayan kişi vidalanır; kontrol ettiniz, kendi kıçınız örtülü, arayan kişiyi kim yazdıysa en azından bir şeyler doğru yaptığınızı söyleyebilirsiniz.
Maximus Minimus
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.