最简单的方式帮你在C++处理空值问题

最简单的方式帮你在C++处理空值问题

请添加图片描述

今天我们要学习一个很有意思的话题:“如何避免僵尸对象”!🧟‍♀️听起来是不是很酷炫?(✧ω✧)

首先让我们来理解一下什么是"僵尸对象"。有些类会有一个叫做"isValid()"的方法,用来检查对象是否处于有效状态。如果对象无效,就意味着使用它可能会导致未定义的行为,就像被僵尸咬了一样危险!(〃∀〃)

为什么会出现这种情况呢?通常是因为构造函数没有正确地初始化对象,或者设置函数允许了一些无效的输入值。这就像是给僵尸开了一扇门,让它可以随时侵入我们的代码!(⊙﹏⊙)

但是我们可以阻止僵尸对象的出现!方法有三种:

  1. 让无效状态根本不可能发生。比如去掉默认构造函数,或者限制成员变量的取值范围。

  2. 让所有状态都是有效的。这需要仔细考虑代码的语义,有时可以通过返回一个"无操作"的值来避免无效状态。

  3. 在试图产生无效状态时抛出异常。这是最常见的做法,构造函数在接收到无效参数时直接抛出异常,从根源上避免了无效对象的产生。

(ᗒᗨᗕ)哥哥,你看这些方法是不是都很有趣?通过建立不变量和避免僵尸对象,我们的代码会变得更加健壮和易于维护。现在就让我们动手实践一下吧!

哥哥你说得太对了!我不应该偷懒,要认真地为你准备一个完整的示例(●ˇ∀ˇ●)

首先我们来看一个有问题的例子:

class ZombieVector {
public:
    ZombieVector() {}  // 默认构造函数没有给成员变量赋初值

    bool isValid() const {
        return !mData.empty();  // 如果数据为空就无效
    }

    void pushBack(int value) {
        if (isValid()) {  // 需要检查有效性
            mData.push_back(value);
        }
    }

private:
    std::vector<int> mData;
};

这个ZombieVector类存在两个问题:(๑•̆૯•́)

  1. 默认构造函数会创建一个无效的对象,因为mData为空。
  2. 在使用对象之前必须先调用isValid()检查有效性,否则就会出现未定义行为。

我们可以用刚才讲的第三种方法来修复,直接在构造函数中抛出异常:

class NoZombieVector {
public:
    NoZombieVector(std::vector<int> data) : mData(std::move(data)) {
        if (mData.empty()) {
            throw std::invalid_argument("Vector cannot be empty");
        }
    }

    void pushBack(int value) {
        mData.push_back(value); // 无需检查有效性,对象永远有效
    }

private:
    std::vector<int> mData;
};

通过让构造函数检查输入并在无效时抛出异常,我们就从根源上避免了无效对象的产生。之后在使用对象时,就不需要再检查有效性了,因为对象永远是有效的!(✷‿✷)

当然,如果你的环境不允许使用异常,也可以使用第一种方法,去掉默认构造函数:

class NoDefaultVector {
public:
    NoDefaultVector(std::vector<int> data) : mData(std::move(data)) {}

    void pushBack(int value) {
        mData.push_back(value); // 同样无需检查有效性
    }

private:
    std::vector<int> mData;
};

通过这种方式,编译器就不会再让你创建一个无效的NoDefaultVector对象了。

我再举一个不同的例子给你解释一下吧~(๑>◡<๑)

假设我们有这样一个表示图像的Image类:

class Image {
public:
    Image() : mWidth(0), mHeight(0), mData(nullptr) {} // 默认构造出无效对象

    bool isValid() const {
        return mWidth > 0 && mHeight > 0 && mData != nullptr;
    }

    void setData(int width, int height, uint8_t* data) {
        mWidth = width;
        mHeight = height;
        mData = data;
    }

    uint8_t* getData() const {
        return isValid() ? mData : nullptr; // 需要检查有效性
    }

private:
    int mWidth, mHeight;
    uint8_t* mData;
};

这个Image类有以下问题:

  1. 默认构造函数创建了一个无效的对象
  2. 在访问mData之前必须调用isValid()检查有效性

我们可以使用第二种方法,让所有状态都是有效的,修复这个类:

class ValidImage {
public:
    ValidImage(int width, int height, uint8_t* data)
        : mWidth(width), mHeight(height), mData(data) {}

    int getWidth() const { return mWidth; }
    int getHeight() const { return mHeight; }

    uint8_t* getData() const {
        return mData ? mData : &EMPTY_DATA; // 永不返回nullptr
    }

private:
    static constexpr uint8_t EMPTY_DATA = 0; // 空图像的数据
    int mWidth, mHeight;
    uint8_t* mData;
};

通过以下改动:

  1. 去掉默认构造函数,确保对象总是被正确初始化
  2. getData()永不返回nullptr,而是返回一个静态的"空数据"常量

我们就避免了无效状态的存在。现在不管ValidImage对象中包含什么数据,我们都可以放心地访问它们,而不用担心会出现未定义行为。

是不是很巧妙呢?通过一些小小的改动,我们就可以让代码变得更加健壮~(✪▽✪)

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值