diff --git "a/src/site/notes/\347\274\226\347\250\213\350\257\255\350\250\200/c++/\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213\347\232\204\350\241\250\347\216\260\345\275\242\345\274\217.md" "b/src/site/notes/\347\274\226\347\250\213\350\257\255\350\250\200/c++/\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213\347\232\204\350\241\250\347\216\260\345\275\242\345\274\217.md" deleted file mode 100644 index cabcb20..0000000 --- "a/src/site/notes/\347\274\226\347\250\213\350\257\255\350\250\200/c++/\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213\347\232\204\350\241\250\347\216\260\345\275\242\345\274\217.md" +++ /dev/null @@ -1,349 +0,0 @@ ---- -{"dg-publish":true,"permalink":"/编程语言/c++/基本数据类型的表现形式/","dgPassFrontmatter":true} ---- - -# 整数类型 -C++提供的整数数据类型有三种:int、long、short。一般来说int类型与long类型在内存中都占32位整数,short类型在内存中占16位整数。 - -可以将整数数据类型理解为存放整数的“杯子”,其中每种整数数据类型表示一个具有特定容量和规格(即位宽和有符号/无符号)的杯子。 -**容量**:不同数据类型的“杯子”具有不同的容量。例如,8位整数容纳的数字范围较小,而64位整数可以容纳的数字范围很大。 -**有符号 / 无符号**:有符号整数的“杯子”可以容纳负数和正数,而无符号整数的“杯子”只能容纳非负数。这与现实生活中可以装冷热饮的保温杯和只能装冷饮的塑料杯类似。 -**溢出**:在整数数据类型中,存储的数值若超过了一个类型的最大值,就会出现溢出现象。类似地,向一个小杯子中倒入过多水会导致水溢出。 -## 无符号整数 -无符号整数的所有位都用来表示数值。unsigned int为例,此类型的变量在内存中占32位数,则表示范围为0~4294967295。 -当无符号整型不足32位时,用0来填充剩余高位,直到占满32位。比如数字5,二进制为00000000 00000000 00000000 00000101,转换成十六进制数0x00000005之后,在内存中以“小尾方式”存放,以字节为单位,按照数据类型长度,高数据位对高地址,低数据位对低地址 -``` -内存地址: 0 1 2 3 -存储内容: 0x05 0x00 0x00 0x00 -``` - -"以字节为单位"的说法是指在内存中以字节(byte,1字节=8位,2个16进制数)为最小存储单元来表示和操作数据,一个内存地址可以存储8位 -## 有符号整数 -有符号整数中用来表示符号的是最高位。最高位为0表示正数,最高位为1表示负数,有符号整数的取值范围要比无符号整数取值范围少1位,即0x80000000~0x7FFFFFFF,如果转换为十进制数,则表示范围为-2 147 483 648~2 147 483 647。 -比如数字-5 -``` -原码(正数5的二进制形式): 0000 0000 0000 0000 0000 0000 0000 0101 -反码(将原码取反): 1111 1111 1111 1111 1111 1111 1111 1010 -补码(反码加1): 1111 1111 1111 1111 1111 1111 1111 1011 -``` - -``` -内存地址: 0 1 2 3 -存储内容: 0xFB 0xFF 0xFF 0xFF -``` - -``` -二进制 十六进制 -0000 <=> 0 -0001 <=> 1 -0010 <=> 2 -0011 <=> 3 -0100 <=> 4 -0101 <=> 5 -0110 <=> 6 -0111 <=> 7 -1000 <=> 8 -1001 <=> 9 -1010 <=> A -1011 <=> B -1100 <=> C -1101 <=> D -1110 <=> E -1111 <=> F -``` -# 浮点数类型 -整型类型是将十进制转换成二进制保存在内存中。浮点类型并不是将一个浮点小数直接转换成二进制数保存,而是将浮点小数转换成的二进制码重新编码,再进行存储 -## 浮点数的编码方式 -浮点数编码会将一个浮点数转换为二进制数。以科学记数法划分,将浮点数拆分为3部分:符号、指数、尾数。 -### float类型的编码方式 -以12.25为例 -要将十进制小数转换为二进制数,您可以使用以下方法: - -1. 将小数部分乘以2; -2. 记下整数部分的结果; -3. 如果产生的小数部分为0,则可停止转换;如果仍存在小数部分,重复步骤1和2,直到达到所需的精度或小数部分为0。 - -以下是将十进制小数0.25转换为二进制数的过程: - -``` -0.25 × 2 = 0.5 (整数部分:0) -0.50 × 2 = 1.0 (整数部分:1) -``` - -在这个示例中,我们已经达到了精确值,因此可以停止转换。将整数部分的结果按顺序排列:`01`。 - -所以,十进制小数0.25对应的二进制数为:`0.01`。 - -1. **确定符号位(sign)**:数值为正,所以符号位为0。 - -2. **计算指数(exponent)**: a. 转换十进制数12.25为二进制:`1100.01`。 b. 规范化二进制表示式:`1.10001(×2^3)`(小数点左移3位,指数+3,即移动到除符号位的最高位为1处)。 c. 将指数(+3)添加一个偏移量(对于32位浮点数,偏移量为127):`3 + 127 = 130`。 d. 将指数130转换为8位二进制表示:`1000 0010`。 - -3. **计算尾数(mantissa)**: a. 从规范化的二进制表示式中取出尾数部分(去掉隐含的1和小数点):`10001`。 b. 在尾数的右侧添加0,以使之达到23位:`100 0100 0000 0000 0000 0000`。 - -4. **将符号位、指数和尾数组合成一个单精度IEEE 754编码**: - - - 符号位(1位):`0` - - 指数(8位):`1000 0010` - - 尾数(23位):`100 0100 0000 0000 0000 0000` - - 组合所有部分:`0100 0001 0100 0100 0000 0000 0000 0000` - --12.25 -1. **确定符号位(sign)**:数值为负,所以符号位为1。 - -2. **计算指数(exponent)**: a. 转换十进制数 12.25(暂时忽略负号)为二进制:`1100.01`。 b. 规范化二进制表示式:`1.10001(×2^3)`(小数点左移 3 位,指数 +3)。 c. 将指数(+3)添加一个偏移量(对于32位浮点数,偏移量为 127):`3 + 127 = 130`。 d. 将指数 130 转换为 8 位二进制表示:`1000 0010`。 - -3. **计算尾数(mantissa)**: a. 从规范化的二进制表示式中取出尾数部分(去掉隐含的1和小数点):`10001`。 b. 在尾数的右侧添加 0,使之达到 23 位:`100 0100 0000 0000 0000 0000`。 - -4. **将符号位、指数和尾数组合成一个单精度 IEEE 754 编码**: - - - 符号位(1位):`1` - - 指数(8位):`1000 0010` - - 尾数(23位):`100 0100 0000 0000 0000 0000` - - 组合所有部分:`1100 0001 0010 0100 0000 0000 0000 0000` -0.25 -1. **确定符号位(sign)**:数值为正,所以符号位为 0。 - -2. **计算指数(exponent)**: a. 转换十进制数 0.25 为二进制:`0.01`。 b. 规范化二进制表示式:`1.0(×2^(-2))`(小数点右移 2 位,指数 -2)。 c. 将指数(-2)添加一个偏移量(对于 32 位浮点数,偏移量为 127):`-2 + 127 = 125`。 d. 将指数 125 转换为 8 位二进制表示:`0111 1101`。 - -3. **计算尾数(mantissa)**: a. 从规范化的二进制表示式中取出尾数部分(去掉隐含的 1 和小数点):`0`。 b. 在尾数的右侧添加 0,使之达到 23 位:`000 0000 0000 0000 0000 0000`。 - -4. **将符号位、指数和尾数组合成一个单精度 IEEE 754 编码**: - - - 符号位(1 位):`0` - - 指数(8 位):`0111 1101` - - 尾数(23 位):`000 0000 0000 0000 0000 0000` - - 组合所有部分:`0011 1110 1000 0000 0000 0000 0000 0000` -1.3 -1. 将 0.3 乘以 2 得到 0.6。整数部分为 0,小数部分为 0.6。 -2. 将第1步中的小数部分(0.6)继续乘以 2 得到 1.2。整数部分为 1,小数部分为 0.2。 -3. 将第2步中的小数部分(0.2)继续乘以 2 得到 0.4。整数部分为 0,小数部分为 0.4。 -4. 将第3步中的小数部分(0.4)继续乘以 2 得到 0.8。整数部分为 0,小数部分为 0.8。 -5. 将第4步中的小数部分(0.8)继续乘以 2 得到 1.6。整数部分为 1,小数部分为 0.6。 -6. 此时,我们注意到小数部分又回到了 0.6,所以会继续重复之前的步骤,因此产生循环小数。 - -带入计算的二进制表示如下(约23位): - -``` -0.3 (十进制) = 0.01001100110011001100110 (二进制) -``` -1. **确定符号位(sign)**:数值为正,所以符号位为 0。 - -2. **计算指数(exponent)**: a. 将十进制数 1.3 转换为二进制表示。由于 1.3 不能表示为一个精确的二进制小数,我们需要对其进行近似处理。对于 1.3,我们可以得到一个近似的二进制表示:`1.01001100110011001100110` (约 23 位尾数长度)。 b. 规范化二进制表示式:`1.01001100110011001100110(×2^0)`(无需位移,指数为 0)。 c. 将指数(0)添加一个偏移量(对于32位浮点数,偏移量为 127):`0 + 127 = 127`。 d. 将指数 127 转换为 8 位二进制表示:`0111 1111`。 - -3. **计算尾数(mantissa)**: a. 从规范化的二进制表示式中取出尾数部分(去掉隐含的 1 和小数点):`01001100110011001100110`。 b. 在尾数的右侧添加 0,使之达到 23 位:`010 0110 0110 0110 0110 0110`(四舍五入至23位)。 - -4. **将符号位、指数和尾数组合成一个单精度 IEEE 754 编码**: - - - 符号位(1位):`0` - - 指数(8位):`0111 1111` - - 尾数(23位):`010 0110 0110 0110 0110 0110` - - 组合所有部分:`0011 1111 0100 1100 1100 1100 1100 1100` -# 字符和字符串 -## 字符 -| 汉字 | 你 | 好 | 世 | 界 | 天 | 气 | -| --------- | ---- | ---- | ---- | ---- | ---- | ---- | -| GB2312 编码 | C4E3 | BAE3 | CACB | BDDF | CCE2 | C6F7 | -## 字符串 -字符串是由一系列线性排列的字符组成的。在程序中,只要知道字符串的首地址和结束地址就可以确定字符串的长度和大小。字符串的首地址很容易确定,因为在定义字符串的时候都会先指定好首地址。但是结束地址如何确定呢?有两种方法,一种是在首地址的4字节中保存字符串的总长度;另一种是在字符串的结尾处使用一个规定好的特殊字符,即结束符,Unicode编码使用两个字节'\\0'。 -在程序中,一般都会使用一个变量来存放字符串中第一个字符的地址,以便于查找使用字符串。在程序中使用字符型指针char*、wchar_t* 来保存字符串首地址。 -# 布尔类型 -布尔类型用于判断执行结果,它的判断比较值只有两种情况:0与非0。C++中定义0为假,非0为真。 -# 地址、指针和引用 -在C++中,地址标号使用十六进制表示,取一个变量的地址使用“&”符号,只有变量才存在内存地址 -指针的定义使用“TYPE*”格式,TYPE为数据类型,任何数据类型都可以定义指针。指针用于保存各种数据类型在内存中的地址。 -引用的定义格式为“TYPE&”,TYPE为数据类型,引用表示一个变量的别名,对它的任何操作本质上都是在操作它所表示的变量。 -## 指针和地址的区别 -地址是一个由32位二进制数字组成的值,可以将计算机内存中的地址理解为“门牌号”。指针是一种变量类型,它用于存储内存地址(即“门牌号”)。指针变量的主要特点是它们存储的是内存地址而不是实际数据。这意味着,通过指针变量,我们可以间接地访问和操作内存中的数据。 -指针透露了内存地址(门牌号)后面保存了什么类型的数据。 -```c -int *intptr; -float *floatptr; - -// 假设 intptr 和 floatptr 指向同一内存地址 -``` -在这个代码片段中,我们声明了两个指针变量,分别是整型指针变量(`int *intptr`)和浮点型指针变量(`float *floatptr`)。 - -1. `int *intptr;` 这一行代码声明了一个整型指针变量 `intptr`,即这个指针变量指向整型数据(`int`类型)在内存中的位置。当我们在程序中使用 `intptr` 时,我们意味着要通过这个指针变量访问或操作一个整型变量。 - -2. `float *floatptr;` 这一行代码声明了一个浮点型指针变量 `floatptr`,即这个指针变量指向浮点型数据(`float` 类型)在内存中的位置。当我们在程序中使用 `floatptr` 时,我们意味着要通过这个指针变量访问或操作一个浮点型变量。 -这两个指针指向的实际内存地址是相同的,但指针类型不同。程序会根据指针类型来正确解释位于此内存地址的数据。使用 `intptr` 访问内存地址会将数据作为整数解释,而使用 `floatptr` 访问内存地址则会将数据作为浮点数解释。 -这就是为什么我们需要指针类型,因为它告诉编译器如何解释存储在内存地址中的数据。对于没有明确类型的地址值,我们无法单独解释它,因为我们不知道如何正确地处理这段内存上的数据。 -## 指针的工作方式 -```c -int n = 0x12345678; -int *p1 = &n; -``` -1.1. `int n = 0x12345678;` - - `00DF1722 mov dword ptr [n],12345678h` - - - `mov` 是一个 x86 汇编指令,用于在内存、寄存器之间移动数据。 - - `dword ptr [n]` 表示访问变量 `n` 的内存位置。这里的 `dword ptr` 指定数据大小为 4 字节(即 32 位)。 `ptr` 关键字表示我们正在操作内存地址,方括号(`[...]`)通常用来表示内存地址 - - `12345678h` 是将要存储在 `n` 的内存地址中的十六进制数值,它等于十进制整数 305419896。 - - 所以这条指令的意思是将整型数值 0x12345678 存储到变量 `n` 的内存位置。 - -2. `int *p1 = &n;` - - ``` - 00DF1729 lea eax, [n] - 00DF172C mov dword ptr [p1], eax - ``` - - - `00DF1729 lea eax, [n]` - - - `lea`(Load Effective Address)是一个 x86 汇编指令,用于计算有效地址并将其结果存储在目标寄存器中。在这里,重要的部分是该指令不实际访问内存,而仅加载地址。 - - `eax` 是由指令操作的寄存器,它是 x86 架构中常用的 32 位通用寄存器。 - - `[n]` 是要加载的内存地址,即变量 `n` 的内存地址。 - - 这条指令的作用是将整型变量 `n` 的内存地址加载到寄存器 `eax` 中。 - - - `00DF172C mov dword ptr [p1], eax` - - - `mov` 指令在这里又出现了。这次,它用于将寄存器 `eax` 中的值(即变量 `n` 的内存地址)复制到内存地址 `[p1]`,即整型指针 `p1` 的内存位置。 - - 这条指令将寄存器 `eax` 中的内存地址(即整型变量 `n` 的内存地址)存储在整型指针变量 `p1` 中。 -## 指针的加法 -指针的加法指的是指针与一个整数相加。这实际上是移动(偏移)指针指向的内存地址。指针与整数相加时,所移动的字节数取决于指针类型。具体地说,指针加一个整数值将使指针移动到与当前指向地址相对应的整数倍的数据类型大小的位置。 -如果指针类型为 `type`,则计算目标地址的方法如下: - -``` -目标地址 = 首地址 + type类型的大小 * n -``` - -```c -int x; -sizeof(int) // 表示 int 类型的大小,通常为 4 字节(32 位系统) - -char c; -sizeof(char) // 表示 char 类型的大小,通常为 1 字节 - -float f; -sizeof(float) // 表示 float 类型的大小,通常为 4 字节(32 位系统) - -double d; -sizeof(double) // 表示 double 类型的大小,通常为 8 字节(32 位系统) -``` - -```c -int arr[] = { 1, 2, 3, 4 }; -int *p = arr; // p 指向 arr 的首元素,即 arr[0] -p = p + 2; -``` -1. 首先,初始化整型数组 `arr`: - -```assembly -mov dword ptr [ebp-18h], 1 ; 初始化 arr[0],将值 1 存储在地址 ebp-18h -mov dword ptr [ebp-14h], 2 ; 初始化 arr[1],将值 2 存储在地址 ebp-14h -mov dword ptr [ebp-10h], 3 ; 初始化 arr[2],将值 3 存储在地址 ebp-10h -mov dword ptr [ebp-0Ch], 4 ; 初始化 arr[3],将值 4 存储在地址 ebp-0Ch -``` - -这里,在栈中分配了足够的空间(4 个 `int` 类型)来存储整型数组 `arr`。数组元素依次存储在栈中,地址从 `ebp-18h` 至 `ebp-0Ch`。 - -2. 将指针 `p` 指向数组 `arr` 的首元素 `arr[0]`: - -```assembly -lea eax, [ebp-18h] ; 将地址 ebp-18h 加载到寄存器 eax,此地址保存 arr[0] 的值 -mov dword ptr [ebp-24h], eax ; 将地址 ebp-18h(保存在 eax 中)存储到变量 p(位于 ebp-24h) -``` - -这里,通过使用 `lea` 指令将数组 `arr` 的起始地址(即 `ebp-18h`)加载到寄存器 `eax` 中。随后,将该地址存储到内存地址 `ebp-24h`,即 `int` 型指针 `p` 的位置。 - -3. 计算 `p = p + 2`,将指针 `p` 向后移动2个 `int` 类型位置(通常在32位系统中为8字节): - -```assembly -mov eax, dword ptr [ebp-24h] ; 从指针 p 处的内存地址(即 ebp-24h)加载值到寄存器 eax -add eax, 8 ; 递增 eax 寄存器的值,添加2个int(通常在32位系统中为8字节偏移) -mov dword ptr [ebp-24h], eax ; 更新寄存器 eax 的值回到内存地址 ebp-24h,更新指针 p 的值 -``` - -首先,将指针 `p` 的值从内存地址 `ebp-24h` 加载到寄存器 `eax`。然后,用 `add` 指令将寄存器 `eax` 的值递增8,这使得指针 `p`(保存在寄存器 `eax` 中)向后移动两个 `int` 类型的位置。最后,将更新后的指针 `p` 值(正在寄存器 `eax` 中)保存回内存地址 `ebp-24h`。 - -现在,在这组汇编指令执行完毕后,指针 `p` 指向了数组 `arr` 的第二个元素,即 `arr[2]`。 -# 常量 -在C++中,可以使用宏机制#define来定义常量,也可以使用const将变量定义为一个常量。#define定义常量名称,编译器在对其进行编译时,会将代码中的宏名称替换成对应信息。宏的使用可以增 -加代码的可读性。const是为了增加程序的健壮性而存在的。 -## define和const的区别 -define修饰的符号名称是一个真量数值,而const修饰的栈常量,是一个“假”常量。在实际中,使用const定义的栈变量,最终还是一个变量,只是在编译期间对语法进行了检查,发现代码有对const修饰的变量存在直接修改行为则报错。被const修饰过的栈变量本质上是可以被修改的。我们可以利用指针获取const修饰过的栈变量地址,强制将const属性修饰去掉,就可以修改对应的数据内容 - -# 常见汇编指令 - -1. 数据移动: - - - `mov dest, src`: 将数据从源操作数(`src`)复制到目标操作数(`dest`)。 - - - `lea dest, [src]`: 将有效地址(通常表示为基址和偏移量组合)加载到目标寄存器(`dest`)中。 - -2. 算术操作: - - - `add dest, src`: 把源 (`src`) 和目标 (`dest`) 的值相加,并将结果存储在目标操作数中。 - - - `sub dest, src`: 从目标操作数 (`dest`) 中减去源操作数 (`src`) 的值,并将结果存储在目标操作数中。 - - - `mul src`: 执行无符号整数乘法。将源操作数 (`src`) 与 `eax` 寄存器相乘,结果存储在 `edx:eax` 中。 - - - `imul src`: 执行带符号整数乘法。将源操作数 (`src`) 与 `eax` 寄存器相乘,结果存储在 `edx:eax` 中。 - - - `div src`: 用源操作数 (`src`) 将 `edx:eax` 中的值除以源操作数,将商和余数存储在 `eax` 和 `edx` 中。 - - - `idiv src`: 执行带符号整数除法。将 `edx:eax` 中的值除以源操作数 (`src`),将商和余数存储在 `eax` 和 `edx` 中。 - -3. 逻辑操作: - - - `and dest, src`: 执行按位逻辑与操作。将目标操作数 (`dest`) 与源操作数 (`src`) 进行位与运算,并将结果存储在目标操作数中。位与运算:当且仅当两个输入位都为 1 时,结果位才为 1;否则为 0 - - - `or dest, src`: 执行按位逻辑或操作。将目标操作数 (`dest`) 与源操作数 (`src`) 进行位或运算,并将结果存储在目标操作数中。位或运算:两个输入位任意一个为 1 时,结果位才为 1;否则为 0 - - - `xor dest, src`: 执行按位逻辑异或操作。将目标操作数 (`dest`) 与源操作数 (`src`) 进行位异或运算,并将结果存储在目标操作数中。只有当输入位中仅有一个 1 时,结果位才为 1。 - - - - `not dest`: 对目标操作数 (`dest`) 进行按位取反。 - -4. 比较和测试: - - - `cmp a, b`: 比较两个操作数 (a 和 b)。实际上是先执行一个减法操作(但不保存结果),然后根据结果设置标志位(如 Zero 标志位,Sign 标志位和 Overflow 标志位等)。 - - - `test a, b`: 执行按位逻辑与操作 (AND),并根据结果设置特定标志位(如 Zero 标志位),但不修改操作数。 - -5. 分支和跳转: - - - `jmp label`: 无条件地跳转到标签定义的代码位置。分支可以是有向的。 - - - `je, jne, jg, jge, jl, jle, ja, jae, jb, jbe` 等:根据条件(等于、不等于、大于、大于等于、小于、小于等于、大于无符号、大于等于无符号、小于无符号、小于等于无符号)跳转到对应的标签位置。 - -6. 循环: - - - `loop label`: 将 `ecx` 寄存器递减 1,然后根据 `ecx` 是否为零跳转到指定标签。 - -7. 函数调用: - - - `call func`: 调用函数。程序跳转到函数的起始处,并将返回地址(即下一条指令的地址)入栈。 - - - `ret`: 返回函数调用点。从栈中弹出返回地址并跳转到该地址。 -寄存器看作是特殊类型的内存地址,都可以看作是存储数据的位置。 -1. 存储位置:寄存器是 CPU 内部的小型存储空间,位于处理器芯片上。相比之下,内存地址指向计算机主内存(RAM)中的位置。 - -2. 访问速度:寄存器的访问速度非常快,因为它们位于 CPU 内部。内存访问速度相对较慢,因为它需要通过内存总线来进行读取或写入操作。 - -3. 存储容量:寄存器非常有限且特殊化,每个处理器架构都有特定数量的寄存器。另一方面,内存地址空间通常更大,可以存储大量数据。 - -4. 用途:寄存器通常用于保存计算过程中的中间值、指令指针、栈指针等特定信息。内存地址则用于存储变量、程序代码、栈和堆中的数据等。 -mov dest, src 指令,目标操作数(dest)可以是寄存器或内存地址 -标志寄存器(FLAGS或EFLAGS)是一个16位或32位寄存器,用于表示有关CPU状态的信息。以下是Zero 标志位(ZF)、Sign 标志位(SF)和Overflow 标志位(OF)在FLAGS或EFLAGS寄存器中的位置: - -1. Zero 标志位(ZF)位于第6位(从0开始计数)。 - - 寄存器:`|15 - 7|-ZF-|5 - 0|` - -2. Sign 标志位(SF)位于第7位(从0开始计数)。 - - 寄存器:`|15 - 8|-SF-|6 - 0|` - -3. Overflow 标志位(OF)位于第11位(从0开始计数)。 - - 寄存器:`|15 - 12|-OF-|10 - 0|` \ No newline at end of file