Подтвердить что ты не робот

Возможная ошибка сегментации: правильно ли я использую оператор "this->"?

Я выполняю домашнюю работу, о которой я задаю. Если вы не чувствуете себя комфортно в решении домашней проблемы, я должен сказать, что мой инструктор побудил нас попросить о помощи на этом сайте, когда мы полностью в тупике. Кроме того, я выполнил основную часть задания самостоятельно, и теперь я делаю дополнительную проблему с проблемами. В любом случае, к проблеме!

Будучи новичком в ООП и C++ в целом, мне трудно понять оператор "this- > ". Мы не рассматривали его в классе, но я видел его в другом месте, и я вроде как предполагаю, как он предназначен для использования.

Для назначения мне нужно создать консольную игру Tic-Tac-Toe. Только вызовная часть задания хочет, чтобы мы создали противника ИИ, и мы не получаем дополнительного кредита за выполнение задачи, я просто хочу знать, как это сделать. Я изучаю такие вещи, как минимаксные и игровые деревья, но пока я просто хотел создать функцию "выбрать случайную, открытую точку".

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

void TicTacToe::makeAutoMove(){
    srand(time(NULL));
    int row = rand() % 3 + 1;
    int col = rand() % 3 + 1;
    if(this->isValidMove(row, col)){
        this->makeMove(row, col);
    }else{
        this->makeAutoMove();
    }
}

Единственное, что предназначена для этой функции - сделать ход на доске, предполагая, что она открыта. Плата настроена так:

char board[4][4];

и когда я его распечатаю, он выглядит так:

   1  2  3
1  -  -  - 
2  -  -  -
3  -  -  -

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

Я думаю, что оператор "this- > " функционирует как указатель, и если указатель имеет NULL и к нему обращаются, он может дать мне эту проблему. Это верно? Есть ли способ исправить это?

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

Вот мой файл .cpp, если он помогает:

TicTacToe::TicTacToe()
{
    for(int row = 0; row < kNumRows; row++){
        for(int col = 0; col < kNumCols; col++){
            if(col == 0 && row == 0){
                board[row][col] = ' ';
            }else if(col == 0){
                board[row][col] = static_cast<char>('0' + row);
            }else if(row == 0){
                board[row][col] = static_cast<char>('0' + col);
            }else{
                board[row][col] = '-';
            }
        }
    }
    currentPlayer = 'X';
}

char TicTacToe::getCurrentPlayer(){
    return currentPlayer;
}

char TicTacToe::getWinner(){
    //Check for diagonals (Only the middle square can do this)
    char middle = board[2][2];
    if(board[1][1] == middle && board[3][3] == middle && middle != '-'){
        return middle;
    }else if(middle == board[3][1] && middle == board[1][3] && middle != '-'){
        return middle;
    }

    //Check for horizontal wins
    for(int row = 1; row < kNumRows; row++){
        if(board[row][1] == board[row][2] && board[row][2] == board[row][3] && board[row][1] != '-'){
            return board[row][1];
        }
    }

    //Check for vertical wins
    for(int col = 1; col < kNumCols; col++){
        if(board[1][col] == board[2][col] && board[2][col] == board[3][col] && board[1][col] != '-'){
            return board[1][col];
        }
    }

    //Otherwise, in the case of a tie game, return a dash.
    return '-';
}

void TicTacToe::makeMove(int row, int col){
    board[row][col] = currentPlayer;
    if(currentPlayer == 'X'){
        currentPlayer = 'O';
    }else if(currentPlayer == 'O'){
        currentPlayer = 'X';
    }
}

//TODO: Make sure this works after you make the make-move function
bool TicTacToe::isDone(){
    bool fullBoard = true;
    //First check to see if the board is full
    for(int col = 1; col < kNumCols; col++){
        for(int row = 1; row < kNumRows; row++){
            if(board[row][col] == '-'){
                fullBoard = false;
            }
        }
    }

    //If the board is full, the game is done. Otherwise check for consecutives.
    if(fullBoard){
        return true;
    }else{
        //Check for diagonals (Only the middle square can do this)
        char middle = board[2][2];
        if(board[1][1] == middle && board[3][3] == middle && middle != '-'){
            return true;
        }else if(middle == board[3][1] && middle == board[1][3] && middle != '-'){
            return true;
        }

        //Check for horizontal wins
        for(int row = 1; row < kNumRows; row++){
            if(board[row][1] == board[row][2] && board[row][2] == board[row][3] && board[row][1] != '-'){
                return true;
            }
        }

        //Check for vertical wins
        for(int col = 1; col < kNumCols; col++){
            if(board[1][col] == board[2][col] && board[2][col] == board[3][col] && board[1][col] != '-'){
                return true;
            }
        }
    }
    //If all other tests fail, then the game is not done
    return false;
}

bool TicTacToe::isValidMove(int row, int col){
    if(board[row][col] == '-' && row <= 3 && col <= 3){
        return true;
    }else{
        //cout << "That is an invalid move" << endl;
        return false;
    }
}

void TicTacToe::print(){
    for(int row = 0; row < kNumRows; row++){
        for(int col = 0; col < kNumCols; col++){
            cout << setw(3) << board[row][col];
        }
        cout << endl;

    }
}
4b9b3361

Ответ 1

Общее предисловие: вам почти не нужно явно использовать this. В функции-члене, чтобы ссылаться на переменные-члены или методы-члены, вы просто называете переменную или метод. Как с:

class Foo
{
  int mN;
public:
  int getIt()
  {
    return mN; // this->mN legal but not needed
  }
};

Я думаю, что оператор "this- > " работает как указатель, а если указатель имеет значение NULL, и к нему обращаются, он может дать мне эту проблему. Является это правильно? Есть ли способ исправить это?

this - это указатель, да. (На самом деле это ключевое слово.) Если вы вызываете функцию-член < static класса, this указывает на объект. Например, если бы мы назовем getIt() выше:

int main()
{
  Foo f;
  int a = f.getIt();
}

то this будет указывать на f из main().

У статических функций-членов нет указателя this. this не может быть NULL, и вы не можете изменить значение this.

В С++ существует несколько случаев, когда использование this является одним из способов решения проблемы, а в других случаях используется this . См. эту статью для списка этих ситуаций.

Ответ 2

Я могу воспроизвести ошибку coliru g++ 4.8.1, когда не компилируется с оптимизациями. Как я сказал в комментарии, проблема заключается в srand в сочетании с time и рекурсией:

Возвращаемое значение time часто является временем Unix, в секундах. То есть, если вы вызываете time в течение одной и той же секунды, вы получите одинаковое возвращаемое значение. При использовании этого возвращаемого значения для семени srand (через srand(time(NULL))) вы должны установить одно и то же семя в течение этой секунды.

void TicTacToe::makeAutoMove(){
    srand(time(NULL));
    int row = rand() % 3 + 1;
    int col = rand() % 3 + 1;
    if(this->isValidMove(row, col)){
        this->makeMove(row, col);
    }else{
        this->makeAutoMove();
    }
}

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

Поскольку семя не изменяется в течение одной и той же секунды, вызовы на rand также будут давать одинаковые значения в течение этой секунды - для каждой итерации первый rand всегда будет давать некоторое значение X, а второе всегда будет некоторое значение Y в течение этой секунды.

Если X и Y приводят к недействительному движению, вы получите бесконечную рекурсию до тех пор, пока семена не изменятся. Если ваш компьютер достаточно быстр, он может называть makeAutoMove достаточно часто, чтобы занять достаточное пространство стека в течение этой секунды, чтобы вызвать переполнение стека.


Обратите внимание, что для генерации генератора псевдослучайных чисел, используемого rand, не требуется более одного раза. Как правило, вы делаете только семя один раз, чтобы инициализировать PRNG. Последующие вызовы rand затем генерируют псевдослучайные числа.

Из cppreference:

Каждый раз, когда rand() засевается с помощью srand(), он должен вызывать одну и ту же последовательность значений.

cppreference: rand, srand

Ответ 3

Вот первый проход:

  • Массивы начинают отсчет с нуля. Поэтому вам не нужны +1 в строках типа rand() % 3 + 1;
  • Действительно this - это точка для текущего объекта. Обычно вам не нужно его использовать. т.е. this->makeMove(row, col); и makeMove(row, col); работают одинаково
  • char board[4][4];1 should be char плата [3] [3]; `как вам нужна плата 3x3. См. 1) выше
  • board[row][col] = static_cast<char>('0' + row); - вам не требуется статический литье '0' + row будет достаточно
  • Вам нужно учитывать (1) в остальной части вашего кода.
  • Если возникают проблемы с сегментацией, лучше использовать отладчик. Очень умение учиться

В любом случае - Удачи вам в учебе. Это освежает, чтобы получить новый плакат на этом веб-сайте, который хочет научиться

Ответ 4

Только побочная заметка о рекурсии, эффективности, надежном кодировании и том, как параноик может помочь.

Вот "очищенная" версия вашей проблемной функции.
См. Другие ответы для объяснений о том, что пошло не так с оригиналом.

void TicTacToe::makeAutoMove() {

    // pick a random position
    int row = rand() % 3;
    int col = rand() % 3;

    // if it corresponds to a valid move
    if (isValidMove(row, col)){
        // play it
        makeMove(row, col);
    }else{
        // try again
        makeAutoMove(); // <-- makeAutoMove is calling itself
    }
}

Рекурсия

На простом английском языке вы можете описать, что делает код:

  • выберите случайную (строку, col) пару.
  • если эта пара представляет действительную позицию перемещения, воспроизведите этот ход
  • else повторить попытку

Вызов makeAutoMove() - действительно очень логичный способ повторить попытку, но не настолько эффективен для программирования.

Каждый новый вызов вызовет некоторое выделение памяти в стеке:

  • 4 байта для каждой локальной переменной (всего 8 байтов)
  • 4 байта для обратного адреса

Таким образом, потребление стека будет выглядеть следующим образом:

makeAutoMove             <-- 12 bytes
    makeAutoMove         <-- 24
        makeAutoMove     <-- 36
            makeAutoMove <-- 48
                         <-- etc. 

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

Затем функция будет бесконечно называть себя. Это будет только вопрос времени, когда стек памяти исчерпается, и программа выйдет из строя. И, учитывая вычислительную мощность вашего среднего ПК, авария произойдет в мгновение ока.

Этот крайний случай иллюстрирует (скрытую) стоимость использования рекурсивных вызовов. Но даже если функция в конечном итоге преуспеет, стоимость каждой попытки повторения еще не существует.

То, что мы можем узнать оттуда:

  • Рекурсивные вызовы имеют стоимость
  • они могут привести к сбоям, когда условия завершения не выполняются.
  • многие из них (но не все) могут быть легко заменены петлями, как мы увидим

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

Избегать рекурсии

Чтобы избавиться от этой непослушной рекурсии, мы могли бы повторить попытку следующим образом:

void TicTacToe::makeAutoMove() {
try_again:
    int row = rand() % 3;
    int col = rand() % 3;
    if (isValidMove(row, col)){
        makeMove(row, col);
    }else{
        goto try_again; // <-- try again by jumping to function start
    }
}

В конце концов, нам не нужно снова называть нашу функцию. Прыжки назад к началу этого будет достаточно. Это то, что делает goto.

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

Сохранение регулярного потока программы

Мы не хотим сохранять этот неудобный goto, так как он нарушает обычный поток управления и делает код очень трудным для понимания, поддержки и отладки *.

Мы можем, однако, легко заменить его условным циклом:

void TicTacToe::makeAutoMove() {

    // while a valid move has not been found
    bool move_found = false;
    while (! move_found) {

        // pick a random position
        int row = rand() % 3;
        int col = rand() % 3;

        // if it corresponds to a valid move
        if (isValidMove(row, col)){
            // play it
            makeMove(row, col);
            move_found = true; // <-- stop trying
        }
    }
}

Хорошее: до свидания, мистер goto
Плохо: привет Mrs move_found

Сохранение кода гладким

Мы заменили goto на флаг.
Это уже лучше (поток программы больше не прерывается), но мы добавили некоторую сложность в код.

Мы можем относительно легко избавиться от флага:

    while (true) { // no way out ?!?

        // pick a random position
        int row = rand() % 3;
        int col = rand() % 3;

        // if it corresponds to a valid move
        if (isValidMove(row, col)){
            // play it
            makeMove(row, col);
            break; // <-- found the door!
        }
    }
}

Хорошее: до свидания миссис move_found
Плохо: мы используем break, что немного больше, чем прирученный goto (что-то вроде "goto the end of the loop" ).

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

Использование явных условий выхода

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

Поэтому всегда рекомендуется сделать условия выхода как можно более четкими.

Вот как сделать условие выхода более очевидным:

void TicTacToe::makeAutoMove() {

    // pick a random valid move
    int row, col;
    do {
        row = rand() % 3;
        col = rand() % 3;
    } while (!isValidMove (row, col)); // <-- if something goes wrong, it here

    // play it
    makeMove(row, col);
}

Возможно, вы могли бы сделать это несколько иначе. Это не имеет значения, пока мы достигаем всех этих целей:

  • нет рекурсии
  • нет посторонних переменных
  • значимое условие выхода
  • гладкий код

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

надежность кода

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

Это заставляет вашу функцию зависеть от внешних условий, с неприятными последствиями, если эти условия не выполняются (зависание программы и/или сбой).

Это делает это решение хрупким выбором дизайна.

Паранойя на помощь

Возможно, вы захотите сохранить этот хрупкий дизайн по разным причинам. Например, вы можете предпочесть вино и пообедать в g/f, а не посвятить свой вечер повышению надежности программного обеспечения.

Даже если ваш g/f в конечном итоге узнает, как справиться с выродками, будут случаи, когда лучшее решение, о котором вы можете думать, будет иметь присущие потенциальным несоответствиям.

Это совершенно нормально, если эти несоответствия обнаружены и защищены.

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

Способ сделать это состоит в том, чтобы ввести параноидальное состояние ума, воображая, что каждый системный вызов не удастся, вызывающий любой из ваших функций сделает все возможное, чтобы он сбой, каждый пользователь вход будет исходить от бешеных российских хакеров и т.д.

В нашем случае нам не нужно нанимать бешеных российских хакеров, и системного вызова нет. Тем не менее, мы знаем, как злой программист может вызвать у нас проблемы, поэтому мы постараемся защититься от этого:

void TicTacToe::makeAutoMove() {

    // pick a random valid move
    int row, col;

    int watchdog = 0; // <-- let get paranoid

    do {
        row = rand() % 3;
        col = rand() % 3;

        assert (watchdog++ < 1000); // <-- inconsistency detection

    } while (!isValidMove (row, col));

    // play it
    makeMove(row, col);
}

assert - это макрос, который заставит выйти из программы, если условие, переданное как параметр, не выполняется, с сообщением консоли и/или всплывающим окном, говорящим как-то вроде assertion "watchdog++ < 1000" failed in tictactoe.cpp line 238.
Вы можете видеть это как способ избавиться от программы, если обнаружен фатальный алгоритмический недостаток (т.е. Тот недостаток, который потребует капитального ремонта исходного кода, поэтому есть немного смысла в сохранении этой непоследовательной версии программы).

Добавляя сторожевой таймер, мы гарантируем, что программа будет явно выйти, если обнаружит ненормальное состояние, изящно указывая местоположение потенциальной проблемы (tictactoe.cpp line 238 в нашем случае).

В то время как рефакторинг вашего кода для устранения несоответствий может быть трудным или даже невозможным, обнаружение несоответствий чаще всего очень просто и дешево.

Условие не должно быть очень точным, единственное, что нужно сделать, чтобы ваш код выполнялся в "разумном" согласованном контексте.

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

Поскольку этот код существует только для повышения надежности, он не должен быть эффективным. Это просто средство, чтобы перейти от "почему, черт возьми, моя программа висит?!?" ситуация с "dang", я, должно быть, назвал makeAutoMove после конечной игры (ближайшей) немедленной реализации.

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

На самом деле есть средства для сохранения параноидального кода без ущерба для эффективности, но это другая история.

То, что это сводится к:

  • привыкнуть замечать потенциальные несоответствия в вашем коде, особенно если эти несоответствия могут иметь серьезные последствия.
  • постарайтесь убедиться, что как можно больше фрагментов кода может обнаружить несоответствия.
  • посыпать свой код параноидальными проверками, чтобы увеличить ваши шансы обнаружить неправильные шаги раньше.

Рефакторинг кода

В идеальном мире каждая функция должна давать последовательный результат и оставлять систему в согласованном состоянии. Это редко происходит в реальной жизни, если вы не принимаете некоторые ограничения для вашего творчества.

Тем не менее, было бы интересно посмотреть, что вы могли бы достичь, если бы разработали tic-tac-toe 2.0 с учетом этих рекомендаций. Я уверен, что вы найдете много полезных рецензентов здесь, в StackOverflow.

Не стесняйтесь спрашивать, нашли ли вы какие-то интересные моменты во всех этих нарядах, и добро пожаловать в чудесный мир вундеркиндов:)
(kuroi dot neko at wanadoo dot fr)


* goto может выглядеть достаточно безвредным в таком маленьком примере, но вы можете мне доверять: злоупотребление goto приведет вас к миру боли. Просто не делайте этого, если у вас нет очень, очень веской причины.