C++ 编程
就像一个人拥有一个与其他人区分开的姓名一样,变量为一个特定对象类型实例分配一个名称或标签,通过该名称或标签可以引用该实例。变量是编程中最重要的概念,它是代码如何操作数据的核心。根据其在代码中的用途,变量在硬件方面具有特定的局部性,并且根据代码的结构,它还具有特定的作用域,在该作用域中,编译器会将其识别为有效的。所有这些特性都是由程序员定义的。
我们需要一种方法来存储数据,这些数据可以通过编程在硬件上存储、访问和修改。大多数计算机系统使用二进制逻辑运行。计算机使用两个电压电平表示值,通常 0V 表示逻辑 0,+3.3 V 或 +5V 表示逻辑 1。这两个电压电平恰好代表两个不同的值,按照惯例,这些值分别是零和一。这两个值巧合地对应于二进制数系统中使用的两个数字。由于计算机使用的逻辑电平和二进制数系统中使用的两个数字之间存在对应关系,因此计算机采用二进制系统就不足为奇了。
- 二进制数系统
二进制数系统使用以 2 为基数,因此只需要0 和1 两个数字。
我们通常将二进制数写成一位序列(位是二进制数字的简称)。这也是一个正常约定,这些位序列为了使二进制数更易于阅读和理解,在特定的相关边界处添加空格,这些边界将从使用该数字的上下文中选择。就像我们在较大的十进制数字中使用逗号(英国和大多数前殖民地)或点来隔开每三个数字一样。例如,二进制值 44978 可以写成1010 1111 1011 0010。
这些是特定位序列的定义边界。
名称 | 大小(位) | 示例 |
---|---|---|
位 | 1 | 1 |
字节 | 4 | 0101 |
字节 | 8 | 0000 0101 |
字 | 16 | 0000 0000 0000 0101 |
双字 | 32 | 0000 0000 0000 0000 0000 0000 0000 0101 |
- 位
二进制计算机上最小的数据单位是一个位。由于单个位只能表示两个不同的值(通常是零或一),你可能认为可以用单个位表示的项目数量非常少。不正确!你可以用一个位表示无数个项目。
用一个位,你可以表示任何两个不同的项目。例如,零或一,真或假,开或关,男或女,对或错。但是,使用多个位,你将不再局限于表示二进制数据类型(即只有两个不同值的那些对象)。
更令人困惑的是,不同的位可以表示不同的东西。例如,一个位可能用于表示值零和一,而相邻的位可能用于表示颜色红色或黑色。你如何通过查看位来判断?答案当然是不行。但这说明了计算机数据结构的整个理念:数据就是你定义的。
如果你使用一个位来表示一个布尔值(真/假),那么该位(根据你的定义)就代表真或假。为了使该位具有任何真实意义,你必须保持一致。也就是说,如果你在程序中的某个地方使用一个位来表示真或假,你不应该在稍后使用该位中存储的真/假值来表示红色或黑色。
由于你将尝试建模的大多数项目都需要超过两个不同的值,因此单个位值并不是最流行的数据类型。但是,由于其他所有事物都由位组构成,因此位将在你的程序中发挥重要作用。当然,有一些数据类型需要两个不同的值,因此似乎位本身很重要。但是,你很快就会看到,单个位很难操作,因此我们通常使用其他数据类型来表示布尔值。
- 字节
字节是 4 位边界上的位集合。除了两个项目之外,它不会是一个特别有趣的数据结构:BCD(二进制编码十进制)数字和十六进制(16 进制)数字。表示一个 BCD 或十六进制数字需要四个位。
用一个字节,我们可以表示多达 16 个不同的值。在十六进制数字的情况下,值 0、1、2、3、4、5、6、7、8、9、A、B、C、D、E 和 F 用四个位表示。
BCD 使用十个不同的数字(0、1、2、3、4、5、6、7、8、9)并且需要四个位。实际上,任何 16 个不同的值都可以用一个字节表示,但十六进制和 BCD 数字是我们可以用一个字节表示的主要项目。
- 字节
字节是我们可以在计算机上访问或修改的最小单个数据块,毫无疑问,它是当今微处理器使用最重要的数据结构。PC 中的主内存和 I/O 地址都是字节地址。
在几乎所有类型的计算机上,一个字节都包含 8 位,尽管确实存在字节更大的计算机。字节是微处理器中最小的可寻址数据(数据项),这就是为什么处理器只能处理字节或字节组,而不能处理位的原因。要访问任何更小的东西,你需要读取包含数据的字节并屏蔽掉不需要的位。
由于计算机是字节可寻址的机器,事实证明,操作整个字节比操作单个位或字节更有效率。因此,大多数程序员使用整个字节来表示需要不超过 256 个项目的那些数据类型,即使少于 8 位就足够了。例如,我们通常将布尔值真和假分别表示为 00000001 和 00000000。
可能一个字节最重要的用途是保存一个字符代码。在键盘上输入的字符、显示在屏幕上的字符以及打印在打印机上的字符都具有数值。
一个字节(通常)包含 8 位。一个位只能取 0 或 1 的值。如果所有位都设置为 1,则二进制中的 11111111 等于十进制中的 255。
一个字节中的位从位零(b0)到七(b7)编号如下:b7 b6 b5 b4 b3 b2 b1 b0
位 0(b0)是低位或最低有效位(lsb),位 7 是高位或最高有效位(msb)。我们将通过它们的数字来引用所有其他位。
一个字节也正好包含两个字节。位 b0 到 b3 构成低位字节,位 b4 到 b7 构成高位字节。
由于一个字节包含 8 位,正好是两个字节,因此字节值需要两位十六进制数字。它可以表示 2^8 或 256 个不同的值。一般来说,我们将使用一个字节来表示
- 范围为 0 => 255 的无符号数值
- 范围为 -128 => +127 的有符号数
- ASCII 字符代码
- 其他需要不超过 256 个不同值的特殊数据类型。许多数据类型少于 256 个项目,因此 8 位通常就足够了。
在本计算机字节表示中,位号用于标记字节中的每个位。位从 7 到 0 编号,而不是从 0 到 7 甚至从 1 到 8,因为处理器总是从 0 开始计数。正如我们将看到的那样,对于计算机来说,使用 0 更加方便。位也按降序排列,因为与十进制数字(正常的 10 进制)一样,我们将更重要的数字放在左侧。
考虑十进制中的数字 254。这里的 2 比其他数字更重要,因为它表示百位,而不是 5 表示的十位或 4 表示的个位。二进制也是如此。更重要的数字放在左边。在二进制中,只有 2 个数字,而不是从 0 计数到 9,我们只从 0 计数到 1,但计数的原理与十进制计数完全相同。如果我们要计数超过 1,那么我们需要在左边添加一个更重要的数字。在十进制中,当我们计数超过 9 时,我们需要在下一个重要的数字上加 1。有时它看起来可能令人困惑或不同,仅仅因为人类习惯于使用 10 个数字进行计数。
在十进制中,每个数字代表 10 的幂的倍数。因此,在十进制数 254 中。
- 4 代表四个 1 的倍数 ( 因为 )。
- 由于我们在十进制(以 10 为底)中工作,因此 5 代表五个 10 的倍数 ()
- 最后,2 代表两个 100 的倍数 ()
所有这些都是基本知识。关键是要认识到,当我们在数字中从右到左移动时,数字的意义以 10 的倍数增加。当我们看到以下等式时,这应该是显而易见的
在二进制中,每个数字只能是两种可能性之一(0 或 1),因此当我们处理二进制时,我们使用的是以 2 为底而不是以 10 为底。因此,要将二进制数 1101 转换为十进制,我们可以使用以下以 10 为底的等式,这与上面的等式非常相似
要转换数字,我们只需将位值 () 相加,其中 1 出现在的位置。让我们再看看我们示例字节,并尝试找到它在十进制中的值。
首先,我们看到位 #5 是 1,所以我们有 作为我们的总计。接下来是位 #3,所以我们加 。这给了我们 40。然后下一个是位 #2,所以 40 + 4 是 44。最后是位 #0,得到 44 + 1 = 45。所以这个二进制数在十进制中是 45。
如您所见,不同的位组合不可能给出相同的十进制值。这里有一个简单的示例,展示了二进制(以 2 为底)计数和十进制(以 10 为底)计数之间的关系。
= , = , = , =
这些数字所在的进制显示在数字右边的下标中。
进位位
[edit | edit source]顺便说一下,如果你给 255 加 1 会怎样?除非我们添加更多位,否则任何组合都无法表示 256。下一个值(如果我们可以有另一个数字)将是 256。因此我们的字节将是这样的。
但是这个 位(位#8)并不存在。那么它去哪里了呢?准确地说,它实际上进入了进位位。进位位驻留在计算机的处理器中,有一个专用于进位运算(如这种情况)的内部位。因此,如果在字节中存储的 255 中加 1,结果将是 0,并且 CPU 中的进位位将被设置。当然,C++ 程序员永远不会直接使用此位。您需要学习汇编语言才能做到这一点。
字节序
[edit | edit source]在检查完单个字节之后,现在该看看如何表示大于 255 的数字了。这是通过将字节分组来实现的,我们可以表示比 255 大得多的数字。如果我们将 2 个字节组合在一起,我们就会使数字中的位数加倍。实际上,16 位允许表示高达 65535 的数字 (unsigned
),而 32 位允许表示超过 40 亿的数字。
以下是一些基本的基本类型
- char (1 字节(根据定义),最大
unsigned
值:至少 255)
- short int (至少 2 个字节,最大
unsigned
值:至少 65535)
- long int (至少 4 个字节,最大
unsigned
值:至少 4294967295)
- float (通常为 4 个字节,浮点数)
- double (通常为 8 个字节,浮点数)
关于字节的所有信息都适用于其他基本类型。区别仅仅在于使用的位数不同,而 msb 现在对于 short 而言是位#15,对于 long 而言是位#31(假设 32 位 long 类型)。
在 short(16 位)中,人们可能会认为在内存中,位 15 到 8 的字节将紧随位 7 到 0 的字节。换句话说,字节 #0 将是高字节,而字节 #1 将是低字节。对于其他一些系统来说,这是正确的。例如,摩托罗拉 68000 系列 CPU 确实使用这种字节顺序。但是,在 PC 上(使用 8088/286/386/486/奔腾),情况并非如此。顺序相反,因此低字节位于高字节之前。表示位 0 到 7 的字节始终位于 PC 上所有其他字节之前。这被称为小端字节序。其他顺序(如 M68000 上的顺序)称为大端字节序。在执行旨在跨系统移植的低级字节操作时,这一点非常重要。
对于大端计算机,基本思想是将高位放在左边或前面。对于小端计算机,基本思想是将低位放在低字节中。除了一个奇特之处之外,这两种方案没有内在的优势。使用小端 long int 作为更小的 int 类型在理论上是可行的,因为低字节始终位于相同的位置(第一个字节)。在大端中,低字节的位置始终不同,具体取决于类型的尺寸。例如(在大端中),低字节是 long int 中的第 个字节,而 short int 中的第 个字节。因此,必须进行适当的类型转换,低级技巧变得相当危险。
要从一种字节序转换为另一种字节序,需要反转字节的值,将最高字节的值放在最低字节中,将最低字节的值放在最高字节中,并交换所有中间字节的值,因此,如果有一个 4 字节小端整数 0x0A0B0C0D(0x 表示值为十六进制),那么将其转换为大端将把它更改为 0x0D0C0B0A。
位字节序,其中字节内部的位顺序发生变化,很少用于数据存储,而且实际上只有在串行通信链路中才真正重要,在这些链路中,硬件处理它。
有一些计算机不遵循严格的大端或小端位布局,但它们很少见。一个例子是 PDP-11 存储 32 位值的方式。
了解二进制补码
[edit | edit source]补码是一种在纯二进制表示中存储负数的方法。选择补码方法来存储负数的原因是,它允许 CPU 对有符号和无符号
数字使用相同的加减指令。
要将一个正数转换为其负补码格式,您首先要翻转该数中的所有位(1 变成 0,0 变成 1),然后加 1。(这也能将负数转换回正数,例如:-34 转换为 34,反之亦然)。
让我们尝试将数字 45 转换为补码。
首先,我们将所有位翻转...
然后加 1。
现在,如果我们将所有 1 位的值加起来,我们得到... 128+64+16+2+1=211?这里发生了什么?嗯,这个数字实际上是 211。这完全取决于你如何解释它。如果你认为这个数字是无符号
,那么它的值是 211。但如果你认为它是带符号的,那么它的值是 -45。你完全可以自己决定如何处理这个数字。
当且仅当你决定将它视为一个带符号的数字时,请查看 msb(最高有效位 [位#7])。如果它是 1,那么它是一个负数。 如果它是 0,那么它是一个正数。在 C++ 中,在类型前面使用无符号
将告诉编译器你希望将此变量用作无符号
数字,否则它将被视为带符号数字。
现在,如果你看到 msb 被设置了,那么你就会知道它是负数。所以使用上面描述的步骤将其转换回正数以找出它的实际值。
让我们看几个例子。
无符号
字节。它的十进制值是多少?由于这是一个无符号
数字,不需要特殊处理。只需将所有 1 位的值加起来。128+64+32+4=228。所以这个二进制数字在十进制中是 228。
由于现在是一个带符号的数字,我们首先要检查 msb 是否被设置。让我们看看。是的,位#7 被设置了。所以我们必须进行补码转换才能将其值作为正数获取(然后我们在后面添加负号)。
好的,所以让我们翻转所有位...
然后加 1。这有点棘手,因为进位会传播到第三位。对于位#0,我们执行 1+1 = 10(二进制)。所以位#0 有一个 0。现在我们必须将进位加到第二位(位#1)。1+1=10。位#1 是 0,我们再次将 1 进位到 位(位#2)。0+1 = 1,我们完成了转换。
现在,我们将所有 1 位的值加起来。16+8+4 = 28。由于我们进行了转换,因此我们将负号加起来得到 -28 的值。所以,如果我们将 11100100(二进制)视为一个带符号的数字,它的值是 -28。如果我们将其视为一个无符号
数字,它的值是 228。
让我们尝试最后一个例子。
无符号
数字。首先作为无符号
数字。所以我们将所有 1 位的值加起来。4+1 = 5。对于无符号
数字,它的值是 5。
现在对于带符号的数字。我们检查 msb 是否被设置。不,位#7 是 0。所以对于带符号的数字,它的值也是 5。
如你所见,如果带符号的数字的 msb 没有被设置,那么你将它完全视为无符号
数字。
浮点数表示
[edit | edit source]具有小数部分的通用实数也可以用二进制格式表示。例如,二进制中的 110.01 对应于
指数表示法(也称为科学记数法或标准形式,当与基数 10 一起使用时,如) 也可以使用,同一个数字可以表示为
当小数点左侧只有一个非零数字时,该表示法被称为规范化。
在计算应用中,实数由符号位 (S)、指数 (e) 和尾数 (M) 表示。指数域需要表示正负指数。为此,将偏置 E 添加到实际指数以获得存储的指数,并将符号位 (S)(指示数字是否为负)转换为 +1 或 -1,得到 s。因此,实数表示为
S、e 和 M 在一个 32 位字中一个接一个地连接起来以创建一个单精度浮点数,在 64 位双字中连接起来以创建一个双精度浮点数。对于单精度浮点型,使用 8 位表示指数,使用 23 位表示尾数,指数偏移量为 E=127。对于双精度浮点型,使用 11 位表示指数,使用 52 位表示尾数,指数偏移量为 E=1023。
浮点数有两种类型:规格化和非规格化。规格化数的指数 e 在 0<e<28 - 1(在 00000000 和 11111111 之间,不包括)范围内,对于单精度浮点数,指数 e 在 0<e<211 - 1(在 00000000000 和 11111111111 之间,不包括)范围内,对于双精度浮点数。规格化数表示为符号乘以 1.尾数乘以 2e-E。非规格化数是指指数为 0 的数字。它们表示为符号乘以 0.尾数乘以 21-E。非规格化数用于存储值为 0 的值,其中指数和尾数均为 0。浮点数可以存储 +0 和 -0,具体取决于符号。当数字不是规格化或非规格化时(其指数全为 1),如果尾数为零,则数字将为正无穷或负无穷,具体取决于符号;如果尾数不为零,则数字将为正 NaN(非数字)或负 NaN(非数字),具体取决于符号。
例如,数字 5.0(使用 float 类型)的二进制表示为
0 10000001 01000000000000000000000
第一个位是 0,表示数字为正,指数为 129-127=2,尾数为 1.01(注意,前导 1 不包括在二进制表示中)。1.01 在十进制表示中对应于 1.25。因此 1.25*4=5。
浮点数并不总是值的精确表示。像 1010110110001110101001101 这样的数字无法用单精度浮点数表示,因为,不考虑前导 1(不是尾数的一部分),有 24 位,而单精度浮点数只能在其尾数中存储 23 个数字,所以最后的 1 必须丢弃,因为它是最不重要的位。此外,有些值在十进制中可以很容易地表示,但在二进制中却无法表示,例如,十进制中的 0.3 将是 0.0010011001100110011... 或者类似的东西。许多其他数字无法用二进制浮点数精确表示,无论其尾数使用多少位,仅仅因为它会产生像这样的重复模式。
局部性(硬件)
[edit | edit source]变量具有两个不同的特征:在堆栈上创建的变量(局部变量)和通过硬编码内存地址访问的变量(全局变量)。
全局变量
[edit | edit source]通常,一个变量绑定到 计算机内存 中的特定地址,该地址在运行时自动分配,其固定字节数由变量的对象类型的尺寸和对变量执行的任何操作决定,并影响存储在该特定内存位置中的一个或多个 值。
所有全局定义的变量都将具有静态生命周期。只有那些未定义为 const
的变量默认情况下允许外部链接。
局部变量
[edit | edit source]如果变量的大小和位置事先未知,则该变量在内存中的位置存储在另一个变量中,而原始变量的大小由存储第一个变量内存位置的第二个值的类型的尺寸决定。这被称为 引用,而保存其他变量内存位置的变量称为指针。
范围
[edit | edit source]变量也驻留在特定的 范围 中。变量的范围是决定变量生命周期的最重要的因素。进入范围开始变量的生命周期,离开范围结束变量的生命周期。当在范围内时,变量是可见的,除非它被封闭范围内具有相同名称的变量隐藏。变量可以位于全局范围、命名空间
范围、文件范围或复合语句范围。
例如,在以下代码片段中,变量 'i' 仅在相应的注释之间的行中处于范围内
{
int i; /*'i' is now in scope */
i = 5;
i = i + 1;
cout << i;
}/* 'i' is now no longer in scope */
有一些特定的关键字可以延长变量的生命周期,而复合语句定义自己的局部 范围。
// Example of a compound statement defining a local scope
{
{
int i = 10; //inside a statement block
}
i = 2; //error, variable does not exist outside of the above compound statement
}
在同一级别的范围内声明同一个变量两次是错误的。
全局变量唯一可以定义的 范围 是 命名空间
,它处理变量的可见性,而不是其有效性,其主要目的是避免名称冲突。
在处理类时,与变量相关的范围的概念变得极其重要,因为构造函数在进入范围时被调用,而析构函数在离开范围时被调用。