БлогNot. C++: пишем маленькие классы

C++: пишем маленькие классы

Просто несколько учебных примеров на классы под общим лозунгом "КРТКСТ - СТ".

1. Переопределение операторов - удобная и красивая вещь, на неё в блоге есть и теория, и примеры. Тем не менее, вот ещё один маленький пример - научим программу складывать объекты класса c.

В простых случаях достаточно, чтобы переопределённый оператор возвращал новый объект класса, тогда будет работать и сложение "по цепочке" вида a+b+... Аргумент слева от знака "+" доступен через указатель this, справа - через ссылочный аргумент оператора b:

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

class c {
public:
 double val;
 c(double val = 0);
 c operator + (c&);
};
c::c(double val) { this->val = val; }
c c::operator + (c& b) {
 c d(this->val + b.val); return d;
}

int main(void) {
 c a(1), b(2), d(3);
 c* e = new c(); *e = a + b + d; cout << e->val;
 c f; f = a + b + d; cout << endl << f.val;
 cin.get(); return 0;
}

Правильные теоретики также часто рекомендуют делать для бинарных операторов дружественные функции:

friend c operator + (const c & a, const c & b); //прототип в классе

c operator + (const c& a, const c& b) { //реализация
 return c (a.x + b.x);
}

2. Класс "Человек" с условием задачи:

  • Свойства: Имя; Год рождения
  • Метод(ы): Показать информацию; Показать текущий возраст в полных годах
  • Конструктор(ы): С заданными свойствами
  • Оператор(ы): < (достиг ли возраст указанного значения); += (изменить имя на новую строку)

Чтобы не заморачиваться с получением текущего года из <ctime>, сделаем его статическим членом класса, который, как положено, инициализируем вне всех функций и до main. Оператор "<" определим дважды, для возможности сравнения как с целым числом, так и с возрастом, взятым из другого экземпляра класса.

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

class c {
 string name;
 int age; //имя и год рождения
public:
 static int year; //статическое св-во "текущий год"
 void show(); //показать информацию
 int getAge(); //показать возраст в годах
 c(string name = 0, int age = 0);
 c& operator += (string); //переопределить имя
 bool operator < (int); //достиг ли возраст значения
};
int c::year = 2021;
void c::show() { cout << endl << name << ", " << age; }
int c::getAge() { return year - age + 1; }
c::c(string name, int age) { this->name = name; this->age = age; }
c& c::operator += (string b) {
 this->name = b; return *this;
}
bool c::operator < (int b) { return getAge() <= b; }

int main(void) {
 c a("Ivanov", 2000); a.show();
 string s("Petrov"); a += s; a.show();
 bool b = a < 30;
 cout << endl << (b ? "1" : "0");
 cout << endl; system("pause"); return 0;
}

3. Класс "Стек из целых чисел" с условем задачи:

  • Свойства: Размерность; Элементы
  • Метод(ы): Добавить элемент в начало стека; Извлечь элемент из начала стека
  • Конструктор(ы): С заданным количеством элементов
  • Оператор(ы): ! (Текущее количество элементов в стеке)

Для удобства добавим в стек ещё и свойство "текущее количество элементов" current.

Если элемент нельзя положить в стек из-за заполненности, не будем делать ничего (хотя можно было что-то возвращать из метода push).

Если элемент нельзя извлечь потому, что стек пуст, будем возвращать INT_MAX.

Тогда реализация окажется простой.

#include <iostream>
#include <string>
#include <climits>
using namespace std;
class c {
 int size; //размер
 int* items; //элементы
 int current; //текущая позиция
public:
 void push(int); //положить элемент
 int pop(); //взять элемент
 c(int size = 2);
 ~c();
 int operator !(void); //текущая емкость
};
c::c(int size) {
 this->size = size; current = 0;
 items = new int[size];
}
c::~c() { if (size > 0) delete [] items; }
int c::operator !(void) { return current; }
void c::push(int item) {
 if (current < size) items[current++] = item;
}
int c::pop(void) {
 return current > 0 ? items[--current] : INT_MAX;
}

int main(void) {
 c a(3);
 a.push(1); a.push(2); a.push(3);
 cout << a.pop() << ",";
 cout << a.pop() << ",";
 cout << a.pop(); a.push(4);
 cout << endl << !a;
 cout << endl; cin.get(); return 0;
}

Страшное случится, когда мы это запустим. Вместо ожидаемого порядка извлечения "3, 2, 1" мы увидим на экране консоли:

1,2,3
0
Для продолжения нажмите любую клавишу . . .

Если заменить оператор, помеченный комментарием //!!! на

cout << a.pop() << ","; cout << a.pop() << ",";  cout << a.pop();

то всё будет нормально:

3,2,1
0
Для продолжения нажмите любую клавишу . . .

Такие вещи способны свести с ума, пока не осознаешь некоторых концепций:

Коротко:
Нельзя рассчитывать на то, аргументы функции вычисляются слева направо.
Вызов функции, изменяющей некие величины, не нужно ставить в одном операторе с изменяемыми величинами!

4. Класс "Строка" с условием задачи:

  • Свойства: Текст
  • Метод(ы): Показать информацию; Очистить текст
  • Конструктор(ы): По умолчанию; С заданным значением
  • Оператор(ы): + (сцепление текстов любого количества строк); == (узнать текущую длину строки)

Так как здесь мы именно моделируем строку, готовый класс string не уместен, но используется классический char *. Кроме текста строки, конечно, введём и свойство "размер". "Очищать" строку будем, просто зануляя первый байт. Как и в предыдущим примерах, для краткости не пишем явный деструктор и конструктор копирования.

Переопределённый оператор == не будет использовать свой правый аргумент, ну и что? Переопределённый постфиксный аргумент тоже не использует, но работает же :)

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>
using namespace std;

class c {
 int size; char *text; //длина и сам текст
public:
 void show() { cout << endl << text; } //показать
 void clear() { size = 0; text[0] = '\0'; } //очистить
 c(int size = 0) { //конструктор по умолчанию
  this->size = size;
  if (size > 0) text = new char[size + 1];
  else text = NULL;
 }
 c(const char* text) { //конструктор с заданным знач.
  int size = strlen(text);
  if (size) {
   this->text = new char[size + 1];
   strcpy(this->text, text);
  }
  else this->text = NULL;
 }
 c operator + (c& b) { //сцепление любого кол-ва строк
  int len = strlen(this->text) + strlen(b.text);
  c d (len);
  strcpy (d.text, this->text);
  strcat (d.text, b.text);
  return d;
  //почему возвращаем объект стека, а не "кучи"?
 }
 int operator == (int b) { return strlen(text); };
 //почему не стоит создавать здесь явный деструктор?
};

int main(void) {
 c a("Stroka"), b("Esche"), d("i esche");
 c e = a + b + d; e.show();
 int f = (e == 0); cout << endl << f;
 cout << endl; cin.get(); return 0;
}

Вариант, при котором код работает и при явном деструкторе с возвращением объекта из бинарного оператора (только изменённые и новые методы). Отличия в поведении компилятора объясняются тем, что в данном случае он вернёт новый объект через стек и не удалит его автоматически по выходе из области видимости переменной.

c(const char* text, const char* text2) {
  int len = strlen(text) + strlen(text2);
  if (len) {
   size = len;
   this->text = new char[size + 1];
   strcpy(this->text, text);
   strcat(this->text, text2);
  }
 }
 c operator + (c& b) { //сцепление любого кол-ва строк
  return c(this->text,b.text);
 }
 ~c() { if (size && text) delete[] text; }

Я проверял это всё в QT, но и в Studio не должно быть разницы, кроме его склонности ругаться на "устаревшие" функции вроде strlen. На самом деле, устарели не функции, а Microsoft, вынужденный латать стандарты в поисках совместимости с эпохой однобайтовых кодировок и восьмибитных игрушек :)


теги: c++ учебное программирование

10.04.2017, 22:39; рейтинг: 2428