6.2 参数传递


如前所述,每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。

和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。当形参是引用类型时,我们说它对应的实参被引用传递(或者函数被传引用调用。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。

当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递或者函数被传值调用。

传值参数

当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响对应的初始值。

指针形参

指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值

#include<iostream>
using namespace std;

void result(int* p)
{
    cout << "前:result内部的*p:" << *p << endl;
    cout << "前:result内部的p:" << p << endl;
    *p = 0;
    p = NULL;
    cout << "后:result内部的p:" << p << endl;
}

int main()
{
    int i = 42;
    int* q = &i;
    result(q);
    cout << "外部的*q:"<< * q << endl<<"外部的q:" << q << endl;
    return 0;
}

输出结果:
在这里插入图片描述
所以对于上述情况下,有很多未知的情况会被我们直接忽略:

熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。在C++语言中,建议使用引用类型的形参替代指针。

传引用参数

回忆过去所学的知识,我们知道对于引用的操作实际上是作用在引用所引的对象上

#include<iostream>
using namespace std;

void result(int& p)
{
    cout << "前:result内部的p:" << p << endl;
    p = 0;
    cout << "后:result内部的p:" << p << endl;
}

int main()
{
    int i = 42;
    cout << "前:外部的p:" << i << endl;
    result(i);
    cout << "前:外部的p:" << i << endl;
    return 0;
}

输出结果:
在这里插入图片描述
在上述调用过程中,形参p仅仅是的i的一个名字。在result内部对p的使用即是对i的使用。

使用引用避免拷贝

拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括I0类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。

举个例子,我们准备编写-一个函数比较两个string对象的长度。因为string对象可能会非常长,所以应该尽量避免直接拷贝它们,这时使用引用形参是比较明智的选择。又因为比较长度无须改变string 对象的内容,所以把形参定义成对常量的引用:

样例:

void my_function(const string&s1,const string& s2)
{
    if (s1.size()>s2.size())
    {
        cout << "前者大";
    }
    else if (s1.size()==s2.size())
    {
        cout << "一样大";
    }
    else
    {
        cout << "后者大";
    }
}

使用引用形参返回额外信息

一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。举个例子,我们定义一个名为find char 的函数,它返回在string对象中某个指定字符第一次出现的位置。同时,我们也希望函数能返回该字符出现的总次数。

该如何定义函数使得它能够既返回位置也返回出现次数呢?一种方法是定义一个新的数据类型,让它包含位置和数量两个成员。还有另一种更简单的方法,我们可以给函数传入一个额外的引用实参,令其保存字符出现的次数:

样例:

#include<iostream>
#include<string>
using namespace std;

int my_function(const string&s1,char c,int& num)
{
    num = 0;
    int ret = 0;
    for (size_t i = 0; i < s1.size(); i++)
    {
        if (s1[i] == c)
        {
            num++;
            if (num==1)
            {
                ret = i+1;
            }
        }
    }
    return ret;
}

int main()
{
    int num;
    string s1 = "adqwdawdwadwadaw";
    if (my_function(s1, 'd', num)!=0)
    {
        cout << "出现第一次d是第" << my_function(s1, 'd', num) << "位数(从1开始)" << endl;
        cout << "出现的次数是:" << num<<endl;
    }
    else
    {
        cout << "找不到该元素"<<endl;
    }
    return 0;
}

输出结果:
在这里插入图片描述

const形参和实参

当形式参数是const类型的时候,我们需要注意的是关于顶层const的讨论。

void fcn(const int i)
void fcn(int i)
/*上面两个重复了,虽然看的出来一个是可以修改内部的值,一个是不能,
但对于程序来说,这两个就是重复的,原因大家学完顶层const和函数重载,
可以自己想想为什么,还是很好理解的。*/

指针或引用形参与const

形参的初始化方式和变量的初始化方式是一样的,所以回顾通用的初始化规则有助于理解本节知识。我们可以使用非常量初始化一个底层 const 对象,但是反过来不行,同时一个普通的引用必须用同类型的对象初始化。

尽量使用常量引用

把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。就像刚刚看到的,我们不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。

总而言之,不采用常量引用可能造成传入的类型错误,比如 string& s="hello"类似的这一种错误,还有就是修改了该string内部的值,导致此string后续再也用不了。

为了解决需要修改时,我们可以不采用引用,如此就相当于拷贝构造桉树,或者形参是const &类型的内部进行一次拷贝构造。

数组形参

数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组以及使用数组时(通常)会将其转换成指针。因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。

void function(const int*)
void function(const int[])
void function(const int[number])

尽管表现形式不同,但上面的三个函数是等价的:每个函数的唯一形参都是constint类型的。当编译器处理对function函数的调用时,只检查传入的参数是否是const int类型:

int i = 1 , j[2] = {1,2} ;
function(&i);//正确
function(j);//正确,即使我们传入的是一个数组,也会自动转化为指针进行传入

和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。

因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术。

使用标记指定数组长度

管理数组实参的第一种方法是要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串。C风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符。函数在处理C风格字符串时遇到空字符停止。

这种方法适用于那些有明显结束标记且该标记不会与普通数据混淆的情况,但是对于像int这样所有取值都是合法值的数据就不太有效了。

使用标准库规范

管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针,这种方法受到了标准库技术的启发。

void function(const int* beg,const int* end)
{
	while(beg!=end)
	{
		cout<<*beg++<<endl;
	}
}

而这种情况下对于函数的使用就需要:

int j[2] = {1,2};
function(begin(j),end(j));

这也是本文比较推荐的方式。

显式传递一个数组的长度

第三种管理数组实参的方法是专门定义一个表示数组大小的形参,在C程序和过去的C++程序中常常使用这种方法。使用该方法,可以将function函数重写成如下形式:

void function(const int* p,size_t size)
{
	for(size_t i = 0;i != size;i++)
	{
		cout<<*i<<endl;
	}
}

数组形参和const

我们的三种方式下函数都把数组形参定义成了指向const的指针,关于引用的讨论同样适用于指针。当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。

数组引用形参

C++语言允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上:

void function(const int(&arr)[2])
{
    for (int i : arr)
    {
        cout<<i<<endl;
    }
}

值得注意的是,此时内部的2必须填上,这样才会被当作数组。

而此时我们在使用这个函数的时候就一定要传入一个大小为2的数组,比如上文的j数组。

PS:

int j[2] ={1,2}; 
int (&r)[2] = &j;//正确,r是数组j的别名
int &r[2] = &j;//错误,引用的数组,不存在引用的数组。

传递多维数组

我们曾经介绍过,在C++语言中实际上没有真正的多维数组,所谓多维数组其实是数组的数组。

和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为我们处理的是数组的数组,所以首元素本身就是一一个数组,指针就是一一个指向数组的指针。数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略:

void function(const int (*arr)[10]);
void function(const int arr[][10]);

上述两种做法也都是一致的,但是值得注意的是10那个数字是一定要填上的,第二种做法下的[]可填可不填(建议不填)。

main:处理命令行选项

第二个形参argv是一个数组,它的元素是指向C风格字符串的指针;第一个形参 argc表示数组中字符串的数量。

而具体理解,实际上就是运行main函数之前,使用者输入的字符串的数目,以及具体的是什么,值得注意的是:对于argv[0]来说存储的一定是main函数的执行地址。所以实际上是从argv[1]开始储存的。

实际上这里的用法主要体现在linux的c表达上,这里只是简单的介绍一下(用处不大):

#include<iostream>

using namespace std;

int main(int argc , char* argv[])
{
    int i;
    string s = "aaa";
    cout << "argc=" << argc<<endl;
    for ( i = 0; i < argc; i++)
    {
        cout << "argv[" << i << "]=" << argv[i]<<endl;
    }
    return 0;
}

这段代码可以自行运行一下,促进一下理解。

含有可变形参的函数

有时我们无法提前预知应该向函数传递几个实参。例如,我们想要编写代码输出程序产生的错误信息,此时最好用同一个函数实现该项功能,以便对所有错误的处理能够整齐划一。然而,错误信息的种类不同,所以调用错误输出函数时传递的实参也各不相同。为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为initializer_ list 的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板,关于它的细节将在之后进行介绍。

C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参。本节将简要介绍省略符形参,不过需要注意的是,这种功能一般只用 于与C函数交互的接口程序。

initializer_list形参

如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_ list类型的形参。initializer_ list是一种标准库类型,用于表示某种特定类型的值的数组。initializer_ list类型定义在同名的头文件中,它提供的操作如下表。

格式意义
initializer_list<T> p ;默认初始化,T类型元素的空列表
initializer_list<T> p{a,b,c···} ;拷贝初始化,对应元素相整合,内部元素的类型是const的
p1(p2)或者p1=p2拷贝或者赋值
p.size()返回列表中元素的数量
p.begin()返回头指针
p.end()返回尾指针

和vector一样,initializer_list 也是一种模板类型。和vector不一样的是,initializer_ list 对象中的元素永远是常量值,我们无法改变initializer_ list 对象中元素的值。

给个样例:

#include<iostream>
#include<initializer_list>
#include<string>

using namespace std;

void my_function(initializer_list<string> list)
{
	for (auto beg = list.begin();beg!=list.end();beg++ )
	{
		cout << *beg << " ";
	}
	cout << endl;
}

int main()
{
	bool flag;
	cin >> flag;
	if (flag)
	{
		my_function({ "正确的输出",":","Hello World" });
	}
	else
	{
		my_function({ "错误的输出","helloworld" });
	}
    return 0;
}

输出结果:
在这里插入图片描述
在这里插入图片描述

省略符形参

省略符形参是为了便于C++程序访问某些特殊的C代码而设置的这些代码使用了名为varargs的C标准库功能。通常,省略符形参不应用于其他目的。你的C编译器文档会描述如何使用varargs。

省略符形参应该仅仅用于C和C++通用的类型。特别应该注意的是,大多数类型的对象在传递给省略符形参时都无法正确拷贝。

但实际上用处很小,建议还是用上面的用法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值