文章目录
C++primer学习心得 -第六章 -函数
6.1 函数基础
函数包括:返回类型、函数名字、形参组成的列表和函数体。
我们通过调用运算符来执行函数。
函数的调用完成两项工作:一是用实参初始化函数对应的形参,而是将控制权转移给调用函数。此时主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行。
执行函数的定义步是 (隐式)定义并初始化它的形参。当遇到一条return语句时函数结束执行过程。return完成两项工作:返回return语句中的值,将控制权从被调函数转移到主调函数。
形参和实参
实参是形参的初始值。实参的类型必须与对应的形参的类型匹配。
函数的形参列表可以为空,但不能省略。为了与c语言兼容也可以用void来表示函数没有形参。
形参列表中的形参通常用逗号隔开,每个形参都含有一个声明符的声明。
void f1(){};
void f2(void){};
int f3(int v1,int v2);
大多数类型都可作为函数的返回类型,有一种特殊的返回类型void,表示函数不返回任何值。函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
1.局部对象
有两个重要的概念:
- 名字有作用域,名字的作用域是程序文本的一部分,名字在其中可见
- 对象有生命周期,对象的生命周期是程序执行过程中该对象存在的一段时间
函数体是一个语句块,块构成一个新的作用域,形参和函数体内定义的变量统称为局部变量,仅在函数的作用域内可见,同时局部变量会隐藏在外层作用域中的同名的其他声明。
对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。我们把只存在于执行期间的对象称为自动对象。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。
形参时一种自动对象。函数开始时为形参申请存储空间,一旦函数终止,形参也被销毁。
体会一下下面程序
#include<iostream>
using namespace std;
void add_ten(int a) {
a += 10;
}
void add_tens(int* a) {
*a += 10;
}
void add_tenss(int &a) {
a += 10;
}
int main() {
int a = 100;
add_ten(a);
cout << a << endl;//a=100
add_tens(&a);
cout << a << endl;//a=110
add_tenss(a);
cout << a << endl;//a=120
return 0;
}
要想改变变量a的值我们需要传给函数的时a的地址而不是a本身。这里需要用指针。或者将形参定义为引用类型。
局部静态对象
有时我们希望局部变量的生命周期能贯穿函数调用及之后的时间,这时我们可以将局部变量定义成static类型。局部静态对象在程序的首次执行时被定义并初始化,但函数执行结束不会对它造成影响,
#include<iostream>
using namespace std;
size_t count() {
static size_t ctr = 0;
++ctr;
return ctr;
}
int main() {
for (size_t i = 0; i != 10; i++)
cout << count() << endl;
return 0;
}
2.函数声明
函数的声明和函数的定义唯一的区别时函数声明不用函数体。函数声明也称函数原型。
3.分离式编译
可以阅读编译器的用户收册,弄清楚由多个文件组成的程序时如何编译并执行的。
6.2参数传递
形参的类型决定了形参和实参交互的方式:如果形参时引用类型,他将绑定到对应的实参上,否则,它将实参的值拷贝后赋值给形参。形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。当实参被拷贝给形参时形参和实参时两个相互独立的对象,我们说这样的实参被值传递或者函数被传值调用了。
使用引用形参可以让函数有多个返回值。
const 形参和实参
实参初始化形参时会忽略掉顶层const。若形参有顶层const,相对应的实参可以是常量也可以是非常量。
形参的初始化与变量的初始化基本是相通的,所以我们可以按照变量初始化规定的那一套来理解形参的初始化。
数组形参
在我们定义和使用作用在数组上的函数时要注意数据的两个特殊的性质:不允许拷贝数组和使用数组时会将其转化为指针。
数组引用形参:形参也可以时数组的引用。
含有可变形参的函数
若函数体的实参数量位置但全部实参的类型相同,我们可以使用initializer_list类型的形参。initializer_list是一种标准库类型(模板类型),用于表示某种特定类型的值的数组。它定义在同名的头文件中。
它提供以下操作:
- initializer_list<T> lst; 默认初始化
- initializer_list<T> lst{a,b,c,…}; lst的元素数量和初始值一样多;lst的元素是对应的初始值的副本,列表中的元素是const
- lst2(lst) 拷贝或赋值一个initailizer_list对象不会拷贝列表中的元素;拷贝后原始列表和副本共享元素
- lst2=lst
- lst.size() 列表中的元素数量
- lst.begin() 返回指向lst的首元素的指针
- lst.end() 返回指向lst中尾元素的指针
#include<iostream>
using namespace std;
int add_list(initializer_list<int> lst) {
int sum = 0;
for (const auto& l : lst) {
sum += l;
}
return sum;
}
int main() {
int all = add_list({1,2,3,23,23,1232,343,23,4323});
cout << all << endl;
return 0;
}
6.3 返回类型和return语句
当void函数需要提前退出时可以用"return;"。
有返回值的函数必须用return返回一个值。
返回一个值的方式和初始化一个变量的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
注意不要返回局部变量的引用或指针。
注意,调用一个返回引用的函数得到左值,其他类型返回右值。我们可以为返回类型时非常量引用的函数的结果赋值。
我们允许main函数没有return语句直接结束,此时编译器会隐式地插入一条返回0的return语句。
递归
如果函数调用了它自身,称该函数为递归函数。
#include<iostream>
using namespace std;
int f(int v) {
if (v > 1)
return f(v - 1) * v;
return 1;
}
int main() {
cout << f(10) << endl;
return 0;
}
返回数组指针
返回一个数组指针的函数声明如下
Type (*function(parameter_list))[dimension]
比如
int (*func(int i))[10]
- func(int i)表示调用函数需要一个int类的实参
- (*func(int i))表示我们可以对函数调用的结果解引用
- (*func(int i))[10] 表示解引用会得到一个大小为10的数组
- int (*func(int i))[10] int类型
使用尾置返回类型
尾置返回类型跟在形参后面以->开头且在本应出现分会类型的地方放置一个auto
auto func(int i)->int(*)[10];
另外我们也可以使用decltype类型。
6.4函数重载
若同一作用域中几个函数函数名相同但形参列表不同,我们称之为重载(overloaded)函数。
6.5 特殊用途语言特性
1. 默认实参
调用含有默认实参的函数时,可以包含该实参也可以省略该实参。
注意一个形参只能被赋予一次默认实参。局部变量不能作为默认实参。
2.内联(inline)函数和constexpr函数
内联函数可避免函数调用上的开销。将函数指定为内联函数,通常时将它在每个调用节点上“内联地”展开。
很多编译器不支持内联递归调用。
constexpr函数
指能用于常量表达式的函数。注意:函数的返回类型和形参类型都时字面值类型,且函数体中只能有一条return语句。
内联函数和constexpr函数通常定义在头文件中。
3.调试帮助
程序可以包含一些用于调试的代码,这些代码只在开发时使用。当程序编写完成准备发布时要先屏蔽掉调试代码。这时要用到两项预处理功能:assert和NDEBUG。
assert预处理宏
预处理宏是一个预处理变量,行为类似于内联函数。assert宏使用一个表达式来作为他的条件。
assert(expr);
先对expr求值,表达式为假,assert输出信息并终止程序的执行。表达式为真,assert什么也不做。
NDEBUG预处理变量
assert的行为依赖于名为NDEBUG的预处理变量的状态。若定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert执行运行时检查。
可以使用#define语句来定义NDEBUG来关闭调试状态。
6.7函数指针
函数指针指向的是函数而非对象。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
声明一个可以指向某个函数的指针只需要用指针替换函数名,如
bool lengthCompare(const string &,const string &);
bool (*pf)(const string &,const string &);//未初始化
注意*pf的括号不能省。
当我们把函数名作为一个值使用时函数自动转换为指针。
pf=lengthCompare;
pf=&lengthCompare;//取地址符号有也行
我们也能直接使用指向函数的指针调用该函数,不需要提前解引用指针
bool b1=pf("bello","goodbye");
bool b2=(*pf)("hello","goodbye");
bool b3=lengthCompare("hello","goodbye");
我们也可以为指针赋值nullptr或0,表示空指针。
形参也可以是指向函数类型的指针。为了简化函数指针的使用可以使用typedef和decltype。
typedef bool func(const string&,const string&);//函数类型
typedef decltype(lengthCompare) func2;
typedef bool (*funcp)(const string&,const string&);//指针
typedef decltype(lengthCompare)*funcp2;
//使用类型别名
void useBigger(const string&,const string &,func);
void useBigger (const string&,const string&,funcp2);
返回指向函数的指针
使用类型别名来声明一个返回函数指针的函数
using F=int (int*,int);//函数类型
using FP=int(*)(int*,int);//指针
PF f1(int);//正确
F f1(int);//错误,F是函数类型
F *f1(int);//正确,显示指定返回类型是指向函数的指针
//直接声明
int(*f1(int))(int*,int);//阅读顺序由内向外
//尾置返回类型
auto f1(int)->int(*)(int*,int)
另外如果我们明确知道返回的函数时哪一个可以使用decltype简化书写过程。
#include<iostream>
#include<vector>
using namespace std;
int funa(int i1, int i2) {
return i1 + i2;
}
int funb(int i1, int i2) { return i1 - i2; }
int func(int i1, int i2) { return i1 * i2; }
int fund(int i1, int i2) { return i1 / i2; }
int main() {
vector<decltype(funa)*> fp;
fp.push_back(funa);
fp.push_back(funb);
fp.push_back(func);
fp.push_back(fund);
for (auto f : fp)
cout << f(555, 111) << endl;
return 0;
}
利用指向函数的指针实现本来需要if-else语句实现的判断语句,注意到大多数if-else语句都可以用下面这种形式来实现,但实际操作起来比if-else要繁琐不少,所以通常我们不会想去做这种绕远路的事情的,但需要明白确实有这样的方式可以实现判断语句
#include<iostream>
#include<vector>
using namespace std;
void f1() {
cout << "high pass" << endl;
}
void f2() {
cout << "pass" << endl;
}
void f3() {
cout << "failed" << endl;
}
int main() {
int grade;
vector<void(*)()>fp;
fp.push_back(f2);
fp.push_back(f3);
fp.push_back(f1);
while (cin >> grade) {
bool b1 = grade > 90;
bool b2 = grade < 60;
int i1 = b1, i2 = b2, i3 = i1 * 2 + i2;
fp[i3]();
}
return 0;
}