C++ 函数

本文详细介绍了C++中函数的定义、调用以及参数传递的不同方式,包括值传递、地址传递和引用传递。通过示例代码展示了函数如何接收参数、返回值以及如何在不同的函数文件中组织代码。此外,还讨论了无参无返、有参无返、无参有返和有参有返四种函数样式,并强调了函数声明和定义的重要性。
摘要由CSDN通过智能技术生成

目录

1. 概述

2.函数的定义和调用

2.1 返回值类型:

2.2 函数名:

2.3 参数列表: 

2.3.1 形参

2.3.2 实参

2.4 函数体语句:

2.5 return 表达式:

2.6 示例:

3. 参数传递

3.1 值传递

3.2 地址传递

3.3 引用传递

4.函数的样式

4.1 无参无返

4.2 有参无返

4.3 无参有返

4.4 有参有返

5.函数的声明

6.函数的分文件编写

6.1 创建后缀名为 .h 的头文件 + 写声明

6.2 创建源文件 + 写定义

6.3 主函数文件调用

6.4 总结


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; 这种引用是非法的.

C++ 引用_AusrEnder的博客-CSDN博客

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, 写自建头文件, 写主函数, 在主函数中调用封装函数.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值