Loading AI tools
język programowania Z Wikipedii, wolnej encyklopedii
C – imperatywny, proceduralny język programowania ogólnego przeznaczenia[3], stworzony na początku lat 70. XX wieku przez Dennisa Ritchiego, ówczesnego pracownika Bell Labs. W języku C powstały narzędzia systemowe dla Uniksa, a potem również kod systemu Unix. Język C został ustandaryzowany w 1989 roku przez ANSI. Od tego czasu jego rozwojem zajmuje się grupa robocza w ramach ISO.
![]() Logo języka | |
Pojawienie się |
1972 |
---|---|
Paradygmat | |
Typowanie | |
Implementacje |
Borland Turbo C, GCC, Microsoft Visual C, MinGW, LLVM, Tiny C Compiler |
Pochodne | |
Aktualna wersja stabilna |
C18 (czerwiec 2018)[1] |
Aktualna wersja testowa |
C2x[2] |
Twórca | |
Platforma sprzętowa | |
Platforma systemowa | |
Strona internetowa |
Język C powstawał jako rozwinięcie języka B, wzbogacając się stopniowo o kolejne funkcje. Okresy najszybszego rozwoju języka C to lata 1972–1973 oraz 1977–1979. Z kolei lata 1980. to czas zdobywania przez niego popularności, czego efektem była dostępność kompilatorów dla praktycznie wszystkich używanych wtedy architektur komputerów i systemów operacyjnych.
C jest jednym z popularniejszych języków programowania. Służy zarówno do pisania elementów systemów operacyjnych, jak i aplikacji użytkowych. Mimo to spotyka się z krytyką z powodu pewnych decyzji projektowych, takich jak priorytety operatorów lub nadmiernie lakoniczna składnia.
Początki C są ściśle związane z rozwojem systemu Unix, napisanego pierwotnie przez Dennisa Ritchiego i Kena Thompsona w asemblerze na komputer PDP-7. Późniejsza wersja systemu, przeznaczona na maszynę PDP-11, również powstała w asemblerze[4].
Thompson czuł potrzebę wykorzystania języka wyższego poziomu do napisania narzędzi systemowych dla Uniksa. Początkowo próbował przygotować kompilator Fortranu, lecz dość szybko porzucił ten pomysł. W zamian stworzył własną, okrojoną wersję BCPL, którą nazwał B. W języku tym powstało niewiele narzędzi, ponieważ wynikowe programy były powolne oraz nie mogły korzystać z adresowania poszczególnych bajtów pamięci (funkcji dostępnej np. w PDP-11)[4].
Dennis Ritchie rozpoczął w 1972 roku rozwijanie języka B. Jedną z pierwszych zmian było dodanie typów danych dla zmiennych. Początkowo dostępne były dwa: znakowy (char
) i całkowitoliczbowy (int
). Wprowadzono również sposób deklarowania zmiennych typu wskaźnikowego oraz odwołań do funkcji. Tak znaczące zmiany spowodowały, że Ritchie nadał językowi nową nazwę: C[4].
Zgodnie z sugestią Alana Snydera, na początkowym etapie rozwoju języka Ritchie rozdzielił operatory koniunkcji i alternatywy na bitowe (składające się z pojedynczego symbolu) oraz logiczne (złożone z dwóch znaków). W języku B oba warianty oznaczano w taki sam sposób, a odpowiednie działanie było wybierane przez interpreter, w zależności od kontekstu[4].
Kompilator C, wraz z kilkoma programami napisanymi w tym języku, został dołączony do drugiej wersji systemu Unix[5]. Latem 1973 jądro tego systemu zostało przepisane w języku C[4].
Do tego czasu wprowadzono również do składni języka obsługę struktur. Mniej więcej w tym samym czasie powstała pierwsza wersja preprocesora. Umożliwiała ona dołączanie plików zewnętrznych (dyrektywa #include
) oraz wykonywanie prostych podstawień (definiując makra bez parametrów z użyciem #define
). Funkcjonalności takie jak kompilacja warunkowa czy parametryzowane makra zostały wprowadzone niedługo później – przez Mike’a Leska oraz Johna Reisera. Na tym etapie rozwoju języka preprocesor stanowił opcjonalny dodatek, który nie musiał być uruchamiany w trakcie kompilacji[4].
Około 1977 roku Dennis Ritchie, Ken Thompson i Stephen Johnson skupili się na przenośności oprogramowania napisanego w C[4]. W tym celu przyjrzeli się często wykorzystywanym konstrukcjom i zachowaniom, które były zależne od architektury komputera. Ponadto Johnson napisał kompilator pcc , w którym oddzielono część związaną z architekturą od części uniwersalnej, co ułatwiło wspieranie nowych platform sprzętowych oraz utrzymanie tych już wspieranych[6][4].
W 1978 opublikowane zostało pierwsze wydanie książki The C Programming Language (wyd. polskie Język C, 1987[7]), autorstwa Briana Kernighana i Dennisa Ritchiego. Stanowiła ona pierwszą, nieformalną, specyfikację języka C[4]. Wersja ta bywa nazywana C78, gdzie nazwa pochodzi od roku wydania. Inne skrócone określenie, K&R C, pochodzi od nazwisk autorów książki[8].
Jeszcze przed publikacją książki do języka C włączono kwalifikatory short
i long
, pozwalające określić wielkość zmiennej typu całkowitoliczbowego, a także specyfikator unsigned
, oznaczający liczby nieujemne[9].
W specyfikacji C78 złożone operatory przypisania uzyskały jednoznaczną składnię. Do roku 1976 instrukcja „zmniejsz wartość zmiennej i
o 10” mogłaby zostać napisana jako: i =- 10
, co jest bardzo podobne do polecenia zapisania w zmiennej i
wartości -10. Nowe symbole cechowała zamieniona kolejność operatora i znaku równości, np. -=
zamiast =-
. Dwuznaczne oznaczenia wywodziły się jeszcze z języka B i sposobu przetwarzania kodu przez jego analizator składni[4].
W książce Kernighana i Ritchiego znalazł się również opis biblioteki wejścia/wyjścia. Podwaliny pod nią położył w 1972 roku Mike Lesk, pisząc „przenośną bibliotekę wejścia/wyjścia”. W następnych latach, wraz z pracami nad przenośnością systemu Unix, została ona rozwinięta i usprawniona. Funkcje biblioteczne nie zostały przez autorów książki uznane za część języka, lecz dodatek do niego[4][10].
Autorzy publikacji wspomnieli również o planowanym zniesieniu większości ograniczeń dotyczących struktur[11]. Do 1980 roku możliwe stało się przekazywanie ich jako parametry funkcji oraz bezpośrednie przypisywanie. Jedynie brak notacji dla literału powodował, że nie były typem pierwszoklasowym. W okresie po wydaniu książki do języka trafiło również wsparcie dla typów wyliczeniowych oraz funkcji niezwracających żadnej wartości, choć o tych kwestiach nie można znaleźć informacji w I wydaniu publikacji Kernighana i Ritchiego[4].
Język taki, jak opisany przez Kernighana i Ritchiego w 1978 roku nie przewidywał żadnej możliwości określenia typów przyjmowanych przez parametry funkcji[4]. Powstały odrębne narzędzia, takie jak lint, które przeprowadzały analizę kodu bardziej rygorystyczną niż kompilator, a następnie zgłaszały problemy ze spójnością i przestrzeganiem dobrych praktyk[6].
Książka Język C nie stanowiła ścisłego standardu. Miejscami pozostawiała miejsce do interpretacji szczegółów języka. To, w połączeniu z dalszym rozwojem i popularyzacją C na różnych platformach, spowodowało potrzebę standaryzacji[4].
W latach 80. XX wieku Bjarne Stroustrup stworzył język C++, który stanowił rozszerzenie C, dodające przede wszystkim wsparcie dla programowania obiektowego. Dodatków tych nie wcielono do C, a C++ został niezależnie ustandaryzowany przez ANSI i ISO[12].
W 1983 roku, ANSI sformowała komitet X3J11, którego celem było „sformułowanie jasnego, spójnego i jednoznacznego standardu C, który skodyfikuje powszechnie istniejący język w sposób promujący zgodność programów z różnymi środowiskami uruchomieniowymi”. Odpowiedni dokument przyjęto w 1989 roku jako standard ANSI X3.159-1989 „Język programowania C”[4]. Opisana w nim wersja języka jest zwykle określana jako ANSI C lub C89 (od daty publikacji)[8].
Rok później, w 1990 standard ANSI C został przyjęty (ze zmianami redakcyjnymi) przez Międzynarodową Organizację Normalizacyjną jako ISO/IEC 9899:1990[4][8]. Ta wersja bywa również nazywana C90 lub ISO C, choć opisuje ten sam język, co dokument ANSI[8].
Oprócz dodatków autorstwa Ritchiego, zaimplementowanych po publikacji książki Język C, komitet standaryzacyjny ANSI wprowadził do języka np. prototypy funkcji czy wskaźniki na nieznany typ (void*
). W C89 pojawiła się także możliwość deklarowania typów parametrów funkcji w nagłówku, ale starsza składnia pozostała dopuszczalna dla zgodności z istniejącym kodem. Standard wzbogacił również preprocesor o dodatkowe instrukcje, takie jak #elif
oraz wsparcie dla łączenia napisów (operator ##
)[13]. Wzorując się na języku C++ zaadaptowano również składnię i znaczenie kwalifikatorów typu const
i volatile
[14].
Podczas prac nad językiem Kernighan i Ritchie nie skupiali się na ustandaryzowaniu biblioteki standardowej. Odpowiednie funkcje dostarczał system Unix, niezależnie od stosowanego kompilatora. Implementacje C na innych platformach były wyposażone w biblioteki, które starały się naśladować to, co było dostępne na Uniksie, jednak nie istniały wytyczne określające zestaw procedur, jakie muszą być udostępnione programistom. Komitet ANSI przygotował opis biblioteki standardowej języka C, wymagający dostarczenia programiście odpowiednich funkcji o ściśle określonym działaniu i przeznaczeniu[13].
W 1995 roku została opublikowana poprawka do standardu, wprowadzająca obsługę wielobajtowych kodowań znaków (ang. wide characters, typ wchar_t
), wraz z zestawem funkcji do operacji na składających się z nich napisach[15]. Inne zmiany obejmują dodanie pliku nagłówkowego iso646.h
, definiującego makra, których można użyć do zastąpienia niektórych operatorów słowami, jak and
zamiast &&
czy not_eq
zamiast !=
. Oprócz istniejących już trójznaków (będących innym sposobem zapisania pewnych symboli), wprowadzono 6 dwuznaków o tym samym zastosowaniu: <:
, :>
, <%
, %>
, %:
i %:%:
[16][17]. W poprawce ustanowiono również wymóg, by implementacje ustawiały makro __STDC_VERSION__
na wartość 199409, co ma wskazywać na zgodność z nowszą wersją standardu[16].
Druga wersja standardu języka C została opublikowana przez ISO w 1999 roku. Zaczerpnęła z języka C++ funkcje inline
i komentarze jednoliniowe (rozpoczynające się sekwencją //
). Zdefiniowano wtedy również kolejne typy danych: long long int
(minimum 64-bitowa liczba całkowita) oraz typ logiczny _Bool
, który może przyjmować jedynie wartości 0 oraz 1[18][19] (wraz z makrami bool
, true
i false
)[20]. W tej wersji standardu dodano także kolejny kwalifikator typu restrict
[19].
Od standardu C99 zdefiniowany jest sposób obsługi liczb zespolonych w języku. Służą do tego typy z określeniami _Imaginary
oraz _Complex
[18], przechowujące – odpowiednio – wartości urojone oraz zespolone. Kwalifikatorów tych można używać jedynie z typami zmiennoprzecinkowymi[20].
W 1999 roku do języka C wprowadzono także obsługę tablic o długości ustalanej w czasie działania programu[19]. Implementacje wspierające wersję C99 powinny ustawić makro __STDC_VERSION__
na 199901[20].
Kolejna edycja standardu została przyjęta w grudniu 2011. Podczas prac nad nią, komitety odpowiedzialne za języki C i C++ współpracowały ze sobą, by zachować wzajemną zgodność obu na tyle, na ile to możliwe. W tej wersji standardu zadecydowano, że tablice o zmiennej długości oraz typy zespolone staną się funkcjonalnością opcjonalną (C99 wymagał, by kompilatory je wspierały). Rozbudowano również bibliotekę standardową o procedury sprawdzające zakres i granice obszaru pamięci podczas operowania na napisach. Z powodu licznych podatności usunięto funkcję gets()
, zastępując ją bezpieczniejszą gets_s()
[21].
Publikując tę wersję standardu, komitet ustandaryzował wsparcie dla wielowątkowości, m.in. definiując sposoby zarządzania i synchronizowania wątków oraz wprowadzając typy atomowe (w tym kwalifikator typu _Atomic
). Umożliwiono również oznaczenie funkcji bez powrotu słowem kluczowym _Noreturn
, co pozwala kompilatorom na pewne optymalizacje generowanego kodu[21].
W 2011 roku do języka C wprowadzono także mechanizm definiowania makr generycznych ze względu na typ parametrów oraz wsparcie dla literałów napisowych w standardach Unicode i UTF-8[21].
W tej wersji __STDC_VERSION__
przyjmuje wartość 201112[22].
Czwarta wersja standardu nie przyniosła żadnych znaczących zmian w języku w stosunku do C11[23]. Dokument przyjęty przez ISO w czerwcu 2018[1] przyniósł poprawki i uściślenia tekstu, dlatego zmiany w zachowaniu kompilatorów po jego implementacji wynikają z interpretacji niejednoznacznych zapisów[23]. Jedyną zmianą normatywną było nadanie wartości 201710 makro __STDC_VERSION__
[22].
Formalna gramatyka języka C jest opisana w standardzie ISO C[27]. Podział kodu na linie oraz białe znaki są ignorowane. Ich stosowanie jest obowiązkowe jedynie, kiedy służą rozdzieleniu poszczególnych jednostek leksykalnych[28].
Program w języku C zapisywany jest w plikach źródłowych, które nie muszą być kompilowane w tym samym czasie. Plik źródłowy razem z plikami dołączonymi dyrektywą #include
tworzy jednostkę tłumaczenia, która po przetłumaczeniu na kod maszynowy może być konsolidowana z innymi w celu stworzenia wykonywalnego programu[29].
Identyfikator stanowi nazwę obiektu, funkcji, etykiety itp. Może on oznaczać różne byty w różnych momentach wykonywania programu[30]. Istotna jest jedynie określona liczba znaków w nazwie. W przypadku, gdy dwa identyfikatory różnią się jedynie znakami na nieistotnych pozycjach, zachowanie jest nieokreślone. Minimalna znacząca długość jest zależna od implementacji i może się różnić w zależności od sposobu linkowania określanego przezeń bytu[31], choć standard wymaga, by przynajmniej 63 pierwsze znaki identyfikatorów wewnętrznych i 31 początkowych znaków identyfikatorów zewnętrznych było znaczących[32].
Identyfikatory mogą składać się jedynie z liter, cyfr i znaku podkreślenia, choć cyfra nie może występować na pierwszym miejscu[31]. Aby zastosować w nazwie znaki spoza tego zbioru, konieczne jest użycie sekwencji ucieczki \uxxxx
lub \Uxxxxxxxx
(gdzie x
to cyfra szesnastkowa)[33]. Na potrzeby kompilatora i powiązanej z nim biblioteki zarezerwowane są identyfikatory zaczynające się dwoma znakami podkreślenia lub podkreśleniem z następującą po nim wielką literą[34].
W zależności od tego, do czego odwołuje się identyfikator, należy on do jednej z przestrzeni nazw[35]:
Ponadto każdy identyfikator ma swój zakres widoczności, zależny od miejsca deklaracji[30].
Z identyfikatorem może być skojarzony jeden z trzech sposobów łączności: zewnętrzna, wewnętrzna lub brak. Łączność decyduje o tym, czy dwa identyczne identyfikatory w różnych miejscach programu odnoszą się do tego samego bytu[35]:
Ciąg operatorów i operandów stanowi w języku C wyrażenie. Oznacza ono obliczenie wartości lub odwołanie do obiektu. W trakcie jego wywoływania mogą także występować skutki uboczne[36] (na przykład inkrementacja zmiennej[37]). Kolejność obliczania wartości i zachodzenia skutków ubocznych jest nieokreślona, lecz zgodna z pierwszeństwem operatorów. Standard języka C gwarantuje, że zdarzą się one przed następnym punktem sekwencyjnym[38].
Jeżeli tego samego obiektu dotyczy więcej niż jeden skutek uboczny i nie są one rozdzielone punktem sekwencyjnym, wtedy zachowanie jest nieokreślone[38][36]. Takie punkty wprowadzane są[39]:
&&
, ||
, ,
;?:
a drugiego lub trzeciego;W języku C są zdefiniowane następujące operatory[40]:
+
, -
, *
, /
, %
~
, &
, ^
, |
.
, ->
()
++
, --
!
, &&
, ||
<
, >
, <=
, >=
, ==
, !=
<<
, >>
=
*=
, /=
, %=
, +=
, -=
, <<=
, >>=
, &=
, ^=
, |=
sizeof
(typ)
,
?:
*
, &
, []
_Alignof
()
Obiekt (zwany też zmienną[41]), według standardu języka C, to region pamięci środowiska wykonawczego, mogący reprezentować wartości[42]. Wyrażenie, które może odwoływać się do obiektu jest l-wartością. Jej typ określa, w jaki sposób interpretować zawartość obiektu[43].
Przykładem l-wartości jest identyfikator obiektu. Może nią być również rezultat dereferencji wskaźnika[43].
Deklaracja obiektu polega na podaniu typu danych oraz opcjonalnie klasy pamięci i sposobu linkowania, po których występuje przynajmniej jeden identyfikator tworzonego obiektu (lub obiektów)[44].
W języku C rozróżniane są[45]:
{...}
).Definicje funkcji w języku C nie mogą być zagnieżdżane[46].
Funkcje w języku C nie mogą być przeciążane[47], ale istnieje mechanizm definiowania funkcji o zmiennej liczbie argumentów[48].
if
i switch
do realizacji równoważnych fragmentów kodu.W języku C można wyróżnić trzy klasy instrukcji sterujących. Pierwszą z nich są polecenia umożliwiające warunkowe wykonanie fragmentów kodu. Do tej grupy należą[49]:
if ... else
– pozwala na wykonanie jednej z dwóch gałęzi kodu na podstawie wartości typu logicznego,switch
– wykonuje jeden z wielu bloków kodu na podstawie porównania wartości liczbowej z wyrażeniami w poszczególnych etykietach case
.for
.Druga grupa instrukcji sterujących służy realizacji pętli. Każda z nich jest wykonywana tak długo, jak podany warunek jest prawdziwy. Składają się na nią[50]:
while
– warunek jest sprawdzany przed każdą iteracją;do ... while
– warunek jest sprawdzany po każdej iteracji;for
– pozwala na określenie instrukcji, która wykona się przed pierwszą iteracją oraz instrukcji do wywołania po każdym przebiegu pętli.Ostatnią grupę stanowią instrukcje skoku. Należą do nich[51]:
goto
– realizuje skok do etykiety o podanej nazwie, ale nie jest możliwe wyjście z aktualnie wykonywanej funkcji;continue
– przerywa wykonywanie bieżącej iteracji pętli i przechodzi do kolejnej;break
– przerywa wykonywanie pętli lub instrukcji switch
;return
– przerywa wykonywanie bieżącej funkcji i opcjonalnie przekazuje wartość do miejsca wywołania.Dyrektywy preprocesora rozpoczynają się od znaku #
i muszą znajdować się w osobnych liniach (dopuszczalne jest by przed symbolem kratki znajdowały się spacje)[52].
Preprocesor w języku C pozwala na manipulację kodem źródłowym przed właściwą kompilacją. Wspiera mechanizmy kompilacji warunkowej (dyrektywa #if
i jej warianty) oraz dołączania innych plików źródłowych (#include
). Odpowiada również za rozwijanie makr (zdefiniowanych przy użyciu #define
)[53][54].
Dzięki dyrektywie #pragma
możliwe jest przekazywanie instrukcji specyficznych dla kompilatora[55][uwaga 1].
W języku C występują komentarze liniowe i blokowe. Ich zawartość przetwarzana jest przez kompilator wyłącznie w celu znalezienia końca komentarza (symbolizowanego odpowiednią sekwencją lub końcem linii). Do ich wprowadzenia służą znaki /*
(blokowy, kończy się */
) oraz //
(liniowy, trwa do końca linii). Sekwencje te tracą swoje znaczenie, jeśli znajdują się w literale tekstowym lub wewnątrz innego komentarza[56].
W trakcie kompilacji, komentarze zastępowane są znakiem spacji[29].
Typ danych określa zbiór wartości, które może przyjąć dany obiekt, jak również dozwolone operacje na nim[57]. Język udostępnia zestaw typów podstawowych oraz mechanizmów konstruowania typów pochodnych[58].
Szczególnym typem danych w C jest typ pusty void
, który nie przechowuje żadnej wartości. W związku z tym można wykorzystywać go jedynie w sytuacjach, gdy wartość nie jest wymagana – np. jako lewy argument operatora ,
lub w charakterze instrukcji. Rzutowanie tego typu na jakikolwiek inny typ, zarówno jawne, jak i niejawne jest niedozwolone[59]. Słowem void
oznacza się między innymi funkcje nie zwracające nic[60].
W języku C istnieje kilka bazowych typów danych, które można dookreślać z użyciem odpowiednich słów kluczowych w celu uzyskania odpowiedniego zakresu wartości. Służą do przechowywania liczb całkowitych (char
i int
) oraz zmiennoprzecinkowych (float
i double
)[61].
Razem z typem int
można stosować kwalifikatory short
oraz long
. Pozwalają one programiście wykorzystywać typy danych krótsze i dłuższe niż naturalne dla danej architektury. Ponadto nazwę każdego typu, służącego do przechowywania liczb całkowitych, można również poprzedzić słowem signed
lub unsigned
, aby określić, czy dany obiekt ma być w stanie przechowywać liczby ujemne[62]. Reprezentacja bitowa wartości, które można zapisać zarówno w wariancie signed
, jak i unsigned
danego typu jest w obu wariantach taka sama[63].
Standard języka C nie ustala w sposób sztywny zakresów wartości, jakie muszą się zmieścić w obiektach poszczególnych typów. Podobnie nie są określone ich rozmiary w bitach lub bajtach[61]. Od implementacji języka wymaga się, by poszczególne typy danych pozwalały na przechowywanie przynajmniej liczb z ustalonego przedziału[32], a w przypadku typu logicznego _Bool
– by miał rozmiar wystarczający do zmieszczenia wartości 0 i 1[64].
Typ | Minimalny zakres | Minimalny rozmiar w bitach |
---|---|---|
signed char |
8 | |
unsigned char |
8 | |
short int [uwaga 2] |
16 | |
unsigned short int [uwaga 2] |
16 | |
int |
16 | |
unsigned int [uwaga 2] |
16 | |
long int [uwaga 2] |
32 | |
unsigned long int [uwaga 2] |
32 | |
long long int [uwaga 2] |
64 | |
unsigned long long int [uwaga 2] |
64 |
W powyższej tabeli zebrano minimalne wymagania stawiane dostępnym w C typom całkowitoliczbowym. Dodatkowym ograniczeniem, stawianym przez standard jest to, aby kolejne typy miały zakres niemniejszy od poprzednich. Na przykład obiekt typu short
nie może być dłuższy niż int
, który z kolei musi być niedłuższy od long
[61].
Użycie kwalifikatora long
jest dopuszczalne również w połączeniu z typem double
, choć standard C nie gwarantuje, że uzyskany w ten sposób typ będzie miał większą pojemność niż wyjściowy. Podobnie jak w przypadku liczb całkowitych, dostępne typy zmiennoprzecinkowe również nie mają sztywno określonego zakresu wartości oraz minimalnej dokładności[67].
W czasie, gdy ukazało się pierwsze wydanie książki Język C Kernighana i Ritchiego, dopuszczalna była również konstrukcja long float
, równoważna typowi double
, jednak została ona zniesiona przez standard ANSI C[60].
Cecha | float |
double |
long double |
---|---|---|---|
Liczba dziesiętnych cyfr znaczących | 6 | 10 | 10 |
Największa liczba dodatnia | |||
Najmniejsza dodatnia liczba znormalizowana | |||
Epsilon maszynowy |
Do typów podstawowych należy również wyliczenie. Składa się ono z listy stałych symbolicznych, każda o wartości będącej liczbą całkowitą. Typy wyliczeniowe funkcjonalnie są równoważne całkowitoliczbowym[69][60].
Z niezerowej liczby obiektów tego samego typu można stworzyć tablicę[70]. Jej elementy są ułożone w pamięci komputera po kolei i bez żadnych przerw[71], a dostęp do nich można uzyskać za pomocą składni tablica[indeks]
, gdzie indeksy rozpoczynają się od zera[72][uwaga 3]. Do raz utworzonej zmiennej typu tablicowego nie jest możliwe przypisanie innej tablicy[74]. Kiedy nazwa tablicy zostanie użyta w wyrażeniu, dokonuje się jej niejawna konwersja na wskaźnik do zerowego elementu tablicy. Wskaźnik ten nie może być użyty w charakterze l-wartości[43].
Do przechowywania adresu obiektu określonego typu służą wskaźniki[75]. Dozwolone jest wykonywanie na nich niektórych operacji arytmetycznych. Dowolny wskaźnik można przyrównać do zera (oznaczającego literał pusty, zapisywany też jako NULL
)[76]. Porównywanie oraz odejmowanie dwóch wskaźników jest zdefiniowane wyłącznie wtedy, kiedy dotyczą one tej samej tablicy lub – wyłącznie w przypadku porównywania – tego samego obiektu złożonego[77]. Do wartości wskaźnika można również dodać lub odjąć dowolną liczbę całkowitą. Zabronione jest wykonywanie innych działań, takich jak mnożenie czy dzielenie[76]. W działaniach arytmetycznych, w których biorą udział liczby całkowite oraz wskaźniki, liczba jest traktowana jako liczba elementów tablicy odpowiedniego typu, a nie liczba bajtów[uwaga 4][78].
Jedna lub kilka zmiennych może tworzyć strukturę. W przeciwieństwie do tablic, takie składowe mogą być różnych typów, a poszczególne pola rozróżnia się z użyciem ich identyfikatorów[79]. Każda struktura stanowi odrębną przestrzeń nazw, toteż pole o tej samej nazwie może występować w kilku strukturach[80][35]. Zagnieżdżanie struktur jest dozwolone – składowe mogą również być strukturami. Standard ISO C zabrania natomiast rekursywnego zagnieżdżania struktury samej w sobie. Mimo to struktura może zawierać wskaźnik na inną strukturę tego samego typu[81]. Zarówno przypisywanie do zmiennej typu strukturalnego nowej wartości, jak i przekazywanie jej jako argument oraz zwracanie z funkcji są dopuszczalnymi operacjami[82].
Układ składowych w pamięci jest zależny od implementacji, z pewnymi wyjątkami: pierwsze pole musi się zaczynać od razu na początku struktury (tak by wskaźnik na strukturę był równy co do wartości wskaźnikowi na jej pierwsze pole), a ponadto pola muszą następować w pamięci w tej samej kolejności, co w deklaracji. Dopuszcza się jednak istnienie niewykorzystanych i nienazwanych przestrzeni w środku oraz na końcu struktury[83].
Struktury mogą zawierać również pola bitowe, które pozwalają na określenie rozmiaru obiektu z dokładnością do bitów[84]. Takie pola w trakcie kompilacji są umieszczane w odpowiednio dużej, adresowalnej jednostce pamięci. Jeżeli ma ona wystarczający rozmiar, by pomieścić sąsiednie pola, zostaną one w niej razem upakowane. Standard ISO C nie określa kolejności, w której przylegające do siebie pola bitowe są przechowywane w pamięci[83]. Pobieranie adresu pola bitowego jest niedozwolone[85].
Przykładowa deklaracja struktury opisującej węzeł pewnego drzewa[86]:
struct tnode {
char *word;
int count;
struct tnode *left;
struct tnode *right;
}
Typem podobnym do struktur są unie. Pozwalają one przechowywać wartości różnych typów pod tym samym adresem, z poszanowaniem ograniczeń dotyczących ich ułożenia w pamięci. Unia ma przynajmniej taki rozmiar, jak największa spośród jej składowych. Wszystkie operacje, które są dopuszczalne dla struktur, można wykonywać również na uniach[87].
Standard języka C definiuje również typy atomowe, choć stanowią one opcjonalną funkcjonalność, która nie musi być obsługiwana przez implementacje[88]. Biblioteka standardowa dostarcza funkcje pozwalające m.in. na zmianę wartości zmiennych takich typów w sposób atomowy, tj. z gwarancją, że operacja nie zostanie przerwana przez inne działanie na tej samej zmiennej[89].
W języku C dostępne są także typy danych opisujące funkcje. Zawierają one informacje o typie parametrów oraz wartości zwracanej[70]. Zmienna typu funkcyjnego, jeśli nie jest argumentem operatora pozyskania adresu &
, niejawnie przekształca się we wskaźnik do funkcji[43]. Wywołania, dokonywane za pośrednictwem wskaźników nie są przez standard rozróżniane od tych zawierających wprost nazwę funkcji[90].
W języku C dostępny jest mechanizm pozwalający na zdefiniowanie synonimów dla istniejących typów danych. Aliasowanie nie tworzy nowego typu, zatem obiekty utworzone z użyciem zarówno pierwotnej, jak i nowej nazwy mają identyczne właściwości[91].
Mechanizm ten wykorzystuje się między innymi w celu zapewnienia przenośności oprogramowania podczas wykorzystania typów zależnych od docelowej architektury. Przykładem takiego zastosowania są size_t
i ptrdiff_t
, pochodzące z biblioteki standardowej języka C[92]. Przechowują one liczby całkowite, lecz ich dokładny typ zależy od implementacji[93].
Inny powód, dla którego typom nadawane są nowe nazwy, to ułatwienie zrozumienia programu poprzez nadanie opisowych nazw bardziej złożonym konstrukcjom[94]. Poniższe trzy deklaracje funkcji są sobie równoważne[95]:
// fv jest aliasem dla funkcji przyjmującej parametr int i nie zwracającej żadnej wartości
typedef void fv(int);
// pfv to alias dla wskaźnika na funkcję taką jak powyżej
typedef void (*pfv)(int);
// Równoważne deklaracje:
void (*signal(int, void (*)(int)))(int);
fv *signal(int, fv *);
pfv signal(int, pfv);
Kwalifikatory typów nadają specyficzne znaczenia wyrażeniom, które są l-wartością[96]. Można je ze sobą dowolnie łączyć[96].
const
const
mogą zostać umieszczone w pamięci z dostępem tylko do odczytu[99].volatile
volatile
są odpowiednim wyborem przy modelowaniu układów wejścia/wyjścia mapowanego w przestrzeni adresowej procesora[99]. Próba modyfikacji tak zdefiniowanego obiektu przez l-wartość usuwającą to ograniczenie jest zachowaniem niezdefiniowanym[102].restrict
_Atomic
<stdatomic.h>
[103].code
, data
, idata
, bdata
, xdata
, far
i pdata
[105]. Natomiast kompilator C dla z/OS wspiera __callback
, __far
, __fdptr
, __ptr32
i __ptr64
[106]. Rozszerzenia dotyczące wsparcia różnych przestrzeni adresowych implementuje również kompilator GNU C na przykład __flash
, __memx
, __far
, __ea
, __seg_fs
lub __seg_gs
. Ich dostępność zależy od docelowego procesora, na który jest kompilowany kod[107].Standard języka C gwarantuje, że na czas życia obiektu zostanie mu przydzielona pamięć. Ponadto przez cały ten czas dany obiekt będzie dostępny pod stałym adresem i będzie przechowywał ostatnio zapisaną wartość. Momenty utworzenia i zniszczenia obiektu są zależne od przypisanej mu klasy pamięci[108]. Dostęp do pamięci nie jest kontrolowany przez język[109], ale próby odczytu lub zapisu pod nieprawidłowymi adresami mogą skończyć się naruszeniami ochrony pamięci[110].
Każdy obiekt (poza polami bitowymi) składa się z ciągłej sekwencji bajtów, których kolejność i sposób kodowania wartości może zależeć od implementacji[88].
Po upłynięciu czasu życia zmiennej, wszelkie odwołania do niej prowadzą do niezdefiniowanego zachowania. Podobnie wartość wskazywana przez wskaźnik staje się nieokreślona, kiedy wskazywany obiekt kończy życie[108].
Deklarując obiekt, można wskazać jego klasę pamięci, która określa między innymi jego czas życia i zasięg widoczności. Zmienne lokalne, o ile nie użyto żadnego specyfikatora klasy pamięci, są deklarowane jako zmienne automatyczne[41]. Wartości niezainicjalizowanych zmiennych są niezdefiniowane i mogą mieć wartość zależną od kompilatora lub od tego, co akurat było w pamięci[111]. Są one dostępne przez cały czas wykonywania bloku, gdzie zostały zadeklarowane[uwaga 5]. W przypadku rekurencyjnego wkroczenia do tego samego bloku, za każdym razem tworzona jest osobna instancja obiektu. Tę klasę pamięci można również określić jawnie słowem kluczowym auto
[108].
Podobne do zmiennych automatycznych są zmienne rejestrowe. Zadeklarowanie obiektu ze słowem register
sugeruje kompilatorowi, by umieścił go w pamięci o szybkim dostępie (np. rejestrze procesora[112]). Ostateczny wybór rodzaju pamięci, w której znajdzie się zmienna, należy jednak do kompilatora. Niezależnie od tego, czy obiekt zlokalizowany będzie w pamięci adresowalnej, zabronione jest pobieranie adresu zmiennej rejestrowej[113].
Innym rodzajem zmiennych są zmienne statyczne (deklarowane słowem static
). Pamięć dla nich inicjalizowana jest w momencie uruchomienia programu i od tego momentu wszystkie odwołania dotyczą tego samego regionu pamięci[108]. W przypadku, gdy zmienną lokalną zadeklarowano jako statyczną, będzie ona przechowywała tę samą wartość pomiędzy różnymi wywołaniami funkcji. Z kolei statyczne zmienne zewnętrzne charakteryzują się linkowaniem wewnętrznym, co oznacza, że nie są widoczne na zewnątrz jednostki tłumaczenia, w której je zadeklarowano[112].
Obiektowi można również nadać klasę extern
, która jawnie informuje, że cechuje się on linkowaniem zewnętrznym. Jest to domyślna własność zmiennych deklarowanych na zewnątrz funkcji[114]. Zmienne statyczne i zewnętrzne są inicjalizowane zerem, jeśli nie podano żadnej wartości początkowej[115].
Istnieje również klasa pamięci _Thread_local
, której czas życia jest powiązany z wątkiem. Obiekty zadeklarowane w ten sposób są tworzone w momencie rozpoczęcia wątku. Dostęp pośredni do zmiennych tego rodzaju należących do innych wątków wywołuje zachowanie zależne od implementacji[108].
Formalnie, specyfikatorem klasy pamięci jest także typedef
. Zalicza się go do tej grupy dla wygody, choć nie służy do deklarowania pamięci[116].
Biblioteka języka C udostępnia funkcje służące do alokacji pamięci o arbitralnie wybranym rozmiarze (malloc
i odmiany). Zwracają one wskaźnik do pierwszego bajtu obszaru pamięci, który można następnie rzutować na dowolny inny typ wskaźnikowy. Zarezerwowany w ten sposób zakres, w zależności od wywołanej funkcji, może być zainicjalizowany samymi zerami, lecz nie musi. Czas życia obiektów w alokowanej jawnie pamięci trwa od jej przydzielenia aż do zwolnienia za pomocą funkcji free
[117].
Zastosowanie ręcznej alokacji pamięci może prowadzić do wystąpienia błędów w kodzie, wynikających np. z odwołań do wiszących wskaźników , ale także przepełnienia bufora i próby powtórnej dealokacji. Obecne w programie podatności mogą zależeć od wykorzystanego algorytmu rezerwacji pamięci[118].
Program wyświetlający na ekranie napis „Hello world” był pierwszym fragmentem kodu w języku C, umieszczonym w książce Język C[119]. Składnia kodu z pierwszego wydania tej książki odbiega od późniejszych standardów[120]. Program ten, w bardzo podobnej formie, znajdował się również w dokumencie Programming in C: A Tutorial, przygotowanym przez B. Kernighana w 1974 roku[121][122].
main()
{
printf("hello, world");
}
Powyższy kod prawdopodobnie nie zostanie skompilowany, jeśli nie wymusi się na kompilatorze przyjęcia standardu C90. Nowsza wersja tego programu, rozumiana przez współczesne kompilatory, wygląda następująco[120]:
#include <stdio.h>
int main(void){
printf("hello, world");
}
Pierwsza linijka tego kodu informuje preprocesor, aby dołączył w to miejsce zawartość pliku stdio.h
, który wchodzi w skład biblioteki standardowej języka C[123]. Zadeklarowane w nim są procedury odpowiadające za obsługę wejścia i wyjścia, w tym printf
[124]. Następnie definiowana jest funkcja main
. To od niej rozpoczyna się wykonanie programu napisanego w C. Funkcja ta nie przyjmuje żadnych argumentów i zwraca wartość całkowitą. Wewnątrz nawiasów klamrowych znajduje się lista instrukcji, które zostaną wywołane po uruchomieniu programu[125].
W przykładowym programie funkcja main
składa się z jednej instrukcji: wywołania printf
z argumentem "hello, world"
. Służy ono do wyświetlenia tekstu na ekranie[123]. Mogłaby się tam znaleźć jeszcze instrukcja return
, ale zgodnie ze standardem ISO C, umieszczenie jej w funkcji main
jest opcjonalne. Zwrócona do systemu operacyjnego zostanie wtedy wartość 0[126]. Wartość zwracana przez funkcję main
to tzw. kod wyjścia (ang. exit status albo return code), a wartość 0 zazwyczaj oznacza udane wykonanie programu[127][128].
Głównym celem, który przyświecał Dennisowi Ritchiemu przy tworzeniu języka C było ułatwienie pisania oprogramowania systemowego. Potrzebny był język wysokopoziomowy, ale jednocześnie tak wydajny jak asembler[129]. Z tego też powodu C bywa nazywany „wysokopoziomowym asemblerem”[130]. Powstały w nim m.in. Unix[130] i jądro Linux[131]. W praktyce dla każdej nowej architektury język C jest pierwszym dostępnym językiem programowania poza asemblerem[132].
W latach 80. XX wieku C zdobył popularność również wśród twórców oprogramowania aplikacyjnego. Jego głównymi konkurentami były wtedy BASIC i Pascal. C miał nad obydwoma przewagę, będąc dość ściśle ustandaryzowanym i pozbawionym niezgodnych ze sobą odmian. Ponadto zapewniał w tamtym czasie najpełniejsze wsparcie dla programowania strukturalnego, pozwalając również na hermetyzację kodu[130].
Zastosowanie języka C pozwoliło części producentów oprogramowania zrezygnować ze stosowania języka asemblera[130]. W ten sposób postąpiono np. z Oracle Database w 1983 roku, co pozwoliło na osiągnięcie jego przenośności. W konsekwencji, Oracle Database 3 stało się pierwszym silnikiem relacyjnych baz danych, który można było uruchomić zarówno na komputerach typu mainframe, minikomputerach jak i komputerach osobistych[133]. W C został napisany również najpopularniejszy na świecie silnik bazodanowy, SQLite[134].
Innymi przykładami oprogramowania przygotowanego w języku C są serwer HTTP Apache[135], biblioteka kryptograficzna OpenSSL[136], a także gry Doom[137] i Quake[138].
Środowiska wykonawcze dla programów napisanych w innych językach również powstają z wykorzystaniem kodu w C. Dotyczy to np. Java Runtime Environment[139], CPythona (referencyjnej implementacji Pythona)[140] czy interpretera PHP[141]. Ponadto C bywa także stosowany jako język pośredni dla kompilatorów języków wyższego poziomu. Takie kompilatory zostały napisane m.in. dla Basica, Pythona czy Prologa[142]. Niektóre języki wymagają jednak cech, których C nie oferuje, np. rekurencji ogonowej albo wsparcia dla odśmiecania pamięci. Z tego powodu zaprojektowano m.in. C--[143].
Język C, przez ponad 20 lat prowadzenia rankingu TIOBE Index (od 2001 roku), plasował się na jednym z dwóch czołowych miejsc, trzykrotnie zdobywając miano „języka roku”[144], nadawane językowi, który odnotuje najwyższy wzrost wskaźnika rankingowego rok do roku[145]. Z kolei w PYPL – alternatywnym rankingu popularności języków programowania – liczone łącznie języki C i C++ osiągnęły w czerwcu 2022 piątą lokatę[146].
Badanie projektów open source na platformie GitHub z 2014 roku wskazało, że ok. 60% linii kodu źródłowego napisanych było w C, choć znajdowały się one jedynie w 10% przebadanych repozytoriów. Autorzy badania są zdania, że tak wysoka wartość pierwszego wskaźnika może wynikać w faktu, iż w C powstały znaczące części systemów operacyjnych, m.in. jądro Linux i jego odmiany, które należą do projektów o największej bazie kodu[3].
Według Stack Overflow Developer Survey 2022, co szósty profesjonalny programista zna język C. Większość respondentów (60%) oceniła go jednak jako „przerażający” (w dwustopniowej skali, drugą opcją było „uwielbiany”). Natomiast 4,3% osób zadeklarowało, że chce rozpocząć pracę z tym językiem[147].
Popularność i szerokie zastosowanie języka C ma bezpośredni wpływ na składnię lub semantykę innych języków programowania[132]. Przede wszystkim najbardziej podobnym i kompatybilnym jest język C++, który powstawał jako rozszerzenie „C z klasami”[148]. Inne języki wykazujące podobieństwa do składni C to między innymi C#[149], csh[150], D[151], Go[152], IDL[153], Java[154] (w zasadzie do C++[155]), JavaScript[156], Objective-C[157], OpenCL[158], OpenGL[159], PAWN[160], Perl[161], PHP[162], tudzież Swift[163].
C charakteryzuje się słabym typowaniem. Jego specyfikacja pozwala na rzutowanie typów wskaźnikowych na dowolne inne typy wskaźnikowe. W konsekwencji dowolny region pamięci może być traktowany tak, jakby zawierał dane dowolnego typu. Jednocześnie narzędzia wspomagające pisanie kodu nie są w stanie sprawdzić, czy konwersja typów jest sensowna[164]. W C jest ponadto dozwolone przypisanie wartości do zmiennej innego typu. Jako przykład słabości wynikającej ze słabego typowania, I. Pohl i D. Edelson podali typową implementację funkcji, zmieniającej małe litery na wielkie. Wykorzystuje ona operacje arytmetyczne na liczbach całkowitych oraz zmiennych typu char
, mimo działania – na poziomie koncepcji – wyłącznie w dziedzinie znakowej[46]. Powszechne wykorzystanie wskaźników, pozwalających na niskopoziomowy dostęp do pamięci jest czynnikiem osłabiającym bezpieczeństwo programów. Niejawna konwersja typów całkowitoliczbowych (zmiana szerokości oraz możliwości przechowywania liczb ujemnych) oraz ciche przekroczenia zakresu wartości również należą do częstych przyczyn błędów oprogramowania, objawiających się w szczególności tam, gdzie dokonywane są operacje pamięciowe i prowadzą zwykle do przepełnienia buforów[109].
Mimo deklarowania tablic jako składające się z określonej liczby elementów, nie istnieje mechanizm sprawdzający, czy programista odwołuje się do elementu wewnątrz tablicy[165]; co więcej, dozwolone jest używanie ujemnych indeksów, aby odwołać się do obiektów leżących w pamięci przed zerowym[166]. Przeszkadza to w implementacji np. odśmiecania pamięci, ale też stanowi powszechne źródło trudnych do wykrycia błędów w oprogramowaniu[165]. Możliwość zapisu danych poza tablicę bywa wykorzystywana jako wektor ataku , prowadzącego do uzyskania kontroli nad systemem komputerowym poprzez wstrzyknięcie do programu odpowiednio spreparowanych poleceń lub zmianę stanu innych zmiennych[167].
Wielowymiarowe tablice są w rzeczywistości tablicami jednowymiarowymi, co w przeszłości utrudniało pisanie funkcji wykonujących np. obliczenia macierzowe[168][165]. W pierwotnej wersji standardu język C nie wspierał obsługi tablic o zmiennej (to znaczy ustalanej dopiero w momencie działania programu) liczbie elementów[169], w tym przekazywania takich tablic jako argumentów funkcji. Dlatego do adresowania np. elementów macierzy wykorzystywano pomocniczą tablicę wskaźników do pierwszych elementów każdego wiersza[168].
Innym problemem, który podnoszono jest niekonsekwencja twórców standardu, objawiająca się w określeniach void
i void*
. Pierwsze z nich oznacza brak wartości. Zatem zgodnie z logiką konstruowania typów, ten drugi powinien być wskaźnikiem na wartość pustą. W rzeczywistości jednak jest to wskaźnik na wartość dowolnego typu. Jeszcze inne zastosowanie słowa void
, niezwiązane z desygnowaniem typu, to wskazanie, że funkcja nie przyjmuje żadnych argumentów[165].
Z perspektywy czasu niektóre decyzje dotyczące składni okazały się nieoptymalne. Jedną z nich jest niższy priorytet operatorów bitowych niż porównania, co wymusza pisanie konstrukcji takich jak (a & mask) == b
, które bez nawiasów działałyby niezgodnie z intencją programisty[4]. Inną z częściej spotykanych pułapek jest zamienienie operatorów porównania (==
) i przypisania (=
). Kod napisany w ten sposób pozostaje poprawny składniowo, więc pomyłka nie zostanie zgłoszona przez kompilator. Innym operatorem, który utrudnia zrozumienie programu jest przecinek, pozwalający na wykonanie kilku wyrażeń w jednej instrukcji. Ten sam symbol oddziela od siebie np. argumenty funkcji[46].
Sama składnia deklarowania typów pochodnych również może konfundować. Założenie, że sposób deklaracji odpowiada sposobowi użycia zmiennej danego typu, spowodowało, że trzeba je czytać od środka na zewnątrz. Jest to konwencja trudna do uchwycenia przez wiele osób. Tego mankamentu dałoby się uniknąć, gdyby operator dereferencji był przyrostkowym, a nie przedrostkowym, jak to ma miejsce w C[4].
Cechy języka, krytykowane jako trudne do odczytania lub zrozumienia, bywają również wykorzystywane do celowego zaciemniania kodu, czego skrajnym przypadkiem są programy zgłaszane do konkursu IOCCC[170].
Standard języka C opisuje wiele sytuacji, dla których zachowanie jest niezdefiniowane lub nieokreślone . Daje to pewne pole manewru na różne optymalizacje w zależności od platformy sprzętowej. W rezultacie ten sam kod skompilowany na różnych kompilatorach lub z różnymi opcjami kompilacji może się inaczej zachowywać[171]. W szczególności standard C nie określa, w jaki sposób przechowywane są w pamięci wielobajtowe wartości skalarne. Większość programów polega jedynie na domyślnym zachowaniu zapewnionym przez ABI. Nie zapewnia ono jednak przenośności między różnymi platformami[172]. Jest to jeden z głównych powodów nieprzenaszalności kodu, który ujawnia się podczas wymiany danych poprzez pamięć współdzieloną, masową lub ich transmisję[173]. Jedną z naczelnych zasad ducha programowania w C jest tworzenie wydajnego kodu nawet za cenę jego przenośności[174].
Niezainicjowana zmienna to problem, który dotyczy wielu języków programowania[175]. Błąd ten powstaje z winy programisty[176]. Jest on trudny do wykrycia, gdyż taki program może się zupełnie inaczej zachowywać w środowisku diagnostycznym pod kontrolą debuggera niż w środowisku produkcyjnym[177] lub wręcz w kolejnych wykonaniach przy takich samych danych wejściowych[178]. Kompilator języka jedynie rezerwuje dla zmiennej obszar w pamięci, a jej wartość jest taka jaka wynika ze stanu bitów, która się w tej pamięci aktualnie znajduje[179]. Kompilator często ostrzega o próbie dostępu do niezainicjowanej zmiennej[180].
#pragma once
n
do wskaźnika ptr
na typ T
zwróci nowy wskaźnik o adresie ptr + n * sizeof(T)
. Podobnie, odjęcie od siebie dwóch wskaźników, zwróci różnicę indeksów tabeli, odpowiadających wskazywanym elementom.