Я всегда слышал, что с библиотеками в С++ что-то не так, как и с ограничением максимального целочисленного значения, да и вообще то, что язык сложный и непонятный. Что же, сегодня, мы начнём писать собственную библиотеку больших чисел, полностью своими руками c 0, и узнаем, так ли страшен С++, как его малюют?
Если вы не разбираетесь в С++, не переживайте, эта статья имеет нулевой порог вхождения. Мы начнём с лёгкого, но вы даже не заметите, как начнёте разбираться в более сложных и непонятных, на первый взгляд, вещах. Главное, писать код логично. Думаю, данная статья будет интересна не только начинающим, ведь я постарался затронуть достаточно много тем. (для старожилов: моя цель не сделать оптимизирование или быстрее, а показать, что С++ не такой уж и сложный язык программирования. И да, я знаю, что существуют другие библиотеки, которые делают это быстрее и лучше. И да, было бы круче, если бы мы использовали булевую алгебру. И да, С++ про вечную оптимизацию, но это статья не про это. Спасибо.)
За сегодня мы узнаем, что такое: Перегрузка функций/конструкторов, прототипы функций, обработка исключений, пространство имён, псевдонимы типов, заголовок.h, как пользоваться отладчиком и как писать продвинутые/красивые комментарии. Пристёгивайтесь, будет безумно интересно.
▍ Предисловие и планы
С++ строготипизированный язык программирования, где максимально возможное значение целочисленной переменной, является максимальное значение unsigned long long int (где-то 18 446 744 073 709 551 615). Этого бывает недостаточно, поэтому я решил разработать собственную библиотеку больших чисел (BigNumLib). Единственное ограничение размерности BigNumLib переменной – это количество цифр, из которого будет состоять число, то есть, максимально в число поместится 4 294 967 295 цифр.
▍ Начало разработки
Итак, начинаем разработку! Для начала нам необходимо продумать логику и возможности нашего собственного типа данных. Как мы создадим свой тип данных? В ЯП С++ нельзя расширить стандартные типы (int, double и т.п.), поэтому, единственный возможный вариант, который у нас остался, это работа через struct и class.
Чем отличаются Struct и Class?
Ответ: единственное различие между ними, так это то, что в struct модификаторы доступа по умолчанию public, а в class — private. Также отличается и наследование по умолчанию.
Итак, откроем Visual Studio с пустым проектом. Создадим папки (если они не созданы): “Файлы заголовков” с файлом BigNumLib.h и “Исходные файлы” с файлами Main.cpp, BigNumLib.cpp. У кого трудности на данном этапе, ничего страшного, ниже представлены фото и gif.
Как создать и настроить проект
В файле заголовка (.h), можно заметить строчку #pragma once. Что это?
Ответ: В языках программирования С и C++ #pragma once — нестандартная, но широко распространённая препроцессорная директива, разработанная для контроля за тем, чтобы конкретный исходный файл при компиляции подключался строго один раз.
Здесь, в этом файле заголовка, мы будем создавать прототипы функций (функции без тела) и вообще описывать весь класс, например, какие дополнительные библиотеки будут подключены, для исправной работы нашей, или какие функции будут доступны пользователю, а какие нет (модификаторы доступа public, private). Зачем мы вообще создали данный заголовок? Всё просто, чтобы подключить внешний код, необходимо использовать именно заголовок.
▍ Создание bignum класса
Итак, для начала нам необходимо создать класс и его поля:
class bignum {
private:
std::string _value;
size_t _size;
bool _isNegative;
public:
bignum();
}
Ловим ошибку, что не подключили библиотеку и подключаем:
#include <string>
Итак, что мы написали?
_value =
здесь будет храниться наше число в виде строки_size =
из скольких цифр состоит наше число (размер). size_t
это псевдоним, то есть, то же самое что и unsigned int
(положительные целые числа)_isNegative =
является ли отрицательным числом. (true или false)bignum() =
конструктор класса. Он вызывается при создании экземпляра класса.private: –
поле, где доступ к данным имеет лишь класс. Приватные переменные, как правило, пишутся через ‘_’.Так отлично, теперь откроем BigNumLib.cpp и напишем там такой код:
#include "bignum.h"
bignum::bignum()
{
_value = "0";
_size = 1;
_isNegative = false;
}
Здесь мы подключили наш заголовок и описали конструктор класса, где доступ к конструктору мы получаем через пространство имён (
bignum::
)Теперь мы можем открыть наш основной файл (Main.cpp) и проверить работу библиотеки:
#include "BigNumLib.h"
int main() {
bignum a;
return 0;
}
// Важное замечание, локальные заголовки, которые находятся в одном решении, подключаются с помощью кавычек.Что же, теперь ставим точку остановы на
return 0
, и смотрим нашу переменную.Отлично, всё работает! По поводу отладчика, это безумно удобный интерфейс. Как сказал один мудрый человек, если программист не умеет пользоваться отладчиком, то этот человек не программист. Краткий экскурс по данному чуду: f5 (запуск отладки), shift+f5 (остановка), ctrl+shift+f5 (перезапуск), f10 (шаг с заходом в функцию), f11 (шаг с обходом функции), shift+f11 (шаг с выходом из функции), f5 (во время отладки, перейти к следующей точке остановы).
▍ Геттеры
Так, теперь создадим функцию геттер, чтобы иметь возможность читать поле нашего класса. Для этого объявим эту функцию в BigNumLib.h (файл заголовок) в поле
public
://@return string
std::string getValue();
Мы написали комментарии в стиле DOC++. Этот тип комментариев понимает Visual Studio и красиво отображает нам. (ключевые слова пишутся через ‘@’:
@return, @param
)Теперь необходимо прописать логику данных функций в BigNumLib.cpp.
std::string bignum::getValue()
{
std::string _value = this->_value;
if (this->_isNegative)
_value.insert(0, "-");
return _value;
}
Чтобы обратиться к внутренним полям класса, мы используем указатель
this
, таким образом, пользователь имеет доступ к переменной _value
только на чтение.В функции
getValue()
мы создаём локальную переменную _value и заполняем её данными лежащими в поле класса. Если поле _isNegative имеет значение true (число отрицательно), то мы вставляем в начало строки ‘-’. (insert(позиция, знак))Отлично, теперь проверим наш код в действии!
#include "BigNumLib.h"
#include <iostream>
int main() {
bignum a;
std::cout << "bignum a = " << a.getValue() << std::endl;
return 0;
}
▍ Перегрузка конструктора (приём int)
Далее нам необходимо создать перегрузку конструктора класса, который на вход принимает long long int. Для этого объявим:
BigNumLib.h
public:
bignum(long long int other_value);
BigNumLib.cpp
bignum::bignum(long long other_value)
{
_isNegative = other_value < 0 ? true : false;
_value = _isNegative ? std::to_string(other_value).erase(0, 1) : std::to_string(other_value);
_size = _value.size();
}
Перегрузка конструктора \ функции \ оператора – это когда они имеют одно и то же имя, но принимают разные параметры. Та или иная функция вызывается в зависимости от принимаемых ею аргументов.
Здесь, мы впервые использовали тернарный оператор. Сокращённое написание конструкции
if, else
. Всё предельно просто, если на вход поступает отрицательное число, то поле _isNegative
становится true
. После чего, число переводится в строку и если число отрицательное, то удаляется первый символ из строки (‘-’).Сейчас мы можем протестировать это и присвоить число. Попробуем присвоить положительное и отрицательное число, посмотрим, правильно ли работает наша программа:
Отлично, теперь попробуем ввести огромное число:
Как мы можем видеть, Visual Studio запрещает нам присваивать такое огромное число, потому что оно выходит за рамки long long int (превысив значение самого большого стандартного типа). Как мы будем обходить данный запрет? С помощью строки, ведь она, практически безгранична.
▍ Перегрузка конструктора (приём string, char*)
Создадим конструктор и пару функций в файле заголовка.
BigNumLib.h
public:
bignum(const char* other_value);
bignum(std::string other_value);
private:
void parsStringToBigNumParams();
BigNumLib.cpp
bignum::bignum(const char* other_value)
{
_value = other_value;
parsStringToBigNumParams();
}
bignum::bignum(std::string other_value)
{
_value = other_value;
parsStringToBigNumParams();
}
void bignum::parsStringToBigNumParams()
{
if (_value[0] == '-') {
_value = _value.erase(0, 1);
_isNegative = true;
}
else
_isNegative = false;
if (_value.find_first_not_of("0123456789") != std::string::npos)
throw std::runtime_error(_value + " it's not a number!");
_size = _value.size();
}
Мы написали 2 конструктора, где один из них принимает string, а другой массив char. Зачем? Потому что в случае, когда после присвоения сразу записывается значение, то будет массив char. А если создать string переменную и присвоить уже её, то активируется другой конструктор (со string параметром).
По поводу функции
parsStringToBigNumParams()
. Данная функция превращает строку, в набор параметров нашего класса. В начале она проверяет, стоит ли ‘-’, на первой позиции в _value
, если да, убрать знак из строки и присвоить параметру _isNegative = true
. После чего идёт проверка, если в _value найдено, что какой-то элемент не совпадает с цифирным набором (npos — не найдено совпадений), то выкинуть исключение. И дальше присвоить размер.Что делает исключение? Полностью останавливает работу программы, если его не обработать. Давайте же опробуем работу наших конструкторов и обработаем исключение:
Как работает обработка исключений? В блоке
try
пишется небезопасный код, если в нём происходит ошибка, мы тут же попадаем в блок catch
. Из-за того, что мы добавили описание в нашу самодельную ошибку, мы можем увидеть, какое значение, смогло сломать нашу библиотеку.▍ Заключение
Что же, я чувствую, что выдал достаточно много инфы и, если продолжить, у новичков она может превратиться в кашу. За сегодня мы прошли очень много интересных тем и познакомились с некоторыми особенностями языка С++, но впереди ещё больше крутой информации, такая как перегрузка операторов, указатели, resize string и собственная логика в математических операциях. Если вам заходит такой формат обучения/разработки реального проекта, дайте знать, буду пилить 2 часть в таком же формате, ну, если вы вообще ждёте 2 часть :)
p/s Ссылка на GitHub