常常能够看到“数组就是指针”、“数组名就是常量指针”这些说法,但真的是这样吗?让咱们先看一下指针和数组的定义。html
1. 指针node
根据C99标准,指针的定义以下:linux
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".
指针是一种派生类型,它描述了这样一个对象,其值为对某种类型实体的引用。它包含了两方面的涵义:实体和类型。ios
须要注意的是,指针变量只是指针的一种形态,但指针并不只仅只有指针变量。shell
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”.
数组类型一样是派生类型,它描述了某种对象的连续的非空集合,由其中元素类型和元素个数来刻画。数组
由指针和数组定义能够看出,指针和数组是彻底不一样的类型。但数组名是指针吗?ide
根据《征服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)的简便写法。实际上,至少对于编译器来讲,[]这样的运算符彻底能够不存在。[]运算符是为了方便人们读写而引入的,是一种语法糖。
注意,认为[]和数组没有关系中的[]指的是表达式中出现的下标运算符,而不是声明中的[]。
请看一小程序:
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:利用二级指针删除单向链表中,做者对利用二级指针的程序还附上了一个比较详细的解说:
“
对于——
1)(第12行)若是不删除当前结点 —— curr保存的是当前结点next指针的地址。
2)(第5行) entry 保存了 *curr —— 这意味着在下一次循环:entry就是prev->next指针所指向的内存。
3)(第8行)删除结点:*curr = entry->next; —— 因而:prev->next 指向了 entry -> next;
”
《征服C指针》
《C和指针》