Nickolay.info. Алгоритмы. Примеры на работу с классами в C++

Эти примеры, точней, своего рода образцы трёх лабораторных работ по C++, показывают работу с классами на этом языке. Проверено на Borland C++ 3.1, до сих пор часто используемом в учебных целях, возможно, код неоптимален, так как делался в спешке...

В первом примере требуется создать класс "Служащий" со свойствами "Имя", "Возраст" и "Рабочий стаж", а также всеми необходимыми методами. Все описания сделаем в заголовочном файле Lab1.h

Во-первых, определим условную директиву для предотвращения повторного включения заголовочного файла в проект:

#ifndef EMPLOYEEH 
#define EMPLOYEEH 
//здесь будет всё описание класса
#endif

Теперь определим поля класса, их всего три, причём поле name для хранения имени будет указателем. Это значит, что в конструкторе или другом методе класса мы предусмотрим выделение оперативной памяти под строку, адресованную этим указателем:

class EMPLOYEE {
  private:
   char *name;
   int age;
   int job;

В секции public разместим заголовки нескольких конструкторов и деструктора. Первый конструктор встроен, а конструктор копирования (реализация присваивания объектов класса EMPLOYEE) будет задан явно. Это нужно потому, что в противном случае EMPLOYEE A=B присвоит свойству A.name только указатель на B.name, и, например, при разрушении объекта B, строка, содержащая его имя, будет потеряна.

public:
   EMPLOYEE(): name (NULL), age (0), job (0) {}
   // конструктор без параметров, тело конструктора встроено
   EMPLOYEE (char*,int,int); // конструктор с параметрами 
   EMPLOYEE (const EMPLOYEE&); // конструктор копирования 
   ~EMPLOYEE(); //деструктор

Поскольку все основные свойства класса - приватные, понадобятся методы для их получения:

//методы для получения свойств класса:
   char * getname(); 
   int const getage(); 
   int const getjob();

а также методы для их установки:

//методы для установки свойств класса:
   void setname (char*); 
   void setage (int); 
   void setjob (int); 
   void set (char*,int,int);

наконец, методы вывода будут печатать значения свойств без заголовка и с заголовком, переданным как параметр:

void show();
   void show(char *);
 };

В файле Lab1.cpp реализуем перечисленные свойства и методы. Для этого подключим заголовки функций стандартных библиотек и файл Lab1.h с описанием класса:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#include "Lab1.h"

Оба конструктора будут делать, в сущности, одно и то же, только конструктор копирования будет брать данные из объекта-параметра, а обычному конструктору они будут переданы как отдельные формальные параметры:

EMPLOYEE :: EMPLOYEE (char *name, int age, int job) {
 setname (name);
 setage (age);
 setjob (job);
}
 
EMPLOYEE :: EMPLOYEE (const EMPLOYEE &src) {
 setname (src.name);
 setage (src.age);
 setjob (src.job);
}

Деструктор просто освобождает память, выделенную под строку name:

EMPLOYEE :: ~EMPLOYEE () {
 free (name);
}

Методы получения свойств текущего объекта (в C++ он доступен через специальный указатель this) также очень просты:

char * EMPLOYEE :: getname () { return this->name; }
int EMPLOYEE :: getage () { return this->age; }
int EMPLOYEE :: getjob () { return this->job; }

Метод setname выделит память под имя, переданное параметром:

void EMPLOYEE :: setname (char *name) {
 int n = strlen (name);
 if (n>0) {
  if (this->name) free (this->name);
  this->name = (char *) calloc (strlen(name),sizeof(char));
  if (this->name) {
   strcpy (this->name,name);
  }
 }
}

А остальными методам установки свойств класса достаточно простого присваивания:

void EMPLOYEE :: setage (int age) { this->age=age; }
void EMPLOYEE :: setjob (int job) { this->job=job; }

Метод set, не создавая нового объекта, может поменять свойства у объекта существующего:

void EMPLOYEE :: set (char *name,int age,int job) {
 setname (name);
 setage (age);
 setjob (job);
}

Оба метода show, думаю, также вполне понятны:

void EMPLOYEE :: show () {
 printf ("Name=%s,Age=%d,Job=%d\n",name,age,job);
}
 
void EMPLOYEE :: show (char *hdr) {
 puts (hdr); show();
}

Наконец, напишем файл lab1demo.cpp, который продемонстрирует работу созданного класса. Чтобы не зависеть от формата файла проекта, принятого в том или ином конкретном компиляторе C++, подключим файл Lab1.cpp также директивой #include:

#include <stdio.h>
#include "Lab1.cpp"

Основные возможные действия с объектами класса EMPLOYEE иллюстрируются далее. Например, конструктор копирования явно или неявно вызывается:

Объекты могут размещаться как в статической, так и в динамической памяти, также из них можно создавать массивы. Наконец, бывает нужна и такая экзотика, как вызов метода объекта через указатель на компонентную функцию.

void view (EMPLOYEE e) {
 e.show ("Передача объекта функции по значению:");
}
 
EMPLOYEE temp (EMPLOYEE &e) {
 EMPLOYEE Noname(e);
 Noname.setname ("Noname");
 return Noname;
}
 
int main () {
 EMPLOYEE Popov("Popov",30,5);
 Popov.show("Запись \"студент Попов\":");
 EMPLOYEE Popov2 = Popov;
 Popov2.show("Копия для записи \"Попов\":");
 view (Popov);
 EMPLOYEE Noname = temp (Popov2);
 Noname.show ("Построение временного объекта как возвращаемого значения:");
 
 EMPLOYEE firm[3];
 char *Names [] = { "Иванов", "Петров", "Сидоров" };
 char buf[80];
 for (int i=0; i<3; i++) {
  firm[i].set (Names[i],20+i*5,1+i*2);
  sprintf(buf,"Элемент статического массива %d",i);
  firm[i].show (buf);
 }
 
 EMPLOYEE *firm2 = new EMPLOYEE [3];
 for (i=0; i<3; i++) {
  firm2[i].set (Names[i],20+i*5,1+i*2);
  sprintf(buf,"Элемент динамического массива %d",i);
  firm2[i].show (buf);
 }
 
 void (EMPLOYEE::*pf)(char *)=&EMPLOYEE::show;
 (firm2[2].*pf)("Вызов через указатель на компонентную функцию");
 
 return 0;
}

Скачать код примера можно по ссылке:

 Файлы примера 1 в архиве ZIP (12 Кб)

Второй пример показывает иерархию классов. Например, напишем класс "Животное" (ANIMAL) с дочерними от него "Млекопитающим" (MAMMAL) и "Птицей" (BIRD), а у млекопитающего будет ещё класс-наследник "Парнокопытное" (HOOFED). Класс ANIMAL будет абстрактным, то есть, содержащим виртуальный метод show. Все классы-наследники будут использовать собственные реализации этого метода. Кроме того, почти всегда делают виртуальным деструктор базового класса - иначе можно потерять оперативную память при освобождении её от классов-потомков.

Впрочем, в ООП всегда есть где потерять оперативку и без иерархии деструкторов. В нашем примере будет также поддерживаться список из объектов разных классов, для чего в базовом абстрактном классе мы определим несколько статических (static, единых для всех объектов класса) свойств, которые помогут нам поддерживать список из объектов разных классов. Будет в классе ANIMAL и статическая (также единственная для всех экземпляров класса) функция print, чтобы показать этот список. Всё остальное - из синтаксиса C++ и предыдущего примера.

Файл Lab2.h - описание классов:

#ifndef ANIMALH
#define ANIMALH
 class ANIMAL {
  protected:
   char *name;
  public:
   ANIMAL(): name (NULL) {}
   ANIMAL (char *);
   ANIMAL (const ANIMAL&);
   virtual ~ANIMAL() {};
   char * getname();
   void setname (char *);
   virtual void show (void) = 0;
   static void print(void); // просмотр списка
   void add ();
   static int count;
   static ANIMAL **animals;
   static ANIMAL *begin; // указатель на начало списка
 };
 class MAMMAL : public ANIMAL {
 public:
  MAMMAL(): ANIMAL(NULL) {}
  MAMMAL (char *);
  ~MAMMAL() { free (name); }
  void show (void);
 };
 class BIRD : public ANIMAL {
 public:
  BIRD(): ANIMAL(NULL) {}
  BIRD (char *);
  ~BIRD() { free (name); }
  void show (void);
 };
 class HOOFED : public MAMMAL {
 public:
  HOOFED(): MAMMAL(NULL) {}
  HOOFED (char *);
  ~HOOFED() { free (name); }
  void show (void);
 };
 //Все статические данные инициализируем вне описания класса
 int ANIMAL :: count = 0;
 const int size = 4; //Предельный размер списка
 ANIMAL ** ANIMAL::animals = new ANIMAL * [size];
 ANIMAL * ANIMAL ::begin =  ANIMAL ::animals[0];
  //Не используется, просто для иллюстрации
#endif

Файл Lab2.cpp - реализации:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#include "Lab2.h"
 
ANIMAL :: ANIMAL (char *name) {  setname (name); }
ANIMAL :: ANIMAL (const ANIMAL &src) { setname (src.name); }
char * ANIMAL :: getname () { return this->name; }
 
void ANIMAL :: setname (char *name) {
 int n = strlen (name);
 if (n>0) {
  //Вот тут-то можно и потерять память, как обычно в ООП
  this->name = (char *) calloc (strlen(name),sizeof(char));
  if (this->name!=NULL) strcpy (this->name,name);
 }
}
 
void ANIMAL :: add () {
 if (count<size) {
  animals[count++] = this;
  if (count<size) animals[count] = NULL;
 }
}
 
void ANIMAL :: print () {
 printf ("Всего: %d\n",count);
 for (int i=0; i<count; i++)
  if (animals[i]!=NULL) animals[i]->show();
}
 
MAMMAL :: MAMMAL (char *name) : ANIMAL (name) {}
void MAMMAL :: show () { printf ("MAMMAL: %s\n",name); }
 
BIRD :: BIRD (char *name) : ANIMAL (name) {}
void BIRD :: show () { printf ("BIRD: %s\n",name); }
 
HOOFED :: HOOFED (char *name) : MAMMAL (name) {}
void HOOFED :: show () { printf ("HOOFED: %s\n",name); }

Наконец, демка - файл Lab2demo.cpp. Здесь мы создаём список из кота, вороны и лося, показываем его, потом всех удаляем :)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#include "Lab2.cpp"
 
void main () {
 MAMMAL *cat = new MAMMAL ("Cat");
 cat->show();
 
 BIRD *crow = new BIRD ("Crow");
 crow->show();
 
 HOOFED *elk = new HOOFED ("Elk");
 elk->show();
 
 cat->add();
 crow->add();
 elk->add();
 
 ANIMAL::print(); //Печатает весь список
 
 delete elk;
 delete crow;
 delete cat;
}

 Файлы примера 2 в архиве ZIP (10 Кб)

Ещё одна полезная возможность ООП в C++ - переопределение операторов. Действительно, неплохо писать C=A*B, если A и B - матрицы. Только предварительно, конечно, придётся сделать реализацию этого оператора умножения.

Но сначала, чтобы примеры не были слишком большими, напишем что-нибудь простенькое. Скажем, определим класс "точка на плоскости" и в нём - постфиксный и префиксный инкремент и декремент. Исключительно в иллюстративных целях префиксные операторы сделаны не членами, но "друзьями" (friend) класса - то есть, они не являются функциями класса, но имеют доступ к его свойствам. На практике использовать "друзей" не рекомендуется вообще - если уж следовать странной концепции ООП, предполагающей максимальную изолированность классов, то зачем эту изолированность так грубо нарушать?

В программе также определены бинарные "+" и "-", то есть, обычное сложение и вычитание координат точек, и унарный "-" (смена знака). Путаницы не возникает, так как для одного оператора может быть несколько определений, отличающихся формальными параметрами. Кстати, согласно документации по C++, определение постфиксных операций ++ и -- должно иметь последним параметром функции-оператора целое значение (int). Так как это значение нигде не используется, мы его и не именовали. На этот раз всё описано в одном файле.

#include <stdio.h>
 
class Point {
 public:
  double x,y;
  Point(double x0 = 0.0, double y0 =0.0) : x (x0), y (y0) {};
   //Так можно встроить конструктор + сразу задать значения параметров по умолчанию + 
   //присвоить параметры свойствам класса
  Point operator ++ (int);
  Point operator -- (int); //постфиксные
  friend Point operator ++ (Point &);
  friend Point operator -- (Point &); //префиксные
  Point operator + (Point &); //бинарные
  Point operator - (Point &);
  Point operator - (); //унарный
  void show (char *);
};
 
Point Point::operator - () { return Point(-x,-y); }
 
Point Point::operator ++ (int) {  return Point(x++,y++); }
 
Point Point::operator -- (int) {  return Point(x--,y--); }
 
Point operator ++ (Point &p) { return Point(++p.x,++p.y); }
 
Point operator -- (Point &p) { return Point(--p.x,--p.y);  }
 
Point Point::operator+ (Point &p) { return Point (x + p.x, y + p.y); }
 
Point Point::operator- (Point &p) { return Point (x - p.x, y - p.y); }
 
void Point:: show (char *s) { puts(s); printf ("x=%.2lf,y=%.2lf\n",x,y); }
 
void main () {
 Point origin; //Получилось (0,0) - очевидно, почему :)
 
 Point p1= Point (1,2);
 p1.show ("p1");
 Point p2=origin + p1++;
 p2.show("0+p1++");
 p1.show ("p1 изменилось");
 p1--;
 p2= origin + ++p1;
 p2.show("0+ ++p1");
 
 p1 = Point (1,2);
 p1.show ("New p1");
 p2=origin - p1--;
 p2.show("0-p1--");
 p1++;
 p2= origin - --p1;
 p2.show("0- --p1");
 
 Point p3=-Point(1,-4);
 p3.show("-(1,-4)");
}

Для класса целочисленных матриц Matrix тоже сделаем несколько определений операторов, позволяющих складывать, вычитать и умножать матрицы с другими матрицами и с числами. Выделение и освобождение памяти на этот раз сделаем операторами C++ new и delete, а не взятыми из классического Си функциями calloc и free. В остальном идея выделения памяти под матрицу та же, что в этой главе учебника по Си (п. 8.5).

Для простоты, как и в других примерах, не проверяется корректность данных. Унарные операторы имеют тип void, так как непосредственно изменяют свой объект, бинарные операторы получают и возвращают новый объект Matrix.

Не обойтись в такой программе и без явного конструктора копирования плюс деструктора, грамотно (в порядке, обратном выделению памяти) удаляющего динамические данные объекта. В противном случае рискуем получить "Null pointer assignment" (следствие работы с указателем, как с массивом данных - например, когда указатель не проверен на NULL или объект, к которому мы обращаемся, был уже разрушен).

#include <stdio.h>
 
class Matrix {
 public:
  int **c;
  int n,m;
  Matrix () : n(0), m(0) {};
  Matrix (int r=2);
  Matrix (int n0=2, int m0=2);
  Matrix (const Matrix &);
  ~Matrix();
  void operator = (Matrix &);
  void operator += (Matrix &);
  void operator -= (Matrix &);
  void operator *= (Matrix &);
  void operator += (int);
  void operator -= (int);
  void operator *= (int);
  Matrix operator + (Matrix &);
  Matrix operator - (Matrix &);
  Matrix operator * (Matrix &);
  Matrix operator + (int);
  Matrix operator - (int);
  Matrix operator * (int);
  int * operator [] (int i) { return c[i]; }
  void show (char *);
};
 
Matrix::Matrix (int r) {
 new Matrix (r,r);
}
 
Matrix::Matrix (int n0,int m0) {
 n=n0; m=m0;
 c= new int * [n];
 for (int i=0; i<n; i++)
  c[i]=new int [m];
}
 
Matrix::~Matrix () {
 for (int i=n-1; i>-1; i--) delete c[i];
 delete c;
}
 
void Matrix::operator = (Matrix &p) {
 n=p.n; m=p.m;
 for (int i=0; i<n; i++) for (int j=0; j<m; j++) c[i][j]=p.c[i][j];
}
 
Matrix::Matrix (const Matrix &p) {
 n=p.n; m=p.m;
 c= new int * [n];
 for (int i=0; i<n; i++) c[i]=new int [m];
 for (i=0; i<n; i++) for (int j=0; j<m; j++) c[i][j]=p.c[i][j];
}
 
Matrix Matrix::operator + (Matrix &p) {
 Matrix r=Matrix(n,m);
 int i,j;
 for (i=0; i<n; i++)
 for (j=0; j<m; j++) r.c[i][j]=c[i][j]+p.c[i][j];
 return r;
}
 
Matrix Matrix::operator - (Matrix &p) {
 Matrix r=Matrix(n,m);
 int i,j;
 for (i=0; i<n; i++)
 for (j=0; j<m; j++) r.c[i][j]=c[i][j]-p.c[i][j];
 return r;
}
 
Matrix Matrix::operator * (Matrix &p) {
 Matrix r=Matrix(n,p.m);
 int i,j,k;
 for (i=0; i<n; i++)
 for (j=0; j<p.m; j++) {
  r.c[i][j]=0;
  for (k=0; k<m; k++) r.c[i][j]+=c[i][k]*p.c[k][j];
 }
 return r;
}
 
Matrix Matrix::operator + (int p) {
 Matrix r=Matrix(n,m);
 int i,j;
 for (i=0; i<n; i++)
 for (j=0; j<m; j++) r.c[i][j]=c[i][j]+p;
 return r;
}
 
Matrix Matrix::operator - (int p) {
 Matrix r=Matrix(n,m);
 int i,j;
 for (i=0; i<n; i++)
 for (j=0; j<m; j++) r.c[i][j]=c[i][j]-p;
 return r;
}
 
Matrix Matrix::operator * (int p) {
 Matrix r=Matrix(n,m);
 int i,j;
 for (i=0; i<n; i++)
 for (j=0; j<m; j++) r.c[i][j]=c[i][j]*p;
 return r;
}
 
void Matrix::operator += (Matrix &p) {
 int i,j;
 for (i=0; i<n; i++)
 for (j=0; j<m; j++) c[i][j]+=p.c[i][j];
}
 
void Matrix::operator -= (Matrix &p) {
 int i,j;
 for (i=0; i<n; i++)
 for (j=0; j<m; j++) c[i][j]-=p.c[i][j];
}
 
void Matrix::operator *= (Matrix &p) {
 Matrix r=Matrix(p.n,p.m);
 for (int i=0; i<p.n; i++)
 for (int j=0; j<p.m; j++) {
  r.c[i][j]=0;
  for (int k=0; k<m; k++) r.c[i][j]+=c[i][k]*p.c[k][j];
 }
 for (i=0; i<p.n; i++)
 for (int j=0; j<p.m; j++) c[i][j]=r.c[i][j];
}
 
void Matrix::operator += (int p) {
 int i,j;
 for (i=0; i<n; i++)
 for (j=0; j<m; j++) c[i][j]+=p;
}
 
void Matrix::operator -= (int p) {
 int i,j;
 for (i=0; i<n; i++)
 for (j=0; j<m; j++) c[i][j]-=p;
}
 
void Matrix::operator *= (int p) {
 int i,j;
 for (i=0; i<n; i++)
 for (j=0; j<m; j++) c[i][j]*=p;
}
 
void Matrix::show (char *s) {
 puts (s);
 int i,j;
 for (i=0; i<n; i++) {
  for (j=0; j<m; j++) printf ("%d ",c[i][j]);
  printf ("\n");
 }
}
 
void main () {
 Matrix a(2,2);
 a[0][0]=1; a[0][1]=2;
 a[1][0]=3; a[1][1]=4;
 a.show ("Заполнение матрицы A" );
 Matrix b(2,2);
 b[0][0]=2; b[0][1]=0;
 b[1][0]=1; b[1][1]=3;
 b.show ("Заполнение матрицы B" );
 Matrix c=b+a;
 c.show ("C=B+A");
 Matrix d = a + 1;
 d.show ("D=A+1");
 Matrix e = a * b;
 e.show ("E=A*B");
 a*=2;
 a.show ("A*=2");
 b+=b;
 b.show ("B+=B");
 b-=b*2;
 b.show ("B-=B*2");
 d*=d;
 d.show ("D*=D");
}

 Файлы примера 3 в архиве ZIP (28 Кб)

P.S. Для операции *= (домножение на матрицу) реализация сомнительна, если размерности матриц - разные. В самом деле, что означает "домножить матрицу F[3][2] на матрицу T[2][3]? Если это означает "получить новую матрицу размером 3 на 3 и потом переписать в F ту часть, что переписывается", тогда будет такая изменённая реализация функции:

void Matrix::operator *= (Matrix &p) {
 Matrix r=Matrix(n,p.m);
 for (int i=0; i<n; i++)
 for (int j=0; j<p.m; j++) {
  r.c[i][j]=0;
  for (int k=0; k<m; k++) r.c[i][j]+=c[i][k]*p.c[k][j];
 }
 for (i=0; i<n; i++)
 for (int j=0; j<m; j++) c[i][j]=r.c[i][j];
}

Пример вызова:

Matrix f(3,2); int i,j;
for (i=0; i<3; i++) for (j=0; j<2; j++) f[i][j]=i+j;
f.show ("F");
Matrix t(2,3);
for (i=0; i<2; i++) for (j=0; j<3; j++) t[i][j]=j;
t.show ("T");
f*=t;
f.show ("F*=T");

Рейтинг@Mail.ru

вверх гостевая; E-mail