Bir C kütüphanesinin işlevleri her zaman bir dizginin uzunluğunu beklemeli mi?


15

Şu anda C ile yazılmış bir kütüphane üzerinde çalışıyorum. Bu kütüphanenin birçok işlevi argümanlarında char*veya const char*argümanlarında bir dize bekliyor . Ben size_tboş sonlandırma gerekli değildi böylece her zaman dize uzunluğunu bekliyor bu fonksiyonları ile başladı . Bununla birlikte, test yazarken, bu, aşağıdakilerin sık sık kullanılmasına neden oldu strlen():

const char* string = "Ugh, strlen is tedious";
libFunction(string, strlen(string));

Kullanıcının uygun şekilde sonlandırılmış dizeleri geçmesine güvenmek, daha az güvenli, ancak daha özlü ve (bence) okunabilir kodlara yol açacaktır:

libFunction("I hope there's a null-terminator there!");

Peki, burada mantıklı uygulama nedir? API'yi kullanımı daha karmaşık hale getirin, ancak kullanıcıyı girdilerini düşünmeye zorlayın veya boş sonlandırılmış bir dizeye gereksinimi belgelendirin ve arayan kişiye güvenin mi?

Yanıtlar:


4

Kesinlikle ve kesinlikle uzunluğu etrafında taşıyın . Standart C kütüphanesi bu şekilde rezil bir şekilde kırılmıştır, bu da tampon taşmaları ile uğraşırken ağrının sona ermesine neden olmamıştır. Bu yaklaşım, modern derleyicilerin bu tür standart kütüphane işlevlerini kullanırken aslında uyaracak, sızlanacak ve şikayet edecek kadar nefret ve acı çekmenin odak noktasıdır.

O kadar kötü ki, bu soruya bir röportajda rastlarsanız - ve teknik mülakatçınız birkaç yıllık tecrübeye sahip gibi görünüyor - saf zealotry işi indirebilir - eğer alıntı yapabilirseniz gerçekten çok ileri gidebilirsiniz C dizesi sonlandırıcısını arayan API'ları uygulayan birini vurma emsali .

Her şeyin duygularını bir kenara bırakarak, dizenizin sonunda, hem okumada hem de manipüle ederken NULL ile yanlış gidebilecek çok şey var - artı gerçekten derinlemesine savunma gibi modern tasarım konseptlerini doğrudan ihlal ediyor (mutlaka güvenliğe değil, API tasarımına uygulanır). Uzunluğu taşıyan C API örnekleri - ör. Windows API'sı.

Aslında, bu sorun 90'larda bir zamanlar çözüldü, bugün ortaya çıkan fikir birliği, dizelerinize bile dokunmamanız gerektiğidir .

Daha sonra düzenleme : Bu oldukça canlı bir tartışmadır, bu yüzden aşağıda ve yukarıda herkese güvenmekten hoşlanacağınızı ve kütüphane str * işlevlerini kullanmanın output = malloc(strlen(input)); strcpy(output, input);veya gibi klasik şeyleri görene kadar tamam olduğunu ekleyeceğim while(*src) { *dest=transform(*src); dest++; src++; }. Mozart'ın Lacrimosa'yı neredeyse arka planda duyabiliyorum.


1
Arayanın dizelerin uzunluğunu sağlaması gereken Windows API örneğini anlamıyorum. Örneğin, tipik bir Win32 API işlevi gibi CreateFilebir LPTCSTR lpFileNameparametre girdi alır . Arayandan dize uzunluğu beklenmez. Aslında, NUL sonlandırılmış dizelerin kullanımı o kadar kökleşmiş ki, belgeler dosya adının NUL sonlandırılması gerektiğinden bile bahsetmiyor (ama elbette olmalı).
Greg Hewgill

1
Aslında Win32, LPSTRtip dizeleri söylüyor olabilir boş karakter sonlandırmalı ve eğer olmak değil , o ilişkili şartnamede belirtilecektir. Dolayısıyla, aksi özellikle belirtilmedikçe, Win32'deki bu tür dizelerin NUL sonlandırılması beklenir.
Greg Hewgill

Harika bir nokta, ben kesin değildi. Windows NT 3.1'den (90'ların başında) CreateFile ve onun demet civarında olduğunu düşünün; mevcut API (yani, XP SP2'de Strsafe.h'ın piyasaya sürülmesinden bu yana - Microsoft'un genel özürleriyle birlikte), NULL tarafından sonlandırılan tüm öğeleri açıkça reddetti. Microsoft, NULL sonlandırılmış dizeleri kullanmak için gerçekten çok üzüldüğünü ilk kez, aslında VB, COM ve eski WINAPI'yi aynı tekneye getirmek için OLE 2.0 spesifikasyonunda BSTR'yi tanıtmak zorunda kaldıklarında çok daha erken oldu.
vski

1
Hatta içinde StringCbCatörneğin, sadece hedef mantıklı maksimum tampon vardır. Kaynak hala normal NULL-ile-sonlanan Cı dizisidir. Belki bir girdi parametresi ile bir çıktı parametresi arasındaki farkı netleştirerek cevabınızı geliştirebilirsiniz . Çıktı parametreleri her zaman maksimum arabellek uzunluğuna sahip olmalıdır; giriş parametreleri genellikle NUL ile sonlandırılır (istisnalar vardır, ancak deneyimlerime göre nadirdir).
Greg Hewgill

1
Evet. Dizeler, hem JVM / Dalvik hem de .NET CLR'de platform düzeyinde ve diğer birçok dilde değişmez. Şimdiye kadar gidip yerli dünyanın bunu (C ++ 11 standardı) henüz yapamadığını tahmin ediyorum a) miras (gerçekten dizelerinizin bir kısmının değişmez olmasıyla çok fazla kazanmazsınız) ve b ) Bu işi yapmak için gerçekten bir GC'ye ve bir dize tablosuna ihtiyacınız var, C ++ 11'deki kapsamlı ayırıcılar onu tamamen kesemez.
vski

16

C'de deyim, karakter dizelerinin NUL ile sonlandırılmasıdır, bu nedenle ortak uygulamaya uymak mantıklıdır - kütüphanenin kullanıcılarının NUL sonlandırılmamış dizeleri olması nispeten düşüktür (bunlar yazdırmak için ekstra çalışmaya ihtiyaç duyduklarından) printf kullanma ve diğer bağlamlarda kullanma). Başka bir tür ip kullanmak doğal değildir ve muhtemelen nispeten nadirdir.

Ayrıca, koşullar altında, testiniz bana biraz garip görünüyor, çünkü doğru çalışmak (strlen kullanarak), ilk etapta NUL sonlu bir dize varsayıyorsunuz. Kütüphanenizi onlarla çalışmak istiyorsanız NUL sonlandırılmamış dizelerin durumunu test etmelisiniz.


-1, üzgünüm, bu sadece tavsiye edilmez.
vski

Eski günlerde bu her zaman doğru değildi. NULL sonlandırılmamış sabit uzunluklu alanlarda dize verileri koymak ikili protokolleri ile çok çalıştı. Bu gibi durumlarda, uzun süren işlevlerle çalışmak çok elverişliydi. Yine de on yıldır C yapmadım.
Robot Gort

4
@vski, hedef işlev çağırmadan önce kullanıcıyı 'taşma' çağırmaya zorlamak nasıl arabellek taşması sorunları önlemek için? En azından uzunluğu hedef fonksiyon içinde kendiniz kontrol ederseniz, hangi uzunluk duyusunun kullanıldığından emin olabilirsiniz (null terminal dahil veya yok).
Charles E. Grant

@Charles E. Grant: Strsafe.h'deki StringCbCat ve StringCbCatN hakkında yukarıdaki açıklamaya bakın. Sadece bir karakteriniz * varsa ve hiçbir uzunluğu yoksa, gerçekten str * işlevlerini kullanmaktan başka gerçek bir seçeneğiniz yoktur, ancak nokta uzunluk uzunluğunu taşımaktır, böylece str * ve strn * arasında bir seçenek haline gelir. ikincisi tercih edilen fonksiyonlar.
vski

2
@vski Bir dizginin uzunluğundan geçmeye gerek yoktur . Orada olan bir etrafında geçmesine gerek tampon 'in uzunluğu. Tüm arabellekler dizge değildir ve tüm dizeler arabellek değildir.
jamesdlin

10

"Güvenlik" argümanınız gerçekten geçerli değil. Belgelediğiniz şey (ve düz C için "norm" nedir) size boş değerli sonlandırılmış bir dize vermesi konusunda kullanıcıya güvenmiyorsanız, size verdikleri uzunluğa da gerçekten güvenemezsiniz. muhtemelen strleneğer kullanışlı olmadıkça yaptığınız gibi kullanın ve "dize" ilk başta bir dize değilse başarısız olur).

Bununla birlikte, bir uzunluk gerektirmenin geçerli nedenleri vardır: işlevlerinizin alt dizelerde çalışmasını istiyorsanız, bir uzunluğu geçirmek muhtemelen boş bayt almak için kullanıcının bazı kopyalama sihirlerini yapmasını sağlamaktan daha kolaydır (ve verimli). doğru yerde (ve yol boyunca tek tek hataları riske atın).
Boş baytların sonlandırma olmadığı durumlarda kodlamaları işleyebilme veya boş (null) gömülü dizeleri işleyebilme bazı durumlarda yararlı olabilir (işlevlerinizin tam olarak ne yaptığına bağlıdır).
Boş sonlandırılmamış verileri (sabit uzunluklu diziler) işleyebilmek de kullanışlıdır.
Kısacası: kitaplığınızda ne yaptığınıza ve kullanıcılarınızın ne tür verileri işlemesini beklediğinize bağlıdır.

Bunun muhtemelen bir performans yönü de vardır. İşlevinizin dizenin uzunluğunu önceden bilmesi gerekiyorsa ve kullanıcılarınızın en azından genellikle bu bilgileri bilmelerini bekliyorsanız, bunları (sahip olmak değil, hesaplamak yerine) birkaç döngü tıraş edebilir.

Ancak kitaplığınız sıradan düz ASCII metin dizeleri bekliyorsa ve zorlu performans kısıtlamaları ve kullanıcılarınızın kitaplığınızla nasıl etkileşime gireceğine dair çok iyi bir anlayışa sahip değilseniz, bir uzunluk parametresi eklemek iyi bir fikir gibi gelmiyor. Dize düzgün bir şekilde sonlandırılmazsa, length parametresi de sahte gibi olacaktır. Onunla fazla kazanacağını sanmıyorum.


Bu yaklaşıma kesinlikle katılmıyorum. Arayanlara asla güvenmeyin, özellikle bir kütüphane API'sının arkasında, size verdikleri şeyleri sorgulamak ve incelikle başarısız olmak için elinizden gelenin en iyisini yapın. Darleşmiş uzunluğu taşıyın, NULL sonlu dizelerle çalışmak "arayanlarınızla gevşek ve callees'lerinizle sıkı olmak" anlamına gelmez.
vski

2
Çoğunlukla pozisyonunuza katılıyorum , ancak bu uzunluk argümanına çok fazla güveniyorsunuz gibi görünüyor - null sonlandırıcıdan daha güvenilir olması için hiçbir neden yok. Benim konumum bu kütüphanenin ne yaptığına bağlı.
Mat

Dizelerde NULL sonlandırıcı ile değerden geçen uzunluktan daha fazla yanlış gidebilecek çok daha fazlası var. C'de, kişinin uzunluğa güvenmesinin tek nedeni, mantıksız ve pratik olmamaktır - tampon uzunluğunu taşımak iyi bir cevap değildir, alternatifleri göz önünde bulundurarak en iyisidir. Dizelerin (ve genel olarak tamponların) RAD dillerinde düzgün bir şekilde paketlenip kapsüllenmesinin nedenlerinden biridir.
vski

2

Hayır. Dizeler her zaman tanım gereği null sonlandırılır, dize uzunluğu gereksizdir.

Boş değerli sonlandırılmamış karakter verilerine asla "dize" denilmemelidir. Bunu işlemek (ve uzunlukları atmak) genellikle API'nin bir parçası değil, bir kütüphane içinde kapsüllenmelidir. Yalnızca tek strlen () çağrılarından kaçınmak için uzunluğun parametre olarak gerekli olması muhtemelen Erken Optimizasyon olabilir.

Bir API işlevinin arayanına güvenmek güvenli değildir ; belgelenmiş önkoşullar karşılanmazsa tanımsız davranış mükemmeldir.

Tabii ki, iyi tasarlanmış bir API tuzaklar içermemeli ve doğru bir şekilde kullanılmasını kolaylaştırmalıdır. Ve bu, artıklıklardan kaçınarak ve dilin kurallarını izleyerek mümkün olduğunca basit ve anlaşılır olması gerektiği anlamına gelir.


hafıza açısından güvenli, tek iş parçacıklı bir dile geçmediği sürece sadece mükemmel bir şekilde değil, aslında kaçınılmazdır. Daha fazla gerekli kısıtlama bırakmış olabilir ...
Tekilleştirici

1

Her zaman uzunluğunu korumalısın. Birincisi, kullanıcılarınız bunlara NULL eklemek isteyebilir. İkincisi, strlenbunun O (N) olduğunu ve tüm güle güle güle önbelleğine dokunmayı gerektirdiğini unutmayın . Ve üçüncüsü, alt kümelerin etrafından geçmeyi kolaylaştırır - örneğin, gerçek uzunluktan daha az verebilirler.


4
Kütüphane işlevinin dizelerdeki gömülü NULL'larla ilgilenip ilgilenmediği çok iyi belgelenmelidir. C kütüphanesi işlevlerinin çoğu, hangisi önce olursa, NULL veya uzunlukta durur. (Ve yetkin bir şekilde yazılmışsa, uzun sürmeyenler asla strlenbir döngü testinde kullanmazlar .)
Robotu

1

Bir ipin etrafından geçme ile bir tamponun etrafından geçme arasında ayrım yapmalısınız .

C'de, dizeler geleneksel olarak NUL ile sonlandırılır. Bunu beklemek tamamen mantıklı. Bu nedenle genellikle ipin uzunluğundan geçmeye gerek yoktur; strlengerekirse hesaplanabilir .

Bir tamponun etrafında , özellikle de bir tane yazılırken, kesinlikle tampon boyutu boyunca geçmelisiniz. Bir hedef arabelleği için bu, arayanın arabelleğin taşmamasını sağlar. Bir giriş arabelleği için, özellikle giriş arabelleği güvenilir olmayan bir kaynaktan gelen rasgele veriler içeriyorsa, arayanın uçtan sonra okumayı engellemesine izin verir.

Belki de bazı karışıklıklar vardır çünkü hem dizgiler hem de tamponlar olabilir char*ve birçok dize fonksiyonu hedef tamponlarına yazarak yeni dizeler üretir. Bazı insanlar dize işlevlerinin dize uzunlukları alması gerektiği sonucuna varır. Ancak, bu yanlış bir sonuçtur. Bir tampon ile bir boyut ekleme uygulaması (bu tamponun dizeler, tamsayı dizileri, yapılar, herhangi bir şey için kullanılıp kullanılmayacağı) daha kullanışlı ve daha genel bir mantradır.

Giriş boş karakter sonlandırmalı olmayabilir çünkü ((örn bir ağ soket) güvenilmeyen bir kaynaktan gelen bir dize okuma durumunda, bir uzunluk arz etmek önemlidir. Ancak , sen gerektiğini değil bir dize girdi düşünün. Sen keyfi bir bilgi olarak kabul gerektiğini tampon olabilecek bir dizeyi içeren (ama aslında bunu doğrulamak kadar bilmiyorum) bu hala tamponlar boyutları ilişkili olması ve bu dizeleri onlara ihtiyacım yok ilkesini takip böylece.)


Soru ve diğer cevapların tam olarak cevapsız olduğu şey budur.
Blrfl

0

İşlevler esas olarak dize değişmezleriyle kullanılırsa, bazı uzunluklar tanımlanarak açık uzunluklarla uğraşmanın acısı en aza indirilebilir. Örneğin, bir API işlevi verildiğinde:

void use_string(char *string, int length);

bir makro tanımlanabilir:

#define use_strlit(x) use_string(x, sizeof ("" x "")-1)

ve sonra gösterildiği gibi çağırın:

void test(void)
{
  use_strlit("Hello");
}

Derlemek ama aslında işe yaramaz makroyu geçmek için "yaratıcı" şeyler bulmak mümkün olsa da "", "sizeof" değerlendirmesinde dizenin her iki tarafında kullanımı karakter kullanmak için yanlışlıkla girişimleri yakalamak gerekir ayrıştırılmış dize değişmez değerleri dışındaki işaretçiler [bunların yokluğunda, ""bir karakter işaretçisini geçme girişimi hatalı olarak bir işaretçinin boyutu olarak eksi bir boyut verir.

C99'da alternatif bir yaklaşım, bir "işaretçi ve uzunluk" yapı tipi tanımlamak ve bir dize hazır bilgisini bu yapı türünün bileşik bir hazır bilgisine dönüştüren bir makro tanımlamak olacaktır. Örneğin:

struct lstring { char const *ptr; int length; };
#define as_lstring(x) \
  (( struct lstring const) {x, sizeof("" x "")-1})

Eğer kişi böyle bir yaklaşım kullanırsa, bu tür yapıları adreslerini iletmekten ziyade değere göre geçirmelidir. Aksi takdirde:

struct lstring *p;
if (foo)
{
  p = &as_lstring("Hello");
}
else
{
  p = &as_lstring("Goodbye!");
}
use_lstring(p);

bileşik değişmezlerin ömrü, ekli ifadelerinin sonunda sona ereceğinden başarısız olabilir.

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.