函数

文章目录
  1. 1. 函数基础
    1. 1.1. 局部对象
    2. 1.2. 函数声明
  2. 2. 参数传递
    1. 2.1. 传值参数
    2. 2.2. 传引用参数
    3. 2.3. const形参和实参
    4. 2.4. 数组形参
    5. 2.5. main:处理命令行选项
    6. 2.6. 含有可变形参的函数
      1. 2.6.1. initializer_list
    7. 2.7. 省略符形参
  3. 3. 返回类型和return语句
    1. 3.1. 无返回值函数
    2. 3.2. 有返回值的函数
      1. 3.2.1. 值是如何被返回的
      2. 3.2.2. 不要返回局部对象的指针或者引用
      3. 3.2.3. 返回类类型的函数和调用运算符
      4. 3.2.4. 引用返回左值
      5. 3.2.5. 列表初始化返回值
      6. 3.2.6. 主函数main的返回值
    3. 3.3. 返回数组指针

主要内容:

  1. 函数基础
  2. 参数传递
  3. 函数类型和return语句
  4. 函数重载
  5. 特殊用途语言特性
  6. 函数匹配
  7. 函数指针

函数是一个命名了的代码块,我们通过调用函数执行响应的代码。

函数基础

函数的组成部分:

  1. 返回类型
  2. 函数名字
  3. 形参列表
  4. 函数体

我们通过调用运算符来执行函数,调用运算符的形式是一对圆括号。

函数调用完成的工作:

  1. 用实参初始化函数对应的形参
  2. 将控制权转移给被调函数,此时主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行

return语句完成的工作:

  1. 返回return语句中的值
  2. 将控制权从被调函数转移回主调函数。

函数的形参:即使两个形参的类型一样,也必须把两个类型都写出来。任何两个形参都不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。

函数返回类型:函数的返回类型不能是数组类型、函数类型,但是可以是指向数组或者函数的指针。为啥?

局部对象

名字有作用域,对象有生命周期。

  1. 名字的作用域是程序文本的一部分,名字在其中可见
  2. 对象的生命周期是程序执行过程中该对象存在的一段时间

形参和函数体内部定义的变量统称为局部变量。局部变量会隐藏在外层作用域中同名的其他所有声明。

在所有函数体之外定义的对象,存在于程序的整个过程中,此类对象在程序启动的时候被创建,直到程序结束才会销毁。

自动对象:当函数的控制路径经过变量定义语句时,创建该对象,当达到定义语句所在的快末尾时销毁,我们把只存在于快执行期间的对象称为自动对象。

形参是自动变量,函数开始执行时为形参申请空间,因为形参定义在函数作用域内,所以一旦函数终止,形参就被销毁。

局部静态对象:生命周期贯穿函数调用及之后的时间,在程序执行路径第一次经过对象的定义语句时初始化对象。并且直到程序终止才被销毁。

如果局部静态变量没有显示的初始化,执行值初始化,内置类型的局部静态变量初始化为0.

函数声明

函数的声明不包括函数体,所以他无需形参名字。

参数传递

  1. 引用传递:引用形参是他对象实参的别名
  2. 值传递:值拷贝,形参和实参是两个相互独立的对象

传值参数

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

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

传引用参数

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

优点吧:

  1. 使用引用避免拷贝:拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型根本不支持拷贝操作,当某种类型不支持拷贝操作时,函数只能通过引用参数访问该类型对象。
  2. 使用引用形参返回额外信息。

const形参和实参

当形参有顶层const时,传给它常量或非常量对象都可以。

我们可以使用非常量初始化一个底层const对象。C++允许字面值初始化常量引用。

常量引用接收的实参范围大,可以接收非常量、常量、字面值、需要类型转换的对象。

数组形参

数组的特点:

  1. 内置类型。
  2. 不允许拷贝,所以不能以值传递方式使用数组。
  3. 使用数组时,大部分情况会转换为指向数组首元素的指针。

管理指针形参,标记数组的大小:

  1. 使用标记指定数组的大小,就是数组本身包含一个结束标记,典型的例子是C风格的字符串
  2. 使用标准库,需要传入两个指针,一个指向数组首元素,另一个指向尾元素的下一个位置。
  3. 显示传递一个表示数组大小的形参

数组引用形参:引用绑定到数组实参上。纬度也是类型的一部分,但是这样限制只能传递数组大小为10 的实参,函数模板可以实现传递任何大小的数组。

传递多维数组: 数组真正传递的是首元素的地址,因为,多维数组时数组的数组,所以首元素本身是一个数组,就是指向数组的指针,可以想象,数组的第二纬的大小是数组类型的一部分。

1
int  (*matrix)[10] ; //第二个纬度是类型的一部分。

main:处理命令行选项

argv中的可选实参从argv[1]开始,argv[0]保存程序的名字,而非用户的输入。

含有可变形参的函数

可变参数的的实现方式:

  1. 传递initializer——list标准库类型,所有实参类型相同
  2. 可变参数模板,参数类型不同
  3. 特殊的形参类型——省略号,一般用于和C交互的程序

initializer_list

实参数量未知,类型相同。

initializer_list提供的操作 说明
initializer_list<T> lst; 默认初始化,T类型元素的空列表
initializer_list<T> lst{a,b,c...} list2 = list lst的元素数量和初始值一样多,lst元素是对应初始值的副本,列表中的元素是const
lst2(lst) 拷贝或者赋值一个initializer_list对象,不会拷贝列表中的元素,拷贝后,原始列表和副本共享元素
list.size() 列表中元素数量
list.begin() list中首元素指针
list.end() list中尾元素下一个位置

如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一个或括号中。

省略符形参

省略符形参是为了便于C++程序访问某些特殊的C代码设置的。这些代码使用名为varargs的C标准库。大多数类类型对象在传递给省略符形参时,都无法正确拷贝。省略符形参只能出现在形参列表的最后一个位置。省略符形参对应的实参无需进行类型检查。省略符前面的逗号可以省略。

返回类型和return语句

无返回值函数

返回void的函数不要求非得有return语句,因为在这类函数的最后一句后面会隐式的执行return。。

强行令void函数返回其他类型的表达式将产生编译错误。

有返回值的函数

return语句的返回值类型必须与函数的返回类型形同,或者可以隐式转换。

值是如何被返回的

返回一个值,和初始化一个变量或者形参的方式完全一样。

不要返回局部对象的指针或者引用

因为局部对象释放了,引用或者指针无效

返回类类型的函数和调用运算符

为啥点运算符和箭头运算符、调用运算符优先级形同呢?如果调用一个函数,返回一个对象,我么可以直接通过点运算符访问对象的属性。他们是左结合律

引用返回左值

我们能为返回类型是非常量引用的函数的结果赋值。就是把函数调用放在赋值语句的左侧。

列表初始化返回值

主函数main的返回值

main可以不写返回值,编译器帮插入

返回数组指针

因为数组不能拷贝,所以函数不能返回数组,不过函数可以返回数组的指针或者引用。

1
2
typedef int arrT[10];
using arrT = int[10]; //arrT是是一个类型别名,表示类型是10个整数的数组。

基本形式]、示例:

``
type (*function(parameter_list))[demension]

int (*func(int i))[10];

1
2
3
4
5
6
7
8
9

理解的方式:

1. func(int i) 表示条用func函数时需要一个int类型的实参
2. (* func(int i)) 意味着我们可以对函数调用的结果执行解引用操作
3. (* func(int i))[10] 表示解引用func的调用将得到一个大小是10的数组
4. int (*func(int i))[10] 表示数组中的元素类型是int类型

使用尾置返回类型: C++新标准中可以还有一种简化上述func声明的方法,就是使用位置返回类型。任何函数定义都能使用位置返回,位置返回类型跟在新参列表后面,并以->开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto

auto func(int i) -> int(*)[10];

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
使用decltype: 如果我们知道函数返回的指针指向哪个数组,就可以使用decltype关键字声明返回类型。decltype并不负责把数组类型转换为对应的指针。所以decltype的结果是数组。

## 函数重载

对于重载的函数来说,他们应该在形参数量或形参类型上有所不同。返回类型不能当做重载的判断依据。

重载和const:顶层const不构成重载。底层const构成重载

const_cast和重载:const_cast在重载函数的场景最有用。

### 重载与作用域

如果在内存作用域中声明名字,他将隐藏外层作用域中声明的同名实体。在不同的作用域无法重载函数名。

在C++中,名字查找发生在类型检查之前

## 特殊用途语言特殊

### 默认实参

某些函数有这样一种形参,在函数的很多次调用中,他们都被赋予一个相同的值。此时,我们吧这个反复出现的值称为函数的默认实参。调用含有默认实参的函数时,可以包含该实参,也可以省略改实参。

默认实参作为形参的初始值出现在形参列表中,我们可以为一个或多个形参定义默认值,不过注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

函数调用时,实参按位置解析,默认实参负责填补函数调用缺少的尾部实参。


当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面。而让那些经常使用默认值的形参出现在后面。


#### 默认实参声明

对于函数的声明,通常的习惯是将其放在头文件中,并且一个函数只声明一次,但是多次声明同一个函数也是合法的。不过有一点注意:在给定的作用域中,一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参。而且该形参的右侧的所有形参必须有默认值。

通常,应该在函数声明中指定默认实参,并将声明放在合适的头文件中。

默认实参的初始值:用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程烦死在函数调用时。


### 内联函数 和constexpr函数

内联函数可以避免函数调用的开销: 通常在调用点内联的展开。在函数的返回类型前面加上关键字inline,可以将函数声明为内联的。内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。

一般来说:内联机制用于优化规模较小、流程直接、频繁调用的函数。

constexpr函数:是指 能用于常量表达式的函数。需要满足两个条件

1. 函数的返回类型和所有形参类型都是字面值类型
2. 函数体内必须有且只有一条return语句。

为了能在编译过程中随时展开,constexpr函数被隐式的指定为内联函数。

constexpr函数不一定返回常量表达式。由编译器确定是不是常量。

### 调试帮组

#### assert预处理宏

assert(expr)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

首先对expr求值,如果表达式的值为假(0),assert输出信息并终止程序执行,如果表达式为真,assert什么页不做。

预处理名字由预处理器而非编译器管理,所以使用assert无需使用using声明。宏名字在程序内必须唯一。

#### NDEBUG 预处理变量

assert的行为依赖于一个名为NDEUBG的预处理变量的状态。如果定义了NDEBUG,则assert什么页不做。

除了C++编译器定义的__func__之外,预处理器还定义了另外4个对于程序调试很有用的名字。

1. `__FILE__` 存放文件名的字符串字面值
2. `__LINE__ `存放当前行号的整形字面值
3. `__TIME__` 存放文件编译时间的字符串字面值
4. `__DATW__ `存放文件编译日志的字符串字面值。

## 函数匹配

### 确定候选函数、可行函数

函数匹配的第一步是选定本次调用对象的重载函数集,集合中的函数称为候选函数(candidate function)。候选函数具备两个特征:

1. 于被调用函数同名
2. 其声明在调用点可见

第二步:考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数(viable function)。可行函数的特征

1. 其形参数量与本次调用提供的实参数量相等
2. 每个实参的类型与对象的形参类型相同或者能转化为形参类型。

第三步: 寻找最佳匹配

最佳的条件:

1. 该函数每个实参的匹配都不劣于其他可行函数的匹配
2. 至少有一个实参的匹配优于其他可行函数提供的匹配。

### 实参类型转换

编译器将实参类型到形参类型的转化划分为几个等级:具体的排序如下:

1. 精确匹配: 类型完全相同、实参从数组类型或函数类型转换成对应的指针类型、向实参类型添加顶层const或者从实参中删除顶层const
2. 通过const转化实现的匹配
3. 通过类型提升实现的匹配
4. 通过算术类型转换实现的匹配
5. 通过类类型转换实现的匹配


### 函数匹配和const实参

如果两个函数的唯一区别是他的指针形参指向常量或者非常量。则编译器通过实参是否常量来决定需用那个函数。如果实参是指向常量的指针,调用形参是const*的函数。否则,调用普通版本函数。

## 函数指针

函数指针指向的是函数,而非对象,和其他指针一样,函数指针指向某种特定的类型。函数的类型是由它的返回类型和形参类型共同决定的。于函数名无关。要想声明一个可以指向函数的指针,只需要用指针代替函数名即可:

bool (*pf)(const string&, const string &);

1
2

当我们把函数名当一个值使用时,函数自动的转化为指针。我们还可以直接使用函数指针调用改函数,无需提前解引用指针。不同函数类型的指针间不存在类型转换

pf = lengthCompara;
pf = & lengthCompara; //等价的赋值语句

1
2
3
4
5
6
7


重载函数的指针:指针类型必须与重载函数中的某一个精确匹配。

函数指针形参:虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。形参如果是函数类型,会自动转化为函数指针类型。decltype作用于函数,只返回函数类型,如果需要函数指针,需要单加*

返回指向函数的指针:和数组类型,不能返回一个函数,但能返回指向函数类型的指针,我们必须把返回类型写成指针形式,编译器不会自动将函数类型转换为指针类型

typedef bool Func(const string&, const string &); //函数类型
using Func = bool(const string&, const string &) //函数类型

typedef bool (Func1)(const string&, const string &); //函数指针类型
using Func1 = bool(
)(const string&, const string &) //函数指针类型

1
2


int (f1(int))( int , int)
```

  1. f1 有形参列表,所以f1是个函数,f1前面有个* 所以f1 返回一个指针
  2. 指针的类型本身也包含了形参列表。因此,指针直线函数
  3. 改函数的返回类型是int