6.1 函数基础
形参和实参
- 实参是形参的初始值.尽管实参和形参存在对应关系,但是并没有规定实参的求值顺序(参见4.1.3,123页).编译器能以任意可行的顺序对实参求值
6.1.1 局部对象
- 形参和函数体体内定义的变量统称为局部变量
- 自动对象:只存在于块执行期间的对象.形参是一种自动对象
局部静态对象
- 可以将局部对象定义成static类型从而获得这样的对象
- 局部静态对象:在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才能被销毁,在此期间即使对象所在的函数结束执行,也不会对他有影响
6.1.2 函数声明
- 函数的名字必须在使用之前声明.函数只能定义一次,但可以声明多次
- 函数的声明不包含函数体,所以也就无需形参的名字
在头文件中进行函数声明
- 函数应该在头文件中声明在源文件中定义
6.1.3 分离式编译
6.2参数传递
- 每次调用函数时都会重新创建它的形参,并用传入的实参对其进行初始化
6.2.1 传值参数
指针形参
- 指针的行为和其他非引用类型一样.当执行指针拷贝操作时,拷贝的是指针的值.拷贝之后,两个指针时不同的值.
6.2.2 传引用参数
使用引用避免拷贝
如果函数无需改变引用形参的值,最好将其声明为常量引用
使用引用形参返回额外信息
6.2.3 const形参和实参
- 当应实参初始化形参时会忽略掉顶层的const.即形参的顶层const会被过滤掉.当形参有顶层const时,传给它常量或者非常量对象都是可以的.
//上面的话也就是解释了下面的程序为什么不会构成重载.
void f(const int x);
void f(int )x;
指针或者引用形参与const
尽量使用常量引用
- 把函数不会改变的形参定义成普通引用是一种比较常见的错误,这么做会给函数的调用者一种误导,即函数可以修改他的实参的值,此外,使用普通引用也会极大的限制函数所能接受的实参的类型.如果是普通引用int&只能结果int型的实参不能接受const int型的实参.
find_char(const string &s);
//改为
find_char(string &s);
find_char("wwwww"); // 会报错,error
6.2.4 数组形参
-
数组的两个性质影响我们定义和使用作用在数组上的函数有影响
- 不允许拷贝数组.所以我们无法以值传递的方式使用数组参数
- 使用数组时(通常)将其转换成指针.所有当我们在函数中传递一个数组时,实际上在传递的是数组首元素的指针,数组的大小对函数的调用没有影响. -
虽然不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式.
// 尽管形式不同,但这个print函数时等价的
// 每个函数都有一个const iht*的形参
void print(const int *);
void print(const int []);
void print(const int [10]);
//每个参数的形参都是const int *
传递数组大小的三个方法
使用标记指定数组长度
- 要求数组本身包含一个结束标记,典型例子是C风格的字符串.C风格字符串存储在字符数组中,并且最后一个字符后面跟着一个空字符.
void print(const int * cp)
{
if(cp){
while(*cp){
}
}
}
使用标准库规范
- 传递指向数组首元素和尾元素的指针.
void print(const int * begin,const int * end)
{
while(begin != end){
}
}
}
int main()
{
int j[2];
print(begin(j),end(j))
}
显示传递一个表示数组大小的形参
void print(const int * begin,size_type size)
数组形参和const
数组引用形参
- C++语言允许数组的引用
void print(int (&arr)[10]);
//这样无形中也限制了数组的大小
int &arr[10]; //error,将arr声明为引用的数组
int (&arr)[100] //将qrr声明为数组的引用,即arr是具有10个整数的整型数组的引用.
- 16.1.1节将会介绍如何呈递任意大小的数组
传递多维数组
- 在C++语言中没有真正的多维数组,所谓多维数组其实是数组的数组.当将多维数组传递给函数时,真正传递的是指向数组首元素的指针.因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针.数组的第二维的大小都是数组的一部分
void print(int (*matrix)[10]); //使用数组的语法进行定义
//声明一个指向含有10个整数构成的数组.编译器会一如既往的忽略掉第一个维度,所以最好不要把他包括在形参列表中
上述等价于
void print(int matrix[][10]);
//matrix的声明看起来是一个二维数组,实际上形参是指向含有10个整数的数组的指针
习题
6.3 返回类型和return语句
6.3.1 无返回值函数
6.3.2 有返回值函数
- return语句返回值的类型必须与函数的返回类型相同,或者能隐式的转换成函数的返回类型.
值是如何被返回的
- 返回一个值的方式和初始化一个变量或者形参的形式完全一样:返回的值用于初始化调用点的一个临时变量,该临时变量就是函数调用的结果.
- 例如返回类型为string,意味着返回值将被拷贝到调用点.因此将返回一个变量的副本或者一个未命名的临时string对象.
- 如果是返回类型是引用,则该引用仅是他所引对象的一个别名.不管是调用函数还是返回结果都不会真正拷贝引用所指的对象.
不要返回局部对象的引用和指针
- 函数完成后,它所指向的存储空间也随之被释放掉.因此函数终止意味着局部变量的引用将指向不再有效的内存区域.
string test1()
{
string str = "123";
return str;
}
string test02()
{
return "123";
}
上述两个函数都是返回局部对象,都是错误的.
- 返回局部对象的引用是错误的.返回局部的对象的指针也是错误的.
返回类类型的函数和调用运算符
- 如果函数返回指针,引用或者类的对象,我们就能使用函数调用的结果访问结果对象的成员.
引用返回左值
- 函数的返回类型决定函数调用是否是左值,调用一个返回引用的函数得到左值,其他返回类型得到右值.我们可以为返回类型是非常量引用的函数的结果赋值.
列表初始化返回值
- 函数可以返回花括号包围的值的列表
vector<string> process()
{
if(....){
return {}; // 返回一个空vector对象
}else if(){
return {"1","2"};
}else{
return {"1"};
}
}
- 如果函数返回的是内置类型,则花括号包围的列表最多包含一个值.如果函数返回的是类类型,有类本身定义初始值如何使用.
主函数main的返回值
- 如果函数的返回类型不是void,则必须返回一个值.但是这条规则有关例外.我们允许main函数没有return语句直接结束.但是编译器会自己添加.
6.3.3 返回数组指针
- 因为数组不能被拷贝,所以函数不能返回数组.但是可以返回数组的指针或者引用.
typedef int arrT[10];
arrT* process(int a[][10]) //返回数组指针
{
return a;
}
int (*process1(int a[][10]))[10] //返回数组指针,--太过繁琐
{
return a;
}
int (*process1(int a[][10]))[10];
process先于()结合表明其是一个函数,然后去掉其与括号,剩下的是返回类型:int(*)[10]
数组指针
声明一个返回数组指针的函数
- 返回数组指针的函数形式如下:
使用尾置返回函数类型
- C++11为了简化返回类型为数组指针或者数组引用的函数的书写,提供
尾置返回类型
,任何函数都可以使用尾置返回类型.极大的简化返回类型复杂的函数.为了表示函数真正的返回类型更在形参列表之后,我们在本应该出现返回类型的地方放置一个auto
auto func(int i) -> int(*)[10];
// func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
// auto test01(int i) ->string (&)[10] 返回的是数组的引用
// auto test01(int i) ->string &[10] 返回的是引用数组,error,不存在引用数组
// auto test01(int i) ->string *[10] 返回的是指针数组
// auto test01(int i) ->string (*)[10] 返回的是数组指针
使用decltype
习题
int &arr[10]; //error,将arr声明为引用的数组
int (&arr)[100] //将qrr声明为数组的引用,即arr是具有10个整数的整型数组的引用.
# 使用尾置返回类型
auto test01(int i) -> string (&)[10];
# 使用原始声明
string (&test02(int i))[10];
#使用using进行简化
using refer = string[10];
refer& test03(int i);
# 使用decltype进行
string str[10];
decltype(str) &func();
6.4 函数重载
main函数不能重载
重载与const形参
- 一个拥有顶层const的形参无法与另外一个没有顶层const的形参区分开来
void loo(Phone);
void loo(const Phone);
void loo(Phone*);
void loo( Phone * const);
- 如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的.
void loo(Account &)
void loo(const Account &) //作用于常量引用
void loo(Account *)
void loo(const Account *) //作用于指向常量的指针
- 因为const不能转化为其他类型,所以我们只能把const对象传递给const形参.非const两个都可以但是编译器会优化调用非常量版本.
const_cast和重载
const string & shorterString(const string &s1,const string s2)
{
return s1.size() <= s2.size() ? s1:s2;
}
上面的函数对于常量调用好着,但是两个非常量对象调用却返回常量调用
const string & shorterString(const string &s1,const string s2)
{
return s1.size() <= s2.size() ? s1:s2;
}
// 上面的函数对于常量调用好着,但是两个非常量对象调用却返回常量调用
string & shorterString( string &s1, string s2)
{
auto &r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string&>(r);
}
调用重载的函数
- 当调用重载函数时有三种可能的结果:
- 最佳匹配
- 无匹配
- 二义性调用
int calc(int,int);
int calc (const int,const int);
// 非法,顶层const不影响传入函数的对象,所以一个拥有顶层const的形参无法与一个没有顶层const的形参区分开来.
6.4.1 重载与作用域
- 在不同作用域中的函数无法构成重载,因为
在C++语言中,名字查找发生在类型检查之前.
6.5 特殊用途语言特性
6.5.1 默认实参
- 一旦某个形参被赋予了默认值,它后面的所有形参都必须被赋予了默认值.
使用默认实参调用函数
默认实参声明
int test(int a,int b=1);
/*
int test(int a,int b =2) //error
{
}
*/
int test(int a=1,int b)
{
return 1;
}
int main() {
std::cout << "Hello, World!" << std::endl;
test(1);
test();
return 0;
}
- 对于函数的声明来说,通常的习惯是将其放在头文件中,并且一个函数只声明一次,但是多次声明也是合法的.(即使在头文件中声明在源文件中实现也按照这样规程进行)
- 在给定的作用域中一个形参只能被赋予一次默认实参,函数的后续声明中只能为之前那些没有默认值的形参添加默认实参.
6.5.2 内联函数和constexptr函数
内联函数可避免函数调用的开销
- 将函数指定为内联函数,通常就是将它在每个调用点内联的展开.
- 可以消除调用函数的运行时开销.
内联说明只是想编译器发出一个请求,编译器可以忽略这个请求
constexpr函数
- 指能用于常量表达式的函数.
- 但是有以下约定
- 函数的返回值类型必须是字面值
- 所有形参的类型必须是字面值类型
- 函数中必须有且只有一个return语句
所谓字面值类型:就是可以用来声明constant的类型,这中类型的特点是:简单,值显而易见.
//error:string 不是字面值类型
constexpr int test01(string s)
{
}
- 指向该初始化任务时,编译器把对constexpr函数的调用替换成其结果值.为了能在编译过程中随时展开,constexpr函数被隐式的指定为内联函数.
- constexpr函数体内页可以包含其他语句,只要这些语句在运行时不执行任何操作就行.例如:空语句,类型别名,using声明等.
- 我们允许constexpr函数的返回值并非一个常量:
constexpr int new_sz() {return 42;}
constexpr size_t scale(size_t cnt)
{
return new_sz() * cnt;
}
int main() {
std::cout << "Hello, World!" << std::endl;
constexpr int foo = new_sz(); // foo是一个常量表达式
int a = 1;
// constexpr int f1 = scale(a); // a不是常量表达式,所以scale(a)不是
constexpr int f2 = scale(1); //1 是常量表达式,所以scale(1)也是
constexpr int f3 = scale(foo); // foo是常量表达式,所以scale(foo)也是
return 0;
}
constexpr函数不一定返回常量表达式
把内联函数和constexpr函数放在头文件中
- 内联函数和constexpr函数可以在程序中多次定义,但是它的多个定义必须完全一致,基于这个原因,内联函数和constexpr函数通常定义在头文件中.
6.5.3 调试帮助
assert预处理宏
assert(exp)
:首先先对exp求值,如果表达式为假,assert输出信息并终止.
NDEBUG预处理变量
6.6 函数匹配
6.6.1 实参类型转换
需要类型提升和算术类型转换的匹配
- 小整数一般都会提升到int类型或者更大的整数类型.假设有两个函数,一个接受int,一个接受short,则只有当调用提供的是short类型的值时才会选择short版本的函数.有时候实参是很小的值也会直接提升成int.
#include <iostream>
using namespace std;
void t(int in) {
cout << "int____" << endl;
}
void t(short sh) {
cout << "char___" << endl;
}
int main() {
// t(3.14); //error
t(1); //int____
t('a'); //int____
short sh = 1;
t(sh); //char___
std::cout << "Hello, World!" << std::endl;
return 0;
}
- 所有算术类型转换的级别都是一样的.例如上面的t(3.14).再入:从int–>unsigned int的转换并不比int–>double的转换级别高
#include <iostream>
using namespace std;
void t(long lon) {
cout << "long____" << endl;
}
void t(float fl) {
cout << "float___" << endl;
}
int main() {
t(3.14); //error ambiguous
t(1); //error ambiguous
short sh = 1;
t(sh); //error ambiguous
std::cout << "Hello, World!" << std::endl;
return 0;
}
函数匹配和const实参
习题
#include <iostream>
using namespace std;
void t(int & i,int & j) {
cout << "int &,int &" << endl;
}
void t(const int & ci,const int & cj) {
cout << "const int &,const int &" << endl;
}
int main() {
t(1,2);
int i,j;
t(i,j);
/*
* 两个函数的区别是是他们的引用类型的形参是否引用了常量,属于底层const.可以将两个函数区分开来
*/
return 0;
}
#include <iostream>
using namespace std;
void t(char * i,char * j) {
cout << "char *" << endl;
}
void t(const char * ci,const char * cj) {
cout << "const char *" << endl;
}
int main() {
t("=aaa","sdsdd");
char *a = "aaa";
t(a,a);
/*
* 两个函数的区别是他们的指针类型的形参是否指向了常量
* ,属于底层const,可以将两个函数区别开来
*/
return 0;
}
6.7 函数指针
- 函数的类型由它的返回类型和形参共同决定.
bool lengthCompare(const string &,const string &);
- 上面函数的类型是:bool (const string &,const string &);
- 要想声明一个可以指向该函数的指针,只需用指针替换函数名即可:
`bool (pf)(const string &,const string &);`
使用函数指针
- 当我们把函数名作为一个值使用时,该函数自动的转换成指针.
pf = lengthCompare;
pf = &lengyhCompare; //& 取地址符是可选的.
- 我还能直接使用指向函数的指针调用该函数,无须提前解引用.
- 在指向不同函数类型的指针之间不存在转换规则.
重载函数的指针
- 编译器通过指针类型决定选用那个函数,指针类型必须与重载函数中的某一个精确匹配
函数指针形参
- 和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针.此时,形参看起来是函数类型,实际上却是当成指针使用.
void use(bool (*pf)(const string &,const string &));// 参数是函数指针类型
void use(bool (pf)(const string &,const string &));//参数是函数类型
- 可以使用decltype和typedef来简化
bool lengthCompare(const string &);
typedef bool Func1(const string &);
typedef decltype(lengthCompare) Func2; //decltype(lengthCompare)返回函数类型,此时不会将函数类型自动转换成指针类型
using Func3 = bool (const string &);
// Func1和Func2,Func3都是执行函数类型
//PFunc1和PFunc2,PFunc3都是指向函数指针的类型
typedef bool (* PFunc1)(const string &);
typedef decltype(lengthCompare) *PFunc2;
using PFunc3 = bool (*)(const string &);
void use(Func1);// 参数是函数指针类型
void use(*Func1);//参数是函数类型
void use(Func1);// 参数是函数指针类型
void use(FPunc1);//参数是函数类型