W C ++ sizeof('a') == sizeof(char) == 1
. Ma to sens intuicyjny, ponieważ 'a'
jest to literał znakowy i sizeof(char) == 1
zgodnie z definicją w standardzie.
W C jednak sizeof('a') == sizeof(int)
. Oznacza to, że wydaje się, że literały znakowe C są w rzeczywistości liczbami całkowitymi. Czy ktoś wie dlaczego? Mogę znaleźć wiele wzmianek o tym dziwactwie C, ale nie ma wyjaśnienia, dlaczego istnieje.
Odpowiedzi:
dyskusja na ten sam temat
źródło
char
zmienna nie jest liczbą int, więc uczynienie ze znaku stałej wartości jeden jest przypadkiem szczególnym. I jest to łatwe w użyciu wartości znaków bez promowania go:c1 = c2;
. OTOHc1 = 'x'
to konwersja w dół. Najważniejsze,sizeof(char) != sizeof('x')
co jest poważną awarią językową. Jeśli chodzi o wielobajtowe stałe znakowe: one są powodem, ale są przestarzałe.Pierwotne pytanie brzmi „dlaczego?”
Powodem jest to, że definicja znaku dosłownego ewoluowała i zmieniła się, starając się zachować zgodność wsteczną z istniejącym kodem.
W ciemnych dniach wczesnego C nie było żadnych typów. Do czasu, gdy po raz pierwszy nauczyłem się programowania w C, wprowadzono typy, ale funkcje nie miały prototypów, które mogłyby powiedzieć dzwoniącemu, jakie są typy argumentów. Zamiast tego ustandaryzowano, że wszystko przekazywane jako parametr będzie albo wielkością int (obejmującą wszystkie wskaźniki), albo będzie podwójne.
Oznaczało to, że kiedy pisałeś funkcję, wszystkie parametry, które nie były podwójne, były przechowywane na stosie jako wartości typu int, bez względu na to, jak je zadeklarowałeś, a kompilator umieścił kod w funkcji, aby obsłużyć to za Ciebie.
To spowodowało, że rzeczy były nieco niespójne, więc kiedy K&R napisał swoją słynną książkę, przyjęli zasadę, że literał znakowy będzie zawsze promowany do int w dowolnym wyrażeniu, a nie tylko w parametrze funkcji.
Kiedy komisja ANSI po raz pierwszy ustandaryzowała C, zmienili tę zasadę, aby literał znakowy był po prostu int, ponieważ wydawało się to prostszym sposobem osiągnięcia tego samego.
Kiedy projektowano C ++, wszystkie funkcje musiały mieć pełne prototypy (nadal nie jest to wymagane w C, chociaż jest to powszechnie akceptowane jako dobra praktyka). Z tego powodu zdecydowano, że literał znaku może być przechowywany w char. Zaletą tego w C ++ jest to, że funkcja z parametrem char i funkcja z parametrem int mają różne sygnatury. Ta zaleta nie występuje w przypadku C.
Dlatego są różne. Ewolucja...
źródło
void f(unsigned char)
Vs.void f(signed char)
f('a')
, prawdopodobnie chcesz wybrać rozwiązanie przeciążeniaf(char)
dla tego wywołania, a nief(int)
. Jak mówisz, względne rozmiaryint
ichar
nie są istotne.Nie znam konkretnych powodów, dla których literał znakowy w C jest typu int. Ale w C ++ jest dobry powód, aby tego nie robić. Rozważ to:
Można się spodziewać, że wywołanie print wybiera drugą wersję przyjmującą znak. Posiadanie znaku będącego dosłownym intem uniemożliwiłoby to. Należy zauważyć, że w literałach C ++ mających więcej niż jeden znak nadal mają typ int, chociaż ich wartość jest zdefiniowana w implementacji. Więc
'ab'
ma typint
, podczas gdy'a'
ma typchar
.źródło
używając gcc na moim MacBooku, próbuję:
co po uruchomieniu daje:
co sugeruje, że znak ma 8 bitów, jak podejrzewasz, ale literał znaku to int.
źródło
Kiedy pisano C, język asemblera MACRO-11 PDP-11 miał:
Tego rodzaju rzeczy są dość powszechne w języku asemblerowym - niskie 8 bitów będzie przechowywać kod znaku, inne bity wyczyszczone do 0. PDP-11 miał nawet:
Zapewniło to wygodny sposób załadowania dwóch znaków do niskiego i wysokiego bajtu rejestru 16-bitowego. Możesz następnie napisać je w innym miejscu, aktualizując niektóre dane tekstowe lub pamięć ekranu.
Tak więc pomysł promowania znaków do rozmiaru rejestru jest całkiem normalny i pożądany. Ale powiedzmy, że musisz umieścić „A” w rejestrze nie jako część zakodowanego na stałe kodu operacyjnego, ale z dowolnego miejsca w pamięci głównej zawierającej:
Jeśli chcesz odczytać tylko „A” z tej pamięci głównej do rejestru, który byś przeczytał?
Niektóre procesory mogą tylko bezpośrednio obsługiwać odczyt wartości 16-bitowej do rejestru 16-bitowego, co oznaczałoby, że odczyt na poziomie 20 lub 22 wymagałby wyczyszczenia bitów z `` X '' i w zależności od endianness procesora jeden lub drugi wymagałoby przesunięcia na bajt o najniższej kolejności.
Niektóre procesory mogą wymagać odczytu wyrównanego do pamięci, co oznacza, że najniższy adres musi być wielokrotnością rozmiaru danych: możesz być w stanie odczytać z adresów 24 i 25, ale nie 27 i 28.
Tak więc kompilator generujący kod w celu pobrania „A” do rejestru może preferować zmarnowanie trochę dodatkowej pamięci i zakodować wartość jako 0 „A” lub „A” 0 - w zależności od endianness, a także upewnić się, że jest prawidłowo wyrównana ( tj. nie pod dziwnym adresem pamięci).
Domyślam się, że C po prostu przeniósł ten poziom zachowania zorientowanego na procesor, myśląc o stałych znakowych zajmujących rozmiary rejestrów pamięci, potwierdzając powszechną ocenę C jako „asemblera wysokiego poziomu”.
(Patrz 6.3.3 na stronach 6-25 w http://www.dmv.net/dec/pdf/macro.pdf )
źródło
Pamiętam, jak czytałem K&R i widziałem fragment kodu, który odczytywałby po jednym znaku, dopóki nie trafił EOF. Ponieważ wszystkie znaki są prawidłowymi znakami, które mają znajdować się w pliku / strumieniu wejściowym, oznacza to, że EOF nie może być żadną wartością typu char. To, co zrobił kod, polegało na umieszczeniu odczytanego znaku w int, a następnie przetestowaniu pod kątem EOF, a następnie przekonwertowaniu na znak char, jeśli tak nie było.
Zdaję sobie sprawę, że to nie jest dokładną odpowiedzią na twoje pytanie, ale miałoby jakiś sens, gdyby reszta literałów znakowych miała wartość sizeof (int), gdyby literał EOF był.
źródło
Nie widziałem uzasadnienia (literały C char są typami int), ale oto coś, co Stroustrup miał do powiedzenia na ten temat (z Design and Evolution 11.2.1 - Fine-Grain Resolution):
Więc w większości nie powinno to powodować żadnych problemów.
źródło
Historyczny powód jest taki, że C i jego poprzednik B zostały pierwotnie opracowane na różnych modelach minikomputerów DEC PDP o różnych rozmiarach słów, które obsługiwały 8-bitowy ASCII, ale mogły wykonywać operacje arytmetyczne tylko na rejestrach. (Jednak nie PDP-11; to przyszło później). Wczesne wersje języka C definiowały
int
jako rodzimy rozmiar słowa maszyny, a każda wartość mniejsza niżint
potrzebna do poszerzeniaint
w celu przesłania do lub z funkcji lub używane w wyrażeniach bitowych, logicznych lub arytmetycznych, ponieważ tak działał podstawowy sprzęt.Z tego powodu reguły promocji liczb całkowitych nadal mówią, że
int
promowany jest każdy typ danych mniejszy niż anint
. Implementacje C mogą również używać matematyki uzupełnienia jednego zamiast uzupełnienia do dwóch z podobnych powodów historycznych. Powód, dla którego znaki ósemkowe ucieczki i stałe ósemkowe są obywatelami pierwszej klasy w porównaniu z hexem, jest podobny, ponieważ te wczesne minikomputery DEC miały rozmiary słów podzielne na trzy-bajtowe fragmenty, ale nie czterobajtowe.źródło
char
miał dokładnie 3 cyfry ósemkoweJest to prawidłowe zachowanie, zwane „integralną promocją”. Może się to zdarzyć także w innych przypadkach (głównie operatory binarne, jeśli dobrze pamiętam).
EDYCJA: Dla pewności sprawdziłem swoją kopię Expert C Programming: Deep Secrets i potwierdziłem, że literał znaku nie zaczyna się od typu int . Początkowo jest typu char, ale gdy jest używany w wyrażeniu , jest promowany do typu int . Z książki cytuję:
źródło
Nie wiem, ale zgaduję, że łatwiej było to zaimplementować w ten sposób i nie miało to większego znaczenia. Dopiero w C ++, kiedy typ mógł określić, która funkcja zostanie wywołana, należało to naprawić.
źródło
Naprawdę tego nie wiedziałem. Zanim istniały prototypy, wszystko węższe niż int było konwertowane na int podczas używania go jako argumentu funkcji. To może być częścią wyjaśnienia.
źródło
char
doint
sprawiłaby, że stałe znakowe stałyby się niepotrzebne . Istotne jest to, że język traktuje stałe znakowe inaczej (nadając im inny typ) niżchar
zmienne, a potrzebne jest wyjaśnienie tej różnicy.Jest to tylko styczne do specyfikacji języka, ale w sprzęcie procesor ma zwykle tylko jeden rozmiar rejestru - powiedzmy 32 bity - więc zawsze, gdy faktycznie działa na znaku (przez dodawanie, odejmowanie lub porównywanie), jest niejawna konwersja na int, gdy jest ładowana do rejestru. Kompilator dba o prawidłowe maskowanie i przesuwanie liczby po każdej operacji, więc jeśli dodasz, powiedzmy, 2 do (unsigned char) 254, zawinie się do 0 zamiast 256, ale wewnątrz krzemu jest to naprawdę int dopóki nie zapiszesz go z powrotem w pamięci.
To trochę akademicka uwaga, ponieważ język i tak mógł określić 8-bitowy typ literału, ale w tym przypadku specyfikacja języka lepiej odzwierciedla to, co naprawdę robi procesor.
(x86 wonks można zauważyć, że istnieje np rodem addh op, który dodaje rejestry krótkie szerokości w jednym etapie, ale wewnątrz rdzenia RISC to przekłada się na dwa etapy: dodawanie numerów, a następnie przedłużyć znak, jak dodać / extsh pary na PowerPC)
źródło
char
zmienne mają różne typy. Automatyczne promocje, które odzwierciedlają sprzęt, nie są istotne - w rzeczywistości są nieistotne, ponieważchar
zmienne są automatycznie promowane, więc nie ma powodu, aby literały znaków nie były typuchar
. Prawdziwym powodem są wielobajtowe literały, które są teraz przestarzałe.