Przejdź do treści

Instrukcja przypisania i typ znakowy char

Część programistyczna: Instrukcja przypisania

W tej lekcji opowiemy o innej metodzie nadawania wartości zmiennej poza jej wczytaniem. Jest nią instrukcja przypisania. Aby nadać zmiennej \(a\) typu int wartość 5, możemy użyć następującej instrukcji przypisania:

a = 5;

Przypisania tego typu zazwyczaj stosuje się do ustawienia początkowej wartości zmiennej.

Jeśli przypisanie następuje bezpośrednio po deklaracji zmiennej, możemy je połączyć z deklaracją:

int a = 5;

Zamiast pojedynczej liczby w przypisaniu może także wystąpić dowolne wyrażenie matematyczne. Przykładowo, jeśli wczytaliśmy wartości zmiennych całkowitych \(a\), \(x\) oraz \(b\), możemy obliczyć wartość funkcji liniowej \(ax+b\) i przypisać ją do zmiennej \(y\):

int a, b, x, y;
cin >> a >> x >> b;
y = a * x + b;

W ten sposób wprowadza się zmienną pomocniczą, co nierzadko pomaga uprościć obliczenia. W ten sposób w algorytmie rozwiązywania równania kwadratowego (patrz komentarz do poprzedniej lekcji) moglibyśmy wprowadzić zmienną pomocniczą \(Delta\).

Co ciekawe, zmienna, której przypisujemy nową wartość, może wystąpić także w wyrażeniu po prawej stronie instrukcji przypisania! Np. w takiej instrukcji:

a = a + 3;

Jak to rozumieć? W instrukcji przypisania najpierw obliczana jest wartość wyrażenia po prawej stronie (tu: \(a+3\)), a następnie staje się ona nową wartością zmiennej po lewej stronie (tu: \(a\)). Tak więc powyższe przypisanie odpowiada zwiększeniu wartości zmiennej \(a\) o 3.

Zmiennym możemy wielokrotnie przypisywać różne wartości. To uzasadnia, dlaczego nazywamy je właśnie zmiennymi.

Czas na przykład. W znakomitej książce Mozaika matematyczna pod redakcją Endre Hodi (Wiedza Powszechna, Warszawa 1987) można znaleźć opis następującej zabawy-zgadywanki.

Zobacz tekst nagrania

Alina mówi do Bolka: rzuć trzema kostkami, zapamiętaj wyniki i nie pokazuj mi. Pierwszy pomnóż przez 2, dodaj 5, pomnóż przez 5, dodaj wynik z drugiej kostki, pomnóż przez 10, na koniec dodaj wynik z trzeciej kostki i powiedz, ile Ci wyszło.

Bolek oznajmia Alinie sam wynik, po czym ona zawsze potrafi zgadnąć, jakie liczby wypadły na kostkach. W jaki sposób? Tym nie będziemy się tutaj zajmować – nie chcemy Ci psuć zabawy z rozwiązywania zagadki. W zamian za to napiszemy program, który działa zgodnie z procedurą wykonywaną przez Bolka. Wykorzystamy w nim jedną zmienną pomocniczą \(w\), do której zastosujemy przypisania opisane w zagadce.

#include <iostream>
using namespace std;

int main() {
    int kostka1, kostka2, kostka3;
    cin >> kostka1 >> kostka2 >> kostka3;
    int w = kostka1;
    w = w * 2;
    w = w + 5;
    w = w * 5;
    w = w + kostka2;
    w = w * 10;
    w = w + kostka3;
    cout << w << endl;
}

Załóżmy dla przykładu, że Bolek wyrzucił odpowiednio 4 oczka, 2 oczka i 1 oczko. Wówczas na powyższy program można spojrzeć jak na ciąg operacji wykonywanych na tablicy (lub na kartce z użyciem ołówka i gumki). Początkowa wartość zmiennej \(w\) to 4. Piszemy więc na tablicy liczbę 4. Kolejne przypisanie mówi: weź wartość zmiennej i pomnóż przez 2. Mażemy zatem z tablicy liczbę 4, a w jej miejsce piszemy \(4 \cdot 2 = 8\). Dalej mamy zwiększyć to, co wyszło poprzednio, o 5. W miejsce liczby 8 piszemy zatem \(8 + 5 = 13\). Kontynuujemy w ten sposób, nadpisując wartość zmiennej \(w\) kolejno przez 65, 67, 670 i 671.

Powyższy program moglibyśmy też napisać tak:

#include <iostream>
using namespace std;

int main() {
    int kostka1, kostka2, kostka3;
    cin >> kostka1 >> kostka2 >> kostka3;
    int w1 = kostka1;
    int w2 = w1 * 2;
    int w3 = w2 + 5;
    int w4 = w3 * 5;
    int w5 = w4 + kostka2;
    int w6 = w5 * 10;
    int w7 = w6 + kostka3;
    cout << w7 << endl;
}

W ten sposób niepotrzebnie korzystamy z aż siedmiu różnych zmiennych, podczas gdy wystarczy do tego tylko jedna!

Skrócone instrukcje przypisania

W języku C++ są też dostępne skrócone instrukcje przypisania. Otóż zamiast:

a = a + b;
a = a - b;
a = a * b;
a = a / b;
a = a % b;

możemy napisać odpowiednio:

a += b;
a -= b;
a *= b;
a /= b;
a %= b;

Używając tych instrukcji, program z kostkami możemy napisać tak:

#include <iostream>
using namespace std;

int main() {
    int kostka1, kostka2, kostka3;
    cin >> kostka1 >> kostka2 >> kostka3;
    int w = kostka1;
    w *= 2;
    w += 5;
    w *= 5;
    w += kostka2;
    w *= 10;
    w += kostka3;
    cout << w << endl;
}

Jak okaże się w kolejnych lekcjach, szczególnie często wykonuje się przypisanie polegające na zmniejszeniu lub zwiększeniu wartości danej zmiennej o jeden. Do tego celu można użyć skróconych instrukcji tzw. inkrementacji i dekrementacji:

a++; // to samo co: a += 1;
a--; // to samo co: a -= 1;

W nawiązaniu do pierwszej z tych instrukcji w dość dowcipny sposób stworzono nazwę języka programowania, którym posługujemy się w tym kursie: język C++ jest rozszerzeniem języka programowania C. Co ciekawe, poprzednikiem języka C jest język B, natomiast jego następca – niedawno powstały język będący alternatywą dla C++ – to język D.

A oto inny przykład. Zobaczymy, jak zastosowanie instrukcji przypisania może uprościć rozwiązanie zadania Czas z drugiej lekcji. Przypomnijmy, że należało w nim przeliczyć czas \(t\) sekund na zapis w godzinach, minutach i sekundach.

To zadanie można oczywiście rozwiązać bez użycia instrukcji przypisania, jednak z jej pomocą rozwiązanie staje się bardziej przejrzyste. Klucz do rozwiązania stanowi wprowadzenie trzech zmiennych: \(g\), \(m\) i \(s\), które oznaczają odpowiednie fragmenty wyniku: liczbę pełnych godzin w czasie \(t\), liczbę pełnych minut w tym czasie z wyłączeniem pełnych godzin i pozostałą liczbę sekund. Wartości tych zmiennych możemy obliczać w kolejności \(g\), \(m\), \(s\) lub odwrotnie. W poniższym programie obraliśmy właśnie kolejność odwrotną, natomiast rozwiązanie wybierające kolejność zgodną z podaną pozostawiamy już uczestnikom kursu.

#include <iostream>
using namespace std;

int main() {
    int t;
    cin >> t;
    int g, m, s;
    s = t % 60;
    t /= 60;
    m = t % 60;
    t /= 60;
    g = t;
    cout << g << "g" << m << "m" << s << "s" << endl;
}

Bardziej złożony przykład

W tym przykładzie wykorzystamy większość elementów języka C++, których nauczyliśmy się do tej pory.

Zobacz tekst nagrania

Wyobraźmy sobie, że przyszliśmy do automatu z napojami. Są w nim dostępne puszki z naszym ulubionym napojem, w cenie 6 zł za sztukę. Automat przyjmuje jedynie monety o nominałach 1 zł, 2 zł i 5 zł. Niestety, na automacie znajduje się informacja, że nie wydaje on reszty – tak więc, chcąc zakupić ulubiony napój, musimy zawsze wrzucać do niego odliczoną kwotę 6 zł.

Zakładając, że w automacie dostępnych jest dowolnie wiele puszek naszego ulubionego napoju, chcielibyśmy wiedzieć, ile puszek możemy kupić za pomocą monet, które aktualnie mamy w kieszeni. Napiszemy program, który wczyta liczby poszczególnych typów monet, które mamy, i wypisze liczbę puszek, które chcemy kupić.

Myślenie nad rozwiązaniem warto rozpocząć od rozważenia wszystkich możliwych układów dających łącznie 6 zł. Są to:

6 = 5 + 1
6 = 2 + 2 + 2
6 = 2 + 2 + 1 + 1
6 = 2 + 1 + 1 + 1 + 1
6 = 1 + 1 + 1 + 1 + 1 + 1

Łatwo zauważyć, że w naszym zadaniu jest istotne, których układów będziemy używać. Np. w przypadku posiadania w kieszeni sześciu monet jednozłotowych i sześciu pięciozłotowych nie opłaca nam się ani razu używać układu 1+1+1+1+1+1, gdyż wtedy monety pięciozłotowe nam się już na nic nie przydadzą. Lepiej jest kupić sześć puszek napoju, korzystając z układu 5+1.

Zauważmy, że w ogóle najbardziej opłaca nam się używać układu 5+1. Jest to jedyny układ, w którym występuje moneta pięciozłotowa, więc jeśli którejś monety o tym nominale nie zużyjemy w ten sposób, to nie wykorzystamy jej już wcale. Co więcej, jest to jedyny układ używający tylko jednej złotówki. Gdyby nam miała zostać na końcu jakaś pięciozłotówka, a którąś jednozłotówkę wykorzystalibyśmy w innym układzie, moglibyśmy wyjąć ją z tego innego układu i stworzyć z tych dwóch monet układ 5+1. W ten sposób liczba stworzonych układów, czyli liczba kupionych puszek, na pewno by nie zmalała. Kontynuując to postępowanie, możemy uzasadnić, że rzeczywiście opłaca nam się stworzyć możliwie wiele układów 5+1.

Dalej możemy uzasadnić podobnie, że kolejnym najbardziej opłacalnym układem jest 2+2+2. Jeśli mamy układ z monetami dwu- i jednozłotowymi i odpowiednio wiele dwuzłotówek poza nim, możemy podmienić jednozłotówki z układu na te dwuzłotówki, otrzymując układ 2+2+2. Wtedy kupujemy puszkę, zużywając te dwuzłotówki, i kontynuujemy to podejście, aż zostanie nam łącznie mniej niż trzy dwuzłotówki. Nietrudno zobaczyć, że możemy je wszystkie umieścić w jednym układzie typu 2+2+1+1 lub 2+1+1+1+1, jeśli oczywiście wystarczy nam jednozłotówek, a pozostałe jednozłotówki wrzucać już do automatu po sześć.

Opisane pomysły możemy zapisać w postaci pierwszego rozwiązania. Użyjemy w nim zmiennej pomocniczej \(wynik\), w której zapamiętamy liczbę już zakupionych puszek.

#include <iostream>
using namespace std;

int main() {
    int n1, n2, n5;
    cin >> n1 >> n2 >> n5;
    int wynik = 0;

    /* 6 = 5 + 1 */
    if (n5 >= n1) {
        wynik += n1;
        n5 -= n1;
        n1 = 0;
    } else {
        // Pamietaj o zaktualizowaniu wszystkich zmiennych!
        wynik += n5;
        n1 -= n5;
        n5 = 0;
    }

    /* 6 = 2 + 2 + 2 */
    wynik += n2 / 3;
    n2 %= 3;

    /* 6 = 2 + 2 + 1 + 1
       6 = 2 + 1 + 1 + 1 + 1 */
    if (n2 >= 1 && 2 * n2 + n1 >= 6) {
        // Pamietaj o zaktualizowaniu wszystkich zmiennych!
        wynik++;
        n1 -= 6 - 2 * n2;
        n2 = 0;
    }

    /* 6 = 1 + 1 + 1 + 1 + 1 + 1 */
    wynik += n1 / 6;
    n1 %= 6;

    cout << wynik << endl;
}

Mamy tu trochę przypadków. Warto zastanowić się, czy nie dało się tego zadania rozwiązać prościej.

Zacznijmy od początku rozwiązania. Liczba układów 5+1, które jesteśmy w stanie stworzyć, jest równa \(\min(n1,n5)\). Okazuje się, że w języku C++ dostępne są funkcje min i max. Aby mieć do nich dostęp, należy na początku programu dopisać jeden dodatkowy wiersz, w którym zadeklarujemy chęć używania podstawowych algorytmów w języku C++:

#include <iostream>
#include <algorithm>
using namespace std;

Jednak nawet gdybyśmy o tym nie wiedzieli, moglibyśmy zamiast nich napisać prostą instrukcję warunkową, tak jak w jednym z zadań domowych w lekcji 3.

To pozwoli nam uprościć część rozwiązania dotyczącą przypadku 5+1. A co z pozostałymi przypadkami? Okazuje się, że z nimi będzie jeszcze prościej. Wyobraźmy sobie, że wszystkie monety dwu- i jednozłotowe, jakie pozostały nam po rozważaniu przypadku 5+1, ustawiliśmy w rzędzie – najpierw dwuzłotówki, a potem jednozłotówki. Jeśli będziemy je wrzucać do automatu w tym porządku, zakupimy dokładnie

(2 * n2 + n1) / 6;

puszek napoju. A przecież więcej się nie da, co wynika z sumy nominałów tych monet! Tak więc możemy wszystkie pozostałe przypadki skleić w jeden. Drugie rozwiązanie poniżej.

#include <iostream>
#include <algorithm>
using namespace std;

int main() {
    int n1, n2, n5;
    cin >> n1 >> n2 >> n5;

    /* 6 = 5 + 1 */
    int wynik = min(n1, n5);
    n1 -= wynik;

    /* pozostale */
    wynik += (2 * n2 + n1) / 6;

    cout << wynik << endl;
}

Komentarz: Reprezentacja danych wejściowych

Czasami kłopoty ze sformułowaniem algorytmu wynikają z niedoprecyzowania postaci danych wejściowych. Oto przykład zadania zaczerpnięty z notatek Piotra Chrząstowskiego ze "Wstępu do programowania" dla studentów I roku informatyki na Uniwersytecie Warszawskim.

Zobacz tekst nagrania

Problem Dwa prostokąty (wersja nieprecyzyjna) Dane są dwa prostokąty \(P\) i \(Q\) o bokach równoległych do osi układu współrzędnych. Należy wyznaczyć prostokąt, który jest częścią wspólną prostokątów \(P\) i \(Q\).

Nie jest jasne, w jaki sposób prostokąty są zadane. Czy są np. narysowane na kartce? W jaki sposób wprowadzić zawartość kartki do komputera? Zeskanować? Jak stwierdzić na przykład, że jakiś konkretny punkt leży wewnątrz prostokąta? Jeżeli zaczniemy się przyglądać punktowi narysowanemu bardzo blisko brzegu za pomocą mikroskopu, to odkryjemy, że to nie żaden punkt, tylko olbrzymie nieregularne rozlewiska tuszu na górzystej przestrzeni papieru.

Francuski uczony Ren\u00e9 Descartes (znany jako Kartezjusz) zaproponował metodę opisu obiektów geometrycznych za pomocą ciągów liczb, wprowadzając w tym celu prostokątny układ współrzędnych, zwany też układem współrzędnych kartezjańskich. W układzie kartezjańskim punkt opisać można za pomocą dwóch liczb, a np. trójkąt za pomocą trzech punktów (czyli sześciu liczb). Dzisiaj wszyscy uczymy się tej metody w szkole.

Załóżmy, że autorowi zadania chodziło o to, że prostokąty są zadane właśnie za pomocą współrzędnych kartezjańskich. Na to wskazywałoby sformułowanie "osie układu współrzędnych" pojawiające się w treści zadania. Dość oczywiste jest, że punkt \(A\) możemy teraz opisać za pomocą dwóch liczb \(Ax\) i \(Ay\) będących odpowiednio współrzędnymi \(x\) i \(y\) punktu \(A\) w kartezjańskim układzie współrzędnych. Wciąż jednak nie jest jasne, jaki wariant opisu prostokątów autor zadania miał na myśli. Dla przykładu, prostokąt może być zadany jako:

  • cztery wierzchołki w dowolnej kolejności,
  • dwa przeciwległe wierzchołki w dowolnej kolejności,
  • dolny lewy wierzchołek, długość boku równoległego do osi X, długość boku równoległego do osi Y,
  • górny lewy wierzchołek i dolny prawy wierzchołek,
  • dolny lewy wierzchołek i górny prawy wierzchołek.

Ponieważ mamy tu pewną swobodę (autor nie sprecyzował opisu danych), to przeanalizujemy najpierw nasze zadanie od strony matematycznej, a potem spróbujemy dobrać dane tak, żeby można było zaproponować możliwie najprostszy algorytm obliczania części wspólnej danych prostokątów \(P\) i \(Q\).

Po chwili namysłu pojawiają się nam kolejne pytania:

  • Czy prostokąt składa się z brzegu i wnętrza, samego brzegu, czy też samego wnętrza?
  • Czy w reprezentacji prostokąta występują liczby całkowite, wymierne, rzeczywiste?

Na potrzeby tej lekcji i dla prostoty wywodów przyjmiemy, że brzeg prostokąta nie jest jego częścią. Wówczas część wspólna dwóch prostokątów stykających się tylko w wierzchołku lub na brzegach jest pusta. Więcej, dzięki temu rozważane przez nas niepuste prostokąty są zawsze "porządne" – nie mogą być zdegenerowane do punktu ani odcinka. Przyjmiemy, że w opisie prostokątów będziemy mieli do czynienia tylko z liczbami całkowitymi – nie widać dobrego powodu, dlaczego liczby rzeczywiste miałyby przynieść nam w tym zadaniu jakąś korzyść.

Analizę naszego zadania zacznijmy od przypomnienia sobie, jaka jest część wspólna dwóch prostokątów. Niech \(R\) będzie częścią wspólną prostokątów \(P\) i \(Q\). Prostokąty \(P\), \(Q\) i \(R\) (a skąd wiadomo, że \(R\) jest prostokątem?) to zbiory punktów. Zastanówmy się, kiedy punkt \(A\) należy do \(R\). Zgodnie z definicją:

punkt \(A\) należy do \(R\), przecięcia prostokątów \(P\) i \(Q\), wtedy i tylko wtedy, gdy należy zarówno do prostokąta \(P\), jak i prostokąta \(Q\).

Spróbujmy sformułować jakieś łatwe kryterium na to, kiedy punkt \(A\) należy do prostokąta \(P\) (podobnie do prostokąta \(Q\)). W tym celu przyjrzyjmy się dwóm przeciwległym wierzchołkom prostokąta \(P\) – lewemu dolnemu i prawemu górnemu. Oznaczmy ich współrzędne odpowiednio przez \((a,b)\) i \((c,d)\). Zauważmy, że mamy

\(a < c\) oraz \(b < d\).

Nietrudno teraz zauważyć, że każdy punkt należący do prostokąta \(P\) (pamiętajmy, że do prostokąta nie zaliczamy jego brzegu) musi mieć współrzędną \(x\) z przedziału otwartego \((a,c)\), a współrzędną \(y\) z przedziału \((b,d)\). Zatem punkt \(A=(s,t)\) należy do prostokąta \(P\) wtedy i tylko wtedy, gdy \(a < s < c\) oraz \(b < t < d\). Podobnie dla prostokąta \(Q\) o wierzchołkach \((e,f)\) i \((k,l)\) mamy, że punkt \(A\) należy do prostokąta \(Q\) wtedy i tylko wtedy, gdy \(e < s < k\) oraz \(f < t < l\). Z tego wynika, że punkt \(A\) należy do przecięcia \(R\) prostokątów \(P\) i \(Q\), gdy \(s\) należy do części wspólnej przedziałów \((a,c)\) i \((e,k)\), natomiast \(t\) należy do części wspólnej przedziałów \((b,d)\) i \((f,l)\).

W ten sposób nasze zadanie wyznaczenia przecięcia dwóch prostokątów sprowadziliśmy do dużo prostszego zadania obliczenia części wspólnej dwóch otwartych przedziałów liczbowych zadanych przez ich końce. Co jest zatem częścią wspólną dwóch przedziałów liczbowych \((x_1,y_1)\) i \((x_2,y_2)\), przy czym wiemy, że \(x_1 < y_1\) i \(x_2 < y_2\)? Nietrudno zauważyć, że każda liczba \(z\), dla której \(x_1 < z < y_1\) oraz \(x_2 < z < y_2\), musi spełniać: \(\max(x_1,x_2) < z\) oraz \(z < \min(y_1,y_2)\). Rozważmy kilka przykładów:

  • \((x_1,y_1) = (5,10)\), \((x_2,y_2) = (7,12)\) \(\max(x_1,x_2) = 7\), \(\min(y_1,y_2) = 10\) i częścią wspólną przedziałów \((x_1,y_1)\) i \((x_2,y_2)\) jest \((7,10)\)
  • \((x_1,y_1) = (5,10)\), \((x_2,y_2) = (3,15)\) \(\max(x_1,x_2) = 5\), \(\min(y_2,y_2) = 10\) i częścią wspólną przedziałów \((x_1,y_1)\) i \((x_2,y_2)\) jest \((5,10)\)
  • \((x_1,y_1) = (5,10)\), \((x_2,y_2) = (12,15)\) \(\max(x_1,x_2) = 12\), \(\min(y_1,y_2) = 10\) i częścią wspólną przedziałów \((x_1,y_1)\) i \((x_2,y_2)\) jest \((12,10)\); jeśli przyjmiemy, że gdy lewy koniec przedziału otwartego jest nie mniejszy od jego prawego końca, to oznacza to przedział pusty, wówczas i w tym przypadku dostajemy poprawne rozwiązanie.

Po tej analizie jesteśmy już w stanie podać dokładną specyfikację naszego zadania i prosty algorytm jego rozwiązania. Pozostawiamy to uczestnikom kursu jako ćwiczenie.

Warto także postawić sobie pytanie, jak zmieniłoby się rozwiązanie zadania, gdybyśmy wybrali inną reprezentację danych wejściowych. Na przykład, co by było, gdyby prostokąty były reprezentowane razem z brzegiem?

Część techniczna: Typ znakowy char

W części technicznej lekcji wprowadzimy nowy typ zmiennych – typ znakowy char. Pozwala on przechowywać pojedyncze znaki (małe i wielkie litery, cyfry, znaki przestankowe itp.). Wartości typu char są w języku C++ otoczone apostrofami, np. 'a', '8', '+', '.'.

Jako pierwszy przykład napiszmy program, który wczytuje dany znak, o którym wiemy, że jest małą literą, i sprawdza, czy jest to samogłoska, czy spółgłoska.

#include <iostream>
using namespace std;

int main() {
  char c;
  cin >> c;
  if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' || c == 'y')
    cout << "samogloska" << endl;
  else
    cout << "spolgloska" << endl;
}

Każdy ze znaków typu char ma przypisany numer będący liczbą całkowitą. To przyporządkowanie, używane powszechnie w komputerach do reprezentowania znaków, nazywa się kodem ASCII. Wygląda ono tak:

kod znak kod znak kod znak kod znak kod znak
0-31 znaki specjalne 51 3 71 G 91 [ 111 o
32 spacja 52 4 72 H 92 \ 112 p
33 ! 53 5 73 I 93 ] 113 q
34 " 54 6 74 J 94 ^ 114 r
35 # 55 7 75 K 95 _ 115 s
36 $ 56 8 76 L 96 ` 116 t
37 % 57 9 77 M 97 a 117 u
38 & 58 : 78 N 98 b 118 v
39 ' 59 ; 79 O 99 c 119 w
40 ( 60 < 80 P 100 d 120 x
41 ) 61 = 81 Q 101 e 121 y
42 * 62 > 82 R 102 f 122 z
43 + 63 ? 83 S 103 g 123 {
44 , 64 @ 84 T 104 h 124
45 - 65 A 85 U 105 i 125 }
46 . 66 B 86 V 106 j 126 ~
47 / 67 C 87 W 107 k 127 znak specjalny
48 0 68 D 88 X 108 l
49 1 69 E 89 Y 109 m
50 2 70 F 90 Z 110 n

Oczywiście nie trzeba pamiętać kodów ASCII poszczególnych znaków. Warto jedynie wiedzieć, że małe litery oraz wielkie litery alfabetu angielskiego (łacińskiego) są ustawione w kodzie kolejno w porządku alfabetycznym, a cyfry – od najmniejszej do największej. Porównywanie znaków typu char za pomocą operatorów <, <=, >, >= odbywa się według kodów ASCII, tak więc małe litery oraz wielkie litery są porównywane alfabetycznie, a cyfry od najmniejszej do największej. Znaki o kodach od 0 do 31 oraz znak o kodzie 127 to tzw. kody sterujące. Znajdują się wśród nich m.in. znaki końca wiersza i tabulacji; wiele z tych znaków wyszło już z użycia.

Wartości zmiennych typu char możemy więc traktować jako niewielkie liczby całkowite. Dokładniej, zmienna typu char przyjmuje wartości od -128 do 127, przy czym wartości nieujemne odpowiadają znakom kodu ASCII, a pozostałe mogą służyć do reprezentowania innych symboli (np. polskich znaków ą, ę, ź, ć itp. w niektórych kodowaniach). Typ char jest więc typem całkowitym jednobajtowym, którego brakowało w komentarzu do lekcji 2. Odpowiadającym mu typem całkowitym nieujemnym (o wartościach od 0 do 255) jest typ unsigned char.

Przyjrzyjmy się, jakie konsekwencje ma ta dwoista natura typu char.

Przypisując wartość zmiennej typu char, możemy to zrobić, albo wstawiając żądany znak w apostrofy, albo podając numer tego znaku w kodzie ASCII. Czyli np. oba poniższe przypisania są równoważne:

char znak = 'a';
char znak = 97;

Z przyczyn technicznych przy wczytywaniu i wypisywaniu to już tak łatwo nie zadziała. Chodzi o to, że typ char wczytuje i wypisuje znak, a nie liczbę. Jeśli więc przy takim fragmencie programu:

char znak;
cin >> znak;

wpiszemy na wejściu liczbę 97, to zamiast znaku 'a' o kodzie ASCII 97 zostanie wczytany po prostu znak '9', jako pierwszy znak na wejściu!

Aby wczytać znak o danym kodzie ASCII, należy wczytać ten kod jako liczbę całkowitą innego typu niż char (np. typu int) i przypisać zmiennej znak wczytaną wartość:

int kod;
char znak;
cin >> kod;
znak = kod;

Oczywiście zadziała to tylko wtedy, gdy kod będzie faktycznie liczbą z zakresu typu char – w przeciwnym razie wystąpi znany nam już błąd przekroczenia zakresu typu.

Podobnie z wypisywaniem kodu ASCII znaku:

char znak;
int kod;
kod = znak;
cout << kod << endl;

Jest to szczególny przykład tzw. konwersji typów (inaczej: rzutowania typów), czyli zmiany jednego typu na inny. W przypadku typów całkowitych w C++ dokonuje się ona automatycznie, w momencie przypisania zmiennej jednego typu wartości zmiennej (bądź wyrażenia) innego typu. Można też jawnie "poprosić" kompilator C++, aby dokonał konwersji. Robi się to, umieszczając nazwę typu przed zmienną:

char znak;
cout << (int)znak << endl;

Trochę więcej o konwersjach opowiemy w następnych lekcjach. Tymczasem jeszcze jeden, "złośliwy" przykład. Powiedzmy, że chcielibyśmy napisać program, który wczyta liczbę \(i\) i wypisze \(i\)-tą małą literę alfabetu angielskiego (a zatem \(i \in \{1,\ldots,26\}\), bowiem alfabet angielski ma 26 liter). Moglibyśmy to próbować zrobić tak:

int numer;
cin >> numer;
cout << 'a' + numer - 1;

Gdy na wejściu wprowadzimy np. liczbę 3, na wyjściu otrzymamy... liczbę 99, czyli kod ASCII litery c! Jest tak dlatego, że wyniki działań arytmetycznych są w C++ domyślnie interpretowane jako liczby (w tym przypadku typu int). Aby otrzymać na wyjściu rzeczywiście literę c, musimy użyć konwersji na typ char:

int numer;
cin >> numer;
cout << (char)('a' + numer - 1);

Zadania

Oto zadania do dzisiejszej lekcji. W tej lekcji mamy również przygotowane jedno zadanie z gwiazdką. Przypominamy, że punkty za zadania z gwiazdką nie wpływają na zaliczenie kursu (tym bardziej, że Wielkanoc jest jeszcze daleko). UWAGA: Zaliczenie kursu dotyczyło edycji z 2016 roku.

Łańcuszek

Kody szesnastkowe

Podzielne

Wielkanoc (*)