Kurs podstawy programowania UMCS, zdjęcie laptopa.

Podstawy programowania – Laboratorium 2

Wejściówka – Podstawy

Napisz program w języku C++, który pobierze ze standardowego wejścia zmiennoprzecinkowe długości podstaw i wysokość trapezu, a następnie wyświetli na standardowym wyjściu jego pole.

Rozwiązanie:
#include <iostream>
using namespace std;

int main() {
    float a, b, h;

    cin >> a >> b >> h;
    cout << "Pole trapezu:" << (a + b) * h / 2.f << endl;

    return 0;
}

Zadanie 1 – Deklaracja, definicja, inicjalizacja

Napisz program w języku C++, który wczytuje od użytkownika dwie liczby zmiennoprzecinkowe podwójnej precyzji a i b. Program powinien wyświetlić te liczby, w następującym formacie a = wartosc_a oraz w kolejnej linii b = wartosc_b. Następnie zamienić wartościami te liczby i ponownie je wyświetlić w tym samym formacie. (tzw. funkcja swap)

Rozwiązanie:
//Version 1.0
#include <iostream>
using namespace std;

int main () {
    double a, b, temp;

    cin >> a >> b;
    cout << "a = " << a << endl << "b = " << b << endl;

    temp = a;
    a = b;
    b = temp;

    cout << "a = " << a << endl << "b = " << b << endl;

    return 0;
}
//Version 2.0
#include <cstdio>

int main () {
    double a, b, temp;

    scanf("%lf%lf", &a, &b);
    printf("a = %lf\nb = %lf\n", a, b);

    temp = a;
    a = b;
    b = temp;

    printf("a = %lf\nb = %lf\n", a, b);

    return 0;
}
Omówienie:

W poprzedniej części kursu pojawiły się słowa tj. deklaracja, definicja i inicjalizacja. Dwa ostatnie zostały użyte w kontekście zmiennych. W dniu dzisiejszym rozbudujemy zasób naszego słownictwa, związanego z programowaniem. Powinniśmy wiedzieć, czym różnią się te słowa, czy można je stosować zamiennie.

Deklaracja jest informacją dla kompilatora, że dana nazwa jest już znana. Jednakże w tym momencie pamięć dla obiektu nie jest jeszcze przydzielona. Do obiektu nie można się odwołać, nie można mu przypisać wartości, bowiem on tak na prawdę jeszcze nie istnieje. Deklaracja występuje w kontekście zmiennych, funkcji, czy typów danych. Należy dodać, że w programie może być kilka deklaracji tego samego elementu.

Deklaracje zmiennych znajdują się zazwyczaj w plikach nagłówkowych, zaś definicje w plikach źródłowych C/C++. Niestety w trakcie trwania tego kursu rzadko będziemy spotykać deklaracje zmiennych. Z drugiej strony należy pamiętać, że deklaracje mają duże znaczenie. W trakcie omawiania funkcji, częściej skorzystamy z deklaracji w naszych programach. Należy dodać, że w przypadku funkcji deklaracja nazywana jest również prototypem funkcji.

Definicja w przeciwieństwie do deklaracji nie informuje tylko, że dany identyfikator istnieje w programie, ale także dokładnie określa, czym on jest. Definicja rezerwuje miejsce w pamięci dla zadeklarowanej zmiennej i już można jej przypisać wartość (w przypadku funkcji definicja zawiera ciało funkcji, czyli jej implementację). Podsumowując każda definicja jest jednocześnie deklaracją, ale ta zależność nie zachodzi w drugą stronę.

Z definicją mieliśmy już styczność w prawie każdym programie na poprzednich zajęciach (np. definicje zmiennych typ nazwa_zmiennej;). Podobnie, jak w przypadku deklaracji, z definicji będziemy korzystać również przy omawianiu funkcji.

Inicjalizacja (inicjowanie) polega na przypisaniu wartości do danej zmiennej w momencie jej definicji. Aby zainicjować zmienną piszemy:
typ nazwa_zmiennej = wartosc;
Inicjalizacja następuje jedynie w przypadku przypisania wartości w momencie deklaracji. Przypisanie wartości danej zmiennej w dalszej części programu nie jest już inicjalizacją, tylko zwykłym przypisaniem. Tym samym inicjalizacja służy do przypisania początkowej wartości zmiennej i nie jest ona obowiązkowa. Jednakże warto pamiętać, że niezainicjalizowane zmienne automatyczne będą zawierać niezdefiniowane dane, które były w pamięci.

Przykładem inicjalizacji jest:
int a = 1;
, ale nie jest nim:
int a;
a = 1;


Dodatkowe informacje: Kontynuując temat zmiennych i ich nazewnictwa, warto wspomnieć o obiektach automatycznych. Obiekty automatyczne to zmienne lokalne, które są tworzone w ciele funkcji. Są one tworzone tylko na potrzeby funkcji, na stosie, więc po opuszczeniu ciała funkcji (lub ogólnie bloku {...}), są natychmiast likwidowane. Nie są one zerowane, tylko zawsze znajdują się w nich niezdefiniowane wartości. Wszystkie zmienne tworzone bez słowa kluczowego static oraz wewnątrz funkcji (czyli nie-globalne) są tworzone na stosie, a ich cykl życia ograniczony jest do danego bloku. Z tego punktu widzenia, funkcja main jest taką samą funkcją, jak każda inna. Tylko obiekty globalne i lokalne statyczne są zerowane (na start). Zmienne statyczne lokalne zachowują swoją wartość pomiędzy wywołaniami funkcji, czyli nawet po opuszczeniu bloku, w którym zostały zdefiniowane. Dodatkowo takie zmienne są inicjalizowane tylko raz. Zaś zupełnie co innego oznacza static zastosowane dla zmiennej globalnej. Jest ona wtedy widoczna tylko w jednym pliku.

Zadanie 2 – Stałe

Napisz program w języku C++, który stworzy stałą zmienną N, a następnie wyświetli jej wartość. (Próby zmiany wartości tej zmiennej powinny zakończyć się błędem kompilacji.)

Rozwiązanie:
#include <iostream>
using namespace std;

int main() {
	const int N = 10; //– musi być zainicjalizowana podczas deklaracji, nie może ulec zmianie
	cout << N << endl;
	return 0;
}
Omówienie:

Stałe w języku C++ to obiekty zadeklarowane ze słowem kluczowym const, które jest kwalifikatorem dla tej zmiennej. Tak zadeklarowane zmienne są stałe w pełnym tego słowa znaczeniu, czyli nie można ich modyfikować, są niemodyfikowalne.
const int INIT_VALUE = 5;
int value_1 = INIT_VALUE;
INIT_VALUE = 4; / * tu kompilator zwróci błąd, zaprotestuje */
int value_2 = INIT_VALUE;


Ciekawostka: W celu stworzenia takiej stałej w języku C należy użyć dyrektywy preprocesora #define. W C/C++ stała jest dokładnie tym samym co zmienia, z tym tylko zastrzeżeniem, że nie można jej jawnie modyfikować (ale można zmodyfikować zawartość wskaźnika do adresu stałej, czyli zmodyfikować stałą). Poniekąd konsekwencją tego jest fakt, że globalnie deklarowane stałe w języku C mają to samo wiązanie co zmienne, czyli zewnętrzne. W języku C++ stałe mają domyślnie wiązanie lokalne i aby były one zewnętrzne (dzielone między jednostkami kompilacji), muszą być zadeklarowane razem z inicjalizacją i słowem kluczowym extern.
extern const int N = 10; //Stała dostępna globalnie


Wskazówka: Używanie stałych jest bardzo dobrą praktyką programowania, ponieważ umożliwia uniknięcia przypadkowych pomyłek, a ponadto kompilator może często zoptymalizować ich użycie (np. od razu podstawiając ich wartość do kodu). Jednym z przykładów dobrego zwyczaju programistycznego jest zastępowanie umieszczonych na stałe w kodzie liczb (hardcoding) zmiennymi stałymi. Daje to większą kontrolę nad kodem, a stałe umieszczone w jednym miejscu można łatwo modyfikować. W takiej sytuacji nie trzeba szukać (po całym kodzie) liczb, które chcemy zmienić. Podsumowując używanie stałych przynosi następujące korzyści:
Bezpieczeństwo – unikamy przypadkowych modyfikacji wartości, co zapewnia większe bezpieczeństwo w kodzie.
Optymalizacja – kompilator może zoptymalizować użycie stałych, często wstawiając ich wartości bezpośrednio do kodu.
Łatwiejsza konserwacja – jeżeli wartości stałe są zebrane w jednym miejscu, zmiana ich wartości wymaga zmiany tylko jednej linii kodu, co jest szczególnie użyteczne, gdy stała jest używana w wielu miejscach.


Innym sposobem na przechowywanie stałych symbolicznych jest użycie dyrektywy preprocesora #defina. Tak zdefiniowaną stałą nazywamy stałą symboliczną. W przeciwieństwie do stałej zmiennej zadeklarowanej z użyciem słowa kluczowego const stała zdefiniowana przy użyciu #define jest zastępowana daną wartością w każdym miejscu, gdzie występuje. Stąd też może być używana w miejscach, gdzie „normalna” stała nie mogłaby dobrze spełnić swojej roli. Stałe definiowane za pomocą dyrektywy preprocesora tworzymy w następujący sposób:
#define NAZWA_STALEJ WARTOSC
Taki zapis spowoduje, że każde wystąpienie słowa NAZWA_STALEJ w kodzie zostanie zastąpione przez WARTOSC.

UWAGA: W sytuacji gdy w miejscu wartości stałej znajduje się wyrażenie, to należy je umieścić w nawiasach. Unikniemy w ten sposób niespodzianek związanych z priorytetem operatorów. Przykład:

#include <cstdio>

#define SIX 4+5
#define NINE 8+1

int main(void) {
    printf("%d * %d = %d\n", SIX, NINE, SIX * NINE);
    return 0;
}

Po skompilowaniu programu i jego uruchomieniu otrzymujemy następujący komunikat: 6 * 9 = 42, a oczekiwalibyśmy 6 * 9 = 54. Przyczyną błędu jest interpretacja wyrażenia: 1+5*8+1 ze względu na brak nawiasów i priorytety operatorów (wyższy priorytet * niż +) jest to interpretowane jako: 1+(5*8)+1 a nie (1+5)*(8+1).

W związku z powyższym, w C++ zaleca się unikanie #define do definiowania stałych, gdyż const oferuje więcej korzyści, takich jak typowanie i możliwość użycia stałych w funkcjach wewnętrznych czy przestrzeniach nazw. Jeśli nie ma specjalnych powodów, aby używać #define, to zaleca się korzystanie z const do definiowania stałych wartości.

Zadanie 3 – deklarowanie własnych nazw istniejących typów

Napisz program w języku C++, który przyjmuje jedno bajtową liczbę całkowitą oraz jedno bajtową liczbę całkowitą bez znaku. Program powinien wyświetlić wartość wczytanych zmiennych.

Rozwiązanie:
//Version 1.0
#include <cstdio>

typedef unsigned char uchar;

int main() {
    char v1;
    uchar v2;
    scanf("%hhd%hhu", &v1, &v2);  //Warto sprawdzić, jak program zachowa się przy wczytywaniu wartości za pomocą %d lub %c.
    printf("%d\t%d\n", v1, v2);
    return 0;
}

Porównanie zachowania programu z innym wczytywaniem danych.

//Version 2.0
#include <cstdio>
#include <iostream>

typedef unsigned char uchar;

int main() {
    char v1;
    uchar v2;
    std::cin >> v1 >> v2;
    printf("%d\t%d\n", v1, v2);
    return 0;
}

Zadanie 4

Napisz program w języku C++, który do jednobajtowej liczby całkowitej ze znakiem doda wartość 200. Wyświetl zmienną po zmianie, uzasadnij wynik.

Rozwiązanie:
#include <iostream>

int main() {
    char number = -56;
    
    number += 200;
    
    std::cout << (int)number << std::endl;
    
    return 0;
}
Omówienie:

Podczas dodawania wartości 200, przeprowadzane jest dodawanie modulo, czyli jeśli przekroczona zostaje wartość maksymalna dla zmiennej typu char (127), to liczba przechodzi na swoją najmniejszą wartość -128 i kontynuuje liczenie od tego punktu. Na podstawie tej reguły, wartość -56 + 200 wynosi 144. Aby wyświetlić tą wartość jako liczbę całkowitą a nie znak, musimy dokonać konwersji typów.

Zadanie 5 – rzutowanie, konwersja typów

Napisz program w języku C++, który przyjmuje dwie liczby całkowite, a następnie wyświetli iloraz tych liczb.

Rozwiązanie:
#include <iostream>
using namespace std;

int main() {
    int v1, v2;

    cin >> v1 >> v2;
    cout << v1 / float(v2); //(1.f * v2), (float) v2

    return 0;
}
Omówienie:

Rzutowanie to konwersja danej jednego typu na daną innego typu. Konwersja może być wykonana niejawnie (automatyczna, domyślna konwersja przyjęta przez kompilator) lub jawnie (wymuszona, zaimplementowana przez programistę za pomocą operatora rzutowania np. float(v)).

Przykłady konwersji niejawnej:
int i = 42.7; /* konwersja z double do int */
float f = i; /* konwersja z int do float */
double d = f; /* konwersja z float do double */
unsigned u = i; /* konwersja z int do unsigned int */
f = 4.2; /* konwersja z double do float */
i = d; /* konwersja z double do int */
char *str = "foo"; /* konwersja z const char* do char* */
const char *cstr = str; /* konwersja z char* do const char* */
void *ptr = str; /* konwersja z char* do void* */

Kompilator automatycznie dokonuje konwersji niejawnej, jeśli uzna, że jest to możliwe i bezpieczne. Podczas konwersji zmiennych zawierających większe ilości danych do typów prostszych (np. double do int) musimy liczyć się z utratą informacji. Gdy dokonujemy konwersji liczby zmiennoprzecinkowej do liczby całkowitej nie możemy przechowywać części ułamkowej toteż zostaje ona odcięta.

Zaskakująca może wydawać się konwersja z typu const char* do typu char*, która w standardzie C nie jest dopuszczalna, jednakże literały napisowe (które są typu const char*) stanowią tutaj wyjątek. Wynika to z faktu, że były one używane na długo przed wprowadzeniem słówka const do języka i brak wspomnianego wyjątku spowodowałby, że duża część kodu zostałaby nagle zakwalifikowana jako niepoprawny.

Przykłady konwersji jawnej:
double d = 3.14;
int pi_1 = (int)d;
int pi_2 = int(d);
float pi_3 = (float)d;
float pi_4 = pi_1 * 1.f;

Konwersje przedstawione wyżej są dopuszczane przez standard jako jawne konwersje. Rzutowanie jawne w C++ można wymusić na różne sposoby tak, jak wyżej, klasycznie C-style cast (typ)wartość oraz używając bardziej specyficzne operatory rzutowania w C++ (np. static_cast, dynamic_cast, const_cast, reinterpret_cast). Każdy z nich służy do jawnej konwersji konkretnego rodzaju danych, krótko je omówimy ale ich dokładna znajomość wykracza za zakres tego kursu:
dynamic_cast jest używany głównie do bezpiecznego rzutowania między klasami w hierarchii dziedziczenia, gdy chcemy sprawdzić, czy wskaźnik lub referencja do klasy bazowej faktycznie wskazuje na obiekt klasy pochodnej. Używa mechanizmu RTTI (Run-Time Type Information), co pozwala na dynamiczne sprawdzanie typu obiektu w czasie działania programu. Jeśli rzutowanie nie powiedzie się, dynamic_cast zwraca nullptr (dla wskaźników) lub zgłasza wyjątek bad_cast (dla referencji).
const_cast jest używany do usuwania lub dodawania kwalifikatorów const lub volatile z obiektu. Umożliwia to zmianę obiektu, który był pierwotnie oznaczony jako const, ale nie zmienia rzeczywistego typu danych. Jest to jedyny operator rzutowania, który może modyfikować kwalifikatory const lub volatile.
reinterpret_cast jest najpotężniejszym i jednocześnie najniebezpieczniejszym operatorem rzutowania. Służy do rzutowania między zupełnie niepowiązanymi typami, najczęściej do rzutowania wskaźników. Umożliwia np. rzutowanie wskaźnika na dowolny inny typ wskaźnikowy, co pozwala na bardziej niskopoziomowe operacje na pamięci. W przeciwieństwie do static_cast, reinterpret_cast nie sprawdza zgodności typów i może prowadzić do bardzo niebezpiecznych operacji. Należy go używać tylko wtedy, gdy jest to absolutnie konieczne i z pełnym zrozumieniem konsekwencji.
static_cast jest najczęściej używanym operatorem rzutowania w C++. Służy do wykonywania dobrze zdefiniowanych konwersji między typami, które są związane (np. konwersje między typami liczbowymi, rzutowanie w górę i w dół drzewa dziedziczenia). Jest preferowany, ponieważ daje kompilatorowi większą kontrolę nad poprawnością konwersji i może wykryć niektóre błędy na etapie kompilacji.
W ramach tego kursu operator static_cast zazwyczaj będzie najczęstszym (możliwe, że jedynym) i najlepszym wyborem. Ponadto jest on preferowany, gdyż jest bardziej bezpieczny niż klasyczne rzutowanie w stylu C, ponieważ kompilator może lepiej kontrolować błędy.

Należy zaznaczyć, że niektóre konwersje są błędne, np.:
const char *cstr = "foo";
char *str = cstr; //tu nastąpi błąd konwersji

W takich sytuacjach można użyć operatora rzutowania aby wymusić konwersję:
char *str = (char*)cstr;

Wskazówka: Należy unikać sytuacji, gdy wymuszamy konwersję i nigdy nie stosować rzutowania w celu „uciszenia kompilatora”. Rzutowanie nie jest metodą na naprawienie błędów, ale narzędziem, które należy używać ostrożnie. Zanim użyjemy operatora rzutowania należy się zastanowić, co tak na prawdę będzie on robił, jakie przyniesie efekty, czy też nie ma innego sposobu wykonania danej operacji, który wyeliminowałby podejmowanie takich kroków. Lepszą praktyką zamiast wymuszania rzutowania jest poprawne zarządzanie typami zmiennych.

Zadanie 6 – funkcje matematyczne

Napisz program w języku C++, który pobierze ze standardowego wejścia liczbę zmiennoprzecinkową, a następnie wyświetli jej wartość bezwzględną.

Rozwiązanie:
#include <iostream>
#include <cmath>

int main() {
    double number;

    std::cin >> number;
    std::cout << fabs(number) << std::endl;

    return 0;
}

Zadanie 7

Oblicz długość przeciwprostokątnej trójkąta dla długości przyprostokątnych wprowadzonych przez użytkownika. Zakładamy, że wprowadzane wartości będą liczbami całkowitymi. Skorzystaj z metody sqrt() i pow() z biblioteki <cmath>.

Rozwiązanie:
#include <iostream>
#include <cmath>

int main() {
    int a, b;
    double c;
    std::cin >> a >> b;
    c = sqrt(pow(a, 2) + pow(b, 2)); //std::sqrt warto zweryfikować różnicę
    std::cout << "Przeciwprostokątna: " << c;
    return 0;
}

Zadanie 8

Napisz program w języku C++, który obliczy pole sześciokąta foremnego. Program powinien wczytać długości boku od użytkownika, a następnie wyświetlić wynik.

Rozwiązanie:
#include <iostream>
#include <cmath>
using namespace std;

int main() {
	int a;
	cin >> a;
	cout << "Pole sześciokąta foremnego: " << 3 * a * a * sqrt(3) / 2.0f << endl;
	return 0;
}

Zadanie 9

Napisz program w języku C++, który policzy odległość pomiędzy dwoma punktami. Program powinien pobierać pary liczb określające współrzędne x i y kolejnych wierzchołków.

Rozwiązanie:
#include <iostream>
#include <cmath>
using namespace std;

int main(){
    float ax, ay, bx, by;
    cin >> ax >> ay >> bx >> by;
    cout << sqrt(pow(ax - bx, 2) + pow(ay - by, 2));
    return 0;
}

Zadanie 10 – inkrementacja i dekrementacja

Napisz program w języku C++, który wczyta od użytkownika dwie liczby całkowite i zwiększy ich wartość o jeden. Następnie program powinien wypisać iloczyn tych liczb zmniejszony o jeden.

Rozwiązanie:
#include <iostream>
using namespace std;

int main() {
    int a, b;

    cin >> a >> b;
    a += 1; //a++;
    b += 1; //b++;

    int c = a * b;
    cout << c-- << endl; //(a*b)--
    cout << c << endl;
    //lub

    // To nie zadziała: cout << (a++ * b++)--;
    /* Operator dekrementacji nie może być zastosowany do tymczasowej wartości (r-wartości), 
    ponieważ operator dekrementacji wymaga, aby operand był l-wartością (ang. l-value). 
    L-wartość to obiekt, który istnieje w pamięci i może być zmodyfikowany, np. zmienna.
    */
    c = ++a * ++b;
    cout << --c << endl;

    return 0;
}
Omówienie:

W celu skrócenia zapisu zmiany liczby o plus lub minus jeden, wprowadzono dodatkowe operatory: inkrementacji (++) oraz dekrementacji (--), które dodatkowo mogą być pre- lub postfiksowe. W rezultacie mamy cztery operatory:

  • pre-inkrementacje (++i),
  • post-inkrementacje (i++),
  • pre-dekrementacje (--i),
  • post-dekrementacje (i--).

Operatory inkrementacji zwiększają, a dekrementacji zmniejszają wartość argumentu o jeden. Ponadto operatory pre- zwracają nową wartość argumentu, natomiast post- starą wartość argumentu. Przykład:

int a = 5;
cout << ++a << endl;  // Wyświetli 6 (pre-inkrementacja)
cout << a++ << endl;  // Wyświetli 6, ale a zostanie zwiększone do 7 (post-inkrementacja)
cout << a << endl;    // Wyświetli 7

int b = 5;
cout << --b << endl;  // Wyświetli 4 (pre-dekrementacja)
cout << b-- << endl;  // Wyświetli 4, ale b zostanie zmniejszone do 3 (post-dekrementacja)
cout << b << endl;    // Wyświetli 3

Ciekawostka: Czasami (szczególnie w C++) użycie operatorów stawianych za argumentem jest nieco mniej efektywne, wydajne, ponieważ kompilator musi stworzyć nową zmienną by przechować wartość tymczasową. W szczególności w przypadku obiektów (np. iteratorów w STL) pre-inkrementacja (++i) będzie często bardziej wydajna.

Uwaga: Bardzo ważne jest, abyśmy poprawnie stosowali operatory dekrementacji i inkrementacji. Chodzi o to, aby w jednej instrukcji nie umieszczać kilku operatorów, które modyfikują ten sam obiekt (zmienną). Jeżeli taka sytuacja zaistnieje, to efekt działania instrukcji jest nieokreślony. Przykład:

int a = 1;
a = a++;
a = ++a; 
a = a++ + ++a;
printf("%d %d\n", ++a, ++a);
printf("%d %d\n", a++, a++);

Kompilator GCC potrafi ostrzegać przed takimi błędami, aby to czynił należy podać mu jako argument opcję: -Wsequence-point lub -Wall.

Zadanie 11 – operatory porównania

Napisz program w języku C++, który stworzy i zainicjuje dwie liczby zmiennoprzecinkowe następującymi wartościami: 1/10, 1-9/10. Następnie program powinien porównać ich wartości.

Rozwiązanie:
//Version 1.0
#include <cstdio>

int main() {
	float a = 1.0f / 10.0f;
	float b = 1.0f - 0.9f;

	printf("a=%g, b=%g\n", a, b);
	if (a == b) {
		printf("Zgadza sie.\n");
	} else {
		printf("Nie zgadza sie!\n");
	}
}
//Version 2.0
#include <cstdio>
#include <cmath>

int main() {
    float a = 1.0f / 10.0f;
    float b = 1.0f - 0.9f;

    printf("a=%g, b=%g\n", a, b);
    if (fabsf(b - a) < 0.00001f) //pewien epsilon na błąd
        printf("Zgadza sie.\n");
    else
        printf("Nie zgadza sie!\n");
}
//Version 3.0
#include <cstdio>
#include <cmath>
#include <cfloat>

int main() {
    float a = 1.0f / 10.0f;
    float b = 1.0f - 0.9f;

    printf("a=%g, b=%g\n", a, b);
    if (fabsf(b - a) < FLT_EPSILON) //pewien epsilon na błąd
        printf("Zgadza sie.\n");
    else
        printf("Nie zgadza sie!\n");
}
Omówienie:

Z dużym prawdopodobieństwem rezultat, który otrzymamy będzie bliski:
a = 0x3dcccccd ≈ 0.100000001
b = 0x3dccccd0 ≈ 0.100000024

W języku C/C++ występują następujące operatory porównań: równe (==), różne (!=), mniejsze (<), większe (>), mniejsze lub równe (<=) i większe lub równe (>=). Wykonują one odpowiednie porównanie swoich argumentów i zwracają 1, jeżeli warunek jest spełniony lub 0 jeżeli nie jest.

Poza operatorami porównań istnieją również następujące operatory logiczne: negacji (!), iloczynu logicznego (&&) i sumy logicznej (||). Należy pamiętać, że tak jak w przypadku operatorów arytmetycznych tutaj również obowiązuje kolejność wykonywania działań. Priorytet tych operatorów jest zgodny z tym, jak zostały wymienione, czyli najwyższy ma operator negacji, zaś najniższy sumy logicznej. Kolejnością wykonywania operacji można jednak manipulować poprzez nawiasy okrągłe – tak samo, jak ma to miejsce w przypadku działań arytmetycznych.

Warto wspomnieć, że istnieją także operatory alternatywne (zdecydowanie rzadziej spotykane w implementacjach w języku C++), które wprowadzone zostały w standardzie C++98. Operatory te zostały dodane w celu zwiększenia czytelności kodu i umożliwienia programistom pisania kodu, który wygląda bardziej zbliżenie do języka naturalnego. Poniżej pełna lista alternatywnych słów kluczowych dla operatorów logicznych:
and zamiast &&,
or zamiast ||
not zamiast !
and_eq zamiast &=
or_eq zamiast |=
xor_eq zamiast ^=
not_eq zamiast !=

Istnieją one również dla operacji bitowych, które poznamy w przyszłości:
bitand zamiast &
bitor zamiast |
xor zamiast ^
compl zamiast ~


Wskazówka: Język C/C++ wykonuje skrócone obliczenia wyrażeń logicznych (ang. short-circuit evaluation). Jest to równoznaczne z tym, że oblicza wyrażenie tylko tak długo, jak nie wie, jaka będzie jego ostateczna wartość. Tym samym kolejność wyrażeń i operacji logicznych ma duże znaczenie i może wpłynąć na wydajność naszego programu. Kontynuując wyrażenia są intepretowane od lewej do prawej i kolejno obliczane, gdy jest już znana wartość całości, nie liczy reszty wyrażenia. Prosty przykład powinien to bardziej rozjaśnić:
A && B
A || B
, jeśli A jest fałszywe to dla powyższego iloczynu logicznego, nie trzeba obliczać B, bo koniunkcja fałszu i dowolnego wyrażenia zawsze da fałsz. Analogicznie, w drugim przykładzie, jeśli A jest prawdziwe, to całe wyrażenie jest prawdziwe i wartość B nie ma znaczenia.

Oczywiście redukcja ilości obliczeń, zwiększenie wydajności i szybkości naszego programu, to ogromna zaleta. Jednakże dodatkowo skorzystanie z takiego rozwiązania umożliwia stosowanie tzw. efektów ubocznych. Idea efektu ubocznego opiera się na tym, że w wyrażeniu można wywołać funkcje, które będą robiły poza zwracaniem wyniku inne rzeczy, oraz używać podstawień.
( (a > 0) || (a < 0) || (a = 1) )
Jeśli a będzie większe od 0 to obliczona zostanie tylko wartość wyrażenia (a > 0) – da ono prawdę, czyli reszta obliczeń jest niepotrzebna. Z drugiej strony, jeśli a będzie mniejsze od zera, najpierw zostanie obliczone pierwsze podwyrażenie, a następnie drugie, które da prawdę. Ciekawy będzie jednak przypadek, gdy a będzie równe zero – do a zostanie podstawiona jedynka i całość wyrażenia zwróci prawdę (,bo 1 jest traktowane jako prawda).

Efekty uboczne pozwalają na wykonywanie różnych złożonych operacji w samych warunkach logicznych, jednak przesadne używanie tego typu konstrukcji może spowodować, że kod stanie się nieczytelny i jest to uważane za zły styl programistyczny.


Instrukcje warunkowe są jednymi z instrukcji sterujących, czyli fundamentalnych instrukcji w programowaniu. Instrukcje warunkowe pozwalają na pisanie poleceń tak, jak „jeśli ten warunek zostanie spełnione to zrób coś” itp. Dzisiaj poznamy składnię if, if else, if else if else, użycie tej instrukcji warunkowej wygląda następująco:

if (wyrażenie) {
    // blok wykonany, jeśli wyrażenie jest prawdziwe
}
// dalsze instrukcje

Istnieje także możliwość reakcji na nieprawdziwość wyrażenia, wtedy należy zastosować słowo kluczowe else:

if (wyrażenie) {
    // blok wykonany, jeśli wyrażenie jest prawdziwe
} else {
    // blok wykonany, jeśli wyrażenie jest nieprawdziwe
}
// dalsze instrukcje

Dodatkowo możemy zweryfikować kilka wyrażeń, reagować na każde z nich i dodatkowo zareagować, wtedy gdy żadne z nich nie jest spełnione.

if (wyrażenie_1) {
    // blok wykonany, jeśli wyrażenie pierwsze jest prawdziwe
} else if(wyrażenie_2) {
    // blok wykonany, jeśli pierwsze wyrażenie jest nieprawdziwe, a wyrażenie drugie jest prawdziwe
} else {
    // blok wykonany, jeśli żadne z wyrażeń nie jest prawdziwe
}
// dalsze instrukcje

Warto dodać, że można stosować skrócone zapisy w wyrażeniach warunków logicznych tzn. każda wartość różna od zera interpretowana jest jako prawda logiczna, zaś zero jako fałsz.

Zadanie 12

Napisz program w języku C++, który zapamięta wartość dowolnego wyrażenia logicznego, a następnie wyświetli jego wartość logiczną (1 lub 0).

Rozwiązanie:
#include <iostream>
using namespace std;

int main() {
	bool b1 = false, b2 = true; //RÓŻNE WYRAŻENIA LOGICZNE NP. int a1=1, a2=1, a3=0:   	a1 || a2 && a3 - kolejność nawiasów itd.
	b2 = b2+b1;
	if( b2 ) cout << "Suma zmiennych bool " <<  b2 << endl;
	return 0;
}

Zadanie 13 – operator trójargumentowy

Zmodyfikuj poprzedni program tak, aby wyświetlał odpowiednio true i false.

Rozwiązanie:
#include <iostream>
using namespace std;

int main() {
	bool b1 = true;
	cout << (b1 ? "true" : "false");
	return 0;
}
Omówienie:

Czasami zamiast pisać instrukcję if możemy użyć operatora wyrażenia warunkowego tzw. operator trójargumentowy. Zamiast pisać:
if (warunek)
/* A: blok wykonany, jeśli warunek spełniony */
else
/* B: blok wykonany, jeśli warunek nie jest spełniony */

możemy zapisać:
warunek ? (blok A) : (blok B);
Przykład:

int b, a = 1;
if(a != 0)
    b = 1/a;
else
    b = 0;

//lub

b = (a !=0) ? 1/a : 0;

Zadanie 14

Napisz program w języku C++, który pobierze ze standardowego wejścia 3 liczby zmiennoprzecinkowe – długości boków trójkąta, a następnie wyświetli informację, czy z podanych długości można utworzyć trójkąt.

Rozwiązanie:
#include <iostream>

int main() {
    double a, b, c;

    std::cin >> a >> b >> c;

    if (a + b > c && a + c > b && b + c > a) std::cout << "Można utworzyć trójkąt." << std::endl;
    else std::cout << "Nie można utworzyć trójkąta." << std::endl;

    return 0;
}

Zadanie 15

Napisz program w języku C++ wyznaczający relatywistyczny wzrost masy obiektu poruszającego się z prędkością V, pobieranej z klawiatury.

Rozwiązanie:
#include <iostream>
#include <cmath>

int main() {
    double v, m = 1.0, c = 1.0, m_rel;
    std::cin >> v;

    if (v >= 1 || v <= -1) {
        std::cout << "Predkosc musi byc mniejsza od 1 i wieksza od -1." << std::endl;
        return 1;
    }
    m_rel = m/sqrt(1 - v*v/c*c);
    
    std::cout << "Relatywistyczna masa obiektu wynosi: " << m_rel << std::endl;
    
    return 0;
}

Zadanie 16

Prawo Moore’a głosi, że liczba tranzystorów w procesorze podwaja się co 24 miesiące. Jako punkt wyjścia załóżmy procesor Intel 4004 z 1971 roku, który wyposażony był w 2250 tranzystorów. Napisz program w języku C++, który przyjmie ze standardowego wyjścia rok i zwróci liczbę tranzystorów wynikającą z prawa Moore’a. Zweryfikuj założenia prawa Moore’a z danymi historycznymi.

Rozwiązanie:
#include <iostream>
#include <cmath>

int main() {
    int year;
    std::cin >> year;

    if(year < 1971) std::cout << "Rok musi byc wiekszy lub rowny 1971 " << std::endl;
    else {
        int num_years = year - 1971;
        int num_periods = num_years / 2; 
        double num_transistors = 2250 * pow(2, num_periods);

        std::cout << "Liczba tranzystorow w procesorze wynosi: " << num_transistors << std::endl;
    }
    
    return 0;
}

Zadanie 17

Napisz program w języku C++, który pobierze ze standardowego wejścia rok i liczbę tranzystorów w procesorze. Program powinien wyświetlić, jaka jest różnica między liczbą tranzystorów w procesorze, a wynikającą z prawa Moore’a w roku jego produkcji. Wynik należy podać jako liczbę tranzystorów oraz procent liczby tranzystorów rzeczywistego procesora, zaokrąglony do dwóch liczb po przecinku.

Rozwiązanie:
#include <iostream>
#include <cmath>
#include <iomanip>

int main() {
    int year;
    double num_transistors;
    std::cout << "Podaj rok: ";
    std::cin >> year;
    std::cout << "Podaj liczbe tranzystorow: ";
    std::cin >> num_transistors;

    if(year < 1971) {
        std::cout << "Rok musi byc wiekszy lub rowny 1971 " << std::endl;
    } else {
        int num_years = year - 1971;
        int num_periods = num_years / 2; 
        double num_transistors_moore = 2250.0 * pow(2.0, num_periods);
        
        double difference = num_transistors - num_transistors_moore;
        double percentages  = difference / num_transistors_moore * 100;

        std::cout << "Wynikajaca z prawa Moore'a liczba tranzystorow: " << num_transistors_moore << std::endl;
        std::cout << "Roznica wynosi: " << difference << " tranzystorow, co stanowi ";
        std::cout << std::setprecision(2) << std::fixed << percentages << "% liczby tranzystorow rzeczywistego procesora." << std::endl;
    }
    
    return 0;
}

Zadanie 18

Napisz program w języku C++, który przyjmie dwie pary: rok i liczbę tranzystorów opisujące dwa procesory. Program powinien wyświetlić co ile miesięcy podwajałaby się liczba tranzystorów, gdyby wziąć pod uwagę wyłącznie te dwa procesory.

Rozwiązanie:
#include <iostream>
#include <cmath>

int main() {
    int year_0, year_1;
    double num_transistors_0, num_transistors_1;
    
    std::cout << "Podaj rok i liczbe tranzystorow pierwszego procesora: ";
    std::cin >> year_0 >> num_transistors_0;
    std::cout << "Podaj rok i liczbe tranzystorow drugiego procesora: ";
    std::cin >> year_1 >> num_transistors_1;
    
    if(year_0 > year_1) {
        std::swap(year_0, year_1);
        std::swap(num_transistors_0, num_transistors_1);
    }

    int num_years = year_1 - year_0;
    double num_doublings = log2(num_transistors_1 / num_transistors_0);
    double doublings_period = num_years * 12 / num_doublings;

    std::cout << "Liczba podwojen: " << num_doublings << std::endl;
    std::cout << "Okres podwojen w miesiacach: " << doublings_period << std::endl;

    return 0;
}

Zadanie 19

Napisz program w języku C++, który pobierze ze standardowego wejścia znak i wyświetli informację, czy jest to mała litera.

Rozwiązanie:
#include <iostream>

int main() {
    char v;

    std::cin >> v;
    std::cout << (v >= 'a' && v <= 'z' ? "Podany znak jest małą literą." : "Podany znak nie jest małą literą.") << std::endl;

    return 0;
}

Zadanie 20

Napisz program w języku C++, który pobierze od użytkownika 6 liczb zmiennoprzecinkowych będących współrzędnymi (x, y) trzech punktów na płaszczyźnie. Jeżeli leżą one na jednej prostej, program powinien wyświetlić współczynniki a, b równania tej prostej w postaci y = ax + b. W przeciwnym razie program powinien wyświetlić komunikat „niewspółliniowe”.

Rozwiązanie:
#include <iostream>

int main() {
    float a_x, a_y, b_x, b_y, c_x, c_y; 
    std::cin >> a_x >> a_y >> b_x >> b_y >> c_x >> c_y; 
    
    float slope_0 = (b_x - a_x) * (c_y - a_y);
    float slope_1 = (b_y - a_y) * (c_x - a_x);
    float a = (b_y - a_y) / (b_x - a_x);
    if(slope_0 == slope_1)
        std::cout << "y = " << a << "x + " << a_y - a * a_x << std::endl;
    else
        std::cout << "niewspółliniowe\n";

    return 0;
}

Zadanie 21

Napisz program w języku C++, który wczytuje ze standardowego wejścia współczynniki układu dwóch równań liniowych z dwoma niewiadomymi i wypisuje na standardowym wyjściu rozwiązanie układu równań. W przypadku nieskończonej liczby lub braku rozwiązań program powinien wypisać na standardowym wyjściu odpowiednią informację.

Podpowiedź: zaimplementuj algorytm rozwiązywania układów równań metodą wyznaczników (inaczej nazywaną wzorami Cramera).

Rozwiązanie:
#include <iostream>

int main() {
    float a_0, b_0, c_0, a_1, b_1, c_1, determinant, x, y;
    std::cin >> a_0 >> b_0 >> c_0 >> a_1 >> b_1 >> c_1; 

    determinant = a_0 * b_1 - b_0 * a_1;
    x = (c_0 * b_1 - b_0 * c_1);
    y = (a_0 * c_1 - c_0 * a_1);
    if (determinant) 
        std::cout << "x = " << x / determinant<< ", y = " << y / determinant << std::endl;
    else 
        std::cout << (!x && !y ? "Nieskończenie wiele rozwiązań" : "Brak rozwiązań") << std::endl;

    return 0;
}

Zadanie 22

Napisz program w języku C++, który pobierze ze standardowego wejścia dwie liczby całkowite. Program powinien wyświetlić informację czy są one równe.

Rozwiązanie:
#include <iostream>

int main() {
    int a, b;

    std::cin >> a >> b;
    std::cout << (a == b ? "Równe." : "Nierówne.") << std::endl;

    return 0;
}

Zadanie 23

Napisz program w języku C++, który pobierze ze standardowego wejścia dwie liczby zmiennoprzecinkowe. Program powinien wyświetlić ich iloraz, jeżeli druga liczba jest zerem, należy wyświetlić stosowną informację.

Rozwiązanie:
#include <iostream>

int main() {
    float a, b;

    std::cin >> a >> b;
    
    if(b) std::cout << a / b << std::endl;
    else std::cout << "Dzielenie przez 0.\n";

    return 0;
}

Zadanie 24

Napisz program w języku C++, który pobierze ze standardowego wejścia trzy liczby całkowite: a, b, c. Program powinien wyświetlić informację, czy liczba b, jest większa od a, ale mniejsza od c.

Rozwiązanie:
#include <iostream>

int main() {
    int a, b, c;

    std::cin >> a >> b >> c;  
    std::cout << (b > a && b < c ? "Tak" : "Nie") << std::endl;

    return 0;
}

Zadanie 25

Napisz program w języku C++, który pobierze ze standardowego wejścia liczbę całkowitą, a następnie wyświetli czy jest ona podzielna bez reszty przez 6, 3 lub 2.

Rozwiązanie:
#include <iostream>

int main() {
    int value;

    std::cin >> value;
    if (value % 6 == 0) std::cout << "Liczba jest podzielna bez reszty przez 6." << std::endl;
    else if (value % 3 == 0) std::cout << "Liczba jest podzielna bez reszty przez 3." << std::endl;
    else if (value % 2 == 0) std::cout << "Liczba jest podzielna bez reszty przez 2." << std::endl;
    else std::cout << "Liczba nie jest podzielna bez reszty przez 2, 3 ani 6." << std::endl;

    return 0;
}

Zadanie 26

Napisz program w języku C++, który pobierze trzy zmiennoprzecinkowe współczynniki równania kwadratowego i wyświetli rozwiązanie(a) lub informację o braku rozwiązań.

Rozwiązanie:
#include <iostream>
#include <cmath>

int main() {
    double a, b, c;

    std::cin >> a >> b >> c;

    if (a == 0) {
        if (b == 0)
            if (c == 0) 
                std::cout << "Nieskonczenie wiele rozwiazan." << std::endl;
            else 
                std::cout << "Brak rozwiazan." << std::endl;
        else 
            std::cout << "Jedno rozwiazanie: x = " << -c / b << std::endl;
    } else {
        double delta = pow(b, 2) - 4 * a * c;
        if (delta > 0) {
            double x1 = (-b + sqrt(delta)) / (2 * a);
            double x2 = (-b - sqrt(delta)) / (2 * a);
            std::cout << "Dwa rozwiazania: x1 = " << x1 << ", x2 = " << x2 << std::endl;
        } else if (delta == 0) 
            std::cout << "Jedno podwojne rozwiazanie: x = " << -b / (2 * a) << std::endl;
        else 
            std::cout << "Brak rozwiazan." << std::endl;
    }

    return 0;
}

Przygotuj się na kolejne laboratorium!

W celu przygotowania się na kolejne zajęcia, spróbuj wykonać poniższe zadania samodzielnie.

Zadanie 1

Napisz program w języku C++, który pobierze ze standardowego wejścia liczbę całkowitą dodatnią x i wyświetli wszystkie liczby całkowite z jednostronnie domkniętego zakresu [0, x).

Zadanie 2

Zmodyfikuj program aby pobierał liczbę całkowitą (także ujemną) i wyświetlał wszystkie liczby całkowite z zakresu domkniętego [x, 0] lub [0, x].

Zadanie 3

Napisz program w języku C++, który pobierze liczbę całkowitą dodatnią n oraz n liczb całkowitych. Program powinien wyświetlić średnią arytmetyczną tych liczb.

Zadanie 4

Napisz program w języku C++, który pobierze liczbę całkowitą dodatnią n oraz n liczb całkowitych. Program powinien wyświetlić największą spośród tych liczb.

Zadanie 5

Napisz program w języku C++, który będzie pobierał ze standardowego wejścia liczby zmiennoprzecinkowe i sumował je, aż do momentu podania liczby zero. Wówczas program powinien wyświetlić sumę liczb.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *