【C语言基础】Chap. 5. 语句基础

1,152 阅读9分钟

前言1:此专栏为本人的C语言学习记录整理。如果潦草的学习记录为第1版的话,这份整理和重构后的笔记就是第2版了。按道理这份笔记只是给我自己看的,但如果有除了我以外的其他读者(比如您),甚至您能在这里有所收获的话,那真是万分荣幸。

前言2:此版内容来自B站Up主鹏哥C语言的《C语言:从入门到精通》、K. N. King的《C语言程序设计:现代方法》以及本人的补充和拓展。


1. 各类语句

C语言是结构化的程序设计语言,而结构化指的是现实世界事物发展逻辑的抽象,即顺序结构、选择结构和循环结构。

C语言的大多数语句属于以下3大类。

  • 分支语句

  • 循环语句

  • 跳转语句

此外还有复合语句(compound statement,把几条语句组合成一条语句)和空语句(void statement,不执行任何操作)。

学习这些语句之前需要了解是语句什么。

语句是程序运行时执行的命令。C语言规定每条语句都要以分号结尾(但也存在少许例外),因为语句可以连续占用多行,需要用分号来确定其结束位置——而指令通常只占用一行,因此不需要分号结尾。

因此可以简单理解为:在C语言中,由一个分号;隔开的就是一条语句。

2. 分支语句/选择语句

在一组可选项中选择一条特定的执行路径的语句,称为分支语句/选择语句(selection statement)。

2.1 if语句

语法结构如下:

第一种:
if(表达式)
    语句;

第二种:
if(表达式)
    语句1;
else
    语句2;

第三种(多分支/级联式):
if(表达式1)
    语句1;
else if(表达式2)
    语句2;
else
    语句2;

当表达式结果为真时,其对应的分支就会执行。

那么C语言中如何表达真假呢?在C89规范中,非0就是真(包括-1等负数),0就是假。

(C99中提供了_Bool型和能够提供bool宏的头<stdbool.h>,但由于本文主要讲C89规范,因此先不进行讲解。实际上,C99中的truefalse也只是使用了无符号整型的10而已。)

因此括号中的表达式可以使用各种语句,包括关系操作符等来计算。

补充1

  • 级联式if语句不是新的语句类型,而是普通的if语句,只不过碰巧有另外一条if语句作为else下的子句。

  • 如果要一个分支下要执行多条语句,则应该使用代码块{}来组成复合语句。

注意:代码块{}后不需要加分号;

补充2:悬空else问题

int a = 0;
int b = 2;
if (a == 1)
    if (b == 2)
        printf("yes\n");
else
    printf("no\n");

上面这段代码的else对应的是哪个if呢?

C语言遵循的规则是,else子句应该属于离它最近的且还未和其他else匹配的if语句。

因此这里的else会被判断为第二个if语句的分支。

这表明,想要使分支语句有清晰的逻辑和结构,则最好加上代码块{}

另外,某些if...else...表达式可以被条件表达式(三元操作符)替代。

2.2 switch语句

2.2.1 介绍

switch也是一种分支语句,常常用于多分支的情况,比如将表达式和一系列值进行比较,从中找出当前匹配的值。

其结构为:

switch(整型表达式) //注意,这里必须是 整型,int long 甚至 char 都行,但 float 不可以
{
    语句项;
}

语句项里是一些case语句,如下:

case 整型常量表达式:
    语句;

但在switch语句无法直接实现分支,搭配break使用才能真正实现分支效果,即: C

switch(整型表达式) //控制表达式
{
    case 整型常量表达式: //分支标号,注意,这里必须是 整型常量
        语句;
        break;
}

补充:整型常量表达式可以使用char的原因是字符在底层存储时使用的是ACII码值。

switch语句什么时候能用到呢?

举个例子,假设我们需要输出一周的每一天,此时用级联式if语句来书写过于复杂,因此可以选择使用结构清晰明了的switch语句:

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;
}

而且,switch语句还可以有如下的特殊写法,将多个分支标号放置在同一组语句的前面:

switch (day)
{
    case 1:
    case 2:
    case 3:
    case 4:
    case 5:
        printf("工作日\n");
        break;
    case 6:
    case 7:
        printf("休息日\n");
        break;
}

甚至可以写成这样:

switch (day)
{
    case 1: case 2: case 3: case 4: case 5:
        printf("工作日\n");
        break;
    case 6: case 7: printf("休息日\n");
                    break;
}

因为非强制要求的语法格式并不影响C语言本身的编译,非强制要求的格式只是一种代码风格。

当表达的值与所有case标签的值都不匹配,则会跳过所有语句,但如果不想忽略这部分不匹配标签的表达式的值时,则可以在列表中增加一条default子句(可选)。

default子句可以写在任何一个case标签可以出现的位置,当switch表达式的值不匹配所有case标签的值时,default子句后面的语句就会执行,所以每个switch语句中只能出现一条default子句。

C语言不允许有重复的分支标号,但对分支的顺序没有要求,因此default语句可以出现在语句列表的任何位置,而且语句流会像贯穿一个case标签一样贯穿default子句。

但习惯上一般将default子句放在最后:

switch(整型表达式)
{
    case 整型常量表达式1:
        语句1;
        break;
    case 整型常量表达式2:
        语句2;
        break;
    defalut:
        语句3;
        break;
}

补充

  • switch允许嵌套使用。

  • 为了防止误写类似if (a = 1)这样的语句,可以写为if (1 == a)

  • switch语句除了比级联式if语句易读之外,往往比后者执行速度更快,特别是在有许多情况要判定的情况下。

2.2.2 break语句的作用

switch语句中需要break语句的原因是,switch语句实际上是一种“基于计算的跳转”。对控制表达式求值时,控制会跳转到与switch表达式的值相匹配的分支标号处。

而分支标号只是一个说明switch内部位置的标记——这种直接的跳转也是switch往往比级联if更快的原因,因为不用作正确分支之前可能存在的多次判断。

执行完正确分支中的语句后,如果没有break来使流程跳出switch语句,那么就会自动执行下一个分支而无视分支标号是否符合控制表达式里的判断,从而可能导致程序出错。

这就是break语句被需要的原因。

:最后一个分支理论上不需要break语句,但为了防止将来自己或他人追加分支数目时出现问题,还是写上为好。

3. 循环语句/重复语句

重复执行一条或一组语句的语句,称为循环语句/重复语句(loop statement)。

循环语句一般包含控制表达式(controlling expression)和循环体(loop body)。前者用于判断是否继续循环,后者是被循环的内容。

3.1 while

当条件满足的情况下,if语句后的语句会执行,但只执行一次。但有些时候需要程序执行多次,此时就可以使用while语句。

其语法结构如下:

while(表达式) //控制表达式
    循环语句; //循环体

举个例子,当我们需要打印1到10的数字时,可以这样做:

int i = 1; //初始化
while (i <= 10) //判断部分
//当i增加到11时,判断表达式将计算为0(假),判断不成立,不再进入循环
{
    printf("%d\n", i );
    i++; //调整部分
}

同样的,我们随时可以用break终止整个循环,另外也可以用continue跳过本轮循环:

int i = 1;
while (i <= 10)
{
    if (9 == i)
    {
        i++;
        break;
    }
    if (5 == i)
    {
        i++;
        continue;
    }
    printf("%d\n", i); //1 2 3 4 6 7 8
    i++;
    //上面两行可以结合,写为 printf("%d\n", i++);
}

:当循环体只有一条语句时,跟if语句一样,可以将花括号{}去掉。

补充1

  • getchar():用于从流中或标准输入(键盘)中读取一个字符,并返回读到的字符,如果读到错误或者文件结束,则返回一个EOF(end of file,是文件的结束标志,-1)。
    • 比如输入一个A,则相当于把A\n放入缓冲区中。
    • 输入ctrl + z时getchar读取到结束。
    • getchar读取的实际上是ASCII码值(或EOF),所以读取到的字符可以有类型。
  • putchar():输出一个传入的字符。

使用示例:

int ch = 0;
while ((ch = getchar()) != EOF)
putchar(ch); //输出每一个输入的字符,其实就是循环读取然后输出

补充2getchar应用场景,需要用户输入和确认密码:

char password[20] = { 0 };
printf("请输入密码:>");
scanf("%s", password); //这里不用&取地址的原因是数组中存储的原本就是一系列地址
printf("请确认密码(Y/N):>");

//清理缓冲区中的多个字符,直到缓冲区为空
int tmp = 0;
while ((tmp = getchar()) != '\n')
{
    ;
}

int ch = getchar();
if ('Y' == ch)
{
    printf("确认成功\n");
}
else
{
    printf("确认失败\n");
}

补充3:应用场景,只打印数字

int ch = 0;
while ((ch = getchar()) != EOF)
{
    if (ch < '0' || ch > '9')
        continue;
    putchar(ch);
}

3.2 do while

do语句和while语句本质上是相同的,只不过前者的控制表达式是在每次循环玩循环体之后进行判定的。

其语法结构为:

do
    循环语句;
while(表达式);
int i = 1;
do
{
    printf("%d ", i);
    i++;
} while (i > 1 && i < 10);

不过do...while适用场景有限,较少被使用。

此外,breakcontinue同样可以在其中使用。

3.3 for

for语句(也可以叫for循环)是最被频繁使用的一种循环,虽然结构上比前两种循环多了些条件,但也因此变得十分灵活,可读性也更高。

其语法结构为:

for(表达式1; 表达式2; 表达式3)
    循环语句;

表达式1为初始化部分,用于初始化循环变量;表达式2为条件判断部分,用于判断循环什么时候终止;表达式3为调整部分,用于循环条件的调整。

示例:打印数字1-10

int i = 0;
for (i = 1; i <= 10; i++)
    printf("%d ", i); //由于循环体只有一条,可以省去花括号

相比while循环,for循环将初始化、终止判断和条件调整都集合到一起,易于阅读和修改。

此外,breakcontinue也可以在for循环中使用,效果基本相同,但for中的continue会自动跳到调整部分,而while里则没有这种功能。

建议

  1. 不在for循环体内修改循环变量,防止for循环失去控制;

  2. for语句的循环控制变量的取值采用“前闭后开区间”写法,类似i < 10

补充for循环的变种

//1. 三个部分可以任意省略,但判断部分省略后恒为真,会陷入死循环
for (;;) //注意,语句可以省略,但分号不能省略
    printf("...\n");

//2. 可以有多个控制变量和多个判断条件
int x, y;
for(x = 0, y = 0; x < 2 && y < 5; ++x, y++)
    printf("hallo\n");

4. 跳转语句

已经提到过的breakcontinuereturn就是跳转语句(jump statement),它们导致无条件地跳转到程序中的某个位置。

在循环语句中添加跳转语句的作用是,程序员可以将程序设计为已经满足某种条件的情况下自动跳出循环。 除了以上三个语句外,C语言中还提供了可以随意滥用的goto语句和标记跳转的标号。

语法结构:

//标号语句
标识符: 
    语句;

//goto语句
goto 标识符;

理论上goto语句是没有必要使用的,实践中没有goto语句也可以很容易地写出代码:

flag: //程序会跳回此处重新开始运行
    printf("a\n");
    printf("b\n");
    goto flag; //跳转

示例:关机程序,程序运行后,电脑就在1min内关机,如果输入停止,则取消关机。

//命令行cmd关机
// shutdown -s -t 60 60s后关机
// shutdown -a 停止关机

#include <string.h>
int main()
{
	//关机
	//C语言提供了一个函数:system() -执行系统命令的函数
	char input[20] = { 0 }; //存放输入的信息
	system("shutdown -s -t 60");

again:
    printf("请注意,电脑将在1分钟内关机,如果输入「停止」,则取消关机\n");
    scanf("%s", input);
    if (strcmp(input, "停止") == 0)
    {
        system("shutdown -a");
    }
    else
    {
        goto again;
    }
    return 0;
}

去掉goto改造版:

#include <string.h>
int main()
{
    char input[20] = { 0 }; //存放输入的信息
    system("shutdown -s -t 60");
    
    while (1)
    {
        printf("请注意,电脑将在1分钟内关机,如果输入「停止」,则取消关机\n");
        scanf("%s", input);
        if (strcmp(input, "停止") == 0)
        {
            system("shutdown -a");
            break;
        }
    }
    return 0;
}

但某些场合下goto语句也能起到作用,最常见的用法就是终止程序在某些深度嵌套的结构的处理过程,例如一次跳出两层或多层循环。

此时break的效果就不如goto,因为它只能从最内层循环推出到上一层的循环。

例如:

for(...)
{
    for(...)
    {
        for(...)
        {
            if(disaster)
                goto error;
        }
    }
}

error:
    if(disaster) //处理错误情况

注意goto语句只能在一个函数范围内跳转,不能跨函数。

以下情况是错误的:

void test()
{
    flag:
        printf("test\n");
}

int main()
{
    goto flag;
    return 0;
}