Dlaczego literały znaków C są ints zamiast chars?

103

W C ++ sizeof('a') == sizeof(char) == 1. Ma to sens intuicyjny, ponieważ 'a'jest to literał znakowy i sizeof(char) == 1zgodnie 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.

Joseph Garvin
źródło
sizeof zwróciłby po prostu rozmiar bajtu, prawda? Czy char i int nie są równe wielkości?
Josh Smeaton
1
Jest to prawdopodobnie zależne od kompilatora (i architektury). Możesz powiedzieć, czego używasz? Standard (przynajmniej do '89) był bardzo luźny.
dmckee --- ex-moderator kitten
2
Nie. char ma zawsze rozmiar 1 bajtu, więc sizeof ('a') == 1 zawsze (w c ++), podczas gdy int może teoretycznie mieć rozmiar 1, ale wymagałoby to bajtu mającego co najmniej 16 bitów, co jest bardzo mało prawdopodobne: ), więc sizeof ('a')! = sizeof (int) jest bardzo prawdopodobne w C ++ w większości implementacji
Johannes Schaub - litb
2
... podczas gdy w C. zawsze jest źle
Johannes Schaub - litb
22
„a” to int w C - kropka. C dotarł tam pierwszy - C ustalił zasady. C ++ zmienił zasady. Można argumentować, że reguły C ++ mają więcej sensu, ale zmiana reguł C przyniosłaby więcej szkody niż pożytku, więc komitet standaryzacyjny C mądrze tego nie poruszył.
Jonathan Leffler

Odpowiedzi:

36

dyskusja na ten sam temat

„Dokładniej rzecz biorąc, promocje integralne. W K&R C praktycznie (?) Niemożliwe było użycie wartości znakowej bez uprzedniego promowania jej do wartości int, więc zmiana znaku na stałą int w pierwszej kolejności wyeliminowała ten krok. Istniało i nadal jest wiele znaków stałe, takie jak „abcd” lub jak wiele będzie pasować do int. "

Malx
źródło
Stałe wieloznakowe nie są przenośne, nawet między kompilatorami na jednej maszynie (chociaż GCC wydaje się być samoistny na różnych platformach). Zobacz: stackoverflow.com/questions/328215
Jonathan Leffler
8
Chciałbym zauważyć, że a) Ten cytat jest nieprzypisany; cytat mówi jedynie „Czy nie zgodziłbyś się z tą opinią, która została opublikowana w poprzednim wątku omawiającym dany problem?” ... oraz b) Jest to śmieszne , ponieważ charzmienna 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;. OTOH c1 = '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.
Jim Balter,
27

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

John Vincent
źródło
2
+1 ode mnie za odpowiedź „dlaczego?”. Ale nie zgadzam się z ostatnim stwierdzeniem - "Zaletą tego w C ++ jest to, że funkcja z parametrem char i funkcja z parametrem int mają różne sygnatury" - w C ++ nadal możliwe jest, aby 2 funkcje miały parametry ten sam rozmiar i różne sygnatury, np . void f(unsigned char)Vs. void f(signed char)
Peter K
3
@PeterK John mógłby to ująć lepiej, ale to, co mówi, jest zasadniczo dokładne. Motywacją do zmiany w C ++ było to, że jeśli piszesz f('a'), prawdopodobnie chcesz wybrać rozwiązanie przeciążenia f(char)dla tego wywołania, a nie f(int). Jak mówisz, względne rozmiary inti charnie są istotne.
zwol
21

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:

void print(int);
void print(char);

print('a');

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 typ int, podczas gdy 'a'ma typ char.

Johannes Schaub - litb
źródło
Tak, „Design and Evolution of C ++” mówi, że przeciążone procedury wejścia / wyjścia były głównym powodem zmiany reguł przez C ++.
Max Lybbert,
5
Max, tak, oszukiwałem. zajrzałem do standardu w dziale kompatybilności :)
Johannes Schaub - litb
18

używając gcc na moim MacBooku, próbuję:

#include <stdio.h>
#define test(A) do{printf(#A":\t%i\n",sizeof(A));}while(0)
int main(void){
  test('a');
  test("a");
  test("");
  test(char);
  test(short);
  test(int);
  test(long);
  test((char)0x0);
  test((short)0x0);
  test((int)0x0);
  test((long)0x0);
  return 0;
};

co po uruchomieniu daje:

'a':    4
"a":    2
"":     1
char:   1
short:  2
int:    4
long:   4
(char)0x0:      1
(short)0x0:     2
(int)0x0:       4
(long)0x0:      4

co sugeruje, że znak ma 8 bitów, jak podejrzewasz, ale literał znaku to int.

dmckee --- kociak byłego moderatora
źródło
7
+1 za bycie interesującym. Ludzie często myślą, że sizeof ("a") i sizeof ("") są znakami char * i powinny dać 4 (lub 8). Ale w rzeczywistości są one w tym momencie char [] (sizeof (char [11]) daje 11). Pułapka dla początkujących.
paxdiablo
3
Literał znakowy nie jest promowany do int, to już jest int. Nie ma żadnej promocji, jeśli obiekt jest operandem operatora sizeof. Gdyby tak było, zniweczyłoby to rozmiar celu.
Chris Young,
@Chris Young: Ya. Czek. Dzięki.
dmckee --- kociak byłego moderatora
8

Kiedy pisano C, język asemblera MACRO-11 PDP-11 miał:

MOV #'A, R0      // 8-bit character encoding for 'A' into 16 bit register

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:

MOV #"AB, R0     // 16-bit character encoding for 'A' (low byte) and 'B'

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:

address: value
20: 'X'
21: 'A'
22: 'A'
23: 'X'
24: 0
25: 'A'
26: 'A'
27: 0
28: 'A'

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 )

Tony Delroy
źródło
5

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

int r;
char buffer[1024], *p; // don't use in production - buffer overflow likely
p = buffer;

while ((r = getc(file)) != EOF)
{
  *(p++) = (char) r;
}
Kyle Cronin
źródło
Jednak nie sądzę, że 0 jest prawidłowym znakiem.
gbjbaanb
3
@gbjbaanb: Jasne, że tak. To znak zerowy. Pomyśl o tym. Czy uważasz, że plik nie powinien zawierać żadnych bajtów zerowych?
P Daddy
1
Przeczytaj wikipedię - „Rzeczywista wartość EOF to zależna od systemu liczba ujemna, zwykle -1, która gwarantuje nierówność dowolnego prawidłowego kodu znaku”.
Malx
2
Jak mówi Malx - EOF nie jest typem char - to typ int. getchar () i przyjaciele zwracają wartość int, która może przechowywać dowolne znaki, jak również EOF bez konfliktów. To naprawdę nie wymagałoby dosłownych znaków, aby miały typ int.
Michael Burr,
2
EOF == -1 pojawiło się długo po stałych znakowych C, więc to nie jest odpowiedź i nawet nie ma znaczenia.
Jim Balter,
5

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):

W języku C typ literału znakowego, na przykład 'a'is int. Zaskakujące jest, że podanie 'a'typu charw C ++ nie powoduje żadnych problemów ze zgodnością. Z wyjątkiem patologicznego przykładu sizeof('a'), każdy konstrukt, który można wyrazić zarówno w C, jak i C ++, daje ten sam wynik.

Więc w większości nie powinno to powodować żadnych problemów.

Michael Burr
źródło
Ciekawy! Kinda zaprzecza temu, co inni mówili o tym, że komitet normalizacyjny C „mądrze” postanowił nie usuwać tego dziwactwa z C.
j_random_hacker
2

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 intjako rodzimy rozmiar słowa maszyny, a każda wartość mniejsza niż intpotrzebna do poszerzenia intw 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 intpromowany jest każdy typ danych mniejszy niż an int. 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.

Davislor
źródło
... i charmiał dokładnie 3 cyfry ósemkowe
Antti Haapala
1

Jest 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ę:

Literały znakowe mają typ int i docierają do niego postępując zgodnie z regułami promocji z typu char. Jest to zbyt krótko omówione w K&R 1, na stronie 39, gdzie jest napisane:

Każdy znak w wyrażeniu jest konwertowany na int .... Zwróć uwagę, że wszystkie zmiennoprzecinkowe w wyrażeniu są konwertowane na double .... Ponieważ argument funkcji jest wyrażeniem, konwersja typów ma miejsce również wtedy, gdy argumenty są przekazywane do funkcji: in szczególnie char i short stają się int, float staje się double.

PolyThinker
źródło
Jeśli wierzyć innym komentarzom, wyrażenie „a” zaczyna się od typu int - wewnątrz funkcji sizeof () nie jest wykonywana żadna promocja typu. Wygląda na to, że „a” ma typ int, to po prostu dziwactwo C.
j_random_hacker
2
Char dosłowny nie mają typu int. Norma ANSI / ISO 99 nazywa je „stałymi znakowymi typu integer” (aby odróżnić je od „stałych szerokich znaków”, które mają typ wchar_t), a konkretnie mówi: „Stała znakowa będąca liczbą całkowitą ma typ int.”
Michael Burr,
Chodziło mi o to, że nie zaczyna się od typu int, ale raczej jest konwertowane na int z char (edytowana odpowiedź). Oczywiście, prawdopodobnie nie dotyczy to nikogo poza twórcami kompilatorów, ponieważ konwersja jest zawsze wykonywana.
PolyThinker
3
Nie! Jeśli przeczytasz standard ANSI / ISO 99 C , zauważysz, że w C wyrażenie „a” zaczyna się od typu int. Jeśli masz funkcję void f (int) i zmienna char c, to f (c) będzie wykonywać integralną promocji, ale f ( 'a') nie jako typ 'A' jest już int. Dziwne ale prawdziwe.
j_random_hacker
2
„Dla pewności” - możesz być bardziej pewny czytając instrukcję: „Literały znakowe mają typ int”. „Mogę tylko przypuszczać, że była to jedna z cichych zmian” - błędnie zakładasz. Literały znakowe w C zawsze były typu int.
Jim Balter,
0

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

Roland Rabien
źródło
0

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.

Blaisorblade
źródło
1
Kolejna kiepska „odpowiedź”. Automatyczna konwersja chardo intsprawił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ż charzmienne, a potrzebne jest wyjaśnienie tej różnicy.
Jim Balter,
Dziękuję za wyjaśnienie, które podałeś poniżej. Możesz chcieć dokładniej opisać swoje wyjaśnienie w odpowiedzi, która powinna zostać poddana głosowaniu w górę i łatwo dostrzeżona przez odwiedzających. Poza tym nigdy nie powiedziałem, że mam tutaj dobrą odpowiedź. Dlatego twoja ocena wartości nie jest pomocna.
Blaisorblade
0

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)

Crashworks
źródło
1
Kolejna zła odpowiedź. Problem polega na tym, dlaczego literały znakowe i charzmienne mają różne typy. Automatyczne promocje, które odzwierciedlają sprzęt, nie są istotne - w rzeczywistości są nieistotne, ponieważ charzmienne są automatycznie promowane, więc nie ma powodu, aby literały znaków nie były typu char. Prawdziwym powodem są wielobajtowe literały, które są teraz przestarzałe.
Jim Balter,
@Jim Balter Wielobajtowe literały wcale nie są przestarzałe; są wielobajtowe znaki Unicode i UTF.
Crashworks,
@Crashworks Mówimy o wielobajtowego znakowych literały, a nie wielobajtowe strunowe literałów. Staraj się uważać.
Jim Balter,
4
Chrashworks pisał postacie . Powinieneś był napisać, że literały szerokich znaków (powiedzmy L'à ') zajmują więcej bajtów, ale nie są nazywane wielobajtowymi literałami znaków. Bycie mniej aroganckim pomoże ci być bardziej dokładnym.
Blaisorblade
@Blaisorblade Szerokie literały znaków nie są tutaj istotne - nie mają one nic wspólnego z tym, co napisałem. Byłem dokładny, a ty brakuje ci zrozumienia, a twoja fałszywa próba poprawienia mnie jest arogancka.
Jim Balter,