第9章 函数

9.1 复习函数

函数(function)是完成特定任务的独立代码单元。

函数让程序更加模块化,提高代码可读性,方便修改。

9.1.1 创建并使用简单函数

函数原型指明了函数的返回值类型和函数接受的参数类型。这些信息称为该函数的签名(signature)

9.1.3 函数参数

9.1.4 定义带形式参数的函数

ANSI C风格的函数头:

void show_n_char(char ch, int num)

改行告知编译器本函数具有两个参数ch和num,类型分别是char和int。这两个参数被称为形式参数(formal parameter),简称形参。形式参数是局部变量。

注意每个变量前都要声明其类型,不能写成这样:void dibs(int x, y, z)

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

函数原型一般是下面这种形式:

void show_n_char(char ch, int num)

也可以省略变量名:

void show_n_char(char, num)

9.1.6 调用带实际参数的函数

函数调用中,实际参数(actual argument,简称实参)提供了ch和num的值。

show_n_char(SPACE,12)

调用时实参的值(SPACE,12)赋值给函数中相应的形式参数(ch,num)。

9.1.7 黑盒视角

9.1.8 使用return从函数中返回值

9.1.9 函数类型

声明函数时必须声明函数的类型,带返回值的函数类型和其返回值类型相同,

不带返回值的函数应该声明为void类型。

9.2 ANSI C函数原型

9.2.1 问题所在

9.2.2ANSI的解决方案

针对参数不匹配的问题,ANSI C要求函数声明时还要声明变量的类型,即 使用函数原型来声明函数的返回类型、参数的数量和每个参数的类型。有了这些信息,编译器可以检查函数调用是否和函数原型匹配。

9.2.3 无参数和未指定参数

为了表明函数没有参数,应该在圆括号内使用void

void print_name(void);

一些函数接受许多参数,ANSI C允许使用部分原型:

int printf(const char *, …)

表明第一个参数是字符串,可能还有其他参数。

9.2.4 函数原型的优点

让编译器在第1次执行到该函数前就知道如何使用它。

将整个函数定义放在第1次调用该函数之前,也有同样的效果。

此时,函数定义也相当于函数原型。

9.3 递归
关于递归的概念,我们都不陌生。简单的来说递归就是一个函数直接或间接地调用自身,是为直接或间接递归。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。用递归需要注意以下两点:(1) 递归就是在过程或函数里调用自身。(2) 在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。
递归一般用于解决三类问题:
   (1)数据的定义是按递归定义的。(Fibonacci函数,n的阶乘)
   (2)问题解法按递归实现。(回溯)
   (3)数据的结构形式是按递归定义的。(二叉树的遍历,图的搜索)
递归的缺点:
  递归解题相对常用的算法如普通循环等,运行效率较低。因此,应该尽量避免使用递归,除非没有更好的算法或者某种特定情况,递归更为适合的时候。在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,因此递归次数过多容易造成栈溢出。
  用线性递归实现Fibonacci函数,程序如下所示:
int FibonacciRecursive(int n)
{
if( n < 2)
return n;
return (FibonacciRecursive(n-1)+FibonacciRecursive(n-2));
}

//用普通的递归计算Fibonacci数列:

#include “stdio.h”
#include “math.h”

int factorial(int n);

int main(void)
{
int i, n, rs;
printf(“请输入斐波那契数n:”);
scanf("%d",&n);

rs = factorial(n);
printf("%d \n", rs);return 0;

}

// 递归
int factorial(int n){
if(n <= 2) {
return 1;
}
else {
return factorial(n-1) + factorial(n-2);
}
}
/*http://www.nowamagic.net/librarys/veda/detail/2317
wannian07@wannian07-PC:~/Desktop/Beginning C 5TH$ ./maopao
请输入斐波那契数n:20
6765
*/
一个典型的递归例子是对已排序数组的二分查找算法。
现在有一个已经排序好的数组,要在这个数组中查找一个元素,以确定它是否在这个数组中,很一般的想法是顺序检查每个元素,看它是否与待查找元素相同。这个方法很容易想到,但它的效率不能让人满意,它的复杂度是O(n)的。现在我们来看看递归在这里能不能更有效。
还是考虑上面的两个条件:
第一:这个问题是否可以分解为形式相同但规模更小的问题?
第二:如果存在这样一种分解,那么这种分解是否存在一种简单情境?

#include “stdio.h”
#include “stdlib.h”

void selectionSort(int data[], int count);
int binary_search(int *a, int n, int key);

int main()
{
int i, key, rs;
int arr[10];
int count;

printf("排序前数组为:");
srand((int)time(0));
for(i=0; i < 10; i++)
{arr[i] = rand()%100;printf("%d ",arr[i]);
}count = sizeof(arr)/sizeof(arr[0]);
selectionSort(arr, count);printf("\n排序后数组为:");
for(i=0; i < 10; i++)
{printf("%d ", arr[i]);
}printf("\n请输入要查找的数字:");
scanf("%d",&key);rs = binary_search(arr, 10, key);
printf("%d ", rs);

}

void selectionSort(int data[], int count)
{
int i, j, min, temp;
for(i = 0; i < count; i ++) {
/find the minimum/
min = i;
for(j = i + 1; j < count; j ++)
if(data[j] < data[min])
min = j;
temp = data[i];
data[i] = data[min];
data[min] = temp;
}
}

int binary_search(int *data, int n, int key)
{
int mid;
if(n == 1){
return (data[0] == key);
}else{
mid = n/2;
printf(“mid=%d\n”, data[mid]);
if(data[mid-1] == key)
return 1;
else if(data[mid-1] > key)
{
printf(“key %d 比 data[mid-1] %d 小,取前半段 \n”, key, data[mid-1]);
return binary_search(&data[0], mid, key);
}
else
{
printf(“key %d 比 data[mid-1] %d 大,取后半段 \n”, key, data[mid-1]);
return binary_search(&data[mid], n - mid, key);
}
}
}
/*http://www.nowamagic.net/librarys/veda/detail/2317
wannian07@wannian07-PC:~/Desktop/Beginning C 5TH$ ./maopao
排序前数组为:71 64 88 51 77 78 75 54 9 51
排序后数组为:9 51 51 54 64 71 75 77 78 88
请输入要查找的数字:20
mid=71
key 20 比 data[mid-1] 64 小,取前半段
mid=51
key 20 比 data[mid-1] 51 小,取前半段
mid=51
key 20 比 data[mid-1] 9 大,取后半段
http://www.nowamagic.net/librarys/veda/detail/2317
*/
9.3.3 尾递归

尾递归就是从最后开始计算, 每递归一次就算出相应的结果, 也就是说, 函数调用出现在调用者函数的尾部, 因为是尾部, 所以根本没有必要去保存任何局部变量. 直接让被调用的函数返回时越过调用者, 返回到调用者的调用者去。尾递归就是把当前的运算结果(或路径)放在参数里传给下层函数,深层函数所面对的不是越来越简单的问题,而是越来越复杂的问题,因为参数里带有前面若干步的运算路径。
  尾递归是极其重要的,不用尾递归,函数的堆栈耗用难以估量,需要保存很多中间函数的堆栈。比如f(n, sum) = f(n-1) + value(n) + sum; 会保存n个函数调用堆栈,而使用尾递归f(n, sum) = f(n-1, sum+value(n)); 这样则只保留后一个函数堆栈即可,之前的可优化删去。
  采用尾递归实现Fibonacci函数,程序如下所示:
int FibonacciTailRecursive(int n,int ret1,int ret2)
{
if(n==0)
return ret1;
return FibonacciTailRecursive(n-1,ret2,ret1+ret2);
}

#include <stdio.h>
#include <stdlib.h>

typedef struct node
{
int data;
struct node* next;
}node,*linklist;

void InitLinklist(linklist* head)
{
if(*head != NULL)
free(*head);
head = (node)malloc(sizeof(node));
(*head)->next = NULL;
}

void InsertNode(linklist* head,int d)
{
node* newNode = (node*)malloc(sizeof(node));
newNode->data = d;
newNode->next = (*head)->next;
(*head)->next = newNode;
}

//直接递归求链表的长度
int GetLengthRecursive(linklist head)
{
if(head->next == NULL)
return 0;
return (GetLengthRecursive(head->next) + 1);
}
//采用尾递归求链表的长度,借助变量acc保存当前链表的长度,不断的累加
int GetLengthTailRecursive(linklist head,int *acc)
{
if(head->next == NULL)
return *acc;
*acc = *acc+1;
return GetLengthTailRecursive(head->next,acc);
}

void PrintLinklist(linklist head)
{
node* pnode = head->next;
while(pnode)
{
printf("%d->",pnode->data);
pnode = pnode->next;
}
printf("->NULL\n");
}

int main()
{
linklist head = NULL;
int len = 0;
InitLinklist(&head);
InsertNode(&head,10);
InsertNode(&head,21);
InsertNode(&head,14);
InsertNode(&head,19);
InsertNode(&head,132);
InsertNode(&head,192);
PrintLinklist(head);
printf(“The length of linklist is: %d\n”,GetLengthRecursive(head));
GetLengthTailRecursive(head,&len);
printf(“The length of linklist is: %d\n”,len);
system(“pause”);
}
/*wannian07@wannian07-PC:~/Desktop/Beginning C 5TH$ ./maopao
192->132->19->14->21->10->->NULL
The length of linklist is: 6
The length of linklist is: 6
sh: 1: pause: not found
*/
用尾递归实现斐波那契数。

#include “stdio.h”
#include “math.h”

int factorial_tail(int n,int acc1,int acc2);

int main(void)
{
int i, n, rs;
printf(“请输入斐波那契数n:”);
scanf("%d",&n);

rs = factorial_tail(n, 1, 1);
printf("%d\n ", rs);return 0;

}

int factorial_tail(int n,int acc1,int acc2){
if (n < 2){
return acc1;
}
else{
return factorial_tail(n-1,acc2,acc1+acc2);
}
}
/*
wannian07@wannian07-PC:~/Desktop/Beginning C 5TH$ ./maopao
请输入斐波那契数n:20
6765
*/
http://www.nowamagic.net/librarys/veda/detail/2336
尾递归,很多人的理解仅局限于它是递归和尾调用的一个合体,比普通递归效率高。至于效率为什么高,高在哪,可能没有深究过。 
尾调用
要说尾递归,得先说尾调用。我理解的尾调用大概是这么一种情况:
1.函数A里面调用了函数B。
2.函数B执行后,函数A马上返回。
3.也就是说调用函数B(并返回执行结果)是函数A所做的最后一件事。
4.相当于执行完函数B后,函数A也就执行完。
因此在执行函数B时,函数A的栈帧其实是已经大部分没用了,可以被修改或覆盖。编译器可以利用这一点进行优化,函数B执行后直接返回到函数A的调用者。 
这里有一点需要注意:它是来自于编译器的优化。 这一点点的优化对于普通的尾调用来说可能意义不大,但是对于尾递归来说就很重要了。 
尾递归
尾递归是一种基于尾调用形式的递归,相当于前面说的函数B就是函数A本身。 
普通递归会存在的一些问题是,每递归一层就会消耗一定的栈空间,递归过深还可能导致栈溢出,同时又是函数调用,免不了push来pop去的,消耗时间。 
采用尾调用的形式来实现递归,即尾递归,理论上可以解决普通递归的存在的问题。因为下一层递归所用的栈帧可以与上一层有重叠(利用jmp来实现),局部变量可重复利用,不需要额外消耗栈空间,也没有push和pop。 
再次提一下,它的实际效果是来自于编译器的优化(目前的理解)。在使用尾递归的情况下,编译器觉得合适就会将递归调用优化成循环。目前大多数编译器都是支持尾递归优化的。有一些语言甚至十分依赖于尾递归(尾调用),比如erlang或其他函数式语言(传说它们为了更好的处理continuation-passing style)。 
假如不存在优化,大家真刀真枪进行函数调用,那尾递归是毫无优势可言的,甚至还有缺点——代码写起来不直观。 
现代编译器的优化能力很强悍,很多情况下编译器优化起来毫不手软(于是有了volatile)。但有时编译器又很傲娇,你需要主动给它一点信号,它才肯优化。尾递归就相当于传递一个信号给编译器,尽情优化我吧! 
测试
为了验证尾递归优化,可以写个小程序进行测试。在VS2010下将使用/O1或以上的优化选项,一般就会尾递归优化。Gcc3.2.2(这版本好旧)下一般需要使用-O2优化选项。
先看看普通递归:
// 递归
int factorial(int n){
if(n <= 2){
return 1;
}
else{
return factorial(n-1) + factorial(n-2);
}
}
其汇编代码:
00401371 push %ebp
00401372 mov %esp,%ebp
00401374 push %ebx
00401375 sub $0x14,%esp
00401378 cmpl $0x2,0x8(%ebp)
0040137C jg 0x401385 <factorial+20>
0040137E mov $0x1,%eax
00401383 jmp 0x4013a4 <factorial+51>
00401385 mov 0x8(%ebp),%eax
00401388 dec %eax
00401389 mov %eax,(%esp)
0040138C call 0x401371
00401391 mov %eax,%ebx
00401393 mov 0x8(%ebp),%eax
00401396 sub $0x2,%eax
00401399 mov %eax,(%esp)
0040139C call 0x401371
004013A1 lea (%ebx,%eax,1),%eax
004013A4 add $0x14,%esp
004013A7 pop %ebx
004013A8 leave
004013A9 ret
在0040138C,0040139C这些位置,我们看到递归仍然是使用call指令,那么尾递归在汇编角度是怎么处理的呢?
尾递归:
int factorial_tail(int n,int acc1,int acc2){
if (n < 2){
return acc1;
}
else{
return factorial_tail(n-1,acc2,acc1+acc2);
}
}
其汇编代码:
00401381 push %ebp
00401382 mov %esp,%ebp
00401384 sub $0x18,%esp
00401387 cmpl $0x1,0x8(%ebp)
0040138B jg 0x401392 <factorial_tail+17>
0040138D mov 0xc(%ebp),%eax
00401390 jmp 0x4013b2 <factorial_tail+49>
00401392 mov 0x10(%ebp),%eax
00401395 mov 0xc(%ebp),%edx
00401398 lea (%edx,%eax,1),%eax
0040139B mov 0x8(%ebp),%edx
0040139E dec %edx
0040139F mov %eax,0x8(%esp)
004013A3 mov 0x10(%ebp),%eax
004013A6 mov %eax,0x4(%esp)
004013AA mov %edx,(%esp)
004013AD call 0x401381 <factorial_tail>
004013B2 leave
004013B3 ret
在00401390位置上,尾递归是直接使用jmp实现循环跳转。

9.3.4 递归和倒序计算//https://zhuanlan.zhihu.com/p/33832791
递归问题中相对较难分析的题目应该是汉诺塔问题的输出问题
对于该问题的分析应该循序渐进
从细小的递归程序分析开始
逐渐掌握其中的零零散散的规律
下面给出了简单的递归过程分析以及使用递归程序解决多位数字倒叙输出的问题。

//递归过程分析
#include"stdio.h"void fun(int n){
if(n>1)
{
fun(n-1);
}
printf("%d\n",n);}
int main(){
int m;
scanf("%d",&m);
fun(m);}

#include"stdio.h"
void turn (long m)
{
if(m>10)
{
printf("%d",m%10);
turn(m/10);
}
else
printf("%d",m);
}
int main()
{
long n;
printf(“Input a number:”);
scanf("%d",&n);
printf(“The number is %d\n”,n);
printf(“The turn is :”);
turn(n);
}
wannian07@wannian07-PC:~/Desktop/Beginning C 5TH$ ./maopao
Input a number:123456789
The number is 123456789
The turn is :987654321

//https://www.cnblogs.com/YaLi/p/8695712.html
#include<stdio.h>
void print(char* str);
int main(void)
{
char str[100];
scanf("%s",str);
print(str);
printf("\n");
return 0;
}
void print(char* str)
{
char c = str;
if(c != ‘\0’)
print(str+1);
if(c != ‘\0’)
printf("%c",c);
}/

wannian07@wannian07-PC:~/Desktop/Beginning C 5TH$ ./maopao
IloveYou
uoYevolI

*/
回文是一种字符串,它正着读和反着读都是一样的。比如level,eye都是回文。用迭代的方法可以很快地判断一个字符串是否为回文。用递归的方法如何来实现呢?
首先我们要考虑使用递归的两个条件:
第一:这个问题是否可以分解为形式相同但规模更小的问题?
第二:如果存在这样一种分解,那么这种分解是否存在一种简单情境?
先来看第一点,是否存在一种符合条件的分解。容易发现,如果一个字符串是回文,那么在它的内部一定存在着更小的回文。 比如level里面的eve也是回文。 而且,我们注意到,一个回文的第一个字符和最后一个字符一定是相同的。
所以我们很自然的有这样的方法:
先判断给定字符串的首尾字符是否相等,若相等,则判断去掉首尾字符后的字符串是否为回文,若不相等,则该字符串不是回文。
注意,我们已经成功地把问题的规模缩小了,去掉首尾字符的字符串当然比原字符串小。
接着再来看第二点, 这种分解是否存在一种简单情境呢?简单情境在使用递归的时候是必须的,否则你的递归程序可能会进入无止境的调用。
对于回文问题,我们容易发现,一个只有一个字符的字符串一定是回文,所以,只有一个字符是一个简单情境,但它不是唯一的简单情境,因为空字符串也是回文。这样,我们就得到了回文问题的两个简单情境:字符数为1和字符数为0。
好了,两个条件都满足了,基于以上分析,我们可以很容易的编写出解决回文问题的递归实现方式:

#include “stdio.h”
#include “string.h”

int main(void)
{
int n, rs;
char str[50];

printf("请输入需要判断回文的字符串:");
scanf("%s",&str);n = (int)strlen(str);
rs = is_palindereme(str, n);
printf("%d ", rs);

}

int is_palindereme(char str, int n)
{
printf(“Length: %d \n”,n);
printf("%c ----- %c\n", str[0], str[n-1]);
if(n == 0 || n == 1)
return 1;
else{
//printf("%d, %d\n", str[0], str[n-1]);
return ((str[0] == str[n-1]) ? is_palindereme(str+1, n-2) : 0);
}
}
/

wannian07@wannian07-PC:~/Desktop/Beginning C 5TH$ ./maopao
请输入需要判断回文的字符串:level
Length: 5
l ----- l
Length: 3
e ----- e
Length: 1
v ----- v

*/
9.3.5 递归的优缺点
返回一个二叉树的深度:
int depth(Tree t){
if(!t) return 0;
else {
int a=depth(t.right);
int b=depth(t.left);
return (a>b)?(a+1):(b+1);
}
}
判断一个二叉树是否平衡:
int isB(Tree t){
if(!t) return 0;
int left=isB(t.left);
int right=isB(t.right);
if( left >=0 && right >=0 && left - right <= 1 || left -right >=-1)
return (left < right)? (right +1) : (left + 1);
else return -1;
}
第一个算法还是比较好理解的,但第二个就不那么好理解了。第一个算法的思想是:如果这个树是空,则返回0;否则先求左边树的深度,再求右边数的深度,然后对这两个值进行比较哪个大就取哪个值+1。而第二个算法,首先应该明白isB函数的功能,它对于空树返回0,对于平衡树返回树的深度,对于不平衡树返回-1。明白了函数的功能再看代码就明白多了,只要有一个函数返回了-1,则整个函数就会返回-1。(具体过程只要认真看下就明白了)
对于递归,最好的理解方式便是从函数的功能意义的层面来理解。了解一个问题如何被分解为它的子问题,这样对于递归函数代码也就理解了。这里有一个误区(我也曾深陷其中),就是通过分析堆栈,分析一个一个函数的调用过程、输出结果来分析递归的算法。这是十分要不得的,这样只会把自己弄晕,其实递归本质上也是函数的调用,调用的函数是自己或者不是自己其实没什么区别。在函数调用时总会把一些临时信息保存到堆栈,堆栈只是为了函数能正确的返回,仅此而已。我们只要知道递归会导致大量的函数调用,大量的堆栈操作就可以了。

http://www.nowamagic.net/librarys/veda/detail/2268

牛顿迭代法(Newton’s method)又称为牛顿-拉夫逊方法(Newton-Raphson method),它是牛顿在17世纪提出的一种在实数域和复数域上近似求解方程的方法。多数方程不存在求根公式,因此求精确根非常困难,甚至不可能,从而寻找方程的近似根就显得特别重要。方法使用函数f(x)的泰勒级数的前面几项来寻找方程f(x) = 0的根。牛顿迭代法是求方程根的重要方法之一,其最大优点是在方程f(x) = 0的单根附近具有平方收敛,而且该法还可以用来求方程的重根、复根。另外该方法广泛用于计算机编程中。
设r是f(x) = 0的根,选取x0作为r初始近似值,过点(x0,f(x0))做曲线y = f(x)的切线L,L的方程为y = f(x0)+f’(x0)(x-x0),求出L与x轴交点的横坐标 x1 = x0-f(x0)/f’(x0),称x1为r的一次近似值。
过点(x1,f(x1))做曲线y = f(x)的切线,并求该切线与x轴交点的横坐标 x2 = x1-f(x1)/f’(x1),称x2为r的二次近似值。重复以上过程,得r的近似值序列,其中x(n+1)=x(n)-f(x(n))/f’(x(n)),称为r的n+1次近似值,上式称为牛顿迭代公式。
根据牛顿迭代的原理,可以得到以下的迭代公式:X(n+1)=[X(n)+p/Xn]/2
一般性的编程方法如下:
double sqr(double n) {
double k=1.0;
while(abs(kk-n)>1e-9) {
k=(k+n/k)/2;
}
return k;
}
求n的平方根,先随便取一个不是0的数作为迭代开始的x(0),例如最简单的x(0)=1,然后反复代入x(k+1) = 0.5[x(k)+n/x(k)]求得下一个x,代入次数越多解约精确。
例如,2的平方根:
x(0) = 1
x(1) = (1/2)(1+2/1) = 3/2 = 1.5
x(2) = (1/2)[3/2+2/(3/2)] = 17/12 = 1.41666667
x(3) = (1/2)[17/12 + 2/(17/12)] = 577/408 = 1.41421568…
就这样,反复代入上式计算,得到的值越来越精确。
或者这么解释:
1.对x的平方根的值一个猜想y。
2.通过执行一个简单的操作去得到一个更好的猜测:只需要求出y和x/y的平均值(它更接近实际的平方根值)。
例如,可以用这样方式去计算2的平方根。
猜想 商 平均值
1  2/1=2  (2+1)/2 = 1.5
1.5  2/1.5=1.3333    (1.3333+1.5)/2 = 1.4167
1.4167  2/1.4167=1.4118   (1.4167+1.4118)/2=1.4142
1.4142      …          …
继续这一计算过程,我们就能得到对2的平方根的越来越好的近似值。
下面用C语言实现一遍:
#include “stdio.h”
#include “math.h”
int main(void){
double n,y=1.0;
printf(“请输入一个需要求其平方根的数:”);
scanf("%lf",&n);
// 反复代入 x(k+1) = 0.5[x(k)+n/x(k)]
while(fabs((1.0/2.0
(y+n/y))-y)>=0.00001){
y=1.0/2.0*(y+n/y);
printf( “y=%lf\n”, y );
}
printf(“平方根为%f\n”,y);
return 0;
}
程序运行结果:
请输入一个需要求其平方根的数:2
y=1.500000
y=1.416667
y=1.414216
平方根为1.414216

请输入一个需要求其平方根的数:3
y=2.000000
y=1.750000
y=1.732143
y=1.732051
平方根为1.732051
PS:Quake III公开源码后,有人在game/code/q_math.c里发现了这样一段代码。它的作用是将一个数开平方并取倒,经测试这段代码比(float)(1.0/sqrt(x))快4倍,有兴趣的可以研究一下。不过这是后话了,
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y;
i = 0x5f3759df - ( i >> 1 );
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) );
// y = y * ( threehalfs - ( x2 * y * y ) );
#ifndef Q3_VM
#ifdef linux
assert( !isnan(y) );
#endif
#endif
return y;
}
http://www.nowamagic.net/librarys/veda/detail/2268
9.4 编译多源代码文件的程序
/* c1.h (程序名) /
#include<string.h>
#include<ctype.h>
#include<malloc.h> /
malloc()等 /
#include<limits.h> /
INT_MAX等 /
#include<stdio.h> /
EOF(=^Z或F6),NULL /
#include<stdlib.h> /
atoi() /
#include<io.h> /
eof() /
#include<math.h> /
floor(),ceil(),abs() /
#include<process.h> /
exit() /
/
函数结果状态代码 /
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
/
#define OVERFLOW -2 因为在math.h中已定义OVERFLOW的值为3,故去掉此行 /
typedef int Status; /
Status是函数的类型,其值是函数结果状态代码,如OK等 /
typedef int Boolean; /
Boolean是布尔类型,其值是TRUE或FALSE */

/* c1-1.h 采用动态分配的顺序存储结构 */
typedef ElemType Triplet; / 由InitTriplet分配3个元素存储空间 /
/
Triplet类型是ElemType类型的指针,存放ElemType类型的地址 */

/* bo1-1.c 抽象数据类型Triplet和ElemType(由c1-1.h定义)的基本操作(8个) */
Status InitTriplet(Triplet T,ElemType v1,ElemType v2,ElemType v3)
{ /
操作结果:构造三元组T,依次置T的3个元素的初值为v1,v2和v3 */
*T=(ElemType )malloc(3sizeof(ElemType));
if(!*T)
exit(OVERFLOW);
(*T)[0]=v1,(*T)[1]=v2,(*T)[2]=v3;
return OK;
}

Status DestroyTriplet(Triplet T)
{ /
操作结果:三元组T被销毁 */
free(*T);
*T=NULL;
return OK;
}

Status Get(Triplet T,int i,ElemType e)
{ /
初始条件:三元组T已存在,1≤i≤3。操作结果:用e返回T的第i元的值 */
if(i<1||i>3)
return ERROR;
*e=T[i-1];
return OK;
}

Status Put(Triplet T,int i,ElemType e)
{ /* 初始条件:三元组T已存在,1≤i≤3。操作结果:改变T的第i元的值为e */
if(i<1||i>3)
return ERROR;
T[i-1]=e;
return OK;
}

Status IsAscending(Triplet T)
{ /* 初始条件:三元组T已存在。操作结果:如果T的3个元素按升序排列,返回1,否则返回0 */
return(T[0]<=T[1]&&T[1]<=T[2]);
}

Status IsDescending(Triplet T)
{ /* 初始条件:三元组T已存在。操作结果:如果T的3个元素按降序排列,返回1,否则返回0 */
return(T[0]>=T[1]&&T[1]>=T[2]);
}

Status Max(Triplet T,ElemType e)
{ /
初始条件:三元组T已存在。操作结果:用e返回指向T的最大元素的值 */
*e=T[0]>=T[1]?T[0]>=T[2]?T[0]:T[2]:T[1]>=T[2]?T[1]:T[2];
return OK;
}

Status Min(Triplet T,ElemType e)
{ /
初始条件:三元组T已存在。操作结果:用e返回指向T的最小元素的值 */
*e=T[0]<=T[1]?T[0]<=T[2]?T[0]:T[2]:T[1]<=T[2]?T[1]:T[2];
return OK;
}

/* main1-1.c 检验基本操作bo1-1.c的主函数 /
#include"c1.h" /
要将程序中所有#include命令所包含的文件拷贝到当前目录下 /
/
以下2行可根据需要选用一个(且只能选用一个),而不需改变基本操作bo1-1.c /
typedef int ElemType; /
定义抽象数据类型ElemType在本程序中为整型 */
/typedef double ElemType; / 定义抽象数据类型ElemType在本程序中为双精度型 /
#include"c1-1.h" /
在此命令之前要定义ElemType的类型 /
#include"bo1-1.c" /
在此命令之前要包括c1-1.h文件(因为其中定义了Triplet) /
Int main()
{
Triplet T;
ElemType m;
Status i;
i=InitTriplet(&T,5,7,9); /
初始化三元组T,其3个元素依次为5,7,9 */
/i=InitTriplet(&T,5.0,7.1,9.3); / 当ElemType为双精度型时,可取代上句 /
printf(“调用初始化函数后,i=%d(1:成功) T的3个值为:%d %d %d\n”,i,T[0],T[1],T[2]); /
当ElemType的类型变化时,要相应改变printf()的格式符。 /
i=Get(T,2,&m); /
将三元组T的第2个值赋给m /
if(i==OK) /
调用Get()成功 /
printf(“T的第2个值为:%d\n”,m);
i=Put(T,2,6); /
将三元组T的第2个值改为6 /
if(i==OK) /
调用Put()成功 /
printf(“将T的第2个值改为6后,T的3个值为:%d %d %d\n”,T[0],T[1],T[2]);
i=IsAscending(T); /
此类函数实参与ElemType的类型无关,当ElemType的类型变化时,实参不需改变 /
printf(“调用测试升序的函数后,i=%d(0:否 1:是)\n”,i);
i=IsDescending(T);
printf(“调用测试降序的函数后,i=%d(0:否 1:是)\n”,i);
if((i=Max(T,&m))==OK) /
先赋值再比较 /
printf(“T中的最大值为:%d\n”,m);
if((i=Min(T,&m))==OK)
printf(“T中的最小值为:%d\n”,m);
DestroyTriplet(&T); /
函数也可以不带回返回值 */
printf(“销毁T后,T=%u(NULL)\n”,T);
}

9.5 查找地址:& 运算符

指针(pointer)用于存放变量的 地址。

之前使用的 scanf_s()就是用地址作为参数。

概括来讲,如果主调函数不使用 return 返回的值,则必须通过地址才能修改主调函数中的值。

以下是一些带有 地址参数 的函数以及 & 的用法。

若变量名为 name ,则 &name 就是其地址pc 地址通常以十六进制显示%p 是 输出地址 的转换说明

#include <stdio.h>
int main(void) {
char name[10] = { 0 };
printf(“Please enter your name within 9 characters:\n”);
scanf_s("%s", name, 10);
printf(“Okey, your name is %s.\nAnd the address of it is:%p.\n”, name, &name);
return 0;
}

结果:
Please enter your name within 9 characters:
Alice
Okey, your name is Alice.
And the address of it is:007CF890.

注意,在 C 中,被调函数 并 不能改变 主调函数 中 变量的地址

这样可以防止原始变量被被调函数中的副作用意外修改
9·6 更改主调函数中的变量

首先对比一下两个程序及其结果:

其功能都是要实现数值的转换

interchange 函数是要对调 main()中的 x y 的值

#include <stdio.h>

void interchange(int u, int v);

int main(void) {

int x, y;
x = 5;
y = 10;printf("Before: x = %2d y = %2d\n", x, y);
interchange(x, y);
printf("Now:    x = %2d y = %2d\n", x, y);return 0;

}

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

Before: x = 5 y = 10
Now: x = 5 y = 10

然而从上述的结果看 x y 的值并未对调,我们添加 printf()函数来检查一下

#include <stdio.h>

void interchange(int u, int v);

int main(void) {

int x, y;
x = 5;
y = 10;printf("Before: x = %2d y = %2d\n", x, y);
interchange(x, y);
printf("Now:    x = %2d y = %2d\n", x, y);return 0;

}

void interchange(int u, int v) {
printf(“Before: u = %2d v = %2d\n”, u, v);
int temp;
temp = u;
u = v;
v = temp;
printf(“Now: u = %2d v = %2d\n”, u, v);
}

Before: x = 5 y = 10
Before: u = 5 v = 10
Now: u = 10 v = 5
Now: x = 5 y = 10

我们发现,在被调函数中,u v 的值确实调换了,但把值传回 main()时,interchange()中的值却并不是 main()中的值,一般我们会想到在 interchange()中添加 return(u); ,但这只能传回一个值,为了传回两个值,就必须用到指针。

9·7 指针简介

指针 (pointer)是一个 值为内存地址 的变量(或数据对象)

(以下我们会将指针作为函数参数使用)

先看如下程序:

ptr = &pooh;
/*
这里 ptr 是指针名
pooh 是一个变量
&pooh 是变量对应的地址
/
/

这条语句我们解释为:
指针 ptr 指向 pooh
*/

借鉴数据结构的思想,指针表示为:
指针内存储的值为地址
指向
ptr
&pooh
pooh
9·7·1 间接运算符 *

间接运算符(indirection operator):也称作 解引用运算符通过 指针 / 地址 间接地找到其原变量的值

#include <stdio.h>
int main(void){
int pooh = 233; //给变量 pooh 赋值
int val;
int* ptr; //指针使用前要声明,下一节讲述
ptr = &pooh; //将 pooh 的地址赋给指针 ptr
val = * ptr; //使用间接运算符 * 找到指针所指向的变量 pooh 的值
printf(“val = %d”, val);
return 0;
//程序的语句可以等价为:val = pooh
}
结果:
val = 233;
9·7·2 声明指针

声明指针变量时必须 指定 指针所 指向的数据类型

int * ptr;
//星号()表明声明的变量是一个指针
//类型说明符(int)表明指针所指向对象的类型
//此声明的含义是:ptr 是指针,
ptr 是 int 类型

星号(*)+ 指针名 = 指向对象的值

星号(*)和指针名中间的空格(space)问题:

我们约定:在声明类型时使用空格,在解引用变量时不使用空格

注意:

指针的值是地址,一般由十六进制的无符号整数表示
但指针不是整数类型,而是一个新的类型——地址
ANSI C 专门为指针提供 %p 转换说明

9·7·3 使用指针在函数中通信

我们仍使用之前的程序:

#include <stdio.h>

void interchange(int u, int v);

int main(void) {

int x, y;
x = 5;
y = 10;printf("Before: x = %2d y = %2d\n", x, y);
interchange(&x, &y);                           //此处调用了变量 x,y 的地址,所以在函数中要将它们声明为指针
printf("Now:    x = %2d y = %2d\n", x, y);return 0;

}

void interchange(int* u, int* v) { //将得到的地址赋给相应的指针
int temp; //定义中间变量
temp = *u; //u 的值是 &x,我们要改变地址上变量的值,所以要对 *u 进行操作
*u = *v;
*v = temp;
}

结果:
Before: x = 5 y = 10
Now: x = 10 y = 5

以下列举两种函数调用形式:

//第一种
function1(x);
//这种方式调用了变量的值

//对应的函数定义:
function1(int x)
//要求函数定义中的形式参数必须是一个与 x 的类型相同的变量


//第二种
function2(&x);
//这种方式调用了变量的地址

//对应的函数定义:
function2(int* x)
//要求形式参数必须是一个指向正确类型的指针
如果要 计算 或 处理 值 ,则使用第一种函数调用

如果要在被调函数中 改变 主调函数 的 变量 ,就使用第二种函数调用

普通变量 把 值 作为基本量,把 地址 作为通过 & 运算符获得的 派生量指针 把 地址 作为基本量,把 值 作为通过 * 运算符获得的 派生量

板凳——————————————————(老树)C Prime Plus( 第9章 函数)相关推荐

  1. python第六章函数课后答案_浙大PTA-Python题库 函数题(6-1~6-6)题解

    其他各章题解链接如下 浙大PTA-Python题库 编程题第一章(1-1~1-3)题解 https://blog.csdn.net/zimuzi2019/article/details/1070206 ...

  2. c语言函数与编译预处理教学视频,C语言课程第6章 函数及编译预处理.ppt

    C语言课程第6章 函数及编译预处理 6.1模块化程序设计与函数 在设计较复杂的程序时,我们一般采用的方法是:把问题分成几个部分,每部分又可分成更细的若干小部分,逐步细化,直至分解成很容易求解的小问题. ...

  3. Python 精要参考(第二版) 第六章 函数与函数编程

    1. 第六章 函数与函数编程 为便于代码维护,绝大多数子程序都被分解并重新组织为函数以使代码模块化. 在 Python中定义一个函数很简单,Python从其它函数编程语言中借鉴了很多有用的思路用来简化 ...

  4. Swift 1.1语言第7章 函数和闭包

    Swift 1.1语言第7章  函数和闭包 在编程中,随着处理问题的越来越复杂,代码量飞速增加.其中,大量的代码往往相互重复或者近似重复.如果不采有效方式加以解决,代码将很难维护.为了解决这个问题,人 ...

  5. 【数理知识】《数值分析》李庆扬老师-第3章-函数逼近与快速傅里叶变换

    第2章 回到目录 第4章 第3章-函数逼近与快速傅里叶变换 3.1 函数逼近的基本概念 3.2 正交多项式 3.3 最佳平方逼近 3.4 曲线拟合的最小二乘法 3.5 有利逼近 3.6 三角多项式逼近 ...

  6. 《BOOST程序库完全开发指南》 第11章 函数与回调

    第11章  函数回调 #include <iostream> #include <boost/assign.hpp> #include <boost/ref.hpp> ...

  7. python编程设计大学ppt_Python程序设计-清华大学出版社-董付国第5章-函数的设计和使用PPT...

    PPT内容 这是Python程序设计-清华大学出版社-董付国第5章-函数的设计和使用PPT下载,主要介绍了函数定义:斐波那契数列:形参与实参:参数类型:默认值参数:关键参数:可变长度参数:参数传递的序 ...

  8. c语言程序设计函数6,C语言程序设计》第6章函数-XiamenUniversity.PDF

    厦门大学非计算机专业本科生公共课 (2012-2013第2学期) <C语言程序设计> 第6章 函数 林子雨 厦门大学计算机科学系 E-mail: ziyulin@ 个人主页:/linziy ...

  9. 《Go语言圣经》学习笔记 第五章函数

    <Go语言圣经>学习笔记 第五章 函数 目录 函数声明 递归 多返回值 匿名函数 可变参数 Deferred函数 Panic异常 Recover捕获异常 注:学习<Go语言圣经> ...

最新文章

  1. MySQL列的别名 insert into select from
  2. 绝望!导师只给我一周时间.......
  3. 在MAC上搭建eclipse+android开发环境
  4. WinForm开发中针对TreeView控件改变当前选择节点的字体与颜色
  5. 记录每个用户的操作记录(命令)
  6. 吴军:数学,为人生之题解出漂亮的答案
  7. ComponentArt.web.ui中文帮助之Grid(六)
  8. 关于百度地图(二):只需要定位,不需要地图(什么?回调混乱?一招解决!)
  9. python 批量下载种子_批量下载btbbt种子
  10. Kylin高级主题-Cube构建算法介绍(逐层算法和快速算法)
  11. dataTable 固定列
  12. 为什么程序员不缺网?三步教你破解WiFi密码,够简单!
  13. Androi移动开发基础
  14. 三层架构(我的理解及详细分析)
  15. C++ 四种cast 详解
  16. free software
  17. 2020王者营地服务器维护,至尊宝重磅返场,王者营地服务器崩溃,只因玩家等待了五年的它...
  18. 什么是广播风暴,如何防止局域网环路引起的广播风暴?
  19. 已拿腾讯offer分享面试经历(含答案、推荐书籍、资料分享)
  20. Visual assistx(西红柿)插件的安装及简单使用

热门文章

  1. Oracle: archive归档速度控制尝试
  2. UML 的概述 和 顺序图
  3. centos 8网卡管理
  4. Linux下MKL库的安装部署与使用,并利用cmake编译器调用MKL库去提升eigen库的计算速度
  5. oracle存储回车换行,oracle中去掉回车换行空格的方法详解
  6. 若依-前后台分离-后端模块启动问题记录
  7. MobaXterm解除存储会话上限
  8. 电商库存设计:销售层、调度层、仓库层,三层详解电商库存体系
  9. 双边滤波原理与参数的理解+双边滤波matlab实现
  10. 无锡设计培训——室内设计会用到软件有哪些?