## 一.参数
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 命名空间
//调用后的处理
}
```
- 阅读说明
- 1.1 概述
- C++基础
- 1.2 变量与常量
- 1.2.1 变量
- 1.2.2 字面值常量
- 字符型常量
- 数值型常量
- 1.2.3 cv限定符
- 1.3 作用域
- 1.3.1 标识符
- 1.3.2 *命名空间
- 1.3.3 作用域
- 1.3.4 可见性
- 1.4 数据类型
- 1.4.1 概述
- 1.4.2 处理类型
- 类型别名
- * auto说明符
- * decltype说明符
- 1.4.3 数组
- 1.4.4 指针
- 1.4.5 引用
- 1.5 表达式
- 1.5.1 概述
- 1.5.2 值的类别
- 1.5.3 *初始化
- 1.5.4 运算符
- 算术运算符
- 逻辑和关系运算符
- 赋值运算符
- 递增递减运算符
- 成员访问运算符
- 位运算符
- 其他运算符
- 1.5.5 *常量表达式
- 1.5.6 类型转换
- 第2章 面向过程编程
- 2.1 流程语句
- 2.1.1 条件语句
- 2.1.2 循环语句
- 2.1.3 跳转语句
- 2.1.4 *异常处理
- 2.2 函数
- 2.2.1 概述
- 2.2.2 函数参数
- 2.2.3 内置函数
- 2.2.4 函数重载
- 2.2.5 * 匿名函数
- 2.3 存储和生命期
- 2.3.1 生命周期与存储区域
- 2.3.2 动态内存
- 2.4 *预处理命令
- 第3章 面向对象编程
- 3.1 概述
- 3.2 类和对象
- 3.3 成员
- 3.3.1 访问限制
- 3.3.2 常成员
- 3.3.3 静态成员
- 3.3.4 成员指针
- 3.3.5 this指针
- 3.4 特殊的成员函数
- 3.4.1 概述
- 3.4.2 构造函数
- 3.4.3 析构函数
- 3.4.4 拷贝语义
- 3.4.5 * 移动语义
- 3.5 友元
- 3.6 运算符重载与类型转换
- 3.6.1 概述
- 3.6.2 重载方法
- 3.6.3 类型转换
- 3.7 继承与多态性
- 3.7.1 概述
- 3.7.2 派生类
- 3.7.3 子类型
- 3.7.4 虚基类
- 3.7.5 虚函数
- 3.7.6 抽象类
- 3.8 模板与泛型
- 3.8.1 概述
- 3.8.2 模板类型
- 3.8.3 *模板参数
- 3.8.4 *模板编译
- 3.8.5 *模板推断
- 3.8.6 *实例化与特例化
- 第4章 C++标准库
- 4.1 概述
- 4.2 输入输出流
- 4.2.1 概述
- 4.2.2 *流的状态
- 4.2.3 *常用流
- 4.2.4 *格式化I/O
- 4.2.5 *低级I/O
- 4.2.6 *随机访问
- 4.3 *C输入输出
- 4.3.1 *字符输入输出
- 4.3.2 *格式化输入输出
- 4.4 * 容器
- 4.4.1 * 概述
- 4.4.2 * 基本操作
- 4.4.3 * 顺序容器
- 4.4.4 * 迭代器
- 4.4.5 * 容器适配器
- 4.5 * 泛型算法
- 4.6 * 内存管理
- 4.6.1 * 自动指针
- 4.7 * 其他设施