本章介绍以下内容:
关键字:return
运算符:*(一元)、&(一元)
函数及其定义方式
如何使用参数和返回值如何把指针变量用作函数参数
函数类型
ANSI C原型
递归
函数(function)是完成特定任务的独立程序代码单元。
#include <stdio.h>
#define SIZE 50
int main(void)
{
float list[SIZE];
readlist(list, SIZE);
sort(list, SIZE);
average(list, SIZE);
bargraph(list, SIZE);
return 0;
}
当然,还要编写4个函数readlist()、sort()、average()和bargraph()的实现细节。
创建并使用简单函数
/* lethead1.c */
#include <stdio.h>
#define NAME "GIGATHINK, INC."
#define ADDRESS "101 Megabuck Plaza"
#define PLACE "Megapolis, CA 94904"
#define WIDTH 40 void starbar(void); /* 函数原型 */
int main(void)
{
starbar();
printf("%s\n", NAME);
printf("%s\n", ADDRESS);
printf("%s\n", PLACE);
starbar(); /* 使用函数 */
return 0;
}
void starbar(void) /* 定义函数 */
{
int count;
for (count = 1; count <= WIDTH; count++)
putchar('*');
putchar('\n');
}
分析程序
函数原型(function prototype)告诉编译器函数starbar()的类型;
函数调用(function call)表明在此处执行函数;
函数定义(function definition)明确地指定了函数要做什么。
void starbar(void);
圆括号表明starbar是一个函数名。
第1个void是函数类型,void类型表明函数没有返回值。
第2个void(在圆括号中)表明该函数不带参数。
分号表明这是在声明函数,不是定义函数。
函数参数
定义带形式参数的函数
函数定义从下面的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要求在每个变量前都声明其类型。
声明带形式参数函数的原型
在使用函数之前,要用ANSI C形式声明函数原型:
void show_n_char(char ch, int num);
调用带实际参数的函数
在函数调用中,实际参数(actual argument,简称实参)提供了ch和num 的值。
show_n_char(SPACE, 12);
使用return从函数中返回值
int imin(int n, int m)
{
int min;
if (n < m)
min = n;
else
min = m;
return min;
}
函数类型
声明函数时必须声明函数的类型。带返回值的函数类型应该与其返回值类型相同,而没有返回值的函数应声明为void类型。
如果没有声明函数的类型,旧版本的C编译器会假定函数的类型是int。这一惯例源于C的早期,那时的函数绝大多数都是int类型。然而,C99标准不再支持int类型函数的这种假定设置。
类型声明是函数定义的一部分。要记住,函数类型指的是返回值的类型,不是函数参数的类型。
递归
C允许函数调用它自己,这种调用过程称为递归(recursion)。
结束递归是使用递归的难点,因为如果递归代码中没有终止递归的条件测试部分,一个调用自己的函数会无限递归。
可以使用循环的地方通常都可以使用递归。
演示递归
/* 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
}
递归的基本原理
第1,每级函数调用都有自己的变量。
第2,每次函数调用都会返回一次。
第3,递归函数中位于递归调用之前的语句,均按被调函数的顺序执行。
第4,递归函数中位于递归调用之后的语句,均按被调函数相反的顺序执行。
第5,虽然每级递归都有自己的变量,但是并没有拷贝函数的代码。
最后,递归函数必须包含能让递归调用停止的语句。
尾递归
最简单的递归形式是把递归调用置于函数的末尾,即正好在 return 语句之前。这种形式的递归被称为尾递归(tail recursion),因为递归调用在函数的末尾。尾递归是最简单的递归形式,因为它相当于循环。
// factor.c -- 使用循环和递归计算阶乘
#include <stdio.h>
long fact(int n);
long rfact(int n);
int main(void)
{
int num;
printf("This program calculates factorials.\n");
printf("Enter a value in the range 0-12 (q to quit):\n");
while (scanf("%d", &num) == 1)
{
if (num < 0)
printf("No negative numbers, please.\n");
else if (num > 12)
printf("Keep input under 13.\n");
else
{
printf("loop: %d factorial = %ld\n", num, fact(num));
printf("recursion: %d factorial = %ld\n", num, rfact(num));
}
printf("Enter a value in the range 0-12 (q to quit):\n");
}
printf("Bye.\n");
return 0;
}
long fact(int n) // 使用循环的函数
{
long ans;
for (ans = 1; n > 1; n--)
ans *= n;
return ans;
}
long rfact(int n) // 使用递归的函数
{
long ans;
if (n > 0)
ans = n * rfact(n - 1);
else ans = 1;
return ans;
}
递归的优缺点
优点是递归为某些编程问题提供了最简单的解决方案。缺点是一些递归算法会快速消耗计算机的内存资源。
编译多源代码文件的程序
使用多个函数最简单的方法是把它们都放在同一个文件中,然后像编译只有一个函数的文件那样编译该文件即可。
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不变,可以使用以下命令编译第1个文件,并与第2个文件的目标代码合并:
cc file1.c file2.o
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
Windows和苹果的IDE编译器
项目(project)描述的是特定程序使用的资源。
资源包括源代码文件。
这种IDE中的编译器要创建项目来运行单文件程序。对于多文件程序,要使用相应的菜单命令,把源代码文件加入一个项目中。要确保所有的源代码文件都在项目列表中列出。许多IDE都不用在项目列表中列出头文件(即扩展名为.h的文件),因为项目只管理使用的源代码文件,源代码文件中的#include指令管理该文件中使用的头文件。但是,Xcode要在项目中添加头文件。
使用头文件
如果把main()放在第1个文件中,把函数定义放在第2个文件中,那么第1个文件仍然要使用函数原型。把函数原型放在头文件中,就不用在每次使用函数文件时都写出函数的原型。
把#define 指令放进头文件,然后在每个源文件中使用#include指令包含该文件即可。
#include "filename.h"
查找地址:&运算符
指针(pointer)是 C 语言最重要的(有时也是最复杂的)概念之一,用于储存变量的地址。
一元&运算符给出变量的存储地址。如果pooh
是变量名,那么&pooh
是变量的地址。
printf("%d %p\n", pooh, &pooh);
/* 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; /* mikado()的局部变量 */
printf("In mikado(), pooh = %d and &pooh = %p\n", pooh, &pooh);
printf("In mikado(), bah = %d and &bah = %p\n", bah, &bah);
}
更改主调函数中的变量
/* swap1.c -- 第1个版本的交换函数 */
#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.\n", x, y);
return 0;
}
void interchange(int u, int v) /* 定义函数 */
{
int temp;
temp = u;
u = v;
v = temp;
}
两个变量的值并未交换!
指针简介
指针(pointer)是一个值为内存地址的变量(或数据对象)。
假设一个指针变量名是ptr,可以编写如下语句:
ptr = &pooh; // 把pooh的地址赋给ptr
ptr和&pooh的区别是ptr是变量, 而&pooh是常量。
要创建指针变量,先要声明指针变量的类型。假设想把ptr声明为储存int类型变量地址的指针,就要使用下面介绍的新运算符: *。
间接运算符:*
使用间接运算符*(indirection operator)找出储存在bah中的值,该运算符有时也称为解引用运算符(dereferencing operator)。
ptr = &bah
val = *ptr; // 找出ptr指向的值
语句ptr = &bah;
和val = *ptr;
放在一起相当于下面的语句:
val = bah;
声明指针
int * pi; // pi是指向int类型变量的指针
char * pc; // pc是指向char类型变量的指针
float * pf, * pg; // pf、pg都是指向float类型变量的指针
*和指针名之间的空格可有可无。通常,程序员在声明时使用空格,在解引用变量时省略空格。