💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 一.参数 1. **形参** 函数定义时,参数的名称。 2. **实参** 形参的初始值。第一个实参初始化第一个形参,第二个实参初始化第二个形参,以此类推。尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序。编译器能以任意可行的顺序对实参求值。 ## 二.参数传递 1. 传值调用 + C++中参数传递的缺省初始化方法是把实参的值拷贝到参数的存储区中,此时函数不会访问当前调用的实参。 + 数组作参数时,传递的是数组第一个元素的地址。 > 数组形参会被编译器替换成指向数组第一个元素的指针形参。 + 此时在函数内改变形参的值对实参没有影响。 2. 传指针调用 + 指针使我们可以间接地访问它所指的对象,通过指针可以修改它所指对象的值。 + 修改指针的指向对实参无影响。 3. 传引用调用 + 当参数是引用时,函数接收的是实参的左值。此时修改参数可以影响实参。 + 传引用调用通常用于以下情况: + 需要直接修改实参 + 传递一个较大的对象 + 从函数返回多个值 4. 引用和指针参数的关系 >引用必须被初始化为指向一个对象,一旦初始化了,它就不能再指向其他对象。指针可以指向一系列不同的对象也可以什么都不指向。 >[info]如果一个参数可能在函数中指向不同的对象,或者这个参数可能不指向任何对象,则必 须使用指针参数。 ## 三.默认参数 1. 函数调用的实参按位置解析,缺省实参只能用来替换函数调用缺少的尾部实参。 2. 定义/声明时,默认参数应 **从右向左** 依次定义。 >[danger] [错了] `int fun(int a = 0,double b,char c)` >[success] [对的] `int fun(int a,double b,char c = '\n')` 3. 调用函数时, 参数 **从左向右** 匹配。 ```c++ #include <iostream> using namespace std; int fun(int a = 0,int b = 1,int c = 2,int d = 3) { cout<<"a="<<a<<";b="<<b<<";c="<<c<<";d="<<d<<";"<<endl; return 0; } int main() { fun(2019);//a=2019 fun(22,33);//a=22 b=33 fun(2019,2020,2021);//a=2019 b=2020 c=2021 fun(2017,2018,2019,2020);//a=2017 b=2018 c=2019 d=2020 } ``` >[test] >a=2019;b=1;c=2;d=3; >a=22;b=33;c=2;d=3; >a=2019;b=2020;c=2021;d=3; >a=2017;b=2018;c=2019;d=2020; >Press any key to continue 3. 既有声明又有定义时,定义中可省略默认参数;只有定义时,默认参数须出现在定义中。 4. 含有普通局部变量的表达式不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参。 >[success]默认参数的函数调用在编译期时就需要确定其值,或者其地址。 > >+ 常量表达式能够确定其值。 >+ 静态变量能确定其地址。 >+ 含有静态变量的表达式能够确定其值。 >+ 普通局部变量本身在编译期值不确定,地址也不确定。 >+ 含有普通局部变量的表达式的值在编译期无法确定。 >[warning]默认实参表达式的值的计算发生在函数调用的时候。 >[success] [14+] 允许不被求值的局部变量作为函数形参,如 `sizeof(局部变量)` ## [$]四.省略符形参 ### 1.省略符形参实现可变参数 1. 定义一个可变参数的函数:在形参列表末尾加上 `...` ```c++ void one_more_args(int size,...) { //your code } void zero_more_args(...) { //your code } ``` 接下来便可以这么调用: ```c++ one_more_args(5,1,2,3,4,5); zero_more_args(1,2,3); zero_more_args(1,2,3,4); ``` 需要注意的是,如果省略符形参前面没有固定的参数,那么这些省略号参数是不能正常接收的。 2. 接收可变参数 需要引入 `cstdarg` 或 `stdarg.h` 头文件。并确保函数至少有一个固定的形参。 ```c++ long add(int num,...) { long result = 0; va_list args;//定义一个形参列表 va_start(args, num); //初始化 args 变量,其中 size 是最后一个已知形参 for (int i = 0; i < num; i++) { result += va_arg(args, int);//读取传入的参数 } va_end(args); //参数读取完毕 return result; } ``` 接下来就可以实现无限的数字相加了: ```c++ cout<<add(4,1,5,8,7); ``` >[test] >21 ### 2.省略符形参实现可变参数的浅层原理 函数调用时,参数(除了引用形参)会按照形参表的顺序依次入栈,每个实参彼此相邻(由于编译器的内存对齐策略,这些实参之间可能会有少量空隙)。这样,我们就可以通过指针来依次访问实参。 + `va_list` 存储有函数参数列表的信息,理论上是一个字符指针。 + `va_start` 是一个宏函数。第一个参数为 `va_list` ,第二个参数为最后一个固定参数。根据固定参数的存储位置和大小,可以算出第一个不定参数的位置,并赋值给第一个参数。 + `va_arg` 是一个宏函数。第一个参数为 `va_list` ,第二个参数为读取的类型名称。根据第一个参数和类型名称的大小,可以算出下一个参数的内存位置。 + `va_end` 是一个宏函数。将第一个参数置为空。 ### 3.省略符形参的应用 + `scanf` + `printf` ### 4.省略符形参的局限性 1. 对类类型的支持不友好,有时构造函数无法正常调用。 ```c++ class MyInt { private: int num_; public: MyInt(int num) : num_(num) { cout << "构造MyInt,值为 " << num << endl; } friend MyInt &operator+=(MyInt &m1, const MyInt &m2); friend ostream &operator<<(ostream &os, const MyInt &m); }; MyInt &operator+=(MyInt &m1, const MyInt &m2) { m1.num_ = m1.num_ + m2.num_; return m1; } ostream &operator<<(ostream &os, const MyInt &m) { return os << m.num_; } MyInt add(int num, ...) { MyInt result = 0; va_list args; //定义一个形参列表 va_start(args, num); //初始化 args 变量,其中 size 是最后一个已知形参 for (int i = 0; i < num; i++) { result += va_arg(args, MyInt); //读取传入的参数 } va_end(args); //参数读取完毕 return result; } ``` 相加: ```c++ cout << add(4, 1, 5, 8, 7); ``` >[test] >构造MyInt,值为 0 >21 从输出结果来看,这里的四个省略符参数并没有正常构造。 >[warning] 一般省略符形参应该仅仅用于C和C++通用的类型。 2. 不支持引用形参。因为引用形参不会占用空间。 3. 编译器不会对省略符形参进行类型检查。 ## \[11+\][$]五.initializer list 形参 `initializer_list` 是一种标准库模板类,用于表示某种特定类型的值的数组。 它定义在 `initializer_list` 头文件中。 ### 1.用途 + 实参数量未知 + 全部实参的类型相同 ### 2.基本操作 + `initializer_list<T> list;` 定义一个`initializer_list`。 + `initializer_list<T> list{a,b,c,...};` 定义并使用大括号初始化一个`initializer_list`。 + 和其他对象一样的拷贝操作 + `list.size()` 元素的数目。 + `list.begin()` 返回一个迭代器,指向首元素。 + `list.end()` 返回一个迭代器,指向末尾元素的下一个元素。 ### 3.initializer_list 形参实现不定参数 ```c++ long add(initializer_list<int> list) { long result = 0; //通过迭代器访问元素 for (auto iter = list.begin(); iter != list.end(); iter++) result += *iter; return result; } ``` 调用函数: ```c++ //调用时,这些参数需要加上大括号 cout << add({1, 3, 5, 7, 9}); ``` >[test] >25 ## \[11+\][$]六.转发 ### 1.概念 + 把实参传递给其他函数 ### 2.不完整的转发 #### 例1 ```c++ //用于调用其他函数,其中 f 是一个可调用对象,包括函数指针、函数对象 //这种转发会丢掉 顶层的const 和引用 template <typename F, typename T1, typename T2> void call(F f,T1 t1,T2 t2) { //调用前的处理 f(t1,t2); //调用后的处理 } void f(int v1, int &v2) //注意v2是一个引用 { v2 += v1; } ``` 再定义下面两个变量: ```c++ int i = 66; int j = 600; ``` 当我们直接调用 `f`时: ```c++ f(i,j); cout<<i<<","<<j<<endl; ``` >[test] >66,666 当我们通过 `call` 间接调用 `f`时: ```c++ call(f,i,j); cout<<i<<","<<j<<endl; ``` >[test] >66,600 很显然,`call` 和直接调用 `f` 的结果是不一样的。原因在于这两种方式调用 `f` 时,`v1` 引用的是 `i`;而后者在调用 `call` 时, `i` 会被复制一份到 `t1`, `v1` 引用的是 `t1` 。 #### 例2 为了解决上面的问题,我们调整 `call` 函数: ```c++ //用于调用其他函数,其中 f 是一个可调用对象,包括函数指针、函数对象 //传入左值时,t1,t2为左值引用;传入右值时,t1,t2为右值引用。具体请参见 引用 template <typename F, typename T1, typename T2> void call(F f,T1 &&t1,T2 &&t2) { //调用前的处理 f(t1,t2); //调用后的处理 } void f(int v1, int &v2) //注意v2是一个左值引用 { v2 += v1; } void f2(int &&v1, int &v2) //注意v2是一个左值引用,v1是一个右值引用 { v2 += v1; } ``` 这样,通过 `call` 调用 `f` 和直接调用 `f` 就没有什么差别了。 但是,`call` 不能调用 `f2` 。因为具名的右值引用是一个左值,而左值是不能作为右值引用的参数的。 ### 3.std::forward 实现完美转发 + **头文件** `utility` + **功能** 保持给定实参的引用属性以及顶层的 `const` 属性。 + **格式** `std::forward<参数的类型>(参数名)` #### 例3 ```c++ template <typename F, typename T1, typename T2> void call(F f, T1 t1, T2 t2) { //调用前的处理 f(forward<T1>(t1), forward<T2>(t2));//这里已经使用了 std 命名空间 //调用后的处理 } ```