可能的分段错误:我使用“this->”操作员正确?

时间:2014-01-13 19:57:16

标签: 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();
    }
}

这个功能唯一要做的就是在棋盘上移动,假设它是开放的。董事会的设置如下:

char board[4][4];

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

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

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

我认为“这 - >” operator作为指针运行,如果指针为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;

    }
}

4 个答案:

答案 0 :(得分:10)

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

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

我认为“这 - &gt;” operator作为指针,如果是   指针是NULL,它被访问它可以给我这个问题。是   这个对吗?有办法解决这个问题吗?

this是一个指针,是的。 (实际上,它是一个关键字。)如果您调用类的非static成员函数,this指向该对象。例如,如果我们在上面调用getIt()

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

然后this会从f指向main()

静态成员函数没有this指针。 this不能为NULL,并且您无法更改this的值。

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

答案 1 :(得分:9)

使用优化进行编译时,我可以在coliru's g++4.8.1上重现该错误。正如我在评论中所说,问题是srand结合time和递归:

The return value of time is often the Unix time,以秒为单位。也就是说,如果您在同一秒内调用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的迭代,则每次调用都将占用堆栈的一小部分。因此,当经常调用时,这将产生Stack Overflow(幸运的是,你去了正确的站点)。

由于种子在同一秒内没有变化,对rand的调用 在该秒内产生相同的值 - 对于每次迭代,第一个rand将始终产生某个值为X,第二个值在该秒内总是为某个值。

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


请注意,不需要为rand多次使用的伪随机数生成器播种。通常,您只播种一次,以初始化PRNG。随后对rand的调用会产生伪随机数。

来自cppreference:

  

每次rand()播种srand()时,都必须生成相同的值序列。

cppreference:randsrand

答案 2 :(得分:5)

这是第一遍:

  1. 数组从零开始计数。因此,您不需要像rand() % 3 + 1;
  2. 这样的行中的+1
  3. 确实this是指向当前对象的点。通常你不需要使用它。即this->makeMove(row, col);makeMove(row, col);使用相同的
  4. char board[4][4];1 should be char board [3] [3];`因为你想要3x3板。见上文1)
  5. board[row][col] = static_cast<char>('0' + row); - 您不需要静态转换'0' + row就足够了
  6. 您需要在代码的其余部分中考虑(1)
  7. 如果出现分段问题,最好使用调试器。学习的一项技巧
  8. 无论如何 - 祝你学习顺利。在这个热衷于学习的网站上获得一张新海报令人耳目一新

答案 3 :(得分:5)

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

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

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
    }
}

递归

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

  • 挑选一对随机(一排,一对)夫妻。
  • 如果这对夫妇代表一个有效的移动位置,那就玩那个移动
  • 其他再试一次

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

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

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

因此堆栈消耗将如下所示:

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

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

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

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

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

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

作为旁注中的旁注,正如 dyp 正确指出的那样,现代编译器非常聪明,他们可以出于各种原因检测代码中的某些模式,以便消除它们这种递归电话。
尽管如此,你永远都不知道你的特定编译器是否足够聪明,可以从你邋fe的脚下移除香蕉皮,所以如果你问我,最好完全避免草率。

避免递归

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

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
坏事:你好,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(类似于&#34;转到循环的结尾&#34;)。

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

使用显式退出条件

退出条件对于确定一段代码是否有效非常重要(我们的函数永远被卡住的原因正是在某些情况下,从未满足退出条件)。

因此,尽可能清楚地突出退出条件总是一个好主意。

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

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);
}

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

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

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

代码健壮性

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

这使得你的功能依赖于外部条件,如果不满足这些条件(程序挂断和/或崩溃),会产生令人讨厌的后果。

这使得此解决方案成为 脆弱的设计选择

妄想救济

出于各种原因,您可能希望保留这种脆弱的设计。例如,您可能更喜欢葡萄酒和葡萄酒。用你的g / f而不是专心致志地改善软件的稳健性。

即使您的g / f最终了解如何cope with geeks,也会出现您能想到的最佳解决方案存在固有潜在不一致的情况。

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

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

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

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

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);
}

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

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

虽然将代码重构为消除不一致可能很困难甚至不可能,但检测不一致通常非常简单且便宜。

条件不一定非常精确,唯一的一点是确保你的代码在合理的情况下执行&#34;一致的背景。

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

由于此代码只是为了提高稳健性,因此不必高效。它只是一种从#34出发的方法;为什么我的程序会挂起来?!?&#34;在结束比赛之后我必须打电话给makeAutoMove&#34; (近)立即实现。

一旦您测试并证明了自己的计划,并且 真的 有充分的理由(即,如果您的偏执检查会导致严重的性能问题)你可以决定清理那些偏执的代码,在你的来源中留下非常明确的评论,说明这条代码的使用方式。

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

归结为:

  • 习惯于注意代码中可能存在的不一致之处,尤其是当这些不一致会产生严重后果时
  • 尝试确保尽可能多的代码段可以检测到不一致
  • 将你的代码撒上偏执检查,以增加你早期发现错误行动的机会

代码重构

在理想的世界中,每个函数都应该给出一致的结果并使系统保持一致状态。这在现实生活中很少发生,除非你接受some limitations to your creativity

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

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


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