Skip to content

Instantly share code, notes, and snippets.

@arhadthedev
Created October 20, 2017 09:30
Show Gist options
  • Save arhadthedev/b3f13592922587335899acb14795d9bb to your computer and use it in GitHub Desktop.
Save arhadthedev/b3f13592922587335899acb14795d9bb to your computer and use it in GitHub Desktop.
#define _CRT_SECURE_NO_WARNINGS
#include <cstring>
#include <iostream>
// Забываем про using namespace std - сэкономит кучу нервов при появлении классов,
// одноимённых с таковыми из стандартной библиотеки
// Так как эти функция используется только здесь, объявил их как static (чтобы не были видна
// извне)
//
// Никогда, НИКОГДА не храните размер массива в знаковых целых. Для индексации есть
// специальный тип, size_t.
//
// И ещё, приручаемся ставить const всем указателям и переменным, которые не должны
// изменяться. Мало того, что это поможет отлавливать ошибки, так ещё мы получим возможность
// передавать строковые константы, которые имеют тип const char*
static size_t StrLen(const char* _str);
static void StrCpy(char* in_str, const char* src_str);
static char* StrCat(char*& str1, const char* str2);
class String
{
public:
// Класс-обёртка над строкой, введённая для возможности определения своего operator+
//
// Да-да, вся эта затея исключительно для того, чтобы перейти от неподконтрольного
// и вообще бессмысленного operator +(char*, char*) к нашему operator+ (Literal&, char*).
class Literal
{
public:
explicit Literal(char** handledString);
void operator +(char *s);
// Для возможности неявного преобразования Literal в char*, чтобы Literal мог стоять
// по праую сторону знака приравнивания в строке:
//
// a[0] + a[1];
//
// а также быть переданным cout-у, ожидающему именно указатель на строку.
operator char*() const;
// Для возможности присваивания строк Literal-у в выражениях вида:
//
// a[0] = "1";
//
// ибо a[i] возвращает теперь Literal, о котором компилятор мало что знаетю
//
// Метод возвращает ссылку на Literal, чтобы можно было присваивать цепочкой:
//
// a[0] = a[1] = a[2] = "1";
//
// Принимаемый указатель константен, потому что сам литералы (то, что в кавычках)
// константны.
Literal& operator=(const char* in);
private:
// Вообще-то мы работаем с просто указателем на строку, но так как нам надо
// её изменять, вводим второй уровень косвенности
char** str_;
};
String(size_t);
String(const String&);
~String(); //Деструкторы принято помещать рядом с конструкторами, хотя это
//вопрос исключительно стиля
void showArray();
// В оригинале этот метод зачем-то возвращал _ссылку_ на указатель. Зачем - не понял,
// но раз мы всё равно возвращаем обёртку, убрал.
Literal operator[](size_t j);
private:
// Опять int -> size_t
size_t sizeOfArray;
char **str;
};
String::String(size_t size)
// Учимся пользоваться списками инициализации (заодно переупорядочил поля
// в объявлении класса, так как значение str зависит от значения sizeOfArray)
: sizeOfArray(size)
, str(new char *[sizeOfArray])
{
for (size_t i = 0; i < sizeOfArray; i++)
{
str[i] = new char[2048];
}
}
String::String(const String& obj)
: sizeOfArray(obj.sizeOfArray)
, str(new char *[sizeOfArray])
{
for (size_t i = 0; i < sizeOfArray; i++)
{
str[i] = new char[2048];
// Эта строка в оригинале:
//
// str[i] = obj.str[i];
//
// не копировала символы, а делала второй указатель на исходную строку. Из-за
// этого уничтожение класса, откуда производилось копирование, уничтожало
// и этот общий буфер, приводя к скорой порче кучи. И ещё эта строка оставляла
// свежевыжевыделенный в предыдущей строке буфер "висячим", без указателей
// на него.
//
// Для посимвольного копирования и в Си, и в С++ есть специальная функция memcpy()
// (если вы не хотите полязоваться классом std::string, который сам делает всю эту
// работу).
//
// std::memcpy(str[i], obj.str[i], 2048 * sizeof(char));
// str[2047] = '\0';
//
// В отличие от strcpy() эта функция не сломается при отсутствии нуль-терминатора.
// И да, раз мы предоставляем посимвольный доступ к строковому массиву, стоит
// дополнительно удостовериться, что нуль-терминатор гарантированно есть.
//
// Но раз у вас уже есть StrCpy(), воспользуемся ей. Так как пример учебный,
// восстановление нуль-терминатора тоже пока опустим (хотя это делается в одну строку).
//
// И ещё, я поставил два двоеточия в начале вызова, чтобы указать компилятору, что эта
// функция должна быть обязательно вне классов и пространств имён.
::StrCpy(str[i], obj.str[i]);
}
}
String::~String()
{
// В оригинале была единственная строка:
//
// delete str;
//
// что провоцировало утечку памяти. Да, при введении подобной очистки, действительно, ломается
// оригинальная логика копирования строковых указателей в String(const String& obj),
// но мы её уже починили. Зато сразу приручаемся писать классы, которые можно массово
// создавать и удалять, не опасаясь израсходования адресного пространства из-за утечек.
//
// И да, раз вы использовали new[], то и удалять надо парным оператором delete[]. Сейчас
// всё вроде бы всё и так работает, но с выходом какой-нибудь новой версии стандартной
// библиотеки всё может сломаться.
for (size_t i = 0; i < sizeOfArray; i++)
{
delete[] str[i];
}
delete[] str;
}
void String::showArray()
{
for (size_t i = 0; i < sizeOfArray; i++)
{
std::cout << str[i] << " | ";
}
std::cout << std::endl << std::endl;
}
String::Literal String::operator[](size_t j)
{
// Переупорядочил ветви, чтобы сначала шёл нормальный ход работы.
// Кстати, переход на беззнаковый size_t заодни избавил нас от необходимости проверки
// индекса на неотрицательность, ибо это теперь железобетонное ограничение самого типа.
if(j < sizeOfArray)
{
// Нам нужен указатель на указатель. Думаем, где его взять.
//
// Для этого рассмотрим схему размещения данных:
//
//
// char** str
// | +---+---+---+---+---+-----
// char* +---> | 0 | 1 | 2 | 3 | 4 | ...
// +---+---+---+---+---+-----
// |
// char, ... +---> H e l l o , W o r l d
//
// То есть для доступа к буферу достаточно копии указателя из второго уровня.
// Однако для модификации второго уровня нам нужен указатель на соответствующий
// элемент этого уровня:
//
// +--- (*) <- Вот этот указатель
// char** str v
// | +---+---+---+---+---+-----
// char* +---> | 0 | 1 | 2 | 3 | 4 | ...
// +---+---+---+---+---+-----
// |
// char, ... +---> H e l l o , W o r l d
//
// Соответственно, нам достаточно вызвать str[j], которая вернёт ссылку на
// элемент второго уровня, а затем преобразовать ссылку в указатель оператором
// "&". Это работает, потому что на уровне компилятора ссылка является синонимом
// какой-то переменной и не имеет собственного адреса.
return Literal(&str[j]);
}
else
{
// Как-то странно безапеляционно прибивать программу вместо выброса исключения,
// которое вызывающая сторона могла бы обработать. Но это уже на усмотрение
// автора исходного кода
exit(1);
}
}
String::Literal::Literal(char** handledString)
: str_(handledString)
{
}
void String::Literal::operator +(char *s)
{
// Вот почему мы держали двойной указатель. Вообще, мы могли бы держать сразу
// ссылку на указатель, но ссылки принято использовать там, где они долго не живут.
// Поле класса в подобный сценарий не вписывается.
StrCat(*str_, s);
}
String::Literal::operator char*() const
{
//Второй уровень указателей нам был нужен для внутренни манипуляциц. Пользователю
// класса он не нужен, поэтому разыменовываем.
return *str_;
}
String::Literal& String::Literal::operator=(const char* in)
{
// Так как мы принимаем константные (то есть неизменяемые) строки,
// то для дальнейшей корректной работы operator+ входную строку
// надо скопировать в свой внутренний буфер
::StrCpy(*str_, in);
return *this;
}
static size_t StrLen(const char* _str)
{
size_t size = 0;
// Мы работаем со строками, поэтому вместо нуля стоит использовать литерал нуль-символа
for (; _str[size] != '\0'; size++);
return size;
}
static void StrCpy(char* in_str, const char* src_str)
{
for (size_t i = 0; i < StrLen(in_str); i++)
in_str[i] = src_str[i];
}
static char* StrCat(char*& str1, const char* str2)
{
const size_t sz = StrLen(str1) + StrLen(str2);
char* const ts = new char[sz + 1];
for (size_t i = 0; i < StrLen(str1); i++)
ts[i] = str1[i];
for (size_t ii = StrLen(str1), j = 0; ii <= sz; ii++, j++)
ts[ii] = str2[j];
delete str1;
// Вы переопределяете значение _ копии_ под именем str1. Оригинальный
// указатель при этом продолжает указывать на уже освобождённый участок
// памяти. Исправил, сделав str1 ссылкой на указатель.
str1 = ts;
return str1;
}
int main()
{
String a(3);
String b(6);
a[0] = "1";
a[1] = "2";
a[2] = "3";
a[0] + a[1];
std::cout << a[0] << std::endl;
system("pause");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment