8 位定点小数
8 位有符号整型的值域是 [-128, 127]
, 8 位定点小数就是将这个整型视为一个分数的分子,这个分数的分母是 21282^{128}2128 。
这样这个分数的值域就是:
−1,127128\] \[-1, \\frac{127}{128}\] \[−1,128127
于是就可以表示小于 1 的数了。
分辨率为:
128−1=127=1128 \frac{1}{2^{8-1}}=\frac{1}{2^7}=\frac{1}{128} 28−11=271=1281
所以 8 位定点小数本质上就是一个整型值 int8_t, 只不过计算的时候要记得除以 128, 这样才能得到实际值。
定点小数与分数类
在 C++ 中可以直接设计一个分数类
cpp
///
/// @brief 分数类
///
class Int64Fraction final :
public base::ICanToString
{
private:
int64_t _num = 0;
int64_t _den = 1;
public:
/* #region 构造函数 */
///
/// @brief 默认构造,分子为 0,分母为 1.
///
constexpr Int64Fraction() = default;
///
/// @brief 从整型值构造。分子为 num, 分母为 1.
///
/// @param num 分子。
///
template <typename T>
requires(std::is_integral_v<T>)
constexpr Int64Fraction(T num)
{
_num = num;
_den = 1;
}
///
/// @brief 通过分子,分母进行构造。
/// @param num 分子
/// @param den 分母
///
constexpr Int64Fraction(int64_t num, int64_t den)
{
SetNum(num);
if (num == 0)
{
SetDen(1);
}
else
{
SetDen(den);
}
}
///
/// @brief 通过浮点数构造。
///
/// @param value
///
constexpr Int64Fraction(base::Double const &value)
{
if (value.Value() == 0.0)
{
SetNum(0);
SetDen(1);
return;
}
double db = value.Value();
// 要保证分数计算过程不溢出,需要保证 factor * db <= INT64_MAX.
int64_t factor = INT64_MAX / base::ceil(db);
base::Int64Fraction int_part{static_cast<int64_t>(db)};
db -= static_cast<double>(int_part);
db *= factor;
base::Int64Fraction fractional_part{static_cast<int64_t>(db), factor};
*this += int_part + fractional_part;
}
/* #endregion */
/* #region 分子分母 */
///
/// @brief 获取分子。
///
/// @return
///
constexpr int64_t Num() const
{
return _num;
}
///
/// @brief 设置分子。
///
/// @param value
///
constexpr void SetNum(int64_t value)
{
_num = value;
}
///
/// @brief 获取分母。
///
/// @return
///
constexpr int64_t Den() const
{
return _den;
}
///
/// @brief 设置分母。
///
/// @param value
///
constexpr void SetDen(int64_t value)
{
if (value == 0)
{
throw std::invalid_argument{"分母不能为 0."};
}
_den = value;
}
/* #endregion */
/* #region 计算函数 */
///
/// @brief 化简自身。
///
///
constexpr void Simplify()
{
if (_den == 0)
{
throw std::invalid_argument{"分母不能为 0."};
}
if (_num == 0)
{
_den = 1;
return;
}
// 分子分母同时除以最大公约数
int64_t gcd_value = std::gcd(_num, _den);
int64_t scaled_num = _num / gcd_value;
int64_t scaled_den = _den / gcd_value;
if (scaled_den < 0)
{
// 如果分母小于 0,分子分母同时取相反数
scaled_num = -scaled_num;
scaled_den = -scaled_den;
}
_num = scaled_num;
_den = scaled_den;
}
///
/// @brief 化简后的形式。
///
/// @note 返回化简后的值,不改变自身。
///
/// @return
///
constexpr Int64Fraction SimplifiedForm() const
{
base::Int64Fraction ret{*this};
ret.Simplify();
return ret;
}
///
/// @brief 倒数
/// @return
///
constexpr Int64Fraction Reciprocal() const
{
base::Int64Fraction ret{_den, _num};
return ret.SimplifiedForm();
}
///
/// @brief 取绝对值。
///
/// @return
///
constexpr Int64Fraction Abs() const
{
if (*this < 0)
{
return -*this;
}
return *this;
}
///
/// @brief 向下取整
/// @return
///
constexpr int64_t Floor() const
{
int64_t ret = Div();
if (*this < 0)
{
if (Mod())
{
ret -= 1;
}
}
else
{
/* 因为 C++ 除法近 0 截断,所以如果 Div >0 ,本来就是向下取整了,
* 不用再额外的操作了。
*
* Div = 0 就更不用说了,也不用什么额外的操作,直接返回 0 就完事了。
*/
}
return ret;
}
///
/// @brief 向上取整
/// @return
///
constexpr int64_t Ceil() const
{
int64_t ret = Div();
if (*this > 0)
{
if (Mod())
{
ret += 1;
}
}
return ret;
}
///
/// @brief 获取分子除以分母的值
/// @return
///
constexpr int64_t Div() const
{
return _num / _den;
}
///
/// @brief 获取分子除以分母的余数
/// @return
///
constexpr int64_t Mod() const
{
return _num % _den;
}
///
/// @brief 降低分辨率。
///
/// @param resolution
///
constexpr void ReduceResolution(base::Int64Fraction const &resolution)
{
if (resolution <= 0)
{
throw std::invalid_argument{CODE_POS_STR + "分辨率不能 <= 0."};
}
Simplify();
if (_den >= resolution._den)
{
// 本分数的分母比 resolution 的分母大,说明本分数的分辨率大于 resolution.
//
// 首先需要减小本分数的分母,将分辨率降下来。分子分母同时除以一个系数进行截断,
// 从而降低分辨率。
int64_t multiple = _den / resolution._den;
// 首先将分辨率降低到 1 / resolution._den.
_num /= multiple;
_den /= multiple;
// 如果 resolution._num > 1, 则还不够,刚才的分辨率降低到 1 / resolution._den 了,
// 还要继续降低。
_num = _num / resolution._num * resolution._num;
}
else
{
// 本分数的分母比 resolution 的分母小。但这不能说明本分数的分辨率小于 resolution,
// 因为 resolution 的分子可能较大。
//
// 将 resolution 的分子分母同时除以一个系数,将 resolution 的分母调整到与本分数的分母
// 相等,然后看一下调整后的 resolution 的分子,如果不等于 0, 即没有被截断成 0, 说明原本的
// 分子确实较大,大到足以放大 resolution 的大分母所导致的小步长,导致步长很大,分辨率低。
// 这种情况下本分数的分辨率才是高于 resolution, 才需要降低分辨率。
int64_t multiple = resolution._den / _den;
int64_t div = resolution._num / multiple;
if (div != 0)
{
_num = _num / div * div;
}
}
Simplify();
}
/* #endregion */
constexpr Int64Fraction operator-() const
{
Int64Fraction ret{-_num, _den};
return ret.SimplifiedForm();
}
/* #region 四则运算符 */
constexpr Int64Fraction operator+(Int64Fraction const &value) const
{
// 通分后的分母为本对象的分母和 value 的分母的最小公倍数
int64_t scaled_den = std::lcm(_den, value.Den());
// 通分后的分子为本对象的分子乘上分母所乘的倍数
int64_t scaled_num = _num * (scaled_den / _den);
int64_t value_scaled_num = value.Num() * (scaled_den / value.Den());
Int64Fraction ret{
scaled_num + value_scaled_num,
scaled_den,
};
return ret.SimplifiedForm();
}
constexpr Int64Fraction operator-(Int64Fraction const &value) const
{
Int64Fraction ret = *this + (-value);
return ret.SimplifiedForm();
}
constexpr Int64Fraction operator*(Int64Fraction const &value) const
{
base::Int64Fraction ret;
ret.SetNum(_num * value.Num());
ret.SetDen(_den * value.Den());
return ret.SimplifiedForm();
}
constexpr Int64Fraction operator/(Int64Fraction const &value) const
{
Int64Fraction ret{*this * value.Reciprocal()};
return ret.SimplifiedForm();
}
/* #endregion */
/* #region 自改变四则运算符 */
constexpr Int64Fraction &operator+=(Int64Fraction const &value)
{
*this = *this + value;
return *this;
}
constexpr Int64Fraction &operator-=(Int64Fraction const &value)
{
*this = *this - value;
return *this;
}
constexpr Int64Fraction &operator*=(Int64Fraction const &value)
{
*this = *this * value;
return *this;
}
constexpr Int64Fraction &operator/=(Int64Fraction const &value)
{
*this = *this / value;
return *this;
}
/* #endregion */
///
/// @brief 将分数转化为字符串
/// @return
///
virtual std::string ToString() const override
{
return std::to_string(_num) + " / " + std::to_string(_den);
}
/* #region 强制转换运算符 */
constexpr explicit operator int64_t() const
{
return Div();
}
constexpr explicit operator uint64_t() const
{
return static_cast<uint64_t>(Div());
}
constexpr explicit operator int32_t() const
{
return static_cast<int32_t>(Div());
}
constexpr explicit operator uint32_t() const
{
return static_cast<uint32_t>(Div());
}
constexpr explicit operator int16_t() const
{
return static_cast<int16_t>(Div());
}
constexpr explicit operator uint16_t() const
{
return static_cast<uint16_t>(Div());
}
constexpr explicit operator int8_t() const
{
return static_cast<int8_t>(Div());
}
constexpr explicit operator uint8_t() const
{
return static_cast<uint8_t>(Div());
}
constexpr explicit operator double() const
{
base::Int64Fraction copy{*this};
double int_part = static_cast<double>(copy.Div());
copy -= copy.Div();
double fraction_part = static_cast<double>(copy.Num()) / static_cast<double>(copy.Den());
return int_part + fraction_part;
}
/* #endregion */
/* #region 比较 */
///
/// @brief 本对象等于 another.
/// @param another
/// @return
///
constexpr bool operator==(Int64Fraction const &another) const
{
if (Num() == 0 && another.Num() == 0)
{
/* 2 个分子都为 0 直接返回相等,这样更加安全,避免分子都为 0
* 分母不相等时错误地将两个分数判断为不相等。
*/
return true;
}
Int64Fraction f1 = SimplifiedForm();
Int64Fraction f2 = another.SimplifiedForm();
return f1.Num() == f2.Num() && f1.Den() == f2.Den();
}
///
/// @brief 本对象大于 another.
/// @param another
/// @return
///
constexpr bool operator>(Int64Fraction const &another) const
{
// 先化简,避免分母为负数,然后使用交叉乘法比大小。
Int64Fraction f1 = SimplifiedForm();
Int64Fraction f2 = another.SimplifiedForm();
int64_t num1{f1.Num()};
int64_t den1{f1.Den()};
int64_t num2{f2.Num()};
int64_t den2{f2.Den()};
return num1 * den2 > num2 * den1;
}
///
/// @brief 本对象小于 another.
/// @param another
/// @return
///
constexpr bool operator<(Int64Fraction const &another) const
{
// 先化简,避免分母为负数,然后使用交叉乘法比大小。
Int64Fraction f1 = SimplifiedForm();
Int64Fraction f2 = another.SimplifiedForm();
int64_t num1{f1.Num()};
int64_t den1{f1.Den()};
int64_t num2{f2.Num()};
int64_t den2{f2.Den()};
return num1 * den2 < num2 * den1;
}
///
/// @brief 本对象大于等于 another.
///
/// @param another
/// @return true
/// @return false
///
constexpr bool operator>=(Int64Fraction const &another) const
{
if (*this == another)
{
return true;
}
if (*this > another)
{
return true;
}
return false;
}
///
/// @brief 本对象小于等于 another.
///
/// @param another
/// @return true
/// @return false
///
constexpr bool operator<=(Int64Fraction const &another) const
{
if (*this == another)
{
return true;
}
if (*this < another)
{
return true;
}
return false;
}
/* #endregion */
};
当一个 int8_t 变量 a 被视为 8 位定点小数,用它来初始化这个分数时就是
cpp
base::Int64Fraction frac{a, 128};
即把这个 int8_t 变量 a 作为分数的分子,128 作为分母,得到的分数值就是这个定点小数要表示的值。
视为每一位的权重发生改变了

N 位定点小数
8 位定点小数可以看作是分子是 8 位有符号整型,分母是 28−1=27=1282^{8-1} =2^7=12828−1=27=128 的分数,则 N 位定点小数可以看作是分子是 N 位有符号整型,分母是 2N−12^{N-1}2N−1 的分数。
值域为:
−1,2N−1−12N−1\] \[-1, \\frac{2\^{N-1}-1}{2\^N-1}\] \[−1,2N−12N−1−1
分辨率为:
12N−1 \frac{1}{2^{N-1}} 2N−11
相关的软考例题
例12 的那些选项应该是打错了,实际上这些选项想要表示的应该是 2 的幂,只不过 2 右边的内容没有往 2 的右上角偏,而是错误地与 2 平齐了。
上文说定点小数实际上就是一个补码形式的有符号整型,只不过你要当作它有一个分母存在。N 位的整型共有 2N2^N2N 种不同的值,所以被解释成定点小数后也是有这么多个值,所以选 A.
例13 所谓的定点整数其实就是有符号整型。所以选 B.