继续《C++ Primer》,很多知识点真的是用到时才会有体会,多记多写吧。这一篇笔记包括前置与后置递增递减运算符对比,局部静态对象,函数以数组作为形参,以及函数返回数组指针。
前置递增递减vs后置递增递减
关于i++
与++i
:除非必须,否则不用递增递减运算符的后置版本。
前置版本的递增运算符避免了不必要的工作,它把值加1后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。
对于整数和指针类型来说,编译器可能对这种额外的工作进行一定的优化;但是对于相对复杂的迭代器类型,这种额外的工作就消耗巨大了。建议养成使用前置版本的习惯,这样不仅不需要担心性能的问题,而且更重要的是写出的代码会更符合编程的初衷。
局部静态对象
某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static
类型从而获得这样的对象。局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
举个例下,下面的函数统计它自己被调用了多少次:
1 | size_t count_calls() |
这段程序将输出从1到10(包括10在内)的数字。
在控制流第一次经过ctr
的定义之前,ctr
被创建并初始化为0。每次调用将ctr
加1并返回新值。每次执行count_calls
函数时,变量ctr
的值都已经存在并且等于函数上一次退出时ctr
的值,因此,第二次调用时ctr
的值是1,第三次调用时ctr
的值是2,以此类推。
如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0。
函数数组形参
当数组作为函数的形参时,数组会被转换成指针。当为一个函数传递一个数组时,实际上传递的是指向数组首个元素的指针。
以下三个函数是等价的:
1 | // despite appearances, these three declarations of print are equivalent |
每个函数的唯一形参都是const int*
类型,当编译器处理对print
的调用时,只检查传入的参数是否是const int*
类型:
1 | int i = 0, j[2] = {0, 1}; |
如果我们传给print
函数的是一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数调用没有影响。
因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用技术:
使用标记指定数组长度
这种方法要求数组有一个结束标记,典型示例是C风格字符串:
1 | void print(const char *cp) |
使用标准库规范
管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针。可以按照如下方式输出元素的内容:
1 | void print(const int *beg, const int *end) |
为调用此函数,我们需要传入两个指针,一个只想要输出的首元素,另一个指向尾元素的下一位置:
1 | int j[2] = {0, 1}; |
注意std::end(..)
得到的是尾后元素的指针,类似于STL库中容器的超尾迭代器,不能解引用。
显式传递一个表示数组大小的形参
第三种管理数组实参的方法是专门定义一个表示数组大小的形参,在C程序和过去的C++程序中常常使用这种方法。
1 | // const int ia[] is equivalent to const int* ia |
这个版本的程序通过形参size
的值确定要输出多少个元素,调用print
函数时必须传入这个表示数组大小的值:
1 | intj[]={0,1}; // int array of size2 |
数组引用形参
形参可以是数组的引用。此时,引用形参绑定到对应的实参上,即绑定到数组上:
1 | // ok: parameter is a reference to an array; the dimension is part of the type |
注意不能将&arr
两端的括号去掉,f(int &arr[10])
中将arr
声明成了引用的数组。
注意:因为数组的大小是构成数组类型的一部分,所以只要不超过维度,在函数体内就可以放心地使用数组。但是,这一用法也无形中限制了print
函数的可用性,我们只能将函数作用于大小为10的数组:
1 | int i = 0, j[2] = {0, 1}; |
传递多维数组
C++中的多维数组其实就是数组的数组。
和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维(及后边所有维度)的大小都是数组类型的一部分,不能省略:
1 | // matrix points to the first element in an array whose elements are arrays of ten ints |
上述语句将matrix
声明成含有10个整数的数组的指针。注意*matrix
两端的括号必不可少,int *matrix[10]
指的是由10个指针构成的数组。
我们也可以使用数组的语法定义函数,此时编译器会一如既往地忽略掉第一个维度,所以最好不要把它包括在形参列表内:
1 | // equivalent definition |
matrix
的声明看起来像是一个二维数组,实际上形参是指向含有10个整数的数组的指针。
函数返回数组指针
因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较烦琐,但是有一些方法可以简化这一任务,其中最直接的方法是使用类型别名。
1 | typedef int arrT[10]; // arrT is a synonym for the type array of ten ints |
其中arrT
是含有10个整数的数组的别名。因为我们无法返回数组,所以将返回类型定义成数组的指针。因此,func
函数接受一个int
实参,返同一个指向包含10个整数的数组的指针。
声明一个返回数组指针的函数
要想在声明func
时不使用类型别名,我们必须牢记被定义的名字后面数组的维度:
1 | int arr[10]; // arr is an array of ten ints |
和这些声明一样,如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。然而,函数的形参列表也跟在函数名宇后面且形参列表应该先于数组的维度。因此,返回数组指针的函数形式如下所示:
1 | Type (*function(parameter_list))[dimension] |
类似于其他数组的声明,Type
表示元素的类型,dimension
表示数组的大小。(*function(parameter_list))
两端的括号必须存在,就像我们定义p2
时两端必须有括号一样。如果没有这对括写,函数的返回类型将是指针的数组。
举个具体点的例子,下面这个func
函数的声明没有使用类型别名:
1 | int (*func(int i))[10]; |
可以按照以下的顺序来逐层理解该声明的含义:
func(int)
表示调用func
函数时需要一个int
类型的实参。(*func(int))
意味着我们可以对函数调用的结果执行解引用操作。(*func(int))[10]
表示解引用func
的调用将得到一个大小是10的数组。int (*func(int))[10]
表示数组中的元素是int类型。
使用尾置返回类型
在C++11新标准中还有一种可以简化上述func
声明的方法,就是使用尾置返回类型(trailing return type)。任何函数的定义都能使用尾置返回,但是这种形式对于返同类型比较复杂的函数最有效,比如返同类型是数组的指针或数组的引用。尾置返回类型在形参列表后面并以一个->
符号开头。为了表示函数真正的返同类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto
:
1 | // func takes an int argument and returns a pointer to an array of ten ints |
因为我们把函数的返同类型放在了形参列表之后,所以可以清楚地看到func
函数返回的是一个指针,并且该指针指向了含有10个整数的数组。
使用decltype
还有一种情况,如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype
关键字返回类型。例如,下面的函数返回一个指针,该指针根据参数i的不同指向两个已知数组中的某一个:
1 | int odd[] = {1,3,5,7,9}; |
arrPtr
使用关键字decltype
表示它的返同类型是个指针,并且该指针所指的对象与odd
的类型一致,因为odd
是数组,所以arrPtr
返回一个指向含有5个整数的数组的指针。有一个地方需要注意:decltype
并不负责把数组类型转换成对应的指针,所以decltype
的结果是个数组,要想表示arrPtr
返回指针还必须在函数声明时加一个*
符号。