杂七杂八补充的东西 | Miscellaneous
一、Switch语句
我们在分支结构这一节,介绍了if-else结构及其嵌套。但我们常常碰到这样一些情况:当一个变量等于某个值A时,执行某段代码;当它等于值B时,执行另一段代码,当它等于值C时,执行又一段代码;...。以此类推。如果我们用此前学的if-else语句来实现,代码大致长这样:
if (var == 1) {
// 执行1的代码
} else if (var == 2) {
// 执行2的代码
} else if (var == 3) {
// 执行3的代码
} else if (var == 4) {
// 执行4的代码
}
虽然通过if-else结构的嵌套,可以实现这样的功能,但代码多少看起来有点冗长,可读性不强。为了解决这个问题,C语言提供了switch语句。
switch语句是分支结构的一种,它的语法结构如下:
switch (expression) {
case constant1:
// 当expression等于constant1时执行的代码
break;
case constant2:
// 当expression等于constant2时执行的代码
break;
// 可以有任意数量的case语句
default:
// default部分并不是必须存在的。
// 如果expression不等于任何case中的常量,执行这里的代码
// 如果没有default语句,而expression又没有任何匹配,则switch语句不会执行任何操作。
}
switch语句的工作原理是:计算expression的值,然后将其与每个case标签后的常量进行比较。如果找到匹配的常量,就执行对应的代码块,直到遇到break语句为止。如果没有任何case匹配,则执行default块中的代码(如果有的话)。
例如,这个输入今天星期几的程序,可以用switch语句来实现:
#include <stdio.h>
int main() {
int day;
printf("请输入今天是星期几(1-7):");
scanf("%d", &day);
switch (day) {
case 1:
printf("今天是星期一\n");
break;
case 2:
printf("今天是星期二\n");
break;
case 3:
printf("今天是星期三\n");
break;
case 4:
printf("今天是星期四\n");
break;
case 5:
printf("今天是星期五\n");
break;
case 6:
printf("今天是星期六\n");
break;
case 7:
printf("今天是星期日\n");
break;
default:
printf("输入无效,请输入1到7之间的数字。\n");
}
return 0;
}
二、goto语句
goto语句是一种无条件跳转语句,可以让程序直接跳转到指定的标签位置。我们通过一个简单的例子来说明goto语句的用法:
#include <stdio.h>
int main() {
printf("这是句子1\n");
goto skip; // 跳过下面的代码,直接跳到标签skip处
printf("这是句子2\n"); // 这行代码将被跳过
skip: // 标签skip位置
printf("这是句子3\n");
return 0;
}
C语言的goto语句是和标签结合起来用的。在这个例子中,程序首先打印"这是句子1",然后遇到goto skip;语句,程序会跳过"这是句子2"的打印,直接跳到标签skip:处,打印"这是句子3"。
值得说明的是,不同于变量使用必须在声明之后,标签可以出现在goto语句之前或之后。这是因为它们在编译时会被编译器解析,而不是像变量声明那样涉及到内存空间的分配。标签的命名规则和变量类似,但通常建议使用有意义的名字以提高代码可读性。
相当重要的是,goto语句虽然可以简化某些跳转逻辑,但过度使用会导致代码极度难以理解和维护、增加逻辑错误的风险。此外,goto语句可能会导致无法预料的行为,例如跳过变量声明而直接使用变量等等。因此,建议在编写代码时尽量避免使用goto,除非在某些特定情况下(如错误处理)确实需要它。
这已经成为主流编程规范的共识:
"The goto statement should not be used."(不应使用
goto语句。) --MISRA C:2012 Rule 15.1
"Finish every set of statements with a break or return rather than a goto."(应使用break或return结束语句块,而不是goto。)--CERT C - MSC17-C
三、常量与const关键字
我们早在1.2时就学习了变量的声明。而与变量相对的是常量。与变量在程序运行过程中,值常常发生改变不同,常量的基本特征是在程序运行时不可改变的量。
常量通常指字面常量、常变量,有时也包含宏常量。
1.字面常量
字面常量(Literal Constants)是指在代码中直接写出的固定值,比如数字、字符或字符串。它们的值在程序运行期间不会改变。例如:
b = a + 5; // 这里的5就是字面常量。它是在源码中"写好"的,不会改变。
又如:
int a = 100; // a是一个整型变量,而右边的100是常量。
// 这句赋值语句是把常量100的值"赋"给变量a,使得它的值成为100。
// a的值可以再变化,但写好的100就是100。
字面常量也有它们对应数据类型的概念,和变量的类型一一对应。例如常见的字面常量类型有:
- 整型常量:如
10,-5,0 - 浮点型常量:如
3.14,-0.001,2.0 - 字符常量:如
'A','b','\n' - 字符串常量:如
"Hello","C语言"
这些常量固定地"写好"在代码中,编译后其值不会改变。
2.常变量
有时,为了避免在多处重复书写某个数值(如圆周率3.1415926),我们会声明一个变量来简化代码,并便于统一修改(例如改为3.14)。
例如:
float pi = 3.1415926;
不难发现,pi这样的值,在程序执行过程中通常应该不被改变。并且要尽量避免因为内存访问错误的原因,被危险地、不知情地修改。为此,仿照字面常量的特征,C语言提供const关键字,可以将一个变量声明为常变量。
常变量的语法格式为:
const 数据类型 变量名 = 初始值;
例如:
const float pi = 3.1415926;
const int max_students = 100;
const char grade = 'A';
用const修饰的变量具有以下特点:
- 必须在声明时初始化
- 一旦初始化后,值被严格地保护起来,不能被修改
- 如果试图修改常变量的值,编译器会报错
const int num = 10;
num = 20; // 编译错误:不能修改常变量的值
这样一来,对于一些我们不希望被改变的量,我们可以通过定义常变量将它们更好地“保护起来”。
值得说明的是,常变量与字面常量具有一个重要区别:常变量与变量一样,会在内存中分配一块空间存储它的值,而字面常量则会在编译层面直接处理。
3.宏常量
除了字面常量和常变量,C语言还提供了宏常量的概念。使用#define预处理指令可以定义宏常量:
#define PI 3.1415926
#define MAX_SIZE 1000
宏常量在预处理阶段会被直接替换,相当于文本替换。它们在习惯上,通常使用全大写字母命名以区别于变量。
#include <stdio.h>
#define PI 3.1415926
int main() {
double radius = 5.0;
double area = PI * radius * radius;
printf("圆的面积:%.2f\n", area);
return 0;
}
由于宏常量文本替换的特性,这样一段啼笑皆非的代码是可以被编译通过的:
#define 喵喵喵 main
#define 喵喵 int
#define 喵 (
#define 喵喵呜 )
#define 喵喵喵喵 {
#define 喵呜喵 }
#define 呜 printf
#define 呜呜 "喵喵喵!"
#define 呜呜呜 return
#define 呜喵 0
#define 喵呜 ;
#include <stdio.h>
喵喵 喵喵喵 喵 喵喵呜
喵喵喵喵
呜 喵 呜呜 喵喵呜 喵呜
呜呜呜 呜喵 喵呜
喵呜喵
运行结果是输出喵喵喵!。
由于宏定义”文本替换“的特性,宏常量和字面常量一样,都是在编译层面处理,而不像常变量涉及到内存空间的分配。
三种常量各有千秋,而合理使用常量是良好的编程习惯。
四、三元运算符
三元运算符(Ternary Operator)是C语言中一种简洁的条件表达式,也被称为条件运算符。它是唯一一个需要三个操作数的运算符,语法格式为:
条件表达式 ? 表达式1 : 表达式2
工作原理:
- 如果条件表达式为真(非零),则整个三元表达式等于表达式1的值
- 如果条件表达式为假(零),则整个三元表达式等于表达式2的值
例如,以下代码使用三元运算符找出两个数中的最大值:
#include <stdio.h>
int main() {
int a = 10, b = 20;
int max = (a > b) ? a : b;
printf("最大值是:%d\n", max);
return 0;
}
这等价于使用if-else语句:
int max;
if (a > b) {
max = a;
} else {
max = b;
}
三元运算符的优势在于:
- 简洁性:一行代码就能完成简单的条件判断
- 表达式性质(重要!):可以直接用在赋值、函数参数等需要表达式的地方
更多应用示例:
// 判断奇偶数
int num = 15;
printf("%d是%s数\n", num, (num % 2 == 0) ? "偶" : "奇");
// 绝对值计算
int x = -5;
int abs_x = (x >= 0) ? x : -x;
// 三元运算符的嵌套(不推荐过度使用)
int score = 85;
char grade = (score >= 90) ? 'A' : (score >= 80) ? 'B' : 'C';
需要注意的是,虽然三元运算符可以使代码更加简洁,但过度使用或嵌套使用会降低代码的可读性。建议在简单的条件判断中使用,复杂的逻辑仍应使用if-else语句。
五、typedef关键字
在C语言中,我们已经熟悉一些常见的数据类型,如int、float、char等。但从字面上,它们只含有“整数”、“小数”、“字符”的意思。
考虑这样一个例子,我们用一个八位整数来存储某一天的日期。例如20250901、20251225等。虽然我们可以直接使用int类型来存储这些值,但从代码可读性的角度来看,直接使用int并不能清晰地表达出这个变量的含义。
为了解决这个问题,我们可以使用typedef关键字为这种特定含义的类型定义一个新的名称。例如:
typedef int Date; // 定义一个新的类型Date,实际上是int的别名
这样,我们就可以用Date来表示日期类型,而不是使用int,即使它本质上还是int。这使得代码更加清晰易懂。
Date today = 20250901;
typedef的语法格式为:
typedef 原有类型 新类型名;
这样,我们就对C语言原有的类型进行了重新命名。typedef并不会创建新的数据类型,它只是为现有类型创建一个新的名字。
typedef有利有弊:
- 优点:
- 提高代码可读性:通过有意义的类型名,代码更容易理解。
- 便于维护:如果需要更改底层类型,只需修改
typedef定义即可。 - 简化复杂类型的使用:对于后续要学习的结构体、指针等复杂类型,
typedef可以简化声明。
- 缺点:
- 可能增加理解难度:在代码中突然出现用
typedef定义的类型,需要查找其定义,看看到底是什么基本数据类型,增加理解成本。
- 可能增加理解难度:在代码中突然出现用
六、显式与隐式类型转换
在C语言中,类型转换是指将一种数据类型的值转换为另一种数据类型的过程。类型转换分为两种:隐式类型转换和显式类型转换。
1.隐式类型转换
隐式类型转换(Implicit Type Conversion),也称为自动类型转换,是指编译器在表达式中自动进行的类型转换。当不同类型的数据参与运算时,编译器会根据一定的规则将它们转换为相同的类型,以确保运算的正确性。
例如:
int a = 5;
float b = 2.5;
float c = a + b; // 隐式类型转换,将a中本是int类型的5,在计算时自动转换为float类型5.0,然后与b相加。
又例如:
char ch = 'A'; // 字符'A'的ASCII值是65
int ascii_value = ch; // 隐式类型转换,将char类型的ch转换为int类型65
2.显式类型转换
显式类型转换(Explicit Type Conversion),也称为强制类型转换,是指程序员通过类型转换运算符,明确地将一种数据类型转换为另一种数据类型。显式类型转换的语法格式为:
(目标类型) 表达式
例如:
int a = 10;
int b = 3;
float c = (float)a / (float)b; // 显式类型转换,(float)a和(float)b都看作float类型计算
七、一些系统函数
C语言标准库(头文件)提供了许多有用的系统函数,帮助我们完成各种任务。以下是一些常用的系统函数:
1. 标准输入输出库(stdio.h)
在前面的章节中,我们已经多次使用了printf和scanf函数,它们都是标准输入输出库中的函数。这个库还包含其他一些有用的函数:
getchar(void);:从标准输入读取一个字符putchar(int char);:向标准输出写入一个字符puts(const char *str);:向标准输出写入一个字符串,并在末尾自动添加换行符gets(char *str);:从标准输入读取一行字符串(不推荐使用,存在读取错误的风险),已被C11标准移除。
2. 数学函数库(math.h)
数学函数库提供了各种数学运算函数,如:
sqrt(double x);:计算平方根pow(double base, double exp);:计算幂sin(double x);:计算正弦值cos(double x);:计算余弦值tan(double x);:计算正切值log(double x);:计算自然对数exp(double x);:计算e的x次幂fabs(double x);:计算绝对值ceil(double x);:向上取整floor(double x);:向下取整round(double x);:四舍五入fmod(double x, double y);:计算x除以y的余数
值得说明的是:
- 使用这些函数时,需要包含头文件
#include <math.h>。 math.h库中的函数大多以double作为返回类型,特别地,在上面列出的所有函数返回值均是double类型。即使像向上取整、向下取整这样的函数,返回值也是double类型。
3. 时间库(time.h)
时间库提供了处理时间和日期的函数,如:
time(NULL);:获取当前时间(自1970年1月1日以来的秒数)
4. 标准常用库(stdlib.h)
标准常用库提供了一些常用的实用函数,如:
abs(int x);:计算整数的绝对值rand();:生成一个随机整数srand(unsigned int seed);:设置随机数种子exit(int status);:终止程序执行,并返回值status给操作系统。类似于在main函数中使用return status;语句。
这里值得说明的是,rand()函数生成的随机数是伪随机数,原理是从一串足够长的“随机”序列中顺序读数。
随机数序列取决于 “种子值”(Seed),如果不加处理,这个值默认为1,也就是每次运行程序时,这个“随机”序列都是相同的。
例如,可能第一次运行一个程序,这个程序调用三次,rand()函数,生成了三个随机数,如31、293、5;第二次运行这个程序,生成的随机数序列没有变,因此调用rand()函数得到的伪随机数序列还是一样为31、293、5、...。
为了获得不同的随机数序列,可以使用srand()函数设置不同的“种子”值。通常,我们会用当前时间来设置种子值,因为时间总是在变化的。
例如:
#include
#include
#include
int main() {
srand((unsigned int)time(NULL)); // 用当前时间作为随机数种子
for (int i = 0; i < 5; i++) {
int random_num = rand(); // 生成随机数
printf("%d\n", random_num);
}
return 0;
}



Comments | NOTHING