目录

1. 函数的定义和调用

1.1 函数定义格式

1.2 函数调用

2. 函数声明

2.1 实际参数的类型转换

2.1.1 编译器在调用前遇到函数原型

2.1.2 编译器在调用前没有遇到函数原型

2.2 示例运行结果

3. 数组型实际参数

4. return语句

5. 程序终止

6. 递归

6.1 递归算法简单示例

6.2 递归算法分析

6.2.1 适用场景

6.2.2 递归过程分析

6.2.3 递归编程注意事项

6.2.4 递归算法的缺点

6.2.5 递归和循环的比较

6.3 快速排序算法分析与实现

7. 函数使用的基本原则

8. 在调用函数前计算出实参


1. 函数的定义和调用

1.1 函数定义格式

返回类型  函数名(形式参数)
{声明 // C89中声明必须在语句之前语句
}

说明1:函数本质上是一个自带声明和语句的小程序

说明2:函数的每个形式参数都要有类型说明

说明3:函数返回类型规则

① 不能返回数组,但关于返回类型没有其他限制

② 指定返回类型为void说明函数没有返回值

③ 如省略返回类型,C89假定函数返回值类型为int,C99中不合法

1.2 函数调用

说明1:形参本质上是一个局部变量,其初始值在函数调用时由实参提供

说明2:实参不一定要是变量,任何正确类型的表达式均可(实参的关键是一个值,有类型的值)

说明3:函数调用时,即使没有实参也要有括号

average;

是合法的表达式,但一般没有意义,编译器把不跟圆括号的函数名看成指向函数的指针

补充一个较冷僻的知识点:在函数调用f(a, b)中,编译器如何知道逗号是标点符号还是运算符 ?

关键:函数调用中的实参不能是任意的表达式,而必须是"赋值表达式"(给形参赋值),而在赋值表达式中不能用逗号作为运算符,除非逗号在括号中

示例:

a = 1, 2   // 这就不是赋值表达式,而是逗号表达式(逗号运算符优先级最低)
a = (1, 2) //这就是赋值表达式

所以f(a, b)中的逗号是标点符号,而f((a, b))中的逗号是运算符

2. 函数声明

我们先来研究一下在C89中调用未声明的函数会发生什么(C99中调用未声明或未定义的函数是非法的)

int main(void)
{double x = 3.0;printf("square: %d\n", square(x));return 0;
}int square(int n)
{return n * n;
}

2.1 实际参数的类型转换

2.1.1 编译器在调用前遇到函数原型

就像赋值一样,每个实际参数的值被隐式地转换成相应的形式参数的类型(也就是说,和赋值过程中的类型转换相同)

2.1.2 编译器在调用前没有遇到函数原型

编译器执行默认的实际参数提升

① 把float类型的实参转换成double类型(浮点型 ---> double)

② 执行整值提升,把char类型和short类型的实参转换成int 类型(整型 ---> int)

2.2 示例运行结果

由于调用了未声明的函数,编译器为该函数创建了一个隐式声明(implicit declaration)

该隐式声明:

① 假定返回值为int类型

② 进行默认的实际参数提升

参照上例,就是创建了隐式声明

int square(double);

运行结果:无声明时输出为0,有声明时输出为9。如果square的真实返回值类型不是int,也就是和隐式声明假定的返回值类型不同,则编译时报错。

说明1:从上例的执行结果看,有声明时是把double型实参转换成int型并赋值给形参(本质上是一次赋值运算,发生赋值时类型转换),虽然损失了精度但形参类型仍然是正确的

说明2:若没有遇到原型说明,则是给予形参n一个double型变量,也就是用整型的位模式去解释一个double型变量,得到的结果是混乱的

如何验证我上面这个观点是正确的呢 ? 将double x = 3.0改成double x = 3.123456789,输出为1412027609。可见形参的int型位模式中存储的是一个double型变量

所以,有声明时发生的函数调用时参数类型转换,和没有声明时的隐式声明错误是完全不同的两个概念

上面发生的过程可以用下面的小例子来说明:

① 有声明

double d = 3.0;
int i = d; // 精度损失,但整型变量i中存储的位模式正确

② 没有声明

double d = 3.0;
int *pi = (int *)&d; // 用整型位模式去解释一个浮点数,自然是错误的

说明3:函数声明补充知识点

① 使用函数原型解决了函数间调用顺序的问题

② 有时省略原型中的形参名是出于防御目的,比如恰好有一个宏的名字和形参名一样时(但不推荐这么做)

③ 函数的声明可以放在另一个函数体内,但此时该声明只在这个函数中有效(这是作用域原因,不推荐)

④ 若几个函数具有相同的返回类型,可以合并声明(严重不推荐)

⑤ C语言甚至允许把函数和变量声明合并在一起(但严重不推荐)

void print_pun(void), print_count(int n);
double x, y, average(double a, double b);

3. 数组型实际参数

int sum_array(int a[], int n)
{int i, sum = 0;for (i = 0; i < n; ++i)sum += a[i];return sum;
}

说明1:int a[]被解释为int *,函数无法检测传入数组的正确性。更准确地说,a是要处理的数组的起始地址,n是要处理的元素个数

但是不要告诉函数,数组型实际参数比实际情况大,这会造成下标越界,从而导致未定义的行为

说明2:如果形式参数是多维数组,声明参数时只能省略第一维的长度

int sum_two_dimensional_array(int a[][LEN],  int n);
// 可以看出,C语言中不能传递具有任意列数的多维数组

关于为什么只能省略第一维长度的说明:

① 声明一维数组形参省略长度

如有a[i] = 0;编译器按如下方式计算a[i]的地址,

a + i * 每个元素的大小 // 这个过程中,并没有依赖一维数组的长度

② 声明二维数组省略第一维的长度

如有a[i][j] = 0;编译器按如下方式计算a[i][j]的地址,

 a + i * 数组每行的大小 + j * 每个元素的大小// 其中,依赖了第二维的长度(即列数),但不依赖第一维的大小(即行数)

补充:变长数组形式参数(Page. 140)

// 两个参数的顺序不能调换,编译器基于第一个参数确定变长数组的长度
int sum_array(int n, int a[n]);

4. return语句

① 非void函数

// 如表达式类型与函数返回值类型不匹配,把表达式类型隐式转换成函数返回值类型
return 表达式;

② void函数

// 函数调用返回
return;

③ void函数

// 错误!!!
return 表达式;

④ 非void函数

// C89:在程序试图使用函数返回值时,导致未定义行为
// C99:非法!!!
return;

5. 程序终止

① main函数返回的值是状态码,可供检查。(Linux C编程中有介绍,其父进程可以检查子进程返回的状态码,以确定其返回状态)

② 终止程序

a. main + return语句

b. 调用exit函数(<stdlib.h>)

说明1:不管哪个函数调用exit函数都会导致程序终止,return只有当main函数调用时才会导致程序终止

说明2:exit函数可带参数,该参数表明程序终止时的状态,如exit(0)。可改进为:exit(EXIT_SUCCESS)和exit(EXIT_FAILURE),使用的两个宏定义在<stdlib.h>中,通常是0和1

6. 递归

6.1 递归算法简单示例

本质:函数直接或间接调用自己

实现:由于函数栈的存在,每次调用的参数状态均不同

以计算阶乘为例,

// 求阶乘,n! =  n * (n - 1)!
int fact(int n)
{// 递归出口,0! = 1, 1! = 1if (n <= 1)return 1;else// 要等计算出fact(n - 1)的值,才能得到fact(n)的值return n * fact(n - 1);
}

上例中,函数调用他自身只有一次,对于要求调用自身2次或多次的算法来说,递归要有用得多

递归经常作为分治法(divide-and-conquer)的结果自然出现,分治法把一个大问题划分成多个较小的问题(准确说是降低问题的规模),然后采用相同的算法分别解决这些小问题

6.2 递归算法分析

6.2.1 适用场景

能采用递归描述的算法通常有这样的特征:

① 为求解规模为N的问题,设法将他分解成规模较小的问题,然后从这些小问题的解方便地构造出大问题的解

② 并且这些规模较小的问题也能采用同样的分解和综合办法,分解成规模更小的问题,并从这些更小问题的解构造出规模较大问题的解。特别地,当N = 1时能直接得解

6.2.2 递归过程分析

以计算斐波那契数列的第n项为例,

int fib(int n)
{if (0 == n) return 0;if (1 == n) return 1;if (n > 1) return fib(n - 1) + fib(n - 2);
}

递归算法的执行过程分为递推和回归两个阶段,

① 递推阶段

把较复杂的问题(规模为n)的求解推到比原问题简单一些的问题(规模小于n)的求解。例如上例中,求解fib(n),把他推到求解fib(n - 1)和fib(n - 2),也就是说为计算fib(n),必须先计算fib(n - 1)和fib(n – 2),而计算fib(n - 1)和fib(n - 2)又必须先计算fib(n - 3)和fib(n - 4)。依次类推,直到计算fib(1)和fib(0),分别能立即得到结果1和0。在递推阶段,必须有终止递归的情况,例如上例中当n = 1和n = 0的情况

② 回归阶段

当获得最简单情况的解后,逐次返回,依次得到稍复杂问题的解。例如得到fib(0)和fib(1)后,返回得到fib(2)的结果。在得到fib(n - 1)和fib(n - 2)的结果后,返回得到fib(n)的结果

在编写递归函数的时候要注意,函数中的局部变量和参数只是局限在当前调用层,当递推进入"简单问题"层,他们各有自己的参数和局部变量(函数调用栈)

6.2.3 递归编程注意事项

在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。程序中不应出现无终止的递归调用,而只应出现有限次数的、有终止的递归调用,这可以用if 语句来控制,只有在某一条件成立时才继续执行递归调用;否则就不再继续。(if结构在递归算法中经常扮演控制递归是否进行的角色)

而且分支的逻辑表达式必须使用形参,因为形参是函数外部的,也就是说递归调用是由函数外部的因素控制的

6.2.4 递归算法的缺点

由于递归引起一系列的函数调用,并且可能会有一系列的计算,递归算法的执行效率相对较低。当某个递归算法能较方便地转化成递推算法时,通常按递推算法编写程序

而且过深层次的递归也会消耗大量的栈空间,甚至造成栈的溢出

6.2.5 递归和循环的比较

① 使用递归解决问题,只需要考虑问题相邻2步的关系

② 使用循环解决问题,需要想清楚解决问题的整个过程

6.3 快速排序算法分析与实现

假设数组下标从1到n,快速排序算法如下,

① 选择数组元素e作为"分割元素",然后重新排列数组使得元素从1到i - 1都小于或等于e,元素i 包含e,而元素i + 1到n都大于等于e ,即e在正确的有序位置上

② 通过递归地采用快速排序法,对从1到i - 1的元素进行排序

③ 通过递归地采用快速排序法,对从i + 1到n 的元素进行排序

递归出口:待排序的小数组仅有一个或没有元素

可见快速排序算法中,函数会递归地调用自己2次,具体实现如下,

int split(int a[], int low, int high)
{// 确定分割元素,此处选择子数组的第1个元素int partElement = a[low];while(1){while ((low < high) && (partElement <= a[high]))--high;if (low >= high) break;a[low++] = a[high];while ((low < high) && (partElement >= a[low]))++low;if (low >= high) break;a[high--] = a[low];}a[high] = partElement;return high;
}void quicksort(int a[], int low, int high)
{int middle;if (low >= high) return; //递归出口middle = split(a, low, high);quicksort(a, low, middle - 1);  //递归排序左半边quicksort(a, middle + 1, high); //递归排序右半边
}

快速排序算法说明:

① 每调用1次split函数会使1个元素在正确的位置上并返回其下标,该下标将作为后续递归调用的依据

② split函数会从数组两端遍历数组,直到low & high索引值相遇

③ 待放置的分割点元素被取出并存储在partElement变量中,数组中便空出了一个位置,后续分割数组时就是使用这个空出的位置

④ 哪个元素被保存了,他的位置就可以使用了

7. 函数使用的基本原则

① 函数的职责应当明确而单一

做好并只做一件事

② 函数的代码应当短小而精干

a. 短小精干的函数可以借助她下一层级的函数调用,使其变得强大

b. 一个函数的代码量应该在50行左右

③ 函数应当避免嵌套层次太多

④ 函数应当避免重复代码

每当我们想复制代码的时候,就想想是不是应该把这段代码提取成一个单独的函数

8. 在调用函数前计算出实参

不同系统中,实参的计算顺序不同,微机上一般是从右向左。为避免由此引发的混乱,一般应在调用函数前计算出实参的值。示例如下,

int f(int a, int b)
{int c;if (a > b)c = 1;else if (a == b)c = 0;elsec = -1;return c;
}int main()
{int i = 2;int result;result = f(i, ++i); //对于不同的实参计算顺序,结果不同,会造成混乱printf(“%d”, result);return 0;
}

C程序设计语言现代方法09:函数相关推荐

  1. C程序设计语言现代方法03:格式化输入输出

    目录 1. printf函数 1.1 基本格式 1.2 格式字符串详解 1.2.1 普通字符 1.2.2 转换说明(conversion specification) 1.3 使用注意事项 2. sc ...

  2. Python程序设计语言基础05:函数和代码复用

    目录 1. 函数的定义与使用 1.1 函数的理解和定义 1.1.1 函数的理解 1.1.2 函数的定义 1.2 函数的使用及调用过程 1.3 函数的参数传递 1.3.1 无参数传递 1.3.2 可选参 ...

  3. C程序设计语言现代方法18:声明

    目录 1. 声明的语法 2. 变量的性质 2.1 变量性质的构成 2.2 变量默认性质 2.3 修改变量默认性质 2.3.1 修改局部变量默认性质 2.3.2 修改全局变量默认性质 2.4 exter ...

  4. C程序设计语言现代方法17:指针的高级应用

    目录 1. 动态存储分配 1.1 malloc函数 1.2 calloc函数 1.3 realloc函数 1.4 free函数 2. 空指针NULL解析 2.1 NULL的定义形式 2.2 程序如何知 ...

  5. C程序设计语言现代方法15:编写大型程序

    目录 1. C语言程序一般构成 2. 源文件 2.1 源文件内容 2.2 将文件划分成多个源文件的优点 3. 头文件 3.1 包含头文件的3种方式 3.2 头文件共享内容 3.2.1 宏定义和类型定义 ...

  6. C程序设计语言现代方法14:预处理器

    目录 1. 预处理器工作原理 1.1 预处理器性质 1.2 预处理器主要功能 1.3 GCC编译过程及常用选项 1.3.1 GCC编译过程 1.3.2 编译选项实例 1.4 注意事项 2. 预处理指令 ...

  7. C程序设计语言现代方法13:字符串

    目录 1. 字符串字面量 1.1 定义 1.2 字符串字面量包含转义序列 1.3 延续字符串字面量 1.3.1 使用续行符 1.3.2 仅用空白字符分割字符串字面量 1.4 存储字符串字面量 1.5 ...

  8. C程序设计语言现代方法12:指针和数组

    目录 1. 指针的算术运算 1.1 概述 1.2 C语言支持的算术运算类型 2. 指针的比较 3. 指针用于数组处理 4. 数组名与指针 4.1 用数组名作指针 4.2 数组取下标操作 4.3 惯用法 ...

  9. C程序设计语言现代方法08:数组

    目录 1. C语言中的变量 2. 一维数组 2.1 数组的声明 2.2 数组初始化 2.3 对数组使用sizeof运算符 3. 多维数组 4. 常量数组 4. C语言数组类型 4.1 数组类型 4.2 ...

最新文章

  1. Datawhale Ring限量100份来了!
  2. python常用命令汇总-酷帅吊炸天的 Pandas 常用操作命令汇总
  3. javascript算法题:求任意一个1-9位不重复的N位数在该组合中的大小排列序号
  4. 计算机应用基础 pdf 陈建军教案,温州市第二职业中等专业学校(温五中) 教学资源 温州市《计算机应用基础》学业水平测试考纲(转发)...
  5. XV6700刷evdo详细教程
  6. 2019年总结:把能努力的都努力好,最终等待命运垂青
  7. codevs原创抄袭题 5960 信使
  8. java 日志乱码_【开发者成长】JAVA 线上故障排查完整套路!
  9. QQ红包源码 大转盘抽奖源码下载 微信红包源码
  10. 大数据_Flink_数据处理_运行时架构7_程序结构和数据流图---Flink工作笔记0022
  11. 编程范式--并发编程相关代码
  12. 关于移动支付的一点知识
  13. WDM驱动程序的基本结构和实例
  14. MongoDB基本操作
  15. 上传文件到服务器太大怎么办,超大文件怎么上传到云服务器
  16. Viruses!!!!!
  17. Java调用 新浪微博API 接口发微博(包含js微博组件、springMVC新浪登录)详解
  18. base64 的加密和解密
  19. SQL注入漏洞(postgresql注入)
  20. 控件为何不能自动装载?--全面总结

热门文章

  1. 网络编程1-初探winSocket
  2. request.getRequestDispatcher().forward(request,response)和response.sendRedirect()的区别
  3. Python标准库中的shutil
  4. php怎么异步执行,php中异步执行的四种方式
  5. css的类选择器#和id选择器.
  6. Oracle数据库是如何执行SQL的
  7. python异步调用_python如何实现异步调用函数执行
  8. linux 常用正则表达式,Linux中基本正则表达式
  9. 解决The valid characters are defined in RFC 7230 and RFC 3986错误问题
  10. 解决MYSQL不报错误详细信息的问题 Can‘t find error-message file