Przejdź do zawartości

C (język programowania): Różnice pomiędzy wersjami

Z Wikipedii, wolnej encyklopedii
[wersja przejrzana][wersja przejrzana]
Usunięta treść Dodana treść
PG (dyskusja | edycje)
drobne redakcyjne
→‎Przykład „Hello world”: +sekcja o zastosowaniach języka C
Linia 298: Linia 298:


W przykładowym programie funkcja <code>main</code> składa się z jednej [[Instrukcja (informatyka)|instrukcji]]: wywołania <code>printf</code> z argumentem <code>"hello, world!"</code>. Służy ono do wyświetlenia tekstu na ekranie{{odn|Kernighan|Ritchie|2002|s=25}}. Mogłaby się tam znaleźć jeszcze instrukcja <code>return</code>, ale zgodnie ze standardem ISO&nbsp;C, umieszczenie jej w funkcji <code>main</code> jest opcjonalne. Zwrócona do [[System operacyjny|systemu operacyjnego]] zostanie wtedy wartość 0{{odn|ISO/IEC9899:2017|s=11}}.
W przykładowym programie funkcja <code>main</code> składa się z jednej [[Instrukcja (informatyka)|instrukcji]]: wywołania <code>printf</code> z argumentem <code>"hello, world!"</code>. Służy ono do wyświetlenia tekstu na ekranie{{odn|Kernighan|Ritchie|2002|s=25}}. Mogłaby się tam znaleźć jeszcze instrukcja <code>return</code>, ale zgodnie ze standardem ISO&nbsp;C, umieszczenie jej w funkcji <code>main</code> jest opcjonalne. Zwrócona do [[System operacyjny|systemu operacyjnego]] zostanie wtedy wartość 0{{odn|ISO/IEC9899:2017|s=11}}.

== Zastosowania ==
Głównym celem, który przyświecał Dennisowi Ritchiemu przy tworzeniu języka C było ułatwienie pisania [[Oprogramowanie systemowe|oprogramowania systemowego]]. Potrzebny był [[Język wysokiego poziomu|język wysokopoziomowy]], ale jednocześnie tak wydajny jak [[asembler]]<ref>{{Cytuj |autor = P. Fazio; K. Gowri |tytuł = Structural analysis software and the C programming language |czasopismo = Computers & Structures |data = 1987 |wolumin = 25 |numer = 3 |s = 463 |doi = 10.1016/0045-7949(87)90138-6 |język = en}}</ref>. Z tego też powodu bywa nazywany „wysokopoziomowym asemblerem”<ref name=":0">{{Cytuj |autor = Russel Jones |tytuł = The C programming language: Popularity grows for the high-level assembly language |czasopismo = Data Processing |data = 1985-12 |wolumin = 27 |numer = 10 |s = 35-38 |doi = 10.1016/0011-684X(85)90032-2 |język = en}}</ref>. W C powstały m.in. [[Unix]]<ref name=":0" /> i jądro [[Linux (jądro)|Linux]]<ref>{{Cytuj |tytuł = Programming Language — The Linux Kernel documentation |data dostępu = 2022-06-10 |opublikowany = www.kernel.org |url = https://www.kernel.org/doc/html/latest/process/programming-language.html |język = en}}</ref>.

W latach 80. XX wieku C zdobył popularność również wśród twórców [[Oprogramowanie użytkowe|oprogramowania aplikacyjnego]]. Jego głównymi konkurentami były wtedy [[BASIC]] i [[Pascal (język programowania)|Pascal]]. C miał przewagę nad oboma, będąc dość ściśle ustandaryzowanym i pozbawionym niezgodnych ze sobą odmian. Ponadto, zapewniał w tamtym czasie najpełniejsze wsparcie dla [[Programowanie strukturalne|programowania strukturalnego]], pozwalając również na [[Hermetyzacja (informatyka)|hermetyzację kodu]]<ref name=":0" />.

Zastosowanie języka C pozwoliło części producentów oprogramowania zrezygnować ze stosowania języka asemblera<ref name=":0" />. 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 [[Model relacyjny|relacyjnych baz danych]], który można było uruchomić zarówno na komputerach typu [[mainframe]], [[Minikomputer|minikomputerach]] i [[Komputer osobisty|komputerach osobistych]]<ref>{{Cytuj |autor = Lance Ashdown, Donna Keesling, Tom Kyte |rozdział = Brief History of Oracle Database |tytuł = Introduction to Oracle Database |data dostępu = 2022-06-10 |opublikowany = Oracle Help Center |url = https://docs.oracle.com/en/database/oracle/oracle-database/21/cncpt/introduction-to-oracle-database.html#GUID-43F9DD5C-8D8C-4E61-A2B4-5C05907D3CEC |język = en}}</ref>. W C został napisany również najpopularniejszy na świecie silnik bazodanowy, [[SQLite]]<ref>{{Cytuj |tytuł = SQLite Home Page |data dostępu = 2022-06-10 |opublikowany = www.sqlite.org |url = https://www.sqlite.org/index.html |język = en}}</ref>.

Innymi przykładami oprogramowania przygotowanego w języku C są [[Apache HTTP Server|serwer HTTP Apache]]<ref>{{Cytuj |autor = "Apache Software Foundation" |tytuł = apache/httpd |data = |data dostępu = 2022-06-10 |opublikowany = GitHub |url = https://github.com/apache/httpd |język = en}}</ref>, biblioteka kryptograficzna [[OpenSSL]]<ref>{{Cytuj |autor = "OpenSSL Project" |tytuł = openssl/openssl: TLS/SSL and crypto library |data = |data dostępu = 2022-06-10 |opublikowany = GitHub |url = https://github.com/openssl/openssl |język = en}}</ref>, a także gry [[Doom (gra komputerowa 1993)|Doom]]<ref>{{Cytuj |autor = Paul C. Schuytema |tytuł = The Lighter Side Of Doom |czasopismo = Computer Gaming World |data = 1994-08 |wolumin = 121 |s = 140, 142 |url = https://www.cgwmuseum.org/galleries/issues/cgw_121.pdf |język = en}}</ref> i [[Quake]]<ref>{{Cytuj |autor = "id-Software" |tytuł = id-Software/Quake: Quake GPL Source Release |data dostępu = 2022-06-10 |opublikowany = GitHub |url = https://github.com/id-Software/Quake |język = en}}</ref>.

[[Biblioteka uruchomieniowa|Środowiska wykonawcze]] dla programów napisanych w innych językach również powstają z wykorzystaniem kodu w C. Dotyczy to np. [[Java Runtime Environment]]<ref>{{Cytuj |tytuł = The Java(tm) Language: An Overview |data = 2008-02-22 |data dostępu = 2022-06-10 |opublikowany = web.archive.org |url = https://web.archive.org/web/20080222200518/http://java.sun.com/docs/overviews/java/java-overview-1.html |język = en}}</ref>, [[CPython|CPythona]] (referencyjnej implementacji [[Python|Pythona]])<ref>{{Cytuj |autor = "Python Software Foundation" |tytuł = python/cpython: The Python programming language |data dostępu = 2022-06-10 |opublikowany = GitHub |url = https://github.com/python/cpython |język = en}}</ref> czy interpretera [[PHP]]<ref>{{Cytuj |autor = "The PHP Group" |tytuł = php/php-src: The PHP Interpreter |data dostępu = 2022-06-10 |opublikowany = GitHub |url = https://github.com/php/php-src |język = en}}</ref>. Ponadto, C bywa także stosowany jako język pośredni przez kompilatory języków wyższego poziomu. Takie kompilatory zostały napisane m.in. dla Basica, Pythona czy [[Prolog (język programowania)|Prologa]]<ref>{{Cytuj |autor = D. Bohdan |tytuł = Compilers targeting C |data = 2021-06-30 |data dostępu = 2022-06-10 |opublikowany = GitHub |url = https://github.com/dbohdan/compilers-targeting-c/blob/master/README.md |język = en}}</ref>. Niektóre języki wymagają jednak cech, których C nie oferuje, np. [[Rekurencja ogonowa|rekurencji ogonowej]] albo wsparcia dla [[Odśmiecanie pamięci|odśmiecania pamięci]]. Z tego powodu zaprojektowano m.in. [[C--]]<ref>{{Cytuj |autor = Simon Peyton Jones; Thomas Nordin; Dino Oliva |tytuł = C--: A portable assembly language |czasopismo = Implementation of Functional Languages |data = 1997 |doi = 10.1007/BFb0055421 |język = en}}</ref>.


== Krytyka języka C ==
== Krytyka języka C ==

Wersja z 20:00, 10 cze 2022

C
Logo języka C
Logo języka
Pojawienie się

1972

Paradygmat

imperatywny (proceduralny)

Typowanie

statyczne (słabe)

Implementacje

Borland Turbo C, GCC, Microsoft Visual C, MinGW, LLVM, Tiny C Compiler

Pochodne

K&R C, ANSI C, C99, C++

Aktualna wersja stabilna

C18[1] (lipiec 2018)

Aktualna wersja testowa

C2x

Twórca

Dennis Ritchie

Platforma sprzętowa

wieloplatformowy

Platforma systemowa

wieloplatformowy

Strona internetowa

Cimperatywny, strukturalny język programowania wysokiego poziomu stworzony na początku lat siedemdziesiątych XX w. przez Dennisa Ritchiego do programowania systemów operacyjnych i innych zadań niskiego poziomu.

Historia

Początki C są ściśle związane z rozwojem systemu Unix, napisanego pierwotnie przez Dennisa Ritchiego i Kena Thompsona w języku asemblera na komputer PDP-7. Późniejsza wersja systemu, przeznaczona na maszynę PDP-11, również powstała w asemblerze[2].

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 tym języku powstało niewiele narzędzi, ponieważ wynikowe programy były powolne oraz nie mogły wykorzystywać możliwości adresowania poszczególnych bajtów pamięci (funkcji dostępnej np. w PDP-11)[2].

Wczesny rozwój

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[2].

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[2].

Kompilator C, wraz z kilkoma programami napisanymi w tym języku, został dołączony do drugiej wersji systemu Unix[3].

Latem 1973 jądro systemu zostało przepisane w języku C. Do tego czasu wprowadzono również do składni obsługę struktur. Mniej więcej w tym samym czasie, powstała pierwsza wersja preprocesora. Umożliwiała dołączanie plików zewnętrznych (dyrektywa #include) oraz wykonywanie prostych podstawień (definiując makra bez parametrów z użyciem #define). Funkcjonalność taka jak kompilacja warunkowa czy parametryzowane makra została wprowadzona 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[2].

Około 1977 roku, Dennis Ritchie, Ken Thompson i Stephen Johnson skupili się na przenośności oprogramowania napisanego w C[2]. 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ą i uniwersalną[4][2].

K&R C

W 1978 opublikowane zostało pierwsze wydanie książki The C Programming Language (wyd. polskie Język C, 1987[5]), autorstwa Briana Kernighana i Dennisa Ritchiego. Stanowiła ona pierwszą, nieformalną, specyfikację języka C[2]. Od roku wydania, wersja ta bywa nazywana C78. Inne skrócone określenie, K&R C, pochodzi od nazwisk autorów książki[6].

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

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 analizator składni[2].

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[2][8].

Autorzy publikacji wspomnieli również o planowanym zniesieniu większości ograniczeń dotyczących struktur[9]. Do 1980 roku możliwe stało się przekazywanie ich jako parametrów 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ż wparcie 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[2].

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[2]. 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[4].

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[2].

ANSI C i ISO C

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”[2]. Opisana w nim wersja języka jest zwykle określana jako ANSI C lub C89 (od daty publikacji)[6].

Rok później, w 1990 standard ANSI C został przyjęty (ze zmianami redakcyjnymi) przez Międzynarodową Organizację Normalizacyjną jako ISO/IEC 9899:1990[2][6]. Ta wersja bywa również nazywana C90 lub ISO C, choć opisuje ten sam język, co dokument ANSI[6].

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 ##)[10].

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[10].

Składnia

Formalna gramatyka języka C jest opisana w standardzie ISO C[11]. Podział kodu na linie oraz białe znaki są ignorowane. Ich stosowanie jest obowiązkowe jedynie, kiedy służą rozdzieleniu poszczególnych jednostek leksykalnych[12].

Identyfikatory

Identyfikator stanowi nazwę obiektu, funkcji, etykiety itp.[13] W skład identyfikatorów mogą wchodzić jedynie litery, cyfry i znak podkreślenia, choć cyfra nie może występować na pierwszym miejscu[14]. Aby zastosować w nazwie znaki spoza tego zbioru konieczne jest użycie sekwencji ucieczki \uxxxx lub \Uxxxxxxxx (gdzie x to cyfra szesnastkowa)[15].

W zależności od tego, do czego odwołuje się identyfikator, należy on do jednej z przestrzeni nazw[16]:

Ponadto, każdy identyfikator ma swój zakres widoczności, zależny od miejsca deklaracji[13].

Wyrażenia

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[17] (na przykład inkrementacja zmiennej[18]). 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[19].

Jeżeli tego samego obiektu dotyczy więcej niż jeden skutek uboczny i nie są one rozdzielone punktem sekwencyjnym, wtedy zachowanie jest nieokreślone[19][17]. Takie punkty wprowadzane są[20]:

  • w trakcie wywoływania funkcji: przed rozpoczęciem obliczania argumentów funkcji oraz przed skokiem do funkcji;
  • między obliczeniem wartości operandów &&, ||, ,;
  • między obliczeniem wartości pierwszego operandu ?: a drugiego lub trzeciego;
  • między wywołaniem kolejnych pełnych wyrażeń;
  • w momencie powrotu z funkcji bibliotecznych oraz w niektórych przypadkach w trakcie ich działania.

Operatory

W języku C są zdefiniowane następujące operatory[21]:

  • arytmetyczne: +, -, *, /, %
  • bitowe: ~, &, ^, |
  • dostępu: ., ->
  • grupowania wyrażeń: ()
  • inkrementacji i dekrementacji: ++, --
  • logiczne: !, &&, ||
  • porównania: <, >, <=, >=, ==, !=
  • przesunięcia bitowego: <<, >>
  • przypisania: =
  • przypisania złożonego: *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |=
  • rozmiaru obiektu: sizeof
  • rzutowania: (typ)
  • sekwencyjny: ,
  • warunkowy: ?:
  • wskaźnikowe: *, &, []
  • wyrównania dla typu: _Alignof
  • wywołania funkcji: ()

Obiekty

Obiekt, według standardu języka C, to region pamięci środowiska wykonawczego, mogący reprezentować wartości[22]. 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[23].

Przykładem l-wartości jest identyfikator obiektu. Może nią być również rezultat dereferencji wskaźnika[23].

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)[24].

Funkcje

W języku C rozróżniane są[25]:

  • deklaracja funkcji – polega na podaniu zwracanego typu, nazwy funkcji oraz listy parametrów (być może pustej); kończy się średnikiem
  • definicja funkcji – składa się z deklaracji oraz listy instrukcji, objętej w nawiasy klamrowe ({...}).

Definicje funkcji w języku C nie mogą być zagnieżdżane[26].

Funkcje w języku C nie mogą być przeciążane[27], ale istnieje mechanizm definiowania funkcji o zmiennej liczbie argumentów[28].

Instrukcje sterujące

Wykorzystanie instrukcji 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żą[29]:

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

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ą[30]:

  • 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[31]:

  • 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

Dyrektywy preprocesora rozpoczynają się od znaku # i muszą znajdować się w osobnych liniach (dopuszczalne jest by przed symbolem kratki znajdowały się spacje)[32].

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 z użyciem #define)[33][34].

Komentarze

W języku C występują komentarze liniowe jak i blokowe. Ich zawartość jest przetwarzana 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[35].

W trakcie kompilacji, komentarze zastępowane są znakiem spacji[36].

Typy danych

Typ danych określa zbiór wartości, które może przyjąć dany obiekt, jak również dozwolone operacje na nim[37]. Język udostępnia zestaw typów podstawowych oraz mechanizmów konstruowania typów pochodnych[38].

Szczególnym typem danych w C jest typ pusty void, który nie przechowuje żadnej wartości. W związku z tym można go wykorzystywać 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[39]. Słowem void oznacza się między innymi funkcje nie zwracające nic[40].

Typy podstawowe

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)[41].

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[42]. Reprezentacja bitowa wartości, które można zapisać zarówno w wariancie signed jak i unsigned danego typu jest taka sama w obu wariantach[43].

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[41]. Od implementacji języka wymaga się, by poszczególne typy danych pozwalały na przechowywanie przynajmniej wartości z ustalonego przedziału[44].

Minimalne zakresy wartości dla typów całkowitoliczbowych[45]
Typ Minimalny zakres Minimalny rozmiar w bitach
signed char 8
unsigned char 8
short int[a] 16
unsigned short int[a] 16
int 16
unsigned int[a] 16
long int[a] 32
unsigned long int[a] 32
long long int[a] 64
unsigned long long int[a] 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[41].

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[47].

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 zniesiona przez standard ANSI C[40].

Minimalne wymagania standardu języka C wobec typów zmiennoprzecinkowych[48]
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[49][40].

Typy pochodne

Z niezerowej liczby obiektów tego samego typu można stworzyć tablicę[50]. Jej elementy są ułożone w pamięci komputera po kolei i bez żadnych przerw[51], a dostęp do nich można uzyskać za pomocą składni tablica[indeks], gdzie indeksy rozpoczynają się od zera[52]. Do raz utworzonej zmiennej typu tablicowego nie jest możliwe przypisanie innej tablicy[53]. Nazwa tablicy stanowi jednocześnie adres jej zerowego elementu[54].

Do przechowywania adresu obiektu określonego typu służą wskaźniki[55]. 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). Porównywanie oraz odejmowanie dwóch wskaźników jest dozwolone wyłącznie wtedy, kiedy dotyczą one tej samej tablicy. 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[56].

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[57]. Każda struktura stanowi odrębną przestrzeń nazw, toteż pole o tej samej nazwie może występować w kilku strukturach[58][16]. 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 tego, struktura może zawierać wskaźnik na inną strukturę tego samego typu[59]. Zarówno przypisywanie do zmiennej typu strukturalnego nowej wartości, jak i przekazywanie jej jako argument oraz zwracanie z funkcji są dopuszczalnymi operacjami[60].

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[61].

Struktury mogą zawierać również pola bitowe, które pozwalają na określenie rozmiaru obiektu z dokładnością do bitów[62]. 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[61]. Pobieranie adresu pola bitowego jest niedozwolone[63].

Przykładowa deklaracja struktury, opisującej węzeł pewnego drzewa[64]:

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[65].

Standard języka C definiuje również typy atomowe, choć stanowią one opcjonalną funkcjonalność, która nie musi być obsługiwana przez implementacje[66]. 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[67].

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[50]. Zmienna typu funkcyjnego, jeśli nie jest argumentem operatora pozyskania adresu &, niejawnie przekształca się we wskaźnik do funkcji[23]. Wywołania, dokonywane za pośrednictwem wskaźników nie są przez standard rozróżniane od tych zawierających wprost nazwę funkcji[68].

Aliasy typów

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 pierwotnej, jak i nowej nazwy mają identyczne właściwości[69].

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[70]. Przechowują one liczby całkowite, lecz ich dokładny typ zależy od implementacji[71].

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[72]. Poniższe trzy deklaracje funkcji są sobie równoważne[73]:

// 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);

Przykład „Hello world”

Kartka papieru do drukarki z odręcznie napisanym kodem w języku C. Poniżej podpis autora.
Program „Hello world” napisany odręcznie przez Briana Kernighana

Program wyświetlający na ekranie napis „Hello world” był pierwszym fragmentem kodu w języku C, umieszczonym w książce Język C[74]. Składnia kodu z pierwszego wydania tej książki odbiega od późniejszych standardów[75]. Program ten, w bardzo podobnej formie, znajdował się również w dokumencie Programming in C: A Tutorial, przygotowanym przez B. Kernighana w 1974 roku[76][77].

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[75]:

#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[78]. Zadeklarowane w nim są procedury, odpowiadające za obsługę wejścia i wyjścia, w tym printf[79]. 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[80].

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[78]. 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[81].

Zastosowania

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[82]. Z tego też powodu bywa nazywany „wysokopoziomowym asemblerem”[75]. W C powstały m.in. Unix[75] i jądro Linux[83].

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ł przewagę nad oboma, 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[75].

Zastosowanie języka C pozwoliło części producentów oprogramowania zrezygnować ze stosowania języka asemblera[75]. 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 i komputerach osobistych[84]. W C został napisany również najpopularniejszy na świecie silnik bazodanowy, SQLite[85].

Innymi przykładami oprogramowania przygotowanego w języku C są serwer HTTP Apache[86], biblioteka kryptograficzna OpenSSL[87], a także gry Doom[88] i Quake[89].

Środowiska wykonawcze dla programów napisanych w innych językach również powstają z wykorzystaniem kodu w C. Dotyczy to np. Java Runtime Environment[90], CPythona (referencyjnej implementacji Pythona)[91] czy interpretera PHP[92]. Ponadto, C bywa także stosowany jako język pośredni przez kompilatory języków wyższego poziomu. Takie kompilatory zostały napisane m.in. dla Basica, Pythona czy Prologa[93]. 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--[94].

Krytyka języka C

Język C pozwala na wykonywanie niskopoziomowych operacji, przez co wiele prostych błędów programistycznych nie jest wykrywanych przez kompilator, a przy wykonywaniu programu ujawniają się dopiero po jakimś czasie i w przypadkowych miejscach. Twórcy języka chcieli uniknąć sprawdzeń w czasie kompilacji i wykonywania programu, bo były one zbyt kosztowne czasowo, gdy C był implementowany po raz pierwszy. Z czasem powstały zewnętrzne narzędzia do wykonywania części z tych sprawdzeń. Nic nie przeszkadza implementacji języka w dostarczaniu takich sprawdzeń, ale też nie są one wymagane przez oficjalne standaryzacje.

Używanie języka C wymaga od programisty dokładnego zrozumienia pisanego kodu źródłowego, łącznie z mechanizmami kompilacyjnymi, dodatkowo komplikowanymi nieprzenośnością między platformami i kompilatorami, jak również rygorystycznego przestrzegania dobrych praktyk, szczególnie w odniesieniu do funkcji obsługujących wszelkiego rodzaju buforowania. Podobnie brak standaryzacji bibliotek wyższego poziomu jest powodem do uznania C za język niezalecany dla początkujących. Jednakże wiele z tych niedogodności można zniwelować, tworząc własne elastyczniejsze rozwiązania. Pod względem zastosowań praktycznych C nie ustępuje innym językom, traci jednak w stosunku do nich, gdy wziąć pod uwagę czas i inne środki niezbędne do implementacji porównywalnych systemów.

Niedostępne właściwości

C był tworzony jako mały i prosty język, co niewątpliwie przyczyniło się do jego popularności, ponieważ nowe kompilatory języka mogły być szybko tworzone na nowe platformy. Relatywnie niskopoziomowa natura języka daje programiście dokładną kontrolę nad tym, co robi komputer, jednocześnie pozwalając na specjalne dostosowanie i agresywne optymalizacje na konkretnych platformach. Pozwala to na szybkie działanie kodu nawet na ograniczonym sprzęcie, na przykład w systemach wbudowanych.

C nie zawiera wielu właściwości dostępnych w innych językach programowania:

  • Nie można przypisywać tablic (nie mylić ze wskaźnikami traktowanymi jako tablice) lub ciągów znaków – kopiowanie może zostać wykonane za pomocą standardowych funkcji; możliwe jest przypisywanie obiektów o typach struct lub union.
  • Brak odśmiecacza (ang. garbage collection).
  • Brak wymagania sprawdzania zakresu tablic.
  • Brak operacji na całych tablicach.
  • Brak składni dla zasięgów, na przykład notacji A..B używanej w wielu językach, z wyjątkiem zasięgu dla pól bitowych.
  • Brak funkcji zagnieżdżonych.
  • Brak domknięć lub przekazywania funkcji jako parametru (tylko wskaźniki do funkcji i zmiennych).
  • Brak generatorów i współprogramów; kontrola przepływu programu w obrębie wątku opiera się tylko na zagnieżdżonych wywołaniach funkcji, nie licząc funkcji bibliotecznych longjmp czy setcontext.
  • Brak obsługi wyjątków; funkcje standardowe pokazują błędy za pomocą globalnej zmiennej errno lub specjalnych zwracanych wartości.
  • Ograniczona obsługa programowania modułowego.
  • Brak polimorfizmu w czasie kompilacji w formie przeciążania funkcji i operatorów.
  • Brak obsługi programowania obiektowego, a w szczególności polimorfizmu, dziedziczenia i ograniczona (tylko w obrębie modułu) obsługa enkapsulacji.
  • Brak bezpośredniej obsługi programowania wielowątkowego i sieci.
  • Brak standardowych bibliotek graficznych i innych.

Wiele z tych właściwości jest dostępnych w różnych kompilatorach jako dodatkowe rozszerzenia lub może zostać dostarczone przez zewnętrzne biblioteki albo zasymulowane przez odpowiednią dyscyplinę przy programowaniu. Na przykład, w większości języków zorientowanych obiektowo, funkcje-metody mają specjalny wskaźnik „this”, który wskazuje na aktualny obiekt. Przekazując ten wskaźnik jako zwykły argument funkcji podobna funkcjonalność może zostać uzyskana w C. Gdy w C++ napisano by:

stack->push(val);

w C można zapisać:

push(stack,val);

Możliwości graficzne można rozszerzyć poprzez:

Niezdefiniowane zachowania

Wiele operacji w C mających niezdefiniowane zachowanie nie jest sprawdzanych w czasie kompilacji. W przypadku C, „niezdefiniowane zachowanie” oznacza, że zachowanie nie jest opisane w standardzie i co dokładnie się stanie nie musi być opisane w dokumentacji danej implementacji C. W praktyce czasami poleganie na niezdefiniowanych zachowaniach może prowadzić do trudnych w rozpoznaniu błędów. Zachowania te mogą różnić się między kompilatorami C. Głównym celem pozostawienia niektórych zachowań jako niezdefiniowane jest pozwolenie kompilatorowi na generowanie bardziej wydajnego kodu dla zdefiniowanych zachowań, co jest ważne dla głównej roli języka C jako języka implementacji systemów; unikanie niezdefiniowanych zachowań jest odpowiedzialnością programisty. Przykłady niezdefiniowanych zachowań:

  • Odczyt i zapis poza zasięgiem tablicy.
  • Porównanie wskaźników wskazujących obiekty w różnych tablicach.
  • Przekroczenie zakresu liczb całkowitych.
  • Dotarcie do końca funkcji zwracającej wartość, bez napotkania na wyrażenie return.
  • Odczytanie zmiennej przed zapisaniem do niej wartości.

Wszystkie te operacje to błędy programistyczne, które mogą się zdarzyć w wielu językach programowania; C przyciąga krytykę ponieważ jego standard wyraźnie wylicza wiele przypadków niezdefiniowanego zachowania, także tam, gdzie mogłoby ono zostać dobrze zdefiniowane i nie zawiera żadnego mechanizmu obsługi błędów w czasie wykonywania programu.

Alokacja pamięci

Automatycznie i dynamicznie alokowane obiekty nie są koniecznie zainicjalizowane; początkowo mają niezdefiniowane wartości (zwykle zbiór bitów który akurat był poprzednio w danym miejscu w pamięci, który nawet może nie reprezentować żadnej prawidłowej wartości dla danego typu danych). Gdy program próbuje odczytać taką niezainicjalizowaną wartość, rezultat jest niezdefiniowany. Wiele współczesnych kompilatorów próbuje wykryć i ostrzec przed tym problemem, ale pojawiają się błędy pierwszego i drugiego rodzaju.

Innym częstym problemem jest konieczność ręcznej synchronizacji użycia pamięci na stercie. Na przykład, gdy jedyny wskaźnik na przydzieloną pamięć wyjdzie poza zasięg lub gdy jego wartość się zmieni przed wywołaniem na nim free (), to pamięć nie może zostać już odzyskana do dalszego użycia i jest stracona do końca działania programu. Zjawisko to nazywa się wyciekiem pamięci. Odwrotnie, możliwe jest zwolnienie pamięci zbyt wcześnie i mimo to dalsze odwoływanie się do niej; ponieważ system alokacji pamięci może ją w każdej chwili wykorzystać do innych celów, dochodzi do nieprzewidywalnych zachowań programu, gdy dane miejsce pamięci ma wielu użytkowników jednocześnie uszkadzających sobie nawzajem dane. Zwykle symptomy te pojawiają się w miejscach programu zupełnie oddalonych od faktycznego błędu. Błędy te można ograniczyć przez użycie dodatkowego odśmiecacza lub RAII.

Wskaźniki

Wskaźniki są głównym źródłem zagrożeń w języku C. Ponieważ mogą zwykle wskazywać na dowolny obszar pamięci, prowadzić to może do niepożądanych efektów. Nawet odpowiednio używane wskaźniki wskazujące na bezpieczne miejsca, mogą zostać przypadkiem przeniesione na miejsca niebezpieczne przez użycie nieodpowiedniej arytmetyki wskaźników; pamięć na którą wskazują może być zwolniona i użyta już na coś innego (zwisający wskaźnik); mogą być niezainicjalizowane (dziki wskaźnik), lub mogą mieć bezpośrednio przypisaną wartość poprzez rzutowanie, unię, lub inny uszkodzony wskaźnik. Ogólnie C pozwala na swobodną manipulację i konwersję typów wskaźników, chociaż kompilatory zwykle dostarczają opcje różnego poziomu ich kontroli. Inne języki niwelują problemy ze wskaźnikami poprzez użycie bardziej ograniczonych typów referencji.

Tablice

Chociaż C wspiera tablice statyczne, nie jest wymagane, aby sprawdzany był zasięg ich indeksów. Na przykład, można zapisać w szóstym elemencie tablicy pięcioelementowej, powodując nadpisanie innej pamięci. Ten rodzaj błędu, przepełnienie bufora, jest źródłem wielu problemów z bezpieczeństwem komputerowym. Z drugiej strony, ponieważ technologia eliminacji sprawdzania zasięgu tablic praktycznie nie istniała w czasie tworzenia języka C, sprawdzanie zasięgu miało duży narzut czasu działania programu, zwłaszcza w obliczeniach numerycznych. Kilka lat później, niektóre kompilatory Fortranu miały przełącznik do włączania lub wyłączania sprawdzania zasięgu tablic. Byłoby to jednak dużo mniej użyteczne w języku C, gdzie argumenty o typie tablicowym są przekazywane przez zwykłe wskaźniki.

Tablice wielowymiarowe są często używane w algorytmach numerycznych (zwłaszcza z algebry liniowej) do zapisu macierzy. Struktura tablicy w języku C jest bardzo dobrze przystosowana do tego zadania. Ponieważ zmienne są przekazywane jedynie jako proste wskaźniki, zasięg tablicy musi być znany i stały lub osobno przekazywany do funkcji korzystających z nich i dostęp do tablic dynamicznych nie może być realizowany za pomocą podwójnego indeksu (obejściem jest użycie dodatkowej tablicy „rzędu” wskaźników do kolumn). Problemy te są omówione w książce Numerical Recipes in C, rozdział 1.2, strona 22ff.

C99 wprowadził tablice o zmiennym rozmiarze, które rozwiązują niektóre problemy ze zwykłymi tablicami z C.

Składnia

Chociaż naśladowana przez wiele języków z powodu jej popularności, składnia C jest często uznawana za jeden z jego słabszych punktów. Na przykład, Kernighan i Ritchie mówią w drugiej edycji The C Programming Language: „C, tak jak każdy inny język, ma swoje słabe punkty. Niektóre operatory mają zły priorytet; niektóre części składni mogłyby być lepsze.” Niektóre konkretne problemy to:

  • Brak sprawdzenia liczby i typów argumentów gdy deklaracja funkcji ma pustą listę parametrów. Pozwala to na kompatybilność wstecz z K&R C, w którym nie było prototypów funkcji.
  • Wspomniany przez Kerninghan i Ritchie wyżej kwestionowany wybór niektórych priorytetów operatorów, na przykład == wiążący ściślej niż & i | w wyrażeniu takim jak x & 1 == 0.
  • Użycie operatora =, używanego w matematyce do porównania, jako operatora przypisania, podążając za Fortran, PL/I, BASIC, ale w przeciwieństwie do ALGOL i jego pochodnych. Ritchie dokonał tego wyboru świadomie, bazując na tym, że przypisanie występuje częściej niż porównanie.
  • Podobieństwo operatorów przypisania i porównania (= i ==), przez co łatwo je pomylić. Słaby system typów języka C pozwala na ich błędną podmianę bez podania błędu kompilacji (chociaż niektóre kompilatory emitują ostrzeżenia).
  • Brak operatorów infiksowych dla złożonych struktur, zwłaszcza dla operacji na ciągach znaków, co czyni programy gęsto wykorzystujące te operacje nieczytelne.
  • Duże oparcie na symbolach nawet tam, gdzie według niektórych jest to mniej czytelne, na przykład && i || zamiast odpowiednio and i or. Możliwe do pomylenia są też operatory bitowe („&” i „|”) z operatorami logicznymi („&&” i „||”), ponieważ te pierwsze mogą być często, ale nie zawsze, użyte w miejsce drugich bez zmiany działania programu.
  • Składnia deklaracji może być nieintuicyjna, zwłaszcza dla wskaźników do funkcji. (Pomysłem Ritchiego była deklaracja identyfikatorów w kontekstach przypominających ich użycie.)

Oszczędność wyrażenia

Jedną z krytykowanych cech języka C jest możliwość tworzenia zwięzłych ponad miarę fragmentów kodu. Klasyczny przykład pojawiający się w K&R to poniższa funkcja kopiująca zawartość ciągu znaków wskazywanego przez t do ciągu znaków wskazywanego przez s:

void strcpy(char *s, char *t)
{
    while (*s++ = *t++);
}

W tym przykładzie, s i t to wskaźniki na pierwsze elementy tablic znaków zakończonych wartościami null. Każde przejście pętli wyrażenia while wykonuje poniższe operacje:

  • Kopiowanie znaku wskazywanego przez t (oryginalnie ustawionego na pierwszy znak ciągu znaków do skopiowania) do odpowiadającej pozycji wskazywanej przez s (oryginalnie ustawionego na pierwszy znak ciągu znaków do którego kopiowane są dane).
  • Zwiększenie wartości wskaźników s i t, tak by wskazywały na kolejne znaki. Zauważ, że wartości s i t mogą być bezpiecznie zmieniane ponieważ są to lokalne kopie wskaźników na oryginalne tablice.
  • Sprawdza czy skopiowany znak (rezultat operatora przypisania) to null oznaczający koniec ciągu znaków. Test mógłby być zapisany jako ((*s++ = *t++) != '\0') (gdzie \0 to znak null), ale w C test wartości boolowskiej sprawdza tylko czy zmienna różni się od zera. Stąd test zwraca prawdę tak długo jak tylko znak jest inny od null (\0, kod ASCII 0) – kończącego ciąg znaków.
  • Dopóki znak nie jest null, warunek daje prawdę, powodując powtórzenie pętli while. W szczególności, ponieważ kopiowanie znaku następuje przed obliczeniem wyrażenia, jest gwarancja, że kończąca wartość null jest też skopiowana).
  • Ciągle powtarzające się ciało pętli while jest pustym wyrażeniem, oznaczonym przez pojedynczy średnik (który nie jest częścią składni pętli while). Puste ciało pętli nie jest rzadkością.

Powyższy kod może zostać zapisany jako:

void strcpy(char *s, char *t)
{
    char aux;
    do {
        *s = *t;
        aux = *s;
        s++;
        t++;
    } while (aux != '\0');
}

Przy użyciu współczesnego optymalizującego kompilatora powyższe dwie funkcje skompilują się do identycznej sekwencji instrukcji procesora, więc mniejszy kod programu niekoniecznie oznacza mniejszy kod wynikowy.

Osobliwości języka C

Osobliwością języka C jest sposób traktowania tablic[95], a w szczególności ich indeksowania. W zasięgu deklaracji:

int i,t[10];

dostęp do np. drugiego elementu tablicy t uzyskuje się poprzez zapis:

i = t[1];

Jednakże (w odróżnieniu od większości innych języków programowania) symbol „[]” nie jest tylko elementem składni, ale również operatorem, który przez kompilator traktowany jest następująco:

i = *(t + 1);

Ponieważ dodawanie jest przemienne, przemienny jest również operator „[]”, a to oznacza, że poniższy fragment kodu (mimo dość zaskakującego zapisu) jest poprawny i równoważny przytoczonemu powyżej:

i = 1[t];

Cechy tej nie mają nawet te języki, których składnia wywodzi się z C, jak np. Java, JavaScript czy Perl.

W języku C istnieje tzw. operator połączenia, zapisywany jako „,” (przecinek). Operator ten powoduje obliczenie najpierw wartości lewego argumentu, potem prawego, a wartością i typem całego wyrażenia jest wartość i typ prawego argumentu. Może to powodować nieoczekiwane skutki, jeśli program kodowany jest przez początkującego i mało uważnego programistę. Poniższy fragment kodu (który mógłby powstać jako skutek pomylenia kropki dziesiętnej z przecinkiem) zostanie przez kompilator potraktowany jako poprawny, a wartością zmiennej x stanie się 5.0:

float x;

x = (2,5);

Zobacz też

Uwagi

  1. a b c d e f g Możliwe jest również stosowanie skróconej nazwy, bez członu int[46].

Przypisy

  1. The Current C Programming Language Standard - ISO/IEC 9899:2018 (C18) - ANSI Blog [online], The ANSI Blog, 13 listopada 2018 [dostęp 2021-03-11] (ang.).
  2. a b c d e f g h i j k l m n o Dennis M. Ritchie, The development of the C language, „ACM SIGPLAN Notices”, 28 (3), 1993, s. 201–208, DOI10.1145/155360.155580, ISSN 0362-1340 [dostęp 2022-01-15] [zarchiwizowane z adresu 2022-01-07] (ang.).
  3. M. Douglas McIlroy, A Research UNIX Reader: Annotated Excerpts from the Programmer’s Manual, 1971-1986 [online], s. 10 [dostęp 2022-01-15] [zarchiwizowane z adresu 2021-11-01] (ang.).
  4. a b Stephen C. Johnson, Dennis M. Ritchie, Portability of C Programs and the UNIX System [online], s. 4 [dostęp 2022-01-15] [zarchiwizowane z adresu 2021-12-18] (ang.).
  5. Kernighan i Ritchie 2002 ↓, s. 13.
  6. a b c d Gabor Kovesdan, c78(7) manual page [online] [dostęp 2022-01-15] [zarchiwizowane z adresu 2021-08-14] (ang.).
  7. Dennis M. Ritchie, Brian Kernighan, The C Programming Language, wyd. 1, Englewood Cliffs: Prentice-Hall, 1978, s. 34, ISBN 0-13-110163-3 (ang.).
  8. Dennis M. Ritchie, Brian Kernighan, The C Programming Language, wyd. 1, Englewood Cliffs: Prentice-Hall, 1978, s. 143, ISBN 0-13-110163-3 (ang.).
  9. Dennis M. Ritchie, Brian Kernighan, The C Programming Language, wyd. 1, Englewood Cliffs: Prentice-Hall, 1978, s. 121, ISBN 0-13-110163-3 (ang.).
  10. a b Mark Gass, Using the New Features in ANSI C [online] [dostęp 2022-01-15] (ang.).
  11. ISO/IEC9899:2017 ↓, s. i.
  12. 1.6. White Space, [w:] The GNU C Reference Manual, GNU [dostęp 2022-03-13].
  13. a b ISO/IEC9899:2017 ↓, s. 28.
  14. ISO/IEC9899:2017 ↓, s. 43.
  15. ISO/IEC9899:2017 ↓, s. 44.
  16. a b ISO/IEC9899:2017 ↓, s. 29.
  17. a b ISO/IEC9899:2017 ↓, s. 55.
  18. ISO/IEC9899:2017 ↓, s. 61.
  19. a b ISO/IEC9899:2017 ↓, s. 12.
  20. ISO/IEC9899:2017 ↓, s. 366.
  21. ISO/IEC9899:2017 ↓, s. 57-75.
  22. ISO/IEC9899:2017 ↓, s. 5.
  23. a b c ISO/IEC9899:2017 ↓, s. 40.
  24. ISO/IEC9899:2017 ↓, s. 78-80.
  25. 5. Functions, [w:] The GNU C Reference Manual, GNU [dostęp 2022-03-13] (ang.).
  26. Ira Pohl, Daniel Edelson, A to Z: C language shortcomings, „Computer Languages”, 13 (2), 1988, s. 60, DOI10.1016/0096-0551(88)90009-4 (ang.).
  27. Kalsoom Bibi, Function Overloading in C [online] [dostęp 2022-03-13] (ang.).
  28. 5.5 Variable Length Parameter Lists, [w:] The GNU C Reference Manual, GNU [dostęp 2022-03-13] (ang.).
  29. ISO/IEC9899:2017 ↓, s. 108.
  30. ISO/IEC9899:2017 ↓, s. 109-110.
  31. ISO/IEC9899:2017 ↓, s. 110-112.
  32. ISO/IEC9899:2017 ↓, s. 118.
  33. ISO/IEC9899:2017 ↓, s. 118-121.
  34. Kernighan i Ritchie 2002 ↓, s. 305.
  35. ISO/IEC9899:2017 ↓, s. 54.
  36. ISO/IEC9899:2017 ↓, s. 9.
  37. Kernighan i Ritchie 2002 ↓, s. 60.
  38. Kernighan i Ritchie 2002 ↓, s. 257-258.
  39. Kernighan i Ritchie 2002 ↓, s. 262.
  40. a b c Kernighan i Ritchie 2002 ↓, s. 258.
  41. a b c Kernighan i Ritchie 2002 ↓, s. 61.
  42. Kernighan i Ritchie 2002 ↓, s. 61-62.
  43. Kernighan i Ritchie 2002 ↓, s. 257.
  44. ISO/IEC9899:2017 ↓, s. 20.
  45. ISO/IEC9899:2017 ↓, s. 20-22.
  46. ISO/IEC9899:2017 ↓, s. 80.
  47. Kernighan i Ritchie 2002 ↓, s. 62.
  48. ISO/IEC9899:2017 ↓, s. 24-25.
  49. Kernighan i Ritchie 2002 ↓, s. 65.
  50. a b ISO/IEC9899:2017 ↓, s. 32.
  51. Kernighan i Ritchie 2002 ↓, s. 136.
  52. Kernighan i Ritchie 2002 ↓, s. 45.
  53. Kernighan i Ritchie 2002 ↓, s. 138.
  54. Kernighan i Ritchie 2002 ↓, s. 141.
  55. Kernighan i Ritchie 2002 ↓, s. 130-131.
  56. Kernighan i Ritchie 2002 ↓, s. 141-143.
  57. Kernighan i Ritchie 2002 ↓, s. 173-175.
  58. Kernighan i Ritchie 2002 ↓, s. 174.
  59. ISO/IEC9899:2017 ↓, s. 81.
  60. Kernighan i Ritchie 2002 ↓, s. 175-176.
  61. a b ISO/IEC9899:2017 ↓, s. 82.
  62. Kernighan i Ritchie 2002 ↓, s. 200.
  63. ISO/IEC9899:2017 ↓, s. 64.
  64. Kernighan i Ritchie 2002 ↓, s. 188.
  65. Kernighan i Ritchie 2002 ↓, s. 198-200.
  66. ISO/IEC9899:2017 ↓, s. 33.
  67. ISO/IEC9899:2017 ↓, s. 206-209.
  68. ISO/IEC9899:2017 ↓, s. 58.
  69. Kernighan i Ritchie 2002 ↓, s. 197.
  70. Kernighan i Ritchie 2002 ↓, s. 197-198.
  71. Kernighan i Ritchie 2002 ↓, s. 270-271.
  72. Kernighan i Ritchie 2002 ↓, s. 198.
  73. ISO/IEC9899:2017 ↓, s. 100.
  74. Dennis M. Ritchie, Brian Kernighan, The C Programming Language, wyd. 1, Englewood Cliffs: Prentice-Hall, 1978, s. 6, ISBN 0-13-110163-3 (ang.).
  75. a b c d e f C Language Tutorial => Original "Hello, World!" in K&R C [online], riptutorial.com [dostęp 2022-04-12]. Błąd w przypisach: Nieprawidłowy znacznik <ref>; nazwę „:0” zdefiniowano więcej niż raz z różną zawartością
    BŁĄD PRZYPISÓW
  76. Brian W. Kernighan, Programming in C: A Tutorial, Bell Laboratories, 1974, s. 1 [dostęp 2022-04-12] [zarchiwizowane z adresu 2022-03-22] (ang.).
  77. Dennis M. Ritchie, C Reference Manual [online], Bell Telephone Laboratories, s. 25 [dostęp 2022-04-12] [zarchiwizowane z adresu 2022-04-12] (ang.).
  78. a b Kernighan i Ritchie 2002 ↓, s. 25.
  79. Kernighan i Ritchie 2002 ↓, s. 322-325.
  80. Kernighan i Ritchie 2002 ↓, s. 24-25.
  81. ISO/IEC9899:2017 ↓, s. 11.
  82. P. Fazio, K. Gowri, Structural analysis software and the C programming language, „Computers & Structures”, 25 (3), 1987, s. 463, DOI10.1016/0045-7949(87)90138-6 (ang.).
  83. Programming Language — The Linux Kernel documentation [online], www.kernel.org [dostęp 2022-06-10] (ang.).
  84. Brief History of Oracle Database, [w:] Lance Ashdown, Donna Keesling, Tom Kyte, Introduction to Oracle Database, Oracle Help Center [dostęp 2022-06-10] (ang.).
  85. SQLite Home Page [online], www.sqlite.org [dostęp 2022-06-10] (ang.).
  86. Apache Software Foundation, apache/httpd [online], GitHub [dostęp 2022-06-10] (ang.).
  87. OpenSSL Project, openssl/openssl: TLS/SSL and crypto library [online], GitHub [dostęp 2022-06-10] (ang.).
  88. Paul C. Schuytema, The Lighter Side Of Doom, „Computer Gaming World”, 121, sierpień 1994, s. 140, 142 (ang.).
  89. id-Software, id-Software/Quake: Quake GPL Source Release [online], GitHub [dostęp 2022-06-10] (ang.).
  90. The Java(tm) Language: An Overview [online], web.archive.org, 22 lutego 2008 [dostęp 2022-06-10] [zarchiwizowane z adresu 2008-02-22] (ang.).
  91. Python Software Foundation, python/cpython: The Python programming language [online], GitHub [dostęp 2022-06-10] (ang.).
  92. The PHP Group, php/php-src: The PHP Interpreter [online], GitHub [dostęp 2022-06-10] (ang.).
  93. D. Bohdan, Compilers targeting C [online], GitHub, 30 czerwca 2021 [dostęp 2022-06-10] (ang.).
  94. Simon Peyton Jones, Thomas Nordin, Dino Oliva, C--: A portable assembly language, „Implementation of Functional Languages”, 1997, DOI10.1007/BFb0055421 (ang.).
  95. Dennis M. Ritchie: The Development of the C Language. Lucent Technologies Inc., 2003. [dostęp 2015-11-29]. [zarchiwizowane z tego adresu (2013-06-22)]. (ang.).

Bibliografia

Linki zewnętrzne