6. Функции

План:
Функциите като черни кутии
Писане на функции
Коментари и документиране
Връщане на стойност и оператор return
Параметри на функции
Деклариране на функции
Странични ефекти и процедури
Параметри-псевдоними
Константни псевдоними
Област на действие на променливите, глобални променливи
Постъпково прецизиране, създаване на кода, проиграване


** Функциите като черни кутии

Функция е част от код, определен с име.
До сега сме използвали редица функции, които са предоставени:
-- от системните библиотеки на С++
    sqrt(x) - пресмята корен квадратен на числото с плаваща точка x
  getline(cin, s)
- чете един ред от потока cin
-- като член-функции от класа string на Стандартната библиотека с шаблони на С++
   
s.length() - връща дължината на низа s
--  като член-функции от потребителските класове Time и Employee, дефинирани в учебника
  t.get_second() - връща секундите от обекта t на класа Time

Функциите могат да се разглеждат като черни кутии, за които знаем само вход и изход, без да се интересуваме как работи функцията.

Пример: Функцията за пресмятане на квадратен корен
sqrt(x)
int main()
{ cout << "Enter a positive number: ";
double x;
cin >> x;
x = sqrt(x); // sqrt is running
cout << "The result is " << x << "\n";
return 0;
}
- Изпълнението на main() временно се прекратява докато се изпълнява функцията sqrt.
- Функцията sqrt става активна и пресмята стойността за връщане, използвайки някой метод, който ще даде конкретен резултат.
- Върнатата стойност се прехвърля обратно на 
main, която продължава изпълнението като използва тази стойност - присвоява я на променливата
х и после я отпечатва на екрана.
Функция Вид на параметъра
sqrt(x) променлива
sqrt(2.5) константа
sqrt(x + 1.02) израз
Функция Брой на параметрите
sqrt(x) 1
pow(x,y) 2
harry.get_salary() 0

Член-функция Неявен параметър Тип на
неявен параметър

Явен параметър
Тип на
явен параметър
harry.get_salary() harry Employee няма
-
t.add_seconds(10) t Time 10
int
s.substr(0,num) s string 0, num
int, string
Функция Тип на върнатата стойност
sqrt(x) double
c.substr(0, num) string
t.get_seconds() int
e.set_salary(s) no return value
Функция Изисквания за параметрите - тип и lvalue/rvalue
fabs(w) Параметърът е от тип  double - константа, променлива, израз (rvalue)
t.second_from(t1) Параметърът t1 е обект от класа Time - константа, променлива
getline(cin,s) Първият параметър е обектът cin, вторият параметър е променлива (lvalue) от тип string


** Писане на функции

Пример: Да се пресметне стойността на влог с начална сума 1000 лв. след 10 години с годишната лихва да е p%.  Да се използва се формулата за пресмятане на сложна лихва.
double future_value(double p)
{  double b = 1000 * pow(1 + p / 100, 10);
    return b;
}
* Функцията се дефинира като се зададе:
- тип на резултата (на връщаната стойност),
- име на функцията,
- тип на параметъра,
- име на параметъра,
- тяло на функцията.

// futval1.cpp
#include
<iostream>
#include <cmath>
using namespace std;
 
// дефиниция на функция
double future_value(double p)
{  double b = 1000 * pow(1 + p / 100, 10);
    return b;
}
 
int main()
{  cout << "Please enter the interest rate in percent: ";
    double rate;
    cin >> rate;
 // извикване на функция
    double balance = future_value(rate);
    cout << "After 10 years, the balance is " 
         << balance << "\n";
 
    return 0;
}

nkirov@cpp % c++ futval1.cpp
nkirov@cpp % ./a.out
Please enter the interest rate in percent: 8
After 10 years, the balance is 2158.92

* Твърдо свързани стойности (начална сума 1000 и брой години 10) - да се избягват!
* Многократно използване на функцията (reusable).
* Друго решение на същата задача - сега функцията future_value има 3 параметъра: началната сума, годишната лихва и брой години на престоя на влога в банката.
double future_value(double initial_balance, double p, int n)
{ double b = initial_balance * pow(1 + p / 100, n);
return b;
}
double balance = future_value(1000, rate, 10);
futval.cpp
nkirov@cpp % c++ futval.cpp
nkirov@cpp % ./a.out
Please enter the interest rate in percent: 12
After 10 years, the balance is 3105.85


** Коментари и документиране
Пример:
/**
Computes the value of an investment with compound interest.
@param initial_balance - the initial value of the investment
@param p the interest rate per period in percent
@param n the number of periods the investment is held
@return the balance after n periods
*/
double future_value(double initial_balance, double p, int n)
{ double b = initial_balance * pow(1 + p / 100, n);
return b;
}
Може да се използва и показания формат за български език.
/* 
ЦЕЛ:       пресмята стойността на влог със сложна лихва 
ПОЛУЧАВА:  initial_balance - начална стойност на влога 
           p - годишен лихвен процент 
           nyear - броя на годините 
ЗАБЕЛЕЖКИ: лихвата се начислява всеки месец 
*/

** Връщане на стойност и оператор return

Когато се достигне до оператор return, от функцията се излиза веднага и управлението се връща на извикващата функция.
double future_value(double initial_balance, double p, int n)
{ if (n < 0) return 0;
if (p < 0) return 0;
double b = initial_balance * pow(1 + p / 100, n);
return b;
}
Всеки клон на функцията (вследствие на оператор if) трябва да връща някаква стойност.

Пример:
double future_value(double initial_balance, double p, int n)
{ if (p >= 0)
return initial_balance * pow(1 + p / 100, n);
/* Error */
}
Последният оператор на всяка функция би трябвало да бъде оператор return.

Пример: Функция за пресмятане на относителна близост на две числа.
bool approx_equal(double x, double y)
{ const
double EPSILON = 1E-14;
if (x == 0) return fabs(y) <= EPSILON;
if (y == 0) return fabs(x) <= EPSILON;
return fabs(x - y) / max(fabs(x), fabs(y)) <= EPSILON;
}
Функция, която връща булева стойност се нарича предикат.
 // approx.cpp

#include
<iostream>
#include <algorithm>
using namespace std;
/**
    Tests whether two floating-point numbers are 
    approximately equal.
    @param x a floating-point number
    @param y another floating-point number
    @return true if x and y are approximately equal
*/
bool approx_equal(double x, double y)
{ 
const
double EPSILON = 1E-14;
    if (x == 0) return fabs(y) <= EPSILON;
    if (y == 0) return fabs(x) <= EPSILON;
    return fabs(x - y) / max(fabs(x), fabs(y)) <= EPSILON;
}
 
int main()
{ 
double
x;
    cout << "Enter a number: ";
    cin >> x;
 
    double y;
    cout << "Enter another number: ";
    cin >> y;
 
    if (approx_equal(x, y))
       cout << "The numbers are approximately equal.\n";
    else
       cout << "The numbers are different.\n";
    return 0;
}

nkirov@cpp % c++ approx.cpp
nkirov@cpp % ./a.out
Enter a number: 1
Enter another number: 1.0000000000000001
The numbers are approximately equal.
nkirov@cpp % ./a.out
Enter a number: 10
Enter another number: 10.001
The numbers are different.


** Параметри на функции

* Формални параметри  - параметри-променливи, които се задават при дефиниране на функцията.

double future_value(double initial_balance, double p, int n)

* Фактически параметри (аргументи) - представляват изрази за инициализация на параметрите-променливи и се задават при извикване на функцията.

b = future_value(total/2, rate, year2-year1);

Фактическите параметри съответстват по тип и брой на формалните параметри: променливите total и rate са от тип double, а year1 и year2 са от тип int . 

Формалните параметри са променливи, които може да се променят в тялото на функцията.

double future_value(double initial_balance, double p, int n)
{ p = 1 + p / 100;
double b = initial_balance * pow(p, n);
return b;
}

Често това се счита за лош стил на програмиране (bad programming style).


** Деклариране на функции

Функциите трябва да бъдат известни, преди да бъдат използвани.
Дефинирането на дадена функция преди да бъде използвана не винаги е удобно, особено при по-големи програми, които съдържат много функции.

За да се използва (извика) една функция, достатъчно е са известни типа и името на функцията, и типовете на параметрите й.
Тези данни се съдържат в декларацията на функцията, която се състои от:
- тип на функцията (на връщаната стойност),
- име на функцията,
- типове и имена на параметри,
- точка и запетая.

В езика C++ файлът с текста на програмата се подрежда най-често по следния начин: 
- декларация на функция,
- използване на функция,
- дефиниране на функция.
 
#include <iostream>
#include <cmath>
using namespace std;

/* декларация на функцията future_value */
double future_value(double initial_balance, double p, int nyear);

int main()
{ ...
    /* използване на функцията future_value */
    double bal = future_value(10000, rate, 10);
    ...
}

/* дефиниция на функцията future_value */
double future_value(double initial_balance, double p, int nyear)
{
    ...
}

Декларациите на стандартните за езика функции са в заглавните файлове.

** Странични ефекти и процедури
Пример: Процедура print_time за отпечатване на обект от тип Time.
 // printime.cpp 

#include
<iostream>
#include <iomanip>

using namespace std;
 
#include "ccc_time.h"
/**
    Print a time in the format h:mm:ss
    @param t the time to print
*/
void print_time(Time t)
{ 
cout << t.get_hours() << ":" << setw(2) << setfill('0') 
         << t.get_minutes() << ":" << setw(2) 
         << t.get_seconds() << setfill(' ');
}
 
int main()
{ 
Time liftoff(7, 0, 15);
    Time now;
    cout << "Liftoff: ";
    print_time(liftoff);
    cout << "\n";
 
    cout << "Now: ";
    print_time(now);
    cout << "\n";
 
    return 0;
}

nkirov@cpp % c++ printime.cpp ccc_time.cpp
nkirov@cpp % ./a.out
Liftoff: 7:00:15
Now: 11:00:00


**Параметри-псевдоними

Пример: Функция за увеличаване на заплата. 
Процедурата raise_salary увеличава заплатата на служител e с by процента.

raise_salary(harry, 5);
void raise_salary(Employee& e, double by)
{

double
new_salary = e.get_salary() * ( 1 + by / 100);
e.set_salary(new_salary);
}

 // raisesal.cpp

#include
<iostream>

using namespace std;
 
#include
"ccc_empl.h"
 
/**
    Raise an employee salary 
    @param e employee receiving raise
    @param by the percentage of the raise
*/
void raise_salary(Employee& e, double by)
{ 
double new_salary = e.get_salary() * (1 + by/100);
    e.set_salary(new_salary);
}
 
int main()
{ 
Employee harry("Hacker, Harry", 45000.00);
    raise_salary(harry, 5);
    cout << "New salary: " << harry.get_salary() << "\n";

    return 0;
}

** Константни псевдоними

* Използват се за да се избегне копирането - особено нежелателно, ако параметърът е обект, съдържащ много данни.
* Тъй като стойността на константния псевдоним не може да се променя, той не "пренася" данни от извиканата функция (print_employee) в извикващата функция (main). 

Пример: Функция ца отпечатване на данните на обект от тип Employee
void print_employee(const Employee & e) 
{ 
 cout << "Name:   " << e.get_name() 
      << "Salary: " << e.get_salary(); 
}
 
int main()

   Employee harry("Hacker, Harry", 45000.00);

   print_employee(harry);

   return 0;
}

Параметри-променливи Параметри-псевдоними Константни псевдоними
Входни параметри за функцията Входно-изходни параметри за функцията -
Предаване на параметри чрез копиране Предаване на параметри по адрес (не се копира)
Предаване на параметри по адрес (не се копира)
Стойността на параметъра може да се променя (фактическия параметър не се променя)
Стойността на параметъра може да се променя (фактическия параметър се променя)
Стойността на параметъра не може да се променя
Фактически параметър може да бъде константа, променлива или израз (rvalue)
Фактически параметър може да бъде само променлива (lvalue)
Фактически параметър може да бъде константа, променлива или израз (rvalue)


** Област на действие на променливите (scope), глобални променливи

* Ако едно и също име на променлива се използва в две функции,  то не съществува  връзка между двете променливи.
* Областта на действие на променлива, която е дефинирана в блока на функция, се простира единствено в рамките на тази функция. 

Пример:
Сложна лихва - променливи с едно и също име в две функциии.

// futval1.cpp 
#include <iostream> 
#include <cmath> 
using namespace std;

double f_val(double i_bal, double p, int ny) /*дефиниции на параметрите-променливи i_bal, p и ny*/ 
{ 
  double r; /*дефиниция на променливата r в блока на функцията future_value*/ 
  r = i_bal*pow(1 + p/(12*100), 12*ny); 
  return r; 
} /*край на областта на действие на променливите i_bal, p, ny и r*/ 
int main() 
{ 
 cout << "Interest rate in percent: "; 
 double r; /*дефиниция на променливата r в блока на функцията main*/ 
 cin >> r; 
 double bal = f_val(1000, r, 10); /*дефиниция на променливата bal*/ 
 cout << "After 10 years, the balance is " << bal << "\n"; 
 return 0; 
} /* край на областта на действие на променливите r и bal*/

В примера няма връзка между двете променливи с име r в двете функции future_value и main. 

* Променлива, дефинирана в блок на функция, се нарича локална променлива.
* Областта на действие на локална променлива е от мястото на дефинирането до края на блока, в който е дефинирана променливата (block scope).
* В тази област променливата е видима, т.е. може да бъде използвана. 

* Променлива, дефинирана извън блок на функция, се нарича глобална променлива.
* Областта на действие на глобална променлива е от мястото на дефинирането до края на файла, в който е дефинирана променливата (file scope).
* Досега са използвани глобалните променливи (обекти) cin и cout, които са дефинирани в заглавния файл iostream.h. 

Пример: Увеличаване на заплата с глобална променлина.

// global.cpp

#include <iostream> 
using namespace std;

#include "ccc_empl.cpp"

double annual_raise; /* дефиниране на глобална променлива */ 

void raise_salary(Employee& e) 
{
  double new_salary = e.get_salary()
             *(1 + annual_raise/100);
  /* използване на глобалната променлива */
  e.set_salary(new_salary); 
} 

int main() 

   Employee boss("Reindeer, Rudolf", 48000.00);
 
   Employee harry("Hacker, Harry", 35000.00); 
   annual_raise = 5; /* използване на глобалната променлива */ 
   raise_salary(boss); 
   raise_salary(harry); 
   cout << boss.get_name() << " " << boss.get_salary() << "\n"; 
   cout << harry.get_name()<< " " << harry.get_salary()<< "\n";

   return 0; 
}

Reindeer, Rudolf 50400 
Hacker, Harry 36750

* Глобалните променливи предават данни между функциите, което трябва да се избягва.
* Използването им трябва да става само ако има основателна причина за това.
* Нормалният обмен на данни между функциите става с помощта на параметрите на функциите. 

**
Постъпково прецизиране, създаване на кода, проиграване.
 
* Една от най-добрите стратегии за решаване на трудни задачи е декомпозицията (постъпково прецизиране) - разбиване на задачата на няколко по-прости задачи.
* Получените задачи също се разбиват на по-прости и процесът продължава, докато се получат задачи, които могат да се решат директно.


Пример:
Задача за записване на число с думи.

// intname.cpp

#include <iostream> 
#include <string> 
using namespace std;

string digit_name(int n) 
/* ЦЕЛ:  превръща цифра в английското й име 
   ПОЛУЧАВА: n - цяло число между 1 и 9 
   ВРЪЩА:  името на n ("one" . . . "nine") 
*/ 
{  if (n == 1) return "one"; 
   else if (n == 2) return "two"; 
   else if (n == 3) return "three"; 
   else if (n == 4) return "four"; 
   else if (n == 5) return "five"; 
   else if (n == 6) return "six"; 
   else if (n == 7) return "seven"; 
   else if (n == 8) return "eight"; 
   else if (n == 9) return "nine"; 
   return ""; 
} 
string teen_name(int n) 
/* ЦЕЛ:  превръща цяло число между 10 и 19 в английското му име 
   ПОЛУЧАВА: n - цяло число между 10 и 19 
   ВРЪЩА:  името на n ("ten" . . . "nineteen") 
*/ 
{  if (n == 10) return "ten"; 
   else if (n == 11) return "eleven"; 
   else if (n == 12) return "twelve"; 
   else if (n == 13) return "thirteen"; 
   else if (n == 14) return "fourteen"; 
   else if (n == 15) return "fifteen"; 
   else if (n == 16) return "sixteen"; 
   else if (n == 17) return "seventeen"; 
   else if (n == 18) return "eighteen"; 
   else if (n == 19) return "nineteen"; 
   return ""; 
} 
string tens_name(int n) 
/* ЦЕЛ:  дава английското име на десетиците между 20 и 90 
   ПОЛУЧАВА: n - цяло число между 2 и 9 
   ВРЪЩА:  името на 10*n ("twenty" . . . "ninety") 
*/ 
{  if (n == 2) return "twenty"; 
   else if (n == 3) return "thirty"; 
   else if (n == 4) return "forty"; 
   else if (n == 5) return "fifty"; 
   else if (n == 6) return "sixty"; 
   else if (n == 7) return "seventy"; 
   else if (n == 8) return "eighty"; 
   else if (n == 9) return "ninety"; 
   return ""; 
} 
string int_name(int n) 
/* ЦЕЛ:  превръща цяло число в английското му име 
   ПОЛУЧАВА: n - цяло положително число < 1000000 
   ВРЪЩА:  името на n (т.е. "two hundred seventy four") 
*/ 
{  int c = n; /* оставащата за превръщане част */ 
   string r;  /* стойността на функцията */ 
   if (c >= 1000) 
   {  r = int_name(c / 1000) + " thousand"; 
      c = c % 1000; 
   } 
   if (c >= 100) 
   {  r = r + " " + digit_name(c / 100) + " hundred"; 
      c = c % 100; 
   } 
   if (c >= 20) 
   {  r = r + " " + tens_name(c / 10); 
      c = c % 10; 

   } 
   else if (c >= 10) 
   {  r = r + " " + teen_name(c); 
      c = 0; 
   } 
   if (c > 0) 
      r = r + " " + digit_name(c); 
   return r; 
} 
int main() 
{  int n; 
   cout << "Please enter a positive integer: "; 
   cin >> n; 
   cout << int_name(n); 
   return 0; 
}

Please enter a positive integer: 20119 
 twenty thousand one hundred nineteen

Проиграване:
int_name(20119)
n: 20119
c: 20119 | 119 | 19 | 0
r: twenty | twenty thousand | twenty thousand one hundred | twenty thousand one hundred nineteen

int_name(20)
n: 20
c: 20 | 0
r: twenty