C++. Розробка класу, що реалізує масив рядків типу char*




Розробка класу, що реалізує масив рядків типу char*

У мові C++ рядок можна представити типом string або типом char*. Тип string є класом і має потужний арсенал зручних засобів для обробки рядків. Робота з рядками типу char* часто викликає труднощі. У даній темі продемонстровано клас ArrayPChar, який представляє масив рядків типу char*. Робота з рядками типу char* є не менш цікавою ніж з рядками типу string. Тема буде корисна початківцям у вивченні питань динамічного виділення/звільнення пам’яті для масиву, доступу за покажчиком в масиві покажчиків, роботі з рядками char*.


Зміст


Пошук на інших ресурсах:

Умова задачі

Розробити клас, який оперує рядками типу char*. У класі реалізувати наступні поля та методи:

  • внутрішні змінні, що представляють масив рядків;
  • конструктори, що ініціалізують масив різними способами;
  • конструктор копіювання;
  • методи GetAi() та SetAi() для доступу до окремого рядка;
  • операторна функція operator=(), яка перевизначає операцію присвоювання. Функція повинна реалізовувати присвоювання масивів рядків;
  • метод Add() додавання рядка в кінець масиву;
  • метод Del() видалення рядка з заданої позиції;
  • деструктор.

 


Розв’язок

1. Загальна побудова класу. Додавання внутрішніх змінних

Перш за все оголошується клас з іменем ArrayPChar та в ньому оголошуються наступні внутрішні змінні:

  • A – масив рядків типу char*. Тип масиву char**;
  • length – кількість рядків у масиві A.

 

#include <iostream>
using namespace std;

// Масив рядків типу char*
class ArrayPChar
{
private:
  // Внутрішні дані
  char** A; // масив рядків
  int length; // кількість рядків у масиві
}

У майбутньому до цього класу будуть додаватись різні методи згідно з умовою задачі.

 

2. Розробка додаткових методів класу

У розділ private класу потрібно ввести ряд методів загального призначення. Ці методи (функції) викликаються з інших методів і реалізують типові операції з масивом.

2.1. Метод CheckIndex(). Перевірка коректності індексу масиву

Метод CheckIndex() виконує перевірку, чи індекс масиву лежить в допустимих межах. Програмний код методу наступний:

// Метод, що визначає, чи індекс index є допустимим індексом масиву.
// Метод повертає true, якщо значення індексу є коректним.
bool CheckIndex(int index)
{
  return ((index >= 0) && (index < length));
}

Цей метод потрібно ввести в розділ private класу.

 

2.2. Метод CopyStr(char** dest, const char* source). Копіювання рядка з виділенням пам’яті

Оскільки, копіювання рядків є основною операцією в даному класі, то дуже корисною буде реалізація функції CopyStr(), яка копіює один рядок в інший. Особливість цієї функції полягає в тому, що всередині функції виділяється пам’ять для рядка-призначення.

Функція отримує 2 параметри:

  • вихідний рядок source. Тип рядка char*;
  • покажчик на рядок-призначення dest (тип покажчика char**). Звертання до рядка-призначення *dest. Тип рядка *destchar*. Розмір рядка *dest визначається динамічно на основі розміру рядка source.

Програмний код функції наступний:

// Внутрішня функція - копіює рядок source в dest,
// пам'ять для dest виділяється всередині функції
void CopyStr(char** dest, const char* source)
{
  int i;

  // Виділити пам'ять для рядка *dest
  try
  {
    *dest = new char[strlen(source) + 1];
  }
  catch (bad_alloc e)
  {
    cout << e.what() << endl;
    return;
  }

  // Копіювання з символом '\0' включно
  for (i = 0; source[i]!='\0'; i++)
    (*dest)[i] = source[i];
  (*dest)[i] = '\0';
}

Текст функції потрібно помістити в розділ private класу ArrayPChar.

 

2.3. Метод CopyArrayStr(char***, char**, int). Копіювання масивів типу char* з виділенням пам’яті

На основі попередньої функції копіювання рядка CopyStr() розроблено функцію копіювання масивів CopyArrayStr(). Функція CopyArrayStr() отримує 3 параметри:

  • покажчик на масив-призначення dest (тип покажчика char***). Звертання до масиву *dest (тип масиву char**). Звертання до i-го рядка (*dest)[i]. Кожен рядок (*dest)[i] має тип char*;
  • масив-джерело source (тип масиву char**). Тип рядка в масиві char*;
  • кількість рядків length у масиві source.

Всередині функції CopyArrayStr() виділяється пам’ять для масиву-призначення *dest. Розмір виділеної пам’яті визначається на основі розміру масиву-джерела source.

Текст функції наступний:

// Функція яка копіює масив рядків source в масив dest.
// Пам'ять для масиву dest виділяється всередині функції
void CopyArrayStr(char*** dest, char** source, int length)
{
  int i, j;
  int n;

  // Виділити пам'ять для масиву dest як масиву покажчиків
  try
  {
    *dest = new char* [length];
  }
  catch (bad_alloc e)
  {
    cout << e.what() << endl;
    return;
  }

  // Цикл копіювання рядків
  for (i = 0; i < length; i++)
    CopyStr(&((*dest)[i]), source[i]);
}

Як видно з вищенаведеного коду, для копіювання рядків та виділення пам’яті для цих рядків функція CopyArrayStr() викликає функцію CopyStr() з п.2.2. Щоб передати покажчик на рядок (*dest)[i] у функцію CopyStr(), використовується наступне звертання

...

CopyStr(&((*dest)[i]), source[i]);

...

Функцію CopyArrayStr() можна помістити в розділ private класу.

 

2.4. Метод Free(char**, int). Звільнення пам’яті в масиві

Частою операцією є звільнення пам’яті для внутрішнього масиву A. Для реалізації цієї операції у класі розроблено функцію Free(), яка звільняє пам’ять для вхідного масиву. Функція отримує два параметри:

  • масив рядків;
  • кількість рядків у масиві length.

Текст функції наступний:

// Внутрішня функція, яка звільнює пам'ять для вхідного масиву
void Free(char** A, int length)
{
  if (length > 0)
  {
    // Звільнити пам'ять, виділену для кожного рядка
    for (int i = 0; i < length; i++)
      delete[] A[i];

    // Звільнити пам'ять для масиву покажчиків на рядки
    delete[] A;
  }
}

Перевірка на те, чи була виділена пам’ять, відбувається на основі параметру length. Звільнення пам’яті відбувається в два етапи. Спочатку звільнюється пам’ять, виділена для кожного рядка (тип елементу char*). Потім звільняється пам’ять, виділена для масиву покажчиків в цілому (тип масиву char**, тип покажчика char*).

 

3. Розробка основних методів оперування рядками згідно з умовою задачі

У розділі public класу оголошуються методи, які необхідні згідно з умовою задачі.

3.1. Конструктор без параметрів ArrayPChar()

Конструктор без параметрів викликається, коли створюється об’єкт класу ArrayPChar в якому в масиві A немає жодного елементу. Наприклад,

// оголошується екземпляр (об’єкт) з іменем AC
ArrayPChar AC; // викликається конструктор без параметрів

Текст конструктора без параметрів наступний

...

public:
  // Конструктор без параметрів
  ArrayPChar()
  {
    A = nullptr;
    length = 0;
  }

...

 

3.2. Конструктор з двома параметрами ArrayPChar(char**, int)

Конструктор з двома параметрами призначений для ініціалізації поточного масиву елементами іншого масиву типу char**. Конструктор отримує два параметри:

  • масив рядків _A. Тип масиву char**. Тип рядків у масиві char*;
  • кількість рядків у масиві length.

 

...

// Конструктор, що ініціалізує внутрішні дані значенням іншого масиву
ArrayPChar(char** _A, int length)
{
  // Оголосити локальні змінні
  int i, j;

  // Скопіювати A = _A
  CopyArrayStr(&A, _A, _length);

  // Встановити кількість рядків
  length = _length;
}

...

Як видно з вищенаведеного коду, всю роботу з копіювання масивів виконує функція CopyArrayStr(), яка описується в п. 2.3.

 

3.3. Конструктор копіювання ArrayPChar(const ArrayPChar&)

У даному класі необхідно реалізовувати конструктор копіювання, тому що в класі використовуються покажчики для яких динамічно виділяється пам’ять. Більш детально про необхідність використання конструктора копіювання описується в темі:

Текст конструктора копіювання наступний

// Конструктор копіювання
ArrayPChar(const ArrayPChar& _A)
{
  CopyArrayStr(&A, _A.A, _A.length);
  length = _A.length;
}

 

3.4. Метод GetAi(int). Читання окремого рядка в масиві

Обов’язковими методами є методи, які здійснюють читання/запис конкретного рядка, розміщеного в позиції i. Метод GetAi() повертає покажчик на рядок масиву, який має позицію i.

// Читання рядка за індексом i
char* GetAi(int i)
{
  if (CheckIndex(i))
    return A[i];
  else
    return nullptr;
}

 

3.5. Метод SetAi(const char* , int). Запис рядка в масив за заданим індексом

Метод SetAi() реалізує запис рядка типу char* в масив у заданій позиції. Пам’ять, виділена для попереднього рядка, перерозподіляється під розмір нового рядка.

// Запис рядка типу char* в рядок A[i] за індексом i,
// Функція повертає true, якщо запис відбувся успішно.
bool SetAi(const char* str, int i)
{
  // Перевірка, чи індекс в допустимих межах
  if (CheckIndex(i))
  {
    // Звільнити попередньо виділену пам'ять для рядка A[i]
    if (A[i] != nullptr)
      delete[] A[i];

    // Скопіювати рядок: A[i] = str
    CopyStr(&A[i], str);

    return true;
  }
  return false;
}

 

3.6. Метод Add(). Додати рядок в кінець масиву

У методі Add() здійснюється додавання рядка в кінець масиву. У методі оголошується додаткова змінна-масив A2 типу char**, яка служить копією основного масиву з додатковим останнім елементом.

Загальний алгоритм роботи методу наступний:

  • сформувати новий масив A2 з додатковим останнім елементом;
  • скопіювати дані з основного масиву A в додатковий масив A2;
  • звільнити пам’ять, попередньо виділену для масиву A;
  • перенаправити покажчик з масиву A на область пам’яті, виділену для масиву A2.

Текст методу наступний

// Додати рядок в кінець масиву
void Add(const char* str)
{
  // 1. Оголосити додаткові внутрішні змінні
  char** A2 = nullptr; // Додатковий локальний масив

  // 2. Виділити пам'ять для масиву A2 - на 1 елемент більше
  try
  {
    A2 = new char* [length + 1];
  }
  catch (bad_alloc e)
  {
    cout << e.what() << endl;
    return;
  }

  // 3. Скопіювати рядки з A в A2
  for (int i = 0; i < length; i++)
    CopyStr(&A2[i], A[i]);

  // 4. Звільнити пам'ять, виділену для масиву A
  Free(A, length);

  // 5. Записати останній елемент в A2,
  //   пам'ять для A2[length] виділяється всередині функції CopyStr()
  CopyStr(&A2[length], str);

  // 6. Збільшити кількість рядків на 1
  length++;

  // 7. Перенаправити покажчик A на A2
  A = A2;
}

 

3.7. Метод Del(). Видалення елементу в заданій позиції

Корисною для масиву є операція видалення елементу з заданої позиції. Для реалізації цієї операції використовується метод Del(). У методі використовується додаткова змінна-масив A2, що служить копією основного масиву A без рядка, який потрібно видалити. Позиція рядка визначається параметром index.

Загальний алгоритм роботи методу наступний:

  • скопіювати елементи з масиву A в масив A2 до позиції index. Елемент в позиції index не включається в результуючий масив A2;
  • скопіювати елементи з масиву A в масив A2 після позиції index. Таким чином, елемент в позиції index не включається в результуючий масив A2;
  • перенаправити покажчик з масиву A на масив A2.

Текст методу наступний

// Видалення елементу з заданої позиції index = 0, 1, ..., length-1
void Del(int index)
{
  // Додатковий масив
  char** A2 = nullptr;

  // Перевірка, чи коректне значення позиції
  if (CheckIndex(index))
  {
    // Виділити пам'ять для масиву A2 - на 1 рядок менше
    try
    {
      A2 = new char* [length - 1];
    }
    catch (bad_alloc e)
    {
      cout << e.what() << endl;
      return;
    }

    // Цикл копіювання даних з A в A2
    // До позиції index
    for (int i = 0; i < index; i++)
      CopyStr(&A2[i], A[i]); // Копіювання даних A2[i] = A[i]

    // Після позиції index
    for (int i = index + 1; i < length; i++)
      CopyStr(&A2[i - 1], A[i]);

    // Звільнити пам'ять, виділену для A
    Free(A, length);

    // Зменшити загальну кількість рядків на 1
    length--;

    // Перенаправити покажчики
    A = A2;
  }
}

 

3.8. Операторний метод operator=(). Перевантаження оператора присвоєння

Операторний метод (функція) operator=() необхідний, щоб коректно виконувалось присвоєння на кшталт

A1 = A2;

де A1, A2 – екземпляри (об’єкти) типу ArrayPChar. Якщо не виконати перевантаження оператора присвоєння (=), то буде виконуватись побітове копіювання яке надається компілятором за замовчуванням. Якщо в класі використовуються покажчики (як у нашому класі ArrayPChar), побітове копіювання покажчиків призводить до того, що покажчики різних екземплярів (об’єктів) класу показують на одну й ту ж ділянку пам’яті, а це неприпустимо. Після знищення об’єктів класів викликається деструктор, який звільняє ту саму ділянку пам’яті два рази (для кожного об’єкту окремо). Перший раз (для першого об’єкту) знищення відбудеться коректно, другий раз (для другого об’єкту) система згенерує виключення, оскільки пам’ять уже звільнена. Як наслідок, програма “вилетить”. Більш детально про недоліки побітового копіювання та необхідність перевантаження оператора присвоєння та конструктора копіювання описується тут.

Текст операторного методу operator=() наступний

// Перевантажений оператор присвоєння
ArrayPChar& operator=(ArrayPChar& _A)
{
  // 1. Оголосити додаткові внутрішні змінні
  char** A2;

  // 2. Звільнити пам'ять, раніше виділену для масиву A
  Free(A, length);

  // 3. Перевірка на коректність значень
  if (_A.length <= 0)
  {
    length = 0;
    A = nullptr;
    return *this;
  }

  // 4. Присвоєння A = _A.A
  CopyArrayStr(&A, _A.A, _A.length);

  // 5. Встановити нову кількість рядків
  length = _A.length;

  // 6. Повернути поточний екземпляр
  return *this;
}

 

3.9. Деструктор

Деструктор призначений для звільнення динамічно-виділеної пам’яті після того, як знищується об’єкт класу ArrayPChar. У нашому випадку пам’ять, виділена динамічно, звільняється у внутрішньому методі Free(). Відповідно програмний код деструктора такий

// Деструктор
~ArrayPChar()
{
  // Звільнити пам'ять, виділену для масиву A
  Free(A, length);
}

 

3.10. Метод Print(). Виведення масиву на екран

Завжди при розробці класу доцільно реалізовувати метод, що виводить вміст даних екземпляру класу. Це необхідно для проведення контролю, тестування тощо. У нашому випадку реалізовано метод Print(), текст якого наступний

// Метод виведення масиву рядків на екран,
// метод отримує параметром коментар, який теж виводиться.
void Print(const char* text)
{
  cout << "----------------" << endl;
  cout << text << endl;

  if (length <= 0)
  {
    cout << "Array is empty." << endl;
    return;
  }

  // Вивести рядки
  for (int i = 0; i < length; i++)
  {
    cout << A[i] << endl;
  }
}

 

4. Метод main(). Тестування класу

У методі main() продемонстровано використання класу ArrayPChar. За бажанням, можна змінити код методу для проведення власних досліджень.

void main()
{
  ArrayPChar AC1;
  ArrayPChar AC2 = AC1; // Викликається конструктор копіювання

  AC1.Print("AC1");
  AC2.Print("AC2");

  AC1.Add("Hello!");
  AC1.Print("AC1");
  AC1.Add("World");

  // Перевірка конструктора копіювання
  ArrayPChar AC3 = AC1;
  AC3.Print("AC3");

  // Перевірка перевизначеного оператора присвоєння
  AC2 = AC3;
  AC2.Print("AC2");

  // Перевірка функції GetAi()
  char* s1;
  s1 = AC2.GetAi(0);
  cout << "s1 = " << s1 << endl;

  // Перевірка функції SetAi()
  AC2.SetAi("ABCD", 1);
  AC2.Print("AC2");

  // Перевірка функції Del()
  AC2.Add("JKLMN");
  AC2.Add("OPRST");
  AC2.Print("AC2");
  AC2.Del(3);
  AC2.Print("AC2-[3]");

  // Перевірка конструктора, що отримує масив типу char**
  const char* Array[3] = { "Programming", "C++", "Strings" };
  ArrayPChar AC4((char**)Array, 3);
  AC4.Print("AC4");

  AC4.Add("Java");
  AC4.Print("AC4");
}

 

5. Текст усієї програми. Скорочений варіант

 

#include <iostream>
using namespace std;

// Масиви рядків типу char*
class ArrayPChar
{
private:
  // Внутрішні дані
  char** A; // масив рядків
  int length; // кількість рядків у масиві типу char*

  // Метод, що визначає, чи індекс index є допустимим індексом масиву.
  // Метод повертає true, якщо значення індексу є коректним.
  bool CheckIndex(int index)
  {
    ...
  }

  // Внутрішня функція - копіює рядок source в dest,
  // пам'ять для dest виділяється всередині функції
  void CopyStr(char** dest, const char* source)
  {
    ...
  }

  // Функція яка копіює масив рядків source в масив dest.
  // Пам'ять для масиву dest виділяється всередині функції
  void CopyArrayStr(char*** dest, char** source, int length)
  {
    ...
  }

  // Внутрішня функція, яка звільнює пам'ять для вхідного масиву
  void Free(char** A, int length)
  {
    ...
  }

public:
  // Конструктори
  // Конструктор без параметрів
  ArrayPChar()
  {
    ...
  }

  // Конструктор, що ініціалізує внутрішні дані значенням іншого масиву
  ArrayPChar(char** _A, int _length)
  {
    ...
  }

  // Конструктор копіювання
  ArrayPChar(const ArrayPChar& _A)
  {
    ...
  }

  // Методи доступу до окремого рядка GetAi(), SetAi()
  // Читання рядка за індексом i
  char* GetAi(int i)
  {
    ...
  }

  // Запис рядка типу char* в рядок A[i] за індексом i,
  // Функція повертає true, якщо запис відбувся успішно.
  bool SetAi(const char* str, int i)
  {
    ...
  }

  // --------------- Методи оперування рядками ----------------
  // Додати рядок в кінець масиву
  void Add(const char* str)
  {
    ...
  }

  // Видалення елементу з заданої позиції index = 0, 1, ..., length-1
  void Del(int index)
  {
    ...
  }

  // ----------------------------------------------------------
  // Перевантажений оператор присвоєння
  ArrayPChar& operator=(ArrayPChar& _A)
  {
    ...
  }

  // Деструктор
  ~ArrayPChar()
  {
    ...
  }

  // Метод виведення масиву рядків на екран,
  // метод отримує параметром коментар, який теж виводиться.
  void Print(const char* text)
  {
    ...
  }
};

void main()
{
  ...
}

 


Зв’язані теми