C

Programowanie w językach niskiego poziomu wymaga rozumienia tego, jak komputer faktycznie wykonuje instrukcje, zarządza pamięcią i reprezentuje dane. Jednym z najważniejszych języków, który pozwala pracować bardzo blisko sprzętu, a jednocześnie zachować względną przenośność między systemami, jest język programowania C. To język programowania, proceduralny, kompilowany, o niewielkiej liczbie abstrakcji, dający pełną kontrolę nad pamięcią i reprezentacją danych.

Język programowania C – niski poziom z kontrolą pamięci i minimalną warstwą abstrakcji

C powstał na początku lat 70. XX wieku w laboratoriach Bell. Autorem był Dennis Ritchie. Język został zaprojektowany do implementacji systemu operacyjnego Unix, co mocno wpłynęło na jego charakter: prostota składni, bezpośredni dostęp do pamięci, brak automatycznego zarządzania zasobami.

C jest językiem:

  • kompilowanym (kod źródłowy → kod maszynowy),
  • proceduralnym,
  • statycznie typowanym,
  • bez wbudowanego systemu klas czy wyjątków,
  • z ręcznym zarządzaniem pamięcią.

Brak „ochronnych” mechanizmów powoduje, że programista odpowiada za poprawność niemal każdego aspektu działania programu: zakresy tablic, alokację pamięci, zwalnianie zasobów, konwersje typów.

Przykładowy minimalny program:

#include <stdio.h>

int main() {
    printf("Hello, world!\n");
    return 0;
}

Każdy element ma znaczenie:

  • #include <stdio.h> – dołączenie deklaracji funkcji wejścia/wyjścia,
  • int main() – punkt wejścia programu,
  • return 0; – kod zakończenia procesu.

W C nie ma ukrytych konstrukcji startowych. Kompilator generuje kod maszynowy, który po uruchomieniu przekazuje sterowanie funkcji main.

Język C – model pamięci, wskaźniki i arytmetyka adresów jako fundament zrozumienia języka

Najważniejszym elementem, który odróżnia C od języków wysokiego poziomu, jest jawna praca z pamięcią.

Segmenty pamięci procesu

Typowy proces posiada:

  • segment kodu (instrukcje),
  • segment danych globalnych,
  • stertę (heap),
  • stos (stack).

Zmienne lokalne trafiają na stos:

int main() {
    int x = 5;
    return 0;
}

x istnieje tylko w czasie działania funkcji.

Pamięć dynamiczna pochodzi ze sterty:

#include <stdlib.h>

int main() {
    int *p = malloc(sizeof(int));
    *p = 10;
    free(p);
    return 0;
}

Tu programista musi sam wywołać free. Brak zwolnienia pamięci oznacza wyciek.

Wskaźniki

Wskaźnik to zmienna przechowująca adres:

int a = 5;
int *p = &a;
  • &a – adres zmiennej,
  • *p – dereferencja (odczyt/zapis wartości pod adresem).

Arytmetyka wskaźników:

int tab[3] = {1, 2, 3};
int *p = tab;

p++;   // przesunięcie o sizeof(int)

Zwiększenie wskaźnika nie dodaje 1 bajtu, lecz rozmiar typu.

Błędy typowe

  • dereferencja niezainicjalizowanego wskaźnika,
  • podwójne free,
  • przekroczenie zakresu tablicy,
  • użycie pamięci po zwolnieniu.

C nie chroni przed tymi błędami.

Programowanie C w kontekście kompilacji, standardu języka i przenośności kodu między systemami

C nie jest jednym, zamkniętym bytem. Istnieją standardy:

  • C89 / C90
  • C99
  • C11
  • C17
  • C23

Różnice dotyczą m.in.:

  • deklaracji zmiennych w dowolnym miejscu bloku (C99),
  • typów o stałej szerokości (stdint.h),
  • wsparcia wielowątkowości (C11).

Proces kompilacji składa się z etapów:

  1. Preprocesor – rozwija makra i dyrektywy #include.
  2. Kompilator – generuje kod pośredni / asembler.
  3. Assembler – generuje kod maszynowy.
  4. Linker – łączy moduły i biblioteki.

Przykład kompilacji:

gcc program.c -o program

Przenośność kodu zależy od:

  • używania standardowych bibliotek,
  • unikania rozszerzeń kompilatora,
  • kontrolowania rozmiarów typów.

Podstawowe konstrukcje językowe i kontrola przepływu programu

Typy podstawowe

  • int
  • char
  • float
  • double
  • void

Rozmiar typu zależy od architektury. Dla precyzyjnej kontroli stosuje się:

#include <stdint.h>

int32_t x;
uint64_t y;

Instrukcje warunkowe

if (x > 0) {
    ...
} else {
    ...
}

Instrukcja switch:

switch (x) {
    case 1:
        break;
    default:
        break;
}

Pętle

for (int i = 0; i < 10; i++) {
    ...
}
while (x > 0) {
    x--;
}

Brak kontroli zakresów powoduje, że nieskończona pętla jest łatwa do utworzenia.

Struktury danych w C: struktury, tablice i podstawy abstrakcji

Struktury

struct Punkt {
    int x;
    int y;
};

Użycie:

struct Punkt p;
p.x = 1;
p.y = 2;

Wskaźnik do struktury:

struct Punkt *ptr = &p;
ptr->x = 5;

Operator -> dereferencjonuje wskaźnik do struktury.

Tablice

int tab[5];

Tablica jest blokiem ciągłej pamięci. Nie przechowuje informacji o długości. Funkcja przyjmująca tablicę:

void f(int *t, int n) {
    for (int i = 0; i < n; i++) {
        ...
    }
}

Rozmiar musi być przekazany osobno.

Porównanie z C++ i Python – różnice w modelu wykonania i poziomie abstrakcji

C++

C++ rozszerza C o:

  • klasy,
  • dziedziczenie,
  • przeciążanie operatorów,
  • RAII,
  • standardową bibliotekę kontenerów.

Ten sam przykład dynamicznej alokacji:

#include <iostream>

int main() {
    int *p = new int(10);
    delete p;
    return 0;
}

C++ oferuje też std::vector, który eliminuje ręczne zarządzanie pamięcią.

Python

Python to język interpretowany, z automatycznym garbage collection.

Przykład:

x = 5
y = [1, 2, 3]

Brak wskaźników, brak ręcznego free, dynamiczne typowanie.

W C programista zarządza wszystkim jawnie. W Pythonie większość mechanizmów jest ukryta.

Pułapki i częste błędy początkujących

  1. Niezainicjalizowane zmienne lokalne.
  2. Brak sprawdzania wyniku malloc.
  3. Przekroczenie zakresu tablicy.
  4. Błędne rzutowania wskaźników.
  5. Założenie, że int ma zawsze 32 bity.

Dodatkowo:

  • mieszanie pamięci stosu i sterty,
  • zapominanie o const,
  • brak prototypów funkcji.

Zastosowania języka w systemach operacyjnych, embedded i oprogramowaniu niskopoziomowym

C jest używany w:

  • systemach operacyjnych,
  • sterownikach urządzeń,
  • mikrokontrolerach,
  • bibliotekach systemowych,
  • silnikach baz danych,
  • implementacjach kompilatorów.

Daje przewidywalność czasową i kontrolę nad zużyciem pamięci, co jest kluczowe w systemach wbudowanych.

Programowanie w C wymaga dokładności i zrozumienia tego, jak działa maszyna. Nie oferuje ochrony przed błędami logicznymi ani pamięciowymi. W zamian daje pełną kontrolę nad zasobami, przewidywalność i możliwość budowania fundamentów dla innych języków i systemów.