C++ Primer Notes(2): 内置整数类型的长度和转换

这一章的标题是「变量 和 基本类型」。内容很多,本篇只记录 section 2.1 的 Primitive Built-in Types (内置类型)相关笔记:内置整数类型长度和转换。

类型(types)很重要,能决定两件事:

  • data 的含义
  • 操作(函数)的含义
    例如
i = i + j

在这里插入图片描述
C++ 之所以提供 class 机制,允许用户自定义类,就是希望用户能通过自定义类型, 来提供和内置类型(built-in types)一样的使用体验; 而更进一步,为什么要提供这样的使用体验,或者说,让这样的使用体验成为可能,就是为了提供更好的抽象。既然如此,务必先弄清楚 built-in types 支持哪些操作。

类型长度

对于内置的 Arithmetic Types(数值类型),包括整数、浮点数两类;编译器只能规定它们的最小字节数:
在这里插入图片描述
在这里插入图片描述

危险类型: long 类型

典型例子是 long, 在 Linux-x64 下的长度是8字节, 和在 Windows-x64 的长度是4字节。这个特点,引发了很多跨平台编译的代码,运行结果不同的bug。

我们应该吸取教训;当看到基础库头文件中定义:

xxx_def.h:

typedef long MLong;
typedef int MInt32;

这两种定义都是危险的,只不过从实际经验来看, long 类型的坑比较明显。 而维护者声称所有SDK都应该用这个 xxx_def.h 的内容,方便在切换平台时,重新修改上述 MXxx 类型的定义。 这明显不安全,是应该规避的。正确做法是使用 #include <stdint.h> 里的 int8_t, uint8_t 等固定长度类型。

危险类型: char 类型

char 类型可以是 signed, 也可以是 unsigned 类型,不同编译器结果不一样。
在这里插入图片描述
在这里插入图片描述

在C++11之前,unsigned char ([0, 255]) -> char -> unsigned char 的转换结果, C++ 标准并不保证和原始数据一样; GCC/Clang 编译器下结果和原始值一样,那仅仅是 implementation defined。

从 C++11 开始, 这个转换是得到了确保得到原始值。

https://en.cppreference.com/w/cpp/language/types

在这里插入图片描述
这样的语言级保证,确保了诸如 std::ifstream, std::ofstream 在读写二进制数据时, 使用到的 reinterpret_cast<char*> 是安全的:

#include <string>
#include <fstream>
#include <iostream>

int main()
{
    int data = 42;
    const std::string filename = "data.bin";

    std::ofstream fout(filename, std::ios::binary);
    fout.write(reinterpret_cast<char*>(&data), sizeof(data));
    fout.close();

    int data2;
    std::ifstream fin(filename, std::ios::binary);
    fin.read(reinterpret_cast<char*>(&data2), sizeof(data2));
    fin.close();

    std::cout << data2 << std::endl;

    return 0;
}

变量赋值时超过范围?

在这里插入图片描述
1)当我们赋值超过范围的值到 unsigend 类型变量上时, 结果是取模:

  • 被模的数量,是类型能表达的数量
  • 例如
unsigned char a = -1; // 255:  (-1 + 256) % 256 = 255

在这里插入图片描述

2)当赋值到一个signed 类型时,如果被赋予的值是超出范围的, 那么结果是 未定义的
在这里插入图片描述
例如
在这里插入图片描述

3)当计算表达式中同时出现了 signed 和 unsigned 类型, 则 signed 类型被转为 unsigned 类型。相当于说临时生成了一个 unsigned 类型的匿名变量,并且对这个变量的赋值,如果出现了值超出范围,则遵循前面提到的,取模运算, 原因是这个匿名变量是 unsigned 类型

尽管我们没有刻意的把一个负数赋值给一个unsigned 类型,但是做大小比较的表达式中同时出现了 size_t 和 int 类型,而 size_t 可以认为是 unsigned int 类型。 此时会转换 int 类型到 unsigned 类型,具体的转换,就像是我们把int类型赋值给到unsigned类型时,如果超出 unsigned 的表示范围, 则执行取模运算:
在这里插入图片描述

例如:

unsigned u = 10;
int i = -42;
std;:cout << u + i << std::endl; // 输出-32吗?不是!输出 -32+std::numeric_limits<unsigned>::max(), 也就是 -32 + 2^{32} = 4294967264

实例分析

这个例子来自实际工程代码, 在设备部署时表现为: “当车道线数量为0, 程序出现死循环”,其实并不是真的死循环, 但 for 循环执行了 2 64 − 1 2^{64}-1 2641 次 (18446744073709551615).

#include <stddef.h>
#include <iostream>

// 模拟 CNN 网络给出的算法结果
int get_num_lanes() {
    return 0;
}

int main()
{
    int num_lane = get_num_lanes();

    // 执行后处理, 本意是车道线数量为0时,不会执行循环, 实际则执行了循环
    for (size_t i = 0; i < num_lane - 1; i++)
    {
        std::cout << i << std::endl;
    }

    return 0;
}

在这里插入图片描述

  • 问题出在 i < num_lane - 1 这一表达式。 并且注意结合 i 和 num_lane 的类型做分析
  • i 是 size_t 类型, 是 unsigned 而不是 signed,在64位上时 unsigned int64_t 这么大
  • num_lane 是 int 类型, num_lane - 1 仍然是 int 类型
  • 看起来是 0 < -1 的比较, 其实是 0ULL < -1 的比较
    在这里插入图片描述
  • 因为表达式中出现了 unsigned 和 signed 类型, 按照前面提到的规则3, 会转换到 unsigned 类型做比较: 0 < (std::numeric_limits<size_t>::max() - 1), 也就是 0 2 64 − 1 2^{64}-1 2641 做比较

也就是说,当 int num_lane=0 , size_t i=0 时, 表达式 i < num_lane - 1 被这样依次展开:

(size_t)0 < (int)-1
(size_t)0 < (std::numeric_limits<size_t>::max() - 1)
0 < 18446744073709551615

也就是说,含有bug的代码是在做:

    for (size_t i = 0; i < 18446744073709551615; i++)
    {
        std::cout << i << std::endl;
    }

难怪表现为 “死循环” !

实例分析2

https://github.com/vallentin/LoadBMP/blob/master/loadbmp.h#L209-L240

unsigned y;
unsigned h = get_h();
for (y = (h - 1); y != -1; y--)
    ...

由于 y 是 unsigned 类型, 如果 for 循环的条件写成 y >= 0 则会导致死循环, y != -1 则是正确的写法。 原因是, y!=-1 是一个 arithmetic 表达式, 由于 y 是 unsigned 类型, -1 是 int 类型, -1 会被转换到 unsigned 类型, 而 -1 本身不在 unsigned 类型表示范围之内, 因此会执行取模运算,从而把 -1 转换到 std::numeric_limits<unsigned>::max()-1, 也就是 2 32 − 1 2^{32}-1 2321. 因此相当于如下的 for 循环:

unsigned y;
unsigned h = get_h();
for (y = (h - 1); y != 2147483647; y--)
    ...

而其中涉及的第二个 arithmetic 表达式,是 y--, 也就是 y=y-1 的意思, 一旦y等于0,则 y=y-1 并不会得到 “-1”, 而是得到 (-1 + mod) % mod 也就是 (-1 + 2 32 2^{32} 232)% 2 32 2^{32} 232, 得到 2 32 − 1 2^{32}-1 2321.

4)不要混合使用signed和unsigned类型
原因是: 在 arithmetic 表达式中,同时出现 signed 和 unsigned 类型时, signed 类型会自动转为 unsigned 类型, 这个转换如前面所述,如果值不在 unsigned 类型范围则会执行取模运算。
在这里插入图片描述
举例:

#include <iostream>

int main()
{
    int a = -1;
    unsigned b = 1;

    std::cout << a * b << std::endl; // 得到-1吗?不!得到 4294967295

    return 0;
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值