1、函数的定义
- 在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组 成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
- 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
2、函数的分类
1、库函数
2、自定义函数
库函数
库函数的查询工具
- www.cplusplus.com
- http://en.cppreference.com(英文版)
- http://zh.cppreference.com(中文版)
库函数的种类大致存在以下几类:
- IO函数
- 字符串操作函数
- 字符操作函数
- 内存操作函数
- 时间/日期函数
- 数学函数
- 其他库函数
#include的使用
- #include 是C语言的预处理指令之一,所谓预处理,就是在编译之前做的处理,预处理指令一般以 # 开头
- #include 指令后面会跟着一个文件名,预处理器发现 #include 指令后,就会根据文件名去查找文件,并把这个文件的内容包含到当前文件中。被包含文件中的文本将替换源文件中的 #include 指令,就像你把被包含文件中的全部内容拷贝到这个 #include 指令所在的位置一样。所以第一行指令的作用是将stdio.h文件里面的所有内容拷贝到第一行中。
- 如果被包含的文件拓展名为.h,我们称之为"头文件"(Header File),头文件可以用来声明函数,要想使用这些函数,就必须先用 #include 指令包含函数所在的头文件
- #include 指令不仅仅限于.h头文件,可以包含任何编译器能识别的C/C++代码文件,包括.c、.hpp、.cpp等,甚至.txt、.abc等等都可以
#include <>和#include ""的区别
二者的区别在于:当被include的文件路径不是绝对路径的时候,有不同的搜索顺序。
1> 对于使用双引号""来include文件,搜索的时候按以下顺序:
- 先在这条include指令的父文件所在文件夹内搜索,所谓的父文件,就是这条include指令所在的文件
- 如果上一步找不到,则在父文件的父文件所在文件夹内搜索;
- 如果上一步找不到,则在编译器设置的include路径内搜索;
- 如果上一步找不到,则在系统的INCLUDE环境变量内搜索
2> 对于使用尖括号<>来include文件,搜索的时候按以下顺序:
- 在编译器设置的include路径内搜索;
- 如果上一步找不到,则在系统的INCLUDE环境变量内搜索
自定义函数
函数的组成:
ret_type fun_name(para1, * )
{
statement; //语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
举例
#include <stdio.h>
//get_max函数的设计
int get_max(int x, int y)
{
return (x > y) ? (x) : (y);
}
int main()
{
int num1 = 10;
int num2 = 20;
int max = get_max(num1, num2);
printf("max = %d\n", max);
return 0;
}
include <stdio.h>
//实现成函数,但是不能完成任务,传值调用
void Swap1(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
//正确的版本,传址调用
void Swap2(int* px, int* py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap1(num1, num2);
printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
Swap2(&num1, &num2);
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
这个部分的实现原理需要了解参数的传值调用和传址调用
3、函数的参数
实际参数
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类 型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配 内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在 函数中有效。
4、函数的调用
传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用
- 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
- 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操 作函数外部的变量
5、函数的声明与定义
- 在C语言中,函数的定义顺序是有讲究的:默认情况下,只有后面定义的函数才可以调用前面定义过的函数
- 如果想把函数的定义写在main函数后面,而且main函数能正常调用这些函数,那就必须在main函数的前面进行函数的声明
函数的声明
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数 声明决定不了。
- 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的。
声明的格式
返回值类型 函数名 (参数1, 参数2, ...);
只要你在main函数前面声明过一个函数,main函数就知道这个函数的存在,就可以调用这个函数。而且只要知道函数名、函数的返回值、函数接收多少个参数、每个参数是什么类型的,就能够调用这个函数了,因此,声明函数的时候可以省略参数名称。
函数的定义
函数的定义是指函数的具体实现,交待函数的功能实现。
多源文件开发
test.h的内容
放置函数的声明
#ifndef __TEST_H__
#define __TEST_H__
//函数的声明
int Add(int x, int y);
#endif //__TEST_H__
test.c的内容
放置函数的实现
#include "test.h"
//函数Add的实现
int Add(int x, int y)
{
return x+y;
}
多源文件开发的必要性
- 编写的所有C语言代码都保存在拓展名为.c的源文件中,编写完毕后就进行编译、链接,最后运行程序。
- 在实际开发过程中,项目做大了,源代码肯定非常多,很容易就上万行代码了,甚至上十万、百万都有可能。这个时候如果把所有的代码都写到一个.c源文件中,那么这个文件将会非常庞大,也非常恶心,你可以想象一下,一个文件有十几万行文字,不要说调试程序了,连阅读代码都非常困难。
- 为了模块化开发,一般会将不同的功能写到不同的.c源文件中,这样的话,每个开发人员都负责修改不同的源文件,达到分工合作的目的,能够大大提高开发效率。也就是说,一个正常的C语言项目是由多个.c源文件构成。
函数的递归
- 程序调用自身的编程技巧称为递归( recursion)。
- 递归做为一种算法在程序设计语言中广泛应用。
- 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解, 递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
- 递归的主要思考方式在于:把大事化小
递归的必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
递归的具体实现
-
每级递归都有自己的变量,可能名称相同,但是其值不同。
递归调用时,系统自动保留当前函数的参数变量。每次调用系统都会为函数开辟相应的空间。
-
每次调用都要返回值,递归执行结束后,控制权传回到上一级函数。
调用结束后,系统释放本次调用所开辟的空间,程序返回到上一次的调用点,同时获得初进该级调用的参数。
每级递归必须逐级返回,不可跳跃或间断。
-
函数中递归语句之前的代码,按被调函数的顺序执行,递归之后的代码,与被调函数相反的顺序执行。
递归的经典案例
1、打印整数每一位
用递归的方式,实现打印一个整数的每一位的功能。
输入:1234
输出:1 2 3 4
print(1234)
= = =print(123)
+4
= = =print(12)
+3+4
= = =print(1)
+2+3+4
= = =printf(1)
+2+3+4
这便是前面使用场景中所写的,将题目要求问题转化为新的问题,且变量有规律的变化
n
是不是个位数,递推调用n / 10
n
是个位数,回归打印n % 10
void Print(int n)
{
if (n > 9)
{
Print(n / 10);
}
printf("%d ", n%10);
}
int main()
{
int num = 0;
scanf("%d", &num);
Print(num);
return 0;
}
2、递归和非递归求n阶乘
用递归和非递归的方法,分别实现求n的阶乘的功能(不考虑溢出)。
输入:5
输出:120
n ∗ n − 1 ∗ n − 2 ∗ n − 3 ∗ … ∗ 1 n*n-1*n-2*n-3*…*1 n∗n−1∗n−2∗n−3∗…∗1
f a c ( n ) = n ∗ f a c ( n − 1 ) , n > 0 fac(n) = n * fac(n-1) , n>0 fac(n)=n∗fac(n−1),n>0
f a c ( n ) = 1 , n = 0 fac(n) = 1 , n=0 fac(n)=1,n=0
int fac(int n)//非递归
{
int ret = 1;
for (int i = 1; i <= n; i++)
{
ret *= i;
}
return ret;
}
int fac(int n)//递归
{
if (n > 0)
return n * fac2(n - 1);
else
return 1;
}
int main()
{
int n = 0;
scanf("%d", &n);
printf("%d\n", fac(n));
return 0;
}
3、编写函数不允许创建临时变量,求字符串的长度
输入输出示例
输入:abcdef
输出:6
strlen(abcdef\0)
1+strlen(bcdef\0)
1+1+strlen(cdef\0)
1+1+1+strlen(def\0)
1+1+1+1+strlen(ef\0)
1+1+1+1+1+strlen(f\0)
1+1+1+1+1+1+strlen(\0)
#include <stdio.h>
int Strlen(const char* str)
{
if (*str == '\0')
return 0;
else
return 1 + Strlen(str + 1);
}
int main()
{
char* p = "abcdef";
int len = Strlen(p);
printf("%d\n", len);
return 0;
}
4、递归求斐波那契数列
int fib(int n)
{
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
但是这个程序存在一定缺陷
- 在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
- 使用 factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。
因此可以使用循环语句的方法进行改写
//求n的阶乘
int factorial(int n)
{
int result = 1;
while (n > 1)
{
result *= n;
n -= 1;
}
return result;
}
//求第n个斐波那契数
int fib(int n)
{
int result;
int pre_result;
int next_older_result;
result = pre_result = 1;
while (n > 2)
{
n -= 1;
next_older_result = pre_result;
pre_result = result;
result = pre_result + next_older_result;
}
return result;
}