C++定义函数

  • Post author:
  • Post category:其他




一、函数传递参数的方式


术语:


主调函数

:调用其他函数的函数,大部分时候为main函数


被调函数

:被其他函数如main函数调用的函数


变元

:在主调函数中传递给被调函数的变量或常量,如函数调用语句function(a,b)中变元为a和b


形式参数

:主调函数传递给被调函数的变量或常量




1.按值传送机制


此类函数传送机制是将主调函数中的变元生成一个副本作为被调函数的形式参数,即将变元重新拷贝一份然后再传递给被调函数。显然,在这种传递机制下,在被调函数中对形式参数的操作并不会影响主调函数内的变量。



1.1传递普通变量


这里的普通变量是指基本类型变量,如 Int , double 等,当这些变量作为变元传递给被调函数后,被调函数可对这些变量进行操作,但不会影响主调函数内的变元。



1.2传递指针变量(指针还没整明白的同学

戳这里(指针专题)



首先应明确,指针变量的值是一个对应类型变量的地址,所以传递指针仍是按值传递。当我们将指针作为变元传递给被调函数时,程序依然会生成一个指针变量副本并作为形式参数传递给被调函数,因此形式参数指针存储的地址与原指针一模一样,即两个指针指向内存中同一块区域。因此,当我们在被调函数中对形式参数指针进行解引用赋值等操作时,对应内存区域上的值也会被改变。



1.3传递数组


当传递整个数组时,我们一般会选择将数组名传递给被调函数以节省时间开销。数组名本身是一个常量指针,指向内存中一块连续的存储数组元素的空间。当数组名作为变元时,数组名被复制后传递给被调函数,此时被调函数中的指针也指向同一块连续的内存空间,因此,我们也可以通过形式参数指针对这块内存进行操作。



1.4传递const 指针参数


通过1.2和1.3我们发现,每次传递指针后,被调函数都能直接通过形式参数指针来对原指针指向的内存空间进行修改,而有时我们并不希望被调函数修改它们。所以,当我们只需要访问元素而并不需要修改它们时,我们可以在形式参数指针前加上一个

const

关键词,如:

void function(const int* p) {
	*p = 3;//无法编译,因为此时p是常量,无法作为表达式左值
	std::cout << *p;//可以访问
}
int main()
{
	int a{ 10 };
	int *pa{ &a };
	function(pa);
}


值得注意的一点是,


非const变元可以传递给const形式参数和非const形式参数,但const变元只能传递给const形式参数。





2.按引用传送机制



什么是引用


引用是一个变量的别名,也可以说,引用等价于原变量。一个引用必须满足以下两个条件:1.引用不可为空,一定要初始化。2.一旦初始化后,引用不可改变其指向。要使用左值引用类型,只需在类型名后面加上&,如:

	int variable{ 3 };
	int &cite = variable;
	cite++;
	std::cout << variable;//结果为4


此时 cite 是 variable 的引用,对 cite 的操作完全等价于对 variable 的操作。



按引用传递参数


当被调函数的形式参数为引用时,同样的,它就是主调函数中变元的别名。此时,在传递过程中,程序不会生成变元副本,而是直接将变元本身传递过去。因此在被调函数中对引用参数的操作会直接影响主调函数中的变元。主调函数中变元地址与引用参数地址是相同的。

void function(int &cite) {
	cite++;
	std::cout << &cite << std::endl;//引用参数cite的地址
}
int main()//从main函数开始阅读程序是一个良好的习惯
{
	int variable{ 3 };
	function(variable);
	std::cout << variable << std::endl;//结果为4,其值已经在function中加1
	std::cout << &variable << std::endl;//主调函数中变元地址
}


在上面代码中,两个输出的地址结果是一样的,这也进一步表明了,

引用只是一个变量的别名,它等价于原变量。


那么,为什么我们要按引用给函数传递参数呢?


一个明显的优点是,引用不需要对变元进行复制,特别是一些长的 string 字符串或者是矢量 vector,能节省很多的时间开销。


同理,当我们只需要访问元素而不希望修改它们时,我们仍可以在引用参数前加上

const

关键词。如:

void function(const int &cite) {
	cite++;//无法编译,cite为常量
}
int main()
{
	int variable{ 3 };
	function(variable);
}



二、函数默认参数值


可以为一个函数设置默认的变元值,即只有我们在希望参数值不同于默认值时,才指定参数值。有默认值的形式参数要放在参数列表的最后面。

需要注意的是,默认变元值只能在函数原型或函数声明中指定,而不是在函数定义中。如:

void function(int x = 3);//函数声明
void function(int x) {//函数定义
	std::cout << x << std::endl;
}
int main()
{
	int variable{ 3 };
	function();//输出3
	function(10);//输出10
}


当有多个默认参数时,所有默认参数都要同一放在参数列表的后面,且

调用函数时省略一个变元,其后面的所有变元都必须要一同省略。因此,应将最可能省略的变元放在参数列表最后面。



三、函数返回值



1.返回基本类型


函数中返回一个基本类型是简单的,只需要 return 关键词后加上想要返回的变量即可。



2.返回指针


指针又来了,当我们从函数中返回指针时,要永远遵循一个

Golden rule

:




永远不要从函数中返回自动局部变量的地址。


好好剖析一下这句话:

自动局部变量是作用域为函数的变量,它在一个函数中被创建,内存自动为其在栈上分配一块空间。当函数体结束时,内存为其分配的空间又自动释放。

因此,若函数返回自动局部变量的指针,那么这个指针所指的内存上的位置实际上是一块不存储任何变量的空间

,因为自动局部变量已经被释放掉了,因此返回的指针是一个“野指针”。

不过奇怪的是

,当我们在主函数中为返回的野指针解除引用,好像又能得到我们”希望“的值,如下:

int* function(int x) {
	int a = x;
	return &a;
}
int main()
{
	int *p = function(3);
	std::cout << *p << std::endl;//结果竟为3
}


这又是为什么, 为什么野指针 p 解除引用后能得到 function 函数中自动局部变量的值?

其实,当自动局部变量被释放掉后,它原来占用的这一块内存就会立即被标为可使用,也就是可供其他变量存储。

但是,这块内存存储的值在未被其他变量覆盖前仍是原来自动局部变量的值

,此时的野指针 p 仍可以访问其上的值。


那么我们要返回怎样的指针呢?答案可以是手动分配内存的局部变量的指针!


当我们在函数中用

new,malloc

关键字时,我们为局部变量在堆上手动分配了一块内存,这块内存只有到程序结束或者我们手动用

delete,free

关键词才会被释放,而不会随着函数的结束而释放,所以函数返回手动分配内存的指针不是野指针。



四、函数的重载


我们常常需要用两个或多个函数完成相同的任务,但其参数列表不同,在这种情况下我们可以使用

名称相同的重载函数。


不同的函数有不同的函数签名,函数签名包括一个函数的名称及其参数列表,但不包括函数返回类型。重载函数也是不同的函数,既然它们的名称是相同的,那么其参数列表一定不能相同。重载函数满足以下两个条件之一:

1.函数参数个数不同。

2.至少有一对对应参数的类型不同。

如以下两个重载函数的函数:

int function(int a,int b) {
	return a + b;
}
double function(double a, double b) {
	return a + b;
}
int main()
{
	double a{ 3.14 }, b{ 4.13 };
	double result{ function(a,b) };
	std::cout << result << std::endl;//结果为3
}


以上两个函数的参数类型不同。当我们在主调函数中调用 function 函数时,编译器会自动识别变元类型,并匹配相应的重载函数,若找不到相应的重载函数,编译器便会报错。



五、函数模板



5.1函数模板生成与调用


在上述两个重载函数中,唯一区别是它们的参数类型不同,其他代码完全一样。此时,我们可以定义一个函数模板,函数模板是定义一系列函数的蓝图,它不是函数的定义。编译器在需要时使用函数模板生成一个函数定义,如果不需要,就不从模板中生成代码。从

函数模板

中生成的函数定义称为该模板的一个实例,称为

模板函数



上述重载函数的函数模板如下:

template <typename T> 
T function(T a, T b) {
	return a + b;
}


其中,

template

关键字标识一个函数模板,

typename

关键字把 T 标识为类型。这个函数模板的参数为类型,当我们在主调函数中调用函数 function 时,编译器会自动推断变元的类型,并用该实际类型替换类型 T 。如代码调用模板:

double a{3.14},b{4.13};
double result;
result=function(a,b);


实际等价于调用函数:

double function(double a, double b) {
	return a + b;
}


我们也可以显示指定数据类型 T 为任意类型如 double,代码如下:

double a{3.14},b{4.13};
double result;
result=function<double>(a,b);


在函数模板调用语句中,< >内的参数是模板的参数列表,( )内是函数的参数列表



5.2函数模板的特例


5.1内函数模板在某些情况下会不适用,或者说无法达到我们预期的结果,如:

	double a{ 3.14 }, b{ 4.13 };
	double *pa{ &a }, *pb{ &b };
	double* result=function(pa,pb);
	


此时,调用5.1的函数模板编译器会报错,因为我们不能使两个指针直接相加,而我们希望的是返回存储两个变量 a 和 b 相加的结果的指针。对于这种情况,我们可以有两种解决方法供选择:


1.直接用函数重载方法重载模板函数,编译器优先调用该函数:

double* function(double *a, double *b) {
	double temp = *a + *b;
	double *p = &temp;
	return p;
}


2.定义一个5.1函数模板的特例(注意:特例应放在函数模板声明或定义之后)在特例里,template后面的尖括号必须为空,并且手动将类型T替换为我们希望的类型,形式如下:

template <>
double* function(double* a, double* b) {
	double temp = *a + *b;
	double *p = &temp;
	return p;
}



5.3函数模板的重载


函数模板与函数模板之间可以重载,重载规则与普通函数重载相同,即函数内参数列表不同。以下模板为5.1模板的重载:

template<typename T>
T function(vector<T>&data) {
	for (size_t i = 0; i < data.size(); i++)
	{
		data[i] = data[i] * data[i];
	}
}



5.4带有多个参数的函数模板


前面的函数模板只有一个参数 T ,我们也可以在函数模板内使用多个参数,当有一个参数提供函数返回类型时,必须在函数调用时显示指定返回类型,因为编译器无法推断函数模板返回类型。

两个参数的函数模板:

template<typename F,typename T>
F function(T a, T b) {  //F是返回类型,T是参数类型
	return a + b;
}
int main()
{
	double a{ 3.14 }, b{ 4.13 };
	double result=function<int,double>(a, b);//返回类型为int
	std::cout << result << std::endl;//输出为7
}


下图可帮助理解函数模板与函数调用

函数模板与函数调用



六、拖尾返回类型



知识前提



decltype(expression)

关键字可以返回括号内表达式的类型。


带有多个参数的函数模板的返回类型可能是不确定的,这时我们首先想到的是可以让用户指定返回类型,如模板:

template <typename Treturn,typename T1,typename T2>
Treturn function(vector<T1>& a, vector<T2>& b) {
	Treturn sum{};
	for (size_t i = 0; i < ; i++)//假设两个vector元素数量相同
	{
		sum += a[i] * b[i];
	}
	return sum;
}


这个模板的作用是返回两个不同类型 vector 对应元素乘积之和,但是 T1 和 T2 我们是不确定的,我们不能保证 T1 和 T2 相加一定会得到我们设定的 Treturn 类型,如果能让编译器自己判断返回类型,可能会更好。C++11提供了

拖尾返回类型

,其形式为:

auto function(vector<T1>& a, vector<T2>& b)->decltype(a[0]*b[0]) {//拖尾返回类型
	Treturn sum{};
	for (size_t i = 0; i < ; i++)//假设两个vector元素数量相同
	{
		sum += a[i] * b[i];
	}
	return sum;
}



auto

关键字表示让编译器自动推断返回值类型,

decltype

(a[0]*b[0])表示返回 a[0] 和 b[0] 相乘后结果的类型,即T1 和 T2 作用后的类型。比如

int



double

类型的变量相乘后为

double

类型,那么

decltype

得到的返回类型就是

double

。->表示拖尾返回数值。



七、函数指针


与普通变量指针类似,函数指针是可以存储函数地址的变量,它可用于调用自身指向的地址上的函数。但与普通指针相比,函数指针要略微复杂,它还必须存储每个参数的类型以及返回类型。

函数指针基本形式为:

返回类型 ( * 指针名)(参数列表)

如:

int function(int a, int b) {
	return a + b;
}
int main()
{
	int(*pfun)(int, int) = function;
}


此时,pfun 为指向函数 function 的指针,它只能指向返回类型为 int ,参数列表为(int,int)的函数,绝不可以指向返回类型或参数列表与 pfun 定义不同的函数。

为了方便,我们也可以直接让编译器来推断函数指针的类型:

int function(int a, int b) {
	return a + b;
}
int main()
{
	auto pfun2 = function;
}


此时的 pfun2 与 pfun 是一模一样的函数指针。

另外,函数指针也一样必须要初始化,否则是个野指针。



有问题欢迎私信交流,转载请注明出处,谢谢~



版权声明:本文为weixin_43692909原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。