Przejdź do treści

Wczytywanie, wypisywanie, zmienne

Część programistyczna: Zmienne i wczytywanie

W części programistycznej wprowadzimy do naszych programów elementy interaktywne.

Filmik instruktażowy:

W poprzedniej lekcji nauczyliśmy się wypisywać proste komunikaty oraz – w ostatnim przykładzie – liczby całkowite. Aby program był w pełni interaktywny, musi być możliwe wczytywanie danych wprowadzonych przez użytkownika. Dane te trzeba w jakiś sposób przechowywać. Służą do tego zmienne.

Zmienna odpowiada jakiemuś miejscu w pamięci komputera, w którym można przechowywać określone dane. Najprostszym typem zmiennych są zmienne całkowite (przechowujące liczby całkowite). Żeby móc korzystać ze zmiennej, musimy ją w programie zadeklarować, czyli poinformować kompilator o chęci jej użycia. Deklaracja zmiennej całkowitej wygląda tak:

int nazwa_zmiennej;

Słowo int informuje kompilator, że zmienna jest typu całkowitego (ang. integer). Natomiast dobór nazwy_zmiennej należy do programisty. Inne typy zmiennych będziemy wprowadzać w kolejnych lekcjach.

Gdy zadeklarujemy zmienną, możemy wczytać coś do tej zmiennej – w przypadku zmiennej całkowitej będzie to oczywiście liczba całkowita. Wczytywanie wykonujemy podobnie jak wypisywanie, tylko używając słówka cin zamiast cout i odwrotnych dzióbków (>> zamiast <<). W poniższym programie wczytujemy jedną liczbę całkowitą i po prostu ją wypisujemy:

#include <iostream>
using namespace std;

int main() {
    int a;
    cin >> a;
    cout << "Wczytana liczba to: " << a << endl;
}

Jeżeli skompilujesz i uruchomisz ten program, to "zatrzyma się" on, oczekując, aż podasz mu (wpiszesz na klawiaturze) jakąś liczbę całkowitą, np. 123. Kiedy wpiszesz liczbę i naciśniesz Enter, program wypisze liczbę wraz z komunikatem i zakończy działanie. Zauważ, że w przeciwieństwie do napisów-komunikatów, nazwy zmiennej nie umieszczamy w cudzysłowach.

W kolejnym programie wczytujemy i wypisujemy dwie liczby. Zauważ, że przy wypisywaniu musimy je jakoś rozdzielić, np. poprzez spację lub endl. Natomiast program da sobie radę z ich wczytaniem niezależnie od tego, czy zostaną podane w tym samym wierszu czy w różnych wierszach, a także niezależnie od dodatkowych spacji wprowadzonych przez użytkownika.

#include <iostream>
using namespace std;

int main() {
    int a;
    int b;
    cin >> a >> b;
    cout << "Wczytane liczby to: " << a << " " << b << endl;
}

Po wczytaniu zmiennych całkowitych możemy traktować je w programie tak jak liczby. Poniższy program wczytuje długości boków prostokąta i oblicza pole i obwód tego prostokąta. Zauważ, że jeśli używamy kilku zmiennych tego samego typu, możemy umieścić je, po przecinku, w jednej deklaracji:

#include <iostream>
using namespace std;

int main() {
    int a, b;
    cin >> a >> b;
    cout << "Pole: " << a * b
         << " obwod: " << 2 * (a + b) << endl;
}

Przykładowo, wynikiem programu dla danych a=2, b=3 jest:

Pole: 6 obwod: 10

Spośród standardowych działań dostępne jest dodawanie +, odejmowanie - i mnożenie *. W przypadku liczb całkowitych możemy też korzystać z dzielenia z resztą, przy czym / oznacza iloraz, a % resztę z dzielenia:

#include <iostream>
using namespace std;

int main() {
    int a, b;
    cin >> a >> b;
    cout << a << " / " << b << " = " << a / b << " r. " << a % b << endl;
}

Wynikiem tego programu dla wejścia: a=14, b=5 jest:

14 / 5 = 2 r. 4

Uwaga: podobnie jak w życiu, w C++ nie można dzielić przez 0. Próba wykonania takiego dzielenia (czy to za pomocą /, czy %) zakończy się tzw. błędem wykonania. Więcej o takich błędach dowiesz się w lekcji 7.

Kolejność wykonywania działań w C++ jest taka sama jak w matematyce. Dodawanie i odejmowanie mają taką samą ważność, tak więc działania te są wykonywane od lewej do prawej. Mnożenie i oba typy dzielenia również mają taką samą ważność, więc także są wykonywane od lewej do prawej. Natomiast mnożenie i oba typy dzielenia są wykonywane przed dodawaniem i odejmowaniem. Jeśli chcemy uzyskać inną kolejność wykonywania działań, możemy stosować nawiasy. C++ dopuszcza w wyrażeniach tylko nawiasy okrągłe. Przykładowo, program:

#include <iostream>
using namespace std;

int main() {
    cout << 1 + 2 * 3 << endl;
    cout << (1 + 2) * 3 << endl;
}

wypisuje liczby 7 i 9.

Styl programów

Należy wiedzieć o kilku ograniczeniach dotyczących nazw zmiennych. Nazwa zmiennej może składać się z małych i wielkich liter, cyfr oraz znaku podkreślenia _, przy czym nie może zaczynać się od cyfry. W przeciwieństwie do niektórych języków programowania, w nazwach zmiennych rozróżniane są małe i wielkie litery – właściwie w nazwach zmiennych polecamy w ogóle nie używać wielkich liter. Deklaracja zmiennej może zostać umieszczona w programie w dowolnym miejscu przed miejscem, w którym chcemy ze zmiennej skorzystać (czyli np. wczytać lub wypisać jej wartość). Nazwy zmiennych nie mogą się powtarzać (wyjątki od tego ostatniego stwierdzenia przedstawimy później).

Poniższy program oblicza pole i obwód prostokąta dokładnie tak samo jak poprzedni. Jest on jednak istotnie mniej czytelny.

#include <iostream>
using namespace std;

int main() {
    int pierwszy_bok,Boknr2;
cin >> pierwszy_bok >> Boknr2;
  cout << "Pole: " << pierwszy_bok*Boknr2
<< " obwod: " << 2*(pierwszy_bok+Boknr2) << endl;
}

Dobrze jednak dbać o to, by Twoje programy wyglądały jednolicie – wtedy za kilka dni łatwiej będzie Ci do nich powrócić. W tym kursie przyjęliśmy kilka powszechnie używanych konwencji, które pomagają zwiększyć czytelność kodu źródłowego, m.in.:

  • każdy operator matematyczny (dodawanie, odejmowanie itp.) jest otoczony z obu stron pojedynczymi spacjami,
  • po każdym przecinku jest spacja,
  • na końcach wierszy nie ma dodatkowych spacji,
  • każdy wiersz funkcji main() jest wcięty w prawo na taki sam odstęp (to pozwala wyróżnić w kodzie zawartość tej głównej funkcji).

Więcej tego typu konwencji pojawi się przy poznawaniu kolejnych elementów języka C++, jednak zazwyczaj nie będziemy o nich mówić wprost – po prostu będziemy je konsekwentnie stosować.

Teraz powiemy jeszcze tylko o jednym ważnym drobiazgu – o komentarzach. Komentarze są to fragmenty kodu źródłowego, które kompilator zupełnie pomija. Ich celem jest zazwyczaj objaśnianie pewnych fragmentów kodu (dla siebie lub dla innych czytelników kodu) albo tymczasowe usuwanie (wykomentowywanie) pewnych fragmentów kodu, których w danym momencie nie chcemy jeszcze na trwałe usuwać, ale chcemy, żeby nie były one aktywne. W C++ są dwa typy komentarzy, które wyglądają tak:

#include <iostream>
using namespace std;

int main() {
    int a, b; // kompilator zignoruje wszystko do konca wiersza
    cin >> a >> b; /* a taki komentarz ...
    moze zajmowac wiele wierszy, pod warunkiem, ze
    zamknie sie znakami: */
    cout << "Iloraz: " << a / b << " reszta: " << a % b << endl;
} //tu nie musi byc spacji, ale ze spacja ladnie wyglada

Im dłuższe programy, tym częściej będziemy stosować komentarze.

Zakres typu

Typ int pozwala przechowywać tylko liczby całkowite z ograniczonego zakresu: od −2147483648 do 2147483647, czyli mniej więcej od minus dwóch miliardów do plus dwóch miliardów. Jest to tzw. zakres typu. Próba umieszczenia w zmiennej tego typu liczby spoza zakresu (np. poprzez wczytanie czy w wyniku obliczeń) spowoduje błąd przekroczenia zakresu. Wówczas wynik będzie niepoprawny.

Ma to szczególne znaczenie, gdy w programie dodajemy dosyć duże liczby lub wykonujemy mnożenia. Przykładowo, jeśli uruchomimy program obliczający pole prostokąta i jako długości boków podamy 50000, otrzymamy kompletnie błędne pole prostokąta:

Pole: -1794967296 obwod: 200000

Dla wygody programisty, w języku C++ wprowadzono kilka typów całkowitych, różniących się zakresami. Poniżej umieszczamy listę tych typów. Na tym etapie nie musisz ich próbować zapamiętać – w początkowych lekcjach typ int będzie dla nas w zupełności wystarczający.

typ zakres
short (pełna nazwa: short int) \([−32768,32767]\)
int, long (pełna nazwa: long int) \([−2147483648,2147483647]\)
long long (pełna nazwa: long long int) mniej więcej \([−10^{19},10^{19}]\)
unsigned short (pełna nazwa: unsigned short int) \([0,65536]\)
unsigned int, unsigned long (pełna nazwa: unsigned long int) \([0,4294967296]\)
unsigned long long (pełna nazwa: unsigned long long int) mniej więcej \([0,2\cdot10^{19}]\)

Więcej na temat tego, skąd się wzięły takie właśnie typy i ich zakresy, znajdziesz w komentarzu do lekcji.

Uwaga: Są inne kompilatory języka C++ (z których w tym kursie nie będziemy korzystać), w których typ int ma tylko zakres typu short int. Aby mieć absolutną pewność co do zakresu typu, można zamiast typu int używać typu long int.

Komentarz: Bit i bajt

Dowiedzieliśmy się już, że typy całkowite nie reprezentują wszystkich liczb całkowitych. Więcej, liczby te mogą być reprezentowane przez różne typy o specyficznych zakresach reprezentacji. Dlaczego tak jest?

Do zapisywania liczb w powszechnie używanym układzie dziesiętnym używamy dziesięciu cyfr. W komputerze, z przyczyn technologicznych, do zapisywania informacji używamy dwóch cyfr (znaków): 0 (zera) i 1 (jedynki). Zauważmy, że 1 i 0 możemy interpretować odpowiednio jako jest i nie ma, prawda i fałsz, białe i czarne lub włączony i wyłączony. Mając tylko dwie wartości, można przekazać elementarną informację o stanie pewnego obiektu. Przyjęto, że najmniejszą jednostką informacji jest bit który może przyjmować jedną z dwóch wartości.

W komputerze liczby całkowite są reprezentowane w postaci ciągów bitów, przy czym dla ustalonego języka programowania jest też ustalona długość takiego ciągu. Może być czasem kilka różnych ustalonych długości. Na przykład w języku C++ mamy kilka różnych typów dla liczb całkowitych, tj. short int, long int i long long int oraz wersje tych typów dla liczb nieujemnych (z przedrostkiem unsigned).

Reprezentacja liczb całkowitych nieujemnych

Najpierw opiszemy, w jaki sposób są reprezentowane liczby nieujemne. Niech długością reprezentacji będzie \(k\). Będziemy mieli zatem bity jak poniżej (tak zapisujemy też liczby w układzie dziesiętnym – najbardziej znacząca cyfra jest z lewej strony, a najmniej znacząca z prawej strony):

\(b_{k−1}b_{k−2}b_{k−3}…b_3b_2b_1b_0\),

a reprezentowana liczba będzie miała wartość:

\(b_0⋅2^0+b_1⋅2^1+b_2⋅2^2+…+b_{k−2}⋅2^{k−2}+b_{k−1}⋅2^{k−1}\).

Najmniejszą z reprezentowanych liczb jest \(0\) (same bity 0), a największą \(2^{k−1}\) (same bity \(1\)). Przedział \([0,2^{k−1}]\) (lub \([0,2^k)\)) nazywa się zakresem reprezentacji. Typowe wartości \(k\) to 8, 16, 32, 64. Zapiszmy w tabelce odpowiadające im zakresy:

k 8 16 32 64
zakres 256 65536 \(≈4⋅10^9\) \(10^{19}\)

Długość 8 jest szczególna, jako rozsądnie możliwie najkrótsza, i nazywa się bajt (ang. byte). Jednak reprezentacja o długości 8 jest stosowana rzadko. Najczęściej długością reprezentacji jest 16 (dwubajtowy typ short int), 32 (czterobajtowy typ long int) lub nawet 64 (ośmiobajtowy typ long long int).

Operacje +, −, ∗ i / na liczbach całkowitych zapisanych dwójkowo wykonuje się według prostych algorytmów działań pisemnych znanych ze szkoły (czasem trochę przyśpieszonych przez zastosowanie sprytnych pomysłów). Przykładowo, dodawanie 100+86 zapisane dwójkowo wygląda tak (zauważ, że w przypadku dodawania dwóch jedynek powstaje cyfra – tj. bit – przeniesienia):

7 6 5 4 3 2 1 0 numer bitu
0 1 1 0 0 1 0 0 (100=26+25+22)
+ 0 1 0 1 0 1 1 0 (86=26+24+22+21)
1 0 1 1 1 0 1 0 (186=27+25+24+23+21)

Należy jednak pamiętać, że wynik może być nieokreślony (dzielenie przez 0), ale może także wyjść poza zakres (wtedy też jest w pewnym sensie nieokreślony). Gdy w arytmetyce jednobajtowej wykonamy dodawanie 129+129, to otrzymamy:

8 7 6 5 4 3 2 1 0 numer bitu
1 0 0 0 0 0 0 1 (129)
+ 1 0 0 0 0 0 0 1 (129)
1 0 0 0 0 0 0 1 0 (258)

czyli wynik wychodzący poza zakres. Objawia się to bitem na pozycji 8 równym 1.

Jeśli interesuje Cię, jak komputer radzi sobie z takimi sytuacjami, to jest to tak, że bity niemieszczące się w zakresie zostają utracone – czyli wynikiem powyższego działania w typie jednobajtowym byłoby po prostu 2. Odpowiada to obliczeniu reszty z dzielenia wyniku przez \(2^k\).

Reprezentacja liczb ujemnych

Liczby całkowite można reprezentować, wyróżniając jakiś bit, na przykład pierwszy od lewej strony w słowie, i traktując go jako znak liczby. Gdy bit ten równa się 0, liczba jest nieujemna, gdy równa się 1, liczba jest ujemna. Taki rodzaj reprezentacji nazywa się znak-moduł. Ta reprezentacja wyszła zupełnie z użycia, bowiem operacje arytmetyczne realizuje się w niej niewygodnie. Inna reprezentacja, rozpowszechniona dzisiaj, nazywa się uzupełnieniową. Polega ona na traktowaniu pierwszej od lewej pozycji jako \(−2^{k−1}\). Tak więc zapis składający się z bitów \(b_{k−1}b_{k−2}b_{k−3}…b_3b_2b_1b_0\), reprezentuje liczbę całkowitą

\(b_0⋅2^0+b_1⋅2^1+b_2⋅2^2+…+b_{k−2}⋅2^{k−2}-b_{k−1}⋅2^{k−1}\).

Liczba całkowita w reprezentacji uzupełnieniowej należy do zakresu od \(−2^{k−1}\) (zapis: jedynka i same zera) do \(2^{k−1}−1\) (zapis: zero i same jedynki). Np. dla 16-bitowego typu short int zakresem jest [−32768,32767], a dla 32-bitowego typu long int: \([−2147483648,2147483647]\).

W reprezentacji uzupełnieniowej dodawanie i odejmowanie wykonuje się tak jak dla liczb nieujemnych, tylko zapominamy o ostatnim przeniesieniu. Na przykład dla \(k=8\) wykonajmy dodawanie \(−64+64\):

-128 64 32 16 8 4 2 1 wartość bitu
7 6 5 4 3 2 1 0 numer bitu
1 1 0 0 0 0 0 0 (-64)
+ 0 1 0 0 0 0 0 0 (64)
0 0 0 0 0 0 0 0 (0)

Natomiast dodawanie −64+(−128)

będzie wyglądało następująco:

-128 64 32 16 8 4 2 1 wartość bitu
1 1 0 0 0 0 0 0 (-64)
+ 1 0 0 0 0 0 0 0 (-128)
0 1 0 0 0 0 0 0 (64)

Zauważmy, że w ostatnim przypadku wynik jest błędny. Jest tak dlatego, że faktyczny wynik -192 nie mieści się w 8-bitowej reprezentacji.

Część techniczna: Błędy kompilacji

Wiemy już, że kompilator języka C++ jest pod wieloma względami dosyć restrykcyjny. Drobne usterki, takie jak literówka czy "czeski błąd", zazwyczaj od razu powodują, że kompilator zgłosi błąd i zaprzestanie próby kompilowania programu. Taką sytuację określamy mianem błędu kompilacji. Radzenie sobie z takimi błędami jest zazwyczaj całkiem proste – wystarczy przeczytać dokładnie treść błędu zgłoszonego przez kompilator i poprawić odpowiedni fragment kodu. Jeśli kompilacja nie udała się, Code Blocks zaznacza wiersz, który spowodował błąd kompilacji, a na dole ekranu wyświetla opis błędu.

Dla przykładu, przepiszemy nasz wcześniejszy program obliczający pole i obwód prostokąta, umieszczając w nim pewne usterki, które najczęściej przydarzają się początkującym programistom, i przyjrzymy się występującym błędom kompilacji. Oto pierwsza błędna wersja:

#include <iostream>
using namespace std;

int main() {
    int a, b;
    cin << a << b;
    cout << "Pole: " << a * b
         << " obwod: " << 2 * (a + b) << endl;
}

Treść błędu kompilacji pozwala nam zauważyć, że przy wczytywaniu pomyliliśmy znaki >> z <<.

In function 'int main()':
Wiersz 6: error: no match for 'operator<<' in 'std::cin << a'

Druga błędna wersja:

#include <iostream>
using namespace std;

int main() {
    int a, b;
    cin >> a >> b
    cout << "Pole: " << a * b
         << " obwod: " << 2 * (a + b) << endl;
}

Z treści błędu kompilacji możemy wywnioskować, że na końcu wiersza z wczytywaniem zapomnieliśmy dać średnika:

In function 'int main()':
Wiersz 7: error: expected ';' before 'cout'

Trzeci błąd:

#include <iostream>
using namespace std;

int main() {
    int a;
    cin >> a >> b;
    cout << "Pole: " << a * b
         << " obwod: " << 2 * (a + b) << endl;
}

Ewidentnie nie zadeklarowaliśmy jednej z używanych zmiennych:

In function 'int main()':
Wiersz 6: error: 'b' was not declared in this scope

A oto jeszcze jedna wersja programu, w której zupełnie zapomnieliśmy o dwóch początkowych wierszach:

int main() {
    int a, b;
    cin >> a >> b;
    cout << "Pole: " << a * b
         << " obwod: " << 2 * (a + b) << endl;
}

Błąd kompilacji, tym razem dość obszerny, wskazuje, że kompilator nie rozpoznał żadnego ze słówek cin, cout, endl, służących do wczytywania i wypisywania danych.

prog.cpp: In function 'int main()':
Wiersz 3: error: 'cin' was not declared in this scope
Wiersz 4: error: 'cout' was not declared in this scope
Wiersz 5: error: 'endl' was not declared in this scope

Możliwych jest wiele innych typów drobnych usterek. Bywa też tak, że trzeba wczytać się dokładnie w komunikat kompilatora, gdyż czasem kompilator może nam sugerować inne źródło problemu niż to, co było nim w istocie (np. musimy spojrzeć na jeden z wcześniejszych wierszy).

Jest też kilka innych typów błędów, na jakie może natknąć się programista. Gdy program skompiluje się poprawnie, może dawać w wyniku błędne odpowiedzi, czyli działać inaczej, niż miał w zamierzeniu. Może też np. zakończyć się błędem wykonania (patrz przykład z dzieleniem przez 0). Sposoby radzenia sobie z poszczególnymi typami błędów będziemy przedstawiać w częściach technicznych kolejnych lekcji.

Zadania

W tej lekcji mamy dla Ciebie trzy zadania. W każdym zadaniu, w sekcji "Wejście" znajduje się opis danych, które program ma wczytać, natomiast w sekcji "Wyjście" podano wymagany sposób wypisania wyniku. Twoje rozwiązanie zostanie sprawdzone automatycznie z użyciem pewnej liczby testów, czyli różnych zestawów danych wejściowych (możesz to sobie wyobrazić tak, że nasz automat uruchamia Twój program, "wpisuje" odpowiednie dane i sprawdza wynik jego działania). W każdym teście dane wejściowe są idealnie zgodne z opisem podanym w sekcji "Wejście", a wynik Twojego programu musi dokładnie odpowiadać temu, co jest opisane w sekcji "Wyjście". Jak wspominaliśmy poprzednio, dopuszczalne są nadmiarowe spacje na końcach wierszy i puste wiersze na końcu wyjścia. Program nie może wypisywać żadnych dodatkowych komunikatów, o które nie jest proszony w treści zadania. Twój program zostanie uznany za poprawny, jeśli zadziała poprawnie na wszystkich naszych testach.

Przypominamy, że zadania należy rozwiązywać samodzielnie. Jeśli chcesz, możesz przy rozwiązywaniu wyszukiwać dodatkowe informacje w Internecie lub zasięgnąć pomocy u nauczyciela lub konsultanta. Zabronione jest jednak wspólne rozwiązywanie zadań przez uczestników kursu lub proszenie innych o napisanie rozwiązań za nas.

A oto obiecane zadania – powodzenia!

Zadanie 1. Na odwrót

Zadaniem Twojego programu będzie wczytanie trzech liczb całkowitych i wypisanie ich w takiej samej kolejności oraz w kolejności odwrotnej.

Wejście

W jedynym wierszu wejścia znajdują się trzy liczby całkowite oddzielone spacjami: \(a, b, c\) (\(−1000 \leq a, b, c \leq 1000\)).

Wyjście

W pierwszym wierszu należy wypisać podane liczby w kolejności wczytania: \(a, b, c\). W drugim wierszu należy wypisać podane liczby w kolejności odwrotnej do kolejności wczytania: \(c, b, a\). W obu wierszach liczby powinny być rozdzielane pojedynczymi spacjami.

Przykład

Wejście Wyjście
7 3 5 7 3 5
5 3 7

Sprawdź kod na Szkopule

Zadanie 2. Prostopadłościan

Zadaniem Twojego programu będzie obliczenie objętości i pola powierzchni prostopadłościanu o zadanych wymiarach.

Wejście

Jedyny wiersz wejścia zawiera trzy liczby całkowite dodatnie \(a, b, c\) (\(1 \leq a, b, c < 500 000 000\)) oddzielone spacjami. Oznaczają one trzy wymiary prostopadłościanu, czyli długości trzech prostopadłych krawędzi.

Wyjście

W pierwszym wierszu należy wypisać objętość prostopadłościanu o krawędziach długości \(a, b, c\). W drugim wierszu należy wypisać pole powierzchni tego prostopadłościanu. Możesz założyć, że zarówno pole powierzchni jak i objętość nie przekroczy \(2 000 000 000\).

Przykład

Wejście Wyjście
1 1 2 2
10

Sprawdź kod na Szkopule

Zadanie 3. Czas

Napisz program, który przelicza czas podany w sekundach na zapis uwzględniający godziny, minuty oraz sekundy.

Wejście

Jedyny wiersz wejścia zawiera jedną liczbę całkowitą \(t\) (\(1 \leq t \leq 1 000 000\)), oznaczającą czas wyrażony w sekundach.

Wyjście

Twój program powinien wypisać czas \(t\) w postaci \(g\) g \(m\) m \(s\) s, gdzie \(g\), \(m\) i \(s\) oznaczają odpowiednio liczbę godzin, minut i sekund. Innymi słowy, \(g\) godzin, \(m\) minut i \(s\) sekund powinno łącznie dawać \(t\) sekund. Liczby \(g\), \(m\) i \(s\) powinny być całkowite i nieujemne, a liczby \(m\) i \(s\) nie powinny przekraczać \(59\). W liczbach nie należy wypisywać dodatkowych zer wiodących.

Przykład

Wejście Wyjście
4000 1g6m40s

Sprawdź kod na Szkopule