printf with std :: string?

157

Rozumiem, że stringjest to element członkowski stdprzestrzeni nazw, więc dlaczego ma miejsce następujący przebieg?

#include <iostream>

int main()
{
    using namespace std;

    string myString = "Press ENTER to quit program!";
    cout << "Come up and C++ me some time." << endl;
    printf("Follow this command: %s", myString);
    cin.get();

    return 0;
}

wprowadź opis obrazu tutaj

Za każdym razem, gdy program jest uruchamiany, myStringwyświetla pozornie losowy ciąg 3 znaków, tak jak na powyższym wyjściu.

TheDarkIn1978
źródło
8
Chciałem tylko powiedzieć, że wiele osób krytykuje tę książkę. Co mogę zrozumieć, ponieważ nie ma zbyt wiele na temat programowania obiektowego, ale nie sądzę, że jest tak złe, jak twierdzą ludzie.
Jesse Good
ouf! cóż, dobrze jest o tym pamiętać, kiedy będę czytał książkę. Jestem pewien, że nie będzie to jedyna książka C ++, którą przeczytam w ciągu najbliższego roku, więc mam nadzieję, że nie zrobi zbyt wiele
szkód
Użycie najwyższego ostrzeżenia kompilatora byłoby odpowiedzią na twoje pytanie - podczas kompilacji z gcc. Jak radzi sobie z tym MSVC - nie wiem.
Peter VARGA,

Odpowiedzi:

237

Kompiluje się, ponieważ printfnie jest bezpieczny dla typów, ponieważ używa argumentów zmiennych w sensie C 1 . printfnie ma opcji dla std::string, tylko łańcuch w stylu C. Używanie czegoś innego zamiast tego, czego się oczekuje, z pewnością nie przyniesie oczekiwanych rezultatów. To właściwie nieokreślone zachowanie, więc wszystko może się zdarzyć.

Najłatwiejszym sposobem rozwiązania tego problemu, ponieważ używasz C ++, jest wydrukowanie go normalnie std::cout, ponieważ std::stringobsługuje to poprzez przeciążenie operatorów:

std::cout << "Follow this command: " << myString;

Jeśli z jakiegoś powodu musisz wyodrębnić napis w stylu C, możesz użyć c_str()metody of, std::stringaby uzyskać ciąg const char *zakończony znakiem null. Na podstawie twojego przykładu:

#include <iostream>
#include <string>
#include <stdio.h>

int main()
{
    using namespace std;

    string myString = "Press ENTER to quit program!";
    cout << "Come up and C++ me some time." << endl;
    printf("Follow this command: %s", myString.c_str()); //note the use of c_str
    cin.get();

    return 0;
}

Jeśli potrzebujesz funkcji, która jest podobna printf, ale bezpieczna, spójrz na różne szablony (C ++ 11, obsługiwane we wszystkich głównych kompilatorach od MSVC12). Możesz znaleźć przykład jednego tutaj . Nie ma nic, co wiem o takiej implementacji w standardowej bibliotece, ale konkretnie może istnieć w Boost boost::format.


[1]: Oznacza to, że możesz przekazać dowolną liczbę argumentów, ale funkcja polega na tym, że podasz jej liczbę i typy tych argumentów. W przypadku printf, oznacza to ciąg z zakodowanymi informacjami o typie, takimi jak %dznaczenie int. Jeśli kłamiesz na temat typu lub liczby, funkcja nie ma standardowego sposobu poznania, chociaż niektóre kompilatory mają możliwość sprawdzania i ostrzegania, gdy kłamiesz.

chris
źródło
@MooingDuck, dobra uwaga. To jest w odpowiedzi Jerry'ego, ale będąc akceptowaną odpowiedzią, ludzie widzą to i mogą odejść, zanim zobaczą innych. Dodałem tę opcję, aby być pierwszym widzianym i zalecanym rozwiązaniem.
chris
43

Proszę nie używać printf("%s", your_string.c_str());

Użyj cout << your_string;zamiast tego. Krótkie, proste i bezpieczne. W rzeczywistości, pisząc w C ++, generalnie chcesz printfcałkowicie tego uniknąć - jest to pozostałość po C, która jest rzadko potrzebna lub przydatna w C ++.

Jeśli chodzi o to, dlaczego powinieneś używać coutzamiast printf, powodów jest wiele. Oto kilka z najbardziej oczywistych:

  1. Jak pokazuje pytanie, printfnie jest bezpieczny. Jeśli typ, który przekazujesz różni się od tego podanego w specyfikatorze konwersji, printfspróbuje użyć wszystkiego, co znajdzie na stosie, tak jakby był to określony typ, dając niezdefiniowane zachowanie. Niektóre kompilatory mogą ostrzec o tym w pewnych okolicznościach, ale niektóre kompilatory nie mogą / nie chcą w ogóle, a żaden nie może w każdych okolicznościach.
  2. printfnie jest rozszerzalny. Możesz przekazać do niego tylko typy pierwotne. Zbiór specyfikatorów konwersji, który rozumie, jest zakodowany na stałe w swojej implementacji i nie ma możliwości dodania więcej / innych. Większość dobrze napisanych C ++ powinno używać tych typów głównie do implementacji typów zorientowanych na rozwiązywany problem.
  3. To znacznie utrudnia porządne formatowanie. Na przykład, gdy drukujesz liczby do przeczytania, zazwyczaj chcesz wstawiać separatory tysięcy co kilka cyfr. Dokładna liczba cyfr i znaków używanych jako separatory jest różna, ale coutobejmuje to również. Na przykład:

    std::locale loc("");
    std::cout.imbue(loc);
    
    std::cout << 123456.78;

    Bezimienne ustawienia regionalne („”) wybierają ustawienia narodowe na podstawie konfiguracji użytkownika. Dlatego na moim komputerze (skonfigurowanym dla języka angielskiego w USA) jest to drukowane jako 123,456.78. Dla kogoś, kto ma swój komputer skonfigurowany dla (powiedzmy) Niemiec, wydrukowałby coś takiego 123.456,78. Dla kogoś, kto skonfigurowałby go dla Indii, wydrukowałby go jako 1,23,456.78(i oczywiście jest wiele innych). Z printfotrzymuję dokładnie jeden wynik: 123456.78. Jest spójny, ale zawsze jest zły dla wszystkich i wszędzie. Zasadniczo jedynym sposobem obejścia tego jest oddzielne formatowanie, a następnie przekazanie wyniku w postaci ciągu znaków printf, ponieważ printfsamo po prostu nie wykona zadania poprawnie.

  4. Chociaż są dość zwarte, printfciągi formatujące mogą być dość nieczytelne. Nawet wśród programistów C, którzy używają printfpraktycznie każdego dnia, przypuszczam, że co najmniej 99% musiałoby sprawdzić, co oznacza #in %#xi czym różni się od tego, co oznacza #in %#f(i tak, mają na myśli zupełnie inne rzeczy ).
Jerry Coffin
źródło
11
@ TheDarkIn1978: Prawdopodobnie zapomniałeś #include <string>. VC ++ ma pewne dziwactwa w swoich nagłówkach, które pozwolą ci zdefiniować ciąg, ale nie wysyłaj go do niego cout, bez dołączania <string>nagłówka.
Jerry Coffin
28
@Jerry: Chcę tylko zaznaczyć, że używanie printf jest DUŻO szybsze niż używanie cout w przypadku dużych ilości danych. Dlatego proszę nie mówić, że jest to bezużyteczne: D
Programista,
7
@Programmer: patrz stackoverflow.com/questions/12044357/… . Podsumowanie: w większości przypadków coutjest to wolniejsze, ponieważ używałeś std::endltam, gdzie nie powinieneś.
Jerry Coffin
29
Typowa arogancja eksperta C ++. Jeśli printf istnieje, dlaczego go nie użyć?
kuroi neko
6
OK, przepraszam za zgryźliwy komentarz. Mimo to printf jest całkiem przydatny do debugowania, a strumienie, choć znacznie potężniejsze, mają tę wadę, że kod nie daje żadnego pojęcia o rzeczywistym wyniku. Dla sformatowanego wyjścia printf jest nadal realną alternatywą i szkoda, że ​​oba systemy nie mogą lepiej współpracować. Tylko moja opinia, oczywiście.
kuroi neko
28

użyj, myString.c_str()jeśli chcesz, aby string ( const char*) podobny do c był używany z printf

dzięki

Alessandro Pezzato
źródło
6

Użyj przykładu std :: printf i c_str ():

std::printf("Follow this command: %s", myString.c_str());
Adel Ben Hamadi
źródło
1

Głównym powodem jest prawdopodobnie to, że łańcuch w C ++ jest strukturą zawierającą wartość bieżącej długości, a nie tylko adres sekwencji znaków zakończonych bajtem 0. Printf i jego krewni spodziewają się znaleźć taką sekwencję, a nie strukturę, i dlatego są zdezorientowani przez łańcuchy C ++.

Mówiąc za siebie, uważam, że printf ma miejsce, którego nie da się łatwo wypełnić funkcjami składniowymi C ++, tak jak struktury tabel w html mają miejsce, które nie może być łatwo wypełnione przez elementy div. Jak pisał później Dykstra o goto, nie miał zamiaru zakładać religii i tak naprawdę argumentował tylko przeciwko używaniu jej jako głupoty do zrekompensowania źle zaprojektowanego kodu.

Byłoby całkiem fajnie, gdyby projekt GNU dodał rodzinę printf do ich rozszerzeń g ++.

MMacD
źródło
1

Printf jest całkiem niezły w użyciu, jeśli rozmiar ma znaczenie. Oznacza to, że jeśli uruchamiasz program, w którym problem dotyczy pamięci, printf jest w rzeczywistości bardzo dobrym i słabo ocenianym rozwiązaniem. Cout zasadniczo przesuwa bity, aby zrobić miejsce na łańcuch, podczas gdy printf po prostu pobiera jakieś parametry i wyświetla je na ekranie. Gdybyś miał skompilować prosty program hello world, printf byłby w stanie skompilować go w mniej niż 60 000 bitów, w przeciwieństwie do cout, kompilacja zajęłaby ponad 1 milion bitów.

W twojej sytuacji id sugeruje użycie cout tylko dlatego, że jest znacznie wygodniejszy w użyciu. Chociaż uważałbym, że printf to coś, co warto wiedzieć.

Howard Howard
źródło
1

printfakceptuje zmienną liczbę argumentów. Mogą mieć tylko typy zwykłych starych danych (POD). Kod, który przekazuje coś innego niż POD, printftylko kompiluje się, ponieważ kompilator zakłada, że ​​masz właściwy format. %soznacza, że ​​odpowiedni argument ma być wskaźnikiem do a char. W twoim przypadku tak std::stringnie jest const char*. printfnie wie o tym, ponieważ typ argumentu ginie i powinien zostać przywrócony z parametru format. Podczas obracania tegostd::string argument wconst char* wynikowy wskaźnik wskaże na jakiś nieistotny region pamięci zamiast żądanego ciągu C. Z tego powodu Twój kod wyświetla bełkot.

Chociaż printfjest to doskonały wybór do drukowania sformatowanego tekstu (szczególnie jeśli zamierzasz mieć dopełnienie), może być niebezpieczny, jeśli nie włączyłeś ostrzeżeń kompilatora. Zawsze włączaj ostrzeżenia, ponieważ takie błędy można łatwo uniknąć. Nie ma powodu, aby stosować niezdarny std::coutmechanizm, jeśli printfrodzina może wykonać to samo zadanie w znacznie szybszy i ładniejszy sposób. Po prostu upewnij się, że włączyłeś wszystkie ostrzeżenia ( -Wall -Wextra) i wszystko będzie dobrze. W przypadku korzystania z własnej niestandardowej printfimplementacji należy zadeklarować ją za pomocą __attribute__mechanizmu, który umożliwia kompilatorowi sprawdzenie ciągu formatu z podanymi parametrami .

Hiena
źródło