Група 49 Иноформатика 1 курс Материали

Упражнения

19/05/2009 · Вашият коментар

Упражнение 3-1

Наследяване. Производни класове – дефиниране, достъп

 

     Забележка: В това упражнение да не се използват конструктори, деструктори, операторна функция за присвояване и конструктор за присвояване.

 

Задача 1. Какъв е резултатът от изпълнението на програмата:

                #include <iostream.h>

class A

{public:

 void print(int n) const

 {cout << n << endl;

 }

 };

class D : public A

{public:

 void printD(int n) const

 {if (n<=1) print(n);

  else if (n%2 ==0) printD(n/2);

  else printD(3*n+1);

 }

};

int main()

{D d;

 d.printD(3);

 return 0;

}

Определете отговора без да компилирате и изпълнявате програмата.

     Отговорът е 1.

     Ще се промени ли поведението на програмата ако атрибутът за област в наследения клас D се промени от public в private или protected? (NE)

     Променете програмата като използвате един и същ идентификатор за извеждане print, т.е.

#include <iostream.h>

class A

{public:

 void print(int n) const

 {cout << n << endl;

 }

 };

class D : public A

{public:

 void print(int n) const

 {if (n<=1) A::print(n);    // пълно име

  else if (n%2 ==0) print(n/2);

  else print(3*n+1);

 }

};

int main()

{D d;

 d.print(3);

 return 0;

}

Изкажете правилото за приоритета на локалното име (вижте лекцията).

    Задача 2. Да се дефинира клас “Точка в равнината”. Като се използва този клас, да се определи клас “Точка в тримерното пространство”. И най-накрая да се определи класът “Точка в тримерното пространство с цвят”, наследник на клас “Точка в тримерното пространство”. Цвят се задава чрез цяло число.

 

     Една примерна програма е:

#include <iostream.h>

class Point2

{public:

 void ReadPoint2(int absc = 0, int ord = 0)

 {x = absc;

  y = ord;

 }

 void PrintPoint2()

 {cout << x << „, “ << y ;

 }

 private:

  int x, y;

 };

class Point3 : public Point2

{public:

 void ReadPoint3(int a, int o, int b)

 {ReadPoint2(a, o);

  z = b;

 }

 void PrintPoint3()

 {PrintPoint2();

  cout << „, “ << z << endl;

 }

 private:

  int z;

};

class ColPoint3 : public Point3

{public:

 void ReadColPoint3(int a, int o, int b, int c)

 {ReadPoint3(a, o, b);

  col = c;

 }

 void PrintColPoint3()

 {PrintPoint3();

  cout << „collor: “ << col << endl;

 }

 private:

  int col;

};

 

int main()

{Point2 p2;

 p2.ReadPoint2(5,10);

 p2.PrintPoint2();

 cout << endl;

 Point3 p3;

 p3.ReadPoint3(2,4,6);

 p3.PrintPoint3();

 ColPoint3 p4;

 p4.ReadColPoint3(2,4,6, 111);

 p4.PrintColPoint3();

 return 0;

}

Променете атрибутите за област и определете:

а) вида на наследяване на компонентите;

б) достъпа на член-функциите и на p1, p2, p3, p4 до компонентите на базовите класове.

Задача 3. Дефинирайте следната йерархия:

 

base

 

    der1/public         der2/private           der3/protected

 

der11/public       der21/protected       der31/private

 

der12/protected         der22/private      der32/public

 

       
       
 

 

 

 

der13/private           der23/protected             der33/public

и определете:

     а) вида на наследяване на компонентите;

б) достъпа на член-функциите и на обектите и външните функции до компонентите на базовите класове.

(оставете ги сами да работят, след което напишете част от йерархията)

    

     Други задачи.

Упражнение 3-2

Наследяване. Производни класове –

конструктори, деструктори

 

Задача 1. Какъв е резултатът от изпълнението на програмата?

 

#include <iostream.h>

class base

{public:

 void init(int x)

 {bx = x;

 }

 void display() const

 {cout << “ class base: bx= “ << bx << endl;

 }

 protected:

  int bx;

 private:

// …

};

class der: public base

{public:

 void init(int x)

 {bx = x;

  base::bx = x + 5;

 }

 void display() const

 {cout << “ class der: bx = “ << bx;

  cout << “ base::bx = “ << base::bx << endl;

 }

 protected:

  int bx;

 private:

 //…

};

class derder: public der

{public:

 void init(int x)

 {bx = x;

  base::bx = x + 5;

  der::bx = x + 10;

 }

 void display() const

 {cout << “ class der-der: bx = “ << bx;

 cout << “ class der: bx = “ << der::bx;

  cout << “ base::bx = “ << base::bx << endl;

 }

 protected:

  int bx;

 private:

 //…

};

void main()

{base b;

 der d;

 derder dd;

 b.init(5); d.init(10); dd.init(100);

 b.display(); d.display(); dd.display();

 d.base::init(20);

 d.base::display();

 d.display();

 b.display();

}

Задача 2. Какъв ще е резултатът от изпълнението на програмата:

#include <iostream.h>

class B

{public:

  B();

  B(int n);

};

B::B()

{cout << “B::B()\n”;

}

B::B(int n)

{cout << “B::B(“ << n << “)\n”;

}

class D : public B

{public:

 D();

 D(int n);

 private: B b;

};

D::D()

{cout << “D::D()\n”;

}

D::D(int n) : B(n)

{b = B(-n);

 cout << “D::D(“ << n << “)\n”;

}

 int main()

 {D d(3);

 return 0;

 }

Определете отговора на ръка – без да използвате компютър.

Отг.

B::B(3)

B::B()

B::B(-3)

D::D(3)

Обяснете резултата от втората линия (заради наличието на декларацията B b;, определяща обект). След това променете програмата в:

#include <iostream.h>

class B

{public:

  B();

  B(int n);

};

B::B()

{cout << „B::B()\n“;

}

B::B(int n)

{cout << „B::B(“ << n << „)\n“;

}

class D : public B

{public:

 D();

 D(int n);

 private: B b;

          B c;     // нов фрагмент

};

D::D()

{cout << „D::D()\n“;

}

D::D(int n) : B(n)

{b = B(-n);

 cout << „D::D(“ << n << „)\n“;

}

 int main()

 {D d(3);

 return 0;

 }

Рез: два пъти ще се извика B() – за обекта b и за обекта c.

Задача 3. Намерете грешките в дефинициите на класовете:

     class B

{public:

 B();

 B(int n);

 void print() const;

 private:

 int b;

};

 B::B()

 {b=0;

 }

 B::B(int n)

 {b = n;

 }

 void B:: print() const

 {cout << “B: “ << b << endl;

 }

 class D : public B

 {public:

  D();

  D(int n);

  void print(int n) const;

  private:

  B b;

 };

 D::D()

 {}

 D::D(int n) : B(n)

 {b = n;

 }

 D::print()const

 {cout << “D: “ << b << endl;

 }

Поправете ги!!! Naprimer:

#include <iostream.h>

class B

{public:

 B();

 B(int n);

void print() const;

private:

 int b;

};

B::B()

{b=0;

}

B::B(int n)

{b = n;

}

void B:: print() const

{cout << „B: “ << b << endl;

}

class D : public B

{public:

 D();

 D(int n);

 void print() const;

 private:

 B b;

};

D::D()

{}

D::D(int n) : B(n)

{b = n;

}

void D::print()const

{cout << „D: “ ;

 b.print();

}

 int main()

 {D d(3);

  d.print();

 return 0;

 }

 

Упражнение 3

Наследяване. Дефиниране на конструктори за присвояване и

операторни функции за присвояване на производни класове

    

Задача 1. Изпълнете стъпка по стъпка програмата:

#include <iostream.h>

class base

{public:

 base(int x)

 {b = x;}

 base(const base &x)

 {b = x.b + 1;

 }

 protected:

  int b;

};

class der1 : public base

{public:

 der1(int x = 1) : base(x)

 {d = x;

 }

 der1(const der1& x): base(x)    // кой base се използва?

 {d = x.d + 2;

 }

 void Print()

 {cout << „der: “ << d << „  base: “ << b << endl;

 }

 private:

  int d;

};

void main()

{der1 d11(5);

 der1 d12 = d11;

 cout << „d11: „; d11.Print();

 cout << „d12: „; d12.Print();

}

Възможно ли е конструкторът за присвояване да се промени в:

der1(const der1& x): base(x+5)

 {d = x.d + 2;

 }

(не, защото + не е предефинирано за x от der1)

Задача 2. Какъв е резултатът от изпълнението на програмата:

#include <iostream.h>

class base

{public:

 base(int x)

 {b = x;}

 base& operator=(const base &x)

 {b = x.b + 1;

  return *this;

 }

 protected:

  int b;

};

class der1 : public base

{public:

der1(int x = 1) :base(x)

 {d = x;

 }

 der1& operator=(const der1& x)

 {d = x.d + 2;

  b = x.b + 3;

  return *this;

 }

 void Print()

 {cout << „der: “ << d << „  base: “ << b << endl;

 }

 private:

  int d;

};

class der2 : public base

{public:

der2(int x = 2) : base(x)

 {d = x;

 }

 der2& operator=(const der2& x)

 {d = x.d + 3;

  return *this;

 }

 void Print()

 {cout << „der: “ << d << „  base: “ << b << endl;

 }

 private:

  int d;

};

class der3 : public base

{public:

der3(int x = 3) : base(x)

 {d = x;

 }

 void Print()

 {cout << „der: “ << d << „  base: “ << b << endl;

 }

 private:

  int d;

};

void main()

{der1 d11(5), d12;

 der2 d21(5), d22;

 der3 d31(5), d32;

 d12 = d11;

 d22 = d21;

 d32 = d31;

 cout << „d11: „; d11.Print();

 cout << „d12: „; d12.Print();

 cout << „d21: „; d21.Print();

 cout << „d22: „; d22.Print();

 cout << „d31: „; d31.Print();

 cout << „d32: „; d32.Print();

}

рез:

d11: der: 5  base: 5

d12: der: 7  base: 8

d21: der: 5  base: 5

d22: der: 8  base: 2

d31: der: 5  base: 5

d32: der: 5  base: 6

Изпълнете стъпка по стъпка.

 

Задача 3. Като задача 2, малки промени има. Пак я изпълнете стъпка по стъпка. (Използвайте текста на първата задача, който сте написали вече на дъската)

 

#include <iostream.h>

class base

{public:

 protected:

  int b;

};

class der1 : public base

{public:

der1(int x = 1)

 {d = x;

 }

 der1& operator=(const der1& x)

 {d = x.d + 2;

  b = x.b + 3;

  return *this;

 }

 void Print()

 {cout << „der: “ << d << „  base: “ << b << endl;

 }

 private:

  int d;

};

class der2 : public base

{public:

 der2(int x = 2)

 {d = x;

 }

 der2& operator=(const der2& x)

 {d = x.d + 3;

  return *this;

 }

 void Print()

 {cout << „der: “ << d << „  base: “ << b << endl;

 }

 private:

  int d;

};

class der3 : public base

{public:

 der3(int x = 3)

 {d = x;

 }

 void Print()

 {cout << „der: “ << d << „  base: “ << b << endl;

 }

 private:

  int d;

};

void main()

{der1 d11(5), d12;

 der2 d21(5), d22;

 der3 d31(5), d32;

 d12 = d11;

 d22 = d21;

 d32 = d31;

 cout << „d11: „; d11.Print();

 cout << „d12: „; d12.Print();

 cout << „d21: „; d21.Print();

 cout << „d22: „; d22.Print();

 cout << „d31: „; d31.Print();

 cout << „d32: „; d32.Print();

}

Направете други (полезни според вас) промени и изпълнете стъпка по стъпка.

 

Задача 4. Напишете основен клас Worker (работник), който определя работник с име и заплащане за 1 час. Напишете два производни на Worker класа HourlyWorler (почасов работник) и SalariedWorker (щатен работник). За всеки вид работник са дадени: броят на часовете, които той е работил през седмицата; видът работа, която е извършвал (низ) /само един вид работа е работил/. Почасовият работник получава заплата за седмица по следното правило: Всеки час до 40 часа се заплаща по указаната цена. За всеки час от 41 до 60 се заплаща 1.5 пъти повече от указаната цена и за часовете на 60 – 2 пъти повече. Щатният работник получава заплата за 40 часа независомо колко е работил. Класовете да реализират голямата четворка /конструктор по подразбиране, деструктор, констр. за присвояване и операторна функция за присвояване/. Да се създадат масиви, съдържащи двата вида работници. Да се пресметне и изведе заплатата на всеки работник. Да се изведат работниците от всеки вид, сортирани по заплата.

    

Оставяте ги да работят самостоятелно. На лаборат упражн. да я изпълнят. Вижте решените задачи в лекциите. Преобразуване от вида:

     (Student)(*this) = (Student)st;

не се извършва във всяка реализация. Затова е заменено с

Student::operator=(st).

 

Дайте им и други задачи.

 

Упражнение 4

Преобразувания на типове. Шаблони на наследени класове

Задача 1. Има ли грешки в програмата? Ако не, какъв е резултатът от изпълнението й?

#include <iostream.h>

class base

{public:

 base(int x = 0)

 {b = x;

 }

 int get_b() const

 {return b;

 }

 void f()const

 {cout << „b: “ << b << endl;

 }

 void f1()const

 {cout << „f1:\n“;

 }

 private:

  int b;

 };

class der : public base

{public:

 der(int x = 0) : base(x)

 {d = 5;

 }

 int get_d() const

 {return d;

 }

 void f_der()const

 {cout << „class der: d: “ << d

          << “ b: “ << get_b() << endl;

 }

 private:

  int d;

 };

void main()

{void (base::*pb)()const = base::f;

 void (der::*pd)()const = pb;

 der y(20);

 (y.*pd)();

}

     Задача 2. Нека класът D е наследник на класа B. Кои от следните присвоявания са допустими?

B b;

D d;

B* pb;

D* pd;

b = d;

d = b;

pd = pb;

pb = pd;

d = pd;

b = *pd;

*pd = *pb;

 

Задача 3. Дефинирайте шаблон на клас Point3, определящ точка в тримерното пространство с координати от тип Т. Определете шаблон на производен клас ColPoint3 на класа Point, определящ точка в тримерното пространство с координати  от тип Т и цвят от тип U. Дефинирайте и шаблон на клас, определящ точка в тримерното пространство с координати от тип Т, цвят от тип U и тегло V. Дефинирайте масив от точки в тримерното пространство с цвят и тегло.

а) Намерете най-тежката точка от масива с цвят в диапазона [1, 5] и лежаща в рaвнината ax+by+cz = d (a, b, c, d са дадени реални числа).

б) Сортирайте в низходящ ред по тегло точките с цвят от диапазона [1, 10], лежащи в кълбото с център координатното начало и радиус R.

Други задачи.

Упражнение 5 и 6

Множествено наследяване. Виртуални класове и функции

 

 

Задача 1. Какъв резултат ще изведе следната програма:

 

#include <iostream.h>

class A

{public:

 A(int a = 1)

{n = a; x = 1.1;

 cout << „A: “ << n << „,“ << x << endl;

}

 private:

 int n;

 double x;

};

class B

{public:

 B(double b = 1)

{n = 2; y = b;

 cout << „B: “ << n << „,“ << y << endl;

}

 private:

 int n;

 double y;

};

class C : public B, public A

{public:

C(int x = 1, int y = 2, int z = 3, double u = 0.0):

A(x), B(y)

{n = z; m = x*y;

 cout << „C: “ << n << „,“ << m << endl;

}

 private:

 int n, m;

};

void main()

{C c1;

 C c2(2,4,6,1.0);

}

Нарисувайте разположението на обектите c1 и c2 в ОП. Ще се промени ли резултатът ако се променят атрибутите за област.

Задача 2. Да се изгради йерархията:

 

 
   

 

 

 

 

 

 

 

 

 

 

така, че A е виртуален за класовете B и C и не е виртуален за класа E. Класовете да съдържат голямата четворка.

(не съм гледала решението по-долу, взех го от стар файл, прожерете го)

 

#include <iostream.h>

#include <string.h>

class A

{public:

  A(char* = „“);

  ~A();

  A(const A&);

  A& operator=(const A &);

  void print() const;

 private:

  char* x;

 };

A::A(char* s)

{x = new char[strlen(s)+1];

 strcpy(x, s);

}

A::~A()

{delete x;

}

A::A(const A& p)

{x = new char[strlen(p.x)+1];

 strcpy(x, p.x);

}

A& A::operator=(const A& p)

{if (this != &p)

{delete x;

 x = new char[strlen(p.x)+1];

 strcpy(x, p.x);

}

 return *this;

}

void A::print() const

{cout << „A:: x “ << x << endl;

}

class B: virtual public A

{public:

  B(char* = „“, char* = „“);

  ~B();

  B(const B&);

  B& operator=(const B&);

  void print() const;

 private:

  char* x;

};

B::B(char* a, char* b): A(a)

{x = new char[strlen(b)+1];

 strcpy(x, b);

}

B::~B()

{delete x;

}

B::B(const B& p) : A(p)

{x = new char[strlen(p.x)+1];

 strcpy(x, p.x);

}

B& B::operator=(const B& p)

{if (this != &p)

{A::operator=(p);

 delete x;

 x = new char[strlen(p.x)+1];

 strcpy(x, p.x);

}

 return *this;

}

void B::print() const

{A::print();

 cout << „B:: x “ << x << endl;

}

class C: virtual public A

{public:

  C(char* = „“, char* = „“);

  ~C();

  C(const C&);

  C& operator=(const C&);

  void print() const;

 private:

  char* x;

};

C::C(char* a, char* b): A(a)

{x = new char[strlen(b)+1];

 strcpy(x, b);

}

C::~C()

{delete x;

}

C::C(const C& p) : A(p)

{x = new char[strlen(p.x)+1];

 strcpy(x, p.x);

}

C& C::operator=(const C& p)

{if (this != &p)

{A::operator=(p);

 delete x;

 x = new char[strlen(p.x)+1];

 strcpy(x, p.x);

}

 return *this;

}

void C::print() const

{A::print();

 cout << „C:: x “ << x << endl;

}

class E: public A

{public:

  E(char* = „“, char* = „“);

  ~E();

  E(const E&);

  E& operator=(const E&);

  void print() const;

 private:

  char* x;

};

E::E(char* a, char* b): A(a)

{x = new char[strlen(b)+1];

 strcpy(x, b);

}

E::~E()

{delete x;

}

E::E(const E& p) : A(p)

{x = new char[strlen(p.x)+1];

 strcpy(x, p.x);

}

E& E::operator=(const E& p)

{if (this != &p)

{A::operator=(p);

 delete x;

 x = new char[strlen(p.x)+1];

 strcpy(x, p.x);

}

 return *this;

}

void E::print() const

{A::print();

 cout << „E:: x “ << x << endl;

}

class D: public B, public C, public E

{public:

  D(char* a = „“, char* b = „“, char* c = „“,char* d = „“,

     char* e = „“) : A(a), B(a, b), C(a, c), E(a, d)

  {x = new char[strlen(e)+1];

   strcpy(x, e);

  }

  ~D()

  {cout << „~D(): \n“;

   delete x;

  }

  D(const D& p):  B(p), C(p), E(p)

  {x = new char[strlen(p.x)+1];

   strcpy(x, p.x);

  }

  D& operator=(const D&p)

  {if(this!=&p)

  {B::operator =(p);

   C::operator =(p);

   E::operator =(p);

   delete x;

   x = new char[strlen(p.x)+1];

   strcpy(x, p.x);

  }

  return *this;

  }

 void print() const;

 private:

  char* x;

};

void D::print() const

{B::print();

 C::print();

 E::print();

 cout << „D::x: “ << x << endl;

}

void main()

{D d(„Mimi“, „Toni“, „Liza“, „Lora“, „Vesi“);

 d.print();

 D d1, d2;

 d1 = d2 = d;

 d1.print();

 d2.print();

}

Коментирайте конструкторите на клас D. Този с 5 параметъра използва обръщение до конструктора на виртуалния клас А, а констр. за присвояване на D, не (не може да направи преобр. A(p)) и използва констр. по подразбиране. Променете конструктора за присвояване на класа D като използвате явно преобразуване:

D(const D& p): A((A)(B)p), B(p), C(p), E(p)

  {x = new char[strlen(p.x)+1];

   strcpy(x, p.x);

  }

или по другия клон /A((A)(C)p)/.

 

 

Задача 3. Коментирайте програмата:

#include <iostream.h>

class Base

{virtual void f()

 {cout << „Base – f()\n“;

 }

 virtual void g()

 {cout << „Base – g()\n“;

 }

 void h()

 {cout << „Base – h()\n“;

 }

};

class Der : public Base

{public:

 virtual void f()

 {cout << „Der – f()\n“;

 }

void g(int x)

 {int y;

  y = x;

  cout << „y= “ << y << endl;

 }

 void h()

 {cout << „Der – h()\n“;

 }

};

void main()

{int a; Der x; Base y;

 Base *bp = &x;

 bp->f();

 bp->g();

 bp->h();

 bp = &y;

 bp->f();

}

Кои обръщения не са коректни? Защо?

 

Задача 4. Кои от следващите извиквания са статично свързани и кои динамично? Какво извежда програмата?

 

#include <iostream.h>

class B

{public:

 B(){}

 virtual void p() const

 {cout << “B::p\n”;

 }

 void q() const

 {cout << “B::q\n”;

 }

 

};

class D : public B

{public:

 D(){}

 void p() const

 {cout << “D::p\n”;

 }

 void q() const

 {cout << “D::q\n”;

 }

};

int main()

{B b; D d;

B* pb = new B;

B* pd = new D;

B* pd2 = new D;

b.p(); b.q();

d.p(); d.q();

pb->p(); pb->q();

pd->p(); pd->q();

pd2->p(); pd2->q();

return 0;

}

измислете и други задачи

→ Leave a CommentКатегории: Uncategorized

Дванадесета част

19/05/2009 · Вашият коментар


14

 

Класове

  

Класовете са нови типове данни, дефинирани от потребителя. Те могат да обогатяват възможностите на вече съществуващ тип или да представят напълно нов тип данни.
Класовете са подобни на структурите, даже може да се каже, че в някои отношения са почти идентични. В C++ класът може да се разглежда като структура, на която са наложени някои ограничения по отношение на правата на достъп. Отначало ще разгледаме основното, което е приложимо както за класовете, така и за структурите. Затова ще останем в познатите означения на структурите.

     

14.1  Пример за програма, която дефинира и използва клас

 

Основен принцип на процедурното програмиране е модулния. Програмата се разделя на “подходящи” взаимносвързани части (функции, модули), всяка от които се реализира чрез определени средства. Важен е обаче начинът, по който да става определянето на частите и връзките помежду им. Целта е, следващи промени в представянето на данните да не променят голям брой от модулите на програмата. Разсъждения в тази посока довеждат до подхода абстракция със структури от данни, който вече разгледахме. Ще напомним, че при него методите за използване на данните се разделят от методите за тяхното конкретно представяне. Програмите се конструират така, че да работят с “абстрактни данни” – данни с неуточнено представяне. След това представянето се конкретизира с помощта на множество функции, наречени конструктори, мутатори и функции за достъп, които реализират “абстрактните данни” по конкретен начин. Така при решаването на даден проблем се оформят следните нива на абстракцията:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

избор на представяне

на данните

 

 

 

 

 

 

Добра реализация на подхода е тази, при която всяко ниво използва единствено средствата на предходното. Предимствата са, че възникнали промени на едно ниво ще се отразят само на следващото над него. Например, промяна на представянето на данните ще доведе до промени единствено на реализацията на някои от конструкторите, мутаторите или функциите за достъп.

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

След анализ на правилата, реализиращи тези операции, в глава 11 стигнахме до необходимостта от реализирането на следните примитивни операции за работа с рационални числа:

-            конструиране на рационално число по зададени две цели числа, представящи съответно неговите числител и знаменател;

-            извличане на числителя на дадено рационално число;

-            извличане на знаменателя на дадено рационално число.

Към тях ще добавим и функциите:

-                промяна на стойността на рационално число чрез въвеждане, например;

-            извеждане на рационално число.

 

Реализирането на подхода абстракция със структури от данни в този случай показва следните четири нива на абстракция

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

избор на представяне на рационално число

 

 

 

 

 

 

 

Ще започнем с реализирането на нивата отдолу нагоре.

 

Избор на представяне на рационално число

Тъй като рационалното число е частно на две цели числа, можем да го определим чрез структурата:

struct rat

{int numer;

 int denom;

};

където полето numer означава числителя, а полето denom – знаменателя на рационално число. Тези две полета се наричат член-данни, само данни  или още абстрактни данни на структурата. Те определят множеството от стойности на типа rat, който дефинираме. Трябва да добавим и някакви операции и вградени функции, които да могат да се изпълняват над данни от тип rat. Това ще постигнем с реализацията на следващите две нива на абстракция, определени по-горе.

 

     Реализиране на примитивните операции

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

 

     а) конструктори

     Конструкторите са член-функции, чрез които се инициализират променливите на структурата. Имената им съвпадат с името на структурата. Ще дефинираме два конструктора на структурата rat:

     rat() – конструктор без параметри и

     rat(int, int) – конструктор с два цели параметъра.

Първият конструктор се нарича още конструктор по подразбиране. Използва се за инициализиране на променлива от тип rat, когато при дефиницията й не са зададени параметри. Ще го дефинираме така:

rat::rat()

     {numer = 0;

      denom = 1;

}

    Пример: След дефиницията

     rat p = rat();

или съкратено

rat p;

p се инициализира с рационалното число 0/1.

Вторият конструктор

     rat::rat(int x, int y)

     {numer = x;

      denom = y;

}

позволява променлива величина от тип rat да се инициализира с указана от потребителя стойност.

    Примери: След дефиницията

     rat p = rat(1,3);

p се инициализира с 1/3, а дефиницията

     rat q(2,5);

инициализира q с 2/5.

     Ще отбележим, че и двата конструктора имат едно и също име, но се различават по броя на параметрите си. В този случай се казва, че функцията rat е предефинирана.

     Декларацията на структура може да съдържа, но може и да не съдържа конструктори.

 

     б) мутатори

     Това са функции, които променят данните на структурата. Ще дефинираме мутатора read(), който въвежда от клавиатурата две цели числа (второто различно от нула) и ги свързва с абстрактните данни numer и denom.

     void rat::read()

     {cout << “numer: “;

      cin >> numer;

      do

 {cout << “denom: “;

       cin >> denom;

      } while (denom == 0);

     }

     След обръщението

     p.read();

стойността на p се променя като полетата й numer и denom се свързват с въведените от потребителя стойности за числител и знаменател съответно.

 

     в) функции за достъп

     Тези функции не променят член-данните на структурата, а само извличат информация за тях. Последното е указано чрез използването на запазената дума const, записана след затварящата скоба на формалните параметри и пред знака ;. Ще дефинираме следните функции за достъп:

     int get_numer() const;

     int get_denom() const;

     void print() const;

Първата от тях извлича числителя, втората – знаменателя, а третата извежда върху екрана рационалното число numer/denom. Реализациите им имат вида:

     int rat::get_numer() const

     {return numer;

}

int rat::get_denom() const

     {return denom;

}

void rat::print() const

{cout << numer << “/” << denom << endl;

}

След включване на прототипите на тези конструктори, мутатори и функции за достъп във фигурните скоби на декларацията на структурата rat, получаваме:

struct rat

{int numer;

 int denom;

 // конструктори

 rat();

 rat(int, int);

 // мутатор

 void read();

 // функции за достъп

 int get_numer() const;

 int get_denom() const;

 void print() const;

};

След направените дефиниции са възможни следните действия над рационални числа:

// p се инициализира с 0/1, q – с 1/6, а r – с 5/9

rat p, q(1,6), r=rat(5,9);

// p се извежда чрез данновите полета на структурата rat

  cout << p.numer << „/“

       << p.denom << endl;

// q се извежда като се използват 

     // функциите за достъп до компонентите му

cout << q.get_numer() << „/“

                << q.get_denom() << endl;

// q се извежда чрез функцията за достъп print()

  1. q.print();  

// p се модифицира чрез мутатора read()

         p.read();

 

С това завършихме реализирането на двете най-долни нива на абстракция и преминаваме към следващото ниво.

 

     Реализиране на правилата за рационално-цифрова аритметика

     Като използваме дефинираните конструктори, мутатори и функции за достъп, ще  реализираме функциите:

     rat sum(rat const &, rat const &);

     rat sub(rat const &, rat const &);

     rat prod(rat const &, rat const &);

     rat quot(rat const &, rat const &);

извършващи рационално-числовата аритметика. Функцията sum може да се дефинира по следния начин:

rat sum(rat const& r1, rat const& r2)

{rat r(r1.get_numer()*r2.get_denom() +

      r2.get_numer()*r1.get_denom(),

      r1.get_denom()*r2.get_denom());

 return r;

}

Другите функции се реализират по аналогичен начин.

 

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

Например, функцията sum, дефинирана по-горе може да се реализира и така:

rat sum(rat const& r1, rat const& r2)

{rat r(r1.numer*r2.denom + r2.numer*r1.denom,

      r1.denom*r2.denom);

 return r;

}

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

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

Езикът C++ позволява да се ограничи свободата на достъп до членовете на структурата като се поставят подходящи спецификатори на достъп в декларацията й. Такива спецификатори са private и public. Записват се като етикети. Всички членове, следващи спецификатора на достъп private, са достъпни само за член-функциите в декларацията на структурата. Всички членове, следващи спецификатора на достъп public, са достъпни за всяка функция, която е в областта на структурата. Ако са пропуснати спецификаторите на достъп, всички членове са public (както е в случая). Има още един спецификатор на достъп – protected, който е еднакъв със спецификатора private, освен ако структурата не е част от йерархия на класовете, което ще разгледаме по-късно.

     С цел реализиране на идеите на подхода абстракция със структури от данни, ще променим дефинираната по-горе структура по следния начин:

struct rat

{private:

 int numer;

 int denom;

 public:

 rat();

 rat(int, int);

 void read();

 int get_numer() const;

 int get_denom() const;

 void print() const;

};

По такъв начин позволяваме член-данните numer и denom да се използват единствено от член-функциите на структурата rat. Операторът

cout << p.numer << „/“

  << p.denom << endl;

вече е недопустим.

 Следва програмата, която решава задачата.

#include <iostream.h>

struct rat

{private:

 int numer;

 int denom;

 public:

 rat();

 rat(int, int);

 void read();

 int get_numer() const;

 int get_denom() const;

 void print() const;

};

rat::rat()

{numer = 0;

 denom = 1;

}

rat::rat(int x, int y)

{numer = x;

 denom = y;

}

void rat::read()

{cout << „numer: „;

 cin >> numer;

 do

 {cout << „denom: „;

  cin >> denom;

 }while(denom==0);

}

int rat::get_numer() const

{return numer;

}

int rat::get_denom() const

{return denom;

}

void rat::print() const

{cout << numer << „/“ << denom << endl;

}

rat sum(rat const &, rat const &);

rat sub(rat const &, rat const &);

rat prod(rat const &, rat const &);

rat quot(rat const &, rat const &);

int main()

{rat p(1,4), q(1,2);

 p.print();

 q.print();

 cout << „sum:\n“;

 sum(p,q).print();

 cout << „subtraction:\n“;

 sub(p,q).print();

 cout << „product:\n“;

 prod(p,q).print();

 cout << „quotient:\n“;

 quot(p,q).print();

 return 0;

}

rat sum(rat const& r1, rat const& r2)

{rat r(r1.get_numer()*r2.get_denom()+

      r2.get_numer()*r1.get_denom(),

      r1.get_denom()*r2.get_denom());

 return r;

}

rat sub(rat const & r1, rat const & r2)

{rat r(r1.get_numer()*r2.get_denom()-

      r2.get_numer()*r1.get_denom(),

      r1.get_denom()*r2.get_denom());

 return r;

}

rat prod(rat const & r1, rat const & r2)

{rat r(r1.get_numer()*r2.get_numer(),

      r1.get_denom()*r2.get_denom());

 return r;

}

rat quot(rat const & r1, rat const & r2)

{rat r(r1.get_numer()*r2.get_denom(),

      r1.get_denom()*r2.get_numer());

 return r;

}

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

rat::rat(int x, int y)

{if (x == 0 || y==0)

 {numer = 0;

 denom = 1;

 }

 else

 {int g = gcd(abs(x), abs(y));

  if (x>0 && y>0 || x<0 && y<0)

  {numer = abs(x)/g;

denom = abs(y)/g;

  }

  else

  {numer = -abs(x)/g;

   denom = abs(y)/g;}

  }

}

където int gcd(int x, int y) е известната вече функция за намиране на най-големия общ делител на две естествени числa.

     Ще отбележим, че ако в горната програма заменим запазената дума struct с class, програмата няма да промени поведението си. Така дефинирахме класа rat:

class rat

{private:

 int numer;

 int denom;

 public:

 rat();

 rat(int, int);

 void read();

 int get_numer() const;

 int get_denom() const;

 void print() const;

};

а дефиницията

     rat p, q=rat(1,7), r(-2,9);

определя три негови обекта: p, инициализиран с рационалното число 0/1; q, инициализиран с 1/7 и r, инициализиран с -2/9.

     Спецификаторът private, забранява използването на член-данните numer и denom извън класа. Получава се скриване на информация, което се нарича още капсолиране на информация. Член-функциите на класа rat са обявени като public. Те са видими извън класа и могат да се използват от външни функции. Затова public-частта се нарича още интерфейсна част на класа или само интерфейс. Чрез нея класът комуникира с външната среда. Освен функции, интерфейсът може да съдържа и член-данни, но засега ще се стараем това да не се случва.

Ще отбележим, че конструкторите се използват само когато се създават обекти. Опитите за промяна на обект чрез обръщение към конструктор предизвикват грешки.

Пример:

rat q(1,7);    // коректно

  1. q.rat();     // предизвиква грешка

q(2,9);        // предизвиква грешка

  1. q.rat(3,4);   // предизвиква грешка.

Забележете, езикът C++ дефинира структурите и класовете почти идентично. Съществената разлика е свързана със спецификаторите на достъп. По подразбиране членовете на структура имат public (публичен) достъп, а членовете на клас – private (частен) достъп. Все пак възниква въпросът: Защо да има две различни конструкции struct и class, когато разликите са толкова малки? Причината е свързaна с мобилността на програмите, с цел да се запази съвместимостта между езиците C и C++. Бярне Страуструп обяснява, че причината е по-скоро културна, отколкото техническа. Той препоръчва структурите да се използват само когато се реализират свойства, които са прости и включват малки “натоварвания”. По-точно, когато реализираната структура от данни е идентична с интерфейса си. В останалите случаи да се използват класове.

 

14.2 Дефиниране на класове

 

Класовете осигуряват механизми за създаване на напълно нови типове данни, които могат да бъдат интегрирани в езика, а също за обогатяване възможностите на вече съществуващи типове. Дефинирането на един клас се състои от две части:

-            декларация на класа и

-            дефиниция на неговите член-функции (методи).

 

14.2.1. Декларация на клас

 

Декларацията на клас се състои от заглавие и тяло. Заглавието започва със запазената дума class, следвано от името на класа. Тялото е заградено във фигурни скоби. След скобите стои знакът “;” или списък от обекти. В тялото на класа са декларирани членовете на класа (член-данни и член-функции) със съответните им нива на достъп. Фиг. 14.1 илюстрира непълно (но достатъчно за целите на настящите разглеждания) синтаксиса на декларацията на клас.

 

Декларация на клас

<декларация_на_клас> ::= <заглавие> <тяло>

<заглавие> ::= class [<име_на_клас>]опц

<тяло> ::= {<декларация_на_член>;

               {<декларация_на_член>;}опц

           }[<списък_от_обекти>]опц;

<декларация_на_член> ::=

        <декларация_на_конструктор>|<декларация_на_мутатор>|

    <декларация_на_функция_за_достъп>|<декларация_на_данна>

<декларация_на_конструктор> ::=

        [<спецификатор_на_достъп>:]опц<име_на_клас>(<параметри>)

<декларация_на_мутатор> ::=

        [<спецификатор_на_достъп>:]опц <тип>

                                       <име_на_мутатор>(<параметри>)

<декларация_на_функция_за_достъп> ::= 

       [<спецификатор_на_достъп>:]опц <тип>

                  <име_на_функция_за_достъп>(<параметри>) const;

<спецификатор_на_достъп> ::= private | public | protected

<параметри> :: <празно> | void |

                            <параметър> {, <параметър>}опц

<параметър> ::= <тип> [ &|опц * [const]опц] опц

<декларация_на_данна> ::= <тип> <име_на_данна>{, <име_на_данна>}опц

<тип> ::= <име_на_тип>|<дефиниция_на_тип>

<списък_от_обекти> ::=

       <обект> [= <име_на_клас>(<фактически_параметри>)]опц

               {,<обект>[=<име_на_клас>(<фактически_параметри>)]опц }опц

               {, <обект>(<фактически_параметри>)}опц

               {, <обект> = <вече_дефиниран_обект>}опц

където <име_на_клас>, <име_на_мутатор>, <име_на_данна>, <обект> и <име_на_функция_за_достъп> са идентификатори, а <фактически_ параметри> е определенo в Глава 8.

 

Фиг. 14.1 Декларация на клас

 

За имената на класовете важат същите правила, които се прилагат за имената на всички останали типове и променливи. Също като при структурите името на класа може да бъде пропуснато. Ще напомним, че използването на долния индекс “опц” означава, че означението е от езика на Бекус-Наур за описание на синтаксиса на език за програмиране.

Имената на членовете на класа са локални за него, т.е. в различни класове в рамките на една програма могат да се дефинират членове с еднакви имена. Член-данни от един и същ тип могат да се изредят, разделени със запетая и предшествани от типа им.

Пример:

class point

{private:

  double x, y;       // x и y са член-данни на класа prat

 public:

  point(double, double);

  void read();           // мутатор със същото име като на класа rat

  int get_x() const;

  int get_y() const;

  void print() const;    // със същото име като на класа rat

}p=point(2,7), q(-2,3), r=q;

 

Препоръчва се член-данните да се декларират в нарастващ ред по броя на байтовете, необходим за представянето им в паметта. Така за повечето реализации се получава оптимално изравняване до дума.

Забележка: Типът на член-данна на клас не може да съвпада с името на класа, но типът на член-функция на клас може да съвпада с името на класа.

В тялото, някои декларации на членове могат да бъдат предшествани от спецификаторите на достъп private, public или protected. Областта на един спецификатор на достъп започва от спецификатора и продължава до следващия спецификатор. Подразбиращ се спецификатор за достъп е private. Един и същ спецификатор на достъп може да се използва повече от веднъж в декларация на клас.

Препоръчва се, ако секция public съществува, да бъде първа в декларацията, а секцията private да бъде последна в тялото на класа.

 

Достъпът до членовете на класовете може да се разгледа на следните две нива:

 - По отношение на член-функциите в класа е в сила, че те имат достъп до всички членове на класа. При това не е необходимо тези компоненти да се предават като параметри. Този режим на достъп се нарича режим на пряк достъп. Поради тази причина функциите rat(), read(), print(), get_numer() и get_denom() са без параметри. Освен това член-функцията print() може да бъде дефинирана и по следния начин:

void rat::print() const

{cout << get_numer() << “/” <<

       << get_denom() << endl;

}

или

void rat::print() const

{cout << this->get_numer() << “/” <<

       << this->get_denom() << endl;

}

Смисълът на this ще бъде обяснен в т. 14.4.5, а на -> – в т.14.5.

- По отношение на функциите, които са външни за класа, режимът на достъп са определя от начина на деклариране на членовете.

Членовете на даден клас, декларирани като private (декларирани след запазената дума private) са видими (достъпни) само в рамките на класа. Външните функции нямат достъп до тях. По подразбиране членовете на класовете са private. Това позволява декларацията на класа rat да запишем и по следния начин:

class rat

{int numer;

 int denom;

 public:

 rat();

 rat(int, int);

 void read();

 int get_numer() const;

 int get_denom() const;

 void print() const;

};

Чрез използването на членове, обявени като private, се постига скриване на членове за външната за класа среда. Процесът на скриване се нарича още капсолиране на информацията.

Членовете на клас, които трябва да бъдат видими извън класа (да бъдат достъпни за функции, които не са методи на дадения клас) трябва да бъдат декларирани като public (декларирани след запазената дума public). Всички методи на класа rat са декларирани като public и следователно могат да се използват навсякъде в програмата за работа с рационални числа.

Освен като private и public, членовете на класовете могат да бъдат декларирани и като protected. Тъй като този спецификатор на достъп има отношение към производните класове и процеса на наследяване, разглеждането му засега ще бъде отложено. Ще отбележим, че ако в класа rat заменим private с protected, поведението на класа няма да се промени.

 

14.2.2   Дефиниране на методите на клас

 

След декларирането на клас, трябва да се дефинират неговите методи. Дефинициите са аналогични на дефинициите на функции, но името на метода се предшества от името на класа, на който принадлежи метода, следвано от оператора за принадлежност :: (Нарича се още оператор за област на действие). Такива имена се наричат пълни. (Операторът :: е ляво-асоциативен и с един и същ приоритет със (), [] и ->). На Фиг. 14.2 е даден синтаксисът на дефиницията на метод на клас.

 

Дефиниция на метод на клас

<дефиниция_на_метод_на_клас> ::=

[<тип>]опц <име_на_клас>::<име_на_функция>(<параметри>) [const]опц

{<тяло>}

<тяло> ::= <редица_от_оператори_и_дефиниции>

където <име_на_клас> и <име_на_функция> са идентификатори, а <параметри> се определя както в дефиниция на функция.

 

Фиг. 14.2 Дефиниция на метод на клас

 

Ще отбележим, че дефиницията на конструктор не започва с <тип>, а запазената дума const може да присъства само в дефинициите на функциите за достъп. Добрият стил на програмиране изисква използването на const в дефинициите на функциите за достъп и също в техните декларации.   Ако се пренебрегне това изискване, могат да се създадат класове, които да не могат да се използват от други програмисти.

    Пример: Нека искаме да използваме класа rat, но програмистът му е забравил или нарочно не е декларирал член-функцията print() като const и rat има вида:

     class rat

     {private:

       …

      public:

       …

       void print();

     };

Нека декларираме класа prat, използващ класа rat, коректно, т.е. функциите за достъп обявяваме като const.

     class prat

     {private:

      int a;

 rat p;    // използване на класа rat

      …

      public:

      …

      void print() const;

     };

където

     void prat::print() const

     {cout << a << endl;

      p.print(); // тази print() е член-функцията на класа rat

     };

Компилаторът ще съобщи за грешка в обръщението p.print(), защото p е обект на класа rat, а член-функцията rat::print() не е декларирана като const. Компилаторът предполага, че p.print() може да модифицира p. Но p е член-данна на prat, а prat::print() е const, с което твърдо е обещава да не го модифицира.

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

Пример: Класът rat може да бъде дефиниран и по следния начин:

class rat

{private:

  int numer;

  int denom;

 public:

  rat()

  {numer = 0;

   denom = 1;

  }

  rat(int a, int b)

  {if (a == 0 || b==0)

{numer = 0;

 denom = 1;

}

   else

   {int g = gcd(abs(a), abs(b));

    if (a>0 && b>0 || a<0 && b<0)

   {numer = abs(a)/g;

    denom = abs(b)/g;}

    else

 {numer = – abs(a)/g;

  denom = abs(b)/g;

 }

   }

  }

 void read()

 {cout << „numer: „;

  cin >> numer;

  do

  {cout << „denom: „;

   cin >> denom;

  } while (denom == 0);

 }

 int get_numer() const

 {return numer;

 }

 int get_denom() const

 {return denom;

 }

 void print() const

 {cout << numer << „/“ << denom << endl;

 }

};

В този случай обаче член-функциите се третират като вградени (inline) функции.

 

Допълнение (вградени функции) С цел повишаване на бързодействието, езикът C++ поддържа т.нар. вградени функции. Кодът на тези функции не се съхранява на едно място, а се копира на всяко място в паметта, където има обръщение към тях. Използват се като останалите функции, но при декларирането и дефинирането им заглавието им се предшества от модификатора inline.

Пример:

#include <iostream.h>

inline int f(int, int); // декларация на вградената функция f

void main()

{cout << f(1,5) << endl;

}

inline int f(int a, int b) // дефиниция на вградената функция f

{return (a+b)*(a-b);

}

Ще добавим, че дефиницията на вградена функция трябва да се намира в същия файл, където се използва, т.е. не е възможна разделна компилация, тъй като компилаторът няма да разполага с кода за вграждане. Използването на вградени функции води до икономия на време, за сметка на паметта. Затова се препоръчва използването им само при “кратки” функции. Ще отбележим също, че модификаторът inline е само заявка към компилатора, която може да бъде, но може и да не бъде изпълнена. Възможно е компилаторът да откаже вграждане, ако реши, че функцията е прекалено голяма или има други причини, възприпятстващи вграждането. Ограниченията за вграждане зависят от конкретния компилатор.

 

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

class rat

{private:

  int numer;

  int denom;

 public:

  rat();

  rat(int, int);

  void read();

  int get_numer() const;

  int get_denom() const;

  void print() const;

};

inline rat::rat()

{numer = 0;

 denom = 1;

}

inline rat::rat(int x, int y)

inline void rat::read()

inline int rat::get_numer() const

inline int rat::get_denom() const

inline void rat::print() const

Телата на някои от член-функциите са пропуснати, тъй като вече са известни.

Ще отбележим също, че в тялото на дефиницията на член-функция явно не се указва обектът, върху който тя ще се приложи. Този обект участва неявно – чрез член-данните на класа. Заради това се нарича неявен параметър, а член-данните – абстрактни данни. Връзката между неявния параметър и обект ще бъде показана в т. 3. Параметри, които участват явно в дефиницията на член-функция се наричат явни. Всяка член-функция има точно един неявен параметър и нула или повече явни.

 

14.2.3   Област на класовете   

 

За разлика от функциите, класовете могат да се декларират на различни нива в програмата: глобално (ниво функция) и локално (вътре във функция или в тялото на клас).

Областта на глобално деклариран клас започва от декларацията и продължава до края на програмата. Примерите досега бяха с такива класове.

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

Пример:

void f(int i, int* p)

{int k;

 class CL

 {public:

  // всички методи са дефинирани в тялото на класа

  …

  private:

  …

 };

// тяло на функцията f

 CL x;

 …

}

Областта на клас, дефиниран във функция, е функцията. Обектите на такъв клас са видими само в тялото на функцията.

Възможно е използването на обекти (в широкия смисъл на думата) с еднакви имена. В сила е правилото, че в областта си локалният обект скрива нелокалния.

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

Пример: Нека сме в означенията на горния пример.

void f(…)

{…

 class cl

 {

  // не може да се използва функцията f

 };

 …

}

 

14.3 Обекти

 

След като даден клас е дефиниран, могат да бъдат създавани негови екземпляри, които се наричат обекти. Връзката между клас и обект в езика C++ е подобна на връзката между тип данни и променлива, но за разлика от обикновените променливи, обектите се състоят от множество компоненти (член-данни и член-функции). На Фиг. 14.3 е даден синтаксисът на дефиниция на обект на клас.

 

Дефиниция на обект на клас

<дефиниция-на_обект_на_клас> ::=

<име_на_клас> <обект> [=<име_на_клас>(<фактически_параметри>)]опц

             {, <обект>[=<име_на_клас>(<фактически_параметри>)]опц }опц

             {, <обект>(<фактически_параметри>)}опц

             {, <обект> = <вече_дефиниран_обект>}опц;

     <обект> ::= <идентификатор>

където <фактически_параметри> е определено в Глава 8.

 

Фиг. 14.3 Дефиниция на обект на клас

 

Когато за даден клас явно са дефинирани конструктори, при всяко дефиниране на обект на класа те автоматично се извикват с цел да се инициализира обектът. Ако дефиницията е без явна инициализация (например rat p;), дефинираният обект се инициализира според дефиницията на конструктора по подразбиране, ако такъв е определен, и се съобщава за грешка в противен случай. Ако дефиницията е с явна инициализация, обръщението към конструкторите трябва да бъде коректно.

Пример: Дефиницията

rat p, q(2,3), r=rat(3,8);

определя три обекта: p, инициализиран с рационалното число 0/1, q, инициализиран с 2/3 и r, инициализиран с 3/8. Тя е добре оределена, тъй като класът rat има конструктор по подразбиране и двуаргументен конструктор. Ако елиминираме конструктора по подразбиране в класа rat, горната дефиниция ще съобщи за грешка заради p. Валидна е обаче дефиницията:

rat q(2,3), r=rat(3,8);

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

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

Пример:

#include <iostream.h>

class pom

{private:

  int a;

 public:

  int b;

  void read();

  void print() const;

};

void main()

{pom x;  // инициализация според подразбиращия се

//конструктор, генериран от компилатора на C++

 x.read();

 x.print();

}

void pom::print()const

{cout << „a= „<< a << “ b=“ << b << endl;

}

void pom::read()

{cout << „a= „;

 cin >> a;

 cout << „b= „;

 cin >> b;

}

     В случая обектът x се инициализира с неопределена стойност. Опитите за инициализацията му като структура предизвикват грешки.

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

     rat p, q(2, 3), r = rat(3, 8);

заделя за обектите p, q и r по 8 байта ОП (по 4B за всяка от данните им numer и denom).

     Достъпът до компонентите на обектите (ако е възможен) се осъществява чрез задаване на името на обекта и името на данната или метода, разделени с точка (Фиг. 14.4). Изключение от това правило правят конструкторите (Фиг. 14.5).

    

    Достъп до компонента на обект

     <компонента_на_обект> ::=  <обект>.<данна>|

                     <обект>.<име_на_член_функция>()|

                                      <обект>.<име_на_член_функция>(<параметри>)

     <име_на_член_функция> е <идентификатор>, означаващ име на мутатор или име на функция за достъп.

 

Фиг. 14.4 Достъп до компонента на обект

 

    Пример:

     rat p(1,2), q;

  1. p.get_numer()   // достъп до член-функцията get_numer() за обекта p

q.get_numer()   // достъп до член-функцията get_numer() за обекта q

Ще отбележим също, че на практика обектите p и q нямат свои копия на метода get_numer(). И двете обръщения се отнасят за един и същ метод, но при първото обръщение се работи с данните за обекта p, а при второто – с данните за обекта q.

При създаването на обекти на един клас кодът на методите на този клас не се копира във всеки обект, а се намира само на едно място в паметта.

Естествено възниква въпросът по какъв начин методите на един клас “разбират” за кой обект на този клас са били извикани. Отговорът на този въпрос дава указателят this. Всяка член-функция на клас поддържа допълнителен формален параметър – указател с име this и от тип <име_на_клас>*. За да разберем точно как става това, ще разгледаме как компилаторът на C++ обработва член-функция и обръщение към член-функция на клас. Извършват се следните преобразувания:

а) Всяка член-функция на даден клас се транслира в обикновена функция с уникално име и един допълнителен параметър – указателят this.

Пример: Функцията

void rat::print()

{cout << numer << “/” << denom << endl;

}

се транслира в

     void print_rat(rat* this)

     {cout << this->numer << “/” << this->denom << endl;

     }

     б) Всяко обръщение към член-функция се транслира в съответствие с преобразуванието от а).

Пример:   Обръщението

p.print();

се транслира в

print_rat(&p);

     Указателят this може да се използва явно в кода на съответната член-функция, макар че е глупаво да се напише:

void rat::print()

     {cout << this->numer << “/” << this->denom << endl;

     }

     Като приложение на указателя this ще реализираме функцията

     rat sum(rat const &, rat const &);

като член-функция на класа rat. За целта ще включим псевдонима й

rat sum(rat const &, rat const &);

в public-секцията на тялото на rat и ще я дефинираме по следния начин:

rat rat::sum(rat const & r1, rat const & r2)

{numer = r1.numer*r2.denom+r2.numer*r1.denom;

 denom = r1.denom*r2.denom;

 return *this;

}

Нека

rat p=rat(1,4), r(1,2), q=rat(1,4);

Фрагментът

r.sum(p.sum(p, r), q);  

r.print();

p.print();

съобщава 6/8 за стойност на p и 32/32 – за стойност на r. Обръщението p.sum(p, r) намира сумата на рационалните числа p и r и я свързва с обекта p, a r.sum(p.sum(p, r), q) събира полученото рационално число с q и свързва резултата с обекта r.   

 

Обекти от един и същ клас могат да се присвояват един на друг. Присвояването може да е и на ниво инициализация (фиг. 14.3).

     Пример: Допустими са дефинициите

     rat p, q(4,5), r=q;

p = q;

     …

     r = p;

При присвояването се копират всички член-данни на обекта. Така присвояването

     r = p;

е еквивалентно на

     r.numer = p.numer;

     r.denom = p.denom;

Подробности относно процеса на присвояване са дадени в следващите части на тази глава.

 

Някои задачи върху дефиниране на класове

Задача 118. Да се дефинира клас “точка в равнината” с две член-данни – двете декартови координати на точката и подходящи член-функции. Като се използва дефинираният клас да се  напише програма, която:

а) въвежда n различни точки от равнината, след което ги транслира с (2, 4) и извежда получените точки;

б) намира разстоянието между всеки две точки (все едно старите или новите);

в) намира точките, разстоянието между които е най-малко (най-голямо);

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

Програма Zad118.cpp решава задачата.

// Program Zad118.cpp

#include <iostream.h>

#include <math.h>

class point

{private:

  double x;

  double y;

 public:

  point(double=0, double=0);

  void read();

  void move(double, double);

  double get_x() const

  {return x;

  }

  double get_y() const

  {return y;

  }

  void print() const;

};

point::point(double a, double b)

{x = a;

 y = b;

}

void point::read()

{cout << „x= „;

 cin >> x;

 cout << „y= „;

 cin >> y;

}

void point::print() const

{cout << „(“ << x << „, “ << y << „)“ << endl;

}

void point::move(double dx, double dy)

{x = x + dx;

 y = y + dy;

}

double dist(point X, point Y)

{return sqrt(pow(Y.get_x()-X.get_x(), 2) +

                 pow(Y.get_y()-X.get_y(), 2));

}

int main()

{// а)

 cout << „n= „;

 int n;

 cin >> n;

 point table[10];

 for (int i=0; i<=n-1; i++)

  table[i].read();

 for (i=0; i<=n-1; i++)

  table[i].print();

 cout << endl;

 for (i=0; i<=n-1; i++)

 {table[i].move(2,4);

  table[i].print();

 } // б)

 for (i=0; i<=n-2; i++)

  for (int j=i+1; j<=n-1; j++)

    cout << dist(table[i], table[j]) << endl;

 return 0;

}

Подточки в) и г) оставяме за самостоятелна работа. Разгледайте добре решението и обяснете ролята на конструктора с подразбиращи се параметри. Какво щеше да стане ако параметрите му не бяха подразбиращи се?

Задача 119. Да се дефинират класове “точка в пространството” и “пирамида” с подходящи член-данни и член-функции. Като се използват дефинираните класове да се напише програма, която въвежда n точки в пространството и установява дали всички те принадлежат на пирамидата

                     z

          

                  c

                                        

                     0                  b       y

                a

 

              x   

включително на контура й.

     Програма Zad119. cpp решава задачата.

// Program Zad119.cpp

#include <iostream.h>

class Point

{public:

  void Read();

  double GetX() const;

  double GetY() const;

  double GetZ() const;

 private:

  double x, y, z;

};

void Point::Read()

{cout << „x= „; cin >> x;

 cout << „y= „; cin >> y;

 cout << „z= „; cin >> z;

}

double Point::GetX()const

{return x;

}

double Point::GetY()const

{return y;

}

double Point::GetZ()const

{return z;

}

class Piramid

{public:

  void Read();

  bool IsPointIn(Point const &p) const;

 private:

  double a, b, c;

};

void Piramid::Read()

{cout << „a= „; cin >> a;

 cout << „b= „; cin >> b;

 cout << „c= „; cin >> c;

}

bool Piramid::IsPointIn(Point const &p) const

{double x =  p.GetX()/a + p.GetY()/b + p.GetZ()/c;

 return (p.GetX()>=0 && p.GetY()>=0 && p.GetZ()>=0 &&

          p.GetX()/a + p.GetY()/b + p.GetZ()/c <=1);

}

int main()

{Piramid p;

 cout << „Input piramid \n“;

 p.Read();

 Point pt[100];

 cout << „number of points: „;

 int n; cin >> n;

 for (int i=0; i<=n-1; i++)

 {cout << „Point “ << i << „: \n“;

  pt[i].Read();

 }

 int x = 0;

 while (x<=n-2 && p.IsPointIn(pt[x])) x++;

 if (p.IsPointIn(pt[x])) cout << „Yes\n“;

 else cout <<“No\n“;

 return 0;

}

 

 

 

17.4 Конструктори

 

Създаването на обекти е свързано с отделяне на памет, запомняне на текущо състояние, задаване на начални стойности и др. дейности, които се наричат инициализация на обекта. В езика C++ тези дейности се изпълняват от специален вид член-функции на класовете – конструкторите.

 

14.4.1. Дефиниране на конструктор

 

На Фиг. 14.5 е дадена най-често използваната форма за дефиниране на конструктор.

 

Дефиниране на конструктор (най-често използвана форма)

<дефиниция_на_конструктор> ::=

<име_на_клас>::<име_на_клас>(<параметри>)

{<тяло>}

<тяло> ::= <редица_от_оператори_и_дефиниции>

<параметри> се определя както формални параметри на функция.

 

Фиг. 14.5 Дефиниране на конструктор (най-често използвана форма)

 

Ще напомним, че конструкторът е член-функция, която притежава повечето характеристики на другите член-функции, но има и редица особености, като:

-            Името на конструктора съвпада с името на класа.

-            Типът на резултата е указателят this и явно не се указва.

-            Изпълнява се автоматично при създаване на обекти.

-            Не може да се извиква явно (обръщение от вида r.rat(1,4) е недопустимо).

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

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

Пример:

class CL

{public:

  CL(int, int, int);

  void print();

  …

 private:

  int a, b, c;

};

CL::CL(int x, int y, int z) // конструктор с три параметъра

{a = x;

 b = y;

 c = z;

}

Класът CL в този пример има един триаргументен конструктор. При създаване на обект на класа, този конструктор ще се изпълнява автоматично, стига да е коректно извикан, в резултат на което член-данните a, b и c на създадения обект ще се свържат със стойностите, които се подадат като фактически параметри на конструктора.

     На Фиг. 14.6 е дадена по-обща дефиниция на кoнструктор.

 

Дефиниране на конструктор

<дефиниция_на_конструктор> ::=

<име_на_клас>::<име_на_клас>(<параметри>):

       <член_данна>(<израз>){,<член_данна>(<израз>)}опц

{<тяло>}

<тяло> ::= <редица_от_оператори_и_дефиниции>

 

Фиг. 14.6 Дефиниране на конструктор

 

Забелязваме, че е възможно член_данна да се свърже с инициализираща стойност в заглавието на конструктора.

     Пример: Конструкторът на класа CL може да се дефинира и по следния начин

CL::CL(int x, int y, int z): a(x), b(y), c(z)

{}

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

     Пример: Допустима е дефиницията

CL::CL(int x, int y, int z): a(x)

{b = y;

 c = z;

}

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

Пример: Ще дефинираме класа prat, като член-данна в него е обект на класа rat.

class prat

{public:

  prat(int, int, int);

  …

 private:

  int a;

  rat r;

};

където

     prat::prat(int x, int y, int z)

     {a = x;

      r = rat(y, z);

     }

и сме дефинирали обекта q

     prat q=prat(1,2,3);

Преди да започне изпълнението на конструктора prat, автоматично се извиква конструкторът по подразбиране на rat и член-данната r на prat се инициализира с 0/1. Веднага след това r се свързва с обекта rat(y,z) за текущите y и z. По-ефективно е данната r да се свърже с правилната стойност направо, без междинна инициализация. Това може да се реализира чрез дефиницията:

prat::prat(int x, int y, int z): r(rat(y, z))

{a = x;

}

или съкратено

prat::prat(int x, int y, int z): r(y, z)

{a = x;

}

 

14.4.2   Предефинирани конструктори

 

Обект (в общия смисъл) е предефиниран, ако за него има няколко различни дефиниции, задаващи различни негови интерпретации. За да бъдат използвани такива конструкции е необходим критерии, по който те да се различат.

В рамките на една програма може да се извършва предефиниране на функции. Възможно е:

а) да се използват функции с едно и също име с различни области на видимост

В този случай не възниква проблем с различаването.

б) да се използват функции с едно и също име в една и съща област на видимост

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

-            точно съответствие (по брой и тип на формалните и фактическите параметри)

-            съответствие чрез разширяване на типа.

Извършва се разширяване по веригата

    char -> short -> int -> longint или

float -> double

-            други съответствия (правила въведени от потребителя).

 

В един клас може да са дефинирани няколко конструктора. Всички те имат едно име (името на класа), но трябва да се различават по броя и/или типа на параметрите си. Наричат се предефинирани конструктори. При създаването на обект на класа се изпълнява само един от тях. Определя се съгласно критерия за най-добро съвпадане.

Пример: В класа rat дефинирахме два конструктора rat() и rat(int, int), които се различават по броя на параметрите си.

 

14.4.3   Подразбиращ се конструктор

 

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

     Подразбиращият се конструктор може да бъде предефиниран. За целта е необходимо в класа да бъде дефиниран конструктор без параметри.

    Пример: В класа rat, подразбиращият се конструктор беше предефиниран от конструктора

     rat::rat()

     {numer = 0;

      denom = 1;

     }

 

14.4.4   Конструктори с подразбиращи се параметри

 

Функциите в езика C++ могат да имат подразбиращи се параметри. За тези параметри се задават подразбиращи се стойности, които се използват само ако при извикването на функцията не бъде зададена стойност за съответния параметър.

Задаването на подразбираща се стойност се извършва чрез задаване на конкретна стойност в прототипа на функцията или в нейната дефиниция.

Пример: Да разгледаме програмата

#include <iostream.h>

void f(double, int=10, char* =“example1″); //интервал между * и =

int main()

{double x = 1.5;

 int y = 5;

 char z[] = „example 2″;

 f(x, y, z);

 f(x, y);

 f(x);

 return 0;

}

void f(double x, int y, char* z)

{cout << „x= “ << x << “ y= “ << y

     << “ z= “ << z << endl;

}

В тази програма е дефинирана функцията f с три формални параметъра. От прототипа й се вижда, че два от тях (вторият и третият) са подразбиращи се със стойности по подразбиране 10 и “example 1” съответно. Tъй като в обръщенията към f

f(x, y); и

f(x);

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

В резултат от изпълнението на програмата се получава:

x = 1.5 y = 5 example 2

x = 1.5 y = 5 example 1

x = 1.5 y = 10 example 1

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

     Пример: Прототип на функция

     void f(double = 1.5, int, char* “example 1”);

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

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

Всичко казано за подразбиращите се параметри на функции се отнася и за конструкторите.

Ако променим дефиницията на класа rat по следния начин:

class rat

{private:

  int numer;

  int denom;

 public:

  rat(int=0, int=1);

  void read();

  int get_numer() const;

  int get_denom() const;

  void print() const;

};

където конструкторът rat(int, int); се дефинира по същия начин, са допустими следните дефиниции на обекти:

     rat p,                           // p се инициализира с 0/1

          q = rat(),                 // q се инициализира с 0/1

          r = rat(5),            // r се инициализира с 5/1

          s = rat(13,21),        // s се инициализира с 13/21

          t(2);                      // t се инициализира с 2/1

 

14.4.5 Конструктори за присвояване и копиране

 

Инициализацията на новосъздаден обект на даден клас може да зависи от друг обект на същия клас. За да се укаже такава зависимост се използва знакът за присвояване или кръгли скоби.

Пример: Нека

rat p = rat(1,4);

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

  rat q = p;

  rat q(p);

се създава обектът q от клас rat, като инициализацията на q зависи от p. Тази инициализация се създава от специален конструктор, наречен конструктор за присвояване.

Конструкторът за присвояване е конструктор, поддържащ формален параметър от тип <име_на_клас> const &.

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

Пример: В класа rat не беше дефиниран конструктор за присвояване. Затова при създаване на обект p чрез дефиницията rat p = q;

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

rat::rat(rat const & r)

{numer = r.numer;

 denom = r.denom;

}

или по-точно

rat::rat(rat* this, rat const & r)

{this -> numer = r.numer;

 this -> denom = r.denom;

}

 

Дефиницията rat p=q създава нов обект p (без викане на конструктор), в който се копират съответните стойности на обекта q. Това е резултат от изпълнението на обръщението към rat(&p, q).

- Ако в класа е дефиниран конструктор за присвояване, компилаторът го използва.

Пример: Ще добавим един безсмислен, даже глупав, конструктор за присвояване към класа rat. Правим го с експериментална цел.

class rat

{private:

  int numer;

  int denom;

 public:

  rat(rat const &); // конструктор за присвояване

  rat();

  rat(int=0, int=1);

  void read();

  int get_numer() const;

  int get_denom() const;

  void print() const;

};

rat::rat(rat const & r)

{numer = r.numer + 1;

 denom = r.denom + 1;

}

Компилаторът превежда този конструктор за присвояване във вида:

     rat_rat(rat *this, rat const &r)

{this->numer = r.numer + 1;

 this->denom = r.denom + 1;

}

а

rat q = p;

в

rat_rat(&q, p);

Така дефинираният конструктор за присвояване увеличава с 1 числителя и знаменателя на рационалното число p, фактически параметър на обекта r (формален параметър на конструктора) и ги свързва с числителя и знаменателя на обекта q сочен от указателя this.

В резултат имаме:

rat p,          // p се инициализира с 0/1

    q = p,      // q се инициализира с 1/2

 r = q       // r се инициализира с 2/3

 s = r,      // s се инициализира с 3/4

   t(s);        // t се инициализира с 4/5.

 

Предефиниране на служебния конструктор за копиране се налага когато обектите имат член-данни, указващи динамична памет.

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

Правилата, определящи достъпа на функциите до обектите, съществено не се различават от тези, регламентиращи достъпа на функциите до обикновените променливи. Обектите могат да се предават като параметри на функциите по един от известните вече три начина: по стойност, по указател и по псевдоним. При предаване по стойност функциите работят с копия на параметрите, а не със самите параметри. При другите два начина за предаване на параметрите не се правят копия (функциите работят с оригиналните параметри).

Когато обектите се предават като параметри на функции, спецификаторите на достъп public и private имат същия смисъл, т.е. функциите имат достъп само до public компонентите на обектите, когато им се подават като параметри.

Пример: Нека останем в означенията на класа rat с глупавия конструктор за присвояване от предишния пример и нека функцията sum, намираща сума на две рационални числа, е дефинирана по следния начин:

а) rat sum(rat r1, rat r2)

{rat r(r1.get_numer()*r2.get_denom()+

        r2.get_numer()*r1.get_denom(),

        r1.get_denom()*r2.get_denom());

 return r;

}

Обръщението sum(p, q).print() извежда 8/7. Този резултат може да се обясни по следния начин. Тъй като r1 и  r2 са параметри стойности, те се свързват с фактическите си параметри чрез присвояване. В резултат r1 се свързва с ½ (не с 0/1), а r2 – с 2/3 (не с ½). След изпълнението на инициализацията

rat r(r1.get_numer()*r2.get_denom()+

      r2.get_numer()*r1.get_denom(),

      r1.get_denom()*r2.get_denom());

/чрез двуаргументния конструктор rat(int, int)/ обектът r се свързва със 7/6. Тъй като функцията rat е от тип rat, при изпълнение на return r;  се прави още едно прилагане на глупавото присвояване и се получава 8/7.

б) rat sum(rat const & r1, rat const & r2)

{rat r(r1.get_numer()*r2.get_denom()+

        r2.get_numer()*r1.get_denom(),

        r1.get_denom()*r2.get_denom());

 return r;

}

Сега обръщението sum(p, q).print() извежда 2/3. Този резултат може да се обясни по следния начин. Тъй като r1 и  r2 са параметри псевдоними, те директно се свързват с фактическите си параметри (не се извършва присвояване), т.е. r1 се свързва с 0/1, а r2 – 1/2. След изпълнението на инициализацията

rat r(r1.get_numer()*r2.get_denom()+

      r2.get_numer()*r1.get_denom(),

      r1.get_denom()*r2.get_denom());

r се свързва със 1/2. Аналогично на случай а), при изпълнение на return r; се прилагане “глупавото” присвояване и се получава 2/3.

в) rat sum(rat* r1, rat* r2)

{rat r(r1->get_numer()*r2->get_denom()+

       r2->get_numer()*r1->get_denom(),

       r1->get_denom()*r2->get_denom());

return r;

}

и

rat* p1 = &p,

* q1 = &q;

Обръщението sum(p1, q1).print() извежда 2/3. Този резултат може да се обясни по следния начин. Тъй като r1 и  r2 са параметри указатели, те се свързват с фактическите си параметри чрез адрес. В резултат r1 се свързва с 0/1, а r2 – с 1/2. След изпълнението на инициализацията

rat r(r1->get_numer()*r2->get_denom()+

       r2->get_numer()*r1->get_denom(),

              r1->get_denom()*r2->get_denom());

r се свързва със 1/2. Тъй като функцията rat е от тип rat, при изпълнение на return r; се прилага присвояването и се получава 2/3.

 

14.5      Указатели към обекти на класове

 

Дефинират се по същия начин както се дефинират указатели към променливи от стандартните типове данни.

    Пример:

     rat p;

     rat * ptr = &p;

В резултат ще се отделят 4B ОП, които ще се именуват с ptr и ще се инициализират с адреса на обекта p.

Достъпът до компонентите на рационалното число, сочено от ptr, се осъществява по общоприетия начин:

(*ptr).get_numer()

(*ptr).get_denom()

Синтактичната конструкция (*ptr). е еквивалентна на ptr ->. Така горните обръщения могат да се запишат и по следния начин:

ptr -> get_numer()

ptr -> get_denom()

Ще напомним, че this е указател от тип <име_на_клас>*.

 

14.6 Масиви и обекти

 

Елементите на масив могат да са обекти, но разбира се от един и същ клас. Дефинират се по общоприетия начин (Фиг. 14.7).

 

    Дефиниция на масив от обекти

     <дефиниция_на_променлива_от_тип_масив_от_обекти> ::=

         T <променлива>[size] [= {<инициализиращ_списък>}]опц;

където

     – Т e име или декларация на клас;

     – <променлива> е идентификатор;

     – size е константен израз от интегрален или изброен тип с положителна стойност;

   – <инициализиращ_списък> се дефинира по следния начин:

     <инициализиращ_списък> ::= <стойност>{, <стойност>}опц

              {, <име_на_конструктор>(<фактически_параметри>)}опц

 

Фиг. 14.7 Дефиниция на масив от обекти

 

Пример:

rat table[10];

определя масив от 10 обекта от клас rat.

     Достъпът до елементите на масива е пряк и се осъществява по стандартния начин – чрез индексирани променливи.

    Пример: Чрез индексираните променливи

     table[0], table[1], …, table[9]

се осъществява достъп до първия, втория и т.н. до десетия елемент на table.

     Тъй като table[i] (i = 0, 1, …, 9) са обекти, възможни са следните обръщения към техни компоненти:

     table[i].read();            // въвежда стойност на table[i]

     table[i].print();           // извежда стойността на table[i]

table[i].get_numer();    // намира числителя на table[i]

     table[i].get_denom();   // намира знаменателя на table[i].

     Връзката между масиви и указатели е в сила и в случая когато елементите на масива са обекти. Името на масива е указател към първия му елемент, т.е. ако

     rat * p = table;        // p сочи към table[0]

                                      // т.е. p==&table[0]

     *(p+i) == table[i], i = 0, 1,…,9

Тогава

     (*(p+i)).print();       // е еквивалентно на table[i].print();

     Масивът може да е член-данна на клас.

Пример: Конструкцията

     class example

     {int a;

      int table[10];

      public:

      int array[10];

     } x[5];

дефинира масив с 5 компоненти, които са от тип example. Достъпът до компонентите на масива array ще се осъществи по следния начин:

     x[i].array[j], i = 0, 1, …, 4; j = 0, 1, …, 9.

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

-            неявно (чрез извикване на системния конструктор по подразбиране за всеки обект – елемент на масива);

-            явно (чрез инициализиращ списък).

Примери:

а) Класът

const NUM = 5;

class student

{public:

 void read_student();

 void print_student() const;

 bool is_better(student const &) const;

 double average() const;

 private:

 int facnom;

 char name[26];

 double marks[NUM];

};

няма явно дефиниран конструктор. Дефиницията

student table[30];

на масива table от 30 обекта от клас student е правилна.

Инициализацията се осъществява чрез извикване на “системния” конструктор по подразбиране за всеки обект – елемент на масива.

б) Класът rat, дефиниран по-долу

class rat

{private:

  int numer;

  int denom;

 public:

  rat(int=0, int=1);

  void read();

  int get_numer() const;

  int get_denom() const;

  void print() const;

};

притежава явно дефиниран конструктор с два подразбиращи се параметъра. В този случай са допустими дефиниции от вида:

     rat x[10]; // x[i] се инициализира с 0/1, за всяко i=0,1,…9.

rat x[10] = {1,2,3,4,5,6,7,8,9,10}; //x[i] == i/1

rat x[10] = {rat(1,21),rat(2),rat(3, 5),4,5,6,7,8,9,10};

     // x[0] == 1/21; x[1] == 2/1; x[2] == 3/5, x[3]== 4/1, …

Ако променим конструктора на класа rat от

rat(int=0, int=1);

в

rat(int, int);

т.е. без подразбиращи се параметри и трите дефиниции от по-горе ще съобщят за грешка. Единствено допустима дефиниция на x[10] е с инициализация с 10 обръщения към двуаргументния конструктор rat с явно указани два аргумента.

 

Задача 120. Да се напише програма, която въвежда следната информация за компютри: име на модела (name), цена (price) и точки (score) между 1 и 100. Да се изведе въведената информация, след което да се изведе сортирана в низходящ ред по съотношението точки/цена.

 

Отново ще използваме подхода абстракция със структури от данни. Първите две нива на абстракция ще реализираме чрез дефиницията на класа

class product

{public:

  void read();

  void print() const;

  bool is_better_from(product const &) const;

  double get_price() const;

  int get_score() const;

 private:

  char name[21];

  double price;

  int score;

};

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

void read();                  – въвежда информация за компютър

void print() const;      – извежда информация за компютър

bool is_better_from(product const &) const;

-            проверява дали текущият компютър има

по-добро съотношение score/price

    от това на указания като формален параметър

double get_price() const; – намира цената на компютър

int get_score() const;   – намира точките на компютър

  Програма Zad120.cpp решава задачата.

 

// Program Zad120.cpp

#include <iostream.h>

#include <iomanip.h>

#include <string.h>

class product

{private:

  char name[21];

  double price;

  int score;

 public:

  void read();

  void print() const;

  bool is_better_from(product const &) const;

  double get_price() const;

  int get_score() const;

};

void sorttable(int n, product* []);

int main()

{cout << setprecision(4) << setiosflags(ios::fixed);

 product table[300];

 product* ptable[300];

 int n;

 do

 {cout << „number of products? „;

  cin >> n;

 } while (n<1 || n>300);

 int i;

 for (i = 0; i <= n-1; i++)

 {table[i].read();

  ptable[i] = &table[i];

 }

 cout << „table: \n“;

 for (i = 0; i <= n-1; i++)

 {table[i].print();

  cout << endl;

 }

 sorttable(n, ptable);

 cout << „\n New Table: \n“;

 for (i = 0; i <= n-1; i++)

 {ptable[i]->print();

  cout << setw(7)

            << ptable[i]->get_score()/ptable[i]->get_price()

    << endl;

 }

 return 0;

}

void product::read()

{cout << „name: „;

 cin >> name;

 cout << „price: „;

 cin >> price;

 cout << „score: „;

 cin >> score;

}

void product::print() const

{cout << setw(25) << name

     << setw(10) << price

     << setw(12) << score;

}

bool product::is_better_from(product const & x) const

{return score/price > x.score/x.price;

}

double product::get_price() const

{return price;

}

int product::get_score() const

{return score;

}

void sorttable(int n, product* a[])

{for (int i = 0; i <= n-2; i++)

 {int k = i;

  product* max = a[i];

  for (int j = i+1; j <= n-1; j++)

  if (a[j]->is_better_from(*max)) 

   {max = a[j];

   k = j;

  }

  max = a[i]; a[i] = a[k]; a[k] = max;

 }

}

     Ще дадем още един пример, показващ връзка между класове и масиви. В него масивът е член-данна на клас, описващ последователно представяне на структурата от данни стек.

 

14.7 Стек

 

Стекът е линейна динамична структура от данни. В Глава 8 (Увод в програмирането на базата на езика C++) направихме кратко описание на тази структура. В тази част ще направим по-пълно описание. Ще започнем със следната задача.

 

Задача 121. Да се напише програма, която извежда двоичното представяне на естествено число.

Операторът

do

 {cout << k%2;

  k/=2;

 } while (k);

извежда двоичното представяне на числото k, но в обратен ред. За решаване на задачата ще използваме динамичната структура от данни стек.

     Стекът е крайна редица от елементи от един и същ тип. Операциите включване и изключване на елемент са допустими само за единия край на редицата, който се нарича връх на стека. Възможен е достъп само до елемента, намиращ се на върха на стека като достъпът е пряк.

     При тази организация на логическите операции, последният включен елемент се изключва пръв. Затова стекът се определя още като структура “последен влязъл – пръв излязъл”.

     Широко се използват два основни начина за физическо представяне (представяне в ОП) на стек: последователно и свързано. За целите на тази задача ще използваме последователното представяне.

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

     an, an-1, …, a1

е стек с връх an, последователното представяне на стека има вида:

 

 
 
n                      

 

a1

 

 

 

an-1

 

an

 

 

 

 

 

                                               указател към

                                               върха на стека

 

 

   
   
 
   
 
   

 

 

 

 

 

 

                                        връх на стека

                                                        неизползвана

част

             

При включване на елементи в стека, те се поместват в последователни адреси в неизползваната част веднага след върха на стека.

     Това физическо представяне ще реализираме като използваме структурата от дании масив. За указател към върха на стека ще служи цяла променлива. Ще използваме подхода абстракция със структури от данни. Първите две нива на абстракция реализираме чрез класа stack:

class stack

{public:

  stack();

  void push(int);

  void pop();

  void print();

  int top() const;

  bool empty() const;

 private:

  int n;        // указател към върха на стека

  int arr[NUM]; // представяне на стека

};

където

const int NUM = 100;

По такъв начин ограничаваме размера на стека до 99 (arr[0] ще инициализираме с 0 и няма да използваме). Масивът arr ще представя стека, а n ще е указателя към върха му.

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

void push(int);          – включва елемент в стека

void pop();                   – изключва елемент от стека

void print();                 – извежда елементите на стека като

                                     разрушава стека

int top() const;              – намира елемента от върха на стека

bool empty() const;      – проверява дали стекът е празен

 

Програма Zad121.cpp решава задачата.

// Program Zad121.cpp 

#include <iostream.h>

const NUM = 100;

class stack

{public:

  stack();

  void push(int);

  void pop();

  int top() const;

  bool empty() const;

  void print();

 private:

  int n;

  int arr[NUM];

};

stack num_stack(int); // конструира стек от двоичното представяне

                              // на указано цяло число

void main()

{cout << „number: „;

 int n;

 cin >> n;

 num_stack(n).print();

}

stack::stack()

{n = 0;

 arr[0]=0;

}

void stack::push(int x)

{n++;

 arr[n] = x;

}

void stack::pop()

{n–;

}

int stack::top() const

{return arr[n];

}

bool stack::empty() const

{return n == 0;

}

void stack::print()

{while (!empty())

 {cout << top();

  pop();

 }

 cout << endl;

}

stack num_stack(int x)

{stack st;

 while (x)

 {st.push(x%2);

  x/=2;

 }

 return st;

}

      

14.8 Динамични обекти

 

Вече разгледахме в най-общ план разпределението на ОП по време на изпълнението на програма. От Фиг. 8.1 на Глава 8 се вижда, че всяка програма има две “места” за памет: програмен стек (стек) и област за динамичните данни (динамична памет, хийп или куп).

Стекът е област за временно съхранение на информация. Той е кратковременна памет. C++ използва стека основно за реализиране на обръщения към функции. Всяко обръщение към функция предизвиква конструиране на нова стекова рамка, която се установява на върха на стека. По такъв начин когато функция A извика функция B, която от своя страна вика функция C, стекът нараства. Когато пък всяка от тези функции завършва, стековите рамки на тези функции автоматично се разрушава. Така стекът се свива.

Хийпът е по-постоянна област за съхранение на данни. Той е един вид дълготрайна памет. Особеност на тази памет е, че тя не се свързва с имена на променливи. С разположените в нея обекти се работи косвено – чрез указатели. Обикновено се използва при работа с т. нар. динамични структури от данни. Динамичните данни са такива обекти (в широкия смисъл на думата), чийто брой не е известен в момента на проектирането на програмата. Те се създават и разрушават по време на изпълнението на програмата. След разрушаването им, заетата от тях памет се освобождава и може да се използва отново. Така паметта се използва по-ефективно.

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

Създаването и разрушаването на динамични обекти в C++ се осъществява чрез операторите new и delete. Извикването на new заделя в хийпа необходимата памет и връща указател към нея. Този указател може да се съхрани в някаква променлива и да се пази докато е необходимо. За разлика от стека, заделянето на памет в хийпа е явно – чрез new. Освобождаването на паметта от хийпа също става явно, чрез delete. Всяко извикване на new трябва да бъде балансирано чрез извикване на delete. Последното се налага, тъй като за разлика от стека, хийпът не се изчиства автоматично. В C++ няма система за “събиране на боклуци” (автоматично премахване на обекти, които вече не са необходими). Затова трябва явно да бъдат изтрити създадените в хийпа обекти. Описанието на оператора new е дадено на Фиг. 14.8.

Оператор new

Синтаксис

new <име_на_тип> [ [size] ]опц;|

new <име_на_тип> (<инициализация>);

където

     – <име_на_тип> е име на някой от стандартните типове int, double, char и др. или е име на клас;

- size е израз с произволна сложност, но трябва да може да се преобразува до цял. Показва броя на компонентите от тип <име_на_тип>, за които да се задели памет в хийпа и се нарича размерност;

     – <инициализация> е израз от тип <име_на_тип> или инициализация на обект според синтаксиса на конструктора на класа, ако <име_на_тип> е име на клас.

     Семантика

     Заделя в хийпа (ако е възможно):

- sizeof(<име_на_тип>) байта, ако не са зададени size и <инициализация> или

- sizeof(<име_на_тип>)*size байта, ако явно е указан size или

- sizeof(<име_на_тип>) байта, ако е специфицирана <инициализация>, която памет се инициализира с <инициализация>

и връща указател към заделената памет.

Ако няма достатъчно памет в хийпа, операторът new връща NULL.

Фиг. 14.8 Оператор new

     Забележка: Ако <име_на_тип> е име на клас и след него има кръгли скоби, в тях трябва да стоят фактически параметри (аргументи) на конструктора на класа. Ако скобите липсват, класът трябва да притежава конструктор по подразбиране или да няма явно дефиниран конструктор. Ако след името на класа е поставен заграден в квадратни скоби израз, new заделя място за масив от обекти на указания клас и извиква конструктора по подразбиране за инициализиране на отделената памет.

Примери:

а) int* q = new int(2+5*5);  

отделя (ако е възможно) 4B памет в хийпа, инициализира я с 27 – стойността на израза 2+5*5 и свързва q с адреса на тази памет, т.е.

стекова рамка               област на динамична памет

 

       
       
 

 

 

 

 

 

       q                                                      0×00790D30

 

       
       

 

 

 

 

б) int* p = new int[10];

отделя (ако е възможно) 40B в хийпа (за 10 елемента от тип int) и свързва p с адреса на тази памет, т.е.

стекова рамка               област на динамична памет

 

       
     
 

 

 

0×00790D00

 

 

 

 

 

 

                                                                   0×00790D00             

       p                                                      0×00790D04

 

       
       
 
     

 

 

 

 

                                                                   0×00790D24        

                                     

в) rat* r = new rat(1,5);    

отделя памет в хийпа за един обект от клас rat, свързва r с адреса на тази памет и извиква конструктора rat(1,5) за да я инициализира, т.е.

стекова рамка               област на динамична памет

 

       
 

 

 

0×00790D20

 

 
 

 

 

1

 

5

 

 

 

 

 

 

 

      

r                                                       0×00790D20

 

       
       
 
     

 

 

 

 

 

г) rat* r = new rat;

отделя памет в хийпа за обект от тип rat, записва адреса на тази памет в r и извиква конструктора по подразбиране на класа rat за инициализиране на тази памет, т.е.

стекова рамка               област на динамична памет

 

       
       
 

 

 

 

 

 

       r                                                      0×00790D20

 

       
   
 
     

 

 

 

 

д) rat* r = new rat[10];

отделя памет в хийпа за 10 обекта от класа rat, записва адреса на тази памет в r, извиква конструктора по подразбиране на класа rat и инициализира отделената памет;

е) rat** parr = new rat*[5]; 

отделя 20B памет в хийпа за масив от 5 указателя към стойности от тип rat и записва в parr адреса на тази памет, т.е.

стекова рамка               област на динамична памет

 

       
 

 

 

0×00790D20

 

     
 

 

 

 

 

                                                                   0×00790D20             

  parr                                                        Ox00790D24

 0×00790D28

                                                                   0×00790D2C

                                                                   0×00790D30        

                                         

 

     Заделянето на памет по време на компилация се нарича статично заделяне на памет, заделянето на памет по време на изпълнение на програмата – динамично разпределение на паметта. Паметта за променливите q, p, r и parr, от примерите по-горе, е заделена статично, а всяка една от тези променливи има за стойност адрес от хийпа. Казва се още, че q, p, r и parr адресират динамична памет.

     Под период на активност на една променлива се разбира частта от времето за изпълнение на програмата, през което променливата е свързана с конкретно място в паметта. Паметта за глобалните променливи се заделя в началото и остава свързана с тях до завършването на изпълнението на програмата. Паметта за локалните променливи се заделя при влизане в локалната област и се освобождава при напускането й. Паметта на динамичните променливи се заделя от оператора new. Заделената по този начин памет остава свързана със съответната променлива докато не се освободи явно от програмиста. Явното освобождаване на динамична променлива се осъществява чрез оператора delete, приложен към указателя, който адресира съответната променлива. Едно непълно негово описание е дадено на Фиг. 14.9.

 

Оператор delete

Синтаксис

delete <указател_към_динамичен_обект>;

където <указател_към_динамичен_обект> е указател към динамичен обект (в широкия смисъл на думата), създаден чрез оператора new.

     Семантика

     Разрушава обекта, адресиран от указателя, като паметта, която заема този обект, се освобождава. Ако обектът, адресиран от указателя, е обект на клас, отначало се извиква деструкторът на класа (т.15.9) и след това се освобождава паметта.

 

Фиг. 14.9 Оператор delete

 

     Ако в хийпа е заделена памет, след което тази памет не е освободена чрез delete, се получава загуба на памет. Парчето памет, което не е освободено, е като остров в хийпа, заемащо пространство, което иначе би могло да се използва за други цели.

     За да се разруши масив, създаден чрез new по следния начин:

int* arr = new int[5];

трябва да се запише:

     delete [] arr;

     Забележка: Някои реализиции на езика допускат разрушаването в горния случай да стане и чрез delete arr.

     Ако обаче масивът съдържа в себе си указатели, първо трябва да бъде обходен и да бъде извикан операторът delete за всеки негов елемент. Едва след това може да се извика delete за масива по показания по-горе начин.

     Следващата задача илюстрира казаното.

 

Задача 122. Да се напише програма, която отделя динамична памет за масив от 10 указателя към тип int. Програмата да проверява дали отделянето  на памет е станало; запълва  масива с указатели към цели числа; извежда стойностите и съдържанието на указателите и освобождава отделената динамична памет.

 

// Program Zad122.cpp

#include <iostream.h>

int main()

{// заделяне на памет за масив от указатели към int

 int**  arr = new int*[10];

 if (arr == NULL)  //или if (!arr)

 {cout << „Not enough memory!\n“;

  return 1;

 }

 // запълване на масива с указатели

 int i;

 for (i=0; i<10; i++)

 {arr[i] = new int;      // заделяне на памет за указателя

  if (arr[i]==NULL) //или if (!arr[i])

  {cout << “Not enough memory!\n”;

   return 1;}

  *arr[i] = i;       // инициализиране на указваната стойност

 }

 // извеждане на стойностите и съдържанието на указателите

 for (i=0; i<10; i++)

   cout << arr[i] << “  “ << *arr[i] << „, „;

 cout << endl;

 // освобождаване на заетата динамична памет

 for (i = 0; i < 10; i++)

   delete arr[i];

 delete [] arr;

 return 0;

}

Операторът delete трябва да се използва само за освобождаване на динамична памет, заделена с new. В противен случай действието му е непредсказуемо. Няма забрана за прилагане на delete към указател със стойност 0. Ако стойността на указателя е 0, той е свободен и не адресира нищо.

Пример:

void example()

{…

 int a = 7;

 char *str = “abv”;

 int *pa = &a;

 rat *ptr = 0;

 double *x = new double;

 delete str; //некоректно обръщение, str не е адресирано чрез new

 delete pa; //некоректно обръщение, pa не е адресирано чрез new

 delete ptr; //некоректно обръщение, ptr не е адресирано чрез new

 delete x; // коректно обръщение

 …

}

Динамичната памет не е неограничена. Тя може да се изчерпи по време на изпълнение на програмата. Неуспешното завършване на delete ускорява изчерпването. Ако наличната в момента динамична памет е недостатъчна, new връща нулев указател и програмата (функцията) няма да работи. Затова се препоръчва след всяко извикване на new да се прави проверка за успешността й.

Чрез оператора new могат да се създават т. нар. динамични масиви – масиви с променлива дължина. Динамичните масиви се създават в динамичната памет. Следващата програма илюстрира този процес.

 

Задача 123. Да се напише програма, която създава динамичен масив от цели числа. Да се изведе масивът.

 

// Program 123. cpp

#include <iostream.h>

int main()

{int size; // дължина на масива

 do

 {cout << „size of array: „;

  cin >> size;

 } while (size<1);

 // създаване на динамичен масив arr от size елемента от тип int

 int* arr = new int[size];

 int i;

 for (i = 0; i <= size-1; i++)

  arr[i] = i;

 // извеждане на елементите на arr

 for (i = 0; i <= size-1; i++)

  cout << arr[i] << “ „;

 cout << endl;

 // освобождаване на заетата динамична памет

 delete [] arr;

 return 0;

}
Чрез оператора

delete [] arr;

е разрушен динамичният масив arr. Това може да стане и ако се напише само

delete arr;

тъй като елементите на масива arr са числа.

Забелязваме, че размерът size на динамичния масив може да се въведе или изчисли по време на изпълнение на програмата и не е задължително да бъде известен  по време на компилация. Това позволява да се подобри програмата на задача 120, като се използват динамични, а не “статични” масиви (т. 14.8).

Методите на класовете също могат да използват динамична памет, която се заделя и освобождава по време на изпълнението им, чрез операторите new и delete.

 

Задача 124. Да се модифицира класът product, реализиран в задача 120, така че за всяко име на компютър да се отделя точно толкова памет, колкото е необходимо, а не точно 21 байта.

 

За целта ще определим променливата name като указател към char и необходимата памет за съхраняването на името на компютър ще се заделя по време на изпълнение на член-функцията read(). Следват само фрагментите, където се налага модификация.

char s[40];

class product

{private:

  char* name;

  double price;

  int score;

 public:

  void read();

  void print() const;

  bool is_better_from(product const &) const;

  double get_price() const;

  int get_score() const;

};

void product::read()

{cout << „name: „;

 cin >> s;

 name = new char[strlen(s)+1];

 strcpy(name, s);

 cout << „price: „;

 cin >> price;

 cout << „score: „;

 cin >> score;

}

Използвана е глобална променлива s от тип низ, която играе ролята на буфер – временно съхранява въведено име. След това се намира дължината на този низ и в динамичната памет за name се отделя точно толкова памет, колкото е необходимо. Така за член-данната name на всеки обект на класа product ще се заделя нужната памет, а не винаги 21 B, която памет може да се окаже и недостатъчна.

Ще отбележим също, че заделената от член-функциите динамична памет не се освобождава автоматично при разрушаване на обектите на класове. Освобождаването на тази памет трябва да стане явно чрез оператора delete, който трябва да се изпълни преди разрушаването на обекта. Този процес може да бъде автоматизиран чрез използване на специален вид методи, наречени деструктори.

 

14.9 Деструктори

 

Разрушаването на обекти на класове в някои случаи е свързано  с извършване на определени действия, които се наричат заключителни. Най-често тези действия са свързани с освобождаване на заделена преди това динамична памет, възстановяване на състояние на програмата и др. Ефектът от заключителните действия е противоположен на ефекта на инициализацията. Естествено е да се даде възможност заключителните действия да се извършат автоматично при разрушаването на обекта. Това се осъществява от деструкторите.

Деструкторът е член-функция, която се извиква при:

- разрушаването на обект чрез оператора delete,

- излизане от блок, в който е бил създаден обект от класа.

Един клас може да има явно дефиниран точно един деструктор. Името му съвпада с името на класа, предшествано от символа ‘~’ (тилда), типът му е void и явно не се задава в заглавието. Деструкторът няма формални параметри.

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

 

Задача 125. Да се промени класът product, дефиниран в програма Zad120.cpp, като методът read() се преобразува в конструктор по подразбиране и се добави деструктор. Освен това table и ptable да се реализират като динамични масиви.

 

Програма Zad125.cpp решава тази задача. Освен исканите модификации тя  добавя и функцията за достъп

char* get_name() const;

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

 

// Program Zad125.cpp

#include <iostream.h>

#include <iomanip.h>

#include <string.h>

char s[40];

class product

{private:

  char* name;

  double price;

  int score;

 public:

  product();

  ~product();

  void print() const;

  bool is_better_from(product const &) const;

  char* get_name() const;

  double get_price() const;

  int get_score() const;

};

void sorttable(int n, product* a[]);

int main()

{cout << setprecision(4) << setiosflags(ios::fixed);

 cout << „size: „; // размерност на масива

 int size;

 cin >> size;

 //създава динамичен масив от size обекта на product

 product* table = new product[size];

 // заделя памет за динамичен масив от указатели

 // към size обекта на product

 product** ptable = new product*[size];

 int i;

 cout << „table: \n“;

 for (i = 0; i <= size-1; i++)

 {table[i].print();

  cout << endl;

  ptable[i] = &table[i];

 }

 sorttable(size, ptable);

 cout << „\n New Table: \n“;

 for (i = 0; i <= size-1; i++)

 {ptable[i]->print();

  cout << setw(7)

            << ptable[i]->get_score()/ptable[i]->get_price()

        << endl;

 }

 delete [size] table; // някои реализации допускат

 // пропускането на size

 delete [] ptable;       // някои реализации допускат

                                   // пропускането на []

 return 0;

}

product::~product()

{delete name;

}

product::product()

{cout << „name: „;

 cin >> s;

 name = new char[strlen(s)+1];

 strcpy(name, s);

 cout << „price: „;

 cin >> price;

 cout << „score: „;

 cin >> score;

}

void product::print() const

{cout << setw(25) << name

     << setw(10) << price

     << setw(12) << score;

}

bool product::is_better_from(product const & x) const

{return score/price > x.score/x.price;

}

char* product::get_name() const

{return name;

}

double product::get_price() const

{return price;

}

int product::get_score() const

{return score;

}

void sorttable(int n, product* a[])

{for (int i = 0; i <= n-2; i++)

 {int k = i;

  product* max = a[i];

  for (int j = i+1; j <= n-1; j++)

  if (a[j]->is_better_from(*max)) 

   {max = a[j];

   k = j;

  }

  max = a[i]; a[i] = a[k]; a[k] = max;

 }

}

В резултат от изпълнението на оператора

product* table = new product[size];

се създава динамичен масив table със size обекта на класа product. При създаването на всеки от тези обекти се изпълнява конструкторът на класа, т.е. за всеки обект на table ще бъдат въведени име, цена и точки.

     Операторът

product** ptable = new product*[size];

предизвиква заделяне на памет за динамичен масив от указатели към size обекта на product.

     Накрая, чрез операторите

delete [size] table;

delete [] ptable;

се разрушават динамичните масиви. При разрушаването на масива table деструкторът се изпълнява size пъти, което води до освобождаване на заделената чрез конструктора памет за съхраняване на имената на обектите.

     Недостатък на горната програма е, че не анализира резултата от new. Възможно е да няма достатъчно памет в хийпа. Тази ситуация е критична и изпълнението на програмата трябва да завърши. Това може да се коригира като след използването на new се добавят програмни фрагменти от вида:

if (!table)

{cout << „Not enough memory!\n“;

 return 1;

     }

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

     По повод на това, че за всяко обръщение към new трябва да има съответен delete, възниква въпросът: Когато функция върне указател или псевдоним към обект, създаден чрез new, кой носи отговорността за извикването на delete за този указател?

     Например, къде в програмния фрагмент

     struct object

     {int a, b;

     }

     object& myfunc();

     int main()

     {object& rmyobj = myfunc();

      cout << rmyobj.a << rmyobj.b << endl;

      return 0;

     }

     object& myfunc()

     {object *o = new object;

      o->a = 20;

      o->b = 40;

      return *o;   // връща самия обект

     }

да бъде изтрит rmyobj?

Функцията, която създава указателя или псевдонима нищо не може да направи, защото когато указателят или псевдонимът бъде върнат, тя вече ще е завършила. Така че, който е извикал функцията, той след това трябва да извика и delete. Възможно е извикващият да е програма, принадлежаща на друг програмист или ваша стара програма и да не помните тази подробност. Затова като цяло се смята за лошо програмиране връщането на указател, който после трябва да бъде изтрит. По-добре е да се върне обекта по стойност. В примерната програма по-горе в main, след извеждането на rmyobj трябва да се включи операторът delete &rmyobj.

 

14.10 Създаване и разрушаване на обекти на класове

 

     Съществуват два начина за създаване на обекти:

-            чрез дефиниция;

-            чрез функциите за динамично управление на паметта.

В първия случай обектът се създава при срещане на дефиницията (във функция или блок) и се разрушава при завършване на изпълнението на функцията или при излизане от блока. Дефиницията може да бъде поставена където и да е в тялото на функцията или блока, като пред и след нея може да има изпълними оператори. Дефиницията, чрез която се създава обект, може да бъде допълнена с инициализация, която може да се основава на извикване на обикновен конструктор или на конструктора за присвояване.

Разрушаването на обекта е свързано с извикването на деструктора на класа, ако такъв явно е дефиниран или с извикването на “системния” деструктор (деструктора по подразбиране), ако в класа явно не е дефиниран деструктор.

Пример: Нека в класа rat добавим деструктора

rat::~rat()

{cout << “destruct number: “

<< number << “/”

<< denum << endl;

}

Този избор на деструктор направихме с цел да наблюдаваме къде той ще бъде извикан. Ще напомним, че деструкторът извършва заключителни действия, определени от програмиста (или компилатора). Нека сега изпълним програмата с класа rat, в който е включен и деструкторът ~rat() с главна функция от вида:

void main()

{rat p(1,8);    // създава обект p и го инициализира с 1/8

 rat q=rat(2,9);     // създава обект q и го инициализира с 2/9

 for(int i=1; i<=5; i++)

 {rat r(i, i+1);     // създава обект r и го инициализира с i/(i+1)

  1.   r.print();         // за i = 1, …, 5

 }

 p.print();

 q.print();

}

В резултат от изпълнението на main се получава:

1/2

destruct number:1/2

2/3

destruct number:2/3

3/4

destruct number:3/4

4/5

destruct number:4/5

5/6

destruct number:5/6

1/8

2/9

destruct number:2/9

destruct number:1/8

От изпълнението се вижда, че деструкторът на класа rat е извикан толкова пъти, колкото  пъти са извършвани дейности по създаване на обекти на класа rat. Пътвите пет извиквания на деструктора са при завършване на изпълнението на блока на оператора for, а последните две – при завършване на изпълнението на функцията main.

Във втория случай създаването и разрушаването на обекти се управлява от програмиста. Създаването става с new, а разрушаването чрез delete. Операциите new се включват в конструкторите, а операциите delete – в деструктора на класа.

Пример:

rat *p = new rat(3,7); // търси в хийпа 8B, свързва адреса

                                // им с p, извиква конструктора

                                // rat(9,13) и инициализира паметта

(*p).print();

delete p;                // извиква деструктора, след което

// разрушава обекта

В този случай деструкторът само регистрира присъствието си. Получаваме:

3/7

destruct number: 3/7

 

14.11 Инициализиране на обекти на класове

 

     Езикът C++ позволява обектите на класове (както и обикновените променливи) да бъдат инициализирани при дефиницията си и при извикването на функцията new. При обикновените променливи инициализаторът задава стойност на променливата, а при обектите – осигурява аргументи на конструкторите. Инициализацията на обект на клас се извършва по следните начини:

     <име_на-клас> <обект>(<инициализатор>);|

     <име_на-клас> <обект> = <инициализатор>;

     Възможни са:

     а) инициализаторът не е обект на класа

     В този случай се създава обекта, след това се намира стойността на израза–инициализатор и се подава на подходящия конструктор (ако има такъв).

     Пример: Нека в класа rat конструкторът е с прототип:

rat(int = 0; int = 1);

Инициализацията

     rat p = 7;

ще създаде обекта p и ще извика конструктора rat(7), с който ще инициализира p. Ако в класа rat не беше дефиниран конструктор с един аргумент, опитът за тази инициализация щеше да е неуспешен.

     б) инициализаторът е обект на класа

     В този случай съществуват някои особености. Ако съществува явно дефиниран конструктор за присвояване, той се използва. В противен случай се използва подразбиращия се конструктор за копиране.

     Конструктор за присвояване явно не е дефиниран

Тогава се извиква подразбиращия се системен конструктор за копиране.

    Пример:

     rat p(1,9);

     rat q = p;

Създава се нов обект q с член-данни абсолютни копия на съответните член-данни на p.

     В този случай възникват проблеми ако някоя член-данна на обекта е указател към динамичната памет, тъй като член-променливата на новия обект, който е указател към динамичната памет, е със същата стойност като на стария (указва към същата памет). В този случай става поделяне на компонента на обектите.

    Пример: Ще използваме класа product, като ще направим някои модификации в него:

class product

{private:

  char* name;

  double price;

  int score;

 public:

  ~product();

  product();

  void print() const;

  bool is_better_from(product const &) const;

  char* get_name() const;

  double get_price() const;

  int get_score() const;

};

product::~product()

{delete name;

 cout << „destruct data for: “ << this << endl;

}

product::product()

{cout << „name: „;

 cin >> s;

 name = new char[strlen(s)+1];

 strcpy(name, s);

 cout << „price: „;

 cin >> price;

 cout << „score: „;

 cin >> score;

 cout << „new data: “ << this << endl;

}

void main()

{product p;

 product q = p;

}

В този случай дефинираният конструктор по подразбиране product() се извиква веднъж – при инициализирането на p. Тъй като няма явно дефиниран конструктор зa присвояване, генерираният от системата конструктор за копиране откопирва член-данните на обекта p в q като член-данната name е поделена. При завършване на блока – тяло на main, първо се разрушава обектът q. За него се извиква деструкторът. Поделената памет се освобождава, след което се разрушава и q. Забележете, q е разрушен, но е разрушена и част от p – поделената динамична памет. После започва процедурата по разрушаването и на обекта p. Извиква се деструкторът, който се опитва да освободи вече освободена памет. Това води до грешка, чийто последствия са непредвидими.

     Конструктор за присвояване явно е дефиниран

     Вече дефинирахме един глупав конструктор за присвояване за класа rat и правихме експерименти с него. Ще напомним, че конструкторът за присвояване е член-функция от вида:

     <име_на_клас>(<име_на_клас> const&)

     {<тяло>}

Ще дефинираме подходящ конструктор за присвояване в класа product и ще го извикаме за да реализираме коректно инициализацията от последния пример.

product::product(product const & p)

{name = new char[strlen(p.name)+1];

 strcpy(name, p.name);

 price = p.price;

 score = p.score;

}

и включваме прототипа му

     product(product const & p);

в public-частта на класа product.

Тогава функцията

void main()

{product p;

 product q = p;

}

вече работи добре. Дефинирани са два обекта p – инициализиран чрез конструктора по подразбиране product() и q – чрез дефинирания явно конструктор за присвояване. Деструкторът е извикан два пъти при завършване изпълнението на блока и разрушава обектите q и p.

     Дефинираният конструктор за присвояване решава проблемите, възникващи при инициализацията на обект на клас product. Използва се при предаване на обект по стойност, а също при връщане на обект като стойност на функция. Като цяло обаче той не решава проблемите на операцията за присвояване.

     Пример: Ако променим main по следния начин:

void main()

{product p, q;

 q = p;

}

отново възникват проблеми. Присвояването q = p; ще промени член-данните на q, но q вече има част в динамичната памет, която първо трябва да бъде освободена. Налага се да бъде дефинирана нова операторна функция за присвояване. Последното ще направим по следния начин:

product& product::operator=(product const & p)

{cout << „assignment!\n“;

 if(this != &p)

 {delete name;

  name = new char[strlen(p.name)+1];

  strcpy(name, p.name);

  price = p.price;

  score = p.score;

 }

 return *this;

}

като ще включим прототипа й

product& operator=(product const &);

в public-частта на класа.

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

     Дефинираната операторна функция има за формален параметър константен псевдоним от клас product. По този начин се избягва създаването на нов обект и извикването на конструктора за присвояване. Въпреки, че не е задължително, използването на константен псевдоним е препоръчително. Това позволява на компилатора да следи за евентуална промяна на обекта – фактически параметър. Освен, че променя обекта, указван от this, операторната функция от примера връща като резултат псевдонима му. Като следствие, конструкцията p = q може да се разглежда като израз (p = q връща p), а също да бъде лява страна на израз. Изразът p = q = r е допустим и е еквивалентен на q = r; p = q;

     В този пример реализирахме като член-функции на класа product конструктор за присвояване, операторна функция за присвояване и деструктор. Тези три функции се наричат “голямата тройка” или “канонична форма на класа”. На пратика те са задължителни при класове, използващи динамичната памет. Това не е просто добра идея, това е закон.

 

14.12 Масиви от обекти

 

Създаването на масив от обекти става по два начина:

-            чрез дефиниция

-            чрез функциите за динамично управление на паметта.

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

Примери: Ще използваме класа rat

а) {…

   rat x[10];

  }

В този случай конструкторът rat() е извикан 10 пъти. Конструиран е масивът от обекти x:

     x[0] x[1] … x[9]

     0/1  0/1  …  0/1

При завършване изпълнението на блока деструкторът ~rat() ще бъде извикан също 10 пъти за да разруши последователно x[9], x[8], …, x[0].

     б) {rat x[10] = {rat(1,2), rat(5), 8, rat(1,7)};

         }

В този случай конструкторът rat(int = 0, int = 1) е извикан 10 пъти. Конструиран е масивът от обекти x:

     x[0] x[1] x[3] x[4] x[5]     x[6] … x[9]

     1/2  5/1  8/1  1/7  0/1  0/1  …  0/1

При завършване изпълнението на блока деструкторът ~rat() ще бъде извикан също 10 пъти за да разруши последователно x[9], x[8], … x[0].

     При втория начин, създаването и разрушаването на масив от обекти се управлява от програмиста. Отново създаването става чрез new, а разрушаването – с delete, като операторите new се включват в конструкторите, а операторите delete – в деструкторът на класа, от който са обектите на масива.

    Примери:

     а){rat *px = new rat[10];

         delete[] px;

       }

В резултат, в хийпа се заделя блок от 80 байта (ако е възможно) и адресът на този блок се записва в px. Тъй като има дефиниран конструктор по подразбиране, конструкторът се извиква и px[i] (i=0, 1, …, 9) се инициализират с рационалното число 0/1. При масивите, реализирани в динамичната памет, инициализация в явен вид не може да се зададе. Разрушаването на px става чрез delete[] px;. Преди да прекъсне връзката между px и динамичната памет, операторът delete[] извиква деструктора за всеки от обектите на масива.

     б) {rat *px = new rat[10];

          delete px;

        }

В резултат, в хийпа се заделя блок от 80 байта (ако е възможно) и адресът на блока се записва в px. Тъй като в класа има конструктор по подразбиране, този конструктор се извиква и px[i] (i=0, 1, …, 9) се инициализират с рационалното число 0/1. Операторът delete px; извиква деструктора само на обекта px[0] и прекъсва връзката на px с динамичната памет. Компилаторът съобщава за грешка. Причината е, че px е масив от обекти в динамичната памет, a деструкторът на класа rat е извикан само за px[0]. Ще отбележим отново, че ако px беше масив в динамичната памет, но не от обекти на клас, операторът delete px; щеше да работи нормално.

     в) {int size;

          cin >> size;

          rat* px = new rat[size];

          delete[size] px;

         }

В този случай деструкторът на класа rat се извиква size пъти. Реализацията на Visual C++ 6.0 пренебрегва size от delete, но за някои други реализации това не е така.

 

14.13 Приятелски класове и функции

 

     Често е необходимо съвместното използване на два класа. Обектно-ориентираното програмиране налага капсолирането на данните. Достъпът до private-компонентите на даден клас от функция извън класа е забранено. В редица случаи това е сериозно затруднение. Например, дефинирани са два класа, представящи вектор и матрица съответно. Функцията, която ще реализира произведението на вектор с матрица ще трябва да има достъп до членовете и на двата класа. Един начин за реализирането на това е да се направят член-данните и на двата класа public. Това ще доведе до загубване на предимствата на капсолирането. Друг начин е да се използват public функции на достъп, осъществяващи достъп до стойностите на член-променливите. Това води до забавяне на изпълнението на програмата. Трети начин за решаване на този проблем е декларирането на функции или класове – приятели на класа. Приятелите на даден клас (функции или класове) имат достъп до всички негови компоненти, т.е. членовете на класа са винаги public за функциите приятели. Ако клас е деклариран като приятел, всички негови член-функции стават функции приятели.

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

 

14.14 Оператори. Предефиниране на оператори

 

     Езикът C++ има богат набор от оператори. В него са дадени също средства за предефиниране на оператори.

     Всеки оператор се характеризира с:

-            позиция на оператора спрямо аргументите му;

-            приори  тет;

-            асоциативност.

Позицията на оператора спрямо аргументите му го определя като: префиксен (операторът е пред единствения му аргумент), инфиксен (операторът е между аргументите си) и постфиксен (операторът е след аргумента си).

Пример: Операторът / е инфиксен (4/8), операторът + e както инфиксен, така и префиксен (2+8, +78), а операторът ++ е както постфиксен, така и префиксен.

Приоритетът определя реда на изпълнение на операторите в операторен терм. Оператор с по-висок приоритет се изпълнява преди оператор с по-нисък приоритет.

Пример: Приоритетът на * и / е по-висок от този на + и -.

Асоциативността определя реда на изпълнение на оператори с еднакъв приоритет в операторен терм. В C++ има лявоасоциативни и дясноасоциативни оператори. Лявоасоциативните оператори се изпълняват отляво надясно, а дясноасоциативните – отдясно наляво.

В C++ не могат да се дефинират нови оператори, но всеки съществуващ едноаргументен или двуаргументен оператор с изключение на ::, ?:, ., *, # и ## може да бъде предефиниран от програмиста, стига  поне един операнд на оператора да е обект на някакъв клас.

Например, възможно е да се предефинират операторите +, -, * и /, така че да могат да събират, изваждат, умножават и делят рационални числа. Тогава вместо sum(p, q), sub(p, q), mult(p, q) и quot(p, q) ще можем да пишем p+q, p-q, p*q и p/q, което безспорно е много по-удобно.

Предефинирането се осъществява чрез дефиниране на специален вид функции, наречени операторни функции. Последните имат синтаксис като на обикновените функции, но името им се състои от запазената дума operator, следвана от мнемоничното означение на предефинирания оператор. Когато предефинирането на оператор изисква достъп до компонентите на класове, обявени като private или protected, операторната дефиниция трябва да е член-функция или функция-приятел на тези класове. Предефинираният оператор запазва всички характеристики на оригиналния.

Предефинирането може да стане по два начина:

-            чрез функция–приятел;

-            чрез член-функция.

Чрез примери ще покажем тези два начина.

Предефиниране чрез функция-приятел

     Задача 126. Да се предефинират операторите +, -, * и / така, че да могат да бъдат използвани за събиране, изваждане, умножение и деление на рационални числа.

 

     Програма Zad126_1.cpp решава задачата. В public частта на класа rat са включени декларациите на предефинираните оператори, предшествани от запазената дума friend:

friend rat operator+(rat const &, rat const &);

friend rat operator-(rat const &, rat const &);

friend rat operator*(rat const &, rat const &);

friend rat operator/(rat const &, rat const &);

а след дефиницията на функцията main са дадени и техните дефиниции.

    

// Program Zad126_1.cpp

#include <iostream.h>

class rat

{private:

  int numer;

  int denom;

 public:

  rat(int=0, int=1);

  ~rat()

  {cout << „destruct number: “ << numer <<

           „/“ << denom << endl;

  }

  void read();

  int get_numer() const;

  int get_denom() const;

  void print() const;

  friend rat operator+(rat const &, rat const &);

  friend rat operator-(rat const &, rat const &);

  friend rat operator*(rat const &, rat const &);

  friend rat operator/(rat const &, rat const &);

};

rat::rat(int a, int b)

{numer = a;

 denom = b;

 cout << „construct! \n“;

}

void rat::read()

{cout << „numer: „;

 cin >> numer;

 do

 {cout << „denom: „;

  cin >> denom;

 } while (denom == 0);

}

int rat::get_numer() const

int rat::get_denom() const

void rat::print() const

int main()

{rat p(1,3), q(2,5), r(p+q);

 r.print();

 r = p-q-q;

 r.print();

 return 0;

}

// предефиниране на оператора +

rat operator+(rat const & r1, rat const & r2)

{rat r(r1.numer*r2.denom +

      r2.numer*r1.denom,

      r1.denom*r2.denom);

 return r;

}

// предефиниране на оператора -

rat operator-(rat const & r1, rat const & r2)

{rat r(r1.numer*r2.denom-

      r2.numer*r1.denom,

      r1.denom*r2.denom);

 return r;

}

// предефиниране на оператора *

rat operator*(rat const & r1, rat const & r2)

{rat r(r1.numer*r2.numer,

      r1.denom*r2.denom);

 return r;

}

// предефиниране на оператора /

rat operator/(rat const & r1, rat const & r2)

{rat r(r1.numer*r2.denom,

      r1.denom*r2.numer);

 return r;

}

 

    Забележки:

1)        Изразът p+q се интерпретира като извикване на операторната функция operator+(p, q).

2)        Запазва се асоциативността. Изразът p-q-r се интерпретира като (p-q)-r.

 

Предефиниране чрез член-функция

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

 

Ще модифицираме предишната програма като дефинираме операторните функции за събиране, изваждане, умножение и деление като член-функции на класа rat.

// Program Zad126_2.cpp

#include <iostream.h>

class rat

{private:

  int numer;

  int denom;

 public:

  rat(int=0, int=1);

  ~rat()

  {cout << „destruct number: “ << numer

       << „/“ << denom << endl;

  }

  void read();

  int get_numer() const;

  int get_denom() const;

  void print() const;

  rat operator+(rat const &) const;

  rat operator-(rat const &) const;

  rat operator*(rat const &) const;

  rat operator/(rat const &) const;

 

};

rat::rat(int a, int b)

void rat::read()

int rat::get_numer() const

int rat::get_denom() const

void rat::print() const

int main()

{rat p(1,3), q(2,5), r(p+q);

 r.print();

 r = p-q-q;

 r.print();

 return 0;

}

rat rat::operator+(rat const & r1) const

{rat r(numer*r1.denom + r1.numer*denom,

      denom*r1.denom);

 return r;

}

rat rat::operator-(rat const & r1) const

{rat r(numer*r1.denom – denom*r1.numer,

      denom*r1.denom);

 return r;

}

rat rat::operator*(rat const & r1) const

{rat r(numer*r1.numer,

      denom*r1.denom);

 return r;

}

rat rat::operator/(rat const & r1) const

{rat r(numer*r1.denom,

      denom*r1.numer);

 return r;

}

Ще отбележим, че в този случай изразът p+q се интерпретира като p.operator+(q).

    

14.15 Приложение на средствата за работа

    с динамичната памет

Ще конструираме клас stack, който ще реализира свързаното представяне на стек от цели числа. Фиг. 14.10 илюстрира това представяне.

 

 

NULL

 

  start

 

 

 

 

 

5    NULL

 

  start              inf  link

 

 

 
   

 

 

 

 

 

 

5      NULL

 

15    

 

 

  start              inf  link     inf  link   

 

 

       
       

 

 

 

 

 

 

5    NULL

 

15       

 

25    

 

 

  start              inf  link     inf  link   inf  link  

 

 

           
           

 

 

 

 

 

Фиг. 14.10 Свързано представяне на стек

 

Забелязваме, че има указател start, който в първия случай представя празен стек, а в останалите случай – непразен, като сочи двойна кутия с информационна част (inf) от тип int и свързваща част (link) от типа на start. Това представяне ще реализираме по следния начин:

struct elem

{int inf;

 elem* link;

} *start = NULL, *p;

След тази дефиниция start представя празен стек. Включването на елемента 5 можем да направим чрез изпълнение на следните действия:

p = start;

start = new elem;

start->inf = 5;

start->link = p;

Включването на 15 ще направим по аналогичен начин

p = start;

start = new elem;

start->inf = 15;

start->link = p;

а на 25 – чрез

p = start;

start = new elem;

start->inf = 25;

start->link = p;

Тези разсъждения показват, че който и да е елемент x може да се включи в стека чрез изпълнение на фрагмента:

p = start;

start = new elem;

start->inf = x;

start->link = p;

     Изключването на елемент от последния стек от фиг. 14.10 води до получаване на стека, илюстриран на по-горната стъпка на същата фигура и може да се реализира така:

p = start;

x = start->inf;

start = start->link;

delete p;

В x е запомнен изключеният елемент. Така получаваме следната непълна реализиция на свързаното представяне на стек от цели числа:

struct elem

{int inf;

 elem* link;

};

class stack

{public:

  stack(); // конструктор по подразбиране

  void push(int const&); // член-функция за включване на елемент

  int pop(int & x);  // член-функция за изключване на елемент

 private:

  elem *start;

};

stack::stack()

{start = NULL;

}

void stack::push(int const& s)

{elem* p = start;

 start = new elem;

 start->inf = s;

 start->link = p;

}

int stack::pop(int & s)

{elem *p;

 if (start)

 {s = start->inf;

  p = start;

  start = start->link;

  delete p;

  return 1;

 }

return 0;

}

Забелязваме, че pop връща 1 ако операцията изключване е възможна и 0 – ако не е.

     Тъй като обектите на класа stack са реализирани в динамичната памет, за него трябва да реализираме каноничното представяне.

     деструктор

     Единствената член-данна на класа stack e указател към динамичната памет, където е разположен стекът. Затова деструкторът трябва да изтрие стека от паметта. Тъй като изтриването на стек ще е необходимо и за други цели, ще го реализираме чрез член-функцията delstack() на класа.

void stack::delstack()

{elem *p;

 while (start)

 {p = start;

  start = start->link;

  delete p;

 }

}

Тогава деструкторът ~stack() има вида:

stack::~stack()

{delstack();

}

     конструктор за присвояване

     Kонструкторът за присвояване

     stack(stack const & r);

 откопирва стека r в неявния параметър и ще го реализираме по следния начин:

stack::stack(stack const & r)

{copy(r);

}

където copy(r) ще дефинираме като член-функция на stack и тя ще реализира копиране на стека r в неявния параметър.

     операторна функция за присвояване

     Ще я реализираме така:

stack& stack::operator=(stack const& r)

{if (this != &r)

 {delstack();

  copy(r);

 }

 return *this;

}

Копирането ще реализираме чрез член-функцията copy(stack const &r). За целта ще сканираме елементите на r (без да ги разрушаваме) и ще ги запишем на друго място в динамичната памет.

void stack::copy(stack const & r)

{if (r.start) // r не е празен

{elem *p = r.start, *q = NULL, *s=NULL;

 start = new elem;  

 start->inf = p->inf;

 start->link = q;

 q = start;

 p = p->link;

 while (p)

 {s = new elem;

  s->inf = p->inf;

  q->link = s;

  q = s;

  p = p->link;

 }

 q->link = NULL;

}

else start = NULL;

  }

Класът stack, реализиращ свързаното представяне на стек, има вида:

struct elem

{int inf;

 elem* link;

};

class stack

{public:

  stack();

  ~stack();

  stack(stack const &);

  stack& operator=(stack const & r);

  void push(int const&);

  int  pop(int & x);

  bool empty() const;

  void print();

 private:

  elem *start;

  void delstack();

  void copy(stack const&);

};

stack::stack()

{start = NULL;

}

stack::~stack()

{delstack();

}

stack::stack(stack const & r)

{copy(r);

}

stack& stack::operator=(stack const& r)

{if (this != &r)

{delstack();

 copy(r);

}

return *this;

}

void stack::delstack()

{elem *p;

 while (start)

 {p = start;

  start = start->link;

  delete p;

 }

}

void stack::copy(stack const & r)

{if(r.start)

{elem *p = r.start, *q = NULL, *s=NULL;

 start = new elem;

 start->inf = p->inf;

 start->link = q;

 q = start;

 p = p->link;

 while (p)

 {s = new elem;

  s->inf = p->inf;

  q->link = s;

  q = s;

  p = p->link;

 }

 q->link = NULL;

}

else start = NULL;

}

void stack::push(int const& s)

{elem* p = start;

 start = new elem;

 start->inf = s;

 start->link = p;

}

int stack::pop(int & s)

{if (start)

 {s = start->inf;

  elem *p= start;

  start = start->link;

  delete p;

  return 1;

 }

 else return 0;

}

void stack::print()

{int x;

 while (pop(x))

   cout << x << “ „;

 cout << endl;

}

bool stack::empty() const

{return start==NULL;

}

В него са добавени и член-функциите:

     void print();

която извежда елементите на стек и

     bool empty() const;

проверяваща дали стек е празен.

     Следващите програми използват този клас.

    

Задача 127. Да се напише функция void sortstack(stack s, stack & ns), която сортира елементите на стека от цели числа s по метода на пряката селекция. Стекът ns съдържа резултата.

 

     Функцията

void minstack(stack s, int& min, stack &newst);

намира минималния елемент на стека s, а също стека newst, съдържащ елементите на s без минималния.

void minstack(stack s, int& min, stack &newst)

{int x;

 s.pop(min);

 while (s.pop(x))

 if (x<min)

  {newst.push(min);

   min = x;

  }

 else newst.push(x);

 }

 

void sortstack(stack s, stack& ns)

{int min;

 while (!s.empty())

 {stack s1;

  minstack(s, min, s1);

  ns.push(min);

  s = s1;

 }

}

Едно приложение на структурата от данни стек е пресмятането на стойности на изрази.

 

Задача 128. От клавиатурата е въведена записана без грешка формула от вида:

<формула> ::=  <цифра>|

                     M(<формула>, <формула>)|

                     m(<формула>, <формула>)

<цифра> ::= 0|1|…|9,

където M означава функция за намиране на максимума на две цифри, а m – функция за намиране на минимума на две цифри. Въвеждането продължава до срещане на символа ‘.’. Например, стойността на формулата 8 е 8, а стойността на M(1,m(9,6)) e 6.

 

     Функцията formula решава задачата. Тя използва помощен стек от символите ‘М’, ‘m’ и цифрите и реализира следния алгоритъм. Ако прочетеният символ е ‘M’, ‘m’, ‘0’, …,’9’, се записва в стека s. Ако е ‘)’, от s се изключват три елемента. Тъй като въвежданата формула е правилна (по условие) изключените елементи са два операнда и операция (M – max или m – min). Извършва се операцията и полученият резултат се включва в стека. Останалите символи (интервали, ‘(‘, ‘,’, символите за преминаване на нов ред) се пропускат.

     Помощният стек от символи ще реализираме като обект на клас stack от символи, който получаваме като заместим в дефиницията на класа stack типа int с char.

int formula()

{char c, x, y, op;

 stack st;

 cin >> c;

 while (c!=’.')

 {if (c==’m'||c==’M'||c>=’0′&&c<=’9′) st.push(c);

  else

    if (c==’)')

    {st.pop(y);

     st.pop(x);

     st.pop(op);

     switch (op)

     {case ‘m’: if(x<y)c=x;else c=y; break;

      case ‘M’: if(x>y)c=x;else c=y;

     }

     st.push(c);

    }

    cin >> c;

 }

 st.pop(c);

 return (int)c-(int)’0′;

     }

 

14.16 Шаблони на функции и класове

 

В предните разглеждания създадохме класа stack, реализиращ стек от цели числа. След това за други цели се наложи да променим този клас в клас, реализиращ стек от символи. Промяната беше елементарна – просто заменихме типа int на елементите на стека с char. Възможно е обаче в рамките на една и съща програма да е необходимо конструирането на няколко стека от различен тип данни. Така възниква необходимостта от средства, реализиращи класове, зависещи от параметри, задаващи типове данни и при конкретни приложения параметрите да се конкретизират.

Такива средства са шаблоните. Те позволяват създаването на класове, използващи неопределени (хипотетични) типове данни за своите аргументи и по такъв начин позволяват да бъдат описвани “обобщени” типове данни. Използват се за изграждане на общоцелеви класове-контейнери (стекове, опашки, списъци и др.). Например, чрез шаблон може да се дефинира обобщен клас за стек с неопределен тип на елементите, след което от шаблона да се получат специфични класове (клас, реализиращ стек от реални числа; клас, реализиращ стек от символи и т.н.). При наличие на шаблони на класове възниква необходимостта и от шаблони на функции, използващи шаблони на класове. Например, искаме на реализираме сортиране на елементите на шаблон на стек. Налага се да дефинираме шаблони на функциите за намиране на минимален елемент на стек с произволен тип на елементите и сортиране на елементите на стек с произволен тип на елементите.

 

14.16.1 Шаблони на функции

 

Дефиницията на шаблон на функция е дадена на Фиг. 14.11.

 

Дефиниция на шаблон на функция

<дефиниция_на_шаблон_на_функция> ::=

  template < class <параметър> {,class <параметър>}опц >

    <тип_на_функция> <име_на_функция>(<формални_параметри>)

    <тяло>

където

  – <параметър> и <име_на_функция> са идентификатори;

- <формални_параметри> и <тяло> се определят както при дефиниция на функция. В тях са използвани указаните параметри на шаблона, вместо конкретните типове.

 

Фиг. 14.11 Дефиниция на шаблон на функция

 

Дефиницията започва със запазената дума template (шаблон), следвана от списък от параметри на шаблона, които трябва да участват като типове на аргументи на дефинираната като шаблон функция.

     Използването на дефинираните шаблони на функции става чрез обръщение към “обобщената” функция, която шаблонът дефинира, с параметри от конкретен тип. Компилаторът генерира т. нар. шаблонна функция, като замества параметрите на шаблона с типовете на съответните фактически параметри. При това заместване не се извършват преобразувания на типове.

 

     Задача 129. Да се напише програма, която дефинира шаблон на процедура за въвеждане елементите на масив и шаблон на функция, намираща минималния елемент на масив от елементи, които могат да се сравняват.

 

     // Procedure Zad129.cpp

#include <iostream.h>

template <class T>

void read(int n, T* a)

{for (int i = 0; i<=n-1; i++)

 {cout << „a[" << i << "]= „;

  cin >> a[i];

 }

}

template <class T>

T minarray(int n, T* a)

{T min = a[0];

 for (int i = 1; i<=n-1; i++)

  if (a[i]<min) min = a[i];

 return min;

}

int main()

{cout << „n: „;

 int n;

 cin >> n;

 int a[10];

 read(n, a);

 cout << minarray(n, a) << endl;

 double b[10];

 read(n, b);

 cout << minarray(n, b) << endl;

 return 0;

}

 

14.16.2 Шаблони на класове

 

Дефинирането на шаблон на клас се състои от декларация на шаблона и дефиниране на член-функциите му. На Фиг. 14.12 е даден непълно синтаксисът на декларацията на шаблон на клас.

 

Декларация на шаблон на клас

<декларация_на_шаблон_на_клас> ::=

    template < class <параметър>[=<име_на_тип>]опц

                 {,class <параметър>[=<име_на_тип>]опц}опц

                     >

    class <име_на_шаблон_на_клас>   

      <тяло>;

където

  – <параметър>, <име_на_шаблон_на_клас> и <име_на_тип> са идентификатори. В <тяло> са използвани указаните параметри на шаблона, вместо конкретните типове.

 

Фиг. 14.12 Декларация на шаблон на клас

 

Броят на параметрите на шаблон на клас е произволен. Параметрите могат да участват на произволни места в дефиницията на шаблона. Освен това е възможно някои или всички параметри на шаблона да са подразбиращи се. Това се осъществява чрез добавяне на инициализацията =<име_на_тип> след името на параметъра. В този случай, при пропускане на параметър, се използва подразбиращият се тип.

    Пример: Декларацията

  template <class T, class S = int> class CLASS

  {public:

   T func1(T x, S y);

   S func2(T x, S y);

   private:

    T a;

    S b;

  };

определя шаблон на клас CLASS с два параметъра T и S, като вторият е подразбиращ се – при неуказване, S се интерпретира като тип int.

     Дефинирането на член-функции на шаблон се осъществява по два начина – като вградени и не като вградени (описани извън декларацията). При дефинирането на вградените член-функции няма особености. Ако е необходимо, използват се параметрите на класа.

    Пример:

     template <class T, class S = int> class CLASS

     {public:

       T func1(T x, S y)         // вградена член-функция

       {cout << „func1 \n“;

        return x;

       }

       S func2(T x, S y);

      private:

       T a;

       S b;

      };

 

В другия случай дефиницията се предшества от

template <списък_от_параметри>

а пълното име на член-функцията на шаблона се получава с префикса

     <име_на_шаблон_на_клас> < <параметър> {,<параметър>}опц >

    Пример:

template <class T, class S>

S CLASS<T, S>::func2(T x, S y)

{cout << „func2 \n“;

 return y;

}

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

Шаблоните на класове не са истински класове, а описания, които се използват от компилатора за създаване на конкретни (шаблонни) класове. Наричат се още специализации на шаблона на класа.

     Фиг. 14.13 дава дефиницията на шаблонни класове.

 

    Дефиниция на шаблонен клас

     <дефиниция_на_шаблонен_клас> ::=

           <име_на_шаблон_на_клас> < <тип>, <тип>,… >                       

   <тип> ::= <име_на_тип>

 
Фиг. 14.13 Дефиниция на шаблонен клас

 

Ако някой <тип> е пропуснат, използва се подразбиращият се, ако декларацията на шаблона е с подразбиращи се параметри, или се съобщава за грешка. При срещане на декларация на шаблонен клас, на базата на зададените типове, компилаторът генерира съответен шаблонен клас.

Пример:

а) typedef CLASS<int, double> obj1;

дефинира класа obj1, който е специализация на шаблона на класа CLASS при T – int и S – double. Дефиницията

obj1 о1;

определя o1 за обект на класа obj1, който може да се обръща към public-членовете на obj1, т.е. допустимо е обръщението

o1.func1(5, 10.87);

б) typedef CLASS<int> obj2;

дефинира класа obj2, който е специализация на шаблона на класа CLASS при T – int и S – int. Дефиницията

obj2 о2;

определя o2 за обект на класа obj2, който може да се обръща към public-членовете на obj2, т.е. допустимо е обръщението

o2.func2(5,8);

Забележка: Ако на CLASS и двата параметър бяха подразбиращи се, щеше да е възможна и специализацията:

typedef CLASS <> obj3;   // ъгловите скоби <> трябва да присъстват

 

     Като илюстрация на казаното ще дефинираме и използваме шаблон на класа stack.

 

Задача 130. Да се дефинират шаблон на класа stack и шаблонна функция за сортиране на елементите на шаблон на стек.

 

Програма Zad130.cpp решава задачата.

// Program Zad130.cpp

#include <iostream.h>

template <class T>

struct elem

{T inf;

 elem* link;

};

template <class T>

class stack

{public:

  stack();

  ~stack();

  stack(stack const &);

  stack& operator=(stack const & r);

  void push(T const&);

  int  pop(T & x);

  bool empty() const;

  void print();

 private:

  elem<T> *start; // указател към шаблона на структурата elem

  void delstack();

  void copy(stack const&);

};

template <class T>

stack<T>::stack()

{start = NULL;

}

template <class T>

stack<T>::~stack()

{delstack();

}

template <class T>

stack<T>::stack(stack<T> const & r)

{copy(r);

}

template <class T>

stack<T>& stack<T>::operator=(stack<T> const& r)

{if (this != &r)

{delstack();

 copy(r);

}

return *this;

}

template <class T>

void stack<T>::delstack()

{elem<T> *p;

 while (start)

 {p = start;

  start = start->link;

  delete p;

 }

}

template <class T>

void stack<T>::copy(stack<T> const & r)

{if (r.start)

{elem<T> *p = r.start, *q = NULL, *s=NULL;

 start = new elem<T>;

 start->inf = p->inf;

 start->link = q;

 q = start;

 p = p->link;

 while (p)

 {s = new elem<T>;

  s->inf = p->inf;

  q->link = s;

  q = s;

  p = p->link;

 }

 q->link = NULL;

}

else start = NULL;

}

template <class T>

void stack<T>::push(T const& s)

{elem<T> *p = start;

 start = new elem<T>;

 start->inf = s;

 start->link = p;

}

template <class T>

int stack<T>::pop(T & s)

{if (start)

 {s = start->inf;

  elem<T> *p= start;

  start = start->link;

  delete p;

  return 1;

 }

 else return 0;

}

template <class T>

void stack<T>::print()

{T x;

 while (pop(x))

   cout << x << “ „;

 cout << endl;

}

template <class T>

bool stack<T>::empty() const

{return start == NULL;

}

template <class T>

void minstack(stack<T> s, T& min, stack<T> &newst)

{T x;

 s.pop(min);

 while (s.pop(x))

 if (x<min)

  {newst.push(min);

   min = x;

  }

 else newst.push(x);

}

template <class T>

void sortstack(stack<T> s, stack<T>& ns)

{T min;

 while (!s.empty())

  {stack<T> s1;

   minstack(s, min, s1);

   ns.push(min);

   s = s1;

  }

}

void main()

{typedef stack<int> IntStack;

 IntStack s, s1;

 s.push(10); s.push(1); s.push(5); s.push(12); 

 s.push(8); s.push(14); s.push(19); s.push(0);

 sortstack(s, s1);

 s1.print();

}

Във функцията main чрез

typedef stack<int> IntStack;

е дефиниран класът IntStack. Освен това са дефинирани два шаблона на функции – за намиране на минимален елемент на стек с елементи от тип Т и за сортиране на елементи на стек от тип Т.

     Шаблонът на клас дефинира съвкупност от класове. Понякога е по-удобно за някакъв тип данни член-функция на шаблона на класа да се реализира по различен алгоритъм. В C++  е възможно предефинирането на член-функция на шаблон на клас за конкретен тип. Този процес се нарича специализация на член-функция.

     Пример: Член-функцията print() на шаблона на stack извежда елементите на стек от тип T. Ако стекът, за който я използваме е от символи – ще бъдат изведени символите. Ако е необходимо само в този случай да се извеждат ASCII-кодовете на символите, може да бъде предефинирана член-функцията print() като самостоятелна функция от вида:

void stack<char>::print()

{char x;

 while (pop(x))

     cout << (int)x << “ „;

 cout << endl;

}

Последната ще бъде използвана само при обръщение за извеждане на стек от символи, т.е.

typedef stack<char> CharStack;     // дефиниция на клас CharStack

CharStack s;             // s е обект на класа CharStack

s.push(‘u’); s.push(‘b’);

s.push(‘p’); s.push(‘x’); 

s.print();  // ще се изведат ASCII-кодовете

// на символите на стека.

     Шаблоните на класове могат да имат за приятели класове, функции, шаблони на класове и шаблони на функции.

    Пример: Нека имаме дефинициите:

     class CLASS1 {…};

     double func1(…){…}

     template <class T> class CLASS2{…};

     template <class T> double func2(T){..};

     В дефиницията на шаблон на нов клас може да се въведат декларациите:

friend class CLASS1;

     friend double func1(…);

     friend class CLASS2<int>;   // само специализацията за int на

// шаблона на класа CLASS2 е приятел

     friend double func2(double); // само специализацията за

// double е приятел

   friend class CLASS2<T>;       // всеки обект на шаблона

// на класа CLASS2 е приятел

     friend double func2(T);          // всяка функция, създадена

// от шаблона, е приятел.

 

Допълнителна литература

  1. B. Stroustrup, C++ Programming Language. Third Edition, Addison – Wesley, 1997.
    1. К. Хорстман, Принципи на програмирането със C++, София, ИК СОФТЕХ, 2000.
    2. Ал. Стивънс, Кл. Уолнъм, C++ БИБЛИЯ, София, АЛЕКССОФТ, 2000.
    3. Ст. Липман, Езикът C++ в примери, “КОЛХИДА ТРЕЙД” КООП, София, 1993.

→ Leave a CommentКатегории: Програмиране

Десета част

19/05/2009 · Вашият коментар

 

 

11

 

 

Структури

 

 

11.1 Структура от данни запис

 

Логическо описание

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

Физическо представяне

Полетата на записа се представят последователно в паметта.

 

Примери:

1. Данните за студент от една група (име, адрес, факултетен номер, оценки по изучаваните предмети) могат да се зададат чрез запис с четири полета.

2. Данните за книга от библиотека (заглавие, автор, година на издаване, издателство, цена) могат да се зададат чрез запис с пет полета.

3. Комплексно число може да се зададе чрез запис с две реални полета.

 

В езика C++ записите се реализират чрез структури. Ще разгледаме последните в развитие. Отначало ще опишем възможностите им на ниво -език C.

 

11.2 Дефиниране и използване на структури

 

Една структура се определя чрез имената и типовете на съставящите я полета. Фиг. 11.1 дава непълна дефиниция на структура.

 

Дефиниция на структура

<дефиниция_на_структура> ::= struct <име_на_структура>

                {<дефиниция_на_полета>;

                 {<дефиниция_на_полета>;}опц

                };

<име_на_структура> ::= <идентификатор>

<дефиниция_на_полета> ::= <тип> <име_на_поле>{, <име_на_поле>}опц

<тип> ::= <име_на_тип>|<дефиниция_на_тип>

<име_на_поле> ::= <идентификатор>

 

Фиг. 11.1 Дефиниция на структура

 

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

 

Примери:

  1. struct complex

{double re, im;};

задава структура с име complex с две полета с имена re и im от тип double. Чрез нея се задават комплексните числа.

  1. struct book

{char name[41], author[31];

 int year;

 double price;

};

задава структура с име book с четири полета: първо поле с име name от тип символен низ с максимална дължина 40 и определящо името на книгата; второ поле с име author от тип символен низ с максимална дължина 30, определящо името на автора на книгата; трето поле с име year от тип int, определящо годината на издаване и четвърто поле с име price от тип double и определящо цената на книгата. Чрез тази структура се задава информация за книга.

  1. struct student

{int facnum;

 char name[36];

 double marks[30];

};

задава структура с име student и с три полета: първо поле с име facnum от тип int, означаващо факултетния номер на студента; второ поле с име name от тип символен низ с максимална дължина 35, определящо името на студента и трето поле с име marks от тип реален масив с 30 компоненти и означаващо оценките от положените изпити.

     Възможно е за име на структура, на нейно поле и на произволна променлива на програмата да се използва един и същ идентификатор. Но тъй като това намалява читаемостта на програмата, засега не препоръчваме използването му.

     Допуска се влагане на структури, т.е. поле на структура може да е от тип структура.

     Пример: Допустими са дефинициите:

struct xx

{int a, b, c;

};

struct pom

{int a;

 double b;

 char c;

 xx d;

};

     Не е възможно обаче поле на структура да е от тип, съвпадащ с името на структурата.

     Пример: Не е допустима дефиниция от вида

     struct xxx{

         xxx member;        // опит за рекурсивна дефиниция

     };

тъй като компилаторът не може да определи размера на xxx. Допустима е обаче дефиницията:

     struct xxx{

         xxx* member;      

     };

    Допълнение: За да е възможно две дефиниции на структури да се обръщат една към друга, е необходимо пред дефинициите им да се постави декларацията на втората структура. Например, ако искаме дефиницията на структурата list да използва дефиницията на структурата link и обратно, ще трябва да ги подредим по следния начин:

     struct list;       // декларация на втората структура

     struct link{

         link* pred;

         link* succ;

         list* member;

     };

     struct list{

         link* head;

     };

     Дефиницията на структура не предизвиква отделянето на памет за съхраняване на компонентите й. Може да се постави извън функция, в началото на функция или в началото на блок. Местоположението на дефиницията определя областта на името на структурата – съответно за всички функции след дефиницията, в рамките на функцията и в рамките на блока. Най-често дефиницията се задава пред първата функция на програмата и така става достъпна за всички функции.

    

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

 

    Множество от стойности

 

     Множеството от стойностите на една структура се състои от всички крайни редици от по толкова елемента, колкото са полетата й, като всеки елемент е от тип, съвместим с типа на  съответното поле.

 

     Примери:

1. Множеството от стойности на структурата complex се състои от всички двойки от реални числа.

2. Множеството от стойности на структурата book се състои от всички четворки от вида:

{char[41], char[31], int, double}.

3. Множеството от стойности на структурата student се състои от всички тройки от вида:

{int, char[36], double[30]}.

     Променлива величина, множеството от допустимите стойности, на която съвпада с множеството от стойности на дадена структура, е променлива от дадения тип структура. Променлива от тип структура се дефинира в областта на структурата по следния начин (Фиг. 11.2):

 

    Дефиниция на променлива от тип структура

     <дефиниция_на_променлива_от_тип_структура> ::=

[struct]опц <име_на_структура>

        <променлива> [={<редица_от_изрази>}]опц

        {,<променлива> [={<редица_от_изрази>}]опц}опц ;|

struct {<дефиниция_на_полета>;

         {<дефиниция_на_полета>;}опц

             }<променлива> [={<редица_от_изрази>}]опц

       {,<променлива> [={<редица_от_изрази>}]опц}цоп ;

     <променлива> ::= <идентификатор>

     <редеица_от_изрази> ::= <израз>|

                                          <израз>,<редица_от_изрази>

 

Фиг. 11.2 Дефиниция на променлива от тип структура

 

В C++ използването на запазената дума struct не е задължително. Някои програмисти го използват заради яснотата на кода. Конструкцията {<редица_от_изрази>} предизвиква инициализация на дефинирана променлива. Изразите, изредени във фигурните скоби, се разделят със запетая. Всеки израз инициализира поле на структурата и трябва да е от тип, съвместим с типа на съответното поле. Ако са записани по-малко изрази от броя на полетата, компилаторът допълва останалите с нулевите стойности за съответния тип на поле.

     Примери:

     complex z1, z2 = {5.6, -8.3}, z3;

     book b1, b2, b3;

     struct student s1 = {44505, “Ivan Ivanov”, {5.5, 6, 5, 6}};

struct

{int x;

    double y;

   } p, q = {-2, -1.6};

pom y;

Последната дефиниция от примера по-горе дефинира променливите p и q като променливи от тип структурата

struct

{int x;

    double y;

   }

която участва с дефиницията, а не с името си.

     Достъпът до полетата на структура е пряк. Един начин за неговото осъществяване е чрез променлива от тип структурата, като променливата и името на полето на структурата се разделят с оператора точка (Фиг. 11.3). Получените конструкции са променливи от типа на полето и се наричат полета на променливата от тип структура или член-данни на структурата, свързани с променливата.

     Операторът . е лявоасоциативен и има приоритет равен на този на () и [].

 

Поле на структура

      <поле_на_променлива_структура> ::=

              <променлива_структура>.<име_на_поле>

 

Фиг. 11.3 Поле на структура

 

Примери:

С променливите z1, z2, z3 се свързват променливите от тип double:

     z1.re, z1.im, z2.re, z2.im, z3.re и z3.im

а с b1, s1 и  y –

  1. b1.name    – от тип char [41],    b1.author – от тип char [31],
  2. b1.year    – от тип int,                   b1.price – от тип double,
  3. s1.facnum – от тип int,                s1.name  - от тип char [36],
  4. s1.marks  – от тип double [30],    y.a      – от тип int,
  5. y.b        – от тип double,           y.c      – от тип char,
  6. y.d        –    от тип xx и

с полета y.d.a, y.d.b и y.d.c от тип int.

 

Дефинирането на променлива от тип структура свързва променливата с множеството от стойности на съответната структура. След дефинициите от примера по-горе, променливите z1, z2 и z3 се свързват с множеството от стойностите на типа complex. При това свързване z2 е свързано с комплексното число 5.6–i8.3 чрез инициализация. Свързването на z1 и z3 със съответни комплексни числа може да стане чрез инициализация, подобно на z2, чрез присвояване, например z1 = z2;, или чрез задаване на стойности на полетата на променливата, например

     z3.re = 3.4;

     z3.im = -30.5;

свързва z3 с комплексното число 3.4 –i30.5.

Освен това, дефинирането на променлива от тип структура предизвиква отделяне на определено количество памет за всяко поле на променливата. Последното се определя от типа на полето. Полетата се разполагат последователно в паметта. Обикновено всяко поле се разполага от началото на машинна дума. Полетата, които не изискват цяло число на брой машинни думи, не използват напълно отредената им памет. Този начин за подреждане на полетата на структура се нарича изравняване до границата на машинна дума. За реализацията Visual C++ 6.0 размерът на 1 машинна дума е 8B.

Пример: За променливите p и q, дефинирани по-горе, ще се отделят по 16, а не по 12 байта

ОП

  1. p.x        p.y      q.x      q.y

 -              -            -2            -1.6

8B              8B            8B            8B

     Допълнение: Две структури, дефинирани по един и същ начин, са различни. Например, дефинициите

     struct str1{

         int a;

         int b;

     };

и

struct str2{

         int a;

         int b;

     };

определят два различни типа. Дефинициите

     str1 x;

     str2 y = x;

предизвикват грешка заради смесване на типовете (x и y са от различни типове).

 

Операции и вградени функции

 

Операциите над структури зависят от реализацията на езика. По стандарт за всяка реализация са определени следните операции и вградени функции:

 

а) над полетата на променливи от тип структура

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

 

б) над променливи от тип структура

- Възможно е на променлива от тип структура да се присвои стойността на вече инициализирана променлива от същия тип структура или стойността на израз от същия тип.

Пример: Допустими са присвояванията

z3 = z2;

p = q;

- Възможно е формален параметър на функция, а също резултатът от изпълнението й, да са структури. Структури с големи размери обикновено се предават чрез указатели или псевдоними на структури. Така се спестяват ресурси. Освен това, тези начини за предаване са по-сигурни.

 

Ще илюстрираме използването на структурите чрез следния пример.

 

Задача 100. Да се напише програма, която:

а) въвежда факултетните номера, имената и оценките по 5 предмета на студентите от една група;

б) извежда в табличен вид въведените данни;

в) сортира в низходящ ред по среден успех данните;

г) извежда сортираните данни, като за всеки студент извежда и средния му успех.

 

Програма Zad100.cpp решава задачата. Данните за студент се дефинират чрез структурата:

struct student

{int facnom;

 char name[26];

 double marks[NUM];

};

където NUM е константа, определяща броя на предметите. Данните за групата са представени чрез масива

     student table[30];

     Реализирани са следните процедури и функции:

void read_student(student&);

     Въвежда стойности на полетата на структура от тип student.

void print_student(const student &);

Извежда върху екрана полетата на структура от тип student.

void sorttable(int n, student[]);

Сортира компонентите на масив от структури от тип student в низходящ ред по среден успех. Резултатът от сортирането е в същия масив от структури.

double average(double*);

Намира средно-аритметичното на елементите на масив от NUM реални числа.

 

// Program Zad100.cpp

#include <iostream.h>

#include <iomanip.h>

#include <string.h>

const NUM = 5;

struct student

{int facnom;

 char name[26];

 double marks[NUM];

};

void read_student(student&);

void print_student(const student&);

void sorttable(int n, student[]);

double average(double*);

int main()

{cout << setprecision(2) << setiosflags(ios::fixed);

 student table[30];

 int n;

 do

 {cout << „number of students? „;

  cin >> n;

 } while (n < 1 || n > 30);

 int i;

 for (i = 0; i <= n-1; i++)

   read_student(table[i]);

 cout << „Table: \n“;

 for (i = 0; i <= n-1; i++)

 {print_student(table[i]);

  cout << endl;

 }

 sorttable(n, table);

 cout << „\n New Table: \n“;

 for (i = 0; i <= n-1; i++)

 {print_student(table[i]);

  cout << setw(7) << average(table[i].marks) << endl;

 }

 return 0;

}

void read_student(student& s)

{cout << „fak. nomer: „;

 cin >> s.facnom;

 char p[100];

 cin.getline(p, 100);

 cout << „name: „;

 cin.getline(s.name, 40);

 for (int i = 0; i <= NUM-1; i++)

 {cout << i << “ -th mark: „;

  cin >> s.marks[i];

 }

}

void print_student(const student& stud)

{cout << setw(6) << stud.facnom << setw(30) << stud.name;

 for (int i = 0; i <= NUM-1; i++)

 cout << setw(6) << stud.marks[i];

}

void sorttable(int n, student a[])

{for (int i = 0; i <= n-2; i++)

 {int k = i;

  double max = average(a[i].marks);

  for (int j = i+1; j <= n-1; j++)

   if (average(a[j].marks) > max)

   {max = average(a[j].marks);

    k = j;

   }

  student x = a[i]; a[i] = a[k]; a[k] = x;

 }

}

double average(double* a)

{double s = 0;

 for (int j = 0; j <= NUM-1; j++)

   s += a[j];

 return s/NUM;

}

Процедурата за сортиране void sorttable(int n, student a[]) е реализирана неефективно тъй като се разместват структури. При структури с големи размери сортирането е много бавно. Реализацията може да се подобри като се създаде масив от указатели към структурите – елементи на table. При необходимост от размяна, тя се осъществява не със структурите, а с адресите на съответните им указатели.

 

11.3 Указатели към структури

 

Дефинират се по общоприетия начин (фиг. 11.4).

 

Указател към структура

<указател_към_структура> ::=

struct <име_на_структура> * <променлива_указател>

                                    [= & <променлива>]опц;

където

<променлива> е от тип <име_на_структура>.

 

Фиг. 11.4 Указател към структура

 

     В C++ запазената дума struct може да се пропусне.

Пример:

     student st1, st2;

     …

     student *pst = &st1;

     …

     pst = &st2;

     …

     В резултат за променливата-указател pst се отделят 4B ОП, в които отначало се записва адресът на st1, след което – адресът на st2.

 

     Достъпът до полетата на променлива от тип структура чрез указател към нея се осъществява чрез обръщението:

     (*<променлива_указател>).<име_на_поле>

което е еквивалентно на

     <променлива_указател> -> <име_на_поле>

За разделител са използвани знаците – и >, записани последователно.

     Пример: Достъпът до полетата на st2 чрез указателя pst се реализира чрез обръщенията:

     pst -> facnom

pst -> name

pst -> marks

 

Задача 101. Да се модифицира функцията за сортиране sorttable от задача 100, като за сортирането се използва помощен масив от указатели към структурата student.

 

Пред вид промени и в main ще дадем програмен фрагмент, решаващ задачата. Функциите read_student(), print_student() и average() са пропуснати, тъй като са същите като в задача 100.

// Program Zad101.cpp

#include <iostream.h>

#include <iomanip.h>

#include <string.h>

const NUM = 5;

struct student

{int facnom;

 char name[26];

 double marks[NUM];

};

void read_student(student&);

void print_student(const student&);

void sorttable(int n, student* []);

double average(double*);

int main()

{cout << setprecision(2) << setiosflags(ios::fixed);

 student table[30];

 student* ptable[30];

 int n;

 do

 {cout << „number of students? „;

  cin >> n;

 } while (n < 1 || n > 30);

 int i;

 for (i = 0; i <= n-1; i++)

 {read_student(table[i]);

  ptable[i] = &table[i];

 }

 cout << „Table: \n“;

 for (i = 0; i <= n-1; i++)

 {print_student(table[i]);

  cout << endl;

 }

 sorttable(n, ptable);

 cout << „\n New Table: \n“;

 for (i = 0; i <= n-1; i++)

 {print_student(*ptable[i]);

  cout << setw(7) << average(ptable[i]->marks) << endl;

 }

 return 0;

}

void sorttable(int n, student* a[])

{for (int i = 0; i <= n-2; i++)

 {int k = i;

  double max = average(a[i]->marks);

  for (int j = i+1; j <= n-1; j++)

   if (average(a[j]->marks) > max)

   {max = average(a[j]->marks);

    k = j;

   }

  student* x;

  x = a[i]; a[i] = a[k]; a[k] = x;

 }

}

     …

 

     За работа със структури от данни се използва подходът абстракция със структури от данни.

 

11.4 Абстракция със структури от данни

 

При този подход методите за използване на данните са разделени от методите за тяхното представяне. Програмите се конструират така, че да работят с “абстрактни данни” – данни с неуточнено представяне. След това представянето се конкретизира с помощта на множество функции, наречени конструктори, селектори и предикати, които реализират “абстрактните данни” по конкретен начин.

Ще го илюстрираме чрез следната задача.

 

Задача 102. Да се напише програма, която реализира основните рационално-числови операции – събиране, изваждане, умножение и деление на рационални числа.

 

Програма Zad102.cpp решава задачата. Тя дефинира функции за събиране, изваждане, умножение и деление на рационални числа като реализира следните общоизвестни правила:

 

 
   

 

 

Тези операции лесно могат да се реализират ако има начин за конструиране на рационално число по зададени две цели числа, представящи съответно неговите числител и знаменател и ако има начини, които по дадено рационално число извличат неговите числител и знаменател. Затова в програмата Zad102.cpp са дефинирани функциите:

    

void makerat(rat& r, int a, int b)– която конструира рационално

число r по дадени числител a и

знаменател b;

int numer(rat& r)             – която намира числителя на рационалното     

  число r;

int denom(rat& r)      – която намира знаменателя на рационалното

число r,

където с rat означаваме типа рационално число.

Все още не знаем как точно да реализираме тези функции, нито как се представя рационално число, но ако ги имаме, функциите за рационално-числова аритметика и процедурата за извеждане на рационално число могат да се реализират по следния начин:

rat sumrat(rat& r1, rat& r2)          //събира рационални числа

{rat r;

 makerat(r, numer(r1)*denom(r2)+numer(r2)*denom(r1),

             denom(r1)*denom(r2));

 return r;

}

rat subrat(rat& r1, rat& r2)           // изважда рационални числа

{rat r;                                    

 makerat(r, numer(r1)*denom(r2)-numer(r2)*denom(r1),

              denom(r1)*denom(r2));

 return r;

}

rat multrat(rat& r1, rat& r2)           // умножава рационални числа

{rat r;                                         

 makerat(r, numer(r1)*numer(r2),

              denom(r1)*denom(r2));

 return r;

}

rat quotrat(rat& r1, rat& r2)           // дели рационални числа

{rat r;

 makerat(r, numer(r1)*denom(r2),

             denom(r1)*numer(r2));

 return r;

}

void printrat(rat& r)                  // извежда рационално число

{cout << numer(r) << „/“ << denom(r) << ‘\n’;

}

Сега да се върнем към представянето на рационалните числа, а също към реализацията на примитивните операции: конструктора makerat и селекторите numer и denom. Тъй като рационалните числа са частни на две цели числа, удобно представяне на рационално число е структура от вида:

struct rat

{int num, den;

   };

Тогава примитивните функции, реализиращи конструктора makerat и двата селектора numer и denom, имат вида:

void makerat(rat& r, int a, int b)

{r.num = a;

 r.den = b;

}

int numer(rat& r)

{return r.num;

}

int denom(rat& r)

{return r.den;

}

Тези функции са включени в Zad102.cpp и са използвани за намиране на сумата, разликата, произведението и делението на рационалните числа ½ и ¾.

// Program Zad102.cpp

#include <iostream.h>

struct rat

{int num, den;

};

void makerat(rat& r, int a, int b)

{r.num = a;

 r.den = b;

}

int numer(rat& r)

{return r.num;

}

int denom(rat& r)

{return r.den;

}

rat sumrat(rat& r1, rat& r2)

{rat r;

 makerat(r, numer(r1)*denom(r2)+numer(r2)*denom(r1),

             denom(r1)*denom(r2));

 return r;

}

rat subrat(rat& r1, rat& r2)

{rat r;

 makerat(r, numer(r1)*denom(r2)-numer(r2)*denom(r1),

              denom(r1)*denom(r2));

 return r;

}

rat multrat(rat& r1, rat& r2)

{rat r;

 makerat(r, numer(r1)*numer(r2),

              denom(r1)*denom(r2));

 return r;

}

rat quotrat(rat& r1, rat& r2)

{rat r;

 makerat(r, numer(r1)*denom(r2),

             denom(r1)*numer(r2));

 return r;

}

void printrat(rat& r)

{cout << numer(r) << „/“ << denom(r)<< endl;

}

int main()

{rat r1, r2;

 makerat(r1, 1, 2);

 makerat(r2, 3, 4);

 printrat(sumrat(r1, r2));

 printrat(subrat(r1, r2));

 printrat(multrat(r1, r2));

 printrat(quotrat(r1, r2));

 return 0;

}

Реализирането на подхода абстракция със структури от данни в Задача 102, показва следните четири нива на абстракция:

-          Използване на рационалните числа  в проблемна област (във функцията main);

-          Реализиране на правилата за рационално-числова аритметика (sumrat, subrat, multrat, quotrat, printrat);

-          Избор на представяне на рационалните числа и реализиране на примитивни конструктори и селектори (makerat, numer и denom);

-          Работа на ниво структура.

Използването на подхода прави програмите по-лесни за описание и модификация. Ако разгледаме по-внимателно изпълнението на горната програма, забелязваме, че тя има редица недостатъци, но основният е, че не съкращава рационални числа. За да поправим този недостатък, се налага да променим единствено функцията makerat. За целта ще използваме помощната функция gcd, дефинирана в глава 8. Новата makerat има вида:

void makerat(rat& r, int a, int b)

{if (a == 0) {r.num = 0; r.den = b;}

 else

 {int g = gcd(abs(a), abs(b));

  if (a > 0 && b > 0 || a < 0 && b < 0)

  {r.num = abs(a)/g; r.den = abs(b)/g;}

  else {r.num = – abs(a)/g; r.den = abs(b)/g;}

 }

}

С това проблемът със съкращаването на рационални числа е решен. За разрешаването му се наложи малка модификация на програмата, която засегна само премитивния конструктор makerat. Последното илюстрира лесната модифицируемост на програмите, реализиращи горния подход.

 

11.       5 От структури към класове

 

     В тази задача дефинирахме структурата rat, определяща рационални числа, и реализирахме някой основни функции за работа с такива числа. Възниква въпросът: Може ли да използваме тази структура като тип данни рационално число? Отговорът е не, защото при типовете данни представянето на данните е скрито за потребителя. На последния са известни множеството от стойности и операциите и вградените функции, допустими за типа. Така възниква усещането, че трябва да се обедини представянето на рационално число като запис с две полета с примитивните операции (makerat, numer и denom), пряко използващи представянето. Последното е възможно, тъй като в езика C++ се допуска полетата на структура да са функции, разбира се от тип, различен от типа на структурата.

     В следващото описание примитивните операции, реализирани чрез функциите makerat, numer и denom, а също функцията за извеждане на рационално число, ще направим полета на структутара rat. За целта реализираме следните две стъпки:

∙ Включване във фигурните скоби на дефиницията на структурата rat на декларациите:

void makerat(rat& r, int a, int b);

int numer(rat& r);

int denom(rat& r);

void printrat(rat& r);

от които елеминираме участието на формалния параметър rat& r, тъй като неговата функция ще се изпълни от полетата num и den. Получаваме структурата:

struct rat

{int num;

 int den;

 void makerat(int a, int b);

 int numer();

 int denom();

 void printrat();

};

която може да се интерпретира като запис с две полета num и den, над които могат да се изпълняват функциите:

-          makerat, която конструира рационалното число a/b чрез свързването на num и den с a и b съответно;

-          numer, която намира числителя num на рационалното число num/den;

-          denom, която намира знаменателя den на рационалното число num/den;

-          printrat, която извежда рационалното число num/den.

∙ Отразяване на тези промени в дефинициите на функциите. За целта ще изтрием участията на rat& r и r., а между типа и името на всяка функция ще поставим името на структурата rat, следвано от оператора ::.

Получаваме:

void rat::makerat(int a, int b)

{if (a == 0) {num = 0; den = b;}

 else

 {int g = gcd(abs(a), abs(b));

  if (a > 0 && b > 0 || a < 0 && b < 0)

  {num = abs(a)/g;

   den = abs(b)/g;

  }

  else

{num = – abs(a)/g;

 den = abs(b)/g;

}

 }

}

int rat::numer()

{return num;

}

int rat::denom()

{return den;

}

void rat::printrat()

{cout << num << „/“ << den << endl;

}

     Тези функции се наричат член-функции на структурата rat. Извикването им се осъществява като полета на структура.

Пример:

rat r;                  // дефиниция на променлива от тип rat

  1.      r.makerat(1, 5)         // r се свързва с рационалното число 1/5
  2.      r.numer()               // намира числителя на r, в случая 1
  3.      r.denom()               // връща знаменателя на r, в случая 5
  4.      r.printrat()            // извежда върху екрана r.

 

     Обръщението r.numer() е еквивалентно на изпълнение на оператора

     return r.num;

 

     Програма Zad102_1.cpp реализира последните промени.

    

     Program Zad102_1.cpp

#include <iostream.h>

#include <math.h>

struct rat

{int num;

 int den;

 void makerat(int, int);

 int numer();

 int denom();

 void printrat();

};

void rat::printrat()

{cout << num << „/“ << den << endl;

}

int gcd(int a, int b)

{while (a!=b)

 if (a > b) a = a-b; else b = b-a;

 return a;

}

void rat::makerat(int a, int b)

{if (a == 0) {num = 0; den = b;}

 else

 {int g = gcd(abs(a), abs(b));

  if (a>0 && b>0 || a<0 && b < 0)

  {num = abs(a)/g;

   den = abs(b)/g;

  }

  else

  {num = – abs(a)/g;

   den = abs(b)/g;

  }

 }

}

int rat::numer()

{return num;

}

int rat::denom()

{return den;

}

rat sumrat(rat& r1, rat& r2)

{rat r; r.makerat(r1.numer() * r2.denom() +

                      r2.numer() * r1.denom(),

                      r1.denom() * r2.denom());

 return r;

}

rat subrat(rat& r1, rat& r2)

{rat r; r.makerat(r1.numer() * r2.denom() -

                      r2.numer() * r1.denom(),

                      r1.denom() * r2.denom());

 return r;

}

rat multrat(rat& r1, rat& r2)

{rat r; r.makerat(r1.numer()*r2.numer(),

                      r1.denom()*r2.denom());

 return r;

}

rat quotrat(rat& r1, rat& r2)

{rat r; r.makerat(r1.numer()*r2.denom(),

                      r1.denom()*r2.numer());

 return r;

}

     int main()

{rat r1; r1.makerat(-1,2);

 rat r2; r2.makerat(3,4);

 sumrat(r1, r2).printrat();

 // или rat r = sumrat(r1, r2); r.print();

 subrat(r1, r2).printrat();

 // или r = subrat(r1, r2); r.printrat();

 multrat(r1, r2).printrat();

 // или r = multrat(r1, r2); r.printrat();

 quotrat(r1, r2).printrat();

 // или r = quotrat(r1, r2); r.printrat();

 return 0;

}

Забелязваме, че във функциите sumrat, subrat, multrat и quotrat не се използват полетата на записа num и den, но ако направим опит за използването им даже на ниво main, опитът ще бъде успешен. Последното може да се забрани, ако се използва етикетите private: пред дефиницията на полетата num и den и public: пред декларациите на член-функциите. Структурата rat получава вида:

struct rat

{private:

 int num;

 int den;

 public:

 void makerat(int, int);

 int numer();

 int denom();

 void printrat();

};

Опитът за използването на полетата num и den на структурата rat извън член-функциите вече ще предизвиква грешка.

Етикетите private и public се наричат спецификатори за достъп. Всички член-данни, следващи спецификатора за достъп private, са достъпни само за член-функциите от дефиницията на структурата. Всички член-данни и член-функции, следващи спецификатора за достъп public, са достъпни за всяка функция, която е в областта на структурата. Ако специфитаторите за достъп с пропуснати, всички членове са public. Един и същ спецификатор за достъп може да се използва повече от веднъж в една и съща дефиниция на структура.

     Така специфицирането на num и den като private прави невъзможно използването им извън член-функциите makerat, numer, denom и printrat.

     Ако заменим запазената дума struct със class, последната програма не променя поведението си. Така дефинирахме първия си клас с име rat, създадохме два негови обекта – рационалните числа r1 и r2 и работихме с тях.

    

     Задача 103. Като се използва подходът абстракция със структури от данни да се даде ново представяне на задача 100.

 

     // Program Zad103

#include <iostream.h>

#include <iomanip.h>

#include <string.h>

const NUM = 5;

class student

{private:

 int facnom;

 char name[26];

 public:

 double marks[5];

 void read_student();

 void print_student();

};

void sorttable(int n, student[]);

double average(double*);

int main()

{cout << setprecision(2) << setiosflags(ios::fixed);

 student table[30];

 int n;

 do

 {cout << „number of students? „;

  cin >> n;

 }while (n < 1 || n > 30);

 int i;

 for (i = 0; i <= n-1; i++)

   table[i].read_student();

 cout << „table: \n“;

 for (i = 0; i <= n-1; i++)

 {table[i].print_student();

  cout << endl;

 }

 sorttable(n, table);

 cout << „\n New Table: \n“;

 for (i = 0; i <= n-1; i++)

 {table[i].print_student();

  cout << setw(7) << average(table[i].marks) << endl;

     }

 return 0;

}

void student::read_student()

{cout << „fak. nomer: „;

 cin >> facnom;

 char p[100];

 cin.getline(p, 100);

 cout << „name: „;

 cin.getline(name, 40);

 for (int i = 0; i <= NUM-1; i++)

 {cout << i << “ -th mark: „;

  cin >> marks[i];

 }

}

void student::print_student()

{cout << setw(6) << facnom << setw(30) << name;

 for (int i = 0; i <= NUM-1; i++)

 cout << setw(6) << marks[i];

}

void sorttable(int n, student a[])

{for (int i = 0; i <= n-2; i++)

 {int k = i;

  double max = average(a[i].marks);

  for (int j = i+1; j <= n-1; j++)

   if (average(a[j].marks) > max)

   {max = average(a[j].marks);

    k = j;

   }

  student x = a[i]; a[i] = a[k]; a[k] = x;

 }

}

double average(double* a)

{double s = 0;

 for (int j = 0; j <= NUM-1; j++)

   s += a[j];

 return s/NUM;

}

 

     Тези идеи са в основата на нов подход за програмиране – обектно – ориентирания.

 

 

Задачи

 

Задача 1. Да се напише функция, която намира разстоянието между две точки в равнината. Като се използва тази функция, да се напише програма, която въвежда координатите на n точки от равнината, намира и извежда най-голямото разстояние между тях. За целта да се дефинира структура, определяща точка от равнината с координати (x, y).

     Задача 2. Да се напише програма, която въвежда факултетните номера, имената и успеха по k предмета на студентите от една група и извежда следната таблица:

     N    име      предмет1      …  предметK      среден успех

  ==============================================================

     .    .          .              .              .

     .    .          .              .              .

     .    .          .              .              .

  ==============================================================

                      ср. успех  … ср. успех         ср. успех

     Задача 3. Да се напише:

     а) булева функция equal(rat x, rat y), която установява дали рационалните числа x и y са равни.

в) функция maxrat(int n, rat x[]), която намира най-голямото от рационалните числа на масива x.

г) функция sortrat(int n, rat x[]), която сортира елементите на редицата от рационални числа x0, x1, …, xn-1.

Задача 4. Да се напише програма, която решава системата уравнения

         a x + b y = e

         c x + d y = f

където коефициентите a, b, c, d, e, f, а също и неизвестните x и y са рационални числа.

     Задача 5. Нека a0, a1, …, an-1  и x са  рационални  числа. Да се напише функция, която намира стойността на полинома

     P(x) = a0xn + a1xn-1 + … + an.

     Задача 6. Да се дефинира структура, определяща точка от равнината с координати (x, y), където x и y приемат за стойности числата от 1 до 100. Да се напише програма, която чете координатите на четири точки, представляващи върховете A, B, C и D на четириъгълник в цикличен ред и определя дали ABCD е квадрат, правоъгълник или друга фигура.

     Задача 7. Да се напише програма, която решава системата уравнения

         a x + b y = e

         c x + d y = f

където коефициентите a, b, c, d, e, f, а също и неизвестните x и y са комплексни числа.

     Задача 8. Нека a0, a1, …, an-1 и x са комплексни числа. Да се напише функция, която намира стойността на полинома

     P(x) = a0xn + a1xn-1 + … + an.

 

 
   

     Задача 9. Дадени са естественото число n и комплексното число z. Да се напише програма, която пресмята стойността на следната комплексна функция:

 

 

 

Допълнителна литература

 

  1. B. Stroustrup, C++ Programming Language. Third Edition, Addison – Wesley, 1997.
  2. Ал Стивънс, Кл. Уолнъм, C++ библия, АЛЕКС СОФТ, София, 2000.
  3. М. Тодорова, Езици за функционално и логическо програмиране – функционално програмиране, СОФТЕХ, София, 1998.
  4. Ст. Липман, Езикът C++ в примери, КОЛХИДА ТРЕЙД КООП, София, 1993.
  5. Ч. Сфар, Visual C++ 6.0, том 1, СОФТПРЕС, София 2000.

→ Leave a CommentКатегории: Програмиране

Девета част

19/05/2009 · Вашият коментар

 

9

 

Функции от по-висок ред

 

 

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

В езика C++ е възможно формален параметър на функция да е указател към функция, а също е възможно резултатът от изпълнението на функция да е указател към функция. Това позволява да се реализират функции от по-висок ред, а също и такива, които връщат функция.

 

9.1 Указател към функция

 

Името на функция е константен указател, сочещ към първата машинна инструкция от изпълнимия й машинен код. В езика C++ е възможно да се дефинират променливи, които са указатели към функции (Фиг. 9.1).

 

Дефиниция на указател към функция

<дефиниция_на_променлива_указател_към_функция> ::=

<тип_на_функция>(*<указател_към_функция>)(<формални_параметри>)

                                 [= <име_на_функция>]опц;

където

     – <указател_към_функция> е идентификатор;

- <име_на_функция> e идентификатор, означаващ име на функция от тип <тип_на_функция> и параметри – <формални_параметри>;

- <тип_на_функция> и <формални_параметри> се дефинират аналогично на съответните от заглавието на дефиниция функция. Имената на параметрите могат да се пропуснат.

Фиг. 9.1 Дефиниция на указател към функция

 

     Забележка: Скобите, ограждащи *<указател_към_функция>, са задължителни. В противен случай дефиницията ще се изтълкува от компилатора като декларация на функция с име < указател_към_функция>, с параметри – <формални_параметри> и тип – указател към <тип_на_функция>.

     В резултат на дефиницията на променлива от тип указател към функция, за променливата се отделят 4B ОП, която е с неопределена стойност, ако дефиницията е без инициализация, и съдържа адреса на първата машинна команда от изпълнимия код на функцията, чрез която е направена инициализацията, ако дефиницията е с инициализация.

Примери:

  1. double (*p)(double, double);

е дефиниция на променлива p от тип указател към функция от тип double с два аргумента също от тип double. В резултат за p се отделят 4B ОП, които са с неопределена стойност.

  1. int (*q)(int, int*);

дефинира променлива q от тип указател към функция от тип int, с два аргумента, единият от които цял, а другият – указател към int. За q се отделят 4B ОП, които са с неопределена стойност.

Нека са дефинирани следните функции за сортиране на числови редици:

void bubblesort(int, int*);   // метод на мехурчето

void mergesort(int, int*);         // сортиране чрез сливане

void heapsort(int, int*); // пирамидално сортиране

Променливата r може да е указател към тези функции ако е дефинирана по следния начин:

     void (*r)(int, int*);

r не може да е указател към функциите:

     int f1(int, int*);

     int f2(int, int*);

Указател към последните може да е променливата s, където:

     int (*s)(int, int*);

Горните дефиниции на p, q, r и s са без инициализации.

Дефиниците на променливите x и y

     void (*x)(int, int*) = bubbesort;

     int (*y)(int, int*) = f2;

са с инициализация. За всяка от тях се отделят 4B ОП, в която памет се записват адресите на първите команди на изпълнимите кодове на bubblesort и f2 съответно.

     На променлива от тип указател към функция може да се присвои името на функция от същия тип. Присвояването се извършва по общоприетия начин.

Пример: Допустими са присвояванията:

     r = mergesort;

     x = heapsort;

Обръщението към функция освен директно може да се осъществява и индиректно – чрез указател към нея. След инициализация на променлива от тип указател към функция, чрез променливата може да се осъществи обръщение към конкретна функция. Така се предоставя ефективен способ за предаване на управлението към потребителски и библиотечни функции.

     Пример:

     void (*r)(int, int*) = bubblesort;

     bubblesort(n, a);  // директно обръщение

     (*r)(n, a);                  // индиректно обръщение (чрез r).

    Забележка: Някой компилатори, в това число и на Visual C++ 6.0, допускат извикването на функция чрез указател да се осъществява и само чрез името на указателя.

     Пример: Ако

     void (*r)(int, int*) = bubblesort;

Обръщението към bubblesort

     (*r)(n, a);

може да се запише и по следния начин: r(n, a);                     

 

9.2 Функциите като формални параметри

 

     Указател към функция може да е формален параметър на функция. Ще илюстрираме тази възможност с няколко примери.

 

 

 
   

     Задача 80. Да се напише функция, която реализира математическата абстракция:

 

където a и b са дадени реални числа (a ≤ b), f е реална едноаргументна функция, задаваща терма, а next е реална едноаргументна функция, задаваща стъпката за промяна на управляващия параметър на сумата.

 

Преди да решим задачата в общияй вид ще предложим няколко частни решения.

а) Да се дефинира функция, която намира стойността на сумата:

  sin(a) + sin(a+1) + sin(a+2) + … + sin(b),

където a и b са дадени реални числа.

Функцията sum_sin намира тази сума.

double sum_sin(double a, double b)

{double s = 0;

 for (double i = a; i <= b + 1E-14; i = i + 1)

   s = s + sin(i);

 return s;

}

б) Да се дефинира функция, която намира стойността на сумата:

  cos(a) + cos(a + 0.2) + cos(a + 0.4) + … + sin(b)

където a и b са дадени реални числа.

Функцията sum_cos намира тази сума.

double sum_cos(double a, double b)

{double s = 0;

 for (double i = a; i <= b + 1e-14; i = i + 0.2)

    s = s + cos(i);

 return s;

}

Забелязваме, че тези две функции се “приличат”. Написани са по следния общ шаблон:

double <name>(double a, double b)

{double s = 0;

 for (double i = a; i <= b + 1e-14; i = <next>(i))

   s = s + <f>(i);

 return s;

}

Елементите, по които функциите sum_sin и sum_cos се различават са означени с <…> в шаблона. Това са две функции: f, означаваща терма и next – стъпката на сумата. Като използваме възможността формален параметър на функция да е указател към функция, можем да изнесем <f> и <next> като формални параметри на функцията и да обобщим тези частни случаи. Така стигаме до функцията sum:

// Function Zad80

double sum(double a, double b, double (*f)(double),

                double (*next)(double))

{double s = 0;

 for (double i = a; i <= b + 1е-14; i = next(i))

   s = s + f(i);

 return s;

}

Обръщенията към sum:

     sum(a, b, sin, next1)

     sum(a, b, cos, next2)

където

int next1(double x)

{return x + 1;

}

int next2(double x)

{return x + 0.2;

}

реализират горните частни случаи.

sum е функция от по-висок ред. В нея третият и четвъртият параметри са указатели към функции.

     Като използваме sum, може да дефинираме sum_sin и sum_cos по следния начин:

double sum_sin(double a, double b)

{return sum(a, b, sin, next1);

}

double sum_cos(double a, double b)

{return sum(a, b, cos, next2);

}

 

 

 
   

    Задача 81. Да се напише функция, която реализира математическата абстракция:

 

където a и b са реални числа, f е реална едноаргументна функция, задаваща терма, а next – реална едноаргументна функция, задаваща стъпката за промяна на управляващия параметър на произведението. Да се включи тази функция в програма и се намерят:

tg(1) * tg(1.5) * tg(2) * tg(2.5) * tg(3)

и

arctg(1) * arctg(1.1) * arctg(1.2) * arctg(1.3).

 

     Програма Zad81.cpp решава задачата. Функцията prod от нея се реализира чрез последователно преминаване през стъпки, аналигични на тези от задача 80.

     // Program Zad81.cpp

#include <iostream.h>

#include <math.h>

double prod(double, double, double (*)(double),

double (*) (double));

double next1(double);

double next2(double);

int main()

{cout << prod(1, 3, tan, next1) << ‘\n’;

 cout << prod(1, 1.3, atan, next2) << ‘\n’;

 return 0;

}

double prod(double a, double b, double (*f)(double),

   double (*next)(double))

{double s = 1.0;

 for (double i = a; i <= b + 1e-14; i = next(i))

   s = s * f(i);

 return s;

}

double next1(double x)

{return x + 0.5;

}

double next2(double x)

{return x + 0.1;

}

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

prod(1, 3, tan, next1)

и

prod(1, 1.3, atan, next2),

намиращи търсените произведения.

     Забелязваме, че функциите sum и prod си “приличат”. Написани са по следния общ шаблон.

double <name>(double a, double b, double (*f)(double),

  double (*next)(double))

{double s = <null_val>;

 for (double i = a; i <= b + 1е-14; i = next(i))

   s = s <op> f(i);

 return s;

}

И в този случай, елементите, по които sum и prod се различават са оградени с <…>. Това са операцията op и нулата на операцията – null_val. Отново ще изнесем op и null_val като формални параметри на функцията. Тъй като op е бинарна инфиксна операция, а не име на функция, ще дефинираме помощна реална функция с име op, с два реални параметъра и връщаща резултата от прилагането на операцията op към аргументите на функцията op. Така получаваме още едно обобщение на горните абстракции – функцията от по-висок ред accumulate (задача 82).

 

 

 
   

Задача 82. Да се напише програма, която реализира следната математическа абстракция:

 

където с Ä е означена произволна бинарна целочислена операция, а f и  next имат смисъла, определен в предходните две задачи.

 

     Програма Zad82.cpp решава задачата.

// Program Zad82.cpp

#include <iostream.h>

#include <math.h>

double accumulate(double (*) (double, double),

                     double, double, double,

                         double (*)(double), double (*) (double));

double plus(double, double);

double mult(double, double);

double next1(double);

double next2(double);

int main()

{cout << „a, b= „;

 double a, b;

 cin >> a >> b;

 if (!cin)

 {cout << „Error! \n“;

  return 1;

 }

 cout << accumulate(plus, 0, a, b, cos, next1) << ‘\n’;

 cout << accumulate(mult, 1, a, b, sin, next2) << ‘\n’;

 return 0;

}

 

double accumulate(double (*op)(double, double),

                      double null_val, double a, double b,

                      double (*f)(double), double (*next)(double))

{double s = null_val;

 for (double i = a; i <= b +1e-14; i = next(i))

   s = op(s, f(i));

 return s;

}

double next1(double x)

{return x + 1;

}

double next2(double x)

{return x + 2;

}

double plus(double x, double y)

{return x + y;

}

double mult(double x, double y)

{return x * y;

}

Функцията accumulate е функция от по-висок ред. Нейните първи, пети и шести формални параметри са указатели към функции, задаващи съответно операцията op, терма f и стъпката next.

В тази програма са направени две обръщения към функцията accumulate, които намират:

cos(a) + cos(a+1) + cos(a+2) + … + cos(b)

и

sin(a) * sin(a+2) *  sin(a+4) * … * sin(b)

съответно.

Чрез accumulate, функциите sum и prod могат да се дефинират по следния начин:

double sum(double a, double b, double (*f)(double),

            double (*next)(double))

{return accumulate(plus, 0, a, b, f, next);

}

double prod(double a, double b, double (*f)(double),

             double (*next)(double))

{return accumulate(mult, 1, a, b, f, next);

}

където plus и mult са дефинирани в програма Zad82.cpp.

     Използването на променливи, които са указатели към функции, усложнява записа на дефиницията на функция. Добре би било да дадем имена на типовете указател към функция и вместо дефиницията да използваме името на типа. Задаването на имена на типове може да се осъществи чрез оператора typedef. На Фиг. 9.2 са дадени синтаксисът и семантиката на този оператор.

 

    Оператор typedef

     Синтаксис

     typedef <тип> <име>;

където

     – <тип> е дефиниция на тип;

     – <име> е идентификатор, определящ името на новия тип.

     Семантика

     Определя <име> за синоним на типа от <тип>.

 
Фиг. 9.2 Оператор typedef

 

     Примери:

     typedef unsigned char BYTE; // BYTE е синоним на unsigned char

     typedef double REAL;  // REAL е синоним на double

     Задаването на алтернативно име на тип указател към функция чрез typedef се осъществява по аналогичен начин на дефиниране на променлива от тип указател към функция като новото име на типа заема мястото на променливата.

     Примери:

  1. typedef double(*mytype)(double);

определя mytype като синоним на типа double (*)(double);

  1. typedef double(*newtype)(double, double);

определя newtype като синоним на типа double (*)(double, double).

     Като използваме оператора typedef и дефинираме:

     typedef double (*type1) (double, double);

     typedef double (*type2) (double);

декларацията на функцията accumulate ot Zad82.cpp:

double accumulate(double (*) (double, double),

                     double, double, double,

                                 double (*)(double), double (*) (double));

може да се запише по следния начин:

double accumulate(type1, double, double, double, type2, type2);

 

 

 
   

Задача 83. Като се направи подходяща модификация на accumulate, да се напише програма, която по дадени естествено число n и реално число x, намира сумата:

 

 

 

 
   

Програма Zad83.cpp решава задачата. В нея a и b са 0 и n съответно. Термът f е:

 

 

 
   

а стъпката се задава от:

 

 

Направена е модификация на функцията accumulate. Последната се налага заради промяната на типовете на a, b, на f и next.

// Program Zad83.cpp

#include <iostream.h>

#include <math.h>

typedef double (*type1) (double, double);

typedef double (*type2)(int);

typedef int (*type3) (int);

double x;

double accumulate(type1, double, int, int, type2, type3);

double f(int);

int next(int);

double sum(double, double);

int main()

{cout << „n= „;

 int n;

 cin >> n;

 if (!cin || n < 0)

 {cout << „Error! \n“;

  return 1;

 }

 cout << „x= „;

 cin >> x;

 if (!cin)

 {cout << „Error! \n“;

  return 1;

 }

 cout << accumulate(sum, 0, 0, n, f, next) << ‘\n’;

 return 0;

}

double f(int i)

{int p = 1;

 for (int j = 1; j <= i; j++) p = p*j;

  return pow(x, i)/p;

}

int next(int x)

{return x + 1;

}

double sum(double x, double y)

{return x + y;

}

double accumulate(type1 op, double null_val,

int a, int b, type2 f, type3 next)

{double s = null_val;

 for (int i = a; i <= b; i = next(i))

   s = op(s, f(i));

 return s;

}

 

9.3 Функциите като върнати оценки

 

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

     int (*fun(int, int))(int*, int);

определя функцията fun с два цели аргумента и връщаща указател към функция от тип

int (*)(int*, int).

Ако зададем име на този тип чрез typedef, т.е.

typedef int (*fun-point)(int*, int);

този запис може да се опрости до:

fun-point fun(int, int);

 

 

 
   

Задача 84. Да се напише програма, която по зададено реално число x и символ (a, b, c или d) избира за изпълнение функция, определена чрез зависимостта:

 

 

     Програма Zad84.cpp решава задачата.

     // Program Zad84.cpp

#include <iostream.h>

#include <math.h>

typedef double (*f_type)(double);

f_type table(char ch)

{switch(ch)

 {case ‘a’: return sin; break;

  case ‘b’: return cos; break;

  case ‘c’: return exp; break;

  case ‘d’: return log; break;

  default: cout << „Error! \n“; return tan;

 }

}

int main()

{char ch;

 cout << „ch= „;

 cin >> ch;

 if (ch < ‘a’ || ch >’d') cout << „Incorrect input! \n“;

 else

 {double x;

  cout << „x= „;

  cin >> x;

  cout << table(ch)(x) << ‘\n’;

 }

 return 0;

}

Илюстрираните в тази част възможности на езика C++ показват, че данните от тип функции съществено не се отличават от другите видове данни. Това показва високата степен на унифицираност в езика и води до увеличаване на изразителната му сила.

 

Задачи

 

 

 
   

Задача 1. Като използвате функциите от по-висок ред sum и prod, намерете:

 

 

 
   

Задача 2. Като използвате функцията от по-висок ред prod, намерете:

 

а) xn, където x е дадено реално, а n – дадено естествено число.

б) n!, където n е дадено естествено число.

в) броят на вариациите от n елемента от k-ти клас (n и k са дадени естествени числа, 0 ≤ k ≤ n).

г) броят на комбинациите от n елемента от k-ти клас (n и k са дадени естествени числа, 0 ≤ k ≤ n).

 

Допълнителна литература

 

1. Ст. Липман, Езикът C++ в примери, “КОЛХИДА ТРЕЙД” КООП, София, 1993.

2. Д. Луис, C/C++ бърз справочник, ИНФОДАР, София, 1998.

3.  М. Тодорова, Езици за функционално и логическо програмиране. Функционално програмиране, СОФТЕХ, София, 1998.

4. B. Stroustrup, C++ Programming Language. Third Edition, Addison – Wesley, 1997.

→ Leave a CommentКатегории: Програмиране

Осма част

19/05/2009 · Вашият коментар

 

 

8

 

Функции

 

 

    Добавянето на нови оператори и функции в приложенията, реализирани на езика C++, се осъществява чрез функциите. Те са основни структурни единици, от които се изграждат програмите на езика. Всяка функция се състои от множество от оператори, оформени подходящо  за да се използват като обобщено действие или операция. След като една функция бъде дефинирана, тя може да бъде изпълнявана многократно за различни входни данни.

     Програмите на езика C++ се състоят от една или повече функции. Сред тях задължително трябва да има точно една с име main и наречена главна функция. Тя е първата функция, която се изпълнява при стартиране на програмата. Главната функция от своя страна може да се обръща към други функции. Нормалното изпълнение на програмата завършва с изпълнението на главната функция (Възможно е изпълнението да завърши принудително с изпълнението на функция, различна от главната).

     Използването на функции има следните предимства:

- Програмите стават ясни и лесни за тестване и модифициране.  

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

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

 

     Ще разгледаме най-общо разпределението на оперативната памет за изпълнима програма на C++. Чрез няколко примерни програми ще покажем дефинирането, обръщението и изпълнението на функции, след което ще направим съответните обобщения.

 

8.1 Разпределение на ОП за изпълнима програма

 

Разпределението на ОП зависи от изчислителната система, от типа на операционната система, а също от модела памет. Най-общо се състои от: програмен код, област на статичните данни, област на динамичните данни и програмен стек (Фиг. 8.1).

 

 

 

Програмен  стек

 

                                            краен адрес на ОП

 

                                      

                                             указател на стека

                                             (запълване в посока

                                   към малките адреси)

      

 

 

 
   

 

 

 

 

 

 

 

 

 

 

                                                    начален адрес на ОП

 

 

Фиг. 8.1 Разпределение на ОП

 

Програмен код

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

     Област на статичните данни

     В нея са записани глобалните обекти (в широкия смисъл на думата) на програмата.

     Област на динамичните данни

За реализиране на динамични структури от данни (списъци, дървета, графи, …) се използват средства за динамично разпределение на паметта. Чрез тях се заделя и освобождава памет в процеса на изпълнение на програмата, а не преди това (при компилирането й). Тази памет е от областта на динамичните данни.

Програмен стек

Този вид памет съхранява данните на функциите на програмата. Стекът е динамична структура, организирана по правилото “последен влязъл – пръв излязъл”. Той е редица от елементи с пряк достъп до елементите от единия си край, наречен връх. Достъпът се реализира чрез указател. Операцията включване се осъществява само пред елемента от върха, а операцията изключване – само за елемента от върха.

Елементите на програмния стек са “блокове” от памет, съхраняващи данни, дефинирани в някаква функция. Наричат се стекови рамки.

 

8.2 Примери за програми, които дефинират и използват функции

 

Задача 68. Да се напише програма, която въвежда стойности на естествените числа a, b, c и d и намира и извежда най-големият общ делител на числата a и b, след това на c и d и накрая на a, b, c и d.

 

Програма Zad68.cpp решава задачата. Тя се състои от две функции: gcd и main. Функцията gcd(x, y) намира най-големия общ делител на естествените числа x и y.  Тъй като main се обръща към (извиква) gcd, функцията gcd трябва да бъде известна преди функцията main. Най-лесният начин да се постигне това е във файла, съдържащ програмата, първо да се постави дефиницията на gcd, а след това тази на main. Ще бъде показан алтернативен начин по-късно.

Описанието на функцията gcd прилича на това на функцията main. Състои се от заглавие

int gcd(int x, int y)

и тяло

{while (x != y)

 if (x > y) x = x-y; else y = y-x;

 return x;

 }

Заглавието определя, че gcd е име на двуаргументна целочислена функция с цели аргументи, т.е.

     gcd: int x int              int

Името е произволен идентификатор. В случая е направен мнемонически избор. Запазената дума int пред името на функцията е типа й (по-точно е типа на резултата на функцията). В кръгли сkоби и отделени със запетая са описани параметрите x и y на gcd. Те са различни идентификатори. Предшестват се от типовете си. Наричат се формални параметри за  функцията.

     Тялото на функцията е блок, реализиращ алгоритъма на Евклид за намиране на най-големия общ делител на естествените числа x и y. Завършва с оператора

return x;

чрез който се прекратява изпълнението на функцията като стойността на израза след return се връща като стойност на gcd в мястото, в случая в main, в което е направено обръщението към нея.

 

// Program Zad68.cpp

#include <iostream.h>

int gcd(int x, int y)

{while (x != y)

 if (x > y) x = x-y; else y = y-x;

 return x;

 }

 int main()

 {cout << „a, b, c, d= „;

  int a, b, c, d;

  cin >> a >> b >> c >> d;

  if (!cin || a < 1 || b < 1 || c < 1 || d < 1)

  {cout << „Error! \n“;

   return 1;

  }

  int r = gcd(a, b);

  cout << „gcd{“ << a << „, “ << b << „}= “ << r << „\n“;

  int s = gcd(c, d);

  cout << „gcd{“ << c << „, “ << d << „}= “ << s << „\n“;

  cout << „gcd{“ << a << „, “ << b << „, “ << c << „, „

        << d << „}= “ << gcd(r, s) << „\n“;

   return 0;

 }

 

     Изпълнение на програма Zad68.cpp

    

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

cout << „a, b, c, d= „;

int a, b, c, d;

cin >> a >> b >> c >> d;

if (!cin || a < 1 || b < 1 || c < 1 || d < 1)

{cout << „Error \n“;

 return 1;

}

дефинира и въвежда стойности на целите променливи a, b, c и d като осигурява да са естествени числа. Нека за a, b, c и d са въведени 14, 21, 42 и 7 съответно. В тази последователност те се записват в дъното на програмния стек (Фиг. 8.2). Така на дъното на стека се оформя “блок” от памет за main с достатъчно големи размери, който освен променливите от main съдържа и някои “вътрешни” данни. Този блок се нарича стекова рамка на main.

     Операторът

int r = gcd(a, b);

дефинира цялата променлива r като в стековата рамка на main, веднага след променливата d отделя 4B, в които ще запише резултатът от обръщението gcd(a, b) към функцията gcd. Променливите a и b се наричат фактически параметри за това обръщение. Забелязваме, че типът им е същия като на съответните им формални параметри x и y.

    

 

 

 

14

21

42

 

7

 

-

 

 

 

 

 

 

 

 

 

BIOS

и DOS

 

                        a                       0×0066FDF4                       a                                              b                      0×0066FDF0

 

                        c                       0×0066FDEC

                        d                       0×0066FDE8              стекова рамка

                        r                       0×0066FDE4              на main

 

 
   

 

 

 

                                                                       указател на стека

             

         main            …  0×0040100F

    

                   gcd                     0×0040100A              програмен код

                                              

 

   
   
 
   

 

 

 

 

 

 

 

 

 Фиг. 8.2 Разпределение на ОП програмата Zad68.cpp

 

Обръщение към gcd(a, b)

В програмния стек се генерира нов блок памет – стекова рамка за функцията gcd. В него се записват формалните и локалните параметри на gcd, а също и някои “вътрешни” данни като return-адреса и адреса на стековата рамка на main. Указателят на стека се премества след тази стекова рамка.

Обръщението се осъществява по следния начин:

а) Свързване на формалните с фактическите параметри

В стековата рамка на gcd, се отделят по 4 байта за формалните параметри x и y в обратен ред на реда, в които са записани в заглавието. В тази памет се откопирват стойностите на съответните им фактически параметри. Отделят се също 4B за т. нар. return-адрес, адреса на мястото в main, където ще се върне резултатът, а също се отделя памет, в която се записва адресът на предишната стекова рамка, т.е.

 

памет за gcd (I-во обръщение към него)

 

 
   

 

 

 

 

14

 

     y                                0×0066FD90

 

x                             0×0066FD8C

                           0×0066FD88

 

                                      return-адрес

 

                                      адрес на предишната

стекова рамка

                                                             указател на стека

     б) Изпълнение на тялото на gcd

Тъй като е в сила y > x, стойността на y се променя на 7, т.е.

  памет в стека за gcd

 

 
   

 

 

 

 

14

 

     y                                0×0066FD90

 

x                             0×0066FD8C

                           0×0066FD88

 

                                      return-адрес

 

                                      адрес на предишната

стекова рамка

                                                         указател на стека

Сега пък е в сила x > y, което води до промяна на стойността на x на 7, т.е.

памет в стека за gcd

 

 
   

 

 

 

 

7

 

     y                                0×0066FD90

 

x                             0×0066FD8C

                           0×0066FD88

                                      return-адрес

                                     

                              адрес на предишната

                                      стекова рамка

               указател на стека

                                                                

Операторът за цикъл завършва изпълнението си. Изпълнението на оператора

return x;

преустановява изпълнението на gcd като връща в main в мястото на прекъсването (return-адреса) стойността 7 на обръщението gcd(a, b). Отделената за gcd стекова рамка се освобождава. Указателят на стека се установява в края на стековата рамка на main. Изпълнението на програмата продължава с инициализацията на r. Резултатът от обръщението gcd(14, 21) се записва в отделената за r памет.

     Операторът

cout << „gcd{“ << a << „, “ << b << „}= “ << r << „\n“;

извежда получения резултат.

     Изпълнението на останалите обръщения към gcd се реализира по същия начин. При обръщението към всяко от тях в стека се създава стекова рамка на gcd, а след завършване на обръщението, рамката се освобождава. При достигане до оператора return 0; от main, се освобождава и стековата рамка на main.

     Функцията gcd реализира най-простото и “чисто” дефиниране и използване на функции – получава входните си стойности единствено чрез формалните си параметри и връща резултата от работата си чрез оператора return. Забелязваме, че обръщението gcd(a, b) работи с копия на стойностите на а и b, запомнени в x и y, а не със самите a и b. В процеса на изпълнение на тялото на gcd, стойностите на x и y се променят, но това не оказва влияние на стойностите на фактическите параметри a и b.

     Такова свързване на формалните с фактическите параметри се нарича свързване по стойност или още предаване на параметрите по стойност. При него фактическите параметри могат да бъдат не само променливи, но и изрази от типове, съвместими с типовете на съответните формални параметри. Обръщението gcd(gcd(a, b), gcd(c, d)) е коректно и намира най-големия общ делител на a, b, c и d.

     В редица случаи се налага функцията да получи входа си чрез някои от формалните си параметри и да върне резултат не по обичайния начин – чрез оператора return, а чрез същите или други параметри. Задача 69 дава пример за това.

 

Задача 69. Да се напише програма, която въвежда стойности на реалните променливи a, b, c и d, след което разменя стойностите на a и b и на c и d съответно.

 

Ако дефинираме функция swapi(double* x, double* y), която разменя стойностите на реалните променливи, към които сочат указателите x и y, обръщението swapi(&a, &b) ще размени стойностите на a и b, а обръщението swapi(&c, &d) ще размени стойностите на c и d. Програма Zad69.cpp решава задачата. Тя се състои от функциите: swapi и main. Тъй като main се обръща към (извиква) swapi, функцията swapi трябва да бъде известна преди функцията main. Затова във файла, съдържащ програмата, първо е поставена функцията swapi, а след това – main.

 

// Program Zad69.cpp

#include <iostream.h>

#include <iomanip.h>

void swapi(double* x, double* y)

{double work = *x;

 *x = *y;

 *y = work;

 return;

}

int main()

{cout << „a, b, c, d= „;

 double a, b, c, d;

 cin >> a >> b >> c >> d;

 cout << setprecision(2) << setiosflags(ios :: fixed);

 cout << setw(10) << a << setw(10) << b

    << setw(10) << c << setw(10) << d << ‘\n’;

 swapi(&a, &b);

 swapi(&c, &d);

 cout << setw(10) << a << setw(10) << b

    << setw(10) << c << setw(10) << d << ‘\n’;

 return 0;

}

     Функцията swapi има подобна структура като на gcd. Но и заглавието, и тялото й са по-различни. Типът на swapi е указан чрез запазената дума void. Това означава, че функцията не връща стойност чрез оператора return. Затова в тялото на swapi е пропуснат изразът след return (възможно е да бъде пропуснат и самия return). Формалните параметри x и y са указатели към типа double, а в тялото се работи със съдържанията на указателите.

     Забелязваме също, че обръщенията към swapi в main

swapi(&a, &b);

swapi(&c, &d);

не участват като аргументи на операции, а са оператори.

     Изпълнение на програма Zad69.cpp

Дефинициите на функциите main и swapi се записват в областта на паметта, определена за програмния код. Изпълнението на програмата започва с изпълнение на функцията main.

 

 

1.5

 

2.75

 

3.25

 

 

8.2

 

 

 

 

 

 

 

 

…                                                                            

 

 

 

 

BIOS

и DOS

 

    

 

                        a    a                  0×0066FDF0             

                        b                       0×0066FDE8        

                        c                       0×0066FDE0         стекова рамка

                                                                       на main

                        d                       0×0066FDD8

                                              

 

 
   

 

 

 

                                                                       указател на стека

 

 

                main            …  0×00401046

    

                swapi                     0×00401019         програмен код

 

 

 
   

 

 

 

 

 

 

Фиг. 8.3 Разпределение на паметта за swapi

Фрагментът

cout << „a, b, c, d= „;

double a, b, c, d;

cin >> a >> b  >> c >> d;

cout << setprecision(2) << setiosflags(ios :: fixed);

cout << setw(10) << a << setw(10) << b

   << setw(10) << c << setw(10) << d << ‘\n’;

дефинира и въвежда стойности за реалните променливи a, b, c и d и ги извежда върху екрана според дефинираното форматиране. Нека за стойности на a, b, c и d са въведени 1.5, 2.75, 3.25 и 8.2 съответно (Фиг. 8.3).

Обръщението

swapi(&a, &b);

се изпълнява по следния начин:

а) Свързване на формалните с фактическите параметри

В стека се конструира нова рамка – рамката на swapi. Указателят на стека се установява след нея. Oтделят се по 4 байта за формалните параметри x и y, в която памет се записват адресите на съответните им фактически параметри, още 4B, в които се записва адресът на swapi(c, d), от където трябва да се продължи изпълнението на main (return-адреса), а също и памет, в която се записва адресът на предишната стекова рамка (в случая на main).

 

0×0066FDE8

 

 

 

 

 

 

  y                           0×0066FD38

 

 

 

0×0066FDF0

 

                             

 

x                          0×0066FD34   

                                     

return-                               стекова рамка

                                      адрес    (адрес от main)         на swapi

 

 
   

 

 

 

                                      адрес на

предишна стекова рамка

        

                                                             указател на стека

б) Изпълнение на тялото на swapi

Изпълнява се като блок. За реалната променлива work се отделят 8 байта в стековата рамка на swapi, в които се записва съдържанието на x, в случая 1.5, т.е.

 

 
   

 

 

 

 

0×0066FDF0

 

     y                                0×0066FD38

 

x                             0×0066FD34

                          

  1.   return-                   0×0066FD..                       стекова рамка

  адрес                          адрес от main               на swapi

 

                                      адрес на предишната

 

 1.5

 

стекова рамка                    

 

     work                        0×0066FD24                      

 

 указател на стека

Oператорът

 *x = *y;

променя съдържанието на x с това на y, а операторът

 *y = work;

променя съдържанието на y като го свързва със стойността на work, т.е.

 

стекова рамка на main

 

2.75

 

1.5

 

3.25

 

8.2

 

 

 

        

 

                        a    a                  0×0066FDF0

                        b                       0×0066FDE8

                        c                       0×0066FDE0

                        d                       0×0066FDD8

 

 

                                              

 

    

Операторът return; прекъсва работа на на swapi и предава управлението в точката на извикването му в главната функция (return-адреса). Стековата рамка, отделена за swapi се освобождава. Указателят на стека сочи стековата рамка на main. В резултат стойностите на променливите a и b са разменени.

     Обръщението swapi(c, d) се изпълнява по аналогичен начин. За нея се генерира нова стекова рамка (на същите адреси), която се освобождава когато изпълнението на swapi завърши.

     Функцията swapi получава входните си стойности чрез формалните си параметри и връща резултата си чрез тях. Забелязваме, че обръщението swapi(&a, &b) работи не с копия на стойностите на а и b, а с адресите им. В процеса на изпълнение на тялото се променят стойностите на фактическите параметри a и b при първото обръщение към нея и на c и d – при второто.

     Такова свързване на формалните с фактическите параметри се нарича свързване на параметрите по указател или още предаване на параметрите по указател или свързване по адрес. При този вид предаване на параметрите, фактическите параметри задължително са променливи или адреси на променливи.

     Освен тези два начина на предаване на параметри, в езика C++ има още един – предаване на параметри по псевдоним. Той е сравнително по-удобен от предаването по указател и се предпочита от програмистите.

     Ще го илюстрираме чрез същата задача. Програма Zad69_1.cpp реализира функция swapi, в която предаването на параметрите е по псевдоним.

 

// Program Zad69_1.cpp

#include <iostream.h>

#include <iomanip.h>

void swapi(double& x, double& y)

{double work = x;

 x = y;

 y = work;

 return;

}

int main()

{cout << „a, b, c, d= „;

 double a, b, c, d;

 cin >> a >> b >> c >> d;

 cout << setprecision(2) << setiosflags(ios :: fixed);

 cout << setw(10) << a << setw(10) << b

    << setw(10) << c << setw(10) << d << ‘\n’;

 swapi(a, b);

 swapi(c, d);

 cout << setw(10) << a << setw(10) << b

    << setw(10) << c << setw(10) << d << ‘\n’;

 return 0;

}

     Ще проследим изпълнението и на тази модификация.

Изпълнението на програмата започва с изпълнение на функцията main. Фрагментът

cout << „a, b, c, d= „;

double a, b, c, d;

cin >> a >> b >> c >> d;

cout << setprecision(2) << setiosflags(ios :: fixed);

cout << setw(10) << a << setw(10) << b

   << setw(10) << c << setw(10) << d << ‘\n’;

дефинира и въвежда стойности за реалните променливи a, b, c и d и ги извежда върху екрана според дефинираното форматиране. Нека за стойности на a, b, c и d отново са въведени 1.5, 2.75, 3.25 и 8.2 съответно. След обработката му в стека се конструира стековата рамка на main.

 

памет на main

 

1.5

 

2.75

 

3.25

 

8.2

 

 

 

 

 

 

        

 

                        a    a                  0×0066FDF0             

                        b                       0×0066FDE8        

                        c                       0×0066FDE0

                        d                       0×0066FDD8

    

 

 

 

Обръщението

swapi(a, b);

се изпълнява по следния начин:

а) Свързване на формалните с фактическите параметри

За целта се генерира нова стекова рамка – рамката на swapi. Указателят на стека сочи тази рамка. Тъй като формалните параметри x и y са псевдоними на променливите a и b съответно, за тях памет в стековата рамка на swapi не се отделя. Параметърът x “прелита” и се “закачва” за фактическия параметър a и аналогично y “прелита” и се “закачва” за фактическия параметър b от стековата рамка на main. Така всички действия с x и y в swapi се изпълняват с фактическите параметри a и b от main съответно.

памет на main

 

1.5

 

2.75

 

3.25

 

8.2

 

 

 

 

 

 

        

 

                   x    a    a                  0×0066FDF0             

                   y    b                       0×0066FDE8        

                        c                       0×0066FDE0

                        d                       0×0066FDD8

    

 

 

 

б) Изпълнение на тялото на swapi

Изпълнява се като блок. В рамката на swapi, за реалната променлива work се отделят 8 байта, в които се записва стойността на x, в случая 1.5, т.е.

     стекова рамка на swapi

 

 

 

 

 

 

 

 

 

1.5

 

                          

 

                                   return-                  

                                   адрес  (адрес от main)    стекова рамка

                                                                            на swapi

                                      адрес на последната

                                      стекова рамка

work                          0×0066FD24

                                                        указател на стека

 

Oператорът

x = y;

присвоява на a стойността на b, а операторът

y = work;

променя стойността на променливата b като й присвоява стойността на work, т.е.

стекова рамка на main

 

2.75

 

1.5

 

3.25

 

8.2

 

 

 

 

 

 

        

 

                   x    a    a                  0×0066FDF0             

                   y    b                       0×0066FDE8        

                        c                       0×0066FDE0

                        d                       0×0066FDD8

 

 

 

 

 

Операторът return; прекъсва работа на на swapi и предава управлението на return-адреса от главната функция. Стековата рамка на swapi се освобождава. Указателят на стека сочи стековата рамка на main. В резултат, стойностите на променливите a и b са разменени. Променливите a и b са “освободени от“ x и y. Следва изпълнение на обръщението

swapi(c, d);

което се реализира по същия начин (даже стековата му рамка се разполага на същите адреси в стека).

     Забелязваме, че фактическите параметри, съответстващи на формални параметри-псевдоними са променливи.

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

     Нека в тялото на main на Zad69_1.cpp преди оператора return; включим фрагмента:

int m, n;

cin >> m >> n;

swapi(m, n);

   cout << setw(10) << m << setw(10) << n << „\n“;

Ще отбележим, че m и n са от тип int, а формалните параметри на swapi са псевдоними на тип double. Някои реализации (в това число Visual C++ 6.0) ще сигнализират грешка на третата линия – невъзможност за преобразуване на параметър от int в double &, други обаче ще имат нормално поведение, но няма да разменят стойностите на m и n. Последното е така, тъй като при несъответствие на типа на псевдонима с типа на инициализатора, в стековата рамка на swapi, се създават “временни” променливи x и y, в които се запомнят конвертираните стойности на инициализаторите. Размяната се извършва, но само в стековата рамка на swapi.

 

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

     Възможно е някои параметри да се подават по стойност, други по псевдоним или по указател, а също функцията да връща резултат и чрез оператора return. Примери ще бъдат дадени в следващите части на изложението. Ще бъдат обсъдени също предимствата и недостатъците на всеки от начините за предаване на параметрите.

     Ако функция не връща резултат чрез return (типът й е void), се нарича още процедура.

     Разгледаните програми се състояха от две функции. По-сериозните приложения съдържат повече функции. Подредбата им може да започва с main, след която в произволен ред да се дефинират останалите функции. В този случай, дефиницията на main трябва да се предшества от декларациите на останалите функции. Декларацията на една функция се състои от заглавието й, следвано от ;. Имената на формалните параметри могат да се пропуснат. Например, програмата от Zad69_1.cpp може да се запише във вида:

// Program Zad69_1.cpp

#include <iostream.h>

#include <iomanip.h>

void swapi(double&, double&); // декларация на swapi

int main()

{cout << „a, b, c, d= „;

 double a, b, c, d;

 cin >> a >> b >> c >> d;

 cout << setprecision(2) << setiosflags(ios :: fixed);

 cout << setw(10) << a << setw(10) << b

    << setw(10) << c << setw(10) << d << ‘\n’;

 swapi(a, b);

 swapi(c, d);

 cout << setw(10) << a << setw(10) << b

    << setw(10) << c << setw(10) << d << ‘\n’;

 return 0;

}

void swapi(double& x, double& y) // дефиниция на swapi

{double work = x;

 x = y;

 y = work;

 return;

}

 

8.3 Дефиниране на функции

 

Синтаксис

 

Дефиницията на функция се състои от две части: заглавие (прототип) и тяло. Синтаксисът й е показан на Фиг. 8.4.

 

     Дефиниране на функция

[<модификатор>]опц[<тип_на_функция>] опц <име_на_функция>

(<формални_параметри>)

{<тяло>

}

където

  <модификатор>::= inline|static| …

  <тип_на_функцията> ::= <име_на_тип> | <дефиниция_на_тип>

  <име_на_функция> ::= <идентификатор>

  <формални_параметри> :: <празно> | void |

                                       <параметър> {, <параметър>}

  <параметър> ::= <тип>[ & |opc * [const]opc] opc <име_на_параметър>

  <тип> ::= <име_на_тип>

  <име_на_параметър> ::= <идентификатор>

  <тяло> ::= <редица_от_оператори_и_дефиниции>

    

Фиг. 8.4 Дефиниция на функция

 

     Модификаторите са спецификатори, които задават препоръка за компилатора (inline), класа памет (extern или static) и др. характеристики. Ще дадем примери в следващите разглеждания. Ако <модификатор> е пропуснат, подразбира се extern.

Типът на функцията е произволен тип без масив и функционален, но се допуска да е указател към такива обекти (в широкия смисъл на думата). Ако е пропуснат, подразбира се int.

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

Списъкът от формални параметри (нарича се още сигнатура) може да е празен или void. Например, следната функция извежда текст:

void printtext(void)

{cout << “C++ Programming Language \n”

      cout << “B. Stroustrup \n”;

      return;

}

В случай, че списъкът е непразен, имената на параметрите трябва да са различни. Те заедно с името определят еднозначно функцията. Формалните параметри са: параметри – стойности, параметри – указатели и параметри – псевдоними. Името на параметъра се предшества от тип.

     Примери:

     int a, int const& b, double& x, int const * y, const int* a

Засега няма да използваме параметри, специфицирани със const.

Тялото на функцията е редица от дефиниции и оператори. Тя описва алгоритъма, реализиращ функцията. Може да съдържа един или повече оператора return.

Операторът return (Фиг. 8.5) връща резултата на функцията в мястото на извикването.

 

    Оператор return

     Синтаксис

      return [<израз>]опц

където

     – return  е запазена дума;

- <израз> е произволен израз от тип <тип_на_функцията> или съвместим с него. Ако типът на функцията е void, <израз> се пропуска. В този случай е възможно и return да се пропусне.

     Семантика

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

 

Фиг. 8.5 Оператор return

 

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

     Функциите могат да се дефинират в произволно място на програмата, но не и в други функции. Преди да се извика една функция, тя трябва да е “позната” на компилатора. Това става, като дефиницията на функцията се постави пред main или когато функцията се дефинира на произволно място в частта за дефиниране на функции, а преди дефинициите на функциите се постави само нейната декларация (Фиг. 8.6).

 

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

<декларация_на_функция> ::=

[<модификатор>][<тип_на_резултата>]<име_на_функция>

([<формални_параметри>]);

 

Фиг. 8.6 Декларация на функция

 

Възможно е имената на параметрите във <формални_параметри> да се пропуснат.

 

    Семантика

 

     Описанието на функция задава параметрите, които носят входа и изхода, типа на резултата, а също и алгоритъма, за реализиране на действията, което функцията дефинира. Параметрите-стойности най-често задават входа на функцията. Параметрите-указатели и псевдоними са входно-изходните параметри за нея. Алгоритъмът се описва в тялото на функцията. Изпълнението на функцията завършва при достигане на края на тялото или след изпълнение на оператор return [<израз>]опц;.

 

8.4 Обръщение към функция

 

Синтаксис

<обръщение_към_функция> ::=

<име_на_функция>()|

<име_на_функция>(void)|

<име_на_функция>(<фактически_параметри>)

където <фактически_параметри> са толкова на брой, колкото са формалните параметри. Освен по брой, формалните и фактическите параметри трябва да си съответстват по тип, по вид и по смисъл.

     Съответствието по тип означава, че типът на i-тия фактически параметър трябва да съвпада (да е съвместим) с типа на i-тия формален параметър. Съответствието по вид се състои в следното: ако формалният параметър е параметър-указател, съответният му фактически параметър задължително е променлива или адрес на променлива, ако е параметър-псевдоним, съответният му фактически параметър задължително е променлива (за реализацията Visual C++, 6.0 от същия тип) и ако е параметър-стойност – съответният му фактически параметър е израз.

 

Семантика

Обръщението към функция е унарна операция с най-висок приоритет и с операнд – името на функцията. Последното пък е указател със стойност адреса на мястото в паметта където е записан програмният код на функцията. Ако функцията определя процедура, обръщението към нея се оформя като оператор (завършва с ;) . Опитът за използването й като израз предизвиква грешка. Ако функцията връща резултат както чрез return, така и чрез някой от формалните си параметри, обръщението към нея може да се разглежда и като оператор, и като израз. И ако функцията връща резултат единствено чрез оператора return, обръщението към нея има единствено смисъла на израз. Използването му като оператор не води до грешка, но не предизвиква видим резултат.

Обръщението към функция предизвиква генериране на нова стекова рамка и се осъществява на следните два етапа:

 

1. Свързване на формалните с фактическите параметри

 

     За целта първият формален параметър се свързва с първия фактически, вторият формален параметър се свързва с втория фактически и т.н. последният формален параметър се свързва с последния фактически параметър. Свързването се реализира по различни начини в зависимост от вида на формалния параметър.

     а) формален параметър – стойност

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

б) формален параметър – указател

В този случай в стековата рамка на функцията за формалния параметър се отделят 4B, в които се записва стойността на фактическия параметър, която е адрес на променлива. Действията, описани в тялото се изпълняват със съдържанието на формалния параметър – указател. По такъв начин е възможна промяна на стойността на променливата, чийто адреа е предаден като фактически параметър.

в) формален параметър – псевдоним

Формалният параметър-псевдоним се свързва с адреса на фактическия. За него в стековата рамка на функцията памет не се отделя. Той просто “прелита” и се “закачва” за фактическия си параметър. Действията с него се извършват над фактическия параметър.

 

2. Изпълнение на тялото на функцията

Аналогично е на изпълнението на блок.

 

При всяко обръщение към функция в програмния стек се включва нов “блок” от данни. В него се съхраняват формалните параметри на функцията, нейните локални променливи, а също и някои “вътрешни” данни като return-адреса и др. Този блок се нарича стекова рамка на функцията.

В дъното на стека е стековата рамка на main. На върха на стека е стековата рамка на функцията, която се обработва в момента. Под нея е стековата рамка на функцията, извикала функцията, обработваща се в момента. Ако изпълнението на една функция завършва, нейната стекова рамка се отстранява от стека.

Видът на стековата рамка зависи от реализацията. С точност до наредба, тя има вида:

 

 
 
    

 

Формални параметри

 

 

Адрес за връщане

 

 

Адрес на предходна

рамка на стека

 

 

 

Локални параметри

 

 

 

 

 

 

 

 

   
   
 
   
 
   

 

 

 

 

 

 

 

 

 

 

8.7 Стекова рамка

 

Област на идентификаторите в програмата на C++

 

Идентификаторите означават имена на константи, променливи, формални параметри, функции, класове. Най-общо казано, има три вида области на идентификаторите: глобална, локална и област за клас. Областите се задават неявно – чрез позицията на идентификатора в програмата и явно – чрез декларация. Отново разглеждането ще е непълно, заради пропускането на класовете и явното задаване на област.

  Глобални идентификатори

  Дефинираните пред всички функции константи и променливи могат да се използват във всички функции на модула, освен ако не е дефиниран локален идентификатор със същото име в някоя функция на модула. Наричат се глобални идентификатори, а областта им – глобална.

  Локални идентификатори

Повечето константи и променливи имат локална област. Те са дефинирани вътре във функциите и не са достъпни за кода в другите функции на модула. Областта им се определя според общото правило – започва от мястото на дефинирането и завършва в края на оператора (блока), в който идентификаторът е дефиниран. Формалните параметри на функциите също имат локална видимост. Областта им е тялото на функцията.

В различните области могат да се използват еднакви идентификатори. Ако областта на един идентификатор се съдържа в областта на друг, последният се нарича нелокален за първоначалния. В този случай е в сила правилото: Локалният идентификатор “скрива” нелокалния в областта си.

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

 

8.5 Масивите като формални параметри

 

Едномерни масиви

Съществуват различни начини за задаване на формални параметри от тип едномерен масив.

а) традиционен

Дефиницията

T a[]

където T е скаларен тип, задава параметър a от тип едномерен масив с базов тип T. Може да се укаже горна граница на масива, но компилаторът я пренебрегва.

     Примери:

     int a[]       – a е параметър от тип масив от цели числа,

     int a[10] – еквивалентна е на int a[],

     double b[]    – b е параметър от тип масив от реални числа,

     char c[]      – c е параметър от тип масив от символи.

     б) чрез указател

     Дефиницията

     T* p

където T е скаларен тип, задава параметър p от тип указател към тип T. От връзката между масив и указател следва, че тази дефиниция може да се използва и за дефиниране на формален параметър от тип масив.

     Примери: Следните дефиниции на формални параметри са еквивалентни на тези от примера по-горе:

int* a      – a е параметър от тип указател към int

     double* b     – b е параметър от тип указател към double.

     char* c       – c е параметър от тип указател към char.

     И в двата случая фактическият параметър се указва с името на едномерен масив от същия тип. Необходимо е също на функцията да се подаде като параметър и размерът на масива.

 

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

 

Програма Zad70.cpp решава задачата.

// Program Zad70.cpp

#include <iostream.h>

int gcd(int, int);

void readarr(int, int[]);

void writearr(int, int[]);

int main()

{cout << „n= „;

 int n;

 cin >> n;

 int a[20];

 readarr(n, a);

 writearr(n, a);

 int x = a[0];

 for (int i = 1; i <= n-1; i++)

          x = gcd(x, a[i]);

 cout << „gcd = “ << x << ‘\n’;

 return 0;

}

int gcd(int a, int b)

{while (a != b)

 if (a > b) a = a-b; else b = b-a;

 return a;

}

void readarr(int m, int arr[])

// m е размерността на масива

// arr е едномерен масив

{for (int i = 0; i <= m-1; i++)

 {cout << „arr[" << i << "]= „;

  cin >> arr[i];

 }

}

void writearr(int m, int arr[])

// m е размерността на масива

// arr е едномерен масив

{for (int i = 0; i <= m-1; i++)

 cout << „arr[" << i << "]= “ << arr[i] << ‘\n’;

}

Изпълнение на програмата

Фрагментът

cout << „n= „;

int n;

cin >> n;

int a[20];

дефинира и въвежда стойност на n, а също дефинира променлива a от тип масив. Нека за n е въведено 5. В резултат е създадена стековата рамка на main. ОП до този момент има вида:

 

 
   

 

 

 

                   n                           0×0066FDF4             

                   a[19]                       0×0066FDF0        

                   a[18]                       0×0066FDEC

                   a[17]                       0×0066FDE8              стекова рамка

                   …                                                 на main

                  

a[2]                      0×0066FDAC   

                   a[1]                    0×0066FDA8

                   a[0]                    0×0066FDA4

 

       
     
   
 

 

 

 

                                                            

                                                                       указател на стека

 

 

 
   

 

 

 

      main               …  0×00401023

    

              gcd                         0×0040101E              програмен код

                                              

              readarr                     0×00401019

    

              writearr                    0×00401014

 

 
   

 

 

 

 

 

Обръщението readarr(n, a); се реализира като се свързват формалните с фактическите параметри и се изпълни тялото. За целта се формира нова стекова рамка – тази на readarr, в която за формалния параметър arr се отделят 4B, в която памет се откопирва стойността на фактическия параметър a (адресът на a[0]), за m се отделят също 4B, в които се откопирва 5 – стойността на фактическия параметър n. Тялото на функцията се изпълнява като блок. Операторът за цикъл

for (int i = 0; i <= m-1; i++)

{cout << „arr[" << i << "]= „;

 cin >> arr[i];

         }

е еквивалентен на

for (int i = 0; i <= m-1; i++)

{cout << „arr[" << i << "]= „;

 cin >> *(arr + i);

         }

и се изпълнява по следния начин: За цялата променлива i се отделят 4B в стековата рамка на readarr. i последователно приема стойностите 0, 1, …, 4 и за всяка стойност се изпълнява блокът

{cout << „arr[" << i << "]= „;

 cin >> *(arr + i);

         }

Операторът cin >> *(arr + i); въвежда стойност на индексираната променлива a[i], тъй като arr + i е адреса на i-тия елемент на a, а *(arr+i) е неговата стойност. Така във функцията се работи с формалния параметър arr, а в действителност действията се изпълняват с фактическия параметър – едномерния масив a. Функцията readarr работи с масива a, а не с негово копие.

 

5

 

-

 

 

-

 

 

0×0066FDA4

 

5

 

 

 

 

0 (, 1, 2, 3, 4)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

                        n                       0×0066FDF4             

 

 

 
   

 

 

 

                   a[19]                        0×0066FDF0        

                   …                                                  стекова рамка

                   a[0]                    0×0066FDA4              на main

 

 
   

 

 

 

                  

                   arr                                                     

                     m                                                 стекова рамка

                                                                            на readarr

return – адрес

   

                   i                                                                                                                                                  указател на стека

 

След достигане на края на функцията изпълнението й завършва и се  освобождава стековата й рамка. В резултат, първите 5 елемента на масива a получават текущи стойности. Операторът             

writearr(n, a);

се изпълнява по аналогичен начин. Отново се работи с фактическия параметър – масива a, а не с негово копие. В този случай, елементите a[0], a[1], …, a[n-1] на a само се сканират и извеждат. Не се извършват промени над тях. За да ги защитим от неправомерен достъп, е добре формалният параметър arr да дефинираме като указател към цяла константа, т.е. като const int arr[]. Тогава всеки опит да се променя arr[i] (i = 0, 1, …, n-1) в writearr ще предизвика грешка.

Фрагментът

int x = a[0];

for (int i = 1; i <= n-1; i++)

          x = gcd(x, a[i]);

намира най-големия общ делител на елементите на редицата.

     В заглавията на последните две процедури горните граници на индексите могат явно да се укажат. Например

void writearr(int m, int arr[20])

и

void readarr(int m, int arr[20])

са валидни заглавия, но компилаторът не се нуждае от горната граница. Трябват му само скобите [], за да разпознае параметър от тип масив. Може също да се използва второто представяне на формален параметър от тип масив, т.е.

void writearr(int m, int* arr)

и

void readarr(int m, int* arr)

Тези представяния на формалните параметри са напълно еквивлентни.

    Забележки:

  1. Функциите readarr и writearr работят с направо с масива a, а не с негови копия. Промените на елементите на масива се запазват след излизане от функцията.
  2. Размерът на масивът не може да се разбере от неговото описание. Затова се налага използването на допълнителния параметър m в списъка от аргументи на функциите. Последното не се отнася за масивите, представляващи символни низове, тъй като те завършват със знака за край на низ ‘’.

 

Задача 71. Да се напише функция len(char* s), която намира дължината на символен низ, а също функция eqstrs(char*, char*), която сравнява два символни низа за лексикографско равно.

 

Функциите Zad71_1 и Zad71_2 решават задачата.

// Function Zad71_1

int len(char* s)

{int k = 0;

 while(*s)

 {k++;

  s++;

 }

 return k;

}

 

// Function Zad71_2

bool eqstrs(char* str1, char* str2)

{while (*str1 == *str2 && * str1)

  {str1++; str2++;}

 return *str1 == *str2;

 }

Обърнете внимание, че тъй като всеки низ завършва със символа ‘’, който се интерпретира като false, изразът *s в оператора за цикъл while на функцията len, ще бъде истина и тялото ще се изпълнява до достигане на края на низа. Аналогична конструкция имаме и при дефиницията на функцията eqstrs.

 

    Задача 72. Да се напише булева функция, която проверява дали цялото число x е елемент на редицата от цели числа a0, a1, …, an-1.

 

     Функцията Zad72 решава задачата.

 

     // Function Zad72

bool search(int n, int a[], int x)

{int i = 0;

 while (a[i] != x && i < n-1)i++;

 return a[i]==x;

}

Допълнение: Обръщението

search(m, b, y)

проберява дали елементът y се съдържа в редицата b0, b1, …, bm-1, а

search(k, b + m, y)

проверява дали y се съдържа в подредицата bm, bm+1, …, bm+k-1.

     Еквивалентна дефиниция на тази функция е:

bool search(int n, int* a, int x)

{int i = 0;

 while (*(a+i) != x && i < n-1)i++;

 return *(a+i)==x;

 }

 

Задача 73. Да се напише функция, която проверява дали редицата от цели числа a0, a1, …, an-1 е монотонно намаляваща.

 

     Функцията Zad73 решава задачата.

 

  // Function Zad73

bool monnam(int n, int a[])

{int i = 0;

 while (a[i] >= a[i+1] && i < n-2)i++;

 return a[i] >= a[i+1];

}

или

bool monnam(int n, int* a)

{int i = 0;

 while (*(a+i) >= *(a+i+1) && i < n-2)i++;

 return *(a+i) >= *(a+i+1);

}

Допълнение: Обръщението

monnam(m, b)

проберява дали редицата b0, b1, …, bm-1 е монотонно намаляваща, а

monnam(k, b + m)

- дали подредицата bm, bm+1, …, bm+k-1 на b0, b1, …, bm-1 е монотонно намаляваща.

 

Задача 74. Да се напише функция, която проверява дали редицата от цели числа a0, a1, …, an-1 се състои от различни елементи.

 

     Функцията Zad74 решава задачата.

 

   // Function Zad74

    bool differ(int n, int a[]

)

    {int i = -1;

     bool b; int j;

     do

     {i++; j = i;

      do

      {j++;

       b = a[i] != a[j];

      }while (b && j < n-1);

      }while (b && i < n-2);

      return b;

     }

 

Допълнение: Обръщението

differ(m, b, y)

проберява дали редицата b0, b1, …, bm-1 се състои от различни елементи, а

differ(k, b + m)

- дали подредицата bm, bm+1, …, bm+k-1 на b0, b1, …, bm-1 се състои от различни елементи.

 

Задача 75. Да се напише програма, която въвежда две числови редици, сортира ги във възходящ ред, слива ги и извежда получената редица.

 

Програма Zad75.cpp решава задачата. За целта са дефинирани следните функции:

 readarr – въвежда числова редица

 writearr – извежда числова редица върху екрана

 sortarr    – сортира във възходящ ред елементите на числова редица

 mergearrs – слива числови редици.

 

 // Program Zad75.cpp

 #include <iostream.h>

 #include <iomanip.h>

 void writearr(int, double[]);

 void readarr(int, double[]);

 void sortarr(int, double[]);

 void mergearrs(int, double[], int, double[], int&, double[]);

 

 int main()

 {cout << „n= „;

  int n;

  cin >> n;

  double a[20];

  readarr(n, a);

  cout << endl;

  writearr(n, a);

  cout << endl;

  sortarr(n, a);

  cout << endl;

  writearr(n, a);

  cout << „m= „;

  int m;

  cin >> m;

  double b[30];

  readarr(m, b);

  cout << endl;

  writearr(m, b);

  cout << endl;

  sortarr(m, b);

  cout << endl;

  writearr(m, b);

  cout << endl;

  int p;

  double c[50];

  mergearrs(n, a, m, b, p, c);

  writearr(p, c);

  return 0;

 }

 void writearr(int m, double arr[])

 {cout << setprecision(3) << setiosflags(ios::fixed);

  for (int i = 0; i <= m-1; i++)

    cout << setw(10) << arr[i];

  cout << „\n“;

 }

      void readarr(int m, double arr[])

 {for (int i = 0; i <= m-1; i++)

 {cout << „arr[" << i << "]= „;

  cin >> arr[i];

 }

 }

  void sortarr(int n, double a[])

 {for (int i = 0; i <= n-2; i++)

 {int k = i;

  double min = a[i];

  for (int j = i+1; j <= n-1; j++)

   if (a[j] < min)

   {min = a[j];

    k = j;

   }

  double x = a[i]; a[i] = a[k]; a[k] = x;

 }

}

void mergearrs(int n, double a[], int m, double b[],

int& k, double c[])

{int i = 0, j = 0;

 k = -1;

 while (i <= n-1 && j <= m-1)

 if (a[i] <= b[j])

 {k++;

  c[k] = a[i];

  i++;

 }

 else

 {k++;

  c[k] = b[j];

  j++;

 }

 int l;

 if (i > n-1)

   for (l = j; l <= m-1; l++)

   {k++;

    c[k] = b[l];

   }

 else

  for (l = i; l <= n-1; l++)

  {k++;

   c[k] = a[l];

  }

  k++;

 }

 

    Многомерни масиви

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

     void readarr2(int n, int matr[][20]);

определя matr като двумерен масив (редица от двадесеторки от цели числа). Описанието

     int (*matr)[20]

е еквивалентно на

int matr[][20]

Скобите, ограждащи *matr, са задължителни. В противен случай, тъй като [] е с по-висок приоритет от *, int *matr[20] ще се интерпретира като “matr е масив с 20 елемента от тип *int”.

 

     Задача 76. Да се напише програма, която въвежда квадратна матрица от цели числа, след което я извежда като увеличава всеки от елементите на матрицата над главния диагонал с 5 и намалява всеки от елементите под главния диагонал с 5.

 

     Програма Zad76.cpp решава задачата. Тя дефинира функциите:

     readarr2 – въвежда квадратна матрица

     writearr2 – извежда квадратна матрица

     transff  – увеличава всеки от елементите на матрицата над главния диагонал с 5 и намалява всеки от елементите под главния диагонал с 5.

 

// Program Zad76.cpp

#include <iostream.h>

#include <iomanip.h>

void readarr2(int, int[][10]);

void writearr2(int, int[][10]);

void transff(int, int[][10]);

int main()

{int a[10][10];

 cout << „n= „;

 int n;

 cin >> n;

 if (!cin)

 {cout << „Error. Bad input! \n“;

  return 1;

 }

 if (n < 1 || n > 10)

 {cout << „Incorrect input! \n“;

  return 1;

 }

 readarr2(n, a);

 cout << ‘\n’;

 writearr2(n, a);

 cout << ‘\n’;

 transff(n, a);

 writearr2(n, a);

 return 0;

}

void readarr2(int n, int arr[][10])

{for (int i = 0; i <= n-1; i++)

  for (int j = 0; j <= n-1; j++)

  cin >> arr[i][j];

}

void writearr2(int n, int arr[][10])

{for (int i = 0; i <= n-1; i++)

 {for (int j = 0; j <= n-1; j++)

  cout << setw(5) << arr[i][j];

  cout << „\n“;

 }

}

void transff(int n, int arr[][10])

{int i, j;

 for (i = 1; i <= n-1; i++)

  for (j = 0; j <= i-1; j++)

    arr[i][j] = arr[i][j] – 5;

 for(i = 0; i <= n-2; i++)

  for(j = i+1; j <= n-1; j++)

    arr[i][j] = arr[i][j] + 5;

}

Обръщението

  transff(k, a + m);

 

 
   

ще извърши същото действие над квадратната подматрица на дадената матрица:

 

    

     Задача 77. Да се напише програма, която въвежда редица от думи не по-дълги от 14 знака и дума, също не по-дълга от 14 знака. Програмата да проверява дали думата се среща в редицата. За целта да се оформят подходящи функции.

 

     Програма Zad77.cpp решава задачата. В нея са дефинирани функциите:

void readarrstr(int n, char s[][15]) – въвежда редица от n думи,

bool search(int n, char s[][15], char* x) – търси думата x в редицата s от n думи. За целта използва помощната функция

bool eqstrs(char* str1, char* str2);

от Задача 71.

 

// Program Zad77.cpp

     #include <iostream.h>

#include <string.h>

void readarrstr(int, char [][15]);

bool eqstrs(char*, char*);

bool search(int, char [][15], char*);

int main()

{char a[20][15];

 cout << „n= „;

 int n;

 cin >> n;

 readarrstr(n, a);

 cout << „word: „;

 char word[15];

 cin >> word;

 if (search(n, a, word)) cout << „yes \n“;

 else cout << „no \n“;

 return 0;

}

void readarrstr(int n, char s[][15])

{for(int i = 0; i <= n-1; i++)

 {cout << „s[" << i << "]= „;

  cin >> s[i];

 }

}

bool eqstrs(char* str1, char* str2)

 {while (*str1 && *str1 == *str2)

   {str1++;

 str2++;

}

  if(*str1 != *str2) return false;

  else return true;

 }

bool search(int n, char s[][15], char* x)

{int i = 0;

 while (!eqstrs(s[i], x) && i < n-1) i++;

 return eqstrs(s[i], x);

}

    

     Задача 78. Да се напише програма, която умножава две матрици.

 

     Програма Zad78.cpp решава задачата. Тя дефинира следните функции:

     readarr2 – въвежда матрица,

     writearr2 – извежда матрица,

     multmatr – умножава матрици.

 

     // Program Zad78.cpp

     #include <iostream.h>

#include <iomanip.h>

void readarr2(int n, int m, double [][30]);

void writearr2(int n, int m, double [][30]);

void multmatr(int, int, int, double [][30],

             double [][30], double [][30]);

 

int main()

{double a[10][30], b[20][30], c[10][30];

 cout << „n= „;

 int n;

 cin >> n;

 if (!cin)

 {cout << „Error. Bad input! \n“;

  return 1;

 }

 if (n < 1 || n > 10)

 {cout << „Incorrect input! \n“;

  return 1;

 }

 cout << „m= „;

 int m;

 cin >> m;

 if (!cin)

 {cout << „Error. Bad input! \n“;

  return 1;

 }

 if (m < 1 || m > 30)

 {cout << „Incorrect input! \n“;

  return 1;

 }

  cout << „k= „;

 int k;

 cin >> k;

 if (!cin)

 {cout << „Error. Bad input! \n“;

  return 1;

 }

 if (k < 1 || k > 30)

 {cout << „Incorrect input! \n“;

  return 1;

 }

 readarr2(n, m, a);

 writearr2(n, m, a);

 cout << „\n“;

 readarr2(m, k, b);

 cout << „\n“;

 writearr2(m, k, b);

 cout << „\n“;

 multmatr(n, m, k, a, b, c);

 writearr2(n, k, c);

 return 0;

}

void readarr2(int n, int m, double arr [][30])

{for (int i = 0; i <= n-1; i++)

   for (int j = 0; j <= m-1; j++)

     cin >> arr[i][j];

 return;

}

void writearr2(int n, int m, double arr[][30])

{cout << setprecision(3) << setiosflags(ios::fixed);

 for (int i = 0; i <= n-1; i++)

 {for (int j = 0; j <= m-1; j++)

  cout << setw(10) << arr[i][j];

  cout << „\n“;

 }

 return;

}

void multmatr(int n, int m, int k, double a[][30],

               double b[][30], double c[][30])

{for (int i = 0; i <= n-1; i++)

  for (int j = 0; j <= m-1; j++)

  {c[i][j] = 0;

   for (int p = 0; p <= m-1; p++)

     c[i][j] += a[i][p] * b[p][j];

  }

}

 

8.6 Масивите като върнати оценки

 

Въпреки, че масивите могат да са параметри на функции, функциите не могат да са от тип масив. Възможно е обаче да са от тип указател. Това позвлява дефинирането на функции, които връщат масиви.

     Пример: В следващата програма е дефинирана функцията readarr, която въвежда стойности на едномерен масив. Тя връща резултат не само чрез променливата от тип масив arr, но и чрез оператора return. Това позволява обръщенията към нея да служат както за оператори, така и за изрази.

#include <iostream.h>

void writearr(int, int[]);

int* readarr(int, int[]);

int main()

{cout << „n= „;

 int n;

 cin >> n;

 int a[20];

 int* p = readarr(n, a);

 writearr(n, p);

 cout << endl;

 return 0;

}

void writearr(int m, int arr[])

{for (int i = 0; i <= m-1; i++)

 cout << „arr[" << i << "]= “ << arr[i] << ‘\n’;

return;

}

     int* readarr(int m, int arr[])

{for (int i = 0; i <= m-1; i++)

    {cout << „arr[" << i << "]= „;

     cin >> arr[i];

 }

return arr;

}

    Въпрос: Допустима ли е конструкцията:  readarr(n, a)[i], където i е цяло число от 0 до n-1? Ако това е така, какъв е резултатът от изпълнението му?

 

     Задача 79. Да се напише функция, която намира и връща като резултат конкатенацията на два низа. Функцията да променя първия си аргумент като в резултат той също да съдържа конкатенацията на низовете.

 

     Програма Zad79.cpp решава задачата.

// Program Zad79.cpp

#include <iostream.h>

int len(char*);

char *cat(char*, char*);

int main()

{char s1[100];

 cout << „s1= „;

 cin >> s1;

 cout << „s2= „;

 char s2[100];

 cin >> s2;

 cout << cat(s1, s2) << „  “ << s1 << ‘\n’;

 return 0;

}

int len(char* s)

{int k = 0;

 while (*s)

 {k++; s++;

 }

 return k;

}

char* cat(char *s1, char *s2)

{int i = len(s1);

 while (*s2)

 {s1[i] = *s2;

  i++;

  s2++;

     }   

 s1[i] = “;

 return s1;

}

 

 

Задачи

    

Задача 1. Въпреки многото й недостатъци, следващата програма е доста поучителна. Тя дефинирана функцията readarr, която има за формален параметър броя на елементите на масива и връща едномерен масив, определен чрез указател към първия му елемент.

#include <iostream.h>

int a[20];

void writearr(int, int[]);

int* readarr(int);

int main()

{cout << „n= „;

 int n;

 cin >> n;

 int* p = readarr(n);

 writearr(n, p);

 cout << ‘\n’;

 return 0;

}

void writearr(int m, int arr[])

{for (int i = 0; i <= m-1; i++)

 cout << „arr[" << i << "]= “ << arr[i] << ‘\n’;

}

int* readarr(int m)

{for (int i = 0; i <= m-1; i++)

{cout << „a[" << i << "]= „;

 cin >> a[i];

}

return a;

}

Извършете експерименти с тази програма.

 

 
   

Задача 2. Да се напише програма, която въвежда полиномите:

 

 

 
   

и реалната променлива x и намира и извежда стойностите на полиномите в x.

 

Задача 3. Да се напише програма, която въвежда стойности на редиците:

         a0, a1, …, an-1,

  b0, b1, …, bm-1,

  c0, c1, …, cp-1

 

 
   

и намира и извежда AR1, AR2, BR1, BR2, CR1 и CR2, където за дадена редица x0, x1, …, xk-1

 

      Задача 4. Да се напише функция, която намира разстоянието между две точки в равнината, зададени чрез координатите си (x1, y1) и (x2, y2). Като се използва тази функция да се напише програма, която чете координатите на n точки (n ≥ 1) от равнината и намира и извежда разстоянието между всеки две от тях.

Задача 5. да се напише функция, която връща стойност true, ако a, b и c са страни на триъгълник и false – в противен случай. Като се използва тази функция, да се напише програма, която въвежда стойности на елементите на матрицата A3xn и определя кои от тройките (a[0][i], a[1][i], a[2][i]), i = 0, 1, …, n-1 могат да служат за страни на триъгълник.

     Задача 6. Да се напише функция, която връща стойност true ако редицата от цели числа x0, x1, …, xk-1 има поне два последователни нулеви елемента. Като се използва тази функция, да се напише програма, която намира и извежда номерата на редовете на матрицата A [n x n], от цели числа, които имат поне два последователни нулеви елемента.

Задача 7. Да се напише функция, която намира сумата на два полинома. Като се използва тази функция, да се напише програма, която намира сумата на всеки два от полиномите:

 

 
   

 

Задача 8. Даден е триъгълник със страни a, b и c. Да се напише програма, която намира медианите на триъгълник, страните на който са медианите на дадения триъгълник.
     Упътване: Медианата към страната a на триъгълника е равна на

 

     Задача 9. Дадени са координатите на върховете на n триъгълника. Да се напише програма, която определя, кой от триъгълниците е с по-голямо лице.

 

 
   

Задача 10. Дадени са естественото число p > 1 и реалните квадратни матрици с размерности n x n – A, B и C. Да се напише програма, която намира матрицата

 

 

 

Допълнителна литература

 

  1. B. Stroustrup, C++ Programming Language. Third Edition, Addison – Wesley, 1997.
  2. Ст. Липман, Езикът C++ в примери, “КОЛХИДА ТРЕЙД” КООП, София, 1993.

            3. Д. Луис, C/C++ бърз справочник, ИнфоДАР, София, 1998.

4. И. Момчев, К. Чакъров, Програмиране III, C и C++, ТУ, София, 1996.

5. М. Тодорова, Програмиране на Паскал, Полипринт, София, 1993.

→ Leave a CommentКатегории: Uncategorized

Шеста част

19/05/2009 · Вашият коментар

 

 

 

6

 

 

Съставни типове данни.

Масив. Символен низ

 

 

6.1 Структура от данни масив

 

Под структура от данни се разбира организирана информация, която може да бъде описана, създадена и обработена с помощта на програма.

     За да се определи една структура от данни е необходимо да се направи:

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

- физическо представяне на структурата, което дава методи за представяне на структурата в паметта на компютъра.

В предходните глави разгледахме структурите числа и символи. За всяка от тях в езика C++ са дадени съответни типове данни, които ги реализират. Тъй като елементите на тези структури се състоят от една компонента, те се наричат прости, или скаларни.

Структури от данни, компонентите на които са редици от елементи, се наричат съставни.

Структури от данни, за които операциите включване и изключване на елемент не са допустими, се наричат статични, в противен случай – динамични.

В тази глава ще разгледаме структурата от данни масив и средствата, които я реализират.

 

Логическо описание

 

Масивът е крайна редица от фиксиран брой елементи от един и същ тип. Към всеки елемент от редицата е възможен пряк достъп, който се осъществява чрез индекс. Операциите включване и изключване на елемент в/от масива са недопустими, т.е. масивът е статична структура от данни.

 

Физическо представяне

 

Елементите на масива се записват последователно в паметта на компютъра, като за всеки елемент на редицата се отделя определено количество памет.

 

В езика C++ структурата масив се реализира чрез типа масив.

 

6.2 Тип масив

 

В C++ структурата от данни масив е реализирана малко ограничено. Разглежда се като крайна редица от елементи от един и същ тип с пряк достъп до всеки елемент, осъществяващ се чрез индекс с цели стойности, започващи от 0 и нарастващи с 1 до указана горна граница. Дефинира се от програмиста.

 

Дефиниране на масив

 

 Типът масив се определя чрез задаване на типа и броя на елементите на редицата, определяща масив. Нека T е име или дефиниция на произволен тип, различен от псевдоним, void и функционален. За типа T и константния израз  от интегрален или изброен тип с положителна стойност size, T[size] е тип масив от size елемента от тип T. Елементите се индексират от 0 до size–1. T се нарича базов тип за типа масив, а size – горна граница.

  Примери:

int[5] дефинира масив от 5 елемента от тип int, индексирани от 0 до 4;

double[10] дефинира масив от 10 елемента от тип double, индексирани от 0 до 9;

bool[4] дефинира масив от 4 елемента от тип bool, индексирани от 0 до 3.

 

Множество от стойности

 

Множеството от стойности на типа T[size] се състои от всички редици от по size елемента, които са произволни константи от тип T. Достъпът до елементите на редиците е пряк и се осъществява с помощта на индекс, като достъпът до първия елемент се осъществява с индекс със стойност 0, до последния – с индекс със стойност size-1, а до всеки от останалите елементи – с индекс със стойност с 1 по-голяма от тази на индекса на предишния елемент.

Примери:

1. Множеството от стойности на типа int[5] се състои от всички редици от по 5 цели числа. Достъпът до елементите на редиците се осъществява с индекс със стойности 0, 1, 2, 3 и 4. 

 

                                                                    int[5]

 

 

               
               

 

 

 

 

 

           0        1        2        3         4

 

2. Множеството от стойности на типа double[10] се състои от всички редици от по 10 реални числа. Достъпът до елементите на редиците се осъществява с индекс със стойности 0, 1, 2, 3 и т.н. 9. 

 

                                                                    double[10]

 

           
           

 

 

 

 

 

 

           0             1                 9

Елементите от множеството от стойности на даден тип масив са константите на този тип масив.

Примери:

1. Следните редици {1,2,3,4,5}, {-3, 0, 1, 2, 0}, {12, -14, 8, 23, 1000} са константи от тип int[5].

 

2. Редиците {1.5, -2.3, 3.4, 4.9, 5.0, -11.6, -123.56, 13.7, -32.12, 0.98}, {-13, 0.5, 11.9, 21.98, 0.03, 1e2, -134.9, 0.09, 12.3, 15.6} са константи от тип double[10].

 

 Променлива величина, множеството от допустимите стойности на която съвпада с множеството от стойности на даден тип масив, се нарича променлива от дадения тип масив. Понякога ще я наричаме само масив.

     Фиг. 6.1 определя дефиницията на променлива от тип масив. Тук общоприетият запис е нарушен. Променливата се записва между името на типа и размерността.

 

Дефиниция на масив

     <дефиниция_на_променлива_от_тип_масив> ::=

      T <променлива>[size] [= {<редица_от_константни_изрази>}]опц

      {,<променлива>[size] [= {<редица_от_константни_изрази>}]опц }опц;

където

     – Т e име или дефиниция на произволен тип, различен от псевдоним, void, функционален;

     – <променлива> е идентификатор;

- size е константен израз от интегрален или изброен тип с положителна стойност;

   – <редица_от_константни_изрази> се дефинира по следния начин:

     <редица_от_константни_изрази> ::= <константен_израз>|

              <константен_израз>, <редица_от_константни_изрази>

като константните изрази са от тип T или от тип, съвместим с него.

 

Фиг. 6.1 Дефиниция на масив

 

     Примери:

     int a[5];

     double c[10];

     bool b[3];

     enum {FALSE, TRUE} x[20];

     double p[4] = {1.25, 2.5, 9.25, 4.12};

 

Дефиницията

T <променлива>[size] = {<редица_от_константни_изрази>}

се нарича дефиниция на масив с инициализация, а Фрагмента {<редица_от_константни_изрази>} – инициализация. При нея е възможно size да се пропусне. Тогава за стойност на size се подразбира броят на константните изрази, изброени в инициализацията. Ако size е указано и изброените константни изрази в инициализацията са по-малко от size, останалите се приемат за 0.

     Примери:

  1. Дефиницията

int q[5] =  {1, 2, 3};

е еквивалентна на

int q[] =  {1, 2, 3, 0, 0};

  1. Дефиницията

double r[] = {0, 1, 2, 3};

e еквивалентна на

         double r[4] = {0, 1, 2, 3};

     Забележка: Не са възможни конструкции от вида:

int q[5];      

q = {0, 1, 2, 3, 4};

а също

     int q[];

и

     double r[4] = {0.5, 1.2, 2.4, 1.2, 3.4};

     Ще отбележим, че Фрагментите

<променлива>[size] и

     <променлива>[size] = {<редица_от_константни_изрази>}

от дефиницията от Фиг. 6.1 могат да се повтарят. За разделител се използва знакът запетая.

     Пример: Дефиницията

     double m1[20], m2[35], proben[30];

е еквивалентна на дефинициите:

     double m1[20];

     double m2[35];

     double proben[30];

     Дефиницията с инициализация е един начин за свързване на променлива от тип масив с конкретна константа от множеството от стойности на този тип масив. Друг начин предоставят т.нар. индексирани променливи. С всяка променлива от тип масив е свързан набор от индексирани променливи. Фиг. 6.2 илюстрира техния синтаксис.

    

Синтаксис на индексирани променлии

<индексирана_променлива> ::=

              <променлива_от_тип_масив>[<индекс>]

където

<индекс> e израз от интегрален или изброен тип.

Всяка индексирана променлива е от базовия тип.

 

Фиг. 6.2 Синтаксис на индексираните променливи

 

 Примери:

1. С променливата a, дефинирана по-горе, са свързани индексираните променливи a[0], a[1], a[2], a[3] и a[4], които са от тип int.

2. С променливата b са свързани индексираните променливи b[0], b[1],…, b[9], които са от тип double.

3. С променливата x са свързани индексираните променливи x[0], x[1],…, x[19], които са от тип enum {FALSE, TRUE}.

 

     Дефиницията на променлива от тип масив не само свързва променливата с множеството от стойности на указания тип, но и отделя определено количество памет (обикновено 4B), в която записва адреса в паметта на първата индексирана променлива на масива. Останалите индексирани променливи се разполагат последователно след първата. За всяка индексирана променлива се отделя по толкова памет, колкото базовият тип изисква.

     Пример:

     ОП

а          a[0] a[1] …       a[4] b             b[0] b[1]     …         b[9] …

адрес      -       -                 -       адрес         -         -                 -

на a[0]                                          на b[0]

4B              4B       4B       …       4B       4B      8B        8B        …        8B

За краткост, вместо “адрес на а[0]” ще записваме стрелка от а към a[0]. Стойността на отделената за индексираните променливи памет е неопределено освен ако не е зададена дефиниция с инициализация. Тогава в клетките се записват инициализиращите стойности.

     Пример: Разпределението на паметта за променливите p и q, дефинирани в примерите по-горе, е следното:

 

     ОП

     p             p[0]     p[1]     p[2]     p[3]    

  1.                    1.25     2.5      9.25     4.12    

 

q          q[0] q[1] q[2] q[3] q[4]

1         2    3  0         0

 

    Операции и вградени функции

 

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

     Пример: Нека

     int a[5], b[5];

Недопустими са:

cin >> a >> b;

     a = b;

а също a == b или a != b.

     Операторът

     cout << a;

е допустим и извежда адреса на a[0].

 

Задачи върху тип масив

 

     Задача 48. Да се напише програма, която въвежда последователно n числа, след което ги извежда в обратен ред.

 

     Програма Zad48.cpp решава задачата.

     // Program Zad48.cpp

     #include <iostream.h>

int main()

{double x[100];

 cout << „n= „;

 int n;

 cin >> n; // въвеждане на стойност за n

 if (!cin)

 {cout << „Error. Bad input! \n“;

  return 1;

 }

 if (n < 0 || n > 100)

 {cout << „Incorrect input! \n“;

  return 1;

 }

 // n е цяло число от интервала [1, 100]

 // въвеждане на стойности за елементите на масива x

 for (int i = 0; i <= n-1; i++)

 {cout << „x[" << i << "]= „;

  cin >> x[i];

  if (!cin)

  {cout << „Error. Bad Input! \n“;

   return 1;

  }

 } // извеждане на елементите на x в обратен ред

 for (i = n-1; i >= 0; i–)

   cout << x[i] << „\n“;

 return 0;

}

 

Изпълнение на програма Zad48.cpp

 

Дефиницията double x[100]; води до отделяне на 800B ОП, които се именуват последователно с x[0], x[1], …, x[99] и са с неопределени стойности. Освен това се отделят 4B ОП за променливата x, в които се записва адресът на индексираната променлива x[0]. Следващият програмен фрагмент  въвежда стойност на n (броя на елементите на масива, които ще бъдат използвани). Операторът

for (int i = 0; i <= n-1; i++)

 {cout << „x[" << i << "]= „;

  cin >> x[i];

  if (!cin)

  {cout << „Error. Bad Input! \n“;

   return 1;

  }

 }

въвежда стойности на целите променливи x[0], x[1], …, x[n-1]. Всяка въведена стойност е предшествана от подсещане. Операторът

for (i = n-1; i >= 0; i–)

   cout << x[i] << „\n“;

извежда в обратен ред компонентите на масива x.

 

     Забележка: Фрагментите:

 …                                     и             …

 cout << „n= „;                        int n = 10;

 int n;                                          int x[10];

 cin >> n;                                  …

      int x[n];

      …

са недопустими, тъй като n не е константен израз. Фрагментът

     const int n = 10;

     double x[n];

е допустим.

 

6.3 Някои приложения на структурата от данни масив

 

Търсене на елемент в редица

Нека са дадени редица от елементи a0, a1, …, an-1, елемент x и релация r. Могат да се формулират две основни зaдачи, свързани с търсене на елемент в редицата, който да е в релация r с елемента x.

a) Да се намерят всички елементи на редицата, които са в релация r с елемента x.

б) Да се установи, съществува ли елемент от редицата, който е в релация r с елемента x.

Съществуват редица методи, които решават едната, другата или и двете задачи. Ще разгледаме метода на последователното търсене, чрез който магат да се решат и двете задачи. Методът се състои в следното: последователно се обхождат елементите на редицата и за всеки елемент се проверява дали е в релация r с елемента x. При първата задача процесът продължава до изчерпване на редицата, а при втората – до намиране на първия елемент ak (k = 0, 1, …, n-1), който е в релация r с x, или до изчерпване на редицата без да е намерен елемент с търсеното свойство.

Следващите четири задачи илюстрират този метод.

 

Задача 49. Дадени са редицата от цели числа a0, a1, …, an-1 (n ≥ 1) и цялото число x. Да се напише програма, която намира колко пъти x се съдържа в редицата.

 

В случая релацията r е операцията сравнение за равенство, която се реализира чрез оператора ==. Налага се всеки елемент на редицата да бъде сравнен с x, т.е. имаме задача от първия вид. Тя описва индуктивен цикличен процес.

Програма Zad49.cpp решава задачата.

 

// Program Zad49.cpp

#include <iostream.h>

int main()

{int a[20];

 cout << „n= „;

 int n;

 cin >> n; // въвеждане на дължината на редицата

 if (!cin)

 {cout << „Error. Bad input! \n“;

  return 1;

 }

 if (n < 1 || n > 20)

 {cout << „Incorrect input! \n“;

  return 1;

 }

 // въвеждане на редицата

 int i;

 for (i = 0; i <= n-1; i++)

 {cout << „a[" << i << "]= „;

  cin >> a[i];

  if (!cin)

  {cout << „Error. Bad input! \n“;

   return 1;

  }

 }

 // въвеждане на стойност за x

 int x;

 cout << „x= „;

 cin >> x;

 if (!cin)

 {cout << „Error. Bad input! \n“;

  return 1;

 }

 // намиране на броя br на срещанията на x в редицата

 int br = 0;

 for (i = 0; i <= n-1; i++)

   if (a[i] == x) br++;

 cout << „number = “ << br << „\n“;

 return 0;

}

 

Задача 50. Дадени са редицата от цели числа a0, a1, …, an-1 (n ≥ 1) и цялото число x. Да се напише програма, която проверява дали x се съдържа в редицата.

 

В този случай се изисква при първото срещане на елемент от редицата, който е равен на x, да се преустанови работата с подходящо съобщение. Броят на сравненията на x с елементите от редицата е ограничен отгоре от n, но не е известен.

Програма Zad50.cpp решава задачата. Фрагментът, реализиращ входа, е същия като в Zad49.cpp и затова е пропуснат.

 

// Program Zad50.cpp

#include <iostream.h>

int main()

{int a[20];

 …

 i = 0;

 while (a[i] != x && i < n-1)

   i++;

 if (a[i] == x) cout << „yes \n“;

 else cout << „no \n“;

 return 0;

}

 

Обхождането на редицата става чрез промяна на стойностите на индекса i – започват от 0 и на всяка стъпка от изпълнението на тялото на цикъла се увеличават с 1. Максималната им стойност е n-1. При излизането от цикъла ще е в сила отрицанието на условието (a[i] != x && i < n-1), т.е. (a[i] == x || i == n-1). Ако е в сила a[i] == x, тъй като сме осигурили a[i] да е елемент на редицата, отговорът “yes” е коректен. В противен случай е в сила i == n-1, т.е. сканиран е и последният елемент на редицата и за него не е вярно a[i] == x. Това е реализирано чрез отговора “no” от алтернативата на условния оператор.

Фрагментът

i = -1;

do

i++;

while (a[i] != x && i < n-1);

if (a[i] == x) cout << „yes \n“;

else cout << „no \n“;

реализира търсенето чрез използване на оператора do/while.

 

Задача 51. Да се напише програма, която установява, дали редицата от цели числа a0, a1, …, an-1 е монотонно намаляваща.

 

     a) За решаването на задачата е необходимо да се установи, дали за всяко i (0 ≤ i ≤ n-2) е в сила релацията a[i] >= a[i+1]. Това може да се реализира като се провери дали броят на целите числа i (0≤i≤n-2), за които е в сила релацията a[i] ≥ a[i+1], е равен на n-1.

     Програмата Zad51_1.cpp реализира този начин за проверка дали редица е монотонно намаляваща. Фрагментите, реализиращи въвеждането на n и масива a, са известни вече и затова са пропуснати.

 

     // Program Zad51_1.cpp

#include <iostream.h>

int main()

{int a[100];

 // дефиниране и въвеждане на стойност на n

 …

 // въвеждане на масива а

 …

 int br = 0;

 for (i = 0; i <= n-2; i++)

   if (a[i] >= a[i+1]) br++;

 if (br == n-1) cout << „yes \n“;

 else cout << „no \n“;

 return 0;

}

     б) Задачата може да се сведе до търсене на i (i = 0, 1,…, n-2), така че a[I] < a[i+1], т.е. до задача за съществуване.

     Програма Zad51_2.cpp реализира този начин за проверка дали редица е монотонно намаляваща. Фрагментите, реализиращи въвеждането на n и масива a отново са пропуснати.

 

     // Program Zad51_2.cpp;

     #include <iostream.h>

int main()

{int a[100];

 //въвеждане на размерността n и масива a

 …

 i = 0;

 while (a[i] >= a[i+1] && i < n-2) i++;

 if (a[i] >= a[i+1]) cout << „yes \n“;

 else cout << „no \n“;

 return 0;

}

     Решение б) е по-ефективно, тъй като при първото срещане на a[i], така че релацията a[i] < a[i+1] е в сила, изпълнението на цикъла while завършва. Решение а) реализира последователно търсене е пълно изчерпване, а решение б) – задача за съществуване на елемент в редица, който е в определена релация с друг елемент (в случая съседния му).

 

    Задача 52. Да се напише програма, която установява, дали редицата от цели числа a0, a1, …, an-1  се състои от различни елементи.

 

a) За решаването на задачата е необходимо да се установи, дали за всяка двойка (i, j): 0 ≤ i ≤ n-2 и i+1 ≤ j ≤ n-1  е в сила релацията a[i] != a[j]. Това може да се постигне като се провери дали броят на двойките (i, j): 0 ≤ i ≤ n-2 и i+1 ≤ j ≤ n-1, за които е в сила релацията a[i] != a[j], е равен на n*(n-1)/2.

     Програма Zad52_1.cpp реализира тази идея за решение на задачата – търсене с пълно изчерпване. Фрагментите, реализиращи въвеждането на n и масива a, отново са пропуснати.

 

// Program Zad52_1.cpp

#include <iostream.h>

int main()

{int a[100];

 //въвеждане на размерността n и масива a

 …

 int br = 0;

 for (i = 0; i <= n-2; i++)

   for (int j = i+1; j <= n-1; j++)

        if (a[i] != a[j]) br++;

 if (br == n*(n-1)/2) cout << „yes \n“;

 else cout << „no \n“;

 return 0;

}

     б) Задачата може да се сведе до проверка за съществуване на двойка индекси (i, j): 0 ≤ i ≤ n-2 и i+1 ≤ j ≤ n-1, за които не  е в сила релацията a[i] != a[j]. Програма Zad52_2.cpp реализира тази идея.

 

     // Program Zad52_2.cpp

#include <iostream.h>

int main()

{int a[100];

      // въвеждане стойности на размерността n и масива a

      …

 i = -1;

 int j;

 do

 {i++;

  j = i+1;

  while (a[i]