c/docs/notes/01_c-basic/05_xdx/index.md

80 KiB
Raw Blame History

第一章:数据类型(

1.1 概述

  • 根据变量存储不同,我们可以将变量分为两类:

    • 普通变量:变量所对应的内存中存储的是普通值
    • 指针变量:变量所对应的内存中存储的是另一个变量的地址
  • 如下图所示:

Note

普通变量和指针变量的相同点:

  • 普通变量有内存空间,指针变量也有内存空间。
  • 普通变量有内存地址,指针变量也有内存地址。
  • 普通变量所对应的内存空间中有值,指针变量所对应的内存空间中也有值。

普通变量和指针变量的不同点:

  • 普通变量所对应的内存空间存储的是普通的值,如:整数、小数、字符等;指针变量所对应的内存空间存储的是另外一个变量的地址。
  • 普通变量有普通变量的运算方式,而指针变量有指针变量的运算方式(后续讲解)。
  • 那么,在 C 语言中变量的数据类型就可以这么划分,如下所示:

Note

  • 根据普通变量存储的类型不同,可以将普通变量类型划分为基本数据类型(整型、字符类型、浮点类型、布尔类型)和复合数据类型(数组类型、结构体类型、共用体类型、枚举类型)。
  • 根据指针变量指向空间存储的类型不同,可以将指针类型分为基本数据类型指针复合数据类型指针函数指针数组指针等,例如:如果指针所指向的空间保存的是 int 类型,那么该指针就是 int 类型的指针。

1.2 整数类型

1.2.1 概述

  • 整数类型简称整型用于存储整数值12、20、50 等。
  • 根据所占内存空间大小的不同,可以将整数类型划分为:
  • ① 短整型:
类型 存储空间(内存空间) 取值范围
unsigned short (无符号短整型) 2 字节 0 ~ 65,535 (2^16 - 1)
[signed] short有符号短整型默认 2 字节 -32,768 (- 2^15) ~ 32,767 (2^15 -1)
  • ② 整型:
类型 存储空间(内存空间) 取值范围
unsigned int无符号整型 4 字节(通常) 0 ~ 4294967295 (0 ~2^32 -1)
[signed] int有符号整型默认 4 字节(通常) -2147483648- 2^31 ~ 2147483647 (2^31-1)
  • ③ 长整型:
类型 存储空间(内存空间) 取值范围
unsigned long无符号长整型 4 字节(通常) 0 ~2^32 -1
[signed] long有符号长整型默认 4 字节(通常) - 2^31 ~ 2^31-1
  • ④ 长长整型:
类型 存储空间(内存空间) 取值范围
unsigned long long无符号长整型 8 字节(通常) 0 ~2^64 -1
[signed] long long有符号长整型默认 8 字节(通常) - 2^63 ~ 2^63-1

Note

  • ① 数据类型在内存中占用的存储单元字节数就称为该数据类型的长度步长short 占用 2 个字节的内存,就称 short 的长度(步长)是 2。

  • ② C 语言并没有严格规定各种整数数据类型在内存中所占存储单元的长度,只做了宽泛的限制:

    • short 至少占用 2 个字节的存储单元。
    • int 建议为一个机器字长(指计算机的处理器在一次操作中能够处理的二进制数据的位数,机器字长是处理器的“字”长度,它决定了处理器在一个时钟周期内能够处理的数据量,如:早期的计算机的处理器通常是 8 位的机器字长,意味着处理器一次只能处理 8 位(二进制)数据;之后的计算机的处理器有 16 位的机器字长,意味着处理器一次可以处理 16 位的数据;再之后计算机的处理器有 32 位或 64 位的机器字长,意味着处理器一次可以处理 32 位或 64位的数据。即32 位环境下 int 占用 4 个字节的存储单元64 位环境下 int 占用 8 个字节的存储单元。
    • short 的长度(步长)不能大于 intlong 的长度(步长)不能小于 intlong long 不能小于 long。
  • ③ 那么,各种整数数据类型在内存中所占存储单元的长度的公式就是 2 ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long),具体的存储空间由编译系统自行决定。其中,sizeof 是测量类型或变量、常量长度的运算符

Important

  • ① 之所以这么规定,是为了可以让 C 语言长久使用,因为目前主流的 CPU 都是 64 位,但是在 C 语言刚刚出现的时候CPU 还是以 8 位和 16 位为主。如果当时就将整型定死为 8 位或 16 位,那么现在我们肯定不会再学习 C 语言了。
  • ② 整型分为有符号 signed 和无符号 unsigned 两种,默认是 signed。
  • ③ 在实际开发中,最常用的整数类型就是 int 类型了,如果取值范围不够,就使用 long 或 long long 。
  • ④ C 语言中的格式占位符非常多,只需要大致了解即可;因为,我们在实际开发中,一般都会使用 C++ 或 Rust 以及其它的高级编程语言Java 等,早已经解决了必须通过格式占位符来才能将变量进行输入和输出。

1.2.2 短整型(了解)

  • 语法:
unsigned short x = 10 ; // 无符号短整型
short x = -10; // 有符号短整型

Note

  • ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。
  • ② 在 printf无符号短整型unsigned short格式占位符%hu 有符号短整型signed short格式占位符%hd
  • ③ 可以通过 sizeof 运算符获取无符号短整型unsigned short有符号短整型signed short存储空间(所占内存空间)
  • ③ 可以通过 #include <limits.h> 来获取 无符号短整型unsigned short有符号短整型signed short取值范围
  • 示例:定义和打印短整型变量
#include <stdio.h>

int main() {

    // 定义有符号 short 类型
    signed short s1 = -100;

    printf("s1 = %hd \n", s1); // s1 = -100

    // 定义无符号 short 类型
    unsigned short s2 = 100;
    printf("s2 = %hu \n", s2); // s2 = 100

    // 定义 short 类型,默认是有符号
    short s3 = -200;
    printf("s3 = %hd \n", s3); // s3 = -200

    return 0;
}
  • 示例:获取类型占用的内存大小(存储空间)
#include <stdio.h>

int main() {

    size_t s1 = sizeof(unsigned short);
    printf("unsigned short 的存储空间是 %zu 字节 \n", s1); // 2

    size_t s2 = sizeof(signed short);
    printf("signed short 的存储空间是 %zu 字节 \n", s2); // 2

    size_t s3 = sizeof(short);
    printf("short 的存储空间是 %zu 字节 \n", s3); // 2

    return 0;
}
  • 示例:获取类型的取值范围
#include <limits.h>
#include <stdio.h>

int main() {

    printf("unsigned short 类型的范围是[0,%hu]\n", USHRT_MAX); // [0,65535]
    printf("short 类型的范围是[%hd,%hd]\n", SHRT_MIN,SHRT_MAX); // [-32768,32767]

    return 0;
}

1.2.3 整型

  • 语法:
unsigned int x = 10 ; // 无符号整型
int x = -10; // 有符号整型

Note

  • ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。
  • ② 在 printf无符号整型unsigned int格式占位符%u 有符号整型signed int格式占位符%d
  • ③ 可以通过 sizeof 运算符获取无符号整型unsigned int有符号整型signed int存储空间(所占内存空间)
  • ③ 可以通过 #include <limits.h> 来获取 无符号整型unsigned int有符号整型signed int取值范围
  • 示例:定义和打印整型变量
#include <stdio.h>

int main() {

    // 定义有符号 int 类型
    signed int i1 = -100;

    printf("i1 = %d \n", i1); // i1 = -100

    // 定义无符号 int 类型
    unsigned int i2 = 100;
    printf("i2 = %u \n", i2); // i2 = 100

    // 定义 int 类型,默认是有符号
    short i3 = -200;
    printf("i3 = %d \n", i3); // i3 = -200

    return 0;
}
  • 示例:获取类型占用的内存大小(存储空间)
#include <stdio.h>

int main() {

    size_t i1 = sizeof(unsigned int);
    printf("unsigned int 的存储空间是 %zu 字节 \n", i1); // 4

    size_t i2 = sizeof(signed int);
    printf("signed int 的存储空间是 %zu 字节 \n", i2); // 4

    size_t i3 = sizeof(int);
    printf("int 的存储空间是 %zu 字节 \n", i3); // 4

    return 0;
}
  • 示例:获取类型的取值范围
#include <limits.h>
#include <stdio.h>

int main() {

    printf("unsigned int 类型的范围是[0,%u]\n", UINT_MAX); // [0,4294967295]
    printf("int 类型的范围是[%d,%d]\n", INT_MIN,INT_MAX); // [-2147483648,2147483647]

    return 0;
}

1.2.4 长整型(了解)

  • 语法:
unsigned long x = 10 ; // 无符号长整型
long x = -10; // 有符号长整型

Note

  • ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。
  • ② 在 printf无符号长整型unsigned long格式占位符%lu 有符号长整型signed long格式占位符%ld
  • ③ 可以通过 sizeof 运算符获取无符号长整型unsigned long有符号长整型signed long存储空间(所占内存空间)
  • ③ 可以通过 #include <limits.h> 来获取 无符号长整型unsigned long有符号长整型signed long取值范围
  • 示例:定义和打印长整型变量
#include <stdio.h>

int main() {

    // 定义有符号 long 类型
    signed long l1 = -100;

    printf("l1 = %ld \n", l1); // l1 = -100

    // 定义无符号 long 类型
    unsigned long l2 = 100;
    printf("l2 = %lu \n", l2); // l2 = 100

    // 定义 long 类型,默认是有符号
    long l3 = -200;
    printf("l3 = %ld \n", l3); // l3 = -200

    return 0;
}
  • 示例:获取类型占用的内存大小(存储空间)
#include <stdio.h>

int main() {

    size_t l1 = sizeof(unsigned long);
    printf("unsigned long 的存储空间是 %zu 字节 \n", l1); // 4

    size_t l2 = sizeof(signed long);
    printf("signed long 的存储空间是 %zu 字节 \n", l2); // 4

    size_t l3 = sizeof(long);
    printf("long 的存储空间是 %zu 字节 \n", l3); // 4

    return 0;
}
  • 示例:获取类型的取值范围
#include <limits.h>
#include <stdio.h>

int main() {

    printf("unsigned long 类型的范围是[0,%lu]\n", ULONG_MAX); // [0,4294967295]
    printf("long 类型的范围是[%ld,%ld]\n", LONG_MIN,LONG_MAX); // [-2147483648,2147483647]

    return 0;
}

1.2.5 长长整型(了解)

  • 语法:
unsigned long long x = 10 ; // 无符号长长整型
long long x = -10; // 有符号长长整型

Note

  • ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。
  • ② 在 printf无符号长长整型unsigned long long格式占位符%llu 有符号长长整型signed long long格式占位符%lld
  • ③ 可以通过 sizeof 运算符获取无符号长长整型unsigned long long有符号长长整型signed long long存储空间(所占内存空间)
  • ③ 可以通过 #include <limits.h> 来获取 无符号长长整型unsigned long long有符号长长整型signed long long取值范围
  • 示例:定义和打印长长整型变量
#include <stdio.h>

int main() {

    // 定义有符号 long long 类型
    signed long long ll1 = -100;

    printf("ll1 = %lld \n", ll1); // ll1 = -100

    // 定义无符号 long long 类型
    unsigned long long ll2 = 100;
    printf("ll2 = %llu \n", ll2); // ll2 = 100

    // 定义 long long 类型,默认是有符号
    long long ll3 = -200;
    printf("ll3 = %lld \n", ll3); // ll3 = -200

    return 0;
}
  • 示例:获取类型占用的内存大小(存储空间)
#include <stdio.h>

int main() {

    size_t ll1 = sizeof(unsigned long long);
    printf("unsigned long long 的存储空间是 %zu 字节 \n", ll1); // 8

    size_t ll2 = sizeof(signed long long);
    printf("signed long long 的存储空间是 %zu 字节 \n", ll2); // 8

    size_t ll3 = sizeof(long long);
    printf("long long 的存储空间是 %zu 字节 \n", ll3); // 8

    return 0;
}
  • 示例:获取类型的取值范围
#include <limits.h>
#include <stdio.h>

int main() {

    printf("unsigned long long 类型的范围是[0,%llu]\n", ULLONG_MAX); // [0,18446744073709551615]
    printf("long long 类型的范围是[%lld,%lld]\n", LLONG_MIN,LLONG_MAX); // [-9223372036854775808,9223372036854775807]

    return 0;
}

1.2.6 字面量后缀

  • 字面量源代码中一个固定值表示方法,用于直接表示数据,即:
int num1 = 100; // 100 就是字面量
long num2 = 100L; // 100L 就是字面量
long long num3 = 100LL; // 100LL 就是字面量

Note

  • ① 默认情况下的,整数字面量的类型是 int 类型。
  • ② 如果需要表示 long 类型的字面量,需要添加后缀 lL ,建议 L
  • ③ 如果需要表示 long long 类型的字面量,需要添加后缀 llLL,建议 LL
  • ④ 如果需要表示无符号整数类型的字面量,需要添加 uU,建议 U
  • 示例:
#include <stdio.h>

int main() {

    int num = 100;
    printf("num = %d\n", num); // num = 100

    long num2 = 100L;
    printf("num2 = %ld\n", num2); // num2 = 100

    long long num3 = 100LL;
    printf("num3 = %lld\n", num3); // num3 = 100

    unsigned int num4 = 100U;
    printf("num4 = %u\n", num4); // num4 = 100

    unsigned long num5 = 100LU;
    printf("num5 = %lu\n", num5); // num5 = 100

    unsigned long long num6 = 100ULL;
    printf("num6 = %llu\n", num6); // num6 = 100

    return 0;
}

1.2.7 精确宽度类型

  • 在前文,我们了解到 C 语言的整数类型short 、int、long、long long在不同计算机上占用的字节宽度可能不一样。但是有的时候我们希望整数类型的存储空间字节宽度是精确的在任意平台计算机上都能一致以提高程序的可移植性。

Note

  • Java 语言中的数据类型的存储空间(字节宽度)是一致的,这也是 Java 语言能够跨平台的原因之一(最主要的原因还是 JVM
  • 在嵌入式开发中,使用精确宽度类型可以确保代码在各个平台上的一致性。
  • 在 C 语言的标准头文件 <stdint.h> 中定义了一些新的类型别名,如下所示:
类型名称 含义
int8_t 8 位有符号整数
int16_t 16 位有符号整数
int32_t 32 位有符号整数
int64_t 64 位有符号整数
uint8_t 8 位无符号整数
uint16_t 16 位无符号整数
uint32_t 32 位无符号整数
uint64_t 64 位无符号整数

Note

上面的这些类型都是类型别名,编译器会指定它们指向的底层类型,如:在某个系统中,如果 int 类型是 32 位,那么 int32_t 就会指向 int ;如果 long 类型是 32 位,那么 int32_t 就会指向 long。

  • 示例:
#include <stdio.h>
#include <stdint.h>

int main() {

    // 变量 x32 声明为 int32_t 类型,可以保证是 32 位(4个字节)的宽度。
    int32_t x32 = 45933945;
    printf("x32 = %d \n", x32); // x32 = 45933945

    return 0;
}

1.2.8 sizeof 运算符

  • 语法:
sizeof(表达式)

Note

  • ① sizeof 是运算符,不是内置函数。

  • ② 表达式可以是任何类型的数据类型、变量或常量。

  • ③ 用来获取某种数据类型、变量或常量占用的字节数量(内存中的存储单元),并且 sizeof(...)返回值类型size_t ;并且,如果是变量名称,可以省略 ();如果是数据类型,则不能省略 ()

  • ④ 在 printf 中使用占位符 %zu 来处理 size_t 类型的值。

  • ⑤ 之前也提过C 语言没有一个统一的官方机构来制定或强制执行其标准而是由一个标准委员会负责制定标准。不同的编译器可以选择部分或完全遵循这些标准。因此C 语言的编译器实现可能会有所不同,这就要求程序员在编写跨平台代码时特别注意数据类型的大小和布局。

  • ⑥ 与 C 语言不同Java 和 JavaScript 等语言的标准是强制性的。在 Java 语言中,int 类型在所有平台上都是 4 个字节,无论是在 Linux、MacOS 还是 Windows 上。因此,这些语言不需要像 C 语言那样依赖 sizeof 来处理不同平台上的数据类型大小差异,因为编译器已经在底层处理了这些差异。换言之,sizeof 运算符在 C 语言中的重要性在于它为程序员提供了一个处理不同平台上数据类型大小差异的工具。当然,如果你在 C 语言中,使用精确宽度类型,如:int8_tint16_tint32_tuint8_tuint16_tuint32_t 等,也可以确保代码在各个平台上的一致性。

  • 示例:参数是数据类型
#include <stdio.h>
#include <stddef.h>

int main() {

    size_t s = sizeof(int);

    printf("%zu \n", s); // 4

    return 0;
}
  • 示例:参数是变量
#include <stdio.h>
#include <stddef.h>

int main() {

    int num = 10;

    size_t s = sizeof(num);

    printf("%zu \n", s); // 4

    return 0;
}
  • 示例:参数是常量
#include <stdio.h>
#include <stddef.h>

int main() {

    size_t s = sizeof(10);

    printf("%zu \n", s); // 4

    return 0;
}

1.3 数值溢出

1.3.1 概述

  • 在生活中,如果一个容器的容量是固定的,我们不停的向其中注入水,那么当容器中充满水之后,再继续注入,水就会从杯子中溢出来,如下所示:

  • 在程序中也是一样的各种整数类型在内存中占用的存储单元是不同的short 在内存中占用 2 个字节的存储单元int 在内存中占用 4 个字节的存储单元。这也就意味着,各种整数类型只能存储有限的数值,当数值过大或多小的时候,超出的部分就会被直接截掉,那么数值就不能被正确的存储,我们就将这种现象就称为溢出overflow

Note

  • 如果这个数目前是最大值,再进行加法计算,数据就会超过该类型能够表示的最大值,叫做上溢出(如果最大值 + 1 会“绕回”到最小值)。
  • 如果这个数目前是最小值,再进行减法计算,数据就会超过该类型能够表示的最小值, 叫做下溢出(如果最小值 - 1 会“绕回”到最大值)。

Important

  • ① 在 C 语言中程序产生数值溢出的时候并不会引发错误而使程序自动停止这是因为计算机底层是采用二进制补码的运算规则进行处理的很多编程语言也是这样处理的Java 等)。
  • ② 但是这可能会导致不可预料的后果1996 年的亚利安 5 号运载火箭爆炸、2004 年的 Comair 航空公司航班停飞事故。
  • ③ 在实际开发中,编程时要特别注意,以避免数值溢出问题,特别是在涉及大数或小数的运算(特指整数)。

1.3.2 无符号数的取值范围

  • 在 C 语言中,无符号数unsigned 类型)的取值范围(最大值和最小值)的计算是很容易的,即:将内存中的所有位,设置为 0 就是最小值,设置为 1 就是最大值

Important

在 C 语言中,无符号整数,最高位不是符号位,它是数值的一部分。

  • unsigned char 类型为例,它在内存中占用的存储单元是 1 个字节,即 8 位。如果所有位都设置为 0 ,它的最小值就是 0 ;如果所有位设置为 1 ,它的最大值就是 2⁸ - 1 = 255 ,如下所示:

  • 那么,unsigned char 的最大值是如何计算出来的?最简单的方法就是这样的,如下所示:
  1 × 2⁰ + 1 × 2¹ + 1 × 2² + 1 × 2³ + 1 × 2⁴ + 1 × 2⁵ + 1 × 2⁶ + 1 × 2⁷ 
= 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 
= 255
  • 但是,这种计算方法虽然有效,但是非常麻烦,如果是 8 个字节的 long ,那么计算就非常麻烦了(可能要计算半天)。当然,我们也知道,这就是等比数列(高中知识),等比数列的公式,如下所示:

S_n = a_1 \times \frac{1 - r^n}{1 - r}

  • 那么,结果就是:S_8 = 1 \times \frac{1 - 2^8}{1 - 2} = \frac{1 - 256}{-1} = 255

  • 但是,貌似还是很复杂,我们可以换个思路,就是让 1111 1111+1 ,然后再 -1,这样一增一减正好抵消掉,并且不会影响最终的结果,如下所示:

  1111 1111 + 1 - 1
= 10000 0000 - 1
= 2⁹⁻¹ - 1
= 2⁸ - 1 
= 255
  • 其对应的换算过程,如下所示:

Important

  • ① 当内存中所有的位都是 1 的时候,这种“凑整”的技巧非常实用!!!
  • ② 按照上述的技巧,我们可以很容易得计算出:
    • unsinged char1 个字节) 的取值范围是:[0, 2⁸ - 1]
    • unsinged short2 个字节)的取值范围是:[0, 2¹⁶ - 1]
    • unsinged int4 个字节)的取值范围是:[0, 2³² - 1]
    • unsinged long8 个字节)的取值范围是:[0, 2⁶⁴ - 1]

1.3.3 有符号数的取值范围

  • 在 C 语言中,有符号数signed 类型)在计算机底层是以补码的形式存储的(计算的时候,也是以补码的形式进行计算的,并且符号位参与计算);但是,在读取的时候,需要采用逆向的转换,即:将补码转换为原码。

Important

在 C 语言中,有符号整数,最高位是符号位,用于表示正负数。

  • char 类型为例,它的取值范围,如下所示:
补码 反码 原码
1111 1111 1111 1110 1000 0001 -1
1111 1110 1111 1101 1000 0010 -2
1111 1101 1111 1100 1000 0011 -3
... ... ... ...
1000 0011 1000 0010 1111 1101 -125
1000 0010 1000 0001 1111 1110 -126
1000 0001 1000 0000 1111 1111 -127
1000 0000 --- --- -128
0111 1111 0111 1111 0111 1111 127
0111 1110 0111 1110 0111 1110 126
0111 1101 0111 1101 0111 1101 125
... ... ... ...
0000 0010 0000 0010 0000 0010 2
0000 0001 0000 0001 0000 0001 1
0000 0000 0000 0000 0000 0000 0
  • 从上面的列表中,我们可以得知,char 类型的取值范围是:[-2⁸, 2⁸ - 1],即:[-128, 127]
  • 对于 -128 而言,它的补码是 1000 0000,是无法按照传统的补码表示法来计算原码的,因为在补码转换到反码的时候需要 -1 ,而 1000 0000 - 1需要向高位借 1 ,而最高位是符号位是不能借的,这就非常矛盾。

Important

计算机规定,1000 0000 这个特殊的补码就表示 -128

  • 但是,为什么偏偏是 -128 ,而不是其它数字?是因为 -128 使得 char 类型的取值范围保持连贯,中间没有“空隙”。如果我们按照传统的方式来计算 -128 的补码,如下所示:
    • ① 原码:在原码表示法中,-128 的数据位是 1000 0000,但是 char 的数据位只有 7 位,那么最高位 1 就变为了符号位,剩下的数据位就是 000 0000;所以,-128 的原码就是 1000 0000
    • ② 反码:对数据位取反,-128 的反码就是:1111 1111
    • ③ 补码:在反码的基础上 +1,得到 1000 0000,是因为符号位被覆盖了,补码最终依然是 1000 0000

Note

-128 从原码转换到补码的过程中,符号位被 1 覆盖了两次,而负数的符号位本来就是 1,被 1 覆盖多少次也不会影响到数字的符号。

  • 虽然从 1000 0000 这个补码推算不出 -128,但是从 -128 却能推算出 1000 0000 这个补码,即:有符号数在存储之前先要转换为补码。

Important

  • ① 通过这种方式,-128 就成为了补码的最小值 1000 0000,而这个值不会与其他任何正数或负数的补码冲突。
    • 如果采用原码存储,那么将会出现 +0-0 的情况,即:0000 00001000 0000,这样在取值范围内,就存在两个相同的值,多此一举。
    • 如果采用原码存储,最大值不变是 127 ,但是最小值只能存储到 -127 ,不能存储到 -128,因为 -128 的原码是 1000 0000,和 -0 的原码冲突。
  • ② 这就是补码系统的强大之处,它能让整数的范围连贯,并且实现了加法和减法的统一处理。
  • ③ 按照上述的方法,我们可以很容易得计算出:
    • char1 个字节) 的取值范围是:[-2⁸, 2⁸ - 1]
    • short2 个字节)的取值范围是:[-2¹⁶, 2¹⁶ - 1]
    • int4 个字节)的取值范围是:[-2³², 2³² - 1]
    • long8 个字节)的取值范围是:[-2⁶⁴, 2⁶⁴ - 1]

1.3.4 数值溢出

  • 对于无符号的数值溢出:
    • 当数据到达最大值的时候,再 +1 就会回到无符号数的最小值。
    • 当数据达到最小值的时候,再 -1 就会回到无符号数的最大值。

Important

  • ① 对于无符号整数的运算,如:加、减、乘、除、取余等,其最小值是 0 ,最大值是 2^n - 1 。如果某个计算结果超出了这个范围,计算机会自动将结果对 2^N 取余(模),从而丢失高位,只保留低位。
  • ② 以 8 位无符号整数而言,最大值是 2551111 1111那么 255 + 1 的结果就是 (2^8 -1 + 1) % 2^8 = 0,商是 256
  • ③ 以 8 位无符号整数而言,最小值是 00000 0000那么 0 - 1 的结果就是 (0 - 1) % 2^8 = 255,商是 -1
  • 那么,无符号上溢出,原理就是这样的:

  • 那么,无符号下溢出,原理就是这样的:

  • 对于有符号的数值溢出:

    • 当数据到达最大值的时候,再 +1 就会回到有符号数的最小值。
    • 当数据达到最小值的时候,再 -1 就会回到有符号数的最大值。
  • 那么,有符号上溢出,原理就是这样的:

  • 那么,有符号下溢出,原理就是这样的:

  • 示例:无符号的上溢出和下溢出
#include <limits.h>
#include <stdio.h>

int main() {

    unsigned short s1 = USHRT_MAX + 1;
    printf("无符号的上溢出 = %hu \n", s1); // 0

    unsigned short s2 = 0 - 1;
    printf("无符号的下溢出 = %hu \n", s2); // 65535

    return 0;
}
  • 示例:有符号的上溢出和下溢出
#include <limits.h>
#include <stdio.h>

int main() {

    short s1 = SHRT_MAX + 1;
    printf("有符号的上溢出 = %hd \n", s1); // -32768

    short s2 = SHRT_MIN - 1;
    printf("有符号的下溢出 = %hd \n", s2); // 32767

    return 0;
}

1.4 浮点类型

1.4.1 概述

  • 在生活中,我们除了使用整数18、25 之外,还会使用到小数3.1415926、6.18 等,小数在计算机中也被称为浮点数(和底层存储有关)。
  • 整数在计算机底层的存储被称为定点存储,如下所示:

  • 小数在计算机底层的存储被称为浮点存储,如下所示:

Note

  • ① 计算机底层就是采取类似科学计数法的形式来存储小数的而科学计数法的表现就是这样的3.12 * 10^-2 其中10 是基数,-2 是指数,而 3.12 是尾数。
  • ② 因为尾数区的内存空间的宽度不同,导致了小数的精度也不相同,所以小数在计算机中也称为浮点数。
  • 在 C 语言中,变量的浮点类型,如下所示:
类型 存储大小 值的范围 有效小数位数
float单精度 4 字节 1.2E-38 ~ 3.4E+38 6 ~ 9
double双精度 8 字节 2.3E-308 ~ 1.7E+308 15 ~ 18
long double长双精度 16 字节 3.4E-4932 ~ 1.2E+4932 18 或更多

Note

  • ① 各类型的存储大小和精度受到操作系统、编译器、硬件平台的影响。
  • ② 浮点型数据有两种表现形式:
    • 十进制数形式3.12、512.0f、0.512.512,可以省略 0
    • 科学计数法形式5.12e2e 表示基数 10、5.12E-2E 表示基数 10
  • ③ 在实际开发中,对于浮点类型,建议使用 double 类型;如果范围不够,就使用 long double 类型。

1.4.2 格式占位符

  • 对于 float 类型的格式占位符,是 %f ,默认会保留 6 位小数,不足 6 位以 0 补充;可以指定小数位,如:%.2f 表示保留 2 位小数。
  • 对于 double 类型的格式占位符,是 %lf ,默认会保留 6 位小数,不足 6 位以 0 补充;可以指定小数位,如:%.2lf 表示保留 2 位小数。
  • 对于 long double 类型的格式占位符,是 %Lf ,默认会保留 6 位小数,不足 6 位以 0 补充;可以指定小数位,如:%.2Lf 表示保留 2 位小数。

Note

  • ① 如果想输出科学计数法形式的 float 类型的浮点数,则使用 %e
  • ② 如果想输出科学计数法形式的 double 类型的浮点数,则使用 %le
  • ③ 如果想输出科学计数法形式的 long double 类型的浮点数,则使用 %Le

Note

  • ① 浮点数还有一种更加智能的输出方式,就是使用 %gggeneral format 的缩写,即:通用格式),%g 会根据数值的大小自动判断,选择使用普通的浮点数格式(%f)进行输出,还是使用科学计数法(%e)进行输出,即:float 类型的两种输出形式。
  • ② 同理,%lg 会根据数值的大小自动判断,选择使用普通的浮点数格式(%lf)进行输出,还是使用科学计数法(%le)进行输出,即:double 类型的两种输出形式。
  • ③ 同理,%Lg 会根据数值的大小自动判断,选择使用普通的浮点数格式(%Lf)进行输出,还是使用科学计数法(%Le)进行输出,即:long double 类型的两种输出形式。
  • 示例:
#include <stdio.h>

int main() {

    float f1 = 10.0;

    printf("f1 = %f \n", f1); // f1 = 10.000000
    printf("f1 = %.2f \n", f1); // f1 = 10.00

    return 0;
}
  • 示例:
#include <stdio.h>

int main() {

    double d1 = 13.14159265354;

    printf("d1 = %lf \n", d1); // d1 = 13.141593
    printf("d1 = %.2lf \n", d1); // d1 = 13.14

    return 0;
}
  • 示例:
#include <stdio.h>

int main() {

    long double d1 = 13.14159265354;

    printf("d1 = %LF \n", d1); // d1 = 13.141593
    printf("d1 = %.2LF \n", d1); // d1 = 13.14

    return 0;
}
  • 示例:
#include <stdio.h>

int main() {

    float       f1 = 3.1415926;
    double      d2 = 3.14e2;

    printf("f1 = %.2f \n", f1); // f1 = 3.14
    printf("f1 = %.2e \n", f1); // f1 = 3.14e+00
    printf("d2 = %.2lf \n", d2); // d2 = 314.00
    printf("d2 = %.2e \n", d2); // d2 = 3.14e+02

    return 0;
}

1.4.3 字面量后缀

  • 浮点数字面量默认是 double 类型。

  • 如果需要表示 float 类型的字面量,需要后面添加后缀 fF,建议 F

  • 如果需要表示 long double 类型的字面量,需要后面添加后缀 lL,建议 L

  • 示例:

#include <stdio.h>

int main() {

    float       f1 = 3.1415926f;
    double      d2 = 3.1415926;
    long double d3 = 3.1415926L;

    printf("f1 = %.2f \n", f1); // f1 = 3.14
    printf("d2 = %.3lf \n", d2); // d2 = 3.142
    printf("d3 = %.4Lf \n", d3); // d3 = 3.1416

    return 0;
}

1.4.4 类型占用的内存大小(存储空间)

  • 可以通过 sizeof 运算符来获取 float、double 以及 long double 类型占用的内存大小(存储空间)。

  • 示例:

#include <stdio.h>

int main() {

    printf("float 的存储空间是 %zu 字节 \n", sizeof(float)); // 4
    printf("double 的存储空间是 %zu 字节 \n", sizeof(double)); // 8
    printf("long double 的存储空间是 %zu 字节 \n", sizeof(long double)); // 16

    return 0;
}

1.4.5 类型的取值范围

  • 可以通过 #include <float.h> 来获取类型的取值范围。

  • 示例:

#include <float.h>
#include <stdio.h>

int main() {

    printf("float 的取值范围是:[%.38f, %f] \n", FLT_MIN, FLT_MAX);
    printf("double 的取值范围是:[%lf, %lf] \n", DBL_MIN, DBL_MAX);
    printf("double 的取值范围是:[%Lf, %Lf] \n", LDBL_MIN, LDBL_MAX);

    return 0;
}

1.4.6 整数和浮点数的相互赋值

  • 在 C 语言中,整数和浮点数是可以相互赋值的,即:
    • 将一个整数赋值给小数类型,只需要在小数点后面加 0 就可以了。
    • 将一个浮点数赋值给整数类型,就会将小数部分丢掉,只会取整数部分,会改变数字本身的值。

Warning

  • ① 在 C 语言中浮点数赋值给整数类型会直接截断小数点后面的数编译器一般只会给出警告让我们注意一下C 语言在检查类型匹配方面不太严格,最好不要养成这样的习惯)。
  • ② 但是,在 Java 等编程语言中,这样的写法是不可以的,会在编译阶段直接报错。
  • 示例:
#include <stdio.h>

int main() {

    // 禁用 stdout 缓冲区
    setbuf(stdout, NULL);

    float a = 123;        // 整数赋值给浮点类型,只需要在小数点,后面加 0 即可
    printf("a=%f \n", a); // a=123.000000

    int b = 123.00;       // 浮点赋值给整数类型,会直接截断小数点后面的数
    printf("b=%d \n", b); // b=123
    return 0;
}

1.5 字符类型

1.5.1 概述

  • 在生活中,我们会经常说:今天天气真 ,我的性别是 ,我今年 10 岁等。像这类数据,在 C 语言中就可以用字符类型char来表示。字符类型表示个字符,使用单引号('')括起来,如:'1''A''&'
  • 但是,在生活中,也许会听到:你是好人,只是现阶段,我想学习好的啊,我们在一起等。像这类数据,在 C 语言中就可以用字符串String来表示。字符串类型表示个字符的集合,使用双引号("")括起来,如:"1""A""&""我们"

Note

  • ① C 语言的出现在 1972 年,由美国人丹尼斯·里奇设计出来;那个时候,只需要 1 个字节的内存空间就可以完美的表示拉丁体系英文文字a-z、A-Z、0-9 以及一些特殊符号所以C 语言中不支持多个字节的字符,如:中文、日文等。
  • ② 像拉丁体系英文文字a-z、A-Z、0-9 以及一些特殊符号,只需要单个字节的内存存储空间就能存储的,我们就称为窄类型;而像中文、日文等单个字节的内存空间存储不了的,我们就称为宽类型。
  • ③ 在 C 语言中是没有字符串类型是使用字符数组char 数组)来模拟字符串的。字符串中的字符在内存中按照次序、紧挨着排列,整个字符串占用一块连续的内存。
  • ④ 在 C 语言中如果想要输出中文、日文等多字节字符就需要使用字符数组char 数组)。
  • ⑤ 在 C++、Java 等高级编程语言中,已经提供了 String (字符串)类型,原生支持 Unicode可以方便地处理多语言和特殊字符。
  • 在 C 语言中,可以使用转义字符 \来表示特殊含义的字符。
转义字符 说明
\b 退格
\n 换行符
\r 回车符
\t 制表符
\" 双引号
\' 单引号
\\ 反斜杠
...

1.5.2 格式占位符

  • 在 C 语言中,使用 %c 来表示 char 类型。

  • 示例:

#include <stdio.h>

int main() {

    char c = '&';

    printf("c = %c \n", c); // c = &

    char c2 = 'a';
    printf("c2 = %c \n", c2); // c2 = a

    char c3 = 'A';
    printf("c3 = %c \n", c3); // c3 = A

    return 0;
}

1.5.3 类型占用的内存大小(存储空间)

  • 可以通过 sizeof 运算符来获取 char 类型占用的内存大小(存储空间)。

  • 示例:

#include <stdio.h>

int main() {

    printf("char 的存储空间是 %d 字节\n", sizeof(char)); // 1 
    printf("unsigned char 的存储空间是 %d 字节\n", sizeof(unsigned char)); // 1

    return 0;
}

1.5.4 类型的取值范围

  • 可以通过 #include <limits.h> 来获取类型的取值范围。

  • 示例:

#include <limits.h>
#include <stdio.h>

int main() {

    printf("char 范围是[%d,%d] \n", CHAR_MIN,CHAR_MAX); // [-128,127]
    printf("unsigned char 范围是[0,%d]\n", UCHAR_MAX); // [0,255]

    return 0;
}

1.5.5 字符类型的本质

  • 在 C 语言中char 本质上就是一个整数,是 ASCII 码中对应的数字,占用的内存大小是 1 个字节(存储空间),所以 char 类型也可以进行数学运算。

  • char 类型同样分为 signed char无符号和 unsigned char有符号其中 signed char 取值范围 -128 ~ 127unsigned char 取值范围 0 ~ 255默认是否带符号取决于当前运行环境。
  • 字符类型的数据在计算机中存储读取的过程,如下所示:

  • 示例:
#include <limits.h>
#include <stdio.h>

int main() {
    // char 类型字面量需要使用单引号包裹
    char a1 = 'A';
    char a2 = '9';
    char a3 = '\t';
    printf("c1=%c, c3=%c, c2=%c \n", a1, a3, a2);

    // char 类型本质上整数可以进行运算
    char b1 = 'b';
    char b2 = 101;
    printf("%c->%d \n", b1, b1);
    printf("%c->%d \n", b2, b2);
    printf("%c+%c=%d \n", b1, b2, b1 + b2);

    // char 类型取值范围
    unsigned char c1 = 200; // 无符号 char 取值范围 0 ~255
    signed char   c2 = 200; // 有符号 char 取值范围 -128~127c2会超出范围
    char          c3 = 200; // 当前系统char 默认是 signed char
    printf("c1=%d, c2=%d, c3=%d", c1, c2, c3);

    return 0;
}

1.5.6 输出字符方式二(了解)

  • 在 C 语言中,除了可以使用 printf() 函数输出字符之外,还可以使用 putchar()函数输出字符。

Note

  • putchar() 函数每次只能输出一个字符,如果需要输出多个字符需要调用多次;而 printf() 函数一次可以输出多个字符,并且 char 类型对应的格式占位符是 %c
  • ② 在实际开发中,使用 printf() 函数居多。
  • 示例:
#include <stdio.h>

int main() {

    char a = '1';
    char b = '2';
    char c = '&';

    /* 12& */
    putchar(a);
    putchar(b);
    putchar(c);

    return 0;
}

1.5.7 初谈字符串(了解)

  • 在 C 语言中没有专门的字符串类型,是使用字符数组来模拟字符串的,即:可以使用字符数组来存储字符串。

Note

  • ① 在 C 语言中,数组指针通常会一起出现,所以当字符数组可以保存字符串,也就意味着可以使用指针来间接存储字符串。
  • ② 在 C 语言中,可以使用 puts() 函数输出字符串,每调用一次 puts() 函数,除了输出字符串之外,还会在字符串后面加上换行,即:\n
  • ③ 在 C 语言中,可以使用 printf() 函数输出字符串,并且字符串对应的格式占位符是 %s。和 puts() 函数不同的是,printf() 函数不会在字符串后面加上换行,即:\n
  • ④ 在实际开发中,使用 printf() 函数居多。
  • 示例:
#include <stdio.h>

int main() {

    // 存储字符串
    char  str[] = "我";
    char *str2  = "爱你";

    puts(str); // 我
    puts(str2); // 爱你

    return 0;
}
  • 示例:
#include <stdio.h>

int main() {

    // 存储字符串
    char  str[] = "你";
    char *str2  = "是好人";

    printf("%s\n", str); // 你
    printf("%s\n", str2); // 是好人

    return 0;
}

1.6 布尔类型

1.6.1 概述

  • 布尔值用于表示 true、false两种状态通常用于逻辑运算和条件判断。

1.6.2 早期的布尔类型

  • 在 C 语言标准C89并没有为布尔值单独设置一个数据类型所以在判断真、假的时候使用 0 表示 false(假),非 0 表示 true(真)。

  • 示例:

#include <stdio.h>

int main() {
	// 禁用 stdout 缓冲区
    setbuf(stdout, NULL);
    
    // 使用整型来表示真和假两种状态
    int handsome = 0; 
    printf("帅不帅[0 丑1 帅] ");
    scanf("%d", &handsome);

    if (handsome) {
        printf("你真的很帅!!!");
    } else {
        printf("你真的很丑!!!");
    }

    return 0;
}

1.6.3 宏定义的布尔类型

  • 判断真假的时候,以 0false(假)、1true(真),并不直观;所以,我们可以借助 C 语言的宏定义。

  • 示例:

#include <stdio.h>

// 宏定义
#define BOOL int
#define TRUE 1
#define FALSE 0

int main() {
    // 禁用 stdout 缓冲区
    setbuf(stdout, NULL);
    
    BOOL handsome = 0;
    printf("帅不帅[FALSE 丑TRUE 帅] ");
    scanf("%d", &handsome);

    if (handsome) {
        printf("你真的很帅!!!");
    } else {
        printf("你真的很丑!!!");
    }

    return 0;
}

1.6.4 C99 标准中的布尔类型

  • 在 C99 中提供了 _Bool 关键字,用于表示布尔类型;其实,_Bool类型的值是整数类型的别名,和一般整型不同的是,_Bool类型的值只能赋值为 01 0 表示假、1 表示真),其它非 0 的值都会被存储为 1

  • 示例:

#include <stdio.h>

int main() {
    // 禁用 stdout 缓冲区
    setbuf(stdout, NULL);

    int   temp; // 使用 int 类型的变量临时存储输入
    _Bool handsome = 0;
    printf("帅不帅[0 丑1 帅] ");
    scanf("%d", &temp);

    // 将输入值转换为 _Bool 类型
    handsome = (temp != 0);

    if (handsome) {
        printf("你真的很帅!!!");
    } else {
        printf("你真的很丑!!!");
    }

    return 0;
}

1.6.5 C99 标准头文件中的布尔类型(推荐)

  • 在 C99 中提供了一个头文件 <stdbool.h>,定义了 bool 代表 _Boolfalse 代表 0 true 代表 1

Important

  • ① 在 C++、Java 等高级编程语言中是有 boolean 类型的关键字的。
  • ② 在 C23 标准中,将一些 C11 存在的关键字改为小写并去掉前置下划线,如:_Bool 改为 bool,以前的写法主要是为了避免与旧的代码发生冲突。
  • ③ 在 C23 标准中,加入了 truefalse 关键字。
  • 示例:
#include <stdbool.h>
#include <stdio.h>
#include <string.h>

int main() {

    // 禁用 stdout 缓冲区
    setbuf(stdout, NULL);

    char input[10];
    bool handsome = false;

    printf("帅不帅[false 丑true 帅] ");
    scanf("%s", input); // 使用 %s 读取字符串

    // 将输入字符串转换为布尔值
    if (strcmp(input, "true") == 0) {
        handsome = true;
    } else if (strcmp(input, "false") == 0) {
        handsome = false;
    } else {
        printf("无效输入!\n");
        return 1;
    }

    if (handsome) {
        printf("你真的很帅!!!");
    } else {
        printf("你真的很丑!!!");
    }

    return 0;
}

1.7 数据类型转换

1.7.1 概述

  • 在 C 语言编程中,经常需要对不同类型的数据进行运算,运算前需要先转换为同一类型,再运算。为了解决数据类型不一致的问题,需要对数据的类型进行转换。

1.7.2 自动类型转换(隐式转换)

1.7.2.1 运算过程中的自动类型转换

  • 不同类型的数据进行混合运算的时候,会发生数据类型转换,窄类型会自动转换为宽类型,这样就不会造成精度损失。

  • 转换规则:
    • ① 不同类型的整数进行运算的时候,窄类型整数会自动转换为宽类型整数。
    • ② 不同类型的浮点数进行运算的时候,精度小的类型会自动转换为精度大的类型。
    • ③ 整数和浮点数进行运算的时候,整数会自动转换为浮点数。
  • 转换方向:

Warning

最好避免无符号整数与有符号整数的混合运算,因为这时 C 语言会自动将 signed int 转为 unsigned int ,可能不会得到预期的结果。

  • 示例:
#include <stdio.h>

/**
 * 不同的整数类型混合运算时,宽度较小的类型会提升为宽度较大的类型。
 * 比如 short 转为 int int 转为 long 等。
 */
int main() {

    short s1 = 10;

    int i = 20;

    // s1 是 short 类型i 是 int 类型。
    // 当 s1 和 i 运算的时候,会自动转为 int 类型后,然后再计算。
    int result = s1 + i;

    printf("result = %d \n", result);

    return 0;
}
  • 示例:
#include <stdio.h>


int main() {

    int          n2 = -100;
    unsigned int n3 = 20;

    // n2 是有符号n3 是无符号。
    // 当 n2 和 n3 运算的时候,会自动转为无符号类型后,然后再计算。
    int result = n2 + n3;

    printf("result = %d \n", result);

    return 0;
}
  • 示例:
#include <stdio.h>

/**
* 不同的浮点数类型混合运算时,宽度较小的类型转为宽度较大的类型。
* 比如 float 转为 double double 转为 long double 。
*/
int main() {

    float  f1 = 1.25f;
    double d2 = 4.58667435;

    // f1 是 float 类型d2 是 double 类型。
    // 当 f1 和 d2 运算的时候,会自动转为 double 类型后,然后再计算。
    double result = f1 + d2;

    printf("result = %.8lf \n", result);

    return 0;
}
  • 示例:
#include <stdio.h>

/**
 * 整型与浮点型运算,整型转为浮点型
 */
int main() {

    int    n4 = 10;
    double d3 = 1.67;

    // n4 是 int 类型d3 是 double 类型。
    // 当 n4 和 d3 运算的时候,会自动转为 double 类型后,然后再计算。
    double result = n4 + d3;

    printf("%.2lf", result);

    return 0;
}

1.7.2.2 赋值时的自动类型转换

  • 在赋值运算中,赋值号两边量的数据类型不同时,等号右边的类型将转换为左边的类型。
  • 如果窄类型赋值给宽类型,不会造成精度损失;如果宽类型赋值给窄类型,会造成精度损失。

Warning

C 语言在检查类型匹配方面不太严格,最好不要养成这样的习惯。

  • 示例:
#include <stdio.h>

int main() {

    // 赋值:窄类型赋值给宽类型
    int    a1 = 10;
    double a2 = a1;
    printf("a2: %.2f\n", a2); // a2: 10.00

    // 转换:将宽类型转换为窄类型
    double b1 = 10.5;
    int    b2 = b1;
    printf("b2: %d\n", b2); // b2: 10

    return 0;
}

1.7.3 强制类型转换

  • 隐式类型转换中的宽类型赋值给窄类型,编译器是会产生警告的,提示程序存在潜在的隐患,如果非常明确地希望转换数据类型,就需要用到强制(或显式)类型转换。
  • 语法:
数据类型 变量名 = (类型名)变量、常量或表达式;

Warning

强制类型转换可能会导致精度损失!!!

  • 示例:
#include <stdio.h>

int main(){
    double d1 = 1.934;
    double d2 = 4.2;
    int num1 = (int)d1 + (int)d2;         // d1 转为 1d2 转为 4结果是 5
    int num2 = (int)(d1 + d2);            // d1+d2 = 6.1346.134 转为 6
    int num3 = (int)(3.5 * 10 + 6 * 1.5); // 35.0 + 9.0 = 44.0 -> int = 44

    printf("num1=%d \n", num1);
    printf("num2=%d \n", num2);
    printf("num3=%d \n", num3);

    return 0;
}

1.7.4 数据类型转换只是临时性的

  • 无论是自动类型转换还是强制类型转换,都是为了本次运算而进行的临时性转换,其转换的结果只会保存在临时的内存空间,并不会改变数据原先的类型或值,如下所示:
#include <stdio.h>
int main() {

    double total = 100.12; // 总价
    int    count = 2;      // 总数
    double price = 0.0;    // 单价

    int totalInt = (int)total; // 强制类型转换

    price = total / count; // 计算单价

    printf("total = %.2lf\n", total); // total = 100.12
    printf("totalInt = %d\n", totalInt); // totalInt = 100
    printf("price = %.2lf\n", price); // price = 50.06

    return 0;
}
  • 虽然 total 变量,通过强制类型转换变为了 int 类型,才可以赋值给 totalInt变量;但是,这种转换并没有影响 total 变量本身的类型

Note

  • ① 如果 total 变量的类型变化了,那么 total 的显示结果,就应该是 100.00 ,而不是 100.12
  • ② 那么,price 的结果,显而易见就应该是 50.00 ,而不是 50.06 了。

1.7.5 自动类型转换 VS 强制类型转换

  • 在 C 语言中,有些数据类型即可以自动类型转换,也可以强制类型转换,如:int --> doubledouble --> int 等。但是,有些数据类型只能强制类型转换,不能自动类型转换,如:void* --> int*
  • 可以自动类型转换的类型一定可以强制类型转换;但是,可以强制类型转换的类型却不一定能够自动类型转换。

Note

  • ① 目前学习到的数据类型,既可以自动类型转换,也可以强制类型转换。
  • ② 后面,如果学到指针,就会发生指针有的时候,只能强制类型转换却不能自动类型转换;需要说明的是,并非所有的指针都可以强制类型转换,是有条件的,后文讲解。
  • 可以自动类型转换的类型,在发生类型转换的时候,一般风险较低,不会给程序带来严重的后果,如:int --> double 就没什么毛病,而 double --> int 无非丢失精度而已。但是 ,只能强制类型转换的类型,在发生类型转换的时候,通常风险较高,如:char* --> int* 就非常奇怪,会导致取得的值也很奇怪,进而导致程序崩溃。

Important

  • ① 在实际开发中,如果使用 C 语言进行开发,在进行强制类型转换的时候,需要小心谨慎,防止出现一些奇怪的问题,进而导致程序崩溃!!!
  • ② 现代化的高级编程语言Java 等,直接屏蔽了指针。所以,在使用这些编程语言的时候,无需担心进行强制类型转换时,会出现一些奇怪的问题,进而导致程序崩溃!!!

1.8 再谈数据类型

  • 通过之前的知识我们知道CPU 是直接和内存打交道的CPU 在处理数据的时候会将数据临时存放到内存中。内存那么大CPU 是怎么找到对应的数据的?

  • 首先CPU 会将内存按照字节1 Bytes = 8 bit我们也称为存储单元进行划分如下所示

Note

  • ① 操作系统其实并不会直接操作实际的内存而是会通过内存管理单元MMU来操作内存并通过虚拟地址映射Virtual Address Mapping将程序使用的虚拟地址转换为物理地址。虚拟地址映射可以实现内存保护、内存共享和虚拟内存等功能使得程序能够使用比实际物理内存更大的内存空间同时确保程序间不会相互干扰。
  • ② 为了方便初学者学习,后文一律会描述 CPU 直接操作内存(这种说法不严谨,但足够简单和方便理解)。
  • ③ 这些存储单元中,存储的都是 0 和 1 这样的数据,因为计算机只能识别二进制数。

  • 并且,为了方便管理,每个独立的小单元格,即:存储单元,都有自己唯一的编号(内存地址),如下所示:

Note

之所以,要给每个存储单元加上内存地址,就是为了加快数据的存取速度,可以类比生活中的字典以及快递单号

  • 我们在定义变量的时候,是这么定义的,如下所示:
int num = 10;

Note

上述的代码其实透露了三个重要的信息:

  • ① 数据存储在哪里。
  • ② 数据的长度是多少。
  • ③ 数据的处理方式。
  • 其实,在编译器对程序进行编译的时候,是这样做的,如下所示:

Note

  • ① 编译器在编译的时候,就将变量替换为内存中存储单元的内存地址(知道了你家的门牌号),这样就可以方便的进行存取数据了(解答了上述的问题 ① )。
  • ② 变量中其实存储的是初始化值 10 在内存中存储单元的首地址,我们也知道,数据类型 int 的存储空间是 4 个字节,那么根据首地址 + 4 个字节就可以完整的将数据从内存空间中取出来或存进去(解答了上述的问题 ② )。
  • ③ 我们知道,数据在计算机底层的存储方式是不一样的,如:整数在计算机底层的存储就是计算机补码的方式,浮点数在计算机底层的存储类似于科学计数法;但是,字符类型在计算机底层的存储和整数以及浮点数完全不同,需要查码表,即:在存储的时候,需要先查询码表,转换为二进制进行存储;在读取的时候,也需要先查询码表,将二进制转换为对应的字符(解答了上述的问题 ③ )。

Important

  • ① 数据类型只在定义变量的时候声明,而且必须声明;在使用变量的时候,就无需再声明,因为此时的数据类型已经确定的。
  • ② 在实际开发中,我们通常将普通变量等价于内存中某个区域的值(底层到底是怎么转换的,那是编译器帮我们完成的,我们通常无需关心,也没必要关心)。
  • ③ 某些动态的编程语言JavaScript ,在定义变量的时候,是不需要给出数据类型的,编译器会根据赋值情况自动推断出变量的数据类型,貌似很智能;但是,这无疑增加了编译器的工作,降低了程序的性能(动态一时爽,重构火葬场,说的就是动态编程语言,不适合大型项目的开发;所以,之后微软推出了 TypeScript ,就是为了给 JavaScript 增加强类型系统,以提高开发和运行效率)。
  • 程序中的变量在内存中的表示,就是这样的,如下所示:

第二章:运算符(

2.1 概述

  • 运算符是一种特殊的符号,用于数据的运算、赋值和比较等。
  • 表达式指的是一组运算数、运算符的组合,表达式一定具有值,一个变量或一个常量可以是表达式,变量、常量和运算符也可以组成表达式,如:

  • 操作数指的是参与运算或者对象,如:

  • 根据操作数个数,可以将运算符分为:
    • 一元运算符(一目运算符)。
    • 二元运算符(二目运算符)。
    • 三元运算符(三目运算符)。
  • 根据功能,可以将运算符分为:
    • 算术运算符。
    • 关系运算符(比较运算符)。
    • 逻辑运算符。
    • 赋值运算符。
    • 逻辑运算符。
    • 位运算符。
    • 三元运算符。

Note

掌握一个运算符,需要关注以下几个方面:

  • ① 运算符的含义。
  • ② 运算符操作数的个数。
  • ③ 运算符所组成的表达式。
  • ④ 运算符有无副作用,即:运算后是否会修改操作数的值。

Caution

  • ① 其实在后端语言C、C++、Java 等,表达式和语法并不会区分的很明显。
  • ② 但是,对于前端 JavaScript 框架中的 React 而言,其在 JSX 中要求 {}中必须是表达式,而不能是语句,如下所示:
import React, { useState } from 'react';

function Welcome() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  // 切换登录状态的函数
  const toggleLogin = () => {
    setIsLoggedIn(!isLoggedIn);
  };

  // 在 JSX 外部处理逻辑
  let message;
  if (isLoggedIn) {
    message = "Welcome back!";
  } else {
    message = "Please sign in";
  }

  return (
    <div>
      {/* 使用 JSX 表达式来渲染内容 */}
      <h1>{message}</h1>

      {/* 使用三元运算符 */}
      <p>{isLoggedIn ? "You have new notifications." : "No notifications"}</p>

      {/* 使用逻辑运算符 && 渲染内容 */}
      {isLoggedIn && <p>You are logged in as a premium user.</p>}

      {/* 切换登录状态按钮 */}
      <button onClick={toggleLogin}>
        {isLoggedIn ? "Log out" : "Log in"}
      </button>
    </div>
  );
}

export default Welcome;

2.2 算术运算符

  • 算术运算符是对数值类型的变量进行运算的,如下所示:
运算符 描述 操作数个数 组成的表达式的值 副作用
+ 正号 1 操作数本身
- 负号 1 操作数符号取反
+ 加号 2 两个操作数之和
- 减号 2 两个操作数之差
* 乘号 2 两个操作数之积
/ 除号 2 两个操作数之商
% 取模(取余) 2 两个操作数相除的余数
++ 自增 1 操作数自增前或自增后的值
-- 自减 1 操作数自减前或自减后的值

Note

自增和自减:

  • ① 自增、自减运算符可以写在操作数的前面也可以写在操作数后面,不论前面还是后面,对操作数的副作用是一致的。
  • ② 自增、自减运算符在前在后,对于表达式的值是不同的。 如果运算符在前,表达式的值是操作数自增、自减之后的值;如果运算符在后,表达式的值是操作数自增、自减之前的值。
  • 变量前++:变量先自增 1 ,然后再运算;变量后++:变量先运算,然后再自增 1 。
  • 变量前--:变量先自减 1 ,然后再运算;变量后--:变量先运算,然后再自减 1 。
  • ⑤ 对于 i++i-- 各种编程语言的用法和支持是不同的例如C/C++、Java 等完全支持Python 压根一点都不支持Go 语言虽然支持 i++i-- ,却只支持这些操作符作为独立的语句,并且不能嵌入在其它的表达式中。
  • 示例:正号和负号
#include <stdio.h>

int main() {

    int x  = 12;
    int x1 = -x, x2 = +x;

    int y  = -67;
    int y1 = -y, y2 = +y;

    printf("x1=%d, x2=%d \n", x1, x2); // x1=-12, x2=12
    printf("y1=%d, y2=%d \n", y1, y2); // y1=67, y2=-67

    return 0;
}
  • 示例:加、减、乘、除(整数之间做除法时,结果只保留整数部分而舍弃小数部分)、取模
#include <stdio.h>

int main() {

    int a = 5;
    int b = 2;

    printf("%d + %d = %d\n", a, b, a + b); // 5 + 2 = 7
    printf("%d - %d = %d\n", a, b, a - b); // 5 - 2 = 3
    printf("%d × %d = %d\n", a, b, a * b); // 5 × 2 = 10
    printf("%d / %d = %d\n", a, b, a / b); // 5 / 2 = 2
    printf("%d %% %d = %d\n", a, b, a % b); // 5 % 2 = 1

    return 0;
}
  • 示例:取模(运算结果的符号与被模数也就是第一个操作数相同。)
#include <stdio.h>

int main() {

    int res1 = 10 % 3;
    printf("10 %% 3 = %d\n", res1); // 10 % 3 = 1

    int res2 = -10 % 3;
    printf("-10 %% 3 = %d\n", res2); // -10 % 3 = -1

    int res3 = 10 % -3;
    printf("10 %% -3 = %d\n", res3); // 10 % -3 = 1

    int res4 = -10 % -3;
    printf("-10 %% -3 = %d\n", res4); // -10 % -3 = -1

    return 0;
}
  • 示例:自增和自减
#include <stdio.h>

int main() {

    int i1 = 10, i2 = 20;
    int i  = i1++;
    printf("i = %d\n", i); // i = 10
    printf("i1 = %d\n", i1); // i1 = 11

    i = ++i1;
    printf("i = %d\n", i); // i = 12
    printf("i1 = %d\n", i1); // i1 = 12

    i = i2--;
    printf("i = %d\n", i); // i = 20
    printf("i2 = %d\n", i2); // i2 = 19

    i = --i2;
    printf("i = %d\n", i); // i = 18
    printf("i2 = %d\n", i2); // i2 = 18

    return 0;

  • 示例:
#include <stdio.h>

/*
  随意给出一个整数,打印显示它的个位数,十位数,百位数的值。
  格式如下:
    数字xxx的情况如下
    个位数:
    十位数:
    百位数:
  例如:
    数字153的情况如下
    个位数3
    十位数5
    百位数1
 */
int main() {

    int num = 153;

    int bai = num / 100;
    int shi = num % 100 / 10;
    int ge  = num % 10;
    printf("百位为:%d \n", bai);
    printf("十位为:%d \n", shi);
    printf("个位为:%d \n", ge);

    return 0;
}

2.3 关系运算符(比较运算符)

  • 常见的关系运算符,如下所示:
运算符 描述 操作数个数 组成的表达式的值 副作用
== 相等 2 0 或 1
!= 不相等 2 0 或 1
< 小于 2 0 或 1
> 大于 2 0 或 1
<= 小于等于 2 0 或 1
>= 大于等于 2 0 或 1

Note

  • ① C 语言中,没有严格意义上的布尔类型,可以使用 0 或 1表示布尔类型的值。
  • ② 不要将 == 写成 === 是比较运算符,而 = 是赋值运算符。
  • >=<=含义是只需要满足 大于或等于小于或等于其中一个条件,结果就返回真。
  • 示例:
#include <stdio.h>

int main() {

    int a = 8;
    int b = 7;

    printf("a > b 的结果是:%d \n", a > b); // a > b 的结果是1
    printf("a >= b 的结果是:%d \n", a >= b); // a >= b 的结果是1
    printf("a < b 的结果是:%d \n", a < b); // a < b 的结果是0
    printf("a <= b 的结果是:%d \n", a <= b); // a <= b 的结果是0
    printf("a == b 的结果是:%d \n", a == b); // a == b 的结果是0
    printf("a != b 的结果是:%d \n", a != b); // a != b 的结果是1

    return 0;
}

2.4 逻辑运算符

  • 常见的逻辑运算符,如下所示:
运算符 描述 操作数个数 组成的表达式的值 副作用
&& 逻辑与 2 0 或 1
|| 逻辑或 2 0 或 1
! 逻辑非 2 0 或 1
  • 逻辑运算符提供逻辑判断功能,用于构建更复杂的表达式,如下所示:
a b a && b a || b !a
1 1 1 1 0
1 0 0 1 0
0 1 0 1 1
0 0 0 0 1

Note

  • ① 对于逻辑运算符来说,任何非零值都表示零值表示,如:5 || 0 返回 1 5 && 0 返回 0
  • ② 逻辑运算符的理解:
    • && 的理解就是:两边条件,同时满足
    • ||的理解就是:两边条件,二选一
    • ! 的理解就是:条件取反
  • ③ 短路现象:
    • 对于 a && b 操作来说,当 a 为假(或 0 )时,因为 a && b 结果必定为 0所以不再执行表达式 b。
    • 对于 a || b 操作来说,当 a 为真(或非 0 )时,因为 a || b 结果必定为 1所以不再执行表达式 b。
  • 示例:
#include <stdio.h>

int main() {

    int a = 0;
    int b = 0;

    printf("请输入整数a的值");
    scanf("%d", &a);
    printf("请输入整数b的值");
    scanf("%d", &b);

    if (a > b) {
        printf("%d > %d", a, b);
    } else if (a < b) {
        printf("%d < %d", a, b);
    } else {
        printf("%d = %d", a, b);
    }

    return 0;
}
  • 示例:
#include <stdio.h>

// 短路现象
int main() {

    int i = 0;
    int j = 10;
    if (i && j++ > 0) {
        printf("床前明月光\n"); // 这行代码不会执行
    } else {
        printf("我叫郭德纲\n");
    }
    printf("%d \n", j); //10

    return 0;
}
  • 示例:
#include <stdio.h>

// 短路现象

int main() {

    int i = 1;
    int j = 10;
    if (i || j++ > 0) {
        printf("床前明月光 \n");
    } else {
        printf("我叫郭德纲 \n"); // 这行代码不会被执行
    }
    printf("%d\n", j); //10

    return 0;
}

2.5 赋值运算符

  • 常见的赋值运算符,如下所示:
运算符 描述 操作数个数 组成的表达式的值 副作用
== 赋值 2 左边操作数的值
+= 相加赋值 2 左边操作数的值
-= 相减赋值 2 左边操作数的值
*= 相乘赋值 2 左边操作数的值
/= 相除赋值 2 左边操作数的值
%= 取余赋值 2 左边操作数的值
<<= 左移赋值 2 左边操作数的值
>>= 右移赋值 2 左边操作数的值
&= 按位与赋值 2 左边操作数的值
^= 按位异或赋值 2 左边操作数的值
|= 按位或赋值 2 左边操作数的值

Note

  • ① 赋值运算符的第一个操作数(左值)必须是变量的形式,第二个操作数可以是任何形式的表达式。
  • ② 赋值运算符的副作用针对第一个操作数。
  • 示例:
#include <stdio.h>

int main() {

    int a = 3;
    a += 3; // a = a + 3
    printf("a = %d\n", a); // a = 6

    int b = 3;
    b -= 3; // b = b - 3
    printf("b = %d\n", b); // b = 0

    int c = 3;
    c *= 3; // c = c * 3
    printf("c = %d\n", c); // c = 9

    int d = 3;
    d /= 3; // d = d / 3
    printf("d = %d\n", d); // d = 1

    int e = 3;
    e %= 3; // e = e % 3
    printf("e = %d\n", e); // e = 0

    return 0;
}

2.6 位运算符(了解)

2.6.1 概述

  • C 语言提供了一些位运算符能够让我们操作二进制位bit
  • 常见的位运算符,如下所示。
运算符 描述 操作数个数 运算规则 副作用
& 按位与 2 两个二进制位都为 1 ,结果为 1 ,否则为 0 。
| 按位或 2 两个二进制位只要有一个为 1包含两个都为 1 的情况),结果为 1 ,否则为 0 。
^ 按位异或 2 两个二进制位一个为 0 ,一个为 1 ,结果为 1否则为 0 。
~ 按位取反 2 将每一个二进制位变成相反值,即 0 变成 1 1 变 成 0 。
<< 二进制左移 2 将一个数的各二进制位全部左移指定的位数,左 边的二进制位丢弃,右边补 0。
>> 二进制右移 2 将一个数的各二进制位全部右移指定的位数,正数左补 0负数左补 1右边丢弃。

Note

操作数在进行位运算的时候,以它的补码形式计算!!!

2.6.2 输出二进制位

  • 在 C 语言中,printf 是没有提供输出二进制位的格式占位符的;但是,我们可以手动实现,以方便后期操作。

  • 示例:

#include <stdio.h>

/**
 * 获取指定整数的二进制表示
 * @param num 整数
 * @return 二进制表示的字符串,不包括前导的 '0b' 字符
 */
char* getBinary(int num) {
    static char binaryString[33];
    int         i, j;

    for (i = sizeof(num) * 8 - 1, j = 0; i >= 0; i--, j++) {
        const int bit   = (num >> i) & 1;
        binaryString[j] = bit + '0';
    }

    binaryString[j] = '\0';
    return binaryString;
}

int main() {

    int a = 17;
    int b = -12;

    printf("整数 %d 的二进制表示:%s \n", a, getBinary(a));
    printf("整数 %d 的二进制表示:%s \n", b, getBinary(b));

    return 0;
}

2.6.3 按位与

  • 按位与 & 的运算规则是:如果二进制对应的位上都是 1 才是 1 ,否则为 0 ,即:

    • 1 & 1 的结果是 1
    • 1 & 0 的结果是 0
    • 0 & 1 的结果是 0
    • 0 & 0 的结果是 0
  • 示例:9 & 7 = 1

  • 示例:-9 & 7 = 7

2.6.4 按位或

  • 按位与 | 的运算规则是:如果二进制对应的位上只要有 1 就是 1 ,否则为 0 ,即:

    • 1 | 1 的结果是 1
    • 1 | 0 的结果是 1
    • 0 | 1 的结果是 1
    • 0 | 0 的结果是 0
  • 示例:9 | 7 = 15

  • 示例:-9 | 7 = -9

2.6.5 按位异或

  • 按位与 ^ 的运算规则是:如果二进制对应的位上一个为 1 一个为 0 就为 1 ,否则为 0 ,即:
    • 1 ^ 1 的结果是 0
    • 1 ^ 0 的结果是 1
    • 0 ^ 1 的结果是 1
    • 0 ^ 0 的结果是 0

Note

按位异或的场景有:

  • ① 交换两个数值:异或操作可以在不使用临时变量的情况下交换两个变量的值。
  • ② 加密或解密:异或操作用于简单的加密和解密算法。
  • ③ 错误检测和校正异或操作可以用于奇偶校验位的计算和检测错误RAID-3 以及以上)。
  • ……
  • 示例:9 ^ 7 = 14

  • 示例:-9 ^ 7 = -16

2.6.6 按位取反

  • 运算规则:如果二进制对应的位上是 1则结果为 0如果是 0 ,则结果为 1 。

    • ~0 的结果是 1
    • ~1 的结果是 0
  • 示例:~9 = -10

  • 示例:~-9 = 8

2.6.7 二进制左移

  • 在一定范围内,数据每向左移动一位,相当于原数据 × 2。正数、负数都适用

  • 示例:3 << 4 = 48 3 × 2^4

  • 示例:-3 << 4 = -48 -3 × 2 ^4

2.6.8 二进制右移

  • 在一定范围内,数据每向右移动一位,相当于原数据 ÷ 2。正数、负数都适用

Note

  • ① 如果不能整除,则向下取整。
  • ② 右移运算符最好只用于无符号整数,不要用于负数。因为不同系统对于右移后如何处理负数的符号位,有不同的做法,可能会得到不一样的结果。
  • 示例:69 >> 4 = 4 69 ÷ 2^4

  • 示例:-69 >> 4 = -5 -69 ÷ 2^4

2.7 三元运算符

  • 语法:
条件表达式 ? 表达式1 : 表达式2 ;

Note

  • 如果条件表达式为非 0 (真),则整个表达式的值是表达式 1 。
  • 如果条件表达式为 0 (假),则整个表达式的值是表达式 2 。
  • 示例:
#include <stdio.h>

int main() {

    int m      = 110;
    int n      = 20;
    int result = m > n ? m : n;
    printf("result = %d\n", result); // result = 110

    return 0;
}

2.8 运算符的优先级和结合性

  • 在数学中,如果一个表达式是 a + b * c ,我们知道其运算规则就是:先算乘除再算加减。其实,在 C 语言中也是一样的先算乘法再算加减C 语言中乘除的运算符比加减的运算符的优先级要高。

Note

优先级结合性的定义,如下所示:

  • ① 所谓的优先级:就是当多个运算符出现在同一个表达式中时,先执行哪个运算符。
  • ② 所谓的结合性:就是当多个相同优先级的运算符出现在同一个表达式中的时候,是从左到右运算,还是从右到左运算。
    • 左结合性:具有相同优先级的运算符将从左到右➡️)进行计算。
    • 右结合性:具有相同优先级的运算符将从右到左⬅️)进行计算。

总结:先看优先级;如果优先级相同,再看结合性

  • C 语言中运算符的优先级有几十个,有的运算符优先级不同,有的运算符优先级相同,如下所示:
优先级 运算符 名称或含义 结合方向
0 () 小括号,最高优先级 ➡️(从左到右)
1 ++-- 后缀自增和自减,如:i++i-- ➡️(从左到右)
() 小括号,函数调用,如:sum(1,2)
[] 数组下标,如:arr[0]arr[1]
. 结构体或共用体成员访问
-> 结构体或共用体成员通过指针访问
2 ++-- 前缀自增和自减,如:++i--i ⬅️(从右到左)
+ 一元加运算符,表示操作数的正,如:+2
- 一元减运算符,表示操作数的负,如:-3
! 逻辑非运算符(逻辑运算符)
~ 按位取反运算符(位运算符)
typename 强制类型转换
* 解引用运算符
& 取地址运算符
sizeof 取大小运算符
3 / 除法运算符(算术运算符) ➡️(从左到右)
* 乘法运算符(算术运算符)
% 取模(取余)运算符(算术运算符)
4 + 二元加运算符(算术运算符),如:2 + 3 ➡️(从左到右)
- 二元减运算符(算术运算符),如:3 - 2
5 << 左移位运算符(位运算符) ➡️(从左到右)
>> 右移位运算符(位运算符)
6 > 大于运算符(比较运算符) ➡️(从左到右)
>= 大于等于运算符(比较运算符)
< 小于运算符(比较运算符)
<= 小于等于运算符(比较运算符)
7 == 等于运算符(比较运算符) ➡️(从左到右)
!= 不等于运算符(比较运算符)
8 & 按位与运算符(位运算符) ➡️(从左到右)
9 ^ 按位异或运算符(位运算符) ➡️(从左到右)
10 | 按位或运算符(位运算符) ➡️(从左到右)
11 && 逻辑与运算符(逻辑运算符) ➡️(从左到右)
12 || 逻辑或运算符(逻辑运算符) ➡️(从左到右)
13 ?: 三目(三元)运算符 ⬅️(从右到左)
14 = 简单赋值运算符(赋值运算符) ⬅️(从右到左)
/= 除后赋值运算符(赋值运算符)
*= 乘后赋值运算符(赋值运算符)
%= 取模后赋值运算符(赋值运算符)
+= 加后赋值运算符(赋值运算符)
-= 减后赋值运算符(赋值运算符)
<<= 左移后赋值运算符(赋值运算符)
>>= 右移后赋值运算符(赋值运算符)
&= 按位与后赋值运算符(赋值运算符)
^= 按位异或后赋值运算符(赋值运算符)
|= 按位或后赋值运算符(赋值运算符)
15 , 逗号运算符 ➡️(从左到右)

Warning

  • ① 不要过多的依赖运算符的优先级来控制表达式的执行顺序,这样可读性太差,尽量使用小括号来控制表达式的执行顺序。
  • ② 不要把一个表达式写得过于复杂,如果一个表达式过于复杂,则把它分成几步来完成。
  • ③ 运算符优先级不用刻意地去记忆,总体上:一元运算符 > 算术运算符 > 关系运算符 > 逻辑运算符 > 三元运算符 > 赋值运算符。