问题

  1. 函数执行的第一步是什么。
  2. 知识点:在函数调用时会发生控制权的转移:主调函数被终端,被调函数开始执行。
  3. 知识点:函数的返回类型不可以是数组或函数,但是可以是数组指针或函数指针。
  4. 局部静态变量的生命周期
  5. 函数的三要素
  6. 函数使用引用形参时的注意事项
  7. 处理可变形参(即可以传递不同数量实参的形参)的方法。
  8. 理解 initializer_list
  9. 函数返回引用和非引用的区别。
  10. 知识点:可以在 return 语句中直接构造返回的对象,比如在 return 语句中调用某个类的构造函数临时构造对象以返回,或直接使用列表初始化

回答

  1. 隐式地定义并初始化它的实参。
  2. 知识点:在函数调用时会发生控制权的转移:主调函数被终端,被调函数开始执行。
  3. 函数的返回类型不可以时数组或函数,但是可以是数组指针或函数指针。
  4. 从程序执行到静态变量定义开始,到程序执行结束。
  5. 返回类型、函数名、形参类型
  6. 尽量使用常量引用形参,const 对象、字面值、需要类型转换的对象都只能传递给常量引用形参,不能传递给普通引用形参。
  7. 可以使用 initailizer_list 类型或省略符形参(基本不用)。
  8. initializer_list 是一个模板,只能使用花括号初始化,本质上 vector 等容器的列表初始化就是采用了 initializer_list 类型作为构造函数的形参。
  9. 返回引用的函数返回的是左值,返回非引用的函数返回的是右值。可以为返回类型是非常量引用的函数的结果赋值。注意返回引用时不能返回局部变量的引用。
  10. 知识点:可以在 return 语句中直接构造返回的对象,比如在 return 语句中调用某个类的构造函数临时构造对象以返回,或直接使用列表初始化。

问题

  1. main 的返回值是什么
  2. assert 预处理宏的用法
  3. 预处理器定义了 5 个对程序调试很有用的名字。
  4. 如何定义函数指针?如何定义类型别名来使用函数指针
  5. 如何使用默认实参

回答

  1. 如果程序执行成功,main 返回 0,返回其他值表示执行失败。
  2. 用于调试 assert(expr)。
  3. func, FILE, LINE, TIME, DATA
  4. int (pf) (const int &n); 定义类型别名:using PF = int()(const int &n); PF 和 pf 类型相同。
  5. 通常在函数声明时指定默认实参。有默认值的形参应该在没有默认值的形参之后。可以用全局变量和字面值作为默认实参,不能用局部变量。(区分函数的默认实参和类的类内初始值)

第6章 函数

6.1 函数基础

通过调用运算符 () 来执行函数。

函数的调用会完成****两项工作

  1. 用实参初始化函数对应的形参。
  2. 将控制器转移给被调用函数。

调用函数后,主调函数被中断,被调函数开始执行。

函数执行的第一步:隐式地定义并初始化它的形参。实参是形参的初始值,第一个实参初始化第一个形参。

return 语句完成****两项工作:

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

形参

可以没有形参名,但是函数无法使用未命名的形参,即使形参未命名,也要传入实参。

返回类型

返回类型不能是数组或函数类型,但是可以是数组指针函数指针

6.1.1 局部对象

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

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

形参和函数体内定义的变量统称局部变量。

函数开始时为形参申请存储空间,函数终止形参被销毁

在所有函数外定义的对象存在于程序的整个执行过程中

局部静态变量

如果要让局部变量的声明周期不局限于函数内,可以将局部变量定义成 static 类型。

局部静态变量的生命周期:在程序的执行路径第一次经过对象定义语句时初始化,直到程序终止被销毁。

如果局部静态变量没有显式的初始值,将执行值初始化。

注意:局部静态变量是 static 型而不是 const 型

6.1.2 函数声明

函数的名字必须在使用前声明。

函数可以只声明无定义。

函数的声明和定义的区别在于声明不需要函数体,用一个分号代替。

函数的声明无需形参的名字(因为声明不包含函数体)。

函数的三要素:返回类型、函数名、形参类型。三要素描述了函数的接口。

建议在头文件中声明函数,在源文件中定义函数。

6.1.3 分离式编译

分离式编译允许把程序分割到几个文件中,每个文件独立编译。

6.2 参数传递

形参初始化的机理和变量初始化一样。

6.2.1 传值参数

c++ 中建议使用引用形参代替指针形参

6.2.2 传引用参数

引用形参在传递实参时直接传入对象。

如果函数不需要改变引用形参的值,最好声明为常量引用。

6.2.3 const形参和实参

用实参初始化形参时会忽略掉顶层 const,即形参类型为 const int 和 const 是一样的。理解:引用是没有顶层 const 的,因此顶层 const 适用于指针及其他类型,对于传值来说传递的是实参的副本,无论如何都不会改变实参,因此形参加不加顶层 const 都是一样的。

尽量使用常量引用做形参

注意常量引用的 const 是底层 const。

可以用字面值初始化常量引用

不能把 const 对象、字面值或需要类型转换的对象传递给普通的引用形参。但是可以传递给常量引用形参。

注意如果函数 a 把形参定义为了常量引用,函数 b 形参是普通引用,那么不能在 a 中使用 b 调用该常量引用形参。

6.2.4 数组形参

数组的两个特殊性质:不允许拷贝数组、使用数组时常会将其转换成指针

1
2
3
void print(const int*);
void print(const int[]);
void print(const int[10]);//三种声明等价,数字 10 没有什么实际影响

编译器只检查传入的实参是否为 const int* 类型。

使用数组做形参确保数组访问不越界的方法:

  1. 使用一个结束标记指定数组已结束,典型代表为 C 风格字符串
  2. 传递指向数组首元素和尾后元素的指针
  3. 专门定义一个表示数组大小的形参。

数组引用形参

可以定义数组引用形参。注意数组的大小是构成数组类型的一部分

1
2
3
4
5
func(int &arr[10]);//错误:arr 是引用的数组
func(int (&arr)[10]);//正确:array 是包含 10 个整数的整型数组的引用

int *arr[10];//指针的数组
int (*arr)[10];//指向数组的指针

6.2.5 main:处理命令行选项

可以给 main 函数传递实参,不过情况很少。

6.2.6 含有可变形参的函数

处理不同数量实参的主要方法有两种:

  1. 如果所有实参类型相同,传递一个 initializer_list 类型
  2. 使用省略符形参,它可以传递可变数量的实参,注意它一般仅用于与 C 函数交互的接口程序

initializer_list 形参

initializer_list 也是一种模板类型,定义在同名的头文件中。

initializer_list 与 vector 容器大致相同,但是它的元素都是常量值。

initializer_list 对象只能使用花括号初始化。

C++ 里的 vector 等各类容器使用列表初始化时本质上都是通过一个采用了 initializer_list 形参的构造函数进行初始化的。

1
2
3
4
5
initializer_list<int> initlst;//默认初始化:空列表
initializer_list<int> initlst{1,2,3,4};//initlast 的元素数量与初始值一样多
lst2(initlst); lst2 = initlst;//拷贝或赋值一个 initializer_list 对象不会复制元素,而是拷贝后两者共享元素。
initlst.size();
initlst.begin(); initlst.end();//注意返回的是指针

如果向 initailizer_list 形参中传递一个值的序列,必须把序列放在花括号里。

1
2
void func(initializer_list<int> il)
func({3,4,5,2});

省略符形参

省略符形参仅用于 C 和 C++ 通用的类型,大多数类类型的对象传递给省略符形参都无法正确拷贝。

省略符形参只能出现于形参列表的最后一个位置

1
2
void func(parm_list,...);
void func(...);

6.3 返回类型和return语句

return的两个作用:

  1. 返回 return 语句中的值
  2. 终止当前正在执行的函数,将控制权返回到调用该函数的地方

6.3.1 无返回值函数

没有返回值的 return 只能用于无返回值函数

返回 void 的函数可以没有 return 语句,因为它会在最后一句后面隐式地执行 return。

6.3.2 有返回值函数

在含有 return 语句的循环和条件后面也应该有一条 return 语句。

返回一个值和初始化一个变量或形参的方式一样,返回的值用于初始化调用点的变量。

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

1
2
3
4
const string& func()
{
return "LiMing"
}//错误,字符串字面值转换成一个局部临时变量,不能返回局部对象的引用

引用返回左值

返回引用的函数返回的是左值,其他返回类型得到右值。

可以为返回类型是非常量引用的函数的结果赋值。

1
get_val(s,0) = 'A';

列表初始化返回值

1
2
3
4
5
6
7
8
vector<string> process()
{
string s;
if(condition1)
return {};// 返回一个空 vector 对象
else if(condition2)
return {"funcitonX",s};//返回一个列表初始化的 vector 对象
}

main 的返回值

允许 main 函数没有 return 语句直接结束,编译器隐式地插入一条 return 0;

main 函数地返回值相当于一种状态指示器,返回 0 表示执行成功,返回其他值表示执行失败。

cstdlib 头文件定义了两个预处理变量来表示成功与失败

1
2
return EXIT_FAILURE;//失败
return EXIT_SUCCESS;//成功

6.3.3 返回数组指针

函数可以返回数组的指针或引用

声明一个返回数组指针的函数

1
2
3
4
5
6
7
8
int *pf[10]; // 错误,指针的数组
int (*pf)[10]; // 正确,数组的指针

int (*func())[10];// 定义返回数组指针的函数。

auto func() -> int(*)[10];// 同上,此处使用了尾置返回类型
using arrT = int[10]; arrT* func();// 使用而类型别名
decltype(arr)* func();// 使用 decltype

三种方法简化返回数组指针的函数的声明。

使用类型别名

可以使用类型别名简化数组指针的使用

1
2
3
4
typedef int arrT[10]; // arrT 表示含有 10 个整数的数组
using arrT = int[10];

arrT* func(int i); // 函数 func 返回一个指向含有 10 个整数的数组的指针

使用尾置返回类型

任何函数的定义都可以使用尾置返回,但是它更适用于返回类型复杂的函数

1
2
auto func() -> int;//返回 int 类型
auto func() -> int(*)[10];//返回一个指向 int 数组的指针

使用 decltype

如果已知函数返回的指针将指向哪个数组,可以使用 decltype

1
2
int odd[]={1,3,5,7,9};
decltype(odd)* func();//返回一个数组指针

注意 decltype 的结果是一个数组,要返回数组指针要加星号。

6.4 函数重载

main 函数不能重载

函数重载无法区分顶层 const 形参和非顶层 const 形参,但是可以区分底层 const 形参与普通形参

1
2
3
4
5
6
7
8
9
10
11
int func(int i);
int func(const int i);//顶层const,无法区分,相当于重复声明

int func(int* p);
int func(int* const p);//顶层const,无法区分,相当于重复声明

int func(int* p);
int func(const int* p);//底层const,可以区分,一个普通指针,一个常量指针

int func(int& i);
int func(const int& i);//底层const,可以区分,一个普通引用,一个常量引用

最好只重载确实非常相似的操作

const_cast 在重载中的应用

强制类型转换 const_cast 在重载函数中最有用。一个函数可能同时要有接受常量引用的版本也要有接受非常量引用的版本。

当要重载常量引用与非常量引用的版本时,在非常量引用的版本中可以通过 const_cast 将参数和返回值从常量引用转换为非常量引用,以实现对常量引用版本的调用。

1
2
3
string& s;
const_cast <const string&> (s);// 将 s 转换为常量引用
const_cast <string&> (s);// 将 s 转换回非常量引用

6.4.1 重载与作用域

不同的重载版本要定义在同一作用域中(一般都是全局)

6.5 特殊用途语言特性

6.5.1 默认实参

设置默认值的形参必须都放在没有默认值的形参后面。

使用默认实参的时候省略该实参即可。默认实参负责填补函数调用时缺少的尾部实参。

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

局部变量不能作为默认实参,全局变量字面值都可以。

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

6.5.2 内联函数和constexpr函数

将规模较小的操作定义为函数****的优点:

  1. 阅读和理解函数调用更简单
  2. 使用函数可以确保行为统一
  3. 修改函数更方便
  4. 函数可以被重复利用

使用函数的缺点:

  1. 调用函数更慢

原因:需要保护现场及恢复等一系列操作,可能要拷贝实参,程序要转到新位置继续执行。

使用内联函数可****以避免调用函数的时间开销

在函数前用 inline 声明一下即表明是内联函数

内联函数适用于规模小、流程直接、频繁调用的函数

constexpr函数

constexpr 函数被隐式地指定为内联函数

cosntexpr 是指能用于常量表达式的函数。但是 constexpr 函数不一定返回常量表达式。

constexpr 函数的返回类型及所有的形参类型都必须是字面值类型函数体中必须有且只有一条 return 语句

内联函数和 constexpr 函数可以多次定义,但是多个定义必须完全一致。

应该把内联函数和constexpr函数的定义放到头文件里

6.5.3 调试帮助

assert 预处理宏

用法:assert(expr); assert 宏定义在头文件 cassert 中。

如果表达式 expr 为假,assert 输出信息并终止程序,如果表达式为真,assert 什么也不做。

assert 常用于检查“不能发生”的条件

NDEBUG 预处理变量

如果定义了 NDEBUG,则 assert 什么也不做,默认状态下没有定义 NDEBUG。

预处理器定义了 5 个对于程序调试很有用的名字。

1
2
3
4
5
__func__;//当前函数名的字符串字面值
__FILE__;//当前文件名的字符串字面值
__LINE__;//当前行号的整型字面值
__TIME__;//文件编译时间的字符串字面值
__DATA__;//文件编译日期的字符串字面值

6.6 函数匹配

当有多个重载函数时,选出要调用的重载函数的流程:

1、找出所有的候选函数:同名函数并且在调用点声明可见。

2、从候选函数中找出所有的可选函数:实参数量与形参数量相同且类型相同或能转换为形参的类型。

3、从可行函数中找出最佳匹配。如果不存在最佳匹配会报错:二义性。

调用重载函数应尽量避免强制类型转换。

6.6.1 实参类型转换

实参类型到形参类型的转换分为几个等级,排序如下:

1、从数组类型或函数类型转化为对应的指针类型,添加顶层const或删除顶层const

2、通过const转换实现的匹配。

3、通过类型提升实现的匹配

4、通过算术类型转换或指针转换实现的匹配

5、通过类类型转换实现的匹配

注意:所有算数类型转换的级别都一样。

6.7 函数指针

函数指针是指向函数类型的指针,就像 int 指针是指向 int 类型的指针。

一种函数指针只能指向一种特定的函数类型:

1
2
3
4
5
6
bool Compare(const string&, const string&);  // 此函数的类型是 bool(const string&, const string&);
bool (*pf)(const string&, const string&); // 声明了一个指向 bool(const string&, const string&) 类型函数的指针,注意括号不能少;
pf = Compare; // 给指针赋值,指向 Compare 函数
pf = &Compare; // 这两种赋值语句完全等价
bool b1 = pf("hello","goodbye"); // 可以直接使用指针替代函数名调用函数。
bool b2 = (*pf)("hello","goodbye"); // 与上面的等价

当把函数名作为一个值使用时,函数自动地转换成指针。

不同函数类型的指针间不能相互转换。函数指针也可以指向 nullptr 或 0。

对于重载函数,指针类型必须与重载函数中的某一个精确比配。

函数指针的别名

函数指针写起来很复杂,尤其是将函数指针作为函数的返回值时,因此一般为其定义别名。定义别名时要注意区分函数类型、函数指针。

下面几个等价:定义的别名都是函数类型。

1
2
3
typedef bool func(const string&, const string&);    // 定义了一个别名:func,但是 func 是函数类型
typedef decltype(Compare) func2; // 定义了一个别名:func2,func2 也是函数类型
using func3 = bool(const string&, const string&); // 定义了一个别名:func3,func3 也是函数类型

注意 decltype(函数名) 返回的不是指针,是函数类型,加上 * 才表示函数指针。

下面几个等价:定义的别名都是函数指针。

1
2
3
typedef bool (*func)(const string&, const string&);  // 定义了一个别名:func, func 是函数指针
typedef decltype(Compare)* func2; // 定义了一个别名:func2,func2 也是函数指针
using func3 = bool(*)(const string&, const string&); // 定义了一个别名:func3,func3 也是函数指针

函数指针形参

函数不能作形参,但是函数指针可以做形参,之后在调用时可以直接传入函数名作实参

函数名做形参也会自动的转换为指针。

1
2
bool GetBigger(const string& s1, const string& s2, bool(*comp)(const string&, const string&));  // 函数指针做形参
GetBigger(s1, s2, Compare); // 实参直接传入函数名 Compare

返回函数指针

不能返回一个函数,但是可以返回函数指针(注意这时函数名不会自动转换为函数指针)。

声明一个返回函数指针的函数有几种方法,其中直接声明最麻烦,使用尾置类型和 decltype 更简单一些,但是最好使用类型别名。

1
2
3
4
5
6
7
8
9
10
'直接声明'
bool (*f1(int))(double); // f1 是一个函数,函数的形参列表为 int,返回类型为函数指针。这个函数指针的类型为 bool (*)(double),即形参为 double,返回类型为 bool。
'使用尾置类型'
auto f1(int) -> bool(*)(double);
'使用 decltype'
bool func(double);
decltype(func)* f1(double);
'使用类型别名'
using PF = bool(*)(double);
PF f1(int);