C/C++ function passing arguments

参数传递

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

因为C中没有引用,所以C中传递参数的方式只有值传递,而C++中还有引用,不仅有值传递,还有引用传递。

值传参和引用传参

值传参

当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值。值传参的原理和这个一样,函数对形参做的所有操作都不会影响实参。

指针形参

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

引用传参

函数接受的参数是引用类型的话就是引用传参。通过使用引用形参,函数可以改变一个或者多个实参的值。
引用传参的好处:

  1. 避免拷贝,可以避免低效的拷贝操作,或者有些类型不支持拷贝,比如IO类型。
  2. 间接的实现多个返回值(也可以通过值传递指针实现)。

值传参和引用传参的区别

值传参是将原始变量的值拷贝一份给形参,函数对形参的操作不会影响实参(指针可以简介的修改)。
而引用传参是相当于直接把实参的引用给传递了形参,任何对形参的修改都是直接对实参的修改。

const形参和实参

当形参是const时,必须注意顶层const,顶层const作业于对象本身。当用实参初始化形参时,会忽略掉顶层const,即形参的顶层const被忽略掉了。当形参有顶层const时,传递给它常量或者非常量对象都是可以的。

指针或者引用形参和const

形参的初始化方式和变量的初始化方式是一样的,所以指针或者引用形参和const结合时,按照const变量的初始化规则执行就行。

尽量使用常量引用

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

数组形参

数组有两个特殊的性质:

  1. 不允许拷贝,因为不能拷贝数组,所以不能以值传递的方式使用数组参数。
  2. 在使用数组时通常会将其转换成指针。因为数组会被转换成指针,所以为函数传递数组时,实际上传递的是指向数组首元素的指针,这样子可以节约开销。

管理数组转换的指针

当传递给函数一个数组时,实参自动的转成指向数组首元素的指针,数组的大小对于函数的调用没有什么影响。因为数组是以指针的形式传递给函数的,所以函数其实是不知道数组的大小的,调用者应该为此提供一些额外的信息。通常有三种方式:

  1. 显示传递一个表示数组大小的形参
  2. 使用标记指定数组长度,要求数组本身包含一个结束标记,典型的例子是C风格字符串。
  3. 使用标准库规范,传递数组首元素和尾后元素的指针。可以使用beginend函数获得数组的首元素和尾后元素的指针。

数组形参和const

当函数不需要对数组进行写操作时,数组形参应该是指向const的指针(底层const)。只有当函数确实需要改变数组元素值的时候,才把形参定义成指向非常量的指针。

数组引用形参

C++允许将变量定义成数组的引用,形参也可以是数组的引用。此时,引用形参绑定到数组上。

传递多维数组

C++其实没有真正的多维数组,多维数组其实就是数组的数组。把多维数组传递给函数时,传递的是指向数组首元素的指针。而多维数组是数组的数组,首元素本身就是一个数组,多维数组转换成指向数组的指针。数组第二维以及后面维度都是数组类型的一部分,不能省略。

函数指针形参

和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。这个时候,形参看起来是函数类型,实际上是当成指针使用。
可以直接把函数作为实参使用,这个时候它会自动的转换成指针:

1
2
3
4
5
6
//第三个参数是函数类型,它会自动的转换成指向函数的指针
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
//第三个参数是显式声明的指向函数的指针
void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));
// 函数lengthCompare会被自动的转换成函数指针
useBigger(s1, s2, lengthCompare);

使用typedefdelctype简化函数指针

可以使用下列语句定义函数指针:

1
2
3
4
5
6
7
8
//下面两个是函数类型:
typedef bool Func(const string&, const string&);
typedef delctype(lengthCompare) Func2;


//下面两个是函数指针类型:
typedef bool (*FuncP)(const string&, const string&);
typedef delctype(lengthCompare) *FuncP2;

需要注意的是,decltype返回函数类型,不会将函数自动转换成指针类型,只有在前面加上*才能得到指针。

可变形参

当不知道向函数传递多少个参数时,C++ 11提供了两种方法处理不同数量实参,如果所有实参类型相同,传递initializer_list标准库类型,如果实参类型不同,编写特殊的函数,可变参数模板。
此外,C++ 还有一种特殊的形参,叫做省略符,可以用它传递可变数量的实参。

initializer_list形参

如果实参数量未知,但是类型相同,可以使用标准库类型initializer_list类型的形参。该标准库提供的操作如下:

  • initializer_list<T> lst,默认初始化,T类型元素的空列表
  • initializer_list<T> lst{a, b, c...},list元素是对应初始值的副本,列表中的元素是const
  • lst2(lst)lst2 = lst,注意,拷贝或者赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素。
  • lst.size()
  • lst.begin()
  • lst.end()

initializer_list是一个标准库类型,它也是一个模板,它的元素永远都是常量。

有一个疑问,就是std::initializer_liststd::vector有什么区别?[2,3,4]
拷贝std::initializer_list的时候并不会拷贝底层的对象。相当于拷贝了“指针”,或者说std::initializer_list有reference semantics而std::vector具有value semantics。

Initializer lists may be implemented as a pair of pointers or pointer and length. Copying a std::initializer_list does not copy the underlying objects.

省略符形参

省略符形参是为了便于C++访问某些特殊的C代码而设置的。省略符形参只能出现在形参列表的最后一个位置:

1
2
void foo(param_list, ...)
void foo(...);

main命令行选项

详细可以查看C/C++ main argc argv

参考文献

1.《C++ Primer》第五版
2.https://stackoverflow.com/questions/27753420/initializer-list-vs-vector
3.https://en.cppreference.com/w/cpp/utility/initializer_list
4.https://stackoverflow.com/questions/14414832/why-use-initializer-list-instead-of-vector-in-parameters