PP – 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:
#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;
}
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.

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ść. 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.

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 (losowe) wartości. Wszystkie zmienne tworzone bez słowa kluczowego static oraz wewnątrz funkcji (czyli nie-globalne) są tworzone na stosie. Z tego punktu widzenia, funkcja main jest taką samą funkcją, jak każda inna. Tylko obiekty globalne i lokalne statyczne są zerowane (na start). 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.

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 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ć.

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 1+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).

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("%c%c", &v1, &v2);  //Warto sprawdzić, jak program zachowa się przy wczytywaniu wartości za pomocą %d.
    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 – 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ć niejawna (domyślna konwersja przyjęta przez kompilator) lub jawna (wymuszona, zaimplementowana przez programistę).

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* */

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 (tj. konwersja z double do int), jednak 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”. 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.

Zadanie 5 – funkcje matematyczne

Oblicz długość przeciw prostoką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 6

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 7

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 8 – 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++)--; ale to już tak:
    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.

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ą.

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 9 – 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");
}
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.


Wskazówka: Język C wykonuje skrócone obliczenia wyrażeń logicznych. 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 10

Napisz program w języku C++, który zapamięta wartość dozwolonego 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 &amp;&amp; a3 - kolejność nawiasów itd.
	b2 = b2+b1;
	if( b2 ) cout << "Suma zmiennych bool " <<  b2 << endl;
	return 0;
}

Zadanie 11 – 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;

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 *