一、函数基础
1.1形参和实参
实参是形参的初始值。形参的类型必须与对应的形参类型匹配。函数的形参列表可以为空,但是不能省略,可使用void表示函数没有形参:
void f1() { /*...*/ } // 隐式地定义空形参列表
void f1(void) { /*...*/ } // 显式地定义空形参列表
形参名是可选的,但由于无法使用未命名的形参,所以形参一般都应该有个名字。
1.2函数返回类型
void是一种特殊的返回类型,它表示函数不返回任何值。函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
二、局部对象
在C++中,名字有作用域,对象有生命周期。
· 名字的作用域是程序文本的一部分,名字在其中可见。
· 对象的生命周期是程序执行过程中该对象存在的一段时间。
函数体是一个语句块。块构成一个新的作用域,可以在其中定义变量。形参和函数体内部定义的变量统称为局部变量。
2.1自动对象
把只存在于块执行期间的对象称为自动对象。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。
2.2局部静态对象
当需要令局部变量的生命周期贯穿函数调用以及之后的时间。可以将局部变量定义为static类型从而获得这样的对象。局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁。
size_t count_calls()
{
static size_t ctr = 0; // 调用结束后,这个值仍然有效
return ++ctr;
}
int main()
{
for(size_t i=0; i!=10; ++i)
cout<<count_calls()<<endl;
return 0;
}
这段程序将输出从1到10数字。
三、函数声明
void print(int i, float j);
函数三要素:返回类型、函数名、形参类型描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型。
分离式编译:把函数声明放在.h文件里,把函数定义放在.cc文件里。
四、参数传递
每次调用函数都会重新创建它的形参,并用传入的实参进行初始化;
引用传递(传引用调用):可能会在函数中改变传入实参的值;
值传递(传值调用):对传入的实参不会改变;
使用引用避免拷贝:有时候拷贝所占的内存较大,因此使用引用可避免这一问题。
4.1const形参和实参
当形参是const时,必须要注意顶层const的讨论。
尽量使用常量引用
double calc(double a);
int count(const string&a, char b);
int sum(vector<int>::iterator beg, vector<int>::iterator end);
vector<int>vec(10);
calc(23.4, 55); // 错误:传入实参过多
count("abd", "o"); // 第一个实参顶层const错误
calc(66); // 正确
sum(vec.begin(), vec.end(), 3.8); //正确
4.2数组形参
数组的两个性质:不允许拷贝数组以及使用数组时会将其转换为指针。所以当我们在为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
void print(const int*);
void print(const int[]); // 可以看出,函数的意图是作用于一个数组
int i = 0;, j[2] = {0,1};
print(&i); // 正确:&i的类型是int*
print(j); // 正确:j转换成int*并指向j[0]
数组形参和const
当函数不需要对数组元素执行写操作的时候,数组的形参应该是指向const的指针。只有当函数确实要改变元素值的时候才把形参定义成指向非常量的指针。
4.3含有可变形参的函数
如无法知道应该向函数传递几个实参。可以使用两种主要的方法:
1.如果所有实参类型相同,可以使用initializer_list的标准库:
initializer_list<T>lst; // 默认初始化;T类型元素的空列表
initializer_list<T>lst{a,b,c,d...};lst的元素和初始化一样多;lst的元素是对应初始值的副本;列表中的元素是const
void print(initializer_list<int> i1);
print({1,2,3,4}; // 正确
print({1,2}); // 正确
2.省略符形参是为了C++程序访问某些特殊C代码而设置的。
void foo(parm_list, ...);
void foo(...);
4.4函数占位参数
占位参数:返回值类型 函数名(数据类型){}
占位参数 还可以由默认实参
void func(int a, int)
{
cout<<"this is func"<<endl;
}
五、返回类型和return语句
return语句终止当前正在执行的函数,并将控制权返回到调用该函数的地方。
return;
return expression;
不要返回局部对象的引用或指针
函数完成后,它所占用的存储空间也随之被释放掉,因此函数终止意味着局部变量的引用将指向不再有效的内存区域。
5.1引用返回左值
函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。
5.2返回数组指针
因为数组不能拷贝,所以函数不能直接返回数组。不过函数可以返回数组的指针或引用。
typedef int arrT[10]; // arrT是一个类型别名
using arrT = int[10]; // arrT的等价声明
arrT* runc(int i); // func返回一个指向含有10个整数的数组指针
使用尾置返回类型
// func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i)->int(*)[10];
六、函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数。
6.1重载和const形参
顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
此外,若形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象实现函数重载,此时const是底层的:
int look(int&); // 函数作用于int的引用
int look(const int&); // 新函数,作用于常量引用
int look(int*); // 新函数,作用于指向int的指针
int look(const int*); // 新函数,作用于指向常量的指针
6.2const_cast和重载
const_cast在重载函数的情景中最有用:
const string &shorterString(const string&s1, const string &s2)
{
return s1.size()<= s2.size()? s1:s2;
} // 这个函数的参数和返回类型都是const string 的引用,但我们需要得到一个普通的引用。
string &shorterString(const string&s1, const string &s2)
{
auto& r= (s1.size()<= s2.size()? s1:s2);
return const_cast<string&>(r);
}
6.3调用重载的函数
函数匹配是指一个过程:在这个过程中吧函数调用与一组重载函数中的某一个关联起来,函数匹配也叫重载确定。当调用重载函数时有三种可能的结果:
(1)编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码。
(2)找不到任何一个函数与调用的实参匹配,此时编译器发出误匹配的错误。
(3)有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用。
七、特殊用途语言特性
7.1默认实参
当一个函数很多次调用形参都被赋予一个相同的值,此时可以把这个反复出现的值称为函数的默认实参:
string screen(size_t i=24, size_t j=36, char k=' ');
当使用默认实参时,切记传入函数的参数也需按照顺序来,不然会出现不匹配或少匹配错误:
void screen(int i=24, int j=25, string k)
{
cout<<1;
}
screen("s"); // 错误,传入的第一个实参为string类型,与第一个形参int类型不匹配
7.2内联函数和constexpr函数
7.2.1内联函数(inline)
把规模较小的操作定义成函数有很多好处:
(1)阅读和理解函数的调用比读懂等价什么的条件表达式容易的多;
(2)使用函数可以确保行为的统一,每次相关操作都能按照同样的方法进行;
(3)如果我们需要修改修改计算内容,显然修改函数要更加简洁;
(4)函数可以被其他应用重复利用,省去了程序员重新编写的代价。
但多次调用这种函数存在一个缺点:调用函数一般比求等价表达式的值要慢一些。一次调用函数调用前要先保存寄存器,并在返回时恢复,也可能需要拷贝实参等等。所以,内联函数可避免函数调用的开销:
const string &shorterString(const string&s1, const string &s2)
{
return s1.size()<= s2.size()? s1:s2;
}
将函数声明为inline const string & shorterString(const string &s1, const string &s2);
相当于在调用函数处进行展开,展开为s1.size()<= s2.size()? s1:s2。
7.2.2constexpr函数
constexpr函数是指能用于常量表达式的函数,与其他函数类似的定义方法,但需遵循以下约定:
(1)函数的返回类型及所有形参的类型都得是字面值类型;
(2)函数体中必须有且只有一条return语句。
??还不知道constexpr函数有什么用??
通常把内联函数和constexpr函数放在头文件内:这两个函数可以在程序中多次定义。不过对于某个内联或constexpr函数来说,它的多个定义必须完全一致,基于这个原因,内联函数和constexpr函数通常需要定位在头文件里。
7.3调试帮助
7.3.1assert预处理宏
是一个预处理变量,行为有点类似内联函数,assert宏使用一个表达式作为它的条件:
assert(expr);
assert定义在casser头文件里;assert宏常用于检查”不能发生“的条件,例如:
assert(word.size()>threshold);
7.3.2NDEBUG预处理变量
assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。
定义NDEBUG能避免检查各种条件所需的运行时的开销。
除了assert外,也可以使用NDEBUG编写自己的调试代码。如果NDEBUG未定义,将执行#ifndef和#endif之间的代码;如果定义了NDEBUG,这些代码将被忽略掉:
void print(const int ia[], size_t size)
{
#ifndef NDEBUG
cerr<<___func__<<"..."<<size<<endl;
#endif
}
__func__ 存放函数的名字
__FILE__ 存放当前文件名的字符串字面值
__LINE__ 存放当前行号的整型字面值
__TIME__ 存放文件编译时间的字符串字面值
__DATE__ 存放文件编译日期的字符串字面值
八、函数匹配
候选函数:具有相同函数名的函数为一个集合称为候选函数;
可行函数:从候选函数中选出能被这组实参调用的函数。
8.1实参类型转换
为了确定最佳匹配,编译器将实参类型到形参类型的转换划分几个等级:
(1)精确匹配:实参和形参类型相同或实参从数组类型/函数类型转换到对应的指针类型;
(2)通过const转换实现的匹配;
(3)通过类型提升实现的匹配;
(4)通过算术类型转换或指针实现的匹配;
(5)通过类类型转换实现的匹配。
九、函数指针
函数指针指向的是函数而非对象。
bool lengthcompare(const string&, const string&);
bool (*pf)(const string&, const string&); // 指向上面函数的指针,未初始化。
9.1使用函数指针
当把一个函数名作为一个值使用时,该函数自动地转换成指针:
pf = lengthcompare; // pf指向名为lengthcompare的函数
pf = &lengthcompare; // 等价的赋值语句:&是可选项
此外,我们还能直接使用指向函数的指针调用该函数,无须提前解引用指针:
bool b1 = pf("h", "g"); // 调用lengthcompare函数
bool b2 = (*pf)("h", "g"); // 一个等价的调用
bool b3 = lengthcompare("h", "g"); // 另一个等价的调用
9.2返回指向函数的指针
和数组类似,可以返回指向函数的指针:
using F = int(int*, int); // F是函数类型
using FF = int(*)(int*,int); FF是指针类型
FF f1(int); // 正确:返回指向函数的指针
F f1(int); // 错误:F是函数类型,不能返回
F *f1(int); // 正确:返回指向函数的指针
9.3将auto和decltype用于函数指针类型
auto f1(int) -> int(*)(int*, int);
如果知道返回的函数类型是哪一个,就能使用decltype简化:
string size_type sumlength(const string&, const string&);
decltype(sumlength) *getfcn(const string &); // 声明getfcn唯一需要注意的地方是,牢记将decltype作用于某个函数时,它返回的函数类型而非指针类型。因此我们要显示加上*以表明我们需要返回指针。