Co sprawia, że ​​użycie wskaźników jest nieprzewidywalne?

108

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.

trungnt
źródło
2
Czy na pewno poprawnie odtworzyłeś kod profesora? Chociaż formalnie można argumentować, że ten program może powodować „nieprzewidywalne” zachowanie, nie ma to sensu. I wątpię, czy jakikolwiek profesor użyłby czegoś tak ezoterycznego, aby zilustrować uczniom „nieprzewidywalne”.
AnT
1
@Lightness Races in Orbit: Kompilatory mogą „akceptować” źle sformułowany kod po wydaniu wymaganych komunikatów diagnostycznych. Jednak specyfikacja języka nie definiuje zachowania kodu. To znaczy z powodu błędu w inicjalizacji sprogramu, jeśli zostanie zaakceptowany przez jakiś kompilator, formalnie zachowuje się nieprzewidywalnie.
AnT
2
@TheParamagneticCroissant: Nie. Inicjalizacja jest źle ukształtowana w dzisiejszych czasach.
Wyścigi lekkości na orbicie
2
@The Paramagnetic Croissant: Jak powiedziałem powyżej, język nie wymaga źle sformułowanego kodu, aby „nie skompilować”. Kompilatory są po prostu wymagane do wydania diagnostyki. Następnie mogą kontynuować i „pomyślnie” kompilować kod. Jednak zachowanie takiego kodu nie jest zdefiniowane w specyfikacji języka.
AnT
2
Bardzo chciałbym wiedzieć, jaką odpowiedź udzielił ci twój profesor.
Daniël W. Crompton

Odpowiedzi:

125

Zachowanie programu nie istnieje, ponieważ jest źle sformułowane.

char* s = "My String";

To jest nielegalne. Przed 2011 r. Był przestarzały przez 12 lat.

Prawidłowa linia to:

const char* s = "My String";

Poza tym program jest w porządku. Twój profesor powinien pić mniej whisky!

Wyścigi lekkości na orbicie
źródło
10
z -pedantic robi to: main.cpp: 6: 16: ostrzeżenie: ISO C ++ zabrania konwertowania stałej łańcuchowej na „char *” [-Wpedantic]
marcinj
17
@black: Nie, fakt, że konwersja jest nielegalna, powoduje, że program jest źle sformułowany. W przeszłości był przestarzały . Nie jesteśmy już w przeszłości.
Wyścigi lekkości na orbicie
17
(Co jest głupie, ponieważ taki był cel 12-letniego wycofania)
Lightness Races in Orbit
17
@black: zachowanie programu, który jest źle sformułowany, nie jest „idealnie zdefiniowane”.
Wyścigi lekkości na orbicie
11
Niezależnie od tego, pytanie dotyczy C ++, a nie jakiejś konkretnej wersji GCC.
Wyścigi lekkości na orbicie
81

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:

char * s = "My String";

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ż charrodzina literałów łańcuchowych taka inicjalizacja była zawsze niedozwolona. Na przykład:

const int arr[] = {1};
int *p = arr; // nope!

Jednak w pre-C ++ 11, dla literałów ciągów, był wyjątek w §4.2 / 2:

Literał łańcuchowy (2.13.4), który nie jest literałem szerokiego łańcucha, można przekonwertować na wartość r typu „ wskaźnik na znak ”; […]. W obu przypadkach wynikiem jest wskaźnik do pierwszego elementu tablicy. Ta konwersja jest brana pod uwagę tylko wtedy, gdy istnieje wyraźny odpowiedni typ docelowy wskaźnika, a nie wtedy, gdy istnieje ogólna potrzeba konwersji z l-wartości na r-wartość. [Uwaga: ta konwersja jest przestarzała . Patrz załącznik D. ]

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:

const int arr[] = {1};
int *p = const_cast<int*>(arr); // OK, technically

Dzięki temu reszta programu jest w porządku, ponieważ nigdy więcej nie dotykasz s. Czytanie utworzonego constobiektu za pomocą constwskaźnika niebędącego wskaźnikiem jest całkowicie OK. Pisanie utworzonego constobiektu za pomocą takiego wskaźnika jest niezdefiniowanym zachowaniem:

std::cout << *p; // fine, prints 1
*p = 5;          // will compile, but undefined behavior, which
                 // certainly qualifies as "unpredictable"

Ponieważ sw 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. Napisz sjednak, 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

Barry
źródło
Myślę, że „nieprzewidywalny” w zamierzeniu profesora oznacza, że ​​nie można użyć standardu do przewidzenia, co kompilator zrobi ze źle sformułowanym kodem (poza wydaniem diagnostyki). Tak, mógłby traktować to tak, jak mówi C ++ 03, że powinien być traktowany, a (ryzykując błędem „Nie jest prawdziwym Szkotem”) zdrowy rozsądek pozwala nam przewidzieć z pewną pewnością, że jest to jedyna rzecz, którą rozsądny autor kompiluje kiedykolwiek zdecyduje, czy kod w ogóle się kompiluje. Z drugiej strony, może potraktować jako sens odwrócenie literału ciągu przed rzutowaniem go na wartość inną niż stała. Standardowy C ++ nie obchodzi.
Steve Jessop
2
@SteveJessop Nie kupuję tej interpretacji. Nie jest to ani niezdefiniowane zachowanie, ani kategoria źle sformułowanego kodu, którą standard określa jako niewymagającą diagnostyki. Jest to proste naruszenie systemu typów, które powinno być bardzo przewidywalne (kompiluje i robi normalne rzeczy w C ++ 03, nie kompiluje się w C ++ 11). Naprawdę nie można używać błędów kompilatora (lub licencji artystycznych), aby zasugerować, że kod jest nieprzewidywalny - w przeciwnym razie cały kod byłby nieprzewidywalny pod względem tautologicznym.
Barry
Nie mówię o błędach kompilatora, mówię o tym, czy standard definiuje zachowanie (jeśli w ogóle) kodu. Podejrzewam, że profesor robi to samo, a „nieprzewidywalny” to tylko zawzięty sposób na powiedzenie, że obecny standard nie definiuje zachowania. W każdym razie wydaje mi się to bardziej prawdopodobne niż to, że profesor błędnie uważa, że ​​jest to dobrze uformowany program o nieokreślonym zachowaniu.
Steve Jessop
1
Nie. Standard nie definiuje zachowania źle sformułowanych programów.
Steve Jessop
1
@supercat: to słuszna uwaga, ale nie sądzę, że jest to główny powód. Myślę, że głównym powodem, dla którego standard nie określa zachowania źle sformułowanych programów, jest to, że kompilatory mogą obsługiwać rozszerzenia języka, dodając składnię, która nie jest dobrze sformułowana (tak jak robi to Objective C). Zezwolenie implementacji na zrobienie totalnego horlika ze sprzątania po nieudanej kompilacji to tylko bonus :-)
Steve Jessop
20

Inne odpowiedzi dotyczyły tego, że ten program jest źle sformułowany w C ++ 11 z powodu przypisania const chartablicy do a char *.

Jednak program był źle sformułowany również przed C ++ 11.

Do operator<<przeciążenia są <ostream>. Wymóg iostreamdołączenia ostreamzostał dodany w C ++ 11.

Historycznie, większość wdrożeń iostreami ostreamtak zawierała , być może dla ułatwienia implementacji lub może w celu zapewnienia lepszej QoI.

Ale byłoby zgodne, gdybyśmy iostreamzdefiniowali tylko ostreamklasę bez definiowania operator<<przeciążeń.

MM
źródło
13

Jedyną nieznacznie błędną rzeczą, jaką widzę w tym programie, jest to, że nie należy przypisywać literału ciągu do zmiennego charwskaźnika, chociaż jest to często akceptowane jako rozszerzenie kompilatora.

W przeciwnym razie ten program wydaje mi się dobrze zdefiniowany:

  • Reguły, które dyktują, w jaki sposób tablice znaków stają się wskaźnikami znaków, gdy są przekazywane jako parametry (takie jak with cout << s2), są dobrze zdefiniowane.
  • Tablica jest zakończona znakiem null, co jest warunkiem dla operator<<znaku char*(lub a const char*).
  • #include <iostream>obejmuje <ostream>, co z kolei definiuje operator<<(ostream&, const char*), więc wszystko wydaje się być na swoim miejscu.
zneak
źródło
12

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ł.

Graham
źródło
10

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

supercat
źródło