Dlaczego musimy wspomnieć o typie danych zmiennej w C.

19

Zwykle w C musimy podać komputerowi rodzaj danych w deklaracji zmiennej. Np. W poniższym programie chcę wydrukować sumę dwóch liczb zmiennoprzecinkowych X i Y.

#include<stdio.h>
main()
{
  float X=5.2;
  float Y=5.1;
  float Z;
  Z=Y+X;
  printf("%f",Z);

}

Musiałem poinformować kompilator o typie zmiennej X.

  • Czy kompilator nie może sam określić typu X?

Tak, może to zrobić, jeśli to zrobię:

#define X 5.2

Mogę teraz napisać mój program bez informowania kompilatora o typie X:

#include<stdio.h>
#define X 5.2
main()
{
  float Y=5.1;
  float Z;
  Z=Y+X;
  printf("%f",Z);

}  

Widzimy więc, że język C ma jakąś funkcję, za pomocą której może samodzielnie określać rodzaj danych. W moim przypadku ustalono, że Xjest typu float.

  • Dlaczego musimy wspomnieć o rodzaju danych, kiedy deklarujemy coś w main ()? Dlaczego kompilator nie może samodzielnie określić typu danych zmiennej main()tak jak w #define.
użytkownik106313
źródło
14
W rzeczywistości te dwa programy nie są równoważne, ponieważ mogą dawać nieco inne wyniki! 5.2jest a double, więc pierwszy program zaokrągla podwójne literały do floatprecyzji, następnie dodaje je jako liczby zmiennoprzecinkowe, podczas gdy drugi zaokrągla podwójną reprezentację 5.1 z powrotem doublei dodaje ją do doublewartości 5.2 za pomocą doubledodawania, a następnie zaokrągla wynik tego obliczenia do floatprecyzji . Ponieważ zaokrąglanie występuje w różnych miejscach, wynik może być inny. To tylko jeden przykład rodzajów zmiennych wpływających na zachowanie innego identycznego programu.
12
Kiedy to zrobisz #define X 5.2, Xnie jest zmienną, ale stałą, więc jest dosłownie zamieniony na preprocesor, 5.2gdziekolwiek wspomniano X. Nie możesz ponownie przypisać X.
scriptin
16
Uwaga: to błogosławieństwo i przekleństwo. Z jednej strony musisz wpisać kilka znaków, gdy kompilator naprawdę mógł to zrobić za Ciebie (C ++ autofaktycznie robi to, co chcesz). Z drugiej strony, jeśli uważasz, że wiesz, co robi Twój kod i faktycznie wpisałeś coś innego, wpisywanie statyczne w ten sposób wcześniej wykryje błąd, zanim stanie się on ogromnym problemem. Każdy język zachowuje równowagę: pisanie statyczne, wnioskowanie typu, pisanie dynamiczne. W przypadku niektórych zadań dodatkowe pisanie jest naprawdę tego warte. Dla innych to marnotrawstwo.
Cort Ammon - Przywróć Monikę
Naucz się Ocaml i / lub Haskell ... będziesz zadowolony z ich umiejętności wnioskowania.
Basile Starynkevitch,

Odpowiedzi:

46

Porównujesz deklaracje zmiennych do #defines, co jest niepoprawne. Za pomocą a #definetworzysz odwzorowanie między identyfikatorem a fragmentem kodu źródłowego. Preprocesor C dosłownie zastąpi wszelkie wystąpienia tego identyfikatora dostarczonym fragmentem kodu. Pisanie

#define FOO 40 + 2
int foos = FOO + FOO * FOO;

kończy się dla kompilatora tym samym, co pisanie

int foos = 40 + 2 + 40 + 2 * 40 + 2;

Pomyśl o tym jak o automatycznym kopiowaniu i wklejaniu.

Ponadto zmienne normalne można przypisać ponownie, a makra utworzonego za #definepomocą nie można (choć można to zmienić #define). Wyrażenie FOO = 7byłoby błędem kompilatora, ponieważ nie możemy przypisać do „wartości”: 40 + 2 = 7jest nielegalne.

Dlaczego więc w ogóle potrzebujemy typów? Najwyraźniej niektóre języki pozbywają się typów, jest to szczególnie powszechne w językach skryptowych. Zwykle mają jednak coś, co nazywa się „typowaniem dynamicznym”, w którym zmienne nie mają ustalonych typów, ale wartości mają. Jest to o wiele bardziej elastyczne, ale także mniej wydajne. C lubi wydajność, więc ma bardzo prostą i wydajną koncepcję zmiennych:

Jest pewien odcinek pamięci zwany „stosem”. Każda zmienna lokalna odpowiada obszarowi na stosie. Teraz pytanie brzmi, ile bajtów musi mieć ten obszar? W języku C każdy typ ma dobrze zdefiniowany rozmiar, za pomocą którego można wyszukiwać sizeof(type). Kompilator musi znać typ każdej zmiennej, aby mógł zarezerwować odpowiednią ilość miejsca na stosie.

Dlaczego stałe utworzone za #definepomocą adnotacji typu nie są potrzebne? Nie są przechowywane na stosie. Zamiast tego #definetworzy fragmenty kodu źródłowego wielokrotnego użytku w nieco łatwiejszy do utrzymania sposób niż kopiowanie i wklejanie. Literały w kodzie źródłowym, takie jak "foo"lub 42.87są przechowywane przez kompilator, albo jako instrukcje specjalne, albo w osobnej sekcji danych wynikowego pliku binarnego.

Jednak literały mają typy. Dosłowny ciąg znaków to char *. 42jest, intale można go również stosować w przypadku krótszych typów (konwersja zwężająca). 42.8byłoby double. Jeśli masz dosłownym i chcesz go mieć inny typ (np aby lub ), a następnie można użyć przyrostków - list po dosłownym, że zmiany w jaki sposób traktuje kompilator, że dosłowne. W naszym przypadku możemy powiedzieć lub .42.8float42unsigned long int42.8f42ul

Niektóre języki mają pisanie statyczne jak w C, ale adnotacje typu są opcjonalne. Przykładami są ML, Haskell, Scala, C #, C ++ 11 i Go. Jak to działa? Magia? Nie, nazywa się to „wnioskowaniem typu”. W C # i Go kompilator patrzy na prawą stronę zadania i wydedukuje typ tego zadania. Jest to dość proste, jeśli prawa strona jest dosłowna, np 42ul. To oczywiste, jaki powinien być typ zmiennej. Inne języki mają również bardziej złożone algorytmy, które uwzględniają sposób użycia zmiennej. Na przykład jeśli tak x/2, to xnie może być ciągiem, ale musi mieć jakiś typ liczbowy.

amon
źródło
Dziękuję za wyjaśnienie. Rozumiem, że kiedy deklarujemy typ zmiennej (lokalny lub globalny), tak naprawdę mówimy kompilatorowi, ile miejsca powinno zarezerwować dla tej zmiennej na stosie. Z drugiej strony #definemamy stałą, która jest bezpośrednio konwertowana na kod binarny - jakkolwiek by nie był - i jest przechowywana w pamięci bez zmian.
user106313,
2
@ user31782 - niezupełnie. Kiedy deklarujesz zmienną, typ informuje kompilator, jakie właściwości ma zmienna. Jedną z tych właściwości jest rozmiar; inne właściwości obejmują sposób, w jaki reprezentuje wartości oraz jakie operacje można wykonać na tych wartościach.
Pete Becker,
@PeteBecker Skąd więc kompilator zna te inne właściwości #define X 5.2?
user106313,
1
Dzieje się tak, ponieważ przekazując niewłaściwy typ printf, wywołujesz niezdefiniowane zachowanie. Na moim komputerze ten fragment wypisuje za każdym razem inną wartość, w Ideone ulega awarii po wydrukowaniu zera.
Matteo Italia,
4
@ user31782 - „Wydaje się, że mogę wykonać żadnej operacji na wszelkiego rodzaju danych” Nie. X*YNie jest ważne, jeżeli Xi Ysą wskaźnikami, ale to jest w porządku, jeśli są one ints; *Xnie jest poprawny, jeśli Xjest int, ale jest w porządku, jeśli jest wskaźnikiem.
Pete Becker,
4

X w drugim przykładzie nigdy nie jest zmiennoprzecinkowy. Nazywa się to makrem, zastępuje zdefiniowaną wartość makra „X” w źródle wartością. Czytelny artykuł na temat #define jest tutaj .

W przypadku dostarczonego kodu, przed kompilacją preprocesor zmienia kod

Z=Y+X;

do

Z=Y+5.2;

i to się kompiluje.

Oznacza to, że można również zastąpić te „wartości” kodem podobnym do kodu

#define X sqrt(Y)

lub nawet

#define X Y
James Snell
źródło
3
Nazywa się to po prostu makro, a nie makro variadic. Makro variadic to makro, które przyjmuje zmienną liczbę argumentów, np #define FOO(...) { __VA_ARGS__ }.
hvd,
2
Mój zły, naprawi :)
James Snell
1

Krótka odpowiedź brzmi: C potrzebuje typów ze względu na historię / reprezentację sprzętu.

Historia: C został opracowany na początku lat siedemdziesiątych i miał być językiem programowania systemów. Kod jest idealnie szybki i najlepiej wykorzystuje możliwości sprzętu.

Wnioskowanie typów w czasie kompilacji byłoby możliwe, ale i tak już wolne czasy kompilacji wzrosłyby (patrz kreskówka „kompilowania” XKCD. Dotyczyło to „hello world” przez co najmniej 10 lat po opublikowaniu C ). Wnioskowanie typów w czasie wykonywania nie byłoby zgodne z celami programowania systemów. Wnioskowanie w czasie wykonywania wymaga dodatkowej biblioteki wykonawczej. C pojawił się na długo przed pierwszym komputerem. Który miał 256 pamięci RAM. Nie gigabajty lub megabajty, ale kilobajty.

W twoim przykładzie, jeśli pominiesz typy

   X=5.2;
   Y=5.1;

   Z=Y+X;

Następnie kompilator mógł z radością odkryć, że X i Y są zmiennoprzecinkowe i uczynić Z tym samym. W rzeczywistości nowoczesny kompilator również by się przekonał, że X i Y nie są potrzebne i po prostu ustawił Z na 10.3.

Załóżmy, że obliczenia są osadzone w funkcji. Autor funkcji może chcieć wykorzystać swoją wiedzę o sprzęcie lub rozwiązanym problemie.

Czy podwójne byłoby bardziej odpowiednie niż float? Zajmuje więcej pamięci i jest wolniejszy, ale dokładność wyniku byłaby wyższa.

Być może zwracana wartość funkcji może być int (lub long), ponieważ ułamki dziesiętne nie są ważne, chociaż konwersja z liczby zmiennoprzecinkowej na int nie jest bez kosztów.

Wartość zwracaną można również podwójnie zagwarantować, że liczba zmiennoprzecinkowa + liczba zmiennoprzecinkowa nie zostanie przelana.

Wszystkie te pytania wydają się bezcelowe dla ogromnej większości pisanego dzisiaj kodu, ale były niezbędne, gdy C został stworzony.

itj
źródło
1
nie wyjaśnia to np. dlaczego deklaracje typu nie zostały opcjonalne, co pozwala programiście wybrać albo zadeklarować je jawnie, albo polegać na kompilatorze do wnioskowania
gnat
1
Rzeczywiście to nie @gnat. Poprawiłem tekst, ale nie było sensu tego robić. Domena C została zaprojektowana tak, aby faktycznie chciała zdecydować o przechowywaniu 17 w 1 bajcie, 2 bajtach lub 4 bajtach lub jako ciąg znaków lub jako 5 bitów w jednym słowie.
itj
0

C nie ma wnioskowania o typie (tak się nazywa, gdy kompilator zgadnie typ zmiennej dla Ciebie), ponieważ jest stary. Został opracowany na początku lat siedemdziesiątych

Wiele nowszych języków ma systemy, które pozwalają używać zmiennych bez określania ich typu (ruby, javascript, python itp.)

Tristan Burnside
źródło
12
Żaden z wymienionych przez ciebie języków (Ruby, JS, Python) nie ma funkcji wnioskowania typu jako funkcji językowej, chociaż implementacje mogą go używać do zwiększenia wydajności. Zamiast tego używają dynamicznego pisania, gdy wartości mają typy, ale zmienne lub inne wyrażenia nie.
amon
2
JS nie pozwala ci pominąć tego typu - po prostu nie pozwala ci to zadeklarować. Wykorzystuje dynamiczne pisanie, gdzie wartości mają typy (np. trueJest boolean), a nie zmienne (np. var xMogą zawierać wartość dowolnego typu). Ponadto, wnioskowanie o typach dla takich prostych przypadków, jak te z pytania, było prawdopodobnie znane dziesięć lat przed wydaniem C.
scriptin
2
To nie powoduje, że instrukcja jest fałszywa (aby wymusić coś, musisz również na to zezwolić). Istniejące wnioskowanie o typie nie zmienia faktu, że system typów C jest wynikiem jego kontekstu historycznego (w przeciwieństwie do konkretnie określonego rozumowania filozoficznego lub ograniczeń technicznych)
Tristan Burnside
2
Biorąc pod uwagę, że ML - który jest prawie tak samo stary jak C - ma wnioskowanie typu, „jest stary” nie jest dobrym wytłumaczeniem. Kontekst, w którym C był używany i rozwijany (małe maszyny, które wymagały bardzo małej powierzchni dla kompilatora) wydaje się bardziej prawdopodobny. Nie mam pojęcia, dlaczego wspomniałbyś o dynamicznych językach do pisania zamiast tylko niektórych przykładów języków z wnioskowaniem o typie - Haskell, ML, do diabła, C # - nie jest to już mało znana funkcja.
Voo,
2
@BradS. Fortran nie jest dobrym przykładem, ponieważ pierwsza litera nazwy zmiennej jest deklaracją typu, chyba że użyjesz implicit nonew takim przypadku musisz zadeklarować typ.
dmckee,