C语言系列4——C控制语句、函数


  利用C语言能够写出健壮、高效、多功能的程序,诀窍就是控制程序流。对于计算机科学而言,一门语言应该提供以下三种形式的程序流:

  • 执行语句序列;
  • 如果满足某些条件就重复执行语句序列(循环);
  • 通过测试选择执行哪一个语句序列(分支)。

1. 循环语句

1.1 for循环

  for语句的关键字是for,形式为:

for(initialize; test; update)
statement

for语句使用3个控制表达式控制循环过程,分别使用逗号隔开。initialize表达式在执行for语句之前只执行一次(通常用于初始化变量。C99允许在这一步声明变量,变量的作用域和生命期都限制在for循环中);然后对test表达式求值,如果表达式为真(非零),则执行循环一次(statement语句);接着对update表达式求值,并再次检查test表达式。for语句是一种入口条件循环语句,即在执行循环之前就决定是否执行循环。因此,for循环可能一次都不执行。statement部分可以是一条简单语句也可以是复合语句。如果是一条简单语句,可以不加花括号括起来。

1.2. while语句

  while语句的关键字是while,形式为:

while(expression)
statement

while语句在expression为假之前重复执行。while语句也是一种入口条件循环,因此循环可能一次也不执行。statement可以是一个简单语句也可以是复杂句。

1.3. do while语句

  do while语句的关键字是dowhile,形式为:

do
statement
while(expression);

do while语句创建的循环在expression为假或0之前重复执行循环体的内容。与for循环和while循环不同,do while语句是一种出口条件循环,即在执行完循环体之后才根据测试条件决定是否再次执行循环。因此,该循环至少执行一次。statement部分可以是简单语句或复合语句。
  那么如何选择这三种循环呢?可以从两个方面考虑:

  1. 确定需要入口循环还是出口循环。一般而言,在执行循环之前测试条件比较好,并且测试放在循环的开头,程序的可读性更高。在许多情况下,要求一开始不满足测试条件时就直接跳过整个循环。
  2. 当循环涉及初始化和更新变量时,用for循环比较合适,而在其它情况,while循环更好。当然for循环很灵活,如果省略for循环的第1个和第3个表达式,这时for循环看起来像while循环。

2.分支语句

  分支语句的作用是让程序根据测试条件执行相应的行为。

2.1. if语句

  if语句的关键词是ifelse,有三种组合形式:

1.形式1:

if(expression)
statement

如果expression为真,则执行statement部分;
2. 形式2:

if(expression)
statement1
else
statement2

如果expression为真,则执行statement1部分,否则,执行statement2部分;
3. 形式3:

if(expression1)
statement1
else if(expression2)
statement2
else
statement3

如果expression1为真,则执行statement1部分,如果expression2为真,执行statement2部分,否则,执行statement3部分。依此类推,可嵌套多个。if和else的搭配遵循“就近原则”,也就是说if和离它最近的else语句配套使用。

2.2. 多重选择的switch语句

  switch语句的关键字是switch,形式为:

switch(expression)
{
	case label1: statement1
	break;              //break语句跳出switch,如果没有,程序会往下执行,产生错误
	case label2: statement2
	break;
	default: statement3 //default语句可选
	break;
}

程序控制根据expression的值跳转至相应的case标签处。然后,程序流执行剩下的所有语句,除非执行到break语句进行重定向。expression和case标签都必须是整数值(包括char类型),标签必须是常量或完全由常量组成的表达式。如果没有case标签与expression的值匹配,控制则转至标有default的语句(如果有的话);否则,控制将转至紧跟在switch语句后面的语句。控制转至特定标签后,将执行switch语句中其后的所有语句,除非达到switch末尾,或执行到break语句。

3. 跳转语句

  跳转语句使程序流从一处跳转至另一处。

语句关键字注解(说明)
break 语句break所有的循环和switch语句都可以使用break语句,它使程序控制跳出当前循环或switch语句的剩余部分,并继续执行跟在循环或switch后面的语句
continue语句continue所有的循环语句都可以使用continue,但是switch语句不行。continue语句使程序控制跳出循环的剩余部分。对while和for循环而言,程序执行到continue会进入下一轮迭代,对于do while循环,对出口条件求值后,如有必要,进入下一轮迭代
goto语句goto使程序控制跳转至相应的标签语句,一般不建议使用,只需要知道表达的意思即可

补充:

  1. break语句和continue语句的区别在于break语句会使程序跳出当前循环,continue语句会使程序回到当前循环的测试条件:

break语句和continue语句都只能作用于当前循环,对嵌套的外层循环没有影响。
2. goto语句包含两部分:关键词goto和标签名,标签名遵循变量命名规则,如果有goto part1;,要想让它正常工作,函数还必须包含另一条标为part1的语句,该语句标签名后紧跟一个冒号part1: statement。一般goto语句的内容都可以使用更有条理更清晰的其它控制语句代替,而且当多种情况混在一起时,goto语句容易造成混乱,所以一般不推荐使用该语句。C语言的创始人也说过,goto语句易被滥用。

4. 函数

  函数(function)是完成特定任务的独立程序代码单元。C语言组织程序的设计思想是把函数用作构件块。前面已经使用过C标准库的函数,如printf()、scanf()、getchar()函数等,还介绍过C语言的主函数,现在来介绍怎么创建自己的函数。下面以一个例子来具体说明(程序来源于《C Primer Plus》中文版第六版第9章,书250面程序清单9.3 lesser.c):

/*找出两个整数中较小的一个*/
#include <stdio.h>

int imin(int x, int y); //函数原型

int main(void)
{
    int evil1, evil2;

    printf("Enter a pair of integers (q to quit): \n");
    while (scanf("%d %d", &evil1, &evil2) == 2)
    {
        printf("The lesser of %d and %d is %d.\n", evil1, evil2, imin(evil1, evil2)); //调用函数
        printf("Enter a apir of integers (q to quit): \n");
    }
    printf("Bye!\n");

    return 0;
}

int imin(int n, int m) //函数定义
{
    int min;

    if (n < m)
        min = n;
    else
        min = m;

    return min;
}

  在程序中一共在3处使用了imin标识符,从上往下看,第一处是声明函数原型,第二处是调用函数,第三处是定义函数。实际上,在C语言中使用函数,都要经过这三步。

4.1. 声明函数原型

  C语言中的函数都要声明才能使用,int imin(int, int);就是声明函数原型。圆括号表示imin是一个函数名。函数也和变量一样有多种类型,例子中int是函数类型,表明该函数返回一个int类型的值;imin是函数名;括号里面两个int表明接受两个int类型的参数。一般而言,函数原型指明了函数的返回类型和函数接受的参数类型,这些信息被称为函数签名。对imin()函数而言,其签名为函数的返回值为整数型(int),接受两个整型参数。
  圆括号中的x,y叫做形式参数(formal parameter),简称形参,这里并没有实际创建一个变量(没有分配内存,定义与声明的区别),x,y并没有实际意义,甚至可以省略:int imin(int, int);,只是告诉编译器该函数接受两个整型参数。如果没有形参,括号里应写void而不是空着不写。ANSI C要求每个形参前都要声明其类型,也就意味着不能使用同一类型的变量列表:int imin(int x,y);这样的声明是无效的。
  函数原型的声明也可以放在main()函数里面。以前声明函数原型还可以只在圆括号里列出参数,然后在后面声明类型:

int imin(x , y)
int x,y;

现在C标准逐渐废弃这种写法,不要这么写。遇到以前的代码能看懂就行。

4.2. 定义函数

  函数的定义从函数头开始:int imin(int n, int m)int是函数类型,imin是函数名,圆括号表示这是一个函数,int n, int m表示函数接受两个整型参数。可以看到,函数定义和函数原型的形参可以不同,再次说明形参没有实际意义。但是注意,声明函数原型末尾有分号,定义函数没有分号,而且定义函数时形参不能省略。函数名过后用花括号括起来的是函数体,描述函数的具体功能。关键字return后面的表达式的值就是函数的返回值,上面的例子中该函数的返回值就是变量min的值。返回值也可以用作表达式的一部分,也可以是任意表达式的值。imin()中的变量是该函数的局部变量,只在这个函数内部起作用,因此,即使变量名和主函数中的变量名相同,也不会引起冲突。

4.3. 函数调用

  声明且定义了函数,那么我们就可以调用函数做一些事情。第12行就是打印两个整数中较小的一个。其中imin(evil1, evil2)中的evil1,evil2称为实际参数,简称实参,顾名思义,实参就是在程序运行中有实际意义的、具体的值。形参是被调函数中的变量,实参是主调函数赋给被调函数的具体值。

4.4. 可变参数

  stdarg.h头文件为函数提供了接受可变参数的功能。但是必须按照以下步骤进行:

  1. 提供一个使用省略号的函数原型;
  2. 在函数定义中创建一个va_list类型的变量;
  3. 用宏把变量初始化为一个参数列表;
  4. 用宏访问参数列表
  5. 用宏完成清理工作。

这些函数原型都是有效的:

int imins(int x, ...);
void ch(const char *st ,int n , ...);

但是像这样的

int fun(char c1 ,... , char c2);
double imax(...)

声明是无效的,因为省略号必须放在最后,且最少有一个形参。最右边的形参(即省略号前面一个形参)起着特殊作用,标准中用parmN术语来描述这个形参。传递给该形参的实际参数是省略号部分代表的参数数量。例如可以这样调用前面的函数imins(2 ,40 ,39),或imins(4 ,8 ,90 ,56 ,78)等。
  接下来,声明在stdarg.h中的va_list类型代表一种用于储存形参对应的形参列表中省略号部分的数据对象。因此,变参函数的定义起始部分类似这样:

double sum(int lim, ...)
{
	va_list ap; //声明一个储存参数的对象

然后,该函数将使用定义在stdarg.h中的va_start()宏,把参数列表拷贝到va_list类型的变量中。该宏有两个参数:va_list类型的变量和parmN形参。所以照上面例子,可以这样调用:va_start(ap ,lim);,把ap初始化为参数列表。用宏va_arg()访问参数列表的内容,该宏接受两个参数:一个va_list类型的变量和一个类型名。第一次调用时,返回参数列表的第1项,第二次调用时,返回参数列表的第2项,以此类推。表示类型的参数指定了返回值的类型。最后使用va_end()完成清理工作。完整的示例如下(该程序来源于《C Primer Plus》第六版中文版第16章,书561面程序清单16.21 vargars.c):

/*使用变参函数*/
#include <stdio.h>
#include <stdarg.h>

double sum(int, ...); //声明函数原型,最后一项必须是省略号

int main(void)
{
    double s, t;

    s = sum(3, 1.1, 2.5, 13.3);                //第一个3是省略号代表的参数数量
    t = sum(6, 1.1, 2.1, 13.1, 4.1, 5.1, 6.1); //第一个6也是省略号代表的参数数量
    printf("return value for "
           " sum(3, 1.1, 2.5, 13.3):  %g\n ",
           s);
    printf("return value for "
           "sum(6, 1.1, 2.1, 13.1, 4.1, 5.1, 6.1):  %g\n",
           t);

    return 0;
}

double sum(int lim, ...)
{
    va_list ap; //声明对象存储参数
    double tot = 0;
    int i;

    va_start(ap, lim); //把ap初始化为参数列表
    for (i = 0; i < lim; i++)
        tot += va_arg(ap, double); //访问参数列表中的每一项
    va_end(ap);                    //清理工作

    return tot;
}

运行结果:

在这里插入图片描述

4.5. 递归

  递归(recursion)是函数自己调用自己,使用递归的难点在于结束递归,因为如果递归代码中没有终止递归的条件测试部分,一个调用自己的函数会无限递归。递归方案简洁,但是效率没有循环高。下面程序演示了递归(程序来源于《C Primer Plus》中文版第六版第9章,书257面程序爱清单9.6 recur.c):

/*演示递归*/
#include <stdio.h>

void up_and_down(int);

int main(void)
{
    up_and_down(1);

    return 0;
}

void up_and_down(int n)
{
    printf("Level %d: n location %p\n", n, &n); //#1
    if (n < 4)
        up_and_down(n + 1);
    printf("Level %d: n location %p\n", n, &n); //#2
}

运行结果如下:

  现在来分析下递归是如何工作的:首先,main()调用了带参数1的up_and_down()函数,执行结果是up_and_down()中的形参n,值是1,所以打印语句#1打印Level1。然后,由于n小于4,up_and_down()(第1级)调用实参为n+1(即2)的up_and_down()(第2级)。于是第2级调用中的n的值是2,打印语句#1打印Level2。与此类似,下面两次调用打印的分别是 Level3和Level4。当执行到第4级时,n的值是4,所以if测试条件为假。up_and_down()函数不再调用自己。第4级调用接着执行打印语句#2,即打印 Level4,因为n的值是4。此时,第4级调用结束,控制被传回它的主调函数(即第3级调用)。在第3级调用中,执行的最后一条语句是调用if语句中的第4级调用,被调函数(第4级调用)把控制返回在这个位置,因此,第3级调用继续执行后面的代码,打印语句#2打印Level3然后第3级调用结束,控制被传回第2级调用,接着打印 Level2,以此类推。注意,每级递归的变量n都属于本级递归私有。这从程序输出的地址值可以看出(当然,不同的系统表示的地址格式不同,这里关键要注意, Level1和Level1的地址相同, Level2和 Level2的地址相同,等等)。
  了解了递归的执行过程,这里总结几个要点:

  1. 每级函数调用都有自己的变量。也就是说,第1级的n和第2级的n不同,所以程序创建了4个单独的变量,每个变量名都是n,但是它们的值各不相同;
  2. 每次函数调用都会返回一次。当函数执行完毕后,控制权将会被传回上一级递归,程序必须按照顺序逐级返回递归;
  3. 递归函数中位于递归调用之前的语句,均按被调函数的顺序执行;递归函数中位于递归调用之后的语句,均按被调函数相反的顺序执行;
  4. 虽然每级递归都有自己的变量,但是并没有拷贝函数的代码;
  5. 递归函数必须包含能让递归调用停止的语句。

由于递归每次都调用自己,所以效率和速度都比循环低,而且每一级都创建变量,所以占用内存多,容易造成堆栈溢出。C语言的尾递归(最简单的递归形式,把递归调用置于函数的末尾,即正好在return语句之前)可以优化内存和运行效率,Python本身没有针对尾递归的优化。
  最后贴上手写的斐波那契(Fibonacci)数列代码:

/*递归写斐波那契数列*/
#include <stdio.h>

int fib(int n);

int main(void)
{
    int m, count;

    printf("Please enter an integer to incidate the number of items you want to print: \n");
    scanf("%d", &m); //输入显示的第m项斐波那契数列
    count = fib(m);
    printf("%d ", count); //打印第m项斐波那契数列的值

    return 0;
}

int fib(int n)
{
    if (n <= 2)
        return 1;
    else
        return fib(n - 1) + fib(n - 2);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值