Po pierwsze, zauważę, że chociaż wspominam tu tylko o „C”, to samo dotyczy również w równym stopniu C ++.
Komentarz dotyczący Godela był częściowo (ale tylko częściowo) trafny.
Kiedy się do tego zabierasz, niezdefiniowane zachowanie w standardach C w dużej mierze po prostu wskazuje granicę między tym, co standard próbuje zdefiniować, a tym, czego nie określa.
Twierdzenia Godela (są dwa) w zasadzie mówią, że niemożliwe jest zdefiniowanie systemu matematycznego, który można udowodnić (według własnych zasad), aby był zarówno kompletny, jak i spójny. Możesz stworzyć swoje reguły, aby były kompletne (przypadek, którym zajmował się, były „normalnymi” regułami dla liczb naturalnych), albo możesz umożliwić udowodnienie jego spójności, ale nie możesz mieć obu.
W przypadku czegoś takiego jak C nie ma to bezpośredniego zastosowania - w większości przypadków „sprawdzalność” kompletności lub spójności systemu nie jest priorytetem dla większości projektantów języków. Jednocześnie tak, prawdopodobnie wpłynęło to na nich (przynajmniej w pewnym stopniu), wiedząc, że określenie „idealnego” systemu jest niemożliwe do udowodnienia - takiego, który jest kompletnie i spójny. Świadomość, że coś takiego jest niemożliwe, mogła nieco ułatwić cofnięcie się, odetchnąć i zdecydować o granicach tego, co spróbowaliby zdefiniować.
Ryzykując (jeszcze raz) oskarżenie o arogancję, scharakteryzuję standard C jako podlegający (częściowo) dwóm podstawowym ideom:
- Język powinien obsługiwać jak najszerszy zakres sprzętu (najlepiej cały „rozsądny” sprzęt do rozsądnego dolnego limitu).
- Język powinien obsługiwać pisanie możliwie szerokiej gamy oprogramowania dla danego środowiska.
Pierwszy oznacza, że jeśli ktoś zdefiniuje nowy procesor, powinno być możliwe zapewnienie do tego celu dobrej, solidnej, użytecznej implementacji języka C, o ile projekt będzie co najmniej dość zbliżony do kilku prostych wytycznych - w zasadzie jeśli postępuje zgodnie z ogólną kolejnością modelu von Neumanna i zapewnia przynajmniej pewną rozsądną minimalną ilość pamięci, która powinna wystarczyć, aby umożliwić implementację C. W przypadku implementacji „hostowanej” (takiej, która działa w systemie operacyjnym), musisz obsługiwać pewne pojęcia, które dość ściśle odpowiadają plikom i mieć zestaw znaków z pewnym minimalnym zestawem znaków (wymagane jest 91).
Drugi oznacza, że powinno być możliwe pisanie kodu, który bezpośrednio manipuluje sprzętem, dzięki czemu można pisać takie rzeczy, jak programy ładujące, systemy operacyjne, oprogramowanie wbudowane, które działa bez żadnego systemu operacyjnego itp. Ostatecznie istnieją pewne ograniczenia w tym zakresie, więc prawie każdy praktyczny system operacyjny, moduł ładujący itp. może zawierać co najmniej trochę kodu napisanego w języku asemblera. Podobnie nawet mały wbudowany system może zawierać co najmniej pewien rodzaj wcześniej napisanych procedur bibliotecznych zapewniających dostęp do urządzeń w systemie hosta. Chociaż trudno jest precyzyjnie określić granicę, chodzi o to, aby zależność od takiego kodu była ograniczona do minimum.
Nieokreślone zachowanie w języku wynika w dużej mierze z zamiaru, aby język wspierał te możliwości. Na przykład język pozwala przekonwertować dowolną liczbę całkowitą na wskaźnik i uzyskać dostęp do wszystkiego, co dzieje się pod tym adresem. Standard nie próbuje powiedzieć, co się stanie, kiedy to zrobisz (np. Nawet czytanie z niektórych adresów może mieć widoczne efekty zewnętrzne). W tym samym czasie, nie ma próbę uniemożliwia robi takie rzeczy, bo trzeba do niektórych rodzajów oprogramowania jesteś ma być w stanie napisać w C
Istnieją również nieokreślone zachowania wynikające z innych elementów projektu. Na przykład jednym innym celem C jest obsługa oddzielnej kompilacji. Oznacza to (na przykład), że zamierzone jest „łączenie” elementów za pomocą linkera, który z grubsza podąża za tym, co większość z nas uważa za zwykły model linkera. W szczególności powinna istnieć możliwość łączenia oddzielnie skompilowanych modułów w kompletny program bez znajomości semantyki języka.
Istnieje inny rodzaj niezdefiniowanego zachowania (który jest znacznie bardziej powszechny w C ++ niż C), który występuje po prostu z powodu ograniczeń technologii kompilatora - rzeczy, które w zasadzie wiemy, są błędami i prawdopodobnie chcieliby, aby kompilator diagnozował jako błędy, ale biorąc pod uwagę obecne ograniczenia technologii kompilatora, wątpliwe jest, aby można je było zdiagnozować w każdych okolicznościach. Wiele z nich wynika z innych wymagań, takich jak osobna kompilacja, więc w dużej mierze chodzi o zrównoważenie sprzecznych wymagań, w którym to przypadku komitet zasadniczo zdecydował się na wsparcie większych możliwości, nawet jeśli oznacza to brak diagnozy niektórych możliwych problemów, zamiast ograniczać możliwości, aby zdiagnozować wszystkie możliwe problemy.
Te różnice w umyśle powodują większość różnic między C a czymś takim jak Java lub systemy oparte na CLI Microsoftu. Te ostatnie są dość wyraźnie ograniczone do pracy ze znacznie bardziej ograniczonym zestawem sprzętu lub wymagają oprogramowania do emulacji bardziej specyficznego sprzętu, na który są kierowane. Mają również na celu zapobieganie wszelkim bezpośrednim manipulacjom sprzętowym, zamiast tego wymagają użycia czegoś takiego jak JNI lub P / Invoke (i kodu napisanego w języku C), aby nawet podjąć taką próbę.
Wracając przez chwilę do twierdzeń Godela, możemy narysować coś podobnego: Java i CLI wybrali alternatywę „wewnętrznie spójną”, podczas gdy C wybrał alternatywę „kompletną”. Oczywiście, jest to bardzo szorstki analogia - wątpię ktoś usiłuje formalny dowód zarówno wewnętrznej spójności i kompletności w obu przypadkach. Niemniej jednak ogólne pojęcie dość dobrze pasuje do dokonanych wyborów.