第八章 函数

本章介绍以下内容:
关键字: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类型变量的指针
*和指针名之间的空格可有可无。通常,程序员在声明时使用空格,在解引用变量时省略空格。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容

  • 一、函数基础知识。 1、函数的形式: (1)无参函数:类型标识符 函数名(){声明部分 语句部分}; (2)有参函...
    逆流而上jiao阅读 138评论 1 1
  • (五)函数模板 1.函数模板的使用:属于泛型编程的一种 函数模板,template<typename AnyTy...
    阿厉a_li阅读 289评论 1 5
  • C++内联函数 要使用这种特性,必须采取下述措施之一: 在函数声明钱加上关键字inline; 在函数定义钱加上关键...
    鬼枭嗜阅读 391评论 0 0
  • c++赋予了一些新的关于函数的特性,包括,内联函数,按引用传递变量,默认的参数值,函数重载(多态),以及模板函数...
    阿厉a_li阅读 269评论 2 6
  • 函数 函数是执行特定任务的自包含代码块。给定函数一个名称作为标识,并在需要的时候通过调用其名称来执行任务。 Swi...
    BoyceC阅读 423评论 0 1