为什么`this`等于0x0会导致我的程序崩溃?

4
我正在设计一个简单的连4游戏。到目前为止,我已经有了4个基本类: Colour - 负责表示颜色(RGBA)。包括转换操作。 Player - 表示游戏中的玩家。每个Player都有一个Colour和一个名字。 Board - 表示游戏棋盘。它包含维度以及具有这些维度的Tile的2D向量。 Tile - 是Board内部的一个嵌套类。表示棋盘上的一个空间。每个Tile都有一个Colour和一个指向该方块所有者的std::unique_ptr。所有者最初为nullptr,可以更改一次为Player。颜色最初为透明黑色。
我已经测试了我的Colour类,看起来工作正常。我的Player类也非常完美。但是,我在Board/Tile类上遇到了一些问题。
我的测试包括创建两个玩家和一个棋盘。这些都执行正常。接下来,我遍历棋盘的维度,每个方块执行一次。然后我调用:
board.tile (j, i).claimBy (p2); 

循环通过 i 遍历行,通过 j 遍历列,按照预期的方式进行打印。 tile (j, i) 检索我正在使用的瓷砖。它按预期工作。
导致崩溃的事件链: claimBy (p2) 将瓷砖设置为由玩家2声明。它的实现如下:
bool Board::Tile::claimBy (const Player &owner)
{
    if (!_owner)
    {
        *_owner = owner;
        _colour = owner.colour();
        return true;
    }

    return false;
}

_owner是我的std::unique_ptr<Player>。它首先检查瓦片的所有者是否已经设置(即不为nullptr)。如果没有,则将其中的Player设置为传入的一个。然后更新瓦片的颜色并返回true。如果瓦片以前已被占据,则返回false

根据调试器,崩溃出现在*_owner = owner;这一行。进入该行会进入到struct Player(我声明的Player类),我认为这是隐式拷贝构造函数(记住该类只有Colour _colourstd::string _name)。

再次进入会进入到Colour::operator=(对于拷贝构造函数来说调用这个函数有意义)。以下是定义:

Colour &Colour::operator= (const Colour &rhs)
{
    if (*this != rhs)
    {
        _red = rhs.red();
        _green = rhs.green();
        _blue = rhs.blue();
        _alpha = rhs.alpha();
    }

    return *this;
}

路径变为*this != rhs。这只是对operator==的反向调用,其代码如下:
return red() == rhs.red()
    && green() == rhs.green()
    && blue() == rhs.blue()
    && alpha() == rhs.alpha();

这里的第一个比较 red() == rhs.red() 中的red()只是执行了return _red; 。这就是程序崩溃的地方。调试器显示this(this->_red)为0x0。
我对发生这种情况毫无头绪。我最好的猜测是我错误地使用了智能指针。我之前从未使用过智能指针,但它应该与普通指针非常相似,而且如果指针是nullptr,我认为release不会产生任何作用。 this 为什么会是0x0的原因可能是什么?
编辑:我确信所有内容都已初始化,因为我在每个构造函数中进行了初始化,在成员初始化器中也进行了初始化(例如:Board::Tile::Tile() : _colour (Colours::NONE), _owner (nullptr){}),其中NONE是透明黑色。
我对调试器也不是太熟练,因为我很少使用它来打印调试值。

旁注:瓦片真的应该被称为棋盘吗? - Cameron
@Cameron,is-a关系涉及继承。我在Board中的Tile类的目标是完全包含它。tile函数最初是一个const返回,但我只是为了测试而改变了它。可以从Board外部访问这些瓷砖,但不能创建新的瓷砖,只能处理已有的瓷砖。当然,Board通过瓷砖向量提供非const访问(这就是我提供的has-a关系)。简而言之,一个board对象具有2D瓷砖向量。 - chris
关于你的设计,一个无关的评论:操纵棋盘的游戏逻辑不应该涉及RGBA颜色。我会让方块只是指向控制它的玩家,并让绘制代码根据玩家选择颜色。 - Wyzard
@chris:是的,我在质疑继承 :-) 虽然从Board继承可能很方便,但从概念上讲并不太合理。 - Cameron
@Wyzard,我正在考虑这个问题,因为owner().colour()只需要多加一步简短的操作。 - chris
显示剩余3条评论
2个回答

3

这条线

*_owner = owner;

意思是"复制所有者对象,并将其存储在_owner指向的位置。"问题在于_owner还没有指向任何东西; 它仍然为空。

如果您真的想在玩家控制的每个方块中制作Player对象的副本,则需要执行以下操作:

_owner.reset(new Player(owner));

但是复制 Player 对象是一件奇怪的事情。考虑使用 shared_ptr 替代 - 你可以将 owner_owner 都设为 shared_ptr,并像通常一样将一个指针赋值给另一个指针。


我会研究一下,谢谢。我希望参数是一个实际的“Player”,但我考虑使用unique_ptr,因为它只能是nullptr或一个Player。我想我想错了。 - chris
“unique_ptr” 中的 “unique” 并不意味着一旦您将其指向“Player”,就不能使其指向其他对象。它意味着当您将其指向“Player”时,您承诺没有其他东西指向同一个“Player”,因此在“unique_ptr”超出范围时安全删除“Player”。 “shared_ptr” 允许您拥有对同一“Player”的多个指针(例如,玩家控制的每个图块中都有一个指针),并且只有当所有这些指针超出范围时,“Player”才会被删除。 - Wyzard
“reset” 看起来解决了问题 :) 我曾经考虑过,但事实上每个玩家都是初始化的,并且从那时起没有进行任何更改。上一层是“接口”,负责回合、胜利等。由于这种行为,完全可以复制并存储它,仅用于读取玩家的颜色,同时确定它是否被拥有。我已经阅读了智能指针的相关内容,所以我知道它们之间的区别,但我想我的决策逻辑是错误的。 - chris
制作副本不会产生错误结果(只要您之后不需要修改“Player”),但这是低效的。我建议使用shared_ptr指向单个Player对象,以养成良好习惯,即使当前设计现在并不会引起重大问题。 - Wyzard
@chris:既然你正在复制玩家,能不能也复制一份给我?我在这里需要额外的帮助。更严肃地说,像这样复制玩家违反了至少一种最小惊讶原则:你可以拥有两个不同的玩家对象,它们引用同一个玩家。因此,例如,如果两个瓷砖都没有所有者,则 tile(j,i)._owner == tile(l,k)._owner 将为真。 - Managu
显示剩余4条评论

1

你从一个默认初始化的std::unique_ptr<Player>开始。也就是说,它相当于一个带有一些清理语义的空指针。然后你尝试在语句*_owner=owner;中对其进行解引用,以便你可以对其进行赋值。

因此,语句*_owner=owner;基本上等同于((Player*)NULL)->operator=(owner);,调用隐式赋值运算符。这首先做的事情相当于((Player*)NULL)->_colour=owner._colour;。在这里找到this==NULL并不奇怪;实际上,这是预期的。

问题的解决方法取决于你实际想要发生的情况。每个Board::Tile是否应该被赋予其owner的全新副本?那么你应该使用_owner.reset(new Player(owner))来替代。你只是想让每个方块持有对已存在玩家的引用吗?你能保证Player对象owner的寿命比Board::Tile对象长吗?那么你需要一个原始指针:(在Board::Tile的声明中)Player const *_owner; (在实现中)_owner=&owner;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接