Програмен език C++
Част 34
.
 Масивите като променливи на класовете
(Arrays as Instance Data)
.
(съдържание)
.
Използването на масивите като променливи на класа (въведено в лекция 16) предполага няколко интересни проблема, свързани с тяхното оразмеряване и инициализиране. Ето защо в настоящата лекция тези проблеми ще бъдат разгледани и ще бъде предложено решение, което напълно съответства на концепцията за обектно-ориентираното програмиране (ООП).
.
1. Оразмеряване на масив (Sizing an Array). В предишната лекция 31 в програмата stackcon.cpp масивът st беше оразмерен с константата 20 по следния начин:

class Stack                   // a stack holds up to 20 ints
{
   private:
      int st[20];             // integers are stored in array
      int top;                // index of last item pushed

   public:
   . . .

};

Какви са проблемите?

1. Числото 20 нищо не говори на един друг програмист. Ако програмата е значително голяма, след време даже и на автора и ще му струва усилие да разбере какво е това число.

2. Число 19 (20 - 1) е необходимо във функцията на класа push(), за да се защити програмата от работа извън паметта на масива:

void push(int var)
{
  if(top >= 19)                // if at or beyond the end of array,
    cout << "Illegal push";    // error message
  else
    st(++top) = var;
}

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

Проблемът може да бъде решен като масивът се оразмери с външна константа (array sized with external const), чието име ни подсеща за нейното предназначение - например size или array_size или даже още по-добре stack_array_size. Освен това е хубаво да се постави и коментар.

  // size of st variable of Stack class
  const int stack_array_size = 20;

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

2. Инициализиране на променливи на класа (Initializing Member Variables). За съжаление следният запис по-долу е невалиден

Stack
{
  private:
    const int size = 20;  // illegal initialization!!!
    int st[size];
    int top;
  public:
  . . .

};

и компилаторът издава съобщение

 Cannot initialize a class member here.

тъй като променливите на класа трябва се инициализират от списък за инициализация (initialization list), както това е обясненено в лекция 32. Причина за това е, че грешното твърдение по-горе е опит една променлива на класа да бъде инициализирана с една и съща стойност, която тя ще има във всички обекти на класа. Това последното е в разрез с концепцията за ООП, защото един обект се инициализира точно когато се създава.

Разбира се, може да инициализираме променливата size в конструктора по следния начин

  Stack() : top(-1), size(20)       // constructor
  {   }

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

  int st[size];

Даже и да запишем в конструктора, че size е константа по следния начин

class Stack            // a stack holds up to 20 ints
{
   private:
     const int size;   // array size; mention that it is defined as constant!!!
     int st[size];     // the constructor complains "Constant expression required."!!!
     int top;          // index of last item pushed

   public:
     Stack() : top(-1), size(20) // constructor with an initialization of size!
     {   }

   . . .

};

то компилаторът издава съобщение

Constant expression required.

Тук трябва да се отбележи, че инициализацията не е присвояване (Initialization Is Not Assignment) и затова компилаторът не се оплаква, че инициализираме константата size, както това би станало ако в конструктора сложим твърдението size = 20;, както е по-долу:

class Stack                   // a stack holds up to 20 ints
{
   private:
     const int size;  // array size
     int st[size];           // integers are stored in array
     int top;                // index of last item pushed

   public:
     Stack() : top(-1)       // constructor
     {
       size = 20;
     }

   . . .

}

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

Constant member 'Stack::size' is not initialized.

Cannot modify a const object.

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

3. Инициализация с изброен тип (The enum Hack). Не случайно Лафоор е употребил думата Hack [1], която означава "справям се с", (което дава името на хакерството), защото това не е най-елегантния начин за справяне с проблема, а просто едно негово заобикаляне. Ето и частта от класа, която използва изброения тип (enum), за да се справи с проблема. (За изброените типове данни вижте лекция 22).

class Stack
{
  private:
    enum { size = 20 };  // established at compile time
    int st[size];        // OK
    int top;

  . . .

};

Този начин работи, защото компилаторът просто заменя името size с числото 20. Както споменахме, този начин не е елегантен и създава затруднения за бързото му разбиране от друг програмист, ако програмата е голяма. Ето защо създателите на C++ са допълнили ANSI/ISO версията с друга възможност.

4. Инициализация със статична константа (Static Constant Variables). Това е едно разширение на езика, което за съжаление според Лафоор [1] още не се поддържа от всички компилатори. Ето и кодът, който пояснява тази инициализация:

class Stack
{
  private:
    static const int size = 20;  // static constant
    int st[size];
    int top;

  . . .

}

Константата size не може да се променя (както и искаме!), но понеже е статична, тя се инициализира по време на компилацията и затова може да се използва за оразмеряване на масива st. Статични променливи на класа бяха въведени в лекция 28.

Ето и променената програма staticon.cpp, която върши същото като предишната програма stackcon.cpp от лекция 31.

// staticon.cpp
// class models a stack
// uses static constant for member array size
#include <iostream.h>
#include <conio.h>            // for getch()

class Stack                   // a stack holds up to 20 ints
{
   private:
      static const int size = 20;  // array size
      int st[size];           // integers are stored in array
      int top;                // index of last item pushed

   public:
      Stack() : top(-1)       // constructor
      {   }

      void push(int var)      // place an item on the stack
      {
        if(top >= size - 1)                // if at or beyond the end of array,
          cout << "Illegal push";    // error message
        else
          st[++top] = var;
      }

      // remove an item from the stack
      int pop()
      {
        if (top < 0)                                 // if beyond the beginning of array,
        {
          cout << "Cannot pop: stack is emty! ";     // error message
          return 0;
        }
        else
          return st[top--];
      }
};  // class Stack

void main()
{
   Stack s1;                  // create a stack object
   s1.push(11);               // push 3 items onto stack
   s1.push(12);
   s1.push(13);

   cout << s1.pop() << endl;  // pop 3 items and display them
   cout << s1.pop() << endl;
   cout << s1.pop() << endl;

   getch();
}   // main()

Обърнете внимание на защитата if(top >= size - 1), която проверява дали сме на края на масива top = size - 1 (той започва от нула!) или сме след последната стойност top > size - 1. Както и на защитата във функцията pop(): редът return 0; в нея е необходим, защото cout се опитва да върне някаква стойност и ако го няма този ред, връща произволна стойност, което е объркващо.

5. Инициализация на масив, който е променлива на класа (Initializing a Member Array). Масивите не могат да се инициализират нито чрез инициализиращ списък (initialization list) в конструктора, нито директно като променливи на класа.

В програмата weekdays.cpp от лекция 20 масивът от символи day_name беше дефиниран като глобална константа и инициализиран преди класа weekday. Но той е част от класа и е правилно да бъде дефиниран вътре в него. За съжаление, следните две конструкции са неверни и не могат да бъдат компилирани:

// FIRST INCORRECT construction
class weekday
{
  private:
    // no good
    char day_name[DPW][MAX] = { "Sunday",    "Monday",   "Tuesday",
                                "Wednesday", "Thursday", "Friday",
                                "Saturday"};

  . . .

};  // end of class weekday

дава съобщението

 Cannot initialize a class member here.

// SECOND INCORRECT construction
class weekday
{
  private:
    char day_name[DPW][MAX]

  public:
    // no good!!!
    weekday() : day_name[0]("Sunday"),   day_name[1]("Monday"),
                day_name[2]("Tuesday"),  day_name[3]("Wednesday"),
                day_name[4]("Thursday"), day_name[5]("Friday"),
                day_name[6]("Saturday")
    { }

  . . .

};  // end of class weekday

дава съобщенията (вижда се, че са повторени няколко пъти)

( expected.
Member 'weekday::day_name' is initialized more than once.
( expected.
Member 'weekday::day_name' is initialized more than once.
( expected.
Member 'weekday::day_name' is initialized more than once.
( expected.
Member 'weekday::day_name' is initialized more than once.
( expected.
Member 'weekday::day_name' is initialized more than once.
( expected.
Member 'weekday::day_name' is initialized more than once.
( expected.

При втория фрагмент от код повторенията се дължат на това, че компилаторът не очаква средните скоби след day_name, а иска веднага кръгла скоба (с инициализиращата стойност след нея). Също така, срещането на day_name[0], day_name[1] и т.н. се разбира от компилаторът като срещане на няколко пъти на името day_name и съответно неговото повторно инициализиране в инициализиращия списък на конструктора.

Проблемът може да бъде решен, ако променливата day_name бъде дакларирана като статична и инициализирана след декларацията на класа по следния начин:

class weekday                         // class of day names
{
   private:
      int day_number;                 // Sunday = 0, etc.
      static const char day_name[DPW][MAX];
  . . .

};  // end of class weekday

const char weekday::day_name[DPW][MAX] = {"Sunday",  "Monday",
                                          "Tuesday", "Wednesday",
                                          "Thursday", "Friday",
                                          "Saturday" };

Ето и цялата програма, която прави същото както старата:

// weekdays1.cpp
// creates a class of days of the week objects

#include <iostream.h>
#include <string.h>     // for stricmp()
#include <conio.h>      // for getche()

class weekday                         // class of day names
{
   private:
      int day_number;                 // Sunday = 0, etc.
      static const int MAX = 10;     // maximum length of day name, +1
      static const int DPW = 7;      // days per week
      static const char day_name[DPW][MAX];

   public:
      void inday()                    // user inputs day name
      {
         char tempday[MAX];           // holds user input
         int gotit = 0;               // match found? (0=false)
         int j;                       // loop variable

         while( !gotit )              // cycle until user enters
         {                            // a correct name
            cout << "Enter day of week (e.g., Friday): ";

            cin >> tempday;
            for (j = 0; j < DPW; j++)      // compare user input
            {                              // with list of names
               if (stricmp(tempday, day_name[j]) == 0)
               {                   // if there's a match,
                  gotit = 1;          // set flag,
                  break;              // break out of for loop
               }
            }  // end for

            day_number = j;           // record what day it was
         }  // end while
      }  // void inday()

      void outday()                   // display the day name
      {
         cout << day_name[day_number];
      }

      void outnumber()                // display the day number
      {
         cout << (day_number + 1);
      }

      void add(int days)             // add days to
      {                              // this weekday
         day_number += days;         // add days
         day_number %= DPW;          // ensure not > 7
      }
};  // end weekdays class

const char weekday::day_name[DPW][MAX] = {"Sunday",  "Monday",
                                          "Tuesday", "Wednesday",
                                          "Thursday", "Friday",
                                          "Saturday" };

////////////////////////////////////////////////////////////////
void main()
{
   weekday wd;                      // make a weekday object

   cout << "What day is it?" << endl;
   wd.inday();                      // user gives it a value

   cout << "You entered ";
   wd.outday();                     // display its day name

   cout << "\nThat's day number ";
   wd.outnumber();                  // display its day number

   wd.add(10);                      // add 10 days to it
   cout << "\nTen days later is ";
   wd.outday();                     // display its day name

   // stop the flow on the monitor
   getch();

}  //end main

Обърнете внимание, че константите MAX и DPW също са обявени като статични:

      static const int MAX = 10;     // maximum length of day name, +1
      static const int DPW = 7;      // days per week

За да осмислите напълно използването на статичните константи в горната програма припомнете си лекция 28 за статични променливи на класа.
.

(съдържание)
.
Литература
.
[1] Robert Lafore; C++ Interactive Course. Waite Group Press, Macmillan Computer Publishing, 1996.
.
Автор: Проф. Процесор Аритметиков
.
[ това е материал от брой 33 от септември 2009 г на списание "Коснос" www.kosnos.com ]

Keywords: С++,  OOP programming , C++ , Classes , Inheritance , Reusability , Creating New Data Types , Polymorphism and Overloading
Ключови думи: клас , обект, обектно ориентирано програмиране , полиморфизъм switch if else ?
Arrays as Instance Data
Array Sized with Constant
Static Constant Variables
The enum Hack
Arrays Sized at Compile Time
Initialization Is Not Assignment
Instance Data Initialization