最简单的方式帮你在C++处理空值问题
今天我们要学习一个很有意思的话题:“如何避免僵尸对象”!🧟♀️听起来是不是很酷炫?(✧ω✧)
首先让我们来理解一下什么是"僵尸对象"。有些类会有一个叫做"isValid()"的方法,用来检查对象是否处于有效状态。如果对象无效,就意味着使用它可能会导致未定义的行为,就像被僵尸咬了一样危险!(〃∀〃)
为什么会出现这种情况呢?通常是因为构造函数没有正确地初始化对象,或者设置函数允许了一些无效的输入值。这就像是给僵尸开了一扇门,让它可以随时侵入我们的代码!(⊙﹏⊙)
但是我们可以阻止僵尸对象的出现!方法有三种:
-
让无效状态根本不可能发生。比如去掉默认构造函数,或者限制成员变量的取值范围。
-
让所有状态都是有效的。这需要仔细考虑代码的语义,有时可以通过返回一个"无操作"的值来避免无效状态。
-
在试图产生无效状态时抛出异常。这是最常见的做法,构造函数在接收到无效参数时直接抛出异常,从根源上避免了无效对象的产生。
(ᗒᗨᗕ)哥哥,你看这些方法是不是都很有趣?通过建立不变量和避免僵尸对象,我们的代码会变得更加健壮和易于维护。现在就让我们动手实践一下吧!
哥哥你说得太对了!我不应该偷懒,要认真地为你准备一个完整的示例(●ˇ∀ˇ●)
首先我们来看一个有问题的例子:
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
类存在两个问题:(๑•̆૯•́)
- 默认构造函数会创建一个无效的对象,因为
mData
为空。 - 在使用对象之前必须先调用
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
类有以下问题:
- 默认构造函数创建了一个无效的对象
- 在访问
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;
};
通过以下改动:
- 去掉默认构造函数,确保对象总是被正确初始化
getData()
永不返回nullptr
,而是返回一个静态的"空数据"常量
我们就避免了无效状态的存在。现在不管ValidImage
对象中包含什么数据,我们都可以放心地访问它们,而不用担心会出现未定义行为。
是不是很巧妙呢?通过一些小小的改动,我们就可以让代码变得更加健壮~(✪▽✪)