11. Вход, симулации и тестване

План:
Четене на данни и пренасочване на входния и изходния потоци
Генератор на случайни числа и симулации
Самостоятелно тестване на функции
Подбор на тестови примери
Оценка на резултатите от тестването
Макрос assert
Трасиране на програмата


** Четене на данни и пренасочване на входния и изходния потоци

Конструкцията в C и C++
while (cin >> x)
чете данни от стндартния вход докато не се появи грешка във входния поток или край на входния поток.

Грешка във входния поток се появява когато има несъответствие между типа на променливата x и типа на въведената стойност.

Край на входния поток се задава, като се въведе специален символ за край на входа (край на файла).
За операционната система Windows този символ е Ctrl-Z (ASCII 26),  за UNIX и MAC OS - Ctrl D (ASCII 04).

Пример: Брой на думите от входния поток.
Дума е низ без интервали. Разделители между думите са интервали или край на ред (white space).

// words.cpp
#include
<iostream> #include <string> using namespace std; int main() {  int count = 0;    string word;    while (cin >> word) count++;    cout << count << " words." << endl;    return 0; }
nkirov@cpp % c++ words.cpp
nkirov@cpp % ./a.out
abc xyz 123
words
stop
5 words.
nkirov@cpp %


* Пренасочване на стандартния вход
(обекта cin).
    C:\my\>myprog < a_cin.txt
Вместо от клавиатурата, операционната система пренасочва входа от текстов файл (в случая това е файла 
a_cin.txt).
По този начин програмата за намиране на броя на думите може намери броя на думите в текстов файл (едно по-смислено приложение на тази програма).

Пример: Брой на думите във файла
words.cpp (за macOS):

nkirov@MBP-5 CIT107 %c++ words.cpp
nkirov@MBP-5 CIT107 %./a.out < words.cpp
30 words.

* Пренасочване на стандартния изход (
обекта cout) от конзолaта (терминала) в MS Windows:
    C:\my\>myprog > a_cout.txt 
Вместо на екрана, операционната система пренасочва изхода на текстов файл (в случая това е файла a_cout.txt).

* Четене на редове
Текстов файл - съдържа ASCII кодове 32-255 и символи за край на ред.
В различни операционни системи:
- Unix - LF (Line feed, '\n', 0x0A, 10 in decimal)
- MAC OS - CR (Carriage return, '\r', 0x0D, 13 in decimal)
- Windows - LF CR
Константа endl от потоковата библиотека служи за извеждане на "край на ред" по стандарта на съответната ОС.
За четене на редове в C++ се използва функцията getline. Тя връща true при нормално състояние на входа и false при грешка във входа или край на входа.
string line;
while (getline(cin, line))
{
process
line
}
Пример: Брой на редовете от входния поток.

// lines.cpp
#include <iostream>
#include <string>
using namespace std;

int main()
{  int count = 0;
   string lines;
   while (getline(cin, lines)) count++;
   cout << count << " lines." << endl;
   return 0;
}
nkirov@cpp % c++ lines.cpp
nkirov@cpp % ./a.out
first line
secod line

one empty line
4 lines.

Пример: Брой на редовете във файла lines.cpp (за macOS):

nkirov@MBP-5 CIT107 %c++ lines.cpp
nkirov@MBP-5 CIT107 %./a.out < lines.cpp
11 lines.
 
* Четене на символи - букви, цифри и специални символи
За въвеждане на символ по символ (включително с ASCII кодове < 32) се използва член-функцията get:
char ch;
while (cin.get(ch))
{
process
ch
}
Пример: Брой на символите (знаците) във входния поток.

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

int main()
{  int count = 0;
   char ch;
   while (cin.get(ch)) count++;
   cout << count << " chars." << endl;
   return 0;
}
nkirov@cpp % c++ chars.cpp
nkirov@cpp % ./a.out
12345
aaaaa
12 chars.
nkirov@cpp %

Пример: Брой на байтовете във файла chars.cpp (за macOS):

nkirov@MBP-5 CIT107 %c++ chars.cpp
nkirov@MBP-5 CIT107 %./a.out < chars.cpp
163 chars.


**
Генератор на случайни числа и симулации

Симулация е решаване на задача с генериране на случайни събития и оценката на техните резултати.
C++ библиотеката разполага с генератор на случайни числа, който произвежда числа, чието поведение прилича на случайно.
Функцията rand() връща случайно число между 0 и RAND_MAX (обикновено 32 767).
Тези числа действително произхождат от дълга редица от числа, изчислени от доста прости формули; те имат поведение (свойства) на случайни числа.
Поради тази причина те често се наричат псевдослучайни числа.

Пример: Следващатата програма отпечатва редица от случайни числа, като редицата е същата всеки път, когато програмата се изпълнява (защото числата са получени с формула).
int main()
{ int i;
for (i = 1; i <= 10; i++)
{
int r = rand();
cout << r << "\n";
}
return 0;
}
random.cpp
Time now;
int seed = now.seconds_from(Time(0,0,0));
srand(seed);
За генериране на случайни цели числа в интервала  [a, b] се използва следната функция:
int rand_int(int a, int b)
{
return a + rand() % (b - a + 1);
}
Пример: Хвърляне на зарове.
// dice.cpp
#include
<iostream> #include <string> #include <cstdlib> #include <ctime> using namespace std; /**    Sets the seed of the random number generator. */ void rand_seed() {  int seed = static_cast<int>(time(0));    srand(seed); } /**     Compute a random integer in a range    @param a the bottom of the range    @param b the top of the range    @return a random integer x, a <= x and x <= b */ int rand_int(int a, int b) {  return a + rand() % (b - a + 1); } int main() {  rand_seed();    int i;    for (i = 1; i <= 10; i++)    {  int d1 = rand_int(1, 6);       int d2 = rand_int(1, 6);       cout << d1 << " " << d2 << "\n";    }    cout << "\n";    return 0; }
nkirov@cpp % c++ dice.cpp
nkirov@cpp % ./a.out
2 1
1 6
2 3
5 1

nkirov@cpp % ./a.out
5 3
3 2
3 1
5 1


Генериране на случайни числа тип double в интервала  [a, b]:
double rand_double(double a, double b)
{
return a + (b - a) * rand() * (1.0 / RAND_MAX);
}

Пример: Иглата на Бюфон (за самостоятелно изучаване).
Игла с дължина 1 инч (2.54 см) се пуска върху лист хартия, на който са начертани хоризонтални линии, намиращи се на разстояние 2 инча една от друга.
Ако иглата падне върху такава линия, казваме, че имаме попадение.

Хипотезата на Бюфон е, че отношението на броя на всички опити към броя на попаденията клони към числото pi. 

// buffon.cpp
 
#include <iostream> 
#include <cstdlib> 
#include <cmath> 
#include <ctime> 
using namespace std;

void rand_seed() 
/* ЦЕЛ: Инициализира генератора за случайни числа 
*/ 
{  int seed = static_cast<int>(time(0)); 
   srand(seed); 
} 
double rand_double(double a, double b) 
/* ЦЕЛ:  намира случайно число в интервала [a, b] 
   ПОЛУЧАВА: границите на интервала 
   ВРЪЩА: случайно число x, a <= x и x <= b 
*/ 
{  return a + (b - a)*rand()*(1.0/RAND_MAX); 
} 
double deg2rad(double alpha) 
/* ЦЕЛ:  превръща градуси в радиани 
   ПОЛУЧАВА: alpha - големина на ъгъл в градуси 
   ВРЪЩА:  големината на ъгъла в радиани 
*/ 
{  const double PI = 3.141592653589793; 
   return alpha * PI / 180; 
} 
int main() 
{  int NTRIES = 10000; 
   int i; 
   int hits = 0; 
   rand_seed(); 
   for (i = 1; i <= NTRIES; i++) 
   {  double ylow = rand_double(0, 2); 
      double angle = rand_double(0, 180); 
      double yhigh = ylow + sin(deg2rad(angle)); 
      if (yhigh >= 2) hits++; 
   } 
   cout << "Tries / Hits = " << NTRIES*(1.0/hits) << "\n"; 
   return 0; 
}

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

** Самостоятелно тестване на функции

* Данните, с които ще се тества функцията, се получават по 3 начина: 

-- от входния поток (от клавиатура или от текстов файл с пренасочване на входния поток); 
-- като стойности, получени от цикъл; 
-- случайни числа.

* Примери за тестване на функцията squareroot за намиране на квадратен корен по метода на Херон (Babylonian method).
x0 = axn+1 = ( xn + a/xn ) / 2, n = 0,1, 2, 3, ...

Първи пример - данните идват от входния поток:

// sqrtest1.cpp 
#include
<iostream>
#include <cmath>
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;
}

/* Function to be tested */
/**
   Computes the square root using Heron's formula
   @param a an integer >= 0
   @return the square root of a
*/
double squareroot(double a)
{ 
if
(a == 0) return 0;

   double xnew = a;
   double xold;
   do
   {  xold = xnew;
      xnew = (xold + a / xold) / 2;
   }
   while (!approx_equal(xnew, xold));
   return xnew;
}

/* Test harness */
int main()
{ 
double x;
   while (cin >> x)
   { 
double y = squareroot(x);
      cout << "squareroot of " << x << " = " << y << "\n";
   }
   return 0;
}
25 
squareroot of 25 = 5 
3 
squareroot of 3 = 1.73205 
q

Втори пример - входните стойности на функцията се генерират от цикъл.

// sqrtest2.cpp 
/* Test harness */
int main()
{ 
double
x;
   for (x = 0; x <= 10; x = x + 0.5)
   {  double y = squareroot(x);
      cout << "squareroot of " << x << " = " << y << "\n";
   }
   return 0;
}
squareroot of 0 = 0 
squareroot of 0.5 = 0.707107 
squareroot of 1 = 1 
squareroot of 1.5 = 1.22474 
squareroot of 2 = 1.41421

Трети пример - входните стойности се получават от генератор за случайни числа. 

// sqrtest3.cpp 
#include
<iostream>
#include <cstdlib>
#include <cmath>
#include <ctime>
using namespace std;

/**
   Sets the seed of the random number generator.
*/
void rand_seed()
{ 
int
seed = static_cast<int>(time(0));
   srand(seed);
}

/** 
   Compute a random floating point number in a range
   @param a the bottom of the range
   @param b the top of the range
   @return a random floating point number x, 
   a <= x and x <= b
*/
double rand_double(double a, double b)
{ 
return
a + (b - a) * rand() * (1.0 / RAND_MAX);
}

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

/* Function to be tested */
/**
   Computes the square root using Heron's formula
   @param a an integer >= 0
   @return the square root of a
*/
double squareroot(double a)
{ 
if
(a == 0) return 0;

   double xnew = a;
   double xold;
   do
   { 
xold = xnew;
      xnew = (xold + a / xold) / 2;
   }
   while (!approx_equal(xnew, xold));
   return xnew;
}

/* Test harness */
int main()
{ 
rand_seed
();
   int i;
   for (i = 1; i <= 100; i++)
   { 
double
x = rand_double(0, 1E6);
      double y = squareroot(x);
      cout << "squareroot of " << x << " = " << y << "\n";
   }
   return 0;
}
squareroot of 185949 = 431.218 
squareroot of 680715 = 825.055 
squareroot of 17883.8 = 133.73 
squareroot of 238868 = 488.742

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

1. Докато се пише програмата, трябва да имаме прост тестов пример, на който знаем решението. 
2. Програмата се проверява с други тестови примери, също с известни решения - позитивни тестове. 
3. Включват се и граничните случаи.
- За функцията squareroot това са 0, големи числа (напр. 1Е20) и числа, близки до 0 (напр. 1е-20).
Целта е да се определят границите на параметрите, за които функцията работи вярно. 
4. Функцията се проверява с негативни тестови примери - некоректни входни данни.
- Такива за squareroot са отрицателни стойности на параметъра.
 
Използване на файл за запазване на тестовия вход и изпълнение с пренасочване на входния и изходния потоци.

>sqrtest1 < test.in > test.out



**
Оценка на резултатите от тестването

* Как можем да оценим дали върнатата стойност от функцията е вярна?
- Като предварително знаем решението; 
- С програма за проверка на върнатите стойности; 
- С "оракул" - друга функция, която решава същата задача (възможно бавно и неефективно!). 

* За нашия пример можем да сравним квадрата на върнатото число от функцията с входната стойност.
Ето тестващото гнездо: 

// sqrtest4.cpp
/* Test harness */
int main()
{ 
int
i;
   for (i = 1; i <= 100; i++)
   { 
double
x = rand_double(0, 1E6);
      double y = squareroot(x);
      if (
!approx_equal(y * y, x)) cout << "Test failed. ";
      else cout << "Test passed. ";
      cout << "squareroot of " << x << " = " << y << "\n";
   }
   return 0;
}
nkirov@cpp % c++ sqrtest4.cpp
nkirov@cpp % ./a.out
Test passed. squareroot of 7.82637 = 2.79756
Test passed. squareroot of 131538 = 362.681
Test passed. squareroot of 755605 = 869.256
Test passed. squareroot of 458650 = 677.237


*  За оракул можем да използваме стандартната аритметична функция 
pow(x,0.5)или функцията sqrt(x).

// sqrtest5.cpp
/* Тестващо гнездо */
 
int main() 
{ 
   rand_seed(); 
   int i; 
   for (i = 1; i <= 100; i++) 
   { 
      double x = rand_double(0, 1E6);
 
      double y = squareroot(x); 
      if (!approx_equal(y, pow(x, 0.5))) cout << "Test failed. "; 
      else                              cout << "Test passed. "; 
      cout << "squareroot of " << x << " = " << y << "\n"; 
   } 
   return 0; 
}
 
nkirov@cpp % c++ sqrtest5.cpp
nkirov@cpp % ./a.out
Test passed. squareroot of 300597 = 548.267
Test passed. squareroot of 135541 = 368.159
Test passed. squareroot of 41061.9 = 202.637
Test passed. squareroot of 127746 = 357.416


** Макрос assert (Предусловия и макрос assert)

Функциите често съдържат неявни предположения - напр. знаменатели трябва да са различни от нула, заплатите не трябва да бъдат отрицателни и т.н.
Такива "незаконни" стойности могат "да се вмъкнат" в програмата от входа или в резултат на предишна грешка.

Какво трябва да се направи, когато дадена функция е извикана с неподходяща стойност (напр sqrt(-1))?
Най-бруталният начин е отпечатване на съобщение и прекратяване на цялата програма.
Използване на изключения - C++ поддържа сложен механизъм, наречен обработка на изключения.
Когато е възможно, е желателно да се избегне прекъсване на програмата (въпреки, че е трудно).

Макрос assert осигурява ценна проверка на "здрав разум" в етапа на тестване на програмата.

Пример: Промяна на заплата.
void raise_salary(Employee& e, double by)
{
assert(e.get_salary() >= 0 );
assert(by >= -100);

double new_salary = e.get_salary() * (1 + by / 100);
e.set_salary(new_salary);
}
Ако условието не е изпълнено, програмата завършва с полезно съобщение за грешка и показва номера на реда в текста на програмата.
assertion failed in file finclac.cpp line 61: by >= -100
Това е сигнал, че нещо се е объркало другаде и че програмата се нуждае от по-нататъшно тестване.
За да се използва макроса, трябва да се добави #include<cassert>.

Пример: Сложна лихва.

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

double future_value(double initial_value, double p, int n)
{  assert(p > 0);
   assert(n > 0);
   return initial_value * pow(1 + p / 100, n);
}

int main()
{  cout << "Please enter the interest rate in percent: ";
   double rate;
   cin >> rate;

   double balance = future_value(1000, rate, 10);
   cout << "After 10 years, the balance is "
        << balance << "\n";

   return 0;
}
nkirov@cpp % c++ futval0.cpp
nkirov@cpp % ./a.out
Please enter the interest rate in percent: 10
After 10 years, the balance is 2593.74
nkirov@cpp % ./a.out
Please enter the interest rate in percent: -10
Assertion failed: (p > 0), function future_value, file futval0.cpp, line 9.
zsh: abort      ./a.out
nkirov@cpp %



** Трасиране на програмата

За да се получи разпечатка за работата на програмата, се вмъкват съобщения (контролни печати) в началото и в края на всяка функция.
Полезно е отпечатване на входните параметри, когато се влиза във функцията и отпечатване на стойността за връщане, когато се излиза от функцията.
string int_name(int n)
{ cout << "Entering digit_name. n = " << n << "\n";
...
cout << "Exiting digit name. Return value = "
<< s << "\n";
return s;
}
[trace.cpp]

Трудности при трасиране:
-- За вмъкване на подходящи контролни печати се иска доста време.
-- Ако са добавени твърде много съобщения, ще се получи лавина от печат, който трудно се анализира.
-- Ако са добавени твърде малко съобщения, може да не се получи достатъчно информация къде е грешката.
-- Когато се завърши програмата, всички контролни печати трябва да бъдат изтрити.
-- Ако отново се появи грешка, контролните печати трябва да се пишат отново.

 Професионалните програмисти използват дебъгер за откриване на грешки в текста на програмата.


** Дебъгер

Debugging with GDB

Упражнения