函数的使用

目录

1 复习函数

1.1 创建并使用简单函数

1.2 分析程序

1.3 函数参数

  1.4 定义带形式参数的函数

1.5 声明带形式参数函数的原型

1.6 调用带实际参数的函数

1.7 黑盒视角

1.8 使用return从函数中返回值

1.9 函数类型

2 ANSI C 函数原型

2.1 问题所在

2.2 ANSI 的解决方案

2.3 无参数和未指定参数

2.4 函数原型的优点

3  递归

3.1 演示递归

3.2 递归的基本原理

3.3 尾递归

3.4 递归和倒序计算

3.5 递归的优缺点

4 编译多源代码文件的程序

4.1 Linux

4.2 使用头文件

5  查找地址:&运算符

6 更改主调函数中的变量

7 指针简介

7.1 间接运算符:*

7.2 声明指针

7.3 使用指针在函数间通信

8 复习题


1 复习函数

首先,什么是函数?函数(function)是完成特定任务的独立程序代码单元。语法规则定义了函数的结构和使用方式。虽然C中的函数和其他语言中的函数,子程序,过程作用相同,但是细节上略有不同。一些函数执行某些动作,如printf()把数据打印到屏幕上;一些函数找出一个值供程序使用,如strlen()把指定字符串的长度返回给程序。一把而言,函数可以同时具备以上两种可能。

为什么要使用函数?首先,使用函数可以省去编写重复代码的苦差。如果程序要多次完成某项任务,那么只需要编写一个合适的函数,就可以在需要时使用这个函数,或者在不同的程序中使用该函数,就像许多程序中使用putchar()一样。其次,即使程序只完成某项任务一次,也值得使用函数。因为函数让程序更加模块化,从而提高了程序代码的可读性更方便后期修改,完善

许多程序员喜欢把函数看作是根据传入信息(输入)及其生成的值或响应的动作(输出)来定义的“黑盒”。如果不是自己编写的函数,根本不用关心黑盒的内部行为。例如,使用printf()时,只需要知道给该函数传入格式字符串或一些参数以及printf()生成的输出,无需了解printf()的内部代码。以这种方式看待函数有助于把注意力集中在程序的整体设计,而不是函数的实现细节上。因此,在动手编写代码之前,仔细考虑一下函数应该完成什么任务,以及函数和程序整体的关系。

如何了解函数?首先要知道如何正确地定义函数,如何调用函数和如何建立函数间的通信。

1.1 创建并使用简单函数

      

1.2 分析程序

 该程序要注意以下几点。

1.程序在3处使用了starbar标识符:函数原型告诉编译器starbar()的类型;函数调用 表明在此处执行函数;函数定义 明确地指定了函数要做什么。

2.函数和变量一样,有多种类型。任何程序在使用函数之前都要声明该函数的类型。因此,在main()函数定义的前面出现了下面ANSI C风格的函数原型:

void starbar(void);

圆括号表明starbar是一个函数名。第1个void是函数类型,void类型表明函数没有返回值。第2个void(在圆括号中)表明该函数不带参数。分号表明这是在声明函数,不是定义函数。也就是说,这行声明了程序将使用一个名为starbar(),没有返回值,没有参数的函数,并告诉编译器在别处查找该函数的定义。对于不识别ANSI风格原型的编译器,只需要声明函数的类型,如下所示:

void starbar();

注意,一些老版本的编译器甚至连void都识别不了。如果使用这种编译器,就要把没有返回值的函数声明为int类型。当然,最好还是换一个新的编译器。

3.一般而言,函数原型指明了函数的返回值类型和函数接受的参数类型。这些信息称为该函数的签名。对于starbar()函数而言,其签名是该函数没有返回值,没有参数。

4.程序把starbar()原型置于main()的前面。当然,也可以放在main里面的声明变量处。放在哪个位置都可以。

5.在main()中,执行到下面的语句时调用了starbar()函数:

starbar();

这是调用void类型函数的一种形式。当计算机执行到starbar();语句时,会找到该函数的定义并执行其中的内容。执行完starbar()中的代码后,计算机返回主调函数继续执行下一行(编译器把c程序翻译乘执行以上操作的机器语言代码)。

6.程序中starbar()和main()的定义形式相同。首先函数头包括函数类型,函数名和圆括号,接着左花括号,变量声明,函数表达式语句,最后以右括号结束。注意,函数头中的starbar()后面没有分号,告诉编译器这是定义starbar(),而不是调用函数或声明函数原型。

7.程序把starbar()和main()放在一个文件中。当然,也可以把它们分别放在两个文件中。把函数都放在一个文件中的单文件形式比较容易编译,而使用多个文件方便在不同的程序中使用同一个函数。如果把函数放在一个单独的文件中,要把#define和#include指令也放入该文件。现在,先把所有的函数都放在一个文件中。main()的右花括号告诉编译器该函数结束的位置,后面的starbar()函数头告诉编译器starbar()是一个函数。

8.starbar()函数中的变量count是局部变量(local  variable),意思是该变量 只属于starbar()函数。可以在程序中的其他地方(包括main()中)使用count,这不会引起名称冲突,它们是同名的不同变量。

       如果starbar()看作是一个黑盒,那么它的行为是打印一行星号。不用给该函数提供任何输入,因为调用它不需要其他信息。而且,它没有返回值,所以也不给main()提供(或返回)任何信息。简而言之,starbar()不需要与主调函数通信。

1.3 函数参数

      在上一个程序的输出中,如果文字能居中,信头会更加美观。可以通过在打印文字之前打印一定数量的空格来实现。

/* lethead2.c */
#include<stdio.h>
#include<string.h>    /* 为strlen()提供原型 */
#define NAME      "GIGATHINK, INC."
#define ADDRESS   "101 Megabuck Plaza" 
#define PLACE     "Megapolis, CA 94904"
#define WIDTH     40
#define SPACE     ' '
void show_n_char(char ch, int num);
int main(void)
{
     int spaces;

	 show_n_char('*',WIDTH);                    // 用符号常量作为参数
	 putchar('\n');
	 show_n_char(SPACE,12);                     // 用符号常量作为参数
     printf("%s\n",NAME);
	 spaces = (WIDTH - strlen(ADDRESS)) / 2;    // 计算要跳过多少个空格

	 show_n_char(SPACE,spaces);                 // 用一个变量作为参数
	 printf("%s\n",ADDRESS);

	 show_n_char(SPACE, (WIDTH - strlen(PLACE)) / 2);

	 printf("%s\n",PLACE);                      // 用一个表达式作为参数
	 show_n_char('*',WIDTH);          
	 putchar('\n');

     return 0;

}
/* show_n_char()函数的定义*/
void show_n_char(char ch, int num)
{
      int count;
	  for(count = 1; count <= num; count++)
	  {
		  putchar(ch);
	  }
}

  1.4 定义带形式参数的函数

      函数定义从下面的ANSI C风格的函数头开始:

      void show_n_char(char ch, int num)

       该行告知编译器show_n_char()使用两个参数ch和num,ch是char类型,num是int类型。这两个变量被称为形式参数(formal argument,但是最近的标准推荐使用formal parameter),简称形参。和定义在函数中变量一样,形式参数也是局部变量,属该函数私有。这意味着在其他函数中使用同名变量不会引起名称冲突。每次调用函数,就会给这些变量赋值。

       注意,ANSI C要求在每个变量前都声明其类型。也就是说,不能像普通变量声明那样使用同一类型的变量列表:

       void  dibs(int x,y,z  );     // 无效的函数头

       void   dubs(int x, int y, int z)   //有效的函数头

      虽然show_n_char()接受来自main()的值,但是它没有返回值。因此,show_n_char()的类型是void。

1.5 声明带形式参数函数的原型

       在使用函数之前,要用ANSI C形式声明函数原型;

       void show_n_char(char ch, int num);

       当函数接受参数时,函数原型用逗号分隔的列表指明参数的数量和类型。根据个人喜好,你也可以省略变量名:

        void show_n_char(char, int);

        在原型中使用变量名并没有实际创建变量,char仅代表一个char类型的变量,以此类推。

1.6 调用带实际参数的函数

        在函数调用中,实际参数(actual argument,简称实参)提供了ch和num的值。考虑上面的程序第二次调用show_n_char();

        show_n_char(SPACE, 12);

        实际参数是空格字符和12。这两个值被赋给show_n_char()中相应的形式参数:变量ch和num。简而言之,形式参数是被调函数中的变量,实际参数是主调函数赋给被调函数的具体值

如上例所示,实际参数可以是常量,变量,或甚至是更复杂的表达式。无论实际参数是何种形式都要被求值,然后该值被拷贝给被调函数相应的形式参数。上面程序第四次调用show_n_char()为例;

       show_n_char(SPACE,(WIDTH - strlen(pPLACE)) / 2 );

       构成该函数第2个实际参数的是一个很长的表达式,对该表达式求值为10。然后,10被赋给变量num。被调用函数不知道也不关心传入的数值是来自常量,变量还是一般表达式。再次强调,实际参数是具体的值,该值要被赋给作为形式参数的变量。因为被调函数使用的值是从主调函数中拷贝而来,所以无论被调函数对拷贝数据进行什么操作,都不会影响主调函数中的原始数据。

1.7 黑盒视角

       从黑盒的视角看show_n_char(),待显示的字符和显示的次数是输入。执行后的结果是打印指定数量的字符。输入以参数的形式被传递给函数。这些信息清楚地表明了如何在main()中使用该函数。而且,这也可以作为编写该函数的设计说明。

        黑盒方法的核心部分是:ch,num和count都是show_n_char()私有的局部变量。如果在main()中使用同名变量,那么它们相互独立,互不影响了。也就是说,如果main()有一个count变量,那么改变它的值不会改变show_n_char()中的count,反之亦然。黑盒里发生什么对主调函数是不可见的。

1.8 使用return从函数中返回值

      前面介绍了如何把信息从主调函数传递给被调函数。反过来,函数的返回值可以把信息从被调函数传回主调函数。为了进一步说明,我们将创建一个返回两个参数中较小值的函数。由于函数被设计用来处理int类型,所以被命名为imin()。另外,还要创建一个简单的main(),用于检查imin()是否正常工作。这种被设计用于测试函数的程序有时被称为驱动程序,该驱动程序调用一个函数。如果函数成功通过了测试,就可以安装在一个更重要的程序中使用。

     scanf()返回成功读取数据的个数,所以如果输入不是两个整数会导致循环终止。

     关键字return后面的表达式的值就是函数的返回值。在该例中,该函数返回的值就是变量min的值。因为min是int类型的变量,所以imin()函数的类型 也是int。

      变量min属于imin()函数私有,但是return语句把min的值传回了主调函数。下面这条语句的作用是把min的值赋给lesser:

       lesser = imin(n,m);

       是否可以写成下面这样:

        imin(n,m);

        lesser = min;

        不能,因为主调函数甚至不知道min的存在。记住,imin()中的变量是imin()的局部变量。函数调用imin(evil1,evil2)只是把两个变量的值拷贝了一份。

        返回值不仅可以赋给变量,也可以被用作表达式的一部分。例如,可以这样:

        answer = 2 * imin(z,zstar) + 25;

        printf("%d\n",imin(-32 + answer, LIMIT);

        返回值不一定是变量的值,也可以是任意表达式的值。例如,可以用以下的代码简化程序示例:

/* 返回最小值的函数,第2个版本*/

imin(int n, int m)

{

        return (n<m)? n : m;

}                                                                                                                                                         

条件表达式的值是n和m中的较小者,该值要被返回给主调函数。虽然这里不要求用圆括号把返回值括起来,但是如果想让程序条理更清楚或统一风格,可以把返回值放在圆括号内。

     如果函数返回的类型与函数声明的类型不匹配会怎样?

     int what_if(int n)

     { 

          double z  = 100.0 / (double) n;

          return z;   // 会发生什么?

      }

     实际得到的返回值相当于把函数中指定的返回值赋给与函数类型相同的变量所得到的值。因此在本例中,相当于把z的值赋给int类型的变量,然后返回int类型变量的值。例如,假设有下面的函数调用:

     result = what_if(64);

     虽然在what_if()函数中赋给z的值是1.5626,但是return 语句返回int类型的值1。

     使用return语句的另一个作用是,终止函数并把控制返回给主调函数的下一条语句。因此,可以这样编写imin():

      /* 返回最小值的函数,第3个版本*/

imin(int n,int m)

{      

      if (n < m)

             return n;

       else

              return m;

}

许多C程序员都认为只在函数末尾使用一次return语句比较好,因为这样做更方便浏览程序的人理解函数的控制流。但是,在函数中使用多个return语句也没有错。无论如何,对用户而言,这3个版本的函数用起来都一样,因为所有的输入和输出都完全相同,不同的是函数的内部的实现细节。下面的版本也没有问题:

//   返回最小值的函数,第4个版本

imin (int n, int m)

{

      if (n < m)

               return n;

      else

               return m;

       printf("Professor Fleppard is like totally a fopdoodle.\n");

 }

return语句导致printf()语句永远不会被执行。如果Fleppard教授在自己的程序中使用这个版本的函数,可能永远不知道编写的学生对他的看法。

另外,还可以这样使用return:

return;

这条语句会导致终止函数,并把控制返回给主调函数。因为return后面没有任何表达式,所以没有返回值,只有在void函数中才会用到这种形式。

1.9 函数类型

     声明函数时必须声明函数的类型。带返回值的函数类型应该与其返回值类型相同,而没有返回值的函数应声明为void类型。如果没有声明函数的类型,旧版本的C编译器会假定函数的类型是int。这一惯例源于C的早期,那时的函数绝大多数都是int类型。然而,C99标准不再支持int类型的这种假设设置。

     类型声明是函数定义的一部分。要记住,函数类型指的是返回值的类型,不是函数参数的类型。例如,下面的函数头定义了一个带两个int类型参数的函数,但是其返回值是double类型。

double klink(int a, int b)

      要正确地使用函数,程序在第1次使用函数之前必须知道函数的类型。方法之一是,把完整的函数定义放在第1次调用函数的前面。然而,这种方法增加了程序的阅读难度。而且,要使用的函数可能在C库或者其他的文件中。因此,通常的做法是提前声明函数,把函数的信息告知编译器。

例如,在上面的程序当中main()函数包含以下几行代码:

#include<stdio.h>

int imin(int, int);

int  main(void)

{

       int evil1, evil2, lesser;

第2行代码说明imin是一个函数名,有两个int类型的形参,且返回int类型的值。现在,编译器在程序中调用imin()函数时就知道如何处理。

在上面的程序当中,我们把函数的前置声明放在主调函数外面。当然,也可以放在主调函数的里面。

      ANSI C标准库中,函数被分成多个系列,每一系列都有各自的头文件。这些头文件中除了其他内容,还包含了本系列所有函数的声明。例如,stdio.h头文件包含了标准I/O库函数(如,printf()和scanf())的声明。math.h头文件包含了各种数学函数的声明。例如,下面的声明:

       double sqrt(double);

       告知编译器sqrt()函数有一个double类型的形参,而且返回double类型的值。不要混淆函数的声明和定义。函数声明告知编译器函数的类型,而函数的定义则提供实际的代码。在程序中包含math.h头文件告知编译器:sqrt()返回double类型,但是sqrt()函数的代码在另一个库函数的文件中。

2 ANSI C 函数原型

      在ANSI C标准之前,声明函数的方案有缺陷,因为只需要声明函数的类型,不用声明任何参数。下面我看一下使用旧式的函数声明会导致什么问题。

      下面是ANSI之前的函数声明,告知编译器imin()返回int类型的值;

       int  imin();

       然而,以上函数声明并未给出imin()函数的参数个数和类型。因此,如果调用imin()时使用的参数个数不对或类型不匹配,编译器根本不会察觉出来。

2.1 问题所在

     由于不同系统的内部机制不同,所以出现问题的具体情况也不同。主调函数把它的参数存储在被称为 栈 的临时存储区,被调函数从栈中读取这些参数。主调函数根据函数调用中的实际参数决定传递的类型,而被调用函数根据它的形式参数读取值。

2.2 ANSI 的解决方案

     针对参数不匹配的问题,ANSI C标准要求在函数声明时还要声明变量的类型,即使用 函数原型

来声明函数的返回类型,参数的数量和每个参数的类型。未标明imax()函数有两个int类型的参数,可以使用下面两种函数原型来声明:

int    imax(int, int);

int    imax(int a, int b); 

     第1中形式使用以逗号分隔的类型列表,第2种形式在类型后面添加了变量名。注意,这里的变量名是假名,不必与函数定义的形式参数名一致。

     有了这些信息,编译器可以检查函数调用是否与函数原型匹配。参数的数量是否正确?参数的类型类型是否匹配?以imax()为例,如果两个参数都是数字,但是类型不匹配,编译器会把实际参数的类型转换成形式参数的类型。例如,imax(3.0, 5.0)会被转换成imax(3, 5)。

     下面的函数调用:

     imax(3.9, 5.4)

    相当于:

     imax(3, 5)

错误和警告的区别是:错误导致无法编译,而警告仍然允许编译。一些编译器在进行类似的类型转换时不会通知用户,因为C标准中对此未作要求。不过,许多编译器都允许用户选择警告级别来控制编译器在描述警告时的详细程序。

2.3 无参数和未指定参数

   假设有下面的函数原型:

   void print_name();

   一个不支持ANSI C的编译器会假定用户没有用函数原型来声明函数,它将不会检查参数。为了表明函数确实没有参数,应该在圆括号中使用void关键字:

    void print_name(void);

    支持ANSI C的编译器解释为print_name()不接受任何参数。编译器会检查以确保没有使用参数。

    一些函数接受(如,printf()和scanf())许多参数。例如对于printf(),第1个参数是字符串,但是其余参数的类型和数量都不固定。对于这种情况,ANSI C允许使用部分原型。例如,对于printf()可以使用下面的原型:

    int  printf(const char *, ...);

    这种原型表明,第1个参数是一个字符串,可能还有其他未指定的参数。

    C库通过stdarg.h头文件提供了一个定义这类(形参数量不固定的)函数的标准方法。

2.4 函数原型的优点

  函数原型是C语言的一个强有力的工具,它让编译器捕获在使用函数时可能出现的许多错误或疏漏。如果编译器没有发现这些问题,就很难觉察出来。是否必须使用函数原型?不一定。你也可以使用旧式的函数声明(即不用声明任何形参),但是这样做的弊大于利。

有一种方法可以省略函数原型却保留函数原型的优点。首先要明白,之所以使用函数原型,是为了让编译器在第1次执行该函数之前就知道如何使用它。因此,把整个函数定义放在第1次调用该函数之前,也有相同的效果。此时,函数定义也相当于函数原型。对于较小的函数,这种用法很普遍:

 //   下面这行代码既是函数定义,也是函数原型

 int imax(int a, int b) { return a > b ? a : b;  }

 int main ()

{    

      int x, z;

...

      z = imax (x, 50);

}

3  递归

      C 允许函数调用它自己,这种调用过程称为 递归(recursion)。递归有时难以捉摸,有时却很方便使用。结束递归是使用递归的难点 ,因为如果递归代码中没有终止递归的条件测试部分,一个调用自己的函数会无限递归。

      可以使用循环的地方通常都可以使用递归。有时用循环解决问题比较好,但有时用递归更好。递归方案更简洁,但效率却没有循环高。

3.1 演示递归

     我们通过一个程序示例,来学习什么是递归。下面的程序当中的main()函数调用up_and_down()函数,这次调用称为“第1级递归”。然后up_and_down()调用自己,这次调用称为“第2级递归”。接着第2级递归调用第3级递归,以此类推。该程序共有4级递归。为了进一步深入研究递归时发生了什么,程序不仅显示了变量n的值,还显示了存储n的内存地址&n。(&运算符,printf()函数使用%p转换说明打印地址,如果你的系统不支持这种格式,请使用%u或%lu代替%p)。

     我们来仔细分析 程序中的递归是如何工作的。首先,main()调用了带参数1的up_and_down()函数,执行结果是up_and_down()中的形式参数n的值是1,所以打印语句#1打印Level 1。然后,由于n小于4,up_and_down()(第1 级)调用实际参数为n+1(或2)的up_and_down()(第2级)。于是第2级调用中的n的值是2,打印语句#1打印Level 2。与此类似,下面两次调用打印的分别是Level 3 和 Level 4。

    当执行到第4级时,n的值是4,所以if测试条件为假。up_and_down()函数不再调用自己。第4级调用接着执行打印语句#2,即打印LEVEL 4,因为n的值是4。此时,第4级调用结束,控制被传回它的主调函数(第3级调用)。在第3级调用中,执行的最后一条语句是调用if语句中的第4级调用。被调函数(第4级调用)把控制返回在这个位置,因此,第3级调用继续执行后面的代码,打印语句#2打印LEVEL 3。然后第3级调用结束,控制被传回第2级调用,接着打印LEVEL 2,以此类推。

     注意,每级递归的变量n都属于本级递归私有。这从程序输出的地址值可以看出(当然,不同的系统表示的地址格式不同,这里关键要注意,Level 1和LEVEL 1 的地址相同,Level 2 和 LEVEL 2的地址相同,等等)。

      如果觉得不好理解,可以假设有一条调用链-----fun1()调用fun2(),fun2()调用fun3(),fun3()调用fun4()。当fun4()结束时,控制传回fun3();当fun3()结束时,控制传回fun2();当fun2()结束时,控制传回fun1()。递归的情况与此类似,只不过fun1(),fun2(),fun3()和fun4()都是相同的函数。

3.2 递归的基本原理

        初次接触递归会觉得较难理解。为了帮助读者理解递归过程,下面以上面的程序清单为例讲解几个要点。第1,每级函数调用都有自己的变量。也就是说,第1级的n和第2级的n不同,所以程序创建了4个单独的变量,每个变量名都是n,但是它们的值各不相同。当程序最终返回up_and_down()的第1级调用时,最初的n仍然是它的初值1。

        第2,每次函数调用都会返回一次。当函数执行完毕后,控制权将被传回上一级递归。程序必须按顺序逐级回递归,从某级up_and_down()返回上一个级的up_and_down(),不能跳级回到main()中的第1级调用。

        第3,递归函数中位于递归调用之前的语句,均按被调函数的顺序执行。例如,程序中的打印语句#1位于递归调用之前,它按照递归的顺序:第1级,第2级,第3级和第4级,被执行了4次。

        第4,递归函数中位于递归调用之后的语句,均按被调用函数相反的顺序执行。例如,打印语句#2位于递归调用之后,其执行的顺序是第4级,第3级,第2级,第1级。递归调用的这种特性在解决涉及相反顺序的编程问题很有用。

        第5,虽然每级递归都有自己的变量,但是并没有拷贝函数的代码。程序按顺序执行函数中的代码,而递归调用就相当于又从头开始执行函数的代码。除了为每次递归调用创建变量以外,递归调用非常类似于一个循环语句。实际上,递归有时可用循环来代替,循环有时也能用递归来代替。

        第6,递归函数必须包含能让递归调用停止的语句。通常,递归函数都使用if或其他等价的测试条件在函数形参等于某特定值时终止递归。为此,每次递归调用的形参都要使用不同的 值。例如,上面的程序当中的up_and_down(n)调用up_and_down(n+1)。最终,实际参数等于4时,if的测试条件(n<4)为假。

3.3 尾递归

      最简单的递归形式是把递归调用置于函数的末尾,即正好在return语句之前。这种形式的递归被称为尾递归,因为递归调用在函数的末尾。尾递归是最简单的递归形式,因为它相当于循环。

      下面要介绍的程序示例中,分别用循环和尾递归计算阶乘。一个正整数的阶乘(factorial)是从1到该整数的所有整数的乘积。例如,3的阶乘(写作3!)是1×2×3。另外,0!等于1,负数没有阶乘。程序清单中,第1个函数使用for循环计算阶乘,第2个函数使用递归计算阶乘。

       测试驱动程序把输入限制在0~12。因为12!已快接近5亿,而13!比62亿还大,已超过我们系统中long类型能表示的范围。要计算超过12的阶乘,必须使用能表示更大范围的类型,如double或long long。

     使用循环的函数把ans初始化为1,然后把ans与从n~2的所有递减整数相乘。根据阶乘的公式,还应该乘以1,但是这并不会改变结果。

     现在考虑使用递归的函数。该函数的关键是n!=n×(n-1)!。可以可以这样做是因为(n-1)!是n-1~1的所有正整数的乘积。因此,n乘以n-1的阶乘就得到n的阶乘。阶乘的这一特性很适合使用递归。如果 调用函数rfact(),rfact(n)是n*rfact(n-1)。因此,通过调用rfact(n-1)来计算rfact(n)。当然,必须要在满足某条件时结束递归,可以在n等于0时把返回值设置为1。

     上面的程序中使用递归的输出和使用循环的输出相同。注意,虽然rfact()的递归调用不是函数的最后一行,但是当n>0时,它是该函数执行的最后一条语句,因此它也是尾递归。

      既然用递归和循环来计算都没问题,那么到底应该使用哪一个?一般而言,选择循环比较好。首先,每次递归都会创建一组变量,所以递归使用的内存更多,而且递归调用都会把创建的一组新变量放在栈中。递归调用的数量受限于内存空间。其次,由于每次函数调用要花费一定的时间,所以递归的执行速度较慢。那么,演示这个程序示例的目的是什么?因为尾递归中递归中最简单的形式,比较容易理解。某些情况下,不能用简单的循环代替递归,因此读者还是要好好理解递归。

3.4 递归和倒序计算

     递归在处理倒序时非常方便(在解决这类问题中,递归比循环简单)。我们要解决的问题是:编译一个函数,打印一个整数的二进制数。二进制表示法根据2的幂来表示数字。十进制234实际上是2×10²+3×10(1)+4×10(0)(括号中的1和0分别表示1次方和0次方,所以二进制数101实际上是1×2²+0×2(1)+1×2(0)。二进制数由0和1表示。

     我们要设计一个以二进制形式表示整数的方法或算法。例如,如何用二进制表示十进制数5?在二进制中,奇数的末尾一定是1,偶数的末尾一定是0,所以通过5%2即可确定5的二进制数的最一位是1还是0.一般而言,对于数字n,其二进制的最后一位是n%2。因此,计算的第一位数字实际上是待输出二进制数的最后一位。这一规律提示我们,在递归函数的递归调用之前计算n%2,在递归调用之后打印计算结果。这样,计算的第1个值正好是最后一个打印的值。

      要获得下一位数字,必须把原数除以2。这种计算方法相当于在十进制下把小数点左移一位,如果计算结果是偶数,那么二进制的下一位数就是0;如果是奇数,就是1。例如,5/2得2(整数除法),2是偶数(2%2得0),所以下一位二进制数就是0。到目前为止,我们已经获得01。继续重复这个过程。2/2得1,1%2得1,所以下一位二进制数是1。因此,我们得到5得等价二进制数是101。那么,程序应该何时停止计算?当与2相除得结果小于2时停止计算,因为只要结果大于或等于2,就说明还有二进制位。每次除以2就相当于去掉一位二进制,直到计算出最后一位为止(如果不好理解,可以拿十进制来做类比:628%10得8,因此8就是该数最后一位;而628/10得62,而62%10得2,所以该数的下一位是2,以此类推)。

// binary.c -- 以二进制形式打印整数
#include<stdio.h>
void to_binary(unsigned long n);

int main(void)
{
    unsigned long number;
	printf("Enter an integer(q to quit):\n");
	while( scanf("%ld",&number) == 1)
	{
        printf("Binary equivalent: ");
		to_binary(number);
		putchar('\n');
	    printf("Enter an integer(q to quit):\n");

	}

    printf("Done.\n");

	return 0;
}


void to_binary(unsigned long n)  //递归函数
{
     int r;

	 r = n % 2;

	 if(n >= 2)
		 to_binary(n / 2);

     putchar(r == 0? '0' : '1');


     return;
}

        在该程序中,如果r的值是0,to_binary()函数就显示字符‘0’;如果r的值是1,to_binary()函数则显示字符‘1’。条件表达式r == 0?‘0’ :‘1’用于把数值转换成字符。

         不用递归,是否能实现这种用二进制形式表示整数的算法?当然可以。但是由于这种算法要首先计算最后一位二进制数,所以在显示结果之前必须把所有的位数都存储在别处(例如,数组)。

3.5 递归的优缺点

       递归既有优点也有缺点。优点是递归为某些编程问题提供了最简单的解决方案。缺点是一些递归算法会快速消耗计算机的内存资源。另外,递归不方便阅读和维护。

       所有的c函数戒平等:程序中的每个C函数与其他函数都是平等的。每个函数都可以调用其他函数,或被其他函数调用。虽然过程可以嵌套在另外一个过程中,但是嵌套在不同过程中的过程之间不能相互调用。

       main()函数是否与其他函数不同?是的,main()的确有点特殊。当main()与程序中的其他函数放在一起时,最开始执行的是main()函数中的第1条语句,但是这也是局限之处。main()也可以被自己或其他函数递归调用-尽管很少这样做。

4 编译多源代码文件的程序

       使用多个函数最简单的方法是把它们都放在同一个文件中,然后像编译只有一个函数的文件那样编译该文件即可。其他方法因操作系统而异。

4.1 Linux

       假定Linux系统安装了GNU C编译器GCC。假设file1.c和file2.c是两个内含c函数的文件,下面的命令将编译两个文件并生成名为a.out的可执行文件:

       gcc file1.c  file2.c

       另外,还生成两个名为file1.o和file2.o的目标文件。如果后来改动了file1.c,而file2.c不变,可以使用以下命令编译第1个文件,并与第2个文件的目标文件合并:

       gcc file1.c file2.o

4.2 使用头文件

      如果把main()放在第1个文件中,把函数定义放在第2个文件中,那么第1个文件仍然要使用函数原型。把函数原型放在头文件中,就不用再每次使用函数文件时都写出函数的原型。C标准库就是这样做的,例如,把I/O函数原型放在stdio.h中,把数学函数原型放在math.h中。你也可以这样用自定义的函数文件。

     另外,程序中经常用C预处理器定义符号常量。这种定义只存储了那些包含#include指令访问每个文件。如果把程序的一个函数放进一个独立的文件中,你也可以使用#include指令访问每个文件。最直接的方法是在每个文件中再次输入指令,但是这个方法既耗时又容易出错。另外,还会有维护的问题:如果修改了#define定义的值,就必须在每个文件中修改。更好的做法是,把#define指令放进头文件,然后在每个源文件中使用#include指令包含该文件即可。

     总之,把函数原型和已定义的字符常量放在头文件中是一个良好的编程习惯。我们考虑一个例子:假设要管理4家酒店的客房服务,每家酒店的房价不同,但是每家酒店所有房间的房价相同。对于预定住宿多天的客户,第2天的房费是第1天的95%,第3天是第2天的95%,以此类推。设计一个程序让用户指定酒店和入住天数,然后计算并显示总费用。同时,程序要实现一份菜单,允许用户反复输入数据,除非用户选择退出。

     在UNIX和DOS环境中,#include"hotels.h"指令中的双引号表明被包含的文件位于当前目录中(通常是包含源代码的目录)。

-----usehotel.c 控制模块

-----hotel.c函数支持模块

// hotel.c -- 酒店管理函数
#include<stdio.h>
#include"hotel.h"
int menu(void)
{
     int code, status;

   	 printf("\n%s%s\n",STARS,STARS); 
     printf("Enter the number  of the desired hotel:\n");
	 printf("1) Fairfield Arms     2) Hotel Olympic\n");
     printf("3) Chertworthy Plaza  4) The Stockton\n");
	 printf("5) quit\n");

	 printf("%s%s\n", STARS, STARS);

	 while((status = scanf("%d",&code)) != 1 || (code < 1 || code > 5))
	 {
		 if(status != 1)
		 {
			 scanf("%*s");   // 处理非整数输入
		 }

		 printf("Enter an integer from 1 to 5,please.\n");
	 }

    return code;
}

dint getnights(void)
{
    int nights;

	printf("How many nights are needed? ");
	while( scanf("%d", &nights) != 1)
	{
         scanf("%*s");       // 处理非整数输入
		 printf("Please enter an integer, such as 2.\n");

	}
    return nights;
}

void showprice(double rate, int nights)
{
     int n;
	 double total = 0.0;
	 double factor = 1.0;

	 for(n = 1; n<= nights; n++,factor *= DISCOUNT)
		 total += rate * factor;
     printf("The total cost will be $%0.2f.\n",total);	 

}

----- hotel.h头文件

       顺带一提,该程序中有几处编写得很巧妙。尤其是,menu()和getnights()函数通过测试scanf()的返回值来跳过非数值数据,而且调用scanf("%*s")跳至下一个空白字符。注意,menu()函数中是如何检查非数值输入和超出范围的数据:

        while( (status = scanf("%d",&code)) ! = 1 || (code < 1 || code > 5))

        以上代码段利用了C语言的两个规则:从左往右对逻辑表达式求值;一旦求值结果为假,立即停止求值。在该例中,只有在scanf()成功读入一个整数值后,才会检查code的值。

        用不同的函数处理不同的任务时应检查数据的有效性。当然,首次编写menu()或getnights()函数时可以暂不添加这一功能,只写一个简单的scanf()即可。待基本版本运行正常后,再逐步改善各模块。

5  查找地址:&运算符

       指针(pointer)是C语言最重要的(有时也是最复杂的)概念之一,用于存储变量的地址。前面使用的scanf()函数中就使用地址作为参数。概括地说,如果主调函数不使用return返回的值,则必须通过地址才能修改主调函数中的值。接下来,我们将介绍带地址参数的函数。首先介绍一元&运算符的用法。

       一元&运算符给出变量的存储地址。如果pooh是变量名,那么&pooh是变量的地址。可以把地址看作是变量在内存中的位置。假设有下面的语句:

       pooh = 24;

       假设pooh的存储地址是0B76(PC 地址通常用十六进制形式表示)。那么,下面的语句:

       printf("%d %p\n", pooh, &pooh);

       将输出如下内容(%p是输出地址的转换说明):

       24 0B76

     运行结果如下:

        每个十六进制数对应4位,该例显示12个十六进制数,对应48位地址。

        该例的输出说明了什么?首先, 两个pooh的地址不同,两个bah的地址也不同。因此,和前面介绍的一样,计算机把它们看成4个独立的变量。

6 更改主调函数中的变量

        有时需要在一个函数中更改其他函数的变量。例如,普通的排序任务中交换两个变量的值。假设要交换两个变量X和Y的值。简单的思路是:

         x = y;

         y = x;

        这完全不起作用,因为执行到第2行时,x的原始值已经被y的原始值替换了。因此,要多写一行代码,存储x的原始值:

         temp = x;

         x = y;

         y = temp;

        上面这3行代码便可实现交换值的功能,可以编写成一个函数并构造一个驱动程序来测试。为清楚地表明变量属于哪个函数,在main()中使用变量x和y,在interchange()中使用u和v。

运行该程序后,输出如下:

    两个变量的值并未交换!我们在interchange()中添加一些打印语句来检查错误。

   下面是该程序的输出:

      看来,interchange()没有问题,它交换了u和v的值。问题出在把结果传回main()时。interchange()使用的变量并不是main()中的变量。因此,交换u和v的值对x和y的值没有影响!是否能用return语句把值传回main()?当然可以,在interchange()的末尾加上下面一行语句:

      return(u);

      然后修改main()中的调用:

      x = interchange(x,y);

      这只能改变x的值,而y的值依旧没变。用return语句只能把被调函数中的一个值传回主调函数,但是现在要传回两个值。这没有问题!不过,要使用指针。

7 指针简介

    指针?什么是指针?从根本上看,指针(pointer)是一个值为内存地址的变量(或数据对象)。正如char类型变量的值是字符,int类型变量的值是整数,指针变量的值是地址。在C语言中,指针有许多用法。

     假设一个指针变量名是ptr,可以编写如下语句:

     ptr = &pooh;   //把pooh的地址赋给ptr

     对于这条语句,我们说ptr"指向"pooh。ptr和&pooh的区别是ptr是变量,而&pooh是常量。或者,ptr是可修改的左值,而&pooh是右值。还可以把ptr指向别处:

    ptr = &bah;  // 把ptr指向bah,而不是pooh

7.1 间接运算符:*

    假设已知ptr指向bah,如下所示:

    ptr = &bah;

    然后使用间接运算符* 找出存储在bah中的值,该运算符有时也称为解引用运算符。不要把间接运算符和二元乘法运算符(*)混淆,虽然它们使用的符号相同,但是语法功能不同。

   val = *ptr;   //找出ptr指向的值

   语句 ptr = &bah;和val = *ptr;放在一起相当于下面的语句:

   val = bah;

    由此可见,使用地址和间接运算符可以间接完成上面这条语句的功能,这也是"间接运算符"名称的由来。

7.2 声明指针

    相信读者已经很熟悉如何声明int类型和其他基本类型的变量,那么如何声明指针变量?你也许认为是这样声明:

    pointer  ptr;    //不能这样声明指针

     为什么不能这样声明?因为声明指针变量时必须指定指针所指向变量的类型因为不同的变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小。另外,程序必须知道存储在指定地址上的数据类型。long和float可能占用相同的存储空间,但是它们存储数字却大相径庭。下面是一些指针的声明示例:

     int* pi;    //pi是指向int类型变量的指针

     char* pc;    //pc是指向char类型变量的指针

     float*  pf, *pg;  // pf,pg都是指向float类型变量的指针

     类型说明符表明了指针所指向对象的类型,星号(*)表明声明的变量是一个指针。int *pi;声明的意思是pi是一个指针,*pi是int类型。

     *和指针名之间的空格可有可无。通常,程序员在声明时使用空格,在解引用变量时省略空格。

     pc指向的值(*pc)是char类型。pc本身是什么类型?我们描述它的类型是“指向char类型的指针”。pc的值是一个地址,在大部分系统内部,该地址由一个无符号整数表示。但是,不要把指针认为是整数类型。一些处理整数的操作不能用来处理指针,反之亦然。例如,可以把两个整数相乘,但是不能把两个指针相乘。所以,指针实际上是一个新类型,不是整数类型。因此,如前所述,ANSI C专门为指针提供了%p格式的转换说明。

7.3 使用指针在函数间通信

     我们才刚刚接触指针,指针的世界丰富多彩。本节着重介绍如何使用指针解决函数间的通信问题。请看如下程序清单,该程序interchange()函数中使用指针参数。

    该函数传递的不是x和y的值,而是它们的地址。这意味着出现在interchange()原型和定义中的形式参数u和v将把地址作为它们的值。因此,应把它们声明为指针。由于x和y是整数,u和v是指向整数的指针,其声明如下:

     void interchange(int* u,int* v)

     接下来,在函数体中声明了一个交换值时必需的临时变量:

     int temp;

     通过下面的语句把x的值存储在temp中:

     temp = *u;

     记住,u的值是&x,所以u指向x。这意味着用*u即可表示x的值,这正是我们需要的。不要写成这样:

     temp = u; //不要这样做

     因为这条语句赋给temp的是x的地址(u的值就是x的地址),而不是x的值。函数要交换的是x和y的值,而不是它们的地址。

      与此类似,把y的值赋给x,要使用下面的语句:

      *u = *v;

      这条语句相当于:

       x = y;

       我们总结一下该程序示例做了什么。我们需要一个函数交换x和y的值。把x和y的地址传递给函数,我们让interchange()访问这两个变量。使用指针和*运算符,该函数可以访问存储在这些位置的值并改变它们。

       可以省略ANSI C风格的函数原型中的形参名,如下所示:

       void interchange(int *, int *);

       一般而言,可以把变量相关的两类信息传递给函数。如果这种形式的函数调用,那么传递的是x的值:

       function1(x);

       如果下面形式的函数调用,那么传递的是x的地址:

       function2(&x);

       第1种形式要求函数定义中的形式参数必须是一个与x的类型相同的变量:

       int  function1(int x);

       第2种形式要求函数定义种的形式参数必须是一个指向正确类型的指针:

       int  function2(int* ptr);

       如果要计算或处理值,那么使用第1种形式的函数调用;如果要在被调函数种改变主调函数的变量,则使用第2种形式的函数调用。我们用过的scanf()函数就是这样。当程序要把一个值读入变量时(如本例中的num),调用的是scanf("%d", &num)。scanf()读取一个值,然后把该值存储到指定的地址上。

      对本例而言,指针让interchange()函数通过自己的局部变量改变main()函数中变量的值。

8 复习题

1.实际参数和形式参数的区别是什么?

  形式参数是定义在被调函数中的变量。实际参数是出现在函数调用中的值,该值被赋给形式参数。可以把实际参数视为在函数调用时初始化形式参数的值。

2.根据下面各函数的描述,分别编写它们的ANSI C函数头。注意,只需要写出函数头,不用写函数体。

a.donut()接受一个int类型的参数,打印若干(参数指定数目)个0  void donut(int n);

b.gear()接受两个int类型的参数,返回int类型的值   int gear(int t1, int t2);

c.guess()不接受参数,返回一个int类型的值   int guess(void); 

d.stuff_it()接受一个double类型的值和double类型变量的地址,把第1个值存储在指定位置

void stuff_it(double d , double* pd)

根据下面各函数的描述,分别编写它们的ANSI C函数头。注意,只写出函数头,不用写函数体。

a. n_to_char()接受一个int类型的参数,返回一个char类型的值   char n_to_char(int i);

b. digit()接受一个double类型的参数和一个int类型的参数,返回一个int类型的值

 int digit(double d,int i);

c.  which()接受两个可存储double类型变量的地址,返回一个double类型的地址 

double* which(double* pd1, double* pd2);

d.  random()不接受参数,返回一个int类型的值  int random(void);

4.设计一个函数,返回两个整数之和

int add(int a,int b)
{
     return (a+b);
}

5.如果把复习题4改成返回两个double类型的值之和,应如何修改函数

double add(double a, double b)
{
     return (a+b);
}

6.设计一个名为alter()的函数,接受两个int类型的变量x和变量y,把它们的值分别改成两个变量之和以及两变量之差。

void alter(int* x, int* y)
{  
           int temp;
           temp = *pa + *pb;
           *pb = *pa - *pb;
           *pa = temp;
}
         

 7.下面的函数定义是否正确?

 void salami(num)

{   

          int num, count;

          for(count = 1; count <= num; num ++)

             printf("O salami mio!\n);

}

不正确。num应声明在salami()函数的参数列表中,而不是声明在函数体中。另外,把count++改成num++。

8.编写一个函数,返回3个整数参数中的最大值

int getMax(int a,int b,int c)
{
       int max;

       max = a < b ? b : a;
       max = max < c ? c : max;
      
       return max;
}

9.给定下面的输出:

Please choose one of the following:

1) copy files                             2) move files

3) remove files                         4) quit

Enter the number of your choice:

a.编写一个函数,显示一份有4个选项的菜单,提示用户进行选择(输出如上所示)

b.编写一个函数,接受两个int类型的参数分别表示上限和下限。该函数从用户的输入中读取整数。如果整数超出规定上下限,函数再次打印菜单(使用a部分的函数)提示用户输入,然后获取一个新值。如果用户输入的整数在规定范围内,该函数则把该整数返回主调函数。如果用户输入一个非整数字符,该函数应返回4.

c.使用本题a和b部分的函数编写一个最小型的程序。最小型的意思是,该程序不需要实现菜单中各选项的功能,只需显示这个选项并获取有效的响应即可。

#include<stdio.h>

int getChoice(void);//获取正确的选项
int getFirst(void);//获取第一个字符

int main(void)
{
    int ch;

    while((ch = getChoice()) != '4')
	{
          switch(ch)
		  {
			  case '1':
				        printf("copy files!\n");
						break;
			  case '2':
						printf("move files!\n");
						break;
			  case '3':
						printf("remove files!\n");
						break;
			  default:
						printf("program error!\n");
						break;
		  }

	}
	 
    printf("bye!\n");


	return 0;
}


// 获取正确的选项
int getChoice(void)
{
	 int ch; 

     printf("Please choose one of the following:\n");
     printf("1) copy files        2) move files\n");
	 printf("3) remove files      4) quit\n");
     printf("Enter the number of your choice:\n");

     ch = getFirst();     


     while( (ch < '1' || ch > '4'))
	 {

             printf("Please respond with 1,2,3,4\n");
			 ch = getFirst();
	 }



     return ch;
}



// 获取第1个字符
int getFirst(void)
{
      int ch;

	  ch = getchar();

	  while( getchar() != '\n')
		   continue;

      return ch;

}





10.设计一个函数min(x,y),返回两个double类型值的较小值。在一个简单的驱动程序中测试该函数。

#include<stdio.h>

double min(double x, double y);

int main(void)
{
    printf("1.0和9.0中较小的数是%1.2f\n",min(1.0,9.0)); 


	return 0;
}


double min(double x, double y)
{
      return (x < y?y:x);       
}

11.设计一个函数chline(ch, i, j),打印指定的字符j行i列。在一个简单的驱动程序中测试该函数。

#include<stdio.h>

void chline(char,int,int);

int main(void)
{
    printf("将字符a打印3行5列\n");
    chline('a',3,5);

	return 0;
}


void chline(char ch, int j, int i)
{
     int x,y;
	 for(x = 0; x < j; x++)
	 {
          for(y = 0; y < i; y++)
		  {
			  printf("%c",ch);
		  }

          putchar('\n');
	 }




}

12. 两数的调和平均数这样计算:先得到两数的倒数,然后计算两个倒数的平均值,最后取计算结果的倒数。编写一个函数,接受两个double类型的参数,返回这两个参数的调和平均数。

double average(double x, double y)
{
   if(x == 0 || y == 0)
   {
	   printf("paramter error!\n");
	   return ;
   }

   return 1/((1/x + 1/y)/2);
    
}

13.编写并测试一个函数larger_of(),该函数把两个double类型变量的值替换为较大的值。例如,larger_of(x,y)会把x和y中较大的值重新赋给两个变量。

#include<stdio.h>

void larger_of(double*,double*);


int main(void)
{
    double x = 15.0;
	double y = 57.0;
	printf("Orignally x = %0.2f ,y = %0.2f\n",x,y);
	larger_of(&x,&y);
	printf("now x = %0.2f, y = %0.2f\n",x,y);


	return 0;
}


void larger_of(double* p1,double *p2)
{
	 double max;
     max = *p1 < *p2 ? *p2 : *p1;
     
     *p1 = max;
	 *p2 = max;

}

14.编写并测试一个函数,该函数以3个double变量的地址作为参数,把最小值放入第1个变量,中间值变量放入第2个变量,最大值放入第3个变量。

#include<stdio.h>

void in_order(double* p1, double* p2, double* p3)
{
      int min,max,sum;
      
	  sum = *p1 + *p2 + *p3;
	  max = min = *p1;

	  if(max < *p2)
	  {
		  max = *p2;
	  }
      if(max < *p3)
	  {
		  max = *p3;
	  }
      if(min > *p2)
	  {
		  min = *p2;
	  }
      if(min > *p1)
	  {
		  min = *p1;
	  }
      *p1 = min;
	  *p3 = max;
	  *p2 = sum - min -max;
  

}

int main(void)
{
	double x,y,z;
	x = 14.0;
	y = 5.0;
	z = 13.0;
    printf("Orignally x = %0.1f,y= %0.1f ,z= %0.1f\n",x,y,z);
    in_order(&x,&y,&z);
	printf("now x = %0.1f, y = %0.1f ,z = %0.1f\n",x,y,z);

 	return 0;
}



15.编写一个函数,从标准输入中读取字符,知道遇到文件结尾。程序要报告每个字符是否是字母。如果是,还要报告该字母在字母表中的数值位置。例如,c和C在字母表中的位置都是3。合并一个函数,以一个字符作为参数,如果该字符是一个字母则返回一个数值位置,否则返回-1.

#include<stdio.h>
#include<ctype.h>

int position(char ch);

int main(void)
{
    int ch;
    int pos_num;

    while( (ch = getchar()) != EOF)
	{
		 pos_num = position(ch);
		 if(pos_num == -1)
		 {
			 printf("该字符不是一个字母\n");
		 }
		 else
		 {
			 printf("该字母在字母表中的数值位置是%d\n",pos_num);
		 }

	}

    printf("Bye!\n");

	return 0;
}



int position(char ch)
{
    if(isalpha(ch))
	{
        if(islower(ch))
			ch -= 32;
		return (ch -64);

	}
    return -1;
}

16.为了让程序to_binary()函数更通用,编写一个to_base_n()函数接收两个参数,且第二个参数在2~10范围内,然后以第2个参数中指定的进制打印第1个参数的数值。例如,to_base_n(129,8)显示的结果为201,也就是129的八进制数,在一个完成的程序中测试该程序。

#include<stdio.h>

void to_base_n(int num,int n);

int main(void)
{

    to_base_n(129,8);
    putchar('\n');
	return 0;
}


void to_base_n(int num,int n)
{
    if( n>=2 && n<= 10)
	{
		int i;
		i = num%n;

		if(num >= n)
		{
			to_base_n(num/n,n);
		}

		printf("%d",i);
   
	}

}

   

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值