指针和数组解析
指针和数组
指针和数组是一样的吗?
经常可以看到“数组就是指针”、“数组名就是常量指针”这些说法,但真的是这样吗?让我们先看一下指针和数组的定义。
1. 指针
根据C99标准,指针的定义如下:
A pointer type may be derived from a function type or an object type, called the referenced type. A pointer type describes an object whose value provides a reference to an entity of the referenced type. A pointer type derived from the referenced type T is sometimes called “pointer to T”. The construction of a pointer type from a referenced type is called "pointer type derivation".
指针是一种派生类型,它描述了这样一个对象,其值为对某种类型实体的引用。它包含了两方面的涵义:实体和类型。
需要注意的是,指针变量只是指针的一种形态,但指针并不仅仅只有指针变量。
2. 数组
数组的定义如下:
An array type describes a contiguously allocated nonempty set of objects with a particular member object type, called the element type. Array types are characterized by their element type and by the number of elements in the array. An array type is said to be derived from its element type, and if its element type is T , the array type is sometimes called “array of T”. The construction of an array type from an element type is called “array type derivation”.
数组类型同样是派生类型,它描述了某种对象的连续的非空集合,由其中元素类型和元素个数来刻画。
由指针和数组定义可以看出,指针和数组是完全不同的类型。但数组名是指针吗?
根据《征服C指针》一书,数组名并不是指针,只不过在表达式中,数组名可以解读成“指向它的初始元素的指针”。
在一篇博文第二章 数组名是一个指针常量吗?中,作者就分析的更加透彻:
“
数组名是一个指针常量这种观点来源于数组名在表达式计算中与指针的结果等效性。例如下面的代码:
1 int a[10], *p = a, *q; 2 q = a + 1; 3 q = p + 1;
在效果上看,a + 1与 p + 1是相同的,这很容易给人一种a就是p的假象,但,这仅仅是假象。
在《C与指针》一书中,作者用一个著名的例子阐述了数组名与指针的不同。在一个文件中定义:int a[10]; 然后在另一个文件中声明:extern int *a; 笔者不在这里重复其中的原理,书中的作者试图从底层操作上阐述数组名与指针的不同点,但笔者认为这个例子存在一些不足,a在表达式中会转换为一个非对象的符号地址,而指针a却是一个对象,用一个非对象去跟一个对象比较,有“偷跑”的嫌疑,这个例子只是说明了数组名的非对象性质,只能证明对象与非对象实体在底层操作上的不同,事实上,如上一章所述,指针也有非对象形态。笔者认为,无须从底层的角度上花费那么多唇舌,仅仅从字面上的语义就可以推翻数组名是一个指针的观点。
首先,在C/C++中,数组类型跟指针类型是两种不同的派生类型,数组名跟指针是两种不同类型的实体,把数组类型的实体说成“是”另一个类型的实体,本身就是荒谬的;
其次,a + 1在效果上之所以等同于p + 1,是因为a进行了数组到指针的隐式转换,这是一个转换的过程,是converted to而不是is a的过程。如果是两个相同的事物,又怎会有转换的过程呢?当把a放在a + 1表达式中时,a已经从一个数组名转换为一个指针,a是作为指针而不是数组名参与运算的;
第三,a + 1与p + 1是等效关系,不是等价关系。等价是相同事物的不同表现形式,而等效是不同事物的相同效果。把数组名说成是指针实际上把等效关系误解为等价关系。
因此,数组名不是指针,永远也不是,但在一定条件下,数组名可以转换为指针。
”
而根据《C和指针》一书的第8章8.1节,作者提到:“只有当数组名在表达式中使用时,编译器才会为它产生一个指针常量”。注意这个值是指针常量,而不是指针变量。我们不能修改常量的值。只有在两种场合下,数组名在表达式中不用指针常量来表示——就是当数组名作为sizeof操作符或单目操作符&的操作数时。sizeof返回整个数组的长度,而不是指向数组的指针的长度。取一个数组名的地址所产生的是一个指向数组的指针,而不是一个指向某个指针常量值的指针。
综上所述,数组不是指针,数组名也只有在表达式中才会被当成一个指针常量。
下标运算符[]和数组有关系吗?
对于下标运算符,相信大家都再熟悉不过了,我们可用其方便快速访问数组中的元素。但它和数组有关系吗?先看一个例子:
1 #include <iostream> 2 using namespace std; 3 4 int main() 5 { 6 int p[10]; 7 for (int i = 0; i < 10; i++) 8 p[i] = i + 1; 9 10 cout << "*(p + i)\t" << "p[i]" << endl; 11 for (int i = 0; i < 10; i++) 12 cout << *(p + i) << "\t\t" << p[i] << endl; 13 14 return 0; 15 }
程序的输出如下:
所以,*(p + i)跟p[i]的效果是一样的。根据《征服C指针》,p[i]这种写法只不过是*(p + i)的简便写法。实际上,至少对于编译器来说,[]这样的运算符完全可以不存在。[]运算符是为了方便人们读写而引入的,是一种语法糖。
注意,认为[]和数组没有关系中的[]指的是表达式中出现的下标运算符,而不是声明中的[]。
sizeof对指针和数组求得的不同结果
请看一小程序:
1 #include <iostream> 2 using namespace std; 3 4 void myFunc(int a[]) 5 { 6 cout << sizeof(a) << endl; 7 } 8 9 int main() 10 { 11 int a[]{1, 2, 3, 4}; 12 int *p = a; 13 cout << sizeof(a) << endl; 14 cout << sizeof(p) << endl; 15 myFunc(a); 16 17 return 0; 18 }
在本人机器上的运行结果如下;
很明显,对于数组而言,sizeof的结果是数组所占的所有字节数;而对于指针而言,sizeof的结果是指针类型的大小。为什么呢?因为:
1)对于数组而言,它的大小是固定的和已知的,所以sizeof求到的结果是数组所占的所有字节数(虽然在表达式中数组名被当作指针处理);
2)对于指针而言,我们只能知道它指向的内存的字节大小,而无法知道它知道它指向的连续内存的字节大小(因为不清楚在哪里结束),所以sizeof是无法返回指针所指连续内存的字节大小。在这种情况下,可能返回指针类型的大小可能较好。
另外,当把当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。所以函数myFunc输出结果是4。
二级指针和指针数组
二级指针是指向指针的指针的简称,如下常见例子:
int **p;
而指针数组则是元素类型为指针的数组,如:
int *p[10];
二级指针和指针数组区别与联系
我们知道指针和数组是不一样的,当然二级指针和指针数组也是不一样的。那他们有什么联系呢?
下边请看一个例子:
对于main函数,常见的其中两种写法如下:
int main(int argc, char *argv[]);
或
int main(int argc, char **argv);
根据Linux C编程一站式学习说法,函数原型中的[]
表示指针而不表示数组,等价于char **argv
。那为什么要写成char *argv[]
而不写成char **argv
呢?这样写给读代码的人提供了有用信息,argv
不是指向单个指针,而是指向一个指针数组的首元素。数组中每个元素都是char *
指针,指向一个命令行参数字符串。
其实,就算在表达式中,它们也是等效的。
二级指针和指针数组如何表示二维数组?
在表达式中,二级指针和指针数组是等效的。所以我们下文可以只以二级指针来说明。
对于一个二级指针而言,第一级指针指向一个指针数组的首元素,因此利用下标运算符[]即可获得指针数组的每一个元素;而指针数组每个元素存储的是指针,可以再额外指向另一个数组。这样一来,我们就可以利用二级指针来实现一个二维数组了,如下例:
先看一个简单例子:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 5 int main(int argc, char *argv[]) 6 { 7 int i; 8 for(i = 0; i < argc; i++) 9 { 10 printf("argv[%d]=%s\t", i, argv[i]); 11 int len = strlen(argv[i]), j; 12 for(j = 0; j < len; j++) 13 printf("%s[%d]=%c ", argv[i], j, argv[i][j]); 14 printf("\n"); 15 } 16 return 0; 17 }
在Linux下编译执行:
二级指针妙用:删除单向链表
主要参考:
Linus:利用二级指针删除单向链表(有详细解释,非常值得参考!)
在删除单向链表(保留头指针head)时,我们可能会采用比较典型的做法:
1 typedef struct node 2 { 3 struct node * next; 4 .... 5 } node; 6 7 typedef bool (* remove_fn)(node const * v); 8 9 // Remove all nodes from the supplied list for which the 10 // supplied remove function returns true. 11 // Returns the new head of the list. 12 node * remove_if(node * head, remove_fn rm) 13 { 14 for (node * prev = NULL, * curr = head; curr != NULL; ) 15 { 16 node * const next = curr->next; 17 if (rm(curr)) 18 { 19 if (prev) 20 prev->next = next; 21 else 22 head = next; 23 free(curr); 24 } 25 else 26 prev = curr; 27 curr = next; 28 } 29 return head; 30 }
但Linus大婶就要说这样子写的人了:“This person doesn’t understand pointers”。那对于Linus大婶来说,怎样做才是最好的?那就是利用二级指针,具体如下:
1 void remove_if(node ** head, remove_fn rm) 2 { 3 for (node** curr = head; *curr; ) 4 { 5 node * entry = *curr; 6 if (rm(entry)) 7 { 8 *curr = entry->next; 9 free(entry); 10 } 11 else 12 curr = &entry->next; 13 } 14 }
果然,改写之后的程序简洁了许多,而且也不需要维护一个prev表项指针和考虑头指针的问题。
在博文Linus:利用二级指针删除单向链表中,作者对利用二级指针的程序还附上了一个比较详细的解说:
“
对于——
- 删除节点是表头的情况,输入参数中传入head的二级指针,在for循环里将其初始化curr,然后entry就是*head(*curr),我们马上删除它,那么第8行就等效于*head = (*head)->next,就是删除表头的实现。
- 删除节点不是表头的情况,对于上面的代码,我们可以看到——
1)(第12行)如果不删除当前结点 —— curr保存的是当前结点next指针的地址。
2)(第5行) entry 保存了 *curr —— 这意味着在下一次循环:entry就是prev->next指针所指向的内存。
3)(第8行)删除结点:*curr = entry->next; —— 于是:prev->next 指向了 entry -> next;
”
参考资料
《征服C指针》
《C和指针》