C++笔记(三)

继续《C++ Primer》,很多知识点真的是用到时才会有体会,多记多写吧。这一篇笔记包括前置与后置递增递减运算符对比,局部静态对象,函数以数组作为形参,以及函数返回数组指针。

前置递增递减vs后置递增递减

关于i++++i:除非必须,否则不用递增递减运算符的后置版本。

前置版本的递增运算符避免了不必要的工作,它把值加1后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。

对于整数和指针类型来说,编译器可能对这种额外的工作进行一定的优化;但是对于相对复杂的迭代器类型,这种额外的工作就消耗巨大了。建议养成使用前置版本的习惯,这样不仅不需要担心性能的问题,而且更重要的是写出的代码会更符合编程的初衷。

局部静态对象

某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

举个例下,下面的函数统计它自己被调用了多少次:

1
2
3
4
5
6
7
8
9
10
11
12
size_t count_calls()
{
static size_t ctr = 0; // value will persist across calls
return ++ctr;
}

int main()
{
for (size_t i = 0; i != 10; ++i)
cout << count_calls() << endl;
return 0;
}

这段程序将输出从1到10(包括10在内)的数字。

在控制流第一次经过ctr的定义之前,ctr被创建并初始化为0。每次调用将ctr加1并返回新值。每次执行count_calls函数时,变量ctr的值都已经存在并且等于函数上一次退出时ctr的值,因此,第二次调用时ctr的值是1,第三次调用时ctr的值是2,以此类推。

如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0。

函数数组形参

当数组作为函数的形参时,数组会被转换成指针。当为一个函数传递一个数组时,实际上传递的是指向数组首个元素的指针。

以下三个函数是等价的:

1
2
3
4
5
// despite appearances, these three declarations of print are equivalent
// each function has a single parameter of type const int*
void print(const int*);
void print(const int[]); // shows the intent that the function takes an array
void print(const int[10]); // dimension for documentation purposes (at best)

每个函数的唯一形参都是const int*类型,当编译器处理对print的调用时,只检查传入的参数是否是const int*类型:

1
2
3
int i = 0, j[2] = {0, 1};
print(&i); // ok: &i is int*
print(j); // ok: j is converted to an int* that points to j[0]

如果我们传给print函数的是一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数调用没有影响。

因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用技术:

使用标记指定数组长度

这种方法要求数组有一个结束标记,典型示例是C风格字符串:

1
2
3
4
5
6
void print(const char *cp)
{
if (cp) // if cp is not a null pointer
while (*cp) // so long as the character it points to is not a null character
cout << *cp++; // print the character and advance the pointer
}

使用标准库规范

管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针。可以按照如下方式输出元素的内容:

1
2
3
4
5
6
void print(const int *beg, const int *end)
{
// print every element starting at beg up to but not including end
while (beg != end)
cout << *beg++ << endl; // print the current element // and advance the pointer
}

为调用此函数,我们需要传入两个指针,一个只想要输出的首元素,另一个指向尾元素的下一位置:

1
2
3
4
int j[2] = {0, 1};
// j is converted to a pointer to the first element in j
// the second argument is a pointer to one past the end of j
print(begin(j), end(j)); // begin and end functions

注意std::end(..)得到的是尾后元素的指针,类似于STL库中容器的超尾迭代器,不能解引用。

显式传递一个表示数组大小的形参

第三种管理数组实参的方法是专门定义一个表示数组大小的形参,在C程序和过去的C++程序中常常使用这种方法。

1
2
3
4
5
6
7
8
9
// const int ia[] is equivalent to const int* ia
// size is passed explicitly and used to control access to elements of ia
void print(const int ia[], size_t size)
{
for (size_t i = 0; i != size; ++i)
{
cout << ia[i] << endl;
}
}

这个版本的程序通过形参size的值确定要输出多少个元素,调用print函数时必须传入这个表示数组大小的值:

1
2
intj[]={0,1}; // int array of size2 
print(j, end(j) - begin(j));

数组引用形参

形参可以是数组的引用。此时,引用形参绑定到对应的实参上,即绑定到数组上:

1
2
3
4
5
6
// ok: parameter is a reference to an array; the dimension is part of the type 
void print(int (&arr)[10])
{
for (auto elem : arr)
cout << elem << endl;
}

注意不能将&arr两端的括号去掉,f(int &arr[10])中将arr声明成了引用的数组。

注意:因为数组的大小是构成数组类型的一部分,所以只要不超过维度,在函数体内就可以放心地使用数组。但是,这一用法也无形中限制了print函数的可用性,我们只能将函数作用于大小为10的数组:

1
2
3
4
5
int i = 0, j[2] = {0, 1};
int k[10] = {0,1,2,3,4,5,6,7,8,9};
print(&i); // error: argument is not an array of ten ints
print(j); // error: argument is not an array of ten ints
print(k); // ok: argument is an array of ten ints

传递多维数组

C++中的多维数组其实就是数组的数组。

和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维(及后边所有维度)的大小都是数组类型的一部分,不能省略:

1
2
// matrix points to the first element in an array whose elements are arrays of ten ints 
void print(int (*matrix)[10], int rowSize) { /* . . . */ }

上述语句将matrix声明成含有10个整数的数组的指针。注意*matrix两端的括号必不可少,int *matrix[10]指的是由10个指针构成的数组。

我们也可以使用数组的语法定义函数,此时编译器会一如既往地忽略掉第一个维度,所以最好不要把它包括在形参列表内:

1
2
// equivalent definition
void print(int matrix[][10], int rowSize) { /* . . . */ }

matrix的声明看起来像是一个二维数组,实际上形参是指向含有10个整数的数组的指针。

函数返回数组指针

因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较烦琐,但是有一些方法可以简化这一任务,其中最直接的方法是使用类型别名。

1
2
3
typedef int arrT[10]; // arrT is a synonym for the type array of ten ints
using arrtT = int[10]; // equivalent declaration of arrT
arrT* func(int i); // func returns a pointer to an array of five ints

其中arrT是含有10个整数的数组的别名。因为我们无法返回数组,所以将返回类型定义成数组的指针。因此,func函数接受一个int实参,返同一个指向包含10个整数的数组的指针。

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

要想在声明func时不使用类型别名,我们必须牢记被定义的名字后面数组的维度:

1
2
3
int arr[10]; // arr is an array of ten ints
int *p1[10]; // p1 is an array of ten pointers
int (*p2)[10] = &arr; // p2 points to 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
2
// func takes an int argument and returns a pointer to an array of ten ints
auto func(int i) -> int(*)[10];

因为我们把函数的返同类型放在了形参列表之后,所以可以清楚地看到func函数返回的是一个指针,并且该指针指向了含有10个整数的数组。

使用decltype

还有一种情况,如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字返回类型。例如,下面的函数返回一个指针,该指针根据参数i的不同指向两个已知数组中的某一个:

1
2
3
4
5
6
7
int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
// returns a pointer to an array of five int elements
decltype(odd) *arrPtr(int i)
{
return (i % 2) ? &odd : &even; // returns a pointer to the array
}

arrPtr使用关键字decltype表示它的返同类型是个指针,并且该指针所指的对象与odd的类型一致,因为odd是数组,所以arrPtr返回一个指向含有5个整数的数组的指针。有一个地方需要注意:decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组,要想表示arrPtr返回指针还必须在函数声明时加一个*符号。

REFERENCE

《C++ Primer 第五版》