Fork me on GitHub

函数基础

函数基础

一个典型的函数定义包括以下部分:返回类型、函数名、由0个或多个形参组成的列表以及函数体

函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。此时,主调函数的执行被暂时中断,被调函数开始执行。

形参和实参

函数有几个形参,我们就必须提供相同数量的实参。

局部对象

形参和函数体内部定义的变量(含{}块域)统称为局部变量。他们对函数而言是“局部”的,仅在函数的作用域内可见,同时局部变量还会隐藏在外层作用域中同名的其他所有声明中。

自动对象

对于普通局部变量对应的对象来说, 当函数的控制路径经过变量定义语句时创建该对象, 当到达定义所在的块末尾时销毁它. 我们把只存在于块执行期间的对象称为自动对象, 当块的执行结束后, 块中创建的自动对象的值就变成未定义的了.

形参是一种自动对象, 函数开始时候为形参申请存储空间, 因为形参定义在函数体作用域之内, 所以函数终止, 形参被销毁.

对于局部变量对应的自动对象来说, 分两种情况:
如果变量定义本身含初始值, 就用这个初始值初始化;否则, 如果变量定义本身不含初始值, 执行默认初始化.意味着内置类型的未初始化局部变量将产生未定义的值.

局部静态对象

当有些时候, 有必要令局部变量的生命周期贯穿函数调用及之后的时间,可以将局部变量定义为static 类型, 局部静态变量在程序执行路径第一次经过对象定义语句时候初始化,并直到程序终止才被销毁。

1
2
3
4
5
6
7
8
9
10
11
12
size_t count_calls()
{
static size_t ctr=0;
eturn ++ctr;
}
int main()
{
for(size_t i=0; i!=10; i++)
cout<< count_calls() << endl;
return 0;
}

在控制流第一次经过ctr的定义之前, ctr被创建且初始化为0; 每次调用ctr加1. 每次执行函数, 变量ctr的值已经存在并等于函数上一次退出的时候的值。

函数声明

函数只能定义一次,但可以声明多次。(有例外),如果一个函数永远不可能给我们用到,那他可以只有声明没有定义。

函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需要的全部信息,函数声明也称作函数原型

best practices: 在头文件中进行函数声明,含有函数声明的头文件应该包含到定义函数的源文件中。

分离式编译

分离式编译,C++允许我们将程序分割到几个文件中去,每个文件独立编译。

简单的示例:

目录树:

source 
      facc.cpp
      main.cpp
header
      myHead.h
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
//facc.cpp
#include <iostream>
#include "MyHead.h" //定义fac函数,肯定要包含其声明
using namespace std;
int fac(int n){
if (n==1){
return 1;
}else {
return n*fac(n-1);
}
}
//main.cpp
#include <iostream>
#include "MyHead.h" //使用fac,肯定也要包含其声明
using namespace std;
int main()
{
cout<<fac(5)<<endl;
return 0;
}
//myHead.h //头文件中只有函数的声明
#ifndef MYHEAD_H_INCLUDED
#define MYHEAD_H_INCLUDED
int fac(int n);
#endif // MYHEAD_H_INCLUDED

const形参和实参

实参初始化形参会忽略掉顶层const,也就是当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。

我们可以使用非常量初始化底层const,但是反过来不行。

数组形参

数组的两个特殊点:

  • 不允许拷贝数组,所以不可以使用传值的方式使用数组参数(传值即为拷贝)。
  • 通常数组的传递使用的是指针形式,传递的是指针的首地址。

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

1
2
3
4
5
//尽管形式不同,但这三个print函数是等价的
//每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]);
void print(const int[10]) //这里的维度表示我们期望数组含有多少元素,实际不一定

当编译器处理对print函数的调用时,只检查传入的参数是否是const int*类型:

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

如果我们传给print函数的是一个数组,则实参自动地转换成指向首元素的指针,数组的大小对函数的调用没有影响。

由于数组实际上是以指针的形式传递给函数的,因此一开始函数并不知道数组的确切尺寸,调用者应该为此提供额外的一些信息。

管理指针形参有三种常用的技术:

1.使用标记指定数组长度

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

1
2
3
4
5
6
7
8
void print(const char *cp)
{
if(cp){ //若cp不是空指针
while(*cp){ //只要指针所指字符不是空字符
cout<<*cp++; //输出当前字符并将指针前移
}
}
}

这个方法适用于那些有明显结束标记且该标记不会与普通数据混淆的情况。

2.显示传递一个表示数组大小的形参

这种方法是专门定义一个表示数组大小的形参,在C程序和过去的C++程序中常常使用这种方法。

1
2
3
4
5
6
7
8
//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]<<" ";
}
}

这种方法通过形参size的值确定要输出多少个元素,调用print函数时必须传入这个表示数组大小的值:

1
2
int j[] = {1,2,3};
print(j,3);

3.使用标准库函数begin和end

C++11标准引入两个名为begin和end的函数,begin函数返回指向数组首元素的指针,end函数返回指向数组尾元素下一位置的指针,这两个函数定义在iterator头文件中。
示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
using namespace std;
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,-1,5,3,4};
int *pbeg = begin(arr); //指向arr首元素的指针
int *pend = end(arr); //指向arr尾元素的指针
//寻找第一个负值元素,如果已经检查完全部元素则结束循环
while(pbeg != pend && *pbeg >= 0){
++pbeg;
}
cout<<*pbeg<<endl; //输出第一个负数的值
}

对于本文的print函数,可以写成如下形式:

1
2
3
4
5
6
void print(const int *beg, const int *end)
{
//输出beg到end之间(不含end)的所有元素
while(beg != end)
cout<<*beg++<<" ";
}

为了调用这两个函数,我们需要传入两个指针:一个指向要输出的首元素,另一个指向尾元素的下一个位置。使用方法如下:

1
2
int a[] = {1,2,3};
print(begin(j),end(j));

数组形参和const

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

数组引用形参

形参也可以是数组的引用,此时,引用形参绑定到对应的实参上,也就是绑定到数组上:

1
2
3
4
5
void print(int (&arr)[10]) {
for(auto elem : arr){
cout<<elem<<endl;
}
}

注意:

1
2
f(int &arr[10])//错误,将arr声明为引用的数组
f(int (&arr)[10])//正确, arr是有10个整形的数组引用

传递多维数组

和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是指向数组的指针,数组的第二维(以及后面所有维度)的大小都是数组类型的一部分, 不能省略:

1
2
3
4
//matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[10], int rowsize){...}
//等价于
void print(int matrix[][10], int rowsize){...}

我们也可以使用数组的语法定义函数,此时编译器会一如既往地忽略掉第一个维度,所以最好不要把它包括在形参列表内。

main: 处理命令行选项

C/C++语言中的main函数,经常带有参数argc,argv,如下:

1
2
int main(int argc, char** argv)
int main(int argc, char* argv[])

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

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

下面的程序演示argc和argv的使用:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;
int main(int argc,char *argv[])
{
int i;
for (i=0; i < argc; i++) {
cout<<"Argument "<<i<<" is "<<argv[i]<<endl;
}
return 0;
}

假如上述代码编译为hello.exe,那么运行
hello.exe a b c d e

将得到

Argument 0 is hello.exe.
Argument 1 is a.
Argument 2 is b.
Argument 3 is c.
Argument 4 is d.
Argument 5 is e.

含有可变形参的参数(先知道有这么个东西,后面详细了解)

返回函数指针

函数重载(重点,待完善)

对于函数重载来说,形参数量或形参类型上有所不同。

不允许两个函数除了返回类型外,其他所有的要素都相同。

特殊用途语言特性

默认实参

知识点1:函数反复调用的过程中重复出现的形参,这样的值被称为默认实参。该参数在使用过程中可以被用户指定,也可以使用默认数值

知识点2:调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。

知识点3:一旦某个形参被赋予了默认值,其后所有形参都必须有默认值。

知识点4:顺序很重要!在设计函数时,将默认值的形参放在后面。

知识点5:在给定的作用域中,一个形参只能被赋予一次默认实参,且局部变量不能作为默认实参。

内联函数(inline)

调用函数函数一般比求等价表达式的值要慢一些。在大多数机器上,一次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复:可能需要拷贝实参;程序转向一个新的位置继续执行。

内联函数可以避免函数调用的开销。

将函数指定为“内联函数(inline)”,将它在每个调用点上“内联的展开”,该说明只是向编译器发出一个请求,编译器可以选择忽略这个请求。一般来说,内联的机制用于优化规模较小、流程直接、频繁调用的函数,建议不大于75行。

constexpr函数

constexpr函数是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有参数的类型都得是字面值类型,而且函数体中必须有且只有一条return语句。

1
2
constexpr int new_sz(){ return 42;}
constexpr int foo = new_sz(); //正确:foo是一个常量表达式

constexpr函数不一定返回常量表达式

程序的调试帮助:assert和NDEBUG

知识点1:预处理宏assert(expr):包含一个表达式,expr为真时,assert什么也不做,为假时输出信息并终止程序。包含在cassert头文件中。通常用于检查不能发生的条件

知识点2:assert依赖于一个NDEBUG的预处理变量的状态,如果定义了NDEBUG,assert什么也不做,默认状态下NDEBUG是未定义的。编译器也可以预先定义该变量。

知识点3:也可以使用NDEBUG编写自己的条件调试代码,如果NDEBUG未定义,将执行#ifndef到#endif之间的代码,如果定义了NDEBUG,这些代码将被忽略。

1
2
3
4
5
6
7
void pp()
{
#ifndef NDEBUG
cerr<<"my name is:"<<__func__<<endl;
#endif
//其他代码
}

一些C++编译器定义的调试有用的名字:

_ func _ :一个静态数组,存放函数的名字

_ FILE _ :存放文件名的字符串字面值

_ LINE _ :存放当前行号的整形字面值

_ TIME _ :存放文件编译时间的字符串字面值

_ DATE _ :存放文件编译日期的字符串字面值

函数匹配

  1. 首先确定候选函数:候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见。
  2. 接着选出可行函数:可行函数具备两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。

如果函数含有默认实参,则我们在调用该函数时传入的实参数量可能少于它实际使用的实参数量。

如果没有找到可行函数,编译器将报告无匹配函数的错误。

3.寻找最佳匹配:它的基本思想是,实参类型与形参类型越接近,他们匹配得越好

编译器一次检查每个实参以确定哪个函数是最佳匹配。如果有且只有一个函数满足下列条件,则匹配成功:

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

如果在检查了所有实参之后没有任何一个函数脱颖而出,则该调用是错误的。编译器将报告二义性调用的信息。

函数指针

函数指针指向的是函数而非对象,和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。例如:

1
bool lengthCompare(const string &,const string &);

该函数的类型是bool(const string&,const string&)。要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可

1
2
//pf指向一个函数,该函数的参数是两个const string 的引用,返回值是bool类型
bool (*pf) const string& ,const string &); //未初始化

从我们声明的名字开始观察,pf前面有个*,因此pf是指针;右侧是形参列表,表示pf指向的是函数;在观察左侧,发现函数的返回类型是布尔值。因此,pf就是一个指向函数的指针,其中该函数的参数是两个const string的引用,返回值是bool类型。

注意: *pf两端的括号必不可少。

知识点2:当我们把函数名当作一个值使用时,函数自动的转换为指针,直接赋予或者取址皆可。可以直接使用只想该函数的指针调用该函数。

知识点3:给指针赋予nullptr或者0时,指针不指向任何函数。

知识点4:函数重载时,指针的类型必须与重载函数精确匹配,包括形参类型数量和返回值类型。

知识点5:虽然不能返回一个函数,但是可以返回一个指向函数的指针。

返回指向函数的指针

1
int (*f1(int)) (int *,int)

我们看到f1有形参列表,所以f1是个函数;f1前面有*,所以f1返回一个指针;进一步观察发现,指针的类型本身也包含形参列表,因此指针指向函数,该函数的返回类型是int.

出于完整性的考虑,有必要提醒读者我们还可以使用尾置返回类型的方式声明一个返回函数指针的函数。

1
auto f1(int) -> int (*)(int *,int);

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include<string>
#include<vector>
using namespace std;
int add(int a, int b)
{
return a+b;
}
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) { return b != 0 ? a / b : 0; }//声明定义函数
int main(int argc, char** argv)
{
typedef int(*p)(int a, int b); //声明函数指针,未初始化,p为指向函数的指针。使用typedef的声明语句定义的不再是变量而是类型别名
//就是将变量转化为类型别名的一种方式,p原来是指向函数的指针变量,现在变成了指向函数的指针变量的类型别名
vector<p> vec{add, subtract, multiply, divide};//vector初始化的C++11新特性
for (auto f : vec)
cout << f(2, 2) <<endl;
return 0;
}
-------------本文结束感谢您的阅读-------------

本文地址:http://www.wangxinri.cn/2017/10/19/函数基础/
转载请注明出处,谢谢!

梦想夹带眼泪,咸咸的汗水!