[itk] Számítástechnika kezdőknek

C++ programozás kezdőknek - függvények

[2022. január 06.] [ christo161 ]

Ebben a tananyagrészben arról lesz szó, hogyan tudunk egy bizonyos kódrészletet a program forráskódjának különböző pontjain végrehajtani, anélkül, hogy ismételten leírnánk. Ezt függvények segítségével oldhatjuk meg.
Bár a függvényeknek nem csak akkor van értelme, ha többször használjuk őket. A kód részfeladatokra bontását is ellátják, ami akkor is átláthatóbbá teszi a kódot, ha van olyan részfeladat, amit csak egyszer hajtunk végre a kódban.
Fontos, hogy egy függvény csak egy feladatot hajtson végre (pl. egy tömb kiíratása vagy számok átlagának kiszámítása), egy függvényen belül ne helyezzünk el több feladatot is végrehajtó utasításokat.

előző tananyagrész: string-szám, szám-string konverziók
következő tananyagrész: osztályok/objektumok

Tartalom

Alapvető tudnivalók

Az első program c. tananyagrész elején már szó esett a függvényekről. Egyrészt minden C++ program forráskódjában szerepelnie kell egy main nevű függvénynek, ami egy speciális függvény, a main függvény első utasításával kezdődik a program végrehajtása.
Másrészt minden C++ fordítóhoz mellékelve van egy úgynevezett C++ standard library, ami többek között alapvető és hasznos műveleteket megvalósító függvényeket tartalmaz.

Elvétve az eddigi tananyagrészekben is előfordult, hogy saját függvényeket készítettünk, de ebben a tananyagrészben részletesebben is szó lesz erről.

A függvényeket szokták alprogramoknak is nevezni. Hasonló fogalom a szubrutin is. Angolul a függvényeket functionnek nevezik.

Egy függvény definíciója (létrehozása) szemléltetésszerűen így néz ki (ez csak egy szemléltető példa, használata fordítási hibát okoz):

return_type function_name (param1, param2, ...) {
  //statements
}

A C++ nyelvben két féle függvény létezik, az egyik, ami visszaad egy eredményt (ezt úgy szokták mondani, hogy van visszatérési értéke), a másik, ami nem ad vissza eredményt, csak lefuttat utasításokat (ez utóbbit például Pascal és ADA nyelvekben eljárásnak (angolul procedure) nevezik). Mindkettőre nézünk példát.

Példák

Számok átlaga

//function example: average
#include <iostream>
#include <vector>

double avg (const std::vector<double>& numbers) {
  if (numbers.size() == 0) {return 0;}
  double result = 0;
  for (int i = 0; i < numbers.size(); ++i) {
    result += numbers.at(i);
  }
  result /= numbers.size();
  return result;
}

int main() {
  std::vector<double> stdvec_example1 = {1,2,3,4,5,6};

  std::cout << "average of stdvec_example1 " << avg(stdvec_example1) << '\n';

  std::vector<double> stdvec_example2 = {9,8,7,6,5,4};

  std::cout << "average of stdvec_example2 " << avg(stdvec_example2) << '\n';
}

Ebben a példában az avg nevű függvénnyel kiszámítjuk azoknak a számoknak az átlagát, amely számokat egy std::vectorban tárolunk. A matematikai hátterét gondolom nem kell különösebben ismertetni, összeadjuk a számokat, majd elosztjuk annyival, ahány számot összeadtunk (ez az std::vector elemszáma).

A függvény definíciójának első sorában, a kerek zárójelek között adjuk meg azokat az adatokat, amiket a függvény átvesz, ez ugyebár logikusan egy std::vector (az avg nevű függvény nem látja azokat az adatokat, amiket a main függvényben használunk, csak akkor ha átadjuk neki). Ha nem akarunk rajta módosítani, úgynevezett konstans referenciaként vesszük át, ami annyit jelent, hogy a típusa elé constot írunk, a típusa mögé pedig egy & jelet.

Fontos a függvény utolsó utasítása is. Azt, hogy milyen eredményt adunk át, a return kulcsszóval és az utána írt változóval jelezzük. A példa esetében a return result; utasítás a result változó értékét adja vissza.

A main függvényben kétszer is meghívjuk ezt a függvényt, és minden függvényhívásnál egy másik std::vectort adunk át a függvénynek.

Maximumkeresés

Ebben a feladatban egy std::vector értékei közül megkeressük a legnagyobbat. A függvényben először az std::vector első elemét adjuk értékül a max változónak, majd az std::vector elemein végiglépkedve ha találunk nála nagyobbat, akkor azt adjuk értékül a max változónak.

Szintén konstans referenciaként vesszük át a függvényben az std::vectort, mint az előző feladatban, hiszen itt sem módosítjuk az elemeit.

Ebben a függvényben szintén visszaadunk egy eredményt, a függvény visszatérési értékét, ami a ciklus lefutása után az std::vector legnagyobb eleme lesz.

//function example: find max
#include <iostream>
#include <vector>

int find_max (const std::vector<int>& numbers) {
  int max = numbers.at(0);
  for (int i = 1; i < numbers.size(); ++i) {
    if (numbers.at(i) > max) {
      max = numbers.at(i);
    }
  }
  return max;
}

int main() {
  std::vector<int> stdvec_example1 = {2,5,9,1,4,16,10};

  std::cout << "max of stdvec_example1: " << find_max(stdvec_example1) << '\n';

  std::vector<int> stdvec_example2 = {1,6,8,14,3,12,11};

  std::cout << "max of stdvec_example2: " << find_max(stdvec_example2) << '\n';
}

Két változó értékének cseréje

//procedure example: swap
#include <iostream>

void swap (int& num1, int& num2) {
  int temp = num1;
  num1 = num2;
  num2 = temp;
}

int main() {
  int i_example1 = 5, i_example2 = 6;

  swap(i_example1, i_example2);

  std::cout << i_example1 << ' ' << i_example2 << '\n';
}

Ebben a példában egy olyan függvényt írunk, ami kicseréli két int típusú változónak az értékét.

Ahhoz, hogy módosítani tudjunk egy függvénynek átadott értéket, a függvényben úgynevezett referenciaként kell átvennünk, amit úgy tudunk megtenni, hogy az adott változó típusa után egy & jelet írunk.

Ebben a függvényben nem adunk vissza értéket. Ezt úgy tudjuk megtenni, hogy a függvény visszatérési típusának void-ot adunk meg.

Bármilyen típusú változók értékeinek cseréje

A fentebbi példa csak int típusú változók cseréjéhez használható. Template függvénnyel bármilyen típusú változók cseréjét elvégezhetjük, legyen az akár std::string.

//template function: swap
#include <iostream>

template <typename T>
void swap (T& param_1, T& param_2) {
  T temp = param_1;
  param_1 = param_2;
  param_2 = temp;
}

int main() {
  int i_example1 = 1, i_example2 = 6;
  double d_example1 = 3.14, d_example2 = -1.5;

  swap(i_example1, i_example2);
  swap(d_example1, d_example2);

  std::cout << i_example1 << ' ' << i_example2 << '\n';
  std::cout << d_example1 << ' ' << d_example2 << '\n';
}

std::vector kiíratása

Egy függvény bármilyen feladatot is elláthat, lehet az akár egy nagyon alapvető feladat is, például egy tömb vagy egy std::vector kiíratása.

A feladatban többször is szükség van a kiíratásra: először kiíratjuk, aztán rendezzük, majd megint kiíratjuk az std::vector elemeit.

//function example: print vector
#include <iostream>
#include <vector>
#include <algorithm>

void print_int_vector (const std::vector<int>& numbers) {
  for (const int& element : numbers) {
    std::cout << element << ' ';
  }
  std::cout.put('\n');
}

int main() {
  std::vector<int> stdvec_example = {3, 2, 9, 6, 5, 1};

  print_int_vector(stdvec_example);

  std::sort(stdvec_example.begin(), stdvec_example.end());

  print_int_vector(stdvec_example);
}

Szintén egy nagyon egyszerű feladatot megvalósító függvény, ha a programban lévő különböző feladatok közé elválasztósort szeretnénk írni. Azért érdemes erre függvényt alkalmazni, mert ha módosítani szeretnénk az elválasztósort, akkor függvény használatával elég csak egy helyen átírni.

//delimiter characters
#include <iostream>

void print_delimiter() {
  std::cout << "----------\n";
}

int main() {
  std::cout << "task1\n";

  print_delimiter();

  std::cout << "task2\n";

  print_delimiter();

  std::cout << "task3\n";

  print_delimiter();

  std::cout << "task4\n";

  print_delimiter();

  std::cout << "task5\n";
}

negatív számok törlése egy std::vectorból

//delete negative numbers
#include <iostream>
#include <vector>

void print_vector (const std::vector<int>& stdvec) {
  for (const int& element : stdvec) {
    std::cout << element << ' ';
  }
  std::cout.put('\n');
}

void delete_negative (std::vector<int>& stdvec) {
  for (int i = 0; i < stdvec.size(); ++i) {
    if (stdvec.at(i) < 0) {
      stdvec.erase(stdvec.begin()+i);
    }
  }
}

int main() {
  std::vector<int> numbers = {1, 3, 5, -22, 4, -8};

  print_vector(numbers);

  delete_negative(numbers);

  print_vector(numbers);
}

std::string kiíratása fordítva

Ez például egy nagyon jó példa arra, hogy miből érdemes függvényt készíteni. Bár nem túl sok utasítás, viszont függvény nélkül sokszor le kellene írni ezeket az utasításokat, ami átláthatatlanabbá tenné a kódot, illetve ha módosítani kell rajta, akkor több helyen is el kellene végezni a módosításokat.

//function: string reverse
#include <iostream>
#include <string>
#include <algorithm>

std::string string_reverse (const std::string& p_string) {
  std::string reverse;
  std::for_each(
    p_string.rbegin(), p_string.rend(), [&reverse](const char& element) {reverse += element;}
  );
  return reverse;
}

int main() {
  std::string str_example1 = "first";
  std::string str_example2 = "second";

  std::cout << string_reverse(str_example1) << '\n';
  std::cout << string_reverse(str_example2) << '\n';
}

Páros-e?

//iseven
#include <iostream>
#include <vector>

bool is_even (const int& value) {
  return value % 2 == 0;
}

void print_is_even(const int& value) {
  std::cout << "is " << value << " even? " << is_even(value) << '\n';
}

int main() {
  std::cout.setf(std::ios::boolalpha);

  std::vector<int> stdvec = {1,2,3,4,5,6};

  for (const int& element : stdvec) {
    print_is_even(element);
  }
}

Prímszám-e?

//isprime
#include <iostream>
#include <cmath>
#include <vector>

bool isprime(int number) {
  if (number <= 1) {
    return false;
  } else if (number % 2 == 0 && number != 2) {
    return false;
  } else if (number == 2) {
    return true;
  } else {
    for (int i = 3; i < sqrt(number); ++i) {
      if (number % i == 0) { return false; }
    }
    return true;
  }
}

void print_isprime(int number) {
  if (isprime(number)) {
    std::cout << number << " is prime.\n";
  } else {
    std::cout << number << " is not prime.\n";
  }
}

int main() {
  std::vector<int> numbers = {-2, 3, 4, 6, 7, 41};
  for (const int& element : numbers) {
    print_isprime(element);
  }
}

Tömbök és függvények

Tömbök esetén a tömb méretét át szoktuk adni a függvénynek, mivel a függvénynek átadott tömb pointerré konvertálódik és így a függvényen belül nem használható rendeltetésszerűen a sizeof operátor.

//pass array to function
#include <iostream>

void print_array (int numbers[], int size) {
  for (int i = 0; i < size-1; ++i) {
    std::cout << numbers[i] << ' ';
  }
  std::cout << numbers[size-1] << '\n';
}

int main() {
  int arr_example[] = {1,2,3,4,5,6};
  int arr_size = sizeof(arr_example)/sizeof(arr_example[0]);

  print_array(arr_example, arr_size);
}

Template függvény használatával megoldható, hogy a tömb méretét ne kelljen átadni a függvénynek, hanem a fordító következtesse ki.

//pass array to function
#include <iostream>

template <std::size_t size>
void print_array (int (&numbers)[size]) {
  for (int i = 0; i < size-1; ++i) {
    std::cout << numbers[i] << ' ';
  }
  std::cout << numbers[size-1] << '\n';
}

int main() {
  int arr_example[] = {1,2,3,4,5,6};

  print_array(arr_example);
}

Függvények visszatérési típusa nem lehet tömb, csak tömbre mutató pointer, esetleg std::array vagy std::vector.

Láthatóság (scope), élettartam (lifetime)

Fontos kihangsúlyozni, hogy a függvények blokkjában létrehozott változók, tömbök csak a függvények blokkjában érhetőek el, és ha a vezérlés eléri a függvények blokkjának a végét, akkor a függvények blokkjában létrehozott változók, tömbök megsemmisülnek.

Fontos, hogy egy függvény visszatérési értéke ne legyen olyan változó, amit a függvény blokkjában hoztunk létre (úgynevezett lokális változó). Kivételt képeznek a static tárolási osztályú változók.

static változók

A static tárolási osztályú változók olyan változók, amik élettartama a program futásának végéig tart, vagyis a bennük tárolt érték a függvény futásának befejeztével is megmarad.

//static variable example
#include <iostream>

void execute_count () {
  static int counter = 1;
  std::cout << "number of executions so far: " << counter << '\n';
  ++counter;
}

int main() {
  execute_count();
  execute_count();
  execute_count();
  execute_count();
}

Indokolt lehet static változó használata például abban az esetben, ha egy függvényt sokszor futtatunk, és nem akarjuk, hogy a változó minden futtatás alkalmával létrejöjjön.

Deklaráció, definíció

Alapesetben akkor használhatunk (hívhatunk meg) egy függvényt, ha a függvény definíciója megelőzi a függvényhívást a forráskódban. Kivéve ha a használat előtt leírjuk a függvények deklarációját (a függvények első sorát), ez esetben a függvények definíciója akár a függvényhívások után is következhet.

//split
#include <iostream>
#include <vector>
#include <string>
#include <sstream>

std::vector<std::string> split(const std::string &s, char delimiter);

void print_vector (const std::vector<std::string>& p_vector);

int main() {
  std::vector<std::string> example = split("The quick brown fox jumps over the lazy dog", ' ');

  print_vector(example);
}

std::vector<std::string> split(const std::string &p_str, char delimiter) {
  std::vector<std::string> elements;
  std::stringstream sstream(p_str);
  std::string actual;
  while (getline(sstream, actual, delimiter)) {
    elements.push_back(actual);
  }
  return elements;
}

void print_vector (const std::vector<std::string>& p_vector) {
  for (const std::string& element : p_vector) {
    std::cout << element << '\n';
  }
}

Paraméterek

A függvény definíciójában lévő paramétereket formális paraméternek nevezzük, a függvényhívás paramétereit pedig aktuális paramétereknek, vagy argumentumoknak.

érték szerinti átadás

Ha egy formális paraméter típusa után nem írunk & jelet (ezt érték szerinti átadásnak nevezzük), akkor függvényhíváskor átadott aktuális paraméter értéke lemásolódik, és a függvényben elvégzett értékmódosítások nem lesznek hatással az eredeti változóra.

Alaptípusú változókat szoktunk így átadni függvényeknek, akkor, ha nem akarjuk módosítani az értéküket, mivel alaptípusú változók esetén a címképzés költségesebb művelet mint az értékük lemásolása.

cím (referencia) szerinti átadás

Ha a formális paraméter típusa után & jelet írunk (ezt cím szerinti átadásnak nevezzük), akkor a függvényben végzett módosítások az aktuális paraméterben átadott változóra hatással lesznek.

Objektumokat (pl. std::string típusú változókat) szoktunk így átadni, ha módosítani szeretnénk az értéküket.

konstans referencia szerinti átadás

Ha nem szeretnénk, hogy másolat készüljön az aktuális paraméterről, viszont módosítást sem szeretnénk végezni az átadott értéken, akkor használhatunk úgynevezett konstans referencia szerinti átadást (const type& name). 

Szintén objektumok (pl. std::string típusú változók) esetén használjuk, ha nem szeretnénk módosítani az értéküket.

Függvény mellékhatása

Egy visszatérési értékkel rendelkező függvény mellékhatásának tekintjük azokat az utasításokat, amiket a függvény a visszaadott érték kiszámításától függetlenül hajt végre. Pl. ha a függvény azon kívül, hogy visszaad egy értéket, kiír valamit couttal vagy megváltoztatja valaminek az értékét (ami nem a visszatérési érték előállításához kell).

Függvény túlterhelése

Egy függvény túlterhelése (angolul function overloading) nem jelent mást, mint két vagy több függvényt létrehozni ugyanazzal a névvel, viszont eltérő paraméterlistával. Visszatérési érték alapján nem lehet túlterhelni egy függvényt.

Például ha azt szeretnénk, hogy egy minimum értéket megkereső függvénynek tömböt vagy std::vectort is át lehessen adni, akkor ugyanazzal a névvel két változatban tudjuk definiálni a függvényt.

//function overload example
#include <iostream>
#include <vector>

int find_min (const std::vector<int>& numbers) {
  int min = numbers.at(0);
  for (int i = 1; i < numbers.size(); ++i) {
    if (numbers.at(i) < min) {
      min = numbers.at(i);
    }
  }
  return min;
}

template <std::size_t size>
int find_min (int (&numbers)[size]) {
  int min = numbers[0];
  for (int i = 1; i < size; ++i) {
    if (numbers[i] < min) {
      min = numbers[i];
    }
  }
  return min;
}

int main() {
  int arr_example[] = {2,5,9,1,4,16,10};

  std::cout << "min of arr_example: " << find_min(arr_example) << '\n';

  std::vector<int> stdvec_example = {1,6,8,14,3,12,11};

  std::cout << "min of stdvec_example: " << find_min(stdvec_example) << '\n';
}

Default értékek

Egy függvény definíciójában default értékeket adhatunk a paraméterlista végén lévő változóknak. A paraméterlista közepén nem adhatunk default értéket egy változónak, csak akkor, ha az utána következő változóknak is van default értékük.

Például ha egy mondatot szeretnénk szavakra feldarabolni, akkor logikus lehet, hogy a legtöbb esetben szóközök választják el az egyes szavakat, így az elválasztókarakter default értéke lehet szóköz.

std::vector<std::string> split(const std::string& p_str, char delimiter = ' ') { /*...*/ }

Ekkor a függvényhíváskor egyel kevesebb paramétert kell megadni:

std::vector<std::string> example1 = split("The quick brown fox jumps over the lazy dog.");

Ha viszont nem a default értéket szeretnénk megadni, akkor a függvény minden paraméterét meg kell adnunk:

std::vector<std::string> example2 = split("Effort#is#only#effort#when#it#begins#to#hurt.", '#');
//default parameter value example
#include <iostream>
#include <vector>
#include <string>
#include <sstream>

std::vector<std::string> split(const std::string& p_str, char delimiter = ' ') {
  std::vector<std::string> elements;
  std::stringstream sstream(p_str);
  std::string actual;
  while (getline(sstream, actual, delimiter)) {
    elements.push_back(actual);
  }
  return elements;
}

void print_vector (const std::vector<std::string>& p_vector) {
  for (const std::string& element : p_vector) {
    std::cout << element << '\n';
  }
}

int main() {
  std::vector<std::string> example1 = split("The quick brown fox jumps over the lazy dog.");

  print_vector(example1);

  std::cout.put('\n');

  std::vector<std::string> example2 = split("Effort#is#only#effort#when#it#begins#to#hurt.", '#');

  print_vector(example2);
}

Globális változók

A globális változók olyan változók, amik a függvények blokkján kívül lettek létrehozva, és nem kell átadni őket egy függvénynek, ahhoz, hogy a függvény hozzá tudjon férni. Használatuk ellenjavallt, mivel nehéz nyomon követni, hogy melyik függvény mikor változtatja meg az értékét. Esetleg a konstans (const) globális változók használata megfontolható.

Globális függvény és tagfüggvény

Tagfüggvénynek (angolul member function) vagy metódusnak nevezünk egy olyan függvényt, ami egy osztályhoz tartozik. Például az std::string egy osztály, a size() pedig a tagfüggvénye. Ebből az következik, hogy minden egyes std::string típusú változónak van size() tagfüggvénye, amivel le lehet kérdezni a méretét.
Például Java és C# nyelvekben egy függvény mindenképp tagfüggvény.
Például C, C++, Javascript nyelvekben létrehozhatunk olyan függvényeket, amik nem tartoznak osztályokhoz. Ezeket szokták globális függvénynek vagy angolul free functionnek, esetleg non-member functionnek nevezni.
Ilyen függvény például a C++ standard library string header fájljában deklarált getline függvény.

inline függvények

Az inline függvények olyan függvények, amik tulajdonképpen nem függvényként viselkednek, hanem a bennük lévő utastásokat a fordító bemásolja a függvényhívás helyére. Ettől persze még hasznosak, mivel ha változtatni akarunk az algoritmuson, akkor elég csak egy helyen módosítani.

Általában néhány utasításból álló, egyszerű utasításokat tartalmazó függvényeket szoktunk inlineosítani. Pl. ciklust vagy switch elágazást tartalmazó függvényt nem szoktunk inlineosítani.

Például:

inline bool is_even (const int& value) { return value % 2 == 0; }

Rekurzió

Rekurziónak nevezzük, ha egy függvény saját magát hívja meg. A rekurzió egyes esetekben helyettesíthető ciklussal (pl. faktoriális számítás).

//fibonacci numbers (the first 10)
#include <iostream>

int fibonacci(int n) {
  if (n <= 1) {
    return n;
  }
  return fibonacci(n-1) + fibonacci(n-2);
}

int main () {
  const int n = 10;

  for (int i = 0; i < n; ++i) {
    std::cout << fibonacci(i) << ' ';
  }
  std::cout.put('\n');
}

Hibalehetőségek

Futási idejű hibát okoz, ha egy lokális változót, vagy egy érték szerint átadott paramétert adunk vissza egy függvényből, mivel ezek a változók a függvény végrehajtását követően megszűnnek.
Lokális változó esetén megoldás lehet, ha staticként definiáljuk, vagy dinamikusan allokáljuk, és úgy adjuk vissza.

Fordítási hibát okoz, ha literált cím szerint adunk paraméterül egy függvénynek.

Egyéb tananyag

előző tananyagrész: string-szám, szám-string konverziók
következő tananyagrész: osztályok/objektumok

A bejegyzés trackback címe:

https://itkezdoknek.blog.hu/api/trackback/id/tr6013118702

Kommentek:

A hozzászólások a vonatkozó jogszabályok  értelmében felhasználói tartalomnak minősülnek, értük a szolgáltatás technikai  üzemeltetője semmilyen felelősséget nem vállal, azokat nem ellenőrzi. Kifogás esetén forduljon a blog szerkesztőjéhez. Részletek a  Felhasználási feltételekben és az adatvédelmi tájékoztatóban.

Nincsenek hozzászólások.
süti beállítások módosítása