Obecnie uczę się wskazówek, a mój profesor podał ten fragment kodu jako przykład:
//We cannot predict the behavior of this program!
#include <iostream>
using namespace std;
int main()
{
char * s = "My String";
char s2[] = {'a', 'b', 'c', '\0'};
cout << s2 << endl;
return 0;
}
W komentarzach napisał, że nie możemy przewidzieć zachowania programu. Co dokładnie sprawia, że jest to nieprzewidywalne? Nie widzę w tym nic złego.
s
programu, jeśli zostanie zaakceptowany przez jakiś kompilator, formalnie zachowuje się nieprzewidywalnie.Odpowiedzi:
Zachowanie programu nie istnieje, ponieważ jest źle sformułowane.
To jest nielegalne. Przed 2011 r. Był przestarzały przez 12 lat.
Prawidłowa linia to:
Poza tym program jest w porządku. Twój profesor powinien pić mniej whisky!
źródło
Odpowiedź brzmi: to zależy od tego, z jakim standardem C ++ kompilujesz. Cały kod jest doskonale sformułowany we wszystkich standardach ‡ z wyjątkiem tego wiersza:
Teraz literał ciągu ma typ
const char[10]
i próbujemy zainicjować do niego wskaźnik niebędący stałym. Dla wszystkich typów innych niżchar
rodzina literałów łańcuchowych taka inicjalizacja była zawsze niedozwolona. Na przykład:Jednak w pre-C ++ 11, dla literałów ciągów, był wyjątek w §4.2 / 2:
Tak więc w C ++ 03 kod jest całkowicie w porządku (chociaż jest przestarzały) i ma jasne, przewidywalne zachowanie.
W C ++ 11 ten blok nie istnieje - nie ma takiego wyjątku dla literałów ciągów konwertowanych na
char*
, więc kod jest tak samo źle sformułowany, jakint*
przykład, który właśnie podałem. Kompilator jest zobowiązany do wydania diagnostyki, a najlepiej w przypadkach takich jak ten, które są wyraźnym naruszeniem systemu typu C ++, oczekiwalibyśmy, że dobry kompilator nie tylko będzie zgodny w tym zakresie (np. Wydając ostrzeżenie), ale zawiedzie wprost.Idealnie byłoby, gdyby kod nie był kompilowany - ale działa zarówno na gcc, jak i clang (zakładam, że prawdopodobnie istnieje wiele kodu, który zostałby zepsuty z niewielkim zyskiem, pomimo tego, że ten typ dziury w systemie jest przestarzały przez ponad dekadę). Kod jest źle sformułowany, dlatego nie ma sensu rozważać, jakie może być zachowanie kodu. Biorąc jednak pod uwagę ten konkretny przypadek i historię wcześniejszego dopuszczania go, nie uważam, aby interpretowanie powstałego kodu tak, jakby był to dorozumiany
const_cast
, był nierozsądnym naciąganiem , na przykład:Dzięki temu reszta programu jest w porządku, ponieważ nigdy więcej nie dotykasz
s
. Czytanie utworzonegoconst
obiektu za pomocąconst
wskaźnika niebędącego wskaźnikiem jest całkowicie OK. Pisanie utworzonegoconst
obiektu za pomocą takiego wskaźnika jest niezdefiniowanym zachowaniem:Ponieważ
s
w kodzie nie ma żadnych modyfikacji , program działa dobrze w C ++ 03, nie powinien się skompilować w C ++ 11, ale i tak robi - a biorąc pod uwagę, że kompilatory na to pozwalają, nadal nie ma w nim niezdefiniowanego zachowania † . Biorąc pod uwagę fakt, że kompilatory nadal [niepoprawnie] interpretują reguły C ++ 03, nie widzę niczego, co prowadziłoby do „nieprzewidywalnego” zachowania. Napiszs
jednak, a wszystkie zakłady są wyłączone. Zarówno w C ++ 03, jak i C ++ 11.† Chociaż, ponownie, z definicji źle sformułowany kod nie daje żadnych oczekiwań dotyczących rozsądnego zachowania
‡ Z wyjątkiem, że nie, zobacz odpowiedź Matta McNabba
źródło
Inne odpowiedzi dotyczyły tego, że ten program jest źle sformułowany w C ++ 11 z powodu przypisania
const char
tablicy do achar *
.Jednak program był źle sformułowany również przed C ++ 11.
Do
operator<<
przeciążenia są<ostream>
. Wymógiostream
dołączeniaostream
został dodany w C ++ 11.Historycznie, większość wdrożeń
iostream
iostream
tak zawierała , być może dla ułatwienia implementacji lub może w celu zapewnienia lepszej QoI.Ale byłoby zgodne, gdybyśmy
iostream
zdefiniowali tylkoostream
klasę bez definiowaniaoperator<<
przeciążeń.źródło
Jedyną nieznacznie błędną rzeczą, jaką widzę w tym programie, jest to, że nie należy przypisywać literału ciągu do zmiennego
char
wskaźnika, chociaż jest to często akceptowane jako rozszerzenie kompilatora.W przeciwnym razie ten program wydaje mi się dobrze zdefiniowany:
cout << s2
), są dobrze zdefiniowane.operator<<
znakuchar*
(lub aconst char*
).#include <iostream>
obejmuje<ostream>
, co z kolei definiujeoperator<<(ostream&, const char*)
, więc wszystko wydaje się być na swoim miejscu.źródło
Nie możesz przewidzieć zachowania kompilatora z powodów wymienionych powyżej. ( Powinien się nie skompilować, ale może nie.)
Jeśli kompilacja powiedzie się, zachowanie jest dobrze zdefiniowane. Z pewnością możesz przewidzieć zachowanie programu.
Jeśli się nie skompiluje, nie ma programu. W języku kompilowanym program jest plikiem wykonywalnym, a nie kodem źródłowym. Jeśli nie masz pliku wykonywalnego, nie masz programu i nie możesz mówić o zachowaniu czegoś, co nie istnieje.
Więc powiedziałbym, że stwierdzenie twojego profesora jest błędne. Nie można przewidzieć zachowania kompilatora w obliczu tego kodu, ale różni się to od zachowania programu . Więc jeśli chce wybierać gnidy, lepiej upewnij się, że ma rację. Albo, oczywiście, mogłeś go źle zacytować i pomyłka tkwi w twoim tłumaczeniu tego, co powiedział.
źródło
Jak zauważyli inni, kod jest nielegalny w C ++ 11, chociaż był ważny we wcześniejszych wersjach. W związku z tym kompilator dla C ++ 11 jest wymagany do wydania co najmniej jednej diagnostyki, ale zachowanie kompilatora lub pozostałej części systemu kompilacji jest nieokreślone poza tym. Nic w standardzie nie zabraniałoby kompilatorowi gwałtownego zakończenia w odpowiedzi na błąd, pozostawiając częściowo zapisany plik obiektowy, który konsolidator mógłby uważać za prawidłowy, dając zepsuty plik wykonywalny.
Chociaż dobry kompilator powinien zawsze upewnić się przed zakończeniem pracy, że każdy plik obiektowy, który ma zostać utworzony, będzie ważny, nieistniejący lub rozpoznawalny jako nieprawidłowy, takie problemy nie podlegają jurysdykcji normy. Chociaż w przeszłości istniały (i mogą nadal istnieć) platformy, na których nieudana kompilacja może skutkować prawnie wyglądającymi plikami wykonywalnymi, które po załadowaniu ulegają awarii w dowolny sposób (i musiałem pracować z systemami, w których błędy łączy często miały takie zachowanie) , Nie powiedziałbym, że konsekwencje błędów składniowych są generalnie nieprzewidywalne. W dobrym systemie próba kompilacji generuje zwykle albo plik wykonywalny, który kompilator dołoży wszelkich starań, aby wygenerować kod, albo w ogóle nie da pliku wykonywalnego. Niektóre systemy pozostawiają stary plik wykonywalny po nieudanej kompilacji,
Osobiście wolałbym, aby systemy dyskowe zmieniły nazwę pliku wyjściowego, aby umożliwić rzadkie sytuacje, w których ten plik wykonywalny byłby przydatny, jednocześnie unikając zamieszania, które może wynikać z błędnego przekonania, że ktoś uruchamia nowy kod, oraz do programowania osadzonego systemy, aby umożliwić programiście określenie dla każdego projektu programu, który powinien zostać załadowany, jeśli poprawny plik wykonywalny nie jest dostępny pod normalną nazwą [najlepiej coś, co bezpiecznie wskazuje na brak użytecznego programu]. Zestaw narzędzi systemów wbudowanych generalnie nie miałby możliwości dowiedzenia się, co taki program powinien robić, ale w wielu przypadkach ktoś piszący „prawdziwy” kod systemu będzie miał dostęp do kodu testowego sprzętu, który można łatwo dostosować do cel, powód. Nie wiem, czy widziałem zmianę nazwy, ale
źródło