第九章 函数(3)

发布于 2021-08-22  694 次阅读


9.4 编译多源码文件的程序

爱自己是终身浪漫的开始
使用多个函数最简单的方法是把他们都放在同一个文件中,然后像编译值有一个函数的文件那样编译该文件即可。

9.4.1 UNIX

假定再UNIX系统中安装了UNIX C编译器cc(最初的cc已经停用,但是许多UNIX系统都给cc命令起了一个别名用作其他编译命令,典型的是gcc或clang)。
假设file1.c和file2.c是两个内含C函数的文件,下面的命令将编译两个文件并生成一个名为a.out的可执行文件。
cc file1.c file2.c
另外还生成两个名为file1.o和file2.o的目标文件。如果后来改动了file1.c而file2.c不变,可以用下面命令编译第一个文件,并与第二个文件的目标代码合并;
cc file1.c file2.o
UNIX系统的make命令可以自动管理多文件程序。

9.4.2 Linux

假定Linux系统安装了GUN C编译器GCC。
假设file1.c和file2.c是两个内含C函数的文件,下面的命令将编译两个文件并生成一个名为a.out的可执行文件。
gcc file1.c file2.c
另外还生成两个名为file1.o和file2.o的目标文件。如果后来改动了file1.c而file2.c不变,可以用下面命令编译第一个文件,并与第二个文件的目标代码合并;
gcc file1.c file2.o

9.4.3 DOS命令行编译器

与UNIX类似

9.4.4 Windows和苹果IDE

他们的系统使用的继承开发环境中的编译器都是面向项目的。
项目(project)描述的是特定程序使用的资源。
资源包括源代码文件。
这种IDE中的编辑要创建项目来运行单文件程序。
对于多文件程序,要使用相应的菜单命令,把源代码文件加入一个项目中,要确保所有的源代码文件都再项目列表中列出。
许多IDE都不用再项目列表中列出头文件(即扩展名.h的文件)因为项目之管理使用的源代码文件,源代码文件中的#include指令管理该文件中使用的头文件,但是Xcode要再项目中添加头文件

9.4.5 使用头文件

如果把main()放在第1个文件中,把指定函数定义放在第2个文件中,那么第1个文件仍然要使用函数原型。把函数原型放在头文件中,就不用每次使用函数文件时都写出函数原型。C标准库就是这样做的。例如,把I/O函数原型放在stdio.h中,把数学函数原型放在math.h中。
另外程序中经常用到C预处理器定义符号常量。这种定义只存储了那些包含#define指令的文件。如果要把程序的一个函数放进一个独立的文件夹中,你也可以使用#define指令访问每个文件夹。
最直接的方法是在每个文件夹中再次输入指令,但是这个方法既耗费事件又容易出错。还有维护问题。更好的做法,把#define指令放进头文件,然后再每个源文件中使用#include指令包含该文件即可。
把函数原型和已定义的字符常量放在头文件中是一个良好的编程习惯。
Q:假设要管理4家酒店的客服服务,每家酒店的房价不同,但是每家酒店所有房间的房价相同。对于预定多天的客户,第2天收费是第1天的95%,第3天是第2天的95%,以此类推。设计一个程序让用户指定酒店和入住天数,然后计算并显示总费用。
同时,程序要实现一份菜单,允许用户反复输入数据,除非用户选择退出。
//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)qiut\n");
    printf("\n%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;
 } 

 int 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;
    double factor = 1.0;
    for(n=1;n<=nights;n++,factor = factor *DISCOUNT)
    {
        total = total+rate*factor;
     }
     printf("The total cost will be $%0.2f.\n",total);
 }
 //usehotel.c       --房间费率程序 
#include<stdio.h>
#include"hotel.h"

int main(void)
{
    int nights;
    double hotel_rate;
    int code;

    while((code = menu())!=QUIT)
    {
        switch(code)
        {
            case 1:hotel_rate = HOTEL1;
                  break;
            case 2:hotel_rate = HOTEL2;
                  break;
            case 3:hotel_rate = HOTEL3;
                  break;
            case 4:hotel_rate = HOTEL4;
                  break;
            default:hotel_rate = 0.0;
                  printf("Oops!\n");
                  break;

        }
        nights = getnights();
        showprice(hotel_rate,nights);

    }
    printf("Thank you and goodbye!\n");

    return 0;
}
//hotel.h       符号常量和hotel.c中的所有函数的原型
#define QUIT    5
#define HOTEL1  180.00 
#define HOTEL2  225.00
#define HOTEL3  225.00
#define HOTEL4  355.00
#define DISCOUNT    0.95
#define STARS   "**************************"

//显示选择列表

int menu(void);

//返回预定天数

int getnights(void);

//根据费率、入住天数计算费用
void showprice(double rate,int nights);

file

scanf((staus = scanf("%d",&code))!=||(code<1||code>5))
以上代码利用了C语言的两个规则,从左到右对逻辑表达式求值:一旦求值结果为假,立即停止求值。在该例中,只有scanf()在成功读入一个帧数值后,才会检查code的值。

9.5 查找地址:&运算符

指针(pointer)是C语言最重要的概念之一,用于存储变量的地址。前面使用的scanf()函数就使用地址作为参数。概括的说,如果主调函数不使用return返回的值,则必须通过地址才能修改主调函数中的值。
一元&运算符给出的变量的存储地址。如果pooh是变量名,那么&pooh是变量的地址。可以把地址看作是变量在内存的位置。
假设:
pooh = 24;
 pooh的存储地址是0B76
 那么
 printf("%d %p",pooh,&pooh);
 为:
 24 0B76
//loccheck.c        --查看变量被存储在何处
#include<stdio.h>
void mikado(int);
int main(void)
{
    int pooh = 2,bah = 5;   //main()局部变量

    printf("In main(),pooh = %d and &pooh = %p\n",pooh,&pooh);
    printf("In main(),bah  = %d and &bah  = %p\n",bah,&bah);
    mikado(pooh);

    return 0; 
 } 
 void mikado(int bah)
 {
    int pooh = 10;
    printf("In main(),pooh = %d and &pooh = %p\n",pooh,&pooh);
    printf("In main(),bah  = %d and &bah  = %p\n",bah,&bah);
 }

file

两个pooh地址不同,两个bah的地址也不同。因此,和前面介绍的一样,计算机把它们看作4个独立的变量。
其次,函数调用mikado(pooh)把实际参数(main()中的pooh)的值(2)传递给了形式参数(mikado中的bah)。这种传递只传递了值,涉及到两个变量(main()pooh和,mikado()中的bah)并未改变。

9.6 更改主调函数中的变量

有时需要在一个函数中更改其他函数的变量。
例如,普通的排序任务中交换两个变量的值。假设要交换两个变量x和y的值。简单思路:
x=y;
y=x;
显然不行:
temp = x;
x = y;
y = temp;
//swap1.c   --第一个版本的交换函数
 #include<stdio.h>
 void interchange(int u,int v);
 int main(void)
 {
    int x=5,y=10;

    printf("Originally x = %d and y = %d.\n",x,y);
    interchange(x,y);
    printf("Now x = %d and y = %d",x,y);

    return 0;
  } 
  void interchange(int u, int v)
  {
    int temp;

    temp = u;
    u = v;
    v = temp;

  }

file

两个值并未交换
//swap1.c   --第一个版本的交换函数
 #include<stdio.h>
 void interchange(int u,int v);
 int main(void)
 {
    int x=5,y=10;

    printf("Originally x = %d and y = %d.\n",x,y);
    interchange(x,y);
    printf("Now x = %d and y = %d",x,y);

    return 0;
  } 
  void interchange(int u, int v)
  {
    int temp;

    printf("Originally u = %d and v = %d.\n",u,v);

    temp = u;
    u = v;
    v = temp;
    printf("Now x = %d and y = %d\n",u,v);
  }

file

使用return语句只能把被调函数中的一个值返回主调函数。

9.7 指针简洁

从根本上看,指针就是一个值为内存地址的变量。正如char 类型变脸的值是字符,int是类型的变变量的值是整数,,,指针变量的值是地址。
如何把指针作为函数参数使用,以及为什么要这么用,
假设一个指针变量名为ptr。可以编写如下语句:
ptr = &pooh;    //把破哦好的地址赋给ptr
ptr指向pooh。ptr和&pooh的区别是ptr是变量,而&pooh是常量。
ptr是可修改的左值,而&pooh是右值。
要创建指针变量,先要声明指针变量的类型。
假设要把ptr声明成为存储int类型的变量的指针

9.7.1 间接运算符:*

假设已知ptr指向bah,
ptr = &bah;
然后使用间接运算符*(indirection operator)找出存储在bah中的值,该运算符又是也称为解引用运算符(dereferencing operator)。
val = *ptr;
相当于:
val = bah;
使用地址和间接运算符可以间接的完成上述功能。

小结:与指针相关的运算符

地址运算符:&
一般注解:后跟一个变量名时,&给出该变量的地址。
示例:
&nurse表示nurse的地址

地址运算符:*
一般注解:后跟一个指针名或地址时,*给出存储在指针指向地址上的值。
示例:
nurse = 22;
ptr = &nurse;
val = *ptr;//把ptr指向地址上的值赋给val
把22赋给val。

9.7.2 声明指针

声明指针变量时必须指定指针所指向的变量的类型。
不同变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小。程序必须知道存储在只当地址上的数据类型。
long和float可能占用相同的存储空间,但是它们存储数字却大致相同。
int *pi;    //pi是指向int类型变量的指针
char *pc;
float *pf,*pg;
类型说明符表明了指针所指向对象的类型,星号(*)表明声明的变量是一个指针。int *p;
声明的意思是pi是一个指针,*pi是int类型。

file

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

9.7.3 使用指针在函数间通信

//swap3.c   --使用指针解决交换函数的问题
#include<stdio.h>
void interchange(int *u,int *v);
int main(void)
{
    int x = 2,y = 5;    //main()局部变量

    printf("In main(),x = %d and y = %d\n",x,y);
    interchange(&x,&y);
    printf("Now x = %d, y = %d\n",x,y);

    return 0; 
 } 
 void interchange(int *u,int *v)
 {
    int temp;
    temp = *u;
    *u = *v;
    *v = temp;

 }

file

该函数传递的不是x和y的值,而实它们的地址。这意味着出现在interchange()原型和定义中的形式参数u和v将地址作为它们的值。。
。。。
。。。
一般而言,可以把变量相关的两类信息传递给函数。如果这种形式的函数调用,那么传递的是x的值。
function1(x);
传递x的地址
function2(&x);
第一种形式要求函数定义中的形式参数必须是一个与x的类型相同的变量:
int function1(int num)
第2种形式要求函数定义种的形式参数必须是指向一个正确类型的指针:
int function2(int *p)
如果要计算或处理值,那么使用第1种形式的函数调用:如果要在被调函数种改变主调函数的变量,则使用第二种形式的函数调用。我们用过的scanf()函数就是这样。当程序要把一个值读入变量时,调用的是scanf(),scanf()就是读取一个值,然会把该值存储到指定的地址上。
对本例而言,指针是让interchange()函数通过自己的局部变量改变main()种的变量。

file

变量:名称、地址和值
编写程序时,可以认为变量有两个属性:名称和值。计算机编译加载程序后,认为变量也有两个属性:地址和值,地址就是变量在计算机内部的名称。
pbarn = &pbarn;那么*p表示的是存储在&barn地址上的值。
普通变量把值作为基本量,把地址作为通过&运算符获得的派生量,而指针把地址作为基本量,把值作为通过*运算符获得的派生量。
打印地址不是主要用途,更重要的是使用&、*和指针
可以操纵地址和地址上的内容。
 小结 
形式:
典型的ANSI C函数的定义形式为:
返回类型 名称(形参声明列表)
函数体
形参声明表是用逗号分隔的一系列变量声明。除形参变量外,函数的其他变量均在函数体的花括号之内声明。
例如:
int diff(int x,int y)   //ANSI C
{
    //函数体
    int z;
    z= x-y;
    return z;   //返回一个值
}
传递值:
实参用于把值从主调函数传递给被调函数。
如果变量是a和b的值分别是5和2
那么:c=diff(a,b);
把5和2分别传递给变量x和变量y。5和2称为实际参数。diff()函数定义种的变量x和y称为形式参数。使用return把被调函数种的一个值传回主调函数。
本例中,c接受z的值3.被调函数一般不会改变主调函数中的变量,如果要改变,应使用指针作为参数。如果希望把更多的值传回主调函数,必须这么做。
函数的返回类型:
函数的返回类型指的是函数返回值的类型。如果返回值的类型与声明的返回类型不匹配,返回值将被转换成函数声明的返回类型。
函数签名:
函数的返回类型和形参列表构成了函数签名。因此,函数签名制定了传入函数的值的类型和函数返回值的类型。
示例:
double duff(double,int) //函数原型
int main(void)
{
    double q,x;
    int n;
    ...
    q=duff(x,n);    //调用函数
    ...

}
double duff(double u,int k) //函数定义
{
    double tor;
    ..
    return tor; //返回double型的值
}

9.8 关键概念

如果想要用C编出高效灵活的程序,必须理解函数。把大型程序组织成若干函数非常有用,甚至很关键。
如果让一个函数处理一个任务,程序会更好理解,更方便调试。要理解函数是如何把信息从一个函数传递到另一个函数,理解函数参数和返回值的工作原理。
要明白函数形参合其他局部变量都属于函数私有,声明在不同函数中的同名变量是完全不同的变量。
而且,函数无法直接访问其他函数中的变量。
这种限制保护了数据的完整性。当确实需要在函数中访问一个函数的数据时,可以把指针作为函数的参数。

9.9 本章小结

函数可以作为组成大型程序的构件块。每个函数都应该有一个单独且定义好的功能。使用参数把值传给函数,使用关键字return把值返回函数。
如果函数返回值不是int类型,则必须在函数定义和函数原型中指定函数的类型,如果需要在被调函数中修改主调函数的变量,使用地址或指针作为参数。
ANSI  C提供了一个强大的工具--函数原型,允许编译器验证函数调用中使用的参数个数和类型是否正确。
C函数可以调用本身,这种调用方式被称为递归。
一些编程问题要用递归来解决问题,但是递归不仅消耗内存多,效率不高,而且费时。。

擦肩而过的概率