可能的分段错误:我是否正确使用"this->"运算符?

she*_*ach 24 c++ oop this

我正在做一个我有问题的作业问题.如果你不方便协助解决作业问题,我应该说我的导师鼓励我们在完全难倒的时候在这个网站上寻求帮助.另外,我已经完成了自己的任务的基本部分,现在我正在做一个可选的挑战问题.无论如何,关于问题!

作为一般的OOP和C++的新手,我无法理解"this->"运算符.我们没有在课堂上介绍它,但我已经在其他地方看到了它,我猜它是如何被使用的.

对于作业,我必须创建一个基于控制台的Tic-Tac-Toe游戏.只有作业的挑战部分需要我们创建一个AI对手,而我们没有获得任何额外的功劳来应对挑战,我只是想知道如何去做.我正在研究像minimax和游戏树这样的东西,但是现在我只是想创建一个"选择一个随机的,开放的"功能.

我有一个名为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();
    }
}
Run Code Online (Sandbox Code Playgroud)

这个函数唯一要做的就是在板上移动,假设它是开放的.董事会的设置如下:

char board[4][4];
Run Code Online (Sandbox Code Playgroud)

当我打印它时,它看起来像:

   1  2  3
1  -  -  - 
2  -  -  -
3  -  -  -
Run Code Online (Sandbox Code Playgroud)

问题在于,有时计算机会进行移动,因为函数的随机性,我会发现一个难以追踪的错误.我认为这是一个段错误,但我无法分辨,因为我无法在调试器中复制它.

我认为"this->"运算符用作指针,如果指针为NULL并且它被访问它可能会给我这个问题.它是否正确?有没有办法来解决这个问题?

我理解这对社区的许多成员来说可能是一个非常低级别的问题,但我会感谢你的帮助,只要它没有附带关于这是多么微不足道的讽刺言论,或者我必须是多么愚蠢.我在学习,这意味着我有时会遇到一些愚蠢的问题.

如果它有帮助,这里有更多我的.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;

    }
}
Run Code Online (Sandbox Code Playgroud)

Joh*_*ing 10

一般序言:您几乎不需要this明确使用.在成员函数中,为了引用成员变量或成员方法,只需命名变量或方法即可.与:一样:

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

我认为"this->"运算符用作指针,如果指针为NULL并且它被访问它可能会给我这个问题.它是否正确?有没有办法来解决这个问题?

this是一个指针,是的.(实际上,它是一个关键字.)如果调用类的非static成员函数,则this指向该对象.例如,如果我们打电话给getIt()上面:

int main()
{
  Foo f;
  int a = f.getIt();
}
Run Code Online (Sandbox Code Playgroud)

然后this将指向fmain().

静态成员函数没有this指针. this不能为NULL,并且不能更改值this.

在C++中有几种情况,其中使用this是解决问题的一种方法,以及this 必须使用的其他情况.有关这些情况的列表,请参阅帖子.


dyp*_*dyp 9

没有使用优化进行编译时,我可以重现coliru的g ++ 4.8.1上的错误.正如我在评论中所说,问题是结合和递归:srandtime

返回值time通常是Unix时间,以秒为单位.也就是说,如果你time在同一秒内调用,你将获得相同的返回值.当使用此返回值来播种srand(via 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

如果不使用优化进行编译,或者编译器需要使用堆栈空间进行迭代makeAutoMove,则每次调用都会占用堆栈的一小部分.因此,当经常调用时,这将产生Stack Overflow(幸运的是,你去了正确的站点).

作为种子不会在同一秒内发生变化,呼吁rand产生内的第二次相同的值-每次迭代,第一兰特总是会产生一些数值X和第二始终是第二个中的一些值Y.

如果X和Y导致无效移动,则在播种发生变化之前,您将获得无限递归.如果您的计算机足够快,它可能makeAutoMove经常调用足以在该秒内占用足够的堆栈空间以导致堆栈溢出.


请注意,不需要rand多次使用伪随机数生成器.通常情况下,你只有一次种子,初始化PRNG.随后的调用rand然后产生伪随机数.

从cppreference:

每次rand()播种时srand(),它必须产生相同的值序列.

cppreference : rand,srand


Ed *_*eal 5

这是第一关:

  1. 数组从零开始计数.所以你不需要行中的+1rand() % 3 + 1;
  2. 确实this是当前对象的一个​​要点.通常你不需要使用它.即this->makeMove(row, col);makeMove(row, col);工作一样
  3. char board[4][4];1 should bechar board [3] [3];`因为你想要3x3板.见上文1)
  4. board[row][col] = static_cast<char>('0' + row);- 你不需要静态强制转换'0' + row就足够了
  5. 您需要在代码的其余部分中考虑(1)
  6. 如果出现分段问题,最好使用调试器.学习的一项技巧

无论如何 - 祝你学习顺利.在这个渴望学习的网站上获得一张新海报令人耳目一新

  • @zsherman良好编程的一些关键是学习从抽象,模块化,准确名称和(避免)依赖性的角度思考.将行/列号和板内容放在同一个数组中会混合抽象,并使代码依赖于您打印它的特定方式.你的"董事会"不仅仅是游戏板,而是董事会加上这些数字......将这些分开是更好的选择.编号应该是显示功能的一部分......它与游戏玩法无关. (2认同)

kur*_*eko 5

只是关于递归,效率,强大编码以及偏执狂如何帮助的旁注.

这是您有问题的功能的"清理"版本.
有关原始版本出现问题的解释,请参阅其他答案.

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
    }
}
Run Code Online (Sandbox Code Playgroud)

递归

用简单的英语,您可以描述代码的作用:

  • 选择一个随机(行,col)夫妇.
  • 如果这对夫妇代表一个有效的移动位置,那就玩这个移动
  • 否则再试一次

调用makeAutoMove()确实是一种非常合乎逻辑的再次尝试方式,但是编程方式不那么有效.

每次新调用都会在堆栈上产生一些内存分配:

  • 每个局部变量4个字节(总共8个字节)
  • 返回地址为4个字节

所以堆栈消耗将如下所示:

makeAutoMove             <-- 12 bytes
    makeAutoMove         <-- 24
        makeAutoMove     <-- 36
            makeAutoMove <-- 48
                         <-- etc. 
Run Code Online (Sandbox Code Playgroud)

想象一下,在无法成功的情况下(当游戏结束且没有更多有效的移动可用时)你无意中调用了这个函数.

然后该函数将无休止地调用自己.堆栈内存耗尽并且程序崩溃只是时间问题.而且,考虑到普通PC的计算能力,崩溃将在眨眼间发生.

这种极端情况说明了使用递归调用的(隐藏)成本.但即使该功能最终成功,每次重试的成本仍然存在.

我们可以从那里学到的东西:

  • 递归调用有成本
  • 当不符合终止条件时,它们可能导致崩溃
  • 正如我们将要看到的,很多(但不是全部)很容易被循环取代

正如旁注所暗示的那样,现代编译器非常聪明,它们可以出于各种原因检测代码中的某些模式,这些模式允许它们消除这种类型的递归调用.
然而,你永远都不知道你的特定编译器是否足够聪明,可以从你邋fe的脚下移除香蕉皮,所以如果你问我,最好完全避免邋iness.

避免递归

为了摆脱那种顽皮的递归,我们可以再次实现这样的尝试:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

毕竟,我们真的不需要再次调用我们的函数.跳回到它的开始就足够了.这就是它的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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

好处:再见先生goto
坏人:你好,搬家的女士

保持代码光滑

我们把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!
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

好处:再见Mrs. move_found
糟糕:我们使用a break,这只是一种驯服goto(比如"转到循环结束").

我们可以在那里结束改进,但是这个版本仍然有些烦人:循环的退出条件隐藏在代码中,这使得初看起来更难理解.

使用显式退出条件

退出条件对于确定一段代码是否有效是特别重要的(我们的函数永远被卡住的原因恰恰是在某些情况下永远不会满足退出条件).

因此,尽可能清楚地使退出条件脱颖而出总是一个好主意.

这是一种使退出条件更明显的方法:

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's here

    // play it
    makeMove(row, col);
}
Run Code Online (Sandbox Code Playgroud)

你可能会有点不同.只要我们实现所有这些目标,这无关紧要:

  • 没有递归
  • 没有多余的变数
  • 有意义的退出条件
  • 光滑的代码

当您将最新的细化与原始版本进行比较时,您会发现它已经变异为显着不同的东西.

代码稳健性

正如我们所看到的,如果没有更多合法的移动(即游戏已经结束),这个功能永远不会成功.
这种设计可以工作,但它需要你的算法的其余部分,以确保在调用此函数之前正确检查最终游戏条件.

这使得您的函数依赖于外部条件,如果不满足这些条件(程序挂起和/或崩溃)会产生令人讨厌的后果.

这使得该解决方案成为一种脆弱的设计选择.

偏执狂来救援

出于各种原因,您可能希望保持这种脆弱的设计.例如,您可能更愿意品尝和品尝您的g/f,而不是将您的晚上专注于软件稳健性改进.

即使你的g/f最终学会了如何应对极客,也会出现你能想到的最佳解决方案存在固有的潜在不一致的情况.

这是完全可以的,只要发现并防止这些不一致.

代码稳健性的第一步是确保检测到潜在危险的设计选择,如果不完全纠正的话.

这样做的一种方式是进入一种偏执的心态,想象每个系统调用都会失败,你的任何函数的调用者都会尽力使它崩溃,每个用户输入都将来自一个狂热的俄罗斯黑客,等等.

在我们的案例中,我们不需要聘请狂热的俄罗斯黑客,并且看不到系统调用.不过,我们知道一个邪恶的程序员如何让我们陷入困境,所以我们会尽力防范:

void TicTacToe::makeAutoMove() {

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

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

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

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

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

    // play it
    makeMove(row, col);
}
Run Code Online (Sandbox Code Playgroud)

assert是一个宏,如果不满足作为参数传递的条件,将强制程序退出,控制台消息和/或弹出窗口说明如下assertion "watchdog++ < 1000" failed in tictactoe.cpp line 238.
你可以看到它是一种摆脱程序的方法,如果一个致命的算法缺陷(即需要源代码大修的那种缺陷,那么保持程序运行的这个不一致版本没有什么意义)已被检测到.

通过添加看门狗,我们确保程序在检测到异常情况时会明确退出,并优雅地指出潜在问题的位置(tictactoe.cpp line 238在我们的例子中).

虽然重构代码以消除不一致可能很困难甚至不可能,但检测不一致通常非常容易且便宜.

条件不必非常精确,唯一的一点是确保您的代码在"合理"一致的上下文中执行.

在这个例子中,获得合法移动的实际试验次数并不容易估计(它是基于击中禁止移动的单元的累积概率),但我们可以很容易地发现在1000之后未能找到合法移动尝试意味着算法严重错误.

由于此代码只是为了提高稳健性,因此不必高效.这只是一种方法,从"为什么我的程序挂起来?!?" 情况到"当,我必须makeAutoMove在结束游戏后打电话"(近)立即实现.

一旦你已经测试并证明你的程序,如果你有真正的了解(即,如果你的偏执检查导致严重的性能问题),你可以决定清理好的原因,偏执代码,留下你对源非常明确的意见应使用此特定代码的方式.

实际上有办法在不牺牲效率的情况下保持偏执代码的存在,但这是另一个故事.

它归结为:

  • 习惯于注意代码中潜在的不一致性,特别是当这些不一致会产生严重后果时
  • 尽量确保尽可能多的代码片段可以检测到不一致
  • 撒上偏执检查代码,以增加早期发现错误动作的机会

代码重构

在理想的世界中,每个函数都应该给出一致的结果并使系统保持一致状态.这在现实生活中很少发生,除非你接受一些创造力的限制.

但是,如果您考虑到这些指导原则设计了一个井字游戏,那么看看您可以实现的目标可能会很有趣.我相信你会在StackOverflow上找到很多有用的评论者.

随意询问您是否在所有这些咆哮中找到了一些兴趣点,并欢迎来到极客的奇妙世界:)
(wanadoo dot fr的kuroi dot neko)


*goto在这么小的例子中看起来可能看起来很无害,但是你可以相信我:滥用goto会让你陷入痛苦的世界.除非你有一个非常非常好的理由,否则不要这样做.