2.4 常量与变量

常数的问题

2.4.1 常数的问题.mp4 (13.29MB)

数字常量

  • 数字常量
  • 实数的科学计数法表示方式
  • 字符常量
  • 字符串常量
  • 转义字符

2.4.2 数字常量.mp4 (23.04MB)

变量

  • 变量的概念
  • 变量的地址
  • 变量的值
  • 变量的名称

2.4.3 变量.mp4 (25.24MB)

notes

简述

理解在程序中直接使用常量的弊端
知道常量是程序中无法被改变的量
知道变量是程序中可以被改变的量
知道多种在程序中表示常量的方式,以及这些方式之间的差异
知道 C 语言中都有哪些常量
掌握变量的定义和初始化
掌握浮点数的科学计数表示法

直接使用常数的弊端

在 C 语言中,直接使用常数可能存在以下问题:

  1. 可读性差:因为常数可能没有具体的含义和描述,难以理解常数在程序中的作用。
  2. 代码维护困难:当需要修改常数值时,需要在程序中进行全局搜索和替换,很容易出现错误和遗漏,导致代码维护困难。
  3. 可移植性差:可能会依赖于特定的编译器或平台,导致代码不具备可移植性。
  4. 错误风险高:容易出现类型错误或计算错误,导致程序运行出错或结果不正确。

小结:

  • 在程序设计中,应尽量避免直接使用常数
  • 可以使用 宏定义、常量变量、枚举类型 等方法来定义常数,并在程序中使用这些定义的常数,以提高代码的可读性和可维护性

#define 宏定义

  • 在 C 语言中,宏定义是通过 #define 指令来定义的,其语法格式如下:

    1. #define 宏名 替换文本
  • 宏名是标识符,用来表示宏定义的名称;

  • 替换文本是宏定义的具体内容,可以是任意文本,包括常数、表达式、语句、函数等;
  1. #define PI 3.14159 // 定义常数宏
  2. #define SQUARE(x) x*x // 定义带参数宏
  3. #define PRINT printf // 定义函数宏
  • 在宏定义中使用括号可以避免一些意外的错误,特别是带参数的宏定义中,必须使用括号来保证优先级的正确性
  • 在使用宏定义时,也需要注意避免一些常见的陷阱,如多次计算、副作用等问题

const 常量变量

  • 在 C 语言中,常量变量是通过使用 const 关键字来定义的,其语法格式如下:

    1. const 数据类型 常量名称 = 常量值;
  • const 是关键字,用来表示该变量是常量,不可修改;

  • 数据类型 表示常量的数据类型;
  • 常量名称 是标识符,用来表示常量的名称;
  • 常量值 是常量的具体数值,可以是任意合法的表达式或常量;
  1. const int MAX_NUM = 100; // 定义整型常量变量
  2. const float PI = 3.14159; // 定义浮点型常量变量
  3. const char* MESSAGE = "Hello"; // 定义字符串常量变量
  • 常量变量一旦被定义后,就不能被修改了。如果试图修改常量变量的值,将会导致编译错误。
  • 常量变量的作用是为了 提高程序的可读性和可维护性,避免程序中出现不必要的硬编码常数
  • 常量变量的作用域和生命周期与普通变量相同,可以在任意位置使用和访问。
  • 常量变量可以用来作为函数的参数和返回值,以及数组的维度等。

为什么推荐使用常量变量来定义常数,而不推荐使用宏定义来定义常数

image.png

推荐使用常量变量来定义常数,因为它具有数据类型的限制,可以进行类型检查,便于程序调试和维护

在 C 语言中,宏定义和常量变量都可以用来定义常数,但它们之间有一些差异:

  • 定义方式不同
    • 宏定义使用 #define 指令来定义
    • 常量变量则需要使用 const 关键字和数据类型来定义
  • 编译时期处理不同
    • 宏定义是在预处理阶段进行处理的
    • 常量变量是在编译阶段进行处理的
  • 存储方式不同
    • 宏定义只是简单的文本替换,不会分配存储空间
    • 常量变量会分配存储空间
  • 数据类型不同
    • 宏定义没有数据类型的限制
    • 常量变量需要指定数据类型
  • 取值方式不同
    • 宏定义取值时是简单的文本替换,没有类型检查
    • 常量变量取值时需要进行类型转换
  • 作用域不同
    • 宏定义没有作用域的限制
    • 常量变量的作用域和生命周期是有限制的
  • 调试信息不同
    • 宏定义不会出现在调试信息中
    • 常量变量会出现在调试信息中

输入半径,求圆的周长和面积

  1. #include "stdio.h"
  2. int main() {
  3. float r, circum, area;
  4. printf("input r:");
  5. scanf("%f", &r);
  6. circum = 2 * 3.1415926 * r; // 3.1415926 常数
  7. area = 3.1415926 * r * r;
  8. printf("circumference = %.2f\n", circum);
  9. printf("area = %.2f\n", area);
  10. return 0;
  11. }
  12. /* 运行结果:
  13. input r:5.3
  14. circumference = 33.30
  15. area = 88.25
  16. */
  1. #include "stdio.h"
  2. #define PI 3.1415926
  3. int main() {
  4. float r, circum, area;
  5. printf("input r:");
  6. scanf("%f", &r);
  7. circum = 2 * PI * r;
  8. area = PI * r * r;
  9. printf("circumference = %.2f\n", circum);
  10. printf("area = %.2f\n", area);
  11. return 0;
  12. }

#define PI 3.1415926
注意:#define 语句结尾不要加分号

  1. #include "stdio.h"
  2. int main() {
  3. const double pi = 3.1415926;
  4. float r, circum, area;
  5. printf("input r:");
  6. scanf("%f", &r);
  7. circum = 2 * pi * r;
  8. area = pi * r * r;
  9. printf("circumference = %.2f\n", circum);
  10. printf("area = %.2f\n", area);
  11. return 0;
  12. }

#define PI 3.1415926 VS const double pi = 3.1415926;

  • const 常量具有数据类型
  • 某些集成化调试工具可以对 const 常量进行调试

思考:之前的课程中,导入头文件我们使用的都是 #include <stdio.h> 而本节使用的是 #include "stdio.h" 这两种写法之间是等效的嘛? 答:就上述程序,我们可以认为这两种写法都是等效的。但实际上它们是存在一些细微的差异的 👉🏻 对比两种不同的引入头文件的写法之间的差异 #include “stdio.h” 和 #include

头文件

  • include 是一个预处理器指令,用于包含一个头文件。

  • 头文件中包含了常量、函数声明和定义、宏定义等信息。
  • include 指令包含的头文件分为系统头文件和用户头文件。

    • 系统头文件是由编译器提供的,它们包含了一些与平台相关的函数声明和定义、宏定义等信息。这些头文件通常位于系统目录中,如 等。
    • 用户头文件则是由程序员自己编写的,它们包含了自定义的函数声明和定义、宏定义等信息。这些头文件通常位于程序所在的目录中,或者自定义的目录中,如 “myheader.h”。
  • 在 #include 指令中,头文件名可以用尖括号括起来,也可以用双引号括起来。
    • 使用尖括号括起来的头文件名表示系统头文件,编译器会从系统目录中查找该文件;
    • 使用双引号括起来的头文件名表示用户头文件,编译器会先在程序所在目录中查找该文件,如果找不到再去系统目录中查找。

Q:#include 和 #include “stdio.h” 之间的差异?
A:#include 表示包含系统头文件 stdio.h,而 #include “stdio.h” 表示包含用户头文件 stdio.h。

对比两种不同的引入头文件的写法之间的差异 #include “stdio.h” 和 #include

在 C 语言中,**#include** 是用来包含头文件的预处理指令,用于将头文件中定义的函数和变量导入到当前文件中以供使用。

#include "filename"#include <filename> 之间的差异在于 搜索头文件的路径不同

  • #include "filename" 会先在当前源文件所在目录下搜索指定的头文件,如果找到了则直接使用,如果没有找到,则在编译器指定的标准库目录中查找。
  • #include <filename> 则只在编译器指定的标准库目录中查找指定的头文件,不会在当前源文件所在目录下搜索。

最佳实践:

  • 如果头文件是自己编写的,位于当前源文件所在的目录下,应该使用 #include "filename"
  • 如果头文件是标准库中已经存在的,应该使用 #include <filename>

注意:头文件包含的内容可能会与编译器和操作系统有关,因此在不同的编译器和操作系统下,可能需要使用不同的头文件和头文件路径

整型常量

image.png

  1. #include <stdio.h>
  2. int main() {
  3. // 十进制整型常量
  4. int a = 100;
  5. printf("a = %d\n", a);
  6. // 八进制整型常量
  7. int b = 012;
  8. printf("b = %d\n", b);
  9. // 十六进制整型常量
  10. int c = 0x12;
  11. printf("c = %d\n", c);
  12. // 输出二进制整型常量
  13. printf("binary: %d\n", 0b1100);
  14. return 0;
  15. }
  16. /* 运行结果:
  17. a = 100
  18. b = 10
  19. c = 18
  20. binary: 12
  21. */
  1. #include <stdio.h>
  2. #include <limits.h>
  3. int main() {
  4. long int li = 123456789012345; // 长整型
  5. unsigned int ui = 4294967295; // 无符号整型
  6. unsigned long int uli = 12345678901234567890UL; // 无符号长整型
  7. printf("long int: %ld, %ld to %ld\n", li, LONG_MIN, LONG_MAX);
  8. printf("unsigned int: %u, 0 to %u\n", ui, UINT_MAX);
  9. printf("unsigned long int: %lu, 0 to %lu\n", uli, ULONG_MAX);
  10. return 0;
  11. }
  12. /* 运行结果:
  13. long int: 123456789012345, -9223372036854775808 to 9223372036854775807
  14. unsigned int: 4294967295, 0 to 4294967295
  15. unsigned long int: 12345678901234567890, 0 to 18446744073709551615
  16. */

二进制、八进制、十进制、十六进制、位、字节

进制是一种数值表示方式,是指在数的表示中采用的基数,也叫基数或底数。目前广泛使用的进制有四种,分别是二进制、八进制、十进制、十六进制。

二进制(Binary)

  • 二进制也叫二进制数,是计算机中使用的基本进位制
  • 其数字只有 0 和 1 两种状态,表示方式以 2 为基数
  • 在计算机中,二进制常用于表示机器指令、内存地址等信息

八进制(Octal)

  • 八进制也叫八进制数,是一种基数为 8 的进位制数
  • 其数字由 0、1、2、3、4、5、6、7 这 8 个数码组成
  • 在计算机中,八进制常用于表示文件权限等信息

十进制(Decimal)

  • 十进制也叫十进制数,以 10 为基数
  • 其数字由 0、1、2、3、4、5、6、7、8、9 这 10 个数码组成
  • 是人们生活中最常使用的数字系统

十六进制(Hexadecimal)

  • 十六进制也叫十六进制数,以 16 为基数
  • 其数字由 0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F 这 16 个数码组成
  • 在计算机中,十六进制常用于表示颜色、内存地址等信息

位(bit)

  • 是计算机信息处理的最小单位
  • 用二进制的 0 和 1 表示

字节(byte)

  • 是计算机存储数据的基本单位,一个字节等于八个二进制位(即 8 个 bit)
  • 在计算机中,一个英文字母通常占用一个字节的空间,一个汉字通常占用两个字节的空间

<limits.h> 是一个 C 语言标准库头文件,用于定义一些常量和类型特性,包括整数类型的最大值和最小值,字符类型的最大值和最小值等。

下面是一些常见的宏定义:

  • CHAR_BIT:定义每个字符的位数
  • SCHAR_MIN:有符号字符型的最小值
  • SCHAR_MAX:有符号字符型的最大值
  • UCHAR_MAX:无符号字符型的最大值
  • SHRT_MIN:有符号短整型的最小值
  • SHRT_MAX:有符号短整型的最大值
  • USHRT_MAX:无符号短整型的最大值
  • INT_MIN:有符号整型的最小值
  • INT_MAX:有符号整型的最大值
  • UINT_MAX:无符号整型的最大值
  • LONG_MIN:有符号长整型的最小值
  • LONG_MAX:有符号长整型的最大值
  • ULONG_MAX:无符号长整型的最大值
  1. #include <stdio.h>
  2. #include <limits.h>
  3. int main() {
  4. printf("char size: %d bits\n", CHAR_BIT);
  5. printf("signed char min: %d\n", SCHAR_MIN);
  6. printf("signed char max: %d\n", SCHAR_MAX);
  7. printf("unsigned char max: %d\n", UCHAR_MAX);
  8. printf("signed short min: %d\n", SHRT_MIN);
  9. printf("signed short max: %d\n", SHRT_MAX);
  10. printf("unsigned short max: %d\n", USHRT_MAX);
  11. printf("signed int min: %d\n", INT_MIN);
  12. printf("signed int max: %d\n", INT_MAX);
  13. printf("unsigned int max: %u\n", UINT_MAX);
  14. printf("signed long min: %ld\n", LONG_MIN);
  15. printf("signed long max: %ld\n", LONG_MAX);
  16. printf("unsigned long max: %lu\n", ULONG_MAX);
  17. return 0;
  18. }
  19. /* 运行结果:
  20. char size: 8 bits
  21. signed char min: -128
  22. signed char max: 127
  23. unsigned char max: 255
  24. signed short min: -32768
  25. signed short max: 32767
  26. unsigned short max: 65535
  27. signed int min: -2147483648
  28. signed int max: 2147483647
  29. unsigned int max: 4294967295
  30. signed long min: -9223372036854775808
  31. signed long max: 9223372036854775807
  32. unsigned long max: 18446744073709551615
  33. */

浮点数的科学计数法表示

浮点数的科学计数法表示形式是指:将浮点数表示为一个系数和一个指数的乘积的形式

这种表示方法主要用于较大或较小的数,能够方便地表示非常大或非常小的浮点数,例如物理学中的粒子质量、天文学中的星球质量等。

浮点数的科学计数法表示通常用如下形式表示:a × 10^b

  • a 表示浮点数的尾数(也称为系数或有效数字),必须在 1 到 10 之间
  • b 表示浮点数的指数,可以是任意整数

例:

  • 浮点数 12345678.9 可以表示为 1.23456789 × 10^7
  • 浮点数 0.00012345 可以表示为 1.2345 × 10^-4

在 C 语言中,浮点数也可以使用科学计数法表示,例如:

  1. float f1 = 1.23456789e7; // 科学计数法表示 12345678.9
  2. float f2 = 1.2345e-4; // 科学计数法表示 0.00012345

注意:

  • 在使用浮点数时,应该注意数值的精度问题,避免出现舍入误差等问题
  • 注意指数的符号和位数,避免出现数值超出浮点数的范围而导致的溢出或下溢问题

字符常量

image.png

  • 在 C 语言中,字符常量是一种特殊的常量,用来表示单个字符或转义字符
  • 字符常量必须用单引号'括起来
  • 字符的值必须在字符集中有对应的编码
  • 字符常量和字符型变量在内存中都是以 ASCII 码形式存储的,在进行字符常量的赋值和比较时,可以使用单引号括起来的字符,也可以使用对应的 ASCII 码值,例如 ‘A’ 和 65 在赋值和比较上是等价的

常见的字符常量:

  • 单个字符常量
    • 例如:'A''b''5' ……
    • 注意:字符常量必须用 单引号 括起来,而不是双引号,双引号表示字符串常量
  • 转义字符常量
    • 例如:
      • '\n'(换行符)
      • '\t'(制表符)
      • '\r'(回车符)
      • '\b'(退格符)
      • '\0'(字符串结束符)
      • ……
    • 转义字符常量用于表示不能直接输入的控制字符,需要使用反斜杠 \ 来表示

例如:

  1. char c1 = 'A'; // 单个字符常量
  2. char c2 = '\n'; // 换行符
  3. char c3 = '\t'; // 制表符
  4. char c4 = '\0'; // 字符串结束符

转义字符

image.png

  • 在 C 语言中,转义字符是一些特殊的字符,它们 以反斜杠 **\** 开头,用于表示一些不能直接输入的控制字符
  • 转义字符只能用于字符和字符串中,不能用于数字和其他数据类型
  • 在使用时,需要注意字符集的编码方式,因为不同的字符集可能有不同的转义字符

常见的转义字符包括:

  • \':单引号
  • \":双引号
  • \\:反斜杠
  • \n:换行符
  • \t:制表符
  • \r:回车符
  • \b:退格符
  • \f:换页符
  • \v:垂直制表符
  • \0:空字符,也叫字符串结束符

如果需要在字符串中输入反斜杠字符、单引号或双引号等字符,也需要使用转义字符进行表示:

  1. char c1 = '\\'; // 反斜杠
  2. char c2 = '\''; // 单引号
  3. char c3 = '\"'; // 双引号

转义字符还可以用于表示一些不能直接输入的控制字符,例如制表符和换行符:

  1. printf("Name\tAge\tGender\n"); // 使用制表符对齐输出
  2. printf("Tom\t18\tMale\n");
  3. printf("Lucy\t20\tFemale\n");
  4. /* output:
  5. Name Age Gender
  6. Tom 18 Male
  7. Lucy 20 Female
  8. */

字符串常量

image.png

  • 在 C 语言中,字符串常量是由一系列字符组成的常量序列,以空字符 **'\0'** 结尾
  • 字符串常量必须用双引号 " 括起来:

    1. char str1[] = "Hello, world!"; // 字符串常量
  • 在内存中,字符串常量会被存储为字符数组,其中每个字符占用一个字节,字符串的最后一个字符为空字符

  • 例如,上述字符串常量 "Hello, world!" 的内存表示如下:

    1. +---+---+---+---+---+---+---+---+---+---+---+---+---+---+
    2. | H | e | l | l | o | , | | w | o | r | l | d | ! | 0 |
    3. +---+---+---+---+---+---+---+---+---+---+---+---+---+---+
    4. | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10| 11| 12| 13|
  • 字符串常量是不可修改的

  • 在 C 语言中,如果需要修改字符串中的某个字符,必须将字符串常量复制到字符数组中,并修改字符数组中的元素,例如:

    1. char str2[] = "Hello, world!"; // 字符数组
    2. str2[7] = '-'; // 修改字符数组中的元素
    3. printf("%s\n", str2); // 输出修改后的字符数组 => Hello, -orld!
  • 在定义字符数组时,如果没有指定数组大小,则编译器会根据字符串常量的长度自动计算数组大小

  • 例如,上述字符数组的大小为 13,即字符串中的字符数(不包括空字符)

符号常量

image.png

符号常量(Symbolic Constants)就是用宏定义定义的常量,也称为宏常量(Macro Constants)。

  1. #include <stdio.h>
  2. #define PI 3.1415926 // 定义符合常量 PI
  3. int main() {
  4. int r = 5;
  5. double area = PI * r * r; // 使用符合常量 PI
  6. printf("半径为 5 的圆面积:%.2f\n", area); // => 半径为 5 的圆面积:78.54
  7. return 0;
  8. }

变量

image.png

  • 变量是一种用于 存储数据的占位符
  • 变量是在程序执行过程中,其值可以改变的量
  • 变量是程序中最基本的存储单元之一,用于在程序执行期间存储和操作各种类型的数据
    • 变量可以存储不同类型的数据,如整型、字符型、浮点型等,这些数据可以是用户输入的数据、程序计算的结果或者是程序中的常量
  • 使用变量可以提高程序的灵活性和可维护性,使程序能够更好地适应不同的需求
  • 变量必须先定义后使用。即:先告诉编译器这个变量的类型和名称,然后才能在程序中使用这个变量
  • 定义变量时,需要明确变量的数据类型和变量的名称

定义变量

变量定义的语法如下:

  1. type variable_name;

type 表示变量的类型,可以是整型、字符型、浮点型、指针类型等
variable_name 表示变量的名称,可以是由字母、数字、下划线组成的任意字符串,但是不能以数字开头

  1. int num; // 定义了一个整型变量 num
  2. char ch; // 定义了一个字符型变量 ch

变量定义后,编译器就对为其分配相应的存储空间,用于存储变量的值。

变量的初始化

image.png

变量的初始化,也叫给变量赋值。

可以使用赋值语句为变量赋初值,赋值语句的语法如下:

  1. variable_name = value;

variable_name 表示要赋值的变量名称
value 表示要赋给变量的值

  1. int num; // 定义了一个整型变量 num
  2. num = 10; // 将其赋值为 10

在程序中,可以多次给变量赋值,每次赋值可以改变变量的值

  1. int num;
  2. num = 10; // 第一次赋值为 10
  3. num = 20; // 第二次赋值为 20,改变了变量的值

注意:在使用变量之前,一定要先为其赋初值
如果在程序中使用一个未初始化的变量,其值是不确定的,可能是一个随机的值,也可能是一个未定义的值。

变量定义之后,在读变量的值之前,一定要进行初始化

使用变量的步骤:

  1. 定义:变量从无到有
  2. 初始化:变量从没有值到有值
  3. 使用:读写变量的值

使用(读)未初始化的变量存在的问题:

  • 如果在程序中使用了未初始化的变量,可能会出现奇怪的错误,导致程序无法正常运行
  • 使用未初始化的变量会导致程序的行为不可预测,因为未初始化的变量的值是不确定的,可能是随机的垃圾值,也可能是某些编译器默认的值
  • 未初始化的变量也可能会引发安全漏洞。例如,如果一个未初始化的指针变量被错误地用作内存地址,可能会导致程序崩溃或者执行恶意代码。

小结:在编写程序时应该尽可能地避免使用未初始化的变量,可以使用赋初值的方式或者在定义变量时进行初始化。

变量地址

  • 含义:变量地址是指变量在计算机内存中存储的位置
  • 唯一性:每个变量在内存中都有一个 唯一 的地址,用于标识变量在内存中的位置
  • 获取变量地址:可以使用取地址符 & 来获取变量的地址

变量地址的应用

  1. #include "stdio.h"
  2. int main() {
  3. int num = 10;
  4. int* p = &num; // 获取变量 num 的地址,并将其赋给指针变量 p
  5. printf("%d", *p); // 输出变量 num 的值,结果为 10
  6. return 0;
  7. }

int *p = &num; 变量 num 的地址被赋值给了指针变量 p,p 就可以用来访问变量 num 在内存中的值
printf("%d", *p); 通过指针 p 来访问变量 num 的值,需要使用解引用符 * 来获取指针所指向的变量的值。

  1. #include "stdio.h"
  2. void swap(int* a, int* b) {
  3. int temp = *a;
  4. *a = *b;
  5. *b = temp;
  6. }
  7. int main() {
  8. int x = 10, y = 20;
  9. printf("Before swap: x = %d, y = %d\n", x, y);
  10. swap(&x, &y); // 传递变量的地址
  11. printf("After swap: x = %d, y = %d\n", x, y);
  12. return 0;
  13. }
  14. /* 运行结果:
  15. Before swap: x = 10, y = 20
  16. After swap: x = 20, y = 10
  17. */

在上面的代码中,函数 swap 通过传递指针来交换变量 x 和 y 的值。

变量地址还可以用于在函数之间传递参数。在 C 语言中,函数的参数传递是通过值传递的,也就是说,函数的参数实际上是参数值的副本,而不是原始变量本身。但是,可以通过传递指针来实现对原始变量的修改

补充:

  • 变量地址的概念在指针的使用中尤为重要。
  • 指针是一个变量,它存储的是另一个变量的地址,可以通过指针来访问变量的值。
  • 有关指针的更多内容,会在 5. 指针 中详细介绍,上述程序若无法理解,暂不要紧。