函数是什么
数学中我们其实就⻅过函数的概念,⽐如:⼀次函数 y = kx + b ,k和b都是常数,给⼀个任意的x ,就得到⼀个 y 值。其实在C/C++语⾔中就引⼊了函数(function)的概念,有些翻译为:⼦程序,⼦程序这种翻译更加准确⼀些。函数就是⼀个完成某项特定的任务的⼀⼩段代码,这段代码是有特殊的写法和调⽤⽅法的。
其实⼤家在前⾯学习的时候,就⻅过函数了,⽐如:main函数、scanf函数、printf函数、pow函数、sqrt函数等。C/C++语⾔程序其实是由若⼲个⼩的函数组合⽽成的,也可以说:⼀个⼤的计算任务可以分解成若⼲个较⼩的函数(对应较⼩的任务)完成。同时⼀个函数如果能完成某项特定任务的话,这个函数也是可以复⽤的,提升了开发软件的效率。
#include <iostream>
using namespace std;
int main()
{ int arr[10] = { 0 }; //打印数组的内容 //给数组的元素赋值为1~10 //打印数组 return 0;
}
如果没有函数,我们能写出的代码如下的代码1,其中橙⾊部分的内容和绿⾊部分的代码是重复的,⽬的就是打印数组内容,那如果我能把这部分封装成⼀个函数,在需要的时候调⽤,就⽅便很多,这就是代码2
//代码1
#include<iostream>
using namespace std;
int main()
{ int arr[10] = { 0 }; //打印数组的内容 int i = 0; for (i = 0; i < 10; i++) { cout << arr[i] << " "; } cout << endl; //给数组的元素赋值为1~10 for (i = 0; i < 10; i++) { arr[i] = i + 1; } //打印数组 for (i = 0; i < 10; i++) { cout << arr[i] << " "; } cout << endl; return 0;
}
//代码2
#include<iostream>
using namespace std;
void print_arr(int arr[])
{ int i = 0; for (i = 0; i < 10; i++) { cout << arr[i] << " "; } cout << endl;
} int main()
{ int arr[10] = { 0 }; //打印数组的内容 print_arr(arr); //给数组的元素赋值为1~10 for (int i = 0; i < 10; i++) { arr[i] = i + 1; } //打印数组 print_arr(arr);return 0;
}
在代码2中 print_arr 就是我们⾃⼰定义的⼀个函数,使⽤函数能有什么好处呢?
- 模块化开发,⼀个⼤的功能,总能拆分成各种⼦功能,每个⼦功能都可以设计成⼀个函数,每个函数可以作为⼀个独⽴的模块存在,程序的逻辑更加清晰,逻辑关系更加明确。
- 代码可以复⽤,只要根据需要定义出⼀个函数,需要这个功能的地⽅,直接调⽤函数就⾏,降低了代码的冗余,提升了开发效率。
- ⽅便多个程序员之间协作开发,⽅便程序的多个模块之间互相交互。
- 熟悉函数的使⽤之后,代码的编写、阅读、调试、维护都变得更加容易。
函数的分类
在C/C++中,函数⼀般分为库函数和⾃定义函数
库函数
printf 、 scanf 都是库函数,库函数是标准库中提供的现成的函数,我们只要学习函数的功能,就能直接使⽤。有了库函数,⼀些常⻅的功能就不需要程序员⾃⼰实现了,⼀定程度提升了效率;同时库函数的质量和执⾏效率上都更有保证。
编译器的标准库中提供了⼀系列的库函数,这些库函数根据功能的划分,都在不同的头⽂件中进⾏了声明。
库函数的学习和查阅⼯具很多,⽐如:
C/C++官⽅参考⼿册:https://zh.cppreference.com/w/cpp/header
C/C++第三⽅⽹站:https://legacy.cplusplus.com/reference/
库函数中有数学相关的,有⽇期相关的,有算法相关的等等。这些库函数相关的信息都有⾃⼰对应的头⽂件,每⼀个头⽂件中都包含了相关的⼀组函数和类型等信息,库函数的学习不⽤着急⼀次性全部学会,慢慢学习,各个击破就⾏。
C++是兼容C语⾔,所以在C++中也包含了⼀些来⾃C语⾔的头⽂件,这些头⽂件的后缀是 .h ,如果需要也可以直接包含使⽤;有⼀些来⾃C语⾔的头⽂件,在C++中会在原来C语⾔的头⽂件进⾏了封装,在C++程序中更推荐C++的头⽂件写法,以下是常⻅C和C++对应的头⽂件:
解释 | C头文件 | C++头文件 |
---|---|---|
常用数学函数 | <math.h> | <cmath> |
浮点类型的极限 | <float.h> | <cfloat> |
C风格输入输出 | <stdio.h> | <cstdio> |
字符串处理 | <string.h> | <string> |
库函数使⽤举例
sqrt
double sqrt (double x);
- sqrt 是函数名
- x 是函数的参数,表⽰调⽤ sqrt 函数需要传递⼀个 double 类型的值
- double 是返回值类型,表⽰函数计算的结果是 double 类型的值
作用
Compute square root计算平⽅根
Returns the square root of x.(返回平⽅根)
头文件
库函数是在标准库中对应的头⽂件中声明的,所以库函数的使⽤,务必包含对应的头⽂件,不包含是可能会出现⼀些问题的。
#include <iostream>
#include <cmath> //数学函数头⽂件,不包含则⽆法使⽤sqrt函数
using namespace std;
int main()
{ double d = 16.0; double r = sqrt(d); cout << r << endl; return 0;
}
⾃定义函数
了解了库函数,其实库函数的功能是有限的,实际开发过程中还是得根据需要将代码封装成⾃定义的函数;⾃定义的函数就是⾃⼰设计和实现的函数
函数的语法形式
ret_type fun_name(形式参数)
{ }
- ret_type 是⽤来表⽰函数计算结果的类型,有时候返回类型可以是 void ,表⽰什么都不返回
- fun_name 是为了⽅便使⽤函数;就像⼈的名字⼀样,有了名字⽅便称呼,函数有了名字⽅便调⽤,所以函数名尽量要根据函数的功能起的有意义。
- 函数的参数就相当于⼯⼚加⼯需要的原材料,函数的参数也可以是 void ,明确表⽰函数没有参数。如果有参数,多个参数⽤逗号隔开,每个参数要分别交代清楚参数的类型和名字。
- {} 括起来的部分被称为函数体,函数体就是完成计算的过程。
函数定义
写⼀个加法函数,完成2个整型变量的加法操作
#include<iostream>
using namespace std;
int main()
{ int a = 0; int b = 0; //输⼊ cin >> a >> b; //任务:调⽤加法函数,完成a和b的相加 //求和的结果放在r中 //to do //输出 cout << r << endl; return 0;
}
给函数取名: Add ,函数 Add 需要接收2个整型类型的参数,函数计算的结果也是整型
#include<iostream>
using namespace std;
//这就是函数的定义
int Add(int x, int y)
{ int z = 0; z = x + y; return z;
} int main()
{ int a = 0; int b = 0; //输⼊ cin >> a >> b; //调⽤加法函数,完成a和b的相加 //求和的结果放在r中 int r = Add(a, b); //输出 cout << r << endl; return 0;
}
Add 函数也可以简化为
int Add(int x, int y)
{
return x + y;
}
函数的参数部分需要交代清楚:参数个数,每个参数的类型是啥,形参的名字叫啥
函数参数和返回值
实参和形参
实际上,在函数定义和使⽤的过程中,函数的参数要被分为两种:
- 实际参数,简称实参
- 形式参数,简称形参
实参
实际参数就是真实传递给函数的参数。
在上⾯代码中,第4~6⾏是 Add 函数的定义,有了函数后,再第19⾏调⽤ Add 函数的。
我们把第19⾏调⽤ Add 函数时,传递给函数的参数 a 和 b ,称为实际参数,简称实参。
形参
在上⾯代码中,第4⾏定义函数的时候,在函数名 Add 后的括号中写的 x 和 y ,称为形式参数,简称形参。
为什么叫形式参数呢?实际上,如果只是定义了 Add 函数,⽽不去调⽤的话, Add 函数的参数 x 和 y 只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在函数被调⽤的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形式的实例化。
实参和形参的关系
虽然我们提到了实参是传递给形参的,他们之间是有联系的,但是形参和实参各⾃是独⽴的内存空间。
这个现象是可以通过调试来观察的。请看下⾯的代码和调试演⽰:
#include<iostream>
using namespace std;
int Add(int x, int y)
{ int z = 0; z = x + y; return z;
}
int main()
{ int a = 0; int b = 0; //输⼊ cin >> a >> b; //调⽤加法函数,完成a和b的相加 //求和的结果放在r中 int r = Add(a, b); //输出 cout << r << endl; return 0;
}
我们在调试的可以观察到, x 和 y 确实得到了 a 和 b 的值,但是 x 和 y 的地址和 a 和 b 的地址不⼀样的,当 a 和 b 传参给形参x和y的时候, x 和 y 只是得到了 a 和 b 的值,他们得有⾃⼰独⽴的空间。所以我们可以理解为形参是实参的⼀份临时拷⻉。
函数传参
数组做函数参数
在使⽤函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进⾏操作。⽐如:写⼀个函数将⼀个整型数组的内容,全部置为 -1 ,再写⼀个函数打印数组的内容。
#inculude <iostream>
using namespace std;
int main()
{ int arr[] = {1,2,3,4,5,6,7,8,9,10}; set_arr(); //设置数组内容为-1 print_arr(); //打印数组内容 return 0;
}
这⾥的 set_arr 函数要能够对数组内容进⾏设置,就得把数组作为参数传递给函数,同时函数内部在设置数组每个元素的时候,也得遍历数组,需要知道数组的元素个数。所以我们需要给 set_arr传递2个参数,⼀个是数组,另外⼀个是数组的元素个数。仔细分析 print_arr 也是⼀样的,只有拿到了数组和元素个数,才能遍历打印数组的每个元素。
int main()
{ int arr[] = {1,2,3,4,5,6,7,8,9,10}; int sz = sizeof(arr)/sizeof(arr[0]); set_arr(arr, sz); //设置数组内容为-1print_arr(arr, sz); //打印数组内容return 0;
}
数组作为参数传递给了 set_arr 和 print_arr 函数了,那这两个函数应该如何设计呢?
这⾥我们需要知道数组传参的⼏个重点知识:
- 函数的实参的名字和形参的名字可以相同,也可以不同
- 函数的形式参数要和函数的实参个数匹配
- 函数的实参是数组,形参也写成数组形式的
- 形参如果是⼀维数组,数组⼤⼩可以省略不写
- 形参如果是⼆维数组,⾏可以省略,但是列不能省略
- 数组传参,形参是不会创建新的数组的
- 形参操作的数组和实参的数组是同⼀个数组
void set_arr(int arr[], int sz)
{ int i = 0; for(i = 0; i < sz; i++) { arr[i] = -1; }
}
void print_arr(int arr[], int sz)
{ int i = 0; for(i = 0; i < sz; i++) { cout << arr[i] << " "; } cout << endl;
}
字符串做函数参数
那如果将字符串做函数参数呢?其实也很简单,直接在形参的部分使⽤字符串来接收就可以。这⾥的形参 s 也是实参 s 的⼀份临时拷⻉,对形参的修改不能影响实参。
void test(string s)
{ cout << s << endl;
}
int main()
{ string s("hello world"); test(s);
}
全局变量不⽤传参
全局变量的作⽤域很⼤,在整个程序中都可以使⽤,那么只要把变量、数组等定义成全局变量,在函数中使⽤,是可以不⽤传参的,在竞赛中为了⽅便经常使⽤,但是在软件⼯程中很少这么使⽤
#include <iostream>
using namespace std;
int arr[10] = { 0 }; void print_arr()
{ int i = 0; for (i = 0; i < 10; i++) { cout << arr[i] << " "; } cout << endl;
} int main()
{ //打印数组的内容 print_arr(); //给数组的元素赋值为1~10 for (int i = 0; i < 10; i++) { arr[i] = i + 1;} //打印数组 print_arr(); return 0;
}
有时候变量或者数组,定义成全局的时候,是不能解决问题,⽐如:递归等场景,这时候,就得考虑传参的问题