Gdy program C jest uruchomiony, dane są przechowywane na stercie lub stosie. Wartości są przechowywane w adresach RAM. Ale co ze wskaźnikami typu (np. int
Lub char
)? Czy są również przechowywane?
Rozważ następujący kod:
char a = 'A';
int x = 4;
Przeczytałem, że A i 4 są tutaj przechowywane w adresach RAM. Ale co a
i x
? Co najbardziej mylące, skąd egzekucja wie, że a
jest char i x
int? Mam na myśli, int
czy char
wspomniano gdzieś w pamięci RAM?
Powiedzmy, że wartość jest przechowywana gdzieś w pamięci RAM jako 10011001; jeśli jestem programem wykonującym kod, to skąd mam wiedzieć, czy ten 10011001 jest a, char
czy int
?
To, czego nie rozumiem, to skąd komputer wie, kiedy odczytuje wartość zmiennej z adresu takiego jak 10001, bez względu na to, czy jest to int
lub char
. Wyobraź sobie, że klikam program o nazwie anyprog.exe
. Kod natychmiast zaczyna działać. Czy ten plik wykonywalny zawiera informacje, czy przechowywane zmienne są typu, int
czy char
?
x
jest char, ale uruchamiany jest kod drukujący char, ponieważ właśnie to wybrał kompilator.Odpowiedzi:
Aby odpowiedzieć na pytanie, które opublikowałeś w kilku komentarzach (które moim zdaniem należy edytować w swoim poście):
Dodajmy do tego trochę kodu. Powiedzmy, że piszesz:
Załóżmy, że jest przechowywany w pamięci RAM:
Pierwsza część to adres, druga część to wartość. Kiedy twój program (który wykonuje się jako kod maszynowy) działa, widzi
0x00010004
tylko wartość0x000000004
. Nie „zna” tego typu danych i nie wie, w jaki sposób „powinien” zostać użyty.Jak więc twój program wymyślił właściwą rzecz? Rozważ ten kod:
Mamy tutaj przeczytanie i napisanie. Gdy program odczytuje
x
z pamięci, znajduje się0x00000004
tam. A twój program wie, jak go dodać0x00000005
. Powodem, dla którego Twój program „wie”, że jest to poprawna operacja, jest to, że kompilator zapewnia poprawność operacji dzięki bezpieczeństwu typu. Twój kompilator już zweryfikował, że możesz dodać4
i5
razem. Kiedy więc uruchamia się twój kod binarny (exe), nie trzeba go weryfikować. Po prostu wykonuje każdy krok na ślepo, zakładając, że wszystko jest w porządku (złe rzeczy zdarzają się, gdy w rzeczywistości nie są w porządku).Inny sposób myślenia o tym jest taki. Dam ci te informacje:
Taki sam format jak poprzednio - adres po lewej, wartość po prawej. Jakiego typu jest wartość? W tym momencie znasz tyle samo informacji o tej wartości, co twój komputer, gdy wykonuje kod. Gdybym kazał ci dodać 12743 do tej wartości, możesz to zrobić. Nie masz pojęcia, jakie będą konsekwencje tej operacji dla całego systemu, ale dodanie dwóch liczb to coś, w czym jesteś naprawdę dobry, więc możesz to zrobić. Czy to sprawia, że wartość jest an
int
? Niekoniecznie - widać tylko 32-bitowe wartości i operator dodawania.Być może pewnym zamieszaniem jest odzyskanie danych. Jeśli mamy:
Skąd komputer wie, że wyświetla się
a
w konsoli? Jest na to wiele kroków. Pierwszym z nich jest przejście doA
lokalizacji w pamięci i odczytanie jej:Wartość szesnastkowa
a
w ASCII wynosi 0x61, więc powyższe może być czymś, co zobaczysz w pamięci. Teraz nasz kod maszynowy zna wartość całkowitą. Skąd wie, że zmienia wartość całkowitą w znak, aby ją wyświetlić? Mówiąc najprościej, kompilator wykonał wszystkie niezbędne kroki, aby dokonać tego przejścia. Ale sam komputer (lub program / exe) nie ma pojęcia, jaki jest typ tych danych. Ta 32-bitowa wartość może być dowolna -int
,char
połowadouble
, wskaźnik, część tablicy, częśćstring
, część instrukcji itp.Oto krótka interakcja Twojego programu (exe) z komputerem / systemem operacyjnym.
Program: chcę zacząć. Potrzebuję 20 MB pamięci.
System operacyjny: znajduje 20 wolnych MB pamięci, które nie są używane, i przekazuje je
(Ważna uwaga jest taka, że może to zwrócić dowolne 20 wolnych MB pamięci, nie muszą nawet być ciągłe. W tym momencie program może teraz działać w pamięci, którą posiada, bez rozmowy z systemem operacyjnym)
Program: Zakładam, że pierwszym miejscem w pamięci jest 32-bitowa zmienna całkowita
x
.(Kompilator upewnia się, że dostęp do innych zmiennych nigdy nie dotknie tego miejsca w pamięci. W systemie nic nie mówi, że pierwszy bajt jest zmienny
x
lub ta zmiennax
jest liczbą całkowitą. Analogia: masz torbę. Mówisz ludziom, że umieścisz w tej torbie tylko żółte kulki. Gdy ktoś później wyciągnie coś z torby, szokujące byłoby wyciągnięcie czegoś niebieskiego lub sześcianu - coś poszło strasznie nie tak. To samo dotyczy komputerów: twój program przyjmuje teraz, że pierwszym miejscem w pamięci jest zmienna x i że jest liczbą całkowitą. Jeśli w tym bajcie pamięci zostanie kiedykolwiek napisane coś innego lub zakłada się, że jest to coś innego - wydarzyło się coś strasznego. Kompilator zapewnia, że takie rzeczy nie się zdarzyło)Program: Teraz napiszę
2
do pierwszych czterech bajtów, w których, jak zakładam,x
jest.Program: Chcę dodać 5 do
x
.Odczytuje wartość X do rejestru tymczasowego
Dodaje 5 do rejestru tymczasowego
Przechowuje wartość rejestru tymczasowego z powrotem w pierwszym bajcie, który nadal jest przyjmowany
x
.Program: założę, że następnym dostępnym bajtem jest zmienna char
y
.Program: Napiszę
a
do zmiennejy
.Biblioteka służy do znalezienia wartości bajtu dla
a
Bajt jest zapisywany na adres, który zakłada program
y
.Program: Chcę wyświetlić zawartość
y
Odczytuje wartość w drugim miejscu pamięci
Używa biblioteki do konwersji z bajtu na znak
Używa bibliotek graficznych do zmiany ekranu konsoli (ustawianie pikseli z czarnego na biały, przewijanie jednej linii itp.)
(I zaczyna się stąd)
To, co prawdopodobnie Cię rozłącza, to - co dzieje się, gdy nie ma już pierwszego miejsca w pamięci
x
? czy drugi już nie jesty
? Co się dzieje, gdy ktoś czytax
jako wskaźnikchar
luby
wskaźnik? Krótko mówiąc, zdarzają się złe rzeczy. Niektóre z tych rzeczy mają dobrze zdefiniowane zachowanie, a niektóre mają niezdefiniowane zachowanie. Nieokreślone zachowanie jest dokładnie tym - wszystko może się zdarzyć, od niczego, po awarię programu lub systemu operacyjnego. Nawet dobrze zdefiniowane zachowanie może być złośliwe. Jeśli mogę zmienićx
wskaźnik na mój program i sprawić, by Twój program używał go jako wskaźnika, to mogę sprawić, że Twój program zacznie uruchamiać mój program - właśnie to robią hakerzy. Kompilator pomaga upewnić się, że nie używamy goint x
jakostring
i rzeczy tego rodzaju. Sam kod maszynowy nie jest świadomy typów i robi tylko to, co nakazują instrukcje. Istnieje również duża ilość informacji odkrytych w czasie wykonywania: z których bajtów pamięci może korzystać program? Czyx
zaczyna się od pierwszego bajtu, czy od 12?Ale możesz sobie wyobrazić, jak okropnie byłoby pisać takie programy (i możesz to zrobić w języku asemblera). Zaczynasz od „zadeklarowania” zmiennych - mówisz sobie, że bajt 1 to
x
bajt 2y
, a kiedy piszesz każdy wiersz kodu, ładując i przechowując rejestry, musisz (jako człowiek) pamiętać, który jest,x
a który jeden jesty
, ponieważ system nie ma pojęcia. A ty (jako człowiek) musisz pamiętać, jakie typyx
i jakiey
są, ponieważ znowu - system nie ma pojęcia.źródło
Otherwise how can console or text file outputs a character instead of int
Ponieważ istnieje inna sekwencja instrukcji wyprowadzania zawartości lokalizacji w pamięci jako liczba całkowita lub jako znaki alfanumeryczne. Kompilator wie o typach zmiennych, wybiera odpowiednią sekwencję instrukcji w czasie kompilacji i zapisuje ją w EXE.Wydaje mi się, że twoim głównym pytaniem jest: „Jeśli typ zostanie usunięty w czasie kompilacji i nie zostanie zachowany w czasie wykonywania, to skąd komputer wie, czy wykonać kod, który interpretuje go jako,
int
czy wykonać kod, który interpretuje go jakochar
? „Odpowiedź brzmi… komputer nie. Jednak kompilator nie wie i to będzie po prostu umieścić poprawny kod w pliku binarnego w pierwszej kolejności. Gdyby zmienna została wpisana jako
char
, to kompilator nie umieściłby kodu do traktowania jej jakoint
programu, a kod potraktowałby ją jakochar
.Tam są powody, aby zachować typ w czasie wykonywania:
+
Operatora), więc z tego powodu nie potrzebuje typu środowiska wykonawczego. Jednak znowu typ środowiska wykonawczego jest czymś innym niż typ statyczny, np. W Javie można teoretycznie usunąć typy statyczne i nadal zachować typ środowiska wykonawczego dla polimorfizmu. Zauważ też, że jeśli zdecentralizujesz i specjalizujesz kod wyszukiwania typu i umieścisz go w obiekcie (lub klasie), to niekoniecznie będziesz potrzebował typu środowiska wykonawczego, np. Vtables C ++.Jedynym powodem, aby utrzymać typ w środowisku wykonawczym w C, jest debugowanie, jednak debugowanie zwykle odbywa się przy dostępnym źródle, a następnie można po prostu wyszukać typ w pliku źródłowym.
Usuwanie typu jest całkiem normalne. Nie wpływa to na bezpieczeństwo typu: typy są sprawdzane w czasie kompilacji, gdy kompilator upewni się, że program jest bezpieczny dla typu, typy nie są już potrzebne (z tego powodu). Nie wpływa na statyczny polimorfizm (inaczej przeciążenie): po zakończeniu rozwiązywania problemu z przeciążeniem, a kompilator wybrał odpowiednie przeciążenie, nie potrzebuje już typów. Typy mogą również kierować optymalizacją, ale ponownie, gdy optymalizator wybierze optymalizacje na podstawie typów, nie będzie ich już potrzebował.
Zachowywanie typów w środowisku wykonawczym jest wymagane tylko wtedy, gdy chcesz coś zrobić z typami w środowisku wykonawczym.
Haskell jest jednym z najbardziej rygorystycznych, najbardziej rygorystycznych, bezpiecznych dla języka statycznych typów, a kompilatory Haskell zwykle usuwają wszystkie typy. (Uważam, że wyjątkiem jest przekazywanie słowników metod dla klas typów).
źródło
char
do skompilowanego pliku binarnego. To nie ma wyjścia dla koduint
, to nie ma wyjścia dla kodubyte
, to nie wyjście kod do wskaźnika, to po prostu wyświetla tylko kod dlachar
. Na podstawie typu nie są podejmowane żadne decyzje dotyczące środowiska wykonawczego. Nie potrzebujesz tego typu. Jest to całkowicie i całkowicie nieistotne. Wszystkie istotne decyzje zostały już podjęte w czasie kompilacji.public class JoergsAwesomeNewType {};
Widzieć? Właśnie wymyśliłem nowy typ! Musisz kupić nowy procesor!Komputer nie „wie”, jakie są adresy, ale znajomość tego, co jest zapisane w instrukcjach programu.
Kiedy piszesz program C, który zapisuje i odczytuje zmienną char, kompilator tworzy kod asemblera, który zapisuje ten fragment danych gdzieś jako char, a także gdzieś indziej kod, który odczytuje adres pamięci i interpretuje go jako char. Jedyne, co łączy te dwie operacje razem, to lokalizacja tego adresu pamięci.
Kiedy przychodzi czas na czytanie, instrukcje nie mówią „zobacz, jaki jest typ danych”, po prostu mówi coś w rodzaju „załaduj tę pamięć jako liczbę zmiennoprzecinkową”. Jeśli adres do odczytu zostanie zmieniony lub coś zastąpi tę pamięć czymś innym niż zmiennoprzecinkowe, procesor po prostu z przyjemnością załaduje tę pamięć jako zmiennoprzecinkowe, w wyniku czego mogą się zdarzyć różne dziwne rzeczy.
Zły czas na analogię: wyobraź sobie skomplikowany magazyn wysyłkowy, w którym magazyn to pamięć, a ludzie wybierają rzeczy to procesor. Jedna część „programu” magazynu umieszcza różne przedmioty na półce. Kolejny program idzie i zabiera przedmioty z magazynu i umieszcza je w pudełkach. Kiedy są ściągane, nie są sprawdzane, po prostu wchodzą do kosza. Cały magazyn funkcjonuje, wszystko synchronizuje się, a właściwe przedmioty zawsze znajdują się we właściwym miejscu we właściwym czasie, w przeciwnym razie wszystko ulega awarii, tak jak w prawdziwym programie.
źródło
Tak nie jest. Po skompilowaniu C do kodu maszynowego maszyna widzi tylko kilka bitów. Sposób interpretacji tych bitów zależy od tego, jakie operacje są na nich wykonywane, w przeciwieństwie do niektórych dodatkowych metadanych.
Typy, które wpisujesz w kodzie źródłowym, są tylko dla kompilatora. Przyjmuje, jaki typ danych ma być, i najlepiej jak potrafi, stara się upewnić, że dane te są wykorzystywane tylko w sensowny sposób. Gdy kompilator wykona jak najlepszą robotę, sprawdzając logikę kodu źródłowego, konwertuje go na kod maszynowy i odrzuca dane typu, ponieważ kod maszynowy nie ma możliwości jego przedstawienia (przynajmniej na większości komputerów) .
źródło
int a = 65
ichar b = 'A'
po skompilowaniu kodu.Większość procesorów udostępnia różne instrukcje dotyczące pracy z danymi różnych typów, więc informacje o typie są zwykle „wstawiane” do generowanego kodu maszynowego. Nie ma potrzeby przechowywania dodatkowych metadanych typu.
Niektóre konkretne przykłady mogą pomóc. Poniższy kod maszynowy został wygenerowany przy użyciu gcc 4.1.2 na systemie x86_64 z systemem SuSE Linux Enterprise Server (SLES) 10.
Załóżmy następujący kod źródłowy:
Oto treść wygenerowanego kodu asemblera odpowiadającego powyższemu źródłu (za pomocą
gcc -S
), z dodanymi przeze mnie komentarzami:Oto kilka dodatkowych rzeczy
ret
, ale nie ma to znaczenia w dyskusji.%eax
to 32-bitowy rejestr danych ogólnego przeznaczenia.%rsp
to 64-bitowy rejestr zarezerwowany do zapisywania wskaźnika stosu , który zawiera adres ostatniej rzeczy wypchniętej na stos.%rbp
jest 64-bitowym rejestrem zarezerwowanym do zapisywania wskaźnika ramki , który zawiera adres bieżącej ramki stosu . Ramka stosu jest tworzona na stosie po wprowadzeniu funkcji i rezerwuje miejsce na argumenty funkcji i zmienne lokalne. Dostęp do argumentów i zmiennych można uzyskać za pomocą przesunięć wskaźnika ramki. W tym przypadku pamięć dla zmiennejx
wynosi 12 bajtów „poniżej” adresu zapisanego w%rbp
.W powyższym kodzie kopiujemy wartość całkowitą
x
(1, przechowywaną w-12(%rbp)
) do rejestru%eax
za pomocąmovl
instrukcji, która służy do kopiowania 32-bitowych słów z jednej lokalizacji do drugiej. Następnie wywołujemyaddl
, co dodaje wartość całkowitąy
(przechowywaną w-8(%rbp)
) do wartości już w%eax
. Następnie zapisujemy wynik-4(%rbp)
, czyliz
.Teraz zmieńmy to, abyśmy mieli do czynienia z
double
wartościami zamiastint
wartościami:gcc -S
Ponowne uruchomienie daje nam:Kilka różnic. Zamiast
movl
iaddl
używamymovsd
iaddsd
(przypisujemy i dodajemy zmiennoprzecinkowe podwójnej precyzji). Zamiast przechowywać wartości pośrednie%eax
, używamy%xmm0
.Mam na myśli to, co mówię, gdy typ jest „wstawiany” do kodu maszynowego. Kompilator po prostu generuje odpowiedni kod maszynowy do obsługi tego konkretnego typu.
źródło
Historycznie C uważał pamięć za złożoną z kilku grup ponumerowanych miejsc typu
unsigned char
(zwany także „bajtem”, chociaż nie zawsze musi to być 8 bitów). Każdy kod, który wykorzystywałby cokolwiek przechowywanego w pamięci, musiałby wiedzieć, w którym gnieździe lub gniazdach była przechowywana informacja i wiedzieć, co należy zrobić z zawartymi tam informacjami [np. ”Zinterpretować cztery bajty zaczynające się pod adresem 123: 456 jako 32-bitowe wartość zmiennoprzecinkowa ”lub„ zapisz 16 bitów ostatnio obliczonej wielkości w dwóch bajtach, zaczynając od adresu 345: 678]. Sama pamięć nie wiedziałaby ani nie obchodziło, co wartości przechowywane w gniazdach pamięci „oznaczają”. kod próbował zapisać pamięć przy użyciu jednego typu i odczytać ją jako inny, wzorce bitów zapisane przez zapis byłyby interpretowane zgodnie z regułami drugiego typu, bez względu na konsekwencje.Na przykład, jeśli kod ma zostać zapisany
0x12345678
w wersji 32-bitowejunsigned int
, a następnie spróbuj odczytać dwie kolejne 16-bitoweunsigned int
wartości z jego adresu i powyższej, to w zależności od tego, w której połowieunsigned int
zapisano, gdzie kod może odczytać wartości 0x1234 i 0x5678 lub 0x5678 i 0x1234.Standard C99 nie wymaga jednak, aby pamięć zachowywała się jak grupa numerowanych gniazd, które nie wiedzą nic o tym, co reprezentują ich wzory bitowe . Kompilator może zachowywać się tak, jakby gniazda pamięci były świadome typów danych, które są w nich przechowywane, i zezwala tylko na dane, które są zapisywane przy użyciu dowolnego innego rodzaju niż
unsigned char
do odczytu przy użyciu dowolnego typuunsigned char
lub tego samego typu, w jakim zostały zapisane z; kompilatory mogą dalej zachowywać się tak, jakby gniazda pamięci miały moc i skłonność do arbitralnego niszczenia zachowania każdego programu, który próbuje uzyskać dostęp do pamięci w sposób sprzeczny z tymi regułami.Dany:
niektóre implementacje mogą drukować 0x1234, a inne mogą drukować 0x5678, ale zgodnie ze standardem C99 implementacja może drukować „ZASADY FRINK!” lub zrobić cokolwiek innego, zgodnie z teorią, zgodnie z którą legalne byłoby, aby lokalizacje pamięci
a
zawierały sprzęt, który rejestruje, jakiego typu użyto do ich zapisania, oraz aby taki sprzęt reagował w jakikolwiek sposób na nieprawidłową próbę odczytu, w tym powodując „ZASADY FRINK!” być wyjściem.Zauważ, że nie ma znaczenia, czy taki sprzęt faktycznie istnieje - fakt, że taki sprzęt mógłby legalnie istnieć, pozwala kompilatorom generować kod, który zachowuje się tak, jakby działał w takim systemie. Jeśli kompilator może ustalić, że określona lokalizacja pamięci zostanie zapisana jako jeden typ, a odczytana jako inny, może udawać, że działa w systemie, którego sprzęt może dokonać takiego określenia, i może zareagować z dowolnym stopniem kaprysu, jaki autor kompilatora uzna za stosowny. .
Celem tej reguły było umożliwienie kompilatorom, które wiedziały, że grupa bajtów o wartości pewnego typu posiadała określoną wartość w pewnym momencie i że od tego czasu nie zapisano żadnej wartości tego samego typu, aby wnioskować o tej grupie bajtów nadal utrzymywałoby tę wartość. Na przykład procesor wczytał grupę bajtów do rejestru, a następnie chciał ponownie użyć tych samych informacji, gdy był jeszcze w rejestrze, kompilator mógł użyć zawartości rejestru bez konieczności ponownego odczytu wartości z pamięci. Przydatna optymalizacja. Przez około pierwsze dziesięć lat obowiązywania reguły naruszenie tego oznaczałoby na ogół, że jeśli zmienna zostanie zapisana innym typem niż ten, który jest używany do jej odczytu, zapis może wpływać na odczytaną wartość lub nie. Takie zachowanie może w niektórych przypadkach być katastrofalne, ale w innych przypadkach może być nieszkodliwe,
Jednak około 2009 roku autorzy niektórych kompilatorów, takich jak CLANG, ustalili, że skoro Standard pozwala kompilatorom robić wszystko, co im się podoba w przypadkach, gdy pamięć jest zapisywana przy użyciu jednego typu i odczytywana jako inna, kompilatory powinny wywnioskować, że programy nigdy nie otrzymają danych wejściowych, które mogłyby spowodować coś takiego. Ponieważ Standard mówi, że kompilator może robić wszystko, co chce, po otrzymaniu takich nieprawidłowych danych wejściowych, kod, który miałby wpływ tylko w przypadkach, w których Standard nie nakłada żadnych wymagań, może (a zdaniem niektórych autorów kompilatora powinien zostać pominięty) jako nieistotne. Zmienia to zachowanie naruszeń aliasingu z tego, że przypomina pamięć, która przy danym żądaniu odczytu może dowolnie zwrócić ostatnią zapisaną wartość przy użyciu tego samego typu jak żądanie odczytu lub dowolną późniejszą wartość zapisaną przy użyciu innego typu,
źródło
int x,y,z;
wyrażeniex*y > z
nigdy nie zrobiłoby nic innego niż zwrócenie 1 lub 0, lub gdzie naruszenia aliasingu miałyby jakikolwiek skutek inne niż pozwolenie kompilatorowi na arbitralne zwrócenie starej lub nowej wartości.unsigned char
wartości, które są używane do budowy typu „pochodzą”. Jeśli program miałby rozkładać wskaźnik naunsigned char[]
, wyświetlać krótko jego zawartość szesnastkową na ekranie, a następnie usunąć wskaźnikunsigned char[]
, a następnie zaakceptować niektóre liczby szesnastkowe z klawiatury, skopiować je z powrotem do wskaźnika, a następnie oderwać ten wskaźnik , zachowanie byłoby dobrze zdefiniowane w przypadku, gdy wpisana liczba pasowała do wyświetlanej liczby.W C tak nie jest. Inne języki (np. Lisp, Python) mają typy dynamiczne, ale C ma typ statyczny. Oznacza to, że Twój program musi wiedzieć, jakiego typu dane mają poprawnie interpretować jako znak, liczba całkowita itp.
Zwykle kompilator dba o to, a jeśli zrobisz coś złego, pojawi się błąd kompilacji (lub ostrzeżenie).
źródło
10001
. Zadaniem użytkownika lub kompilatora jest , zależnie od przypadku, ręczne nadpisywanie takich rzeczy podczas pisania kodu maszyny lub zestawu.Trzeba rozróżnić
compiletime
iruntime
z jednej strony, acode
idata
z drugiej strony.Z punktu widzenia maszynowego to ma różnicy między tym, co nazywacie
code
alboinstructions
i co nazywaszdata
. Wszystko sprowadza się do liczb. Ale niektóre sekwencje - jak to nazwalibyśmycode
- robią coś, co uważamy za przydatne, inne po prostucrash
maszynę.Praca wykonywana przez CPU to prosta 4-etapowa pętla:
instruction
)Nazywa się to cyklem instrukcji .
a
ix
są zmiennymi, które są symbolami zastępczymi dla adresów, w których program może znaleźć „treść” zmiennych. Tak więc za każdym razem, gdya
używana jest zmienna , efektywnie jest adresa
użytej zawartości .Egzekucja nic nie wie. Z tego, co powiedziano we wstępie, CPU pobiera tylko dane i interpretuje je jako instrukcje.
Funkcja printf została zaprojektowana w taki sposób, aby „wiedziała”, jaki rodzaj danych w niej wkładasz, tj. Wynikowy kod zawiera odpowiednie instrukcje dotyczące postępowania ze specjalnym segmentem pamięci. Oczywiście możliwe jest generowanie nonsensownych danych wyjściowych: użycie adresu, w którym nie jest przechowywany żaden ciąg znaków wraz z „% s”,
printf()
spowoduje, że dane nonsensowne zostaną zatrzymane tylko przez losowe miejsce w pamięci, gdzie jest 0 (\0
).To samo dotyczy punktu wejścia programu. Pod C64 możliwe było umieszczanie programów pod (prawie) każdym znanym adresem. Programy asemblacyjne rozpoczęto od instrukcji wywoływanej
sys
po adresie:sys 49152
było to wspólne miejsce do umieszczania kodu asemblera. Ale nic nie powstrzymało Cię przed załadowaniem np. Danych graficznych49152
, co spowodowało awarię komputera po „uruchomieniu” od tego miejsca. W tym przypadku cykl instrukcji rozpoczął się od odczytu „danych graficznych” i próby interpretacji go jako „kodu” (co oczywiście nie miało sensu); efekty były zdumiewające;)Jak powiedziano: „Kontekst” - tj. Poprzednie i następne instrukcje - pomagają traktować dane tak, jak chcemy. Z perspektywy maszyny nie ma różnicy w żadnej lokalizacji pamięci.
int
ichar
jest tylko słownictwem, które ma senscompiletime
; podczasruntime
(na poziomie asemblera) nie machar
lubint
.Komputer nic nie wie . Programista robi. Skompilowany kod generuje kontekst , który jest niezbędny do generowania znaczących wyników dla ludzi.
Tak i Nie . Informacje, niezależnie od tego, czy jest to
int
czy nie,char
są tracone. Ale z drugiej strony kontekst (instrukcje, jak radzić sobie z lokalizacjami pamięci, w których przechowywane są dane) jest zachowany; więc domyślnie tak, „informacja” jest domyślnie dostępna.źródło
Pozostawmy tę dyskusję tylko w języku C.
Program, o którym mowa, jest napisany w języku wysokiego poziomu, takim jak C. Komputer rozumie tylko język maszynowy. Języki wyższego poziomu dają programiście możliwość wyrażania logiki w sposób bardziej przyjazny dla człowieka, który jest następnie tłumaczony na kod maszynowy, który mikroprocesor może dekodować i wykonywać. Teraz omówmy wspomniany kod:
Spróbujmy przeanalizować każdą część:
Tak więc identyfikatory typu danych int / char są używane tylko przez kompilator, a nie przez mikroprocesor podczas wykonywania programu. Dlatego nie są przechowywane w pamięci.
źródło
Moja odpowiedź tutaj jest nieco uproszczona i będzie odnosić się tylko do C.
Nie, informacje o typie nie są zapisywane w programie.
int
lubchar
nie są wskaźnikami typu do procesora; tylko do kompilatora.Plik exe utworzony przez kompilator będzie miał instrukcje do manipulowania
int
s, jeśli zmienna została zadeklarowana jakoint
. Podobnie, jeśli zmienna została zadeklarowana jako achar
, exe będzie zawierać instrukcje dotyczące manipulowania achar
.W C:
Ten program wydrukuje swój komunikat, ponieważ
char
iint
mają te same wartości w pamięci RAM.Teraz, jeśli zastanawiasz się, jak
printf
udaje się wyprowadzić65
dlaint
iA
dlachar
, oznacza to , że musisz określić w „ciągu formatu”, jakprintf
należy traktować wartość .(Na przykład
%c
oznacza traktowanie wartości jako achar
i%d
oznacza traktowanie wartości jako liczby całkowitej; jednak ta sama wartość w obu przypadkach.)źródło
printf
. @OP:int a = 65; printf("%c", a)
wyświetli'A'
. Dlaczego? Ponieważ procesor nie dba o to. Do tego wszystko, co widzi, to kawałki. Twój program powiedział procesorowi, aby zapisał 65 (przypadkowo wartość'A'
w ASCII) w,a
a następnie wypisał znak, co chętnie robi. Dlaczego? Ponieważ to nie obchodzi.Na najniższym poziomie w rzeczywistym fizycznym procesorze nie ma żadnych typów (ignorując jednostki zmiennoprzecinkowe). Po prostu wzory bitów. Komputer działa bardzo szybko, manipulując wzorami bitów.
To wszystko, co procesor kiedykolwiek robi, wszystko, co może zrobić. Nie ma czegoś takiego jak int lub char.
Wykona się jako:
Instrukcja iadd uruchamia sprzęt, który zachowuje się tak, jakby rejestry 1 i 2 były liczbami całkowitymi. Jeśli tak naprawdę nie reprezentują liczb całkowitych, wszelkiego rodzaju rzeczy mogą później pójść nie tak. Najlepszy wynik to zwykle awaria.
Kompilator wybiera prawidłową instrukcję na podstawie typów podanych w źródle, ale w rzeczywistym kodzie maszynowym wykonanym przez CPU nie ma nigdzie żadnych typów.
edycja: Zauważ, że rzeczywisty kod maszynowy w rzeczywistości nie wspomina 4, 5, ani liczb całkowitych. to tylko dwa wzorce bitów i instrukcja, która bierze dwa wzorce bitowe, zakłada, że są liczbami całkowitymi i dodaje je razem.
źródło
Krótka odpowiedź, typ jest zakodowany w instrukcjach procesora generowanych przez kompilator.
Chociaż informacje o typie lub rozmiarze informacji nie są bezpośrednio przechowywane, kompilator śledzi te informacje podczas uzyskiwania dostępu, modyfikowania i przechowywania wartości w tych zmiennych.
Nie robi tego, ale kiedy kompilator tworzy kod maszynowy, wie o tym. A
int
i achar
mogą mieć różne rozmiary. W architekturze, w której char jest wielkością bajtu, a int jest 4 bajtami, wówczas zmiennax
nie ma adresu 10001, ale także 10002, 10003 i 10004. Gdy kod musi załadować wartośćx
do rejestru procesora, wykorzystuje instrukcję ładowania 4 bajtów. Podczas ładowania znaku używa instrukcji, aby załadować 1 bajt.Jak wybrać jedną z dwóch instrukcji? Kompilator decyduje podczas kompilacji, nie jest to wykonywane w czasie wykonywania po sprawdzeniu wartości w pamięci.
Należy również pamiętać, że rejestry mogą mieć różne rozmiary. W procesorach Intel x86 EAX ma szerokość 32 bitów, połowa z nich to AX, czyli 16, a AX jest podzielony na AH i AL, oba 8 bitów.
Jeśli więc chcesz załadować liczbę całkowitą (na procesorach x86), użyj instrukcji MOV dla liczb całkowitych, aby załadować znak, użyj instrukcji MOV dla znaków. Oba są nazywane MOV, ale mają różne kody operacji. Skutecznie będąc dwiema różnymi instrukcjami. Typ zmiennej jest zakodowany w instrukcji użycia.
To samo dzieje się z innymi operacjami. Istnieje wiele instrukcji wykonywania dodawania, w zależności od wielkości operandów, a nawet jeśli są one podpisane lub niepodpisane. Zobacz https://en.wikipedia.org/wiki/ADD_(x86_instruction) które zawierają listę różnych możliwych dodatków.
Po pierwsze, char będzie wynosił 10011001, ale int będzie 00000000 00000000 00000000 10011001, ponieważ mają one różne rozmiary (na komputerze o takich samych rozmiarach, jak wspomniano powyżej). Ale rozważmy sprawę do
signed char
VSunsigned char
.To, co jest przechowywane w pamięci, można interpretować w dowolny sposób. Do obowiązków kompilatora C należy zapewnienie, że to, co jest przechowywane i odczytywane ze zmiennej, odbywa się w spójny sposób. Nie chodzi więc o to, że program wie, co jest przechowywane w miejscu pamięci, ale o to, że z góry zgadza się, że zawsze będzie tam czytać i pisać takie same rzeczy. (nie licząc rzeczy takich jak typy rzutowania).
źródło
W językach z kontrolą typu, takich jak C #, sprawdzanie typu jest wykonywane przez kompilator. Kod benji napisał:
Po prostu odmówiłby kompilacji. Podobnie, jeśli próbowałeś pomnożyć ciąg i liczbę całkowitą (chciałem powiedzieć add, ale operator „+” jest przeciążony konkatenacją łańcucha i może po prostu działać).
Kompilator po prostu odmówiłby wygenerowania kodu maszynowego z tego C #, bez względu na to, jak bardzo ciąg do niego pocałował.
źródło
Pozostałe odpowiedzi są poprawne, ponieważ zasadniczo każde napotkane urządzenie konsumenckie nie przechowuje informacji o typie. W przeszłości istniało jednak kilka projektów sprzętowych (i obecnie, w kontekście badań), które wykorzystują oznakowaną architekturę - przechowują zarówno dane, jak i typ (i ewentualnie także inne informacje). Dotyczy to przede wszystkim maszyn Lisp .
Niejasno pamiętam, jak słyszałem o architekturze sprzętowej zaprojektowanej do programowania obiektowego, która miała coś podobnego, ale nie mogę jej teraz znaleźć.
źródło