2. Полиморфизъм. Потоци I

План:
Пренасочване на входния и изходния потоци
Четене и запис на текстови файлове
Йерархия на потоковите класове
Абстрактни класове

Полиморфизъм

Дефинираме вектор от часовници (обекти от базовия клас) и използваме цикъл за показване на часа.
vector<Clock> clocks(3);
/* populate clocks */
clocks[0] = Clock(true);
clocks[1] = TravelClock(true, "Rome", -1);
clocks[2] = TravelClock(false, "Tokyo", 5);

for (int i = 0; i < clocks.size(); i++)
cout << clocks[i].get_location() << " time: "
<< clocks[i].get_hours() << ":"
<< setw(2) << setfill('0')
<< clocks[i].get_minutes()
<< setfill(' ') << "\n";
[ clocks2a.cpp

Резултатът от работата на програмата е:
Local time is 21:15
Local time is 21:15
Local time is 9:15
Защо?
- Тъй като обект от производния клас е обект и от базовия клас, то можем да използваме операция присвояване с ляв аргумент обект от (базовия клас) Clock и  десен аргумент - обект от (производния клас) TravelClock (обратното не е вярно).
- Ефектът обаче е отрязване на даните (при операция присвояване), дефинирани в производния клас, т.е. на location и time_difference.

Можем да използваме указатели към базовия клас за елементи на вектора, тъй като указател от базов клас може съдържа адрес на обект от производен клас (обратното не е вярно).
Бележка: Указателите от различни класове имат една и съща дължина (няма отрязване).
vector<Clock*> clocks(3);
/* populate clocks */
clocks[0] = new Clock(true);
clocks[1] = new TravelClock(true, "Rome", -1);
clocks[2] = new TravelClock(false, "Tokyo", 5);

for (int i = 0; i < clocks.size(); i++)
cout << clocks[i]->get_location() << " time: "
<< clocks[i]->get_hours() << ":"
<< setw(2) << setfill('0')
<< clocks[i]->get_minutes()
<< setfill(' ') << "\n";
Векторът clocks съдържа колекция от различни часовници - указатели към обекти от различни класове. Такава колекция се нарича полиморфна.
[ clocks2b.cpp

Резултатът от новия вариант е същия:
Local time is 21:15
Local time is 21:15
Local time is 9:15
Защо?
Указателите са дефинирани от базовия клас (Clock) и съответно с тях се вика функция от същия клас.

Това свързване на обект (неявен параметър, напр. clocks[i]) и член-функция (напр. get_location()) като аргументи на операция стрелка, се нарича статично свързване (clocks[i]->get_location()) .

В С++ може да се реализира и динамично свързване - когато се вика член-функция (втори аргумент на операция стрелка) от класа, от който е обектът, към който сочи указателя (първи аргумент на операция стрелка).
Напр.
-- clocks[0]->get_location()
- clocks[0] е указател от базовия клас Clock, съдържа адрес на обект от базовия клас Clock(true) и get_location() е член-функция от базовия клас Clock;
-- clocks[1]->get_location() - clocks[1] е указател от базовия клас Clock, съдържа адрес на обект от производния клас TravelClock(true, "Rome", -1) и get_location() е член-функция от производния клас TravelClock.

За такова динамично свързване е необходимо функциите да се обяват като виртуални.
class Clock  {
public:
Clock(bool use_military);
virtual string get_location() const;
virtual int get_hours() const;
int get_minutes() const;
bool is_military() const;
private:
. . .
};
Ето реализация на цялата програма с динамично свързване:
 // clocks3.cpp
#include
<iostream> #include <iomanip> #include <string> #include <vector> using namespace std; #include "ccc_time.h" class Clock { public: Clock(bool use_military); virtual string get_location() const; virtual int get_hours() const; int get_minutes() const; bool is_military() const; private: bool military; }; Clock::Clock(bool use_military) { military = use_military; } string Clock::get_location() const { return "Local"; } int Clock::get_hours() const { Time now; int hours = now.get_hours(); if (military) return hours; if (hours == 0) return 12; else if (hours > 12) return hours - 12; else return hours; } int Clock::get_minutes() const { Time now; return now.get_minutes(); } bool Clock::is_military() const { return military; } class TravelClock : public Clock { public: TravelClock(bool mil, string loc, int diff); string get_location() const; int get_hours() const; private: string location; int time_difference; }; TravelClock::TravelClock(bool mil, string loc, int diff) : Clock(mil) { location = loc; time_difference = diff; while (time_difference < 0)
time_difference = time_difference + 24; } string TravelClock::get_location() const { return location; } int TravelClock::get_hours() const { int h = Clock::get_hours(); if (is_military()) return (h + time_difference) % 24; else { h = (h + time_difference) % 12; if (h == 0) return 12; else return h; } } int main() { vector<Clock*> clocks(3); clocks[0] = new Clock(true); clocks[1] = new TravelClock(true, "Rome", -1); clocks[2] = new TravelClock(false, "Tokyo", 5); for (int i = 0; i < clocks.size(); i++) { cout << clocks[i]->get_location() << " time: "
<< clocks[i]->get_hours() << ":" << setw(2) << setfill('0')
<< clocks[i]->get_minutes()
<< setfill(' ') << "\n"; } return 0; }
Резултатът е:

Local time is 18:07
Rome time is 17:07
Tokyo time is 11:07

Когато в класа има виртуални член-функции, компилаторът създава виртуална таблица (virtual table), в която са описани възможните връзки обект-функция.
Когато програмата се изпълнява, при всяко извикване на виртуална член-функция се преглежда таблицата, за да се намери функция от кой клас да се изпълни, съответстваща на типа на обекта, с който се вика функцията (динамично свързване).

Потоци

Пренасочване на входния и изходния потоци (преговор)

Пренасочване на стандартния вход (обекта cin).
C:\my\>myprog < a.txt

Вместо от клавиатурата, операционната система пренасочва входа от текстов файл, в случая това е файла a.txt.
 
 C:\my\>myprog > b.txt
Вместо на екрана, операционната система пренасочва изхода на текстов файл, в случая това е файла b.txt. 

Пример: Четене на думи.
// words.cpp
#include
<iostream> #include <string> using namespace std; int main() {  int count = 0;    string word;    while (cin >> word) count++;    cout << count << " words.\n";    return 0;
}

С пренасочване на входа програмата words намира броя на думите в текстов файл.
Пример: Четене на редове.
// lines.cpp
#include <iostream>
#include <string>
using namespace std;

int main()
{ int count = 0;
string lines;
while (getline(cin, lines)) count++;
cout << count << " lines.\n";
return 0;
}
С пренасочване на входа програмата lines намира броя на редовете в текстов
      файл.
Пример: Четене на символи (букви, цифри и специални символи).
// chars.cpp
#include <iostream>
using namespace std;

int main()
{ int count = 0;
char ch;
while (cin.get(ch)) count++;
cout << count << " chars.\n";
return 0;
}

С пренасочване на входа програмата chars намира броя на байтовете на текстов файл.

Четене и запис на текстови файлове.

** В заглавния файл fstream.h са дефинирани обекти, функции и операции за работа с файлове.

А) Четене от файл.

* Дефиниране на файлова променлива за четене - обект от класа ifstream:
    ifstream inp_data;
 
* Отваряне на файл от текущата директория с име input.dat:
    inp_data.open("input.dat");
 
* Четене от файл на:
 - цели числа и числа с плаваща точка - с операция входен поток
   int n;
   double x;
   inp_data >> n >> x;
 - низове с операция входен поток и функция getline
   string s;
   inp_data >> s;        // чете до разделител
   getline(inp_data, s); // чете цял ред
 - символи с член-функция get
   char ch;
   inp_data.get(ch);    // чете един символ
   inp_data.unget();    // връща последния прочетен символ в буфера
 
* Затваряне на файл
   inp_data.close();

Пример: Четене на числа от файл, намиране и отпечатване на най-голямото число.

// maxval1.cpp
#include <string>

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

/**
   Reads numbers from a file and finds the maximum value
   @param in the input stream to read from
   @return the maximum value or 0 if the file has no numbers
*/           
double read_data(ifstream& in)
{  double highest;
   double next;
   if (in >> next) highest = next;
   else return 0;

   while (in >> next)
     if (next > highest) highest = next;
  
   return highest;
}

int main()
{  string filename;
   cout << "Please enter the data file name: ";
   cin >> filename;

   ifstream infile;
   infile.open(filename.c_str());

   if (infile.fail())
   {  cout << "Error opening " << filename << "\n";
      return 1;
   }

   double max = read_data(infile);
   cout << "The maximum value is " << max << "\n";

   infile.close();
   return 0;
}

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

Пример: Отделяне на цифрови от текстови данни.

// readfile.cpp
#include <string>

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

int main()
{  cout << "Enter the file name: ";
   string fname;
   cin >> fname;
  
   ifstream infile;
   infile.open(fname.c_str());
   if (infile.fail())
   {  cout << "ERROR";
      return 1;           
   }
  
   int sum = 0;
   while (!infile.eof())
   {  char ch;
      infile.get(ch);
      if ('0' <= ch && ch <= '9') /* it was a digit */
      {  infile.unget();          /* oops - didn't want to read it */
         int n;
         infile >> n;             /* read integer starting with ch */
         sum += n;
      }
      else cout << ch;
   }
   infile.close();
   cout << "The sum is " << sum << endl;
   return 0;
}

Б) Писане във файл.

* дефиниране на файлова променлива за писане - обект от класа ofstream:
   ofstream out_data;
 
* отваряне на файл
   out_data.open("output.dat");
Ако в текущата папка съществува файл с това име, операционната система го изтрива и създава празен файл с това име.

* писане във  файл с:
 - изходен поток
   int n = 2;
   double x = 1.5;
   string s = " Hello";
   out_data << n << "  " << x << s;
 - член-функция
   char ch = 'A';
   out_data.put(ch);
 
* затваряне на файл
   out_data.close();

Пример: Отделяне на цифрови от текстови данни и записване на числата във файл.
// readwritefiles.cpp
#include <string> #include <iostream> #include <fstream> using namespace std; int main() { cout << "Enter the input file name: "; string finame; cin >> finame; ifstream infile; infile.open(finame.c_str()); if (infile.fail()) { cout << "Error opening " << finame << endl; return 1; } cout << "Enter the output file name: "; string foname; cin >> foname; ofstream outfile; outfile.open(foname.c_str()); if (outfile.fail()) { cout << "Error opening " << foname << endl;; return 1; } while (!infile.eof()) { char ch; infile.get(ch); if ('0' <= ch && ch <= '9') /* it was a digit */ { infile.unget(); /* oops - didn't want to read it */ int n; infile >> n; /* read integer starting with ch */ outfile << n << endl; } else cout << ch; } infile.close(); outfile.close(); return 0; }

** Йерархия на потоковите класове

* Потоковата библиотека на С++ се състои от няколко класа, свързани в следната йерархия:


* Има още един (най-базов) клас с име ios, които е базов клас на istream и ostream (и напряк базов клас за всички други класове в йерархията).
* Обектите cin и cout са от класове, които са системно зависими и нямат стандартни имена, но са производни класове съответно на  istream и ostream.

ios

Затова можем да дефинираме:

double read_data(istream& in);

и след това да извикаме функцията с параметри от различни производни на istream класове: .

max = read_data(infile);
max = read_data(cin);

Пример: Четене на числа от файл или стандартен вход, намиране и отпечатване на най-голямото число.

// maxval2.cpp
#include <string> #include <iostream> #include <fstream> using namespace std; /** Reads numbers from a file and finds the maximum value @param in the input stream to read from @return the maximum value or 0 if the file has no numbers */ double read_data(istream& in) { double highest; double next; if (in >> next) highest = next; else return 0; while (in >> next) if (next > highest) highest = next; return highest; } int main() { double max; string input; cout << "Do you want to read from a file? (y/n) "; cin >> input; if (input == "y") { string filename; cout << "Please enter the data file name: "; cin >> filename; ifstream infile; infile.open(filename.c_str()); if (infile.fail()) { cout << "Error opening " << filename << "\n"; return 1; } max = read_data(infile); infile.close(); } else max = read_data(cin); cout << "The maximum value is " << max << "\n"; return 0; }

** Абстрактни класове

Пример: Йерархия на геометрични фигури в равнината.

- Ако искаме да имаме полиморфна функция за лице на фигура - area, то тази функция трябва да бъде дефинирана в базовия клас Shape.
- Тази функция има смисъл за всеки клас, производен на класа Shape, но в класа Shape тя няма смисъл.
- Такава функция се декларира по специален начин в базовия клас и се нарича чисто виртуална член-функция.
class Shape {
...
virtual double area() const = 0;
};

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

Пример:
class Shape {
public:

    virtual double perimeter() const = 0;
    virtual double area() const = 0;
};

class Triangle: public Shape {
public:
    ...

    virtual double perimeter() const
    { return a + b + c; }
    virtual double area() const
    {   double p = (a + b + c)/2;
        return sqrt(p*(p-a)*(p-b)*(p-c));
    }
private:
    double a, b, c;
};

class Circle: public Shape {
public:
    ...
    virtual double perimeter() const
    { return 2*PI*r; }
    virtual double area() const
    { return PI*r*r; }
private:
    double r;
};
...
int main()
{
//  Shape* s = new Shape;               // ERROR!!
    Shape* s = new Triangle(4, 5, 6);   // First s is a triangle

    cout << "Area is " << s.area() << "\n";   
    s = new Circle(10);                 // Now it is a square
    cout << "Area is " << s.area() << "\n";
    ...
    return 0;
}