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]. 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].
Typy pochodne[edytuj edytuj kod]
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]. 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].
Wczesny rozwój[edytuj edytuj kod]
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 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(inne języki)[38].
Typy podstawowe[edytuj edytuj kod]
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. 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. Zadeklarowanie obiektu ze słowem register sugeruje kompilatorowi, by umieścił go w pamięci o szybkim dostępie (np. rejestrze procesora[112]).
Komentarze[edytuj edytuj kod]
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.
ANSI C i ISO C[edytuj edytuj kod]
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]. Z niezerowej liczby obiektów tego samego typu można stworzyć tablicę[70].
Alokacja pamięci[edytuj edytuj kod]
Służą do przechowywania liczb całkowitych (char i int) oraz zmiennoprzecinkowych (float i double)[61]. 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).
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 Psychologiczny dziennik Forex Trader wykorzystanie wskaźników, pozwalających na niskopoziomowy dostęp do pamięci jest czynnikiem osłabiającym bezpieczeństwo programów. 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.
Zalicza się go do tej grupy dla wygody, choć nie służy do deklarowania pamięci[116]. 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]. 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]. 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].
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]. Około 1977 roku Dennis Ritchie, Ken Thompson i Stephen Johnson skupili się na przenośności oprogramowania napisanego w C[4].
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[d][78]. 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].
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 opisuje wiele sytuacji, dla których zachowanie jest niezdefiniowane(inne języki) lub nieokreślone(inne języki). Daje to pewne pole manewru na różne optymalizacje w zależności od platformy sprzętowej.
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]. 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].
- Wewnątrz nawiasów klamrowych znajduje się lista instrukcji, które zostaną wywołane po uruchomieniu programu[125].
- Zastosowanie języka C pozwoliło części producentów oprogramowania zrezygnować ze stosowania języka asemblera[130].
- 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].
- Są one dostępne przez cały czas wykonywania bloku, gdzie zostały zadeklarowane[e].
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(inne języki) \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].
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]. Typ danych określa zbiór wartości, które może przyjąć dany obiekt, jak również dozwolone operacje na nim[57].
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]. 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]. Struktury mogą zawierać również pola bitowe, które pozwalają na określenie rozmiaru obiektu z dokładnością do bitów[84].
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][c]. Do raz utworzonej zmiennej typu tablicowego nie jest możliwe przypisanie innej tablicy[74]. Kiedy nazwa tablicy zostanie użyta w wyrażeniu, dokonuje 10 Day Trading Strategies For Beginners 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]. 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.
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]. 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]. Autorzy publikacji wspomnieli również o planowanym zniesieniu większości ograniczeń dotyczących struktur[11].
W trakcie kompilacji, komentarze zastępowane są znakiem spacji[29]. Dyrektywy preprocesora rozpoczynają się od znaku # i muszą znajdować się w osobnych liniach (dopuszczalne jest by przed symbolem kratki znajdowały się spacje)[52]. 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].
Jądro Linux i jego odmiany, które należą do projektów o największej bazie kodu[3]. 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].
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].
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].