Języki programowania
Składnia i specyfika języków wysokiego poziomu (najczęściej C++, Java, Python lub C#), typy zmiennych, rzutowanie typów, obsługa wyjątków i błędów, systemy wejścia/wyjścia (pliki).
Wstęp: egzamin nie pyta „co to jest zmienna"
Ten dział sprawdza się w dwóch miejscach naraz. W części pisemnej dostajesz krótki listing i pytanie „co wypisze ten program" albo „ile bajtów zajmuje zmienna typu int". W części praktycznej piszesz kod, który musi się skompilować i obsłużyć błąd otwarcia pliku, bo za brak obsługi błędu traci się punkty w kryteriach. Dlatego poniżej wszystko jest podane liczbowo: rozmiary typów, zakresy, kolejność etapów kompilacji i gotowe wzorce kodu, które przepisujesz na egzaminie prawie bez zmian. Językiem wiodącym jest C++ — to on najczęściej pojawia się w arkuszach INF.04 — a tam, gdzie składnia Pythona pomaga zrozumieć różnicę, dokładam krótki odpowiednik.
📋 Spis treści
Języki i powstawanie programu
1
Podział języków programowania
Języki dzieli się na kilka niezależnych sposobów naraz i to jest źródłem większości pomyłek. „Wysokiego poziomu" nie znaczy „interpretowany", a „dynamicznie typowany" nie znaczy „słabo typowany". Każdy podział odpowiada na inne pytanie.
Instrukcje odpowiadają jeden do jednego rozkazom procesora. Kod jest zależny od architektury — program w asemblerze na x86 nie uruchomi się na ARM. Programista sam zarządza rejestrami i pamięcią. Zalety: maksymalna kontrola i wydajność, minimalny rozmiar. Wady: bardzo wolne pisanie i brak przenośności. Przykłady: kod maszynowy (same zera i jedynki) oraz asembler.
Składnia zbliżona do języka naturalnego i matematyki, niezależna od procesora. Jedna instrukcja zamienia się na wiele rozkazów maszynowych. Programista operuje pojęciami typu funkcja, klasa, pętla, a nie rejestrami. Przykłady: C, C++, Java, C#, Python, PHP. Ten sam kod źródłowy da się skompilować lub uruchomić na różnych platformach.
- Kompilowany — cały kod jest tłumaczony przed uruchomieniem na kod maszynowy konkretnej platformy. Powstaje plik wykonywalny (
.exew Windows, plik ELF w Linuksie), który działa bez obecności kompilatora. Błędy składniowe wychodzą przed startem. Najszybsze wykonanie. Przykłady: C, C++, Pascal. - Interpretowany — interpreter czyta i wykonuje kod instrukcja po instrukcji przy każdym uruchomieniu. Nie powstaje samodzielny plik wykonywalny; na docelowym komputerze musi być zainstalowany interpreter. Błąd składniowy w rzadko wykonywanej linii ujawni się dopiero, gdy program do niej dojdzie. Wolniejsze, ale wygodne w testowaniu. Przykłady: Python, PHP, JavaScript.
- Hybrydowy (maszyna wirtualna) — kod źródłowy jest kompilowany do kodu pośredniego, a ten wykonywany przez maszynę wirtualną, która w trakcie działania dokompilowuje gorące fragmenty do kodu maszynowego (kompilacja JIT). Java produkuje bajtkod w plikach
.classuruchamiany przez JVM, C# produkuje kod CIL w zestawie uruchamianym przez CLR platformy .NET. Stąd hasło „napisz raz, uruchom wszędzie".
Statyczne — typ zmiennej jest znany i sprawdzany na etapie kompilacji, deklarujesz go jawnie (C++, Java, C#). Zmienna zadeklarowana jako int do końca życia jest intem. Dynamiczne — typ jest związany z wartością, nie ze zmienną, i ustalany w czasie działania (Python, PHP, JavaScript). Ta sama zmienna może przechowywać kolejno liczbę i napis.
Silne — język nie pozwala mieszać niezgodnych typów bez jawnej konwersji. W Pythonie dodanie liczby do napisu kończy się błędem TypeError, mimo że Python jest dynamiczny. Słabe — język sam wykonuje konwersje, czasem zaskakujące. W PHP i JavaScripcie napis „5" dodany do liczby 5 daje wynik bez błędu.
Zapamiętaj kontrprzykład: Python jest dynamicznie, ale silnie typowany. To rozbija najczęstsze mylne skojarzenie „dynamiczny = słaby".
| Język | Sposób wykonania | Typowanie | Zarządzanie pamięcią | Typowe zastosowanie |
|---|---|---|---|---|
| C++ | kompilowany do kodu maszynowego | statyczne, silne | ręczne (new / delete), bez odśmiecacza | gry, systemy, sterowniki, aplikacje wydajnościowe |
| Java | kompilacja do bajtkodu, wykonanie w JVM (JIT) | statyczne, silne | automatyczne — Garbage Collector | systemy korporacyjne, Android, backend |
| Python | interpretowany (z pośrednim bajtkodem .pyc) | dynamiczne, silne | automatyczne — zliczanie referencji i GC | skrypty, analiza danych, automatyzacja, uczenie maszynowe |
| C# | kompilacja do kodu CIL, wykonanie w CLR (JIT) | statyczne, silne | automatyczne — Garbage Collector | aplikacje Windows, ASP.NET, gry w Unity |
Różnicę w ilości „rusztowania" widać od razu. C++ i Java wymagają deklaracji typów i punktu wejścia, Python nie wymaga niczego.
// ---------- C++ ----------
#include <iostream>
int main() {
int a = 5, b = 3;
std::cout << "Suma: " << a + b << std::endl;
return 0;
}
// ---------- Java ----------
public class Program {
public static void main(String[] args) {
int a = 5, b = 3;
System.out.println("Suma: " + (a + b));
}
}
// ---------- C# ----------
using System;
class Program {
static void Main() {
int a = 5, b = 3;
Console.WriteLine("Suma: " + (a + b));
}
}
# ---------- Python ----------
a = 5
b = 3
print("Suma:", a + b)2
Od kodu źródłowego do pliku wykonywalnego
Kolejność etapów bywa punktowana wprost, a nazwa „konsolidacja" (linkowanie) jest najczęściej mylonym pojęciem w całym dziale. Zapamiętaj cztery kroki i to, jaki plik powstaje po każdym z nich.
- Preprocesor — obsługuje dyrektywy zaczynające się od znaku hash: wstawia treść plików nagłówkowych w miejsce
#include, podstawia makra#define, usuwa komentarze i wycina fragmenty odrzucone przez kompilację warunkową. Wynik: powiększony kod źródłowy (plik.i), nadal czytelny tekst. - Kompilacja — analiza leksykalna, składniowa i semantyczna, a następnie tłumaczenie na kod maszynowy. Tu wychodzą błędy składniowe i błędy typów. Wynik: plik obiektowy (
.olub.obj) — kod maszynowy, ale jeszcze nieuruchamialny, bo brakuje w nim adresów funkcji z innych plików. - Konsolidacja (linkowanie) — konsolidator (linker) łączy wszystkie pliki obiektowe programu z bibliotekami i uzupełnia odwołania do funkcji zewnętrznych. To tutaj wychodzi błąd „undefined reference", gdy funkcja została zadeklarowana, ale nigdzie nie zdefiniowana.
- Plik wykonywalny — gotowy program, uruchamiany bezpośrednio przez system operacyjny. Do jego uruchomienia nie jest potrzebny kompilator.
# Etapy rozbite na osobne polecenia (kompilator g++):
g++ -E main.cpp -o main.i # 1. tylko preprocesor -> tekst
g++ -S main.i -o main.s # (opcjonalnie) kod w asemblerze
g++ -c main.cpp -o main.o # 2. kompilacja -> plik obiektowy
g++ main.o funkcje.o -o program # 3. konsolidacja -> plik wykonywalny
# W praktyce robi się to jednym poleceniem - g++ sam wywoła wszystkie etapy:
g++ main.cpp funkcje.cpp -o program
./program
# Typowe komunikaty i etap, na którym powstaja:
# "expected ';' before '}' token" -> kompilacja (blad skladniowy)
# "undefined reference to 'suma(int)'" -> konsolidacja (brak definicji)
# "No such file or directory: iostream" -> preprocesor (brak naglowka)
Nie ma etapu konsolidacji ani pliku wykonywalnego. Interpreter czyta plik
.py,
tłumaczy go na wewnętrzny bajtkod (cache w plikach
.pyc)
i od razu wykonuje. Program przeniesiony na inny komputer zadziała tylko
tam, gdzie jest zainstalowany interpreter w odpowiedniej wersji.
Kompilator javac
zamienia plik .java
na bajtkod w pliku .class.
Bajtkod nie jest kodem maszynowym żadnego procesora — wykonuje go maszyna
wirtualna JVM, kompilując gorące fragmenty w locie (JIT). W C# analogicznie
powstaje kod CIL wykonywany przez środowisko CLR. Warunkiem uruchomienia
jest zainstalowane środowisko uruchomieniowe.
Skrót do zapamiętania: C++ tłumaczy raz, przed uruchomieniem, na kod procesora. Python tłumaczy za każdym razem, w trakcie działania. Java i C# tłumaczą raz na kod pośredni, a potem maszyna wirtualna dokłada resztę w locie. Stąd wynikają wszystkie różnice w szybkości i przenośności.
3
Typy proste, rozmiary i zakresy
Zakres typu wynika wprost z liczby bitów. Typ ze znakiem o n bitach obejmuje wartości od minus 2 do potęgi (n−1) do 2 do potęgi (n−1) minus jeden — dla 4-bajtowego int daje to od −2 147 483 648 do 2 147 483 647. To wyliczenie warto umieć odtworzyć, bo pytanie o zakres pojawia się regularnie.
| Typ | Rozmiar | Zakres / dokładność | Przykład |
|---|---|---|---|
| bool | 1 bajt | true albo false (1 lub 0) | bool ok = true; |
| char | 1 bajt | −128…127 (ze znakiem), 0…255 (unsigned) | char z = 'A'; |
| short | 2 bajty | −32 768…32 767 | short rok = 2026; |
| int | 4 bajty | −2 147 483 648…2 147 483 647 | int n = 100; |
| unsigned int | 4 bajty | 0…4 294 967 295 | unsigned int i = 0; |
| long long | 8 bajtów | około ±9,22 · 10 do potęgi 18 | long long d = 9e18; |
| float | 4 bajty | około ±3,4 · 10 do potęgi 38, ok. 7 cyfr znaczących | float pi = 3.14f; |
| double | 8 bajtów | około ±1,7 · 10 do potęgi 308, ok. 15–16 cyfr znaczących | double x = 3.14159; |
| std::string | zmienny | typ złożony z biblioteki standardowej, rośnie wraz z tekstem | string s = "tekst"; |
Podane rozmiary to wartości typowe dla 64-bitowych kompilatorów na PC. Standard C++ gwarantuje jedynie relacje (short nie większy niż int, int nie większy niż long) oraz że char ma dokładnie 1 bajt — dlatego w C++ rozmiar typu zależy od platformy i kompilatora i sprawdza się go operatorem sizeof.
W Javie nie ma „zależy od platformy". Specyfikacja języka podaje sztywno: byte 1 bajt (−128…127), short 2 bajty, int 4 bajty, long 8 bajtów, float 4 bajty, double 8 bajtów, char 2 bajty (znak Unicode UTF-16, zakres 0…65 535), boolean — wartość logiczna. Ta gwarancja jest częścią przenośności Javy: ten sam program da identyczne wyniki na każdej maszynie. W C# jest podobnie, przy czym int to alias typu System.Int32.
Pytanie egzaminacyjne brzmi zwykle tak: „ile bajtów zajmuje zmienna typu int" — odpowiedź 4 bajty, czyli 32 bity, jest poprawna dla obu światów. Ale jeżeli w treści pada „char", uważaj: w C++ to 1 bajt, w Javie 2 bajty.
#include <iostream>
#include <climits>
using namespace std;
int main() {
cout << "sizeof(char) = " << sizeof(char) << " B" << endl;
cout << "sizeof(int) = " << sizeof(int) << " B" << endl;
cout << "sizeof(float) = " << sizeof(float) << " B" << endl;
cout << "sizeof(double) = " << sizeof(double) << " B" << endl;
cout << "INT_MIN = " << INT_MIN << ", INT_MAX = " << INT_MAX << endl;
return 0;
}
/* Typowe wyjscie na 64-bitowym PC:
sizeof(char) = 1 B
sizeof(int) = 4 B
sizeof(float) = 4 B
sizeof(double) = 8 B
INT_MIN = -2147483648, INT_MAX = 2147483647
*/
4
Deklaracja, zasięg zmiennych i stałe
Deklaracja to poinformowanie kompilatora, że zmienna o danej nazwie i typie istnieje. Inicjalizacja to nadanie jej pierwszej wartości. Zmienna zadeklarowana lokalnie i niezainicjalizowana zawiera w C++ wartość przypadkową — to źródło błędów, których nie widać na pierwszy rzut oka.
int a; // deklaracja bez inicjalizacji - wartosc SMIECIOWA
int b = 10; // deklaracja z inicjalizacja (kopiujaca)
int c(20); // inicjalizacja nawiasowa
int d{30}; // inicjalizacja klamrowa (od C++11, najbezpieczniejsza)
int e = 0, f = 0; // dwie zmienne w jednej instrukcji
double cena = 19.99;
char ocena = '5'; // pojedynczy znak - apostrofy
string imie = "Anna"; // napis - cudzyslowy
bool zdany = true;
// int 2liczba; // BLAD: nazwa nie moze zaczynac sie od cyfry
// int int; // BLAD: slowo kluczowe nie moze byc nazwa
// int moja-zmienna; // BLAD: myslnik jest operatorem odejmowania
# ---- odpowiednik w Pythonie: typu sie nie podaje ----
# a = 10
# cena = 19.99
# imie = "Anna"
# zdany = True
- Lokalny — zmienna zadeklarowana wewnątrz bloku, czyli między klamrami { a }. Istnieje od miejsca deklaracji do końca bloku, potem jest niszczona. Zmienna z pętli for zadeklarowana w jej nagłówku znika po pętli.
- Globalny — zmienna zadeklarowana poza wszystkimi funkcjami. Widoczna w całym pliku, żyje przez cały czas działania programu, jest automatycznie zerowana. Nadużywanie zmiennych globalnych to zły styl: każda funkcja może je zmienić i trudno wyśledzić, która to zrobiła.
- Statyczny — zmienna lokalna oznaczona słowem
static. Widoczna tylko w swoim bloku (jak lokalna), ale zachowuje wartość między wywołaniami funkcji i jest inicjalizowana tylko raz. Idealna do licznika wywołań. - Przesłanianie — jeśli zmienna lokalna ma tę samą nazwę co globalna, wewnątrz bloku „wygrywa" lokalna. Do globalnej można się wtedy dostać przez operator zakresu, zapisywany jako dwa dwukropki przed nazwą.
#include <iostream>
using namespace std;
int licznikGlobalny = 0; // ZASIEG GLOBALNY
void test() {
int lokalna = 0; // ZASIEG LOKALNY - tworzona od nowa
static int statyczna = 0; // ZASIEG STATYCZNY - tworzona raz
lokalna++;
statyczna++;
licznikGlobalny++;
cout << "lokalna=" << lokalna
<< " statyczna=" << statyczna
<< " globalna=" << licznikGlobalny << endl;
}
int main() {
test();
test();
test();
return 0;
}
/* WYJSCIE:
lokalna=1 statyczna=1 globalna=1
lokalna=1 statyczna=2 globalna=2
lokalna=1 statyczna=3 globalna=3
*/
Zadanie typu „co wypisze program": jeśli widzisz w listingu słowo static przy zmiennej lokalnej, licznik NIE resetuje się przy kolejnym wejściu do funkcji. To dokładnie ta różnica, którą sprawdza powyższy przykład: kolumna „lokalna" stoi w miejscu, kolumna „statyczna" rośnie.
Stała to wartość, której nie wolno zmienić po nadaniu. Próba przypisania kończy się błędem kompilacji, a nie błędem w czasie działania — i o to właśnie chodzi: błąd wychodzi wcześniej.
const double PI = 3.14159; // C++: stala z typem, zalecana
constexpr int MAX = 100; // C++11: wyliczana na etapie kompilacji
#define STAWKA_VAT 0.23 // makro preprocesora - bez typu, styl z C
// PI = 3.0; // BLAD KOMPILACJI: assignment of read-only variable 'PI'
// Java: final double PI = 3.14159;
// C#: const double PI = 3.14159;
// Python: PI = 3.14159 (konwencja WIELKIE_LITERY - jezyk nie chroni)Różnica const kontra #define: const ma typ, respektuje zasięg i widzi go debuger. Makro #define jest tylko tekstowym podstawieniem robionym przez preprocesor przed kompilacją — dlatego w nowym kodzie C++ używa się const i constexpr. W Pythonie prawdziwych stałych nie ma, jest tylko umowa zapisu wielkimi literami.
5
Rzutowanie typów i operatory
Rzutowanie (konwersja typu) bywa niejawne — robi je kompilator sam — albo jawne, wymuszone przez programistę. Konwersja „w górę" (int na double) jest bezpieczna. Konwersja „w dół" (double na int) obcina część ułamkową i to jest najczęstsza pułapka całego działu.
Reguła jest jedna: jeżeli oba argumenty dzielenia są całkowite, to C++ wykonuje dzielenie całkowite i odrzuca resztę. To, że wynik przypisujesz do zmiennej typu double, niczego nie ratuje — konwersja następuje dopiero po obliczeniu, gdy reszta jest już stracona.
#include <iostream>
using namespace std;
int main() {
int a = 7, b = 2;
cout << a / b << endl; // 3 <- dzielenie CALKOWITE
cout << a % b << endl; // 1 <- reszta z dzielenia
double w1 = a / b; // najpierw 7/2 = 3, potem 3.0
cout << w1 << endl; // 3 PULAPKA
double w2 = (double)a / b; // rzutowanie JAWNE (styl C)
cout << w2 << endl; // 3.5 POPRAWNIE
double w3 = static_cast<double>(a) / b; // rzutowanie JAWNE (styl C++)
cout << w3 << endl; // 3.5 POPRAWNIE
double w4 = a / 2.0; // 2.0 jest double -> konwersja
cout << w4 << endl; // 3.5 NIEJAWNA konwersja b
int x = 3.99; // OBCIECIE, nie zaokraglenie
cout << x << endl; // 3
int y = -3.99;
cout << y << endl; // -3 (obciecie w strone zera)
char znak = 65; // kod ASCII 65
cout << znak << endl; // A
int kod = 'A';
cout << kod << endl; // 65
float f = 0.1f;
double d = 0.1;
cout << (f == d) << endl; // 0 (falsz!) - inna precyzja
return 0;
}Wystarczy, że jeden argument jest zmiennoprzecinkowy — wtedy drugi zostaje niejawnie przekształcony i całe działanie idzie na liczbach rzeczywistych. Stąd trzy równoważne poprawki: rzutuj licznik, rzutuj mianownik albo zapisz literał jako 2.0 zamiast 2.
Wykonuje je kompilator bez pytania, gdy typy różnią się w wyrażeniu lub przy przypisaniu. Kierunek promocji: char → short → int → long → float → double. Rzutowanie w tę stronę jest bezstratne. W drugą stronę kompilator też je wykona, ale zwykle wypisze ostrzeżenie o możliwej utracie danych.
Programista wymusza konwersję: zapisem w stylu C przez typ w nawiasie przed wartością albo zapisem w stylu C++ przez static_cast z typem w nawiasach ostrych. W Javie i C# używa się zapisu z nawiasem, w Pythonie funkcji int(), float() i str(). Jawne rzutowanie mówi też czytelnikowi kodu: „wiem, co robię, utrata części ułamkowej jest zamierzona".
| Priorytet | Grupa | Operatory |
|---|---|---|
| 1 (najwyższy) | nawiasy, dostęp | ( ) [ ] . -> |
| 2 | jednoargumentowe | ! ++ -- minus jednoargumentowy, rzutowanie |
| 3 | multiplikatywne | * / % |
| 4 | addytywne | + - |
| 5 | relacyjne | < <= > >= |
| 6 | równościowe | == != |
| 7 | logiczne | && (i), potem || (lub) |
| 8 (najniższy) | przypisania | = += -= *= /= %= |
Kolejność działań arytmetycznych jest ta sama co w matematyce, a nawias zmienia wszystko. Operatory logiczne mają niższy priorytet niż relacyjne, więc zapis a > 5 && a < 10 działa poprawnie bez dodatkowych nawiasów.
int a = 2, b = 3, c = 4;
int w1 = a + b * c; // 2 + 12 = 14 (mnozenie przed dodawaniem)
int w2 = (a + b) * c; // 5 * 4 = 20 (nawias zmienia kolejnosc)
int w3 = 10 % 3 + 1; // 1 + 1 = 2 (modulo ma priorytet jak *)
int w4 = 17 / 5 * 5; // 3 * 5 = 15 (nie 17! dzielenie calkowite)
// Preinkrementacja kontra postinkrementacja - ulubione pytanie:
int i = 5;
int x = ++i; // NAJPIERW zwieksz, POTEM przypisz -> i = 6, x = 6
int j = 5;
int y = j++; // NAJPIERW przypisz, POTEM zwieksz -> j = 6, y = 5
// Skrocone przypisania:
int n = 10;
n += 5; // n = n + 5 -> 15
n *= 2; // n = n * 2 -> 30
n %= 7; // n = n % 7 -> 2
// Skrocone wartosciowanie (short-circuit):
// jesli pierwszy warunek falszywy, drugi NIE jest sprawdzany
if (b != 0 && a / b > 1) { /* bezpieczne - nie bedzie dzielenia przez zero */ }
6
Tablice, wskaźniki i przekazywanie parametrów
Tablica to ciąg elementów tego samego typu leżących obok siebie w pamięci. Numeracja zaczyna się od zera, więc tablica o rozmiarze n ma ostatni indeks n−1. Odwołanie do indeksu n jest wyjściem poza tablicę i w C++ nie jest sprawdzane — program po prostu czyta cudzy fragment pamięci.
#include <iostream>
#include <vector>
using namespace std;
int main() {
// --- TABLICA STATYCZNA: rozmiar znany podczas kompilacji, pamiec na stosie
int oceny[5] = {5, 4, 3, 5, 2};
cout << oceny[0] << endl; // 5 - pierwszy element ma indeks 0
cout << oceny[4] << endl; // 2 - ostatni ma indeks 4, nie 5!
// oceny[5] -> WYJSCIE POZA TABLICE, brak kontroli, nieokreslone zachowanie
int suma = 0;
for (int i = 0; i < 5; i++) suma += oceny[i];
cout << "Srednia: " << (double)suma / 5 << endl; // 3.8
// --- TABLICA DWUWYMIAROWA
int macierz[2][3] = {{1, 2, 3}, {4, 5, 6}};
cout << macierz[1][2] << endl; // 6
// --- TABLICA DYNAMICZNA: rozmiar podany w czasie dzialania, pamiec na stercie
int n;
cout << "Podaj rozmiar: ";
cin >> n;
int* tab = new int[n]; // przydzial pamieci
for (int i = 0; i < n; i++) tab[i] = i * i;
cout << tab[n - 1] << endl;
delete[] tab; // ZWOLNIENIE - inaczej wyciek pamieci
tab = nullptr;
// --- WEKTOR: tablica dynamiczna z biblioteki standardowej, sama sie sprzata
vector<int> v = {10, 20, 30};
v.push_back(40);
cout << v.size() << endl; // 4
cout << v.at(10) << endl; // at() rzuca wyjatek out_of_range - kontrola!
return 0;
}
Zmienna przechowująca adres innej zmiennej. Deklarujesz gwiazdką przy typie, adres pobierasz ampersandem, a wartość spod adresu odczytujesz gwiazdką (dereferencja). Wskaźnik można przestawić na inny obiekt i można mu przypisać nullptr, czyli „nie wskazuje na nic". Wskaźniki są konieczne przy pamięci dynamicznej i przy strukturach takich jak listy czy drzewa.
Alias, czyli drugie imię istniejącej zmiennej. Deklarujesz ampersandem przy typie i musisz ją zainicjalizować od razu. Referencji nie da się później przestawić na inną zmienną ani ustawić na „nic". Nie wymaga dereferencji — używasz jej jak zwykłej zmiennej. Najczęstsze zastosowanie: przekazywanie parametrów do funkcji.
int liczba = 42;
int* wsk = &liczba; // wsk przechowuje ADRES zmiennej liczba
int& ref = liczba; // ref to DRUGA NAZWA tej samej zmiennej
cout << liczba << endl; // 42
cout << *wsk << endl; // 42 - dereferencja: wartosc spod adresu
cout << ref << endl; // 42 - alias, bez zadnych gwiazdek
cout << wsk << endl; // np. 0x7ffd3c... - sam adres
*wsk = 100; // zapis przez wskaznik
cout << liczba << endl; // 100 - zmienila sie oryginalna zmienna
ref = 7; // zapis przez referencje
cout << liczba << endl; // 7
int inna = 1;
wsk = &inna; // OK - wskaznik mozna przestawic
// int& ref2; // BLAD - referencja musi byc zainicjalizowana
To najpewniejsze pytanie z tego działu. Przez wartość funkcja dostaje kopię i zmiany nie wychodzą na zewnątrz. Przez referencję funkcja pracuje na oryginale i zmiany są widoczne w funkcji wywołującej.
#include <iostream>
using namespace std;
void przezWartosc(int x) { // x to KOPIA argumentu
x = x * 2;
cout << " wewnatrz przezWartosc: x = " << x << endl;
}
void przezReferencje(int& x) { // x to ALIAS argumentu (ampersand przy typie)
x = x * 2;
cout << " wewnatrz przezReferencje: x = " << x << endl;
}
void przezWskaznik(int* x) { // x to ADRES argumentu
*x = *x * 2;
cout << " wewnatrz przezWskaznik: *x = " << *x << endl;
}
int main() {
int a = 5;
cout << "przed: a = " << a << endl;
przezWartosc(a);
cout << "po przezWartosc: a = " << a << endl;
int b = 5;
przezReferencje(b);
cout << "po przezReferencje: b = " << b << endl;
int c = 5;
przezWskaznik(&c); // przekazujemy ADRES
cout << "po przezWskaznik: c = " << c << endl;
return 0;
}
/* WYJSCIE:
przed: a = 5
wewnatrz przezWartosc: x = 10
po przezWartosc: a = 5 <-- ORYGINAL NIEZMIENIONY
wewnatrz przezReferencje: x = 10
po przezReferencje: b = 10 <-- ORYGINAL ZMIENIONY
wewnatrz przezWskaznik: *x = 10
po przezWskaznik: c = 10 <-- ORYGINAL ZMIENIONY
*/Kiedy czego używać: przez wartość — dla małych typów prostych, gdy funkcja ma tylko odczytać dane. Przez referencję — gdy funkcja ma zmienić argument albo gdy obiekt jest duży i kopiowanie byłoby kosztowne (wtedy dodaje się const, żeby zabronić modyfikacji). W Javie i C# typy proste zawsze idą przez wartość, a obiekty przez wartość referencji — dlatego zmiana pola obiektu w metodzie jest widoczna na zewnątrz, ale podstawienie nowego obiektu już nie.
7
Obsługa wyjątków
Wyjątek to sygnał, że wystąpiła sytuacja, z którą dany fragment kodu nie potrafi sobie poradzić: dzielenie przez zero, brak pliku, zła konwersja, wyjście poza zakres. Zamiast zwracać kod błędu, który wywołujący może zignorować, funkcja rzuca wyjątek — a ten musi zostać gdzieś przechwycony, inaczej program się kończy.
try— blok, w którym może pojawić się problem. Umieszczasz w nim kod „normalnego przebiegu", bez rozpraszających sprawdzeń.catch— blok przechwytujący wyjątek określonego typu. Bloków catch może być wiele; sprawdzane są po kolei od góry, więc typy najbardziej szczegółowe muszą stać przed ogólnymi.throw— rzucenie wyjątku. Wykonywanie bieżącej funkcji natychmiast się przerywa, a sterowanie wędruje w górę stosu wywołań do pierwszego pasującego catch.finally— blok wykonywany zawsze: i gdy wyjątku nie było, i gdy był. Służy do sprzątania (zamknięcie pliku, zwolnienie zasobu). Istnieje w Javie, C# i Pythonie. W C++ nie ma słowa finally — jego rolę pełni mechanizm destruktorów, czyli technika RAII: obiekt sam zwalnia zasób, gdy kończy się jego zasięg.
Wyjątki to klasy dziedziczące po wspólnym przodku, dzięki czemu jeden catch może przechwycić całą rodzinę błędów. W C++ korzeniem jest std::exception z nagłówka <stdexcept>, w Javie klasa Exception, w Pythonie klasa Exception, w C# klasa System.Exception.
std::exception (korzen hierarchii w C++)
|
+-- std::logic_error blad logiki, dalo sie go przewidziec
| +-- std::invalid_argument zly argument
| +-- std::out_of_range indeks poza zakresem (np. vector::at)
| +-- std::domain_error
|
+-- std::runtime_error blad wykrywalny dopiero w czasie dzialania
+-- std::overflow_error przepelnienie
+-- std::range_error
// Kolejnosc blokow catch MA ZNACZENIE:
// catch (const std::exception& e) <- gdyby stal pierwszy, przechwycilby WSZYSTKO
// catch (const std::out_of_range& e) <- ten nigdy by sie nie wykonal#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
// WLASNY wyjatek - dziedziczy po std::runtime_error
class BladDzielenia : public runtime_error {
public:
BladDzielenia(const string& komunikat)
: runtime_error(komunikat) {}
};
double podziel(double a, double b) {
if (b == 0) {
throw BladDzielenia("Proba dzielenia przez zero!"); // RZUCENIE
}
return a / b;
}
int sprawdzWiek(int wiek) {
if (wiek < 0 || wiek > 120) {
throw invalid_argument("Wiek poza zakresem 0-120");
}
return wiek;
}
int main() {
try {
cout << podziel(10, 2) << endl; // 5
cout << sprawdzWiek(20) << endl; // 20
cout << podziel(10, 0) << endl; // tu leci wyjatek
cout << "Ta linia sie NIE wykona" << endl;
}
catch (const BladDzielenia& e) { // najpierw typ SZCZEGOLOWY
cout << "Blad dzielenia: " << e.what() << endl;
}
catch (const invalid_argument& e) {
cout << "Zly argument: " << e.what() << endl;
}
catch (const exception& e) { // na koncu typ OGOLNY
cout << "Inny blad: " << e.what() << endl;
}
catch (...) { // lapie absolutnie wszystko
cout << "Nieznany blad" << endl;
}
cout << "Program dziala dalej." << endl;
return 0;
}
/* WYJSCIE:
5
20
Blad dzielenia: Proba dzielenia przez zero!
Program dziala dalej.
*/
# ---- Ten sam mechanizm w Pythonie, z blokami else i finally ----
class BladDzielenia(Exception):
pass
def podziel(a, b):
if b == 0:
raise BladDzielenia("Proba dzielenia przez zero!")
return a / b
try:
wynik = podziel(10, 0)
except BladDzielenia as e:
print("Blad dzielenia:", e)
except ValueError as e:
print("Zla wartosc:", e)
else:
print("Wynik:", wynik) # tylko gdy NIE bylo wyjatku
finally:
print("Ten blok wykona sie ZAWSZE")
// ---- Java / C#: schemat identyczny ----
// try { ... }
// catch (ArithmeticException e) { System.out.println(e.getMessage()); }
// finally { plik.close(); }
Po co wyjątki zamiast kodów błędów?
Kod błędu zwracany przez funkcję (np. −1) można po prostu zignorować i program
poleci dalej z błędnymi danymi. Wyjątku zignorować się nie da — nieprzechwycony
kończy program. Poza tym kod błędu miesza się z wynikiem funkcji (co, jeśli −1
jest poprawnym wynikiem?) i zaśmieca każdą linię sprawdzeniami. Wyjątki
oddzielają ścieżkę normalną od ścieżki błędu: w bloku try
czytasz sam algorytm, a obsługę problemów masz zebraną niżej. Dodatkowo wyjątek
sam przenosi się przez wiele poziomów wywołań, więc nie trzeba przekazywać
kodu błędu ręcznie z funkcji do funkcji.
8
Wejście/wyjście, pliki i biblioteki standardowe
W zadaniu praktycznym schemat pracy z plikiem jest zawsze taki sam: otwórz → sprawdź, czy się udało → przetwarzaj → zamknij. Punkt drugi to ten, który uczniowie pomijają najczęściej, a bez niego program próbuje czytać z nieistniejącego pliku i kończy się cichym błędem.
W C++ wejście/wyjście opiera się na strumieniach. Konsolę obsługuje nagłówek <iostream> (obiekty cin, cout, cerr), a pliki nagłówek <fstream>.
| Klasa / tryb | Znaczenie | Uwaga |
|---|---|---|
| ifstream | strumień wejściowy — odczyt z pliku | domyślny tryb ios::in |
| ofstream | strumień wyjściowy — zapis do pliku | tworzy plik, jeśli nie istnieje |
| fstream | strumień dwukierunkowy | tryb podajesz jawnie |
| ios::out | zapis od początku | kasuje dotychczasową treść |
| ios::app | dopisywanie na końcu | zachowuje istniejącą treść |
| ios::binary | tryb binarny | bez tłumaczenia znaków końca linii |
W Pythonie odpowiedniki trybów to litery: „r" odczyt, „w" zapis z wyczyszczeniem pliku, „a" dopisywanie, „rb"/„wb" tryb binarny.
#include <iostream>
#include <string>
using namespace std;
int main() {
int wiek;
string imie, calaLinia;
cout << "Podaj imie: ";
cin >> imie; // czyta do pierwszego BIALEGO ZNAKU (spacji)
cout << "Podaj wiek: ";
cin >> wiek;
cin.ignore(); // usuwa znak konca linii zostawiony przez cin
cout << "Podaj adres: ";
getline(cin, calaLinia); // czyta CALA linie razem ze spacjami
cout << "Witaj, " << imie << ", masz " << wiek << " lat." << endl;
cerr << "To trafia na strumien bledow, nie na standardowe wyjscie" << endl;
// Kontrola poprawnosci danych wejsciowych:
if (cin.fail()) {
cout << "Podano dane w zlym formacie!" << endl;
cin.clear(); // kasuje flage bledu strumienia
}
return 0;
}
# ---- Python ----
# imie = input("Podaj imie: ")
# wiek = int(input("Podaj wiek: ")) # input zwraca TEKST, trzeba rzutowac
# print(f-string albo print("Witaj,", imie))
To jest gotowy szkielet zadania praktycznego. Sprawdzenie is_open() i zamknięcie pliku to punktowane kryteria — nie pomijaj ich, nawet jeśli program bez nich zadziała.
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ifstream plik("dane.txt"); // 1. OTWARCIE do odczytu
if (!plik.is_open()) { // 2. SPRAWDZENIE, czy sie udalo
cerr << "Blad: nie mozna otworzyc pliku dane.txt" << endl;
return 1; // kod wyjscia rozny od zera = blad
}
string linia;
int numer = 0;
while (getline(plik, linia)) { // 3. ODCZYT linia po linii
numer++; // getline zwraca falsz na koncu pliku
cout << numer << ": " << linia << endl;
}
plik.close(); // 4. ZAMKNIECIE - zwolnienie zasobu
cout << "Wczytano linii: " << numer << endl;
return 0;
}
/* Wariant z wyjatkiem zamiast kodu powrotu:
ifstream plik("dane.txt");
if (!plik) throw runtime_error("Nie mozna otworzyc pliku dane.txt");
*/#include <fstream>
#include <iostream>
#include <string>
using namespace std;
int main() {
// --- ZAPIS: tryb domyslny KASUJE dotychczasowa zawartosc pliku
ofstream wy("wyniki.txt");
if (!wy) {
cerr << "Nie mozna utworzyc pliku wyniki.txt" << endl;
return 1;
}
wy << "Jan Kowalski;5" << endl;
wy << "Anna Nowak;4" << endl;
wy.close();
// --- DOPISANIE na koncu istniejacego pliku
ofstream dopisz("wyniki.txt", ios::app);
dopisz << "Piotr Zielinski;3" << endl;
dopisz.close();
// --- ODCZYT liczb do konca pliku
ifstream we("liczby.txt");
if (!we.is_open()) return 1;
int x, suma = 0, ile = 0;
while (we >> x) { // petla konczy sie na koncu pliku lub blednych danych
suma += x;
ile++;
}
we.close();
if (ile > 0) cout << "Srednia: " << (double)suma / ile << endl;
return 0;
}
# ---- Python: konstrukcja with sama zamyka plik, takze przy wyjatku ----
# try:
# with open("dane.txt", "r", encoding="utf-8") as f:
# for numer, linia in enumerate(f, start=1):
# print(numer, linia.strip())
# except FileNotFoundError:
# print("Nie znaleziono pliku dane.txt")
Biblioteka standardowa to zestaw gotowych typów i funkcji dostarczanych razem z językiem. Nie trzeba jej instalować — wystarczy dołączyć nagłówek dyrektywą include (C++), zaimportować pakiet (import w Javie i Pythonie) albo przestrzeń nazw (using w C#).
- <iostream> — cin, cout, cerr, endl
- <fstream> — ifstream, ofstream, fstream
- <string> — typ string, metody length, substr, find
- <vector> — tablica dynamiczna, metody push_back, size, at
- <cmath> — sqrt, pow, abs, sin, cos, round
- <algorithm> — sort, find, max_element, reverse
- <stdexcept> — klasy wyjątków standardowych
- <climits> — stałe INT_MAX, INT_MIN i pokrewne
- Java — pakiet java.lang (dostępny bez importu), java.util (kolekcje, Scanner), java.io (pliki), java.time (daty)
- C# — przestrzenie System, System.IO, System.Collections.Generic, System.Linq
- Python — moduły math, os, sys, random, datetime, json, re; instalowane biblioteki zewnętrzne dokłada menedżer pip
Uwaga na zapis using namespace std w C++. Skraca kod, bo pozwala pisać cout zamiast std::cout, ale w większych projektach uchodzi za zły styl, bo wciąga do programu wszystkie nazwy z przestrzeni standardowej.
- Dzielenie dwóch intów zapisane do zmiennej double. Zapis double w = 7 / 2; daje 3, a nie 3.5. Konwersja następuje po obliczeniu. Ratunek: rzutuj jeden z argumentów albo zapisz literał jako 2.0.
- Mylenie „dynamicznie typowany" ze „słabo typowany". Python jest dynamiczny i jednocześnie silnie typowany — dodanie liczby do napisu daje w nim błąd, a nie automatyczną konwersję.
- Rzutowanie double na int jako zaokrąglanie. To obcięcie części ułamkowej w stronę zera: 3.99 daje 3, a −3.99 daje −3. Do zaokrąglania służy funkcja round.
- Gubienie ostatniego elementu tablicy. Tablica o rozmiarze n ma indeksy od 0 do n−1. Pętla z warunkiem i <= n wychodzi poza tablicę, a C++ tego nie zgłosi.
- Pominięcie sprawdzenia is_open() po otwarciu pliku. W zadaniu praktycznym to osobne kryterium oceny. Sam close() bez wcześniejszej kontroli otwarcia nie wystarczy.
- Ustawienie catch dla typu ogólnego przed szczegółowym. Bloki catch sprawdzane są po kolei, więc catch na std::exception postawiony pierwszy przechwyci wszystko i pozostałe nigdy się nie wykonają.
- Szukanie słowa finally w C++. Go tam nie ma — sprzątaniem zajmują się destruktory (RAII). finally występuje w Javie, C# i Pythonie.
- Zapomniane delete[] po new[]. Każdemu przydziałowi pamięci dynamicznej musi odpowiadać zwolnienie, inaczej powstaje wyciek pamięci. Java, C# i Python robią to za programistę odśmiecaczem, C++ nie.
- Mylenie preinkrementacji z postinkrementacją. W zapisie x = ++i zmienna i rośnie przed przypisaniem, w zapisie y = j++ rośnie po nim — wartości x i y różnią się o jeden.
Typów i zakresów nie da się nauczyć przez czytanie. Przepisz przykład z dzieleniem 7 przez 2, uruchom go i sam zobacz, kiedy wychodzi 3, a kiedy 3.5 — a potem sprawdź się na pytaniach z tego działu.