Kurs podstawy programowania UMCS, zdjęcie laptopa.

Podstawy programowania – Laboratorium 8

Zadanie 1 – wskaźniki

Napisz program w języku C++, który modyfikuje wartość zmiennej za pomocą wskaźnika.

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

int main() {
    int a = 5;
    
    int *ptr = &a;
    cout << a << endl;
    
    *ptr = 10;
    cout << a << endl;
    return 0;
}
Omówienie:

Wskaźnik to specjalny rodzaj zmiennej, w której zapisany jest adres komórki pamięci komputera. Innymi słowy, zmienna typu wskaźnikowego wskazuje na pewną lokalizację w pamięci, w której przechowywane są informacje. Lub jeszcze inaczej, wartością zmiennej wskaźnikowej jest adres pamięci.

Adres pamięci to pewna liczba całkowita, jednoznacznie definiująca położenie pewnego obiektu w pamięci komputera. Takimi obiektami mogą być np. zmienne, elementy tablic, funkcje. Każdy z tych obiektów jest przechowywany w pamięci i ma swój adres.

Podstawowe symbole dla wskaźników:

symbol      znaczenie                                   użycie
*           deklaracja wskaźnika do wartości            int *x;
*           weź wartość x (wyłuskanie/dereferencja)     *x
&           weź adres                                   &x

Mimo, że wskaźnik jest zawsze typu adresowego, kompilator wymaga przy deklaracji podania typu zmiennej, na którą wskaźnik będzie wskazywał. Wskaźnik deklarujemy poprzez określenie typu i dodanie * (gwiazdki) przed nazwą wskaźnika:
int *ptr;
Jako, że wskaźnik nie jest nowym typem danych, należy uważać na następującą implementację:
int *a, b, c;
W powyższej sytuacji uzyskamy tylko jeden wskaźnik a, oraz dwie liczby całkowite b i c. Aby stworzyć trzy wskaźniki należy postawić gwiazdkę przed każdym identyfikatorem:
int *a, *b, *c;
Aby uniknąć pomyłek, lepiej jest pisać gwiazdkę tuż przy zmiennej, albo jeszcze lepiej – nie mieszać deklaracji wskaźników i zmiennych:
int *a, *b;
int c, d;

Uwaga:

Wskaźnik zawsze wskazuje na jakieś miejsce w pamięci. Dodatkowo możemy tworzyć wskaźniki zainicjalizowane zerem. Taki wskaźniki nazywamy wskaźnikiem pustym. Pusty wskaźnik wskazuje na adres zerowy. Dokładniej, NULL jest zadeklarowaną stałą (makrem) przez dyrektywę preprocesora, zaś nullptr jest wbudowaną w język wartością. Z drugiej strony wskaźnik niezainicjalizowany – wskazuje na przypadkową lokalizację i tego należy unikać, bo nie da się rozróżnić takiego wskaźnika od wskaźnika, który wskazuje na określoną przez nas lokalizację.

int *ptr1 = 0;
int *ptr2 = NULL;
int *ptr3 = nullptr; // C++11

Od standardu C++11 wprowadzono nullptr jako bardziej bezpieczny wskaźnik pusty w porównaniu do starszego NULL. nullptr jest bezpieczniejszy, ponieważ jest rzeczywistym typem wskaźnika (wskaźnikiem pustym), co oznacza, że kompilator może rozpoznać jego kontekst w wyrażeniach logicznych, np. przy przeciążeniach funkcji. NULL to makrodefinicja stałej 0, co może prowadzić do niejednoznaczności w niektórych przypadkach.

Ważną informacją, oprócz samego typu wskazywanej zmiennej, jest adres tej zmiennej. W celu uzyskania adres jakiejkolwiek zmiennej używamy operatora & (operator pobierania adresu). Podsumowując, aby uniknąć błędów, dobrą praktyką jest inicjalizować wskaźniki konkretną wartością (adres obiektu) lub zerem (0, NULL, nullptr).

Przykład:
int a = 10;
int *ptr = &a;

Należy dodać, że rozmiar wskaźnika zależy od architektury systemu. Na systemie 32-bitowym wskaźnik ma zazwyczaj 4 bajty, natomiast na 64-bitowym systemie – 8 bajtów. Jest to rozmiar samego adresu, a nie wartości przechowywanej pod tym adresem.


Aby dobrać się do wartości wskazywanej przez wskaźnik, należy użyć unarnego operatora * (gwiazdka), zwanego operatorem wyłuskania lub operatorem dereferencji. Mimo, że kolejny raz pojawia się symbol gwiazdki, oznacza ona teraz coś zupełnie innego. Jest tak, ponieważ używamy jej w zupełnie innym miejscu: nie przy deklaracji zmiennej (gdzie gwiazdka oznacza deklarowanie wskaźnika), a przy wykorzystaniu zmiennej, gdzie odgrywa rolę operatora, podobnie jak operator & (pobrania adresu obiektu). Program ilustrujący:

#include <cstdio>

int main() {
    int a = 5;
    int *ptr = &a;
    printf("wartość zmiennej: %d, wartość wskazywana przez wskaźnik: %d\n", a, *ptr);
    *ptr = 10;
    printf("wartość zmiennej: %d, wartość wskazywana przez wskaźnik: %d\n", a, *ptr);
    a = 12; //0x42
    printf("wartość zmiennej: %d, wartość wskazywana przez wskaźnik: %d\n", a, *ptr);
    return 0;
}

Kod ilustrujący porównanie adresu wskaźnika, adresu zmiennej, wartości wskazywanej przez wskaźnik zmiennej, wartość wskaźnika i wartość zmiennej:

#include <cstdio>

int main() {
    int a = 5;
    int *ptr = &a;
    printf("wartość a: %d, wartość wskazywana przez wskaźnik: %d, adres zmiennej: %d, adres wskaźnika: %d, wskazywany adres przez wskaźnik: %d\n", a, *ptr, &a, &ptr, ptr);
    return 0;
}

Zadanie 2 – wskaźniki cd.

Napisz funkcje w języku C++, która zamienia wartościami dwie liczby rzeczywiste. Napisz program, który przetestuje działanie tej funkcji.

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

void swap1(float *a, float *b) { // przekazywanie przez wskaźnik
    float t = *a;
    *a = *b;
    *b = t;
}
// LUB
void swap2(float &a, float &b) { // przekazywanie przez referencje
    float t = a;
    a = b;
    b = t;
}

//Gdybyśmy mówili o liczbach całkowitych moglibyśmy pozwolić sobie na poniższy zapis.
void swap3(int *x, int *y) {
    if (x != y) {
        *x ^= *y;
        *y ^= *x;
        *x ^= *y;
    }
}

int main() {
    float a = 5.f, b = 10.f;
    cout << a << " " << b << endl;
    swap1(&a, &b);
    cout << a << " " << b << endl;
    swap2(a, b);
    cout << a << " " << b << endl;
    int i_a = a, i_b = b;
    swap3(&i_a, &i_b);
    cout << i_a << " " << i_b << endl;
    return 0;
}
Omówienie:

Argumentami funkcji mogą być również wskaźniki. W przypadku zwykłych zmiennych, nasza funkcja otrzymuje jedynie lokalne kopie argumentów, które zostały jej podane. Wszelkie zmiany dokonują się lokalnie i nie są widziane poza funkcją.

Przekazując do funkcji wskaźnik, również zostaje stworzona kopia wskaźnika, na którym możemy operować. Jednakże, ta nowo utworzona kopia wskaźnika, będzie zawierała ten sam adres, co wskaźnik przekazany do funkcji, czyli oba wskaźniki będą wskazywały na ten sam obiekt. Nie jest natomiast kopiowany obiekt, na który wskazuje ten wskaźnik. Obiekt ten znajduje się gdzieś w pamięci i możemy na nim działać, tak więc zmiany wykonane na tym obiekcie będą widoczne globalnie.

#include <iostream>
using namespace std;

void f_var(int v) {
    v = 4;
}

void f_ptr(int *v) {
    (*v) = 5;
}

void f_ref(int &v) {
    v = 7;
}

int main (void)
{
    int z = 3;
    cout << "z = " << z << endl;
    f_var(z);
    cout << "z = " << z << endl;
    f_ptr(&z);
    cout << "z = " << z << endl;
    f_ref(z);
    cout << "z = " << z << endl;
    return 0;
}

W powyższym kodzie w funkcji f_ptr modyfikujemy wartość zmiennej, wskazywanej przez wskaźnik a nie wartość wskaźnika, czyli adres na który wskazuje.

Język C/C++ poza zwracaniem wartości za pomocą instrukcji return, umożliwia także modyfikowanie danych, przekazanych jako argumenty. Ten sposób przekazywania argumentów do funkcji, przedstawiony w powyższym kodzie (dla funkcji f_ptr), jest nazywany przekazywaniem przez wskaźnik. Należy pamiętać, by do funkcji oczekującej wskaźnika przekazać adres zmiennej f_ptr(&z);, a nie samą zmienną.


Przekazywanie przez wskaźnik: Argumenty wskaźnikowe wskazują na zmienne spoza funkcji, więc wewnątrz funkcji nie są tworzone kopie tych zmiennych, pozwala to na modyfikowanie wielu zmiennych spoza funkcji, bez użycia instrukcji return. Należy pamiętać, że wskaźnik przekazywany jest przez wartość, wynika to z faktu, że wskaźnik też jest typem zmiennej (zmienna wskaźnikowa). Jednakże mimo, że wskaźnik (adres miejsca w pamięci) zostanie skopiowany to wartość wyłuskana ze wskaźnika nie jest już kopią.

Przekazywanie przez referencję: Funkcja f_ref przedstawia kolejną możliwość przekazywania argumentów do funkcji – przekazywanie przez referencję. Aby przekazać argument przez referencję należy poprzedzić nazwę argumentu symbolem &, wtedy zmienne wewnątrz funkcji nie są kopią. Oznacza to, że operując na zmiennych referencyjnych operujemy także na zmiennej oryginalnej. Referencja występuje jedynie w języku C++ (oraz innych językach wysokiego poziomu tj. C# i Java). Wszystko odbywa się na takiej samej zasadzie, jak w przypadku wskaźników. Sama referencja także bardzo przypomina wskaźnik.

Różnica pomiędzy referencją a wskaźnikiem: Wskaźnik jest zmienną wskaźnikową. Do wskaźnika możemy przypisać dowolny adres pamięci, na którą ten wskaźnik będzie wskazywał. Skoro wskaźnik jest rodzajem zmiennej, to w dowolnym momencie możemy zmienić adres na jaki wskazuje. Wskaźnik może być również inicjalizowany zerem lub być niezainicjalizowany, co oczywiście niesie pewne ryzyko błędu. Referencja natomiast jest bezpośrednim adresem pamięci danej zmiennej. Ujmując to trochę inaczej referencja jest aliasem do isntiejącej zmiennej. Oznacza to, że nie można jej zmienić, skasować ani uszkodzić oraz musi być zainicjalizowana w momencie deklaracji. Dodatkowo referencja zawsze „wskazuje” na ten sam obiekt, przez cały czas swojego istnienia, czego wskaźnik robić nie musi. Usunięcie wskaźnika, nie spowoduje utraty zmiennej na jaką wskazywał. Jakiekolwiek nadpisanie danych pod referencją (adresem zmiennej), spowoduje natychmiastową utratę poprzednich informacji.

Ponadto warto zauważyć, że przekazywanie argumentów przez referencję jest łatwiejsze, ponieważ nie trzeba operować na * i &. To jedyna różnica między referencją a wskaźnikami w argumentach funkcji.


Podczas korzystania ze wskaźników należy uważać na możliwość odwołania się do komórki wskazywanej przez wskaźnik o wartości NULL (nullptr, 0), czy odwołania się do wskaźnika niezainicjowanego. Prowadzić to będzie do pojawienia się błędów w naszym programie:

int *wsk;
cout << *wsk << endl;
wsk = NULL;
cout << *wsk << endl;

Operator sizeof dla argumentu będącego wskaźnikiem będzie zwracał wielkość, która będzie oznaczała rozmiar adresu, a nie rozmiar typu użytego podczas deklarowania wskaźnika. Wielkość ta będzie zawsze miała taki sam rozmiar dla każdego wskaźnika, w zależności od kompilatora, a także docelowej platformy i architektury systemu.

char *var;
int z = sizeof(var);  //wielkość adresu
z = sizeof(char*);  //to samo, co wyżej
z = sizeof(*var);  //wielkość typu zmiennej, na którą wskazuje wskaźnik
z = sizeof(char);  //to samo, co wyżej

Typ wskaźnika, jest istotnym elementem składni języka C/C++. Należy pamiętać, że różne typy zajmują w pamięci różną wielkość. Tym samym typ wskaźnika służy do tego, aby odpowiednio odczytywać dane znajdujące się pod wskazanym adresem. Odwoływanie się do zmiennej za pomocą wskaźnika innego typu może prowadzić do tego, że odczytamy tylko część bitów, bądź co gorsze, gdy użyjemy wskaźnika typu zajmującego więcej bajtów w pamięci, odczytane zostaną bajty z niewiadomą zawartością. Może to prowadzić do nieprzyjemnych skutków takich, jak zmiana wartości innych zmiennych, czy wręcz natychmiastowe przerwanie programu.

#include <iostream>
using namespace std;

int main()
{
    unsigned char tab[10] = {100, 101, 102, 103, 104, 105, 106, 107, 108, 109};
    unsigned short *ptr = (unsigned short*)&tab[2];
    unsigned i;

    *ptr = 0xffff;
    for (i = 0; i < 10; ++i) {
        cout << (int)tab[i] << endl;
        tab[i] = tab[i] - 100;
    }
    cout << "poza tablica: " << (int)tab[10] << endl;
    tab[10] = -1;
    return 0;
}

Ciekawostka: Niektóre architektury wymagają odpowiedniego wyrównywania danych wielobajtowych w pamięci np. zmienna dwubajtowa może się znajdować jedynie pod parzystymi adresami. Wówczas, podczas przypisania do adres zmiennej jednobajtowej wskaźnika na zmienną dwubajtową mogłoby dojść do nieprzewidzianych błędów wynikających z próby odczytu niewyrównanej danej.

Co ważniejsze, nie należy beztrosko rzutować wyrażeń pomiędzy różnymi typami wskaźnikowymi, bo grozi to nieprzewidywalnymi błędami.

Zadanie 3 – operacje arytmetyczne na wskaźnikach

Napisz funkcję w języku C++, która przyjmuje jako argument wskaźnik na początek i koniec tablicy liczb całkowitych. Funkcja powinna wyświetlić wszystkie elementy tej tablicy. Napisz program w języku C++, który przetestuje działanie tej funkcji.

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

void printArray(int *begin, int *end) {
    for(int *i = begin; i <= end; ++i)
        cout << *i << endl;
}

int main() {
    int arr[5] = {4, 2, 8, 1, 3};
    printArray(arr, &arr[4]);
    return 0;
}
Omówienie:

W języku C/C++ można wykonać operację arytmetyczną wskaźnika i liczby całkowitej. Dozwolone są następujące operacje: dodawanie liczby całkowitej do wskaźnika, odejmowanie liczby całkowitej od wskaźnika. Zmiana adresu jest ściśle związana z typem obiektu, na który wskazuje wskaźnik. Dodanie do wskaźnika liczby 2 nie spowoduje przesunięcia się w pamięci komputera o dwa bajty a przesunięcie się o dwukrotność rozmiaru typu zmiennej.

Zakładając, że int zajmuje 4 bajty, poniższa operacja ptr += 2; spowoduje przesunięcie wskaźnika o 2 * 4 = 8 bajtów.

int *ptr;
int a[] = {1, 2, 3, 5, 7};
ptr = a; /* &a[0] */
cout << *ptr << endl;
 
ptr += 2;
cout << *ptr << endl;

Uzupełniając dwóch wskaźników nie można do siebie dodać ale można je od siebie odejmować, czego wynikiem jest odległość dwóch wskazywanych wartości. Odległość zwracana jest jako liczba obiektów danego typu, a nie liczba bajtów.

int a[] = {1, 2, 3, 5, 7};
int *ptr = a + 2; //lub int *ptr = a; a += 2;

for(int i = 0; i < 5; ++i) {
    printf("Adres elementu %d: %p\n", i, &a[i]);
    printf("Adres elementu %d: %p\n", i, ptr + i);
    printf("Wartosc elementu %d: %d\n", i, a[i]);
    printf("Wartosc elementu %d: %d\n", i, *(ptr + i));
}

int diff = ptr - a;  /* diff ma wartość 2 (a nie 2*sizeof(int)) */

Wynikiem może być oczywiście liczba ujemna. Operacja jest przydatna do obliczania wielkości tablicy (długości łańcucha znaków) jeżeli mamy wskaźnik na jej pierwszy i ostatni element.

int tablica[2] = {1, 2};
int * ptr_b = tablica;
int * ptr_e = tablica + 2;

std::cout << ptr_b << '\n' << ptr_e << std::endl;
std::cout << ptr_e - ptr_b << std::endl;

Operacje arytmetyczne na wskaźnikach są operacjami, przy których powinniśmy zachować szczególną ostrożność. Należy zadbać o poprawność takich operacji. Pomyłka, próba zapisu lub odczytu do nieodpowiednich komórek pamięci, prowadzi do błędnego wykonania programu lub do jego zakończenia przez system operacyjny.

int a[] = {1, 2, 3, 5, 7};
int *ptr;
ptr = a + 10; /* niezdefiniowane */
ptr = a - 10; /* niezdefiniowane */
ptr = a + 5;  /* zdefiniowane (element za ostatnim) */
*ptr = 10;    /* źle! */

Inną operacja, której powinniśmy unikać jest odejmowanie od siebie wskaźników wskazujących na obiekty znajdujące się w różnych tablicach, np.:

int a[] = {1, 2, 3}, b[] = {5, 7};
int *ptr1 = a, *ptr2 = b;
int diff = ptr1 - ptr2;

Zadanie 4

Napisz funkcję rekurencyjną w języku C++, która oblicza rozmiar napisu (char str[]). Napisz program w języku C++, który przetestuje działanie tej funkcji.

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

int f_rec(char *a, int num) {
    return *a ? f_rec(a + 1, num + 1) : num;
}

int main() {
    char arr[] = "12345";
    cout << f_rec(arr, 0);
    return 0;
}
Omówienie:

Należy pamiętać, by do funkcji przekazać adres elementu a nie sam element. Aby uzyskać adres pierwszego elementu tablicy wystarczy napisać po prostu nazwę naszej tablicy: arr.

W związku z tym, powyższy zapis a + 1 najpierw pobiera adres pierwszego elementu tablicy, a następnie za sprawą operacji + 1 przechodzi do adresu kolejnego elementu tablicy. Wynika to z wcześniej omawianej operacji arytmetycznej wskaźnika i liczby całkowitej. W ten sposób, do funkcji f_rec przekazano adres kolejnego elementu tablicy.

Zadanie 5

Napisz funkcję w języku C++, która przyjmie jako argumenty automatyczną tablicę wartości całkowitych, jej rozmiar oraz poszukiwaną wartość.
Funkcja powinna zwrócić wskaźnik na pierwszą komórkę tablicy, w której znajdzie tą wartość. Jeżeli nie ma takiej liczby w tablicy, należy zwrócić wskaźnik na NULL.

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

int *f(int arr[], int n, int a) {
    for(int i = 0; i < n; ++i)
        if(arr[i] == a)
            return arr + i;
    return NULL;
}


int main() {
    int arr[5]={3, 6, 9, 5, 2};
    int *result = f(arr, 5, 5);
    if(result != NULL)  cout << "Wartosc: " << *result << " Pozycja: " << result - arr  << endl;
    return 0;
}

Zadanie 6

Napisz funkcję w języku C++, która przyjmuje w argumencie liczbę całkowitą oraz zmienną bool przez referencję. Funkcja powinna sprawdzić, za pomocą operacji bitowych, czy cyfry przekazanej liczby całkowitej są parzyste. Ponadto funkcja powinna ustawić przekazaną zmienną typu bool na wartość true, jeśli wszystkie cyfry są parzyste i false w przeciwnym wypadku.

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

void f(int number, bool &result) {
    while(number) {
        if(number % 10 & 1) {
            result = false;
            return;
        }
        number /= 10;
    }
    result = true;
}

int main() {
    bool result;
    f(212222, result);
    cout << result << endl;
    return 0;
}

Zadanie 7

Zmodyfikuj poprzedni program tak, aby zmienna typu bool w argumencie była przekazywana przez wskaźnik.

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

void f(int number, bool *result) {
	while(number) {
		if(number % 10 & 1) {
			*result = false;
			return;
		}
		number /= 10;
	}
	*result = true;
}

int main() {
    bool result;
	f(212222, &result);
	cout << result << endl;
    return 0;
}

Zadanie 8

Napisz funkcję w języku C++, która dostaje dwa argumenty: wskaźnik na stałą typu int i stały wskaźnik na zmienną typu int. Funkcja powinna przepisać zawartość stałej wskazywanej przez pierwszy argument do zmiennej wskazywanej przez drugi argument. Napisz program, który przetestuje działanie tej funkcji.

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

void f(int const *a, int *const b) {
	*b = *a;
}

int main() {
    const int v1 = 11;
    int v2 = 12;
    int const *a = &v1;
    int *const b = &v2;
	
	f(a, b);
	cout << v1 << " " << v2 << endl;
	
    return 0;
}
Omówienie:

Podobnie jak możemy deklarować zwykłe stałe, tak samo możemy mieć stałe wskaźniki, jednak są ich dwa rodzaje. Wskaźniki na stałą wartość:

const int *a;
int const * a;

oraz stałe wskaźniki:

int * const b;

Słówko const przed typem działa jak w przypadku zwykłych stałych tzn. nie możemy zmienić wartości wskazywanej przy pomocy wskaźnika. W drugim przypadku słowo const jest tuż za gwiazdką oznaczającą typ wskaźnikowy, co skutkuje stworzeniem stałego wskaźnika, czyli takiego którego nie można przestawić na inny adres. Obie opcje można połączyć, deklarując stały wskaźnik, którym nie można zmienić wartości wskazywanej zmiennej, i również można zrobić to na dwa sposoby:

const int * const c;
int const * const c;

Zadanie 9

Napisz odpowiednie funkcje oraz program w języku C++, które umożliwią stworzenie kopii wczytanego od użytkownika napisu i sprawdzenie jego rozmiaru. Napis nie może być dłuższy niż 100 znaków. Do zaimplementowania powyższego wykorzystaj arytmetykę wskaźników.

Rozwiązanie:
#include <cstdio>
int str_len(char *str) {
    int len = 0;
    while(*(str + len++));
    return len - 1;
}

void str_cpy(char *src, char *dst) {
    while(*dst++ = *src++);
}

int main() {
    char str[101], str_tmp[101];
    scanf("%100s", str);

    str_cpy(str, str_tmp);

    printf("%s\n%d\n", str_tmp, str_len(str));
    return 0;
}

Zadanie 10

Napisz funkcję w języku C++, która przyjmuje jako argumenty wskaźniki do trzech zmiennych typu int. Funkcja powinna do trzeciego argumentu zapisać sumę wartości z liczb wskazywanych przez dwa poprzednie argumenty. Napisz program w języku C++, który przetestuje działanie tej funkcji.

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

void f(const int *a, const int *b, int *result) {
    *result = *a + *b;
}

int main() {
    int result, a = 7, b = 6;
    int *ptr = &result;
    f(&a, &b, ptr);
    cout << result << endl;
    return 0;
}

Zadanie 11

Zmodyfikuj poprzedni program tak aby korzystał z referencji.

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

void f(int &a, int &b, int &result) {
	result = a + b;
}

int main() {
    int result, a = 7, b = 6;
	f(a, b, result);
    cout << result << endl;
    return 0;
}

Zadanie 12

Napisz funkcję w języku C++, która przyjmuje jako argumenty automatyczną tablicę wartości całkowitych, jej rozmiar oraz referencję do zmiennej logicznej. Funkcja powinna posortować tablicę w taki sposób, że wartości nieparzyste znajdą się w początkowej części tablicy, a parzyste w końcowej. Kolejność wartości wewnątrz tych grup jest dowolna. Funkcja powinna zwrócić wskaźnik na komórkę tablicy, w której pojawi się pierwsza liczba parzysta. Zmiennej przekazanej przez referencję podany w argumencie, należy przypisać prawdę, jeżeli funkcja dokonała w tablicy jakąkolwiek zmianę, a fałsz w przeciwnym przypadku. W zadaniu wykorzystaj wskaźnikowy sposób indeksowania tablicy. Dla ułatwienia załóżmy, że w tablicy jest co najmniej jedna wartość parzysta i nieparzysta.

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

void print_array(int *begin, int *end) {
    for(int *it = begin; it < end; ++it)
        cout << *it << " ";
    cout << endl;
}

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int *f(int arr[], int n, bool &changed) {
    int *even = nullptr;
    changed = false;
    do {
        for(int i = 0; i < n - 1; ++i) {
            if(*(arr + i) % 2 == 0 && *(arr + i + 1) % 2 == 1) {
                swap(arr + i, arr + i + 1);
                even = arr + i + 1;
                changed = true;
            }
        }
        n--;
    } while(n > 0);
    return even;
}

int main() {
    bool c;
    int arr[7] = {1, 4, 3, 5, 2, 7, 8};
    int *even = f(arr, 7, c);
    print_array(arr, even);
    print_array(even, arr + 7);
    print_array(arr, arr + 7);
    return 0;
}

Zadanie 13

Napisz program w języku C++, który wyświetli wartość liczby typu float ale interpretowanej jak gdyby była ona liczbą typu unisigned int. Użyj odpowiednich rzutowań wskaźnikowych.

Rozwiązanie:
#include <iostream>

int main() {
    float value = 6.5f;
    float *f_ptr = &value;
    std::cout << *(unsigned int *)f_ptr << std::endl;
    return 0;
}

Zadanie 14

Napisz funkcję w jeżyku C++, która przyjmuje w argumentach liczbę 4-bajtową całkowitą nieujemną word oraz cztery wskaźniki na 1-bajtowe liczby całkowitą nieujemne b0, b1, b2, b3. Funkcja ma wpisać w argumenty wskazywane wskaźnikami b0..b3 wartości kolejnych bajtów z liczby word. Zadanie zrealizuj za pomocą arytmetyki wskaźnikowej i odpowiedniego ich rzutowania. Przetestuj działanie tej funkcji.

Rozwiązanie:
#include <iostream>

typedef unsigned char uchar;
typedef unsigned int uint;

void f(uint word, uchar *b0, uchar *b1, uchar *b2, uchar *b3) {
    *b0 = *((uchar*)&word + 0);
    *b1 = *((uchar*)&word + 1);
    *b2 = *((uchar*)&word + 2);
    *b3 = *((uchar*)&word + 3);
}

int main() {
    uint value = 0x0A0B0C0D;
    uchar v0 = 0, v1 = 0, v2 = 0, v3 = 0;
    uchar *b0 = &v0, *b1 = &v1, *b2 = &v2, *b3 = &v3;
    f(value, b0, b1, b2, b3);
    std::cout << (int)v0 << " " << (int)v1 << " " << (int)v2 << " " << (int)v3 << "\n";
    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 funkcję w języku C++, która przyjmie jako parametry wskaźniki na pierwszy i ostatni element tablicy. Bez użycia operatora subskryptowego [], a jedynie korzystając z arytmetyki wskaźników, wyświetl zawartość tablicy.

Zadanie 2

Napisz program w języku C++, który utworzy wskaźnik na liczbę całkowitą. Następnie program powinien zaalokwoać dynamicznie pamięć dla tej zmiennej za pomocą operatora new i przypisać ją do tego wskaźnika. Na koniec program powinien ustawić wartość tej zmiennej, wyświetlić ją, a następnie skasować.

Zadanie 3

Napisz program w języku C++, który będzie pobierał ze standardowego wejścia dodatnią liczbę całkowitą n. Następnie program powinien utworzyć wskaźnik na liczbę całkowitą i zaalkować dynamicznie pamięć dla n-elementowej tablicy liczb całkowitych za pomocą operatora new. Na koniec program powinien wypełnić tablicę dowolnymi wartościami, wyświetlic ją i skasować.

Zadanie 4

Zmodyfikuj poprzedni program tak, aby obliczył sumę wszystkich liczb nieparzystych w stworzonej tablicy, a następnie wyświetli wynik na standardowym wyjściu. Na koniec program powinien stworzyć nową tablicę, do której przepisze wartości nieparzyste i wyświetlić tą tablicę.

Zadanie 5

Rozmiar pamięci zaalokowanej za pomocą funkcji malloc można zmienić funkcją realloc. Dla pamięci zaalokowanej operatorem new nie ma odpowiednika funkcji realloc. Napisz funkcję w języku C++, która spełni takie zadanie dla tablicy typu int. Funkcja powinna przyjąć jako parametry: wskaźnik na dynamicznie zaalokowaną tablicę oraz dwie liczby całkowite s i n określające stary i nowy rozmiar tablicy. Funkcja powinna dynamicznie utworzyć nową tablicę. Jeżeli s > n, do nowej tablicy należy przepisać n początkowych komórek starej tablicy. Jeżeli n > s, należy przepisać wszystkie komórki, a w pozostałych komórkach nowej tablicy pozostawić przypadkowe wartości. Funkcja powinna skasować starą tablicę oraz zwrócić wskaźnik na nową tablicę.

Dodaj komentarz

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