• Как создать многомерный массив в одной области памяти?

    AshBlade
    @AshBlade
    Просто хочу быть счастливым
    Одномерный массив размером X * Y - единственное решение, если нужен непрерывный участок памяти.
    Для получения первого индекса - index / X, для второго - index % X.
    Но надо позаботиться - чтобы места было достаточно, иначе однажды получишь OOM либо когда место закончится, либо при сильной фрагментации памяти.

    Вариант с "зубчатым" массивом, тоже норм - отложенное выделение можно реализовать. Но по скорости будет проигрывать из-за локальности данных. Хотя, если нужен непрерывный участок - уже не подходит
    Ответ написан
    2 комментария
  • Как создать многомерный массив в одной области памяти?

    wataru
    @wataru Куратор тега C++
    Разработчик на С++, экс-олимпиадник.
    Есть еще 2 варианта:

    1) Используйте std::vector<std::array>.

    2) Что-то среднее между двумя вариантами у вас: как и везде, у вас есть одномерный массив для данных. А operator[] на классе возвращает int* на первый элемент в строке. То же, что у вас, только не надо никакого вспомогательного класса. Таким образом двойная индексация будет работать как надо. Но, как и во втором упомянутом вами примере, тут не будет проверки выхода за границы массива.
    Ответ написан
    Комментировать
  • Как реализовать операторы в классе математического вектора?

    wataru
    @wataru Куратор тега Математика
    Разработчик на С++, экс-олимпиадник.
    Вектора можно складывать. Покомпонентно. Сложение векторов разного размера или вектора и числа бессмысленны.

    Вектора можно перемножать. Скалярное или вектороное произведения. Чаще скалярное ставят на operator*, а векторное на какое-нибудь operator^

    Операторы сравнения обычно делают лексикографически, покомпонентно. Сравнили первое число - у какого вектора больше - тот и больше. Если числа равны, сравниваем вторые компоненты. И т.д. Но это делают редко, потому что смысла вектора сортировать особо нет. Лексикографический порядок пользы особо не приносит.
    Ответ написан
    9 комментариев
  • Что быстрее индексы или указатели?

    wataru
    @wataru Куратор тега C++
    Разработчик на С++, экс-олимпиадник.
    Зависит от модели процессора, версии и опций компилятора и немножечко фазы луны. В целом без разницы.

    Практический совет - лучше писать через индексы, ибо так понятнее и больше шансов что компилятор там все наоптимизирует (например, он сможет векторизовать работу через какие-нибудь SSE инструкции процессора).

    Совет по бенчмарку - если памяти не хватает, стоит по одному достаточно большому массиву пройтись 10000 раз. А лучше использовать готовые фреймворки для измерения скорости, вроде того де gbenchmark.

    Еще, иногда полезно посмотреть на ассемблерный выхлоп. Вот, например, что происходит при -O3 опции компилятора. Он генерирует вообще идентичный код для обеих функций (развернув циклы)! И даже при -O2 оно одинаковый код выдает.

    Без оптимизаций код разный, но там все не так как вы думаете. Вместо инструкции mov eax, dword ptr [rax + 4*rcx] в варианте с индексами используется инструкция mov eax, dword ptr [rax] для указателей. Это самое "складывание с указателем массива" вообще не отдельная операция - а вариант адрессации в инструкции mov. Они могут вообще одинаковое количество тактов занимать, это надо мануал по конкретной архитектуре процессоров читать.
    Ответ написан
    Комментировать
  • Не удается сопоставить определение функции существующему объявлению. Как можно исправить?

    @dima20155
    you don't choose c++. It chooses you
    Для этих целей вам необходим ещё один шаблонный параметр.
    Можно сделать вот так:
    template <const size_t c, const size_t b = c*c>
    struct Sudoku_impl{
        size_t s;
        Sudoku_impl() {std::cout << b;}
    };
    
    int main(){
        constexpr size_t x = 5;
        Sudoku_impl<x> a;
        return 0;
    }

    выглядит сомнительно из-за возможности переопределить второй шаблонный параметр, поэтому наружу такой судоку-класс отдавать не стоит. Чтобы решить эту проблему можно добавить псевдоним, ограничивающий количество шаблонных параметров до одного
    например так:
    template <const size_t c>
    using Sudoku = Sudoku_impl<c, c*c>;


    или просто напишите шаблон для конкретной функции
    template <const size_t cell>
    class Sudoku {
    private:
        class Slot;
        arr2<Slot, size> board;
    public:
        template <size_t size = cell*cell>
        arr<Slot, size>& operator[](size_t index);
    }
    Ответ написан
    Комментировать
  • Как использовать класс объявленный в другом файле?

    xzripper
    @xzripper
    0xC0000005
    Измените extern Logger logg на Logger logg;

    Еще немного улучшил код:
    #include <iostream>
    #include <fstream>
    
    class Logger {
    public:
        Logger() {
            openFile("Logger.txt");
        }
    
        ~Logger() {
            closeFile();
        }
    
        template<class T>
        Logger& operator<<(const T& value) {
            file << value;
    
            std::cout << value;
    
            return *this;
        }
    
        Logger& operator<<(std::ostream& (*manipulator) (std::ostream&)) {
            file << manipulator;
        
            std::cout << manipulator;
    
            return *this;
        }
    
        void openFile(const char *path) {
            file.open(path);
    
            if(!file.is_open()) {
                throw std::runtime_error("Failed to open 'Logger.txt'");
            }
        }
    
        void closeFile() {
            file.close();
        }
    
    private:
        std::ofstream file;
    };
    
    Logger logger;


    logger << 6 << " is six" << std::endl;

    64e1e14d1a6e4270143548.png
    Ответ написан
    Комментировать
  • Как использовать класс объявленный в другом файле?

    maaGames
    @maaGames
    Погроммирую программы
    Сразу скажу, что это плохой код и делать так не надо.
    Однако, отвечая на вопрос: весь этот код написан в .h файле. Нужно сделать .cpp файл, в котором будет сздана переменная logg. Сейчас объявлено, что переменная есть, но самого объекта нет.
    Ответ написан
    4 комментария
  • Нужно ли делать защиту при делении на ноль?

    mayton2019
    @mayton2019
    Bigdata Engineer
    Вообще ты сам себе ответил на вопрос
    в разных отраслях для личного пользования.


    Главный пользователь и заказчик - это ты. И когда ты разрабатываешь свою библиотеку векторной алгебры - то пишешь модульные тесты. И тесты, как-бы документируют твою библиотеку и закрывают все вопросы
    с нулями и бесконечностями.

    Какие вообще у тебя варианты по отработке неопределенностей? Я вижу такие.
    1) Возвращать бесконечность +Inf. Это нормально для floating-point. Но влияние результата на стек
    вызовов дальше надо учитывать. Эта бесконечность пойдет в другие формулы порождая новые бесконечности и т.п.

    2) Бросать исключение. Это не в духе С++ и не всегда удобно для выскокой производительности. Но языки высокого уровня этим часто пользуются. Здесь мы предполагаем что такой результат - крайне нежелателен и работа стека расчета векторов будет аварийно прервана.

    3) Возвращать специальный контейнер с результататом (Optional или Either) или пустой контейнер. Это в духе функционального кодинга. Но весь твой стек должен тоже быть адаптированным к таким Optional параметрам результатов.

    И есть еще вариант - просто найти такой базис вычислений в котором нет такой проблемы. Пускай допустим векторы так и остаются основным типом данных но расчеты ты будешь делать в каком-нибудь другом типе где эта операция на уровне математики - безопасна и всегда определена. Грубо говоря как в углах Эйлера. Чтоб не писать всякие проверки условий (if) - можно перейти к кватерниону и там вроде как вращение легче идет.
    Ответ написан
    Комментировать
  • Нужно ли делать защиту при делении на ноль?

    wataru
    @wataru
    Разработчик на С++, экс-олимпиадник.
    Для облегчения отладки в будущем, стоит вставить assert какой-нибудь. Чтобы программа падала с понятной ошибкой, при попытке поделить на 0.
    Ответ написан
    Комментировать
  • Как оформить код?

    @dima20155
    you don't choose c++. It chooses you
    самое простое - используйте typedef или using
    А также можно написать свой класс-обертку для удобства.
    Ответ написан
    3 комментария
  • Как определить есть ли противоречия в цепочке логических выражений?

    wataru
    @wataru Куратор тега Математика
    Разработчик на С++, экс-олимпиадник.
    Алгоритм называется "обход в глубину на графе". Работает за линию, все очень быстро. Правда, его придется применить несколько раз.

    Все неравенства "==" замените на пару "<=" и ">=".
    Добавьте неравенства 1 < 2, 3 < 4 и т.д. для каждой пары соседних на числовой прямой чисел во входных данных

    Постройте граф: Каждой переменной и уникальному числу во входных данных сопоставьте одну вершину. Проведите для каждого неравнества ребро от меньшей вершины к большей, раскрашенное в 2 цвета: черный, если неравнество нестрогое (<=), белый - иначе.

    Теперь, если в этом графе нет циклов, содержащих белые ребра (строгие неравенства) - то противоречий нет: Все циклы целиком из черных ребер означают, что все вершины имеют одинаковое значение. Можно эти вершины все объединить в одну новую. Раз белые ребра (<) циклов не образуют, то получившийся граф будет ациклическим и можно назначить всем вершинам какие-то числовые значения, удовлетворяющие условиям. Проблема может еще быть, что нет целых решений вроде 1== a < b < c == 2, но это можно потом проверить в топологической сортировке жадно назначая всем вершинам числа. Или противоречия вида 2==3. Тоже решается после получения компонент связности.

    Итак, алгоритм: найдите в этом графе компоненты сильной связности. Потом проверьте все ребра. Если белое ребро (строгое неравнество) имеет оба конца в одной и той же компоненте - вы нашли противоречие.

    Теперь надо постараться назначить кадой компоненте числовое значение так, чтобы не было противоречий. Это можно делать жадно, назначая каждой компоненте минимально возможное значение.

    Пройдитесь по компонентам в топологическом порядке (они в таком виде уже будут получены алгоритмом выше). Если в компоненте есть вершина-число, то назначаете всей компоненте это значение. Если есть несколько вершин-чисел - то это противоречие. Также посмотрите все обратные ребра из всех вершин в компоненте. Если они ведут в другую компоненту, то обновите минимально возможное значение в данной компоненте как равное, или на 1 большее уже известного значения для второй компоненты (в зависимости от цвета ребра, т.е. строгости неравнества). Если получили, что это минимально возможное значение больше уже фиксированного за счет вершины-числа - то вы нашли противоречие.

    В конце вы получите для каждой компоненты ее численное значение без каких-либо противоречий.
    Ответ написан
    4 комментария
  • Калькулятор C++ как убрать 1.33333e+06 подобные результаты вычисления?

    wataru
    @wataru Куратор тега C++
    Разработчик на С++, экс-олимпиадник.
    Выводить в фиксированном виде:
    std::cout << std::fixed;  // Меняем формат вывода вещественных чисел 
    std::cout.precision(10);  // Сколько вы там хотите знаков после запятой выводить.
    double e = 1.3333e6;
    std::cout << e;  // 1333300.00000000000;
    Ответ написан
    1 комментарий
  • List убирает string значения из класса, что делать?

    @dima20155
    you don't choose c++. It chooses you
    Во-первых, зачем вы приводите целый исходник сюда. Люди куда быстрее поймут суть, если вы сократите иерархию классов до демонстративного минимума.

    Во-вторых, во всех ваших case'ах
    Создается временный объект, затем вы зачем-то создаете ещё и временную переменную-указатель и сохраняете его адрес (зачем?). А после запихиваете все это в лист. Объект создается на стеке и при выходе за границы фигурных скобок вызывается деструктор и он перестает существовать, а все его содержимое будет перезаписано любой следующей переменной, размешенной на стеке. Вот код, о котором я говорю:
    case 1: {
          cout << endl << "Введите модель:";
          cin >> model;
          cout << endl << "Введите прозводителя:";
          cin >> producer;
          cout << endl << "Введите цену:";
          cin >> price;
          cout << endl << "Введите год выхода:";
          cin >> year;
          Appliances appliances{ model,producer,price,year };
          Appliances* pb = &appliances;
          catalog.push_back(pb);
          break;
        }


    Вот наглядный пример того что у вас получается.
    https://godbolt.org/z/bx5Wbo1bG

    Как исправить? Создавайте элемент в куче (new OhMyClass()). А лучше храните в std::list, например, std::unque_ptr
    Ответ написан
    2 комментария
  • Как правильно подключать библиотеку в CMake?

    @sergiodev
    Нужно ещё скомпоновать приложение (Core) с библиотеками, которые вы cоздали в подпапках:

    target_link_libraries(Core Window Out)

    после вызовов add_subdirectory().

    P.S. cmake_minimum_required(...) нужно по идее вызывать только в корневом CMakeLists.txt (где объявляется проект), во вложенных файлах это необязательно, насколько знаю.
    Ответ написан
    2 комментария
  • Почему явная специализация невозможна?

    wataru
    @wataru Куратор тега C++
    Разработчик на С++, экс-олимпиадник.
    Проблема вызвана использованием шаблона rewrite из шаблона search.

    Если вы перенесете специализацию шаблона rewrite вверх, до специализации search, то все скомпилируется. Или надо где-то выше первого использования шаблона rewrite задекларировать специализацию (что ваш закомментированный код и делает).

    Вызвана эта ошибка стандартом.
    Надо, чтобы специализация шаблона была задекларирована до любого использования:
    Specialization must be declared before the first use that would cause implicit instantiation, in every translation unit where such use occurs:
    Ответ написан
    1 комментарий
  • Уменьшается ли используемая память программы?

    wataru
    @wataru Куратор тега C++
    Разработчик на С++, экс-олимпиадник.
    Не гарантированно, но в некоторых случаев компилятор действительно сможет переиспользовать место на стеке под переменную a для какой-то новой локальной переменной, когда a выйдет из зоны видимости. Но чаще это место просто будет пустым до конца функции и никакой экономии памяти вы не получите.

    Но вообще, делать так для экономии памяти никогда, категорически не рекомендуется. Код становится менее читаем а экономите вы на спичках. Это локальные переменные - они на стеке. Их много можно выделить только рекурсией или большими массивами (ну не объявите вы в коде миллион локальных переменных). В обоих случаях, если стека не хватает - надо или избавлятся от рекурсии/больших массивов изменением логики, или выносить их в кучу.

    Использование фигурных скобок для ограничения зоны видимости переменной действительно используется на практике, когда вам надо ограничить время жизни переменной и добиться вызова деструктора в определенное время. Так делают, например, когда захватывают мютекс в многопоточных программах - специальный класс-обертка в конструкторе его хватает, в деструкторе освобождает. И иногда не надо держать мютекс во всей функции, а только в определенном месте. Допустим, дальше идут долгие вычисления, не требующие мютекса. Тут логично мютекс освободить. Но это должно встречаться редко. Если вам в функции надо несколько раз такое проворачивать, то надо ее отрефакторить и разбить на части.
    Ответ написан
    Комментировать
  • Уменьшается ли используемая память программы?

    vabka
    @vabka
    Токсичный шарпист

    Стоит ли на практике такое делать?

    В некоторых случаях это действительно полезно.


    Если да, то как лучше оформлять это в коде для читабельности?

    По возможности - лучше не применять.


    то переменная очистится и память требуемая программе уменьшется

    Нет, не очистится. Она же на стеке - под нё уже заранее заготовлено место.

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

    Ну и в случае с одним int это будет байта четыре - что вообще очень смешной объём памяти по сравнению с 16мб*, которые выделяются на стек потока.

    * Цифра не точная, может зависеть от ОС и её настроек.
    Ответ написан
    Комментировать
  • Уменьшается ли используемая память программы?

    bingo347
    @bingo347
    Crazy on performance...
    Во-первых, размер стека фиксирован, стек выделяется в момент запуска потока.
    Во-вторых, компилятор и сам достаточно умный, чтобы переиспользовать стек под разные переменные использование которых не пересекается.
    В-третьих, экономия на спичках, а читаемость ухудшается.

    P.S. такую штуку действительно иногда используют, но ради того чтоб вызвать деструктор в нужной точке кода.
    Ответ написан
    Комментировать
  • Можно ли при вызове функции указать в него тип данных?

    @dima20155
    you don't choose c++. It chooses you
    Полагаю, что вам удобно будет использовать здесь шаблоны, если я правильно понял вопрос.
    Например:
    template <typename T>
    auto search (std::string str) {
        // T - data type
        T res;
        // do something
        return res;
    }
    
    int main () {
        auto a = search<int>("a");
        auto b = search<std::string>("a");
    }
    Ответ написан
    5 комментариев
  • Вопрос по оформлению кода C++?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Каждый вопрос здесь подразумевает довольно большой объем информации при обосновании ответов. В общем смысле весь вопрос сводится к выбору одного из стилей написания кода.
    Обычно стиль кода закреплен стандартом языка, но в стандарте C++ такого нет. Поэтому стиль кода в C++ является предметом выбора каждого. Опять же, обычно для C++ стиль кодирования выбирают наобум, просто потому что понравилось так или потому что в этом стиле пишет любимая компания (GCS - хороший пример выбора на эмоциях и яркий пример очень плохого стиля для C++). Но обоснование своего выбора является очень важным.
    В C++ Core Guidelines есть отдельная секция с описанием стиля кодирования стандартной библиотеки.
    И тем не менее.

    1) Как называть переменные:

    Зависит от того, как будут называться функции и константы.
    И вот почему
    Книжки читают быстро, а код - еще быстрее. При беглом чтении всегда нужно уметь разделять переменные, константы и функции. C++ итак сложный, а если все будет написано в одной манере, то код на C++ будет только еще сложнее.
    Моей рекомендацией будет переменные и локальные константы писать в lower_cast_snake_style, а глобальные константы, макроопределения и элементы нестрогих перечислений писать в UPPER_CAST_SNAKE_STYLE.
    Таким образом достигается единообразие. Стиль змейки во всех своих видах сразу отходит под описание данных, создавая акцент для читателя. Таким образом данные будут читаться легче.
    Свои типы, имена элементов строгих перечислений, имена пространств и имена функций с методами, при этом, стоит писать в UpperCamelCase. Почему все эти и только в одном стиле. А потому что они и концептуально связаны, и разделены настолько, что не перемешиваются при чтении.
    Все составные типы формируют свои пространства имен для вложенных объявлений. Поэтому строгое перечисление, структура, класс или пространство имен разумно называть в едином стиле.
    Функции являются точками входа в подпрограмму, их стилистически неверно было бы писать, например, в lowerCamelCase. Первая заглавная буква много значит при чтении, она является акцентом для читателя.


    2) Что лучше присваивать булевым переменным:

    Литералы 0 и 1 имеют тип int. Если тип переменной - bool, то с какой стати справа от типа должны присутствовать значения с типом int?
    Следует использовать только литералы с типом bool: true и false.
    И вот почему
    При написании кода самым важным является не отражение алгоритма или формальное соответствие стандарту, а именно не вызывать вопросов у читателя. Нужно всегда понимать, что у читателя свой контекст, читатель решает свою задачу, здесь у тебя в коде он только для сбора информации. Его не должны сбивать с толку никакие изыски в написанном коде. Когда читатель видит слева тип bool, а справа значение с типом int, у него появляются вопросы, закрадывается подозрение в достоверности прочитанного, он выпадает из своего контекста. Это - очень плохо.


    3) Как лучше называть переменые итераторы во вложенных циклах:

    Абсолютно каждое имя должно отвечать на вопрос: "Зачем ты тут существуешь?"
    Могут ли однобуквенные имена ответить на этот вопрос внятно через всего одну свою букву? Нет.
    Имя - это смысл. Имя - это причина существования. Имя - это цель использования.
    И вот почему
    Чтение кода вынуждает читателя создавать и поддерживать некоторый контекст читаемого кода. Чем сложнее читателю дается поддержка такого контекста, тем менее понятен читаемый код и тем больше времени уйдет на его изучение. Если же в результате читателя выкинет из контекста решаемой им задачи, то это будет совсем плохо и виноват в этом будет именно плохо написанный код.
    Написанное в коде имя создает отметку в контексте для читателя. Чем более это имя понятно и отвечает общему изложению кода, тем легче читателю дается поддержка контекста читаемого кода.
    Существует масса концепций именования, море семантических пар имен, гора ярких и кучи общих имен. Важным остается только одно - переменная должна своим именем говорить о том, что она хранит, а функция - что делает. Тип должен в своем имени раскрывать природу существования своих объектов.
    Имя должно быть обязательно конкретным. Data, Interface, Iterator - это общие имена, которые не несут никакой конкретики. Общие имена допускаются только в абстрактном коде, т.е. в коде интерфейсов, шаблонов, макросов. Между именем вызываемой функции и именем переменной, принимающей результат вызываемой функции должна быть семантическая связь. И разрыв этой связи допускается только при переходе от общего имени к конкретному. Например так: auto hosts = local_network.GetIterator();. И ведь тут с полпинка все становится понятно, даже думать не надо.


    4) Очень локальный вопрос стоит ли писать else, если ниже нет другого кода ниже:

    Ветвление всегда подразумевает ровно один прыжок или продолжение исполнения кода. Иногда ветвление подразумевает два прыжка: или прыжок в начало альтернативной ветви, или прыжок из конца основной ветви за пределы кода ветвления. Оптимизатор сам выбирает лучший вариант реализации ветвления, более выгодную основную ветвь и от писателя в этом процессе мало что зависит. Но для читателя ветвление и циклы всегда подразумевают очень большое усложнение кода. else стоит писать только тогда, когда без него иначе невозможно.
    И вот почему
    При чтении кода важно чтобы код был понятен читателю. Когда в коде появляется ветвление, читатель вынужден раздвоить контекст читаемого кода для себя. Это всегда сложно. Если читатель видит только одну ветвь в ветвлении, второй контекст читателю дастся легче. Если читатель видит что у ветвления есть две ветви, они будет вынужден напрячься чтобы поддержать сразу два контекста в параллели. И если в конце окажется что вторая ветвь ветвления - это лихо замаскированная линейная часть остатка кода до конца подпрограммы, у читателя снова появятся большие вопросы к целям такого изложения кода.


    6) Писать ли пробел между стандартными функциями и скобками:

    Пробелы нужны для разделения связанных цепочек символов - слов. Код - это запись рассуждений автора о том, что должна делать программа. Код должен читаться как рассказ, в котором слова правильно разделены между собой и правильно расставлены смысловые акценты.
    И вот почему
    Пробелы нужны чтобы отделить одно от другого. С какой целью? Наверное с целью обратить внимание читателя на то, что пробелами отделено. Пробелы сами не являются акцентами, но позволяют акцентировать внимание читателя, в то время как любые другие символы только забирают на себя внимание потому что читателю надо понять смысл присутствия символов в месте их присутствия.
    a==5 - никаких акцентов, ничего не видно. Даже с подсветкой синтаксиса 5 и == читаются плохо и практически неотличимы от a=5 при беглом чтении. В такие моменты у читателя в контекст вносится ошибка или, как минимум, неопределенность ошибки. Но основная цель писателя кода - это написать понятный для чтения код. Поэтому через пробелы надо акцентировать внимание читателя именно на символе эквивалентности - a == 5, позволяя ему правильно прочитать написанное при беглом чтении.
    if(a == 5){ - в этом коде видно только акцент на знаке эквивалентности, но не на выражении условия. if (a == 5) { - уже лучше, но скобки требуют от читателя понять природу их нахождения, что это именно условие, а также вчитаться в левый и правый аргументы условия. if( a == 5 ){ - здесь для читателя акцент поставлен именно на всем условии, теряется только знак начала области видимости - {. И именно поэтому египетские скобки - это плохо. Область видимости должна начинаться на своей строке, потому что для нее нужно создать максимально заметный акцент.
    for (int a = 0; a < 10; a++) { - тут акценты созданы, но не так, чтобы читатель легко прочитал тип счетчика или операцию шага. for( int a = 0; a < 10; a++ ) - а вот тут внимание читателя акцентируется именно на выражении счетчика. И читателю уже не надо выискивать глазами условия, инициализацию и шаг. Это все выделено пробелами и подано для самого комфортного чтения.


    7) Тот же вопрос только про функции, что я сам написал:

    С этого момента тебе должно стать понятно, на чем именно нужно делать акценты чтобы не выводить читателя из себя. Главное - это при написании кода всегда помнить, что возможно читать его будет натуральный маньяк-психопат, который точно знает где ты живешь. И ты точно не хочешь разгневать его своим кодом. :)
    И вот почему
    Код всегда пишется для читателя. Не для транслятора, не для чего-то еще. Транслятору важно только формальное соответствие кода стандарту. Читателю важно понять логику кода, а для этого код надо читать и разбираться в его логических связях. Поэтому, когда пишешь код, всегда нужно думать о том, как его будут читать, не будет ли вопросов к конкретным строчкам, понятны ли имена и отражает ли написанное вложенную в этот код логику.
    Ответ написан
    2 комментария