C++. Разработка класса, который реализует массив строк типа char*




Разработка класса, который реализует массив строк типа char*

В языке C++ строку можно представить типом string или типом char*. Тип string есть классом и имеет мощный арсенал удобных средств для обработки строк. Работа со строками типа char* часто бывает сложна.

В данной теме продемонстрирован класс ArrayPChar, который представляет массив строк типа char*. Работа со строками типа char* не менее интересна чем со строками типа string. Тема будет полезна начинающим в изучении вопросов динамического выделения/освобождения памяти для массива, доступа по указателю в массиве указателей, работе с строками char*.


Содержание


Поиск на других ресурсах:

Условие задачи

Разработать класс ArrayPChar, который оперирует строками типа 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;

  // Проверка, корректно ли значение index
  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()
{
  ...
}

 


Связанные темы