Przegląd rozdziału
Kolejny rozdział za nami! Pozostaje tylko ten nieznośny quiz do rozwiązania…
Argumenty funkcji można przekazywać przez wartość, referencję lub adres. Użyj przekazywania wartości dla podstawowych typów danych i modułów wyliczających. Użyj przekazywania przez referencję dla struktur, klas lub gdy potrzebujesz funkcji do modyfikacji argumentu. Użyj adresu przekazywanego do przekazywania wskaźników lub tablic wbudowanych. W miarę możliwości spraw, aby Twoje podanie było stałe przez referencję i adres.
Wartości mogą być zwracane przez wartość, referencję lub adres. W większości przypadków zwrot przez wartość jest w porządku, jednak zwrot przez referencję lub adres może być przydatny podczas pracy z dynamicznie alokowanymi danymi, strukturami lub klasami. Jeśli zwracasz przez referencję lub adres, pamiętaj, aby upewnić się, że nie zwracasz czegoś, co wykracza poza zakres.
Wskaźniki funkcji pozwalają nam przekazać funkcję do innej funkcji. Może to być przydatne, aby umożliwić wywołującemu dostosowanie zachowania funkcji, na przykład sposobu sortowania listy.
Pamięć dynamiczna jest alokowana na stercie.
Stos wywołań śledzi wszystkie aktywne funkcje (te, które zostały wywołane, ale jeszcze nie zostały zakończone) od początku programu do bieżącego punktu wykonania. Zmienne lokalne są alokowane na stosie. Stos ma ograniczony rozmiar. std::vector może zostać użyty do zaimplementowania zachowania podobnego do stosu.
Funkcja rekurencyjna to funkcja, która wywołuje samą siebie. Wszystkie funkcje rekurencyjne wymagają warunku zakończenia.
Argumenty wiersza poleceń umożliwiają użytkownikom lub innym programom przekazywanie danych do naszego programu podczas uruchamiania. Argumenty wiersza poleceń są zawsze ciągami znaków w stylu C i jeśli pożądane są wartości liczbowe, należy je przekonwertować na liczby.
Elipsa umożliwia przekazywanie do funkcji zmiennej liczby argumentów. Jednak argumenty wielokropka wstrzymują sprawdzanie typu i nie wiedzą, ile argumentów zostało przekazanych. Śledzenie tych szczegółów zależy od programu.
Funkcje lambda to funkcje, które można zagnieżdżać w innych funkcjach. Nie potrzebują nazwy i są bardzo przydatne w połączeniu z biblioteką algorytmów.
Czas quizu
Pytanie nr 1
Napisz prototypy funkcji dla poniższych przypadków. Użyj const, jeśli/kiedy to konieczne.
a) Funkcja o nazwie max(), która przyjmuje dwie liczby podwójne i zwraca większą z nich.
b) Funkcja o nazwie swap(), która zamienia dwie liczby całkowite.
c) Funkcja o nazwie getLargestElement(), która pobiera dynamicznie przydzieloną tablicę liczb całkowitych i zwraca największą liczbę w taki sposób, że osoba wywołująca może zmienić wartość zwrócony element (nie zapomnij o parametrze długości).
Pytanie nr 2
Co jest nie tak z tymi programami?
A)
int& doSomething()
{
int array[]{ 1, 2, 3, 4, 5 };
return array[3];
}B)
int sumTo(int value)
{
return value + sumTo(value - 1);
}C)
float divide(float x, float y)
{
return x / y;
}
double divide(float x, float y)
{
return x / y;
}D)
#include <iostream>
int main()
{
int array[100000000]{};
for (auto x: array)
std::cout << x << ' ';
std::cout << '\n';
return 0;
}mi)
#include <iostream>
int main(int argc, char* argv[])
{
int age{ argv[1] };
std::cout << "The user's age is " << age << '\n';
return 0;
}Pytanie nr 3
Najlepszy algorytm sprawdzania, czy wartość istnieje w posortowanej tablicy, nazywa się wyszukiwaniem binarnym.
Wyszukiwanie binarne działa w następujący sposób:
- Spójrz na środkowy element tablicy (jeśli tablica ma parzystą liczbę elementów, zaokrąglij w dół).
- Jeśli środkowy element jest większy niż element docelowy, odrzuć górną połowę tablicy (lub wykonaj rekurs na dolną połowę)
- Jeśli środkowy element jest mniejszy niż element docelowy, odrzuć dolną połowę tablicy (lub powtórz operację na górnej połowie).
- Jeśli środkowy element jest równy element docelowy, zwróć indeks elementu centralnego.
- Jeśli odrzucisz całą tablicę bez znalezienia elementu docelowego, zwróć wskaźnik reprezentujący „nie znaleziono” (w tym przypadku użyjemy -1, ponieważ jest to nieprawidłowy indeks tablicy).
Ponieważ w każdej iteracji możemy wyrzucić połowę tablicy, algorytm ten jest bardzo szybki. Nawet w przypadku tablicy składającej się z miliona elementów wystarczy maksymalnie 20 iteracji, aby określić, czy wartość istnieje w tablicy, czy nie! Działa to jednak tylko na posortowanych tablicach.
Modyfikowanie tablicy (np. odrzucenie połowy elementów tablicy) jest kosztowne, dlatego zazwyczaj nie modyfikujemy tablicy. Zamiast tego używamy dwóch liczb całkowitych (min i max) do przechowywania indeksów minimalnego i maksymalnego elementu tablicy, którą chcemy zbadać.
Przyjrzyjmy się próbce działania tego algorytmu, biorąc pod uwagę tablicę { 3, 6, 7, 9, 12, 15, 18, 21, 24 } i wartość docelową 7. Na początku min = 0, max = 8, ponieważ przeszukujemy całą tablicę (tablica ma długość 9, więc indeks ostatniego elementu wynosi 8).
- Zaliczono 1) Obliczamy środek min (0) i max (8), czyli 4. Element nr 4 ma wartość 12, która jest większa niż nasza wartość docelowa. Ponieważ tablica jest posortowana, wiemy, że wszystkie elementy o indeksie równym lub większym od środka (4) muszą być za duże. Więc zostawiamy min w spokoju i ustawiamy max na 3.
- Zaliczony 2) Obliczamy punkt środkowy min (0) i max (3), czyli 1. Element nr 1 ma wartość 6, która jest mniejsza niż nasza wartość docelowa. Ponieważ tablica jest posortowana, wiemy, że wszystkie elementy o indeksie równym lub mniejszym od środka (1) muszą być za małe. Zatem ustawiamy min na 2, a max zostawiamy w spokoju.
- Zaliczono 3) Obliczamy punkt środkowy min (2) i max (3), czyli 2. Element nr 2 ma wartość 7, która jest naszą wartością docelową. Zwracamy więc 2.
W C++20 obliczenie punktu środkowego można wykonać poprzez wywołanie std::midpoint. Przed wersją C++20 musisz samodzielnie obliczyć punkt środkowy — w tym przypadku możesz użyć min + ((max - min) / 2) aby uniknąć przepełnienia (gdy min i max są dodatnie).
Biorąc pod uwagę następujący kod:
#include <iostream>
#include <iterator>
// array is the array to search over.
// target is the value we're trying to determine exists or not.
// min is the index of the lower bounds of the array we're searching.
// max is the index of the upper bounds of the array we're searching.
// binarySearch() should return the index of the target element if the target is found, -1 otherwise
int binarySearch(const int* array, int target, int min, int max)
{
}
int main()
{
constexpr int array[]{ 3, 6, 8, 12, 14, 17, 20, 21, 26, 32, 36, 37, 42, 44, 48 };
// We're going to test a bunch of values to see if the algorithm returns the result we expect
// Here are the test values
constexpr int testValues[]{ 0, 3, 12, 13, 22, 26, 43, 44, 48, 49 };
// And here are the results that we expect for those test values
int expectedResult[]{ -1, 0, 3, -1, -1, 8, -1, 13, 14, -1 };
// Make sure we have the same number of test values and expected results
static_assert(std::size(testValues) == std::size(expectedResult));
int numTestValues { std::size(testValues) };
// Loop through all of the test values
for (int count{ 0 }; count < numTestValues; ++count)
{
// See if our test value is in the array
int index{ binarySearch(array, testValues[count], 0, static_cast<int>(std::size(array)) - 1) };
// If it matches our expected value, then great!
if (index == expectedResult[count])
std::cout << "test value " << testValues[count] << " passed!\n";
else // otherwise, our binarySearch() function must be broken
std::cout << "test value " << testValues[count] << " failed. There's something wrong with your code!\n";
}
return 0;
}a) Napisz iteracyjną wersję funkcji binarySearch.
Wskazówka: możesz bezpiecznie powiedzieć, że element docelowy nie istnieje, gdy indeks min jest większy niż maksymalny indeks.
b) Napisz rekurencyjną wersję funkcji binarySearch.
Wskazówka
std::binary_search zwraca wartość true, jeśli wartość istnieje na posortowanej liście.std::equal_range zwraca iteratory do pierwszego i ostatniego elementu o podanej wartości.
Nie używaj tych funkcji do rozwiązywania quizu, ale użyj ich w przyszłości, jeśli potrzebujesz pliku binarnego search.

