VS | ||
Data pierwszej wersji | 1972 | |
---|---|---|
Paradygmat | Tryb imperatywny , proceduralny , ustrukturyzowany | |
Autor | Dennis Ritchie , Brian Kernighan | |
Deweloper | Dennis Ritchie i Kenneth Thompson, Bell Labs | |
Pisanie na maszynie | Statyczne , słabe | |
Normy | ANSI X3.159-1989 (ANSI C, C89) ISO / IEC 9899: 1990 (C90) ISO / IEC 9899: 1990 / AMD1: 1995 (C95) ISO / IEC 9899: 1999 (C99) ISO / IEC 9899: 2011 ( C11) ISO / IEC 9899: 2018 (C18) |
|
Wpływem | BCPL , B , Algol 68 , Fortran | |
Pod wpływem | awk , csh , C ++ , C # , Objective-C , D , Concurrent C , Java , JavaScript , PHP , Perl | |
Wdrożenia | GCC , MSVC , Borland C , Clang , TCC | |
Rozszerzenia plików | .c, .h | |
Jest to imperatywny język programowania niskiego poziomu ogólnego przeznaczenia . Wynaleziony we wczesnych latach siedemdziesiątych XX wieku w celu przepisania UNIX-a , C stał się jednym z najczęściej używanych języków, nawet dzisiaj. Wiele bardziej nowoczesnych języków, takich jak C ++ , C # , Java i PHP lub Javascript , przyjęło składnię podobną do C i częściowo odebrało jej logikę. C oferuje programiście znaczny margines kontroli nad maszyną (w szczególności nad zarządzaniem pamięcią) i dlatego jest używany do budowania „podstaw” (kompilatorów, tłumaczy itp.) Tych bardziej nowoczesnych języków.
Język C został wynaleziony w 1972 roku w Bell Laboratories . Został opracowany w tym samym czasie co UNIX przez Dennisa Ritchiego i Kennetha Thompsona. Kenneth Thompson opracował poprzednika języka C, języka B , który sam w sobie jest inspirowany przez BCPL . Dennis Ritchie przekształcił język B w nową wersję, która była wystarczająco inna, w tym dodawanie typów , tak że nazywała się C.
Chociaż C jest oficjalnie inspirowany przez B i BCPL, zauważamy silny wpływ PL / I (lub PL360); moglibyśmy powiedzieć, że C było tym, czym dla Uniksa i PDP-11 było PL / I przy przepisywaniu Multics .
Następnie Brian Kernighan pomógł spopularyzować język C.Doprowadziło to również do pewnych zmian w ostatniej chwili.
W 1978 roku Kernighan był głównym autorem książki The C Programming Language, opisującej ostatecznie ustabilizowany język; Ritchie zajął się dodatkami i przykładami w systemie Unix. Ta książka jest również nazywana „K&R” i mówimy o tradycyjnym C lub C K&R , odnosząc się do języka takiego, jaki istniał w tamtym czasie.
W 1983 roku The American National Standards Institute (ANSI) utworzyli Komitet Standardów językowe (X3J11), który w 1989 roku zakończył się tzw ANSI C lub C89 (formalnie ANSI X3.159-1989) standard. W 1990 roku norma ta została również przyjęta przez Międzynarodową Organizację Normalizacyjną ( C90, C ISO , formalnie ISO / IEC 9899: 1990). ANSI C to ewolucja K&R C, która pozostaje niezwykle kompatybilna. Zajmuje się niektórymi koncepcjami C ++ , w szczególności pojęciem kwalifikatorów prototypów i typów.
W latach 1994 i 1996 , grupa robocza ISO (ISO / IEC JTC1 / SC22 / WG14) opublikował dwie poprawki i jedną poprawkę do C90: ISO / IEC 9899 / COR1: 1994 Techniczne Sprostowanie 1 , ISO / IEC 9899 / Amd1: 1995 Integralność C oraz ISO / IEC 9899 / COR1: 1996 Techniczne sprostowanie 2 . Te dość skromne zmiany są czasami nazywane C89 ze zmianą 1 lub C94 / C95. Dodano trzy pliki nagłówkowe, z których dwa odnoszą się do szerokich znaków, a drugi definiuje szereg makr związanych ze standardem znaków ISO 646 .
W 1999 roku nowa wersja języka została znormalizowana przez ISO : C99 (formalnie ISO / IEC 9899: 1999). Nowe funkcje obejmują tablice o zmiennej wielkości, ograniczone wskaźniki, liczby zespolone, literały złożone, instrukcje mieszane z instrukcjami, funkcje wbudowane , zaawansowaną obsługę liczb zmiennoprzecinkowych i składnię komentarzy w języku C ++. C 99 Standardowa biblioteka została wzbogacona o sześć plików nagłówkowych od poprzedniego standardu.
W 2011 roku ISO ratyfikuje nową wersję normy: C11 , formalnie ISO / IEC 9899: 2011. Wprowadzono tę zmianę, w tym obsługę programowania wielowątkowego , wyrażenia w takich ogólnych i lepszą obsługę Unicode .
Jest to język programowania o charakterze imperatywnym i uniwersalnym. Jest kwalifikowany jako język niskiego poziomu w tym sensie, że każda instrukcja języka jest zaprojektowana tak, aby była zestawiana w pewną liczbę instrukcji maszynowych, które są dość przewidywalne pod względem zajętości pamięci i obciążenia obliczeniowego. Ponadto oferuje szereg typów całkowitych i zmiennoprzecinkowych zaprojektowanych tak, aby mogły bezpośrednio odpowiadać typom danych obsługiwanym przez procesor . Wreszcie, intensywnie wykorzystuje obliczenia adresów pamięci z koncepcją wskaźnika .
Oprócz typów podstawowych, C obsługuje typy wyliczeniowe , złożone i nieprzezroczyste . Jednak nie oferuje żadnej operacji, która bezpośrednio przetwarza obiekty wyższego poziomu ( plik komputerowy , ciąg znaków , lista , tablica skrótów itp.). Z tymi bardziej rozwiniętymi typami trzeba sobie radzić, manipulując wskaźnikami i typami złożonymi. Podobnie język nie oferuje standardowo zarządzania programowaniem obiektowym ani systemu zarządzania wyjątkami . Istnieją standardowe funkcje do zarządzania danymi wejściowymi i wyjściowymi oraz ciągami znaków , ale w przeciwieństwie do innych języków nie ma określonego operatora, który poprawiłby ergonomię. Ułatwia to zastąpienie standardowych funkcji funkcjami zaprojektowanymi specjalnie dla danego programu.
Te cechy sprawiają, że jest to preferowany język, gdy próbuje się kontrolować używane zasoby sprzętowe, a język maszynowy i dane binarne generowane przez kompilatory są stosunkowo przewidywalne. Dlatego język ten jest szeroko stosowany w takich dziedzinach, jak programowanie osadzone na mikrokontrolerach , intensywne obliczenia, pisanie systemów operacyjnych i modułów, w których ważna jest szybkość przetwarzania. Jest to dobra alternatywa dla języka asemblera w tych obszarach, z zaletami bardziej wyrazistej składni i przenośności kodu źródłowego . Język C został wynaleziony do pisania systemu operacyjnego UNIX i nadal jest używany do programowania systemu. W związku z tym jądro dużych systemów operacyjnych, takich jak Windows i Linux, jest opracowywane głównie w C.
Z drugiej strony, tworzenie programów w C, zwłaszcza jeśli używają złożonych struktur danych, jest trudniejsze niż w przypadku języków wyższego poziomu. Rzeczywiście, ze względu na wydajność, język C wymaga od użytkownika zaprogramowania pewnych procesów (zwolnienia pamięci, sprawdzenia poprawności indeksów w tabelach itp.), Które są automatycznie obsługiwane w językach wysokiego poziomu.
Pozbawiony udogodnień zapewnianych przez standardową bibliotekę, C jest prostym językiem, podobnie jak jego kompilator . Jest to odczuwalne w czasie opracowywania kompilatora C dla nowej architektury procesora : Kernighan i Ritchie oszacowali, że może zostać opracowany w ciągu dwóch miesięcy, ponieważ „zauważymy, że 80% kodu nowego kompilatora jest identyczne z kodem kody innych istniejących kompilatorów. "
Jest to jeden z najczęściej używanych języków, ponieważ:
Jego główne wady to:
Program Hello world jest oferowany jako przykład w 1978 roku w języku programowania C autorstwa Briana Kernighana i Dennisa Ritchiego . Stworzenie programu wyświetlającego „ hello world ” stało się od tamtego czasu przykładem pokazującym podstawy nowego języka. Oto oryginalny przykładem 1 st Edition 1978:
main() { printf("hello, world\n"); }Ten sam program, zgodny ze standardem ISO i zgodnie ze współczesnymi dobrymi praktykami:
#include <stdio.h> int main(void) { printf("hello, world\n"); return 0; }Składnia języka C została zaprojektowana jako krótka. Historycznie często był porównywany do języka Pascal , imperatywnego języka stworzonego również w latach 70 . Oto przykład z funkcją silni :
/* En C (norme ISO) */ int factorielle(int n) { return n > 1 ? n * factorielle(n - 1) : 1; } { En Pascal } function factorielle(n: integer) : integer begin if n > 1 then factorielle := n * factorielle(n - 1) else factorielle := 1 end.Gdzie Pascal 7 użyciu słów kluczowych ( function, integer, begin, if, then, elsei end) C wykorzystuje tylko 2 ( inti return).
Język wyrażeńZwięzłość języka C to nie tylko składnia. Duża liczba dostępnych operatorów , fakt, że większość instrukcji zawiera wyrażenie, że wyrażenia prawie zawsze generują wartość, a instrukcje testowe po prostu porównują wartość testowanego wyrażenia do zera, wszystko to przyczynia się do zwięzłości kodu źródłowego.
Oto przykład funkcji copy string - której zasadą jest kopiowanie znaków do momentu skopiowania znaku null, który zgodnie z konwencją oznacza koniec łańcucha w C - podany w The C Programming Language, wydanie 2 , s. 106 :
void strcpy(char *s, char *t) { while (*s++ = *t++) ; }Pętla whileużywa klasycznego stylu C, dzięki czemu zyskała reputację słabo czytelnego języka. Wyrażenie *s++ = *t++zawiera: dwie dereferencje wskaźnika ; dwa przyrosty wskaźnika; zadanie ; a przypisana wartość jest porównywana z zerem przez while. Ta pętla nie ma treści, ponieważ wszystkie operacje są wykonywane w wyrażeniu testowym while. Uważamy, że konieczne jest opanowanie tego rodzaju notacji, aby opanować C.
Dla porównania, wersja nie używająca operatorów skrótów i niejawnego porównania zerowego dałaby:
void strcpy(char *s, char *t) { while (*t != '\0') { *s = *t; s = s + 1; t = t + 1; } *s = *t; }Program napisany w C jest zwykle podzielony na kilka plików źródłowych kompilowanych osobno.
Pliki źródłowe C to pliki tekstowe , zwykle zakodowane w systemie hosta. Można je pisać za pomocą prostego edytora tekstu . Istnieje wiele edytorów, nawet zintegrowanych środowisk programistycznych (IDE), które mają określone funkcje wspomagające pisanie źródeł C.
Celem jest podanie nazwy pliku rozszerzeń .c i .hplików źródłowych C. Pliki .hnazywane są plikami nagłówkowymi , nagłówkiem w języku angielskim . Są przeznaczone do umieszczania na początku plików źródłowych i zawierają tylko instrukcje .
Gdy plik .club .hużywa identyfikatora zadeklarowanego w innym pliku .h, zawiera ten drugi. Ogólnie stosowana zasada polega na zapisaniu pliku .hdla każdego pliku .ci zadeklarowaniu w pliku .hwszystkiego, co jest przez plik eksportowane .c.
Generowanie pliku wykonywalnego z plików źródłowych odbywa się w kilku krokach, które często są zautomatyzowane przy użyciu narzędzi takich jak make , SCons lub narzędzia specyficzne dla zintegrowanego środowiska programistycznego. Istnieją cztery kroki prowadzące od źródła do pliku wykonywalnego: prekompilacja , kompilacja , asemblacja , edycja linków . Kiedy projekt jest kompilowany, tylko pliki .csą częścią listy plików do skompilowania; pliki .hsą zawarte w dyrektywach preprocesora zawartych w plikach źródłowych.
Preprocesor C wykonuje dyrektywy zawarte w plikach źródłowych. Rozpoznaje je po tym, że znajdują się na początku linii, a wszystkie zaczynają się od krzyża # . Oto niektóre z najczęstszych wskazówek:
Oprócz dyrektyw wykonawczych preprocesor zastępuje komentarze odstępami i zastępuje makra. W pozostałej części kod źródłowy jest przesyłany bez zmian do kompilatora w następnej fazie. Jednak każdy #includew kodzie źródłowym musi być rekurencyjnie zastępowany przez dołączony kod źródłowy. W ten sposób kompilator otrzymuje pojedyncze źródło z preprocesora, które stanowi jednostkę kompilacji.
Oto przykład pliku źródłowego, który copyarray.hklasycznie korzysta z dyrektyw preprocesora:
#ifndef COPYARRAY_H #define COPYARRAY_H #include <stddef.h> void copyArray(int *, size_t); #endifTe #ifndef, #defineoraz dyrektyw #endifupewnić się, że wewnątrz kod jest kompilowany tylko raz, nawet jeśli jest włączone wielokrotnie. Dyrektywa #include <stddef.h>zawiera nagłówek, który deklaruje typ size_tużyty poniżej.
Faza kompilacji zasadniczo składa się z wygenerowania kodu asemblera . To najbardziej intensywna faza zabiegów. Jest wykonywany przez sam kompilator. Dla każdej jednostki kompilacji otrzymujemy plik języka asemblera.
Ten krok można podzielić na podetapy:
Przez nadużycie języka kompilację wywołuje się całą fazę generowania pliku wykonywalnego, zaczynając od plików źródłowych. Ale to tylko jeden z kroków prowadzących do utworzenia pliku wykonywalnego.
Niektóre kompilatory C działają na tym poziomie w dwóch fazach, pierwsza generuje plik skompilowany w języku pośrednim przeznaczonym dla idealnej maszyny wirtualnej (patrz Kod bajtowy lub P-Code ) przenośny z jednej platformy na drugą, druga konwertuje język pośredni w asemblerze język zależny od platformy docelowej. Inne kompilatory C umożliwiają nie generowanie języka asemblera, a jedynie plik skompilowany w języku pośrednim , który zostanie zinterpretowany lub skompilowany automatycznie w kodzie natywnym w czasie wykonywania na maszynie docelowej (przez maszynę wirtualną, która zostanie połączona w ostatecznym program).
Ten krok polega na wygenerowaniu pliku obiektowego w języku maszynowym dla każdego pliku kodu asemblera. Pliki obiektowe mają generalnie rozszerzenie .ow systemie Unix i .objwraz z narzędziami programistycznymi dla MS-DOS , Microsoft Windows , VMS , CP / M … Ta faza jest czasami grupowana z poprzednią poprzez ustanowienie wewnętrznego przepływu danych bez przekazywania plików w języku pośrednim lub język asemblera. W tym przypadku kompilator bezpośrednio generuje plik obiektowy.
W przypadku kompilatorów, które generują kod pośredni, tę fazę asemblacji można również całkowicie wyeliminować: jest to maszyna wirtualna, która zinterpretuje lub skompiluje ten język do natywnego kodu maszynowego. Maszyna wirtualna może być składnikiem systemu operacyjnego lub biblioteką współużytkowaną.
Łączenie jest ostatnim krokiem i ma na celu połączenie wszystkich elementów programu. Różne pliki obiektowe są następnie łączone razem z bibliotekami statycznymi w celu utworzenia tylko jednego pliku wykonywalnego.
Celem linkowania jest wybranie przydatnych elementów kodu obecnych w zbiorze skompilowanych kodów i bibliotek oraz rozwiązanie wzajemnych odniesień między tymi różnymi elementami, aby umożliwić im bezpośrednie odniesienie do wykonania programu. Łączenie nie powiedzie się, jeśli brakuje elementów kodu, do których istnieją odniesienia.
ASCII character set jest wystarczająca, aby pisać w C. Jest nawet możliwe, ale niezwykły, aby ograniczyć się do niezmiennego zestawu znaków ISO 646 standardzie , stosując sekwencje zwane trigraph. Zazwyczaj źródła C są pisane z zestawem znaków systemu hosta. Jednak możliwe jest, że zestaw znaków wykonawczych nie jest tym, który pochodzi ze źródła.
W C rozróżniana jest wielkość liter . Białe znaki ( spacja , tabulator , koniec wiersza ) mogą być dowolnie używane do układu, ponieważ w większości przypadków są równoważne pojedynczej spacji.
C89 ma 32 słowa kluczowe, z których pięć nie istniało w K&R C i które są w porządku alfabetycznym:
auto, break, case, char, const(C89), continue, default, do, double, else, enum(C89), extern, float, for, goto, if, int, long, register, return, short, signed(C89), sizeof, static, struct, switch, typedef, union, unsigned, void(C89), volatile(C89) while.Są to terminy zastrzeżone i nie należy ich używać w inny sposób.
Wersja C99 dodaje pięć:
_Bool, _Complex, _Imaginary, inline, restrict.Te nowe słowa kluczowe zaczynają się wielką literą poprzedzoną podkreśleniem, aby zmaksymalizować zgodność z istniejącymi kodami. Standardowe nagłówki bibliotek zawierają aliasy bool( <stdbool.h>) complexi imaginary( <complex.h>).
Najnowsza wersja, C11, wprowadza kolejne siedem nowych słów kluczowych z tymi samymi konwencjami:
_Alignas, _Alignof, _Atomic, _Generic, _Noreturn, _Static_assert, _Thread_local.Standardowe nagłówki <stdalign.h>, <stdnoreturn.h>, <assert.h>i <threads.h>zapewniają aliasy odpowiednio alignasa alignof, noreturn, static_asserti thread_local.
W języku C preprocesora oferty następujących dyrektyw:
#include, #define, #pragma(C89) #if, #ifdef, #ifndef, #elif(C89), #else, #endif, #undef, #line, #error.Język C rozumie wiele rodzajów z liczb całkowitych , zajmując mniej lub więcej bitów . Rozmiar typów jest tylko częściowo znormalizowany: norma określa tylko minimalny rozmiar i minimalną wielkość. Minimalne wielkości są zgodne z reprezentacjami binarnymi innymi niż uzupełnienia do dwóch , chociaż ta reprezentacja jest prawie zawsze używana w praktyce. Ta elastyczność umożliwia efektywne dostosowanie języka do wielu różnych procesorów , ale komplikuje przenośność programów napisanych w C.
Każdy typ liczby całkowitej ma formę „ze znakiem”, która może przedstawiać liczby ujemne i dodatnie, oraz postać „bez znaku”, która może przedstawiać tylko liczby naturalne . Podpisane i niepodpisane kształty muszą mieć ten sam rozmiar.
Najpopularniejszym typem jest intmaszyna słów.
W przeciwieństwie do wielu innych języków, type charjest typem całkowitoliczbowym, jak każdy inny, chociaż zwykle jest używany do reprezentowania znaków. Jego rozmiar to z definicji jeden bajt .
Rodzaj | Minimalna zdolność do reprezentacji | Minimalna wielkość wymagana przez normę |
---|---|---|
char | jak signed charlub unsigned char, w zależności od implementacji | 8 bitów |
signed char | -127 do 127 | |
unsigned char (C89) | Od 0 do 255 | |
short signed short |
−32 767 do 32767 | 16 bitów |
unsigned short | Od 0 do 65 535 | |
int signed int |
−32 767 do 32767 | 16 bitów |
unsigned int | Od 0 do 65 535 | |
long signed long |
-2 147 483 647 do 2147 483 647 | 32-bitowy |
unsigned long | Od 0 do 4 294 967 295 | |
long long(C99) signed long long(C99) |
−9 223 372 036 854 775 807 do 9 223 372 036 854 775 807 | 64 bity |
unsigned long long (C99) | Od 0 do 18 446 744 073 709 552 000 |
Te wymienione typy są definiowane za pomocą słowa kluczowego enum.
Istnieją typy liczb zmiennoprzecinkowych , dokładność, stąd długość w bitach, zmienna; rosnąco:
Rodzaj | Precyzja | Wielkość |
---|---|---|
float | ≥ 6 cyfr dziesiętnych | około 10 -37 do 10 +37 |
double | ≥ 10 cyfr dziesiętnych | około 10 -37 do 10 +37 |
long double | ≥ 10 cyfr dziesiętnych | około 10 -37 do 10 +37 |
long double (C89) | ≥ 10 cyfr dziesiętnych |
C99 dodaje się float complex, double complexi long double complexreprezentujące związane liczb zespolonych .
Rozbudowane typy:
Typ _Booljest znormalizowany przez C99. We wcześniejszych wersjach języka powszechnie definiowano synonim:
typedef enum boolean {false, true} bool;Typ voidreprezentuje void, na przykład pustą listę parametrów funkcji lub funkcję, która nic nie zwraca.
Typ void*jest wskaźnikiem ogólnym: każdy wskaźnik danych może być niejawnie konwertowany zi do void*. Jest to na przykład typ zwracany przez funkcję standardową malloc, która przydziela pamięć. Ten typ nie nadaje się do operacji wymagających znajomości rozmiaru typu spiczastego (arytmetyka wskaźników, dereferencja).
C obsługuje typy złożone z pojęciem struktury . Aby zdefiniować strukturę, należy użyć słowa kluczowego, structpo którym następuje nazwa struktury. Członkowie należy następnie zadeklarować w nawiasach klamrowych. Jak każde stwierdzenie, średnik kończy całość.
/* Déclaration de la structure personne */ struct Personne { int age; char *nom; };Aby uzyskać dostęp do elementów struktury, musisz użyć operatora ..
int main() { struct Personne p; p.nom = "Albert"; p.age = 46; }Funkcje mogą otrzymywać wskaźniki do struktur. Działają z tą samą składnią, co klasyczne wskaźniki. Jednak operator ->musi być używany na wskaźniku, aby uzyskać dostęp do pól struktury. Możliwe jest również wyłuskiwanie wskaźnika, aby nie używać tego operatora i nadal używać operatora ..
void anniversaire(struct Personne * p) { p->age++; printf("Joyeux anniversaire %s !", (*p).nom); } int main() { struct Personne p; p.nom = "Albert"; p.age = 46; anniversaire(&p); }W wersjach C wcześniejszych niż C99 komentarze musiały zaczynać się ukośnikiem i gwiazdką („/ *”) i kończyć gwiazdką i ukośnikiem. Prawie wszystkie współczesne języki używają tej składni do pisania komentarzy w kodzie. Wszystko między tymi symbolami to komentarz, w tym podział wiersza:
/* Ceci est un commentaire sur deux lignes ou plus */Standard C99 przejął od komentarzy końca linii w C ++, wprowadzony dwoma ukośnikami i zakończony linią:
// Commentaire jusqu'à la fin de la ligneSkładnia różnych istniejących struktur kontrolnych w C jest szeroko stosowana w kilku innych językach, takich jak oczywiście C ++, ale także Java , C # , PHP, a nawet JavaScript .
Istnieją trzy główne typy konstrukcji:
Funkcje w C to bloki instrukcji, które odbierają jeden lub więcej argumentów i mogą zwracać wartość. Jeśli funkcja nie zwraca żadnej wartości, voidużywane jest słowo kluczowe . Funkcja nie może również przyjmować żadnych argumentów. W voidtym przypadku zalecane jest użycie słowa kluczowego .
// Fonction ne retournant aucune valeur (appelée procédure) void afficher(int a) { printf("%d", a); } // Fonction retournant un entier int somme(int a, int b) { return a + b; } // Fonction sans aucun argument int saisir(void) { int a; scanf("%d", &a); return a; } PrototypPrototyp polega na zadeklarowaniu funkcji i jej parametrów bez instrukcji, które ją tworzą. Prototyp kończy się średnikiem.
// Prototype de saisir int saisir(void); // Fonction utilisant saisir int somme(void) { int a = saisir(), b = saisir(); return a + b; } // Définition de saisir int saisir(void) { int a; scanf("%d", &a); return a; }Zwykle wszystkie prototypy są zapisywane w plikach .h , a funkcje są definiowane w pliku .c .
Standard języka C celowo nie określa pewnych operacji. Ta właściwość języka C pozwala kompilatorom bezpośrednio używać instrukcji specyficznych dla procesora , przeprowadzać optymalizacje lub ignorować pewne operacje, a także kompilować krótkie i wydajne programy wykonywalne. W zamian za to, że czasami przyczyną błędów o przenośności z kodem źródłowym napisanych w C.
Istnieją trzy kategorie takich zachowań:
W C zachowania zdefiniowane przez implementację to te, w przypadku których implementacja musi wybrać zachowanie i się go trzymać. Wybór ten może być wolny lub pochodzić z listy możliwości, jakie daje norma. Wybór musi być udokumentowany przez implementację, aby programista mógł go znać i używać.
Jednym z najważniejszych przykładów takiego zachowania jest rozmiar liczb całkowitych. Norma C określa minimalny rozmiar typów podstawowych, ale nie określa ich dokładnego rozmiaru. Zatem intna przykład typ odpowiadający słowu maszynowemu musi mieć minimalny rozmiar 16 bitów . Może mieć rozmiar 16-bitowy w przypadku procesora 16-bitowego i rozmiar 64-bitowy w przypadku procesora 64-bitowego.
Innym przykładem jest reprezentacja liczb całkowitych ze znakiem. Może to być dopełnienie do dwóch , dopełnienie własne lub system z bitem znaku i bitami wartości (en) . Zdecydowana większość nowoczesnych systemów wykorzystuje dopełnienie do dwóch, które jest na przykład jedynym nadal obsługiwanym przez GCC. Stare systemy używają innych formatów, takich jak IBM 7090, który używa formatu znak / wartość, PDP-1 lub UNIVAC i jego potomkowie, z których niektóre są nadal używane, na przykład seria UNIVAC 1100/2200 # UNISYS 2200 series ( en) , które wykorzystują swoje dopełnienie.
Innym przykładem jest przesunięcie w prawo ujemnej liczby całkowitej ze znakiem. Zazwyczaj implementacja może wybrać przesunięcie jak dla liczby całkowitej bez znaku lub propagację najbardziej znaczącego bitu reprezentującego znak.
Nieokreślone zachowaniaNieokreślone zachowania są podobne do zachowań zdefiniowanych w implementacji, ale zachowanie przyjęte przez implementację nie musi być dokumentowane. Nie musi nawet być taki sam w każdych okolicznościach. Niemniej jednak program pozostaje poprawny, programista po prostu nie może polegać na określonej regule.
Na przykład kolejność oceny parametrów podczas wywołania funkcji nie jest określona. Kompilator może nawet zdecydować się na ocenę parametrów dwóch wywołań tej samej funkcji w innej kolejności, jeśli pomoże to w jej optymalizacji.
Niezdefiniowane zachowaniaStandard C definiuje pewne przypadki, w których konstrukcje poprawne składniowo mają niezdefiniowane zachowanie. Zgodnie ze standardem wszystko może się wtedy zdarzyć: kompilacja może się nie powieść lub może spowodować powstanie pliku wykonywalnego, którego wykonanie zostanie przerwane, lub który da fałszywe wyniki, a nawet sprawi wrażenie, że działa bezbłędnie. Gdy program zawiera niezdefiniowane zachowanie, to zachowanie całego programu staje się niezdefiniowane, a nie tylko zachowanie instrukcji zawierającej błąd. Zatem błędna instrukcja może spowodować uszkodzenie danych, które będą przetwarzane znacznie później, opóźniając w ten sposób wystąpienie błędu. I nawet bez wykonania zła instrukcja może spowodować, że kompilator przeprowadzi optymalizacje na podstawie błędnych założeń, tworząc plik wykonywalny, który w ogóle nie robi tego, czego się oczekuje.
PrzykładyMożemy wskazać klasyczne dzielenie przez zero lub wielokrotne przypisanie zmiennej w tym samym wyrażeniu za pomocą przykładu:
int i = 4; i = i++; /* Comportement indéfini. */Można by więc pomyśleć, że w tym przykładzie imoże być wart 4 lub 5 w zależności od wyboru kompilatora, ale równie dobrze może być wart 42, albo przypisanie może zatrzymać wykonywanie, albo kompilator może odmówić kompilacji. Nie ma gwarancji, gdy tylko pojawi się niezdefiniowane zachowanie.
Aby przytoczyć tylko kilka przykładów, wyłuskiwanie wskaźnika zerowego, uzyskiwanie dostępu do tablicy poza jej granicami, używanie niezainicjowanej zmiennej lub przepełnienie liczb całkowitych ze znakiem mają niezdefiniowane zachowania. Kompilator może w niektórych przypadkach wykorzystać fakt, że kompilacja jest niezdefiniowana, aby założyć, że taki przypadek nigdy nie występuje i bardziej agresywnie zoptymalizować kod. Chociaż powyższy przykład może wydawać się oczywisty, niektóre złożone przykłady mogą być znacznie subtelniejsze i czasami prowadzić do poważnych błędów.
Na przykład wiele kodu zawiera kontrole, które mają na celu uniknięcie wykonywania w przypadkach poza zakresem, które mogą wyglądać tak:
char buffer[BUFLEN]; char *buffer_end = buffer + BUFLEN; unsigned int len; /* ... */ if (buffer + len >= buffer_end || /* vérification de dépassement du buffer */ buffer + len < buffer) /* vérification de débordement si len très large */ return; /* Si pas de débordement, effectue les opérations prévues */ /* ... */Najwyraźniej ten kod jest ostrożny i przeprowadza niezbędne kontrole bezpieczeństwa, aby nie przepełnić przydzielonego bufora. W praktyce najnowsze wersje kompilatorów, takie jak GCC , Clang lub Microsoft Visual C ++, mogą pomijać drugi test i powodować przepełnienia. Rzeczywiście, norma określa, że arytmetyka wskaźnika na obiekcie nie może dać wskaźnika poza tym obiektem. Kompilator może zatem zdecydować, że test jest nadal fałszywy i go usunąć. Prawidłowe sprawdzenie wygląda następująco:
char buffer[BUFLEN]; unsigned int len; /* ... */ if (len >= BUFLEN) /* vérification de dépassement du buffer */ return; /* Si pas de débordement, effectue les opérations prévues */ /* ... */W 2008 roku, kiedy twórcy GCC zmodyfikowali kompilator, aby zoptymalizować pewne testy przepełnienia, które opierały się na niezdefiniowanych zachowaniach, CERT wydał ostrzeżenie o używaniu najnowszych wersji GCC . Te optymalizacje są w rzeczywistości obecne w większości nowoczesnych kompilatorów, CERT zmienił swoje zastrzeżenie w tym celu.
Istnieją narzędzia do wykrywania tych problematycznych kompilacji, a najlepsze kompilatory wykrywają niektóre z nich (czasami trzeba włączyć określone opcje) i mogą je oflagować, ale żaden nie twierdzi, że jest wyczerpujący.
Standaryzowane standardowe biblioteki , dostępna we wszystkich implementacjach, wyposażony prostotę związanego z językiem niskiego poziomu. Oto lista niektórych nagłówków deklarujących typy i funkcje biblioteki standardowej:
Znormalizowana biblioteka standardowa nie oferuje wsparcia dla interfejsu graficznego , sieci, I / O na portach szeregowych lub równoległych, systemów czasu rzeczywistego , procesów, a nawet zaawansowanej obsługi błędów (jak w przypadku wyjątków strukturalnych). Mogłoby to dodatkowo ograniczyć praktyczną przenośność programów, które muszą używać niektórych z tych funkcji, bez istnienia bardzo wielu przenośnych bibliotek i rekompensaty za ten brak; w świecie UNIX ta potrzeba doprowadziła również do pojawienia się innego standardu, POSIX .1.
Język C jest jednym z najczęściej używanych języków programowania, wiele bibliotek zostało utworzonych do użytku z C: glib itp. Często podczas wymyślania formatu danych istnieje biblioteka referencyjna C lub oprogramowanie do manipulowania formatem. Dzieje się tak w przypadku zlib , libjpeg , libpng , Expat , referencyjnych dekoderów MPEG , libsocket itp.
Oto kilka przykładów przedstawiających bardzo krótko niektóre właściwości C. Aby uzyskać więcej informacji, zobacz WikiBook „Programowanie w języku C” .
Struktura int_listreprezentuje jeden element połączonej listy liczb całkowitych. Następujące dwie funkcje ( insert_nexti remove_next) służą do dodawania i usuwania pozycji z listy.
/* La gestion de la mémoire n'est pas intégrée au langage mais assurée par des fonctions de la bibliothèque standard. */ #include <stdlib.h> struct int_list { struct int_list *next; /* pointeur sur l'élément suivant */ int value; /* valeur de l'élément */ }; /* * Ajouter un élément à la suite d'un autre. * node : élément après lequel ajouter le nouveau * value : valeur de l'élément à ajouter * Retourne : adresse de l'élément ajouté, ou NULL en cas d'erreur. */ struct int_list *insert_next(struct int_list *node, int value) { /* Allocation de la mémoire pour un nouvel élément. */ struct int_list *const new_next = malloc(sizeof *new_next); /* Si l'allocation a réussi, alors insérer new_next entre node et node->next. */ if (new_next) { new_next->next = node->next; node->next = new_next; new_next->value = value; } return new_next; } /* * Supprimer l'élément suivant un autre. * node : élément dont le suivant est supprimé * Attention : comportement indéterminé s'il n'y pas d'élément suivant ! */ void remove_next(struct int_list *node) { struct int_list *const node_to_remove = node->next; /* Retire l'élément suivant de la liste. */ node->next = node->next->next; /* Libère la mémoire occupée par l'élément suivant. */ free(node_to_remove); }W tym przykładzie dwie podstawowe funkcje to malloci free. Pierwszy służy do alokacji pamięci, otrzymany parametr to liczba bajtów , które chcemy zaalokować i zwraca adres pierwszego przydzielonego bajtu, w przeciwnym razie zwraca NULL. freesłuży do zwalniania pamięci przydzielonej przez malloc.