zl程序教程

您现在的位置是:首页 >  后端

当前栏目

C++Primer第五版学习(函数部分 二)

C++学习 函数 部分 primer
2023-09-11 14:17:48 时间

今天学习的内容时C++ Primer函数这一章的第二节内容,本章内容围绕参数传递展开,主要有以下内容:

参数传递

1.传值参数

2.传引用参数

3.const形参实参

4.数组形参

5.main:处理命令行选项

6.含有可变形参的函数


参数传递

形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。

当形参是引用类型时,则可以说它对应的实参被引用传递(passed by reference)或者函数被传引用调用(called by reference),和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。

当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。则可以说这样的实参被值传递(passed by value)或者函数被传值调用(called by value)。

1.传值参数

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

指针形参

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

int n =o, i = 42;
int *p=in, *q=&i;     //p指向向n,q指向i
*p= 42             //n的值改变; p不变
p=q;              //p现在指向了i;但是i和n的值都不变

指针形参的行为与之类似

//该函数接受一个指针,然后将指针所指的值置为。
void reset (int *ip)
{    
*ip=0; //改变指针ip所指对象的值    
ip =0; //只改变了ip的局部拷贝,实参未被改变
}

2.传引用参数

对于引用的操作实际上是作用在引用所引的对象上。

使用引用避免拷贝

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

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

//比较两个string对象的长度bool isShorter (const string &sl, const string &s2){   return sl.size() < s2.size();}

如果函数无须改变引用形参的值,最好将其声明为常量引用。

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

一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。


3.const形参实参

我们都知道顶层const作用于对象本身,举例如下:

const int ci =42;     //不能改变ci, const是顶层的int i = ci;          //正确:当拷贝ci时,忽略了它的顶层constint * const p=&i;     // const是顶层的,不能给p赋值*p=0;               //正确:通过p改变对象的内容是允许的,现在i变成了0

和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。换句话说,形参的

顶层const被忽略掉了。当形参有顶层const时,传给它常量对象或者非常量对象都是

可以的:

void fcn(const int i){/*fcn能够读取i,但是不能向i写值*/ }调用fcn函数时,既可以传入const int也可以传入int。忽略掉形参的顶层const可能产生意想不到的结果:void fcn(const int i){/* fcn能够读取i,但是不能向i写值*/ }void fcn(int i){/*...*/ } //错误:重复定义了fcn(int)

指针或引用形参与const

形参的初始化方式和变量的初始化方式是一样的,我们可以使用非常量初始化一个底层const对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。

int i =42const int *cp = &i; //正确:但是cp不能改变iconst int &r=i; //正确:但是r不能改变iconst int &r2 =42; //正确int *p =сp; //错误: p的类型和cp的类型不匹配int &r3 = r; //错误: r3的类型和r的类型不匹配int &r4 = 42; //错误:不能用宇面值初始化一个非常量引用

4.数组形参

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

尽管不能以值传递的方式传递数组,但是依然可以把形参写成类似数组的形式:

//尽管形式不同,但这三个print函数是等价的//每个函数都有一个const int*类型的形参void print (const int*);void print (const int []); //可以看出来,函数的意图是作用于一个数组void print (const int [10]); //这里的维度表示我们期望数组含有多少元素,实际不一定

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

int i =0, j [2] ={0, 1};   print (&i);       // 正确: &i的类型是int*print (j);  //正确: j转换成int*并指向j[0]

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

(1) 使用标记指定数组长度

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

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

(2 ) 使用标准库规范

管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针。使用该方法,我们可以按照如下形式输出元素内容:

void print (const int *beg, const int *end){   //输出beg到end之间(不含end)的所有元素    while (beg != end)    {          cout << *beg++ << endl; // 输出当前元素并将指针向前移动一个位置      }}

(3)显式传递一个表示数组大小的形参

第三种管理数组实参的方法是专门定义一个表示数组大小的形参,在C程序和过去的

C++程序中常常使用这种方法。使用该方法,可以将print函数重写成如下形式:

// const int ia[]等价于const int* ia// size表示数组的大小,将它显式地传给函数用于控制对ia元素的访问void print (const int ia [], size_t size){    for (size_t i= 0; i != size; ++i)    {        cout << ia [i] << endl;    }}

数组引用形参

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

//正确:形参是数组的引用,维度是类型的一部分void print (int (&arr) [10]){    for (auto elem : arr)    {     cout << elem << endl;         } }

&arr两端的括号必不可少.

f(int &arr[10]) //错误:将arr声明成了引用的数组f(int (&arr) [10]) //正确: arr是具有10个整数的整型数组的引用

因为数组的大小是构成数组类型的一部分,所以只要不超过维度,在函数体内就可以放心地使用数组。但是,这一用法也无形中限制了print函数的可用性,我们只能将函数作用于大小为10的数组:

int i = 0, j [2] = [0, 1];int k [10] = [0,1, 2,3,4,5,6,7,8,9);print (&i); //错误:实参不是含有10个整数的数组print (j); //错误:实参不是含有10个整数的数组print (k) ; //正确:实参是含有10个整数的数组

5.main:处理命令行选项

main函数是演示C++程序如何向函数传递数组的好例子。到目前为止,我们定义的main函数都只有空形参列表:

int main () {... }

然而,有时我们确实需要给main传递实参,一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作。例如,假定main函数位于可执行文件prog之内,我们可以向程序传递下面的选项:

prog -d -o ofile data0

这些命令行选项通过两个(可选的)形参传递给main函数:

int main (int argc, char *argv []) {     ...})

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

表示数组中字符串的数量。因为第二个形参是数组,所以main函数也可以定义成:

int main (int argc, char **argv){...}  //其中argv指向 char*。

当实参传给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,

接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。

以上面提供的命令行为例, argc应该等于5, argv应该包含如下的C风格字符串:

argv [0] = "prog"; // 或者argv [0]也可以指向一个空字符串argv [1] = "-d";argv [2] = "-O";argv [3] = "ofile";argv [4] = "data0";argv [5]= 0;

当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。

6.含有可变形参的函数

有时我们无法提前预知应该向函数传递几个实参。为了编写能处理不同数量实参的函数, C++11新标准提供了两种主要的方法:

(1)如果所有的实参类型相同,可以传递一个名为initializer list的标准库类型;

(2)如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板,这部分内容在后面学习。

C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参,这种功能一般只用于与C函数交互的接口程序。

initializer_list 形参

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

initializer_list lst;     默认初始化; T类型元素的空列表initializer_list lst(a,b,c...);   lst的元素数量和初始值一样多;                               lst的元素是对应初始值的副本;列表中的元素是constlst2 (lst)  拷贝或赋值一个initializer_list对象,不会拷贝列表中的元素;
            拷贝后,原始列表和副本共享元素lst2 = lstlst.size() 列表中的元素数量lst.begin () 返回指向lst 中首元素的指针lst.end () 返回指向lst中尾元素下一位置的指针

和vector一样, initializer list也是一种模板类型,定义initializer list对象时,必须说明列表中所含元素的类型;

initializer_list<string> ls; // initializer_list的元素类型是stringinitializer_list<int> li; // initializer_list的元素类型是int

和vector不一样的是:initializer_list对象中的元素永远是常量值,我们无法改变initializer list对象中元素的值。

省略符形参

省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。通常,省略符形参不应用于其他目的。省略符形参应该仅仅用于C和C++通用的类型

省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:

void foo (parm_list, ...);void foo (...);