C++ 第七章 指针、数组与引用 - 7.4 数组中的指针

7.4 数组中的指针

在C++语言中,指针与数组密切相关。数组名可以看成是指向数组首元素的指针,例如:

int v[] = {
    
    1,2,3,4};
int* p1 = v;		//指向数组首元素的指针(隐式转换)
int* p2 = &v[0];	//指向数组首元素的指针
int* p3 = v+4;	//指向数组尾后位置的指针

或者表示成图形的形式:
在这里插入图片描述
令指针指向数组的最后一个元素的下一个位置(尾后位置)是有效的,这对于很多算法(见4.5节和33.1节)非常重要。不过,因为该指针事实上指向的并不是数组中的任何一个元素,所以不能对它进行读写操作。试图获取和使用数组首元素之前或者尾元素之后的地址都是未定义的行为,应该尽量避免。例如:

int* p4 = v - 1;	//首元素之前的位置是未定义的,应该避免使用
int* p5 = v + 7;	//尾元素之后的位置是未定义的,应该避免使用

数组名向数组首元素指针的隐式类型转换在C风格代码的函数调用中广泛使用,例如:

extern “C” int strlen(const char*);	//定义在<string.h>中
void f()
{
    
    
	char v[] = “Annemarie”;
	char* p = v;					//char[]到char*的隐式类型转换
	strlen(p);
	strlen(v);						//char[]到char*的隐式类型转换
	v = p;							//错误:不允许给数组直接赋值
}

两次调用传入标准库函数strlen()的值是相同的。唯一的问题是这种隐式类型转换无法避免,注定会发生。换句话说,我们不可能让函数接受整个数组v。不过幸运的是,不存在从指针向数组的显式或隐式类型转换。

数组实参向指针的隐式类型转换意味着数组的大小在调用函数时丢掉了,然而函数经常需要以某种方式得到数组的大小以便执行某些必要的操作。和其他接受字符指针的C标准库函数一样,strlen()也用0作为字符串的结束符,strlen§返回的是字符串中除了结束符0之外其他字符的总数。以上提及的都是一些非常底层的细节。标准库vector(见4.4.1节,13.6节和31.4节)、array(见8.2.4节和34.2.1节)和string(见4.2节)不受这些问题困扰。这些标准库类型的元素数量可以通过size()得到,不需要每次都重新计算。

7.4.1数组漫游

如何便捷高效地访问数组(以及类似的数据结构)是很多算法的关键(见4.5节和第32章)。我们既可以通过指向数组的指针加上一个索引值来访问数组元素,也可以通过直接指向数组元素的指针进行访问。例如:

void fi(char v[])
{
    
    
	for(int i = 0; v[i]!=0; ++i)
		user(v[i]);
}
void fp(char v[])
{
    
    
	for(char* p = v; *p != 0; ++p)
		use(*p);
}

前置运算符执行解引用运算,因此p是指针p所指的字符,++运算令p指向数组的下一个元素。

这两个版本的代码不存在谁比谁更快的问题。在现代编译器中,两个例子编译生成的代码应该是一样的(大多数情况下也确实一样)。程序员可以从逻辑性和优美程度出发自由选择。

内置数组的取下标操作是通过组合指针的+和*两种运算得到的。对于内置数组a和数组范围之内的整数j,有下式成立:

a[j] == *(&a[0] + j) == *(a + j) == *(j + a) == j[a]

人们常常会纠结于为什么a[j] == j[a],比如 3[“Texas”] == “Texas”[3] == ‘a’,其实这种小聪明在实际的代码中并没有多少展示的空间。上面这些等价关系属于非常底层的规则,并不适用于array和vector等标准库容器。

把+、-、++、–等算术运算符用在指针上得到的结果依赖于指针所指对象的数据类型。当我们对T*类型的指针p执行算术运算时,p指向T类型的数组元素,p+1指向数组的下一个元素,p-1指向上一个元素。上述规则意味着p+1对应的整数值比p对应的整数值大sizeof(T)。例如:

template<typename T>
int byte_diff(T* p, T* q)
{
    
    
	return reinterpret_cast<char*>(q)-reinterpret_cast<char*>(p);
}

void diff_test()
{
    
    
	int vi[10];
	short vs[10];
	cout<< vi << ‘ ’ <<&vi[1] << ‘ ’ << &vi[1]-&vi[0] << ‘ ’ << byte_diff(&vi[0], &vi[1] << ‘\n’;
	cout<< vs << ‘ ’ <<&vs[1] << ‘ ’ << &vs[1]-&vs[0] << ‘ ’ << byte_diff(&vs[0], &vs[1] << ‘\n’;
}

这段代码的输出结果是

0x7fffaef0 0x7fffaef4 1 4
0x7fffaedc 0x7fffaede 1 2

指针值以默认的十六进制形式输出,从上面的结果我们可以知道,在我所用的C++实现版本中sizeof(short)是2,sizeof(int)是4。

指针的减法运算只有当参与运算的两个指针指向的是同一个数组中的元素时才有效(尽管C++语言本身并没有一种机制可以快速地检测该条件是否满足)。当计算两个指针p和q的差值(q-p)时,所得结果是序列[p:q)中的元素数量(一个整数)。我们可以给指针加上一个整数或者从指针中减去一个整数,得到的结果都是指针。如果该指针指向的位置既不是原数组中的元素,也不是尾后元素,那我们不能使用它,否则会产生未定义的行为。例如:

void f()
{
    
    
	int v1[10];
	int v2[10];

	int i1 = &v1[5]-&v1[3];		//i1 = 2
	int i2 = &v1[5]-&v2[3];		//结果是未定义的

	int* p1 = v2+2;				//p1 = &v2[2]
	int* p2 = v2-2;				//*p2是未定义的
}

复杂的指针算术运算通常没什么必要,最好避免使用。此外,直接把两个指针相加没有实际意义,C++也不允许这样做。

因为数组的元素数量不一定能与数组本身存储在一起,所以数组不具有自解释性。当我们需要遍历一个数组并且它不像C风格的字符串那样具有明确的终结符时,我们必须以某种方式提供元素的数量。例如:

void fp(char v[], int size)
{
    
    
	for(int i = 0; i != size; ++i)
		use(v[i]);		//祈祷数组v至少包含size个元素,否则就会越界
	for(int x : v)
		use(x);			//错误:范围for循环对指针无效

	const int N = 7;
	char v2[N];
	for(int i = 0; i!=N; ++i)
		use(v2[i]);
	for(int x : v2)
		use(x);			//当已知数组的大小时,可以使用范围for循环
}

数组是一个底层的语言概念,标准库容器array(见8.2.4节和34.2.1节)具有内置数组的绝大多数优点,同时规避掉了它的很多缺点。某些C++实现为数组提供了可选的范围检查操作,然而这种范围检查的时空开销可能会非常大,因此大多数时候我们只把它作为开发的辅助工具,而不会真的包含在最终代码中。如果你不打算使用范围检查功能,则一定要设法以一种切实有效的措施确保访问元素不会越界。我的建议是使用vector等更高层次的容易类型管理数组,元素的有效范围非常明确,我们一般不会用错。

7.4.2 多维数组

多维数组是指数组的数组。我们可以用下面的语句声明一个 3*5的数组:

int ma[3][5];		//3行,每行5个int

初始化ma的语句是:

void init_ma()
{
    
    
	for(int i = 0; i != 3; i++)
		for(int j = 0; j != 5; j++)
			ma[i][j] = 10*i+j;
}

或者表示成图形的形式:
在这里插入图片描述
如果数组ma包含3行且每行有5个int,则我们可以把它看成是连续15个int。在内存中不存在一个表示矩阵ma的单独的对象,我们只存储了数组的元素。维度3和5只在编译器源代码中有效。当我们编写代码时,必须shi’ke1谨记数组的维度并且在需要使用的时候提供出来。例如,我们用下面的代码输出ma的内容:

void print_ma()
{
    
    
	for(int i = 0; i != 3; i++){
    
    
		for(int j = 0; j != 5; j++)
			cout<<ma[i][j]<< ‘\t’;
		cout<< ‘\n’;
	}
}

在别的某些编程语言中,有时候用逗号分隔数组的边界。C++不允许这样做,因为逗号(,)是表示序列的运算符(见10.3.2节)。好在编译器能捕获大多数此类错误,例如:

int bad[3,5];				//错误:常量表达式中不能使用逗号
int good[3][5];				//3行,每行5个int
int ouch = good[1, 4];		//错误:试图用int*初始化int(good[1,4]的意思是good[4],他的类型显然是int*)
int nice = good[1][4];

7.4.3 传递数组

不能以值传递的方式直接把数组传给函数,我们通常传递的是指向数组首元素的指针。例如:

void comp(double arg[10])			//arg的类型是double*
{
    
    
	for(int i = 0; i != 10; ++i)
		arg[i] += 99;
}
void f()
{
    
    
	double a1[10];
	double a2[5];
	double a3[100]

	comp(a1);
	comp(a2);	//严重错误!
	comp(a3);	//只用到了前10个元素
}

上面的代码看似正确,实则不然。它虽然能通过编译,但是调用comp(a2)试图向a2的合法边界之外的区域写入内容。此外,如果你期望数组以值传递的方式传给函数,恐怕也要大失所望了:对arg[i]执行写操作实际上是直接向comp()的实参的元素写内容,而不是工作在该实参的一份副本上。comp()函数的等价形式是:

void comp(double* arg)
{
    
    
	for(int i = 0; i != 10; ++i)
		arg[i] += 99;
}

现在问题更加明显了。当数组作为函数的实参时,我们完全把数组的第一维当成指针使用,而忽略了数组的边界。因此,如果你想在给函数传入一组元素的同时不丢掉数组的大小,就不能使用内置数组类型。你可以把数组放在类中作为类的成员(类似于std::array),或者直接定义一个句柄类(类似于std::string和std::vector)。

如果苛刻点儿评价的话,使用内置数组有百弊而无一利。当我们需要定义一个接受二维矩阵的函数时,如果编译时知道数组的具体维度当然没有问题:

void print_m35(int m[3][5])
{
    
    
	for(int i = 0; i != 3; i++){
    
    
		for(int j = 0; j != 5; j++)
			cout << m[i][j] << ‘\t’;
		cout << ‘\n’;
	}
}

实参从形式上看虽然是多维数组表示的矩阵,但实际传入函数的是个指针(而非矩阵的副本,见7.4节)。数组的第一个维度与定位元素无关,它只负责指明当前类型(此处是int[5])包含几个元素(此处是3)。例如在前面提到的ma中,只要知道第二个维度是5,我们就能定位任意的ma[i][5]。此时,可以把数组的第一个维度当成实参传入函数:

void print_mi5(int m[][5], int dim1)
{
    
    
	for(int i = 0; i != dim1; i++){
    
    
		for(int j = 0; j != 5; j++)
			cout << m[i][j] << ‘\t’;
		cout << ‘\n’;
	}
}

但是当需要传入两个维度时,“显而易见的解决方案”并不有效:

void print_mij(int m[][], int dim1, int dim2)		//预期的结果并不一致
{
    
    
	for(int i = 0; i != dim1; i++){
    
    
		for(int j = 0; j != dim2; j++)
			cout << m[i][j] << ‘\t’;				//意料之外的结果!
		cout << ‘\n’;
	}
}

好在编译器会因实参声明m[ ][ ]非法而报错,因为多维数组的第二个维度必须是已知的,这样我们才能准确定位其中的元素。然而,表达式m[i][j]会被编译器理解成*(*(m+i)+j),尽管这绝非程序员的愿意。一种正确的解决方案是:

void print_mij(int* m, int dim1, int dim2)
{
    
    
	for(int i = 0; i != dim1; i++){
    
    
		for(int j = 0; j != dim2; j++)
			cout << m[i*dim2+j] << ‘\t’;		//有点儿难懂
		cout << ‘\n’;
	}
}

其中用来访问数组成员的表达式就是编译器在已知最后一个维度的时候所用的方式。

要想调用该函数,我们只需传入一个代表矩阵的指针即可:

int test()
{
    
    
	int v[3][5] = {
    
    
				{
    
    0,1,2,3,4},(10,11,12,13,14),{
    
    20,21,22,23,24}
				};
	print_m35(v);
	print_mi5(v,3);
	print_mij(&v[0][0], 3, 5);
}

请注意,在最后一个调用中我们使用了v[0][0]。此处使用v[0]也是可以的,因为它与v[0][0]等价;但是直接用传入v会引发类型错误。这样的代码含义微妙、用法凌乱,还是越少出现越好。如果你必须直接处理多维数组,那么记得把有关的代码封装起来,这样其他程序员在用到你的代码时会容易上手一些。最好的办法是给多维数组提供适当的下标运算符,这样用户就不必被数组中元素的分布情况困扰了(见29.2.2节和40.5.2节)。

标准库vector(见31.4节)完美地解决了上述问题。

猜你喜欢

转载自blog.csdn.net/qq_40660998/article/details/121852137
今日推荐