Obecnie pracuję nad biblioteką napisaną w C. Wiele funkcji tej biblioteki oczekuje łańcucha jako char*
lub const char*
w swoich argumentach. Zacząłem od tych funkcji, które zawsze oczekiwały długości łańcucha, size_t
aby nie było wymagane zakończenie zerowe. Jednak podczas pisania testów spowodowało to częste korzystanie z strlen()
:
const char* string = "Ugh, strlen is tedious";
libFunction(string, strlen(string));
Ufanie użytkownikowi, że przekaże poprawnie zakończone ciągi, prowadziłoby do mniej bezpiecznego, ale bardziej zwięzłego i (moim zdaniem) czytelnego kodu:
libFunction("I hope there's a null-terminator there!");
Jaka jest tutaj rozsądna praktyka? Czy interfejs API jest bardziej skomplikowany w użyciu, ale zmusić użytkownika do przemyślenia jego danych wejściowych, lub udokumentować wymaganie łańcucha zakończonego znakiem zerowym i zaufać programowi wywołującemu?
CreateFile
przyjmowanieLPTCSTR lpFileName
parametru jako danych wejściowych. Od osoby dzwoniącej nie oczekuje się długości ciągu. W rzeczywistości użycie ciągów zakończonych przez NUL jest tak zakorzenione, że dokumentacja nawet nie wspomina, że nazwa pliku musi być zakończona przez NUL (ale oczywiście tak musi być).LPSTR
typ mówi, że łańcuchy mogą być zakończone NUL, a jeśli nie , będzie to wskazane w powiązanej specyfikacji. Tak więc, chyba że wyraźnie zaznaczono inaczej, oczekuje się, że takie łańcuchy w Win32 będą kończyć się na NUL.StringCbCat
na przykład tylko miejsce docelowe ma maksymalny bufor, co ma sens. Źródłem jest nadal zwykłym znakiem NUL C ciąg. Być może mógłbyś poprawić swoją odpowiedź, wyjaśniając różnicę między parametrem wejściowym a parametrem wyjściowym . Parametry wyjściowe powinny zawsze mieć maksymalną długość bufora; parametry wejściowe są zwykle zakończone NUL (są wyjątki, ale z mojego doświadczenia rzadko).W języku C idiomem jest to, że ciągi znaków są zakończone NUL, więc sensowne jest przestrzeganie powszechnej praktyki - w rzeczywistości jest stosunkowo mało prawdopodobne, aby użytkownicy biblioteki mieli ciągi nie zakończone NUL (ponieważ wymagają one dodatkowej pracy do wydrukowania używając printf i używaj w innym kontekście). Używanie dowolnego innego ciągu jest nienaturalne i prawdopodobnie stosunkowo rzadkie.
Ponadto, w tych okolicznościach, twoje testowanie wygląda dla mnie trochę dziwnie, ponieważ do poprawnej pracy (przy użyciu strlen), w pierwszej kolejności zakładasz łańcuch zakończony znakiem NUL. Powinieneś testować ciągi znaków nie zakończone NUL, jeśli zamierzasz z nimi pracować w bibliotece.
źródło
Twój argument „bezpieczeństwa” tak naprawdę nie ma zastosowania. Jeśli nie ufasz, że użytkownik poda ci ciąg zakończony zerem, gdy to jest to, co udokumentowałeś (i co jest „normą” dla zwykłego C), nie możesz tak naprawdę ufać długości, którą ci daje (co zrobi prawdopodobnie skorzystasz z tego,
strlen
co robisz, jeśli nie mają go pod ręką, a to się nie powiedzie, jeśli „ciąg” nie był ciągiem w pierwszej kolejności.Istnieją jednak uzasadnione powody, aby wymagać długości: jeśli chcesz, aby twoje funkcje działały na podciągach, być może znacznie łatwiej (i wydajniej) jest podać długość niż pozwolić użytkownikowi na wykonanie magii kopiowania tam iz powrotem, aby uzyskać bajt zerowy we właściwym miejscu (i ryzykuj po drodze błędy popełniane po drodze).
Możliwość obsługi kodowań, w których bajty zerowe nie są zakończeniami, lub obsługa łańcuchów z osadzonymi wartościami zerowymi (celowo) może być przydatna w niektórych okolicznościach (zależy od tego, co dokładnie robią twoje funkcje).
Przydaje się również możliwość obsługi danych, które nie są zakończone zerem (tablice o stałej długości).
Krótko mówiąc: zależy od tego, co robisz w bibliotece i jakiego rodzaju danych oczekujesz od użytkowników.
Być może wiąże się to również z aspektem wydajności. Jeśli Twoja funkcja musi znać długość łańcucha z wyprzedzeniem, a oczekujesz, że użytkownicy przynajmniej zwykle już znają tę informację, przekazanie jej (a nie jej obliczenie) może ogolić kilka cykli.
Ale jeśli twoja biblioteka oczekuje zwykłych zwykłych ciągów tekstowych ASCII, a nie masz rozdzierających ograniczeń wydajności i bardzo dobrego zrozumienia, w jaki sposób użytkownicy będą wchodzić w interakcje z twoją biblioteką, dodanie parametru długości nie wydaje się dobrym pomysłem. Jeśli łańcuch nie zostanie poprawnie zakończony, szanse na to, że parametr długości będzie równie fałszywy. Nie sądzę, że zyskasz na tym dużo.
źródło
Nie. Ciągi znaków są z definicji zawsze zakończone zerem, długość ciągu jest zbędna.
Dane znakowe nie zakończone znakiem null nigdy nie powinny być nazywane „ciągiem”. Przetwarzanie go (i wyrzucanie długości) powinno zwykle być zawarte w bibliotece, a nie w części interfejsu API. Wymaganie długości jako parametru tylko w celu uniknięcia pojedynczych wywołań strlen () jest prawdopodobnie przedwczesną optymalizacją.
Ufanie dzwoniącemu funkcji API nie jest niebezpieczne ; niezdefiniowane zachowanie jest całkowicie poprawne, jeśli nie są spełnione udokumentowane warunki wstępne.
Oczywiście dobrze zaprojektowany interfejs API nie powinien zawierać pułapek i powinien ułatwiać prawidłowe korzystanie z niego. A to oznacza po prostu, że powinno to być tak proste i jednoznaczne, jak to możliwe, unikając redundancji i przestrzegając konwencji języka.
źródło
Zawsze powinieneś zachować swoją długość. Po pierwsze, użytkownicy mogą chcieć zawierać w nich wartości NULL. Po drugie, nie zapominaj, że
strlen
jest to O (N) i wymaga dotknięcia całej pamięci podręcznej bye bye cache. Po trzecie, łatwiej jest ominąć podzbiory - na przykład mogą dać mniej niż rzeczywista długość.źródło
strlen
w teście pętli.)Należy rozróżnić między przekazywaniem ciągu a przekazywaniem bufora .
W C łańcuchy są tradycyjnie zakończone NUL. Jest całkowicie uzasadnione, aby się tego spodziewać. Dlatego zwykle nie ma potrzeby omijania długości łańcucha; w
strlen
razie potrzeby można to obliczyć .Podczas przekazywania bufora , zwłaszcza takiego, do którego jest napisane, absolutnie powinieneś podać rozmiar bufora. W przypadku bufora docelowego pozwala to odbiorcy upewnić się, że nie przepełnia bufora. W przypadku bufora wejściowego pozwala to odbiorcy uniknąć odczytu poza koniec, szczególnie jeśli bufor wejściowy zawiera dowolne dane pochodzące z niezaufanego źródła.
Być może istnieje pewne zamieszanie, ponieważ zarówno łańcuchy, jak i bufory mogą być,
char*
i ponieważ wiele funkcji łańcuchowych generuje nowe łańcuchy, pisząc do buforów docelowych. Niektóre osoby dochodzą następnie do wniosku, że funkcje łańcuchowe powinny mieć długości łańcuchowe. Jest to jednak niedokładny wniosek. Praktyka dołączania rozmiaru z buforem (niezależnie od tego, czy bufor ten ma być używany do łańcuchów, tablic liczb całkowitych, struktur itp.) Jest bardziej przydatną i bardziej ogólną mantrą.(W przypadku odczytu ciągu z niezaufanego źródła (np. Gniazda sieciowego) ważne jest podanie długości, ponieważ dane wejściowe mogą nie być zakończone NUL. Nie należy jednak traktować danych wejściowych jako ciągów. powinien traktować go jako dowolny bufor danych, który może zawierać ciąg znaków (ale nie wiesz, dopóki go nie zweryfikujesz), więc nadal jest to zgodne z zasadą, że bufory powinny mieć powiązane rozmiary i łańcuchy ich nie potrzebują.)
źródło
Jeśli funkcje są używane głównie z literałami ciągów, ból związany z jawnymi długościami można zminimalizować poprzez zdefiniowanie niektórych makr. Na przykład, biorąc pod uwagę funkcję API:
można zdefiniować makro:
a następnie wywołaj go, jak pokazano na:
Chociaż może być możliwe wymyślenie „kreatywnych” rzeczy do przekazania makra, które się skompiluje, ale tak naprawdę nie zadziała, użycie
""
po obu stronach łańcucha w ramach oceny „sizeof” powinno wyłapać przypadkowe próby użycia znaku wskaźniki inne niż rozłożone literały łańcuchowe [w przypadku ich braku""
próba przekazania wskaźnika znaku błędnie dałaby długość jako rozmiar wskaźnika minus jeden.Alternatywnym podejściem w C99 byłoby zdefiniowanie typu struktury „wskaźnik i długość” oraz zdefiniowanie makra, które konwertuje literał łańcuchowy na literał złożony tego typu struktury. Na przykład:
Zauważ, że jeśli zastosujemy takie podejście, należy przekazywać takie struktury pod względem wartości, a nie przekazywać ich adresy. W przeciwnym razie coś takiego:
może zawieść, ponieważ czas życia literałów złożonych kończy się na końcu ich załączających instrukcji.
źródło