《C++ Primer》函数总结(部分)

《C++ Primer》函数总结(部分)

简单地说,函数是命了名的计算单元,也就是一个代码块:调用了函数就可以执行相应的代码块。在这篇笔记里,我不打算详细的介绍很多基本概念,相反地,我会更加关注于一些稍微进阶一点的知识以及如何避免出现问题的方法。

函数基础

调用函数的过程

首先,函数的调用会完成两项工作:1. 用实参初始化函数对应的形参;2. 将控制权转移给被调函数。主调函数的执行被中断,被调函数开始执行。当遇到一条 return 语句的时候函数结束执行过程。

没有规定实参的求值顺序

实参是形参的初始值。第一个实参初始化第一个形参,第二个实参初始化第二个形参,以此类推。也就是说实参和形参存在对应关系,但是需要注意的是:没有规定实参的求值顺序,编译器能以任意可行的顺序对实参求值。

函数的返回类型

函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。(后面会讲到这个内容)。

局部对象

首先理解生命周期的概念:程序执行过程中该对象存在的一段时间

各种变量的生命周期?

  • 在所有函数体之外定义的变量存在于程序的整个执行过程中。那么这类对象在程序启动时创建,直到程序结束时才会销毁。
  • 那对于局部变量呢?
    • 对于普通局部变量对应的对象来说:当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾销毁。(这种对象也称为自动对象
    • 而对于局部静态对象:它在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止时才销毁。

参数传递

每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。

传值调用与传引用调用

  • 传值调用:初始值拷贝给形参
  • 传引用调用:引用形参绑定初始化它的对象

使用引用避免拷贝

拷贝大的类类型或者容器对象比较低效,甚至有的类型(包括 IO 类型在内)根本就不支持拷贝操作(比如没有拷贝构造函数或者拷贝赋值运算符的类类型)。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
同时,当函数无须修改引用参数的值时,最好使用常量引用

const 形参和实参

当用实参初始化 const 形参时会忽略掉顶层 const 。也就是说,传给它常量对象还是非常量对象都是可以的,那么就会出现问题:

void fcn(const int i);
void fcn(int i); // 错误:重复定义,二义性调用

尽量使用常量引用

使用引用而不是常量引用会极大地限制函数所能接受的实参类型:例如不能把 const 对象、字面值或者需要类型转换的对象传递给普通的引用形参。
如果函数无须改变引用形参的值,最好将其声明为常量引用。

数组形参

简单地列举使用数组形参的几种方式:

void print(const int*);
void print(const int[]);
void print(const int[10]);

void print(const char *cp);

// 使用标准库提供的 begin() 和 end()
void print(const int *beg, const int *end);
// 使用方法: print(begin(arr), end(arr));

// 数组引用形参 &arr两端的括号必不可少
void print(int (&arr)[10]);

// 如何传递多维数组?
void print(int matrix[][10]);
void print(int (*matrix)[10]); // 同样 *matrix 两端的括号必不可少

返回类型和 return 语句

首先关注的一个问题是值是如何被返回的?其实答案不复杂:返回的值初始化一个临时变量,该临时变量就是函数调用的结果;如果返回的是引用类型,那么返回的引用就仅仅是它所引用对象的一个别名。
那么这也可以告诉我们:不要返回局部变量的引用或者指针,因为函数完成后,局部变量所占用的内存也就随之被释放掉了。

还有一点值得关注的就是:引用返回左值。那我们也就可以“骚操作”一下:为返回类型是非常量引用的函数的结果赋值。

列表初始化返回值

C++11 新标准规定,函数可以返回花括号包围的值的列表。

vector<string> getString() {
    return {"Hello", "World"};
}

返回数组指针

使用类型别名

使用类型别名是最简单的方法:

typedef int arrT[10]; // arrT 是一个类型别名,表示含有10个整数的数组
using arrT = int[10]; // C++11 新标准

arrT *func(int i);    // func 返回一个数组指针

啥也不用,就硬刚

真的勇士:int (*func(int i))[10];

使用尾置返回类型

C++11 新标准:`auto func(int i) -> int(*)[10];

使用 decltype

有一种情况是,如果我们知道函数返回的数组指针将会指向哪个数组,就可以使用 decltype 关键字声明返回类型。

int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};

decltype(odd) *func(int i) {
    return (i % 2) ? &odd : &even;
}

有一个地方需要注意:decltype 并不会把数组类型转换为对应的指针,所以 decltype 在这里的结果是一个数组,这也就意味着我们需要在函数声明前加多一个 * 符号。

特殊用途语言特性

默认实参

这里不用多说了,主要就是注意一个地方:一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值

内联函数

为什么会出现内联函数?在大多数机器上,一个函数调用其实包含着一系列的工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝形参;程序转向一个新的地方执行。
对于规模较小的函数来说,这一系列操作未免也太浪费性能了,于是就出现了内联函数。
内联函数可以避免函数调用的开销,把函数指定为内敛(inline),通常就意味着在每个调用点上“内联地”展开。

inline const string &shorterString(const string &s1, const string &s2) {
    return s1.size() <= s2.size() ? s1 : s2;
}

// 如果像这样调用:
std::cout << shorterString(s1, s2) << std::endl;
// 那么在编译时会类似这样展开:
std::cout << s1.size() <= s2.size() ? s1 : s2 << std::endl;

需要注意的是:内联说明只是向编译器提供的一个建议,是否内联最终还是由编译器来决定
建议:内联机制用于优化规模较小、流程直接、调用频繁的函数

constexpr 函数

constexpr 函数是指能够作用于常量表达式的函数,它有几点要求:

  • 函数的返回类型以及所有的形参都得是字面值类型
  • 函数体中必须有且只有一条 return 语句
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz();

作用机理:编译器会将对 constexpr 函数的调用替换成其结果值,为了能在编译过程中随时展开,constexpr 函数被隐式地指定为内联函数。

值得注意的是,constexpr 函数不一定返回常量表达式

constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }

int arr[scale(2)];  // 正确:scale(2)是常量表达式
int i = 2;          // i 不是常量表达式
int a2[scale(i)];   // 错误:scale(i)不是常量表达式

从上面的例子,也就是说当给 scale() 传入一个常量表达式时,它返回一个常量表达式,否则它会返回一个非常量表达式。

最后的建议:把内联函数和 constexpr 函数放在头文件内