目录
1. 概述
函数的作用:将一段经常使用的代码封装起来,减少重复代码
一个较大的程序, 一般都会用函数分成若干个程序块, 每个模块实现特定的功能.
2.函数的定义和调用
定义一个函数需要在int main(){} 之前, 这样主函数才能正确地调用函数.
函数的定义一般需要5个步骤(组成) :
1. 返回值的类型
2. 函数名
3. 参数列表
4. 函数体语句
5. return表达式
返回值类型 函数名 (参数列表)
{
函数体语句
return表达式
}
2.1 返回值类型:
调用函数时,通常会希望函数返回一些结果值.
比如设计一个求两数之和的函数, 那么就需要这个函数将计算后的结果给出来, 这个值的数据类型就叫做返回值类型.
当你的函数不需要返回值的时候,返回值类型为 void.
同时也不需要写return表达式.
拓展:
在C++函数中返回多个数值的三种方法_c++可以return多个值吗_荒原之梦网的博客-CSDN博客
2.2 函数名:
就像调用变量需要先定义变量类型和变量名一样, 调用函数时也需要用到函数的名称.
2.3 参数列表:
比如设计一个求两数之和的函数, 那么就需要向函数提供这两个数, 提供的这两个数就叫做参数.
一个函数可以获取多个参数, 组成参数列表.
每个参数列表中的参数都需要定义数据类型.
2.3.1 形参
参数列表中的参数,又叫做形式参数,简称形参.函数在定义时,并没有创建真实的变量,也就是并没有给形参分配内存,而是在调用时才临时分配.
2.3.2 实参
如果调用函数时, 使用函数外的变量或常量, 而不是直接写出具体值时, 被调用的变量称为实际参数,简称实参.实参在被调用的函数外,在主函数内定义的时候, 就会被分配内存空间.
在调用时,实参的值会传递给形参.
后文会提到它们之间的其他区别.
2.4 函数体语句:
用来实现具体的函数功能, 比如将传入的参数进行某种处理.
2.5 return 表达式:
通过return表达式来向函数外返回结果. 就像int main()需要 return 0 ; 来表示结束一样.
当你的函数不需要返回值的时候, 不需要写return表达式, 前提是返回值类型为void.
2.6 示例:
定义一个加法函数, 输入两个数, 返回它们的和.
#include <iostream>
using namespace std;
//定义一个整型加法函数,需要在int main(){}之前
//两个整型变量相加得到的结果还是整型, 将输入的两个参数相加之后返回结果
int add(int a, int b)
{
int sum = a + b;
return sum;
}
int main()
{
cout << add(3, 5) << endl; //直接用具体值调用函数,函数视为一个表达式,返回值就是表达式的值
int i = 6;
int j = 12;
cout << add(i, j) << endl; //用变量作为参数调用函数,此时的 i , j 为实参.
int c = add(i, j); //既然是表达式,当然也可以用它来赋值
cout << c << endl;
return 0;
}
3. 参数传递
调用函数时, 实参的值会传递给形参, 有三种传递方式, 分别是 值传递 , 地址传递 和 引用传递.
3.1 值传递
使用值传递的方式传递参数, 在函数语句中, 即便形参的值发生改变, 也不会影响到实参.
示例:
定义一个两数交换的函数, 将参数位置互换, 并在互换前后分别检查形参和实参.
这个案例可以清晰明了地解释形参和实参的区别.
#include <iostream>
using namespace std;
//定义一个函数,让两个数位置互换,由于不需要返回值, 返回值类型为void, 不需要写return表达式.
void swap(int num1, int num2)
{
//检查交换前的值
cout << "交换前的num1,num2 :" << num1 << "," << num2 << endl;
int temp = num1;
num1 = num2;
num2 = temp;
//检查交换后的值
cout << "交换后的num1,num2 :" << num1 << "," << num2 << endl;
}
int main()
{
int a = 10;
int b = 20;
cout << "交换前的a,b : " << a << "," << b << endl;
swap(a, b);
cout << "交换后的a,b : " << a << "," << b << endl;
return 0;
}
可以看到, 函数调用后, 形参发生了改变, 但是实参没有被改变.
这是因为, 调用函数时程序才给形参分配内存空间, 程序一直在对temp, num1, num2 这三个形参变量的内存空间进行读写操作, 但是在交换前仅读取了实参的内存空间, 而整个交换过程并不涉及 实参a, b的内存空间.
总结起来就是, 在使用值传递时, 无论形参如何变化, 都不会影响到实参.
3.2 地址传递
地址传递 本质上也是 值传递 , 但它传递的变量一定是 指针变量.(也就是地址.)
但是值传递无法影响到实参, 而地址传递可以.
值传递的形式参数和主函数的实际参数存放地址不同, 因此操作形参时不会影响到实参.
地址传递 会传递实参的地址 , 虽然对应的指针变量也是存放在不同的地址中, 但是通过解引用的方式, 就可以利用实参的地址来修改实参.
地址传递本质上仍然是值传递, 值传递中形参改变不会影响到实参.
在地址传递中, 形参和实参就是 指针变量(地址), 就算在函数中指针指向发生了改变, 也不会影响实参指针的指向.
当然有的教程可能会说, 地址传递中形参改变会影响到实参, 那是因为他指的 形参和实参 是需要操作的数值本身, 而地址传递调用的是这些数值存放的地址.
换句话说, 如果你在函数中直接操作参数, 相对于这些参数来说, 你使用的是值传递的方式.
如果你在函数中对参数解引用后再操作, 相对于这些参数对应的内存来说, 你使用的是地址传递.
如果无法理解,请看代码:
#include <iostream>
using namespace std;
//地址传递
//交换函数,在函数内直接打印,不需要返回值,返回值类型为void
void swap1(int* p1, int* p2) //值传递中, 函数取得的值叫做参数, 函数直接用参数进行操作
//而地址传递中, 函数取得的是地址, 形参和实参都是地址, 函数需要解引用才能得到"所谓的那个实参"
//所以参数列表中, p1 和 p2 的数据类型为 (int *)
{
//交换前
cout << "交换前的形参:" << endl;
cout << "*p1 = :" << *p1 << endl;
cout << "*p2 = :" << *p2 << endl;
cout << "p1 = :" << p1 << endl;
cout << "p2 = :" << p2 << endl;
int temp = *p1; //显然, 这样的交换相当于直接操作形参(地址), 但间接操作形参的内存(值)
*p1 = *p2; //所以在地址传递中, 内存(值)会改变, 但指针(地址)不会改变
*p2 = temp;
cout << "解引用交换后的形参:" << endl;
cout << "*p1 = :" << *p1 << endl; //检查一下形参指向的内存(也就是 a 和 b 的值)
cout << "*p2 = :" << *p2 << endl; //已经交换
cout << "p1 = :" << p1 << endl; //检查一下形参的指向(形参的值)
cout << "p2 = :" << p2 << endl; //没有变化, 正常, 因为我们还没有对 p1 p2 本身进行操作
int* tmp = nullptr;
tmp = p1;
p1 = p2; //交换 p1 p2 ,也就是交换形参的指向, 相当于 p1指向原来的 *p2
p2 = tmp; //相当于 p2 指向原来的 *p1
cout << "指针交换后的形参:" << endl; //检查一下形参的指向(形参的值)
cout << "*p1 = :" << *p1 << endl; //没有变化, 正常, 因为我们这一步没有对 *p1 *p2 本身进行操作
cout << "*p2 = :" << *p2 << endl;
cout << "p1 = :" << p1 << endl; //发生交换, p1 指向了原来 p2 指向的地方, 也就是指向 b 的地址
cout << "p2 = :" << p2 << endl; //发生交换, p2 指向了原来 p1 指向的地方, 也就是指向 a 的地址
}
int main()
{
int a = 3;
int b = 5;
cout << "交换前的实参:" << endl;
cout << "a = :" << a << endl;
cout << "b = :" << b << endl;
cout << "a的地址 = :" << &a << endl;
cout << "b的地址 = :" << &b << endl;
swap1(&a, &b);
cout << "交换后的实参:" << endl;
cout << "a = :" << a << endl; //发生交换, 这证明值交换 和 指向交换 两件事没有同时发生
cout << "b = :" << b << endl; //否则的话交换两次应该是等于没有交换的
cout << "a的地址 = :" << &a << endl; //检查指向, 发现 a 的地址与调用函数前无变化, 说明 指向交换这件事没有同时发生.
cout << "b的地址 = :" << &b << endl; //而值交换一定发生了, 否则实参指针指向的 a b 不会交换.
//总结, 函数中值和地址都交换了, 主函数中值交换了, 地址没有交换.
//地址传递 的形参和实参都是地址, 这说明地址传递中形参发生改变, 也不会影响到实参.
//说明地址传递也是值传递的一种.
//但当地址传递对形参解引用后操作时, 实参指向的值可以发生改变.
return 0;
}
注意主函数在调用函数之前还有输出内容.
3.3 引用传递
如果既想用形参修饰实参, 又觉得地址传递麻烦, 那么可以用引用传递.
优点是, 可以简化指针操作.
//相比于值传递, 引用传递仅在参数列表中多了个 &
void swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int a = 10;
int b = 20;
swap(a, b);
cout << a << endl;
cout << b << endl;
return 0;
}
引用传递的本质是将 形参名 作为 实参名 的别名, 这样操作形参的时候也等于操作实参.
(但真正的本质是封装指针常量, 引用语句会被编译器自动转换为指针常量语句.)
如代码中所示, 形参 a 被称为 实参 a 的别名. 因为实参 a 在main 函数中, 形参 a 在swap函数中,
因此变量可以重名.
但在同一个函数中, int &a = a; 这种引用是非法的.
4.函数的样式
函数的种类有很多,如果根据有无参数和有无返回值来划分,有以下4种常见类型:
4.1 无参无返
既不需要参数, 也不需要返回值的函数称为无参无返函数.
这种函数的参数列表为空, 返回值类型为void.
void类型的函数在调用的时候不被认为是一个表达式, 无法作为赋值符号的右值.
调用void类型的函数的语法:
函数名();
示例:
#include <iostream>
using namespace std;
//无参无返类函数,函数类型为void, 参数列表为空
void function1()
{
// void b = 10; 这句是错的,因为void类型不能作为变量和常量的数据类型.
cout << "helloworld!" << endl;
int a = 10; //虽然没有返回值,但是仍然可以定义形参并且输出,但是无法返回.
cout << a << endl;
}
int main()
{
function1(); //直接调用
//int b = function1(); 这样也是错的,void类型的函数不是表达式,不能用来赋值.
return 0;
}
4.2 有参无返
调用时需要传递参数,但是不需要返回值的函数被称为有参无返类函数.
同样地, 其返回值类型为void, 也无法用来赋值.
示例:
//有参无返类
void function2(int num)
{
cout << num << endl;
}
int main()
{
function2(100);
return 0;
}
4.3 无参有返
字面意思. 有返回值则返回值类型不能是void, 且需要写return表达式.
有返回值, 函数可以被当做表达式, 可以用来赋值.
示例:
//无参有返类
int function3() //有返回值,返回值类型不能是void.
{
cout << "无参有返\t" ;
return 1000 ; //有返回值,必须要写return表达式.
}
int main()
{
int a = function3(); //有返,可以当做表达式来赋值; 无参,调用时不需要参数.
cout << a << endl;
return 0;
}
4.4 有参有返
既需要参数,又需要有返回值.
定义有参有返类函数时必须要写参数列表, 且返回值类型不能是void, 必须要写return表达式.
示例:
//有参有返类
//五大要素都要写齐,写对
int function4(int num1, int num2)
{
int multi = num1 * num2;
cout << "有参有返\t" ;
return multi;
}
int main()
{
int b = function4(8,9);//有返,可以当做表达式来赋值; 有参,调用时必须给参数.
cout << b << endl;
return 0;
}
5.函数的声明
声明函数的作用是, 在定义一个函数之前, 告知编译器该函数的存在.
未经声明的函数在定义时必须写在int main(){} 的前面,因为程序是从上往下一行行执行的.
如果一个函数经过声明,那么它的定义就可以写在int main(){} 后面.
(当然有的编译器可以不经声明就写在后面,不过稳妥起见最好还是写上声明.)
但要注意的是, 声明不等于定义, 只有声明没有定义的函数是无法调用的.
但只声明不定义的函数可能可以通过编译检查.
一个函数可以多次声明,但是只能定义一次.
声明一个函数只需要写返回值类型, 函数名 和 参数列表(可能需要). 注意要写分号.
其实就是没有定义主体的定义语句.
//声明函数. 注意写分号.
int add(int num1 , int num2);
//定义函数. 注意不要多写分号.
int add(int num1 , int num2)
{
int sum = num1 + num2 ;
return sum;
}
6.函数的分文件编写
分文件编写程序(一般来说, 一个文件封装一个或多个函数)的目的是使代码结构更清晰,便于阅读.
特别是对于那些大型项目来说, 如果海量代码都挤在一个文件里面, 将不利于代码维护工作.
分文件编写总共有4个步骤:
创建头(.h)文件, 创建源(.cpp)文件, 写声明, 写定义.
那么编译器时如何跨文件找到函数的声明和定义的呢?
一般来说, 主函数会放在一个cpp文件中, 封装好的功能函数放在另一个cpp文件中, 再创建一个头文件声明被封装的函数.
为了将三者关联起来, 需要在头文件中写 iostream 头文件 和 std 命名空间, 写好函数的声明, 在封装函数的源文件中写这个自建的头文件和函数的定义(这时就不需要 iostream 和 std 了.),在存放主函数的源文件中也写上这个自建的头文件, iostream 和 std , 但是就不再需要声明和定义了.
这样, 在主函数中调用函数时,会从主函数的头文件中找到自建的头文件, 再根据函数的声明找到存放函数定义的源文件(当然, 它们声明/定义的函数名称必须是一致的, 这也是它们关联的依据.)
6.1 创建后缀名为 .h 的头文件 + 写声明
注意头文件要存放在专门的文件夹中.
写头文件:
头文件中也要包含 iostream头文件 和命名空间std.
还要包含函数的声明.
#include <iostream>
using namespace std;
void swap(int num1, int num2);
6.2 创建源文件 + 写定义
这个大家都很熟悉就不说了, 但是需要掌握 如何将 头文件 和 源文件 关联起来.
其实与平时写头文件差不多, 区别在于, 此处的< > 应该改为双引号, 这样表示这个头文件是自建的.
并且头文件名称不再是 iostream , 而是你自建的头文件的名称, 注意还要包含后缀 .h .
这个源文件仅作封装函数用, 所以不需要包含iostream 和 std.
封装函数的源文件中当然要写函数的定义.
比如我写的头文件名称是 swap.h , 那么源文件中就应该这样写:
#include "swap.h"
void swap (int a ,int b)
{
int temp = a;
a = b;
b = temp;
cout << a << " , " << b << endl;
}
可以看出, 头文件和源文件中的形参命名不需要一致, 但数据类型必须一致, 不然会报错.
6.3 主函数文件调用
经过上面的步骤, 函数已经封装完成了,但是我们还没有调用它.
再创建一个源文件, 用于存放主函数的代码, 直接调用这个函数就可以了.
注意这个文件中, 一定不要再次定义这个函数, 否则等于多次定义, 会报错.
#include <iostream>
using namespace std;
#include "swap.h"
int main()
{
swap(3, 4);
return 0;
}
大家运行一下试试吧.
6.4 总结
最后总结一下, 一共三种文件分别要写什么 :
头文件, 写 iostream 和 std , 写函数的声明;
封装函数的源文件, 写自建头文件, 写函数的定义;
存放主函数的源文件, 写 iostream 和 std, 写自建头文件, 写主函数, 在主函数中调用封装函数.