《C专家编程》:再论指针

Posted 戎·码一生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《C专家编程》:再论指针相关的知识,希望对你有一定的参考价值。

     千万不要忘了,当你把一个手指指向别人的时候,你手上的另外还有三个手指指向了你自己.... 

                                                                                                                                   -----多疑间谍的格言

     在笔记(7)里面我们也讲解了,多维数组和指针的一些知识和转换。这节内容我们将继续介绍数组与指针有关的知识。
一、数组的数组和指针数组

       我们知道多维数组虽然看起来其存储结构是一张表,但是其实系统是决不允许程序按这种方式进行存储数据的。其单个元素的存储与引用都是以线性形式排列在内存中。如下图一所示:


图一

    数组下标规则告诉我们如何计算左值array[i][j],首先找到array[i]的位置,然后根据偏移量[j]取得字符。因此,array[i][j]总是被编译器解释为:*(*(array+i)+j)。
   但是我们需要特别注意的是: array[i]的步长将随数组定义的不用而改变。
   我们可以通过声明一个一维指针数组,其中每个指针指向一个字符串来取得类似二维数组字符数组的效果:
char *p[4]; //表示p是一个数组,数组里每个元素是一个指向char *的指针。
   这里我们主要对其进行了简化,指针实际上是声明为指向单个字符的。但是如果定义为指向字符的指针,就存在一种可能性,就是其他字符可能紧邻着它存储,隐式的形成了一个字符串。但是像下面这样的声明:
char  (*p[4])[5];//这才是真正声明了一个指向字符串的指针数组,并且它限制了每个字符串的长度只能为5;

其结构图如下图二所示。


图二

这种数组必须用指向为字符串而分配的内存的指针进行初始化,可以在编译时用一个常量初始值,也可以在运行的时候用下面这样的代码进行初始化。
for(i=0;i<4;i++)
p[i]=malloc(5);
或者是一次性的全部分配:
p=malloc(sizeof(char)*size_row*size_column);
这样可以减少调用malloc的维护开销,但是缺点是当处理完一个字符串时无法单独将其释放。
像上面所介绍的那样:虽然char *p[4]与char p[4][5]最终都被编译器翻译为:*(*(p+i)+j).但是他们在各自的情况下所引用的实际类型并不相同。

具体区别如下图三和图四:


图三


图四


      以上 char *p[4]的定义表示p是一个包含4个元素的数组,每个元素为一个指向char的指针。查询的过程先找到数组第i个元素(每个元素均为指针),取出指针的值,加上偏移量j,依次为地址,取出地址的内容。类似于笔记(4)里的指针的下表引用图三的过程。
二、锯齿状数组
    看这样一种情况,如果我们要存储50个字符串,并且每个字符串的长度可以达到256个字符,那么我们可以申请如下的数组:
char p[50][256];
   但是很不幸,在这个字符串里只有一个字符串的长度达到了256,其他的长度都只有10个字节左右。如果我们经常这样做的话,内存的浪费将非常大。一种替代方法就是使用字符串指针数组,注意它的所有二级数组并不要求长度相同。
char *p[4];

如果根据需要为这些字符串分配内存,将会大大节省系统的资源,有些人把它称作为“锯齿状”数组是因为它右端的长度不一。

其结构图如下图五所示:


图五

具体实现代码如下:

#include <iostream>
using namespace std;
int main()
{
	char *p[3];
	char *p1="hello!";
	char *p2="abc";
	char *p3="This is my life!";
	p[0]=p1;
	p[1]=p2;
	p[2]=p3;
	for(int i=0;i<3;i++)
		cout<<p[i]<<endl;
	system("pause");
	return 0;
}

运行结果如下:


    注:有时候的数据共享和移动,只要可能,尽量不要拷贝整个字符串,拷贝一个指针比拷贝整个数组要快的多,而且还大大节省了内存空间。
三、数组和指针是如何被编译器修改的?

 “数组名被改写成一个指针的参数”规则并不是递归定义的。数组的数组会改写为“数组的指针”,而不是“指针的指针”;

具体规则如下图六所示:


图六

      你之所以能在main()函数中看到char **argv这样的参数,是因为argv是个指针数组(即char *argv[]).这个表达式被编译器改写为指向数组第一个元素的指针,也就是一个指向指针的指针。如果argv参数实事上被声明为一个数组的数组(也就是char argv[4][5]),它将被编译器翻译为char (*)[5](一个数组指针),而不是指针的指针char **argv。

具体数组与指针的改写部分代码如下:

#include <iostream>
using namespace std;
void my_function1(int array[2][3][5])
{
	cout<<"1、It's OK"<<endl;
}
void my_function2(int (*array)[3][5])
{
	cout<<"2、It's OK"<<endl;
}
void my_function3(int array[][3][5])
{
	cout<<"3、It's OK"<<endl;
}
int main()
{
	int arr[2][3][5];
	my_function1(arr);
	my_function2(arr);
	my_function3(arr);
	
	int (*arr1)[3][5]=arr;
	my_function1(arr1);
	my_function2(arr1);
	my_function3(arr1);
	
	int (*arr2)[2][3][5]=&arr;
	my_function1(*arr2);
	my_function2(*arr2);
	my_function3(*arr2);
	system("pause");
	return 0;
}
运行结果如下:


四、C语言中,怎么确保向函数传递一个多维的数组?
1、对于一个一维的数组而言,形参被改写成指向第一个元素的指针,所以需要一个约定来提示数组的长度。一般有两个方法:
(1)增加一个额外的参数,表示元素的数目,这也是我们最常规的一种做法。例如argc就是起这个作用。
(2)赋予数组最后一个元素一个特殊的值。提示它是数组的尾部(字符串‘\\0’的结尾就是这个作用)。这个特殊值必须不会作为正常的元素值出现在数组中。
        一维数组比较简单,但是复杂的是二维数组。对于二维数组我们需要至少数组中的一行合适结束,这样指针才能进行自增操作的时候,调转到写一行,什么时候整个数组结束。但是这是比较困难的,我们一半放弃传递二维数组a[i][j],但还可以将其改写为:a[i+x]d的形式,这样就转化为上面的两种解决方案了。
     我们知道,在C语言中无法传递一个普通的多维数组。
    这是因为我们需要知道每一维的长度。一便在地址运行的时候提供正确的长度单位。在C语言中,我们没有办法在实参和形参之间交流这种数据。因此我们必须提供除了最左边意以外的所有长度信息,不然就会出错。
例如调用函数如下:
void my_function2(int array[][3][5]){}
我们可以按照下列的方式调用:
int arr[100][3][5];   my_function(arr); //OK
int arr[10][3][5];    my_function(arr);  //OK
但是像下面的就不行:
int arr[100][33][24]; my_function(arr); //ERROR
int arr[10][4][6];    my_function(arr);    //ERROR
这将无法通过编译器。
那么怎么来传递一个多维数组呢?
方法一:定义void my_function3(int array[3][5])
这样最简单,但是作用也是最小的,因为它只能处理3行4列的数组数据。其实多维数组的最左边的一维长度可以省略,因为函数知道了其他维的信息,它就可以一次跳过一个完整的行,到达下一行。

方法二:void my_function3(int array[][5])。

这也就是方法一中提到的省略最左边第一维的数据。这样的做法表示每一行都必须正好是5个整数的长度。函数也可以类似的声明:

void my_function3(int array(*p)[5])
  其中的括号是必须要要的,确保它是一个指向5个int类型的数组指针,而不是一个5个int *类型元素的指针。
  按照最初的设计,Pascal也具有和C语言同样的功能缺陷--没有办法像同一个函数传递长度不同的数组。事实上,Pascal的情况更糟,因为他甚至不能支持一维数组的情况。而C语言倒是可以。因为数组边界是函数原型的一部分。但是Pascal如果形参和实参的长度不一样,就会产生一个不匹配的错误。例如如下代码:
var arr:array[1..10] of integer;
precedure function(a:array[1..15] of integer;
function(arr);//无法通过编译,但是C语言中可以。

方法三:放弃传递二维数组。也就是说创建一个一维数组,数组中的元素是指向其他东西的指针。

回想一下前面介绍的main函数里,我们已经习惯了char *argv[]参数的形式,有时候我们也能看见,char **argv的形式。所以我们可以简单地传递一个指向数组参数的第一个元素的指针,如下:

void my_function3(int **array);
注意:只有把二维数组改为一个指向向量的指针数组的前提下才可以这样做。
方法四:采用迂回的方法:char array[i*columns+j];

   因为数组的存储都是连续的线性的特点,所以我们可以将多维数组降维处理。因为 C语言是数组的数组,所以我们可以将多维数组降为一维数组,这样我们处理起来就比较方便了!符合我们的习惯!

  毕竟函数中数组的传递总是会被编译器将其翻译为指针,这样我们正好可以利用这一特性,有些时候可以简化多维数组的操作,(并不代表全部)。

以上是关于《C专家编程》:再论指针的主要内容,如果未能解决你的问题,请参考以下文章

《C专家编程》读书笔记

13. 再论C语言中的指针

13. 再论C语言中的指针

13. 再论C语言中的指针

《C专家编程》数组和指针并不同--多维数组

《C专家编程》数组和指针并不同--多维数组